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

Reply via email to