diff --git a/README.md b/README.md index 7e057ce..aa4b834 100644 --- a/README.md +++ b/README.md @@ -219,6 +219,7 @@ for special primitives from the [`typing`](https://docs.python.org/3/library/typ * [`Final`](https://docs.python.org/3/library/typing.html#typing.Final) * [`Self`](https://docs.python.org/3/library/typing.html#typing.Self) * [`Unpack`](https://docs.python.org/3/library/typing.html#typing.Unpack) +* [`ReadOnly`](https://docs.python.org/3/library/typing.html#typing.ReadOnly) for standard interpreter types from [`types`](https://docs.python.org/3/library/types.html#standard-interpreter-types) module: * [`NoneType`](https://docs.python.org/3/library/types.html#types.NoneType) @@ -279,6 +280,7 @@ for backported types from [`typing-extensions`](https://github.com/python/typing * [`Self`](https://docs.python.org/3/library/typing.html#typing.Self) * [`TypeVarTuple`](https://docs.python.org/3/library/typing.html#typing.TypeVarTuple) * [`Unpack`](https://docs.python.org/3/library/typing.html#typing.Unpack) +* [`ReadOnly`](https://docs.python.org/3/library/typing.html#typing.ReadOnly) for arbitrary types: * [user-defined types](#user-defined-types) diff --git a/mashumaro/core/meta/helpers.py b/mashumaro/core/meta/helpers.py index db7e386..d1aa018 100644 --- a/mashumaro/core/meta/helpers.py +++ b/mashumaro/core/meta/helpers.py @@ -325,6 +325,15 @@ def is_typed_dict(typ: Type) -> bool: return False +def is_readonly(typ: Type) -> bool: + origin = get_type_origin(typ) + for module in (typing, typing_extensions): + with suppress(AttributeError): + if origin is getattr(module, "ReadOnly"): + return True + return False + + def is_named_tuple(typ: Type) -> bool: try: return issubclass(typ, tuple) and hasattr(typ, "_fields") diff --git a/mashumaro/core/meta/types/pack.py b/mashumaro/core/meta/types/pack.py index 07aad53..d95ae0f 100644 --- a/mashumaro/core/meta/types/pack.py +++ b/mashumaro/core/meta/types/pack.py @@ -33,6 +33,7 @@ is_new_type, is_not_required, is_optional, + is_readonly, is_required, is_self, is_special_typing_primitive, @@ -545,6 +546,8 @@ def pack_special_typing_primitive(spec: ValueSpec) -> Optional[Expression]: return PackerRegistry.get(spec.copy(type=evaluated)) elif is_type_alias_type(spec.type): return PackerRegistry.get(spec.copy(type=spec.type.__value__)) + elif is_readonly(spec.type): + return PackerRegistry.get(spec.copy(type=get_args(spec.type)[0])) raise UnserializableDataError( f"{spec.type} as a field type is not supported by mashumaro" ) diff --git a/mashumaro/core/meta/types/unpack.py b/mashumaro/core/meta/types/unpack.py index b04f6b2..58d0ab7 100644 --- a/mashumaro/core/meta/types/unpack.py +++ b/mashumaro/core/meta/types/unpack.py @@ -44,6 +44,7 @@ is_new_type, is_not_required, is_optional, + is_readonly, is_required, is_self, is_special_typing_primitive, @@ -873,6 +874,8 @@ def unpack_special_typing_primitive(spec: ValueSpec) -> Optional[Expression]: return UnpackerRegistry.get(spec.copy(type=evaluated)) elif is_type_alias_type(spec.type): return UnpackerRegistry.get(spec.copy(type=spec.type.__value__)) + elif is_readonly(spec.type): + return UnpackerRegistry.get(spec.copy(type=get_args(spec.type)[0])) raise UnserializableDataError( f"{spec.type} as a field type is not supported by mashumaro" ) diff --git a/mashumaro/jsonschema/schema.py b/mashumaro/jsonschema/schema.py index dfe6bde..54bd1e3 100644 --- a/mashumaro/jsonschema/schema.py +++ b/mashumaro/jsonschema/schema.py @@ -40,6 +40,7 @@ is_named_tuple, is_new_type, is_not_required, + is_readonly, is_required, is_special_typing_primitive, is_type_var, @@ -457,6 +458,8 @@ def on_special_typing_primitive( ) elif is_type_var_tuple(instance.type): return get_schema(instance.derive(type=tuple[Any, ...]), ctx) + elif is_readonly(instance.type): + return get_schema(instance.derive(type=args[0]), ctx) elif isinstance(instance.type, ForwardRef): evaluated = evaluate_forward_ref( instance.type, diff --git a/tests/entities.py b/tests/entities.py index 98e9199..fcb5af2 100644 --- a/tests/entities.py +++ b/tests/entities.py @@ -13,7 +13,7 @@ class StrEnum(str, Enum): pass -from typing_extensions import NamedTuple, TypedDict, TypeVar +from typing_extensions import NamedTuple, ReadOnly, TypedDict, TypeVar from mashumaro import DataClassDictMixin from mashumaro.config import TO_DICT_ADD_OMIT_NONE_FLAG, BaseConfig @@ -254,6 +254,10 @@ class TypedDictOptionalKeysWithOptional(TypedDict, total=False): y: float +class TypedDictWithReadOnly(TypedDict): + x: ReadOnly[int] + + class GenericTypedDict(TypedDict, Generic[T]): x: T y: int diff --git a/tests/test_data_types.py b/tests/test_data_types.py index 7e6cf69..d4cc108 100644 --- a/tests/test_data_types.py +++ b/tests/test_data_types.py @@ -61,7 +61,11 @@ SerializableType, SerializationStrategy, ) -from tests.entities import MyUntypedNamedTupleWithDefaults, TDefaultInt +from tests.entities import ( + MyUntypedNamedTupleWithDefaults, + TDefaultInt, + TypedDictWithReadOnly, +) from .conftest import add_unpack_method from .entities import ( @@ -1197,6 +1201,15 @@ class DataClass(DataClassDictMixin): } +def test_dataclass_with_typed_dict_with_read_only_key(): + @dataclass + class DataClass(DataClassDictMixin): + x: TypedDictWithReadOnly + + assert DataClass.from_dict({"x": {"x": "42"}}) == DataClass({"x": 42}) + assert DataClass({"x": 42}).to_dict() == {"x": {"x": 42}} + + def test_dataclass_with_named_tuple(): @dataclass class DataClass(DataClassDictMixin): diff --git a/tests/test_jsonschema/test_jsonschema_generation.py b/tests/test_jsonschema/test_jsonschema_generation.py index 9ffd4a0..eb961f9 100644 --- a/tests/test_jsonschema/test_jsonschema_generation.py +++ b/tests/test_jsonschema/test_jsonschema_generation.py @@ -107,6 +107,7 @@ TypedDictRequiredAndOptionalKeys, TypedDictRequiredKeys, TypedDictRequiredKeysWithOptional, + TypedDictWithReadOnly, ) from tests.test_pep_655 import ( TypedDictCorrectNotRequired, @@ -721,6 +722,11 @@ def test_jsonschema_for_typeddict(): additionalProperties=False, required=["required"], ) + assert build_json_schema(TypedDictWithReadOnly) == JSONObjectSchema( + properties={"x": JSONSchema(type=JSONSchemaInstanceType.INTEGER)}, + additionalProperties=False, + required=["x"], + ) def test_jsonschema_for_mapping():