Author: Matti Picus <matti.pi...@gmail.com> Branch: py3.6 Changeset: r96083:c51e9fb1f2f9 Date: 2019-02-19 09:41 +0200 http://bitbucket.org/pypy/pypy/changeset/c51e9fb1f2f9/
Log: redo 9387f96a5518 in two steps since file case changed (windows ...) diff too long, truncating to 2000 out of 19492 lines diff --git a/lib-python/3/idlelib/__init__.py b/lib-python/3/idlelib/__init__.py new file mode 100644 --- /dev/null +++ b/lib-python/3/idlelib/__init__.py @@ -0,0 +1,10 @@ +"""The idlelib package implements the Idle application. + +Idle includes an interactive shell and editor. +Starting with Python 3.6, IDLE requires tcl/tk 8.5 or later. +Use the files named idle.* to start Idle. + +The other files are private implementations. Their details are subject to +change. See PEP 434 for more. Import them at your own risk. +""" +testing = False # Set True by test.test_idle. diff --git a/lib-python/3/idlelib/__main__.py b/lib-python/3/idlelib/__main__.py new file mode 100644 --- /dev/null +++ b/lib-python/3/idlelib/__main__.py @@ -0,0 +1,8 @@ +""" +IDLE main entry point + +Run IDLE as python -m idlelib +""" +import idlelib.pyshell +idlelib.pyshell.main() +# This file does not work for 2.7; See issue 24212. diff --git a/lib-python/3/idlelib/_pyclbr.py b/lib-python/3/idlelib/_pyclbr.py new file mode 100644 --- /dev/null +++ b/lib-python/3/idlelib/_pyclbr.py @@ -0,0 +1,402 @@ +# A private copy of 3.7.0a1 pyclbr for use by idlelib.browser +"""Parse a Python module and describe its classes and functions. + +Parse enough of a Python file to recognize imports and class and +function definitions, and to find out the superclasses of a class. + +The interface consists of a single function: + readmodule_ex(module, path=None) +where module is the name of a Python module, and path is an optional +list of directories where the module is to be searched. If present, +path is prepended to the system search path sys.path. The return value +is a dictionary. The keys of the dictionary are the names of the +classes and functions defined in the module (including classes that are +defined via the from XXX import YYY construct). The values are +instances of classes Class and Function. One special key/value pair is +present for packages: the key '__path__' has a list as its value which +contains the package search path. + +Classes and Functions have a common superclass: _Object. Every instance +has the following attributes: + module -- name of the module; + name -- name of the object; + file -- file in which the object is defined; + lineno -- line in the file where the object's definition starts; + parent -- parent of this object, if any; + children -- nested objects contained in this object. +The 'children' attribute is a dictionary mapping names to objects. + +Instances of Function describe functions with the attributes from _Object. + +Instances of Class describe classes with the attributes from _Object, +plus the following: + super -- list of super classes (Class instances if possible); + methods -- mapping of method names to beginning line numbers. +If the name of a super class is not recognized, the corresponding +entry in the list of super classes is not a class instance but a +string giving the name of the super class. Since import statements +are recognized and imported modules are scanned as well, this +shouldn't happen often. +""" + +import io +import sys +import importlib.util +import tokenize +from token import NAME, DEDENT, OP + +__all__ = ["readmodule", "readmodule_ex", "Class", "Function"] + +_modules = {} # Initialize cache of modules we've seen. + + +class _Object: + "Informaton about Python class or function." + def __init__(self, module, name, file, lineno, parent): + self.module = module + self.name = name + self.file = file + self.lineno = lineno + self.parent = parent + self.children = {} + + def _addchild(self, name, obj): + self.children[name] = obj + + +class Function(_Object): + "Information about a Python function, including methods." + def __init__(self, module, name, file, lineno, parent=None): + _Object.__init__(self, module, name, file, lineno, parent) + + +class Class(_Object): + "Information about a Python class." + def __init__(self, module, name, super, file, lineno, parent=None): + _Object.__init__(self, module, name, file, lineno, parent) + self.super = [] if super is None else super + self.methods = {} + + def _addmethod(self, name, lineno): + self.methods[name] = lineno + + +def _nest_function(ob, func_name, lineno): + "Return a Function after nesting within ob." + newfunc = Function(ob.module, func_name, ob.file, lineno, ob) + ob._addchild(func_name, newfunc) + if isinstance(ob, Class): + ob._addmethod(func_name, lineno) + return newfunc + +def _nest_class(ob, class_name, lineno, super=None): + "Return a Class after nesting within ob." + newclass = Class(ob.module, class_name, super, ob.file, lineno, ob) + ob._addchild(class_name, newclass) + return newclass + +def readmodule(module, path=None): + """Return Class objects for the top-level classes in module. + + This is the original interface, before Functions were added. + """ + + res = {} + for key, value in _readmodule(module, path or []).items(): + if isinstance(value, Class): + res[key] = value + return res + +def readmodule_ex(module, path=None): + """Return a dictionary with all functions and classes in module. + + Search for module in PATH + sys.path. + If possible, include imported superclasses. + Do this by reading source, without importing (and executing) it. + """ + return _readmodule(module, path or []) + +def _readmodule(module, path, inpackage=None): + """Do the hard work for readmodule[_ex]. + + If inpackage is given, it must be the dotted name of the package in + which we are searching for a submodule, and then PATH must be the + package search path; otherwise, we are searching for a top-level + module, and path is combined with sys.path. + """ + # Compute the full module name (prepending inpackage if set). + if inpackage is not None: + fullmodule = "%s.%s" % (inpackage, module) + else: + fullmodule = module + + # Check in the cache. + if fullmodule in _modules: + return _modules[fullmodule] + + # Initialize the dict for this module's contents. + tree = {} + + # Check if it is a built-in module; we don't do much for these. + if module in sys.builtin_module_names and inpackage is None: + _modules[module] = tree + return tree + + # Check for a dotted module name. + i = module.rfind('.') + if i >= 0: + package = module[:i] + submodule = module[i+1:] + parent = _readmodule(package, path, inpackage) + if inpackage is not None: + package = "%s.%s" % (inpackage, package) + if not '__path__' in parent: + raise ImportError('No package named {}'.format(package)) + return _readmodule(submodule, parent['__path__'], package) + + # Search the path for the module. + f = None + if inpackage is not None: + search_path = path + else: + search_path = path + sys.path + spec = importlib.util._find_spec_from_path(fullmodule, search_path) + _modules[fullmodule] = tree + # Is module a package? + if spec.submodule_search_locations is not None: + tree['__path__'] = spec.submodule_search_locations + try: + source = spec.loader.get_source(fullmodule) + if source is None: + return tree + except (AttributeError, ImportError): + # If module is not Python source, we cannot do anything. + return tree + + fname = spec.loader.get_filename(fullmodule) + return _create_tree(fullmodule, path, fname, source, tree, inpackage) + + +def _create_tree(fullmodule, path, fname, source, tree, inpackage): + """Return the tree for a particular module. + + fullmodule (full module name), inpackage+module, becomes o.module. + path is passed to recursive calls of _readmodule. + fname becomes o.file. + source is tokenized. Imports cause recursive calls to _readmodule. + tree is {} or {'__path__': <submodule search locations>}. + inpackage, None or string, is passed to recursive calls of _readmodule. + + The effect of recursive calls is mutation of global _modules. + """ + f = io.StringIO(source) + + stack = [] # Initialize stack of (class, indent) pairs. + + g = tokenize.generate_tokens(f.readline) + try: + for tokentype, token, start, _end, _line in g: + if tokentype == DEDENT: + lineno, thisindent = start + # Close previous nested classes and defs. + while stack and stack[-1][1] >= thisindent: + del stack[-1] + elif token == 'def': + lineno, thisindent = start + # Close previous nested classes and defs. + while stack and stack[-1][1] >= thisindent: + del stack[-1] + tokentype, func_name, start = next(g)[0:3] + if tokentype != NAME: + continue # Skip def with syntax error. + cur_func = None + if stack: + cur_obj = stack[-1][0] + cur_func = _nest_function(cur_obj, func_name, lineno) + else: + # It is just a function. + cur_func = Function(fullmodule, func_name, fname, lineno) + tree[func_name] = cur_func + stack.append((cur_func, thisindent)) + elif token == 'class': + lineno, thisindent = start + # Close previous nested classes and defs. + while stack and stack[-1][1] >= thisindent: + del stack[-1] + tokentype, class_name, start = next(g)[0:3] + if tokentype != NAME: + continue # Skip class with syntax error. + # Parse what follows the class name. + tokentype, token, start = next(g)[0:3] + inherit = None + if token == '(': + names = [] # Initialize list of superclasses. + level = 1 + super = [] # Tokens making up current superclass. + while True: + tokentype, token, start = next(g)[0:3] + if token in (')', ',') and level == 1: + n = "".join(super) + if n in tree: + # We know this super class. + n = tree[n] + else: + c = n.split('.') + if len(c) > 1: + # Super class form is module.class: + # look in module for class. + m = c[-2] + c = c[-1] + if m in _modules: + d = _modules[m] + if c in d: + n = d[c] + names.append(n) + super = [] + if token == '(': + level += 1 + elif token == ')': + level -= 1 + if level == 0: + break + elif token == ',' and level == 1: + pass + # Only use NAME and OP (== dot) tokens for type name. + elif tokentype in (NAME, OP) and level == 1: + super.append(token) + # Expressions in the base list are not supported. + inherit = names + if stack: + cur_obj = stack[-1][0] + cur_class = _nest_class( + cur_obj, class_name, lineno, inherit) + else: + cur_class = Class(fullmodule, class_name, inherit, + fname, lineno) + tree[class_name] = cur_class + stack.append((cur_class, thisindent)) + elif token == 'import' and start[1] == 0: + modules = _getnamelist(g) + for mod, _mod2 in modules: + try: + # Recursively read the imported module. + if inpackage is None: + _readmodule(mod, path) + else: + try: + _readmodule(mod, path, inpackage) + except ImportError: + _readmodule(mod, []) + except: + # If we can't find or parse the imported module, + # too bad -- don't die here. + pass + elif token == 'from' and start[1] == 0: + mod, token = _getname(g) + if not mod or token != "import": + continue + names = _getnamelist(g) + try: + # Recursively read the imported module. + d = _readmodule(mod, path, inpackage) + except: + # If we can't find or parse the imported module, + # too bad -- don't die here. + continue + # Add any classes that were defined in the imported module + # to our name space if they were mentioned in the list. + for n, n2 in names: + if n in d: + tree[n2 or n] = d[n] + elif n == '*': + # Don't add names that start with _. + for n in d: + if n[0] != '_': + tree[n] = d[n] + except StopIteration: + pass + + f.close() + return tree + + +def _getnamelist(g): + """Return list of (dotted-name, as-name or None) tuples for token source g. + + An as-name is the name that follows 'as' in an as clause. + """ + names = [] + while True: + name, token = _getname(g) + if not name: + break + if token == 'as': + name2, token = _getname(g) + else: + name2 = None + names.append((name, name2)) + while token != "," and "\n" not in token: + token = next(g)[1] + if token != ",": + break + return names + + +def _getname(g): + "Return (dotted-name or None, next-token) tuple for token source g." + parts = [] + tokentype, token = next(g)[0:2] + if tokentype != NAME and token != '*': + return (None, token) + parts.append(token) + while True: + tokentype, token = next(g)[0:2] + if token != '.': + break + tokentype, token = next(g)[0:2] + if tokentype != NAME: + break + parts.append(token) + return (".".join(parts), token) + + +def _main(): + "Print module output (default this file) for quick visual check." + import os + try: + mod = sys.argv[1] + except: + mod = __file__ + if os.path.exists(mod): + path = [os.path.dirname(mod)] + mod = os.path.basename(mod) + if mod.lower().endswith(".py"): + mod = mod[:-3] + else: + path = [] + tree = readmodule_ex(mod, path) + lineno_key = lambda a: getattr(a, 'lineno', 0) + objs = sorted(tree.values(), key=lineno_key, reverse=True) + indent_level = 2 + while objs: + obj = objs.pop() + if isinstance(obj, list): + # Value is a __path__ key. + continue + if not hasattr(obj, 'indent'): + obj.indent = 0 + + if isinstance(obj, _Object): + new_objs = sorted(obj.children.values(), + key=lineno_key, reverse=True) + for ob in new_objs: + ob.indent = obj.indent + indent_level + objs.extend(new_objs) + if isinstance(obj, Class): + print("{}class {} {} {}" + .format(' ' * obj.indent, obj.name, obj.super, obj.lineno)) + elif isinstance(obj, Function): + print("{}def {} {}".format(' ' * obj.indent, obj.name, obj.lineno)) + +if __name__ == "__main__": + _main() diff --git a/lib-python/3/idlelib/autocomplete.py b/lib-python/3/idlelib/autocomplete.py new file mode 100644 --- /dev/null +++ b/lib-python/3/idlelib/autocomplete.py @@ -0,0 +1,231 @@ +"""Complete either attribute names or file names. + +Either on demand or after a user-selected delay after a key character, +pop up a list of candidates. +""" +import os +import string +import sys + +# These constants represent the two different types of completions. +# They must be defined here so autocomple_w can import them. +COMPLETE_ATTRIBUTES, COMPLETE_FILES = range(1, 2+1) + +from idlelib import autocomplete_w +from idlelib.config import idleConf +from idlelib.hyperparser import HyperParser +import __main__ + +# 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 = os.sep +if os.altsep: # e.g. '/' on Windows... + SEPS += os.altsep + + +class AutoComplete: + + def __init__(self, editwin=None): + self.editwin = editwin + if editwin is not None: # not in subprocess or test + self.text = editwin.text + self.autocompletewindow = None + # id of delayed call, and the index of the text insert when + # the delayed call was issued. If _delayed_completion_id is + # None, there is no delayed call. + self._delayed_completion_id = None + self._delayed_completion_index = None + + @classmethod + def reload(cls): + cls.popupwait = idleConf.GetOption( + "extensions", "AutoComplete", "popupwait", type="int", default=0) + + def _make_autocomplete_window(self): + return autocomplete_w.AutoCompleteWindow(self.text) + + def _remove_autocomplete_window(self, event=None): + if self.autocompletewindow: + self.autocompletewindow.hide_window() + self.autocompletewindow = None + + def force_open_completions_event(self, event): + """Happens when the user really wants to open a completion list, even + if a function call is needed. + """ + self.open_completions(True, False, True) + return "break" + + def try_open_completions_event(self, event): + """Happens when it would be nice to open a completion list, but not + really necessary, for example after a dot, so function + calls won't be made. + """ + lastchar = self.text.get("insert-1c") + if lastchar == ".": + self._open_completions_later(False, False, False, + COMPLETE_ATTRIBUTES) + elif lastchar in SEPS: + self._open_completions_later(False, False, False, + COMPLETE_FILES) + + def autocomplete_event(self, event): + """Happens when the user wants to complete his word, and if necessary, + open a completion list after that (if there is more than one + completion) + """ + if hasattr(event, "mc_state") and event.mc_state or\ + not self.text.get("insert linestart", "insert").strip(): + # A modifier was pressed along with the tab or + # there is only previous whitespace on this line, so tab. + return None + if self.autocompletewindow and self.autocompletewindow.is_active(): + self.autocompletewindow.complete() + return "break" + else: + opened = self.open_completions(False, True, True) + return "break" if opened else None + + def _open_completions_later(self, *args): + self._delayed_completion_index = self.text.index("insert") + if self._delayed_completion_id is not None: + self.text.after_cancel(self._delayed_completion_id) + self._delayed_completion_id = \ + self.text.after(self.popupwait, self._delayed_open_completions, + *args) + + def _delayed_open_completions(self, *args): + self._delayed_completion_id = None + if self.text.index("insert") == self._delayed_completion_index: + self.open_completions(*args) + + def open_completions(self, evalfuncs, complete, userWantsWin, mode=None): + """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. + """ + # Cancel another delayed call, if it exists. + if self._delayed_completion_id is not None: + self.text.after_cancel(self._delayed_completion_id) + self._delayed_completion_id = None + + 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==COMPLETE_FILES): + # 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 + # XXX could consider raw strings here and unescape the string value if it's + # not raw. + self._remove_autocomplete_window() + mode = COMPLETE_FILES + # Find last separator or string start + while i and curline[i-1] not in "'\"" + SEPS: + i -= 1 + comp_start = curline[i:j] + j = i + # Find string start + 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==COMPLETE_ATTRIBUTES): + self._remove_autocomplete_window() + mode = COMPLETE_ATTRIBUTES + while i and (curline[i-1] in ID_CHARS or ord(curline[i-1]) > 127): + i -= 1 + comp_start = curline[i:j] + if i and curline[i-1] == '.': + hp.set_index("insert-%dc" % (len(curline)-(i-1))) + comp_what = hp.get_expression() + if not comp_what or \ + (not evalfuncs and comp_what.find('(') != -1): + return None + else: + comp_what = "" + else: + return None + + if complete and not comp_what and not comp_start: + return None + comp_lists = self.fetch_completions(comp_what, mode) + 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, userWantsWin) + + def fetch_completions(self, what, mode): + """Return a pair of lists of completions for something. The first list + is a sublist of the second. Both are sorted. + + If there is a Python subprocess, get the comp. list there. Otherwise, + either fetch_completions() is running in the subprocess itself or it + was called in an IDLE EditorWindow before any script had been run. + + The subprocess environment is that of the most recently run script. If + two unrelated modules are being edited some calltips in the current + module may be inoperative if the module was not the last to run. + """ + try: + rpcclt = self.editwin.flist.pyshell.interp.rpcclt + except: + rpcclt = None + if rpcclt: + return rpcclt.remotecall("exec", "get_the_completion_list", + (what, mode), {}) + else: + if mode == COMPLETE_ATTRIBUTES: + if what == "": + namespace = __main__.__dict__.copy() + namespace.update(__main__.__builtins__.__dict__) + bigl = eval("dir()", namespace) + bigl.sort() + if "__all__" in bigl: + smalll = sorted(eval("__all__", namespace)) + else: + smalll = [s for s in bigl if s[:1] != '_'] + else: + try: + entity = self.get_entity(what) + bigl = dir(entity) + bigl.sort() + if "__all__" in bigl: + smalll = sorted(entity.__all__) + else: + smalll = [s for s in bigl if s[:1] != '_'] + except: + return [], [] + + elif mode == COMPLETE_FILES: + if what == "": + what = "." + try: + expandedpath = os.path.expanduser(what) + bigl = os.listdir(expandedpath) + bigl.sort() + smalll = [s for s in bigl if s[:1] != '.'] + except OSError: + return [], [] + + if not smalll: + smalll = bigl + return smalll, bigl + + def get_entity(self, name): + """Lookup name in a namespace spanning sys.modules and __main.dict__""" + namespace = sys.modules.copy() + namespace.update(__main__.__dict__) + return eval(name, namespace) + + +AutoComplete.reload() + +if __name__ == '__main__': + from unittest import main + main('idlelib.idle_test.test_autocomplete', verbosity=2) diff --git a/lib-python/3/idlelib/autocomplete_w.py b/lib-python/3/idlelib/autocomplete_w.py new file mode 100644 --- /dev/null +++ b/lib-python/3/idlelib/autocomplete_w.py @@ -0,0 +1,467 @@ +""" +An auto-completion window for IDLE, used by the autocomplete extension +""" +import platform + +from tkinter import * +from tkinter.ttk import Scrollbar + +from idlelib.autocomplete import COMPLETE_FILES, COMPLETE_ATTRIBUTES +from idlelib.multicall import MC_SHIFT + +HIDE_VIRTUAL_EVENT_NAME = "<<autocompletewindow-hide>>" +HIDE_FOCUS_OUT_SEQUENCE = "<FocusOut>" +HIDE_SEQUENCES = (HIDE_FOCUS_OUT_SEQUENCE, "<ButtonPress>") +KEYPRESS_VIRTUAL_EVENT_NAME = "<<autocompletewindow-keypress>>" +# We need to bind event beyond <Key> so that the function will be called +# before the default specific IDLE function +KEYPRESS_SEQUENCES = ("<Key>", "<Key-BackSpace>", "<Key-Return>", "<Key-Tab>", + "<Key-Up>", "<Key-Down>", "<Key-Home>", "<Key-End>", + "<Key-Prior>", "<Key-Next>") +KEYRELEASE_VIRTUAL_EVENT_NAME = "<<autocompletewindow-keyrelease>>" +KEYRELEASE_SEQUENCE = "<KeyRelease>" +LISTUPDATE_SEQUENCE = "<B1-ButtonRelease>" +WINCONFIG_SEQUENCE = "<Configure>" +DOUBLECLICK_SEQUENCE = "<B1-Double-ButtonRelease>" + +class AutoCompleteWindow: + + def __init__(self, widget): + # The widget (Text) on which we place the AutoCompleteWindow + self.widget = widget + # The widgets we create + self.autocompletewindow = self.listbox = self.scrollbar = None + # The default foreground and background of a selection. Saved because + # they are changed to the regular colors of list items when the + # completion start is not a prefix of the selected completion + self.origselforeground = self.origselbackground = None + # The list of completions + self.completions = None + # A list with more completions, or None + self.morecompletions = None + # The completion mode. Either autocomplete.COMPLETE_ATTRIBUTES or + # autocomplete.COMPLETE_FILES + self.mode = None + # The current completion start, on the text box (a string) + self.start = None + # The index of the start of the completion + self.startindex = None + # The last typed start, used so that when the selection changes, + # the new start will be as close as possible to the last typed one. + self.lasttypedstart = None + # Do we have an indication that the user wants the completion window + # (for example, he clicked the list) + self.userwantswindow = None + # event ids + self.hideid = self.keypressid = self.listupdateid = self.winconfigid \ + = self.keyreleaseid = self.doubleclickid = None + # Flag set if last keypress was a tab + self.lastkey_was_tab = False + + def _change_start(self, newstart): + min_len = min(len(self.start), len(newstart)) + i = 0 + while i < min_len and self.start[i] == newstart[i]: + i += 1 + if i < len(self.start): + self.widget.delete("%s+%dc" % (self.startindex, i), + "%s+%dc" % (self.startindex, len(self.start))) + if i < len(newstart): + self.widget.insert("%s+%dc" % (self.startindex, i), + newstart[i:]) + self.start = newstart + + def _binary_search(self, s): + """Find the first index in self.completions where completions[i] is + greater or equal to s, or the last index if there is no such + one.""" + i = 0; j = len(self.completions) + while j > i: + m = (i + j) // 2 + if self.completions[m] >= s: + j = m + else: + i = m + 1 + return min(i, len(self.completions)-1) + + def _complete_string(self, s): + """Assuming that s is the prefix of a string in self.completions, + return the longest string which is a prefix of all the strings which + s is a prefix of them. If s is not a prefix of a string, return s.""" + first = self._binary_search(s) + if self.completions[first][:len(s)] != s: + # There is not even one completion which s is a prefix of. + return s + # Find the end of the range of completions where s is a prefix of. + i = first + 1 + j = len(self.completions) + while j > i: + m = (i + j) // 2 + if self.completions[m][:len(s)] != s: + j = m + else: + i = m + 1 + last = i-1 + + if first == last: # only one possible completion + return self.completions[first] + + # We should return the maximum prefix of first and last + first_comp = self.completions[first] + last_comp = self.completions[last] + min_len = min(len(first_comp), len(last_comp)) + i = len(s) + while i < min_len and first_comp[i] == last_comp[i]: + i += 1 + return first_comp[:i] + + def _selection_changed(self): + """Should be called when the selection of the Listbox has changed. + Updates the Listbox display and calls _change_start.""" + cursel = int(self.listbox.curselection()[0]) + + self.listbox.see(cursel) + + lts = self.lasttypedstart + selstart = self.completions[cursel] + if self._binary_search(lts) == cursel: + newstart = lts + else: + min_len = min(len(lts), len(selstart)) + i = 0 + while i < min_len and lts[i] == selstart[i]: + i += 1 + newstart = selstart[:i] + self._change_start(newstart) + + if self.completions[cursel][:len(self.start)] == self.start: + # start is a prefix of the selected completion + self.listbox.configure(selectbackground=self.origselbackground, + selectforeground=self.origselforeground) + else: + self.listbox.configure(selectbackground=self.listbox.cget("bg"), + selectforeground=self.listbox.cget("fg")) + # If there are more completions, show them, and call me again. + if self.morecompletions: + self.completions = self.morecompletions + self.morecompletions = None + self.listbox.delete(0, END) + for item in self.completions: + self.listbox.insert(END, item) + self.listbox.select_set(self._binary_search(self.start)) + self._selection_changed() + + def show_window(self, comp_lists, index, complete, mode, userWantsWin): + """Show the autocomplete list, bind events. + If complete is True, complete the text, and if there is exactly one + matching completion, don't open a list.""" + # Handle the start we already have + self.completions, self.morecompletions = comp_lists + self.mode = mode + self.startindex = self.widget.index(index) + self.start = self.widget.get(self.startindex, "insert") + if complete: + completed = self._complete_string(self.start) + start = self.start + self._change_start(completed) + i = self._binary_search(completed) + if self.completions[i] == completed and \ + (i == len(self.completions)-1 or + self.completions[i+1][:len(completed)] != completed): + # There is exactly one matching completion + return completed == start + self.userwantswindow = userWantsWin + self.lasttypedstart = self.start + + # Put widgets in place + self.autocompletewindow = acw = Toplevel(self.widget) + # Put it in a position so that it is not seen. + acw.wm_geometry("+10000+10000") + # Make it float + acw.wm_overrideredirect(1) + try: + # This command is only needed and available on Tk >= 8.4.0 for OSX + # Without it, call tips intrude on the typing process by grabbing + # the focus. + acw.tk.call("::tk::unsupported::MacWindowStyle", "style", acw._w, + "help", "noActivates") + except TclError: + pass + self.scrollbar = scrollbar = Scrollbar(acw, orient=VERTICAL) + self.listbox = listbox = Listbox(acw, yscrollcommand=scrollbar.set, + exportselection=False, bg="white") + for item in self.completions: + listbox.insert(END, item) + self.origselforeground = listbox.cget("selectforeground") + self.origselbackground = listbox.cget("selectbackground") + scrollbar.config(command=listbox.yview) + scrollbar.pack(side=RIGHT, fill=Y) + listbox.pack(side=LEFT, fill=BOTH, expand=True) + acw.lift() # work around bug in Tk 8.5.18+ (issue #24570) + + # Initialize the listbox selection + self.listbox.select_set(self._binary_search(self.start)) + self._selection_changed() + + # bind events + self.hideaid = acw.bind(HIDE_VIRTUAL_EVENT_NAME, self.hide_event) + self.hidewid = self.widget.bind(HIDE_VIRTUAL_EVENT_NAME, self.hide_event) + acw.event_add(HIDE_VIRTUAL_EVENT_NAME, HIDE_FOCUS_OUT_SEQUENCE) + for seq in HIDE_SEQUENCES: + self.widget.event_add(HIDE_VIRTUAL_EVENT_NAME, seq) + + self.keypressid = self.widget.bind(KEYPRESS_VIRTUAL_EVENT_NAME, + self.keypress_event) + for seq in KEYPRESS_SEQUENCES: + self.widget.event_add(KEYPRESS_VIRTUAL_EVENT_NAME, seq) + self.keyreleaseid = self.widget.bind(KEYRELEASE_VIRTUAL_EVENT_NAME, + self.keyrelease_event) + self.widget.event_add(KEYRELEASE_VIRTUAL_EVENT_NAME,KEYRELEASE_SEQUENCE) + self.listupdateid = listbox.bind(LISTUPDATE_SEQUENCE, + self.listselect_event) + self.winconfigid = acw.bind(WINCONFIG_SEQUENCE, self.winconfig_event) + self.doubleclickid = listbox.bind(DOUBLECLICK_SEQUENCE, + self.doubleclick_event) + return None + + def winconfig_event(self, event): + if not self.is_active(): + return + # Position the completion list window + text = self.widget + text.see(self.startindex) + x, y, cx, cy = text.bbox(self.startindex) + acw = self.autocompletewindow + acw_width, acw_height = acw.winfo_width(), acw.winfo_height() + text_width, text_height = text.winfo_width(), text.winfo_height() + new_x = text.winfo_rootx() + min(x, max(0, text_width - acw_width)) + new_y = text.winfo_rooty() + y + if (text_height - (y + cy) >= acw_height # enough height below + or y < acw_height): # not enough height above + # place acw below current line + new_y += cy + else: + # place acw above current line + new_y -= acw_height + acw.wm_geometry("+%d+%d" % (new_x, new_y)) + + if platform.system().startswith('Windows'): + # See issue 15786. When on Windows platform, Tk will misbehave + # to call winconfig_event multiple times, we need to prevent this, + # otherwise mouse button double click will not be able to used. + acw.unbind(WINCONFIG_SEQUENCE, self.winconfigid) + self.winconfigid = None + + def _hide_event_check(self): + if not self.autocompletewindow: + return + + try: + if not self.autocompletewindow.focus_get(): + self.hide_window() + except KeyError: + # See issue 734176, when user click on menu, acw.focus_get() + # will get KeyError. + self.hide_window() + + def hide_event(self, event): + # Hide autocomplete list if it exists and does not have focus or + # mouse click on widget / text area. + if self.is_active(): + if event.type == EventType.FocusOut: + # On Windows platform, it will need to delay the check for + # acw.focus_get() when click on acw, otherwise it will return + # None and close the window + self.widget.after(1, self._hide_event_check) + elif event.type == EventType.ButtonPress: + # ButtonPress event only bind to self.widget + self.hide_window() + + def listselect_event(self, event): + if self.is_active(): + self.userwantswindow = True + cursel = int(self.listbox.curselection()[0]) + self._change_start(self.completions[cursel]) + + def doubleclick_event(self, event): + # Put the selected completion in the text, and close the list + cursel = int(self.listbox.curselection()[0]) + self._change_start(self.completions[cursel]) + self.hide_window() + + def keypress_event(self, event): + if not self.is_active(): + return None + keysym = event.keysym + if hasattr(event, "mc_state"): + state = event.mc_state + else: + state = 0 + if keysym != "Tab": + self.lastkey_was_tab = False + if (len(keysym) == 1 or keysym in ("underscore", "BackSpace") + or (self.mode == COMPLETE_FILES and keysym in + ("period", "minus"))) \ + and not (state & ~MC_SHIFT): + # Normal editing of text + if len(keysym) == 1: + self._change_start(self.start + keysym) + elif keysym == "underscore": + self._change_start(self.start + '_') + elif keysym == "period": + self._change_start(self.start + '.') + elif keysym == "minus": + self._change_start(self.start + '-') + else: + # keysym == "BackSpace" + if len(self.start) == 0: + self.hide_window() + return None + self._change_start(self.start[:-1]) + self.lasttypedstart = self.start + self.listbox.select_clear(0, int(self.listbox.curselection()[0])) + self.listbox.select_set(self._binary_search(self.start)) + self._selection_changed() + return "break" + + elif keysym == "Return": + self.complete() + self.hide_window() + return 'break' + + elif (self.mode == COMPLETE_ATTRIBUTES and keysym in + ("period", "space", "parenleft", "parenright", "bracketleft", + "bracketright")) or \ + (self.mode == COMPLETE_FILES and keysym in + ("slash", "backslash", "quotedbl", "apostrophe")) \ + and not (state & ~MC_SHIFT): + # If start is a prefix of the selection, but is not '' when + # completing file names, put the whole + # selected completion. Anyway, close the list. + cursel = int(self.listbox.curselection()[0]) + if self.completions[cursel][:len(self.start)] == self.start \ + and (self.mode == COMPLETE_ATTRIBUTES or self.start): + self._change_start(self.completions[cursel]) + self.hide_window() + return None + + elif keysym in ("Home", "End", "Prior", "Next", "Up", "Down") and \ + not state: + # Move the selection in the listbox + self.userwantswindow = True + cursel = int(self.listbox.curselection()[0]) + if keysym == "Home": + newsel = 0 + elif keysym == "End": + newsel = len(self.completions)-1 + elif keysym in ("Prior", "Next"): + jump = self.listbox.nearest(self.listbox.winfo_height()) - \ + self.listbox.nearest(0) + if keysym == "Prior": + newsel = max(0, cursel-jump) + else: + assert keysym == "Next" + newsel = min(len(self.completions)-1, cursel+jump) + elif keysym == "Up": + newsel = max(0, cursel-1) + else: + assert keysym == "Down" + newsel = min(len(self.completions)-1, cursel+1) + self.listbox.select_clear(cursel) + self.listbox.select_set(newsel) + self._selection_changed() + self._change_start(self.completions[newsel]) + return "break" + + elif (keysym == "Tab" and not state): + if self.lastkey_was_tab: + # two tabs in a row; insert current selection and close acw + cursel = int(self.listbox.curselection()[0]) + self._change_start(self.completions[cursel]) + self.hide_window() + return "break" + else: + # first tab; let AutoComplete handle the completion + self.userwantswindow = True + self.lastkey_was_tab = True + return None + + elif any(s in keysym for s in ("Shift", "Control", "Alt", + "Meta", "Command", "Option")): + # A modifier key, so ignore + return None + + elif event.char and event.char >= ' ': + # Regular character with a non-length-1 keycode + self._change_start(self.start + event.char) + self.lasttypedstart = self.start + self.listbox.select_clear(0, int(self.listbox.curselection()[0])) + self.listbox.select_set(self._binary_search(self.start)) + self._selection_changed() + return "break" + + else: + # Unknown event, close the window and let it through. + self.hide_window() + return None + + def keyrelease_event(self, event): + if not self.is_active(): + return + if self.widget.index("insert") != \ + self.widget.index("%s+%dc" % (self.startindex, len(self.start))): + # If we didn't catch an event which moved the insert, close window + self.hide_window() + + def is_active(self): + return self.autocompletewindow is not None + + def complete(self): + self._change_start(self._complete_string(self.start)) + # The selection doesn't change. + + def hide_window(self): + if not self.is_active(): + return + + # unbind events + self.autocompletewindow.event_delete(HIDE_VIRTUAL_EVENT_NAME, + HIDE_FOCUS_OUT_SEQUENCE) + for seq in HIDE_SEQUENCES: + self.widget.event_delete(HIDE_VIRTUAL_EVENT_NAME, seq) + + self.autocompletewindow.unbind(HIDE_VIRTUAL_EVENT_NAME, self.hideaid) + self.widget.unbind(HIDE_VIRTUAL_EVENT_NAME, self.hidewid) + self.hideaid = None + self.hidewid = None + for seq in KEYPRESS_SEQUENCES: + self.widget.event_delete(KEYPRESS_VIRTUAL_EVENT_NAME, seq) + self.widget.unbind(KEYPRESS_VIRTUAL_EVENT_NAME, self.keypressid) + self.keypressid = None + self.widget.event_delete(KEYRELEASE_VIRTUAL_EVENT_NAME, + KEYRELEASE_SEQUENCE) + self.widget.unbind(KEYRELEASE_VIRTUAL_EVENT_NAME, self.keyreleaseid) + self.keyreleaseid = None + self.listbox.unbind(LISTUPDATE_SEQUENCE, self.listupdateid) + self.listupdateid = None + if self.winconfigid: + self.autocompletewindow.unbind(WINCONFIG_SEQUENCE, self.winconfigid) + self.winconfigid = None + + # Re-focusOn frame.text (See issue #15786) + self.widget.focus_set() + + # destroy widgets + self.scrollbar.destroy() + self.scrollbar = None + self.listbox.destroy() + self.listbox = None + self.autocompletewindow.destroy() + self.autocompletewindow = None + + +if __name__ == '__main__': + from unittest import main + main('idlelib.idle_test.test_autocomplete_w', verbosity=2, exit=False) + +# TODO: autocomplete/w htest here diff --git a/lib-python/3/idlelib/autoexpand.py b/lib-python/3/idlelib/autoexpand.py new file mode 100644 --- /dev/null +++ b/lib-python/3/idlelib/autoexpand.py @@ -0,0 +1,96 @@ +'''Complete the current word before the cursor with words in the editor. + +Each menu selection or shortcut key selection replaces the word with a +different word with the same prefix. The search for matches begins +before the target and moves toward the top of the editor. It then starts +after the cursor and moves down. It then returns to the original word and +the cycle starts again. + +Changing the current text line or leaving the cursor in a different +place before requesting the next selection causes AutoExpand to reset +its state. + +There is only one instance of Autoexpand. +''' +import re +import string + + +class AutoExpand: + wordchars = string.ascii_letters + string.digits + "_" + + def __init__(self, editwin): + self.text = editwin.text + self.bell = self.text.bell + self.state = None + + def expand_word_event(self, event): + "Replace the current word with the next expansion." + curinsert = self.text.index("insert") + curline = self.text.get("insert linestart", "insert lineend") + if not self.state: + words = self.getwords() + index = 0 + else: + words, index, insert, line = self.state + if insert != curinsert or line != curline: + words = self.getwords() + index = 0 + if not words: + self.bell() + return "break" + word = self.getprevword() + self.text.delete("insert - %d chars" % len(word), "insert") + newword = words[index] + index = (index + 1) % len(words) + if index == 0: + self.bell() # Warn we cycled around + self.text.insert("insert", newword) + curinsert = self.text.index("insert") + curline = self.text.get("insert linestart", "insert lineend") + self.state = words, index, curinsert, curline + return "break" + + def getwords(self): + "Return a list of words that match the prefix before the cursor." + word = self.getprevword() + if not word: + return [] + before = self.text.get("1.0", "insert wordstart") + wbefore = re.findall(r"\b" + word + r"\w+\b", before) + del before + after = self.text.get("insert wordend", "end") + wafter = re.findall(r"\b" + word + r"\w+\b", after) + del after + if not wbefore and not wafter: + return [] + words = [] + dict = {} + # search backwards through words before + wbefore.reverse() + for w in wbefore: + if dict.get(w): + continue + words.append(w) + dict[w] = w + # search onwards through words after + for w in wafter: + if dict.get(w): + continue + words.append(w) + dict[w] = w + words.append(word) + return words + + def getprevword(self): + "Return the word prefix before the cursor." + line = self.text.get("insert linestart", "insert") + i = len(line) + while i > 0 and line[i-1] in self.wordchars: + i = i-1 + return line[i:] + + +if __name__ == '__main__': + from unittest import main + main('idlelib.idle_test.test_autoexpand', verbosity=2) diff --git a/lib-python/3/idlelib/browser.py b/lib-python/3/idlelib/browser.py new file mode 100644 --- /dev/null +++ b/lib-python/3/idlelib/browser.py @@ -0,0 +1,248 @@ +"""Module browser. + +XXX TO DO: + +- reparse when source changed (maybe just a button would be OK?) + (or recheck on window popup) +- add popup menu with more options (e.g. doc strings, base classes, imports) +- add base classes to class browser tree +- finish removing limitation to x.py files (ModuleBrowserTreeItem) +""" + +import os +from idlelib import _pyclbr as pyclbr +import sys + +from idlelib.config import idleConf +from idlelib import pyshell +from idlelib.tree import TreeNode, TreeItem, ScrolledCanvas +from idlelib.window import ListedToplevel + + +file_open = None # Method...Item and Class...Item use this. +# Normally pyshell.flist.open, but there is no pyshell.flist for htest. + + +def transform_children(child_dict, modname=None): + """Transform a child dictionary to an ordered sequence of objects. + + The dictionary maps names to pyclbr information objects. + Filter out imported objects. + Augment class names with bases. + Sort objects by line number. + + The current tree only calls this once per child_dic as it saves + TreeItems once created. A future tree and tests might violate this, + so a check prevents multiple in-place augmentations. + """ + obs = [] # Use list since values should already be sorted. + for key, obj in child_dict.items(): + if modname is None or obj.module == modname: + if hasattr(obj, 'super') and obj.super and obj.name == key: + # If obj.name != key, it has already been suffixed. + supers = [] + for sup in obj.super: + if type(sup) is type(''): + sname = sup + else: + sname = sup.name + if sup.module != obj.module: + sname = f'{sup.module}.{sname}' + supers.append(sname) + obj.name += '({})'.format(', '.join(supers)) + obs.append(obj) + return sorted(obs, key=lambda o: o.lineno) + + +class ModuleBrowser: + """Browse module classes and functions in IDLE. + """ + # This class is also the base class for pathbrowser.PathBrowser. + # Init and close are inherited, other methods are overridden. + # PathBrowser.__init__ does not call __init__ below. + + def __init__(self, master, path, *, _htest=False, _utest=False): + """Create a window for browsing a module's structure. + + Args: + master: parent for widgets. + path: full path of file to browse. + _htest - bool; change box location when running htest. + -utest - bool; suppress contents when running unittest. + + Global variables: + file_open: Function used for opening a file. + + Instance variables: + name: Module name. + file: Full path and module with .py extension. Used in + creating ModuleBrowserTreeItem as the rootnode for + the tree and subsequently in the children. + """ + self.master = master + self.path = path + self._htest = _htest + self._utest = _utest + self.init() + + def close(self, event=None): + "Dismiss the window and the tree nodes." + self.top.destroy() + self.node.destroy() + + def init(self): + "Create browser tkinter widgets, including the tree." + global file_open + root = self.master + flist = (pyshell.flist if not (self._htest or self._utest) + else pyshell.PyShellFileList(root)) + file_open = flist.open + pyclbr._modules.clear() + + # create top + self.top = top = ListedToplevel(root) + top.protocol("WM_DELETE_WINDOW", self.close) + top.bind("<Escape>", self.close) + if self._htest: # place dialog below parent if running htest + top.geometry("+%d+%d" % + (root.winfo_rootx(), root.winfo_rooty() + 200)) + self.settitle() + top.focus_set() + + # create scrolled canvas + theme = idleConf.CurrentTheme() + background = idleConf.GetHighlight(theme, 'normal')['background'] + sc = ScrolledCanvas(top, bg=background, highlightthickness=0, + takefocus=1) + sc.frame.pack(expand=1, fill="both") + item = self.rootnode() + self.node = node = TreeNode(sc.canvas, None, item) + if not self._utest: + node.update() + node.expand() + + def settitle(self): + "Set the window title." + self.top.wm_title("Module Browser - " + os.path.basename(self.path)) + self.top.wm_iconname("Module Browser") + + def rootnode(self): + "Return a ModuleBrowserTreeItem as the root of the tree." + return ModuleBrowserTreeItem(self.path) + + +class ModuleBrowserTreeItem(TreeItem): + """Browser tree for Python module. + + Uses TreeItem as the basis for the structure of the tree. + Used by both browsers. + """ + + def __init__(self, file): + """Create a TreeItem for the file. + + Args: + file: Full path and module name. + """ + self.file = file + + def GetText(self): + "Return the module name as the text string to display." + return os.path.basename(self.file) + + def GetIconName(self): + "Return the name of the icon to display." + return "python" + + def GetSubList(self): + "Return ChildBrowserTreeItems for children." + return [ChildBrowserTreeItem(obj) for obj in self.listchildren()] + + def OnDoubleClick(self): + "Open a module in an editor window when double clicked." + if os.path.normcase(self.file[-3:]) != ".py": + return + if not os.path.exists(self.file): + return + file_open(self.file) + + def IsExpandable(self): + "Return True if Python (.py) file." + return os.path.normcase(self.file[-3:]) == ".py" + + def listchildren(self): + "Return sequenced classes and functions in the module." + dir, base = os.path.split(self.file) + name, ext = os.path.splitext(base) + if os.path.normcase(ext) != ".py": + return [] + try: + tree = pyclbr.readmodule_ex(name, [dir] + sys.path) + except ImportError: + return [] + return transform_children(tree, name) + + +class ChildBrowserTreeItem(TreeItem): + """Browser tree for child nodes within the module. + + Uses TreeItem as the basis for the structure of the tree. + """ + + def __init__(self, obj): + "Create a TreeItem for a pyclbr class/function object." + self.obj = obj + self.name = obj.name + self.isfunction = isinstance(obj, pyclbr.Function) + + def GetText(self): + "Return the name of the function/class to display." + name = self.name + if self.isfunction: + return "def " + name + "(...)" + else: + return "class " + name + + def GetIconName(self): + "Return the name of the icon to display." + if self.isfunction: + return "python" + else: + return "folder" + + def IsExpandable(self): + "Return True if self.obj has nested objects." + return self.obj.children != {} + + def GetSubList(self): + "Return ChildBrowserTreeItems for children." + return [ChildBrowserTreeItem(obj) + for obj in transform_children(self.obj.children)] + + def OnDoubleClick(self): + "Open module with file_open and position to lineno." + try: + edit = file_open(self.obj.file) + edit.gotoline(self.obj.lineno) + except (OSError, AttributeError): + pass + + +def _module_browser(parent): # htest # + if len(sys.argv) > 1: # If pass file on command line. + file = sys.argv[1] + else: + file = __file__ + # Add nested objects for htest. + class Nested_in_func(TreeNode): + def nested_in_class(): pass + def closure(): + class Nested_in_closure: pass + ModuleBrowser(parent, file, _htest=True) + +if __name__ == "__main__": + if len(sys.argv) == 1: # If pass file on command line, unittest fails. + from unittest import main + main('idlelib.idle_test.test_browser', verbosity=2, exit=False) + from idlelib.idle_test.htest import run + run(_module_browser) diff --git a/lib-python/3/idlelib/calltip.py b/lib-python/3/idlelib/calltip.py new file mode 100644 --- /dev/null +++ b/lib-python/3/idlelib/calltip.py @@ -0,0 +1,178 @@ +"""Pop up a reminder of how to call a function. + +Call Tips are floating windows which display function, class, and method +parameter and docstring information when you type an opening parenthesis, and +which disappear when you type a closing parenthesis. +""" +import inspect +import re +import sys +import textwrap +import types + +from idlelib import calltip_w +from idlelib.hyperparser import HyperParser +import __main__ + + +class Calltip: + + def __init__(self, editwin=None): + if editwin is None: # subprocess and test + self.editwin = None + else: + self.editwin = editwin + self.text = editwin.text + self.active_calltip = None + self._calltip_window = self._make_tk_calltip_window + + def close(self): + self._calltip_window = None + + def _make_tk_calltip_window(self): + # See __init__ for usage + return calltip_w.CalltipWindow(self.text) + + def _remove_calltip_window(self, event=None): + if self.active_calltip: + self.active_calltip.hidetip() + self.active_calltip = None + + def force_open_calltip_event(self, event): + "The user selected the menu entry or hotkey, open the tip." + self.open_calltip(True) + return "break" + + def try_open_calltip_event(self, event): + """Happens when it would be nice to open a calltip, but not really + necessary, for example after an opening bracket, so function calls + won't be made. + """ + self.open_calltip(False) + + def refresh_calltip_event(self, event): + if self.active_calltip and self.active_calltip.tipwindow: + self.open_calltip(False) + + def open_calltip(self, evalfuncs): + self._remove_calltip_window() + + hp = HyperParser(self.editwin, "insert") + sur_paren = hp.get_surrounding_brackets('(') + if not sur_paren: + return + hp.set_index(sur_paren[0]) + expression = hp.get_expression() + if not expression: + return + if not evalfuncs and (expression.find('(') != -1): + return + argspec = self.fetch_tip(expression) + if not argspec: + return + self.active_calltip = self._calltip_window() + self.active_calltip.showtip(argspec, sur_paren[0], sur_paren[1]) + + def fetch_tip(self, expression): + """Return the argument list and docstring of a function or class. + + If there is a Python subprocess, get the calltip there. Otherwise, + either this fetch_tip() is running in the subprocess or it was + called in an IDLE running without the subprocess. + + The subprocess environment is that of the most recently run script. If + two unrelated modules are being edited some calltips in the current + module may be inoperative if the module was not the last to run. + + To find methods, fetch_tip must be fed a fully qualified name. + + """ + try: + rpcclt = self.editwin.flist.pyshell.interp.rpcclt + except AttributeError: + rpcclt = None + if rpcclt: + return rpcclt.remotecall("exec", "get_the_calltip", + (expression,), {}) + else: + return get_argspec(get_entity(expression)) + + +def get_entity(expression): + """Return the object corresponding to expression evaluated + in a namespace spanning sys.modules and __main.dict__. + """ + if expression: + namespace = sys.modules.copy() + namespace.update(__main__.__dict__) + try: + return eval(expression, namespace) + except BaseException: + # An uncaught exception closes idle, and eval can raise any + # exception, especially if user classes are involved. + return None + +# The following are used in get_argspec and some in tests +_MAX_COLS = 85 +_MAX_LINES = 5 # enough for bytes +_INDENT = ' '*4 # for wrapped signatures +_first_param = re.compile(r'(?<=\()\w*\,?\s*') +_default_callable_argspec = "See source or doc" +_invalid_method = "invalid method signature" +_argument_positional = "\n['/' marks preceding arguments as positional-only]\n" + +def get_argspec(ob): + '''Return a string describing the signature of a callable object, or ''. + + For Python-coded functions and methods, the first line is introspected. + Delete 'self' parameter for classes (.__init__) and bound methods. + The next lines are the first lines of the doc string up to the first + empty line or _MAX_LINES. For builtins, this typically includes + the arguments in addition to the return value. + ''' + argspec = default = "" + try: + ob_call = ob.__call__ + except BaseException: + return default + + fob = ob_call if isinstance(ob_call, types.MethodType) else ob + + try: + argspec = str(inspect.signature(fob)) + except ValueError as err: + msg = str(err) + if msg.startswith(_invalid_method): + return _invalid_method + + if '/' in argspec: + """Using AC's positional argument should add the explain""" + argspec += _argument_positional + if isinstance(fob, type) and argspec == '()': + """fob with no argument, use default callable argspec""" + argspec = _default_callable_argspec + + lines = (textwrap.wrap(argspec, _MAX_COLS, subsequent_indent=_INDENT) + if len(argspec) > _MAX_COLS else [argspec] if argspec else []) + + if isinstance(ob_call, types.MethodType): + doc = ob_call.__doc__ + else: + doc = getattr(ob, "__doc__", "") + if doc: + for line in doc.split('\n', _MAX_LINES)[:_MAX_LINES]: + line = line.strip() + if not line: + break + if len(line) > _MAX_COLS: + line = line[: _MAX_COLS - 3] + '...' + lines.append(line) + argspec = '\n'.join(lines) + if not argspec: + argspec = _default_callable_argspec + return argspec + + +if __name__ == '__main__': + from unittest import main + main('idlelib.idle_test.test_calltip', verbosity=2) diff --git a/lib-python/3/idlelib/calltip_w.py b/lib-python/3/idlelib/calltip_w.py new file mode 100644 --- /dev/null +++ b/lib-python/3/idlelib/calltip_w.py @@ -0,0 +1,200 @@ +"""A call-tip window class for Tkinter/IDLE. + +After tooltip.py, which uses ideas gleaned from PySol. +Used by calltip.py. +""" +from tkinter import Label, LEFT, SOLID, TclError + +from idlelib.tooltip import TooltipBase + +HIDE_EVENT = "<<calltipwindow-hide>>" +HIDE_SEQUENCES = ("<Key-Escape>", "<FocusOut>") +CHECKHIDE_EVENT = "<<calltipwindow-checkhide>>" +CHECKHIDE_SEQUENCES = ("<KeyRelease>", "<ButtonRelease>") +CHECKHIDE_TIME = 100 # milliseconds + +MARK_RIGHT = "calltipwindowregion_right" + + +class CalltipWindow(TooltipBase): + """A call-tip widget for tkinter text widgets.""" + + def __init__(self, text_widget): + """Create a call-tip; shown by showtip(). + + text_widget: a Text widget with code for which call-tips are desired + """ + # Note: The Text widget will be accessible as self.anchor_widget + super(CalltipWindow, self).__init__(text_widget) + + self.label = self.text = None + self.parenline = self.parencol = self.lastline = None + self.hideid = self.checkhideid = None + self.checkhide_after_id = None + + def get_position(self): + """Choose the position of the call-tip.""" + curline = int(self.anchor_widget.index("insert").split('.')[0]) + if curline == self.parenline: + anchor_index = (self.parenline, self.parencol) + else: + anchor_index = (curline, 0) + box = self.anchor_widget.bbox("%d.%d" % anchor_index) + if not box: + box = list(self.anchor_widget.bbox("insert")) + # align to left of window + box[0] = 0 + box[2] = 0 + return box[0] + 2, box[1] + box[3] + + def position_window(self): + "Reposition the window if needed." + curline = int(self.anchor_widget.index("insert").split('.')[0]) + if curline == self.lastline: + return + self.lastline = curline + self.anchor_widget.see("insert") + super(CalltipWindow, self).position_window() + + def showtip(self, text, parenleft, parenright): + """Show the call-tip, bind events which will close it and reposition it. + + text: the text to display in the call-tip + parenleft: index of the opening parenthesis in the text widget + parenright: index of the closing parenthesis in the text widget, + or the end of the line if there is no closing parenthesis + """ + # Only called in calltip.Calltip, where lines are truncated + self.text = text + if self.tipwindow or not self.text: + return + + self.anchor_widget.mark_set(MARK_RIGHT, parenright) + self.parenline, self.parencol = map( + int, self.anchor_widget.index(parenleft).split(".")) + + super(CalltipWindow, self).showtip() + + self._bind_events() + + def showcontents(self): + """Create the call-tip widget.""" + self.label = Label(self.tipwindow, text=self.text, justify=LEFT, + background="#ffffe0", relief=SOLID, borderwidth=1, + font=self.anchor_widget['font']) + self.label.pack() + + def checkhide_event(self, event=None): + """Handle CHECK_HIDE_EVENT: call hidetip or reschedule.""" + if not self.tipwindow: + # If the event was triggered by the same event that unbound + # this function, the function will be called nevertheless, + # so do nothing in this case. + return None + + # Hide the call-tip if the insertion cursor moves outside of the + # parenthesis. + curline, curcol = map(int, self.anchor_widget.index("insert").split('.')) + if curline < self.parenline or \ + (curline == self.parenline and curcol <= self.parencol) or \ + self.anchor_widget.compare("insert", ">", MARK_RIGHT): + self.hidetip() + return "break" + + # Not hiding the call-tip. + + self.position_window() + # Re-schedule this function to be called again in a short while. + if self.checkhide_after_id is not None: + self.anchor_widget.after_cancel(self.checkhide_after_id) + self.checkhide_after_id = \ + self.anchor_widget.after(CHECKHIDE_TIME, self.checkhide_event) + return None + + def hide_event(self, event): + """Handle HIDE_EVENT by calling hidetip.""" + if not self.tipwindow: + # See the explanation in checkhide_event. + return None + self.hidetip() + return "break" + + def hidetip(self): + """Hide the call-tip.""" + if not self.tipwindow: + return + + try: + self.label.destroy() + except TclError: + pass + self.label = None + + self.parenline = self.parencol = self.lastline = None + try: + self.anchor_widget.mark_unset(MARK_RIGHT) + except TclError: + pass + + try: + self._unbind_events() + except (TclError, ValueError): + # ValueError may be raised by MultiCall + pass + + super(CalltipWindow, self).hidetip() + + def _bind_events(self): + """Bind event handlers.""" + self.checkhideid = self.anchor_widget.bind(CHECKHIDE_EVENT, + self.checkhide_event) + for seq in CHECKHIDE_SEQUENCES: + self.anchor_widget.event_add(CHECKHIDE_EVENT, seq) + self.anchor_widget.after(CHECKHIDE_TIME, self.checkhide_event) + self.hideid = self.anchor_widget.bind(HIDE_EVENT, + self.hide_event) + for seq in HIDE_SEQUENCES: + self.anchor_widget.event_add(HIDE_EVENT, seq) + + def _unbind_events(self): + """Unbind event handlers.""" + for seq in CHECKHIDE_SEQUENCES: + self.anchor_widget.event_delete(CHECKHIDE_EVENT, seq) + self.anchor_widget.unbind(CHECKHIDE_EVENT, self.checkhideid) + self.checkhideid = None + for seq in HIDE_SEQUENCES: + self.anchor_widget.event_delete(HIDE_EVENT, seq) + self.anchor_widget.unbind(HIDE_EVENT, self.hideid) + self.hideid = None + + +def _calltip_window(parent): # htest # + from tkinter import Toplevel, Text, LEFT, BOTH + + top = Toplevel(parent) + top.title("Test call-tips") + x, y = map(int, parent.geometry().split('+')[1:]) + top.geometry("250x100+%d+%d" % (x + 175, y + 150)) + text = Text(top) + text.pack(side=LEFT, fill=BOTH, expand=1) + text.insert("insert", "string.split") + top.update() + + calltip = CalltipWindow(text) + def calltip_show(event): + calltip.showtip("(s='Hello world')", "insert", "end") + def calltip_hide(event): + calltip.hidetip() + text.event_add("<<calltip-show>>", "(") + text.event_add("<<calltip-hide>>", ")") + text.bind("<<calltip-show>>", calltip_show) + text.bind("<<calltip-hide>>", calltip_hide) + + text.focus_set() + +if __name__ == '__main__': + from unittest import main + main('idlelib.idle_test.test_calltip_w', verbosity=2, exit=False) + + from idlelib.idle_test.htest import run + run(_calltip_window) diff --git a/lib-python/3/idlelib/codecontext.py b/lib-python/3/idlelib/codecontext.py new file mode 100644 --- /dev/null +++ b/lib-python/3/idlelib/codecontext.py @@ -0,0 +1,240 @@ +"""codecontext - display the block context above the edit window + +Once code has scrolled off the top of a window, it can be difficult to +determine which block you are in. This extension implements a pane at the top +of each IDLE edit window which provides block structure hints. These hints are +the lines which contain the block opening keywords, e.g. 'if', for the +enclosing block. The number of hint lines is determined by the maxlines +variable in the codecontext section of config-extensions.def. Lines which do +not open blocks are not shown in the context hints pane. + +""" +import re +from sys import maxsize as INFINITY + +import tkinter +from tkinter.constants import TOP, LEFT, X, W, SUNKEN + +from idlelib.config import idleConf + +BLOCKOPENERS = {"class", "def", "elif", "else", "except", "finally", "for", + "if", "try", "while", "with", "async"} +UPDATEINTERVAL = 100 # millisec +CONFIGUPDATEINTERVAL = 1000 # millisec + + +def get_spaces_firstword(codeline, c=re.compile(r"^(\s*)(\w*)")): + "Extract the beginning whitespace and first word from codeline." + return c.match(codeline).groups() + + +def get_line_info(codeline): + """Return tuple of (line indent value, codeline, block start keyword). + + The indentation of empty lines (or comment lines) is INFINITY. + If the line does not start a block, the keyword value is False. + """ + spaces, firstword = get_spaces_firstword(codeline) + indent = len(spaces) + if len(codeline) == indent or codeline[indent] == '#': + indent = INFINITY + opener = firstword in BLOCKOPENERS and firstword + return indent, codeline, opener + + +class CodeContext: + "Display block context above the edit window." + + def __init__(self, editwin): + """Initialize settings for context block. + + editwin is the Editor window for the context block. + self.text is the editor window text widget. + self.textfont is the editor window font. + + self.context displays the code context text above the editor text. + Initially None, it is toggled via <<toggle-code-context>>. + self.topvisible is the number of the top text line displayed. + self.info is a list of (line number, indent level, line text, + block keyword) tuples for the block structure above topvisible. + self.info[0] is initialized with a 'dummy' line which + starts the toplevel 'block' of the module. + + self.t1 and self.t2 are two timer events on the editor text widget to + monitor for changes to the context text or editor font. + """ + self.editwin = editwin + self.text = editwin.text + self.textfont = self.text["font"] + self.contextcolors = CodeContext.colors + self.context = None + self.topvisible = 1 + self.info = [(0, -1, "", False)] + # Start two update cycles, one for context lines, one for font changes. + self.t1 = self.text.after(UPDATEINTERVAL, self.timer_event) + self.t2 = self.text.after(CONFIGUPDATEINTERVAL, self.config_timer_event) + + @classmethod + def reload(cls): + "Load class variables from config." + cls.context_depth = idleConf.GetOption("extensions", "CodeContext", + "maxlines", type="int", default=15) + cls.colors = idleConf.GetHighlight(idleConf.CurrentTheme(), 'context') + + def __del__(self): + "Cancel scheduled events." + try: + self.text.after_cancel(self.t1) + self.text.after_cancel(self.t2) + except: + pass + + def toggle_code_context_event(self, event=None): + """Toggle code context display. + + If self.context doesn't exist, create it to match the size of the editor + window text (toggle on). If it does exist, destroy it (toggle off). + Return 'break' to complete the processing of the binding. + """ + if not self.context: + # Calculate the border width and horizontal padding required to + # align the context with the text in the main Text widget. + # + # All values are passed through getint(), since some + # values may be pixel objects, which can't simply be added to ints. + widgets = self.editwin.text, self.editwin.text_frame + # Calculate the required horizontal padding and border width. + padx = 0 + border = 0 + for widget in widgets: + padx += widget.tk.getint(widget.pack_info()['padx']) _______________________________________________ pypy-commit mailing list pypy-commit@python.org https://mail.python.org/mailman/listinfo/pypy-commit