diff --git a/Doc/library/idle.rst b/Doc/library/idle.rst index a59a5d3a465703..99dae78ead84f9 100644 --- a/Doc/library/idle.rst +++ b/Doc/library/idle.rst @@ -663,6 +663,23 @@ If there are arguments: * Otherwise, arguments are files opened for editing and ``sys.argv`` reflects the arguments passed to IDLE itself. + +Optional Startup Code Execution +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +In addition to the ``-``, ``-s``, ``-c``, and ``-r`` command line options for +executing Python code upon startup, code can be entered using the IDLE +Configuration Dialog under the ``Startup`` tab. The code entered here will +be executed if the appropriate check box is selected for running on +startup and if command line options ``-``, ``-c``, and ``-r`` were not used. +Those options take precedence and preclude the configuration code from running. +However, the configuration-level code will be executed before the ``-s`` +option file is run. + +Note that the configuration code isn't checked for errors. If it can't be +executed, then the shell may show errors upon starting. + + Startup failure ^^^^^^^^^^^^^^^ diff --git a/Lib/idlelib/config-main.def b/Lib/idlelib/config-main.def index 28ae94161d5c03..4a044680dd96fc 100644 --- a/Lib/idlelib/config-main.def +++ b/Lib/idlelib/config-main.def @@ -67,9 +67,14 @@ font-size= 10 font-bold= 0 encoding= none line-numbers-default= 0 +editor-template-code= -[PyShell] +[ShellWindow] +line-numbers-default= 0 auto-squeeze-min-lines= 50 +startup-code-on= False +restart-code-on= False +shell-startup-code= [Indent] use-spaces= 1 diff --git a/Lib/idlelib/configdialog.py b/Lib/idlelib/configdialog.py index c52a04b503adb4..376a5eca1a89b5 100644 --- a/Lib/idlelib/configdialog.py +++ b/Lib/idlelib/configdialog.py @@ -116,11 +116,13 @@ def create_widgets(self): self.fontpage = FontPage(note, self.highpage) self.keyspage = KeysPage(note) self.genpage = GenPage(note) + self.startpage = StartPage(note) self.extpage = self.create_page_extensions() note.add(self.fontpage, text='Fonts/Tabs') note.add(self.highpage, text='Highlights') note.add(self.keyspage, text=' Keys ') note.add(self.genpage, text=' General ') + note.add(self.startpage, text=' Startup ') note.add(self.extpage, text='Extensions') note.enable_traversal() note.pack(side=TOP, expand=TRUE, fill=BOTH) @@ -2196,6 +2198,176 @@ def update_help_changes(self): ';'.join(self.user_helplist[num-1][:2])) +class StartPage(Frame): + + def __init__(self, master): + super().__init__(master) + self.create_page_startup() + self.load_startup_cfg() + + def create_page_startup(self): + """Return frame of widgets for Startup tab. + + Enable users to provisionally change startup options. Function + load_startup_cfg intializes tk variables using idleConf. + Radiobuttons startup_shell_on and startup_editor_on + set var startup_edit. Entry boxes win_width_int and win_height_int + set var win_width and win_height. Setting var_name invokes the + default callback that adds option to changes. + + Widgets for StartupPage(Frame): (*) widgets bound to self + frame_window: LabelFrame + frame_run: Frame + startup_title: Label + (*)startup_editor_on: Radiobutton - startup_edit + (*)startup_shell_on: Radiobutton - startup_edit + frame_win_size: Frame + win_size_title: Label + win_width_title: Label + (*)win_width_int: Entry - win_width + win_height_title: Label + (*)win_height_int: Entry - win_height + frame_code: Frame + frame_code_shell: Frame + (*)shell_startup_toggle: Checkbutton - startup_code_on + (*)shell_restart_toggle: Checkbutton - restart_code_on + (*)shell_startup_text: Text + frame_code_editor: Frame + (*)editor_template_text: Text + """ + # Integer values need StringVar because int('') raises. + self.startup_edit = tracers.add( + IntVar(self), ('main', 'General', 'editor-on-startup')) + self.win_width = tracers.add( + StringVar(self), ('main', 'EditorWindow', 'width')) + self.win_height = tracers.add( + StringVar(self), ('main', 'EditorWindow', 'height')) + self.startup_code_on = tracers.add( + BooleanVar(self), ('main', 'ShellWindow', 'startup-code-on')) + self.restart_code_on = tracers.add( + BooleanVar(self), ('main', 'ShellWindow', 'restart-code-on')) + + # Create widgets: + # Section frames. + frame_window = LabelFrame(self, borderwidth=2, relief=GROOVE, + text=' Window Preferences') + frame_shell = LabelFrame(self, borderwidth=2, relief=GROOVE, + text=' Shell Startup Code ') + frame_editor = LabelFrame(self, borderwidth=2, relief=GROOVE, + text=' Editor Template Code ') + + # Frame_window. + frame_run = Frame(frame_window, borderwidth=0) + startup_title = Label(frame_run, text='At Startup') + self.startup_editor_on = Radiobutton( + frame_run, variable=self.startup_edit, value=1, + text="Open Edit Window") + self.startup_shell_on = Radiobutton( + frame_run, variable=self.startup_edit, value=0, + text='Open Shell Window') + + frame_win_size = Frame(frame_window, borderwidth=0) + win_size_title = Label( + frame_win_size, text='Initial Window Size (in characters)') + win_width_title = Label(frame_win_size, text='Width') + self.win_width_int = Entry( + frame_win_size, textvariable=self.win_width, width=3) + win_height_title = Label(frame_win_size, text='Height') + self.win_height_int = Entry( + frame_win_size, textvariable=self.win_height, width=3) + + # Frame_shell. + frame_shell_toggle = Frame(frame_shell) + self.shell_startup_toggle = Checkbutton(frame_shell_toggle, + variable=self.startup_code_on, + onvalue=True, offvalue=False, + text='Run code on startup') + self.shell_restart_toggle = Checkbutton(frame_shell_toggle, + variable=self.restart_code_on, + onvalue=True, offvalue=False, + text='Run code on restart') + self.shell_startup_text = Text(frame_shell, width=20, height=10) + scroll_shell = Scrollbar(frame_shell) + scroll_shell['command'] = self.shell_startup_text.yview + self.shell_startup_text['yscrollcommand'] = scroll_shell.set + + # Frame_editor. + self.editor_template_text = Text(frame_editor, width=20, height=10) + scroll_editor = Scrollbar(frame_editor) + scroll_editor['command'] = self.editor_template_text.yview + self.editor_template_text['yscrollcommand'] = scroll_editor.set + + # Pack widgets: + # Body. + frame_window.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH) + # frame_run. + frame_run.pack(side=TOP, padx=5, pady=0, fill=X) + startup_title.pack(side=LEFT, anchor=W, padx=5, pady=5) + self.startup_shell_on.pack(side=RIGHT, anchor=W, padx=5, pady=5) + self.startup_editor_on.pack(side=RIGHT, anchor=W, padx=5, pady=5) + # frame_win_size. + frame_win_size.pack(side=TOP, padx=5, pady=0, fill=X) + win_size_title.pack(side=LEFT, anchor=W, padx=5, pady=5) + self.win_height_int.pack(side=RIGHT, anchor=E, padx=10, pady=5) + win_height_title.pack(side=RIGHT, anchor=E, pady=5) + self.win_width_int.pack(side=RIGHT, anchor=E, padx=10, pady=5) + win_width_title.pack(side=RIGHT, anchor=E, pady=5) + # Frame_shell. + frame_shell.pack(side='top', padx=5, pady=5, expand=True, fill='both') + frame_shell_toggle.pack(side='top') + self.shell_startup_toggle.pack(side='left', padx=2, pady=2) + self.shell_restart_toggle.pack(side='right', padx=2, pady=2) + scroll_shell.pack(side='right', anchor='w', fill='y') + self.shell_startup_text.pack(fill='x') + # Frame_editor. + frame_editor.pack(side='top', padx=5, pady=0, fill='x') + scroll_editor.pack(side='right', anchor='w', fill='y') + self.editor_template_text.pack(fill='x') + + def load_startup_cfg(self): + "Load current configuration settings for the startup options." + # Set variables for all windows. + self.startup_edit.set(idleConf.GetOption( + 'main', 'General', 'editor-on-startup', type='bool')) + self.win_width.set(idleConf.GetOption( + 'main', 'EditorWindow', 'width', type='int')) + self.win_height.set(idleConf.GetOption( + 'main', 'EditorWindow', 'height', type='int')) + + self.startup_code_on.set(idleConf.GetOption( + 'main', 'ShellWindow', 'startup-code-on', type='bool')) + self.restart_code_on.set(idleConf.GetOption( + 'main', 'ShellWindow', 'restart-code-on', type='bool')) + shell_code = idleConf.GetOption( + 'main', 'ShellWindow', 'shell-startup-code') + self.shell_startup_text.insert('end', shell_code or '') + self.shell_startup_text.edit_modified(False) + # Text widgets don't have trace methods, but instead set a + # modified flag on inserts or deletes. + self.shell_startup_text.bind('<>', + self.var_changed_shell_text) + + editor_template = idleConf.GetOption( + 'main', 'EditorWindow', 'editor-template-code') + self.editor_template_text.insert('end', editor_template or '') + self.editor_template_text.edit_modified(False) + self.editor_template_text.bind('<>', + self.var_changed_editor_template) + + def var_changed_shell_text(self, *params): + "Store changes to shell startup text." + value = self.shell_startup_text.get('1.0', 'end-1c') + changes.add_option('main', 'ShellWindow', 'shell-startup-code', value) + self.shell_startup_text.edit_modified(False) + + def var_changed_editor_template(self, *params): + "Store changes to editor template text." + value = self.editor_template_text.get('1.0', 'end-1c') + changes.add_option('main', 'EditorWindow', + 'editor-template-code', value) + self.editor_template_text.edit_modified(False) + + class VarTrace: """Maintain Tk variables trace state.""" diff --git a/Lib/idlelib/editor.py b/Lib/idlelib/editor.py index 66e9da5a9dccf9..09a6a6cd11652b 100644 --- a/Lib/idlelib/editor.py +++ b/Lib/idlelib/editor.py @@ -366,8 +366,15 @@ def set_width(self): self.width = pixel_width // zero_char_width def new_callback(self, event): + """Create a new editor window. + + A new editor is created with default template text. + """ dirname, basename = self.io.defaultfilename() - self.flist.new(dirname) + editor = self.flist.new(dirname) + template = idleConf.GetOption('main', 'EditorWindow', + 'editor-template-code') + editor.text.insert('end', template or '') return "break" def home_callback(self, event): diff --git a/Lib/idlelib/idle_test/test_config.py b/Lib/idlelib/idle_test/test_config.py index 697fda527968de..3d1dad02cb1021 100644 --- a/Lib/idlelib/idle_test/test_config.py +++ b/Lib/idlelib/idle_test/test_config.py @@ -342,11 +342,11 @@ def test_get_section_list(self): self.assertCountEqual( conf.GetSectionList('default', 'main'), - ['General', 'EditorWindow', 'PyShell', 'Indent', 'Theme', + ['General', 'EditorWindow', 'ShellWindow', 'Indent', 'Theme', 'Keys', 'History', 'HelpFiles']) self.assertCountEqual( conf.GetSectionList('user', 'main'), - ['General', 'EditorWindow', 'PyShell', 'Indent', 'Theme', + ['General', 'EditorWindow', 'ShellWindow', 'Indent', 'Theme', 'Keys', 'History', 'HelpFiles']) with self.assertRaises(config.InvalidConfigSet): diff --git a/Lib/idlelib/idle_test/test_configdialog.py b/Lib/idlelib/idle_test/test_configdialog.py index 98ddc67afdcc08..a090ac08a2dfcc 100644 --- a/Lib/idlelib/idle_test/test_configdialog.py +++ b/Lib/idlelib/idle_test/test_configdialog.py @@ -2,6 +2,7 @@ Half the class creates dialog, half works with user customizations. """ +import os from idlelib import configdialog from test.support import requires requires('gui') @@ -1232,18 +1233,12 @@ def test_load_general_cfg(self): # Set to wrong values, load, check right values. eq = self.assertEqual d = self.page - d.startup_edit.set(1) d.autosave.set(1) - d.win_width.set(1) - d.win_height.set(1) d.helplist.insert('end', 'bad') d.user_helplist = ['bad', 'worse'] idleConf.SetOption('main', 'HelpFiles', '1', 'name;file') d.load_general_cfg() - eq(d.startup_edit.get(), 0) eq(d.autosave.get(), 0) - eq(d.win_width.get(), '80') - eq(d.win_height.get(), '40') eq(d.helplist.get(0, 'end'), ('name',)) eq(d.user_helplist, [('name', 'file', '1')]) @@ -1441,6 +1436,110 @@ def test_update_help_changes(self): d.update_help_changes = Func() +class StartPageTest(unittest.TestCase): + """Test that startup tab widgets enable users to make changes. + + Test that widget actions set vars and that var changes add + options. + """ + @classmethod + def setUpClass(cls): + page = cls.page = dialog.startpage + dialog.note.select(page) + + @classmethod + def tearDownClass(cls): + page = cls.page + + def setUp(self): + changes.clear() + + def test_load_startup_cfg(self): + # Set to wrong values, load, check right values. + eq = self.assertEqual + d = self.page + d.startup_edit.set(1) + d.win_width.set(1) + d.win_height.set(1) + d.startup_code_on.set(1) + d.restart_code_on.set(1) + d.shell_startup_text.insert('end', 'shell spam') + d.editor_template_text.insert('end', 'editor spam') + d.load_startup_cfg() + eq(d.startup_edit.get(), 0) + eq(d.win_width.get(), '80') + eq(d.win_height.get(), '40') + eq(d.startup_code_on.get(), False) + eq(d.restart_code_on.get(), False) + self.assertNotIn('spam', d.shell_startup_text.get('1.0')) + self.assertNotIn('spam', d.editor_template_text.get('1.0')) + + def test_startup(self): + d = self.page + d.startup_editor_on.invoke() + self.assertEqual(mainpage, + {'General': {'editor-on-startup': '1'}}) + changes.clear() + d.startup_shell_on.invoke() + self.assertEqual(mainpage, + {'General': {'editor-on-startup': '0'}}) + + def test_editor_size(self): + d = self.page + d.win_height_int.delete(0, 'end') + d.win_height_int.insert(0, '11') + self.assertEqual(mainpage, {'EditorWindow': {'height': '11'}}) + changes.clear() + d.win_width_int.delete(0, 'end') + d.win_width_int.insert(0, '11') + self.assertEqual(mainpage, {'EditorWindow': {'width': '11'}}) + + def test_startup_code_on(self): + d = self.page + d.shell_startup_toggle.invoke() + self.assertEqual(mainpage, + {'ShellWindow': {'startup-code-on': 'True'}}) + changes.clear() + d.shell_startup_toggle.invoke() + self.assertEqual(mainpage, + {'ShellWindow': {'startup-code-on': 'False'}}) + + def test_restart_code_on(self): + d = self.page + d.shell_restart_toggle.invoke() + self.assertEqual(mainpage, + {'ShellWindow': {'restart-code-on': 'True'}}) + changes.clear() + d.shell_restart_toggle.invoke() + self.assertEqual(mainpage, + {'ShellWindow': {'restart-code-on': 'False'}}) + + def test_shell_startup_text(self): + d = self.page + d.shell_startup_text.delete('1.0', 'end') + text = f'import os{os.linesep}import re' + d.shell_startup_text.insert('end', text) + self.assertEqual(mainpage, + {'ShellWindow': {'shell-startup-code': text}}) + changes.clear() + d.shell_startup_text.delete('1.0', 'end') + self.assertEqual(mainpage, + {'ShellWindow': {'shell-startup-code': ''}}) + + def test_editor_template_text(self): + d = self.page + d.editor_template_text.delete('1.0', 'end') + text = f'# This is a comment.{os.linesep}# Second line' + d.editor_template_text.insert('end', text) + self.assertEqual( + mainpage, + {'EditorWindow': {'editor-template-code': text}}) + changes.clear() + d.editor_template_text.delete('1.0', 'end') + self.assertEqual(mainpage, + {'EditorWindow': {'editor-template-code': ''}}) + + class VarTraceTest(unittest.TestCase): @classmethod diff --git a/Lib/idlelib/pyshell.py b/Lib/idlelib/pyshell.py index 0407ca9cfd8bfd..a4bbf460695a4c 100755 --- a/Lib/idlelib/pyshell.py +++ b/Lib/idlelib/pyshell.py @@ -518,6 +518,11 @@ def restart_subprocess(self, with_cwd=False, filename=''): # reload remote debugger breakpoints for all PyShellEditWindows debug.load_breakpoints() self.compile.compiler.flags = self.original_compiler_flags + if idleConf.GetOption('main', 'ShellWindow', + 'restart-code-on', type='bool'): + config_startup = idleConf.GetOption( + 'main', 'ShellWindow', 'shell-startup-code') + self.execsource(config_startup) self.restarting = False return self.rpcclt @@ -1403,6 +1408,7 @@ def main(): debug = False cmd = None script = None + config_startup = None startup = False try: opts, args = getopt.getopt(sys.argv[1:], "c:deihnr:st:") @@ -1466,11 +1472,16 @@ def main(): dir = os.getcwd() if dir not in sys.path: sys.path.insert(0, dir) - # check the IDLE settings configuration (but command line overrides) + # Check the IDLE settings configuration (but command line overrides). edit_start = idleConf.GetOption('main', 'General', 'editor-on-startup', type='bool') enable_edit = enable_edit or edit_start enable_shell = enable_shell or not enable_edit + if idleConf.GetOption('main', 'ShellWindow', + 'startup-code-on', type='bool'): + config_startup = idleConf.GetOption( + 'main', 'ShellWindow', 'shell-startup-code') + sys.argv = ['-c'] + [config_startup] # Setup root. Don't break user code run in IDLE process. # Don't change environment when testing. @@ -1536,7 +1547,7 @@ def main(): os.environ.get("PYTHONSTARTUP") if filename and os.path.isfile(filename): shell.interp.execfile(filename) - if cmd or script: + if cmd or script or config_startup: shell.interp.runcommand("""if 1: import sys as _sys _sys.argv = %r @@ -1547,6 +1558,8 @@ def main(): elif script: shell.interp.prepend_syspath(script) shell.interp.execfile(script) + elif config_startup: + shell.interp.execsource(config_startup) elif shell: # If there is a shell window and no cmd or script in progress, # check for problematic issues and print warning message(s) in diff --git a/Misc/NEWS.d/next/IDLE/2018-02-04-19-45-46.bpo-5594.Ydp0aG.rst b/Misc/NEWS.d/next/IDLE/2018-02-04-19-45-46.bpo-5594.Ydp0aG.rst new file mode 100644 index 00000000000000..981f33cbb394ed --- /dev/null +++ b/Misc/NEWS.d/next/IDLE/2018-02-04-19-45-46.bpo-5594.Ydp0aG.rst @@ -0,0 +1,4 @@ +Add new Startup tab to configuration and move the current startup-related +items from the General tab to this tab. On the Startup tab, add a new +widget for code that should be run at shell startup/restart and a second new +widget for template code to copy to new, blank editors.