Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 21 additions & 21 deletions wx/py/introspect.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -174,35 +172,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."
Expand All @@ -219,6 +218,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:
Expand Down
132 changes: 121 additions & 11 deletions wx/py/shell.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
import wx
from wx import stc

import keyword
import os
import sys
import time
Expand All @@ -25,6 +24,13 @@
from .version import VERSION
from .magic import magic
from .path import ls,cd,pwd,sx
try:
from _pyrepl import _module_completer
except (ModuleNotFoundError, ImportError):
_module_completer = None
import rlcompleter
import warnings


sys.ps3 = '<-- ' # Input prompt.
USE_MAGIC=True
Expand Down Expand Up @@ -271,7 +277,14 @@ 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]
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

# Keep track of the last non-continuation prompt positions.
self.promptPosStart = 0
Expand Down Expand Up @@ -324,10 +337,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)
Expand All @@ -339,6 +355,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)
Expand Down Expand Up @@ -721,7 +738,13 @@ 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)
if not self.autoCompleteShow(command):
event.Skip() # not handled
else:
event.Skip()

# Don't toggle between insert mode and overwrite mode.
Expand Down Expand Up @@ -1174,18 +1197,97 @@ 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):
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 = self._get_last_identifier(command)
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("."):
return False
offset = len(last)

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 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_:
return False

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:
command_ = last_identifier
else:
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:
completion = options[0]
self.write(completion[offset:])
if completion.endswith("("):
wx.CallAfter(self.autoCallTipShow, self._last_completion_command+completion)

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"""
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."""
Expand Down Expand Up @@ -1353,6 +1455,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)
Expand Down Expand Up @@ -1501,6 +1610,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")
Expand Down
Loading