Issue 27437: Add query.ModuleName and use it for file => Load Module.

Users can now edit bad entries instead of starting over.
This commit is contained in:
Terry Jan Reedy 2016-07-03 19:11:13 -04:00
parent bae75cf3fe
commit 0cd6b97701
5 changed files with 185 additions and 77 deletions

View file

@ -41,7 +41,6 @@ config.py # Load, fetch, and save configuration (nim).
configdialog.py # Display user configuration dialogs. configdialog.py # Display user configuration dialogs.
config_help.py # Specify help source in configdialog. config_help.py # Specify help source in configdialog.
config_key.py # Change keybindings. config_key.py # Change keybindings.
config_sec.py # Spefify user config section name
dynoption.py # Define mutable OptionMenu widget (nim). dynoption.py # Define mutable OptionMenu widget (nim).
debugobj.py # Define class used in stackviewer. debugobj.py # Define class used in stackviewer.
debugobj_r.py # Communicate objects between processes with rpc (nim). debugobj_r.py # Communicate objects between processes with rpc (nim).
@ -66,6 +65,7 @@ pathbrowser.py # Create path browser window.
percolator.py # Manage delegator stack (nim). percolator.py # Manage delegator stack (nim).
pyparse.py # Give information on code indentation pyparse.py # Give information on code indentation
pyshell.py # Start IDLE, manage shell, complete editor window pyshell.py # Start IDLE, manage shell, complete editor window
query.py # Query user for informtion
redirector.py # Intercept widget subcommands (for percolator) (nim). redirector.py # Intercept widget subcommands (for percolator) (nim).
replace.py # Search and replace pattern in text. replace.py # Search and replace pattern in text.
rpc.py # Commuicate between idle and user processes (nim). rpc.py # Commuicate between idle and user processes (nim).
@ -192,8 +192,8 @@ Options
Configure IDLE # eEW.config_dialog, configdialog Configure IDLE # eEW.config_dialog, configdialog
(tabs in the dialog) (tabs in the dialog)
Font tab # config-main.def Font tab # config-main.def
Highlight tab # config_sec, config-highlight.def Highlight tab # query, config-highlight.def
Keys tab # config_key, configconfig_secg-keus.def Keys tab # query, config_key, config_keys.def
General tab # config_help, config-main.def General tab # config_help, config-main.def
Extensions tab # config-extensions.def, corresponding .py Extensions tab # config-extensions.def, corresponding .py
--- ---

View file

