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

Type stub improvements
This commit is contained in:
Scott Talbert
2025-03-14 21:29:35 -04:00
committed by GitHub
9 changed files with 392 additions and 156 deletions

View File

@@ -43,7 +43,9 @@ def run():
# Allow on-the-fly creation of a wx.BitmapBundle from a wx.Bitmap, wx.Icon
# or a wx.Image
c.convertFromPyObject = """\
c.convertFromPyObject = tools.AutoConversionInfo(
('wx.Bitmap', 'wx.Icon', ),
"""\
// Check for type compatibility
if (!sipIsErr) {
if (sipCanConvertToType(sipPy, sipType_wxBitmap, SIP_NO_CONVERTORS))
@@ -86,7 +88,7 @@ def run():
*sipCppPtr = reinterpret_cast<wxBitmapBundle*>(
sipConvertToType(sipPy, sipType_wxBitmapBundle, sipTransferObj, SIP_NO_CONVERTORS, 0, sipIsErr));
return 0; // not a new instance
"""
""")
c = module.find('wxBitmapBundleImpl')

View File

@@ -192,7 +192,9 @@ def run():
# String with color name or #RRGGBB or #RRGGBBAA format
# None (converts to wxNullColour)
c.allowNone = True
c.convertFromPyObject = """\
c.convertFromPyObject = tools.AutoConversionInfo(
('wx.Colour', '_ThreeInts', '_FourInts', 'str', 'None'),
"""\
// is it just a typecheck?
if (!sipIsErr) {
if (sipPy == Py_None)
@@ -273,7 +275,7 @@ def run():
*sipCppPtr = reinterpret_cast<wxColour*>(sipConvertToType(
sipPy, sipType_wxColour, sipTransferObj, SIP_NO_CONVERTORS, 0, sipIsErr));
return 0; // not a new instance
"""
""")
module.addPyCode('NamedColour = wx.deprecated(Colour, "Use Colour instead.")')

View File

@@ -70,7 +70,9 @@ def run():
c.find('GetPtr').overloads[0].ignore()
c.convertFromPyObject = """\
c.convertFromPyObject = tools.AutoConversionInfo(
('str', 'None', ),
"""\
// Code to test a PyObject for compatibility with wxPGPropArgCls
if (!sipIsErr) {
if (sipCanConvertToType(sipPy, sipType_wxPGPropArgCls, SIP_NO_CONVERTORS))
@@ -109,7 +111,7 @@ def run():
SIP_NO_CONVERTORS, 0, sipIsErr));
return 0; // not a new instance
}
"""
""")
#----------------------------------------------------------

View File

@@ -76,7 +76,9 @@ def run():
c.includeCppCode('src/stream_input.cpp')
# Use that class for the convert code
c.convertFromPyObject = """\
c.convertFromPyObject = tools.AutoConversionInfo(
(), # TODO: Track down what python types actually can be wrapped
"""\
// is it just a typecheck?
if (!sipIsErr) {
if (wxPyInputStream::Check(sipPy))
@@ -86,7 +88,7 @@ def run():
// otherwise do the conversion
*sipCppPtr = new wxPyInputStream(sipPy);
return 0; //sipGetState(sipTransferObj);
"""
""")
# Add Python file-like methods so a wx.InputStream can be used as if it
# was any other Python file object.
@@ -236,7 +238,9 @@ def run():
c.includeCppCode('src/stream_output.cpp')
# Use that class for the convert code
c.convertFromPyObject = """\
c.convertFromPyObject = tools.AutoConversionInfo(
(), # TODO: Track down what python types can actually be converted
"""\
// is it just a typecheck?
if (!sipIsErr) {
if (wxPyOutputStream::Check(sipPy))
@@ -246,7 +250,7 @@ def run():
// otherwise do the conversion
*sipCppPtr = new wxPyOutputStream(sipPy);
return sipGetState(sipTransferObj);
"""
""")
# Add Python file-like methods so a wx.OutputStream can be used as if it

View File

