Update Pier_v1 project
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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).
|
||||
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
After Width: | Height: | Size: 120 B |
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 634 B |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 1019 B |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 75 B |
|
After Width: | Height: | Size: 125 B |
|
After Width: | Height: | Size: 78 B |
|
After Width: | Height: | Size: 380 B |
|
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.
|
||||
--------------------------------------------------------------------
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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 => 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">>>></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"><same</span> <span class="pre">args></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">>>> </span><span class="n">s</span> <span class="o">=</span> <span class="s1">'a</span><span class="se">\t</span><span class="s1">b</span><span class="se">\a</span><span class="s1"><</span><span class="se">\x02</span><span class="s1">><</span><span class="se">\r</span><span class="s1">></span><span class="se">\b</span><span class="s1">c</span><span class="se">\n</span><span class="s1">d'</span> <span class="c1"># Enter 22 chars.</span>
|
||||
<span class="gp">>>> </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">>>> </span><span class="n">s</span> <span class="c1"># Display repr(s)</span>
|
||||
<span class="go">'a\tb\x07<\x02><\r>\x08c\nd'</span>
|
||||
<span class="gp">>>> </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">''</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">>>></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 => 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)
|
||||
@@ -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)
|
||||