3434
3535import roundup .instance
3636from roundup import __version__ as roundup_version
37- from roundup import date , hyperdb , init , password , token_r
38- from roundup .anypy import scandir_
37+ from roundup import date , hyperdb , init , password , support , token_r
38+ from roundup .anypy import scandir_ # noqa: F401 define os.scandir
3939from roundup .anypy .my_input import my_input
4040from roundup .anypy .strings import repr_export
4141from roundup .configuration import (
4949)
5050from roundup .exceptions import UsageError
5151from roundup .i18n import _ , get_translation
52- from roundup import support
5352
5453try :
5554 from UserDict import UserDict
@@ -93,6 +92,13 @@ class AdminTool:
9392 Additional help may be supplied by help_*() methods.
9493 """
9594
95+ # import here to make AdminTool.readline accessible or
96+ # mockable from tests.
97+ try :
98+ import readline # noqa: I001, PLC0415
99+ except ImportError :
100+ readline = None
101+
96102 # Make my_input a property to allow overriding in testing.
97103 # my_input is imported in other places, so just set it from
98104 # the imported value rather than moving def here.
@@ -1815,6 +1821,111 @@ def do_pragma(self, args):
18151821 type (self .settings [setting ]).__name__ )
18161822 self .settings [setting ] = value
18171823
1824+ def do_readline (self , args ):
1825+ '' """Usage: readline initrc_line | 'emacs' | 'history' | 'reload' | 'vi'
1826+
1827+ Using 'reload' will reload the file ~/.roundup_admin_rlrc.
1828+ 'history' will show (and number) all commands in the history.
1829+
1830+ You can change input mode using the 'emacs' or 'vi' parameters.
1831+ The default is emacs. This is the same as using::
1832+
1833+ readline set editing-mode emacs
1834+
1835+ or::
1836+
1837+ readline set editing-mode vi
1838+
1839+ Any command that can be placed in a readline .inputrc file can
1840+ be executed using the readline command. You can assign
1841+ dump-variables to control O using::
1842+
1843+ readline Control-o: dump-variables
1844+
1845+ Assigning multi-key values also works.
1846+
1847+ pyreadline3 support on windows:
1848+
1849+ Mode switching doesn't work, emacs only.
1850+
1851+ Binding single key commands works with::
1852+
1853+ readline Control-w: history-search-backward
1854+
1855+ Multiple key sequences don't work.
1856+
1857+ Setting values may work. Difficult to tell because the library
1858+ has no way to view the live settings.
1859+
1860+ """
1861+
1862+ # TODO: allow history 20 # most recent 20 commands
1863+ # history 100-200 # show commands 100-200
1864+
1865+ if not self .readline :
1866+ print (_ ("Readline support is not available." ))
1867+ return
1868+ # The if test allows pyreadline3 settings like:
1869+ # bind_exit_key("Control-z") get through to
1870+ # parse_and_bind(). It is not obvious that this form of
1871+ # command is supported. Pyreadline3 is supposed to parse
1872+ # readline style commands, so we use those for emacs/vi.
1873+ # Trying set-mode(...) as in the pyreadline3 init file
1874+ # didn't work in testing.
1875+
1876+ if len (args ) == 1 and args [0 ].find ('(' ) == - 1 :
1877+ if args [0 ] == "vi" :
1878+ self .readline .parse_and_bind ("set editing-mode vi" )
1879+ print (_ ("Enabled vi mode." ))
1880+ elif args [0 ] == "emacs" :
1881+ self .readline .parse_and_bind ("set editing-mode emacs" )
1882+ print (_ ("Enabled emacs mode." ))
1883+ elif args [0 ] == "history" :
1884+ print ("history size" ,
1885+ self .readline .get_current_history_length ())
1886+ print ('\n ' .join ([
1887+ str ("%3d " % (i + 1 ) +
1888+ self .readline .get_history_item (i + 1 ))
1889+ for i in range (
1890+ self .readline .get_current_history_length ())
1891+ ]))
1892+ elif args [0 ] == "reload" :
1893+ try :
1894+ # readline is a singleton. In testing previous
1895+ # tests using read_init_file are loading from ~
1896+ # not the test directory because it doesn't
1897+ # matter. But for reload we want to test with the
1898+ # init file under the test directory. Calling
1899+ # read_init_file() calls with the ~/.. init
1900+ # location and I can't seem to reset it
1901+ # or the readline state.
1902+ # So call with explicit file here.
1903+ self .readline .read_init_file (
1904+ self .get_readline_init_file ())
1905+ except FileNotFoundError as e :
1906+ # If user invoked reload explicitly, report
1907+ # if file not found.
1908+ #
1909+ # DOES NOT WORK with pyreadline3. Exception
1910+ # is not raised if file is missing.
1911+ #
1912+ # Also e.filename is None under cygwin. A
1913+ # simple test case does set e.filename
1914+ # correctly?? sigh. So I just call
1915+ # get_readline_init_file again to get
1916+ # filename.
1917+ fn = e .filename or self .get_readline_init_file ()
1918+ print (_ ("Init file %s not found." ) % fn )
1919+ else :
1920+ print (_ ("File %s reloaded." ) %
1921+ self .get_readline_init_file ())
1922+ else :
1923+ print (_ ("Unknown readline parameter %s" ) % args [0 ])
1924+ return
1925+
1926+ self .readline .parse_and_bind (" " .join (args ))
1927+ return
1928+
18181929 designator_re = re .compile ('([A-Za-z]+)([0-9]+)$' )
18191930 designator_rng = re .compile ('([A-Za-z]+):([0-9]+)-([0-9]+)$' )
18201931
@@ -2365,29 +2476,34 @@ def history_features(self, feature):
23652476 # setting the bit disables the feature, so use not.
23662477 return not self .settings ['history_features' ] & features [feature ]
23672478
2479+ def get_readline_init_file (self ):
2480+ return os .path .join (os .path .expanduser ("~" ),
2481+ ".roundup_admin_rlrc" )
2482+
23682483 def interactive (self ):
23692484 """Run in an interactive mode
23702485 """
23712486 print (_ ('Roundup %s ready for input.\n Type "help" for help.' )
23722487 % roundup_version )
23732488
2374- initfile = os .path .join (os .path .expanduser ("~" ),
2375- ".roundup_admin_rlrc" )
2489+ initfile = self .get_readline_init_file ()
23762490 histfile = os .path .join (os .path .expanduser ("~" ),
23772491 ".roundup_admin_history" )
23782492
2379- try :
2380- import readline
2493+ if self .readline :
2494+ # clear any history that might be left over from caller
2495+ # when reusing AdminTool from tests or program.
2496+ self .readline .clear_history ()
23812497 try :
23822498 if self .history_features ('load_rc' ):
2383- readline .read_init_file (initfile )
2384- except IOError : # FileNotFoundError under python3
2499+ self . readline .read_init_file (initfile )
2500+ except FileNotFoundError :
23852501 # file is optional
23862502 pass
23872503
23882504 try :
23892505 if self .history_features ('load_history' ):
2390- readline .read_history_file (histfile )
2506+ self . readline .read_history_file (histfile )
23912507 except IOError : # FileNotFoundError under python3
23922508 # no history file yet
23932509 pass
@@ -2397,19 +2513,75 @@ def interactive(self):
23972513 # Pragma history_length allows setting on a per
23982514 # invocation basis at startup
23992515 if self .settings ['history_length' ] != - 1 :
2400- readline .set_history_length (
2516+ self . readline .set_history_length (
24012517 self .settings ['history_length' ])
2402- except ImportError :
2403- readline = None
2404- print (_ ('Note: command history and editing not available' ))
24052518
2519+ if hasattr (self .readline , 'backend' ):
2520+ # FIXME after min 3.13 version; no backend prints pyreadline3
2521+ print (_ ("Readline enabled using %s." ) % self .readline .backend )
2522+ else :
2523+ print (_ ("Readline enabled using unknown library." ))
2524+
2525+ else :
2526+ print (_ ('Command history and line editing not available' ))
2527+
2528+ autosave_enabled = sys .stdin .isatty () and sys .stdout .isatty ()
24062529 while 1 :
24072530 try :
24082531 command = self .my_input ('roundup> ' )
2532+ # clear an input hook in case it was used to prefill
2533+ # buffer.
2534+ self .readline .set_pre_input_hook ()
24092535 except EOFError :
24102536 print (_ ('exit...' ))
24112537 break
24122538 if not command : continue # noqa: E701
2539+ if command .startswith ('!' ): # Pull numbered command from history
2540+ print_only = command .endswith (":p" )
2541+ try :
2542+ hist_num = int (command [1 :]) \
2543+ if not print_only else int (command [1 :- 2 ])
2544+ command = self .readline .get_history_item (hist_num )
2545+ except ValueError :
2546+ # pass the unknown command
2547+ pass
2548+ else :
2549+ if autosave_enabled and \
2550+ hasattr (self .readline , "replace_history_item" ):
2551+ # history has the !23 input. Replace it if possible.
2552+ # replace_history_item not supported by pyreadline3
2553+ # so !23 will show up in history not the command.
2554+ self .readline .replace_history_item (
2555+ self .readline .get_current_history_length () - 1 ,
2556+ command )
2557+
2558+ if print_only :
2559+ # fill the edit buffer with the command
2560+ # the user selected.
2561+
2562+ # from https://stackoverflow.com/questions/8505163/is-it-possible-to-prefill-a-input-in-python-3s-command-line-interface
2563+ # This triggers:
2564+ # B023 Function definition does not bind loop variable
2565+ # `command`
2566+ # in ruff. command will be the value of the command
2567+ # variable at the time the function is run.
2568+ # Not the value at define time. This is ok since
2569+ # hook is run before command is changed by the
2570+ # return from (readline) input.
2571+ def hook ():
2572+ self .readline .insert_text (command ) # noqa: B023
2573+ self .readline .redisplay ()
2574+ self .readline .set_pre_input_hook (hook )
2575+ # we clear the hook after the next line is read.
2576+ continue
2577+
2578+ if not autosave_enabled :
2579+ # needed to make testing work and also capture
2580+ # commands received on stdin from file/other command
2581+ # output. Disable saving with pragma on command line:
2582+ # -P history_features=2.
2583+ self .readline .add_history (command )
2584+
24132585 try :
24142586 args = token_r .token_split (command )
24152587 except ValueError :
@@ -2426,8 +2598,9 @@ def interactive(self):
24262598 self .db .commit ()
24272599
24282600 # looks like histfile is saved with mode 600
2429- if readline and self .history_features ('save_history' ):
2430- readline .write_history_file (histfile )
2601+ if self .readline and self .history_features ('save_history' ):
2602+ self .readline .write_history_file (histfile )
2603+
24312604 return 0
24322605
24332606 def main (self ): # noqa: PLR0912, PLR0911
0 commit comments