Merge pull request #2468 from lojack5/lojack-typed-typestubs
Some checks failed
ci-build / build-source-dist (push) Failing after 0s
ci-build / build-wheels (x64, macos-13, 3.10) (push) Has been skipped
ci-build / build-wheels (x64, macos-13, 3.11) (push) Has been skipped
ci-build / build-wheels (x64, macos-13, 3.12) (push) Has been skipped
ci-build / build-wheels (x64, macos-13, 3.13) (push) Has been skipped
ci-build / build-wheels (x64, macos-13, 3.9) (push) Has been skipped
ci-build / build-wheels (x64, ubuntu-22.04, 3.10) (push) Has been skipped
ci-build / build-wheels (x64, ubuntu-22.04, 3.11) (push) Has been skipped
ci-build / build-wheels (x64, ubuntu-22.04, 3.12) (push) Has been skipped
ci-build / build-wheels (x64, ubuntu-22.04, 3.13) (push) Has been skipped
ci-build / build-wheels (x64, ubuntu-22.04, 3.9) (push) Has been skipped
ci-build / build-wheels (x64, windows-2022, 3.10) (push) Has been skipped
ci-build / build-wheels (x64, windows-2022, 3.11) (push) Has been skipped
ci-build / build-wheels (x64, windows-2022, 3.12) (push) Has been skipped
ci-build / build-wheels (x64, windows-2022, 3.13) (push) Has been skipped
ci-build / build-wheels (x64, windows-2022, 3.9) (push) Has been skipped
ci-build / build-wheels (x86, windows-2022, 3.10) (push) Has been skipped
ci-build / build-wheels (x86, windows-2022, 3.11) (push) Has been skipped
ci-build / build-wheels (x86, windows-2022, 3.12) (push) Has been skipped
ci-build / build-wheels (x86, windows-2022, 3.13) (push) Has been skipped
ci-build / build-wheels (x86, windows-2022, 3.9) (push) Has been skipped
ci-build / Publish Python distribution to PyPI (push) Has been skipped
ci-build / Create GitHub Release and upload source (push) Has been skipped
ci-build / Upload wheels to snapshot-builds on wxpython.org (push) Has been skipped
ci-build / Build wxPython documentation (push) Has been cancelled

Improve Python type-stubs
This commit is contained in:
Scott Talbert
2025-01-06 22:14:20 -05:00
committed by GitHub
8 changed files with 308 additions and 115 deletions

View File

@@ -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
@@ -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
@@ -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.
@@ -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
@@ -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:
@@ -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
@@ -421,7 +430,7 @@ 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.
@@ -429,7 +438,7 @@ def run():
"""),
PyFunctionDef('GetResult', '(self)', 'return self.result',
PyFunctionDef('GetResult', '(self) -> _T', 'return self.result',
doc="""\
Returns the value of the callable.
@@ -437,7 +446,7 @@ def run():
: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):
@@ -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")
""")

View File

@@ -464,21 +464,11 @@ class FunctionDef(BaseDef, FixWxPrefix):
"""
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',
@@ -486,6 +476,7 @@ class FunctionDef(BaseDef, FixWxPrefix):
'wxString()': '""',
'wxArrayString()' : '[]',
'wxArrayInt()' : '[]',
'wxEmptyString': "''", # Makes signatures much shorter
}
if isinstance(self, CppMethodDef):
# rip apart the argsString instead of using the (empty) list of parameters
@@ -504,7 +495,14 @@ class FunctionDef(BaseDef, FixWxPrefix):
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)
@@ -515,25 +513,36 @@ class FunctionDef(BaseDef, FixWxPrefix):
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):

View File

@@ -81,11 +81,14 @@ def nci(text, numSpaces=0, stripLeading=True):
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'):
if dontWrap and line.lstrip().startswith(dontWrap):
lines.append(line)
else:
lines.append(tw.fill(line))
return '\n'.join(lines)

View File

