From d2b3079e2368a17632901d1f74cfc67d2ed281ef Mon Sep 17 00:00:00 2001 From: DietmarSchwertberger Date: Sat, 16 May 2026 21:59:29 +0200 Subject: [PATCH 01/11] improve auto completion with _pyrepl and rlcompleter --- wx/py/shell.py | 99 +++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 90 insertions(+), 9 deletions(-) diff --git a/wx/py/shell.py b/wx/py/shell.py index d5b83f3a8..37bf28f91 100755 --- a/wx/py/shell.py +++ b/wx/py/shell.py @@ -25,6 +25,9 @@ from .version import VERSION from .magic import magic from .path import ls,cd,pwd,sx +from _pyrepl import _module_completer +import rlcompleter + sys.ps3 = '<-- ' # Input prompt. USE_MAGIC=True @@ -272,6 +275,10 @@ def __init__(self, parent, id=-1, pos=wx.DefaultPosition, # Find out for which keycodes the interpreter will autocomplete. self.autoCompleteKeys = self.interp.getAutoCompleteKeys() + self._module_completer = _module_completer.ModuleCompleter(locals) + self._rl_completer = rlcompleter.Completer(locals) + self.Bind(wx.stc.EVT_STC_AUTOCOMP_COMPLETED, self.OnAutoCompCompleted) + self._last_completion_command = None # Keep track of the last non-continuation prompt positions. self.promptPosStart = 0 @@ -721,7 +728,12 @@ def OnKeyDown(self, event): # Only allow these keys after the latest prompt. elif key in (wx.WXK_TAB, wx.WXK_DELETE): - if self.CanEdit(): + if not self.CanEdit(): event.Skip() + if key==wx.WXK_TAB and self.autoComplete and wx.WXK_TAB in self.autoCompleteKeys: + # from shell OnChar, which will not be called for TAB: start auto-completion + command = self.GetTextRange(self.promptPosEnd, currpos) + self.autoCompleteShow(command) + else: event.Skip() # Don't toggle between insert mode and overwrite mode. @@ -1174,19 +1186,88 @@ def runfile(self, filename): else: self.run(command, prompt=False, verbose=True) - def autoCompleteShow(self, command, offset = 0): + ############################################################################ + # new implementation of autoCompleteShow + import re + _last_identifier_re = re.compile(r".*?([a-z_]?\w*)?$", re.IGNORECASE+re.DOTALL) + _last_identifier_dot_re = re.compile(r".*?([a-z_\.]?[\w\.]*)?$", re.IGNORECASE+re.DOTALL) + def _get_last_identifier(self, command, include_dot=False): + if include_dot: + return self._last_identifier_dot_re.match(command).group(1) + return self._last_identifier_re.match(command).group(1) + def _get_completions(self, command): + ret = [] + while True: + a = self._rl_completer.complete(command, len(ret)) + if not a: break + ret.append(a.rsplit(".")[-1]) + if ret: + ret.sort(key=str.casefold) + return ret + # some hard-coded completions: + #last = command.rsplit(" ",1)[-1].rsplit(".",1)[-1] + last = self._get_last_identifier(command) + print("last", last) + completions = [] + if command.startswith("from"): + if not last: return ["import "] + completions.append("import ") + completions = [c for c in completions if c.startswith(last)] + return completions + + def autoCompleteShow(self, command:str, offset = 0): """Display auto-completion popup list.""" self.AutoCompSetAutoHide(self.autoCompleteAutoHide) self.AutoCompSetIgnoreCase(self.autoCompleteCaseInsensitive) - list = self.interp.getAutoCompleteList(command, - includeMagic=self.autoCompleteIncludeMagic, - includeSingle=self.autoCompleteIncludeSingle, - includeDouble=self.autoCompleteIncludeDouble) - if list: - options = ' '.join(list) - #offset = 0 + self._last_completion_command = None + last = self._get_last_identifier(command) + if not last and not command.endswith("."): + self.write("\t") + return + offset = len(last) + options = self._module_completer.get_completions(command) + if options: + options = [m.rsplit(".",1)[-1] for m in options] + # some hard-coded extensions: + if command.startswith("from ") and not "import" in command: + options = [m+" " for m in options] + + if len(options)==1: + self.write(options[0][offset:]) + elif options: + options = ' '.join(options) + self.AutoCompShow(offset, options) + return + + # symbol or statement, not a module + # for multi-line inputs, we take the last line only and strip "... " + command_ = command.rsplit("\r",1)[-1].rsplit("\n",1)[-1].rsplit("\t",1)[-1].lstrip(". ") + if not command_: + self.write("\t") + return + options = self._get_completions(command_) + if not options: + command_ = self._get_last_identifier(command_, include_dot=True) + options = self._get_completions(command_) + if not options: + # hard-coded extensions + if last=="ra": options = ["range("] + if last=="pr": options = ["print("] + + if len(options)==1: + self.write(options[0][offset:]) + elif options: + self._last_completion_command = command_ + options = ' '.join(options) self.AutoCompShow(offset, options) + def OnAutoCompCompleted(self, evt): + """If the user has selected a completion ending with '(', display the call tip""" + completion = evt.GetString() + if completion.endswith("(") and self._last_completion_command: + wx.CallAfter(self.autoCallTipShow, self._last_completion_command+completion) + evt.Skip() + def autoCallTipShow(self, command, insertcalltip = True, forceCallTip = False): """Display argument spec and docstring in a popup window.""" if self.CallTipActive(): From 2a9d1bc4fe1044e05a21bc602feb5e66f11a0b52 Mon Sep 17 00:00:00 2001 From: DietmarSchwertberger Date: Sat, 16 May 2026 21:59:50 +0200 Subject: [PATCH 02/11] activate completion on tab key --- wx/py/shell.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wx/py/shell.py b/wx/py/shell.py index 37bf28f91..f02af34b4 100755 --- a/wx/py/shell.py +++ b/wx/py/shell.py @@ -274,7 +274,7 @@ def __init__(self, parent, id=-1, pos=wx.DefaultPosition, self.buffer = Buffer() # Find out for which keycodes the interpreter will autocomplete. - self.autoCompleteKeys = self.interp.getAutoCompleteKeys() + self.autoCompleteKeys = self.interp.getAutoCompleteKeys() + [wx.WXK_TAB] self._module_completer = _module_completer.ModuleCompleter(locals) self._rl_completer = rlcompleter.Completer(locals) self.Bind(wx.stc.EVT_STC_AUTOCOMP_COMPLETED, self.OnAutoCompCompleted) From 9f3d1c0c3c69875db4246d832ecf80e376f0efd0 Mon Sep 17 00:00:00 2001 From: DietmarSchwertberger Date: Sat, 16 May 2026 22:00:51 +0200 Subject: [PATCH 03/11] display calltip also on class instantiations; drop support for old Python versions --- wx/py/introspect.py | 40 +++++++++++++++++++++------------------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/wx/py/introspect.py b/wx/py/introspect.py index 052f895a9..41cd4068e 100644 --- a/wx/py/introspect.py +++ b/wx/py/introspect.py @@ -174,35 +174,36 @@ def getCallTip(command='', locals=None): pass tip1 = '' argspec = '' + doc = '' obj = inspect.unwrap(obj) if inspect.isbuiltin(obj): # Builtin functions don't have an argspec that we can get. pass - elif inspect.isfunction(obj): + elif callable(obj): # tip1 is a string like: "getCallTip(command='', locals=None)" try: - argspec = str(inspect.signature(obj)) # PY35 or later - except AttributeError: - argspec = inspect.getfullargspec(obj) - argspec = inspect.formatargspec(*argspec) - if dropSelf: - # The first parameter to a method is a reference to an - # instance, usually coded as "self", and is usually passed - # automatically by Python; therefore we want to drop it. - temp = argspec.split(',') - if len(temp) == 1: # No other arguments. - argspec = '()' - elif temp[0][:2] == '(*': # first param is like *args, not self - pass - else: # Drop the first argument. - argspec = '(' + ','.join(temp[1:]).lstrip() - tip1 = name + argspec - doc = '' - if callable(obj): + argspec = str(inspect.signature(obj)) + except ValueError: + pass + else: + if dropSelf: + # The first parameter to a method is a reference to an + # instance, usually coded as "self", and is usually passed + # automatically by Python; therefore we want to drop it. + temp = argspec.split(',') + if len(temp) == 1: # No other arguments. + argspec = '()' + elif temp[0][:2] == '(*': # first param is like *args, not self + pass + else: # Drop the first argument. + argspec = '(' + ','.join(temp[1:]).lstrip() + tip1 = name + argspec + try: doc = inspect.getdoc(obj) except: pass + if doc: # tip2 is the first separated line of the docstring, like: # "Return call tip text for a command." @@ -219,6 +220,7 @@ def getCallTip(command='', locals=None): tip = '%s%s\n\n%s' % (tip1, tip2, tip3) else: tip = tip1 + # Extract argspec from the signature e.g., (x, /, *, ...) -> int m = re.search(r'\((.*)\)', argspec) if m: From 392fd71dfdaf3d95fb8eb2ce515523b8a9b0b6cf Mon Sep 17 00:00:00 2001 From: DietmarSchwertberger Date: Sat, 16 May 2026 22:02:29 +0200 Subject: [PATCH 04/11] remove unused imports --- wx/py/introspect.py | 2 -- wx/py/shell.py | 1 - 2 files changed, 3 deletions(-) diff --git a/wx/py/introspect.py b/wx/py/introspect.py index 41cd4068e..3124c0b80 100644 --- a/wx/py/introspect.py +++ b/wx/py/introspect.py @@ -7,8 +7,6 @@ import sys import inspect import tokenize -import types -import wx from io import BytesIO def getAutoCompleteList(command='', locals=None, includeMagic=1, diff --git a/wx/py/shell.py b/wx/py/shell.py index f02af34b4..45380208c 100755 --- a/wx/py/shell.py +++ b/wx/py/shell.py @@ -9,7 +9,6 @@ import wx from wx import stc -import keyword import os import sys import time From 3a2254200305300826c83be221bc0707c5e3db2c Mon Sep 17 00:00:00 2001 From: DietmarSchwertberger Date: Sat, 16 May 2026 22:02:55 +0200 Subject: [PATCH 05/11] add context menu item "Copy History" --- wx/py/shell.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/wx/py/shell.py b/wx/py/shell.py index 45380208c..c89cc13bc 100755 --- a/wx/py/shell.py +++ b/wx/py/shell.py @@ -330,10 +330,13 @@ def __init__(self, parent, id=-1, pos=wx.DefaultPosition, self.ID_UNDO = wx.NewIdRef() self.ID_REDO = wx.NewIdRef() + self.ID_COPY_HISTORY = wx.NewIdRef() + # Assign handlers for edit events self.Bind(wx.EVT_MENU, lambda evt: self.Cut(), id=self.ID_CUT) self.Bind(wx.EVT_MENU, lambda evt: self.Copy(), id=self.ID_COPY) self.Bind(wx.EVT_MENU, lambda evt: self.CopyWithPrompts(), id=frame.ID_COPY_PLUS) + self.Bind(wx.EVT_MENU, lambda evt: self.CopyHistory(), id=self.ID_COPY_HISTORY) self.Bind(wx.EVT_MENU, lambda evt: self.Paste(), id=self.ID_PASTE) self.Bind(wx.EVT_MENU, lambda evt: self.PasteAndRun(), id=frame.ID_PASTE_PLUS) self.Bind(wx.EVT_MENU, lambda evt: self.SelectAll(), id=self.ID_SELECTALL) @@ -345,6 +348,7 @@ def __init__(self, parent, id=-1, pos=wx.DefaultPosition, self.Bind(wx.EVT_UPDATE_UI, lambda evt: evt.Enable(self.CanCut()), id=self.ID_CLEAR) self.Bind(wx.EVT_UPDATE_UI, lambda evt: evt.Enable(self.CanCopy()), id=self.ID_COPY) self.Bind(wx.EVT_UPDATE_UI, lambda evt: evt.Enable(self.CanCopy()), id=frame.ID_COPY_PLUS) + self.Bind(wx.EVT_UPDATE_UI, lambda evt: evt.Enable(bool(self.history)), id=self.ID_COPY_HISTORY) self.Bind(wx.EVT_UPDATE_UI, lambda evt: evt.Enable(self.CanPaste()), id=self.ID_PASTE) self.Bind(wx.EVT_UPDATE_UI, lambda evt: evt.Enable(self.CanPaste()), id=frame.ID_PASTE_PLUS) self.Bind(wx.EVT_UPDATE_UI, lambda evt: evt.Enable(self.CanUndo()), id=self.ID_UNDO) @@ -1433,6 +1437,13 @@ def CopyWithPromptsPrefixed(self): data = wx.TextDataObject(command) self._clip(data) + def CopyHistory(self): + """Copy input history and place it on the clipboard.""" + if self.history: + data = os.linesep.join( reversed(self.history) ) + data = wx.TextDataObject(data) + self._clip(data) + def _clip(self, data): if wx.TheClipboard.Open(): wx.TheClipboard.UsePrimarySelection(False) @@ -1581,6 +1592,7 @@ def GetContextMenu(self): menu.Append(self.ID_CUT, "Cut") menu.Append(self.ID_COPY, "Copy") menu.Append(frame.ID_COPY_PLUS, "Copy With Prompts") + menu.Append(self.ID_COPY_HISTORY, "Copy History") menu.Append(self.ID_PASTE, "Paste") menu.Append(frame.ID_PASTE_PLUS, "Paste And Run") menu.Append(self.ID_CLEAR, "Clear") From cede7b3994181507dabb77bc2045acbe5509a8de Mon Sep 17 00:00:00 2001 From: DietmarSchwertberger Date: Sat, 16 May 2026 22:53:32 +0200 Subject: [PATCH 06/11] fix tooltip for completed command --- wx/py/shell.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/wx/py/shell.py b/wx/py/shell.py index c89cc13bc..bb18da3dd 100755 --- a/wx/py/shell.py +++ b/wx/py/shell.py @@ -1258,9 +1258,13 @@ def autoCompleteShow(self, command:str, offset = 0): if last=="pr": options = ["print("] if len(options)==1: - self.write(options[0][offset:]) + completion = options[0] + self.write(completion[offset:]) + if completion.endswith("("): + wx.CallAfter(self.autoCallTipShow, command_[:-offset]+completion) + elif options: - self._last_completion_command = command_ + self._last_completion_command = command_[:-offset] options = ' '.join(options) self.AutoCompShow(offset, options) From c3ed9a378a51418b01a370a8eb9ed001e4b69fe7 Mon Sep 17 00:00:00 2001 From: DietmarSchwertberger Date: Sun, 17 May 2026 15:06:45 +0200 Subject: [PATCH 07/11] change priorities: last identifier first --- wx/py/shell.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/wx/py/shell.py b/wx/py/shell.py index bb18da3dd..09368db7c 100755 --- a/wx/py/shell.py +++ b/wx/py/shell.py @@ -1248,10 +1248,14 @@ def autoCompleteShow(self, command:str, offset = 0): if not command_: self.write("\t") return - options = self._get_completions(command_) - if not options: - command_ = self._get_last_identifier(command_, include_dot=True) + + last_identifier = self._get_last_identifier(command_, include_dot=True) + options = self._get_completions(last_identifier) + if options: + command_ = last_identifier + else: options = self._get_completions(command_) + if not options: # hard-coded extensions if last=="ra": options = ["range("] From 631ca456d26f80b6bf5685ae4d6b7ff54896aece Mon Sep 17 00:00:00 2001 From: DietmarSchwertberger Date: Sun, 17 May 2026 16:18:56 +0200 Subject: [PATCH 08/11] handle Python <3.13 where _pyrepl is not available --- wx/py/shell.py | 38 +++++++++++++++++++++++--------------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/wx/py/shell.py b/wx/py/shell.py index 09368db7c..d268959cb 100755 --- a/wx/py/shell.py +++ b/wx/py/shell.py @@ -24,7 +24,10 @@ from .version import VERSION from .magic import magic from .path import ls,cd,pwd,sx -from _pyrepl import _module_completer +try: + from _pyrepl import _module_completer +except ModuleNotFoundError: + _module_completer = None import rlcompleter @@ -274,7 +277,10 @@ def __init__(self, parent, id=-1, pos=wx.DefaultPosition, # Find out for which keycodes the interpreter will autocomplete. self.autoCompleteKeys = self.interp.getAutoCompleteKeys() + [wx.WXK_TAB] - self._module_completer = _module_completer.ModuleCompleter(locals) + if _module_completer is not None: + self._module_completer = _module_completer.ModuleCompleter(locals) + else: + self._module_completer = None self._rl_completer = rlcompleter.Completer(locals) self.Bind(wx.stc.EVT_STC_AUTOCOMP_COMPLETED, self.OnAutoCompCompleted) self._last_completion_command = None @@ -1228,19 +1234,21 @@ def autoCompleteShow(self, command:str, offset = 0): self.write("\t") return offset = len(last) - options = self._module_completer.get_completions(command) - if options: - options = [m.rsplit(".",1)[-1] for m in options] - # some hard-coded extensions: - if command.startswith("from ") and not "import" in command: - options = [m+" " for m in options] - - if len(options)==1: - self.write(options[0][offset:]) - elif options: - options = ' '.join(options) - self.AutoCompShow(offset, options) - return + + if self._module_completer: + options = self._module_completer.get_completions(command) + if options: + options = [m.rsplit(".",1)[-1] for m in options] + # some hard-coded extensions: + if command.startswith("from ") and not "import" in command: + options = [m+" " for m in options] + + if len(options)==1: + self.write(options[0][offset:]) + elif options: + options = ' '.join(options) + self.AutoCompShow(offset, options) + return # symbol or statement, not a module # for multi-line inputs, we take the last line only and strip "... " From 6f343640da43ff9dc94edb2e2eb1e2e81de51518 Mon Sep 17 00:00:00 2001 From: DietmarSchwertberger Date: Sun, 17 May 2026 16:19:33 +0200 Subject: [PATCH 09/11] fix calltip follow-up --- wx/py/shell.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/wx/py/shell.py b/wx/py/shell.py index d268959cb..8c623ea71 100755 --- a/wx/py/shell.py +++ b/wx/py/shell.py @@ -1257,6 +1257,7 @@ def autoCompleteShow(self, command:str, offset = 0): self.write("\t") return + self._last_completion_command = command_[:-offset] if offset else command_ last_identifier = self._get_last_identifier(command_, include_dot=True) options = self._get_completions(last_identifier) if options: @@ -1273,10 +1274,9 @@ def autoCompleteShow(self, command:str, offset = 0): completion = options[0] self.write(completion[offset:]) if completion.endswith("("): - wx.CallAfter(self.autoCallTipShow, command_[:-offset]+completion) + wx.CallAfter(self.autoCallTipShow, self._last_completion_command+completion) elif options: - self._last_completion_command = command_[:-offset] options = ' '.join(options) self.AutoCompShow(offset, options) From ec2b8e64e5f5ee394199c6292cd592900892306a Mon Sep 17 00:00:00 2001 From: DietmarSchwertberger Date: Sun, 17 May 2026 21:41:06 +0200 Subject: [PATCH 10/11] catch also ImportError (on Python 3.13 _pyrepl is available, but not _module_completer --- wx/py/shell.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wx/py/shell.py b/wx/py/shell.py index 8c623ea71..dca85dab6 100755 --- a/wx/py/shell.py +++ b/wx/py/shell.py @@ -26,7 +26,7 @@ from .path import ls,cd,pwd,sx try: from _pyrepl import _module_completer -except ModuleNotFoundError: +except (ModuleNotFoundError, ImportError): _module_completer = None import rlcompleter From 560e2d0f719864154b17712cf73944e5af23c5d4 Mon Sep 17 00:00:00 2001 From: DietmarSchwertberger Date: Mon, 18 May 2026 20:06:19 +0200 Subject: [PATCH 11/11] some fixes --- wx/py/shell.py | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/wx/py/shell.py b/wx/py/shell.py index dca85dab6..5c478fd2f 100755 --- a/wx/py/shell.py +++ b/wx/py/shell.py @@ -29,6 +29,7 @@ except (ModuleNotFoundError, ImportError): _module_completer = None import rlcompleter +import warnings sys.ps3 = '<-- ' # Input prompt. @@ -741,7 +742,8 @@ def OnKeyDown(self, event): if key==wx.WXK_TAB and self.autoComplete and wx.WXK_TAB in self.autoCompleteKeys: # from shell OnChar, which will not be called for TAB: start auto-completion command = self.GetTextRange(self.promptPosEnd, currpos) - self.autoCompleteShow(command) + if not self.autoCompleteShow(command): + event.Skip() # not handled else: event.Skip() @@ -1205,18 +1207,19 @@ def _get_last_identifier(self, command, include_dot=False): return self._last_identifier_dot_re.match(command).group(1) return self._last_identifier_re.match(command).group(1) def _get_completions(self, command): - ret = [] - while True: - a = self._rl_completer.complete(command, len(ret)) - if not a: break - ret.append(a.rsplit(".")[-1]) + with warnings.catch_warnings(action="ignore"): + if "." in command and not "[" in command and not "(" in command: + ret = self._rl_completer.attr_matches(command) + elif "." in command: + ret = [] + else: + ret = self._rl_completer.global_matches(command) if ret: + ret = [a.rsplit(".")[-1] for a in ret] ret.sort(key=str.casefold) return ret # some hard-coded completions: - #last = command.rsplit(" ",1)[-1].rsplit(".",1)[-1] last = self._get_last_identifier(command) - print("last", last) completions = [] if command.startswith("from"): if not last: return ["import "] @@ -1231,8 +1234,7 @@ def autoCompleteShow(self, command:str, offset = 0): self._last_completion_command = None last = self._get_last_identifier(command) if not last and not command.endswith("."): - self.write("\t") - return + return False offset = len(last) if self._module_completer: @@ -1248,14 +1250,13 @@ def autoCompleteShow(self, command:str, offset = 0): elif options: options = ' '.join(options) self.AutoCompShow(offset, options) - return + return True # symbol or statement, not a module # for multi-line inputs, we take the last line only and strip "... " command_ = command.rsplit("\r",1)[-1].rsplit("\n",1)[-1].rsplit("\t",1)[-1].lstrip(". ") if not command_: - self.write("\t") - return + return False self._last_completion_command = command_[:-offset] if offset else command_ last_identifier = self._get_last_identifier(command_, include_dot=True) @@ -1279,6 +1280,7 @@ def autoCompleteShow(self, command:str, offset = 0): elif options: options = ' '.join(options) self.AutoCompShow(offset, options) + return True def OnAutoCompCompleted(self, evt): """If the user has selected a completion ending with '(', display the call tip"""