@@ -311,7 +311,9 @@ def run():
# Add some code (like MappedTypes) to automatically convert from a Python
# datetime.date or a datetime.datetime object
c.convertFromPyObject = """\
c.convertFromPyObject = tools.AutoConversionInfo(
('datetime', 'date', ),
"""\
// Code to test a PyObject for compatibility with wxDateTime
if (!sipIsErr) {
if (sipCanConvertToType(sipPy, sipType_wxDateTime, SIP_NO_CONVERTORS))
@@ -335,7 +337,7 @@ def run():
sipPy, sipType_wxDateTime, sipTransferObj, SIP_NO_CONVERTORS, 0, sipIsErr));
return 0; // Not a new instance
"""
""")
#---------------------------------------------

View File

@@ -15,12 +15,13 @@ wxWidgets API info which we need from them.
import sys
import os
import pprint
from typing import Optional
import xml.etree.ElementTree as ET
import copy
from .tweaker_tools import FixWxPrefix, magicMethods, \
from .tweaker_tools import AutoConversionInfo, FixWxPrefix, MethodType, magicMethods, \
guessTypeInt, guessTypeFloat, guessTypeStr, \
textfile_open
textfile_open, Signature, removeWxPrefix
from sphinxtools.utilities import findDescendants
if sys.version_info >= (3, 11):
@@ -280,11 +281,14 @@ class FunctionDef(BaseDef, FixWxPrefix):
"""
Information about a standalone function.
"""
_default_method_type = MethodType.FUNCTION
def __init__(self, element=None, **kw):
super(FunctionDef, self).__init__()
self.type = None
self.definition = ''
self.argsString = ''
self.signature: Optional[Signature] = None
self.pyArgsString = ''
self.isOverloaded = False
self.overloads = []
@@ -406,7 +410,10 @@ class FunctionDef(BaseDef, FixWxPrefix):
else:
parent = self.klass
item = self.findOverload(matchText)
assert item is not None
item.pyName = newName
if item.signature:
item.signature.method_name = newName
item.__dict__.update(kw)
if item is self and not self.hasOverloads():
@@ -470,8 +477,8 @@ 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.
"""
params = list()
returns = list()
params: list[Signature.Parameter] = []
returns: list[str] = []
if self.type and self.type != 'void':
returns.append(self.cleanType(self.type))
@@ -483,6 +490,7 @@ class FunctionDef(BaseDef, FixWxPrefix):
'wxArrayInt()' : '[]',
'wxEmptyString': "''", # Makes signatures much shorter
}
P = Signature.Parameter
if isinstance(self, CppMethodDef):
# rip apart the argsString instead of using the (empty) list of parameters
lastP = self.argsString.rfind(')')
@@ -495,22 +503,15 @@ class FunctionDef(BaseDef, FixWxPrefix):
if '=' in arg:
default = arg.split('=')[1].strip()
arg = arg.split('=')[0].strip()
if default in defValueMap:
default = defValueMap.get(default)
else:
default = self.fixWxPrefix(default, True)
default = defValueMap.get(default, default)
default = self.fixWxPrefix(default, True)
# now grab just the last word, it should be the variable name
# 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)
arg, arg_type = self.parseNameAndType(arg, arg_type, True)
params.append(P(arg, arg_type, default))
if default == 'None':
params[-1].make_optional()
else:
for param in self.items:
assert isinstance(param, ParamDef)
@@ -518,36 +519,40 @@ class FunctionDef(BaseDef, FixWxPrefix):
continue
if param.arraySize:
continue
s, param_type = self.parseNameAndType(param.pyName or param.name, param.type)
s, param_type = self.parseNameAndType(param.pyName or param.name, param.type, not param.out)
if param.out:
if param_type:
returns.append(param_type)
else:
default = ''
if param.inOut:
if param_type:
returns.append(param_type)
if param.default:
default = param.default
if default in defValueMap:
default = defValueMap.get(default)
if param_type:
if default == 'None':
s = f'{s}: Optional[{param_type}]'
else:
s = f'{s}: {param_type}'
default = defValueMap.get(default, default)
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 = f"({', '.join(params)})"
if not returns:
self.pyArgsString = f'{self.pyArgsString} -> None'
params.append(P(s, param_type, default))
if default == 'None':
params[-1].make_optional()
if getattr(self, 'isCtor', False):
name = '__init__'
else:
name = self.pyName or self.name
name = self.fixWxPrefix(name)
# __bool__ and __nonzero__ need to be defined as returning int for SIP, but for Python
# __bool__ is required to return a bool:
if name in ('__bool__', '__nonzero__'):
return_type = 'bool'
elif not returns:
return_type = 'None'
elif len(returns) == 1:
self.pyArgsString = f'{self.pyArgsString} -> {returns[0]}'
elif len(returns) > 1:
self.pyArgsString = f"{self.pyArgsString} -> Tuple[{', '.join(returns)}]"
return_type = returns[0]
else:
return_type = f"Tuple[{', '.join(returns)}]"
kind = MethodType.STATIC_METHOD if getattr(self, 'isStatic', False) else type(self)._default_method_type
self.signature = Signature(name, *params, return_type=return_type, method_type=kind)
self.pyArgsString = self.signature.args_string(False)
def collectPySignatures(self):
@@ -584,6 +589,8 @@ class MethodDef(FunctionDef):
"""
Represents a class method, ctor or dtor declaration.
"""
_default_method_type = MethodType.METHOD
def __init__(self, element=None, className=None, **kw):
super(MethodDef, self).__init__()
self.className = className
@@ -693,7 +700,7 @@ class ClassDef(BaseDef):
self.headerCode = []
self.cppCode = []
self.convertToPyObject = None
self.convertFromPyObject = None
self._convertFromPyObject = None
self.allowNone = False # Allow the convertFrom code to handle None too.
self.instanceCode = None # Code to be used to create new instances of this class
self.innerclasses = []
@@ -713,6 +720,26 @@ class ClassDef(BaseDef):
if element is not None:
self.extract(element)
@property
def convertFromPyObject(self) -> Optional[str]:
return self._convertFromPyObject
@convertFromPyObject.setter
def convertFromPyObject(self, value: AutoConversionInfo) -> None:
self._convertFromPyObject = value.code
name = self.pyName or self.name
name = removeWxPrefix(name)
FixWxPrefix.register_autoconversion(name, value.convertables)
def is_top_level(self) -> bool:
"""Check if this class is a subclass of wx.TopLevelWindow"""
if not self.nodeBases:
return False
all_classes, specials = self.nodeBases
if 'wxTopLevelWindow' in specials:
return True
return 'wxTopLevelWindow' in all_classes
def renameClass(self, newName):
self.pyName = newName
@@ -1270,7 +1297,7 @@ class CppMethodDef(MethodDef):
NOTE: This one is not automatically extracted, but can be added to
classes in the tweaker stage
"""
def __init__(self, type, name, argsString, body, doc=None, isConst=False,
def __init__(self, type, name, argsString: str, body, doc=None, isConst=False,
cppSignature=None, virtualCatcherCode=None, **kw):
super(CppMethodDef, self).__init__()
self.type = type

View File

@@ -22,7 +22,7 @@ want to add some type info to that version of the file eventually...
"""
import sys, os, re
from typing import Union
from typing import Optional, Union
import etgtools.extractors as extractors
import etgtools.generators as generators
from etgtools.generators import nci, Utf8EncodingStream, textfile_open
@@ -80,6 +80,7 @@ header_pyi = """\
typing_imports = """\
from __future__ import annotations
from datetime import datetime, date
from enum import IntEnum, IntFlag, auto
from typing import (Any, overload, TypeAlias, Generic,
Union, Optional, List, Tuple, Callable
@@ -89,6 +90,12 @@ try:
except ImportError:
from typing_extensions import ParamSpec
_TwoInts: TypeAlias = Tuple[int, int]
_ThreeInts: TypeAlias = Tuple[int, int, int]
_FourInts: TypeAlias = Tuple[int, int, int, int]
_TwoFloats: TypeAlias = Tuple[float, float]
_FourFloats: TypeAlias = Tuple[float, float, float, float]
"""
#---------------------------------------------------------------------------
@@ -295,6 +302,8 @@ class PiWrapperGenerator(generators.WrapperGeneratorBase, FixWxPrefix):
name = define.pyName or define.name
if '"' in define.value:
stream.write(f'{name}: str\n')
elif define.value in ('true', 'false'):
stream.write(f'{name}: bool\n')
else:
stream.write(f'{name}: int\n')
@@ -320,7 +329,8 @@ class PiWrapperGenerator(generators.WrapperGeneratorBase, FixWxPrefix):
t = typedef.type.replace('>', '')
t = t.replace(' ', '')
bases = t.split('<')
bases = [self.fixWxPrefix(b, True) for b in bases]
bases = (self.fixWxPrefix(b, True) for b in bases)
bases = [b.replace('*', '') for b in bases] # fix for RichTextLine*
name = self.fixWxPrefix(typedef.name)
# Now write the Python equivalent class for the typedef
@@ -388,7 +398,7 @@ class PiWrapperGenerator(generators.WrapperGeneratorBase, FixWxPrefix):
# these are the only kinds of items allowed to be items in a PyClass
dispatch = {
extractors.PyFunctionDef : self.generatePyFunction,
extractors.PyPropertyDef : self.generatePyProperty,
extractors.PyPropertyDef : lambda a,b,c: self.generatePyProperty(pc, a, b, c),
extractors.PyCodeDef : self.generatePyCode,
extractors.PyClassDef : self.generatePyClass,
}
@@ -410,16 +420,11 @@ class PiWrapperGenerator(generators.WrapperGeneratorBase, FixWxPrefix):
stream.write('\n@overload')
elif is_overload:
stream.write('\n@overload')
stream.write('\ndef %s' % function.pyName)
argsString = function.pyArgsString
if not argsString:
argsString = '()'
if '(' != argsString[0]:
pos = argsString.find('(')
argsString = argsString[pos:]
argsString = argsString.replace('::', '.')
stream.write(argsString)
stream.write(':\n')
if not function.signature:
function.makePyArgsString()
assert function.signature is not None
for line in function.signature.definition_lines():
stream.write(f'\n{line}')
if is_overload:
stream.write(' ...\n')
else:
@@ -498,8 +503,8 @@ class PiWrapperGenerator(generators.WrapperGeneratorBase, FixWxPrefix):
dispatch = {
extractors.MemberVarDef : self.generateMemberVar,
extractors.TypedefDef : lambda a,b,c: None,
extractors.PropertyDef : self.generateProperty,
extractors.PyPropertyDef : self.generatePyProperty,
extractors.PropertyDef : lambda a,b,c: self.generateProperty(klass, a, b, c),
extractors.PyPropertyDef : lambda a,b,c: self.generatePyProperty(klass, a, b, c),
extractors.MethodDef : self.generateMethod,
extractors.EnumDef : self.generateEnum,
extractors.CppMethodDef : self.generateCppMethod,
@@ -517,7 +522,8 @@ class PiWrapperGenerator(generators.WrapperGeneratorBase, FixWxPrefix):
if item.isCtor:
item.klass = klass
self.generateMethod(item, stream, indent2,
name='__init__', docstring=klass.pyDocstring)
name='__init__', docstring=klass.pyDocstring,
is_top_level_init=klass.is_top_level())
for item in public:
item.klass = klass
@@ -532,6 +538,15 @@ class PiWrapperGenerator(generators.WrapperGeneratorBase, FixWxPrefix):
stream.write('%s# end of class %s\n\n' % (indent, klassName))
def find_method(self, klass: extractors.ClassDef, method_name: str) -> Optional[extractors.MethodDef]:
methods = (i for i in klass if isinstance(i, extractors.MethodDef) and not i.isCtor and not i.isDtor)
for method in methods:
name = method.name or method.pyName
if name == method_name:
return method
return None
def generateMemberVar(self, memberVar, stream, indent):
assert isinstance(memberVar, extractors.MemberVarDef)
if memberVar.ignored or piIgnored(memberVar):
@@ -544,27 +559,58 @@ class PiWrapperGenerator(generators.WrapperGeneratorBase, FixWxPrefix):
stream.write(f'{indent}{memberVar.name}: {member_type}\n')
def generateProperty(self, prop, stream, indent):
def generateProperty(self, klass, prop, stream, indent):
assert isinstance(prop, extractors.PropertyDef)
self._generateProperty(prop, stream, indent)
self._generateProperty(klass, prop, stream, indent)
def generatePyProperty(self, prop, stream, indent):
def generatePyProperty(self, klass, prop, stream, indent):
assert isinstance(prop, extractors.PyPropertyDef)
self._generateProperty(prop, stream, indent)
self._generateProperty(klass, prop, stream, indent)
def _generateProperty(self, prop: Union[extractors.PyPropertyDef, extractors.PropertyDef], stream, indent: str):
def _generateProperty(self, klass: extractors.ClassDef, prop: Union[extractors.PyPropertyDef, extractors.PropertyDef], stream, indent: str):
if prop.ignored or piIgnored(prop):
return
value_type = ''
if prop.getter:
getter = self.find_method(klass, prop.getter)
if getter and getter.signature:
value_type = getter.signature.return_type
if prop.setter:
setter = self.find_method(klass, prop.setter)
if setter and setter.signature:
value_type = setter.signature[0].type_hint
if prop.setter and prop.getter:
stream.write(f'{indent}{prop.name} = property({prop.getter}, {prop.setter})\n')
if value_type:
stream.write(f'{indent}@property\n')
stream.write(f'{indent}def {prop.name}(self) -> {value_type}: ...\n')
stream.write(f'{indent}@{prop.name}.setter\n')
stream.write(f'{indent}def {prop.name}(self, value: {value_type}, /) -> None: ...\n')
else:
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')
if value_type:
stream.write(f'{indent}@property\n')
stream.write(f'{indent}def {prop.name}(self) -> {value_type}: ...\n')
else:
stream.write(f'{indent}{prop.name} = property({prop.getter})\n')
elif prop.setter:
# Can't use the decorator syntax in this situation
stream.write(f'{indent}{prop.name} = property(fset={prop.setter})\n')
def generateMethod(self, method, stream, indent, name=None, docstring=None, is_overload=False):
def generateMethod(self, method, stream, indent, name=None, docstring=None, is_overload=False, is_top_level_init=False):
"""Write the python declaration for a method (type-stub or otherwise):
method: MethodDef holding information about the method
stream: output stream to write to
indent: indentation level to use when writing
name: name of the method, if wanting to override what is identified in `method`
docstring: docstring to use, if wanting to override what is in method.pyDocString
is_overload: If this declaration should be marked with `@typing.overload`
is_top_level_init: If this class is a subclass of wx.TopLevelWindow and is an __init__ method, to apply the
transformation `parent: <WindowType>` -> `parent: Optional[<WindowType>]`, because TopLevelWindow
allows for a `None` parent.
"""
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):
@@ -575,34 +621,23 @@ class PiWrapperGenerator(generators.WrapperGeneratorBase, FixWxPrefix):
if method.isDtor:
return
name = name or method.pyName or method.name
if name in magicMethods:
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)
self.generateMethod(m, stream, indent, name, None, True, is_top_level_init)
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))
argsString = method.pyArgsString
if not argsString:
argsString = '()'
if '(' != argsString[0]:
pos = argsString.find('(')
argsString = argsString[pos:]
if not method.isStatic:
if argsString == '()':
argsString = '(self)'
else:
argsString = '(self, ' + argsString[1:]
argsString = argsString.replace('::', '.')
stream.write(argsString)
stream.write(':\n')
if not method.signature:
method.makePyArgsString()
assert method.signature is not None
if name is not None:
method.signature.method_name = name
if is_top_level_init and 'parent' in method.signature:
method.signature['parent'].make_optional()
for line in method.signature.definition_lines():
stream.write(f'\n{indent}{line}')
stream.write('\n')
indent2 = indent + ' '*4
# docstring

