-

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
  • +
  • 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, @@ -111,24 +120,27 @@

    Navigation

  • 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

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

  • -
  • C-Backspace delete word left; C-Del delete word 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

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

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

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

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

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

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

    +
  • 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 +
    • 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 +
    • 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 +
    • C-p go up one line (usually you can also use the cursor key for this)
    • -
    • C-d delete next character
    • +
    • 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 +493,63 @@

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-c interrupts executing command

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

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

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

    +
  • 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 +558,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 @@ -565,46 +624,112 @@

25.5.3.2. Startup failure

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.

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 from a console (python -m idlelib) and see if a message appears.

-
-

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.

+

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.

+
+
+

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 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'
+>>> 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 +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 +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 +737,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. @@ -638,24 +763,50 @@

25.5.3.5. Running without a subprocess -

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 +822,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
              -
            • 25.5.3.1. Command line usage
            • -
            • 25.5.3.2. Startup failure
            • -
            • 25.5.3.3. IDLE-console differences
            • -
            • 25.5.3.4. Developing tkinter applications
            • -
            • 25.5.3.5. Running without a subprocess
            • +
            • Startup and code execution
            • -
            • 25.5.4. Help and preferences @@ -715,16 +871,16 @@

              Table Of Contents

              Previous topic

              25.4. tkinter.scrolledtext — Scrolled Text Widget

              + title="previous chapter">tkinter.scrolledtext — Scrolled Text Widget

              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..0603ede822badf 100644 --- a/Lib/idlelib/help.py +++ b/Lib/idlelib/help.py @@ -126,7 +126,10 @@ def handle_endtag(self, tag): if tag in ['h1', 'h2', 'h3']: self.indent(0) # clear tag, reset indent 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'))) elif tag in ['span', 'em']: self.chartags = '' elif tag == 'a': @@ -142,11 +145,15 @@ def handle_data(self, data): 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)) @@ -233,28 +240,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 proceedure 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: @@ -271,5 +278,8 @@ def show_idlehelp(parent): 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/hyperparser.py b/Lib/idlelib/hyperparser.py index 450a709c09bbfa..7e7e0ae8024754 100644 --- a/Lib/idlelib/hyperparser.py +++ b/Lib/idlelib/hyperparser.py @@ -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..429081f7ef25a8 100644 --- a/Lib/idlelib/idle_test/htest.py +++ b/Lib/idlelib/idle_test/htest.py @@ -65,6 +65,7 @@ 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 tkinter as tk from tkinter.ttk import Scrollbar @@ -79,11 +80,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 = { @@ -113,7 +117,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 +141,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 +162,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." } @@ -230,7 +233,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 +299,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': {}, @@ -341,7 +334,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_tk.py b/Lib/idlelib/idle_test/mock_tk.py index 6e351297d75db9..a54f51f1949c08 100644 --- a/Lib/idlelib/idle_test/mock_tk.py +++ b/Lib/idlelib/idle_test/mock_tk.py @@ -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..398cb359e0931f 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 87%." -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): @@ -30,17 +28,19 @@ 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): @@ -57,7 +57,7 @@ def test_remove_autocomplete_window(self): self.assertIsNone(self.autocomplete.autocompletewindow) def test_force_open_completions_event(self): - # Test that force_open_completions_event calls _open_completions + # 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') @@ -70,16 +70,16 @@ def test_try_open_completions_event(self): o_c_l = Func() autocomplete._open_completions_later = o_c_l - # _open_completions_later should not be called with no text in editor + # _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) + # _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) + # _open_completions_later should be called with COMPLETE_FILES (2). self.text.delete('1.0', 'end') self.text.insert('1.0', '"./Lib/') trycompletions('event') @@ -90,7 +90,7 @@ def test_autocomplete_event(self): autocomplete = self.autocomplete # Test that the autocomplete event is ignored if user is pressing a - # modifier key in addition to the tab key + # modifier key in addition to the tab key. ev = Event(mc_state=True) self.assertIsNone(autocomplete.autocomplete_event(ev)) del ev.mc_state @@ -100,15 +100,15 @@ def test_autocomplete_event(self): self.assertIsNone(autocomplete.autocomplete_event(ev)) self.text.delete('1.0', 'end') - # If autocomplete window is open, complete() method is called + # If autocomplete window is open, complete() method is called. self.text.insert('1.0', 're.') - # This must call autocomplete._make_autocomplete_window() + # 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 + o_cs = Func() # .result = None. autocomplete.open_completions = o_cs Equal(self.autocomplete.autocomplete_event(ev), None) Equal(o_cs.args, (False, True, True)) @@ -117,32 +117,130 @@ def test_autocomplete_event(self): Equal(o_cs.args, (False, True, True)) def test_open_completions_later(self): - # Test that autocomplete._delayed_completion_id is set - pass + # Test that autocomplete._delayed_completion_id is set. + acp = self.autocomplete + acp._delayed_completion_id = None + acp._open_completions_later(False, False, False, ac.COMPLETE_ATTRIBUTES) + cb1 = acp._delayed_completion_id + self.assertTrue(cb1.startswith('after')) + + # Test that cb1 is cancelled and cb2 is new. + acp._open_completions_later(False, False, False, ac.COMPLETE_FILES) + self.assertNotIn(cb1, self.root.tk.call('after', 'info')) + cb2 = acp._delayed_completion_id + self.assertTrue(cb2.startswith('after') and cb2 != cb1) + self.text.after_cancel(cb2) 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 + # Test that autocomplete._delayed_completion_id set to None + # and that open_completions is not called if the index is not + # equal to _delayed_completion_index. + acp = self.autocomplete + acp.open_completions = Func() + acp._delayed_completion_id = 'after' + acp._delayed_completion_index = self.text.index('insert+1c') + acp._delayed_open_completions(1, 2, 3) + self.assertIsNone(acp._delayed_completion_id) + self.assertEqual(acp.open_completions.called, 0) + + # Test that open_completions is called if indexes match. + acp._delayed_completion_index = self.text.index('insert') + acp._delayed_open_completions(1, 2, 3, ac.COMPLETE_FILES) + self.assertEqual(acp.open_completions.args, (1, 2, 3, 2)) def test_open_completions(self): # Test completions of files and attributes as well as non-completion - # of errors - pass + # of errors. + self.text.insert('1.0', 'pr') + self.assertTrue(self.autocomplete.open_completions(False, True, True)) + self.text.delete('1.0', 'end') + + # Test files. + self.text.insert('1.0', '"t') + #self.assertTrue(self.autocomplete.open_completions(False, True, True)) + self.text.delete('1.0', 'end') + + # Test with blank will fail. + self.assertFalse(self.autocomplete.open_completions(False, True, True)) + + # Test with only string quote will fail. + self.text.insert('1.0', '"') + self.assertFalse(self.autocomplete.open_completions(False, True, True)) + 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 '.'. + autocomplete = self.autocomplete + small, large = self.autocomplete.fetch_completions( + '', ac.COMPLETE_ATTRIBUTES) + if __main__.__file__ != ac.__file__: + self.assertNotIn('AutoComplete', small) # See issue 36405. + + # Test attributes + s, b = autocomplete.fetch_completions('', ac.COMPLETE_ATTRIBUTES) + 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 = autocomplete.fetch_completions('', ac.COMPLETE_ATTRIBUTES) + 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 = autocomplete.fetch_completions('foo', ac.COMPLETE_ATTRIBUTES) + 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 = autocomplete.fetch_completions('', ac.COMPLETE_FILES) + self.assertEqual(s, ['bar', 'foo']) + self.assertEqual(b, ['.hidden', 'bar', 'foo']) + + s, b = autocomplete.fetch_completions('~', ac.COMPLETE_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__. + autocomplete = self.autocomplete + Equal = self.assertEqual + + Equal(self.autocomplete.get_entity('int'), int) + + # Test name from sys.modules. + mock = Mock() + with patch.dict('sys.modules', {'tempfile': mock}): + Equal(autocomplete.get_entity('tempfile'), mock) + + # Test name from __main__.__dict__. + di = {'foo': 10, 'bar': 20} + with patch.dict('__main__.__dict__', {'d': di}): + Equal(autocomplete.get_entity('d'), di) + + # Test name not in namespace. + with patch.dict('__main__.__dict__', {}): + with self.assertRaises(NameError): + autocomplete.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..e5f44c46871325 100644 --- a/Lib/idlelib/idle_test/test_autoexpand.py +++ b/Lib/idlelib/idle_test/test_autoexpand.py @@ -1,9 +1,9 @@ -"""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: @@ -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() + requires('gui') + cls.tk = Tk() + cls.text = Text(cls.tk) cls.auto_expand = AutoExpand(Dummy_Editwin(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..dfbab6dd6b5e5b 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 diff --git a/Lib/idlelib/idle_test/test_calltips.py b/Lib/idlelib/idle_test/test_calltip.py similarity index 69% rename from Lib/idlelib/idle_test/test_calltips.py rename to Lib/idlelib/idle_test/test_calltip.py index a58229d36ede70..833351bd799601 100644 --- a/Lib/idlelib/idle_test/test_calltips.py +++ b/Lib/idlelib/idle_test/test_calltip.py @@ -1,9 +1,12 @@ +"Test calltip, coverage 60%" + +from idlelib import calltip import unittest -import idlelib.calltips as ct import textwrap import types -default_tip = ct._default_callable_argspec +default_tip = calltip._default_callable_argspec + # Test Class TC is used in multiple get_argspec test methods class TC(): @@ -31,9 +34,11 @@ def cm(cls, a): 'doc' @staticmethod def sm(b): 'doc' + tc = TC() +signature = calltip.get_argspec # 2.7 and 3.x use different functions + -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 @@ -54,14 +59,18 @@ 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, '(iterable=(), /)' + calltip._argument_positional + + '\n' + List.__doc__) gtest(list.__new__, - '(*args, **kwargs)\nCreate and return a new object. See help(type) for accurate signature.') + '(*args, **kwargs)\n' + 'Create 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." + '(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.") gtest(list.append, '(self, object, /)' + append_doc) gtest(List.append, '(self, object, /)' + append_doc) gtest([].append, '(object, /)' + append_doc) @@ -70,12 +79,17 @@ def gtest(obj, out): 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 + gtest(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''') - gtest(p.sub, '''(repl, string, count=0)\nReturn the string obtained by replacing the leftmost non-overlapping occurrences o...''') + gtest(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: @@ -85,10 +99,39 @@ def test_signature_wrap(self): 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 + + str_foo = "(s='aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"\ + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n" + indent + "aaaaaaaaa"\ + "aaaaaaaaaa')" + str_bar = "(s='aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"\ + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n" + indent + "aaaaaaaaa"\ + "aaaaaaaaaa')\nHello Guido" + str_baz = "(s='aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"\ + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n" + indent + "aaaaaaaaa"\ + "aaaaaaaaaa', z='bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"\ + "bbbbbbbbbbbbbbbbb\n" + indent + "bbbbbbbbbbbbbbbbbbbbbb"\ + "bbbbbbbbbbbbbbbbbbbbbb')" + + self.assertEqual(calltip.get_argspec(foo), str_foo) + self.assertEqual(calltip.get_argspec(bar), str_bar) + self.assertEqual(calltip.get_argspec(baz), str_baz) + def test_docline_truncation(self): def f(): pass f.__doc__ = 'a'*300 - self.assertEqual(signature(f), '()\n' + 'a' * (ct._MAX_COLS-3) + '...') + self.assertEqual(signature(f), '()\n' + 'a' * (calltip._MAX_COLS-3) + '...') def test_multiline_docstring(self): # Test fewer lines than max. @@ -107,7 +150,7 @@ def test_multiline_docstring(self): # Test more than max lines def f(): pass f.__doc__ = 'a\n' * 15 - self.assertEqual(signature(f), '()' + '\na' * ct._MAX_LINES) + self.assertEqual(signature(f), '()' + '\na' * calltip._MAX_LINES) def test_functions(self): def t1(): 'doc' @@ -135,8 +178,9 @@ def test_methods(self): 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)"),): + 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): @@ -153,7 +197,7 @@ def m2(**kwargs): pass class Test: def __call__(*, a): pass - mtip = ct._invalid_method + mtip = calltip._invalid_method self.assertEqual(signature(C().m2), mtip) self.assertEqual(signature(Test()), mtip) @@ -161,7 +205,7 @@ 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)' + assert calltip._first_param.sub('', uni) == '(a)' def test_no_docstring(self): def nd(s): @@ -194,9 +238,10 @@ def test_non_callables(self): class Get_entityTest(unittest.TestCase): def test_bad_entity(self): - self.assertIsNone(ct.get_entity('1/0')) + self.assertIsNone(calltip.get_entity('1/0')) def test_good_entity(self): - self.assertIs(ct.get_entity('int'), int) + self.assertIs(calltip.get_entity('int'), int) + if __name__ == '__main__': - unittest.main(verbosity=2, exit=False) + 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_codecontext.py b/Lib/idlelib/idle_test/test_codecontext.py new file mode 100644 index 00000000000000..6c6893580f42f6 --- /dev/null +++ b/Lib/idlelib/idle_test/test_codecontext.py @@ -0,0 +1,409 @@ +"Test codecontext, coverage 100%" + +from idlelib import codecontext +import unittest +from test.support import requires +from tkinter import 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 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.pack(side='top', fill='both', expand=1) + 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.cc = codecontext.CodeContext(self.editor) + + 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.textfont, ed.text['font']) + self.assertIsNone(cc.context) + eq(cc.info, [(0, -1, '', False)]) + eq(cc.topvisible, 1) + eq(self.root.tk.call('after', 'info', self.cc.t1)[1], 'timer') + eq(self.root.tk.call('after', 'info', self.cc.t2)[1], 'timer') + + def test_del(self): + self.cc.__del__() + with self.assertRaises(TclError) as msg: + self.root.tk.call('after', 'info', self.cc.t1) + self.assertIn("doesn't exist", msg) + with self.assertRaises(TclError) as msg: + self.root.tk.call('after', 'info', self.cc.t2) + self.assertIn("doesn't exist", msg) + # For coverage on the except. Have to delete because the + # above Tcl error is caught by after_cancel. + del self.cc.t1, self.cc.t2 + self.cc.__del__() + + def test_reload(self): + codecontext.CodeContext.reload() + self.assertEqual(self.cc.colors, {'background': 'lightgray', + 'foreground': '#000000'}) + 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. + eq(toggle(), 'break') + self.assertIsNotNone(cc.context) + eq(cc.context['font'], cc.textfont) + eq(cc.context['fg'], cc.colors['foreground']) + eq(cc.context['bg'], cc.colors['background']) + eq(cc.context.get('1.0', 'end-1c'), '') + eq(cc.editwin.label, 'Hide Code Context') + + # Toggle off. + eq(toggle(), 'break') + self.assertIsNone(cc.context) + eq(cc.editwin.label, 'Show Code 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(f'{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(f'{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(f'{12}.0') + cc.update_code_context() + eq(cc.topvisible, 12) + cc.context.mark_set('insert', '1.0') + jump() + eq(cc.topvisible, 8) + + @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_config_timer_event(self): + eq = self.assertEqual + cc = self.cc + save_font = cc.text['font'] + save_colors = codecontext.CodeContext.colors + test_font = 'FakeFont' + test_colors = {'background': '#222222', 'foreground': '#ffff00'} + + # Ensure code context is not active. + if cc.context: + cc.toggle_code_context_event() + + # Nothing updates on inactive code context. + cc.text['font'] = test_font + codecontext.CodeContext.colors = test_colors + cc.config_timer_event() + eq(cc.textfont, save_font) + eq(cc.contextcolors, save_colors) + + # Activate code context, but no change to font or color. + cc.toggle_code_context_event() + cc.text['font'] = save_font + codecontext.CodeContext.colors = save_colors + cc.config_timer_event() + eq(cc.textfont, save_font) + eq(cc.contextcolors, save_colors) + eq(cc.context['font'], save_font) + eq(cc.context['background'], save_colors['background']) + eq(cc.context['foreground'], save_colors['foreground']) + + # Active code context, change font. + cc.text['font'] = test_font + cc.config_timer_event() + eq(cc.textfont, test_font) + eq(cc.contextcolors, save_colors) + eq(cc.context['font'], test_font) + eq(cc.context['background'], save_colors['background']) + eq(cc.context['foreground'], save_colors['foreground']) + + # Active code context, change color. + cc.text['font'] = save_font + codecontext.CodeContext.colors = test_colors + cc.config_timer_event() + eq(cc.textfont, save_font) + eq(cc.contextcolors, test_colors) + eq(cc.context['font'], save_font) + eq(cc.context['background'], test_colors['background']) + eq(cc.context['foreground'], test_colors['foreground']) + codecontext.CodeContext.colors = save_colors + cc.config_timer_event() + + +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..7e2c1fd2958cee 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. @@ -256,9 +254,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 @@ -357,11 +355,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 +373,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', @@ -453,7 +447,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(''' 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..37e83439c471ed 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 @@ -61,6 +60,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 +211,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 +242,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): @@ -604,40 +606,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 @@ -1086,6 +1083,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): @@ -1170,7 +1168,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..12bc8473668334 100644 --- a/Lib/idlelib/idle_test/test_editor.py +++ b/Lib/idlelib/idle_test/test_editor.py @@ -1,14 +1,46 @@ +"Test editor, coverage 35%." + +from idlelib import editor import unittest -from idlelib.editor import EditorWindow +from test.support import requires +from tkinter import Tk + +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 EditorFunctionTest(unittest.TestCase): -class Editor_func_test(unittest.TestCase): def test_filename_to_unicode(self): - func = EditorWindow._filename_to_unicode - class dummy(): filesystemencoding = 'utf-8' + func = Editor._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) + 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_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..8dbfc63779d380 100644 --- a/Lib/idlelib/idle_test/test_hyperparser.py +++ b/Lib/idlelib/idle_test/test_hyperparser.py @@ -1,9 +1,10 @@ -"""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): @@ -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..743a05b3c3134e 100644 --- a/Lib/idlelib/idle_test/test_iomenu.py +++ b/Lib/idlelib/idle_test/test_iomenu.py @@ -1,233 +1,36 @@ -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 16%." - 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 IOBindigTest(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() + 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): + io = iomenu.IOBinding(self.editwin) + self.assertIs(io.editwin, self.editwin) + io.close 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..68156a743d7b9b --- /dev/null +++ b/Lib/idlelib/idle_test/test_multicall.py @@ -0,0 +1,40 @@ +"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) + + +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_paragraph.py b/Lib/idlelib/idle_test/test_paragraph.py index ba350c976534e8..0cb966fb96ca0e 100644 --- a/Lib/idlelib/idle_test/test_paragraph.py +++ b/Lib/idlelib/idle_test/test_paragraph.py @@ -1,9 +1,10 @@ -# Test the functions and main class method of paragraph.py +"Test paragraph, coverage 76%." + +from idlelib import paragraph as pg import unittest -from idlelib import paragraph as fp -from idlelib.editor import EditorWindow -from tkinter import Tk, Text from test.support import requires +from tkinter import Tk, Text +from idlelib.editor import EditorWindow class Is_Get_Test(unittest.TestCase): @@ -15,26 +16,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(pg.is_all_white('')) + self.assertTrue(pg.is_all_white('\t\n\r\f\v')) + self.assertFalse(pg.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(pg.get_indent(self.test_comment), '') + Equal(pg.get_indent(self.trailingws_comment), '') + Equal(pg.get_indent(self.leadingws_comment), ' ') + Equal(pg.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(pg.get_comment_header(self.test_comment), '#') + Equal(pg.get_comment_header(self.trailingws_comment), '#') + Equal(pg.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(pg.get_comment_header(self.leadingws_nocomment), ' ') + Equal(pg.get_comment_header(self.test_nocomment), '') class FindTest(unittest.TestCase): @@ -62,7 +63,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(pg.find_paragraph(text, tempindex), expected) text.delete('1.0', 'end') def test_find_comment(self): @@ -161,7 +162,7 @@ class ReformatFunctionTest(unittest.TestCase): def test_reformat_paragraph(self): Equal = self.assertEqual - reform = fp.reformat_paragraph + reform = pg.reformat_paragraph hw = "O hello world" Equal(reform(' ', 1), ' ') Equal(reform("Hello world", 20), "Hello world") @@ -192,7 +193,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 = pg.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 +202,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 = pg.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 +211,7 @@ def test_reformat_comment(self): class FormatClassTest(unittest.TestCase): def test_init_close(self): - instance = fp.FormatParagraph('editor') + instance = pg.FormatParagraph('editor') self.assertEqual(instance.editwin, 'editor') instance.close() self.assertEqual(instance.editwin, None) @@ -269,14 +270,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 = pg.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 diff --git a/Lib/idlelib/idle_test/test_parenmatch.py b/Lib/idlelib/idle_test/test_parenmatch.py index 3caa2754a6d8a2..f58819abf11211 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') 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..0534301b36102f --- /dev/null +++ b/Lib/idlelib/idle_test/test_pyparse.py @@ -0,0 +1,466 @@ +"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 + + # Split def across lines. + setcode('"""This is a module docstring"""\n' + 'class C():\n' + ' def __init__(self, a,\n' + ' b=True):\n' + ' pass\n' + ) + + # No value sent for is_char_in_string(). + self.assertIsNone(start()) + + # 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(is_char_in_string=lambda index: 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(is_char_in_string=lambda index: 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 contination. + 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 continutation docstring.\n\n", + 0, 49, "'", None, ((0, 0), (0, 1), (49, 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..581444ca5ef21f --- /dev/null +++ b/Lib/idlelib/idle_test/test_pyshell.py @@ -0,0 +1,42 @@ +"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 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..c1c4a25cc50608 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 91%). 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 -import sys from tkinter import Tk -import unittest + +import sys from unittest import mock from idlelib.idle_test.mock_tk import Var -from idlelib import query # NON-GUI TESTS 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 index 130e6be257fe58..2bc7c6f035e96b 100644 --- a/Lib/idlelib/idle_test/test_rstrip.py +++ b/Lib/idlelib/idle_test/test_rstrip.py @@ -1,5 +1,7 @@ +"Test rstrip, coverage 100%." + +from idlelib import rstrip import unittest -import idlelib.rstrip as rs from idlelib.idle_test.mock_idle import Editor class rstripTest(unittest.TestCase): @@ -7,7 +9,7 @@ class rstripTest(unittest.TestCase): def test_rstrip_line(self): editor = Editor() text = editor.text - do_rstrip = rs.RstripExtension(editor).do_rstrip + do_rstrip = rstrip.Rstrip(editor).do_rstrip do_rstrip() self.assertEqual(text.get('1.0', 'insert'), '') @@ -20,12 +22,12 @@ def test_rstrip_line(self): 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()) + # Comment above, uncomment 3 below to test with real Editor & Text. + #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 + do_rstrip = rstrip.Rstrip(editor).do_rstrip original = ( "Line with an ending tab \n" @@ -45,5 +47,7 @@ def test_rstrip_multiple(self): do_rstrip() self.assertEqual(text.get('1.0', 'insert'), stripped) + + if __name__ == '__main__': - unittest.main(verbosity=2, exit=False) + unittest.main(verbosity=2) diff --git a/Lib/idlelib/idle_test/test_run.py b/Lib/idlelib/idle_test/test_run.py index d7e627d23d3841..46f0235fbfdca1 100644 --- a/Lib/idlelib/idle_test/test_run.py +++ b/Lib/idlelib/idle_test/test_run.py @@ -1,11 +1,14 @@ +"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 class RunTest(unittest.TestCase): + def test_print_exception_unhashable(self): class UnhashableException(Exception): def __eq__(self, other): @@ -20,10 +23,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 +34,231 @@ def __eq__(self, other): self.assertIn('UnhashableException: ex1', tb[10]) +# PseudoFile 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 PseudeInputFilesTest(unittest.TestCase): + + def test_misc(self): + shell = MockShell() + f = run.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 = run.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 = run.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 = run.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 = run.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 = run.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) + + +class PseudeOutputFilesTest(unittest.TestCase): + + def test_misc(self): + shell = MockShell() + f = run.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 = run.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 = run.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() + + 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.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 = run.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) + + 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..09a7fff51de1dc 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 Tk +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 @@ -97,11 +98,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 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_squeezer.py b/Lib/idlelib/idle_test/test_squeezer.py new file mode 100644 index 00000000000000..4e3da030a3adce --- /dev/null +++ b/Lib/idlelib/idle_test/test_squeezer.py @@ -0,0 +1,487 @@ +"Test squeezer, coverage 95%" + +from collections import namedtuple +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 tearDown(self): + # Clean up the Squeezer class's reference to its instance, + # to avoid side-effects from one test case upon another. + if Squeezer._instance_weakref is not None: + Squeezer._instance_weakref = None + + def make_mock_editor_window(self, with_text_widget=False): + """Create a mock EditorWindow instance.""" + editwin = NonCallableMagicMock() + # isinstance(editwin, PyShell) must be true for Squeezer to enable + # auto-squeezing; in practice this will always be true. + editwin.__class__ = PyShell + + 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) + squeezer.get_line_width = Mock(return_value=80) + 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) + squeezer.get_line_width.return_value = 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) + squeezer.load_font = Mock() + + 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) + squeezer.load_font.assert_called() + + 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..6f0c1930518a51 100644 --- a/Lib/idlelib/idle_test/test_textview.py +++ b/Lib/idlelib/idle_test/test_textview.py @@ -1,17 +1,15 @@ -'''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 +import unittest from test.support import requires requires('gui') -import unittest import os from tkinter import Tk from tkinter.ttk import Button @@ -75,7 +73,6 @@ class TextFrameTest(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') @@ -109,10 +106,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 +118,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 +162,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..44ea1110e155dc --- /dev/null +++ b/Lib/idlelib/idle_test/test_tooltip.py @@ -0,0 +1,146 @@ +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 root_update(): + global root + root.update() + +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 test_showtip(self): + tooltip = Hovertip(self.button, 'ToolTip text') + self.addCleanup(tooltip.hidetip) + self.assertFalse(tooltip.tipwindow and tooltip.tipwindow.winfo_viewable()) + tooltip.showtip() + self.assertTrue(tooltip.tipwindow and tooltip.tipwindow.winfo_viewable()) + + def test_showtip_twice(self): + tooltip = Hovertip(self.button, 'ToolTip text') + self.addCleanup(tooltip.hidetip) + self.assertFalse(tooltip.tipwindow and tooltip.tipwindow.winfo_viewable()) + tooltip.showtip() + self.assertTrue(tooltip.tipwindow and tooltip.tipwindow.winfo_viewable()) + orig_tipwindow = tooltip.tipwindow + tooltip.showtip() + self.assertTrue(tooltip.tipwindow and tooltip.tipwindow.winfo_viewable()) + 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(tooltip.tipwindow and tooltip.tipwindow.winfo_viewable()) + + 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(tooltip.tipwindow and tooltip.tipwindow.winfo_viewable()) + self.button.event_generate('', x=0, y=0) + root_update() + self.assertTrue(tooltip.tipwindow and tooltip.tipwindow.winfo_viewable()) + self.assertGreater(len(tooltip.showtip.call_args_list), 0) + + def test_showtip_on_mouse_enter_hover_delay(self): + tooltip = Hovertip(self.button, 'ToolTip text', hover_delay=50) + self.addCleanup(tooltip.hidetip) + tooltip.showtip = add_call_counting(tooltip.showtip) + root_update() + self.assertFalse(tooltip.tipwindow and tooltip.tipwindow.winfo_viewable()) + self.button.event_generate('', x=0, y=0) + root_update() + self.assertFalse(tooltip.tipwindow and tooltip.tipwindow.winfo_viewable()) + time.sleep(0.1) + root_update() + self.assertTrue(tooltip.tipwindow and tooltip.tipwindow.winfo_viewable()) + self.assertGreater(len(tooltip.showtip.call_args_list), 0) + + 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(tooltip.tipwindow and tooltip.tipwindow.winfo_viewable()) + self.assertGreater(len(tooltip.showtip.call_args_list), 0) + + def test_dont_show_on_mouse_leave_before_delay(self): + tooltip = Hovertip(self.button, 'ToolTip text', hover_delay=50) + 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() + time.sleep(0.1) + root_update() + self.assertFalse(tooltip.tipwindow and tooltip.tipwindow.winfo_viewable()) + self.assertEqual(tooltip.showtip.call_args_list, []) + + +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..9be9abee361f08 100644 --- a/Lib/idlelib/idle_test/test_tree.py +++ b/Lib/idlelib/idle_test/test_tree.py @@ -1,11 +1,9 @@ -''' 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 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..f5bced597aa821 100644 --- a/Lib/idlelib/iomenu.py +++ b/Lib/idlelib/iomenu.py @@ -40,8 +40,8 @@ # 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 + if locale_encoding is None or locale_encoding == '': + # situation occurs on macOS locale_encoding = 'ascii' codecs.lookup(locale_encoding) except (NameError, AttributeError, LookupError): @@ -50,8 +50,8 @@ # 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 + if locale_encoding is None or locale_encoding == '': + # situation occurs on macOS locale_encoding = 'ascii' codecs.lookup(locale_encoding) except (ValueError, LookupError): @@ -567,8 +567,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..f834220fc2bb75 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,9 @@ ('E_xpand Word', '<>'), ('Show C_all Tip', '<>'), ('Show Surrounding P_arens', '<>'), + ]), - ]), -('format', [ + ('format', [ ('_Indent Region', '<>'), ('_Dedent Region', '<>'), ('Comment _Out Region', '<>'), @@ -70,30 +71,40 @@ ('F_ormat Paragraph', '<>'), ('S_trip Trailing Whitespace', '<>'), ]), + ('run', [ ('Python Shell', '<>'), ('C_heck Module', '<>'), ('R_un Module', '<>'), ]), + ('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', '<>'), - ]), - ('windows', [ + None, + ('Show _Code Context', '<>'), ('Zoom Height', '<>'), ]), + + ('window', [ + ]), + ('help', [ ('_About IDLE', '<>'), None, @@ -106,3 +117,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..ecc53ef0195dc6 100644 --- a/Lib/idlelib/outwin.py +++ b/Lib/idlelib/outwin.py @@ -78,6 +78,7 @@ def __init__(self, *args): EditorWindow.__init__(self, *args) self.text.bind("<>", self.goto_file_line) self.text.unbind("<>") + self.update_menu_state('options', '*Code Context', 'disabled') # 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 index 1270115a44ce44..81422571fa32f4 100644 --- a/Lib/idlelib/paragraph.py +++ b/Lib/idlelib/paragraph.py @@ -190,6 +190,5 @@ def get_comment_header(line): if __name__ == "__main__": - import unittest - unittest.main('idlelib.idle_test.test_paragraph', - verbosity=2, exit=False) + from unittest import main + 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..81e7f539803c08 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. +class ParseMap(dict): + r"""Dict subclass that maps anything not in dict to 'x'. - 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. + 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'. - For example, to 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 + # Calling this triples access time; see bpo-32940 + def __missing__(self, key): + return 120 # ord('x') - def __getitem__(self, item): - return self._get(item) - def __len__(self): - return len(self._non_defaults) - - 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,25 +128,26 @@ 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 + """ + 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 if not is_char_in_string: # no clue -- make the caller pass everything @@ -168,13 +156,13 @@ def find_good_parse_start(self, is_char_in_string=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 +176,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 +185,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 +194,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 +218,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 +235,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 +264,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 +291,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 +302,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 +311,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 +341,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 +397,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 +438,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 +453,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 +533,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 +550,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 statemtent 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..2de42658b01cb8 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 + 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 @@ -404,10 +418,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): @@ -635,6 +646,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): @@ -838,10 +852,14 @@ 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", "<>"), + ] # New classes from idlelib.history import History @@ -863,7 +881,7 @@ def __init__(self, flist=None): self.usetabs = True # indentwidth must be 8 when using tabs. See note in EditorWindow: self.indentwidth = 8 - + self.context_use_ps1 = True self.sys_ps1 = sys.ps1 if hasattr(sys, 'ps1') else '>>> ' self.prompt_last_line = self.sys_ps1.split('\n')[-1] self.prompt = self.sys_ps1 # Changes when debug active @@ -880,6 +898,9 @@ 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 @@ -1019,7 +1040,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") @@ -1255,6 +1276,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: @@ -1465,7 +1494,7 @@ def main(): if system() == 'Windows': iconfile = os.path.join(icondir, 'idle.ico') root.wm_iconbitmap(default=iconfile) - else: + elif not macosx.isAquaTk(): ext = '.png' if TkVersion >= 8.6 else '.gif' iconfiles = [os.path.join(icondir, 'idle_%d%s' % (size, ext)) for size in (16, 32, 48)] @@ -1523,12 +1552,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 +1573,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..f0b72553db87f7 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. @@ -143,6 +142,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." @@ -301,8 +304,8 @@ def entry_ok(self): 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) 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 index 18c86f9b2c8896..f93b5e8fc20021 100644 --- a/Lib/idlelib/rstrip.py +++ b/Lib/idlelib/rstrip.py @@ -1,6 +1,6 @@ 'Provides "Strip trailing whitespace" under the "Format" menu.' -class RstripExtension: +class Rstrip: def __init__(self, editwin): self.editwin = editwin @@ -25,5 +25,5 @@ def do_rstrip(self, event=None): undo.undo_block_stop() if __name__ == "__main__": - import unittest - unittest.main('idlelib.idle_test.test_rstrip', verbosity=2, exit=False) + from unittest import main + main('idlelib.idle_test.test_rstrip', verbosity=2,) diff --git a/Lib/idlelib/run.py b/Lib/idlelib/run.py index 176fe3db743bd4..6fa373f2584c27 100644 --- a/Lib/idlelib/run.py +++ b/Lib/idlelib/run.py @@ -11,7 +11,7 @@ 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 @@ -462,7 +462,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): diff --git a/Lib/idlelib/runscript.py b/Lib/idlelib/runscript.py index 45bf56345825a1..83433b1cf0a459 100644 --- a/Lib/idlelib/runscript.py +++ b/Lib/idlelib/runscript.py @@ -193,3 +193,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..5bbe9d6b5dc8a1 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 seach 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..f0e3d6f14ba49b 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: @@ -42,6 +42,7 @@ def __init__(self, root, engine): icon (of dialog): ditto, use unclear if cannot minimize dialog. ''' self.root = root + self.bell = root.bell self.engine = engine self.top = None @@ -80,7 +81,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) @@ -192,9 +192,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/squeezer.py b/Lib/idlelib/squeezer.py new file mode 100644 index 00000000000000..869498d753a2cd --- /dev/null +++ b/Lib/idlelib/squeezer.py @@ -0,0 +1,377 @@ +"""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-cliking 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 weakref + +import tkinter as tk +from tkinter.font import Font +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. + """ + _instance_weakref = None + + @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, + ) + + # Loading the font info requires a Tk root. IDLE doesn't rely + # on Tkinter's "default root", so the instance will reload + # font info using its editor windows's Tk root. + if cls._instance_weakref is not None: + instance = cls._instance_weakref() + if instance is not None: + instance.load_font() + + 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 + + Squeezer._instance_weakref = weakref.ref(self) + self.load_font() + + # 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. + """ + linewidth = self.get_line_width() + return count_lines_with_wrapping(s, linewidth) + + def get_line_width(self): + # The maximum line length in pixels: The width of the text + # widget, minus twice the border width and internal padding. + linewidth_pixels = \ + self.base_text.winfo_width() - self.window_width_delta + + # Divide the width of the Text widget by the font width, + # which is taken to be the width of '0' (zero). + # http://www.tcl.tk/man/tcl8.6/TkCmd/text.htm#M21 + return linewidth_pixels // self.zero_char_width + + def load_font(self): + text = self.base_text + self.zero_char_width = \ + Font(text, font=text.cget('font')).measure('0') + + 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..4867a80db1abe6 100644 --- a/Lib/idlelib/textview.py +++ b/Lib/idlelib/textview.py @@ -1,15 +1,37 @@ """Simple text browser for IDLE """ -from tkinter import Toplevel, Text +from tkinter import Toplevel, Text, TclError,\ + HORIZONTAL, VERTICAL, N, S, E, W from tkinter.ttk import Frame, Scrollbar, Button from tkinter.messagebox import showerror +from idlelib.colorizer import color_config + + +class AutoHiddenScrollbar(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 TextFrame(Frame): "Display text with scrollbar." - def __init__(self, parent, rawtext): + def __init__(self, parent, rawtext, wrap='word'): """Create a frame for Textview. parent - parent widget for this frame @@ -18,31 +40,40 @@ def __init__(self, parent, rawtext): 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 + + self.text = text = Text(self, wrap=wrap, highlightthickness=0) + color_config(text) + text.grid(row=0, column=0, sticky=N+S+E+W) + self.grid_rowconfigure(0, weight=1) + self.grid_columnconfigure(0, weight=1) text.insert(0.0, rawtext) text['state'] = 'disabled' text.focus_set() - scroll.pack(side='right', fill='y') - text.pack(side='left', expand=True, fill='both') + # vertical scrollbar + self.yscroll = yscroll = AutoHiddenScrollbar(self, orient=VERTICAL, + takefocus=False, + command=text.yview) + text['yscrollcommand'] = yscroll.set + yscroll.grid(row=0, column=1, sticky=N+S) + + if wrap == 'none': + # horizontal scrollbar + self.xscroll = xscroll = AutoHiddenScrollbar(self, orient=HORIZONTAL, + takefocus=False, + command=text.xview) + text['xscrollcommand'] = xscroll.set + xscroll.grid(row=1, column=0, sticky=E+W) class ViewFrame(Frame): "Display TextFrame and Close button." - def __init__(self, parent, text): + def __init__(self, parent, text, wrap='word'): super().__init__(parent) self.parent = parent self.bind('', self.ok) self.bind('', self.ok) - self.textframe = TextFrame(self, text) + self.textframe = TextFrame(self, text, wrap=wrap) 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 +87,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, text, modal=True, wrap='word', *, _htest=False, _utest=False): """Show the given text in a scrollable window with a 'close' button. @@ -66,6 +97,7 @@ 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 + 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 +109,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, text, 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 +124,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, text, 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 + 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, text, 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 +162,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..f54ea36f059d6f 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: + 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("