bpo-5680: IDLE: Customize running a module.#13763
bpo-5680: IDLE: Customize running a module.#13763terryjreedy merged 19 commits intopython:masterfrom
Conversation
Lib/idlelib/query.py
Outdated
| cli_args = self.entry.get().strip() | ||
| if not cli_args: | ||
| self.showerror('no arguments specified.') | ||
| return None |
There was a problem hiding this comment.
Not sure about this edit, but it makes everything a little nicer right now. The reason I'm not sure about it is due to the behavior of the current Run Module. Should Run Module use the last settings from Custom Run or should it work exactly as it works now (while in a subprocess)? Run Module without a subprocess uses the Custom Run settings from the last run.
There was a problem hiding this comment.
IMO "Run Module" should always ignore the latest "Custom Run" settings, regardless of whether or not a subprocess is used.
There was a problem hiding this comment.
I believe Run Module is currently unchanged and agree that this should continue.
Lib/idlelib/query.py
Outdated
| return lex | ||
|
|
||
| def restart_ok(self): | ||
| return self.restartvar.get() |
There was a problem hiding this comment.
Could add edits here for when restart cannot be False.
| _basename(_sys.argv[0]) != _basename(__file__)): | ||
| _sys.argv = [__file__] | ||
| _basename(_sys.argv[0]) != _basename(__file__) or | ||
| len(argv) > 1): |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
I believe that this can only matter if one used the deprecated -n option. I have no intention of changing F5
terryjreedy
left a comment
There was a problem hiding this comment.
Test of behavior without looking much at code yet. Will respond to other comments later. Immediate issue: I have a custom keyset without new pseudoevent binding and on startup see
Warning: config.py - IdleConf.GetCoreKeys -
problem retrieving key binding for event '<>'
from key set 'test'.
returning default value: ['']
The fix is simple and I know how to do it if you don't beat me.
Separate issue if not done yet: write back keyset with new bindings.
Otherwise, works great.
|
When you're done making the requested changes, leave the comment: |
|
|
I have made the requested changes; please review again. @terryjreedy, I added this pseudoevent to the |
|
Thanks for making the requested changes! @terryjreedy: please review the changes made to this pull request. |
|
Thanks for looking at this, @taleinat!
Terry wants to have more things on that dialog to be able to change the run environment, so using 'arguments' on the menu isn't inclusive enough. If 'Run Custom' is awkward, maybe 'Customize Run'?
Yes, there was a few things that I listed on the bpo issue that I didn't do in this first round, including saving the values. Terry requested creating a minimal version to start with. Saving the items wouldn't (shouldn't?) complicate the code, but I wanted to try to keep it simple and then iterate over it in the next round. He had also mentioned printing the run options in the shell window so that multiple runs would be annotated. Both features would be helpful. |
I see. Perhaps just "Run..."? |
taleinat
left a comment
There was a problem hiding this comment.
As a general note, I'm not sure about the usefulness of the non-GUI tests for the RunCustom dialog (CustomRunCLIargsokTest, CustomRunEntryokTest) . They rely rather heavily on the internal implementation. Since we have GUI tests run by the CI now, I prefer to have simpler tests actually using Tk with less mocking. Would it be possible to simply move the tests from these classes into CustomRunGuiTest?
Lib/idlelib/idle_test/test_query.py
Outdated
| def test_blank_args(self): | ||
| dialog = self.Dummy_CustomRun(' ') | ||
| self.assertEqual(dialog.cli_args_ok(), None) | ||
| self.assertIn('no arguments', dialog.entry_error['text']) |
There was a problem hiding this comment.
This should use assertRegex to do a case-insensitive check.
| self.assertIn('no arguments', dialog.entry_error['text']) | |
| self.assertRegex(r'(?i)no arguments', dialog.entry_error['text']) |
Lib/idlelib/idle_test/test_query.py
Outdated
|
|
||
| class Dummy_CustomRun: | ||
| cli_args_ok = query.CustomRun.cli_args_ok # Function being tested. | ||
| entry = Var() |
There was a problem hiding this comment.
Why is this a class attribute? Besides being unclear, this also doesn't reflect the actual CustomRun class.
There was a problem hiding this comment.
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.
Lib/idlelib/idle_test/test_query.py
Outdated
| class Dummy_CustomRun: | ||
| cli_args_ok = query.CustomRun.cli_args_ok # Function being tested. | ||
| entry = Var() | ||
| entry_error = {} |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
Lib/idlelib/idle_test/test_query.py
Outdated
| for dialog.restart in {True, False}: | ||
| for cli_args, result in ((None, None), | ||
| ('my arg', ('my arg', dialog.restart))): | ||
| with self.subTest(): |
There was a problem hiding this comment.
| with self.subTest(): | |
| with self.subTest(restart=dialog.restart, cli_args=cli_args): |
| @@ -0,0 +1 @@ | |||
| Run modules from the editor using command line arguments. No newline at end of file | |||
There was a problem hiding this comment.
This needs to be a bit more explicit, mentioning that a new menu option is added.
| dialog = query.CustomRun(root, 'Title', _utest=True) | ||
| dialog.entry.insert(0, 'okay') | ||
| dialog.button_ok.invoke() | ||
| self.assertEqual(dialog.result, (['okay'], True)) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
| dialog.button_ok.invoke() | ||
| self.assertEqual(dialog.result, (['okay'], True)) | ||
| del dialog | ||
| root.destroy() |
There was a problem hiding this comment.
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)There was a problem hiding this comment.
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.
Lib/idlelib/idle_test/test_query.py
Outdated
| dialog.entry.insert(0, 'okay') | ||
| dialog.button_ok.invoke() | ||
| self.assertEqual(dialog.result, (['okay'], True)) | ||
| del dialog |
There was a problem hiding this comment.
Are this del call, and the one below, actually needed?
There was a problem hiding this comment.
No. Gone. Added class attributes need deletion, but function locals already go on return, and there is no evident issue here with destroying first.
Lib/idlelib/query.py
Outdated
| self.showerror('no arguments specified.') | ||
| return None | ||
| try: | ||
| lex = shlex.split(cli_args, posix=True) |
There was a problem hiding this comment.
What does lex stand for? For me, this is confusing both here and in entry_ok(). Perhaps args_list?
There was a problem hiding this comment.
Agreed. I switched to cli_string and cli_args. cli_args_ok should return cli_args.
|
I am reviewing and editing with the intention of getting this into 3.7.4. While the user UI can be changed after that, it is my primary focus. Summary responses to comments above.
|
Revise corresponding doc entry.
Lib/idlelib/query.py
Outdated
| cli_args = self.entry.get().strip() | ||
| if not cli_args: | ||
| self.showerror('no arguments specified.') | ||
| return None |
There was a problem hiding this comment.
I believe Run Module is currently unchanged and agree that this should continue.
Lib/idlelib/runscript.py
Outdated
| # User cancelled. | ||
| if not run_args: | ||
| return 'break' | ||
| return self._run_module_event(event, cli_args=run_args[0], restart=run_args[1]) |
There was a problem hiding this comment.
Changed to pass run_args and unpack in _run_module.
Lib/idlelib/query.py
Outdated
| self.showerror('no arguments specified.') | ||
| return None | ||
| try: | ||
| lex = shlex.split(cli_args, posix=True) |
There was a problem hiding this comment.
Agreed. I switched to cli_string and cli_args. cli_args_ok should return cli_args.
Lib/idlelib/query.py
Outdated
| return lex | ||
|
|
||
| def restart_ok(self): | ||
| return self.restartvar.get() |
Lib/idlelib/idle_test/test_query.py
Outdated
| for dialog.restart in {True, False}: | ||
| for cli_args, result in ((None, None), | ||
| ('my arg', ('my arg', dialog.restart))): | ||
| with self.subTest(): |
| dialog = query.CustomRun(root, 'Title', _utest=True) | ||
| dialog.entry.insert(0, 'okay') | ||
| dialog.button_ok.invoke() | ||
| self.assertEqual(dialog.result, (['okay'], True)) |
There was a problem hiding this comment.
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.
Lib/idlelib/idle_test/test_query.py
Outdated
| dialog.entry.insert(0, 'okay') | ||
| dialog.button_ok.invoke() | ||
| self.assertEqual(dialog.result, (['okay'], True)) | ||
| del dialog |
There was a problem hiding this comment.
No. Gone. Added class attributes need deletion, but function locals already go on return, and there is no evident issue here with destroying first.
| dialog.button_ok.invoke() | ||
| self.assertEqual(dialog.result, (['okay'], True)) | ||
| del dialog | ||
| root.destroy() |
There was a problem hiding this comment.
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.
|
buglet to fix: focus is not being shifted to shell correctly. |
Don't ask for config if still may toss. Want focus to go to shell unless cancel.
|
Changes: 1) Check code before bother to customize. 2) Add module title to title bar. 3) Make shell.text the parent, so focus goes to shell. Good enough to merge. See issue for 2 remaining issues. |
|
Thanks @csabella for the PR, and @terryjreedy for merging it 🌮🎉.. I'm working now to backport this PR to: 3.7. |
|
Thanks @csabella for the PR, and @terryjreedy for merging it 🌮🎉.. I'm working now to backport this PR to: 3.8. |
The initialize options are 1) add command line options, which are appended to sys.argv as if passed on a real command line, and 2) skip the shell restart. The customization dialog is accessed by a new entry on the Run menu. (cherry picked from commit 201bc2d) Co-authored-by: Cheryl Sabella <[email protected]>
|
GH-14184 is a backport of this pull request to the 3.7 branch. |
|
GH-14185 is a backport of this pull request to the 3.8 branch. |
The initialize options are 1) add command line options, which are appended to sys.argv as if passed on a real command line, and 2) skip the shell restart. The customization dialog is accessed by a new entry on the Run menu. (cherry picked from commit 201bc2d) Co-authored-by: Cheryl Sabella <[email protected]>
The initialize options are 1) add command line options, which are appended to sys.argv as if passed on a real command line, and 2) skip the shell restart. The customization dialog is accessed by a new entry on the Run menu. (cherry picked from commit 201bc2d) Co-authored-by: Cheryl Sabella <[email protected]>
|
@taleinat, thank you for the review and @terryjreedy, thank you for the review and making the additional changes. |
The initialize options are 1) add command line options, which are appended to sys.argv as if passed on a real command line, and 2) skip the shell restart. The customization dialog is accessed by a new entry on the Run menu.
The initialize options are 1) add command line options, which are appended to sys.argv as if passed on a real command line, and 2) skip the shell restart. The customization dialog is accessed by a new entry on the Run menu.
The initialize options are 1) add command line options, which are appended to sys.argv as if passed on a real command line, and 2) skip the shell restart. The customization dialog is accessed by a new item on the Run menu.
https://bugs.python.org/issue5680