View File

@@ -14,6 +14,7 @@ the various XML elements passed by the Phoenix extractors into ReST format.
"""
# Standard library stuff
import keyword
import os
import operator
import sys
@@ -28,7 +29,7 @@ import xml.etree.ElementTree as ET
import etgtools.extractors as extractors
import etgtools.generators as generators
from etgtools.item_module_map import ItemModuleMap
from etgtools.tweaker_tools import removeWxPrefix
from etgtools.tweaker_tools import removeWxPrefix, ParameterType
# Sphinx-Phoenix specific stuff
from sphinxtools.inheritance import InheritanceDiagram
@@ -580,31 +581,14 @@ class ParameterList(Node):
if xml_item.hasOverloads() and not is_overload:
return
arguments = xml_item.pyArgsString
if xml_item.signature is None:
xml_item.makePyArgsString()
assert xml_item.signature is not None
signature = xml_item.signature.signature()
arguments = list(xml_item.signature)
if not arguments:
return
if hasattr(xml_item, 'isStatic') and not xml_item.isStatic:
if arguments[:2] == '()':
return
arguments = arguments[1:]
if '->' in arguments:
arguments, dummy = arguments.split("->")
arguments = arguments.strip()
if arguments.endswith(','):
arguments = arguments[0:-1]
if arguments.startswith('('):
arguments = arguments[1:]
if arguments.endswith(')'):
arguments = arguments[0:-1]
signature = name + '(%s)'%arguments
arguments = arguments.split(',')
py_parameters = []
for key, parameter in self.py_parameters.items():
pdef = parameter.pdef
@@ -619,37 +603,23 @@ class ParameterList(Node):
' ==> Parameter list from wxWidgets XML items: %s\n\n' \
'This may be a documentation bug in wxWidgets or a side-effect of removing the `wx` prefix from signatures.\n\n'
theargs = []
for arg in arguments:
myarg = arg.split('=')[0].strip()
if myarg:
theargs.append(myarg)
if '*' in arg or ')' in arg:
arg_name = arg.name
if arg.position_type in (ParameterType.VAR_ARGS, ParameterType.KWARGS):
continue
if arg_name.startswith('_') and keyword.iskeyword(arg_name[1:]): # Reserved Python keywords we've had to rename
arg_name = arg_name[1:]
#if '*' in arg_name:
# continue
arg = arg.split('=')[0].strip()
if arg and arg not in py_parameters:
if arg_name not in py_parameters:
class_name = ''
if hasattr(xml_item, 'className') and xml_item.className is not None:
class_name = wx2Sphinx(xml_item.className)[1] + '.'
print((message % (class_name + name, arg, signature, py_parameters)))
## for param in py_parameters:
## if param not in theargs:
## class_name = ''
## if hasattr(xml_item, 'className') and xml_item.className is not None:
## class_name = wx2Sphinx(xml_item.className)[1] + '.'
##
## print '\n ||| %s;%s;%s |||\n'%(class_name[0:-1], signature, param)
## with open('mismatched.txt', 'a') as fid:
## fid.write('%s;%s;%s\n'%(class_name[0:-1], signature, param))
# -----------------------------------------------------------------------

View File

@@ -12,6 +12,7 @@ Some helpers and utility functions that can assist with the tweaker
stage of the ETG scripts.
"""
import enum
import etgtools as extractors
from .generators import textfile_open
import keyword
@@ -19,7 +20,7 @@ import re
import sys, os
import copy
import textwrap
from typing import Optional, Tuple
from typing import NamedTuple, Optional, Tuple, Union
isWindows = sys.platform.startswith('win')
@@ -40,6 +41,174 @@ magicMethods = {
}
class AutoConversionInfo(NamedTuple):
convertables: Tuple[str, ...] # String type-hints for each of the types that can be automatically converted to this class
code: str # Code that will be added to SIP for this conversion
class ParameterType(enum.Enum):
VAR_ARGS = enum.auto()
KWARGS = enum.auto()
POSITIONAL_ONLY = enum.auto()
DEFAULT = enum.auto()
class MethodType(enum.Enum):
STATIC_METHOD = enum.auto() # Class @staticmethod method
CLASS_METHOD = enum.auto() # Class @classmethod method
METHOD = enum.auto() # Class regular method
FUNCTION = enum.auto() # non-class function
class Signature:
"""Like inspect.Signature, but a bit simpler because we need it only for a few purposes:
- Creation from a C++ args string
- We *don't* want stringized (ie: all of them) type-hints to be evaluated, since we're
processing them in a context where most of them will be unresolvable.
"""
class Parameter:
__slots__ = ('name', 'type_hint', 'default', 'position_type', )
name: str
type_hint: Optional[str]
default: Optional[str]
position_type: ParameterType
def __init__(self, name: str, type_hint: Optional[str] = None, default: Optional[str] = None, position_type: ParameterType = ParameterType.DEFAULT) -> None:
if name.startswith('**'):
name = name[2:]
position_type = ParameterType.KWARGS
elif name.startswith('*'):
name = name[1:]
position_type = ParameterType.VAR_ARGS
type_hint = type_hint.replace('::', '.') if type_hint else None
default = default.replace('::', '.') if default else None
self.name = name
self.type_hint = type_hint
self.default = default
self.position_type = position_type
@property
def _position_marking(self) -> str:
if self.position_type is ParameterType.KWARGS:
return '**'
elif self.position_type is ParameterType.VAR_ARGS:
return '*'
return ''
def untyped(self) -> str:
if self.default is None:
return f'{self._position_marking}{self.name}'
else:
return f'{self._position_marking}{self.name}={self.default}'
def __str__(self) -> str:
if self.type_hint is None and self.default is None:
return f'{self._position_marking}{self.name}'
elif self.type_hint is None:
return f'{self._position_marking}{self.name}={self.default}'
elif self.default is None:
return f'{self._position_marking}{self.name}: {self.type_hint}'
else:
return f'{self._position_marking}{self.name}: {self.type_hint}={self.default}'
def make_optional(self) -> None:
if self.type_hint is not None and not self.type_hint.startswith('Optional['):
self.type_hint = f'Optional[{self.type_hint}]'
__slots__ = ('_method_name', 'return_type', '_parameters', '_method_type', )
_method_name: str
return_type: Optional[str]
_parameters: dict[str, Parameter]
_method_type: MethodType
def __init__(self, method_name: str, *parameters: Parameter, return_type: Optional[str] = None, method_type: MethodType = MethodType.METHOD) -> None:
self._parameters = {
p.name: p
for p in parameters
}
self.return_type = return_type.replace('::', '.') if return_type else None
self._method_type = method_type
self.method_name = method_name
@property
def method_name(self) -> str:
return self._method_name
@method_name.setter
def method_name(self, value: str, /) -> None:
self._method_name = magicMethods.get(value, value)
def __getitem__(self, key: Union[str, int]) -> Parameter:
"""Get parameter by name or by index. Indexing is into the paramters skips 'cls' and 'self' for
classmethods and methods.
"""
if isinstance(key, int):
key = list(self._parameters)[key]
if isinstance(key, str):
return self._parameters[key]
else:
raise TypeError(f'Indexing must be via parameter name or index, got {key}')
def __iter__(self):
return iter(self._parameters.values())
def __contains__(self, parameter_name: str) -> bool:
return parameter_name in self._parameters
def args_string(self, typed: bool = True, include_selfcls: bool = False) -> str:
"""Get a string of just the parameters needed for the method signature,
optionally with 'self' or 'cls' where applicable, and type-hints
"""
if include_selfcls and self.is_classmethod:
parameters = (type(self).Parameter('cls'), *self._parameters.values())
elif include_selfcls and self.is_method:
parameters = (type(self).Parameter('self'), *self._parameters.values())
else:
parameters = self._parameters.values()
stringizer = str if typed else type(self).Parameter.untyped
return_type = f' -> {self.return_type}' if self.return_type else ''
return f"({', '.join(map(stringizer, parameters))}){return_type}"
def signature(self, typed: bool = True) -> str:
"""Get the full signature for the function/method, including method and
all required python syntax, optionally including all type-hints.
"""
return f'def {self.method_name}{self.args_string(typed, True)}:'
def __str__(self) -> str:
return self.signature()
def definition_lines(self, typed: bool = True) -> list[str]:
"""return the lines required to write the full method definition,
including decorators
"""
if self.is_staticmethod:
lines = ['@staticmethod']
elif self.is_classmethod:
lines = ['@classmethod']
else:
lines = []
lines.append(self.signature(typed))
return lines
@property
def is_staticmethod(self) -> bool:
return self._method_type is MethodType.STATIC_METHOD
@property
def is_classmethod(self) -> bool:
return self._method_type is MethodType.CLASS_METHOD
@property
def is_method(self) -> bool:
return self._method_type is MethodType.METHOD
@property
def is_function(self) -> bool:
return self._method_type is MethodType.FUNCTION
def removeWxPrefixes(node):
"""
Rename items with a 'wx' prefix to not have the prefix. If the back-end
@@ -85,6 +254,11 @@ class FixWxPrefix(object):
"""
_coreTopLevelNames = None
_auto_conversions: dict[str, Tuple[str, ...]] = {}
@classmethod
def register_autoconversion(cls, class_name: str, convertables: Tuple[str, ...]) -> None:
cls._auto_conversions[class_name] = convertables
def fixWxPrefix(self, name, checkIsCore=False):
# By default remove the wx prefix like normal
@@ -126,7 +300,9 @@ class FixWxPrefix(object):
names.append(item.name)
elif isinstance(item, ast.AnnAssign):
if isinstance(item.target, ast.Name):
names.append(item.target.id)
# Exclude typing TypeAlias's from detection
if not (item.annotation == 'TypeAlias' and item.target.id.startswith('_')):
names.append(item.target.id)
names = list()
filename = 'wx/core.pyi'
@@ -150,7 +326,11 @@ class FixWxPrefix(object):
Finally, the 'wx.' prefix is added if needed.
"""
for txt in ['const', '*', '&', ' ']:
name = re.sub(r'(const(?![\w\d]))', '', name) # remove 'const', but not 'const'raints
replacements = [' ', '*']
if not is_expression:
replacements.extend(['&'])
for txt in replacements:
name = name.replace(txt, '')
name = name.replace('::', '.')
if not is_expression:
@@ -161,9 +341,9 @@ class FixWxPrefix(object):
if fix_wx:
return self.fixWxPrefix(name, True)
else:
return removeWxPrefix(name)
return name
def cleanType(self, type_name: str) -> str:
def cleanType(self, type_name: str, is_input: bool = False) -> 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.
@@ -216,9 +396,13 @@ class FixWxPrefix(object):
return f'List[{type_name}]'
else:
return 'list'
allowed_types = self._auto_conversions.get(type_name, ())
if allowed_types and is_input:
allowed_types = (type_name, *(self.cleanType(t) for t in allowed_types))
type_name = f"Union[{', '.join(allowed_types)}]"
return type_map.get(type_name, type_name)
def parseNameAndType(self, name_string: str, type_string: Optional[str]) -> Tuple[str, Optional[str]]:
def parseNameAndType(self, name_string: str, type_string: Optional[str], is_input: bool = False) -> 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.
@@ -227,7 +411,7 @@ class FixWxPrefix(object):
"""
name_string = self.cleanName(name_string, fix_wx=False)
if type_string:
type_string = self.cleanType(type_string)
type_string = self.cleanType(type_string, is_input)
if type_string == '...':
name_string = '*args'
type_string = None
@@ -856,7 +1040,9 @@ def addGetIMMethodTemplate(module, klass, fields):
def convertTwoIntegersTemplate(CLASS):
# Note: The GIL is already acquired where this code is used.
return """\
return AutoConversionInfo(
('_TwoInts', ),
"""\
// is it just a typecheck?
if (!sipIsErr) {{
// is it already an instance of {CLASS}?
@@ -884,12 +1070,14 @@ def convertTwoIntegersTemplate(CLASS):
Py_DECREF(o1);
Py_DECREF(o2);
return SIP_TEMPORARY;
""".format(**locals())
""".format(**locals()))
def convertFourIntegersTemplate(CLASS):
# Note: The GIL is already acquired where this code is used.
return """\
return AutoConversionInfo(
('_FourInts', ),
"""\
// is it just a typecheck?
if (!sipIsErr) {{
// is it already an instance of {CLASS}?
@@ -921,13 +1109,15 @@ def convertFourIntegersTemplate(CLASS):
Py_DECREF(o3);
Py_DECREF(o4);
return SIP_TEMPORARY;
""".format(**locals())
""".format(**locals()))
def convertTwoDoublesTemplate(CLASS):
# Note: The GIL is already acquired where this code is used.
return """\
return AutoConversionInfo(
('_TwoFloats', ),
"""\
// is it just a typecheck?
if (!sipIsErr) {{
// is it already an instance of {CLASS}?
@@ -955,12 +1145,14 @@ def convertTwoDoublesTemplate(CLASS):
Py_DECREF(o1);
Py_DECREF(o2);
return SIP_TEMPORARY;
""".format(**locals())
""".format(**locals()))
def convertFourDoublesTemplate(CLASS):
# Note: The GIL is already acquired where this code is used.
return """\
return AutoConversionInfo(
('_FourFloats', ),
"""\
// is it just a typecheck?
if (!sipIsErr) {{
// is it already an instance of {CLASS}?
@@ -993,7 +1185,7 @@ def convertFourDoublesTemplate(CLASS):
Py_DECREF(o3);
Py_DECREF(o4);
return SIP_TEMPORARY;
""".format(**locals())
""".format(**locals()))