Update Pier_v1 project

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
CopilotandCopilot committed 2026-11-03 12:04:58 +09:00
1 parent f25e36c050
commit ad73ddec2b
2476 files changed
+3423 -4902

No files matched your search

@@ -0,0 +1,47 @@
Guido van Rossum, as well as being the creator of the Python language, is the
original creator of IDLE. Other contributors prior to Version 0.8 include
Mark Hammond, Jeremy Hylton, Tim Peters, and Moshe Zadka.
Until Python 2.3, IDLE's development was carried out in the SF IDLEfork project. The
objective was to develop a version of IDLE which had an execution environment
which could be initialized prior to each run of user code.
IDLefork was merged into the Python code base in 2003.
The IDLEfork project was initiated by David Scherer, with some help from Peter
Schneider-Kamp and Nicholas Riley. David wrote the first version of the RPC
code and designed a fast turn-around environment for VPython. Guido developed
the RPC code and Remote Debugger currently integrated in IDLE. Bruce Sherwood
contributed considerable time testing and suggesting improvements.
Besides David and Guido, the main developers who were active on IDLEfork
are Stephen M. Gava, who implemented the configuration GUI, the new
configuration system, and the About dialog, and Kurt B. Kaiser, who completed
the integration of the RPC and remote debugger, implemented the threaded
subprocess, and made a number of usability enhancements.
Other contributors include Raymond Hettinger, Tony Lownds (Mac integration),
Neal Norwitz (code check and clean-up), Ronald Oussoren (Mac integration),
Noam Raphael (Code Context, Call Tips, many other patches), and Chui Tey (RPC
integration, debugger integration and persistent breakpoints).
Scott David Daniels, Tal Einat, Hernan Foffani, Christos Georgiou,
Jim Jewett, Martin v. Löwis, Jason Orendorff, Guilherme Polo, Josh Robb,
Nigel Rowe, Bruce Sherwood, Jeff Shute, and Weeble have submitted useful
patches. Thanks, guys!
Major contributors since 2005:
- 2005: Tal Einat
- 2010: Terry Jan Reedy (current maintainer)
- 2013: Roger Serwy
- 2014: Saimadhav Heblikar
- 2015: Mark Roseman
- 2017: Louie Lu, Cheryl Sabella, and Serhiy Storchaka
For additional details refer to NEWS.txt and Changelog.
If we missed you, feel free to submit a PR with a summary of
contributions (for instance, at least 5 merged PRs).
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,296 @@
IDLE History
============
This file contains the release messages for previous IDLE releases.
As you read on you go back to the dark ages of IDLE's history.
What's New in IDLEfork 0.8.1?
=============================
*Release date: 22-Jul-2001*
- New tarball released as a result of the 'revitalisation' of the IDLEfork
project.
- This release requires python 2.1 or better. Compatibility with earlier
versions of python (especially ancient ones like 1.5x) is no longer a
priority in IDLEfork development.
- This release is based on a merging of the earlier IDLE fork work with current
cvs IDLE (post IDLE version 0.8), with some minor additional coding by Kurt
B. Kaiser and Stephen M. Gava.
- This release is basically functional but also contains some known breakages,
particularly with running things from the shell window. Also the debugger is
not working, but I believe this was the case with the previous IDLE fork
release (0.7.1) as well.
- This release is being made now to mark the point at which IDLEfork is
launching into a new stage of development.
- IDLEfork CVS will now be branched to enable further development and
exploration of the two "execution in a remote process" patches submitted by
David Scherer (David's is currently in IDLEfork) and GvR, while stabilisation
and development of less heavyweight improvements (like user customisation)
can continue on the trunk.
What's New in IDLEfork 0.7.1?
==============================
*Release date: 15-Aug-2000*
- First project tarball released.
- This was the first release of IDLE fork, which at this stage was a
combination of IDLE 0.5 and the VPython idle fork, with additional changes
coded by David Scherer, Peter Schneider-Kamp and Nicholas Riley.
IDLEfork 0.7.1 - 29 May 2000
-----------------------------
David Scherer <dscherer@cmu.edu>
- This is a modification of the CVS version of IDLE 0.5, updated as of
2000-03-09. It is alpha software and might be unstable. If it breaks, you
get to keep both pieces.
- If you have problems or suggestions, you should either contact me or post to
the list at http://www.python.org/mailman/listinfo/idle-dev (making it clear
that you are using this modified version of IDLE).
- Changes:
- The ExecBinding module, a replacement for ScriptBinding, executes programs
in a separate process, piping standard I/O through an RPC mechanism to an
OnDemandOutputWindow in IDLE. It supports executing unnamed programs
(through a temporary file). It does not yet support debugging.
- When running programs with ExecBinding, tracebacks will be clipped to
exclude system modules. If, however, a system module calls back into the
user program, that part of the traceback will be shown.
- The OnDemandOutputWindow class has been improved. In particular, it now
supports a readline() function used to implement user input, and a
scroll_clear() operation which is used to hide the output of a previous run
by scrolling it out of the window.
- Startup behavior has been changed. By default IDLE starts up with just a
blank editor window, rather than an interactive window. Opening a file in
such a blank window replaces the (nonexistent) contents of that window
instead of creating another window. Because of the need to have a
well-known port for the ExecBinding protocol, only one copy of IDLE can be
running. Additional invocations use the RPC mechanism to report their
command line arguments to the copy already running.
- The menus have been reorganized. In particular, the excessively large
'edit' menu has been split up into 'edit', 'format', and 'run'.
- 'Python Documentation' now works on Windows, if the win32api module is
present.
- A few key bindings have been changed: F1 now loads Python Documentation
instead of the IDLE help; shift-TAB is now a synonym for unindent.
- New modules:
ExecBinding.py Executes program through loader
loader.py Bootstraps user program
protocol.py RPC protocol
Remote.py User-process interpreter
spawn.py OS-specific code to start programs
- Files modified:
autoindent.py ( bindings tweaked )
bindings.py ( menus reorganized )
config.txt ( execbinding enabled )
editorwindow.py ( new menus, fixed 'Python Documentation' )
filelist.py ( hook for "open in same window" )
formatparagraph.py ( bindings tweaked )
idle.bat ( removed absolute pathname )
idle.pyw ( weird bug due to import with same name? )
iobinding.py ( open in same window, EOL convention )
keydefs.py ( bindings tweaked )
outputwindow.py ( readline, scroll_clear, etc )
pyshell.py ( changed startup behavior )
readme.txt ( <Recursion on file with id=1234567> )
IDLE 0.5 - February 2000 - Release Notes
----------------------------------------
This is an early release of IDLE, my own attempt at a Tkinter-based
IDE for Python.
(For a more detailed change log, see the file ChangeLog.)
FEATURES
IDLE has the following features:
- coded in 100% pure Python, using the Tkinter GUI toolkit (i.e. Tcl/Tk)
- cross-platform: works on Windows and Unix (on the Mac, there are
currently problems with Tcl/Tk)
- multi-window text editor with multiple undo, Python colorizing
and many other features, e.g. smart indent and call tips
- Python shell window (a.k.a. interactive interpreter)
- debugger (not complete, but you can set breakpoints, view and step)
USAGE
The main program is in the file "idle.py"; on Unix, you should be able
to run it by typing "./idle.py" to your shell. On Windows, you can
run it by double-clicking it; you can use idle.pyw to avoid popping up
a DOS console. If you want to pass command line arguments on Windows,
use the batch file idle.bat.
Command line arguments: files passed on the command line are executed,
not opened for editing, unless you give the -e command line option.
Try "./idle.py -h" to see other command line options.
IDLE requires Python 1.5.2, so it is currently only usable with a
Python 1.5.2 distribution. (An older version of IDLE is distributed
with Python 1.5.2; you can drop this version on top of it.)
COPYRIGHT
IDLE is covered by the standard Python copyright notice
(http://www.python.org/doc/Copyright.html).
New in IDLE 0.5 (2/15/2000)
---------------------------
Tons of stuff, much of it contributed by Tim Peters and Mark Hammond:
- Status bar, displaying current line/column (Moshe Zadka).
- Better stack viewer, using tree widget. (XXX Only used by Stack
Viewer menu, not by the debugger.)
- Format paragraph now recognizes Python block comments and reformats
them correctly (MH)
- New version of pyclbr.py parses top-level functions and understands
much more of Python's syntax; this is reflected in the class and path
browsers (TP)
- Much better auto-indent; knows how to indent the insides of
multi-line statements (TP)
- Call tip window pops up when you type the name of a known function
followed by an open parenthesis. Hit ESC or click elsewhere in the
window to close the tip window (MH)
- Comment out region now inserts ## to make it stand out more (TP)
- New path and class browsers based on a tree widget that looks
familiar to Windows users
- Reworked script running commands to be more intuitive: I/O now
always goes to the *Python Shell* window, and raw_input() works
correctly. You use F5 to import/reload a module: this adds the module
name to the __main__ namespace. You use Control-F5 to run a script:
this runs the script *in* the __main__ namespace. The latter also
sets sys.argv[] to the script name
New in IDLE 0.4 (4/7/99)
------------------------
Most important change: a new menu entry "File -> Path browser", shows
a 4-column hierarchical browser which lets you browse sys.path,
directories, modules, and classes. Yes, it's a superset of the Class
browser menu entry. There's also a new internal module,
MultiScrolledLists.py, which provides the framework for this dialog.
New in IDLE 0.3 (2/17/99)
-------------------------
Most important changes:
- Enabled support for running a module, with or without the debugger.
Output goes to a new window. Pressing F5 in a module is effectively a
reload of that module; Control-F5 loads it under the debugger.
- Re-enable tearing off the Windows menu, and make a torn-off Windows
menu update itself whenever a window is opened or closed.
- Menu items can now be have a checkbox (when the menu label starts
with "!"); use this for the Debugger and "Auto-open stack viewer"
(was: JIT stack viewer) menu items.
- Added a Quit button to the Debugger API.
- The current directory is explicitly inserted into sys.path.
- Fix the debugger (when using Python 1.5.2b2) to use canonical
filenames for breakpoints, so these actually work. (There's still a
lot of work to be done to the management of breakpoints in the
debugger though.)
- Closing a window that is still colorizing now actually works.
- Allow dragging of the separator between the two list boxes in the
class browser.
- Bind ESC to "close window" of the debugger, stack viewer and class
browser. It removes the selection highlighting in regular text
windows. (These are standard Windows conventions.)
New in IDLE 0.2 (1/8/99)
------------------------
Lots of changes; here are the highlights:
General:
- You can now write and configure your own IDLE extension modules; see
extend.txt.
File menu:
The command to open the Python shell window is now in the File menu.
Edit menu:
New Find dialog with more options; replace dialog; find in files dialog.
Commands to tabify or untabify a region.
Command to format a paragraph.
Debug menu:
JIT (Just-In-Time) stack viewer toggle -- if set, the stack viewer
automatically pops up when you get a traceback.
Windows menu:
Zoom height -- make the window full height.
Help menu:
The help text now show up in a regular window so you can search and
even edit it if you like.
IDLE 0.1 was distributed with the Python 1.5.2b1 release on 12/22/98.
======================================================================
@@ -0,0 +1,51 @@
IDLE-PYTHON LOGOS
These are sent to tk on Windows, *NIX, and non-Aqua macOS
in pyshell following "# set application icon".
2006?: Andrew Clover made variously sized python icons for win23.
https://www.doxdesk.com/software/py/pyicons.html
2006: 16, 32, and 48 bit .png versions were copied to CPython
as Python application icons, maybe in PC/icons/py.ico.
https://github.com/python/cpython/issues/43372
2014: They were copied (perhaps a bit revised) to idlelib/Icons.
https://github.com/python/cpython/issues/64605
.gif versions were also added.
2020: Add Clover's 256-bit image.
https://github.com/python/cpython/issues/82620
Other fixups were done.
The idle.ico file used for Windows was created with ImageMagick:
$ convert idle_16.png idle_32.png idle_48.png idle_256.png idle.ico
** This needs redoing whenever files are changed.
?? Do Start, Desktop, and Taskbar use idlelib/Icons files?
Issue added Windows Store PC/icons/idlex44.png and .../idlex150.png.
https://github.com/python/cpython/pull/22817
?? Should these be updated with major changes?
2022: Optimize .png images in CPython repository with external program.
https://github.com/python/cpython/pull/21348
idle.ico (and idlex##) were not updated.
The idlexx.gif files are only needed for *nix running tcl/tk 8.5.
As of 2022, this was known true for 1 'major' Linux distribution.
(Same would be true for any non-Aqua macOS with 8.5, but now none?)
Can be deleted when we require 8.6 or it is known always used.
Future: Derivatives of Python logo should be submitted for approval.
PSF Trademark Working Group / Committee psf-trademarks@python.org
https://www.python.org/community/logos/ # Original files
https://www.python.org/psf/trademarks-faq/
https://www.python.org/psf/trademarks/ # Usage.
OTHER GIFS: These are used by browsers using idlelib.tree.
At least some will not be used when tree is replaced by ttk.Treeview.
Edited 2024 August 26 by TJR.
Binary file not shown.

After

Width:  |  Height:  |  Size: 120 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 634 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1019 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 380 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 B

@@ -0,0 +1,660 @@
What's New in IDLE 2.7? (Merged into 3.1 before 2.7 release.)
=======================
*Release date: XX-XXX-2010*
- idle.py modified and simplified to better support developing experimental
versions of IDLE which are not installed in the standard location.
- OutputWindow/PyShell right click menu "Go to file/line" wasn't working with
file paths containing spaces. Bug 5559.
- Windows: Version string for the .chm help file changed, file not being
accessed Patch 5783 Guilherme Polo
- Allow multiple IDLE GUI/subprocess pairs to exist simultaneously. Thanks to
David Scherer for suggesting the use of an ephemeral port for the GUI.
Patch 1529142 Weeble.
- Remove port spec from run.py and fix bug where subprocess fails to
extract port from command line when warnings are present.
- Tk 8.5 Text widget requires 'wordprocessor' tabstyle attr to handle
mixed space/tab properly. Issue 5129, patch by Guilherme Polo.
- Issue #3549: On MacOS the preferences menu was not present
- IDLE would print a "Unhandled server exception!" message when internal
debugging is enabled.
- Issue #4455: IDLE failed to display the windows list when two windows have
the same title.
- Issue #4383: When IDLE cannot make the connection to its subprocess, it would
fail to properly display the error message.
- help() was not paging to the shell. Issue1650.
- CodeContext was not importing.
- Corrected two 3.0 compatibility errors reported by Mark Summerfield:
http://mail.python.org/pipermail/python-3000/2007-December/011491.html
- Shell was not colorizing due to bug introduced at r57998, Bug 1586.
- Issue #1585: IDLE uses non-existent xrange() function.
- Windows EOL sequence not converted correctly, encoding error.
Caused file save to fail. Bug 1130.
- IDLE converted to Python 3000 syntax.
- Strings became Unicode.
- CallTips module now uses the inspect module to produce the argspec.
- IDLE modules now use absolute import instead of implied relative import.
- atexit call replaces sys.exitfunc. The functionality of delete-exitfunc flag
in config-main.cfg remains unchanged: if set, registered exit functions will
be cleared before IDLE exits.
What's New in IDLE 2.6
======================
*Release date: 01-Oct-2008*, merged into 3.0 releases detailed above (3.0rc2)
- Issue #2665: On Windows, an IDLE installation upgraded from an old version
would not start if a custom theme was defined.
- Home / Control-A toggles between left margin and end of leading white
space. issue1196903, patch by Jeff Shute.
- Improved AutoCompleteWindow logic. issue2062, patch by Tal Einat.
- Autocompletion of filenames now support alternate separators, e.g. the
'/' char on Windows. issue2061 Patch by Tal Einat.
- Configured selection highlighting colors were ignored; updating highlighting
in the config dialog would cause non-Python files to be colored as if they
were Python source; improve use of ColorDelagator. Patch 1334. Tal Einat.
- ScriptBinding event handlers weren't returning 'break'. Patch 2050, Tal Einat
- There was an error on exit if no sys.exitfunc was defined. Issue 1647.
- Could not open files in .idlerc directory if latter was hidden on Windows.
Issue 1743, Issue 1862.
- Configure Dialog: improved layout for keybinding. Patch 1457 Tal Einat.
- tabpage.py updated: tabbedPages.py now supports multiple dynamic rows
of tabs. Patch 1612746 Tal Einat.
- Add confirmation dialog before printing. Patch 1717170 Tal Einat.
- Show paste position if > 80 col. Patch 1659326 Tal Einat.
- Update cursor color without restarting. Patch 1725576 Tal Einat.
- Allow keyboard interrupt only when user code is executing in subprocess.
Patch 1225 Tal Einat (reworked from IDLE-Spoon).
- configDialog cleanup. Patch 1730217 Tal Einat.
- textView cleanup. Patch 1718043 Tal Einat.
- Clean up EditorWindow close.
- Patch 1693258: Fix for duplicate "preferences" menu-OS X. Backport of r56204.
- OSX: Avoid crash for those versions of Tcl/Tk which don't have a console
- Bug in idlelib.MultiCall: Options dialog was crashing IDLE if there was an
option in config-extensions w/o a value. Patch #1672481, Tal Einat
- Corrected some bugs in AutoComplete. Also, Page Up/Down in ACW implemented;
mouse and cursor selection in ACWindow implemented; double Tab inserts
current selection and closes ACW (similar to double-click and Return); scroll
wheel now works in ACW. Added AutoComplete instructions to IDLE Help.
- AutoCompleteWindow moved below input line, will move above if there
isn't enough space. Patch 1621265 Tal Einat
- Calltips now 'handle' tuples in the argument list (display '<tuple>' :)
Suggested solution by Christos Georgiou, Bug 791968.
- Add 'raw' support to configHandler. Patch 1650174 Tal Einat.
- Avoid hang when encountering a duplicate in a completion list. Bug 1571112.
- Patch #1362975: Rework CodeContext indentation algorithm to
avoid hard-coding pixel widths.
- Bug #813342: Start the IDLE subprocess with -Qnew if the parent
is started with that option.
- Honor the "Cancel" action in the save dialog (Debian bug #299092)
- Some syntax errors were being caught by tokenize during the tabnanny
check, resulting in obscure error messages. Do the syntax check
first. Bug 1562716, 1562719
- IDLE's version number takes a big jump to match the version number of
the Python release of which it's a part.
What's New in IDLE 1.2?
=======================
*Release date: 19-SEP-2006*
- File menu hotkeys: there were three 'p' assignments. Reassign the
'Save Copy As' and 'Print' hotkeys to 'y' and 't'. Change the
Shell hotkey from 's' to 'l'.
- IDLE honors new quit() and exit() commands from site.py Quitter() object.
Patch 1540892, Jim Jewett
- The 'with' statement is now a Code Context block opener.
Patch 1540851, Jim Jewett
- Retrieval of previous shell command was not always preserving indentation
(since 1.2a1) Patch 1528468 Tal Einat.
- Changing tokenize (39046) to detect dedent broke tabnanny check (since 1.2a1)
- ToggleTab dialog was setting indent to 8 even if cancelled (since 1.2a1).
- When used w/o subprocess, all exceptions were preceded by an error
message claiming they were IDLE internal errors (since 1.2a1).
- Bug #1525817: Don't truncate short lines in IDLE's tool tips.
- Bug #1517990: IDLE keybindings on MacOS X now work correctly
- Bug #1517996: IDLE now longer shows the default Tk menu when a
path browser, class browser or debugger is the frontmost window on MacOS X
- EditorWindow.test() was failing. Bug 1417598
- EditorWindow failed when used stand-alone if sys.ps1 not set.
Bug 1010370 Dave Florek
- Tooltips failed on new-syle class __init__ args. Bug 1027566 Loren Guthrie
- Avoid occasional failure to detect closing paren properly.
Patch 1407280 Tal Einat
- Rebinding Tab key was inserting 'tab' instead of 'Tab'. Bug 1179168.
- Colorizer now handles #<builtin> correctly, also unicode strings and
'as' keyword in comment directly following import command. Closes 1325071.
Patch 1479219 Tal Einat
- Patch #1162825: Support non-ASCII characters in IDLE window titles.
- Source file f.flush() after writing; trying to avoid lossage if user
kills GUI.
- Options / Keys / Advanced dialog made functional. Also, allow binding
of 'movement' keys.
- 'syntax' patch adds improved calltips and a new class attribute listbox.
MultiCall module allows binding multiple actions to an event.
Patch 906702 Noam Raphael
- Better indentation after first line of string continuation.
IDLEfork Patch 681992, Noam Raphael
- Fixed CodeContext alignment problem, following suggestion from Tal Einat.
- Increased performance in CodeContext extension Patch 936169 Noam Raphael
- Mac line endings were incorrect when pasting code from some browsers
when using X11 and the Fink distribution. Python Bug 1263656.
- <Enter> when cursor is on a previous command retrieves that command. Instead
of replacing the input line, the previous command is now appended to the
input line. Indentation is preserved, and undo is enabled.
Patch 1196917 Jeff Shute
- Clarify "tab/space" Error Dialog and "Tab Width" Dialog associated with
the Untabify command.
- Corrected "tab/space" Error Dialog to show correct menu for Untabify.
Patch 1196980 Jeff Shute
- New files are colorized by default, and colorizing is removed when
saving as non-Python files. Patch 1196895 Jeff Shute
Closes Python Bugs 775012 and 800432, partial fix IDLEfork 763524
- Improve subprocess link error notification.
- run.py: use Queue's blocking feature instead of sleeping in the main
loop. Patch # 1190163 Michiel de Hoon
- Add config-main option to make the 'history' feature non-cyclic.
Default remains cyclic. Python Patch 914546 Noam Raphael.
- Removed ability to configure tabs indent from Options dialog. This 'feature'
has never worked and no one has complained. It is still possible to set a
default tabs (v. spaces) indent 'manually' via config-main.def (or to turn on
tabs for the current EditorWindow via the Format menu) but IDLE will
encourage indentation via spaces.
- Enable setting the indentation width using the Options dialog.
Bug # 783877
- Add keybindings for del-word-left and del-word-right.
- Discourage using an indent width other than 8 when using tabs to indent
Python code.
- Restore use of EditorWindow.set_indentation_params(), was dead code since
Autoindent was merged into EditorWindow. This allows IDLE to conform to the
indentation width of a loaded file. (But it still will not switch to tabs
even if the file uses tabs.) Any change in indent width is local to that
window.
- Add Tabnanny check before Run/F5, not just when Checking module.
- If an extension can't be loaded, print warning and skip it instead of
erroring out.
- Improve error handling when .idlerc can't be created (warn and exit).
- The GUI was hanging if the shell window was closed while a raw_input()
was pending. Restored the quit() of the readline() mainloop().
http://mail.python.org/pipermail/idle-dev/2004-December/002307.html
- The remote procedure call module rpc.py can now access data attributes of
remote registered objects. Changes to these attributes are local, however.
What's New in IDLE 1.1?
=======================
*Release date: 30-NOV-2004*
- On OpenBSD, terminating IDLE with ctrl-c from the command line caused a
stuck subprocess MainThread because only the SocketThread was exiting.
- Saving a Keyset w/o making changes (by using the "Save as New Custom Key Set"
button) caused IDLE to fail on restart (no new keyset was created in
config-keys.cfg). Also true for Theme/highlights. Python Bug 1064535.
- A change to the linecache.py API caused IDLE to exit when an exception was
raised while running without the subprocess (-n switch). Python Bug 1063840.
- When paragraph reformat width was made configurable, a bug was
introduced that caused reformatting of comment blocks to ignore how
far the block was indented, effectively adding the indentation width
to the reformat width. This has been repaired, and the reformat
width is again a bound on the total width of reformatted lines.
- Improve keyboard focus binding, especially in Windows menu. Improve
window raising, especially in the Windows menu and in the debugger.
IDLEfork 763524.
- If user passes a non-existent filename on the commandline, just
open a new file, don't raise a dialog. IDLEfork 854928.
- EditorWindow.py was not finding the .chm help file on Windows. Typo
at Rev 1.54. Python Bug 990954
- checking sys.platform for substring 'win' was breaking IDLE docs on Mac
(darwin). Also, Mac Safari browser requires full file:// URIs. SF 900580.
- Redirect the warning stream to the shell during the ScriptBinding check of
user code and format the warning similarly to an exception for both that
check and for runtime warnings raised in the subprocess.
- CodeContext hint pane visibility state is now persistent across sessions.
The pane no longer appears in the shell window. Added capability to limit
extensions to shell window or editor windows. Noam Raphael addition
to Patch 936169.
- Paragraph reformat width is now a configurable parameter in the
Options GUI.
- New Extension: CodeContext. Provides block structuring hints for code
which has scrolled above an edit window. Patch 936169 Noam Raphael.
- If nulls somehow got into the strings in recent-files.lst
EditorWindow.update_recent_files_list() was failing. Python Bug 931336.
- If the normal background is changed via Configure/Highlighting, it will
update immediately, thanks to the previously mentioned patch by Nigel Rowe.
- Add a highlight theme for builtin keywords. Python Patch 805830 Nigel Rowe
This also fixed IDLEfork bug [ 693418 ] Normal text background color not
refreshed and Python bug [897872 ] Unknown color name on HP-UX
- rpc.py:SocketIO - Large modules were generating large pickles when downloaded
to the execution server. The return of the OK response from the subprocess
initialization was interfering and causing the sending socket to be not
ready. Add an IO ready test to fix this. Moved the polling IO ready test
into pollpacket().
- Fix typo in rpc.py, s/b "pickle.PicklingError" not "pickle.UnpicklingError".
- Added a Tk error dialog to run.py inform the user if the subprocess can't
connect to the user GUI process. Added a timeout to the GUI's listening
socket. Added Tk error dialogs to PyShell.py to announce a failure to bind
the port or connect to the subprocess. Clean up error handling during
connection initiation phase. This is an update of Python Patch 778323.
- Print correct exception even if source file changed since shell was
restarted. IDLEfork Patch 869012 Noam Raphael
- Keybindings with the Shift modifier now work correctly. So do bindings which
use the Space key. Limit unmodified user keybindings to the function keys.
Python Bug 775353, IDLEfork Bugs 755647, 761557
- After an exception, run.py was not setting the exception vector. Noam
Raphael suggested correcting this so pdb's postmortem pm() would work.
IDLEfork Patch 844675
- IDLE now does not fail to save the file anymore if the Tk buffer is not a
Unicode string, yet eol_convention is. Python Bugs 774680, 788378
- IDLE didn't start correctly when Python was installed in "Program Files" on
W2K and XP. Python Bugs 780451, 784183
- config-main.def documentation incorrectly referred to idle- instead of
config- filenames. SF 782759 Also added note about .idlerc location.
What's New in IDLE 1.0?
=======================
*Release date: 29-Jul-2003*
- Added a banner to the shell discussing warnings possibly raised by personal
firewall software. Added same comment to README.txt.
- Calltip error when docstring was None Python Bug 775541
- Updated extend.txt, help.txt, and config-extensions.def to correctly
reflect the current status of the configuration system. Python Bug 768469
- Fixed: Call Tip Trimming May Loop Forever. Python Patch 769142 (Daniels)
- Replaced apply(f, args, kwds) with f(*args, **kwargs) to improve performance
Python Patch 768187
- Break or continue statements outside a loop were causing IDLE crash
Python Bug 767794
- Convert Unicode strings from readline to IOBinding.encoding. Also set
sys.std{in|out|err}.encoding, for both the local and the subprocess case.
SF IDLEfork patch 682347.
- Extend AboutDialog.ViewFile() to support file encodings. Make the CREDITS
file Latin-1.
- Updated the About dialog to reflect re-integration into Python. Provide
buttons to display Python's NEWS, License, and Credits, plus additional
buttons for IDLE's README and NEWS.
- TextViewer() now has a third parameter which allows inserting text into the
viewer instead of reading from a file.
- (Created the .../Lib/idlelib directory in the Python CVS, which is a clone of
IDLEfork modified to install in the Python environment. The code in the
interrupt module has been moved to thread.interrupt_main(). )
- Printing the Shell window was failing if it was not saved first SF 748975
- When using the Search in Files dialog, if the user had a selection
highlighted in his Editor window, insert it into the dialog search field.
- The Python Shell entry was disappearing from the Windows menu.
- Update the Windows file list when a file name change occurs
- Change to File / Open Module: always pop up the dialog, using the current
selection as the default value. This is easier to use habitually.
- Avoided a problem with starting the subprocess when 'localhost' doesn't
resolve to the user's loopback interface. SF 747772
- Fixed an issue with highlighted errors never de-colorizing. SF 747677. Also
improved notification of Tabnanny Token Error.
- File / New will by default save in the directory of the Edit window from
which it was initiated. SF 748973 Guido van Rossum patch.
What's New in IDLEfork 0.9b1?
=============================
*Release date: 02-Jun-2003*
- The current working directory of the execution environment (and shell
following completion of execution) is now that of the module being run.
- Added the delete-exitfunc option to config-main.def. (This option is not
included in the Options dialog.) Setting this to True (the default) will
cause IDLE to not run sys.exitfunc/atexit when the subprocess exits.
- IDLE now preserves the line ending codes when editing a file produced on
a different platform. SF 661759, SF 538584
- Reduced default editor font size to 10 point and increased window height
to provide a better initial impression on Windows.
- Options / Fonts/Tabs / Set Base Editor Font: List box was not highlighting
the default font when first installed on Windows. SF 661676
- Added Autosave feature: when user runs code from edit window, if the file
has been modified IDLE will silently save it if Autosave is enabled. The
option is set in the Options dialog, and the default is to prompt the
user to save the file. SF 661318 Bruce Sherwood patch.
- Improved the RESTART annotation in the shell window when the user restarts
the shell while it is generating output. Also improved annotation when user
repeatedly hammers the Ctrl-F6 restart.
- Allow IDLE to run when not installed and cwd is not the IDLE directory
SF Patch 686254 "Run IDLEfork from any directory without set-up" - Raphael
- When a module is run from an EditorWindow: if its directory is not in
sys.path, prepend it. This allows the module to import other modules in
the same directory. Do the same for a script run from the command line.
- Correctly restart the subprocess if it is running user code and the user
attempts to run some other module or restarts the shell. Do the same if
the link is broken and it is possible to restart the subprocess and re-
connect to the GUI. SF RFE 661321.
- Improved exception reporting when running commands or scripts from the
command line.
- Added a -n command line switch to start IDLE without the subprocess.
Removed the Shell menu when running in that mode. Updated help messages.
- Added a comment to the shell startup header to indicate when IDLE is not
using the subprocess.
- Restore the ability to run without the subprocess. This can be important for
some platforms or configurations. (Running without the subprocess allows the
debugger to trace through parts of IDLE itself, which may or may not be
desirable, depending on your point of view. In addition, the traditional
reload/import tricks must be use if user source code is changed.) This is
helpful for developing IDLE using IDLE, because one instance can be used to
edit the code and a separate instance run to test changes. (Multiple
concurrent IDLE instances with subprocesses is a future feature)
- Improve the error message a user gets when saving a file with non-ASCII
characters and no source encoding is specified. Done by adding a dialog
'EncodingMessage', which contains the line to add in a fixed-font entry
widget, and which has a button to add that line to the file automatically.
Also, add a configuration option 'EditorWindow/encoding', which has three
possible values: none, utf-8, and locale. None is the default: IDLE will show
this dialog when non-ASCII characters are encountered. utf-8 means that files
with non-ASCII characters are saved as utf-8-with-bom. locale means that
files are saved in the locale's encoding; the dialog is only displayed if the
source contains characters outside the locale's charset. SF 710733 - Loewis
- Improved I/O response by tweaking the wait parameter in various
calls to signal.signal().
- Implemented a threaded subprocess which allows interrupting a pass
loop in user code using the 'interrupt' extension. User code runs
in MainThread, while the RPCServer is handled by SockThread. This is
necessary because Windows doesn't support signals.
- Implemented the 'interrupt' extension module, which allows a subthread
to raise a KeyboardInterrupt in the main thread.
- Attempting to save the shell raised an error related to saving
breakpoints, which are not implemented in the shell
- Provide a correct message when 'exit' or 'quit' are entered at the
IDLE command prompt SF 695861
- Eliminate extra blank line in shell output caused by not flushing
stdout when user code ends with an unterminated print. SF 695861
- Moved responsibility for exception formatting (i.e. pruning IDLE internal
calls) out of rpc.py into the client and server.
- Exit IDLE cleanly even when doing subprocess I/O
- Handle subprocess interrupt with an RPC message.
- Restart the subprocess if it terminates itself. (VPython programs do that)
- Support subclassing of exceptions, including in the shell, by moving the
exception formatting to the subprocess.
What's New in IDLEfork 0.9 Alpha 2?
===================================
*Release date: 27-Jan-2003*
- Updated INSTALL.txt to claify use of the python2 rpm.
- Improved formatting in IDLE Help.
- Run menu: Replace "Run Script" with "Run Module".
- Code encountering an unhandled exception under the debugger now shows
the correct traceback, with IDLE internal levels pruned out.
- If an exception occurs entirely in IDLE, don't prune the IDLE internal
modules from the traceback displayed.
- Class Browser and Path Browser now use Alt-Key-2 for vertical zoom.
- IDLE icons will now install correctly even when setup.py is run from the
build directory
- Class Browser now compatible with Python2.3 version of pyclbr.py
- Left cursor move in presence of selected text now moves from left end
of the selection.
- Add Meta keybindings to "IDLE Classic Windows" to handle reversed
Alt/Meta on some Linux distros.
- Change default: IDLE now starts with Python Shell.
- Removed the File Path from the Additional Help Sources scrolled list.
- Add capability to access Additional Help Sources on the web if the
Help File Path begins with //http or www. (Otherwise local path is
validated, as before.)
- Additional Help Sources were not being posted on the Help menu in the
order entered. Implement sorting the list by [HelpFiles] 'option'
number.
- Add Browse button to New Help Source dialog. Arrange to start in
Python/Doc if platform is Windows, otherwise start in current directory.
- Put the Additional Help Sources directly on the Help menu instead of in
an Extra Help cascade menu. Rearrange the Help menu so the Additional
Help Sources come last. Update help.txt appropriately.
- Fix Tk root pop-ups in configSectionNameDialog.py and configDialog.py
- Uniform capitalization in General tab of ConfigDialog, update the doc string.
- Fix bug in ConfigDialog where SaveAllChangedConfig() was unexpectedly
deleting Additional Help Sources from the user's config file.
- Make configHelpSourceEdit OK button the default and bind <Return>
- Fix Tk root pop-ups in configHelpSourceEdit: error dialogs not attached
to parents.
- Use os.startfile() to open both Additional Help and Python Help on the
Windows platform. The application associated with the file type will act as
the viewer. Windows help files (.chm) are now supported via the
Settings/General/Additional Help facility.
- If Python Help files are installed locally on Linux, use them instead of
accessing python.org.
- Make the methods for finding the Python help docs more robust, and make
them work in the installed configuration, also.
- On the Save Before Run dialog, make the OK button the default. One
less mouse action!
- Add a method: EditorWindow.get_geometry() for future use in implementing
window location persistence.
- Removed the "Help/Advice" menu entry. Thanks, David! We'll remember!
- Change the "Classic Windows" theme's paste key to be <ctrl-v>.
- Rearrange the Shell menu to put Stack Viewer entries adjacent.
- Add the ability to restart the subprocess interpreter from the shell window;
add an associated menu entry "Shell/Restart" with binding Control-F6. Update
IDLE help.
- Upon a restart, annotate the shell window with a "restart boundary". Add a
shell window menu "Shell/View Restart" with binding F6 to jump to the most
recent restart boundary.
- Add Shell menu to Python Shell; change "Settings" to "Options".
- Remove incorrect comment in setup.py: IDLEfork is now installed as a package.
- Add INSTALL.txt, HISTORY.txt, NEWS.txt to installed configuration.
- In installer text, fix reference to Visual Python, should be VPython.
Properly credit David Scherer.
- Modified idle, idle.py, idle.pyw to improve exception handling.
What's New in IDLEfork 0.9 Alpha 1?
===================================
*Release date: 31-Dec-2002*
- First release of major new functionality. For further details refer to
Idle-dev and/or the Sourceforge CVS.
- Adapted to the Mac platform.
- Overhauled the IDLE startup options and revised the idle -h help message,
which provides details of command line usage.
- Multiple bug fixes and usability enhancements.
- Introduced the new RPC implementation, which includes a debugger. The output
of user code is to the shell, and the shell may be used to inspect the
environment after the run has finished. (In version 0.8.1 the shell
environment was separate from the environment of the user code.)
- Introduced the configuration GUI and a new About dialog.
- Removed David Scherer's Remote Procedure Call code and replaced with Guido
van Rossum's. GvR code has support for the IDLE debugger and uses the shell
to inspect the environment of code Run from an Edit window. Files removed:
ExecBinding.py, loader.py, protocol.py, Remote.py, spawn.py
--------------------------------------------------------------------
Refer to HISTORY.txt for additional information on earlier releases.
--------------------------------------------------------------------
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,290 @@
README.txt: an index to idlelib files and the IDLE menu.
IDLE is Python's Integrated Development and Learning
Environment. The user documentation is part of the Library Reference and
is available in IDLE by selecting Help => IDLE Help. This README documents
idlelib for IDLE developers and curious users.
IDLELIB FILES lists files alphabetically by category,
with a short description of each.
IDLE MENU show the menu tree, annotated with the module
or module object that implements the corresponding function.
This file is descriptive, not prescriptive, and may have errors
and omissions and lag behind changes in idlelib.
IDLELIB FILES
=============
Implementation files not in IDLE MENU are marked (nim).
Startup
-------
__init__.py # import, does nothing
__main__.py # -m, starts IDLE
idle.bat
idle.py
idle.pyw
Implementation
--------------
autocomplete.py # Complete attribute names or filenames.
autocomplete_w.py # Display completions.
autoexpand.py # Expand word with previous word in file.
browser.py # Create module browser window.
calltip.py # Create calltip text.
calltip_w.py # Display calltip.
codecontext.py # Show compound statement headers otherwise not visible.
colorizer.py # Colorize text (nim).
config.py # Load, fetch, and save configuration (nim).
configdialog.py # Display user configuration dialogs.
config_key.py # Change keybindings.
debugger.py # Debug code run from shell or editor; show window.
debugger_r.py # Debug code run in remote process.
debugobj.py # Define class used in stackviewer.
debugobj_r.py # Communicate objects between processes with rpc (nim).
delegator.py # Define base class for delegators (nim).
dynoption.py # Define mutable OptionMenu widget (nim)
editor.py # Define most of editor and utility functions.
filelist.py # Open files and manage list of open windows (nim).
format.py # Define format menu options.
grep.py # Find all occurrences of pattern in multiple files.
help.py # Display IDLE's html doc.
help_about.py # Display About IDLE dialog.
history.py # Get previous or next user input in shell (nim)
hyperparser.py # Parse code around a given index.
iomenu.py # Open, read, and write files
macosx.py # Help IDLE run on Macs (nim).
mainmenu.py # Define most of IDLE menu.
multicall.py # Wrap tk widget to allow multiple calls per event (nim).
outwin.py # Create window for grep output.
parenmatch.py # Match fenceposts: (), [], and {}.
pathbrowser.py # Create path browser window.
percolator.py # Manage delegator stack (nim).
pyparse.py # Give information on code indentation
pyshell.py # Start IDLE, manage shell, complete editor window
query.py # Query user for information
redirector.py # Intercept widget subcommands (for percolator) (nim).
replace.py # Search and replace pattern in text.
rpc.py # Communicate between idle and user processes (nim).
run.py # Manage user code execution subprocess.
runscript.py # Check and run user code.
scrolledlist.py # Define scrolledlist widget for IDLE (nim).
search.py # Search for pattern in text.
searchbase.py # Define base for search, replace, and grep dialogs.
searchengine.py # Define engine for all 3 search dialogs.
sidebar.py # Define line number and shell prompt sidebars.
squeezer.py # Squeeze long shell output (nim).
stackviewer.py # View stack after exception.
statusbar.py # Define status bar for windows (nim).
tabbedpages.py # Define tabbed pages widget (nim).
textview.py # Define read-only text widget (nim).
tooltip.py # Define popups for calltips, squeezer (nim).
tree.py # Define tree widget, used in browsers (nim).
undo.py # Manage undo stack.
util.py # Define common objects imported elsewhere (nim).
windows.py # Manage window list and define listed top level.
zoomheight.py # Zoom window to full height of screen.
zzdummy.py # Example extension.
Configuration
-------------
config-extensions.def # Defaults for extensions
config-highlight.def # Defaults for colorizing
config-keys.def # Defaults for key bindings
config-main.def # Defaults for font and general tabs
Text
----
CREDITS.txt # not maintained, displayed by About IDLE
HISTORY.txt # NEWS up to July 2001
NEWS.txt # commits, displayed by About IDLE
NEWS2.txt # commits to Python2
README.txt # this file, displayed by About IDLE
TODO.txt # needs review
extend.txt # about writing extensions
help.html # copy of idle.html in docs, displayed by IDLE Help
Subdirectories
--------------
Icons # small image files
idle_test # files for human test and automated unit tests
IDLE MENUS
==========
Top level items and most submenu items are defined in mainmenu.
Extensions add submenu items when active. The names given are
found, quoted, in one of these modules, paired with a '<<pseudoevent>>'.
Each pseudoevent is bound to an event handler. Some event handlers
call another function that does the actual work. The annotations below
are intended to at least give the module where the actual work is done.
'eEW' = editor.EditorWindow
File
New File # eEW.new_callback
Open... # iomenu.open
Open Module # eEw.open_module
Recent Files
Class Browser # eEW.open_class_browser, browser.ClassBrowser
Path Browser # eEW.open_path_browser, pathbrowser
---
Save # iomenu.save
Save As... # iomenu.save_as
Save Copy As... # iomenu.save_a_copy
---
Print Window # iomenu.print_window
---
Close # eEW.close_event
Exit # flist.close_all_callback (bound in eEW)
Edit
Undo # undodelegator
Redo # undodelegator
--- # eEW.right_menu_event
Cut # eEW.cut
Copy # eEW.copy
Paste # eEW.past
Select All # eEW.select_all (+ see eEW.remove_selection)
--- # Next 5 items use searchengine; dialogs use searchbase
Find # eEW.find_event, search.SearchDialog.find
Find Again # eEW.find_again_event, sSD.find_again
Find Selection # eEW.find_selection_event, sSD.find_selection
Find in Files... # eEW.find_in_files_event, grep
Replace... # eEW.replace_event, replace.ReplaceDialog.replace
Go to Line # eEW.goto_line_event
Show Completions # autocomplete extension and autocompleteWidow (&HP)
Expand Word # autoexpand extension
Show call tip # Calltips extension and CalltipWindow (& Hyperparser)
Show surrounding parens # parenmatch (& Hyperparser)
Format (Editor only) [fFR = format.FormatRegion]
Format Paragraph # format.FormatParagraph.format_paragraph_event
Indent Region # fFR.indent_region_event
Dedent Region # fFR.dedent_region_event
Comment Out Reg. # fFR.comment_region_event
Uncomment Region # fFR.uncomment_region_event
Tabify Region # fFR.tabify_region_event
Untabify Region # fFR.untabify_region_event
Toggle Tabs # format.Indents.toggle_tabs_event
New Indent Width # format.Indents.change_indentwidth_event
Strip tailing whitespace # format.rstrip
Zin # zzdummy
Zout # zzdummy
Run (Editor only)
Run Module # runscript.ScriptBinding.run_module_event
Run... Customized # runscript.ScriptBinding.run_custom_event
Check Module # runscript.ScriptBinding.check_module_event
Python Shell # pyshell.Pyshell, pyshell.ModifiedInterpreter
Shell # pyshell
View Last Restart # pyshell.PyShell.view_restart_mark
Restart Shell # pyshell.PyShell.restart_shell
Previous History # history.History.history_prev
Next History # history.History.history_next
Interrupt Execution # pyshell.PyShell.cancel_callback
Debug (Shell only)
Go to File/Line # outwin.OutputWindow.goto_file_line
debugger # debugger, debugger_r, PyShell.toggle_debugger
Stack Viewer # stackviewer, PyShell.open_stack_viewer
Auto-open Stack Viewer # stackviewer
Options
Configure IDLE # eEW.config_dialog, config, configdialog (cd)
(Parts of the dialog)
Buttons # cd.ConfigDialog
Font tab # cd.FontPage, config-main.def
Highlight tab # cd.HighPage, query, config-highlight.def
Keys tab # cd.KeysPage, query, config_key, config_keys.def
Windows tab # cd.WinPage, config_main.def
Shell/Ed tab # cd.ShedPage, config-main.def
Extensions tab # config-extensions.def, corresponding .py files
---
... Code Context # codecontext
... Line Numbers # sidebar
Zoomheight # zoomheight
Window
<open windows> # windows
Help
About IDLE # eEW.about_dialog, help_about.AboutDialog
---
IDLE Help # eEW.help_dialog, help.show_idlehelp
Python Docs # eEW.python_docs
Turtle Demo # eEW.open_turtle_demo
---
<other help sources>
<Context Menu> (right click)
Defined in editor, PyShell.pyshell
Cut
Copy
Paste
---
Go to file/line (shell and output only)
Set Breakpoint (editor only)
Clear Breakpoint (editor only)
Defined in debugger
Go to source line
Show stack frame
<No menu>
Center Insert # eEW.center_insert_event
OTHER TOPICS
============
Generally use PEP 8.
import statements
-----------------
Put imports at the top, unless there is a good reason otherwise.
PEP 8 says to group stdlib, 3rd-party dependencies, and package imports.
For idlelib, the groups are general stdlib, tkinter, and idlelib.
Sort modules within each group, except that tkinter.ttk follows tkinter.
Sort 'from idlelib import mod1' and 'from idlelib.mod2 import object'
together by module, ignoring within module objects.
Put 'import __main__' after other idlelib imports.
Imports only needed for testing are put not at the top but in an
htest function def or "if __name__ == '__main__'" clause.
Within module imports like "from idlelib.mod import class" may cause
circular imports to deadlock. Even without this, circular imports may
require at least one of the imports to be delayed until a function call.
What's New entries
------------------
Repository directory Doc/whatsnew/ has a file 3.n.rst for each 3.n
Python version. For the first entry in each file, add subsection
'IDLE and idlelib', in alphabetical position, to the 'Improved Modules'
section. For the rest of cpython, entries to 3.(n+1).rst begin with
the release of 3.n.0b1. For IDLE, entries for features backported from
'main' to '3.n' during its beta period do not got in 3.(n+1).rst. The
latter usually gets its first entry during the 3.n.0 candidate period
or after the 3.n.0 release.
When, as per PEP 434, feature changes are backported, entries are placed
in the 3.n.rst file *in the main branch* for each Python version n that
gets the backport. (Note: the format of entries have varied between
versions.) Add a line "New in 3.n maintenance releases." before the
first back-ported feature after 3.n.0 is released. Since each older
version file gets a different number of backports, it is easiest to
make a separate PR for each file and label it with the backports
needed.
Github repository and issues
----------------------------
The CPython repository is https://github.com/python/cpython. The
IDLE Issues listing is https://github.com/orgs/python/projects/31.
The main classification is by Topic, based on the IDLE menu. View the
topics list by clicking the [<]] button in the upper right.
@@ -0,0 +1,210 @@
Original IDLE todo, much of it now outdated:
============================================
TO DO:
- improve debugger:
- manage breakpoints globally, allow bp deletion, tbreak, cbreak etc.
- real object browser
- help on how to use it (a simple help button will do wonders)
- performance? (updates of large sets of locals are slow)
- better integration of "debug module"
- debugger should be global resource (attached to flist, not to shell)
- fix the stupid bug where you need to step twice
- display class name in stack viewer entries for methods
- suppress tracing through IDLE internals (e.g. print) DONE
- add a button to suppress through a specific module or class or method
- more object inspection to stack viewer, e.g. to view all array items
- insert the initial current directory into sys.path DONE
- default directory attribute for each window instead of only for windows
that have an associated filename
- command expansion from keywords, module contents, other buffers, etc.
- "Recent documents" menu item DONE
- Filter region command
- Optional horizontal scroll bar
- more Emacsisms:
- ^K should cut to buffer
- M-[, M-] to move by paragraphs
- incremental search?
- search should indicate wrap-around in some way
- restructure state sensitive code to avoid testing flags all the time
- persistent user state (e.g. window and cursor positions, bindings)
- make backups when saving
- check file mtimes at various points
- Pluggable interface with RCS/CVS/Perforce/Clearcase
- better help?
- don't open second class browser on same module (nor second path browser)
- unify class and path browsers
- Need to define a standard way whereby one can determine one is running
inside IDLE (needed for Tk mainloop, also handy for $PYTHONSTARTUP)
- Add more utility methods for use by extensions (a la get_selection)
- Way to run command in totally separate interpreter (fork+os.system?) DONE
- Way to find definition of fully-qualified name:
In other words, select "UserDict.UserDict", hit some magic key and
it loads up UserDict.py and finds the first def or class for UserDict.
- need a way to force colorization on/off
- need a way to force auto-indent on/off
Details:
- ^O (on Unix -- open-line) should honor autoindent
- after paste, show end of pasted text
- on Windows, should turn short filename to long filename (not only in argv!)
(shouldn't this be done -- or undone -- by ntpath.normpath?)
- new autoindent after colon even indents when the colon is in a comment!
- sometimes forward slashes in pathname remain
- sometimes star in window name remains in Windows menu
- With unix bindings, ESC by itself is ignored
- Sometimes for no apparent reason a selection from the cursor to the
end of the command buffer appears, which is hard to get rid of
because it stays when you are typing!
- The Line/Col in the status bar can be wrong initially in PyShell DONE
Structural problems:
- too much knowledge in FileList about EditorWindow (for example)
- should add some primitives for accessing the selection etc.
to repeat cumbersome code over and over
======================================================================
Jeff Bauer suggests:
- Open Module doesn't appear to handle hierarchical packages.
- Class browser should also allow hierarchical packages.
- Open and Open Module could benefit from a history, DONE
either command line style, or Microsoft recent-file
style.
- Add a Smalltalk-style inspector (i.e. Tkinspect)
The last suggestion is already a reality, but not yet
integrated into IDLE. I use a module called inspector.py,
that used to be available from python.org(?) It no longer
appears to be in the contributed section, and the source
has no author attribution.
In any case, the code is useful for visually navigating
an object's attributes, including its container hierarchy.
>>> from inspector import Tkinspect
>>> Tkinspect(None, myObject)
Tkinspect could probably be extended and refined to
integrate better into IDLE.
======================================================================
Comparison to PTUI
------------------
+ PTUI's help is better (HTML!)
+ PTUI can attach a shell to any module
+ PTUI has some more I/O commands:
open multiple
append
examine (what's that?)
======================================================================
Notes after trying to run Grail
-------------------------------
- Grail does stuff to sys.path based on sys.argv[0]; you must set
sys.argv[0] to something decent first (it is normally set to the path of
the idle script).
- Grail must be exec'ed in __main__ because that's imported by some
other parts of Grail.
- Grail uses a module called History and so does idle :-(
======================================================================
Robin Friedrich's items:
Things I'd like to see:
- I'd like support for shift-click extending the selection. There's a
bug now that it doesn't work the first time you try it.
- Printing is needed. How hard can that be on Windows? FIRST CUT DONE
- The python-mode trick of autoindenting a line with <tab> is neat and
very handy.
- (someday) a spellchecker for docstrings and comments.
- a pagedown/up command key which moves to next class/def statement (top
level)
- split window capability
- DnD text relocation/copying
Things I don't want to see.
- line numbers... will probably slow things down way too much.
- Please use another icon for the tree browser leaf. The small snake
isn't cutting it.
----------------------------------------------------------------------
- Customizable views (multi-window or multi-pane). (Markus Gritsch)
- Being able to double click (maybe double right click) on a callable
object in the editor which shows the source of the object, if
possible. (Gerrit Holl)
- Hooks into the guts, like in Emacs. (Mike Romberg)
- Sharing the editor with a remote tutor. (Martijn Faassen)
- Multiple views on the same file. (Tony J Ibbs)
- Store breakpoints in a global (per-project) database (GvR); Dirk
Heise adds: save some space-trimmed context and search around when
reopening a file that might have been edited by someone else.
- Capture menu events in extensions without changing the IDLE source.
(Matthias Barmeier)
- Use overlapping panels (a "notebook" in MFC terms I think) for info
that doesn't need to be accessible simultaneously (e.g. HTML source
and output). Use multi-pane windows for info that does need to be
shown together (e.g. class browser and source). (Albert Brandl)
- A project should invisibly track all symbols, for instant search,
replace and cross-ref. Projects should be allowed to span multiple
directories, hosts, etc. Project management files are placed in a
directory you specify. A global mapping between project names and
project directories should exist [not so sure --GvR]. (Tim Peters)
- Merge attr-tips and auto-expand. (Mark Hammond, Tim Peters)
- Python Shell should behave more like a "shell window" as users know
it -- i.e. you can only edit the current command, and the cursor can't
escape from the command area. (Albert Brandl)
- Set X11 class to "idle/Idle", set icon and title to something
beginning with "idle" -- for window managers. (Randall Hopper)
- Config files editable through a preferences dialog. (me) DONE
- Config files still editable outside the preferences dialog.
(Randall Hopper) DONE
- When you're editing a command in PyShell, and there are only blank
lines below the cursor, hitting Return should ignore or delete those
blank lines rather than deciding you're not on the last line. (me)
- Run command (F5 c.s.) should be more like Pythonwin's Run -- a
dialog with options to give command line arguments, run the debugger,
etc. (me)
- Shouldn't be able to delete part of the prompt (or any text before
it) in the PyShell. (Martijn Faassen) DONE
- Emacs style auto-fill (also smart about comments and strings).
(Jeremy Hylton)
- Output of Run Script should go to a separate output window, not to
the shell window. Output of separate runs should all go to the same
window but clearly delimited. (David Scherer) REJECT FIRST, LATTER DONE
- GUI form designer to kick VB's butt. (Robert Geiger) THAT'S NOT IDLE
- Printing! Possibly via generation of PDF files which the user must
then send to the printer separately. (Dinu Gherman) FIRST CUT
@@ -0,0 +1,10 @@
"""The idlelib package implements the Idle application.
Idle includes an interactive shell and editor.
Starting with Python 3.6, IDLE requires tcl/tk 8.5 or later.
Use the files named idle.* to start Idle.
The other files are private implementations. Their details are subject to
change. See PEP 434 for more. Import them at your own risk.
"""
testing = False # Set True by test.test_idle.
@@ -0,0 +1,7 @@
"""
IDLE main entry point
Run IDLE as python -m idlelib
"""
import idlelib.pyshell
idlelib.pyshell.main()
@@ -0,0 +1,228 @@
"""Complete either attribute names or file names.
Either on demand or after a user-selected delay after a key character,
pop up a list of candidates.
"""
import __main__
import keyword
import os
import string
import sys
# Modified keyword list is used in fetch_completions.
completion_kwds = [s for s in keyword.kwlist
if s not in {'True', 'False', 'None'}] # In builtins.
completion_kwds.extend(('match', 'case')) # Context keywords.
completion_kwds.sort()
# Two types of completions; defined here for autocomplete_w import below.
ATTRS, FILES = 0, 1
from idlelib import autocomplete_w
from idlelib.config import idleConf
from idlelib.hyperparser import HyperParser
# Tuples passed to open_completions.
# EvalFunc, Complete, WantWin, Mode
FORCE = True, False, True, None # Control-Space.
TAB = False, True, True, None # Tab.
TRY_A = False, False, False, ATTRS # '.' for attributes.
TRY_F = False, False, False, FILES # '/' in quotes for file name.
# This string includes all chars that may be in an identifier.
# TODO Update this here and elsewhere.
ID_CHARS = string.ascii_letters + string.digits + "_"
SEPS = f"{os.sep}{os.altsep if os.altsep else ''}"
TRIGGERS = f".{SEPS}"
class AutoComplete:
def __init__(self, editwin=None, tags=None):
self.editwin = editwin
if editwin is not None: # not in subprocess or no-gui test
self.text = editwin.text
self.tags = tags
self.autocompletewindow = None
# id of delayed call, and the index of the text insert when
# the delayed call was issued. If _delayed_completion_id is
# None, there is no delayed call.
self._delayed_completion_id = None
self._delayed_completion_index = None
@classmethod
def reload(cls):
cls.popupwait = idleConf.GetOption(
"extensions", "AutoComplete", "popupwait", type="int", default=0)
def _make_autocomplete_window(self): # Makes mocking easier.
return autocomplete_w.AutoCompleteWindow(self.text, tags=self.tags)
def _remove_autocomplete_window(self, event=None):
if self.autocompletewindow:
self.autocompletewindow.hide_window()
self.autocompletewindow = None
def force_open_completions_event(self, event):
"(^space) Open completion list, even if a function call is needed."
self.open_completions(FORCE)
return "break"
def autocomplete_event(self, event):
"(tab) Complete word or open list if multiple options."
if hasattr(event, "mc_state") and event.mc_state or\
not self.text.get("insert linestart", "insert").strip():
# A modifier was pressed along with the tab or
# there is only previous whitespace on this line, so tab.
return None
if self.autocompletewindow and self.autocompletewindow.is_active():
self.autocompletewindow.complete()
return "break"
else:
opened = self.open_completions(TAB)
return "break" if opened else None
def try_open_completions_event(self, event=None):
"(./) Open completion list after pause with no movement."
lastchar = self.text.get("insert-1c")
if lastchar in TRIGGERS:
args = TRY_A if lastchar == "." else TRY_F
self._delayed_completion_index = self.text.index("insert")
if self._delayed_completion_id is not None:
self.text.after_cancel(self._delayed_completion_id)
self._delayed_completion_id = self.text.after(
self.popupwait, self._delayed_open_completions, args)
def _delayed_open_completions(self, args):
"Call open_completions if index unchanged."
self._delayed_completion_id = None
if self.text.index("insert") == self._delayed_completion_index:
self.open_completions(args)
def open_completions(self, args):
"""Find the completions and create the AutoCompleteWindow.
Return True if successful (no syntax error or so found).
If complete is True, then if there's nothing to complete and no
start of completion, won't open completions and return False.
If mode is given, will open a completion list only in this mode.
"""
evalfuncs, complete, wantwin, mode = args
# Cancel another delayed call, if it exists.
if self._delayed_completion_id is not None:
self.text.after_cancel(self._delayed_completion_id)
self._delayed_completion_id = None
hp = HyperParser(self.editwin, "insert")
curline = self.text.get("insert linestart", "insert")
i = j = len(curline)
if hp.is_in_string() and (not mode or mode==FILES):
# Find the beginning of the string.
# fetch_completions will look at the file system to determine
# whether the string value constitutes an actual file name
# XXX could consider raw strings here and unescape the string
# value if it's not raw.
self._remove_autocomplete_window()
mode = FILES
# Find last separator or string start
while i and curline[i-1] not in "'\"" + SEPS:
i -= 1
comp_start = curline[i:j]
j = i
# Find string start
while i and curline[i-1] not in "'\"":
i -= 1
comp_what = curline[i:j]
elif hp.is_in_code() and (not mode or mode==ATTRS):
self._remove_autocomplete_window()
mode = ATTRS
while i and (curline[i-1] in ID_CHARS or ord(curline[i-1]) > 127):
i -= 1
comp_start = curline[i:j]
if i and curline[i-1] == '.': # Need object with attributes.
hp.set_index("insert-%dc" % (len(curline)-(i-1)))
comp_what = hp.get_expression()
if (not comp_what or
(not evalfuncs and comp_what.find('(') != -1)):
return None
else:
comp_what = ""
else:
return None
if complete and not comp_what and not comp_start:
return None
comp_lists = self.fetch_completions(comp_what, mode)
if not comp_lists[0]:
return None
self.autocompletewindow = self._make_autocomplete_window()
return not self.autocompletewindow.show_window(
comp_lists, "insert-%dc" % len(comp_start),
complete, mode, wantwin)
def fetch_completions(self, what, mode):
"""Return a pair of lists of completions for something. The first list
is a sublist of the second. Both are sorted.
If there is a Python subprocess, get the comp. list there. Otherwise,
either fetch_completions() is running in the subprocess itself or it
was called in an IDLE EditorWindow before any script had been run.
The subprocess environment is that of the most recently run script. If
two unrelated modules are being edited some calltips in the current
module may be inoperative if the module was not the last to run.
"""
try:
rpcclt = self.editwin.flist.pyshell.interp.rpcclt
except:
rpcclt = None
if rpcclt:
return rpcclt.remotecall("exec", "get_the_completion_list",
(what, mode), {})
else:
if mode == ATTRS:
if what == "": # Main module names.
namespace = {**__main__.__builtins__.__dict__,
**__main__.__dict__}
bigl = eval("dir()", namespace)
bigl.extend(completion_kwds)
bigl.sort()
if "__all__" in bigl:
smalll = sorted(eval("__all__", namespace))
else:
smalll = [s for s in bigl if s[:1] != '_']
else:
try:
entity = self.get_entity(what)
bigl = dir(entity)
bigl.sort()
if "__all__" in bigl:
smalll = sorted(entity.__all__)
else:
smalll = [s for s in bigl if s[:1] != '_']
except:
return [], []
elif mode == FILES:
if what == "":
what = "."
try:
expandedpath = os.path.expanduser(what)
bigl = os.listdir(expandedpath)
bigl.sort()
smalll = [s for s in bigl if s[:1] != '.']
except OSError:
return [], []
if not smalll:
smalll = bigl
return smalll, bigl
def get_entity(self, name):
"Lookup name in a namespace spanning sys.modules and __main.dict__."
return eval(name, {**sys.modules, **__main__.__dict__})
AutoComplete.reload()
if __name__ == '__main__':
from unittest import main
main('idlelib.idle_test.test_autocomplete', verbosity=2)
@@ -0,0 +1,498 @@
"""
An auto-completion window for IDLE, used by the autocomplete extension
"""
import platform
from tkinter import *
from tkinter.ttk import Scrollbar
from idlelib.autocomplete import FILES, ATTRS
from idlelib.multicall import MC_SHIFT
HIDE_VIRTUAL_EVENT_NAME = "<<autocompletewindow-hide>>"
HIDE_FOCUS_OUT_SEQUENCE = "<FocusOut>"
HIDE_SEQUENCES = (HIDE_FOCUS_OUT_SEQUENCE, "<ButtonPress>")
KEYPRESS_VIRTUAL_EVENT_NAME = "<<autocompletewindow-keypress>>"
# We need to bind event beyond <Key> so that the function will be called
# before the default specific IDLE function
KEYPRESS_SEQUENCES = ("<Key>", "<Key-BackSpace>", "<Key-Return>", "<Key-Tab>",
"<Key-Up>", "<Key-Down>", "<Key-Home>", "<Key-End>",
"<Key-Prior>", "<Key-Next>", "<Key-Escape>")
KEYRELEASE_VIRTUAL_EVENT_NAME = "<<autocompletewindow-keyrelease>>"
KEYRELEASE_SEQUENCE = "<KeyRelease>"
LISTUPDATE_SEQUENCE = "<B1-ButtonRelease>"
WINCONFIG_SEQUENCE = "<Configure>"
DOUBLECLICK_SEQUENCE = "<B1-Double-ButtonRelease>"
class AutoCompleteWindow:
def __init__(self, widget, tags):
# The widget (Text) on which we place the AutoCompleteWindow
self.widget = widget
# Tags to mark inserted text with
self.tags = tags
# The widgets we create
self.autocompletewindow = self.listbox = self.scrollbar = None
# The default foreground and background of a selection. Saved because
# they are changed to the regular colors of list items when the
# completion start is not a prefix of the selected completion
self.origselforeground = self.origselbackground = None
# The list of completions
self.completions = None
# A list with more completions, or None
self.morecompletions = None
# The completion mode, either autocomplete.ATTRS or .FILES.
self.mode = None
# The current completion start, on the text box (a string)
self.start = None
# The index of the start of the completion
self.startindex = None
# The last typed start, used so that when the selection changes,
# the new start will be as close as possible to the last typed one.
self.lasttypedstart = None
# Do we have an indication that the user wants the completion window
# (for example, he clicked the list)
self.userwantswindow = None
# event ids
self.hideid = self.keypressid = self.listupdateid = \
self.winconfigid = self.keyreleaseid = self.doubleclickid = None
# Flag set if last keypress was a tab
self.lastkey_was_tab = False
# Flag set to avoid recursive <Configure> callback invocations.
self.is_configuring = False
def _change_start(self, newstart):
min_len = min(len(self.start), len(newstart))
i = 0
while i < min_len and self.start[i] == newstart[i]:
i += 1
if i < len(self.start):
self.widget.delete("%s+%dc" % (self.startindex, i),
"%s+%dc" % (self.startindex, len(self.start)))
if i < len(newstart):
self.widget.insert("%s+%dc" % (self.startindex, i),
newstart[i:],
self.tags)
self.start = newstart
def _binary_search(self, s):
"""Find the first index in self.completions where completions[i] is
greater or equal to s, or the last index if there is no such.
"""
i = 0; j = len(self.completions)
while j > i:
m = (i + j) // 2
if self.completions[m] >= s:
j = m
else:
i = m + 1
return min(i, len(self.completions)-1)
def _complete_string(self, s):
"""Assuming that s is the prefix of a string in self.completions,
return the longest string which is a prefix of all the strings which
s is a prefix of them. If s is not a prefix of a string, return s.
"""
first = self._binary_search(s)
if self.completions[first][:len(s)] != s:
# There is not even one completion which s is a prefix of.
return s
# Find the end of the range of completions where s is a prefix of.
i = first + 1
j = len(self.completions)
while j > i:
m = (i + j) // 2
if self.completions[m][:len(s)] != s:
j = m
else:
i = m + 1
last = i-1
if first == last: # only one possible completion
return self.completions[first]
# We should return the maximum prefix of first and last
first_comp = self.completions[first]
last_comp = self.completions[last]
min_len = min(len(first_comp), len(last_comp))
i = len(s)
while i < min_len and first_comp[i] == last_comp[i]:
i += 1
return first_comp[:i]
def _selection_changed(self):
"""Call when the selection of the Listbox has changed.
Updates the Listbox display and calls _change_start.
"""
cursel = int(self.listbox.curselection()[0])
self.listbox.see(cursel)
lts = self.lasttypedstart
selstart = self.completions[cursel]
if self._binary_search(lts) == cursel:
newstart = lts
else:
min_len = min(len(lts), len(selstart))
i = 0
while i < min_len and lts[i] == selstart[i]:
i += 1
newstart = selstart[:i]
self._change_start(newstart)
if self.completions[cursel][:len(self.start)] == self.start:
# start is a prefix of the selected completion
self.listbox.configure(selectbackground=self.origselbackground,
selectforeground=self.origselforeground)
else:
self.listbox.configure(selectbackground=self.listbox.cget("bg"),
selectforeground=self.listbox.cget("fg"))
# If there are more completions, show them, and call me again.
if self.morecompletions:
self.completions = self.morecompletions
self.morecompletions = None
self.listbox.delete(0, END)
for item in self.completions:
self.listbox.insert(END, item)
self.listbox.select_set(self._binary_search(self.start))
self._selection_changed()
def show_window(self, comp_lists, index, complete, mode, userWantsWin):
"""Show the autocomplete list, bind events.
If complete is True, complete the text, and if there is exactly
one matching completion, don't open a list.
"""
# Handle the start we already have
self.completions, self.morecompletions = comp_lists
self.mode = mode
self.startindex = self.widget.index(index)
self.start = self.widget.get(self.startindex, "insert")
if complete:
completed = self._complete_string(self.start)
start = self.start
self._change_start(completed)
i = self._binary_search(completed)
if self.completions[i] == completed and \
(i == len(self.completions)-1 or
self.completions[i+1][:len(completed)] != completed):
# There is exactly one matching completion
return completed == start
self.userwantswindow = userWantsWin
self.lasttypedstart = self.start
self.autocompletewindow = acw = Toplevel(self.widget)
acw.withdraw()
acw.wm_overrideredirect(1)
try:
# Prevent grabbing focus on macOS.
acw.tk.call("::tk::unsupported::MacWindowStyle", "style", acw._w,
"help", "noActivates")
except TclError:
pass
self.scrollbar = scrollbar = Scrollbar(acw, orient=VERTICAL)
self.listbox = listbox = Listbox(acw, yscrollcommand=scrollbar.set,
exportselection=False)
for item in self.completions:
listbox.insert(END, item)
self.origselforeground = listbox.cget("selectforeground")
self.origselbackground = listbox.cget("selectbackground")
scrollbar.config(command=listbox.yview)
scrollbar.pack(side=RIGHT, fill=Y)
listbox.pack(side=LEFT, fill=BOTH, expand=True)
#acw.update_idletasks() # Need for tk8.6.8 on macOS: #40128.
acw.lift() # work around bug in Tk 8.5.18+ (issue #24570)
# Initialize the listbox selection
self.listbox.select_set(self._binary_search(self.start))
self._selection_changed()
# bind events
self.hideaid = acw.bind(HIDE_VIRTUAL_EVENT_NAME, self.hide_event)
self.hidewid = self.widget.bind(HIDE_VIRTUAL_EVENT_NAME, self.hide_event)
acw.event_add(HIDE_VIRTUAL_EVENT_NAME, HIDE_FOCUS_OUT_SEQUENCE)
for seq in HIDE_SEQUENCES:
self.widget.event_add(HIDE_VIRTUAL_EVENT_NAME, seq)
self.keypressid = self.widget.bind(KEYPRESS_VIRTUAL_EVENT_NAME,
self.keypress_event)
for seq in KEYPRESS_SEQUENCES:
self.widget.event_add(KEYPRESS_VIRTUAL_EVENT_NAME, seq)
self.keyreleaseid = self.widget.bind(KEYRELEASE_VIRTUAL_EVENT_NAME,
self.keyrelease_event)
self.widget.event_add(KEYRELEASE_VIRTUAL_EVENT_NAME,KEYRELEASE_SEQUENCE)
self.listupdateid = listbox.bind(LISTUPDATE_SEQUENCE,
self.listselect_event)
self.is_configuring = False
self.winconfigid = acw.bind(WINCONFIG_SEQUENCE, self.winconfig_event)
self.doubleclickid = listbox.bind(DOUBLECLICK_SEQUENCE,
self.doubleclick_event)
return None
def winconfig_event(self, event):
if self.is_configuring:
# Avoid running on recursive <Configure> callback invocations.
return
self.is_configuring = True
if not self.is_active():
return
# Since the <Configure> event may occur after the completion window is gone,
# catch potential TclError exceptions when accessing acw. See: bpo-41611.
try:
# Position the completion list window
text = self.widget
text.see(self.startindex)
x, y, cx, cy = text.bbox(self.startindex)
acw = self.autocompletewindow
if platform.system().startswith('Windows'):
# On Windows an update() call is needed for the completion
# list window to be created, so that we can fetch its width
# and height. However, this is not needed on other platforms
# (tested on Ubuntu and macOS) but at one point began
# causing freezes on macOS. See issues 37849 and 41611.
acw.update()
acw_width, acw_height = acw.winfo_width(), acw.winfo_height()
text_width, text_height = text.winfo_width(), text.winfo_height()
new_x = text.winfo_rootx() + min(x, max(0, text_width - acw_width))
new_y = text.winfo_rooty() + y
if (text_height - (y + cy) >= acw_height # enough height below
or y < acw_height): # not enough height above
# place acw below current line
new_y += cy
else:
# place acw above current line
new_y -= acw_height
acw.wm_geometry("+%d+%d" % (new_x, new_y))
acw.deiconify()
acw.update_idletasks()
except TclError:
pass
if platform.system().startswith('Windows'):
# See issue 15786. When on Windows platform, Tk will misbehave
# to call winconfig_event multiple times, we need to prevent this,
# otherwise mouse button double click will not be able to used.
try:
acw.unbind(WINCONFIG_SEQUENCE, self.winconfigid)
except TclError:
pass
self.winconfigid = None
self.is_configuring = False
def _hide_event_check(self):
if not self.autocompletewindow:
return
try:
if not self.autocompletewindow.focus_get():
self.hide_window()
except KeyError:
# See issue 734176, when user click on menu, acw.focus_get()
# will get KeyError.
self.hide_window()
def hide_event(self, event):
# Hide autocomplete list if it exists and does not have focus or
# mouse click on widget / text area.
if self.is_active():
if event.type == EventType.FocusOut:
# On Windows platform, it will need to delay the check for
# acw.focus_get() when click on acw, otherwise it will return
# None and close the window
self.widget.after(1, self._hide_event_check)
elif event.type == EventType.ButtonPress:
# ButtonPress event only bind to self.widget
self.hide_window()
def listselect_event(self, event):
if self.is_active():
self.userwantswindow = True
cursel = int(self.listbox.curselection()[0])
self._change_start(self.completions[cursel])
def doubleclick_event(self, event):
# Put the selected completion in the text, and close the list
cursel = int(self.listbox.curselection()[0])
self._change_start(self.completions[cursel])
self.hide_window()
def keypress_event(self, event):
if not self.is_active():
return None
keysym = event.keysym
if hasattr(event, "mc_state"):
state = event.mc_state
else:
state = 0
if keysym != "Tab":
self.lastkey_was_tab = False
if (len(keysym) == 1 or keysym in ("underscore", "BackSpace")
or (self.mode == FILES and keysym in
("period", "minus"))) \
and not (state & ~MC_SHIFT):
# Normal editing of text
if len(keysym) == 1:
self._change_start(self.start + keysym)
elif keysym == "underscore":
self._change_start(self.start + '_')
elif keysym == "period":
self._change_start(self.start + '.')
elif keysym == "minus":
self._change_start(self.start + '-')
else:
# keysym == "BackSpace"
if len(self.start) == 0:
self.hide_window()
return None
self._change_start(self.start[:-1])
self.lasttypedstart = self.start
self.listbox.select_clear(0, int(self.listbox.curselection()[0]))
self.listbox.select_set(self._binary_search(self.start))
self._selection_changed()
return "break"
elif keysym == "Return":
self.complete()
self.hide_window()
return 'break'
elif (self.mode == ATTRS and keysym in
("period", "space", "parenleft", "parenright", "bracketleft",
"bracketright")) or \
(self.mode == FILES and keysym in
("slash", "backslash", "quotedbl", "apostrophe")) \
and not (state & ~MC_SHIFT):
# If start is a prefix of the selection, but is not '' when
# completing file names, put the whole
# selected completion. Anyway, close the list.
cursel = int(self.listbox.curselection()[0])
if self.completions[cursel][:len(self.start)] == self.start \
and (self.mode == ATTRS or self.start):
self._change_start(self.completions[cursel])
self.hide_window()
return None
elif keysym in ("Home", "End", "Prior", "Next", "Up", "Down") and \
not state:
# Move the selection in the listbox
self.userwantswindow = True
cursel = int(self.listbox.curselection()[0])
if keysym == "Home":
newsel = 0
elif keysym == "End":
newsel = len(self.completions)-1
elif keysym in ("Prior", "Next"):
jump = self.listbox.nearest(self.listbox.winfo_height()) - \
self.listbox.nearest(0)
if keysym == "Prior":
newsel = max(0, cursel-jump)
else:
assert keysym == "Next"
newsel = min(len(self.completions)-1, cursel+jump)
elif keysym == "Up":
newsel = max(0, cursel-1)
else:
assert keysym == "Down"
newsel = min(len(self.completions)-1, cursel+1)
self.listbox.select_clear(cursel)
self.listbox.select_set(newsel)
self._selection_changed()
self._change_start(self.completions[newsel])
return "break"
elif (keysym == "Tab" and not state):
if self.lastkey_was_tab:
# two tabs in a row; insert current selection and close acw
cursel = int(self.listbox.curselection()[0])
self._change_start(self.completions[cursel])
self.hide_window()
return "break"
else:
# first tab; let AutoComplete handle the completion
self.userwantswindow = True
self.lastkey_was_tab = True
return None
elif any(s in keysym for s in ("Shift", "Control", "Alt",
"Meta", "Command", "Option")):
# A modifier key, so ignore
return None
elif event.char and event.char >= ' ':
# Regular character with a non-length-1 keycode
self._change_start(self.start + event.char)
self.lasttypedstart = self.start
self.listbox.select_clear(0, int(self.listbox.curselection()[0]))
self.listbox.select_set(self._binary_search(self.start))
self._selection_changed()
return "break"
else:
# Unknown event, close the window and let it through.
self.hide_window()
return None
def keyrelease_event(self, event):
if not self.is_active():
return
if self.widget.index("insert") != \
self.widget.index("%s+%dc" % (self.startindex, len(self.start))):
# If we didn't catch an event which moved the insert, close window
self.hide_window()
def is_active(self):
return self.autocompletewindow is not None
def complete(self):
self._change_start(self._complete_string(self.start))
# The selection doesn't change.
def hide_window(self):
if not self.is_active():
return
# unbind events
self.autocompletewindow.event_delete(HIDE_VIRTUAL_EVENT_NAME,
HIDE_FOCUS_OUT_SEQUENCE)
for seq in HIDE_SEQUENCES:
self.widget.event_delete(HIDE_VIRTUAL_EVENT_NAME, seq)
self.autocompletewindow.unbind(HIDE_VIRTUAL_EVENT_NAME, self.hideaid)
self.widget.unbind(HIDE_VIRTUAL_EVENT_NAME, self.hidewid)
self.hideaid = None
self.hidewid = None
for seq in KEYPRESS_SEQUENCES:
self.widget.event_delete(KEYPRESS_VIRTUAL_EVENT_NAME, seq)
self.widget.unbind(KEYPRESS_VIRTUAL_EVENT_NAME, self.keypressid)
self.keypressid = None
self.widget.event_delete(KEYRELEASE_VIRTUAL_EVENT_NAME,
KEYRELEASE_SEQUENCE)
self.widget.unbind(KEYRELEASE_VIRTUAL_EVENT_NAME, self.keyreleaseid)
self.keyreleaseid = None
self.listbox.unbind(LISTUPDATE_SEQUENCE, self.listupdateid)
self.listupdateid = None
if self.winconfigid:
self.autocompletewindow.unbind(WINCONFIG_SEQUENCE, self.winconfigid)
self.winconfigid = None
# Re-focusOn frame.text (See issue #15786)
self.widget.focus_set()
# destroy widgets
self.scrollbar.destroy()
self.scrollbar = None
self.listbox.destroy()
self.listbox = None
self.autocompletewindow.destroy()
self.autocompletewindow = None
if __name__ == '__main__':
from unittest import main
main('idlelib.idle_test.test_autocomplete_w', verbosity=2, exit=False)
# TODO: autocomplete/w htest here
@@ -0,0 +1,96 @@
'''Complete the current word before the cursor with words in the editor.
Each menu selection or shortcut key selection replaces the word with a
different word with the same prefix. The search for matches begins
before the target and moves toward the top of the editor. It then starts
after the cursor and moves down. It then returns to the original word and
the cycle starts again.
Changing the current text line or leaving the cursor in a different
place before requesting the next selection causes AutoExpand to reset
its state.
There is only one instance of Autoexpand.
'''
import re
import string
class AutoExpand:
wordchars = string.ascii_letters + string.digits + "_"
def __init__(self, editwin):
self.text = editwin.text
self.bell = self.text.bell
self.state = None
def expand_word_event(self, event):
"Replace the current word with the next expansion."
curinsert = self.text.index("insert")
curline = self.text.get("insert linestart", "insert lineend")
if not self.state:
words = self.getwords()
index = 0
else:
words, index, insert, line = self.state
if insert != curinsert or line != curline:
words = self.getwords()
index = 0
if not words:
self.bell()
return "break"
word = self.getprevword()
self.text.delete("insert - %d chars" % len(word), "insert")
newword = words[index]
index = (index + 1) % len(words)
if index == 0:
self.bell() # Warn we cycled around
self.text.insert("insert", newword)
curinsert = self.text.index("insert")
curline = self.text.get("insert linestart", "insert lineend")
self.state = words, index, curinsert, curline
return "break"
def getwords(self):
"Return a list of words that match the prefix before the cursor."
word = self.getprevword()
if not word:
return []
before = self.text.get("1.0", "insert wordstart")
wbefore = re.findall(r"\b" + word + r"\w+\b", before)
del before
after = self.text.get("insert wordend", "end")
wafter = re.findall(r"\b" + word + r"\w+\b", after)
del after
if not wbefore and not wafter:
return []
words = []
dict = {}
# search backwards through words before
wbefore.reverse()
for w in wbefore:
if dict.get(w):
continue
words.append(w)
dict[w] = w
# search onwards through words after
for w in wafter:
if dict.get(w):
continue
words.append(w)
dict[w] = w
words.append(word)
return words
def getprevword(self):
"Return the word prefix before the cursor."
line = self.text.get("insert linestart", "insert")
i = len(line)
while i > 0 and line[i-1] in self.wordchars:
i = i-1
return line[i:]
if __name__ == '__main__':
from unittest import main
main('idlelib.idle_test.test_autoexpand', verbosity=2)
@@ -0,0 +1,260 @@
"""Module browser.
XXX TO DO:
- reparse when source changed (maybe just a button would be OK?)
(or recheck on window popup)
- add popup menu with more options (e.g. doc strings, base classes, imports)
- add base classes to class browser tree
"""
import os
import pyclbr
import sys
from idlelib.config import idleConf
from idlelib import pyshell
from idlelib.tree import TreeNode, TreeItem, ScrolledCanvas
from idlelib.util import py_extensions
from idlelib.window import ListedToplevel
file_open = None # Method...Item and Class...Item use this.
# Normally pyshell.flist.open, but there is no pyshell.flist for htest.
# The browser depends on pyclbr and importlib which do not support .pyi files.
browseable_extension_blocklist = ('.pyi',)
def is_browseable_extension(path):
_, ext = os.path.splitext(path)
ext = os.path.normcase(ext)
return ext in py_extensions and ext not in browseable_extension_blocklist
def transform_children(child_dict, modname=None):
"""Transform a child dictionary to an ordered sequence of objects.
The dictionary maps names to pyclbr information objects.
Filter out imported objects.
Augment class names with bases.
The insertion order of the dictionary is assumed to have been in line
number order, so sorting is not necessary.
The current tree only calls this once per child_dict as it saves
TreeItems once created. A future tree and tests might violate this,
so a check prevents multiple in-place augmentations.
"""
obs = [] # Use list since values should already be sorted.
for key, obj in child_dict.items():
if modname is None or obj.module == modname:
if hasattr(obj, 'super') and obj.super and obj.name == key:
# If obj.name != key, it has already been suffixed.
supers = []
for sup in obj.super:
if isinstance(sup, str):
sname = sup
else:
sname = sup.name
if sup.module != obj.module:
sname = f'{sup.module}.{sname}'
supers.append(sname)
obj.name += '({})'.format(', '.join(supers))
obs.append(obj)
return obs
class ModuleBrowser:
"""Browse module classes and functions in IDLE.
"""
# This class is also the base class for pathbrowser.PathBrowser.
# Init and close are inherited, other methods are overridden.
# PathBrowser.__init__ does not call __init__ below.
def __init__(self, master, path, *, _htest=False, _utest=False):
"""Create a window for browsing a module's structure.
Args:
master: parent for widgets.
path: full path of file to browse.
_htest - bool; change box location when running htest.
-utest - bool; suppress contents when running unittest.
Global variables:
file_open: Function used for opening a file.
Instance variables:
name: Module name.
file: Full path and module with supported extension.
Used in creating ModuleBrowserTreeItem as the rootnode for
the tree and subsequently in the children.
"""
self.master = master
self.path = path
self._htest = _htest
self._utest = _utest
self.init()
def close(self, event=None):
"Dismiss the window and the tree nodes."
self.top.destroy()
self.node.destroy()
def init(self):
"Create browser tkinter widgets, including the tree."
global file_open
root = self.master
flist = (pyshell.flist if not (self._htest or self._utest)
else pyshell.PyShellFileList(root))
file_open = flist.open
pyclbr._modules.clear()
# create top
self.top = top = ListedToplevel(root)
top.protocol("WM_DELETE_WINDOW", self.close)
top.bind("<Escape>", self.close)
if self._htest: # place dialog below parent if running htest
top.geometry("+%d+%d" %
(root.winfo_rootx(), root.winfo_rooty() + 200))
self.settitle()
top.focus_set()
# create scrolled canvas
theme = idleConf.CurrentTheme()
background = idleConf.GetHighlight(theme, 'normal')['background']
sc = ScrolledCanvas(top, bg=background, highlightthickness=0,
takefocus=1)
sc.frame.pack(expand=1, fill="both")
item = self.rootnode()
self.node = node = TreeNode(sc.canvas, None, item)
if not self._utest:
node.update()
node.expand()
def settitle(self):
"Set the window title."
self.top.wm_title("Module Browser - " + os.path.basename(self.path))
self.top.wm_iconname("Module Browser")
def rootnode(self):
"Return a ModuleBrowserTreeItem as the root of the tree."
return ModuleBrowserTreeItem(self.path)
class ModuleBrowserTreeItem(TreeItem):
"""Browser tree for Python module.
Uses TreeItem as the basis for the structure of the tree.
Used by both browsers.
"""
def __init__(self, file):
"""Create a TreeItem for the file.
Args:
file: Full path and module name.
"""
self.file = file
def GetText(self):
"Return the module name as the text string to display."
return os.path.basename(self.file)
def GetIconName(self):
"Return the name of the icon to display."
return "python"
def GetSubList(self):
"Return ChildBrowserTreeItems for children."
return [ChildBrowserTreeItem(obj) for obj in self.listchildren()]
def OnDoubleClick(self):
"Open a module in an editor window when double clicked."
if not is_browseable_extension(self.file):
return
if not os.path.exists(self.file):
return
file_open(self.file)
def IsExpandable(self):
"Return True if Python file."
return is_browseable_extension(self.file)
def listchildren(self):
"Return sequenced classes and functions in the module."
if not is_browseable_extension(self.file):
return []
dir, base = os.path.split(self.file)
name, _ = os.path.splitext(base)
try:
tree = pyclbr.readmodule_ex(name, [dir] + sys.path)
except ImportError:
return []
return transform_children(tree, name)
class ChildBrowserTreeItem(TreeItem):
"""Browser tree for child nodes within the module.
Uses TreeItem as the basis for the structure of the tree.
"""
def __init__(self, obj):
"Create a TreeItem for a pyclbr class/function object."
self.obj = obj
self.name = obj.name
self.isfunction = isinstance(obj, pyclbr.Function)
def GetText(self):
"Return the name of the function/class to display."
name = self.name
if self.isfunction:
return "def " + name + "(...)"
else:
return "class " + name
def GetIconName(self):
"Return the name of the icon to display."
if self.isfunction:
return "python"
else:
return "folder"
def IsExpandable(self):
"Return True if self.obj has nested objects."
return self.obj.children != {}
def GetSubList(self):
"Return ChildBrowserTreeItems for children."
return [ChildBrowserTreeItem(obj)
for obj in transform_children(self.obj.children)]
def OnDoubleClick(self):
"Open module with file_open and position to lineno."
try:
edit = file_open(self.obj.file)
edit.gotoline(self.obj.lineno)
except (OSError, AttributeError):
pass
def _module_browser(parent): # htest #
if len(sys.argv) > 1: # If pass file on command line.
file = sys.argv[1]
else:
file = __file__
# Add nested objects for htest.
class Nested_in_func(TreeNode):
def nested_in_class(): pass
def closure():
class Nested_in_closure: pass
ModuleBrowser(parent, file, _htest=True)
if __name__ == "__main__":
if len(sys.argv) == 1: # If pass file on command line, unittest fails.
from unittest import main
main('idlelib.idle_test.test_browser', verbosity=2, exit=False)
from idlelib.idle_test.htest import run
run(_module_browser)
@@ -0,0 +1,205 @@
"""Pop up a reminder of how to call a function.
Call Tips are floating windows which display function, class, and method
parameter and docstring information when you type an opening parenthesis, and
which disappear when you type a closing parenthesis.
"""
import __main__
import inspect
import re
import sys
import textwrap
import types
from idlelib import calltip_w
from idlelib.hyperparser import HyperParser
class Calltip:
def __init__(self, editwin=None):
if editwin is None: # subprocess and test
self.editwin = None
else:
self.editwin = editwin
self.text = editwin.text
self.active_calltip = None
self._calltip_window = self._make_tk_calltip_window
def close(self):
self._calltip_window = None
def _make_tk_calltip_window(self):
# See __init__ for usage
return calltip_w.CalltipWindow(self.text)
def remove_calltip_window(self, event=None):
if self.active_calltip:
self.active_calltip.hidetip()
self.active_calltip = None
def force_open_calltip_event(self, event):
"The user selected the menu entry or hotkey, open the tip."
self.open_calltip(True)
return "break"
def try_open_calltip_event(self, event):
"""Happens when it would be nice to open a calltip, but not really
necessary, for example after an opening bracket, so function calls
won't be made.
"""
self.open_calltip(False)
def refresh_calltip_event(self, event):
if self.active_calltip and self.active_calltip.tipwindow:
self.open_calltip(False)
def open_calltip(self, evalfuncs):
"""Maybe close an existing calltip and maybe open a new calltip.
Called from (force_open|try_open|refresh)_calltip_event functions.
"""
hp = HyperParser(self.editwin, "insert")
sur_paren = hp.get_surrounding_brackets('(')
# If not inside parentheses, no calltip.
if not sur_paren:
self.remove_calltip_window()
return
# If a calltip is shown for the current parentheses, do
# nothing.
if self.active_calltip:
opener_line, opener_col = map(int, sur_paren[0].split('.'))
if (
(opener_line, opener_col) ==
(self.active_calltip.parenline, self.active_calltip.parencol)
):
return
hp.set_index(sur_paren[0])
try:
expression = hp.get_expression()
except ValueError:
expression = None
if not expression:
# No expression before the opening parenthesis, e.g.
# because it's in a string or the opener for a tuple:
# Do nothing.
return
# At this point, the current index is after an opening
# parenthesis, in a section of code, preceded by a valid
# expression. If there is a calltip shown, it's not for the
# same index and should be closed.
self.remove_calltip_window()
# Simple, fast heuristic: If the preceding expression includes
# an opening parenthesis, it likely includes a function call.
if not evalfuncs and (expression.find('(') != -1):
return
argspec = self.fetch_tip(expression)
if not argspec:
return
self.active_calltip = self._calltip_window()
self.active_calltip.showtip(argspec, sur_paren[0], sur_paren[1])
def fetch_tip(self, expression):
"""Return the argument list and docstring of a function or class.
If there is a Python subprocess, get the calltip there. Otherwise,
either this fetch_tip() is running in the subprocess or it was
called in an IDLE running without the subprocess.
The subprocess environment is that of the most recently run script. If
two unrelated modules are being edited some calltips in the current
module may be inoperative if the module was not the last to run.
To find methods, fetch_tip must be fed a fully qualified name.
"""
try:
rpcclt = self.editwin.flist.pyshell.interp.rpcclt
except AttributeError:
rpcclt = None
if rpcclt:
return rpcclt.remotecall("exec", "get_the_calltip",
(expression,), {})
else:
return get_argspec(get_entity(expression))
def get_entity(expression):
"""Return the object corresponding to expression evaluated
in a namespace spanning sys.modules and __main.dict__.
"""
if expression:
namespace = {**sys.modules, **__main__.__dict__}
try:
return eval(expression, namespace) # Only protect user code.
except BaseException:
# An uncaught exception closes idle, and eval can raise any
# exception, especially if user classes are involved.
return None
# The following are used in get_argspec and some in tests
_MAX_COLS = 85
_MAX_LINES = 5 # enough for bytes
_INDENT = ' '*4 # for wrapped signatures
_first_param = re.compile(r'(?<=\()\w*\,?\s*')
_default_callable_argspec = "See source or doc"
_invalid_method = "invalid method signature"
def get_argspec(ob):
'''Return a string describing the signature of a callable object, or ''.
For Python-coded functions and methods, the first line is introspected.
Delete 'self' parameter for classes (.__init__) and bound methods.
The next lines are the first lines of the doc string up to the first
empty line or _MAX_LINES. For builtins, this typically includes
the arguments in addition to the return value.
'''
# Determine function object fob to inspect.
try:
ob_call = ob.__call__
except BaseException: # Buggy user object could raise anything.
return '' # No popup for non-callables.
# For Get_argspecTest.test_buggy_getattr_class, CallA() & CallB().
fob = ob_call if isinstance(ob_call, types.MethodType) else ob
# Initialize argspec and wrap it to get lines.
try:
argspec = str(inspect.signature(fob))
except Exception as err:
msg = str(err)
if msg.startswith(_invalid_method):
return _invalid_method
else:
argspec = ''
if isinstance(fob, type) and argspec == '()':
# If fob has no argument, use default callable argspec.
argspec = _default_callable_argspec
lines = (textwrap.wrap(argspec, _MAX_COLS, subsequent_indent=_INDENT)
if len(argspec) > _MAX_COLS else [argspec] if argspec else [])
# Augment lines from docstring, if any, and join to get argspec.
doc = inspect.getdoc(ob)
if doc:
for line in doc.split('\n', _MAX_LINES)[:_MAX_LINES]:
line = line.strip()
if not line:
break
if len(line) > _MAX_COLS:
line = line[: _MAX_COLS - 3] + '...'
lines.append(line)
argspec = '\n'.join(lines)
return argspec or _default_callable_argspec
if __name__ == '__main__':
from unittest import main
main('idlelib.idle_test.test_calltip', verbosity=2)
@@ -0,0 +1,202 @@
"""A call-tip window class for Tkinter/IDLE.
After tooltip.py, which uses ideas gleaned from PySol.
Used by calltip.py.
"""
from tkinter import Label, LEFT, SOLID, TclError
from idlelib.tooltip import TooltipBase
HIDE_EVENT = "<<calltipwindow-hide>>"
HIDE_SEQUENCES = ("<Key-Escape>", "<FocusOut>")
CHECKHIDE_EVENT = "<<calltipwindow-checkhide>>"
CHECKHIDE_SEQUENCES = ("<KeyRelease>", "<ButtonRelease>")
CHECKHIDE_TIME = 100 # milliseconds
MARK_RIGHT = "calltipwindowregion_right"
class CalltipWindow(TooltipBase):
"""A call-tip widget for tkinter text widgets."""
def __init__(self, text_widget):
"""Create a call-tip; shown by showtip().
text_widget: a Text widget with code for which call-tips are desired
"""
# Note: The Text widget will be accessible as self.anchor_widget
super().__init__(text_widget)
self.label = self.text = None
self.parenline = self.parencol = self.lastline = None
self.hideid = self.checkhideid = None
self.checkhide_after_id = None
def get_position(self):
"""Choose the position of the call-tip."""
curline = int(self.anchor_widget.index("insert").split('.')[0])
if curline == self.parenline:
anchor_index = (self.parenline, self.parencol)
else:
anchor_index = (curline, 0)
box = self.anchor_widget.bbox("%d.%d" % anchor_index)
if not box:
box = list(self.anchor_widget.bbox("insert"))
# align to left of window
box[0] = 0
box[2] = 0
return box[0] + 2, box[1] + box[3]
def position_window(self):
"Reposition the window if needed."
curline = int(self.anchor_widget.index("insert").split('.')[0])
if curline == self.lastline:
return
self.lastline = curline
self.anchor_widget.see("insert")
super().position_window()
def showtip(self, text, parenleft, parenright):
"""Show the call-tip, bind events which will close it and reposition it.
text: the text to display in the call-tip
parenleft: index of the opening parenthesis in the text widget
parenright: index of the closing parenthesis in the text widget,
or the end of the line if there is no closing parenthesis
"""
# Only called in calltip.Calltip, where lines are truncated
self.text = text
if self.tipwindow or not self.text:
return
self.anchor_widget.mark_set(MARK_RIGHT, parenright)
self.parenline, self.parencol = map(
int, self.anchor_widget.index(parenleft).split("."))
super().showtip()
self._bind_events()
def showcontents(self):
"""Create the call-tip widget."""
self.label = Label(self.tipwindow, text=self.text, justify=LEFT,
background="#ffffd0", foreground="black",
relief=SOLID, borderwidth=1,
font=self.anchor_widget['font'])
self.label.pack()
def checkhide_event(self, event=None):
"""Handle CHECK_HIDE_EVENT: call hidetip or reschedule."""
if not self.tipwindow:
# If the event was triggered by the same event that unbound
# this function, the function will be called nevertheless,
# so do nothing in this case.
return None
# Hide the call-tip if the insertion cursor moves outside of the
# parenthesis.
curline, curcol = map(int, self.anchor_widget.index("insert").split('.'))
if curline < self.parenline or \
(curline == self.parenline and curcol <= self.parencol) or \
self.anchor_widget.compare("insert", ">", MARK_RIGHT):
self.hidetip()
return "break"
# Not hiding the call-tip.
self.position_window()
# Re-schedule this function to be called again in a short while.
if self.checkhide_after_id is not None:
self.anchor_widget.after_cancel(self.checkhide_after_id)
self.checkhide_after_id = \
self.anchor_widget.after(CHECKHIDE_TIME, self.checkhide_event)
return None
def hide_event(self, event):
"""Handle HIDE_EVENT by calling hidetip."""
if not self.tipwindow:
# See the explanation in checkhide_event.
return None
self.hidetip()
return "break"
def hidetip(self):
"""Hide the call-tip."""
if not self.tipwindow:
return
try:
self.label.destroy()
except TclError:
pass
self.label = None
self.parenline = self.parencol = self.lastline = None
try:
self.anchor_widget.mark_unset(MARK_RIGHT)
except TclError:
pass
try:
self._unbind_events()
except (TclError, ValueError):
# ValueError may be raised by MultiCall
pass
super().hidetip()
def _bind_events(self):
"""Bind event handlers."""
self.checkhideid = self.anchor_widget.bind(CHECKHIDE_EVENT,
self.checkhide_event)
for seq in CHECKHIDE_SEQUENCES:
self.anchor_widget.event_add(CHECKHIDE_EVENT, seq)
self.anchor_widget.after(CHECKHIDE_TIME, self.checkhide_event)
self.hideid = self.anchor_widget.bind(HIDE_EVENT,
self.hide_event)
for seq in HIDE_SEQUENCES:
self.anchor_widget.event_add(HIDE_EVENT, seq)
def _unbind_events(self):
"""Unbind event handlers."""
for seq in CHECKHIDE_SEQUENCES:
self.anchor_widget.event_delete(CHECKHIDE_EVENT, seq)
self.anchor_widget.unbind(CHECKHIDE_EVENT, self.checkhideid)
self.checkhideid = None
for seq in HIDE_SEQUENCES:
self.anchor_widget.event_delete(HIDE_EVENT, seq)
self.anchor_widget.unbind(HIDE_EVENT, self.hideid)
self.hideid = None
def _calltip_window(parent): # htest #
from tkinter import Toplevel, Text, LEFT, BOTH
top = Toplevel(parent)
top.title("Test call-tips")
x, y = map(int, parent.geometry().split('+')[1:])
top.geometry("250x100+%d+%d" % (x + 175, y + 150))
text = Text(top)
text.pack(side=LEFT, fill=BOTH, expand=1)
text.insert("insert", "string.split")
top.update()
calltip = CalltipWindow(text)
def calltip_show(event):
calltip.showtip("(s='Hello world')", "insert", "end")
def calltip_hide(event):
calltip.hidetip()
text.event_add("<<calltip-show>>", "(")
text.event_add("<<calltip-hide>>", ")")
text.bind("<<calltip-show>>", calltip_show)
text.bind("<<calltip-hide>>", calltip_hide)
text.focus_set()
if __name__ == '__main__':
from unittest import main
main('idlelib.idle_test.test_calltip_w', verbosity=2, exit=False)
from idlelib.idle_test.htest import run
run(_calltip_window)
@@ -0,0 +1,270 @@
"""codecontext - display the block context above the edit window
Once code has scrolled off the top of a window, it can be difficult to
determine which block you are in. This extension implements a pane at the top
of each IDLE edit window which provides block structure hints. These hints are
the lines which contain the block opening keywords, e.g. 'if', for the
enclosing block. The number of hint lines is determined by the maxlines
variable in the codecontext section of config-extensions.def. Lines which do
not open blocks are not shown in the context hints pane.
For EditorWindows, <<toggle-code-context>> is bound to CodeContext(self).
toggle_code_context_event.
"""
import re
from sys import maxsize as INFINITY
from tkinter import Frame, Text, TclError
from tkinter.constants import NSEW, SUNKEN
from idlelib.config import idleConf
BLOCKOPENERS = {'class', 'def', 'if', 'elif', 'else', 'while', 'for',
'try', 'except', 'finally', 'with', 'async'}
def get_spaces_firstword(codeline, c=re.compile(r"^(\s*)(\w*)")):
"Extract the beginning whitespace and first word from codeline."
return c.match(codeline).groups()
def get_line_info(codeline):
"""Return tuple of (line indent value, codeline, block start keyword).
The indentation of empty lines (or comment lines) is INFINITY.
If the line does not start a block, the keyword value is False.
"""
spaces, firstword = get_spaces_firstword(codeline)
indent = len(spaces)
if len(codeline) == indent or codeline[indent] == '#':
indent = INFINITY
opener = firstword in BLOCKOPENERS and firstword
return indent, codeline, opener
class CodeContext:
"Display block context above the edit window."
UPDATEINTERVAL = 100 # millisec
def __init__(self, editwin):
"""Initialize settings for context block.
editwin is the Editor window for the context block.
self.text is the editor window text widget.
self.context displays the code context text above the editor text.
Initially None, it is toggled via <<toggle-code-context>>.
self.topvisible is the number of the top text line displayed.
self.info is a list of (line number, indent level, line text,
block keyword) tuples for the block structure above topvisible.
self.info[0] is initialized with a 'dummy' line which
starts the toplevel 'block' of the module.
self.t1 and self.t2 are two timer events on the editor text widget to
monitor for changes to the context text or editor font.
"""
self.editwin = editwin
self.text = editwin.text
self._reset()
def _reset(self):
self.context = None
self.cell00 = None
self.t1 = None
self.topvisible = 1
self.info = [(0, -1, "", False)]
@classmethod
def reload(cls):
"Load class variables from config."
cls.context_depth = idleConf.GetOption("extensions", "CodeContext",
"maxlines", type="int",
default=15)
def __del__(self):
"Cancel scheduled events."
if self.t1 is not None:
try:
self.text.after_cancel(self.t1)
except TclError: # pragma: no cover
pass
self.t1 = None
def toggle_code_context_event(self, event=None):
"""Toggle code context display.
If self.context doesn't exist, create it to match the size of the editor
window text (toggle on). If it does exist, destroy it (toggle off).
Return 'break' to complete the processing of the binding.
"""
if self.context is None:
# Calculate the border width and horizontal padding required to
# align the context with the text in the main Text widget.
#
# All values are passed through getint(), since some
# values may be pixel objects, which can't simply be added to ints.
widgets = self.editwin.text, self.editwin.text_frame
# Calculate the required horizontal padding and border width.
padx = 0
border = 0
for widget in widgets:
info = (widget.grid_info()
if widget is self.editwin.text
else widget.pack_info())
padx += widget.tk.getint(info['padx'])
padx += widget.tk.getint(widget.cget('padx'))
border += widget.tk.getint(widget.cget('border'))
context = self.context = Text(
self.editwin.text_frame,
height=1,
width=1, # Don't request more than we get.
highlightthickness=0,
padx=padx, border=border, relief=SUNKEN, state='disabled')
self.update_font()
self.update_highlight_colors()
context.bind('<ButtonRelease-1>', self.jumptoline)
# Get the current context and initiate the recurring update event.
self.timer_event()
# Grid the context widget above the text widget.
context.grid(row=0, column=1, sticky=NSEW)
line_number_colors = idleConf.GetHighlight(idleConf.CurrentTheme(),
'linenumber')
self.cell00 = Frame(self.editwin.text_frame,
bg=line_number_colors['background'])
self.cell00.grid(row=0, column=0, sticky=NSEW)
menu_status = 'Hide'
else:
self.context.destroy()
self.context = None
self.cell00.destroy()
self.cell00 = None
self.text.after_cancel(self.t1)
self._reset()
menu_status = 'Show'
self.editwin.update_menu_label(menu='options', index='*ode*ontext',
label=f'{menu_status} Code Context')
return "break"
def get_context(self, new_topvisible, stopline=1, stopindent=0):
"""Return a list of block line tuples and the 'last' indent.
The tuple fields are (linenum, indent, text, opener).
The list represents header lines from new_topvisible back to
stopline with successively shorter indents > stopindent.
The list is returned ordered by line number.
Last indent returned is the smallest indent observed.
"""
assert stopline > 0
lines = []
# The indentation level we are currently in.
lastindent = INFINITY
# For a line to be interesting, it must begin with a block opening
# keyword, and have less indentation than lastindent.
for linenum in range(new_topvisible, stopline-1, -1):
codeline = self.text.get(f'{linenum}.0', f'{linenum}.end')
indent, text, opener = get_line_info(codeline)
if indent < lastindent:
lastindent = indent
if opener in ("else", "elif"):
# Also show the if statement.
lastindent += 1
if opener and linenum < new_topvisible and indent >= stopindent:
lines.append((linenum, indent, text, opener))
if lastindent <= stopindent:
break
lines.reverse()
return lines, lastindent
def update_code_context(self):
"""Update context information and lines visible in the context pane.
No update is done if the text hasn't been scrolled. If the text
was scrolled, the lines that should be shown in the context will
be retrieved and the context area will be updated with the code,
up to the number of maxlines.
"""
new_topvisible = self.editwin.getlineno("@0,0")
if self.topvisible == new_topvisible: # Haven't scrolled.
return
if self.topvisible < new_topvisible: # Scroll down.
lines, lastindent = self.get_context(new_topvisible,
self.topvisible)
# Retain only context info applicable to the region
# between topvisible and new_topvisible.
while self.info[-1][1] >= lastindent:
del self.info[-1]
else: # self.topvisible > new_topvisible: # Scroll up.
stopindent = self.info[-1][1] + 1
# Retain only context info associated
# with lines above new_topvisible.
while self.info[-1][0] >= new_topvisible:
stopindent = self.info[-1][1]
del self.info[-1]
lines, lastindent = self.get_context(new_topvisible,
self.info[-1][0]+1,
stopindent)
self.info.extend(lines)
self.topvisible = new_topvisible
# Last context_depth context lines.
context_strings = [x[2] for x in self.info[-self.context_depth:]]
showfirst = 0 if context_strings[0] else 1
# Update widget.
self.context['height'] = len(context_strings) - showfirst
self.context['state'] = 'normal'
self.context.delete('1.0', 'end')
self.context.insert('end', '\n'.join(context_strings[showfirst:]))
self.context['state'] = 'disabled'
def jumptoline(self, event=None):
""" Show clicked context line at top of editor.
If a selection was made, don't jump; allow copying.
If no visible context, show the top line of the file.
"""
try:
self.context.index("sel.first")
except TclError:
lines = len(self.info)
if lines == 1: # No context lines are showing.
newtop = 1
else:
# Line number clicked.
contextline = int(float(self.context.index('insert')))
# Lines not displayed due to maxlines.
offset = max(1, lines - self.context_depth) - 1
newtop = self.info[offset + contextline][0]
self.text.yview(f'{newtop}.0')
self.update_code_context()
def timer_event(self):
"Event on editor text widget triggered every UPDATEINTERVAL ms."
if self.context is not None:
self.update_code_context()
self.t1 = self.text.after(self.UPDATEINTERVAL, self.timer_event)
def update_font(self):
if self.context is not None:
font = idleConf.GetFont(self.text, 'main', 'EditorWindow')
self.context['font'] = font
def update_highlight_colors(self):
if self.context is not None:
colors = idleConf.GetHighlight(idleConf.CurrentTheme(), 'context')
self.context['background'] = colors['background']
self.context['foreground'] = colors['foreground']
if self.cell00 is not None:
line_number_colors = idleConf.GetHighlight(idleConf.CurrentTheme(),
'linenumber')
self.cell00.config(bg=line_number_colors['background'])
CodeContext.reload()
if __name__ == "__main__":
from unittest import main
main('idlelib.idle_test.test_codecontext', verbosity=2, exit=False)
# Add htest.
@@ -0,0 +1,384 @@
import builtins
import keyword
import re
import time
from idlelib.config import idleConf
from idlelib.delegator import Delegator
DEBUG = False
def any(name, alternates):
"Return a named group pattern matching list of alternates."
return "(?P<%s>" % name + "|".join(alternates) + ")"
def make_pat():
kw = r"\b" + any("KEYWORD", keyword.kwlist) + r"\b"
match_softkw = (
r"^[ \t]*" + # at beginning of line + possible indentation
r"(?P<MATCH_SOFTKW>match)\b" +
r"(?![ \t]*(?:" + "|".join([ # not followed by ...
r"[:,;=^&|@~)\]}]", # a character which means it can't be a
# pattern-matching statement
r"\b(?:" + r"|".join(keyword.kwlist) + r")\b", # a keyword
]) +
r"))"
)
case_default = (
r"^[ \t]*" + # at beginning of line + possible indentation
r"(?P<CASE_SOFTKW>case)" +
r"[ \t]+(?P<CASE_DEFAULT_UNDERSCORE>_\b)"
)
case_softkw_and_pattern = (
r"^[ \t]*" + # at beginning of line + possible indentation
r"(?P<CASE_SOFTKW2>case)\b" +
r"(?![ \t]*(?:" + "|".join([ # not followed by ...
r"_\b", # a lone underscore
r"[:,;=^&|@~)\]}]", # a character which means it can't be a
# pattern-matching case
r"\b(?:" + r"|".join(keyword.kwlist) + r")\b", # a keyword
]) +
r"))"
)
builtinlist = [str(name) for name in dir(builtins)
if not name.startswith('_') and
name not in keyword.kwlist]
builtin = r"([^.'\"\\#]\b|^)" + any("BUILTIN", builtinlist) + r"\b"
comment = any("COMMENT", [r"#[^\n]*"])
stringprefix = r"(?i:r|u|f|fr|rf|b|br|rb)?"
sqstring = stringprefix + r"'[^'\\\n]*(\\.[^'\\\n]*)*'?"
dqstring = stringprefix + r'"[^"\\\n]*(\\.[^"\\\n]*)*"?'
sq3string = stringprefix + r"'''[^'\\]*((\\.|'(?!''))[^'\\]*)*(''')?"
dq3string = stringprefix + r'"""[^"\\]*((\\.|"(?!""))[^"\\]*)*(""")?'
string = any("STRING", [sq3string, dq3string, sqstring, dqstring])
prog = re.compile("|".join([
builtin, comment, string, kw,
match_softkw, case_default,
case_softkw_and_pattern,
any("SYNC", [r"\n"]),
]),
re.DOTALL | re.MULTILINE)
return prog
prog = make_pat()
idprog = re.compile(r"\s+(\w+)")
prog_group_name_to_tag = {
"MATCH_SOFTKW": "KEYWORD",
"CASE_SOFTKW": "KEYWORD",
"CASE_DEFAULT_UNDERSCORE": "KEYWORD",
"CASE_SOFTKW2": "KEYWORD",
}
def matched_named_groups(re_match):
"Get only the non-empty named groups from an re.Match object."
return ((k, v) for (k, v) in re_match.groupdict().items() if v)
def color_config(text):
"""Set color options of Text widget.
If ColorDelegator is used, this should be called first.
"""
# Called from htest, TextFrame, Editor, and Turtledemo.
# Not automatic because ColorDelegator does not know 'text'.
theme = idleConf.CurrentTheme()
normal_colors = idleConf.GetHighlight(theme, 'normal')
cursor_color = idleConf.GetHighlight(theme, 'cursor')['foreground']
select_colors = idleConf.GetHighlight(theme, 'hilite')
text.config(
foreground=normal_colors['foreground'],
background=normal_colors['background'],
insertbackground=cursor_color,
selectforeground=select_colors['foreground'],
selectbackground=select_colors['background'],
inactiveselectbackground=select_colors['background'], # new in 8.5
)
class ColorDelegator(Delegator):
"""Delegator for syntax highlighting (text coloring).
Instance variables:
delegate: Delegator below this one in the stack, meaning the
one this one delegates to.
Used to track state:
after_id: Identifier for scheduled after event, which is a
timer for colorizing the text.
allow_colorizing: Boolean toggle for applying colorizing.
colorizing: Boolean flag when colorizing is in process.
stop_colorizing: Boolean flag to end an active colorizing
process.
"""
def __init__(self):
Delegator.__init__(self)
self.init_state()
self.prog = prog
self.idprog = idprog
self.LoadTagDefs()
def init_state(self):
"Initialize variables that track colorizing state."
self.after_id = None
self.allow_colorizing = True
self.stop_colorizing = False
self.colorizing = False
def setdelegate(self, delegate):
"""Set the delegate for this instance.
A delegate is an instance of a Delegator class and each
delegate points to the next delegator in the stack. This
allows multiple delegators to be chained together for a
widget. The bottom delegate for a colorizer is a Text
widget.
If there is a delegate, also start the colorizing process.
"""
if self.delegate is not None:
self.unbind("<<toggle-auto-coloring>>")
Delegator.setdelegate(self, delegate)
if delegate is not None:
self.config_colors()
self.bind("<<toggle-auto-coloring>>", self.toggle_colorize_event)
self.notify_range("1.0", "end")
else:
# No delegate - stop any colorizing.
self.stop_colorizing = True
self.allow_colorizing = False
def config_colors(self):
"Configure text widget tags with colors from tagdefs."
for tag, cnf in self.tagdefs.items():
self.tag_configure(tag, **cnf)
self.tag_raise('sel')
def LoadTagDefs(self):
"Create dictionary of tag names to text colors."
theme = idleConf.CurrentTheme()
self.tagdefs = {
"COMMENT": idleConf.GetHighlight(theme, "comment"),
"KEYWORD": idleConf.GetHighlight(theme, "keyword"),
"BUILTIN": idleConf.GetHighlight(theme, "builtin"),
"STRING": idleConf.GetHighlight(theme, "string"),
"DEFINITION": idleConf.GetHighlight(theme, "definition"),
"SYNC": {'background': None, 'foreground': None},
"TODO": {'background': None, 'foreground': None},
"ERROR": idleConf.GetHighlight(theme, "error"),
# "hit" is used by ReplaceDialog to mark matches. It shouldn't be changed by Colorizer, but
# that currently isn't technically possible. This should be moved elsewhere in the future
# when fixing the "hit" tag's visibility, or when the replace dialog is replaced with a
# non-modal alternative.
"hit": idleConf.GetHighlight(theme, "hit"),
}
if DEBUG: print('tagdefs', self.tagdefs)
def insert(self, index, chars, tags=None):
"Insert chars into widget at index and mark for colorizing."
index = self.index(index)
self.delegate.insert(index, chars, tags)
self.notify_range(index, index + "+%dc" % len(chars))
def delete(self, index1, index2=None):
"Delete chars between indexes and mark for colorizing."
index1 = self.index(index1)
self.delegate.delete(index1, index2)
self.notify_range(index1)
def notify_range(self, index1, index2=None):
"Mark text changes for processing and restart colorizing, if active."
self.tag_add("TODO", index1, index2)
if self.after_id:
if DEBUG: print("colorizing already scheduled")
return
if self.colorizing:
self.stop_colorizing = True
if DEBUG: print("stop colorizing")
if self.allow_colorizing:
if DEBUG: print("schedule colorizing")
self.after_id = self.after(1, self.recolorize)
return
def close(self):
if self.after_id:
after_id = self.after_id
self.after_id = None
if DEBUG: print("cancel scheduled recolorizer")
self.after_cancel(after_id)
self.allow_colorizing = False
self.stop_colorizing = True
def toggle_colorize_event(self, event=None):
"""Toggle colorizing on and off.
When toggling off, if colorizing is scheduled or is in
process, it will be cancelled and/or stopped.
When toggling on, colorizing will be scheduled.
"""
if self.after_id:
after_id = self.after_id
self.after_id = None
if DEBUG: print("cancel scheduled recolorizer")
self.after_cancel(after_id)
if self.allow_colorizing and self.colorizing:
if DEBUG: print("stop colorizing")
self.stop_colorizing = True
self.allow_colorizing = not self.allow_colorizing
if self.allow_colorizing and not self.colorizing:
self.after_id = self.after(1, self.recolorize)
if DEBUG:
print("auto colorizing turned",
"on" if self.allow_colorizing else "off")
return "break"
def recolorize(self):
"""Timer event (every 1ms) to colorize text.
Colorizing is only attempted when the text widget exists,
when colorizing is toggled on, and when the colorizing
process is not already running.
After colorizing is complete, some cleanup is done to
make sure that all the text has been colorized.
"""
self.after_id = None
if not self.delegate:
if DEBUG: print("no delegate")
return
if not self.allow_colorizing:
if DEBUG: print("auto colorizing is off")
return
if self.colorizing:
if DEBUG: print("already colorizing")
return
try:
self.stop_colorizing = False
self.colorizing = True
if DEBUG: print("colorizing...")
t0 = time.perf_counter()
self.recolorize_main()
t1 = time.perf_counter()
if DEBUG: print("%.3f seconds" % (t1-t0))
finally:
self.colorizing = False
if self.allow_colorizing and self.tag_nextrange("TODO", "1.0"):
if DEBUG: print("reschedule colorizing")
self.after_id = self.after(1, self.recolorize)
def recolorize_main(self):
"Evaluate text and apply colorizing tags."
next = "1.0"
while todo_tag_range := self.tag_nextrange("TODO", next):
self.tag_remove("SYNC", todo_tag_range[0], todo_tag_range[1])
sync_tag_range = self.tag_prevrange("SYNC", todo_tag_range[0])
head = sync_tag_range[1] if sync_tag_range else "1.0"
chars = ""
next = head
lines_to_get = 1
ok = False
while not ok:
mark = next
next = self.index(mark + "+%d lines linestart" %
lines_to_get)
lines_to_get = min(lines_to_get * 2, 100)
ok = "SYNC" in self.tag_names(next + "-1c")
line = self.get(mark, next)
##print head, "get", mark, next, "->", repr(line)
if not line:
return
for tag in self.tagdefs:
self.tag_remove(tag, mark, next)
chars += line
self._add_tags_in_section(chars, head)
if "SYNC" in self.tag_names(next + "-1c"):
head = next
chars = ""
else:
ok = False
if not ok:
# We're in an inconsistent state, and the call to
# update may tell us to stop. It may also change
# the correct value for "next" (since this is a
# line.col string, not a true mark). So leave a
# crumb telling the next invocation to resume here
# in case update tells us to leave.
self.tag_add("TODO", next)
self.update_idletasks()
if self.stop_colorizing:
if DEBUG: print("colorizing stopped")
return
def _add_tag(self, start, end, head, matched_group_name):
"""Add a tag to a given range in the text widget.
This is a utility function, receiving the range as `start` and
`end` positions, each of which is a number of characters
relative to the given `head` index in the text widget.
The tag to add is determined by `matched_group_name`, which is
the name of a regular expression "named group" as matched by
by the relevant highlighting regexps.
"""
tag = prog_group_name_to_tag.get(matched_group_name,
matched_group_name)
self.tag_add(tag,
f"{head}+{start:d}c",
f"{head}+{end:d}c")
def _add_tags_in_section(self, chars, head):
"""Parse and add highlighting tags to a given part of the text.
`chars` is a string with the text to parse and to which
highlighting is to be applied.
`head` is the index in the text widget where the text is found.
"""
for m in self.prog.finditer(chars):
for name, matched_text in matched_named_groups(m):
a, b = m.span(name)
self._add_tag(a, b, head, name)
if matched_text in ("def", "class"):
if m1 := self.idprog.match(chars, b):
a, b = m1.span(1)
self._add_tag(a, b, head, "DEFINITION")
def removecolors(self):
"Remove all colorizing tags."
for tag in self.tagdefs:
self.tag_remove(tag, "1.0", "end")
def _color_delegator(parent): # htest #
from tkinter import Toplevel, Text
from idlelib.idle_test.test_colorizer import source
from idlelib.percolator import Percolator
top = Toplevel(parent)
top.title("Test ColorDelegator")
x, y = map(int, parent.geometry().split('+')[1:])
top.geometry("700x550+%d+%d" % (x + 20, y + 175))
text = Text(top, background="white")
text.pack(expand=1, fill="both")
text.insert("insert", source)
text.focus_set()
color_config(text)
p = Percolator(text)
d = ColorDelegator()
p.insertfilter(d)
if __name__ == "__main__":
from unittest import main
main('idlelib.idle_test.test_colorizer', verbosity=2, exit=False)
from idlelib.idle_test.htest import run
run(_color_delegator)
@@ -0,0 +1,62 @@
# config-extensions.def
#
# The following sections are for features that are no longer extensions.
# Their options values are left here for back-compatibility.
[AutoComplete]
popupwait= 2000
[CodeContext]
maxlines= 15
[FormatParagraph]
max-width= 72
[ParenMatch]
style= expression
flash-delay= 500
bell= True
# IDLE reads several config files to determine user preferences. This
# file is the default configuration file for IDLE extensions settings.
#
# Each extension must have at least one section, named after the
# extension module. This section must contain an 'enable' item (=True to
# enable the extension, =False to disable it), it may contain
# 'enable_editor' or 'enable_shell' items, to apply it only to editor ir
# shell windows, and may also contain any other general configuration
# items for the extension. Other True/False values will also be
# recognized as boolean by the Extension Configuration dialog.
#
# Each extension must define at least one section named
# ExtensionName_bindings or ExtensionName_cfgBindings. If present,
# ExtensionName_bindings defines virtual event bindings for the
# extension that are not user re-configurable. If present,
# ExtensionName_cfgBindings defines virtual event bindings for the
# extension that may be sensibly re-configured.
#
# If there are no keybindings for a menus' virtual events, include lines
# like <<toggle-code-context>>=.
#
# Currently it is necessary to manually modify this file to change
# extension key bindings and default values. To customize, create
# ~/.idlerc/config-extensions.cfg and append the appropriate customized
# section(s). Those sections will override the defaults in this file.
#
# Note: If a keybinding is already in use when the extension is loaded,
# the extension's virtual event's keybinding will be set to ''.
#
# See config-keys.def for notes on specifying keys and extend.txt for
# information on creating IDLE extensions.
# A fake extension for testing and example purposes. When enabled and
# invoked, inserts or deletes z-text at beginning of every line.
[ZzDummy]
enable= False
enable_shell = False
enable_editor = True
z-text= Z
[ZzDummy_cfgBindings]
z-in= <Control-Shift-KeyRelease-Insert>
[ZzDummy_bindings]
z-out= <Control-Shift-KeyRelease-Delete>
@@ -0,0 +1,105 @@
# IDLE reads several config files to determine user preferences. This
# file is the default config file for idle highlight theme settings.
[IDLE Classic]
normal-foreground= #000000
normal-background= #ffffff
keyword-foreground= #ff7700
keyword-background= #ffffff
builtin-foreground= #900090
builtin-background= #ffffff
comment-foreground= #dd0000
comment-background= #ffffff
string-foreground= #00aa00
string-background= #ffffff
definition-foreground= #0000ff
definition-background= #ffffff
hilite-foreground= #000000
hilite-background= gray
break-foreground= black
break-background= #ffff55
hit-foreground= #ffffff
hit-background= #000000
error-foreground= #000000
error-background= #ff7777
context-foreground= #000000
context-background= lightgray
linenumber-foreground= gray
linenumber-background= #ffffff
#cursor (only foreground can be set, restart IDLE)
cursor-foreground= black
#shell window
stdout-foreground= blue
stdout-background= #ffffff
stderr-foreground= red
stderr-background= #ffffff
console-foreground= #770000
console-background= #ffffff
[IDLE New]
normal-foreground= #000000
normal-background= #ffffff
keyword-foreground= #ff7700
keyword-background= #ffffff
builtin-foreground= #900090
builtin-background= #ffffff
comment-foreground= #dd0000
comment-background= #ffffff
string-foreground= #00aa00
string-background= #ffffff
definition-foreground= #0000ff
definition-background= #ffffff
hilite-foreground= #000000
hilite-background= gray
break-foreground= black
break-background= #ffff55
hit-foreground= #ffffff
hit-background= #000000
error-foreground= #000000
error-background= #ff7777
context-foreground= #000000
context-background= lightgray
linenumber-foreground= gray
linenumber-background= #ffffff
#cursor (only foreground can be set, restart IDLE)
cursor-foreground= black
#shell window
stdout-foreground= blue
stdout-background= #ffffff
stderr-foreground= red
stderr-background= #ffffff
console-foreground= #770000
console-background= #ffffff
[IDLE Dark]
comment-foreground = #dd0000
console-foreground = #ff4d4d
error-foreground = #FFFFFF
hilite-background = #7e7e7e
string-foreground = #02ff02
stderr-background = #002240
stderr-foreground = #ffb3b3
console-background = #002240
hit-background = #fbfbfb
string-background = #002240
normal-background = #002240
hilite-foreground = #FFFFFF
keyword-foreground = #ff8000
error-background = #c86464
keyword-background = #002240
builtin-background = #002240
break-background = #808000
builtin-foreground = #ff00ff
definition-foreground = #5e5eff
stdout-foreground = #c2d1fa
definition-background = #002240
normal-foreground = #FFFFFF
cursor-foreground = #ffffff
stdout-background = #002240
hit-foreground = #002240
comment-background = #002240
break-foreground = #FFFFFF
context-foreground= #ffffff
context-background= #454545
linenumber-foreground= gray
linenumber-background= #002240
@@ -0,0 +1,309 @@
# IDLE reads several config files to determine user preferences. This
# file is the default config file for idle key binding settings.
# Where multiple keys are specified for an action: if they are separated
# by a space (eg. action=<key1> <key2>) then the keys are alternatives, if
# there is no space (eg. action=<key1><key2>) then the keys comprise a
# single 'emacs style' multi-keystoke binding. The tk event specifier 'Key'
# is used in all cases, for consistency in auto key conflict checking in the
# configuration gui.
[IDLE Classic Windows]
copy=<Control-Key-c> <Control-Key-C>
cut=<Control-Key-x> <Control-Key-X>
paste=<Control-Key-v> <Control-Key-V>
beginning-of-line= <Key-Home>
center-insert=<Control-Key-l> <Control-Key-L>
close-all-windows=<Control-Key-q> <Control-Key-Q>
close-window=<Alt-Key-F4> <Meta-Key-F4>
do-nothing=<Control-Key-F12>
end-of-file=<Control-Key-d> <Control-Key-D>
python-docs=<Key-F1>
python-context-help=<Shift-Key-F1>
history-next=<Alt-Key-n> <Meta-Key-n> <Alt-Key-N> <Meta-Key-N>
history-previous=<Alt-Key-p> <Meta-Key-p> <Alt-Key-P> <Meta-Key-P>
interrupt-execution=<Control-Key-c> <Control-Key-C>
view-restart=<Key-F6>
restart-shell=<Control-Key-F6>
open-class-browser=<Alt-Key-c> <Meta-Key-c> <Alt-Key-C> <Meta-Key-C>
open-module=<Alt-Key-m> <Meta-Key-m> <Alt-Key-M> <Meta-Key-M>
open-new-window=<Control-Key-n> <Control-Key-N>
open-window-from-file=<Control-Key-o> <Control-Key-O>
plain-newline-and-indent=<Control-Key-j> <Control-Key-J>
print-window=<Control-Key-p> <Control-Key-P>
redo=<Control-Shift-Key-Z> <Control-Shift-Key-z>
remove-selection=<Key-Escape>
save-copy-of-window-as-file=<Alt-Shift-Key-S> <Alt-Shift-Key-s>
save-window-as-file=<Control-Shift-Key-S> <Control-Shift-Key-s>
save-window=<Control-Key-s> <Control-Key-S>
select-all=<Control-Key-a> <Control-Key-A>
toggle-auto-coloring=<Control-Key-slash>
undo=<Control-Key-z> <Control-Key-Z>
find=<Control-Key-f> <Control-Key-F>
find-again=<Control-Key-g> <Key-F3> <Control-Key-G>
find-in-files=<Alt-Key-F3> <Meta-Key-F3>
find-selection=<Control-Key-F3>
replace=<Control-Key-h> <Control-Key-H>
goto-line=<Alt-Key-g> <Meta-Key-g> <Alt-Key-G> <Meta-Key-G>
smart-backspace=<Key-BackSpace>
newline-and-indent=<Key-Return> <Key-KP_Enter>
smart-indent=<Key-Tab>
indent-region=<Control-Key-bracketright>
dedent-region=<Control-Key-bracketleft>
comment-region=<Alt-Key-3> <Meta-Key-3>
uncomment-region=<Alt-Key-4> <Meta-Key-4>
tabify-region=<Alt-Key-5> <Meta-Key-5>
untabify-region=<Alt-Key-6> <Meta-Key-6>
toggle-tabs=<Alt-Key-t> <Meta-Key-t> <Alt-Key-T> <Meta-Key-T>
change-indentwidth=<Alt-Key-u> <Meta-Key-u> <Alt-Key-U> <Meta-Key-U>
del-word-left=<Control-Key-BackSpace>
del-word-right=<Control-Key-Delete>
force-open-completions= <Control-Key-space>
expand-word= <Alt-Key-slash>
force-open-calltip= <Control-Key-backslash>
format-paragraph= <Alt-Key-q>
flash-paren= <Control-Key-0>
run-module= <Key-F5>
run-custom= <Shift-Key-F5>
check-module= <Alt-Key-x>
zoom-height= <Alt-Key-2>
[IDLE Classic Unix]
copy=<Alt-Key-w> <Meta-Key-w>
cut=<Control-Key-w>
paste=<Control-Key-y>
beginning-of-line=<Control-Key-a> <Key-Home>
center-insert=<Control-Key-l>
close-all-windows=<Control-Key-x><Control-Key-c>
close-window=<Control-Key-x><Control-Key-0>
do-nothing=<Control-Key-x>
end-of-file=<Control-Key-d>
history-next=<Alt-Key-n> <Meta-Key-n>
history-previous=<Alt-Key-p> <Meta-Key-p>
interrupt-execution=<Control-Key-c>
view-restart=<Key-F6>
restart-shell=<Control-Key-F6>
open-class-browser=<Control-Key-x><Control-Key-b>
open-module=<Control-Key-x><Control-Key-m>
open-new-window=<Control-Key-x><Control-Key-n>
open-window-from-file=<Control-Key-x><Control-Key-f>
plain-newline-and-indent=<Control-Key-j>
print-window=<Control-x><Control-Key-p>
python-docs=<Control-Key-h>
python-context-help=<Control-Shift-Key-H>
redo=<Alt-Key-z> <Meta-Key-z>
remove-selection=<Key-Escape>
save-copy-of-window-as-file=<Control-Key-x><Control-Key-y>
save-window-as-file=<Control-Key-x><Control-Key-w>
save-window=<Control-Key-x><Control-Key-s>
select-all=<Alt-Key-a> <Meta-Key-a>
toggle-auto-coloring=<Control-Key-slash>
undo=<Control-Key-z>
find=<Control-Key-u><Control-Key-u><Control-Key-s>
find-again=<Control-Key-u><Control-Key-s>
find-in-files=<Alt-Key-s> <Meta-Key-s>
find-selection=<Control-Key-s>
replace=<Control-Key-r>
goto-line=<Alt-Key-g> <Meta-Key-g>
smart-backspace=<Key-BackSpace>
newline-and-indent=<Key-Return> <Key-KP_Enter>
smart-indent=<Key-Tab>
indent-region=<Control-Key-bracketright>
dedent-region=<Control-Key-bracketleft>
comment-region=<Alt-Key-3>
uncomment-region=<Alt-Key-4>
tabify-region=<Alt-Key-5>
untabify-region=<Alt-Key-6>
toggle-tabs=<Alt-Key-t>
change-indentwidth=<Alt-Key-u>
del-word-left=<Alt-Key-BackSpace>
del-word-right=<Alt-Key-d>
force-open-completions= <Control-Key-space>
expand-word= <Alt-Key-slash>
force-open-calltip= <Control-Key-backslash>
format-paragraph= <Alt-Key-q>
flash-paren= <Control-Key-0>
run-module= <Key-F5>
run-custom= <Shift-Key-F5>
check-module= <Alt-Key-x>
zoom-height= <Alt-Key-2>
[IDLE Modern Unix]
copy = <Control-Shift-Key-C> <Control-Key-Insert>
cut = <Control-Key-x> <Shift-Key-Delete>
paste = <Control-Key-v> <Shift-Key-Insert>
beginning-of-line = <Key-Home>
center-insert = <Control-Key-l>
close-all-windows = <Control-Key-q>
close-window = <Control-Key-w> <Control-Shift-Key-W>
do-nothing = <Control-Key-F12>
end-of-file = <Control-Key-d>
history-next = <Alt-Key-n> <Meta-Key-n>
history-previous = <Alt-Key-p> <Meta-Key-p>
interrupt-execution = <Control-Key-c>
view-restart = <Key-F6>
restart-shell = <Control-Key-F6>
open-class-browser = <Control-Key-b>
open-module = <Control-Key-m>
open-new-window = <Control-Key-n>
open-window-from-file = <Control-Key-o>
plain-newline-and-indent = <Control-Key-j>
print-window = <Control-Key-p>
python-context-help = <Shift-Key-F1>
python-docs = <Key-F1>
redo = <Control-Shift-Key-Z>
remove-selection = <Key-Escape>
save-copy-of-window-as-file = <Alt-Shift-Key-S>
save-window-as-file = <Control-Shift-Key-S>
save-window = <Control-Key-s>
select-all = <Control-Key-a>
toggle-auto-coloring = <Control-Key-slash>
undo = <Control-Key-z>
find = <Control-Key-f>
find-again = <Key-F3>
find-in-files = <Control-Shift-Key-f>
find-selection = <Control-Key-h>
replace = <Control-Key-r>
goto-line = <Control-Key-g>
smart-backspace = <Key-BackSpace>
newline-and-indent = <Key-Return> <Key-KP_Enter>
smart-indent = <Key-Tab>
indent-region = <Control-Key-bracketright>
dedent-region = <Control-Key-bracketleft>
comment-region = <Control-Key-d>
uncomment-region = <Control-Shift-Key-D>
tabify-region = <Alt-Key-5>
untabify-region = <Alt-Key-6>
toggle-tabs = <Control-Key-T>
change-indentwidth = <Alt-Key-u>
del-word-left = <Control-Key-BackSpace>
del-word-right = <Control-Key-Delete>
force-open-completions= <Control-Key-space>
expand-word= <Alt-Key-slash>
force-open-calltip= <Control-Key-backslash>
format-paragraph= <Alt-Key-q>
flash-paren= <Control-Key-0>
run-module= <Key-F5>
run-custom= <Shift-Key-F5>
check-module= <Alt-Key-x>
zoom-height= <Alt-Key-2>
[IDLE Classic Mac]
copy=<Command-Key-c>
cut=<Command-Key-x>
paste=<Command-Key-v>
beginning-of-line= <Key-Home>
center-insert=<Control-Key-l>
close-all-windows=<Command-Key-q>
close-window=<Command-Key-w>
do-nothing=<Control-Key-F12>
end-of-file=<Control-Key-d>
python-docs=<Key-F1>
python-context-help=<Shift-Key-F1>
history-next=<Control-Key-n>
history-previous=<Control-Key-p>
interrupt-execution=<Control-Key-c>
view-restart=<Key-F6>
restart-shell=<Control-Key-F6>
open-class-browser=<Command-Key-b>
open-module=<Command-Key-m>
open-new-window=<Command-Key-n>
open-window-from-file=<Command-Key-o>
plain-newline-and-indent=<Control-Key-j>
print-window=<Command-Key-p>
redo=<Shift-Command-Key-Z>
remove-selection=<Key-Escape>
save-window-as-file=<Shift-Command-Key-S>
save-window=<Command-Key-s>
save-copy-of-window-as-file=<Option-Command-Key-s>
select-all=<Command-Key-a>
toggle-auto-coloring=<Control-Key-slash>
undo=<Command-Key-z>
find=<Command-Key-f>
find-again=<Command-Key-g> <Key-F3>
find-in-files=<Command-Key-F3>
find-selection=<Shift-Command-Key-F3>
replace=<Command-Key-r>
goto-line=<Command-Key-j>
smart-backspace=<Key-BackSpace>
newline-and-indent=<Key-Return> <Key-KP_Enter>
smart-indent=<Key-Tab>
indent-region=<Command-Key-bracketright>
dedent-region=<Command-Key-bracketleft>
comment-region=<Control-Key-3>
uncomment-region=<Control-Key-4>
tabify-region=<Control-Key-5>
untabify-region=<Control-Key-6>
toggle-tabs=<Control-Key-t>
change-indentwidth=<Control-Key-u>
del-word-left=<Control-Key-BackSpace>
del-word-right=<Control-Key-Delete>
force-open-completions= <Control-Key-space>
expand-word= <Option-Key-slash>
force-open-calltip= <Control-Key-backslash>
format-paragraph= <Option-Key-q>
flash-paren= <Control-Key-0>
run-module= <Key-F5>
run-custom= <Shift-Key-F5>
check-module= <Option-Key-x>
zoom-height= <Option-Key-0>
[IDLE Classic OSX]
toggle-tabs = <Control-Key-t>
interrupt-execution = <Control-Key-c>
untabify-region = <Control-Key-6>
remove-selection = <Key-Escape>
print-window = <Command-Key-p>
replace = <Command-Key-r>
goto-line = <Command-Key-j>
plain-newline-and-indent = <Control-Key-j>
history-previous = <Control-Key-p>
beginning-of-line = <Control-Key-Left>
end-of-line = <Control-Key-Right>
comment-region = <Control-Key-3>
redo = <Shift-Command-Key-Z>
close-window = <Command-Key-w>
restart-shell = <Control-Key-F6>
save-window-as-file = <Shift-Command-Key-S>
close-all-windows = <Command-Key-q>
view-restart = <Key-F6>
tabify-region = <Control-Key-5>
find-again = <Command-Key-g> <Key-F3>
find = <Command-Key-f>
toggle-auto-coloring = <Control-Key-slash>
select-all = <Command-Key-a>
smart-backspace = <Key-BackSpace>
change-indentwidth = <Control-Key-u>
do-nothing = <Control-Key-F12>
smart-indent = <Key-Tab>
center-insert = <Control-Key-l>
history-next = <Control-Key-n>
del-word-right = <Option-Key-Delete>
undo = <Command-Key-z>
save-window = <Command-Key-s>
uncomment-region = <Control-Key-4>
cut = <Command-Key-x>
find-in-files = <Command-Key-F3>
dedent-region = <Command-Key-bracketleft>
copy = <Command-Key-c>
paste = <Command-Key-v>
indent-region = <Command-Key-bracketright>
del-word-left = <Option-Key-BackSpace> <Option-Command-Key-BackSpace>
newline-and-indent = <Key-Return> <Key-KP_Enter>
end-of-file = <Control-Key-d>
open-class-browser = <Command-Key-b>
open-new-window = <Command-Key-n>
open-module = <Command-Key-m>
find-selection = <Shift-Command-Key-F3>
python-context-help = <Shift-Key-F1>
save-copy-of-window-as-file = <Option-Command-Key-s>
open-window-from-file = <Command-Key-o>
python-docs = <Key-F1>
force-open-completions= <Control-Key-space>
expand-word= <Option-Key-slash>
force-open-calltip= <Control-Key-backslash>
format-paragraph= <Option-Key-q>
flash-paren= <Control-Key-0>
run-module= <Key-F5>
run-custom= <Shift-Key-F5>
check-module= <Option-Key-x>
zoom-height= <Option-Key-0>
@@ -0,0 +1,93 @@
# IDLE reads several config files to determine user preferences. This
# file is the default config file for general idle settings.
#
# When IDLE starts, it will look in
# the following two sets of files, in order:
#
# default configuration files in idlelib
# --------------------------------------
# config-main.def default general config file
# config-extensions.def default extension config file
# config-highlight.def default highlighting config file
# config-keys.def default keybinding config file
#
# user configuration files in ~/.idlerc
# -------------------------------------
# config-main.cfg user general config file
# config-extensions.cfg user extension config file
# config-highlight.cfg user highlighting config file
# config-keys.cfg user keybinding config file
#
# On Windows, the default location of the home directory ('~' above)
# depends on the version. For Windows 10, it is C:\Users\<username>.
#
# Any options the user saves through the config dialog will be saved to
# the relevant user config file. Reverting any general or extension
# setting to the default causes that entry to be wiped from the user
# file and re-read from the default file. This rule applies to each
# item, except that the three editor font items are saved as a group.
#
# User highlighting themes and keybinding sets must have (section) names
# distinct from the default names. All items are added and saved as a
# group. They are retained unless specifically deleted within the config
# dialog. Choosing one of the default themes or keysets just applies the
# relevant settings from the default file.
#
# Additional help sources are listed in the [HelpFiles] section below
# and should be viewable by a web browser (or the Windows Help viewer in
# the case of .chm files). These sources will be listed on the Help
# menu. The pattern, and two examples, are:
#
# <sequence_number = menu item;/path/to/help/source>
# 1 = IDLE;C:/Programs/Python36/Lib/idlelib/help.html
# 2 = Pillow;https://pillow.readthedocs.io/en/latest/
#
# You can't use a semi-colon in a menu item or path. The path will be
# platform specific because of path separators, drive specs etc.
#
# The default files should not be edited except to add new sections to
# config-extensions.def for added extensions. The user files should be
# modified through the Settings dialog.
[General]
editor-on-startup= 0
autosave= 0
print-command-posix=lpr %%s
print-command-win=start /min notepad /p %%s
delete-exitfunc= 1
[EditorWindow]
width= 80
height= 40
cursor-blink= 1
font= TkFixedFont
# For TkFixedFont, the actual size and boldness are obtained from tk
# and override 10 and 0. See idlelib.config.IdleConf.GetFont
font-size= 10
font-bold= 0
encoding= none
line-numbers-default= 0
[PyShell]
auto-squeeze-min-lines= 50
[Indent]
use-spaces= 1
num-spaces= 4
[Theme]
default= 1
name= IDLE Classic
name2=
# name2 set in user config-main.cfg for themes added after 2015 Oct 1
[Keys]
default= 1
name=
name2=
# name2 set in user config-main.cfg for keys added after 2016 July 1
[History]
cyclic=1
[HelpFiles]
@@ -0,0 +1,917 @@
"""idlelib.config -- Manage IDLE configuration information.
The comments at the beginning of config-main.def describe the
configuration files and the design implemented to update user
configuration information. In particular, user configuration choices
which duplicate the defaults will be removed from the user's
configuration files, and if a user file becomes empty, it will be
deleted.
The configuration database maps options to values. Conceptually, the
database keys are tuples (config-type, section, item). As implemented,
there are separate dicts for default and user values. Each has
config-type keys 'main', 'extensions', 'highlight', and 'keys'. The
value for each key is a ConfigParser instance that maps section and item
to values. For 'main' and 'extensions', user values override
default values. For 'highlight' and 'keys', user sections augment the
default sections (and must, therefore, have distinct names).
Throughout this module there is an emphasis on returning usable defaults
when a problem occurs in returning a requested configuration value back to
idle. This is to allow IDLE to continue to function in spite of errors in
the retrieval of config information. When a default is returned instead of
a requested config value, a message is printed to stderr to aid in
configuration problem notification and resolution.
"""
# TODOs added Oct 2014, tjr
from configparser import ConfigParser
import os
import sys
from tkinter.font import Font
import idlelib
class InvalidConfigType(Exception): pass
class InvalidConfigSet(Exception): pass
class InvalidTheme(Exception): pass
class IdleConfParser(ConfigParser):
"""
A ConfigParser specialised for idle configuration file handling
"""
def __init__(self, cfgFile, cfgDefaults=None):
"""
cfgFile - string, fully specified configuration file name
"""
self.file = cfgFile # This is currently '' when testing.
ConfigParser.__init__(self, defaults=cfgDefaults, strict=False)
def Get(self, section, option, type=None, default=None, raw=False):
"""
Get an option value for given section/option or return default.
If type is specified, return as type.
"""
# TODO Use default as fallback, at least if not None
# Should also print Warning(file, section, option).
# Currently may raise ValueError
if not self.has_option(section, option):
return default
if type == 'bool':
return self.getboolean(section, option)
elif type == 'int':
return self.getint(section, option)
else:
return self.get(section, option, raw=raw)
def GetOptionList(self, section):
"Return a list of options for given section, else []."
if self.has_section(section):
return self.options(section)
else: #return a default value
return []
def Load(self):
"Load the configuration file from disk."
if self.file:
self.read(self.file)
class IdleUserConfParser(IdleConfParser):
"""
IdleConfigParser specialised for user configuration handling.
"""
def SetOption(self, section, option, value):
"""Return True if option is added or changed to value, else False.
Add section if required. False means option already had value.
"""
if self.has_option(section, option):
if self.get(section, option) == value:
return False
else:
self.set(section, option, value)
return True
else:
if not self.has_section(section):
self.add_section(section)
self.set(section, option, value)
return True
def RemoveOption(self, section, option):
"""Return True if option is removed from section, else False.
False if either section does not exist or did not have option.
"""
if self.has_section(section):
return self.remove_option(section, option)
return False
def AddSection(self, section):
"If section doesn't exist, add it."
if not self.has_section(section):
self.add_section(section)
def RemoveEmptySections(self):
"Remove any sections that have no options."
for section in self.sections():
if not self.GetOptionList(section):
self.remove_section(section)
def IsEmpty(self):
"Return True if no sections after removing empty sections."
self.RemoveEmptySections()
return not self.sections()
def Save(self):
"""Update user configuration file.
If self not empty after removing empty sections, write the file
to disk. Otherwise, remove the file from disk if it exists.
"""
fname = self.file
if fname and fname[0] != '#':
if not self.IsEmpty():
try:
cfgFile = open(fname, 'w')
except OSError:
os.unlink(fname)
cfgFile = open(fname, 'w')
with cfgFile:
self.write(cfgFile)
elif os.path.exists(self.file):
os.remove(self.file)
class IdleConf:
"""Hold config parsers for all idle config files in singleton instance.
Default config files, self.defaultCfg --
for config_type in self.config_types:
(idle install dir)/config-{config-type}.def
User config files, self.userCfg --
for config_type in self.config_types:
(user home dir)/.idlerc/config-{config-type}.cfg
"""
def __init__(self, _utest=False):
self.config_types = ('main', 'highlight', 'keys', 'extensions')
self.defaultCfg = {}
self.userCfg = {}
self.cfg = {} # TODO use to select userCfg vs defaultCfg
# See https://bugs.python.org/issue4630#msg356516 for following.
# self.blink_off_time = <first editor text>['insertofftime']
if not _utest:
self.CreateConfigHandlers()
self.LoadCfgFiles()
def CreateConfigHandlers(self):
"Populate default and user config parser dictionaries."
idledir = os.path.dirname(__file__)
self.userdir = userdir = '' if idlelib.testing else self.GetUserCfgDir()
for cfg_type in self.config_types:
self.defaultCfg[cfg_type] = IdleConfParser(
os.path.join(idledir, f'config-{cfg_type}.def'))
self.userCfg[cfg_type] = IdleUserConfParser(
os.path.join(userdir or '#', f'config-{cfg_type}.cfg'))
def GetUserCfgDir(self):
"""Return a filesystem directory for storing user config files.
Creates it if required.
"""
cfgDir = '.idlerc'
userDir = os.path.expanduser('~')
if userDir != '~': # expanduser() found user home dir
if not os.path.exists(userDir):
if not idlelib.testing:
warn = ('\n Warning: os.path.expanduser("~") points to\n ' +
userDir + ',\n but the path does not exist.')
try:
print(warn, file=sys.stderr)
except OSError:
pass
userDir = '~'
if userDir == "~": # still no path to home!
# traditionally IDLE has defaulted to os.getcwd(), is this adequate?
userDir = os.getcwd()
userDir = os.path.join(userDir, cfgDir)
if not os.path.exists(userDir):
try:
os.mkdir(userDir)
except OSError:
if not idlelib.testing:
warn = ('\n Warning: unable to create user config directory\n' +
userDir + '\n Check path and permissions.\n Exiting!\n')
try:
print(warn, file=sys.stderr)
except OSError:
pass
raise SystemExit
# TODO continue without userDIr instead of exit
return userDir
def GetOption(self, configType, section, option, default=None, type=None,
warn_on_default=True, raw=False):
"""Return a value for configType section option, or default.
If type is not None, return a value of that type. Also pass raw
to the config parser. First try to return a valid value
(including type) from a user configuration. If that fails, try
the default configuration. If that fails, return default, with a
default of None.
Warn if either user or default configurations have an invalid value.
Warn if default is returned and warn_on_default is True.
"""
try:
if self.userCfg[configType].has_option(section, option):
return self.userCfg[configType].Get(section, option,
type=type, raw=raw)
except ValueError:
warning = ('\n Warning: config.py - IdleConf.GetOption -\n'
' invalid %r value for configuration option %r\n'
' from section %r: %r' %
(type, option, section,
self.userCfg[configType].Get(section, option, raw=raw)))
_warn(warning, configType, section, option)
try:
if self.defaultCfg[configType].has_option(section,option):
return self.defaultCfg[configType].Get(
section, option, type=type, raw=raw)
except ValueError:
pass
#returning default, print warning
if warn_on_default:
warning = ('\n Warning: config.py - IdleConf.GetOption -\n'
' problem retrieving configuration option %r\n'
' from section %r.\n'
' returning default value: %r' %
(option, section, default))
_warn(warning, configType, section, option)
return default
def SetOption(self, configType, section, option, value):
"""Set section option to value in user config file."""
self.userCfg[configType].SetOption(section, option, value)
def GetSectionList(self, configSet, configType):
"""Return sections for configSet configType configuration.
configSet must be either 'user' or 'default'
configType must be in self.config_types.
"""
if not (configType in self.config_types):
raise InvalidConfigType('Invalid configType specified')
if configSet == 'user':
cfgParser = self.userCfg[configType]
elif configSet == 'default':
cfgParser=self.defaultCfg[configType]
else:
raise InvalidConfigSet('Invalid configSet specified')
return cfgParser.sections()
def GetHighlight(self, theme, element):
"""Return dict of theme element highlight colors.
The keys are 'foreground' and 'background'. The values are
tkinter color strings for configuring backgrounds and tags.
"""
cfg = ('default' if self.defaultCfg['highlight'].has_section(theme)
else 'user')
theme_dict = self.GetThemeDict(cfg, theme)
fore = theme_dict[element + '-foreground']
if element == 'cursor':
element = 'normal'
back = theme_dict[element + '-background']
return {"foreground": fore, "background": back}
def GetThemeDict(self, type, themeName):
"""Return {option:value} dict for elements in themeName.
type - string, 'default' or 'user' theme type
themeName - string, theme name
Values are loaded over ultimate fallback defaults to guarantee
that all theme elements are present in a newly created theme.
"""
if type == 'user':
cfgParser = self.userCfg['highlight']
elif type == 'default':
cfgParser = self.defaultCfg['highlight']
else:
raise InvalidTheme('Invalid theme type specified')
# Provide foreground and background colors for each theme
# element (other than cursor) even though some values are not
# yet used by idle, to allow for their use in the future.
# Default values are generally black and white.
# TODO copy theme from a class attribute.
theme ={'normal-foreground':'#000000',
'normal-background':'#ffffff',
'keyword-foreground':'#000000',
'keyword-background':'#ffffff',
'builtin-foreground':'#000000',
'builtin-background':'#ffffff',
'comment-foreground':'#000000',
'comment-background':'#ffffff',
'string-foreground':'#000000',
'string-background':'#ffffff',
'definition-foreground':'#000000',
'definition-background':'#ffffff',
'hilite-foreground':'#000000',
'hilite-background':'gray',
'break-foreground':'#ffffff',
'break-background':'#000000',
'hit-foreground':'#ffffff',
'hit-background':'#000000',
'error-foreground':'#ffffff',
'error-background':'#000000',
'context-foreground':'#000000',
'context-background':'#ffffff',
'linenumber-foreground':'#000000',
'linenumber-background':'#ffffff',
#cursor (only foreground can be set)
'cursor-foreground':'#000000',
#shell window
'stdout-foreground':'#000000',
'stdout-background':'#ffffff',
'stderr-foreground':'#000000',
'stderr-background':'#ffffff',
'console-foreground':'#000000',
'console-background':'#ffffff',
}
for element in theme:
if not (cfgParser.has_option(themeName, element) or
# Skip warning for new elements.
element.startswith(('context-', 'linenumber-'))):
# Print warning that will return a default color
warning = ('\n Warning: config.IdleConf.GetThemeDict'
' -\n problem retrieving theme element %r'
'\n from theme %r.\n'
' returning default color: %r' %
(element, themeName, theme[element]))
_warn(warning, 'highlight', themeName, element)
theme[element] = cfgParser.Get(
themeName, element, default=theme[element])
return theme
def CurrentTheme(self):
"Return the name of the currently active text color theme."
return self.current_colors_and_keys('Theme')
def CurrentKeys(self):
"""Return the name of the currently active key set."""
return self.current_colors_and_keys('Keys')
def current_colors_and_keys(self, section):
"""Return the currently active name for Theme or Keys section.
idlelib.config-main.def ('default') includes these sections
[Theme]
default= 1
name= IDLE Classic
name2=
[Keys]
default= 1
name=
name2=
Item 'name2', is used for built-in ('default') themes and keys
added after 2015 Oct 1 and 2016 July 1. This kludge is needed
because setting 'name' to a builtin not defined in older IDLEs
to display multiple error messages or quit.
See https://bugs.python.org/issue25313.
When default = True, 'name2' takes precedence over 'name',
while older IDLEs will just use name. When default = False,
'name2' may still be set, but it is ignored.
"""
cfgname = 'highlight' if section == 'Theme' else 'keys'
default = self.GetOption('main', section, 'default',
type='bool', default=True)
name = ''
if default:
name = self.GetOption('main', section, 'name2', default='')
if not name:
name = self.GetOption('main', section, 'name', default='')
if name:
source = self.defaultCfg if default else self.userCfg
if source[cfgname].has_section(name):
return name
return "IDLE Classic" if section == 'Theme' else self.default_keys()
@staticmethod
def default_keys():
if sys.platform[:3] == 'win':
return 'IDLE Classic Windows'
elif sys.platform == 'darwin':
return 'IDLE Classic OSX'
else:
return 'IDLE Modern Unix'
def GetExtensions(self, active_only=True,
editor_only=False, shell_only=False):
"""Return extensions in default and user config-extensions files.
If active_only True, only return active (enabled) extensions
and optionally only editor or shell extensions.
If active_only False, return all extensions.
"""
extns = self.RemoveKeyBindNames(
self.GetSectionList('default', 'extensions'))
userExtns = self.RemoveKeyBindNames(
self.GetSectionList('user', 'extensions'))
for extn in userExtns:
if extn not in extns: #user has added own extension
extns.append(extn)
for extn in ('AutoComplete','CodeContext',
'FormatParagraph','ParenMatch'):
extns.remove(extn)
# specific exclusions because we are storing config for mainlined old
# extensions in config-extensions.def for backward compatibility
if active_only:
activeExtns = []
for extn in extns:
if self.GetOption('extensions', extn, 'enable', default=True,
type='bool'):
#the extension is enabled
if editor_only or shell_only: # TODO both True contradict
if editor_only:
option = "enable_editor"
else:
option = "enable_shell"
if self.GetOption('extensions', extn,option,
default=True, type='bool',
warn_on_default=False):
activeExtns.append(extn)
else:
activeExtns.append(extn)
return activeExtns
else:
return extns
def RemoveKeyBindNames(self, extnNameList):
"Return extnNameList with keybinding section names removed."
return [n for n in extnNameList if not n.endswith(('_bindings', '_cfgBindings'))]
def GetExtnNameForEvent(self, virtualEvent):
"""Return the name of the extension binding virtualEvent, or None.
virtualEvent - string, name of the virtual event to test for,
without the enclosing '<< >>'
"""
extName = None
vEvent = '<<' + virtualEvent + '>>'
for extn in self.GetExtensions(active_only=0):
for event in self.GetExtensionKeys(extn):
if event == vEvent:
extName = extn # TODO return here?
return extName
def GetExtensionKeys(self, extensionName):
"""Return dict: {configurable extensionName event : active keybinding}.
Events come from default config extension_cfgBindings section.
Keybindings come from GetCurrentKeySet() active key dict,
where previously used bindings are disabled.
"""
keysName = extensionName + '_cfgBindings'
activeKeys = self.GetCurrentKeySet()
extKeys = {}
if self.defaultCfg['extensions'].has_section(keysName):
eventNames = self.defaultCfg['extensions'].GetOptionList(keysName)
for eventName in eventNames:
event = '<<' + eventName + '>>'
binding = activeKeys[event]
extKeys[event] = binding
return extKeys
def __GetRawExtensionKeys(self,extensionName):
"""Return dict {configurable extensionName event : keybinding list}.
Events come from default config extension_cfgBindings section.
Keybindings list come from the splitting of GetOption, which
tries user config before default config.
"""
keysName = extensionName+'_cfgBindings'
extKeys = {}
if self.defaultCfg['extensions'].has_section(keysName):
eventNames = self.defaultCfg['extensions'].GetOptionList(keysName)
for eventName in eventNames:
binding = self.GetOption(
'extensions', keysName, eventName, default='').split()
event = '<<' + eventName + '>>'
extKeys[event] = binding
return extKeys
def GetExtensionBindings(self, extensionName):
"""Return dict {extensionName event : active or defined keybinding}.
Augment self.GetExtensionKeys(extensionName) with mapping of non-
configurable events (from default config) to GetOption splits,
as in self.__GetRawExtensionKeys.
"""
bindsName = extensionName + '_bindings'
extBinds = self.GetExtensionKeys(extensionName)
#add the non-configurable bindings
if self.defaultCfg['extensions'].has_section(bindsName):
eventNames = self.defaultCfg['extensions'].GetOptionList(bindsName)
for eventName in eventNames:
binding = self.GetOption(
'extensions', bindsName, eventName, default='').split()
event = '<<' + eventName + '>>'
extBinds[event] = binding
return extBinds
def GetKeyBinding(self, keySetName, eventStr):
"""Return the keybinding list for keySetName eventStr.
keySetName - name of key binding set (config-keys section).
eventStr - virtual event, including brackets, as in '<<event>>'.
"""
eventName = eventStr[2:-2] #trim off the angle brackets
binding = self.GetOption('keys', keySetName, eventName, default='',
warn_on_default=False).split()
return binding
def GetCurrentKeySet(self):
"Return CurrentKeys with 'darwin' modifications."
result = self.GetKeySet(self.CurrentKeys())
if sys.platform == "darwin":
# macOS (OS X) Tk variants do not support the "Alt"
# keyboard modifier. Replace it with "Option".
# TODO (Ned?): the "Option" modifier does not work properly
# for Cocoa Tk and XQuartz Tk so we should not use it
# in the default 'OSX' keyset.
for k, v in result.items():
v2 = [ x.replace('<Alt-', '<Option-') for x in v ]
if v != v2:
result[k] = v2
return result
def GetKeySet(self, keySetName):
"""Return event-key dict for keySetName core plus active extensions.
If a binding defined in an extension is already in use, the
extension binding is disabled by being set to ''
"""
keySet = self.GetCoreKeys(keySetName)
activeExtns = self.GetExtensions(active_only=1)
for extn in activeExtns:
extKeys = self.__GetRawExtensionKeys(extn)
if extKeys: #the extension defines keybindings
for event in extKeys:
if extKeys[event] in keySet.values():
#the binding is already in use
extKeys[event] = '' #disable this binding
keySet[event] = extKeys[event] #add binding
return keySet
def IsCoreBinding(self, virtualEvent):
"""Return True if the virtual event is one of the core idle key events.
virtualEvent - string, name of the virtual event to test for,
without the enclosing '<< >>'
"""
return ('<<'+virtualEvent+'>>') in self.GetCoreKeys()
# TODO make keyBindings a file or class attribute used for test above
# and copied in function below.
former_extension_events = { # Those with user-configurable keys.
'<<force-open-completions>>', '<<expand-word>>',
'<<force-open-calltip>>', '<<flash-paren>>', '<<format-paragraph>>',
'<<run-module>>', '<<check-module>>', '<<zoom-height>>',
'<<run-custom>>',
}
def GetCoreKeys(self, keySetName=None):
"""Return dict of core virtual-key keybindings for keySetName.
The default keySetName None corresponds to the keyBindings base
dict. If keySetName is not None, bindings from the config
file(s) are loaded _over_ these defaults, so if there is a
problem getting any core binding there will be an 'ultimate last
resort fallback' to the CUA-ish bindings defined here.
"""
# TODO: = dict(sorted([(v-event, keys), ...]))?
keyBindings={
# virtual-event: list of key events.
'<<copy>>': ['<Control-c>', '<Control-C>'],
'<<cut>>': ['<Control-x>', '<Control-X>'],
'<<paste>>': ['<Control-v>', '<Control-V>'],
'<<beginning-of-line>>': ['<Control-a>', '<Home>'],
'<<center-insert>>': ['<Control-l>'],
'<<close-all-windows>>': ['<Control-q>'],
'<<close-window>>': ['<Alt-F4>'],
'<<do-nothing>>': ['<Control-x>'],
'<<end-of-file>>': ['<Control-d>'],
'<<python-docs>>': ['<F1>'],
'<<python-context-help>>': ['<Shift-F1>'],
'<<history-next>>': ['<Alt-n>'],
'<<history-previous>>': ['<Alt-p>'],
'<<interrupt-execution>>': ['<Control-c>'],
'<<view-restart>>': ['<F6>'],
'<<restart-shell>>': ['<Control-F6>'],
'<<open-class-browser>>': ['<Alt-c>'],
'<<open-module>>': ['<Alt-m>'],
'<<open-new-window>>': ['<Control-n>'],
'<<open-window-from-file>>': ['<Control-o>'],
'<<plain-newline-and-indent>>': ['<Control-j>'],
'<<print-window>>': ['<Control-p>'],
'<<redo>>': ['<Control-y>'],
'<<remove-selection>>': ['<Escape>'],
'<<save-copy-of-window-as-file>>': ['<Alt-Shift-S>'],
'<<save-window-as-file>>': ['<Alt-s>'],
'<<save-window>>': ['<Control-s>'],
'<<select-all>>': ['<Alt-a>'],
'<<toggle-auto-coloring>>': ['<Control-slash>'],
'<<undo>>': ['<Control-z>'],
'<<find-again>>': ['<Control-g>', '<F3>'],
'<<find-in-files>>': ['<Alt-F3>'],
'<<find-selection>>': ['<Control-F3>'],
'<<find>>': ['<Control-f>'],
'<<replace>>': ['<Control-h>'],
'<<goto-line>>': ['<Alt-g>'],
'<<smart-backspace>>': ['<Key-BackSpace>'],
'<<newline-and-indent>>': ['<Key-Return>', '<Key-KP_Enter>'],
'<<smart-indent>>': ['<Key-Tab>'],
'<<indent-region>>': ['<Control-Key-bracketright>'],
'<<dedent-region>>': ['<Control-Key-bracketleft>'],
'<<comment-region>>': ['<Alt-Key-3>'],
'<<uncomment-region>>': ['<Alt-Key-4>'],
'<<tabify-region>>': ['<Alt-Key-5>'],
'<<untabify-region>>': ['<Alt-Key-6>'],
'<<toggle-tabs>>': ['<Alt-Key-t>'],
'<<change-indentwidth>>': ['<Alt-Key-u>'],
'<<del-word-left>>': ['<Control-Key-BackSpace>'],
'<<del-word-right>>': ['<Control-Key-Delete>'],
'<<force-open-completions>>': ['<Control-Key-space>'],
'<<expand-word>>': ['<Alt-Key-slash>'],
'<<force-open-calltip>>': ['<Control-Key-backslash>'],
'<<flash-paren>>': ['<Control-Key-0>'],
'<<format-paragraph>>': ['<Alt-Key-q>'],
'<<run-module>>': ['<Key-F5>'],
'<<run-custom>>': ['<Shift-Key-F5>'],
'<<check-module>>': ['<Alt-Key-x>'],
'<<zoom-height>>': ['<Alt-Key-2>'],
}
if keySetName:
if not (self.userCfg['keys'].has_section(keySetName) or
self.defaultCfg['keys'].has_section(keySetName)):
warning = (
'\n Warning: config.py - IdleConf.GetCoreKeys -\n'
' key set %r is not defined, using default bindings.' %
(keySetName,)
)
_warn(warning, 'keys', keySetName)
else:
for event in keyBindings:
binding = self.GetKeyBinding(keySetName, event)
if binding:
keyBindings[event] = binding
# Otherwise return default in keyBindings.
elif event not in self.former_extension_events:
warning = (
'\n Warning: config.py - IdleConf.GetCoreKeys -\n'
' problem retrieving key binding for event %r\n'
' from key set %r.\n'
' returning default value: %r' %
(event, keySetName, keyBindings[event])
)
_warn(warning, 'keys', keySetName, event)
return keyBindings
def GetExtraHelpSourceList(self, configSet):
"""Return list of extra help sources from a given configSet.
Valid configSets are 'user' or 'default'. Return a list of tuples of
the form (menu_item , path_to_help_file , option), or return the empty
list. 'option' is the sequence number of the help resource. 'option'
values determine the position of the menu items on the Help menu,
therefore the returned list must be sorted by 'option'.
"""
helpSources = []
if configSet == 'user':
cfgParser = self.userCfg['main']
elif configSet == 'default':
cfgParser = self.defaultCfg['main']
else:
raise InvalidConfigSet('Invalid configSet specified')
options=cfgParser.GetOptionList('HelpFiles')
for option in options:
value=cfgParser.Get('HelpFiles', option, default=';')
if value.find(';') == -1: #malformed config entry with no ';'
menuItem = '' #make these empty
helpPath = '' #so value won't be added to list
else: #config entry contains ';' as expected
value=value.split(';')
menuItem=value[0].strip()
helpPath=value[1].strip()
if menuItem and helpPath: #neither are empty strings
helpSources.append( (menuItem,helpPath,option) )
helpSources.sort(key=lambda x: x[2])
return helpSources
def GetAllExtraHelpSourcesList(self):
"""Return a list of the details of all additional help sources.
Tuples in the list are those of GetExtraHelpSourceList.
"""
allHelpSources = (self.GetExtraHelpSourceList('default') +
self.GetExtraHelpSourceList('user') )
return allHelpSources
def GetFont(self, root, configType, section):
"""Retrieve a font from configuration (font, font-size, font-bold)
Intercept the special value 'TkFixedFont' and substitute
the actual font, factoring in some tweaks if needed for
appearance sakes.
The 'root' parameter can normally be any valid Tkinter widget.
Return a tuple (family, size, weight) suitable for passing
to tkinter.Font
"""
family = self.GetOption(configType, section, 'font', default='courier')
size = self.GetOption(configType, section, 'font-size', type='int',
default='10')
bold = self.GetOption(configType, section, 'font-bold', default=0,
type='bool')
if (family == 'TkFixedFont'):
f = Font(name='TkFixedFont', exists=True, root=root)
actualFont = Font.actual(f)
family = actualFont['family']
size = actualFont['size']
if size <= 0:
size = 10 # if font in pixels, ignore actual size
bold = actualFont['weight'] == 'bold'
return (family, size, 'bold' if bold else 'normal')
def LoadCfgFiles(self):
"Load all configuration files."
for key in self.defaultCfg:
self.defaultCfg[key].Load()
self.userCfg[key].Load() #same keys
def SaveUserCfgFiles(self):
"Write all loaded user configuration files to disk."
for key in self.userCfg:
self.userCfg[key].Save()
idleConf = IdleConf()
_warned = set()
def _warn(msg, *key):
key = (msg,) + key
if key not in _warned:
try:
print(msg, file=sys.stderr)
except OSError:
pass
_warned.add(key)
class ConfigChanges(dict):
"""Manage a user's proposed configuration option changes.
Names used across multiple methods:
page -- one of the 4 top-level dicts representing a
.idlerc/config-x.cfg file.
config_type -- name of a page.
section -- a section within a page/file.
option -- name of an option within a section.
value -- value for the option.
Methods
add_option: Add option and value to changes.
save_option: Save option and value to config parser.
save_all: Save all the changes to the config parser and file.
delete_section: If section exists,
delete from changes, userCfg, and file.
clear: Clear all changes by clearing each page.
"""
def __init__(self):
"Create a page for each configuration file"
self.pages = [] # List of unhashable dicts.
for config_type in idleConf.config_types:
self[config_type] = {}
self.pages.append(self[config_type])
def add_option(self, config_type, section, item, value):
"Add item/value pair for config_type and section."
page = self[config_type]
value = str(value) # Make sure we use a string.
if section not in page:
page[section] = {}
page[section][item] = value
@staticmethod
def save_option(config_type, section, item, value):
"""Return True if the configuration value was added or changed.
Helper for save_all.
"""
if idleConf.defaultCfg[config_type].has_option(section, item):
if idleConf.defaultCfg[config_type].Get(section, item) == value:
# The setting equals a default setting, remove it from user cfg.
return idleConf.userCfg[config_type].RemoveOption(section, item)
# If we got here, set the option.
return idleConf.userCfg[config_type].SetOption(section, item, value)
def save_all(self):
"""Save configuration changes to the user config file.
Clear self in preparation for additional changes.
Return changed for testing.
"""
idleConf.userCfg['main'].Save()
changed = False
for config_type in self:
cfg_type_changed = False
page = self[config_type]
for section in page:
if section == 'HelpFiles': # Remove it for replacement.
idleConf.userCfg['main'].remove_section('HelpFiles')
cfg_type_changed = True
for item, value in page[section].items():
if self.save_option(config_type, section, item, value):
cfg_type_changed = True
if cfg_type_changed:
idleConf.userCfg[config_type].Save()
changed = True
for config_type in ['keys', 'highlight']:
# Save these even if unchanged!
idleConf.userCfg[config_type].Save()
self.clear()
# ConfigDialog caller must add the following call
# self.save_all_changed_extensions() # Uses a different mechanism.
return changed
def delete_section(self, config_type, section):
"""Delete a section from self, userCfg, and file.
Used to delete custom themes and keysets.
"""
if section in self[config_type]:
del self[config_type][section]
configpage = idleConf.userCfg[config_type]
configpage.remove_section(section)
configpage.Save()
def clear(self):
"""Clear all 4 pages.
Called in save_all after saving to idleConf.
XXX Mark window *title* when there are changes; unmark here.
"""
for page in self.pages:
page.clear()
# TODO Revise test output, write expanded unittest
def _dump(): # htest # (not really, but ignore in coverage)
from zlib import crc32
line, crc = 0, 0
def sprint(obj):
nonlocal line, crc
txt = str(obj)
line += 1
crc = crc32(txt.encode(encoding='utf-8'), crc)
print(txt)
#print('***', line, crc, '***') # Uncomment for diagnosis.
def dumpCfg(cfg):
print('\n', cfg, '\n') # Cfg has variable '0xnnnnnnnn' address.
for key in sorted(cfg):
sections = cfg[key].sections()
sprint(key)
sprint(sections)
for section in sections:
options = cfg[key].options(section)
sprint(section)
sprint(options)
for option in options:
sprint(option + ' = ' + cfg[key].Get(section, option))
dumpCfg(idleConf.defaultCfg)
dumpCfg(idleConf.userCfg)
print('\nlines = ', line, ', crc = ', crc, sep='')
if __name__ == '__main__':
from unittest import main
main('idlelib.idle_test.test_config', verbosity=2, exit=False)
_dump()
# Run revised _dump() (700+ lines) as htest? More sorting.
# Perhaps as window with tabs for textviews, making it config viewer.
@@ -0,0 +1,354 @@
"""
Dialog for building Tkinter accelerator key bindings
"""
from tkinter import Toplevel, Listbox, StringVar, TclError
from tkinter.ttk import Frame, Button, Checkbutton, Entry, Label, Scrollbar
from tkinter import messagebox
from tkinter.simpledialog import _setup_dialog
import string
import sys
FUNCTION_KEYS = ('F1', 'F2' ,'F3' ,'F4' ,'F5' ,'F6',
'F7', 'F8' ,'F9' ,'F10' ,'F11' ,'F12')
ALPHANUM_KEYS = tuple(string.ascii_lowercase + string.digits)
PUNCTUATION_KEYS = tuple('~!@#%^&*()_-+={}[]|;:,.<>/?')
WHITESPACE_KEYS = ('Tab', 'Space', 'Return')
EDIT_KEYS = ('BackSpace', 'Delete', 'Insert')
MOVE_KEYS = ('Home', 'End', 'Page Up', 'Page Down', 'Left Arrow',
'Right Arrow', 'Up Arrow', 'Down Arrow')
AVAILABLE_KEYS = (ALPHANUM_KEYS + PUNCTUATION_KEYS + FUNCTION_KEYS +
WHITESPACE_KEYS + EDIT_KEYS + MOVE_KEYS)
def translate_key(key, modifiers):
"Translate from keycap symbol to the Tkinter keysym."
mapping = {'Space':'space',
'~':'asciitilde', '!':'exclam', '@':'at', '#':'numbersign',
'%':'percent', '^':'asciicircum', '&':'ampersand',
'*':'asterisk', '(':'parenleft', ')':'parenright',
'_':'underscore', '-':'minus', '+':'plus', '=':'equal',
'{':'braceleft', '}':'braceright',
'[':'bracketleft', ']':'bracketright', '|':'bar',
';':'semicolon', ':':'colon', ',':'comma', '.':'period',
'<':'less', '>':'greater', '/':'slash', '?':'question',
'Page Up':'Prior', 'Page Down':'Next',
'Left Arrow':'Left', 'Right Arrow':'Right',
'Up Arrow':'Up', 'Down Arrow': 'Down', 'Tab':'Tab'}
key = mapping.get(key, key)
if 'Shift' in modifiers and key in string.ascii_lowercase:
key = key.upper()
return f'Key-{key}'
class GetKeysFrame(Frame):
# Dialog title for invalid key sequence
keyerror_title = 'Key Sequence Error'
def __init__(self, parent, action, current_key_sequences):
"""
parent - parent of this dialog
action - the name of the virtual event these keys will be
mapped to
current_key_sequences - a list of all key sequence lists
currently mapped to virtual events, for overlap checking
"""
super().__init__(parent)
self['borderwidth'] = 2
self['relief'] = 'sunken'
self.parent = parent
self.action = action
self.current_key_sequences = current_key_sequences
self.result = ''
self.key_string = StringVar(self)
self.key_string.set('')
# Set self.modifiers, self.modifier_label.
self.set_modifiers_for_platform()
self.modifier_vars = []
for modifier in self.modifiers:
variable = StringVar(self)
variable.set('')
self.modifier_vars.append(variable)
self.advanced = False
self.create_widgets()
def showerror(self, *args, **kwargs):
# Make testing easier. Replace in #30751.
messagebox.showerror(*args, **kwargs)
def create_widgets(self):
# Basic entry key sequence.
self.frame_keyseq_basic = Frame(self, name='keyseq_basic')
self.frame_keyseq_basic.grid(row=0, column=0, sticky='nsew',
padx=5, pady=5)
basic_title = Label(self.frame_keyseq_basic,
text=f"New keys for '{self.action}' :")
basic_title.pack(anchor='w')
basic_keys = Label(self.frame_keyseq_basic, justify='left',
textvariable=self.key_string, relief='groove',
borderwidth=2)
basic_keys.pack(ipadx=5, ipady=5, fill='x')
# Basic entry controls.
self.frame_controls_basic = Frame(self)
self.frame_controls_basic.grid(row=1, column=0, sticky='nsew', padx=5)
# Basic entry modifiers.
self.modifier_checkbuttons = {}
column = 0
for modifier, variable in zip(self.modifiers, self.modifier_vars):
label = self.modifier_label.get(modifier, modifier)
check = Checkbutton(self.frame_controls_basic,
command=self.build_key_string, text=label,
variable=variable, onvalue=modifier, offvalue='')
check.grid(row=0, column=column, padx=2, sticky='w')
self.modifier_checkbuttons[modifier] = check
column += 1
# Basic entry help text.
help_basic = Label(self.frame_controls_basic, justify='left',
text="Select the desired modifier keys\n"+
"above, and the final key from the\n"+
"list on the right.\n\n" +
"Use upper case Symbols when using\n" +
"the Shift modifier. (Letters will be\n" +
"converted automatically.)")
help_basic.grid(row=1, column=0, columnspan=4, padx=2, sticky='w')
# Basic entry key list.
self.list_keys_final = Listbox(self.frame_controls_basic, width=15,
height=10, selectmode='single')
self.list_keys_final.insert('end', *AVAILABLE_KEYS)
self.list_keys_final.bind('<ButtonRelease-1>', self.final_key_selected)
self.list_keys_final.grid(row=0, column=4, rowspan=4, sticky='ns')
scroll_keys_final = Scrollbar(self.frame_controls_basic,
orient='vertical',
command=self.list_keys_final.yview)
self.list_keys_final.config(yscrollcommand=scroll_keys_final.set)
scroll_keys_final.grid(row=0, column=5, rowspan=4, sticky='ns')
self.button_clear = Button(self.frame_controls_basic,
text='Clear Keys',
command=self.clear_key_seq)
self.button_clear.grid(row=2, column=0, columnspan=4)
# Advanced entry key sequence.
self.frame_keyseq_advanced = Frame(self, name='keyseq_advanced')
self.frame_keyseq_advanced.grid(row=0, column=0, sticky='nsew',
padx=5, pady=5)
advanced_title = Label(self.frame_keyseq_advanced, justify='left',
text=f"Enter new binding(s) for '{self.action}' :\n" +
"(These bindings will not be checked for validity!)")
advanced_title.pack(anchor='w')
self.advanced_keys = Entry(self.frame_keyseq_advanced,
textvariable=self.key_string)
self.advanced_keys.pack(fill='x')
# Advanced entry help text.
self.frame_help_advanced = Frame(self)
self.frame_help_advanced.grid(row=1, column=0, sticky='nsew', padx=5)
help_advanced = Label(self.frame_help_advanced, justify='left',
text="Key bindings are specified using Tkinter keysyms as\n"+
"in these samples: <Control-f>, <Shift-F2>, <F12>,\n"
"<Control-space>, <Meta-less>, <Control-Alt-Shift-X>.\n"
"Upper case is used when the Shift modifier is present!\n\n" +
"'Emacs style' multi-keystroke bindings are specified as\n" +
"follows: <Control-x><Control-y>, where the first key\n" +
"is the 'do-nothing' keybinding.\n\n" +
"Multiple separate bindings for one action should be\n"+
"separated by a space, eg., <Alt-v> <Meta-v>." )
help_advanced.grid(row=0, column=0, sticky='nsew')
# Switch between basic and advanced.
self.button_level = Button(self, command=self.toggle_level,
text='<< Basic Key Binding Entry')
self.button_level.grid(row=2, column=0, stick='ew', padx=5, pady=5)
self.toggle_level()
def set_modifiers_for_platform(self):
"""Determine list of names of key modifiers for this platform.
The names are used to build Tk bindings -- it doesn't matter if the
keyboard has these keys; it matters if Tk understands them. The
order is also important: key binding equality depends on it, so
config-keys.def must use the same ordering.
"""
if sys.platform == "darwin":
self.modifiers = ['Shift', 'Control', 'Option', 'Command']
else:
self.modifiers = ['Control', 'Alt', 'Shift']
self.modifier_label = {'Control': 'Ctrl'} # Short name.
def toggle_level(self):
"Toggle between basic and advanced keys."
if self.button_level.cget('text').startswith('Advanced'):
self.clear_key_seq()
self.button_level.config(text='<< Basic Key Binding Entry')
self.frame_keyseq_advanced.lift()
self.frame_help_advanced.lift()
self.advanced_keys.focus_set()
self.advanced = True
else:
self.clear_key_seq()
self.button_level.config(text='Advanced Key Binding Entry >>')
self.frame_keyseq_basic.lift()
self.frame_controls_basic.lift()
self.advanced = False
def final_key_selected(self, event=None):
"Handler for clicking on key in basic settings list."
self.build_key_string()
def build_key_string(self):
"Create formatted string of modifiers plus the key."
keylist = modifiers = self.get_modifiers()
final_key = self.list_keys_final.get('anchor')
if final_key:
final_key = translate_key(final_key, modifiers)
keylist.append(final_key)
self.key_string.set(f"<{'-'.join(keylist)}>")
def get_modifiers(self):
"Return ordered list of modifiers that have been selected."
mod_list = [variable.get() for variable in self.modifier_vars]
return [mod for mod in mod_list if mod]
def clear_key_seq(self):
"Clear modifiers and keys selection."
self.list_keys_final.select_clear(0, 'end')
self.list_keys_final.yview('moveto', '0.0')
for variable in self.modifier_vars:
variable.set('')
self.key_string.set('')
def ok(self):
self.result = ''
keys = self.key_string.get().strip()
if not keys:
self.showerror(title=self.keyerror_title, parent=self,
message="No key specified.")
return
if (self.advanced or self.keys_ok(keys)) and self.bind_ok(keys):
self.result = keys
return
def keys_ok(self, keys):
"""Validity check on user's 'basic' keybinding selection.
Doesn't check the string produced by the advanced dialog because
'modifiers' isn't set.
"""
final_key = self.list_keys_final.get('anchor')
modifiers = self.get_modifiers()
title = self.keyerror_title
key_sequences = [key for keylist in self.current_key_sequences
for key in keylist]
if not keys.endswith('>'):
self.showerror(title, parent=self,
message='Missing the final Key')
elif (not modifiers
and final_key not in FUNCTION_KEYS + MOVE_KEYS):
self.showerror(title=title, parent=self,
message='No modifier key(s) specified.')
elif (modifiers == ['Shift']) \
and (final_key not in
FUNCTION_KEYS + MOVE_KEYS + ('Tab', 'Space')):
msg = 'The shift modifier by itself may not be used with'\
' this key symbol.'
self.showerror(title=title, parent=self, message=msg)
elif keys in key_sequences:
msg = 'This key combination is already in use.'
self.showerror(title=title, parent=self, message=msg)
else:
return True
return False
def bind_ok(self, keys):
"Return True if Tcl accepts the new keys else show message."
try:
binding = self.bind(keys, lambda: None)
except TclError as err:
self.showerror(
title=self.keyerror_title, parent=self,
message=(f'The entered key sequence is not accepted.\n\n'
f'Error: {err}'))
return False
else:
self.unbind(keys, binding)
return True
class GetKeysWindow(Toplevel):
def __init__(self, parent, title, action, current_key_sequences,
*, _htest=False, _utest=False):
"""
parent - parent of this dialog
title - string which is the title of the popup dialog
action - string, the name of the virtual event these keys will be
mapped to
current_key_sequences - list, a list of all key sequence lists
currently mapped to virtual events, for overlap checking
_htest - bool, change box location when running htest
_utest - bool, do not wait when running unittest
"""
super().__init__(parent)
self.withdraw() # Hide while setting geometry.
self['borderwidth'] = 5
self.resizable(height=False, width=False)
# Needed for winfo_reqwidth().
self.update_idletasks()
# Center dialog over parent (or below htest box).
x = (parent.winfo_rootx() +
(parent.winfo_width()//2 - self.winfo_reqwidth()//2))
y = (parent.winfo_rooty() +
((parent.winfo_height()//2 - self.winfo_reqheight()//2)
if not _htest else 150))
self.geometry(f"+{x}+{y}")
self.title(title)
self.frame = frame = GetKeysFrame(self, action, current_key_sequences)
self.protocol("WM_DELETE_WINDOW", self.cancel)
frame_buttons = Frame(self)
self.button_ok = Button(frame_buttons, text='OK',
width=8, command=self.ok)
self.button_cancel = Button(frame_buttons, text='Cancel',
width=8, command=self.cancel)
self.button_ok.grid(row=0, column=0, padx=5, pady=5)
self.button_cancel.grid(row=0, column=1, padx=5, pady=5)
frame.pack(side='top', expand=True, fill='both')
frame_buttons.pack(side='bottom', fill='x')
self.transient(parent)
_setup_dialog(self)
self.grab_set()
if not _utest:
self.deiconify() # Geometry set, unhide.
self.wait_window()
@property
def result(self):
return self.frame.result
@result.setter
def result(self, value):
self.frame.result = value
def ok(self, event=None):
self.frame.ok()
self.grab_release()
self.destroy()
def cancel(self, event=None):
self.result = ''
self.grab_release()
self.destroy()
if __name__ == '__main__':
from unittest import main
main('idlelib.idle_test.test_config_key', verbosity=2, exit=False)
from idlelib.idle_test.htest import run
run(GetKeysWindow)
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,602 @@
"""Debug user code with a GUI interface to a subclass of bdb.Bdb.
The Idb idb and Debugger gui instances each need a reference to each
other or to an rpc proxy for each other.
If IDLE is started with '-n', so that user code and idb both run in the
IDLE process, Debugger is called without an idb. Debugger.__init__
calls Idb with its incomplete self. Idb.__init__ stores gui and gui
then stores idb.
If IDLE is started normally, so that user code executes in a separate
process, debugger_r.start_remote_debugger is called, executing in the
IDLE process. It calls 'start the debugger' in the remote process,
which calls Idb with a gui proxy. Then Debugger is called in the IDLE
for more.
"""
import bdb
import os
from tkinter import *
from tkinter.ttk import Frame, Scrollbar
from idlelib import macosx
from idlelib.scrolledlist import ScrolledList
from idlelib.window import ListedToplevel
class Idb(bdb.Bdb):
"Supply user_line and user_exception functions for Bdb."
def __init__(self, gui):
self.gui = gui # An instance of Debugger or proxy thereof.
super().__init__()
def user_line(self, frame):
"""Handle a user stopping or breaking at a line.
Convert frame to a string and send it to gui.
"""
if _in_rpc_code(frame):
self.set_step()
return
message = _frame2message(frame)
try:
self.gui.interaction(message, frame)
except TclError: # When closing debugger window with [x] in 3.x
pass
def user_exception(self, frame, exc_info):
"""Handle an the occurrence of an exception."""
if _in_rpc_code(frame):
self.set_step()
return
message = _frame2message(frame)
self.gui.interaction(message, frame, exc_info)
def _in_rpc_code(frame):
"Determine if debugger is within RPC code."
if frame.f_code.co_filename.count('rpc.py'):
return True # Skip this frame.
else:
prev_frame = frame.f_back
if prev_frame is None:
return False
prev_name = prev_frame.f_code.co_filename
if 'idlelib' in prev_name and 'debugger' in prev_name:
# catch both idlelib/debugger.py and idlelib/debugger_r.py
# on both Posix and Windows
return False
return _in_rpc_code(prev_frame)
def _frame2message(frame):
"""Return a message string for frame."""
code = frame.f_code
filename = code.co_filename
lineno = frame.f_lineno
basename = os.path.basename(filename)
message = f"{basename}:{lineno}"
if code.co_name != "?":
message = f"{message}: {code.co_name}()"
return message
class Debugger:
"""The debugger interface.
This class handles the drawing of the debugger window and
the interactions with the underlying debugger session.
"""
vstack = None
vsource = None
vlocals = None
vglobals = None
stackviewer = None
localsviewer = None
globalsviewer = None
def __init__(self, pyshell, idb=None):
"""Instantiate and draw a debugger window.
:param pyshell: An instance of the PyShell Window
:type pyshell: :class:`idlelib.pyshell.PyShell`
:param idb: An instance of the IDLE debugger (optional)
:type idb: :class:`idlelib.debugger.Idb`
"""
if idb is None:
idb = Idb(self)
self.pyshell = pyshell
self.idb = idb # If passed, a proxy of remote instance.
self.frame = None
self.make_gui()
self.interacting = False
self.nesting_level = 0
def run(self, *args):
"""Run the debugger."""
# Deal with the scenario where we've already got a program running
# in the debugger and we want to start another. If that is the case,
# our second 'run' was invoked from an event dispatched not from
# the main event loop, but from the nested event loop in 'interaction'
# below. So our stack looks something like this:
# outer main event loop
# run()
# <running program with traces>
# callback to debugger's interaction()
# nested event loop
# run() for second command
#
# This kind of nesting of event loops causes all kinds of problems
# (see e.g. issue #24455) especially when dealing with running as a
# subprocess, where there's all kinds of extra stuff happening in
# there - insert a traceback.print_stack() to check it out.
#
# By this point, we've already called restart_subprocess() in
# ScriptBinding. However, we also need to unwind the stack back to
# that outer event loop. To accomplish this, we:
# - return immediately from the nested run()
# - abort_loop ensures the nested event loop will terminate
# - the debugger's interaction routine completes normally
# - the restart_subprocess() will have taken care of stopping
# the running program, which will also let the outer run complete
#
# That leaves us back at the outer main event loop, at which point our
# after event can fire, and we'll come back to this routine with a
# clean stack.
if self.nesting_level > 0:
self.abort_loop()
self.root.after(100, lambda: self.run(*args))
return
try:
self.interacting = True
return self.idb.run(*args)
finally:
self.interacting = False
def close(self, event=None):
"""Close the debugger and window."""
try:
self.quit()
except Exception:
pass
if self.interacting:
self.top.bell()
return
if self.stackviewer:
self.stackviewer.close(); self.stackviewer = None
# Clean up pyshell if user clicked debugger control close widget.
# (Causes a harmless extra cycle through close_debugger() if user
# toggled debugger from pyshell Debug menu)
self.pyshell.close_debugger()
# Now close the debugger control window....
self.top.destroy()
def make_gui(self):
"""Draw the debugger gui on the screen."""
pyshell = self.pyshell
self.flist = pyshell.flist
self.root = root = pyshell.root
self.top = top = ListedToplevel(root)
self.top.wm_title("Debug Control")
self.top.wm_iconname("Debug")
top.wm_protocol("WM_DELETE_WINDOW", self.close)
self.top.bind("<Escape>", self.close)
self.bframe = bframe = Frame(top)
self.bframe.pack(anchor="w")
self.buttons = bl = []
self.bcont = b = Button(bframe, text="Go", command=self.cont)
bl.append(b)
self.bstep = b = Button(bframe, text="Step", command=self.step)
bl.append(b)
self.bnext = b = Button(bframe, text="Over", command=self.next)
bl.append(b)
self.bret = b = Button(bframe, text="Out", command=self.ret)
bl.append(b)
self.bret = b = Button(bframe, text="Quit", command=self.quit)
bl.append(b)
for b in bl:
b.configure(state="disabled")
b.pack(side="left")
self.cframe = cframe = Frame(bframe)
self.cframe.pack(side="left")
if not self.vstack:
self.__class__.vstack = BooleanVar(top)
self.vstack.set(1)
self.bstack = Checkbutton(cframe,
text="Stack", command=self.show_stack, variable=self.vstack)
self.bstack.grid(row=0, column=0)
if not self.vsource:
self.__class__.vsource = BooleanVar(top)
self.bsource = Checkbutton(cframe,
text="Source", command=self.show_source, variable=self.vsource)
self.bsource.grid(row=0, column=1)
if not self.vlocals:
self.__class__.vlocals = BooleanVar(top)
self.vlocals.set(1)
self.blocals = Checkbutton(cframe,
text="Locals", command=self.show_locals, variable=self.vlocals)
self.blocals.grid(row=1, column=0)
if not self.vglobals:
self.__class__.vglobals = BooleanVar(top)
self.bglobals = Checkbutton(cframe,
text="Globals", command=self.show_globals, variable=self.vglobals)
self.bglobals.grid(row=1, column=1)
self.status = Label(top, anchor="w")
self.status.pack(anchor="w")
self.error = Label(top, anchor="w")
self.error.pack(anchor="w", fill="x")
self.errorbg = self.error.cget("background")
self.fstack = Frame(top, height=1)
self.fstack.pack(expand=1, fill="both")
self.flocals = Frame(top)
self.flocals.pack(expand=1, fill="both")
self.fglobals = Frame(top, height=1)
self.fglobals.pack(expand=1, fill="both")
if self.vstack.get():
self.show_stack()
if self.vlocals.get():
self.show_locals()
if self.vglobals.get():
self.show_globals()
def interaction(self, message, frame, info=None):
self.frame = frame
self.status.configure(text=message)
if info:
type, value, tb = info
try:
m1 = type.__name__
except AttributeError:
m1 = "%s" % str(type)
if value is not None:
try:
# TODO redo entire section, tries not needed.
m1 = f"{m1}: {value}"
except:
pass
bg = "yellow"
else:
m1 = ""
tb = None
bg = self.errorbg
self.error.configure(text=m1, background=bg)
sv = self.stackviewer
if sv:
stack, i = self.idb.get_stack(self.frame, tb)
sv.load_stack(stack, i)
self.show_variables(1)
if self.vsource.get():
self.sync_source_line()
for b in self.buttons:
b.configure(state="normal")
self.top.wakeup()
# Nested main loop: Tkinter's main loop is not reentrant, so use
# Tcl's vwait facility, which reenters the event loop until an
# event handler sets the variable we're waiting on.
self.nesting_level += 1
self.root.tk.call('vwait', '::idledebugwait')
self.nesting_level -= 1
for b in self.buttons:
b.configure(state="disabled")
self.status.configure(text="")
self.error.configure(text="", background=self.errorbg)
self.frame = None
def sync_source_line(self):
frame = self.frame
if not frame:
return
filename, lineno = self.__frame2fileline(frame)
if filename[:1] + filename[-1:] != "<>" and os.path.exists(filename):
self.flist.gotofileline(filename, lineno)
def __frame2fileline(self, frame):
code = frame.f_code
filename = code.co_filename
lineno = frame.f_lineno
return filename, lineno
def cont(self):
self.idb.set_continue()
self.abort_loop()
def step(self):
self.idb.set_step()
self.abort_loop()
def next(self):
self.idb.set_next(self.frame)
self.abort_loop()
def ret(self):
self.idb.set_return(self.frame)
self.abort_loop()
def quit(self):
self.idb.set_quit()
self.abort_loop()
def abort_loop(self):
self.root.tk.call('set', '::idledebugwait', '1')
def show_stack(self):
if not self.stackviewer and self.vstack.get():
self.stackviewer = sv = StackViewer(self.fstack, self.flist, self)
if self.frame:
stack, i = self.idb.get_stack(self.frame, None)
sv.load_stack(stack, i)
else:
sv = self.stackviewer
if sv and not self.vstack.get():
self.stackviewer = None
sv.close()
self.fstack['height'] = 1
def show_source(self):
if self.vsource.get():
self.sync_source_line()
def show_frame(self, stackitem):
self.frame = stackitem[0] # lineno is stackitem[1]
self.show_variables()
def show_locals(self):
lv = self.localsviewer
if self.vlocals.get():
if not lv:
self.localsviewer = NamespaceViewer(self.flocals, "Locals")
else:
if lv:
self.localsviewer = None
lv.close()
self.flocals['height'] = 1
self.show_variables()
def show_globals(self):
gv = self.globalsviewer
if self.vglobals.get():
if not gv:
self.globalsviewer = NamespaceViewer(self.fglobals, "Globals")
else:
if gv:
self.globalsviewer = None
gv.close()
self.fglobals['height'] = 1
self.show_variables()
def show_variables(self, force=0):
lv = self.localsviewer
gv = self.globalsviewer
frame = self.frame
if not frame:
ldict = gdict = None
else:
ldict = frame.f_locals
gdict = frame.f_globals
if lv and gv and ldict is gdict:
ldict = None
if lv:
lv.load_dict(ldict, force, self.pyshell.interp.rpcclt)
if gv:
gv.load_dict(gdict, force, self.pyshell.interp.rpcclt)
def set_breakpoint(self, filename, lineno):
"""Set a filename-lineno breakpoint in the debugger.
Called from self.load_breakpoints and EW.setbreakpoint
"""
self.idb.set_break(filename, lineno)
def clear_breakpoint(self, filename, lineno):
self.idb.clear_break(filename, lineno)
def clear_file_breaks(self, filename):
self.idb.clear_all_file_breaks(filename)
def load_breakpoints(self):
"""Load PyShellEditorWindow breakpoints into subprocess debugger."""
for editwin in self.pyshell.flist.inversedict:
filename = editwin.io.filename
try:
for lineno in editwin.breakpoints:
self.set_breakpoint(filename, lineno)
except AttributeError:
continue
class StackViewer(ScrolledList):
"Code stack viewer for debugger GUI."
def __init__(self, master, flist, gui):
if macosx.isAquaTk():
# At least on with the stock AquaTk version on OSX 10.4 you'll
# get a shaking GUI that eventually kills IDLE if the width
# argument is specified.
ScrolledList.__init__(self, master)
else:
ScrolledList.__init__(self, master, width=80)
self.flist = flist
self.gui = gui
self.stack = []
def load_stack(self, stack, index=None):
self.stack = stack
self.clear()
for i in range(len(stack)):
frame, lineno = stack[i]
try:
modname = frame.f_globals["__name__"]
except:
modname = "?"
code = frame.f_code
filename = code.co_filename
funcname = code.co_name
import linecache
sourceline = linecache.getline(filename, lineno)
sourceline = sourceline.strip()
if funcname in ("?", "", None):
item = "%s, line %d: %s" % (modname, lineno, sourceline)
else:
item = "%s.%s(), line %d: %s" % (modname, funcname,
lineno, sourceline)
if i == index:
item = "> " + item
self.append(item)
if index is not None:
self.select(index)
def popup_event(self, event):
"Override base method."
if self.stack:
return ScrolledList.popup_event(self, event)
def fill_menu(self):
"Override base method."
menu = self.menu
menu.add_command(label="Go to source line",
command=self.goto_source_line)
menu.add_command(label="Show stack frame",
command=self.show_stack_frame)
def on_select(self, index):
"Override base method."
if 0 <= index < len(self.stack):
self.gui.show_frame(self.stack[index])
def on_double(self, index):
"Override base method."
self.show_source(index)
def goto_source_line(self):
index = self.listbox.index("active")
self.show_source(index)
def show_stack_frame(self):
index = self.listbox.index("active")
if 0 <= index < len(self.stack):
self.gui.show_frame(self.stack[index])
def show_source(self, index):
if not (0 <= index < len(self.stack)):
return
frame, lineno = self.stack[index]
code = frame.f_code
filename = code.co_filename
if os.path.isfile(filename):
edit = self.flist.open(filename)
if edit:
edit.gotoline(lineno)
class NamespaceViewer:
"Global/local namespace viewer for debugger GUI."
def __init__(self, master, title, odict=None): # XXX odict never passed.
width = 0
height = 40
if odict:
height = 20*len(odict) # XXX 20 == observed height of Entry widget
self.master = master
self.title = title
import reprlib
self.repr = reprlib.Repr()
self.repr.maxstring = 60
self.repr.maxother = 60
self.frame = frame = Frame(master)
self.frame.pack(expand=1, fill="both")
self.label = Label(frame, text=title, borderwidth=2, relief="groove")
self.label.pack(fill="x")
self.vbar = vbar = Scrollbar(frame, name="vbar")
vbar.pack(side="right", fill="y")
self.canvas = canvas = Canvas(frame,
height=min(300, max(40, height)),
scrollregion=(0, 0, width, height))
canvas.pack(side="left", fill="both", expand=1)
vbar["command"] = canvas.yview
canvas["yscrollcommand"] = vbar.set
self.subframe = subframe = Frame(canvas)
self.sfid = canvas.create_window(0, 0, window=subframe, anchor="nw")
self.load_dict(odict)
prev_odict = -1 # Needed for initial comparison below.
def load_dict(self, odict, force=0, rpc_client=None):
if odict is self.prev_odict and not force:
return
subframe = self.subframe
frame = self.frame
for c in list(subframe.children.values()):
c.destroy()
self.prev_odict = None
if not odict:
l = Label(subframe, text="None")
l.grid(row=0, column=0)
else:
#names = sorted(dict)
#
# Because of (temporary) limitations on the dict_keys type (not yet
# public or pickleable), have the subprocess to send a list of
# keys, not a dict_keys object. sorted() will take a dict_keys
# (no subprocess) or a list.
#
# There is also an obscure bug in sorted(dict) where the
# interpreter gets into a loop requesting non-existing dict[0],
# dict[1], dict[2], etc from the debugger_r.DictProxy.
# TODO recheck above; see debugger_r 159ff, debugobj 60.
keys_list = odict.keys()
names = sorted(keys_list)
row = 0
for name in names:
value = odict[name]
svalue = self.repr.repr(value) # repr(value)
# Strip extra quotes caused by calling repr on the (already)
# repr'd value sent across the RPC interface:
if rpc_client:
svalue = svalue[1:-1]
l = Label(subframe, text=name)
l.grid(row=row, column=0, sticky="nw")
l = Entry(subframe, width=0, borderwidth=0)
l.insert(0, svalue)
l.grid(row=row, column=1, sticky="nw")
row = row+1
self.prev_odict = odict
# XXX Could we use a <Configure> callback for the following?
subframe.update_idletasks() # Alas!
width = subframe.winfo_reqwidth()
height = subframe.winfo_reqheight()
canvas = self.canvas
self.canvas["scrollregion"] = (0, 0, width, height)
if height > 300:
canvas["height"] = 300
frame.pack(expand=1)
else:
canvas["height"] = height
frame.pack(expand=0)
def close(self):
self.frame.destroy()
if __name__ == "__main__":
from unittest import main
main('idlelib.idle_test.test_debugger', verbosity=2, exit=False)
# TODO: htest?
@@ -0,0 +1,390 @@
"""Support for remote Python debugging.
Some ASCII art to describe the structure:
IN PYTHON SUBPROCESS # IN IDLE PROCESS
#
# oid='gui_adapter'
+----------+ # +------------+ +-----+
| GUIProxy |--remote#call-->| GUIAdapter |--calls-->| GUI |
+-----+--calls-->+----------+ # +------------+ +-----+
| Idb | # /
+-----+<-calls--+------------+ # +----------+<--calls-/
| IdbAdapter |<--remote#call--| IdbProxy |
+------------+ # +----------+
oid='idb_adapter' #
The purpose of the Proxy and Adapter classes is to translate certain
arguments and return values that cannot be transported through the RPC
barrier, in particular frame and traceback objects.
"""
import reprlib
import types
from idlelib import debugger
debugging = 0
idb_adap_oid = "idb_adapter"
gui_adap_oid = "gui_adapter"
#=======================================
#
# In the PYTHON subprocess:
frametable = {}
dicttable = {}
codetable = {}
tracebacktable = {}
def wrap_frame(frame):
fid = id(frame)
frametable[fid] = frame
return fid
def wrap_info(info):
"replace info[2], a traceback instance, by its ID"
if info is None:
return None
else:
traceback = info[2]
assert isinstance(traceback, types.TracebackType)
traceback_id = id(traceback)
tracebacktable[traceback_id] = traceback
modified_info = (info[0], info[1], traceback_id)
return modified_info
class GUIProxy:
def __init__(self, conn, gui_adap_oid):
self.conn = conn
self.oid = gui_adap_oid
def interaction(self, message, frame, info=None):
# calls rpc.SocketIO.remotecall() via run.MyHandler instance
# pass frame and traceback object IDs instead of the objects themselves
self.conn.remotecall(self.oid, "interaction",
(message, wrap_frame(frame), wrap_info(info)),
{})
class IdbAdapter:
def __init__(self, idb):
self.idb = idb
#----------called by an IdbProxy----------
def set_step(self):
self.idb.set_step()
def set_quit(self):
self.idb.set_quit()
def set_continue(self):
self.idb.set_continue()
def set_next(self, fid):
frame = frametable[fid]
self.idb.set_next(frame)
def set_return(self, fid):
frame = frametable[fid]
self.idb.set_return(frame)
def get_stack(self, fid, tbid):
frame = frametable[fid]
if tbid is None:
tb = None
else:
tb = tracebacktable[tbid]
stack, i = self.idb.get_stack(frame, tb)
stack = [(wrap_frame(frame2), k) for frame2, k in stack]
return stack, i
def run(self, cmd):
import __main__
self.idb.run(cmd, __main__.__dict__)
def set_break(self, filename, lineno):
msg = self.idb.set_break(filename, lineno)
return msg
def clear_break(self, filename, lineno):
msg = self.idb.clear_break(filename, lineno)
return msg
def clear_all_file_breaks(self, filename):
msg = self.idb.clear_all_file_breaks(filename)
return msg
#----------called by a FrameProxy----------
def frame_attr(self, fid, name):
frame = frametable[fid]
return getattr(frame, name)
def frame_globals(self, fid):
frame = frametable[fid]
gdict = frame.f_globals
did = id(gdict)
dicttable[did] = gdict
return did
def frame_locals(self, fid):
frame = frametable[fid]
ldict = frame.f_locals
did = id(ldict)
dicttable[did] = ldict
return did
def frame_code(self, fid):
frame = frametable[fid]
code = frame.f_code
cid = id(code)
codetable[cid] = code
return cid
#----------called by a CodeProxy----------
def code_name(self, cid):
code = codetable[cid]
return code.co_name
def code_filename(self, cid):
code = codetable[cid]
return code.co_filename
#----------called by a DictProxy----------
def dict_keys(self, did):
raise NotImplementedError("dict_keys not public or pickleable")
## return dicttable[did].keys()
### Needed until dict_keys type is finished and pickleable.
# xxx finished. pickleable?
### Will probably need to extend rpc.py:SocketIO._proxify at that time.
def dict_keys_list(self, did):
return list(dicttable[did].keys())
def dict_item(self, did, key):
value = dicttable[did][key]
return reprlib.repr(value) # Can't pickle module 'builtins'.
#----------end class IdbAdapter----------
def start_debugger(rpchandler, gui_adap_oid):
"""Start the debugger and its RPC link in the Python subprocess
Start the subprocess side of the split debugger and set up that side of the
RPC link by instantiating the GUIProxy, Idb debugger, and IdbAdapter
objects and linking them together. Register the IdbAdapter with the
RPCServer to handle RPC requests from the split debugger GUI via the
IdbProxy.
"""
gui_proxy = GUIProxy(rpchandler, gui_adap_oid)
idb = debugger.Idb(gui_proxy)
idb_adap = IdbAdapter(idb)
rpchandler.register(idb_adap_oid, idb_adap)
return idb_adap_oid
#=======================================
#
# In the IDLE process:
class FrameProxy:
def __init__(self, conn, fid):
self._conn = conn
self._fid = fid
self._oid = "idb_adapter"
self._dictcache = {}
def __getattr__(self, name):
if name[:1] == "_":
raise AttributeError(name)
if name == "f_code":
return self._get_f_code()
if name == "f_globals":
return self._get_f_globals()
if name == "f_locals":
return self._get_f_locals()
return self._conn.remotecall(self._oid, "frame_attr",
(self._fid, name), {})
def _get_f_code(self):
cid = self._conn.remotecall(self._oid, "frame_code", (self._fid,), {})
return CodeProxy(self._conn, self._oid, cid)
def _get_f_globals(self):
did = self._conn.remotecall(self._oid, "frame_globals",
(self._fid,), {})
return self._get_dict_proxy(did)
def _get_f_locals(self):
did = self._conn.remotecall(self._oid, "frame_locals",
(self._fid,), {})
return self._get_dict_proxy(did)
def _get_dict_proxy(self, did):
if did in self._dictcache:
return self._dictcache[did]
dp = DictProxy(self._conn, self._oid, did)
self._dictcache[did] = dp
return dp
class CodeProxy:
def __init__(self, conn, oid, cid):
self._conn = conn
self._oid = oid
self._cid = cid
def __getattr__(self, name):
if name == "co_name":
return self._conn.remotecall(self._oid, "code_name",
(self._cid,), {})
if name == "co_filename":
return self._conn.remotecall(self._oid, "code_filename",
(self._cid,), {})
class DictProxy:
def __init__(self, conn, oid, did):
self._conn = conn
self._oid = oid
self._did = did
## def keys(self):
## return self._conn.remotecall(self._oid, "dict_keys", (self._did,), {})
# 'temporary' until dict_keys is a pickleable built-in type
def keys(self):
return self._conn.remotecall(self._oid,
"dict_keys_list", (self._did,), {})
def __getitem__(self, key):
return self._conn.remotecall(self._oid, "dict_item",
(self._did, key), {})
def __getattr__(self, name):
##print("*** Failed DictProxy.__getattr__:", name)
raise AttributeError(name)
class GUIAdapter:
def __init__(self, conn, gui):
self.conn = conn
self.gui = gui
def interaction(self, message, fid, modified_info):
##print("*** Interaction: (%s, %s, %s)" % (message, fid, modified_info))
frame = FrameProxy(self.conn, fid)
self.gui.interaction(message, frame, modified_info)
class IdbProxy:
def __init__(self, conn, shell, oid):
self.oid = oid
self.conn = conn
self.shell = shell
def call(self, methodname, /, *args, **kwargs):
##print("*** IdbProxy.call %s %s %s" % (methodname, args, kwargs))
value = self.conn.remotecall(self.oid, methodname, args, kwargs)
##print("*** IdbProxy.call %s returns %r" % (methodname, value))
return value
def run(self, cmd, locals):
# Ignores locals on purpose!
seq = self.conn.asyncqueue(self.oid, "run", (cmd,), {})
self.shell.interp.active_seq = seq
def get_stack(self, frame, tbid):
# passing frame and traceback IDs, not the objects themselves
stack, i = self.call("get_stack", frame._fid, tbid)
stack = [(FrameProxy(self.conn, fid), k) for fid, k in stack]
return stack, i
def set_continue(self):
self.call("set_continue")
def set_step(self):
self.call("set_step")
def set_next(self, frame):
self.call("set_next", frame._fid)
def set_return(self, frame):
self.call("set_return", frame._fid)
def set_quit(self):
self.call("set_quit")
def set_break(self, filename, lineno):
msg = self.call("set_break", filename, lineno)
return msg
def clear_break(self, filename, lineno):
msg = self.call("clear_break", filename, lineno)
return msg
def clear_all_file_breaks(self, filename):
msg = self.call("clear_all_file_breaks", filename)
return msg
def start_remote_debugger(rpcclt, pyshell):
"""Start the subprocess debugger, initialize the debugger GUI and RPC link
Request the RPCServer start the Python subprocess debugger and link. Set
up the Idle side of the split debugger by instantiating the IdbProxy,
debugger GUI, and debugger GUIAdapter objects and linking them together.
Register the GUIAdapter with the RPCClient to handle debugger GUI
interaction requests coming from the subprocess debugger via the GUIProxy.
The IdbAdapter will pass execution and environment requests coming from the
Idle debugger GUI to the subprocess debugger via the IdbProxy.
"""
global idb_adap_oid
idb_adap_oid = rpcclt.remotecall("exec", "start_the_debugger",\
(gui_adap_oid,), {})
idb_proxy = IdbProxy(rpcclt, pyshell, idb_adap_oid)
gui = debugger.Debugger(pyshell, idb_proxy)
gui_adap = GUIAdapter(rpcclt, gui)
rpcclt.register(gui_adap_oid, gui_adap)
return gui
def close_remote_debugger(rpcclt):
"""Shut down subprocess debugger and Idle side of debugger RPC link
Request that the RPCServer shut down the subprocess debugger and link.
Unregister the GUIAdapter, which will cause a GC on the Idle process
debugger and RPC link objects. (The second reference to the debugger GUI
is deleted in pyshell.close_remote_debugger().)
"""
close_subprocess_debugger(rpcclt)
rpcclt.unregister(gui_adap_oid)
def close_subprocess_debugger(rpcclt):
rpcclt.remotecall("exec", "stop_the_debugger", (idb_adap_oid,), {})
def restart_subprocess_debugger(rpcclt):
idb_adap_oid_ret = rpcclt.remotecall("exec", "start_the_debugger",\
(gui_adap_oid,), {})
assert idb_adap_oid_ret == idb_adap_oid, 'Idb restarted with different oid'
if __name__ == "__main__":
from unittest import main
main('idlelib.idle_test.test_debugger_r', verbosity=2, exit=False)
@@ -0,0 +1,146 @@
"""Define tree items for debug stackviewer, which is only user.
"""
# XXX TO DO:
# - popup menu
# - support partial or total redisplay
# - more doc strings
# - tooltips
# object browser
# XXX TO DO:
# - for classes/modules, add "open source" to object browser
from reprlib import Repr
from idlelib.tree import TreeItem, TreeNode, ScrolledCanvas
myrepr = Repr()
myrepr.maxstring = 100
myrepr.maxother = 100
class ObjectTreeItem(TreeItem):
def __init__(self, labeltext, object_, setfunction=None):
self.labeltext = labeltext
self.object = object_
self.setfunction = setfunction
def GetLabelText(self):
return self.labeltext
def GetText(self):
return myrepr.repr(self.object)
def GetIconName(self):
if not self.IsExpandable():
return "python"
def IsEditable(self):
return self.setfunction is not None
def SetText(self, text):
try:
value = eval(text)
self.setfunction(value)
except:
pass
else:
self.object = value
def IsExpandable(self):
return not not dir(self.object)
def GetSubList(self):
keys = dir(self.object)
sublist = []
for key in keys:
try:
value = getattr(self.object, key)
except AttributeError:
continue
item = make_objecttreeitem(
str(key) + " =",
value,
lambda value, key=key, object_=self.object:
setattr(object_, key, value))
sublist.append(item)
return sublist
class ClassTreeItem(ObjectTreeItem):
def IsExpandable(self):
return True
def GetSubList(self):
sublist = ObjectTreeItem.GetSubList(self)
if len(self.object.__bases__) == 1:
item = make_objecttreeitem("__bases__[0] =",
self.object.__bases__[0])
else:
item = make_objecttreeitem("__bases__ =", self.object.__bases__)
sublist.insert(0, item)
return sublist
class AtomicObjectTreeItem(ObjectTreeItem):
def IsExpandable(self):
return False
class SequenceTreeItem(ObjectTreeItem):
def IsExpandable(self):
return len(self.object) > 0
def keys(self):
return range(len(self.object))
def GetSubList(self):
sublist = []
for key in self.keys():
try:
value = self.object[key]
except KeyError:
continue
def setfunction(value, key=key, object_=self.object):
object_[key] = value
item = make_objecttreeitem(f"{key!r}:", value, setfunction)
sublist.append(item)
return sublist
class DictTreeItem(SequenceTreeItem):
def keys(self):
# TODO return sorted(self.object)
keys = list(self.object)
try:
keys.sort()
except:
pass
return keys
dispatch = {
int: AtomicObjectTreeItem,
float: AtomicObjectTreeItem,
str: AtomicObjectTreeItem,
tuple: SequenceTreeItem,
list: SequenceTreeItem,
dict: DictTreeItem,
type: ClassTreeItem,
}
def make_objecttreeitem(labeltext, object_, setfunction=None):
t = type(object_)
if t in dispatch:
c = dispatch[t]
else:
c = ObjectTreeItem
return c(labeltext, object_, setfunction)
def _debug_object_browser(parent): # htest #
import sys
from tkinter import Toplevel
top = Toplevel(parent)
top.title("Test debug object browser")
x, y = map(int, parent.geometry().split('+')[1:])
top.geometry("+%d+%d" % (x + 100, y + 175))
top.configure(bd=0, bg="yellow")
top.focus_set()
sc = ScrolledCanvas(top, bg="white", highlightthickness=0, takefocus=1)
sc.frame.pack(expand=1, fill="both")
item = make_objecttreeitem("sys", sys)
node = TreeNode(sc.canvas, None, item)
node.update()
if __name__ == '__main__':
from unittest import main
main('idlelib.idle_test.test_debugobj', verbosity=2, exit=False)
from idlelib.idle_test.htest import run
run(_debug_object_browser)
@@ -0,0 +1,41 @@
from idlelib import rpc
def remote_object_tree_item(item):
wrapper = WrappedObjectTreeItem(item)
oid = id(wrapper)
rpc.objecttable[oid] = wrapper
return oid
class WrappedObjectTreeItem:
# Lives in PYTHON subprocess
def __init__(self, item):
self.__item = item
def __getattr__(self, name):
value = getattr(self.__item, name)
return value
def _GetSubList(self):
sub_list = self.__item._GetSubList()
return list(map(remote_object_tree_item, sub_list))
class StubObjectTreeItem:
# Lives in IDLE process
def __init__(self, sockio, oid):
self.sockio = sockio
self.oid = oid
def __getattr__(self, name):
value = rpc.MethodProxy(self.sockio, self.oid, name)
return value
def _GetSubList(self):
sub_list = self.sockio.remotecall(self.oid, "_GetSubList", (), {})
return [StubObjectTreeItem(self.sockio, oid) for oid in sub_list]
if __name__ == '__main__':
from unittest import main
main('idlelib.idle_test.test_debugobj_r', verbosity=2)
@@ -0,0 +1,34 @@
class Delegator:
def __init__(self, delegate=None):
self.delegate = delegate
self.__cache = set()
# Cache is used to only remove added attributes
# when changing the delegate.
def __getattr__(self, name):
attr = getattr(self.delegate, name) # May raise AttributeError
setattr(self, name, attr)
self.__cache.add(name)
return attr
def resetcache(self):
"Removes added attributes while leaving original attributes."
# Function is really about resetting delegator dict
# to original state. Cache is just a means
for key in self.__cache:
try:
delattr(self, key)
except AttributeError:
pass
self.__cache.clear()
def setdelegate(self, delegate):
"Reset attributes and change delegate."
self.resetcache()
self.delegate = delegate
if __name__ == '__main__':
from unittest import main
main('idlelib.idle_test.test_delegator', verbosity=2)
@@ -0,0 +1,57 @@
"""
OptionMenu widget modified to allow dynamic menu reconfiguration
and setting of highlightthickness
"""
from tkinter import OptionMenu, _setit, StringVar, Button
class DynOptionMenu(OptionMenu):
"""Add SetMenu and highlightthickness to OptionMenu.
Highlightthickness adds space around menu button.
"""
def __init__(self, master, variable, value, *values, **kwargs):
highlightthickness = kwargs.pop('highlightthickness', None)
OptionMenu.__init__(self, master, variable, value, *values, **kwargs)
self['highlightthickness'] = highlightthickness
self.variable = variable
self.command = kwargs.get('command')
def SetMenu(self,valueList,value=None):
"""
clear and reload the menu with a new set of options.
valueList - list of new options
value - initial value to set the optionmenu's menubutton to
"""
self['menu'].delete(0,'end')
for item in valueList:
self['menu'].add_command(label=item,
command=_setit(self.variable,item,self.command))
if value:
self.variable.set(value)
def _dyn_option_menu(parent): # htest #
from tkinter import Toplevel # + StringVar, Button
top = Toplevel(parent)
top.title("Test dynamic option menu")
x, y = map(int, parent.geometry().split('+')[1:])
top.geometry("200x100+%d+%d" % (x + 250, y + 175))
top.focus_set()
var = StringVar(top)
var.set("Old option set") #Set the default value
dyn = DynOptionMenu(top, var, "old1","old2","old3","old4",
highlightthickness=5)
dyn.pack()
def update():
dyn.SetMenu(["new1","new2","new3","new4"], value="new option set")
button = Button(top, text="Change option set", command=update)
button.pack()
if __name__ == '__main__':
# Only module without unittests because of intention to replace.
from idlelib.idle_test.htest import run
run(_dyn_option_menu)
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,83 @@
Writing an IDLE extension
=========================
An IDLE extension can define new key bindings and menu entries for IDLE
edit windows. There is a simple mechanism to load extensions when IDLE
starts up and to attach them to each edit window. (It is also possible
to make other changes to IDLE, but this must be done by editing the IDLE
source code.)
The list of extensions loaded at startup time is configured by editing
the file config-extensions.def. See below for details.
An IDLE extension is defined by a class. Methods of the class define
actions that are invoked by event bindings or menu entries. Class (or
instance) variables define the bindings and menu additions; these are
automatically applied by IDLE when the extension is linked to an edit
window.
An IDLE extension class is instantiated with a single argument,
`editwin', an EditorWindow instance. The extension cannot assume much
about this argument, but it is guaranteed to have the following instance
variables:
text a Text instance (a widget)
io an IOBinding instance (more about this later)
flist the FileList instance (shared by all edit windows)
(There are a few more, but they are rarely useful.)
The extension class must not directly bind Window Manager (e.g. X) events.
Rather, it must define one or more virtual events, e.g. <<z-in>>, and
corresponding methods, e.g. z_in_event(). The virtual events will be
bound to the corresponding methods, and Window Manager events can then be bound
to the virtual events. (This indirection is done so that the key bindings can
easily be changed, and so that other sources of virtual events can exist, such
as menu entries.)
An extension can define menu entries. This is done with a class or instance
variable named menudefs; it should be a list of pairs, where each pair is a
menu name (lowercase) and a list of menu entries. Each menu entry is either
None (to insert a separator entry) or a pair of strings (menu_label,
virtual_event). Here, menu_label is the label of the menu entry, and
virtual_event is the virtual event to be generated when the entry is selected.
An underscore in the menu label is removed; the character following the
underscore is displayed underlined, to indicate the shortcut character (for
Windows).
At the moment, extensions cannot define whole new menus; they must define
entries in existing menus. Some menus are not present on some windows; such
entry definitions are then ignored, but key bindings are still applied. (This
should probably be refined in the future.)
Extensions are not required to define menu entries for all the events they
implement. (They are also not required to create keybindings, but in that
case there must be empty bindings in config-extensions.def)
Here is a partial example from zzdummy.py:
class ZzDummy:
menudefs = [
('format', [
('Z in', '<<z-in>>'),
('Z out', '<<z-out>>'),
] )
]
def __init__(self, editwin):
self.editwin = editwin
def z_in_event(self, event=None):
"...Do what you want here..."
The final piece of the puzzle is the file "config-extensions.def", which is
used to configure the loading of extensions and to establish key (or, more
generally, event) bindings to the virtual events defined in the extensions.
See the comments at the top of config-extensions.def for information. It's
currently necessary to manually modify that file to change IDLE's extension
loading or extension key bindings.
For further information on binding refer to the Tkinter Resources web page at
python.org and to the Tk Command "bind" man page.
@@ -0,0 +1,132 @@
"idlelib.filelist"
import os
from tkinter import messagebox
class FileList:
# N.B. this import overridden in PyShellFileList.
from idlelib.editor import EditorWindow
def __init__(self, root):
self.root = root
self.dict = {}
self.inversedict = {}
self.vars = {} # For EditorWindow.getrawvar (shared Tcl variables)
def open(self, filename, action=None):
assert filename
filename = self.canonize(filename)
if os.path.isdir(filename):
# This can happen when bad filename is passed on command line:
messagebox.showerror(
"File Error",
f"{filename!r} is a directory.",
master=self.root)
return None
key = os.path.normcase(filename)
if key in self.dict:
edit = self.dict[key]
edit.top.wakeup()
return edit
if action:
# Don't create window, perform 'action', e.g. open in same window
return action(filename)
else:
edit = self.EditorWindow(self, filename, key)
if edit.good_load:
return edit
else:
edit._close()
return None
def gotofileline(self, filename, lineno=None):
edit = self.open(filename)
if edit is not None and lineno is not None:
edit.gotoline(lineno)
def new(self, filename=None):
return self.EditorWindow(self, filename)
def close_all_callback(self, *args, **kwds):
for edit in list(self.inversedict):
reply = edit.close()
if reply == "cancel":
break
return "break"
def unregister_maybe_terminate(self, edit):
try:
key = self.inversedict[edit]
except KeyError:
print("Don't know this EditorWindow object. (close)")
return
if key:
del self.dict[key]
del self.inversedict[edit]
if not self.inversedict:
self.root.quit()
def filename_changed_edit(self, edit):
edit.saved_change_hook()
try:
key = self.inversedict[edit]
except KeyError:
print("Don't know this EditorWindow object. (rename)")
return
filename = edit.io.filename
if not filename:
if key:
del self.dict[key]
self.inversedict[edit] = None
return
filename = self.canonize(filename)
newkey = os.path.normcase(filename)
if newkey == key:
return
if newkey in self.dict:
conflict = self.dict[newkey]
self.inversedict[conflict] = None
messagebox.showerror(
"Name Conflict",
f"You now have multiple edit windows open for {filename!r}",
master=self.root)
self.dict[newkey] = edit
self.inversedict[edit] = newkey
if key:
try:
del self.dict[key]
except KeyError:
pass
def canonize(self, filename):
if not os.path.isabs(filename):
try:
pwd = os.getcwd()
except OSError:
pass
else:
filename = os.path.join(pwd, filename)
return os.path.normpath(filename)
def _test(): # TODO check and convert to htest
from tkinter import Tk
from idlelib.editor import fixwordbreaks
from idlelib.run import fix_scaling
root = Tk()
fix_scaling(root)
fixwordbreaks(root)
root.withdraw()
flist = FileList(root)
flist.new()
if flist.inversedict:
root.mainloop()
if __name__ == '__main__':
from unittest import main
main('idlelib.idle_test.test_filelist', verbosity=2)
# _test()
@@ -0,0 +1,426 @@
"""Format all or a selected region (line slice) of text.
Region formatting options: paragraph, comment block, indent, deindent,
comment, uncomment, tabify, and untabify.
File renamed from paragraph.py with functions added from editor.py.
"""
import re
from tkinter.messagebox import askyesno
from tkinter.simpledialog import askinteger
from idlelib.config import idleConf
class FormatParagraph:
"""Format a paragraph, comment block, or selection to a max width.
Does basic, standard text formatting, and also understands Python
comment blocks. Thus, for editing Python source code, this
extension is really only suitable for reformatting these comment
blocks or triple-quoted strings.
Known problems with comment reformatting:
* If there is a selection marked, and the first line of the
selection is not complete, the block will probably not be detected
as comments, and will have the normal "text formatting" rules
applied.
* If a comment block has leading whitespace that mixes tabs and
spaces, they will not be considered part of the same block.
* Fancy comments, like this bulleted list, aren't handled :-)
"""
def __init__(self, editwin):
self.editwin = editwin
@classmethod
def reload(cls):
cls.max_width = idleConf.GetOption('extensions', 'FormatParagraph',
'max-width', type='int', default=72)
def close(self):
self.editwin = None
def format_paragraph_event(self, event, limit=None):
"""Formats paragraph to a max width specified in idleConf.
If text is selected, format_paragraph_event will start breaking lines
at the max width, starting from the beginning selection.
If no text is selected, format_paragraph_event uses the current
cursor location to determine the paragraph (lines of text surrounded
by blank lines) and formats it.
The length limit parameter is for testing with a known value.
"""
limit = self.max_width if limit is None else limit
text = self.editwin.text
first, last = self.editwin.get_selection_indices()
if first and last:
data = text.get(first, last)
comment_header = get_comment_header(data)
else:
first, last, comment_header, data = \
find_paragraph(text, text.index("insert"))
if comment_header:
newdata = reformat_comment(data, limit, comment_header)
else:
newdata = reformat_paragraph(data, limit)
text.tag_remove("sel", "1.0", "end")
if newdata != data:
text.mark_set("insert", first)
text.undo_block_start()
text.delete(first, last)
text.insert(first, newdata)
text.undo_block_stop()
else:
text.mark_set("insert", last)
text.see("insert")
return "break"
FormatParagraph.reload()
def find_paragraph(text, mark):
"""Returns the start/stop indices enclosing the paragraph that mark is in.
Also returns the comment format string, if any, and paragraph of text
between the start/stop indices.
"""
lineno, col = map(int, mark.split("."))
line = text.get("%d.0" % lineno, "%d.end" % lineno)
# Look for start of next paragraph if the index passed in is a blank line
while text.compare("%d.0" % lineno, "<", "end") and is_all_white(line):
lineno = lineno + 1
line = text.get("%d.0" % lineno, "%d.end" % lineno)
first_lineno = lineno
comment_header = get_comment_header(line)
comment_header_len = len(comment_header)
# Once start line found, search for end of paragraph (a blank line)
while get_comment_header(line)==comment_header and \
not is_all_white(line[comment_header_len:]):
lineno = lineno + 1
line = text.get("%d.0" % lineno, "%d.end" % lineno)
last = "%d.0" % lineno
# Search back to beginning of paragraph (first blank line before)
lineno = first_lineno - 1
line = text.get("%d.0" % lineno, "%d.end" % lineno)
while lineno > 0 and \
get_comment_header(line)==comment_header and \
not is_all_white(line[comment_header_len:]):
lineno = lineno - 1
line = text.get("%d.0" % lineno, "%d.end" % lineno)
first = "%d.0" % (lineno+1)
return first, last, comment_header, text.get(first, last)
# This should perhaps be replaced with textwrap.wrap
def reformat_paragraph(data, limit):
"""Return data reformatted to specified width (limit)."""
lines = data.split("\n")
i = 0
n = len(lines)
while i < n and is_all_white(lines[i]):
i = i+1
if i >= n:
return data
indent1 = get_indent(lines[i])
if i+1 < n and not is_all_white(lines[i+1]):
indent2 = get_indent(lines[i+1])
else:
indent2 = indent1
new = lines[:i]
partial = indent1
while i < n and not is_all_white(lines[i]):
# XXX Should take double space after period (etc.) into account
words = re.split(r"(\s+)", lines[i])
for j in range(0, len(words), 2):
word = words[j]
if not word:
continue # Can happen when line ends in whitespace
if len((partial + word).expandtabs()) > limit and \
partial != indent1:
new.append(partial.rstrip())
partial = indent2
partial = partial + word + " "
if j+1 < len(words) and words[j+1] != " ":
partial = partial + " "
i = i+1
new.append(partial.rstrip())
# XXX Should reformat remaining paragraphs as well
new.extend(lines[i:])
return "\n".join(new)
def reformat_comment(data, limit, comment_header):
"""Return data reformatted to specified width with comment header."""
# Remove header from the comment lines
lc = len(comment_header)
data = "\n".join(line[lc:] for line in data.split("\n"))
# Reformat to maxformatwidth chars or a 20 char width,
# whichever is greater.
format_width = max(limit - len(comment_header), 20)
newdata = reformat_paragraph(data, format_width)
# re-split and re-insert the comment header.
newdata = newdata.split("\n")
# If the block ends in a \n, we don't want the comment prefix
# inserted after it. (Im not sure it makes sense to reformat a
# comment block that is not made of complete lines, but whatever!)
# Can't think of a clean solution, so we hack away
block_suffix = ""
if not newdata[-1]:
block_suffix = "\n"
newdata = newdata[:-1]
return '\n'.join(comment_header+line for line in newdata) + block_suffix
def is_all_white(line):
"""Return True if line is empty or all whitespace."""
return re.match(r"^\s*$", line) is not None
def get_indent(line):
"""Return the initial space or tab indent of line."""
return re.match(r"^([ \t]*)", line).group()
def get_comment_header(line):
"""Return string with leading whitespace and '#' from line or ''.
A null return indicates that the line is not a comment line. A non-
null return, such as ' #', will be used to find the other lines of
a comment block with the same indent.
"""
m = re.match(r"^([ \t]*#*)", line)
if m is None: return ""
return m.group(1)
# Copied from editor.py; importing it would cause an import cycle.
_line_indent_re = re.compile(r'[ \t]*')
def get_line_indent(line, tabwidth):
"""Return a line's indentation as (# chars, effective # of spaces).
The effective # of spaces is the length after properly "expanding"
the tabs into spaces, as done by str.expandtabs(tabwidth).
"""
m = _line_indent_re.match(line)
return m.end(), len(m.group().expandtabs(tabwidth))
class FormatRegion:
"Format selected text (region)."
def __init__(self, editwin):
self.editwin = editwin
def get_region(self):
"""Return line information about the selected text region.
If text is selected, the first and last indices will be
for the selection. If there is no text selected, the
indices will be the current cursor location.
Return a tuple containing (first index, last index,
string representation of text, list of text lines).
"""
text = self.editwin.text
first, last = self.editwin.get_selection_indices()
if first and last:
head = text.index(first + " linestart")
tail = text.index(last + "-1c lineend +1c")
else:
head = text.index("insert linestart")
tail = text.index("insert lineend +1c")
chars = text.get(head, tail)
lines = chars.split("\n")
return head, tail, chars, lines
def set_region(self, head, tail, chars, lines):
"""Replace the text between the given indices.
Args:
head: Starting index of text to replace.
tail: Ending index of text to replace.
chars: Expected to be string of current text
between head and tail.
lines: List of new lines to insert between head
and tail.
"""
text = self.editwin.text
newchars = "\n".join(lines)
if newchars == chars:
text.bell()
return
text.tag_remove("sel", "1.0", "end")
text.mark_set("insert", head)
text.undo_block_start()
text.delete(head, tail)
text.insert(head, newchars)
text.undo_block_stop()
text.tag_add("sel", head, "insert")
def indent_region_event(self, event=None):
"Indent region by indentwidth spaces."
head, tail, chars, lines = self.get_region()
for pos in range(len(lines)):
line = lines[pos]
if line:
raw, effective = get_line_indent(line, self.editwin.tabwidth)
effective = effective + self.editwin.indentwidth
lines[pos] = self.editwin._make_blanks(effective) + line[raw:]
self.set_region(head, tail, chars, lines)
return "break"
def dedent_region_event(self, event=None):
"Dedent region by indentwidth spaces."
head, tail, chars, lines = self.get_region()
for pos in range(len(lines)):
line = lines[pos]
if line:
raw, effective = get_line_indent(line, self.editwin.tabwidth)
effective = max(effective - self.editwin.indentwidth, 0)
lines[pos] = self.editwin._make_blanks(effective) + line[raw:]
self.set_region(head, tail, chars, lines)
return "break"
def comment_region_event(self, event=None):
"""Comment out each line in region.
## is appended to the beginning of each line to comment it out.
"""
head, tail, chars, lines = self.get_region()
for pos in range(len(lines) - 1):
line = lines[pos]
lines[pos] = '##' + line
self.set_region(head, tail, chars, lines)
return "break"
def uncomment_region_event(self, event=None):
"""Uncomment each line in region.
Remove ## or # in the first positions of a line. If the comment
is not in the beginning position, this command will have no effect.
"""
head, tail, chars, lines = self.get_region()
for pos in range(len(lines)):
line = lines[pos]
if not line:
continue
if line[:2] == '##':
line = line[2:]
elif line[:1] == '#':
line = line[1:]
lines[pos] = line
self.set_region(head, tail, chars, lines)
return "break"
def tabify_region_event(self, event=None):
"Convert leading spaces to tabs for each line in selected region."
head, tail, chars, lines = self.get_region()
tabwidth = self._asktabwidth()
if tabwidth is None:
return
for pos in range(len(lines)):
line = lines[pos]
if line:
raw, effective = get_line_indent(line, tabwidth)
ntabs, nspaces = divmod(effective, tabwidth)
lines[pos] = '\t' * ntabs + ' ' * nspaces + line[raw:]
self.set_region(head, tail, chars, lines)
return "break"
def untabify_region_event(self, event=None):
"Expand tabs to spaces for each line in region."
head, tail, chars, lines = self.get_region()
tabwidth = self._asktabwidth()
if tabwidth is None:
return
for pos in range(len(lines)):
lines[pos] = lines[pos].expandtabs(tabwidth)
self.set_region(head, tail, chars, lines)
return "break"
def _asktabwidth(self):
"Return value for tab width."
return askinteger(
"Tab width",
"Columns per tab? (2-16)",
parent=self.editwin.text,
initialvalue=self.editwin.indentwidth,
minvalue=2,
maxvalue=16)
class Indents:
"Change future indents."
def __init__(self, editwin):
self.editwin = editwin
def toggle_tabs_event(self, event):
editwin = self.editwin
usetabs = editwin.usetabs
if askyesno(
"Toggle tabs",
"Turn tabs " + ("on", "off")[usetabs] +
"?\nIndent width " +
("will be", "remains at")[usetabs] + " 8." +
"\n Note: a tab is always 8 columns",
parent=editwin.text):
editwin.usetabs = not usetabs
# Try to prevent inconsistent indentation.
# User must change indent width manually after using tabs.
editwin.indentwidth = 8
return "break"
def change_indentwidth_event(self, event):
editwin = self.editwin
new = askinteger(
"Indent width",
"New indent width (2-16)\n(Always use 8 when using tabs)",
parent=editwin.text,
initialvalue=editwin.indentwidth,
minvalue=2,
maxvalue=16)
if new and new != editwin.indentwidth and not editwin.usetabs:
editwin.indentwidth = new
return "break"
class Rstrip: # 'Strip Trailing Whitespace" on "Format" menu.
def __init__(self, editwin):
self.editwin = editwin
def do_rstrip(self, event=None):
text = self.editwin.text
undo = self.editwin.undo
undo.undo_block_start()
end_line = int(float(text.index('end')))
for cur in range(1, end_line):
txt = text.get('%i.0' % cur, '%i.end' % cur)
raw = len(txt)
cut = len(txt.rstrip())
# Since text.delete() marks file as changed, even if not,
# only call it when needed to actually delete something.
if cut < raw:
text.delete('%i.%i' % (cur, cut), '%i.end' % cur)
if (text.get('end-2c') == '\n' # File ends with at least 1 newline;
and not hasattr(self.editwin, 'interp')): # & is not Shell.
# Delete extra user endlines.
while (text.index('end-1c') > '1.0' # Stop if file empty.
and text.get('end-3c') == '\n'):
text.delete('end-3c')
# Because tk indexes are slice indexes and never raise,
# a file with only newlines will be emptied.
# patchcheck.py does the same.
undo.undo_block_stop()
if __name__ == "__main__":
from unittest import main
main('idlelib.idle_test.test_format', verbosity=2, exit=False)
@@ -0,0 +1,223 @@
"""Grep dialog for Find in Files functionality.
Inherits from SearchDialogBase for GUI and uses searchengine
to prepare search pattern.
"""
import fnmatch
import os
import sys
from tkinter import StringVar, BooleanVar
from tkinter.ttk import Checkbutton # Frame imported in ...Base
from idlelib.searchbase import SearchDialogBase
from idlelib import searchengine
# Importing OutputWindow here fails due to import loop
# EditorWindow -> GrepDialog -> OutputWindow -> EditorWindow
def grep(text, io=None, flist=None):
"""Open the Find in Files dialog.
Module-level function to access the singleton GrepDialog
instance and open the dialog. If text is selected, it is
used as the search phrase; otherwise, the previous entry
is used.
Args:
text: Text widget that contains the selected text for
default search phrase.
io: iomenu.IOBinding instance with default path to search.
flist: filelist.FileList instance for OutputWindow parent.
"""
root = text._root()
engine = searchengine.get(root)
if not hasattr(engine, "_grepdialog"):
engine._grepdialog = GrepDialog(root, engine, flist)
dialog = engine._grepdialog
searchphrase = text.get("sel.first", "sel.last")
dialog.open(text, searchphrase, io)
def walk_error(msg):
"Handle os.walk error."
print(msg)
def findfiles(folder, pattern, recursive):
"""Generate file names in dir that match pattern.
Args:
folder: Root directory to search.
pattern: File pattern to match.
recursive: True to include subdirectories.
"""
for dirpath, _, filenames in os.walk(folder, onerror=walk_error):
yield from (os.path.join(dirpath, name)
for name in filenames
if fnmatch.fnmatch(name, pattern))
if not recursive:
break
class GrepDialog(SearchDialogBase):
"Dialog for searching multiple files."
title = "Find in Files Dialog"
icon = "Grep"
needwrapbutton = 0
def __init__(self, root, engine, flist):
"""Create search dialog for searching for a phrase in the file system.
Uses SearchDialogBase as the basis for the GUI and a
searchengine instance to prepare the search.
Attributes:
flist: filelist.Filelist instance for OutputWindow parent.
globvar: String value of Entry widget for path to search.
globent: Entry widget for globvar. Created in
create_entries().
recvar: Boolean value of Checkbutton widget for
traversing through subdirectories.
"""
super().__init__(root, engine)
self.flist = flist
self.globvar = StringVar(root)
self.recvar = BooleanVar(root)
def open(self, text, searchphrase, io=None):
"""Make dialog visible on top of others and ready to use.
Extend the SearchDialogBase open() to set the initial value
for globvar.
Args:
text: Multicall object containing the text information.
searchphrase: String phrase to search.
io: iomenu.IOBinding instance containing file path.
"""
SearchDialogBase.open(self, text, searchphrase)
if io:
path = io.filename or ""
else:
path = ""
dir, base = os.path.split(path)
head, tail = os.path.splitext(base)
if not tail:
tail = ".py"
self.globvar.set(os.path.join(dir, "*" + tail))
def create_entries(self):
"Create base entry widgets and add widget for search path."
SearchDialogBase.create_entries(self)
self.globent = self.make_entry("In files:", self.globvar)[0]
def create_other_buttons(self):
"Add check button to recurse down subdirectories."
btn = Checkbutton(
self.make_frame()[0], variable=self.recvar,
text="Recurse down subdirectories")
btn.pack(side="top", fill="both")
def create_command_buttons(self):
"Create base command buttons and add button for Search Files."
SearchDialogBase.create_command_buttons(self)
self.make_button("Search Files", self.default_command, isdef=True)
def default_command(self, event=None):
"""Grep for search pattern in file path. The default command is bound
to <Return>.
If entry values are populated, set OutputWindow as stdout
and perform search. The search dialog is closed automatically
when the search begins.
"""
prog = self.engine.getprog()
if not prog:
return
path = self.globvar.get()
if not path:
self.top.bell()
return
from idlelib.outwin import OutputWindow # leave here!
save = sys.stdout
try:
sys.stdout = OutputWindow(self.flist)
self.grep_it(prog, path)
finally:
sys.stdout = save
def grep_it(self, prog, path):
"""Search for prog within the lines of the files in path.
For the each file in the path directory, open the file and
search each line for the matching pattern. If the pattern is
found, write the file and line information to stdout (which
is an OutputWindow).
Args:
prog: The compiled, cooked search pattern.
path: String containing the search path.
"""
folder, filepat = os.path.split(path)
if not folder:
folder = os.curdir
filelist = sorted(findfiles(folder, filepat, self.recvar.get()))
self.close()
pat = self.engine.getpat()
print(f"Searching {pat!r} in {path} ...")
hits = 0
try:
for fn in filelist:
try:
with open(fn, errors='replace') as f:
for lineno, line in enumerate(f, 1):
if line[-1:] == '\n':
line = line[:-1]
if prog.search(line):
sys.stdout.write(f"{fn}: {lineno}: {line}\n")
hits += 1
except OSError as msg:
print(msg)
print(f"Hits found: {hits}\n(Hint: right-click to open locations.)"
if hits else "No hits.")
except AttributeError:
# Tk window has been closed, OutputWindow.text = None,
# so in OW.write, OW.text.insert fails.
pass
def _grep_dialog(parent): # htest #
from tkinter import Toplevel, Text, SEL
from tkinter.ttk import Frame, Button
from idlelib.pyshell import PyShellFileList
top = Toplevel(parent)
top.title("Test GrepDialog")
x, y = map(int, parent.geometry().split('+')[1:])
top.geometry(f"+{x}+{y + 175}")
flist = PyShellFileList(top)
frame = Frame(top)
frame.pack()
text = Text(frame, height=5)
text.pack()
text.insert('1.0', 'import grep')
def show_grep_dialog():
text.tag_add(SEL, "1.0", '1.end')
grep(text, flist=flist)
text.tag_remove(SEL, "1.0", '1.end')
button = Button(frame, text="Show GrepDialog", command=show_grep_dialog)
button.pack()
if __name__ == "__main__":
from unittest import main
main('idlelib.idle_test.test_grep', verbosity=2, exit=False)
from idlelib.idle_test.htest import run
run(_grep_dialog)
@@ -0,0 +1,805 @@
<section id="idle">
<span id="idle-python-editor-and-shell"></span><h1>IDLE — Python editor and shell<a class="headerlink" href="#idle" title="Link to this heading"></a></h1>
<p><strong>Source code:</strong> <a class="extlink-source reference external" href="https://github.com/python/cpython/tree/main/Lib/idlelib/">Lib/idlelib/</a></p>
<span class="target" id="index-0"></span><hr class="docutils" />
<p>IDLE is Python’s Integrated Development and Learning Environment.</p>
<p>IDLE has the following features:</p>
<ul class="simple">
<li><p>cross-platform: works mostly the same on Windows, Unix, and macOS</p></li>
<li><p>Python shell window (interactive interpreter) with colorizing
of code input, output, and error messages</p></li>
<li><p>multi-window text editor with multiple undo, Python colorizing,
smart indent, call tips, auto completion, and other features</p></li>
<li><p>search within any window, replace within editor windows, and search
through multiple files (grep)</p></li>
<li><p>debugger with persistent breakpoints, stepping, and viewing
of global and local namespaces</p></li>
<li><p>configuration, browsers, and other dialogs</p></li>
</ul>
<section id="menus">
<h2>Menus<a class="headerlink" href="#menus" title="Link to this heading"></a></h2>
<p>IDLE has two main window types, the Shell window and the Editor window. It is
possible to have multiple editor windows simultaneously. On Windows and
Linux, each has its own top menu. Each menu documented below indicates
which window type it is associated with.</p>
<p>Output windows, such as used for Edit =&gt; Find in Files, are a subtype of editor
window. They currently have the same top menu but a different
default title and context menu.</p>
<p>On macOS, there is one application menu. It dynamically changes according
to the window currently selected. It has an IDLE menu, and some entries
described below are moved around to conform to Apple guidelines.</p>
<section id="file-menu-shell-and-editor">
<h3>File menu (Shell and Editor)<a class="headerlink" href="#file-menu-shell-and-editor" title="Link to this heading"></a></h3>
<dl class="simple">
<dt>New File</dt><dd><p>Create a new file editing window.</p>
</dd>
<dt>Open…</dt><dd><p>Open an existing file with an Open dialog.</p>
</dd>
<dt>Open Module…</dt><dd><p>Open an existing module (searches sys.path).</p>
</dd>
<dt>Recent Files</dt><dd><p>Open a list of recent files. Click one to open it.</p>
</dd>
</dl>
<dl class="simple" id="index-1">
<dt>Module Browser</dt><dd><p>Show functions, classes, and methods in the current Editor file in a
tree structure. In the shell, open a module first.</p>
</dd>
<dt>Path Browser</dt><dd><p>Show sys.path directories, modules, functions, classes and methods in a
tree structure.</p>
</dd>
<dt>Save</dt><dd><p>Save the current window to the associated file, if there is one. Windows
that have been changed since being opened or last saved have a * before
and after the window title. If there is no associated file,
do Save As instead.</p>
</dd>
<dt>Save As…</dt><dd><p>Save the current window with a Save As dialog. The file saved becomes the
new associated file for the window. (If your file namager is set to hide
extensions, the current extension will be omitted in the file name box.
If the new filename has no ‘.’, ‘.py’ and ‘.txt’ will be added for Python
and text files, except that on macOS Aqua,’.py’ is added for all files.)</p>
</dd>
<dt>Save Copy As…</dt><dd><p>Save the current window to different file without changing the associated
file. (See Save As note above about filename extensions.)</p>
</dd>
<dt>Print Window</dt><dd><p>Print the current window to the default printer.</p>
</dd>
<dt>Close Window</dt><dd><p>Close the current window (if an unsaved editor, ask to save; if an unsaved
Shell, ask to quit execution). Calling <code class="docutils literal notranslate"><span class="pre">exit()</span></code> or <code class="docutils literal notranslate"><span class="pre">close()</span></code> in the Shell
window also closes Shell. If this is the only window, also exit IDLE.</p>
</dd>
<dt>Exit IDLE</dt><dd><p>Close all windows and quit IDLE (ask to save unsaved edit windows).</p>
</dd>
</dl>
</section>
<section id="edit-menu-shell-and-editor">
<h3>Edit menu (Shell and Editor)<a class="headerlink" href="#edit-menu-shell-and-editor" title="Link to this heading"></a></h3>
<dl class="simple">
<dt>Undo</dt><dd><p>Undo the last change to the current window. A maximum of 1000 changes may
be undone.</p>
</dd>
<dt>Redo</dt><dd><p>Redo the last undone change to the current window.</p>
</dd>
<dt>Select All</dt><dd><p>Select the entire contents of the current window.</p>
</dd>
<dt>Cut</dt><dd><p>Copy selection into the system-wide clipboard; then delete the selection.</p>
</dd>
<dt>Copy</dt><dd><p>Copy selection into the system-wide clipboard.</p>
</dd>
<dt>Paste</dt><dd><p>Insert contents of the system-wide clipboard into the current window.</p>
</dd>
</dl>
<p>The clipboard functions are also available in context menus.</p>
<dl class="simple">
<dt>Find…</dt><dd><p>Open a search dialog with many options</p>
</dd>
<dt>Find Again</dt><dd><p>Repeat the last search, if there is one.</p>
</dd>
<dt>Find Selection</dt><dd><p>Search for the currently selected string, if there is one.</p>
</dd>
<dt>Find in Files…</dt><dd><p>Open a file search dialog. Put results in a new output window.</p>
</dd>
<dt>Replace…</dt><dd><p>Open a search-and-replace dialog.</p>
</dd>
<dt>Go to Line</dt><dd><p>Move the cursor to the beginning of the line requested and make that
line visible. A request past the end of the file goes to the end.
Clear any selection and update the line and column status.</p>
</dd>
<dt>Show Completions</dt><dd><p>Open a scrollable list allowing selection of existing names. See
<a class="reference internal" href="#completions"><span class="std std-ref">Completions</span></a> in the Editing and navigation section below.</p>
</dd>
<dt>Expand Word</dt><dd><p>Expand a prefix you have typed to match a full word in the same window;
repeat to get a different expansion.</p>
</dd>
<dt>Show Call Tip</dt><dd><p>After an unclosed parenthesis for a function, open a small window with
function parameter hints. See <a class="reference internal" href="#calltips"><span class="std std-ref">Calltips</span></a> in the
Editing and navigation section below.</p>
</dd>
<dt>Show Surrounding Parens</dt><dd><p>Highlight the surrounding parenthesis.</p>
</dd>
</dl>
</section>
<section id="format-menu-editor-window-only">
<span id="format-menu"></span><h3>Format menu (Editor window only)<a class="headerlink" href="#format-menu-editor-window-only" title="Link to this heading"></a></h3>
<dl class="simple">
<dt>Format Paragraph</dt><dd><p>Reformat the current blank-line-delimited paragraph in comment block or
multiline string or selected line in a string. All lines in the
paragraph will be formatted to less than N columns, where N defaults to 72.</p>
</dd>
<dt>Indent Region</dt><dd><p>Shift selected lines right by the indent width (default 4 spaces).</p>
</dd>
<dt>Dedent Region</dt><dd><p>Shift selected lines left by the indent width (default 4 spaces).</p>
</dd>
<dt>Comment Out Region</dt><dd><p>Insert ## in front of selected lines.</p>
</dd>
<dt>Uncomment Region</dt><dd><p>Remove leading # or ## from selected lines.</p>
</dd>
<dt>Tabify Region</dt><dd><p>Turn <em>leading</em> stretches of spaces into tabs. (Note: We recommend using
4 space blocks to indent Python code.)</p>
</dd>
<dt>Untabify Region</dt><dd><p>Turn <em>all</em> tabs into the correct number of spaces.</p>
</dd>
<dt>Toggle Tabs</dt><dd><p>Open a dialog to switch between indenting with spaces and tabs.</p>
</dd>
<dt>New Indent Width</dt><dd><p>Open a dialog to change indent width. The accepted default by the Python
community is 4 spaces.</p>
</dd>
<dt>Strip Trailing Chitespace</dt><dd><p>Remove trailing space and other whitespace characters after the last
non-whitespace character of a line by applying str.rstrip to each line,
including lines within multiline strings. Except for Shell windows,
remove extra newlines at the end of the file.</p>
</dd>
</dl>
</section>
<section id="run-menu-editor-window-only">
<span id="index-2"></span><h3>Run menu (Editor window only)<a class="headerlink" href="#run-menu-editor-window-only" title="Link to this heading"></a></h3>
<dl class="simple" id="run-module">
<dt>Run Module</dt><dd><p>Do <a class="reference internal" href="#check-module"><span class="std std-ref">Check Module</span></a>. If no error, restart the shell to clean the
environment, then execute the module. Output is displayed in the Shell
window. Note that output requires use of <code class="docutils literal notranslate"><span class="pre">print</span></code> or <code class="docutils literal notranslate"><span class="pre">write</span></code>.
When execution is complete, the Shell retains focus and displays a prompt.
At this point, one may interactively explore the result of execution.
This is similar to executing a file with <code class="docutils literal notranslate"><span class="pre">python</span> <span class="pre">-i</span> <span class="pre">file</span></code> at a command
line.</p>
</dd>
</dl>
<dl class="simple" id="run-custom">
<dt>Run… Customized</dt><dd><p>Same as <a class="reference internal" href="#run-module"><span class="std std-ref">Run Module</span></a>, but run the module with customized
settings. <em>Command Line Arguments</em> extend <a class="reference internal" href="sys.html#sys.argv" title="sys.argv"><code class="xref py py-data docutils literal notranslate"><span class="pre">sys.argv</span></code></a> as if passed
on a command line. The module can be run in the Shell without restarting.</p>
</dd>
</dl>
<dl class="simple" id="check-module">
<dt>Check Module</dt><dd><p>Check the syntax of the module currently open in the Editor window. If the
module has not been saved IDLE will either prompt the user to save or
autosave, as selected in the General tab of the Idle Settings dialog. If
there is a syntax error, the approximate location is indicated in the
Editor window.</p>
</dd>
</dl>
<dl class="simple" id="python-shell">
<dt>Python Shell</dt><dd><p>Open or wake up the Python Shell window.</p>
</dd>
</dl>
</section>
<section id="shell-menu-shell-window-only">
<h3>Shell menu (Shell window only)<a class="headerlink" href="#shell-menu-shell-window-only" title="Link to this heading"></a></h3>
<dl class="simple">
<dt>View Last Restart</dt><dd><p>Scroll the shell window to the last Shell restart.</p>
</dd>
<dt>Restart Shell</dt><dd><p>Restart the shell to clean the environment and reset display and exception handling.</p>
</dd>
<dt>Previous History</dt><dd><p>Cycle through earlier commands in history which match the current entry.</p>
</dd>
<dt>Next History</dt><dd><p>Cycle through later commands in history which match the current entry.</p>
</dd>
<dt>Interrupt Execution</dt><dd><p>Stop a running program.</p>
</dd>
</dl>
</section>
<section id="debug-menu-shell-window-only">
<h3>Debug menu (Shell window only)<a class="headerlink" href="#debug-menu-shell-window-only" title="Link to this heading"></a></h3>
<dl class="simple">
<dt>Go to File/Line</dt><dd><p>Look on the current line. with the cursor, and the line above for a filename
and line number. If found, open the file if not already open, and show the
line. Use this to view source lines referenced in an exception traceback
and lines found by Find in Files. Also available in the context menu of
the Shell window and Output windows.</p>
</dd>
</dl>
<dl class="simple" id="index-3">
<dt>Debugger (toggle)</dt><dd><p>When activated, code entered in the Shell or run from an Editor will run
under the debugger. In the Editor, breakpoints can be set with the context
menu. This feature is still incomplete and somewhat experimental.</p>
</dd>
<dt>Stack Viewer</dt><dd><p>Show the stack traceback of the last exception in a tree widget, with
access to locals and globals.</p>
</dd>
<dt>Auto-open Stack Viewer</dt><dd><p>Toggle automatically opening the stack viewer on an unhandled exception.</p>
</dd>
</dl>
</section>
<section id="options-menu-shell-and-editor">
<h3>Options menu (Shell and Editor)<a class="headerlink" href="#options-menu-shell-and-editor" title="Link to this heading"></a></h3>
<dl class="simple">
<dt>Configure IDLE</dt><dd><p>Open a configuration dialog and change preferences for the following:
fonts, indentation, keybindings, text color themes, startup windows and
size, additional help sources, and extensions. On macOS, open the
configuration dialog by selecting Preferences in the application
menu. For more details, see
<a class="reference internal" href="#preferences"><span class="std std-ref">Setting preferences</span></a> under Help and preferences.</p>
</dd>
</dl>
<p>Most configuration options apply to all windows or all future windows.
The option items below only apply to the active window.</p>
<dl class="simple">
<dt>Show/Hide Code Context (Editor Window only)</dt><dd><p>Open a pane at the top of the edit window which shows the block context
of the code which has scrolled above the top of the window. See
<a class="reference internal" href="#code-context"><span class="std std-ref">Code Context</span></a> in the Editing and Navigation section
below.</p>
</dd>
<dt>Show/Hide Line Numbers (Editor Window only)</dt><dd><p>Open a column to the left of the edit window which shows the number
of each line of text. The default is off, which may be changed in the
preferences (see <a class="reference internal" href="#preferences"><span class="std std-ref">Setting preferences</span></a>).</p>
</dd>
<dt>Zoom/Restore Height</dt><dd><p>Toggles the window between normal size and maximum height. The initial size
defaults to 40 lines by 80 chars unless changed on the General tab of the
Configure IDLE dialog. The maximum height for a screen is determined by
momentarily maximizing a window the first time one is zoomed on the screen.
Changing screen settings may invalidate the saved height. This toggle has
no effect when a window is maximized.</p>
</dd>
</dl>
</section>
<section id="window-menu-shell-and-editor">
<h3>Window menu (Shell and Editor)<a class="headerlink" href="#window-menu-shell-and-editor" title="Link to this heading"></a></h3>
<p>Lists the names of all open windows; select one to bring it to the foreground
(deiconifying it if necessary).</p>
</section>
<section id="help-menu-shell-and-editor">
<h3>Help menu (Shell and Editor)<a class="headerlink" href="#help-menu-shell-and-editor" title="Link to this heading"></a></h3>
<dl class="simple">
<dt>About IDLE</dt><dd><p>Display version, copyright, license, credits, and more.</p>
</dd>
<dt>IDLE Help</dt><dd><p>Display this IDLE document, detailing the menu options, basic editing and
navigation, and other tips.</p>
</dd>
<dt>Python Docs</dt><dd><p>Access local Python documentation, if installed, or start a web browser
and open docs.python.org showing the latest Python documentation.</p>
</dd>
<dt>Turtle Demo</dt><dd><p>Run the turtledemo module with example Python code and turtle drawings.</p>
</dd>
</dl>
<p>Additional help sources may be added here with the Configure IDLE dialog under
the General tab. See the <a class="reference internal" href="#help-sources"><span class="std std-ref">Help sources</span></a> subsection below
for more on Help menu choices.</p>
</section>
<section id="context-menus">
<span id="index-4"></span><h3>Context menus<a class="headerlink" href="#context-menus" title="Link to this heading"></a></h3>
<p>Open a context menu by right-clicking in a window (Control-click on macOS).
Context menus have the standard clipboard functions also on the Edit menu.</p>
<dl class="simple">
<dt>Cut</dt><dd><p>Copy selection into the system-wide clipboard; then delete the selection.</p>
</dd>
<dt>Copy</dt><dd><p>Copy selection into the system-wide clipboard.</p>
</dd>
<dt>Paste</dt><dd><p>Insert contents of the system-wide clipboard into the current window.</p>
</dd>
</dl>
<p>Editor windows also have breakpoint functions. Lines with a breakpoint set are
specially marked. Breakpoints only have an effect when running under the
debugger. Breakpoints for a file are saved in the user’s <code class="docutils literal notranslate"><span class="pre">.idlerc</span></code>
directory.</p>
<dl class="simple">
<dt>Set Breakpoint</dt><dd><p>Set a breakpoint on the current line.</p>
</dd>
<dt>Clear Breakpoint</dt><dd><p>Clear the breakpoint on that line.</p>
</dd>
</dl>
<p>Shell and Output windows also have the following.</p>
<dl class="simple">
<dt>Go to file/line</dt><dd><p>Same as in Debug menu.</p>
</dd>
</dl>
<p>The Shell window also has an output squeezing facility explained in the <em>Python
Shell window</em> subsection below.</p>
<dl class="simple">
<dt>Squeeze</dt><dd><p>If the cursor is over an output line, squeeze all the output between
the code above and the prompt below down to a ‘Squeezed text’ label.</p>
</dd>
</dl>
</section>
</section>
<section id="editing-and-navigation">
<span id="id1"></span><h2>Editing and Navigation<a class="headerlink" href="#editing-and-navigation" title="Link to this heading"></a></h2>
<section id="editor-windows">
<h3>Editor windows<a class="headerlink" href="#editor-windows" title="Link to this heading"></a></h3>
<p>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.</p>
<p>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.</p>
<p>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.</p>
</section>
<section id="key-bindings">
<h3>Key bindings<a class="headerlink" href="#key-bindings" title="Link to this heading"></a></h3>
<p>The IDLE insertion cursor is a thin vertical bar between character
positions. When characters are entered, the insertion cursor and
everything to its right moves right one character and
the new character is entered in the new space.</p>
<p>Several non-character keys move the cursor and possibly
delete characters. Deletion does not puts text on the clipboard,
but IDLE has an undo list. Wherever this doc discusses keys,
‘C’ refers to the <kbd class="kbd docutils literal notranslate">Control</kbd> key on Windows and
Unix and the <kbd class="kbd docutils literal notranslate">Command</kbd> key on macOS. (And all such discussions
assume that the keys have not been re-bound to something else.)</p>
<ul class="simple">
<li><p>Arrow keys move the cursor one character or line.</p></li>
<li><p><kbd class="kbd compound docutils literal notranslate"><kbd class="kbd docutils literal notranslate">C</kbd>-<kbd class="kbd docutils literal notranslate">LeftArrow</kbd></kbd> and <kbd class="kbd compound docutils literal notranslate"><kbd class="kbd docutils literal notranslate">C</kbd>-<kbd class="kbd docutils literal notranslate">RightArrow</kbd></kbd> moves left or right one word.</p></li>
<li><p><kbd class="kbd docutils literal notranslate">Home</kbd> and <kbd class="kbd docutils literal notranslate">End</kbd> go to the beginning or end of the line.</p></li>
<li><p><kbd class="kbd docutils literal notranslate">Page Up</kbd> and <kbd class="kbd docutils literal notranslate">Page Down</kbd> go up or down one screen.</p></li>
<li><p><kbd class="kbd compound docutils literal notranslate"><kbd class="kbd docutils literal notranslate">C</kbd>-<kbd class="kbd docutils literal notranslate">Home</kbd></kbd> and <kbd class="kbd compound docutils literal notranslate"><kbd class="kbd docutils literal notranslate">C</kbd>-<kbd class="kbd docutils literal notranslate">End</kbd></kbd> go to beginning or end of the file.</p></li>
<li><p><kbd class="kbd docutils literal notranslate">Backspace</kbd> and <kbd class="kbd docutils literal notranslate">Del</kbd> (or <kbd class="kbd compound docutils literal notranslate"><kbd class="kbd docutils literal notranslate">C</kbd>-<kbd class="kbd docutils literal notranslate">d</kbd></kbd>) delete the previous
or next character.</p></li>
<li><p><kbd class="kbd compound docutils literal notranslate"><kbd class="kbd docutils literal notranslate">C</kbd>-<kbd class="kbd docutils literal notranslate">Backspace</kbd></kbd> and <kbd class="kbd compound docutils literal notranslate"><kbd class="kbd docutils literal notranslate">C</kbd>-<kbd class="kbd docutils literal notranslate">Del</kbd></kbd> delete one word left or right.</p></li>
<li><p><kbd class="kbd compound docutils literal notranslate"><kbd class="kbd docutils literal notranslate">C</kbd>-<kbd class="kbd docutils literal notranslate">k</kbd></kbd> deletes (‘kills’) everything to the right.</p></li>
</ul>
<p>Standard keybindings (like <kbd class="kbd compound docutils literal notranslate"><kbd class="kbd docutils literal notranslate">C</kbd>-<kbd class="kbd docutils literal notranslate">c</kbd></kbd> to copy and <kbd class="kbd compound docutils literal notranslate"><kbd class="kbd docutils literal notranslate">C</kbd>-<kbd class="kbd docutils literal notranslate">v</kbd></kbd> to paste)
may work. Keybindings are selected in the Configure IDLE dialog.</p>
</section>
<section id="automatic-indentation">
<h3>Automatic indentation<a class="headerlink" href="#automatic-indentation" title="Link to this heading"></a></h3>
<p>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, <kbd class="kbd docutils literal notranslate">Backspace</kbd> deletes up
to 4 spaces if they are there. <kbd class="kbd docutils literal notranslate">Tab</kbd> 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.</p>
<p>See also the indent/dedent region commands on the
<a class="reference internal" href="#format-menu"><span class="std std-ref">Format menu</span></a>.</p>
</section>
<section id="search-and-replace">
<h3>Search and Replace<a class="headerlink" href="#search-and-replace" title="Link to this heading"></a></h3>
<p>Any selection becomes a search target. However, only selections within
a line work because searches are only performed within lines with the
terminal newline removed. If <code class="docutils literal notranslate"><span class="pre">[x]</span> <span class="pre">Regular</span> <span class="pre">expression</span></code> is checked, the
target is interpreted according to the Python re module.</p>
</section>
<section id="completions">
<span id="id2"></span><h3>Completions<a class="headerlink" href="#completions" title="Link to this heading"></a></h3>
<p>Completions are supplied, when requested and available, for module
names, attributes of classes or functions, or filenames. Each request
method displays a completion box with existing names. (See tab
completions below for an exception.) For any box, change the name
being completed and the item highlighted in the box by
typing and deleting characters; by hitting <kbd class="kbd docutils literal notranslate">Up</kbd>, <kbd class="kbd docutils literal notranslate">Down</kbd>,
<kbd class="kbd docutils literal notranslate">PageUp</kbd>, <kbd class="kbd docutils literal notranslate">PageDown</kbd>, <kbd class="kbd docutils literal notranslate">Home</kbd>, and <kbd class="kbd docutils literal notranslate">End</kbd> keys;
and by a single click within the box. Close the box with <kbd class="kbd docutils literal notranslate">Escape</kbd>,
<kbd class="kbd docutils literal notranslate">Enter</kbd>, and double <kbd class="kbd docutils literal notranslate">Tab</kbd> keys or clicks outside the box.
A double click within the box selects and closes.</p>
<p>One way to open a box is to type a key character and wait for a
predefined interval. This defaults to 2 seconds; customize it
in the settings dialog. (To prevent auto popups, set the delay to a
large number of milliseconds, such as 100000000.) For imported module
names or class or function attributes, type ‘.’.
For filenames in the root directory, type <a class="reference internal" href="os.html#os.sep" title="os.sep"><code class="xref py py-data docutils literal notranslate"><span class="pre">os.sep</span></code></a> or
<a class="reference internal" href="os.html#os.altsep" title="os.altsep"><code class="xref py py-data docutils literal notranslate"><span class="pre">os.altsep</span></code></a> immediately after an opening quote. (On Windows,
one can specify a drive first.) Move into subdirectories by typing a
directory name and a separator.</p>
<p>Instead of waiting, or after a box is closed, open a completion box
immediately with Show Completions on the Edit menu. The default hot
key is <kbd class="kbd compound docutils literal notranslate"><kbd class="kbd docutils literal notranslate">C</kbd>-<kbd class="kbd docutils literal notranslate">space</kbd></kbd>. If one types a prefix for the desired name
before opening the box, the first match or near miss is made visible.
The result is the same as if one enters a prefix
after the box is displayed. Show Completions after a quote completes
filenames in the current directory instead of a root directory.</p>
<p>Hitting <kbd class="kbd docutils literal notranslate">Tab</kbd> after a prefix usually has the same effect as Show
Completions. (With no prefix, it indents.) However, if there is only
one match to the prefix, that match is immediately added to the editor
text without opening a box.</p>
<p>Invoking ‘Show Completions’, or hitting <kbd class="kbd docutils literal notranslate">Tab</kbd> after a prefix,
outside of a string and without a preceding ‘.’ opens a box with
keywords, builtin names, and available module-level names.</p>
<p>When editing code in an editor (as oppose to Shell), increase the
available module-level names by running your code
and not restarting the Shell thereafter. This is especially useful
after adding imports at the top of a file. This also increases
possible attribute completions.</p>
<p>Completion boxes initially exclude names beginning with ‘_’ or, for
modules, not included in ‘__all__’. The hidden names can be accessed
by typing ‘_’ after ‘.’, either before or after the box is opened.</p>
</section>
<section id="calltips">
<span id="id3"></span><h3>Calltips<a class="headerlink" href="#calltips" title="Link to this heading"></a></h3>
<p>A calltip is shown automatically when one types <kbd class="kbd docutils literal notranslate">(</kbd> after the name
of an <em>accessible</em> function. A function name expression may include
dots and subscripts. A calltip remains until it is clicked, the cursor
is moved out of the argument area, or <kbd class="kbd docutils literal notranslate">)</kbd> is typed. Whenever the
cursor is in the argument part of a definition, select Edit and “Show
Call Tip” on the menu or enter its shortcut to display a calltip.</p>
<p>The calltip consists of the function’s signature and docstring up to
the latter’s first blank line or the fifth non-blank line. (Some builtin
functions lack an accessible signature.) A ‘/’ or ‘*’ in the signature
indicates that the preceding or following arguments are passed by
position or name (keyword) only. Details are subject to change.</p>
<p>In Shell, the accessible functions depends on what modules have been
imported into the user process, including those imported by Idle itself,
and which definitions have been run, all since the last restart.</p>
<p>For example, restart the Shell and enter <code class="docutils literal notranslate"><span class="pre">itertools.count(</span></code>. A calltip
appears because Idle imports itertools into the user process for its own
use. (This could change.) Enter <code class="docutils literal notranslate"><span class="pre">turtle.write(</span></code> and nothing appears.
Idle does not itself import turtle. The menu entry and shortcut also do
nothing. Enter <code class="docutils literal notranslate"><span class="pre">import</span> <span class="pre">turtle</span></code>. Thereafter, <code class="docutils literal notranslate"><span class="pre">turtle.write(</span></code>
will display a calltip.</p>
<p>In an editor, import statements have no effect until one runs the file.
One might want to run a file after writing import statements, after
adding function definitions, or after opening an existing file.</p>
</section>
<section id="code-context">
<span id="id4"></span><h3>Code Context<a class="headerlink" href="#code-context" title="Link to this heading"></a></h3>
<p>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
<code class="docutils literal notranslate"><span class="pre">class</span></code>, <code class="docutils literal notranslate"><span class="pre">def</span></code>, or <code class="docutils literal notranslate"><span class="pre">if</span></code> 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.</p>
<p>The text and background colors for the context pane can be configured under
the Highlights tab in the Configure IDLE dialog.</p>
</section>
<section id="shell-window">
<h3>Shell window<a class="headerlink" href="#shell-window" title="Link to this heading"></a></h3>
<p>In IDLE’s Shell, enter, edit, and recall complete statements. (Most
consoles and terminals only work with a single physical line at a time).</p>
<p>Submit a single-line statement for execution by hitting <kbd class="kbd docutils literal notranslate">Return</kbd>
with the cursor anywhere on the line. If a line is extended with
Backslash (<kbd class="kbd docutils literal notranslate">\</kbd>), the cursor must be on the last physical line.
Submit a multi-line compound statement by entering a blank line after
the statement.</p>
<p>When one pastes code into Shell, it is not compiled and possibly executed
until one hits <kbd class="kbd docutils literal notranslate">Return</kbd>, as specified above.
One may edit pasted code first.
If one pastes more than one statement into Shell, the result will be a
<a class="reference internal" href="exceptions.html#SyntaxError" title="SyntaxError"><code class="xref py py-exc docutils literal notranslate"><span class="pre">SyntaxError</span></code></a> when multiple statements are compiled as if they were one.</p>
<p>Lines containing <code class="docutils literal notranslate"><span class="pre">RESTART</span></code> mean that the user execution process has been
re-started. This occurs when the user execution process has crashed,
when one requests a restart on the Shell menu, or when one runs code
in an editor window.</p>
<p>The editing features described in previous subsections work when entering
code interactively. IDLE’s Shell window also responds to the following:</p>
<ul class="simple">
<li><p><kbd class="kbd compound docutils literal notranslate"><kbd class="kbd docutils literal notranslate">C</kbd>-<kbd class="kbd docutils literal notranslate">c</kbd></kbd> attempts to interrupt statement execution (but may fail).</p></li>
<li><p><kbd class="kbd compound docutils literal notranslate"><kbd class="kbd docutils literal notranslate">C</kbd>-<kbd class="kbd docutils literal notranslate">d</kbd></kbd> closes Shell if typed at a <code class="docutils literal notranslate"><span class="pre">&gt;&gt;&gt;</span></code> prompt.</p></li>
<li><p><kbd class="kbd compound docutils literal notranslate"><kbd class="kbd docutils literal notranslate">Alt</kbd>-<kbd class="kbd docutils literal notranslate">p</kbd></kbd> and <kbd class="kbd compound docutils literal notranslate"><kbd class="kbd docutils literal notranslate">Alt</kbd>-<kbd class="kbd docutils literal notranslate">n</kbd></kbd> (<kbd class="kbd compound docutils literal notranslate"><kbd class="kbd docutils literal notranslate">C</kbd>-<kbd class="kbd docutils literal notranslate">p</kbd></kbd> and <kbd class="kbd compound docutils literal notranslate"><kbd class="kbd docutils literal notranslate">C</kbd>-<kbd class="kbd docutils literal notranslate">n</kbd></kbd> on macOS)
retrieve to the current prompt the previous or next previously
entered statement that matches anything already typed.</p></li>
<li><p><kbd class="kbd docutils literal notranslate">Return</kbd> while the cursor is on any previous statement
appends the latter to anything already typed at the prompt.</p></li>
</ul>
</section>
<section id="text-colors">
<h3>Text colors<a class="headerlink" href="#text-colors" title="Link to this heading"></a></h3>
<p>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 <code class="docutils literal notranslate"><span class="pre">class</span></code> and
<code class="docutils literal notranslate"><span class="pre">def</span></code>, strings, and comments. For any text window, these are the cursor (when
present), found text (when possible), and selected text.</p>
<p>IDLE also highlights the <a class="reference internal" href="../reference/lexical_analysis.html#soft-keywords"><span class="std std-ref">soft keywords</span></a> <a class="reference internal" href="../reference/compound_stmts.html#match"><code class="xref std std-keyword docutils literal notranslate"><span class="pre">match</span></code></a>,
<a class="reference internal" href="../reference/compound_stmts.html#match"><code class="xref std std-keyword docutils literal notranslate"><span class="pre">case</span></code></a>, and <a class="reference internal" href="../reference/compound_stmts.html#wildcard-patterns"><code class="xref std std-keyword docutils literal notranslate"><span class="pre">_</span></code></a> in
pattern-matching statements. However, this highlighting is not perfect and
will be incorrect in some rare cases, including some <code class="docutils literal notranslate"><span class="pre">_</span></code>-s in <code class="docutils literal notranslate"><span class="pre">case</span></code>
patterns.</p>
<p>Text coloring is done in the background, so uncolorized text is occasionally
visible. To change the color scheme, use the Configure IDLE dialog
Highlighting tab. The marking of debugger breakpoint lines in the editor and
text in popups and dialogs is not user-configurable.</p>
</section>
</section>
<section id="startup-and-code-execution">
<h2>Startup and Code Execution<a class="headerlink" href="#startup-and-code-execution" title="Link to this heading"></a></h2>
<p>Upon startup with the <code class="docutils literal notranslate"><span class="pre">-s</span></code> option, IDLE will execute the file referenced by
the environment variables <span class="target" id="index-5"></span><code class="xref std std-envvar docutils literal notranslate"><span class="pre">IDLESTARTUP</span></code> or <span class="target" id="index-6"></span><a class="reference internal" href="../using/cmdline.html#envvar-PYTHONSTARTUP"><code class="xref std std-envvar docutils literal notranslate"><span class="pre">PYTHONSTARTUP</span></code></a>.
IDLE first checks for <code class="docutils literal notranslate"><span class="pre">IDLESTARTUP</span></code>; if <code class="docutils literal notranslate"><span class="pre">IDLESTARTUP</span></code> is present the file
referenced is run. If <code class="docutils literal notranslate"><span class="pre">IDLESTARTUP</span></code> is not present, IDLE checks for
<code class="docutils literal notranslate"><span class="pre">PYTHONSTARTUP</span></code>. 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.</p>
<p>In addition, <code class="docutils literal notranslate"><span class="pre">Tk</span></code> also loads a startup file if it is present. Note that the
Tk file is loaded unconditionally. This additional file is <code class="docutils literal notranslate"><span class="pre">.Idle.py</span></code> 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.</p>
<section id="command-line-usage">
<h3>Command line usage<a class="headerlink" href="#command-line-usage" title="Link to this heading"></a></h3>
<div class="highlight-none notranslate"><div class="highlight"><pre><span></span>idle.py [-c command] [-d] [-e] [-h] [-i] [-r file] [-s] [-t title] [-] [arg] ...
-c command run command in the shell window
-d enable debugger and open shell window
-e open editor window
-h print help message with legal combinations and exit
-i open shell window
-r file run file in shell window
-s run $IDLESTARTUP or $PYTHONSTARTUP first, in shell window
-t title set title of shell window
- run stdin in shell (- must be last option before args)
</pre></div>
</div>
<p>If there are arguments:</p>
<ul class="simple">
<li><p>If <code class="docutils literal notranslate"><span class="pre">-</span></code>, <code class="docutils literal notranslate"><span class="pre">-c</span></code>, or <code class="docutils literal notranslate"><span class="pre">r</span></code> is used, all arguments are placed in
<code class="docutils literal notranslate"><span class="pre">sys.argv[1:...]</span></code> and <code class="docutils literal notranslate"><span class="pre">sys.argv[0]</span></code> is set to <code class="docutils literal notranslate"><span class="pre">''</span></code>, <code class="docutils literal notranslate"><span class="pre">'-c'</span></code>,
or <code class="docutils literal notranslate"><span class="pre">'-r'</span></code>. No editor window is opened, even if that is the default
set in the Options dialog.</p></li>
<li><p>Otherwise, arguments are files opened for editing and
<code class="docutils literal notranslate"><span class="pre">sys.argv</span></code> reflects the arguments passed to IDLE itself.</p></li>
</ul>
</section>
<section id="startup-failure">
<h3>Startup failure<a class="headerlink" href="#startup-failure" title="Link to this heading"></a></h3>
<p>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
usually displays a <code class="docutils literal notranslate"><span class="pre">Tk</span></code> error box with a ‘cannot connect’ message
that directs the user here. It then exits.</p>
<p>One specific connection failure on Unix systems results from
misconfigured masquerading rules somewhere in a system’s network setup.
When IDLE is started from a terminal, one will see a message starting
with <code class="docutils literal notranslate"><span class="pre">**</span> <span class="pre">Invalid</span> <span class="pre">host:</span></code>.
The valid value is <code class="docutils literal notranslate"><span class="pre">127.0.0.1</span> <span class="pre">(idlelib.rpc.LOCALHOST)</span></code>.
One can diagnose with <code class="docutils literal notranslate"><span class="pre">tcpconnect</span> <span class="pre">-irv</span> <span class="pre">127.0.0.1</span> <span class="pre">6543</span></code> in one
terminal window and <code class="docutils literal notranslate"><span class="pre">tcplisten</span> <span class="pre">&lt;same</span> <span class="pre">args&gt;</span></code> in another.</p>
<p>A common cause of failure is a user-written file with the same name as a
standard library module, such as <em>random.py</em> and <em>tkinter.py</em>. When such a
file is located in the same directory as a file that is about to be run,
IDLE cannot import the stdlib file. The current fix is to rename the
user file.</p>
<p>Though less common than in the past, an antivirus or firewall program may
stop the connection. If the program cannot be taught to allow the
connection, then it must be turned off for IDLE to work. It is safe to
allow this internal connection because no data is visible on external
ports. A similar problem is a network mis-configuration that blocks
connections.</p>
<p>Python installation issues occasionally stop IDLE: multiple versions can
clash, or a single installation might need admin access. If one undo the
clash, or cannot or does not want to run as admin, it might be easiest to
completely remove Python and start over.</p>
<p>A zombie pythonw.exe process could be a problem. On Windows, use Task
Manager to check for one and stop it if there is. Sometimes a restart
initiated by a program crash or Keyboard Interrupt (control-C) may fail
to connect. Dismissing the error box or using Restart Shell on the Shell
menu may fix a temporary problem.</p>
<p>When IDLE first starts, it attempts to read user configuration files in
<code class="docutils literal notranslate"><span class="pre">~/.idlerc/</span></code> (~ 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. Instead, use the
configuration dialog, under Options. Once there is an error in a user
configuration file, the best solution may be to delete it and start over
with the settings dialog.</p>
<p>If IDLE quits with no message, and it was not started from a console, try
starting it from a console or terminal (<code class="docutils literal notranslate"><span class="pre">python</span> <span class="pre">-m</span> <span class="pre">idlelib</span></code>) and see if
this results in an error message.</p>
<p>On Unix-based systems with tcl/tk older than <code class="docutils literal notranslate"><span class="pre">8.6.11</span></code> (see
<code class="docutils literal notranslate"><span class="pre">About</span> <span class="pre">IDLE</span></code>) certain characters of certain fonts can cause
a tk failure with a message to the terminal. This can happen either
if one starts IDLE to edit a file with such a character or later
when entering such a character. If one cannot upgrade tcl/tk,
then re-configure IDLE to use a font that works better.</p>
</section>
<section id="running-user-code">
<h3>Running user code<a class="headerlink" href="#running-user-code" title="Link to this heading"></a></h3>
<p>With rare exceptions, the result of executing Python code with IDLE is
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, <code class="docutils literal notranslate"><span class="pre">sys.modules</span></code> starts with more entries,
and <code class="docutils literal notranslate"><span class="pre">threading.active_count()</span></code> returns 2 instead of 1.</p>
<p>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 <code class="docutils literal notranslate"><span class="pre">sys.stdin</span></code>, <code class="docutils literal notranslate"><span class="pre">sys.stdout</span></code>, and <code class="docutils literal notranslate"><span class="pre">sys.stderr</span></code>
with objects that get input from and send output to the Shell window.
The original values stored in <code class="docutils literal notranslate"><span class="pre">sys.__stdin__</span></code>, <code class="docutils literal notranslate"><span class="pre">sys.__stdout__</span></code>, and
<code class="docutils literal notranslate"><span class="pre">sys.__stderr__</span></code> are not touched, but may be <code class="docutils literal notranslate"><span class="pre">None</span></code>.</p>
<p>Sending print output from one process to a text widget in another is
slower than printing to a system terminal in the same process.
This has the most effect when printing multiple arguments, as the string
for each argument, each separator, the newline are sent separately.
For development, this is usually not a problem, but if one wants to
print faster in IDLE, format and join together everything one wants
displayed together and then print a single string. Both format strings
and <a class="reference internal" href="stdtypes.html#str.join" title="str.join"><code class="xref py py-meth docutils literal notranslate"><span class="pre">str.join()</span></code></a> can help combine fields and lines.</p>
<p>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 <code class="docutils literal notranslate"><span class="pre">input</span></code> from
sys.stdin or <code class="docutils literal notranslate"><span class="pre">print</span></code> or <code class="docutils literal notranslate"><span class="pre">write</span></code> to sys.stdout or sys.stderr,
IDLE should be started in a command line window. (On Windows,
use <code class="docutils literal notranslate"><span class="pre">python</span></code> or <code class="docutils literal notranslate"><span class="pre">py</span></code> rather than <code class="docutils literal notranslate"><span class="pre">pythonw</span></code> or <code class="docutils literal notranslate"><span class="pre">pyw</span></code>.)
The secondary subprocess
will then be attached to that window for input and output.</p>
<p>If <code class="docutils literal notranslate"><span class="pre">sys</span></code> is reset by user code, such as with <code class="docutils literal notranslate"><span class="pre">importlib.reload(sys)</span></code>,
IDLE’s changes are lost and input from the keyboard and output to the screen
will not work correctly.</p>
<p>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. These include system-specific functions that
determine whether a key has been pressed and if so, which.</p>
<p>The IDLE code running in the execution process adds frames to the call stack
that would not be there otherwise. IDLE wraps <code class="docutils literal notranslate"><span class="pre">sys.getrecursionlimit</span></code> and
<code class="docutils literal notranslate"><span class="pre">sys.setrecursionlimit</span></code> to reduce the effect of the additional stack
frames.</p>
<p>When user code raises SystemExit either directly or by calling sys.exit,
IDLE returns to a Shell prompt instead of exiting.</p>
</section>
<section id="user-output-in-shell">
<h3>User output in Shell<a class="headerlink" href="#user-output-in-shell" title="Link to this heading"></a></h3>
<p>When a program outputs text, the result is determined by the
corresponding output device. When IDLE executes user code, <code class="docutils literal notranslate"><span class="pre">sys.stdout</span></code>
and <code class="docutils literal notranslate"><span class="pre">sys.stderr</span></code> 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.</p>
<p>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.</p>
<p>A Tk Text widget, and hence IDLE’s Shell, displays characters (codepoints) in
the BMP (Basic Multilingual Plane) subset of Unicode. Which characters are
displayed with a proper glyph and which with a replacement box depends on the
operating system and installed fonts. Tab characters cause the following text
to begin after the next tab stop. (They occur every 8 ‘characters’). Newline
characters cause following text to appear on a new line. Other control
characters are ignored or displayed as a space, box, or something else,
depending on the operating system and font. (Moving the text cursor through
such output with arrow keys may exhibit some surprising spacing behavior.)</p>
<div class="highlight-python3 notranslate"><div class="highlight"><pre><span></span><span class="gp">&gt;&gt;&gt; </span><span class="n">s</span> <span class="o">=</span> <span class="s1">&#39;a</span><span class="se">\t</span><span class="s1">b</span><span class="se">\a</span><span class="s1">&lt;</span><span class="se">\x02</span><span class="s1">&gt;&lt;</span><span class="se">\r</span><span class="s1">&gt;</span><span class="se">\b</span><span class="s1">c</span><span class="se">\n</span><span class="s1">d&#39;</span> <span class="c1"># Enter 22 chars.</span>
<span class="gp">&gt;&gt;&gt; </span><span class="nb">len</span><span class="p">(</span><span class="n">s</span><span class="p">)</span>
<span class="go">14</span>
<span class="gp">&gt;&gt;&gt; </span><span class="n">s</span> <span class="c1"># Display repr(s)</span>
<span class="go">&#39;a\tb\x07&lt;\x02&gt;&lt;\r&gt;\x08c\nd&#39;</span>
<span class="gp">&gt;&gt;&gt; </span><span class="nb">print</span><span class="p">(</span><span class="n">s</span><span class="p">,</span> <span class="n">end</span><span class="o">=</span><span class="s1">&#39;&#39;</span><span class="p">)</span> <span class="c1"># Display s as is.</span>
<span class="go"># Result varies by OS and font. Try it.</span>
</pre></div>
</div>
<p>The <code class="docutils literal notranslate"><span class="pre">repr</span></code> 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.</p>
<p>Normal and error output are generally kept separate (on separate lines)
from code input and each other. They each get different highlight colors.</p>
<p>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.</p>
<p>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.</p>
<p>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.</p>
</section>
<section id="developing-tkinter-applications">
<h3>Developing tkinter applications<a class="headerlink" href="#developing-tkinter-applications" title="Link to this heading"></a></h3>
<p>IDLE is intentionally different from standard Python in order to
facilitate development of tkinter programs. Enter <code class="docutils literal notranslate"><span class="pre">import</span> <span class="pre">tkinter</span> <span class="pre">as</span> <span class="pre">tk;</span>
<span class="pre">root</span> <span class="pre">=</span> <span class="pre">tk.Tk()</span></code> in standard Python and nothing appears. Enter the same
in IDLE and a tk window appears. In standard Python, one must also enter
<code class="docutils literal notranslate"><span class="pre">root.update()</span></code> to see the window. IDLE does the equivalent in the
background, about 20 times a second, which is about every 50 milliseconds.
Next enter <code class="docutils literal notranslate"><span class="pre">b</span> <span class="pre">=</span> <span class="pre">tk.Button(root,</span> <span class="pre">text='button');</span> <span class="pre">b.pack()</span></code>. Again,
nothing visibly changes in standard Python until one enters <code class="docutils literal notranslate"><span class="pre">root.update()</span></code>.</p>
<p>Most tkinter programs run <code class="docutils literal notranslate"><span class="pre">root.mainloop()</span></code>, which usually does not
return until the tk app is destroyed. If the program is run with
<code class="docutils literal notranslate"><span class="pre">python</span> <span class="pre">-i</span></code> or from an IDLE editor, a <code class="docutils literal notranslate"><span class="pre">&gt;&gt;&gt;</span></code> shell prompt does not
appear until <code class="docutils literal notranslate"><span class="pre">mainloop()</span></code> returns, at which time there is nothing left
to interact with.</p>
<p>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
interact with the live application. One just has to remember to
re-enable the mainloop call when running in standard Python.</p>
</section>
<section id="running-without-a-subprocess">
<h3>Running without a subprocess<a class="headerlink" href="#running-without-a-subprocess" title="Link to this heading"></a></h3>
<p>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.
If firewall software complains anyway, you can ignore it.</p>
<p>If the attempt to make the socket connection fails, Idle will notify you.
Such failures are sometimes transient, but if persistent, the problem
may be either a firewall blocking the connection or misconfiguration of
a particular system. Until the problem is fixed, one can run Idle with
the -n command line switch.</p>
<p>If IDLE is started with the -n command line switch it will run in a
single process and will not create the subprocess which runs the RPC
Python execution server. This can be useful if Python cannot create
the subprocess or the RPC socket interface on your platform. However,
in this mode user code is not isolated from IDLE itself. Also, the
environment is not restarted when Run/Run Module (F5) is selected. If
your code has been modified, you must reload() the affected modules and
re-import any specific items (e.g. from foo import baz) if the changes
are to take effect. For these reasons, it is preferable to run IDLE
with the default subprocess if at all possible.</p>
<div class="deprecated">
<p><span class="versionmodified deprecated">Deprecated since version 3.4.</span></p>
</div>
</section>
</section>
<section id="help-and-preferences">
<h2>Help and Preferences<a class="headerlink" href="#help-and-preferences" title="Link to this heading"></a></h2>
<section id="help-sources">
<span id="id5"></span><h3>Help sources<a class="headerlink" href="#help-sources" title="Link to this heading"></a></h3>
<p>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.</p>
<p>Help menu entry “Python Docs” opens the extensive sources of help,
including tutorials, available at <code class="docutils literal notranslate"><span class="pre">docs.python.org/x.y</span></code>, 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.</p>
<p>Selected URLs can be added or removed from the help menu at any time using the
General tab of the Configure IDLE dialog.</p>
</section>
<section id="setting-preferences">
<span id="preferences"></span><h3>Setting preferences<a class="headerlink" href="#setting-preferences" title="Link to this heading"></a></h3>
<p>The font preferences, highlighting, keys, and general preferences can be
changed via Configure IDLE on the Option menu.
Non-default user settings are saved in a <code class="docutils literal notranslate"><span class="pre">.idlerc</span></code> 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 <code class="docutils literal notranslate"><span class="pre">.idlerc</span></code>.</p>
<p>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.</p>
<p>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.</p>
</section>
<section id="idle-on-macos">
<h3>IDLE on macOS<a class="headerlink" href="#idle-on-macos" title="Link to this heading"></a></h3>
<p>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.</p>
</section>
<section id="extensions">
<h3>Extensions<a class="headerlink" href="#extensions" title="Link to this heading"></a></h3>
<p>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
information. The only current default extension is zzdummy, an example
also used for testing.</p>
</section>
</section>
<section id="module-idlelib">
<span id="idlelib-implementation-of-idle-application"></span><h2>idlelib — implementation of IDLE application<a class="headerlink" href="#module-idlelib" title="Link to this heading"></a></h2>
<p><strong>Source code:</strong> <a class="extlink-source reference external" href="https://github.com/python/cpython/tree/main/Lib/idlelib">Lib/idlelib</a></p>
<hr class="docutils" />
<p>The Lib/idlelib package implements the IDLE application. See the rest
of this page for how to use IDLE.</p>
<p>The files in idlelib are described in idlelib/README.txt. Access it
either in idlelib or click Help =&gt; About IDLE on the IDLE menu. This
file also maps IDLE menu items to the code that implements the item.
Except for files listed under ‘Startup’, the idlelib code is ‘private’ in
sense that feature changes can be backported (see <span class="target" id="index-7"></span><a class="pep reference external" href="https://peps.python.org/pep-0434/"><strong>PEP 434</strong></a>).</p>
</section>
</section>
@@ -0,0 +1,297 @@
""" help.py: Implement the Idle help menu.
Contents are subject to revision at any time, without notice.
Help => About IDLE: display About Idle dialog
<to be moved here from help_about.py>
Help => IDLE Help: Display help.html with proper formatting.
Doc/library/idle.rst (Sphinx)=> Doc/build/html/library/idle.html
(help.copy_strip)=> Lib/idlelib/help.html
HelpParser - Parse help.html and render to tk Text.
HelpText - Display formatted help.html.
HelpFrame - Contain text, scrollbar, and table-of-contents.
(This will be needed for display in a future tabbed window.)
HelpWindow - Display HelpFrame in a standalone window.
copy_strip - Copy the text part of idle.html to help.html while rstripping each line.
show_idlehelp - Create HelpWindow. Called in EditorWindow.help_dialog.
"""
from html.parser import HTMLParser
from os.path import abspath, dirname, isfile, join
from platform import python_version
from tkinter import Toplevel, Text, Menu
from tkinter.ttk import Frame, Menubutton, Scrollbar, Style
from tkinter import font as tkfont
from idlelib.config import idleConf
from idlelib.colorizer import color_config
## About IDLE ##
## IDLE Help ##
class HelpParser(HTMLParser):
"""Render help.html into a text widget.
The overridden handle_xyz methods handle a subset of html tags.
The supplied text should have the needed tag configurations.
The behavior for unsupported tags, such as table, is undefined.
If the tags generated by Sphinx change, this class, especially
the handle_starttag and handle_endtags methods, might have to also.
"""
def __init__(self, text):
HTMLParser.__init__(self, convert_charrefs=True)
self.text = text # Text widget we're rendering into.
self.tags = '' # Current block level text tags to apply.
self.chartags = '' # Current character level text tags.
self.hdrlink = False # Exclude html header links.
self.level = 0 # Track indentation level.
self.pre = False # Displaying preformatted text?
self.hprefix = '' # Heading prefix (like '25.5'?) to remove.
self.nested_dl = False # In a nested <dl>?
self.simplelist = False # In a simple list (no double spacing)?
self.toc = [] # Pair headers with text indexes for toc.
self.header = '' # Text within header tags for toc.
self.prevtag = None # Previous tag info (opener?, tag).
def indent(self, amt=1):
"Change indent (+1, 0, -1) and tags."
self.level += amt
self.tags = '' if self.level == 0 else 'l'+str(self.level)
def handle_starttag(self, tag, attrs):
"Handle starttags in help.html."
class_ = ''
for a, v in attrs:
if a == 'class':
class_ = v
s = ''
if tag == 'p' and self.prevtag and not self.prevtag[0]:
# Begin a new block for <p> tags after a closed tag.
# Avoid extra lines, e.g. after <pre> tags.
lastline = self.text.get('end-1c linestart', 'end-1c')
s = '\n\n' if lastline and not lastline.isspace() else '\n'
elif tag == 'span' and class_ == 'pre':
self.chartags = 'pre'
elif tag == 'span' and class_ == 'versionmodified':
self.chartags = 'em'
elif tag == 'em':
self.chartags = 'em'
elif tag in ['ul', 'ol']:
if class_.find('simple') != -1:
s = '\n'
self.simplelist = True
else:
self.simplelist = False
self.indent()
elif tag == 'dl':
if self.level > 0:
self.nested_dl = True
elif tag == 'li':
s = '\n* '
elif tag == 'dt':
s = '\n\n' if not self.nested_dl else '\n' # Avoid extra line.
self.nested_dl = False
elif tag == 'dd':
self.indent()
s = '\n'
elif tag == 'pre':
self.pre = True
self.text.insert('end', '\n\n')
self.tags = 'preblock'
elif tag == 'a' and class_ == 'headerlink':
self.hdrlink = True
elif tag == 'h1':
self.tags = tag
elif tag in ['h2', 'h3']:
self.header = ''
self.text.insert('end', '\n\n')
self.tags = tag
self.text.insert('end', s, (self.tags, self.chartags))
self.prevtag = (True, tag)
def handle_endtag(self, tag):
"Handle endtags in help.html."
if tag in ['h1', 'h2', 'h3']:
assert self.level == 0
indent = (' ' if tag == 'h3' else
' ' if tag == 'h2' else
'')
self.toc.append((indent+self.header, self.text.index('insert')))
self.tags = ''
elif tag in ['span', 'em']:
self.chartags = ''
elif tag == 'a':
self.hdrlink = False
elif tag == 'pre':
self.pre = False
self.tags = ''
elif tag in ['ul', 'dd', 'ol']:
self.indent(-1)
self.prevtag = (False, tag)
def handle_data(self, data):
"Handle date segments in help.html."
if not self.hdrlink:
d = data if self.pre else data.replace('\n', ' ')
if self.tags == 'h1':
try:
self.hprefix = d[:d.index(' ')]
if not self.hprefix.isdigit():
self.hprefix = ''
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))
class HelpText(Text):
"Display help.html."
def __init__(self, parent, filename):
"Configure tags and feed file to parser."
uwide = idleConf.GetOption('main', 'EditorWindow', 'width', type='int')
uhigh = idleConf.GetOption('main', 'EditorWindow', 'height', type='int')
uhigh = 3 * uhigh // 4 # Lines average 4/3 of editor line height.
Text.__init__(self, parent, wrap='word', highlightthickness=0,
padx=5, borderwidth=0, width=uwide, height=uhigh)
normalfont = self.findfont(['TkDefaultFont', 'arial', 'helvetica'])
fixedfont = self.findfont(['TkFixedFont', 'monaco', 'courier'])
color_config(self)
self['font'] = (normalfont, 12)
self.tag_configure('em', font=(normalfont, 12, 'italic'))
self.tag_configure('h1', font=(normalfont, 20, 'bold'))
self.tag_configure('h2', font=(normalfont, 18, 'bold'))
self.tag_configure('h3', font=(normalfont, 15, 'bold'))
self.tag_configure('pre', font=(fixedfont, 12))
preback = self['selectbackground']
self.tag_configure('preblock', font=(fixedfont, 10), lmargin1=25,
background=preback)
self.tag_configure('l1', lmargin1=25, lmargin2=25)
self.tag_configure('l2', lmargin1=50, lmargin2=50)
self.tag_configure('l3', lmargin1=75, lmargin2=75)
self.tag_configure('l4', lmargin1=100, lmargin2=100)
self.parser = HelpParser(self)
with open(filename, encoding='utf-8') as f:
contents = f.read()
self.parser.feed(contents)
self['state'] = 'disabled'
def findfont(self, names):
"Return name of first font family derived from names."
for name in names:
if name.lower() in (x.lower() for x in tkfont.names(root=self)):
font = tkfont.Font(name=name, exists=True, root=self)
return font.actual()['family']
elif name.lower() in (x.lower()
for x in tkfont.families(root=self)):
return name
class HelpFrame(Frame):
"Display html text, scrollbar, and toc."
def __init__(self, parent, filename):
Frame.__init__(self, parent)
self.text = text = HelpText(self, filename)
self.style = Style(parent)
self['style'] = 'helpframe.TFrame'
self.style.configure('helpframe.TFrame', background=text['background'])
self.toc = toc = self.toc_menu(text)
self.scroll = scroll = Scrollbar(self, command=text.yview)
text['yscrollcommand'] = scroll.set
self.rowconfigure(0, weight=1)
self.columnconfigure(1, weight=1) # Only expand the text widget.
toc.grid(row=0, column=0, sticky='nw')
text.grid(row=0, column=1, sticky='nsew')
scroll.grid(row=0, column=2, sticky='ns')
def toc_menu(self, text):
"Create table of contents as drop-down menu."
toc = Menubutton(self, text='TOC')
drop = Menu(toc, tearoff=False)
for lbl, dex in text.parser.toc:
drop.add_command(label=lbl, command=lambda dex=dex:text.yview(dex))
toc['menu'] = drop
return toc
class HelpWindow(Toplevel):
"Display frame with rendered html."
def __init__(self, parent, filename, title):
Toplevel.__init__(self, parent)
self.wm_title(title)
self.protocol("WM_DELETE_WINDOW", self.destroy)
self.frame = HelpFrame(self, filename)
self.frame.grid(column=0, row=0, sticky='nsew')
self.grid_columnconfigure(0, weight=1)
self.grid_rowconfigure(0, weight=1)
def copy_strip(): # pragma: no cover
"""Copy the text part of idle.html to idlelib/help.html while stripping trailing whitespace.
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 on the master branch, with
sphinx-build -bhtml . build/html
python_d.exe -c "from idlelib.help import copy_strip; copy_strip()"
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 change
the displayed text, but might break HelpParser.
As long as master and maintenance versions of idle.rst remain the
same, help.html can be backported. The internal Python version
number is not displayed. If maintenance idle.rst diverges from
the master version, then instead of backporting help.html from
master, repeat the procedure above to generate a maintenance
version.
"""
src = join(abspath(dirname(dirname(dirname(__file__)))),
'Doc', 'build', 'html', 'library', 'idle.html')
dst = join(abspath(dirname(__file__)), 'help.html')
with open(src, 'r', encoding="utf-8") as inn, open(dst, 'w', encoding="utf-8") as out:
copy = False
for line in inn:
if '<section id="idle">' in line: copy = True
if '<div class="clearer">' in line: break
if copy: out.write(line.strip() + '\n')
print(f'{src} copied to {dst}')
def show_idlehelp(parent):
"Create HelpWindow; called from Idle Help event handler."
filename = join(abspath(dirname(__file__)), 'help.html')
if not isfile(filename): # pragma: no cover
# Try copy_strip, present message.
return
return HelpWindow(parent, filename, 'IDLE Doc (%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)
@@ -0,0 +1,211 @@
"""About Dialog for IDLE
"""
import os
import sys
import webbrowser
from platform import python_version, architecture
from tkinter import Toplevel, Frame, Label, Button, PhotoImage
from tkinter import SUNKEN, TOP, BOTTOM, LEFT, X, BOTH, W, EW, NSEW, E
from idlelib import textview
pyver = python_version()
if sys.platform == 'darwin':
bits = '64' if sys.maxsize > 2**32 else '32'
else:
bits = architecture()[0][:2]
class AboutDialog(Toplevel):
"""Modal about dialog for idle
"""
def __init__(self, parent, title=None, *, _htest=False, _utest=False):
"""Create popup, do not return until tk widget destroyed.
parent - parent of this dialog
title - string which is title of popup dialog
_htest - bool, change box location when running htest
_utest - bool, don't wait_window when running unittest
"""
Toplevel.__init__(self, parent)
self.configure(borderwidth=5)
# place dialog below parent if running htest
self.geometry("+%d+%d" % (
parent.winfo_rootx()+30,
parent.winfo_rooty()+(30 if not _htest else 100)))
self.bg = "#bbbbbb"
self.fg = "#000000"
self.create_widgets()
self.resizable(height=False, width=False)
self.title(title or
f'About IDLE {pyver} ({bits} bit)')
self.transient(parent)
self.grab_set()
self.protocol("WM_DELETE_WINDOW", self.ok)
self.parent = parent
self.button_ok.focus_set()
self.bind('<Return>', self.ok) # dismiss dialog
self.bind('<Escape>', self.ok) # dismiss dialog
self._current_textview = None
self._utest = _utest
if not _utest:
self.deiconify()
self.wait_window()
def create_widgets(self):
frame = Frame(self, borderwidth=2, relief=SUNKEN)
frame_buttons = Frame(self)
frame_buttons.pack(side=BOTTOM, fill=X)
frame.pack(side=TOP, expand=True, fill=BOTH)
self.button_ok = Button(frame_buttons, text='Close',
command=self.ok)
self.button_ok.pack(padx=5, pady=5)
frame_background = Frame(frame, bg=self.bg)
frame_background.pack(expand=True, fill=BOTH)
header = Label(frame_background, text='IDLE', fg=self.fg,
bg=self.bg, font=('courier', 24, 'bold'))
header.grid(row=0, column=0, sticky=E, padx=10, pady=10)
tkpatch = self._root().getvar('tk_patchLevel')
ext = '.png' if tkpatch >= '8.6' else '.gif'
icon = os.path.join(os.path.abspath(os.path.dirname(__file__)),
'Icons', f'idle_48{ext}')
self.icon_image = PhotoImage(master=self._root(), file=icon)
logo = Label(frame_background, image=self.icon_image, bg=self.bg)
logo.grid(row=0, column=0, sticky=W, rowspan=2, padx=10, pady=10)
byline_text = "Python's Integrated Development\nand Learning Environment" + 5*'\n'
byline = Label(frame_background, text=byline_text, justify=LEFT,
fg=self.fg, bg=self.bg)
byline.grid(row=2, column=0, sticky=W, columnspan=3, padx=10, pady=5)
forums_url = "https://discuss.python.org"
forums = Button(frame_background, text='Python (and IDLE) Discussion', width=35,
highlightbackground=self.bg,
command=lambda: webbrowser.open(forums_url))
forums.grid(row=6, column=0, sticky=W, padx=10, pady=10)
docs_url = ("https://docs.python.org/%d.%d/library/idle.html" %
sys.version_info[:2])
docs = Button(frame_background, text='IDLE Documentation', width=35,
highlightbackground=self.bg,
command=lambda: webbrowser.open(docs_url))
docs.grid(row=7, column=0, columnspan=2, sticky=W, padx=10, pady=10)
Frame(frame_background, borderwidth=1, relief=SUNKEN,
height=2, bg=self.bg).grid(row=8, column=0, sticky=EW,
columnspan=3, padx=5, pady=5)
tclver = str(self.info_patchlevel())
tkver = ' and ' + tkpatch if tkpatch != tclver else ''
versions = f"Python {pyver} with tcl/tk {tclver}{tkver}"
vers = Label(frame_background, text=versions, fg=self.fg, bg=self.bg)
vers.grid(row=9, column=0, sticky=W, padx=10, pady=0)
py_buttons = Frame(frame_background, bg=self.bg)
py_buttons.grid(row=10, column=0, columnspan=2, sticky=NSEW)
self.py_license = Button(py_buttons, text='License', width=8,
highlightbackground=self.bg,
command=self.show_py_license)
self.py_license.pack(side=LEFT, padx=10, pady=10)
self.py_copyright = Button(py_buttons, text='Copyright', width=8,
highlightbackground=self.bg,
command=self.show_py_copyright)
self.py_copyright.pack(side=LEFT, padx=10, pady=10)
self.py_credits = Button(py_buttons, text='Credits', width=8,
highlightbackground=self.bg,
command=self.show_py_credits)
self.py_credits.pack(side=LEFT, padx=10, pady=10)
Frame(frame_background, borderwidth=1, relief=SUNKEN,
height=2, bg=self.bg).grid(row=11, column=0, sticky=EW,
columnspan=3, padx=5, pady=5)
idle = Label(frame_background, text='IDLE', fg=self.fg, bg=self.bg)
idle.grid(row=12, column=0, sticky=W, padx=10, pady=0)
idle_buttons = Frame(frame_background, bg=self.bg)
idle_buttons.grid(row=13, column=0, columnspan=3, sticky=NSEW)
self.readme = Button(idle_buttons, text='Readme', width=8,
highlightbackground=self.bg,
command=self.show_readme)
self.readme.pack(side=LEFT, padx=10, pady=10)
self.idle_news = Button(idle_buttons, text='News', width=8,
highlightbackground=self.bg,
command=self.show_idle_news)
self.idle_news.pack(side=LEFT, padx=10, pady=10)
self.idle_credits = Button(idle_buttons, text='Credits', width=8,
highlightbackground=self.bg,
command=self.show_idle_credits)
self.idle_credits.pack(side=LEFT, padx=10, pady=10)
# License, copyright, and credits are of type _sitebuiltins._Printer
def show_py_license(self):
"Handle License button event."
self.display_printer_text('About - License', license)
def show_py_copyright(self):
"Handle Copyright button event."
self.display_printer_text('About - Copyright', copyright)
def show_py_credits(self):
"Handle Python Credits button event."
self.display_printer_text('About - Python Credits', credits)
# Encode CREDITS.txt to utf-8 for proper version of Loewis.
# Specify others as ascii until need utf-8, so catch errors.
def show_idle_credits(self):
"Handle Idle Credits button event."
self.display_file_text('About - Credits', 'CREDITS.txt', 'utf-8')
def show_readme(self):
"Handle Readme button event."
self.display_file_text('About - Readme', 'README.txt', 'ascii')
def show_idle_news(self):
"Handle News button event."
self.display_file_text('About - News', 'News3.txt', 'utf-8')
def display_printer_text(self, title, printer):
"""Create textview for built-in constants.
Built-in constants have type _sitebuiltins._Printer. The
text is extracted from the built-in and then sent to a text
viewer with self as the parent and title as the title of
the popup.
"""
printer._Printer__setup()
text = '\n'.join(printer._Printer__lines)
self._current_textview = textview.view_text(
self, title, text, _utest=self._utest)
def display_file_text(self, title, filename, encoding=None):
"""Create textview for filename.
The filename needs to be in the current directory. The path
is sent to a text viewer with self as the parent, title as
the title of the popup, and the file encoding.
"""
fn = os.path.join(os.path.abspath(os.path.dirname(__file__)), filename)
self._current_textview = textview.view_file(
self, title, fn, encoding, _utest=self._utest)
def ok(self, event=None):
"Dismiss help_about dialog."
self.grab_release()
self.destroy()
if __name__ == '__main__':
from unittest import main
main('idlelib.idle_test.test_help_about', verbosity=2, exit=False)
from idlelib.idle_test.htest import run
run(AboutDialog)
@@ -0,0 +1,106 @@
"Implement Idle Shell history mechanism with History class"
from idlelib.config import idleConf
class History:
''' Implement Idle Shell history mechanism.
store - Store source statement (called from pyshell.resetoutput).
fetch - Fetch stored statement matching prefix already entered.
history_next - Bound to <<history-next>> event (default Alt-N).
history_prev - Bound to <<history-prev>> event (default Alt-P).
'''
def __init__(self, text):
'''Initialize data attributes and bind event methods.
.text - Idle wrapper of tk Text widget, with .bell().
.history - source statements, possibly with multiple lines.
.prefix - source already entered at prompt; filters history list.
.pointer - index into history.
.cyclic - wrap around history list (or not).
'''
self.text = text
self.history = []
self.prefix = None
self.pointer = None
self.cyclic = idleConf.GetOption("main", "History", "cyclic", 1, "bool")
text.bind("<<history-previous>>", self.history_prev)
text.bind("<<history-next>>", self.history_next)
def history_next(self, event):
"Fetch later statement; start with earliest if cyclic."
self.fetch(reverse=False)
return "break"
def history_prev(self, event):
"Fetch earlier statement; start with most recent."
self.fetch(reverse=True)
return "break"
def fetch(self, reverse):
'''Fetch statement and replace current line in text widget.
Set prefix and pointer as needed for successive fetches.
Reset them to None, None when returning to the start line.
Sound bell when return to start line or cannot leave a line
because cyclic is False.
'''
nhist = len(self.history)
pointer = self.pointer
prefix = self.prefix
if pointer is not None and prefix is not None:
if self.text.compare("insert", "!=", "end-1c") or \
self.text.get("iomark", "end-1c") != self.history[pointer]:
pointer = prefix = None
self.text.mark_set("insert", "end-1c") # != after cursor move
if pointer is None or prefix is None:
prefix = self.text.get("iomark", "end-1c")
if reverse:
pointer = nhist # will be decremented
else:
if self.cyclic:
pointer = -1 # will be incremented
else: # abort history_next
self.text.bell()
return
nprefix = len(prefix)
while True:
pointer += -1 if reverse else 1
if pointer < 0 or pointer >= nhist:
self.text.bell()
if not self.cyclic and pointer < 0: # abort history_prev
return
else:
if self.text.get("iomark", "end-1c") != prefix:
self.text.delete("iomark", "end-1c")
self.text.insert("iomark", prefix, "stdin")
pointer = prefix = None
break
item = self.history[pointer]
if item[:nprefix] == prefix and len(item) > nprefix:
self.text.delete("iomark", "end-1c")
self.text.insert("iomark", item, "stdin")
break
self.text.see("insert")
self.text.tag_remove("sel", "1.0", "end")
self.pointer = pointer
self.prefix = prefix
def store(self, source):
"Store Shell input statement into history list."
source = source.strip()
if len(source) > 2:
# avoid duplicates
try:
self.history.remove(source)
except ValueError:
pass
self.history.append(source)
self.pointer = None
self.prefix = None
if __name__ == "__main__":
from unittest import main
main('idlelib.idle_test.test_history', verbosity=2, exit=False)
@@ -0,0 +1,312 @@
"""Provide advanced parsing abilities for ParenMatch and other extensions.
HyperParser uses PyParser. PyParser mostly gives information on the
proper indentation of code. HyperParser gives additional information on
the structure of code.
"""
from keyword import iskeyword
import string
from idlelib import pyparse
# all ASCII chars that may be in an identifier
_ASCII_ID_CHARS = frozenset(string.ascii_letters + string.digits + "_")
# all ASCII chars that may be the first char of an identifier
_ASCII_ID_FIRST_CHARS = frozenset(string.ascii_letters + "_")
# lookup table for whether 7-bit ASCII chars are valid in a Python identifier
_IS_ASCII_ID_CHAR = [(chr(x) in _ASCII_ID_CHARS) for x in range(128)]
# lookup table for whether 7-bit ASCII chars are valid as the first
# char in a Python identifier
_IS_ASCII_ID_FIRST_CHAR = \
[(chr(x) in _ASCII_ID_FIRST_CHARS) for x in range(128)]
class HyperParser:
def __init__(self, editwin, index):
"To initialize, analyze the surroundings of the given index."
self.editwin = editwin
self.text = text = editwin.text
parser = pyparse.Parser(editwin.indentwidth, editwin.tabwidth)
def index2line(index):
return int(float(index))
lno = index2line(text.index(index))
if not editwin.prompt_last_line:
for context in editwin.num_context_lines:
startat = max(lno - context, 1)
startatindex = repr(startat) + ".0"
stopatindex = "%d.end" % lno
# We add the newline because PyParse requires a newline
# 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_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:
break
parser.set_lo(bod or 0)
else:
r = text.tag_prevrange("console", index)
if r:
startatindex = r[1]
else:
startatindex = "1.0"
stopatindex = "%d.end" % lno
# 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_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.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
self.bracketing = parser.get_last_stmt_bracketing()
# find which pairs of bracketing are openers. These always
# correspond to a character of rawtext.
self.isopener = [i>0 and self.bracketing[i][1] >
self.bracketing[i-1][1]
for i in range(len(self.bracketing))]
self.set_index(index)
def set_index(self, index):
"""Set the index to which the functions relate.
The index must be in the same statement.
"""
indexinrawtext = (len(self.rawtext) -
len(self.text.get(index, self.stopatindex)))
if indexinrawtext < 0:
raise ValueError("Index %s precedes the analyzed statement"
% index)
self.indexinrawtext = indexinrawtext
# find the rightmost bracket to which index belongs
self.indexbracket = 0
while (self.indexbracket < len(self.bracketing)-1 and
self.bracketing[self.indexbracket+1][0] < self.indexinrawtext):
self.indexbracket += 1
if (self.indexbracket < len(self.bracketing)-1 and
self.bracketing[self.indexbracket+1][0] == self.indexinrawtext and
not self.isopener[self.indexbracket+1]):
self.indexbracket += 1
def is_in_string(self):
"""Is the index given to the HyperParser in a string?"""
# The bracket to which we belong should be an opener.
# If it's an opener, it has to have a character.
return (self.isopener[self.indexbracket] and
self.rawtext[self.bracketing[self.indexbracket][0]]
in ('"', "'"))
def is_in_code(self):
"""Is the index given to the HyperParser in normal code?"""
return (not self.isopener[self.indexbracket] or
self.rawtext[self.bracketing[self.indexbracket][0]]
not in ('#', '"', "'"))
def get_surrounding_brackets(self, openers='([{', mustclose=False):
"""Return bracket indexes or None.
If the index given to the HyperParser is surrounded by a
bracket defined in openers (or at least has one before it),
return the indices of the opening bracket and the closing
bracket (or the end of line, whichever comes first).
If it is not surrounded by brackets, or the end of line comes
before the closing bracket and mustclose is True, returns None.
"""
bracketinglevel = self.bracketing[self.indexbracket][1]
before = self.indexbracket
while (not self.isopener[before] or
self.rawtext[self.bracketing[before][0]] not in openers or
self.bracketing[before][1] > bracketinglevel):
before -= 1
if before < 0:
return None
bracketinglevel = min(bracketinglevel, self.bracketing[before][1])
after = self.indexbracket + 1
while (after < len(self.bracketing) and
self.bracketing[after][1] >= bracketinglevel):
after += 1
beforeindex = self.text.index("%s-%dc" %
(self.stopatindex, len(self.rawtext)-self.bracketing[before][0]))
if (after >= len(self.bracketing) or
self.bracketing[after][0] > len(self.rawtext)):
if mustclose:
return None
afterindex = self.stopatindex
else:
# We are after a real char, so it is a ')' and we give the
# index before it.
afterindex = self.text.index(
"%s-%dc" % (self.stopatindex,
len(self.rawtext)-(self.bracketing[after][0]-1)))
return beforeindex, afterindex
# the set of built-in identifiers which are also keywords,
# i.e. keyword.iskeyword() returns True for them
_ID_KEYWORDS = frozenset({"True", "False", "None"})
@classmethod
def _eat_identifier(cls, str, limit, pos):
"""Given a string and pos, return the number of chars in the
identifier which ends at pos, or 0 if there is no such one.
This ignores non-identifier eywords are not identifiers.
"""
is_ascii_id_char = _IS_ASCII_ID_CHAR
# Start at the end (pos) and work backwards.
i = pos
# Go backwards as long as the characters are valid ASCII
# identifier characters. This is an optimization, since it
# is faster in the common case where most of the characters
# are ASCII.
while i > limit and (
ord(str[i - 1]) < 128 and
is_ascii_id_char[ord(str[i - 1])]
):
i -= 1
# If the above loop ended due to reaching a non-ASCII
# character, continue going backwards using the most generic
# test for whether a string contains only valid identifier
# characters.
if i > limit and ord(str[i - 1]) >= 128:
while i - 4 >= limit and ('a' + str[i - 4:pos]).isidentifier():
i -= 4
if i - 2 >= limit and ('a' + str[i - 2:pos]).isidentifier():
i -= 2
if i - 1 >= limit and ('a' + str[i - 1:pos]).isidentifier():
i -= 1
# The identifier candidate starts here. If it isn't a valid
# identifier, don't eat anything. At this point that is only
# possible if the first character isn't a valid first
# character for an identifier.
if not str[i:pos].isidentifier():
return 0
elif i < pos:
# All characters in str[i:pos] are valid ASCII identifier
# characters, so it is enough to check that the first is
# valid as the first character of an identifier.
if not _IS_ASCII_ID_FIRST_CHAR[ord(str[i])]:
return 0
# All keywords are valid identifiers, but should not be
# considered identifiers here, except for True, False and None.
if i < pos and (
iskeyword(str[i:pos]) and
str[i:pos] not in cls._ID_KEYWORDS
):
return 0
return pos - i
# This string includes all chars that may be in a white space
_whitespace_chars = " \t\n\\"
def get_expression(self):
"""Return a string with the Python expression which ends at the
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 "
"if index is inside a code.")
rawtext = self.rawtext
bracketing = self.bracketing
brck_index = self.indexbracket
brck_limit = bracketing[brck_index][0]
pos = self.indexinrawtext
last_identifier_pos = pos
postdot_phase = True
while True:
# Eat whitespaces, comments, and if postdot_phase is False - a dot
while True:
if pos>brck_limit and rawtext[pos-1] in self._whitespace_chars:
# Eat a whitespace
pos -= 1
elif (not postdot_phase and
pos > brck_limit and rawtext[pos-1] == '.'):
# Eat a dot
pos -= 1
postdot_phase = True
# The next line will fail if we are *inside* a comment,
# but we shouldn't be.
elif (pos == brck_limit and brck_index > 0 and
rawtext[bracketing[brck_index-1][0]] == '#'):
# Eat a comment
brck_index -= 2
brck_limit = bracketing[brck_index][0]
pos = bracketing[brck_index+1][0]
else:
# If we didn't eat anything, quit.
break
if not postdot_phase:
# We didn't find a dot, so the expression end at the
# last identifier pos.
break
ret = self._eat_identifier(rawtext, brck_limit, pos)
if ret:
# There is an identifier to eat
pos = pos - ret
last_identifier_pos = pos
# Now, to continue the search, we must find a dot.
postdot_phase = False
# (the loop continues now)
elif pos == brck_limit:
# We are at a bracketing limit. If it is a closing
# bracket, eat the bracket, otherwise, stop the search.
level = bracketing[brck_index][1]
while brck_index > 0 and bracketing[brck_index-1][1] > level:
brck_index -= 1
if bracketing[brck_index][0] == brck_limit:
# We were not at the end of a closing bracket
break
pos = bracketing[brck_index][0]
brck_index -= 1
brck_limit = bracketing[brck_index][0]
last_identifier_pos = pos
if rawtext[pos] in "([":
# [] and () may be used after an identifier, so we
# continue. postdot_phase is True, so we don't allow a dot.
pass
else:
# We can't continue after other types of brackets
if rawtext[pos] in "'\"":
# Scan a string prefix
while pos > 0 and rawtext[pos - 1] in "rRbBuU":
pos -= 1
last_identifier_pos = pos
break
else:
# We've found an operator or something.
break
return rawtext[last_identifier_pos:self.indexinrawtext]
if __name__ == '__main__':
from unittest import main
main('idlelib.idle_test.test_hyperparser', verbosity=2)
@@ -0,0 +1,4 @@
@echo off
rem Start IDLE using the appropriate Python interpreter
set CURRDIR=%~dp0
start "IDLE" "%CURRDIR%..\..\pythonw.exe" "%CURRDIR%idle.pyw" %1 %2 %3 %4 %5 %6 %7 %8 %9
@@ -0,0 +1,14 @@
import os.path
import sys
# Enable running IDLE with idlelib in a non-standard location.
# This was once used to run development versions of IDLE.
# Because PEP 434 declared idle.py a public interface,
# removal should require deprecation.
idlelib_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if idlelib_dir not in sys.path:
sys.path.insert(0, idlelib_dir)
from idlelib.pyshell import main # This is subject to change
main()
@@ -0,0 +1,17 @@
try:
import idlelib.pyshell
except ImportError:
# IDLE is not installed, but maybe pyshell is on sys.path:
from . import pyshell
import os
idledir = os.path.dirname(os.path.abspath(pyshell.__file__))
if idledir != os.getcwd():
# We're not in the IDLE directory, help the subprocess find run.py
pypath = os.environ.get('PYTHONPATH', '')
if pypath:
os.environ['PYTHONPATH'] = pypath + ':' + idledir
else:
os.environ['PYTHONPATH'] = idledir
pyshell.main()
else:
idlelib.pyshell.main()
@@ -0,0 +1,441 @@
import io
import os
import shlex
import sys
import tempfile
import tokenize
from tkinter import filedialog
from tkinter import messagebox
from tkinter.simpledialog import askstring # loadfile encoding.
from idlelib.config import idleConf
from idlelib.util import py_extensions
py_extensions = ' '.join("*"+ext for ext in py_extensions)
encoding = 'utf-8'
errors = 'surrogatepass' if sys.platform == 'win32' else 'surrogateescape'
class IOBinding:
# One instance per editor Window so methods know which to save, close.
# Open returns focus to self.editwin if aborted.
# EditorWindow.open_module, others, belong here.
def __init__(self, editwin):
self.editwin = editwin
self.text = editwin.text
self.__id_open = self.text.bind("<<open-window-from-file>>", self.open)
self.__id_save = self.text.bind("<<save-window>>", self.save)
self.__id_saveas = self.text.bind("<<save-window-as-file>>",
self.save_as)
self.__id_savecopy = self.text.bind("<<save-copy-of-window-as-file>>",
self.save_a_copy)
self.fileencoding = 'utf-8'
self.__id_print = self.text.bind("<<print-window>>", self.print_window)
def close(self):
# Undo command bindings
self.text.unbind("<<open-window-from-file>>", self.__id_open)
self.text.unbind("<<save-window>>", self.__id_save)
self.text.unbind("<<save-window-as-file>>",self.__id_saveas)
self.text.unbind("<<save-copy-of-window-as-file>>", self.__id_savecopy)
self.text.unbind("<<print-window>>", self.__id_print)
# Break cycles
self.editwin = None
self.text = None
self.filename_change_hook = None
def get_saved(self):
return self.editwin.get_saved()
def set_saved(self, flag):
self.editwin.set_saved(flag)
def reset_undo(self):
self.editwin.reset_undo()
filename_change_hook = None
def set_filename_change_hook(self, hook):
self.filename_change_hook = hook
filename = None
dirname = None
def set_filename(self, filename):
if filename and os.path.isdir(filename):
self.filename = None
self.dirname = filename
else:
self.filename = filename
self.dirname = None
self.set_saved(1)
if self.filename_change_hook:
self.filename_change_hook()
def open(self, event=None, editFile=None):
flist = self.editwin.flist
# Save in case parent window is closed (ie, during askopenfile()).
if flist:
if not editFile:
filename = self.askopenfile()
else:
filename=editFile
if filename:
# If editFile is valid and already open, flist.open will
# shift focus to its existing window.
# If the current window exists and is a fresh unnamed,
# unmodified editor window (not an interpreter shell),
# pass self.loadfile to flist.open so it will load the file
# in the current window (if the file is not already open)
# instead of a new window.
if (self.editwin and
not getattr(self.editwin, 'interp', None) and
not self.filename and
self.get_saved()):
flist.open(filename, self.loadfile)
else:
flist.open(filename)
else:
if self.text:
self.text.focus_set()
return "break"
# Code for use outside IDLE:
if self.get_saved():
reply = self.maybesave()
if reply == "cancel":
self.text.focus_set()
return "break"
if not editFile:
filename = self.askopenfile()
else:
filename=editFile
if filename:
self.loadfile(filename)
else:
self.text.focus_set()
return "break"
eol_convention = os.linesep # default
def loadfile(self, filename):
try:
try:
with tokenize.open(filename) as f:
chars = f.read()
fileencoding = f.encoding
eol_convention = f.newlines
converted = False
except (UnicodeDecodeError, SyntaxError):
# Wait for the editor window to appear
self.editwin.text.update()
enc = askstring(
"Specify file encoding",
"The file's encoding is invalid for Python 3.x.\n"
"IDLE will convert it to UTF-8.\n"
"What is the current encoding of the file?",
initialvalue='utf-8',
parent=self.editwin.text)
with open(filename, encoding=enc) as f:
chars = f.read()
fileencoding = f.encoding
eol_convention = f.newlines
converted = True
except OSError as err:
messagebox.showerror("I/O Error", str(err), parent=self.text)
return False
except UnicodeDecodeError:
messagebox.showerror("Decoding Error",
"File %s\nFailed to Decode" % filename,
parent=self.text)
return False
if not isinstance(eol_convention, str):
# If the file does not contain line separators, it is None.
# If the file contains mixed line separators, it is a tuple.
if eol_convention is not None:
messagebox.showwarning("Mixed Newlines",
"Mixed newlines detected.\n"
"The file will be changed on save.",
parent=self.text)
converted = True
eol_convention = os.linesep # default
self.text.delete("1.0", "end")
self.set_filename(None)
self.fileencoding = fileencoding
self.eol_convention = eol_convention
self.text.insert("1.0", chars)
self.reset_undo()
self.set_filename(filename)
if converted:
# We need to save the conversion results first
# before being able to execute the code
self.set_saved(False)
self.text.mark_set("insert", "1.0")
self.text.yview("insert")
self.updaterecentfileslist(filename)
return True
def maybesave(self):
"""Return 'yes', 'no', 'cancel' as appropriate.
Tkinter messagebox.askyesnocancel converts these tk responses
to True, False, None. Convert back, as now expected elsewhere.
"""
if self.get_saved():
return "yes"
message = ("Do you want to save "
f"{self.filename or 'this untitled document'}"
" before closing?")
confirm = messagebox.askyesnocancel(
title="Save On Close",
message=message,
default=messagebox.YES,
parent=self.text)
if confirm:
self.save(None)
reply = "yes" if self.get_saved() else "cancel"
else: reply = "cancel" if confirm is None else "no"
self.text.focus_set()
return reply
def save(self, event):
if not self.filename:
self.save_as(event)
else:
if self.writefile(self.filename):
self.set_saved(True)
try:
self.editwin.store_file_breaks()
except AttributeError: # may be a PyShell
pass
self.text.focus_set()
return "break"
def save_as(self, event):
filename = self.asksavefile()
if filename:
if self.writefile(filename):
self.set_filename(filename)
self.set_saved(1)
try:
self.editwin.store_file_breaks()
except AttributeError:
pass
self.text.focus_set()
self.updaterecentfileslist(filename)
return "break"
def save_a_copy(self, event):
filename = self.asksavefile()
if filename:
self.writefile(filename)
self.text.focus_set()
self.updaterecentfileslist(filename)
return "break"
def writefile(self, filename):
text = self.fixnewlines()
chars = self.encode(text)
try:
with open(filename, "wb") as f:
f.write(chars)
f.flush()
os.fsync(f.fileno())
return True
except OSError as msg:
messagebox.showerror("I/O Error", str(msg),
parent=self.text)
return False
def fixnewlines(self):
"""Return text with os eols.
Add prompts if shell else final \n if missing.
"""
if hasattr(self.editwin, "interp"): # Saving shell.
text = self.editwin.get_prompt_text('1.0', self.text.index('end-1c'))
else:
if self.text.get("end-2c") != '\n':
self.text.insert("end-1c", "\n") # Changes 'end-1c' value.
text = self.text.get('1.0', "end-1c")
if self.eol_convention != "\n":
text = text.replace("\n", self.eol_convention)
return text
def encode(self, chars):
if isinstance(chars, bytes):
# This is either plain ASCII, or Tk was returning mixed-encoding
# text to us. Don't try to guess further.
return chars
# Preserve a BOM that might have been present on opening
if self.fileencoding == 'utf-8-sig':
return chars.encode('utf-8-sig')
# See whether there is anything non-ASCII in it.
# If not, no need to figure out the encoding.
try:
return chars.encode('ascii')
except UnicodeEncodeError:
pass
# Check if there is an encoding declared
try:
encoded = chars.encode('ascii', 'replace')
enc, _ = tokenize.detect_encoding(io.BytesIO(encoded).readline)
return chars.encode(enc)
except SyntaxError as err:
failed = str(err)
except UnicodeEncodeError:
failed = "Invalid encoding '%s'" % enc
messagebox.showerror(
"I/O Error",
"%s.\nSaving as UTF-8" % failed,
parent=self.text)
# Fallback: save as UTF-8, with BOM - ignoring the incorrect
# declared encoding
return chars.encode('utf-8-sig')
def print_window(self, event):
confirm = messagebox.askokcancel(
title="Print",
message="Print to Default Printer",
default=messagebox.OK,
parent=self.text)
if not confirm:
self.text.focus_set()
return "break"
tempfilename = None
saved = self.get_saved()
if saved:
filename = self.filename
# shell undo is reset after every prompt, looks saved, probably isn't
if not saved or filename is None:
(tfd, tempfilename) = tempfile.mkstemp(prefix='IDLE_tmp_')
filename = tempfilename
os.close(tfd)
if not self.writefile(tempfilename):
os.unlink(tempfilename)
return "break"
platform = os.name
printPlatform = True
if platform == 'posix': #posix platform
command = idleConf.GetOption('main','General',
'print-command-posix')
command = command + " 2>&1"
elif platform == 'nt': #win32 platform
command = idleConf.GetOption('main','General','print-command-win')
else: #no printing for this platform
printPlatform = False
if printPlatform: #we can try to print for this platform
command = command % shlex.quote(filename)
pipe = os.popen(command, "r")
# things can get ugly on NT if there is no printer available.
output = pipe.read().strip()
status = pipe.close()
if status:
output = "Printing failed (exit status 0x%x)\n" % \
status + output
if output:
output = "Printing command: %s\n" % repr(command) + output
messagebox.showerror("Print status", output, parent=self.text)
else: #no printing for this platform
message = "Printing is not enabled for this platform: %s" % platform
messagebox.showinfo("Print status", message, parent=self.text)
if tempfilename:
os.unlink(tempfilename)
return "break"
opendialog = None
savedialog = None
filetypes = (
("Python files", py_extensions, "TEXT"),
("Text files", "*.txt", "TEXT"),
("All files", "*"),
)
defaultextension = '.py' if sys.platform == 'darwin' else ''
def askopenfile(self):
dir, base = self.defaultfilename("open")
if not self.opendialog:
self.opendialog = filedialog.Open(parent=self.text,
filetypes=self.filetypes)
filename = self.opendialog.show(initialdir=dir, initialfile=base)
return filename
def defaultfilename(self, mode="open"):
if self.filename:
return os.path.split(self.filename)
elif self.dirname:
return self.dirname, ""
else:
try:
pwd = os.getcwd()
except OSError:
pwd = ""
return pwd, ""
def asksavefile(self):
dir, base = self.defaultfilename("save")
if not self.savedialog:
self.savedialog = filedialog.SaveAs(
parent=self.text,
filetypes=self.filetypes,
defaultextension=self.defaultextension)
filename = self.savedialog.show(initialdir=dir, initialfile=base)
return filename
def updaterecentfileslist(self,filename):
"Update recent file list on all editor windows"
if self.editwin.flist:
self.editwin.update_recent_files_list(filename)
def _io_binding(parent): # htest #
from tkinter import Toplevel, Text
top = Toplevel(parent)
top.title("Test IOBinding")
x, y = map(int, parent.geometry().split('+')[1:])
top.geometry("+%d+%d" % (x, y + 175))
class MyEditWin:
def __init__(self, text):
self.text = text
self.flist = None
self.text.bind("<Control-o>", self.open)
self.text.bind('<Control-p>', self.print)
self.text.bind("<Control-s>", self.save)
self.text.bind("<Alt-s>", self.saveas)
self.text.bind('<Control-c>', self.savecopy)
def get_saved(self): return 0
def set_saved(self, flag): pass
def reset_undo(self): pass
def open(self, event):
self.text.event_generate("<<open-window-from-file>>")
def print(self, event):
self.text.event_generate("<<print-window>>")
def save(self, event):
self.text.event_generate("<<save-window>>")
def saveas(self, event):
self.text.event_generate("<<save-window-as-file>>")
def savecopy(self, event):
self.text.event_generate("<<save-copy-of-window-as-file>>")
text = Text(top)
text.pack()
text.focus_set()
editwin = MyEditWin(text)
IOBinding(editwin)
if __name__ == "__main__":
from unittest import main
main('idlelib.idle_test.test_iomenu', verbosity=2, exit=False)
from idlelib.idle_test.htest import run
run(_io_binding)
@@ -0,0 +1,278 @@
"""
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
## Define functions that query the Mac graphics type.
## _tk_type and its initializer are private to this section.
_tk_type = None
def _init_tk_type():
""" Initialize _tk_type for isXyzTk functions.
This function is only called once, when _tk_type is still None.
"""
global _tk_type
if platform == 'darwin':
# When running IDLE, GUI is present, test/* may not be.
# When running tests, test/* is present, GUI may not be.
# If not, guess most common. Does not matter for testing.
from idlelib.__init__ import testing
if testing:
from test.support import requires, ResourceDenied
try:
requires('gui')
except ResourceDenied:
_tk_type = "cocoa"
return
root = tkinter.Tk()
ws = root.tk.call('tk', 'windowingsystem')
if 'x11' in ws:
_tk_type = "xquartz"
elif 'aqua' not in ws:
_tk_type = "other"
elif 'AppKit' in root.tk.call('winfo', 'server', '.'):
_tk_type = "cocoa"
else:
_tk_type = "carbon"
root.destroy()
else:
_tk_type = "other"
return
def isAquaTk():
"""
Returns True if IDLE is using a native OS X Tk (Cocoa or Carbon).
"""
if not _tk_type:
_init_tk_type()
return _tk_type == "cocoa" or _tk_type == "carbon"
def isCarbonTk():
"""
Returns True if IDLE is using a Carbon Aqua Tk (instead of the
newer Cocoa Aqua Tk).
"""
if not _tk_type:
_init_tk_type()
return _tk_type == "carbon"
def isCocoaTk():
"""
Returns True if IDLE is using a Cocoa Aqua Tk.
"""
if not _tk_type:
_init_tk_type()
return _tk_type == "cocoa"
def isXQuartz():
"""
Returns True if IDLE is using an OS X X11 Tk.
"""
if not _tk_type:
_init_tk_type()
return _tk_type == "xquartz"
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):
"""
This ensures that the application will respond to open AppleEvents, which
makes is feasible to use IDLE as the default application for python files.
"""
def doOpenFile(*args):
for fn in args:
flist.open(fn)
# The command below is a hook in aquatk that is called whenever the app
# receives a file open event. The callback can have multiple arguments,
# one for every file that should be opened.
root.createcommand("::tk::mac::OpenDocument", doOpenFile)
def hideTkConsole(root):
try:
root.tk.call('console', 'hide')
except tkinter.TclError:
# Some versions of the Tk framework don't have a console object
pass
def overrideRootMenu(root, flist):
"""
Replace the Tk root menu by something that is more appropriate for
IDLE with an Aqua Tk.
"""
# The menu that is attached to the Tk root (".") is also used by AquaTk for
# all windows that don't specify a menu of their own. The default menubar
# contains a number of menus, none of which are appropriate for IDLE. The
# Most annoying of those is an 'About Tck/Tk...' menu in the application
# menu.
#
# This function replaces the default menubar by a mostly empty one, it
# should only contain the correct application menu and the window menu.
#
# Due to a (mis-)feature of TkAqua the user will also see an empty Help
# menu.
from tkinter import Menu
from idlelib import mainmenu
from idlelib import window
closeItem = mainmenu.menudefs[0][1][-2]
# Remove the last 3 items of the file menu: a separator, close window and
# quit. Close window will be reinserted just above the save item, where
# it should be according to the HIG. Quit is in the application menu.
del mainmenu.menudefs[0][1][-3:]
mainmenu.menudefs[0][1].insert(6, closeItem)
# Remove the 'About' entry from the help menu, it is in the application
# menu
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[-3][1][0:2]
menubar = Menu(root)
root.configure(menu=menubar)
menu = Menu(menubar, name='window', tearoff=0)
menubar.add_cascade(label='Window', menu=menu, underline=0)
def postwindowsmenu(menu=menu):
end = menu.index('end')
if end is None:
end = -1
if end > 0:
menu.delete(0, end)
window.add_windows_to_menu(menu)
window.register_callback(postwindowsmenu)
def about_dialog(event=None):
"Handle Help 'About IDLE' event."
# Synchronize with editor.EditorWindow.about_dialog.
from idlelib import help_about
help_about.AboutDialog(root)
def config_dialog(event=None):
"Handle Options 'Configure IDLE' event."
# Synchronize with editor.EditorWindow.config_dialog.
from idlelib import configdialog
# Ensure that the root object has an instance_dict attribute,
# mirrors code in EditorWindow (although that sets the attribute
# on an EditorWindow instance that is then passed as the first
# argument to ConfigDialog)
root.instance_dict = flist.inversedict
configdialog.ConfigDialog(root, 'Settings')
def help_dialog(event=None):
"Handle Help 'IDLE Help' event."
# Synchronize with editor.EditorWindow.help_dialog.
from idlelib import help
help.show_idlehelp(root)
root.bind('<<about-idle>>', about_dialog)
root.bind('<<open-config-dialog>>', config_dialog)
root.createcommand('::tk::mac::ShowPreferences', config_dialog)
if flist:
root.bind('<<close-all-windows>>', flist.close_all_callback)
# The binding above doesn't reliably work on all versions of Tk
# on macOS. Adding command definition below does seem to do the
# right thing for now.
root.createcommand('::tk::mac::Quit', flist.close_all_callback)
if isCarbonTk():
# for Carbon AquaTk, replace the default Tk apple menu
menu = Menu(menubar, name='apple', tearoff=0)
menubar.add_cascade(label='IDLE', menu=menu)
mainmenu.menudefs.insert(0,
('application', [
('About IDLE', '<<about-idle>>'),
None,
]))
if isCocoaTk():
# replace default About dialog with About IDLE one
root.createcommand('tkAboutDialog', about_dialog)
# replace default "Help" item in Help menu
root.createcommand('::tk::mac::ShowHelp', help_dialog)
# remove redundant "IDLE Help" from menu
del mainmenu.menudefs[-1][1][0]
def fixb2context(root):
'''Removed bad AquaTk Button-2 (right) and Paste bindings.
They prevent context menu access and seem to be gone in AquaTk8.6.
See issue #24801.
'''
root.unbind_class('Text', '<B2>')
root.unbind_class('Text', '<B2-Motion>')
root.unbind_class('Text', '<<PasteSelection>>')
def setupApp(root, flist):
"""
Perform initial OS X customizations if needed.
Called from pyshell.main() after initial calls to Tk()
There are currently three major versions of Tk in use on OS X:
1. Aqua Cocoa Tk (native default since OS X 10.6)
2. Aqua Carbon Tk (original native, 32-bit only, deprecated)
3. X11 (supported by some third-party distributors, deprecated)
There are various differences among the three that affect IDLE
behavior, primarily with menus, mouse key events, and accelerators.
Some one-time customizations are performed here.
Others are dynamically tested throughout idlelib by calls to the
isAquaTk(), isCarbonTk(), isCocoaTk(), isXQuartz() functions which
are initialized here as well.
"""
if isAquaTk():
hideTkConsole(root)
overrideRootMenu(root, flist)
addOpenEventSupport(root, flist)
fixb2context(root)
if __name__ == '__main__':
from unittest import main
main('idlelib.idle_test.test_macosx', verbosity=2)
@@ -0,0 +1,126 @@
"""Define the menu contents, hotkeys, and event bindings.
There is additional configuration information in the EditorWindow class (and
subclasses): the menus are created there based on the menu_specs (class)
variable, and menus not created are silently skipped in the code here. This
makes it possible, for example, to define a Debug menu which is only present in
the PythonShell window, and a Format menu which is only present in the Editor
windows.
"""
from importlib.util import find_spec
from idlelib.config import idleConf
# Warning: menudefs is altered in macosx.overrideRootMenu()
# after it is determined that an OS X Aqua Tk is in use,
# which cannot be done until after Tk() is first called.
# Do not alter the 'file', 'options', or 'help' cascades here
# without altering overrideRootMenu() as well.
# TODO: Make this more robust
menudefs = [
# underscore prefixes character to underscore
('file', [
('_New File', '<<open-new-window>>'),
('_Open...', '<<open-window-from-file>>'),
('Open _Module...', '<<open-module>>'),
('Module _Browser', '<<open-class-browser>>'),
('_Path Browser', '<<open-path-browser>>'),
None,
('_Save', '<<save-window>>'),
('Save _As...', '<<save-window-as-file>>'),
('Save Cop_y As...', '<<save-copy-of-window-as-file>>'),
None,
('Prin_t Window', '<<print-window>>'),
None,
('_Close Window', '<<close-window>>'),
('E_xit IDLE', '<<close-all-windows>>'),
]),
('edit', [
('_Undo', '<<undo>>'),
('_Redo', '<<redo>>'),
None,
('Select _All', '<<select-all>>'),
('Cu_t', '<<cut>>'),
('_Copy', '<<copy>>'),
('_Paste', '<<paste>>'),
None,
('_Find...', '<<find>>'),
('Find A_gain', '<<find-again>>'),
('Find _Selection', '<<find-selection>>'),
('Find in Files...', '<<find-in-files>>'),
('R_eplace...', '<<replace>>'),
None,
('Go to _Line', '<<goto-line>>'),
('S_how Completions', '<<force-open-completions>>'),
('E_xpand Word', '<<expand-word>>'),
('Show C_all Tip', '<<force-open-calltip>>'),
('Show Surrounding P_arens', '<<flash-paren>>'),
]),
('format', [
('F_ormat Paragraph', '<<format-paragraph>>'),
('_Indent Region', '<<indent-region>>'),
('_Dedent Region', '<<dedent-region>>'),
('Comment _Out Region', '<<comment-region>>'),
('U_ncomment Region', '<<uncomment-region>>'),
('Tabify Region', '<<tabify-region>>'),
('Untabify Region', '<<untabify-region>>'),
('Toggle Tabs', '<<toggle-tabs>>'),
('New Indent Width', '<<change-indentwidth>>'),
('S_trip Trailing Whitespace', '<<do-rstrip>>'),
]),
('run', [
('R_un Module', '<<run-module>>'),
('Run... _Customized', '<<run-custom>>'),
('C_heck Module', '<<check-module>>'),
('Python Shell', '<<open-python-shell>>'),
]),
('shell', [
('_View Last Restart', '<<view-restart>>'),
('_Restart Shell', '<<restart-shell>>'),
None,
('_Previous History', '<<history-previous>>'),
('_Next History', '<<history-next>>'),
None,
('_Interrupt Execution', '<<interrupt-execution>>'),
]),
('debug', [
('_Go to File/Line', '<<goto-file-line>>'),
('!_Debugger', '<<toggle-debugger>>'),
('_Stack Viewer', '<<open-stack-viewer>>'),
('!_Auto-open Stack Viewer', '<<toggle-jit-stack-viewer>>'),
]),
('options', [
('Configure _IDLE', '<<open-config-dialog>>'),
None,
('Show _Code Context', '<<toggle-code-context>>'),
('Show _Line Numbers', '<<toggle-line-numbers>>'),
('_Zoom Height', '<<zoom-height>>'),
]),
('window', [
]),
('help', [
('_About IDLE', '<<about-idle>>'),
None,
('_IDLE Doc', '<<help>>'),
('Python _Docs', '<<python-docs>>'),
]),
]
if find_spec('turtledemo'):
menudefs[-1][1].append(('Turtle Demo', '<<open-turtle-demo>>'))
default_keydefs = idleConf.GetCurrentKeySet()
if __name__ == '__main__':
from unittest import main
main('idlelib.idle_test.test_mainmenu', verbosity=2)
@@ -0,0 +1,451 @@
"""
MultiCall - a class which inherits its methods from a Tkinter widget (Text, for
example), but enables multiple calls of functions per virtual event - all
matching events will be called, not only the most specific one. This is done
by wrapping the event functions - event_add, event_delete and event_info.
MultiCall recognizes only a subset of legal event sequences. Sequences which
are not recognized are treated by the original Tk handling mechanism. A
more-specific event will be called before a less-specific event.
The recognized sequences are complete one-event sequences (no emacs-style
Ctrl-X Ctrl-C, no shortcuts like <3>), for all types of events.
Key/Button Press/Release events can have modifiers.
The recognized modifiers are Shift, Control, Option and Command for Mac, and
Control, Alt, Shift, Meta/M for other platforms.
For all events which were handled by MultiCall, a new member is added to the
event instance passed to the binded functions - mc_type. This is one of the
event type constants defined in this module (such as MC_KEYPRESS).
For Key/Button events (which are handled by MultiCall and may receive
modifiers), another member is added - mc_state. This member gives the state
of the recognized modifiers, as a combination of the modifier constants
also defined in this module (for example, MC_SHIFT).
Using these members is absolutely portable.
The order by which events are called is defined by these rules:
1. A more-specific event will be called before a less-specific event.
2. A recently-binded event will be called before a previously-binded event,
unless this conflicts with the first rule.
Each function will be called at most once for each event.
"""
import re
import sys
import tkinter
# the event type constants, which define the meaning of mc_type
MC_KEYPRESS=0; MC_KEYRELEASE=1; MC_BUTTONPRESS=2; MC_BUTTONRELEASE=3;
MC_ACTIVATE=4; MC_CIRCULATE=5; MC_COLORMAP=6; MC_CONFIGURE=7;
MC_DEACTIVATE=8; MC_DESTROY=9; MC_ENTER=10; MC_EXPOSE=11; MC_FOCUSIN=12;
MC_FOCUSOUT=13; MC_GRAVITY=14; MC_LEAVE=15; MC_MAP=16; MC_MOTION=17;
MC_MOUSEWHEEL=18; MC_PROPERTY=19; MC_REPARENT=20; MC_UNMAP=21; MC_VISIBILITY=22;
# the modifier state constants, which define the meaning of mc_state
MC_SHIFT = 1<<0; MC_CONTROL = 1<<2; MC_ALT = 1<<3; MC_META = 1<<5
MC_OPTION = 1<<6; MC_COMMAND = 1<<7
# define the list of modifiers, to be used in complex event types.
if sys.platform == "darwin":
_modifiers = (("Shift",), ("Control",), ("Option",), ("Command",))
_modifier_masks = (MC_SHIFT, MC_CONTROL, MC_OPTION, MC_COMMAND)
else:
_modifiers = (("Control",), ("Alt",), ("Shift",), ("Meta", "M"))
_modifier_masks = (MC_CONTROL, MC_ALT, MC_SHIFT, MC_META)
# a dictionary to map a modifier name into its number
_modifier_names = {name: number
for number in range(len(_modifiers))
for name in _modifiers[number]}
# In 3.4, if no shell window is ever open, the underlying Tk widget is
# destroyed before .__del__ methods here are called. The following
# is used to selectively ignore shutdown exceptions to avoid
# 'Exception ignored' messages. See http://bugs.python.org/issue20167
APPLICATION_GONE = "application has been destroyed"
# A binder is a class which binds functions to one type of event. It has two
# methods: bind and unbind, which get a function and a parsed sequence, as
# returned by _parse_sequence(). There are two types of binders:
# _SimpleBinder handles event types with no modifiers and no detail.
# No Python functions are called when no events are binded.
# _ComplexBinder handles event types with modifiers and a detail.
# A Python function is called each time an event is generated.
class _SimpleBinder:
def __init__(self, type, widget, widgetinst):
self.type = type
self.sequence = '<'+_types[type][0]+'>'
self.widget = widget
self.widgetinst = widgetinst
self.bindedfuncs = []
self.handlerid = None
def bind(self, triplet, func):
if not self.handlerid:
def handler(event, l = self.bindedfuncs, mc_type = self.type):
event.mc_type = mc_type
wascalled = {}
for i in range(len(l)-1, -1, -1):
func = l[i]
if func not in wascalled:
wascalled[func] = True
r = func(event)
if r:
return r
self.handlerid = self.widget.bind(self.widgetinst,
self.sequence, handler)
self.bindedfuncs.append(func)
def unbind(self, triplet, func):
self.bindedfuncs.remove(func)
if not self.bindedfuncs:
self.widget.unbind(self.widgetinst, self.sequence, self.handlerid)
self.handlerid = None
def __del__(self):
if self.handlerid:
try:
self.widget.unbind(self.widgetinst, self.sequence,
self.handlerid)
except tkinter.TclError as e:
if not APPLICATION_GONE in e.args[0]:
raise
# An int in range(1 << len(_modifiers)) represents a combination of modifiers
# (if the least significant bit is on, _modifiers[0] is on, and so on).
# _state_subsets gives for each combination of modifiers, or *state*,
# a list of the states which are a subset of it. This list is ordered by the
# number of modifiers is the state - the most specific state comes first.
_states = range(1 << len(_modifiers))
_state_names = [''.join(m[0]+'-'
for i, m in enumerate(_modifiers)
if (1 << i) & s)
for s in _states]
def expand_substates(states):
'''For each item of states return a list containing all combinations of
that item with individual bits reset, sorted by the number of set bits.
'''
def nbits(n):
"number of bits set in n base 2"
nb = 0
while n:
n, rem = divmod(n, 2)
nb += rem
return nb
statelist = []
for state in states:
substates = list({state & x for x in states})
substates.sort(key=nbits, reverse=True)
statelist.append(substates)
return statelist
_state_subsets = expand_substates(_states)
# _state_codes gives for each state, the portable code to be passed as mc_state
_state_codes = []
for s in _states:
r = 0
for i in range(len(_modifiers)):
if (1 << i) & s:
r |= _modifier_masks[i]
_state_codes.append(r)
class _ComplexBinder:
# This class binds many functions, and only unbinds them when it is deleted.
# self.handlerids is the list of seqs and ids of binded handler functions.
# The binded functions sit in a dictionary of lists of lists, which maps
# a detail (or None) and a state into a list of functions.
# When a new detail is discovered, handlers for all the possible states
# are binded.
def __create_handler(self, lists, mc_type, mc_state):
def handler(event, lists = lists,
mc_type = mc_type, mc_state = mc_state,
ishandlerrunning = self.ishandlerrunning,
doafterhandler = self.doafterhandler):
ishandlerrunning[:] = [True]
event.mc_type = mc_type
event.mc_state = mc_state
wascalled = {}
r = None
for l in lists:
for i in range(len(l)-1, -1, -1):
func = l[i]
if func not in wascalled:
wascalled[func] = True
r = l[i](event)
if r:
break
if r:
break
ishandlerrunning[:] = []
# Call all functions in doafterhandler and remove them from list
for f in doafterhandler:
f()
doafterhandler[:] = []
if r:
return r
return handler
def __init__(self, type, widget, widgetinst):
self.type = type
self.typename = _types[type][0]
self.widget = widget
self.widgetinst = widgetinst
self.bindedfuncs = {None: [[] for s in _states]}
self.handlerids = []
# we don't want to change the lists of functions while a handler is
# running - it will mess up the loop and anyway, we usually want the
# change to happen from the next event. So we have a list of functions
# for the handler to run after it finishes calling the binded functions.
# It calls them only once.
# ishandlerrunning is a list. An empty one means no, otherwise - yes.
# this is done so that it would be mutable.
self.ishandlerrunning = []
self.doafterhandler = []
for s in _states:
lists = [self.bindedfuncs[None][i] for i in _state_subsets[s]]
handler = self.__create_handler(lists, type, _state_codes[s])
seq = '<'+_state_names[s]+self.typename+'>'
self.handlerids.append((seq, self.widget.bind(self.widgetinst,
seq, handler)))
def bind(self, triplet, func):
if triplet[2] not in self.bindedfuncs:
self.bindedfuncs[triplet[2]] = [[] for s in _states]
for s in _states:
lists = [ self.bindedfuncs[detail][i]
for detail in (triplet[2], None)
for i in _state_subsets[s] ]
handler = self.__create_handler(lists, self.type,
_state_codes[s])
seq = "<%s%s-%s>"% (_state_names[s], self.typename, triplet[2])
self.handlerids.append((seq, self.widget.bind(self.widgetinst,
seq, handler)))
doit = lambda: self.bindedfuncs[triplet[2]][triplet[0]].append(func)
if not self.ishandlerrunning:
doit()
else:
self.doafterhandler.append(doit)
def unbind(self, triplet, func):
doit = lambda: self.bindedfuncs[triplet[2]][triplet[0]].remove(func)
if not self.ishandlerrunning:
doit()
else:
self.doafterhandler.append(doit)
def __del__(self):
for seq, id in self.handlerids:
try:
self.widget.unbind(self.widgetinst, seq, id)
except tkinter.TclError as e:
if not APPLICATION_GONE in e.args[0]:
raise
# define the list of event types to be handled by MultiEvent. the order is
# compatible with the definition of event type constants.
_types = (
("KeyPress", "Key"), ("KeyRelease",), ("ButtonPress", "Button"),
("ButtonRelease",), ("Activate",), ("Circulate",), ("Colormap",),
("Configure",), ("Deactivate",), ("Destroy",), ("Enter",), ("Expose",),
("FocusIn",), ("FocusOut",), ("Gravity",), ("Leave",), ("Map",),
("Motion",), ("MouseWheel",), ("Property",), ("Reparent",), ("Unmap",),
("Visibility",),
)
# which binder should be used for every event type?
_binder_classes = (_ComplexBinder,) * 4 + (_SimpleBinder,) * (len(_types)-4)
# A dictionary to map a type name into its number
_type_names = {name: number
for number in range(len(_types))
for name in _types[number]}
_keysym_re = re.compile(r"^\w+$")
_button_re = re.compile(r"^[1-5]$")
def _parse_sequence(sequence):
"""Get a string which should describe an event sequence. If it is
successfully parsed as one, return a tuple containing the state (as an int),
the event type (as an index of _types), and the detail - None if none, or a
string if there is one. If the parsing is unsuccessful, return None.
"""
if not sequence or sequence[0] != '<' or sequence[-1] != '>':
return None
words = sequence[1:-1].split('-')
modifiers = 0
while words and words[0] in _modifier_names:
modifiers |= 1 << _modifier_names[words[0]]
del words[0]
if words and words[0] in _type_names:
type = _type_names[words[0]]
del words[0]
else:
return None
if _binder_classes[type] is _SimpleBinder:
if modifiers or words:
return None
else:
detail = None
else:
# _ComplexBinder
if type in [_type_names[s] for s in ("KeyPress", "KeyRelease")]:
type_re = _keysym_re
else:
type_re = _button_re
if not words:
detail = None
elif len(words) == 1 and type_re.match(words[0]):
detail = words[0]
else:
return None
return modifiers, type, detail
def _triplet_to_sequence(triplet):
if triplet[2]:
return '<'+_state_names[triplet[0]]+_types[triplet[1]][0]+'-'+ \
triplet[2]+'>'
else:
return '<'+_state_names[triplet[0]]+_types[triplet[1]][0]+'>'
_multicall_dict = {}
def MultiCallCreator(widget):
"""Return a MultiCall class which inherits its methods from the
given widget class (for example, Tkinter.Text). This is used
instead of a templating mechanism.
"""
if widget in _multicall_dict:
return _multicall_dict[widget]
class MultiCall (widget):
assert issubclass(widget, tkinter.Misc)
def __init__(self, *args, **kwargs):
widget.__init__(self, *args, **kwargs)
# a dictionary which maps a virtual event to a tuple with:
# 0. the function binded
# 1. a list of triplets - the sequences it is binded to
self.__eventinfo = {}
self.__binders = [_binder_classes[i](i, widget, self)
for i in range(len(_types))]
def bind(self, sequence=None, func=None, add=None):
#print("bind(%s, %s, %s)" % (sequence, func, add),
# file=sys.__stderr__)
if type(sequence) is str and len(sequence) > 2 and \
sequence[:2] == "<<" and sequence[-2:] == ">>":
if sequence in self.__eventinfo:
ei = self.__eventinfo[sequence]
if ei[0] is not None:
for triplet in ei[1]:
self.__binders[triplet[1]].unbind(triplet, ei[0])
ei[0] = func
if ei[0] is not None:
for triplet in ei[1]:
self.__binders[triplet[1]].bind(triplet, func)
else:
self.__eventinfo[sequence] = [func, []]
return widget.bind(self, sequence, func, add)
def unbind(self, sequence, funcid=None):
if type(sequence) is str and len(sequence) > 2 and \
sequence[:2] == "<<" and sequence[-2:] == ">>" and \
sequence in self.__eventinfo:
func, triplets = self.__eventinfo[sequence]
if func is not None:
for triplet in triplets:
self.__binders[triplet[1]].unbind(triplet, func)
self.__eventinfo[sequence][0] = None
return widget.unbind(self, sequence, funcid)
def event_add(self, virtual, *sequences):
#print("event_add(%s, %s)" % (repr(virtual), repr(sequences)),
# file=sys.__stderr__)
if virtual not in self.__eventinfo:
self.__eventinfo[virtual] = [None, []]
func, triplets = self.__eventinfo[virtual]
for seq in sequences:
triplet = _parse_sequence(seq)
if triplet is None:
#print("Tkinter event_add(%s)" % seq, file=sys.__stderr__)
widget.event_add(self, virtual, seq)
else:
if func is not None:
self.__binders[triplet[1]].bind(triplet, func)
triplets.append(triplet)
def event_delete(self, virtual, *sequences):
if virtual not in self.__eventinfo:
return
func, triplets = self.__eventinfo[virtual]
for seq in sequences:
triplet = _parse_sequence(seq)
if triplet is None:
#print("Tkinter event_delete: %s" % seq, file=sys.__stderr__)
widget.event_delete(self, virtual, seq)
else:
if func is not None:
self.__binders[triplet[1]].unbind(triplet, func)
triplets.remove(triplet)
def event_info(self, virtual=None):
if virtual is None or virtual not in self.__eventinfo:
return widget.event_info(self, virtual)
else:
return tuple(map(_triplet_to_sequence,
self.__eventinfo[virtual][1])) + \
widget.event_info(self, virtual)
def __del__(self):
for virtual in self.__eventinfo:
func, triplets = self.__eventinfo[virtual]
if func:
for triplet in triplets:
try:
self.__binders[triplet[1]].unbind(triplet, func)
except tkinter.TclError as e:
if not APPLICATION_GONE in e.args[0]:
raise
_multicall_dict[widget] = MultiCall
return MultiCall
def _multi_call(parent): # htest #
top = tkinter.Toplevel(parent)
top.title("Test MultiCall")
x, y = map(int, parent.geometry().split('+')[1:])
top.geometry("+%d+%d" % (x, y + 175))
text = MultiCallCreator(tkinter.Text)(top)
text.pack()
text.focus_set()
def bindseq(seq, n=[0]):
def handler(event):
print(seq)
text.bind("<<handler%d>>"%n[0], handler)
text.event_add("<<handler%d>>"%n[0], seq)
n[0] += 1
bindseq("<Key>")
bindseq("<Control-Key>")
bindseq("<Alt-Key-a>")
bindseq("<Control-Key-a>")
bindseq("<Alt-Control-Key-a>")
bindseq("<Key-b>")
bindseq("<Control-Button-1>")
bindseq("<Button-2>")
bindseq("<Alt-Button-1>")
bindseq("<FocusOut>")
bindseq("<Enter>")
bindseq("<Leave>")
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)
@@ -0,0 +1,188 @@
"""Editor window that can serve as an output file.
"""
import re
from tkinter import messagebox
from idlelib.editor import EditorWindow
file_line_pats = [
# order of patterns matters
r'file "([^"]*)", line (\d+)',
r'([^\s]+)\((\d+)\)',
r'^(\s*\S.*?):\s*(\d+):', # Win filename, maybe starting with spaces
r'([^\s]+):\s*(\d+):', # filename or path, ltrim
r'^\s*(\S.*?):\s*(\d+):', # Win abs path with embedded spaces, ltrim
]
file_line_progs = None
def compile_progs():
"Compile the patterns for matching to file name and line number."
global file_line_progs
file_line_progs = [re.compile(pat, re.IGNORECASE)
for pat in file_line_pats]
def file_line_helper(line):
"""Extract file name and line number from line of text.
Check if line of text contains one of the file/line patterns.
If it does and if the file and line are valid, return
a tuple of the file name and line number. If it doesn't match
or if the file or line is invalid, return None.
"""
if not file_line_progs:
compile_progs()
for prog in file_line_progs:
match = prog.search(line)
if match:
filename, lineno = match.group(1, 2)
try:
f = open(filename)
f.close()
break
except OSError:
continue
else:
return None
try:
return filename, int(lineno)
except TypeError:
return None
class OutputWindow(EditorWindow):
"""An editor window that can serve as an output file.
Also the future base class for the Python shell window.
This class has no input facilities.
Adds binding to open a file at a line to the text widget.
"""
# Our own right-button menu
rmenu_specs = [
("Cut", "<<cut>>", "rmenu_check_cut"),
("Copy", "<<copy>>", "rmenu_check_copy"),
("Paste", "<<paste>>", "rmenu_check_paste"),
(None, None, None),
("Go to file/line", "<<goto-file-line>>", None),
]
allow_code_context = False
def __init__(self, *args):
EditorWindow.__init__(self, *args)
self.text.bind("<<goto-file-line>>", self.goto_file_line)
# Customize EditorWindow
def ispythonsource(self, filename):
"Python source is only part of output: do not colorize."
return False
def short_title(self):
"Customize EditorWindow title."
return "Output"
def maybesave(self):
"Customize EditorWindow to not display save file messagebox."
return 'yes' if self.get_saved() else 'no'
# Act as output file
def write(self, s, tags=(), mark="insert"):
"""Write text to text widget.
The text is inserted at the given index with the provided
tags. The text widget is then scrolled to make it visible
and updated to display it, giving the effect of seeing each
line as it is added.
Args:
s: Text to insert into text widget.
tags: Tuple of tag strings to apply on the insert.
mark: Index for the insert.
Return:
Length of text inserted.
"""
assert isinstance(s, str)
self.text.insert(mark, s, tags)
self.text.see(mark)
self.text.update()
return len(s)
def writelines(self, lines):
"Write each item in lines iterable."
for line in lines:
self.write(line)
def flush(self):
"No flushing needed as write() directly writes to widget."
pass
def showerror(self, *args, **kwargs):
messagebox.showerror(*args, **kwargs)
def goto_file_line(self, event=None):
"""Handle request to open file/line.
If the selected or previous line in the output window
contains a file name and line number, then open that file
name in a new window and position on the line number.
Otherwise, display an error messagebox.
"""
line = self.text.get("insert linestart", "insert lineend")
result = file_line_helper(line)
if not result:
# Try the previous line. This is handy e.g. in tracebacks,
# where you tend to right-click on the displayed source line
line = self.text.get("insert -1line linestart",
"insert -1line lineend")
result = file_line_helper(line)
if not result:
self.showerror(
"No special line",
"The line you point at doesn't look like "
"a valid file name followed by a line number.",
parent=self.text)
return
filename, lineno = result
self.flist.gotofileline(filename, lineno)
# These classes are currently not used but might come in handy
class OnDemandOutputWindow:
tagdefs = {
# XXX Should use IdlePrefs.ColorPrefs
"stdout": {"foreground": "blue"},
"stderr": {"foreground": "#007700"},
}
def __init__(self, flist):
self.flist = flist
self.owin = None
def write(self, s, tags, mark):
if not self.owin:
self.setup()
self.owin.write(s, tags, mark)
def setup(self):
self.owin = owin = OutputWindow(self.flist)
text = owin.text
for tag, cnf in self.tagdefs.items():
if cnf:
text.tag_configure(tag, **cnf)
text.tag_raise('sel')
self.write = self.owin.write
if __name__ == '__main__':
from unittest import main
main('idlelib.idle_test.test_outwin', verbosity=2, exit=False)
@@ -0,0 +1,183 @@
"""ParenMatch -- for parenthesis matching.
When you hit a right paren, the cursor should move briefly to the left
paren. Paren here is used generically; the matching applies to
parentheses, square brackets, and curly braces.
"""
from idlelib.hyperparser import HyperParser
from idlelib.config import idleConf
_openers = {')':'(',']':'[','}':'{'}
CHECK_DELAY = 100 # milliseconds
class ParenMatch:
"""Highlight matching openers and closers, (), [], and {}.
There are three supported styles of paren matching. When a right
paren (opener) is typed:
opener -- highlight the matching left paren (closer);
parens -- highlight the left and right parens (opener and closer);
expression -- highlight the entire expression from opener to closer.
(For back compatibility, 'default' is a synonym for 'opener').
Flash-delay is the maximum milliseconds the highlighting remains.
Any cursor movement (key press or click) before that removes the
highlight. If flash-delay is 0, there is no maximum.
TODO:
- Augment bell() with mismatch warning in status window.
- Highlight when cursor is moved to the right of a closer.
This might be too expensive to check.
"""
RESTORE_VIRTUAL_EVENT_NAME = "<<parenmatch-check-restore>>"
# We want the restore event be called before the usual return and
# backspace events.
RESTORE_SEQUENCES = ("<KeyPress>", "<ButtonPress>",
"<Key-Return>", "<Key-BackSpace>")
def __init__(self, editwin):
self.editwin = editwin
self.text = editwin.text
# Bind the check-restore event to the function restore_event,
# so that we can then use activate_restore (which calls event_add)
# and deactivate_restore (which calls event_delete).
editwin.text.bind(self.RESTORE_VIRTUAL_EVENT_NAME,
self.restore_event)
self.counter = 0
self.is_restore_active = 0
@classmethod
def reload(cls):
cls.STYLE = idleConf.GetOption(
'extensions','ParenMatch','style', default='opener')
cls.FLASH_DELAY = idleConf.GetOption(
'extensions','ParenMatch','flash-delay', type='int',default=500)
cls.BELL = idleConf.GetOption(
'extensions','ParenMatch','bell', type='bool', default=1)
cls.HILITE_CONFIG = idleConf.GetHighlight(idleConf.CurrentTheme(),
'hilite')
def activate_restore(self):
"Activate mechanism to restore text from highlighting."
if not self.is_restore_active:
for seq in self.RESTORE_SEQUENCES:
self.text.event_add(self.RESTORE_VIRTUAL_EVENT_NAME, seq)
self.is_restore_active = True
def deactivate_restore(self):
"Remove restore event bindings."
if self.is_restore_active:
for seq in self.RESTORE_SEQUENCES:
self.text.event_delete(self.RESTORE_VIRTUAL_EVENT_NAME, seq)
self.is_restore_active = False
def flash_paren_event(self, event):
"Handle editor 'show surrounding parens' event (menu or shortcut)."
indices = (HyperParser(self.editwin, "insert")
.get_surrounding_brackets())
self.finish_paren_event(indices)
return "break"
def paren_closed_event(self, event):
"Handle user input of closer."
# If user bound non-closer to <<paren-closed>>, quit.
closer = self.text.get("insert-1c")
if closer not in _openers:
return
hp = HyperParser(self.editwin, "insert-1c")
if not hp.is_in_code():
return
indices = hp.get_surrounding_brackets(_openers[closer], True)
self.finish_paren_event(indices)
return # Allow calltips to see ')'
def finish_paren_event(self, indices):
if indices is None and self.BELL:
self.text.bell()
return
self.activate_restore()
# self.create_tag(indices)
self.tagfuncs.get(self.STYLE, self.create_tag_expression)(self, indices)
# self.set_timeout()
(self.set_timeout_last if self.FLASH_DELAY else
self.set_timeout_none)()
def restore_event(self, event=None):
"Remove effect of doing match."
self.text.tag_delete("paren")
self.deactivate_restore()
self.counter += 1 # disable the last timer, if there is one.
def handle_restore_timer(self, timer_count):
if timer_count == self.counter:
self.restore_event()
# any one of the create_tag_XXX methods can be used depending on
# the style
def create_tag_opener(self, indices):
"""Highlight the single paren that matches"""
self.text.tag_add("paren", indices[0])
self.text.tag_config("paren", self.HILITE_CONFIG)
def create_tag_parens(self, indices):
"""Highlight the left and right parens"""
if self.text.get(indices[1]) in (')', ']', '}'):
rightindex = indices[1]+"+1c"
else:
rightindex = indices[1]
self.text.tag_add("paren", indices[0], indices[0]+"+1c", rightindex+"-1c", rightindex)
self.text.tag_config("paren", self.HILITE_CONFIG)
def create_tag_expression(self, indices):
"""Highlight the entire expression"""
if self.text.get(indices[1]) in (')', ']', '}'):
rightindex = indices[1]+"+1c"
else:
rightindex = indices[1]
self.text.tag_add("paren", indices[0], rightindex)
self.text.tag_config("paren", self.HILITE_CONFIG)
tagfuncs = {
'opener': create_tag_opener,
'default': create_tag_opener,
'parens': create_tag_parens,
'expression': create_tag_expression,
}
# any one of the set_timeout_XXX methods can be used depending on
# the style
def set_timeout_none(self):
"""Highlight will remain until user input turns it off
or the insert has moved"""
# After CHECK_DELAY, call a function which disables the "paren" tag
# if the event is for the most recent timer and the insert has changed,
# or schedules another call for itself.
self.counter += 1
def callme(callme, self=self, c=self.counter,
index=self.text.index("insert")):
if index != self.text.index("insert"):
self.handle_restore_timer(c)
else:
self.editwin.text_frame.after(CHECK_DELAY, callme, callme)
self.editwin.text_frame.after(CHECK_DELAY, callme, callme)
def set_timeout_last(self):
"""The last highlight created will be removed after FLASH_DELAY millisecs"""
# associate a counter with an event; only disable the "paren"
# tag if the event is for the most recent timer.
self.counter += 1
self.editwin.text_frame.after(
self.FLASH_DELAY,
lambda self=self, c=self.counter: self.handle_restore_timer(c))
ParenMatch.reload()
if __name__ == '__main__':
from unittest import main
main('idlelib.idle_test.test_parenmatch', verbosity=2)
@@ -0,0 +1,107 @@
import importlib.machinery
import os
import sys
from idlelib.browser import ModuleBrowser, ModuleBrowserTreeItem
from idlelib.tree import TreeItem
class PathBrowser(ModuleBrowser):
def __init__(self, master, *, _htest=False, _utest=False):
"""
_htest - bool, change box location when running htest
"""
self.master = master
self._htest = _htest
self._utest = _utest
self.init()
def settitle(self):
"Set window titles."
self.top.wm_title("Path Browser")
self.top.wm_iconname("Path Browser")
def rootnode(self):
return PathBrowserTreeItem()
class PathBrowserTreeItem(TreeItem):
def GetText(self):
return "sys.path"
def GetSubList(self):
sublist = []
for dir in sys.path:
item = DirBrowserTreeItem(dir)
sublist.append(item)
return sublist
class DirBrowserTreeItem(TreeItem):
def __init__(self, dir, packages=[]):
self.dir = dir
self.packages = packages
def GetText(self):
if not self.packages:
return self.dir
else:
return self.packages[-1] + ": package"
def GetSubList(self):
try:
names = os.listdir(self.dir or os.curdir)
except OSError:
return []
packages = []
for name in names:
file = os.path.join(self.dir, name)
if self.ispackagedir(file):
nn = os.path.normcase(name)
packages.append((nn, name, file))
packages.sort()
sublist = []
for nn, name, file in packages:
item = DirBrowserTreeItem(file, self.packages + [name])
sublist.append(item)
for nn, name in self.listmodules(names):
item = ModuleBrowserTreeItem(os.path.join(self.dir, name))
sublist.append(item)
return sublist
def ispackagedir(self, file):
" Return true for directories that are packages."
if not os.path.isdir(file):
return False
init = os.path.join(file, "__init__.py")
return os.path.exists(init)
def listmodules(self, allnames):
modules = {}
suffixes = importlib.machinery.EXTENSION_SUFFIXES[:]
suffixes += importlib.machinery.SOURCE_SUFFIXES
suffixes += importlib.machinery.BYTECODE_SUFFIXES
sorted = []
for suff in suffixes:
i = -len(suff)
for name in allnames[:]:
normed_name = os.path.normcase(name)
if normed_name[i:] == suff:
mod_name = name[:i]
if mod_name not in modules:
modules[mod_name] = None
sorted.append((normed_name, name))
allnames.remove(name)
sorted.sort()
return sorted
if __name__ == "__main__":
from unittest import main
main('idlelib.idle_test.test_pathbrowser', verbosity=2, exit=False)
from idlelib.idle_test.htest import run
run(PathBrowser)
@@ -0,0 +1,120 @@
from idlelib.delegator import Delegator
from idlelib.redirector import WidgetRedirector
class Percolator:
def __init__(self, text):
# XXX would be nice to inherit from Delegator
self.text = text
self.redir = WidgetRedirector(text)
self.top = self.bottom = Delegator(text)
self.bottom.insert = self.redir.register("insert", self.insert)
self.bottom.delete = self.redir.register("delete", self.delete)
self.filters = []
def close(self):
while self.top is not self.bottom:
self.removefilter(self.top)
self.top = None
self.bottom.setdelegate(None)
self.bottom = None
self.redir.close()
self.redir = None
self.text = None
def insert(self, index, chars, tags=None):
# Could go away if inheriting from Delegator
self.top.insert(index, chars, tags)
def delete(self, index1, index2=None):
# Could go away if inheriting from Delegator
self.top.delete(index1, index2)
def insertfilter(self, filter):
# Perhaps rename to pushfilter()?
assert isinstance(filter, Delegator)
assert filter.delegate is None
filter.setdelegate(self.top)
self.top = filter
def insertfilterafter(self, filter, after):
assert isinstance(filter, Delegator)
assert isinstance(after, Delegator)
assert filter.delegate is None
f = self.top
f.resetcache()
while f is not after:
assert f is not self.bottom
f = f.delegate
f.resetcache()
filter.setdelegate(f.delegate)
f.setdelegate(filter)
def removefilter(self, filter):
# XXX Perhaps should only support popfilter()?
assert isinstance(filter, Delegator)
assert filter.delegate is not None
f = self.top
if f is filter:
self.top = filter.delegate
filter.setdelegate(None)
else:
while f.delegate is not filter:
assert f is not self.bottom
f.resetcache()
f = f.delegate
f.setdelegate(filter.delegate)
filter.setdelegate(None)
def _percolator(parent): # htest #
import tkinter as tk
class Tracer(Delegator):
def __init__(self, name):
self.name = name
Delegator.__init__(self, None)
def insert(self, *args):
print(self.name, ": insert", args)
self.delegate.insert(*args)
def delete(self, *args):
print(self.name, ": delete", args)
self.delegate.delete(*args)
top = tk.Toplevel(parent)
top.title("Test Percolator")
x, y = map(int, parent.geometry().split('+')[1:])
top.geometry("+%d+%d" % (x, y + 175))
text = tk.Text(top)
p = Percolator(text)
pin = p.insertfilter
pout = p.removefilter
t1 = Tracer("t1")
t2 = Tracer("t2")
def toggle1():
(pin if var1.get() else pout)(t1)
def toggle2():
(pin if var2.get() else pout)(t2)
text.pack()
text.focus_set()
var1 = tk.IntVar(parent)
cb1 = tk.Checkbutton(top, text="Tracer1", command=toggle1, variable=var1)
cb1.pack()
var2 = tk.IntVar(parent)
cb2 = tk.Checkbutton(top, text="Tracer2", command=toggle2, variable=var2)
cb2.pack()
if __name__ == "__main__":
from unittest import main
main('idlelib.idle_test.test_percolator', verbosity=2, exit=False)
from idlelib.idle_test.htest import run
run(_percolator)
@@ -0,0 +1,589 @@
"""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
# 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)
# Find what looks like the start of a popular statement.
_synchre = re.compile(r"""
^
[ \t]*
(?: while
| else
| def
| return
| assert
| break
| class
| continue
| elif
| try
| except
| raise
| import
| yield
)
\b
""", re.VERBOSE | re.MULTILINE).search
# Match blank line or non-indenting comment line.
_junkre = re.compile(r"""
[ \t]*
(?: \# \S .* )?
\n
""", re.VERBOSE).match
# Match any flavor of string; the terminating quote is optional
# so that we're robust in the face of incomplete program text.
_match_stringre = re.compile(r"""
\""" [^"\\]* (?:
(?: \\. | "(?!"") )
[^"\\]*
)*
(?: \""" )?
| " [^"\\\n]* (?: \\. [^"\\\n]* )* "?
| ''' [^'\\]* (?:
(?: \\. | '(?!'') )
[^'\\]*
)*
(?: ''' )?
| ' [^'\\\n]* (?: \\. [^'\\\n]* )* '?
""", re.VERBOSE | re.DOTALL).match
# Match a line that starts with something interesting;
# used to find the first item of a bracket structure.
_itemre = re.compile(r"""
[ \t]*
[^\s#\\] # if we match, m.end()-1 is the interesting char
""", re.VERBOSE).match
# Match start of statements that should be followed by a dedent.
_closere = re.compile(r"""
\s*
(?: return
| break
| continue
| raise
| pass
)
\b
""", re.VERBOSE).match
# Chew up non-special chars as quickly as possible. If match is
# successful, m.end() less 1 is the index of the last boring char
# matched. If match is unsuccessful, the string starts with an
# interesting char.
_chew_ordinaryre = re.compile(r"""
[^[\](){}#'"\\]+
""", re.VERBOSE).match
class ParseMap(dict):
r"""Dict subclass that maps anything not in dict to 'x'.
This is designed to be used with str.translate in study1.
Anything not specifically mapped otherwise becomes 'x'.
Example: replace everything except whitespace with 'x'.
>>> keepwhite = ParseMap((ord(c), ord(c)) for c in ' \t\n\r')
>>> "a + b\tc\nd".translate(keepwhite)
'x x x\tx\nx'
"""
# Calling this triples access time; see bpo-32940
def __missing__(self, key):
return 120 # ord('x')
# 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:
def __init__(self, indentwidth, tabwidth):
self.indentwidth = indentwidth
self.tabwidth = tabwidth
def set_code(self, s):
assert len(s) == 0 or s[-1] == '\n'
self.code = s
self.study_level = 0
def find_good_parse_start(self, is_char_in_string):
"""
Return index of a good place to begin parsing, as close to the
end of the string as possible. This will be the start of some
popular stmt like "if" or "def". Return None if none found:
the caller should pass more prior context then, if possible, or
if not (the entire program text up until the point of interest
has already been tried) pass 0 to set_lo().
This will be reliable iff given a reliable is_char_in_string()
function, meaning that when it says "no", it's absolutely
guaranteed that the char is not in a string.
"""
code, pos = self.code, None
# Peek back from the end for a good place to start,
# but don't try too often; pos will be left None, or
# bumped to a legitimate synch point.
limit = len(code)
for tries in range(5):
i = code.rfind(":\n", 0, limit)
if i < 0:
break
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
limit = i
if pos is None:
# Nothing looks like a block-opener, or stuff does
# but is_char_in_string keeps returning true; most likely
# we're in or near a giant string, the colorizer hasn't
# caught up enough to be helpful, or there simply *aren't*
# any interesting stmts. In any of these cases we're
# 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(code)
if m and not is_char_in_string(m.start()):
pos = m.start()
return pos
# Peeking back worked; look forward until _synchre no longer
# matches.
i = pos + 1
while m := _synchre(code, i):
s, i = m.span()
if not is_char_in_string(s):
pos = s
return pos
def set_lo(self, lo):
""" 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.code = self.code[lo:]
def _study1(self):
"""Find the line numbers of non-continuation lines.
As quickly as humanly possible <wink>, 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
# Map all uninteresting characters to "x", all open brackets
# 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.
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
# whether & why the last stmt is a continuation.
continuation = C_NONE
level = lno = 0 # level is nesting level; lno is line number
self.goodlines = goodlines = [0]
push_good = goodlines.append
i, n = 0, len(code)
while i < n:
ch = code[i]
i = i+1
# cases are checked in decreasing order of frequency
if ch == 'x':
continue
if ch == '\n':
lno = lno + 1
if level == 0:
push_good(lno)
# else we're in an unclosed bracket structure
continue
if ch == '(':
level = level + 1
continue
if ch == ')':
if level:
level = level - 1
# else the program is invalid, but we can't complain
continue
if ch == '"' or ch == "'":
# consume the string
quote = ch
if code[i-1:i+2] == quote * 3:
quote = quote * 3
firstlno = lno
w = len(quote) - 1
i = i+w
while i < n:
ch = code[i]
i = i+1
if ch == 'x':
continue
if code[i-1:i+w] == quote:
i = i+w
break
if ch == '\n':
lno = lno + 1
if w == 0:
# unterminated single-quoted string
if level == 0:
push_good(lno)
break
continue
if ch == '\\':
assert i < n
if code[i] == '\n':
lno = lno + 1
i = i+1
continue
# else comment char or paren inside string
else:
# didn't break out of the loop, so we're still
# inside a string
if (lno - 1) == firstlno:
# before the previous \n in code, we were in the first
# line of the string
continuation = C_STRING_FIRST_LINE
else:
continuation = C_STRING_NEXT_LINES
continue # with outer loop
if ch == '#':
# consume the comment
i = code.find('\n', i)
assert i >= 0
continue
assert ch == '\\'
assert i < n
if code[i] == '\n':
lno = lno + 1
if i+1 == n:
continuation = C_BACKSLASH
i = i+1
# The last stmt may be continued for all 3 reasons.
# String continuation takes precedence over bracket
# continuation, which beats backslash continuation.
if (continuation != C_STRING_FIRST_LINE
and continuation != C_STRING_NEXT_LINES and level > 0):
continuation = C_BRACKET
self.continuation = continuation
# Push the final line number as a sentinel value, regardless of
# whether it's continued.
assert (continuation == C_NONE) == (goodlines[-1] == lno)
if goodlines[-1] != lno:
push_good(lno)
def get_continuation_type(self):
self._study1()
return self.continuation
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.
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
# 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 = 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(code, p):
i = i-1
else:
break
if i == 0:
# nothing but junk!
assert p == 0
q = p
self.stmt_start, self.stmt_end = p, q
# Analyze this stmt, to find the last open bracket (if any)
# and last interesting character (if any).
lastch = ""
stack = [] # stack of open bracket indices
push_stack = stack.append
bracketing = [(p, 0)]
while p < q:
# suck up all except ()[]{}'"#\\
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 code[i] in " \t\n":
i = i-1
if i >= p:
lastch = code[i]
p = newp
if p >= q:
break
ch = code[p]
if ch in "([{":
push_stack(p)
bracketing.append((p, len(stack)))
lastch = ch
p = p+1
continue
if ch in ")]}":
if stack:
del stack[-1]
lastch = ch
p = p+1
bracketing.append((p, len(stack)))
continue
if ch == '"' or ch == "'":
# consume string
# Note that study1 did this with a Python loop, but
# we use a regexp here; the reason is speed in both
# cases; the string may be huge, but study1 pre-squashed
# strings to a couple of characters per line. study1
# also needed to keep track of newlines, and we don't
# have to.
bracketing.append((p, len(stack)+1))
lastch = ch
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 = code.find('\n', p, q) + 1
assert p > 0
bracketing.append((p, len(stack)))
continue
assert ch == '\\'
p = p+1 # beyond backslash
assert p < q
if code[p] != '\n':
# the program is invalid, but can't complain
lastch = ch + code[p]
p = p+1 # beyond escaped char
# end while p < q:
self.lastch = lastch
self.lastopenbracketpos = stack[-1] if stack else None
self.stmt_bracketing = tuple(bracketing)
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
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(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 = code.find('\n', j) + 1
else:
# nothing interesting follows the bracket;
# reproduce the bracket line's indentation + a level
j = i = origi
while code[j] in " \t":
j = j+1
extra = self.indentwidth
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]
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
code = self.code
i = self.stmt_start
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 = code.find('\n', startpos) + 1
found = level = 0
while i < endpos:
ch = code[i]
if ch in "([{":
level = level + 1
i = i+1
elif ch in ")]}":
if level:
level = level - 1
i = i+1
elif ch == '"' or ch == "'":
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 code[i-1] not in "=<>!") and \
code[i+1] != '=':
found = 1
break
else:
i = i+1
if found:
# 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*\\", code[i:endpos]) is None
if not found:
# oh well ... settle for moving beyond the first chunk
# of non-whitespace chars
i = startpos
while code[i] not in " \t\n":
i = i+1
return len(code[self.stmt_start:i].expandtabs(\
self.tabwidth)) + 1
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
code = self.code
while j < n and code[j] in " \t":
j = j + 1
return code[i:j]
def is_block_opener(self):
"Return True if the last interesting statement opens a block."
self._study2()
return self.lastch == ':'
def is_block_closer(self):
"Return True if the last interesting statement closes a block."
self._study2()
return _closere(self.code, self.stmt_start) is not None
def get_last_stmt_bracketing(self):
"""Return bracketing structure of the last interesting statement.
The returned tuple is in the format defined in _study2().
"""
self._study2()
return self.stmt_bracketing
if __name__ == '__main__':
from unittest import main
main('idlelib.idle_test.test_pyparse', verbosity=2)
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,392 @@
"""
Dialogs that query users and verify the answer before accepting.
Query is the generic base class for a popup dialog.
The user must either enter a valid answer or close the dialog.
Entries are validated when <Return> is entered or [Ok] is clicked.
Entries are ignored when [Cancel] or [X] are clicked.
The 'return value' is .result set to either a valid answer or None.
Subclass SectionName gets a name for a new config file section.
Configdialog uses it for new highlight theme and keybinding set names.
Subclass ModuleName gets a name for File => Open Module.
Subclass HelpSource gets menu item and path for additions to Help menu.
"""
# Query and Section name result from splitting GetCfgSectionNameDialog
# of configSectionNameDialog.py (temporarily config_sec.py) into
# generic and specific parts. 3.6 only, July 2016.
# ModuleName.entry_ok came from editor.EditorWindow.load_module.
# HelpSource was extracted from configHelpSourceEdit.py (temporarily
# config_help.py), with darwin code moved from ok to path_ok.
import importlib.util, importlib.abc
import os
import shlex
from sys import executable, platform # Platform is set for one test.
from tkinter import Toplevel, StringVar, BooleanVar, W, E, S
from tkinter.ttk import Frame, Button, Entry, Label, Checkbutton
from tkinter import filedialog
from tkinter.font import Font
from tkinter.simpledialog import _setup_dialog
class Query(Toplevel):
"""Base class for getting verified answer from a user.
For this base class, accept any non-blank string.
"""
def __init__(self, parent, title, message, *, text0='', used_names={},
_htest=False, _utest=False):
"""Create modal popup, return when destroyed.
Additional subclass init must be done before this unless
_utest=True is passed to suppress wait_window().
title - string, title of popup dialog
message - string, informational message to display
text0 - initial value for entry
used_names - names already in use
_htest - bool, change box location when running htest
_utest - bool, leave window hidden and not modal
"""
self.parent = parent # Needed for Font call.
self.message = message
self.text0 = text0
self.used_names = used_names
Toplevel.__init__(self, parent)
self.withdraw() # Hide while configuring, especially geometry.
self.title(title)
self.transient(parent)
if not _utest: # Otherwise fail when directly run unittest.
self.grab_set()
_setup_dialog(self)
if self._windowingsystem == 'aqua':
self.bind("<Command-.>", self.cancel)
self.bind('<Key-Escape>', self.cancel)
self.protocol("WM_DELETE_WINDOW", self.cancel)
self.bind('<Key-Return>', self.ok)
self.bind("<KP_Enter>", self.ok)
self.create_widgets()
self.update_idletasks() # Need here for winfo_reqwidth below.
self.geometry( # Center dialog over parent (or below htest box).
"+%d+%d" % (
parent.winfo_rootx() +
(parent.winfo_width()/2 - self.winfo_reqwidth()/2),
parent.winfo_rooty() +
((parent.winfo_height()/2 - self.winfo_reqheight()/2)
if not _htest else 150)
) )
self.resizable(height=False, width=False)
if not _utest:
self.deiconify() # Unhide now that geometry set.
self.entry.focus_set()
self.wait_window()
def create_widgets(self, ok_text='OK'): # Do not replace.
"""Create entry (rows, extras, buttons.
Entry stuff on rows 0-2, spanning cols 0-2.
Buttons on row 99, cols 1, 2.
"""
# Bind to self the widgets needed for entry_ok or unittest.
self.frame = frame = Frame(self, padding=10)
frame.grid(column=0, row=0, sticky='news')
frame.grid_columnconfigure(0, weight=1)
entrylabel = Label(frame, anchor='w', justify='left',
text=self.message)
self.entryvar = StringVar(self, self.text0)
self.entry = Entry(frame, width=30, textvariable=self.entryvar)
self.error_font = Font(name='TkCaptionFont',
exists=True, root=self.parent)
self.entry_error = Label(frame, text=' ', foreground='red',
font=self.error_font)
# Display or blank error by setting ['text'] =.
entrylabel.grid(column=0, row=0, columnspan=3, padx=5, sticky=W)
self.entry.grid(column=0, row=1, columnspan=3, padx=5, sticky=W+E,
pady=[10,0])
self.entry_error.grid(column=0, row=2, columnspan=3, padx=5,
sticky=W+E)
self.create_extra()
self.button_ok = Button(
frame, text=ok_text, default='active', command=self.ok)
self.button_cancel = Button(
frame, text='Cancel', command=self.cancel)
self.button_ok.grid(column=1, row=99, padx=5)
self.button_cancel.grid(column=2, row=99, padx=5)
def create_extra(self): pass # Override to add widgets.
def showerror(self, message, widget=None):
#self.bell(displayof=self)
(widget or self.entry_error)['text'] = 'ERROR: ' + message
def entry_ok(self): # Example: usually replace.
"Return non-blank entry or None."
entry = self.entry.get().strip()
if not entry:
self.showerror('blank line.')
return None
return entry
def ok(self, event=None): # Do not replace.
'''If entry is valid, bind it to 'result' and destroy tk widget.
Otherwise leave dialog open for user to correct entry or cancel.
'''
self.entry_error['text'] = ''
entry = self.entry_ok()
if entry is not None:
self.result = entry
self.destroy()
else:
# [Ok] moves focus. (<Return> does not.) Move it back.
self.entry.focus_set()
def cancel(self, event=None): # Do not replace.
"Set dialog result to None and destroy tk widget."
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."
# Used in ConfigDialog.GetNewKeysName, .GetNewThemeName (837)
def __init__(self, parent, title, message, used_names,
*, _htest=False, _utest=False):
super().__init__(parent, title, message, used_names=used_names,
_htest=_htest, _utest=_utest)
def entry_ok(self):
"Return sensible ConfigParser section name or None."
name = self.entry.get().strip()
if not name:
self.showerror('no name specified.')
return None
elif len(name)>30:
self.showerror('name is longer than 30 characters.')
return None
elif name in self.used_names:
self.showerror('name is already in use.')
return None
return name
class ModuleName(Query):
"Get a module name for Open Module menu entry."
# Used in open_module (editor.EditorWindow until move to iobinding).
def __init__(self, parent, title, message, text0,
*, _htest=False, _utest=False):
super().__init__(parent, title, message, text0=text0,
_htest=_htest, _utest=_utest)
def entry_ok(self):
"Return entered module name as file path or None."
name = self.entry.get().strip()
if not name:
self.showerror('no name specified.')
return None
# XXX Ought to insert current file's directory in front of path.
try:
spec = importlib.util.find_spec(name)
except (ValueError, ImportError) as msg:
self.showerror(str(msg))
return None
if spec is None:
self.showerror("module not found.")
return None
if not isinstance(spec.loader, importlib.abc.SourceLoader):
self.showerror("not a source-based module.")
return None
try:
file_path = spec.loader.get_filename(name)
except AttributeError:
self.showerror("loader does not support get_filename.")
return None
except ImportError:
# Some special modules require this (e.g. os.path)
try:
file_path = spec.loader.get_filename()
except TypeError:
self.showerror("loader failed to get filename.")
return None
return file_path
class Goto(Query):
"Get a positive line number for editor Go To Line."
# Used in editor.EditorWindow.goto_line_event.
def entry_ok(self):
try:
lineno = int(self.entry.get())
except ValueError:
self.showerror('not a base 10 integer.')
return None
if lineno <= 0:
self.showerror('not a positive integer.')
return None
return lineno
class HelpSource(Query):
"Get menu name and help source for Help menu."
# Used in ConfigDialog.HelpListItemAdd/Edit, (941/9)
def __init__(self, parent, title, *, menuitem='', filepath='',
used_names={}, _htest=False, _utest=False):
"""Get menu entry and url/local file for Additional Help.
User enters a name for the Help resource and a web url or file
name. The user can browse for the file.
"""
self.filepath = filepath
message = 'Name for item on Help menu:'
super().__init__(
parent, title, message, text0=menuitem,
used_names=used_names, _htest=_htest, _utest=_utest)
def create_extra(self):
"Add path widjets to rows 10-12."
frame = self.frame
pathlabel = Label(frame, anchor='w', justify='left',
text='Help File Path: Enter URL or browse for file')
self.pathvar = StringVar(self, self.filepath)
self.path = Entry(frame, textvariable=self.pathvar, width=40)
browse = Button(frame, text='Browse', width=8,
command=self.browse_file)
self.path_error = Label(frame, text=' ', foreground='red',
font=self.error_font)
pathlabel.grid(column=0, row=10, columnspan=3, padx=5, pady=[10,0],
sticky=W)
self.path.grid(column=0, row=11, columnspan=2, padx=5, sticky=W+E,
pady=[10,0])
browse.grid(column=2, row=11, padx=5, sticky=W+S)
self.path_error.grid(column=0, row=12, columnspan=3, padx=5,
sticky=W+E)
def askfilename(self, filetypes, initdir, initfile): # htest #
# Extracted from browse_file so can mock for unittests.
# Cannot unittest as cannot simulate button clicks.
# Test by running htest, such as by running this file.
return filedialog.Open(parent=self, filetypes=filetypes)\
.show(initialdir=initdir, initialfile=initfile)
def browse_file(self):
filetypes = [
("HTML Files", "*.htm *.html", "TEXT"),
("PDF Files", "*.pdf", "TEXT"),
("Windows Help Files", "*.chm"),
("Text Files", "*.txt", "TEXT"),
("All Files", "*")]
path = self.pathvar.get()
if path:
dir, base = os.path.split(path)
else:
base = None
if platform[:3] == 'win':
dir = os.path.join(os.path.dirname(executable), 'Doc')
if not os.path.isdir(dir):
dir = os.getcwd()
else:
dir = os.getcwd()
file = self.askfilename(filetypes, dir, base)
if file:
self.pathvar.set(file)
item_ok = SectionName.entry_ok # localize for test override
def path_ok(self):
"Simple validity check for menu file path"
path = self.path.get().strip()
if not path: #no path specified
self.showerror('no help file path specified.', self.path_error)
return None
elif not path.startswith(('www.', 'http')):
if path[:5] == 'file:':
path = path[5:]
if not os.path.exists(path):
self.showerror('help file path does not exist.',
self.path_error)
return None
if platform == 'darwin': # for Mac Safari
path = "file://" + path
return path
def entry_ok(self):
"Return apparently valid (name, path) or None"
self.path_error['text'] = ''
name = self.item_ok()
path = self.path_ok()
return None if name is None or path is None else (name, path)
class CustomRun(Query):
"""Get settings for custom run of module.
1. Command line arguments to extend sys.argv.
2. Whether to restart Shell or not.
"""
# Used in runscript.run_custom_event
def __init__(self, parent, title, *, cli_args=[],
_htest=False, _utest=False):
"""cli_args is a list of strings.
The list is assigned to the default Entry StringVar.
The strings are displayed joined by ' ' for display.
"""
message = 'Command Line Arguments for sys.argv:'
super().__init__(
parent, title, message, text0=cli_args,
_htest=_htest, _utest=_utest)
def create_extra(self):
"Add run mode on rows 10-12."
frame = self.frame
self.restartvar = BooleanVar(self, value=True)
restart = Checkbutton(frame, variable=self.restartvar, onvalue=True,
offvalue=False, text='Restart shell')
self.args_error = Label(frame, text=' ', foreground='red',
font=self.error_font)
restart.grid(column=0, row=10, columnspan=3, padx=5, sticky='w')
self.args_error.grid(column=0, row=12, columnspan=3, padx=5,
sticky='we')
def cli_args_ok(self):
"Return command line arg list or None if error."
cli_string = self.entry.get().strip()
try:
cli_args = shlex.split(cli_string, posix=True)
except ValueError as err:
self.showerror(str(err))
return None
return cli_args
def entry_ok(self):
"Return apparently valid (cli_args, restart) or None."
cli_args = self.cli_args_ok()
restart = self.restartvar.get()
return None if cli_args is None else (cli_args, restart)
if __name__ == '__main__':
from unittest import main
main('idlelib.idle_test.test_query', verbosity=2, exit=False)
from idlelib.idle_test.htest import run
run(Query, HelpSource, CustomRun)
@@ -0,0 +1,173 @@
from tkinter import TclError
class WidgetRedirector:
"""Support for redirecting arbitrary widget subcommands.
Some Tk operations don't normally pass through tkinter. For example, if a
character is inserted into a Text widget by pressing a key, a default Tk
binding to the widget's 'insert' operation is activated, and the Tk library
processes the insert without calling back into tkinter.
Although a binding to <Key> could be made via tkinter, what we really want
to do is to hook the Tk 'insert' operation itself. For one thing, we want
a text.insert call in idle code to have the same effect as a key press.
When a widget is instantiated, a Tcl command is created whose name is the
same as the pathname widget._w. This command is used to invoke the various
widget operations, e.g. insert (for a Text widget). We are going to hook
this command and provide a facility ('register') to intercept the widget
operation. We will also intercept method calls on the tkinter class
instance that represents the tk widget.
In IDLE, WidgetRedirector is used in Percolator to intercept Text
commands. The function being registered provides access to the top
of a Percolator chain. At the bottom of the chain is a call to the
original Tk widget operation.
"""
def __init__(self, widget):
'''Initialize attributes and setup redirection.
_operations: dict mapping operation name to new function.
widget: the widget whose tcl command is to be intercepted.
tk: widget.tk, a convenience attribute, probably not needed.
orig: new name of the original tcl command.
Since renaming to orig fails with TclError when orig already
exists, only one WidgetDirector can exist for a given widget.
'''
self._operations = {}
self.widget = widget # widget instance
self.tk = tk = widget.tk # widget's root
w = widget._w # widget's (full) Tk pathname
self.orig = w + "_orig"
# Rename the Tcl command within Tcl:
tk.call("rename", w, self.orig)
# Create a new Tcl command whose name is the widget's pathname, and
# whose action is to dispatch on the operation passed to the widget:
tk.createcommand(w, self.dispatch)
def __repr__(self):
w = self.widget
return f"{self.__class__.__name__,}({w.__class__.__name__}<{w._w}>)"
def close(self):
"Unregister operations and revert redirection created by .__init__."
for operation in list(self._operations):
self.unregister(operation)
widget = self.widget
tk = widget.tk
w = widget._w
# Restore the original widget Tcl command.
tk.deletecommand(w)
tk.call("rename", self.orig, w)
del self.widget, self.tk # Should not be needed
# if instance is deleted after close, as in Percolator.
def register(self, operation, function):
'''Return OriginalCommand(operation) after registering function.
Registration adds an operation: function pair to ._operations.
It also adds a widget function attribute that masks the tkinter
class instance method. Method masking operates independently
from command dispatch.
If a second function is registered for the same operation, the
first function is replaced in both places.
'''
self._operations[operation] = function
setattr(self.widget, operation, function)
return OriginalCommand(self, operation)
def unregister(self, operation):
'''Return the function for the operation, or None.
Deleting the instance attribute unmasks the class attribute.
'''
if operation in self._operations:
function = self._operations[operation]
del self._operations[operation]
try:
delattr(self.widget, operation)
except AttributeError:
pass
return function
else:
return None
def dispatch(self, operation, *args):
'''Callback from Tcl which runs when the widget is referenced.
If an operation has been registered in self._operations, apply the
associated function to the args passed into Tcl. Otherwise, pass the
operation through to Tk via the original Tcl function.
Note that if a registered function is called, the operation is not
passed through to Tk. Apply the function returned by self.register()
to *args to accomplish that. For an example, see colorizer.py.
'''
m = self._operations.get(operation)
try:
if m:
return m(*args)
else:
return self.tk.call((self.orig, operation) + args)
except TclError:
return ""
class OriginalCommand:
'''Callable for original tk command that has been redirected.
Returned by .register; can be used in the function registered.
redir = WidgetRedirector(text)
def my_insert(*args):
print("insert", args)
original_insert(*args)
original_insert = redir.register("insert", my_insert)
'''
def __init__(self, redir, operation):
'''Create .tk_call and .orig_and_operation for .__call__ method.
.redir and .operation store the input args for __repr__.
.tk and .orig copy attributes of .redir (probably not needed).
'''
self.redir = redir
self.operation = operation
self.tk = redir.tk # redundant with self.redir
self.orig = redir.orig # redundant with self.redir
# These two could be deleted after checking recipient code.
self.tk_call = redir.tk.call
self.orig_and_operation = (redir.orig, operation)
def __repr__(self):
return f"{self.__class__.__name__,}({self.redir!r}, {self.operation!r})"
def __call__(self, *args):
return self.tk_call(self.orig_and_operation + args)
def _widget_redirector(parent): # htest #
from tkinter import Toplevel, Text
top = Toplevel(parent)
top.title("Test WidgetRedirector")
x, y = map(int, parent.geometry().split('+')[1:])
top.geometry("+%d+%d" % (x, y + 175))
text = Text(top)
text.pack()
text.focus_set()
redir = WidgetRedirector(text)
def my_insert(*args):
print("insert", args)
original_insert(*args)
original_insert = redir.register("insert", my_insert)
if __name__ == "__main__":
from unittest import main
main('idlelib.idle_test.test_redirector', verbosity=2, exit=False)
from idlelib.idle_test.htest import run
run(_widget_redirector)
@@ -0,0 +1,300 @@
"""Replace dialog for IDLE. Inherits SearchDialogBase for GUI.
Uses idlelib.searchengine.SearchEngine for search capability.
Defines various replace related functions like replace, replace all,
and replace+find.
"""
import re
re.PatternError = re.error # New in 3.13.
from tkinter import StringVar, TclError
from idlelib.searchbase import SearchDialogBase
from idlelib import searchengine
def replace(text, insert_tags=None):
"""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"):
engine._replacedialog = ReplaceDialog(root, engine)
dialog = engine._replacedialog
searchphrase = text.get("sel.first", "sel.last")
dialog.open(text, searchphrase, insert_tags=insert_tags)
class ReplaceDialog(SearchDialogBase):
"Dialog for finding and replacing a pattern in text."
title = "Replace Dialog"
icon = "Replace"
def __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)
self.insert_tags = None
def open(self, text, searchphrase=None, *, insert_tags=None):
"""Make dialog visible on top of others and ready to use.
Also, set the search to include the current selection
(self.ok).
Args:
text: Text widget being searched.
searchphrase: String phrase to search.
"""
SearchDialogBase.open(self, text, searchphrase)
self.ok = True
self.insert_tags = insert_tags
def create_entries(self):
"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, isdef=True)
self.make_button("Replace All", self.replace_all)
def find_it(self, event=None):
"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):
"""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(False)
def _replace_expand(self, m, repl):
"Expand replacement text if regular expression."
if self.engine.isre():
try:
new = m.expand(repl)
except re.PatternError:
self.engine.report_error(repl, 'Invalid Replace Expression')
new = None
else:
new = repl
return new
def replace_all(self, event=None):
"""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
repl = self.replvar.get()
text = self.text
res = self.engine.search_text(text, prog)
if not res:
self.bell()
return
text.tag_remove("sel", "1.0", "end")
text.tag_remove("hit", "1.0", "end")
line = res[0]
col = res[1].start()
if self.engine.iswrap():
line = 1
col = 0
ok = True
first = last = None
# XXX ought to replace circular instead of top-to-bottom when wrapping
text.undo_block_start()
while res := self.engine.search_forward(
text, prog, line, col, wrap=False, ok=ok):
line, m = res
chars = text.get("%d.0" % line, "%d.0" % (line+1))
orig = m.group()
new = self._replace_expand(m, repl)
if new is None:
break
i, j = m.span()
first = "%d.%d" % (line, i)
last = "%d.%d" % (line, j)
if new == orig:
text.mark_set("insert", last)
else:
text.mark_set("insert", first)
if first != last:
text.delete(first, last)
if new:
text.insert(first, new, self.insert_tags)
col = i + len(new)
ok = False
text.undo_block_stop()
if first and last:
self.show_hit(first, last)
self.close()
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
res = self.engine.search_text(text, None, ok)
if not res:
self.bell()
return False
line, m = res
i, j = m.span()
first = "%d.%d" % (line, i)
last = "%d.%d" % (line, j)
self.show_hit(first, last)
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
text = self.text
try:
first = pos = text.index("sel.first")
last = text.index("sel.last")
except TclError:
pos = None
if not pos:
first = last = pos = text.index("insert")
line, col = searchengine.get_line_col(pos)
chars = text.get("%d.0" % line, "%d.0" % (line+1))
m = prog.match(chars, col)
if not prog:
return False
new = self._replace_expand(m, self.replvar.get())
if new is None:
return False
text.mark_set("insert", first)
text.undo_block_start()
if m.group():
text.delete(first, last)
if new:
text.insert(first, new, self.insert_tags)
text.undo_block_stop()
self.show_hit(first, text.index("insert"))
self.ok = False
return True
def show_hit(self, first, last):
"""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")
text.tag_add("sel", first, last)
text.tag_remove("hit", "1.0", "end")
if first == last:
text.tag_add("hit", first)
else:
text.tag_add("hit", first, last)
text.see("insert")
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")
self.insert_tags = None
def _replace_dialog(parent): # htest #
from tkinter import Toplevel, Text, END, SEL
from tkinter.ttk import Frame, Button
top = Toplevel(parent)
top.title("Test ReplaceDialog")
x, y = map(int, parent.geometry().split('+')[1:])
top.geometry("+%d+%d" % (x, y + 175))
# mock undo delegator methods
def undo_block_start():
pass
def undo_block_stop():
pass
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()
text.insert("insert","This is a sample sTring\nPlus MORE.")
text.focus_set()
def show_replace():
text.tag_add(SEL, "1.0", END)
replace(text)
text.tag_remove(SEL, "1.0", END)
button = Button(frame, text="Replace", command=show_replace)
button.pack()
if __name__ == '__main__':
from unittest import main
main('idlelib.idle_test.test_replace', verbosity=2, exit=False)
from idlelib.idle_test.htest import run
run(_replace_dialog)
@@ -0,0 +1,635 @@
"""RPC Implementation, originally written for the Python Idle IDE
For security reasons, GvR requested that Idle's Python execution server process
connect to the Idle process, which listens for the connection. Since Idle has
only one client per server, this was not a limitation.
+---------------------------------+ +-------------+
| socketserver.BaseRequestHandler | | SocketIO |
+---------------------------------+ +-------------+
^ | register() |
| | unregister()|
| +-------------+
| ^ ^
| | |
| + -------------------+ |
| | |
+-------------------------+ +-----------------+
| RPCHandler | | RPCClient |
| [attribute of RPCServer]| | |
+-------------------------+ +-----------------+
The RPCServer handler class is expected to provide register/unregister methods.
RPCHandler inherits the mix-in class SocketIO, which provides these methods.
See the Idle run.main() docstring for further information on how this was
accomplished in Idle.
"""
import builtins
import copyreg
import io
import marshal
import os
import pickle
import queue
import select
import socket
import socketserver
import struct
import sys
import threading
import traceback
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)
return f.getvalue()
class CodePickler(pickle.Pickler):
dispatch_table = {types.CodeType: pickle_code, **copyreg.dispatch_table}
BUFSIZE = 8*1024
LOCALHOST = '127.0.0.1'
class RPCServer(socketserver.TCPServer):
def __init__(self, addr, handlerclass=None):
if handlerclass is None:
handlerclass = RPCHandler
socketserver.TCPServer.__init__(self, addr, handlerclass)
def server_bind(self):
"Override TCPServer method, no bind() phase for connecting entity"
pass
def server_activate(self):
"""Override TCPServer method, connect() instead of listen()
Due to the reversed connection, self.server_address is actually the
address of the Idle Client to which we are connecting.
"""
self.socket.connect(self.server_address)
def get_request(self):
"Override TCPServer method, return already connected socket"
return self.socket, self.server_address
def handle_error(self, request, client_address):
"""Override TCPServer method
Error message goes to __stderr__. No error message if exiting
normally or socket raised EOF. Other exceptions not handled in
server code will cause os._exit.
"""
try:
raise
except SystemExit:
raise
except:
erf = sys.__stderr__
print('\n' + '-'*40, file=erf)
print('Unhandled server exception!', file=erf)
print('Thread: %s' % threading.current_thread().name, file=erf)
print('Client Address: ', client_address, file=erf)
print('Request: ', repr(request), file=erf)
traceback.print_exc(file=erf)
print('\n*** Unrecoverable, server exiting!', file=erf)
print('-'*40, file=erf)
os._exit(0)
#----------------- end class RPCServer --------------------
objecttable = {}
request_queue = queue.Queue(0)
response_queue = queue.Queue(0)
class SocketIO:
nextseq = 0
def __init__(self, sock, objtable=None, debugging=None):
self.sockthread = threading.current_thread()
if debugging is not None:
self.debugging = debugging
self.sock = sock
if objtable is None:
objtable = objecttable
self.objtable = objtable
self.responses = {}
self.cvars = {}
def close(self):
sock = self.sock
self.sock = None
if sock is not None:
sock.close()
def exithook(self):
"override for specific exit action"
os._exit(0)
def debug(self, *args):
if not self.debugging:
return
s = self.location + " " + str(threading.current_thread().name)
for a in args:
s = s + " " + str(a)
print(s, file=sys.__stderr__)
def register(self, oid, object_):
self.objtable[oid] = object_
def unregister(self, oid):
try:
del self.objtable[oid]
except KeyError:
pass
def localcall(self, seq, request):
self.debug("localcall:", request)
try:
how, (oid, methodname, args, kwargs) = request
except TypeError:
return ("ERROR", "Bad request format")
if oid not in self.objtable:
return ("ERROR", f"Unknown object id: {oid!r}")
obj = self.objtable[oid]
if methodname == "__methods__":
methods = {}
_getmethods(obj, methods)
return ("OK", methods)
if methodname == "__attributes__":
attributes = {}
_getattributes(obj, attributes)
return ("OK", attributes)
if not hasattr(obj, methodname):
return ("ERROR", f"Unsupported method name: {methodname!r}")
method = getattr(obj, methodname)
try:
if how == 'CALL':
ret = method(*args, **kwargs)
if isinstance(ret, RemoteObject):
ret = remoteref(ret)
return ("OK", ret)
elif how == 'QUEUE':
request_queue.put((seq, (method, args, kwargs)))
return("QUEUED", None)
else:
return ("ERROR", "Unsupported message type: %s" % how)
except SystemExit:
raise
except KeyboardInterrupt:
raise
except OSError:
raise
except Exception as ex:
return ("CALLEXC", ex)
except:
msg = "*** Internal Error: rpc.py:SocketIO.localcall()\n\n"\
" Object: %s \n Method: %s \n Args: %s\n"
print(msg % (oid, method, args), file=sys.__stderr__)
traceback.print_exc(file=sys.__stderr__)
return ("EXCEPTION", None)
def remotecall(self, oid, methodname, args, kwargs):
self.debug("remotecall:asynccall: ", oid, methodname)
seq = self.asynccall(oid, methodname, args, kwargs)
return self.asyncreturn(seq)
def remotequeue(self, oid, methodname, args, kwargs):
self.debug("remotequeue:asyncqueue: ", oid, methodname)
seq = self.asyncqueue(oid, methodname, args, kwargs)
return self.asyncreturn(seq)
def asynccall(self, oid, methodname, args, kwargs):
request = ("CALL", (oid, methodname, args, kwargs))
seq = self.newseq()
if threading.current_thread() != self.sockthread:
cvar = threading.Condition()
self.cvars[seq] = cvar
self.debug(("asynccall:%d:" % seq), oid, methodname, args, kwargs)
self.putmessage((seq, request))
return seq
def asyncqueue(self, oid, methodname, args, kwargs):
request = ("QUEUE", (oid, methodname, args, kwargs))
seq = self.newseq()
if threading.current_thread() != self.sockthread:
cvar = threading.Condition()
self.cvars[seq] = cvar
self.debug(("asyncqueue:%d:" % seq), oid, methodname, args, kwargs)
self.putmessage((seq, request))
return seq
def asyncreturn(self, seq):
self.debug("asyncreturn:%d:call getresponse(): " % seq)
response = self.getresponse(seq, wait=0.05)
self.debug(("asyncreturn:%d:response: " % seq), response)
return self.decoderesponse(response)
def decoderesponse(self, response):
how, what = response
if how == "OK":
return what
if how == "QUEUED":
return None
if how == "EXCEPTION":
self.debug("decoderesponse: EXCEPTION")
return None
if how == "EOF":
self.debug("decoderesponse: EOF")
self.decode_interrupthook()
return None
if how == "ERROR":
self.debug("decoderesponse: Internal ERROR:", what)
raise RuntimeError(what)
if how == "CALLEXC":
self.debug("decoderesponse: Call Exception:", what)
raise what
raise SystemError(how, what)
def decode_interrupthook(self):
""
raise EOFError
def mainloop(self):
"""Listen on socket until I/O not ready or EOF
pollresponse() will loop looking for seq number None, which
never comes, and exit on EOFError.
"""
try:
self.getresponse(myseq=None, wait=0.05)
except EOFError:
self.debug("mainloop:return")
return
def getresponse(self, myseq, wait):
response = self._getresponse(myseq, wait)
if response is not None:
how, what = response
if how == "OK":
response = how, self._proxify(what)
return response
def _proxify(self, obj):
if isinstance(obj, RemoteProxy):
return RPCProxy(self, obj.oid)
if isinstance(obj, list):
return list(map(self._proxify, obj))
# XXX Check for other types -- not currently needed
return obj
def _getresponse(self, myseq, wait):
self.debug("_getresponse:myseq:", myseq)
if threading.current_thread() is self.sockthread:
# this thread does all reading of requests or responses
while True:
response = self.pollresponse(myseq, wait)
if response is not None:
return response
else:
# wait for notification from socket handling thread
cvar = self.cvars[myseq]
cvar.acquire()
while myseq not in self.responses:
cvar.wait()
response = self.responses[myseq]
self.debug("_getresponse:%s: thread woke up: response: %s" %
(myseq, response))
del self.responses[myseq]
del self.cvars[myseq]
cvar.release()
return response
def newseq(self):
self.nextseq = seq = self.nextseq + 2
return seq
def putmessage(self, message):
self.debug("putmessage:%d:" % message[0])
try:
s = dumps(message)
except pickle.PicklingError:
print("Cannot pickle:", repr(message), file=sys.__stderr__)
raise
s = struct.pack("<i", len(s)) + s
while len(s) > 0:
try:
r, w, x = select.select([], [self.sock], [])
n = self.sock.send(s[:BUFSIZE])
except (AttributeError, TypeError):
raise OSError("socket no longer exists")
s = s[n:]
buff = b''
bufneed = 4
bufstate = 0 # meaning: 0 => reading count; 1 => reading data
def pollpacket(self, wait):
self._stage0()
if len(self.buff) < self.bufneed:
r, w, x = select.select([self.sock.fileno()], [], [], wait)
if len(r) == 0:
return None
try:
s = self.sock.recv(BUFSIZE)
except OSError:
raise EOFError
if len(s) == 0:
raise EOFError
self.buff += s
self._stage0()
return self._stage1()
def _stage0(self):
if self.bufstate == 0 and len(self.buff) >= 4:
s = self.buff[:4]
self.buff = self.buff[4:]
self.bufneed = struct.unpack("<i", s)[0]
self.bufstate = 1
def _stage1(self):
if self.bufstate == 1 and len(self.buff) >= self.bufneed:
packet = self.buff[:self.bufneed]
self.buff = self.buff[self.bufneed:]
self.bufneed = 4
self.bufstate = 0
return packet
def pollmessage(self, wait):
packet = self.pollpacket(wait)
if packet is None:
return None
try:
message = pickle.loads(packet)
except pickle.UnpicklingError:
print("-----------------------", file=sys.__stderr__)
print("cannot unpickle packet:", repr(packet), file=sys.__stderr__)
traceback.print_stack(file=sys.__stderr__)
print("-----------------------", file=sys.__stderr__)
raise
return message
def pollresponse(self, myseq, wait):
"""Handle messages received on the socket.
Some messages received may be asynchronous 'call' or 'queue' requests,
and some may be responses for other threads.
'call' requests are passed to self.localcall() with the expectation of
immediate execution, during which time the socket is not serviced.
'queue' requests are used for tasks (which may block or hang) to be
processed in a different thread. These requests are fed into
request_queue by self.localcall(). Responses to queued requests are
taken from response_queue and sent across the link with the associated
sequence numbers. Messages in the queues are (sequence_number,
request/response) tuples and code using this module removing messages
from the request_queue is responsible for returning the correct
sequence number in the response_queue.
pollresponse() will loop until a response message with the myseq
sequence number is received, and will save other responses in
self.responses and notify the owning thread.
"""
while True:
# send queued response if there is one available
try:
qmsg = response_queue.get(0)
except queue.Empty:
pass
else:
seq, response = qmsg
message = (seq, ('OK', response))
self.putmessage(message)
# poll for message on link
try:
message = self.pollmessage(wait)
if message is None: # socket not ready
return None
except EOFError:
self.handle_EOF()
return None
except AttributeError:
return None
seq, resq = message
how = resq[0]
self.debug("pollresponse:%d:myseq:%s" % (seq, myseq))
# process or queue a request
if how in ("CALL", "QUEUE"):
self.debug("pollresponse:%d:localcall:call:" % seq)
response = self.localcall(seq, resq)
self.debug("pollresponse:%d:localcall:response:%s"
% (seq, response))
if how == "CALL":
self.putmessage((seq, response))
elif how == "QUEUE":
# don't acknowledge the 'queue' request!
pass
continue
# return if completed message transaction
elif seq == myseq:
return resq
# must be a response for a different thread:
else:
cv = self.cvars.get(seq, None)
# response involving unknown sequence number is discarded,
# probably intended for prior incarnation of server
if cv is not None:
cv.acquire()
self.responses[seq] = resq
cv.notify()
cv.release()
continue
def handle_EOF(self):
"action taken upon link being closed by peer"
self.EOFhook()
self.debug("handle_EOF")
for key in self.cvars:
cv = self.cvars[key]
cv.acquire()
self.responses[key] = ('EOF', None)
cv.notify()
cv.release()
# call our (possibly overridden) exit function
self.exithook()
def EOFhook(self):
"Classes using rpc client/server can override to augment EOF action"
pass
#----------------- end class SocketIO --------------------
class RemoteObject:
# Token mix-in class
pass
def remoteref(obj):
oid = id(obj)
objecttable[oid] = obj
return RemoteProxy(oid)
class RemoteProxy:
def __init__(self, oid):
self.oid = oid
class RPCHandler(socketserver.BaseRequestHandler, SocketIO):
debugging = False
location = "#S" # Server
def __init__(self, sock, addr, svr):
svr.current_handler = self ## cgt xxx
SocketIO.__init__(self, sock)
socketserver.BaseRequestHandler.__init__(self, sock, addr, svr)
def handle(self):
"handle() method required by socketserver"
self.mainloop()
def get_remote_proxy(self, oid):
return RPCProxy(self, oid)
class RPCClient(SocketIO):
debugging = False
location = "#C" # Client
nextseq = 1 # Requests coming from the client are odd numbered
def __init__(self, address, family=socket.AF_INET, type=socket.SOCK_STREAM):
self.listening_sock = socket.socket(family, type)
self.listening_sock.bind(address)
self.listening_sock.listen(1)
def accept(self):
working_sock, address = self.listening_sock.accept()
if self.debugging:
print("****** Connection request from ", address, file=sys.__stderr__)
if address[0] == LOCALHOST:
SocketIO.__init__(self, working_sock)
else:
print("** Invalid host: ", address, file=sys.__stderr__)
raise OSError
def get_remote_proxy(self, oid):
return RPCProxy(self, oid)
class RPCProxy:
__methods = None
__attributes = None
def __init__(self, sockio, oid):
self.sockio = sockio
self.oid = oid
def __getattr__(self, name):
if self.__methods is None:
self.__getmethods()
if self.__methods.get(name):
return MethodProxy(self.sockio, self.oid, name)
if self.__attributes is None:
self.__getattributes()
if name in self.__attributes:
value = self.sockio.remotecall(self.oid, '__getattribute__',
(name,), {})
return value
else:
raise AttributeError(name)
def __getattributes(self):
self.__attributes = self.sockio.remotecall(self.oid,
"__attributes__", (), {})
def __getmethods(self):
self.__methods = self.sockio.remotecall(self.oid,
"__methods__", (), {})
def _getmethods(obj, methods):
# Helper to get a list of methods from an object
# Adds names to dictionary argument 'methods'
for name in dir(obj):
attr = getattr(obj, name)
if callable(attr):
methods[name] = 1
if isinstance(obj, type):
for super in obj.__bases__:
_getmethods(super, methods)
def _getattributes(obj, attributes):
for name in dir(obj):
attr = getattr(obj, name)
if not callable(attr):
attributes[name] = 1
class MethodProxy:
def __init__(self, sockio, oid, name):
self.sockio = sockio
self.oid = oid
self.name = name
def __call__(self, /, *args, **kwargs):
value = self.sockio.remotecall(self.oid, self.name, args, kwargs)
return value
# XXX KBK 09Sep03 We need a proper unit test for this module. Previously
# existing test code was removed at Rev 1.27 (r34098).
def displayhook(value):
"""Override standard display hook to use non-locale encoding"""
if value is None:
return
# Set '_' to None to avoid recursion
builtins._ = None
text = repr(value)
try:
sys.stdout.write(text)
except UnicodeEncodeError:
# let's use ascii while utf8-bmp codec doesn't present
encoding = 'ascii'
bytes = text.encode(encoding, 'backslashreplace')
text = bytes.decode(encoding, 'strict')
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,)
@@ -0,0 +1,646 @@
""" idlelib.run
Simplified, pyshell.ModifiedInterpreter spawns a subprocess with
f'''{sys.executable} -c "__import__('idlelib.run').run.main()"'''
'.run' is needed because __import__ returns idlelib, not idlelib.run.
"""
import contextlib
import functools
import io
import linecache
import queue
import sys
import textwrap
import time
import traceback
import _thread as thread
import threading
import warnings
import idlelib # testing
from idlelib import autocomplete # AutoComplete, fetch_encodings
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
from idlelib import rpc # multiple objects
from idlelib import stackviewer # StackTreeItem
import __main__
import tkinter # Use tcl and, if startup fails, messagebox.
if not hasattr(sys.modules['idlelib.run'], 'firstrun'):
# Undo modifications of tkinter by idlelib imports; see bpo-25507.
for mod in ('simpledialog', 'messagebox', 'font',
'dialog', 'filedialog', 'commondialog',
'ttk'):
delattr(tkinter, mod)
del sys.modules['tkinter.' + mod]
# Avoid AttributeError if run again; see bpo-37038.
sys.modules['idlelib.run'].firstrun = False
LOCALHOST = '127.0.0.1'
try:
eof = 'Ctrl-D (end-of-file)'
exit.eof = eof
quit.eof = eof
except NameError: # In case subprocess started with -S (maybe in future).
pass
def idle_formatwarning(message, category, filename, lineno, line=None):
"""Format warnings the IDLE way."""
s = "\nWarning (from warnings module):\n"
s += f' File \"{filename}\", line {lineno}\n'
if line is None:
line = linecache.getline(filename, lineno)
line = line.strip()
if line:
s += " %s\n" % line
s += f"{category.__name__}: {message}\n"
return s
def idle_showwarning_subproc(
message, category, filename, lineno, file=None, line=None):
"""Show Idle-format warning after replacing warnings.showwarning.
The only difference is the formatter called.
"""
if file is None:
file = sys.stderr
try:
file.write(idle_formatwarning(
message, category, filename, lineno, line))
except OSError:
pass # the file (probably stderr) is invalid - this warning gets lost.
_warnings_showwarning = None
def capture_warnings(capture):
"Replace warning.showwarning with idle_showwarning_subproc, or reverse."
global _warnings_showwarning
if capture:
if _warnings_showwarning is None:
_warnings_showwarning = warnings.showwarning
warnings.showwarning = idle_showwarning_subproc
else:
if _warnings_showwarning is not None:
warnings.showwarning = _warnings_showwarning
_warnings_showwarning = None
capture_warnings(True)
tcl = tkinter.Tcl()
def handle_tk_events(tcl=tcl):
"""Process any tk events that are ready to be dispatched if tkinter
has been imported, a tcl interpreter has been created and tk has been
loaded."""
tcl.eval("update")
# Thread shared globals: Establish a queue between a subthread (which handles
# the socket) and the main thread (which runs user code), plus global
# completion, exit and interruptible (the main thread) flags:
exit_now = False
quitting = False
interruptible = False
def main(del_exitfunc=False):
"""Start the Python execution server in a subprocess
In the Python subprocess, RPCServer is instantiated with handlerclass
MyHandler, which inherits register/unregister methods from RPCHandler via
the mix-in class SocketIO.
When the RPCServer 'server' is instantiated, the TCPServer initialization
creates an instance of run.MyHandler and calls its handle() method.
handle() instantiates a run.Executive object, passing it a reference to the
MyHandler object. That reference is saved as attribute rpchandler of the
Executive instance. The Executive methods have access to the reference and
can pass it on to entities that they command
(e.g. debugger_r.Debugger.start_debugger()). The latter, in turn, can
call MyHandler(SocketIO) register/unregister methods via the reference to
register and unregister themselves.
"""
global exit_now
global quitting
global no_exitfunc
no_exitfunc = del_exitfunc
#time.sleep(15) # test subprocess not responding
try:
assert(len(sys.argv) > 1)
port = int(sys.argv[-1])
except:
print("IDLE Subprocess: no IP port passed in sys.argv.",
file=sys.__stderr__)
return
capture_warnings(True)
sys.argv[:] = [""]
threading.Thread(target=manage_socket,
name='SockThread',
args=((LOCALHOST, port),),
daemon=True,
).start()
while True:
try:
if exit_now:
try:
exit()
except KeyboardInterrupt:
# exiting but got an extra KBI? Try again!
continue
try:
request = rpc.request_queue.get(block=True, timeout=0.05)
except queue.Empty:
request = None
# Issue 32207: calling handle_tk_events here adds spurious
# queue.Empty traceback to event handling exceptions.
if request:
seq, (method, args, kwargs) = request
ret = method(*args, **kwargs)
rpc.response_queue.put((seq, ret))
else:
handle_tk_events()
except KeyboardInterrupt:
if quitting:
exit_now = True
continue
except SystemExit:
capture_warnings(False)
raise
except:
type, value, tb = sys.exc_info()
try:
print_exception()
rpc.response_queue.put((seq, None))
except:
# Link didn't work, print same exception to __stderr__
traceback.print_exception(type, value, tb, file=sys.__stderr__)
exit()
else:
continue
def manage_socket(address):
for i in range(3):
time.sleep(i)
try:
server = MyRPCServer(address, MyHandler)
break
except OSError as err:
print("IDLE Subprocess: OSError: " + err.args[1] +
", retrying....", file=sys.__stderr__)
socket_error = err
else:
print("IDLE Subprocess: Connection to "
"IDLE GUI failed, exiting.", file=sys.__stderr__)
show_socket_error(socket_error, address)
global exit_now
exit_now = True
return
server.handle_request() # A single request only
def show_socket_error(err, address):
"Display socket error from manage_socket."
import tkinter
from tkinter.messagebox import showerror
root = tkinter.Tk()
fix_scaling(root)
root.withdraw()
showerror(
"Subprocess Connection Error",
f"IDLE's subprocess can't connect to {address[0]}:{address[1]}.\n"
f"Fatal OSError #{err.errno}: {err.strerror}.\n"
"See the 'Startup failure' section of the IDLE doc, online at\n"
"https://docs.python.org/3/library/idle.html#startup-failure",
parent=root)
root.destroy()
def get_message_lines(typ, exc, tb):
"Return line composing the exception message."
if typ in (AttributeError, NameError):
# 3.10+ hints are not directly accessible from python (#44026).
err = io.StringIO()
with contextlib.redirect_stderr(err):
sys.__excepthook__(typ, exc, tb)
return [err.getvalue().split("\n")[-2] + "\n"]
else:
return traceback.format_exception_only(typ, exc)
def print_exception():
import linecache
linecache.checkcache()
flush_stdout()
efile = sys.stderr
typ, val, tb = excinfo = sys.exc_info()
sys.last_type, sys.last_value, sys.last_traceback = excinfo
sys.last_exc = val
seen = set()
def print_exc(typ, exc, tb):
seen.add(id(exc))
context = exc.__context__
cause = exc.__cause__
if cause is not None and id(cause) not in seen:
print_exc(type(cause), cause, cause.__traceback__)
print("\nThe above exception was the direct cause "
"of the following exception:\n", file=efile)
elif (context is not None and
not exc.__suppress_context__ and
id(context) not in seen):
print_exc(type(context), context, context.__traceback__)
print("\nDuring handling of the above exception, "
"another exception occurred:\n", file=efile)
if tb:
tbe = traceback.extract_tb(tb)
print('Traceback (most recent call last):', file=efile)
exclude = ("run.py", "rpc.py", "threading.py", "queue.py",
"debugger_r.py", "bdb.py")
cleanup_traceback(tbe, exclude)
traceback.print_list(tbe, file=efile)
lines = get_message_lines(typ, exc, tb)
for line in lines:
print(line, end='', file=efile)
print_exc(typ, val, tb)
def cleanup_traceback(tb, exclude):
"Remove excluded traces from beginning/end of tb; get cached lines"
orig_tb = tb[:]
while tb:
for rpcfile in exclude:
if tb[0][0].count(rpcfile):
break # found an exclude, break for: and delete tb[0]
else:
break # no excludes, have left RPC code, break while:
del tb[0]
while tb:
for rpcfile in exclude:
if tb[-1][0].count(rpcfile):
break
else:
break
del tb[-1]
if len(tb) == 0:
# exception was in IDLE internals, don't prune!
tb[:] = orig_tb[:]
print("** IDLE Internal Exception: ", file=sys.stderr)
rpchandler = rpc.objecttable['exec'].rpchandler
for i in range(len(tb)):
fn, ln, nm, line = tb[i]
if nm == '?':
nm = "-toplevel-"
if not line and fn.startswith("<pyshell#"):
line = rpchandler.remotecall('linecache', 'getline',
(fn, ln), {})
tb[i] = fn, ln, nm, line
def flush_stdout():
"""XXX How to do this now?"""
def exit():
"""Exit subprocess, possibly after first clearing exit functions.
If config-main.cfg/.def 'General' 'delete-exitfunc' is True, then any
functions registered with atexit will be removed before exiting.
(VPython support)
"""
if no_exitfunc:
import atexit
atexit._clear()
capture_warnings(False)
sys.exit(0)
def fix_scaling(root):
"""Scale fonts on HiDPI displays."""
import tkinter.font
scaling = float(root.tk.call('tk', 'scaling'))
if scaling > 1.4:
for name in tkinter.font.names(root):
font = tkinter.font.Font(root=root, name=name, exists=True)
size = int(font['size'])
if size < 0:
font['size'] = round(-0.75*size)
def fixdoc(fun, text):
tem = (fun.__doc__ + '\n\n') if fun.__doc__ is not None else ''
fun.__doc__ = tem + textwrap.fill(textwrap.dedent(text))
RECURSIONLIMIT_DELTA = 30
def install_recursionlimit_wrappers():
"""Install wrappers to always add 30 to the recursion limit."""
# see: bpo-26806
@functools.wraps(sys.setrecursionlimit)
def setrecursionlimit(*args, **kwargs):
# mimic the original sys.setrecursionlimit()'s input handling
if kwargs:
raise TypeError(
"setrecursionlimit() takes no keyword arguments")
try:
limit, = args
except ValueError:
raise TypeError(f"setrecursionlimit() takes exactly one "
f"argument ({len(args)} given)")
if not limit > 0:
raise ValueError(
"recursion limit must be greater or equal than 1")
return setrecursionlimit.__wrapped__(limit + RECURSIONLIMIT_DELTA)
fixdoc(setrecursionlimit, f"""\
This IDLE wrapper adds {RECURSIONLIMIT_DELTA} to prevent possible
uninterruptible loops.""")
@functools.wraps(sys.getrecursionlimit)
def getrecursionlimit():
return getrecursionlimit.__wrapped__() - RECURSIONLIMIT_DELTA
fixdoc(getrecursionlimit, f"""\
This IDLE wrapper subtracts {RECURSIONLIMIT_DELTA} to compensate
for the {RECURSIONLIMIT_DELTA} IDLE adds when setting the limit.""")
# add the delta to the default recursion limit, to compensate
sys.setrecursionlimit(sys.getrecursionlimit() + RECURSIONLIMIT_DELTA)
sys.setrecursionlimit = setrecursionlimit
sys.getrecursionlimit = getrecursionlimit
def uninstall_recursionlimit_wrappers():
"""Uninstall the recursion limit wrappers from the sys module.
IDLE only uses this for tests. Users can import run and call
this to remove the wrapping.
"""
if (
getattr(sys.setrecursionlimit, '__wrapped__', None) and
getattr(sys.getrecursionlimit, '__wrapped__', None)
):
sys.setrecursionlimit = sys.setrecursionlimit.__wrapped__
sys.getrecursionlimit = sys.getrecursionlimit.__wrapped__
sys.setrecursionlimit(sys.getrecursionlimit() - RECURSIONLIMIT_DELTA)
class MyRPCServer(rpc.RPCServer):
def handle_error(self, request, client_address):
"""Override RPCServer method for IDLE
Interrupt the MainThread and exit server if link is dropped.
"""
global quitting
try:
raise
except SystemExit:
raise
except EOFError:
global exit_now
exit_now = True
thread.interrupt_main()
except:
erf = sys.__stderr__
print(textwrap.dedent(f"""
{'-'*40}
Unhandled exception in user code execution server!'
Thread: {threading.current_thread().name}
IDLE Client Address: {client_address}
Request: {request!r}
"""), file=erf)
traceback.print_exc(limit=-20, file=erf)
print(textwrap.dedent(f"""
*** Unrecoverable, server exiting!
Users should never see this message; it is likely transient.
If this recurs, report this with a copy of the message
and an explanation of how to make it repeat.
{'-'*40}"""), file=erf)
quitting = True
thread.interrupt_main()
# Pseudofiles for shell-remote communication (also used in pyshell)
class StdioFile(io.TextIOBase):
def __init__(self, shell, tags, encoding='utf-8', errors='strict'):
self.shell = shell
# GH-78889: accessing unpickleable attributes freezes Shell.
# IDLE only needs methods; allow 'width' for possible use.
self.shell._RPCProxy__attributes = {'width': 1}
self.tags = tags
self._encoding = encoding
self._errors = errors
@property
def encoding(self):
return self._encoding
@property
def errors(self):
return self._errors
@property
def name(self):
return '<%s>' % self.tags
def isatty(self):
return True
class StdOutputFile(StdioFile):
def writable(self):
return True
def write(self, s):
if self.closed:
raise ValueError("write to closed file")
s = str.encode(s, self.encoding, self.errors).decode(self.encoding, self.errors)
return self.shell.write(s, self.tags)
class StdInputFile(StdioFile):
_line_buffer = ''
def readable(self):
return True
def read(self, size=-1):
if self.closed:
raise ValueError("read from closed file")
if size is None:
size = -1
elif not isinstance(size, int):
raise TypeError('must be int, not ' + type(size).__name__)
result = self._line_buffer
self._line_buffer = ''
if size < 0:
while line := self.shell.readline():
result += line
else:
while len(result) < size:
line = self.shell.readline()
if not line: break
result += line
self._line_buffer = result[size:]
result = result[:size]
return result
def readline(self, size=-1):
if self.closed:
raise ValueError("read from closed file")
if size is None:
size = -1
elif not isinstance(size, int):
raise TypeError('must be int, not ' + type(size).__name__)
line = self._line_buffer or self.shell.readline()
if size < 0:
size = len(line)
eol = line.find('\n', 0, size)
if eol >= 0:
size = eol + 1
self._line_buffer = line[size:]
return line[:size]
def close(self):
self.shell.close()
class MyHandler(rpc.RPCHandler):
def handle(self):
"""Override base method"""
executive = Executive(self)
self.register("exec", executive)
self.console = self.get_remote_proxy("console")
sys.stdin = StdInputFile(self.console, "stdin",
iomenu.encoding, iomenu.errors)
sys.stdout = StdOutputFile(self.console, "stdout",
iomenu.encoding, iomenu.errors)
sys.stderr = StdOutputFile(self.console, "stderr",
iomenu.encoding, "backslashreplace")
sys.displayhook = rpc.displayhook
# page help() text to shell.
import pydoc # import must be done here to capture i/o binding
pydoc.pager = pydoc.plainpager
# Keep a reference to stdin so that it won't try to exit IDLE if
# sys.stdin gets changed from within IDLE's shell. See issue17838.
self._keep_stdin = sys.stdin
install_recursionlimit_wrappers()
self.interp = self.get_remote_proxy("interp")
rpc.RPCHandler.getresponse(self, myseq=None, wait=0.05)
def exithook(self):
"override SocketIO method - wait for MainThread to shut us down"
time.sleep(10)
def EOFhook(self):
"Override SocketIO method - terminate wait on callback and exit thread"
global quitting
quitting = True
thread.interrupt_main()
def decode_interrupthook(self):
"interrupt awakened thread"
global quitting
quitting = True
thread.interrupt_main()
class Executive:
def __init__(self, rpchandler):
self.rpchandler = rpchandler
if idlelib.testing is False:
self.locals = __main__.__dict__
self.calltip = calltip.Calltip()
self.autocomplete = autocomplete.AutoComplete()
else:
self.locals = {}
def runcode(self, code):
global interruptible
try:
self.user_exc_info = None
interruptible = True
try:
exec(code, self.locals)
finally:
interruptible = False
except SystemExit as e:
if e.args: # SystemExit called with an argument.
ob = e.args[0]
if not isinstance(ob, (type(None), int)):
print('SystemExit: ' + str(ob), file=sys.stderr)
# Return to the interactive prompt.
except:
self.user_exc_info = sys.exc_info() # For testing, hook, viewer.
if quitting:
exit()
if sys.excepthook is sys.__excepthook__:
print_exception()
else:
try:
sys.excepthook(*self.user_exc_info)
except:
self.user_exc_info = sys.exc_info() # For testing.
print_exception()
jit = self.rpchandler.console.getvar("<<toggle-jit-stack-viewer>>")
if jit:
self.rpchandler.interp.open_remote_stack_viewer()
else:
flush_stdout()
def interrupt_the_server(self):
if interruptible:
thread.interrupt_main()
def start_the_debugger(self, gui_adap_oid):
return debugger_r.start_debugger(self.rpchandler, gui_adap_oid)
def stop_the_debugger(self, idb_adap_oid):
"Unregister the Idb Adapter. Link objects and Idb then subject to GC"
self.rpchandler.unregister(idb_adap_oid)
def get_the_calltip(self, name):
return self.calltip.fetch_tip(name)
def get_the_completion_list(self, what, mode):
return self.autocomplete.fetch_completions(what, mode)
def stackviewer(self, flist_oid=None):
if self.user_exc_info:
_, exc, tb = self.user_exc_info
else:
return None
flist = None
if flist_oid is not None:
flist = self.rpchandler.get_remote_proxy(flist_oid)
while tb and tb.tb_frame.f_globals["__name__"] in ["rpc", "run"]:
tb = tb.tb_next
exc.__traceback__ = tb
item = stackviewer.StackTreeItem(exc, flist)
return debugobj_r.remote_object_tree_item(item)
if __name__ == '__main__':
from unittest import main
main('idlelib.idle_test.test_run', verbosity=2)
capture_warnings(False) # Make sure turned off; see bpo-18081.
@@ -0,0 +1,213 @@
"""Execute code from an editor.
Check module: do a full syntax check of the current module.
Also run the tabnanny to catch any inconsistent tabs.
Run module: also execute the module's code in the __main__ namespace.
The window must have been saved previously. The module is added to
sys.modules, and is also added to the __main__ namespace.
TODO: Specify command line arguments in a dialog box.
"""
import os
import tabnanny
import time
import tokenize
from tkinter import messagebox
from idlelib.config import idleConf
from idlelib import macosx
from idlelib import pyshell
from idlelib.query import CustomRun
from idlelib import outwin
indent_message = """Error: Inconsistent indentation detected!
1) Your indentation is outright incorrect (easy to fix), OR
2) Your indentation mixes tabs and spaces.
To fix case 2, change all tabs to spaces by using Edit->Select All followed \
by Format->Untabify Region and specify the number of columns used by each tab.
"""
class ScriptBinding:
def __init__(self, editwin):
self.editwin = editwin
# Provide instance variables referenced by debugger
# XXX This should be done differently
self.flist = self.editwin.flist
self.root = self.editwin.root
# cli_args is list of strings that extends sys.argv
self.cli_args = []
self.perf = 0.0 # Workaround for macOS 11 Uni2; see bpo-42508.
def check_module_event(self, event):
if isinstance(self.editwin, outwin.OutputWindow):
self.editwin.text.bell()
return 'break'
filename = self.getfilename()
if not filename:
return 'break'
if not self.checksyntax(filename):
return 'break'
if not self.tabnanny(filename):
return 'break'
return "break"
def tabnanny(self, filename):
# XXX: tabnanny should work on binary files as well
with tokenize.open(filename) as f:
try:
tabnanny.process_tokens(tokenize.generate_tokens(f.readline))
except tokenize.TokenError as msg:
msgtxt, (lineno, start) = msg.args
self.editwin.gotoline(lineno)
self.errorbox("Tabnanny Tokenizing Error",
"Token Error: %s" % msgtxt)
return False
except tabnanny.NannyNag as nag:
# The error messages from tabnanny are too confusing...
self.editwin.gotoline(nag.get_lineno())
self.errorbox("Tab/space error", indent_message)
return False
return True
def checksyntax(self, filename):
self.shell = shell = self.flist.open_shell()
saved_stream = shell.get_warning_stream()
shell.set_warning_stream(shell.stderr)
with open(filename, 'rb') as f:
source = f.read()
if b'\r' in source:
source = source.replace(b'\r\n', b'\n')
source = source.replace(b'\r', b'\n')
if source and source[-1] != ord(b'\n'):
source = source + b'\n'
editwin = self.editwin
text = editwin.text
text.tag_remove("ERROR", "1.0", "end")
try:
# If successful, return the compiled code
return compile(source, filename, "exec")
except (SyntaxError, OverflowError, ValueError) as value:
msg = getattr(value, 'msg', '') or value or "<no detail available>"
lineno = getattr(value, 'lineno', '') or 1
offset = getattr(value, 'offset', '') or 0
if offset == 0:
lineno += 1 #mark end of offending line
pos = "0.0 + %d lines + %d chars" % (lineno-1, offset-1)
editwin.colorize_syntax_error(text, pos)
self.errorbox("SyntaxError", "%-20s" % msg)
return False
finally:
shell.set_warning_stream(saved_stream)
def run_custom_event(self, event):
return self.run_module_event(event, customize=True)
def run_module_event(self, event, *, customize=False):
"""Run the module after setting up the environment.
First check the syntax. Next get customization. If OK, make
sure the shell is active and then transfer the arguments, set
the run environment's working directory to the directory of the
module being executed and also add that directory to its
sys.path if not already included.
"""
if macosx.isCocoaTk() and (time.perf_counter() - self.perf < .05):
return 'break'
if isinstance(self.editwin, outwin.OutputWindow):
self.editwin.text.bell()
return 'break'
filename = self.getfilename()
if not filename:
return 'break'
code = self.checksyntax(filename)
if not code:
return 'break'
if not self.tabnanny(filename):
return 'break'
if customize:
title = f"Customize {self.editwin.short_title()} Run"
run_args = CustomRun(self.shell.text, title,
cli_args=self.cli_args).result
if not run_args: # User cancelled.
return 'break'
self.cli_args, restart = run_args if customize else ([], True)
interp = self.shell.interp
if pyshell.use_subprocess and restart:
interp.restart_subprocess(
with_cwd=False, filename=filename)
dirname = os.path.dirname(filename)
argv = [filename]
if self.cli_args:
argv += self.cli_args
interp.runcommand(f"""if 1:
__file__ = {filename!r}
import sys as _sys
from os.path import basename as _basename
argv = {argv!r}
if (not _sys.argv or
_basename(_sys.argv[0]) != _basename(__file__) or
len(argv) > 1):
_sys.argv = argv
import os as _os
_os.chdir({dirname!r})
del _sys, argv, _basename, _os
\n""")
interp.prepend_syspath(filename)
# XXX KBK 03Jul04 When run w/o subprocess, runtime warnings still
# go to __stderr__. With subprocess, they go to the shell.
# Need to change streams in pyshell.ModifiedInterpreter.
interp.runcode(code)
return 'break'
def getfilename(self):
"""Get source filename. If not saved, offer to save (or create) file
The debugger requires a source file. Make sure there is one, and that
the current version of the source buffer has been saved. If the user
declines to save or cancels the Save As dialog, return None.
If the user has configured IDLE for Autosave, the file will be
silently saved if it already exists and is dirty.
"""
filename = self.editwin.io.filename
if not self.editwin.get_saved():
autosave = idleConf.GetOption('main', 'General',
'autosave', type='bool')
if autosave and filename:
self.editwin.io.save(None)
else:
confirm = self.ask_save_dialog()
self.editwin.text.focus_set()
if confirm:
self.editwin.io.save(None)
filename = self.editwin.io.filename
else:
filename = None
return filename
def ask_save_dialog(self):
msg = "Source Must Be Saved\n" + 5*' ' + "OK to Save?"
confirm = messagebox.askokcancel(title="Save Before Run or Check",
message=msg,
default=messagebox.OK,
parent=self.editwin.text)
return confirm
def errorbox(self, title, message):
# XXX This should really be a function of EditorWindow...
messagebox.showerror(title, message, parent=self.editwin.text)
self.editwin.text.focus_set()
self.perf = time.perf_counter()
if __name__ == "__main__":
from unittest import main
main('idlelib.idle_test.test_runscript', verbosity=2,)
@@ -0,0 +1,151 @@
from tkinter import *
from tkinter.ttk import Frame, Scrollbar
from idlelib import macosx
class ScrolledList:
default = "(None)"
def __init__(self, master, **options):
# Create top frame, with scrollbar and listbox
self.master = master
self.frame = frame = Frame(master)
self.frame.pack(fill="both", expand=1)
self.vbar = vbar = Scrollbar(frame, name="vbar")
self.vbar.pack(side="right", fill="y")
self.listbox = listbox = Listbox(frame, exportselection=0,
background="white")
if options:
listbox.configure(options)
listbox.pack(expand=1, fill="both")
# Tie listbox and scrollbar together
vbar["command"] = listbox.yview
listbox["yscrollcommand"] = vbar.set
# Bind events to the list box
listbox.bind("<ButtonRelease-1>", self.click_event)
listbox.bind("<Double-ButtonRelease-1>", self.double_click_event)
if macosx.isAquaTk():
listbox.bind("<ButtonPress-2>", self.popup_event)
listbox.bind("<Control-Button-1>", self.popup_event)
else:
listbox.bind("<ButtonPress-3>", self.popup_event)
listbox.bind("<Key-Up>", self.up_event)
listbox.bind("<Key-Down>", self.down_event)
# Mark as empty
self.clear()
def close(self):
self.frame.destroy()
def clear(self):
self.listbox.delete(0, "end")
self.empty = 1
self.listbox.insert("end", self.default)
def append(self, item):
if self.empty:
self.listbox.delete(0, "end")
self.empty = 0
self.listbox.insert("end", str(item))
def get(self, index):
return self.listbox.get(index)
def click_event(self, event):
self.listbox.activate("@%d,%d" % (event.x, event.y))
index = self.listbox.index("active")
self.select(index)
self.on_select(index)
return "break"
def double_click_event(self, event):
index = self.listbox.index("active")
self.select(index)
self.on_double(index)
return "break"
menu = None
def popup_event(self, event):
if not self.menu:
self.make_menu()
menu = self.menu
self.listbox.activate("@%d,%d" % (event.x, event.y))
index = self.listbox.index("active")
self.select(index)
menu.tk_popup(event.x_root, event.y_root)
return "break"
def make_menu(self):
menu = Menu(self.listbox, tearoff=0)
self.menu = menu
self.fill_menu()
def up_event(self, event):
index = self.listbox.index("active")
if self.listbox.selection_includes(index):
index = index - 1
else:
index = self.listbox.size() - 1
if index < 0:
self.listbox.bell()
else:
self.select(index)
self.on_select(index)
return "break"
def down_event(self, event):
index = self.listbox.index("active")
if self.listbox.selection_includes(index):
index = index + 1
else:
index = 0
if index >= self.listbox.size():
self.listbox.bell()
else:
self.select(index)
self.on_select(index)
return "break"
def select(self, index):
self.listbox.focus_set()
self.listbox.activate(index)
self.listbox.selection_clear(0, "end")
self.listbox.selection_set(index)
self.listbox.see(index)
# Methods to override for specific actions
def fill_menu(self):
pass
def on_select(self, index):
pass
def on_double(self, index):
pass
def _scrolled_list(parent): # htest #
top = Toplevel(parent)
x, y = map(int, parent.geometry().split('+')[1:])
top.geometry("+%d+%d" % (x+200, y + 175))
class MyScrolledList(ScrolledList):
def fill_menu(self): self.menu.add_command(label="right click")
def on_select(self, index): print("select", self.get(index))
def on_double(self, index): print("double", self.get(index))
scrolled_list = MyScrolledList(top)
for i in range(30):
scrolled_list.append("Item %02d" % i)
if __name__ == '__main__':
from unittest import main
main('idlelib.idle_test.test_scrolledlist', verbosity=2, exit=False)
from idlelib.idle_test.htest import run
run(_scrolled_list)
@@ -0,0 +1,165 @@
"""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):
"""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"):
engine._searchdialog = SearchDialog(root, engine)
return engine._searchdialog
def find(text):
"""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):
"""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):
"""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)
# TODO - why is this here and not in a create_command_buttons?
self.make_button("Find Next", self.default_command, isdef=True)
def default_command(self, event=None):
"Handle the Find Next button as the default command."
if not self.engine.getprog():
return
self.find_again(self.text)
def find_again(self, text):
"""Repeat the last search.
If no search was previously run, open a new search dialog. In
this case, no search is done.
If a search was previously run, the search dialog won't be
shown and the options from the previous search (including the
search pattern) will be used to find the next occurrence
of the pattern. Next is relative based on direction.
Position the window to display the located occurrence in the
text.
Return True if the search was successful and False otherwise.
"""
if not self.engine.getpat():
self.open(text)
return False
if not self.engine.getprog():
return False
res = self.engine.search_text(text)
if res:
line, m = res
i, j = m.span()
first = "%d.%d" % (line, i)
last = "%d.%d" % (line, j)
try:
selfirst = text.index("sel.first")
sellast = text.index("sel.last")
if selfirst == first and sellast == last:
self.bell()
return False
except TclError:
pass
text.tag_remove("sel", "1.0", "end")
text.tag_add("sel", first, last)
text.mark_set("insert", self.engine.isback() and first or last)
text.see("insert")
return True
else:
self.bell()
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)
return self.find_again(text)
def _search_dialog(parent): # htest #
"Display search test box."
from tkinter import Toplevel, Text
from tkinter.ttk import Frame, Button
top = Toplevel(parent)
top.title("Test SearchDialog")
x, y = map(int, parent.geometry().split('+')[1:])
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)
def show_find():
text.tag_add('sel', '1.0', 'end')
_setup(text).open(text)
text.tag_remove('sel', '1.0', 'end')
button = Button(frame, text="Search (selection ignored)", command=show_find)
button.pack()
if __name__ == '__main__':
from unittest import main
main('idlelib.idle_test.test_search', verbosity=2, exit=False)
from idlelib.idle_test.htest import run
run(_search_dialog)
@@ -0,0 +1,210 @@
'''Define SearchDialogBase used by Search, Replace, and Grep dialogs.'''
from tkinter import Toplevel
from tkinter.ttk import Frame, Entry, Label, Button, Checkbutton, Radiobutton
from tkinter.simpledialog import _setup_dialog
class SearchDialogBase:
'''Create most of a 3 or 4 row, 3 column search dialog.
The left and wide middle column contain:
1 or 2 labeled text entry lines (make_entry, create_entries);
a row of standard Checkbuttons (make_frame, create_option_buttons),
each of which corresponds to a search engine Variable;
a row of dialog-specific Check/Radiobuttons (create_other_buttons).
The narrow right column contains command buttons
(make_button, create_command_buttons).
These are bound to functions that execute the command.
Except for command buttons, this base class is not limited to items
common to all three subclasses. Rather, it is the Find dialog minus
the "Find Next" command, its execution function, and the
default_command attribute needed in create_widgets. The other
dialogs override attributes and methods, the latter to replace and
add widgets.
'''
title = "Search Dialog" # replace in subclasses
icon = "Search"
needwrapbutton = 1 # not in Find in Files
def __init__(self, root, engine):
'''Initialize root, engine, and top attributes.
top (level widget): set in create_widgets() called from open().
frame: container for all widgets in dialog.
text (Text searched): set in open(), only used in subclasses().
ent (ry): created in make_entry() called from create_entry().
row (of grid): 0 in create_widgets(), +1 in make_entry/frame().
default_command: set in subclasses, used in create_widgets().
title (of dialog): class attribute, override in subclasses.
icon (of dialog): ditto, use unclear if cannot minimize dialog.
'''
self.root = root
self.bell = root.bell
self.engine = engine
self.top = None
def open(self, text, searchphrase=None):
"Make dialog visible on top of others and ready to use."
self.text = text
if not self.top:
self.create_widgets()
else:
self.top.deiconify()
self.top.tkraise()
self.top.transient(text.winfo_toplevel())
if searchphrase:
self.ent.delete(0,"end")
self.ent.insert("end",searchphrase)
self.ent.focus_set()
self.ent.selection_range(0, "end")
self.ent.icursor(0)
self.top.grab_set()
def close(self, event=None):
"Put dialog away for later use."
if self.top:
self.top.grab_release()
self.top.transient('')
self.top.withdraw()
def create_widgets(self):
'''Create basic 3 row x 3 col search (find) dialog.
Other dialogs override subsidiary create_x methods as needed.
Replace and Find-in-Files add another entry row.
'''
top = Toplevel(self.root)
top.bind("<Return>", self.default_command)
top.bind("<Escape>", self.close)
top.protocol("WM_DELETE_WINDOW", self.close)
top.wm_title(self.title)
top.wm_iconname(self.icon)
_setup_dialog(top)
self.top = top
self.frame = Frame(top, padding=5)
self.frame.grid(sticky="nwes")
top.grid_columnconfigure(0, weight=100)
top.grid_rowconfigure(0, weight=100)
self.row = 0
self.frame.grid_columnconfigure(0, pad=2, weight=0)
self.frame.grid_columnconfigure(1, pad=2, minsize=100, weight=100)
self.create_entries() # row 0 (and maybe 1), cols 0, 1
self.create_option_buttons() # next row, cols 0, 1
self.create_other_buttons() # next row, cols 0, 1
self.create_command_buttons() # col 2, all rows
def make_entry(self, label_text, var):
'''Return (entry, label), .
entry - gridded labeled Entry for text entry.
label - Label widget, returned for testing.
'''
label = Label(self.frame, text=label_text)
label.grid(row=self.row, column=0, sticky="nw")
entry = Entry(self.frame, textvariable=var, exportselection=0)
entry.grid(row=self.row, column=1, sticky="nwe")
self.row = self.row + 1
return entry, label
def create_entries(self):
"Create one or more entry lines with make_entry."
self.ent = self.make_entry("Find:", self.engine.patvar)[0]
def make_frame(self,labeltext=None):
'''Return (frame, label).
frame - gridded labeled Frame for option or other buttons.
label - Label widget, returned for testing.
'''
if labeltext:
label = Label(self.frame, text=labeltext)
label.grid(row=self.row, column=0, sticky="nw")
else:
label = ''
frame = Frame(self.frame)
frame.grid(row=self.row, column=1, columnspan=1, sticky="nwe")
self.row = self.row + 1
return frame, label
def create_option_buttons(self):
'''Return (filled frame, options) for testing.
Options is a list of searchengine booleanvar, label pairs.
A gridded frame from make_frame is filled with a Checkbutton
for each pair, bound to the var, with the corresponding label.
'''
frame = self.make_frame("Options")[0]
engine = self.engine
options = [(engine.revar, "Regular expression"),
(engine.casevar, "Match case"),
(engine.wordvar, "Whole word")]
if self.needwrapbutton:
options.append((engine.wrapvar, "Wrap around"))
for var, label in options:
btn = Checkbutton(frame, variable=var, text=label)
btn.pack(side="left", fill="both")
return frame, options
def create_other_buttons(self):
'''Return (frame, others) for testing.
Others is a list of value, label pairs.
A gridded frame from make_frame is filled with radio buttons.
'''
frame = self.make_frame("Direction")[0]
var = self.engine.backvar
others = [(1, 'Up'), (0, 'Down')]
for val, label in others:
btn = Radiobutton(frame, variable=var, value=val, text=label)
btn.pack(side="left", fill="both")
return frame, others
def make_button(self, label, command, isdef=0):
"Return command button gridded in command frame."
b = Button(self.buttonframe,
text=label, command=command,
default=isdef and "active" or "normal")
cols,rows=self.buttonframe.grid_size()
b.grid(pady=1,row=rows,column=0,sticky="ew")
self.buttonframe.grid(rowspan=rows+1)
return b
def create_command_buttons(self):
"Place buttons in vertical command frame gridded on right."
f = self.buttonframe = Frame(self.frame)
f.grid(row=0,column=2,padx=2,pady=2,ipadx=2,ipady=2)
b = self.make_button("Close", self.close)
b.lower()
class _searchbase(SearchDialogBase): # htest #
"Create auto-opening dialog with no text connection."
def __init__(self, parent):
import re
from idlelib import searchengine
self.root = parent
self.engine = searchengine.get(parent)
self.create_widgets()
print(parent.geometry())
width,height, x,y = list(map(int, re.split('[x+]', parent.geometry())))
self.top.geometry("+%d+%d" % (x + 40, y + 175))
def default_command(self, dummy): pass
if __name__ == '__main__':
from unittest import main
main('idlelib.idle_test.test_searchbase', verbosity=2, exit=False)
from idlelib.idle_test.htest import run
run(_searchbase)
@@ -0,0 +1,235 @@
'''Define SearchEngine for search dialogs.'''
import re
re.PatternError = re.error # New in 3.13.
from tkinter import StringVar, BooleanVar, TclError
from tkinter import messagebox
def get(root):
'''Return the singleton SearchEngine instance for the process.
The single SearchEngine saves settings between dialog instances.
If there is not a SearchEngine already, make one.
'''
if not hasattr(root, "_searchengine"):
root._searchengine = SearchEngine(root)
# This creates a cycle that persists until root is deleted.
return root._searchengine
class SearchEngine:
"""Handles searching a text widget for Find, Replace, and Grep."""
def __init__(self, root):
'''Initialize Variables that save search state.
The dialogs bind these to the UI elements present in the dialogs.
'''
self.root = root # need for report_error()
self.patvar = StringVar(root, '') # search pattern
self.revar = BooleanVar(root, False) # regular expression?
self.casevar = BooleanVar(root, False) # match case?
self.wordvar = BooleanVar(root, False) # match whole word?
self.wrapvar = BooleanVar(root, True) # wrap around buffer?
self.backvar = BooleanVar(root, False) # search backwards?
# Access methods
def getpat(self):
return self.patvar.get()
def setpat(self, pat):
self.patvar.set(pat)
def isre(self):
return self.revar.get()
def iscase(self):
return self.casevar.get()
def isword(self):
return self.wordvar.get()
def iswrap(self):
return self.wrapvar.get()
def isback(self):
return self.backvar.get()
# Higher level access methods
def setcookedpat(self, pat):
"Set pattern after escaping if re."
# called only in search.py: 66
if self.isre():
pat = re.escape(pat)
self.setpat(pat)
def getcookedpat(self):
pat = self.getpat()
if not self.isre(): # if True, see setcookedpat
pat = re.escape(pat)
if self.isword():
pat = r"\b%s\b" % pat
return pat
def getprog(self):
"Return compiled cooked search pattern."
pat = self.getpat()
if not pat:
self.report_error(pat, "Empty regular expression")
return None
pat = self.getcookedpat()
flags = 0
if not self.iscase():
flags = flags | re.IGNORECASE
try:
prog = re.compile(pat, flags)
except re.PatternError as e:
self.report_error(pat, e.msg, e.pos)
return None
return prog
def report_error(self, pat, msg, col=None):
# Derived class could override this with something fancier
msg = "Error: " + str(msg)
if pat:
msg = msg + "\nPattern: " + str(pat)
if col is not None:
msg = msg + "\nOffset: " + str(col)
messagebox.showerror("Regular expression error",
msg, master=self.root)
def search_text(self, text, prog=None, ok=0):
'''Return (lineno, matchobj) or None for forward/backward search.
This function calls the right function with the right arguments.
It directly return the result of that call.
Text is a text widget. Prog is a precompiled pattern.
The ok parameter is a bit complicated as it has two effects.
If there is a selection, the search begin at either end,
depending on the direction setting and ok, with ok meaning that
the search starts with the selection. Otherwise, search begins
at the insert mark.
To aid progress, the search functions do not return an empty
match at the starting position unless ok is True.
'''
if not prog:
prog = self.getprog()
if not prog:
return None # Compilation failed -- stop
wrap = self.wrapvar.get()
first, last = get_selection(text)
if self.isback():
if ok:
start = last
else:
start = first
line, col = get_line_col(start)
res = self.search_backward(text, prog, line, col, wrap, ok)
else:
if ok:
start = first
else:
start = last
line, col = get_line_col(start)
res = self.search_forward(text, prog, line, col, wrap, ok)
return res
def search_forward(self, text, prog, line, col, wrap, ok=0):
wrapped = 0
startline = line
chars = text.get("%d.0" % line, "%d.0" % (line+1))
while chars:
m = prog.search(chars[:-1], col)
if m:
if ok or m.end() > col:
return line, m
line = line + 1
if wrapped and line > startline:
break
col = 0
ok = 1
chars = text.get("%d.0" % line, "%d.0" % (line+1))
if not chars and wrap:
wrapped = 1
wrap = 0
line = 1
chars = text.get("1.0", "2.0")
return None
def search_backward(self, text, prog, line, col, wrap, ok=0):
wrapped = 0
startline = line
chars = text.get("%d.0" % line, "%d.0" % (line+1))
while True:
m = search_reverse(prog, chars[:-1], col)
if m:
if ok or m.start() < col:
return line, m
line = line - 1
if wrapped and line < startline:
break
ok = 1
if line <= 0:
if not wrap:
break
wrapped = 1
wrap = 0
pos = text.index("end-1c")
line, col = map(int, pos.split("."))
chars = text.get("%d.0" % line, "%d.0" % (line+1))
col = len(chars) - 1
return None
def search_reverse(prog, chars, col):
'''Search backwards and return an re match object or None.
This is done by searching forwards until there is no match.
Prog: compiled re object with a search method returning a match.
Chars: line of text, without \\n.
Col: stop index for the search; the limit for match.end().
'''
m = prog.search(chars)
if not m:
return None
found = None
i, j = m.span() # m.start(), m.end() == match slice indexes
while i < col and j <= col:
found = m
if i == j:
j = j+1
m = prog.search(chars, j)
if not m:
break
i, j = m.span()
return found
def get_selection(text):
'''Return tuple of 'line.col' indexes from selection or insert mark.
'''
try:
first = text.index("sel.first")
last = text.index("sel.last")
except TclError:
first = last = None
if not first:
first = text.index("insert")
if not last:
last = first
return first, last
def get_line_col(index):
'''Return (line, col) tuple of ints from 'line.col' string.'''
line, col = map(int, index.split(".")) # Fails on invalid index
return line, col
if __name__ == "__main__":
from unittest import main
main('idlelib.idle_test.test_searchengine', verbosity=2)
@@ -0,0 +1,544 @@
"""Line numbering implementation for IDLE as an extension.
Includes BaseSideBar which can be extended for other sidebar based extensions
"""
import contextlib
import functools
import itertools
import tkinter as tk
from tkinter.font import Font
from idlelib.config import idleConf
from idlelib.delegator import Delegator
from idlelib import macosx
def get_lineno(text, index):
"""Return the line number of an index in a Tk text widget."""
text_index = text.index(index)
return int(float(text_index)) if text_index else None
def get_end_linenumber(text):
"""Return the number of the last line in a Tk text widget."""
return get_lineno(text, 'end-1c')
def get_displaylines(text, index):
"""Display height, in lines, of a logical line in a Tk text widget."""
res = text.count(f"{index} linestart",
f"{index} lineend",
"displaylines")
return res[0] if res else 0
def get_widget_padding(widget):
"""Get the total padding of a Tk widget, including its border."""
# TODO: use also in codecontext.py
manager = widget.winfo_manager()
if manager == 'pack':
info = widget.pack_info()
elif manager == 'grid':
info = widget.grid_info()
else:
raise ValueError(f"Unsupported geometry manager: {manager}")
# All values are passed through getint(), since some
# values may be pixel objects, which can't simply be added to ints.
padx = sum(map(widget.tk.getint, [
info['padx'],
widget.cget('padx'),
widget.cget('border'),
]))
pady = sum(map(widget.tk.getint, [
info['pady'],
widget.cget('pady'),
widget.cget('border'),
]))
return padx, pady
@contextlib.contextmanager
def temp_enable_text_widget(text):
text.configure(state=tk.NORMAL)
try:
yield
finally:
text.configure(state=tk.DISABLED)
class BaseSideBar:
"""A base class for sidebars using Text."""
def __init__(self, editwin):
self.editwin = editwin
self.parent = editwin.text_frame
self.text = editwin.text
self.is_shown = False
self.main_widget = self.init_widgets()
self.bind_events()
self.update_font()
self.update_colors()
def init_widgets(self):
"""Initialize the sidebar's widgets, returning the main widget."""
raise NotImplementedError
def update_font(self):
"""Update the sidebar text font, usually after config changes."""
raise NotImplementedError
def update_colors(self):
"""Update the sidebar text colors, usually after config changes."""
raise NotImplementedError
def grid(self):
"""Layout the widget, always using grid layout."""
raise NotImplementedError
def show_sidebar(self):
if not self.is_shown:
self.grid()
self.is_shown = True
def hide_sidebar(self):
if self.is_shown:
self.main_widget.grid_forget()
self.is_shown = False
def yscroll_event(self, *args, **kwargs):
"""Hook for vertical scrolling for sub-classes to override."""
raise NotImplementedError
def redirect_yscroll_event(self, *args, **kwargs):
"""Redirect vertical scrolling to the main editor text widget.
The scroll bar is also updated.
"""
self.editwin.vbar.set(*args)
return self.yscroll_event(*args, **kwargs)
def redirect_focusin_event(self, event):
"""Redirect focus-in events to the main editor text widget."""
self.text.focus_set()
return 'break'
def redirect_mousebutton_event(self, event, event_name):
"""Redirect mouse button events to the main editor text widget."""
self.text.focus_set()
self.text.event_generate(event_name, x=0, y=event.y)
return 'break'
def redirect_mousewheel_event(self, event):
"""Redirect mouse wheel events to the editwin text widget."""
self.text.event_generate('<MouseWheel>',
x=0, y=event.y, delta=event.delta)
return 'break'
def bind_events(self):
self.text['yscrollcommand'] = self.redirect_yscroll_event
# Ensure focus is always redirected to the main editor text widget.
self.main_widget.bind('<FocusIn>', self.redirect_focusin_event)
# Redirect mouse scrolling to the main editor text widget.
#
# Note that without this, scrolling with the mouse only scrolls
# the line numbers.
self.main_widget.bind('<MouseWheel>', self.redirect_mousewheel_event)
# Redirect mouse button events to the main editor text widget,
# except for the left mouse button (1).
#
# Note: X-11 sends Button-4 and Button-5 events for the scroll wheel.
def bind_mouse_event(event_name, target_event_name):
handler = functools.partial(self.redirect_mousebutton_event,
event_name=target_event_name)
self.main_widget.bind(event_name, handler)
for button in [2, 3, 4, 5]:
for event_name in (f'<Button-{button}>',
f'<ButtonRelease-{button}>',
f'<B{button}-Motion>',
):
bind_mouse_event(event_name, target_event_name=event_name)
# Convert double- and triple-click events to normal click events,
# since event_generate() doesn't allow generating such events.
for event_name in (f'<Double-Button-{button}>',
f'<Triple-Button-{button}>',
):
bind_mouse_event(event_name,
target_event_name=f'<Button-{button}>')
# start_line is set upon <Button-1> to allow selecting a range of rows
# by dragging. It is cleared upon <ButtonRelease-1>.
start_line = None
# last_y is initially set upon <B1-Leave> and is continuously updated
# upon <B1-Motion>, until <B1-Enter> or the mouse button is released.
# It is used in text_auto_scroll(), which is called repeatedly and
# does have a mouse event available.
last_y = None
# auto_scrolling_after_id is set whenever text_auto_scroll is
# scheduled via .after(). It is used to stop the auto-scrolling
# upon <B1-Enter>, as well as to avoid scheduling the function several
# times in parallel.
auto_scrolling_after_id = None
def drag_update_selection_and_insert_mark(y_coord):
"""Helper function for drag and selection event handlers."""
lineno = get_lineno(self.text, f"@0,{y_coord}")
a, b = sorted([start_line, lineno])
self.text.tag_remove("sel", "1.0", "end")
self.text.tag_add("sel", f"{a}.0", f"{b+1}.0")
self.text.mark_set("insert",
f"{lineno if lineno == a else lineno + 1}.0")
def b1_mousedown_handler(event):
nonlocal start_line
nonlocal last_y
start_line = int(float(self.text.index(f"@0,{event.y}")))
last_y = event.y
drag_update_selection_and_insert_mark(event.y)
self.main_widget.bind('<Button-1>', b1_mousedown_handler)
def b1_mouseup_handler(event):
# On mouse up, we're no longer dragging. Set the shared persistent
# variables to None to represent this.
nonlocal start_line
nonlocal last_y
start_line = None
last_y = None
self.text.event_generate('<ButtonRelease-1>', x=0, y=event.y)
self.main_widget.bind('<ButtonRelease-1>', b1_mouseup_handler)
def b1_drag_handler(event):
nonlocal last_y
if last_y is None: # i.e. if not currently dragging
return
last_y = event.y
drag_update_selection_and_insert_mark(event.y)
self.main_widget.bind('<B1-Motion>', b1_drag_handler)
def text_auto_scroll():
"""Mimic Text auto-scrolling when dragging outside of it."""
# See: https://github.com/tcltk/tk/blob/064ff9941b4b80b85916a8afe86a6c21fd388b54/library/text.tcl#L670
nonlocal auto_scrolling_after_id
y = last_y
if y is None:
self.main_widget.after_cancel(auto_scrolling_after_id)
auto_scrolling_after_id = None
return
elif y < 0:
self.text.yview_scroll(-1 + y, 'pixels')
drag_update_selection_and_insert_mark(y)
elif y > self.main_widget.winfo_height():
self.text.yview_scroll(1 + y - self.main_widget.winfo_height(),
'pixels')
drag_update_selection_and_insert_mark(y)
auto_scrolling_after_id = \
self.main_widget.after(50, text_auto_scroll)
def b1_leave_handler(event):
# Schedule the initial call to text_auto_scroll(), if not already
# scheduled.
nonlocal auto_scrolling_after_id
if auto_scrolling_after_id is None:
nonlocal last_y
last_y = event.y
auto_scrolling_after_id = \
self.main_widget.after(0, text_auto_scroll)
self.main_widget.bind('<B1-Leave>', b1_leave_handler)
def b1_enter_handler(event):
# Cancel the scheduling of text_auto_scroll(), if it exists.
nonlocal auto_scrolling_after_id
if auto_scrolling_after_id is not None:
self.main_widget.after_cancel(auto_scrolling_after_id)
auto_scrolling_after_id = None
self.main_widget.bind('<B1-Enter>', b1_enter_handler)
class EndLineDelegator(Delegator):
"""Generate callbacks with the current end line number.
The provided callback is called after every insert and delete.
"""
def __init__(self, changed_callback):
Delegator.__init__(self)
self.changed_callback = changed_callback
def insert(self, index, chars, tags=None):
self.delegate.insert(index, chars, tags)
self.changed_callback(get_end_linenumber(self.delegate))
def delete(self, index1, index2=None):
self.delegate.delete(index1, index2)
self.changed_callback(get_end_linenumber(self.delegate))
class LineNumbers(BaseSideBar):
"""Line numbers support for editor windows."""
def __init__(self, editwin):
super().__init__(editwin)
end_line_delegator = EndLineDelegator(self.update_sidebar_text)
# Insert the delegator after the undo delegator, so that line numbers
# are properly updated after undo and redo actions.
self.editwin.per.insertfilterafter(end_line_delegator,
after=self.editwin.undo)
def init_widgets(self):
_padx, pady = get_widget_padding(self.text)
self.sidebar_text = tk.Text(self.parent, width=1, wrap=tk.NONE,
padx=2, pady=pady,
borderwidth=0, highlightthickness=0)
self.sidebar_text.config(state=tk.DISABLED)
self.prev_end = 1
self._sidebar_width_type = type(self.sidebar_text['width'])
with temp_enable_text_widget(self.sidebar_text):
self.sidebar_text.insert('insert', '1', 'linenumber')
self.sidebar_text.config(takefocus=False, exportselection=False)
self.sidebar_text.tag_config('linenumber', justify=tk.RIGHT)
end = get_end_linenumber(self.text)
self.update_sidebar_text(end)
return self.sidebar_text
def grid(self):
self.sidebar_text.grid(row=1, column=0, sticky=tk.NSEW)
def update_font(self):
font = idleConf.GetFont(self.text, 'main', 'EditorWindow')
self.sidebar_text['font'] = font
def update_colors(self):
"""Update the sidebar text colors, usually after config changes."""
colors = idleConf.GetHighlight(idleConf.CurrentTheme(), 'linenumber')
foreground = colors['foreground']
background = colors['background']
self.sidebar_text.config(
fg=foreground, bg=background,
selectforeground=foreground, selectbackground=background,
inactiveselectbackground=background,
)
def update_sidebar_text(self, end):
"""
Perform the following action:
Each line sidebar_text contains the linenumber for that line
Synchronize with editwin.text so that both sidebar_text and
editwin.text contain the same number of lines"""
if end == self.prev_end:
return
width_difference = len(str(end)) - len(str(self.prev_end))
if width_difference:
cur_width = int(float(self.sidebar_text['width']))
new_width = cur_width + width_difference
self.sidebar_text['width'] = self._sidebar_width_type(new_width)
with temp_enable_text_widget(self.sidebar_text):
if end > self.prev_end:
new_text = '\n'.join(itertools.chain(
[''],
map(str, range(self.prev_end + 1, end + 1)),
))
self.sidebar_text.insert(f'end -1c', new_text, 'linenumber')
else:
self.sidebar_text.delete(f'{end+1}.0 -1c', 'end -1c')
self.prev_end = end
def yscroll_event(self, *args, **kwargs):
self.sidebar_text.yview_moveto(args[0])
return 'break'
class WrappedLineHeightChangeDelegator(Delegator):
def __init__(self, callback):
"""
callback - Callable, will be called when an insert, delete or replace
action on the text widget may require updating the shell
sidebar.
"""
Delegator.__init__(self)
self.callback = callback
def insert(self, index, chars, tags=None):
is_single_line = '\n' not in chars
if is_single_line:
before_displaylines = get_displaylines(self, index)
self.delegate.insert(index, chars, tags)
if is_single_line:
after_displaylines = get_displaylines(self, index)
if after_displaylines == before_displaylines:
return # no need to update the sidebar
self.callback()
def delete(self, index1, index2=None):
if index2 is None:
index2 = index1 + "+1c"
is_single_line = get_lineno(self, index1) == get_lineno(self, index2)
if is_single_line:
before_displaylines = get_displaylines(self, index1)
self.delegate.delete(index1, index2)
if is_single_line:
after_displaylines = get_displaylines(self, index1)
if after_displaylines == before_displaylines:
return # no need to update the sidebar
self.callback()
class ShellSidebar(BaseSideBar):
"""Sidebar for the PyShell window, for prompts etc."""
def __init__(self, editwin):
self.canvas = None
self.line_prompts = {}
super().__init__(editwin)
change_delegator = \
WrappedLineHeightChangeDelegator(self.change_callback)
# Insert the TextChangeDelegator after the last delegator, so that
# the sidebar reflects final changes to the text widget contents.
d = self.editwin.per.top
if d.delegate is not self.text:
while d.delegate is not self.editwin.per.bottom:
d = d.delegate
self.editwin.per.insertfilterafter(change_delegator, after=d)
self.is_shown = True
def init_widgets(self):
self.canvas = tk.Canvas(self.parent, width=30,
borderwidth=0, highlightthickness=0,
takefocus=False)
self.update_sidebar()
self.grid()
return self.canvas
def bind_events(self):
super().bind_events()
self.main_widget.bind(
# AquaTk defines <2> as the right button, not <3>.
"<Button-2>" if macosx.isAquaTk() else "<Button-3>",
self.context_menu_event,
)
def context_menu_event(self, event):
rmenu = tk.Menu(self.main_widget, tearoff=0)
has_selection = bool(self.text.tag_nextrange('sel', '1.0'))
def mkcmd(eventname):
return lambda: self.text.event_generate(eventname)
rmenu.add_command(label='Copy',
command=mkcmd('<<copy>>'),
state='normal' if has_selection else 'disabled')
rmenu.add_command(label='Copy with prompts',
command=mkcmd('<<copy-with-prompts>>'),
state='normal' if has_selection else 'disabled')
rmenu.tk_popup(event.x_root, event.y_root)
return "break"
def grid(self):
self.canvas.grid(row=1, column=0, sticky=tk.NSEW, padx=2, pady=0)
def change_callback(self):
if self.is_shown:
self.update_sidebar()
def update_sidebar(self):
text = self.text
text_tagnames = text.tag_names
canvas = self.canvas
line_prompts = self.line_prompts = {}
canvas.delete(tk.ALL)
index = text.index("@0,0")
if index.split('.', 1)[1] != '0':
index = text.index(f'{index}+1line linestart')
while (lineinfo := text.dlineinfo(index)) is not None:
y = lineinfo[1]
prev_newline_tagnames = text_tagnames(f"{index} linestart -1c")
prompt = (
'>>>' if "console" in prev_newline_tagnames else
'...' if "stdin" in prev_newline_tagnames else
None
)
if prompt:
canvas.create_text(2, y, anchor=tk.NW, text=prompt,
font=self.font, fill=self.colors[0])
lineno = get_lineno(text, index)
line_prompts[lineno] = prompt
index = text.index(f'{index}+1line')
def yscroll_event(self, *args, **kwargs):
"""Redirect vertical scrolling to the main editor text widget.
The scroll bar is also updated.
"""
self.change_callback()
return 'break'
def update_font(self):
"""Update the sidebar text font, usually after config changes."""
font = idleConf.GetFont(self.text, 'main', 'EditorWindow')
tk_font = Font(self.text, font=font)
char_width = max(tk_font.measure(char) for char in ['>', '.'])
self.canvas.configure(width=char_width * 3 + 4)
self.font = font
self.change_callback()
def update_colors(self):
"""Update the sidebar text colors, usually after config changes."""
linenumbers_colors = idleConf.GetHighlight(idleConf.CurrentTheme(), 'linenumber')
prompt_colors = idleConf.GetHighlight(idleConf.CurrentTheme(), 'console')
foreground = prompt_colors['foreground']
background = linenumbers_colors['background']
self.colors = (foreground, background)
self.canvas.configure(background=background)
self.change_callback()
def _sidebar_number_scrolling(parent): # htest #
from idlelib.idle_test.test_sidebar import Dummy_editwin
top = tk.Toplevel(parent)
text_frame = tk.Frame(top)
text_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
text_frame.rowconfigure(1, weight=1)
text_frame.columnconfigure(1, weight=1)
font = idleConf.GetFont(top, 'main', 'EditorWindow')
text = tk.Text(text_frame, width=80, height=24, wrap=tk.NONE, font=font)
text.grid(row=1, column=1, sticky=tk.NSEW)
editwin = Dummy_editwin(text)
editwin.vbar = tk.Scrollbar(text_frame)
linenumbers = LineNumbers(editwin)
linenumbers.show_sidebar()
text.insert('1.0', '\n'.join('a'*i for i in range(1, 101)))
if __name__ == '__main__':
from unittest import main
main('idlelib.idle_test.test_sidebar', verbosity=2, exit=False)
from idlelib.idle_test.htest import run
run(_sidebar_number_scrolling)
@@ -0,0 +1,345 @@
"""An IDLE extension to avoid having very long texts printed in the shell.
A common problem in IDLE's interactive shell is printing of large amounts of
text into the shell. This makes looking at the previous history difficult.
Worse, this can cause IDLE to become very slow, even to the point of being
completely unusable.
This extension will automatically replace long texts with a small button.
Double-clicking this button will remove it and insert the original text instead.
Middle-clicking will copy the text to the clipboard. Right-clicking will open
the text in a separate viewing window.
Additionally, any output can be manually "squeezed" by the user. This includes
output written to the standard error stream ("stderr"), such as exception
messages and their tracebacks.
"""
import re
import tkinter as tk
from tkinter import messagebox
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("<Double-Button-1>", self.expand)
if macosx.isAquaTk():
# AquaTk defines <2> as the right button, not <3>.
self.bind("<Button-2>", self.context_menu_event)
else:
self.bind("<Button-3>", 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 = messagebox.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=messagebox.CANCEL,
parent=self.text)
if not confirm:
return "break"
index = self.text.index(self)
self.base_text.insert(index, self.s, self.tags)
self.base_text.delete(self)
self.editwin.on_squeezed_expand(index, self.s, self.tags)
self.squeezer.expandingbuttons.remove(self)
def copy(self, event=None):
"""copy event handler
Copy the original text to the clipboard.
"""
self.clipboard_clear()
self.clipboard_append(self.s)
def view(self, event=None):
"""view event handler
View the original text in a separate text viewer window.
"""
view_text(self.text, "Squeezed Output Viewer", self.s,
modal=False, wrap='none')
rmenu_specs = (
# Item structure: (label, method_name).
('copy', 'copy'),
('view', 'view'),
)
def context_menu_event(self, event):
self.text.mark_set("insert", "@%d,%d" % (event.x, event.y))
rmenu = tk.Menu(self.text, tearoff=0)
for label, method_name in self.rmenu_specs:
rmenu.add_command(label=label, command=getattr(self, method_name))
rmenu.tk_popup(event.x_root, event.y_root)
return "break"
class Squeezer:
"""Replace long outputs in the shell with a simple button.
This avoids IDLE's shell slowing down considerably, and even becoming
completely unresponsive, when very long outputs are written.
"""
@classmethod
def reload(cls):
"""Load class variables from config."""
cls.auto_squeeze_min_lines = idleConf.GetOption(
"main", "PyShell", "auto-squeeze-min-lines",
type="int", default=50,
)
def __init__(self, editwin):
"""Initialize settings for Squeezer.
editwin is the shell's Editor window.
self.text is the editor window text widget.
self.base_test is the actual editor window Tk text widget, rather than
EditorWindow's wrapper.
self.expandingbuttons is the list of all buttons representing
"squeezed" output.
"""
self.editwin = editwin
self.text = text = editwin.text
# Get the base Text widget of the PyShell object, used to change
# text before the iomark. PyShell deliberately disables changing
# text before the iomark via its 'text' attribute, which is
# actually a wrapper for the actual Text widget. Squeezer,
# however, needs to make such changes.
self.base_text = editwin.per.bottom
# Twice the text widget's border width and internal padding;
# pre-calculated here for the get_line_width() method.
self.window_width_delta = 2 * (
int(text.cget('border')) +
int(text.cget('padx'))
)
self.expandingbuttons = []
# Replace the PyShell instance's write method with a wrapper,
# which inserts an ExpandingButton instead of a long text.
def mywrite(s, tags=(), write=editwin.write):
# Only auto-squeeze text which has just the "stdout" tag.
if tags != "stdout":
return write(s, tags)
# Only auto-squeeze text with at least the minimum
# configured number of lines.
auto_squeeze_min_lines = self.auto_squeeze_min_lines
# First, a very quick check to skip very short texts.
if len(s) < auto_squeeze_min_lines:
return write(s, tags)
# Now the full line-count check.
numoflines = self.count_lines(s)
if numoflines < auto_squeeze_min_lines:
return write(s, tags)
# Create an ExpandingButton instance.
expandingbutton = ExpandingButton(s, tags, numoflines, self)
# Insert the ExpandingButton into the Text widget.
text.mark_gravity("iomark", tk.RIGHT)
text.window_create("iomark", window=expandingbutton,
padx=3, pady=5)
text.see("iomark")
text.update()
text.mark_gravity("iomark", tk.LEFT)
# Add the ExpandingButton to the Squeezer's list.
self.expandingbuttons.append(expandingbutton)
editwin.write = mywrite
def count_lines(self, s):
"""Count the number of lines in a given text.
Before calculation, the tab width and line length of the text are
fetched, so that up-to-date values are used.
Lines are counted as if the string was wrapped so that lines are never
over linewidth characters long.
Tabs are considered tabwidth characters long.
"""
return count_lines_with_wrapping(s, self.editwin.width)
def squeeze_current_text(self):
"""Squeeze the text block where the insertion cursor is.
If the 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.
@@ -0,0 +1,134 @@
# Rename to stackbrowser or possibly consolidate with browser.
import linecache
import os
import tkinter as tk
from idlelib.debugobj import ObjectTreeItem, make_objecttreeitem
from idlelib.tree import TreeNode, TreeItem, ScrolledCanvas
def StackBrowser(root, exc, flist=None, top=None):
global sc, item, node # For testing.
if top is None:
top = tk.Toplevel(root)
sc = ScrolledCanvas(top, bg="white", highlightthickness=0)
sc.frame.pack(expand=1, fill="both")
item = StackTreeItem(exc, flist)
node = TreeNode(sc.canvas, None, item)
node.expand()
class StackTreeItem(TreeItem):
def __init__(self, exc, flist=None):
self.flist = flist
self.stack = self.get_stack(None if exc is None else exc.__traceback__)
self.text = f"{type(exc).__name__}: {str(exc)}"
def get_stack(self, tb):
stack = []
if tb and tb.tb_frame is None:
tb = tb.tb_next
while tb is not None:
stack.append((tb.tb_frame, tb.tb_lineno))
tb = tb.tb_next
return stack
def GetText(self): # Titlecase names are overrides.
return self.text
def GetSubList(self):
sublist = []
for info in self.stack:
item = FrameTreeItem(info, self.flist)
sublist.append(item)
return sublist
class FrameTreeItem(TreeItem):
def __init__(self, info, flist):
self.info = info
self.flist = flist
def GetText(self):
frame, lineno = self.info
try:
modname = frame.f_globals["__name__"]
except:
modname = "?"
code = frame.f_code
filename = code.co_filename
funcname = code.co_name
sourceline = linecache.getline(filename, lineno)
sourceline = sourceline.strip()
if funcname in ("?", "", None):
item = "%s, line %d: %s" % (modname, lineno, sourceline)
else:
item = "%s.%s(...), line %d: %s" % (modname, funcname,
lineno, sourceline)
return item
def GetSubList(self):
frame, lineno = self.info
sublist = []
if frame.f_globals is not frame.f_locals:
item = VariablesTreeItem("<locals>", frame.f_locals, self.flist)
sublist.append(item)
item = VariablesTreeItem("<globals>", frame.f_globals, self.flist)
sublist.append(item)
return sublist
def OnDoubleClick(self):
if self.flist:
frame, lineno = self.info
filename = frame.f_code.co_filename
if os.path.isfile(filename):
self.flist.gotofileline(filename, lineno)
class VariablesTreeItem(ObjectTreeItem):
def GetText(self):
return self.labeltext
def GetLabelText(self):
return None
def IsExpandable(self):
return len(self.object) > 0
def GetSubList(self):
sublist = []
for key in self.object.keys(): # self.object not necessarily dict.
try:
value = self.object[key]
except KeyError:
continue
def setfunction(value, key=key, object_=self.object):
object_[key] = value
item = make_objecttreeitem(key + " =", value, setfunction)
sublist.append(item)
return sublist
def _stackbrowser(parent): # htest #
from idlelib.pyshell import PyShellFileList
top = tk.Toplevel(parent)
top.title("Test StackViewer")
x, y = map(int, parent.geometry().split('+')[1:])
top.geometry("+%d+%d" % (x + 50, y + 175))
flist = PyShellFileList(top)
try: # to obtain a traceback object
intentional_name_error
except NameError as e:
StackBrowser(top, e, flist=flist, top=top)
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(_stackbrowser)
@@ -0,0 +1,52 @@
from tkinter.ttk import Label, Frame
class MultiStatusBar(Frame):
def __init__(self, master, **kw):
Frame.__init__(self, master, **kw)
self.labels = {}
def set_label(self, name, text='', side='left', width=0):
if name not in self.labels:
label = Label(self, borderwidth=0, anchor='w')
label.pack(side=side, pady=0, padx=4)
self.labels[name] = label
else:
label = self.labels[name]
if width != 0:
label.config(width=width)
label.config(text=text)
def _multistatus_bar(parent): # htest #
from tkinter import Toplevel, Text
from tkinter.ttk import Frame, Button
top = Toplevel(parent)
x, y = map(int, parent.geometry().split('+')[1:])
top.geometry("+%d+%d" %(x, y + 175))
top.title("Test multistatus bar")
frame = Frame(top)
text = Text(frame, height=5, width=40)
text.pack()
msb = MultiStatusBar(frame)
msb.set_label("one", "hello")
msb.set_label("two", "world")
msb.pack(side='bottom', fill='x')
def change():
msb.set_label("one", "foo")
msb.set_label("two", "bar")
button = Button(top, text="Update status", command=change)
button.pack(side='bottom')
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)
@@ -0,0 +1,193 @@
"""Simple text browser for IDLE
"""
from tkinter import Toplevel, Text, TclError,\
HORIZONTAL, VERTICAL, NS, EW, NSEW, NONE, WORD, SUNKEN
from tkinter.ttk import Frame, Scrollbar, Button
from tkinter.messagebox import showerror
from idlelib.colorizer import color_config
class AutoHideScrollbar(Scrollbar):
"""A scrollbar that is automatically hidden when not needed.
Only the grid geometry manager is supported.
"""
def set(self, lo, hi):
if float(lo) > 0.0 or float(hi) < 1.0:
self.grid()
else:
self.grid_remove()
super().set(lo, hi)
def pack(self, **kwargs):
raise TclError(f'{self.__class__.__name__} does not support "pack"')
def place(self, **kwargs):
raise TclError(f'{self.__class__.__name__} does not support "place"')
class ScrollableTextFrame(Frame):
"""Display text with scrollbar(s)."""
def __init__(self, master, wrap=NONE, **kwargs):
"""Create a frame for Textview.
master - master widget for this frame
wrap - type of text wrapping to use ('word', 'char' or 'none')
All parameters except for 'wrap' are passed to Frame.__init__().
The Text widget is accessible via the 'text' attribute.
Note: Changing the wrapping mode of the text widget after
instantiation is not supported.
"""
super().__init__(master, **kwargs)
text = self.text = Text(self, wrap=wrap)
text.grid(row=0, column=0, sticky=NSEW)
self.grid_rowconfigure(0, weight=1)
self.grid_columnconfigure(0, weight=1)
# vertical scrollbar
self.yscroll = AutoHideScrollbar(self, orient=VERTICAL,
takefocus=False,
command=text.yview)
self.yscroll.grid(row=0, column=1, sticky=NS)
text['yscrollcommand'] = self.yscroll.set
# horizontal scrollbar - only when wrap is set to NONE
if wrap == NONE:
self.xscroll = AutoHideScrollbar(self, orient=HORIZONTAL,
takefocus=False,
command=text.xview)
self.xscroll.grid(row=1, column=0, sticky=EW)
text['xscrollcommand'] = self.xscroll.set
else:
self.xscroll = None
class ViewFrame(Frame):
"Display TextFrame and Close button."
def __init__(self, parent, contents, wrap='word'):
"""Create a frame for viewing text with a "Close" button.
parent - parent widget for this frame
contents - text to display
wrap - type of text wrapping to use ('word', 'char' or 'none')
The Text widget is accessible via the 'text' attribute.
"""
super().__init__(parent)
self.parent = parent
self.bind('<Return>', self.ok)
self.bind('<Escape>', self.ok)
self.textframe = ScrollableTextFrame(self, relief=SUNKEN, height=700)
text = self.text = self.textframe.text
text.insert('1.0', contents)
text.configure(wrap=wrap, highlightthickness=0, state='disabled')
color_config(text)
text.focus_set()
self.button_ok = button_ok = Button(
self, text='Close', command=self.ok, takefocus=False)
self.textframe.pack(side='top', expand=True, fill='both')
button_ok.pack(side='bottom')
def ok(self, event=None):
"""Dismiss text viewer dialog."""
self.parent.destroy()
class ViewWindow(Toplevel):
"A simple text viewer dialog for IDLE."
def __init__(self, parent, title, contents, modal=True, wrap=WORD,
*, _htest=False, _utest=False):
"""Show the given text in a scrollable window with a 'close' button.
If modal is left True, users cannot interact with other windows
until the textview window is closed.
parent - parent of this dialog
title - string which is title of popup dialog
contents - text to display in dialog
wrap - type of text wrapping to use ('word', 'char' or 'none')
_htest - bool; change box location when running htest.
_utest - bool; don't wait_window when running unittest.
"""
super().__init__(parent)
self['borderwidth'] = 5
# Place dialog below parent if running htest.
x = parent.winfo_rootx() + 10
y = parent.winfo_rooty() + (10 if not _htest else 100)
self.geometry(f'=750x500+{x}+{y}')
self.title(title)
self.viewframe = ViewFrame(self, contents, wrap=wrap)
self.protocol("WM_DELETE_WINDOW", self.ok)
self.button_ok = button_ok = Button(self, text='Close',
command=self.ok, takefocus=False)
self.viewframe.pack(side='top', expand=True, fill='both')
self.is_modal = modal
if self.is_modal:
self.transient(parent)
self.grab_set()
if not _utest:
self.wait_window()
def ok(self, event=None):
"""Dismiss text viewer dialog."""
if self.is_modal:
self.grab_release()
self.destroy()
def view_text(parent, title, contents, modal=True, wrap='word', _utest=False):
"""Create text viewer for given text.
parent - parent of this dialog
title - string which is the title of popup dialog
contents - text to display in this dialog
wrap - type of text wrapping to use ('word', 'char' or 'none')
modal - controls if users can interact with other windows while this
dialog is displayed
_utest - bool; controls wait_window on unittest
"""
return ViewWindow(parent, title, contents, modal, wrap=wrap, _utest=_utest)
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
with contents of the file.
"""
try:
with open(filename, encoding=encoding) as file:
contents = file.read()
except OSError:
showerror(title='File Load Error',
message=f'Unable to load file {filename!r} .',
parent=parent)
except UnicodeDecodeError as err:
showerror(title='Unicode Decode Error',
message=str(err),
parent=parent)
else:
return view_text(parent, title, contents, modal, wrap=wrap,
_utest=_utest)
return None
if __name__ == '__main__':
from unittest import main
main('idlelib.idle_test.test_textview', verbosity=2, exit=False)
from idlelib.idle_test.htest import run
run(ViewWindow)
@@ -0,0 +1,190 @@
"""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:
"""abstract base class for tooltips"""
def __init__(self, anchor_widget):
"""Create a tooltip.
anchor_widget: the widget next to which the tooltip will be shown
Note that a widget will only be shown when showtip() is called.
"""
self.anchor_widget = anchor_widget
self.tipwindow = None
def __del__(self):
self.hidetip()
def showtip(self):
"""display the tooltip"""
if self.tipwindow:
return
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 :-(
#
# 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):
"""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:
try:
tw.destroy()
except TclError: # pragma: no cover
pass
class OnHoverTooltipBase(TooltipBase):
"""abstract base class for tooltips, with delayed on-hover display"""
def __init__(self, anchor_widget, hover_delay=1000):
"""Create a tooltip with a mouse hover delay.
anchor_widget: the widget next to which the tooltip will be shown
hover_delay: time to delay before showing the tooltip, in milliseconds
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().__init__(anchor_widget)
self.hover_delay = hover_delay
self._after_id = None
self._id1 = self.anchor_widget.bind("<Enter>", self._show_event)
self._id2 = self.anchor_widget.bind("<Leave>", self._hide_event)
self._id3 = self.anchor_widget.bind("<Button>", self._hide_event)
def __del__(self):
try:
self.anchor_widget.unbind("<Enter>", self._id1)
self.anchor_widget.unbind("<Leave>", self._id2) # pragma: no cover
self.anchor_widget.unbind("<Button>", self._id3) # pragma: no cover
except TclError:
pass
super().__del__()
def _show_event(self, event=None):
"""event handler to display the tooltip"""
if self.hover_delay:
self.schedule()
else:
self.showtip()
def _hide_event(self, event=None):
"""event handler to hide the tooltip"""
self.hidetip()
def schedule(self):
"""schedule the future display of the tooltip"""
self.unschedule()
self._after_id = self.anchor_widget.after(self.hover_delay,
self.showtip)
def unschedule(self):
"""cancel the future display of the tooltip"""
after_id = self._after_id
self._after_id = None
if after_id:
self.anchor_widget.after_cancel(after_id)
def hidetip(self):
"""hide the tooltip"""
try:
self.unschedule()
except TclError: # pragma: no cover
pass
super().hidetip()
class Hovertip(OnHoverTooltipBase):
"A tooltip that pops up when a mouse hovers over an anchor widget."
def __init__(self, anchor_widget, text, hover_delay=1000,
foreground="#000000", background="#ffffe0"):
"""Create a text 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
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().__init__(anchor_widget, hover_delay=hover_delay)
self.text = text
self.foreground = foreground
self.background = background
def showcontents(self):
label = Label(self.tipwindow, text=self.text, justify=LEFT,
relief=SOLID, borderwidth=1,
foreground=self.foreground, background=self.background)
label.pack()
def _tooltip(parent): # htest #
top = Toplevel(parent)
top.title("Test tooltip")
x, y = map(int, parent.geometry().split('+')[1:])
top.geometry("+%d+%d" % (x, y + 150))
label = Label(top, text="Place your mouse over buttons")
label.pack()
button1 = Button(top, text="Button 1 -- 1/2 second hover delay")
button1.pack()
Hovertip(button1, "This is tooltip text for button1.", hover_delay=500)
button2 = Button(top, text="Button 2 -- no hover delay")
button2.pack()
Hovertip(button2, "This is tooltip\ntext for button2.", hover_delay=None)
if __name__ == '__main__':
from unittest import main
main('idlelib.idle_test.test_tooltip', verbosity=2, exit=False)
from idlelib.idle_test.htest import run
run(_tooltip)
@@ -0,0 +1,509 @@
# XXX TO DO:
# - popup menu
# - support partial or total redisplay
# - key bindings (instead of quick-n-dirty bindings on Canvas):
# - up/down arrow keys to move focus around
# - ditto for page up/down, home/end
# - left/right arrows to expand/collapse & move out/in
# - more doc strings
# - add icons for "file", "module", "class", "method"; better "python" icon
# - callback for selection???
# - multiple-item selection
# - tooltips
# - redo geometry without magic numbers
# - keep track of object ids to allow more careful cleaning
# - optimize tree redraw after expand of subnode
import os
from tkinter import *
from tkinter.ttk import Frame, Scrollbar
from idlelib.config import idleConf
from idlelib import zoomheight
ICONDIR = "Icons"
# Look for Icons subdirectory in the same directory as this module
try:
_icondir = os.path.join(os.path.dirname(__file__), ICONDIR)
except NameError:
_icondir = ICONDIR
if os.path.isdir(_icondir):
ICONDIR = _icondir
elif not os.path.isdir(ICONDIR):
raise RuntimeError(f"can't find icon directory ({ICONDIR!r})")
def listicons(icondir=ICONDIR):
"""Utility to display the available icons."""
root = Tk()
import glob
list = glob.glob(os.path.join(glob.escape(icondir), "*.gif"))
list.sort()
images = []
row = column = 0
for file in list:
name = os.path.splitext(os.path.basename(file))[0]
image = PhotoImage(file=file, master=root)
images.append(image)
label = Label(root, image=image, bd=1, relief="raised")
label.grid(row=row, column=column)
label = Label(root, text=name)
label.grid(row=row+1, column=column)
column = column + 1
if column >= 10:
row = row+2
column = 0
root.images = images
def wheel_event(event, widget=None):
"""Handle scrollwheel event.
For wheel up, event.delta = 120*n on Windows, -1*n on darwin,
where n can be > 1 if one scrolls fast. Flicking the wheel
generates up to maybe 20 events with n up to 10 or more 1.
Macs use wheel down (delta = 1*n) to scroll up, so positive
delta means to scroll up on both systems.
X-11 sends Control-Button-4,5 events instead.
The widget parameter is needed so browser label bindings can pass
the underlying canvas.
This function depends on widget.yview to not be overridden by
a subclass.
"""
up = {EventType.MouseWheel: event.delta > 0,
EventType.ButtonPress: event.num == 4}
lines = -5 if up[event.type] else 5
widget = event.widget if widget is None else widget
widget.yview(SCROLL, lines, 'units')
return 'break'
class TreeNode:
dy = 0
def __init__(self, canvas, parent, item):
self.canvas = canvas
self.parent = parent
self.item = item
self.state = 'collapsed'
self.selected = False
self.children = []
self.x = self.y = None
self.iconimages = {} # cache of PhotoImage instances for icons
def destroy(self):
for c in self.children[:]:
self.children.remove(c)
c.destroy()
self.parent = None
def geticonimage(self, name):
try:
return self.iconimages[name]
except KeyError:
pass
file, ext = os.path.splitext(name)
ext = ext or ".gif"
fullname = os.path.join(ICONDIR, file + ext)
image = PhotoImage(master=self.canvas, file=fullname)
self.iconimages[name] = image
return image
def select(self, event=None):
if self.selected:
return
self.deselectall()
self.selected = True
self.canvas.delete(self.image_id)
self.drawicon()
self.drawtext()
def deselect(self, event=None):
if not self.selected:
return
self.selected = False
self.canvas.delete(self.image_id)
self.drawicon()
self.drawtext()
def deselectall(self):
if self.parent:
self.parent.deselectall()
else:
self.deselecttree()
def deselecttree(self):
if self.selected:
self.deselect()
for child in self.children:
child.deselecttree()
def flip(self, event=None):
if self.state == 'expanded':
self.collapse()
else:
self.expand()
self.item.OnDoubleClick()
return "break"
def expand(self, event=None):
if not self.item._IsExpandable():
return
if self.state != 'expanded':
self.state = 'expanded'
self.update()
self.view()
def collapse(self, event=None):
if self.state != 'collapsed':
self.state = 'collapsed'
self.update()
def view(self):
top = self.y - 2
bottom = self.lastvisiblechild().y + 17
height = bottom - top
visible_top = self.canvas.canvasy(0)
visible_height = self.canvas.winfo_height()
visible_bottom = self.canvas.canvasy(visible_height)
if visible_top <= top and bottom <= visible_bottom:
return
x0, y0, x1, y1 = self.canvas._getints(self.canvas['scrollregion'])
if top >= visible_top and height <= visible_height:
fraction = top + height - visible_height
else:
fraction = top
fraction = float(fraction) / y1
self.canvas.yview_moveto(fraction)
def lastvisiblechild(self):
if self.children and self.state == 'expanded':
return self.children[-1].lastvisiblechild()
else:
return self
def update(self):
if self.parent:
self.parent.update()
else:
oldcursor = self.canvas['cursor']
self.canvas['cursor'] = "watch"
self.canvas.update()
self.canvas.delete(ALL) # XXX could be more subtle
self.draw(7, 2)
x0, y0, x1, y1 = self.canvas.bbox(ALL)
self.canvas.configure(scrollregion=(0, 0, x1, y1))
self.canvas['cursor'] = oldcursor
def draw(self, x, y):
# XXX This hard-codes too many geometry constants!
self.x, self.y = x, y
self.drawicon()
self.drawtext()
if self.state != 'expanded':
return y + TreeNode.dy
# draw children
if not self.children:
sublist = self.item._GetSubList()
if not sublist:
# _IsExpandable() was mistaken; that's allowed
return y + TreeNode.dy
for item in sublist:
child = self.__class__(self.canvas, self, item)
self.children.append(child)
cx = x+20
cy = y + TreeNode.dy
cylast = 0
for child in self.children:
cylast = cy
self.canvas.create_line(x+9, cy+7, cx, cy+7, fill="gray50")
cy = child.draw(cx, cy)
if child.item._IsExpandable():
if child.state == 'expanded':
iconname = "minusnode"
callback = child.collapse
else:
iconname = "plusnode"
callback = child.expand
image = self.geticonimage(iconname)
id = self.canvas.create_image(x+9, cylast+7, image=image)
# XXX This leaks bindings until canvas is deleted:
self.canvas.tag_bind(id, "<1>", callback)
self.canvas.tag_bind(id, "<Double-1>", lambda x: None)
id = self.canvas.create_line(x+9, y+10, x+9, cylast+7,
##stipple="gray50", # XXX Seems broken in Tk 8.0.x
fill="gray50")
self.canvas.tag_lower(id) # XXX .lower(id) before Python 1.5.2
return cy
def drawicon(self):
if self.selected:
imagename = (self.item.GetSelectedIconName() or
self.item.GetIconName() or
"openfolder")
else:
imagename = self.item.GetIconName() or "folder"
image = self.geticonimage(imagename)
id = self.canvas.create_image(self.x, self.y, anchor="nw", image=image)
self.image_id = id
self.canvas.tag_bind(id, "<1>", self.select)
self.canvas.tag_bind(id, "<Double-1>", self.flip)
def drawtext(self):
textx = self.x+20-1
texty = self.y-4
labeltext = self.item.GetLabelText()
if labeltext:
id = self.canvas.create_text(textx, texty, anchor="nw",
text=labeltext)
self.canvas.tag_bind(id, "<1>", self.select)
self.canvas.tag_bind(id, "<Double-1>", self.flip)
x0, y0, x1, y1 = self.canvas.bbox(id)
textx = max(x1, 200) + 10
text = self.item.GetText() or "<no text>"
try:
self.entry
except AttributeError:
pass
else:
self.edit_finish()
try:
self.label
except AttributeError:
# padding carefully selected (on Windows) to match Entry widget:
self.label = Label(self.canvas, text=text, bd=0, padx=2, pady=2)
theme = idleConf.CurrentTheme()
if self.selected:
self.label.configure(idleConf.GetHighlight(theme, 'hilite'))
else:
self.label.configure(idleConf.GetHighlight(theme, 'normal'))
id = self.canvas.create_window(textx, texty,
anchor="nw", window=self.label)
self.label.bind("<1>", self.select_or_edit)
self.label.bind("<Double-1>", self.flip)
self.label.bind("<MouseWheel>", lambda e: wheel_event(e, self.canvas))
if self.label._windowingsystem == 'x11':
self.label.bind("<Button-4>", lambda e: wheel_event(e, self.canvas))
self.label.bind("<Button-5>", lambda e: wheel_event(e, self.canvas))
self.text_id = id
if TreeNode.dy == 0:
# The first row doesn't matter what the dy is, just measure its
# size to get the value of the subsequent dy
coords = self.canvas.bbox(id)
TreeNode.dy = max(20, coords[3] - coords[1] - 3)
def select_or_edit(self, event=None):
if self.selected and self.item.IsEditable():
self.edit(event)
else:
self.select(event)
def edit(self, event=None):
self.entry = Entry(self.label, bd=0, highlightthickness=1, width=0)
self.entry.insert(0, self.label['text'])
self.entry.selection_range(0, END)
self.entry.pack(ipadx=5)
self.entry.focus_set()
self.entry.bind("<Return>", self.edit_finish)
self.entry.bind("<Escape>", self.edit_cancel)
def edit_finish(self, event=None):
try:
entry = self.entry
del self.entry
except AttributeError:
return
text = entry.get()
entry.destroy()
if text and text != self.item.GetText():
self.item.SetText(text)
text = self.item.GetText()
self.label['text'] = text
self.drawtext()
self.canvas.focus_set()
def edit_cancel(self, event=None):
try:
entry = self.entry
del self.entry
except AttributeError:
return
entry.destroy()
self.drawtext()
self.canvas.focus_set()
class TreeItem:
"""Abstract class representing tree items.
Methods should typically be overridden, otherwise a default action
is used.
"""
def __init__(self):
"""Constructor. Do whatever you need to do."""
def GetText(self):
"""Return text string to display."""
def GetLabelText(self):
"""Return label text string to display in front of text (if any)."""
expandable = None
def _IsExpandable(self):
"""Do not override! Called by TreeNode."""
if self.expandable is None:
self.expandable = self.IsExpandable()
return self.expandable
def IsExpandable(self):
"""Return whether there are subitems."""
return 1
def _GetSubList(self):
"""Do not override! Called by TreeNode."""
if not self.IsExpandable():
return []
sublist = self.GetSubList()
if not sublist:
self.expandable = 0
return sublist
def IsEditable(self):
"""Return whether the item's text may be edited."""
def SetText(self, text):
"""Change the item's text (if it is editable)."""
def GetIconName(self):
"""Return name of icon to be displayed normally."""
def GetSelectedIconName(self):
"""Return name of icon to be displayed when selected."""
def GetSubList(self):
"""Return list of items forming sublist."""
def OnDoubleClick(self):
"""Called on a double-click on the item."""
# Example application
class FileTreeItem(TreeItem):
"""Example TreeItem subclass -- browse the file system."""
def __init__(self, path):
self.path = path
def GetText(self):
return os.path.basename(self.path) or self.path
def IsEditable(self):
return os.path.basename(self.path) != ""
def SetText(self, text):
newpath = os.path.dirname(self.path)
newpath = os.path.join(newpath, text)
if os.path.dirname(newpath) != os.path.dirname(self.path):
return
try:
os.rename(self.path, newpath)
self.path = newpath
except OSError:
pass
def GetIconName(self):
if not self.IsExpandable():
return "python" # XXX wish there was a "file" icon
def IsExpandable(self):
return os.path.isdir(self.path)
def GetSubList(self):
try:
names = os.listdir(self.path)
except OSError:
return []
names.sort(key = os.path.normcase)
sublist = []
for name in names:
item = FileTreeItem(os.path.join(self.path, name))
sublist.append(item)
return sublist
# A canvas widget with scroll bars and some useful bindings
class ScrolledCanvas:
def __init__(self, master, **opts):
if 'yscrollincrement' not in opts:
opts['yscrollincrement'] = 17
self.master = master
self.frame = Frame(master)
self.frame.rowconfigure(0, weight=1)
self.frame.columnconfigure(0, weight=1)
self.canvas = Canvas(self.frame, **opts)
self.canvas.grid(row=0, column=0, sticky="nsew")
self.vbar = Scrollbar(self.frame, name="vbar")
self.vbar.grid(row=0, column=1, sticky="nse")
self.hbar = Scrollbar(self.frame, name="hbar", orient="horizontal")
self.hbar.grid(row=1, column=0, sticky="ews")
self.canvas['yscrollcommand'] = self.vbar.set
self.vbar['command'] = self.canvas.yview
self.canvas['xscrollcommand'] = self.hbar.set
self.hbar['command'] = self.canvas.xview
self.canvas.bind("<Key-Prior>", self.page_up)
self.canvas.bind("<Key-Next>", self.page_down)
self.canvas.bind("<Key-Up>", self.unit_up)
self.canvas.bind("<Key-Down>", self.unit_down)
self.canvas.bind("<MouseWheel>", wheel_event)
if self.canvas._windowingsystem == 'x11':
self.canvas.bind("<Button-4>", wheel_event)
self.canvas.bind("<Button-5>", wheel_event)
#if isinstance(master, Toplevel) or isinstance(master, Tk):
self.canvas.bind("<Alt-Key-2>", self.zoom_height)
self.canvas.focus_set()
def page_up(self, event):
self.canvas.yview_scroll(-1, "page")
return "break"
def page_down(self, event):
self.canvas.yview_scroll(1, "page")
return "break"
def unit_up(self, event):
self.canvas.yview_scroll(-1, "unit")
return "break"
def unit_down(self, event):
self.canvas.yview_scroll(1, "unit")
return "break"
def zoom_height(self, event):
zoomheight.zoom_height(self.master)
return "break"
def _tree_widget(parent): # htest #
top = Toplevel(parent)
x, y = map(int, parent.geometry().split('+')[1:])
top.geometry("+%d+%d" % (x+50, y+175))
sc = ScrolledCanvas(top, bg="white", highlightthickness=0, takefocus=1)
sc.frame.pack(expand=1, fill="both", side=LEFT)
item = FileTreeItem(ICONDIR)
node = TreeNode(sc.canvas, None, item)
node.expand()
if __name__ == '__main__':
from unittest import main
main('idlelib.idle_test.test_tree', verbosity=2, exit=False)
from idlelib.idle_test.htest import run
run(_tree_widget)
@@ -0,0 +1,367 @@
import string
from idlelib.delegator import Delegator
# tkinter import not needed because module does not create widgets,
# although many methods operate on text widget arguments.
#$ event <<redo>>
#$ win <Control-y>
#$ unix <Alt-z>
#$ event <<undo>>
#$ win <Control-z>
#$ unix <Control-z>
#$ event <<dump-undo-state>>
#$ win <Control-backslash>
#$ unix <Control-backslash>
class UndoDelegator(Delegator):
max_undo = 1000
def __init__(self):
Delegator.__init__(self)
self.reset_undo()
def setdelegate(self, delegate):
if self.delegate is not None:
self.unbind("<<undo>>")
self.unbind("<<redo>>")
self.unbind("<<dump-undo-state>>")
Delegator.setdelegate(self, delegate)
if delegate is not None:
self.bind("<<undo>>", self.undo_event)
self.bind("<<redo>>", self.redo_event)
self.bind("<<dump-undo-state>>", self.dump_event)
def dump_event(self, event):
from pprint import pprint
pprint(self.undolist[:self.pointer])
print("pointer:", self.pointer, end=' ')
print("saved:", self.saved, end=' ')
print("can_merge:", self.can_merge, end=' ')
print("get_saved():", self.get_saved())
pprint(self.undolist[self.pointer:])
return "break"
def reset_undo(self):
self.was_saved = -1
self.pointer = 0
self.undolist = []
self.undoblock = 0 # or a CommandSequence instance
self.set_saved(1)
def set_saved(self, flag):
if flag:
self.saved = self.pointer
else:
self.saved = -1
self.can_merge = False
self.check_saved()
def get_saved(self):
return self.saved == self.pointer
saved_change_hook = None
def set_saved_change_hook(self, hook):
self.saved_change_hook = hook
was_saved = -1
def check_saved(self):
is_saved = self.get_saved()
if is_saved != self.was_saved:
self.was_saved = is_saved
if self.saved_change_hook:
self.saved_change_hook()
def insert(self, index, chars, tags=None):
self.addcmd(InsertCommand(index, chars, tags))
def delete(self, index1, index2=None):
self.addcmd(DeleteCommand(index1, index2))
# Clients should call undo_block_start() and undo_block_stop()
# around a sequence of editing cmds to be treated as a unit by
# undo & redo. Nested matching calls are OK, and the inner calls
# then act like nops. OK too if no editing cmds, or only one
# editing cmd, is issued in between: if no cmds, the whole
# sequence has no effect; and if only one cmd, that cmd is entered
# directly into the undo list, as if undo_block_xxx hadn't been
# called. The intent of all that is to make this scheme easy
# to use: all the client has to worry about is making sure each
# _start() call is matched by a _stop() call.
def undo_block_start(self):
if self.undoblock == 0:
self.undoblock = CommandSequence()
self.undoblock.bump_depth()
def undo_block_stop(self):
if self.undoblock.bump_depth(-1) == 0:
cmd = self.undoblock
self.undoblock = 0
if len(cmd) > 0:
if len(cmd) == 1:
# no need to wrap a single cmd
cmd = cmd.getcmd(0)
# this blk of cmds, or single cmd, has already
# been done, so don't execute it again
self.addcmd(cmd, 0)
def addcmd(self, cmd, execute=True):
if execute:
cmd.do(self.delegate)
if self.undoblock != 0:
self.undoblock.append(cmd)
return
if self.can_merge and self.pointer > 0:
lastcmd = self.undolist[self.pointer-1]
if lastcmd.merge(cmd):
return
self.undolist[self.pointer:] = [cmd]
if self.saved > self.pointer:
self.saved = -1
self.pointer = self.pointer + 1
if len(self.undolist) > self.max_undo:
##print "truncating undo list"
del self.undolist[0]
self.pointer = self.pointer - 1
if self.saved >= 0:
self.saved = self.saved - 1
self.can_merge = True
self.check_saved()
def undo_event(self, event):
if self.pointer == 0:
self.bell()
return "break"
cmd = self.undolist[self.pointer - 1]
cmd.undo(self.delegate)
self.pointer = self.pointer - 1
self.can_merge = False
self.check_saved()
return "break"
def redo_event(self, event):
if self.pointer >= len(self.undolist):
self.bell()
return "break"
cmd = self.undolist[self.pointer]
cmd.redo(self.delegate)
self.pointer = self.pointer + 1
self.can_merge = False
self.check_saved()
return "break"
class Command:
# Base class for Undoable commands
tags = None
def __init__(self, index1, index2, chars, tags=None):
self.marks_before = {}
self.marks_after = {}
self.index1 = index1
self.index2 = index2
self.chars = chars
if tags:
self.tags = tags
def __repr__(self):
s = self.__class__.__name__
t = (self.index1, self.index2, self.chars, self.tags)
if self.tags is None:
t = t[:-1]
return s + repr(t)
def do(self, text):
pass
def redo(self, text):
pass
def undo(self, text):
pass
def merge(self, cmd):
return 0
def save_marks(self, text):
marks = {}
for name in text.mark_names():
if name != "insert" and name != "current":
marks[name] = text.index(name)
return marks
def set_marks(self, text, marks):
for name, index in marks.items():
text.mark_set(name, index)
class InsertCommand(Command):
# Undoable insert command
def __init__(self, index1, chars, tags=None):
Command.__init__(self, index1, None, chars, tags)
def do(self, text):
self.marks_before = self.save_marks(text)
self.index1 = text.index(self.index1)
if text.compare(self.index1, ">", "end-1c"):
# Insert before the final newline
self.index1 = text.index("end-1c")
text.insert(self.index1, self.chars, self.tags)
self.index2 = text.index("%s+%dc" % (self.index1, len(self.chars)))
self.marks_after = self.save_marks(text)
##sys.__stderr__.write("do: %s\n" % self)
def redo(self, text):
text.mark_set('insert', self.index1)
text.insert(self.index1, self.chars, self.tags)
self.set_marks(text, self.marks_after)
text.see('insert')
##sys.__stderr__.write("redo: %s\n" % self)
def undo(self, text):
text.mark_set('insert', self.index1)
text.delete(self.index1, self.index2)
self.set_marks(text, self.marks_before)
text.see('insert')
##sys.__stderr__.write("undo: %s\n" % self)
def merge(self, cmd):
if self.__class__ is not cmd.__class__:
return False
if self.index2 != cmd.index1:
return False
if self.tags != cmd.tags:
return False
if len(cmd.chars) != 1:
return False
if self.chars and \
self.classify(self.chars[-1]) != self.classify(cmd.chars):
return False
self.index2 = cmd.index2
self.chars = self.chars + cmd.chars
return True
alphanumeric = string.ascii_letters + string.digits + "_"
def classify(self, c):
if c in self.alphanumeric:
return "alphanumeric"
if c == "\n":
return "newline"
return "punctuation"
class DeleteCommand(Command):
# Undoable delete command
def __init__(self, index1, index2=None):
Command.__init__(self, index1, index2, None, None)
def do(self, text):
self.marks_before = self.save_marks(text)
self.index1 = text.index(self.index1)
if self.index2:
self.index2 = text.index(self.index2)
else:
self.index2 = text.index(self.index1 + " +1c")
if text.compare(self.index2, ">", "end-1c"):
# Don't delete the final newline
self.index2 = text.index("end-1c")
self.chars = text.get(self.index1, self.index2)
text.delete(self.index1, self.index2)
self.marks_after = self.save_marks(text)
##sys.__stderr__.write("do: %s\n" % self)
def redo(self, text):
text.mark_set('insert', self.index1)
text.delete(self.index1, self.index2)
self.set_marks(text, self.marks_after)
text.see('insert')
##sys.__stderr__.write("redo: %s\n" % self)
def undo(self, text):
text.mark_set('insert', self.index1)
text.insert(self.index1, self.chars)
self.set_marks(text, self.marks_before)
text.see('insert')
##sys.__stderr__.write("undo: %s\n" % self)
class CommandSequence(Command):
# Wrapper for a sequence of undoable cmds to be undone/redone
# as a unit
def __init__(self):
self.cmds = []
self.depth = 0
def __repr__(self):
s = self.__class__.__name__
strs = []
for cmd in self.cmds:
strs.append(f" {cmd!r}")
return s + "(\n" + ",\n".join(strs) + "\n)"
def __len__(self):
return len(self.cmds)
def append(self, cmd):
self.cmds.append(cmd)
def getcmd(self, i):
return self.cmds[i]
def redo(self, text):
for cmd in self.cmds:
cmd.redo(text)
def undo(self, text):
cmds = self.cmds[:]
cmds.reverse()
for cmd in cmds:
cmd.undo(text)
def bump_depth(self, incr=1):
self.depth = self.depth + incr
return self.depth
def _undo_delegator(parent): # htest #
from tkinter import Toplevel, Text, Button
from idlelib.percolator import Percolator
top = Toplevel(parent)
top.title("Test UndoDelegator")
x, y = map(int, parent.geometry().split('+')[1:])
top.geometry("+%d+%d" % (x, y + 175))
text = Text(top, height=10)
text.pack()
text.focus_set()
p = Percolator(text)
d = UndoDelegator()
p.insertfilter(d)
undo = Button(top, text="Undo", command=lambda:d.undo_event(None))
undo.pack(side='left')
redo = Button(top, text="Redo", command=lambda:d.redo_event(None))
redo.pack(side='left')
dump = Button(top, text="Dump", command=lambda:d.dump_event(None))
dump.pack(side='left')
if __name__ == "__main__":
from unittest import main
main('idlelib.idle_test.test_undo', verbosity=2, exit=False)
from idlelib.idle_test.htest import run
run(_undo_delegator)
@@ -0,0 +1,37 @@
"""
Idlelib objects with no external idlelib dependencies
which are needed in more than one idlelib module.
They are included here because
a) they don't particularly belong elsewhere; or
b) because inclusion here simplifies the idlelib dependency graph.
TODO:
* Python versions (editor and help_about),
* tk version and patchlevel (pyshell, help_about, maxos?, editor?),
* std streams (pyshell, run),
* warning stuff (pyshell, run).
"""
import sys
# .pyw is for Windows; .pyi is for typing stub files.
# The extension order is needed for iomenu open/save dialogs.
py_extensions = ('.py', '.pyw', '.pyi')
# Fix for HiDPI screens on Windows. CALL BEFORE ANY TK OPERATIONS!
# URL for arguments for the ...Awareness call below.
# https://msdn.microsoft.com/en-us/library/windows/desktop/dn280512(v=vs.85).aspx
if sys.platform == 'win32': # pragma: no cover
def fix_win_hidpi(): # Called in pyshell and turtledemo.
try:
import ctypes
PROCESS_SYSTEM_DPI_AWARE = 1 # Int required.
ctypes.OleDLL('shcore').SetProcessDpiAwareness(PROCESS_SYSTEM_DPI_AWARE)
except (ImportError, AttributeError, OSError):
pass
if __name__ == '__main__':
from unittest import main
main('idlelib.idle_test.test_util', verbosity=2)
@@ -0,0 +1,98 @@
from tkinter import Toplevel, TclError
import sys
class WindowList:
def __init__(self):
self.dict = {}
self.callbacks = []
def add(self, window):
window.after_idle(self.call_callbacks)
self.dict[str(window)] = window
def delete(self, window):
try:
del self.dict[str(window)]
except KeyError:
# Sometimes, destroy() is called twice
pass
self.call_callbacks()
def add_windows_to_menu(self, menu):
list = []
for key in self.dict:
window = self.dict[key]
try:
title = window.get_title()
except TclError:
continue
list.append((title, key, window))
list.sort()
for title, key, window in list:
menu.add_command(label=title, command=window.wakeup)
def register_callback(self, callback):
self.callbacks.append(callback)
def unregister_callback(self, callback):
try:
self.callbacks.remove(callback)
except ValueError:
pass
def call_callbacks(self):
for callback in self.callbacks:
try:
callback()
except:
t, v, tb = sys.exc_info()
print("warning: callback failed in WindowList", t, ":", v)
registry = WindowList()
add_windows_to_menu = registry.add_windows_to_menu
register_callback = registry.register_callback
unregister_callback = registry.unregister_callback
class ListedToplevel(Toplevel):
def __init__(self, master, **kw):
Toplevel.__init__(self, master, kw)
registry.add(self)
self.focused_widget = self
def destroy(self):
registry.delete(self)
Toplevel.destroy(self)
# If this is Idle's last window then quit the mainloop
# (Needed for clean exit on Windows 98)
if not registry.dict:
self.quit()
def update_windowlist_registry(self, window):
registry.call_callbacks()
def get_title(self):
# Subclass can override
return self.wm_title()
def wakeup(self):
try:
if self.wm_state() == "iconic":
self.wm_withdraw()
self.wm_deiconify()
self.tkraise()
self.focused_widget.focus_set()
except TclError:
# This can happen when the Window menu was torn off.
# Simply ignore it.
pass
if __name__ == "__main__":
from unittest import main
main('idlelib.idle_test.test_window', verbosity=2)
@@ -0,0 +1,124 @@
"Zoom a window to maximum height."
import re
import sys
import tkinter
class WmInfoGatheringError(Exception):
pass
class ZoomHeight:
# Cached values for maximized window dimensions, one for each set
# of screen dimensions.
_max_height_and_y_coords = {}
def __init__(self, editwin):
self.editwin = editwin
self.top = self.editwin.top
def zoom_height_event(self, event=None):
zoomed = self.zoom_height()
if zoomed is None:
self.top.bell()
else:
menu_status = 'Restore' if zoomed else 'Zoom'
self.editwin.update_menu_label(menu='options', index='* Height',
label=f'{menu_status} Height')
return "break"
def zoom_height(self):
top = self.top
width, height, x, y = get_window_geometry(top)
if top.wm_state() != 'normal':
# Can't zoom/restore window height for windows not in the 'normal'
# state, e.g. maximized and full-screen windows.
return None
try:
maxheight, maxy = self.get_max_height_and_y_coord()
except WmInfoGatheringError:
return None
if height != maxheight:
# Maximize the window's height.
set_window_geometry(top, (width, maxheight, x, maxy))
return True
else:
# Restore the window's height.
#
# .wm_geometry('') makes the window revert to the size requested
# by the widgets it contains.
top.wm_geometry('')
return False
def get_max_height_and_y_coord(self):
top = self.top
screen_dimensions = (top.winfo_screenwidth(),
top.winfo_screenheight())
if screen_dimensions not in self._max_height_and_y_coords:
orig_state = top.wm_state()
# Get window geometry info for maximized windows.
try:
top.wm_state('zoomed')
except tkinter.TclError:
# The 'zoomed' state is not supported by some esoteric WMs,
# such as Xvfb.
raise WmInfoGatheringError(
'Failed getting geometry of maximized windows, because ' +
'the "zoomed" window state is unavailable.')
top.update()
maxwidth, maxheight, maxx, maxy = get_window_geometry(top)
if sys.platform == 'win32':
# On Windows, the returned Y coordinate is the one before
# maximizing, so we use 0 which is correct unless a user puts
# their dock on the top of the screen (very rare).
maxy = 0
maxrooty = top.winfo_rooty()
# Get the "root y" coordinate for non-maximized windows with their
# y coordinate set to that of maximized windows. This is needed
# to properly handle different title bar heights for non-maximized
# vs. maximized windows, as seen e.g. in Windows 10.
top.wm_state('normal')
top.update()
orig_geom = get_window_geometry(top)
max_y_geom = orig_geom[:3] + (maxy,)
set_window_geometry(top, max_y_geom)
top.update()
max_y_geom_rooty = top.winfo_rooty()
# Adjust the maximum window height to account for the different
# title bar heights of non-maximized vs. maximized windows.
maxheight += maxrooty - max_y_geom_rooty
self._max_height_and_y_coords[screen_dimensions] = maxheight, maxy
set_window_geometry(top, orig_geom)
top.wm_state(orig_state)
return self._max_height_and_y_coords[screen_dimensions]
def get_window_geometry(top):
geom = top.wm_geometry()
m = re.match(r"(\d+)x(\d+)\+(-?\d+)\+(-?\d+)", geom)
return tuple(map(int, m.groups()))
def set_window_geometry(top, geometry):
top.wm_geometry("{:d}x{:d}+{:d}+{:d}".format(*geometry))
if __name__ == "__main__":
from unittest import main
main('idlelib.idle_test.test_zoomheight', verbosity=2, exit=False)
# Add htest?
@@ -0,0 +1,73 @@
"""Example extension, also used for testing.
See extend.txt for more details on creating an extension.
See config-extension.def for configuring an extension.
"""
from idlelib.config import idleConf
from functools import wraps
def format_selection(format_line):
"Apply a formatting function to all of the selected lines."
@wraps(format_line)
def apply(self, event=None):
head, tail, chars, lines = self.formatter.get_region()
for pos in range(len(lines) - 1):
line = lines[pos]
lines[pos] = format_line(self, line)
self.formatter.set_region(head, tail, chars, lines)
return 'break'
return apply
class ZzDummy:
"""Prepend or remove initial text from selected lines."""
# Extend the format menu.
menudefs = [
('format', [
('Z in', '<<z-in>>'),
('Z out', '<<z-out>>'),
] )
]
def __init__(self, editwin):
"Initialize the settings for this extension."
self.editwin = editwin
self.text = editwin.text
self.formatter = editwin.fregion
@classmethod
def reload(cls):
"Load class variables from config."
cls.ztext = idleConf.GetOption('extensions', 'ZzDummy', 'z-text')
@format_selection
def z_in_event(self, line):
"""Insert text at the beginning of each selected line.
This is bound to the <<z-in>> virtual event when the extensions
are loaded.
"""
return f'{self.ztext}{line}'
@format_selection
def z_out_event(self, line):
"""Remove specific text from the beginning of each selected line.
This is bound to the <<z-out>> virtual event when the extensions
are loaded.
"""
zlength = 0 if not line.startswith(self.ztext) else len(self.ztext)
return line[zlength:]
ZzDummy.reload()
if __name__ == "__main__":
import unittest
unittest.main('idlelib.idle_test.test_zzdummy', verbosity=2, exit=False)