diff --git a/mashumaro/core/meta/code/builder.py b/mashumaro/core/meta/code/builder.py index b68d112c..f5c36de6 100644 --- a/mashumaro/core/meta/code/builder.py +++ b/mashumaro/core/meta/code/builder.py @@ -1161,6 +1161,7 @@ def _get_field_packer( metadata=metadata, ), could_be_none=False, + no_copy=self._get_dialect_or_config_option("no_copy", False), ) ) return packer, alias, could_be_none diff --git a/mashumaro/core/meta/types/common.py b/mashumaro/core/meta/types/common.py index 3e33e38a..0c0e9498 100644 --- a/mashumaro/core/meta/types/common.py +++ b/mashumaro/core/meta/types/common.py @@ -78,6 +78,7 @@ class ValueSpec: could_be_none: bool = True annotated_type: Optional[Type] = None owner: Optional[Type] = None + no_copy: bool = False def __setattr__(self, key: str, value: Any) -> None: if key == "type": diff --git a/mashumaro/core/meta/types/pack.py b/mashumaro/core/meta/types/pack.py index 475b1aac..47eee503 100644 --- a/mashumaro/core/meta/types/pack.py +++ b/mashumaro/core/meta/types/pack.py @@ -673,6 +673,18 @@ def inner_expr( ) ) + def _check_sequence_pass_through(ie: Expression) -> bool: + return spec.no_copy and ie == "value" + + def _make_sequence_expression(ie: Expression) -> Expression: + return f"[{ie} for value in {spec.expression}]" + + def _check_mapping_pass_through(ke: Expression, ve: Expression) -> bool: + return spec.no_copy and ke == "key" and ve == "value" + + def _make_mapping_expression(ke: Expression, ve: Expression) -> Expression: + return f"{{{ke}: {ve} for key, value in {spec.expression}.items()}}" + if issubclass(spec.origin_type, typing.ByteString): # type: ignore spec.builder.ensure_object_imported(encodebytes) return f"encodebytes({spec.expression}).decode()" @@ -686,32 +698,44 @@ def inner_expr( elif ensure_generic_collection_subclass( spec, typing.List, typing.Deque, typing.AbstractSet ): - return f"[{inner_expr()} for value in {spec.expression}]" + ie = inner_expr() + if _check_sequence_pass_through(ie): + return spec.expression + return _make_sequence_expression(ie) elif ensure_generic_mapping(spec, args, typing.ChainMap): + ke = inner_expr(0, "key") + ve = inner_expr(1) + if _check_mapping_pass_through(ke, ve): + return spec.expression return ( - f'[{{{inner_expr(0, "key")}: {inner_expr(1)} ' - f"for key, value in m.items()}} " + f"[{{{ke}: {ve} for key, value in m.items()}} " f"for m in {spec.expression}.maps]" ) elif ensure_generic_mapping(spec, args, typing.OrderedDict): - return ( - f'{{{inner_expr(0, "key")}: {inner_expr(1)} ' - f"for key, value in {spec.expression}.items()}}" - ) + ke = inner_expr(0, "key") + ve = inner_expr(1) + if _check_mapping_pass_through(ke, ve): + return spec.expression + return _make_mapping_expression(ke, ve) elif ensure_generic_mapping(spec, args, typing.Counter): - return ( - f'{{{inner_expr(0, "key")}: {inner_expr(1, v_type=int)} ' - f"for key, value in {spec.expression}.items()}}" - ) + ke = inner_expr(0, "key") + ve = inner_expr(1, v_type=int) + if _check_mapping_pass_through(ke, ve): + return spec.expression + return _make_mapping_expression(ke, ve) elif is_typed_dict(spec.origin_type): return pack_typed_dict(spec) elif ensure_generic_mapping(spec, args, typing.Mapping): - return ( - f'{{{inner_expr(0, "key")}: {inner_expr(1)} ' - f"for key, value in {spec.expression}.items()}}" - ) + ke = inner_expr(0, "key") + ve = inner_expr(1) + if _check_mapping_pass_through(ke, ve): + return spec.expression + return _make_mapping_expression(ke, ve) elif ensure_generic_collection_subclass(spec, typing.Sequence): - return f"[{inner_expr()} for value in {spec.expression}]" + ie = inner_expr() + if _check_sequence_pass_through(ie): + return spec.expression + return _make_sequence_expression(ie) @register diff --git a/mashumaro/dialect.py b/mashumaro/dialect.py index c91b49e0..e036083e 100644 --- a/mashumaro/dialect.py +++ b/mashumaro/dialect.py @@ -17,3 +17,4 @@ class Dialect: serialization_strategy: Dict[Any, SerializationStrategyValueType] = {} omit_none: Union[bool, Literal[Sentinel.MISSING]] = Sentinel.MISSING omit_default: Union[bool, Literal[Sentinel.MISSING]] = Sentinel.MISSING + no_copy: bool = False diff --git a/mashumaro/mixins/msgpack.py b/mashumaro/mixins/msgpack.py index 0b4cdeb9..b762a764 100644 --- a/mashumaro/mixins/msgpack.py +++ b/mashumaro/mixins/msgpack.py @@ -15,6 +15,7 @@ class MessagePackDialect(Dialect): + no_copy = True serialization_strategy = { bytes: pass_through, # type: ignore bytearray: { diff --git a/mashumaro/mixins/orjson.py b/mashumaro/mixins/orjson.py index c414400e..600349fe 100644 --- a/mashumaro/mixins/orjson.py +++ b/mashumaro/mixins/orjson.py @@ -18,6 +18,7 @@ class OrjsonDialect(Dialect): + no_copy = True serialization_strategy = { datetime: {"serialize": pass_through}, date: {"serialize": pass_through}, diff --git a/mashumaro/mixins/toml.py b/mashumaro/mixins/toml.py index 7791ffbf..54302c8c 100644 --- a/mashumaro/mixins/toml.py +++ b/mashumaro/mixins/toml.py @@ -21,6 +21,7 @@ class TOMLDialect(Dialect): + no_copy = True omit_none = True serialization_strategy = { datetime: pass_through, diff --git a/tests/test_dialect.py b/tests/test_dialect.py index a1f7457c..89bae8df 100644 --- a/tests/test_dialect.py +++ b/tests/test_dialect.py @@ -1,3 +1,5 @@ +import collections +import typing from dataclasses import dataclass, field from datetime import date, datetime from typing import ( @@ -15,7 +17,7 @@ import pytest from typing_extensions import TypedDict -from mashumaro import DataClassDictMixin +from mashumaro import DataClassDictMixin, pass_through from mashumaro.config import ADD_DIALECT_SUPPORT, BaseConfig from mashumaro.dialect import Dialect from mashumaro.exceptions import BadDialect @@ -1161,3 +1163,40 @@ def test_dataclass_omit_default_dialects(): DataClassWithDefaultAndDialectSupport().to_dict(dialect=EmptyDialect) == complete_dict ) + + +def test_dialect_no_copy(): + class NoCopyDialect(Dialect): + no_copy = True + serialization_strategy = {int: {"serialize": pass_through}} + + @dataclass + class DataClass(DataClassDictMixin): + a: List[str] + b: Set[str] + c: typing.ChainMap[str, str] + d: typing.OrderedDict[str, str] + e: typing.Counter[str] + f: typing.Dict[str, str] + g: typing.Sequence[str] + + class Config(BaseConfig): + dialect = NoCopyDialect + + obj = DataClass( + a=["foo"], + b={"foo"}, + c=collections.ChainMap({"foo": "bar"}), + d=collections.OrderedDict({"foo": "bar"}), + e=collections.Counter({"foo": 1}), + f={"foo": "bar"}, + g=["foo"], + ) + data = obj.to_dict() + assert data["a"] is obj.a + assert data["b"] is obj.b + assert data["c"] is obj.c + assert data["d"] is obj.d + assert data["e"] is obj.e + assert data["f"] is obj.f + assert data["g"] is obj.g