Implement hinting for types that are automatically converted

This handles any type with a defined `.convertFromPyObject` set in its
sip generator.
This commit is contained in:
lojack5
2025-01-17 00:23:08 -07:00
parent 03d7f1e8c9
commit bca7d10602
8 changed files with 85 additions and 30 deletions

View File

@@ -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<wxBitmapBundle*>(
sipConvertToType(sipPy, sipType_wxBitmapBundle, sipTransferObj, SIP_NO_CONVERTORS, 0, sipIsErr));
return 0; // not a new instance
"""
""")
c = module.find('wxBitmapBundleImpl')

View File

@@ -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<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

@@ -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
}
"""
""")
#----------------------------------------------------------

View File

@@ -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

View File

@@ -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
"""
""")
#---------------------------------------------

View File

@@ -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:

View File

@@ -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]
"""
#---------------------------------------------------------------------------

View File

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