Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Create more generic dialog and change keybinding
  • Loading branch information
csabella committed Jun 3, 2019
commit dbdd58076c8a04b58d2c00bafd90336429e1abd3
14 changes: 9 additions & 5 deletions Doc/library/idle.rst
Original file line number Diff line number Diff line change
Expand Up @@ -232,12 +232,16 @@ Run Module
This is similar to executing a file with ``python -i file`` at a command
line.

.. _run-module-with-arguments:
.. _run-custom:

Run Custom
Same as :ref:`Run Module <run-module>`, except allow customization of the
environment used when running the module.

The *Command Line Arguments* are passed to the module like :data:`sys.argv`
arguments would be from the command line. This is similar to executing a
file with ``python -i file [args]``.

Run Module with Arguments
Same as :ref:`Run Module <run-module>`, except allow entry of the
command line arguments that are passed to the module. This is similar
to executing a file with ``python -i file [args]`` at a command line.

Shell menu (Shell window only)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Expand Down
10 changes: 5 additions & 5 deletions Lib/idlelib/config-keys.def
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ force-open-calltip= <Control-Key-backslash>
format-paragraph= <Alt-Key-q>
flash-paren= <Control-Key-0>
run-module= <Key-F5>
run-module-arguments= <Key-F7>
run-custom= <Shift-Key-F5>
check-module= <Alt-Key-x>
zoom-height= <Alt-Key-2>

Expand Down Expand Up @@ -123,7 +123,7 @@ force-open-calltip= <Control-Key-backslash>
format-paragraph= <Alt-Key-q>
flash-paren= <Control-Key-0>
run-module= <Key-F5>
run-module-arguments= <Key-F7>
run-custom= <Shift-Key-F5>
check-module= <Alt-Key-x>
zoom-height= <Alt-Key-2>

Expand Down Expand Up @@ -183,7 +183,7 @@ force-open-calltip= <Control-Key-backslash>
format-paragraph= <Alt-Key-q>
flash-paren= <Control-Key-0>
run-module= <Key-F5>
run-module-arguments= <Key-F7>
run-custom= <Shift-Key-F5>
check-module= <Alt-Key-x>
zoom-height= <Alt-Key-2>

Expand Down Expand Up @@ -243,7 +243,7 @@ force-open-calltip= <Control-Key-backslash>
format-paragraph= <Option-Key-q>
flash-paren= <Control-Key-0>
run-module= <Key-F5>
run-module-arguments= <Key-F7>
run-custom= <Shift-Key-F5>
check-module= <Option-Key-x>
zoom-height= <Option-Key-0>

Expand Down Expand Up @@ -304,6 +304,6 @@ force-open-calltip= <Control-Key-backslash>
format-paragraph= <Option-Key-q>
flash-paren= <Control-Key-0>
run-module= <Key-F5>
run-module-arguments= <Key-F7>
run-custom= <Shift-Key-F5>
check-module= <Option-Key-x>
zoom-height= <Option-Key-0>
2 changes: 1 addition & 1 deletion Lib/idlelib/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -658,7 +658,7 @@ def GetCoreKeys(self, keySetName=None):
'<<flash-paren>>': ['<Control-Key-0>'],
'<<format-paragraph>>': ['<Alt-Key-q>'],
'<<run-module>>': ['<Key-F5>'],
'<<run-module-arguments>>': ['<Key-F7>'],
'<<run-custom>>': ['<Shift-Key-F5>'],
'<<check-module>>': ['<Alt-Key-x>'],
'<<zoom-height>>': ['<Alt-Key-2>'],
}
Expand Down
2 changes: 1 addition & 1 deletion Lib/idlelib/editor.py
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,7 @@ def __init__(self, flist=None, filename=None, key=None, root=None):
scriptbinding = ScriptBinding(self)
text.bind("<<check-module>>", scriptbinding.check_module_event)
text.bind("<<run-module>>", scriptbinding.run_module_event)
text.bind("<<run-module-arguments>>", scriptbinding.run_module_arguments_event)
text.bind("<<run-custom>>", scriptbinding.run_custom_event)
text.bind("<<do-rstrip>>", self.Rstrip(self).do_rstrip)
ctip = self.Calltip(self)
text.bind("<<try-open-calltip>>", ctip.try_open_calltip_event)
Expand Down
5 changes: 2 additions & 3 deletions Lib/idlelib/idle_test/htest.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,10 +108,9 @@ def _wrapper(parent): # htest #
"The default color scheme is in idlelib/config-highlight.def"
}

