Skip to content

Commit

Permalink
Implement hinting for types that are automatically converted
Browse files Browse the repository at this point in the history
This handles any type with a defined `.convertFromPyObject` set in its
sip generator.
  • Loading branch information
lojack5 committed Jan 17, 2025
1 parent 47f3ab7 commit 9c920e9
Show file tree
Hide file tree
Showing 8 changed files with 85 additions and 30 deletions.
7 changes: 5 additions & 2 deletions etg/bmpbndl.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import etgtools
import etgtools.tweaker_tools as tools
from etgtools import MethodDef
import etgtools.tweaker_tools

PACKAGE = "wx"
MODULE = "_core"
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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')
Expand Down
7 changes: 5 additions & 2 deletions etg/colour.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

import etgtools
import etgtools.tweaker_tools as tools
import etgtools.tweaker_tools

PACKAGE = "wx"
MODULE = "_core"
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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.")')

Expand Down
7 changes: 5 additions & 2 deletions etg/propgridiface.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

import etgtools
import etgtools.tweaker_tools as tools
import etgtools.tweaker_tools

PACKAGE = "wx"
MODULE = "_propgrid"
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -109,7 +112,7 @@ def run():
SIP_NO_CONVERTORS, 0, sipIsErr));
return 0; // not a new instance
}
"""
""")


#----------------------------------------------------------
Expand Down
13 changes: 9 additions & 4 deletions etg/stream.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

import etgtools
import etgtools.tweaker_tools as tools
import etgtools.tweaker_tools

PACKAGE = "wx"
MODULE = "_core"
Expand Down Expand Up @@ -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))
Expand All @@ -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.
Expand Down Expand Up @@ -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))
Expand All @@ -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
Expand Down
7 changes: 5 additions & 2 deletions etg/wxdatetime.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

import etgtools
import etgtools.tweaker_tools as tools
import etgtools.tweaker_tools

PACKAGE = "wx"
MODULE = "_core"
Expand Down Expand Up @@ -315,7 +316,9 @@ def fixParseMethod(m, code):

# 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))
Expand All @@ -339,7 +342,7 @@ def fixParseMethod(m, code):
sipPy, sipType_wxDateTime, sipTransferObj, SIP_NO_CONVERTORS, 0, sipIsErr));
return 0; // Not a new instance
"""
""")


#---------------------------------------------
Expand Down
22 changes: 17 additions & 5 deletions etgtools/extractors.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@
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

#---------------------------------------------------------------------------
Expand Down Expand Up @@ -501,7 +501,7 @@ def makePyArgsString(self):
# 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()
Expand All @@ -512,7 +512,7 @@ def makePyArgsString(self):
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)
Expand Down Expand Up @@ -691,7 +691,7 @@ def __init__(self, element=None, kind='class', **kw):
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 = []
Expand All @@ -711,6 +711,18 @@ def __init__(self, element=None, kind='class', **kw):
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:
Expand Down
7 changes: 7 additions & 0 deletions etgtools/pi_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@

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
Expand All @@ -89,6 +90,12 @@
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]
"""

#---------------------------------------------------------------------------
Expand Down
45 changes: 32 additions & 13 deletions etgtools/tweaker_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ def args_string(self, typed: bool = True, include_selfcls: bool = False) -> str:
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
Expand Down Expand Up @@ -255,6 +255,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
Expand Down Expand Up @@ -296,7 +301,9 @@ def _processItem(item, names):
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'
Expand Down Expand Up @@ -341,7 +348,7 @@ def cleanName(self, name: str, is_expression: bool = False, fix_wx: bool = True)
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.
Expand Down Expand Up @@ -394,9 +401,13 @@ def cleanType(self, type_name: str) -> str:
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.
Expand All @@ -405,7 +416,7 @@ def parseNameAndType(self, name_string: str, type_string: Optional[str]) -> Tupl
"""
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
Expand Down Expand Up @@ -1030,7 +1041,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}?
Expand Down Expand Up @@ -1058,12 +1071,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}?
Expand Down Expand Up @@ -1095,13 +1110,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}?
Expand Down Expand Up @@ -1129,12 +1146,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}?
Expand Down Expand Up @@ -1167,7 +1186,7 @@ def convertFourDoublesTemplate(CLASS):
Py_DECREF(o3);
Py_DECREF(o4);
return SIP_TEMPORARY;
""".format(**locals())
""".format(**locals()))



Expand Down

0 comments on commit 9c920e9

Please sign in to comment.