summaryrefslogtreecommitdiff
path: root/Lib/idlelib
diff options
context:
space:
mode:
authorGiampaolo Rodola <g.rodola@gmail.com>2019-03-28 15:23:28 +0100
committerGiampaolo Rodola <g.rodola@gmail.com>2019-03-28 15:23:28 +0100
commite19b28f2bd89c047b12f6a8ffb1fe793834700d3 (patch)
tree8261b98b29eedb8ce67df4d571e8ba9b948d17ab /Lib/idlelib
parentf7868847da9f84cb68605b4b94d8fcc205e0766e (diff)
parent3eca28c61363a03b81b9fb12775490d6e42d8ecf (diff)
downloadcpython-git-e19b28f2bd89c047b12f6a8ffb1fe793834700d3.tar.gz
Merge branch 'master' into bind-socket
Diffstat (limited to 'Lib/idlelib')
-rw-r--r--Lib/idlelib/NEWS.txt26
-rw-r--r--Lib/idlelib/autocomplete.py29
-rw-r--r--Lib/idlelib/autocomplete_w.py2
-rw-r--r--Lib/idlelib/calltip.py7
-rw-r--r--Lib/idlelib/calltip_w.py3
-rw-r--r--Lib/idlelib/colorizer.py2
-rw-r--r--Lib/idlelib/config.py39
-rw-r--r--Lib/idlelib/configdialog.py2
-rw-r--r--Lib/idlelib/grep.py94
-rw-r--r--Lib/idlelib/idle_test/test_autocomplete.py147
-rw-r--r--Lib/idlelib/idle_test/test_config.py4
-rw-r--r--Lib/idlelib/idle_test/test_configdialog.py45
-rw-r--r--Lib/idlelib/idle_test/test_grep.py92
-rwxr-xr-xLib/idlelib/pyshell.py8
-rw-r--r--Lib/idlelib/replace.py111
-rw-r--r--Lib/idlelib/rpc.py3
-rw-r--r--Lib/idlelib/search.py70
17 files changed, 500 insertions, 184 deletions
diff --git a/Lib/idlelib/NEWS.txt b/Lib/idlelib/NEWS.txt
index 715b013e11..be855bc467 100644
--- a/Lib/idlelib/NEWS.txt
+++ b/Lib/idlelib/NEWS.txt
@@ -3,6 +3,32 @@ Released on 2019-10-20?
======================================
+bpo-36429: Fix starting IDLE with pyshell.
+Add idlelib.pyshell alias at top; remove pyshell alias at bottom.
+Remove obsolete __name__=='__main__' command.
+
+bpo-30348: Increase test coverage of idlelib.autocomplete by 30%.
+Patch by Louie Lu.
+
+bpo-23205: Add tests and refactor grep's findfiles.
+
+bpo-36405: Use dict unpacking in idlelib.
+
+bpo-36396: Remove fgBg param of idlelib.config.GetHighlight().
+This param was only used twice and changed the return type.
+
+bpo-23216: IDLE: Add docstrings to search modules.
+
+bpo-36176: Fix IDLE autocomplete & calltip popup colors.
+Prevent conflicts with Linux dark themes
+(and slightly darken calltip background).
+
+bpo-36152: Remove colorizer.ColorDelegator.close_when_done and the
+corresponding argument of .close(). In IDLE, both have always been
+None or False since 2007.
+
+bpo-36096: Make colorizer state variables instance-only.
+
bpo-24310: Document settings dialog font tab sample.
bpo-35689: Add docstrings and tests for colorizer.
diff --git a/Lib/idlelib/autocomplete.py b/Lib/idlelib/autocomplete.py
index 9caf50d5d0..e20b757d87 100644
--- a/Lib/idlelib/autocomplete.py
+++ b/Lib/idlelib/autocomplete.py
@@ -3,6 +3,7 @@
Either on demand or after a user-selected delay after a key character,
pop up a list of candidates.
"""
+import __main__
import os
import string
import sys
@@ -14,7 +15,6 @@ COMPLETE_ATTRIBUTES, COMPLETE_FILES = range(1, 2+1)
from idlelib import autocomplete_w
from idlelib.config import idleConf
from idlelib.hyperparser import HyperParser
-import __main__
# This string includes all chars that may be in an identifier.
# TODO Update this here and elsewhere.
@@ -104,9 +104,14 @@ class AutoComplete:
def open_completions(self, evalfuncs, complete, userWantsWin, mode=None):
"""Find the completions and create the AutoCompleteWindow.
Return True if successful (no syntax error or so found).
- if complete is True, then if there's nothing to complete and no
+ If complete is True, then if there's nothing to complete and no
start of completion, won't open completions and return False.
If mode is given, will open a completion list only in this mode.
+
+ Action Function Eval Complete WantWin Mode
+ ^space force_open_completions True, False, True no
+ . or / try_open_completions False, False, False yes
+ tab autocomplete False, True, True no
"""
# Cancel another delayed call, if it exists.
if self._delayed_completion_id is not None:
@@ -117,11 +122,11 @@ class AutoComplete:
curline = self.text.get("insert linestart", "insert")
i = j = len(curline)
if hp.is_in_string() and (not mode or mode==COMPLETE_FILES):
- # Find the beginning of the string
- # fetch_completions will look at the file system to determine whether the
- # string value constitutes an actual file name
- # XXX could consider raw strings here and unescape the string value if it's
- # not raw.
+ # Find the beginning of the string.
+ # fetch_completions will look at the file system to determine
+ # whether the string value constitutes an actual file name
+ # XXX could consider raw strings here and unescape the string
+ # value if it's not raw.
self._remove_autocomplete_window()
mode = COMPLETE_FILES
# Find last separator or string start
@@ -182,8 +187,8 @@ class AutoComplete:
else:
if mode == COMPLETE_ATTRIBUTES:
if what == "":
- namespace = __main__.__dict__.copy()
- namespace.update(__main__.__builtins__.__dict__)
+ namespace = {**__main__.__builtins__.__dict__,
+ **__main__.__dict__}
bigl = eval("dir()", namespace)
bigl.sort()
if "__all__" in bigl:
@@ -218,10 +223,8 @@ class AutoComplete:
return smalll, bigl
def get_entity(self, name):
- """Lookup name in a namespace spanning sys.modules and __main.dict__"""
- namespace = sys.modules.copy()
- namespace.update(__main__.__dict__)
- return eval(name, namespace)
+ "Lookup name in a namespace spanning sys.modules and __main.dict__."
+ return eval(name, {**sys.modules, **__main__.__dict__})
AutoComplete.reload()
diff --git a/Lib/idlelib/autocomplete_w.py b/Lib/idlelib/autocomplete_w.py
index 7994bc0db1..c249625277 100644
--- a/Lib/idlelib/autocomplete_w.py
+++ b/Lib/idlelib/autocomplete_w.py
@@ -189,7 +189,7 @@ class AutoCompleteWindow:
pass
self.scrollbar = scrollbar = Scrollbar(acw, orient=VERTICAL)
self.listbox = listbox = Listbox(acw, yscrollcommand=scrollbar.set,
- exportselection=False, bg="white")
+ exportselection=False)
for item in self.completions:
listbox.insert(END, item)
self.origselforeground = listbox.cget("selectforeground")
diff --git a/Lib/idlelib/calltip.py b/Lib/idlelib/calltip.py
index 2a9a131ed9..b013a7f6ec 100644
--- a/Lib/idlelib/calltip.py
+++ b/Lib/idlelib/calltip.py
@@ -4,6 +4,7 @@ Call Tips are floating windows which display function, class, and method
parameter and docstring information when you type an opening parenthesis, and
which disappear when you type a closing parenthesis.
"""
+import __main__
import inspect
import re
import sys
@@ -12,7 +13,6 @@ import types
from idlelib import calltip_w
from idlelib.hyperparser import HyperParser
-import __main__
class Calltip:
@@ -103,10 +103,9 @@ def get_entity(expression):
in a namespace spanning sys.modules and __main.dict__.
"""
if expression:
- namespace = sys.modules.copy()
- namespace.update(__main__.__dict__)
+ namespace = {**sys.modules, **__main__.__dict__}
try:
- return eval(expression, namespace)
+ return eval(expression, namespace) # Only protect user code.
except BaseException:
# An uncaught exception closes idle, and eval can raise any
# exception, especially if user classes are involved.
diff --git a/Lib/idlelib/calltip_w.py b/Lib/idlelib/calltip_w.py
index 7553dfefc5..1e0404aa49 100644
--- a/Lib/idlelib/calltip_w.py
+++ b/Lib/idlelib/calltip_w.py
@@ -80,7 +80,8 @@ class CalltipWindow(TooltipBase):
def showcontents(self):
"""Create the call-tip widget."""
self.label = Label(self.tipwindow, text=self.text, justify=LEFT,
- background="#ffffe0", relief=SOLID, borderwidth=1,
+ background="#ffffd0", foreground="black",
+ relief=SOLID, borderwidth=1,
font=self.anchor_widget['font'])
self.label.pack()
diff --git a/Lib/idlelib/colorizer.py b/Lib/idlelib/colorizer.py
index facbef8bb1..db1266fed3 100644
--- a/Lib/idlelib/colorizer.py
+++ b/Lib/idlelib/colorizer.py
@@ -40,7 +40,7 @@ def color_config(text):
# Not automatic because ColorDelegator does not know 'text'.
theme = idleConf.CurrentTheme()
normal_colors = idleConf.GetHighlight(theme, 'normal')
- cursor_color = idleConf.GetHighlight(theme, 'cursor', fgBg='fg')
+ cursor_color = idleConf.GetHighlight(theme, 'cursor')['foreground']
select_colors = idleConf.GetHighlight(theme, 'hilite')
text.config(
foreground=normal_colors['foreground'],
diff --git a/Lib/idlelib/config.py b/Lib/idlelib/config.py
index 79d988f9a1..aa94d6535b 100644
--- a/Lib/idlelib/config.py
+++ b/Lib/idlelib/config.py
@@ -34,7 +34,6 @@ import idlelib
class InvalidConfigType(Exception): pass
class InvalidConfigSet(Exception): pass
-class InvalidFgBg(Exception): pass
class InvalidTheme(Exception): pass
class IdleConfParser(ConfigParser):
@@ -283,34 +282,20 @@ class IdleConf:
raise InvalidConfigSet('Invalid configSet specified')
return cfgParser.sections()
- def GetHighlight(self, theme, element, fgBg=None):
- """Return individual theme element highlight color(s).
+ def GetHighlight(self, theme, element):
+ """Return dict of theme element highlight colors.
- fgBg - string ('fg' or 'bg') or None.
- If None, return a dictionary containing fg and bg colors with
- keys 'foreground' and 'background'. Otherwise, only return
- fg or bg color, as specified. Colors are intended to be
- appropriate for passing to Tkinter in, e.g., a tag_config call).
+ The keys are 'foreground' and 'background'. The values are
+ tkinter color strings for configuring backgrounds and tags.
"""
- if self.defaultCfg['highlight'].has_section(theme):
- themeDict = self.GetThemeDict('default', theme)
- else:
- themeDict = self.GetThemeDict('user', theme)
- fore = themeDict[element + '-foreground']
- if element == 'cursor': # There is no config value for cursor bg
- back = themeDict['normal-background']
- else:
- back = themeDict[element + '-background']
- highlight = {"foreground": fore, "background": back}
- if not fgBg: # Return dict of both colors
- return highlight
- else: # Return specified color only
- if fgBg == 'fg':
- return highlight["foreground"]
- if fgBg == 'bg':
- return highlight["background"]
- else:
- raise InvalidFgBg('Invalid fgBg specified')
+ cfg = ('default' if self.defaultCfg['highlight'].has_section(theme)
+ else 'user')
+ theme_dict = self.GetThemeDict(cfg, theme)
+ fore = theme_dict[element + '-foreground']
+ if element == 'cursor':
+ element = 'normal'
+ back = theme_dict[element + '-background']
+ return {"foreground": fore, "background": back}
def GetThemeDict(self, type, themeName):
"""Return {option:value} dict for elements in themeName.
diff --git a/Lib/idlelib/configdialog.py b/Lib/idlelib/configdialog.py
index 5fdaf82de4..31520a3b0d 100644
--- a/Lib/idlelib/configdialog.py
+++ b/Lib/idlelib/configdialog.py
@@ -1252,7 +1252,7 @@ class HighPage(Frame):
colors = idleConf.GetHighlight(theme, element)
if element == 'cursor': # Cursor sample needs special painting.
colors['background'] = idleConf.GetHighlight(
- theme, 'normal', fgBg='bg')
+ theme, 'normal')['background']
# Handle any unsaved changes to this theme.
if theme in changes['highlight']:
theme_dict = changes['highlight'][theme]
diff --git a/Lib/idlelib/grep.py b/Lib/idlelib/grep.py
index 873233ec15..12513594b7 100644
--- a/Lib/idlelib/grep.py
+++ b/Lib/idlelib/grep.py
@@ -14,11 +14,16 @@ from idlelib.searchbase import SearchDialogBase
from idlelib import searchengine
# Importing OutputWindow here fails due to import loop
-# EditorWindow -> GrepDialop -> OutputWindow -> EditorWindow
+# EditorWindow -> GrepDialog -> OutputWindow -> EditorWindow
def grep(text, io=None, flist=None):
- """Create or find singleton GrepDialog instance.
+ """Open the Find in Files dialog.
+
+ Module-level function to access the singleton GrepDialog
+ instance and open the dialog. If text is selected, it is
+ used as the search phrase; otherwise, the previous entry
+ is used.
Args:
text: Text widget that contains the selected text for
@@ -26,7 +31,6 @@ def grep(text, io=None, flist=None):
io: iomenu.IOBinding instance with default path to search.
flist: filelist.FileList instance for OutputWindow parent.
"""
-
root = text._root()
engine = searchengine.get(root)
if not hasattr(engine, "_grepdialog"):
@@ -36,6 +40,27 @@ def grep(text, io=None, flist=None):
dialog.open(text, searchphrase, io)
+def walk_error(msg):
+ "Handle os.walk error."
+ print(msg)
+
+
+def findfiles(folder, pattern, recursive):
+ """Generate file names in dir that match pattern.
+
+ Args:
+ folder: Root directory to search.
+ pattern: File pattern to match.
+ recursive: True to include subdirectories.
+ """
+ for dirpath, _, filenames in os.walk(folder, onerror=walk_error):
+ yield from (os.path.join(dirpath, name)
+ for name in filenames
+ if fnmatch.fnmatch(name, pattern))
+ if not recursive:
+ break
+
+
class GrepDialog(SearchDialogBase):
"Dialog for searching multiple files."
@@ -50,17 +75,29 @@ class GrepDialog(SearchDialogBase):
searchengine instance to prepare the search.
Attributes:
- globvar: Value of Text Entry widget for path to search.
- recvar: Boolean value of Checkbutton widget
- for traversing through subdirectories.
+ flist: filelist.Filelist instance for OutputWindow parent.
+ globvar: String value of Entry widget for path to search.
+ globent: Entry widget for globvar. Created in
+ create_entries().
+ recvar: Boolean value of Checkbutton widget for
+ traversing through subdirectories.
"""
- SearchDialogBase.__init__(self, root, engine)
+ super().__init__(root, engine)
self.flist = flist
self.globvar = StringVar(root)
self.recvar = BooleanVar(root)
def open(self, text, searchphrase, io=None):
- "Make dialog visible on top of others and ready to use."
+ """Make dialog visible on top of others and ready to use.
+
+ Extend the SearchDialogBase open() to set the initial value
+ for globvar.
+
+ Args:
+ text: Multicall object containing the text information.
+ searchphrase: String phrase to search.
+ io: iomenu.IOBinding instance containing file path.
+ """
SearchDialogBase.open(self, text, searchphrase)
if io:
path = io.filename or ""
@@ -85,9 +122,9 @@ class GrepDialog(SearchDialogBase):
btn.pack(side="top", fill="both")
def create_command_buttons(self):
- "Create base command buttons and add button for search."
+ "Create base command buttons and add button for Search Files."
SearchDialogBase.create_command_buttons(self)
- self.make_button("Search Files", self.default_command, 1)
+ self.make_button("Search Files", self.default_command, isdef=True)
def default_command(self, event=None):
"""Grep for search pattern in file path. The default command is bound
@@ -119,16 +156,21 @@ class GrepDialog(SearchDialogBase):
search each line for the matching pattern. If the pattern is
found, write the file and line information to stdout (which
is an OutputWindow).
+
+ Args:
+ prog: The compiled, cooked search pattern.
+ path: String containing the search path.
"""
- dir, base = os.path.split(path)
- list = self.findfiles(dir, base, self.recvar.get())
- list.sort()
+ folder, filepat = os.path.split(path)
+ if not folder:
+ folder = os.curdir
+ filelist = sorted(findfiles(folder, filepat, self.recvar.get()))
self.close()
pat = self.engine.getpat()
print(f"Searching {pat!r} in {path} ...")
hits = 0
try:
- for fn in list:
+ for fn in filelist:
try:
with open(fn, errors='replace') as f:
for lineno, line in enumerate(f, 1):
@@ -146,30 +188,6 @@ class GrepDialog(SearchDialogBase):
# so in OW.write, OW.text.insert fails.
pass
- def findfiles(self, dir, base, rec):
- """Return list of files in the dir that match the base pattern.
-
- If rec is True, recursively iterate through subdirectories.
- """
- try:
- names = os.listdir(dir or os.curdir)
- except OSError as msg:
- print(msg)
- return []
- list = []
- subdirs = []
- for name in names:
- fn = os.path.join(dir, name)
- if os.path.isdir(fn):
- subdirs.append(fn)
- else:
- if fnmatch.fnmatch(name, base):
- list.append(fn)
- if rec:
- for subdir in subdirs:
- list.extend(self.findfiles(subdir, base, rec))
- return list
-
def _grep_dialog(parent): # htest #
from tkinter import Toplevel, Text, SEL, END
diff --git a/Lib/idlelib/idle_test/test_autocomplete.py b/Lib/idlelib/idle_test/test_autocomplete.py
index d7ee00af94..398cb359e0 100644
--- a/Lib/idlelib/idle_test/test_autocomplete.py
+++ b/Lib/idlelib/idle_test/test_autocomplete.py
@@ -1,8 +1,11 @@
-"Test autocomplete, coverage 57%."
+"Test autocomplete, coverage 87%."
import unittest
+from unittest.mock import Mock, patch
from test.support import requires
from tkinter import Tk, Text
+import os
+import __main__
import idlelib.autocomplete as ac
import idlelib.autocomplete_w as acw
@@ -25,17 +28,19 @@ class AutoCompleteTest(unittest.TestCase):
def setUpClass(cls):
requires('gui')
cls.root = Tk()
+ cls.root.withdraw()
cls.text = Text(cls.root)
cls.editor = DummyEditwin(cls.root, cls.text)
@classmethod
def tearDownClass(cls):
del cls.editor, cls.text
+ cls.root.update_idletasks()
cls.root.destroy()
del cls.root
def setUp(self):
- self.editor.text.delete('1.0', 'end')
+ self.text.delete('1.0', 'end')
self.autocomplete = ac.AutoComplete(self.editor)
def test_init(self):
@@ -52,7 +57,7 @@ class AutoCompleteTest(unittest.TestCase):
self.assertIsNone(self.autocomplete.autocompletewindow)
def test_force_open_completions_event(self):
- # Test that force_open_completions_event calls _open_completions
+ # Test that force_open_completions_event calls _open_completions.
o_cs = Func()
self.autocomplete.open_completions = o_cs
self.autocomplete.force_open_completions_event('event')
@@ -65,16 +70,16 @@ class AutoCompleteTest(unittest.TestCase):
o_c_l = Func()
autocomplete._open_completions_later = o_c_l
- # _open_completions_later should not be called with no text in editor
+ # _open_completions_later should not be called with no text in editor.
trycompletions('event')
Equal(o_c_l.args, None)
- # _open_completions_later should be called with COMPLETE_ATTRIBUTES (1)
+ # _open_completions_later should be called with COMPLETE_ATTRIBUTES (1).
self.text.insert('1.0', 're.')
trycompletions('event')
Equal(o_c_l.args, (False, False, False, 1))
- # _open_completions_later should be called with COMPLETE_FILES (2)
+ # _open_completions_later should be called with COMPLETE_FILES (2).
self.text.delete('1.0', 'end')
self.text.insert('1.0', '"./Lib/')
trycompletions('event')
@@ -85,7 +90,7 @@ class AutoCompleteTest(unittest.TestCase):
autocomplete = self.autocomplete
# Test that the autocomplete event is ignored if user is pressing a
- # modifier key in addition to the tab key
+ # modifier key in addition to the tab key.
ev = Event(mc_state=True)
self.assertIsNone(autocomplete.autocomplete_event(ev))
del ev.mc_state
@@ -95,15 +100,15 @@ class AutoCompleteTest(unittest.TestCase):
self.assertIsNone(autocomplete.autocomplete_event(ev))
self.text.delete('1.0', 'end')
- # If autocomplete window is open, complete() method is called
+ # If autocomplete window is open, complete() method is called.
self.text.insert('1.0', 're.')
- # This must call autocomplete._make_autocomplete_window()
+ # This must call autocomplete._make_autocomplete_window().
Equal(self.autocomplete.autocomplete_event(ev), 'break')
# If autocomplete window is not active or does not exist,
# open_completions is called. Return depends on its return.
autocomplete._remove_autocomplete_window()
- o_cs = Func() # .result = None
+ o_cs = Func() # .result = None.
autocomplete.open_completions = o_cs
Equal(self.autocomplete.autocomplete_event(ev), None)
Equal(o_cs.args, (False, True, True))
@@ -112,32 +117,130 @@ class AutoCompleteTest(unittest.TestCase):
Equal(o_cs.args, (False, True, True))
def test_open_completions_later(self):
- # Test that autocomplete._delayed_completion_id is set
- pass
+ # Test that autocomplete._delayed_completion_id is set.
+ acp = self.autocomplete
+ acp._delayed_completion_id = None
+ acp._open_completions_later(False, False, False, ac.COMPLETE_ATTRIBUTES)
+ cb1 = acp._delayed_completion_id
+ self.assertTrue(cb1.startswith('after'))
+
+ # Test that cb1 is cancelled and cb2 is new.
+ acp._open_completions_later(False, False, False, ac.COMPLETE_FILES)
+ self.assertNotIn(cb1, self.root.tk.call('after', 'info'))
+ cb2 = acp._delayed_completion_id
+ self.assertTrue(cb2.startswith('after') and cb2 != cb1)
+ self.text.after_cancel(cb2)
def test_delayed_open_completions(self):
- # Test that autocomplete._delayed_completion_id set to None and that
- # open_completions only called if insertion index is the same as
- # _delayed_completion_index
- pass
+ # Test that autocomplete._delayed_completion_id set to None
+ # and that open_completions is not called if the index is not
+ # equal to _delayed_completion_index.
+ acp = self.autocomplete
+ acp.open_completions = Func()
+ acp._delayed_completion_id = 'after'
+ acp._delayed_completion_index = self.text.index('insert+1c')
+ acp._delayed_open_completions(1, 2, 3)
+ self.assertIsNone(acp._delayed_completion_id)
+ self.assertEqual(acp.open_completions.called, 0)
+
+ # Test that open_completions is called if indexes match.
+ acp._delayed_completion_index = self.text.index('insert')
+ acp._delayed_open_completions(1, 2, 3, ac.COMPLETE_FILES)
+ self.assertEqual(acp.open_completions.args, (1, 2, 3, 2))
def test_open_completions(self):
# Test completions of files and attributes as well as non-completion
- # of errors
- pass
+ # of errors.
+ self.text.insert('1.0', 'pr')
+ self.assertTrue(self.autocomplete.open_completions(False, True, True))
+ self.text.delete('1.0', 'end')
+
+ # Test files.
+ self.text.insert('1.0', '"t')
+ #self.assertTrue(self.autocomplete.open_completions(False, True, True))
+ self.text.delete('1.0', 'end')
+
+ # Test with blank will fail.
+ self.assertFalse(self.autocomplete.open_completions(False, True, True))
+
+ # Test with only string quote will fail.
+ self.text.insert('1.0', '"')
+ self.assertFalse(self.autocomplete.open_completions(False, True, True))
+ self.text.delete('1.0', 'end')
def test_fetch_completions(self):
# Test that fetch_completions returns 2 lists:
# For attribute completion, a large list containing all variables, and
# a small list containing non-private variables.
# For file completion, a large list containing all files in the path,
- # and a small list containing files that do not start with '.'
- pass
+ # and a small list containing files that do not start with '.'.
+ autocomplete = self.autocomplete
+ small, large = self.autocomplete.fetch_completions(
+ '', ac.COMPLETE_ATTRIBUTES)
+ if __main__.__file__ != ac.__file__:
+ self.assertNotIn('AutoComplete', small) # See issue 36405.
+
+ # Test attributes
+ s, b = autocomplete.fetch_completions('', ac.COMPLETE_ATTRIBUTES)
+ self.assertLess(len(small), len(large))
+ self.assertTrue(all(filter(lambda x: x.startswith('_'), s)))
+ self.assertTrue(any(filter(lambda x: x.startswith('_'), b)))
+
+ # Test smalll should respect to __all__.
+ with patch.dict('__main__.__dict__', {'__all__': ['a', 'b']}):
+ s, b = autocomplete.fetch_completions('', ac.COMPLETE_ATTRIBUTES)
+ self.assertEqual(s, ['a', 'b'])
+ self.assertIn('__name__', b) # From __main__.__dict__
+ self.assertIn('sum', b) # From __main__.__builtins__.__dict__
+
+ # Test attributes with name entity.
+ mock = Mock()
+ mock._private = Mock()
+ with patch.dict('__main__.__dict__', {'foo': mock}):
+ s, b = autocomplete.fetch_completions('foo', ac.COMPLETE_ATTRIBUTES)
+ self.assertNotIn('_private', s)
+ self.assertIn('_private', b)
+ self.assertEqual(s, [i for i in sorted(dir(mock)) if i[:1] != '_'])
+ self.assertEqual(b, sorted(dir(mock)))
+
+ # Test files
+ def _listdir(path):
+ # This will be patch and used in fetch_completions.
+ if path == '.':
+ return ['foo', 'bar', '.hidden']
+ return ['monty', 'python', '.hidden']
+
+ with patch.object(os, 'listdir', _listdir):
+ s, b = autocomplete.fetch_completions('', ac.COMPLETE_FILES)
+ self.assertEqual(s, ['bar', 'foo'])
+ self.assertEqual(b, ['.hidden', 'bar', 'foo'])
+
+ s, b = autocomplete.fetch_completions('~', ac.COMPLETE_FILES)
+ self.assertEqual(s, ['monty', 'python'])
+ self.assertEqual(b, ['.hidden', 'monty', 'python'])
def test_get_entity(self):
# Test that a name is in the namespace of sys.modules and
- # __main__.__dict__
- pass
+ # __main__.__dict__.
+ autocomplete = self.autocomplete
+ Equal = self.assertEqual
+
+ Equal(self.autocomplete.get_entity('int'), int)
+
+ # Test name from sys.modules.
+ mock = Mock()
+ with patch.dict('sys.modules', {'tempfile': mock}):
+ Equal(autocomplete.get_entity('tempfile'), mock)
+
+ # Test name from __main__.__dict__.
+ di = {'foo': 10, 'bar': 20}
+ with patch.dict('__main__.__dict__', {'d': di}):
+ Equal(autocomplete.get_entity('d'), di)
+
+ # Test name not in namespace.
+ with patch.dict('__main__.__dict__', {}):
+ with self.assertRaises(NameError):
+ autocomplete.get_entity('not_exist')
if __name__ == '__main__':
diff --git a/Lib/idlelib/idle_test/test_config.py b/Lib/idlelib/idle_test/test_config.py
index 169d054efd..7e2c1fd295 100644
--- a/Lib/idlelib/idle_test/test_config.py
+++ b/Lib/idlelib/idle_test/test_config.py
@@ -373,10 +373,6 @@ class IdleConfTest(unittest.TestCase):
eq = self.assertEqual
eq(conf.GetHighlight('IDLE Classic', 'normal'), {'foreground': '#000000',
'background': '#ffffff'})
- eq(conf.GetHighlight('IDLE Classic', 'normal', 'fg'), '#000000')
- eq(conf.GetHighlight('IDLE Classic', 'normal', 'bg'), '#ffffff')
- with self.assertRaises(config.InvalidFgBg):
- conf.GetHighlight('IDLE Classic', 'normal', 'fb')
# Test cursor (this background should be normal-background)
eq(conf.GetHighlight('IDLE Classic', 'cursor'), {'foreground': 'black',
diff --git a/Lib/idlelib/idle_test/test_configdialog.py b/Lib/idlelib/idle_test/test_configdialog.py
index 5472a04c18..37e83439c4 100644
--- a/Lib/idlelib/idle_test/test_configdialog.py
+++ b/Lib/idlelib/idle_test/test_configdialog.py
@@ -606,40 +606,35 @@ class HighPageTest(unittest.TestCase):
def test_paint_theme_sample(self):
eq = self.assertEqual
- d = self.page
- del d.paint_theme_sample
- hs_tag = d.highlight_sample.tag_cget
+ page = self.page
+ del page.paint_theme_sample # Delete masking mock.
+ hs_tag = page.highlight_sample.tag_cget
gh = idleConf.GetHighlight
- fg = 'foreground'
- bg = 'background'
# Create custom theme based on IDLE Dark.
- d.theme_source.set(True)
- d.builtin_name.set('IDLE Dark')
+ page.theme_source.set(True)
+ page.builtin_name.set('IDLE Dark')
theme = 'IDLE Test'
- d.create_new(theme)
- d.set_color_sample.called = 0
+ page.create_new(theme)
+ page.set_color_sample.called = 0
# Base theme with nothing in `changes`.
- d.paint_theme_sample()
- eq(hs_tag('break', fg), gh(theme, 'break', fgBg='fg'))
- eq(hs_tag('cursor', bg), gh(theme, 'normal', fgBg='bg'))
- self.assertNotEqual(hs_tag('console', fg), 'blue')
- self.assertNotEqual(hs_tag('console', bg), 'yellow')
- eq(d.set_color_sample.called, 1)
+ page.paint_theme_sample()
+ new_console = {'foreground': 'blue',
+ 'background': 'yellow',}
+ for key, value in new_console.items():
+ self.assertNotEqual(hs_tag('console', key), value)
+ eq(page.set_color_sample.called, 1)
# Apply changes.
- changes.add_option('highlight', theme, 'console-foreground', 'blue')
- changes.add_option('highlight', theme, 'console-background', 'yellow')
- d.paint_theme_sample()
-
- eq(hs_tag('break', fg), gh(theme, 'break', fgBg='fg'))
- eq(hs_tag('cursor', bg), gh(theme, 'normal', fgBg='bg'))
- eq(hs_tag('console', fg), 'blue')
- eq(hs_tag('console', bg), 'yellow')
- eq(d.set_color_sample.called, 2)
+ for key, value in new_console.items():
+ changes.add_option('highlight', theme, 'console-'+key, value)
+ page.paint_theme_sample()
+ for key, value in new_console.items():
+ eq(hs_tag('console', key), value)
+ eq(page.set_color_sample.called, 2)
- d.paint_theme_sample = Func()
+ page.paint_theme_sample = Func()
def test_delete_custom(self):
eq = self.assertEqual
diff --git a/Lib/idlelib/idle_test/test_grep.py b/Lib/idlelib/idle_test/test_grep.py
index ab0d7860f7..a0b5b69171 100644
--- a/Lib/idlelib/idle_test/test_grep.py
+++ b/Lib/idlelib/idle_test/test_grep.py
@@ -5,10 +5,11 @@ An exception raised in one method will fail callers.
Otherwise, tests are mostly independent.
Currently only test grep_it, coverage 51%.
"""
-from idlelib.grep import GrepDialog
+from idlelib import grep
import unittest
from test.support import captured_stdout
from idlelib.idle_test.mock_tk import Var
+import os
import re
@@ -26,23 +27,92 @@ searchengine = Dummy_searchengine()
class Dummy_grep:
# Methods tested
#default_command = GrepDialog.default_command
- grep_it = GrepDialog.grep_it
- findfiles = GrepDialog.findfiles
+ grep_it = grep.GrepDialog.grep_it
# Other stuff needed
recvar = Var(False)
engine = searchengine
def close(self): # gui method
pass
-grep = Dummy_grep()
+_grep = Dummy_grep()
class FindfilesTest(unittest.TestCase):
- # findfiles is really a function, not a method, could be iterator
- # test that filename return filename
- # test that idlelib has many .py files
- # test that recursive flag adds idle_test .py files
- pass
+
+ @classmethod
+ def setUpClass(cls):
+ cls.realpath = os.path.realpath(__file__)
+ cls.path = os.path.dirname(cls.realpath)
+
+ @classmethod
+ def tearDownClass(cls):
+ del cls.realpath, cls.path
+
+ def test_invaliddir(self):
+ with captured_stdout() as s:
+ filelist = list(grep.findfiles('invaliddir', '*.*', False))
+ self.assertEqual(filelist, [])
+ self.assertIn('invalid', s.getvalue())
+
+ def test_curdir(self):
+ # Test os.curdir.
+ ff = grep.findfiles
+ save_cwd = os.getcwd()
+ os.chdir(self.path)
+ filename = 'test_grep.py'
+ filelist = list(ff(os.curdir, filename, False))
+ self.assertIn(os.path.join(os.curdir, filename), filelist)
+ os.chdir(save_cwd)
+
+ def test_base(self):
+ ff = grep.findfiles
+ readme = os.path.join(self.path, 'README.txt')
+
+ # Check for Python files in path where this file lives.
+ filelist = list(ff(self.path, '*.py', False))
+ # This directory has many Python files.
+ self.assertGreater(len(filelist), 10)
+ self.assertIn(self.realpath, filelist)
+ self.assertNotIn(readme, filelist)
+
+ # Look for .txt files in path where this file lives.
+ filelist = list(ff(self.path, '*.txt', False))
+ self.assertNotEqual(len(filelist), 0)
+ self.assertNotIn(self.realpath, filelist)
+ self.assertIn(readme, filelist)
+
+ # Look for non-matching pattern.
+ filelist = list(ff(self.path, 'grep.*', False))
+ self.assertEqual(len(filelist), 0)
+ self.assertNotIn(self.realpath, filelist)
+
+ def test_recurse(self):
+ ff = grep.findfiles
+ parent = os.path.dirname(self.path)
+ grepfile = os.path.join(parent, 'grep.py')
+ pat = '*.py'
+
+ # Get Python files only in parent directory.
+ filelist = list(ff(parent, pat, False))
+ parent_size = len(filelist)
+ # Lots of Python files in idlelib.
+ self.assertGreater(parent_size, 20)
+ self.assertIn(grepfile, filelist)
+ # Without subdirectories, this file isn't returned.
+ self.assertNotIn(self.realpath, filelist)
+
+ # Include subdirectories.
+ filelist = list(ff(parent, pat, True))
+ # More files found now.
+ self.assertGreater(len(filelist), parent_size)
+ self.assertIn(grepfile, filelist)
+ # This file exists in list now.
+ self.assertIn(self.realpath, filelist)
+
+ # Check another level up the tree.
+ parent = os.path.dirname(parent)
+ filelist = list(ff(parent, '*.py', True))
+ self.assertIn(self.realpath, filelist)
class Grep_itTest(unittest.TestCase):
@@ -51,9 +121,9 @@ class Grep_itTest(unittest.TestCase):
# from incomplete replacement, so 'later'.
def report(self, pat):
- grep.engine._pat = pat
+ _grep.engine._pat = pat
with captured_stdout() as s:
- grep.grep_it(re.compile(pat), __file__)
+ _grep.grep_it(re.compile(pat), __file__)
lines = s.getvalue().split('\n')
lines.pop() # remove bogus '' after last \n
return lines
diff --git a/Lib/idlelib/pyshell.py b/Lib/idlelib/pyshell.py
index 11bafdb49a..2de42658b0 100755
--- a/Lib/idlelib/pyshell.py
+++ b/Lib/idlelib/pyshell.py
@@ -1,6 +1,8 @@
#! /usr/bin/env python3
import sys
+if __name__ == "__main__":
+ sys.modules['idlelib.pyshell'] = sys.modules['__main__']
try:
from tkinter import *
@@ -416,10 +418,7 @@ class ModifiedInterpreter(InteractiveInterpreter):
# run from the IDLE source directory.
del_exitf = idleConf.GetOption('main', 'General', 'delete-exitfunc',
default=False, type='bool')
- if __name__ == 'idlelib.pyshell':
- command = "__import__('idlelib.run').run.main(%r)" % (del_exitf,)
- else:
- command = "__import__('run').main(%r)" % (del_exitf,)
+ command = "__import__('idlelib.run').run.main(%r)" % (del_exitf,)
return [sys.executable] + w + ["-c", command, str(self.port)]
def start_subprocess(self):
@@ -1574,7 +1573,6 @@ def main():
capture_warnings(False)
if __name__ == "__main__":
- sys.modules['pyshell'] = sys.modules['__main__']
main()
capture_warnings(False) # Make sure turned off; see issue 18081
diff --git a/Lib/idlelib/replace.py b/Lib/idlelib/replace.py
index 4a834eb790..6be034af96 100644
--- a/Lib/idlelib/replace.py
+++ b/Lib/idlelib/replace.py
@@ -1,7 +1,7 @@
"""Replace dialog for IDLE. Inherits SearchDialogBase for GUI.
-Uses idlelib.SearchEngine for search capability.
+Uses idlelib.searchengine.SearchEngine for search capability.
Defines various replace related functions like replace, replace all,
-replace+find.
+and replace+find.
"""
import re
@@ -10,9 +10,16 @@ from tkinter import StringVar, TclError
from idlelib.searchbase import SearchDialogBase
from idlelib import searchengine
+
def replace(text):
- """Returns a singleton ReplaceDialog instance.The single dialog
- saves user entries and preferences across instances."""
+ """Create or reuse a singleton ReplaceDialog instance.
+
+ The singleton dialog saves user entries and preferences
+ across instances.
+
+ Args:
+ text: Text widget containing the text to be searched.
+ """
root = text._root()
engine = searchengine.get(root)
if not hasattr(engine, "_replacedialog"):
@@ -22,16 +29,36 @@ def replace(text):
class ReplaceDialog(SearchDialogBase):
+ "Dialog for finding and replacing a pattern in text."
title = "Replace Dialog"
icon = "Replace"
def __init__(self, root, engine):
- SearchDialogBase.__init__(self, root, engine)
+ """Create search dialog for finding and replacing text.
+
+ Uses SearchDialogBase as the basis for the GUI and a
+ searchengine instance to prepare the search.
+
+ Attributes:
+ replvar: StringVar containing 'Replace with:' value.
+ replent: Entry widget for replvar. Created in
+ create_entries().
+ ok: Boolean used in searchengine.search_text to indicate
+ whether the search includes the selection.
+ """
+ super().__init__(root, engine)
self.replvar = StringVar(root)
def open(self, text):
- """Display the replace dialog"""
+ """Make dialog visible on top of others and ready to use.
+
+ Also, highlight the currently selected text and set the
+ search to include the current selection (self.ok).
+
+ Args:
+ text: Text widget being searched.
+ """
SearchDialogBase.open(self, text)
try:
first = text.index("sel.first")
@@ -44,37 +71,50 @@ class ReplaceDialog(SearchDialogBase):
first = first or text.index("insert")
last = last or first
self.show_hit(first, last)
- self.ok = 1
+ self.ok = True
def create_entries(self):
- """Create label and text entry widgets"""
+ "Create base and additional label and text entry widgets."
SearchDialogBase.create_entries(self)
self.replent = self.make_entry("Replace with:", self.replvar)[0]
def create_command_buttons(self):
+ """Create base and additional command buttons.
+
+ The additional buttons are for Find, Replace,
+ Replace+Find, and Replace All.
+ """
SearchDialogBase.create_command_buttons(self)
self.make_button("Find", self.find_it)
self.make_button("Replace", self.replace_it)
- self.make_button("Replace+Find", self.default_command, 1)
+ self.make_button("Replace+Find", self.default_command, isdef=True)
self.make_button("Replace All", self.replace_all)
def find_it(self, event=None):
- self.do_find(0)
+ "Handle the Find button."
+ self.do_find(False)
def replace_it(self, event=None):
+ """Handle the Replace button.
+
+ If the find is successful, then perform replace.
+ """
if self.do_find(self.ok):
self.do_replace()
def default_command(self, event=None):
- "Replace and find next."
+ """Handle the Replace+Find button as the default command.
+
+ First performs a replace and then, if the replace was
+ successful, a find next.
+ """
if self.do_find(self.ok):
if self.do_replace(): # Only find next match if replace succeeded.
# A bad re can cause it to fail.
- self.do_find(0)
+ self.do_find(False)
def _replace_expand(self, m, repl):
- """ Helper function for expanding a regular expression
- in the replace field, if needed. """
+ "Expand replacement text if regular expression."
if self.engine.isre():
try:
new = m.expand(repl)
@@ -87,7 +127,15 @@ class ReplaceDialog(SearchDialogBase):
return new
def replace_all(self, event=None):
- """Replace all instances of patvar with replvar in text"""
+ """Handle the Replace All button.
+
+ Search text for occurrences of the Find value and replace
+ each of them. The 'wrap around' value controls the start
+ point for searching. If wrap isn't set, then the searching
+ starts at the first occurrence after the current selection;
+ if wrap is set, the replacement starts at the first line.
+ The replacement is always done top-to-bottom in the text.
+ """
prog = self.engine.getprog()
if not prog:
return
@@ -104,12 +152,13 @@ class ReplaceDialog(SearchDialogBase):
if self.engine.iswrap():
line = 1
col = 0
- ok = 1
+ ok = True
first = last = None
# XXX ought to replace circular instead of top-to-bottom when wrapping
text.undo_block_start()
- while 1:
- res = self.engine.search_forward(text, prog, line, col, 0, ok)
+ while True:
+ res = self.engine.search_forward(text, prog, line, col,
+ wrap=False, ok=ok)
if not res:
break
line, m = res
@@ -130,13 +179,17 @@ class ReplaceDialog(SearchDialogBase):
if new:
text.insert(first, new)
col = i + len(new)
- ok = 0
+ ok = False
text.undo_block_stop()
if first and last:
self.show_hit(first, last)
self.close()
- def do_find(self, ok=0):
+ def do_find(self, ok=False):
+ """Search for and highlight next occurrence of pattern in text.
+
+ No text replacement is done with this option.
+ """
if not self.engine.getprog():
return False
text = self.text
@@ -149,10 +202,11 @@ class ReplaceDialog(SearchDialogBase):
first = "%d.%d" % (line, i)
last = "%d.%d" % (line, j)
self.show_hit(first, last)
- self.ok = 1
+ self.ok = True
return True
def do_replace(self):
+ "Replace search pattern in text with replacement value."
prog = self.engine.getprog()
if not prog:
return False
@@ -180,12 +234,20 @@ class ReplaceDialog(SearchDialogBase):
text.insert(first, new)
text.undo_block_stop()
self.show_hit(first, text.index("insert"))
- self.ok = 0
+ self.ok = False
return True
def show_hit(self, first, last):
- """Highlight text from 'first' to 'last'.
- 'first', 'last' - Text indices"""
+ """Highlight text between first and last indices.
+
+ Text is highlighted via the 'hit' tag and the marked
+ section is brought into view.
+
+ The colors from the 'hit' tag aren't currently shown
+ when the text is displayed. This is due to the 'sel'
+ tag being added first, so the colors in the 'sel'
+ config are seen instead of the colors for 'hit'.
+ """
text = self.text
text.mark_set("insert", first)
text.tag_remove("sel", "1.0", "end")
@@ -199,6 +261,7 @@ class ReplaceDialog(SearchDialogBase):
text.update_idletasks()
def close(self, event=None):
+ "Close the dialog and remove hit tags."
SearchDialogBase.close(self, event)
self.text.tag_remove("hit", "1.0", "end")
diff --git a/Lib/idlelib/rpc.py b/Lib/idlelib/rpc.py
index 9962477cc5..f035bde4a0 100644
--- a/Lib/idlelib/rpc.py
+++ b/Lib/idlelib/rpc.py
@@ -64,8 +64,7 @@ def dumps(obj, protocol=None):
class CodePickler(pickle.Pickler):
- dispatch_table = {types.CodeType: pickle_code}
- dispatch_table.update(copyreg.dispatch_table)
+ dispatch_table = {types.CodeType: pickle_code, **copyreg.dispatch_table}
BUFSIZE = 8*1024
diff --git a/Lib/idlelib/search.py b/Lib/idlelib/search.py
index 6e5a0c7973..5bbe9d6b5d 100644
--- a/Lib/idlelib/search.py
+++ b/Lib/idlelib/search.py
@@ -1,10 +1,23 @@
+"""Search dialog for Find, Find Again, and Find Selection
+ functionality.
+
+ Inherits from SearchDialogBase for GUI and uses searchengine
+ to prepare search pattern.
+"""
from tkinter import TclError
from idlelib import searchengine
from idlelib.searchbase import SearchDialogBase
def _setup(text):
- "Create or find the singleton SearchDialog instance."
+ """Return the new or existing singleton SearchDialog instance.
+
+ The singleton dialog saves user entries and preferences
+ across instances.
+
+ Args:
+ text: Text widget containing the text to be searched.
+ """
root = text._root()
engine = searchengine.get(root)
if not hasattr(engine, "_searchdialog"):
@@ -12,31 +25,71 @@ def _setup(text):
return engine._searchdialog
def find(text):
- "Handle the editor edit menu item and corresponding event."
+ """Open the search dialog.
+
+ Module-level function to access the singleton SearchDialog
+ instance and open the dialog. If text is selected, it is
+ used as the search phrase; otherwise, the previous entry
+ is used. No search is done with this command.
+ """
pat = text.get("sel.first", "sel.last")
return _setup(text).open(text, pat) # Open is inherited from SDBase.
def find_again(text):
- "Handle the editor edit menu item and corresponding event."
+ """Repeat the search for the last pattern and preferences.
+
+ Module-level function to access the singleton SearchDialog
+ instance to search again using the user entries and preferences
+ from the last dialog. If there was no prior search, open the
+ search dialog; otherwise, perform the search without showing the
+ dialog.
+ """
return _setup(text).find_again(text)
def find_selection(text):
- "Handle the editor edit menu item and corresponding event."
+ """Search for the selected pattern in the text.
+
+ Module-level function to access the singleton SearchDialog
+ instance to search using the selected text. With a text
+ selection, perform the search without displaying the dialog.
+ Without a selection, use the prior entry as the search phrase
+ and don't display the dialog. If there has been no prior
+ search, open the search dialog.
+ """
return _setup(text).find_selection(text)
class SearchDialog(SearchDialogBase):
+ "Dialog for finding a pattern in text."
def create_widgets(self):
+ "Create the base search dialog and add a button for Find Next."
SearchDialogBase.create_widgets(self)
- self.make_button("Find Next", self.default_command, 1)
+ # TODO - why is this here and not in a create_command_buttons?
+ self.make_button("Find Next", self.default_command, isdef=True)
def default_command(self, event=None):
+ "Handle the Find Next button as the default command."
if not self.engine.getprog():
return
self.find_again(self.text)
def find_again(self, text):
+ """Repeat the last search.
+
+ If no search was previously run, open a new search dialog. In
+ this case, no search is done.
+
+ If a seach was previously run, the search dialog won't be
+ shown and the options from the previous search (including the
+ search pattern) will be used to find the next occurrence
+ of the pattern. Next is relative based on direction.
+
+ Position the window to display the located occurrence in the
+ text.
+
+ Return True if the search was successful and False otherwise.
+ """
if not self.engine.getpat():
self.open(text)
return False
@@ -66,6 +119,13 @@ class SearchDialog(SearchDialogBase):
return False
def find_selection(self, text):
+ """Search for selected text with previous dialog preferences.
+
+ Instead of using the same pattern for searching (as Find
+ Again does), this first resets the pattern to the currently
+ selected text. If the selected text isn't changed, then use
+ the prior search phrase.
+ """
pat = text.get("sel.first", "sel.last")
if pat:
self.engine.setcookedpat(pat)