From 62f820a8cf0c4c0e819dcc546f1bce3d5b601fa2 Mon Sep 17 00:00:00 2001 From: lojack5 <1458329+lojack5@users.noreply.github.com> Date: Sat, 11 Jan 2025 21:44:57 -0700 Subject: [PATCH 01/14] Fixups for buildings docs (sphinx_generator): - Remove typehints from argsString before checking signature - lie and detect `_from`, `_is`, and `_def` as `from`, `is`, and `def for signature validating purposes - Fix: don't remove const from identifiers containing const (eg: constraint) --- etgtools/extractors.py | 2 +- etgtools/sphinx_generator.py | 4 +++- etgtools/tweaker_tools.py | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/etgtools/extractors.py b/etgtools/extractors.py index bb9ce465..2d9d0c42 100644 --- a/etgtools/extractors.py +++ b/etgtools/extractors.py @@ -538,7 +538,7 @@ class FunctionDef(BaseDef, FixWxPrefix): default = '|'.join([self.cleanName(x, True) for x in default.split('|')]) s = f'{s}={default}' elif param_type: - s = f'{s} : {param_type}' + s = f'{s}: {param_type}' params.append(s) self.pyArgsString = f"({', '.join(params)})" diff --git a/etgtools/sphinx_generator.py b/etgtools/sphinx_generator.py index b41768b3..b1b333f5 100644 --- a/etgtools/sphinx_generator.py +++ b/etgtools/sphinx_generator.py @@ -622,7 +622,9 @@ class ParameterList(Node): theargs = [] for arg in arguments: - + arg = arg.split(':')[0].strip() # Remove the typehint + if arg in ('_from', '_def', '_is'): # Reserved Python keywords we've had to rename + arg = arg[1:] myarg = arg.split('=')[0].strip() if myarg: theargs.append(myarg) diff --git a/etgtools/tweaker_tools.py b/etgtools/tweaker_tools.py index dc74b646..8dfd3745 100644 --- a/etgtools/tweaker_tools.py +++ b/etgtools/tweaker_tools.py @@ -150,7 +150,8 @@ 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 + for txt in ['*', '&', ' ']: name = name.replace(txt, '') name = name.replace('::', '.') if not is_expression: From 1fe85a89079f7f0f53d1fd8ccd112a77cda752d8 Mon Sep 17 00:00:00 2001 From: lojack5 <1458329+lojack5@users.noreply.github.com> Date: Sat, 11 Jan 2025 22:27:05 -0700 Subject: [PATCH 02/14] mark stubs for `__bool__` and `__nonzero__` as returning `bool`. --- etgtools/extractors.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/etgtools/extractors.py b/etgtools/extractors.py index 2d9d0c42..193b9e02 100644 --- a/etgtools/extractors.py +++ b/etgtools/extractors.py @@ -542,6 +542,10 @@ class FunctionDef(BaseDef, FixWxPrefix): params.append(s) self.pyArgsString = f"({', '.join(params)})" + # __bool__ and __nonzero__ need to be defined as returning int for SIP, but for Python + # __bool__ is required to return a bool: + if (self.name or self.pyName) in ('__bool__', '__nonzero__'): + returns = ['bool'] if not returns: self.pyArgsString = f'{self.pyArgsString} -> None' elif len(returns) == 1: From c6cd496e7d31d3c158018063c29790002126929a Mon Sep 17 00:00:00 2001 From: lojack5 <1458329+lojack5@users.noreply.github.com> Date: Sat, 11 Jan 2025 23:21:06 -0700 Subject: [PATCH 03/14] Mark subclasses of `wx.TopLevelWindow` as allowing `None` for their `parent` argument. --- etgtools/extractors.py | 9 +++++++++ etgtools/pi_generator.py | 27 ++++++++++++++++++++++++--- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/etgtools/extractors.py b/etgtools/extractors.py index 193b9e02..f452b9b9 100644 --- a/etgtools/extractors.py +++ b/etgtools/extractors.py @@ -717,6 +717,15 @@ class ClassDef(BaseDef): if element is not None: self.extract(element) + 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 diff --git a/etgtools/pi_generator.py b/etgtools/pi_generator.py index 8c1bd157..05689f79 100644 --- a/etgtools/pi_generator.py +++ b/etgtools/pi_generator.py @@ -517,7 +517,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 @@ -564,7 +565,18 @@ class PiWrapperGenerator(generators.WrapperGeneratorBase, FixWxPrefix): 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: ` -> `parent: Optional[]`, 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): @@ -582,7 +594,7 @@ class PiWrapperGenerator(generators.WrapperGeneratorBase, FixWxPrefix): # 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') @@ -601,6 +613,15 @@ class PiWrapperGenerator(generators.WrapperGeneratorBase, FixWxPrefix): else: argsString = '(self, ' + argsString[1:] argsString = argsString.replace('::', '.') + if is_top_level_init: + # For classes derived from TopLevelWindow, the parent argument is allowed + # to be None. If the argsString isn't already marked this way due to + # having `=None` in it, add the Optional[type] typehint to it. + argsString = re.sub( + r'([,(]\s*)parent\s*:\s*((?!Optional\[)(\w[\w\d_\.]*))\s*([,)=])', + r'\g<1>parent: Optional[\g<3>]\g<4>', + argsString, + ) stream.write(argsString) stream.write(':\n') indent2 = indent + ' '*4 From 58d5b6a27dccfd4decb42ec3bbe2d525391e9604 Mon Sep 17 00:00:00 2001 From: lojack5 <1458329+lojack5@users.noreply.github.com> Date: Sat, 11 Jan 2025 23:22:57 -0700 Subject: [PATCH 04/14] mark values `#define`ed as `true` or `false` in the type-stubs as `bool`. --- etgtools/pi_generator.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/etgtools/pi_generator.py b/etgtools/pi_generator.py index 05689f79..3aed6235 100644 --- a/etgtools/pi_generator.py +++ b/etgtools/pi_generator.py @@ -295,6 +295,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') From 86546a42301dd746577caa79359b7f9f658f9006 Mon Sep 17 00:00:00 2001 From: lojack5 <1458329+lojack5@users.noreply.github.com> Date: Sun, 12 Jan 2025 01:54:12 -0700 Subject: [PATCH 05/14] generate properties in stubs using decorator syntax where possible --- etgtools/pi_generator.py | 58 ++++++++++++++++++++++++++++++++-------- 1 file changed, 47 insertions(+), 11 deletions(-) diff --git a/etgtools/pi_generator.py b/etgtools/pi_generator.py index 3aed6235..0bb4e6ff 100644 --- a/etgtools/pi_generator.py +++ b/etgtools/pi_generator.py @@ -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 @@ -390,7 +390,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, } @@ -500,8 +500,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, @@ -535,6 +535,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): @@ -547,23 +556,50 @@ 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 '->' in getter.pyArgsString: + value_type = getter.pyArgsString.split('->')[1].strip() + if prop.setter: + setter = self.find_method(klass, prop.setter) + if setter: + args = setter.pyArgsString.split('->')[0] + args = args.strip().strip('()') + args = args.split(',') + if args: + value_arg = args[0] + if ':' in value_arg: + value_type = value_arg.split(':')[1].strip() + value_type = value_type.split('=')[0] 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') From 58317175cda2686407f575264427d1d36503c2db Mon Sep 17 00:00:00 2001 From: lojack5 <1458329+lojack5@users.noreply.github.com> Date: Sun, 12 Jan 2025 03:27:53 -0700 Subject: [PATCH 06/14] Correct incorrect remove of `wx` from argument names --- etgtools/tweaker_tools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/etgtools/tweaker_tools.py b/etgtools/tweaker_tools.py index 8dfd3745..ff52b1b7 100644 --- a/etgtools/tweaker_tools.py +++ b/etgtools/tweaker_tools.py @@ -162,7 +162,7 @@ class FixWxPrefix(object): if fix_wx: return self.fixWxPrefix(name, True) else: - return removeWxPrefix(name) + return name def cleanType(self, type_name: str) -> str: """Process a C++ type name for use as a type annotation in Python code. From e76f298bf19bec9563499c161bc5820632d814ed Mon Sep 17 00:00:00 2001 From: lojack5 <1458329+lojack5@users.noreply.github.com> Date: Sun, 12 Jan 2025 04:33:22 -0700 Subject: [PATCH 07/14] Fix for RichTextLineVector: This one has probably been in the generated type-stub for ages. One of the base classes in the generated stub was generated as `RichTextLine*`. --- etgtools/pi_generator.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/etgtools/pi_generator.py b/etgtools/pi_generator.py index 0bb4e6ff..219ec029 100644 --- a/etgtools/pi_generator.py +++ b/etgtools/pi_generator.py @@ -322,7 +322,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 From 03d7f1e8c9b04ac48da556f48999a6b730730db1 Mon Sep 17 00:00:00 2001 From: lojack5 <1458329+lojack5@users.noreply.github.com> Date: Thu, 16 Jan 2025 22:15:13 -0700 Subject: [PATCH 08/14] reworks how typed-argstrings are handled Pulls out the data into a separate class, `tweaker_tools.Signature`. This simplifies stringizing the args string, and allows for some checks to be written in a much less complex way. The motivator for this was adding unions to type-hints (for the upcoming automatically converted python->wx types). This made parsing the already stringized args string very complex to be able to handle the potential for commas in `Union`. --- etgtools/extractors.py | 75 ++++++++------- etgtools/pi_generator.py | 70 ++++---------- etgtools/sphinx_generator.py | 62 +++--------- etgtools/tweaker_tools.py | 176 ++++++++++++++++++++++++++++++++++- 4 files changed, 245 insertions(+), 138 deletions(-) diff --git a/etgtools/extractors.py b/etgtools/extractors.py index f452b9b9..a87e462b 100644 --- a/etgtools/extractors.py +++ b/etgtools/extractors.py @@ -15,12 +15,13 @@ wxWidgets API info which we need from them. import sys import os import pprint -import xml.etree.ElementTree as ET +from typing import Optional +import xml.etree.ElementTree as et import copy -from .tweaker_tools import FixWxPrefix, magicMethods, \ +from .tweaker_tools import FixWxPrefix, MethodType, magicMethods, \ guessTypeInt, guessTypeFloat, guessTypeStr, \ - textfile_open + textfile_open, Signature from sphinxtools.utilities import findDescendants if sys.version_info >= (3, 11): @@ -285,6 +286,7 @@ class FunctionDef(BaseDef, FixWxPrefix): self.type = None self.definition = '' self.argsString = '' + self.signature: Optional[Signature] = None self.pyArgsString = '' self.isOverloaded = False self.overloads = [] @@ -406,7 +408,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 +475,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 +488,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 +501,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) + params.append(P(arg, arg_type, default)) + if default == 'None': + params[-1].make_optional() else: for param in self.items: assert isinstance(param, ParamDef) @@ -523,35 +522,35 @@ class FunctionDef(BaseDef, FixWxPrefix): 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)})" + params.append(P(s, param_type, default)) + if default == 'None': + params[-1].make_optional() + if getattr(self, 'isCtor', False): + name = '__init__' + else: + name = self.name or self.pyName + 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 (self.name or self.pyName) in ('__bool__', '__nonzero__'): - returns = ['bool'] - if not returns: - self.pyArgsString = f'{self.pyArgsString} -> None' + 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 MethodType.METHOD + self.signature = Signature(name, *params, return_type=return_type, method_type=kind) + self.pyArgsString = self.signature.args_string(False) def collectPySignatures(self): @@ -1283,7 +1282,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 diff --git a/etgtools/pi_generator.py b/etgtools/pi_generator.py index 219ec029..5cca2c52 100644 --- a/etgtools/pi_generator.py +++ b/etgtools/pi_generator.py @@ -413,16 +413,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: @@ -572,19 +567,12 @@ class PiWrapperGenerator(generators.WrapperGeneratorBase, FixWxPrefix): value_type = '' if prop.getter: getter = self.find_method(klass, prop.getter) - if getter and '->' in getter.pyArgsString: - value_type = getter.pyArgsString.split('->')[1].strip() + if getter and getter.signature: + value_type = getter.signature.return_type if prop.setter: setter = self.find_method(klass, prop.setter) - if setter: - args = setter.pyArgsString.split('->')[0] - args = args.strip().strip('()') - args = args.split(',') - if args: - value_arg = args[0] - if ':' in value_arg: - value_type = value_arg.split(':')[1].strip() - value_type = value_type.split('=')[0] + if setter and setter.signature: + value_type = setter.signature[0].type_hint if prop.setter and prop.getter: if value_type: stream.write(f'{indent}@property\n') @@ -626,10 +614,6 @@ 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: @@ -637,32 +621,16 @@ class PiWrapperGenerator(generators.WrapperGeneratorBase, FixWxPrefix): 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('::', '.') - if is_top_level_init: - # For classes derived from TopLevelWindow, the parent argument is allowed - # to be None. If the argsString isn't already marked this way due to - # having `=None` in it, add the Optional[type] typehint to it. - argsString = re.sub( - r'([,(]\s*)parent\s*:\s*((?!Optional\[)(\w[\w\d_\.]*))\s*([,)=])', - r'\g<1>parent: Optional[\g<3>]\g<4>', - argsString, - ) - 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 diff --git a/etgtools/sphinx_generator.py b/etgtools/sphinx_generator.py index b1b333f5..4ed8087d 100644 --- a/etgtools/sphinx_generator.py +++ b/etgtools/sphinx_generator.py @@ -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,39 +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: - arg = arg.split(':')[0].strip() # Remove the typehint - if arg in ('_from', '_def', '_is'): # Reserved Python keywords we've had to rename - arg = arg[1:] - 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)) - # ----------------------------------------------------------------------- diff --git a/etgtools/tweaker_tools.py b/etgtools/tweaker_tools.py index ff52b1b7..0e6f38fe 100644 --- a/etgtools/tweaker_tools.py +++ b/etgtools/tweaker_tools.py @@ -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 @@ -151,7 +320,10 @@ class FixWxPrefix(object): Finally, the 'wx.' prefix is added if needed. """ name = re.sub(r'(const(?![\w\d]))', '', name) # remove 'const', but not 'const'raints - for txt in ['*', '&', ' ']: + replacements = [' ', '*'] + if not is_expression: + replacements.extend(['&']) + for txt in replacements: name = name.replace(txt, '') name = name.replace('::', '.') if not is_expression: From bca7d1060227b784bfe3c5119fc5ae3fdb0fc18f Mon Sep 17 00:00:00 2001 From: lojack5 <1458329+lojack5@users.noreply.github.com> Date: Fri, 17 Jan 2025 00:23:08 -0700 Subject: [PATCH 09/14] Implement hinting for types that are automatically converted This handles any type with a defined `.convertFromPyObject` set in its sip generator. --- etg/bmpbndl.py | 7 ++++-- etg/colour.py | 7 ++++-- etg/propgridiface.py | 7 ++++-- etg/stream.py | 13 +++++++---- etg/wxdatetime.py | 7 ++++-- etgtools/extractors.py | 22 ++++++++++++++----- etgtools/pi_generator.py | 7 ++++++ etgtools/tweaker_tools.py | 45 ++++++++++++++++++++++++++++----------- 8 files changed, 85 insertions(+), 30 deletions(-) diff --git a/etg/bmpbndl.py b/etg/bmpbndl.py index a15fb1df..7af51eca 100644 --- a/etg/bmpbndl.py +++ b/etg/bmpbndl.py @@ -10,6 +10,7 @@ import etgtools import etgtools.tweaker_tools as tools from etgtools import MethodDef +import etgtools.tweaker_tools PACKAGE = "wx" MODULE = "_core" @@ -43,7 +44,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 = etgtools.tweaker_tools.AutoConversionInfo( + ('wx.Bitmap', 'wx.Icon', ), + """\ // Check for type compatibility if (!sipIsErr) { if (sipCanConvertToType(sipPy, sipType_wxBitmap, SIP_NO_CONVERTORS)) @@ -86,7 +89,7 @@ def run(): *sipCppPtr = reinterpret_cast( sipConvertToType(sipPy, sipType_wxBitmapBundle, sipTransferObj, SIP_NO_CONVERTORS, 0, sipIsErr)); return 0; // not a new instance - """ + """) c = module.find('wxBitmapBundleImpl') diff --git a/etg/colour.py b/etg/colour.py index e33c707f..cdad3365 100644 --- a/etg/colour.py +++ b/etg/colour.py @@ -9,6 +9,7 @@ import etgtools import etgtools.tweaker_tools as tools +import etgtools.tweaker_tools PACKAGE = "wx" MODULE = "_core" @@ -192,7 +193,9 @@ def run(): # String with color name or #RRGGBB or #RRGGBBAA format # None (converts to wxNullColour) c.allowNone = True - c.convertFromPyObject = """\ + c.convertFromPyObject = etgtools.tweaker_tools.AutoConversionInfo( + ('wx.Colour', '_ThreeInts', '_FourInts', 'str', 'None'), + """\ // is it just a typecheck? if (!sipIsErr) { if (sipPy == Py_None) @@ -273,7 +276,7 @@ def run(): *sipCppPtr = reinterpret_cast(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.")') diff --git a/etg/propgridiface.py b/etg/propgridiface.py index 9425f84e..5e803b52 100644 --- a/etg/propgridiface.py +++ b/etg/propgridiface.py @@ -9,6 +9,7 @@ import etgtools import etgtools.tweaker_tools as tools +import etgtools.tweaker_tools PACKAGE = "wx" MODULE = "_propgrid" @@ -70,7 +71,9 @@ def run(): c.find('GetPtr').overloads[0].ignore() - c.convertFromPyObject = """\ + c.convertFromPyObject = etgtools.tweaker_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 +112,7 @@ def run(): SIP_NO_CONVERTORS, 0, sipIsErr)); return 0; // not a new instance } - """ + """) #---------------------------------------------------------- diff --git a/etg/stream.py b/etg/stream.py index 2b817bb9..00125a86 100644 --- a/etg/stream.py +++ b/etg/stream.py @@ -9,6 +9,7 @@ import etgtools import etgtools.tweaker_tools as tools +import etgtools.tweaker_tools PACKAGE = "wx" MODULE = "_core" @@ -76,7 +77,9 @@ def run(): c.includeCppCode('src/stream_input.cpp') # Use that class for the convert code - c.convertFromPyObject = """\ + c.convertFromPyObject = etgtools.tweaker_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 +89,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 +239,9 @@ def run(): c.includeCppCode('src/stream_output.cpp') # Use that class for the convert code - c.convertFromPyObject = """\ + c.convertFromPyObject = etgtools.tweaker_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 +251,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 diff --git a/etg/wxdatetime.py b/etg/wxdatetime.py index eb18c8ed..acaa94c6 100644 --- a/etg/wxdatetime.py +++ b/etg/wxdatetime.py @@ -9,6 +9,7 @@ import etgtools import etgtools.tweaker_tools as tools +import etgtools.tweaker_tools PACKAGE = "wx" MODULE = "_core" @@ -311,7 +312,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 = etgtools.tweaker_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 +338,7 @@ def run(): sipPy, sipType_wxDateTime, sipTransferObj, SIP_NO_CONVERTORS, 0, sipIsErr)); return 0; // Not a new instance - """ + """) #--------------------------------------------- diff --git a/etgtools/extractors.py b/etgtools/extractors.py index a87e462b..a7d6a30d 100644 --- a/etgtools/extractors.py +++ b/etgtools/extractors.py @@ -19,9 +19,9 @@ from typing import Optional import xml.etree.ElementTree as et import copy -from .tweaker_tools import FixWxPrefix, MethodType, magicMethods, \ +from .tweaker_tools import AutoConversionInfo, FixWxPrefix, MethodType, magicMethods, \ guessTypeInt, guessTypeFloat, guessTypeStr, \ - textfile_open, Signature + textfile_open, Signature, removeWxPrefix from sphinxtools.utilities import findDescendants if sys.version_info >= (3, 11): @@ -506,7 +506,7 @@ class FunctionDef(BaseDef, FixWxPrefix): # 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) + arg, arg_type = self.parseNameAndType(arg, arg_type, True) params.append(P(arg, arg_type, default)) if default == 'None': params[-1].make_optional() @@ -517,7 +517,7 @@ 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) @@ -696,7 +696,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 = [] @@ -716,6 +716,18 @@ 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.name or self.pyName + name = removeWxPrefix(name) + print('Registering:', name, value.convertables) + 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: diff --git a/etgtools/pi_generator.py b/etgtools/pi_generator.py index 5cca2c52..840115ea 100644 --- a/etgtools/pi_generator.py +++ b/etgtools/pi_generator.py @@ -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] + """ #--------------------------------------------------------------------------- diff --git a/etgtools/tweaker_tools.py b/etgtools/tweaker_tools.py index 0e6f38fe..e76f522a 100644 --- a/etgtools/tweaker_tools.py +++ b/etgtools/tweaker_tools.py @@ -168,7 +168,7 @@ class Signature: 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}' + 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 @@ -254,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 @@ -295,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' @@ -336,7 +343,7 @@ class FixWxPrefix(object): else: 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. @@ -389,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. @@ -400,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 @@ -1029,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}? @@ -1057,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}? @@ -1094,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}? @@ -1128,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}? @@ -1166,7 +1185,7 @@ def convertFourDoublesTemplate(CLASS): Py_DECREF(o3); Py_DECREF(o4); return SIP_TEMPORARY; - """.format(**locals()) + """.format(**locals())) From fa2d1af34063210cf5106ca2ee6f2b570a61a196 Mon Sep 17 00:00:00 2001 From: lojack5 <1458329+lojack5@users.noreply.github.com> Date: Fri, 17 Jan 2025 01:46:05 -0700 Subject: [PATCH 10/14] Squash - cleanup imports --- etg/bmpbndl.py | 3 +-- etg/colour.py | 3 +-- etg/propgridiface.py | 3 +-- etg/stream.py | 5 ++--- etg/wxdatetime.py | 3 +-- 5 files changed, 6 insertions(+), 11 deletions(-) diff --git a/etg/bmpbndl.py b/etg/bmpbndl.py index 7af51eca..ee8a118d 100644 --- a/etg/bmpbndl.py +++ b/etg/bmpbndl.py @@ -10,7 +10,6 @@ import etgtools import etgtools.tweaker_tools as tools from etgtools import MethodDef -import etgtools.tweaker_tools PACKAGE = "wx" MODULE = "_core" @@ -44,7 +43,7 @@ def run(): # Allow on-the-fly creation of a wx.BitmapBundle from a wx.Bitmap, wx.Icon # or a wx.Image - c.convertFromPyObject = etgtools.tweaker_tools.AutoConversionInfo( + c.convertFromPyObject = tools.AutoConversionInfo( ('wx.Bitmap', 'wx.Icon', ), """\ // Check for type compatibility diff --git a/etg/colour.py b/etg/colour.py index cdad3365..be45703f 100644 --- a/etg/colour.py +++ b/etg/colour.py @@ -9,7 +9,6 @@ import etgtools import etgtools.tweaker_tools as tools -import etgtools.tweaker_tools PACKAGE = "wx" MODULE = "_core" @@ -193,7 +192,7 @@ def run(): # String with color name or #RRGGBB or #RRGGBBAA format # None (converts to wxNullColour) c.allowNone = True - c.convertFromPyObject = etgtools.tweaker_tools.AutoConversionInfo( + c.convertFromPyObject = tools.AutoConversionInfo( ('wx.Colour', '_ThreeInts', '_FourInts', 'str', 'None'), """\ // is it just a typecheck? diff --git a/etg/propgridiface.py b/etg/propgridiface.py index 5e803b52..da4feaed 100644 --- a/etg/propgridiface.py +++ b/etg/propgridiface.py @@ -9,7 +9,6 @@ import etgtools import etgtools.tweaker_tools as tools -import etgtools.tweaker_tools PACKAGE = "wx" MODULE = "_propgrid" @@ -71,7 +70,7 @@ def run(): c.find('GetPtr').overloads[0].ignore() - c.convertFromPyObject = etgtools.tweaker_tools.AutoConversionInfo( + c.convertFromPyObject = tools.AutoConversionInfo( ('str', 'None', ), """\ // Code to test a PyObject for compatibility with wxPGPropArgCls diff --git a/etg/stream.py b/etg/stream.py index 00125a86..093c7903 100644 --- a/etg/stream.py +++ b/etg/stream.py @@ -9,7 +9,6 @@ import etgtools import etgtools.tweaker_tools as tools -import etgtools.tweaker_tools PACKAGE = "wx" MODULE = "_core" @@ -77,7 +76,7 @@ def run(): c.includeCppCode('src/stream_input.cpp') # Use that class for the convert code - c.convertFromPyObject = etgtools.tweaker_tools.AutoConversionInfo( + c.convertFromPyObject = tools.AutoConversionInfo( (), # TODO: Track down what python types actually can be wrapped """\ // is it just a typecheck? @@ -239,7 +238,7 @@ def run(): c.includeCppCode('src/stream_output.cpp') # Use that class for the convert code - c.convertFromPyObject = etgtools.tweaker_tools.AutoConversionInfo( + c.convertFromPyObject = tools.AutoConversionInfo( (), # TODO: Track down what python types can actually be converted """\ // is it just a typecheck? diff --git a/etg/wxdatetime.py b/etg/wxdatetime.py index acaa94c6..bb4e681b 100644 --- a/etg/wxdatetime.py +++ b/etg/wxdatetime.py @@ -9,7 +9,6 @@ import etgtools import etgtools.tweaker_tools as tools -import etgtools.tweaker_tools PACKAGE = "wx" MODULE = "_core" @@ -312,7 +311,7 @@ def run(): # Add some code (like MappedTypes) to automatically convert from a Python # datetime.date or a datetime.datetime object - c.convertFromPyObject = etgtools.tweaker_tools.AutoConversionInfo( + c.convertFromPyObject = tools.AutoConversionInfo( ('datetime', 'date', ), """\ // Code to test a PyObject for compatibility with wxDateTime From 2071ae765de2fba07f5d82d2991fade8a5903c16 Mon Sep 17 00:00:00 2001 From: lojack5 <1458329+lojack5@users.noreply.github.com> Date: Fri, 17 Jan 2025 03:18:41 -0700 Subject: [PATCH 11/14] Fixup: functions were being generated as if they were methods (had a 'self' parameter) --- etgtools/extractors.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/etgtools/extractors.py b/etgtools/extractors.py index a7d6a30d..4f6cd265 100644 --- a/etgtools/extractors.py +++ b/etgtools/extractors.py @@ -281,6 +281,8 @@ 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 @@ -548,7 +550,7 @@ class FunctionDef(BaseDef, FixWxPrefix): return_type = returns[0] else: return_type = f"Tuple[{', '.join(returns)}]" - kind = MethodType.STATIC_METHOD if getattr(self, 'isStatic', False) else MethodType.METHOD + 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) @@ -587,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 @@ -725,7 +729,6 @@ class ClassDef(BaseDef): self._convertFromPyObject = value.code name = self.name or self.pyName name = removeWxPrefix(name) - print('Registering:', name, value.convertables) FixWxPrefix.register_autoconversion(name, value.convertables) def is_top_level(self) -> bool: From 68f75d28dcb94982be5dcf1ab076933e13e6d7e3 Mon Sep 17 00:00:00 2001 From: lojack5 <1458329+lojack5@users.noreply.github.com> Date: Fri, 17 Jan 2025 03:29:41 -0700 Subject: [PATCH 12/14] Fixup: using incorrect method names in some cases where they have been renamed --- etgtools/extractors.py | 6 ++++-- etgtools/tweaker_tools.py | 6 +++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/etgtools/extractors.py b/etgtools/extractors.py index 4f6cd265..132bb171 100644 --- a/etgtools/extractors.py +++ b/etgtools/extractors.py @@ -538,8 +538,10 @@ class FunctionDef(BaseDef, FixWxPrefix): if getattr(self, 'isCtor', False): name = '__init__' else: - name = self.name or self.pyName + name = self.pyName or self.name name = self.fixWxPrefix(name) + if 'Destroy' in (name, self.name, self.pyName): + print(f'Generating signature for: {name}, {self.name}, {self.pyName}') # __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__'): @@ -727,7 +729,7 @@ class ClassDef(BaseDef): @convertFromPyObject.setter def convertFromPyObject(self, value: AutoConversionInfo) -> None: self._convertFromPyObject = value.code - name = self.name or self.pyName + name = self.pyName or self.name name = removeWxPrefix(name) FixWxPrefix.register_autoconversion(name, value.convertables) diff --git a/etgtools/tweaker_tools.py b/etgtools/tweaker_tools.py index e76f522a..58ca5f3a 100644 --- a/etgtools/tweaker_tools.py +++ b/etgtools/tweaker_tools.py @@ -42,7 +42,7 @@ magicMethods = { class AutoConversionInfo(NamedTuple): - convertables: tuple[str, ...] # String type-hints for each of the types that can be automatically converted to this class + 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 @@ -254,10 +254,10 @@ class FixWxPrefix(object): """ _coreTopLevelNames = None - _auto_conversions: dict[str, tuple[str, ...]] = {} + _auto_conversions: dict[str, Tuple[str, ...]] = {} @classmethod - def register_autoconversion(cls, class_name: str, convertables: tuple[str, ...]) -> None: + def register_autoconversion(cls, class_name: str, convertables: Tuple[str, ...]) -> None: cls._auto_conversions[class_name] = convertables def fixWxPrefix(self, name, checkIsCore=False): From 3c6854c138b4f460d151786fc01509a5a73948a3 Mon Sep 17 00:00:00 2001 From: lojack5 <1458329+lojack5@users.noreply.github.com> Date: Fri, 17 Jan 2025 03:40:41 -0700 Subject: [PATCH 13/14] squash --- etgtools/extractors.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/etgtools/extractors.py b/etgtools/extractors.py index 132bb171..a813bf9f 100644 --- a/etgtools/extractors.py +++ b/etgtools/extractors.py @@ -540,8 +540,6 @@ class FunctionDef(BaseDef, FixWxPrefix): else: name = self.pyName or self.name name = self.fixWxPrefix(name) - if 'Destroy' in (name, self.name, self.pyName): - print(f'Generating signature for: {name}, {self.name}, {self.pyName}') # __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__'): From d92db14141a72c4053e070ddf48568ee2c3ccdb1 Mon Sep 17 00:00:00 2001 From: lojack5 <1458329+lojack5@users.noreply.github.com> Date: Thu, 13 Mar 2025 23:00:49 -0600 Subject: [PATCH 14/14] Fixup from conflict resolution --- etgtools/extractors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/etgtools/extractors.py b/etgtools/extractors.py index a813bf9f..3608bc9e 100644 --- a/etgtools/extractors.py +++ b/etgtools/extractors.py @@ -16,7 +16,7 @@ import sys import os import pprint from typing import Optional -import xml.etree.ElementTree as et +import xml.etree.ElementTree as ET import copy from .tweaker_tools import AutoConversionInfo, FixWxPrefix, MethodType, magicMethods, \