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()))