(root)/
Python-3.12.0/
Lib/
idlelib/
query.py
       1  """
       2  Dialogs that query users and verify the answer before accepting.
       3  
       4  Query is the generic base class for a popup dialog.
       5  The user must either enter a valid answer or close the dialog.
       6  Entries are validated when <Return> is entered or [Ok] is clicked.
       7  Entries are ignored when [Cancel] or [X] are clicked.
       8  The 'return value' is .result set to either a valid answer or None.
       9  
      10  Subclass SectionName gets a name for a new config file section.
      11  Configdialog uses it for new highlight theme and keybinding set names.
      12  Subclass ModuleName gets a name for File => Open Module.
      13  Subclass HelpSource gets menu item and path for additions to Help menu.
      14  """
      15  # Query and Section name result from splitting GetCfgSectionNameDialog
      16  # of configSectionNameDialog.py (temporarily config_sec.py) into
      17  # generic and specific parts.  3.6 only, July 2016.
      18  # ModuleName.entry_ok came from editor.EditorWindow.load_module.
      19  # HelpSource was extracted from configHelpSourceEdit.py (temporarily
      20  # config_help.py), with darwin code moved from ok to path_ok.
      21  
      22  import importlib.util, importlib.abc
      23  import os
      24  import shlex
      25  from sys import executable, platform  # Platform is set for one test.
      26  
      27  from tkinter import Toplevel, StringVar, BooleanVar, W, E, S
      28  from tkinter.ttk import Frame, Button, Entry, Label, Checkbutton
      29  from tkinter import filedialog
      30  from tkinter.font import Font
      31  from tkinter.simpledialog import _setup_dialog
      32  
      33  class ESC[4;38;5;81mQuery(ESC[4;38;5;149mToplevel):
      34      """Base class for getting verified answer from a user.
      35  
      36      For this base class, accept any non-blank string.
      37      """
      38      def __init__(self, parent, title, message, *, text0='', used_names={},
      39                   _htest=False, _utest=False):
      40          """Create modal popup, return when destroyed.
      41  
      42          Additional subclass init must be done before this unless
      43          _utest=True is passed to suppress wait_window().
      44  
      45          title - string, title of popup dialog
      46          message - string, informational message to display
      47          text0 - initial value for entry
      48          used_names - names already in use
      49          _htest - bool, change box location when running htest
      50          _utest - bool, leave window hidden and not modal
      51          """
      52          self.parent = parent  # Needed for Font call.
      53          self.message = message
      54          self.text0 = text0
      55          self.used_names = used_names
      56  
      57          Toplevel.__init__(self, parent)
      58          self.withdraw()  # Hide while configuring, especially geometry.
      59          self.title(title)
      60          self.transient(parent)
      61          if not _utest:  # Otherwise fail when directly run unittest.
      62              self.grab_set()
      63  
      64          _setup_dialog(self)
      65          if self._windowingsystem == 'aqua':
      66              self.bind("<Command-.>", self.cancel)
      67          self.bind('<Key-Escape>', self.cancel)
      68          self.protocol("WM_DELETE_WINDOW", self.cancel)
      69          self.bind('<Key-Return>', self.ok)
      70          self.bind("<KP_Enter>", self.ok)
      71  
      72          self.create_widgets()
      73          self.update_idletasks()  # Need here for winfo_reqwidth below.
      74          self.geometry(  # Center dialog over parent (or below htest box).
      75                  "+%d+%d" % (
      76                      parent.winfo_rootx() +
      77                      (parent.winfo_width()/2 - self.winfo_reqwidth()/2),
      78                      parent.winfo_rooty() +
      79                      ((parent.winfo_height()/2 - self.winfo_reqheight()/2)
      80                      if not _htest else 150)
      81                  ) )
      82          self.resizable(height=False, width=False)
      83  
      84          if not _utest:
      85              self.deiconify()  # Unhide now that geometry set.
      86              self.entry.focus_set()
      87              self.wait_window()
      88  
      89      def create_widgets(self, ok_text='OK'):  # Do not replace.
      90          """Create entry (rows, extras, buttons.
      91  
      92          Entry stuff on rows 0-2, spanning cols 0-2.
      93          Buttons on row 99, cols 1, 2.
      94          """
      95          # Bind to self the widgets needed for entry_ok or unittest.
      96          self.frame = frame = Frame(self, padding=10)
      97          frame.grid(column=0, row=0, sticky='news')
      98          frame.grid_columnconfigure(0, weight=1)
      99  
     100          entrylabel = Label(frame, anchor='w', justify='left',
     101                             text=self.message)
     102          self.entryvar = StringVar(self, self.text0)
     103          self.entry = Entry(frame, width=30, textvariable=self.entryvar)
     104          self.error_font = Font(name='TkCaptionFont',
     105                                 exists=True, root=self.parent)
     106          self.entry_error = Label(frame, text=' ', foreground='red',
     107                                   font=self.error_font)
     108          # Display or blank error by setting ['text'] =.
     109          entrylabel.grid(column=0, row=0, columnspan=3, padx=5, sticky=W)
     110          self.entry.grid(column=0, row=1, columnspan=3, padx=5, sticky=W+E,
     111                          pady=[10,0])
     112          self.entry_error.grid(column=0, row=2, columnspan=3, padx=5,
     113                                sticky=W+E)
     114  
     115          self.create_extra()
     116  
     117          self.button_ok = Button(
     118                  frame, text=ok_text, default='active', command=self.ok)
     119          self.button_cancel = Button(
     120                  frame, text='Cancel', command=self.cancel)
     121  
     122          self.button_ok.grid(column=1, row=99, padx=5)
     123          self.button_cancel.grid(column=2, row=99, padx=5)
     124  
     125      def create_extra(self): pass  # Override to add widgets.
     126  
     127      def showerror(self, message, widget=None):
     128          #self.bell(displayof=self)
     129          (widget or self.entry_error)['text'] = 'ERROR: ' + message
     130  
     131      def entry_ok(self):  # Example: usually replace.
     132          "Return non-blank entry or None."
     133          entry = self.entry.get().strip()
     134          if not entry:
     135              self.showerror('blank line.')
     136              return None
     137          return entry
     138  
     139      def ok(self, event=None):  # Do not replace.
     140          '''If entry is valid, bind it to 'result' and destroy tk widget.
     141  
     142          Otherwise leave dialog open for user to correct entry or cancel.
     143          '''
     144          self.entry_error['text'] = ''
     145          entry = self.entry_ok()
     146          if entry is not None:
     147              self.result = entry
     148              self.destroy()
     149          else:
     150              # [Ok] moves focus.  (<Return> does not.)  Move it back.
     151              self.entry.focus_set()
     152  
     153      def cancel(self, event=None):  # Do not replace.
     154          "Set dialog result to None and destroy tk widget."
     155          self.result = None
     156          self.destroy()
     157  
     158      def destroy(self):
     159          self.grab_release()
     160          super().destroy()
     161  
     162  
     163  class ESC[4;38;5;81mSectionName(ESC[4;38;5;149mQuery):
     164      "Get a name for a config file section name."
     165      # Used in ConfigDialog.GetNewKeysName, .GetNewThemeName (837)
     166  
     167      def __init__(self, parent, title, message, used_names,
     168                   *, _htest=False, _utest=False):
     169          super().__init__(parent, title, message, used_names=used_names,
     170                           _htest=_htest, _utest=_utest)
     171  
     172      def entry_ok(self):
     173          "Return sensible ConfigParser section name or None."
     174          name = self.entry.get().strip()
     175          if not name:
     176              self.showerror('no name specified.')
     177              return None
     178          elif len(name)>30:
     179              self.showerror('name is longer than 30 characters.')
     180              return None
     181          elif name in self.used_names:
     182              self.showerror('name is already in use.')
     183              return None
     184          return name
     185  
     186  
     187  class ESC[4;38;5;81mModuleName(ESC[4;38;5;149mQuery):
     188      "Get a module name for Open Module menu entry."
     189      # Used in open_module (editor.EditorWindow until move to iobinding).
     190  
     191      def __init__(self, parent, title, message, text0,
     192                   *, _htest=False, _utest=False):
     193          super().__init__(parent, title, message, text0=text0,
     194                         _htest=_htest, _utest=_utest)
     195  
     196      def entry_ok(self):
     197          "Return entered module name as file path or None."
     198          name = self.entry.get().strip()
     199          if not name:
     200              self.showerror('no name specified.')
     201              return None
     202          # XXX Ought to insert current file's directory in front of path.
     203          try:
     204              spec = importlib.util.find_spec(name)
     205          except (ValueError, ImportError) as msg:
     206              self.showerror(str(msg))
     207              return None
     208          if spec is None:
     209              self.showerror("module not found.")
     210              return None
     211          if not isinstance(spec.loader, importlib.abc.SourceLoader):
     212              self.showerror("not a source-based module.")
     213              return None
     214          try:
     215              file_path = spec.loader.get_filename(name)
     216          except AttributeError:
     217              self.showerror("loader does not support get_filename.")
     218              return None
     219          except ImportError:
     220              # Some special modules require this (e.g. os.path)
     221              try:
     222                  file_path = spec.loader.get_filename()
     223              except TypeError:
     224                  self.showerror("loader failed to get filename.")
     225                  return None
     226          return file_path
     227  
     228  
     229  class ESC[4;38;5;81mGoto(ESC[4;38;5;149mQuery):
     230      "Get a positive line number for editor Go To Line."
     231      # Used in editor.EditorWindow.goto_line_event.
     232  
     233      def entry_ok(self):
     234          try:
     235              lineno = int(self.entry.get())
     236          except ValueError:
     237              self.showerror('not a base 10 integer.')
     238              return None
     239          if lineno <= 0:
     240              self.showerror('not a positive integer.')
     241              return None
     242          return lineno
     243  
     244  
     245  class ESC[4;38;5;81mHelpSource(ESC[4;38;5;149mQuery):
     246      "Get menu name and help source for Help menu."
     247      # Used in ConfigDialog.HelpListItemAdd/Edit, (941/9)
     248  
     249      def __init__(self, parent, title, *, menuitem='', filepath='',
     250                   used_names={}, _htest=False, _utest=False):
     251          """Get menu entry and url/local file for Additional Help.
     252  
     253          User enters a name for the Help resource and a web url or file
     254          name. The user can browse for the file.
     255          """
     256          self.filepath = filepath
     257          message = 'Name for item on Help menu:'
     258          super().__init__(
     259                  parent, title, message, text0=menuitem,
     260                  used_names=used_names, _htest=_htest, _utest=_utest)
     261  
     262      def create_extra(self):
     263          "Add path widjets to rows 10-12."
     264          frame = self.frame
     265          pathlabel = Label(frame, anchor='w', justify='left',
     266                            text='Help File Path: Enter URL or browse for file')
     267          self.pathvar = StringVar(self, self.filepath)
     268          self.path = Entry(frame, textvariable=self.pathvar, width=40)
     269          browse = Button(frame, text='Browse', width=8,
     270                          command=self.browse_file)
     271          self.path_error = Label(frame, text=' ', foreground='red',
     272                                  font=self.error_font)
     273  
     274          pathlabel.grid(column=0, row=10, columnspan=3, padx=5, pady=[10,0],
     275                         sticky=W)
     276          self.path.grid(column=0, row=11, columnspan=2, padx=5, sticky=W+E,
     277                         pady=[10,0])
     278          browse.grid(column=2, row=11, padx=5, sticky=W+S)
     279          self.path_error.grid(column=0, row=12, columnspan=3, padx=5,
     280                               sticky=W+E)
     281  
     282      def askfilename(self, filetypes, initdir, initfile):  # htest #
     283          # Extracted from browse_file so can mock for unittests.
     284          # Cannot unittest as cannot simulate button clicks.
     285          # Test by running htest, such as by running this file.
     286          return filedialog.Open(parent=self, filetypes=filetypes)\
     287                 .show(initialdir=initdir, initialfile=initfile)
     288  
     289      def browse_file(self):
     290          filetypes = [
     291              ("HTML Files", "*.htm *.html", "TEXT"),
     292              ("PDF Files", "*.pdf", "TEXT"),
     293              ("Windows Help Files", "*.chm"),
     294              ("Text Files", "*.txt", "TEXT"),
     295              ("All Files", "*")]
     296          path = self.pathvar.get()
     297          if path:
     298              dir, base = os.path.split(path)
     299          else:
     300              base = None
     301              if platform[:3] == 'win':
     302                  dir = os.path.join(os.path.dirname(executable), 'Doc')
     303                  if not os.path.isdir(dir):
     304                      dir = os.getcwd()
     305              else:
     306                  dir = os.getcwd()
     307          file = self.askfilename(filetypes, dir, base)
     308          if file:
     309              self.pathvar.set(file)
     310  
     311      item_ok = SectionName.entry_ok  # localize for test override
     312  
     313      def path_ok(self):
     314          "Simple validity check for menu file path"
     315          path = self.path.get().strip()
     316          if not path: #no path specified
     317              self.showerror('no help file path specified.', self.path_error)
     318              return None
     319          elif not path.startswith(('www.', 'http')):
     320              if path[:5] == 'file:':
     321                  path = path[5:]
     322              if not os.path.exists(path):
     323                  self.showerror('help file path does not exist.',
     324                                 self.path_error)
     325                  return None
     326              if platform == 'darwin':  # for Mac Safari
     327                  path =  "file://" + path
     328          return path
     329  
     330      def entry_ok(self):
     331          "Return apparently valid (name, path) or None"
     332          self.path_error['text'] = ''
     333          name = self.item_ok()
     334          path = self.path_ok()
     335          return None if name is None or path is None else (name, path)
     336  
     337  class ESC[4;38;5;81mCustomRun(ESC[4;38;5;149mQuery):
     338      """Get settings for custom run of module.
     339  
     340      1. Command line arguments to extend sys.argv.
     341      2. Whether to restart Shell or not.
     342      """
     343      # Used in runscript.run_custom_event
     344  
     345      def __init__(self, parent, title, *, cli_args=[],
     346                   _htest=False, _utest=False):
     347          """cli_args is a list of strings.
     348  
     349          The list is assigned to the default Entry StringVar.
     350          The strings are displayed joined by ' ' for display.
     351          """
     352          message = 'Command Line Arguments for sys.argv:'
     353          super().__init__(
     354                  parent, title, message, text0=cli_args,
     355                  _htest=_htest, _utest=_utest)
     356  
     357      def create_extra(self):
     358          "Add run mode on rows 10-12."
     359          frame = self.frame
     360          self.restartvar = BooleanVar(self, value=True)
     361          restart = Checkbutton(frame, variable=self.restartvar, onvalue=True,
     362                                offvalue=False, text='Restart shell')
     363          self.args_error = Label(frame, text=' ', foreground='red',
     364                                  font=self.error_font)
     365  
     366          restart.grid(column=0, row=10, columnspan=3, padx=5, sticky='w')
     367          self.args_error.grid(column=0, row=12, columnspan=3, padx=5,
     368                               sticky='we')
     369  
     370      def cli_args_ok(self):
     371          "Validity check and parsing for command line arguments."
     372          cli_string = self.entry.get().strip()
     373          try:
     374              cli_args = shlex.split(cli_string, posix=True)
     375          except ValueError as err:
     376              self.showerror(str(err))
     377              return None
     378          return cli_args
     379  
     380      def entry_ok(self):
     381          "Return apparently valid (cli_args, restart) or None."
     382          cli_args = self.cli_args_ok()
     383          restart = self.restartvar.get()
     384          return None if cli_args is None else (cli_args, restart)
     385  
     386  
     387  if __name__ == '__main__':
     388      from unittest import main
     389      main('idlelib.idle_test.test_query', verbosity=2, exit=False)
     390  
     391      from idlelib.idle_test.htest import run
     392      run(Query, HelpSource, CustomRun)