@ -14,6 +14,7 @@ import traceback
import webbrowser import webbrowser
from idlelib.multicall import MultiCallCreator from idlelib.multicall import MultiCallCreator
from idlelib import query
from idlelib import windows from idlelib import windows
from idlelib import search from idlelib import search
from idlelib import grep from idlelib import grep
@ -573,46 +574,27 @@ class EditorWindow(object):
text.see("insert") text.see("insert")
def open_module(self, event=None): def open_module(self, event=None):
# XXX Shouldn't this be in IOBinding? """Get module name from user and open it.
Return module path or None for calls by open_class_browser
when latter is not invoked in named editor window.
"""
# XXX This, open_class_browser, and open_path_browser
# would fit better in iomenu.IOBinding.
try: try:
name = self.text.get("sel.first", "sel.last") name = self.text.get("sel.first", "sel.last").strip()
except TclError: except TclError:
name = "" name = ''
else: file_path = query.ModuleName(
name = name.strip() self.text, "Open Module",
name = tkSimpleDialog.askstring("Module", "Enter the name of a Python module\n"
"Enter the name of a Python module\n" "to search on sys.path and open:",
"to search on sys.path and open:", name).result
parent=self.text, initialvalue=name) if file_path is not None:
if name: if self.flist:
name = name.strip() self.flist.open(file_path)
if not name: else:
return self.io.loadfile(file_path)
# XXX Ought to insert current file's directory in front of path
try:
spec = importlib.util.find_spec(name)
except (ValueError, ImportError) as msg:
tkMessageBox.showerror("Import error", str(msg), parent=self.text)
return
if spec is None:
tkMessageBox.showerror("Import error", "module not found",
parent=self.text)
return
if not isinstance(spec.loader, importlib.abc.SourceLoader):
tkMessageBox.showerror("Import error", "not a source-based module",
parent=self.text)
return
try:
file_path = spec.loader.get_filename(name)
except AttributeError:
tkMessageBox.showerror("Import error",
"loader does not support get_filename",
parent=self.text)
return
if self.flist:
self.flist.open(file_path)
else:
self.io.loadfile(file_path)
return file_path return file_path
def open_class_browser(self, event=None): def open_class_browser(self, event=None):

View file

@ -235,8 +235,9 @@ _percolator_spec = {
Query_spec = { Query_spec = {
'file': 'query', 'file': 'query',
'kwds': {'title':'Query', 'kwds': {'title': 'Query',
'message':'Enter something', 'message': 'Enter something',
'text0': 'Go',
'_htest': True}, '_htest': True},
'msg': "Enter with <Return> or [Ok]. Print valid entry to Shell\n" 'msg': "Enter with <Return> or [Ok]. Print valid entry to Shell\n"
"Blank line, after stripping, is ignored\n" "Blank line, after stripping, is ignored\n"

View file

@ -8,8 +8,8 @@ import unittest
from unittest import mock from unittest import mock
from idlelib.idle_test.mock_tk import Var, Mbox_func from idlelib.idle_test.mock_tk import Var, Mbox_func
from idlelib import query from idlelib import query
Query, SectionName = query.Query, query.SectionName
Query = query.Query
class Dummy_Query: class Dummy_Query:
# Mock for testing the following methods Query # Mock for testing the following methods Query
entry_ok = Query.entry_ok entry_ok = Query.entry_ok
@ -23,7 +23,7 @@ class Dummy_Query:
self.destroyed = True self.destroyed = True
# entry_ok calls modal messagebox.showerror if entry is not ok. # entry_ok calls modal messagebox.showerror if entry is not ok.
# Mock showerrer returns, so don't need to click to continue. # Mock showerrer so don't need to click to continue.
orig_showerror = query.showerror orig_showerror = query.showerror
showerror = Mbox_func() # Instance has __call__ method. showerror = Mbox_func() # Instance has __call__ method.
@ -46,7 +46,7 @@ class QueryTest(unittest.TestCase):
dialog = self.dialog dialog = self.dialog
Equal = self.assertEqual Equal = self.assertEqual
dialog.entry.set(' ') dialog.entry.set(' ')
Equal(dialog.entry_ok(), '') Equal(dialog.entry_ok(), None)
Equal((dialog.result, dialog.destroyed), (None, False)) Equal((dialog.result, dialog.destroyed), (None, False))
Equal(showerror.title, 'Entry Error') Equal(showerror.title, 'Entry Error')
self.assertIn('Blank', showerror.message) self.assertIn('Blank', showerror.message)
@ -74,44 +74,41 @@ class QueryTest(unittest.TestCase):
class Dummy_SectionName: class Dummy_SectionName:
# Mock for testing the following method of Section_Name entry_ok = query.SectionName.entry_ok # Test override.
entry_ok = SectionName.entry_ok
# Attributes, constant or variable, needed for tests
used_names = ['used'] used_names = ['used']
entry = Var() entry = Var()
class SectionNameTest(unittest.TestCase): class SectionNameTest(unittest.TestCase):
dialog = Dummy_SectionName() dialog = Dummy_SectionName()
def setUp(self): def setUp(self):
showerror.title = None showerror.title = None
def test_blank_name(self): def test_blank_section_name(self):
dialog = self.dialog dialog = self.dialog
Equal = self.assertEqual Equal = self.assertEqual
dialog.entry.set(' ') dialog.entry.set(' ')
Equal(dialog.entry_ok(), '') Equal(dialog.entry_ok(), None)
Equal(showerror.title, 'Name Error') Equal(showerror.title, 'Name Error')
self.assertIn('No', showerror.message) self.assertIn('No', showerror.message)
def test_used_name(self): def test_used_section_name(self):
dialog = self.dialog dialog = self.dialog
Equal = self.assertEqual Equal = self.assertEqual
dialog.entry.set('used') dialog.entry.set('used')
Equal(self.dialog.entry_ok(), '') Equal(self.dialog.entry_ok(), None)
Equal(showerror.title, 'Name Error') Equal(showerror.title, 'Name Error')
self.assertIn('use', showerror.message) self.assertIn('use', showerror.message)
def test_long_name(self): def test_long_section_name(self):
dialog = self.dialog dialog = self.dialog
Equal = self.assertEqual Equal = self.assertEqual
dialog.entry.set('good'*8) dialog.entry.set('good'*8)
Equal(self.dialog.entry_ok(), '') Equal(self.dialog.entry_ok(), None)
Equal(showerror.title, 'Name Error') Equal(showerror.title, 'Name Error')
self.assertIn('too long', showerror.message) self.assertIn('too long', showerror.message)
def test_good_entry(self): def test_good_section_name(self):
dialog = self.dialog dialog = self.dialog
Equal = self.assertEqual Equal = self.assertEqual
dialog.entry.set(' good ') dialog.entry.set(' good ')
@ -119,13 +116,56 @@ class SectionNameTest(unittest.TestCase):
Equal(showerror.title, None) Equal(showerror.title, None)
class Dummy_ModuleName:
entry_ok = query.ModuleName.entry_ok # Test override
text0 = ''
entry = Var()
class ModuleNameTest(unittest.TestCase):
dialog = Dummy_ModuleName()
def setUp(self):
showerror.title = None
def test_blank_module_name(self):
dialog = self.dialog
Equal = self.assertEqual
dialog.entry.set(' ')
Equal(dialog.entry_ok(), None)
Equal(showerror.title, 'Name Error')
self.assertIn('No', showerror.message)
def test_bogus_module_name(self):
dialog = self.dialog
Equal = self.assertEqual
dialog.entry.set('__name_xyz123_should_not_exist__')
Equal(self.dialog.entry_ok(), None)
Equal(showerror.title, 'Import Error')
self.assertIn('not found', showerror.message)
def test_c_source_name(self):
dialog = self.dialog
Equal = self.assertEqual
dialog.entry.set('itertools')
Equal(self.dialog.entry_ok(), None)
Equal(showerror.title, 'Import Error')
self.assertIn('source-based', showerror.message)
def test_good_module_name(self):
dialog = self.dialog
Equal = self.assertEqual
dialog.entry.set('idlelib')
self.assertTrue(dialog.entry_ok().endswith('__init__.py'))
Equal(showerror.title, None)
class QueryGuiTest(unittest.TestCase): class QueryGuiTest(unittest.TestCase):
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
requires('gui') requires('gui')
cls.root = Tk() cls.root = root = Tk()
cls.dialog = Query(cls.root, 'TEST', 'test', _utest=True) cls.dialog = Query(root, 'TEST', 'test', _utest=True)
cls.dialog.destroy = mock.Mock() cls.dialog.destroy = mock.Mock()
@classmethod @classmethod
@ -160,5 +200,43 @@ class QueryGuiTest(unittest.TestCase):
self.assertTrue(dialog.destroy.called) self.assertTrue(dialog.destroy.called)
class SectionnameGuiTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
requires('gui')
def test_click_section_name(self):
root = Tk()
dialog = query.SectionName(root, 'T', 't', {'abc'}, _utest=True)
Equal = self.assertEqual
Equal(dialog.used_names, {'abc'})
dialog.entry.insert(0, 'okay')
dialog.button_ok.invoke()
Equal(dialog.result, 'okay')
del dialog
root.destroy()
del root
class ModulenameGuiTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
requires('gui')
def test_click_module_name(self):
root = Tk()
dialog = query.ModuleName(root, 'T', 't', 'idlelib', _utest=True)
Equal = self.assertEqual
Equal(dialog.text0, 'idlelib')
Equal(dialog.entry.get(), 'idlelib')
dialog.button_ok.invoke()
self.assertTrue(dialog.result.endswith('__init__.py'))
del dialog
root.destroy()
del root
if __name__ == '__main__': if __name__ == '__main__':
unittest.main(verbosity=2, exit=False) unittest.main(verbosity=2, exit=False)

View file

@ -15,7 +15,8 @@ Configdialog uses it for new highlight theme and keybinding set names.
# of configSectionNameDialog.py (temporarily config_sec.py) into # of configSectionNameDialog.py (temporarily config_sec.py) into
# generic and specific parts. # generic and specific parts.
from tkinter import FALSE, TRUE, Toplevel import importlib
from tkinter import Toplevel, StringVar
from tkinter.messagebox import showerror from tkinter.messagebox import showerror
from tkinter.ttk import Frame, Button, Entry, Label from tkinter.ttk import Frame, Button, Entry, Label
@ -24,20 +25,22 @@ class Query(Toplevel):
For this base class, accept any non-blank string. For this base class, accept any non-blank string.
""" """
def __init__(self, parent, title, message, def __init__(self, parent, title, message, text0='',
*, _htest=False, _utest=False): # Call from override. *, _htest=False, _utest=False):
"""Create popup, do not return until tk widget destroyed. """Create popup, do not return until tk widget destroyed.
Additional subclass init must be done before calling this. Additional subclass init must be done before calling this
unless _utest=True is passed to suppress wait_window().
title - string, title of popup dialog title - string, title of popup dialog
message - string, informational message to display message - string, informational message to display
text0 - initial value for entry
_htest - bool, change box location when running htest _htest - bool, change box location when running htest
_utest - bool, leave window hidden and not modal _utest - bool, leave window hidden and not modal
""" """
Toplevel.__init__(self, parent) Toplevel.__init__(self, parent)
self.configure(borderwidth=5) self.configure(borderwidth=5)
self.resizable(height=FALSE, width=FALSE) self.resizable(height=False, width=False)
self.title(title) self.title(title)
self.transient(parent) self.transient(parent)
self.grab_set() self.grab_set()
@ -45,6 +48,7 @@ class Query(Toplevel):
self.protocol("WM_DELETE_WINDOW", self.cancel) self.protocol("WM_DELETE_WINDOW", self.cancel)
self.parent = parent self.parent = parent
self.message = message self.message = message
self.text0 = text0
self.create_widgets() self.create_widgets()
self.update_idletasks() self.update_idletasks()
#needs to be done here so that the winfo_reqwidth is valid #needs to be done here so that the winfo_reqwidth is valid
@ -62,31 +66,34 @@ class Query(Toplevel):
self.wait_window() self.wait_window()
def create_widgets(self): # Call from override, if any. def create_widgets(self): # Call from override, if any.
# Bind widgets needed for entry_ok or unittest to self.
frame = Frame(self, borderwidth=2, relief='sunken', ) frame = Frame(self, borderwidth=2, relief='sunken', )
label = Label(frame, anchor='w', justify='left', label = Label(frame, anchor='w', justify='left',
text=self.message) text=self.message)
self.entry = Entry(frame, width=30) # Bind name for entry_ok. self.entryvar = StringVar(self, self.text0)
self.entry = Entry(frame, width=30, textvariable=self.entryvar)
self.entry.focus_set() self.entry.focus_set()
buttons = Frame(self) # Bind buttons for invoke in unittest. buttons = Frame(self)
self.button_ok = Button(buttons, text='Ok', self.button_ok = Button(buttons, text='Ok',
width=8, command=self.ok) width=8, command=self.ok)
self.button_cancel = Button(buttons, text='Cancel', self.button_cancel = Button(buttons, text='Cancel',
width=8, command=self.cancel) width=8, command=self.cancel)
frame.pack(side='top', expand=TRUE, fill='both') frame.pack(side='top', expand=True, fill='both')
label.pack(padx=5, pady=5) label.pack(padx=5, pady=5)
self.entry.pack(padx=5, pady=5) self.entry.pack(padx=5, pady=5)
buttons.pack(side='bottom') buttons.pack(side='bottom')
self.button_ok.pack(side='left', padx=5) self.button_ok.pack(side='left', padx=5)
self.button_cancel.pack(side='right', padx=5) self.button_cancel.pack(side='right', padx=5)
def entry_ok(self): # Usually replace. def entry_ok(self): # Example: usually replace.
"Check that entry not blank." "Return non-blank entry or None."
entry = self.entry.get().strip() entry = self.entry.get().strip()
if not entry: if not entry:
showerror(title='Entry Error', showerror(title='Entry Error',
message='Blank line.', parent=self) message='Blank line.', parent=self)
return
return entry return entry
def ok(self, event=None): # Do not replace. def ok(self, event=None): # Do not replace.
@ -95,7 +102,7 @@ class Query(Toplevel):
Otherwise leave dialog open for user to correct entry or cancel. Otherwise leave dialog open for user to correct entry or cancel.
''' '''
entry = self.entry_ok() entry = self.entry_ok()
if entry: if entry is not None:
self.result = entry self.result = entry
self.destroy() self.destroy()
else: else:
@ -114,32 +121,72 @@ class SectionName(Query):
def __init__(self, parent, title, message, used_names, def __init__(self, parent, title, message, used_names,
*, _htest=False, _utest=False): *, _htest=False, _utest=False):
"used_names - collection of strings already in use" "used_names - collection of strings already in use"
self.used_names = used_names self.used_names = used_names
Query.__init__(self, parent, title, message, Query.__init__(self, parent, title, message,
_htest=_htest, _utest=_utest) _htest=_htest, _utest=_utest)
# This call does ot return until tk widget is destroyed.
def entry_ok(self): def entry_ok(self):
'''Stripping entered name, check that it is a sensible "Return sensible ConfigParser section name or None."
ConfigParser file section name. Return it if it is, '' if not.
'''
name = self.entry.get().strip() name = self.entry.get().strip()
if not name: if not name:
showerror(title='Name Error', showerror(title='Name Error',
message='No name specified.', parent=self) message='No name specified.', parent=self)
return
elif len(name)>30: elif len(name)>30:
showerror(title='Name Error', showerror(title='Name Error',
message='Name too long. It should be no more than '+ message='Name too long. It should be no more than '+
'30 characters.', parent=self) '30 characters.', parent=self)
name = '' return
elif name in self.used_names: elif name in self.used_names:
showerror(title='Name Error', showerror(title='Name Error',
message='This name is already in use.', parent=self) message='This name is already in use.', parent=self)
name = '' return
return name return name
class ModuleName(Query):
"Get a module name for Open Module menu entry."
# Used in open_module (editor.EditorWindow until move to iobinding).
def __init__(self, parent, title, message, text0='',
*, _htest=False, _utest=False):
"""text0 - name selected in text before Open Module invoked"
"""
Query.__init__(self, parent, title, message, text0=text0,
_htest=_htest, _utest=_utest)
def entry_ok(self):
"Return entered module name as file path or None."
# Moved here from Editor_Window.load_module 2016 July.
name = self.entry.get().strip()
if not name:
showerror(title='Name Error',
message='No name specified.', parent=self)
return
# XXX Ought to insert current file's directory in front of path
try:
spec = importlib.util.find_spec(name)
except (ValueError, ImportError) as msg:
showerror("Import Error", str(msg), parent=self)
return
if spec is None:
showerror("Import Error", "module not found",
parent=self)
return
if not isinstance(spec.loader, importlib.abc.SourceLoader):
showerror("Import Error", "not a source-based module",
parent=self)
return
try:
file_path = spec.loader.get_filename(name)
except AttributeError:
showerror("Import Error",
"loader does not support get_filename",
parent=self)
return
return file_path
if __name__ == '__main__': if __name__ == '__main__':
import unittest import unittest
unittest.main('idlelib.idle_test.test_query', verbosity=2, exit=False) unittest.main('idlelib.idle_test.test_query', verbosity=2, exit=False)