Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for ReadOnly from PEP 705 #272

Merged
merged 1 commit into from
Dec 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading