summaryrefslogtreecommitdiffstats
path: root/Lib/idlelib
diff options
context:
space:
mode:
Diffstat (limited to 'Lib/idlelib')
-rw-r--r--Lib/idlelib/AutoComplete.py12
-rw-r--r--Lib/idlelib/AutoCompleteWindow.py3
-rw-r--r--Lib/idlelib/Bindings.py6
-rw-r--r--Lib/idlelib/CallTipWindow.py8
-rw-r--r--Lib/idlelib/CallTips.py190
-rw-r--r--Lib/idlelib/ColorDelegator.py17
-rw-r--r--Lib/idlelib/Debugger.py3
-rw-r--r--Lib/idlelib/Delegator.py12
-rw-r--r--Lib/idlelib/EditorWindow.py145
-rw-r--r--Lib/idlelib/FormatParagraph.py138
-rw-r--r--Lib/idlelib/GrepDialog.py45
-rw-r--r--Lib/idlelib/HyperParser.py2
-rw-r--r--Lib/idlelib/IOBinding.py50
-rw-r--r--Lib/idlelib/Icons/idle.icobin0 -> 19790 bytes
-rw-r--r--Lib/idlelib/Icons/idle_16.gifbin0 -> 1034 bytes
-rw-r--r--Lib/idlelib/Icons/idle_16.pngbin0 -> 1264 bytes
-rw-r--r--Lib/idlelib/Icons/idle_32.gifbin0 -> 1435 bytes
-rw-r--r--Lib/idlelib/Icons/idle_32.pngbin0 -> 2542 bytes
-rw-r--r--Lib/idlelib/Icons/idle_48.gifbin0 -> 1388 bytes
-rw-r--r--Lib/idlelib/Icons/idle_48.pngbin0 -> 4710 bytes
-rw-r--r--Lib/idlelib/Icons/python.gifbin125 -> 585 bytes
-rw-r--r--Lib/idlelib/IdleHistory.py92
-rw-r--r--Lib/idlelib/MultiCall.py5
-rw-r--r--Lib/idlelib/NEWS.txt101
-rw-r--r--Lib/idlelib/OutputWindow.py2
-rw-r--r--Lib/idlelib/PathBrowser.py11
-rwxr-xr-x[-rw-r--r--]Lib/idlelib/PyShell.py197
-rw-r--r--Lib/idlelib/RstripExtension.py20
-rw-r--r--Lib/idlelib/ScriptBinding.py17
-rw-r--r--Lib/idlelib/SearchDialog.py5
-rw-r--r--Lib/idlelib/SearchDialogBase.py17
-rw-r--r--Lib/idlelib/SearchEngine.py110
-rw-r--r--Lib/idlelib/TreeWidget.py1
-rw-r--r--Lib/idlelib/__main__.py9
-rw-r--r--Lib/idlelib/aboutDialog.py7
-rw-r--r--Lib/idlelib/configDialog.py9
-rw-r--r--Lib/idlelib/configHandler.py21
-rw-r--r--Lib/idlelib/configSectionNameDialog.py111
-rw-r--r--Lib/idlelib/help.txt5
-rw-r--r--Lib/idlelib/idle_test/README.txt110
-rw-r--r--Lib/idlelib/idle_test/__init__.py9
-rw-r--r--Lib/idlelib/idle_test/mock_idle.py27
-rw-r--r--Lib/idlelib/idle_test/mock_tk.py279
-rw-r--r--Lib/idlelib/idle_test/test_calltips.py171
-rw-r--r--Lib/idlelib/idle_test/test_config_name.py75
-rw-r--r--Lib/idlelib/idle_test/test_delegator.py37
-rw-r--r--Lib/idlelib/idle_test/test_formatparagraph.py377
-rw-r--r--Lib/idlelib/idle_test/test_grep.py80
-rw-r--r--Lib/idlelib/idle_test/test_idlehistory.py167
-rw-r--r--Lib/idlelib/idle_test/test_pathbrowser.py12
-rw-r--r--Lib/idlelib/idle_test/test_rstrip.py49
-rw-r--r--Lib/idlelib/idle_test/test_searchengine.py329
-rw-r--r--Lib/idlelib/idle_test/test_text.py228
-rw-r--r--Lib/idlelib/idle_test/test_warning.py73
-rw-r--r--Lib/idlelib/idlever.py2
-rw-r--r--Lib/idlelib/macosxSupport.py16
-rw-r--r--Lib/idlelib/rpc.py30
-rw-r--r--Lib/idlelib/run.py79
-rw-r--r--Lib/idlelib/textView.py5
59 files changed, 2905 insertions, 621 deletions
diff --git a/Lib/idlelib/AutoComplete.py b/Lib/idlelib/AutoComplete.py
index e4c1aff..f366030 100644
--- a/Lib/idlelib/AutoComplete.py
+++ b/Lib/idlelib/AutoComplete.py
@@ -9,9 +9,6 @@ import string
from idlelib.configHandler import idleConf
-# This string includes all chars that may be in a file name (without a path
-# separator)
-FILENAME_CHARS = string.ascii_letters + string.digits + os.curdir + "._~#$:-"
# This string includes all chars that may be in an identifier
ID_CHARS = string.ascii_letters + string.digits + "_"
@@ -163,12 +160,9 @@ class AutoComplete:
if not comp_lists[0]:
return
self.autocompletewindow = self._make_autocomplete_window()
- self.autocompletewindow.show_window(comp_lists,
- "insert-%dc" % len(comp_start),
- complete,
- mode,
- userWantsWin)
- return True
+ return not self.autocompletewindow.show_window(
+ comp_lists, "insert-%dc" % len(comp_start),
+ complete, mode, userWantsWin)
def fetch_completions(self, what, mode):
"""Return a pair of lists of completions for something. The first list
diff --git a/Lib/idlelib/AutoCompleteWindow.py b/Lib/idlelib/AutoCompleteWindow.py
index 7787e70..f666ea6 100644
--- a/Lib/idlelib/AutoCompleteWindow.py
+++ b/Lib/idlelib/AutoCompleteWindow.py
@@ -157,13 +157,14 @@ class AutoCompleteWindow:
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
+ return completed == start
self.userwantswindow = userWantsWin
self.lasttypedstart = self.start
diff --git a/Lib/idlelib/Bindings.py b/Lib/idlelib/Bindings.py
index ec2720b..65c0317 100644
--- a/Lib/idlelib/Bindings.py
+++ b/Lib/idlelib/Bindings.py
@@ -15,7 +15,7 @@ from idlelib import macosxSupport
menudefs = [
# underscore prefixes character to underscore
('file', [
- ('_New Window', '<<open-new-window>>'),
+ ('_New File', '<<open-new-window>>'),
('_Open...', '<<open-window-from-file>>'),
('Open _Module...', '<<open-module>>'),
('Class _Browser', '<<open-class-browser>>'),
@@ -98,6 +98,10 @@ if macosxSupport.runningAsOSXApp():
# menu
del menudefs[-1][1][0:2]
+ # Remove the 'Configure' entry from the options menu, it is in the
+ # application menu as 'Preferences'
+ del menudefs[-2][1][0:2]
+
default_keydefs = idleConf.GetCurrentKeySet()
del sys
diff --git a/Lib/idlelib/CallTipWindow.py b/Lib/idlelib/CallTipWindow.py
index a2431f8..8e29dab 100644
--- a/Lib/idlelib/CallTipWindow.py
+++ b/Lib/idlelib/CallTipWindow.py
@@ -48,13 +48,7 @@ class CallTip:
def showtip(self, text, parenleft, parenright):
"""Show the calltip, bind events which will close it and reposition it.
"""
- # truncate overly long calltip
- if len(text) >= 79:
- textlines = text.splitlines()
- for i, line in enumerate(textlines):
- if len(line) > 79:
- textlines[i] = line[:75] + ' ...'
- text = '\n'.join(textlines)
+ # Only called in CallTips, where lines are truncated
self.text = text
if self.tipwindow or not self.text:
return
diff --git a/Lib/idlelib/CallTips.py b/Lib/idlelib/CallTips.py
index 3c8c096..81bd5f1 100644
--- a/Lib/idlelib/CallTips.py
+++ b/Lib/idlelib/CallTips.py
@@ -5,16 +5,16 @@ 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
-import inspect
from idlelib import CallTipWindow
from idlelib.HyperParser import HyperParser
-import __main__
-
class CallTips:
menudefs = [
@@ -116,152 +116,60 @@ def get_entity(expression):
# exception, especially if user classes are involved.
return None
-# The following are used in both get_argspec and tests
+# 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('(?<=\()\w*\,?\s*')
-_default_callable_argspec = "No docstring, see docs."
+_default_callable_argspec = "See source or doc"
+
def get_argspec(ob):
- '''Return a string describing the arguments and return of a callable object.
+ '''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 last line is the first line of the doc string. For builtins, this typically
- includes the arguments in addition to the return value.
-
+ The next lines are the first lines of the doc string up to the first
+ empty line or _MAX_LINES. For builtins, this typically includes
+ the arguments in addition to the return value.
'''
argspec = ""
- if hasattr(ob, '__call__'):
- if isinstance(ob, type):
- fob = getattr(ob, '__init__', None)
- elif isinstance(ob.__call__, types.MethodType):
- fob = ob.__call__
- else:
- fob = ob
- if isinstance(fob, (types.FunctionType, types.MethodType)):
- argspec = inspect.formatargspec(*inspect.getfullargspec(fob))
- if (isinstance(ob, (type, types.MethodType)) or
- isinstance(ob.__call__, types.MethodType)):
- argspec = _first_param.sub("", argspec)
-
- if isinstance(ob.__call__, types.MethodType):
- doc = ob.__call__.__doc__
- else:
- doc = getattr(ob, "__doc__", "")
- if doc:
- doc = doc.lstrip()
- pos = doc.find("\n")
- if pos < 0 or pos > 70:
- pos = 70
- if argspec:
- argspec += "\n"
- argspec += doc[:pos]
- if not argspec:
- argspec = _default_callable_argspec
+ try:
+ ob_call = ob.__call__
+ except BaseException:
+ return argspec
+ if isinstance(ob, type):
+ fob = ob.__init__
+ elif isinstance(ob_call, types.MethodType):
+ fob = ob_call
+ else:
+ fob = ob
+ if isinstance(fob, (types.FunctionType, types.MethodType)):
+ argspec = inspect.formatargspec(*inspect.getfullargspec(fob))
+ if (isinstance(ob, (type, types.MethodType)) or
+ isinstance(ob_call, types.MethodType)):
+ argspec = _first_param.sub("", argspec)
+
+ lines = (textwrap.wrap(argspec, _MAX_COLS, subsequent_indent=_INDENT)
+ if len(argspec) > _MAX_COLS else [argspec] if argspec else [])
+
+ if isinstance(ob_call, types.MethodType):
+ doc = ob_call.__doc__
+ else:
+ doc = getattr(ob, "__doc__", "")
+ if doc:
+ for line in doc.split('\n', _MAX_LINES)[:_MAX_LINES]:
+ line = line.strip()
+ if not line:
+ break
+ if len(line) > _MAX_COLS:
+ line = line[: _MAX_COLS - 3] + '...'
+ lines.append(line)
+ argspec = '\n'.join(lines)
+ if not argspec:
+ argspec = _default_callable_argspec
return argspec
-#################################################
-#
-# Test code tests CallTips.fetch_tip, get_entity, and get_argspec
-
-def main():
- # Putting expected in docstrings results in doubled tips for test
- def t1(): "()"
- def t2(a, b=None): "(a, b=None)"
- def t3(a, *args): "(a, *args)"
- def t4(*args): "(*args)"
- def t5(a, b=None, *args, **kw): "(a, b=None, *args, **kw)"
-
- class TC(object):
- "(ai=None, *b)"
- def __init__(self, ai=None, *b): "(self, ai=None, *b)"
- def t1(self): "(self)"
- def t2(self, ai, b=None): "(self, ai, b=None)"
- def t3(self, ai, *args): "(self, ai, *args)"
- def t4(self, *args): "(self, *args)"
- def t5(self, ai, b=None, *args, **kw): "(self, ai, b=None, *args, **kw)"
- def t6(no, self): "(no, self)"
- @classmethod
- def cm(cls, a): "(cls, a)"
- @staticmethod
- def sm(b): "(b)"
- def __call__(self, ci): "(self, ci)"
-
- tc = TC()
-
- # Python classes that inherit builtin methods
- class Int(int): "Int(x[, base]) -> integer"
- class List(list): "List() -> new empty list"
- # Simulate builtin with no docstring for default argspec test
- class SB: __call__ = None
-
- __main__.__dict__.update(locals()) # required for get_entity eval()
-
- num_tests = num_fail = 0
- tip = CallTips().fetch_tip
-
- def test(expression, expected):
- nonlocal num_tests, num_fail
- num_tests += 1
- argspec = tip(expression)
- if argspec != expected:
- num_fail += 1
- fmt = "%s - expected\n%r\n - but got\n%r"
- print(fmt % (expression, expected, argspec))
-
- def test_builtins():
- # if first line of a possibly multiline compiled docstring changes,
- # must change corresponding test string
- test('int', "int(x=0) -> integer")
- test('Int', Int.__doc__)
- test('types.MethodType', "method(function, instance)")
- test('list', "list() -> new empty list")
- test('List', List.__doc__)
- test('list.__new__',
- 'T.__new__(S, ...) -> a new object with type S, a subtype of T')
- test('list.__init__',
- 'x.__init__(...) initializes x; see help(type(x)) for signature')
- append_doc = "L.append(object) -> None -- append object to end"
- test('list.append', append_doc)
- test('[].append', append_doc)
- test('List.append', append_doc)
- test('SB()', _default_callable_argspec)
-
- def test_funcs():
- for func in (t1, t2, t3, t4, t5, TC,):
- fdoc = func.__doc__
- test(func.__name__, fdoc + "\n" + fdoc)
- for func in (TC.t1, TC.t2, TC.t3, TC.t4, TC.t5, TC.t6, TC.sm,
- TC.__call__):
- fdoc = func.__doc__
- test('TC.'+func.__name__, fdoc + "\n" + fdoc)
- fdoc = TC.cm.__func__.__doc__
- test('TC.cm.__func__', fdoc + "\n" + fdoc)
-
- def test_methods():
- # test that first parameter is correctly removed from argspec
- # using _first_param re to calculate expected masks re errors
- for meth, mdoc in ((tc.t1, "()"), (tc.t4, "(*args)"), (tc.t6, "(self)"),
- (TC.cm, "(a)"),):
- test('tc.'+meth.__name__, mdoc + "\n" + meth.__doc__)
- test('tc', "(ci)" + "\n" + tc.__call__.__doc__)
- # directly test that re works to delete unicode parameter name
- uni = "(A\u0391\u0410\u05d0\u0627\u0905\u1e00\u3042, a)" # various As
- assert _first_param.sub('', uni) == '(a)'
-
- def test_non_callables():
- # expression evaluates, but not to a callable
- for expr in ('0', '0.0' 'num_tests', b'num_tests', '[]', '{}'):
- test(expr, '')
- # expression does not evaluate, but raises an exception
- for expr in ('1a', 'xyx', 'num_tests.xyz', '[int][1]', '{0:int}[1]'):
- test(expr, '')
-
- test_builtins()
- test_funcs()
- test_non_callables()
- test_methods()
-
- print("%d of %d tests failed" % (num_fail, num_tests))
-
if __name__ == '__main__':
- main()
+ from unittest import main
+ main('idlelib.idle_test.test_calltips', verbosity=2)
diff --git a/Lib/idlelib/ColorDelegator.py b/Lib/idlelib/ColorDelegator.py
index e188192..61e2be4 100644
--- a/Lib/idlelib/ColorDelegator.py
+++ b/Lib/idlelib/ColorDelegator.py
@@ -21,10 +21,11 @@ def make_pat():
# 1st 'file' colorized normal, 2nd as builtin, 3rd as string
builtin = r"([^.'\"\\#]\b|^)" + any("BUILTIN", builtinlist) + r"\b"
comment = any("COMMENT", [r"#[^\n]*"])
- sqstring = r"(\b[rRbB])?'[^'\\\n]*(\\.[^'\\\n]*)*'?"
- dqstring = r'(\b[rRbB])?"[^"\\\n]*(\\.[^"\\\n]*)*"?'
- sq3string = r"(\b[rRbB])?'''[^'\\]*((\\.|'(?!''))[^'\\]*)*(''')?"
- dq3string = r'(\b[rRbB])?"""[^"\\]*((\\.|"(?!""))[^"\\]*)*(""")?'
+ stringprefix = r"(\br|u|ur|R|U|UR|Ur|uR|b|B|br|Br|bR|BR|rb|rB|Rb|RB)?"
+ sqstring = stringprefix + r"'[^'\\\n]*(\\.[^'\\\n]*)*'?"
+ dqstring = stringprefix + r'"[^"\\\n]*(\\.[^"\\\n]*)*"?'
+ sq3string = stringprefix + r"'''[^'\\]*((\\.|'(?!''))[^'\\]*)*(''')?"
+ dq3string = stringprefix + r'"""[^"\\]*((\\.|"(?!""))[^"\\]*)*(""")?'
string = any("STRING", [sq3string, dq3string, sqstring, dqstring])
return kw + "|" + builtin + "|" + comment + "|" + string +\
"|" + any("SYNC", [r"\n"])
@@ -50,6 +51,10 @@ class ColorDelegator(Delegator):
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):
for tag, cnf in self.tagdefs.items():
@@ -149,9 +154,9 @@ class ColorDelegator(Delegator):
self.stop_colorizing = False
self.colorizing = True
if DEBUG: print("colorizing...")
- t0 = time.clock()
+ t0 = time.perf_counter()
self.recolorize_main()
- t1 = time.clock()
+ t1 = time.perf_counter()
if DEBUG: print("%.3f seconds" % (t1-t0))
finally:
self.colorizing = False
diff --git a/Lib/idlelib/Debugger.py b/Lib/idlelib/Debugger.py
index ed66084..d4872ed 100644
--- a/Lib/idlelib/Debugger.py
+++ b/Lib/idlelib/Debugger.py
@@ -254,8 +254,7 @@ class Debugger:
self.sync_source_line()
def show_frame(self, stackitem):
- frame, lineno = stackitem
- self.frame = frame
+ self.frame = stackitem[0] # lineno is stackitem[1]
self.show_variables()
localsviewer = None
diff --git a/Lib/idlelib/Delegator.py b/Lib/idlelib/Delegator.py
index 93253b9..c476516 100644
--- a/Lib/idlelib/Delegator.py
+++ b/Lib/idlelib/Delegator.py
@@ -4,12 +4,12 @@ class Delegator:
def __init__(self, delegate=None):
self.delegate = delegate
- self.__cache = {}
+ self.__cache = set()
def __getattr__(self, name):
attr = getattr(self.delegate, name) # May raise AttributeError
setattr(self, name, attr)
- self.__cache[name] = attr
+ self.__cache.add(name)
return attr
def resetcache(self):
@@ -20,14 +20,6 @@ class Delegator:
pass
self.__cache.clear()
- def cachereport(self):
- keys = list(self.__cache.keys())
- keys.sort()
- print(keys)
-
def setdelegate(self, delegate):
self.resetcache()
self.delegate = delegate
-
- def getdelegate(self):
- return self.delegate
diff --git a/Lib/idlelib/EditorWindow.py b/Lib/idlelib/EditorWindow.py
index 16f63c5..4bf1111 100644
--- a/Lib/idlelib/EditorWindow.py
+++ b/Lib/idlelib/EditorWindow.py
@@ -1,8 +1,10 @@
-import sys
+import importlib
+import importlib.abc
import os
+from platform import python_version
import re
import string
-import imp
+import sys
from tkinter import *
import tkinter.simpledialog as tkSimpleDialog
import tkinter.messagebox as tkMessageBox
@@ -27,42 +29,13 @@ def _sphinx_version():
"Format sys.version_info to produce the Sphinx version string used to install the chm docs"
major, minor, micro, level, serial = sys.version_info
release = '%s%s' % (major, minor)
- if micro:
- release += '%s' % (micro,)
+ release += '%s' % (micro,)
if level == 'candidate':
release += 'rc%s' % (serial,)
elif level != 'final':
release += '%s%s' % (level[0], serial)
return release
-def _find_module(fullname, path=None):
- """Version of imp.find_module() that handles hierarchical module names"""
-
- file = None
- for tgt in fullname.split('.'):
- if file is not None:
- file.close() # close intermediate files
- (file, filename, descr) = imp.find_module(tgt, path)
- if descr[2] == imp.PY_SOURCE:
- break # find but not load the source file
- module = imp.load_module(tgt, file, filename, descr)
- try:
- path = module.__path__
- except AttributeError:
- raise ImportError('No source for module ' + module.__name__)
- if descr[2] != imp.PY_SOURCE:
- # If all of the above fails and didn't raise an exception,fallback
- # to a straight import which can find __init__.py in a package.
- m = __import__(fullname)
- try:
- filename = m.__file__
- except AttributeError:
- pass
- else:
- file = None
- descr = os.path.splitext(filename)[1], None, imp.PY_SOURCE
- return file, filename, descr
-
class HelpDialog(object):
@@ -120,7 +93,7 @@ class EditorWindow(object):
def __init__(self, flist=None, filename=None, key=None, root=None):
if EditorWindow.help_url is None:
- dochome = os.path.join(sys.prefix, 'Doc', 'index.html')
+ dochome = os.path.join(sys.base_prefix, 'Doc', 'index.html')
if sys.platform.count('linux'):
# look for html docs in a couple of standard places
pyver = 'python-docs-' + '%s.%s.%s' % sys.version_info[:3]
@@ -131,13 +104,13 @@ class EditorWindow(object):
dochome = os.path.join(basepath, pyver,
'Doc', 'index.html')
elif sys.platform[:3] == 'win':
- chmfile = os.path.join(sys.prefix, 'Doc',
+ chmfile = os.path.join(sys.base_prefix, 'Doc',
'Python%s.chm' % _sphinx_version())
if os.path.isfile(chmfile):
dochome = chmfile
elif macosxSupport.runningAsOSXApp():
# documentation is stored inside the python framework
- dochome = os.path.join(sys.prefix,
+ dochome = os.path.join(sys.base_prefix,
'Resources/English.lproj/Documentation/index.html')
dochome = os.path.normpath(dochome)
if os.path.isfile(dochome):
@@ -316,11 +289,10 @@ class EditorWindow(object):
self.good_load = True
is_py_src = self.ispythonsource(filename)
self.set_indentation_params(is_py_src)
- if is_py_src:
- self.color = color = self.ColorDelegator()
- per.insertfilter(color)
else:
io.set_filename(filename)
+ self.good_load = True
+
self.ResetColorizer()
self.saved_change_hook()
self.update_recent_files_list()
@@ -341,6 +313,36 @@ class EditorWindow(object):
self.askinteger = tkSimpleDialog.askinteger
self.showerror = tkMessageBox.showerror
+ self._highlight_workaround() # Fix selection tags on Windows
+
+ def _highlight_workaround(self):
+ # On Windows, Tk removes painting of the selection
+ # tags which is different behavior than on Linux and Mac.
+ # See issue14146 for more information.
+ if not sys.platform.startswith('win'):
+ return
+
+ text = self.text
+ text.event_add("<<Highlight-FocusOut>>", "<FocusOut>")
+ text.event_add("<<Highlight-FocusIn>>", "<FocusIn>")
+ def highlight_fix(focus):
+ sel_range = text.tag_ranges("sel")
+ if sel_range:
+ if focus == 'out':
+ HILITE_CONFIG = idleConf.GetHighlight(
+ idleConf.CurrentTheme(), 'hilite')
+ text.tag_config("sel_fix", HILITE_CONFIG)
+ text.tag_raise("sel_fix")
+ text.tag_add("sel_fix", *sel_range)
+ elif focus == 'in':
+ text.tag_remove("sel_fix", "1.0", "end")
+
+ text.bind("<<Highlight-FocusOut>>",
+ lambda ev: highlight_fix("out"))
+ text.bind("<<Highlight-FocusIn>>",
+ lambda ev: highlight_fix("in"))
+
+
def _filename_to_unicode(self, filename):
"""convert filename to unicode in order to display it in Tk"""
if isinstance(filename, str) or not filename:
@@ -434,7 +436,6 @@ class EditorWindow(object):
]
if macosxSupport.runningAsOSXApp():
- del menu_specs[-3]
menu_specs[-2] = ("windows", "_Window")
@@ -479,7 +480,12 @@ class EditorWindow(object):
if iswin:
self.text.config(cursor="arrow")
- for label, eventname, verify_state in self.rmenu_specs:
+ for item in self.rmenu_specs:
+ try:
+ label, eventname, verify_state = item
+ except ValueError: # see issue1207589
+ continue
+
if verify_state is None:
continue
state = getattr(self, verify_state)()
@@ -497,7 +503,8 @@ class EditorWindow(object):
def make_rmenu(self):
rmenu = Menu(self.text, tearoff=0)
- for label, eventname, _ in self.rmenu_specs:
+ for item in self.rmenu_specs:
+ label, eventname = item[0], item[1]
if label is not None:
def command(text=self.text, eventname=eventname):
text.event_generate(eventname)
@@ -653,20 +660,29 @@ class EditorWindow(object):
return
# XXX Ought to insert current file's directory in front of path
try:
- (f, file, (suffix, mode, type)) = _find_module(name)
- except (NameError, ImportError) as msg:
+ loader = importlib.find_loader(name)
+ except (ValueError, ImportError) as msg:
tkMessageBox.showerror("Import error", str(msg), parent=self.text)
return
- if type != imp.PY_SOURCE:
- tkMessageBox.showerror("Unsupported type",
- "%s is not a source module" % name, parent=self.text)
+ if loader is None:
+ tkMessageBox.showerror("Import error", "module not found",
+ parent=self.text)
+ return
+ if not isinstance(loader, importlib.abc.SourceLoader):
+ tkMessageBox.showerror("Import error", "not a source-based module",
+ parent=self.text)
+ return
+ try:
+ file_path = loader.get_filename(name)
+ except AttributeError:
+ tkMessageBox.showerror("Import error",
+ "loader does not support get_filename",
+ parent=self.text)
return
- if f:
- f.close()
if self.flist:
- self.flist.open(file)
+ self.flist.open(file_path)
else:
- self.io.loadfile(file)
+ self.io.loadfile(file_path)
def open_class_browser(self, event=None):
filename = self.io.filename
@@ -806,7 +822,11 @@ class EditorWindow(object):
menuEventDict[menu[0]][prepstr(item[0])[1]] = item[1]
for menubarItem in self.menudict:
menu = self.menudict[menubarItem]
- end = menu.index(END) + 1
+ end = menu.index(END)
+ if end is None:
+ # Skip empty menus
+ continue
+ end += 1
for index in range(0, end):
if menu.type(index) == 'command':
accel = menu.entrycget(index, 'accelerator')
@@ -863,12 +883,9 @@ class EditorWindow(object):
"Load and update the recent files list and menus"
rf_list = []
if os.path.exists(self.recent_files_path):
- rf_list_file = open(self.recent_files_path,'r',
- encoding='utf_8', errors='replace')
- try:
+ with open(self.recent_files_path, 'r',
+ encoding='utf_8', errors='replace') as rf_list_file:
rf_list = rf_list_file.readlines()
- finally:
- rf_list_file.close()
if new_file:
new_file = os.path.abspath(new_file) + '\n'
if new_file in rf_list:
@@ -886,7 +903,7 @@ class EditorWindow(object):
with open(self.recent_files_path, 'w',
encoding='utf_8', errors='replace') as rf_file:
rf_file.writelines(rf_list)
- except IOError as err:
+ except OSError as err:
if not getattr(self.root, "recentfilelist_error_displayed", False):
self.root.recentfilelist_error_displayed = True
tkMessageBox.showerror(title='IDLE Error',
@@ -939,11 +956,14 @@ class EditorWindow(object):
self.undo.reset_undo()
def short_title(self):
+ pyversion = "Python " + python_version() + ": "
filename = self.io.filename
if filename:
filename = os.path.basename(filename)
+ else:
+ filename = "Untitled"
# return unicode string to display non-ASCII chars correctly
- return self._filename_to_unicode(filename)
+ return pyversion + self._filename_to_unicode(filename)
def long_title(self):
# return unicode string to display non-ASCII chars correctly
@@ -1041,7 +1061,10 @@ class EditorWindow(object):
def load_extension(self, name):
try:
- mod = __import__(name, globals(), locals(), [])
+ try:
+ mod = importlib.import_module('.' + name, package=__package__)
+ except ImportError:
+ mod = importlib.import_module(name)
except ImportError:
print("\nFailed to import extension: ", name)
raise
@@ -1430,6 +1453,7 @@ class EditorWindow(object):
def tabify_region_event(self, event):
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:
@@ -1441,6 +1465,7 @@ class EditorWindow(object):
def untabify_region_event(self, event):
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)
@@ -1534,7 +1559,7 @@ class EditorWindow(object):
parent=self.text,
initialvalue=self.indentwidth,
minvalue=2,
- maxvalue=16) or self.tabwidth
+ maxvalue=16)
# Guess indentwidth from text content.
# Return guessed indentwidth. This should not be believed unless
diff --git a/Lib/idlelib/FormatParagraph.py b/Lib/idlelib/FormatParagraph.py
index e3ca7b9..ae4e6e7 100644
--- a/Lib/idlelib/FormatParagraph.py
+++ b/Lib/idlelib/FormatParagraph.py
@@ -1,18 +1,19 @@
-# Extension to format a paragraph
-
-# 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, arent handled :-)
+"""Extension to format a paragraph or selection to a max width.
+
+Does basic, standard text formatting, and also understands Python
+comment blocks. Thus, for editing Python source code, this
+extension is really only suitable for reformatting these comment
+blocks or triple-quoted strings.
+
+Known problems with comment reformatting:
+* If there is a selection marked, and the first line of the
+ selection is not complete, the block will probably not be detected
+ as comments, and will have the normal "text formatting" rules
+ applied.
+* If a comment block has leading whitespace that mixes tabs and
+ spaces, they will not be considered part of the same block.
+* Fancy comments, like this bulleted list, aren't handled :-)
+"""
import re
from idlelib.configHandler import idleConf
@@ -32,42 +33,31 @@ class FormatParagraph:
self.editwin = None
def format_paragraph_event(self, event):
- maxformatwidth = int(idleConf.GetOption('main', 'FormatParagraph',
- 'paragraph', type='int'))
+ """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.
+ """
+ maxformatwidth = idleConf.GetOption(
+ 'main', 'FormatParagraph', 'paragraph', type='int')
text = self.editwin.text
first, last = self.editwin.get_selection_indices()
if first and last:
data = text.get(first, last)
- comment_header = ''
+ comment_header = get_comment_header(data)
else:
first, last, comment_header, data = \
find_paragraph(text, text.index("insert"))
if comment_header:
- # Reformat the comment lines - convert to text sans header.
- lines = data.split("\n")
- lines = map(lambda st, l=len(comment_header): st[l:], lines)
- data = "\n".join(lines)
- # Reformat to maxformatwidth chars or a 20 char width,
- # whichever is greater.
- format_width = max(maxformatwidth - 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 dont want the comment
- # prefix inserted after it. (Im not sure it makes sense to
- # reformat a comment block that isnt 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]
- builder = lambda item, prefix=comment_header: prefix+item
- newdata = '\n'.join(map(builder, newdata)) + block_suffix
+ newdata = reformat_comment(data, maxformatwidth, comment_header)
else:
- # Just a normal text format
newdata = reformat_paragraph(data, maxformatwidth)
text.tag_remove("sel", "1.0", "end")
+
if newdata != data:
text.mark_set("insert", first)
text.undo_block_start()
@@ -80,31 +70,44 @@ class FormatParagraph:
return "break"
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.0 lineend" % lineno)
+ 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.0 lineend" % lineno)
+ 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.0 lineend" % lineno)
+ line = text.get("%d.0" % lineno, "%d.end" % lineno)
last = "%d.0" % lineno
- # Search back to beginning of paragraph
+
+ # Search back to beginning of paragraph (first blank line before)
lineno = first_lineno - 1
- line = text.get("%d.0" % lineno, "%d.0 lineend" % lineno)
+ 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.0 lineend" % lineno)
+ 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)
@@ -127,7 +130,7 @@ def reformat_paragraph(data, limit):
if not word:
continue # Can happen when line ends in whitespace
if len((partial + word).expandtabs()) > limit and \
- partial != indent1:
+ partial != indent1:
new.append(partial.rstrip())
partial = indent2
partial = partial + word + " "
@@ -139,13 +142,50 @@ def reformat_paragraph(data, limit):
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 dont 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 re.match(r"^(\s*)", line).group()
+ """Return the initial space or tab indent of line."""
+ return re.match(r"^([ \t]*)", line).group()
def get_comment_header(line):
- m = re.match(r"^(\s*#*)", line)
+ """Return string with leading whitespace and '#' from line or ''.
+
+ A null return indicates that the line is not a comment line. A non-
+ null return, such as ' #', will be used to find the other lines of
+ a comment block with the same indent.
+ """
+ m = re.match(r"^([ \t]*#*)", line)
if m is None: return ""
return m.group(1)
+
+if __name__ == "__main__":
+ from test import support; support.use_resources = ['gui']
+ import unittest
+ unittest.main('idlelib.idle_test.test_formatparagraph',
+ verbosity=2, exit=False)
diff --git a/Lib/idlelib/GrepDialog.py b/Lib/idlelib/GrepDialog.py
index 27fcc33..c359074 100644
--- a/Lib/idlelib/GrepDialog.py
+++ b/Lib/idlelib/GrepDialog.py
@@ -81,36 +81,24 @@ class GrepDialog(SearchDialogBase):
hits = 0
for fn in list:
try:
- f = open(fn, errors='replace')
- except IOError as msg:
+ 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("%s: %s: %s\n" %
+ (fn, lineno, line))
+ hits += 1
+ except OSError as msg:
print(msg)
- continue
- lineno = 0
- while 1:
- block = f.readlines(100000)
- if not block:
- break
- for line in block:
- lineno = lineno + 1
- if line[-1:] == '\n':
- line = line[:-1]
- if prog.search(line):
- sys.stdout.write("%s: %s: %s\n" % (fn, lineno, line))
- hits = hits + 1
- if hits:
- if hits == 1:
- s = ""
- else:
- s = "s"
- print("Found", hits, "hit%s." % s)
- print("(Hint: right-click to open locations.)")
- else:
- print("No hits.")
+ print(("Hits found: %s\n"
+ "(Hint: right-click to open locations.)"
+ % hits) if hits else "No hits.")
def findfiles(self, dir, base, rec):
try:
names = os.listdir(dir or os.curdir)
- except os.error as msg:
+ except OSError as msg:
print(msg)
return []
list = []
@@ -131,3 +119,10 @@ class GrepDialog(SearchDialogBase):
if self.top:
self.top.grab_release()
self.top.withdraw()
+
+if __name__ == "__main__":
+ # A human test is a bit tricky since EditorWindow() imports this module.
+ # Hence Idle must be restarted after editing this file for a live test.
+ import unittest
+ unittest.main('idlelib.idle_test.test_grep', verbosity=2, exit=False)
+
diff --git a/Lib/idlelib/HyperParser.py b/Lib/idlelib/HyperParser.py
index 4414de7..4af4b08 100644
--- a/Lib/idlelib/HyperParser.py
+++ b/Lib/idlelib/HyperParser.py
@@ -234,7 +234,7 @@ class HyperParser:
# 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 "rRbB":
+ while pos > 0 and rawtext[pos - 1] in "rRbBuU":
pos -= 1
last_identifier_pos = pos
break
diff --git a/Lib/idlelib/IOBinding.py b/Lib/idlelib/IOBinding.py
index c4f14ef..f008b46 100644
--- a/Lib/idlelib/IOBinding.py
+++ b/Lib/idlelib/IOBinding.py
@@ -1,6 +1,6 @@
import os
import types
-import pipes
+import shlex
import sys
import codecs
import tempfile
@@ -63,7 +63,8 @@ locale_encoding = locale_encoding.lower()
encoding = locale_encoding ### KBK 07Sep07 This is used all over IDLE, check!
### 'encoding' is used below in encode(), check!
-coding_re = re.compile("coding[:=]\s*([-\w_.]+)")
+coding_re = re.compile(r'^[ \t\f]*#.*coding[:=][ \t]*([-\w.]+)', re.ASCII)
+blank_re = re.compile(r'^[ \t\f]*(?:[#\r\n]|$)', re.ASCII)
def coding_spec(data):
"""Return the encoding declaration according to PEP 263.
@@ -84,14 +85,18 @@ def coding_spec(data):
lines = data
# consider only the first two lines
if '\n' in lines:
- lst = lines.split('\n')[:2]
+ lst = lines.split('\n', 2)[:2]
elif '\r' in lines:
- lst = lines.split('\r')[:2]
+ lst = lines.split('\r', 2)[:2]
+ else:
+ lst = [lines]
+ for line in lst:
+ match = coding_re.match(line)
+ if match is not None:
+ break
+ if not blank_re.match(line):
+ return None
else:
- lst = list(lines)
- str = '\n'.join(lst)
- match = coding_re.search(str)
- if not match:
return None
name = match.group(1)
try:
@@ -208,12 +213,11 @@ class IOBinding:
try:
# open the file in binary mode so that we can handle
# end-of-line convention ourselves.
- f = open(filename,'rb')
- two_lines = f.readline() + f.readline()
- f.seek(0)
- bytes = f.read()
- f.close()
- except IOError as msg:
+ with open(filename, 'rb') as f:
+ two_lines = f.readline() + f.readline()
+ f.seek(0)
+ bytes = f.read()
+ except OSError as msg:
tkMessageBox.showerror("I/O Error", str(msg), master=self.text)
return False
chars, converted = self._decode(two_lines, bytes)
@@ -373,12 +377,10 @@ class IOBinding:
text = text.replace("\n", self.eol_convention)
chars = self.encode(text)
try:
- f = open(filename, "wb")
- f.write(chars)
- f.flush()
- f.close()
+ with open(filename, "wb") as f:
+ f.write(chars)
return True
- except IOError as msg:
+ except OSError as msg:
tkMessageBox.showerror("I/O Error", str(msg),
master=self.text)
return False
@@ -459,7 +461,7 @@ class IOBinding:
else: #no printing for this platform
printPlatform = False
if printPlatform: #we can try to print for this platform
- command = command % pipes.quote(filename)
+ 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()
@@ -486,6 +488,8 @@ class IOBinding:
("All files", "*"),
]
+ defaultextension = '.py' if sys.platform == 'darwin' else ''
+
def askopenfile(self):
dir, base = self.defaultfilename("open")
if not self.opendialog:
@@ -509,8 +513,10 @@ class IOBinding:
def asksavefile(self):
dir, base = self.defaultfilename("save")
if not self.savedialog:
- self.savedialog = tkFileDialog.SaveAs(master=self.text,
- filetypes=self.filetypes)
+ self.savedialog = tkFileDialog.SaveAs(
+ master=self.text,
+ filetypes=self.filetypes,
+ defaultextension=self.defaultextension)
filename = self.savedialog.show(initialdir=dir, initialfile=base)
return filename
diff --git a/Lib/idlelib/Icons/idle.ico b/Lib/idlelib/Icons/idle.ico
new file mode 100644
index 0000000..3357aef
--- /dev/null
+++ b/Lib/idlelib/Icons/idle.ico
Binary files differ
diff --git a/Lib/idlelib/Icons/idle_16.gif b/Lib/idlelib/Icons/idle_16.gif
new file mode 100644
index 0000000..9f001b1
--- /dev/null
+++ b/Lib/idlelib/Icons/idle_16.gif
Binary files differ
diff --git a/Lib/idlelib/Icons/idle_16.png b/Lib/idlelib/Icons/idle_16.png
new file mode 100644
index 0000000..6abde0a
--- /dev/null
+++ b/Lib/idlelib/Icons/idle_16.png
Binary files differ
diff --git a/Lib/idlelib/Icons/idle_32.gif b/Lib/idlelib/Icons/idle_32.gif
new file mode 100644
index 0000000..af5b2d5
--- /dev/null
+++ b/Lib/idlelib/Icons/idle_32.gif
Binary files differ
diff --git a/Lib/idlelib/Icons/idle_32.png b/Lib/idlelib/Icons/idle_32.png
new file mode 100644
index 0000000..41b70db
--- /dev/null
+++ b/Lib/idlelib/Icons/idle_32.png
Binary files differ
diff --git a/Lib/idlelib/Icons/idle_48.gif b/Lib/idlelib/Icons/idle_48.gif
new file mode 100644
index 0000000..fc5304f
--- /dev/null
+++ b/Lib/idlelib/Icons/idle_48.gif
Binary files differ
diff --git a/Lib/idlelib/Icons/idle_48.png b/Lib/idlelib/Icons/idle_48.png
new file mode 100644
index 0000000..e5fa928
--- /dev/null
+++ b/Lib/idlelib/Icons/idle_48.png
Binary files differ
diff --git a/Lib/idlelib/Icons/python.gif b/Lib/idlelib/Icons/python.gif
index 58271ed..b189c2c 100644
--- a/Lib/idlelib/Icons/python.gif
+++ b/Lib/idlelib/Icons/python.gif
Binary files differ
diff --git a/Lib/idlelib/IdleHistory.py b/Lib/idlelib/IdleHistory.py
index 983a140..d6cb162 100644
--- a/Lib/idlelib/IdleHistory.py
+++ b/Lib/idlelib/IdleHistory.py
@@ -1,81 +1,93 @@
+"Implement Idle Shell history mechanism with History class"
+
from idlelib.configHandler 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.
- def __init__(self, text, output_sep = "\n"):
+ .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.history_prefix = None
- self.history_pointer = None
- self.output_sep = output_sep
+ 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):
- self.history_do(0)
+ "Fetch later statement; start with ealiest if cyclic."
+ self.fetch(reverse=False)
return "break"
def history_prev(self, event):
- self.history_do(1)
+ "Fetch earlier statement; start with most recent."
+ self.fetch(reverse=True)
return "break"
- def _get_source(self, start, end):
- # Get source code from start index to end index. Lines in the
- # text control may be separated by sys.ps2 .
- lines = self.text.get(start, end).split(self.output_sep)
- return "\n".join(lines)
+ def fetch(self, reverse):
+ '''Fetch statememt and replace current line in text widget.
- def _put_source(self, where, source):
- output = self.output_sep.join(source.split("\n"))
- self.text.insert(where, output)
-
- def history_do(self, reverse):
+ 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.history_pointer
- prefix = self.history_prefix
+ 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._get_source("iomark", "end-1c") != self.history[pointer]:
+ 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._get_source("iomark", "end-1c")
+ prefix = self.text.get("iomark", "end-1c")
if reverse:
- pointer = nhist
+ pointer = nhist # will be decremented
else:
if self.cyclic:
- pointer = -1
- else:
+ pointer = -1 # will be incremented
+ else: # abort history_next
self.text.bell()
return
nprefix = len(prefix)
while 1:
- if reverse:
- pointer = pointer - 1
- else:
- pointer = pointer + 1
+ pointer += -1 if reverse else 1
if pointer < 0 or pointer >= nhist:
self.text.bell()
- if not self.cyclic and pointer < 0:
+ if not self.cyclic and pointer < 0: # abort history_prev
return
else:
- if self._get_source("iomark", "end-1c") != prefix:
+ if self.text.get("iomark", "end-1c") != prefix:
self.text.delete("iomark", "end-1c")
- self._put_source("iomark", prefix)
+ self.text.insert("iomark", prefix)
pointer = prefix = None
break
item = self.history[pointer]
if item[:nprefix] == prefix and len(item) > nprefix:
self.text.delete("iomark", "end-1c")
- self._put_source("iomark", item)
+ self.text.insert("iomark", item)
break
- self.text.mark_set("insert", "end-1c")
self.text.see("insert")
self.text.tag_remove("sel", "1.0", "end")
- self.history_pointer = pointer
- self.history_prefix = prefix
+ self.pointer = pointer
+ self.prefix = prefix
- def history_store(self, source):
+ def store(self, source):
+ "Store Shell input statement into history list."
source = source.strip()
if len(source) > 2:
# avoid duplicates
@@ -84,5 +96,11 @@ class History:
except ValueError:
pass
self.history.append(source)
- self.history_pointer = None
- self.history_prefix = None
+ self.pointer = None
+ self.prefix = None
+
+if __name__ == "__main__":
+ from test import support
+ support.use_resources = ['gui']
+ from unittest import main
+ main('idlelib.idle_test.test_idlehistory', verbosity=2, exit=False)
diff --git a/Lib/idlelib/MultiCall.py b/Lib/idlelib/MultiCall.py
index 47f402d..64729ea 100644
--- a/Lib/idlelib/MultiCall.py
+++ b/Lib/idlelib/MultiCall.py
@@ -170,8 +170,9 @@ class _ComplexBinder:
break
ishandlerrunning[:] = []
# Call all functions in doafterhandler and remove them from list
- while doafterhandler:
- doafterhandler.pop()()
+ for f in doafterhandler:
+ f()
+ doafterhandler[:] = []
if r:
return r
return handler
diff --git a/Lib/idlelib/NEWS.txt b/Lib/idlelib/NEWS.txt
index 87c099f..6388d0d 100644
--- a/Lib/idlelib/NEWS.txt
+++ b/Lib/idlelib/NEWS.txt
@@ -1,4 +1,85 @@
-What's New in IDLE 3.2.4?
+What's New in IDLE 3.3.4?
+=========================
+
+- Issue #17390: Add Python version to Idle editor window title bar.
+ Original patches by Edmond Burnett and Kent Johnson.
+
+- Issue #18960: IDLE now ignores the source encoding declaration on the second
+ line if the first line contains anything except a comment.
+
+- Issue #20058: sys.stdin.readline() in IDLE now always returns only one line.
+
+- Issue #19481: print() of string subclass instance in IDLE no longer hangs.
+
+- Issue #18270: Prevent possible IDLE AttributeError on OS X when no initial
+ shell window is present.
+
+
+What's New in IDLE 3.3.3?
+=========================
+
+- Issue #18873: IDLE now detects Python source code encoding only in comment
+ lines.
+
+- Issue #18988: The "Tab" key now works when a word is already autocompleted.
+
+- Issue #18489: Add tests for SearchEngine. Original patch by Phil Webster.
+
+- Issue #18429: Format / Format Paragraph, now works when comment blocks
+ are selected. As with text blocks, this works best when the selection
+ only includes complete lines.
+
+- Issue #18226: Add docstrings and unittests for FormatParagraph.py.
+ Original patches by Todd Rovito and Phil Webster.
+
+- Issue #18279: Format - Strip trailing whitespace no longer marks a file as
+ changed when it has not been changed. This fix followed the addition of a
+ test file originally written by Phil Webster (the issue's main goal).
+
+- Issue #7136: In the Idle File menu, "New Window" is renamed "New File".
+ Patch by Tal Einat, Roget Serwy, and Todd Rovito.
+
+- Remove dead imports of imp.
+
+- Issue #18196: Avoid displaying spurious SystemExit tracebacks.
+
+- Issue #5492: Avoid traceback when exiting IDLE caused by a race condition.
+
+- Issue #17511: Keep IDLE find dialog open after clicking "Find Next".
+ Original patch by Sarah K.
+
+- Issue #18055: Move IDLE off of imp and on to importlib.
+
+- Issue #15392: Create a unittest framework for IDLE.
+ Initial patch by Rajagopalasarma Jayakrishnan.
+ See Lib/idlelib/idle_test/README.txt for how to run Idle tests.
+
+- Issue #14146: Highlight source line while debugging on Windows.
+
+- Issue #17532: Always include Options menu for IDLE on OS X.
+ Patch by Guilherme Simões.
+
+
+What's New in IDLE 3.3.2?
+=========================
+
+- Issue #17390: Display Python version on Idle title bar.
+ Initial patch by Edmond Burnett.
+
+
+What's New in IDLE 3.3.1?
+=========================
+
+- Issue #17625: Close the replace dialog after it is used.
+
+- Issue #16226: Fix IDLE Path Browser crash.
+ (Patch by Roger Serwy)
+
+- Issue #15853: Prevent IDLE crash on OS X when opening Preferences menu
+ with certain versions of Tk 8.5. Initial patch by Kevin Walzer.
+
+
+What's New in IDLE 3.3.0?
=========================
- Issue #17625: Close the replace dialog after it is used.
@@ -7,6 +88,9 @@ What's New in IDLE 3.2.4?
- Issue #15318: Prevent writing to sys.stdin.
+- Issue #4832: Modify IDLE to save files with .py extension by
+ default on Windows and OS X (Tk 8.5) as it already does with X11 Tk.
+
- Issue #13532, #15319: Check that arguments to sys.stdout.write are strings.
- Issue # 12510: Attempt to get certain tool tips no longer crashes IDLE.
@@ -20,15 +104,10 @@ What's New in IDLE 3.2.4?
- Issue #14937: Perform auto-completion of filenames in strings even for
non-ASCII filenames. Likewise for identifiers.
-- Issue #14018: Update checks for unstable system Tcl/Tk versions on OS X
- to include versions shipped with OS X 10.7 and 10.8 in addition to 10.6.
-
-- Issue #15853: Prevent IDLE crash on OS X when opening Preferences menu
- with certain versions of Tk 8.5. Initial patch by Kevin Walzer.
-
+- Issue #8515: Set __file__ when run file in IDLE.
+ Initial patch by Bruce Frederiksen.
-What's New in IDLE 3.2.3?
-=========================
+- IDLE can be launched as `python -m idlelib`
- Issue #14409: IDLE now properly executes commands in the Shell window
when it cannot read the normal config files on startup and
@@ -38,6 +117,9 @@ What's New in IDLE 3.2.3?
- Issue #3573: IDLE hangs when passing invalid command line args
(directory(ies) instead of file(s)).
+- Issue #14018: Update checks for unstable system Tcl/Tk versions on OS X
+ to include versions shipped with OS X 10.7 and 10.8 in addition to 10.6.
+
What's New in IDLE 3.2.1?
=========================
@@ -857,4 +939,3 @@ What's New in IDLEfork 0.9 Alpha 1?
--------------------------------------------------------------------
Refer to HISTORY.txt for additional information on earlier releases.
--------------------------------------------------------------------
-
diff --git a/Lib/idlelib/OutputWindow.py b/Lib/idlelib/OutputWindow.py
index 745ccd2..9dacc49 100644
--- a/Lib/idlelib/OutputWindow.py
+++ b/Lib/idlelib/OutputWindow.py
@@ -106,7 +106,7 @@ class OutputWindow(EditorWindow):
f = open(filename, "r")
f.close()
break
- except IOError:
+ except OSError:
continue
else:
return None
diff --git a/Lib/idlelib/PathBrowser.py b/Lib/idlelib/PathBrowser.py
index d88a48e..ba40719 100644
--- a/Lib/idlelib/PathBrowser.py
+++ b/Lib/idlelib/PathBrowser.py
@@ -1,6 +1,6 @@
import os
import sys
-import imp
+import importlib.machinery
from idlelib.TreeWidget import TreeItem
from idlelib.ClassBrowser import ClassBrowser, ModuleBrowserTreeItem
@@ -70,9 +70,11 @@ class DirBrowserTreeItem(TreeItem):
def listmodules(self, allnames):
modules = {}
- suffixes = imp.get_suffixes()
+ suffixes = importlib.machinery.EXTENSION_SUFFIXES[:]
+ suffixes += importlib.machinery.SOURCE_SUFFIXES[:]
+ suffixes += importlib.machinery.BYTECODE_SUFFIXES[:]
sorted = []
- for suff, mode, flag in suffixes:
+ for suff in suffixes:
i = -len(suff)
for name in allnames[:]:
normed_name = os.path.normcase(name)
@@ -92,4 +94,5 @@ def main():
mainloop()
if __name__ == "__main__":
- main()
+ from unittest import main
+ main('idlelib.idle_test.test_pathbrowser', verbosity=2, exit=False)
diff --git a/Lib/idlelib/PyShell.py b/Lib/idlelib/PyShell.py
index 865472e..2e5ebb2 100644..100755
--- a/Lib/idlelib/PyShell.py
+++ b/Lib/idlelib/PyShell.py
@@ -16,6 +16,7 @@ import io
import linecache
from code import InteractiveInterpreter
+from platform import python_version, system
try:
from tkinter import *
@@ -44,35 +45,55 @@ PORT = 0 # someday pass in host, port for remote debug capability
# internal warnings to the console. ScriptBinding.check_syntax() will
# temporarily redirect the stream to the shell window to display warnings when
# checking user's code.
-global warning_stream
-warning_stream = sys.__stderr__
-try:
- import warnings
-except ImportError:
- pass
-else:
- def idle_showwarning(message, category, filename, lineno,
- file=None, line=None):
- if file is None:
- file = warning_stream
- try:
- file.write(warnings.formatwarning(message, category, filename,
- lineno, line=line))
- except IOError:
- pass ## file (probably __stderr__) is invalid, warning dropped.
- warnings.showwarning = idle_showwarning
- def idle_formatwarning(message, category, filename, lineno, line=None):
- """Format warnings the IDLE way"""
- s = "\nWarning (from warnings module):\n"
- s += ' File \"%s\", line %s\n' % (filename, lineno)
- if line is None:
- line = linecache.getline(filename, lineno)
- line = line.strip()
- if line:
- s += " %s\n" % line
- s += "%s: %s\n>>> " % (category.__name__, message)
- return s
- warnings.formatwarning = idle_formatwarning
+warning_stream = sys.__stderr__ # None, at least on Windows, if no console.
+import warnings
+
+def idle_formatwarning(message, category, filename, lineno, line=None):
+ """Format warnings the IDLE way."""
+
+ s = "\nWarning (from warnings module):\n"
+ s += ' File \"%s\", line %s\n' % (filename, lineno)
+ if line is None:
+ line = linecache.getline(filename, lineno)
+ line = line.strip()
+ if line:
+ s += " %s\n" % line
+ s += "%s: %s\n" % (category.__name__, message)
+ return s
+
+def idle_showwarning(
+ message, category, filename, lineno, file=None, line=None):
+ """Show Idle-format warning (after replacing warnings.showwarning).
+
+ The differences are the formatter called, the file=None replacement,
+ which can be None, the capture of the consequence AttributeError,
+ and the output of a hard-coded prompt.
+ """
+ if file is None:
+ file = warning_stream
+ try:
+ file.write(idle_formatwarning(
+ message, category, filename, lineno, line=line))
+ file.write(">>> ")
+ except (AttributeError, OSError):
+ pass # if file (probably __stderr__) is invalid, skip warning.
+
+_warnings_showwarning = None
+
+def capture_warnings(capture):
+ "Replace warning.showwarning with idle_showwarning, or reverse."
+
+ global _warnings_showwarning
+ if capture:
+ if _warnings_showwarning is None:
+ _warnings_showwarning = warnings.showwarning
+ warnings.showwarning = idle_showwarning
+ else:
+ if _warnings_showwarning is not None:
+ warnings.showwarning = _warnings_showwarning
+ _warnings_showwarning = None
+
+capture_warnings(True)
def extended_linecache_checkcache(filename=None,
orig_checkcache=linecache.checkcache):
@@ -110,12 +131,13 @@ class PyShellEditorWindow(EditorWindow):
self.breakpointPath = os.path.join(idleConf.GetUserCfgDir(),
'breakpoints.lst')
# whenever a file is changed, restore breakpoints
- if self.io.filename: self.restore_file_breaks()
def filename_changed_hook(old_hook=self.io.filename_change_hook,
self=self):
self.restore_file_breaks()
old_hook()
self.io.set_filename_change_hook(filename_changed_hook)
+ if self.io.filename:
+ self.restore_file_breaks()
rmenu_specs = [
("Cut", "<<cut>>", "rmenu_check_cut"),
@@ -211,7 +233,7 @@ class PyShellEditorWindow(EditorWindow):
try:
with open(self.breakpointPath, "r") as fp:
lines = fp.readlines()
- except IOError:
+ except OSError:
lines = []
try:
with open(self.breakpointPath, "w") as new_file:
@@ -222,7 +244,7 @@ class PyShellEditorWindow(EditorWindow):
breaks = self.breakpoints
if breaks:
new_file.write(filename + '=' + str(breaks) + '\n')
- except IOError as err:
+ except OSError as err:
if not getattr(self.root, "breakpoint_error_displayed", False):
self.root.breakpoint_error_displayed = True
tkMessageBox.showerror(title='IDLE Error',
@@ -232,6 +254,9 @@ class PyShellEditorWindow(EditorWindow):
def restore_file_breaks(self):
self.text.update() # this enables setting "BREAK" tags to be visible
+ if self.io is None:
+ # can happen if IDLE closes due to the .update() call
+ return
filename = self.io.filename
if filename is None:
return
@@ -362,6 +387,7 @@ class ModifiedInterpreter(InteractiveInterpreter):
self.port = PORT
self.original_compiler_flags = self.compile.compiler.flags
+ _afterid = None
rpcclt = None
rpcsubproc = None
@@ -453,6 +479,7 @@ class ModifiedInterpreter(InteractiveInterpreter):
self.display_no_subprocess_error()
return None
self.transfer_path(with_cwd=with_cwd)
+ console.stop_readline()
# annotate restart in shell window and mark it
console.text.delete("iomark", "end-1c")
if was_executing:
@@ -480,6 +507,12 @@ class ModifiedInterpreter(InteractiveInterpreter):
threading.Thread(target=self.__request_interrupt).start()
def kill_subprocess(self):
+ if self._afterid is not None:
+ self.tkconsole.text.after_cancel(self._afterid)
+ try:
+ self.rpcclt.listening_sock.close()
+ except AttributeError: # no socket
+ pass
try:
self.rpcclt.close()
except AttributeError: # no socket
@@ -522,7 +555,7 @@ class ModifiedInterpreter(InteractiveInterpreter):
return
try:
response = clt.pollresponse(self.active_seq, wait=0.05)
- except (EOFError, IOError, KeyboardInterrupt):
+ except (EOFError, OSError, KeyboardInterrupt):
# lost connection or subprocess terminated itself, restart
# [the KBI is from rpc.SocketIO.handle_EOF()]
if self.tkconsole.closing:
@@ -551,8 +584,8 @@ class ModifiedInterpreter(InteractiveInterpreter):
pass
# Reschedule myself
if not self.tkconsole.closing:
- self.tkconsole.text.after(self.tkconsole.pollinterval,
- self.poll_subprocess)
+ self._afterid = self.tkconsole.text.after(
+ self.tkconsole.pollinterval, self.poll_subprocess)
debugger = None
@@ -795,7 +828,7 @@ class ModifiedInterpreter(InteractiveInterpreter):
class PyShell(OutputWindow):
- shell_title = "Python Shell"
+ shell_title = "Python " + python_version() + " Shell"
# Override classes
ColorDelegator = ModifiedColorDelegator
@@ -812,7 +845,6 @@ class PyShell(OutputWindow):
]
if macosxSupport.runningAsOSXApp():
- del menu_specs[-3]
menu_specs[-2] = ("windows", "_Window")
@@ -848,8 +880,6 @@ class PyShell(OutputWindow):
text.bind("<<open-stack-viewer>>", self.open_stack_viewer)
text.bind("<<toggle-debugger>>", self.toggle_debugger)
text.bind("<<toggle-jit-stack-viewer>>", self.toggle_jit_stack_viewer)
- self.color = color = self.ColorDelegator()
- self.per.insertfilter(color)
if use_subprocess:
text.bind("<<view-restart>>", self.view_restart_mark)
text.bind("<<restart-shell>>", self.restart_shell)
@@ -887,6 +917,7 @@ class PyShell(OutputWindow):
canceled = False
endoffile = False
closing = False
+ _stop_readline_flag = False
def set_warning_stream(self, stream):
global warning_stream
@@ -962,14 +993,9 @@ class PyShell(OutputWindow):
parent=self.text)
if response is False:
return "cancel"
- if self.reading:
- self.top.quit()
+ self.stop_readline()
self.canceled = True
self.closing = True
- # Wait for poll_subprocess() rescheduling to stop
- self.text.after(2 * self.pollinterval, self.close2)
-
- def close2(self):
return EditorWindow.close(self)
def _close(self):
@@ -1009,6 +1035,8 @@ class PyShell(OutputWindow):
return False
else:
nosub = "==== No Subprocess ===="
+ sys.displayhook = rpc.displayhook
+
self.write("Python %s on %s\n%s\n%s" %
(sys.version, sys.platform, self.COPYRIGHT, nosub))
self.showprompt()
@@ -1016,6 +1044,12 @@ class PyShell(OutputWindow):
tkinter._default_root = None # 03Jan04 KBK What's this?
return True
+ def stop_readline(self):
+ if not self.reading: # no nested mainloop to exit.
+ return
+ self._stop_readline_flag = True
+ self.top.quit()
+
def readline(self):
save = self.reading
try:
@@ -1023,6 +1057,9 @@ class PyShell(OutputWindow):
self.top.mainloop() # nested mainloop()
finally:
self.reading = save
+ if self._stop_readline_flag:
+ self._stop_readline_flag = False
+ return ""
line = self.text.get("iomark", "end-1c")
if len(line) == 0: # may be EOF if we quit our mainloop with Ctrl-C
line = "\n"
@@ -1224,13 +1261,23 @@ class PyShell(OutputWindow):
def resetoutput(self):
source = self.text.get("iomark", "end-1c")
if self.history:
- self.history.history_store(source)
+ self.history.store(source)
if self.text.get("end-2c") != "\n":
self.text.insert("end-1c", "\n")
self.text.mark_set("iomark", "end-1c")
self.set_line_and_column()
def write(self, s, tags=()):
+ if isinstance(s, str) and len(s) and max(s) > '\uffff':
+ # Tk doesn't support outputting non-BMP characters
+ # Let's assume what printed string is not very long,
+ # find first non-BMP character and construct informative
+ # UnicodeEncodeError exception.
+ for start, char in enumerate(s):
+ if char > '\uffff':
+ break
+ raise UnicodeEncodeError("UCS-2", char, start, start+1,
+ 'Non-BMP character not supported in Tk')
try:
self.text.mark_gravity("iomark", "right")
count = OutputWindow.write(self, s, tags, "iomark")
@@ -1284,8 +1331,11 @@ class PseudoOutputFile(PseudoFile):
def write(self, s):
if self.closed:
raise ValueError("write to closed file")
- if not isinstance(s, str):
- raise TypeError('must be str, not ' + type(s).__name__)
+ if type(s) is not str:
+ if not isinstance(s, str):
+ raise TypeError('must be str, not ' + type(s).__name__)
+ # See issue #19481
+ s = str.__str__(s)
return self.shell.write(s, self.tags)
@@ -1331,9 +1381,15 @@ class PseudoInputFile(PseudoFile):
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()
+
usage_msg = """\
@@ -1391,8 +1447,9 @@ echo "import sys; print(sys.argv)" | idle - "foobar"
def main():
global flist, root, use_subprocess
+ capture_warnings(True)
use_subprocess = True
- enable_shell = True
+ enable_shell = False
enable_edit = False
debug = False
cmd = None
@@ -1413,7 +1470,6 @@ def main():
enable_shell = True
if o == '-e':
enable_edit = True
- enable_shell = False
if o == '-h':
sys.stdout.write(usage_msg)
sys.exit()
@@ -1464,9 +1520,22 @@ def main():
edit_start = idleConf.GetOption('main', 'General',
'editor-on-startup', type='bool')
enable_edit = enable_edit or edit_start
+ enable_shell = enable_shell or not enable_edit
# start editor and/or shell windows:
root = Tk(className="Idle")
+ # set application icon
+ icondir = os.path.join(os.path.dirname(__file__), 'Icons')
+ if system() == 'Windows':
+ iconfile = os.path.join(icondir, 'idle.ico')
+ root.wm_iconbitmap(default=iconfile)
+ elif TkVersion >= 8.5:
+ ext = '.png' if TkVersion >= 8.6 else '.gif'
+ iconfiles = [os.path.join(icondir, 'idle_%d%s' % (size, ext))
+ for size in (16, 32, 48)]
+ icons = [PhotoImage(file=iconfile) for iconfile in iconfiles]
+ root.wm_iconphoto(True, *icons)
+
fixwordbreaks(root)
root.withdraw()
flist = PyShellFileList(root)
@@ -1480,20 +1549,22 @@ def main():
args.remove(filename)
if not args:
flist.new()
+
if enable_shell:
shell = flist.open_shell()
if not shell:
return # couldn't open shell
-
if macosxSupport.runningAsOSXApp() and flist.dict:
# On OSX: when the user has double-clicked on a file that causes
# IDLE to be launched the shell window will open just in front of
# the file she wants to see. Lower the interpreter window when
# there are open files.
shell.top.lower()
+ else:
+ shell = flist.pyshell
- shell = flist.pyshell
- # handle remaining options:
+ # Handle remaining options. If any of these are set, enable_shell
+ # was set also, so shell must be true to reach here.
if debug:
shell.open_debugger()
if startup:
@@ -1501,7 +1572,7 @@ def main():
os.environ.get("PYTHONSTARTUP")
if filename and os.path.isfile(filename):
shell.interp.execfile(filename)
- if shell and cmd or script:
+ if cmd or script:
shell.interp.runcommand("""if 1:
import sys as _sys
_sys.argv = %r
@@ -1512,18 +1583,22 @@ def main():
elif script:
shell.interp.prepend_syspath(script)
shell.interp.execfile(script)
-
- # Check for problematic OS X Tk versions and print a warning message
- # in the IDLE shell window; this is less intrusive than always opening
- # a separate window.
- tkversionwarning = macosxSupport.tkVersionWarning(root)
- if tkversionwarning:
- shell.interp.runcommand(''.join(("print('", tkversionwarning, "')")))
+ elif shell:
+ # If there is a shell window and no cmd or script in progress,
+ # check for problematic OS X Tk versions and print a warning
+ # message in the IDLE shell window; this is less intrusive
+ # than always opening a separate window.
+ tkversionwarning = macosxSupport.tkVersionWarning(root)
+ if tkversionwarning:
+ shell.interp.runcommand("print('%s')" % tkversionwarning)
while flist.inversedict: # keep IDLE running while files are open.
root.mainloop()
root.destroy()
+ capture_warnings(False)
if __name__ == "__main__":
sys.modules['PyShell'] = sys.modules['__main__']
main()
+
+capture_warnings(False) # Make sure turned off; see issue 18081
diff --git a/Lib/idlelib/RstripExtension.py b/Lib/idlelib/RstripExtension.py
index 19e35d4..2ce3c7e 100644
--- a/Lib/idlelib/RstripExtension.py
+++ b/Lib/idlelib/RstripExtension.py
@@ -1,13 +1,9 @@
'Provides "Strip trailing whitespace" under the "Format" menu.'
-__author__ = "Roger D. Serwy <roger.serwy at gmail.com>"
-
class RstripExtension:
menudefs = [
- ('format', [None,
- ('Strip trailing whitespace', '<<do-rstrip>>'),
- ]),]
+ ('format', [None, ('Strip trailing whitespace', '<<do-rstrip>>'), ] ), ]
def __init__(self, editwin):
self.editwin = editwin
@@ -20,10 +16,18 @@ class RstripExtension:
undo.undo_block_start()
- end_line = int(float(text.index('end'))) + 1
+ end_line = int(float(text.index('end')))
for cur in range(1, end_line):
- txt = text.get('%i.0' % cur, '%i.0 lineend' % cur)
+ txt = text.get('%i.0' % cur, '%i.end' % cur)
+ raw = len(txt)
cut = len(txt.rstrip())
- text.delete('%i.%i' % (cur, cut), '%i.0 lineend' % cur)
+ # Since text.delete() marks file as changed, even if not,
+ # only call it when needed to actually delete something.
+ if cut < raw:
+ text.delete('%i.%i' % (cur, cut), '%i.end' % cur)
undo.undo_block_stop()
+
+if __name__ == "__main__":
+ import unittest
+ unittest.main('idlelib.idle_test.test_rstrip', verbosity=2, exit=False)
diff --git a/Lib/idlelib/ScriptBinding.py b/Lib/idlelib/ScriptBinding.py
index 18ce965..6bfe128 100644
--- a/Lib/idlelib/ScriptBinding.py
+++ b/Lib/idlelib/ScriptBinding.py
@@ -87,9 +87,8 @@ class ScriptBinding:
self.shell = shell = self.flist.open_shell()
saved_stream = shell.get_warning_stream()
shell.set_warning_stream(shell.stderr)
- f = open(filename, 'rb')
- source = f.read()
- f.close()
+ 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')
@@ -150,16 +149,16 @@ class ScriptBinding:
dirname = os.path.dirname(filename)
# XXX Too often this discards arguments the user just set...
interp.runcommand("""if 1:
- _filename = %r
+ __file__ = {filename!r}
import sys as _sys
from os.path import basename as _basename
if (not _sys.argv or
- _basename(_sys.argv[0]) != _basename(_filename)):
- _sys.argv = [_filename]
+ _basename(_sys.argv[0]) != _basename(__file__)):
+ _sys.argv = [__file__]
import os as _os
- _os.chdir(%r)
- del _filename, _sys, _basename, _os
- \n""" % (filename, dirname))
+ _os.chdir({dirname!r})
+ del _sys, _basename, _os
+ \n""".format(filename=filename, dirname=dirname))
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.
diff --git a/Lib/idlelib/SearchDialog.py b/Lib/idlelib/SearchDialog.py
index 76c444c..bf76c41 100644
--- a/Lib/idlelib/SearchDialog.py
+++ b/Lib/idlelib/SearchDialog.py
@@ -24,13 +24,12 @@ class SearchDialog(SearchDialogBase):
def create_widgets(self):
f = SearchDialogBase.create_widgets(self)
- self.make_button("Find", self.default_command, 1)
+ self.make_button("Find Next", self.default_command, 1)
def default_command(self, event=None):
if not self.engine.getprog():
return
- if self.find_again(self.text):
- self.close()
+ self.find_again(self.text)
def find_again(self, text):
if not self.engine.getpat():
diff --git a/Lib/idlelib/SearchDialogBase.py b/Lib/idlelib/SearchDialogBase.py
index 65914ac..b8b49b2 100644
--- a/Lib/idlelib/SearchDialogBase.py
+++ b/Lib/idlelib/SearchDialogBase.py
@@ -1,6 +1,23 @@
+'''Define SearchDialogBase used by Search, Replace, and Grep dialogs.'''
from tkinter import *
class SearchDialogBase:
+ '''Create most of a modal search dialog (make_frame, create_widgets).
+
+ The wide left column contains:
+ 1 or 2 text entry lines (create_entries, make_entry);
+ a row of standard radiobuttons (create_option_buttons);
+ a row of dialog specific radiobuttons (create_other_buttons).
+
+ The narrow right column contains command buttons
+ (create_command_buttons, make_button).
+ 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 and its execution function.
+ The other dialogs override methods to replace and add widgets.
+ '''
title = "Search Dialog"
icon = "Search"
diff --git a/Lib/idlelib/SearchEngine.py b/Lib/idlelib/SearchEngine.py
index 13a6a6b..9d3c4cb 100644
--- a/Lib/idlelib/SearchEngine.py
+++ b/Lib/idlelib/SearchEngine.py
@@ -1,26 +1,34 @@
+'''Define SearchEngine for search dialogs.'''
import re
-from tkinter import *
+from tkinter import StringVar, BooleanVar, TclError
import tkinter.messagebox as tkMessageBox
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)
- # XXX This will never garbage-collect -- who cares
+ # 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):
- self.root = root
- # State shared by search, replace, and grep;
- # the search dialogs bind these to UI elements.
- self.patvar = StringVar(root) # search pattern
- self.revar = BooleanVar(root) # regular expression?
- self.casevar = BooleanVar(root) # match case?
- self.wordvar = BooleanVar(root) # match whole word?
- self.wrapvar = BooleanVar(root) # wrap around buffer?
- self.wrapvar.set(1) # (on by default)
- self.backvar = BooleanVar(root) # search backwards?
+ '''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
@@ -47,15 +55,23 @@ class SearchEngine:
# Higher level access methods
+ def setcookedpat(self, pat):
+ "Set pattern after escaping if re."
+ # called only in SearchDialog.py: 66
+ if self.isre():
+ pat = re.escape(pat)
+ self.setpat(pat)
+
def getcookedpat(self):
pat = self.getpat()
- if not self.isre():
+ 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")
@@ -67,50 +83,41 @@ class SearchEngine:
try:
prog = re.compile(pat, flags)
except re.error as what:
- try:
- msg, col = what
- except:
- msg = str(what)
- col = -1
+ args = what.args
+ msg = args[0]
+ col = arg[1] if len(args) >= 2 else -1
self.report_error(pat, msg, col)
return None
return prog
def report_error(self, pat, msg, col=-1):
- # Derived class could overrid this with something fancier
+ # Derived class could override this with something fancier
msg = "Error: " + str(msg)
if pat:
- msg = msg + "\np\Pattern: " + str(pat)
+ msg = msg + "\nPattern: " + str(pat)
if col >= 0:
msg = msg + "\nOffset: " + str(col)
tkMessageBox.showerror("Regular expression error",
msg, master=self.root)
- def setcookedpat(self, pat):
- if self.isre():
- pat = re.escape(pat)
- self.setpat(pat)
-
def search_text(self, text, prog=None, ok=0):
- """Search a text widget for the pattern.
+ '''Return (lineno, matchobj) or None for forward/backward search.
- If prog is given, it should be the precompiled pattern.
- Return a tuple (lineno, matchobj); None if not found.
+ This function calls the right function with the right arguments.
+ It directly return the result of that call.
- This obeys the wrap and direction (back) settings.
+ Text is a text widget. Prog is a precompiled pattern.
+ The ok parameteris a bit complicated as it has two effects.
- The search starts at the selection (if there is one) or
- at the insert mark (otherwise). If the search is forward,
- it starts at the right of the selection; for a backward
- search, it starts at the left end. An empty match exactly
- at either end of the selection (or at the insert mark if
- there is no selection) is ignored unless the ok flag is true
- -- this is done to guarantee progress.
+ 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.
- If the search is allowed to wrap around, it will return the
- original selection if (and only if) it is the only match.
+ 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:
@@ -179,15 +186,19 @@ class SearchEngine:
col = len(chars) - 1
return None
-# Helper to search backwards in a string.
-# (Optimized for the case where the pattern isn't found.)
-
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()
+ i, j = m.span() # m.start(), m.end() == match slice indexes
while i < col and j <= col:
found = m
if i == j:
@@ -198,10 +209,9 @@ def search_reverse(prog, chars, col):
i, j = m.span()
return found
-# Helper to get selection end points, defaulting to insert mark.
-# Return a tuple of indices ("line.col" strings).
-
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")
@@ -213,8 +223,12 @@ def get_selection(text):
last = first
return first, last
-# Helper to parse a text index into a (line, col) tuple.
-
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 test import support; support.use_resources = ['gui']
+ import unittest
+ unittest.main('idlelib.idle_test.test_searchengine', verbosity=2, exit=False)
diff --git a/Lib/idlelib/TreeWidget.py b/Lib/idlelib/TreeWidget.py
index d4e524b..25bae48 100644
--- a/Lib/idlelib/TreeWidget.py
+++ b/Lib/idlelib/TreeWidget.py
@@ -16,7 +16,6 @@
import os
from tkinter import *
-import imp
from idlelib import ZoomHeight
from idlelib.configHandler import idleConf
diff --git a/Lib/idlelib/__main__.py b/Lib/idlelib/__main__.py
new file mode 100644
index 0000000..0666f2f
--- /dev/null
+++ b/Lib/idlelib/__main__.py
@@ -0,0 +1,9 @@
+"""
+IDLE main entry point
+
+Run IDLE as python -m idlelib
+"""
+
+
+import idlelib.PyShell
+idlelib.PyShell.main()
diff --git a/Lib/idlelib/aboutDialog.py b/Lib/idlelib/aboutDialog.py
index cfccc0f..7fe1ab8 100644
--- a/Lib/idlelib/aboutDialog.py
+++ b/Lib/idlelib/aboutDialog.py
@@ -66,12 +66,7 @@ class AboutDialog(Toplevel):
labelPythonVer = Label(frameBg, text='Python version: ' + \
sys.version.split()[0], fg=self.fg, bg=self.bg)
labelPythonVer.grid(row=9, column=0, sticky=W, padx=10, pady=0)
- # handle weird tk version num in windoze python >= 1.6 (?!?)
- tkVer = repr(TkVersion).split('.')
- tkVer[len(tkVer)-1] = str('%.3g' % (float('.'+tkVer[len(tkVer)-1])))[2:]
- if tkVer[len(tkVer)-1] == '':
- tkVer[len(tkVer)-1] = '0'
- tkVer = '.'.join(tkVer)
+ tkVer = self.tk.call('info', 'patchlevel')
labelTkVer = Label(frameBg, text='Tk version: '+
tkVer, fg=self.fg, bg=self.bg)
labelTkVer.grid(row=9, column=1, sticky=W, padx=2, pady=0)
diff --git a/Lib/idlelib/configDialog.py b/Lib/idlelib/configDialog.py
index 1f4a3a5..efe5c43 100644
--- a/Lib/idlelib/configDialog.py
+++ b/Lib/idlelib/configDialog.py
@@ -82,9 +82,10 @@ class ConfigDialog(Toplevel):
else:
extraKwds=dict(padx=6, pady=3)
- self.buttonHelp = Button(frameActionButtons,text='Help',
- command=self.Help,takefocus=FALSE,
- **extraKwds)
+# Comment out button creation and packing until implement self.Help
+## self.buttonHelp = Button(frameActionButtons,text='Help',
+## command=self.Help,takefocus=FALSE,
+## **extraKwds)
self.buttonOk = Button(frameActionButtons,text='Ok',
command=self.Ok,takefocus=FALSE,
**extraKwds)
@@ -98,7 +99,7 @@ class ConfigDialog(Toplevel):
self.CreatePageHighlight()
self.CreatePageKeys()
self.CreatePageGeneral()
- self.buttonHelp.pack(side=RIGHT,padx=5)
+## self.buttonHelp.pack(side=RIGHT,padx=5)
self.buttonOk.pack(side=LEFT,padx=5)
self.buttonApply.pack(side=LEFT,padx=5)
self.buttonCancel.pack(side=LEFT,padx=5)
diff --git a/Lib/idlelib/configHandler.py b/Lib/idlelib/configHandler.py
index 7fa481d..a974d54 100644
--- a/Lib/idlelib/configHandler.py
+++ b/Lib/idlelib/configHandler.py
@@ -142,10 +142,11 @@ class IdleUserConfParser(IdleConfParser):
fname = self.file
try:
cfgFile = open(fname, 'w')
- except IOError:
+ except OSError:
os.unlink(fname)
cfgFile = open(fname, 'w')
- self.write(cfgFile)
+ with cfgFile:
+ self.write(cfgFile)
else:
self.RemoveFile()
@@ -206,7 +207,7 @@ class IdleConf:
userDir+',\n but the path does not exist.\n')
try:
sys.stderr.write(warn)
- except IOError:
+ except OSError:
pass
userDir = '~'
if userDir == "~": # still no path to home!
@@ -216,7 +217,7 @@ class IdleConf:
if not os.path.exists(userDir):
try:
os.mkdir(userDir)
- except (OSError, IOError):
+ except OSError:
warn = ('\n Warning: unable to create user config directory\n'+
userDir+'\n Check path and permissions.\n Exiting!\n\n')
sys.stderr.write(warn)
@@ -250,7 +251,7 @@ class IdleConf:
raw=raw)))
try:
sys.stderr.write(warning)
- except IOError:
+ except OSError:
pass
try:
if self.defaultCfg[configType].has_option(section,option):
@@ -267,13 +268,11 @@ class IdleConf:
(option, section, default))
try:
sys.stderr.write(warning)
- except IOError:
+ except OSError:
pass
return default
-
def SetOption(self, configType, section, option, value):
"""In user's config file, set section's option to value.
-
"""
self.userCfg[configType].SetOption(section, option, value)
@@ -379,7 +378,7 @@ class IdleConf:
(element, themeName, theme[element]))
try:
sys.stderr.write(warning)
- except IOError:
+ except OSError:
pass
colour=cfgParser.Get(themeName,element,default=theme[element])
theme[element]=colour
@@ -636,13 +635,11 @@ class IdleConf:
(event, keySetName, keyBindings[event]))
try:
sys.stderr.write(warning)
- except IOError:
+ except OSError:
pass
return keyBindings
-
def GetExtraHelpSourceList(self,configSet):
"""Fetch 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'
diff --git a/Lib/idlelib/configSectionNameDialog.py b/Lib/idlelib/configSectionNameDialog.py
index 4378d6f..b05e38e 100644
--- a/Lib/idlelib/configSectionNameDialog.py
+++ b/Lib/idlelib/configSectionNameDialog.py
@@ -1,97 +1,106 @@
"""
Dialog that allows user to specify a new config file section name.
Used to get new highlight theme and keybinding set names.
+The 'return value' for the dialog, used two placed in configDialog.py,
+is the .result attribute set in the Ok and Cancel methods.
"""
from tkinter import *
import tkinter.messagebox as tkMessageBox
class GetCfgSectionNameDialog(Toplevel):
- def __init__(self,parent,title,message,usedNames):
+ def __init__(self, parent, title, message, used_names):
"""
message - string, informational message to display
- usedNames - list, list of names already in use for validity check
+ used_names - string collection, names already in use for validity check
"""
Toplevel.__init__(self, parent)
self.configure(borderwidth=5)
- self.resizable(height=FALSE,width=FALSE)
+ self.resizable(height=FALSE, width=FALSE)
self.title(title)
self.transient(parent)
self.grab_set()
self.protocol("WM_DELETE_WINDOW", self.Cancel)
self.parent = parent
- self.message=message
- self.usedNames=usedNames
- self.result=''
- self.CreateWidgets()
- self.withdraw() #hide while setting geometry
+ self.message = message
+ self.used_names = used_names
+ self.create_widgets()
+ self.withdraw() #hide while setting geometry
self.update_idletasks()
#needs to be done here so that the winfo_reqwidth is valid
self.messageInfo.config(width=self.frameMain.winfo_reqwidth())
- self.geometry("+%d+%d" %
- ((parent.winfo_rootx()+((parent.winfo_width()/2)
- -(self.winfo_reqwidth()/2)),
- parent.winfo_rooty()+((parent.winfo_height()/2)
- -(self.winfo_reqheight()/2)) )) ) #centre dialog over parent
- self.deiconify() #geometry set, unhide
+ self.geometry(
+ "+%d+%d" % (
+ parent.winfo_rootx() +
+ (parent.winfo_width()/2 - self.winfo_reqwidth()/2),
+ parent.winfo_rooty() +
+ (parent.winfo_height()/2 - self.winfo_reqheight()/2)
+ ) ) #centre dialog over parent
+ self.deiconify() #geometry set, unhide
self.wait_window()
- def CreateWidgets(self):
- self.name=StringVar(self)
- self.fontSize=StringVar(self)
- self.frameMain = Frame(self,borderwidth=2,relief=SUNKEN)
- self.frameMain.pack(side=TOP,expand=TRUE,fill=BOTH)
- self.messageInfo=Message(self.frameMain,anchor=W,justify=LEFT,padx=5,pady=5,
- text=self.message)#,aspect=200)
- entryName=Entry(self.frameMain,textvariable=self.name,width=30)
+ def create_widgets(self):
+ self.name = StringVar(self.parent)
+ self.fontSize = StringVar(self.parent)
+ self.frameMain = Frame(self, borderwidth=2, relief=SUNKEN)
+ self.frameMain.pack(side=TOP, expand=TRUE, fill=BOTH)
+ self.messageInfo = Message(self.frameMain, anchor=W, justify=LEFT,
+ padx=5, pady=5, text=self.message) #,aspect=200)
+ entryName = Entry(self.frameMain, textvariable=self.name, width=30)
entryName.focus_set()
- self.messageInfo.pack(padx=5,pady=5)#,expand=TRUE,fill=BOTH)
- entryName.pack(padx=5,pady=5)
- frameButtons=Frame(self)
- frameButtons.pack(side=BOTTOM,fill=X)
- self.buttonOk = Button(frameButtons,text='Ok',
- width=8,command=self.Ok)
- self.buttonOk.grid(row=0,column=0,padx=5,pady=5)
- self.buttonCancel = Button(frameButtons,text='Cancel',
- width=8,command=self.Cancel)
- self.buttonCancel.grid(row=0,column=1,padx=5,pady=5)
+ self.messageInfo.pack(padx=5, pady=5) #, expand=TRUE, fill=BOTH)
+ entryName.pack(padx=5, pady=5)
- def NameOk(self):
- #simple validity check for a sensible
- #ConfigParser file section name
- nameOk=1
- name=self.name.get()
- name.strip()
+ frameButtons = Frame(self, pady=2)
+ frameButtons.pack(side=BOTTOM)
+ self.buttonOk = Button(frameButtons, text='Ok',
+ width=8, command=self.Ok)
+ self.buttonOk.pack(side=LEFT, padx=5)
+ self.buttonCancel = Button(frameButtons, text='Cancel',
+ width=8, command=self.Cancel)
+ self.buttonCancel.pack(side=RIGHT, padx=5)
+
+ def name_ok(self):
+ ''' After stripping entered name, check that it is a sensible
+ ConfigParser file section name. Return it if it is, '' if not.
+ '''
+ name = self.name.get().strip()
if not name: #no name specified
tkMessageBox.showerror(title='Name Error',
message='No name specified.', parent=self)
- nameOk=0
elif len(name)>30: #name too long
tkMessageBox.showerror(title='Name Error',
message='Name too long. It should be no more than '+
'30 characters.', parent=self)
- nameOk=0
- elif name in self.usedNames:
+ name = ''
+ elif name in self.used_names:
tkMessageBox.showerror(title='Name Error',
message='This name is already in use.', parent=self)
- nameOk=0
- return nameOk
+ name = ''
+ return name
def Ok(self, event=None):
- if self.NameOk():
- self.result=self.name.get().strip()
+ name = self.name_ok()
+ if name:
+ self.result = name
self.destroy()
def Cancel(self, event=None):
- self.result=''
+ self.result = ''
self.destroy()
if __name__ == '__main__':
- #test the dialog
- root=Tk()
+ import unittest
+ unittest.main('idlelib.idle_test.test_config_name', verbosity=2, exit=False)
+
+ # also human test the dialog
+ root = Tk()
def run():
- keySeq=''
dlg=GetCfgSectionNameDialog(root,'Get Name',
- 'The information here should need to be word wrapped. Test.')
+ "After the text entered with [Ok] is stripped, <nothing>, "
+ "'abc', or more that 30 chars are errors. "
+ "Close with a valid entry (printed), [Cancel], or [X]",
+ {'abc'})
print(dlg.result)
- Button(root,text='Dialog',command=run).pack()
+ Message(root, text='').pack() # will be needed for oher dialog tests
+ Button(root, text='Click to begin dialog test', command=run).pack()
root.mainloop()
diff --git a/Lib/idlelib/help.txt b/Lib/idlelib/help.txt
index 815ee40..ff786c5 100644
--- a/Lib/idlelib/help.txt
+++ b/Lib/idlelib/help.txt
@@ -5,7 +5,7 @@ separate window containing the menu is created.
File Menu:
- New Window -- Create a new editing window
+ New File -- Create a new file editing window
Open... -- Open an existing file
Recent Files... -- Open a list of recent files
Open Module... -- Open an existing module (searches sys.path)
@@ -233,8 +233,7 @@ Completions:
Python Shell window:
Control-c interrupts executing command.
- Control-d sends end-of-file; closes window if typed at >>> prompt
- (this is Control-z on Windows).
+ Control-d sends end-of-file; closes window if typed at >>> prompt.
Command history:
diff --git a/Lib/idlelib/idle_test/README.txt b/Lib/idlelib/idle_test/README.txt
new file mode 100644
index 0000000..6b92483
--- /dev/null
+++ b/Lib/idlelib/idle_test/README.txt
@@ -0,0 +1,110 @@
+README FOR IDLE TESTS IN IDLELIB.IDLE_TEST
+
+
+1. Test Files
+
+The idle directory, idlelib, has over 60 xyz.py files. The idle_test
+subdirectory should contain a test_xyy.py for each. (For test modules, make
+'xyz' lower case, and possibly shorten it.) Each file should start with the
+something like the following template, with the blanks after after '.' and 'as',
+and before and after '_' filled in.
+---
+import unittest
+from test.support import requires
+import idlelib. as
+
+class _Test(unittest.TestCase):
+
+ def test_(self):
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2, exit=2)
+---
+Idle tests are run with unittest; do not use regrtest's test_main.
+
+Once test_xyy is written, the following should go at the end of xyy.py,
+with xyz (lowercased) added after 'test_'.
+---
+if __name__ == "__main__":
+ from test import support; support.use_resources = ['gui']
+ import unittest
+ unittest.main('idlelib.idle_test.test_', verbosity=2, exit=False)
+---
+
+
+2. Gui Tests
+
+Gui tests need 'requires' and 'use_resources' from test.support
+(test.test_support in 2.7). A test is a gui test if it creates a Tk root or
+master object either directly or indirectly by instantiating a tkinter or
+idle class. For the benefit of buildbot machines that do not have a graphics
+screen, gui tests must be 'guarded' by "requires('gui')" in a setUp
+function or method. This will typically be setUpClass.
+
+To avoid interfering with other gui tests, all gui objects must be destroyed
+and deleted by the end of the test. If a widget, such as a Tk root, is created
+in a setUpX function, destroy it in the corresponding tearDownX. For module
+and class attributes, also delete the widget.
+---
+ @classmethod
+ def setUpClass(cls):
+ requires('gui')
+ cls.root = tk.Tk()
+
+ @classmethod
+ def tearDownClass(cls):
+ cls.root.destroy()
+ del cls.root
+---
+
+Support.requires('gui') returns true if it is either called in a main module
+(which never happens on buildbots) or if use_resources contains 'gui'.
+Use_resources is set by test.regrtest but not by unittest. So when running
+tests in another module with unittest, we set it ourselves, as in the xyz.py
+template above.
+
+Since non-gui tests always run, but gui tests only sometimes, tests of non-gui
+operations should best avoid needing a gui. Methods that make incidental use of
+tkinter (tk) variables and messageboxes can do this by using the mock classes in
+idle_test/mock_tk.py. There is also a mock text that will handle some uses of the
+tk Text widget.
+
+
+3. Running Tests
+
+Assume that xyz.py and test_xyz.py end with the "if __name__" statements given
+above. In Idle, pressing F5 in an editor window with either loaded will run all
+tests in the test_xyz file with the version of Python running Idle. The test
+report and any tracebacks will appear in the Shell window. The options in these
+"if __name__" statements are appropriate for developers running (as opposed to
+importing) either of the files during development: verbosity=2 lists all test
+methods in the file; exit=False avoids a spurious sys.exit traceback that would
+otherwise occur when running in Idle. The following command lines also run
+all test methods, including gui tests, in test_xyz.py. (The exceptions are that
+idlelib and idlelib.idle start Idle and idlelib.PyShell should (issue 18330).)
+
+python -m idlelib.xyz # With the capitalization of the xyz module
+python -m idlelib.idle_test.test_xyz
+
+To run all idle_test/test_*.py tests, either interactively
+('>>>', with unittest imported) or from a command line, use one of the
+following. (Notes: unittest does not run gui tests; in 2.7, 'test ' (with the
+space) is 'test.regrtest '; where present, -v and -ugui can be omitted.)
+
+>>> unittest.main('idlelib.idle_test', verbosity=2, exit=False)
+python -m unittest -v idlelib.idle_test
+python -m test -v -ugui test_idle
+python -m test.test_idle
+
+The idle tests are 'discovered' by idlelib.idle_test.__init__.load_tests,
+which is also imported into test.test_idle. Normally, neither file should be
+changed when working on individual test modules. The third command runs runs
+unittest indirectly through regrtest. The same happens when the entire test
+suite is run with 'python -m test'. So that command must work for buildbots
+to stay green. Idle tests must not disturb the environment in a way that
+makes other tests fail (issue 18081).
+
+To run an individual Testcase or test method, extend the dotted name given to
+unittest on the command line. (But gui tests will not this way.)
+
+python -m unittest -v idlelib.idle_test.test_xyz.Test_case.test_meth
diff --git a/Lib/idlelib/idle_test/__init__.py b/Lib/idlelib/idle_test/__init__.py
new file mode 100644
index 0000000..1bc9536
--- /dev/null
+++ b/Lib/idlelib/idle_test/__init__.py
@@ -0,0 +1,9 @@
+from os.path import dirname
+
+def load_tests(loader, standard_tests, pattern):
+ this_dir = dirname(__file__)
+ top_dir = dirname(dirname(this_dir))
+ package_tests = loader.discover(start_dir=this_dir, pattern='test*.py',
+ top_level_dir=top_dir)
+ standard_tests.addTests(package_tests)
+ return standard_tests
diff --git a/Lib/idlelib/idle_test/mock_idle.py b/Lib/idlelib/idle_test/mock_idle.py
new file mode 100644
index 0000000..c364a24
--- /dev/null
+++ b/Lib/idlelib/idle_test/mock_idle.py
@@ -0,0 +1,27 @@
+'''Mock classes that imitate idlelib modules or classes.
+
+Attributes and methods will be added as needed for tests.
+'''
+
+from idlelib.idle_test.mock_tk import Text
+
+class Editor:
+ '''Minimally imitate EditorWindow.EditorWindow class.
+ '''
+ def __init__(self, flist=None, filename=None, key=None, root=None):
+ self.text = Text()
+ self.undo = UndoDelegator()
+
+ def get_selection_indices(self):
+ first = self.text.index('1.0')
+ last = self.text.index('end')
+ return first, last
+
+class UndoDelegator:
+ '''Minimally imitate UndoDelegator,UndoDelegator class.
+ '''
+ # A real undo block is only needed for user interaction.
+ def undo_block_start(*args):
+ pass
+ def undo_block_stop(*args):
+ pass
diff --git a/Lib/idlelib/idle_test/mock_tk.py b/Lib/idlelib/idle_test/mock_tk.py
new file mode 100644
index 0000000..762bbc9
--- /dev/null
+++ b/Lib/idlelib/idle_test/mock_tk.py
@@ -0,0 +1,279 @@
+"""Classes that replace tkinter gui objects used by an object being tested.
+
+A gui object is anything with a master or parent paramenter, which is typically
+required in spite of what the doc strings say.
+"""
+
+class Var:
+ "Use for String/Int/BooleanVar: incomplete"
+ def __init__(self, master=None, value=None, name=None):
+ self.master = master
+ self.value = value
+ self.name = name
+ def set(self, value):
+ self.value = value
+ def get(self):
+ return self.value
+
+class Mbox_func:
+ """Generic mock for messagebox functions, which all have the same signature.
+
+ Instead of displaying a message box, the mock's call method saves the
+ arguments as instance attributes, which test functions can then examime.
+ """
+ def __init__(self):
+ self.result = None # The return for all show funcs
+ def __call__(self, title, message, *args, **kwds):
+ # Save all args for possible examination by tester
+ self.title = title
+ self.message = message
+ self.args = args
+ self.kwds = kwds
+ return self.result # Set by tester for ask functions
+
+class Mbox:
+ """Mock for tkinter.messagebox with an Mbox_func for each function.
+
+ This module was 'tkMessageBox' in 2.x; hence the 'import as' in 3.x.
+ Example usage in test_module.py for testing functions in module.py:
+ ---
+from idlelib.idle_test.mock_tk import Mbox
+import module
+
+orig_mbox = module.tkMessageBox
+showerror = Mbox.showerror # example, for attribute access in test methods
+
+class Test(unittest.TestCase):
+
+ @classmethod
+ def setUpClass(cls):
+ module.tkMessageBox = Mbox
+
+ @classmethod
+ def tearDownClass(cls):
+ module.tkMessageBox = orig_mbox
+ ---
+ For 'ask' functions, set func.result return value before calling the method
+ that uses the message function. When tkMessageBox functions are the
+ only gui alls in a method, this replacement makes the method gui-free,
+ """
+ askokcancel = Mbox_func() # True or False
+ askquestion = Mbox_func() # 'yes' or 'no'
+ askretrycancel = Mbox_func() # True or False
+ askyesno = Mbox_func() # True or False
+ askyesnocancel = Mbox_func() # True, False, or None
+ showerror = Mbox_func() # None
+ showinfo = Mbox_func() # None
+ showwarning = Mbox_func() # None
+
+from _tkinter import TclError
+
+class Text:
+ """A semi-functional non-gui replacement for tkinter.Text text editors.
+
+ The mock's data model is that a text is a list of \n-terminated lines.
+ The mock adds an empty string at the beginning of the list so that the
+ index of actual lines start at 1, as with Tk. The methods never see this.
+ Tk initializes files with a terminal \n that cannot be deleted. It is
+ invisible in the sense that one cannot move the cursor beyond it.
+
+ This class is only tested (and valid) with strings of ascii chars.
+ For testing, we are not concerned with Tk Text's treatment of,
+ for instance, 0-width characters or character + accent.
+ """
+ def __init__(self, master=None, cnf={}, **kw):
+ '''Initialize mock, non-gui, text-only Text widget.
+
+ At present, all args are ignored. Almost all affect visual behavior.
+ There are just a few Text-only options that affect text behavior.
+ '''
+ self.data = ['', '\n']
+
+ def index(self, index):
+ "Return string version of index decoded according to current text."
+ return "%s.%s" % self._decode(index, endflag=1)
+
+ def _decode(self, index, endflag=0):
+ """Return a (line, char) tuple of int indexes into self.data.
+
+ This implements .index without converting the result back to a string.
+ The result is contrained by the number of lines and linelengths of
+ self.data. For many indexes, the result is initially (1, 0).
+
+ The input index may have any of several possible forms:
+ * line.char float: converted to 'line.char' string;
+ * 'line.char' string, where line and char are decimal integers;
+ * 'line.char lineend', where lineend='lineend' (and char is ignored);
+ * 'line.end', where end='end' (same as above);
+ * 'insert', the positions before terminal \n;
+ * 'end', whose meaning depends on the endflag passed to ._endex.
+ * 'sel.first' or 'sel.last', where sel is a tag -- not implemented.
+ """
+ if isinstance(index, (float, bytes)):
+ index = str(index)
+ try:
+ index=index.lower()
+ except AttributeError:
+ raise TclError('bad text index "%s"' % index) from None
+
+ lastline = len(self.data) - 1 # same as number of text lines
+ if index == 'insert':
+ return lastline, len(self.data[lastline]) - 1
+ elif index == 'end':
+ return self._endex(endflag)
+
+ line, char = index.split('.')
+ line = int(line)
+
+ # Out of bounds line becomes first or last ('end') index
+ if line < 1:
+ return 1, 0
+ elif line > lastline:
+ return self._endex(endflag)
+
+ linelength = len(self.data[line]) -1 # position before/at \n
+ if char.endswith(' lineend') or char == 'end':
+ return line, linelength
+ # Tk requires that ignored chars before ' lineend' be valid int
+
+ # Out of bounds char becomes first or last index of line
+ char = int(char)
+ if char < 0:
+ char = 0
+ elif char > linelength:
+ char = linelength
+ return line, char
+
+ def _endex(self, endflag):
+ '''Return position for 'end' or line overflow corresponding to endflag.
+
+ -1: position before terminal \n; for .insert(), .delete
+ 0: position after terminal \n; for .get, .delete index 1
+ 1: same viewed as beginning of non-existent next line (for .index)
+ '''
+ n = len(self.data)
+ if endflag == 1:
+ return n, 0
+ else:
+ n -= 1
+ return n, len(self.data[n]) + endflag
+
+
+ def insert(self, index, chars):
+ "Insert chars before the character at index."
+
+ if not chars: # ''.splitlines() is [], not ['']
+ return
+ chars = chars.splitlines(True)
+ if chars[-1][-1] == '\n':
+ chars.append('')
+ line, char = self._decode(index, -1)
+ before = self.data[line][:char]
+ after = self.data[line][char:]
+ self.data[line] = before + chars[0]
+ self.data[line+1:line+1] = chars[1:]
+ self.data[line+len(chars)-1] += after
+
+
+ def get(self, index1, index2=None):
+ "Return slice from index1 to index2 (default is 'index1+1')."
+
+ startline, startchar = self._decode(index1)
+ if index2 is None:
+ endline, endchar = startline, startchar+1
+ else:
+ endline, endchar = self._decode(index2)
+
+ if startline == endline:
+ return self.data[startline][startchar:endchar]
+ else:
+ lines = [self.data[startline][startchar:]]
+ for i in range(startline+1, endline):
+ lines.append(self.data[i])
+ lines.append(self.data[endline][:endchar])
+ return ''.join(lines)
+
+
+ def delete(self, index1, index2=None):
+ '''Delete slice from index1 to index2 (default is 'index1+1').
+
+ Adjust default index2 ('index+1) for line ends.
+ Do not delete the terminal \n at the very end of self.data ([-1][-1]).
+ '''
+ startline, startchar = self._decode(index1, -1)
+ if index2 is None:
+ if startchar < len(self.data[startline])-1:
+ # not deleting \n
+ endline, endchar = startline, startchar+1
+ elif startline < len(self.data) - 1:
+ # deleting non-terminal \n, convert 'index1+1 to start of next line
+ endline, endchar = startline+1, 0
+ else:
+ # do not delete terminal \n if index1 == 'insert'
+ return
+ else:
+ endline, endchar = self._decode(index2, -1)
+ # restricting end position to insert position excludes terminal \n
+
+ if startline == endline and startchar < endchar:
+ self.data[startline] = self.data[startline][:startchar] + \
+ self.data[startline][endchar:]
+ elif startline < endline:
+ self.data[startline] = self.data[startline][:startchar] + \
+ self.data[endline][endchar:]
+ startline += 1
+ for i in range(startline, endline+1):
+ del self.data[startline]
+
+ def compare(self, index1, op, index2):
+ line1, char1 = self._decode(index1)
+ line2, char2 = self._decode(index2)
+ if op == '<':
+ return line1 < line2 or line1 == line2 and char1 < char2
+ elif op == '<=':
+ return line1 < line2 or line1 == line2 and char1 <= char2
+ elif op == '>':
+ return line1 > line2 or line1 == line2 and char1 > char2
+ elif op == '>=':
+ return line1 > line2 or line1 == line2 and char1 >= char2
+ elif op == '==':
+ return line1 == line2 and char1 == char2
+ elif op == '!=':
+ return line1 != line2 or char1 != char2
+ else:
+ raise TclError('''bad comparison operator "%s":'''
+ '''must be <, <=, ==, >=, >, or !=''' % op)
+
+ # The following Text methods normally do something and return None.
+ # Whether doing nothing is sufficient for a test will depend on the test.
+
+ def mark_set(self, name, index):
+ "Set mark *name* before the character at index."
+ pass
+
+ def mark_unset(self, *markNames):
+ "Delete all marks in markNames."
+
+ def tag_remove(self, tagName, index1, index2=None):
+ "Remove tag tagName from all characters between index1 and index2."
+ pass
+
+ # The following Text methods affect the graphics screen and return None.
+ # Doing nothing should always be sufficient for tests.
+
+ def scan_dragto(self, x, y):
+ "Adjust the view of the text according to scan_mark"
+
+ def scan_mark(self, x, y):
+ "Remember the current X, Y coordinates."
+
+ def see(self, index):
+ "Scroll screen to make the character at INDEX is visible."
+ pass
+
+ # The following is a Misc method inherited by Text.
+ # It should properly go in a Misc mock, but is included here for now.
+
+ def bind(sequence=None, func=None, add=None):
+ "Bind to this widget at event sequence a call to function func."
+ pass
diff --git a/Lib/idlelib/idle_test/test_calltips.py b/Lib/idlelib/idle_test/test_calltips.py
new file mode 100644
index 0000000..f363764
--- /dev/null
+++ b/Lib/idlelib/idle_test/test_calltips.py
@@ -0,0 +1,171 @@
+import unittest
+import idlelib.CallTips as ct
+import textwrap
+import types
+
+default_tip = ct._default_callable_argspec
+
+# Test Class TC is used in multiple get_argspec test methods
+class TC():
+ 'doc'
+ tip = "(ai=None, *b)"
+ def __init__(self, ai=None, *b): 'doc'
+ __init__.tip = "(self, ai=None, *b)"
+ def t1(self): 'doc'
+ t1.tip = "(self)"
+ def t2(self, ai, b=None): 'doc'
+ t2.tip = "(self, ai, b=None)"
+ def t3(self, ai, *args): 'doc'
+ t3.tip = "(self, ai, *args)"
+ def t4(self, *args): 'doc'
+ t4.tip = "(self, *args)"
+ def t5(self, ai, b=None, *args, **kw): 'doc'
+ t5.tip = "(self, ai, b=None, *args, **kw)"
+ def t6(no, self): 'doc'
+ t6.tip = "(no, self)"
+ def __call__(self, ci): 'doc'
+ __call__.tip = "(self, ci)"
+ # attaching .tip to wrapped methods does not work
+ @classmethod
+ def cm(cls, a): 'doc'
+ @staticmethod
+ def sm(b): 'doc'
+
+tc = TC()
+
+signature = ct.get_argspec # 2.7 and 3.x use different functions
+class Get_signatureTest(unittest.TestCase):
+ # The signature function must return a string, even if blank.
+ # Test a variety of objects to be sure that none cause it to raise
+ # (quite aside from getting as correct an answer as possible).
+ # The tests of builtins may break if inspect or the docstrings change,
+ # but a red buildbot is better than a user crash (as has happened).
+ # For a simple mismatch, change the expected output to the actual.
+
+ def test_builtins(self):
+
+ # Python class that inherits builtin methods
+ class List(list): "List() doc"
+ # Simulate builtin with no docstring for default tip test
+ class SB: __call__ = None
+
+ def gtest(obj, out):
+ self.assertEqual(signature(obj), out)
+
+ gtest(List, List.__doc__)
+ gtest(list.__new__,
+ 'T.__new__(S, ...) -> a new object with type S, a subtype of T')
+ gtest(list.__init__,
+ 'x.__init__(...) initializes x; see help(type(x)) for signature')
+ append_doc = "L.append(object) -> None -- append object to end"
+ gtest(list.append, append_doc)
+ gtest([].append, append_doc)
+ gtest(List.append, append_doc)
+
+ gtest(types.MethodType, "method(function, instance)")
+ gtest(SB(), default_tip)
+
+ def test_signature_wrap(self):
+ self.assertEqual(signature(textwrap.TextWrapper), '''\
+(width=70, initial_indent='', subsequent_indent='', expand_tabs=True,
+ replace_whitespace=True, fix_sentence_endings=False, break_long_words=True,
+ drop_whitespace=True, break_on_hyphens=True, tabsize=8)''')
+
+ def test_docline_truncation(self):
+ def f(): pass
+ f.__doc__ = 'a'*300
+ self.assertEqual(signature(f), '()\n' + 'a' * (ct._MAX_COLS-3) + '...')
+
+ def test_multiline_docstring(self):
+ # Test fewer lines than max.
+ self.assertEqual(signature(list),
+ "list() -> new empty list\n"
+ "list(iterable) -> new list initialized from iterable's items")
+
+ # Test max lines
+ self.assertEqual(signature(bytes), '''\
+bytes(iterable_of_ints) -> bytes
+bytes(string, encoding[, errors]) -> bytes
+bytes(bytes_or_buffer) -> immutable copy of bytes_or_buffer
+bytes(int) -> bytes object of size given by the parameter initialized with null bytes
+bytes() -> empty bytes object''')
+
+ # Test more than max lines
+ def f(): pass
+ f.__doc__ = 'a\n' * 15
+ self.assertEqual(signature(f), '()' + '\na' * ct._MAX_LINES)
+
+ def test_functions(self):
+ def t1(): 'doc'
+ t1.tip = "()"
+ def t2(a, b=None): 'doc'
+ t2.tip = "(a, b=None)"
+ def t3(a, *args): 'doc'
+ t3.tip = "(a, *args)"
+ def t4(*args): 'doc'
+ t4.tip = "(*args)"
+ def t5(a, b=None, *args, **kw): 'doc'
+ t5.tip = "(a, b=None, *args, **kw)"
+
+ for func in (t1, t2, t3, t4, t5, TC):
+ self.assertEqual(signature(func), func.tip + '\ndoc')
+
+ def test_methods(self):
+ for meth in (TC.t1, TC.t2, TC.t3, TC.t4, TC.t5, TC.t6, TC.__call__):
+ self.assertEqual(signature(meth), meth.tip + "\ndoc")
+ self.assertEqual(signature(TC.cm), "(a)\ndoc")
+ self.assertEqual(signature(TC.sm), "(b)\ndoc")
+
+ def test_bound_methods(self):
+ # test that first parameter is correctly removed from argspec
+ for meth, mtip in ((tc.t1, "()"), (tc.t4, "(*args)"), (tc.t6, "(self)"),
+ (tc.__call__, '(ci)'), (tc, '(ci)'), (TC.cm, "(a)"),):
+ self.assertEqual(signature(meth), mtip + "\ndoc")
+
+ def test_starred_parameter(self):
+ # test that starred first parameter is *not* removed from argspec
+ class C:
+ def m1(*args): pass
+ def m2(**kwds): pass
+ c = C()
+ for meth, mtip in ((C.m1, '(*args)'), (c.m1, "(*args)"),
+ (C.m2, "(**kwds)"), (c.m2, "(**kwds)"),):
+ self.assertEqual(signature(meth), mtip)
+
+ def test_non_ascii_name(self):
+ # test that re works to delete a first parameter name that
+ # includes non-ascii chars, such as various forms of A.
+ uni = "(A\u0391\u0410\u05d0\u0627\u0905\u1e00\u3042, a)"
+ assert ct._first_param.sub('', uni) == '(a)'
+
+ def test_no_docstring(self):
+ def nd(s):
+ pass
+ TC.nd = nd
+ self.assertEqual(signature(nd), "(s)")
+ self.assertEqual(signature(TC.nd), "(s)")
+ self.assertEqual(signature(tc.nd), "()")
+
+ def test_attribute_exception(self):
+ class NoCall:
+ def __getattr__(self, name):
+ raise BaseException
+ class Call(NoCall):
+ def __call__(self, ci):
+ pass
+ for meth, mtip in ((NoCall, default_tip), (Call, default_tip),
+ (NoCall(), ''), (Call(), '(ci)')):
+ self.assertEqual(signature(meth), mtip)
+
+ def test_non_callables(self):
+ for obj in (0, 0.0, '0', b'0', [], {}):
+ self.assertEqual(signature(obj), '')
+
+class Get_entityTest(unittest.TestCase):
+ def test_bad_entity(self):
+ self.assertIsNone(ct.get_entity('1/0'))
+ def test_good_entity(self):
+ self.assertIs(ct.get_entity('int'), int)
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2, exit=False)
diff --git a/Lib/idlelib/idle_test/test_config_name.py b/Lib/idlelib/idle_test/test_config_name.py
new file mode 100644
index 0000000..40e72b9
--- /dev/null
+++ b/Lib/idlelib/idle_test/test_config_name.py
@@ -0,0 +1,75 @@
+"""Unit tests for idlelib.configSectionNameDialog"""
+import unittest
+from idlelib.idle_test.mock_tk import Var, Mbox
+from idlelib import configSectionNameDialog as name_dialog_module
+
+name_dialog = name_dialog_module.GetCfgSectionNameDialog
+
+class Dummy_name_dialog:
+ # Mock for testing the following methods of name_dialog
+ name_ok = name_dialog.name_ok
+ Ok = name_dialog.Ok
+ Cancel = name_dialog.Cancel
+ # Attributes, constant or variable, needed for tests
+ used_names = ['used']
+ name = Var()
+ result = None
+ destroyed = False
+ def destroy(self):
+ self.destroyed = True
+
+# name_ok calls Mbox.showerror if name is not ok
+orig_mbox = name_dialog_module.tkMessageBox
+showerror = Mbox.showerror
+
+class ConfigNameTest(unittest.TestCase):
+ dialog = Dummy_name_dialog()
+
+ @classmethod
+ def setUpClass(cls):
+ name_dialog_module.tkMessageBox = Mbox
+
+ @classmethod
+ def tearDownClass(cls):
+ name_dialog_module.tkMessageBox = orig_mbox
+
+ def test_blank_name(self):
+ self.dialog.name.set(' ')
+ self.assertEqual(self.dialog.name_ok(), '')
+ self.assertEqual(showerror.title, 'Name Error')
+ self.assertIn('No', showerror.message)
+
+ def test_used_name(self):
+ self.dialog.name.set('used')
+ self.assertEqual(self.dialog.name_ok(), '')
+ self.assertEqual(showerror.title, 'Name Error')
+ self.assertIn('use', showerror.message)
+
+ def test_long_name(self):
+ self.dialog.name.set('good'*8)
+ self.assertEqual(self.dialog.name_ok(), '')
+ self.assertEqual(showerror.title, 'Name Error')
+ self.assertIn('too long', showerror.message)
+
+ def test_good_name(self):
+ self.dialog.name.set(' good ')
+ showerror.title = 'No Error' # should not be called
+ self.assertEqual(self.dialog.name_ok(), 'good')
+ self.assertEqual(showerror.title, 'No Error')
+
+ def test_ok(self):
+ self.dialog.destroyed = False
+ self.dialog.name.set('good')
+ self.dialog.Ok()
+ self.assertEqual(self.dialog.result, 'good')
+ self.assertTrue(self.dialog.destroyed)
+
+ def test_cancel(self):
+ self.dialog.destroyed = False
+ self.dialog.Cancel()
+ self.assertEqual(self.dialog.result, '')
+ self.assertTrue(self.dialog.destroyed)
+
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2, exit=False)
diff --git a/Lib/idlelib/idle_test/test_delegator.py b/Lib/idlelib/idle_test/test_delegator.py
new file mode 100644
index 0000000..b8ae5ee
--- /dev/null
+++ b/Lib/idlelib/idle_test/test_delegator.py
@@ -0,0 +1,37 @@
+import unittest
+from idlelib.Delegator import Delegator
+
+class DelegatorTest(unittest.TestCase):
+
+ def test_mydel(self):
+ # test a simple use scenario
+
+ # initialize
+ mydel = Delegator(int)
+ self.assertIs(mydel.delegate, int)
+ self.assertEqual(mydel._Delegator__cache, set())
+
+ # add an attribute:
+ self.assertRaises(AttributeError, mydel.__getattr__, 'xyz')
+ bl = mydel.bit_length
+ self.assertIs(bl, int.bit_length)
+ self.assertIs(mydel.__dict__['bit_length'], int.bit_length)
+ self.assertEqual(mydel._Delegator__cache, {'bit_length'})
+
+ # add a second attribute
+ mydel.numerator
+ self.assertEqual(mydel._Delegator__cache, {'bit_length', 'numerator'})
+
+ # delete the second (which, however, leaves it in the name cache)
+ del mydel.numerator
+ self.assertNotIn('numerator', mydel.__dict__)
+ self.assertIn('numerator', mydel._Delegator__cache)
+
+ # reset by calling .setdelegate, which calls .resetcache
+ mydel.setdelegate(float)
+ self.assertIs(mydel.delegate, float)
+ self.assertNotIn('bit_length', mydel.__dict__)
+ self.assertEqual(mydel._Delegator__cache, set())
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2, exit=2)
diff --git a/Lib/idlelib/idle_test/test_formatparagraph.py b/Lib/idlelib/idle_test/test_formatparagraph.py
new file mode 100644
index 0000000..f4a7c2d
--- /dev/null
+++ b/Lib/idlelib/idle_test/test_formatparagraph.py
@@ -0,0 +1,377 @@
+# Test the functions and main class method of FormatParagraph.py
+import unittest
+from idlelib import FormatParagraph as fp
+from idlelib.EditorWindow import EditorWindow
+from tkinter import Tk, Text, TclError
+from test.support import requires
+
+
+class Is_Get_Test(unittest.TestCase):
+ """Test the is_ and get_ functions"""
+ test_comment = '# This is a comment'
+ test_nocomment = 'This is not a comment'
+ trailingws_comment = '# This is a comment '
+ leadingws_comment = ' # This is a comment'
+ leadingws_nocomment = ' This is not a comment'
+
+ def test_is_all_white(self):
+ self.assertTrue(fp.is_all_white(''))
+ self.assertTrue(fp.is_all_white('\t\n\r\f\v'))
+ self.assertFalse(fp.is_all_white(self.test_comment))
+
+ def test_get_indent(self):
+ Equal = self.assertEqual
+ Equal(fp.get_indent(self.test_comment), '')
+ Equal(fp.get_indent(self.trailingws_comment), '')
+ Equal(fp.get_indent(self.leadingws_comment), ' ')
+ Equal(fp.get_indent(self.leadingws_nocomment), ' ')
+
+ def test_get_comment_header(self):
+ Equal = self.assertEqual
+ # Test comment strings
+ Equal(fp.get_comment_header(self.test_comment), '#')
+ Equal(fp.get_comment_header(self.trailingws_comment), '#')
+ Equal(fp.get_comment_header(self.leadingws_comment), ' #')
+ # Test non-comment strings
+ Equal(fp.get_comment_header(self.leadingws_nocomment), ' ')
+ Equal(fp.get_comment_header(self.test_nocomment), '')
+
+
+class FindTest(unittest.TestCase):
+ """Test the find_paragraph function in FormatParagraph.
+
+ Using the runcase() function, find_paragraph() is called with 'mark' set at
+ multiple indexes before and inside the test paragraph.
+
+ It appears that code with the same indentation as a quoted string is grouped
+ as part of the same paragraph, which is probably incorrect behavior.
+ """
+
+ @classmethod
+ def setUpClass(cls):
+ from idlelib.idle_test.mock_tk import Text
+ cls.text = Text()
+
+ def runcase(self, inserttext, stopline, expected):
+ # Check that find_paragraph returns the expected paragraph when
+ # the mark index is set to beginning, middle, end of each line
+ # up to but not including the stop line
+ text = self.text
+ text.insert('1.0', inserttext)
+ for line in range(1, stopline):
+ linelength = int(text.index("%d.end" % line).split('.')[1])
+ for col in (0, linelength//2, linelength):
+ tempindex = "%d.%d" % (line, col)
+ self.assertEqual(fp.find_paragraph(text, tempindex), expected)
+ text.delete('1.0', 'end')
+
+ def test_find_comment(self):
+ comment = (
+ "# Comment block with no blank lines before\n"
+ "# Comment line\n"
+ "\n")
+ self.runcase(comment, 3, ('1.0', '3.0', '#', comment[0:58]))
+
+ comment = (
+ "\n"
+ "# Comment block with whitespace line before and after\n"
+ "# Comment line\n"
+ "\n")
+ self.runcase(comment, 4, ('2.0', '4.0', '#', comment[1:70]))
+
+ comment = (
+ "\n"
+ " # Indented comment block with whitespace before and after\n"
+ " # Comment line\n"
+ "\n")
+ self.runcase(comment, 4, ('2.0', '4.0', ' #', comment[1:82]))
+
+ comment = (
+ "\n"
+ "# Single line comment\n"
+ "\n")
+ self.runcase(comment, 3, ('2.0', '3.0', '#', comment[1:23]))
+
+ comment = (
+ "\n"
+ " # Single line comment with leading whitespace\n"
+ "\n")
+ self.runcase(comment, 3, ('2.0', '3.0', ' #', comment[1:51]))
+
+ comment = (
+ "\n"
+ "# Comment immediately followed by code\n"
+ "x = 42\n"
+ "\n")
+ self.runcase(comment, 3, ('2.0', '3.0', '#', comment[1:40]))
+
+ comment = (
+ "\n"
+ " # Indented comment immediately followed by code\n"
+ "x = 42\n"
+ "\n")
+ self.runcase(comment, 3, ('2.0', '3.0', ' #', comment[1:53]))
+
+ comment = (
+ "\n"
+ "# Comment immediately followed by indented code\n"
+ " x = 42\n"
+ "\n")
+ self.runcase(comment, 3, ('2.0', '3.0', '#', comment[1:49]))
+
+ def test_find_paragraph(self):
+ teststring = (
+ '"""String with no blank lines before\n'
+ 'String line\n'
+ '"""\n'
+ '\n')
+ self.runcase(teststring, 4, ('1.0', '4.0', '', teststring[0:53]))
+
+ teststring = (
+ "\n"
+ '"""String with whitespace line before and after\n'
+ 'String line.\n'
+ '"""\n'
+ '\n')
+ self.runcase(teststring, 5, ('2.0', '5.0', '', teststring[1:66]))
+
+ teststring = (
+ '\n'
+ ' """Indented string with whitespace before and after\n'
+ ' Comment string.\n'
+ ' """\n'
+ '\n')
+ self.runcase(teststring, 5, ('2.0', '5.0', ' ', teststring[1:85]))
+
+ teststring = (
+ '\n'
+ '"""Single line string."""\n'
+ '\n')
+ self.runcase(teststring, 3, ('2.0', '3.0', '', teststring[1:27]))
+
+ teststring = (
+ '\n'
+ ' """Single line string with leading whitespace."""\n'
+ '\n')
+ self.runcase(teststring, 3, ('2.0', '3.0', ' ', teststring[1:55]))
+
+
+class ReformatFunctionTest(unittest.TestCase):
+ """Test the reformat_paragraph function without the editor window."""
+
+ def test_reformat_paragrah(self):
+ Equal = self.assertEqual
+ reform = fp.reformat_paragraph
+ hw = "O hello world"
+ Equal(reform(' ', 1), ' ')
+ Equal(reform("Hello world", 20), "Hello world")
+
+ # Test without leading newline
+ Equal(reform(hw, 1), "O\nhello\nworld")
+ Equal(reform(hw, 6), "O\nhello\nworld")
+ Equal(reform(hw, 7), "O hello\nworld")
+ Equal(reform(hw, 12), "O hello\nworld")
+ Equal(reform(hw, 13), "O hello world")
+
+ # Test with leading newline
+ hw = "\nO hello world"
+ Equal(reform(hw, 1), "\nO\nhello\nworld")
+ Equal(reform(hw, 6), "\nO\nhello\nworld")
+ Equal(reform(hw, 7), "\nO hello\nworld")
+ Equal(reform(hw, 12), "\nO hello\nworld")
+ Equal(reform(hw, 13), "\nO hello world")
+
+
+class ReformatCommentTest(unittest.TestCase):
+ """Test the reformat_comment function without the editor window."""
+
+ def test_reformat_comment(self):
+ Equal = self.assertEqual
+
+ # reformat_comment formats to a minimum of 20 characters
+ test_string = (
+ " \"\"\"this is a test of a reformat for a triple quoted string"
+ " will it reformat to less than 70 characters for me?\"\"\"")
+ result = fp.reformat_comment(test_string, 70, " ")
+ expected = (
+ " \"\"\"this is a test of a reformat for a triple quoted string will it\n"
+ " reformat to less than 70 characters for me?\"\"\"")
+ Equal(result, expected)
+
+ test_comment = (
+ "# this is a test of a reformat for a triple quoted string will "
+ "it reformat to less than 70 characters for me?")
+ result = fp.reformat_comment(test_comment, 70, "#")
+ expected = (
+ "# this is a test of a reformat for a triple quoted string will it\n"
+ "# reformat to less than 70 characters for me?")
+ Equal(result, expected)
+
+
+class FormatClassTest(unittest.TestCase):
+ def test_init_close(self):
+ instance = fp.FormatParagraph('editor')
+ self.assertEqual(instance.editwin, 'editor')
+ instance.close()
+ self.assertEqual(instance.editwin, None)
+
+
+# For testing format_paragraph_event, Initialize FormatParagraph with
+# a mock Editor with .text and .get_selection_indices. The text must
+# be a Text wrapper that adds two methods
+
+# A real EditorWindow creates unneeded, time-consuming baggage and
+# sometimes emits shutdown warnings like this:
+# "warning: callback failed in WindowList <class '_tkinter.TclError'>
+# : invalid command name ".55131368.windows".
+# Calling EditorWindow._close in tearDownClass prevents this but causes
+# other problems (windows left open).
+
+class TextWrapper:
+ def __init__(self, master):
+ self.text = Text(master=master)
+ def __getattr__(self, name):
+ return getattr(self.text, name)
+ def undo_block_start(self): pass
+ def undo_block_stop(self): pass
+
+class Editor:
+ def __init__(self, root):
+ self.text = TextWrapper(root)
+ get_selection_indices = EditorWindow. get_selection_indices
+
+class FormatEventTest(unittest.TestCase):
+ """Test the formatting of text inside a Text widget.
+
+ This is done with FormatParagraph.format.paragraph_event,
+ which calls functions in the module as appropriate.
+ """
+ test_string = (
+ " '''this is a test of a reformat for a triple "
+ "quoted string will it reformat to less than 70 "
+ "characters for me?'''\n")
+ multiline_test_string = (
+ " '''The first line is under the max width.\n"
+ " The second line's length is way over the max width. It goes "
+ "on and on until it is over 100 characters long.\n"
+ " Same thing with the third line. It is also way over the max "
+ "width, but FormatParagraph will fix it.\n"
+ " '''\n")
+ multiline_test_comment = (
+ "# The first line is under the max width.\n"
+ "# The second line's length is way over the max width. It goes on "
+ "and on until it is over 100 characters long.\n"
+ "# Same thing with the third line. It is also way over the max "
+ "width, but FormatParagraph will fix it.\n"
+ "# The fourth line is short like the first line.")
+
+ @classmethod
+ def setUpClass(cls):
+ requires('gui')
+ cls.root = Tk()
+ editor = Editor(root=cls.root)
+ cls.text = editor.text.text # Test code does not need the wrapper.
+ cls.formatter = fp.FormatParagraph(editor).format_paragraph_event
+ # Sets the insert mark just after the re-wrapped and inserted text.
+
+ @classmethod
+ def tearDownClass(cls):
+ cls.root.destroy()
+ del cls.root
+ del cls.text
+ del cls.formatter
+
+ def test_short_line(self):
+ self.text.insert('1.0', "Short line\n")
+ self.formatter("Dummy")
+ self.assertEqual(self.text.get('1.0', 'insert'), "Short line\n" )
+ self.text.delete('1.0', 'end')
+
+ def test_long_line(self):
+ text = self.text
+
+ # Set cursor ('insert' mark) to '1.0', within text.
+ text.insert('1.0', self.test_string)
+ text.mark_set('insert', '1.0')
+ self.formatter('ParameterDoesNothing')
+ result = text.get('1.0', 'insert')
+ # find function includes \n
+ expected = (
+" '''this is a test of a reformat for a triple quoted string will it\n"
+" reformat to less than 70 characters for me?'''\n") # yes
+ self.assertEqual(result, expected)
+ text.delete('1.0', 'end')
+
+ # Select from 1.11 to line end.
+ text.insert('1.0', self.test_string)
+ text.tag_add('sel', '1.11', '1.end')
+ self.formatter('ParameterDoesNothing')
+ result = text.get('1.0', 'insert')
+ # selection excludes \n
+ expected = (
+" '''this is a test of a reformat for a triple quoted string will it reformat\n"
+" to less than 70 characters for me?'''") # no
+ self.assertEqual(result, expected)
+ text.delete('1.0', 'end')
+
+ def test_multiple_lines(self):
+ text = self.text
+ # Select 2 long lines.
+ text.insert('1.0', self.multiline_test_string)
+ text.tag_add('sel', '2.0', '4.0')
+ self.formatter('ParameterDoesNothing')
+ result = text.get('2.0', 'insert')
+ expected = (
+" The second line's length is way over the max width. It goes on and\n"
+" on until it is over 100 characters long. Same thing with the third\n"
+" line. It is also way over the max width, but FormatParagraph will\n"
+" fix it.\n")
+ self.assertEqual(result, expected)
+ text.delete('1.0', 'end')
+
+ def test_comment_block(self):
+ text = self.text
+
+ # Set cursor ('insert') to '1.0', within block.
+ text.insert('1.0', self.multiline_test_comment)
+ self.formatter('ParameterDoesNothing')
+ result = text.get('1.0', 'insert')
+ expected = (
+"# The first line is under the max width. The second line's length is\n"
+"# way over the max width. It goes on and on until it is over 100\n"
+"# characters long. Same thing with the third line. It is also way over\n"
+"# the max width, but FormatParagraph will fix it. The fourth line is\n"
+"# short like the first line.\n")
+ self.assertEqual(result, expected)
+ text.delete('1.0', 'end')
+
+ # Select line 2, verify line 1 unaffected.
+ text.insert('1.0', self.multiline_test_comment)
+ text.tag_add('sel', '2.0', '3.0')
+ self.formatter('ParameterDoesNothing')
+ result = text.get('1.0', 'insert')
+ expected = (
+"# The first line is under the max width.\n"
+"# The second line's length is way over the max width. It goes on and\n"
+"# on until it is over 100 characters long.\n")
+ self.assertEqual(result, expected)
+ text.delete('1.0', 'end')
+
+# The following block worked with EditorWindow but fails with the mock.
+# Lines 2 and 3 get pasted together even though the previous block left
+# the previous line alone. More investigation is needed.
+## # Select lines 3 and 4
+## text.insert('1.0', self.multiline_test_comment)
+## text.tag_add('sel', '3.0', '5.0')
+## self.formatter('ParameterDoesNothing')
+## result = text.get('3.0', 'insert')
+## expected = (
+##"# Same thing with the third line. It is also way over the max width,\n"
+##"# but FormatParagraph will fix it. The fourth line is short like the\n"
+##"# first line.\n")
+## self.assertEqual(result, expected)
+## text.delete('1.0', 'end')
+
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2, exit=2)
diff --git a/Lib/idlelib/idle_test/test_grep.py b/Lib/idlelib/idle_test/test_grep.py
new file mode 100644
index 0000000..0d8ff0d
--- /dev/null
+++ b/Lib/idlelib/idle_test/test_grep.py
@@ -0,0 +1,80 @@
+""" !Changing this line will break Test_findfile.test_found!
+Non-gui unit tests for idlelib.GrepDialog methods.
+dummy_command calls grep_it calls findfiles.
+An exception raised in one method will fail callers.
+Otherwise, tests are mostly independent.
+*** Currently only test grep_it.
+"""
+import unittest
+from test.support import captured_stdout
+from idlelib.idle_test.mock_tk import Var
+from idlelib.GrepDialog import GrepDialog
+import re
+
+class Dummy_searchengine:
+ '''GrepDialog.__init__ calls parent SearchDiabolBase which attaches the
+ passed in SearchEngine instance as attribute 'engine'. Only a few of the
+ many possible self.engine.x attributes are needed here.
+ '''
+ def getpat(self):
+ return self._pat
+
+searchengine = Dummy_searchengine()
+
+class Dummy_grep:
+ # Methods tested
+ #default_command = GrepDialog.default_command
+ grep_it = GrepDialog.grep_it
+ findfiles = GrepDialog.findfiles
+ # Other stuff needed
+ recvar = Var(False)
+ engine = searchengine
+ def close(self): # gui method
+ pass
+
+grep = Dummy_grep()
+
+class FindfilesTest(unittest.TestCase):
+ # findfiles is really a function, not a method, could be iterator
+ # test that filename return filename
+ # test that idlelib has many .py files
+ # test that recursive flag adds idle_test .py files
+ pass
+
+class Grep_itTest(unittest.TestCase):
+ # Test captured reports with 0 and some hits.
+ # Should test file names, but Windows reports have mixed / and \ separators
+ # from incomplete replacement, so 'later'.
+
+ def report(self, pat):
+ grep.engine._pat = pat
+ with captured_stdout() as s:
+ grep.grep_it(re.compile(pat), __file__)
+ lines = s.getvalue().split('\n')
+ lines.pop() # remove bogus '' after last \n
+ return lines
+
+ def test_unfound(self):
+ pat = 'xyz*'*7
+ lines = self.report(pat)
+ self.assertEqual(len(lines), 2)
+ self.assertIn(pat, lines[0])
+ self.assertEqual(lines[1], 'No hits.')
+
+ def test_found(self):
+
+ pat = '""" !Changing this line will break Test_findfile.test_found!'
+ lines = self.report(pat)
+ self.assertEqual(len(lines), 5)
+ self.assertIn(pat, lines[0])
+ self.assertIn('py: 1:', lines[1]) # line number 1
+ self.assertIn('2', lines[3]) # hits found 2
+ self.assertTrue(lines[4].startswith('(Hint:'))
+
+class Default_commandTest(unittest.TestCase):
+ # To write this, mode OutputWindow import to top of GrepDialog
+ # so it can be replaced by captured_stdout in class setup/teardown.
+ pass
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2, exit=False)
diff --git a/Lib/idlelib/idle_test/test_idlehistory.py b/Lib/idlelib/idle_test/test_idlehistory.py
new file mode 100644
index 0000000..d7c3d70
--- /dev/null
+++ b/Lib/idlelib/idle_test/test_idlehistory.py
@@ -0,0 +1,167 @@
+import unittest
+from test.support import requires
+
+import tkinter as tk
+from tkinter import Text as tkText
+from idlelib.idle_test.mock_tk import Text as mkText
+from idlelib.IdleHistory import History
+from idlelib.configHandler import idleConf
+
+line1 = 'a = 7'
+line2 = 'b = a'
+
+class StoreTest(unittest.TestCase):
+ '''Tests History.__init__ and History.store with mock Text'''
+
+ @classmethod
+ def setUpClass(cls):
+ cls.text = mkText()
+ cls.history = History(cls.text)
+
+ def tearDown(self):
+ self.text.delete('1.0', 'end')
+ self.history.history = []
+
+ def test_init(self):
+ self.assertIs(self.history.text, self.text)
+ self.assertEqual(self.history.history, [])
+ self.assertIsNone(self.history.prefix)
+ self.assertIsNone(self.history.pointer)
+ self.assertEqual(self.history.cyclic,
+ idleConf.GetOption("main", "History", "cyclic", 1, "bool"))
+
+ def test_store_short(self):
+ self.history.store('a')
+ self.assertEqual(self.history.history, [])
+ self.history.store(' a ')
+ self.assertEqual(self.history.history, [])
+
+ def test_store_dup(self):
+ self.history.store(line1)
+ self.assertEqual(self.history.history, [line1])
+ self.history.store(line2)
+ self.assertEqual(self.history.history, [line1, line2])
+ self.history.store(line1)
+ self.assertEqual(self.history.history, [line2, line1])
+
+ def test_store_reset(self):
+ self.history.prefix = line1
+ self.history.pointer = 0
+ self.history.store(line2)
+ self.assertIsNone(self.history.prefix)
+ self.assertIsNone(self.history.pointer)
+
+
+class TextWrapper:
+ def __init__(self, master):
+ self.text = tkText(master=master)
+ self._bell = False
+ def __getattr__(self, name):
+ return getattr(self.text, name)
+ def bell(self):
+ self._bell = True
+
+class FetchTest(unittest.TestCase):
+ '''Test History.fetch with wrapped tk.Text.
+ '''
+ @classmethod
+ def setUpClass(cls):
+ requires('gui')
+ cls.root = tk.Tk()
+
+ def setUp(self):
+ self.text = text = TextWrapper(self.root)
+ text.insert('1.0', ">>> ")
+ text.mark_set('iomark', '1.4')
+ text.mark_gravity('iomark', 'left')
+ self.history = History(text)
+ self.history.history = [line1, line2]
+
+ @classmethod
+ def tearDownClass(cls):
+ cls.root.destroy()
+ del cls.root
+
+ def fetch_test(self, reverse, line, prefix, index, *, bell=False):
+ # Perform one fetch as invoked by Alt-N or Alt-P
+ # Test the result. The line test is the most important.
+ # The last two are diagnostic of fetch internals.
+ History = self.history
+ History.fetch(reverse)
+
+ Equal = self.assertEqual
+ Equal(self.text.get('iomark', 'end-1c'), line)
+ Equal(self.text._bell, bell)
+ if bell:
+ self.text._bell = False
+ Equal(History.prefix, prefix)
+ Equal(History.pointer, index)
+ Equal(self.text.compare("insert", '==', "end-1c"), 1)
+
+ def test_fetch_prev_cyclic(self):
+ prefix = ''
+ test = self.fetch_test
+ test(True, line2, prefix, 1)
+ test(True, line1, prefix, 0)
+ test(True, prefix, None, None, bell=True)
+
+ def test_fetch_next_cyclic(self):
+ prefix = ''
+ test = self.fetch_test
+ test(False, line1, prefix, 0)
+ test(False, line2, prefix, 1)
+ test(False, prefix, None, None, bell=True)
+
+ # Prefix 'a' tests skip line2, which starts with 'b'
+ def test_fetch_prev_prefix(self):
+ prefix = 'a'
+ self.text.insert('iomark', prefix)
+ self.fetch_test(True, line1, prefix, 0)
+ self.fetch_test(True, prefix, None, None, bell=True)
+
+ def test_fetch_next_prefix(self):
+ prefix = 'a'
+ self.text.insert('iomark', prefix)
+ self.fetch_test(False, line1, prefix, 0)
+ self.fetch_test(False, prefix, None, None, bell=True)
+
+ def test_fetch_prev_noncyclic(self):
+ prefix = ''
+ self.history.cyclic = False
+ test = self.fetch_test
+ test(True, line2, prefix, 1)
+ test(True, line1, prefix, 0)
+ test(True, line1, prefix, 0, bell=True)
+
+ def test_fetch_next_noncyclic(self):
+ prefix = ''
+ self.history.cyclic = False
+ test = self.fetch_test
+ test(False, prefix, None, None, bell=True)
+ test(True, line2, prefix, 1)
+ test(False, prefix, None, None, bell=True)
+ test(False, prefix, None, None, bell=True)
+
+ def test_fetch_cursor_move(self):
+ # Move cursor after fetch
+ self.history.fetch(reverse=True) # initialization
+ self.text.mark_set('insert', 'iomark')
+ self.fetch_test(True, line2, None, None, bell=True)
+
+ def test_fetch_edit(self):
+ # Edit after fetch
+ self.history.fetch(reverse=True) # initialization
+ self.text.delete('iomark', 'insert', )
+ self.text.insert('iomark', 'a =')
+ self.fetch_test(True, line1, 'a =', 0) # prefix is reset
+
+ def test_history_prev_next(self):
+ # Minimally test functions bound to events
+ self.history.history_prev('dummy event')
+ self.assertEqual(self.history.pointer, 1)
+ self.history.history_next('dummy event')
+ self.assertEqual(self.history.pointer, None)
+
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2, exit=2)
diff --git a/Lib/idlelib/idle_test/test_pathbrowser.py b/Lib/idlelib/idle_test/test_pathbrowser.py
new file mode 100644
index 0000000..7ad7c97
--- /dev/null
+++ b/Lib/idlelib/idle_test/test_pathbrowser.py
@@ -0,0 +1,12 @@
+import unittest
+import idlelib.PathBrowser as PathBrowser
+
+class PathBrowserTest(unittest.TestCase):
+
+ def test_DirBrowserTreeItem(self):
+ # Issue16226 - make sure that getting a sublist works
+ d = PathBrowser.DirBrowserTreeItem('')
+ d.GetSubList()
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2, exit=False)
diff --git a/Lib/idlelib/idle_test/test_rstrip.py b/Lib/idlelib/idle_test/test_rstrip.py
new file mode 100644
index 0000000..1c90b93
--- /dev/null
+++ b/Lib/idlelib/idle_test/test_rstrip.py
@@ -0,0 +1,49 @@
+import unittest
+import idlelib.RstripExtension as rs
+from idlelib.idle_test.mock_idle import Editor
+
+class rstripTest(unittest.TestCase):
+
+ def test_rstrip_line(self):
+ editor = Editor()
+ text = editor.text
+ do_rstrip = rs.RstripExtension(editor).do_rstrip
+
+ do_rstrip()
+ self.assertEqual(text.get('1.0', 'insert'), '')
+ text.insert('1.0', ' ')
+ do_rstrip()
+ self.assertEqual(text.get('1.0', 'insert'), '')
+ text.insert('1.0', ' \n')
+ do_rstrip()
+ self.assertEqual(text.get('1.0', 'insert'), '\n')
+
+ def test_rstrip_multiple(self):
+ editor = Editor()
+ # Uncomment following to verify that test passes with real widgets.
+## from idlelib.EditorWindow import EditorWindow as Editor
+## from tkinter import Tk
+## editor = Editor(root=Tk())
+ text = editor.text
+ do_rstrip = rs.RstripExtension(editor).do_rstrip
+
+ original = (
+ "Line with an ending tab \n"
+ "Line ending in 5 spaces \n"
+ "Linewithnospaces\n"
+ " indented line\n"
+ " indented line with trailing space \n"
+ " ")
+ stripped = (
+ "Line with an ending tab\n"
+ "Line ending in 5 spaces\n"
+ "Linewithnospaces\n"
+ " indented line\n"
+ " indented line with trailing space\n")
+
+ text.insert('1.0', original)
+ do_rstrip()
+ self.assertEqual(text.get('1.0', 'insert'), stripped)
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2, exit=False)
diff --git a/Lib/idlelib/idle_test/test_searchengine.py b/Lib/idlelib/idle_test/test_searchengine.py
new file mode 100644
index 0000000..129a5a3
--- /dev/null
+++ b/Lib/idlelib/idle_test/test_searchengine.py
@@ -0,0 +1,329 @@
+'''Test functions and SearchEngine class in SearchEngine.py.'''
+
+# With mock replacements, the module does not use any gui widgets.
+# The use of tk.Text is avoided (for now, until mock Text is improved)
+# by patching instances with an index function returning what is needed.
+# This works because mock Text.get does not use .index.
+
+import re
+import unittest
+from test.support import requires
+from tkinter import BooleanVar, StringVar, TclError # ,Tk, Text
+import tkinter.messagebox as tkMessageBox
+from idlelib import SearchEngine as se
+from idlelib.idle_test.mock_tk import Var, Mbox
+from idlelib.idle_test.mock_tk import Text as mockText
+
+def setUpModule():
+ # Replace s-e module tkinter imports other than non-gui TclError.
+ se.BooleanVar = Var
+ se.StringVar = Var
+ se.tkMessageBox = Mbox
+
+def tearDownModule():
+ # Restore 'just in case', though other tests should also replace.
+ se.BooleanVar = BooleanVar
+ se.StringVar = StringVar
+ se.tkMessageBox = tkMessageBox
+
+
+class Mock:
+ def __init__(self, *args, **kwargs): pass
+
+class GetTest(unittest.TestCase):
+ # SearchEngine.get returns singleton created & saved on first call.
+ def test_get(self):
+ saved_Engine = se.SearchEngine
+ se.SearchEngine = Mock # monkey-patch class
+ try:
+ root = Mock()
+ engine = se.get(root)
+ self.assertIsInstance(engine, se.SearchEngine)
+ self.assertIs(root._searchengine, engine)
+ self.assertIs(se.get(root), engine)
+ finally:
+ se.SearchEngine = saved_Engine # restore class to module
+
+class GetLineColTest(unittest.TestCase):
+ # Test simple text-independent helper function
+ def test_get_line_col(self):
+ self.assertEqual(se.get_line_col('1.0'), (1, 0))
+ self.assertEqual(se.get_line_col('1.11'), (1, 11))
+
+ self.assertRaises(ValueError, se.get_line_col, ('1.0 lineend'))
+ self.assertRaises(ValueError, se.get_line_col, ('end'))
+
+class GetSelectionTest(unittest.TestCase):
+ # Test text-dependent helper function.
+## # Need gui for text.index('sel.first/sel.last/insert').
+## @classmethod
+## def setUpClass(cls):
+## requires('gui')
+## cls.root = Tk()
+##
+## @classmethod
+## def tearDownClass(cls):
+## cls.root.destroy()
+## del cls.root
+
+ def test_get_selection(self):
+ # text = Text(master=self.root)
+ text = mockText()
+ text.insert('1.0', 'Hello World!')
+
+ # fix text.index result when called in get_selection
+ def sel(s):
+ # select entire text, cursor irrelevant
+ if s == 'sel.first': return '1.0'
+ if s == 'sel.last': return '1.12'
+ raise TclError
+ text.index = sel # replaces .tag_add('sel', '1.0, '1.12')
+ self.assertEqual(se.get_selection(text), ('1.0', '1.12'))
+
+ def mark(s):
+ # no selection, cursor after 'Hello'
+ if s == 'insert': return '1.5'
+ raise TclError
+ text.index = mark # replaces .mark_set('insert', '1.5')
+ self.assertEqual(se.get_selection(text), ('1.5', '1.5'))
+
+
+class ReverseSearchTest(unittest.TestCase):
+ # Test helper function that searches backwards within a line.
+ def test_search_reverse(self):
+ Equal = self.assertEqual
+ line = "Here is an 'is' test text."
+ prog = re.compile('is')
+ Equal(se.search_reverse(prog, line, len(line)).span(), (12, 14))
+ Equal(se.search_reverse(prog, line, 14).span(), (12, 14))
+ Equal(se.search_reverse(prog, line, 13).span(), (5, 7))
+ Equal(se.search_reverse(prog, line, 7).span(), (5, 7))
+ Equal(se.search_reverse(prog, line, 6), None)
+
+
+class SearchEngineTest(unittest.TestCase):
+ # Test class methods that do not use Text widget.
+
+ def setUp(self):
+ self.engine = se.SearchEngine(root=None)
+ # Engine.root is only used to create error message boxes.
+ # The mock replacement ignores the root argument.
+
+ def test_is_get(self):
+ engine = self.engine
+ Equal = self.assertEqual
+
+ Equal(engine.getpat(), '')
+ engine.setpat('hello')
+ Equal(engine.getpat(), 'hello')
+
+ Equal(engine.isre(), False)
+ engine.revar.set(1)
+ Equal(engine.isre(), True)
+
+ Equal(engine.iscase(), False)
+ engine.casevar.set(1)
+ Equal(engine.iscase(), True)
+
+ Equal(engine.isword(), False)
+ engine.wordvar.set(1)
+ Equal(engine.isword(), True)
+
+ Equal(engine.iswrap(), True)
+ engine.wrapvar.set(0)
+ Equal(engine.iswrap(), False)
+
+ Equal(engine.isback(), False)
+ engine.backvar.set(1)
+ Equal(engine.isback(), True)
+
+ def test_setcookedpat(self):
+ engine = self.engine
+ engine.setcookedpat('\s')
+ self.assertEqual(engine.getpat(), '\s')
+ engine.revar.set(1)
+ engine.setcookedpat('\s')
+ self.assertEqual(engine.getpat(), r'\\s')
+
+ def test_getcookedpat(self):
+ engine = self.engine
+ Equal = self.assertEqual
+
+ Equal(engine.getcookedpat(), '')
+ engine.setpat('hello')
+ Equal(engine.getcookedpat(), 'hello')
+ engine.wordvar.set(True)
+ Equal(engine.getcookedpat(), r'\bhello\b')
+ engine.wordvar.set(False)
+
+ engine.setpat('\s')
+ Equal(engine.getcookedpat(), r'\\s')
+ engine.revar.set(True)
+ Equal(engine.getcookedpat(), '\s')
+
+ def test_getprog(self):
+ engine = self.engine
+ Equal = self.assertEqual
+
+ engine.setpat('Hello')
+ temppat = engine.getprog()
+ Equal(temppat.pattern, re.compile('Hello', re.IGNORECASE).pattern)
+ engine.casevar.set(1)
+ temppat = engine.getprog()
+ Equal(temppat.pattern, re.compile('Hello').pattern, 0)
+
+ engine.setpat('')
+ Equal(engine.getprog(), None)
+ engine.setpat('+')
+ engine.revar.set(1)
+ Equal(engine.getprog(), None)
+ self.assertEqual(Mbox.showerror.message,
+ 'Error: nothing to repeat\nPattern: +')
+
+ def test_report_error(self):
+ showerror = Mbox.showerror
+ Equal = self.assertEqual
+ pat = '[a-z'
+ msg = 'unexpected end of regular expression'
+
+ Equal(self.engine.report_error(pat, msg), None)
+ Equal(showerror.title, 'Regular expression error')
+ expected_message = ("Error: " + msg + "\nPattern: [a-z")
+ Equal(showerror.message, expected_message)
+
+ Equal(self.engine.report_error(pat, msg, 5), None)
+ Equal(showerror.title, 'Regular expression error')
+ expected_message += "\nOffset: 5"
+ Equal(showerror.message, expected_message)
+
+
+class SearchTest(unittest.TestCase):
+ # Test that search_text makes right call to right method.
+
+ @classmethod
+ def setUpClass(cls):
+## requires('gui')
+## cls.root = Tk()
+## cls.text = Text(master=cls.root)
+ cls.text = mockText()
+ test_text = (
+ 'First line\n'
+ 'Line with target\n'
+ 'Last line\n')
+ cls.text.insert('1.0', test_text)
+ cls.pat = re.compile('target')
+
+ cls.engine = se.SearchEngine(None)
+ cls.engine.search_forward = lambda *args: ('f', args)
+ cls.engine.search_backward = lambda *args: ('b', args)
+
+## @classmethod
+## def tearDownClass(cls):
+## cls.root.destroy()
+## del cls.root
+
+ def test_search(self):
+ Equal = self.assertEqual
+ engine = self.engine
+ search = engine.search_text
+ text = self.text
+ pat = self.pat
+
+ engine.patvar.set(None)
+ #engine.revar.set(pat)
+ Equal(search(text), None)
+
+ def mark(s):
+ # no selection, cursor after 'Hello'
+ if s == 'insert': return '1.5'
+ raise TclError
+ text.index = mark
+ Equal(search(text, pat), ('f', (text, pat, 1, 5, True, False)))
+ engine.wrapvar.set(False)
+ Equal(search(text, pat), ('f', (text, pat, 1, 5, False, False)))
+ engine.wrapvar.set(True)
+ engine.backvar.set(True)
+ Equal(search(text, pat), ('b', (text, pat, 1, 5, True, False)))
+ engine.backvar.set(False)
+
+ def sel(s):
+ if s == 'sel.first': return '2.10'
+ if s == 'sel.last': return '2.16'
+ raise TclError
+ text.index = sel
+ Equal(search(text, pat), ('f', (text, pat, 2, 16, True, False)))
+ Equal(search(text, pat, True), ('f', (text, pat, 2, 10, True, True)))
+ engine.backvar.set(True)
+ Equal(search(text, pat), ('b', (text, pat, 2, 10, True, False)))
+ Equal(search(text, pat, True), ('b', (text, pat, 2, 16, True, True)))
+
+
+class ForwardBackwardTest(unittest.TestCase):
+ # Test that search_forward method finds the target.
+## @classmethod
+## def tearDownClass(cls):
+## cls.root.destroy()
+## del cls.root
+
+ @classmethod
+ def setUpClass(cls):
+ cls.engine = se.SearchEngine(None)
+## requires('gui')
+## cls.root = Tk()
+## cls.text = Text(master=cls.root)
+ cls.text = mockText()
+ # search_backward calls index('end-1c')
+ cls.text.index = lambda index: '4.0'
+ test_text = (
+ 'First line\n'
+ 'Line with target\n'
+ 'Last line\n')
+ cls.text.insert('1.0', test_text)
+ cls.pat = re.compile('target')
+ cls.res = (2, (10, 16)) # line, slice indexes of 'target'
+ cls.failpat = re.compile('xyz') # not in text
+ cls.emptypat = re.compile('\w*') # empty match possible
+
+ def make_search(self, func):
+ def search(pat, line, col, wrap, ok=0):
+ res = func(self.text, pat, line, col, wrap, ok)
+ # res is (line, matchobject) or None
+ return (res[0], res[1].span()) if res else res
+ return search
+
+ def test_search_forward(self):
+ # search for non-empty match
+ Equal = self.assertEqual
+ forward = self.make_search(self.engine.search_forward)
+ pat = self.pat
+ Equal(forward(pat, 1, 0, True), self.res)
+ Equal(forward(pat, 3, 0, True), self.res) # wrap
+ Equal(forward(pat, 3, 0, False), None) # no wrap
+ Equal(forward(pat, 2, 10, False), self.res)
+
+ Equal(forward(self.failpat, 1, 0, True), None)
+ Equal(forward(self.emptypat, 2, 9, True, ok=True), (2, (9, 9)))
+ #Equal(forward(self.emptypat, 2, 9, True), self.res)
+ # While the initial empty match is correctly ignored, skipping
+ # the rest of the line and returning (3, (0,4)) seems buggy - tjr.
+ Equal(forward(self.emptypat, 2, 10, True), self.res)
+
+ def test_search_backward(self):
+ # search for non-empty match
+ Equal = self.assertEqual
+ backward = self.make_search(self.engine.search_backward)
+ pat = self.pat
+ Equal(backward(pat, 3, 5, True), self.res)
+ Equal(backward(pat, 2, 0, True), self.res) # wrap
+ Equal(backward(pat, 2, 0, False), None) # no wrap
+ Equal(backward(pat, 2, 16, False), self.res)
+
+ Equal(backward(self.failpat, 3, 9, True), None)
+ Equal(backward(self.emptypat, 2, 10, True, ok=True), (2, (9,9)))
+ # Accepted because 9 < 10, not because ok=True.
+ # It is not clear that ok=True is useful going back - tjr
+ Equal(backward(self.emptypat, 2, 9, True), (2, (5, 9)))
+
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2, exit=2)
diff --git a/Lib/idlelib/idle_test/test_text.py b/Lib/idlelib/idle_test/test_text.py
new file mode 100644
index 0000000..5ac2fd7
--- /dev/null
+++ b/Lib/idlelib/idle_test/test_text.py
@@ -0,0 +1,228 @@
+# Test mock_tk.Text class against tkinter.Text class by running same tests with both.
+import unittest
+from test.support import requires
+
+from _tkinter import TclError
+import tkinter as tk
+
+class TextTest(object):
+
+ hw = 'hello\nworld' # usual initial insert after initialization
+ hwn = hw+'\n' # \n present at initialization, before insert
+
+ Text = None
+ def setUp(self):
+ self.text = self.Text()
+
+ def test_init(self):
+ self.assertEqual(self.text.get('1.0'), '\n')
+ self.assertEqual(self.text.get('end'), '')
+
+ def test_index_empty(self):
+ index = self.text.index
+
+ for dex in (-1.0, 0.3, '1.-1', '1.0', '1.0 lineend', '1.end', '1.33',
+ 'insert'):
+ self.assertEqual(index(dex), '1.0')
+
+ for dex in 'end', 2.0, '2.1', '33.44':
+ self.assertEqual(index(dex), '2.0')
+
+ def test_index_data(self):
+ index = self.text.index
+ self.text.insert('1.0', self.hw)
+
+ for dex in -1.0, 0.3, '1.-1', '1.0':
+ self.assertEqual(index(dex), '1.0')
+
+ for dex in '1.0 lineend', '1.end', '1.33':
+ self.assertEqual(index(dex), '1.5')
+
+ for dex in 'end', '33.44':
+ self.assertEqual(index(dex), '3.0')
+
+ def test_get(self):
+ get = self.text.get
+ Equal = self.assertEqual
+ self.text.insert('1.0', self.hw)
+
+ Equal(get('end'), '')
+ Equal(get('end', 'end'), '')
+ Equal(get('1.0'), 'h')
+ Equal(get('1.0', '1.1'), 'h')
+ Equal(get('1.0', '1.3'), 'hel')
+ Equal(get('1.1', '1.3'), 'el')
+ Equal(get('1.0', '1.0 lineend'), 'hello')
+ Equal(get('1.0', '1.10'), 'hello')
+ Equal(get('1.0 lineend'), '\n')
+ Equal(get('1.1', '2.3'), 'ello\nwor')
+ Equal(get('1.0', '2.5'), self.hw)
+ Equal(get('1.0', 'end'), self.hwn)
+ Equal(get('0.0', '5.0'), self.hwn)
+
+ def test_insert(self):
+ insert = self.text.insert
+ get = self.text.get
+ Equal = self.assertEqual
+
+ insert('1.0', self.hw)
+ Equal(get('1.0', 'end'), self.hwn)
+
+ insert('1.0', '') # nothing
+ Equal(get('1.0', 'end'), self.hwn)
+
+ insert('1.0', '*')
+ Equal(get('1.0', 'end'), '*hello\nworld\n')
+
+ insert('1.0 lineend', '*')
+ Equal(get('1.0', 'end'), '*hello*\nworld\n')
+
+ insert('2.3', '*')
+ Equal(get('1.0', 'end'), '*hello*\nwor*ld\n')
+
+ insert('end', 'x')
+ Equal(get('1.0', 'end'), '*hello*\nwor*ldx\n')
+
+ insert('1.4', 'x\n')
+ Equal(get('1.0', 'end'), '*helx\nlo*\nwor*ldx\n')
+
+ def test_no_delete(self):
+ # if index1 == 'insert' or 'end' or >= end, there is no deletion
+ delete = self.text.delete
+ get = self.text.get
+ Equal = self.assertEqual
+ self.text.insert('1.0', self.hw)
+
+ delete('insert')
+ Equal(get('1.0', 'end'), self.hwn)
+
+ delete('end')
+ Equal(get('1.0', 'end'), self.hwn)
+
+ delete('insert', 'end')
+ Equal(get('1.0', 'end'), self.hwn)
+
+ delete('insert', '5.5')
+ Equal(get('1.0', 'end'), self.hwn)
+
+ delete('1.4', '1.0')
+ Equal(get('1.0', 'end'), self.hwn)
+
+ delete('1.4', '1.4')
+ Equal(get('1.0', 'end'), self.hwn)
+
+ def test_delete_char(self):
+ delete = self.text.delete
+ get = self.text.get
+ Equal = self.assertEqual
+ self.text.insert('1.0', self.hw)
+
+ delete('1.0')
+ Equal(get('1.0', '1.end'), 'ello')
+
+ delete('1.0', '1.1')
+ Equal(get('1.0', '1.end'), 'llo')
+
+ # delete \n and combine 2 lines into 1
+ delete('1.end')
+ Equal(get('1.0', '1.end'), 'lloworld')
+
+ self.text.insert('1.3', '\n')
+ delete('1.10')
+ Equal(get('1.0', '1.end'), 'lloworld')
+
+ self.text.insert('1.3', '\n')
+ delete('1.3', '2.0')
+ Equal(get('1.0', '1.end'), 'lloworld')
+
+ def test_delete_slice(self):
+ delete = self.text.delete
+ get = self.text.get
+ Equal = self.assertEqual
+ self.text.insert('1.0', self.hw)
+
+ delete('1.0', '1.0 lineend')
+ Equal(get('1.0', 'end'), '\nworld\n')
+
+ delete('1.0', 'end')
+ Equal(get('1.0', 'end'), '\n')
+
+ self.text.insert('1.0', self.hw)
+ delete('1.0', '2.0')
+ Equal(get('1.0', 'end'), 'world\n')
+
+ delete('1.0', 'end')
+ Equal(get('1.0', 'end'), '\n')
+
+ self.text.insert('1.0', self.hw)
+ delete('1.2', '2.3')
+ Equal(get('1.0', 'end'), 'held\n')
+
+ def test_multiple_lines(self): # insert and delete
+ self.text.insert('1.0', 'hello')
+
+ self.text.insert('1.3', '1\n2\n3\n4\n5')
+ self.assertEqual(self.text.get('1.0', 'end'), 'hel1\n2\n3\n4\n5lo\n')
+
+ self.text.delete('1.3', '5.1')
+ self.assertEqual(self.text.get('1.0', 'end'), 'hello\n')
+
+ def test_compare(self):
+ compare = self.text.compare
+ Equal = self.assertEqual
+ # need data so indexes not squished to 1,0
+ self.text.insert('1.0', 'First\nSecond\nThird\n')
+
+ self.assertRaises(TclError, compare, '2.2', 'op', '2.2')
+
+ for op, less1, less0, equal, greater0, greater1 in (
+ ('<', True, True, False, False, False),
+ ('<=', True, True, True, False, False),
+ ('>', False, False, False, True, True),
+ ('>=', False, False, True, True, True),
+ ('==', False, False, True, False, False),
+ ('!=', True, True, False, True, True),
+ ):
+ Equal(compare('1.1', op, '2.2'), less1, op)
+ Equal(compare('2.1', op, '2.2'), less0, op)
+ Equal(compare('2.2', op, '2.2'), equal, op)
+ Equal(compare('2.3', op, '2.2'), greater0, op)
+ Equal(compare('3.3', op, '2.2'), greater1, op)
+
+
+class MockTextTest(TextTest, unittest.TestCase):
+
+ @classmethod
+ def setUpClass(cls):
+ from idlelib.idle_test.mock_tk import Text
+ cls.Text = Text
+
+ def test_decode(self):
+ # test endflags (-1, 0) not tested by test_index (which uses +1)
+ decode = self.text._decode
+ Equal = self.assertEqual
+ self.text.insert('1.0', self.hw)
+
+ Equal(decode('end', -1), (2, 5))
+ Equal(decode('3.1', -1), (2, 5))
+ Equal(decode('end', 0), (2, 6))
+ Equal(decode('3.1', 0), (2, 6))
+
+
+class TkTextTest(TextTest, unittest.TestCase):
+
+ @classmethod
+ def setUpClass(cls):
+ requires('gui')
+ from tkinter import Tk, Text
+ cls.Text = Text
+ cls.root = Tk()
+
+ @classmethod
+ def tearDownClass(cls):
+ cls.root.destroy()
+ del cls.root
+
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2, exit=False)
diff --git a/Lib/idlelib/idle_test/test_warning.py b/Lib/idlelib/idle_test/test_warning.py
new file mode 100644
index 0000000..18627dd
--- /dev/null
+++ b/Lib/idlelib/idle_test/test_warning.py
@@ -0,0 +1,73 @@
+'''Test warnings replacement in PyShell.py and run.py.
+
+This file could be expanded to include traceback overrides
+(in same two modules). If so, change name.
+Revise if output destination changes (http://bugs.python.org/issue18318).
+Make sure warnings module is left unaltered (http://bugs.python.org/issue18081).
+'''
+
+import unittest
+from test.support import captured_stderr
+
+import warnings
+# Try to capture default showwarning before Idle modules are imported.
+showwarning = warnings.showwarning
+# But if we run this file within idle, we are in the middle of the run.main loop
+# and default showwarnings has already been replaced.
+running_in_idle = 'idle' in showwarning.__name__
+
+from idlelib import run
+from idlelib import PyShell as shell
+
+# The following was generated from PyShell.idle_formatwarning
+# and checked as matching expectation.
+idlemsg = '''
+Warning (from warnings module):
+ File "test_warning.py", line 99
+ Line of code
+UserWarning: Test
+'''
+shellmsg = idlemsg + ">>> "
+
+class RunWarnTest(unittest.TestCase):
+
+ @unittest.skipIf(running_in_idle, "Does not work when run within Idle.")
+ def test_showwarnings(self):
+ self.assertIs(warnings.showwarning, showwarning)
+ run.capture_warnings(True)
+ self.assertIs(warnings.showwarning, run.idle_showwarning_subproc)
+ run.capture_warnings(False)
+ self.assertIs(warnings.showwarning, showwarning)
+
+ def test_run_show(self):
+ with captured_stderr() as f:
+ run.idle_showwarning_subproc(
+ 'Test', UserWarning, 'test_warning.py', 99, f, 'Line of code')
+ # The following uses .splitlines to erase line-ending differences
+ self.assertEqual(idlemsg.splitlines(), f.getvalue().splitlines())
+
+class ShellWarnTest(unittest.TestCase):
+
+ @unittest.skipIf(running_in_idle, "Does not work when run within Idle.")
+ def test_showwarnings(self):
+ self.assertIs(warnings.showwarning, showwarning)
+ shell.capture_warnings(True)
+ self.assertIs(warnings.showwarning, shell.idle_showwarning)
+ shell.capture_warnings(False)
+ self.assertIs(warnings.showwarning, showwarning)
+
+ def test_idle_formatter(self):
+ # Will fail if format changed without regenerating idlemsg
+ s = shell.idle_formatwarning(
+ 'Test', UserWarning, 'test_warning.py', 99, 'Line of code')
+ self.assertEqual(idlemsg, s)
+
+ def test_shell_show(self):
+ with captured_stderr() as f:
+ shell.idle_showwarning(
+ 'Test', UserWarning, 'test_warning.py', 99, f, 'Line of code')
+ self.assertEqual(shellmsg.splitlines(), f.getvalue().splitlines())
+
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2, exit=False)
diff --git a/Lib/idlelib/idlever.py b/Lib/idlelib/idlever.py
index 643ea9f..9ad7d89 100644
--- a/Lib/idlelib/idlever.py
+++ b/Lib/idlelib/idlever.py
@@ -1 +1 @@
-IDLE_VERSION = "3.2.6"
+IDLE_VERSION = "3.3.6"
diff --git a/Lib/idlelib/macosxSupport.py b/Lib/idlelib/macosxSupport.py
index 9690442..67069fa 100644
--- a/Lib/idlelib/macosxSupport.py
+++ b/Lib/idlelib/macosxSupport.py
@@ -12,12 +12,22 @@ _appbundle = None
def runningAsOSXApp():
"""
Returns True if Python is running from within an app on OSX.
- If so, assume that Python was built with Aqua Tcl/Tk rather than
- X11 Tcl/Tk.
+ If so, the various OS X customizations will be triggered later (menu
+ fixup, et al). (Originally, this test was supposed to condition
+ behavior on whether IDLE was running under Aqua Tk rather than
+ under X11 Tk but that does not work since a framework build
+ could be linked with X11. For several releases, this test actually
+ differentiates between whether IDLE is running from a framework or
+ not. As a future enhancement, it should be considered whether there
+ should be a difference based on framework and any needed X11 adaptions
+ should be made dependent on a new function that actually tests for X11.)
"""
global _appbundle
if _appbundle is None:
- _appbundle = (sys.platform == 'darwin' and '.app' in sys.executable)
+ _appbundle = sys.platform == 'darwin'
+ if _appbundle:
+ import sysconfig
+ _appbundle = bool(sysconfig.get_config_var('PYTHONFRAMEWORK'))
return _appbundle
_carbonaquatk = None
diff --git a/Lib/idlelib/rpc.py b/Lib/idlelib/rpc.py
index 29e687e..ddce6e9 100644
--- a/Lib/idlelib/rpc.py
+++ b/Lib/idlelib/rpc.py
@@ -40,6 +40,7 @@ import traceback
import copyreg
import types
import marshal
+import builtins
def unpickle_code(ms):
@@ -144,7 +145,7 @@ class SocketIO(object):
def exithook(self):
"override for specific exit action"
- os._exit()
+ os._exit(0)
def debug(self, *args):
if not self.debugging:
@@ -196,8 +197,12 @@ class SocketIO(object):
return ("ERROR", "Unsupported message type: %s" % how)
except SystemExit:
raise
+ except KeyboardInterrupt:
+ raise
except socket.error:
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"
@@ -257,6 +262,9 @@ class SocketIO(object):
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):
@@ -331,7 +339,7 @@ class SocketIO(object):
r, w, x = select.select([], [self.sock], [])
n = self.sock.send(s[:BUFSIZE])
except (AttributeError, TypeError):
- raise IOError("socket no longer exists")
+ raise OSError("socket no longer exists")
except socket.error:
raise
else:
@@ -596,3 +604,21 @@ class MethodProxy(object):
# 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
diff --git a/Lib/idlelib/run.py b/Lib/idlelib/run.py
index 7d0941e..c1859b6 100644
--- a/Lib/idlelib/run.py
+++ b/Lib/idlelib/run.py
@@ -7,6 +7,7 @@ import traceback
import _thread as thread
import threading
import queue
+import tkinter
from idlelib import CallTips
from idlelib import AutoComplete
@@ -22,24 +23,45 @@ import __main__
LOCALHOST = '127.0.0.1'
-try:
- import warnings
-except ImportError:
- pass
-else:
- def idle_formatwarning_subproc(message, category, filename, lineno,
- line=None):
- """Format warnings the IDLE way"""
- s = "\nWarning (from warnings module):\n"
- s += ' File \"%s\", line %s\n' % (filename, lineno)
- if line is None:
- line = linecache.getline(filename, lineno)
- line = line.strip()
- if line:
- s += " %s\n" % line
- s += "%s: %s\n" % (category.__name__, message)
- return s
- warnings.formatwarning = idle_formatwarning_subproc
+import warnings
+
+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(PyShell.idle_formatwarning(
+ message, category, filename, lineno, line))
+ except IOError:
+ 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
@@ -79,6 +101,8 @@ def main(del_exitfunc=False):
print("IDLE Subprocess: no IP port passed in sys.argv.",
file=sys.__stderr__)
return
+
+ capture_warnings(True)
sys.argv[:] = [""]
sockthread = threading.Thread(target=manage_socket,
name='SockThread',
@@ -96,6 +120,7 @@ def main(del_exitfunc=False):
try:
seq, request = rpc.request_queue.get(block=True, timeout=0.05)
except queue.Empty:
+ handle_tk_events()
continue
method, args, kwargs = request
ret = method(*args, **kwargs)
@@ -105,6 +130,7 @@ def main(del_exitfunc=False):
exit_now = True
continue
except SystemExit:
+ capture_warnings(False)
raise
except:
type, value, tb = sys.exc_info()
@@ -170,7 +196,9 @@ def print_exception():
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 context not in seen:
+ elif (context is not None and
+ not exc.__suppress_context__ and
+ context not in seen):
print_exc(type(context), context, context.__traceback__)
print("\nDuring handling of the above exception, "
"another exception occurred:\n", file=efile)
@@ -232,6 +260,7 @@ def exit():
if no_exitfunc:
import atexit
atexit._clear()
+ capture_warnings(False)
sys.exit(0)
class MyRPCServer(rpc.RPCServer):
@@ -278,9 +307,15 @@ class MyHandler(rpc.RPCHandler):
sys.stderr = PyShell.PseudoOutputFile(self.console, "stderr",
IOBinding.encoding)
+ 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
+
self.interp = self.get_remote_proxy("interp")
rpc.RPCHandler.getresponse(self, myseq=None, wait=0.05)
@@ -318,6 +353,10 @@ class Executive(object):
exec(code, self.locals)
finally:
interruptable = False
+ except SystemExit:
+ # Scripts that raise SystemExit should just
+ # return to the interactive prompt
+ pass
except:
self.usr_exc_info = sys.exc_info()
if quitting:
@@ -361,3 +400,5 @@ class Executive(object):
sys.last_value = val
item = StackViewer.StackTreeItem(flist, tb)
return RemoteObjectBrowser.remote_object_tree_item(item)
+
+capture_warnings(False) # Make sure turned off; see issue 18081
diff --git a/Lib/idlelib/textView.py b/Lib/idlelib/textView.py
index 1eaa464..dd50544 100644
--- a/Lib/idlelib/textView.py
+++ b/Lib/idlelib/textView.py
@@ -66,7 +66,7 @@ def view_file(parent, title, filename, encoding=None, modal=True):
try:
with open(filename, 'r', encoding=encoding) as file:
contents = file.read()
- except IOError:
+ except OSError:
import tkinter.messagebox as tkMessageBox
tkMessageBox.showerror(title='File Load Error',
message='Unable to load file %r .' % filename,
@@ -80,7 +80,8 @@ if __name__ == '__main__':
root=Tk()
root.title('textView test')
filename = './textView.py'
- text = file(filename, 'r').read()
+ with open(filename, 'r') as f:
+ text = f.read()
btn1 = Button(root, text='view_text',
command=lambda:view_text(root, 'view_text', text))
btn1.pack(side=LEFT)