-

25.5. IDLE

+

IDLE

Source code: Lib/idlelib/


-

IDLE is Python’s Integrated Development and Learning Environment.

+

IDLE is Python’s Integrated Development and Learning Environment.

IDLE has the following features:

    -
  • coded in 100% pure Python, using the tkinter GUI toolkit
  • -
  • cross-platform: works mostly the same on Windows, Unix, and Mac OS X
  • -
  • Python shell window (interactive interpreter) with colorizing -of code input, output, and error messages
  • -
  • multi-window text editor with multiple undo, Python colorizing, -smart indent, call tips, auto completion, and other features
  • -
  • search within any window, replace within editor windows, and search -through multiple files (grep)
  • -
  • debugger with persistent breakpoints, stepping, and viewing -of global and local namespaces
  • -
  • configuration, browsers, and other dialogs
  • +
  • coded in 100% pure Python, using the tkinter GUI toolkit

  • +
  • cross-platform: works mostly the same on Windows, Unix, and macOS

  • +
  • Python shell window (interactive interpreter) with colorizing +of code input, output, and error messages

  • +
  • multi-window text editor with multiple undo, Python colorizing, +smart indent, call tips, auto completion, and other features

  • +
  • search within any window, replace within editor windows, and search +through multiple files (grep)

  • +
  • debugger with persistent breakpoints, stepping, and viewing +of global and local namespaces

  • +
  • configuration, browsers, and other dialogs

-

25.5.2. Editing and navigation

-

In this section, ‘C’ refers to the Control key on Windows and Unix and -the Command key on Mac OSX.

+

Editing and navigation

+
+

Editor windows

+

IDLE may open editor windows when it starts, depending on settings +and how you start IDLE. Thereafter, use the File menu. There can be only +one open editor window for a given file.

+

The title bar contains the name of the file, the full path, and the version +of Python and IDLE running the window. The status bar contains the line +number (‘Ln’) and column number (‘Col’). Line numbers start with 1; +column numbers with 0.

+

IDLE assumes that files with a known .py* extension contain Python code +and that other files do not. Run Python code with the Run menu.

+
+
+

Key bindings

+

In this section, ‘C’ refers to the Control key on Windows and Unix and +the Command key on macOS.

    -
  • Backspace deletes to the left; Del deletes to the right

    -
  • -
  • C-Backspace delete word left; C-Del delete word to the right

    -
  • -
  • Arrow keys and Page Up/Page Down to move around

    -
  • -
  • C-LeftArrow and C-RightArrow moves by words

    -
  • -
  • Home/End go to begin/end of line

    -
  • -
  • C-Home/C-End go to begin/end of file

    -
  • -
  • Some useful Emacs bindings are inherited from Tcl/Tk:

    +
  • Backspace deletes to the left; Del deletes to the right

  • +
  • C-Backspace delete word left; C-Del delete word to the right

  • +
  • Arrow keys and Page Up/Page Down to move around

  • +
  • C-LeftArrow and C-RightArrow moves by words

  • +
  • Home/End go to begin/end of line

  • +
  • C-Home/C-End go to begin/end of file

  • +
  • Some useful Emacs bindings are inherited from Tcl/Tk:

      -
    • C-a beginning of line
    • -
    • C-e end of line
    • -
    • C-k kill line (but doesn’t put it in clipboard)
    • -
    • C-l center window around the insertion point
    • -
    • C-b go backward one character without deleting (usually you can -also use the cursor key for this)
    • -
    • C-f go forward one character without deleting (usually you can -also use the cursor key for this)
    • -
    • C-p go up one line (usually you can also use the cursor key for -this)
    • -
    • C-d delete next character
    • +
    • C-a beginning of line

    • +
    • C-e end of line

    • +
    • C-k kill line (but doesn’t put it in clipboard)

    • +
    • C-l center window around the insertion point

    • +
    • C-b go backward one character without deleting (usually you can +also use the cursor key for this)

    • +
    • C-f go forward one character without deleting (usually you can +also use the cursor key for this)

    • +
    • C-p go up one line (usually you can also use the cursor key for +this)

    • +
    • C-d delete next character

-

Standard keybindings (like C-c to copy and C-v to paste) +

Standard keybindings (like C-c to copy and C-v to paste) may work. Keybindings are selected in the Configure IDLE dialog.

+
-

25.5.2.1. Automatic indentation

+

Automatic indentation

After a block-opening statement, the next line is indented by 4 spaces (in the Python Shell window by one tab). After certain keywords (break, return etc.) -the next line is dedented. In leading indentation, Backspace deletes up -to 4 spaces if they are there. Tab inserts spaces (in the Python +the next line is dedented. In leading indentation, Backspace deletes up +to 4 spaces if they are there. Tab inserts spaces (in the Python Shell window one tab), number depends on Indent width. Currently, tabs are restricted to four spaces due to Tcl/Tk limitations.

-

See also the indent/dedent region commands in the edit menu.

+

See also the indent/dedent region commands on the +Format menu.

-

25.5.2.2. Completions

+

Completions

Completions are supplied for functions, classes, and attributes of classes, both built-in and user-defined. Completions are also provided for filenames.

The AutoCompleteWindow (ACW) will open after a predefined delay (default is -two seconds) after a ‘.’ or (in a string) an os.sep is typed. If after one +two seconds) after a ‘.’ or (in a string) an os.sep is typed. If after one of those characters (plus zero or more other characters) a tab is typed the ACW will open immediately if a possible continuation is found.

If there is only one possible completion for the characters entered, a -Tab will supply that completion without opening the ACW.

-

‘Show Completions’ will force open a completions window, by default the -C-space will open a completions window. In an empty +Tab will supply that completion without opening the ACW.

+

‘Show Completions’ will force open a completions window, by default the +C-space will open a completions window. In an empty string, this will contain the files in the current directory. On a blank line, it will contain the built-in and user-defined functions and classes in the current namespaces, plus any modules imported. If some characters have been entered, the ACW will attempt to be more specific.

If a string of characters is typed, the ACW selection will jump to the -entry most closely matching those characters. Entering a tab will +entry most closely matching those characters. Entering a tab will cause the longest non-ambiguous match to be entered in the Editor window or -Shell. Two tab in a row will supply the current ACW selection, as +Shell. Two tab in a row will supply the current ACW selection, as will return or a double click. Cursor keys, Page Up/Down, mouse selection, and the scroll wheel all operate on the ACW.

-

“Hidden” attributes can be accessed by typing the beginning of hidden -name after a ‘.’, e.g. ‘_’. This allows access to modules with -__all__ set, or to class-private attributes.

-

Completions and the ‘Expand Word’ facility can save a lot of typing!

+

“Hidden” attributes can be accessed by typing the beginning of hidden +name after a ‘.’, e.g. ‘_’. This allows access to modules with +__all__ set, or to class-private attributes.

+

Completions and the ‘Expand Word’ facility can save a lot of typing!

Completions are currently limited to those in the namespaces. Names in -an Editor window which are not via __main__ and sys.modules will +an Editor window which are not via __main__ and sys.modules will not be found. Run the module once with your imports to correct this situation. Note that IDLE itself places quite a few modules in sys.modules, so much can be found by default, e.g. the re module.

-

If you don’t like the ACW popping up unbidden, simply make the delay +

If you don’t like the ACW popping up unbidden, simply make the delay longer or disable the extension.

-

25.5.2.3. Calltips

-

A calltip is shown when one types ( after the name of an accessible +

Calltips

+

A calltip is shown when one types ( after the name of an accessible function. A name expression may include dots and subscripts. A calltip remains until it is clicked, the cursor is moved out of the argument area, -or ) is typed. When the cursor is in the argument part of a definition, +or ) is typed. When the cursor is in the argument part of a definition, the menu or shortcut display a calltip.

A calltip consists of the function signature and the first line of the docstring. For builtins without an accessible signature, the calltip @@ -457,40 +512,61 @@

25.5.2.3. Calltipsitertools.count(. A calltip +

For example, restart the Shell and enter itertools.count(. A calltip appears because Idle imports itertools into the user process for its own use. -(This could change.) Enter turtle.write( and nothing appears. Idle does +(This could change.) Enter turtle.write( and nothing appears. Idle does not import turtle. The menu or shortcut do nothing either. Enter -import turtle and then turtle.write( will work.

+import turtle and then turtle.write( will work.

In an editor, import statements have no effect until one runs the file. One might want to run a file after writing the import statements at the top, or immediately run an existing file before editing.

+
+

Code Context

+

Within an editor window containing Python code, code context can be toggled +in order to show or hide a pane at the top of the window. When shown, this +pane freezes the opening lines for block code, such as those beginning with +class, def, or if keywords, that would have otherwise scrolled +out of view. The size of the pane will be expanded and contracted as needed +to show the all current levels of context, up to the maximum number of +lines defined in the Configure IDLE dialog (which defaults to 15). If there +are no current context lines and the feature is toggled on, a single blank +line will display. Clicking on a line in the context pane will move that +line to the top of the editor.

+

The text and background colors for the context pane can be configured under +the Highlights tab in the Configure IDLE dialog.

+
-

25.5.2.4. Python Shell window

+

Python Shell window

+

With IDLE’s Shell, one enters, edits, and recalls complete statements. +Most consoles and terminals only work with a single physical line at a time.

+

When one pastes code into Shell, it is not compiled and possibly executed +until one hits Return. One may edit pasted code first. +If one pastes more that one statement into Shell, the result will be a +SyntaxError when multiple statements are compiled as if they were one.

+

The editing features described in previous subsections work when entering +code interactively. IDLE’s Shell window also responds to the following keys.

    -
  • C-c interrupts executing command

    -
  • -
  • C-d sends end-of-file; closes window if typed at a >>> prompt

    -
  • -
  • Alt-/ (Expand word) is also useful to reduce typing

    +
  • C-c interrupts executing command

  • +
  • C-d sends end-of-file; closes window if typed at a >>> prompt

  • +
  • Alt-/ (Expand word) is also useful to reduce typing

    Command history

      -
    • Alt-p retrieves previous command matching what you have typed. On -OS X use C-p.
    • -
    • Alt-n retrieves next. On OS X use C-n.
    • -
    • Return while on any previous command retrieves that command
    • +
    • Alt-p retrieves previous command matching what you have typed. On +macOS use C-p.

    • +
    • Alt-n retrieves next. On macOS use C-n.

    • +
    • Return while on any previous command retrieves that command

-

25.5.2.5. Text colors

+

Text colors

Idle defaults to black on white text, but colors text with special meanings. For the shell, these are shell output, shell error, user output, and user error. For Python code, at the shell prompt or in an editor, these are -keywords, builtin class and function names, names following class and -def, strings, and comments. For any text window, these are the cursor (when +keywords, builtin class and function names, names following class and +def, strings, and comments. For any text window, these are the cursor (when present), found text (when possible), and selected text.

Text coloring is done in the background, so uncolorized text is occasionally visible. To change the color scheme, use the Configure IDLE dialog @@ -499,22 +575,22 @@

25.5.2.5. Text colors -

25.5.3. Startup and code execution

-

Upon startup with the -s option, IDLE will execute the file referenced by -the environment variables IDLESTARTUP or PYTHONSTARTUP. -IDLE first checks for IDLESTARTUP; if IDLESTARTUP is present the file -referenced is run. If IDLESTARTUP is not present, IDLE checks for -PYTHONSTARTUP. Files referenced by these environment variables are +

Startup and code execution

+

Upon startup with the -s option, IDLE will execute the file referenced by +the environment variables IDLESTARTUP or PYTHONSTARTUP. +IDLE first checks for IDLESTARTUP; if IDLESTARTUP is present the file +referenced is run. If IDLESTARTUP is not present, IDLE checks for +PYTHONSTARTUP. Files referenced by these environment variables are convenient places to store functions that are used frequently from the IDLE shell, or for executing import statements to import common modules.

-

In addition, Tk also loads a startup file if it is present. Note that the -Tk file is loaded unconditionally. This additional file is .Idle.py and is -looked for in the user’s home directory. Statements in this file will be +

In addition, Tk also loads a startup file if it is present. Note that the +Tk file is loaded unconditionally. This additional file is .Idle.py and is +looked for in the user’s home directory. Statements in this file will be executed in the Tk namespace, so this file is not useful for importing -functions to be used from IDLE’s Python shell.

+functions to be used from IDLE’s Python shell.

-

25.5.3.1. Command line usage

-
idle.py [-c command] [-d] [-e] [-h] [-i] [-r file] [-s] [-t title] [-] [arg] ...
+

Command line usage

+
-

25.5.3.2. Startup failure

+

Startup failure

IDLE uses a socket to communicate between the IDLE GUI process and the user code execution process. A connection must be established whenever the Shell starts or restarts. (The latter is indicated by a divider line that says -‘RESTART’). If the user process fails to connect to the GUI process, it -displays a Tk error box with a ‘cannot connect’ message that directs the +‘RESTART’). If the user process fails to connect to the GUI process, it +displays a Tk error box with a ‘cannot connect’ message that directs the user here. It then exits.

A common cause of failure is a user-written file with the same name as a standard library module, such as random.py and tkinter.py. When such a @@ -561,50 +637,122 @@

25.5.3.2. Startup failure

A zombie pythonw.exe process could be a problem. On Windows, use Task -Manager to detect and stop one. Sometimes a restart initiated by a program -crash or Keyboard Interrupt (control-C) may fail to connect. Dismissing -the error box or Restart Shell on the Shell menu may fix a temporary problem.

+Manager to check for one and stop it if there is. Sometimes a restart +initiated by a program crash or Keyboard Interrupt (control-C) may fail +to connect. Dismissing the error box or using Restart Shell on the Shell +menu may fix a temporary problem.

When IDLE first starts, it attempts to read user configuration files in -~/.idlerc/ (~ is one’s home directory). If there is a problem, an error +~/.idlerc/ (~ is one’s home directory). If there is a problem, an error message should be displayed. Leaving aside random disk glitches, this can -be prevented by never editing the files by hand, using the configuration -dialog, under Options, instead Options. Once it happens, the solution may -be to delete one or more of the configuration files.

+be prevented by never editing the files by hand. Instead, use the +configuration dialog, under Options. Once there is an error in a user +configuration file, the best solution may be to delete it and start over +with the settings dialog.

If IDLE quits with no message, and it was not started from a console, try -starting from a console (python -m idlelib) and see if a message appears.

+starting it from a console or terminal (python -m idlelib) and see if +this results in an error message.

-
-

25.5.3.3. IDLE-console differences

+
+

Running user code

With rare exceptions, the result of executing Python code with IDLE is -intended to be the same as executing the same code in a console window. +intended to be the same as executing the same code by the default method, +directly with Python in a text-mode system console or terminal window. However, the different interface and operation occasionally affect -visible results. For instance, sys.modules starts with more entries.

-

IDLE also replaces sys.stdin, sys.stdout, and sys.stderr with -objects that get input from and send output to the Shell window. -When Shell has the focus, it controls the keyboard and screen. This is +visible results. For instance, sys.modules starts with more entries, +and threading.activeCount() returns 2 instead of 1.

+

By default, IDLE runs user code in a separate OS process rather than in +the user interface process that runs the shell and editor. In the execution +process, it replaces sys.stdin, sys.stdout, and sys.stderr +with objects that get input from and send output to the Shell window. +The original values stored in sys.__stdin__, sys.__stdout__, and +sys.__stderr__ are not touched, but may be None.

+

When Shell has the focus, it controls the keyboard and screen. This is normally transparent, but functions that directly access the keyboard -and screen will not work. If sys is reset with importlib.reload(sys), -IDLE’s changes are lost and things like input, raw_input, and -print will not work correctly.

-

With IDLE’s Shell, one enters, edits, and recalls complete statements. -Some consoles only work with a single physical line at a time. IDLE uses -exec to run each statement. As a result, '__builtins__' is always -defined for each statement.

+and screen will not work. These include system-specific functions that +determine whether a key has been pressed and if so, which.

+

IDLE’s standard stream replacements are not inherited by subprocesses +created in the execution process, whether directly by user code or by modules +such as multiprocessing. If such subprocess use input from sys.stdin +or print or write to sys.stdout or sys.stderr, +IDLE should be started in a command line window. The secondary subprocess +will then be attached to that window for input and output.

+

The IDLE code running in the execution process adds frames to the call stack +that would not be there otherwise. IDLE wraps sys.getrecursionlimit and +sys.setrecursionlimit to reduce the effect of the additional stack frames.

+

If sys is reset by user code, such as with importlib.reload(sys), +IDLE’s changes are lost and input from the keyboard and output to the screen +will not work correctly.

+

When user code raises SystemExit either directly or by calling sys.exit, IDLE +returns to a Shell prompt instead of exiting.

+
+
+

User output in Shell

+

When a program outputs text, the result is determined by the +corresponding output device. When IDLE executes user code, sys.stdout +and sys.stderr are connected to the display area of IDLE’s Shell. Some of +its features are inherited from the underlying Tk Text widget. Others +are programmed additions. Where it matters, Shell is designed for development +rather than production runs.

+

For instance, Shell never throws away output. A program that sends unlimited +output to Shell will eventually fill memory, resulting in a memory error. +In contrast, some system text windows only keep the last n lines of output. +A Windows console, for instance, keeps a user-settable 1 to 9999 lines, +with 300 the default.

+

A Tk Text widget, and hence IDLE’s Shell, displays characters (codepoints) in +the BMP (Basic Multilingual Plane) subset of Unicode. Which characters are +displayed with a proper glyph and which with a replacement box depends on the +operating system and installed fonts. Tab characters cause the following text +to begin after the next tab stop. (They occur every 8 ‘characters’). Newline +characters cause following text to appear on a new line. Other control +characters are ignored or displayed as a space, box, or something else, +depending on the operating system and font. (Moving the text cursor through +such output with arrow keys may exhibit some surprising spacing behavior.)

+
>>> s = 'a\tb\a<\x02><\r>\bc\nd'  # Enter 22 chars.
+>>> len(s)
+14
+>>> s  # Display repr(s)
+'a\tb\x07<\x02><\r>\x08c\nd'
+>>> print(s, end='')  # Display s as is.
+# Result varies by OS and font.  Try it.
+
+
+

The repr function is used for interactive echo of expression +values. It returns an altered version of the input string in which +control codes, some BMP codepoints, and all non-BMP codepoints are +replaced with escape codes. As demonstrated above, it allows one to +identify the characters in a string, regardless of how they are displayed.

+

Normal and error output are generally kept separate (on separate lines) +from code input and each other. They each get different highlight colors.

+

For SyntaxError tracebacks, the normal ‘^’ marking where the error was +detected is replaced by coloring the text with an error highlight. +When code run from a file causes other exceptions, one may right click +on a traceback line to jump to the corresponding line in an IDLE editor. +The file will be opened if necessary.

+

Shell has a special facility for squeezing output lines down to a +‘Squeezed text’ label. This is done automatically +for output over N lines (N = 50 by default). +N can be changed in the PyShell section of the General +page of the Settings dialog. Output with fewer lines can be squeezed by +right clicking on the output. This can be useful lines long enough to slow +down scrolling.

+

Squeezed output is expanded in place by double-clicking the label. +It can also be sent to the clipboard or a separate view window by +right-clicking the label.

-

25.5.3.4. Developing tkinter applications

+

Developing tkinter applications

IDLE is intentionally different from standard Python in order to -facilitate development of tkinter programs. Enter import tkinter as tk; +facilitate development of tkinter programs. Enter import tkinter as tk; root = tk.Tk() in standard Python and nothing appears. Enter the same in IDLE and a tk window appears. In standard Python, one must also enter -root.update() to see the window. IDLE does the equivalent in the -background, about 20 times a second, which is about every 50 milleseconds. -Next enter b = tk.Button(root, text='button'); b.pack(). Again, -nothing visibly changes in standard Python until one enters root.update().

-

Most tkinter programs run root.mainloop(), which usually does not +root.update() to see the window. IDLE does the equivalent in the +background, about 20 times a second, which is about every 50 milliseconds. +Next enter b = tk.Button(root, text='button'); b.pack(). Again, +nothing visibly changes in standard Python until one enters root.update().

+

Most tkinter programs run root.mainloop(), which usually does not return until the tk app is destroyed. If the program is run with -python -i or from an IDLE editor, a >>> shell prompt does not -appear until mainloop() returns, at which time there is nothing left +python -i or from an IDLE editor, a >>> shell prompt does not +appear until mainloop() returns, at which time there is nothing left to interact with.

When running a tkinter program from an IDLE editor, one can comment out the mainloop call. One then gets a shell prompt immediately and can @@ -612,7 +760,7 @@

25.5.3.4. Developing tkinter applications -

25.5.3.5. Running without a subprocess

+

Running without a subprocess

By default, IDLE executes user code in a separate subprocess via a socket, which uses the internal loopback interface. This connection is not externally visible and no data is sent to or received from the Internet. @@ -633,29 +781,55 @@

25.5.3.5. Running without a subprocess -

Deprecated since version 3.4.

+

Deprecated since version 3.4.

-

25.5.4. Help and preferences

-
-

25.5.4.1. Additional help sources

-

IDLE includes a help menu entry called “Python Docs” that will open the -extensive sources of help, including tutorials, available at docs.python.org. -Selected URLs can be added or removed from the help menu at any time using the -Configure IDLE dialog. See the IDLE help option in the help menu of IDLE for -more information.

+

Help and preferences

+
+

Help sources

+

Help menu entry “IDLE Help” displays a formatted html version of the +IDLE chapter of the Library Reference. The result, in a read-only +tkinter text window, is close to what one sees in a web browser. +Navigate through the text with a mousewheel, +the scrollbar, or up and down arrow keys held down. +Or click the TOC (Table of Contents) button and select a section +header in the opened box.

+

Help menu entry “Python Docs” opens the extensive sources of help, +including tutorials, available at docs.python.org/x.y, where ‘x.y’ +is the currently running Python version. If your system +has an off-line copy of the docs (this may be an installation option), +that will be opened instead.

+

Selected URLs can be added or removed from the help menu at any time using the +General tab of the Configure IDLE dialog.

-

25.5.4.2. Setting preferences

+

Setting preferences

The font preferences, highlighting, keys, and general preferences can be -changed via Configure IDLE on the Option menu. Keys can be user defined; -IDLE ships with four built-in key sets. In addition, a user can create a -custom key set in the Configure IDLE dialog under the keys tab.

+changed via Configure IDLE on the Option menu. +Non-default user settings are saved in a .idlerc directory in the user’s +home directory. Problems caused by bad user configuration files are solved +by editing or deleting one or more of the files in .idlerc.

+

On the Font tab, see the text sample for the effect of font face and size +on multiple characters in multiple languages. Edit the sample to add +other characters of personal interest. Use the sample to select +monospaced fonts. If particular characters have problems in Shell or an +editor, add them to the top of the sample and try changing first size +and then font.

+

On the Highlights and Keys tab, select a built-in or custom color theme +and key set. To use a newer built-in color theme or key set with older +IDLEs, save it as a new custom theme or key set and it well be accessible +to older IDLEs.

+
+
+

IDLE on macOS

+

Under System Preferences: Dock, one can set “Prefer tabs when opening +documents” to “Always”. This setting is not compatible with the tk/tkinter +GUI framework used by IDLE, and it breaks a few IDLE features.

-

25.5.4.3. Extensions

+

Extensions

IDLE contains an extension facility. Preferences for extensions can be changed with the Extensions tab of the preferences dialog. See the beginning of config-extensions.def in the idlelib directory for further @@ -671,42 +845,47 @@

25.5.4.3. Extensions
-

Table Of Contents

+

Table of Contents

    -
  • 25.5. IDLE
      -
    • 25.5.1. Menus
        -
      • 25.5.1.1. File menu (Shell and Editor)
      • -
      • 25.5.1.2. Edit menu (Shell and Editor)
      • -
      • 25.5.1.3. Format menu (Editor window only)
      • -
      • 25.5.1.4. Run menu (Editor window only)
      • -
      • 25.5.1.5. Shell menu (Shell window only)
      • -
      • 25.5.1.6. Debug menu (Shell window only)
      • -
      • 25.5.1.7. Options menu (Shell and Editor)
      • -
      • 25.5.1.8. Window menu (Shell and Editor)
      • -
      • 25.5.1.9. Help menu (Shell and Editor)
      • -
      • 25.5.1.10. Context Menus
      • +
      • IDLE
          +
        • Menus
        • -
        • 25.5.2. Editing and navigation
            -
          • 25.5.2.1. Automatic indentation
          • -
          • 25.5.2.2. Completions
          • -
          • 25.5.2.3. Calltips
          • -
          • 25.5.2.4. Python Shell window
          • -
          • 25.5.2.5. Text colors
          • +
          • Editing and navigation
          • -
          • 25.5.3. Startup and code execution

            Previous topic

            -

            25.4. tkinter.scrolledtext — Scrolled Text Widget

            +

            tkinter.tix — Extension widgets for Tk

            Next topic

            25.6. Other Graphical User Interface Packages

            + title="next chapter">Other Graphical User Interface Packages

            This Page

            diff --git a/Lib/idlelib/help.py b/Lib/idlelib/help.py index fa6112a339444f..9f63ea0d3990e6 100644 --- a/Lib/idlelib/help.py +++ b/Lib/idlelib/help.py @@ -2,7 +2,7 @@ Contents are subject to revision at any time, without notice. -Help => About IDLE: diplay About Idle dialog +Help => About IDLE: display About Idle dialog @@ -50,20 +50,22 @@ class HelpParser(HTMLParser): """ def __init__(self, text): HTMLParser.__init__(self, convert_charrefs=True) - self.text = text # text widget we're rendering into - self.tags = '' # current block level text tags to apply - self.chartags = '' # current character level text tags - self.show = False # used so we exclude page navigation - self.hdrlink = False # used so we don't show header links - self.level = 0 # indentation level - self.pre = False # displaying preformatted text - self.hprefix = '' # prefix such as '25.5' to strip from headings - self.nested_dl = False # if we're in a nested
            - self.simplelist = False # simple list (no double spacing) - self.toc = [] # pair headers with text indexes for toc - self.header = '' # text within header tags for toc + self.text = text # Text widget we're rendering into. + self.tags = '' # Current block level text tags to apply. + self.chartags = '' # Current character level text tags. + self.show = False # Exclude html page navigation. + self.hdrlink = False # Exclude html header links. + self.level = 0 # Track indentation level. + self.pre = False # Displaying preformatted text? + self.hprefix = '' # Heading prefix (like '25.5'?) to remove. + self.nested_dl = False # In a nested
            ? + self.simplelist = False # In a simple list (no double spacing)? + self.toc = [] # Pair headers with text indexes for toc. + self.header = '' # Text within header tags for toc. + self.prevtag = None # Previous tag info (opener?, tag). def indent(self, amt=1): + "Change indent (+1, 0, -1) and tags." self.level += amt self.tags = '' if self.level == 0 else 'l'+str(self.level) @@ -75,11 +77,14 @@ def handle_starttag(self, tag, attrs): class_ = v s = '' if tag == 'div' and class_ == 'section': - self.show = True # start of main content + self.show = True # Start main content. elif tag == 'div' and class_ == 'sphinxsidebar': - self.show = False # end of main content - elif tag == 'p' and class_ != 'first': - s = '\n\n' + self.show = False # End main content. + elif tag == 'p' and self.prevtag and not self.prevtag[0]: + # Begin a new block for

            tags after a closed tag. + # Avoid extra lines, e.g. after

             tags.
            +            lastline = self.text.get('end-1c linestart', 'end-1c')
            +            s = '\n\n' if lastline and not lastline.isspace() else '\n'
                     elif tag == 'span' and class_ == 'pre':
                         self.chartags = 'pre'
                     elif tag == 'span' and class_ == 'versionmodified':
            @@ -99,7 +104,7 @@ def handle_starttag(self, tag, attrs):
                     elif tag == 'li':
                         s = '\n* ' if self.simplelist else '\n\n* '
                     elif tag == 'dt':
            -            s = '\n\n' if not self.nested_dl else '\n'  # avoid extra line
            +            s = '\n\n' if not self.nested_dl else '\n'  # Avoid extra line.
                         self.nested_dl = False
                     elif tag == 'dd':
                         self.indent()
            @@ -120,13 +125,18 @@ def handle_starttag(self, tag, attrs):
                         self.tags = tag
                     if self.show:
                         self.text.insert('end', s, (self.tags, self.chartags))
            +        self.prevtag = (True, tag)
             
                 def handle_endtag(self, tag):
                     "Handle endtags in help.html."
                     if tag in ['h1', 'h2', 'h3']:
            -            self.indent(0)  # clear tag, reset indent
            +            assert self.level == 0
                         if self.show:
            -                self.toc.append((self.header, self.text.index('insert')))
            +                indent = ('        ' if tag == 'h3' else
            +                          '    ' if tag == 'h2' else
            +                          '')
            +                self.toc.append((indent+self.header, self.text.index('insert')))
            +            self.tags = ''
                     elif tag in ['span', 'em']:
                         self.chartags = ''
                     elif tag == 'a':
            @@ -135,18 +145,23 @@ def handle_endtag(self, tag):
                         self.pre = False
                         self.tags = ''
                     elif tag in ['ul', 'dd', 'ol']:
            -            self.indent(amt=-1)
            +            self.indent(-1)
            +        self.prevtag = (False, tag)
             
                 def handle_data(self, data):
                     "Handle date segments in help.html."
                     if self.show and not self.hdrlink:
                         d = data if self.pre else data.replace('\n', ' ')
                         if self.tags == 'h1':
            -                self.hprefix = d[0:d.index(' ')]
            -            if self.tags in ['h1', 'h2', 'h3'] and self.hprefix != '':
            -                if d[0:len(self.hprefix)] == self.hprefix:
            -                    d = d[len(self.hprefix):].strip()
            -                self.header += d
            +                try:
            +                    self.hprefix = d[0:d.index(' ')]
            +                except ValueError:
            +                    self.hprefix = ''
            +            if self.tags in ['h1', 'h2', 'h3']:
            +                if (self.hprefix != '' and
            +                    d[0:len(self.hprefix)] == self.hprefix):
            +                    d = d[len(self.hprefix):]
            +                self.header += d.strip()
                         self.text.insert('end', d, (self.tags, self.chartags))
             
             
            @@ -156,7 +171,7 @@ def __init__(self, parent, filename):
                     "Configure tags and feed file to parser."
                     uwide = idleConf.GetOption('main', 'EditorWindow', 'width', type='int')
                     uhigh = idleConf.GetOption('main', 'EditorWindow', 'height', type='int')
            -        uhigh = 3 * uhigh // 4  # lines average 4/3 of editor line height
            +        uhigh = 3 * uhigh // 4  # Lines average 4/3 of editor line height.
                     Text.__init__(self, parent, wrap='word', highlightthickness=0,
                                   padx=5, borderwidth=0, width=uwide, height=uhigh)
             
            @@ -196,7 +211,6 @@ class HelpFrame(Frame):
                 "Display html text, scrollbar, and toc."
                 def __init__(self, parent, filename):
                     Frame.__init__(self, parent)
            -        # keep references to widgets for test access.
                     self.text = text = HelpText(self, filename)
                     self['background'] = text['background']
                     self.toc = toc = self.toc_menu(text)
            @@ -204,7 +218,7 @@ def __init__(self, parent, filename):
                     text['yscrollcommand'] = scroll.set
             
                     self.rowconfigure(0, weight=1)
            -        self.columnconfigure(1, weight=1)  # text
            +        self.columnconfigure(1, weight=1)  # Only expand the text widget.
                     toc.grid(row=0, column=0, sticky='nw')
                     text.grid(row=0, column=1, sticky='nsew')
                     scroll.grid(row=0, column=2, sticky='ns')
            @@ -233,28 +247,28 @@ def __init__(self, parent, filename, title):
             def copy_strip():
                 """Copy idle.html to idlelib/help.html, stripping trailing whitespace.
             
            -    Files with trailing whitespace cannot be pushed to the hg cpython
            +    Files with trailing whitespace cannot be pushed to the git cpython
                 repository.  For 3.x (on Windows), help.html is generated, after
            -    editing idle.rst in the earliest maintenance version, with
            +    editing idle.rst on the master branch, with
                   sphinx-build -bhtml . build/html
                   python_d.exe -c "from idlelib.help import copy_strip; copy_strip()"
            -    After refreshing TortoiseHG workshop to generate a diff,
            -    check  both the diff and displayed text.  Push the diff along with
            -    the idle.rst change and merge both into default (or an intermediate
            -    maintenance version).
            -
            -    When the 'earlist' version gets its final maintenance release,
            -    do an update as described above, without editing idle.rst, to
            -    rebase help.html on the next version of idle.rst.  Do not worry
            -    about version changes as version is not displayed.  Examine other
            -    changes and the result of Help -> IDLE Help.
            -
            -    If maintenance and default versions of idle.rst diverge, and
            -    merging does not go smoothly, then consider generating
            -    separate help.html files from separate idle.htmls.
            +    Check build/html/library/idle.html, the help.html diff, and the text
            +    displayed by Help => IDLE Help.  Add a blurb and create a PR.
            +
            +    It can be worthwhile to occasionally generate help.html without
            +    touching idle.rst.  Changes to the master version and to the doc
            +    build system may result in changes that should not changed
            +    the displayed text, but might break HelpParser.
            +
            +    As long as master and maintenance versions of idle.rst remain the
            +    same, help.html can be backported.  The internal Python version
            +    number is not displayed.  If maintenance idle.rst diverges from
            +    the master version, then instead of backporting help.html from
            +    master, repeat the procedure above to generate a maintenance
            +    version.
                 """
                 src = join(abspath(dirname(dirname(dirname(__file__)))),
            -               'Doc', 'build', 'html', 'library', 'idle.html')
            +            'Doc', 'build', 'html', 'library', 'idle.html')
                 dst = join(abspath(dirname(__file__)), 'help.html')
                 with open(src, 'rb') as inn,\
                      open(dst, 'wb') as out:
            @@ -266,10 +280,13 @@ def show_idlehelp(parent):
                 "Create HelpWindow; called from Idle Help event handler."
                 filename = join(abspath(dirname(__file__)), 'help.html')
                 if not isfile(filename):
            -        # try copy_strip, present message
            +        # Try copy_strip, present message.
                     return
                 HelpWindow(parent, filename, 'IDLE Help (%s)' % python_version())
             
             if __name__ == '__main__':
            +    from unittest import main
            +    main('idlelib.idle_test.test_help', verbosity=2, exit=False)
            +
                 from idlelib.idle_test.htest import run
                 run(show_idlehelp)
            diff --git a/Lib/idlelib/help_about.py b/Lib/idlelib/help_about.py
            index 77b4b18962066c..64b13ac2abb3b2 100644
            --- a/Lib/idlelib/help_about.py
            +++ b/Lib/idlelib/help_about.py
            @@ -195,11 +195,13 @@ def display_file_text(self, title, filename, encoding=None):
             
                 def ok(self, event=None):
                     "Dismiss help_about dialog."
            +        self.grab_release()
                     self.destroy()
             
             
             if __name__ == '__main__':
            -    import unittest
            -    unittest.main('idlelib.idle_test.test_help_about', verbosity=2, exit=False)
            +    from unittest import main
            +    main('idlelib.idle_test.test_help_about', verbosity=2, exit=False)
            +
                 from idlelib.idle_test.htest import run
                 run(AboutDialog)
            diff --git a/Lib/idlelib/history.py b/Lib/idlelib/history.py
            index 56f53a0f2fb991..ad44a96a9de2c0 100644
            --- a/Lib/idlelib/history.py
            +++ b/Lib/idlelib/history.py
            @@ -39,7 +39,7 @@ def history_prev(self, event):
                     return "break"
             
                 def fetch(self, reverse):
            -        '''Fetch statememt and replace current line in text widget.
            +        '''Fetch statement and replace current line in text widget.
             
                     Set prefix and pointer as needed for successive fetches.
                     Reset them to None, None when returning to the start line.
            diff --git a/Lib/idlelib/hyperparser.py b/Lib/idlelib/hyperparser.py
            index 450a709c09bbfa..77baca782b3fdc 100644
            --- a/Lib/idlelib/hyperparser.py
            +++ b/Lib/idlelib/hyperparser.py
            @@ -35,7 +35,7 @@ def index2line(index):
                         return int(float(index))
                     lno = index2line(text.index(index))
             
            -        if not editwin.context_use_ps1:
            +        if not editwin.prompt_last_line:
                         for context in editwin.num_context_lines:
                             startat = max(lno - context, 1)
                             startatindex = repr(startat) + ".0"
            @@ -44,7 +44,7 @@ def index2line(index):
                             # at end. We add a space so that index won't be at end
                             # of line, so that its status will be the same as the
                             # char before it, if should.
            -                parser.set_str(text.get(startatindex, stopatindex)+' \n')
            +                parser.set_code(text.get(startatindex, stopatindex)+' \n')
                             bod = parser.find_good_parse_start(
                                       editwin._build_char_in_string_func(startatindex))
                             if bod is not None or startat == 1:
            @@ -60,12 +60,12 @@ def index2line(index):
                         # We add the newline because PyParse requires it. We add a
                         # space so that index won't be at end of line, so that its
                         # status will be the same as the char before it, if should.
            -            parser.set_str(text.get(startatindex, stopatindex)+' \n')
            +            parser.set_code(text.get(startatindex, stopatindex)+' \n')
                         parser.set_lo(0)
             
                     # We want what the parser has, minus the last newline and space.
            -        self.rawtext = parser.str[:-2]
            -        # Parser.str apparently preserves the statement we are in, so
            +        self.rawtext = parser.code[:-2]
            +        # Parser.code apparently preserves the statement we are in, so
                     # that stopatindex can be used to synchronize the string with
                     # the text box indices.
                     self.stopatindex = stopatindex
            @@ -224,7 +224,7 @@ def get_expression(self):
                     given index, which is empty if there is no real one.
                     """
                     if not self.is_in_code():
            -            raise ValueError("get_expression should only be called"
            +            raise ValueError("get_expression should only be called "
                                          "if index is inside a code.")
             
                     rawtext = self.rawtext
            @@ -308,5 +308,5 @@ def get_expression(self):
             
             
             if __name__ == '__main__':
            -    import unittest
            -    unittest.main('idlelib.idle_test.test_hyperparser', verbosity=2)
            +    from unittest import main
            +    main('idlelib.idle_test.test_hyperparser', verbosity=2)
            diff --git a/Lib/idlelib/idle_test/README.txt b/Lib/idlelib/idle_test/README.txt
            index 5f3678fc7e1de3..566bfd179fdf1b 100644
            --- a/Lib/idlelib/idle_test/README.txt
            +++ b/Lib/idlelib/idle_test/README.txt
            @@ -15,28 +15,27 @@ python -m idlelib.idle_test.htest
             1. Test Files
             
             The idle directory, idlelib, has over 60 xyz.py files. The idle_test
            -subdirectory should contain a test_xyz.py for each, where 'xyz' is
            -lowercased even if xyz.py is not. Here is a possible template, with the
            -blanks after '.' and 'as', and before and after '_' to be filled in.
            +subdirectory contains test_xyz.py for each implementation file xyz.py.
            +To add a test for abc.py, open idle_test/template.py and immediately
            +Save As test_abc.py.  Insert 'abc' on the first line, and replace
            +'zzdummy' with 'abc.
             
            -import unittest
            -from test.support import requires
            -import idlelib. as
            -
            -class _Test(unittest.TestCase):
            +Remove the imports of requires and tkinter if not needed.  Otherwise,
            +add to the tkinter imports as needed.
             
            -    def test_(self):
            +Add a prefix to 'Test' for the initial test class.  The template class
            +contains code needed or possibly needed for gui tests.  See the next
            +section if doing gui tests.  If not, and not needed for further classes,
            +this code can be removed.
             
            -if __name__ == '__main__':
            -    unittest.main(verbosity=2)
            -
            -Add the following at the end of xyy.py, with the appropriate name added
            -after 'test_'. Some files already have something like this for htest.
            -If so, insert the import and unittest.main lines before the htest lines.
            +Add the following at the end of abc.py.  If an htest was added first,
            +insert the import and main lines before the htest lines.
             
             if __name__ == "__main__":
            -    import unittest
            -    unittest.main('idlelib.idle_test.test_', verbosity=2, exit=False)
            +    from unittest import main
            +    main('idlelib.idle_test.test_abc', verbosity=2, exit=False)
            +
            +The ', exit=False' is only needed if an htest follows.
             
             
             
            @@ -55,12 +54,14 @@ from test.support import requires
             requires('gui')
             
             To guard a test class, put "requires('gui')" in its setUpClass function.
            +The template.py file does this.
             
            -To avoid interfering with other GUI tests, all GUI objects must be destroyed and
            -deleted by the end of the test.  The Tk root created in a setUpX function should
            -be destroyed in the corresponding tearDownX and the module or class attribute
            -deleted.  Others widgets should descend from the single root and the attributes
            -deleted BEFORE root is destroyed.  See https://bugs.python.org/issue20567.
            +To avoid interfering with other GUI tests, all GUI objects must be
            +destroyed and deleted by the end of the test.  The Tk root created in a
            +setUpX function should be destroyed in the corresponding tearDownX and
            +the module or class attribute deleted.  Others widgets should descend
            +from the single root and the attributes deleted BEFORE root is
            +destroyed.  See https://bugs.python.org/issue20567.
             
                 @classmethod
                 def setUpClass(cls):
            @@ -75,12 +76,23 @@ deleted BEFORE root is destroyed.  See https://bugs.python.org/issue20567.
                     cls.root.destroy()
                     del cls.root
             
            -The update_idletasks call is sometimes needed to prevent the following warning
            -either when running a test alone or as part of the test suite (#27196).
            +The update_idletasks call is sometimes needed to prevent the following
            +warning either when running a test alone or as part of the test suite
            +(#27196).  It should not hurt if not needed.
            +
               can't invoke "event" command: application has been destroyed
               ...
               "ttk::ThemeChanged"
             
            +If a test creates instance 'e' of EditorWindow, call 'e._close()' before
            +or as the first part of teardown.  The effect of omitting this depends
            +on the later shutdown.  Then enable the after_cancel loop in the
            +template.  This prevents messages like the following.
            +
            +bgerror failed to handle background error.
            +    Original error: invalid command name "106096696timer_event"
            +    Error in bgerror: can't invoke "tk" command: application has been destroyed
            +
             Requires('gui') causes the test(s) it guards to be skipped if any of
             these conditions are met:
             
            diff --git a/Lib/idlelib/idle_test/htest.py b/Lib/idlelib/idle_test/htest.py
            index 442f55e283a484..1373b7642a6ea9 100644
            --- a/Lib/idlelib/idle_test/htest.py
            +++ b/Lib/idlelib/idle_test/htest.py
            @@ -12,7 +12,7 @@
             The first parameter of X must be 'parent'.  When called, the parent
             argument will be the root window.  X must create a child Toplevel
             window (or subclass thereof).  The Toplevel may be a test widget or
            -dialog, in which case the callable is the corresonding class.  Or the
            +dialog, in which case the callable is the corresponding class.  Or the
             Toplevel may contain the widget to be tested or set up a context in
             which a test widget is invoked.  In this latter case, the callable is a
             wrapper function that sets up the Toplevel and other objects.  Wrapper
            @@ -65,7 +65,9 @@ def _wrapper(parent):  # htest #
             outwin.OutputWindow (indirectly being tested with grep test)
             '''
             
            +import idlelib.pyshell  # Set Windows DPI awareness before Tk().
             from importlib import import_module
            +import textwrap
             import tkinter as tk
             from tkinter.ttk import Scrollbar
             tk.NoDefaultRoot()
            @@ -79,11 +81,14 @@ def _wrapper(parent):  # htest #
                        "are correctly displayed.\n [Close] to exit.",
                 }
             
            +# TODO implement ^\; adding '' to function does not work.
             _calltip_window_spec = {
                 'file': 'calltip_w',
                 'kwds': {},
                 'msg': "Typing '(' should display a calltip.\n"
                        "Typing ') should hide the calltip.\n"
            +           "So should moving cursor out of argument area.\n"
            +           "Force-open-calltip does not work here.\n"
                 }
             
             _module_browser_spec = {
            @@ -104,6 +109,16 @@ def _wrapper(parent):  # htest #
                        "The default color scheme is in idlelib/config-highlight.def"
                 }
             
            +CustomRun_spec = {
            +    'file': 'query',
            +    'kwds': {'title': 'Customize query.py Run',
            +             '_htest': True},
            +    'msg': "Enter with  or [Run].  Print valid entry to Shell\n"
            +           "Arguments are parsed into a list\n"
            +           "Mode is currently restart True or False\n"
            +           "Close dialog with valid entry, , [Cancel], [X]"
            +    }
            +
             ConfigDialog_spec = {
                 'file': 'configdialog',
                 'kwds': {'title': 'ConfigDialogTest',
            @@ -113,7 +128,7 @@ def _wrapper(parent):  # htest #
                        "font face of the text in the area below it.\nIn the "
                        "'Highlighting' tab, try different color schemes. Clicking "
                        "items in the sample program should update the choices above it."
            -           "\nIn the 'Keys', 'General' and 'Extensions' tabs, test settings"
            +           "\nIn the 'Keys', 'General' and 'Extensions' tabs, test settings "
                        "of interest."
                        "\n[Ok] to close the dialog.[Apply] to apply the settings and "
                        "and [Cancel] to revert all changes.\nRe-run the test to ensure "
            @@ -137,18 +152,17 @@ def _wrapper(parent):  # htest #
                        "Best to close editor first."
                 }
             
            -# Update once issue21519 is resolved.
             GetKeysDialog_spec = {
                 'file': 'config_key',
                 'kwds': {'title': 'Test keybindings',
                          'action': 'find-again',
            -             'currentKeySequences': [''] ,
            +             'current_key_sequences': [['', '', '']],
                          '_htest': True,
                          },
                 'msg': "Test for different key modifier sequences.\n"
                        " is invalid.\n"
                        "No modifier key is invalid.\n"
            -           "Shift key with [a-z],[0-9], function key, move key, tab, space"
            +           "Shift key with [a-z],[0-9], function key, move key, tab, space "
                        "is invalid.\nNo validity checking if advanced key binding "
                        "entry is used."
                 }
            @@ -159,7 +173,7 @@ def _wrapper(parent):  # htest #
                 'msg': "Click the 'Show GrepDialog' button.\n"
                        "Test the various 'Find-in-files' functions.\n"
                        "The results should be displayed in a new '*Output*' window.\n"
            -           "'Right-click'->'Goto file/line' anywhere in the search results "
            +           "'Right-click'->'Go to file/line' anywhere in the search results "
                        "should open that file \nin a new EditorWindow."
                 }
             
            @@ -192,6 +206,26 @@ def _wrapper(parent):  # htest #
                        "Check that changes were saved by opening the file elsewhere."
                 }
             
            +_linenumbers_drag_scrolling_spec = {
            +    'file': 'sidebar',
            +    'kwds': {},
            +    'msg': textwrap.dedent("""\
            +        1. Click on the line numbers and drag down below the edge of the
            +        window, moving the mouse a bit and then leaving it there for a while.
            +        The text and line numbers should gradually scroll down, with the
            +        selection updated continuously.
            +
            +        2. With the lines still selected, click on a line number above the
            +        selected lines. Only the line whose number was clicked should be
            +        selected.
            +
            +        3. Repeat step #1, dragging to above the window. The text and line
            +        numbers should gradually scroll up, with the selection updated
            +        continuously.
            +
            +        4. Repeat step #2, clicking a line number below the selection."""),
            +    }
            +
             _multi_call_spec = {
                 'file': 'multicall',
                 'kwds': {},
            @@ -230,7 +264,7 @@ def _wrapper(parent):  # htest #
                 'file': 'percolator',
                 'kwds': {},
                 'msg': "There are two tracers which can be toggled using a checkbox.\n"
            -           "Toggling a tracer 'on' by checking it should print tracer"
            +           "Toggling a tracer 'on' by checking it should print tracer "
                        "output to the console or to the IDLE shell.\n"
                        "If both the tracers are 'on', the output from the tracer which "
                        "was switched 'on' later, should be printed first\n"
            @@ -296,16 +330,6 @@ def _wrapper(parent):  # htest #
                        "Check that exc_value, exc_tb, and exc_type are correct.\n"
                 }
             
            -_tabbed_pages_spec = {
            -    'file': 'tabbedpages',
            -    'kwds': {},
            -    'msg': "Toggle between the two tabs 'foo' and 'bar'\n"
            -           "Add a tab by entering a suitable name for it.\n"
            -           "Remove an existing tab by entering its name.\n"
            -           "Remove all existing tabs.\n"
            -           " is an invalid add page and remove page name.\n"
            -    }
            -
             _tooltip_spec = {
                 'file': 'tooltip',
                 'kwds': {},
            @@ -332,7 +356,7 @@ def _wrapper(parent):  # htest #
             ViewWindow_spec = {
                 'file': 'textview',
                 'kwds': {'title': 'Test textview',
            -             'text': 'The quick brown fox jumps over the lazy dog.\n'*35,
            +             'contents': 'The quick brown fox jumps over the lazy dog.\n'*35,
                          '_htest': True},
                 'msg': "Test for read-only property of text.\n"
                        "Select text, scroll window, close"
            @@ -341,7 +365,7 @@ def _wrapper(parent):  # htest #
             _widget_redirector_spec = {
                 'file': 'redirector',
                 'kwds': {},
            -    'msg': "Every text insert should be printed to the console."
            +    'msg': "Every text insert should be printed to the console "
                        "or the IDLE shell."
                 }
             
            diff --git a/Lib/idlelib/idle_test/mock_idle.py b/Lib/idlelib/idle_test/mock_idle.py
            index f279a52fd511f0..71fa480ce4d05c 100644
            --- a/Lib/idlelib/idle_test/mock_idle.py
            +++ b/Lib/idlelib/idle_test/mock_idle.py
            @@ -40,8 +40,9 @@ def __call__(self, *args, **kwds):
             class Editor:
                 '''Minimally imitate editor.EditorWindow class.
                 '''
            -    def __init__(self, flist=None, filename=None, key=None, root=None):
            -        self.text = Text()
            +    def __init__(self, flist=None, filename=None, key=None, root=None,
            +                 text=None):  # Allow real Text with mock Editor.
            +        self.text = text or Text()
                     self.undo = UndoDelegator()
             
                 def get_selection_indices(self):
            diff --git a/Lib/idlelib/idle_test/mock_tk.py b/Lib/idlelib/idle_test/mock_tk.py
            index 6e351297d75db9..576f7d5d609e4d 100644
            --- a/Lib/idlelib/idle_test/mock_tk.py
            +++ b/Lib/idlelib/idle_test/mock_tk.py
            @@ -37,7 +37,7 @@ class Mbox_func:
                 """Generic mock for messagebox functions, which all have the same signature.
             
                 Instead of displaying a message box, the mock's call method saves the
            -    arguments as instance attributes, which test functions can then examime.
            +    arguments as instance attributes, which test functions can then examine.
                 The test can set the result returned to ask function
                 """
                 def __init__(self, result=None):
            @@ -260,7 +260,7 @@ def compare(self, index1, op, index2):
                     elif op == '!=':
                         return line1 != line2 or  char1 != char2
                     else:
            -            raise TclError('''bad comparison operator "%s":'''
            +            raise TclError('''bad comparison operator "%s": '''
                                               '''must be <, <=, ==, >=, >, or !=''' % op)
             
                 # The following Text methods normally do something and return None.
            diff --git a/Lib/idlelib/idle_test/template.py b/Lib/idlelib/idle_test/template.py
            new file mode 100644
            index 00000000000000..725a55b9c47230
            --- /dev/null
            +++ b/Lib/idlelib/idle_test/template.py
            @@ -0,0 +1,30 @@
            +"Test , coverage %."
            +
            +from idlelib import zzdummy
            +import unittest
            +from test.support import requires
            +from tkinter import Tk
            +
            +
            +class Test(unittest.TestCase):
            +
            +    @classmethod
            +    def setUpClass(cls):
            +        requires('gui')
            +        cls.root = Tk()
            +        cls.root.withdraw()
            +
            +    @classmethod
            +    def tearDownClass(cls):
            +        cls.root.update_idletasks()
            +##        for id in cls.root.tk.call('after', 'info'):
            +##            cls.root.after_cancel(id)  # Need for EditorWindow.
            +        cls.root.destroy()
            +        del cls.root
            +
            +    def test_init(self):
            +        self.assertTrue(True)
            +
            +
            +if __name__ == '__main__':
            +    unittest.main(verbosity=2)
            diff --git a/Lib/idlelib/idle_test/test_autocomplete.py b/Lib/idlelib/idle_test/test_autocomplete.py
            index f3f2dea4246df0..1841495fcf1a0c 100644
            --- a/Lib/idlelib/idle_test/test_autocomplete.py
            +++ b/Lib/idlelib/idle_test/test_autocomplete.py
            @@ -1,19 +1,17 @@
            -''' Test autocomplete and autocomple_w
            +"Test autocomplete, coverage 93%."
             
            -Coverage of autocomple: 56%
            -'''
             import unittest
            +from unittest.mock import Mock, patch
             from test.support import requires
             from tkinter import Tk, Text
            +import os
            +import __main__
             
             import idlelib.autocomplete as ac
             import idlelib.autocomplete_w as acw
             from idlelib.idle_test.mock_idle import Func
             from idlelib.idle_test.mock_tk import Event
             
            -class AutoCompleteWindow:
            -    def complete():
            -        return
             
             class DummyEditwin:
                 def __init__(self, root, text):
            @@ -21,7 +19,7 @@ def __init__(self, root, text):
                     self.text = text
                     self.indentwidth = 8
                     self.tabwidth = 8
            -        self.context_use_ps1 = True
            +        self.prompt_last_line = '>>>'  # Currently not used by autocomplete.
             
             
             class AutoCompleteTest(unittest.TestCase):
            @@ -30,119 +28,269 @@ class AutoCompleteTest(unittest.TestCase):
                 def setUpClass(cls):
                     requires('gui')
                     cls.root = Tk()
            +        cls.root.withdraw()
                     cls.text = Text(cls.root)
                     cls.editor = DummyEditwin(cls.root, cls.text)
             
                 @classmethod
                 def tearDownClass(cls):
                     del cls.editor, cls.text
            +        cls.root.update_idletasks()
                     cls.root.destroy()
                     del cls.root
             
                 def setUp(self):
            -        self.editor.text.delete('1.0', 'end')
            +        self.text.delete('1.0', 'end')
                     self.autocomplete = ac.AutoComplete(self.editor)
             
                 def test_init(self):
                     self.assertEqual(self.autocomplete.editwin, self.editor)
            +        self.assertEqual(self.autocomplete.text, self.text)
             
                 def test_make_autocomplete_window(self):
                     testwin = self.autocomplete._make_autocomplete_window()
                     self.assertIsInstance(testwin, acw.AutoCompleteWindow)
             
                 def test_remove_autocomplete_window(self):
            -        self.autocomplete.autocompletewindow = (
            -            self.autocomplete._make_autocomplete_window())
            -        self.autocomplete._remove_autocomplete_window()
            -        self.assertIsNone(self.autocomplete.autocompletewindow)
            +        acp = self.autocomplete
            +        acp.autocompletewindow = m = Mock()
            +        acp._remove_autocomplete_window()
            +        m.hide_window.assert_called_once()
            +        self.assertIsNone(acp.autocompletewindow)
             
                 def test_force_open_completions_event(self):
            -        # Test that force_open_completions_event calls _open_completions
            -        o_cs = Func()
            -        self.autocomplete.open_completions = o_cs
            -        self.autocomplete.force_open_completions_event('event')
            -        self.assertEqual(o_cs.args, (True, False, True))
            -
            -    def test_try_open_completions_event(self):
            -        Equal = self.assertEqual
            -        autocomplete = self.autocomplete
            -        trycompletions = self.autocomplete.try_open_completions_event
            -        o_c_l = Func()
            -        autocomplete._open_completions_later = o_c_l
            -
            -        # _open_completions_later should not be called with no text in editor
            -        trycompletions('event')
            -        Equal(o_c_l.args, None)
            -
            -        # _open_completions_later should be called with COMPLETE_ATTRIBUTES (1)
            -        self.text.insert('1.0', 're.')
            -        trycompletions('event')
            -        Equal(o_c_l.args, (False, False, False, 1))
            -
            -        # _open_completions_later should be called with COMPLETE_FILES (2)
            -        self.text.delete('1.0', 'end')
            -        self.text.insert('1.0', '"./Lib/')
            -        trycompletions('event')
            -        Equal(o_c_l.args, (False, False, False, 2))
            +        # Call _open_completions and break.
            +        acp = self.autocomplete
            +        open_c = Func()
            +        acp.open_completions = open_c
            +        self.assertEqual(acp.force_open_completions_event('event'), 'break')
            +        self.assertEqual(open_c.args[0], ac.FORCE)
             
                 def test_autocomplete_event(self):
                     Equal = self.assertEqual
            -        autocomplete = self.autocomplete
            +        acp = self.autocomplete
             
            -        # Test that the autocomplete event is ignored if user is pressing a
            -        # modifier key in addition to the tab key
            +        # Result of autocomplete event: If modified tab, None.
                     ev = Event(mc_state=True)
            -        self.assertIsNone(autocomplete.autocomplete_event(ev))
            +        self.assertIsNone(acp.autocomplete_event(ev))
                     del ev.mc_state
             
            -        # Test that tab after whitespace is ignored.
            +        # If tab after whitespace, None.
                     self.text.insert('1.0', '        """Docstring.\n    ')
            -        self.assertIsNone(autocomplete.autocomplete_event(ev))
            +        self.assertIsNone(acp.autocomplete_event(ev))
                     self.text.delete('1.0', 'end')
             
            -        # If autocomplete window is open, complete() method is called
            +        # If active autocomplete window, complete() and 'break'.
                     self.text.insert('1.0', 're.')
            -        # This must call autocomplete._make_autocomplete_window()
            -        Equal(self.autocomplete.autocomplete_event(ev), 'break')
            -
            -        # If autocomplete window is not active or does not exist,
            -        # open_completions is called. Return depends on its return.
            -        autocomplete._remove_autocomplete_window()
            -        o_cs = Func()  # .result = None
            -        autocomplete.open_completions = o_cs
            -        Equal(self.autocomplete.autocomplete_event(ev), None)
            -        Equal(o_cs.args, (False, True, True))
            -        o_cs.result = True
            -        Equal(self.autocomplete.autocomplete_event(ev), 'break')
            -        Equal(o_cs.args, (False, True, True))
            -
            -    def test_open_completions_later(self):
            -        # Test that autocomplete._delayed_completion_id is set
            -        pass
            +        acp.autocompletewindow = mock = Mock()
            +        mock.is_active = Mock(return_value=True)
            +        Equal(acp.autocomplete_event(ev), 'break')
            +        mock.complete.assert_called_once()
            +        acp.autocompletewindow = None
            +
            +        # If no active autocomplete window, open_completions(), None/break.
            +        open_c = Func(result=False)
            +        acp.open_completions = open_c
            +        Equal(acp.autocomplete_event(ev), None)
            +        Equal(open_c.args[0], ac.TAB)
            +        open_c.result = True
            +        Equal(acp.autocomplete_event(ev), 'break')
            +        Equal(open_c.args[0], ac.TAB)
            +
            +    def test_try_open_completions_event(self):
            +        Equal = self.assertEqual
            +        text = self.text
            +        acp = self.autocomplete
            +        trycompletions = acp.try_open_completions_event
            +        after = Func(result='after1')
            +        acp.text.after = after
            +
            +        # If no text or trigger, after not called.
            +        trycompletions()
            +        Equal(after.called, 0)
            +        text.insert('1.0', 're')
            +        trycompletions()
            +        Equal(after.called, 0)
            +
            +        # Attribute needed, no existing callback.
            +        text.insert('insert', ' re.')
            +        acp._delayed_completion_id = None
            +        trycompletions()
            +        Equal(acp._delayed_completion_index, text.index('insert'))
            +        Equal(after.args,
            +              (acp.popupwait, acp._delayed_open_completions, ac.TRY_A))
            +        cb1 = acp._delayed_completion_id
            +        Equal(cb1, 'after1')
            +
            +        # File needed, existing callback cancelled.
            +        text.insert('insert', ' "./Lib/')
            +        after.result = 'after2'
            +        cancel = Func()
            +        acp.text.after_cancel = cancel
            +        trycompletions()
            +        Equal(acp._delayed_completion_index, text.index('insert'))
            +        Equal(cancel.args, (cb1,))
            +        Equal(after.args,
            +              (acp.popupwait, acp._delayed_open_completions, ac.TRY_F))
            +        Equal(acp._delayed_completion_id, 'after2')
             
                 def test_delayed_open_completions(self):
            -        # Test that autocomplete._delayed_completion_id set to None and that
            -        # open_completions only called if insertion index is the same as
            -        # _delayed_completion_index
            -        pass
            +        Equal = self.assertEqual
            +        acp = self.autocomplete
            +        open_c = Func()
            +        acp.open_completions = open_c
            +        self.text.insert('1.0', '"dict.')
            +
            +        # Set autocomplete._delayed_completion_id to None.
            +        # Text index changed, don't call open_completions.
            +        acp._delayed_completion_id = 'after'
            +        acp._delayed_completion_index = self.text.index('insert+1c')
            +        acp._delayed_open_completions('dummy')
            +        self.assertIsNone(acp._delayed_completion_id)
            +        Equal(open_c.called, 0)
            +
            +        # Text index unchanged, call open_completions.
            +        acp._delayed_completion_index = self.text.index('insert')
            +        acp._delayed_open_completions((1, 2, 3, ac.FILES))
            +        self.assertEqual(open_c.args[0], (1, 2, 3, ac.FILES))
            +
            +    def test_oc_cancel_comment(self):
            +        none = self.assertIsNone
            +        acp = self.autocomplete
            +
            +        # Comment is in neither code or string.
            +        acp._delayed_completion_id = 'after'
            +        after = Func(result='after')
            +        acp.text.after_cancel = after
            +        self.text.insert(1.0, '# comment')
            +        none(acp.open_completions(ac.TAB))  # From 'else' after 'elif'.
            +        none(acp._delayed_completion_id)
            +
            +    def test_oc_no_list(self):
            +        acp = self.autocomplete
            +        fetch = Func(result=([],[]))
            +        acp.fetch_completions = fetch
            +        self.text.insert('1.0', 'object')
            +        self.assertIsNone(acp.open_completions(ac.TAB))
            +        self.text.insert('insert', '.')
            +        self.assertIsNone(acp.open_completions(ac.TAB))
            +        self.assertEqual(fetch.called, 2)
            +
            +
            +    def test_open_completions_none(self):
            +        # Test other two None returns.
            +        none = self.assertIsNone
            +        acp = self.autocomplete
            +
            +        # No object for attributes or need call not allowed.
            +        self.text.insert(1.0, '.')
            +        none(acp.open_completions(ac.TAB))
            +        self.text.insert('insert', ' int().')
            +        none(acp.open_completions(ac.TAB))
            +
            +        # Blank or quote trigger 'if complete ...'.
            +        self.text.delete(1.0, 'end')
            +        self.assertFalse(acp.open_completions(ac.TAB))
            +        self.text.insert('1.0', '"')
            +        self.assertFalse(acp.open_completions(ac.TAB))
            +        self.text.delete('1.0', 'end')
            +
            +    class dummy_acw():
            +        __init__ = Func()
            +        show_window = Func(result=False)
            +        hide_window = Func()
             
                 def test_open_completions(self):
            -        # Test completions of files and attributes as well as non-completion
            -        # of errors
            -        pass
            +        # Test completions of files and attributes.
            +        acp = self.autocomplete
            +        fetch = Func(result=(['tem'],['tem', '_tem']))
            +        acp.fetch_completions = fetch
            +        def make_acw(): return self.dummy_acw()
            +        acp._make_autocomplete_window = make_acw
            +
            +        self.text.insert('1.0', 'int.')
            +        acp.open_completions(ac.TAB)
            +        self.assertIsInstance(acp.autocompletewindow, self.dummy_acw)
            +        self.text.delete('1.0', 'end')
            +
            +        # Test files.
            +        self.text.insert('1.0', '"t')
            +        self.assertTrue(acp.open_completions(ac.TAB))
            +        self.text.delete('1.0', 'end')
             
                 def test_fetch_completions(self):
                     # Test that fetch_completions returns 2 lists:
                     # For attribute completion, a large list containing all variables, and
                     # a small list containing non-private variables.
                     # For file completion, a large list containing all files in the path,
            -        # and a small list containing files that do not start with '.'
            -        pass
            +        # and a small list containing files that do not start with '.'.
            +        acp = self.autocomplete
            +        small, large = acp.fetch_completions(
            +                '', ac.ATTRS)
            +        if hasattr(__main__, '__file__') and __main__.__file__ != ac.__file__:
            +            self.assertNotIn('AutoComplete', small)  # See issue 36405.
            +
            +        # Test attributes
            +        s, b = acp.fetch_completions('', ac.ATTRS)
            +        self.assertLess(len(small), len(large))
            +        self.assertTrue(all(filter(lambda x: x.startswith('_'), s)))
            +        self.assertTrue(any(filter(lambda x: x.startswith('_'), b)))
            +
            +        # Test smalll should respect to __all__.
            +        with patch.dict('__main__.__dict__', {'__all__': ['a', 'b']}):
            +            s, b = acp.fetch_completions('', ac.ATTRS)
            +            self.assertEqual(s, ['a', 'b'])
            +            self.assertIn('__name__', b)    # From __main__.__dict__
            +            self.assertIn('sum', b)         # From __main__.__builtins__.__dict__
            +
            +        # Test attributes with name entity.
            +        mock = Mock()
            +        mock._private = Mock()
            +        with patch.dict('__main__.__dict__', {'foo': mock}):
            +            s, b = acp.fetch_completions('foo', ac.ATTRS)
            +            self.assertNotIn('_private', s)
            +            self.assertIn('_private', b)
            +            self.assertEqual(s, [i for i in sorted(dir(mock)) if i[:1] != '_'])
            +            self.assertEqual(b, sorted(dir(mock)))
            +
            +        # Test files
            +        def _listdir(path):
            +            # This will be patch and used in fetch_completions.
            +            if path == '.':
            +                return ['foo', 'bar', '.hidden']
            +            return ['monty', 'python', '.hidden']
            +
            +        with patch.object(os, 'listdir', _listdir):
            +            s, b = acp.fetch_completions('', ac.FILES)
            +            self.assertEqual(s, ['bar', 'foo'])
            +            self.assertEqual(b, ['.hidden', 'bar', 'foo'])
            +
            +            s, b = acp.fetch_completions('~', ac.FILES)
            +            self.assertEqual(s, ['monty', 'python'])
            +            self.assertEqual(b, ['.hidden', 'monty', 'python'])
             
                 def test_get_entity(self):
                     # Test that a name is in the namespace of sys.modules and
            -        # __main__.__dict__
            -        pass
            +        # __main__.__dict__.
            +        acp = self.autocomplete
            +        Equal = self.assertEqual
            +
            +        Equal(acp.get_entity('int'), int)
            +
            +        # Test name from sys.modules.
            +        mock = Mock()
            +        with patch.dict('sys.modules', {'tempfile': mock}):
            +            Equal(acp.get_entity('tempfile'), mock)
            +
            +        # Test name from __main__.__dict__.
            +        di = {'foo': 10, 'bar': 20}
            +        with patch.dict('__main__.__dict__', {'d': di}):
            +            Equal(acp.get_entity('d'), di)
            +
            +        # Test name not in namespace.
            +        with patch.dict('__main__.__dict__', {}):
            +            with self.assertRaises(NameError):
            +                acp.get_entity('not_exist')
             
             
             if __name__ == '__main__':
            diff --git a/Lib/idlelib/idle_test/test_autocomplete_w.py b/Lib/idlelib/idle_test/test_autocomplete_w.py
            new file mode 100644
            index 00000000000000..b1bdc6c7c6e1a5
            --- /dev/null
            +++ b/Lib/idlelib/idle_test/test_autocomplete_w.py
            @@ -0,0 +1,32 @@
            +"Test autocomplete_w, coverage 11%."
            +
            +import unittest
            +from test.support import requires
            +from tkinter import Tk, Text
            +
            +import idlelib.autocomplete_w as acw
            +
            +
            +class AutoCompleteWindowTest(unittest.TestCase):
            +
            +    @classmethod
            +    def setUpClass(cls):
            +        requires('gui')
            +        cls.root = Tk()
            +        cls.root.withdraw()
            +        cls.text = Text(cls.root)
            +        cls.acw = acw.AutoCompleteWindow(cls.text)
            +
            +    @classmethod
            +    def tearDownClass(cls):
            +        del cls.text, cls.acw
            +        cls.root.update_idletasks()
            +        cls.root.destroy()
            +        del cls.root
            +
            +    def test_init(self):
            +        self.assertEqual(self.acw.widget, self.text)
            +
            +
            +if __name__ == '__main__':
            +    unittest.main(verbosity=2)
            diff --git a/Lib/idlelib/idle_test/test_autoexpand.py b/Lib/idlelib/idle_test/test_autoexpand.py
            index ae8186cdc49f7b..e734a8be714a2a 100644
            --- a/Lib/idlelib/idle_test/test_autoexpand.py
            +++ b/Lib/idlelib/idle_test/test_autoexpand.py
            @@ -1,12 +1,12 @@
            -"""Unit tests for idlelib.autoexpand"""
            +"Test autoexpand, coverage 100%."
            +
            +from idlelib.autoexpand import AutoExpand
             import unittest
             from test.support import requires
             from tkinter import Text, Tk
            -#from idlelib.idle_test.mock_tk import Text
            -from idlelib.autoexpand import AutoExpand
             
             
            -class Dummy_Editwin:
            +class DummyEditwin:
                 # AutoExpand.__init__ only needs .text
                 def __init__(self, text):
                     self.text = text
            @@ -15,15 +15,27 @@ class AutoExpandTest(unittest.TestCase):
             
                 @classmethod
                 def setUpClass(cls):
            -        if 'tkinter' in str(Text):
            -            requires('gui')
            -            cls.tk = Tk()
            -            cls.text = Text(cls.tk)
            -        else:
            -            cls.text = Text()
            -        cls.auto_expand = AutoExpand(Dummy_Editwin(cls.text))
            +        requires('gui')
            +        cls.tk = Tk()
            +        cls.text = Text(cls.tk)
            +        cls.auto_expand = AutoExpand(DummyEditwin(cls.text))
                     cls.auto_expand.bell = lambda: None
             
            +# If mock_tk.Text._decode understood indexes 'insert' with suffixed 'linestart',
            +# 'wordstart', and 'lineend', used by autoexpand, we could use the following
            +# to run these test on non-gui machines (but check bell).
            +##        try:
            +##            requires('gui')
            +##            #raise ResourceDenied()  # Uncomment to test mock.
            +##        except ResourceDenied:
            +##            from idlelib.idle_test.mock_tk import Text
            +##            cls.text = Text()
            +##            cls.text.bell = lambda: None
            +##        else:
            +##            from tkinter import Tk, Text
            +##            cls.tk = Tk()
            +##            cls.text = Text(cls.tk)
            +
                 @classmethod
                 def tearDownClass(cls):
                     del cls.text, cls.auto_expand
            diff --git a/Lib/idlelib/idle_test/test_browser.py b/Lib/idlelib/idle_test/test_browser.py
            index 34eb332c1df434..25d6dc6630364b 100644
            --- a/Lib/idlelib/idle_test/test_browser.py
            +++ b/Lib/idlelib/idle_test/test_browser.py
            @@ -1,21 +1,16 @@
            -""" Test idlelib.browser.
            +"Test browser, coverage 90%."
             
            -Coverage: 88%
            -(Higher, because should exclude 3 lines that .coveragerc won't exclude.)
            -"""
            +from idlelib import browser
            +from test.support import requires
            +import unittest
            +from unittest import mock
            +from idlelib.idle_test.mock_idle import Func
             
             from collections import deque
             import os.path
             import pyclbr
             from tkinter import Tk
             
            -from test.support import requires
            -import unittest
            -from unittest import mock
            -from idlelib.idle_test.mock_idle import Func
            -
            -from idlelib import browser
            -from idlelib import filelist
             from idlelib.tree import TreeNode
             
             
            @@ -66,16 +61,16 @@ def test_close(self):
             # Nested tree same as in test_pyclbr.py except for supers on C0. C1.
             mb = pyclbr
             module, fname = 'test', 'test.py'
            -f0 = mb.Function(module, 'f0', fname, 1)
            -f1 = mb._nest_function(f0, 'f1', 2)
            -f2 = mb._nest_function(f1, 'f2', 3)
            -c1 = mb._nest_class(f0, 'c1', 5)
            -C0 = mb.Class(module, 'C0', ['base'], fname, 6)
            -F1 = mb._nest_function(C0, 'F1', 8)
            -C1 = mb._nest_class(C0, 'C1', 11, [''])
            -C2 = mb._nest_class(C1, 'C2', 12)
            -F3 = mb._nest_function(C2, 'F3', 14)
            -mock_pyclbr_tree = {'f0': f0, 'C0': C0}
            +C0 = mb.Class(module, 'C0', ['base'], fname, 1)
            +F1 = mb._nest_function(C0, 'F1', 3)
            +C1 = mb._nest_class(C0, 'C1', 6, [''])
            +C2 = mb._nest_class(C1, 'C2', 7)
            +F3 = mb._nest_function(C2, 'F3', 9)
            +f0 = mb.Function(module, 'f0', fname, 11)
            +f1 = mb._nest_function(f0, 'f1', 12)
            +f2 = mb._nest_function(f1, 'f2', 13)
            +c1 = mb._nest_class(f0, 'c1', 15)
            +mock_pyclbr_tree = {'C0': C0, 'f0': f0}
             
             # Adjust C0.name, C1.name so tests do not depend on order.
             browser.transform_children(mock_pyclbr_tree, 'test')  # C0(base)
            @@ -92,12 +87,12 @@ def test_transform_module_children(self):
                     transform = browser.transform_children
                     # Parameter matches tree module.
                     tcl = list(transform(mock_pyclbr_tree, 'test'))
            -        eq(tcl, [f0, C0])
            -        eq(tcl[0].name, 'f0')
            -        eq(tcl[1].name, 'C0(base)')
            +        eq(tcl, [C0, f0])
            +        eq(tcl[0].name, 'C0(base)')
            +        eq(tcl[1].name, 'f0')
                     # Check that second call does not change suffix.
                     tcl = list(transform(mock_pyclbr_tree, 'test'))
            -        eq(tcl[1].name, 'C0(base)')
            +        eq(tcl[0].name, 'C0(base)')
                     # Nothing to traverse if parameter name isn't same as tree module.
                     tcl = list(transform(mock_pyclbr_tree, 'different name'))
                     eq(tcl, [])
            diff --git a/Lib/idlelib/idle_test/test_calltip.py b/Lib/idlelib/idle_test/test_calltip.py
            new file mode 100644
            index 00000000000000..d386b5cd813212
            --- /dev/null
            +++ b/Lib/idlelib/idle_test/test_calltip.py
            @@ -0,0 +1,263 @@
            +"Test calltip, coverage 60%"
            +
            +from idlelib import calltip
            +import unittest
            +import textwrap
            +import types
            +import re
            +
            +
            +# Test Class TC is used in multiple get_argspec test methods
            +class TC():
            +    'doc'
            +    tip = "(ai=None, *b)"
            +    def __init__(self, ai=None, *b): 'doc'
            +    __init__.tip = "(self, ai=None, *b)"
            +    def t1(self): 'doc'
            +    t1.tip = "(self)"
            +    def t2(self, ai, b=None): 'doc'
            +    t2.tip = "(self, ai, b=None)"
            +    def t3(self, ai, *args): 'doc'
            +    t3.tip = "(self, ai, *args)"
            +    def t4(self, *args): 'doc'
            +    t4.tip = "(self, *args)"
            +    def t5(self, ai, b=None, *args, **kw): 'doc'
            +    t5.tip = "(self, ai, b=None, *args, **kw)"
            +    def t6(no, self): 'doc'
            +    t6.tip = "(no, self)"
            +    def __call__(self, ci): 'doc'
            +    __call__.tip = "(self, ci)"
            +    def nd(self): pass  # No doc.
            +    # attaching .tip to wrapped methods does not work
            +    @classmethod
            +    def cm(cls, a): 'doc'
            +    @staticmethod
            +    def sm(b): 'doc'
            +
            +
            +tc = TC()
            +default_tip = calltip._default_callable_argspec
            +get_spec = calltip.get_argspec
            +
            +
            +class Get_argspecTest(unittest.TestCase):
            +    # The get_spec function must return a string, even if blank.
            +    # Test a variety of objects to be sure that none cause it to raise
            +    # (quite aside from getting as correct an answer as possible).
            +    # The tests of builtins may break if inspect or the docstrings change,
            +    # but a red buildbot is better than a user crash (as has happened).
            +    # For a simple mismatch, change the expected output to the actual.
            +
            +    def test_builtins(self):
            +
            +        def tiptest(obj, out):
            +            self.assertEqual(get_spec(obj), out)
            +
            +        # Python class that inherits builtin methods
            +        class List(list): "List() doc"
            +
            +        # Simulate builtin with no docstring for default tip test
            +        class SB:  __call__ = None
            +
            +        if List.__doc__ is not None:
            +            tiptest(List,
            +                    f'(iterable=(), /){calltip._argument_positional}'
            +                    f'\n{List.__doc__}')
            +        tiptest(list.__new__,
            +              '(*args, **kwargs)\n'
            +              'Create and return a new object.  '
            +              'See help(type) for accurate signature.')
            +        tiptest(list.__init__,
            +              '(self, /, *args, **kwargs)'
            +              + calltip._argument_positional + '\n' +
            +              'Initialize self.  See help(type(self)) for accurate signature.')
            +        append_doc = (calltip._argument_positional
            +                      + "\nAppend object to the end of the list.")
            +        tiptest(list.append, '(self, object, /)' + append_doc)
            +        tiptest(List.append, '(self, object, /)' + append_doc)
            +        tiptest([].append, '(object, /)' + append_doc)
            +
            +        tiptest(types.MethodType, "method(function, instance)")
            +        tiptest(SB(), default_tip)
            +
            +        p = re.compile('')
            +        tiptest(re.sub, '''\
            +(pattern, repl, string, count=0, flags=0)
            +Return the string obtained by replacing the leftmost
            +non-overlapping occurrences of the pattern in string by the
            +replacement repl.  repl can be either a string or a callable;
            +if a string, backslash escapes in it are processed.  If it is
            +a callable, it's passed the Match object and must return''')
            +        tiptest(p.sub, '''\
            +(repl, string, count=0)
            +Return the string obtained by replacing the leftmost \
            +non-overlapping occurrences o...''')
            +
            +    def test_signature_wrap(self):
            +        if textwrap.TextWrapper.__doc__ is not None:
            +            self.assertEqual(get_spec(textwrap.TextWrapper), '''\
            +(width=70, initial_indent='', subsequent_indent='', expand_tabs=True,
            +    replace_whitespace=True, fix_sentence_endings=False, break_long_words=True,
            +    drop_whitespace=True, break_on_hyphens=True, tabsize=8, *, max_lines=None,
            +    placeholder=' [...]')''')
            +
            +    def test_properly_formated(self):
            +
            +        def foo(s='a'*100):
            +            pass
            +
            +        def bar(s='a'*100):
            +            """Hello Guido"""
            +            pass
            +
            +        def baz(s='a'*100, z='b'*100):
            +            pass
            +
            +        indent = calltip._INDENT
            +
            +        sfoo = "(s='aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"\
            +               "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n" + indent + "aaaaaaaaa"\
            +               "aaaaaaaaaa')"
            +        sbar = "(s='aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"\
            +               "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n" + indent + "aaaaaaaaa"\
            +               "aaaaaaaaaa')\nHello Guido"
            +        sbaz = "(s='aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"\
            +               "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n" + indent + "aaaaaaaaa"\
            +               "aaaaaaaaaa', z='bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"\
            +               "bbbbbbbbbbbbbbbbb\n" + indent + "bbbbbbbbbbbbbbbbbbbbbb"\
            +               "bbbbbbbbbbbbbbbbbbbbbb')"
            +
            +        for func,doc in [(foo, sfoo), (bar, sbar), (baz, sbaz)]:
            +            with self.subTest(func=func, doc=doc):
            +                self.assertEqual(get_spec(func), doc)
            +
            +    def test_docline_truncation(self):
            +        def f(): pass
            +        f.__doc__ = 'a'*300
            +        self.assertEqual(get_spec(f), f"()\n{'a'*(calltip._MAX_COLS-3) + '...'}")
            +
            +    def test_multiline_docstring(self):
            +        # Test fewer lines than max.
            +        self.assertEqual(get_spec(range),
            +                "range(stop) -> range object\n"
            +                "range(start, stop[, step]) -> range object")
            +
            +        # Test max lines
            +        self.assertEqual(get_spec(bytes), '''\
            +bytes(iterable_of_ints) -> bytes
            +bytes(string, encoding[, errors]) -> bytes
            +bytes(bytes_or_buffer) -> immutable copy of bytes_or_buffer
            +bytes(int) -> bytes object of size given by the parameter initialized with null bytes
            +bytes() -> empty bytes object''')
            +
            +        # Test more than max lines
            +        def f(): pass
            +        f.__doc__ = 'a\n' * 15
            +        self.assertEqual(get_spec(f), '()' + '\na' * calltip._MAX_LINES)
            +
            +    def test_functions(self):
            +        def t1(): 'doc'
            +        t1.tip = "()"
            +        def t2(a, b=None): 'doc'
            +        t2.tip = "(a, b=None)"
            +        def t3(a, *args): 'doc'
            +        t3.tip = "(a, *args)"
            +        def t4(*args): 'doc'
            +        t4.tip = "(*args)"
            +        def t5(a, b=None, *args, **kw): 'doc'
            +        t5.tip = "(a, b=None, *args, **kw)"
            +
            +        doc = '\ndoc' if t1.__doc__ is not None else ''
            +        for func in (t1, t2, t3, t4, t5, TC):
            +            with self.subTest(func=func):
            +                self.assertEqual(get_spec(func), func.tip + doc)
            +
            +    def test_methods(self):
            +        doc = '\ndoc' if TC.__doc__ is not None else ''
            +        for meth in (TC.t1, TC.t2, TC.t3, TC.t4, TC.t5, TC.t6, TC.__call__):
            +            with self.subTest(meth=meth):
            +                self.assertEqual(get_spec(meth), meth.tip + doc)
            +        self.assertEqual(get_spec(TC.cm), "(a)" + doc)
            +        self.assertEqual(get_spec(TC.sm), "(b)" + doc)
            +
            +    def test_bound_methods(self):
            +        # test that first parameter is correctly removed from argspec
            +        doc = '\ndoc' if TC.__doc__ is not None else ''
            +        for meth, mtip  in ((tc.t1, "()"), (tc.t4, "(*args)"),
            +                            (tc.t6, "(self)"), (tc.__call__, '(ci)'),
            +                            (tc, '(ci)'), (TC.cm, "(a)"),):
            +            with self.subTest(meth=meth, mtip=mtip):
            +                self.assertEqual(get_spec(meth), mtip + doc)
            +
            +    def test_starred_parameter(self):
            +        # test that starred first parameter is *not* removed from argspec
            +        class C:
            +            def m1(*args): pass
            +        c = C()
            +        for meth, mtip  in ((C.m1, '(*args)'), (c.m1, "(*args)"),):
            +            with self.subTest(meth=meth, mtip=mtip):
            +                self.assertEqual(get_spec(meth), mtip)
            +
            +    def test_invalid_method_get_spec(self):
            +        class C:
            +            def m2(**kwargs): pass
            +        class Test:
            +            def __call__(*, a): pass
            +
            +        mtip = calltip._invalid_method
            +        self.assertEqual(get_spec(C().m2), mtip)
            +        self.assertEqual(get_spec(Test()), mtip)
            +
            +    def test_non_ascii_name(self):
            +        # test that re works to delete a first parameter name that
            +        # includes non-ascii chars, such as various forms of A.
            +        uni = "(A\u0391\u0410\u05d0\u0627\u0905\u1e00\u3042, a)"
            +        assert calltip._first_param.sub('', uni) == '(a)'
            +
            +    def test_no_docstring(self):
            +        for meth, mtip in ((TC.nd, "(self)"), (tc.nd, "()")):
            +            with self.subTest(meth=meth, mtip=mtip):
            +                self.assertEqual(get_spec(meth), mtip)
            +
            +    def test_buggy_getattr_class(self):
            +        class NoCall:
            +            def __getattr__(self, name):  # Not invoked for class attribute.
            +                raise IndexError  # Bug.
            +        class CallA(NoCall):
            +            def __call__(self, ci):  # Bug does not matter.
            +                pass
            +        class CallB(NoCall):
            +            def __call__(oui, a, b, c):  # Non-standard 'self'.
            +                pass
            +
            +        for meth, mtip  in ((NoCall, default_tip), (CallA, default_tip),
            +                            (NoCall(), ''), (CallA(), '(ci)'),
            +                            (CallB(), '(a, b, c)')):
            +            with self.subTest(meth=meth, mtip=mtip):
            +                self.assertEqual(get_spec(meth), mtip)
            +
            +    def test_metaclass_class(self):  # Failure case for issue 38689.
            +        class Type(type):  # Type() requires 3 type args, returns class.
            +            __class__ = property({}.__getitem__, {}.__setitem__)
            +        class Object(metaclass=Type):
            +            __slots__ = '__class__'
            +        for meth, mtip  in ((Type, default_tip), (Object, default_tip),
            +                            (Object(), '')):
            +            with self.subTest(meth=meth, mtip=mtip):
            +                self.assertEqual(get_spec(meth), mtip)
            +
            +    def test_non_callables(self):
            +        for obj in (0, 0.0, '0', b'0', [], {}):
            +            with self.subTest(obj=obj):
            +                self.assertEqual(get_spec(obj), '')
            +
            +
            +class Get_entityTest(unittest.TestCase):
            +    def test_bad_entity(self):
            +        self.assertIsNone(calltip.get_entity('1/0'))
            +    def test_good_entity(self):
            +        self.assertIs(calltip.get_entity('int'), int)
            +
            +
            +if __name__ == '__main__':
            +    unittest.main(verbosity=2)
            diff --git a/Lib/idlelib/idle_test/test_calltip_w.py b/Lib/idlelib/idle_test/test_calltip_w.py
            new file mode 100644
            index 00000000000000..a5ec76e15ffdf3
            --- /dev/null
            +++ b/Lib/idlelib/idle_test/test_calltip_w.py
            @@ -0,0 +1,29 @@
            +"Test calltip_w, coverage 18%."
            +
            +from idlelib import calltip_w
            +import unittest
            +from test.support import requires
            +from tkinter import Tk, Text
            +
            +
            +class CallTipWindowTest(unittest.TestCase):
            +
            +    @classmethod
            +    def setUpClass(cls):
            +        requires('gui')
            +        cls.root = Tk()
            +        cls.root.withdraw()
            +        cls.text = Text(cls.root)
            +        cls.calltip = calltip_w.CalltipWindow(cls.text)
            +
            +    @classmethod
            +    def tearDownClass(cls):
            +        cls.root.update_idletasks()
            +        cls.root.destroy()
            +        del cls.text, cls.root
            +
            +    def test_init(self):
            +        self.assertEqual(self.calltip.anchor_widget, self.text)
            +
            +if __name__ == '__main__':
            +    unittest.main(verbosity=2)
            diff --git a/Lib/idlelib/idle_test/test_calltips.py b/Lib/idlelib/idle_test/test_calltips.py
            deleted file mode 100644
            index a58229d36ede70..00000000000000
            --- a/Lib/idlelib/idle_test/test_calltips.py
            +++ /dev/null
            @@ -1,202 +0,0 @@
            -import unittest
            -import idlelib.calltips as ct
            -import textwrap
            -import types
            -
            -default_tip = ct._default_callable_argspec
            -
            -# Test Class TC is used in multiple get_argspec test methods
            -class TC():
            -    'doc'
            -    tip = "(ai=None, *b)"
            -    def __init__(self, ai=None, *b): 'doc'
            -    __init__.tip = "(self, ai=None, *b)"
            -    def t1(self): 'doc'
            -    t1.tip = "(self)"
            -    def t2(self, ai, b=None): 'doc'
            -    t2.tip = "(self, ai, b=None)"
            -    def t3(self, ai, *args): 'doc'
            -    t3.tip = "(self, ai, *args)"
            -    def t4(self, *args): 'doc'
            -    t4.tip = "(self, *args)"
            -    def t5(self, ai, b=None, *args, **kw): 'doc'
            -    t5.tip = "(self, ai, b=None, *args, **kw)"
            -    def t6(no, self): 'doc'
            -    t6.tip = "(no, self)"
            -    def __call__(self, ci): 'doc'
            -    __call__.tip = "(self, ci)"
            -    # attaching .tip to wrapped methods does not work
            -    @classmethod
            -    def cm(cls, a): 'doc'
            -    @staticmethod
            -    def sm(b): 'doc'
            -
            -tc = TC()
            -
            -signature = ct.get_argspec  # 2.7 and 3.x use different functions
            -class Get_signatureTest(unittest.TestCase):
            -    # The signature function must return a string, even if blank.
            -    # Test a variety of objects to be sure that none cause it to raise
            -    # (quite aside from getting as correct an answer as possible).
            -    # The tests of builtins may break if inspect or the docstrings change,
            -    # but a red buildbot is better than a user crash (as has happened).
            -    # For a simple mismatch, change the expected output to the actual.
            -
            -    def test_builtins(self):
            -
            -        # Python class that inherits builtin methods
            -        class List(list): "List() doc"
            -
            -        # Simulate builtin with no docstring for default tip test
            -        class SB:  __call__ = None
            -
            -        def gtest(obj, out):
            -            self.assertEqual(signature(obj), out)
            -
            -        if List.__doc__ is not None:
            -            gtest(List, '(iterable=(), /)' + ct._argument_positional + '\n' +
            -                  List.__doc__)
            -        gtest(list.__new__,
            -               '(*args, **kwargs)\nCreate and return a new object.  See help(type) for accurate signature.')
            -        gtest(list.__init__,
            -               '(self, /, *args, **kwargs)' + ct._argument_positional + '\n' +
            -               'Initialize self.  See help(type(self)) for accurate signature.')
            -        append_doc = ct._argument_positional + "\nAppend object to the end of the list."
            -        gtest(list.append, '(self, object, /)' + append_doc)
            -        gtest(List.append, '(self, object, /)' + append_doc)
            -        gtest([].append, '(object, /)' + append_doc)
            -
            -        gtest(types.MethodType, "method(function, instance)")
            -        gtest(SB(), default_tip)
            -        import re
            -        p = re.compile('')
            -        gtest(re.sub, '''(pattern, repl, string, count=0, flags=0)\nReturn the string obtained by replacing the leftmost
            -non-overlapping occurrences of the pattern in string by the
            -replacement repl.  repl can be either a string or a callable;
            -if a string, backslash escapes in it are processed.  If it is
            -a callable, it's passed the Match object and must return''')
            -        gtest(p.sub, '''(repl, string, count=0)\nReturn the string obtained by replacing the leftmost non-overlapping occurrences o...''')
            -
            -    def test_signature_wrap(self):
            -        if textwrap.TextWrapper.__doc__ is not None:
            -            self.assertEqual(signature(textwrap.TextWrapper), '''\
            -(width=70, initial_indent='', subsequent_indent='', expand_tabs=True,
            -    replace_whitespace=True, fix_sentence_endings=False, break_long_words=True,
            -    drop_whitespace=True, break_on_hyphens=True, tabsize=8, *, max_lines=None,
            -    placeholder=' [...]')''')
            -
            -    def test_docline_truncation(self):
            -        def f(): pass
            -        f.__doc__ = 'a'*300
            -        self.assertEqual(signature(f), '()\n' + 'a' * (ct._MAX_COLS-3) + '...')
            -
            -    def test_multiline_docstring(self):
            -        # Test fewer lines than max.
            -        self.assertEqual(signature(range),
            -                "range(stop) -> range object\n"
            -                "range(start, stop[, step]) -> range object")
            -
            -        # Test max lines
            -        self.assertEqual(signature(bytes), '''\
            -bytes(iterable_of_ints) -> bytes
            -bytes(string, encoding[, errors]) -> bytes
            -bytes(bytes_or_buffer) -> immutable copy of bytes_or_buffer
            -bytes(int) -> bytes object of size given by the parameter initialized with null bytes
            -bytes() -> empty bytes object''')
            -
            -        # Test more than max lines
            -        def f(): pass
            -        f.__doc__ = 'a\n' * 15
            -        self.assertEqual(signature(f), '()' + '\na' * ct._MAX_LINES)
            -
            -    def test_functions(self):
            -        def t1(): 'doc'
            -        t1.tip = "()"
            -        def t2(a, b=None): 'doc'
            -        t2.tip = "(a, b=None)"
            -        def t3(a, *args): 'doc'
            -        t3.tip = "(a, *args)"
            -        def t4(*args): 'doc'
            -        t4.tip = "(*args)"
            -        def t5(a, b=None, *args, **kw): 'doc'
            -        t5.tip = "(a, b=None, *args, **kw)"
            -
            -        doc = '\ndoc' if t1.__doc__ is not None else ''
            -        for func in (t1, t2, t3, t4, t5, TC):
            -            self.assertEqual(signature(func), func.tip + doc)
            -
            -    def test_methods(self):
            -        doc = '\ndoc' if TC.__doc__ is not None else ''
            -        for meth in (TC.t1, TC.t2, TC.t3, TC.t4, TC.t5, TC.t6, TC.__call__):
            -            self.assertEqual(signature(meth), meth.tip + doc)
            -        self.assertEqual(signature(TC.cm), "(a)" + doc)
            -        self.assertEqual(signature(TC.sm), "(b)" + doc)
            -
            -    def test_bound_methods(self):
            -        # test that first parameter is correctly removed from argspec
            -        doc = '\ndoc' if TC.__doc__ is not None else ''
            -        for meth, mtip  in ((tc.t1, "()"), (tc.t4, "(*args)"), (tc.t6, "(self)"),
            -                            (tc.__call__, '(ci)'), (tc, '(ci)'), (TC.cm, "(a)"),):
            -            self.assertEqual(signature(meth), mtip + doc)
            -
            -    def test_starred_parameter(self):
            -        # test that starred first parameter is *not* removed from argspec
            -        class C:
            -            def m1(*args): pass
            -        c = C()
            -        for meth, mtip  in ((C.m1, '(*args)'), (c.m1, "(*args)"),):
            -            self.assertEqual(signature(meth), mtip)
            -
            -    def test_invalid_method_signature(self):
            -        class C:
            -            def m2(**kwargs): pass
            -        class Test:
            -            def __call__(*, a): pass
            -
            -        mtip = ct._invalid_method
            -        self.assertEqual(signature(C().m2), mtip)
            -        self.assertEqual(signature(Test()), mtip)
            -
            -    def test_non_ascii_name(self):
            -        # test that re works to delete a first parameter name that
            -        # includes non-ascii chars, such as various forms of A.
            -        uni = "(A\u0391\u0410\u05d0\u0627\u0905\u1e00\u3042, a)"
            -        assert ct._first_param.sub('', uni) == '(a)'
            -
            -    def test_no_docstring(self):
            -        def nd(s):
            -            pass
            -        TC.nd = nd
            -        self.assertEqual(signature(nd), "(s)")
            -        self.assertEqual(signature(TC.nd), "(s)")
            -        self.assertEqual(signature(tc.nd), "()")
            -
            -    def test_attribute_exception(self):
            -        class NoCall:
            -            def __getattr__(self, name):
            -                raise BaseException
            -        class CallA(NoCall):
            -            def __call__(oui, a, b, c):
            -                pass
            -        class CallB(NoCall):
            -            def __call__(self, ci):
            -                pass
            -
            -        for meth, mtip  in ((NoCall, default_tip), (CallA, default_tip),
            -                            (NoCall(), ''), (CallA(), '(a, b, c)'),
            -                            (CallB(), '(ci)')):
            -            self.assertEqual(signature(meth), mtip)
            -
            -    def test_non_callables(self):
            -        for obj in (0, 0.0, '0', b'0', [], {}):
            -            self.assertEqual(signature(obj), '')
            -
            -
            -class Get_entityTest(unittest.TestCase):
            -    def test_bad_entity(self):
            -        self.assertIsNone(ct.get_entity('1/0'))
            -    def test_good_entity(self):
            -        self.assertIs(ct.get_entity('int'), int)
            -
            -if __name__ == '__main__':
            -    unittest.main(verbosity=2, exit=False)
            diff --git a/Lib/idlelib/idle_test/test_codecontext.py b/Lib/idlelib/idle_test/test_codecontext.py
            new file mode 100644
            index 00000000000000..9578cc731a6f93
            --- /dev/null
            +++ b/Lib/idlelib/idle_test/test_codecontext.py
            @@ -0,0 +1,455 @@
            +"Test codecontext, coverage 100%"
            +
            +from idlelib import codecontext
            +import unittest
            +import unittest.mock
            +from test.support import requires
            +from tkinter import NSEW, Tk, Frame, Text, TclError
            +
            +from unittest import mock
            +import re
            +from idlelib import config
            +
            +
            +usercfg = codecontext.idleConf.userCfg
            +testcfg = {
            +    'main': config.IdleUserConfParser(''),
            +    'highlight': config.IdleUserConfParser(''),
            +    'keys': config.IdleUserConfParser(''),
            +    'extensions': config.IdleUserConfParser(''),
            +}
            +code_sample = """\
            +
            +class C1():
            +    # Class comment.
            +    def __init__(self, a, b):
            +        self.a = a
            +        self.b = b
            +    def compare(self):
            +        if a > b:
            +            return a
            +        elif a < b:
            +            return b
            +        else:
            +            return None
            +"""
            +
            +
            +class DummyEditwin:
            +    def __init__(self, root, frame, text):
            +        self.root = root
            +        self.top = root
            +        self.text_frame = frame
            +        self.text = text
            +        self.label = ''
            +
            +    def getlineno(self, index):
            +        return int(float(self.text.index(index)))
            +
            +    def update_menu_label(self, **kwargs):
            +        self.label = kwargs['label']
            +
            +
            +class CodeContextTest(unittest.TestCase):
            +
            +    @classmethod
            +    def setUpClass(cls):
            +        requires('gui')
            +        root = cls.root = Tk()
            +        root.withdraw()
            +        frame = cls.frame = Frame(root)
            +        text = cls.text = Text(frame)
            +        text.insert('1.0', code_sample)
            +        # Need to pack for creation of code context text widget.
            +        frame.pack(side='left', fill='both', expand=1)
            +        text.grid(row=1, column=1, sticky=NSEW)
            +        cls.editor = DummyEditwin(root, frame, text)
            +        codecontext.idleConf.userCfg = testcfg
            +
            +    @classmethod
            +    def tearDownClass(cls):
            +        codecontext.idleConf.userCfg = usercfg
            +        cls.editor.text.delete('1.0', 'end')
            +        del cls.editor, cls.frame, cls.text
            +        cls.root.update_idletasks()
            +        cls.root.destroy()
            +        del cls.root
            +
            +    def setUp(self):
            +        self.text.yview(0)
            +        self.text['font'] = 'TkFixedFont'
            +        self.cc = codecontext.CodeContext(self.editor)
            +
            +        self.highlight_cfg = {"background": '#abcdef',
            +                              "foreground": '#123456'}
            +        orig_idleConf_GetHighlight = codecontext.idleConf.GetHighlight
            +        def mock_idleconf_GetHighlight(theme, element):
            +            if element == 'context':
            +                return self.highlight_cfg
            +            return orig_idleConf_GetHighlight(theme, element)
            +        GetHighlight_patcher = unittest.mock.patch.object(
            +            codecontext.idleConf, 'GetHighlight', mock_idleconf_GetHighlight)
            +        GetHighlight_patcher.start()
            +        self.addCleanup(GetHighlight_patcher.stop)
            +
            +        self.font_override = 'TkFixedFont'
            +        def mock_idleconf_GetFont(root, configType, section):
            +            return self.font_override
            +        GetFont_patcher = unittest.mock.patch.object(
            +            codecontext.idleConf, 'GetFont', mock_idleconf_GetFont)
            +        GetFont_patcher.start()
            +        self.addCleanup(GetFont_patcher.stop)
            +
            +    def tearDown(self):
            +        if self.cc.context:
            +            self.cc.context.destroy()
            +        # Explicitly call __del__ to remove scheduled scripts.
            +        self.cc.__del__()
            +        del self.cc.context, self.cc
            +
            +    def test_init(self):
            +        eq = self.assertEqual
            +        ed = self.editor
            +        cc = self.cc
            +
            +        eq(cc.editwin, ed)
            +        eq(cc.text, ed.text)
            +        eq(cc.text['font'], ed.text['font'])
            +        self.assertIsNone(cc.context)
            +        eq(cc.info, [(0, -1, '', False)])
            +        eq(cc.topvisible, 1)
            +        self.assertIsNone(self.cc.t1)
            +
            +    def test_del(self):
            +        self.cc.__del__()
            +
            +    def test_del_with_timer(self):
            +        timer = self.cc.t1 = self.text.after(10000, lambda: None)
            +        self.cc.__del__()
            +        with self.assertRaises(TclError) as cm:
            +            self.root.tk.call('after', 'info', timer)
            +        self.assertIn("doesn't exist", str(cm.exception))
            +
            +    def test_reload(self):
            +        codecontext.CodeContext.reload()
            +        self.assertEqual(self.cc.context_depth, 15)
            +
            +    def test_toggle_code_context_event(self):
            +        eq = self.assertEqual
            +        cc = self.cc
            +        toggle = cc.toggle_code_context_event
            +
            +        # Make sure code context is off.
            +        if cc.context:
            +            toggle()
            +
            +        # Toggle on.
            +        toggle()
            +        self.assertIsNotNone(cc.context)
            +        eq(cc.context['font'], self.text['font'])
            +        eq(cc.context['fg'], self.highlight_cfg['foreground'])
            +        eq(cc.context['bg'], self.highlight_cfg['background'])
            +        eq(cc.context.get('1.0', 'end-1c'), '')
            +        eq(cc.editwin.label, 'Hide Code Context')
            +        eq(self.root.tk.call('after', 'info', self.cc.t1)[1], 'timer')
            +
            +        # Toggle off.
            +        toggle()
            +        self.assertIsNone(cc.context)
            +        eq(cc.editwin.label, 'Show Code Context')
            +        self.assertIsNone(self.cc.t1)
            +
            +        # Scroll down and toggle back on.
            +        line11_context = '\n'.join(x[2] for x in cc.get_context(11)[0])
            +        cc.text.yview(11)
            +        toggle()
            +        eq(cc.context.get('1.0', 'end-1c'), line11_context)
            +
            +        # Toggle off and on again.
            +        toggle()
            +        toggle()
            +        eq(cc.context.get('1.0', 'end-1c'), line11_context)
            +
            +    def test_get_context(self):
            +        eq = self.assertEqual
            +        gc = self.cc.get_context
            +
            +        # stopline must be greater than 0.
            +        with self.assertRaises(AssertionError):
            +            gc(1, stopline=0)
            +
            +        eq(gc(3), ([(2, 0, 'class C1():', 'class')], 0))
            +
            +        # Don't return comment.
            +        eq(gc(4), ([(2, 0, 'class C1():', 'class')], 0))
            +
            +        # Two indentation levels and no comment.
            +        eq(gc(5), ([(2, 0, 'class C1():', 'class'),
            +                    (4, 4, '    def __init__(self, a, b):', 'def')], 0))
            +
            +        # Only one 'def' is returned, not both at the same indent level.
            +        eq(gc(10), ([(2, 0, 'class C1():', 'class'),
            +                     (7, 4, '    def compare(self):', 'def'),
            +                     (8, 8, '        if a > b:', 'if')], 0))
            +
            +        # With 'elif', also show the 'if' even though it's at the same level.
            +        eq(gc(11), ([(2, 0, 'class C1():', 'class'),
            +                     (7, 4, '    def compare(self):', 'def'),
            +                     (8, 8, '        if a > b:', 'if'),
            +                     (10, 8, '        elif a < b:', 'elif')], 0))
            +
            +        # Set stop_line to not go back to first line in source code.
            +        # Return includes stop_line.
            +        eq(gc(11, stopline=2), ([(2, 0, 'class C1():', 'class'),
            +                                 (7, 4, '    def compare(self):', 'def'),
            +                                 (8, 8, '        if a > b:', 'if'),
            +                                 (10, 8, '        elif a < b:', 'elif')], 0))
            +        eq(gc(11, stopline=3), ([(7, 4, '    def compare(self):', 'def'),
            +                                 (8, 8, '        if a > b:', 'if'),
            +                                 (10, 8, '        elif a < b:', 'elif')], 4))
            +        eq(gc(11, stopline=8), ([(8, 8, '        if a > b:', 'if'),
            +                                 (10, 8, '        elif a < b:', 'elif')], 8))
            +
            +        # Set stop_indent to test indent level to stop at.
            +        eq(gc(11, stopindent=4), ([(7, 4, '    def compare(self):', 'def'),
            +                                   (8, 8, '        if a > b:', 'if'),
            +                                   (10, 8, '        elif a < b:', 'elif')], 4))
            +        # Check that the 'if' is included.
            +        eq(gc(11, stopindent=8), ([(8, 8, '        if a > b:', 'if'),
            +                                   (10, 8, '        elif a < b:', 'elif')], 8))
            +
            +    def test_update_code_context(self):
            +        eq = self.assertEqual
            +        cc = self.cc
            +        # Ensure code context is active.
            +        if not cc.context:
            +            cc.toggle_code_context_event()
            +
            +        # Invoke update_code_context without scrolling - nothing happens.
            +        self.assertIsNone(cc.update_code_context())
            +        eq(cc.info, [(0, -1, '', False)])
            +        eq(cc.topvisible, 1)
            +
            +        # Scroll down to line 1.
            +        cc.text.yview(1)
            +        cc.update_code_context()
            +        eq(cc.info, [(0, -1, '', False)])
            +        eq(cc.topvisible, 2)
            +        eq(cc.context.get('1.0', 'end-1c'), '')
            +
            +        # Scroll down to line 2.
            +        cc.text.yview(2)
            +        cc.update_code_context()
            +        eq(cc.info, [(0, -1, '', False), (2, 0, 'class C1():', 'class')])
            +        eq(cc.topvisible, 3)
            +        eq(cc.context.get('1.0', 'end-1c'), 'class C1():')
            +
            +        # Scroll down to line 3.  Since it's a comment, nothing changes.
            +        cc.text.yview(3)
            +        cc.update_code_context()
            +        eq(cc.info, [(0, -1, '', False), (2, 0, 'class C1():', 'class')])
            +        eq(cc.topvisible, 4)
            +        eq(cc.context.get('1.0', 'end-1c'), 'class C1():')
            +
            +        # Scroll down to line 4.
            +        cc.text.yview(4)
            +        cc.update_code_context()
            +        eq(cc.info, [(0, -1, '', False),
            +                     (2, 0, 'class C1():', 'class'),
            +                     (4, 4, '    def __init__(self, a, b):', 'def')])
            +        eq(cc.topvisible, 5)
            +        eq(cc.context.get('1.0', 'end-1c'), 'class C1():\n'
            +                                            '    def __init__(self, a, b):')
            +
            +        # Scroll down to line 11.  Last 'def' is removed.
            +        cc.text.yview(11)
            +        cc.update_code_context()
            +        eq(cc.info, [(0, -1, '', False),
            +                     (2, 0, 'class C1():', 'class'),
            +                     (7, 4, '    def compare(self):', 'def'),
            +                     (8, 8, '        if a > b:', 'if'),
            +                     (10, 8, '        elif a < b:', 'elif')])
            +        eq(cc.topvisible, 12)
            +        eq(cc.context.get('1.0', 'end-1c'), 'class C1():\n'
            +                                            '    def compare(self):\n'
            +                                            '        if a > b:\n'
            +                                            '        elif a < b:')
            +
            +        # No scroll.  No update, even though context_depth changed.
            +        cc.update_code_context()
            +        cc.context_depth = 1
            +        eq(cc.info, [(0, -1, '', False),
            +                     (2, 0, 'class C1():', 'class'),
            +                     (7, 4, '    def compare(self):', 'def'),
            +                     (8, 8, '        if a > b:', 'if'),
            +                     (10, 8, '        elif a < b:', 'elif')])
            +        eq(cc.topvisible, 12)
            +        eq(cc.context.get('1.0', 'end-1c'), 'class C1():\n'
            +                                            '    def compare(self):\n'
            +                                            '        if a > b:\n'
            +                                            '        elif a < b:')
            +
            +        # Scroll up.
            +        cc.text.yview(5)
            +        cc.update_code_context()
            +        eq(cc.info, [(0, -1, '', False),
            +                     (2, 0, 'class C1():', 'class'),
            +                     (4, 4, '    def __init__(self, a, b):', 'def')])
            +        eq(cc.topvisible, 6)
            +        # context_depth is 1.
            +        eq(cc.context.get('1.0', 'end-1c'), '    def __init__(self, a, b):')
            +
            +    def test_jumptoline(self):
            +        eq = self.assertEqual
            +        cc = self.cc
            +        jump = cc.jumptoline
            +
            +        if not cc.context:
            +            cc.toggle_code_context_event()
            +
            +        # Empty context.
            +        cc.text.yview('2.0')
            +        cc.update_code_context()
            +        eq(cc.topvisible, 2)
            +        cc.context.mark_set('insert', '1.5')
            +        jump()
            +        eq(cc.topvisible, 1)
            +
            +        # 4 lines of context showing.
            +        cc.text.yview('12.0')
            +        cc.update_code_context()
            +        eq(cc.topvisible, 12)
            +        cc.context.mark_set('insert', '3.0')
            +        jump()
            +        eq(cc.topvisible, 8)
            +
            +        # More context lines than limit.
            +        cc.context_depth = 2
            +        cc.text.yview('12.0')
            +        cc.update_code_context()
            +        eq(cc.topvisible, 12)
            +        cc.context.mark_set('insert', '1.0')
            +        jump()
            +        eq(cc.topvisible, 8)
            +
            +        # Context selection stops jump.
            +        cc.text.yview('5.0')
            +        cc.update_code_context()
            +        cc.context.tag_add('sel', '1.0', '2.0')
            +        cc.context.mark_set('insert', '1.0')
            +        jump()  # Without selection, to line 2.
            +        eq(cc.topvisible, 5)
            +
            +    @mock.patch.object(codecontext.CodeContext, 'update_code_context')
            +    def test_timer_event(self, mock_update):
            +        # Ensure code context is not active.
            +        if self.cc.context:
            +            self.cc.toggle_code_context_event()
            +        self.cc.timer_event()
            +        mock_update.assert_not_called()
            +
            +        # Activate code context.
            +        self.cc.toggle_code_context_event()
            +        self.cc.timer_event()
            +        mock_update.assert_called()
            +
            +    def test_font(self):
            +        eq = self.assertEqual
            +        cc = self.cc
            +
            +        orig_font = cc.text['font']
            +        test_font = 'TkTextFont'
            +        self.assertNotEqual(orig_font, test_font)
            +
            +        # Ensure code context is not active.
            +        if cc.context is not None:
            +            cc.toggle_code_context_event()
            +
            +        self.font_override = test_font
            +        # Nothing breaks or changes with inactive code context.
            +        cc.update_font()
            +
            +        # Activate code context, previous font change is immediately effective.
            +        cc.toggle_code_context_event()
            +        eq(cc.context['font'], test_font)
            +
            +        # Call the font update, change is picked up.
            +        self.font_override = orig_font
            +        cc.update_font()
            +        eq(cc.context['font'], orig_font)
            +
            +    def test_highlight_colors(self):
            +        eq = self.assertEqual
            +        cc = self.cc
            +
            +        orig_colors = dict(self.highlight_cfg)
            +        test_colors = {'background': '#222222', 'foreground': '#ffff00'}
            +
            +        def assert_colors_are_equal(colors):
            +            eq(cc.context['background'], colors['background'])
            +            eq(cc.context['foreground'], colors['foreground'])
            +
            +        # Ensure code context is not active.
            +        if cc.context:
            +            cc.toggle_code_context_event()
            +
            +        self.highlight_cfg = test_colors
            +        # Nothing breaks with inactive code context.
            +        cc.update_highlight_colors()
            +
            +        # Activate code context, previous colors change is immediately effective.
            +        cc.toggle_code_context_event()
            +        assert_colors_are_equal(test_colors)
            +
            +        # Call colors update with no change to the configured colors.
            +        cc.update_highlight_colors()
            +        assert_colors_are_equal(test_colors)
            +
            +        # Call the colors update with code context active, change is picked up.
            +        self.highlight_cfg = orig_colors
            +        cc.update_highlight_colors()
            +        assert_colors_are_equal(orig_colors)
            +
            +
            +class HelperFunctionText(unittest.TestCase):
            +
            +    def test_get_spaces_firstword(self):
            +        get = codecontext.get_spaces_firstword
            +        test_lines = (
            +            ('    first word', ('    ', 'first')),
            +            ('\tfirst word', ('\t', 'first')),
            +            ('  \u19D4\u19D2: ', ('  ', '\u19D4\u19D2')),
            +            ('no spaces', ('', 'no')),
            +            ('', ('', '')),
            +            ('# TEST COMMENT', ('', '')),
            +            ('    (continuation)', ('    ', ''))
            +            )
            +        for line, expected_output in test_lines:
            +            self.assertEqual(get(line), expected_output)
            +
            +        # Send the pattern in the call.
            +        self.assertEqual(get('    (continuation)',
            +                             c=re.compile(r'^(\s*)([^\s]*)')),
            +                         ('    ', '(continuation)'))
            +
            +    def test_get_line_info(self):
            +        eq = self.assertEqual
            +        gli = codecontext.get_line_info
            +        lines = code_sample.splitlines()
            +
            +        # Line 1 is not a BLOCKOPENER.
            +        eq(gli(lines[0]), (codecontext.INFINITY, '', False))
            +        # Line 2 is a BLOCKOPENER without an indent.
            +        eq(gli(lines[1]), (0, 'class C1():', 'class'))
            +        # Line 3 is not a BLOCKOPENER and does not return the indent level.
            +        eq(gli(lines[2]), (codecontext.INFINITY, '    # Class comment.', False))
            +        # Line 4 is a BLOCKOPENER and is indented.
            +        eq(gli(lines[3]), (4, '    def __init__(self, a, b):', 'def'))
            +        # Line 8 is a different BLOCKOPENER and is indented.
            +        eq(gli(lines[7]), (8, '        if a > b:', 'if'))
            +        # Test tab.
            +        eq(gli('\tif a == b:'), (1, '\tif a == b:', 'if'))
            +
            +
            +if __name__ == '__main__':
            +    unittest.main(verbosity=2)
            diff --git a/Lib/idlelib/idle_test/test_colorizer.py b/Lib/idlelib/idle_test/test_colorizer.py
            index 238bc3e1141363..c31c49236ca0b9 100644
            --- a/Lib/idlelib/idle_test/test_colorizer.py
            +++ b/Lib/idlelib/idle_test/test_colorizer.py
            @@ -1,55 +1,422 @@
            -'''Test idlelib/colorizer.py
            +"Test colorizer, coverage 93%."
             
            -Perform minimal sanity checks that module imports and some things run.
            -
            -Coverage 22%.
            -'''
            -from idlelib import colorizer  # always test import
            +from idlelib import colorizer
             from test.support import requires
            -from tkinter import Tk, Text
             import unittest
            +from unittest import mock
            +
            +from functools import partial
            +from tkinter import Tk, Text
            +from idlelib import config
            +from idlelib.percolator import Percolator
            +
            +
            +usercfg = colorizer.idleConf.userCfg
            +testcfg = {
            +    'main': config.IdleUserConfParser(''),
            +    'highlight': config.IdleUserConfParser(''),
            +    'keys': config.IdleUserConfParser(''),
            +    'extensions': config.IdleUserConfParser(''),
            +}
            +
            +source = (
            +    "if True: int ('1') # keyword, builtin, string, comment\n"
            +    "elif False: print(0)  # 'string' in comment\n"
            +    "else: float(None)  # if in comment\n"
            +    "if iF + If + IF: 'keyword matching must respect case'\n"
            +    "if'': x or''  # valid string-keyword no-space combinations\n"
            +    "async def f(): await g()\n"
            +    "'x', '''x''', \"x\", \"\"\"x\"\"\"\n"
            +    )
            +
            +
            +def setUpModule():
            +    colorizer.idleConf.userCfg = testcfg
            +
            +
            +def tearDownModule():
            +    colorizer.idleConf.userCfg = usercfg
             
             
             class FunctionTest(unittest.TestCase):
             
                 def test_any(self):
            -        self.assertTrue(colorizer.any('test', ('a', 'b')))
            +        self.assertEqual(colorizer.any('test', ('a', 'b', 'cd')),
            +                         '(?Pa|b|cd)')
             
                 def test_make_pat(self):
            +        # Tested in more detail by testing prog.
                     self.assertTrue(colorizer.make_pat())
             
            +    def test_prog(self):
            +        prog = colorizer.prog
            +        eq = self.assertEqual
            +        line = 'def f():\n    print("hello")\n'
            +        m = prog.search(line)
            +        eq(m.groupdict()['KEYWORD'], 'def')
            +        m = prog.search(line, m.end())
            +        eq(m.groupdict()['SYNC'], '\n')
            +        m = prog.search(line, m.end())
            +        eq(m.groupdict()['BUILTIN'], 'print')
            +        m = prog.search(line, m.end())
            +        eq(m.groupdict()['STRING'], '"hello"')
            +        m = prog.search(line, m.end())
            +        eq(m.groupdict()['SYNC'], '\n')
            +
            +    def test_idprog(self):
            +        idprog = colorizer.idprog
            +        m = idprog.match('nospace')
            +        self.assertIsNone(m)
            +        m = idprog.match(' space')
            +        self.assertEqual(m.group(0), ' space')
            +
             
             class ColorConfigTest(unittest.TestCase):
             
                 @classmethod
                 def setUpClass(cls):
                     requires('gui')
            -        cls.root = Tk()
            -        cls.text = Text(cls.root)
            +        root = cls.root = Tk()
            +        root.withdraw()
            +        cls.text = Text(root)
             
                 @classmethod
                 def tearDownClass(cls):
                     del cls.text
            +        cls.root.update_idletasks()
                     cls.root.destroy()
                     del cls.root
             
            -    def test_colorizer(self):
            -        colorizer.color_config(self.text)
            +    def test_color_config(self):
            +        text = self.text
            +        eq = self.assertEqual
            +        colorizer.color_config(text)
            +        # Uses IDLE Classic theme as default.
            +        eq(text['background'], '#ffffff')
            +        eq(text['foreground'], '#000000')
            +        eq(text['selectbackground'], 'gray')
            +        eq(text['selectforeground'], '#000000')
            +        eq(text['insertbackground'], 'black')
            +        eq(text['inactiveselectbackground'], 'gray')
            +
            +
            +class ColorDelegatorInstantiationTest(unittest.TestCase):
            +
            +    @classmethod
            +    def setUpClass(cls):
            +        requires('gui')
            +        root = cls.root = Tk()
            +        root.withdraw()
            +        text = cls.text = Text(root)
            +
            +    @classmethod
            +    def tearDownClass(cls):
            +        del cls.text
            +        cls.root.update_idletasks()
            +        cls.root.destroy()
            +        del cls.root
            +
            +    def setUp(self):
            +        self.color = colorizer.ColorDelegator()
            +
            +    def tearDown(self):
            +        self.color.close()
            +        self.text.delete('1.0', 'end')
            +        self.color.resetcache()
            +        del self.color
            +
            +    def test_init(self):
            +        color = self.color
            +        self.assertIsInstance(color, colorizer.ColorDelegator)
            +
            +    def test_init_state(self):
            +        # init_state() is called during the instantiation of
            +        # ColorDelegator in setUp().
            +        color = self.color
            +        self.assertIsNone(color.after_id)
            +        self.assertTrue(color.allow_colorizing)
            +        self.assertFalse(color.colorizing)
            +        self.assertFalse(color.stop_colorizing)
            +
             
             class ColorDelegatorTest(unittest.TestCase):
             
                 @classmethod
                 def setUpClass(cls):
                     requires('gui')
            -        cls.root = Tk()
            +        root = cls.root = Tk()
            +        root.withdraw()
            +        text = cls.text = Text(root)
            +        cls.percolator = Percolator(text)
            +        # Delegator stack = [Delegator(text)]
             
                 @classmethod
                 def tearDownClass(cls):
            +        cls.percolator.redir.close()
            +        del cls.percolator, cls.text
            +        cls.root.update_idletasks()
                     cls.root.destroy()
                     del cls.root
             
            -    def test_colorizer(self):
            -        colorizer.ColorDelegator()
            +    def setUp(self):
            +        self.color = colorizer.ColorDelegator()
            +        self.percolator.insertfilter(self.color)
            +        # Calls color.setdelegate(Delegator(text)).
            +
            +    def tearDown(self):
            +        self.color.close()
            +        self.percolator.removefilter(self.color)
            +        self.text.delete('1.0', 'end')
            +        self.color.resetcache()
            +        del self.color
            +
            +    def test_setdelegate(self):
            +        # Called in setUp when filter is attached to percolator.
            +        color = self.color
            +        self.assertIsInstance(color.delegate, colorizer.Delegator)
            +        # It is too late to mock notify_range, so test side effect.
            +        self.assertEqual(self.root.tk.call(
            +            'after', 'info', color.after_id)[1], 'timer')
            +
            +    def test_LoadTagDefs(self):
            +        highlight = partial(config.idleConf.GetHighlight, theme='IDLE Classic')
            +        for tag, colors in self.color.tagdefs.items():
            +            with self.subTest(tag=tag):
            +                self.assertIn('background', colors)
            +                self.assertIn('foreground', colors)
            +                if tag not in ('SYNC', 'TODO'):
            +                    self.assertEqual(colors, highlight(element=tag.lower()))
            +
            +    def test_config_colors(self):
            +        text = self.text
            +        highlight = partial(config.idleConf.GetHighlight, theme='IDLE Classic')
            +        for tag in self.color.tagdefs:
            +            for plane in ('background', 'foreground'):
            +                with self.subTest(tag=tag, plane=plane):
            +                    if tag in ('SYNC', 'TODO'):
            +                        self.assertEqual(text.tag_cget(tag, plane), '')
            +                    else:
            +                        self.assertEqual(text.tag_cget(tag, plane),
            +                                         highlight(element=tag.lower())[plane])
            +        # 'sel' is marked as the highest priority.
            +        self.assertEqual(text.tag_names()[-1], 'sel')
            +
            +    @mock.patch.object(colorizer.ColorDelegator, 'notify_range')
            +    def test_insert(self, mock_notify):
            +        text = self.text
            +        # Initial text.
            +        text.insert('insert', 'foo')
            +        self.assertEqual(text.get('1.0', 'end'), 'foo\n')
            +        mock_notify.assert_called_with('1.0', '1.0+3c')
            +        # Additional text.
            +        text.insert('insert', 'barbaz')
            +        self.assertEqual(text.get('1.0', 'end'), 'foobarbaz\n')
            +        mock_notify.assert_called_with('1.3', '1.3+6c')
            +
            +    @mock.patch.object(colorizer.ColorDelegator, 'notify_range')
            +    def test_delete(self, mock_notify):
            +        text = self.text
            +        # Initialize text.
            +        text.insert('insert', 'abcdefghi')
            +        self.assertEqual(text.get('1.0', 'end'), 'abcdefghi\n')
            +        # Delete single character.
            +        text.delete('1.7')
            +        self.assertEqual(text.get('1.0', 'end'), 'abcdefgi\n')
            +        mock_notify.assert_called_with('1.7')
            +        # Delete multiple characters.
            +        text.delete('1.3', '1.6')
            +        self.assertEqual(text.get('1.0', 'end'), 'abcgi\n')
            +        mock_notify.assert_called_with('1.3')
            +
            +    def test_notify_range(self):
            +        text = self.text
            +        color = self.color
            +        eq = self.assertEqual
            +
            +        # Colorizing already scheduled.
            +        save_id = color.after_id
            +        eq(self.root.tk.call('after', 'info', save_id)[1], 'timer')
            +        self.assertFalse(color.colorizing)
            +        self.assertFalse(color.stop_colorizing)
            +        self.assertTrue(color.allow_colorizing)
            +
            +        # Coloring scheduled and colorizing in progress.
            +        color.colorizing = True
            +        color.notify_range('1.0', 'end')
            +        self.assertFalse(color.stop_colorizing)
            +        eq(color.after_id, save_id)
            +
            +        # No colorizing scheduled and colorizing in progress.
            +        text.after_cancel(save_id)
            +        color.after_id = None
            +        color.notify_range('1.0', '1.0+3c')
            +        self.assertTrue(color.stop_colorizing)
            +        self.assertIsNotNone(color.after_id)
            +        eq(self.root.tk.call('after', 'info', color.after_id)[1], 'timer')
            +        # New event scheduled.
            +        self.assertNotEqual(color.after_id, save_id)
            +
            +        # No colorizing scheduled and colorizing off.
            +        text.after_cancel(color.after_id)
            +        color.after_id = None
            +        color.allow_colorizing = False
            +        color.notify_range('1.4', '1.4+10c')
            +        # Nothing scheduled when colorizing is off.
            +        self.assertIsNone(color.after_id)
            +
            +    def test_toggle_colorize_event(self):
            +        color = self.color
            +        eq = self.assertEqual
            +
            +        # Starts with colorizing allowed and scheduled.
            +        self.assertFalse(color.colorizing)
            +        self.assertFalse(color.stop_colorizing)
            +        self.assertTrue(color.allow_colorizing)
            +        eq(self.root.tk.call('after', 'info', color.after_id)[1], 'timer')
            +
            +        # Toggle colorizing off.
            +        color.toggle_colorize_event()
            +        self.assertIsNone(color.after_id)
            +        self.assertFalse(color.colorizing)
            +        self.assertFalse(color.stop_colorizing)
            +        self.assertFalse(color.allow_colorizing)
            +
            +        # Toggle on while colorizing in progress (doesn't add timer).
            +        color.colorizing = True
            +        color.toggle_colorize_event()
            +        self.assertIsNone(color.after_id)
            +        self.assertTrue(color.colorizing)
            +        self.assertFalse(color.stop_colorizing)
            +        self.assertTrue(color.allow_colorizing)
            +
            +        # Toggle off while colorizing in progress.
            +        color.toggle_colorize_event()
            +        self.assertIsNone(color.after_id)
            +        self.assertTrue(color.colorizing)
            +        self.assertTrue(color.stop_colorizing)
            +        self.assertFalse(color.allow_colorizing)
            +
            +        # Toggle on while colorizing not in progress.
            +        color.colorizing = False
            +        color.toggle_colorize_event()
            +        eq(self.root.tk.call('after', 'info', color.after_id)[1], 'timer')
            +        self.assertFalse(color.colorizing)
            +        self.assertTrue(color.stop_colorizing)
            +        self.assertTrue(color.allow_colorizing)
            +
            +    @mock.patch.object(colorizer.ColorDelegator, 'recolorize_main')
            +    def test_recolorize(self, mock_recmain):
            +        text = self.text
            +        color = self.color
            +        eq = self.assertEqual
            +        # Call recolorize manually and not scheduled.
            +        text.after_cancel(color.after_id)
            +
            +        # No delegate.
            +        save_delegate = color.delegate
            +        color.delegate = None
            +        color.recolorize()
            +        mock_recmain.assert_not_called()
            +        color.delegate = save_delegate
            +
            +        # Toggle off colorizing.
            +        color.allow_colorizing = False
            +        color.recolorize()
            +        mock_recmain.assert_not_called()
            +        color.allow_colorizing = True
            +
            +        # Colorizing in progress.
            +        color.colorizing = True
            +        color.recolorize()
            +        mock_recmain.assert_not_called()
            +        color.colorizing = False
            +
            +        # Colorizing is done, but not completed, so rescheduled.
            +        color.recolorize()
            +        self.assertFalse(color.stop_colorizing)
            +        self.assertFalse(color.colorizing)
            +        mock_recmain.assert_called()
            +        eq(mock_recmain.call_count, 1)
            +        # Rescheduled when TODO tag still exists.
            +        eq(self.root.tk.call('after', 'info', color.after_id)[1], 'timer')
            +
            +        # No changes to text, so no scheduling added.
            +        text.tag_remove('TODO', '1.0', 'end')
            +        color.recolorize()
            +        self.assertFalse(color.stop_colorizing)
            +        self.assertFalse(color.colorizing)
            +        mock_recmain.assert_called()
            +        eq(mock_recmain.call_count, 2)
            +        self.assertIsNone(color.after_id)
            +
            +    @mock.patch.object(colorizer.ColorDelegator, 'notify_range')
            +    def test_recolorize_main(self, mock_notify):
            +        text = self.text
            +        color = self.color
            +        eq = self.assertEqual
            +
            +        text.insert('insert', source)
            +        expected = (('1.0', ('KEYWORD',)), ('1.2', ()), ('1.3', ('KEYWORD',)),
            +                    ('1.7', ()), ('1.9', ('BUILTIN',)), ('1.14', ('STRING',)),
            +                    ('1.19', ('COMMENT',)),
            +                    ('2.1', ('KEYWORD',)), ('2.18', ()), ('2.25', ('COMMENT',)),
            +                    ('3.6', ('BUILTIN',)), ('3.12', ('KEYWORD',)), ('3.21', ('COMMENT',)),
            +                    ('4.0', ('KEYWORD',)), ('4.3', ()), ('4.6', ()),
            +                    ('5.2', ('STRING',)), ('5.8', ('KEYWORD',)), ('5.10', ('STRING',)),
            +                    ('6.0', ('KEYWORD',)), ('6.10', ('DEFINITION',)), ('6.11', ()),
            +                    ('7.0', ('STRING',)), ('7.4', ()), ('7.5', ('STRING',)),
            +                    ('7.12', ()), ('7.14', ('STRING',)),
            +                    # SYNC at the end of every line.
            +                    ('1.55', ('SYNC',)), ('2.50', ('SYNC',)), ('3.34', ('SYNC',)),
            +                   )
            +
            +        # Nothing marked to do therefore no tags in text.
            +        text.tag_remove('TODO', '1.0', 'end')
            +        color.recolorize_main()
            +        for tag in text.tag_names():
            +            with self.subTest(tag=tag):
            +                eq(text.tag_ranges(tag), ())
            +
            +        # Source marked for processing.
            +        text.tag_add('TODO', '1.0', 'end')
            +        # Check some indexes.
            +        color.recolorize_main()
            +        for index, expected_tags in expected:
            +            with self.subTest(index=index):
            +                eq(text.tag_names(index), expected_tags)
            +
            +        # Check for some tags for ranges.
            +        eq(text.tag_nextrange('TODO', '1.0'), ())
            +        eq(text.tag_nextrange('KEYWORD', '1.0'), ('1.0', '1.2'))
            +        eq(text.tag_nextrange('COMMENT', '2.0'), ('2.22', '2.43'))
            +        eq(text.tag_nextrange('SYNC', '2.0'), ('2.43', '3.0'))
            +        eq(text.tag_nextrange('STRING', '2.0'), ('4.17', '4.53'))
            +        eq(text.tag_nextrange('STRING', '7.0'), ('7.0', '7.3'))
            +        eq(text.tag_nextrange('STRING', '7.3'), ('7.5', '7.12'))
            +        eq(text.tag_nextrange('STRING', '7.12'), ('7.14', '7.17'))
            +        eq(text.tag_nextrange('STRING', '7.17'), ('7.19', '7.26'))
            +        eq(text.tag_nextrange('SYNC', '7.0'), ('7.26', '9.0'))
            +
            +    @mock.patch.object(colorizer.ColorDelegator, 'recolorize')
            +    @mock.patch.object(colorizer.ColorDelegator, 'notify_range')
            +    def test_removecolors(self, mock_notify, mock_recolorize):
            +        text = self.text
            +        color = self.color
            +        text.insert('insert', source)
            +
            +        color.recolorize_main()
            +        # recolorize_main doesn't add these tags.
            +        text.tag_add("ERROR", "1.0")
            +        text.tag_add("TODO", "1.0")
            +        text.tag_add("hit", "1.0")
            +        for tag in color.tagdefs:
            +            with self.subTest(tag=tag):
            +                self.assertNotEqual(text.tag_ranges(tag), ())
            +
            +        color.removecolors()
            +        for tag in color.tagdefs:
            +            with self.subTest(tag=tag):
            +                self.assertEqual(text.tag_ranges(tag), ())
             
             
             if __name__ == '__main__':
            diff --git a/Lib/idlelib/idle_test/test_config.py b/Lib/idlelib/idle_test/test_config.py
            index abfec7993e0744..697fda527968de 100644
            --- a/Lib/idlelib/idle_test/test_config.py
            +++ b/Lib/idlelib/idle_test/test_config.py
            @@ -1,10 +1,9 @@
            -'''Test idlelib.config.
            -
            -Coverage: 96% (100% for IdleConfParser, IdleUserConfParser*, ConfigChanges).
            +"""Test config, coverage 93%.
            +(100% for IdleConfParser, IdleUserConfParser*, ConfigChanges).
             * Exception is OSError clause in Save method.
             Much of IdleConf is also exercised by ConfigDialog and test_configdialog.
            -'''
            -import copy
            +"""
            +from idlelib import config
             import sys
             import os
             import tempfile
            @@ -12,7 +11,6 @@
             import unittest
             from unittest import mock
             import idlelib
            -from idlelib import config
             from idlelib.idle_test.mock_idle import Func
             
             # Tests should not depend on fortuitous user configurations.
            @@ -161,19 +159,6 @@ def test_is_empty(self):
                     self.assertFalse(parser.IsEmpty())
                     self.assertCountEqual(parser.sections(), ['Foo'])
             
            -    def test_remove_file(self):
            -        with tempfile.TemporaryDirectory() as tdir:
            -            path = os.path.join(tdir, 'test.cfg')
            -            parser = self.new_parser(path)
            -            parser.RemoveFile()  # Should not raise exception.
            -
            -            parser.AddSection('Foo')
            -            parser.SetOption('Foo', 'bar', 'true')
            -            parser.Save()
            -            self.assertTrue(os.path.exists(path))
            -            parser.RemoveFile()
            -            self.assertFalse(os.path.exists(path))
            -
                 def test_save(self):
                     with tempfile.TemporaryDirectory() as tdir:
                         path = os.path.join(tdir, 'test.cfg')
            @@ -235,7 +220,7 @@ def mock_config(self):
             
                 @unittest.skipIf(sys.platform.startswith('win'), 'this is test for unix system')
                 def test_get_user_cfg_dir_unix(self):
            -        "Test to get user config directory under unix"
            +        # Test to get user config directory under unix.
                     conf = self.new_config(_utest=True)
             
                     # Check normal way should success
            @@ -256,9 +241,9 @@ def test_get_user_cfg_dir_unix(self):
                             with self.assertRaises(FileNotFoundError):
                                 conf.GetUserCfgDir()
             
            -    @unittest.skipIf(not sys.platform.startswith('win'), 'this is test for windows system')
            +    @unittest.skipIf(not sys.platform.startswith('win'), 'this is test for Windows system')
                 def test_get_user_cfg_dir_windows(self):
            -        "Test to get user config directory under windows"
            +        # Test to get user config directory under Windows.
                     conf = self.new_config(_utest=True)
             
                     # Check normal way should success
            @@ -299,12 +284,12 @@ def test_create_config_handlers(self):
                         self.assertIsInstance(user_parser, config.IdleUserConfParser)
             
                     # Check config path are correct
            -        for config_type, parser in conf.defaultCfg.items():
            +        for cfg_type, parser in conf.defaultCfg.items():
                         self.assertEqual(parser.file,
            -                             os.path.join(idle_dir, 'config-%s.def' % config_type))
            -        for config_type, parser in conf.userCfg.items():
            +                             os.path.join(idle_dir, f'config-{cfg_type}.def'))
            +        for cfg_type, parser in conf.userCfg.items():
                         self.assertEqual(parser.file,
            -                             os.path.join(conf.userdir, 'config-%s.cfg' % config_type))
            +                             os.path.join(conf.userdir or '#', f'config-{cfg_type}.cfg'))
             
                 def test_load_cfg_files(self):
                     conf = self.new_config(_utest=True)
            @@ -357,11 +342,11 @@ def test_get_section_list(self):
             
                     self.assertCountEqual(
                         conf.GetSectionList('default', 'main'),
            -            ['General', 'EditorWindow', 'Indent', 'Theme',
            +            ['General', 'EditorWindow', 'PyShell', 'Indent', 'Theme',
                          'Keys', 'History', 'HelpFiles'])
                     self.assertCountEqual(
                         conf.GetSectionList('user', 'main'),
            -            ['General', 'EditorWindow', 'Indent', 'Theme',
            +            ['General', 'EditorWindow', 'PyShell', 'Indent', 'Theme',
                          'Keys', 'History', 'HelpFiles'])
             
                     with self.assertRaises(config.InvalidConfigSet):
            @@ -375,10 +360,6 @@ def test_get_highlight(self):
                     eq = self.assertEqual
                     eq(conf.GetHighlight('IDLE Classic', 'normal'), {'foreground': '#000000',
                                                                      'background': '#ffffff'})
            -        eq(conf.GetHighlight('IDLE Classic', 'normal', 'fg'), '#000000')
            -        eq(conf.GetHighlight('IDLE Classic', 'normal', 'bg'), '#ffffff')
            -        with self.assertRaises(config.InvalidFgBg):
            -            conf.GetHighlight('IDLE Classic', 'normal', 'fb')
             
                     # Test cursor (this background should be normal-background)
                     eq(conf.GetHighlight('IDLE Classic', 'cursor'), {'foreground': 'black',
            @@ -392,7 +373,7 @@ def test_get_highlight(self):
                                                                    'background': '#171717'})
             
                 def test_get_theme_dict(self):
            -        "XXX: NOT YET DONE"
            +        # TODO: finish.
                     conf = self.mock_config()
             
                     # These two should be the same
            @@ -453,7 +434,7 @@ def test_remove_key_bind_names(self):
             
                     self.assertCountEqual(
                         conf.RemoveKeyBindNames(conf.GetSectionList('default', 'extensions')),
            -            ['AutoComplete', 'CodeContext', 'FormatParagraph', 'ParenMatch','ZzDummy'])
            +            ['AutoComplete', 'CodeContext', 'FormatParagraph', 'ParenMatch', 'ZzDummy'])
             
                 def test_get_extn_name_for_event(self):
                     userextn.read_string('''
            @@ -527,7 +508,7 @@ def test_get_current_keyset(self):
                 def test_get_keyset(self):
                     conf = self.mock_config()
             
            -        # Conflic with key set, should be disable to ''
            +        # Conflict with key set, should be disable to ''
                     conf.defaultCfg['extensions'].add_section('Foobar')
                     conf.defaultCfg['extensions'].add_section('Foobar_cfgBindings')
                     conf.defaultCfg['extensions'].set('Foobar', 'enable', 'True')
            diff --git a/Lib/idlelib/idle_test/test_config_key.py b/Lib/idlelib/idle_test/test_config_key.py
            index 9074e23aab35d1..b7fe7fd6b5ec10 100644
            --- a/Lib/idlelib/idle_test/test_config_key.py
            +++ b/Lib/idlelib/idle_test/test_config_key.py
            @@ -1,26 +1,31 @@
            -''' Test idlelib.config_key.
            +"""Test config_key, coverage 98%.
            +
            +Coverage is effectively 100%.  Tkinter dialog is mocked, Mac-only line
            +may be skipped, and dummy function in bind test should not be called.
            +Not tested: exit with 'self.advanced or self.keys_ok(keys)) ...' False.
            +"""
             
            -Coverage: 56% from creating and closing dialog.
            -'''
             from idlelib import config_key
             from test.support import requires
            -import sys
             import unittest
            -from tkinter import Tk
            +from unittest import mock
            +from tkinter import Tk, TclError
             from idlelib.idle_test.mock_idle import Func
            -from idlelib.idle_test.mock_tk import Var, Mbox_func
            +from idlelib.idle_test.mock_tk import Mbox_func
            +
            +gkd = config_key.GetKeysDialog
             
             
             class ValidationTest(unittest.TestCase):
            -    "Test validation methods: OK, KeysOK, bind_ok."
            +    "Test validation methods: ok, keys_ok, bind_ok."
             
            -    class Validator(config_key.GetKeysDialog):
            +    class Validator(gkd):
                     def __init__(self, *args, **kwargs):
                         config_key.GetKeysDialog.__init__(self, *args, **kwargs)
            -            class listKeysFinal:
            +            class list_keys_final:
                             get = Func()
            -            self.listKeysFinal = listKeysFinal
            -        GetModifiers = Func()
            +            self.list_keys_final = list_keys_final
            +        get_modifiers = Func()
                     showerror = Mbox_func()
             
                 @classmethod
            @@ -34,7 +39,7 @@ def setUpClass(cls):
             
                 @classmethod
                 def tearDownClass(cls):
            -        cls.dialog.Cancel()
            +        cls.dialog.cancel()
                     cls.root.update_idletasks()
                     cls.root.destroy()
                     del cls.dialog, cls.root
            @@ -45,49 +50,49 @@ def setUp(self):
                 # A test that sets a non-blank modifier list should reset it to [].
             
                 def test_ok_empty(self):
            -        self.dialog.keyString.set(' ')
            -        self.dialog.OK()
            +        self.dialog.key_string.set(' ')
            +        self.dialog.ok()
                     self.assertEqual(self.dialog.result, '')
                     self.assertEqual(self.dialog.showerror.message, 'No key specified.')
             
                 def test_ok_good(self):
            -        self.dialog.keyString.set('')
            -        self.dialog.listKeysFinal.get.result = 'F11'
            -        self.dialog.OK()
            +        self.dialog.key_string.set('')
            +        self.dialog.list_keys_final.get.result = 'F11'
            +        self.dialog.ok()
                     self.assertEqual(self.dialog.result, '')
                     self.assertEqual(self.dialog.showerror.message, '')
             
                 def test_keys_no_ending(self):
            -        self.assertFalse(self.dialog.KeysOK(''))
            +        self.dialog.list_keys_final.get.result = 'A'
            +        self.assertFalse(self.dialog.keys_ok(''))
                     self.assertIn('No modifier', self.dialog.showerror.message)
             
                 def test_keys_no_modifier_ok(self):
            -        self.dialog.listKeysFinal.get.result = 'F11'
            -        self.assertTrue(self.dialog.KeysOK(''))
            +        self.dialog.list_keys_final.get.result = 'F11'
            +        self.assertTrue(self.dialog.keys_ok(''))
                     self.assertEqual(self.dialog.showerror.message, '')
             
                 def test_keys_shift_bad(self):
            -        self.dialog.listKeysFinal.get.result = 'a'
            -        self.dialog.GetModifiers.result = ['Shift']
            -        self.assertFalse(self.dialog.KeysOK(''))
            +        self.dialog.list_keys_final.get.result = 'a'
            +        self.dialog.get_modifiers.result = ['Shift']
            +        self.assertFalse(self.dialog.keys_ok(''))
                     self.assertIn('shift modifier', self.dialog.showerror.message)
            -        self.dialog.GetModifiers.result = []
            +        self.dialog.get_modifiers.result = []
             
                 def test_keys_dup(self):
                     for mods, final, seq in (([], 'F12', ''),
                                              (['Control'], 'x', ''),
                                              (['Control'], 'X', '')):
                         with self.subTest(m=mods, f=final, s=seq):
            -                self.dialog.listKeysFinal.get.result = final
            -                self.dialog.GetModifiers.result = mods
            -                self.assertFalse(self.dialog.KeysOK(seq))
            +                self.dialog.list_keys_final.get.result = final
            +                self.dialog.get_modifiers.result = mods
            +                self.assertFalse(self.dialog.keys_ok(seq))
                             self.assertIn('already in use', self.dialog.showerror.message)
            -        self.dialog.GetModifiers.result = []
            +        self.dialog.get_modifiers.result = []
             
                 def test_bind_ok(self):
                     self.assertTrue(self.dialog.bind_ok(''))
            @@ -98,5 +103,189 @@ def test_bind_not_ok(self):
                     self.assertIn('not accepted', self.dialog.showerror.message)
             
             
            +class ToggleLevelTest(unittest.TestCase):
            +    "Test toggle between Basic and Advanced frames."
            +
            +    @classmethod
            +    def setUpClass(cls):
            +        requires('gui')
            +        cls.root = Tk()
            +        cls.root.withdraw()
            +        cls.dialog = gkd(cls.root, 'Title', '<>', [], _utest=True)
            +
            +    @classmethod
            +    def tearDownClass(cls):
            +        cls.dialog.cancel()
            +        cls.root.update_idletasks()
            +        cls.root.destroy()
            +        del cls.dialog, cls.root
            +
            +    def test_toggle_level(self):
            +        dialog = self.dialog
            +
            +        def stackorder():
            +            """Get the stack order of the children of the frame.
            +
            +            winfo_children() stores the children in stack order, so
            +            this can be used to check whether a frame is above or
            +            below another one.
            +            """
            +            for index, child in enumerate(dialog.frame.winfo_children()):
            +                if child._name == 'keyseq_basic':
            +                    basic = index
            +                if child._name == 'keyseq_advanced':
            +                    advanced = index
            +            return basic, advanced
            +
            +        # New window starts at basic level.
            +        self.assertFalse(dialog.advanced)
            +        self.assertIn('Advanced', dialog.button_level['text'])
            +        basic, advanced = stackorder()
            +        self.assertGreater(basic, advanced)
            +
            +        # Toggle to advanced.
            +        dialog.toggle_level()
            +        self.assertTrue(dialog.advanced)
            +        self.assertIn('Basic', dialog.button_level['text'])
            +        basic, advanced = stackorder()
            +        self.assertGreater(advanced, basic)
            +
            +        # Toggle to basic.
            +        dialog.button_level.invoke()
            +        self.assertFalse(dialog.advanced)
            +        self.assertIn('Advanced', dialog.button_level['text'])
            +        basic, advanced = stackorder()
            +        self.assertGreater(basic, advanced)
            +
            +
            +class KeySelectionTest(unittest.TestCase):
            +    "Test selecting key on Basic frames."
            +
            +    class Basic(gkd):
            +        def __init__(self, *args, **kwargs):
            +            super().__init__(*args, **kwargs)
            +            class list_keys_final:
            +                get = Func()
            +                select_clear = Func()
            +                yview = Func()
            +            self.list_keys_final = list_keys_final
            +        def set_modifiers_for_platform(self):
            +            self.modifiers = ['foo', 'bar', 'BAZ']
            +            self.modifier_label = {'BAZ': 'ZZZ'}
            +        showerror = Mbox_func()
            +
            +    @classmethod
            +    def setUpClass(cls):
            +        requires('gui')
            +        cls.root = Tk()
            +        cls.root.withdraw()
            +        cls.dialog = cls.Basic(cls.root, 'Title', '<>', [], _utest=True)
            +
            +    @classmethod
            +    def tearDownClass(cls):
            +        cls.dialog.cancel()
            +        cls.root.update_idletasks()
            +        cls.root.destroy()
            +        del cls.dialog, cls.root
            +
            +    def setUp(self):
            +        self.dialog.clear_key_seq()
            +
            +    def test_get_modifiers(self):
            +        dialog = self.dialog
            +        gm = dialog.get_modifiers
            +        eq = self.assertEqual
            +
            +        # Modifiers are set on/off by invoking the checkbutton.
            +        dialog.modifier_checkbuttons['foo'].invoke()
            +        eq(gm(), ['foo'])
            +
            +        dialog.modifier_checkbuttons['BAZ'].invoke()
            +        eq(gm(), ['foo', 'BAZ'])
            +
            +        dialog.modifier_checkbuttons['foo'].invoke()
            +        eq(gm(), ['BAZ'])
            +
            +    @mock.patch.object(gkd, 'get_modifiers')
            +    def test_build_key_string(self, mock_modifiers):
            +        dialog = self.dialog
            +        key = dialog.list_keys_final
            +        string = dialog.key_string.get
            +        eq = self.assertEqual
            +
            +        key.get.result = 'a'
            +        mock_modifiers.return_value = []
            +        dialog.build_key_string()
            +        eq(string(), '')
            +
            +        mock_modifiers.return_value = ['mymod']
            +        dialog.build_key_string()
            +        eq(string(), '')
            +
            +        key.get.result = ''
            +        mock_modifiers.return_value = ['mymod', 'test']
            +        dialog.build_key_string()
            +        eq(string(), '')
            +
            +    @mock.patch.object(gkd, 'get_modifiers')
            +    def test_final_key_selected(self, mock_modifiers):
            +        dialog = self.dialog
            +        key = dialog.list_keys_final
            +        string = dialog.key_string.get
            +        eq = self.assertEqual
            +
            +        mock_modifiers.return_value = ['Shift']
            +        key.get.result = '{'
            +        dialog.final_key_selected()
            +        eq(string(), '')
            +
            +
            +class CancelTest(unittest.TestCase):
            +    "Simulate user clicking [Cancel] button."
            +
            +    @classmethod
            +    def setUpClass(cls):
            +        requires('gui')
            +        cls.root = Tk()
            +        cls.root.withdraw()
            +        cls.dialog = gkd(cls.root, 'Title', '<>', [], _utest=True)
            +
            +    @classmethod
            +    def tearDownClass(cls):
            +        cls.dialog.cancel()
            +        cls.root.update_idletasks()
            +        cls.root.destroy()
            +        del cls.dialog, cls.root
            +
            +    def test_cancel(self):
            +        self.assertEqual(self.dialog.winfo_class(), 'Toplevel')
            +        self.dialog.button_cancel.invoke()
            +        with self.assertRaises(TclError):
            +            self.dialog.winfo_class()
            +        self.assertEqual(self.dialog.result, '')
            +
            +
            +class HelperTest(unittest.TestCase):
            +    "Test module level helper functions."
            +
            +    def test_translate_key(self):
            +        tr = config_key.translate_key
            +        eq = self.assertEqual
            +
            +        # Letters return unchanged with no 'Shift'.
            +        eq(tr('q', []), 'Key-q')
            +        eq(tr('q', ['Control', 'Alt']), 'Key-q')
            +
            +        # 'Shift' uppercases single lowercase letters.
            +        eq(tr('q', ['Shift']), 'Key-Q')
            +        eq(tr('q', ['Control', 'Shift']), 'Key-Q')
            +        eq(tr('q', ['Control', 'Alt', 'Shift']), 'Key-Q')
            +
            +        # Convert key name to keysym.
            +        eq(tr('Page Up', []), 'Key-Prior')
            +        # 'Shift' doesn't change case when it's not a single char.
            +        eq(tr('*', ['Shift']), 'Key-asterisk')
            +
            +
             if __name__ == '__main__':
                 unittest.main(verbosity=2)
            diff --git a/Lib/idlelib/idle_test/test_configdialog.py b/Lib/idlelib/idle_test/test_configdialog.py
            index 982dc0b7eff7e1..1fea6d41df811c 100644
            --- a/Lib/idlelib/idle_test/test_configdialog.py
            +++ b/Lib/idlelib/idle_test/test_configdialog.py
            @@ -1,7 +1,6 @@
            -"""Test idlelib.configdialog.
            +"""Test configdialog, coverage 94%.
             
             Half the class creates dialog, half works with user customizations.
            -Coverage: 95%.
             """
             from idlelib import configdialog
             from test.support import requires
            @@ -9,7 +8,7 @@
             import unittest
             from unittest import mock
             from idlelib.idle_test.mock_idle import Func
            -from tkinter import Tk, Frame, StringVar, IntVar, BooleanVar, DISABLED, NORMAL
            +from tkinter import (Tk, StringVar, IntVar, BooleanVar, DISABLED, NORMAL)
             from idlelib import config
             from idlelib.configdialog import idleConf, changes, tracers
             
            @@ -31,6 +30,7 @@
             keyspage = changes['keys']
             extpage = changes['extensions']
             
            +
             def setUpModule():
                 global root, dialog
                 idleConf.userCfg = testcfg
            @@ -38,6 +38,7 @@ def setUpModule():
                 # root.withdraw()    # Comment out, see issue 30870
                 dialog = configdialog.ConfigDialog(root, 'Test', _utest=True)
             
            +
             def tearDownModule():
                 global root, dialog
                 idleConf.userCfg = usercfg
            @@ -49,6 +50,58 @@ def tearDownModule():
                 root = dialog = None
             
             
            +class ConfigDialogTest(unittest.TestCase):
            +
            +    def test_deactivate_current_config(self):
            +        pass
            +
            +    def activate_config_changes(self):
            +        pass
            +
            +
            +class ButtonTest(unittest.TestCase):
            +
            +    def test_click_ok(self):
            +        d = dialog
            +        apply = d.apply = mock.Mock()
            +        destroy = d.destroy = mock.Mock()
            +        d.buttons['Ok'].invoke()
            +        apply.assert_called_once()
            +        destroy.assert_called_once()
            +        del d.destroy, d.apply
            +
            +    def test_click_apply(self):
            +        d = dialog
            +        deactivate = d.deactivate_current_config = mock.Mock()
            +        save_ext = d.save_all_changed_extensions = mock.Mock()
            +        activate = d.activate_config_changes = mock.Mock()
            +        d.buttons['Apply'].invoke()
            +        deactivate.assert_called_once()
            +        save_ext.assert_called_once()
            +        activate.assert_called_once()
            +        del d.save_all_changed_extensions
            +        del d.activate_config_changes, d.deactivate_current_config
            +
            +    def test_click_cancel(self):
            +        d = dialog
            +        d.destroy = Func()
            +        changes['main']['something'] = 1
            +        d.buttons['Cancel'].invoke()
            +        self.assertEqual(changes['main'], {})
            +        self.assertEqual(d.destroy.called, 1)
            +        del d.destroy
            +
            +    def test_click_help(self):
            +        dialog.note.select(dialog.keyspage)
            +        with mock.patch.object(configdialog, 'view_text',
            +                               new_callable=Func) as view:
            +            dialog.buttons['Help'].invoke()
            +            title, contents = view.kwds['title'], view.kwds['contents']
            +        self.assertEqual(title, 'Help for IDLE preferences')
            +        self.assertTrue(contents.startswith('When you click') and
            +                        contents.endswith('a different name.\n'))
            +
            +
             class FontPageTest(unittest.TestCase):
                 """Test that font widgets enable users to make font changes.
             
            @@ -61,6 +114,7 @@ def setUpClass(cls):
                     page = cls.page = dialog.fontpage
                     dialog.note.select(page)
                     page.set_samples = Func()  # Mask instance method.
            +        page.update()
             
                 @classmethod
                 def tearDownClass(cls):
            @@ -211,6 +265,7 @@ class IndentTest(unittest.TestCase):
                 @classmethod
                 def setUpClass(cls):
                     cls.page = dialog.fontpage
            +        cls.page.update()
             
                 def test_load_tab_cfg(self):
                     d = self.page
            @@ -241,6 +296,7 @@ def setUpClass(cls):
                     page.paint_theme_sample = Func()
                     page.set_highlight_target = Func()
                     page.set_color_sample = Func()
            +        page.update()
             
                 @classmethod
                 def tearDownClass(cls):
            @@ -418,6 +474,48 @@ def click_it(start):
                             eq(d.highlight_target.get(), elem[tag])
                             eq(d.set_highlight_target.called, count)
             
            +    def test_highlight_sample_double_click(self):
            +        # Test double click on highlight_sample.
            +        eq = self.assertEqual
            +        d = self.page
            +
            +        hs = d.highlight_sample
            +        hs.focus_force()
            +        hs.see(1.0)
            +        hs.update_idletasks()
            +
            +        # Test binding from configdialog.
            +        hs.event_generate('', x=0, y=0)
            +        hs.event_generate('', x=0, y=0)
            +        # Double click is a sequence of two clicks in a row.
            +        for _ in range(2):
            +            hs.event_generate('', x=0, y=0)
            +            hs.event_generate('', x=0, y=0)
            +
            +        eq(hs.tag_ranges('sel'), ())
            +
            +    def test_highlight_sample_b1_motion(self):
            +        # Test button motion on highlight_sample.
            +        eq = self.assertEqual
            +        d = self.page
            +
            +        hs = d.highlight_sample
            +        hs.focus_force()
            +        hs.see(1.0)
            +        hs.update_idletasks()
            +
            +        x, y, dx, dy, offset = hs.dlineinfo('1.0')
            +
            +        # Test binding from configdialog.
            +        hs.event_generate('')
            +        hs.event_generate('')
            +        hs.event_generate('', x=x, y=y)
            +        hs.event_generate('', x=x, y=y)
            +        hs.event_generate('', x=dx, y=dy)
            +        hs.event_generate('', x=dx, y=dy)
            +
            +        eq(hs.tag_ranges('sel'), ())
            +
                 def test_set_theme_type(self):
                     eq = self.assertEqual
                     d = self.page
            @@ -604,40 +702,35 @@ def test_set_color_sample(self):
             
                 def test_paint_theme_sample(self):
                     eq = self.assertEqual
            -        d = self.page
            -        del d.paint_theme_sample
            -        hs_tag = d.highlight_sample.tag_cget
            +        page = self.page
            +        del page.paint_theme_sample  # Delete masking mock.
            +        hs_tag = page.highlight_sample.tag_cget
                     gh = idleConf.GetHighlight
            -        fg = 'foreground'
            -        bg = 'background'
             
                     # Create custom theme based on IDLE Dark.
            -        d.theme_source.set(True)
            -        d.builtin_name.set('IDLE Dark')
            +        page.theme_source.set(True)
            +        page.builtin_name.set('IDLE Dark')
                     theme = 'IDLE Test'
            -        d.create_new(theme)
            -        d.set_color_sample.called = 0
            +        page.create_new(theme)
            +        page.set_color_sample.called = 0
             
                     # Base theme with nothing in `changes`.
            -        d.paint_theme_sample()
            -        eq(hs_tag('break', fg), gh(theme, 'break', fgBg='fg'))
            -        eq(hs_tag('cursor', bg), gh(theme, 'normal', fgBg='bg'))
            -        self.assertNotEqual(hs_tag('console', fg), 'blue')
            -        self.assertNotEqual(hs_tag('console', bg), 'yellow')
            -        eq(d.set_color_sample.called, 1)
            +        page.paint_theme_sample()
            +        new_console = {'foreground': 'blue',
            +                       'background': 'yellow',}
            +        for key, value in new_console.items():
            +            self.assertNotEqual(hs_tag('console', key), value)
            +        eq(page.set_color_sample.called, 1)
             
                     # Apply changes.
            -        changes.add_option('highlight', theme, 'console-foreground', 'blue')
            -        changes.add_option('highlight', theme, 'console-background', 'yellow')
            -        d.paint_theme_sample()
            -
            -        eq(hs_tag('break', fg), gh(theme, 'break', fgBg='fg'))
            -        eq(hs_tag('cursor', bg), gh(theme, 'normal', fgBg='bg'))
            -        eq(hs_tag('console', fg), 'blue')
            -        eq(hs_tag('console', bg), 'yellow')
            -        eq(d.set_color_sample.called, 2)
            +        for key, value in new_console.items():
            +            changes.add_option('highlight', theme, 'console-'+key, value)
            +        page.paint_theme_sample()
            +        for key, value in new_console.items():
            +            eq(hs_tag('console', key), value)
            +        eq(page.set_color_sample.called, 2)
             
            -        d.paint_theme_sample = Func()
            +        page.paint_theme_sample = Func()
             
                 def test_delete_custom(self):
                     eq = self.assertEqual
            @@ -651,8 +744,13 @@ def test_delete_custom(self):
                     idleConf.userCfg['highlight'].SetOption(theme_name, 'name', 'value')
                     highpage[theme_name] = {'option': 'True'}
             
            +        theme_name2 = 'other theme'
            +        idleConf.userCfg['highlight'].SetOption(theme_name2, 'name', 'value')
            +        highpage[theme_name2] = {'option': 'False'}
            +
                     # Force custom theme.
            -        d.theme_source.set(False)
            +        d.custom_theme_on.state(('!disabled',))
            +        d.custom_theme_on.invoke()
                     d.custom_name.set(theme_name)
             
                     # Cancel deletion.
            @@ -660,7 +758,7 @@ def test_delete_custom(self):
                     d.button_delete_custom.invoke()
                     eq(yesno.called, 1)
                     eq(highpage[theme_name], {'option': 'True'})
            -        eq(idleConf.GetSectionList('user', 'highlight'), ['spam theme'])
            +        eq(idleConf.GetSectionList('user', 'highlight'), [theme_name, theme_name2])
                     eq(dialog.deactivate_current_config.called, 0)
                     eq(dialog.activate_config_changes.called, 0)
                     eq(d.set_theme_type.called, 0)
            @@ -670,13 +768,26 @@ def test_delete_custom(self):
                     d.button_delete_custom.invoke()
                     eq(yesno.called, 2)
                     self.assertNotIn(theme_name, highpage)
            -        eq(idleConf.GetSectionList('user', 'highlight'), [])
            -        eq(d.custom_theme_on.state(), ('disabled',))
            -        eq(d.custom_name.get(), '- no custom themes -')
            +        eq(idleConf.GetSectionList('user', 'highlight'), [theme_name2])
            +        eq(d.custom_theme_on.state(), ())
            +        eq(d.custom_name.get(), theme_name2)
                     eq(dialog.deactivate_current_config.called, 1)
                     eq(dialog.activate_config_changes.called, 1)
                     eq(d.set_theme_type.called, 1)
             
            +        # Confirm deletion of second theme - empties list.
            +        d.custom_name.set(theme_name2)
            +        yesno.result = True
            +        d.button_delete_custom.invoke()
            +        eq(yesno.called, 3)
            +        self.assertNotIn(theme_name, highpage)
            +        eq(idleConf.GetSectionList('user', 'highlight'), [])
            +        eq(d.custom_theme_on.state(), ('disabled',))
            +        eq(d.custom_name.get(), '- no custom themes -')
            +        eq(dialog.deactivate_current_config.called, 2)
            +        eq(dialog.activate_config_changes.called, 2)
            +        eq(d.set_theme_type.called, 2)
            +
                     del dialog.activate_config_changes, dialog.deactivate_current_config
                     del d.askyesno
             
            @@ -1044,8 +1155,13 @@ def test_delete_custom_keys(self):
                     idleConf.userCfg['keys'].SetOption(keyset_name, 'name', 'value')
                     keyspage[keyset_name] = {'option': 'True'}
             
            +        keyset_name2 = 'other key set'
            +        idleConf.userCfg['keys'].SetOption(keyset_name2, 'name', 'value')
            +        keyspage[keyset_name2] = {'option': 'False'}
            +
                     # Force custom keyset.
            -        d.keyset_source.set(False)
            +        d.custom_keyset_on.state(('!disabled',))
            +        d.custom_keyset_on.invoke()
                     d.custom_name.set(keyset_name)
             
                     # Cancel deletion.
            @@ -1053,7 +1169,7 @@ def test_delete_custom_keys(self):
                     d.button_delete_custom_keys.invoke()
                     eq(yesno.called, 1)
                     eq(keyspage[keyset_name], {'option': 'True'})
            -        eq(idleConf.GetSectionList('user', 'keys'), ['spam key set'])
            +        eq(idleConf.GetSectionList('user', 'keys'), [keyset_name, keyset_name2])
                     eq(dialog.deactivate_current_config.called, 0)
                     eq(dialog.activate_config_changes.called, 0)
                     eq(d.set_keys_type.called, 0)
            @@ -1063,13 +1179,26 @@ def test_delete_custom_keys(self):
                     d.button_delete_custom_keys.invoke()
                     eq(yesno.called, 2)
                     self.assertNotIn(keyset_name, keyspage)
            -        eq(idleConf.GetSectionList('user', 'keys'), [])
            -        eq(d.custom_keyset_on.state(), ('disabled',))
            -        eq(d.custom_name.get(), '- no custom keys -')
            +        eq(idleConf.GetSectionList('user', 'keys'), [keyset_name2])
            +        eq(d.custom_keyset_on.state(), ())
            +        eq(d.custom_name.get(), keyset_name2)
                     eq(dialog.deactivate_current_config.called, 1)
                     eq(dialog.activate_config_changes.called, 1)
                     eq(d.set_keys_type.called, 1)
             
            +        # Confirm deletion of second keyset - empties list.
            +        d.custom_name.set(keyset_name2)
            +        yesno.result = True
            +        d.button_delete_custom_keys.invoke()
            +        eq(yesno.called, 3)
            +        self.assertNotIn(keyset_name, keyspage)
            +        eq(idleConf.GetSectionList('user', 'keys'), [])
            +        eq(d.custom_keyset_on.state(), ('disabled',))
            +        eq(d.custom_name.get(), '- no custom keys -')
            +        eq(dialog.deactivate_current_config.called, 2)
            +        eq(dialog.activate_config_changes.called, 2)
            +        eq(d.set_keys_type.called, 2)
            +
                     del dialog.activate_config_changes, dialog.deactivate_current_config
                     del d.askyesno
             
            @@ -1086,6 +1215,7 @@ def setUpClass(cls):
                     dialog.note.select(page)
                     page.set = page.set_add_delete_state = Func()
                     page.upc = page.update_help_changes = Func()
            +        page.update()
             
                 @classmethod
                 def tearDownClass(cls):
            @@ -1137,6 +1267,10 @@ def test_editor_size(self):
                     d.win_width_int.insert(0, '11')
                     self.assertEqual(mainpage, {'EditorWindow': {'width': '11'}})
             
            +    def test_cursor_blink(self):
            +        self.page.cursor_blink_bool.invoke()
            +        self.assertEqual(mainpage, {'EditorWindow': {'cursor-blink': 'False'}})
            +
                 def test_autocomplete_wait(self):
                     self.page.auto_wait_int.delete(0, 'end')
                     self.page.auto_wait_int.insert(0, '11')
            @@ -1170,7 +1304,7 @@ def test_paragraph(self):
                 def test_context(self):
                     self.page.context_int.delete(0, 'end')
                     self.page.context_int.insert(0, '1')
            -        self.assertEqual(extpage, {'CodeContext': {'numlines': '1'}})
            +        self.assertEqual(extpage, {'CodeContext': {'maxlines': '1'}})
             
                 def test_source_selected(self):
                     d = self.page
            diff --git a/Lib/idlelib/idle_test/test_debugger.py b/Lib/idlelib/idle_test/test_debugger.py
            index bcba9a45c160a9..35efb3411c73b5 100644
            --- a/Lib/idlelib/idle_test/test_debugger.py
            +++ b/Lib/idlelib/idle_test/test_debugger.py
            @@ -1,11 +1,9 @@
            -''' Test idlelib.debugger.
            +"Test debugger, coverage 19%"
             
            -Coverage: 19%
            -'''
             from idlelib import debugger
            +import unittest
             from test.support import requires
             requires('gui')
            -import unittest
             from tkinter import Tk
             
             
            @@ -25,5 +23,7 @@ def test_init(self):
                     debugger.NamespaceViewer(self.root, 'Test')
             
             
            +# Other classes are Idb, Debugger, and StackViewer.
            +
             if __name__ == '__main__':
                 unittest.main(verbosity=2)
            diff --git a/Lib/idlelib/idle_test/test_debugger_r.py b/Lib/idlelib/idle_test/test_debugger_r.py
            new file mode 100644
            index 00000000000000..199f63447ce6ca
            --- /dev/null
            +++ b/Lib/idlelib/idle_test/test_debugger_r.py
            @@ -0,0 +1,29 @@
            +"Test debugger_r, coverage 30%."
            +
            +from idlelib import debugger_r
            +import unittest
            +from test.support import requires
            +from tkinter import Tk
            +
            +
            +class Test(unittest.TestCase):
            +
            +##    @classmethod
            +##    def setUpClass(cls):
            +##        requires('gui')
            +##        cls.root = Tk()
            +##
            +##    @classmethod
            +##    def tearDownClass(cls):
            +##        cls.root.destroy()
            +##        del cls.root
            +
            +    def test_init(self):
            +        self.assertTrue(True)  # Get coverage of import
            +
            +
            +# Classes GUIProxy, IdbAdapter, FrameProxy, CodeProxy, DictProxy,
            +# GUIAdapter, IdbProxy plus 7 module functions.
            +
            +if __name__ == '__main__':
            +    unittest.main(verbosity=2)
            diff --git a/Lib/idlelib/idle_test/test_debugobj.py b/Lib/idlelib/idle_test/test_debugobj.py
            new file mode 100644
            index 00000000000000..131ce22b8bb69b
            --- /dev/null
            +++ b/Lib/idlelib/idle_test/test_debugobj.py
            @@ -0,0 +1,57 @@
            +"Test debugobj, coverage 40%."
            +
            +from idlelib import debugobj
            +import unittest
            +
            +
            +class ObjectTreeItemTest(unittest.TestCase):
            +
            +    def test_init(self):
            +        ti = debugobj.ObjectTreeItem('label', 22)
            +        self.assertEqual(ti.labeltext, 'label')
            +        self.assertEqual(ti.object, 22)
            +        self.assertEqual(ti.setfunction, None)
            +
            +
            +class ClassTreeItemTest(unittest.TestCase):
            +
            +    def test_isexpandable(self):
            +        ti = debugobj.ClassTreeItem('label', 0)
            +        self.assertTrue(ti.IsExpandable())
            +
            +
            +class AtomicObjectTreeItemTest(unittest.TestCase):
            +
            +    def test_isexpandable(self):
            +        ti = debugobj.AtomicObjectTreeItem('label', 0)
            +        self.assertFalse(ti.IsExpandable())
            +
            +
            +class SequenceTreeItemTest(unittest.TestCase):
            +
            +    def test_isexpandable(self):
            +        ti = debugobj.SequenceTreeItem('label', ())
            +        self.assertFalse(ti.IsExpandable())
            +        ti = debugobj.SequenceTreeItem('label', (1,))
            +        self.assertTrue(ti.IsExpandable())
            +
            +    def test_keys(self):
            +        ti = debugobj.SequenceTreeItem('label', 'abc')
            +        self.assertEqual(list(ti.keys()), [0, 1, 2])
            +
            +
            +class DictTreeItemTest(unittest.TestCase):
            +
            +    def test_isexpandable(self):
            +        ti = debugobj.DictTreeItem('label', {})
            +        self.assertFalse(ti.IsExpandable())
            +        ti = debugobj.DictTreeItem('label', {1:1})
            +        self.assertTrue(ti.IsExpandable())
            +
            +    def test_keys(self):
            +        ti = debugobj.DictTreeItem('label', {1:1, 0:0, 2:2})
            +        self.assertEqual(ti.keys(), [0, 1, 2])
            +
            +
            +if __name__ == '__main__':
            +    unittest.main(verbosity=2)
            diff --git a/Lib/idlelib/idle_test/test_debugobj_r.py b/Lib/idlelib/idle_test/test_debugobj_r.py
            new file mode 100644
            index 00000000000000..86e51b6cb2cb22
            --- /dev/null
            +++ b/Lib/idlelib/idle_test/test_debugobj_r.py
            @@ -0,0 +1,22 @@
            +"Test debugobj_r, coverage 56%."
            +
            +from idlelib import debugobj_r
            +import unittest
            +
            +
            +class WrappedObjectTreeItemTest(unittest.TestCase):
            +
            +    def test_getattr(self):
            +        ti = debugobj_r.WrappedObjectTreeItem(list)
            +        self.assertEqual(ti.append, list.append)
            +
            +class StubObjectTreeItemTest(unittest.TestCase):
            +
            +    def test_init(self):
            +        ti = debugobj_r.StubObjectTreeItem('socket', 1111)
            +        self.assertEqual(ti.sockio, 'socket')
            +        self.assertEqual(ti.oid, 1111)
            +
            +
            +if __name__ == '__main__':
            +    unittest.main(verbosity=2)
            diff --git a/Lib/idlelib/idle_test/test_delegator.py b/Lib/idlelib/idle_test/test_delegator.py
            index 85624fbc127c85..922416297a42e0 100644
            --- a/Lib/idlelib/idle_test/test_delegator.py
            +++ b/Lib/idlelib/idle_test/test_delegator.py
            @@ -1,5 +1,8 @@
            -import unittest
            +"Test delegator, coverage 100%."
            +
             from idlelib.delegator import Delegator
            +import unittest
            +
             
             class DelegatorTest(unittest.TestCase):
             
            @@ -36,5 +39,6 @@ def test_mydel(self):
                     self.assertEqual(mydel._Delegator__cache, set())
                     self.assertIs(mydel.delegate, float)
             
            +
             if __name__ == '__main__':
                 unittest.main(verbosity=2, exit=2)
            diff --git a/Lib/idlelib/idle_test/test_editmenu.py b/Lib/idlelib/idle_test/test_editmenu.py
            index 17eb25c4b4c0d9..17478473a3d1b2 100644
            --- a/Lib/idlelib/idle_test/test_editmenu.py
            +++ b/Lib/idlelib/idle_test/test_editmenu.py
            @@ -1,6 +1,6 @@
             '''Test (selected) IDLE Edit menu items.
             
            -Edit modules have their own test files files
            +Edit modules have their own test files
             '''
             from test.support import requires
             requires('gui')
            diff --git a/Lib/idlelib/idle_test/test_editor.py b/Lib/idlelib/idle_test/test_editor.py
            index 64a2a88b7e3765..443dcf021679fc 100644
            --- a/Lib/idlelib/idle_test/test_editor.py
            +++ b/Lib/idlelib/idle_test/test_editor.py
            @@ -1,14 +1,220 @@
            +"Test editor, coverage 35%."
            +
            +from idlelib import editor
             import unittest
            -from idlelib.editor import EditorWindow
            -
            -class Editor_func_test(unittest.TestCase):
            -    def test_filename_to_unicode(self):
            -        func = EditorWindow._filename_to_unicode
            -        class dummy(): filesystemencoding = 'utf-8'
            -        pairs = (('abc', 'abc'), ('a\U00011111c', 'a\ufffdc'),
            -                 (b'abc', 'abc'), (b'a\xf0\x91\x84\x91c', 'a\ufffdc'))
            -        for inp, out in pairs:
            -            self.assertEqual(func(dummy, inp), out)
            +from collections import namedtuple
            +from test.support import requires
            +from tkinter import Tk
            +from idlelib.idle_test.mock_idle import Func
            +
            +Editor = editor.EditorWindow
            +
            +
            +class EditorWindowTest(unittest.TestCase):
            +
            +    @classmethod
            +    def setUpClass(cls):
            +        requires('gui')
            +        cls.root = Tk()
            +        cls.root.withdraw()
            +
            +    @classmethod
            +    def tearDownClass(cls):
            +        cls.root.update_idletasks()
            +        for id in cls.root.tk.call('after', 'info'):
            +            cls.root.after_cancel(id)
            +        cls.root.destroy()
            +        del cls.root
            +
            +    def test_init(self):
            +        e = Editor(root=self.root)
            +        self.assertEqual(e.root, self.root)
            +        e._close()
            +
            +
            +class TestGetLineIndent(unittest.TestCase):
            +    def test_empty_lines(self):
            +        for tabwidth in [1, 2, 4, 6, 8]:
            +            for line in ['', '\n']:
            +                with self.subTest(line=line, tabwidth=tabwidth):
            +                    self.assertEqual(
            +                        editor.get_line_indent(line, tabwidth=tabwidth),
            +                        (0, 0),
            +                    )
            +
            +    def test_tabwidth_4(self):
            +        #        (line, (raw, effective))
            +        tests = (('no spaces', (0, 0)),
            +                 # Internal space isn't counted.
            +                 ('    space test', (4, 4)),
            +                 ('\ttab test', (1, 4)),
            +                 ('\t\tdouble tabs test', (2, 8)),
            +                 # Different results when mixing tabs and spaces.
            +                 ('    \tmixed test', (5, 8)),
            +                 ('  \t  mixed test', (5, 6)),
            +                 ('\t    mixed test', (5, 8)),
            +                 # Spaces not divisible by tabwidth.
            +                 ('  \tmixed test', (3, 4)),
            +                 (' \t mixed test', (3, 5)),
            +                 ('\t  mixed test', (3, 6)),
            +                 # Only checks spaces and tabs.
            +                 ('\nnewline test', (0, 0)))
            +
            +        for line, expected in tests:
            +            with self.subTest(line=line):
            +                self.assertEqual(
            +                    editor.get_line_indent(line, tabwidth=4),
            +                    expected,
            +                )
            +
            +    def test_tabwidth_8(self):
            +        #        (line, (raw, effective))
            +        tests = (('no spaces', (0, 0)),
            +                 # Internal space isn't counted.
            +                 ('        space test', (8, 8)),
            +                 ('\ttab test', (1, 8)),
            +                 ('\t\tdouble tabs test', (2, 16)),
            +                 # Different results when mixing tabs and spaces.
            +                 ('        \tmixed test', (9, 16)),
            +                 ('      \t  mixed test', (9, 10)),
            +                 ('\t        mixed test', (9, 16)),
            +                 # Spaces not divisible by tabwidth.
            +                 ('  \tmixed test', (3, 8)),
            +                 (' \t mixed test', (3, 9)),
            +                 ('\t  mixed test', (3, 10)),
            +                 # Only checks spaces and tabs.
            +                 ('\nnewline test', (0, 0)))
            +
            +        for line, expected in tests:
            +            with self.subTest(line=line):
            +                self.assertEqual(
            +                    editor.get_line_indent(line, tabwidth=8),
            +                    expected,
            +                )
            +
            +
            +def insert(text, string):
            +    text.delete('1.0', 'end')
            +    text.insert('end', string)
            +    text.update()  # Force update for colorizer to finish.
            +
            +
            +class IndentAndNewlineTest(unittest.TestCase):
            +
            +    @classmethod
            +    def setUpClass(cls):
            +        requires('gui')
            +        cls.root = Tk()
            +        cls.root.withdraw()
            +        cls.window = Editor(root=cls.root)
            +        cls.window.indentwidth = 2
            +        cls.window.tabwidth = 2
            +
            +    @classmethod
            +    def tearDownClass(cls):
            +        cls.window._close()
            +        del cls.window
            +        cls.root.update_idletasks()
            +        for id in cls.root.tk.call('after', 'info'):
            +            cls.root.after_cancel(id)
            +        cls.root.destroy()
            +        del cls.root
            +
            +    def test_indent_and_newline_event(self):
            +        eq = self.assertEqual
            +        w = self.window
            +        text = w.text
            +        get = text.get
            +        nl = w.newline_and_indent_event
            +
            +        TestInfo = namedtuple('Tests', ['label', 'text', 'expected', 'mark'])
            +
            +        tests = (TestInfo('Empty line inserts with no indent.',
            +                          '  \n  def __init__(self):',
            +                          '\n  \n  def __init__(self):\n',
            +                          '1.end'),
            +                 TestInfo('Inside bracket before space, deletes space.',
            +                          '  def f1(self, a, b):',
            +                          '  def f1(self,\n         a, b):\n',
            +                          '1.14'),
            +                 TestInfo('Inside bracket after space, deletes space.',
            +                          '  def f1(self, a, b):',
            +                          '  def f1(self,\n         a, b):\n',
            +                          '1.15'),
            +                 TestInfo('Inside string with one line - no indent.',
            +                          '  """Docstring."""',
            +                          '  """Docstring.\n"""\n',
            +                          '1.15'),
            +                 TestInfo('Inside string with more than one line.',
            +                          '  """Docstring.\n  Docstring Line 2"""',
            +                          '  """Docstring.\n  Docstring Line 2\n  """\n',
            +                          '2.18'),
            +                 TestInfo('Backslash with one line.',
            +                          'a =\\',
            +                          'a =\\\n  \n',
            +                          '1.end'),
            +                 TestInfo('Backslash with more than one line.',
            +                          'a =\\\n          multiline\\',
            +                          'a =\\\n          multiline\\\n          \n',
            +                          '2.end'),
            +                 TestInfo('Block opener - indents +1 level.',
            +                          '  def f1(self):\n    pass',
            +                          '  def f1(self):\n    \n    pass\n',
            +                          '1.end'),
            +                 TestInfo('Block closer - dedents -1 level.',
            +                          '  def f1(self):\n    pass',
            +                          '  def f1(self):\n    pass\n  \n',
            +                          '2.end'),
            +                 )
            +
            +        w.prompt_last_line = ''
            +        for test in tests:
            +            with self.subTest(label=test.label):
            +                insert(text, test.text)
            +                text.mark_set('insert', test.mark)
            +                nl(event=None)
            +                eq(get('1.0', 'end'), test.expected)
            +
            +        # Selected text.
            +        insert(text, '  def f1(self, a, b):\n    return a + b')
            +        text.tag_add('sel', '1.17', '1.end')
            +        nl(None)
            +        # Deletes selected text before adding new line.
            +        eq(get('1.0', 'end'), '  def f1(self, a,\n         \n    return a + b\n')
            +
            +        # Preserves the whitespace in shell prompt.
            +        w.prompt_last_line = '>>> '
            +        insert(text, '>>> \t\ta =')
            +        text.mark_set('insert', '1.5')
            +        nl(None)
            +        eq(get('1.0', 'end'), '>>> \na =\n')
            +
            +
            +class RMenuTest(unittest.TestCase):
            +
            +    @classmethod
            +    def setUpClass(cls):
            +        requires('gui')
            +        cls.root = Tk()
            +        cls.root.withdraw()
            +        cls.window = Editor(root=cls.root)
            +
            +    @classmethod
            +    def tearDownClass(cls):
            +        cls.window._close()
            +        del cls.window
            +        cls.root.update_idletasks()
            +        for id in cls.root.tk.call('after', 'info'):
            +            cls.root.after_cancel(id)
            +        cls.root.destroy()
            +        del cls.root
            +
            +    class DummyRMenu:
            +        def tk_popup(x, y): pass
            +
            +    def test_rclick(self):
            +        pass
            +
             
             if __name__ == '__main__':
                 unittest.main(verbosity=2)
            diff --git a/Lib/idlelib/idle_test/test_filelist.py b/Lib/idlelib/idle_test/test_filelist.py
            new file mode 100644
            index 00000000000000..731f1975e50e23
            --- /dev/null
            +++ b/Lib/idlelib/idle_test/test_filelist.py
            @@ -0,0 +1,33 @@
            +"Test filelist, coverage 19%."
            +
            +from idlelib import filelist
            +import unittest
            +from test.support import requires
            +from tkinter import Tk
            +
            +class FileListTest(unittest.TestCase):
            +
            +    @classmethod
            +    def setUpClass(cls):
            +        requires('gui')
            +        cls.root = Tk()
            +        cls.root.withdraw()
            +
            +    @classmethod
            +    def tearDownClass(cls):
            +        cls.root.update_idletasks()
            +        for id in cls.root.tk.call('after', 'info'):
            +            cls.root.after_cancel(id)
            +        cls.root.destroy()
            +        del cls.root
            +
            +    def test_new_empty(self):
            +        flist = filelist.FileList(self.root)
            +        self.assertEqual(flist.root, self.root)
            +        e = flist.new()
            +        self.assertEqual(type(e), flist.EditorWindow)
            +        e._close()
            +
            +
            +if __name__ == '__main__':
            +    unittest.main(verbosity=2)
            diff --git a/Lib/idlelib/idle_test/test_paragraph.py b/Lib/idlelib/idle_test/test_format.py
            similarity index 55%
            rename from Lib/idlelib/idle_test/test_paragraph.py
            rename to Lib/idlelib/idle_test/test_format.py
            index ba350c976534e8..a79bb515089e7b 100644
            --- a/Lib/idlelib/idle_test/test_paragraph.py
            +++ b/Lib/idlelib/idle_test/test_format.py
            @@ -1,9 +1,12 @@
            -# Test the functions and main class method of paragraph.py
            +"Test format, coverage 99%."
            +
            +from idlelib import format as ft
             import unittest
            -from idlelib import paragraph as fp
            -from idlelib.editor import EditorWindow
            -from tkinter import Tk, Text
            +from unittest import mock
             from test.support import requires
            +from tkinter import Tk, Text
            +from idlelib.editor import EditorWindow
            +from idlelib.idle_test.mock_idle import Editor as MockEditor
             
             
             class Is_Get_Test(unittest.TestCase):
            @@ -15,26 +18,26 @@ class Is_Get_Test(unittest.TestCase):
                 leadingws_nocomment = '    This is not a comment'
             
                 def test_is_all_white(self):
            -        self.assertTrue(fp.is_all_white(''))
            -        self.assertTrue(fp.is_all_white('\t\n\r\f\v'))
            -        self.assertFalse(fp.is_all_white(self.test_comment))
            +        self.assertTrue(ft.is_all_white(''))
            +        self.assertTrue(ft.is_all_white('\t\n\r\f\v'))
            +        self.assertFalse(ft.is_all_white(self.test_comment))
             
                 def test_get_indent(self):
                     Equal = self.assertEqual
            -        Equal(fp.get_indent(self.test_comment), '')
            -        Equal(fp.get_indent(self.trailingws_comment), '')
            -        Equal(fp.get_indent(self.leadingws_comment), '    ')
            -        Equal(fp.get_indent(self.leadingws_nocomment), '    ')
            +        Equal(ft.get_indent(self.test_comment), '')
            +        Equal(ft.get_indent(self.trailingws_comment), '')
            +        Equal(ft.get_indent(self.leadingws_comment), '    ')
            +        Equal(ft.get_indent(self.leadingws_nocomment), '    ')
             
                 def test_get_comment_header(self):
                     Equal = self.assertEqual
                     # Test comment strings
            -        Equal(fp.get_comment_header(self.test_comment), '#')
            -        Equal(fp.get_comment_header(self.trailingws_comment), '#')
            -        Equal(fp.get_comment_header(self.leadingws_comment), '    #')
            +        Equal(ft.get_comment_header(self.test_comment), '#')
            +        Equal(ft.get_comment_header(self.trailingws_comment), '#')
            +        Equal(ft.get_comment_header(self.leadingws_comment), '    #')
                     # Test non-comment strings
            -        Equal(fp.get_comment_header(self.leadingws_nocomment), '    ')
            -        Equal(fp.get_comment_header(self.test_nocomment), '')
            +        Equal(ft.get_comment_header(self.leadingws_nocomment), '    ')
            +        Equal(ft.get_comment_header(self.test_nocomment), '')
             
             
             class FindTest(unittest.TestCase):
            @@ -62,7 +65,7 @@ def runcase(self, inserttext, stopline, expected):
                         linelength = int(text.index("%d.end" % line).split('.')[1])
                         for col in (0, linelength//2, linelength):
                             tempindex = "%d.%d" % (line, col)
            -                self.assertEqual(fp.find_paragraph(text, tempindex), expected)
            +                self.assertEqual(ft.find_paragraph(text, tempindex), expected)
                     text.delete('1.0', 'end')
             
                 def test_find_comment(self):
            @@ -161,7 +164,7 @@ class ReformatFunctionTest(unittest.TestCase):
             
                 def test_reformat_paragraph(self):
                     Equal = self.assertEqual
            -        reform = fp.reformat_paragraph
            +        reform = ft.reformat_paragraph
                     hw = "O hello world"
                     Equal(reform(' ', 1), ' ')
                     Equal(reform("Hello    world", 20), "Hello  world")
            @@ -192,7 +195,7 @@ def test_reformat_comment(self):
                     test_string = (
                         "    \"\"\"this is a test of a reformat for a triple quoted string"
                         " will it reformat to less than 70 characters for me?\"\"\"")
            -        result = fp.reformat_comment(test_string, 70, "    ")
            +        result = ft.reformat_comment(test_string, 70, "    ")
                     expected = (
                         "    \"\"\"this is a test of a reformat for a triple quoted string will it\n"
                         "    reformat to less than 70 characters for me?\"\"\"")
            @@ -201,7 +204,7 @@ def test_reformat_comment(self):
                     test_comment = (
                         "# this is a test of a reformat for a triple quoted string will "
                         "it reformat to less than 70 characters for me?")
            -        result = fp.reformat_comment(test_comment, 70, "#")
            +        result = ft.reformat_comment(test_comment, 70, "#")
                     expected = (
                         "# this is a test of a reformat for a triple quoted string will it\n"
                         "# reformat to less than 70 characters for me?")
            @@ -210,7 +213,7 @@ def test_reformat_comment(self):
             
             class FormatClassTest(unittest.TestCase):
                 def test_init_close(self):
            -        instance = fp.FormatParagraph('editor')
            +        instance = ft.FormatParagraph('editor')
                     self.assertEqual(instance.editwin, 'editor')
                     instance.close()
                     self.assertEqual(instance.editwin, None)
            @@ -269,14 +272,16 @@ class FormatEventTest(unittest.TestCase):
                 def setUpClass(cls):
                     requires('gui')
                     cls.root = Tk()
            +        cls.root.withdraw()
                     editor = Editor(root=cls.root)
                     cls.text = editor.text.text  # Test code does not need the wrapper.
            -        cls.formatter = fp.FormatParagraph(editor).format_paragraph_event
            +        cls.formatter = ft.FormatParagraph(editor).format_paragraph_event
                     # Sets the insert mark just after the re-wrapped and inserted  text.
             
                 @classmethod
                 def tearDownClass(cls):
                     del cls.text, cls.formatter
            +        cls.root.update_idletasks()
                     cls.root.destroy()
                     del cls.root
             
            @@ -372,5 +377,292 @@ def test_comment_block(self):
             ##        text.delete('1.0', 'end')
             
             
            +class DummyEditwin:
            +    def __init__(self, root, text):
            +        self.root = root
            +        self.text = text
            +        self.indentwidth = 4
            +        self.tabwidth = 4
            +        self.usetabs = False
            +        self.context_use_ps1 = True
            +
            +    _make_blanks = EditorWindow._make_blanks
            +    get_selection_indices = EditorWindow.get_selection_indices
            +
            +
            +class FormatRegionTest(unittest.TestCase):
            +
            +    @classmethod
            +    def setUpClass(cls):
            +        requires('gui')
            +        cls.root = Tk()
            +        cls.root.withdraw()
            +        cls.text = Text(cls.root)
            +        cls.text.undo_block_start = mock.Mock()
            +        cls.text.undo_block_stop = mock.Mock()
            +        cls.editor = DummyEditwin(cls.root, cls.text)
            +        cls.formatter = ft.FormatRegion(cls.editor)
            +
            +    @classmethod
            +    def tearDownClass(cls):
            +        del cls.text, cls.formatter, cls.editor
            +        cls.root.update_idletasks()
            +        cls.root.destroy()
            +        del cls.root
            +
            +    def setUp(self):
            +        self.text.insert('1.0', self.code_sample)
            +
            +    def tearDown(self):
            +        self.text.delete('1.0', 'end')
            +
            +    code_sample = """\
            +# WS line needed for test.
            +class C1():
            +    # Class comment.
            +    def __init__(self, a, b):
            +        self.a = a
            +        self.b = b
            +
            +    def compare(self):
            +        if a > b:
            +            return a
            +        elif a < b:
            +            return b
            +        else:
            +            return None
            +"""
            +
            +    def test_get_region(self):
            +        get = self.formatter.get_region
            +        text = self.text
            +        eq = self.assertEqual
            +
            +        # Add selection.
            +        text.tag_add('sel', '7.0', '10.0')
            +        expected_lines = ['',
            +                          '    def compare(self):',
            +                          '        if a > b:',
            +                          '']
            +        eq(get(), ('7.0', '10.0', '\n'.join(expected_lines), expected_lines))
            +
            +        # Remove selection.
            +        text.tag_remove('sel', '1.0', 'end')
            +        eq(get(), ('15.0', '16.0', '\n', ['', '']))
            +
            +    def test_set_region(self):
            +        set_ = self.formatter.set_region
            +        text = self.text
            +        eq = self.assertEqual
            +
            +        save_bell = text.bell
            +        text.bell = mock.Mock()
            +        line6 = self.code_sample.splitlines()[5]
            +        line10 = self.code_sample.splitlines()[9]
            +
            +        text.tag_add('sel', '6.0', '11.0')
            +        head, tail, chars, lines = self.formatter.get_region()
            +
            +        # No changes.
            +        set_(head, tail, chars, lines)
            +        text.bell.assert_called_once()
            +        eq(text.get('6.0', '11.0'), chars)
            +        eq(text.get('sel.first', 'sel.last'), chars)
            +        text.tag_remove('sel', '1.0', 'end')
            +
            +        # Alter selected lines by changing lines and adding a newline.
            +        newstring = 'added line 1\n\n\n\n'
            +        newlines = newstring.split('\n')
            +        set_('7.0', '10.0', chars, newlines)
            +        # Selection changed.
            +        eq(text.get('sel.first', 'sel.last'), newstring)
            +        # Additional line added, so last index is changed.
            +        eq(text.get('7.0', '11.0'), newstring)
            +        # Before and after lines unchanged.
            +        eq(text.get('6.0', '7.0-1c'), line6)
            +        eq(text.get('11.0', '12.0-1c'), line10)
            +        text.tag_remove('sel', '1.0', 'end')
            +
            +        text.bell = save_bell
            +
            +    def test_indent_region_event(self):
            +        indent = self.formatter.indent_region_event
            +        text = self.text
            +        eq = self.assertEqual
            +
            +        text.tag_add('sel', '7.0', '10.0')
            +        indent()
            +        # Blank lines aren't affected by indent.
            +        eq(text.get('7.0', '10.0'), ('\n        def compare(self):\n            if a > b:\n'))
            +
            +    def test_dedent_region_event(self):
            +        dedent = self.formatter.dedent_region_event
            +        text = self.text
            +        eq = self.assertEqual
            +
            +        text.tag_add('sel', '7.0', '10.0')
            +        dedent()
            +        # Blank lines aren't affected by dedent.
            +        eq(text.get('7.0', '10.0'), ('\ndef compare(self):\n    if a > b:\n'))
            +
            +    def test_comment_region_event(self):
            +        comment = self.formatter.comment_region_event
            +        text = self.text
            +        eq = self.assertEqual
            +
            +        text.tag_add('sel', '7.0', '10.0')
            +        comment()
            +        eq(text.get('7.0', '10.0'), ('##\n##    def compare(self):\n##        if a > b:\n'))
            +
            +    def test_uncomment_region_event(self):
            +        comment = self.formatter.comment_region_event
            +        uncomment = self.formatter.uncomment_region_event
            +        text = self.text
            +        eq = self.assertEqual
            +
            +        text.tag_add('sel', '7.0', '10.0')
            +        comment()
            +        uncomment()
            +        eq(text.get('7.0', '10.0'), ('\n    def compare(self):\n        if a > b:\n'))
            +
            +        # Only remove comments at the beginning of a line.
            +        text.tag_remove('sel', '1.0', 'end')
            +        text.tag_add('sel', '3.0', '4.0')
            +        uncomment()
            +        eq(text.get('3.0', '3.end'), ('    # Class comment.'))
            +
            +        self.formatter.set_region('3.0', '4.0', '', ['# Class comment.', ''])
            +        uncomment()
            +        eq(text.get('3.0', '3.end'), (' Class comment.'))
            +
            +    @mock.patch.object(ft.FormatRegion, "_asktabwidth")
            +    def test_tabify_region_event(self, _asktabwidth):
            +        tabify = self.formatter.tabify_region_event
            +        text = self.text
            +        eq = self.assertEqual
            +
            +        text.tag_add('sel', '7.0', '10.0')
            +        # No tabwidth selected.
            +        _asktabwidth.return_value = None
            +        self.assertIsNone(tabify())
            +
            +        _asktabwidth.return_value = 3
            +        self.assertIsNotNone(tabify())
            +        eq(text.get('7.0', '10.0'), ('\n\t def compare(self):\n\t\t  if a > b:\n'))
            +
            +    @mock.patch.object(ft.FormatRegion, "_asktabwidth")
            +    def test_untabify_region_event(self, _asktabwidth):
            +        untabify = self.formatter.untabify_region_event
            +        text = self.text
            +        eq = self.assertEqual
            +
            +        text.tag_add('sel', '7.0', '10.0')
            +        # No tabwidth selected.
            +        _asktabwidth.return_value = None
            +        self.assertIsNone(untabify())
            +
            +        _asktabwidth.return_value = 2
            +        self.formatter.tabify_region_event()
            +        _asktabwidth.return_value = 3
            +        self.assertIsNotNone(untabify())
            +        eq(text.get('7.0', '10.0'), ('\n      def compare(self):\n            if a > b:\n'))
            +
            +    @mock.patch.object(ft, "askinteger")
            +    def test_ask_tabwidth(self, askinteger):
            +        ask = self.formatter._asktabwidth
            +        askinteger.return_value = 10
            +        self.assertEqual(ask(), 10)
            +
            +
            +class IndentsTest(unittest.TestCase):
            +
            +    @mock.patch.object(ft, "askyesno")
            +    def test_toggle_tabs(self, askyesno):
            +        editor = DummyEditwin(None, None)  # usetabs == False.
            +        indents = ft.Indents(editor)
            +        askyesno.return_value = True
            +
            +        indents.toggle_tabs_event(None)
            +        self.assertEqual(editor.usetabs, True)
            +        self.assertEqual(editor.indentwidth, 8)
            +
            +        indents.toggle_tabs_event(None)
            +        self.assertEqual(editor.usetabs, False)
            +        self.assertEqual(editor.indentwidth, 8)
            +
            +    @mock.patch.object(ft, "askinteger")
            +    def test_change_indentwidth(self, askinteger):
            +        editor = DummyEditwin(None, None)  # indentwidth == 4.
            +        indents = ft.Indents(editor)
            +
            +        askinteger.return_value = None
            +        indents.change_indentwidth_event(None)
            +        self.assertEqual(editor.indentwidth, 4)
            +
            +        askinteger.return_value = 3
            +        indents.change_indentwidth_event(None)
            +        self.assertEqual(editor.indentwidth, 3)
            +
            +        askinteger.return_value = 5
            +        editor.usetabs = True
            +        indents.change_indentwidth_event(None)
            +        self.assertEqual(editor.indentwidth, 3)
            +
            +
            +class RstripTest(unittest.TestCase):
            +
            +    @classmethod
            +    def setUpClass(cls):
            +        requires('gui')
            +        cls.root = Tk()
            +        cls.root.withdraw()
            +        cls.text = Text(cls.root)
            +        cls.editor = MockEditor(text=cls.text)
            +        cls.do_rstrip = ft.Rstrip(cls.editor).do_rstrip
            +
            +    @classmethod
            +    def tearDownClass(cls):
            +        del cls.text, cls.do_rstrip, cls.editor
            +        cls.root.update_idletasks()
            +        cls.root.destroy()
            +        del cls.root
            +
            +    def tearDown(self):
            +        self.text.delete('1.0', 'end-1c')
            +
            +    def test_rstrip_lines(self):
            +        original = (
            +            "Line with an ending tab    \n"
            +            "Line ending in 5 spaces     \n"
            +            "Linewithnospaces\n"
            +            "    indented line\n"
            +            "    indented line with trailing space \n"
            +            "    \n")
            +        stripped = (
            +            "Line with an ending tab\n"
            +            "Line ending in 5 spaces\n"
            +            "Linewithnospaces\n"
            +            "    indented line\n"
            +            "    indented line with trailing space\n")
            +
            +        self.text.insert('1.0', original)
            +        self.do_rstrip()
            +        self.assertEqual(self.text.get('1.0', 'insert'), stripped)
            +
            +    def test_rstrip_end(self):
            +        text = self.text
            +        for code in ('', '\n', '\n\n\n'):
            +            with self.subTest(code=code):
            +                text.insert('1.0', code)
            +                self.do_rstrip()
            +                self.assertEqual(text.get('1.0','end-1c'), '')
            +        for code in ('a\n', 'a\n\n', 'a\n\n\n'):
            +            with self.subTest(code=code):
            +                text.delete('1.0', 'end-1c')
            +                text.insert('1.0', code)
            +                self.do_rstrip()
            +                self.assertEqual(text.get('1.0','end-1c'), 'a\n')
            +
            +
             if __name__ == '__main__':
                 unittest.main(verbosity=2, exit=2)
            diff --git a/Lib/idlelib/idle_test/test_grep.py b/Lib/idlelib/idle_test/test_grep.py
            index 6b54c1313153d1..a0b5b69171879c 100644
            --- a/Lib/idlelib/idle_test/test_grep.py
            +++ b/Lib/idlelib/idle_test/test_grep.py
            @@ -3,14 +3,16 @@
             dummy_command calls grep_it calls findfiles.
             An exception raised in one method will fail callers.
             Otherwise, tests are mostly independent.
            -*** Currently only test grep_it.
            +Currently only test grep_it, coverage 51%.
             """
            +from idlelib import grep
             import unittest
             from test.support import captured_stdout
             from idlelib.idle_test.mock_tk import Var
            -from idlelib.grep import GrepDialog
            +import os
             import re
             
            +
             class Dummy_searchengine:
                 '''GrepDialog.__init__ calls parent SearchDiabolBase which attaches the
                 passed in SearchEngine instance as attribute 'engine'. Only a few of the
            @@ -21,25 +23,97 @@ def getpat(self):
             
             searchengine = Dummy_searchengine()
             
            +
             class Dummy_grep:
                 # Methods tested
                 #default_command = GrepDialog.default_command
            -    grep_it = GrepDialog.grep_it
            -    findfiles = GrepDialog.findfiles
            +    grep_it = grep.GrepDialog.grep_it
                 # Other stuff needed
                 recvar = Var(False)
                 engine = searchengine
                 def close(self):  # gui method
                     pass
             
            -grep = Dummy_grep()
            +_grep = Dummy_grep()
            +
             
             class FindfilesTest(unittest.TestCase):
            -    # findfiles is really a function, not a method, could be iterator
            -    # test that filename return filename
            -    # test that idlelib has many .py files
            -    # test that recursive flag adds idle_test .py files
            -    pass
            +
            +    @classmethod
            +    def setUpClass(cls):
            +        cls.realpath = os.path.realpath(__file__)
            +        cls.path = os.path.dirname(cls.realpath)
            +
            +    @classmethod
            +    def tearDownClass(cls):
            +        del cls.realpath, cls.path
            +
            +    def test_invaliddir(self):
            +        with captured_stdout() as s:
            +            filelist = list(grep.findfiles('invaliddir', '*.*', False))
            +        self.assertEqual(filelist, [])
            +        self.assertIn('invalid', s.getvalue())
            +
            +    def test_curdir(self):
            +        # Test os.curdir.
            +        ff = grep.findfiles
            +        save_cwd = os.getcwd()
            +        os.chdir(self.path)
            +        filename = 'test_grep.py'
            +        filelist = list(ff(os.curdir, filename, False))
            +        self.assertIn(os.path.join(os.curdir, filename), filelist)
            +        os.chdir(save_cwd)
            +
            +    def test_base(self):
            +        ff = grep.findfiles
            +        readme = os.path.join(self.path, 'README.txt')
            +
            +        # Check for Python files in path where this file lives.
            +        filelist = list(ff(self.path, '*.py', False))
            +        # This directory has many Python files.
            +        self.assertGreater(len(filelist), 10)
            +        self.assertIn(self.realpath, filelist)
            +        self.assertNotIn(readme, filelist)
            +
            +        # Look for .txt files in path where this file lives.
            +        filelist = list(ff(self.path, '*.txt', False))
            +        self.assertNotEqual(len(filelist), 0)
            +        self.assertNotIn(self.realpath, filelist)
            +        self.assertIn(readme, filelist)
            +
            +        # Look for non-matching pattern.
            +        filelist = list(ff(self.path, 'grep.*', False))
            +        self.assertEqual(len(filelist), 0)
            +        self.assertNotIn(self.realpath, filelist)
            +
            +    def test_recurse(self):
            +        ff = grep.findfiles
            +        parent = os.path.dirname(self.path)
            +        grepfile = os.path.join(parent, 'grep.py')
            +        pat = '*.py'
            +
            +        # Get Python files only in parent directory.
            +        filelist = list(ff(parent, pat, False))
            +        parent_size = len(filelist)
            +        # Lots of Python files in idlelib.
            +        self.assertGreater(parent_size, 20)
            +        self.assertIn(grepfile, filelist)
            +        # Without subdirectories, this file isn't returned.
            +        self.assertNotIn(self.realpath, filelist)
            +
            +        # Include subdirectories.
            +        filelist = list(ff(parent, pat, True))
            +        # More files found now.
            +        self.assertGreater(len(filelist), parent_size)
            +        self.assertIn(grepfile, filelist)
            +        # This file exists in list now.
            +        self.assertIn(self.realpath, filelist)
            +
            +        # Check another level up the tree.
            +        parent = os.path.dirname(parent)
            +        filelist = list(ff(parent, '*.py', True))
            +        self.assertIn(self.realpath, filelist)
            +
             
             class Grep_itTest(unittest.TestCase):
                 # Test captured reports with 0 and some hits.
            @@ -47,9 +121,9 @@ class Grep_itTest(unittest.TestCase):
                 # from incomplete replacement, so 'later'.
             
                 def report(self, pat):
            -        grep.engine._pat = pat
            +        _grep.engine._pat = pat
                     with captured_stdout() as s:
            -            grep.grep_it(re.compile(pat), __file__)
            +            _grep.grep_it(re.compile(pat), __file__)
                     lines = s.getvalue().split('\n')
                     lines.pop()  # remove bogus '' after last \n
                     return lines
            @@ -71,10 +145,12 @@ def test_found(self):
                     self.assertIn('2', lines[3])  # hits found 2
                     self.assertTrue(lines[4].startswith('(Hint:'))
             
            +
             class Default_commandTest(unittest.TestCase):
                 # To write this, move outwin import to top of GrepDialog
                 # so it can be replaced by captured_stdout in class setup/teardown.
                 pass
             
            +
             if __name__ == '__main__':
            -    unittest.main(verbosity=2, exit=False)
            +    unittest.main(verbosity=2)
            diff --git a/Lib/idlelib/idle_test/test_help.py b/Lib/idlelib/idle_test/test_help.py
            index 2c68e23b328c24..b542659981894d 100644
            --- a/Lib/idlelib/idle_test/test_help.py
            +++ b/Lib/idlelib/idle_test/test_help.py
            @@ -1,13 +1,12 @@
            -'''Test idlelib.help.
            +"Test help, coverage 87%."
             
            -Coverage: 87%
            -'''
             from idlelib import help
            +import unittest
             from test.support import requires
             requires('gui')
             from os.path import abspath, dirname, join
             from tkinter import Tk
            -import unittest
            +
             
             class HelpFrameTest(unittest.TestCase):
             
            @@ -30,5 +29,6 @@ def test_line1(self):
                     text = self.frame.text
                     self.assertEqual(text.get('1.0', '1.end'), ' IDLE ')
             
            +
             if __name__ == '__main__':
                 unittest.main(verbosity=2)
            diff --git a/Lib/idlelib/idle_test/test_help_about.py b/Lib/idlelib/idle_test/test_help_about.py
            index 1f67aaddb3b411..7c148d23a135b6 100644
            --- a/Lib/idlelib/idle_test/test_help_about.py
            +++ b/Lib/idlelib/idle_test/test_help_about.py
            @@ -1,18 +1,19 @@
            -'''Test idlelib.help_about.
            +"""Test help_about, coverage 100%.
            +help_about.build_bits branches on sys.platform='darwin'.
            +'100% combines coverage on Mac and others.
            +"""
             
            -Coverage: 100%
            -'''
            +from idlelib import help_about
            +import unittest
             from test.support import requires, findfile
             from tkinter import Tk, TclError
            -import unittest
            -from unittest import mock
             from idlelib.idle_test.mock_idle import Func
             from idlelib.idle_test.mock_tk import Mbox_func
            -from idlelib.help_about import AboutDialog as About
            -from idlelib import help_about
             from idlelib import textview
             import os.path
            -from platform import python_version, architecture
            +from platform import python_version
            +
            +About = help_about.AboutDialog
             
             
             class LiveDialogTest(unittest.TestCase):
            @@ -50,35 +51,39 @@ def test_dialog_logo(self):
                 def test_printer_buttons(self):
                     """Test buttons whose commands use printer function."""
                     dialog = self.dialog
            -        button_sources = [(dialog.py_license, license),
            -                          (dialog.py_copyright, copyright),
            -                          (dialog.py_credits, credits)]
            -
            -        for button, printer in button_sources:
            -            printer._Printer__setup()
            -            button.invoke()
            -            get = dialog._current_textview.viewframe.textframe.text.get
            -            self.assertEqual(printer._Printer__lines[0], get('1.0', '1.end'))
            -            self.assertEqual(
            -                    printer._Printer__lines[1], get('2.0', '2.end'))
            -            dialog._current_textview.destroy()
            +        button_sources = [(dialog.py_license, license, 'license'),
            +                          (dialog.py_copyright, copyright, 'copyright'),
            +                          (dialog.py_credits, credits, 'credits')]
            +
            +        for button, printer, name in button_sources:
            +            with self.subTest(name=name):
            +                printer._Printer__setup()
            +                button.invoke()
            +                get = dialog._current_textview.viewframe.textframe.text.get
            +                lines = printer._Printer__lines
            +                if len(lines) < 2:
            +                    self.fail(name + ' full text was not found')
            +                self.assertEqual(lines[0], get('1.0', '1.end'))
            +                self.assertEqual(lines[1], get('2.0', '2.end'))
            +                dialog._current_textview.destroy()
             
                 def test_file_buttons(self):
                     """Test buttons that display files."""
                     dialog = self.dialog
            -        button_sources = [(self.dialog.readme, 'README.txt'),
            -                          (self.dialog.idle_news, 'NEWS.txt'),
            -                          (self.dialog.idle_credits, 'CREDITS.txt')]
            -
            -        for button, filename in button_sources:
            -            button.invoke()
            -            fn = findfile(filename, subdir='idlelib')
            -            get = dialog._current_textview.viewframe.textframe.text.get
            -            with open(fn) as f:
            -                self.assertEqual(f.readline().strip(), get('1.0', '1.end'))
            -                f.readline()
            -                self.assertEqual(f.readline().strip(), get('3.0', '3.end'))
            -            dialog._current_textview.destroy()
            +        button_sources = [(self.dialog.readme, 'README.txt', 'readme'),
            +                          (self.dialog.idle_news, 'NEWS.txt', 'news'),
            +                          (self.dialog.idle_credits, 'CREDITS.txt', 'credits')]
            +
            +        for button, filename, name in button_sources:
            +            with  self.subTest(name=name):
            +                button.invoke()
            +                fn = findfile(filename, subdir='idlelib')
            +                get = dialog._current_textview.viewframe.textframe.text.get
            +                with open(fn, encoding='utf-8') as f:
            +                    self.assertEqual(f.readline().strip(), get('1.0', '1.end'))
            +                    f.readline()
            +                    self.assertEqual(f.readline().strip(), get('3.0', '3.end'))
            +                dialog._current_textview.destroy()
             
             
             class DefaultTitleTest(unittest.TestCase):
            diff --git a/Lib/idlelib/idle_test/test_history.py b/Lib/idlelib/idle_test/test_history.py
            index b27801071be649..67539651444751 100644
            --- a/Lib/idlelib/idle_test/test_history.py
            +++ b/Lib/idlelib/idle_test/test_history.py
            @@ -1,15 +1,18 @@
            +" Test history, coverage 100%."
            +
            +from idlelib.history import History
             import unittest
             from test.support import requires
             
             import tkinter as tk
             from tkinter import Text as tkText
             from idlelib.idle_test.mock_tk import Text as mkText
            -from idlelib.history import History
             from idlelib.config import idleConf
             
             line1 = 'a = 7'
             line2 = 'b = a'
             
            +
             class StoreTest(unittest.TestCase):
                 '''Tests History.__init__ and History.store with mock Text'''
             
            @@ -61,6 +64,7 @@ def __getattr__(self, name):
                 def bell(self):
                     self._bell = True
             
            +
             class FetchTest(unittest.TestCase):
                 '''Test History.fetch with wrapped tk.Text.
                 '''
            diff --git a/Lib/idlelib/idle_test/test_hyperparser.py b/Lib/idlelib/idle_test/test_hyperparser.py
            index 73c8281e430d64..343843c4166e97 100644
            --- a/Lib/idlelib/idle_test/test_hyperparser.py
            +++ b/Lib/idlelib/idle_test/test_hyperparser.py
            @@ -1,16 +1,17 @@
            -"""Unittest for idlelib.hyperparser.py."""
            +"Test hyperparser, coverage 98%."
            +
            +from idlelib.hyperparser import HyperParser
             import unittest
             from test.support import requires
             from tkinter import Tk, Text
             from idlelib.editor import EditorWindow
            -from idlelib.hyperparser import HyperParser
             
             class DummyEditwin:
                 def __init__(self, text):
                     self.text = text
                     self.indentwidth = 8
                     self.tabwidth = 8
            -        self.context_use_ps1 = True
            +        self.prompt_last_line = '>>>'
                     self.num_context_lines = 50, 500, 1000
             
                 _build_char_in_string_func = EditorWindow._build_char_in_string_func
            @@ -52,7 +53,7 @@ def setUp(self):
             
                 def tearDown(self):
                     self.text.delete('1.0', 'end')
            -        self.editwin.context_use_ps1 = True
            +        self.editwin.prompt_last_line = '>>>'
             
                 def get_parser(self, index):
                     """
            @@ -70,7 +71,7 @@ def test_init(self):
                     self.assertIn('precedes', str(ve.exception))
             
                     # test without ps1
            -        self.editwin.context_use_ps1 = False
            +        self.editwin.prompt_last_line = ''
             
                     # number of lines lesser than 50
                     p = self.get_parser('end')
            @@ -270,5 +271,6 @@ def test_eat_identifier_various_lengths(self):
                         self.assertEqual(eat_id('2' + 'a' * (length - 1), 0, length), 0)
                         self.assertEqual(eat_id('2' + 'é' * (length - 1), 0, length), 0)
             
            +
             if __name__ == '__main__':
                 unittest.main(verbosity=2)
            diff --git a/Lib/idlelib/idle_test/test_iomenu.py b/Lib/idlelib/idle_test/test_iomenu.py
            index 65bf5930559562..99f40487967124 100644
            --- a/Lib/idlelib/idle_test/test_iomenu.py
            +++ b/Lib/idlelib/idle_test/test_iomenu.py
            @@ -1,233 +1,48 @@
            -import unittest
            -import io
            -
            -from idlelib.run import PseudoInputFile, PseudoOutputFile
            -
            -
            -class S(str):
            -    def __str__(self):
            -        return '%s:str' % type(self).__name__
            -    def __unicode__(self):
            -        return '%s:unicode' % type(self).__name__
            -    def __len__(self):
            -        return 3
            -    def __iter__(self):
            -        return iter('abc')
            -    def __getitem__(self, *args):
            -        return '%s:item' % type(self).__name__
            -    def __getslice__(self, *args):
            -        return '%s:slice' % type(self).__name__
            -
            -class MockShell:
            -    def __init__(self):
            -        self.reset()
            -
            -    def write(self, *args):
            -        self.written.append(args)
            -
            -    def readline(self):
            -        return self.lines.pop()
            -
            -    def close(self):
            -        pass
            -
            -    def reset(self):
            -        self.written = []
            -
            -    def push(self, lines):
            -        self.lines = list(lines)[::-1]
            -
            -
            -class PseudeOutputFilesTest(unittest.TestCase):
            -    def test_misc(self):
            -        shell = MockShell()
            -        f = PseudoOutputFile(shell, 'stdout', 'utf-8')
            -        self.assertIsInstance(f, io.TextIOBase)
            -        self.assertEqual(f.encoding, 'utf-8')
            -        self.assertIsNone(f.errors)
            -        self.assertIsNone(f.newlines)
            -        self.assertEqual(f.name, '')
            -        self.assertFalse(f.closed)
            -        self.assertTrue(f.isatty())
            -        self.assertFalse(f.readable())
            -        self.assertTrue(f.writable())
            -        self.assertFalse(f.seekable())
            -
            -    def test_unsupported(self):
            -        shell = MockShell()
            -        f = PseudoOutputFile(shell, 'stdout', 'utf-8')
            -        self.assertRaises(OSError, f.fileno)
            -        self.assertRaises(OSError, f.tell)
            -        self.assertRaises(OSError, f.seek, 0)
            -        self.assertRaises(OSError, f.read, 0)
            -        self.assertRaises(OSError, f.readline, 0)
            -
            -    def test_write(self):
            -        shell = MockShell()
            -        f = PseudoOutputFile(shell, 'stdout', 'utf-8')
            -        f.write('test')
            -        self.assertEqual(shell.written, [('test', 'stdout')])
            -        shell.reset()
            -        f.write('t\xe8st')
            -        self.assertEqual(shell.written, [('t\xe8st', 'stdout')])
            -        shell.reset()
            -
            -        f.write(S('t\xe8st'))
            -        self.assertEqual(shell.written, [('t\xe8st', 'stdout')])
            -        self.assertEqual(type(shell.written[0][0]), str)
            -        shell.reset()
            +"Test , coverage 17%."
             
            -        self.assertRaises(TypeError, f.write)
            -        self.assertEqual(shell.written, [])
            -        self.assertRaises(TypeError, f.write, b'test')
            -        self.assertRaises(TypeError, f.write, 123)
            -        self.assertEqual(shell.written, [])
            -        self.assertRaises(TypeError, f.write, 'test', 'spam')
            -        self.assertEqual(shell.written, [])
            -
            -    def test_writelines(self):
            -        shell = MockShell()
            -        f = PseudoOutputFile(shell, 'stdout', 'utf-8')
            -        f.writelines([])
            -        self.assertEqual(shell.written, [])
            -        shell.reset()
            -        f.writelines(['one\n', 'two'])
            -        self.assertEqual(shell.written,
            -                         [('one\n', 'stdout'), ('two', 'stdout')])
            -        shell.reset()
            -        f.writelines(['on\xe8\n', 'tw\xf2'])
            -        self.assertEqual(shell.written,
            -                         [('on\xe8\n', 'stdout'), ('tw\xf2', 'stdout')])
            -        shell.reset()
            -
            -        f.writelines([S('t\xe8st')])
            -        self.assertEqual(shell.written, [('t\xe8st', 'stdout')])
            -        self.assertEqual(type(shell.written[0][0]), str)
            -        shell.reset()
            -
            -        self.assertRaises(TypeError, f.writelines)
            -        self.assertEqual(shell.written, [])
            -        self.assertRaises(TypeError, f.writelines, 123)
            -        self.assertEqual(shell.written, [])
            -        self.assertRaises(TypeError, f.writelines, [b'test'])
            -        self.assertRaises(TypeError, f.writelines, [123])
            -        self.assertEqual(shell.written, [])
            -        self.assertRaises(TypeError, f.writelines, [], [])
            -        self.assertEqual(shell.written, [])
            -
            -    def test_close(self):
            -        shell = MockShell()
            -        f = PseudoOutputFile(shell, 'stdout', 'utf-8')
            -        self.assertFalse(f.closed)
            -        f.write('test')
            -        f.close()
            -        self.assertTrue(f.closed)
            -        self.assertRaises(ValueError, f.write, 'x')
            -        self.assertEqual(shell.written, [('test', 'stdout')])
            -        f.close()
            -        self.assertRaises(TypeError, f.close, 1)
            -
            -
            -class PseudeInputFilesTest(unittest.TestCase):
            -    def test_misc(self):
            -        shell = MockShell()
            -        f = PseudoInputFile(shell, 'stdin', 'utf-8')
            -        self.assertIsInstance(f, io.TextIOBase)
            -        self.assertEqual(f.encoding, 'utf-8')
            -        self.assertIsNone(f.errors)
            -        self.assertIsNone(f.newlines)
            -        self.assertEqual(f.name, '')
            -        self.assertFalse(f.closed)
            -        self.assertTrue(f.isatty())
            -        self.assertTrue(f.readable())
            -        self.assertFalse(f.writable())
            -        self.assertFalse(f.seekable())
            -
            -    def test_unsupported(self):
            -        shell = MockShell()
            -        f = PseudoInputFile(shell, 'stdin', 'utf-8')
            -        self.assertRaises(OSError, f.fileno)
            -        self.assertRaises(OSError, f.tell)
            -        self.assertRaises(OSError, f.seek, 0)
            -        self.assertRaises(OSError, f.write, 'x')
            -        self.assertRaises(OSError, f.writelines, ['x'])
            -
            -    def test_read(self):
            -        shell = MockShell()
            -        f = PseudoInputFile(shell, 'stdin', 'utf-8')
            -        shell.push(['one\n', 'two\n', ''])
            -        self.assertEqual(f.read(), 'one\ntwo\n')
            -        shell.push(['one\n', 'two\n', ''])
            -        self.assertEqual(f.read(-1), 'one\ntwo\n')
            -        shell.push(['one\n', 'two\n', ''])
            -        self.assertEqual(f.read(None), 'one\ntwo\n')
            -        shell.push(['one\n', 'two\n', 'three\n', ''])
            -        self.assertEqual(f.read(2), 'on')
            -        self.assertEqual(f.read(3), 'e\nt')
            -        self.assertEqual(f.read(10), 'wo\nthree\n')
            -
            -        shell.push(['one\n', 'two\n'])
            -        self.assertEqual(f.read(0), '')
            -        self.assertRaises(TypeError, f.read, 1.5)
            -        self.assertRaises(TypeError, f.read, '1')
            -        self.assertRaises(TypeError, f.read, 1, 1)
            -
            -    def test_readline(self):
            -        shell = MockShell()
            -        f = PseudoInputFile(shell, 'stdin', 'utf-8')
            -        shell.push(['one\n', 'two\n', 'three\n', 'four\n'])
            -        self.assertEqual(f.readline(), 'one\n')
            -        self.assertEqual(f.readline(-1), 'two\n')
            -        self.assertEqual(f.readline(None), 'three\n')
            -        shell.push(['one\ntwo\n'])
            -        self.assertEqual(f.readline(), 'one\n')
            -        self.assertEqual(f.readline(), 'two\n')
            -        shell.push(['one', 'two', 'three'])
            -        self.assertEqual(f.readline(), 'one')
            -        self.assertEqual(f.readline(), 'two')
            -        shell.push(['one\n', 'two\n', 'three\n'])
            -        self.assertEqual(f.readline(2), 'on')
            -        self.assertEqual(f.readline(1), 'e')
            -        self.assertEqual(f.readline(1), '\n')
            -        self.assertEqual(f.readline(10), 'two\n')
            -
            -        shell.push(['one\n', 'two\n'])
            -        self.assertEqual(f.readline(0), '')
            -        self.assertRaises(TypeError, f.readlines, 1.5)
            -        self.assertRaises(TypeError, f.readlines, '1')
            -        self.assertRaises(TypeError, f.readlines, 1, 1)
            -
            -    def test_readlines(self):
            -        shell = MockShell()
            -        f = PseudoInputFile(shell, 'stdin', 'utf-8')
            -        shell.push(['one\n', 'two\n', ''])
            -        self.assertEqual(f.readlines(), ['one\n', 'two\n'])
            -        shell.push(['one\n', 'two\n', ''])
            -        self.assertEqual(f.readlines(-1), ['one\n', 'two\n'])
            -        shell.push(['one\n', 'two\n', ''])
            -        self.assertEqual(f.readlines(None), ['one\n', 'two\n'])
            -        shell.push(['one\n', 'two\n', ''])
            -        self.assertEqual(f.readlines(0), ['one\n', 'two\n'])
            -        shell.push(['one\n', 'two\n', ''])
            -        self.assertEqual(f.readlines(3), ['one\n'])
            -        shell.push(['one\n', 'two\n', ''])
            -        self.assertEqual(f.readlines(4), ['one\n', 'two\n'])
            -
            -        shell.push(['one\n', 'two\n', ''])
            -        self.assertRaises(TypeError, f.readlines, 1.5)
            -        self.assertRaises(TypeError, f.readlines, '1')
            -        self.assertRaises(TypeError, f.readlines, 1, 1)
            -
            -    def test_close(self):
            -        shell = MockShell()
            -        f = PseudoInputFile(shell, 'stdin', 'utf-8')
            -        shell.push(['one\n', 'two\n', ''])
            -        self.assertFalse(f.closed)
            -        self.assertEqual(f.readline(), 'one\n')
            -        f.close()
            -        self.assertFalse(f.closed)
            -        self.assertEqual(f.readline(), 'two\n')
            -        self.assertRaises(TypeError, f.close, 1)
            +from idlelib import iomenu
            +import unittest
            +from test.support import requires
            +from tkinter import Tk
            +from idlelib.editor import EditorWindow
            +
            +
            +class IOBindingTest(unittest.TestCase):
            +
            +    @classmethod
            +    def setUpClass(cls):
            +        requires('gui')
            +        cls.root = Tk()
            +        cls.root.withdraw()
            +        cls.editwin = EditorWindow(root=cls.root)
            +        cls.io = iomenu.IOBinding(cls.editwin)
            +
            +    @classmethod
            +    def tearDownClass(cls):
            +        cls.io.close()
            +        cls.editwin._close()
            +        del cls.editwin
            +        cls.root.update_idletasks()
            +        for id in cls.root.tk.call('after', 'info'):
            +            cls.root.after_cancel(id)  # Need for EditorWindow.
            +        cls.root.destroy()
            +        del cls.root
            +
            +    def test_init(self):
            +        self.assertIs(self.io.editwin, self.editwin)
            +
            +    def test_fixnewlines_end(self):
            +        eq = self.assertEqual
            +        io = self.io
            +        fix = io.fixnewlines
            +        text = io.editwin.text
            +        self.editwin.interp = None
            +        eq(fix(), '')
            +        del self.editwin.interp
            +        text.insert(1.0, 'a')
            +        eq(fix(), 'a'+io.eol_convention)
            +        eq(text.get('1.0', 'end-1c'), 'a\n')
            +        eq(fix(), 'a'+io.eol_convention)
             
             
             if __name__ == '__main__':
            diff --git a/Lib/idlelib/idle_test/test_macosx.py b/Lib/idlelib/idle_test/test_macosx.py
            index 3d85f3ca72254c..b6bd922e4b99dd 100644
            --- a/Lib/idlelib/idle_test/test_macosx.py
            +++ b/Lib/idlelib/idle_test/test_macosx.py
            @@ -1,11 +1,9 @@
            -'''Test idlelib.macosx.py.
            +"Test macosx, coverage 45% on Windows."
             
            -Coverage: 71% on Windows.
            -'''
             from idlelib import macosx
            +import unittest
             from test.support import requires
             import tkinter as tk
            -import unittest
             import unittest.mock as mock
             from idlelib.filelist import FileList
             
            diff --git a/Lib/idlelib/idle_test/test_mainmenu.py b/Lib/idlelib/idle_test/test_mainmenu.py
            new file mode 100644
            index 00000000000000..7ec0368371c7df
            --- /dev/null
            +++ b/Lib/idlelib/idle_test/test_mainmenu.py
            @@ -0,0 +1,21 @@
            +"Test mainmenu, coverage 100%."
            +# Reported as 88%; mocking turtledemo absence would have no point.
            +
            +from idlelib import mainmenu
            +import unittest
            +
            +
            +class MainMenuTest(unittest.TestCase):
            +
            +    def test_menudefs(self):
            +        actual = [item[0] for item in mainmenu.menudefs]
            +        expect = ['file', 'edit', 'format', 'run', 'shell',
            +                  'debug', 'options', 'window', 'help']
            +        self.assertEqual(actual, expect)
            +
            +    def test_default_keydefs(self):
            +        self.assertGreaterEqual(len(mainmenu.default_keydefs), 50)
            +
            +
            +if __name__ == '__main__':
            +    unittest.main(verbosity=2)
            diff --git a/Lib/idlelib/idle_test/test_multicall.py b/Lib/idlelib/idle_test/test_multicall.py
            new file mode 100644
            index 00000000000000..ba582bb3ca51b4
            --- /dev/null
            +++ b/Lib/idlelib/idle_test/test_multicall.py
            @@ -0,0 +1,48 @@
            +"Test multicall, coverage 33%."
            +
            +from idlelib import multicall
            +import unittest
            +from test.support import requires
            +from tkinter import Tk, Text
            +
            +
            +class MultiCallTest(unittest.TestCase):
            +
            +    @classmethod
            +    def setUpClass(cls):
            +        requires('gui')
            +        cls.root = Tk()
            +        cls.root.withdraw()
            +        cls.mc = multicall.MultiCallCreator(Text)
            +
            +    @classmethod
            +    def tearDownClass(cls):
            +        del cls.mc
            +        cls.root.update_idletasks()
            +##        for id in cls.root.tk.call('after', 'info'):
            +##            cls.root.after_cancel(id)  # Need for EditorWindow.
            +        cls.root.destroy()
            +        del cls.root
            +
            +    def test_creator(self):
            +        mc = self.mc
            +        self.assertIs(multicall._multicall_dict[Text], mc)
            +        self.assertTrue(issubclass(mc, Text))
            +        mc2 = multicall.MultiCallCreator(Text)
            +        self.assertIs(mc, mc2)
            +
            +    def test_init(self):
            +        mctext = self.mc(self.root)
            +        self.assertIsInstance(mctext._MultiCall__binders, list)
            +
            +    def test_yview(self):
            +        # Added for tree.wheel_event
            +        # (it depends on yview to not be overriden)
            +        mc = self.mc
            +        self.assertIs(mc.yview, Text.yview)
            +        mctext = self.mc(self.root)
            +        self.assertIs(mctext.yview.__func__, Text.yview)
            +
            +
            +if __name__ == '__main__':
            +    unittest.main(verbosity=2)
            diff --git a/Lib/idlelib/idle_test/test_outwin.py b/Lib/idlelib/idle_test/test_outwin.py
            index 231c7bf9cfb620..cd099ecd841b3c 100644
            --- a/Lib/idlelib/idle_test/test_outwin.py
            +++ b/Lib/idlelib/idle_test/test_outwin.py
            @@ -1,12 +1,11 @@
            -""" Test idlelib.outwin.
            -"""
            +"Test outwin, coverage 76%."
             
            +from idlelib import outwin
             import unittest
            +from test.support import requires
             from tkinter import Tk, Text
             from idlelib.idle_test.mock_tk import Mbox_func
             from idlelib.idle_test.mock_idle import Func
            -from idlelib import outwin
            -from test.support import requires
             from unittest import mock
             
             
            diff --git a/Lib/idlelib/idle_test/test_parenmatch.py b/Lib/idlelib/idle_test/test_parenmatch.py
            index 3caa2754a6d8a2..4a41d8433d5483 100644
            --- a/Lib/idlelib/idle_test/test_parenmatch.py
            +++ b/Lib/idlelib/idle_test/test_parenmatch.py
            @@ -1,8 +1,8 @@
            -'''Test idlelib.parenmatch.
            +"""Test parenmatch, coverage 91%.
             
             This must currently be a gui test because ParenMatch methods use
             several text methods not defined on idlelib.idle_test.mock_tk.Text.
            -'''
            +"""
             from idlelib.parenmatch import ParenMatch
             from test.support import requires
             requires('gui')
            @@ -17,7 +17,7 @@ def __init__(self, text):
                     self.text = text
                     self.indentwidth = 8
                     self.tabwidth = 8
            -        self.context_use_ps1 = True
            +        self.prompt_last_line = '>>>' # Currently not used by parenmatch.
             
             
             class ParenMatchTest(unittest.TestCase):
            diff --git a/Lib/idlelib/idle_test/test_pathbrowser.py b/Lib/idlelib/idle_test/test_pathbrowser.py
            index 74b716a3199327..13d8b9e1ba9572 100644
            --- a/Lib/idlelib/idle_test/test_pathbrowser.py
            +++ b/Lib/idlelib/idle_test/test_pathbrowser.py
            @@ -1,19 +1,17 @@
            -""" Test idlelib.pathbrowser.
            -"""
            +"Test pathbrowser, coverage 95%."
             
            +from idlelib import pathbrowser
            +import unittest
            +from test.support import requires
            +from tkinter import Tk
             
             import os.path
             import pyclbr  # for _modules
             import sys  # for sys.path
            -from tkinter import Tk
             
            -from test.support import requires
            -import unittest
             from idlelib.idle_test.mock_idle import Func
            -
             import idlelib  # for __file__
             from idlelib import browser
            -from idlelib import pathbrowser
             from idlelib.tree import TreeNode
             
             
            diff --git a/Lib/idlelib/idle_test/test_percolator.py b/Lib/idlelib/idle_test/test_percolator.py
            index 573b9a1e8e69e3..17668ccd1227b7 100644
            --- a/Lib/idlelib/idle_test/test_percolator.py
            +++ b/Lib/idlelib/idle_test/test_percolator.py
            @@ -1,10 +1,10 @@
            -'''Test percolator.py.'''
            -from test.support import requires
            -requires('gui')
            +"Test percolator, coverage 100%."
             
            +from idlelib.percolator import Percolator, Delegator
             import unittest
            +from test.support import requires
            +requires('gui')
             from tkinter import Text, Tk, END
            -from idlelib.percolator import Percolator, Delegator
             
             
             class MyFilter(Delegator):
            diff --git a/Lib/idlelib/idle_test/test_pyparse.py b/Lib/idlelib/idle_test/test_pyparse.py
            new file mode 100644
            index 00000000000000..f21baf7534420a
            --- /dev/null
            +++ b/Lib/idlelib/idle_test/test_pyparse.py
            @@ -0,0 +1,481 @@
            +"Test pyparse, coverage 96%."
            +
            +from idlelib import pyparse
            +import unittest
            +from collections import namedtuple
            +
            +
            +class ParseMapTest(unittest.TestCase):
            +
            +    def test_parsemap(self):
            +        keepwhite = {ord(c): ord(c) for c in ' \t\n\r'}
            +        mapping = pyparse.ParseMap(keepwhite)
            +        self.assertEqual(mapping[ord('\t')], ord('\t'))
            +        self.assertEqual(mapping[ord('a')], ord('x'))
            +        self.assertEqual(mapping[1000], ord('x'))
            +
            +    def test_trans(self):
            +        # trans is the production instance of ParseMap, used in _study1
            +        parser = pyparse.Parser(4, 4)
            +        self.assertEqual('\t a([{b}])b"c\'d\n'.translate(pyparse.trans),
            +                         'xxx(((x)))x"x\'x\n')
            +
            +
            +class PyParseTest(unittest.TestCase):
            +
            +    @classmethod
            +    def setUpClass(cls):
            +        cls.parser = pyparse.Parser(indentwidth=4, tabwidth=4)
            +
            +    @classmethod
            +    def tearDownClass(cls):
            +        del cls.parser
            +
            +    def test_init(self):
            +        self.assertEqual(self.parser.indentwidth, 4)
            +        self.assertEqual(self.parser.tabwidth, 4)
            +
            +    def test_set_code(self):
            +        eq = self.assertEqual
            +        p = self.parser
            +        setcode = p.set_code
            +
            +        # Not empty and doesn't end with newline.
            +        with self.assertRaises(AssertionError):
            +            setcode('a')
            +
            +        tests = ('',
            +                 'a\n')
            +
            +        for string in tests:
            +            with self.subTest(string=string):
            +                setcode(string)
            +                eq(p.code, string)
            +                eq(p.study_level, 0)
            +
            +    def test_find_good_parse_start(self):
            +        eq = self.assertEqual
            +        p = self.parser
            +        setcode = p.set_code
            +        start = p.find_good_parse_start
            +        def char_in_string_false(index): return False
            +
            +        # First line starts with 'def' and ends with ':', then 0 is the pos.
            +        setcode('def spam():\n')
            +        eq(start(char_in_string_false), 0)
            +
            +        # First line begins with a keyword in the list and ends
            +        # with an open brace, then 0 is the pos.  This is how
            +        # hyperparser calls this function as the newline is not added
            +        # in the editor, but rather on the call to setcode.
            +        setcode('class spam( ' + ' \n')
            +        eq(start(char_in_string_false), 0)
            +
            +        # Split def across lines.
            +        setcode('"""This is a module docstring"""\n'
            +                'class C():\n'
            +                '    def __init__(self, a,\n'
            +                '                 b=True):\n'
            +                '        pass\n'
            +                )
            +
            +        # Passing no value or non-callable should fail (issue 32989).
            +        with self.assertRaises(TypeError):
            +            start()
            +        with self.assertRaises(TypeError):
            +            start(False)
            +
            +        # Make text look like a string.  This returns pos as the start
            +        # position, but it's set to None.
            +        self.assertIsNone(start(is_char_in_string=lambda index: True))
            +
            +        # Make all text look like it's not in a string.  This means that it
            +        # found a good start position.
            +        eq(start(char_in_string_false), 44)
            +
            +        # If the beginning of the def line is not in a string, then it
            +        # returns that as the index.
            +        eq(start(is_char_in_string=lambda index: index > 44), 44)
            +        # If the beginning of the def line is in a string, then it
            +        # looks for a previous index.
            +        eq(start(is_char_in_string=lambda index: index >= 44), 33)
            +        # If everything before the 'def' is in a string, then returns None.
            +        # The non-continuation def line returns 44 (see below).
            +        eq(start(is_char_in_string=lambda index: index < 44), None)
            +
            +        # Code without extra line break in def line - mostly returns the same
            +        # values.
            +        setcode('"""This is a module docstring"""\n'
            +                'class C():\n'
            +                '    def __init__(self, a, b=True):\n'
            +                '        pass\n'
            +                )
            +        eq(start(char_in_string_false), 44)
            +        eq(start(is_char_in_string=lambda index: index > 44), 44)
            +        eq(start(is_char_in_string=lambda index: index >= 44), 33)
            +        # When the def line isn't split, this returns which doesn't match the
            +        # split line test.
            +        eq(start(is_char_in_string=lambda index: index < 44), 44)
            +
            +    def test_set_lo(self):
            +        code = (
            +                '"""This is a module docstring"""\n'
            +                'class C():\n'
            +                '    def __init__(self, a,\n'
            +                '                 b=True):\n'
            +                '        pass\n'
            +                )
            +        p = self.parser
            +        p.set_code(code)
            +
            +        # Previous character is not a newline.
            +        with self.assertRaises(AssertionError):
            +            p.set_lo(5)
            +
            +        # A value of 0 doesn't change self.code.
            +        p.set_lo(0)
            +        self.assertEqual(p.code, code)
            +
            +        # An index that is preceded by a newline.
            +        p.set_lo(44)
            +        self.assertEqual(p.code, code[44:])
            +
            +    def test_study1(self):
            +        eq = self.assertEqual
            +        p = self.parser
            +        setcode = p.set_code
            +        study = p._study1
            +
            +        (NONE, BACKSLASH, FIRST, NEXT, BRACKET) = range(5)
            +        TestInfo = namedtuple('TestInfo', ['string', 'goodlines',
            +                                           'continuation'])
            +        tests = (
            +            TestInfo('', [0], NONE),
            +            # Docstrings.
            +            TestInfo('"""This is a complete docstring."""\n', [0, 1], NONE),
            +            TestInfo("'''This is a complete docstring.'''\n", [0, 1], NONE),
            +            TestInfo('"""This is a continued docstring.\n', [0, 1], FIRST),
            +            TestInfo("'''This is a continued docstring.\n", [0, 1], FIRST),
            +            TestInfo('"""Closing quote does not match."\n', [0, 1], FIRST),
            +            TestInfo('"""Bracket in docstring [\n', [0, 1], FIRST),
            +            TestInfo("'''Incomplete two line docstring.\n\n", [0, 2], NEXT),
            +            # Single-quoted strings.
            +            TestInfo('"This is a complete string."\n', [0, 1], NONE),
            +            TestInfo('"This is an incomplete string.\n', [0, 1], NONE),
            +            TestInfo("'This is more incomplete.\n\n", [0, 1, 2], NONE),
            +            # Comment (backslash does not continue comments).
            +            TestInfo('# Comment\\\n', [0, 1], NONE),
            +            # Brackets.
            +            TestInfo('("""Complete string in bracket"""\n', [0, 1], BRACKET),
            +            TestInfo('("""Open string in bracket\n', [0, 1], FIRST),
            +            TestInfo('a = (1 + 2) - 5 *\\\n', [0, 1], BACKSLASH),  # No bracket.
            +            TestInfo('\n   def function1(self, a,\n                 b):\n',
            +                     [0, 1, 3], NONE),
            +            TestInfo('\n   def function1(self, a,\\\n', [0, 1, 2], BRACKET),
            +            TestInfo('\n   def function1(self, a,\n', [0, 1, 2], BRACKET),
            +            TestInfo('())\n', [0, 1], NONE),                    # Extra closer.
            +            TestInfo(')(\n', [0, 1], BRACKET),                  # Extra closer.
            +            # For the mismatched example, it doesn't look like continuation.
            +            TestInfo('{)(]\n', [0, 1], NONE),                   # Mismatched.
            +            )
            +
            +        for test in tests:
            +            with self.subTest(string=test.string):
            +                setcode(test.string)  # resets study_level
            +                study()
            +                eq(p.study_level, 1)
            +                eq(p.goodlines, test.goodlines)
            +                eq(p.continuation, test.continuation)
            +
            +        # Called again, just returns without reprocessing.
            +        self.assertIsNone(study())
            +
            +    def test_get_continuation_type(self):
            +        eq = self.assertEqual
            +        p = self.parser
            +        setcode = p.set_code
            +        gettype = p.get_continuation_type
            +
            +        (NONE, BACKSLASH, FIRST, NEXT, BRACKET) = range(5)
            +        TestInfo = namedtuple('TestInfo', ['string', 'continuation'])
            +        tests = (
            +            TestInfo('', NONE),
            +            TestInfo('"""This is a continuation docstring.\n', FIRST),
            +            TestInfo("'''This is a multiline-continued docstring.\n\n", NEXT),
            +            TestInfo('a = (1 + 2) - 5 *\\\n', BACKSLASH),
            +            TestInfo('\n   def function1(self, a,\\\n', BRACKET)
            +            )
            +
            +        for test in tests:
            +            with self.subTest(string=test.string):
            +                setcode(test.string)
            +                eq(gettype(), test.continuation)
            +
            +    def test_study2(self):
            +        eq = self.assertEqual
            +        p = self.parser
            +        setcode = p.set_code
            +        study = p._study2
            +
            +        TestInfo = namedtuple('TestInfo', ['string', 'start', 'end', 'lastch',
            +                                           'openbracket', 'bracketing'])
            +        tests = (
            +            TestInfo('', 0, 0, '', None, ((0, 0),)),
            +            TestInfo("'''This is a multiline continuation docstring.\n\n",
            +                     0, 48, "'", None, ((0, 0), (0, 1), (48, 0))),
            +            TestInfo(' # Comment\\\n',
            +                     0, 12, '', None, ((0, 0), (1, 1), (12, 0))),
            +            # A comment without a space is a special case
            +            TestInfo(' #Comment\\\n',
            +                     0, 0, '', None, ((0, 0),)),
            +            # Backslash continuation.
            +            TestInfo('a = (1 + 2) - 5 *\\\n',
            +                     0, 19, '*', None, ((0, 0), (4, 1), (11, 0))),
            +            # Bracket continuation with close.
            +            TestInfo('\n   def function1(self, a,\n                 b):\n',
            +                     1, 48, ':', None, ((1, 0), (17, 1), (46, 0))),
            +            # Bracket continuation with unneeded backslash.
            +            TestInfo('\n   def function1(self, a,\\\n',
            +                     1, 28, ',', 17, ((1, 0), (17, 1))),
            +            # Bracket continuation.
            +            TestInfo('\n   def function1(self, a,\n',
            +                     1, 27, ',', 17, ((1, 0), (17, 1))),
            +            # Bracket continuation with comment at end of line with text.
            +            TestInfo('\n   def function1(self, a,  # End of line comment.\n',
            +                     1, 51, ',', 17, ((1, 0), (17, 1), (28, 2), (51, 1))),
            +            # Multi-line statement with comment line in between code lines.
            +            TestInfo('  a = ["first item",\n  # Comment line\n    "next item",\n',
            +                     0, 55, ',', 6, ((0, 0), (6, 1), (7, 2), (19, 1),
            +                                     (23, 2), (38, 1), (42, 2), (53, 1))),
            +            TestInfo('())\n',
            +                     0, 4, ')', None, ((0, 0), (0, 1), (2, 0), (3, 0))),
            +            TestInfo(')(\n', 0, 3, '(', 1, ((0, 0), (1, 0), (1, 1))),
            +            # Wrong closers still decrement stack level.
            +            TestInfo('{)(]\n',
            +                     0, 5, ']', None, ((0, 0), (0, 1), (2, 0), (2, 1), (4, 0))),
            +            # Character after backslash.
            +            TestInfo(':\\a\n', 0, 4, '\\a', None, ((0, 0),)),
            +            TestInfo('\n', 0, 0, '', None, ((0, 0),)),
            +            )
            +
            +        for test in tests:
            +            with self.subTest(string=test.string):
            +                setcode(test.string)
            +                study()
            +                eq(p.study_level, 2)
            +                eq(p.stmt_start, test.start)
            +                eq(p.stmt_end, test.end)
            +                eq(p.lastch, test.lastch)
            +                eq(p.lastopenbracketpos, test.openbracket)
            +                eq(p.stmt_bracketing, test.bracketing)
            +
            +        # Called again, just returns without reprocessing.
            +        self.assertIsNone(study())
            +
            +    def test_get_num_lines_in_stmt(self):
            +        eq = self.assertEqual
            +        p = self.parser
            +        setcode = p.set_code
            +        getlines = p.get_num_lines_in_stmt
            +
            +        TestInfo = namedtuple('TestInfo', ['string', 'lines'])
            +        tests = (
            +            TestInfo('[x for x in a]\n', 1),      # Closed on one line.
            +            TestInfo('[x\nfor x in a\n', 2),      # Not closed.
            +            TestInfo('[x\\\nfor x in a\\\n', 2),  # "", uneeded backslashes.
            +            TestInfo('[x\nfor x in a\n]\n', 3),   # Closed on multi-line.
            +            TestInfo('\n"""Docstring comment L1"""\nL2\nL3\nL4\n', 1),
            +            TestInfo('\n"""Docstring comment L1\nL2"""\nL3\nL4\n', 1),
            +            TestInfo('\n"""Docstring comment L1\\\nL2\\\nL3\\\nL4\\\n', 4),
            +            TestInfo('\n\n"""Docstring comment L1\\\nL2\\\nL3\\\nL4\\\n"""\n', 5)
            +            )
            +
            +        # Blank string doesn't have enough elements in goodlines.
            +        setcode('')
            +        with self.assertRaises(IndexError):
            +            getlines()
            +
            +        for test in tests:
            +            with self.subTest(string=test.string):
            +                setcode(test.string)
            +                eq(getlines(), test.lines)
            +
            +    def test_compute_bracket_indent(self):
            +        eq = self.assertEqual
            +        p = self.parser
            +        setcode = p.set_code
            +        indent = p.compute_bracket_indent
            +
            +        TestInfo = namedtuple('TestInfo', ['string', 'spaces'])
            +        tests = (
            +            TestInfo('def function1(self, a,\n', 14),
            +            # Characters after bracket.
            +            TestInfo('\n    def function1(self, a,\n', 18),
            +            TestInfo('\n\tdef function1(self, a,\n', 18),
            +            # No characters after bracket.
            +            TestInfo('\n    def function1(\n', 8),
            +            TestInfo('\n\tdef function1(\n', 8),
            +            TestInfo('\n    def function1(  \n', 8),  # Ignore extra spaces.
            +            TestInfo('[\n"first item",\n  # Comment line\n    "next item",\n', 0),
            +            TestInfo('[\n  "first item",\n  # Comment line\n    "next item",\n', 2),
            +            TestInfo('["first item",\n  # Comment line\n    "next item",\n', 1),
            +            TestInfo('(\n', 4),
            +            TestInfo('(a\n', 1),
            +             )
            +
            +        # Must be C_BRACKET continuation type.
            +        setcode('def function1(self, a, b):\n')
            +        with self.assertRaises(AssertionError):
            +            indent()
            +
            +        for test in tests:
            +            setcode(test.string)
            +            eq(indent(), test.spaces)
            +
            +    def test_compute_backslash_indent(self):
            +        eq = self.assertEqual
            +        p = self.parser
            +        setcode = p.set_code
            +        indent = p.compute_backslash_indent
            +
            +        # Must be C_BACKSLASH continuation type.
            +        errors = (('def function1(self, a, b\\\n'),  # Bracket.
            +                  ('    """ (\\\n'),                 # Docstring.
            +                  ('a = #\\\n'),                     # Inline comment.
            +                  )
            +        for string in errors:
            +            with self.subTest(string=string):
            +                setcode(string)
            +                with self.assertRaises(AssertionError):
            +                    indent()
            +
            +        TestInfo = namedtuple('TestInfo', ('string', 'spaces'))
            +        tests = (TestInfo('a = (1 + 2) - 5 *\\\n', 4),
            +                 TestInfo('a = 1 + 2 - 5 *\\\n', 4),
            +                 TestInfo('    a = 1 + 2 - 5 *\\\n', 8),
            +                 TestInfo('  a = "spam"\\\n', 6),
            +                 TestInfo('  a = \\\n"a"\\\n', 4),
            +                 TestInfo('  a = #\\\n"a"\\\n', 5),
            +                 TestInfo('a == \\\n', 2),
            +                 TestInfo('a != \\\n', 2),
            +                 # Difference between containing = and those not.
            +                 TestInfo('\\\n', 2),
            +                 TestInfo('    \\\n', 6),
            +                 TestInfo('\t\\\n', 6),
            +                 TestInfo('a\\\n', 3),
            +                 TestInfo('{}\\\n', 4),
            +                 TestInfo('(1 + 2) - 5 *\\\n', 3),
            +                 )
            +        for test in tests:
            +            with self.subTest(string=test.string):
            +                setcode(test.string)
            +                eq(indent(), test.spaces)
            +
            +    def test_get_base_indent_string(self):
            +        eq = self.assertEqual
            +        p = self.parser
            +        setcode = p.set_code
            +        baseindent = p.get_base_indent_string
            +
            +        TestInfo = namedtuple('TestInfo', ['string', 'indent'])
            +        tests = (TestInfo('', ''),
            +                 TestInfo('def a():\n', ''),
            +                 TestInfo('\tdef a():\n', '\t'),
            +                 TestInfo('    def a():\n', '    '),
            +                 TestInfo('    def a(\n', '    '),
            +                 TestInfo('\t\n    def a(\n', '    '),
            +                 TestInfo('\t\n    # Comment.\n', '    '),
            +                 )
            +
            +        for test in tests:
            +            with self.subTest(string=test.string):
            +                setcode(test.string)
            +                eq(baseindent(), test.indent)
            +
            +    def test_is_block_opener(self):
            +        yes = self.assertTrue
            +        no = self.assertFalse
            +        p = self.parser
            +        setcode = p.set_code
            +        opener = p.is_block_opener
            +
            +        TestInfo = namedtuple('TestInfo', ['string', 'assert_'])
            +        tests = (
            +            TestInfo('def a():\n', yes),
            +            TestInfo('\n   def function1(self, a,\n                 b):\n', yes),
            +            TestInfo(':\n', yes),
            +            TestInfo('a:\n', yes),
            +            TestInfo('):\n', yes),
            +            TestInfo('(:\n', yes),
            +            TestInfo('":\n', no),
            +            TestInfo('\n   def function1(self, a,\n', no),
            +            TestInfo('def function1(self, a):\n    pass\n', no),
            +            TestInfo('# A comment:\n', no),
            +            TestInfo('"""A docstring:\n', no),
            +            TestInfo('"""A docstring:\n', no),
            +            )
            +
            +        for test in tests:
            +            with self.subTest(string=test.string):
            +                setcode(test.string)
            +                test.assert_(opener())
            +
            +    def test_is_block_closer(self):
            +        yes = self.assertTrue
            +        no = self.assertFalse
            +        p = self.parser
            +        setcode = p.set_code
            +        closer = p.is_block_closer
            +
            +        TestInfo = namedtuple('TestInfo', ['string', 'assert_'])
            +        tests = (
            +            TestInfo('return\n', yes),
            +            TestInfo('\tbreak\n', yes),
            +            TestInfo('  continue\n', yes),
            +            TestInfo('     raise\n', yes),
            +            TestInfo('pass    \n', yes),
            +            TestInfo('pass\t\n', yes),
            +            TestInfo('return #\n', yes),
            +            TestInfo('raised\n', no),
            +            TestInfo('returning\n', no),
            +            TestInfo('# return\n', no),
            +            TestInfo('"""break\n', no),
            +            TestInfo('"continue\n', no),
            +            TestInfo('def function1(self, a):\n    pass\n', yes),
            +            )
            +
            +        for test in tests:
            +            with self.subTest(string=test.string):
            +                setcode(test.string)
            +                test.assert_(closer())
            +
            +    def test_get_last_stmt_bracketing(self):
            +        eq = self.assertEqual
            +        p = self.parser
            +        setcode = p.set_code
            +        bracketing = p.get_last_stmt_bracketing
            +
            +        TestInfo = namedtuple('TestInfo', ['string', 'bracket'])
            +        tests = (
            +            TestInfo('', ((0, 0),)),
            +            TestInfo('a\n', ((0, 0),)),
            +            TestInfo('()()\n', ((0, 0), (0, 1), (2, 0), (2, 1), (4, 0))),
            +            TestInfo('(\n)()\n', ((0, 0), (0, 1), (3, 0), (3, 1), (5, 0))),
            +            TestInfo('()\n()\n', ((3, 0), (3, 1), (5, 0))),
            +            TestInfo('()(\n)\n', ((0, 0), (0, 1), (2, 0), (2, 1), (5, 0))),
            +            TestInfo('(())\n', ((0, 0), (0, 1), (1, 2), (3, 1), (4, 0))),
            +            TestInfo('(\n())\n', ((0, 0), (0, 1), (2, 2), (4, 1), (5, 0))),
            +            # Same as matched test.
            +            TestInfo('{)(]\n', ((0, 0), (0, 1), (2, 0), (2, 1), (4, 0))),
            +            TestInfo('(((())\n',
            +                     ((0, 0), (0, 1), (1, 2), (2, 3), (3, 4), (5, 3), (6, 2))),
            +            )
            +
            +        for test in tests:
            +            with self.subTest(string=test.string):
            +                setcode(test.string)
            +                eq(bracketing(), test.bracket)
            +
            +
            +if __name__ == '__main__':
            +    unittest.main(verbosity=2)
            diff --git a/Lib/idlelib/idle_test/test_pyshell.py b/Lib/idlelib/idle_test/test_pyshell.py
            new file mode 100644
            index 00000000000000..4a096676f25796
            --- /dev/null
            +++ b/Lib/idlelib/idle_test/test_pyshell.py
            @@ -0,0 +1,64 @@
            +"Test pyshell, coverage 12%."
            +# Plus coverage of test_warning.  Was 20% with test_openshell.
            +
            +from idlelib import pyshell
            +import unittest
            +from test.support import requires
            +from tkinter import Tk
            +
            +
            +class FunctionTest(unittest.TestCase):
            +    # Test stand-alone module level non-gui functions.
            +
            +    def test_restart_line_wide(self):
            +        eq = self.assertEqual
            +        for file, mul, extra in (('', 22, ''), ('finame', 21, '=')):
            +            width = 60
            +            bar = mul * '='
            +            with self.subTest(file=file, bar=bar):
            +                file = file or 'Shell'
            +                line = pyshell.restart_line(width, file)
            +                eq(len(line), width)
            +                eq(line, f"{bar+extra} RESTART: {file} {bar}")
            +
            +    def test_restart_line_narrow(self):
            +        expect, taglen = "= RESTART: Shell", 16
            +        for width in (taglen-1, taglen, taglen+1):
            +            with self.subTest(width=width):
            +                self.assertEqual(pyshell.restart_line(width, ''), expect)
            +        self.assertEqual(pyshell.restart_line(taglen+2, ''), expect+' =')
            +
            +
            +class PyShellFileListTest(unittest.TestCase):
            +
            +    @classmethod
            +    def setUpClass(cls):
            +        requires('gui')
            +        cls.root = Tk()
            +        cls.root.withdraw()
            +
            +    @classmethod
            +    def tearDownClass(cls):
            +        #cls.root.update_idletasks()
            +##        for id in cls.root.tk.call('after', 'info'):
            +##            cls.root.after_cancel(id)  # Need for EditorWindow.
            +        cls.root.destroy()
            +        del cls.root
            +
            +    def test_init(self):
            +        psfl = pyshell.PyShellFileList(self.root)
            +        self.assertEqual(psfl.EditorWindow, pyshell.PyShellEditorWindow)
            +        self.assertIsNone(psfl.pyshell)
            +
            +# The following sometimes causes 'invalid command name "109734456recolorize"'.
            +# Uncommenting after_cancel above prevents this, but results in
            +# TclError: bad window path name ".!listedtoplevel.!frame.text"
            +# which is normally prevented by after_cancel.
            +##    def test_openshell(self):
            +##        pyshell.use_subprocess = False
            +##        ps = pyshell.PyShellFileList(self.root).open_shell()
            +##        self.assertIsInstance(ps, pyshell.PyShell)
            +
            +
            +if __name__ == '__main__':
            +    unittest.main(verbosity=2)
            diff --git a/Lib/idlelib/idle_test/test_query.py b/Lib/idlelib/idle_test/test_query.py
            index 953f24f0a5ac82..6d026cb5320683 100644
            --- a/Lib/idlelib/idle_test/test_query.py
            +++ b/Lib/idlelib/idle_test/test_query.py
            @@ -1,4 +1,4 @@
            -"""Test idlelib.query.
            +"""Test query, coverage 93%).
             
             Non-gui tests for Query, SectionName, ModuleName, and HelpSource use
             dummy versions that extract the non-gui methods and add other needed
            @@ -8,17 +8,15 @@
             
             The appearance of the widgets is checked by the Query and
             HelpSource htests.  These are run by running query.py.
            -
            -Coverage: 94% (100% for Query and SectionName).
            -6 of 8 missing are ModuleName exceptions I don't know how to trigger.
             """
            +from idlelib import query
            +import unittest
             from test.support import requires
            +from tkinter import Tk, END
            +
             import sys
            -from tkinter import Tk
            -import unittest
             from unittest import mock
             from idlelib.idle_test.mock_tk import Var
            -from idlelib import query
             
             
             # NON-GUI TESTS
            @@ -32,11 +30,9 @@ class Dummy_Query:
                     ok = query.Query.ok
                     cancel = query.Query.cancel
                     # Add attributes and initialization needed for tests.
            -        entry = Var()
            -        entry_error = {}
                     def __init__(self, dummy_entry):
            -            self.entry.set(dummy_entry)
            -            self.entry_error['text'] = ''
            +            self.entry = Var(value=dummy_entry)
            +            self.entry_error = {'text': ''}
                         self.result = None
                         self.destroyed = False
                     def showerror(self, message):
            @@ -82,11 +78,9 @@ class SectionNameTest(unittest.TestCase):
                 class Dummy_SectionName:
                     entry_ok = query.SectionName.entry_ok  # Function being tested.
                     used_names = ['used']
            -        entry = Var()
            -        entry_error = {}
                     def __init__(self, dummy_entry):
            -            self.entry.set(dummy_entry)
            -            self.entry_error['text'] = ''
            +            self.entry = Var(value=dummy_entry)
            +            self.entry_error = {'text': ''}
                     def showerror(self, message):
                         self.entry_error['text'] = message
             
            @@ -117,11 +111,9 @@ class ModuleNameTest(unittest.TestCase):
                 class Dummy_ModuleName:
                     entry_ok = query.ModuleName.entry_ok  # Function being tested.
                     text0 = ''
            -        entry = Var()
            -        entry_error = {}
                     def __init__(self, dummy_entry):
            -            self.entry.set(dummy_entry)
            -            self.entry_error['text'] = ''
            +            self.entry = Var(value=dummy_entry)
            +            self.entry_error = {'text': ''}
                     def showerror(self, message):
                         self.entry_error['text'] = message
             
            @@ -146,9 +138,34 @@ def test_good_module_name(self):
                     self.assertEqual(dialog.entry_error['text'], '')
             
             
            -# 3 HelpSource test classes each test one function.
            +class GotoTest(unittest.TestCase):
            +    "Test Goto subclass of Query."
            +
            +    class Dummy_ModuleName:
            +        entry_ok = query.Goto.entry_ok  # Function being tested.
            +        def __init__(self, dummy_entry):
            +            self.entry = Var(value=dummy_entry)
            +            self.entry_error = {'text': ''}
            +        def showerror(self, message):
            +            self.entry_error['text'] = message
             
            -orig_platform = query.platform
            +    def test_bogus_goto(self):
            +        dialog = self.Dummy_ModuleName('a')
            +        self.assertEqual(dialog.entry_ok(), None)
            +        self.assertIn('not a base 10 integer', dialog.entry_error['text'])
            +
            +    def test_bad_goto(self):
            +        dialog = self.Dummy_ModuleName('0')
            +        self.assertEqual(dialog.entry_ok(), None)
            +        self.assertIn('not a positive integer', dialog.entry_error['text'])
            +
            +    def test_good_goto(self):
            +        dialog = self.Dummy_ModuleName('1')
            +        self.assertEqual(dialog.entry_ok(), 1)
            +        self.assertEqual(dialog.entry_error['text'], '')
            +
            +
            +# 3 HelpSource test classes each test one method.
             
             class HelpsourceBrowsefileTest(unittest.TestCase):
                 "Test browse_file method of ModuleName subclass of Query."
            @@ -180,17 +197,16 @@ class HelpsourcePathokTest(unittest.TestCase):
             
                 class Dummy_HelpSource:
                     path_ok = query.HelpSource.path_ok
            -        path = Var()
            -        path_error = {}
                     def __init__(self, dummy_path):
            -            self.path.set(dummy_path)
            -            self.path_error['text'] = ''
            +            self.path = Var(value=dummy_path)
            +            self.path_error = {'text': ''}
                     def showerror(self, message, widget=None):
                         self.path_error['text'] = message
             
            +    orig_platform = query.platform  # Set in test_path_ok_file.
                 @classmethod
                 def tearDownClass(cls):
            -        query.platform = orig_platform
            +        query.platform = cls.orig_platform
             
                 def test_path_ok_blank(self):
                     dialog = self.Dummy_HelpSource(' ')
            @@ -244,6 +260,56 @@ def test_entry_ok_helpsource(self):
                             self.assertEqual(dialog.entry_ok(), result)
             
             
            +# 2 CustomRun test classes each test one method.
            +
            +class CustomRunCLIargsokTest(unittest.TestCase):
            +    "Test cli_ok method of the CustomRun subclass of Query."
            +
            +    class Dummy_CustomRun:
            +        cli_args_ok = query.CustomRun.cli_args_ok
            +        def __init__(self, dummy_entry):
            +            self.entry = Var(value=dummy_entry)
            +            self.entry_error = {'text': ''}
            +        def showerror(self, message):
            +            self.entry_error['text'] = message
            +
            +    def test_blank_args(self):
            +        dialog = self.Dummy_CustomRun(' ')
            +        self.assertEqual(dialog.cli_args_ok(), [])
            +
            +    def test_invalid_args(self):
            +        dialog = self.Dummy_CustomRun("'no-closing-quote")
            +        self.assertEqual(dialog.cli_args_ok(), None)
            +        self.assertIn('No closing', dialog.entry_error['text'])
            +
            +    def test_good_args(self):
            +        args = ['-n', '10', '--verbose', '-p', '/path', '--name']
            +        dialog = self.Dummy_CustomRun(' '.join(args) + ' "my name"')
            +        self.assertEqual(dialog.cli_args_ok(), args + ["my name"])
            +        self.assertEqual(dialog.entry_error['text'], '')
            +
            +
            +class CustomRunEntryokTest(unittest.TestCase):
            +    "Test entry_ok method of the CustomRun subclass of Query."
            +
            +    class Dummy_CustomRun:
            +        entry_ok = query.CustomRun.entry_ok
            +        entry_error = {}
            +        restartvar = Var()
            +        def cli_args_ok(self):
            +            return self.cli_args
            +
            +    def test_entry_ok_customrun(self):
            +        dialog = self.Dummy_CustomRun()
            +        for restart in {True, False}:
            +            dialog.restartvar.set(restart)
            +            for cli_args, result in ((None, None),
            +                                     (['my arg'], (['my arg'], restart))):
            +                with self.subTest(restart=restart, cli_args=cli_args):
            +                    dialog.cli_args = cli_args
            +                    self.assertEqual(dialog.entry_ok(), result)
            +
            +
             # GUI TESTS
             
             class QueryGuiTest(unittest.TestCase):
            @@ -304,9 +370,7 @@ def test_click_section_name(self):
                     dialog.entry.insert(0, 'okay')
                     dialog.button_ok.invoke()
                     self.assertEqual(dialog.result, 'okay')
            -        del dialog
                     root.destroy()
            -        del root
             
             
             class ModulenameGuiTest(unittest.TestCase):
            @@ -323,9 +387,23 @@ def test_click_module_name(self):
                     self.assertEqual(dialog.entry.get(), 'idlelib')
                     dialog.button_ok.invoke()
                     self.assertTrue(dialog.result.endswith('__init__.py'))
            -        del dialog
                     root.destroy()
            -        del root
            +
            +
            +class GotoGuiTest(unittest.TestCase):
            +
            +    @classmethod
            +    def setUpClass(cls):
            +        requires('gui')
            +
            +    def test_click_module_name(self):
            +        root = Tk()
            +        root.withdraw()
            +        dialog =  query.Goto(root, 'T', 't', _utest=True)
            +        dialog.entry.insert(0, '22')
            +        dialog.button_ok.invoke()
            +        self.assertEqual(dialog.result, 22)
            +        root.destroy()
             
             
             class HelpsourceGuiTest(unittest.TestCase):
            @@ -345,9 +423,25 @@ def test_click_help_source(self):
                     dialog.button_ok.invoke()
                     prefix = "file://" if sys.platform == 'darwin' else ''
                     Equal(dialog.result, ('__test__', prefix + __file__))
            -        del dialog
                     root.destroy()
            -        del root
            +
            +
            +class CustomRunGuiTest(unittest.TestCase):
            +
            +    @classmethod
            +    def setUpClass(cls):
            +        requires('gui')
            +
            +    def test_click_args(self):
            +        root = Tk()
            +        root.withdraw()
            +        dialog =  query.CustomRun(root, 'Title',
            +                                  cli_args=['a', 'b=1'], _utest=True)
            +        self.assertEqual(dialog.entry.get(), 'a b=1')
            +        dialog.entry.insert(END, ' c')
            +        dialog.button_ok.invoke()
            +        self.assertEqual(dialog.result, (['a', 'b=1', 'c'], True))
            +        root.destroy()
             
             
             if __name__ == '__main__':
            diff --git a/Lib/idlelib/idle_test/test_redirector.py b/Lib/idlelib/idle_test/test_redirector.py
            index b0385fa78cd974..a97b3002afcf12 100644
            --- a/Lib/idlelib/idle_test/test_redirector.py
            +++ b/Lib/idlelib/idle_test/test_redirector.py
            @@ -1,12 +1,10 @@
            -'''Test idlelib.redirector.
            +"Test redirector, coverage 100%."
             
            -100% coverage
            -'''
            -from test.support import requires
            +from idlelib.redirector import WidgetRedirector
             import unittest
            -from idlelib.idle_test.mock_idle import Func
            +from test.support import requires
             from tkinter import Tk, Text, TclError
            -from idlelib.redirector import WidgetRedirector
            +from idlelib.idle_test.mock_idle import Func
             
             
             class InitCloseTest(unittest.TestCase):
            diff --git a/Lib/idlelib/idle_test/test_replace.py b/Lib/idlelib/idle_test/test_replace.py
            index df76dec3e6276d..c3c5d2eeb94998 100644
            --- a/Lib/idlelib/idle_test/test_replace.py
            +++ b/Lib/idlelib/idle_test/test_replace.py
            @@ -1,13 +1,14 @@
            -"""Unittest for idlelib.replace.py"""
            +"Test replace, coverage 78%."
            +
            +from idlelib.replace import ReplaceDialog
            +import unittest
             from test.support import requires
             requires('gui')
            +from tkinter import Tk, Text
             
            -import unittest
             from unittest.mock import Mock
            -from tkinter import Tk, Text
             from idlelib.idle_test.mock_tk import Mbox
             import idlelib.searchengine as se
            -from idlelib.replace import ReplaceDialog
             
             orig_mbox = se.tkMessageBox
             showerror = Mbox.showerror
            diff --git a/Lib/idlelib/idle_test/test_rpc.py b/Lib/idlelib/idle_test/test_rpc.py
            new file mode 100644
            index 00000000000000..81eff398c72f45
            --- /dev/null
            +++ b/Lib/idlelib/idle_test/test_rpc.py
            @@ -0,0 +1,29 @@
            +"Test rpc, coverage 20%."
            +
            +from idlelib import rpc
            +import unittest
            +
            +
            +
            +class CodePicklerTest(unittest.TestCase):
            +
            +    def test_pickle_unpickle(self):
            +        def f(): return a + b + c
            +        func, (cbytes,) = rpc.pickle_code(f.__code__)
            +        self.assertIs(func, rpc.unpickle_code)
            +        self.assertIn(b'test_rpc.py', cbytes)
            +        code = rpc.unpickle_code(cbytes)
            +        self.assertEqual(code.co_names, ('a', 'b', 'c'))
            +
            +    def test_code_pickler(self):
            +        self.assertIn(type((lambda:None).__code__),
            +                      rpc.CodePickler.dispatch_table)
            +
            +    def test_dumps(self):
            +        def f(): pass
            +        # The main test here is that pickling code does not raise.
            +        self.assertIn(b'test_rpc.py', rpc.dumps(f.__code__))
            +
            +
            +if __name__ == '__main__':
            +    unittest.main(verbosity=2)
            diff --git a/Lib/idlelib/idle_test/test_rstrip.py b/Lib/idlelib/idle_test/test_rstrip.py
            deleted file mode 100644
            index 130e6be257fe58..00000000000000
            --- a/Lib/idlelib/idle_test/test_rstrip.py
            +++ /dev/null
            @@ -1,49 +0,0 @@
            -import unittest
            -import idlelib.rstrip as rs
            -from idlelib.idle_test.mock_idle import Editor
            -
            -class rstripTest(unittest.TestCase):
            -
            -    def test_rstrip_line(self):
            -        editor = Editor()
            -        text = editor.text
            -        do_rstrip = rs.RstripExtension(editor).do_rstrip
            -
            -        do_rstrip()
            -        self.assertEqual(text.get('1.0', 'insert'), '')
            -        text.insert('1.0', '     ')
            -        do_rstrip()
            -        self.assertEqual(text.get('1.0', 'insert'), '')
            -        text.insert('1.0', '     \n')
            -        do_rstrip()
            -        self.assertEqual(text.get('1.0', 'insert'), '\n')
            -
            -    def test_rstrip_multiple(self):
            -        editor = Editor()
            -        #  Uncomment following to verify that test passes with real widgets.
            -##        from idlelib.editor import EditorWindow as Editor
            -##        from tkinter import Tk
            -##        editor = Editor(root=Tk())
            -        text = editor.text
            -        do_rstrip = rs.RstripExtension(editor).do_rstrip
            -
            -        original = (
            -            "Line with an ending tab    \n"
            -            "Line ending in 5 spaces     \n"
            -            "Linewithnospaces\n"
            -            "    indented line\n"
            -            "    indented line with trailing space \n"
            -            "    ")
            -        stripped = (
            -            "Line with an ending tab\n"
            -            "Line ending in 5 spaces\n"
            -            "Linewithnospaces\n"
            -            "    indented line\n"
            -            "    indented line with trailing space\n")
            -
            -        text.insert('1.0', original)
            -        do_rstrip()
            -        self.assertEqual(text.get('1.0', 'insert'), stripped)
            -
            -if __name__ == '__main__':
            -    unittest.main(verbosity=2, exit=False)
            diff --git a/Lib/idlelib/idle_test/test_run.py b/Lib/idlelib/idle_test/test_run.py
            index d7e627d23d3841..9995dbe2eca502 100644
            --- a/Lib/idlelib/idle_test/test_run.py
            +++ b/Lib/idlelib/idle_test/test_run.py
            @@ -1,11 +1,16 @@
            +"Test run, coverage 42%."
            +
            +from idlelib import run
             import unittest
             from unittest import mock
            -
             from test.support import captured_stderr
            -import idlelib.run as idlerun
            +
            +import io
            +import sys
             
             
             class RunTest(unittest.TestCase):
            +
                 def test_print_exception_unhashable(self):
                     class UnhashableException(Exception):
                         def __eq__(self, other):
            @@ -20,10 +25,10 @@ def __eq__(self, other):
                             raise ex1
                         except UnhashableException:
                             with captured_stderr() as output:
            -                    with mock.patch.object(idlerun,
            +                    with mock.patch.object(run,
                                                        'cleanup_traceback') as ct:
                                     ct.side_effect = lambda t, e: t
            -                        idlerun.print_exception()
            +                        run.print_exception()
             
                     tb = output.getvalue().strip().splitlines()
                     self.assertEqual(11, len(tb))
            @@ -31,5 +36,290 @@ def __eq__(self, other):
                     self.assertIn('UnhashableException: ex1', tb[10])
             
             
            +# StdioFile tests.
            +
            +class S(str):
            +    def __str__(self):
            +        return '%s:str' % type(self).__name__
            +    def __unicode__(self):
            +        return '%s:unicode' % type(self).__name__
            +    def __len__(self):
            +        return 3
            +    def __iter__(self):
            +        return iter('abc')
            +    def __getitem__(self, *args):
            +        return '%s:item' % type(self).__name__
            +    def __getslice__(self, *args):
            +        return '%s:slice' % type(self).__name__
            +
            +
            +class MockShell:
            +    def __init__(self):
            +        self.reset()
            +    def write(self, *args):
            +        self.written.append(args)
            +    def readline(self):
            +        return self.lines.pop()
            +    def close(self):
            +        pass
            +    def reset(self):
            +        self.written = []
            +    def push(self, lines):
            +        self.lines = list(lines)[::-1]
            +
            +
            +class StdInputFilesTest(unittest.TestCase):
            +
            +    def test_misc(self):
            +        shell = MockShell()
            +        f = run.StdInputFile(shell, 'stdin')
            +        self.assertIsInstance(f, io.TextIOBase)
            +        self.assertEqual(f.encoding, 'utf-8')
            +        self.assertEqual(f.errors, 'strict')
            +        self.assertIsNone(f.newlines)
            +        self.assertEqual(f.name, '')
            +        self.assertFalse(f.closed)
            +        self.assertTrue(f.isatty())
            +        self.assertTrue(f.readable())
            +        self.assertFalse(f.writable())
            +        self.assertFalse(f.seekable())
            +
            +    def test_unsupported(self):
            +        shell = MockShell()
            +        f = run.StdInputFile(shell, 'stdin')
            +        self.assertRaises(OSError, f.fileno)
            +        self.assertRaises(OSError, f.tell)
            +        self.assertRaises(OSError, f.seek, 0)
            +        self.assertRaises(OSError, f.write, 'x')
            +        self.assertRaises(OSError, f.writelines, ['x'])
            +
            +    def test_read(self):
            +        shell = MockShell()
            +        f = run.StdInputFile(shell, 'stdin')
            +        shell.push(['one\n', 'two\n', ''])
            +        self.assertEqual(f.read(), 'one\ntwo\n')
            +        shell.push(['one\n', 'two\n', ''])
            +        self.assertEqual(f.read(-1), 'one\ntwo\n')
            +        shell.push(['one\n', 'two\n', ''])
            +        self.assertEqual(f.read(None), 'one\ntwo\n')
            +        shell.push(['one\n', 'two\n', 'three\n', ''])
            +        self.assertEqual(f.read(2), 'on')
            +        self.assertEqual(f.read(3), 'e\nt')
            +        self.assertEqual(f.read(10), 'wo\nthree\n')
            +
            +        shell.push(['one\n', 'two\n'])
            +        self.assertEqual(f.read(0), '')
            +        self.assertRaises(TypeError, f.read, 1.5)
            +        self.assertRaises(TypeError, f.read, '1')
            +        self.assertRaises(TypeError, f.read, 1, 1)
            +
            +    def test_readline(self):
            +        shell = MockShell()
            +        f = run.StdInputFile(shell, 'stdin')
            +        shell.push(['one\n', 'two\n', 'three\n', 'four\n'])
            +        self.assertEqual(f.readline(), 'one\n')
            +        self.assertEqual(f.readline(-1), 'two\n')
            +        self.assertEqual(f.readline(None), 'three\n')
            +        shell.push(['one\ntwo\n'])
            +        self.assertEqual(f.readline(), 'one\n')
            +        self.assertEqual(f.readline(), 'two\n')
            +        shell.push(['one', 'two', 'three'])
            +        self.assertEqual(f.readline(), 'one')
            +        self.assertEqual(f.readline(), 'two')
            +        shell.push(['one\n', 'two\n', 'three\n'])
            +        self.assertEqual(f.readline(2), 'on')
            +        self.assertEqual(f.readline(1), 'e')
            +        self.assertEqual(f.readline(1), '\n')
            +        self.assertEqual(f.readline(10), 'two\n')
            +
            +        shell.push(['one\n', 'two\n'])
            +        self.assertEqual(f.readline(0), '')
            +        self.assertRaises(TypeError, f.readlines, 1.5)
            +        self.assertRaises(TypeError, f.readlines, '1')
            +        self.assertRaises(TypeError, f.readlines, 1, 1)
            +
            +    def test_readlines(self):
            +        shell = MockShell()
            +        f = run.StdInputFile(shell, 'stdin')
            +        shell.push(['one\n', 'two\n', ''])
            +        self.assertEqual(f.readlines(), ['one\n', 'two\n'])
            +        shell.push(['one\n', 'two\n', ''])
            +        self.assertEqual(f.readlines(-1), ['one\n', 'two\n'])
            +        shell.push(['one\n', 'two\n', ''])
            +        self.assertEqual(f.readlines(None), ['one\n', 'two\n'])
            +        shell.push(['one\n', 'two\n', ''])
            +        self.assertEqual(f.readlines(0), ['one\n', 'two\n'])
            +        shell.push(['one\n', 'two\n', ''])
            +        self.assertEqual(f.readlines(3), ['one\n'])
            +        shell.push(['one\n', 'two\n', ''])
            +        self.assertEqual(f.readlines(4), ['one\n', 'two\n'])
            +
            +        shell.push(['one\n', 'two\n', ''])
            +        self.assertRaises(TypeError, f.readlines, 1.5)
            +        self.assertRaises(TypeError, f.readlines, '1')
            +        self.assertRaises(TypeError, f.readlines, 1, 1)
            +
            +    def test_close(self):
            +        shell = MockShell()
            +        f = run.StdInputFile(shell, 'stdin')
            +        shell.push(['one\n', 'two\n', ''])
            +        self.assertFalse(f.closed)
            +        self.assertEqual(f.readline(), 'one\n')
            +        f.close()
            +        self.assertFalse(f.closed)
            +        self.assertEqual(f.readline(), 'two\n')
            +        self.assertRaises(TypeError, f.close, 1)
            +
            +
            +class StdOutputFilesTest(unittest.TestCase):
            +
            +    def test_misc(self):
            +        shell = MockShell()
            +        f = run.StdOutputFile(shell, 'stdout')
            +        self.assertIsInstance(f, io.TextIOBase)
            +        self.assertEqual(f.encoding, 'utf-8')
            +        self.assertEqual(f.errors, 'strict')
            +        self.assertIsNone(f.newlines)
            +        self.assertEqual(f.name, '')
            +        self.assertFalse(f.closed)
            +        self.assertTrue(f.isatty())
            +        self.assertFalse(f.readable())
            +        self.assertTrue(f.writable())
            +        self.assertFalse(f.seekable())
            +
            +    def test_unsupported(self):
            +        shell = MockShell()
            +        f = run.StdOutputFile(shell, 'stdout')
            +        self.assertRaises(OSError, f.fileno)
            +        self.assertRaises(OSError, f.tell)
            +        self.assertRaises(OSError, f.seek, 0)
            +        self.assertRaises(OSError, f.read, 0)
            +        self.assertRaises(OSError, f.readline, 0)
            +
            +    def test_write(self):
            +        shell = MockShell()
            +        f = run.StdOutputFile(shell, 'stdout')
            +        f.write('test')
            +        self.assertEqual(shell.written, [('test', 'stdout')])
            +        shell.reset()
            +        f.write('t\xe8\u015b\U0001d599')
            +        self.assertEqual(shell.written, [('t\xe8\u015b\U0001d599', 'stdout')])
            +        shell.reset()
            +
            +        f.write(S('t\xe8\u015b\U0001d599'))
            +        self.assertEqual(shell.written, [('t\xe8\u015b\U0001d599', 'stdout')])
            +        self.assertEqual(type(shell.written[0][0]), str)
            +        shell.reset()
            +
            +        self.assertRaises(TypeError, f.write)
            +        self.assertEqual(shell.written, [])
            +        self.assertRaises(TypeError, f.write, b'test')
            +        self.assertRaises(TypeError, f.write, 123)
            +        self.assertEqual(shell.written, [])
            +        self.assertRaises(TypeError, f.write, 'test', 'spam')
            +        self.assertEqual(shell.written, [])
            +
            +    def test_write_stderr_nonencodable(self):
            +        shell = MockShell()
            +        f = run.StdOutputFile(shell, 'stderr', 'iso-8859-15', 'backslashreplace')
            +        f.write('t\xe8\u015b\U0001d599\xa4')
            +        self.assertEqual(shell.written, [('t\xe8\\u015b\\U0001d599\\xa4', 'stderr')])
            +        shell.reset()
            +
            +        f.write(S('t\xe8\u015b\U0001d599\xa4'))
            +        self.assertEqual(shell.written, [('t\xe8\\u015b\\U0001d599\\xa4', 'stderr')])
            +        self.assertEqual(type(shell.written[0][0]), str)
            +        shell.reset()
            +
            +        self.assertRaises(TypeError, f.write)
            +        self.assertEqual(shell.written, [])
            +        self.assertRaises(TypeError, f.write, b'test')
            +        self.assertRaises(TypeError, f.write, 123)
            +        self.assertEqual(shell.written, [])
            +        self.assertRaises(TypeError, f.write, 'test', 'spam')
            +        self.assertEqual(shell.written, [])
            +
            +    def test_writelines(self):
            +        shell = MockShell()
            +        f = run.StdOutputFile(shell, 'stdout')
            +        f.writelines([])
            +        self.assertEqual(shell.written, [])
            +        shell.reset()
            +        f.writelines(['one\n', 'two'])
            +        self.assertEqual(shell.written,
            +                         [('one\n', 'stdout'), ('two', 'stdout')])
            +        shell.reset()
            +        f.writelines(['on\xe8\n', 'tw\xf2'])
            +        self.assertEqual(shell.written,
            +                         [('on\xe8\n', 'stdout'), ('tw\xf2', 'stdout')])
            +        shell.reset()
            +
            +        f.writelines([S('t\xe8st')])
            +        self.assertEqual(shell.written, [('t\xe8st', 'stdout')])
            +        self.assertEqual(type(shell.written[0][0]), str)
            +        shell.reset()
            +
            +        self.assertRaises(TypeError, f.writelines)
            +        self.assertEqual(shell.written, [])
            +        self.assertRaises(TypeError, f.writelines, 123)
            +        self.assertEqual(shell.written, [])
            +        self.assertRaises(TypeError, f.writelines, [b'test'])
            +        self.assertRaises(TypeError, f.writelines, [123])
            +        self.assertEqual(shell.written, [])
            +        self.assertRaises(TypeError, f.writelines, [], [])
            +        self.assertEqual(shell.written, [])
            +
            +    def test_close(self):
            +        shell = MockShell()
            +        f = run.StdOutputFile(shell, 'stdout')
            +        self.assertFalse(f.closed)
            +        f.write('test')
            +        f.close()
            +        self.assertTrue(f.closed)
            +        self.assertRaises(ValueError, f.write, 'x')
            +        self.assertEqual(shell.written, [('test', 'stdout')])
            +        f.close()
            +        self.assertRaises(TypeError, f.close, 1)
            +
            +
            +class TestSysRecursionLimitWrappers(unittest.TestCase):
            +
            +    def test_bad_setrecursionlimit_calls(self):
            +        run.install_recursionlimit_wrappers()
            +        self.addCleanup(run.uninstall_recursionlimit_wrappers)
            +        f = sys.setrecursionlimit
            +        self.assertRaises(TypeError, f, limit=100)
            +        self.assertRaises(TypeError, f, 100, 1000)
            +        self.assertRaises(ValueError, f, 0)
            +
            +    def test_roundtrip(self):
            +        run.install_recursionlimit_wrappers()
            +        self.addCleanup(run.uninstall_recursionlimit_wrappers)
            +
            +        # check that setting the recursion limit works
            +        orig_reclimit = sys.getrecursionlimit()
            +        self.addCleanup(sys.setrecursionlimit, orig_reclimit)
            +        sys.setrecursionlimit(orig_reclimit + 3)
            +
            +        # check that the new limit is returned by sys.getrecursionlimit()
            +        new_reclimit = sys.getrecursionlimit()
            +        self.assertEqual(new_reclimit, orig_reclimit + 3)
            +
            +    def test_default_recursion_limit_preserved(self):
            +        orig_reclimit = sys.getrecursionlimit()
            +        run.install_recursionlimit_wrappers()
            +        self.addCleanup(run.uninstall_recursionlimit_wrappers)
            +        new_reclimit = sys.getrecursionlimit()
            +        self.assertEqual(new_reclimit, orig_reclimit)
            +
            +    def test_fixdoc(self):
            +        def func(): "docstring"
            +        run.fixdoc(func, "more")
            +        self.assertEqual(func.__doc__, "docstring\n\nmore")
            +        func.__doc__ = None
            +        run.fixdoc(func, "more")
            +        self.assertEqual(func.__doc__, "more")
            +
            +
             if __name__ == '__main__':
                 unittest.main(verbosity=2)
            diff --git a/Lib/idlelib/idle_test/test_runscript.py b/Lib/idlelib/idle_test/test_runscript.py
            new file mode 100644
            index 00000000000000..5fc60185a663e8
            --- /dev/null
            +++ b/Lib/idlelib/idle_test/test_runscript.py
            @@ -0,0 +1,33 @@
            +"Test runscript, coverage 16%."
            +
            +from idlelib import runscript
            +import unittest
            +from test.support import requires
            +from tkinter import Tk
            +from idlelib.editor import EditorWindow
            +
            +
            +class ScriptBindingTest(unittest.TestCase):
            +
            +    @classmethod
            +    def setUpClass(cls):
            +        requires('gui')
            +        cls.root = Tk()
            +        cls.root.withdraw()
            +
            +    @classmethod
            +    def tearDownClass(cls):
            +        cls.root.update_idletasks()
            +        for id in cls.root.tk.call('after', 'info'):
            +            cls.root.after_cancel(id)  # Need for EditorWindow.
            +        cls.root.destroy()
            +        del cls.root
            +
            +    def test_init(self):
            +        ew = EditorWindow(root=self.root)
            +        sb = runscript.ScriptBinding(ew)
            +        ew._close()
            +
            +
            +if __name__ == '__main__':
            +    unittest.main(verbosity=2)
            diff --git a/Lib/idlelib/idle_test/test_scrolledlist.py b/Lib/idlelib/idle_test/test_scrolledlist.py
            index 56aabfecf4a9ce..2f819fda025ba3 100644
            --- a/Lib/idlelib/idle_test/test_scrolledlist.py
            +++ b/Lib/idlelib/idle_test/test_scrolledlist.py
            @@ -1,11 +1,9 @@
            -''' Test idlelib.scrolledlist.
            +"Test scrolledlist, coverage 38%."
             
            -Coverage: 39%
            -'''
            -from idlelib import scrolledlist
            +from idlelib.scrolledlist import ScrolledList
            +import unittest
             from test.support import requires
             requires('gui')
            -import unittest
             from tkinter import Tk
             
             
            @@ -22,7 +20,7 @@ def tearDownClass(cls):
             
             
                 def test_init(self):
            -        scrolledlist.ScrolledList(self.root)
            +        ScrolledList(self.root)
             
             
             if __name__ == '__main__':
            diff --git a/Lib/idlelib/idle_test/test_search.py b/Lib/idlelib/idle_test/test_search.py
            index 3ab72951efe3fa..de703c195cd229 100644
            --- a/Lib/idlelib/idle_test/test_search.py
            +++ b/Lib/idlelib/idle_test/test_search.py
            @@ -1,25 +1,23 @@
            -"""Test SearchDialog class in idlelib.search.py"""
            +"Test search, coverage 69%."
            +
            +from idlelib import search
            +import unittest
            +from test.support import requires
            +requires('gui')
            +from tkinter import Tk, Text, BooleanVar
            +from idlelib import searchengine
             
             # Does not currently test the event handler wrappers.
             # A usage test should simulate clicks and check highlighting.
             # Tests need to be coordinated with SearchDialogBase tests
             # to avoid duplication.
             
            -from test.support import requires
            -requires('gui')
            -
            -import unittest
            -import tkinter as tk
            -from tkinter import BooleanVar
            -import idlelib.searchengine as se
            -import idlelib.search as sd
            -
             
             class SearchDialogTest(unittest.TestCase):
             
                 @classmethod
                 def setUpClass(cls):
            -        cls.root = tk.Tk()
            +        cls.root = Tk()
             
                 @classmethod
                 def tearDownClass(cls):
            @@ -27,10 +25,10 @@ def tearDownClass(cls):
                     del cls.root
             
                 def setUp(self):
            -        self.engine = se.SearchEngine(self.root)
            -        self.dialog = sd.SearchDialog(self.root, self.engine)
            +        self.engine = searchengine.SearchEngine(self.root)
            +        self.dialog = search.SearchDialog(self.root, self.engine)
                     self.dialog.bell = lambda: None
            -        self.text = tk.Text(self.root)
            +        self.text = Text(self.root)
                     self.text.insert('1.0', 'Hello World!')
             
                 def test_find_again(self):
            diff --git a/Lib/idlelib/idle_test/test_searchbase.py b/Lib/idlelib/idle_test/test_searchbase.py
            index 27b02fbe54602c..aee0c4c69929a6 100644
            --- a/Lib/idlelib/idle_test/test_searchbase.py
            +++ b/Lib/idlelib/idle_test/test_searchbase.py
            @@ -1,11 +1,11 @@
            -'''tests idlelib.searchbase.
            +"Test searchbase, coverage 98%."
            +# The only thing not covered is inconsequential --
            +# testing skipping of suite when self.needwrapbutton is false.
             
            -Coverage: 99%. The only thing not covered is inconsequential --
            -testing skipping of suite when self.needwrapbutton is false.
            -'''
             import unittest
             from test.support import requires
            -from tkinter import Tk, Frame  ##, BooleanVar, StringVar
            +from tkinter import Text, Tk, Toplevel
            +from tkinter.ttk import Frame
             from idlelib import searchengine as se
             from idlelib import searchbase as sdb
             from idlelib.idle_test.mock_idle import Func
            @@ -22,6 +22,7 @@
             ##    se.BooleanVar = BooleanVar
             ##    se.StringVar = StringVar
             
            +
             class SearchDialogBaseTest(unittest.TestCase):
             
                 @classmethod
            @@ -31,6 +32,7 @@ def setUpClass(cls):
             
                 @classmethod
                 def tearDownClass(cls):
            +        cls.root.update_idletasks()
                     cls.root.destroy()
                     del cls.root
             
            @@ -45,16 +47,17 @@ def test_open_and_close(self):
                     # open calls create_widgets, which needs default_command
                     self.dialog.default_command = None
             
            -        # Since text parameter of .open is not used in base class,
            -        # pass dummy 'text' instead of tk.Text().
            -        self.dialog.open('text')
            +        toplevel = Toplevel(self.root)
            +        text = Text(toplevel)
            +        self.dialog.open(text)
                     self.assertEqual(self.dialog.top.state(), 'normal')
                     self.dialog.close()
                     self.assertEqual(self.dialog.top.state(), 'withdrawn')
             
            -        self.dialog.open('text', searchphrase="hello")
            +        self.dialog.open(text, searchphrase="hello")
                     self.assertEqual(self.dialog.ent.get(), 'hello')
            -        self.dialog.close()
            +        toplevel.update_idletasks()
            +        toplevel.destroy()
             
                 def test_create_widgets(self):
                     self.dialog.create_entries = Func()
            @@ -97,11 +100,12 @@ def test_make_frame(self):
                     self.dialog.top = self.root
                     frame, label = self.dialog.make_frame()
                     self.assertEqual(label, '')
            -        self.assertIsInstance(frame, Frame)
            +        self.assertEqual(str(type(frame)), "")
            +        # self.assertIsInstance(frame, Frame) fails when test is run by
            +        # test_idle not run from IDLE editor.  See issue 33987 PR.
             
                     frame, label = self.dialog.make_frame('testlabel')
                     self.assertEqual(label['text'], 'testlabel')
            -        self.assertIsInstance(frame, Frame)
             
                 def btn_test_setup(self, meth):
                     self.dialog.top = self.root
            @@ -147,7 +151,7 @@ def test_create_command_buttons(self):
                     # Look for close button command in buttonframe
                     closebuttoncommand = ''
                     for child in self.dialog.buttonframe.winfo_children():
            -            if child['text'] == 'close':
            +            if child['text'] == 'Close':
                             closebuttoncommand = child['command']
                     self.assertIn('close', closebuttoncommand)
             
            diff --git a/Lib/idlelib/idle_test/test_searchengine.py b/Lib/idlelib/idle_test/test_searchengine.py
            index b3aa8eb81205ee..3d26d62a95a873 100644
            --- a/Lib/idlelib/idle_test/test_searchengine.py
            +++ b/Lib/idlelib/idle_test/test_searchengine.py
            @@ -1,18 +1,19 @@
            -'''Test functions and SearchEngine class in idlelib.searchengine.py.'''
            +"Test searchengine, coverage 99%."
             
            -# With mock replacements, the module does not use any gui widgets.
            -# The use of tk.Text is avoided (for now, until mock Text is improved)
            -# by patching instances with an index function returning what is needed.
            -# This works because mock Text.get does not use .index.
            -
            -import re
            +from idlelib import searchengine as se
             import unittest
             # from test.support import requires
             from tkinter import  BooleanVar, StringVar, TclError  # ,Tk, Text
             import tkinter.messagebox as tkMessageBox
            -from idlelib import searchengine as se
             from idlelib.idle_test.mock_tk import Var, Mbox
             from idlelib.idle_test.mock_tk import Text as mockText
            +import re
            +
            +# With mock replacements, the module does not use any gui widgets.
            +# The use of tk.Text is avoided (for now, until mock Text is improved)
            +# by patching instances with an index function returning what is needed.
            +# This works because mock Text.get does not use .index.
            +# The tkinter imports are used to restore searchengine.
             
             def setUpModule():
                 # Replace s-e module tkinter imports other than non-gui TclError.
            @@ -326,4 +327,4 @@ def test_search_backward(self):
             
             
             if __name__ == '__main__':
            -    unittest.main(verbosity=2, exit=2)
            +    unittest.main(verbosity=2)
            diff --git a/Lib/idlelib/idle_test/test_sidebar.py b/Lib/idlelib/idle_test/test_sidebar.py
            new file mode 100644
            index 00000000000000..2974a9a7b09874
            --- /dev/null
            +++ b/Lib/idlelib/idle_test/test_sidebar.py
            @@ -0,0 +1,374 @@
            +"""Test sidebar, coverage 93%"""
            +import idlelib.sidebar
            +from itertools import chain
            +import unittest
            +import unittest.mock
            +from test.support import requires
            +import tkinter as tk
            +
            +from idlelib.delegator import Delegator
            +from idlelib.percolator import Percolator
            +
            +
            +class Dummy_editwin:
            +    def __init__(self, text):
            +        self.text = text
            +        self.text_frame = self.text.master
            +        self.per = Percolator(text)
            +        self.undo = Delegator()
            +        self.per.insertfilter(self.undo)
            +
            +    def setvar(self, name, value):
            +        pass
            +
            +    def getlineno(self, index):
            +        return int(float(self.text.index(index)))
            +
            +
            +class LineNumbersTest(unittest.TestCase):
            +
            +    @classmethod
            +    def setUpClass(cls):
            +        requires('gui')
            +        cls.root = tk.Tk()
            +
            +        cls.text_frame = tk.Frame(cls.root)
            +        cls.text_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
            +        cls.text_frame.rowconfigure(1, weight=1)
            +        cls.text_frame.columnconfigure(1, weight=1)
            +
            +        cls.text = tk.Text(cls.text_frame, width=80, height=24, wrap=tk.NONE)
            +        cls.text.grid(row=1, column=1, sticky=tk.NSEW)
            +
            +        cls.editwin = Dummy_editwin(cls.text)
            +        cls.editwin.vbar = tk.Scrollbar(cls.text_frame)
            +
            +    @classmethod
            +    def tearDownClass(cls):
            +        cls.editwin.per.close()
            +        cls.root.update()
            +        cls.root.destroy()
            +        del cls.text, cls.text_frame, cls.editwin, cls.root
            +
            +    def setUp(self):
            +        self.linenumber = idlelib.sidebar.LineNumbers(self.editwin)
            +
            +        self.highlight_cfg = {"background": '#abcdef',
            +                              "foreground": '#123456'}
            +        orig_idleConf_GetHighlight = idlelib.sidebar.idleConf.GetHighlight
            +        def mock_idleconf_GetHighlight(theme, element):
            +            if element == 'linenumber':
            +                return self.highlight_cfg
            +            return orig_idleConf_GetHighlight(theme, element)
            +        GetHighlight_patcher = unittest.mock.patch.object(
            +            idlelib.sidebar.idleConf, 'GetHighlight', mock_idleconf_GetHighlight)
            +        GetHighlight_patcher.start()
            +        self.addCleanup(GetHighlight_patcher.stop)
            +
            +        self.font_override = 'TkFixedFont'
            +        def mock_idleconf_GetFont(root, configType, section):
            +            return self.font_override
            +        GetFont_patcher = unittest.mock.patch.object(
            +            idlelib.sidebar.idleConf, 'GetFont', mock_idleconf_GetFont)
            +        GetFont_patcher.start()
            +        self.addCleanup(GetFont_patcher.stop)
            +
            +    def tearDown(self):
            +        self.text.delete('1.0', 'end')
            +
            +    def get_selection(self):
            +        return tuple(map(str, self.text.tag_ranges('sel')))
            +
            +    def get_line_screen_position(self, line):
            +        bbox = self.linenumber.sidebar_text.bbox(f'{line}.end -1c')
            +        x = bbox[0] + 2
            +        y = bbox[1] + 2
            +        return x, y
            +
            +    def assert_state_disabled(self):
            +        state = self.linenumber.sidebar_text.config()['state']
            +        self.assertEqual(state[-1], tk.DISABLED)
            +
            +    def get_sidebar_text_contents(self):
            +        return self.linenumber.sidebar_text.get('1.0', tk.END)
            +
            +    def assert_sidebar_n_lines(self, n_lines):
            +        expected = '\n'.join(chain(map(str, range(1, n_lines + 1)), ['']))
            +        self.assertEqual(self.get_sidebar_text_contents(), expected)
            +
            +    def assert_text_equals(self, expected):
            +        return self.assertEqual(self.text.get('1.0', 'end'), expected)
            +
            +    def test_init_empty(self):
            +        self.assert_sidebar_n_lines(1)
            +
            +    def test_init_not_empty(self):
            +        self.text.insert('insert', 'foo bar\n'*3)
            +        self.assert_text_equals('foo bar\n'*3 + '\n')
            +        self.assert_sidebar_n_lines(4)
            +
            +    def test_toggle_linenumbering(self):
            +        self.assertEqual(self.linenumber.is_shown, False)
            +        self.linenumber.show_sidebar()
            +        self.assertEqual(self.linenumber.is_shown, True)
            +        self.linenumber.hide_sidebar()
            +        self.assertEqual(self.linenumber.is_shown, False)
            +        self.linenumber.hide_sidebar()
            +        self.assertEqual(self.linenumber.is_shown, False)
            +        self.linenumber.show_sidebar()
            +        self.assertEqual(self.linenumber.is_shown, True)
            +        self.linenumber.show_sidebar()
            +        self.assertEqual(self.linenumber.is_shown, True)
            +
            +    def test_insert(self):
            +        self.text.insert('insert', 'foobar')
            +        self.assert_text_equals('foobar\n')
            +        self.assert_sidebar_n_lines(1)
            +        self.assert_state_disabled()
            +
            +        self.text.insert('insert', '\nfoo')
            +        self.assert_text_equals('foobar\nfoo\n')
            +        self.assert_sidebar_n_lines(2)
            +        self.assert_state_disabled()
            +
            +        self.text.insert('insert', 'hello\n'*2)
            +        self.assert_text_equals('foobar\nfoohello\nhello\n\n')
            +        self.assert_sidebar_n_lines(4)
            +        self.assert_state_disabled()
            +
            +        self.text.insert('insert', '\nworld')
            +        self.assert_text_equals('foobar\nfoohello\nhello\n\nworld\n')
            +        self.assert_sidebar_n_lines(5)
            +        self.assert_state_disabled()
            +
            +    def test_delete(self):
            +        self.text.insert('insert', 'foobar')
            +        self.assert_text_equals('foobar\n')
            +        self.text.delete('1.1', '1.3')
            +        self.assert_text_equals('fbar\n')
            +        self.assert_sidebar_n_lines(1)
            +        self.assert_state_disabled()
            +
            +        self.text.insert('insert', 'foo\n'*2)
            +        self.assert_text_equals('fbarfoo\nfoo\n\n')
            +        self.assert_sidebar_n_lines(3)
            +        self.assert_state_disabled()
            +
            +        # Note: deleting up to "2.end" doesn't delete the final newline.
            +        self.text.delete('2.0', '2.end')
            +        self.assert_text_equals('fbarfoo\n\n\n')
            +        self.assert_sidebar_n_lines(3)
            +        self.assert_state_disabled()
            +
            +        self.text.delete('1.3', 'end')
            +        self.assert_text_equals('fba\n')
            +        self.assert_sidebar_n_lines(1)
            +        self.assert_state_disabled()
            +
            +        # Note: Text widgets always keep a single '\n' character at the end.
            +        self.text.delete('1.0', 'end')
            +        self.assert_text_equals('\n')
            +        self.assert_sidebar_n_lines(1)
            +        self.assert_state_disabled()
            +
            +    def test_sidebar_text_width(self):
            +        """
            +        Test that linenumber text widget is always at the minimum
            +        width
            +        """
            +        def get_width():
            +            return self.linenumber.sidebar_text.config()['width'][-1]
            +
            +        self.assert_sidebar_n_lines(1)
            +        self.assertEqual(get_width(), 1)
            +
            +        self.text.insert('insert', 'foo')
            +        self.assert_sidebar_n_lines(1)
            +        self.assertEqual(get_width(), 1)
            +
            +        self.text.insert('insert', 'foo\n'*8)
            +        self.assert_sidebar_n_lines(9)
            +        self.assertEqual(get_width(), 1)
            +
            +        self.text.insert('insert', 'foo\n')
            +        self.assert_sidebar_n_lines(10)
            +        self.assertEqual(get_width(), 2)
            +
            +        self.text.insert('insert', 'foo\n')
            +        self.assert_sidebar_n_lines(11)
            +        self.assertEqual(get_width(), 2)
            +
            +        self.text.delete('insert -1l linestart', 'insert linestart')
            +        self.assert_sidebar_n_lines(10)
            +        self.assertEqual(get_width(), 2)
            +
            +        self.text.delete('insert -1l linestart', 'insert linestart')
            +        self.assert_sidebar_n_lines(9)
            +        self.assertEqual(get_width(), 1)
            +
            +        self.text.insert('insert', 'foo\n'*90)
            +        self.assert_sidebar_n_lines(99)
            +        self.assertEqual(get_width(), 2)
            +
            +        self.text.insert('insert', 'foo\n')
            +        self.assert_sidebar_n_lines(100)
            +        self.assertEqual(get_width(), 3)
            +
            +        self.text.insert('insert', 'foo\n')
            +        self.assert_sidebar_n_lines(101)
            +        self.assertEqual(get_width(), 3)
            +
            +        self.text.delete('insert -1l linestart', 'insert linestart')
            +        self.assert_sidebar_n_lines(100)
            +        self.assertEqual(get_width(), 3)
            +
            +        self.text.delete('insert -1l linestart', 'insert linestart')
            +        self.assert_sidebar_n_lines(99)
            +        self.assertEqual(get_width(), 2)
            +
            +        self.text.delete('50.0 -1c', 'end -1c')
            +        self.assert_sidebar_n_lines(49)
            +        self.assertEqual(get_width(), 2)
            +
            +        self.text.delete('5.0 -1c', 'end -1c')
            +        self.assert_sidebar_n_lines(4)
            +        self.assertEqual(get_width(), 1)
            +
            +        # Note: Text widgets always keep a single '\n' character at the end.
            +        self.text.delete('1.0', 'end -1c')
            +        self.assert_sidebar_n_lines(1)
            +        self.assertEqual(get_width(), 1)
            +
            +    def test_click_selection(self):
            +        self.linenumber.show_sidebar()
            +        self.text.insert('1.0', 'one\ntwo\nthree\nfour\n')
            +        self.root.update()
            +
            +        # Click on the second line.
            +        x, y = self.get_line_screen_position(2)
            +        self.linenumber.sidebar_text.event_generate('', x=x, y=y)
            +        self.linenumber.sidebar_text.update()
            +        self.root.update()
            +
            +        self.assertEqual(self.get_selection(), ('2.0', '3.0'))
            +
            +    def simulate_drag(self, start_line, end_line):
            +        start_x, start_y = self.get_line_screen_position(start_line)
            +        end_x, end_y = self.get_line_screen_position(end_line)
            +
            +        self.linenumber.sidebar_text.event_generate('',
            +                                                    x=start_x, y=start_y)
            +        self.root.update()
            +
            +        def lerp(a, b, steps):
            +            """linearly interpolate from a to b (inclusive) in equal steps"""
            +            last_step = steps - 1
            +            for i in range(steps):
            +                yield ((last_step - i) / last_step) * a + (i / last_step) * b
            +
            +        for x, y in zip(
            +                map(int, lerp(start_x, end_x, steps=11)),
            +                map(int, lerp(start_y, end_y, steps=11)),
            +        ):
            +            self.linenumber.sidebar_text.event_generate('', x=x, y=y)
            +            self.root.update()
            +
            +        self.linenumber.sidebar_text.event_generate('',
            +                                                    x=end_x, y=end_y)
            +        self.root.update()
            +
            +    def test_drag_selection_down(self):
            +        self.linenumber.show_sidebar()
            +        self.text.insert('1.0', 'one\ntwo\nthree\nfour\nfive\n')
            +        self.root.update()
            +
            +        # Drag from the second line to the fourth line.
            +        self.simulate_drag(2, 4)
            +        self.assertEqual(self.get_selection(), ('2.0', '5.0'))
            +
            +    def test_drag_selection_up(self):
            +        self.linenumber.show_sidebar()
            +        self.text.insert('1.0', 'one\ntwo\nthree\nfour\nfive\n')
            +        self.root.update()
            +
            +        # Drag from the fourth line to the second line.
            +        self.simulate_drag(4, 2)
            +        self.assertEqual(self.get_selection(), ('2.0', '5.0'))
            +
            +    def test_scroll(self):
            +        self.linenumber.show_sidebar()
            +        self.text.insert('1.0', 'line\n' * 100)
            +        self.root.update()
            +
            +        # Scroll down 10 lines.
            +        self.text.yview_scroll(10, 'unit')
            +        self.root.update()
            +        self.assertEqual(self.text.index('@0,0'), '11.0')
            +        self.assertEqual(self.linenumber.sidebar_text.index('@0,0'), '11.0')
            +
            +        # Generate a mouse-wheel event and make sure it scrolled up or down.
            +        # The meaning of the "delta" is OS-dependant, so this just checks for
            +        # any change.
            +        self.linenumber.sidebar_text.event_generate('',
            +                                                    x=0, y=0,
            +                                                    delta=10)
            +        self.root.update()
            +        self.assertNotEqual(self.text.index('@0,0'), '11.0')
            +        self.assertNotEqual(self.linenumber.sidebar_text.index('@0,0'), '11.0')
            +
            +    def test_font(self):
            +        ln = self.linenumber
            +
            +        orig_font = ln.sidebar_text['font']
            +        test_font = 'TkTextFont'
            +        self.assertNotEqual(orig_font, test_font)
            +
            +        # Ensure line numbers aren't shown.
            +        ln.hide_sidebar()
            +
            +        self.font_override = test_font
            +        # Nothing breaks when line numbers aren't shown.
            +        ln.update_font()
            +
            +        # Activate line numbers, previous font change is immediately effective.
            +        ln.show_sidebar()
            +        self.assertEqual(ln.sidebar_text['font'], test_font)
            +
            +        # Call the font update with line numbers shown, change is picked up.
            +        self.font_override = orig_font
            +        ln.update_font()
            +        self.assertEqual(ln.sidebar_text['font'], orig_font)
            +
            +    def test_highlight_colors(self):
            +        ln = self.linenumber
            +
            +        orig_colors = dict(self.highlight_cfg)
            +        test_colors = {'background': '#222222', 'foreground': '#ffff00'}
            +
            +        def assert_colors_are_equal(colors):
            +            self.assertEqual(ln.sidebar_text['background'], colors['background'])
            +            self.assertEqual(ln.sidebar_text['foreground'], colors['foreground'])
            +
            +        # Ensure line numbers aren't shown.
            +        ln.hide_sidebar()
            +
            +        self.highlight_cfg = test_colors
            +        # Nothing breaks with inactive code context.
            +        ln.update_colors()
            +
            +        # Show line numbers, previous colors change is immediately effective.
            +        ln.show_sidebar()
            +        assert_colors_are_equal(test_colors)
            +
            +        # Call colors update with no change to the configured colors.
            +        ln.update_colors()
            +        assert_colors_are_equal(test_colors)
            +
            +        # Call the colors update with line numbers shown, change is picked up.
            +        self.highlight_cfg = orig_colors
            +        ln.update_colors()
            +        assert_colors_are_equal(orig_colors)
            +
            +
            +if __name__ == '__main__':
            +    unittest.main(verbosity=2)
            diff --git a/Lib/idlelib/idle_test/test_squeezer.py b/Lib/idlelib/idle_test/test_squeezer.py
            new file mode 100644
            index 00000000000000..e3912f4bbbec89
            --- /dev/null
            +++ b/Lib/idlelib/idle_test/test_squeezer.py
            @@ -0,0 +1,475 @@
            +"Test squeezer, coverage 95%"
            +
            +from textwrap import dedent
            +from tkinter import Text, Tk
            +import unittest
            +from unittest.mock import Mock, NonCallableMagicMock, patch, sentinel, ANY
            +from test.support import requires
            +
            +from idlelib.config import idleConf
            +from idlelib.squeezer import count_lines_with_wrapping, ExpandingButton, \
            +    Squeezer
            +from idlelib import macosx
            +from idlelib.textview import view_text
            +from idlelib.tooltip import Hovertip
            +from idlelib.pyshell import PyShell
            +
            +
            +SENTINEL_VALUE = sentinel.SENTINEL_VALUE
            +
            +
            +def get_test_tk_root(test_instance):
            +    """Helper for tests: Create a root Tk object."""
            +    requires('gui')
            +    root = Tk()
            +    root.withdraw()
            +
            +    def cleanup_root():
            +        root.update_idletasks()
            +        root.destroy()
            +    test_instance.addCleanup(cleanup_root)
            +
            +    return root
            +
            +
            +class CountLinesTest(unittest.TestCase):
            +    """Tests for the count_lines_with_wrapping function."""
            +    def check(self, expected, text, linewidth):
            +        return self.assertEqual(
            +            expected,
            +            count_lines_with_wrapping(text, linewidth),
            +        )
            +
            +    def test_count_empty(self):
            +        """Test with an empty string."""
            +        self.assertEqual(count_lines_with_wrapping(""), 0)
            +
            +    def test_count_begins_with_empty_line(self):
            +        """Test with a string which begins with a newline."""
            +        self.assertEqual(count_lines_with_wrapping("\ntext"), 2)
            +
            +    def test_count_ends_with_empty_line(self):
            +        """Test with a string which ends with a newline."""
            +        self.assertEqual(count_lines_with_wrapping("text\n"), 1)
            +
            +    def test_count_several_lines(self):
            +        """Test with several lines of text."""
            +        self.assertEqual(count_lines_with_wrapping("1\n2\n3\n"), 3)
            +
            +    def test_empty_lines(self):
            +        self.check(expected=1, text='\n', linewidth=80)
            +        self.check(expected=2, text='\n\n', linewidth=80)
            +        self.check(expected=10, text='\n' * 10, linewidth=80)
            +
            +    def test_long_line(self):
            +        self.check(expected=3, text='a' * 200, linewidth=80)
            +        self.check(expected=3, text='a' * 200 + '\n', linewidth=80)
            +
            +    def test_several_lines_different_lengths(self):
            +        text = dedent("""\
            +            13 characters
            +            43 is the number of characters on this line
            +
            +            7 chars
            +            13 characters""")
            +        self.check(expected=5, text=text, linewidth=80)
            +        self.check(expected=5, text=text + '\n', linewidth=80)
            +        self.check(expected=6, text=text, linewidth=40)
            +        self.check(expected=7, text=text, linewidth=20)
            +        self.check(expected=11, text=text, linewidth=10)
            +
            +
            +class SqueezerTest(unittest.TestCase):
            +    """Tests for the Squeezer class."""
            +    def make_mock_editor_window(self, with_text_widget=False):
            +        """Create a mock EditorWindow instance."""
            +        editwin = NonCallableMagicMock()
            +        editwin.width = 80
            +
            +        if with_text_widget:
            +            editwin.root = get_test_tk_root(self)
            +            text_widget = self.make_text_widget(root=editwin.root)
            +            editwin.text = editwin.per.bottom = text_widget
            +
            +        return editwin
            +
            +    def make_squeezer_instance(self, editor_window=None):
            +        """Create an actual Squeezer instance with a mock EditorWindow."""
            +        if editor_window is None:
            +            editor_window = self.make_mock_editor_window()
            +        squeezer = Squeezer(editor_window)
            +        return squeezer
            +
            +    def make_text_widget(self, root=None):
            +        if root is None:
            +            root = get_test_tk_root(self)
            +        text_widget = Text(root)
            +        text_widget["font"] = ('Courier', 10)
            +        text_widget.mark_set("iomark", "1.0")
            +        return text_widget
            +
            +    def set_idleconf_option_with_cleanup(self, configType, section, option, value):
            +        prev_val = idleConf.GetOption(configType, section, option)
            +        idleConf.SetOption(configType, section, option, value)
            +        self.addCleanup(idleConf.SetOption,
            +                        configType, section, option, prev_val)
            +
            +    def test_count_lines(self):
            +        """Test Squeezer.count_lines() with various inputs."""
            +        editwin = self.make_mock_editor_window()
            +        squeezer = self.make_squeezer_instance(editwin)
            +
            +        for text_code, line_width, expected in [
            +            (r"'\n'", 80, 1),
            +            (r"'\n' * 3", 80, 3),
            +            (r"'a' * 40 + '\n'", 80, 1),
            +            (r"'a' * 80 + '\n'", 80, 1),
            +            (r"'a' * 200 + '\n'", 80, 3),
            +            (r"'aa\t' * 20", 80, 2),
            +            (r"'aa\t' * 21", 80, 3),
            +            (r"'aa\t' * 20", 40, 4),
            +        ]:
            +            with self.subTest(text_code=text_code,
            +                              line_width=line_width,
            +                              expected=expected):
            +                text = eval(text_code)
            +                with patch.object(editwin, 'width', line_width):
            +                    self.assertEqual(squeezer.count_lines(text), expected)
            +
            +    def test_init(self):
            +        """Test the creation of Squeezer instances."""
            +        editwin = self.make_mock_editor_window()
            +        squeezer = self.make_squeezer_instance(editwin)
            +        self.assertIs(squeezer.editwin, editwin)
            +        self.assertEqual(squeezer.expandingbuttons, [])
            +
            +    def test_write_no_tags(self):
            +        """Test Squeezer's overriding of the EditorWindow's write() method."""
            +        editwin = self.make_mock_editor_window()
            +        for text in ['', 'TEXT', 'LONG TEXT' * 1000, 'MANY_LINES\n' * 100]:
            +            editwin.write = orig_write = Mock(return_value=SENTINEL_VALUE)
            +            squeezer = self.make_squeezer_instance(editwin)
            +
            +            self.assertEqual(squeezer.editwin.write(text, ()), SENTINEL_VALUE)
            +            self.assertEqual(orig_write.call_count, 1)
            +            orig_write.assert_called_with(text, ())
            +            self.assertEqual(len(squeezer.expandingbuttons), 0)
            +
            +    def test_write_not_stdout(self):
            +        """Test Squeezer's overriding of the EditorWindow's write() method."""
            +        for text in ['', 'TEXT', 'LONG TEXT' * 1000, 'MANY_LINES\n' * 100]:
            +            editwin = self.make_mock_editor_window()
            +            editwin.write.return_value = SENTINEL_VALUE
            +            orig_write = editwin.write
            +            squeezer = self.make_squeezer_instance(editwin)
            +
            +            self.assertEqual(squeezer.editwin.write(text, "stderr"),
            +                              SENTINEL_VALUE)
            +            self.assertEqual(orig_write.call_count, 1)
            +            orig_write.assert_called_with(text, "stderr")
            +            self.assertEqual(len(squeezer.expandingbuttons), 0)
            +
            +    def test_write_stdout(self):
            +        """Test Squeezer's overriding of the EditorWindow's write() method."""
            +        editwin = self.make_mock_editor_window()
            +
            +        for text in ['', 'TEXT']:
            +            editwin.write = orig_write = Mock(return_value=SENTINEL_VALUE)
            +            squeezer = self.make_squeezer_instance(editwin)
            +            squeezer.auto_squeeze_min_lines = 50
            +
            +            self.assertEqual(squeezer.editwin.write(text, "stdout"),
            +                             SENTINEL_VALUE)
            +            self.assertEqual(orig_write.call_count, 1)
            +            orig_write.assert_called_with(text, "stdout")
            +            self.assertEqual(len(squeezer.expandingbuttons), 0)
            +
            +        for text in ['LONG TEXT' * 1000, 'MANY_LINES\n' * 100]:
            +            editwin.write = orig_write = Mock(return_value=SENTINEL_VALUE)
            +            squeezer = self.make_squeezer_instance(editwin)
            +            squeezer.auto_squeeze_min_lines = 50
            +
            +            self.assertEqual(squeezer.editwin.write(text, "stdout"), None)
            +            self.assertEqual(orig_write.call_count, 0)
            +            self.assertEqual(len(squeezer.expandingbuttons), 1)
            +
            +    def test_auto_squeeze(self):
            +        """Test that the auto-squeezing creates an ExpandingButton properly."""
            +        editwin = self.make_mock_editor_window(with_text_widget=True)
            +        text_widget = editwin.text
            +        squeezer = self.make_squeezer_instance(editwin)
            +        squeezer.auto_squeeze_min_lines = 5
            +        squeezer.count_lines = Mock(return_value=6)
            +
            +        editwin.write('TEXT\n'*6, "stdout")
            +        self.assertEqual(text_widget.get('1.0', 'end'), '\n')
            +        self.assertEqual(len(squeezer.expandingbuttons), 1)
            +
            +    def test_squeeze_current_text_event(self):
            +        """Test the squeeze_current_text event."""
            +        # Squeezing text should work for both stdout and stderr.
            +        for tag_name in ["stdout", "stderr"]:
            +            editwin = self.make_mock_editor_window(with_text_widget=True)
            +            text_widget = editwin.text
            +            squeezer = self.make_squeezer_instance(editwin)
            +            squeezer.count_lines = Mock(return_value=6)
            +
            +            # Prepare some text in the Text widget.
            +            text_widget.insert("1.0", "SOME\nTEXT\n", tag_name)
            +            text_widget.mark_set("insert", "1.0")
            +            self.assertEqual(text_widget.get('1.0', 'end'), 'SOME\nTEXT\n\n')
            +
            +            self.assertEqual(len(squeezer.expandingbuttons), 0)
            +
            +            # Test squeezing the current text.
            +            retval = squeezer.squeeze_current_text_event(event=Mock())
            +            self.assertEqual(retval, "break")
            +            self.assertEqual(text_widget.get('1.0', 'end'), '\n\n')
            +            self.assertEqual(len(squeezer.expandingbuttons), 1)
            +            self.assertEqual(squeezer.expandingbuttons[0].s, 'SOME\nTEXT')
            +
            +            # Test that expanding the squeezed text works and afterwards
            +            # the Text widget contains the original text.
            +            squeezer.expandingbuttons[0].expand(event=Mock())
            +            self.assertEqual(text_widget.get('1.0', 'end'), 'SOME\nTEXT\n\n')
            +            self.assertEqual(len(squeezer.expandingbuttons), 0)
            +
            +    def test_squeeze_current_text_event_no_allowed_tags(self):
            +        """Test that the event doesn't squeeze text without a relevant tag."""
            +        editwin = self.make_mock_editor_window(with_text_widget=True)
            +        text_widget = editwin.text
            +        squeezer = self.make_squeezer_instance(editwin)
            +        squeezer.count_lines = Mock(return_value=6)
            +
            +        # Prepare some text in the Text widget.
            +        text_widget.insert("1.0", "SOME\nTEXT\n", "TAG")
            +        text_widget.mark_set("insert", "1.0")
            +        self.assertEqual(text_widget.get('1.0', 'end'), 'SOME\nTEXT\n\n')
            +
            +        self.assertEqual(len(squeezer.expandingbuttons), 0)
            +
            +        # Test squeezing the current text.
            +        retval = squeezer.squeeze_current_text_event(event=Mock())
            +        self.assertEqual(retval, "break")
            +        self.assertEqual(text_widget.get('1.0', 'end'), 'SOME\nTEXT\n\n')
            +        self.assertEqual(len(squeezer.expandingbuttons), 0)
            +
            +    def test_squeeze_text_before_existing_squeezed_text(self):
            +        """Test squeezing text before existing squeezed text."""
            +        editwin = self.make_mock_editor_window(with_text_widget=True)
            +        text_widget = editwin.text
            +        squeezer = self.make_squeezer_instance(editwin)
            +        squeezer.count_lines = Mock(return_value=6)
            +
            +        # Prepare some text in the Text widget and squeeze it.
            +        text_widget.insert("1.0", "SOME\nTEXT\n", "stdout")
            +        text_widget.mark_set("insert", "1.0")
            +        squeezer.squeeze_current_text_event(event=Mock())
            +        self.assertEqual(len(squeezer.expandingbuttons), 1)
            +
            +        # Test squeezing the current text.
            +        text_widget.insert("1.0", "MORE\nSTUFF\n", "stdout")
            +        text_widget.mark_set("insert", "1.0")
            +        retval = squeezer.squeeze_current_text_event(event=Mock())
            +        self.assertEqual(retval, "break")
            +        self.assertEqual(text_widget.get('1.0', 'end'), '\n\n\n')
            +        self.assertEqual(len(squeezer.expandingbuttons), 2)
            +        self.assertTrue(text_widget.compare(
            +            squeezer.expandingbuttons[0],
            +            '<',
            +            squeezer.expandingbuttons[1],
            +        ))
            +
            +    def test_reload(self):
            +        """Test the reload() class-method."""
            +        editwin = self.make_mock_editor_window(with_text_widget=True)
            +        squeezer = self.make_squeezer_instance(editwin)
            +
            +        orig_auto_squeeze_min_lines = squeezer.auto_squeeze_min_lines
            +
            +        # Increase auto-squeeze-min-lines.
            +        new_auto_squeeze_min_lines = orig_auto_squeeze_min_lines + 10
            +        self.set_idleconf_option_with_cleanup(
            +            'main', 'PyShell', 'auto-squeeze-min-lines',
            +            str(new_auto_squeeze_min_lines))
            +
            +        Squeezer.reload()
            +        self.assertEqual(squeezer.auto_squeeze_min_lines,
            +                         new_auto_squeeze_min_lines)
            +
            +    def test_reload_no_squeezer_instances(self):
            +        """Test that Squeezer.reload() runs without any instances existing."""
            +        Squeezer.reload()
            +
            +
            +class ExpandingButtonTest(unittest.TestCase):
            +    """Tests for the ExpandingButton class."""
            +    # In these tests the squeezer instance is a mock, but actual tkinter
            +    # Text and Button instances are created.
            +    def make_mock_squeezer(self):
            +        """Helper for tests: Create a mock Squeezer object."""
            +        root = get_test_tk_root(self)
            +        squeezer = Mock()
            +        squeezer.editwin.text = Text(root)
            +
            +        # Set default values for the configuration settings.
            +        squeezer.auto_squeeze_min_lines = 50
            +        return squeezer
            +
            +    @patch('idlelib.squeezer.Hovertip', autospec=Hovertip)
            +    def test_init(self, MockHovertip):
            +        """Test the simplest creation of an ExpandingButton."""
            +        squeezer = self.make_mock_squeezer()
            +        text_widget = squeezer.editwin.text
            +
            +        expandingbutton = ExpandingButton('TEXT', 'TAGS', 50, squeezer)
            +        self.assertEqual(expandingbutton.s, 'TEXT')
            +
            +        # Check that the underlying tkinter.Button is properly configured.
            +        self.assertEqual(expandingbutton.master, text_widget)
            +        self.assertTrue('50 lines' in expandingbutton.cget('text'))
            +
            +        # Check that the text widget still contains no text.
            +        self.assertEqual(text_widget.get('1.0', 'end'), '\n')
            +
            +        # Check that the mouse events are bound.
            +        self.assertIn('', expandingbutton.bind())
            +        right_button_code = '' % ('2' if macosx.isAquaTk() else '3')
            +        self.assertIn(right_button_code, expandingbutton.bind())
            +
            +        # Check that ToolTip was called once, with appropriate values.
            +        self.assertEqual(MockHovertip.call_count, 1)
            +        MockHovertip.assert_called_with(expandingbutton, ANY, hover_delay=ANY)
            +
            +        # Check that 'right-click' appears in the tooltip text.
            +        tooltip_text = MockHovertip.call_args[0][1]
            +        self.assertIn('right-click', tooltip_text.lower())
            +
            +    def test_expand(self):
            +        """Test the expand event."""
            +        squeezer = self.make_mock_squeezer()
            +        expandingbutton = ExpandingButton('TEXT', 'TAGS', 50, squeezer)
            +
            +        # Insert the button into the text widget
            +        # (this is normally done by the Squeezer class).
            +        text_widget = expandingbutton.text
            +        text_widget.window_create("1.0", window=expandingbutton)
            +
            +        # Set base_text to the text widget, so that changes are actually
            +        # made to it (by ExpandingButton) and we can inspect these
            +        # changes afterwards.
            +        expandingbutton.base_text = expandingbutton.text
            +
            +        # trigger the expand event
            +        retval = expandingbutton.expand(event=Mock())
            +        self.assertEqual(retval, None)
            +
            +        # Check that the text was inserted into the text widget.
            +        self.assertEqual(text_widget.get('1.0', 'end'), 'TEXT\n')
            +
            +        # Check that the 'TAGS' tag was set on the inserted text.
            +        text_end_index = text_widget.index('end-1c')
            +        self.assertEqual(text_widget.get('1.0', text_end_index), 'TEXT')
            +        self.assertEqual(text_widget.tag_nextrange('TAGS', '1.0'),
            +                          ('1.0', text_end_index))
            +
            +        # Check that the button removed itself from squeezer.expandingbuttons.
            +        self.assertEqual(squeezer.expandingbuttons.remove.call_count, 1)
            +        squeezer.expandingbuttons.remove.assert_called_with(expandingbutton)
            +
            +    def test_expand_dangerous_oupput(self):
            +        """Test that expanding very long output asks user for confirmation."""
            +        squeezer = self.make_mock_squeezer()
            +        text = 'a' * 10**5
            +        expandingbutton = ExpandingButton(text, 'TAGS', 50, squeezer)
            +        expandingbutton.set_is_dangerous()
            +        self.assertTrue(expandingbutton.is_dangerous)
            +
            +        # Insert the button into the text widget
            +        # (this is normally done by the Squeezer class).
            +        text_widget = expandingbutton.text
            +        text_widget.window_create("1.0", window=expandingbutton)
            +
            +        # Set base_text to the text widget, so that changes are actually
            +        # made to it (by ExpandingButton) and we can inspect these
            +        # changes afterwards.
            +        expandingbutton.base_text = expandingbutton.text
            +
            +        # Patch the message box module to always return False.
            +        with patch('idlelib.squeezer.tkMessageBox') as mock_msgbox:
            +            mock_msgbox.askokcancel.return_value = False
            +            mock_msgbox.askyesno.return_value = False
            +            # Trigger the expand event.
            +            retval = expandingbutton.expand(event=Mock())
            +
            +        # Check that the event chain was broken and no text was inserted.
            +        self.assertEqual(retval, 'break')
            +        self.assertEqual(expandingbutton.text.get('1.0', 'end-1c'), '')
            +
            +        # Patch the message box module to always return True.
            +        with patch('idlelib.squeezer.tkMessageBox') as mock_msgbox:
            +            mock_msgbox.askokcancel.return_value = True
            +            mock_msgbox.askyesno.return_value = True
            +            # Trigger the expand event.
            +            retval = expandingbutton.expand(event=Mock())
            +
            +        # Check that the event chain wasn't broken and the text was inserted.
            +        self.assertEqual(retval, None)
            +        self.assertEqual(expandingbutton.text.get('1.0', 'end-1c'), text)
            +
            +    def test_copy(self):
            +        """Test the copy event."""
            +        # Testing with the actual clipboard proved problematic, so this
            +        # test replaces the clipboard manipulation functions with mocks
            +        # and checks that they are called appropriately.
            +        squeezer = self.make_mock_squeezer()
            +        expandingbutton = ExpandingButton('TEXT', 'TAGS', 50, squeezer)
            +        expandingbutton.clipboard_clear = Mock()
            +        expandingbutton.clipboard_append = Mock()
            +
            +        # Trigger the copy event.
            +        retval = expandingbutton.copy(event=Mock())
            +        self.assertEqual(retval, None)
            +
            +        # Vheck that the expanding button called clipboard_clear() and
            +        # clipboard_append('TEXT') once each.
            +        self.assertEqual(expandingbutton.clipboard_clear.call_count, 1)
            +        self.assertEqual(expandingbutton.clipboard_append.call_count, 1)
            +        expandingbutton.clipboard_append.assert_called_with('TEXT')
            +
            +    def test_view(self):
            +        """Test the view event."""
            +        squeezer = self.make_mock_squeezer()
            +        expandingbutton = ExpandingButton('TEXT', 'TAGS', 50, squeezer)
            +        expandingbutton.selection_own = Mock()
            +
            +        with patch('idlelib.squeezer.view_text', autospec=view_text)\
            +                as mock_view_text:
            +            # Trigger the view event.
            +            expandingbutton.view(event=Mock())
            +
            +            # Check that the expanding button called view_text.
            +            self.assertEqual(mock_view_text.call_count, 1)
            +
            +            # Check that the proper text was passed.
            +            self.assertEqual(mock_view_text.call_args[0][2], 'TEXT')
            +
            +    def test_rmenu(self):
            +        """Test the context menu."""
            +        squeezer = self.make_mock_squeezer()
            +        expandingbutton = ExpandingButton('TEXT', 'TAGS', 50, squeezer)
            +        with patch('tkinter.Menu') as mock_Menu:
            +            mock_menu = Mock()
            +            mock_Menu.return_value = mock_menu
            +            mock_event = Mock()
            +            mock_event.x = 10
            +            mock_event.y = 10
            +            expandingbutton.context_menu_event(event=mock_event)
            +            self.assertEqual(mock_menu.add_command.call_count,
            +                             len(expandingbutton.rmenu_specs))
            +            for label, *data in expandingbutton.rmenu_specs:
            +                mock_menu.add_command.assert_any_call(label=label, command=ANY)
            +
            +
            +if __name__ == '__main__':
            +    unittest.main(verbosity=2)
            diff --git a/Lib/idlelib/idle_test/test_stackviewer.py b/Lib/idlelib/idle_test/test_stackviewer.py
            new file mode 100644
            index 00000000000000..98f53f9537bb25
            --- /dev/null
            +++ b/Lib/idlelib/idle_test/test_stackviewer.py
            @@ -0,0 +1,47 @@
            +"Test stackviewer, coverage 63%."
            +
            +from idlelib import stackviewer
            +import unittest
            +from test.support import requires
            +from tkinter import Tk
            +
            +from idlelib.tree import TreeNode, ScrolledCanvas
            +import sys
            +
            +
            +class StackBrowserTest(unittest.TestCase):
            +
            +    @classmethod
            +    def setUpClass(cls):
            +        svs = stackviewer.sys
            +        try:
            +            abc
            +        except NameError:
            +            svs.last_type, svs.last_value, svs.last_traceback = (
            +                sys.exc_info())
            +
            +        requires('gui')
            +        cls.root = Tk()
            +        cls.root.withdraw()
            +
            +    @classmethod
            +    def tearDownClass(cls):
            +        svs = stackviewer.sys
            +        del svs.last_traceback, svs.last_type, svs.last_value
            +
            +        cls.root.update_idletasks()
            +##        for id in cls.root.tk.call('after', 'info'):
            +##            cls.root.after_cancel(id)  # Need for EditorWindow.
            +        cls.root.destroy()
            +        del cls.root
            +
            +    def test_init(self):
            +        sb = stackviewer.StackBrowser(self.root)
            +        isi = self.assertIsInstance
            +        isi(stackviewer.sc, ScrolledCanvas)
            +        isi(stackviewer.item, stackviewer.StackTreeItem)
            +        isi(stackviewer.node, TreeNode)
            +
            +
            +if __name__ == '__main__':
            +    unittest.main(verbosity=2)
            diff --git a/Lib/idlelib/idle_test/test_statusbar.py b/Lib/idlelib/idle_test/test_statusbar.py
            new file mode 100644
            index 00000000000000..203a57db89ca6a
            --- /dev/null
            +++ b/Lib/idlelib/idle_test/test_statusbar.py
            @@ -0,0 +1,41 @@
            +"Test statusbar, coverage 100%."
            +
            +from idlelib import statusbar
            +import unittest
            +from test.support import requires
            +from tkinter import Tk
            +
            +
            +class Test(unittest.TestCase):
            +
            +    @classmethod
            +    def setUpClass(cls):
            +        requires('gui')
            +        cls.root = Tk()
            +        cls.root.withdraw()
            +
            +    @classmethod
            +    def tearDownClass(cls):
            +        cls.root.update_idletasks()
            +        cls.root.destroy()
            +        del cls.root
            +
            +    def test_init(self):
            +        bar = statusbar.MultiStatusBar(self.root)
            +        self.assertEqual(bar.labels, {})
            +
            +    def test_set_label(self):
            +        bar = statusbar.MultiStatusBar(self.root)
            +        bar.set_label('left', text='sometext', width=10)
            +        self.assertIn('left', bar.labels)
            +        left = bar.labels['left']
            +        self.assertEqual(left['text'], 'sometext')
            +        self.assertEqual(left['width'], 10)
            +        bar.set_label('left', text='revised text')
            +        self.assertEqual(left['text'], 'revised text')
            +        bar.set_label('right', text='correct text')
            +        self.assertEqual(bar.labels['right']['text'], 'correct text')
            +
            +
            +if __name__ == '__main__':
            +    unittest.main(verbosity=2)
            diff --git a/Lib/idlelib/idle_test/test_text.py b/Lib/idlelib/idle_test/test_text.py
            index a5ba7bb2136654..0f31179e04b28f 100644
            --- a/Lib/idlelib/idle_test/test_text.py
            +++ b/Lib/idlelib/idle_test/test_text.py
            @@ -9,7 +9,7 @@
             class TextTest(object):
                 "Define items common to both sets of tests."
             
            -    hw = 'hello\nworld'  # Several tests insert this after after initialization.
            +    hw = 'hello\nworld'  # Several tests insert this after initialization.
                 hwn = hw+'\n'  # \n present at initialization, before insert
             
                 # setUpClass defines cls.Text and maybe cls.root.
            diff --git a/Lib/idlelib/idle_test/test_textview.py b/Lib/idlelib/idle_test/test_textview.py
            index c129c2f0819a2f..7189378ab3dd61 100644
            --- a/Lib/idlelib/idle_test/test_textview.py
            +++ b/Lib/idlelib/idle_test/test_textview.py
            @@ -1,19 +1,17 @@
            -'''Test idlelib.textview.
            +"""Test textview, coverage 100%.
             
             Since all methods and functions create (or destroy) a ViewWindow, which
             is a widget containing a widget, etcetera, all tests must be gui tests.
             Using mock Text would not change this.  Other mocks are used to retrieve
             information about calls.
            -
            -Coverage: 100%.
            -'''
            +"""
             from idlelib import textview as tv
             from test.support import requires
             requires('gui')
             
            -import unittest
             import os
            -from tkinter import Tk
            +import unittest
            +from tkinter import Tk, TclError, CHAR, NONE, WORD
             from tkinter.ttk import Button
             from idlelib.idle_test.mock_idle import Func
             from idlelib.idle_test.mock_tk import Mbox_func
            @@ -71,14 +69,65 @@ def test_ok(self):
                     view.destroy()
             
             
            -class TextFrameTest(unittest.TestCase):
            +class AutoHideScrollbarTest(unittest.TestCase):
            +    # Method set is tested in ScrollableTextFrameTest
            +    def test_forbidden_geometry(self):
            +        scroll = tv.AutoHideScrollbar(root)
            +        self.assertRaises(TclError, scroll.pack)
            +        self.assertRaises(TclError, scroll.place)
            +
            +
            +class ScrollableTextFrameTest(unittest.TestCase):
            +
            +    @classmethod
            +    def setUpClass(cls):
            +        cls.root = root = Tk()
            +        root.withdraw()
            +
            +    @classmethod
            +    def tearDownClass(cls):
            +        cls.root.update_idletasks()
            +        cls.root.destroy()
            +        del cls.root
            +
            +    def make_frame(self, wrap=NONE, **kwargs):
            +        frame = tv.ScrollableTextFrame(self.root, wrap=wrap, **kwargs)
            +        def cleanup_frame():
            +            frame.update_idletasks()
            +            frame.destroy()
            +        self.addCleanup(cleanup_frame)
            +        return frame
            +
            +    def test_line1(self):
            +        frame = self.make_frame()
            +        frame.text.insert('1.0', 'test text')
            +        self.assertEqual(frame.text.get('1.0', '1.end'), 'test text')
            +
            +    def test_horiz_scrollbar(self):
            +        # The horizontal scrollbar should be shown/hidden according to
            +        # the 'wrap' setting: It should only be shown when 'wrap' is
            +        # set to NONE.
            +
            +        # wrap = NONE -> with horizontal scrolling
            +        frame = self.make_frame(wrap=NONE)
            +        self.assertEqual(frame.text.cget('wrap'), NONE)
            +        self.assertIsNotNone(frame.xscroll)
            +
            +        # wrap != NONE -> no horizontal scrolling
            +        for wrap in [CHAR, WORD]:
            +            with self.subTest(wrap=wrap):
            +                frame = self.make_frame(wrap=wrap)
            +                self.assertEqual(frame.text.cget('wrap'), wrap)
            +                self.assertIsNone(frame.xscroll)
            +
            +
            +class ViewFrameTest(unittest.TestCase):
             
                 @classmethod
                 def setUpClass(cls):
            -        "By itself, this tests that file parsed without exception."
                     cls.root = root = Tk()
                     root.withdraw()
            -        cls.frame = tv.TextFrame(root, 'test text')
            +        cls.frame = tv.ViewFrame(root, 'test text')
             
                 @classmethod
                 def tearDownClass(cls):
            @@ -109,10 +158,10 @@ def test_view_text(self):
                     view = tv.view_text(root, 'Title', 'test text', modal=False)
                     self.assertIsInstance(view, tv.ViewWindow)
                     self.assertIsInstance(view.viewframe, tv.ViewFrame)
            -        view.ok()
            +        view.viewframe.ok()
             
                 def test_view_file(self):
            -        view = tv.view_file(root, 'Title', __file__, modal=False)
            +        view = tv.view_file(root, 'Title', __file__, 'ascii', modal=False)
                     self.assertIsInstance(view, tv.ViewWindow)
                     self.assertIsInstance(view.viewframe, tv.ViewFrame)
                     get = view.viewframe.textframe.text.get
            @@ -121,18 +170,22 @@ def test_view_file(self):
             
                 def test_bad_file(self):
                     # Mock showerror will be used; view_file will return None.
            -        view = tv.view_file(root, 'Title', 'abc.xyz', modal=False)
            +        view = tv.view_file(root, 'Title', 'abc.xyz', 'ascii', modal=False)
                     self.assertIsNone(view)
                     self.assertEqual(tv.showerror.title, 'File Load Error')
             
                 def test_bad_encoding(self):
                     p = os.path
                     fn = p.abspath(p.join(p.dirname(__file__), '..', 'CREDITS.txt'))
            -        tv.showerror.title = None
                     view = tv.view_file(root, 'Title', fn, 'ascii', modal=False)
                     self.assertIsNone(view)
                     self.assertEqual(tv.showerror.title, 'Unicode Decode Error')
             
            +    def test_nowrap(self):
            +        view = tv.view_text(root, 'Title', 'test', modal=False, wrap='none')
            +        text_widget = view.viewframe.textframe.text
            +        self.assertEqual(text_widget.cget('wrap'), 'none')
            +
             
             # Call ViewWindow with _utest=True.
             class ButtonClickTest(unittest.TestCase):
            @@ -161,7 +214,8 @@ def _command():
                 def test_view_file_bind_with_button(self):
                     def _command():
                         self.called = True
            -            self.view = tv.view_file(root, 'TITLE_FILE', __file__, _utest=True)
            +            self.view = tv.view_file(root, 'TITLE_FILE', __file__,
            +                                     encoding='ascii', _utest=True)
                     button = Button(root, text='BUTTON', command=_command)
                     button.invoke()
                     self.addCleanup(button.destroy)
            diff --git a/Lib/idlelib/idle_test/test_tooltip.py b/Lib/idlelib/idle_test/test_tooltip.py
            new file mode 100644
            index 00000000000000..c616d4fde3b6d3
            --- /dev/null
            +++ b/Lib/idlelib/idle_test/test_tooltip.py
            @@ -0,0 +1,161 @@
            +"""Test tooltip, coverage 100%.
            +
            +Coverage is 100% after excluding 6 lines with "# pragma: no cover".
            +They involve TclErrors that either should or should not happen in a
            +particular situation, and which are 'pass'ed if they do.
            +"""
            +
            +from idlelib.tooltip import TooltipBase, Hovertip
            +from test.support import requires
            +requires('gui')
            +
            +from functools import wraps
            +import time
            +from tkinter import Button, Tk, Toplevel
            +import unittest
            +
            +
            +def setUpModule():
            +    global root
            +    root = Tk()
            +
            +def tearDownModule():
            +    global root
            +    root.update_idletasks()
            +    root.destroy()
            +    del root
            +
            +
            +def add_call_counting(func):
            +    @wraps(func)
            +    def wrapped_func(*args, **kwargs):
            +        wrapped_func.call_args_list.append((args, kwargs))
            +        return func(*args, **kwargs)
            +    wrapped_func.call_args_list = []
            +    return wrapped_func
            +
            +
            +def _make_top_and_button(testobj):
            +    global root
            +    top = Toplevel(root)
            +    testobj.addCleanup(top.destroy)
            +    top.title("Test tooltip")
            +    button = Button(top, text='ToolTip test button')
            +    button.pack()
            +    testobj.addCleanup(button.destroy)
            +    top.lift()
            +    return top, button
            +
            +
            +class ToolTipBaseTest(unittest.TestCase):
            +    def setUp(self):
            +        self.top, self.button = _make_top_and_button(self)
            +
            +    def test_base_class_is_unusable(self):
            +        global root
            +        top = Toplevel(root)
            +        self.addCleanup(top.destroy)
            +
            +        button = Button(top, text='ToolTip test button')
            +        button.pack()
            +        self.addCleanup(button.destroy)
            +
            +        with self.assertRaises(NotImplementedError):
            +            tooltip = TooltipBase(button)
            +            tooltip.showtip()
            +
            +
            +class HovertipTest(unittest.TestCase):
            +    def setUp(self):
            +        self.top, self.button = _make_top_and_button(self)
            +
            +    def is_tipwindow_shown(self, tooltip):
            +        return tooltip.tipwindow and tooltip.tipwindow.winfo_viewable()
            +
            +    def test_showtip(self):
            +        tooltip = Hovertip(self.button, 'ToolTip text')
            +        self.addCleanup(tooltip.hidetip)
            +        self.assertFalse(self.is_tipwindow_shown(tooltip))
            +        tooltip.showtip()
            +        self.assertTrue(self.is_tipwindow_shown(tooltip))
            +
            +    def test_showtip_twice(self):
            +        tooltip = Hovertip(self.button, 'ToolTip text')
            +        self.addCleanup(tooltip.hidetip)
            +        self.assertFalse(self.is_tipwindow_shown(tooltip))
            +        tooltip.showtip()
            +        self.assertTrue(self.is_tipwindow_shown(tooltip))
            +        orig_tipwindow = tooltip.tipwindow
            +        tooltip.showtip()
            +        self.assertTrue(self.is_tipwindow_shown(tooltip))
            +        self.assertIs(tooltip.tipwindow, orig_tipwindow)
            +
            +    def test_hidetip(self):
            +        tooltip = Hovertip(self.button, 'ToolTip text')
            +        self.addCleanup(tooltip.hidetip)
            +        tooltip.showtip()
            +        tooltip.hidetip()
            +        self.assertFalse(self.is_tipwindow_shown(tooltip))
            +
            +    def test_showtip_on_mouse_enter_no_delay(self):
            +        tooltip = Hovertip(self.button, 'ToolTip text', hover_delay=None)
            +        self.addCleanup(tooltip.hidetip)
            +        tooltip.showtip = add_call_counting(tooltip.showtip)
            +        root.update()
            +        self.assertFalse(self.is_tipwindow_shown(tooltip))
            +        self.button.event_generate('', x=0, y=0)
            +        root.update()
            +        self.assertTrue(self.is_tipwindow_shown(tooltip))
            +        self.assertGreater(len(tooltip.showtip.call_args_list), 0)
            +
            +    def test_hover_with_delay(self):
            +        # Run multiple tests requiring an actual delay simultaneously.
            +
            +        # Test #1: A hover tip with a non-zero delay appears after the delay.
            +        tooltip1 = Hovertip(self.button, 'ToolTip text', hover_delay=100)
            +        self.addCleanup(tooltip1.hidetip)
            +        tooltip1.showtip = add_call_counting(tooltip1.showtip)
            +        root.update()
            +        self.assertFalse(self.is_tipwindow_shown(tooltip1))
            +        self.button.event_generate('', x=0, y=0)
            +        root.update()
            +        self.assertFalse(self.is_tipwindow_shown(tooltip1))
            +
            +        # Test #2: A hover tip with a non-zero delay doesn't appear when
            +        # the mouse stops hovering over the base widget before the delay
            +        # expires.
            +        tooltip2 = Hovertip(self.button, 'ToolTip text', hover_delay=100)
            +        self.addCleanup(tooltip2.hidetip)
            +        tooltip2.showtip = add_call_counting(tooltip2.showtip)
            +        root.update()
            +        self.button.event_generate('', x=0, y=0)
            +        root.update()
            +        self.button.event_generate('', x=0, y=0)
            +        root.update()
            +
            +        time.sleep(0.15)
            +        root.update()
            +
            +        # Test #1 assertions.
            +        self.assertTrue(self.is_tipwindow_shown(tooltip1))
            +        self.assertGreater(len(tooltip1.showtip.call_args_list), 0)
            +
            +        # Test #2 assertions.
            +        self.assertFalse(self.is_tipwindow_shown(tooltip2))
            +        self.assertEqual(tooltip2.showtip.call_args_list, [])
            +
            +    def test_hidetip_on_mouse_leave(self):
            +        tooltip = Hovertip(self.button, 'ToolTip text', hover_delay=None)
            +        self.addCleanup(tooltip.hidetip)
            +        tooltip.showtip = add_call_counting(tooltip.showtip)
            +        root.update()
            +        self.button.event_generate('', x=0, y=0)
            +        root.update()
            +        self.button.event_generate('', x=0, y=0)
            +        root.update()
            +        self.assertFalse(self.is_tipwindow_shown(tooltip))
            +        self.assertGreater(len(tooltip.showtip.call_args_list), 0)
            +
            +
            +if __name__ == '__main__':
            +    unittest.main(verbosity=2)
            diff --git a/Lib/idlelib/idle_test/test_tree.py b/Lib/idlelib/idle_test/test_tree.py
            index bb597d87ffd104..b3e4c10cf9e38e 100644
            --- a/Lib/idlelib/idle_test/test_tree.py
            +++ b/Lib/idlelib/idle_test/test_tree.py
            @@ -1,12 +1,10 @@
            -''' Test idlelib.tree.
            +"Test tree. coverage 56%."
             
            -Coverage: 56%
            -'''
             from idlelib import tree
            +import unittest
             from test.support import requires
             requires('gui')
            -import unittest
            -from tkinter import Tk
            +from tkinter import Tk, EventType, SCROLL
             
             
             class TreeTest(unittest.TestCase):
            @@ -31,5 +29,32 @@ def test_init(self):
                     node.expand()
             
             
            +class TestScrollEvent(unittest.TestCase):
            +
            +    def test_wheel_event(self):
            +        # Fake widget class containing `yview` only.
            +        class _Widget:
            +            def __init__(widget, *expected):
            +                widget.expected = expected
            +            def yview(widget, *args):
            +                self.assertTupleEqual(widget.expected, args)
            +        # Fake event class
            +        class _Event:
            +            pass
            +        #        (type, delta, num, amount)
            +        tests = ((EventType.MouseWheel, 120, -1, -5),
            +                 (EventType.MouseWheel, -120, -1, 5),
            +                 (EventType.ButtonPress, -1, 4, -5),
            +                 (EventType.ButtonPress, -1, 5, 5))
            +
            +        event = _Event()
            +        for ty, delta, num, amount in tests:
            +            event.type = ty
            +            event.delta = delta
            +            event.num = num
            +            res = tree.wheel_event(event, _Widget(SCROLL, amount, "units"))
            +            self.assertEqual(res, "break")
            +
            +
             if __name__ == '__main__':
                 unittest.main(verbosity=2)
            diff --git a/Lib/idlelib/idle_test/test_undo.py b/Lib/idlelib/idle_test/test_undo.py
            index e872927a6c6d99..beb5b582039f88 100644
            --- a/Lib/idlelib/idle_test/test_undo.py
            +++ b/Lib/idlelib/idle_test/test_undo.py
            @@ -1,14 +1,13 @@
            -"""Unittest for UndoDelegator in idlelib.undo.py.
            +"Test undo, coverage 77%."
            +# Only test UndoDelegator so far.
             
            -Coverage about 80% (retest).
            -"""
            +from idlelib.undo import UndoDelegator
            +import unittest
             from test.support import requires
             requires('gui')
             
            -import unittest
             from unittest.mock import Mock
             from tkinter import Text, Tk
            -from idlelib.undo import UndoDelegator
             from idlelib.percolator import Percolator
             
             
            @@ -131,5 +130,6 @@ def test_addcmd(self):
                         text.insert('insert', 'foo')
                         self.assertLessEqual(len(self.delegator.undolist), max_undo)
             
            +
             if __name__ == '__main__':
                 unittest.main(verbosity=2, exit=False)
            diff --git a/Lib/idlelib/idle_test/test_warning.py b/Lib/idlelib/idle_test/test_warning.py
            index f3269f195af831..221068c5885fcb 100644
            --- a/Lib/idlelib/idle_test/test_warning.py
            +++ b/Lib/idlelib/idle_test/test_warning.py
            @@ -5,20 +5,18 @@
             Revise if output destination changes (http://bugs.python.org/issue18318).
             Make sure warnings module is left unaltered (http://bugs.python.org/issue18081).
             '''
            -
            +from idlelib import run
            +from idlelib import pyshell as shell
             import unittest
             from test.support import captured_stderr
            -
             import warnings
            +
             # Try to capture default showwarning before Idle modules are imported.
             showwarning = warnings.showwarning
             # But if we run this file within idle, we are in the middle of the run.main loop
             # and default showwarnings has already been replaced.
             running_in_idle = 'idle' in showwarning.__name__
             
            -from idlelib import run
            -from idlelib import pyshell as shell
            -
             # The following was generated from pyshell.idle_formatwarning
             # and checked as matching expectation.
             idlemsg = '''
            @@ -29,6 +27,7 @@
             '''
             shellmsg = idlemsg + ">>> "
             
            +
             class RunWarnTest(unittest.TestCase):
             
                 @unittest.skipIf(running_in_idle, "Does not work when run within Idle.")
            @@ -46,6 +45,7 @@ def test_run_show(self):
                         # The following uses .splitlines to erase line-ending differences
                         self.assertEqual(idlemsg.splitlines(), f.getvalue().splitlines())
             
            +
             class ShellWarnTest(unittest.TestCase):
             
                 @unittest.skipIf(running_in_idle, "Does not work when run within Idle.")
            @@ -70,4 +70,4 @@ def test_shell_show(self):
             
             
             if __name__ == '__main__':
            -    unittest.main(verbosity=2, exit=False)
            +    unittest.main(verbosity=2)
            diff --git a/Lib/idlelib/idle_test/test_window.py b/Lib/idlelib/idle_test/test_window.py
            new file mode 100644
            index 00000000000000..5a2645b9cc27dc
            --- /dev/null
            +++ b/Lib/idlelib/idle_test/test_window.py
            @@ -0,0 +1,45 @@
            +"Test window, coverage 47%."
            +
            +from idlelib import window
            +import unittest
            +from test.support import requires
            +from tkinter import Tk
            +
            +
            +class WindowListTest(unittest.TestCase):
            +
            +    def test_init(self):
            +        wl = window.WindowList()
            +        self.assertEqual(wl.dict, {})
            +        self.assertEqual(wl.callbacks, [])
            +
            +    # Further tests need mock Window.
            +
            +
            +class ListedToplevelTest(unittest.TestCase):
            +
            +    @classmethod
            +    def setUpClass(cls):
            +        window.registry = set()
            +        requires('gui')
            +        cls.root = Tk()
            +        cls.root.withdraw()
            +
            +    @classmethod
            +    def tearDownClass(cls):
            +        window.registry = window.WindowList()
            +        cls.root.update_idletasks()
            +##        for id in cls.root.tk.call('after', 'info'):
            +##            cls.root.after_cancel(id)  # Need for EditorWindow.
            +        cls.root.destroy()
            +        del cls.root
            +
            +    def test_init(self):
            +
            +        win = window.ListedToplevel(self.root)
            +        self.assertIn(win, window.registry)
            +        self.assertEqual(win.focused_widget, win)
            +
            +
            +if __name__ == '__main__':
            +    unittest.main(verbosity=2)
            diff --git a/Lib/idlelib/idle_test/test_zoomheight.py b/Lib/idlelib/idle_test/test_zoomheight.py
            new file mode 100644
            index 00000000000000..aa5bdfb4fbd4c6
            --- /dev/null
            +++ b/Lib/idlelib/idle_test/test_zoomheight.py
            @@ -0,0 +1,39 @@
            +"Test zoomheight, coverage 66%."
            +# Some code is system dependent.
            +
            +from idlelib import zoomheight
            +import unittest
            +from test.support import requires
            +from tkinter import Tk
            +from idlelib.editor import EditorWindow
            +
            +
            +class Test(unittest.TestCase):
            +
            +    @classmethod
            +    def setUpClass(cls):
            +        requires('gui')
            +        cls.root = Tk()
            +        cls.root.withdraw()
            +        cls.editwin = EditorWindow(root=cls.root)
            +
            +    @classmethod
            +    def tearDownClass(cls):
            +        cls.editwin._close()
            +        cls.root.update_idletasks()
            +        for id in cls.root.tk.call('after', 'info'):
            +            cls.root.after_cancel(id)  # Need for EditorWindow.
            +        cls.root.destroy()
            +        del cls.root
            +
            +    def test_init(self):
            +        zoom = zoomheight.ZoomHeight(self.editwin)
            +        self.assertIs(zoom.editwin, self.editwin)
            +
            +    def test_zoom_height_event(self):
            +        zoom = zoomheight.ZoomHeight(self.editwin)
            +        zoom.zoom_height_event()
            +
            +
            +if __name__ == '__main__':
            +    unittest.main(verbosity=2)
            diff --git a/Lib/idlelib/iomenu.py b/Lib/idlelib/iomenu.py
            index f9b6907b40cecc..4b2833b8ca56f3 100644
            --- a/Lib/idlelib/iomenu.py
            +++ b/Lib/idlelib/iomenu.py
            @@ -15,6 +15,7 @@
             
             if idlelib.testing:  # Set True by test.test_idle to avoid setlocale.
                 encoding = 'utf-8'
            +    errors = 'surrogateescape'
             else:
                 # Try setting the locale, so that we can find out
                 # what encoding to use
            @@ -24,15 +25,9 @@
                 except (ImportError, locale.Error):
                     pass
             
            -    locale_decode = 'ascii'
                 if sys.platform == 'win32':
            -        # On Windows, we could use "mbcs". However, to give the user
            -        # a portable encoding name, we need to find the code page
            -        try:
            -            locale_encoding = locale.getdefaultlocale()[1]
            -            codecs.lookup(locale_encoding)
            -        except LookupError:
            -            pass
            +        encoding = 'utf-8'
            +        errors = 'surrogateescape'
                 else:
                     try:
                         # Different things can fail here: the locale module may not be
            @@ -40,30 +35,30 @@
                         # resulting codeset may be unknown to Python. We ignore all
                         # these problems, falling back to ASCII
                         locale_encoding = locale.nl_langinfo(locale.CODESET)
            -            if locale_encoding is None or locale_encoding is '':
            -                # situation occurs on Mac OS X
            -                locale_encoding = 'ascii'
            -            codecs.lookup(locale_encoding)
            +            if locale_encoding:
            +                codecs.lookup(locale_encoding)
                     except (NameError, AttributeError, LookupError):
                         # Try getdefaultlocale: it parses environment variables,
                         # which may give a clue. Unfortunately, getdefaultlocale has
                         # bugs that can cause ValueError.
                         try:
                             locale_encoding = locale.getdefaultlocale()[1]
            -                if locale_encoding is None or locale_encoding is '':
            -                    # situation occurs on Mac OS X
            -                    locale_encoding = 'ascii'
            -                codecs.lookup(locale_encoding)
            +                if locale_encoding:
            +                    codecs.lookup(locale_encoding)
                         except (ValueError, LookupError):
                             pass
             
            -    locale_encoding = locale_encoding.lower()
            -
            -    encoding = locale_encoding
            -    # Encoding is used in multiple files; locale_encoding nowhere.
            -    # The only use of 'encoding' below is in _decode as initial value
            -    # of deprecated block asking user for encoding.
            -    # Perhaps use elsewhere should be reviewed.
            +        if locale_encoding:
            +            encoding = locale_encoding.lower()
            +            errors = 'strict'
            +        else:
            +            # POSIX locale or macOS
            +            encoding = 'ascii'
            +            errors = 'surrogateescape'
            +        # Encoding is used in multiple files; locale_encoding nowhere.
            +        # The only use of 'encoding' below is in _decode as initial value
            +        # of deprecated block asking user for encoding.
            +        # Perhaps use elsewhere should be reviewed.
             
             coding_re = re.compile(r'^[ \t\f]*#.*?coding[:=][ \t]*([-\w.]+)', re.ASCII)
             blank_re = re.compile(r'^[ \t\f]*(?:[#\r\n]|$)', re.ASCII)
            @@ -376,20 +371,29 @@ def save_a_copy(self, event):
                     return "break"
             
                 def writefile(self, filename):
            -        self.fixlastline()
            -        text = self.text.get("1.0", "end-1c")
            -        if self.eol_convention != "\n":
            -            text = text.replace("\n", self.eol_convention)
            +        text = self.fixnewlines()
                     chars = self.encode(text)
                     try:
                         with open(filename, "wb") as f:
                             f.write(chars)
            +                f.flush()
            +                os.fsync(f.fileno())
                         return True
                     except OSError as msg:
                         tkMessageBox.showerror("I/O Error", str(msg),
                                                parent=self.text)
                         return False
             
            +    def fixnewlines(self):
            +        "Return text with final \n if needed and os eols."
            +        if (self.text.get("end-2c") != '\n'
            +            and not hasattr(self.editwin, "interp")):  # Not shell.
            +            self.text.insert("end-1c", "\n")
            +        text = self.text.get("1.0", "end-1c")
            +        if self.eol_convention != "\n":
            +            text = text.replace("\n", self.eol_convention)
            +        return text
            +
                 def encode(self, chars):
                     if isinstance(chars, bytes):
                         # This is either plain ASCII, or Tk was returning mixed-encoding
            @@ -429,11 +433,6 @@ def encode(self, chars):
                     # declared encoding
                     return BOM_UTF8 + chars.encode("utf-8")
             
            -    def fixlastline(self):
            -        c = self.text.get("end-2c")
            -        if c != '\n':
            -            self.text.insert("end-1c", "\n")
            -
                 def print_window(self, event):
                     confirm = tkMessageBox.askokcancel(
                               title="Print",
            @@ -567,8 +566,8 @@ def savecopy(self, event):
                 IOBinding(editwin)
             
             if __name__ == "__main__":
            -    import unittest
            -    unittest.main('idlelib.idle_test.test_iomenu', verbosity=2, exit=False)
            +    from unittest import main
            +    main('idlelib.idle_test.test_iomenu', verbosity=2, exit=False)
             
                 from idlelib.idle_test.htest import run
                 run(_io_binding)
            diff --git a/Lib/idlelib/macosx.py b/Lib/idlelib/macosx.py
            index d85278a0b765ae..eeaab59ae80295 100644
            --- a/Lib/idlelib/macosx.py
            +++ b/Lib/idlelib/macosx.py
            @@ -1,6 +1,8 @@
             """
            -A number of functions that enhance IDLE on Mac OSX.
            +A number of functions that enhance IDLE on macOS.
             """
            +from os.path import expanduser
            +import plistlib
             from sys import platform  # Used in _init_tk_type, changed by test.
             
             import tkinter
            @@ -79,14 +81,47 @@ def tkVersionWarning(root):
                     patchlevel = root.tk.call('info', 'patchlevel')
                     if patchlevel not in ('8.5.7', '8.5.9'):
                         return False
            -        return (r"WARNING: The version of Tcl/Tk ({0}) in use may"
            -                r" be unstable.\n"
            -                r"Visit http://www.python.org/download/mac/tcltk/"
            -                r" for current information.".format(patchlevel))
            +        return ("WARNING: The version of Tcl/Tk ({0}) in use may"
            +                " be unstable.\n"
            +                "Visit http://www.python.org/download/mac/tcltk/"
            +                " for current information.".format(patchlevel))
                 else:
                     return False
             
             
            +def readSystemPreferences():
            +    """
            +    Fetch the macOS system preferences.
            +    """
            +    if platform != 'darwin':
            +        return None
            +
            +    plist_path = expanduser('~/Library/Preferences/.GlobalPreferences.plist')
            +    try:
            +        with open(plist_path, 'rb') as plist_file:
            +            return plistlib.load(plist_file)
            +    except OSError:
            +        return None
            +
            +
            +def preferTabsPreferenceWarning():
            +    """
            +    Warn if "Prefer tabs when opening documents" is set to "Always".
            +    """
            +    if platform != 'darwin':
            +        return None
            +
            +    prefs = readSystemPreferences()
            +    if prefs and prefs.get('AppleWindowTabbingMode') == 'always':
            +        return (
            +            'WARNING: The system preference "Prefer tabs when opening'
            +            ' documents" is set to "Always". This will cause various problems'
            +            ' with IDLE. For the best experience, change this setting when'
            +            ' running IDLE (via System Preferences -> Dock).'
            +        )
            +    return None
            +
            +
             ## Fix the menu and related functions.
             
             def addOpenEventSupport(root, flist):
            @@ -128,7 +163,7 @@ def overrideRootMenu(root, flist):
                 # menu.
                 from tkinter import Menu
                 from idlelib import mainmenu
            -    from idlelib import windows
            +    from idlelib import window
             
                 closeItem = mainmenu.menudefs[0][1][-2]
             
            @@ -143,12 +178,12 @@ def overrideRootMenu(root, flist):
                 del mainmenu.menudefs[-1][1][0:2]
                 # Remove the 'Configure Idle' entry from the options menu, it is in the
                 # application menu as 'Preferences'
            -    del mainmenu.menudefs[-2][1][0]
            +    del mainmenu.menudefs[-3][1][0:2]
                 menubar = Menu(root)
                 root.configure(menu=menubar)
                 menudict = {}
             
            -    menudict['windows'] = menu = Menu(menubar, name='windows', tearoff=0)
            +    menudict['window'] = menu = Menu(menubar, name='window', tearoff=0)
                 menubar.add_cascade(label='Window', menu=menu, underline=0)
             
                 def postwindowsmenu(menu=menu):
            @@ -158,8 +193,8 @@ def postwindowsmenu(menu=menu):
             
                     if end > 0:
                         menu.delete(0, end)
            -        windows.add_windows_to_menu(menu)
            -    windows.register_callback(postwindowsmenu)
            +        window.add_windows_to_menu(menu)
            +    window.register_callback(postwindowsmenu)
             
                 def about_dialog(event=None):
                     "Handle Help 'About IDLE' event."
            @@ -192,7 +227,7 @@ def help_dialog(event=None):
                     root.bind('<>', flist.close_all_callback)
             
                     # The binding above doesn't reliably work on all versions of Tk
            -        # on MacOSX. Adding command definition below does seem to do the
            +        # on macOS. Adding command definition below does seem to do the
                     # right thing for now.
                     root.createcommand('exit', flist.close_all_callback)
             
            diff --git a/Lib/idlelib/mainmenu.py b/Lib/idlelib/mainmenu.py
            index 143570d6b11c41..74edce23483829 100644
            --- a/Lib/idlelib/mainmenu.py
            +++ b/Lib/idlelib/mainmenu.py
            @@ -36,7 +36,8 @@
                None,
                ('_Close', '<>'),
                ('E_xit', '<>'),
            -  ]),
            +   ]),
            +
              ('edit', [
                ('_Undo', '<>'),
                ('_Redo', '<>'),
            @@ -56,9 +57,10 @@
                ('E_xpand Word', '<>'),
                ('Show C_all Tip', '<>'),
                ('Show Surrounding P_arens', '<>'),
            +   ]),
             
            -  ]),
            -('format', [
            + ('format', [
            +   ('F_ormat Paragraph', '<>'),
                ('_Indent Region', '<>'),
                ('_Dedent Region', '<>'),
                ('Comment _Out Region', '<>'),
            @@ -67,33 +69,44 @@
                ('Untabify Region', '<>'),
                ('Toggle Tabs', '<>'),
                ('New Indent Width', '<>'),
            -   ('F_ormat Paragraph', '<>'),
                ('S_trip Trailing Whitespace', '<>'),
                ]),
            +
              ('run', [
            -   ('Python Shell', '<>'),
            -   ('C_heck Module', '<>'),
                ('R_un Module', '<>'),
            +   ('Run... _Customized', '<>'),
            +   ('C_heck Module', '<>'),
            +   ('Python Shell', '<>'),
                ]),
            +
              ('shell', [
                ('_View Last Restart', '<>'),
                ('_Restart Shell', '<>'),
                None,
            +   ('_Previous History', '<>'),
            +   ('_Next History', '<>'),
            +   None,
                ('_Interrupt Execution', '<>'),
                ]),
            +
              ('debug', [
                ('_Go to File/Line', '<>'),
                ('!_Debugger', '<>'),
                ('_Stack Viewer', '<>'),
                ('!_Auto-open Stack Viewer', '<>'),
                ]),
            +
              ('options', [
                ('Configure _IDLE', '<>'),
            -   ('_Code Context', '<>'),
            +   None,
            +   ('Show _Code Context', '<>'),
            +   ('Show _Line Numbers', '<>'),
            +   ('_Zoom Height', '<>'),
                ]),
            - ('windows', [
            -   ('Zoom Height', '<>'),
            +
            + ('window', [
                ]),
            +
              ('help', [
                ('_About IDLE', '<>'),
                None,
            @@ -106,3 +119,7 @@
                 menudefs[-1][1].append(('Turtle Demo', '<>'))
             
             default_keydefs = idleConf.GetCurrentKeySet()
            +
            +if __name__ == '__main__':
            +    from unittest import main
            +    main('idlelib.idle_test.test_mainmenu', verbosity=2)
            diff --git a/Lib/idlelib/multicall.py b/Lib/idlelib/multicall.py
            index b74fed4c0cd13f..dc02001292fc14 100644
            --- a/Lib/idlelib/multicall.py
            +++ b/Lib/idlelib/multicall.py
            @@ -441,5 +441,8 @@ def handler(event):
                 bindseq("")
             
             if __name__ == "__main__":
            +    from unittest import main
            +    main('idlelib.idle_test.test_mainmenu', verbosity=2, exit=False)
            +
                 from idlelib.idle_test.htest import run
                 run(_multi_call)
            diff --git a/Lib/idlelib/outwin.py b/Lib/idlelib/outwin.py
            index 6c2a792d86b99a..90272b6feb4af6 100644
            --- a/Lib/idlelib/outwin.py
            +++ b/Lib/idlelib/outwin.py
            @@ -74,10 +74,11 @@ class OutputWindow(EditorWindow):
                     ("Go to file/line", "<>", None),
                 ]
             
            +    allow_code_context = False
            +
                 def __init__(self, *args):
                     EditorWindow.__init__(self, *args)
                     self.text.bind("<>", self.goto_file_line)
            -        self.text.unbind("<>")
             
                 # Customize EditorWindow
                 def ispythonsource(self, filename):
            @@ -109,7 +110,7 @@ def write(self, s, tags=(), mark="insert"):
                     Return:
                         Length of text inserted.
                     """
            -        if isinstance(s, (bytes, bytes)):
            +        if isinstance(s, bytes):
                         s = s.decode(iomenu.encoding, "replace")
                     self.text.insert(mark, s, tags)
                     self.text.see(mark)
            @@ -184,5 +185,5 @@ def setup(self):
                     self.write = self.owin.write
             
             if __name__ == '__main__':
            -    import unittest
            -    unittest.main('idlelib.idle_test.test_outwin', verbosity=2, exit=False)
            +    from unittest import main
            +    main('idlelib.idle_test.test_outwin', verbosity=2, exit=False)
            diff --git a/Lib/idlelib/paragraph.py b/Lib/idlelib/paragraph.py
            deleted file mode 100644
            index 1270115a44ce44..00000000000000
            --- a/Lib/idlelib/paragraph.py
            +++ /dev/null
            @@ -1,195 +0,0 @@
            -"""Format a paragraph, comment block, or selection to a max width.
            -
            -Does basic, standard text formatting, and also understands Python
            -comment blocks. Thus, for editing Python source code, this
            -extension is really only suitable for reformatting these comment
            -blocks or triple-quoted strings.
            -
            -Known problems with comment reformatting:
            -* If there is a selection marked, and the first line of the
            -  selection is not complete, the block will probably not be detected
            -  as comments, and will have the normal "text formatting" rules
            -  applied.
            -* If a comment block has leading whitespace that mixes tabs and
            -  spaces, they will not be considered part of the same block.
            -* Fancy comments, like this bulleted list, aren't handled :-)
            -"""
            -import re
            -
            -from idlelib.config import idleConf
            -
            -
            -class FormatParagraph:
            -
            -    def __init__(self, editwin):
            -        self.editwin = editwin
            -
            -    @classmethod
            -    def reload(cls):
            -        cls.max_width = idleConf.GetOption('extensions', 'FormatParagraph',
            -                                           'max-width', type='int', default=72)
            -
            -    def close(self):
            -        self.editwin = None
            -
            -    def format_paragraph_event(self, event, limit=None):
            -        """Formats paragraph to a max width specified in idleConf.
            -
            -        If text is selected, format_paragraph_event will start breaking lines
            -        at the max width, starting from the beginning selection.
            -
            -        If no text is selected, format_paragraph_event uses the current
            -        cursor location to determine the paragraph (lines of text surrounded
            -        by blank lines) and formats it.
            -
            -        The length limit parameter is for testing with a known value.
            -        """
            -        limit = self.max_width if limit is None else limit
            -        text = self.editwin.text
            -        first, last = self.editwin.get_selection_indices()
            -        if first and last:
            -            data = text.get(first, last)
            -            comment_header = get_comment_header(data)
            -        else:
            -            first, last, comment_header, data = \
            -                    find_paragraph(text, text.index("insert"))
            -        if comment_header:
            -            newdata = reformat_comment(data, limit, comment_header)
            -        else:
            -            newdata = reformat_paragraph(data, limit)
            -        text.tag_remove("sel", "1.0", "end")
            -
            -        if newdata != data:
            -            text.mark_set("insert", first)
            -            text.undo_block_start()
            -            text.delete(first, last)
            -            text.insert(first, newdata)
            -            text.undo_block_stop()
            -        else:
            -            text.mark_set("insert", last)
            -        text.see("insert")
            -        return "break"
            -
            -
            -FormatParagraph.reload()
            -
            -def find_paragraph(text, mark):
            -    """Returns the start/stop indices enclosing the paragraph that mark is in.
            -
            -    Also returns the comment format string, if any, and paragraph of text
            -    between the start/stop indices.
            -    """
            -    lineno, col = map(int, mark.split("."))
            -    line = text.get("%d.0" % lineno, "%d.end" % lineno)
            -
            -    # Look for start of next paragraph if the index passed in is a blank line
            -    while text.compare("%d.0" % lineno, "<", "end") and is_all_white(line):
            -        lineno = lineno + 1
            -        line = text.get("%d.0" % lineno, "%d.end" % lineno)
            -    first_lineno = lineno
            -    comment_header = get_comment_header(line)
            -    comment_header_len = len(comment_header)
            -
            -    # Once start line found, search for end of paragraph (a blank line)
            -    while get_comment_header(line)==comment_header and \
            -              not is_all_white(line[comment_header_len:]):
            -        lineno = lineno + 1
            -        line = text.get("%d.0" % lineno, "%d.end" % lineno)
            -    last = "%d.0" % lineno
            -
            -    # Search back to beginning of paragraph (first blank line before)
            -    lineno = first_lineno - 1
            -    line = text.get("%d.0" % lineno, "%d.end" % lineno)
            -    while lineno > 0 and \
            -              get_comment_header(line)==comment_header and \
            -              not is_all_white(line[comment_header_len:]):
            -        lineno = lineno - 1
            -        line = text.get("%d.0" % lineno, "%d.end" % lineno)
            -    first = "%d.0" % (lineno+1)
            -
            -    return first, last, comment_header, text.get(first, last)
            -
            -# This should perhaps be replaced with textwrap.wrap
            -def reformat_paragraph(data, limit):
            -    """Return data reformatted to specified width (limit)."""
            -    lines = data.split("\n")
            -    i = 0
            -    n = len(lines)
            -    while i < n and is_all_white(lines[i]):
            -        i = i+1
            -    if i >= n:
            -        return data
            -    indent1 = get_indent(lines[i])
            -    if i+1 < n and not is_all_white(lines[i+1]):
            -        indent2 = get_indent(lines[i+1])
            -    else:
            -        indent2 = indent1
            -    new = lines[:i]
            -    partial = indent1
            -    while i < n and not is_all_white(lines[i]):
            -        # XXX Should take double space after period (etc.) into account
            -        words = re.split(r"(\s+)", lines[i])
            -        for j in range(0, len(words), 2):
            -            word = words[j]
            -            if not word:
            -                continue # Can happen when line ends in whitespace
            -            if len((partial + word).expandtabs()) > limit and \
            -                   partial != indent1:
            -                new.append(partial.rstrip())
            -                partial = indent2
            -            partial = partial + word + " "
            -            if j+1 < len(words) and words[j+1] != " ":
            -                partial = partial + " "
            -        i = i+1
            -    new.append(partial.rstrip())
            -    # XXX Should reformat remaining paragraphs as well
            -    new.extend(lines[i:])
            -    return "\n".join(new)
            -
            -def reformat_comment(data, limit, comment_header):
            -    """Return data reformatted to specified width with comment header."""
            -
            -    # Remove header from the comment lines
            -    lc = len(comment_header)
            -    data = "\n".join(line[lc:] for line in data.split("\n"))
            -    # Reformat to maxformatwidth chars or a 20 char width,
            -    # whichever is greater.
            -    format_width = max(limit - len(comment_header), 20)
            -    newdata = reformat_paragraph(data, format_width)
            -    # re-split and re-insert the comment header.
            -    newdata = newdata.split("\n")
            -    # If the block ends in a \n, we don't want the comment prefix
            -    # inserted after it. (Im not sure it makes sense to reformat a
            -    # comment block that is not made of complete lines, but whatever!)
            -    # Can't think of a clean solution, so we hack away
            -    block_suffix = ""
            -    if not newdata[-1]:
            -        block_suffix = "\n"
            -        newdata = newdata[:-1]
            -    return '\n'.join(comment_header+line for line in newdata) + block_suffix
            -
            -def is_all_white(line):
            -    """Return True if line is empty or all whitespace."""
            -
            -    return re.match(r"^\s*$", line) is not None
            -
            -def get_indent(line):
            -    """Return the initial space or tab indent of line."""
            -    return re.match(r"^([ \t]*)", line).group()
            -
            -def get_comment_header(line):
            -    """Return string with leading whitespace and '#' from line or ''.
            -
            -    A null return indicates that the line is not a comment line. A non-
            -    null return, such as '    #', will be used to find the other lines of
            -    a comment block with the same  indent.
            -    """
            -    m = re.match(r"^([ \t]*#*)", line)
            -    if m is None: return ""
            -    return m.group(1)
            -
            -
            -if __name__ == "__main__":
            -    import unittest
            -    unittest.main('idlelib.idle_test.test_paragraph',
            -            verbosity=2, exit=False)
            diff --git a/Lib/idlelib/parenmatch.py b/Lib/idlelib/parenmatch.py
            index 983ca20675af1d..3fd7aadb2aea84 100644
            --- a/Lib/idlelib/parenmatch.py
            +++ b/Lib/idlelib/parenmatch.py
            @@ -179,5 +179,5 @@ def set_timeout_last(self):
             
             
             if __name__ == '__main__':
            -    import unittest
            -    unittest.main('idlelib.idle_test.test_parenmatch', verbosity=2)
            +    from unittest import main
            +    main('idlelib.idle_test.test_parenmatch', verbosity=2)
            diff --git a/Lib/idlelib/percolator.py b/Lib/idlelib/percolator.py
            index d18daf05863c18..db70304f589159 100644
            --- a/Lib/idlelib/percolator.py
            +++ b/Lib/idlelib/percolator.py
            @@ -96,9 +96,8 @@ def toggle2():
                 cb2.pack()
             
             if __name__ == "__main__":
            -    import unittest
            -    unittest.main('idlelib.idle_test.test_percolator', verbosity=2,
            -                  exit=False)
            +    from unittest import main
            +    main('idlelib.idle_test.test_percolator', verbosity=2, exit=False)
             
                 from idlelib.idle_test.htest import run
                 run(_percolator)
            diff --git a/Lib/idlelib/pyparse.py b/Lib/idlelib/pyparse.py
            index 536b2d7f5fef73..d34872b4396e1e 100644
            --- a/Lib/idlelib/pyparse.py
            +++ b/Lib/idlelib/pyparse.py
            @@ -1,16 +1,22 @@
            -from collections.abc import Mapping
            +"""Define partial Python code Parser used by editor and hyperparser.
            +
            +Instances of ParseMap are used with str.translate.
            +
            +The following bound search and match functions are defined:
            +_synchre - start of popular statement;
            +_junkre - whitespace or comment line;
            +_match_stringre: string, possibly without closer;
            +_itemre - line that may have bracket structure start;
            +_closere - line that must be followed by dedent.
            +_chew_ordinaryre - non-special characters.
            +"""
             import re
            -import sys
             
            -# Reason last stmt is continued (or C_NONE if it's not).
            +# Reason last statement is continued (or C_NONE if it's not).
             (C_NONE, C_BACKSLASH, C_STRING_FIRST_LINE,
              C_STRING_NEXT_LINES, C_BRACKET) = range(5)
             
            -if 0:   # for throwaway debugging output
            -    def dump(*stuff):
            -        sys.__stdout__.write(" ".join(map(str, stuff)) + "\n")
            -
            -# Find what looks like the start of a popular stmt.
            +# Find what looks like the start of a popular statement.
             
             _synchre = re.compile(r"""
                 ^
            @@ -70,7 +76,7 @@ def dump(*stuff):
                 [^\s#\\]    # if we match, m.end()-1 is the interesting char
             """, re.VERBOSE).match
             
            -# Match start of stmts that should be followed by a dedent.
            +# Match start of statements that should be followed by a dedent.
             
             _closere = re.compile(r"""
                 \s*
            @@ -93,46 +99,27 @@ def dump(*stuff):
             """, re.VERBOSE).match
             
             
            -class StringTranslatePseudoMapping(Mapping):
            -    r"""Utility class to be used with str.translate()
            -
            -    This Mapping class wraps a given dict. When a value for a key is
            -    requested via __getitem__() or get(), the key is looked up in the
            -    given dict. If found there, the value from the dict is returned.
            -    Otherwise, the default value given upon initialization is returned.
            -
            -    This allows using str.translate() to make some replacements, and to
            -    replace all characters for which no replacement was specified with
            -    a given character instead of leaving them as-is.
            +class ParseMap(dict):
            +    r"""Dict subclass that maps anything not in dict to 'x'.
             
            -    For example, to replace everything except whitespace with 'x':
            +    This is designed to be used with str.translate in study1.
            +    Anything not specifically mapped otherwise becomes 'x'.
            +    Example: replace everything except whitespace with 'x'.
             
            -    >>> whitespace_chars = ' \t\n\r'
            -    >>> preserve_dict = {ord(c): ord(c) for c in whitespace_chars}
            -    >>> mapping = StringTranslatePseudoMapping(preserve_dict, ord('x'))
            -    >>> text = "a + b\tc\nd"
            -    >>> text.translate(mapping)
            +    >>> keepwhite = ParseMap((ord(c), ord(c)) for c in ' \t\n\r')
            +    >>> "a + b\tc\nd".translate(keepwhite)
                 'x x x\tx\nx'
                 """
            -    def __init__(self, non_defaults, default_value):
            -        self._non_defaults = non_defaults
            -        self._default_value = default_value
            -
            -        def _get(key, _get=non_defaults.get, _default=default_value):
            -            return _get(key, _default)
            -        self._get = _get
            -
            -    def __getitem__(self, item):
            -        return self._get(item)
            -
            -    def __len__(self):
            -        return len(self._non_defaults)
            +    # Calling this triples access time; see bpo-32940
            +    def __missing__(self, key):
            +        return 120  # ord('x')
             
            -    def __iter__(self):
            -        return iter(self._non_defaults)
             
            -    def get(self, key, default=None):
            -        return self._get(key)
            +# Map all ascii to 120 to avoid __missing__ call, then replace some.
            +trans = ParseMap.fromkeys(range(128), 120)
            +trans.update((ord(c), ord('(')) for c in "({[")  # open brackets => '(';
            +trans.update((ord(c), ord(')')) for c in ")}]")  # close brackets => ')'.
            +trans.update((ord(c), ord(c)) for c in "\"'\\\n#")  # Keep these.
             
             
             class Parser:
            @@ -141,40 +128,36 @@ def __init__(self, indentwidth, tabwidth):
                     self.indentwidth = indentwidth
                     self.tabwidth = tabwidth
             
            -    def set_str(self, s):
            +    def set_code(self, s):
                     assert len(s) == 0 or s[-1] == '\n'
            -        self.str = s
            +        self.code = s
                     self.study_level = 0
             
            -    # Return index of a good place to begin parsing, as close to the
            -    # end of the string as possible.  This will be the start of some
            -    # popular stmt like "if" or "def".  Return None if none found:
            -    # the caller should pass more prior context then, if possible, or
            -    # if not (the entire program text up until the point of interest
            -    # has already been tried) pass 0 to set_lo.
            -    #
            -    # This will be reliable iff given a reliable is_char_in_string
            -    # function, meaning that when it says "no", it's absolutely
            -    # guaranteed that the char is not in a string.
            -
            -    def find_good_parse_start(self, is_char_in_string=None,
            -                              _synchre=_synchre):
            -        str, pos = self.str, None
            -
            -        if not is_char_in_string:
            -            # no clue -- make the caller pass everything
            -            return None
            +    def find_good_parse_start(self, is_char_in_string):
            +        """
            +        Return index of a good place to begin parsing, as close to the
            +        end of the string as possible.  This will be the start of some
            +        popular stmt like "if" or "def".  Return None if none found:
            +        the caller should pass more prior context then, if possible, or
            +        if not (the entire program text up until the point of interest
            +        has already been tried) pass 0 to set_lo().
            +
            +        This will be reliable iff given a reliable is_char_in_string()
            +        function, meaning that when it says "no", it's absolutely
            +        guaranteed that the char is not in a string.
            +        """
            +        code, pos = self.code, None
             
                     # Peek back from the end for a good place to start,
                     # but don't try too often; pos will be left None, or
                     # bumped to a legitimate synch point.
            -        limit = len(str)
            +        limit = len(code)
                     for tries in range(5):
            -            i = str.rfind(":\n", 0, limit)
            +            i = code.rfind(":\n", 0, limit)
                         if i < 0:
                             break
            -            i = str.rfind('\n', 0, i) + 1  # start of colon line
            -            m = _synchre(str, i, limit)
            +            i = code.rfind('\n', 0, i) + 1  # start of colon line (-1+1=0)
            +            m = _synchre(code, i, limit)
                         if m and not is_char_in_string(m.start()):
                             pos = m.start()
                             break
            @@ -188,7 +171,7 @@ def find_good_parse_start(self, is_char_in_string=None,
                         # going to have to parse the whole thing to be sure, so
                         # give it one last try from the start, but stop wasting
                         # time here regardless of the outcome.
            -            m = _synchre(str)
            +            m = _synchre(code)
                         if m and not is_char_in_string(m.start()):
                             pos = m.start()
                         return pos
            @@ -197,7 +180,7 @@ def find_good_parse_start(self, is_char_in_string=None,
                     # matches.
                     i = pos + 1
                     while 1:
            -            m = _synchre(str, i)
            +            m = _synchre(code, i)
                         if m:
                             s, i = m.span()
                             if not is_char_in_string(s):
            @@ -206,29 +189,22 @@ def find_good_parse_start(self, is_char_in_string=None,
                             break
                     return pos
             
            -    # Throw away the start of the string.  Intended to be called with
            -    # find_good_parse_start's result.
            -
                 def set_lo(self, lo):
            -        assert lo == 0 or self.str[lo-1] == '\n'
            +        """ Throw away the start of the string.
            +
            +        Intended to be called with the result of find_good_parse_start().
            +        """
            +        assert lo == 0 or self.code[lo-1] == '\n'
                     if lo > 0:
            -            self.str = self.str[lo:]
            -
            -    # Build a translation table to map uninteresting chars to 'x', open
            -    # brackets to '(', close brackets to ')' while preserving quotes,
            -    # backslashes, newlines and hashes. This is to be passed to
            -    # str.translate() in _study1().
            -    _tran = {}
            -    _tran.update((ord(c), ord('(')) for c in "({[")
            -    _tran.update((ord(c), ord(')')) for c in ")}]")
            -    _tran.update((ord(c), ord(c)) for c in "\"'\\\n#")
            -    _tran = StringTranslatePseudoMapping(_tran, default_value=ord('x'))
            -
            -    # As quickly as humanly possible , find the line numbers (0-
            -    # based) of the non-continuation lines.
            -    # Creates self.{goodlines, continuation}.
            +            self.code = self.code[lo:]
             
                 def _study1(self):
            +        """Find the line numbers of non-continuation lines.
            +
            +        As quickly as humanly possible , find the line numbers (0-
            +        based) of the non-continuation lines.
            +        Creates self.{goodlines, continuation}.
            +        """
                     if self.study_level >= 1:
                         return
                     self.study_level = 1
            @@ -237,15 +213,15 @@ def _study1(self):
                     # to "(", all close brackets to ")", then collapse runs of
                     # uninteresting characters.  This can cut the number of chars
                     # by a factor of 10-40, and so greatly speed the following loop.
            -        str = self.str
            -        str = str.translate(self._tran)
            -        str = str.replace('xxxxxxxx', 'x')
            -        str = str.replace('xxxx', 'x')
            -        str = str.replace('xx', 'x')
            -        str = str.replace('xx', 'x')
            -        str = str.replace('\nx', '\n')
            -        # note that replacing x\n with \n would be incorrect, because
            -        # x may be preceded by a backslash
            +        code = self.code
            +        code = code.translate(trans)
            +        code = code.replace('xxxxxxxx', 'x')
            +        code = code.replace('xxxx', 'x')
            +        code = code.replace('xx', 'x')
            +        code = code.replace('xx', 'x')
            +        code = code.replace('\nx', '\n')
            +        # Replacing x\n with \n would be incorrect because
            +        # x may be preceded by a backslash.
             
                     # March over the squashed version of the program, accumulating
                     # the line numbers of non-continued stmts, and determining
            @@ -254,9 +230,9 @@ def _study1(self):
                     level = lno = 0     # level is nesting level; lno is line number
                     self.goodlines = goodlines = [0]
                     push_good = goodlines.append
            -        i, n = 0, len(str)
            +        i, n = 0, len(code)
                     while i < n:
            -            ch = str[i]
            +            ch = code[i]
                         i = i+1
             
                         # cases are checked in decreasing order of frequency
            @@ -283,19 +259,19 @@ def _study1(self):
                         if ch == '"' or ch == "'":
                             # consume the string
                             quote = ch
            -                if str[i-1:i+2] == quote * 3:
            +                if code[i-1:i+2] == quote * 3:
                                 quote = quote * 3
                             firstlno = lno
                             w = len(quote) - 1
                             i = i+w
                             while i < n:
            -                    ch = str[i]
            +                    ch = code[i]
                                 i = i+1
             
                                 if ch == 'x':
                                     continue
             
            -                    if str[i-1:i+w] == quote:
            +                    if code[i-1:i+w] == quote:
                                     i = i+w
                                     break
             
            @@ -310,7 +286,7 @@ def _study1(self):
             
                                 if ch == '\\':
                                     assert i < n
            -                        if str[i] == '\n':
            +                        if code[i] == '\n':
                                         lno = lno + 1
                                     i = i+1
                                     continue
            @@ -321,7 +297,7 @@ def _study1(self):
                                 # didn't break out of the loop, so we're still
                                 # inside a string
                                 if (lno - 1) == firstlno:
            -                        # before the previous \n in str, we were in the first
            +                        # before the previous \n in code, we were in the first
                                     # line of the string
                                     continuation = C_STRING_FIRST_LINE
                                 else:
            @@ -330,13 +306,13 @@ def _study1(self):
             
                         if ch == '#':
                             # consume the comment
            -                i = str.find('\n', i)
            +                i = code.find('\n', i)
                             assert i >= 0
                             continue
             
                         assert ch == '\\'
                         assert i < n
            -            if str[i] == '\n':
            +            if code[i] == '\n':
                             lno = lno + 1
                             if i+1 == n:
                                 continuation = C_BACKSLASH
            @@ -360,44 +336,45 @@ def get_continuation_type(self):
                     self._study1()
                     return self.continuation
             
            -    # study1 was sufficient to determine the continuation status,
            -    # but doing more requires looking at every character.  study2
            -    # does this for the last interesting statement in the block.
            -    # Creates:
            -    #     self.stmt_start, stmt_end
            -    #         slice indices of last interesting stmt
            -    #     self.stmt_bracketing
            -    #         the bracketing structure of the last interesting stmt;
            -    #         for example, for the statement "say(boo) or die", stmt_bracketing
            -    #         will be [(0, 0), (3, 1), (8, 0)]. Strings and comments are
            -    #         treated as brackets, for the matter.
            -    #     self.lastch
            -    #         last non-whitespace character before optional trailing
            -    #         comment
            -    #     self.lastopenbracketpos
            -    #         if continuation is C_BRACKET, index of last open bracket
            -
                 def _study2(self):
            +        """
            +        study1 was sufficient to determine the continuation status,
            +        but doing more requires looking at every character.  study2
            +        does this for the last interesting statement in the block.
            +        Creates:
            +            self.stmt_start, stmt_end
            +                slice indices of last interesting stmt
            +            self.stmt_bracketing
            +                the bracketing structure of the last interesting stmt; for
            +                example, for the statement "say(boo) or die",
            +                stmt_bracketing will be ((0, 0), (0, 1), (2, 0), (2, 1),
            +                (4, 0)). Strings and comments are treated as brackets, for
            +                the matter.
            +            self.lastch
            +                last interesting character before optional trailing comment
            +            self.lastopenbracketpos
            +                if continuation is C_BRACKET, index of last open bracket
            +        """
                     if self.study_level >= 2:
                         return
                     self._study1()
                     self.study_level = 2
             
                     # Set p and q to slice indices of last interesting stmt.
            -        str, goodlines = self.str, self.goodlines
            -        i = len(goodlines) - 1
            -        p = len(str)    # index of newest line
            +        code, goodlines = self.code, self.goodlines
            +        i = len(goodlines) - 1  # Index of newest line.
            +        p = len(code)  # End of goodlines[i]
                     while i:
                         assert p
            -            # p is the index of the stmt at line number goodlines[i].
            +            # Make p be the index of the stmt at line number goodlines[i].
                         # Move p back to the stmt at line number goodlines[i-1].
                         q = p
                         for nothing in range(goodlines[i-1], goodlines[i]):
                             # tricky: sets p to 0 if no preceding newline
            -                p = str.rfind('\n', 0, p-1) + 1
            -            # The stmt str[p:q] isn't a continuation, but may be blank
            +                p = code.rfind('\n', 0, p-1) + 1
            +            # The stmt code[p:q] isn't a continuation, but may be blank
                         # or a non-indenting comment line.
            -            if  _junkre(str, p):
            +            if  _junkre(code, p):
                             i = i-1
                         else:
                             break
            @@ -415,21 +392,21 @@ def _study2(self):
                     bracketing = [(p, 0)]
                     while p < q:
                         # suck up all except ()[]{}'"#\\
            -            m = _chew_ordinaryre(str, p, q)
            +            m = _chew_ordinaryre(code, p, q)
                         if m:
                             # we skipped at least one boring char
                             newp = m.end()
                             # back up over totally boring whitespace
                             i = newp - 1    # index of last boring char
            -                while i >= p and str[i] in " \t\n":
            +                while i >= p and code[i] in " \t\n":
                                 i = i-1
                             if i >= p:
            -                    lastch = str[i]
            +                    lastch = code[i]
                             p = newp
                             if p >= q:
                                 break
             
            -            ch = str[p]
            +            ch = code[p]
             
                         if ch in "([{":
                             push_stack(p)
            @@ -456,14 +433,14 @@ def _study2(self):
                             # have to.
                             bracketing.append((p, len(stack)+1))
                             lastch = ch
            -                p = _match_stringre(str, p, q).end()
            +                p = _match_stringre(code, p, q).end()
                             bracketing.append((p, len(stack)))
                             continue
             
                         if ch == '#':
                             # consume comment and trailing newline
                             bracketing.append((p, len(stack)+1))
            -                p = str.find('\n', p, q) + 1
            +                p = code.find('\n', p, q) + 1
                             assert p > 0
                             bracketing.append((p, len(stack)))
                             continue
            @@ -471,76 +448,78 @@ def _study2(self):
                         assert ch == '\\'
                         p = p+1     # beyond backslash
                         assert p < q
            -            if str[p] != '\n':
            +            if code[p] != '\n':
                             # the program is invalid, but can't complain
            -                lastch = ch + str[p]
            +                lastch = ch + code[p]
                         p = p+1     # beyond escaped char
             
                     # end while p < q:
             
                     self.lastch = lastch
            -        if stack:
            -            self.lastopenbracketpos = stack[-1]
            +        self.lastopenbracketpos = stack[-1] if stack else None
                     self.stmt_bracketing = tuple(bracketing)
             
            -    # Assuming continuation is C_BRACKET, return the number
            -    # of spaces the next line should be indented.
            -
                 def compute_bracket_indent(self):
            +        """Return number of spaces the next line should be indented.
            +
            +        Line continuation must be C_BRACKET.
            +        """
                     self._study2()
                     assert self.continuation == C_BRACKET
                     j = self.lastopenbracketpos
            -        str = self.str
            -        n = len(str)
            -        origi = i = str.rfind('\n', 0, j) + 1
            +        code = self.code
            +        n = len(code)
            +        origi = i = code.rfind('\n', 0, j) + 1
                     j = j+1     # one beyond open bracket
                     # find first list item; set i to start of its line
                     while j < n:
            -            m = _itemre(str, j)
            +            m = _itemre(code, j)
                         if m:
                             j = m.end() - 1     # index of first interesting char
                             extra = 0
                             break
                         else:
                             # this line is junk; advance to next line
            -                i = j = str.find('\n', j) + 1
            +                i = j = code.find('\n', j) + 1
                     else:
                         # nothing interesting follows the bracket;
                         # reproduce the bracket line's indentation + a level
                         j = i = origi
            -            while str[j] in " \t":
            +            while code[j] in " \t":
                             j = j+1
                         extra = self.indentwidth
            -        return len(str[i:j].expandtabs(self.tabwidth)) + extra
            -
            -    # Return number of physical lines in last stmt (whether or not
            -    # it's an interesting stmt!  this is intended to be called when
            -    # continuation is C_BACKSLASH).
            +        return len(code[i:j].expandtabs(self.tabwidth)) + extra
             
                 def get_num_lines_in_stmt(self):
            +        """Return number of physical lines in last stmt.
            +
            +        The statement doesn't have to be an interesting statement.  This is
            +        intended to be called when continuation is C_BACKSLASH.
            +        """
                     self._study1()
                     goodlines = self.goodlines
                     return goodlines[-1] - goodlines[-2]
             
            -    # Assuming continuation is C_BACKSLASH, return the number of spaces
            -    # the next line should be indented.  Also assuming the new line is
            -    # the first one following the initial line of the stmt.
            -
                 def compute_backslash_indent(self):
            +        """Return number of spaces the next line should be indented.
            +
            +        Line continuation must be C_BACKSLASH.  Also assume that the new
            +        line is the first one following the initial line of the stmt.
            +        """
                     self._study2()
                     assert self.continuation == C_BACKSLASH
            -        str = self.str
            +        code = self.code
                     i = self.stmt_start
            -        while str[i] in " \t":
            +        while code[i] in " \t":
                         i = i+1
                     startpos = i
             
                     # See whether the initial line starts an assignment stmt; i.e.,
                     # look for an = operator
            -        endpos = str.find('\n', startpos) + 1
            +        endpos = code.find('\n', startpos) + 1
                     found = level = 0
                     while i < endpos:
            -            ch = str[i]
            +            ch = code[i]
                         if ch in "([{":
                             level = level + 1
                             i = i+1
            @@ -549,12 +528,14 @@ def compute_backslash_indent(self):
                                 level = level - 1
                             i = i+1
                         elif ch == '"' or ch == "'":
            -                i = _match_stringre(str, i, endpos).end()
            +                i = _match_stringre(code, i, endpos).end()
                         elif ch == '#':
            +                # This line is unreachable because the # makes a comment of
            +                # everything after it.
                             break
                         elif level == 0 and ch == '=' and \
            -                   (i == 0 or str[i-1] not in "=<>!") and \
            -                   str[i+1] != '=':
            +                   (i == 0 or code[i-1] not in "=<>!") and \
            +                   code[i+1] != '=':
                             found = 1
                             break
                         else:
            @@ -564,54 +545,49 @@ def compute_backslash_indent(self):
                         # found a legit =, but it may be the last interesting
                         # thing on the line
                         i = i+1     # move beyond the =
            -            found = re.match(r"\s*\\", str[i:endpos]) is None
            +            found = re.match(r"\s*\\", code[i:endpos]) is None
             
                     if not found:
                         # oh well ... settle for moving beyond the first chunk
                         # of non-whitespace chars
                         i = startpos
            -            while str[i] not in " \t\n":
            +            while code[i] not in " \t\n":
                             i = i+1
             
            -        return len(str[self.stmt_start:i].expandtabs(\
            +        return len(code[self.stmt_start:i].expandtabs(\
                                                  self.tabwidth)) + 1
             
            -    # Return the leading whitespace on the initial line of the last
            -    # interesting stmt.
            -
                 def get_base_indent_string(self):
            +        """Return the leading whitespace on the initial line of the last
            +        interesting stmt.
            +        """
                     self._study2()
                     i, n = self.stmt_start, self.stmt_end
                     j = i
            -        str = self.str
            -        while j < n and str[j] in " \t":
            +        code = self.code
            +        while j < n and code[j] in " \t":
                         j = j + 1
            -        return str[i:j]
            -
            -    # Did the last interesting stmt open a block?
            +        return code[i:j]
             
                 def is_block_opener(self):
            +        "Return True if the last interesting statement opens a block."
                     self._study2()
                     return self.lastch == ':'
             
            -    # Did the last interesting stmt close a block?
            -
                 def is_block_closer(self):
            +        "Return True if the last interesting statement closes a block."
                     self._study2()
            -        return _closere(self.str, self.stmt_start) is not None
            +        return _closere(self.code, self.stmt_start) is not None
             
            -    # index of last open bracket ({[, or None if none
            -    lastopenbracketpos = None
            +    def get_last_stmt_bracketing(self):
            +        """Return bracketing structure of the last interesting statement.
             
            -    def get_last_open_bracket_pos(self):
            +        The returned tuple is in the format defined in _study2().
            +        """
                     self._study2()
            -        return self.lastopenbracketpos
            +        return self.stmt_bracketing
             
            -    # the structure of the bracketing of the last interesting statement,
            -    # in the format defined in _study2, or None if the text didn't contain
            -    # anything
            -    stmt_bracketing = None
             
            -    def get_last_stmt_bracketing(self):
            -        self._study2()
            -        return self.stmt_bracketing
            +if __name__ == '__main__':
            +    from unittest import main
            +    main('idlelib.idle_test.test_pyparse', verbosity=2)
            diff --git a/Lib/idlelib/pyshell.py b/Lib/idlelib/pyshell.py
            index 8b07d52cc4872a..66ae0f7435daba 100755
            --- a/Lib/idlelib/pyshell.py
            +++ b/Lib/idlelib/pyshell.py
            @@ -1,6 +1,8 @@
             #! /usr/bin/env python3
             
             import sys
            +if __name__ == "__main__":
            +    sys.modules['idlelib.pyshell'] = sys.modules['__main__']
             
             try:
                 from tkinter import *
            @@ -8,6 +10,17 @@
                 print("** IDLE can't import Tkinter.\n"
                       "Your Python may not be configured for Tk. **", file=sys.__stderr__)
                 raise SystemExit(1)
            +
            +# Valid arguments for the ...Awareness call below are defined in the following.
            +# https://msdn.microsoft.com/en-us/library/windows/desktop/dn280512(v=vs.85).aspx
            +if sys.platform == 'win32':
            +    try:
            +        import ctypes
            +        PROCESS_SYSTEM_DPI_AWARE = 1  # Int required.
            +        ctypes.OleDLL('shcore').SetProcessDpiAwareness(PROCESS_SYSTEM_DPI_AWARE)
            +    except (ImportError, AttributeError, OSError):
            +        pass
            +
             import tkinter.messagebox as tkMessageBox
             if TkVersion < 8.5:
                 root = Tk()  # otherwise create root in main
            @@ -27,6 +40,7 @@
             import re
             import socket
             import subprocess
            +from textwrap import TextWrapper
             import threading
             import time
             import tokenize
            @@ -40,7 +54,7 @@
             from idlelib.filelist import FileList
             from idlelib.outwin import OutputWindow
             from idlelib import rpc
            -from idlelib.run import idle_formatwarning, PseudoInputFile, PseudoOutputFile
            +from idlelib.run import idle_formatwarning, StdInputFile, StdOutputFile
             from idlelib.undo import UndoDelegator
             
             HOST = '127.0.0.1' # python execution server on localhost loopback
            @@ -119,6 +133,7 @@ def __init__(self, *args):
                     self.text.bind("<>", self.clear_breakpoint_here)
                     self.text.bind("<>", self.flist.open_shell)
             
            +        #TODO: don't read/write this from/to .idlerc when testing
                     self.breakpointPath = os.path.join(
                             idleConf.userdir, 'breakpoints.lst')
                     # whenever a file is changed, restore breakpoints
            @@ -373,6 +388,19 @@ def handle_EOF(self):
                     "Override the base class - just re-raise EOFError"
                     raise EOFError
             
            +def restart_line(width, filename):  # See bpo-38141.
            +    """Return width long restart line formatted with filename.
            +
            +    Fill line with balanced '='s, with any extras and at least one at
            +    the beginning.  Do not end with a trailing space.
            +    """
            +    tag = f"= RESTART: {filename or 'Shell'} ="
            +    if width >= len(tag):
            +        div, mod = divmod((width -len(tag)), 2)
            +        return f"{(div+mod)*'='}{tag}{div*'='}"
            +    else:
            +        return tag[:-2]  # Remove ' ='.
            +
             
             class ModifiedInterpreter(InteractiveInterpreter):
             
            @@ -380,7 +408,6 @@ def __init__(self, tkconsole):
                     self.tkconsole = tkconsole
                     locals = sys.modules['__main__'].__dict__
                     InteractiveInterpreter.__init__(self, locals=locals)
            -        self.save_warnings_filters = None
                     self.restarting = False
                     self.subprocess_arglist = None
                     self.port = PORT
            @@ -404,10 +431,7 @@ def build_subprocess_arglist(self):
                     # run from the IDLE source directory.
                     del_exitf = idleConf.GetOption('main', 'General', 'delete-exitfunc',
                                                    default=False, type='bool')
            -        if __name__ == 'idlelib.pyshell':
            -            command = "__import__('idlelib.run').run.main(%r)" % (del_exitf,)
            -        else:
            -            command = "__import__('run').main(%r)" % (del_exitf,)
            +        command = "__import__('idlelib.run').run.main(%r)" % (del_exitf,)
                     return [sys.executable] + w + ["-c", command, str(self.port)]
             
                 def start_subprocess(self):
            @@ -481,9 +505,8 @@ def restart_subprocess(self, with_cwd=False, filename=''):
                     console.stop_readline()
                     # annotate restart in shell window and mark it
                     console.text.delete("iomark", "end-1c")
            -        tag = 'RESTART: ' + (filename if filename else 'Shell')
            -        halfbar = ((int(console.width) -len(tag) - 4) // 2) * '='
            -        console.write("\n{0} {1} {0}".format(halfbar, tag))
            +        console.write('\n')
            +        console.write(restart_line(console.width, filename))
                     console.text.mark_set("restart", "end-1c")
                     console.text.mark_gravity("restart", "left")
                     if not filename:
            @@ -635,6 +658,9 @@ def execfile(self, filename, source=None):
                     if source is None:
                         with tokenize.open(filename) as fp:
                             source = fp.read()
            +                if use_subprocess:
            +                    source = (f"__file__ = r'''{os.path.abspath(filename)}'''\n"
            +                              + source + "\ndel __file__")
                     try:
                         code = compile(source, filename, "exec")
                     except (OverflowError, SyntaxError):
            @@ -650,27 +676,11 @@ def execfile(self, filename, source=None):
                 def runsource(self, source):
                     "Extend base class method: Stuff the source in the line cache first"
                     filename = self.stuffsource(source)
            -        self.more = 0
            -        self.save_warnings_filters = warnings.filters[:]
            -        warnings.filterwarnings(action="error", category=SyntaxWarning)
                     # at the moment, InteractiveInterpreter expects str
                     assert isinstance(source, str)
            -        #if isinstance(source, str):
            -        #    from idlelib import iomenu
            -        #    try:
            -        #        source = source.encode(iomenu.encoding)
            -        #    except UnicodeError:
            -        #        self.tkconsole.resetoutput()
            -        #        self.write("Unsupported characters in input\n")
            -        #        return
            -        try:
            -            # InteractiveInterpreter.runsource() calls its runcode() method,
            -            # which is overridden (see below)
            -            return InteractiveInterpreter.runsource(self, source, filename)
            -        finally:
            -            if self.save_warnings_filters is not None:
            -                warnings.filters[:] = self.save_warnings_filters
            -                self.save_warnings_filters = None
            +        # InteractiveInterpreter.runsource() calls its runcode() method,
            +        # which is overridden (see below)
            +        return InteractiveInterpreter.runsource(self, source, filename)
             
                 def stuffsource(self, source):
                     "Stuff source in the filename cache"
            @@ -749,9 +759,6 @@ def runcode(self, code):
                     if self.tkconsole.executing:
                         self.interp.restart_subprocess()
                     self.checklinecache()
            -        if self.save_warnings_filters is not None:
            -            warnings.filters[:] = self.save_warnings_filters
            -            self.save_warnings_filters = None
                     debugger = self.debugger
                     try:
                         self.tkconsole.beginexecuting()
            @@ -810,10 +817,10 @@ def display_port_binding_error(self):
             
                 def display_no_subprocess_error(self):
                     tkMessageBox.showerror(
            -            "Subprocess Startup Error",
            -            "IDLE's subprocess didn't make connection.  Either IDLE can't "
            -            "start a subprocess or personal firewall software is blocking "
            -            "the connection.",
            +            "Subprocess Connection Error",
            +            "IDLE's subprocess didn't make connection.\n"
            +            "See the 'Startup failure' section of the IDLE doc, online at\n"
            +            "https://docs.python.org/3/library/idle.html#startup-failure",
                         parent=self.tkconsole.text)
             
                 def display_executing_dialog(self):
            @@ -838,10 +845,16 @@ class PyShell(OutputWindow):
                     ("edit", "_Edit"),
                     ("debug", "_Debug"),
                     ("options", "_Options"),
            -        ("windows", "_Window"),
            +        ("window", "_Window"),
                     ("help", "_Help"),
                 ]
             
            +    # Extend right-click context menu
            +    rmenu_specs = OutputWindow.rmenu_specs + [
            +        ("Squeeze", "<>"),
            +    ]
            +
            +    allow_line_numbers = False
             
                 # New classes
                 from idlelib.history import History
            @@ -880,15 +893,22 @@ def __init__(self, flist=None):
                     if use_subprocess:
                         text.bind("<>", self.view_restart_mark)
                         text.bind("<>", self.restart_shell)
            +        squeezer = self.Squeezer(self)
            +        text.bind("<>",
            +                  squeezer.squeeze_current_text_event)
             
                     self.save_stdout = sys.stdout
                     self.save_stderr = sys.stderr
                     self.save_stdin = sys.stdin
                     from idlelib import iomenu
            -        self.stdin = PseudoInputFile(self, "stdin", iomenu.encoding)
            -        self.stdout = PseudoOutputFile(self, "stdout", iomenu.encoding)
            -        self.stderr = PseudoOutputFile(self, "stderr", iomenu.encoding)
            -        self.console = PseudoOutputFile(self, "console", iomenu.encoding)
            +        self.stdin = StdInputFile(self, "stdin",
            +                                  iomenu.encoding, iomenu.errors)
            +        self.stdout = StdOutputFile(self, "stdout",
            +                                    iomenu.encoding, iomenu.errors)
            +        self.stderr = StdOutputFile(self, "stderr",
            +                                    iomenu.encoding, "backslashreplace")
            +        self.console = StdOutputFile(self, "console",
            +                                     iomenu.encoding, iomenu.errors)
                     if not use_subprocess:
                         sys.stdout = self.stdout
                         sys.stderr = self.stderr
            @@ -972,12 +992,12 @@ def open_debugger(self):
                 def beginexecuting(self):
                     "Helper for ModifiedInterpreter"
                     self.resetoutput()
            -        self.executing = 1
            +        self.executing = True
             
                 def endexecuting(self):
                     "Helper for ModifiedInterpreter"
            -        self.executing = 0
            -        self.canceled = 0
            +        self.executing = False
            +        self.canceled = False
                     self.showprompt()
             
                 def close(self):
            @@ -1019,7 +1039,7 @@ def short_title(self):
                     return self.shell_title
             
                 COPYRIGHT = \
            -          'Type "copyright", "credits" or "license()" for more information.'
            +          'Type "help", "copyright", "credits" or "license()" for more information.'
             
                 def begin(self):
                     self.text.mark_set("iomark", "insert")
            @@ -1054,7 +1074,7 @@ def stop_readline(self):
                 def readline(self):
                     save = self.reading
                     try:
            -            self.reading = 1
            +            self.reading = True
                         self.top.mainloop()  # nested mainloop()
                     finally:
                         self.reading = save
            @@ -1066,11 +1086,11 @@ def readline(self):
                         line = "\n"
                     self.resetoutput()
                     if self.canceled:
            -            self.canceled = 0
            +            self.canceled = False
                         if not use_subprocess:
                             raise KeyboardInterrupt
                     if self.endoffile:
            -            self.endoffile = 0
            +            self.endoffile = False
                         line = ""
                     return line
             
            @@ -1088,8 +1108,8 @@ def cancel_callback(self, event=None):
                         self.interp.write("KeyboardInterrupt\n")
                         self.showprompt()
                         return "break"
            -        self.endoffile = 0
            -        self.canceled = 1
            +        self.endoffile = False
            +        self.canceled = True
                     if (self.executing and self.interp.rpcclt):
                         if self.interp.getdebugger():
                             self.interp.restart_subprocess()
            @@ -1109,8 +1129,8 @@ def eof_callback(self, event):
                         self.resetoutput()
                         self.close()
                     else:
            -            self.canceled = 0
            -            self.endoffile = 1
            +            self.canceled = False
            +            self.endoffile = True
                         self.top.quit()
                     return "break"
             
            @@ -1255,6 +1275,14 @@ def showprompt(self):
                     self.set_line_and_column()
                     self.io.reset_undo()
             
            +    def show_warning(self, msg):
            +        width = self.interp.tkconsole.width
            +        wrapper = TextWrapper(width=width, tabsize=8, expand_tabs=True)
            +        wrapped_msg = '\n'.join(wrapper.wrap(msg))
            +        if not wrapped_msg.endswith('\n'):
            +            wrapped_msg += '\n'
            +        self.per.bottom.insert("iomark linestart", wrapped_msg, "stderr")
            +
                 def resetoutput(self):
                     source = self.text.get("iomark", "end-1c")
                     if self.history:
            @@ -1263,18 +1291,9 @@ def resetoutput(self):
                         self.text.insert("end-1c", "\n")
                     self.text.mark_set("iomark", "end-1c")
                     self.set_line_and_column()
            +        self.ctip.remove_calltip_window()
             
                 def write(self, s, tags=()):
            -        if isinstance(s, str) and len(s) and max(s) > '\uffff':
            -            # Tk doesn't support outputting non-BMP characters
            -            # Let's assume what printed string is not very long,
            -            # find first non-BMP character and construct informative
            -            # UnicodeEncodeError exception.
            -            for start, char in enumerate(s):
            -                if char > '\uffff':
            -                    break
            -            raise UnicodeEncodeError("UCS-2", char, start, start+1,
            -                                     'Non-BMP character not supported in Tk')
                     try:
                         self.text.mark_gravity("iomark", "right")
                         count = OutputWindow.write(self, s, tags, "iomark")
            @@ -1283,7 +1302,7 @@ def write(self, s, tags=()):
                         raise ###pass  # ### 11Aug07 KBK if we are expecting exceptions
                                        # let's find out what they are and be specific.
                     if self.canceled:
            -            self.canceled = 0
            +            self.canceled = False
                         if not use_subprocess:
                             raise KeyboardInterrupt
                     return count
            @@ -1465,10 +1484,15 @@ def main():
                 if system() == 'Windows':
                     iconfile = os.path.join(icondir, 'idle.ico')
                     root.wm_iconbitmap(default=iconfile)
            -    else:
            -        ext = '.png' if TkVersion >= 8.6 else '.gif'
            +    elif not macosx.isAquaTk():
            +        if TkVersion >= 8.6:
            +            ext = '.png'
            +            sizes = (16, 32, 48, 256)
            +        else:
            +            ext = '.gif'
            +            sizes = (16, 32, 48)
                     iconfiles = [os.path.join(icondir, 'idle_%d%s' % (size, ext))
            -                     for size in (16, 32, 48)]
            +                     for size in sizes]
                     icons = [PhotoImage(master=root, file=iconfile)
                              for iconfile in iconfiles]
                     root.wm_iconphoto(True, *icons)
            @@ -1523,12 +1547,20 @@ def main():
                         shell.interp.execfile(script)
                 elif shell:
                     # If there is a shell window and no cmd or script in progress,
            -        # check for problematic OS X Tk versions and print a warning
            -        # message in the IDLE shell window; this is less intrusive
            -        # than always opening a separate window.
            +        # check for problematic issues and print warning message(s) in
            +        # the IDLE shell window; this is less intrusive than always
            +        # opening a separate window.
            +
            +        # Warn if using a problematic OS X Tk version.
                     tkversionwarning = macosx.tkVersionWarning(root)
                     if tkversionwarning:
            -            shell.interp.runcommand("print('%s')" % tkversionwarning)
            +            shell.show_warning(tkversionwarning)
            +
            +        # Warn if the "Prefer tabs when opening documents" system
            +        # preference is set to "Always".
            +        prefer_tabs_preference_warning = macosx.preferTabsPreferenceWarning()
            +        if prefer_tabs_preference_warning:
            +            shell.show_warning(prefer_tabs_preference_warning)
             
                 while flist.inversedict:  # keep IDLE running while files are open.
                     root.mainloop()
            @@ -1536,7 +1568,6 @@ def main():
                 capture_warnings(False)
             
             if __name__ == "__main__":
            -    sys.modules['pyshell'] = sys.modules['__main__']
                 main()
             
             capture_warnings(False)  # Make sure turned off; see issue 18081
            diff --git a/Lib/idlelib/query.py b/Lib/idlelib/query.py
            index 593506383c41ab..2a88530b4d082a 100644
            --- a/Lib/idlelib/query.py
            +++ b/Lib/idlelib/query.py
            @@ -1,6 +1,5 @@
             """
             Dialogs that query users and verify the answer before accepting.
            -Use ttk widgets, limiting use to tcl/tk 8.5+, as in IDLE 3.6+.
             
             Query is the generic base class for a popup dialog.
             The user must either enter a valid answer or close the dialog.
            @@ -22,10 +21,11 @@
             
             import importlib
             import os
            +import shlex
             from sys import executable, platform  # Platform is set for one test.
             
            -from tkinter import Toplevel, StringVar, W, E, S
            -from tkinter.ttk import Frame, Button, Entry, Label
            +from tkinter import Toplevel, StringVar, BooleanVar, W, E, S
            +from tkinter.ttk import Frame, Button, Entry, Label, Checkbutton
             from tkinter import filedialog
             from tkinter.font import Font
             
            @@ -36,10 +36,10 @@ class Query(Toplevel):
                 """
                 def __init__(self, parent, title, message, *, text0='', used_names={},
                              _htest=False, _utest=False):
            -        """Create popup, do not return until tk widget destroyed.
            +        """Create modal popup, return when destroyed.
             
            -        Additional subclass init must be done before calling this
            -        unless  _utest=True is passed to suppress wait_window().
            +        Additional subclass init must be done before this unless
            +        _utest=True is passed to suppress wait_window().
             
                     title - string, title of popup dialog
                     message - string, informational message to display
            @@ -48,15 +48,17 @@ def __init__(self, parent, title, message, *, text0='', used_names={},
                     _htest - bool, change box location when running htest
                     _utest - bool, leave window hidden and not modal
                     """
            -        Toplevel.__init__(self, parent)
            -        self.withdraw()  # Hide while configuring, especially geometry.
            -        self.parent = parent
            -        self.title(title)
            +        self.parent = parent  # Needed for Font call.
                     self.message = message
                     self.text0 = text0
                     self.used_names = used_names
            +
            +        Toplevel.__init__(self, parent)
            +        self.withdraw()  # Hide while configuring, especially geometry.
            +        self.title(title)
                     self.transient(parent)
                     self.grab_set()
            +
                     windowingsystem = self.tk.call('tk', 'windowingsystem')
                     if windowingsystem == 'aqua':
                         try:
            @@ -69,9 +71,9 @@ def __init__(self, parent, title, message, *, text0='', used_names={},
                     self.protocol("WM_DELETE_WINDOW", self.cancel)
                     self.bind('', self.ok)
                     self.bind("", self.ok)
            -        self.resizable(height=False, width=False)
            +
                     self.create_widgets()
            -        self.update_idletasks()  # Needed here for winfo_reqwidth below.
            +        self.update_idletasks()  # Need here for winfo_reqwidth below.
                     self.geometry(  # Center dialog over parent (or below htest box).
                             "+%d+%d" % (
                                 parent.winfo_rootx() +
            @@ -80,12 +82,19 @@ def __init__(self, parent, title, message, *, text0='', used_names={},
                                 ((parent.winfo_height()/2 - self.winfo_reqheight()/2)
                                 if not _htest else 150)
                             ) )
            +        self.resizable(height=False, width=False)
            +
                     if not _utest:
                         self.deiconify()  # Unhide now that geometry set.
                         self.wait_window()
             
            -    def create_widgets(self):  # Call from override, if any.
            -        # Bind to self widgets needed for entry_ok or unittest.
            +    def create_widgets(self, ok_text='OK'):  # Do not replace.
            +        """Create entry (rows, extras, buttons.
            +
            +        Entry stuff on rows 0-2, spanning cols 0-2.
            +        Buttons on row 99, cols 1, 2.
            +        """
            +        # Bind to self the widgets needed for entry_ok or unittest.
                     self.frame = frame = Frame(self, padding=10)
                     frame.grid(column=0, row=0, sticky='news')
                     frame.grid_columnconfigure(0, weight=1)
            @@ -99,26 +108,31 @@ def create_widgets(self):  # Call from override, if any.
                                            exists=True, root=self.parent)
                     self.entry_error = Label(frame, text=' ', foreground='red',
                                              font=self.error_font)
            -        self.button_ok = Button(
            -                frame, text='OK', default='active', command=self.ok)
            -        self.button_cancel = Button(
            -                frame, text='Cancel', command=self.cancel)
            -
            +        # Display or blank error by setting ['text'] =.
                     entrylabel.grid(column=0, row=0, columnspan=3, padx=5, sticky=W)
                     self.entry.grid(column=0, row=1, columnspan=3, padx=5, sticky=W+E,
                                     pady=[10,0])
                     self.entry_error.grid(column=0, row=2, columnspan=3, padx=5,
                                           sticky=W+E)
            +
            +        self.create_extra()
            +
            +        self.button_ok = Button(
            +                frame, text=ok_text, default='active', command=self.ok)
            +        self.button_cancel = Button(
            +                frame, text='Cancel', command=self.cancel)
            +
                     self.button_ok.grid(column=1, row=99, padx=5)
                     self.button_cancel.grid(column=2, row=99, padx=5)
             
            +    def create_extra(self): pass  # Override to add widgets.
            +
                 def showerror(self, message, widget=None):
                     #self.bell(displayof=self)
                     (widget or self.entry_error)['text'] = 'ERROR: ' + message
             
                 def entry_ok(self):  # Example: usually replace.
                     "Return non-blank entry or None."
            -        self.entry_error['text'] = ''
                     entry = self.entry.get().strip()
                     if not entry:
                         self.showerror('blank line.')
            @@ -130,6 +144,7 @@ def ok(self, event=None):  # Do not replace.
             
                     Otherwise leave dialog open for user to correct entry or cancel.
                     '''
            +        self.entry_error['text'] = ''
                     entry = self.entry_ok()
                     if entry is not None:
                         self.result = entry
            @@ -143,6 +158,10 @@ def cancel(self, event=None):  # Do not replace.
                     self.result = None
                     self.destroy()
             
            +    def destroy(self):
            +        self.grab_release()
            +        super().destroy()
            +
             
             class SectionName(Query):
                 "Get a name for a config file section name."
            @@ -155,7 +174,6 @@ def __init__(self, parent, title, message, used_names,
             
                 def entry_ok(self):
                     "Return sensible ConfigParser section name or None."
            -        self.entry_error['text'] = ''
                     name = self.entry.get().strip()
                     if not name:
                         self.showerror('no name specified.')
            @@ -180,7 +198,6 @@ def __init__(self, parent, title, message, text0,
             
                 def entry_ok(self):
                     "Return entered module name as file path or None."
            -        self.entry_error['text'] = ''
                     name = self.entry.get().strip()
                     if not name:
                         self.showerror('no name specified.')
            @@ -206,6 +223,22 @@ def entry_ok(self):
                     return file_path
             
             
            +class Goto(Query):
            +    "Get a positive line number for editor Go To Line."
            +    # Used in editor.EditorWindow.goto_line_event.
            +
            +    def entry_ok(self):
            +        try:
            +            lineno = int(self.entry.get())
            +        except ValueError:
            +            self.showerror('not a base 10 integer.')
            +            return None
            +        if lineno <= 0:
            +            self.showerror('not a positive integer.')
            +            return None
            +        return lineno
            +
            +
             class HelpSource(Query):
                 "Get menu name and help source for Help menu."
                 # Used in ConfigDialog.HelpListItemAdd/Edit, (941/9)
            @@ -223,8 +256,8 @@ def __init__(self, parent, title, *, menuitem='', filepath='',
                             parent, title, message, text0=menuitem,
                             used_names=used_names, _htest=_htest, _utest=_utest)
             
            -    def create_widgets(self):
            -        super().create_widgets()
            +    def create_extra(self):
            +        "Add path widjets to rows 10-12."
                     frame = self.frame
                     pathlabel = Label(frame, anchor='w', justify='left',
                                       text='Help File Path: Enter URL or browse for file')
            @@ -293,16 +326,64 @@ def path_ok(self):
             
                 def entry_ok(self):
                     "Return apparently valid (name, path) or None"
            -        self.entry_error['text'] = ''
                     self.path_error['text'] = ''
                     name = self.item_ok()
                     path = self.path_ok()
                     return None if name is None or path is None else (name, path)
             
            +class CustomRun(Query):
            +    """Get settings for custom run of module.
            +
            +    1. Command line arguments to extend sys.argv.
            +    2. Whether to restart Shell or not.
            +    """
            +    # Used in runscript.run_custom_event
            +
            +    def __init__(self, parent, title, *, cli_args=[],
            +                 _htest=False, _utest=False):
            +        """cli_args is a list of strings.
            +
            +        The list is assigned to the default Entry StringVar.
            +        The strings are displayed joined by ' ' for display.
            +        """
            +        message = 'Command Line Arguments for sys.argv:'
            +        super().__init__(
            +                parent, title, message, text0=cli_args,
            +                _htest=_htest, _utest=_utest)
            +
            +    def create_extra(self):
            +        "Add run mode on rows 10-12."
            +        frame = self.frame
            +        self.restartvar = BooleanVar(self, value=True)
            +        restart = Checkbutton(frame, variable=self.restartvar, onvalue=True,
            +                              offvalue=False, text='Restart shell')
            +        self.args_error = Label(frame, text=' ', foreground='red',
            +                                font=self.error_font)
            +
            +        restart.grid(column=0, row=10, columnspan=3, padx=5, sticky='w')
            +        self.args_error.grid(column=0, row=12, columnspan=3, padx=5,
            +                             sticky='we')
            +
            +    def cli_args_ok(self):
            +        "Validity check and parsing for command line arguments."
            +        cli_string = self.entry.get().strip()
            +        try:
            +            cli_args = shlex.split(cli_string, posix=True)
            +        except ValueError as err:
            +            self.showerror(str(err))
            +            return None
            +        return cli_args
            +
            +    def entry_ok(self):
            +        "Return apparently valid (cli_args, restart) or None"
            +        cli_args = self.cli_args_ok()
            +        restart = self.restartvar.get()
            +        return None if cli_args is None else (cli_args, restart)
            +
             
             if __name__ == '__main__':
            -    import unittest
            -    unittest.main('idlelib.idle_test.test_query', verbosity=2, exit=False)
            +    from unittest import main
            +    main('idlelib.idle_test.test_query', verbosity=2, exit=False)
             
                 from idlelib.idle_test.htest import run
            -    run(Query, HelpSource)
            +    run(Query, HelpSource, CustomRun)
            diff --git a/Lib/idlelib/redirector.py b/Lib/idlelib/redirector.py
            index ec681de38d457f..9ab34c5acfb22c 100644
            --- a/Lib/idlelib/redirector.py
            +++ b/Lib/idlelib/redirector.py
            @@ -167,9 +167,8 @@ def my_insert(*args):
                 original_insert = redir.register("insert", my_insert)
             
             if __name__ == "__main__":
            -    import unittest
            -    unittest.main('idlelib.idle_test.test_redirector',
            -                  verbosity=2, exit=False)
            +    from unittest import main
            +    main('idlelib.idle_test.test_redirector', verbosity=2, exit=False)
             
                 from idlelib.idle_test.htest import run
                 run(_widget_redirector)
            diff --git a/Lib/idlelib/replace.py b/Lib/idlelib/replace.py
            index abd9e59f4e5d17..6be034af9626b3 100644
            --- a/Lib/idlelib/replace.py
            +++ b/Lib/idlelib/replace.py
            @@ -1,7 +1,7 @@
             """Replace dialog for IDLE. Inherits SearchDialogBase for GUI.
            -Uses idlelib.SearchEngine for search capability.
            +Uses idlelib.searchengine.SearchEngine for search capability.
             Defines various replace related functions like replace, replace all,
            -replace+find.
            +and replace+find.
             """
             import re
             
            @@ -10,9 +10,16 @@
             from idlelib.searchbase import SearchDialogBase
             from idlelib import searchengine
             
            +
             def replace(text):
            -    """Returns a singleton ReplaceDialog instance.The single dialog
            -     saves user entries and preferences across instances."""
            +    """Create or reuse a singleton ReplaceDialog instance.
            +
            +    The singleton dialog saves user entries and preferences
            +    across instances.
            +
            +    Args:
            +        text: Text widget containing the text to be searched.
            +    """
                 root = text._root()
                 engine = searchengine.get(root)
                 if not hasattr(engine, "_replacedialog"):
            @@ -22,16 +29,36 @@ def replace(text):
             
             
             class ReplaceDialog(SearchDialogBase):
            +    "Dialog for finding and replacing a pattern in text."
             
                 title = "Replace Dialog"
                 icon = "Replace"
             
                 def __init__(self, root, engine):
            -        SearchDialogBase.__init__(self, root, engine)
            +        """Create search dialog for finding and replacing text.
            +
            +        Uses SearchDialogBase as the basis for the GUI and a
            +        searchengine instance to prepare the search.
            +
            +        Attributes:
            +            replvar: StringVar containing 'Replace with:' value.
            +            replent: Entry widget for replvar.  Created in
            +                create_entries().
            +            ok: Boolean used in searchengine.search_text to indicate
            +                whether the search includes the selection.
            +        """
            +        super().__init__(root, engine)
                     self.replvar = StringVar(root)
             
                 def open(self, text):
            -        """Display the replace dialog"""
            +        """Make dialog visible on top of others and ready to use.
            +
            +        Also, highlight the currently selected text and set the
            +        search to include the current selection (self.ok).
            +
            +        Args:
            +            text: Text widget being searched.
            +        """
                     SearchDialogBase.open(self, text)
                     try:
                         first = text.index("sel.first")
            @@ -44,37 +71,50 @@ def open(self, text):
                     first = first or text.index("insert")
                     last = last or first
                     self.show_hit(first, last)
            -        self.ok = 1
            +        self.ok = True
             
                 def create_entries(self):
            -        """Create label and text entry widgets"""
            +        "Create base and additional label and text entry widgets."
                     SearchDialogBase.create_entries(self)
                     self.replent = self.make_entry("Replace with:", self.replvar)[0]
             
                 def create_command_buttons(self):
            +        """Create base and additional command buttons.
            +
            +        The additional buttons are for Find, Replace,
            +        Replace+Find, and Replace All.
            +        """
                     SearchDialogBase.create_command_buttons(self)
                     self.make_button("Find", self.find_it)
                     self.make_button("Replace", self.replace_it)
            -        self.make_button("Replace+Find", self.default_command, 1)
            +        self.make_button("Replace+Find", self.default_command, isdef=True)
                     self.make_button("Replace All", self.replace_all)
             
                 def find_it(self, event=None):
            -        self.do_find(0)
            +        "Handle the Find button."
            +        self.do_find(False)
             
                 def replace_it(self, event=None):
            +        """Handle the Replace button.
            +
            +        If the find is successful, then perform replace.
            +        """
                     if self.do_find(self.ok):
                         self.do_replace()
             
                 def default_command(self, event=None):
            -        "Replace and find next."
            +        """Handle the Replace+Find button as the default command.
            +
            +        First performs a replace and then, if the replace was
            +        successful, a find next.
            +        """
                     if self.do_find(self.ok):
                         if self.do_replace():  # Only find next match if replace succeeded.
                                                # A bad re can cause it to fail.
            -                self.do_find(0)
            +                self.do_find(False)
             
                 def _replace_expand(self, m, repl):
            -        """ Helper function for expanding a regular expression
            -            in the replace field, if needed. """
            +        "Expand replacement text if regular expression."
                     if self.engine.isre():
                         try:
                             new = m.expand(repl)
            @@ -87,7 +127,15 @@ def _replace_expand(self, m, repl):
                     return new
             
                 def replace_all(self, event=None):
            -        """Replace all instances of patvar with replvar in text"""
            +        """Handle the Replace All button.
            +
            +        Search text for occurrences of the Find value and replace
            +        each of them.  The 'wrap around' value controls the start
            +        point for searching.  If wrap isn't set, then the searching
            +        starts at the first occurrence after the current selection;
            +        if wrap is set, the replacement starts at the first line.
            +        The replacement is always done top-to-bottom in the text.
            +        """
                     prog = self.engine.getprog()
                     if not prog:
                         return
            @@ -104,12 +152,13 @@ def replace_all(self, event=None):
                     if self.engine.iswrap():
                         line = 1
                         col = 0
            -        ok = 1
            +        ok = True
                     first = last = None
                     # XXX ought to replace circular instead of top-to-bottom when wrapping
                     text.undo_block_start()
            -        while 1:
            -            res = self.engine.search_forward(text, prog, line, col, 0, ok)
            +        while True:
            +            res = self.engine.search_forward(text, prog, line, col,
            +                                             wrap=False, ok=ok)
                         if not res:
                             break
                         line, m = res
            @@ -130,13 +179,17 @@ def replace_all(self, event=None):
                             if new:
                                 text.insert(first, new)
                         col = i + len(new)
            -            ok = 0
            +            ok = False
                     text.undo_block_stop()
                     if first and last:
                         self.show_hit(first, last)
                     self.close()
             
            -    def do_find(self, ok=0):
            +    def do_find(self, ok=False):
            +        """Search for and highlight next occurrence of pattern in text.
            +
            +        No text replacement is done with this option.
            +        """
                     if not self.engine.getprog():
                         return False
                     text = self.text
            @@ -149,10 +202,11 @@ def do_find(self, ok=0):
                     first = "%d.%d" % (line, i)
                     last = "%d.%d" % (line, j)
                     self.show_hit(first, last)
            -        self.ok = 1
            +        self.ok = True
                     return True
             
                 def do_replace(self):
            +        "Replace search pattern in text with replacement value."
                     prog = self.engine.getprog()
                     if not prog:
                         return False
            @@ -180,12 +234,20 @@ def do_replace(self):
                         text.insert(first, new)
                     text.undo_block_stop()
                     self.show_hit(first, text.index("insert"))
            -        self.ok = 0
            +        self.ok = False
                     return True
             
                 def show_hit(self, first, last):
            -        """Highlight text from 'first' to 'last'.
            -        'first', 'last' - Text indices"""
            +        """Highlight text between first and last indices.
            +
            +        Text is highlighted via the 'hit' tag and the marked
            +        section is brought into view.
            +
            +        The colors from the 'hit' tag aren't currently shown
            +        when the text is displayed.  This is due to the 'sel'
            +        tag being added first, so the colors in the 'sel'
            +        config are seen instead of the colors for 'hit'.
            +        """
                     text = self.text
                     text.mark_set("insert", first)
                     text.tag_remove("sel", "1.0", "end")
            @@ -199,18 +261,19 @@ def show_hit(self, first, last):
                     text.update_idletasks()
             
                 def close(self, event=None):
            +        "Close the dialog and remove hit tags."
                     SearchDialogBase.close(self, event)
                     self.text.tag_remove("hit", "1.0", "end")
             
             
             def _replace_dialog(parent):  # htest #
                 from tkinter import Toplevel, Text, END, SEL
            -    from tkinter.ttk import Button
            +    from tkinter.ttk import Frame, Button
             
            -    box = Toplevel(parent)
            -    box.title("Test ReplaceDialog")
            +    top = Toplevel(parent)
            +    top.title("Test ReplaceDialog")
                 x, y = map(int, parent.geometry().split('+')[1:])
            -    box.geometry("+%d+%d" % (x, y + 175))
            +    top.geometry("+%d+%d" % (x, y + 175))
             
                 # mock undo delegator methods
                 def undo_block_start():
            @@ -219,7 +282,9 @@ def undo_block_start():
                 def undo_block_stop():
                     pass
             
            -    text = Text(box, inactiveselectbackground='gray')
            +    frame = Frame(top)
            +    frame.pack()
            +    text = Text(frame, inactiveselectbackground='gray')
                 text.undo_block_start = undo_block_start
                 text.undo_block_stop = undo_block_stop
                 text.pack()
            @@ -231,13 +296,12 @@ def show_replace():
                     replace(text)
                     text.tag_remove(SEL, "1.0", END)
             
            -    button = Button(box, text="Replace", command=show_replace)
            +    button = Button(frame, text="Replace", command=show_replace)
                 button.pack()
             
             if __name__ == '__main__':
            -    import unittest
            -    unittest.main('idlelib.idle_test.test_replace',
            -                verbosity=2, exit=False)
            +    from unittest import main
            +    main('idlelib.idle_test.test_replace', verbosity=2, exit=False)
             
                 from idlelib.idle_test.htest import run
                 run(_replace_dialog)
            diff --git a/Lib/idlelib/rpc.py b/Lib/idlelib/rpc.py
            index 8f57edb836dec8..9962477cc56185 100644
            --- a/Lib/idlelib/rpc.py
            +++ b/Lib/idlelib/rpc.py
            @@ -43,16 +43,20 @@
             import types
             
             def unpickle_code(ms):
            +    "Return code object from marshal string ms."
                 co = marshal.loads(ms)
                 assert isinstance(co, types.CodeType)
                 return co
             
             def pickle_code(co):
            +    "Return unpickle function and tuple with marshalled co code object."
                 assert isinstance(co, types.CodeType)
                 ms = marshal.dumps(co)
                 return unpickle_code, (ms,)
             
             def dumps(obj, protocol=None):
            +    "Return pickled (or marshalled) string for obj."
            +    # IDLE passes 'None' to select pickle.DEFAULT_PROTOCOL.
                 f = io.BytesIO()
                 p = CodePickler(f, protocol)
                 p.dump(obj)
            @@ -625,3 +629,8 @@ def displayhook(value):
                     sys.stdout.write(text)
                 sys.stdout.write("\n")
                 builtins._ = value
            +
            +
            +if __name__ == '__main__':
            +    from unittest import main
            +    main('idlelib.idle_test.test_rpc', verbosity=2,)
            diff --git a/Lib/idlelib/rstrip.py b/Lib/idlelib/rstrip.py
            deleted file mode 100644
            index 18c86f9b2c8896..00000000000000
            --- a/Lib/idlelib/rstrip.py
            +++ /dev/null
            @@ -1,29 +0,0 @@
            -'Provides "Strip trailing whitespace" under the "Format" menu.'
            -
            -class RstripExtension:
            -
            -    def __init__(self, editwin):
            -        self.editwin = editwin
            -
            -    def do_rstrip(self, event=None):
            -
            -        text = self.editwin.text
            -        undo = self.editwin.undo
            -
            -        undo.undo_block_start()
            -
            -        end_line = int(float(text.index('end')))
            -        for cur in range(1, end_line):
            -            txt = text.get('%i.0' % cur, '%i.end' % cur)
            -            raw = len(txt)
            -            cut = len(txt.rstrip())
            -            # Since text.delete() marks file as changed, even if not,
            -            # only call it when needed to actually delete something.
            -            if cut < raw:
            -                text.delete('%i.%i' % (cur, cut), '%i.end' % cur)
            -
            -        undo.undo_block_stop()
            -
            -if __name__ == "__main__":
            -    import unittest
            -    unittest.main('idlelib.idle_test.test_rstrip', verbosity=2, exit=False)
            diff --git a/Lib/idlelib/run.py b/Lib/idlelib/run.py
            index 176fe3db743bd4..5bd84aadcd8011 100644
            --- a/Lib/idlelib/run.py
            +++ b/Lib/idlelib/run.py
            @@ -1,17 +1,23 @@
            +""" idlelib.run
            +
            +Simplified, pyshell.ModifiedInterpreter spawns a subprocess with
            +f'''{sys.executable} -c "__import__('idlelib.run').run.main()"'''
            +'.run' is needed because __import__ returns idlelib, not idlelib.run.
            +"""
            +import functools
             import io
             import linecache
             import queue
             import sys
            +import textwrap
             import time
             import traceback
             import _thread as thread
             import threading
             import warnings
             
            -import tkinter  # Tcl, deletions, messagebox if startup fails
            -
             from idlelib import autocomplete  # AutoComplete, fetch_encodings
            -from idlelib import calltips  # CallTips
            +from idlelib import calltip  # Calltip
             from idlelib import debugger_r  # start_debugger
             from idlelib import debugobj_r  # remote_object_tree_item
             from idlelib import iomenu  # encoding
            @@ -19,11 +25,16 @@
             from idlelib import stackviewer  # StackTreeItem
             import __main__
             
            -for mod in ('simpledialog', 'messagebox', 'font',
            -            'dialog', 'filedialog', 'commondialog',
            -            'ttk'):
            -    delattr(tkinter, mod)
            -    del sys.modules['tkinter.' + mod]
            +import tkinter  # Use tcl and, if startup fails, messagebox.
            +if not hasattr(sys.modules['idlelib.run'], 'firstrun'):
            +    # Undo modifications of tkinter by idlelib imports; see bpo-25507.
            +    for mod in ('simpledialog', 'messagebox', 'font',
            +                'dialog', 'filedialog', 'commondialog',
            +                'ttk'):
            +        delattr(tkinter, mod)
            +        del sys.modules['tkinter.' + mod]
            +    # Avoid AttributeError if run again; see bpo-37038.
            +    sys.modules['idlelib.run'].firstrun = False
             
             LOCALHOST = '127.0.0.1'
             
            @@ -190,11 +201,13 @@ def show_socket_error(err, address):
                 root = tkinter.Tk()
                 fix_scaling(root)
                 root.withdraw()
            -    msg = f"IDLE's subprocess can't connect to {address[0]}:{address[1]}.\n"\
            -          f"Fatal OSError #{err.errno}: {err.strerror}.\n"\
            -          f"See the 'Startup failure' section of the IDLE doc, online at\n"\
            -          f"https://docs.python.org/3/library/idle.html#startup-failure"
            -    showerror("IDLE Subprocess Error", msg, parent=root)
            +    showerror(
            +            "Subprocess Connection Error",
            +            f"IDLE's subprocess can't connect to {address[0]}:{address[1]}.\n"
            +            f"Fatal OSError #{err.errno}: {err.strerror}.\n"
            +            "See the 'Startup failure' section of the IDLE doc, online at\n"
            +            "https://docs.python.org/3/library/idle.html#startup-failure",
            +            parent=root)
                 root.destroy()
             
             def print_exception():
            @@ -294,6 +307,67 @@ def fix_scaling(root):
                             font['size'] = round(-0.75*size)
             
             
            +def fixdoc(fun, text):
            +    tem = (fun.__doc__ + '\n\n') if fun.__doc__ is not None else ''
            +    fun.__doc__ = tem + textwrap.fill(textwrap.dedent(text))
            +
            +RECURSIONLIMIT_DELTA = 30
            +
            +def install_recursionlimit_wrappers():
            +    """Install wrappers to always add 30 to the recursion limit."""
            +    # see: bpo-26806
            +
            +    @functools.wraps(sys.setrecursionlimit)
            +    def setrecursionlimit(*args, **kwargs):
            +        # mimic the original sys.setrecursionlimit()'s input handling
            +        if kwargs:
            +            raise TypeError(
            +                "setrecursionlimit() takes no keyword arguments")
            +        try:
            +            limit, = args
            +        except ValueError:
            +            raise TypeError(f"setrecursionlimit() takes exactly one "
            +                            f"argument ({len(args)} given)")
            +        if not limit > 0:
            +            raise ValueError(
            +                "recursion limit must be greater or equal than 1")
            +
            +        return setrecursionlimit.__wrapped__(limit + RECURSIONLIMIT_DELTA)
            +
            +    fixdoc(setrecursionlimit, f"""\
            +            This IDLE wrapper adds {RECURSIONLIMIT_DELTA} to prevent possible
            +            uninterruptible loops.""")
            +
            +    @functools.wraps(sys.getrecursionlimit)
            +    def getrecursionlimit():
            +        return getrecursionlimit.__wrapped__() - RECURSIONLIMIT_DELTA
            +
            +    fixdoc(getrecursionlimit, f"""\
            +            This IDLE wrapper subtracts {RECURSIONLIMIT_DELTA} to compensate
            +            for the {RECURSIONLIMIT_DELTA} IDLE adds when setting the limit.""")
            +
            +    # add the delta to the default recursion limit, to compensate
            +    sys.setrecursionlimit(sys.getrecursionlimit() + RECURSIONLIMIT_DELTA)
            +
            +    sys.setrecursionlimit = setrecursionlimit
            +    sys.getrecursionlimit = getrecursionlimit
            +
            +
            +def uninstall_recursionlimit_wrappers():
            +    """Uninstall the recursion limit wrappers from the sys module.
            +
            +    IDLE only uses this for tests. Users can import run and call
            +    this to remove the wrapping.
            +    """
            +    if (
            +            getattr(sys.setrecursionlimit, '__wrapped__', None) and
            +            getattr(sys.getrecursionlimit, '__wrapped__', None)
            +    ):
            +        sys.setrecursionlimit = sys.setrecursionlimit.__wrapped__
            +        sys.getrecursionlimit = sys.getrecursionlimit.__wrapped__
            +        sys.setrecursionlimit(sys.getrecursionlimit() - RECURSIONLIMIT_DELTA)
            +
            +
             class MyRPCServer(rpc.RPCServer):
             
                 def handle_error(self, request, client_address):
            @@ -327,17 +401,22 @@ def handle_error(self, request, client_address):
             
             # Pseudofiles for shell-remote communication (also used in pyshell)
             
            -class PseudoFile(io.TextIOBase):
            +class StdioFile(io.TextIOBase):
             
            -    def __init__(self, shell, tags, encoding=None):
            +    def __init__(self, shell, tags, encoding='utf-8', errors='strict'):
                     self.shell = shell
                     self.tags = tags
                     self._encoding = encoding
            +        self._errors = errors
             
                 @property
                 def encoding(self):
                     return self._encoding
             
            +    @property
            +    def errors(self):
            +        return self._errors
            +
                 @property
                 def name(self):
                     return '<%s>' % self.tags
            @@ -346,7 +425,7 @@ def isatty(self):
                     return True
             
             
            -class PseudoOutputFile(PseudoFile):
            +class StdOutputFile(StdioFile):
             
                 def writable(self):
                     return True
            @@ -354,19 +433,12 @@ def writable(self):
                 def write(self, s):
                     if self.closed:
                         raise ValueError("write to closed file")
            -        if type(s) is not str:
            -            if not isinstance(s, str):
            -                raise TypeError('must be str, not ' + type(s).__name__)
            -            # See issue #19481
            -            s = str.__str__(s)
            +        s = str.encode(s, self.encoding, self.errors).decode(self.encoding, self.errors)
                     return self.shell.write(s, self.tags)
             
             
            -class PseudoInputFile(PseudoFile):
            -
            -    def __init__(self, shell, tags, encoding=None):
            -        PseudoFile.__init__(self, shell, tags, encoding)
            -        self._line_buffer = ''
            +class StdInputFile(StdioFile):
            +    _line_buffer = ''
             
                 def readable(self):
                     return True
            @@ -421,12 +493,12 @@ def handle(self):
                     executive = Executive(self)
                     self.register("exec", executive)
                     self.console = self.get_remote_proxy("console")
            -        sys.stdin = PseudoInputFile(self.console, "stdin",
            -                iomenu.encoding)
            -        sys.stdout = PseudoOutputFile(self.console, "stdout",
            -                iomenu.encoding)
            -        sys.stderr = PseudoOutputFile(self.console, "stderr",
            -                iomenu.encoding)
            +        sys.stdin = StdInputFile(self.console, "stdin",
            +                                 iomenu.encoding, iomenu.errors)
            +        sys.stdout = StdOutputFile(self.console, "stdout",
            +                                   iomenu.encoding, iomenu.errors)
            +        sys.stderr = StdOutputFile(self.console, "stderr",
            +                                   iomenu.encoding, "backslashreplace")
             
                     sys.displayhook = rpc.displayhook
                     # page help() text to shell.
            @@ -437,6 +509,8 @@ def handle(self):
                     # sys.stdin gets changed from within IDLE's shell. See issue17838.
                     self._keep_stdin = sys.stdin
             
            +        install_recursionlimit_wrappers()
            +
                     self.interp = self.get_remote_proxy("interp")
                     rpc.RPCHandler.getresponse(self, myseq=None, wait=0.05)
             
            @@ -462,7 +536,7 @@ class Executive(object):
                 def __init__(self, rpchandler):
                     self.rpchandler = rpchandler
                     self.locals = __main__.__dict__
            -        self.calltip = calltips.CallTips()
            +        self.calltip = calltip.Calltip()
                     self.autocomplete = autocomplete.AutoComplete()
             
                 def runcode(self, code):
            @@ -474,15 +548,16 @@ def runcode(self, code):
                             exec(code, self.locals)
                         finally:
                             interruptable = False
            -        except SystemExit:
            -            # Scripts that raise SystemExit should just
            -            # return to the interactive prompt
            -            pass
            +        except SystemExit as e:
            +            if e.args:  # SystemExit called with an argument.
            +                ob = e.args[0]
            +                if not isinstance(ob, (type(None), int)):
            +                    print('SystemExit: ' + str(ob), file=sys.stderr)
            +            # Return to the interactive prompt.
                     except:
                         self.usr_exc_info = sys.exc_info()
                         if quitting:
                             exit()
            -            # even print a user code SystemExit exception, continue
                         print_exception()
                         jit = self.rpchandler.console.getvar("<>")
                         if jit:
            @@ -522,4 +597,9 @@ def stackviewer(self, flist_oid=None):
                     item = stackviewer.StackTreeItem(flist, tb)
                     return debugobj_r.remote_object_tree_item(item)
             
            -capture_warnings(False)  # Make sure turned off; see issue 18081
            +
            +if __name__ == '__main__':
            +    from unittest import main
            +    main('idlelib.idle_test.test_run', verbosity=2)
            +
            +capture_warnings(False)  # Make sure turned off; see bpo-18081.
            diff --git a/Lib/idlelib/runscript.py b/Lib/idlelib/runscript.py
            index 45bf56345825a1..a54108794ab595 100644
            --- a/Lib/idlelib/runscript.py
            +++ b/Lib/idlelib/runscript.py
            @@ -18,6 +18,8 @@
             from idlelib.config import idleConf
             from idlelib import macosx
             from idlelib import pyshell
            +from idlelib.query import CustomRun
            +from idlelib import outwin
             
             indent_message = """Error: Inconsistent indentation detected!
             
            @@ -38,11 +40,16 @@ def __init__(self, editwin):
                     # XXX This should be done differently
                     self.flist = self.editwin.flist
                     self.root = self.editwin.root
            +        # cli_args is list of strings that extends sys.argv
            +        self.cli_args = []
             
                     if macosx.isCocoaTk():
                         self.editwin.text_frame.bind('<>', self._run_module_event)
             
                 def check_module_event(self, event):
            +        if isinstance(self.editwin, outwin.OutputWindow):
            +            self.editwin.text.bell()
            +            return 'break'
                     filename = self.getfilename()
                     if not filename:
                         return 'break'
            @@ -108,20 +115,27 @@ def run_module_event(self, event):
                         # tries to run a module using the keyboard shortcut
                         # (the menu item works fine).
                         self.editwin.text_frame.after(200,
            -                lambda: self.editwin.text_frame.event_generate('<>'))
            +                lambda: self.editwin.text_frame.event_generate(
            +                        '<>'))
                         return 'break'
                     else:
                         return self._run_module_event(event)
             
            -    def _run_module_event(self, event):
            +    def run_custom_event(self, event):
            +        return self._run_module_event(event, customize=True)
            +
            +    def _run_module_event(self, event, *, customize=False):
                     """Run the module after setting up the environment.
             
            -        First check the syntax.  If OK, make sure the shell is active and
            -        then transfer the arguments, set the run environment's working
            -        directory to the directory of the module being executed and also
            -        add that directory to its sys.path if not already included.
            +        First check the syntax.  Next get customization.  If OK, make
            +        sure the shell is active and then transfer the arguments, set
            +        the run environment's working directory to the directory of the
            +        module being executed and also add that directory to its
            +        sys.path if not already included.
                     """
            -
            +        if isinstance(self.editwin, outwin.OutputWindow):
            +            self.editwin.text.bell()
            +            return 'break'
                     filename = self.getfilename()
                     if not filename:
                         return 'break'
            @@ -130,23 +144,34 @@ def _run_module_event(self, event):
                         return 'break'
                     if not self.tabnanny(filename):
                         return 'break'
            +        if customize:
            +            title = f"Customize {self.editwin.short_title()} Run"
            +            run_args = CustomRun(self.shell.text, title,
            +                                 cli_args=self.cli_args).result
            +            if not run_args:  # User cancelled.
            +                return 'break'
            +        self.cli_args, restart = run_args if customize else ([], True)
                     interp = self.shell.interp
            -        if pyshell.use_subprocess:
            -            interp.restart_subprocess(with_cwd=False, filename=
            -                        self.editwin._filename_to_unicode(filename))
            +        if pyshell.use_subprocess and restart:
            +            interp.restart_subprocess(
            +                    with_cwd=False, filename=filename)
                     dirname = os.path.dirname(filename)
            -        # XXX Too often this discards arguments the user just set...
            -        interp.runcommand("""if 1:
            +        argv = [filename]
            +        if self.cli_args:
            +            argv += self.cli_args
            +        interp.runcommand(f"""if 1:
                         __file__ = {filename!r}
                         import sys as _sys
                         from os.path import basename as _basename
            +            argv = {argv!r}
                         if (not _sys.argv or
            -                _basename(_sys.argv[0]) != _basename(__file__)):
            -                _sys.argv = [__file__]
            +                _basename(_sys.argv[0]) != _basename(__file__) or
            +                len(argv) > 1):
            +                _sys.argv = argv
                         import os as _os
                         _os.chdir({dirname!r})
            -            del _sys, _basename, _os
            -            \n""".format(filename=filename, dirname=dirname))
            +            del _sys, argv, _basename, _os
            +            \n""")
                     interp.prepend_syspath(filename)
                     # XXX KBK 03Jul04 When run w/o subprocess, runtime warnings still
                     #         go to __stderr__.  With subprocess, they go to the shell.
            @@ -193,3 +218,8 @@ def errorbox(self, title, message):
                     # XXX This should really be a function of EditorWindow...
                     tkMessageBox.showerror(title, message, parent=self.editwin.text)
                     self.editwin.text.focus_set()
            +
            +
            +if __name__ == "__main__":
            +    from unittest import main
            +    main('idlelib.idle_test.test_runscript', verbosity=2,)
            diff --git a/Lib/idlelib/scrolledlist.py b/Lib/idlelib/scrolledlist.py
            index cdf658404ab643..71fd18ab19ec8a 100644
            --- a/Lib/idlelib/scrolledlist.py
            +++ b/Lib/idlelib/scrolledlist.py
            @@ -1,5 +1,5 @@
             from tkinter import *
            -from tkinter.ttk import Scrollbar
            +from tkinter.ttk import Frame, Scrollbar
             
             from idlelib import macosx
             
            @@ -142,6 +142,8 @@ def on_double(self, index): print("double", self.get(index))
                     scrolled_list.append("Item %02d" % i)
             
             if __name__ == '__main__':
            -    # At the moment, test_scrolledlist merely creates instance, like htest.
            +    from unittest import main
            +    main('idlelib.idle_test.test_scrolledlist', verbosity=2,)
            +
                 from idlelib.idle_test.htest import run
                 run(_scrolled_list)
            diff --git a/Lib/idlelib/search.py b/Lib/idlelib/search.py
            index 4b906593082284..b35f3b59c3d2e8 100644
            --- a/Lib/idlelib/search.py
            +++ b/Lib/idlelib/search.py
            @@ -1,10 +1,23 @@
            +"""Search dialog for Find, Find Again, and Find Selection
            +   functionality.
            +
            +   Inherits from SearchDialogBase for GUI and uses searchengine
            +   to prepare search pattern.
            +"""
             from tkinter import TclError
             
             from idlelib import searchengine
             from idlelib.searchbase import SearchDialogBase
             
             def _setup(text):
            -    "Create or find the singleton SearchDialog instance."
            +    """Return the new or existing singleton SearchDialog instance.
            +
            +    The singleton dialog saves user entries and preferences
            +    across instances.
            +
            +    Args:
            +        text: Text widget containing the text to be searched.
            +    """
                 root = text._root()
                 engine = searchengine.get(root)
                 if not hasattr(engine, "_searchdialog"):
            @@ -12,31 +25,71 @@ def _setup(text):
                 return engine._searchdialog
             
             def find(text):
            -    "Handle the editor edit menu item and corresponding event."
            +    """Open the search dialog.
            +
            +    Module-level function to access the singleton SearchDialog
            +    instance and open the dialog.  If text is selected, it is
            +    used as the search phrase; otherwise, the previous entry
            +    is used.  No search is done with this command.
            +    """
                 pat = text.get("sel.first", "sel.last")
                 return _setup(text).open(text, pat)  # Open is inherited from SDBase.
             
             def find_again(text):
            -    "Handle the editor edit menu item and corresponding event."
            +    """Repeat the search for the last pattern and preferences.
            +
            +    Module-level function to access the singleton SearchDialog
            +    instance to search again using the user entries and preferences
            +    from the last dialog.  If there was no prior search, open the
            +    search dialog; otherwise, perform the search without showing the
            +    dialog.
            +    """
                 return _setup(text).find_again(text)
             
             def find_selection(text):
            -    "Handle the editor edit menu item and corresponding event."
            +    """Search for the selected pattern in the text.
            +
            +    Module-level function to access the singleton SearchDialog
            +    instance to search using the selected text.  With a text
            +    selection, perform the search without displaying the dialog.
            +    Without a selection, use the prior entry as the search phrase
            +    and don't display the dialog.  If there has been no prior
            +    search, open the search dialog.
            +    """
                 return _setup(text).find_selection(text)
             
             
             class SearchDialog(SearchDialogBase):
            +    "Dialog for finding a pattern in text."
             
                 def create_widgets(self):
            +        "Create the base search dialog and add a button for Find Next."
                     SearchDialogBase.create_widgets(self)
            -        self.make_button("Find Next", self.default_command, 1)
            +        # TODO - why is this here and not in a create_command_buttons?
            +        self.make_button("Find Next", self.default_command, isdef=True)
             
                 def default_command(self, event=None):
            +        "Handle the Find Next button as the default command."
                     if not self.engine.getprog():
                         return
                     self.find_again(self.text)
             
                 def find_again(self, text):
            +        """Repeat the last search.
            +
            +        If no search was previously run, open a new search dialog.  In
            +        this case, no search is done.
            +
            +        If a search was previously run, the search dialog won't be
            +        shown and the options from the previous search (including the
            +        search pattern) will be used to find the next occurrence
            +        of the pattern.  Next is relative based on direction.
            +
            +        Position the window to display the located occurrence in the
            +        text.
            +
            +        Return True if the search was successful and False otherwise.
            +        """
                     if not self.engine.getpat():
                         self.open(text)
                         return False
            @@ -66,6 +119,13 @@ def find_again(self, text):
                         return False
             
                 def find_selection(self, text):
            +        """Search for selected text with previous dialog preferences.
            +
            +        Instead of using the same pattern for searching (as Find
            +        Again does), this first resets the pattern to the currently
            +        selected text.  If the selected text isn't changed, then use
            +        the prior search phrase.
            +        """
                     pat = text.get("sel.first", "sel.last")
                     if pat:
                         self.engine.setcookedpat(pat)
            @@ -75,13 +135,16 @@ def find_selection(self, text):
             def _search_dialog(parent):  # htest #
                 "Display search test box."
                 from tkinter import Toplevel, Text
            -    from tkinter.ttk import Button
            +    from tkinter.ttk import Frame, Button
             
            -    box = Toplevel(parent)
            -    box.title("Test SearchDialog")
            +    top = Toplevel(parent)
            +    top.title("Test SearchDialog")
                 x, y = map(int, parent.geometry().split('+')[1:])
            -    box.geometry("+%d+%d" % (x, y + 175))
            -    text = Text(box, inactiveselectbackground='gray')
            +    top.geometry("+%d+%d" % (x, y + 175))
            +
            +    frame = Frame(top)
            +    frame.pack()
            +    text = Text(frame, inactiveselectbackground='gray')
                 text.pack()
                 text.insert("insert","This is a sample string.\n"*5)
             
            @@ -90,13 +153,12 @@ def show_find():
                     _setup(text).open(text)
                     text.tag_remove('sel', '1.0', 'end')
             
            -    button = Button(box, text="Search (selection ignored)", command=show_find)
            +    button = Button(frame, text="Search (selection ignored)", command=show_find)
                 button.pack()
             
             if __name__ == '__main__':
            -    import unittest
            -    unittest.main('idlelib.idle_test.test_search',
            -                  verbosity=2, exit=False)
            +    from unittest import main
            +    main('idlelib.idle_test.test_search', verbosity=2, exit=False)
             
                 from idlelib.idle_test.htest import run
                 run(_search_dialog)
            diff --git a/Lib/idlelib/searchbase.py b/Lib/idlelib/searchbase.py
            index 5f81785b712c08..6fba0b8e583f2b 100644
            --- a/Lib/idlelib/searchbase.py
            +++ b/Lib/idlelib/searchbase.py
            @@ -1,7 +1,7 @@
             '''Define SearchDialogBase used by Search, Replace, and Grep dialogs.'''
             
            -from tkinter import Toplevel, Frame
            -from tkinter.ttk import Entry, Label, Button, Checkbutton, Radiobutton
            +from tkinter import Toplevel
            +from tkinter.ttk import Frame, Entry, Label, Button, Checkbutton, Radiobutton
             
             
             class SearchDialogBase:
            @@ -36,12 +36,13 @@ def __init__(self, root, engine):
                     text (Text searched): set in open(), only used in subclasses().
                     ent (ry): created in make_entry() called from create_entry().
                     row (of grid): 0 in create_widgets(), +1 in make_entry/frame().
            -        default_command: set in subclasses, used in create_widgers().
            +        default_command: set in subclasses, used in create_widgets().
             
                     title (of dialog): class attribute, override in subclasses.
                     icon (of dialog): ditto, use unclear if cannot minimize dialog.
                     '''
                     self.root = root
            +        self.bell = root.bell
                     self.engine = engine
                     self.top = None
             
            @@ -53,6 +54,7 @@ def open(self, text, searchphrase=None):
                     else:
                         self.top.deiconify()
                         self.top.tkraise()
            +        self.top.transient(text.winfo_toplevel())
                     if searchphrase:
                         self.ent.delete(0,"end")
                         self.ent.insert("end",searchphrase)
            @@ -65,6 +67,7 @@ def close(self, event=None):
                     "Put dialog away for later use."
                     if self.top:
                         self.top.grab_release()
            +            self.top.transient('')
                         self.top.withdraw()
             
                 def create_widgets(self):
            @@ -80,7 +83,6 @@ def create_widgets(self):
                     top.wm_title(self.title)
                     top.wm_iconname(self.icon)
                     self.top = top
            -        self.bell = top.bell
             
                     self.row = 0
                     self.top.grid_columnconfigure(0, pad=2, weight=0)
            @@ -172,7 +174,7 @@ def create_command_buttons(self):
                     f = self.buttonframe = Frame(self.top)
                     f.grid(row=0,column=2,padx=2,pady=2,ipadx=2,ipady=2)
             
            -        b = self.make_button("close", self.close)
            +        b = self.make_button("Close", self.close)
                     b.lower()
             
             
            @@ -192,9 +194,10 @@ def __init__(self, parent):
             
                 def default_command(self, dummy): pass
             
            +
             if __name__ == '__main__':
            -    import unittest
            -    unittest.main('idlelib.idle_test.test_searchbase', verbosity=2, exit=False)
            +    from unittest import main
            +    main('idlelib.idle_test.test_searchbase', verbosity=2, exit=False)
             
                 from idlelib.idle_test.htest import run
                 run(_searchbase)
            diff --git a/Lib/idlelib/searchengine.py b/Lib/idlelib/searchengine.py
            index 253f1b0831a619..911e7d4691cac1 100644
            --- a/Lib/idlelib/searchengine.py
            +++ b/Lib/idlelib/searchengine.py
            @@ -231,6 +231,7 @@ def get_line_col(index):
                 line, col = map(int, index.split(".")) # Fails on invalid index
                 return line, col
             
            +
             if __name__ == "__main__":
            -    import unittest
            -    unittest.main('idlelib.idle_test.test_searchengine', verbosity=2, exit=False)
            +    from unittest import main
            +    main('idlelib.idle_test.test_searchengine', verbosity=2)
            diff --git a/Lib/idlelib/sidebar.py b/Lib/idlelib/sidebar.py
            new file mode 100644
            index 00000000000000..41c09684a20251
            --- /dev/null
            +++ b/Lib/idlelib/sidebar.py
            @@ -0,0 +1,341 @@
            +"""Line numbering implementation for IDLE as an extension.
            +Includes BaseSideBar which can be extended for other sidebar based extensions
            +"""
            +import functools
            +import itertools
            +
            +import tkinter as tk
            +from idlelib.config import idleConf
            +from idlelib.delegator import Delegator
            +
            +
            +def get_end_linenumber(text):
            +    """Utility to get the last line's number in a Tk text widget."""
            +    return int(float(text.index('end-1c')))
            +
            +
            +def get_widget_padding(widget):
            +    """Get the total padding of a Tk widget, including its border."""
            +    # TODO: use also in codecontext.py
            +    manager = widget.winfo_manager()
            +    if manager == 'pack':
            +        info = widget.pack_info()
            +    elif manager == 'grid':
            +        info = widget.grid_info()
            +    else:
            +        raise ValueError(f"Unsupported geometry manager: {manager}")
            +
            +    # All values are passed through getint(), since some
            +    # values may be pixel objects, which can't simply be added to ints.
            +    padx = sum(map(widget.tk.getint, [
            +        info['padx'],
            +        widget.cget('padx'),
            +        widget.cget('border'),
            +    ]))
            +    pady = sum(map(widget.tk.getint, [
            +        info['pady'],
            +        widget.cget('pady'),
            +        widget.cget('border'),
            +    ]))
            +    return padx, pady
            +
            +
            +class BaseSideBar:
            +    """
            +    The base class for extensions which require a sidebar.
            +    """
            +    def __init__(self, editwin):
            +        self.editwin = editwin
            +        self.parent = editwin.text_frame
            +        self.text = editwin.text
            +
            +        _padx, pady = get_widget_padding(self.text)
            +        self.sidebar_text = tk.Text(self.parent, width=1, wrap=tk.NONE,
            +                                    padx=2, pady=pady,
            +                                    borderwidth=0, highlightthickness=0)
            +        self.sidebar_text.config(state=tk.DISABLED)
            +        self.text['yscrollcommand'] = self.redirect_yscroll_event
            +        self.update_font()
            +        self.update_colors()
            +
            +        self.is_shown = False
            +
            +    def update_font(self):
            +        """Update the sidebar text font, usually after config changes."""
            +        font = idleConf.GetFont(self.text, 'main', 'EditorWindow')
            +        self._update_font(font)
            +
            +    def _update_font(self, font):
            +        self.sidebar_text['font'] = font
            +
            +    def update_colors(self):
            +        """Update the sidebar text colors, usually after config changes."""
            +        colors = idleConf.GetHighlight(idleConf.CurrentTheme(), 'normal')
            +        self._update_colors(foreground=colors['foreground'],
            +                            background=colors['background'])
            +
            +    def _update_colors(self, foreground, background):
            +        self.sidebar_text.config(
            +            fg=foreground, bg=background,
            +            selectforeground=foreground, selectbackground=background,
            +            inactiveselectbackground=background,
            +        )
            +
            +    def show_sidebar(self):
            +        if not self.is_shown:
            +            self.sidebar_text.grid(row=1, column=0, sticky=tk.NSEW)
            +            self.is_shown = True
            +
            +    def hide_sidebar(self):
            +        if self.is_shown:
            +            self.sidebar_text.grid_forget()
            +            self.is_shown = False
            +
            +    def redirect_yscroll_event(self, *args, **kwargs):
            +        """Redirect vertical scrolling to the main editor text widget.
            +
            +        The scroll bar is also updated.
            +        """
            +        self.editwin.vbar.set(*args)
            +        self.sidebar_text.yview_moveto(args[0])
            +        return 'break'
            +
            +    def redirect_focusin_event(self, event):
            +        """Redirect focus-in events to the main editor text widget."""
            +        self.text.focus_set()
            +        return 'break'
            +
            +    def redirect_mousebutton_event(self, event, event_name):
            +        """Redirect mouse button events to the main editor text widget."""
            +        self.text.focus_set()
            +        self.text.event_generate(event_name, x=0, y=event.y)
            +        return 'break'
            +
            +    def redirect_mousewheel_event(self, event):
            +        """Redirect mouse wheel events to the editwin text widget."""
            +        self.text.event_generate('',
            +                                 x=0, y=event.y, delta=event.delta)
            +        return 'break'
            +
            +
            +class EndLineDelegator(Delegator):
            +    """Generate callbacks with the current end line number after
            +       insert or delete operations"""
            +    def __init__(self, changed_callback):
            +        """
            +        changed_callback - Callable, will be called after insert
            +                           or delete operations with the current
            +                           end line number.
            +        """
            +        Delegator.__init__(self)
            +        self.changed_callback = changed_callback
            +
            +    def insert(self, index, chars, tags=None):
            +        self.delegate.insert(index, chars, tags)
            +        self.changed_callback(get_end_linenumber(self.delegate))
            +
            +    def delete(self, index1, index2=None):
            +        self.delegate.delete(index1, index2)
            +        self.changed_callback(get_end_linenumber(self.delegate))
            +
            +
            +class LineNumbers(BaseSideBar):
            +    """Line numbers support for editor windows."""
            +    def __init__(self, editwin):
            +        BaseSideBar.__init__(self, editwin)
            +        self.prev_end = 1
            +        self._sidebar_width_type = type(self.sidebar_text['width'])
            +        self.sidebar_text.config(state=tk.NORMAL)
            +        self.sidebar_text.insert('insert', '1', 'linenumber')
            +        self.sidebar_text.config(state=tk.DISABLED)
            +        self.sidebar_text.config(takefocus=False, exportselection=False)
            +        self.sidebar_text.tag_config('linenumber', justify=tk.RIGHT)
            +
            +        self.bind_events()
            +
            +        end = get_end_linenumber(self.text)
            +        self.update_sidebar_text(end)
            +
            +        end_line_delegator = EndLineDelegator(self.update_sidebar_text)
            +        # Insert the delegator after the undo delegator, so that line numbers
            +        # are properly updated after undo and redo actions.
            +        end_line_delegator.setdelegate(self.editwin.undo.delegate)
            +        self.editwin.undo.setdelegate(end_line_delegator)
            +        # Reset the delegator caches of the delegators "above" the
            +        # end line delegator we just inserted.
            +        delegator = self.editwin.per.top
            +        while delegator is not end_line_delegator:
            +            delegator.resetcache()
            +            delegator = delegator.delegate
            +
            +        self.is_shown = False
            +
            +    def bind_events(self):
            +        # Ensure focus is always redirected to the main editor text widget.
            +        self.sidebar_text.bind('', self.redirect_focusin_event)
            +
            +        # Redirect mouse scrolling to the main editor text widget.
            +        #
            +        # Note that without this, scrolling with the mouse only scrolls
            +        # the line numbers.
            +        self.sidebar_text.bind('', self.redirect_mousewheel_event)
            +
            +        # Redirect mouse button events to the main editor text widget,
            +        # except for the left mouse button (1).
            +        #
            +        # Note: X-11 sends Button-4 and Button-5 events for the scroll wheel.
            +        def bind_mouse_event(event_name, target_event_name):
            +            handler = functools.partial(self.redirect_mousebutton_event,
            +                                        event_name=target_event_name)
            +            self.sidebar_text.bind(event_name, handler)
            +
            +        for button in [2, 3, 4, 5]:
            +            for event_name in (f'',
            +                               f'',
            +                               f'',
            +                               ):
            +                bind_mouse_event(event_name, target_event_name=event_name)
            +
            +            # Convert double- and triple-click events to normal click events,
            +            # since event_generate() doesn't allow generating such events.
            +            for event_name in (f'',
            +                               f'',
            +                               ):
            +                bind_mouse_event(event_name,
            +                                 target_event_name=f'')
            +
            +        # This is set by b1_mousedown_handler() and read by
            +        # drag_update_selection_and_insert_mark(), to know where dragging
            +        # began.
            +        start_line = None
            +        # These are set by b1_motion_handler() and read by selection_handler().
            +        # last_y is passed this way since the mouse Y-coordinate is not
            +        # available on selection event objects.  last_yview is passed this way
            +        # to recognize scrolling while the mouse isn't moving.
            +        last_y = last_yview = None
            +
            +        def b1_mousedown_handler(event):
            +            # select the entire line
            +            lineno = int(float(self.sidebar_text.index(f"@0,{event.y}")))
            +            self.text.tag_remove("sel", "1.0", "end")
            +            self.text.tag_add("sel", f"{lineno}.0", f"{lineno+1}.0")
            +            self.text.mark_set("insert", f"{lineno+1}.0")
            +
            +            # remember this line in case this is the beginning of dragging
            +            nonlocal start_line
            +            start_line = lineno
            +        self.sidebar_text.bind('', b1_mousedown_handler)
            +
            +        def b1_mouseup_handler(event):
            +            # On mouse up, we're no longer dragging.  Set the shared persistent
            +            # variables to None to represent this.
            +            nonlocal start_line
            +            nonlocal last_y
            +            nonlocal last_yview
            +            start_line = None
            +            last_y = None
            +            last_yview = None
            +        self.sidebar_text.bind('', b1_mouseup_handler)
            +
            +        def drag_update_selection_and_insert_mark(y_coord):
            +            """Helper function for drag and selection event handlers."""
            +            lineno = int(float(self.sidebar_text.index(f"@0,{y_coord}")))
            +            a, b = sorted([start_line, lineno])
            +            self.text.tag_remove("sel", "1.0", "end")
            +            self.text.tag_add("sel", f"{a}.0", f"{b+1}.0")
            +            self.text.mark_set("insert",
            +                               f"{lineno if lineno == a else lineno + 1}.0")
            +
            +        # Special handling of dragging with mouse button 1.  In "normal" text
            +        # widgets this selects text, but the line numbers text widget has
            +        # selection disabled.  Still, dragging triggers some selection-related
            +        # functionality under the hood.  Specifically, dragging to above or
            +        # below the text widget triggers scrolling, in a way that bypasses the
            +        # other scrolling synchronization mechanisms.i
            +        def b1_drag_handler(event, *args):
            +            nonlocal last_y
            +            nonlocal last_yview
            +            last_y = event.y
            +            last_yview = self.sidebar_text.yview()
            +            if not 0 <= last_y <= self.sidebar_text.winfo_height():
            +                self.text.yview_moveto(last_yview[0])
            +            drag_update_selection_and_insert_mark(event.y)
            +        self.sidebar_text.bind('', b1_drag_handler)
            +
            +        # With mouse-drag scrolling fixed by the above, there is still an edge-
            +        # case we need to handle: When drag-scrolling, scrolling can continue
            +        # while the mouse isn't moving, leading to the above fix not scrolling
            +        # properly.
            +        def selection_handler(event):
            +            if last_yview is None:
            +                # This logic is only needed while dragging.
            +                return
            +            yview = self.sidebar_text.yview()
            +            if yview != last_yview:
            +                self.text.yview_moveto(yview[0])
            +                drag_update_selection_and_insert_mark(last_y)
            +        self.sidebar_text.bind('<>', selection_handler)
            +
            +    def update_colors(self):
            +        """Update the sidebar text colors, usually after config changes."""
            +        colors = idleConf.GetHighlight(idleConf.CurrentTheme(), 'linenumber')
            +        self._update_colors(foreground=colors['foreground'],
            +                            background=colors['background'])
            +
            +    def update_sidebar_text(self, end):
            +        """
            +        Perform the following action:
            +        Each line sidebar_text contains the linenumber for that line
            +        Synchronize with editwin.text so that both sidebar_text and
            +        editwin.text contain the same number of lines"""
            +        if end == self.prev_end:
            +            return
            +
            +        width_difference = len(str(end)) - len(str(self.prev_end))
            +        if width_difference:
            +            cur_width = int(float(self.sidebar_text['width']))
            +            new_width = cur_width + width_difference
            +            self.sidebar_text['width'] = self._sidebar_width_type(new_width)
            +
            +        self.sidebar_text.config(state=tk.NORMAL)
            +        if end > self.prev_end:
            +            new_text = '\n'.join(itertools.chain(
            +                [''],
            +                map(str, range(self.prev_end + 1, end + 1)),
            +            ))
            +            self.sidebar_text.insert(f'end -1c', new_text, 'linenumber')
            +        else:
            +            self.sidebar_text.delete(f'{end+1}.0 -1c', 'end -1c')
            +        self.sidebar_text.config(state=tk.DISABLED)
            +
            +        self.prev_end = end
            +
            +
            +def _linenumbers_drag_scrolling(parent):  # htest #
            +    from idlelib.idle_test.test_sidebar import Dummy_editwin
            +
            +    toplevel = tk.Toplevel(parent)
            +    text_frame = tk.Frame(toplevel)
            +    text_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
            +    text_frame.rowconfigure(1, weight=1)
            +    text_frame.columnconfigure(1, weight=1)
            +
            +    font = idleConf.GetFont(toplevel, 'main', 'EditorWindow')
            +    text = tk.Text(text_frame, width=80, height=24, wrap=tk.NONE, font=font)
            +    text.grid(row=1, column=1, sticky=tk.NSEW)
            +
            +    editwin = Dummy_editwin(text)
            +    editwin.vbar = tk.Scrollbar(text_frame)
            +
            +    linenumbers = LineNumbers(editwin)
            +    linenumbers.show_sidebar()
            +
            +    text.insert('1.0', '\n'.join('a'*i for i in range(1, 101)))
            +
            +
            +if __name__ == '__main__':
            +    from unittest import main
            +    main('idlelib.idle_test.test_sidebar', verbosity=2, exit=False)
            +
            +    from idlelib.idle_test.htest import run
            +    run(_linenumbers_drag_scrolling)
            diff --git a/Lib/idlelib/squeezer.py b/Lib/idlelib/squeezer.py
            new file mode 100644
            index 00000000000000..be1538a25fdedf
            --- /dev/null
            +++ b/Lib/idlelib/squeezer.py
            @@ -0,0 +1,345 @@
            +"""An IDLE extension to avoid having very long texts printed in the shell.
            +
            +A common problem in IDLE's interactive shell is printing of large amounts of
            +text into the shell. This makes looking at the previous history difficult.
            +Worse, this can cause IDLE to become very slow, even to the point of being
            +completely unusable.
            +
            +This extension will automatically replace long texts with a small button.
            +Double-clicking this button will remove it and insert the original text instead.
            +Middle-clicking will copy the text to the clipboard. Right-clicking will open
            +the text in a separate viewing window.
            +
            +Additionally, any output can be manually "squeezed" by the user. This includes
            +output written to the standard error stream ("stderr"), such as exception
            +messages and their tracebacks.
            +"""
            +import re
            +
            +import tkinter as tk
            +import tkinter.messagebox as tkMessageBox
            +
            +from idlelib.config import idleConf
            +from idlelib.textview import view_text
            +from idlelib.tooltip import Hovertip
            +from idlelib import macosx
            +
            +
            +def count_lines_with_wrapping(s, linewidth=80):
            +    """Count the number of lines in a given string.
            +
            +    Lines are counted as if the string was wrapped so that lines are never over
            +    linewidth characters long.
            +
            +    Tabs are considered tabwidth characters long.
            +    """
            +    tabwidth = 8  # Currently always true in Shell.
            +    pos = 0
            +    linecount = 1
            +    current_column = 0
            +
            +    for m in re.finditer(r"[\t\n]", s):
            +        # Process the normal chars up to tab or newline.
            +        numchars = m.start() - pos
            +        pos += numchars
            +        current_column += numchars
            +
            +        # Deal with tab or newline.
            +        if s[pos] == '\n':
            +            # Avoid the `current_column == 0` edge-case, and while we're
            +            # at it, don't bother adding 0.
            +            if current_column > linewidth:
            +                # If the current column was exactly linewidth, divmod
            +                # would give (1,0), even though a new line hadn't yet
            +                # been started. The same is true if length is any exact
            +                # multiple of linewidth. Therefore, subtract 1 before
            +                # dividing a non-empty line.
            +                linecount += (current_column - 1) // linewidth
            +            linecount += 1
            +            current_column = 0
            +        else:
            +            assert s[pos] == '\t'
            +            current_column += tabwidth - (current_column % tabwidth)
            +
            +            # If a tab passes the end of the line, consider the entire
            +            # tab as being on the next line.
            +            if current_column > linewidth:
            +                linecount += 1
            +                current_column = tabwidth
            +
            +        pos += 1 # After the tab or newline.
            +
            +    # Process remaining chars (no more tabs or newlines).
            +    current_column += len(s) - pos
            +    # Avoid divmod(-1, linewidth).
            +    if current_column > 0:
            +        linecount += (current_column - 1) // linewidth
            +    else:
            +        # Text ended with newline; don't count an extra line after it.
            +        linecount -= 1
            +
            +    return linecount
            +
            +
            +class ExpandingButton(tk.Button):
            +    """Class for the "squeezed" text buttons used by Squeezer
            +
            +    These buttons are displayed inside a Tk Text widget in place of text. A
            +    user can then use the button to replace it with the original text, copy
            +    the original text to the clipboard or view the original text in a separate
            +    window.
            +
            +    Each button is tied to a Squeezer instance, and it knows to update the
            +    Squeezer instance when it is expanded (and therefore removed).
            +    """
            +    def __init__(self, s, tags, numoflines, squeezer):
            +        self.s = s
            +        self.tags = tags
            +        self.numoflines = numoflines
            +        self.squeezer = squeezer
            +        self.editwin = editwin = squeezer.editwin
            +        self.text = text = editwin.text
            +        # The base Text widget is needed to change text before iomark.
            +        self.base_text = editwin.per.bottom
            +
            +        line_plurality = "lines" if numoflines != 1 else "line"
            +        button_text = f"Squeezed text ({numoflines} {line_plurality})."
            +        tk.Button.__init__(self, text, text=button_text,
            +                           background="#FFFFC0", activebackground="#FFFFE0")
            +
            +        button_tooltip_text = (
            +            "Double-click to expand, right-click for more options."
            +        )
            +        Hovertip(self, button_tooltip_text, hover_delay=80)
            +
            +        self.bind("", self.expand)
            +        if macosx.isAquaTk():
            +            # AquaTk defines <2> as the right button, not <3>.
            +            self.bind("", self.context_menu_event)
            +        else:
            +            self.bind("", self.context_menu_event)
            +        self.selection_handle(  # X windows only.
            +            lambda offset, length: s[int(offset):int(offset) + int(length)])
            +
            +        self.is_dangerous = None
            +        self.after_idle(self.set_is_dangerous)
            +
            +    def set_is_dangerous(self):
            +        dangerous_line_len = 50 * self.text.winfo_width()
            +        self.is_dangerous = (
            +            self.numoflines > 1000 or
            +            len(self.s) > 50000 or
            +            any(
            +                len(line_match.group(0)) >= dangerous_line_len
            +                for line_match in re.finditer(r'[^\n]+', self.s)
            +            )
            +        )
            +
            +    def expand(self, event=None):
            +        """expand event handler
            +
            +        This inserts the original text in place of the button in the Text
            +        widget, removes the button and updates the Squeezer instance.
            +
            +        If the original text is dangerously long, i.e. expanding it could
            +        cause a performance degradation, ask the user for confirmation.
            +        """
            +        if self.is_dangerous is None:
            +            self.set_is_dangerous()
            +        if self.is_dangerous:
            +            confirm = tkMessageBox.askokcancel(
            +                title="Expand huge output?",
            +                message="\n\n".join([
            +                    "The squeezed output is very long: %d lines, %d chars.",
            +                    "Expanding it could make IDLE slow or unresponsive.",
            +                    "It is recommended to view or copy the output instead.",
            +                    "Really expand?"
            +                ]) % (self.numoflines, len(self.s)),
            +                default=tkMessageBox.CANCEL,
            +                parent=self.text)
            +            if not confirm:
            +                return "break"
            +
            +        self.base_text.insert(self.text.index(self), self.s, self.tags)
            +        self.base_text.delete(self)
            +        self.squeezer.expandingbuttons.remove(self)
            +
            +    def copy(self, event=None):
            +        """copy event handler
            +
            +        Copy the original text to the clipboard.
            +        """
            +        self.clipboard_clear()
            +        self.clipboard_append(self.s)
            +
            +    def view(self, event=None):
            +        """view event handler
            +
            +        View the original text in a separate text viewer window.
            +        """
            +        view_text(self.text, "Squeezed Output Viewer", self.s,
            +                  modal=False, wrap='none')
            +
            +    rmenu_specs = (
            +        # Item structure: (label, method_name).
            +        ('copy', 'copy'),
            +        ('view', 'view'),
            +    )
            +
            +    def context_menu_event(self, event):
            +        self.text.mark_set("insert", "@%d,%d" % (event.x, event.y))
            +        rmenu = tk.Menu(self.text, tearoff=0)
            +        for label, method_name in self.rmenu_specs:
            +            rmenu.add_command(label=label, command=getattr(self, method_name))
            +        rmenu.tk_popup(event.x_root, event.y_root)
            +        return "break"
            +
            +
            +class Squeezer:
            +    """Replace long outputs in the shell with a simple button.
            +
            +    This avoids IDLE's shell slowing down considerably, and even becoming
            +    completely unresponsive, when very long outputs are written.
            +    """
            +    @classmethod
            +    def reload(cls):
            +        """Load class variables from config."""
            +        cls.auto_squeeze_min_lines = idleConf.GetOption(
            +            "main", "PyShell", "auto-squeeze-min-lines",
            +            type="int", default=50,
            +        )
            +
            +    def __init__(self, editwin):
            +        """Initialize settings for Squeezer.
            +
            +        editwin is the shell's Editor window.
            +        self.text is the editor window text widget.
            +        self.base_test is the actual editor window Tk text widget, rather than
            +            EditorWindow's wrapper.
            +        self.expandingbuttons is the list of all buttons representing
            +            "squeezed" output.
            +        """
            +        self.editwin = editwin
            +        self.text = text = editwin.text
            +
            +        # Get the base Text widget of the PyShell object, used to change
            +        # text before the iomark. PyShell deliberately disables changing
            +        # text before the iomark via its 'text' attribute, which is
            +        # actually a wrapper for the actual Text widget. Squeezer,
            +        # however, needs to make such changes.
            +        self.base_text = editwin.per.bottom
            +
            +        # Twice the text widget's border width and internal padding;
            +        # pre-calculated here for the get_line_width() method.
            +        self.window_width_delta = 2 * (
            +            int(text.cget('border')) +
            +            int(text.cget('padx'))
            +        )
            +
            +        self.expandingbuttons = []
            +
            +        # Replace the PyShell instance's write method with a wrapper,
            +        # which inserts an ExpandingButton instead of a long text.
            +        def mywrite(s, tags=(), write=editwin.write):
            +            # Only auto-squeeze text which has just the "stdout" tag.
            +            if tags != "stdout":
            +                return write(s, tags)
            +
            +            # Only auto-squeeze text with at least the minimum
            +            # configured number of lines.
            +            auto_squeeze_min_lines = self.auto_squeeze_min_lines
            +            # First, a very quick check to skip very short texts.
            +            if len(s) < auto_squeeze_min_lines:
            +                return write(s, tags)
            +            # Now the full line-count check.
            +            numoflines = self.count_lines(s)
            +            if numoflines < auto_squeeze_min_lines:
            +                return write(s, tags)
            +
            +            # Create an ExpandingButton instance.
            +            expandingbutton = ExpandingButton(s, tags, numoflines, self)
            +
            +            # Insert the ExpandingButton into the Text widget.
            +            text.mark_gravity("iomark", tk.RIGHT)
            +            text.window_create("iomark", window=expandingbutton,
            +                               padx=3, pady=5)
            +            text.see("iomark")
            +            text.update()
            +            text.mark_gravity("iomark", tk.LEFT)
            +
            +            # Add the ExpandingButton to the Squeezer's list.
            +            self.expandingbuttons.append(expandingbutton)
            +
            +        editwin.write = mywrite
            +
            +    def count_lines(self, s):
            +        """Count the number of lines in a given text.
            +
            +        Before calculation, the tab width and line length of the text are
            +        fetched, so that up-to-date values are used.
            +
            +        Lines are counted as if the string was wrapped so that lines are never
            +        over linewidth characters long.
            +
            +        Tabs are considered tabwidth characters long.
            +        """
            +        return count_lines_with_wrapping(s, self.editwin.width)
            +
            +    def squeeze_current_text_event(self, event):
            +        """squeeze-current-text event handler
            +
            +        Squeeze the block of text inside which contains the "insert" cursor.
            +
            +        If the insert cursor is not in a squeezable block of text, give the
            +        user a small warning and do nothing.
            +        """
            +        # Set tag_name to the first valid tag found on the "insert" cursor.
            +        tag_names = self.text.tag_names(tk.INSERT)
            +        for tag_name in ("stdout", "stderr"):
            +            if tag_name in tag_names:
            +                break
            +        else:
            +            # The insert cursor doesn't have a "stdout" or "stderr" tag.
            +            self.text.bell()
            +            return "break"
            +
            +        # Find the range to squeeze.
            +        start, end = self.text.tag_prevrange(tag_name, tk.INSERT + "+1c")
            +        s = self.text.get(start, end)
            +
            +        # If the last char is a newline, remove it from the range.
            +        if len(s) > 0 and s[-1] == '\n':
            +            end = self.text.index("%s-1c" % end)
            +            s = s[:-1]
            +
            +        # Delete the text.
            +        self.base_text.delete(start, end)
            +
            +        # Prepare an ExpandingButton.
            +        numoflines = self.count_lines(s)
            +        expandingbutton = ExpandingButton(s, tag_name, numoflines, self)
            +
            +        # insert the ExpandingButton to the Text
            +        self.text.window_create(start, window=expandingbutton,
            +                                padx=3, pady=5)
            +
            +        # Insert the ExpandingButton to the list of ExpandingButtons,
            +        # while keeping the list ordered according to the position of
            +        # the buttons in the Text widget.
            +        i = len(self.expandingbuttons)
            +        while i > 0 and self.text.compare(self.expandingbuttons[i-1],
            +                                          ">", expandingbutton):
            +            i -= 1
            +        self.expandingbuttons.insert(i, expandingbutton)
            +
            +        return "break"
            +
            +
            +Squeezer.reload()
            +
            +
            +if __name__ == "__main__":
            +    from unittest import main
            +    main('idlelib.idle_test.test_squeezer', verbosity=2, exit=False)
            +
            +    # Add htest.
            diff --git a/Lib/idlelib/stackviewer.py b/Lib/idlelib/stackviewer.py
            index 400fa632a098cf..94ffb4eff4dd26 100644
            --- a/Lib/idlelib/stackviewer.py
            +++ b/Lib/idlelib/stackviewer.py
            @@ -8,6 +8,7 @@
             from idlelib.tree import TreeNode, TreeItem, ScrolledCanvas
             
             def StackBrowser(root, flist=None, tb=None, top=None):
            +    global sc, item, node  # For testing.
                 if top is None:
                     top = tk.Toplevel(root)
                 sc = ScrolledCanvas(top, bg="white", highlightthickness=0)
            @@ -134,7 +135,6 @@ def _stack_viewer(parent):  # htest #
                     intentional_name_error
                 except NameError:
                     exc_type, exc_value, exc_tb = sys.exc_info()
            -
                 # inject stack trace to sys
                 sys.last_type = exc_type
                 sys.last_value = exc_value
            @@ -148,5 +148,8 @@ def _stack_viewer(parent):  # htest #
                 del sys.last_traceback
             
             if __name__ == '__main__':
            +    from unittest import main
            +    main('idlelib.idle_test.test_stackviewer', verbosity=2, exit=False)
            +
                 from idlelib.idle_test.htest import run
                 run(_stack_viewer)
            diff --git a/Lib/idlelib/statusbar.py b/Lib/idlelib/statusbar.py
            index 8618528d822130..c071f898b0f744 100644
            --- a/Lib/idlelib/statusbar.py
            +++ b/Lib/idlelib/statusbar.py
            @@ -42,5 +42,8 @@ def change():
                 frame.pack()
             
             if __name__ == '__main__':
            +    from unittest import main
            +    main('idlelib.idle_test.test_statusbar', verbosity=2, exit=False)
            +
                 from idlelib.idle_test.htest import run
                 run(_multistatus_bar)
            diff --git a/Lib/idlelib/textview.py b/Lib/idlelib/textview.py
            index e3b55065c6d9cc..a66c1a4309a617 100644
            --- a/Lib/idlelib/textview.py
            +++ b/Lib/idlelib/textview.py
            @@ -1,48 +1,97 @@
             """Simple text browser for IDLE
             
             """
            -from tkinter import Toplevel, Text
            +from tkinter import Toplevel, Text, TclError,\
            +    HORIZONTAL, VERTICAL, NS, EW, NSEW, NONE, WORD, SUNKEN
             from tkinter.ttk import Frame, Scrollbar, Button
             from tkinter.messagebox import showerror
             
            +from idlelib.colorizer import color_config
             
            -class TextFrame(Frame):
            -    "Display text with scrollbar."
             
            -    def __init__(self, parent, rawtext):
            +class AutoHideScrollbar(Scrollbar):
            +    """A scrollbar that is automatically hidden when not needed.
            +
            +    Only the grid geometry manager is supported.
            +    """
            +    def set(self, lo, hi):
            +        if float(lo) > 0.0 or float(hi) < 1.0:
            +            self.grid()
            +        else:
            +            self.grid_remove()
            +        super().set(lo, hi)
            +
            +    def pack(self, **kwargs):
            +        raise TclError(f'{self.__class__.__name__} does not support "pack"')
            +
            +    def place(self, **kwargs):
            +        raise TclError(f'{self.__class__.__name__} does not support "place"')
            +
            +
            +class ScrollableTextFrame(Frame):
            +    """Display text with scrollbar(s)."""
            +
            +    def __init__(self, master, wrap=NONE, **kwargs):
                     """Create a frame for Textview.
             
            -        parent - parent widget for this frame
            -        rawtext - text to display
            -        """
            -        super().__init__(parent)
            -        self['relief'] = 'sunken'
            -        self['height'] = 700
            -        # TODO: get fg/bg from theme.
            -        self.bg = '#ffffff'
            -        self.fg = '#000000'
            -
            -        self.text = text = Text(self, wrap='word', highlightthickness=0,
            -                                fg=self.fg, bg=self.bg)
            -        self.scroll = scroll = Scrollbar(self, orient='vertical',
            -                                         takefocus=False, command=text.yview)
            -        text['yscrollcommand'] = scroll.set
            -        text.insert(0.0, rawtext)
            -        text['state'] = 'disabled'
            -        text.focus_set()
            +        master - master widget for this frame
            +        wrap - type of text wrapping to use ('word', 'char' or 'none')
             
            -        scroll.pack(side='right', fill='y')
            -        text.pack(side='left', expand=True, fill='both')
            +        All parameters except for 'wrap' are passed to Frame.__init__().
            +
            +        The Text widget is accessible via the 'text' attribute.
            +
            +        Note: Changing the wrapping mode of the text widget after
            +        instantiation is not supported.
            +        """
            +        super().__init__(master, **kwargs)
            +
            +        text = self.text = Text(self, wrap=wrap)
            +        text.grid(row=0, column=0, sticky=NSEW)
            +        self.grid_rowconfigure(0, weight=1)
            +        self.grid_columnconfigure(0, weight=1)
            +
            +        # vertical scrollbar
            +        self.yscroll = AutoHideScrollbar(self, orient=VERTICAL,
            +                                         takefocus=False,
            +                                         command=text.yview)
            +        self.yscroll.grid(row=0, column=1, sticky=NS)
            +        text['yscrollcommand'] = self.yscroll.set
            +
            +        # horizontal scrollbar - only when wrap is set to NONE
            +        if wrap == NONE:
            +            self.xscroll = AutoHideScrollbar(self, orient=HORIZONTAL,
            +                                             takefocus=False,
            +                                             command=text.xview)
            +            self.xscroll.grid(row=1, column=0, sticky=EW)
            +            text['xscrollcommand'] = self.xscroll.set
            +        else:
            +            self.xscroll = None
             
             
             class ViewFrame(Frame):
                 "Display TextFrame and Close button."
            -    def __init__(self, parent, text):
            +    def __init__(self, parent, contents, wrap='word'):
            +        """Create a frame for viewing text with a "Close" button.
            +
            +        parent - parent widget for this frame
            +        contents - text to display
            +        wrap - type of text wrapping to use ('word', 'char' or 'none')
            +
            +        The Text widget is accessible via the 'text' attribute.
            +        """
                     super().__init__(parent)
                     self.parent = parent
                     self.bind('', self.ok)
                     self.bind('', self.ok)
            -        self.textframe = TextFrame(self, text)
            +        self.textframe = ScrollableTextFrame(self, relief=SUNKEN, height=700)
            +
            +        text = self.text = self.textframe.text
            +        text.insert('1.0', contents)
            +        text.configure(wrap=wrap, highlightthickness=0, state='disabled')
            +        color_config(text)
            +        text.focus_set()
            +
                     self.button_ok = button_ok = Button(
                             self, text='Close', command=self.ok, takefocus=False)
                     self.textframe.pack(side='top', expand=True, fill='both')
            @@ -56,7 +105,7 @@ def ok(self, event=None):
             class ViewWindow(Toplevel):
                 "A simple text viewer dialog for IDLE."
             
            -    def __init__(self, parent, title, text, modal=True,
            +    def __init__(self, parent, title, contents, modal=True, wrap=WORD,
                              *, _htest=False, _utest=False):
                     """Show the given text in a scrollable window with a 'close' button.
             
            @@ -65,7 +114,8 @@ def __init__(self, parent, title, text, modal=True,
             
                     parent - parent of this dialog
                     title - string which is title of popup dialog
            -        text - text to display in dialog
            +        contents - text to display in dialog
            +        wrap - type of text wrapping to use ('word', 'char' or 'none')
                     _htest - bool; change box location when running htest.
                     _utest - bool; don't wait_window when running unittest.
                     """
            @@ -77,13 +127,14 @@ def __init__(self, parent, title, text, modal=True,
                     self.geometry(f'=750x500+{x}+{y}')
             
                     self.title(title)
            -        self.viewframe = ViewFrame(self, text)
            +        self.viewframe = ViewFrame(self, contents, wrap=wrap)
                     self.protocol("WM_DELETE_WINDOW", self.ok)
                     self.button_ok = button_ok = Button(self, text='Close',
                                                         command=self.ok, takefocus=False)
                     self.viewframe.pack(side='top', expand=True, fill='both')
             
            -        if modal:
            +        self.is_modal = modal
            +        if self.is_modal:
                         self.transient(parent)
                         self.grab_set()
                         if not _utest:
            @@ -91,23 +142,27 @@ def __init__(self, parent, title, text, modal=True,
             
                 def ok(self, event=None):
                     """Dismiss text viewer dialog."""
            +        if self.is_modal:
            +            self.grab_release()
                     self.destroy()
             
             
            -def view_text(parent, title, text, modal=True, _utest=False):
            +def view_text(parent, title, contents, modal=True, wrap='word', _utest=False):
                 """Create text viewer for given text.
             
                 parent - parent of this dialog
                 title - string which is the title of popup dialog
            -    text - text to display in this dialog
            +    contents - text to display in this dialog
            +    wrap - type of text wrapping to use ('word', 'char' or 'none')
                 modal - controls if users can interact with other windows while this
                         dialog is displayed
                 _utest - bool; controls wait_window on unittest
                 """
            -    return ViewWindow(parent, title, text, modal, _utest=_utest)
            +    return ViewWindow(parent, title, contents, modal, wrap=wrap, _utest=_utest)
             
             
            -def view_file(parent, title, filename, encoding=None, modal=True, _utest=False):
            +def view_file(parent, title, filename, encoding, modal=True, wrap='word',
            +              _utest=False):
                 """Create text viewer for text in filename.
             
                 Return error message if file cannot be read.  Otherwise calls view_text
            @@ -125,12 +180,14 @@ def view_file(parent, title, filename, encoding=None, modal=True, _utest=False):
                               message=str(err),
                               parent=parent)
                 else:
            -        return view_text(parent, title, contents, modal, _utest=_utest)
            +        return view_text(parent, title, contents, modal, wrap=wrap,
            +                         _utest=_utest)
                 return None
             
             
             if __name__ == '__main__':
            -    import unittest
            -    unittest.main('idlelib.idle_test.test_textview', verbosity=2, exit=False)
            +    from unittest import main
            +    main('idlelib.idle_test.test_textview', verbosity=2, exit=False)
            +
                 from idlelib.idle_test.htest import run
                 run(ViewWindow)
            diff --git a/Lib/idlelib/tooltip.py b/Lib/idlelib/tooltip.py
            index 843fb4a7d0b741..69658264dbd4a4 100644
            --- a/Lib/idlelib/tooltip.py
            +++ b/Lib/idlelib/tooltip.py
            @@ -1,80 +1,167 @@
            -# general purpose 'tooltip' routines - currently unused in idlelib
            -# (although the 'calltips' extension is partly based on this code)
            -# may be useful for some purposes in (or almost in ;) the current project scope
            -# Ideas gleaned from PySol
            +"""Tools for displaying tool-tips.
             
            +This includes:
            + * an abstract base-class for different kinds of tooltips
            + * a simple text-only Tooltip class
            +"""
             from tkinter import *
             
            -class ToolTipBase:
             
            -    def __init__(self, button):
            -        self.button = button
            -        self.tipwindow = None
            -        self.id = None
            -        self.x = self.y = 0
            -        self._id1 = self.button.bind("", self.enter)
            -        self._id2 = self.button.bind("", self.leave)
            -        self._id3 = self.button.bind("", self.leave)
            +class TooltipBase(object):
            +    """abstract base class for tooltips"""
             
            -    def enter(self, event=None):
            -        self.schedule()
            +    def __init__(self, anchor_widget):
            +        """Create a tooltip.
             
            -    def leave(self, event=None):
            -        self.unschedule()
            -        self.hidetip()
            +        anchor_widget: the widget next to which the tooltip will be shown
             
            -    def schedule(self):
            -        self.unschedule()
            -        self.id = self.button.after(1500, self.showtip)
            +        Note that a widget will only be shown when showtip() is called.
            +        """
            +        self.anchor_widget = anchor_widget
            +        self.tipwindow = None
             
            -    def unschedule(self):
            -        id = self.id
            -        self.id = None
            -        if id:
            -            self.button.after_cancel(id)
            +    def __del__(self):
            +        self.hidetip()
             
                 def showtip(self):
            +        """display the tooltip"""
                     if self.tipwindow:
                         return
            -        # The tip window must be completely outside the button;
            +        self.tipwindow = tw = Toplevel(self.anchor_widget)
            +        # show no border on the top level window
            +        tw.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.
            +            tw.tk.call("::tk::unsupported::MacWindowStyle", "style", tw._w,
            +                       "help", "noActivates")
            +        except TclError:
            +            pass
            +
            +        self.position_window()
            +        self.showcontents()
            +        self.tipwindow.update_idletasks()  # Needed on MacOS -- see #34275.
            +        self.tipwindow.lift()  # work around bug in Tk 8.5.18+ (issue #24570)
            +
            +    def position_window(self):
            +        """(re)-set the tooltip's screen position"""
            +        x, y = self.get_position()
            +        root_x = self.anchor_widget.winfo_rootx() + x
            +        root_y = self.anchor_widget.winfo_rooty() + y
            +        self.tipwindow.wm_geometry("+%d+%d" % (root_x, root_y))
            +
            +    def get_position(self):
            +        """choose a screen position for the tooltip"""
            +        # The tip window must be completely outside the anchor widget;
                     # otherwise when the mouse enters the tip window we get
                     # a leave event and it disappears, and then we get an enter
                     # event and it reappears, and so on forever :-(
            -        x = self.button.winfo_rootx() + 20
            -        y = self.button.winfo_rooty() + self.button.winfo_height() + 1
            -        self.tipwindow = tw = Toplevel(self.button)
            -        tw.wm_overrideredirect(1)
            -        tw.wm_geometry("+%d+%d" % (x, y))
            -        self.showcontents()
            +        #
            +        # Note: This is a simplistic implementation; sub-classes will likely
            +        # want to override this.
            +        return 20, self.anchor_widget.winfo_height() + 1
             
            -    def showcontents(self, text="Your text here"):
            -        # Override this in derived class
            -        label = Label(self.tipwindow, text=text, justify=LEFT,
            -                      background="#ffffe0", relief=SOLID, borderwidth=1)
            -        label.pack()
            +    def showcontents(self):
            +        """content display hook for sub-classes"""
            +        # See ToolTip for an example
            +        raise NotImplementedError
             
                 def hidetip(self):
            +        """hide the tooltip"""
            +        # Note: This is called by __del__, so careful when overriding/extending
                     tw = self.tipwindow
                     self.tipwindow = None
                     if tw:
            -            tw.destroy()
            +            try:
            +                tw.destroy()
            +            except TclError:  # pragma: no cover
            +                pass
            +
            +
            +class OnHoverTooltipBase(TooltipBase):
            +    """abstract base class for tooltips, with delayed on-hover display"""
            +
            +    def __init__(self, anchor_widget, hover_delay=1000):
            +        """Create a tooltip with a mouse hover delay.
            +
            +        anchor_widget: the widget next to which the tooltip will be shown
            +        hover_delay: time to delay before showing the tooltip, in milliseconds
             
            -class ToolTip(ToolTipBase):
            -    def __init__(self, button, text):
            -        ToolTipBase.__init__(self, button)
            +        Note that a widget will only be shown when showtip() is called,
            +        e.g. after hovering over the anchor widget with the mouse for enough
            +        time.
            +        """
            +        super(OnHoverTooltipBase, self).__init__(anchor_widget)
            +        self.hover_delay = hover_delay
            +
            +        self._after_id = None
            +        self._id1 = self.anchor_widget.bind("", self._show_event)
            +        self._id2 = self.anchor_widget.bind("", self._hide_event)
            +        self._id3 = self.anchor_widget.bind("
            '), response.read()) + def test_server_title_escape(self): + # bpo-38243: Ensure that the server title and documentation + # are escaped for HTML. + self.serv.set_server_title('test_title