diff options
author | Giampaolo Rodola <g.rodola@gmail.com> | 2019-03-28 15:23:28 +0100 |
---|---|---|
committer | Giampaolo Rodola <g.rodola@gmail.com> | 2019-03-28 15:23:28 +0100 |
commit | e19b28f2bd89c047b12f6a8ffb1fe793834700d3 (patch) | |
tree | 8261b98b29eedb8ce67df4d571e8ba9b948d17ab /Lib/idlelib | |
parent | f7868847da9f84cb68605b4b94d8fcc205e0766e (diff) | |
parent | 3eca28c61363a03b81b9fb12775490d6e42d8ecf (diff) | |
download | cpython-git-e19b28f2bd89c047b12f6a8ffb1fe793834700d3.tar.gz |
Merge branch 'master' into bind-socket
Diffstat (limited to 'Lib/idlelib')
-rw-r--r-- | Lib/idlelib/NEWS.txt | 26 | ||||
-rw-r--r-- | Lib/idlelib/autocomplete.py | 29 | ||||
-rw-r--r-- | Lib/idlelib/autocomplete_w.py | 2 | ||||
-rw-r--r-- | Lib/idlelib/calltip.py | 7 | ||||
-rw-r--r-- | Lib/idlelib/calltip_w.py | 3 | ||||
-rw-r--r-- | Lib/idlelib/colorizer.py | 2 | ||||
-rw-r--r-- | Lib/idlelib/config.py | 39 | ||||
-rw-r--r-- | Lib/idlelib/configdialog.py | 2 | ||||
-rw-r--r-- | Lib/idlelib/grep.py | 94 | ||||
-rw-r--r-- | Lib/idlelib/idle_test/test_autocomplete.py | 147 | ||||
-rw-r--r-- | Lib/idlelib/idle_test/test_config.py | 4 | ||||
-rw-r--r-- | Lib/idlelib/idle_test/test_configdialog.py | 45 | ||||
-rw-r--r-- | Lib/idlelib/idle_test/test_grep.py | 92 | ||||
-rwxr-xr-x | Lib/idlelib/pyshell.py | 8 | ||||
-rw-r--r-- | Lib/idlelib/replace.py | 111 | ||||
-rw-r--r-- | Lib/idlelib/rpc.py | 3 | ||||
-rw-r--r-- | Lib/idlelib/search.py | 70 |
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) |