@@ -22,6 +22,7 @@ want to add some type info to that version of the file eventually...
"""
import sys, os, re
from typing import Union
import etgtools.extractors as extractors
import etgtools.generators as generators
from etgtools.generators import nci, Utf8EncodingStream, textfile_open
@@ -77,6 +78,19 @@ header_pyi = """\
"""
typing_imports = """\
from __future__ import annotations
from enum import IntEnum, IntFlag, auto
from typing import (Any, overload, TypeAlias, Generic,
Union, Optional, List, Tuple, Callable
)
try:
from typing import ParamSpec
except ImportError:
from typing_extensions import ParamSpec
"""
#---------------------------------------------------------------------------
def piIgnored(obj):
@@ -112,18 +126,21 @@ class PiWrapperGenerator(generators.WrapperGeneratorBase, FixWxPrefix):
if not SKIP_PI_FILE:
_checkAndWriteHeader(destFile_pi, header_pi, module.docstring)
self.writeSection(destFile_pi, 'typing-imports', typing_imports, at_end=False)
self.writeSection(destFile_pi, module.name, stream.getvalue())
if not SKIP_PYI_FILE:
_checkAndWriteHeader(destFile_pyi, header_pyi, module.docstring)
self.writeSection(destFile_pyi, 'typing-imports', typing_imports, at_end=False)
self.writeSection(destFile_pyi, module.name, stream.getvalue())
def writeSection(self, destFile, sectionName, sectionText):
def writeSection(self, destFile, sectionName, sectionText, at_end = True):
"""
Read all the lines from destFile, remove those currently between
begin/end markers for sectionName (if any), and write the lines back
to the file with the new text in sectionText.
`at_end` determines where in the file the section is added when missing
"""
sectionBeginLine = -1
sectionEndLine = -1
@@ -139,10 +156,23 @@ class PiWrapperGenerator(generators.WrapperGeneratorBase, FixWxPrefix):
sectionEndLine = idx
if sectionBeginLine == -1:
if at_end:
# not there already, add to the end
lines.append(sectionBeginMarker + '\n')
lines.append(sectionText)
lines.append(sectionEndMarker + '\n')
else:
# not there already, add to the beginning
# Skip the header
idx = 0
for idx, line in enumerate(lines):
if not line.startswith('#'):
break
lines[idx+1:idx+1] = [
sectionBeginMarker + '\n',
sectionText,
sectionEndMarker + '\n',
]
else:
# replace the existing lines
lines[sectionBeginLine+1:sectionEndLine] = [sectionText]
@@ -202,11 +232,42 @@ class PiWrapperGenerator(generators.WrapperGeneratorBase, FixWxPrefix):
assert isinstance(enum, extractors.EnumDef)
if enum.ignored or piIgnored(enum):
return
# These enum classes aren't actually accessible from the real wx
# module, o we need to prepend _. But we want to make a type alias to
# the non-prefixed name, so method signatures can reference it without
# any special code, and also allow bare ints as inputs.
if '@' in enum.name or not enum.name:
# Anonymous enum
enum_name = f"_enum_{enum.name.replace('@', '').strip()}"
alias = ''
else:
alias = self.fixWxPrefix(enum.name)
enum_name = f'_{alias}'
if 'Flags' in enum_name:
enum_type = 'IntFlag'
else:
enum_type = 'IntEnum'
# Create the enum definition
stream.write(f'\n{indent}class {enum_name}({enum_type}):\n')
for v in enum.items:
if v.ignored or piIgnored(v):
continue
name = v.pyName or v.name
stream.write('%s%s = 0\n' % (indent, name))
stream.write(f'{indent} {name} = auto()\n')
# Create the alias if needed
if alias:
stream.write(f'{indent}{alias}: TypeAlias = Union[{enum_name}, int]\n')
# And bring the enum members into global scope. We can't use
# enum.global_enum for this because:
# 1. It's only available on Python 3.11+
# 2. FixWxPrefix wouldn't be able to pick up the names, since it's
# detecting based on AST parsing, not runtime changes (which
# enum.global_enum performs).
for v in enum.items:
if v.ignored or piIgnored(v):
continue
name = v.pyName or v.name
stream.write(f'{indent}{name} = {enum_name}.{name}\n')
#-----------------------------------------------------------------------
def generateGlobalVar(self, globalVar, stream):
@@ -214,22 +275,16 @@ class PiWrapperGenerator(generators.WrapperGeneratorBase, FixWxPrefix):
if globalVar.ignored or piIgnored(globalVar):
return
name = globalVar.pyName or globalVar.name
valTyp = 'Any'
if guessTypeInt(globalVar):
valTyp = '0'
valTyp = 'int'
elif guessTypeFloat(globalVar):
valTyp = '0.0'
valTyp = 'float'
elif guessTypeStr(globalVar):
valTyp = '""'
else:
valTyp = globalVar.type
valTyp = valTyp.replace('const ', '')
valTyp = valTyp.replace('*', '')
valTyp = valTyp.replace('&', '')
valTyp = valTyp.replace(' ', '')
valTyp = self.fixWxPrefix(valTyp)
valTyp += '()'
stream.write('%s = %s\n' % (name, valTyp))
valTyp = 'str'
elif globalVar.type:
valTyp = self.cleanType(globalVar.type) or valTyp
stream.write(f'{name}: {valTyp}\n')
#-----------------------------------------------------------------------
def generateDefine(self, define, stream):
@@ -237,10 +292,11 @@ class PiWrapperGenerator(generators.WrapperGeneratorBase, FixWxPrefix):
if define.ignored or piIgnored(define):
return
# we're assuming that all #defines that are not ignored are integer or string values
name = define.pyName or define.name
if '"' in define.value:
stream.write('%s = ""\n' % (define.pyName or define.name))
stream.write(f'{name}: str\n')
else:
stream.write('%s = 0\n' % (define.pyName or define.name))
stream.write(f'{name}: int\n')
#-----------------------------------------------------------------------
def generateTypedef(self, typedef, stream, indent=''):
@@ -322,7 +378,7 @@ class PiWrapperGenerator(generators.WrapperGeneratorBase, FixWxPrefix):
if pc.bases:
stream.write('(%s):\n' % ', '.join(pc.bases))
else:
stream.write('(object):\n')
stream.write(':\n')
indent2 = indent + ' '*4
if pc.briefDoc:
stream.write('%s"""\n' % indent2)
@@ -344,26 +400,29 @@ class PiWrapperGenerator(generators.WrapperGeneratorBase, FixWxPrefix):
#-----------------------------------------------------------------------
def generateFunction(self, function, stream):
def generateFunction(self, function, stream, is_overload=False):
assert isinstance(function, extractors.FunctionDef)
if not function.pyName:
return
if not is_overload and function.hasOverloads():
for f in function.overloads:
self.generateFunction(f, stream, True)
stream.write('\n@overload')
elif is_overload:
stream.write('\n@overload')
stream.write('\ndef %s' % function.pyName)
if function.hasOverloads():
stream.write('(*args, **kw)')
else:
argsString = function.pyArgsString
if not argsString:
argsString = '()'
if '->' in argsString:
pos = argsString.find(')')
argsString = argsString[:pos+1]
if '(' != argsString[0]:
pos = argsString.find('(')
argsString = argsString[pos:]
argsString = argsString.replace('::', '.')
stream.write(argsString)
stream.write(':\n')
if is_overload:
stream.write(' ...\n')
else:
stream.write(' """\n')
stream.write(nci(function.pyDocstring, 4))
stream.write(' """\n')
@@ -413,8 +472,6 @@ class PiWrapperGenerator(generators.WrapperGeneratorBase, FixWxPrefix):
bases = [self.fixWxPrefix(b, True) for b in bases]
stream.write(', '.join(bases))
stream.write(')')
else:
stream.write('(object)')
stream.write(':\n')
indent2 = indent + ' '*4
@@ -479,24 +536,35 @@ class PiWrapperGenerator(generators.WrapperGeneratorBase, FixWxPrefix):
assert isinstance(memberVar, extractors.MemberVarDef)
if memberVar.ignored or piIgnored(memberVar):
return
stream.write('%s%s = property(None, None)\n' % (indent, memberVar.name))
member_type = memberVar.type
if member_type:
member_type = self.cleanType(member_type)
if not member_type: # Unknown type for the member variable
member_type = 'Any'
stream.write(f'{indent}{memberVar.name}: {member_type}\n')
def generateProperty(self, prop, stream, indent):
assert isinstance(prop, extractors.PropertyDef)
if prop.ignored or piIgnored(prop):
return
stream.write('%s%s = property(None, None)\n' % (indent, prop.name))
self._generateProperty(prop, stream, indent)
def generatePyProperty(self, prop, stream, indent):
assert isinstance(prop, extractors.PyPropertyDef)
self._generateProperty(prop, stream, indent)
def _generateProperty(self, prop: Union[extractors.PyPropertyDef, extractors.PropertyDef], stream, indent: str):
if prop.ignored or piIgnored(prop):
return
stream.write('%s%s = property(None, None)\n' % (indent, prop.name))
if prop.setter and prop.getter:
stream.write(f'{indent}{prop.name} = property({prop.getter}, {prop.setter})\n')
elif prop.getter:
stream.write(f'{indent}{prop.name} = property({prop.getter})\n')
elif prop.setter:
stream.write(f'{indent}{prop.name} = property(fset={prop.setter})\n')
def generateMethod(self, method, stream, indent, name=None, docstring=None):
def generateMethod(self, method, stream, indent, name=None, docstring=None, is_overload=False):
assert isinstance(method, extractors.MethodDef)
for m in method.all(): # use the first not ignored if there are overloads
if not m.ignored or piIgnored(m):
@@ -512,21 +580,18 @@ class PiWrapperGenerator(generators.WrapperGeneratorBase, FixWxPrefix):
name = magicMethods[name]
# write the method declaration
if not is_overload and method.hasOverloads():
for m in method.overloads:
self.generateMethod(m, stream, indent, name, None, True)
stream.write(f'\n{indent}@overload')
elif is_overload:
stream.write(f'\n{indent}@overload')
if method.isStatic:
stream.write('\n%s@staticmethod' % indent)
stream.write('\n%sdef %s' % (indent, name))
if method.hasOverloads():
if not method.isStatic:
stream.write('(self, *args, **kw)')
else:
stream.write('(*args, **kw)')
else:
argsString = method.pyArgsString
if not argsString:
argsString = '()'
if '->' in argsString:
pos = argsString.find(') ->')
argsString = argsString[:pos+1]
if '(' != argsString[0]:
pos = argsString.find('(')
argsString = argsString[pos:]
@@ -541,6 +606,9 @@ class PiWrapperGenerator(generators.WrapperGeneratorBase, FixWxPrefix):
indent2 = indent + ' '*4
# docstring
if is_overload:
stream.write(f'{indent2}...\n')
else:
if not docstring:
if hasattr(method, 'pyDocstring'):
docstring = method.pyDocstring

