diff --git a/packages/smithy-core/src/smithy_core/interfaces/__init__.py b/packages/smithy-core/src/smithy_core/interfaces/__init__.py index 1311b732b..1dc427fae 100644 --- a/packages/smithy-core/src/smithy_core/interfaces/__init__.py +++ b/packages/smithy-core/src/smithy_core/interfaces/__init__.py @@ -118,6 +118,23 @@ class PropertyKey[T](Protocol): Used with :py:class:`Context` to set and get typed values. For a concrete implementation, see :py:class:`smithy_core.types.PropertyKey`. + + Note that unions and other special types cannot easily be used here due to being + incompatible with ``type[T]``. PEP747 proposes a fix to this case, but it has not + yet been accepted. In the meantime, there is a workaround. The PropertyKey must + be assigned to an explicitly typed variable, and the ``value_type`` parameter of + the constructor must have a ``# type: ignore`` comment, like so: + + .. code-block:: python + + UNION_PROPERTY: PropertyKey[str | int] = PropertyKey( + key="union", + value_type=str | int # type: ignore + ) + + Type checkers will be able to use such a property as expected, and the + ``value_type`` property may still be used in ``isinstance`` checks since it also + supports union types as of Python 3.10. """ key: str @@ -151,11 +168,32 @@ class TypedProperties(Protocol): properties = TypedProperties() properties[foo] = "bar" - assert assert_type(properties[foo], str) == "bar - assert assert_type(properties["foo"], Any) == "bar - + assert assert_type(properties[foo], str) == "bar" + assert assert_type(properties["foo"], Any) == "bar" For a concrete implementation, see :py:class:`smithy_core.types.TypedProperties`. + + Note that unions and other special types cannot easily be used here due to being + incompatible with ``type[T]``. PEP747 proposes a fix to this case, but it has not + yet been accepted. In the meantime, there is a workaround. The PropertyKey must + be assigned to an explicitly typed variable, and the ``value_type`` parameter of + the constructor must have a ``# type: ignore`` comment, like so: + + .. code-block:: python + + UNION_PROPERTY: PropertyKey[str | int] = PropertyKey( + key="union", + value_type=str | int # type: ignore + ) + + properties = TypedProperties() + properties[UNION_PROPERTY] = "foo" + + assert assert_type(properties[UNION_PROPERTY], str | int) == "foo" + + Type checkers will be able to use such a property as expected, and the + ``value_type`` property may still be used in ``isinstance`` checks since it also + supports union types as of Python 3.10. """ @overload diff --git a/packages/smithy-core/src/smithy_core/types.py b/packages/smithy-core/src/smithy_core/types.py index 3b1613c37..6fe114e09 100644 --- a/packages/smithy-core/src/smithy_core/types.py +++ b/packages/smithy-core/src/smithy_core/types.py @@ -161,7 +161,25 @@ def format(self, *args: object, **kwargs: str) -> str: @dataclass(kw_only=True, frozen=True, slots=True, init=False) class PropertyKey[T](_PropertyKey[T]): - """A typed property key.""" + """A typed property key. + + Note that unions and other special types cannot easily be used here due to being + incompatible with ``type[T]``. PEP747 proposes a fix to this case, but it has not + yet been accepted. In the meantime, there is a workaround. The PropertyKey must + be assigned to an explicitly typed variable, and the ``value_type`` parameter of + the constructor must have a ``# type: ignore`` comment, like so: + + .. code-block:: python + + UNION_PROPERTY: PropertyKey[str | int] = PropertyKey( + key="union", + value_type=str | int # type: ignore + ) + + Type checkers will be able to use such a property as expected, and the + ``value_type`` property may still be used in ``isinstance`` checks since it also + supports union types as of Python 3.10. + """ key: str """The string key used to access the value.""" @@ -192,8 +210,30 @@ class TypedProperties(UserDict[str, Any], _TypedProperties): properties = TypedProperties() properties[foo] = "bar" - assert assert_type(properties[foo], str) == "bar - assert assert_type(properties["foo"], Any) == "bar + assert assert_type(properties[foo], str) == "bar" + assert assert_type(properties["foo"], Any) == "bar" + + Note that unions and other special types cannot easily be used here due to being + incompatible with ``type[T]``. PEP747 proposes a fix to this case, but it has not + yet been accepted. In the meantime, there is a workaround. The PropertyKey must + be assigned to an explicitly typed variable, and the ``value_type`` parameter of + the constructor must have a ``# type: ignore`` comment, like so: + + .. code-block:: python + + UNION_PROPERTY: PropertyKey[str | int] = PropertyKey( + key="union", + value_type=str | int # type: ignore + ) + + properties = TypedProperties() + properties[UNION_PROPERTY] = "foo" + + assert assert_type(properties[UNION_PROPERTY], str | int) == "foo" + + Type checkers will be able to use such a property as expected, and the + ``value_type`` property may still be used in ``isinstance`` checks since it also + supports union types as of Python 3.10. """ @overload diff --git a/packages/smithy-core/tests/unit/test_types.py b/packages/smithy-core/tests/unit/test_types.py index d36c8b06a..1bd9f2eec 100644 --- a/packages/smithy-core/tests/unit/test_types.py +++ b/packages/smithy-core/tests/unit/test_types.py @@ -323,3 +323,30 @@ def test_properties_typed_pop() -> None: assert "foo" not in properties.data assert properties.pop(foo_key) is None + + +def test_union_property() -> None: + properties = TypedProperties() + union: PropertyKey[str | int] = PropertyKey( + key="union", + value_type=str | int, # type: ignore + ) + + properties[union] = "foo" + assert assert_type(properties[union], str | int) == "foo" + assert assert_type(properties.get(union), str | int | None) == "foo" + assert assert_type(properties.get(union, b"foo"), str | int | bytes) == "foo" + assert assert_type(properties.pop(union), str | int | None) == "foo" + properties[union] = "foo" + assert assert_type(properties.pop(union, b"bar"), str | int | bytes) == "foo" + + properties[union] = 1 + assert assert_type(properties[union], str | int) == 1 + assert assert_type(properties.get(union), str | int | None) == 1 + assert assert_type(properties.get(union, b"foo"), str | int | bytes) == 1 + assert assert_type(properties.pop(union), str | int | None) == 1 + properties[union] = 1 + assert assert_type(properties.pop(union, b"bar"), str | int | bytes) == 1 + + with pytest.raises(ValueError): + properties[union] = b"bar" # type: ignore