8000 gh-65460: IDLE: add completion of dict keys of type str by taleinat · Pull Request #26039 · python/cpython · GitHub
[go: up one dir, main page]

Skip to content
8000

gh-65460: IDLE: add completion of dict keys of type str #26039

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions Doc/library/idle.rst
Original file line number Diff line number Diff line change
Expand Up @@ -475,10 +475,10 @@ Completions
^^^^^^^^^^^

Completions are supplied, when requested and available, for module
names, attributes of classes or functions, or filenames. Each request
method displays a completion box with existing names. (See tab
completions below for an exception.) For any box, change the name
being completed and the item highlighted in the box by
names, attributes of classes or functions, dict keys or filenames.
Each request method displays a completion box with existing names.
(See tab completions below for an exception.) For any box, change
the name being completed and the item highlighted in the box by
typing and deleting characters; by hitting :kbd:`Up`, :kbd:`Down`,
:kbd:`PageUp`, :kbd:`PageDown`, :kbd:`Home`, and :kbd:`End` keys;
and by a single click within the box. Close the box with :kbd:`Escape`,
Expand All @@ -489,8 +489,8 @@ One way to open a box is to type a key character and wait for a
predefined interval. This defaults to 2 seconds; customize it
in the settings dialog. (To prevent auto popups, set the delay to a
large number of milliseconds, such as 100000000.) For imported module
names or class or function attributes, type '.'.
For filenames in the root directory, type :data:`os.sep` or
names or class or function attributes, type '.'. For dict keys, type
'['. For filenames in the root directory, type :data:`os.sep` or
:data:`os.altsep` immediately after an opening quote. (On Windows,
one can specify a drive first.) Move into subdirectories by typing a
directory name and a separator.
Expand Down
217 changes: 202 additions & 15 deletions Lib/idlelib/autocomplete.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,29 +5,32 @@
"""
import __main__
import keyword
import itertools
import os
import re
import string
import sys

# Two types of completions; defined here for autocomplete_w import below.
ATTRS, FILES = 0, 1
# Three types of completions; defined here for autocomplete_w import below.
ATTRS, FILES, DICTKEYS = 0, 1, 2
from idlelib import autocomplete_w
from idlelib.config import idleConf
from idlelib.hyperparser import HyperParser

# Tuples passed to open_completions.
# EvalFunc, Complete, WantWin, Mode
FORCE = True, False, True, None # Control-Space.
TAB = False, True, True, None # Tab.
TRY_A = False, False, False, ATTRS # '.' for attributes.
TRY_F = False, False, False, FILES # '/' in quotes for file name.
# EvalFunc, Complete, Mode
FORCE = True, False, None # Control-Space.
TAB = False, True, None # Tab.
TRY_A = False, False, ATTRS # '.' for attributes.
TRY_D = False, False, DICTKEYS # '[' for dict keys.
TRY_F = False, False, FILES # '/' in quotes for file name.

# This string includes all chars that may be in an identifier.
# TODO Update this here and elsewhere.
ID_CHARS = string.ascii_letters + string.digits + "_"

SEPS = f"{os.sep}{os.altsep if os.altsep else ''}"
TRIGGERS = f".{SEPS}"
TRIGGERS = f".[{SEPS}"

class AutoComplete:

Expand All @@ -43,6 +46,8 @@ def __init__(self, editwin=None, tags=None):
self._delayed_completion_id = None
self._delayed_completion_index = None

self._make_dict_key_reprs = self._make_dict_key_repr_func()

@classmethod
def reload(cls):
cls.popupwait = idleConf.GetOption(
Expand Down Expand Up @@ -76,10 +81,10 @@ def autocomplete_event(self, event):
return "break" if opened else None

def try_open_completions_event(self, event=None):
"(./) Open completion list after pause with no movement."
r"""(./[) Open completion list after pause with no movement."""
lastchar = self.text.get("insert-1c")
if lastchar in TRIGGERS:
args = TRY_A if lastchar == "." else TRY_F
args = {".": TRY_A, "[": TRY_D}.get(lastchar, TRY_F)
self._delayed_completion_index = self.text.index("insert")
if self._delayed_completion_id is not None:
self.text.after_cancel(self._delayed_completion_id)
Expand All @@ -92,14 +97,162 @@ def _delayed_open_completions(self, args):
if self.text.index("insert") == self._delayed_completion_index:
self.open_completions(args)

_dict_str_key_prefix_re = re.compile(r'\s*(fr|rf|f|r|u|)', re.I)

@classmethod
def _is_completing_dict_key(cls, hyper_parser):
hp = hyper_parser
if hp.is_in_string():
# hp.indexbracket is an opening quote;
# check if the bracket before it is '['.
if hp.indexbracket == 0:
return None
opening_bracket_idx = hp.bracketing[hp.indexbracket - 1][0]
if hp.rawtext[opening_bracket_idx] != '[':
return None
# The completion start is everything from the '[', except initial
# whitespace. Check that it's a valid beginning of a str/bytes
# literal.
opening_quote_idx = hp.bracketing[hp.indexbracket][0]
m = cls._dict_str_key_prefix_re.fullmatch(
hp.rawtext[opening_bracket_idx+1:opening_quote_idx])
if m is None:
return None
comp_start = \
m.group(1) + hp.rawtext[opening_quote_idx:hp.indexinrawtext]
elif hp.is_in_code():
# This checks the the last bracket is an opener,
# and also avoids an edge-case exception.
if not hp.isopener[hp.indexbracket]:
return None
# Check if the last opening bracket a '['.
opening_bracket_idx = hp.bracketing[hp.indexbracket][0]
if hp.rawtext[opening_bracket_idx] != '[':
return None
# We want to complete str/bytes dict keys only if the existing
# completion start could be the beginning of a literal. Note
# that an empty or all-whitespace completion start is acceptable.
m = cls._dict_str_key_prefix_re.fullmatch(
hp.rawtext[opening_bracket_idx + 1:hp.indexinrawtext])
if m is None:
return None
comp_start = m.group(1)
else:
return None

hp.set_index("insert-%dc" % (hp.indexinrawtext - opening_bracket_idx))
comp_what = hp.get_expression()
return comp_what, comp_start

@classmethod
def _make_dict_key_repr_func(cls):
# Prepare escaping utilities.
escape_re = re.compile(r'[\0-\15]')
escapes = {ord_: f'\\x{ord_:02x}' for ord_ in range(14)}
escapes.update({0: '\\0', 9: '\\t', 10: '\\n', 13: '\\r'})
def control_char_escape(match):
"""escaping for ASCII control characters 0-13"""
# This is needed to avoid potential visual artifacts displaying
# these characters, and even more importantly to be clear to users.
#
# The characters '\0', '\t', '\n' and '\r' are displayed as done
# here rather than as hex escape codes, which is nicer and clearer.
return escapes[ord(match.group())]
if sys.maxunicode >= 0x10000:
non_bmp_re = re.compile(f'[\\U00010000-\\U{sys.maxunicode:08x}]')
else:
non_bmp_re = re.compile(r'(?!)') # never matches anything
def non_bmp_sub(match):
"""escaping for non-BMP unicode code points"""
# This is needed since Tk doesn't support displaying non-BMP
# unicode code points.
return f'\\U{ord(match.group()):08x}'

def str_reprs(str_comps, prefix, is_raw, quote_type):
repr_prefix = prefix.replace('b', '').replace('B', '')
repr_template = f"{repr_prefix}{quote_type}{{}}{quote_type}"
if not is_raw:
# Escape back-slashes.
str_comps = [x.replace('\\', '\\\\') for x in str_comps]
# Escape quotes.
str_comps = [x.replace(quote_type, '\\' + quote_type) for x in
str_comps]
if len(quote_type) == 3:
# With triple-quotes, we need to escape the final character if
# it is a single quote of the same kind.
str_comps = [
(x[:-1] + '\\' + x[-1]) if x[-1:] == quote_type[0] else x
for x in str_comps
]
if not is_raw:
# Escape control characters.
str_comps = [escape_re.sub(control_char_escape, x)
for x in str_comps]
str_comps = [non_bmp_re.sub(non_bmp_sub, x) for x in str_comps]
str_comps = [repr_template.format(x) for x in str_comps]
else:
# Format as raw literals (r"...") except when there are control
# characters which must be escaped.
non_raw_prefix = repr_prefix.replace('r', '').replace('R', '')
non_raw_template = \
f"{non_raw_prefix}{quote_type}{{}}{quote_type}"
str_comps = [
repr_template.format(x)
if escape_re.search(x) is None and non_bmp_re.search(
x) is None
else non_raw_template.format(
non_bmp_re.sub(non_bmp_sub,
escape_re.sub(control_char_escape,
x)))
for x in str_comps
]
return str_comps

def analyze_prefix_and_quote_type(comp_start):
"""Analyze existing prefix and quote type of a completion start"""
is_raw = False
quote_type = '"'
prefix_match = cls._dict_str_key_prefix_re.match(comp_start)
if prefix_match is not None:
prefix = prefix_match.group(1)
post_prefix = comp_start[prefix_match.end():]

is_raw = 'r' in prefix.lower()
if post_prefix:
quote_type_candidate = post_prefix[0]
if quote_type_candidate not in "'\"":
pass # use the default quote type
elif post_prefix[1:2] == quote_type_candidate:
quote_type = quote_type_candidate * 3
else:
quote_type = quote_type_candidate
else:
prefix = ''

return prefix, is_raw, quote_type

def _dict_key_reprs(comp_start, comp_list):
"""Customized repr() for str.

This retains the prefix and quote type of the completed partial
str literal, if they exist.
"""
prefix, is_raw, quote_type = \
analyze_prefix_and_quote_type(comp_start)

str_comps = [c for c in comp_list if type(c) == str]
return str_reprs(str_comps, prefix, is_raw, quote_type)

return _dict_key_reprs

def open_completions(self, args):
"""Find the completions and create the AutoCompleteWindow.
Return True if successful (no syntax error or so found).
If complete is True, then if there's nothing to complete and no
start of completion, won't open completions and return False.
If mode is given, will open a completion list only in this mode.
"""
evalfuncs, complete, wantwin, mode = args
evalfuncs, complete, mode = args
# Cancel another delayed call, if it exists.
if self._delayed_completion_id is not None:
self.text.after_cancel(self._delayed_completion_id)
Expand All @@ -108,7 +261,22 @@ def open_completions(self, args):
hp = HyperParser(self.editwin, "insert")
curline = self.text.get("insert linestart", "insert")
i = j = len(curline)
if hp.is_in_string() and (not mode or mode==FILES):

comp_lists = None
if mode in (None, DICTKEYS):
comp_what_and_start = self._is_completing_dict_key(hp)
if comp_what_and_start is not None:
comp_what, comp_start = comp_what_and_start
if comp_what and (evalfuncs or '(' not in comp_what):
comp_lists = self.fetch_completions(comp_what, DICTKEYS)
if comp_lists[0]:
self._remove_autocomplete_window()
mode = DICTKEYS
else:
comp_lists = None
if mode == DICTKEYS and comp_lists is None:
return None
if mode in (None, FILES) and hp.is_in_string():
# Find the beginning of the string.
# fetch_completions will look at the file system to determine
# whether the string value constitutes an actual file name
Expand All @@ -125,7 +293,7 @@ def open_completions(self, args):
while i and curline[i-1] not in "'\"":
i -= 1
comp_what = curline[i:j]
elif hp.is_in_code() and (not mode or mode==ATTRS):
elif mode in (None, ATTRS) and hp.is_in_code():
self._remove_autocomplete_window()
mode = ATTRS
while i and (curline[i-1] in ID_CHARS or ord(curline[i-1]) > 127):
Expand All @@ -139,18 +307,28 @@ def open_completions(self, args):
return None
else:
comp_what = ""
elif mode == DICTKEYS:
pass
else:
return None

if complete and not comp_what and not comp_start:
return None
comp_lists = self.fetch_completions(comp_what, mode)
if comp_lists is None:
comp_lists = self.fetch_completions(comp_what, mode)

if mode == DICTKEYS:
assert comp_lists[0] == comp_lists[1]
reprs = self._make_dict_key_reprs(comp_start, comp_lists[0])
reprs.sort()
comp_lists = (reprs, list(reprs))

if not comp_lists[0]:
return None
self.autocompletewindow = self._make_autocomplete_window()
return not self.autocompletewindow.show_window(
comp_lists, "insert-%dc" % len(comp_start),
complete, mode, wantwin)
complete, mode)

def fetch_completions(self, what, mode):
"""Return a pair of lists of completions for something. The first list
Expand Down Expand Up @@ -208,6 +386,15 @@ def fetch_completions(self, what, mode):
except OSError:
return [], []

elif mode == DICTKEYS:
try:
entity = self.get_entity(what)
keys = entity.keys() if isinstance(entity, dict) else []
except:
return [], []
smalll = bigl = [k for k in keys if type(k) == str]
smalll.sort()

if not smalll:
smalll = bigl
return smalll, bigl
Expand Down
Loading
0