View File

@@ -610,7 +610,7 @@ from .%s import *
# get the docstring text
text = nci(extractors.flattenNode(item.briefDoc, False))
text = wrapText(text)
text = wrapText(text, item.pyName or item.name)
#if isinstance(item, extractors.ClassDef):

View File

@@ -14,9 +14,12 @@ stage of the ETG scripts.
import etgtools as extractors
from .generators import textfile_open
import keyword
import re
import sys, os
import copy
import textwrap
from typing import Optional, Tuple
PY3 = sys.version_info[0] == 3
@@ -99,6 +102,7 @@ class FixWxPrefix(object):
testName = name
if '(' in name:
testName = name[:name.find('(')]
testName = testName.split('.')[0]
if testName in FixWxPrefix._coreTopLevelNames:
return 'wx.'+name
@@ -121,6 +125,9 @@ class FixWxPrefix(object):
names.append(item.name)
elif isinstance(item, ast.FunctionDef):
names.append(item.name)
elif isinstance(item, ast.AnnAssign):
if isinstance(item.target, ast.Name):
names.append(item.target.id)
names = list()
filename = 'wx/core.pyi'
@@ -136,7 +143,102 @@ class FixWxPrefix(object):
FixWxPrefix._coreTopLevelNames = names
def cleanName(self, name: str, is_expression: bool = False, fix_wx: bool = True) -> str:
"""Process a C++ name for use in Python code. In all cases, this means
handling name collisions with Python keywords. For names that will be
used for an identifier (ex: class, method, constant) - `is_expression`
is False - this also includes the reserved constant names 'False',
'True', and 'None'. When `is_expression` is True, name are allowed to
include special characters and the reserved constant names - this is
intended for cleaning up type-hint expressions ans default value
expressions.
Finally, the 'wx.' prefix is added if needed.
"""
for txt in ['const', '*', '&', ' ']:
name = name.replace(txt, '')
name = name.replace('::', '.')
if not is_expression:
name = re.sub(r'[^a-zA-Z0-9_\.]', '', name)
if not (is_expression and name in ['True', 'False', 'None']) and keyword.iskeyword(name):
name = f'_{name}' # Python keyword name collision
name = name.strip()
if fix_wx:
return self.fixWxPrefix(name, True)
else:
return removeWxPrefix(name)
def cleanType(self, type_name: str) -> str:
"""Process a C++ type name for use as a type annotation in Python code.
Handles translation of common C++ types to Python types, as well as a
few specific wx types to Python types.
"""
double_type = 'float' if PY3 else 'double'
long_type = 'int' if PY3 else 'long'
type_map = {
# Some types are guesses, marked with TODO to verify automatic
# conversion actually happens. Also, these are the type-names
# after processing by cleanName (so spaces are removed), or
# after potentially lopping off an 'Array' prefix.
# --String types
'String': 'str',
'Char': 'str',
'char': 'str',
'FileName': 'str', # TODO: check conversion
# --Int types
'byte': 'int',
'short': 'int',
'Int': 'int',
'unsigned': 'int',
'unsignedchar': 'int',
'unsignedshort': 'int',
'unsignedint': 'int',
'time_t': 'int',
'size_t': 'int',
'Int32': 'int',
'long': long_type,
'unsignedlong': long_type,
'ulong': long_type,
'LongLong': long_type,
# --Float types
'double': double_type,
'Double': double_type,
# --Others
'void': 'Any',
'PyObject': 'Any',
'WindowID': 'int', # defined in wx/defs.h
'Coord': 'int', # defined in wx/types.h
}
type_name = self.cleanName(type_name)
# Special handling of Vector<type> types -
if type_name.startswith('Vector<') and type_name.endswith('>'):
# Special handling for 'Vector<type>' types
type_name = self.cleanType(type_name[7:-1])
return f'List[{type_name}]'
if type_name.startswith('Array'):
type_name = self.cleanType(type_name[5:])
if type_name:
return f'List[{type_name}]'
else:
return 'list'
return type_map.get(type_name, type_name)
def parseNameAndType(self, name_string: str, type_string: Optional[str]) -> Tuple[str, Optional[str]]:
"""Given an identifier name and an optional type annotation, process
these per cleanName and cleanType. Further performs transforms on the
identifier name that may be required due to the type annotation.
Ex. The transformation "any_identifier : ..." -> "*args" requires
modifying both the identifier name and the annotation.
"""
name_string = self.cleanName(name_string, fix_wx=False)
if type_string:
type_string = self.cleanType(type_string)
if type_string == '...':
name_string = '*args'
type_string = None
if not type_string:
type_string = None
return name_string, type_string
def ignoreAssignmentOperators(node):

View File

@@ -28,3 +28,4 @@ markupsafe==1.1.1
doc2dash==2.3.0
beautifulsoup4
attrdict3 ; sys_platform == 'win32'
typing-extensions; python_version < '3.10'

View File

@@ -3,3 +3,4 @@ numpy < 1.17 ; python_version <= '2.7'
numpy ; python_version >= '3.0' and python_version < '3.12'
# pillow < 3.0
six
typing-extensions; python_version < '3.10'