CommandLineArgs_spec = {
CustomRun_spec = {
'file': 'query',
'kwds': {'title': 'Command Line Args',
'message': 'Enter command line arguments',
'kwds': {'title': 'Custom Run Args',
'_htest': True},
'msg': "Enter with <Return> or [Ok]. Print valid entry to Shell\n"
"Arguments are parsed into a list\n"
Expand Down
48 changes: 34 additions & 14 deletions Lib/idlelib/idle_test/test_query.py
Original file line number Diff line number Diff line change
Expand Up @@ -242,11 +242,11 @@ def test_entry_ok_helpsource(self):
self.assertEqual(dialog.entry_ok(), result)


class CommandLineArgsTest(unittest.TestCase):
"Test CommandLineArgs subclass of Query."
class CustomRunCLIargsokTest(unittest.TestCase):
"Test cli_ok method of the CustomRun subclass of Query."

class Dummy_CommandLineArgs:
entry_ok = query.CommandLineArgs.entry_ok # Function being tested.
class Dummy_CustomRun:
cli_args_ok = query.CustomRun.cli_args_ok # Function being tested.
entry = Var()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this a class attribute? Besides being unclear, this also doesn't reflect the actual CustomRun class.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wrote the query tests, and Cheryl followed the pattern I set. For testing with one instance at a time, or ever, class and instance attributes work the same. For attributes never rebound, I attached them to the class, especially if there was no other need for an init method. Very clear to me. However, I moved Var attributes to init when there is one as it saves a line.

The cli_args_ok method being tested accesses self.query. tk Vars and Entries both have a get method that does the same thing, so a mock Var replaces an Entry quite nicely.

entry_error = {}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be simpler to make Dummy_CustomRun a Mock instance and then check calls to showerror(), rather than the entry_error['text'] stuff which is more implementation-specific.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I used this in all the other functional method tests. The dummy classes have and do exactly what is needed to test the method. A unittest.mock would be much more complicated and difficult and would run much slower. Test speed is important.

def __init__(self, dummy_entry):
Expand All @@ -256,22 +256,43 @@ def showerror(self, message):
self.entry_error['text'] = message

def test_blank_args(self):
dialog = self.Dummy_CommandLineArgs(' ')
self.assertEqual(dialog.entry_ok(), None)
dialog = self.Dummy_CustomRun(' ')
self.assertEqual(dialog.cli_args_ok(), None)
self.assertIn('no arguments', dialog.entry_error['text'])
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should use assertRegex to do a case-insensitive check.

Suggested change
self.assertIn('no arguments', dialog.entry_error['text'])
self.assertRegex(r'(?i)no arguments', dialog.entry_error['text'])

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This assert is gone.


def test_invalid_args(self):
dialog = self.Dummy_CommandLineArgs("'no-closing-quote")
self.assertEqual(dialog.entry_ok(), None)
dialog = self.Dummy_CustomRun("'no-closing-quote")
self.assertEqual(dialog.cli_args_ok(), None)
self.assertIn('No closing', dialog.entry_error['text'])
Comment thread
terryjreedy marked this conversation as resolved.

def test_good_args(self):
dialog = self.Dummy_CommandLineArgs('-n 10 --verbose -p /path --name "my name"')
self.assertEqual(dialog.entry_ok(),
dialog = self.Dummy_CustomRun('-n 10 --verbose -p /path --name "my name"')
self.assertEqual(dialog.cli_args_ok(),
['-n', '10', '--verbose', '-p', '/path', '--name', 'my name'])
self.assertEqual(dialog.entry_error['text'], '')


class CustomRunEntryokTest(unittest.TestCase):
"Test entry_ok method of the CustomRun subclass of Query."

class Dummy_CustomRun:
entry_ok = query.CustomRun.entry_ok
entry_error = {}
def cli_args_ok(self):
return self.cli_args
def restart_ok(self):
return self.restart

def test_entry_ok_customrun(self):
dialog = self.Dummy_CustomRun()
for dialog.restart in {True, False}:
for cli_args, result in ((None, None),
('my arg', ('my arg', dialog.restart))):
with self.subTest():
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
with self.subTest():
with self.subTest(restart=dialog.restart, cli_args=cli_args):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

dialog.cli_args = cli_args
self.assertEqual(dialog.entry_ok(), result)


# GUI TESTS

class QueryGuiTest(unittest.TestCase):
Expand Down Expand Up @@ -378,7 +399,7 @@ def test_click_help_source(self):
del root


class CommandLineArgsGuiTest(unittest.TestCase):
class CustomRunGuiTest(unittest.TestCase):

@classmethod
def setUpClass(cls):
Expand All @@ -387,11 +408,10 @@ def setUpClass(cls):
def test_click_args(self):
root = Tk()
root.withdraw()
dialog = query.CommandLineArgs(root, 'Title', 'message', _utest=True)
Equal = self.assertEqual
dialog = query.CustomRun(root, 'Title', _utest=True)
dialog.entry.insert(0, 'okay')
dialog.button_ok.invoke()
self.assertEqual(dialog.result, ['okay'])
self.assertEqual(dialog.result, (['okay'], True))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The should probably be a call to root.update() after the .invoke() call, to ensure that Tk has a chance to do all of its processing.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe invoke blocks until done. I have run this test at least 20 times without issue. event_generate is different (and flakey). There are 55 invokes in idle tests and my spot check of nearly 10 showed none followed by update. And I know of no resulting failures.

del dialog
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are this del call, and the one below, actually needed?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No. Gone. Added class attributes need deletion, but function locals already go on return, and there is no evident issue here with destroying first.

root.destroy()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest cleaning up using self.addCleanup() immediately after creating the Tk() instance, e.g.:

    root = Tk()
    def cleanup():
        root.update()
        root.destroy()
    self.addCleanup(cleanup)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I far prefer explicit calls. Update is not generally needed. update_idletasks is more often needed. I have not seen any of the tclerror messages that so indicate.

del root
Expand Down
2 changes: 1 addition & 1 deletion Lib/idlelib/mainmenu.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@
('Python Shell', '<<open-python-shell>>'),
('C_heck Module', '<<check-module>>'),
('R_un Module', '<<run-module>>'),
('Run Module with _Arguments', '<<run-module-arguments>>'),
('Run _Custom', '<<run-custom>>'),
]),

('shell', [
Expand Down
56 changes: 43 additions & 13 deletions Lib/idlelib/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@
import shlex
from sys import executable, platform # Platform is set for one test.

from tkinter import Toplevel, StringVar, W, E, S
from tkinter.ttk import Frame, Button, Entry, Label
from tkinter import Toplevel, StringVar, BooleanVar, W, E, S
from tkinter.ttk import Frame, Button, Entry, Label, Checkbutton
from tkinter import filedialog
from tkinter.font import Font

Expand Down Expand Up @@ -84,7 +84,7 @@ def __init__(self, parent, title, message, *, text0='', used_names={},
self.deiconify() # Unhide now that geometry set.
self.wait_window()

def create_widgets(self): # Call from override, if any.
def create_widgets(self, ok_text='OK'): # Call from override, if any.
Comment thread
csabella marked this conversation as resolved.
# Bind to self widgets needed for entry_ok or unittest.
self.frame = frame = Frame(self, padding=10)
frame.grid(column=0, row=0, sticky='news')
Expand All @@ -100,7 +100,7 @@ def create_widgets(self): # Call from override, if any.
self.entry_error = Label(frame, text=' ', foreground='red',
font=self.error_font)
self.button_ok = Button(
frame, text='OK', default='active', command=self.ok)
frame, text=ok_text, default='active', command=self.ok)
self.button_cancel = Button(
frame, text='Cancel', command=self.cancel)

Expand Down Expand Up @@ -303,16 +303,36 @@ def entry_ok(self):
path = self.path_ok()
return None if name is None or path is None else (name, path)

class CommandLineArgs(Query):
"Get a list of parsed command line arguments."
# Used in runscript.run_module_arguments_event
class CustomRun(Query):
"Get arguments for custom run module."
# Used in runscript.run_custom_event

def __init__(self, parent, title, message, *, _htest=False, _utest=False):
super().__init__(parent, title, message, _htest=_htest, _utest=_utest)
def __init__(self, parent, title, *, cli_args='',
_htest=False, _utest=False):
"""Get command line args for custom run environment.

def entry_ok(self):
"Return list of parsed arguments or None."
self.entry_error['text'] = ''
User enters the command line arugments for running the file.
"""
message = 'Command Line Arguments:'
super().__init__(
parent, title, message, text0=cli_args,
_htest=_htest, _utest=_utest)

def create_widgets(self):
super().create_widgets(ok_text='Run')
frame = self.frame
self.restartvar = BooleanVar(self, value=True)
restart = Checkbutton(frame, variable=self.restartvar, onvalue=True,
offvalue=False, text='Restart shell')
self.args_error = Label(frame, text=' ', foreground='red',
font=self.error_font)

restart.grid(column=0, row=4, columnspan=3, padx=5, sticky='w')
self.args_error.grid(column=0, row=12, columnspan=3, padx=5,
sticky='we')
Comment thread
terryjreedy marked this conversation as resolved.

def cli_args_ok(self):
"Validity check and parsing for command line arguments."
cli_args = self.entry.get().strip()
if not cli_args:
self.showerror('no arguments specified.')
Expand All @@ -324,10 +344,20 @@ def entry_ok(self):
return None
return lex

def restart_ok(self):
return self.restartvar.get()
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could add edits here for when restart cannot be False.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No longer possible.


def entry_ok(self):
"Return apparently valid (lex, restart) or None"
self.entry_error['text'] = ''
lex = self.cli_args_ok()
restart = self.restart_ok()
return None if not lex else (lex, restart)


if __name__ == '__main__':
from unittest import main
main('idlelib.idle_test.test_query', verbosity=2, exit=False)

from idlelib.idle_test.htest import run
run(Query, HelpSource, CommandLineArgs)
run(Query, HelpSource, CustomRun)
24 changes: 12 additions & 12 deletions Lib/idlelib/runscript.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
from idlelib.config import idleConf
from idlelib import macosx
from idlelib import pyshell
from idlelib.query import CommandLineArgs
from idlelib.query import CustomRun

indent_message = """Error: Inconsistent indentation detected!

Expand Down Expand Up @@ -114,16 +114,14 @@ def run_module_event(self, event):
else:
return self._run_module_event(event)

def run_module_arguments_event(self, event):
cli_args = CommandLineArgs(
event.widget, "Run with Arguments",
"Command line argunments:").result
def run_custom_event(self, event):
run_args = CustomRun(event.widget, "Customize Run").result
# User cancelled.
if not cli_args:
if not run_args:
return 'break'
return self._run_module_event(event, cli_args)
return self._run_module_event(event, cli_args=run_args[0], restart=run_args[1])
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed to pass run_args and unpack in _run_module.


def _run_module_event(self, event, cli_args=None):
def _run_module_event(self, event, *, cli_args=None, restart=True):
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The "Run Module" and "Run Custom" should each have their own (simple) event handler, with the common code extracted into a common function used by both, which isn't an event handler.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

run_module_event, with initial '', is already the common code for run_module_event and run_custom_event. It is only an 'event handler' to implement the Mac-specific delayed call via an internal pseudo pseudoevent. I am assuming that someone tried calling it directly after the delay. As a separate issue, we might test whether the workaround is needed in 8.6.

"""Run the module after setting up the environment.

First check the syntax. If OK, make sure the shell is active and
Expand All @@ -140,21 +138,23 @@ def _run_module_event(self, event, cli_args=None):
if not self.tabnanny(filename):
return 'break'
interp = self.shell.interp
if pyshell.use_subprocess:
if pyshell.use_subprocess and restart:
interp.restart_subprocess(with_cwd=False, filename=
self.editwin._filename_to_unicode(filename))
dirname = os.path.dirname(filename)
# XXX Too often this discards arguments the user just set...
argv = [filename]
if cli_args:
argv += cli_args
# XXX Too often this discards arguments the user just set...
interp.runcommand(f"""if 1:
__file__ = {filename!r}
import sys as _sys
from os.path import basename as _basename
argv = {argv!r}
if (not _sys.argv or
_basename(_sys.argv[0]) != _basename(__file__)):
_sys.argv = {argv!r}
_basename(_sys.argv[0]) != _basename(__file__) or
len(argv) > 1):
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checking the length so that, if there are additional args, they will replace what was already there. However, existing args won't be replaced by no args. Currently, F5 still runs with no args and restarts the shell, but that's something to keep in mind if F5 changes.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe that this can only matter if one used the deprecated -n option. I have no intention of changing F5

_sys.argv = argv
import os as _os
_os.chdir({dirname!r})
del _sys, _basename, _os
Expand Down