Skip to content

Commit

Permalink
Add support for ReadOnly from PEP 705
Browse files Browse the repository at this point in the history
  • Loading branch information
Fatal1ty committed Dec 21, 2024
1 parent 19d4642 commit 9c5e5e4
Show file tree
Hide file tree
Showing 8 changed files with 45 additions and 2 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
9 changes: 9 additions & 0 deletions mashumaro/core/meta/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
3 changes: 3 additions & 0 deletions mashumaro/core/meta/types/pack.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
is_new_type,
is_not_required,
is_optional,
is_readonly,
is_required,
is_self,
is_special_typing_primitive,
Expand Down Expand Up @@ -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"
)
Expand Down
3 changes: 3 additions & 0 deletions mashumaro/core/meta/types/unpack.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
is_new_type,
is_not_required,
is_optional,
is_readonly,
is_required,
is_self,
is_special_typing_primitive,
Expand Down Expand Up @@ -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"
)
Expand Down
3 changes: 3 additions & 0 deletions mashumaro/jsonschema/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
is_named_tuple,
is_new_type,
is_not_required,
is_readonly,
is_required,
is_special_typing_primitive,
is_type_var,
Expand Down Expand Up @@ -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,
Expand Down
6 changes: 5 additions & 1 deletion tests/entities.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
15 changes: 14 additions & 1 deletion tests/test_data_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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):
Expand Down
6 changes: 6 additions & 0 deletions tests/test_jsonschema/test_jsonschema_generation.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@
TypedDictRequiredAndOptionalKeys,
TypedDictRequiredKeys,
TypedDictRequiredKeysWithOptional,
TypedDictWithReadOnly,
)
from tests.test_pep_655 import (
TypedDictCorrectNotRequired,
Expand Down Expand Up @@ -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():
Expand Down

0 comments on commit 9c5e5e4

Please sign in to comment.