From b64fb4b20a029f4c3bf3bf994c45739c3277fdb7 Mon Sep 17 00:00:00 2001 From: Leilei Zhang Date: Thu, 24 Sep 2026 14:57:10 +0800 Subject: [PATCH 1/3] Model every PropertyType and add the Python Object conversion layer EXPERIMENTAL prototype for design evaluation; not for merge. Core (crates/dynwinrt/src/property_value.rs) now models every Windows.Foundation.PropertyType that has a payload (37 of 41): adds DateTime, TimeSpan, Point, Size, Rect, their arrays and InspectableArray. unbox_property_value classifies payload-less boxes as Unsupported instead of failing, and box_property_value creates system PropertyValue boxes. The JS binding keeps its exact previous behavior through a compile-only adapter. The Python binding gains one conversion layer (src/object_value.rs): to_winrt_object / from_winrt_object, with unbox_object as the same function. A public value model (python/dynwinrt/_values.py) adds PropertyType-tagged scalars (UInt8 ... Char16), typed arrays, immutable Point/Size/Rect and the WinRTObjectValue / WinRTObjectInput aliases, so values read from Object positions write back with their exact WinRT type. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- bindings/js/__test__/index.spec.ts | 5 +- bindings/js/src/property_value.rs | 25 + bindings/py/README.md | 79 +- bindings/py/dynwinrt.pyi | 195 +++- bindings/py/python/dynwinrt/_values.py | 414 +++++++ bindings/py/src/implementation.rs | 28 +- bindings/py/src/lib.rs | 11 +- bindings/py/src/object_value.rs | 1309 +++++++++++++++++++++++ bindings/py/src/package_source.rs | 34 + bindings/py/src/runtime.rs | 97 +- bindings/py/stubtest_allowlist.txt | 2 + bindings/py/tests/test_e2e_winrt.py | 11 +- bindings/py/tests/test_object_values.py | 490 +++++++++ crates/dynwinrt/src/lib.rs | 2 +- crates/dynwinrt/src/property_value.rs | 473 ++++++-- 15 files changed, 2925 insertions(+), 250 deletions(-) create mode 100644 bindings/py/python/dynwinrt/_values.py create mode 100644 bindings/py/src/object_value.rs create mode 100644 bindings/py/src/package_source.rs create mode 100644 bindings/py/tests/test_object_values.py diff --git a/bindings/js/__test__/index.spec.ts b/bindings/js/__test__/index.spec.ts index f9e7b134..f6d97b02 100644 --- a/bindings/js/__test__/index.spec.ts +++ b/bindings/js/__test__/index.spec.ts @@ -1486,7 +1486,10 @@ test('explicitly unbox WinRT property values without changing raw objects', (t) const dateTime = DynWinRtStruct.create(DynWinRtType.structType('Windows.Foundation.DateTime', [DynWinRtType.i64()])) dateTime.setI64(0, 0n) const unsupported = staticsType.methodByName('CreateDateTime').invoke(factory, [dateTime.toValue()]) - t.throws(() => unboxObject(unsupported), { message: /Unsupported WinRT IPropertyValue type/ }) + // The core now models DateTime; JavaScript keeps its exact pre-existing error. + t.throws(() => unboxObject(unsupported), { + message: '0x80004001: Unsupported WinRT IPropertyValue type: 14 (0x80004001)', + }) const keyType = DynWinRtType.hstring() const propertyMap = DynWinRtValue.createMap( diff --git a/bindings/js/src/property_value.rs b/bindings/js/src/property_value.rs index 86457739..54b9a8b6 100644 --- a/bindings/js/src/property_value.rs +++ b/bindings/js/src/property_value.rs @@ -55,6 +55,17 @@ fn null_unknown<'env>(env: Env) -> napi::Result> { unsafe { Unknown::from_napi_value(env.raw(), value) } } +/// JavaScript does not project these PropertyTypes yet. Report them with the +/// exact error the core raised before it modeled every PropertyType, so the +/// JavaScript contract stays unchanged. +fn unsupported_property_type(property_type: windows::Foundation::PropertyType) -> napi::Error { + let error = dynwinrt::Error::WindowsError(windows::core::Error::new( + windows::core::HRESULT(0x80004001_u32 as i32), + format!("Unsupported WinRT IPropertyValue type: {}", property_type.0), + )); + napi::Error::from_reason(error.message()) +} + fn property_value_to_javascript<'env>( env: Env, value: dynwinrt::PropertyValueData, @@ -104,6 +115,17 @@ fn property_value_to_javascript<'env>( .map(property_value_guid) .collect::>(), ), + value @ (PropertyValueData::DateTime(_) + | PropertyValueData::TimeSpan(_) + | PropertyValueData::Point(_) + | PropertyValueData::Size(_) + | PropertyValueData::Rect(_) + | PropertyValueData::InspectableArray(_) + | PropertyValueData::DateTimeArray(_) + | PropertyValueData::TimeSpanArray(_) + | PropertyValueData::PointArray(_) + | PropertyValueData::SizeArray(_) + | PropertyValueData::RectArray(_)) => Err(unsupported_property_type(value.property_type())), } } @@ -129,6 +151,9 @@ pub fn unbox_object<'env>(env: Env, value: Unknown<'env>) -> napi::Result null_unknown(env), dynwinrt::PropertyValueUnboxResult::NotPropertyValue => Ok(value), + dynwinrt::PropertyValueUnboxResult::Unsupported(property_type) => { + Err(unsupported_property_type(property_type)) + } dynwinrt::PropertyValueUnboxResult::Value(value) => { property_value_to_javascript(env, value) } diff --git a/bindings/py/README.md b/bindings/py/README.md index 2a1cb72c..3822522a 100644 --- a/bindings/py/README.md +++ b/bindings/py/README.md @@ -207,27 +207,76 @@ produce a wrapper unless the input actually implements that default interface. Incompatible types raise the ordinary WinRT `OSError`. Static-only metadata classes with no instance surface are not projection targets. -### Explicit boxed-value unboxing +### WinRT `Object` values: automatic boxing and unboxing (experimental) -Generic WinRT `Object`/`IInspectable` results remain raw `DynWinRTValue` -instances. Use `unbox_object()` only where the application expects a boxed -`Windows.Foundation.IPropertyValue`, such as values from -`DeviceInformation.properties`: +> **Experimental prototype.** This section describes the draft automatic +> boxing model under evaluation; the design, names and rules may change. + +Every metadata position typed `Object` (`IInspectable`) — parameters, property +setters and getters, returns, out parameters, collection keys, values and +elements, arrays, async results, event arguments, and the inputs and outputs of +Python-implemented interfaces — converts through one pair of functions: +`to_winrt_object(value)` on the way in and `from_winrt_object(value)` on the +way out. Values boxed as `Windows.Foundation.IPropertyValue` therefore read and +write as ordinary Python values: ```python -from dynwinrt import unbox_object +from dynwinrt import UInt32 + +properties = PropertySet() +properties["count"] = 5 # boxed as Int32 +properties["tags"] = ["a", "b"] # boxed as StringArray +properties["id"] = UInt32(7) # boxed as UInt32 +assert properties["count"] == 5 +assert dict(properties) == {"count": 5, "tags": ["a", "b"], "id": 7} -raw = device_information.properties["System.Devices.DeviceInstanceId"] -instance_id = unbox_object(raw) +size = file_properties["System.Size"] # dynwinrt.UInt64(11) +copy["System.Size"] = size # written back as UInt64, not Int32 ``` -The helper borrows its argument. It maps supported numeric, Boolean, string, -character, GUID, and corresponding array property types to Python values; -64-bit integers use `int`, GUIDs use `uuid.UUID`, and `UInt8Array` uses `bytes`. -`None` stays `None`. If the object does not implement `IPropertyValue`, the -exact same Python object is returned, so identity and later projection remain -intact. Unsupported property types (including `DateTime`, `TimeSpan`, geometry, -inspectable, and other types) and native getter failures raise an exception. +Writing (`to_winrt_object`) applies these rules in order: + +| Python value | WinRT value | +|---|---| +| `None` | null | +| `DynWinRTValue` object, projected wrapper | passed through unchanged (wrappers as `_obj`) | +| `DynWinRTValue` holding a primitive, string, GUID, enum or foundation struct | boxed by its exact kind | +| `bool` | `Boolean` | +| `dynwinrt.UInt8` … `dynwinrt.Char16` tag | exactly that `PropertyType` | +| generated WinRT enum member | `IReference` (as C#) | +| `dynwinrt.Point/Size/Rect`, generated `Windows.Foundation.Point/Size/Rect` | `Point` / `Size` / `Rect` | +| other `int` (including unmarked `IntEnum`) | `Int32`, else `Int64`, else `UInt64`, else `OverflowError` | +| `float` | `Double` | +| `str` | `String` | +| timezone-aware `datetime` / `timedelta` / `uuid.UUID` | `DateTime` / `TimeSpan` / `Guid` (naive `datetime` raises `ValueError`) | +| `bytes`, `bytearray`, `memoryview` | `UInt8Array` | +| `dynwinrt.Array` | that array type | +| `list` / `tuple` | inferred array: homogeneous items give the matching array; plain ints the smallest of `Int32Array`/`Int64Array`/`UInt64Array`; ints mixed with floats `DoubleArray`; plain numbers adopt a single numeric tag present; anything else `InspectableArray` (items converted one by one, `None` as null); empty sequences raise `TypeError` | +| anything else | `TypeError` listing the supported types | + +Reading (`from_winrt_object`) returns `None` for null, the same `DynWinRTValue` +for objects that are not boxed values, and otherwise: + +| `PropertyType` | Python value | +|---|---| +| `Boolean`, `Int32`, `Double`, `String`, `Guid` | `bool`, `int`, `float`, `str`, `uuid.UUID` | +| `UInt8`, `Int16`, `UInt16`, `UInt32`, `Int64`, `UInt64`, `Single`, `Char16` | tagged `dynwinrt.UInt8` … `dynwinrt.Char16` (`int`/`float`/`str` subclasses) | +| `DateTime`, `TimeSpan` | timezone-aware UTC `datetime`, `timedelta` (microsecond resolution) | +| `Point`, `Size`, `Rect` | immutable `dynwinrt.Point`, `dynwinrt.Size`, `dynwinrt.Rect` | +| `UInt8Array` | `bytes` | +| other arrays | `dynwinrt.Array` list subclasses with tagged elements; `InspectableArray` items are converted recursively | +| `Empty`, `Inspectable`, `OtherType`, `OtherTypeArray`, unrepresentable values | the same `DynWinRTValue` (never an exception) | + +A value that is read and written back keeps its `PropertyType` (`DateTime` and +`TimeSpan` keep microseconds). Tags behave like the plain value for `==`, +`hash`, `str`, `format`, `json` and `pickle`; only `repr` shows the tag, and +arithmetic returns plain numbers. Boxes have value semantics: a write creates a +new box, so the COM identity of a boxed value is not preserved; every other +object keeps its identity and still works with `project_as()`. +`unbox_object()` is the same function as `from_winrt_object()`: it is +idempotent and returns plain Python values unchanged. Generated annotations use +`dynwinrt.WinRTObjectValue | None` for outputs and +`dynwinrt.WinRTObjectInput | None` for inputs. Use `wrapper.as_interface(InterfaceClass)` when converting an existing wrapper to an interface view. Use `InterfaceClass.from_value(raw)` for a raw diff --git a/bindings/py/dynwinrt.pyi b/bindings/py/dynwinrt.pyi index 9292a1a5..e975f61f 100644 --- a/bindings/py/dynwinrt.pyi +++ b/bindings/py/dynwinrt.pyi @@ -1,5 +1,7 @@ from collections.abc import Coroutine -from typing import Any, Awaitable, Callable, Generic, List, Literal, Mapping, Optional, Protocol, Sequence, TypeVar, Union, final, overload +from datetime import datetime, timedelta +from typing import Any, Awaitable, Callable, ClassVar, Generic, List, Literal, Mapping, Optional, Protocol, Sequence, SupportsFloat, SupportsIndex, TypeAlias, TypeVar, Union, final, overload +from typing import Self from uuid import UUID _T = TypeVar("_T", covariant=True) @@ -49,7 +51,44 @@ __all__ = [ "projected_lifetime_scope", "project_as", "release_projected", + "to_winrt_object", + "from_winrt_object", "unbox_object", + "WinRTScalar", + "UInt8", + "Int16", + "UInt16", + "Int32", + "UInt32", + "Int64", + "UInt64", + "Single", + "Double", + "Char16", + "WinRTArray", + "Int16Array", + "UInt16Array", + "Int32Array", + "UInt32Array", + "Int64Array", + "UInt64Array", + "SingleArray", + "DoubleArray", + "Char16Array", + "BooleanArray", + "StringArray", + "InspectableArray", + "DateTimeArray", + "TimeSpanArray", + "GuidArray", + "PointArray", + "SizeArray", + "RectArray", + "Point", + "Size", + "Rect", + "WinRTObjectValue", + "WinRTObjectInput", "init_winappsdk", "ro_initialize", "ro_uninitialize", @@ -123,26 +162,146 @@ def project_as( def release_projected(value: object) -> None: ... -_UnboxedPropertyValue = Union[ - bool, - int, - float, - str, - UUID, - bytes, - List[int], - List[float], - List[bool], - List[str], - List[UUID], -] +# ---------------------------------------------------------------------- +# WinRT Object value model (see python/dynwinrt/_values.py) +# ---------------------------------------------------------------------- + +class WinRTScalar: + property_type: ClassVar[str] + +class UInt8(WinRTScalar, int): + def __new__(cls, value: SupportsIndex = ..., /) -> Self: ... + +class Int16(WinRTScalar, int): + def __new__(cls, value: SupportsIndex = ..., /) -> Self: ... + +class UInt16(WinRTScalar, int): + def __new__(cls, value: SupportsIndex = ..., /) -> Self: ... + +class Int32(WinRTScalar, int): + def __new__(cls, value: SupportsIndex = ..., /) -> Self: ... + +class UInt32(WinRTScalar, int): + def __new__(cls, value: SupportsIndex = ..., /) -> Self: ... + +class Int64(WinRTScalar, int): + def __new__(cls, value: SupportsIndex = ..., /) -> Self: ... +class UInt64(WinRTScalar, int): + def __new__(cls, value: SupportsIndex = ..., /) -> Self: ... + +class Single(WinRTScalar, float): + def __new__(cls, value: SupportsFloat | SupportsIndex = ..., /) -> Self: ... + +class Double(WinRTScalar, float): + def __new__(cls, value: SupportsFloat | SupportsIndex = ..., /) -> Self: ... + +class Char16(WinRTScalar, str): + def __new__(cls, value: str, /) -> Self: ... + +_ArrayItem = TypeVar("_ArrayItem") + +class WinRTArray(list[_ArrayItem]): + property_type: ClassVar[str] + def copy(self) -> Self: ... + +class Int16Array(WinRTArray[int]): ... +class UInt16Array(WinRTArray[int]): ... +class Int32Array(WinRTArray[int]): ... +class UInt32Array(WinRTArray[int]): ... +class Int64Array(WinRTArray[int]): ... +class UInt64Array(WinRTArray[int]): ... +class SingleArray(WinRTArray[float]): ... +class DoubleArray(WinRTArray[float]): ... +class Char16Array(WinRTArray[str]): ... +class BooleanArray(WinRTArray[bool]): ... +class StringArray(WinRTArray[str]): ... +class InspectableArray(WinRTArray["WinRTObjectInput | None"]): ... +class DateTimeArray(WinRTArray[datetime]): ... +class TimeSpanArray(WinRTArray[timedelta]): ... +class GuidArray(WinRTArray[UUID]): ... +class PointArray(WinRTArray["Point | _WinRTPointLike"]): ... +class SizeArray(WinRTArray["Size | _WinRTSizeLike"]): ... +class RectArray(WinRTArray["Rect | _WinRTRectLike"]): ... + +class Point: + __match_args__ = ("x", "y") + property_type: ClassVar[str] + def __init__(self, x: float = ..., y: float = ...) -> None: ... + @property + def x(self) -> float: ... + @property + def y(self) -> float: ... + def __hash__(self) -> int: ... + +class Size: + __match_args__ = ("width", "height") + property_type: ClassVar[str] + def __init__(self, width: float = ..., height: float = ...) -> None: ... + @property + def width(self) -> float: ... + @property + def height(self) -> float: ... + def __hash__(self) -> int: ... + +class Rect: + __match_args__ = ("x", "y", "width", "height") + property_type: ClassVar[str] + def __init__( + self, x: float = ..., y: float = ..., width: float = ..., height: float = ... + ) -> None: ... + @property + def x(self) -> float: ... + @property + def y(self) -> float: ... + @property + def width(self) -> float: ... + @property + def height(self) -> float: ... + def __hash__(self) -> int: ... + +class _WinRTObjectWrapper(Protocol): + @property + def _obj(self) -> DynWinRTValue: ... + +# Generated windows.foundation Point/Size/Rect structs. +class _WinRTPointLike(Protocol): + x: float + y: float + +class _WinRTSizeLike(Protocol): + width: float + height: float + +class _WinRTRectLike(Protocol): + x: float + y: float + width: float + height: float + +# Values read from WinRT Object positions. Generated signatures add `| None`. +WinRTObjectValue: TypeAlias = ( + bool | int | float | str | UUID | datetime | timedelta | bytes + | Point | Size | Rect | list[Any] | DynWinRTValue +) +# Values accepted by WinRT Object positions besides None; generated enums are +# int subclasses. +WinRTObjectInput: TypeAlias = ( + WinRTObjectValue | _WinRTObjectWrapper | tuple[Any, ...] | bytearray | memoryview + | _WinRTPointLike | _WinRTSizeLike | _WinRTRectLike +) + +_Passthrough = TypeVar("_Passthrough") + +def to_winrt_object(value: WinRTObjectInput | None, /) -> DynWinRTValue: ... +@overload +def from_winrt_object(value: None, /) -> None: ... @overload -def unbox_object(value: None) -> None: ... +def from_winrt_object(value: DynWinRTValue, /) -> WinRTObjectValue | None: ... @overload -def unbox_object( - value: "DynWinRTValue", -) -> Union[_UnboxedPropertyValue, "DynWinRTValue", None]: ... +def from_winrt_object(value: _Passthrough, /) -> _Passthrough: ... + +unbox_object = from_winrt_object @final diff --git a/bindings/py/python/dynwinrt/_values.py b/bindings/py/python/dynwinrt/_values.py new file mode 100644 index 00000000..354d473a --- /dev/null +++ b/bindings/py/python/dynwinrt/_values.py @@ -0,0 +1,414 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# Public data model for WinRT ``Object`` (IInspectable) values. +# +# Loaded into the native module's globals (like _implementation.py) so the Rust +# conversion layer, ``to_winrt_object`` / ``from_winrt_object``, and Python share +# one set of types. Public types report ``__module__ == 'dynwinrt'`` so ``repr``, +# ``pickle`` and ``copy`` use the public import path. +# +# Names mirror ``Windows.Foundation.PropertyType``. A tag records the exact +# WinRT type of a value read from an ``Object`` position when the plain Python +# write rule would pick a different type (plain ``int`` boxes as Int32, plain +# ``float`` as Double, one-character ``str`` as String). Tags behave like the +# plain value (``==``, ``hash``, ``str``, ``format``, ``json``); only ``repr`` +# shows the tag, and arithmetic returns plain numbers. + +import operator as _values_operator +import struct as _values_struct +from datetime import datetime as _values_datetime, timedelta as _values_timedelta +from uuid import UUID as _values_UUID + +if 'DynWinRTValue' not in globals(): + # A standalone import would create a second, unrelated set of value types. + raise ImportError('dynwinrt._values is loaded by the dynwinrt native module; import dynwinrt') + + +def _values_to_f32(value, label): + try: + return _values_struct.unpack(' 0xFFFF: + raise ValueError(f'WinRT Char16 requires exactly one UTF-16 code unit, not {value!r}') + return str.__new__(cls, value) + + def __repr__(self): + return f'dynwinrt.Char16({str.__repr__(self)})' + + +class WinRTArray(list): + """A list that boxes as one specific WinRT array ``PropertyType``. + + Arrays read from ``Object`` positions are returned as these subclasses, so + empty and homogeneous-``InspectableArray`` values keep their type when they + are written back. Operations that build new lists (slicing, ``+``) return + plain lists; their tagged elements still infer the same array type. + """ + + __slots__ = () + property_type = '' + + def __repr__(self): + return f'dynwinrt.{type(self).__name__}({list.__repr__(self)})' + + def copy(self): + return type(self)(self) + + +class Int16Array(WinRTArray): + __slots__ = () + property_type = 'Int16Array' + + +class UInt16Array(WinRTArray): + __slots__ = () + property_type = 'UInt16Array' + + +class Int32Array(WinRTArray): + __slots__ = () + property_type = 'Int32Array' + + +class UInt32Array(WinRTArray): + __slots__ = () + property_type = 'UInt32Array' + + +class Int64Array(WinRTArray): + __slots__ = () + property_type = 'Int64Array' + + +class UInt64Array(WinRTArray): + __slots__ = () + property_type = 'UInt64Array' + + +class SingleArray(WinRTArray): + __slots__ = () + property_type = 'SingleArray' + + +class DoubleArray(WinRTArray): + __slots__ = () + property_type = 'DoubleArray' + + +class Char16Array(WinRTArray): + __slots__ = () + property_type = 'Char16Array' + + +class BooleanArray(WinRTArray): + __slots__ = () + property_type = 'BooleanArray' + + +class StringArray(WinRTArray): + __slots__ = () + property_type = 'StringArray' + + +class InspectableArray(WinRTArray): + __slots__ = () + property_type = 'InspectableArray' + + +class DateTimeArray(WinRTArray): + __slots__ = () + property_type = 'DateTimeArray' + + +class TimeSpanArray(WinRTArray): + __slots__ = () + property_type = 'TimeSpanArray' + + +class GuidArray(WinRTArray): + __slots__ = () + property_type = 'GuidArray' + + +class PointArray(WinRTArray): + __slots__ = () + property_type = 'PointArray' + + +class SizeArray(WinRTArray): + __slots__ = () + property_type = 'SizeArray' + + +class RectArray(WinRTArray): + __slots__ = () + property_type = 'RectArray' + + +class _WinRTGeometry: + """Immutable float32 fields of a boxed ``Windows.Foundation`` geometry struct. + + Equal to any value with the same ``_dynwinrt_property_type`` (including the + generated ``windows.foundation`` struct of that name) whose fields box to + the same float32 values. + """ + + __slots__ = () + property_type = '' + _dynwinrt_property_type = '' + _fields: tuple[str, ...] = () + + def __init__(self, *args, **kwargs): + names = type(self)._fields + if len(args) > len(names): + raise TypeError(f'{type(self).__name__} takes at most {len(names)} positional arguments') + values = dict(zip(names, args)) + for name, value in kwargs.items(): + if name not in names or name in values: + raise TypeError(f'{type(self).__name__} got an unexpected or repeated argument {name!r}') + values[name] = value + for name in names: + label = f'{type(self).property_type}.{name}' + object.__setattr__( + self, f'_{name}', _values_to_f32(_values_real(values.get(name, 0.0), label), label) + ) + + def __setattr__(self, name, value): + raise AttributeError(f'dynwinrt.{type(self).__name__} is immutable') + + def __delattr__(self, name): + raise AttributeError(f'dynwinrt.{type(self).__name__} is immutable') + + def _values(self): + return tuple(getattr(self, f'_{name}') for name in type(self)._fields) + + def __eq__(self, other): + kind = type(self)._dynwinrt_property_type + if getattr(type(other), '_dynwinrt_property_type', None) != kind: + return NotImplemented + if isinstance(other, _WinRTGeometry): + return self._values() == other._values() + try: + other_values = tuple( + _values_to_f32(_values_real(getattr(other, name), kind), kind) + for name in type(self)._fields + ) + except (AttributeError, OverflowError, TypeError): + return False + return self._values() == other_values + + def __hash__(self): + return hash((type(self)._dynwinrt_property_type, self._values())) + + def __repr__(self): + fields = ', '.join( + f'{name}={value!r}' for name, value in zip(type(self)._fields, self._values()) + ) + return f'dynwinrt.{type(self).__name__}({fields})' + + def __reduce__(self): + return (type(self), self._values()) + + +class Point(_WinRTGeometry): + """A boxed ``Windows.Foundation.Point`` (``PropertyType.Point``).""" + + __slots__ = ('_x', '_y') + __match_args__ = ('x', 'y') + property_type = 'Point' + _dynwinrt_property_type = 'Point' + _fields = ('x', 'y') + x = property(lambda self: self._x) + y = property(lambda self: self._y) + + +class Size(_WinRTGeometry): + """A boxed ``Windows.Foundation.Size`` (``PropertyType.Size``).""" + + __slots__ = ('_width', '_height') + __match_args__ = ('width', 'height') + property_type = 'Size' + _dynwinrt_property_type = 'Size' + _fields = ('width', 'height') + width = property(lambda self: self._width) + height = property(lambda self: self._height) + + +class Rect(_WinRTGeometry): + """A boxed ``Windows.Foundation.Rect`` (``PropertyType.Rect``).""" + + __slots__ = ('_x', '_y', '_width', '_height') + __match_args__ = ('x', 'y', 'width', 'height') + property_type = 'Rect' + _dynwinrt_property_type = 'Rect' + _fields = ('x', 'y', 'width', 'height') + x = property(lambda self: self._x) + y = property(lambda self: self._y) + width = property(lambda self: self._width) + height = property(lambda self: self._height) + + +# The native module namespace this file executes in; it supplies DynWinRTValue +# and the export list. +_values_native = globals() + +# Values ``from_winrt_object`` can return (``None`` is added per position). +WinRTObjectValue = ( + bool | int | float | str | _values_UUID | _values_datetime | _values_timedelta + | bytes | Point | Size | Rect | list | _values_native['DynWinRTValue'] +) +# Values ``to_winrt_object`` accepts besides ``None`` (plus projected wrappers, +# generated enums and generated Windows.Foundation Point/Size/Rect structs). +WinRTObjectInput = WinRTObjectValue | tuple | bytearray | memoryview + +_DYNWINRT_VALUE_EXPORTS = ( + 'WinRTScalar', 'UInt8', 'Int16', 'UInt16', 'Int32', 'UInt32', 'Int64', 'UInt64', + 'Single', 'Double', 'Char16', + 'WinRTArray', 'Int16Array', 'UInt16Array', 'Int32Array', 'UInt32Array', 'Int64Array', + 'UInt64Array', 'SingleArray', 'DoubleArray', 'Char16Array', 'BooleanArray', + 'StringArray', 'InspectableArray', 'DateTimeArray', 'TimeSpanArray', 'GuidArray', + 'PointArray', 'SizeArray', 'RectArray', + 'Point', 'Size', 'Rect', + 'WinRTObjectValue', 'WinRTObjectInput', +) + +for _values_name in _DYNWINRT_VALUE_EXPORTS: + _values_export = _values_native[_values_name] + if isinstance(_values_export, type): + _values_export.__module__ = 'dynwinrt' + if _values_name not in _values_native['__all__']: + _values_native['__all__'].append(_values_name) +for _values_export in (_WinRTInteger, _WinRTGeometry): + _values_export.__module__ = 'dynwinrt' +del _values_name, _values_export, _values_native diff --git a/bindings/py/src/implementation.rs b/bindings/py/src/implementation.rs index dbc65ced..44d88364 100644 --- a/bindings/py/src/implementation.rs +++ b/bindings/py/src/implementation.rs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -use std::path::PathBuf; use std::sync::{ Arc, Mutex, Weak, atomic::{AtomicBool, Ordering}, @@ -448,28 +447,11 @@ pub(crate) fn init(module: &Bound<'_, PyModule>) -> PyResult<()> { (runtime.bind(module.py()).getattr("shutdown")?,), )?; module.add("_dynwinrt_implementation_runtime", runtime)?; - // Load from the installed package, not an embedded string or a build-time - // path. Keep native globals (including __name__) for API/identity compatibility. - let source_path = module - .getattr("__spec__")? - .getattr("origin")? - .extract::()? - .with_file_name("_implementation.py"); - let source_path = module - .py() - .import("os")? - .call_method1("fspath", (source_path,))?; - let loader = module - .py() - .import("importlib.machinery")? - .getattr("SourceFileLoader")? - .call1(("dynwinrt._implementation", source_path))?; - let source = loader.call_method1("get_code", ("dynwinrt._implementation",))?; - module - .py() - .import("builtins")? - .call_method1("exec", (source, module.dict()))?; - Ok(()) + crate::package_source::exec_package_source( + module, + "_implementation.py", + "dynwinrt._implementation", + ) } #[cfg(test)] diff --git a/bindings/py/src/lib.rs b/bindings/py/src/lib.rs index f5bf9fcf..16224efb 100644 --- a/bindings/py/src/lib.rs +++ b/bindings/py/src/lib.rs @@ -7,6 +7,8 @@ mod async_runtime; mod delegate_method; mod errors; mod implementation; +mod object_value; +mod package_source; mod runtime; mod values; #[cfg(feature = "test-hooks")] @@ -572,6 +574,9 @@ def _dynwinrt_dispatch_progress(dispatch_state, value): m.add_class::()?; m.add_class::()?; super::implementation::init(m)?; + // The public Object value model; the conversion layer binds its types. + super::package_source::exec_package_source(m, "_values.py", "dynwinrt._values")?; + super::object_value::init(m)?; m.py().run( c" _Coroutine.register(_DynWinRTAsync) @@ -585,7 +590,11 @@ _Coroutine.register(_DynWinRTAsyncWithProgress) m.add_function(wrap_pyfunction!(super::runtime::init_winappsdk, m)?)?; m.add_function(wrap_pyfunction!(super::runtime::ro_initialize, m)?)?; m.add_function(wrap_pyfunction!(super::runtime::ro_uninitialize, m)?)?; - m.add_function(wrap_pyfunction!(super::runtime::unbox_object, m)?)?; + m.add_function(wrap_pyfunction!(super::object_value::to_winrt_object, m)?)?; + m.add_function(wrap_pyfunction!(super::object_value::from_winrt_object, m)?)?; + // Compatibility name: explicit unboxing is now the idempotent + // from_winrt_object conversion. + m.add("unbox_object", m.getattr("from_winrt_object")?)?; m.add_function(wrap_pyfunction!( super::runtime::register_xaml_runtime_class, m diff --git a/bindings/py/src/object_value.rs b/bindings/py/src/object_value.rs new file mode 100644 index 00000000..adb10d1a --- /dev/null +++ b/bindings/py/src/object_value.rs @@ -0,0 +1,1309 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! The single conversion layer between Python values and WinRT `Object` +//! (`IInspectable`) values. +//! +//! Generated code converts every `Object` position through exactly two +//! functions: [`to_winrt_object`] for inputs and [`from_winrt_object`] for +//! outputs. The Python data model they share (PropertyType-tagged scalars, +//! typed arrays and geometry values) is defined in `python/dynwinrt/_values.py`; +//! the language-neutral `IPropertyValue` payload is `dynwinrt::PropertyValueData`. +//! +//! Contract (see bindings/py/tests/test_object_values.py): +//! - R1: a boxed value read and written back keeps its PropertyType and value +//! (DateTime and TimeSpan at Python's microsecond resolution). +//! - R2: a supported Python value written and read back compares equal. +//! - I1: objects that are not boxed values, and boxes without a Python +//! representation, pass through in both directions as the same Python object. +//! - I2: boxed values have value semantics; every write creates a new box. + +use std::os::raw::c_int; + +use dynwinrt::{PropertyValueData as Data, PropertyValueUnboxResult, WinRTValue}; +use pyo3::IntoPyObjectExt; +use pyo3::exceptions::{PyOverflowError, PyRecursionError, PyTypeError, PyValueError}; +use pyo3::prelude::*; +use pyo3::sync::PyOnceLock; +use pyo3::types::{ + PyBool, PyByteArray, PyBytes, PyDict, PyFloat, PyInt, PyList, PyMemoryView, PyString, PyTuple, + PyType, +}; +use pyo3::{ffi, intern}; +use windows::Foundation::{DateTime, Point, Rect, Size, TimeSpan}; +use windows::core::{GUID, IUnknown}; + +use crate::errors::map_dynwinrt_error; +use crate::runtime::{DynWinRTType, DynWinRTValue, TABLE}; + +const ENUM_MARKER: &str = "_dynwinrt_enum_type"; +const ENUM_HANDLE_CACHE: &str = "_dynwinrt_enum_handle"; +const GEOMETRY_MARKER: &str = "_dynwinrt_property_type"; + +const SUPPORTED_INPUTS: &str = "None, a DynWinRTValue or projected WinRT object, bool, \ +a dynwinrt tag (UInt8, Int16, UInt16, Int32, UInt32, Int64, UInt64, Single, Double, Char16), \ +a generated WinRT enum, dynwinrt.Point/Size/Rect or the generated Windows.Foundation structs, \ +int, float, str, a timezone-aware datetime.datetime, datetime.timedelta, uuid.UUID, \ +bytes, bytearray, memoryview, a dynwinrt typed array, or a list/tuple of those"; + +/// Nested `InspectableArray` levels converted in either direction. Deeper (or +/// cyclic) Python lists raise `RecursionError`; deeper boxed arrays stay native. +const MAX_NESTING: usize = 64; + +const EMPTY_SEQUENCE: &str = "cannot infer the WinRT array type of an empty list or tuple; \ +use a dynwinrt typed array such as dynwinrt.StringArray([]) or dynwinrt.InspectableArray([])"; + +/// The one place this module creates Python-visible native values. +fn native(value: WinRTValue) -> DynWinRTValue { + DynWinRTValue(value) +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum Element { + UInt8, + Int16, + UInt16, + Int32, + UInt32, + Int64, + UInt64, + Single, + Double, + Char16, + Boolean, + String, + Inspectable, + DateTime, + TimeSpan, + Guid, + Point, + Size, + Rect, +} + +impl Element { + fn integer_bounds(self) -> Option<(i128, i128)> { + match self { + Self::UInt8 => Some((0, u8::MAX.into())), + Self::Int16 => Some((i16::MIN.into(), i16::MAX.into())), + Self::UInt16 => Some((0, u16::MAX.into())), + Self::Int32 => Some((i32::MIN.into(), i32::MAX.into())), + Self::UInt32 => Some((0, u32::MAX.into())), + Self::Int64 => Some((i64::MIN.into(), i64::MAX.into())), + Self::UInt64 => Some((0, u64::MAX.into())), + _ => None, + } + } + + fn is_float(self) -> bool { + matches!(self, Self::Single | Self::Double) + } + + fn name(self) -> &'static str { + match self { + Self::UInt8 => "UInt8", + Self::Int16 => "Int16", + Self::UInt16 => "UInt16", + Self::Int32 => "Int32", + Self::UInt32 => "UInt32", + Self::Int64 => "Int64", + Self::UInt64 => "UInt64", + Self::Single => "Single", + Self::Double => "Double", + Self::Char16 => "Char16", + Self::Boolean => "Boolean", + Self::String => "String", + Self::Inspectable => "Inspectable", + Self::DateTime => "DateTime", + Self::TimeSpan => "TimeSpan", + Self::Guid => "Guid", + Self::Point => "Point", + Self::Size => "Size", + Self::Rect => "Rect", + } + } +} + +const TAGS: [Element; 10] = [ + Element::UInt8, + Element::Int16, + Element::UInt16, + Element::Int32, + Element::UInt32, + Element::Int64, + Element::UInt64, + Element::Single, + Element::Double, + Element::Char16, +]; + +const ARRAYS: [Element; 18] = [ + Element::Int16, + Element::UInt16, + Element::Int32, + Element::UInt32, + Element::Int64, + Element::UInt64, + Element::Single, + Element::Double, + Element::Char16, + Element::Boolean, + Element::String, + Element::Inspectable, + Element::DateTime, + Element::TimeSpan, + Element::Guid, + Element::Point, + Element::Size, + Element::Rect, +]; + +const GEOMETRY: [Element; 3] = [Element::Point, Element::Size, Element::Rect]; + +/// Python types of the `_values.py` data model and the helpers it relies on. +struct ValueModel { + tags: Vec<(Py, Element)>, + scalar_base: Py, + arrays: Vec<(Py, Element)>, + array_base: Py, + geometry: Vec<(Py, Element)>, + enum_base: Py, + uuid: Py, + datetime: Py, + timedelta: Py, + int_new: Py, + float_new: Py, + str_new: Py, + ticks_to_datetime: Py, + datetime_to_ticks: Py, + ticks_to_timedelta: Py, + timedelta_to_ticks: Py, +} + +static MODEL: PyOnceLock = PyOnceLock::new(); + +fn model(py: Python<'_>) -> PyResult<&'static ValueModel> { + MODEL.get(py).ok_or_else(|| { + pyo3::exceptions::PyRuntimeError::new_err("dynwinrt value model is not initialized") + }) +} + +/// Bind the data model after `_values.py` has run in the native module. +pub(crate) fn init(module: &Bound<'_, PyModule>) -> PyResult<()> { + let py = module.py(); + let class = |name: &str| -> PyResult> { + Ok(module.getattr(name)?.cast_into::()?.unbind()) + }; + let function = |name: &str| -> PyResult> { Ok(module.getattr(name)?.unbind()) }; + let builtin_new = |typ: Bound<'_, PyType>| -> PyResult> { + Ok(typ.getattr(intern!(py, "__new__"))?.unbind()) + }; + let model = ValueModel { + tags: TAGS + .iter() + .map(|element| Ok((class(element.name())?, *element))) + .collect::>()?, + scalar_base: class("WinRTScalar")?, + arrays: ARRAYS + .iter() + .map(|element| Ok((class(&format!("{}Array", element.name()))?, *element))) + .collect::>()?, + array_base: class("WinRTArray")?, + geometry: GEOMETRY + .iter() + .map(|element| Ok((class(element.name())?, *element))) + .collect::>()?, + enum_base: py + .import("enum")? + .getattr("Enum")? + .cast_into::()? + .unbind(), + uuid: py + .import("uuid")? + .getattr("UUID")? + .cast_into::()? + .unbind(), + datetime: py + .import("datetime")? + .getattr("datetime")? + .cast_into::()? + .unbind(), + timedelta: py + .import("datetime")? + .getattr("timedelta")? + .cast_into::()? + .unbind(), + int_new: builtin_new(py.get_type::())?, + float_new: builtin_new(py.get_type::())?, + str_new: builtin_new(py.get_type::())?, + ticks_to_datetime: function("_dynwinrt_ticks_to_datetime")?, + datetime_to_ticks: function("_dynwinrt_datetime_to_ticks")?, + ticks_to_timedelta: function("_dynwinrt_ticks_to_timedelta")?, + timedelta_to_ticks: function("_dynwinrt_timedelta_to_ticks")?, + }; + // A second initialization of the same interpreter keeps the first model. + let _ = MODEL.set(py, model); + Ok(()) +} + +// ====================================================================== +// from_winrt_object: DynWinRTValue -> Python value +// ====================================================================== + +/// Convert a WinRT `Object` value to its Python representation. +/// +/// Null becomes `None`; boxed `IPropertyValue` payloads become Python values +/// (tagged where the plain write rule would change their type); every other +/// object, including boxes without a Python representation, is returned as the +/// same `DynWinRTValue`. Non-`DynWinRTValue` inputs are returned unchanged, so +/// the conversion is idempotent. +#[pyfunction] +#[pyo3(signature = (value, /))] +pub fn from_winrt_object(py: Python<'_>, value: &Bound<'_, PyAny>) -> PyResult> { + unbox_at(py, value, 0) +} + +fn unbox_at(py: Python<'_>, value: &Bound<'_, PyAny>, depth: usize) -> PyResult> { + let Ok(raw) = value.cast::() else { + return Ok(value.clone().unbind()); + }; + let result = + dynwinrt::unbox_property_value(&raw.try_borrow()?.0).map_err(map_dynwinrt_error)?; + match result { + PropertyValueUnboxResult::Null => Ok(py.None()), + PropertyValueUnboxResult::NotPropertyValue | PropertyValueUnboxResult::Unsupported(_) => { + Ok(value.clone().unbind()) + } + PropertyValueUnboxResult::Value(data) => { + Ok(data_to_python(py, model(py)?, data, depth)? + .unwrap_or_else(|| value.clone().unbind())) + } + } +} + +impl ValueModel { + fn tag_type(&self, element: Element) -> &Py { + self.tags + .iter() + .find(|(_, candidate)| *candidate == element) + .map(|(typ, _)| typ) + .expect("every tag element has a class") + } + + fn array_type(&self, element: Element) -> &Py { + self.arrays + .iter() + .find(|(_, candidate)| *candidate == element) + .map(|(typ, _)| typ) + .expect("every array element has a class") + } + + fn geometry_type(&self, element: Element) -> &Py { + self.geometry + .iter() + .find(|(_, candidate)| *candidate == element) + .map(|(typ, _)| typ) + .expect("every geometry element has a class") + } + + fn integer<'py, T: IntoPyObject<'py>>( + &self, + py: Python<'py>, + element: Element, + value: T, + ) -> PyResult> { + // int.__new__(Tag, value) skips the validating Tag.__new__: the value + // came from WinRT and is in range by construction. + self.int_new.call1(py, (self.tag_type(element), value)) + } + + fn float(&self, py: Python<'_>, element: Element, value: f64) -> PyResult> { + self.float_new.call1(py, (self.tag_type(element), value)) + } + + fn char16(&self, py: Python<'_>, value: u16) -> PyResult> { + // PyUnicode_FromOrdinal preserves unpaired surrogates, unlike Rust strings. + let character = unsafe { + Bound::from_owned_ptr_or_err(py, ffi::PyUnicode_FromOrdinal(c_int::from(value)))? + }; + self.str_new + .call1(py, (self.tag_type(Element::Char16), character)) + } + + fn uuid(&self, py: Python<'_>, value: GUID) -> PyResult> { + let kwargs = PyDict::new(py); + kwargs.set_item(intern!(py, "int"), value.to_u128())?; + self.uuid.call(py, (), Some(&kwargs)) + } + + /// `None` when the instant is outside Python's datetime range. + fn datetime(&self, py: Python<'_>, value: DateTime) -> PyResult>> { + representable(py, self.ticks_to_datetime.call1(py, (value.UniversalTime,))) + } + + /// `None` when the duration is outside Python's timedelta range. + fn timedelta(&self, py: Python<'_>, value: TimeSpan) -> PyResult>> { + representable(py, self.ticks_to_timedelta.call1(py, (value.Duration,))) + } + + fn point(&self, py: Python<'_>, value: Point) -> PyResult> { + self.geometry_type(Element::Point) + .call1(py, (value.X, value.Y)) + } + + fn size(&self, py: Python<'_>, value: Size) -> PyResult> { + self.geometry_type(Element::Size) + .call1(py, (value.Width, value.Height)) + } + + fn rect(&self, py: Python<'_>, value: Rect) -> PyResult> { + self.geometry_type(Element::Rect) + .call1(py, (value.X, value.Y, value.Width, value.Height)) + } + + fn array( + &self, + py: Python<'_>, + element: Element, + items: impl IntoIterator>>, + ) -> PyResult> { + let items = items.into_iter().collect::>>()?; + self.array_type(element) + .call1(py, (PyList::new(py, items)?,)) + } + + /// `None` when any element is outside Python's representable range. + fn optional_array( + &self, + py: Python<'_>, + element: Element, + items: impl IntoIterator>>>, + ) -> PyResult>> { + let mut values = Vec::new(); + for item in items { + let Some(item) = item? else { + return Ok(None); + }; + values.push(item); + } + Ok(Some( + self.array_type(element) + .call1(py, (PyList::new(py, values)?,))?, + )) + } +} + +fn representable(py: Python<'_>, result: PyResult>) -> PyResult>> { + match result { + Ok(value) => Ok(Some(value)), + Err(error) if error.is_instance_of::(py) => Ok(None), + Err(error) => Err(error), + } +} + +/// Map a boxed payload to the Python data model; `None` keeps the raw value. +fn data_to_python( + py: Python<'_>, + model: &ValueModel, + data: Data, + depth: usize, +) -> PyResult>> { + let value = match data { + Data::UInt8(value) => model.integer(py, Element::UInt8, value)?, + Data::Int16(value) => model.integer(py, Element::Int16, value)?, + Data::UInt16(value) => model.integer(py, Element::UInt16, value)?, + Data::Int32(value) => value.into_py_any(py)?, + Data::UInt32(value) => model.integer(py, Element::UInt32, value)?, + Data::Int64(value) => model.integer(py, Element::Int64, value)?, + Data::UInt64(value) => model.integer(py, Element::UInt64, value)?, + Data::Single(value) => model.float(py, Element::Single, f64::from(value))?, + Data::Double(value) => value.into_py_any(py)?, + Data::Char16(value) => model.char16(py, value)?, + Data::Boolean(value) => PyBool::new(py, value).to_owned().into_any().unbind(), + Data::String(value) => value.into_py_any(py)?, + Data::Guid(value) => model.uuid(py, value)?, + Data::DateTime(value) => return model.datetime(py, value), + Data::TimeSpan(value) => return model.timedelta(py, value), + Data::Point(value) => model.point(py, value)?, + Data::Size(value) => model.size(py, value)?, + Data::Rect(value) => model.rect(py, value)?, + Data::UInt8Array(values) => PyBytes::new(py, &values).into_any().unbind(), + Data::Int16Array(values) => model.array( + py, + Element::Int16, + values + .into_iter() + .map(|value| model.integer(py, Element::Int16, value)), + )?, + Data::UInt16Array(values) => model.array( + py, + Element::UInt16, + values + .into_iter() + .map(|value| model.integer(py, Element::UInt16, value)), + )?, + Data::Int32Array(values) => model.array( + py, + Element::Int32, + values.into_iter().map(|value| value.into_py_any(py)), + )?, + Data::UInt32Array(values) => model.array( + py, + Element::UInt32, + values + .into_iter() + .map(|value| model.integer(py, Element::UInt32, value)), + )?, + Data::Int64Array(values) => model.array( + py, + Element::Int64, + values + .into_iter() + .map(|value| model.integer(py, Element::Int64, value)), + )?, + Data::UInt64Array(values) => model.array( + py, + Element::UInt64, + values + .into_iter() + .map(|value| model.integer(py, Element::UInt64, value)), + )?, + Data::SingleArray(values) => model.array( + py, + Element::Single, + values + .into_iter() + .map(|value| model.float(py, Element::Single, f64::from(value))), + )?, + Data::DoubleArray(values) => model.array( + py, + Element::Double, + values.into_iter().map(|value| value.into_py_any(py)), + )?, + Data::Char16Array(values) => model.array( + py, + Element::Char16, + values.into_iter().map(|value| model.char16(py, value)), + )?, + Data::BooleanArray(values) => model.array( + py, + Element::Boolean, + values + .into_iter() + .map(|value| Ok(PyBool::new(py, value).to_owned().into_any().unbind())), + )?, + Data::StringArray(values) => model.array( + py, + Element::String, + values.into_iter().map(|value| value.into_py_any(py)), + )?, + Data::InspectableArray(values) => model.array( + py, + Element::Inspectable, + values.into_iter().map(|value| match value { + None => Ok(py.None()), + Some(object) => { + let element = Bound::new(py, native(WinRTValue::Object(object)))?; + if depth >= MAX_NESTING { + Ok(element.into_any().unbind()) + } else { + unbox_at(py, element.as_any(), depth + 1) + } + } + }), + )?, + Data::DateTimeArray(values) => { + return model.optional_array( + py, + Element::DateTime, + values.into_iter().map(|value| model.datetime(py, value)), + ); + } + Data::TimeSpanArray(values) => { + return model.optional_array( + py, + Element::TimeSpan, + values.into_iter().map(|value| model.timedelta(py, value)), + ); + } + Data::GuidArray(values) => model.array( + py, + Element::Guid, + values.into_iter().map(|value| model.uuid(py, value)), + )?, + Data::PointArray(values) => model.array( + py, + Element::Point, + values.into_iter().map(|value| model.point(py, value)), + )?, + Data::SizeArray(values) => model.array( + py, + Element::Size, + values.into_iter().map(|value| model.size(py, value)), + )?, + Data::RectArray(values) => model.array( + py, + Element::Rect, + values.into_iter().map(|value| model.rect(py, value)), + )?, + }; + Ok(Some(value)) +} + +// ====================================================================== +// to_winrt_object: Python value -> DynWinRTValue +// ====================================================================== + +/// Convert a Python value for a WinRT `Object` position. +/// +/// `DynWinRTValue` objects and projected wrappers pass through unchanged +/// (wrappers as their `_obj`); `None` becomes the null object; supported Python +/// values are boxed as `Windows.Foundation.PropertyValue` objects and generated +/// enums as `IReference`. Anything else raises `TypeError`. +#[pyfunction] +#[pyo3(signature = (value, /))] +pub fn to_winrt_object(py: Python<'_>, value: &Bound<'_, PyAny>) -> PyResult> { + match box_object(value, model(py)?, 0)? { + Boxed::Existing(object) => Ok(object), + Boxed::New(boxed) => Ok(Bound::new(py, native(boxed))?.into_any().unbind()), + } +} + +enum Boxed { + /// An existing native value, passed through as the same Python object. + Existing(Py), + New(WinRTValue), +} + +/// A Python value classified by the write rules, in their pinned order. +enum Kind<'py> { + Null, + Existing(Bound<'py, PyAny>), + /// A `DynWinRTValue` holding a primitive, string, GUID, enum, struct or array. + Native(Bound<'py, DynWinRTValue>), + Boolean(bool), + Tag(Element), + Enum, + Geometry(Element), + Int, + Float, + Str, + DateTime, + TimeSpan, + Guid, + Bytes, + TypedArray(Element), + Sequence, +} + +fn is_native_object(value: &WinRTValue) -> bool { + matches!( + value, + WinRTValue::Object(_) | WinRTValue::Null | WinRTValue::Async(_) + ) +} + +/// Apply the write rules in their pinned order: +/// None -> DynWinRTValue/projected wrapper -> bool -> tags -> enum marker -> +/// geometry markers -> int -> float -> str -> datetime/timedelta/UUID -> +/// bytes-like -> typed arrays -> list/tuple. `None` means unsupported. +/// +/// Order matters because bool, IntEnum/IntFlag and the integer tags are int +/// subclasses, typed arrays are lists, and wrappers may be anything. +fn classify<'py>(value: &Bound<'py, PyAny>, model: &ValueModel) -> PyResult>> { + let py = value.py(); + // Exact built-in scalars cannot match any earlier rule, so this fast path + // is equivalent to the ordered chain below. + if value.is_exact_instance_of::() { + return Ok(Some(Kind::Int)); + } + if value.is_exact_instance_of::() { + return Ok(Some(Kind::Str)); + } + if value.is_exact_instance_of::() { + return Ok(Some(Kind::Float)); + } + if let Ok(boolean) = value.cast::() { + return Ok(Some(Kind::Boolean(boolean.is_true()))); + } + + // 1. None + if value.is_none() { + return Ok(Some(Kind::Null)); + } + // 2. DynWinRTValue and projected wrappers + if let Ok(raw) = value.cast::() { + return Ok(Some(if is_native_object(&raw.try_borrow()?.0) { + Kind::Existing(value.clone()) + } else { + Kind::Native(raw.clone()) + })); + } + if let Some(inner) = value.getattr_opt(intern!(py, "_obj"))? + && inner.cast::().is_ok() + { + return Ok(Some(Kind::Existing(inner))); + } + // 3. bool: handled by the fast path; bool cannot be subclassed. + let typ = value.get_type(); + // 4. PropertyType tags + if value.is_instance(model.scalar_base.bind(py))? { + for (tag, element) in &model.tags { + if typ.is(tag) || value.is_instance(tag.bind(py))? { + return Ok(Some(Kind::Tag(*element))); + } + } + } + // 5. generated enums + if value.is_instance(model.enum_base.bind(py))? && typ.hasattr(ENUM_MARKER)? { + return Ok(Some(Kind::Enum)); + } + // 6. Windows.Foundation Point/Size/Rect: dynwinrt values or generated structs + for (geometry, element) in &model.geometry { + if typ.is(geometry) { + return Ok(Some(Kind::Geometry(*element))); + } + } + if let Some(marker) = typ.getattr_opt(GEOMETRY_MARKER)? + && let Ok(marker) = marker.extract::() + && let Some(element) = GEOMETRY.iter().find(|element| element.name() == marker) + { + return Ok(Some(Kind::Geometry(*element))); + } + // 7-9. int (including unmarked IntEnum), float, str subclasses + if value.is_instance_of::() { + return Ok(Some(Kind::Int)); + } + if value.is_instance_of::() { + return Ok(Some(Kind::Float)); + } + if value.is_instance_of::() { + return Ok(Some(Kind::Str)); + } + // 10. datetime, timedelta, UUID + if value.is_instance(model.datetime.bind(py))? { + return Ok(Some(Kind::DateTime)); + } + if value.is_instance(model.timedelta.bind(py))? { + return Ok(Some(Kind::TimeSpan)); + } + if value.is_instance(model.uuid.bind(py))? { + return Ok(Some(Kind::Guid)); + } + // 11. bytes-like + if value.is_instance_of::() + || value.is_instance_of::() + || value.is_instance_of::() + { + return Ok(Some(Kind::Bytes)); + } + // 12. typed arrays + if value.is_instance(model.array_base.bind(py))? { + for (array, element) in &model.arrays { + if typ.is(array) || value.is_instance(array.bind(py))? { + return Ok(Some(Kind::TypedArray(*element))); + } + } + return Err(PyTypeError::new_err( + "dynwinrt.WinRTArray is abstract; use a typed array such as dynwinrt.Int32Array", + )); + } + // 13. list/tuple + if value.is_instance_of::() || value.is_instance_of::() { + return Ok(Some(Kind::Sequence)); + } + Ok(None) +} + +fn unsupported(value: &Bound<'_, PyAny>) -> PyErr { + let name = value + .get_type() + .qualname() + .map(|name| name.to_string()) + .unwrap_or_else(|_| "value".into()); + PyTypeError::new_err(format!( + "cannot convert {name} to a WinRT Object; expected {SUPPORTED_INPUTS}" + )) +} + +fn box_object(value: &Bound<'_, PyAny>, model: &ValueModel, depth: usize) -> PyResult { + if depth > MAX_NESTING { + return Err(PyRecursionError::new_err(format!( + "WinRT Object arrays cannot nest more than {MAX_NESTING} levels" + ))); + } + let Some(kind) = classify(value, model)? else { + return Err(unsupported(value)); + }; + match kind { + Kind::Null => Ok(Boxed::New(WinRTValue::Null)), + Kind::Existing(object) => Ok(Boxed::Existing(object.unbind())), + Kind::Native(raw) => box_native(&raw.try_borrow()?.0).map(Boxed::New), + Kind::Enum => box_enum(value).map(Boxed::New), + kind => box_data(&payload(value, kind, model, depth)?).map(Boxed::New), + } +} + +fn box_data(data: &Data) -> PyResult { + dynwinrt::box_property_value(data).map_err(map_dynwinrt_error) +} + +/// The boxed payload of a value classified as `kind`. +fn payload( + value: &Bound<'_, PyAny>, + kind: Kind<'_>, + model: &ValueModel, + depth: usize, +) -> PyResult { + Ok(match kind { + Kind::Boolean(value) => Data::Boolean(value), + Kind::Tag(element) | Kind::Geometry(element) => scalar(value, element, model, None)?, + Kind::Int => plain_integer(value)?, + Kind::Float => Data::Double(value.extract()?), + Kind::Str => Data::String(value.extract()?), + Kind::DateTime => scalar(value, Element::DateTime, model, None)?, + Kind::TimeSpan => scalar(value, Element::TimeSpan, model, None)?, + Kind::Guid => scalar(value, Element::Guid, model, None)?, + Kind::Bytes => Data::UInt8Array(bytes_like(value)?), + Kind::TypedArray(element) => array(&items(value)?, element, model, depth)?, + Kind::Sequence => sequence(&items(value)?, model, depth)?, + Kind::Null | Kind::Existing(_) | Kind::Native(_) | Kind::Enum => { + unreachable!("handled by box_object") + } + }) +} + +/// Plain int: Int32 when it fits, then Int64, then UInt64. +fn plain_integer(value: &Bound<'_, PyAny>) -> PyResult { + let number = big_integer(value, "Object")?; + if let Ok(number) = i32::try_from(number) { + Ok(Data::Int32(number)) + } else if let Ok(number) = i64::try_from(number) { + Ok(Data::Int64(number)) + } else if let Ok(number) = u64::try_from(number) { + Ok(Data::UInt64(number)) + } else { + Err(integer_overflow(number, "Int32, Int64 or UInt64")) + } +} + +fn big_integer(value: &Bound<'_, PyAny>, label: &str) -> PyResult { + value.extract::().map_err(|error| { + if error.is_instance_of::(value.py()) { + PyOverflowError::new_err(format!( + "{} is out of range for a WinRT {label} value", + value + .repr() + .map(|repr| repr.to_string()) + .unwrap_or_default() + )) + } else { + error + } + }) +} + +fn integer_overflow(number: i128, target: &str) -> PyErr { + PyOverflowError::new_err(format!("{number} is out of range for WinRT {target}")) +} + +fn context(label: Option<&str>, message: String) -> String { + match label { + Some(label) => format!("{label}: {message}"), + None => message, + } +} + +fn real(value: &Bound<'_, PyAny>, label: &str) -> PyResult { + if value.cast::().is_ok() + || !(value.is_instance_of::() || value.is_instance_of::()) + { + return Err(PyTypeError::new_err(format!( + "WinRT {label} requires a real number, not {}", + value.get_type().qualname()? + ))); + } + value.extract::() +} + +fn to_f32(value: f64, label: &str) -> PyResult { + let single = value as f32; + if value.is_finite() && single.is_infinite() { + return Err(PyOverflowError::new_err(format!( + "{value} is out of range for WinRT {label}" + ))); + } + Ok(single) +} + +fn char16(value: &Bound<'_, PyAny>, label: &str) -> PyResult { + let Ok(text) = value.cast::() else { + return Err(PyTypeError::new_err(format!( + "WinRT {label} requires a str, not {}", + value.get_type().qualname()? + ))); + }; + let unit = if unsafe { ffi::PyUnicode_GetLength(text.as_ptr()) } == 1 { + u16::try_from(unsafe { ffi::PyUnicode_ReadChar(text.as_ptr(), 0) }).ok() + } else { + None + }; + unit.ok_or_else(|| { + PyValueError::new_err(format!( + "WinRT {label} requires exactly one UTF-16 code unit, not {}", + value + .repr() + .map(|repr| repr.to_string()) + .unwrap_or_default() + )) + }) +} + +fn geometry_field(value: &Bound<'_, PyAny>, name: &str, label: &str) -> PyResult { + let label = format!("{label}.{name}"); + to_f32(real(&value.getattr(name)?, &label)?, &label) +} + +/// Convert one value to exactly `element`, as a scalar payload. +/// +/// `label` names the enclosing array element in error messages. +fn scalar( + value: &Bound<'_, PyAny>, + element: Element, + model: &ValueModel, + label: Option<&str>, +) -> PyResult { + let py = value.py(); + let name = label.unwrap_or(element.name()); + let type_error = |expected: &str| -> PyResult { + Err(PyTypeError::new_err(context( + label, + format!( + "WinRT {} requires {expected}, not {}", + element.name(), + value.get_type().qualname()? + ), + ))) + }; + if let Some((low, high)) = element.integer_bounds() { + if value.cast::().is_ok() || !value.is_instance_of::() { + return type_error("an int"); + } + let number = big_integer(value, element.name())?; + if number < low || number > high { + return Err(PyOverflowError::new_err(context( + label, + format!( + "{number} is out of range for WinRT {} ({low}..{high})", + element.name() + ), + ))); + } + return Ok(match element { + Element::UInt8 => Data::UInt8(number as u8), + Element::Int16 => Data::Int16(number as i16), + Element::UInt16 => Data::UInt16(number as u16), + Element::Int32 => Data::Int32(number as i32), + Element::UInt32 => Data::UInt32(number as u32), + Element::Int64 => Data::Int64(number as i64), + Element::UInt64 => Data::UInt64(number as u64), + _ => unreachable!("integer element"), + }); + } + Ok(match element { + Element::Single => Data::Single(to_f32(real(value, name)?, name)?), + Element::Double => Data::Double(real(value, name)?), + Element::Char16 => Data::Char16(char16(value, name)?), + Element::Boolean => match value.cast::() { + Ok(boolean) => Data::Boolean(boolean.is_true()), + Err(_) => return type_error("a bool"), + }, + Element::String => { + if !value.is_instance_of::() { + return type_error("a str"); + } + Data::String(value.extract()?) + } + Element::DateTime => { + if !value.is_instance(model.datetime.bind(py))? { + return type_error("a timezone-aware datetime.datetime"); + } + Data::DateTime(DateTime { + UniversalTime: model.datetime_to_ticks.call1(py, (value,))?.extract(py)?, + }) + } + Element::TimeSpan => { + if !value.is_instance(model.timedelta.bind(py))? { + return type_error("a datetime.timedelta"); + } + Data::TimeSpan(TimeSpan { + Duration: model.timedelta_to_ticks.call1(py, (value,))?.extract(py)?, + }) + } + Element::Guid => { + if !value.is_instance(model.uuid.bind(py))? { + return type_error("a uuid.UUID"); + } + Data::Guid(GUID::from_u128( + value.getattr(intern!(py, "int"))?.extract()?, + )) + } + Element::Point | Element::Size | Element::Rect => { + if !matches!(classify(value, model)?, Some(Kind::Geometry(found)) if found == element) { + return type_error(&format!( + "a dynwinrt.{0} or generated Windows.Foundation.{0}", + element.name() + )); + } + match element { + Element::Point => Data::Point(Point { + X: geometry_field(value, "x", name)?, + Y: geometry_field(value, "y", name)?, + }), + Element::Size => Data::Size(Size { + Width: geometry_field(value, "width", name)?, + Height: geometry_field(value, "height", name)?, + }), + _ => Data::Rect(Rect { + X: geometry_field(value, "x", name)?, + Y: geometry_field(value, "y", name)?, + Width: geometry_field(value, "width", name)?, + Height: geometry_field(value, "height", name)?, + }), + } + } + Element::Inspectable + | Element::UInt8 + | Element::Int16 + | Element::UInt16 + | Element::Int32 + | Element::UInt32 + | Element::Int64 + | Element::UInt64 => unreachable!("not a non-integer scalar element"), + }) +} + +fn bytes_like(value: &Bound<'_, PyAny>) -> PyResult> { + if let Ok(bytes) = value.cast::() { + return Ok(bytes.as_bytes().to_vec()); + } + if let Ok(bytes) = value.cast::() { + return Ok(bytes.to_vec()); + } + // memoryview: Python's bytes() semantics (the raw bytes of the buffer). + Ok(value + .call_method0(intern!(value.py(), "tobytes"))? + .cast_into::()? + .as_bytes() + .to_vec()) +} + +fn items<'py>(value: &Bound<'py, PyAny>) -> PyResult>> { + value.try_iter()?.collect() +} + +/// Convert every item to exactly `element`. +fn array( + items: &[Bound<'_, PyAny>], + element: Element, + model: &ValueModel, + depth: usize, +) -> PyResult { + let array_name = format!("{}Array", element.name()); + let label = |index: usize| format!("{array_name} element {index}"); + macro_rules! collect { + ($variant:ident, $scalar:ident) => { + Data::$variant( + items + .iter() + .enumerate() + .map( + |(index, item)| match scalar(item, element, model, Some(&label(index)))? { + Data::$scalar(value) => Ok(value), + _ => unreachable!("scalar returns the requested element"), + }, + ) + .collect::>()?, + ) + }; + } + Ok(match element { + Element::UInt8 => collect!(UInt8Array, UInt8), + Element::Int16 => collect!(Int16Array, Int16), + Element::UInt16 => collect!(UInt16Array, UInt16), + Element::Int32 => collect!(Int32Array, Int32), + Element::UInt32 => collect!(UInt32Array, UInt32), + Element::Int64 => collect!(Int64Array, Int64), + Element::UInt64 => collect!(UInt64Array, UInt64), + Element::Single => collect!(SingleArray, Single), + Element::Double => collect!(DoubleArray, Double), + Element::Char16 => collect!(Char16Array, Char16), + Element::Boolean => collect!(BooleanArray, Boolean), + Element::String => collect!(StringArray, String), + Element::DateTime => collect!(DateTimeArray, DateTime), + Element::TimeSpan => collect!(TimeSpanArray, TimeSpan), + Element::Guid => collect!(GuidArray, Guid), + Element::Point => collect!(PointArray, Point), + Element::Size => collect!(SizeArray, Size), + Element::Rect => collect!(RectArray, Rect), + Element::Inspectable => Data::InspectableArray( + items + .iter() + .map(|item| { + let object = match box_object(item, model, depth + 1)? { + Boxed::Existing(object) => object + .bind(item.py()) + .cast::()? + .try_borrow()? + .0 + .clone(), + Boxed::New(value) => value, + }; + Ok(object.as_object()) + }) + .collect::>>>()?, + ), + }) +} + +/// How one item of an untagged list or tuple constrains the array type. +#[derive(Clone, Copy, PartialEq, Eq)] +enum Candidate { + Exact(Element), + PlainInt, + PlainFloat, + Other, +} + +/// Infer the array type of an untagged list or tuple. +/// +/// Homogeneous items give the matching array; plain ints give the smallest of +/// Int32/Int64/UInt64 that fits all of them; plain ints mixed with plain floats +/// give DoubleArray; plain numbers adopt a single numeric tag present in the +/// list; anything else, including None and objects, gives an InspectableArray +/// whose items are converted one by one. Empty sequences carry no element type +/// and are rejected. +fn sequence(items: &[Bound<'_, PyAny>], model: &ValueModel, depth: usize) -> PyResult { + if items.is_empty() { + return Err(PyTypeError::new_err(EMPTY_SEQUENCE)); + } + let mut exact: Option = None; + let (mut ints, mut floats, mut mixed) = (false, false, false); + for item in items { + let candidate = match classify(item, model)? { + Some(Kind::Boolean(_)) => Candidate::Exact(Element::Boolean), + Some(Kind::Tag(element) | Kind::Geometry(element)) => Candidate::Exact(element), + Some(Kind::Str) => Candidate::Exact(Element::String), + Some(Kind::DateTime) => Candidate::Exact(Element::DateTime), + Some(Kind::TimeSpan) => Candidate::Exact(Element::TimeSpan), + Some(Kind::Guid) => Candidate::Exact(Element::Guid), + Some(Kind::Int) => Candidate::PlainInt, + Some(Kind::Float) => Candidate::PlainFloat, + Some(_) => Candidate::Other, + None => return Err(unsupported(item)), + }; + match candidate { + Candidate::Exact(element) => match exact { + None => exact = Some(element), + Some(existing) if existing == element => {} + Some(_) => mixed = true, + }, + Candidate::PlainInt => ints = true, + Candidate::PlainFloat => floats = true, + Candidate::Other => mixed = true, + } + } + let element = match exact { + _ if mixed => Element::Inspectable, + None if floats => Element::Double, + None => return plain_integer_array(items), + Some(element) if !ints && !floats => element, + Some(element) if element.integer_bounds().is_some() && !floats => element, + Some(element) if element.is_float() => element, + Some(_) => Element::Inspectable, + }; + array(items, element, model, depth) +} + +fn plain_integer_array(items: &[Bound<'_, PyAny>]) -> PyResult { + let numbers = items + .iter() + .map(|item| big_integer(item, "array element")) + .collect::>>()?; + if let Ok(values) = numbers + .iter() + .map(|n| i32::try_from(*n)) + .collect::>() + { + Ok(Data::Int32Array(values)) + } else if let Ok(values) = numbers + .iter() + .map(|n| i64::try_from(*n)) + .collect::>() + { + Ok(Data::Int64Array(values)) + } else if let Ok(values) = numbers + .iter() + .map(|n| u64::try_from(*n)) + .collect::>() + { + Ok(Data::UInt64Array(values)) + } else { + Err(PyOverflowError::new_err( + "integers in this list do not all fit one WinRT Int32, Int64 or UInt64 array", + )) + } +} + +// ====================================================================== +// Enums and native values +// ====================================================================== + +/// Box a generated WinRT enum member as `IReference`, as C# does. +fn box_enum(value: &Bound<'_, PyAny>) -> PyResult { + let handle = enum_handle(&value.get_type())?; + let number: i64 = value.extract()?; + let member = handle.enum_value(number).map_err(map_dynwinrt_error)?; + dynwinrt::box_ireference(member, handle).map_err(map_dynwinrt_error) +} + +/// The enum's TypeHandle, registered from the class marker once and cached on +/// the generated class. +fn enum_handle(typ: &Bound<'_, PyType>) -> PyResult { + if let Some(cached) = typ.getattr_opt(ENUM_HANDLE_CACHE)? + && let Ok(cached) = cached.cast::() + { + return Ok(cached.try_borrow()?.0.clone()); + } + let (name, backing): (String, String) = typ.getattr(ENUM_MARKER)?.extract()?; + let underlying = match backing.as_str() { + "Int32" => dynwinrt::TypeKind::I32, + "UInt32" => dynwinrt::TypeKind::U32, + _ => { + return Err(PyTypeError::new_err(format!( + "invalid WinRT enum backing type {backing:?} for {name}" + ))); + } + }; + let members = typ + .getattr("__members__")? + .call_method0("items")? + .try_iter()? + .map(|item| { + let (member, value): (String, i64) = item?.extract()?; + let bits = match underlying { + dynwinrt::TypeKind::U32 => u32::try_from(value).ok().map(|value| value as i32), + _ => i32::try_from(value).ok(), + }; + bits.map(|bits| (member, bits)).ok_or_else(|| { + PyOverflowError::new_err(format!("{name}.{value} does not fit {backing}")) + }) + }) + .collect::>>()?; + let handle = TABLE + .enum_type_with_underlying(&name, members, underlying) + .map_err(map_dynwinrt_error)?; + typ.setattr(ENUM_HANDLE_CACHE, DynWinRTType(handle.clone()))?; + Ok(handle) +} + +/// Box a `DynWinRTValue` that holds a non-object value by its exact WinRT kind. +fn box_native(value: &WinRTValue) -> PyResult { + if let WinRTValue::Enum { type_handle, .. } = value { + return dynwinrt::box_ireference(value.clone(), type_handle.clone()) + .map_err(map_dynwinrt_error); + } + box_data(&native_payload(value)?) +} + +fn native_payload(value: &WinRTValue) -> PyResult { + let unsupported = || { + PyTypeError::new_err(format!( + "a DynWinRTValue of kind {:?} has no IPropertyValue representation", + value.get_type_kind() + )) + }; + Ok(match value { + WinRTValue::Bool(value) => Data::Boolean(*value), + WinRTValue::U8(value) => Data::UInt8(*value), + WinRTValue::I16(value) => Data::Int16(*value), + WinRTValue::U16(value) => Data::UInt16(*value), + WinRTValue::I32(value) => Data::Int32(*value), + WinRTValue::U32(value) => Data::UInt32(*value), + WinRTValue::I64(value) => Data::Int64(*value), + WinRTValue::U64(value) => Data::UInt64(*value), + WinRTValue::F32(value) => Data::Single(*value), + WinRTValue::F64(value) => Data::Double(*value), + WinRTValue::HString(value) => Data::String(value.to_string()), + WinRTValue::Guid(value) => Data::Guid(*value), + WinRTValue::Struct(data) => match data.type_handle().signature_string().as_str() { + "struct(Windows.Foundation.Point;f4;f4)" => Data::Point(Point { + X: data.get_field(0), + Y: data.get_field(1), + }), + "struct(Windows.Foundation.Size;f4;f4)" => Data::Size(Size { + Width: data.get_field(0), + Height: data.get_field(1), + }), + "struct(Windows.Foundation.Rect;f4;f4;f4;f4)" => Data::Rect(Rect { + X: data.get_field(0), + Y: data.get_field(1), + Width: data.get_field(2), + Height: data.get_field(3), + }), + "struct(Windows.Foundation.DateTime;i8)" => Data::DateTime(DateTime { + UniversalTime: data.get_field(0), + }), + "struct(Windows.Foundation.TimeSpan;i8)" => Data::TimeSpan(TimeSpan { + Duration: data.get_field(0), + }), + _ => return Err(unsupported()), + }, + WinRTValue::Array(array) => { + let values = (0..array.len()) + .map(|index| array.try_get(index).map_err(map_dynwinrt_error)) + .collect::>>()?; + macro_rules! collect { + ($variant:ident, $scalar:ident) => { + Data::$variant( + values + .iter() + .map(|value| match native_payload(value)? { + Data::$scalar(value) => Ok(value), + _ => Err(unsupported()), + }) + .collect::>()?, + ) + }; + } + match array.element_type.kind() { + dynwinrt::TypeKind::U8 => collect!(UInt8Array, UInt8), + dynwinrt::TypeKind::I16 => collect!(Int16Array, Int16), + dynwinrt::TypeKind::U16 => collect!(UInt16Array, UInt16), + dynwinrt::TypeKind::I32 => collect!(Int32Array, Int32), + dynwinrt::TypeKind::U32 => collect!(UInt32Array, UInt32), + dynwinrt::TypeKind::I64 => collect!(Int64Array, Int64), + dynwinrt::TypeKind::U64 => collect!(UInt64Array, UInt64), + dynwinrt::TypeKind::F32 => collect!(SingleArray, Single), + dynwinrt::TypeKind::F64 => collect!(DoubleArray, Double), + dynwinrt::TypeKind::Bool => collect!(BooleanArray, Boolean), + dynwinrt::TypeKind::Char16 => Data::Char16Array( + values + .iter() + .map(|value| match value { + WinRTValue::U16(unit) => Ok(*unit), + _ => Err(unsupported()), + }) + .collect::>()?, + ), + dynwinrt::TypeKind::HString => collect!(StringArray, String), + dynwinrt::TypeKind::Guid => collect!(GuidArray, Guid), + dynwinrt::TypeKind::Object => { + Data::InspectableArray(values.iter().map(WinRTValue::as_object).collect()) + } + _ => return Err(unsupported()), + } + } + _ => return Err(unsupported()), + }) +} diff --git a/bindings/py/src/package_source.rs b/bindings/py/src/package_source.rs new file mode 100644 index 00000000..6a18fc41 --- /dev/null +++ b/bindings/py/src/package_source.rs @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use std::path::PathBuf; + +use pyo3::prelude::*; + +/// Execute a Python source file shipped next to the native module into the +/// native module's globals. +/// +/// Loads from the installed package, not an embedded string or a build-time +/// path, and keeps the native globals (including `__name__`) so the defined +/// types share the native module's public identity. +pub(crate) fn exec_package_source( + module: &Bound<'_, PyModule>, + file_name: &str, + module_name: &str, +) -> PyResult<()> { + let py = module.py(); + let source_path = module + .getattr("__spec__")? + .getattr("origin")? + .extract::()? + .with_file_name(file_name); + let source_path = py.import("os")?.call_method1("fspath", (source_path,))?; + let loader = py + .import("importlib.machinery")? + .getattr("SourceFileLoader")? + .call1((module_name, source_path))?; + let source = loader.call_method1("get_code", (module_name,))?; + py.import("builtins")? + .call_method1("exec", (source, module.dict()))?; + Ok(()) +} diff --git a/bindings/py/src/runtime.rs b/bindings/py/src/runtime.rs index 3b717856..c403f184 100644 --- a/bindings/py/src/runtime.rs +++ b/bindings/py/src/runtime.rs @@ -6,13 +6,13 @@ use std::sync::{Arc, Mutex}; use dynwinrt; use pyo3::exceptions::{PyIndexError, PyOverflowError, PyRuntimeError, PyTypeError}; use pyo3::prelude::*; -use pyo3::types::{PyBytes, PyDict, PyList}; +use pyo3::types::PyDict; use windows::core::{GUID, HSTRING, IUnknown, Interface}; use crate::errors::{map_dynwinrt_error, map_dynwinrt_error_with_context, map_windows_error}; /// Shared MetadataTable — created once, used everywhere. -static TABLE: std::sync::LazyLock> = +pub(crate) static TABLE: std::sync::LazyLock> = std::sync::LazyLock::new(|| dynwinrt::MetadataTable::new()); pub(crate) static WINUI_MODULES: dynwinrt::WinUiProcessModules = @@ -224,99 +224,6 @@ pub fn ro_uninitialize() { unsafe { RoUninitialize() }; } -fn python_uuid(py: Python<'_>, value: GUID) -> PyResult> { - Ok(py - .import("uuid")? - .getattr("UUID")? - .call1((format!("{value:?}"),))? - .unbind()) -} - -fn python_char(py: Python<'_>, value: u16) -> PyResult> { - Ok(py - .import("builtins")? - .getattr("chr")? - .call1((u32::from(value),))? - .unbind()) -} - -fn property_value_to_python( - py: Python<'_>, - value: dynwinrt::PropertyValueData, -) -> PyResult> { - use dynwinrt::PropertyValueData; - - macro_rules! into_python { - ($value:expr) => { - Ok($value.into_pyobject(py)?.into_any().unbind()) - }; - } - - match value { - PropertyValueData::UInt8(value) => into_python!(value), - PropertyValueData::Int16(value) => into_python!(value), - PropertyValueData::UInt16(value) => into_python!(value), - PropertyValueData::Int32(value) => into_python!(value), - PropertyValueData::UInt32(value) => into_python!(value), - PropertyValueData::Int64(value) => into_python!(value), - PropertyValueData::UInt64(value) => into_python!(value), - PropertyValueData::Single(value) => into_python!(value), - PropertyValueData::Double(value) => into_python!(value), - PropertyValueData::Char16(value) => python_char(py, value), - PropertyValueData::Boolean(value) => { - Ok(value.into_pyobject(py)?.to_owned().into_any().unbind()) - } - PropertyValueData::String(value) => into_python!(value), - PropertyValueData::Guid(value) => python_uuid(py, value), - PropertyValueData::UInt8Array(value) => Ok(PyBytes::new(py, &value).into_any().unbind()), - PropertyValueData::Int16Array(value) => into_python!(value), - PropertyValueData::UInt16Array(value) => into_python!(value), - PropertyValueData::Int32Array(value) => into_python!(value), - PropertyValueData::UInt32Array(value) => into_python!(value), - PropertyValueData::Int64Array(value) => into_python!(value), - PropertyValueData::UInt64Array(value) => into_python!(value), - PropertyValueData::SingleArray(value) => into_python!(value), - PropertyValueData::DoubleArray(value) => into_python!(value), - PropertyValueData::Char16Array(value) => { - let values = value - .into_iter() - .map(|value| python_char(py, value)) - .collect::>>()?; - Ok(PyList::new(py, values)?.into_any().unbind()) - } - PropertyValueData::BooleanArray(value) => into_python!(value), - PropertyValueData::StringArray(value) => into_python!(value), - PropertyValueData::GuidArray(value) => { - let values = value - .into_iter() - .map(|value| python_uuid(py, value)) - .collect::>>()?; - Ok(PyList::new(py, values)?.into_any().unbind()) - } - } -} - -/// Explicitly unbox a supported WinRT `IPropertyValue`. -/// -/// Python `None` and WinRT null are returned as `None`. A non-`IPropertyValue` -/// `DynWinRTValue` is returned unchanged, preserving Python and COM identity. -#[pyfunction] -pub fn unbox_object(py: Python<'_>, value: &Bound<'_, PyAny>) -> PyResult> { - if value.is_none() { - return Ok(py.None()); - } - - let result = { - let raw = value.extract::>()?; - dynwinrt::unbox_property_value(&raw.0).map_err(map_dynwinrt_error)? - }; - match result { - dynwinrt::PropertyValueUnboxResult::Null => Ok(py.None()), - dynwinrt::PropertyValueUnboxResult::NotPropertyValue => Ok(value.clone().unbind()), - dynwinrt::PropertyValueUnboxResult::Value(value) => property_value_to_python(py, value), - } -} - // ====================================================================== // Process-local named XAML runtime classes // ====================================================================== diff --git a/bindings/py/stubtest_allowlist.txt b/bindings/py/stubtest_allowlist.txt index b0caff0f..c3dbcc13 100644 --- a/bindings/py/stubtest_allowlist.txt +++ b/bindings/py/stubtest_allowlist.txt @@ -1 +1,3 @@ dynwinrt\.dynwinrt +# Executed into the native module namespace; never imported on its own. +dynwinrt\._values diff --git a/bindings/py/tests/test_e2e_winrt.py b/bindings/py/tests/test_e2e_winrt.py index b9df955b..215a1f43 100644 --- a/bindings/py/tests/test_e2e_winrt.py +++ b/bindings/py/tests/test_e2e_winrt.py @@ -453,13 +453,16 @@ def test_explicit_unbox_object_for_device_property_values(self): ) ) date_time.set_i64(0, 0) - unsupported = statics_h["CreateDateTime"].invoke( + boxed_date_time = statics_h["CreateDateTime"].invoke( factory, [date_time.to_value()] ) - import pytest + from datetime import datetime, timezone - with pytest.raises(OSError, match="Unsupported WinRT IPropertyValue type"): - unbox_object(unsupported) + # DateTime is part of the full PropertyType model: tick 0 is 1601-01-01 UTC. + assert unbox_object(boxed_date_time) == datetime(1601, 1, 1, tzinfo=timezone.utc) + # Explicit unboxing is idempotent on values that are already unboxed. + assert unbox_object("BLE Device") == "BLE Device" + assert unbox_object(unbox_object(boxed_int64)) == -(2**63) key_type = DynWinRTType.hstring() object_type = DynWinRTType.object() diff --git a/bindings/py/tests/test_object_values.py b/bindings/py/tests/test_object_values.py new file mode 100644 index 00000000..096cc21c --- /dev/null +++ b/bindings/py/tests/test_object_values.py @@ -0,0 +1,490 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +"""The WinRT ``Object`` value model and its single conversion layer. + +``to_winrt_object`` / ``from_winrt_object`` are the only conversions generated +code uses for ``Object`` (IInspectable) positions. These tests pin their +contract without generated bindings: + +- R1: a boxed value read and written back keeps its PropertyType and value. +- R2: a supported Python value written and read back compares equal. +- I1: objects that are not boxed values pass through as the same object. +- I2: boxed values have value semantics (every write creates a new box). +""" + +import copy +import json +import math +import pickle +from datetime import datetime, timedelta, timezone +from enum import IntEnum, IntFlag +from uuid import UUID + +import pytest + +import dynwinrt +from dynwinrt import ( + DynWinRTStruct, + DynWinRTType, + DynWinRTValue, + WinGUID, + from_winrt_object, + ro_initialize, + to_winrt_object, + unbox_object, +) + +ro_initialize(1) + +IID_IPROPERTY_VALUE = WinGUID.parse("4bd682dd-7554-40e9-9a9b-82654ede7e62") +IREFERENCE_PIID = WinGUID.parse("61c17706-2d65-11e0-9ae8-d48564015472") +PROPERTY_TYPES = { + "UInt8": 1, "Int16": 2, "UInt16": 3, "Int32": 4, "UInt32": 5, "Int64": 6, + "UInt64": 7, "Single": 8, "Double": 9, "Char16": 10, "Boolean": 11, "String": 12, + "DateTime": 14, "TimeSpan": 15, "Guid": 16, "Point": 17, "Size": 18, "Rect": 19, + "UInt8Array": 1025, "Int16Array": 1026, "UInt16Array": 1027, "Int32Array": 1028, + "UInt32Array": 1029, "Int64Array": 1030, "UInt64Array": 1031, "SingleArray": 1032, + "DoubleArray": 1033, "Char16Array": 1034, "BooleanArray": 1035, "StringArray": 1036, + "InspectableArray": 1037, "DateTimeArray": 1038, "TimeSpanArray": 1039, + "GuidArray": 1040, "PointArray": 1041, "SizeArray": 1042, "RectArray": 1043, +} +UTC = timezone.utc + + +def property_type(raw): + """Read IPropertyValue.Type through a raw vtable call (no registrations).""" + assert isinstance(raw, DynWinRTValue) + return raw.cast(IID_IPROPERTY_VALUE).call_0(6, DynWinRTType.i32_type()).to_number() + + +def type_name(raw): + number = property_type(raw) + return next(name for name, value in PROPERTY_TYPES.items() if value == number) + + +def uri(): + factory = DynWinRTValue.activation_factory("Windows.Foundation.Uri") + # IUriRuntimeClassFactory.CreateUri is vtable slot 6. + iid = WinGUID.parse("44a9796f-723e-4fdf-a218-033e75b0c084") + signature = ( + dynwinrt.DynWinRTMethodSig() + .add_in(DynWinRTType.hstring()) + .add_out(DynWinRTType.object()) + ) + handle = DynWinRTType.register_interface("IUriRuntimeClassFactory_ObjectValues", iid) + handle = handle.add_method("CreateUri", signature) + return handle.method(6).invoke(factory.cast(iid), [DynWinRTValue.from_hstring("https://example.com/")]) + + +class Mode(IntEnum): + Off = 0 + On = 1 + + +Mode._dynwinrt_enum_type = ("Contoso.ObjectValues.Mode", "Int32") + + +class Options(IntFlag): + Low = 1 + High = 0x8000_0000 + + +Options._dynwinrt_enum_type = ("Contoso.ObjectValues.Options", "UInt32") + + +class UnmarkedMode(IntEnum): + A = 7 + + +class GeneratedPoint: + """Shape of the generated windows.foundation.Point struct.""" + + __slots__ = ("x", "y") + _dynwinrt_property_type = "Point" + + def __init__(self, x=0.0, y=0.0): + self.x = x + self.y = y + + def __eq__(self, other): + if type(other) is not type(self): + return NotImplemented + return (self.x, self.y) == (other.x, other.y) + + +class Wrapper: + """Shape of a generated projected wrapper.""" + + def __init__(self, obj): + self._obj = obj + + +# ---------------------------------------------------------------------- +# The tagged value model +# ---------------------------------------------------------------------- + + +@pytest.mark.parametrize( + ("tag", "low", "high"), + [ + (dynwinrt.UInt8, 0, 2**8 - 1), + (dynwinrt.Int16, -(2**15), 2**15 - 1), + (dynwinrt.UInt16, 0, 2**16 - 1), + (dynwinrt.Int32, -(2**31), 2**31 - 1), + (dynwinrt.UInt32, 0, 2**32 - 1), + (dynwinrt.Int64, -(2**63), 2**63 - 1), + (dynwinrt.UInt64, 0, 2**64 - 1), + ], +) +def test_integer_tags_validate_their_range_and_behave_like_int(tag, low, high): + assert tag(low) == low and tag(high) == high + for invalid in (low - 1, high + 1): + with pytest.raises(OverflowError, match=tag.__name__): + tag(invalid) + with pytest.raises(TypeError): + tag(1.5) + value = tag(5) + assert isinstance(value, int) and isinstance(value, dynwinrt.WinRTScalar) + assert value == 5 and hash(value) == hash(5) and {value: "x"}[5] == "x" + assert repr(value) == f"dynwinrt.{tag.__name__}(5)" + assert str(value) == "5" and f"{value}" == "5" and f"{value:03}" == "005" + assert json.dumps({"n": value}) == '{"n": 5}' + assert type(value + 1) is int and value + 1 == 6 + assert type(-value) is int and type(value * 2) is int + assert value.property_type == tag.__name__ + assert type(tag.__module__) is str and tag.__module__ == "dynwinrt" + assert pickle.loads(pickle.dumps(value)) == 5 + assert type(pickle.loads(pickle.dumps(value))) is tag + assert type(copy.deepcopy(value)) is tag + + +def test_float_and_char16_tags(): + single = dynwinrt.Single(0.1) + assert single != 0.1 and single == 0.10000000149011612 + assert repr(single) == "dynwinrt.Single(0.10000000149011612)" + assert json.dumps(single) == "0.10000000149011612" + assert type(single * 2) is float + assert math.isinf(dynwinrt.Single(float("inf"))) + assert math.isnan(dynwinrt.Single(float("nan"))) + with pytest.raises(OverflowError, match="Single"): + dynwinrt.Single(1e39) + with pytest.raises(TypeError): + dynwinrt.Single("1.5") + double = dynwinrt.Double(3) + assert double == 3.0 and type(double + 1) is float + assert repr(double) == "dynwinrt.Double(3.0)" + + char = dynwinrt.Char16("\ud800") + assert isinstance(char, str) and char == "\ud800" and len(char) == 1 + assert repr(char) == "dynwinrt.Char16('\\ud800')" + assert type(char + "x") is str + with pytest.raises(ValueError): + dynwinrt.Char16("ab") + with pytest.raises(ValueError): + dynwinrt.Char16("\U0001f600") + with pytest.raises(TypeError): + dynwinrt.Char16(97) + assert type(pickle.loads(pickle.dumps(char))) is dynwinrt.Char16 + + +def test_typed_arrays_are_lists_that_remember_their_type(): + values = dynwinrt.UInt32Array([1, 2]) + assert isinstance(values, list) and isinstance(values, dynwinrt.WinRTArray) + assert values == [1, 2] and values.property_type == "UInt32Array" + assert repr(values) == "dynwinrt.UInt32Array([1, 2])" + assert type(values.copy()) is dynwinrt.UInt32Array + assert type(copy.copy(values)) is dynwinrt.UInt32Array + assert type(pickle.loads(pickle.dumps(values))) is dynwinrt.UInt32Array + assert json.dumps(values) == "[1, 2]" + assert type(values[:1]) is list + + +def test_geometry_values_are_immutable_float32_values(): + point = dynwinrt.Point(0.1, 2) + assert point.x == dynwinrt.Single(0.1) and point.y == 2.0 + assert repr(point) == "dynwinrt.Point(x=0.10000000149011612, y=2.0)" + with pytest.raises(AttributeError): + point.x = 3 + assert point == dynwinrt.Point(x=0.1, y=2.0) + assert hash(point) == hash(dynwinrt.Point(0.1, 2.0)) + assert point != dynwinrt.Size(0.1, 2.0) + assert point != (0.1, 2.0) + # Equal to the generated struct when both box to the same float32 fields. + generated = GeneratedPoint(0.1, 2.0) + assert point == generated and generated == point + assert point != GeneratedPoint(0.2, 2.0) + assert pickle.loads(pickle.dumps(point)) == point + match dynwinrt.Rect(1, 2, 3, 4): + case dynwinrt.Rect(x, y, width, height): + assert (x, y, width, height) == (1.0, 2.0, 3.0, 4.0) + assert dynwinrt.Size(width=3).width == 3.0 and dynwinrt.Size().height == 0.0 + with pytest.raises(TypeError): + dynwinrt.Point(1, 2, 3) + with pytest.raises(OverflowError): + dynwinrt.Point(1e39, 0) + + +# ---------------------------------------------------------------------- +# R1/R2: round trips for every PropertyType +# ---------------------------------------------------------------------- + +EPOCH = datetime(1601, 1, 1, tzinfo=UTC) +WRITE_READ = [ + # (written Python value, expected PropertyType, expected read value, exact read type) + (True, "Boolean", True, bool), + (5, "Int32", 5, int), + (2**40, "Int64", 2**40, dynwinrt.Int64), + (2**63, "UInt64", 2**63, dynwinrt.UInt64), + (1.5, "Double", 1.5, float), + ("text", "String", "text", str), + (UUID(int=42), "Guid", UUID(int=42), UUID), + (datetime(2024, 5, 6, 7, 8, 9, 123456, tzinfo=UTC), "DateTime", + datetime(2024, 5, 6, 7, 8, 9, 123456, tzinfo=UTC), datetime), + (datetime(2024, 5, 6, 9, tzinfo=timezone(timedelta(hours=2))), "DateTime", + datetime(2024, 5, 6, 7, tzinfo=UTC), datetime), + (timedelta(days=-1, microseconds=5), "TimeSpan", timedelta(days=-1, microseconds=5), timedelta), + (b"\x00\xff", "UInt8Array", b"\x00\xff", bytes), + (bytearray(b"ab"), "UInt8Array", b"ab", bytes), + (memoryview(b"xyz"), "UInt8Array", b"xyz", bytes), + (dynwinrt.UInt8(255), "UInt8", 255, dynwinrt.UInt8), + (dynwinrt.Int16(-5), "Int16", -5, dynwinrt.Int16), + (dynwinrt.UInt16(65535), "UInt16", 65535, dynwinrt.UInt16), + (dynwinrt.Int32(7), "Int32", 7, int), + (dynwinrt.UInt32(2**32 - 1), "UInt32", 2**32 - 1, dynwinrt.UInt32), + (dynwinrt.Int64(-(2**63)), "Int64", -(2**63), dynwinrt.Int64), + (dynwinrt.UInt64(2**64 - 1), "UInt64", 2**64 - 1, dynwinrt.UInt64), + (dynwinrt.Single(0.5), "Single", 0.5, dynwinrt.Single), + (dynwinrt.Double(3), "Double", 3.0, float), + (dynwinrt.Char16("\ud800"), "Char16", "\ud800", dynwinrt.Char16), + (dynwinrt.Point(1.5, 2.5), "Point", dynwinrt.Point(1.5, 2.5), dynwinrt.Point), + (GeneratedPoint(1.5, 2.5), "Point", dynwinrt.Point(1.5, 2.5), dynwinrt.Point), + (dynwinrt.Size(3, 4), "Size", dynwinrt.Size(3, 4), dynwinrt.Size), + (dynwinrt.Rect(1, 2, 3, 4), "Rect", dynwinrt.Rect(1, 2, 3, 4), dynwinrt.Rect), + ([1, 2], "Int32Array", [1, 2], dynwinrt.Int32Array), + ((1, 2), "Int32Array", [1, 2], dynwinrt.Int32Array), + ([1, 2**40], "Int64Array", [1, 2**40], dynwinrt.Int64Array), + ([1, 2**63], "UInt64Array", [1, 2**63], dynwinrt.UInt64Array), + ([1, 2.5], "DoubleArray", [1.0, 2.5], dynwinrt.DoubleArray), + (["a", ""], "StringArray", ["a", ""], dynwinrt.StringArray), + ([True, False], "BooleanArray", [True, False], dynwinrt.BooleanArray), + ([UUID(int=1)], "GuidArray", [UUID(int=1)], dynwinrt.GuidArray), + ([EPOCH], "DateTimeArray", [EPOCH], dynwinrt.DateTimeArray), + ([timedelta(1)], "TimeSpanArray", [timedelta(1)], dynwinrt.TimeSpanArray), + ([dynwinrt.Point(1, 2)], "PointArray", [dynwinrt.Point(1, 2)], dynwinrt.PointArray), + ([dynwinrt.Size(1, 2)], "SizeArray", [dynwinrt.Size(1, 2)], dynwinrt.SizeArray), + ([dynwinrt.Rect(1, 2, 3, 4)], "RectArray", [dynwinrt.Rect(1, 2, 3, 4)], dynwinrt.RectArray), + ([dynwinrt.Char16("a")], "Char16Array", ["a"], dynwinrt.Char16Array), + ([dynwinrt.UInt8(1), dynwinrt.UInt8(2)], "UInt8Array", b"\x01\x02", bytes), + ([dynwinrt.UInt32(1), 2], "UInt32Array", [1, 2], dynwinrt.UInt32Array), + ([dynwinrt.Single(1), 2, 3.5], "SingleArray", [1.0, 2.0, 3.5], dynwinrt.SingleArray), + ([1, "a", None], "InspectableArray", [1, "a", None], dynwinrt.InspectableArray), + ([True, 1], "InspectableArray", [True, 1], dynwinrt.InspectableArray), + ([[1, 2], ["x"]], "InspectableArray", [[1, 2], ["x"]], dynwinrt.InspectableArray), + (dynwinrt.StringArray([]), "StringArray", [], dynwinrt.StringArray), + (dynwinrt.InspectableArray([]), "InspectableArray", [], dynwinrt.InspectableArray), + (dynwinrt.InspectableArray([1, 2]), "InspectableArray", [1, 2], dynwinrt.InspectableArray), + (dynwinrt.UInt16Array([1, 2]), "UInt16Array", [1, 2], dynwinrt.UInt16Array), +] + + +@pytest.mark.parametrize( + ("written", "expected_type", "expected", "read_type"), + WRITE_READ, + ids=[f"{index}-{case[1]}" for index, case in enumerate(WRITE_READ)], +) +def test_written_values_read_back_equal_with_their_exact_type( + written, expected_type, expected, read_type +): + boxed = to_winrt_object(written) + assert type_name(boxed) == expected_type + read = from_winrt_object(boxed) + assert type(read) is read_type, repr(read) + assert read == expected + # R1: the value read writes back as the same PropertyType and value. + rewritten = to_winrt_object(read) + assert rewritten is not boxed + assert type_name(rewritten) == expected_type + assert from_winrt_object(rewritten) == expected + + +def test_array_elements_keep_their_tags_through_list_operations(): + read = from_winrt_object(to_winrt_object(dynwinrt.UInt32Array([1, 2, 3]))) + assert read == [1, 2, 3] + assert all(type(value) is dynwinrt.UInt32 for value in read) + # Slicing and concatenation drop the list subclass, not the element tags. + for derived in (read[:2], read + [4], [value for value in read]): + assert type(derived) is list + assert type_name(to_winrt_object(derived)) == "UInt32Array" + nested = from_winrt_object(to_winrt_object([dynwinrt.Int16(1), "x", [dynwinrt.UInt8(2)]])) + assert type(nested) is dynwinrt.InspectableArray + assert type(nested[0]) is dynwinrt.Int16 and nested[2] == b"\x02" + + +def test_datetime_and_timespan_keep_microseconds_and_truncate_ticks(): + value = datetime(1999, 12, 31, 23, 59, 59, 999999, tzinfo=UTC) + assert from_winrt_object(to_winrt_object(value)) == value + date_type = DynWinRTType.struct_type("Windows.Foundation.DateTime", [DynWinRTType.i64_type()]) + ticks = DynWinRTStruct.create(date_type) + ticks.set_i64(0, 7) # 700 ns after the WinRT epoch + assert from_winrt_object(to_winrt_object(ticks.to_value())) == EPOCH + + +# ---------------------------------------------------------------------- +# Write rules and their pinned order +# ---------------------------------------------------------------------- + + +def test_write_rule_order_for_int_subclasses_and_markers(): + # bool before int, tags before int, enum markers before int. + assert type_name(to_winrt_object(True)) == "Boolean" + assert type_name(to_winrt_object(dynwinrt.UInt32(1))) == "UInt32" + assert type_name(to_winrt_object(UnmarkedMode.A)) == "Int32" + + enum_type = DynWinRTType.enum_type("Contoso.ObjectValues.Mode", ["Off", "On"], [0, 1]) + boxed = to_winrt_object(Mode.On) + reference_iid = DynWinRTType.parameterized(IREFERENCE_PIID, [enum_type]).iid() + assert boxed.cast(reference_iid).call_0(6, enum_type).get_enum_int() == 1 + # IReference is not an IPropertyValue: it reads back as the same object + # and writes back unchanged (I1), like any other WinRT object. + assert from_winrt_object(boxed) is boxed + assert to_winrt_object(boxed) is boxed + + flags = DynWinRTType.enum_type( + "Contoso.ObjectValues.Options", ["Low", "High"], [1, 0x8000_0000], DynWinRTType.u32_type() + ) + flag_iid = DynWinRTType.parameterized(IREFERENCE_PIID, [flags]).iid() + boxed = to_winrt_object(Options.High | Options.Low) + assert boxed.cast(flag_iid).call_0(6, flags).get_enum_int() == 0x8000_0001 + + # Geometry markers: dynwinrt values and generated structs box as Point. + assert type_name(to_winrt_object(GeneratedPoint(1, 2))) == "Point" + # Typed arrays before list/tuple inference. + assert type_name(to_winrt_object(dynwinrt.DoubleArray([1, 2]))) == "DoubleArray" + + +def test_native_values_and_wrappers_pass_through_by_identity(): + raw = uri() + assert to_winrt_object(raw) is raw + assert to_winrt_object(Wrapper(raw)) is raw + assert from_winrt_object(raw) is raw + null = DynWinRTValue.null_value() + assert to_winrt_object(null) is null + assert from_winrt_object(null) is None + assert to_winrt_object(None).is_null() + # Native values holding a non-object payload box by their exact kind. + assert type_name(to_winrt_object(DynWinRTValue.from_u32(5))) == "UInt32" + assert type_name(to_winrt_object(DynWinRTValue.from_hstring("s"))) == "String" + point_type = DynWinRTType.struct_type( + "Windows.Foundation.Point", [DynWinRTType.f32_type(), DynWinRTType.f32_type()] + ) + point = DynWinRTStruct.create(point_type) + point.set_f32(0, 1.5) + assert from_winrt_object(to_winrt_object(point.to_value())) == dynwinrt.Point(1.5, 0) + with pytest.raises(TypeError, match="no IPropertyValue representation"): + to_winrt_object(DynWinRTValue.from_i8(1)) + + +def test_unsupported_inputs_raise_helpful_errors(): + for value in (object(), {1: 2}, {1, 2}, range(3), 1 + 2j): + with pytest.raises(TypeError, match="expected None, a DynWinRTValue"): + to_winrt_object(value) + with pytest.raises(TypeError, match="empty list or tuple"): + to_winrt_object([]) + with pytest.raises(TypeError, match="empty list or tuple"): + to_winrt_object(()) + with pytest.raises(ValueError, match="timezone-aware"): + to_winrt_object(datetime(2024, 1, 1)) + with pytest.raises(OverflowError, match="Int32, Int64 or UInt64"): + to_winrt_object(2**64) + with pytest.raises(OverflowError): + to_winrt_object([-1, 2**64 - 1]) + with pytest.raises(OverflowError, match="UInt32Array element 1"): + to_winrt_object(dynwinrt.UInt32Array([1, -1])) + with pytest.raises(TypeError, match="StringArray element 0"): + to_winrt_object(dynwinrt.StringArray([1])) + with pytest.raises(TypeError, match="abstract"): + to_winrt_object(dynwinrt.WinRTArray([1])) + with pytest.raises(TypeError, match="expected None"): + to_winrt_object([1, object()]) + + +# ---------------------------------------------------------------------- +# Read rules, identity and idempotency +# ---------------------------------------------------------------------- + + +def test_from_winrt_object_is_idempotent_and_unbox_object_is_the_same_function(): + assert unbox_object is from_winrt_object + for plain in (None, 5, "x", dynwinrt.UInt32(5), [1, 2], dynwinrt.Point(1, 2), object()): + assert from_winrt_object(plain) is plain + once = from_winrt_object(to_winrt_object(dynwinrt.UInt64(9))) + assert from_winrt_object(once) is once + + +def test_unrepresentable_values_stay_native_instead_of_raising(): + date_type = DynWinRTType.struct_type("Windows.Foundation.DateTime", [DynWinRTType.i64_type()]) + ticks = DynWinRTStruct.create(date_type) + ticks.set_i64(0, 2**62) # far beyond Python's datetime.max + boxed = to_winrt_object(ticks.to_value()) + assert type_name(boxed) == "DateTime" + assert from_winrt_object(boxed) is boxed + array = to_winrt_object([ticks.to_value()]) + assert type_name(array) == "InspectableArray" + unboxed = from_winrt_object(array) + assert type(unboxed) is dynwinrt.InspectableArray + assert isinstance(unboxed[0], DynWinRTValue) + + +def test_inspectable_arrays_convert_elements_and_keep_object_identity(): + raw = uri() + unboxed = from_winrt_object(to_winrt_object([None, raw, 3, [dynwinrt.UInt16(4)]])) + assert unboxed[0] is None + assert unboxed[1].identity_raw() == raw.identity_raw() + assert unboxed[2] == 3 and unboxed[3] == [4] and type(unboxed[3][0]) is dynwinrt.UInt16 + rewritten = from_winrt_object(to_winrt_object(unboxed)) + assert rewritten[1].identity_raw() == raw.identity_raw() + + +def test_nesting_is_bounded_in_both_directions(): + cyclic = [1, "x"] + cyclic.append(cyclic) + with pytest.raises(RecursionError, match="64 levels"): + to_winrt_object(cyclic) + deep = 1 + for _ in range(64): + deep = [deep, "x"] + boxed = to_winrt_object(deep) + read = from_winrt_object(boxed) + for _ in range(64): + assert type(read) is dynwinrt.InspectableArray + read = read[0] + assert read == 1 + with pytest.raises(RecursionError): + to_winrt_object([deep, "x"]) + # A box nested one level deeper (built from an existing box) still reads; + # values beyond the limit stay native instead of raising. + deeper = from_winrt_object(to_winrt_object([boxed, "x"])) + for _ in range(65): + deeper = deeper[0] + assert isinstance(deeper, DynWinRTValue) + assert from_winrt_object(deeper) == 1 + + +def test_boxes_have_value_semantics(): + first = to_winrt_object(42) + second = to_winrt_object(from_winrt_object(first)) + assert first.identity_raw() != second.identity_raw() + assert from_winrt_object(first) == from_winrt_object(second) == 42 + + +def test_aliases_describe_the_runtime_model(): + for value in (True, 1, 1.5, "s", UUID(int=1), EPOCH, timedelta(1), b"", + dynwinrt.Point(), dynwinrt.Size(), dynwinrt.Rect(), [1], uri()): + assert isinstance(value, dynwinrt.WinRTObjectValue) + for value in ((1,), bytearray(), memoryview(b"")): + assert isinstance(value, dynwinrt.WinRTObjectInput) + assert not isinstance(value, dynwinrt.WinRTObjectValue) + exported = set(dynwinrt.__all__) + assert {"to_winrt_object", "from_winrt_object", "unbox_object", "UInt32", + "InspectableArray", "Point", "WinRTObjectValue", "WinRTObjectInput"} <= exported diff --git a/crates/dynwinrt/src/lib.rs b/crates/dynwinrt/src/lib.rs index 4465e6a9..82e4cd77 100644 --- a/crates/dynwinrt/src/lib.rs +++ b/crates/dynwinrt/src/lib.rs @@ -60,7 +60,7 @@ pub use crate::element_factory::{ pub use crate::ibuffer::{copy_from_ibuffer, copy_to_ibuffer}; pub use crate::metadata_table::{MetadataTable, MethodHandle, TypeHandle, TypeKind, ValueTypeData}; pub use crate::property_value::{ - PropertyValueData, PropertyValueUnboxResult, unbox_property_value, + PropertyValueData, PropertyValueUnboxResult, box_property_value, unbox_property_value, }; pub use crate::reference::box_ireference; pub use crate::result::{Error, Result}; diff --git a/crates/dynwinrt/src/property_value.rs b/crates/dynwinrt/src/property_value.rs index f318d8ad..144b5699 100644 --- a/crates/dynwinrt/src/property_value.rs +++ b/crates/dynwinrt/src/property_value.rs @@ -1,15 +1,29 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -use windows::Foundation::{IPropertyValue, PropertyType}; -use windows_core::{Array, GUID, HRESULT, HSTRING, Interface}; +//! Language-neutral boxing and unboxing of WinRT `IPropertyValue` objects. +//! +//! This module is the single core definition of which +//! `Windows.Foundation.PropertyType` values a projection can convert to and from +//! plain data. Language bindings map [`PropertyValueData`] to their own value +//! model; they never call `IPropertyValue` getters or `PropertyValue` factories +//! themselves. + +use windows::Foundation::{ + DateTime, IPropertyValue, Point, PropertyType, PropertyValue, Rect, Size, TimeSpan, +}; +use windows_core::{Array, GUID, HRESULT, HSTRING, IInspectable, IUnknown, Interface}; use crate::{Error, Result, WinRTValue}; const E_NOINTERFACE: HRESULT = HRESULT(0x80004002_u32 as i32); -const E_NOTIMPL: HRESULT = HRESULT(0x80004001_u32 as i32); -/// A language-neutral value explicitly read from a WinRT `IPropertyValue`. +/// The payload of a WinRT `IPropertyValue`, for every `PropertyType` that has a +/// language-neutral representation. +/// +/// `Empty`, `Inspectable`, `OtherType` and `OtherTypeArray` carry no payload a +/// projection could reproduce, so reading them yields +/// [`PropertyValueUnboxResult::Unsupported`] instead of a value. #[derive(Debug, Clone, PartialEq)] pub enum PropertyValueData { UInt8(u8), @@ -25,6 +39,11 @@ pub enum PropertyValueData { Boolean(bool), String(String), Guid(GUID), + DateTime(DateTime), + TimeSpan(TimeSpan), + Point(Point), + Size(Size), + Rect(Rect), UInt8Array(Vec), Int16Array(Vec), UInt16Array(Vec), @@ -37,17 +56,75 @@ pub enum PropertyValueData { Char16Array(Vec), BooleanArray(Vec), StringArray(Vec), + /// Arbitrary WinRT objects; `None` is a null element. Bindings convert the + /// elements with their own `Object` conversion, recursively. + InspectableArray(Vec>), + DateTimeArray(Vec), + TimeSpanArray(Vec), GuidArray(Vec), + PointArray(Vec), + SizeArray(Vec), + RectArray(Vec), } -/// Result of explicitly attempting to unbox a raw WinRT object. +impl PropertyValueData { + /// The `Windows.Foundation.PropertyType` this payload boxes as. + pub fn property_type(&self) -> PropertyType { + match self { + Self::UInt8(_) => PropertyType::UInt8, + Self::Int16(_) => PropertyType::Int16, + Self::UInt16(_) => PropertyType::UInt16, + Self::Int32(_) => PropertyType::Int32, + Self::UInt32(_) => PropertyType::UInt32, + Self::Int64(_) => PropertyType::Int64, + Self::UInt64(_) => PropertyType::UInt64, + Self::Single(_) => PropertyType::Single, + Self::Double(_) => PropertyType::Double, + Self::Char16(_) => PropertyType::Char16, + Self::Boolean(_) => PropertyType::Boolean, + Self::String(_) => PropertyType::String, + Self::Guid(_) => PropertyType::Guid, + Self::DateTime(_) => PropertyType::DateTime, + Self::TimeSpan(_) => PropertyType::TimeSpan, + Self::Point(_) => PropertyType::Point, + Self::Size(_) => PropertyType::Size, + Self::Rect(_) => PropertyType::Rect, + Self::UInt8Array(_) => PropertyType::UInt8Array, + Self::Int16Array(_) => PropertyType::Int16Array, + Self::UInt16Array(_) => PropertyType::UInt16Array, + Self::Int32Array(_) => PropertyType::Int32Array, + Self::UInt32Array(_) => PropertyType::UInt32Array, + Self::Int64Array(_) => PropertyType::Int64Array, + Self::UInt64Array(_) => PropertyType::UInt64Array, + Self::SingleArray(_) => PropertyType::SingleArray, + Self::DoubleArray(_) => PropertyType::DoubleArray, + Self::Char16Array(_) => PropertyType::Char16Array, + Self::BooleanArray(_) => PropertyType::BooleanArray, + Self::StringArray(_) => PropertyType::StringArray, + Self::InspectableArray(_) => PropertyType::InspectableArray, + Self::DateTimeArray(_) => PropertyType::DateTimeArray, + Self::TimeSpanArray(_) => PropertyType::TimeSpanArray, + Self::GuidArray(_) => PropertyType::GuidArray, + Self::PointArray(_) => PropertyType::PointArray, + Self::SizeArray(_) => PropertyType::SizeArray, + Self::RectArray(_) => PropertyType::RectArray, + } + } +} + +/// Result of attempting to unbox a raw WinRT object. #[derive(Debug, Clone, PartialEq)] pub enum PropertyValueUnboxResult { /// The input was the WinRT null object. Null, /// The input does not implement `IPropertyValue` and must be preserved. NotPropertyValue, - /// The input was a supported boxed property value. + /// The input is an `IPropertyValue` whose `PropertyType` has no + /// language-neutral payload (`Empty`, `Inspectable`, `OtherType`, + /// `OtherTypeArray`, or a value unknown to this version). It must be + /// preserved like [`PropertyValueUnboxResult::NotPropertyValue`]. + Unsupported(PropertyType), + /// The input was a boxed value with a language-neutral payload. Value(PropertyValueData), } @@ -59,11 +136,13 @@ macro_rules! read_array { }}; } -/// Explicitly reads a supported `IPropertyValue` without consuming or mutating `value`. +/// Reads a boxed value without consuming or mutating `value`. /// -/// A QueryInterface failure with `E_NOINTERFACE` is reported as -/// [`PropertyValueUnboxResult::NotPropertyValue`]. Other QueryInterface failures, -/// unsupported `PropertyType` values, and getter failures are surfaced as errors. +/// Null and non-`IPropertyValue` objects are classified rather than rejected, +/// and so are `IPropertyValue` objects without a language-neutral payload. A +/// QueryInterface failure other than `E_NOINTERFACE` and a failing getter for a +/// supported `PropertyType` are surfaced as errors: those objects are broken, +/// not merely unsupported. pub fn unbox_property_value(value: &WinRTValue) -> Result { if value.is_null_object() { return Ok(PropertyValueUnboxResult::Null); @@ -80,129 +159,280 @@ pub fn unbox_property_value(value: &WinRTValue) -> Result return Err(Error::WindowsError(error)), }; - let property_type = property_value.Type()?; - let value = match property_type { - PropertyType::UInt8 => PropertyValueData::UInt8(property_value.GetUInt8()?), - PropertyType::Int16 => PropertyValueData::Int16(property_value.GetInt16()?), - PropertyType::UInt16 => PropertyValueData::UInt16(property_value.GetUInt16()?), - PropertyType::Int32 => PropertyValueData::Int32(property_value.GetInt32()?), - PropertyType::UInt32 => PropertyValueData::UInt32(property_value.GetUInt32()?), - PropertyType::Int64 => PropertyValueData::Int64(property_value.GetInt64()?), - PropertyType::UInt64 => PropertyValueData::UInt64(property_value.GetUInt64()?), - PropertyType::Single => PropertyValueData::Single(property_value.GetSingle()?), - PropertyType::Double => PropertyValueData::Double(property_value.GetDouble()?), - PropertyType::Char16 => PropertyValueData::Char16(property_value.GetChar16()?), - PropertyType::Boolean => PropertyValueData::Boolean(property_value.GetBoolean()?), - PropertyType::String => PropertyValueData::String(property_value.GetString()?.to_string()), - PropertyType::Guid => PropertyValueData::Guid(property_value.GetGuid()?), + let pv = &property_value; + let data = match pv.Type()? { + PropertyType::UInt8 => PropertyValueData::UInt8(pv.GetUInt8()?), + PropertyType::Int16 => PropertyValueData::Int16(pv.GetInt16()?), + PropertyType::UInt16 => PropertyValueData::UInt16(pv.GetUInt16()?), + PropertyType::Int32 => PropertyValueData::Int32(pv.GetInt32()?), + PropertyType::UInt32 => PropertyValueData::UInt32(pv.GetUInt32()?), + PropertyType::Int64 => PropertyValueData::Int64(pv.GetInt64()?), + PropertyType::UInt64 => PropertyValueData::UInt64(pv.GetUInt64()?), + PropertyType::Single => PropertyValueData::Single(pv.GetSingle()?), + PropertyType::Double => PropertyValueData::Double(pv.GetDouble()?), + PropertyType::Char16 => PropertyValueData::Char16(pv.GetChar16()?), + PropertyType::Boolean => PropertyValueData::Boolean(pv.GetBoolean()?), + PropertyType::String => PropertyValueData::String(pv.GetString()?.to_string()), + PropertyType::Guid => PropertyValueData::Guid(pv.GetGuid()?), + PropertyType::DateTime => PropertyValueData::DateTime(pv.GetDateTime()?), + PropertyType::TimeSpan => PropertyValueData::TimeSpan(pv.GetTimeSpan()?), + PropertyType::Point => PropertyValueData::Point(pv.GetPoint()?), + PropertyType::Size => PropertyValueData::Size(pv.GetSize()?), + PropertyType::Rect => PropertyValueData::Rect(pv.GetRect()?), PropertyType::UInt8Array => { - PropertyValueData::UInt8Array(read_array!(property_value, GetUInt8Array, u8)) + PropertyValueData::UInt8Array(read_array!(pv, GetUInt8Array, u8)) } PropertyType::Int16Array => { - PropertyValueData::Int16Array(read_array!(property_value, GetInt16Array, i16)) + PropertyValueData::Int16Array(read_array!(pv, GetInt16Array, i16)) } PropertyType::UInt16Array => { - PropertyValueData::UInt16Array(read_array!(property_value, GetUInt16Array, u16)) + PropertyValueData::UInt16Array(read_array!(pv, GetUInt16Array, u16)) } PropertyType::Int32Array => { - PropertyValueData::Int32Array(read_array!(property_value, GetInt32Array, i32)) + PropertyValueData::Int32Array(read_array!(pv, GetInt32Array, i32)) } PropertyType::UInt32Array => { - PropertyValueData::UInt32Array(read_array!(property_value, GetUInt32Array, u32)) + PropertyValueData::UInt32Array(read_array!(pv, GetUInt32Array, u32)) } PropertyType::Int64Array => { - PropertyValueData::Int64Array(read_array!(property_value, GetInt64Array, i64)) + PropertyValueData::Int64Array(read_array!(pv, GetInt64Array, i64)) } PropertyType::UInt64Array => { - PropertyValueData::UInt64Array(read_array!(property_value, GetUInt64Array, u64)) + PropertyValueData::UInt64Array(read_array!(pv, GetUInt64Array, u64)) } PropertyType::SingleArray => { - PropertyValueData::SingleArray(read_array!(property_value, GetSingleArray, f32)) + PropertyValueData::SingleArray(read_array!(pv, GetSingleArray, f32)) } PropertyType::DoubleArray => { - PropertyValueData::DoubleArray(read_array!(property_value, GetDoubleArray, f64)) + PropertyValueData::DoubleArray(read_array!(pv, GetDoubleArray, f64)) } PropertyType::Char16Array => { - PropertyValueData::Char16Array(read_array!(property_value, GetChar16Array, u16)) + PropertyValueData::Char16Array(read_array!(pv, GetChar16Array, u16)) } PropertyType::BooleanArray => { - PropertyValueData::BooleanArray(read_array!(property_value, GetBooleanArray, bool)) + PropertyValueData::BooleanArray(read_array!(pv, GetBooleanArray, bool)) + } + PropertyType::StringArray => PropertyValueData::StringArray( + read_array!(pv, GetStringArray, HSTRING) + .into_iter() + .map(|value| value.to_string()) + .collect(), + ), + PropertyType::InspectableArray => PropertyValueData::InspectableArray( + read_array!(pv, GetInspectableArray, IInspectable) + .into_iter() + .map(|value| value.map(IUnknown::from)) + .collect(), + ), + PropertyType::DateTimeArray => { + PropertyValueData::DateTimeArray(read_array!(pv, GetDateTimeArray, DateTime)) } - PropertyType::StringArray => { - let values = read_array!(property_value, GetStringArray, HSTRING); - PropertyValueData::StringArray( - values.into_iter().map(|value| value.to_string()).collect(), - ) + PropertyType::TimeSpanArray => { + PropertyValueData::TimeSpanArray(read_array!(pv, GetTimeSpanArray, TimeSpan)) } PropertyType::GuidArray => { - PropertyValueData::GuidArray(read_array!(property_value, GetGuidArray, GUID)) + PropertyValueData::GuidArray(read_array!(pv, GetGuidArray, GUID)) } - _ => { - return Err(Error::WindowsError(windows_core::Error::new( - E_NOTIMPL, - format!("Unsupported WinRT IPropertyValue type: {}", property_type.0), - ))); + PropertyType::PointArray => { + PropertyValueData::PointArray(read_array!(pv, GetPointArray, Point)) } + PropertyType::SizeArray => { + PropertyValueData::SizeArray(read_array!(pv, GetSizeArray, Size)) + } + PropertyType::RectArray => { + PropertyValueData::RectArray(read_array!(pv, GetRectArray, Rect)) + } + unsupported => return Ok(PropertyValueUnboxResult::Unsupported(unsupported)), }; - Ok(PropertyValueUnboxResult::Value(value)) + Ok(PropertyValueUnboxResult::Value(data)) +} + +/// Boxes `data` as a system `Windows.Foundation.PropertyValue`. +/// +/// The result implements `IPropertyValue` (reporting [`PropertyValueData::property_type`]) +/// and the matching `IReference` / `IReferenceArray`, exactly like values +/// boxed by C#, C++/WinRT or `PropertyValue.Create*`. +pub fn box_property_value(data: &PropertyValueData) -> Result { + let boxed = match data { + PropertyValueData::UInt8(value) => PropertyValue::CreateUInt8(*value), + PropertyValueData::Int16(value) => PropertyValue::CreateInt16(*value), + PropertyValueData::UInt16(value) => PropertyValue::CreateUInt16(*value), + PropertyValueData::Int32(value) => PropertyValue::CreateInt32(*value), + PropertyValueData::UInt32(value) => PropertyValue::CreateUInt32(*value), + PropertyValueData::Int64(value) => PropertyValue::CreateInt64(*value), + PropertyValueData::UInt64(value) => PropertyValue::CreateUInt64(*value), + PropertyValueData::Single(value) => PropertyValue::CreateSingle(*value), + PropertyValueData::Double(value) => PropertyValue::CreateDouble(*value), + PropertyValueData::Char16(value) => PropertyValue::CreateChar16(*value), + PropertyValueData::Boolean(value) => PropertyValue::CreateBoolean(*value), + PropertyValueData::String(value) => PropertyValue::CreateString(&HSTRING::from(value)), + PropertyValueData::Guid(value) => PropertyValue::CreateGuid(*value), + PropertyValueData::DateTime(value) => PropertyValue::CreateDateTime(*value), + PropertyValueData::TimeSpan(value) => PropertyValue::CreateTimeSpan(*value), + PropertyValueData::Point(value) => PropertyValue::CreatePoint(*value), + PropertyValueData::Size(value) => PropertyValue::CreateSize(*value), + PropertyValueData::Rect(value) => PropertyValue::CreateRect(*value), + PropertyValueData::UInt8Array(values) => PropertyValue::CreateUInt8Array(values), + PropertyValueData::Int16Array(values) => PropertyValue::CreateInt16Array(values), + PropertyValueData::UInt16Array(values) => PropertyValue::CreateUInt16Array(values), + PropertyValueData::Int32Array(values) => PropertyValue::CreateInt32Array(values), + PropertyValueData::UInt32Array(values) => PropertyValue::CreateUInt32Array(values), + PropertyValueData::Int64Array(values) => PropertyValue::CreateInt64Array(values), + PropertyValueData::UInt64Array(values) => PropertyValue::CreateUInt64Array(values), + PropertyValueData::SingleArray(values) => PropertyValue::CreateSingleArray(values), + PropertyValueData::DoubleArray(values) => PropertyValue::CreateDoubleArray(values), + PropertyValueData::Char16Array(values) => PropertyValue::CreateChar16Array(values), + PropertyValueData::BooleanArray(values) => PropertyValue::CreateBooleanArray(values), + PropertyValueData::StringArray(values) => { + PropertyValue::CreateStringArray(&values.iter().map(HSTRING::from).collect::>()) + } + PropertyValueData::InspectableArray(values) => { + let values = values + .iter() + .map(|value| value.as_ref().map(Interface::cast).transpose()) + .collect::>>>()?; + PropertyValue::CreateInspectableArray(&values) + } + PropertyValueData::DateTimeArray(values) => PropertyValue::CreateDateTimeArray(values), + PropertyValueData::TimeSpanArray(values) => PropertyValue::CreateTimeSpanArray(values), + PropertyValueData::GuidArray(values) => PropertyValue::CreateGuidArray(values), + PropertyValueData::PointArray(values) => PropertyValue::CreatePointArray(values), + PropertyValueData::SizeArray(values) => PropertyValue::CreateSizeArray(values), + PropertyValueData::RectArray(values) => PropertyValue::CreateRectArray(values), + }?; + Ok(WinRTValue::Object(boxed.into())) } #[cfg(test)] mod tests { - use windows::Foundation::{DateTime, PropertyValue, Uri}; - use windows::Win32::System::WinRT::{RO_INIT_MULTITHREADED, RoInitialize}; - use windows_core::{HSTRING, Interface}; + use windows::Foundation::{IPropertyValue_Impl, Uri}; + use windows_core::{HSTRING, implement}; use super::*; + use crate::test_apartment::initialize_mta; + + const E_NOTIMPL: HRESULT = HRESULT(0x80004001_u32 as i32); fn as_value(value: &T) -> WinRTValue { - WinRTValue::Object(value.cast().expect("IPropertyValue as IUnknown")) + WinRTValue::Object(value.cast().expect("WinRT object as IUnknown")) } - #[test] - fn unboxes_scalar_and_array_property_values() -> windows_core::Result<()> { - let _ = unsafe { RoInitialize(RO_INIT_MULTITHREADED) }; - - let string = PropertyValue::CreateString(&HSTRING::from("BLE Device"))?; - assert_eq!( - unbox_property_value(&as_value(&string)).unwrap(), - PropertyValueUnboxResult::Value(PropertyValueData::String("BLE Device".to_string())) - ); - - let int64 = PropertyValue::CreateInt64(i64::MIN)?; - assert_eq!( - unbox_property_value(&as_value(&int64)).unwrap(), - PropertyValueUnboxResult::Value(PropertyValueData::Int64(i64::MIN)) - ); + fn samples() -> Vec { + let uri: IUnknown = Uri::CreateUri(&HSTRING::from("https://example.com")) + .unwrap() + .cast() + .unwrap(); + let boxed = box_property_value(&PropertyValueData::UInt32(7)) + .unwrap() + .as_object() + .unwrap(); + let guid = GUID::from_u128(0x0123_4567_89ab_cdef_0123_4567_89ab_cdef); + let date = DateTime { + UniversalTime: 133_000_000_000_000_001, + }; + let span = TimeSpan { + Duration: -12_345_678, + }; + let point = Point { X: 1.5, Y: -2.25 }; + let size = Size { + Width: 3.0, + Height: 4.5, + }; + let rect = Rect { + X: 1.0, + Y: 2.0, + Width: 3.0, + Height: 4.0, + }; + vec![ + PropertyValueData::UInt8(u8::MAX), + PropertyValueData::Int16(i16::MIN), + PropertyValueData::UInt16(u16::MAX), + PropertyValueData::Int32(i32::MIN), + PropertyValueData::UInt32(u32::MAX), + PropertyValueData::Int64(i64::MIN), + PropertyValueData::UInt64(u64::MAX), + PropertyValueData::Single(0.1), + PropertyValueData::Double(-0.1), + PropertyValueData::Char16(0xD800), + PropertyValueData::Boolean(true), + PropertyValueData::String("BLE Device".into()), + PropertyValueData::Guid(guid), + PropertyValueData::DateTime(date), + PropertyValueData::TimeSpan(span), + PropertyValueData::Point(point), + PropertyValueData::Size(size), + PropertyValueData::Rect(rect), + PropertyValueData::UInt8Array(vec![0, 1, 127, 255]), + PropertyValueData::Int16Array(vec![i16::MIN, 0, i16::MAX]), + PropertyValueData::UInt16Array(vec![0, u16::MAX]), + PropertyValueData::Int32Array(vec![-1, 0, 42]), + PropertyValueData::UInt32Array(vec![0, u32::MAX]), + PropertyValueData::Int64Array(vec![i64::MIN, i64::MAX]), + PropertyValueData::UInt64Array(vec![0, u64::MAX]), + PropertyValueData::SingleArray(vec![0.5, -1.25]), + PropertyValueData::DoubleArray(vec![0.1, f64::MAX]), + PropertyValueData::Char16Array(vec![0xD800, 0x61]), + PropertyValueData::BooleanArray(vec![true, false]), + PropertyValueData::StringArray(vec!["one".into(), String::new()]), + PropertyValueData::InspectableArray(vec![Some(uri), None, Some(boxed)]), + PropertyValueData::DateTimeArray(vec![date, DateTime::default()]), + PropertyValueData::TimeSpanArray(vec![span]), + PropertyValueData::GuidArray(vec![guid, GUID::zeroed()]), + PropertyValueData::PointArray(vec![point]), + PropertyValueData::SizeArray(vec![size, Size::default()]), + PropertyValueData::RectArray(vec![rect]), + PropertyValueData::Int32Array(Vec::new()), + PropertyValueData::StringArray(Vec::new()), + ] + } - let bytes = PropertyValue::CreateUInt8Array(&[0, 1, 127, 255])?; - assert_eq!( - unbox_property_value(&as_value(&bytes)).unwrap(), - PropertyValueUnboxResult::Value(PropertyValueData::UInt8Array(vec![0, 1, 127, 255])) - ); + #[test] + fn round_trips_every_supported_property_type() { + initialize_mta(); - let strings = - PropertyValue::CreateStringArray(&[HSTRING::from("one"), HSTRING::from("two")])?; - assert_eq!( - unbox_property_value(&as_value(&strings)).unwrap(), - PropertyValueUnboxResult::Value(PropertyValueData::StringArray(vec![ - "one".to_string(), - "two".to_string() - ])) - ); + let samples = samples(); + let mut covered = samples + .iter() + .map(|sample| sample.property_type().0) + .collect::>(); + covered.sort_unstable(); + covered.dedup(); + // Every PropertyType except Empty, Inspectable, OtherType and OtherTypeArray. + assert_eq!(covered.len(), 37); - Ok(()) + for sample in samples { + let boxed = box_property_value(&sample).unwrap(); + let property_value: IPropertyValue = boxed.as_object().unwrap().cast().unwrap(); + assert_eq!(property_value.Type().unwrap(), sample.property_type()); + assert_eq!( + unbox_property_value(&boxed).unwrap(), + PropertyValueUnboxResult::Value(sample.clone()), + "{sample:?}" + ); + let rebox = box_property_value(&match unbox_property_value(&boxed).unwrap() { + PropertyValueUnboxResult::Value(data) => data, + other => panic!("unexpected {other:?}"), + }) + .unwrap(); + let property_value: IPropertyValue = rebox.as_object().unwrap().cast().unwrap(); + assert_eq!(property_value.Type().unwrap(), sample.property_type()); + } } #[test] fn preserves_null_and_non_property_objects() -> windows_core::Result<()> { - let _ = unsafe { RoInitialize(RO_INIT_MULTITHREADED) }; + initialize_mta(); assert_eq!( unbox_property_value(&WinRTValue::Null).unwrap(), PropertyValueUnboxResult::Null ); + assert_eq!( + unbox_property_value(&WinRTValue::I32(5)).unwrap(), + PropertyValueUnboxResult::NotPropertyValue + ); let uri = Uri::CreateUri(&HSTRING::from("https://example.com"))?; let raw = uri.as_raw(); @@ -217,17 +447,76 @@ mod tests { Ok(()) } + #[implement(IPropertyValue)] + struct CustomPropertyValue(PropertyType); + + macro_rules! not_implemented { + ($($getter:ident -> $typ:ty),* $(,)?) => { + $(fn $getter(&self) -> windows_core::Result<$typ> { + Err(E_NOTIMPL.into()) + })* + }; + } + + macro_rules! not_implemented_arrays { + ($($getter:ident: $typ:ty),* $(,)?) => { + $(fn $getter(&self, _value: &mut Array<$typ>) -> windows_core::Result<()> { + Err(E_NOTIMPL.into()) + })* + }; + } + + impl IPropertyValue_Impl for CustomPropertyValue_Impl { + fn Type(&self) -> windows_core::Result { + Ok(self.0) + } + + not_implemented!( + IsNumericScalar -> bool, GetUInt8 -> u8, GetInt16 -> i16, GetUInt16 -> u16, + GetInt32 -> i32, GetUInt32 -> u32, GetInt64 -> i64, GetUInt64 -> u64, + GetSingle -> f32, GetDouble -> f64, GetChar16 -> u16, GetBoolean -> bool, + GetString -> HSTRING, GetGuid -> GUID, GetDateTime -> DateTime, + GetTimeSpan -> TimeSpan, GetPoint -> Point, GetSize -> Size, GetRect -> Rect, + ); + + not_implemented_arrays!( + GetUInt8Array: u8, GetInt16Array: i16, GetUInt16Array: u16, GetInt32Array: i32, + GetUInt32Array: u32, GetInt64Array: i64, GetUInt64Array: u64, GetSingleArray: f32, + GetDoubleArray: f64, GetChar16Array: u16, GetBooleanArray: bool, + GetStringArray: HSTRING, GetInspectableArray: IInspectable, GetGuidArray: GUID, + GetDateTimeArray: DateTime, GetTimeSpanArray: TimeSpan, GetPointArray: Point, + GetSizeArray: Size, GetRectArray: Rect, + ); + } + + fn custom(property_type: PropertyType) -> WinRTValue { + let value: IPropertyValue = CustomPropertyValue(property_type).into(); + as_value(&value) + } + #[test] - fn rejects_unsupported_property_types() -> windows_core::Result<()> { - let _ = unsafe { RoInitialize(RO_INIT_MULTITHREADED) }; + fn classifies_payloadless_property_types_without_failing() { + initialize_mta(); + + for property_type in [ + PropertyType::Empty, + PropertyType::Inspectable, + PropertyType::OtherType, + PropertyType::OtherTypeArray, + PropertyType(0x7fff), + ] { + assert_eq!( + unbox_property_value(&custom(property_type)).unwrap(), + PropertyValueUnboxResult::Unsupported(property_type) + ); + } + } - let date_time = PropertyValue::CreateDateTime(DateTime { UniversalTime: 0 })?; - let error = unbox_property_value(&as_value(&date_time)).unwrap_err(); - assert!(error.message().contains(&format!( - "Unsupported WinRT IPropertyValue type: {}", - PropertyType::DateTime.0 - ))); + #[test] + fn surfaces_getter_failures_for_supported_property_types() { + initialize_mta(); - Ok(()) + let error = unbox_property_value(&custom(PropertyType::UInt32)).unwrap_err(); + assert!(matches!(error, Error::WindowsError(ref error) if error.code() == E_NOTIMPL)); } } From e3c50ad0644aaa21dcbfea310516a2f0dd9f4b30 Mon Sep 17 00:00:00 2001 From: Leilei Zhang Date: Thu, 24 Sep 2026 14:57:52 +0800 Subject: [PATCH 2/3] Route every generated Python Object position through the conversion hooks EXPERIMENTAL prototype for design evaluation; not for merge. python/signature.rs owns the single codegen hook per direction, py_to_winrt_object / py_from_winrt_object. Every TypeMeta::Object arm uses them: parameters and setters, collection and array elements, returns, getters, out params, async results, event arguments, and the inputs and outputs of Python-implemented interfaces. Object no longer shares its return arm with unknown runtime classes and interfaces. type_helpers::py_object_annotation is the single annotation hook; Object positions read WinRTObjectValue | None and accept WinRTObjectInput | None through the shared py_optional_type. The aliases are collision-safe support symbols. Generated enums carry an IReference boxing marker and generated Windows.Foundation Point/Size/Rect carry a PropertyType marker. Composable factory outers keep their raw object-reference projection. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/copilot-instructions.md | 1 + samples/python/device-watcher/app.py | 6 +- samples/python/text-to-speech/app.py | 4 +- tests/e2e/runners/py_runner.py | 41 ++++-- .../typecheck/python_implementation_api.py | 17 +-- .../src/codegen/winrt/python/generator/mod.rs | 10 +- .../codegen/winrt/python/generator/structs.rs | 78 ++++++++++++ .../codegen/winrt/python/generator/types.rs | 34 +++++ .../codegen/winrt/python/implementation.rs | 21 +++- .../src/codegen/winrt/python/method.rs | 28 +++-- .../src/codegen/winrt/python/naming.rs | 14 ++- .../src/codegen/winrt/python/signature.rs | 117 ++++++++++++++++-- .../src/codegen/winrt/python/stub_helpers.rs | 11 +- .../src/codegen/winrt/python/stubs.rs | 5 +- .../src/codegen/winrt/python/type_helpers.rs | 114 +++++++++++++---- .../tests/observable_vector_test.rs | 9 +- .../tests/python_consumer_typing_test.rs | 105 ++++++++++------ .../python_property_name_collision_test.rs | 4 +- .../tests/python_symbol_mapping_test.rs | 15 ++- .../snapshots/data_writer_py/data_writer.py | 3 +- ...i_iterator_i_www_form_url_decoder_entry.py | 3 +- .../tests/snapshots/uri_py/i_stringable.py | 3 +- ...ntime_class_with_absolute_canonical_uri.py | 3 +- .../uri_py/i_www_form_url_decoder_entry.py | 3 +- .../tests/snapshots/uri_py/uri.py | 3 +- .../snapshots/uri_py/www_form_url_decoder.py | 3 +- ..._iterator_i_www_form_url_decoder_entry.pyi | 2 +- .../tests/snapshots/uri_pyi/i_stringable.pyi | 2 +- ...time_class_with_absolute_canonical_uri.pyi | 2 +- .../uri_pyi/i_www_form_url_decoder_entry.pyi | 2 +- .../tests/snapshots/uri_pyi/uri.pyi | 2 +- .../uri_pyi/www_form_url_decoder.pyi | 2 +- .../tests/winrt_implementation_test.rs | 3 + 33 files changed, 541 insertions(+), 129 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 7ad51e44..2e6b9869 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -125,6 +125,7 @@ output, runtime behavior, and the `@microsoft/dynwinrt` root API. - GUID: `WinGUID.parse('...')` - Method call: `method_handle.invoke(obj, [args])` → returns single `DynWinRTValue` - `IReference` values project as native values plus `None`; generated `IReference_*` wrappers remain accepted for input compatibility +- `Object` (IInspectable) positions convert only through `to_winrt_object` / `from_winrt_object` (`bindings/py/src/object_value.rs`, experimental): boxed `IPropertyValue` payloads become Python values, tagged (`dynwinrt.UInt32`, …) where the plain write rule would change their type; other objects stay the same `DynWinRTValue`. Codegen emits them only via `py_to_winrt_object` / `py_from_winrt_object` in `python/signature.rs` ### Common Issues - `test_initialize` is `#[ignore]` — requires `WINAPPSDK_BOOTSTRAP_DLL_PATH` env var diff --git a/samples/python/device-watcher/app.py b/samples/python/device-watcher/app.py index 83f5578e..f47cebe0 100644 --- a/samples/python/device-watcher/app.py +++ b/samples/python/device-watcher/app.py @@ -4,7 +4,7 @@ from collections import Counter from collections.abc import Callable -from dynwinrt import DynWinRTValue, RoApartment, projected_lifetime_scope +from dynwinrt import RoApartment, WinRTObjectValue, projected_lifetime_scope from generated.windows.devices.enumeration import ( DeviceInformation, DeviceInformationUpdate, @@ -72,13 +72,13 @@ def on_removed( def on_enumeration_completed( _sender: DeviceWatcher | None, - _args: DynWinRTValue | None, + _args: WinRTObjectValue | None, ) -> None: loop.call_soon_threadsafe(enumeration_completed.set) def on_stopped( _sender: DeviceWatcher | None, - _args: DynWinRTValue | None, + _args: WinRTObjectValue | None, ) -> None: loop.call_soon_threadsafe(stopped.set) diff --git a/samples/python/text-to-speech/app.py b/samples/python/text-to-speech/app.py index c3267524..30951217 100644 --- a/samples/python/text-to-speech/app.py +++ b/samples/python/text-to-speech/app.py @@ -1,7 +1,7 @@ import argparse import asyncio -from dynwinrt import DynWinRTValue, RoApartment, projected_lifetime_scope +from dynwinrt import RoApartment, WinRTObjectValue, projected_lifetime_scope from generated.windows.media.playback import ( MediaPlayer, MediaPlayerAudioCategory, @@ -32,7 +32,7 @@ async def speak(text: str, smoke: bool) -> None: def on_media_ended( _sender: MediaPlayer | None, - _args: DynWinRTValue | None, + _args: WinRTObjectValue | None, ) -> None: loop.call_soon_threadsafe(ended.set) diff --git a/tests/e2e/runners/py_runner.py b/tests/e2e/runners/py_runner.py index 9bde333f..7695db43 100644 --- a/tests/e2e/runners/py_runner.py +++ b/tests/e2e/runners/py_runner.py @@ -368,12 +368,24 @@ async def run_check( elif kind == 'nullable_object_array_roundtrip': uri_cls = generated_type(pkg_name, 'Uri') uri = uri_cls.create_uri('https://example.com/null-array') - boxed = getattr(cls, member)( + # Raw native inputs still pass through; the Object result unboxes to + # an InspectableArray that keeps the null element and COM identity. + unboxed = getattr(cls, member)( [dw.DynWinRTValue.null_value(), uri._obj] ) - if boxed is None: - cr['error'] = 'CreateInspectableArray returned None' + if not isinstance(unboxed, dw.InspectableArray): + cr['error'] = ( + 'CreateInspectableArray did not unbox to InspectableArray: ' + f'{unboxed!r}' + ) return cr + if len(unboxed) != 2 or unboxed[0] is not None: + cr['error'] = f'null inspectable array element was not preserved: {unboxed!r}' + return cr + if unboxed[1].identity_raw() != uri._obj.identity_raw(): + cr['error'] = 'inspectable array element lost COM identity' + return cr + boxed = dw.to_winrt_object(unboxed) values = boxed.call_0( 38, dw.DynWinRTType.array_type(dw.DynWinRTType.object()), @@ -488,10 +500,11 @@ async def run_check( property_value_cls = generated_type(pkg_name, 'PropertyValue') factory = getattr(property_value_cls, to_snake_case(check['factory'])) - boxed = factory(check['compatibility_value']) + # Object results unbox to tagged values that re-box as the same type. + boxed = dw.to_winrt_object(factory(check['compatibility_value'])) reference_cls = generated_type(pkg_name, check['reference_class']) - reference = reference_cls.from_value(getattr(boxed, '_obj', boxed)) + reference = reference_cls.from_value(boxed) setattr(obj, member, reference) actual = getattr(obj, member) @@ -747,10 +760,11 @@ async def run_check( elif kind == 'value_set_mapping': property_value_cls = generated_type(pkg_name, 'PropertyValue') + # Object results unbox, so the factories return Python values; a raw + # native box and a plain value are both valid Object inputs. first = property_value_cls.create_string('first') second = property_value_cls.create_int32(2) - first_raw = getattr(first, '_obj', first) - second_raw = getattr(second, '_obj', second) + first_raw = dw.to_winrt_object(first) if len(obj) != 0 or obj.size != 0: cr['error'] = 'new ValueSet was not empty' @@ -764,14 +778,19 @@ async def run_check( return cr first_lookup = obj.lookup('first') second_lookup = obj['second'] - if first_lookup.is_null() or second_lookup.is_null(): + if first_lookup is None or second_lookup is None: cr['error'] = 'ValueSet lookup returned null' return cr if ( - first_lookup.identity_raw() != first_raw.identity_raw() - or second_lookup.identity_raw() != second_raw.identity_raw() + first_lookup != 'first' + or type(first_lookup) is not str + or second_lookup != 2 + or type(second_lookup) is not int ): - cr['error'] = 'ValueSet payloads did not round-trip' + cr['error'] = ( + 'ValueSet payloads did not round-trip: ' + f'{first_lookup!r}, {second_lookup!r}' + ) return cr view = obj.get_view() diff --git a/tests/e2e/typecheck/python_implementation_api.py b/tests/e2e/typecheck/python_implementation_api.py index 34182a89..1bf6e51a 100644 --- a/tests/e2e/typecheck/python_implementation_api.py +++ b/tests/e2e/typecheck/python_implementation_api.py @@ -7,7 +7,10 @@ from typing import Never, TypedDict from uuid import UUID -from dynwinrt import DynWinRTImplementationHandle, DynWinRTValue, release_projected +from dynwinrt import ( + DynWinRTImplementationHandle, DynWinRTValue, WinRTObjectInput, WinRTObjectValue, + release_projected, +) from python_bindings.windows.application_model.background import ( IBackgroundTask, IBackgroundTaskInstance, ) @@ -169,7 +172,7 @@ def check_event(sender: IMemoryBufferReference) -> None: ) view: IMemoryBufferReference = IMemoryBufferReference.from_implementation(owner) - def callback(source: IMemoryBufferReference | None, args: DynWinRTValue | None) -> None: + def callback(source: IMemoryBufferReference | None, args: WinRTObjectValue | None) -> None: if source is not None: capacity: int = source.capacity assert capacity >= 0 @@ -247,8 +250,8 @@ def get_uint8_array(self) -> bytes: def get_string_array(self) -> list[str]: return ["owned string"] - def get_inspectable_array(self) -> list[DynWinRTValue | None]: - return [None] + def get_inspectable_array(self) -> list[WinRTObjectInput | None]: + return [None, 42, "boxed"] def get_point_array(self) -> list[Point]: return [Point(x=1.25, y=-3.5)] @@ -278,13 +281,13 @@ class IndexOfResult(TypedDict): class VectorHandlers: - def get_at(self, index: int) -> DynWinRTValue | None: + def get_at(self, index: int) -> WinRTObjectInput | None: return None def get_size(self) -> int: return 1 - def index_of(self, value: DynWinRTValue | None) -> IndexOfResult: + def index_of(self, value: WinRTObjectValue | None) -> IndexOfResult: return {"result": value is None, "index": 0} def first(self) -> IBindableIterator | None: @@ -295,7 +298,7 @@ class TupleResultHandlers: get_at = unused get_size = unused - def index_of(self, value: DynWinRTValue | None) -> tuple[int, bool]: + def index_of(self, value: WinRTObjectValue | None) -> tuple[int, bool]: return (0, True) diff --git a/tools/dynwinrt-codegen/src/codegen/winrt/python/generator/mod.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/generator/mod.rs index 76351781..97e19c2b 100644 --- a/tools/dynwinrt-codegen/src/codegen/winrt/python/generator/mod.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/python/generator/mod.rs @@ -41,6 +41,8 @@ const HEADER: &str = "# Generated by dynwinrt-codegen — do not edit\n"; const FUTURE_ANNOTATIONS: &str = "from __future__ import annotations\n"; fn import_line(context: &PythonProjectionContext) -> String { let object_input = context.support_symbol_import(PythonSupportSymbol::ObjectInput); + let object_value_alias = context.support_symbol_import(PythonSupportSymbol::WinRTObjectValue); + let object_input_alias = context.support_symbol_import(PythonSupportSymbol::WinRTObjectInput); format!( "\ from ._runtime import ( @@ -48,7 +50,7 @@ from ._runtime import ( TYPE_CHECKING, UUID, WinGUID, datetime, timedelta, DynWinRTType, DynWinRTMethodSig, DynWinRTValue, DynWinRTArray, DynWinRTStruct, DynWinRtDelegate, DynWinRTOverrideInterface, - {object_input}, _property, _weakref_ref, + {object_input}, {object_value_alias}, {object_input_alias}, _property, _weakref_ref, _dynwinrt_array, _dynwinrt_bind_overload, _dynwinrt_can_cast, _dynwinrt_create_delegate, _dynwinrt_datetime_to_ticks, _dynwinrt_delegate, _dynwinrt_enum, _dynwinrt_guid, _dynwinrt_map, _dynwinrt_new_vector, _dynwinrt_ticks_to_datetime, @@ -56,6 +58,7 @@ from ._runtime import ( _dynwinrt_cache_projected, _dynwinrt_projected_from_native, _dynwinrt_symbol, _dynwinrt_track_projected, _dynwinrt_uuid, _dynwinrt_vector, _dynwinrt_wrap_values, + _dynwinrt_from_winrt_object, _dynwinrt_to_winrt_object, ) " ) @@ -75,6 +78,11 @@ from weakref import ref as _weakref_ref from dynwinrt import ( DynWinRTType, DynWinRTMethodSig, DynWinRTValue, DynWinRTArray, DynWinRTStruct, DynWinRtDelegate, DynWinRTOverrideInterface, WinGUID, + WinRTObjectInput, WinRTObjectValue, +) +from dynwinrt import ( + from_winrt_object as _dynwinrt_from_winrt_object, + to_winrt_object as _dynwinrt_to_winrt_object, ) from dynwinrt.dynwinrt import ( _dynwinrt_array, _dynwinrt_bind_overload, _dynwinrt_datetime_to_ticks, _dynwinrt_guid, diff --git a/tools/dynwinrt-codegen/src/codegen/winrt/python/generator/structs.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/generator/structs.rs index 8fa74f45..33f92081 100644 --- a/tools/dynwinrt-codegen/src/codegen/winrt/python/generator/structs.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/python/generator/structs.rs @@ -126,6 +126,13 @@ pub(super) fn generate_struct_helpers(context: &PythonProjectionContext, s: &Typ " __slots__ = {}\n", py_string_tuple_literal(&slot_names) )); + if let Some(property_type) = boxable_geometry(namespace, metadata_name, fields) { + // Lets the Object conversion box this struct as PropertyType.Point/Size/Rect + // and makes it compare equal to the dynwinrt value of the same type. + out.push_str(&format!( + " _dynwinrt_property_type = '{property_type}'\n" + )); + } out.push('\n'); if fields.is_empty() { out.push_str(" def __init__(self):\n"); @@ -252,6 +259,27 @@ fn py_struct_slot_name(field: &FieldMeta) -> String { } } +/// The `PropertyType` a generated `Windows.Foundation` geometry struct boxes as +/// in `Object` positions (its field names match dynwinrt.Point/Size/Rect). +fn boxable_geometry(namespace: &str, name: &str, fields: &[FieldMeta]) -> Option<&'static str> { + let expected: &[&str] = match (namespace, name) { + ("Windows.Foundation", "Point") => &["X", "Y"], + ("Windows.Foundation", "Size") => &["Width", "Height"], + ("Windows.Foundation", "Rect") => &["X", "Y", "Width", "Height"], + _ => return None, + }; + let matches = fields.len() == expected.len() + && fields + .iter() + .zip(expected) + .all(|(field, expected)| field.name == *expected && field.typ == TypeMeta::F32); + matches.then_some(match name { + "Point" => "Point", + "Size" => "Size", + _ => "Rect", + }) +} + fn py_struct_field_names(fields: &[FieldMeta]) -> Vec { fields .iter() @@ -393,3 +421,53 @@ fn py_struct_constructor_field_type(context: &PythonProjectionContext, typ: &Typ _ => py_struct_field_type(context, typ), } } + +#[cfg(test)] +mod tests { + use super::*; + + fn geometry(name: &str, fields: &[&str]) -> TypeMeta { + TypeMeta::Struct { + namespace: "Windows.Foundation".into(), + name: name.into(), + fields: fields + .iter() + .map(|field| FieldMeta { + name: (*field).into(), + typ: TypeMeta::F32, + }) + .collect(), + } + } + + #[test] + fn foundation_geometry_structs_carry_the_object_boxing_marker() { + for (typ, marker) in [ + (geometry("Point", &["X", "Y"]), "Point"), + (geometry("Size", &["Width", "Height"]), "Size"), + (geometry("Rect", &["X", "Y", "Width", "Height"]), "Rect"), + ] { + let code = generate_struct_helpers(&PythonProjectionContext::default(), &typ); + assert!( + code.contains(&format!(" _dynwinrt_property_type = '{marker}'\n")), + "{code}" + ); + } + let vector = TypeMeta::Struct { + namespace: "Windows.Foundation.Numerics".into(), + name: "Vector2".into(), + fields: vec![ + FieldMeta { + name: "X".into(), + typ: TypeMeta::F32, + }, + FieldMeta { + name: "Y".into(), + typ: TypeMeta::F32, + }, + ], + }; + let code = generate_struct_helpers(&PythonProjectionContext::default(), &vector); + assert!(!code.contains("_dynwinrt_property_type"), "{code}"); + } +} diff --git a/tools/dynwinrt-codegen/src/codegen/winrt/python/generator/types.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/generator/types.rs index 4eb5dd40..7bea7d25 100644 --- a/tools/dynwinrt-codegen/src/codegen/winrt/python/generator/types.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/python/generator/types.rs @@ -70,6 +70,22 @@ pub fn generate_enum(context: &PythonProjectionContext, en: &TypeMeta) -> Option )); } } + // Lets the Object conversion box members as IReference. Assigned + // after the class body: a body attribute would become an enum member. + if let TypeMeta::Enum { + namespace, + name: winrt_name, + .. + } = en + { + let backing = match en.underlying_type() { + TypeMeta::U32 => "UInt32", + _ => "Int32", + }; + out.push_str(&format!( + "\n\n{name}._dynwinrt_enum_type = ('{namespace}.{winrt_name}', '{backing}')\n" + )); + } Some(out) } @@ -702,6 +718,24 @@ mod tests { let code = generate_enum(&PythonProjectionContext::default(), &value).unwrap(); assert!(code.contains("from enum import IntFlag")); assert!(code.contains("class Options(IntFlag):")); + assert!(code.ends_with("\n\n\nOptions._dynwinrt_enum_type = ('Test.Options', 'UInt32')\n")); + } + + #[test] + fn enums_carry_the_marker_for_object_boxing() { + let value = TypeMeta::Enum { + namespace: "Test".into(), + name: "Mode".into(), + underlying: Box::new(TypeMeta::I32), + members: Vec::new(), + is_flags: false, + doc: None, + deprecated: None, + }; + + let code = generate_enum(&PythonProjectionContext::default(), &value).unwrap(); + assert!(code.contains("class Mode(IntEnum):\n pass\n")); + assert!(code.contains("\nMode._dynwinrt_enum_type = ('Test.Mode', 'Int32')\n")); } #[test] diff --git a/tools/dynwinrt-codegen/src/codegen/winrt/python/implementation.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/implementation.rs index 72dbf32d..e3f8c5f5 100644 --- a/tools/dynwinrt-codegen/src/codegen/winrt/python/implementation.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/python/implementation.rs @@ -15,7 +15,10 @@ use crate::types::TypeMeta; use super::naming::{PythonProjectionContext, to_snake_case}; use super::native_types::{FoundationType, foundation_type}; -use super::signature::{py_convert_return, py_dynwinrt_type, py_runtime_type_symbol}; +use super::signature::{ + py_convert_return, py_dynwinrt_type, py_runtime_type_symbol, py_to_winrt_object, +}; +use super::type_helpers::{ObjectRole, py_object_annotation, py_optional_type}; pub(super) const IMPORTS: &str = "\ from typing import Protocol, TypedDict @@ -333,9 +336,17 @@ impl Projector<'_> { { self.context.reference_name_for_type(&typ.metadata) } + TypeMeta::Object => py_object_annotation( + self.context, + if writing { + ObjectRole::Input + } else { + ObjectRole::Output + }, + ), _ => "DynWinRTValue".into(), }; - format!("{name} | None") + py_optional_type(name) } ImplementationAbi::Scalar | ImplementationAbi::Guid | ImplementationAbi::HString => { match typ.metadata { @@ -466,6 +477,12 @@ impl Projector<'_> { ); } match &typ.abi { + ImplementationAbi::Reference if typ.metadata == TypeMeta::Object => format!( + "_implementation_reference({}, {}, {})", + py_to_winrt_object(value), + self.iid(typ), + quote(label) + ), ImplementationAbi::Reference => format!( "_implementation_reference({value}, {}, {})", self.iid(typ), diff --git a/tools/dynwinrt-codegen/src/codegen/winrt/python/method.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/method.rs index 3cb9aea5..9df965b8 100644 --- a/tools/dynwinrt-codegen/src/codegen/winrt/python/method.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/python/method.rs @@ -11,12 +11,14 @@ use crate::codegen::winrt::shared::imports::{ use super::naming::{PythonProjectionContext, PythonTypeIdentity, to_snake_case}; use super::signature::{ - py_convert_return, py_runtime_named_symbol, py_runtime_symbol, py_type_guard, py_wrap_arg, - py_wrap_async, py_wrap_async_with_converters, + py_convert_return, py_is_composable_outer, py_runtime_named_symbol, py_runtime_symbol, + py_type_guard, py_wrap_arg, py_wrap_async, py_wrap_async_with_converters, + py_wrap_composable_outer, }; use super::type_helpers::{ - method_pydoc, py_delegate_callable_type, py_factory_return_type, py_method_abi_output_count, - py_method_outputs, py_method_return_type, py_output_type, py_param_list, + method_pydoc, py_delegate_callable_type, py_factory_param_list, py_factory_return_type, + py_method_abi_output_count, py_method_outputs, py_method_return_type, py_output_type, + py_param_list, }; fn is_delegate_type(typ: &TypeMeta, context: &PythonProjectionContext) -> bool { @@ -280,7 +282,7 @@ fn generate_factory_method_invoke_named( name_override: Option<&str>, ) -> String { let in_params = get_in_params(method); - let py_params = py_param_list(&in_params, context); + let py_params = py_factory_param_list(method, &in_params, context); let return_py_type = py_factory_return_type(&context.class_name(class), method, context); @@ -303,7 +305,18 @@ fn generate_factory_method_invoke_named( } out.push_str(&method_pydoc(method, &in_params)); - let args_expr = py_build_method_args_expr(&in_params, context); + let args_expr = in_params + .iter() + .map(|param| { + let name = to_snake_case(¶m.name); + if py_is_composable_outer(method, param) { + py_wrap_composable_outer(&name) + } else { + py_wrap_method_arg(&name, ¶m.typ, context) + } + }) + .collect::>() + .join(", "); let iface_symbol = context.reference_name(&iface.type_identity()); let call_expr = method_call_expr( &context.registration_symbol(iface), @@ -1078,8 +1091,9 @@ mod tests { ); assert!(code.contains( - "def write_async(self, buffer: 'DynWinRTValue | _DynWinRTObject') -> WinRTCoroutineWithProgress[int, int]:" + "def write_async(self, buffer: 'WinRTObjectInput | None') -> WinRTCoroutineWithProgress[int, int]:" )); + assert!(code.contains("_dynwinrt_to_winrt_object(buffer)")); assert!(code.contains("return _dynwinrt_track_projected(_DynWinRTAsyncWithProgress(")); assert!(code.contains("'WinRTAsyncWithProgress')")); assert!(code.contains("lambda value: value.to_u32()")); diff --git a/tools/dynwinrt-codegen/src/codegen/winrt/python/naming.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/naming.rs index 3dedf02b..1234f632 100644 --- a/tools/dynwinrt-codegen/src/codegen/winrt/python/naming.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/python/naming.rs @@ -17,12 +17,24 @@ pub type PythonTypeIdentity = TypeIdentity; #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub(crate) enum PythonSupportSymbol { ObjectInput, + /// `dynwinrt.WinRTObjectValue`: values read from WinRT `Object` positions. + WinRTObjectValue, + /// `dynwinrt.WinRTObjectInput`: values accepted by WinRT `Object` positions. + WinRTObjectInput, } impl PythonSupportSymbol { + pub(crate) const ALL: [Self; 3] = [ + Self::ObjectInput, + Self::WinRTObjectValue, + Self::WinRTObjectInput, + ]; + fn name(self) -> &'static str { match self { Self::ObjectInput => "_DynWinRTObject", + Self::WinRTObjectValue => "WinRTObjectValue", + Self::WinRTObjectInput => "WinRTObjectInput", } } } @@ -912,7 +924,7 @@ impl PythonProjectionContext { } } // Support imports yield to metadata declarations and their allocated roles. - for helper in [PythonSupportSymbol::ObjectInput] { + for helper in PythonSupportSymbol::ALL { let preferred = helper.name(); let mut name = preferred.to_string(); let mut index = 2; diff --git a/tools/dynwinrt-codegen/src/codegen/winrt/python/signature.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/signature.rs index 9bbb5ba6..fff7202f 100644 --- a/tools/dynwinrt-codegen/src/codegen/winrt/python/signature.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/python/signature.rs @@ -3,7 +3,7 @@ //! Python method signatures, argument wrapping, and return conversion. -use crate::meta::{InterfaceMeta, MethodMeta, ParamDirection}; +use crate::meta::{InterfaceMeta, MethodMeta, ParamDirection, ParamMeta}; use crate::types::{TypeIdentity, TypeIdentityKind, TypeMeta}; use super::naming::{PythonProjectionContext, PythonSymbol}; @@ -23,6 +23,53 @@ pub(crate) fn py_runtime_symbol( ) } +/// The single codegen hook for a Python value entering a WinRT `Object` +/// position: parameters, setters, collection and array elements, and outputs +/// of Python-implemented handlers. See `bindings/py/src/object_value.rs`. +pub(crate) fn py_to_winrt_object(expr: &str) -> String { + format!("_dynwinrt_to_winrt_object({expr})") +} + +/// The single codegen hook for a WinRT `Object` value leaving to Python: +/// returns, getters, out parameters, collection and array elements, async +/// results, and event and handler arguments. +pub(crate) fn py_from_winrt_object(expr: &str) -> String { + format!("_dynwinrt_from_winrt_object({expr})") +} + +/// Whether `param` is a composable factory's controlling outer: the last +/// input, named outer/base/baseInterface/outerInterface and typed Object, +/// next to an `inner` Object output. +/// +/// It carries COM aggregation identity rather than a value, so it keeps the +/// raw object-reference projection instead of the Object value conversion: +/// boxing a Python value there would hand the factory a controlling outer +/// that nothing keeps alive. +pub(crate) fn py_is_composable_outer(method: &MethodMeta, param: &ParamMeta) -> bool { + let is_last_input = method + .params + .iter() + .rfind(|candidate| candidate.direction == ParamDirection::In) + .is_some_and(|last| std::ptr::eq(last, param)); + let name = param.name.to_ascii_lowercase(); + is_last_input + && param.typ == TypeMeta::Object + && matches!( + name.as_str(), + "outer" | "base" | "baseinterface" | "outerinterface" + ) + && method.params.iter().any(|candidate| { + candidate.direction == ParamDirection::Out + && candidate.typ == TypeMeta::Object + && candidate.name.to_ascii_lowercase().contains("inner") + }) +} + +/// Argument expression for a composable factory's controlling outer. +pub(crate) fn py_wrap_composable_outer(name: &str) -> String { + format!("getattr({name}, '_obj', {name})") +} + pub(crate) fn py_runtime_type_symbol( context: &PythonProjectionContext, typ: &TypeMeta, @@ -391,7 +438,8 @@ pub(crate) fn py_wrap_arg(name: &str, typ: &TypeMeta, context: &PythonProjection TypeMeta::F64 => format!("DynWinRTValue.from_f64({})", name), TypeMeta::Guid => format!("DynWinRTValue.from_guid(_dynwinrt_guid({}))", name), TypeMeta::RuntimeClass { .. } => py_runtime_class_wrap(name, typ), - TypeMeta::Object | TypeMeta::Interface { .. } | TypeMeta::Delegate { .. } => { + TypeMeta::Object => py_to_winrt_object(name), + TypeMeta::Interface { .. } | TypeMeta::Delegate { .. } => { format!("getattr({}, '_obj', {})", name, name) } TypeMeta::Parameterized { .. } => format!("getattr({}, '_obj', {})", name, name), @@ -458,10 +506,10 @@ pub(crate) fn py_wrap_native_value( name ), TypeMeta::RuntimeClass { .. } => py_runtime_class_wrap(name, typ), - TypeMeta::Object - | TypeMeta::Interface { .. } - | TypeMeta::Parameterized { .. } - | TypeMeta::Delegate { .. } => format!("getattr({}, '_obj', {})", name, name), + TypeMeta::Object => py_to_winrt_object(name), + TypeMeta::Interface { .. } | TypeMeta::Parameterized { .. } | TypeMeta::Delegate { .. } => { + format!("getattr({}, '_obj', {})", name, name) + } TypeMeta::Array(inner) => format!( "_dynwinrt_array({}, lambda item: {}, {}, {})", name, @@ -709,7 +757,8 @@ pub(crate) fn py_convert_return( wrapper, expr ) } - Some(TypeMeta::Object | TypeMeta::RuntimeClass { .. } | TypeMeta::Interface { .. }) => { + Some(TypeMeta::Object) => py_from_winrt_object(expr), + Some(TypeMeta::RuntimeClass { .. } | TypeMeta::Interface { .. }) => { format!( "(lambda value: None if value.is_null() else value)({})", expr @@ -864,7 +913,12 @@ pub(crate) fn py_convert_array_return( ) } } - TypeMeta::Object | TypeMeta::Delegate { .. } => format!( + TypeMeta::Object => format!( + "[{} for v in {}.to_values()]", + py_from_winrt_object("v"), + arr_expr + ), + TypeMeta::Delegate { .. } => format!( "[None if v.is_null() else v for v in {}.to_values()]", arr_expr ), @@ -928,6 +982,53 @@ mod tests { } } + #[test] + fn object_positions_use_the_single_conversion_hooks() { + let context = PythonProjectionContext::default(); + assert_eq!( + py_wrap_arg("value", &TypeMeta::Object, &context), + "_dynwinrt_to_winrt_object(value)" + ); + assert_eq!( + py_wrap_native_value("item", &TypeMeta::Object, &context), + "_dynwinrt_to_winrt_object(item)" + ); + assert_eq!( + py_convert_return("raw", Some(&TypeMeta::Object), false, &context), + "_dynwinrt_from_winrt_object(raw)" + ); + let objects = TypeMeta::Array(Box::new(TypeMeta::Object)); + assert_eq!( + py_convert_return("raw", Some(&objects), false, &context), + "[_dynwinrt_from_winrt_object(v) for v in raw.as_array().to_values()]" + ); + assert!( + py_wrap_arg("items", &objects, &context) + .contains("lambda item: _dynwinrt_to_winrt_object(item)") + ); + let async_object = TypeMeta::AsyncOperation(Box::new(TypeMeta::Object)); + assert!( + py_convert_return("raw", Some(&async_object), true, &context) + .contains("lambda value: _dynwinrt_from_winrt_object(value)") + ); + let map = TypeMeta::Parameterized { + namespace: "Windows.Foundation.Collections".into(), + name: "IMap`2".into(), + piid: "3c2925fe-8519-45c1-aa79-197b6718c1c1".into(), + args: vec![TypeMeta::String, TypeMeta::Object], + }; + assert!( + py_wrap_arg("items", &map, &context) + .contains("lambda item: _dynwinrt_to_winrt_object(item)") + ); + // Unknown classes and interfaces are references, not values: they keep + // the null-check projection and never unbox. + assert_eq!( + py_convert_return("raw", Some(&geometry_type()), false, &context), + "(lambda value: None if value.is_null() else value)(raw)" + ); + } + #[test] fn runtime_class_inputs_cast_to_the_expected_interface() { let geometry = geometry_type(); diff --git a/tools/dynwinrt-codegen/src/codegen/winrt/python/stub_helpers.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/stub_helpers.rs index 316b4319..ae6cbf6a 100644 --- a/tools/dynwinrt-codegen/src/codegen/winrt/python/stub_helpers.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/python/stub_helpers.rs @@ -11,8 +11,9 @@ use super::naming::{PythonProjectionContext, PythonSymbol, STRUCT_SYMBOLS, to_sn use super::native_types::{FoundationType, foundation_type}; use super::structs::{py_struct_field_read_type, py_struct_field_type}; use super::type_helpers::{ - method_pydoc_with_indent, py_delegate_callable_type, py_factory_return_type, - py_method_return_type, py_output_type, py_param_list, py_param_type_safe, + method_pydoc_with_indent, py_delegate_callable_type, py_factory_param_list, + py_factory_return_type, py_method_return_type, py_output_type, py_param_list, + py_param_type_safe, }; use crate::codegen::winrt::shared::imports::ireference_inner_type; @@ -369,7 +370,11 @@ pub(super) fn emit_static_method_stub_named( name_override: Option<&str>, ) -> String { let in_params = get_in_params(method); - let py_params = py_param_list(&in_params, context); + let py_params = if is_factory { + py_factory_param_list(method, &in_params, context) + } else { + py_param_list(&in_params, context) + }; let py_return = if is_factory { py_factory_return_type(class_name, method, context) diff --git a/tools/dynwinrt-codegen/src/codegen/winrt/python/stubs.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/stubs.rs index 0568bbf9..c3eac511 100644 --- a/tools/dynwinrt-codegen/src/codegen/winrt/python/stubs.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/python/stubs.rs @@ -42,6 +42,8 @@ const HEADER: &str = "# Generated by dynwinrt-codegen — do not edit\n"; const FUTURE_ANNOTATIONS: &str = "from __future__ import annotations\n"; fn import_line(context: &PythonProjectionContext) -> String { let object_input = context.support_symbol_import(PythonSupportSymbol::ObjectInput); + let object_value_alias = context.support_symbol_import(PythonSupportSymbol::WinRTObjectValue); + let object_input_alias = context.support_symbol_import(PythonSupportSymbol::WinRTObjectInput); format!( "\ import builtins @@ -49,7 +51,7 @@ from ._typing import ( Callable, Iterable, Iterator, Mapping, MutableMapping, MutableSequence, Sequence, UUID, WinGUID, datetime, overload, timedelta, DynWinRTType, DynWinRTValue, DynWinRTArray, DynWinRTStruct, DynWinRtDelegate, - {object_input}, _DynWinRTProjector, + {object_input}, {object_value_alias}, {object_input_alias}, _DynWinRTProjector, )\n" ) } @@ -73,6 +75,7 @@ from dynwinrt import (\n\ DynWinRTType as DynWinRTType, DynWinRTValue as DynWinRTValue,\n\ DynWinRTArray as DynWinRTArray, DynWinRTStruct as DynWinRTStruct,\n\ DynWinRtDelegate as DynWinRtDelegate, WinGUID as WinGUID,\n\ + WinRTObjectInput as WinRTObjectInput, WinRTObjectValue as WinRTObjectValue,\n\ _DynWinRTProjector as _DynWinRTProjector,\n\ _DynWinRTProjectableClass as _DynWinRTProjectableClass,\n\ _DynWinRTRuntimeClass as _DynWinRTRuntimeClass,\n\ diff --git a/tools/dynwinrt-codegen/src/codegen/winrt/python/type_helpers.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/type_helpers.rs index 34d75531..a915fa7b 100644 --- a/tools/dynwinrt-codegen/src/codegen/winrt/python/type_helpers.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/python/type_helpers.rs @@ -57,6 +57,28 @@ pub(super) fn method_pydoc_with_indent( // Python type annotation helpers // ====================================================================== +/// Which side of a WinRT `Object` position an annotation describes. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum ObjectRole { + /// Values accepted by `to_winrt_object`. + Input, + /// Values returned by `from_winrt_object`. + Output, +} + +/// The single annotation hook for WinRT `Object` positions. +/// +/// Names the public `dynwinrt` alias for the role; callers add the position's +/// nullability through [`py_optional_type`], so `Object` positions always +/// include `None`. +pub(crate) fn py_object_annotation(context: &PythonProjectionContext, role: ObjectRole) -> String { + let alias = match role { + ObjectRole::Input => PythonSupportSymbol::WinRTObjectInput, + ObjectRole::Output => PythonSupportSymbol::WinRTObjectValue, + }; + context.support_symbol_reference(alias).to_string() +} + pub(crate) fn py_optional_type(typ: String) -> String { let unquoted = typ .strip_prefix('\'') @@ -108,8 +130,8 @@ fn py_param_type(typ: &TypeMeta, context: &PythonProjectionContext) -> String { } TypeMeta::Array(inner) => py_array_param_type(inner, context), TypeMeta::Object => format!( - "'DynWinRTValue | {}'", - context.support_symbol_reference(PythonSupportSymbol::ObjectInput) + "'{}'", + py_optional_type(py_object_annotation(context, ObjectRole::Input)) ), TypeMeta::Delegate { .. } => "'DynWinRTValue'".to_string(), TypeMeta::Struct { name, .. } if name == "HResult" => "int".to_string(), @@ -363,7 +385,10 @@ fn py_return_type(typ: Option<&TypeMeta>, context: &PythonProjectionContext) -> py_return_type(Some(progress), context) ), Some(TypeMeta::Array(inner)) => py_array_return_type(inner, context), - Some(TypeMeta::Object) | Some(TypeMeta::Delegate { .. }) => "'DynWinRTValue'".to_string(), + Some(TypeMeta::Object) => { + format!("'{}'", py_object_annotation(context, ObjectRole::Output)) + } + Some(TypeMeta::Delegate { .. }) => "'DynWinRTValue'".to_string(), Some(TypeMeta::Struct { name, .. }) if name == "HResult" => "int".to_string(), Some(typ) if foundation_type(typ) == Some(FoundationType::DateTime) => { "datetime".to_string() @@ -415,6 +440,7 @@ fn py_native_element_type(inner: &TypeMeta, context: &PythonProjectionContext) - "'DynWinRTValue'".to_string() } } + TypeMeta::Object => format!("'{}'", py_object_annotation(context, ObjectRole::Output)), _ => "'DynWinRTValue'".to_string(), } } @@ -537,6 +563,30 @@ pub(super) fn py_param_list( .join(", ") } +/// [`py_param_list`] for factory methods, where a composable factory's +/// controlling outer keeps its raw object-reference annotation. +pub(super) fn py_factory_param_list( + method: &MethodMeta, + in_params: &[&crate::meta::ParamMeta], + context: &PythonProjectionContext, +) -> String { + in_params + .iter() + .map(|p| { + if super::signature::py_is_composable_outer(method, p) { + format!( + "{}: 'DynWinRTValue | {}'", + to_snake_case(&p.name), + context.support_symbol_reference(PythonSupportSymbol::ObjectInput) + ) + } else { + py_param_list(std::slice::from_ref(p), context) + } + }) + .collect::>() + .join(", ") +} + /// Produce a typed Python annotation for a delegate parameter, with /// `TypedEventHandler` / `EventHandler` unwrapped. Bespoke non-parametric /// delegates fall back to `Callable[..., object]`. @@ -667,33 +717,41 @@ mod tests { fn object_arrays_return_typed_runtime_values() { assert_eq!( py_array_return_type(&TypeMeta::Object, &PythonProjectionContext::default()), - "list[DynWinRTValue | None]" + "list[WinRTObjectValue | None]" ); } #[test] - fn object_inputs_accept_native_wrappers_without_widening_outputs() { + fn object_positions_use_the_value_aliases_and_stay_nullable() { let context = PythonProjectionContext::default(); + assert_eq!( + py_object_annotation(&context, ObjectRole::Input), + "WinRTObjectInput" + ); + assert_eq!( + py_object_annotation(&context, ObjectRole::Output), + "WinRTObjectValue" + ); assert_eq!( py_param_type_safe(&TypeMeta::Object, &context), - "'DynWinRTValue | _DynWinRTObject'" + "'WinRTObjectInput | None'" ); assert_eq!( py_array_param_type(&TypeMeta::Object, &context), - "DynWinRTArray | Sequence['DynWinRTValue | _DynWinRTObject']" + "DynWinRTArray | Sequence['WinRTObjectInput | None']" ); for (name, piid, args, expected) in [ ( "IIterable`1", "faa585ea-6214-4217-afda-7f46de5869b3", vec![TypeMeta::Object], - "Iterable['DynWinRTValue | _DynWinRTObject']", + "Iterable['WinRTObjectInput | None']", ), ( "IMap`2", "3c2925fe-8519-45c1-aa79-197b6718c1c1", vec![TypeMeta::String, TypeMeta::Object], - "Mapping[str, 'DynWinRTValue | _DynWinRTObject']", + "Mapping[str, 'WinRTObjectInput | None']", ), ] { let typ = TypeMeta::Parameterized { @@ -706,24 +764,32 @@ mod tests { } assert_eq!( py_return_type_safe(Some(&TypeMeta::Object), &context), - "DynWinRTValue | None" + "WinRTObjectValue | None" ); assert_eq!( py_collection_input_type(&TypeMeta::Object, &context), - "DynWinRTValue | _DynWinRTObject | None" + "WinRTObjectInput | None" ); assert_eq!(py_collection_input_type(&TypeMeta::I32, &context), "int"); } #[test] - fn object_input_aliases_reach_collection_inputs_without_changing_outputs() { - let typ = TypeMeta::Struct { + fn object_value_aliases_yield_to_metadata_names() { + let value = TypeMeta::Struct { namespace: "Audit".into(), - name: "_DynWinRTObject".into(), + name: "WinRTObjectValue".into(), fields: vec![], }; - let context = PythonProjectionContext::packaged([typ.type_identity()]).unwrap(); - let context = context.for_struct_module(&typ, std::slice::from_ref(&typ)); + let input = TypeMeta::Struct { + namespace: "Audit".into(), + name: "WinRTObjectInput".into(), + fields: vec![], + }; + let context = + PythonProjectionContext::packaged([value.type_identity(), input.type_identity()]) + .unwrap(); + let structs = [value.clone(), input]; + let context = context.for_struct_module(&value, &structs); let mapping = TypeMeta::Parameterized { namespace: "Windows.Foundation.Collections".into(), name: "IMap`2".into(), @@ -732,19 +798,19 @@ mod tests { }; assert_eq!( py_param_type_safe(&mapping, &context), - "Mapping[str, 'DynWinRTValue | _DynWinRTObject_2']" - ); - assert_eq!( - py_collection_input_type(&TypeMeta::Object, &context), - "DynWinRTValue | _DynWinRTObject_2 | None" + "Mapping[str, 'WinRTObjectInput_2 | None']" ); assert_eq!( py_return_type_safe(Some(&TypeMeta::Object), &context), - "DynWinRTValue | None" + "WinRTObjectValue_2 | None" ); assert_eq!( py_array_return_type(&TypeMeta::Object, &context), - "list[DynWinRTValue | None]" + "list[WinRTObjectValue_2 | None]" + ); + assert_eq!( + context.support_symbol_import(PythonSupportSymbol::WinRTObjectValue), + "WinRTObjectValue as WinRTObjectValue_2" ); } @@ -776,7 +842,7 @@ mod tests { ); assert_eq!( py_return_type_safe(Some(&TypeMeta::Object), &context), - "DynWinRTValue | None" + "WinRTObjectValue | None" ); assert_eq!( py_array_return_type(&runtime_class, &context), diff --git a/tools/dynwinrt-codegen/tests/observable_vector_test.rs b/tools/dynwinrt-codegen/tests/observable_vector_test.rs index 2d2b788a..71378827 100644 --- a/tools/dynwinrt-codegen/tests/observable_vector_test.rs +++ b/tools/dynwinrt-codegen/tests/observable_vector_test.rs @@ -102,11 +102,12 @@ fn observable_vector_projects_python_mutable_sequence_and_typed_events() { py.contains("_dynwinrt_symbol('i_vector_object', 'IVector_Object')._set_native(self, obj)") ); assert!(py.contains("self._observable_obj = obj.cast(IID_IObservableVector_Object)")); - let create_signature = "def create(items: Iterable['DynWinRTValue | _DynWinRTObject']) -> 'IObservableVector_Object':"; + let create_signature = + "def create(items: Iterable['WinRTObjectInput | None']) -> 'IObservableVector_Object':"; assert!(py.contains(create_signature), "{py}"); assert!( py.contains( - "_dynwinrt_new_vector(items, lambda item: getattr(item, '_obj', item), DynWinRTType.object())" + "_dynwinrt_new_vector(items, lambda item: _dynwinrt_to_winrt_object(item), DynWinRTType.object())" ), "{py}" ); @@ -120,7 +121,7 @@ fn observable_vector_projects_python_mutable_sequence_and_typed_events() { let pyi = common::generate_interface_stub(&interface, &known_types, &delegate_types); assert!( pyi.contains( - "class IObservableVector_Object(_IObservableVector_ObjectIdentity, MutableSequence[DynWinRTValue | None]):" + "class IObservableVector_Object(_IObservableVector_ObjectIdentity, MutableSequence[WinRTObjectValue | None]):" ), "{pyi}" ); @@ -129,7 +130,7 @@ fn observable_vector_projects_python_mutable_sequence_and_typed_events() { )); assert!(pyi.contains(&format!("{create_signature} ...")), "{pyi}"); assert!( - pyi.contains("def __getitem__(self, index: int) -> DynWinRTValue | None: ..."), + pyi.contains("def __getitem__(self, index: int) -> WinRTObjectValue | None: ..."), "{pyi}" ); assert!(pyi.contains("def as_vector(self) -> 'IVector_Object': ...")); diff --git a/tools/dynwinrt-codegen/tests/python_consumer_typing_test.rs b/tools/dynwinrt-codegen/tests/python_consumer_typing_test.rs index 622a464b..72d74105 100644 --- a/tools/dynwinrt-codegen/tests/python_consumer_typing_test.rs +++ b/tools/dynwinrt-codegen/tests/python_consumer_typing_test.rs @@ -365,7 +365,7 @@ fn strict_consumers_separate_instances_factories_and_native_object_inputs() { &fixture, &["first", "second"], r#"from typing import assert_type -from dynwinrt import DynWinRTValue, _DynWinRTProjector +from dynwinrt import DynWinRTValue, UInt32, WinRTObjectValue, _DynWinRTProjector from first.contoso__i_item import IItem from first.contoso__resource import Resource, ResourceLike from first.contoso__derived_resource import DerivedResource @@ -399,13 +399,18 @@ def valid(raw: DynWinRTValue, resource: Resource, derived: DerivedResource, assert_type(IItem.from_value(raw), IItem) assert_type(resource.as_interface(IItem), IItem) assert_type(resource.as_interface(OtherItem), OtherItem) - assert_type(content.content, DynWinRTValue | None) + assert_type(content.content, WinRTObjectValue | None) content.content = resource content.content = other content.content = item content.content = raw + content.content = "boxed" + content.content = None + content.content = 42 + content.content = UInt32(42) content.set_object(derived) content.set_objects([resource, other, item, raw]) + content.set_objects([42, "text", None, [1, 2]]) def collections(resource: Resource, derived: OtherDerived, raw: DynWinRTValue, vector: IVector_Object, resources: IVector_Resource, @@ -419,10 +424,13 @@ def collections(resource: Resource, derived: OtherDerived, raw: DynWinRTValue, resources.insert(0, derived) resources.append(derived) mapping[resource] = derived - assert_type(vector[0], DynWinRTValue | None) - assert_type(vector[:], list[DynWinRTValue | None]) + vector[0] = [resource] + vector[:] = [42, "text"] + mapping["key"] = 1.5 + assert_type(vector[0], WinRTObjectValue | None) + assert_type(vector[:], list[WinRTObjectValue | None]) assert_type(resources[0], Resource | None) - assert_type(mapping[resource], DynWinRTValue | None) + assert_type(mapping[resource], WinRTObjectValue | None) del mapping[resource] "#, &[], @@ -458,19 +466,16 @@ def invalid(raw: DynWinRTValue, resource: Resource, unrelated: Unrelated, use_base(unrelated) resource.as_interface(Resource) content.content = object() - content.content = "not boxed" - content.content = None content.content = Resource content.set_object(WrongObject()) - content.set_objects([42]) + content.set_objects([object()]) def invalid_collections(resource: Resource, unrelated: Unrelated, vector: IVector_Object, resources: IVector_Resource, mapping: IMap_Object_Object) -> None: vector[0] = object() - vector[0] = [resource] vector[:] = resource - vector[:] = [42] + vector[:] = [object()] resources[0] = unrelated resources.append(unrelated) mapping[WrongObject()] = resource @@ -484,13 +489,10 @@ def invalid_collections(resource: Resource, unrelated: Unrelated, "[arg-type]", "[assignment]", "[assignment]", - "[assignment]", - "[assignment]", "[arg-type]", "[list-item]", "[call-overload]", "[call-overload]", - "[call-overload]", "[list-item]", "[call-overload]", "[arg-type]", @@ -531,7 +533,7 @@ fn real_windows_consumers_accept_file_stream_content_and_composition_instances() &fixture, &["sdk"], r#"from typing import assert_type -from dynwinrt import DynWinRTValue +from dynwinrt import WinRTObjectValue from sdk.windows.storage import FileIO, StorageFile from sdk.windows.media.playback import MediaPlayer from sdk.windows.media.speech_synthesis import SpeechSynthesisStream @@ -553,8 +555,9 @@ def buffer_view(buffer: Buffer, data: bytes) -> IBuffer: return buffer def content(button: Button, text: TextBlock) -> None: - assert_type(button.content, DynWinRTValue | None) + assert_type(button.content, WinRTObjectValue | None) button.content = text + button.content = "Click me" def reference(animation: ExpressionAnimation, visual: ContainerVisual) -> None: animation.set_reference_parameter("target", visual) @@ -687,7 +690,7 @@ print("subclass-factory-native-ok", flush=True) } #[test] -fn collection_subscripts_accept_projected_inputs_and_keep_raw_outputs() { +fn collection_subscripts_box_values_and_unbox_outputs() { let winmd = Path::new( r"C:\Program Files (x86)\Windows Kits\10\UnionMetadata\10.0.26100.0\Windows.winmd", ); @@ -711,7 +714,7 @@ fn collection_subscripts_accept_projected_inputs_and_keep_raw_outputs() { .unwrap(); assert!(output.status.success(), "{}", diagnostics(&output)); let imports = r#"from typing import assert_type -from dynwinrt import DynWinRTValue +from dynwinrt import DynWinRTValue, UInt32, WinRTObjectValue from sdk.windows.foundation import Uri from sdk.windows.foundation.collections import IMap_String_Object, PropertySet from sdk.windows__foundation__collections__property_set import ( @@ -728,13 +731,21 @@ def valid(uri: Uri, raw: DynWinRTValue, properties: PropertySet, embedded: EmbeddedMap) -> None: properties["uri"] = uri properties["raw"] = raw + properties["string"] = "boxed" + properties["number"] = 42 + properties["tagged"] = UInt32(42) + properties["list"] = [1, 2, 3] + properties["none"] = None + properties.update({{"a": 1, "b": "two"}}) like["uri"] = uri mapping["uri"] = uri + mapping["number"] = 42 embedded["uri"] = uri - assert_type(properties["uri"], DynWinRTValue | None) - assert_type(like["uri"], DynWinRTValue | None) - assert_type(mapping["uri"], DynWinRTValue | None) - assert_type(embedded["uri"], DynWinRTValue | None) + assert_type(properties["uri"], WinRTObjectValue | None) + assert_type(like["uri"], WinRTObjectValue | None) + assert_type(mapping["uri"], WinRTObjectValue | None) + assert_type(embedded["uri"], WinRTObjectValue | None) + assert_type(properties.get("uri"), WinRTObjectValue | None) del properties["uri"] del mapping["uri"] "# @@ -751,9 +762,8 @@ class WrongObject: def invalid(uri: Uri, properties: PropertySet, mapping: IMap_String_Object) -> None: properties["plain"] = object() - properties["string"] = "unboxed" properties["wrong"] = WrongObject() - mapping["number"] = 42 + properties["set"] = {{1, 2}} mapping["class"] = Uri mapping[42] = uri result: Uri = properties["uri"] @@ -764,7 +774,6 @@ def invalid(uri: Uri, properties: PropertySet, mapping: IMap_String_Object) -> N "[assignment]", "[assignment]", "[assignment]", - "[assignment]", "[index]", "[assignment]", ], @@ -772,7 +781,7 @@ def invalid(uri: Uri, properties: PropertySet, mapping: IMap_String_Object) -> N if has_implementation_runtime() { fs::write( fixture.0.join("collections_runtime.py"), - r#"from dynwinrt import DynWinRTValue, RoApartment, projected_lifetime_scope + r#"from dynwinrt import DynWinRTValue, RoApartment, UInt32, projected_lifetime_scope from sdk.windows.foundation import Uri from sdk.windows.foundation.collections import IMap_String_Object, PropertySet @@ -788,6 +797,14 @@ with RoApartment(1), projected_lifetime_scope(): value.release() collection["null"] = DynWinRTValue.null_value() assert collection["null"] is None + collection["none"] = None + assert collection["none"] is None + collection["number"] = 42 + assert collection["number"] == 42 and type(collection["number"]) is int + collection["tagged"] = UInt32(7) + assert repr(collection["tagged"]) == "dynwinrt.UInt32(7)" + collection["tagged_again"] = collection["tagged"] + assert type(collection["tagged_again"]) is UInt32 collection.insert("raw", uri._obj) value = collection["raw"] assert isinstance(value, DynWinRTValue) @@ -821,8 +838,10 @@ fn native_object_inputs_keep_projection_factories_and_context_lifetimes() { let fixture = Fixture::new(); generate_fixture(&fixture, "views"); let script = r#"from typing import get_type_hints -from dynwinrt import DynWinRTValue, RoApartment, project_as, projected_lifetime_scope -from views._runtime import _DynWinRTObject +from dynwinrt import ( + DynWinRTValue, RoApartment, UInt32, WinRTObjectInput, WinRTObjectValue, project_as, + projected_lifetime_scope, +) from views.contoso__i_item import IItem from views.contoso__i_content import IContent from views.contoso__resource import Resource @@ -841,26 +860,27 @@ class ItemHandlers: class ContentHandlers: def __init__(self) -> None: - self.value: DynWinRTValue | None = None - self.items: list[DynWinRTValue | None] = [] + self.value: WinRTObjectValue | None = None + self.items: list[WinRTObjectValue | None] = [] - def get_content(self) -> DynWinRTValue | None: + def get_content(self) -> WinRTObjectInput | None: return self.value - def set_content(self, value: DynWinRTValue | None) -> None: + def set_content(self, value: WinRTObjectValue | None) -> None: self.value = value - def set_object(self, value: DynWinRTValue | None) -> None: + def set_object(self, value: WinRTObjectValue | None) -> None: self.value = value - def set_objects(self, value: list[DynWinRTValue | None]) -> None: + def set_objects(self, value: list[WinRTObjectValue | None]) -> None: self.items = value class WrongObject: _obj = 42 -assert get_type_hints(Content.set_object)["value"] == DynWinRTValue | _DynWinRTObject -assert get_type_hints(Content.content.fset)["value"] == DynWinRTValue | _DynWinRTObject +assert get_type_hints(Content.set_object)["value"] == WinRTObjectInput | None +assert get_type_hints(Content.content.fset)["value"] == WinRTObjectInput | None +assert get_type_hints(Content.content.fget)["return"] == WinRTObjectValue | None assert "value" in get_type_hints(Content.set_objects) item_handlers = ItemHandlers() content_handlers = ContentHandlers() @@ -886,6 +906,19 @@ with RoApartment(1), projected_lifetime_scope(): assert all(item is not None for item in content_handlers.items) content.content = DynWinRTValue.null_value() assert content.content is None + # Values box on the way in, unbox for the Python handler, and + # keep their exact WinRT type on the way back out. + for plain in ("boxed", 42, 2.5, True, UInt32(7), [1, 2], None): + content.content = plain + assert content_handlers.value == plain + assert type(content_handlers.value) is not DynWinRTValue + result = content.content + assert result == plain and type(result) is type(plain) or ( + isinstance(plain, list) and result == plain + ), (plain, result) + content.set_objects(["text", 42, None, resource]) + assert content_handlers.items[:3] == ["text", 42, None] + assert isinstance(content_handlers.items[3], DynWinRTValue) assert item_handlers.closed == 1 try: with resource as entered: @@ -896,7 +929,7 @@ with RoApartment(1), projected_lifetime_scope(): else: raise AssertionError("__exit__ suppressed an exception") assert item_handlers.closed == 2 - for invalid in (object(), "not boxed", None, 42, WrongObject()): + for invalid in (object(), WrongObject(), {"not": "boxable"}): try: content.set_object(invalid) except TypeError: diff --git a/tools/dynwinrt-codegen/tests/python_property_name_collision_test.rs b/tools/dynwinrt-codegen/tests/python_property_name_collision_test.rs index 570bb835..dd1084c6 100644 --- a/tools/dynwinrt-codegen/tests/python_property_name_collision_test.rs +++ b/tools/dynwinrt-codegen/tests/python_property_name_collision_test.rs @@ -50,13 +50,13 @@ fn property_named_property_does_not_shadow_the_decorator() { for name in ["property", "old_value"] { assert!( py.contains(&format!( - " @_property\n def {name}(self) -> DynWinRTValue | None:" + " @_property\n def {name}(self) -> WinRTObjectValue | None:" )), "{py}" ); assert!( pyi.contains(&format!( - " @builtins.property\n def {name}(self) -> DynWinRTValue | None: ..." + " @builtins.property\n def {name}(self) -> WinRTObjectValue | None: ..." )), "{pyi}" ); diff --git a/tools/dynwinrt-codegen/tests/python_symbol_mapping_test.rs b/tools/dynwinrt-codegen/tests/python_symbol_mapping_test.rs index 739bda9b..d7ab13a8 100644 --- a/tools/dynwinrt-codegen/tests/python_symbol_mapping_test.rs +++ b/tools/dynwinrt-codegen/tests/python_symbol_mapping_test.rs @@ -3322,6 +3322,7 @@ fn object_input_helper_consumers(packaged: bool) { ("ObjectLeaf", false), ("_DynWinRTObject_2", false), ("_DynWinRTObject", true), + ("WinRTObjectInput", false), ] { for owner in ["Widget", "IWidget"] { let fixture = Fixture::new(); @@ -3464,9 +3465,16 @@ Holder(17) } else { format!("_DynWinRTObject as {alias}") }; + // Object value positions use the public input alias, which also + // yields to a metadata type of the same name. + let input_alias = if leaf == "WinRTObjectInput" { + "WinRTObjectInput_2" + } else { + "WinRTObjectInput" + }; for text in [&source, &stub] { assert!(text.contains(&format!(" {import},")), "{text}"); - assert!(text.contains(&format!("DynWinRTValue | {alias}")), "{text}"); + assert!(text.contains(&format!("'{input_alias} | None'")), "{text}"); assert!(text.contains(&format!("value: '{leaf}'")), "{text}"); } let public_check = if packaged { @@ -3485,15 +3493,14 @@ Holder(17) r#"{imports} import typing import dynwinrt as dw -from pyviews._runtime import _DynWinRTObject as ObjectInput {public_check} {peer_runtime} assert Leaf.__name__ == '{leaf}' assert typing.get_type_hints(Owner.echo_leaf)['value'] is Leaf assert typing.get_type_hints(Owner.echo_leaf)['return'] is Leaf -assert typing.get_type_hints(Owner.set_object)['value'] == dw.DynWinRTValue | ObjectInput +assert typing.get_type_hints(Owner.set_object)['value'] == dw.WinRTObjectInput | None array_hint = typing.get_type_hints(Owner.set_objects)['value'] -assert typing.get_args(typing.get_args(array_hint)[1])[0] == dw.DynWinRTValue | ObjectInput +assert typing.get_args(typing.get_args(array_hint)[1])[0] == dw.WinRTObjectInput | None with dw.RoApartment(1): for Container, Item, pack, unpack in ( (Holder, Leaf, pack_holder, unpack_holder), diff --git a/tools/dynwinrt-codegen/tests/snapshots/data_writer_py/data_writer.py b/tools/dynwinrt-codegen/tests/snapshots/data_writer_py/data_writer.py index 42b7b420..0e7d58c0 100644 --- a/tools/dynwinrt-codegen/tests/snapshots/data_writer_py/data_writer.py +++ b/tools/dynwinrt-codegen/tests/snapshots/data_writer_py/data_writer.py @@ -5,7 +5,7 @@ TYPE_CHECKING, UUID, WinGUID, datetime, timedelta, DynWinRTType, DynWinRTMethodSig, DynWinRTValue, DynWinRTArray, DynWinRTStruct, DynWinRtDelegate, DynWinRTOverrideInterface, - _DynWinRTObject, _property, _weakref_ref, + _DynWinRTObject, WinRTObjectValue, WinRTObjectInput, _property, _weakref_ref, _dynwinrt_array, _dynwinrt_bind_overload, _dynwinrt_can_cast, _dynwinrt_create_delegate, _dynwinrt_datetime_to_ticks, _dynwinrt_delegate, _dynwinrt_enum, _dynwinrt_guid, _dynwinrt_map, _dynwinrt_new_vector, _dynwinrt_ticks_to_datetime, @@ -13,6 +13,7 @@ _dynwinrt_cache_projected, _dynwinrt_projected_from_native, _dynwinrt_symbol, _dynwinrt_track_projected, _dynwinrt_uuid, _dynwinrt_vector, _dynwinrt_wrap_values, + _dynwinrt_from_winrt_object, _dynwinrt_to_winrt_object, ) from dynwinrt import WinRTCoroutine, WinRTCoroutineWithProgress from dynwinrt.dynwinrt import _DynWinRTAsync, _DynWinRTAsyncWithProgress diff --git a/tools/dynwinrt-codegen/tests/snapshots/uri_py/i_iterator_i_www_form_url_decoder_entry.py b/tools/dynwinrt-codegen/tests/snapshots/uri_py/i_iterator_i_www_form_url_decoder_entry.py index 2937caea..e5ccd0e4 100644 --- a/tools/dynwinrt-codegen/tests/snapshots/uri_py/i_iterator_i_www_form_url_decoder_entry.py +++ b/tools/dynwinrt-codegen/tests/snapshots/uri_py/i_iterator_i_www_form_url_decoder_entry.py @@ -5,7 +5,7 @@ TYPE_CHECKING, UUID, WinGUID, datetime, timedelta, DynWinRTType, DynWinRTMethodSig, DynWinRTValue, DynWinRTArray, DynWinRTStruct, DynWinRtDelegate, DynWinRTOverrideInterface, - _DynWinRTObject, _property, _weakref_ref, + _DynWinRTObject, WinRTObjectValue, WinRTObjectInput, _property, _weakref_ref, _dynwinrt_array, _dynwinrt_bind_overload, _dynwinrt_can_cast, _dynwinrt_create_delegate, _dynwinrt_datetime_to_ticks, _dynwinrt_delegate, _dynwinrt_enum, _dynwinrt_guid, _dynwinrt_map, _dynwinrt_new_vector, _dynwinrt_ticks_to_datetime, @@ -13,6 +13,7 @@ _dynwinrt_cache_projected, _dynwinrt_projected_from_native, _dynwinrt_symbol, _dynwinrt_track_projected, _dynwinrt_uuid, _dynwinrt_vector, _dynwinrt_wrap_values, + _dynwinrt_from_winrt_object, _dynwinrt_to_winrt_object, ) from dynwinrt.dynwinrt import _WinRTIteratorMixin diff --git a/tools/dynwinrt-codegen/tests/snapshots/uri_py/i_stringable.py b/tools/dynwinrt-codegen/tests/snapshots/uri_py/i_stringable.py index d1b2576a..3bced786 100644 --- a/tools/dynwinrt-codegen/tests/snapshots/uri_py/i_stringable.py +++ b/tools/dynwinrt-codegen/tests/snapshots/uri_py/i_stringable.py @@ -5,7 +5,7 @@ TYPE_CHECKING, UUID, WinGUID, datetime, timedelta, DynWinRTType, DynWinRTMethodSig, DynWinRTValue, DynWinRTArray, DynWinRTStruct, DynWinRtDelegate, DynWinRTOverrideInterface, - _DynWinRTObject, _property, _weakref_ref, + _DynWinRTObject, WinRTObjectValue, WinRTObjectInput, _property, _weakref_ref, _dynwinrt_array, _dynwinrt_bind_overload, _dynwinrt_can_cast, _dynwinrt_create_delegate, _dynwinrt_datetime_to_ticks, _dynwinrt_delegate, _dynwinrt_enum, _dynwinrt_guid, _dynwinrt_map, _dynwinrt_new_vector, _dynwinrt_ticks_to_datetime, @@ -13,6 +13,7 @@ _dynwinrt_cache_projected, _dynwinrt_projected_from_native, _dynwinrt_symbol, _dynwinrt_track_projected, _dynwinrt_uuid, _dynwinrt_vector, _dynwinrt_wrap_values, + _dynwinrt_from_winrt_object, _dynwinrt_to_winrt_object, ) from typing import Protocol, TypedDict from dynwinrt import ( diff --git a/tools/dynwinrt-codegen/tests/snapshots/uri_py/i_uri_runtime_class_with_absolute_canonical_uri.py b/tools/dynwinrt-codegen/tests/snapshots/uri_py/i_uri_runtime_class_with_absolute_canonical_uri.py index 64c0fed9..eab13802 100644 --- a/tools/dynwinrt-codegen/tests/snapshots/uri_py/i_uri_runtime_class_with_absolute_canonical_uri.py +++ b/tools/dynwinrt-codegen/tests/snapshots/uri_py/i_uri_runtime_class_with_absolute_canonical_uri.py @@ -5,7 +5,7 @@ TYPE_CHECKING, UUID, WinGUID, datetime, timedelta, DynWinRTType, DynWinRTMethodSig, DynWinRTValue, DynWinRTArray, DynWinRTStruct, DynWinRtDelegate, DynWinRTOverrideInterface, - _DynWinRTObject, _property, _weakref_ref, + _DynWinRTObject, WinRTObjectValue, WinRTObjectInput, _property, _weakref_ref, _dynwinrt_array, _dynwinrt_bind_overload, _dynwinrt_can_cast, _dynwinrt_create_delegate, _dynwinrt_datetime_to_ticks, _dynwinrt_delegate, _dynwinrt_enum, _dynwinrt_guid, _dynwinrt_map, _dynwinrt_new_vector, _dynwinrt_ticks_to_datetime, @@ -13,6 +13,7 @@ _dynwinrt_cache_projected, _dynwinrt_projected_from_native, _dynwinrt_symbol, _dynwinrt_track_projected, _dynwinrt_uuid, _dynwinrt_vector, _dynwinrt_wrap_values, + _dynwinrt_from_winrt_object, _dynwinrt_to_winrt_object, ) from typing import Protocol, TypedDict from dynwinrt import ( diff --git a/tools/dynwinrt-codegen/tests/snapshots/uri_py/i_www_form_url_decoder_entry.py b/tools/dynwinrt-codegen/tests/snapshots/uri_py/i_www_form_url_decoder_entry.py index d255ba1d..4931094f 100644 --- a/tools/dynwinrt-codegen/tests/snapshots/uri_py/i_www_form_url_decoder_entry.py +++ b/tools/dynwinrt-codegen/tests/snapshots/uri_py/i_www_form_url_decoder_entry.py @@ -5,7 +5,7 @@ TYPE_CHECKING, UUID, WinGUID, datetime, timedelta, DynWinRTType, DynWinRTMethodSig, DynWinRTValue, DynWinRTArray, DynWinRTStruct, DynWinRtDelegate, DynWinRTOverrideInterface, - _DynWinRTObject, _property, _weakref_ref, + _DynWinRTObject, WinRTObjectValue, WinRTObjectInput, _property, _weakref_ref, _dynwinrt_array, _dynwinrt_bind_overload, _dynwinrt_can_cast, _dynwinrt_create_delegate, _dynwinrt_datetime_to_ticks, _dynwinrt_delegate, _dynwinrt_enum, _dynwinrt_guid, _dynwinrt_map, _dynwinrt_new_vector, _dynwinrt_ticks_to_datetime, @@ -13,6 +13,7 @@ _dynwinrt_cache_projected, _dynwinrt_projected_from_native, _dynwinrt_symbol, _dynwinrt_track_projected, _dynwinrt_uuid, _dynwinrt_vector, _dynwinrt_wrap_values, + _dynwinrt_from_winrt_object, _dynwinrt_to_winrt_object, ) from typing import Protocol, TypedDict from dynwinrt import ( diff --git a/tools/dynwinrt-codegen/tests/snapshots/uri_py/uri.py b/tools/dynwinrt-codegen/tests/snapshots/uri_py/uri.py index 70d15aa3..f156aeae 100644 --- a/tools/dynwinrt-codegen/tests/snapshots/uri_py/uri.py +++ b/tools/dynwinrt-codegen/tests/snapshots/uri_py/uri.py @@ -5,7 +5,7 @@ TYPE_CHECKING, UUID, WinGUID, datetime, timedelta, DynWinRTType, DynWinRTMethodSig, DynWinRTValue, DynWinRTArray, DynWinRTStruct, DynWinRtDelegate, DynWinRTOverrideInterface, - _DynWinRTObject, _property, _weakref_ref, + _DynWinRTObject, WinRTObjectValue, WinRTObjectInput, _property, _weakref_ref, _dynwinrt_array, _dynwinrt_bind_overload, _dynwinrt_can_cast, _dynwinrt_create_delegate, _dynwinrt_datetime_to_ticks, _dynwinrt_delegate, _dynwinrt_enum, _dynwinrt_guid, _dynwinrt_map, _dynwinrt_new_vector, _dynwinrt_ticks_to_datetime, @@ -13,6 +13,7 @@ _dynwinrt_cache_projected, _dynwinrt_projected_from_native, _dynwinrt_symbol, _dynwinrt_track_projected, _dynwinrt_uuid, _dynwinrt_vector, _dynwinrt_wrap_values, + _dynwinrt_from_winrt_object, _dynwinrt_to_winrt_object, ) if TYPE_CHECKING: diff --git a/tools/dynwinrt-codegen/tests/snapshots/uri_py/www_form_url_decoder.py b/tools/dynwinrt-codegen/tests/snapshots/uri_py/www_form_url_decoder.py index f9baae09..c3d3db20 100644 --- a/tools/dynwinrt-codegen/tests/snapshots/uri_py/www_form_url_decoder.py +++ b/tools/dynwinrt-codegen/tests/snapshots/uri_py/www_form_url_decoder.py @@ -5,7 +5,7 @@ TYPE_CHECKING, UUID, WinGUID, datetime, timedelta, DynWinRTType, DynWinRTMethodSig, DynWinRTValue, DynWinRTArray, DynWinRTStruct, DynWinRtDelegate, DynWinRTOverrideInterface, - _DynWinRTObject, _property, _weakref_ref, + _DynWinRTObject, WinRTObjectValue, WinRTObjectInput, _property, _weakref_ref, _dynwinrt_array, _dynwinrt_bind_overload, _dynwinrt_can_cast, _dynwinrt_create_delegate, _dynwinrt_datetime_to_ticks, _dynwinrt_delegate, _dynwinrt_enum, _dynwinrt_guid, _dynwinrt_map, _dynwinrt_new_vector, _dynwinrt_ticks_to_datetime, @@ -13,6 +13,7 @@ _dynwinrt_cache_projected, _dynwinrt_projected_from_native, _dynwinrt_symbol, _dynwinrt_track_projected, _dynwinrt_uuid, _dynwinrt_vector, _dynwinrt_wrap_values, + _dynwinrt_from_winrt_object, _dynwinrt_to_winrt_object, ) from dynwinrt.dynwinrt import _WinRTIterableMixin, _WinRTSequenceMixin diff --git a/tools/dynwinrt-codegen/tests/snapshots/uri_pyi/i_iterator_i_www_form_url_decoder_entry.pyi b/tools/dynwinrt-codegen/tests/snapshots/uri_pyi/i_iterator_i_www_form_url_decoder_entry.pyi index ae2e8fc3..9cd4f3da 100644 --- a/tools/dynwinrt-codegen/tests/snapshots/uri_pyi/i_iterator_i_www_form_url_decoder_entry.pyi +++ b/tools/dynwinrt-codegen/tests/snapshots/uri_pyi/i_iterator_i_www_form_url_decoder_entry.pyi @@ -5,7 +5,7 @@ from ._typing import ( Callable, Iterable, Iterator, Mapping, MutableMapping, MutableSequence, Sequence, UUID, WinGUID, datetime, overload, timedelta, DynWinRTType, DynWinRTValue, DynWinRTArray, DynWinRTStruct, DynWinRtDelegate, - _DynWinRTObject, _DynWinRTProjector, + _DynWinRTObject, WinRTObjectValue, WinRTObjectInput, _DynWinRTProjector, ) from typing import Protocol, Self, TypeVar diff --git a/tools/dynwinrt-codegen/tests/snapshots/uri_pyi/i_stringable.pyi b/tools/dynwinrt-codegen/tests/snapshots/uri_pyi/i_stringable.pyi index 8a689201..76e91b27 100644 --- a/tools/dynwinrt-codegen/tests/snapshots/uri_pyi/i_stringable.pyi +++ b/tools/dynwinrt-codegen/tests/snapshots/uri_pyi/i_stringable.pyi @@ -5,7 +5,7 @@ from ._typing import ( Callable, Iterable, Iterator, Mapping, MutableMapping, MutableSequence, Sequence, UUID, WinGUID, datetime, overload, timedelta, DynWinRTType, DynWinRTValue, DynWinRTArray, DynWinRTStruct, DynWinRtDelegate, - _DynWinRTObject, _DynWinRTProjector, + _DynWinRTObject, WinRTObjectValue, WinRTObjectInput, _DynWinRTProjector, ) from abc import ABCMeta from typing import Protocol, TypedDict diff --git a/tools/dynwinrt-codegen/tests/snapshots/uri_pyi/i_uri_runtime_class_with_absolute_canonical_uri.pyi b/tools/dynwinrt-codegen/tests/snapshots/uri_pyi/i_uri_runtime_class_with_absolute_canonical_uri.pyi index 10b3a07b..6f658d1d 100644 --- a/tools/dynwinrt-codegen/tests/snapshots/uri_pyi/i_uri_runtime_class_with_absolute_canonical_uri.pyi +++ b/tools/dynwinrt-codegen/tests/snapshots/uri_pyi/i_uri_runtime_class_with_absolute_canonical_uri.pyi @@ -5,7 +5,7 @@ from ._typing import ( Callable, Iterable, Iterator, Mapping, MutableMapping, MutableSequence, Sequence, UUID, WinGUID, datetime, overload, timedelta, DynWinRTType, DynWinRTValue, DynWinRTArray, DynWinRTStruct, DynWinRtDelegate, - _DynWinRTObject, _DynWinRTProjector, + _DynWinRTObject, WinRTObjectValue, WinRTObjectInput, _DynWinRTProjector, ) from abc import ABCMeta from typing import Protocol, TypedDict diff --git a/tools/dynwinrt-codegen/tests/snapshots/uri_pyi/i_www_form_url_decoder_entry.pyi b/tools/dynwinrt-codegen/tests/snapshots/uri_pyi/i_www_form_url_decoder_entry.pyi index 612cec3e..f4fe6a08 100644 --- a/tools/dynwinrt-codegen/tests/snapshots/uri_pyi/i_www_form_url_decoder_entry.pyi +++ b/tools/dynwinrt-codegen/tests/snapshots/uri_pyi/i_www_form_url_decoder_entry.pyi @@ -5,7 +5,7 @@ from ._typing import ( Callable, Iterable, Iterator, Mapping, MutableMapping, MutableSequence, Sequence, UUID, WinGUID, datetime, overload, timedelta, DynWinRTType, DynWinRTValue, DynWinRTArray, DynWinRTStruct, DynWinRtDelegate, - _DynWinRTObject, _DynWinRTProjector, + _DynWinRTObject, WinRTObjectValue, WinRTObjectInput, _DynWinRTProjector, ) from abc import ABCMeta from typing import Protocol, TypedDict diff --git a/tools/dynwinrt-codegen/tests/snapshots/uri_pyi/uri.pyi b/tools/dynwinrt-codegen/tests/snapshots/uri_pyi/uri.pyi index 815ffece..ceccdbf2 100644 --- a/tools/dynwinrt-codegen/tests/snapshots/uri_pyi/uri.pyi +++ b/tools/dynwinrt-codegen/tests/snapshots/uri_pyi/uri.pyi @@ -5,7 +5,7 @@ from ._typing import ( Callable, Iterable, Iterator, Mapping, MutableMapping, MutableSequence, Sequence, UUID, WinGUID, datetime, overload, timedelta, DynWinRTType, DynWinRTValue, DynWinRTArray, DynWinRTStruct, DynWinRtDelegate, - _DynWinRTObject, _DynWinRTProjector, + _DynWinRTObject, WinRTObjectValue, WinRTObjectInput, _DynWinRTProjector, ) from ._typing import _DynWinRTRuntimeClass from typing import Protocol, Self diff --git a/tools/dynwinrt-codegen/tests/snapshots/uri_pyi/www_form_url_decoder.pyi b/tools/dynwinrt-codegen/tests/snapshots/uri_pyi/www_form_url_decoder.pyi index 46688389..51b1fec0 100644 --- a/tools/dynwinrt-codegen/tests/snapshots/uri_pyi/www_form_url_decoder.pyi +++ b/tools/dynwinrt-codegen/tests/snapshots/uri_pyi/www_form_url_decoder.pyi @@ -5,7 +5,7 @@ from ._typing import ( Callable, Iterable, Iterator, Mapping, MutableMapping, MutableSequence, Sequence, UUID, WinGUID, datetime, overload, timedelta, DynWinRTType, DynWinRTValue, DynWinRTArray, DynWinRTStruct, DynWinRtDelegate, - _DynWinRTObject, _DynWinRTProjector, + _DynWinRTObject, WinRTObjectValue, WinRTObjectInput, _DynWinRTProjector, ) from ._typing import _DynWinRTRuntimeClass from typing import Protocol, Self diff --git a/tools/dynwinrt-codegen/tests/winrt_implementation_test.rs b/tools/dynwinrt-codegen/tests/winrt_implementation_test.rs index 9aa88922..93e530a3 100644 --- a/tools/dynwinrt-codegen/tests/winrt_implementation_test.rs +++ b/tools/dynwinrt-codegen/tests/winrt_implementation_test.rs @@ -902,6 +902,9 @@ for name, value in dict( _dynwinrt_cache_projected=lambda value: None, _dynwinrt_symbol=lambda module, name: namespace[name], _dynwinrt_array=lambda values, wrap, element_type, bytes_allowed: Array([wrap(value) for value in values]).to_value(), + # Projection doubles for the Object conversion hooks (native values only). + _dynwinrt_to_winrt_object=lambda value: Value.null_value() if value is None else getattr(value, '_obj', value), + _dynwinrt_from_winrt_object=lambda value: None if value.is_null() else value, ).items(): setattr(runtime, name, value) binding = types.ModuleType('dynwinrt') for name, value in dict(DynWinRTInterfacePlan=Plan, DynWinRTImplementationMethod=Method, From 52bc029e2c1e93bbaf94679483e69968d407240d Mon Sep 17 00:00:00 2001 From: Leilei Zhang Date: Thu, 24 Sep 2026 14:58:24 +0800 Subject: [PATCH 3/3] Add generated-binding Object scenarios and the boxing demo EXPERIMENTAL prototype for design evaluation; not for merge. test_object_values_generated.py generates Windows SDK bindings and runs each scenario in a fresh process: a PropertyValue.create_X round trip for every PropertyType, plain-value writes, PropertySet/ValueSet/StringMap mapping behavior, identity and project_as, generated enums and geometry structs, DeviceInformation and StorageFile property stores, and DeviceWatcher.EnumerationCompleted arguments typed Object. samples/python/object-boxing-demo shows the natural code, times the conversion, and is a mypy --strict / pyright typing consumer. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- bindings/py/tests/object_values_scenarios.py | 299 ++++++++++++++++++ .../py/tests/test_object_values_generated.py | 70 ++++ samples/python/object-boxing-demo/.gitignore | 3 + samples/python/object-boxing-demo/README.md | 34 ++ samples/python/object-boxing-demo/app.py | 113 +++++++ .../python/object-boxing-demo/benchmark.py | 92 ++++++ .../python/object-boxing-demo/generate.ps1 | 64 ++++ samples/python/object-boxing-demo/run.ps1 | 23 ++ .../python/object-boxing-demo/typing_check.py | 47 +++ 9 files changed, 745 insertions(+) create mode 100644 bindings/py/tests/object_values_scenarios.py create mode 100644 bindings/py/tests/test_object_values_generated.py create mode 100644 samples/python/object-boxing-demo/.gitignore create mode 100644 samples/python/object-boxing-demo/README.md create mode 100644 samples/python/object-boxing-demo/app.py create mode 100644 samples/python/object-boxing-demo/benchmark.py create mode 100644 samples/python/object-boxing-demo/generate.ps1 create mode 100644 samples/python/object-boxing-demo/run.ps1 create mode 100644 samples/python/object-boxing-demo/typing_check.py diff --git a/bindings/py/tests/object_values_scenarios.py b/bindings/py/tests/object_values_scenarios.py new file mode 100644 index 00000000..194d2663 --- /dev/null +++ b/bindings/py/tests/object_values_scenarios.py @@ -0,0 +1,299 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +"""Scenarios for generated bindings whose WinRT ``Object`` positions convert +through ``to_winrt_object`` / ``from_winrt_object``. + +Run by ``test_object_values_generated.py``, one scenario per fresh process: +the metadata table is process-global, and other test modules register partial +layouts of the same Windows interfaces. Usage:: + + python object_values_scenarios.py +""" + +import importlib +import sys +import tempfile +import threading +from datetime import datetime, timedelta, timezone +from pathlib import Path +from uuid import UUID + +import dynwinrt +from dynwinrt import ( + DynWinRTType, + DynWinRTValue, + WinGUID, + from_winrt_object, + project_as, + ro_initialize, + to_winrt_object, + unbox_object, +) + +CLASSES = ",".join( + [ + "Windows.Foundation.Collections.PropertySet", + "Windows.Foundation.Collections.ValueSet", + "Windows.Foundation.Collections.StringMap", + "Windows.Foundation.PropertyValue", + "Windows.Foundation.IPropertyValue", + "Windows.Foundation.Uri", + "Windows.Devices.Enumeration.DeviceInformation", + "Windows.Storage.StorageFile", + ] +) +IREFERENCE_PIID = WinGUID.parse("61c17706-2d65-11e0-9ae8-d48564015472") +UTC = timezone.utc + + +class Skip(Exception): + """The scenario's prerequisite is unavailable on this machine.""" + + +def _import(sdk, module): + return importlib.import_module(f"{sdk.__name__}.{module}") + + +def _raw_lookup(properties, key): + """IMap.Lookup without the Object conversion (vtable 6).""" + return properties._collection_obj.call_1( + 6, DynWinRTType.object(), DynWinRTValue.from_hstring(key) + ) + + +def _stored_type(foundation, properties, key): + return foundation.IPropertyValue.from_value(_raw_lookup(properties, key)).type + + +def scenario_property_value_factories_round_trip_every_property_type(foundation, collections): + """Write with PropertyValue.create_X, read, write back: the type is still X.""" + pv, kinds = foundation.PropertyValue, foundation.PropertyType + point, size, rect = foundation.Point, foundation.Size, foundation.Rect + moment = datetime(2024, 5, 6, 7, 8, 9, 123456, tzinfo=UTC) + cases = [ + ("create_uint8", 200, "UInt8"), + ("create_int16", -3, "Int16"), + ("create_uint16", 65535, "UInt16"), + ("create_int32", -7, "Int32"), + ("create_uint32", 2**32 - 1, "UInt32"), + ("create_int64", -(2**63), "Int64"), + ("create_uint64", 2**64 - 1, "UInt64"), + ("create_single", 0.5, "Single"), + ("create_double", 0.1, "Double"), + ("create_char16", "x", "Char16"), + ("create_boolean", True, "Boolean"), + ("create_string", "text", "String"), + ("create_guid", UUID(int=5), "Guid"), + ("create_date_time", moment, "DateTime"), + ("create_time_span", timedelta(seconds=-5), "TimeSpan"), + ("create_point", point(1.5, 2.5), "Point"), + ("create_size", size(3.0, 4.0), "Size"), + ("create_rect", rect(1.0, 2.0, 3.0, 4.0), "Rect"), + ("create_uint8_array", b"\x01\x02", "UInt8Array"), + ("create_int16_array", [1, -2], "Int16Array"), + ("create_uint16_array", [1, 2], "UInt16Array"), + ("create_int32_array", [1, -2], "Int32Array"), + ("create_uint32_array", [1, 2], "UInt32Array"), + ("create_int64_array", [1, -2], "Int64Array"), + ("create_uint64_array", [1, 2], "UInt64Array"), + ("create_single_array", [0.5, 1.5], "SingleArray"), + ("create_double_array", [0.1, 2.0], "DoubleArray"), + ("create_char16_array", ["a", "b"], "Char16Array"), + ("create_boolean_array", [True, False], "BooleanArray"), + ("create_string_array", ["a", ""], "StringArray"), + ("create_inspectable_array", [None, "nested"], "InspectableArray"), + ("create_guid_array", [UUID(int=1)], "GuidArray"), + ("create_date_time_array", [moment], "DateTimeArray"), + ("create_time_span_array", [timedelta(1)], "TimeSpanArray"), + ("create_point_array", [point(1.0, 2.0)], "PointArray"), + ("create_size_array", [size(1.0, 2.0)], "SizeArray"), + ("create_rect_array", [rect(1.0, 2.0, 3.0, 4.0)], "RectArray"), + ] + first, second = collections.PropertySet(), collections.PropertySet() + for factory, argument, expected in cases: + value = getattr(pv, factory)(argument) + assert not isinstance(value, DynWinRTValue), factory + first[factory] = value + assert _stored_type(foundation, first, factory) == kinds[expected], factory + read = first[factory] + assert type(read) is type(value) and read == value, (factory, read, value) + second[factory] = read + assert _stored_type(foundation, second, factory) == kinds[expected], factory + assert second[factory] == value + + +def scenario_plain_python_values_write_and_read_back(foundation, collections): + kinds = foundation.PropertyType + values = { + "bool": (True, "Boolean"), + "int": (5, "Int32"), + "big": (2**40, "Int64"), + "huge": (2**63, "UInt64"), + "float": (1.5, "Double"), + "str": ("text", "String"), + "uuid": (UUID(int=7), "Guid"), + "when": (datetime(2024, 1, 2, tzinfo=UTC), "DateTime"), + "span": (timedelta(minutes=3), "TimeSpan"), + "bytes": (b"\x00\xff", "UInt8Array"), + "ints": ([1, 2, 3], "Int32Array"), + "floats": ([1, 2.5], "DoubleArray"), + "strings": (["a", "b"], "StringArray"), + "mixed": ([1, "a", None], "InspectableArray"), + "point": (dynwinrt.Point(1, 2), "Point"), + "tagged": (dynwinrt.UInt16(7), "UInt16"), + } + properties = collections.PropertySet() + properties.update({key: value for key, (value, _) in values.items()}) + for key, (value, kind) in values.items(): + assert _stored_type(foundation, properties, key) == kinds[kind], key + assert properties[key] == value, key + properties["none"] = None + assert properties["none"] is None and _raw_lookup(properties, "none").is_null() + + +def scenario_property_set_value_set_and_string_map_behave_like_dicts(foundation, collections): + properties = collections.PropertySet() + properties["a"] = 1 + properties.update({"b": "two", "c": [1.5, 2.5]}) + expected = {"a": 1, "b": "two", "c": [1.5, 2.5]} + assert dict(properties) == expected and properties == expected + assert sorted(properties) == ["a", "b", "c"] and len(properties) == 3 + assert properties.get("a") == 1 and properties.get("missing") is None + assert properties.get("missing", "default") == "default" + assert "a" in properties and "missing" not in properties + assert sorted(properties.values(), key=repr) == sorted(expected.values(), key=repr) + assert dict(properties.items()) == expected + assert properties.setdefault("z", 3) == 3 and properties["z"] == 3 + assert properties.pop("b") == "two" and "b" not in properties + del properties["a"] + assert set(properties) == {"c", "z"} + + values = collections.ValueSet() + values.update(properties) + assert dict(values) == dict(properties) + values["tagged"] = dynwinrt.UInt32(9) + assert repr(values["tagged"]) == "dynwinrt.UInt32(9)" + try: + values["object"] = foundation.Uri("https://example.com/") # ValueSet stores values only + except OSError: + pass + else: + raise AssertionError("ValueSet accepted a non-value object") + + strings = collections.StringMap() + strings["k"] = "v" + strings.update({"x": "y"}) + assert dict(strings) == {"k": "v", "x": "y"} and strings.get("k") == "v" + + +def scenario_objects_keep_identity_and_raw_values_stay_compatible(foundation, collections): + uri = foundation.Uri("https://example.com/path") + properties = collections.PropertySet() + properties["uri"] = uri + raw = properties["uri"] + assert isinstance(raw, DynWinRTValue) + assert raw.identity_raw() == uri._obj.identity_raw() + assert project_as(raw, foundation.Uri).host == "example.com" + assert from_winrt_object(raw) is raw and unbox_object(raw) is raw + properties["raw"] = raw + assert properties["raw"].identity_raw() == uri._obj.identity_raw() + properties["legacy"] = to_winrt_object(dynwinrt.Int64(3)) + assert type(properties["legacy"]) is dynwinrt.Int64 + assert unbox_object(properties["legacy"]) == 3 + + +def scenario_generated_geometry_structs_and_enums_box_like_csharp(foundation, collections): + properties = collections.PropertySet() + properties["point"] = foundation.Point(1.5, 2.5) + assert _stored_type(foundation, properties, "point") == foundation.PropertyType.Point + read = properties["point"] + assert read == dynwinrt.Point(1.5, 2.5) + assert read == foundation.Point(1.5, 2.5) and foundation.Point(1.5, 2.5) == read + + properties["kind"] = foundation.PropertyType.UInt32 + raw = properties["kind"] + assert isinstance(raw, DynWinRTValue) # IReference, not an IPropertyValue + enum_type = DynWinRTType.enum_type("Windows.Foundation.PropertyType") + iid = DynWinRTType.parameterized(IREFERENCE_PIID, [enum_type]).iid() + assert raw.cast(iid).call_0(6, enum_type).get_enum_int() == 5 + + +def scenario_device_information_properties_unbox_to_python_values(sdk): + enumeration = _import(sdk, "windows.devices.enumeration") + devices = enumeration.DeviceInformation.find_all_async().wait() + named = [device for device in devices if "System.ItemNameDisplay" in device.properties] + if not named: + raise Skip("no device exposes System.ItemNameDisplay") + assert all(isinstance(device.properties["System.ItemNameDisplay"], str) for device in named) + values = dict(named[0].properties) + # Boxed property values arrive as Python values; only objects stay native. + assert any(not isinstance(value, DynWinRTValue) for value in values.values()) + + +def scenario_storage_file_properties_unbox_uint64_size_and_datetime(sdk, foundation, collections): + storage = _import(sdk, "windows.storage") + with tempfile.TemporaryDirectory() as directory: + path = Path(directory) / "object-values.txt" + path.write_bytes(b"hello world") + file = storage.StorageFile.get_file_from_path_async(str(path)).wait() + values = file.properties.retrieve_properties_async( + ["System.Size", "System.DateModified"] + ).wait() + size, modified = values["System.Size"], values["System.DateModified"] + assert type(size) is dynwinrt.UInt64 and size == 11 + assert isinstance(modified, datetime) and modified.tzinfo is not None + assert abs(datetime.now(UTC) - modified) < timedelta(hours=1) + copy = collections.PropertySet() + copy.update(values) + assert _stored_type(foundation, copy, "System.Size") == foundation.PropertyType.UInt64 + assert _stored_type(foundation, copy, "System.DateModified") == ( + foundation.PropertyType.DateTime + ) + del file, values + + +def scenario_event_arguments_typed_object_use_the_conversion(sdk): + enumeration = _import(sdk, "windows.devices.enumeration") + watcher = enumeration.DeviceInformation.create_watcher() + completed = threading.Event() + arguments = [] + + def on_completed(sender, args): + arguments.append(args) + completed.set() + + unsubscribers = [ + watcher.subscribe_added(lambda sender, info: None), + watcher.subscribe_updated(lambda sender, update: None), + watcher.subscribe_enumeration_completed(on_completed), + ] + watcher.start() + try: + assert completed.wait(60), "EnumerationCompleted was not raised" + finally: + watcher.stop() + for unsubscribe in unsubscribers: + unsubscribe() + assert arguments[:1] == [None] + + +def main(root, package, scenario): + sys.path.insert(0, root) + ro_initialize(1) + sdk = importlib.import_module(package) + foundation = _import(sdk, "windows.foundation") + collections = _import(sdk, "windows.foundation.collections") + function = globals()[f"scenario_{scenario}"] + arguments = {"sdk": sdk, "foundation": foundation, "collections": collections} + names = function.__code__.co_varnames[: function.__code__.co_argcount] + try: + function(*(arguments[name] for name in names)) + except Skip as reason: + print(f"object-values-skip: {reason}", flush=True) + return + print(f"object-values-ok: {scenario}", flush=True) + + +if __name__ == "__main__": + main(*sys.argv[1:]) diff --git a/bindings/py/tests/test_object_values_generated.py b/bindings/py/tests/test_object_values_generated.py new file mode 100644 index 00000000..1b2111bf --- /dev/null +++ b/bindings/py/tests/test_object_values_generated.py @@ -0,0 +1,70 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +"""Generated bindings route every WinRT ``Object`` position through one layer. + +Generates Windows SDK bindings with ``dynwinrt-codegen`` (``DYNWINRT_CODEGEN`` +or the local ``target`` directory) and runs each scenario in +``object_values_scenarios.py`` in a fresh process: collections, factories, +real property stores, and event arguments typed ``Object``. +""" + +import os +import re +import subprocess +import sys +from pathlib import Path + +import pytest + +TESTS = Path(__file__).resolve().parent +ROOT = TESTS.parents[2] +SCENARIOS = TESTS / "object_values_scenarios.py" +PACKAGE = "object_values_sdk" +NAMES = re.findall(r"^def scenario_(\w+)", SCENARIOS.read_text(encoding="utf-8"), re.M) + + +def _codegen(): + configured = os.environ.get("DYNWINRT_CODEGEN") + candidates = ( + [Path(configured)] + if configured + else [ROOT / "target" / profile / "dynwinrt-codegen.exe" for profile in ("debug", "release")] + ) + return next((path for path in candidates if path.is_file()), None) + + +@pytest.fixture(scope="module") +def generated(tmp_path_factory): + codegen = _codegen() + if codegen is None: + pytest.skip("dynwinrt-codegen is unavailable; set DYNWINRT_CODEGEN") + sys.path.insert(0, str(TESTS)) + try: + from object_values_scenarios import CLASSES + finally: + sys.path.remove(str(TESTS)) + root = tmp_path_factory.mktemp("object_values") + result = subprocess.run( + [str(codegen), "generate", "--class-name", CLASSES, "--lang", "py", + "--output", str(root / PACKAGE)], + capture_output=True, text=True, timeout=900, + ) + assert result.returncode == 0, result.stdout + result.stderr + return root + + +@pytest.mark.parametrize("scenario", NAMES) +def test_generated_object_positions(generated, scenario): + result = subprocess.run( + [sys.executable, "-B", str(SCENARIOS), str(generated), PACKAGE, scenario], + capture_output=True, text=True, timeout=300, + ) + assert result.returncode == 0, result.stdout + result.stderr + if "object-values-skip:" in result.stdout: + pytest.skip(result.stdout.split("object-values-skip:", 1)[1].strip()) + assert f"object-values-ok: {scenario}" in result.stdout, result.stdout + result.stderr + + +def test_every_scenario_is_collected(): + assert len(NAMES) == 8 diff --git a/samples/python/object-boxing-demo/.gitignore b/samples/python/object-boxing-demo/.gitignore new file mode 100644 index 00000000..7809e3e2 --- /dev/null +++ b/samples/python/object-boxing-demo/.gitignore @@ -0,0 +1,3 @@ +generated/ +.mypy_cache/ +__pycache__/ diff --git a/samples/python/object-boxing-demo/README.md b/samples/python/object-boxing-demo/README.md new file mode 100644 index 00000000..eb909012 --- /dev/null +++ b/samples/python/object-boxing-demo/README.md @@ -0,0 +1,34 @@ +# Python Object boxing demo (experimental) + +This sample demonstrates the experimental automatic boxing and unboxing of WinRT +`Object` (`IInspectable`) values in the Python projection. Every `Object` +position converts through `dynwinrt.to_winrt_object` and +`dynwinrt.from_winrt_object`, so this natural code works: + +```python +settings = PropertySet() +settings["retries"] = 3 # boxed as Int32 +settings["tags"] = ["a", "b"] # boxed as StringArray +settings["port"] = UInt32(8080) # boxed as UInt32 +assert settings["retries"] + 1 == 4 # read back as a plain int + +size = file_properties["System.Size"] # dynwinrt.UInt64(11) +copy["System.Size"] = size # written back as UInt64 +``` + +The demo covers a `PropertySet` with plain values, a lossless read-then-write +round trip for types that need a tag (`UInt8`, `Int64`, `Char16`, `Single`, +`Int16Array`), the `StorageFile` property store (`System.Size` as +`dynwinrt.UInt64`, `System.DateModified` as `datetime`), the +`DeviceInformation` property store, and objects that keep their identity and +`project_as()` support. + +```powershell +.\generate.ps1 -Codegen ..\..\..\target\release\dynwinrt-codegen.exe +.\run.ps1 -Python C:\path\to\python.exe +.\run.ps1 -Python C:\path\to\python.exe -Benchmark +``` + +A successful run prints `python-object-boxing-demo-ok`. `-Benchmark` times +single `Object` reads and writes and reading a 1000-entry `PropertySet`. +`typing_check.py` is a small consumer for `mypy --strict` and `pyright`. diff --git a/samples/python/object-boxing-demo/app.py b/samples/python/object-boxing-demo/app.py new file mode 100644 index 00000000..ed1ec687 --- /dev/null +++ b/samples/python/object-boxing-demo/app.py @@ -0,0 +1,113 @@ +"""Automatic boxing and unboxing of WinRT Object values (experimental). + +Every metadata position typed Object (IInspectable) converts through +dynwinrt.to_winrt_object / dynwinrt.from_winrt_object, so property bags, +property stores and XAML-style Object properties read and write plain Python +values while keeping the exact WinRT type on a round trip. +""" + +import tempfile +from datetime import datetime, timezone +from pathlib import Path + +import dynwinrt +from dynwinrt import RoApartment, UInt32, WinRTObjectInput, projected_lifetime_scope +from generated.windows.devices.enumeration import DeviceInformation +from generated.windows.foundation import IPropertyValue, Point, PropertyValue, Uri +from generated.windows.foundation.collections import PropertySet +from generated.windows.storage import StorageFile + + +def show(label: str, value: object) -> None: + print(f" {label:<26} {value!r}") + + +def boxed_type(value: WinRTObjectInput | None) -> str: + """The WinRT PropertyType a Python value boxes as.""" + return IPropertyValue.from_value(dynwinrt.to_winrt_object(value)).type.name + + +def property_bag() -> None: + print("PropertySet with plain Python values:") + settings = PropertySet() + settings["retries"] = 3 + settings["ratio"] = 0.75 + settings["name"] = "demo" + settings["enabled"] = True + settings["tags"] = ["a", "b"] + settings["when"] = datetime(2024, 1, 2, tzinfo=timezone.utc) + settings["origin"] = Point(1.5, 2.5) + settings["port"] = UInt32(8080) + settings["missing"] = None + for key in sorted(settings): + show(key, settings[key]) + retries = settings["retries"] + assert isinstance(retries, int) and retries + 1 == 4 + assert settings["tags"] == ["a", "b"] and settings["origin"] == Point(1.5, 2.5) + + +def lossless_round_trip() -> None: + print("Read-then-write keeps the exact WinRT type:") + source, copy = PropertySet(), PropertySet() + source["byte"] = PropertyValue.create_uint8(200) + source["wide"] = PropertyValue.create_int64(5) + source["char"] = PropertyValue.create_char16("x") + source["single"] = PropertyValue.create_single(0.5) + source["shorts"] = PropertyValue.create_int16_array([1, 2]) + for key, value in source.items(): + copy[key] = value + print(f" {key:<26} {copy[key]!r} boxes as {boxed_type(copy[key])}") + assert boxed_type(copy["wide"]) == "Int64" + + +def file_properties() -> None: + print("StorageFile property store:") + with tempfile.TemporaryDirectory() as directory: + path = Path(directory) / "demo.txt" + path.write_bytes(b"hello world") + file = StorageFile.get_file_from_path_async(str(path)).wait() + store = None if file is None else file.properties + if store is None: + raise RuntimeError("StorageFile returned no property store") + values = store.retrieve_properties_async(["System.Size", "System.DateModified"]).wait() + if values is None: + raise RuntimeError("RetrievePropertiesAsync returned no values") + size = values["System.Size"] + show("System.Size", size) + show("System.DateModified", values["System.DateModified"]) + show("size + 1", size + 1 if isinstance(size, int) else None) + del file, store, values + + +def device_names() -> None: + print("DeviceInformation property store:") + for device in DeviceInformation.find_all_async().wait() or (): + properties = None if device is None else device.properties + name = None if properties is None else properties.get("System.ItemNameDisplay") + if isinstance(name, str): + show("System.ItemNameDisplay", name) + return + print(" no device exposes System.ItemNameDisplay") + + +def objects_keep_identity() -> None: + print("Objects that are not boxed values stay native:") + bag = PropertySet() + bag["uri"] = Uri("https://example.com/demo") + raw = bag["uri"] + show("bag['uri']", raw) + show("project_as(raw, Uri).host", dynwinrt.project_as(raw, Uri).host) + + +def main() -> None: + with RoApartment(1), projected_lifetime_scope(): + property_bag() + lossless_round_trip() + file_properties() + device_names() + objects_keep_identity() + print("python-object-boxing-demo-ok") + + +if __name__ == "__main__": + main() diff --git a/samples/python/object-boxing-demo/benchmark.py b/samples/python/object-boxing-demo/benchmark.py new file mode 100644 index 00000000..f49fef7a --- /dev/null +++ b/samples/python/object-boxing-demo/benchmark.py @@ -0,0 +1,92 @@ +"""Measure the cost of the Object conversion layer. + +Runs against pre- and post-boxing builds: values are written through +PropertyValue factories, which both builds accept, and the same reads are +timed. Plain-value writes are only timed when automatic boxing is available. +""" + +import statistics +import sys +import timeit +from collections.abc import Callable +from functools import partial +from datetime import datetime, timezone +from typing import TYPE_CHECKING + +import dynwinrt +from dynwinrt import DynWinRTValue, RoApartment +from generated.windows.foundation import PropertyValue, Uri +from generated.windows.foundation.collections import PropertySet + +if TYPE_CHECKING: + from dynwinrt import WinRTObjectValue + +ENTRIES = 1000 +REPEAT = 15 + + +def measure(label: str, statement: Callable[[], object], number: int) -> None: + samples = timeit.repeat(statement, repeat=REPEAT, number=number) + per_call = [sample / number * 1e6 for sample in samples] + print(f" {label:<52} median {statistics.median(per_call):9.2f} us" + f" min {min(per_call):9.2f} us") + + +def mixed_value(index: int) -> "WinRTObjectValue | None": + kind = index % 4 + if kind == 0: + return PropertyValue.create_int32(index) + if kind == 1: + return PropertyValue.create_string(str(index)) + if kind == 2: + return PropertyValue.create_uint32(index) + return PropertyValue.create_double(index) + + +def main() -> None: + with RoApartment(1): + large = PropertySet() + mixed = PropertySet() + for index in range(ENTRIES): + large[f"key{index:04}"] = PropertyValue.create_int32(index) + mixed[f"key{index:04}"] = mixed_value(index) + single = PropertySet() + single["int32"] = PropertyValue.create_int32(42) + single["string"] = PropertyValue.create_string("forty-two") + single["uint32"] = PropertyValue.create_uint32(42) + single["datetime"] = PropertyValue.create_date_time( + datetime(2024, 1, 2, tzinfo=timezone.utc) + ) + single["object"] = Uri("https://example.com/") + automatic = not isinstance(single["int32"], DynWinRTValue) + print(f"python {sys.version.split()[0]}; automatic boxing: {automatic}") + + print("single Object read (IMap.Lookup through the generated wrapper):") + for key in ("int32", "string", "uint32", "datetime", "object"): + measure(f"properties.lookup({key!r})", partial(single.lookup, key), 20000) + measure("properties['int32'] (HasKey + Lookup)", lambda: single["int32"], 20000) + # The pre-boxing way to get a Python value: an explicit unbox per read. + unbox = dynwinrt.unbox_object + measure("unbox_object(properties.lookup('int32'))", + lambda: unbox(single.lookup("int32")), 20000) + + print(f"iterate and read a {ENTRIES}-entry PropertySet:") + measure("dict(properties) [Int32 values]", lambda: dict(large), 5) + measure("[v for _, v in properties.items()] [Int32]", + lambda: [value for _, value in large.items()], 5) + measure("dict(properties) [mixed Int32/String/UInt32/Double]", lambda: dict(mixed), 5) + measure("{k: unbox_object(v) for k, v in properties.items()}", + lambda: {key: unbox(value) for key, value in large.items()}, 5) + + print("single Object write:") + measure("properties['k'] = PropertyValue.create_int32(5)", + lambda: single.__setitem__("k", PropertyValue.create_int32(5)), 20000) + if automatic: + measure("properties['k'] = 5", lambda: single.__setitem__("k", 5), 20000) + measure("properties['k'] = dynwinrt.UInt32(5)", + lambda: single.__setitem__("k", dynwinrt.UInt32(5)), 20000) + del large, mixed, single + + +if __name__ == "__main__": + main() diff --git a/samples/python/object-boxing-demo/generate.ps1 b/samples/python/object-boxing-demo/generate.ps1 new file mode 100644 index 00000000..9687a2c6 --- /dev/null +++ b/samples/python/object-boxing-demo/generate.ps1 @@ -0,0 +1,64 @@ +#!/usr/bin/env pwsh + +param( + [string]$Winmd, + [string]$Codegen = "dynwinrt-codegen" +) + +$ErrorActionPreference = "Stop" +$caller = (Get-Location).Path + +function Resolve-CallerPath([string]$Path) { + if ([System.IO.Path]::IsPathRooted($Path)) { + return [System.IO.Path]::GetFullPath($Path) + } + return [System.IO.Path]::GetFullPath((Join-Path $caller $Path)) +} + +if ($Winmd) { + $Winmd = Resolve-CallerPath $Winmd +} else { + $Winmd = Get-ChildItem ` + "C:\Program Files (x86)\Windows Kits\10\UnionMetadata" ` + -Filter Windows.winmd ` + -Recurse ` + -ErrorAction SilentlyContinue | + Sort-Object FullName -Descending | + Select-Object -First 1 -ExpandProperty FullName +} +if (-not $Winmd -or -not (Test-Path -LiteralPath $Winmd -PathType Leaf)) { + throw "Windows.winmd was not found. Install a Windows 10/11 SDK or pass -Winmd." +} + +$codegenCandidate = Resolve-CallerPath $Codegen +if (Test-Path -LiteralPath $codegenCandidate -PathType Leaf) { + $codegenCommand = $codegenCandidate +} else { + $codegenCommand = (Get-Command $Codegen -ErrorAction Stop).Source +} + +$output = Join-Path $PSScriptRoot "generated" +if (Test-Path -LiteralPath $output) { + Remove-Item -LiteralPath $output -Recurse -Force +} + +$classes = @( + "Windows.Foundation.Collections.PropertySet", + "Windows.Foundation.Collections.ValueSet", + "Windows.Foundation.PropertyValue", + "Windows.Foundation.IPropertyValue", + "Windows.Foundation.Uri", + "Windows.Devices.Enumeration.DeviceInformation", + "Windows.Storage.StorageFile" +) -join "," + +& $codegenCommand generate ` + --winmd $Winmd ` + --class-name $classes ` + --lang py ` + --output $output +if ($LASTEXITCODE -ne 0) { + throw "dynwinrt-codegen failed with exit code $LASTEXITCODE" +} + +Write-Host "Generated Object boxing demo bindings in $output" diff --git a/samples/python/object-boxing-demo/run.ps1 b/samples/python/object-boxing-demo/run.ps1 new file mode 100644 index 00000000..6b914d2e --- /dev/null +++ b/samples/python/object-boxing-demo/run.ps1 @@ -0,0 +1,23 @@ +#!/usr/bin/env pwsh + +param( + [string]$Python = "python", + [switch]$Benchmark +) + +$ErrorActionPreference = "Stop" +if (-not (Test-Path -LiteralPath (Join-Path $PSScriptRoot "generated\__init__.py"))) { + throw "Generated bindings were not found. Run generate.ps1 first." +} +if (-not [System.IO.Path]::IsPathRooted($Python)) { + $callerPython = Join-Path (Get-Location).Path $Python + if (Test-Path -LiteralPath $callerPython -PathType Leaf) { + $Python = [System.IO.Path]::GetFullPath($callerPython) + } +} + +$script = if ($Benchmark) { "benchmark.py" } else { "app.py" } +& $Python (Join-Path $PSScriptRoot $script) +if ($LASTEXITCODE -ne 0) { + exit $LASTEXITCODE +} diff --git a/samples/python/object-boxing-demo/typing_check.py b/samples/python/object-boxing-demo/typing_check.py new file mode 100644 index 00000000..6defa776 --- /dev/null +++ b/samples/python/object-boxing-demo/typing_check.py @@ -0,0 +1,47 @@ +"""Static typing of WinRT Object positions. + +Check with ``mypy --strict typing_check.py`` and ``pyright typing_check.py`` +after running generate.ps1. +""" + +from datetime import datetime, timezone +from typing import assert_type + +import dynwinrt +from dynwinrt import DynWinRTValue, UInt32, WinRTObjectInput, WinRTObjectValue +from generated.windows.foundation import Point, Uri +from generated.windows.foundation.collections import PropertySet + + +def write(properties: PropertySet, uri: Uri, raw: DynWinRTValue) -> None: + properties["count"] = 5 + properties["ratio"] = 0.5 + properties["name"] = "demo" + properties["tags"] = ["a", "b"] + properties["pair"] = (1, 2) + properties["when"] = datetime.now(timezone.utc) + properties["port"] = UInt32(8080) + properties["origin"] = Point(1.0, 2.0) + properties["uri"] = uri + properties["raw"] = raw + properties["missing"] = None + properties.insert("inserted", b"bytes") + properties.update({"a": 1, "b": "two"}) + + +def read(properties: PropertySet) -> int: + value = properties["count"] + assert_type(value, WinRTObjectValue | None) + if isinstance(value, bool): + return int(value) + if isinstance(value, int): + return value + 1 + if isinstance(value, DynWinRTValue): + return 0 + return -1 + + +def convert(value: WinRTObjectInput) -> WinRTObjectValue | None: + boxed = dynwinrt.to_winrt_object(value) + assert_type(boxed, DynWinRTValue) + return dynwinrt.from_winrt_object(boxed)