Skip to content

Commit

Permalink
Merge pull request #2468 from lojack5/lojack-typed-typestubs
Browse files Browse the repository at this point in the history
Improve Python type-stubs
  • Loading branch information
swt2c authored Jan 7, 2025
2 parents 37dcdca + 17cbd02 commit c3092be
Show file tree
Hide file tree
Showing 8 changed files with 309 additions and 116 deletions.
33 changes: 21 additions & 12 deletions etg/_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,16 @@ def run():
""")


module.addPyFunction('CallAfter', '(callableObj, *args, **kw)', doc="""\
module.addPyCode('import typing', order=10)
module.addPyCode("""\
_T = typing.TypeVar('_T')
try:
_P = typing.ParamSpec('_P')
except AttributeError:
import typing_extensions
_P = typing_extensions.ParamSpec('_P')
""")
module.addPyFunction('CallAfter', '(callableObj: typing.Callable[_P, _T], *args: _P.args, **kw: _P.kwargs) -> None', doc="""\
Call the specified function after the current and pending event
handlers have been completed. This is also good for making GUI
method calls from non-GUI threads. Any extra positional or
Expand Down Expand Up @@ -322,7 +331,7 @@ def run():
wx.PostEvent(app, evt)""")


module.addPyClass('CallLater', ['object'],
module.addPyClass('CallLater', ['typing.Generic[_P, _T]'],
doc="""\
A convenience class for :class:`wx.Timer`, that calls the given callable
object once after the given amount of milliseconds, passing any
Expand All @@ -342,7 +351,7 @@ def run():
""",
items = [
PyCodeDef('__instances = {}'),
PyFunctionDef('__init__', '(self, millis, callableObj, *args, **kwargs)',
PyFunctionDef('__init__', '(self, millis, callableObj: typing.Callable[_P, _T], *args: _P.args, **kwargs: _P.kwargs) -> None',
doc="""\
Constructs a new :class:`wx.CallLater` object.
Expand All @@ -366,7 +375,7 @@ def run():

PyFunctionDef('__del__', '(self)', 'self.Stop()'),

PyFunctionDef('Start', '(self, millis=None, *args, **kwargs)',
PyFunctionDef('Start', '(self, millis: typing.Optional[int]=None, *args: _P.args, **kwargs: _P.kwargs) -> None',
doc="""\
(Re)start the timer
Expand All @@ -388,7 +397,7 @@ def run():
self.running = True"""),
PyCodeDef('Restart = Start'),

PyFunctionDef('Stop', '(self)',
PyFunctionDef('Stop', '(self) -> None',
doc="Stop and destroy the timer.",
body="""\
if self in CallLater.__instances:
Expand All @@ -397,16 +406,16 @@ def run():
self.timer.Stop()
self.timer = None"""),

PyFunctionDef('GetInterval', '(self)', """\
PyFunctionDef('GetInterval', '(self) -> int', """\
if self.timer is not None:
return self.timer.GetInterval()
else:
return 0"""),

PyFunctionDef('IsRunning', '(self)',
PyFunctionDef('IsRunning', '(self) -> bool',
"""return self.timer is not None and self.timer.IsRunning()"""),

PyFunctionDef('SetArgs', '(self, *args, **kwargs)',
PyFunctionDef('SetArgs', '(self, *args: _P.args, **kwargs: _P.kwargs) -> None',
doc="""\
(Re)set the args passed to the callable object. This is
useful in conjunction with :meth:`Start` if
Expand All @@ -421,23 +430,23 @@ def run():
self.args = args
self.kwargs = kwargs"""),

PyFunctionDef('HasRun', '(self)', 'return self.hasRun',
PyFunctionDef('HasRun', '(self) -> bool', 'return self.hasRun',
doc="""\
Returns whether or not the callable has run.
:rtype: bool
"""),

PyFunctionDef('GetResult', '(self)', 'return self.result',
PyFunctionDef('GetResult', '(self) -> _T', 'return self.result',
doc="""\
Returns the value of the callable.
:rtype: a Python object
:return: result from callable
"""),

PyFunctionDef('Notify', '(self)',
PyFunctionDef('Notify', '(self) -> None',
doc="The timer has expired so call the callable.",
body="""\
if self.callable and getattr(self.callable, 'im_self', True):
Expand All @@ -456,7 +465,7 @@ def run():
module.addPyCode("FutureCall = deprecated(CallLater, 'Use CallLater instead.')")

module.addPyCode("""\
def GetDefaultPyEncoding():
def GetDefaultPyEncoding() -> str:
return "utf-8"
GetDefaultPyEncoding = deprecated(GetDefaultPyEncoding, msg="wxPython now always uses utf-8")
""")
Expand Down
53 changes: 31 additions & 22 deletions etgtools/extractors.py
Original file line number Diff line number Diff line change
Expand Up @@ -464,28 +464,19 @@ def makePyArgsString(self):
"""
Create a pythonized version of the argsString in function and method
items that can be used as part of the docstring.
TODO: Maybe (optionally) use this syntax to document arg types?
http://www.python.org/dev/peps/pep-3107/
"""
def _cleanName(name):
for txt in ['const', '*', '&', ' ']:
name = name.replace(txt, '')
name = name.replace('::', '.')
name = self.fixWxPrefix(name, True)
return name

params = list()
returns = list()
if self.type and self.type != 'void':
returns.append(_cleanName(self.type))
returns.append(self.cleanType(self.type))

defValueMap = { 'true': 'True',
'false': 'False',
'NULL': 'None',
'wxString()': '""',
'wxArrayString()' : '[]',
'wxArrayInt()' : '[]',
'wxEmptyString': "''", # Makes signatures much shorter
}
if isinstance(self, CppMethodDef):
# rip apart the argsString instead of using the (empty) list of parameters
Expand All @@ -504,7 +495,14 @@ def _cleanName(name):
else:
default = self.fixWxPrefix(default, True)
# now grab just the last word, it should be the variable name
arg = arg.split()[-1]
# The rest will be the type information
arg_type, arg = arg.rsplit(None, 1)
arg, arg_type = self.parseNameAndType(arg, arg_type)
if arg_type:
if default == 'None':
arg = f'{arg}: Optional[{arg_type}]'
else:
arg = f'{arg}: {arg_type}'
if default:
arg += '=' + default
params.append(arg)
Expand All @@ -515,25 +513,36 @@ def _cleanName(name):
continue
if param.arraySize:
continue
s = param.pyName or param.name
s, param_type = self.parseNameAndType(param.pyName or param.name, param.type)
if param.out:
returns.append(s)
if param_type:
returns.append(param_type)
else:
if param.inOut:
returns.append(s)
if param_type:
returns.append(param_type)
if param.default:
default = param.default
if default in defValueMap:
default = defValueMap.get(default)

s += '=' + '|'.join([_cleanName(x) for x in default.split('|')])
if param_type:
if default == 'None':
s = f'{s}: Optional[{param_type}]'
else:
s = f'{s}: {param_type}'
default = '|'.join([self.cleanName(x, True) for x in default.split('|')])
s = f'{s}={default}'
elif param_type:
s = f'{s} : {param_type}'
params.append(s)

self.pyArgsString = '(' + ', '.join(params) + ')'
if len(returns) == 1:
self.pyArgsString += ' -> ' + returns[0]
if len(returns) > 1:
self.pyArgsString += ' -> (' + ', '.join(returns) + ')'
self.pyArgsString = f"({', '.join(params)})"
if not returns:
self.pyArgsString = f'{self.pyArgsString} -> None'
elif len(returns) == 1:
self.pyArgsString = f'{self.pyArgsString} -> {returns[0]}'
elif len(returns) > 1:
self.pyArgsString = f"{self.pyArgsString} -> Tuple[{', '.join(returns)}]"


def collectPySignatures(self):
Expand Down
7 changes: 5 additions & 2 deletions etgtools/generators.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,12 +81,15 @@ def _allSpaces(text):
return newText


def wrapText(text):
def wrapText(text, dontWrap: str = ''):
import textwrap
lines = []
tw = textwrap.TextWrapper(width=70, break_long_words=False)
for line in text.split('\n'):
lines.append(tw.fill(line))
if dontWrap and line.lstrip().startswith(dontWrap):
lines.append(line)
else:
lines.append(tw.fill(line))
return '\n'.join(lines)


Expand Down
Loading

0 comments on commit c3092be

Please sign in to comment.