From 402dea5c7b35ce2f9421ab5fc9cf7b153b66aee0 Mon Sep 17 00:00:00 2001 From: Michiel <918128+mvanderlee@users.noreply.github.com> Date: Sat, 9 Mar 2024 23:06:28 +0100 Subject: [PATCH 01/12] Add Annotated support and therefore set minimum python version as 3.9 --- .pre-commit-config.yaml | 2 +- README.md | 19 ++++++------ marshmallow_dataclass/__init__.py | 48 +++++++++++++++++++------------ marshmallow_dataclass/typing.py | 7 +++-- tests/test_annotated.py | 31 ++++++++++++++++++++ tests/test_mypy.yml | 2 +- 6 files changed, 74 insertions(+), 35 deletions(-) create mode 100644 tests/test_annotated.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0aa3b11a..d465df2a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,7 +3,7 @@ repos: rev: v3.3.1 hooks: - id: pyupgrade - args: ["--py36-plus"] + args: ["--py37-plus"] - repo: https://github.com/python/black rev: 23.1.0 hooks: diff --git a/README.md b/README.md index d862f599..e4949e42 100644 --- a/README.md +++ b/README.md @@ -242,28 +242,25 @@ class Sample: See [marshmallow's documentation about extending `Schema`](https://marshmallow.readthedocs.io/en/stable/extending.html). -### Custom NewType declarations +### Custom type declarations -This library exports a `NewType` function to create types that generate [customized marshmallow fields](https://marshmallow.readthedocs.io/en/stable/custom_fields.html#creating-a-field-class). - -Keyword arguments to `NewType` are passed to the marshmallow field constructor. +This library allows you to specify [customized marshmallow fields](https://marshmallow.readthedocs.io/en/stable/custom_fields.html#creating-a-field-class) using python's Annoted type [PEP-593](https://peps.python.org/pep-0593/). ```python -import marshmallow.validate -from marshmallow_dataclass import NewType +from typing import Annotated +import marshmallow.fields as mf +import marshmallow.validate as mv -IPv4 = NewType( - "IPv4", str, validate=marshmallow.validate.Regexp(r"^([0-9]{1,3}\\.){3}[0-9]{1,3}$") -) +IPv4 = Annotated[str, mf.String(validate=mv.Regexp(r"^([0-9]{1,3}\\.){3}[0-9]{1,3}$"))] ``` -You can also pass a marshmallow field to `NewType`. +You can also pass a marshmallow field class. ```python import marshmallow from marshmallow_dataclass import NewType -Email = NewType("Email", str, field=marshmallow.fields.Email) +Email = Annotated[str, marshmallow.fields.Email] ``` For convenience, some custom types are provided: diff --git a/marshmallow_dataclass/__init__.py b/marshmallow_dataclass/__init__.py index 1fe9813a..23a261b5 100644 --- a/marshmallow_dataclass/__init__.py +++ b/marshmallow_dataclass/__init__.py @@ -43,6 +43,8 @@ class User: import warnings from enum import Enum from functools import lru_cache, partial +from typing import Annotated, Any, Callable, Dict, FrozenSet, List, Mapping +from typing import NewType as typing_NewType from typing import ( Any, Callable, @@ -52,16 +54,17 @@ class User: Mapping, NewType as typing_NewType, Optional, + Sequence, Set, Tuple, Type, TypeVar, Union, cast, + get_args, + get_origin, get_type_hints, overload, - Sequence, - FrozenSet, ) import marshmallow @@ -69,15 +72,10 @@ class User: from marshmallow_dataclass.lazy_class_attribute import lazy_class_attribute - if sys.version_info >= (3, 11): from typing import dataclass_transform -elif sys.version_info >= (3, 7): - from typing_extensions import dataclass_transform else: - # @dataclass_transform() only helps us with mypy>=1.1 which is only available for python>=3.7 - def dataclass_transform(**kwargs): - return lambda cls: cls + from typing_extensions import dataclass_transform __all__ = ["dataclass", "add_schema", "class_schema", "field_for_schema", "NewType"] @@ -547,7 +545,7 @@ def _internal_class_schema( # Update the schema members to contain marshmallow fields instead of dataclass fields type_hints = get_type_hints( - clazz, globalns=schema_ctx.globalns, localns=schema_ctx.localns + clazz, globalns=schema_ctx.globalns, localns=schema_ctx.localns, include_extras=True, ) attributes.update( ( @@ -639,12 +637,27 @@ def _field_for_generic_type( """ If the type is a generic interface, resolve the arguments and construct the appropriate Field. """ - origin = typing_inspect.get_origin(typ) - arguments = typing_inspect.get_args(typ, True) + origin = get_origin(typ) + arguments = get_args(typ) if origin: # Override base_schema.TYPE_MAPPING to change the class used for generic types below type_mapping = base_schema.TYPE_MAPPING if base_schema else {} + if origin is Annotated: + marshmallow_annotations = [ + arg + for arg in arguments[1:] + if (inspect.isclass(arg) and issubclass(arg, marshmallow.fields.Field)) + or isinstance(arg, marshmallow.fields.Field) + ] + if marshmallow_annotations: + field = marshmallow_annotations[-1] + # Got a field instance, return as is. User must know what they're doing + if isinstance(field, marshmallow.fields.Field): + return field + + return field(**metadata) + if origin in (list, List): child_type = _field_for_schema(arguments[0], base_schema=base_schema) list_type = cast( @@ -806,6 +819,7 @@ def _field_for_schema( metadata.setdefault("allow_none", True) return marshmallow.fields.Raw(**metadata) + # i.e.: Literal['abc'] if typing_inspect.is_literal_type(typ): arguments = typing_inspect.get_args(typ) return marshmallow.fields.Raw( @@ -817,6 +831,7 @@ def _field_for_schema( **metadata, ) + # i.e.: Final[str] = 'abc' if typing_inspect.is_final_type(typ): arguments = typing_inspect.get_args(typ) if arguments: @@ -870,13 +885,7 @@ def _field_for_schema( # enumerations if issubclass(typ, Enum): - try: - return marshmallow.fields.Enum(typ, **metadata) - except AttributeError: - # Remove this once support for python 3.6 is dropped. - import marshmallow_enum - - return marshmallow_enum.EnumField(typ, **metadata) + return marshmallow.fields.Enum(typ, **metadata) # Nested marshmallow dataclass # it would be just a class name instead of actual schema util the schema is not ready yet @@ -939,7 +948,8 @@ def NewType( field: Optional[Type[marshmallow.fields.Field]] = None, **kwargs, ) -> Callable[[_U], _U]: - """NewType creates simple unique types + """DEPRECATED: Use typing.Annotated instead. + NewType creates simple unique types to which you can attach custom marshmallow attributes. All the keyword arguments passed to this function will be transmitted to the marshmallow field constructor. diff --git a/marshmallow_dataclass/typing.py b/marshmallow_dataclass/typing.py index 01291eb8..35bdee23 100644 --- a/marshmallow_dataclass/typing.py +++ b/marshmallow_dataclass/typing.py @@ -1,8 +1,9 @@ +from typing import Annotated + import marshmallow.fields -from . import NewType -Url = NewType("Url", str, field=marshmallow.fields.Url) -Email = NewType("Email", str, field=marshmallow.fields.Email) +Url = Annotated[str, marshmallow.fields.Url] +Email = Annotated[str, marshmallow.fields.Email] # Aliases URL = Url diff --git a/tests/test_annotated.py b/tests/test_annotated.py new file mode 100644 index 00000000..9b57ac51 --- /dev/null +++ b/tests/test_annotated.py @@ -0,0 +1,31 @@ +import unittest +from typing import Annotated, Optional + +import marshmallow +import marshmallow.fields + +from marshmallow_dataclass import dataclass + + +class TestAnnotatedField(unittest.TestCase): + def test_annotated_field(self): + @dataclass + class AnnotatedValue: + value: Annotated[str, marshmallow.fields.Email] + default_string: Annotated[ + Optional[str], marshmallow.fields.String(load_default="Default String") + ] = None + + schema = AnnotatedValue.Schema() + + self.assertEqual( + schema.load({"value": "test@test.com"}), + AnnotatedValue(value="test@test.com", default_string="Default String"), + ) + self.assertEqual( + schema.load({"value": "test@test.com", "default_string": "override"}), + AnnotatedValue(value="test@test.com", default_string="override"), + ) + + with self.assertRaises(marshmallow.exceptions.ValidationError): + schema.load({"value": "notavalidemail"}) diff --git a/tests/test_mypy.yml b/tests/test_mypy.yml index 479abd57..1487cb73 100644 --- a/tests/test_mypy.yml +++ b/tests/test_mypy.yml @@ -27,7 +27,7 @@ reveal_type(user.email) # N: Revealed type is "builtins.str" User(id=42, email="user@email.com") # E: Argument "id" to "User" has incompatible type "int"; expected "str" [arg-type] - User(id="a"*32, email=["not", "a", "string"]) # E: Argument "email" to "User" has incompatible type "List[str]"; expected "str" [arg-type] + User(id="a"*32, email=["not", "a", "string"]) # E: Argument "email" to "User" has incompatible type "list[str]"; expected "str" [arg-type] - case: marshmallow_dataclass_keyword_arguments mypy_config: | follow_imports = silent From 48b7e1768e6db9f8397a204c3ed0ab87225ec23c Mon Sep 17 00:00:00 2001 From: Michiel <918128+mvanderlee@users.noreply.github.com> Date: Sun, 10 Mar 2024 02:01:06 +0100 Subject: [PATCH 02/12] Add support back for Python3.8 --- .gitignore | 3 +++ .pre-commit-config.yaml | 1 + marshmallow_dataclass/__init__.py | 7 ++++++- marshmallow_dataclass/typing.py | 7 ++++++- setup.py | 2 +- 5 files changed, 17 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index cbfc4da4..02da4179 100644 --- a/.gitignore +++ b/.gitignore @@ -95,6 +95,9 @@ venv.bak/ # Rope project settings .ropeproject +# VSCode project settings +.vscode + # mkdocs documentation /site diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d465df2a..dade9fa2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,6 +3,7 @@ repos: rev: v3.3.1 hooks: - id: pyupgrade + # I've kept it on py3.7 so that it doesn't replace `Dict` with `dict` args: ["--py37-plus"] - repo: https://github.com/python/black rev: 23.1.0 diff --git a/marshmallow_dataclass/__init__.py b/marshmallow_dataclass/__init__.py index 23a261b5..47631731 100644 --- a/marshmallow_dataclass/__init__.py +++ b/marshmallow_dataclass/__init__.py @@ -43,7 +43,7 @@ class User: import warnings from enum import Enum from functools import lru_cache, partial -from typing import Annotated, Any, Callable, Dict, FrozenSet, List, Mapping +from typing import Any, Callable, Dict, FrozenSet, List, Mapping from typing import NewType as typing_NewType from typing import ( Any, @@ -72,6 +72,11 @@ class User: from marshmallow_dataclass.lazy_class_attribute import lazy_class_attribute +if sys.version_info >= (3, 9): + from typing import Annotated +else: + from typing_extensions import Annotated + if sys.version_info >= (3, 11): from typing import dataclass_transform else: diff --git a/marshmallow_dataclass/typing.py b/marshmallow_dataclass/typing.py index 35bdee23..4db2f152 100644 --- a/marshmallow_dataclass/typing.py +++ b/marshmallow_dataclass/typing.py @@ -1,7 +1,12 @@ -from typing import Annotated +import sys import marshmallow.fields +if sys.version_info >= (3, 9): + from typing import Annotated +else: + from typing_extensions import Annotated + Url = Annotated[str, marshmallow.fields.Url] Email = Annotated[str, marshmallow.fields.Email] diff --git a/setup.py b/setup.py index 7cb2659a..9ab3a242 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,4 @@ -from setuptools import setup, find_packages +from setuptools import find_packages, setup VERSION = "9.0.0" From 41e56e8fed3bb194b53e4252f984af058bc4f445 Mon Sep 17 00:00:00 2001 From: Michiel <918128+mvanderlee@users.noreply.github.com> Date: Sun, 10 Mar 2024 03:14:49 +0100 Subject: [PATCH 03/12] Fix Python 3.8 and add tox config for cross version testing --- CONTRIBUTING.md | 2 ++ marshmallow_dataclass/__init__.py | 56 ++++++++++++++++++------------- setup.py | 2 ++ tests/test_annotated.py | 8 ++++- tests/test_mypy.yml | 2 +- tox.ini | 11 ++++++ 6 files changed, 56 insertions(+), 25 deletions(-) create mode 100644 tox.ini diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2ec0fb4f..832feb03 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -21,3 +21,5 @@ Every commit is checked with pre-commit hooks for : - type safety with [mypy](http://mypy-lang.org/) - test conformance by running [tests](./tests) with [pytest](https://docs.pytest.org/en/latest/) - You can run `pytest` from the command line. + + - You can also run `tox` from the command line to test in all supported python versions. Note that this will require you to have all supported python versions installed. \ No newline at end of file diff --git a/marshmallow_dataclass/__init__.py b/marshmallow_dataclass/__init__.py index 47631731..19746996 100644 --- a/marshmallow_dataclass/__init__.py +++ b/marshmallow_dataclass/__init__.py @@ -34,6 +34,7 @@ class User: }) Schema: ClassVar[Type[Schema]] = Schema # For the type checker """ + import collections.abc import dataclasses import inspect @@ -61,13 +62,12 @@ class User: TypeVar, Union, cast, - get_args, - get_origin, get_type_hints, overload, ) import marshmallow +import typing_extensions import typing_inspect from marshmallow_dataclass.lazy_class_attribute import lazy_class_attribute @@ -151,8 +151,7 @@ def dataclass( frozen: bool = False, base_schema: Optional[Type[marshmallow.Schema]] = None, cls_frame: Optional[types.FrameType] = None, -) -> Type[_U]: - ... +) -> Type[_U]: ... @overload @@ -165,8 +164,7 @@ def dataclass( frozen: bool = False, base_schema: Optional[Type[marshmallow.Schema]] = None, cls_frame: Optional[types.FrameType] = None, -) -> Callable[[Type[_U]], Type[_U]]: - ... +) -> Callable[[Type[_U]], Type[_U]]: ... # _cls should never be specified by keyword, so start it with an @@ -225,15 +223,13 @@ def decorator(cls: Type[_U], stacklevel: int = 1) -> Type[_U]: @overload -def add_schema(_cls: Type[_U]) -> Type[_U]: - ... +def add_schema(_cls: Type[_U]) -> Type[_U]: ... @overload def add_schema( base_schema: Optional[Type[marshmallow.Schema]] = None, -) -> Callable[[Type[_U]], Type[_U]]: - ... +) -> Callable[[Type[_U]], Type[_U]]: ... @overload @@ -242,8 +238,7 @@ def add_schema( base_schema: Optional[Type[marshmallow.Schema]] = None, cls_frame: Optional[types.FrameType] = None, stacklevel: int = 1, -) -> Type[_U]: - ... +) -> Type[_U]: ... def add_schema(_cls=None, base_schema=None, cls_frame=None, stacklevel=1): @@ -294,8 +289,7 @@ def class_schema( *, globalns: Optional[Dict[str, Any]] = None, localns: Optional[Dict[str, Any]] = None, -) -> Type[marshmallow.Schema]: - ... +) -> Type[marshmallow.Schema]: ... @overload @@ -305,8 +299,7 @@ def class_schema( clazz_frame: Optional[types.FrameType] = None, *, globalns: Optional[Dict[str, Any]] = None, -) -> Type[marshmallow.Schema]: - ... +) -> Type[marshmallow.Schema]: ... def class_schema( @@ -514,7 +507,15 @@ def _internal_class_schema( base_schema: Optional[Type[marshmallow.Schema]] = None, ) -> Type[marshmallow.Schema]: schema_ctx = _schema_ctx_stack.top - schema_ctx.seen_classes[clazz] = clazz.__name__ + + if typing_extensions.get_origin(clazz) is Annotated and sys.version_info < (3, 10): + # https://github.com/python/cpython/blob/3.10/Lib/typing.py#L977 + class_name = clazz._name or clazz.__origin__.__name__ # type: ignore[attr-defined] + else: + class_name = clazz.__name__ + + schema_ctx.seen_classes[clazz] = class_name + try: # noinspection PyDataclass fields: Tuple[dataclasses.Field, ...] = dataclasses.fields(clazz) @@ -549,9 +550,18 @@ def _internal_class_schema( include_non_init = getattr(getattr(clazz, "Meta", None), "include_non_init", False) # Update the schema members to contain marshmallow fields instead of dataclass fields - type_hints = get_type_hints( - clazz, globalns=schema_ctx.globalns, localns=schema_ctx.localns, include_extras=True, - ) + + if sys.version_info >= (3, 9): + type_hints = get_type_hints( + clazz, + globalns=schema_ctx.globalns, + localns=schema_ctx.localns, + include_extras=True, + ) + else: + type_hints = get_type_hints( + clazz, globalns=schema_ctx.globalns, localns=schema_ctx.localns + ) attributes.update( ( field.name, @@ -642,8 +652,8 @@ def _field_for_generic_type( """ If the type is a generic interface, resolve the arguments and construct the appropriate Field. """ - origin = get_origin(typ) - arguments = get_args(typ) + origin = typing_extensions.get_origin(typ) + arguments = typing_extensions.get_args(typ) if origin: # Override base_schema.TYPE_MAPPING to change the class used for generic types below type_mapping = base_schema.TYPE_MAPPING if base_schema else {} @@ -889,7 +899,7 @@ def _field_for_schema( ) # enumerations - if issubclass(typ, Enum): + if inspect.isclass(typ) and issubclass(typ, Enum): return marshmallow.fields.Enum(typ, **metadata) # Nested marshmallow dataclass diff --git a/setup.py b/setup.py index 9ab3a242..39d6351d 100644 --- a/setup.py +++ b/setup.py @@ -24,6 +24,8 @@ # re: pypy: typed-ast (a dependency of mypy) fails to install on pypy # https://github.com/python/typed_ast/issues/111 "pytest-mypy-plugins>=1.2.0; implementation_name != 'pypy'", + "tox>=4", + "virtualenv-pyenv", ], } EXTRAS_REQUIRE["dev"] = ( diff --git a/tests/test_annotated.py b/tests/test_annotated.py index 9b57ac51..e9105a61 100644 --- a/tests/test_annotated.py +++ b/tests/test_annotated.py @@ -1,11 +1,17 @@ +import sys import unittest -from typing import Annotated, Optional +from typing import Optional import marshmallow import marshmallow.fields from marshmallow_dataclass import dataclass +if sys.version_info >= (3, 9): + from typing import Annotated +else: + from typing_extensions import Annotated + class TestAnnotatedField(unittest.TestCase): def test_annotated_field(self): diff --git a/tests/test_mypy.yml b/tests/test_mypy.yml index 1487cb73..479abd57 100644 --- a/tests/test_mypy.yml +++ b/tests/test_mypy.yml @@ -27,7 +27,7 @@ reveal_type(user.email) # N: Revealed type is "builtins.str" User(id=42, email="user@email.com") # E: Argument "id" to "User" has incompatible type "int"; expected "str" [arg-type] - User(id="a"*32, email=["not", "a", "string"]) # E: Argument "email" to "User" has incompatible type "list[str]"; expected "str" [arg-type] + User(id="a"*32, email=["not", "a", "string"]) # E: Argument "email" to "User" has incompatible type "List[str]"; expected "str" [arg-type] - case: marshmallow_dataclass_keyword_arguments mypy_config: | follow_imports = silent diff --git a/tox.ini b/tox.ini new file mode 100644 index 00000000..6bcf7ddd --- /dev/null +++ b/tox.ini @@ -0,0 +1,11 @@ +[tox] +requires = + tox>=4 +env_list = py{38,39,310,311,312} + +[testenv] +deps = pytest +commands = pytest +extras = dev +set_env = + VIRTUALENV_DISCOVERY = pyenv From 0d43c4117b358b871d2a17ecb64bc83638b67df3 Mon Sep 17 00:00:00 2001 From: Michiel <918128+mvanderlee@users.noreply.github.com> Date: Wed, 20 Mar 2024 17:49:18 +0100 Subject: [PATCH 04/12] Fix style issues. Rebase did not trigger pre-commit. --- marshmallow_dataclass/__init__.py | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/marshmallow_dataclass/__init__.py b/marshmallow_dataclass/__init__.py index 19746996..5af1c832 100644 --- a/marshmallow_dataclass/__init__.py +++ b/marshmallow_dataclass/__init__.py @@ -44,12 +44,11 @@ class User: import warnings from enum import Enum from functools import lru_cache, partial -from typing import Any, Callable, Dict, FrozenSet, List, Mapping -from typing import NewType as typing_NewType from typing import ( Any, Callable, Dict, + FrozenSet, Generic, List, Mapping, @@ -151,7 +150,8 @@ def dataclass( frozen: bool = False, base_schema: Optional[Type[marshmallow.Schema]] = None, cls_frame: Optional[types.FrameType] = None, -) -> Type[_U]: ... +) -> Type[_U]: + ... @overload @@ -164,7 +164,8 @@ def dataclass( frozen: bool = False, base_schema: Optional[Type[marshmallow.Schema]] = None, cls_frame: Optional[types.FrameType] = None, -) -> Callable[[Type[_U]], Type[_U]]: ... +) -> Callable[[Type[_U]], Type[_U]]: + ... # _cls should never be specified by keyword, so start it with an @@ -223,13 +224,15 @@ def decorator(cls: Type[_U], stacklevel: int = 1) -> Type[_U]: @overload -def add_schema(_cls: Type[_U]) -> Type[_U]: ... +def add_schema(_cls: Type[_U]) -> Type[_U]: + ... @overload def add_schema( base_schema: Optional[Type[marshmallow.Schema]] = None, -) -> Callable[[Type[_U]], Type[_U]]: ... +) -> Callable[[Type[_U]], Type[_U]]: + ... @overload @@ -238,7 +241,8 @@ def add_schema( base_schema: Optional[Type[marshmallow.Schema]] = None, cls_frame: Optional[types.FrameType] = None, stacklevel: int = 1, -) -> Type[_U]: ... +) -> Type[_U]: + ... def add_schema(_cls=None, base_schema=None, cls_frame=None, stacklevel=1): @@ -289,7 +293,8 @@ def class_schema( *, globalns: Optional[Dict[str, Any]] = None, localns: Optional[Dict[str, Any]] = None, -) -> Type[marshmallow.Schema]: ... +) -> Type[marshmallow.Schema]: + ... @overload @@ -299,7 +304,8 @@ def class_schema( clazz_frame: Optional[types.FrameType] = None, *, globalns: Optional[Dict[str, Any]] = None, -) -> Type[marshmallow.Schema]: ... +) -> Type[marshmallow.Schema]: + ... def class_schema( From 113c478a1662bd1ee3e14aa7a019332145ce4902 Mon Sep 17 00:00:00 2001 From: Michiel <918128+mvanderlee@users.noreply.github.com> Date: Tue, 18 Jun 2024 23:36:39 +0200 Subject: [PATCH 05/12] Remove enum from pre-commit --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index dade9fa2..b1f1ea64 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,7 +20,7 @@ repos: rev: v1.1.1 hooks: - id: mypy - additional_dependencies: [marshmallow-enum,typeguard,marshmallow] + additional_dependencies: [typeguard,marshmallow] args: [--show-error-codes] - repo: https://github.com/asottile/blacken-docs rev: 1.13.0 From 7eb92813336c6b252a9b1b8ecb6cf03155c6349a Mon Sep 17 00:00:00 2001 From: Michiel <918128+mvanderlee@users.noreply.github.com> Date: Tue, 18 Jun 2024 23:40:48 +0200 Subject: [PATCH 06/12] re-add but deprecate the newtype documentation --- README.md | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e4949e42..f2e50f22 100644 --- a/README.md +++ b/README.md @@ -242,7 +242,7 @@ class Sample: See [marshmallow's documentation about extending `Schema`](https://marshmallow.readthedocs.io/en/stable/extending.html). -### Custom type declarations +### Custom type aliases This library allows you to specify [customized marshmallow fields](https://marshmallow.readthedocs.io/en/stable/custom_fields.html#creating-a-field-class) using python's Annoted type [PEP-593](https://peps.python.org/pep-0593/). @@ -269,6 +269,32 @@ For convenience, some custom types are provided: from marshmallow_dataclass.typing import Email, Url ``` +### Custom NewType declarations [__deprecated__] + +> NewType is deprecated in favor or type aliases using Annotated, as described above. + +This library exports a `NewType` function to create types that generate [customized marshmallow fields](https://marshmallow.readthedocs.io/en/stable/custom_fields.html#creating-a-field-class). + +Keyword arguments to `NewType` are passed to the marshmallow field constructor. + +```python +import marshmallow.validate +from marshmallow_dataclass import NewType + +IPv4 = NewType( + "IPv4", str, validate=marshmallow.validate.Regexp(r"^([0-9]{1,3}\\.){3}[0-9]{1,3}$") +) +``` + +You can also pass a marshmallow field to `NewType`. + +```python +import marshmallow +from marshmallow_dataclass import NewType + +Email = NewType("Email", str, field=marshmallow.fields.Email) +``` + Note: if you are using `mypy`, you will notice that `mypy` throws an error if a variable defined with `NewType` is used in a type annotation. To resolve this, add the `marshmallow_dataclass.mypy` plugin to your `mypy` configuration, e.g.: From fae55c3da25f1c4025bea405039e2017514105e3 Mon Sep 17 00:00:00 2001 From: Michiel <918128+mvanderlee@users.noreply.github.com> Date: Tue, 18 Jun 2024 23:42:52 +0200 Subject: [PATCH 07/12] Move Annotated and Union handling to their own functions --- marshmallow_dataclass/__init__.py | 58 +++++++++++++++++++++++-------- 1 file changed, 43 insertions(+), 15 deletions(-) diff --git a/marshmallow_dataclass/__init__.py b/marshmallow_dataclass/__init__.py index 5af1c832..aefa77b6 100644 --- a/marshmallow_dataclass/__init__.py +++ b/marshmallow_dataclass/__init__.py @@ -664,21 +664,6 @@ def _field_for_generic_type( # Override base_schema.TYPE_MAPPING to change the class used for generic types below type_mapping = base_schema.TYPE_MAPPING if base_schema else {} - if origin is Annotated: - marshmallow_annotations = [ - arg - for arg in arguments[1:] - if (inspect.isclass(arg) and issubclass(arg, marshmallow.fields.Field)) - or isinstance(arg, marshmallow.fields.Field) - ] - if marshmallow_annotations: - field = marshmallow_annotations[-1] - # Got a field instance, return as is. User must know what they're doing - if isinstance(field, marshmallow.fields.Field): - return field - - return field(**metadata) - if origin in (list, List): child_type = _field_for_schema(arguments[0], base_schema=base_schema) list_type = cast( @@ -728,6 +713,41 @@ def _field_for_generic_type( **metadata, ) + return None + + +def _field_for_annotated_type( + typ: type, + **metadata: Any, +) -> Optional[marshmallow.fields.Field]: + """ + If the type is an Annotated interface, resolve the arguments and construct the appropriate Field. + """ + origin = typing_extensions.get_origin(typ) + arguments = typing_extensions.get_args(typ) + if origin and origin is Annotated: + marshmallow_annotations = [ + arg + for arg in arguments[1:] + if (inspect.isclass(arg) and issubclass(arg, marshmallow.fields.Field)) + or isinstance(arg, marshmallow.fields.Field) + ] + if marshmallow_annotations: + field = marshmallow_annotations[-1] + # Got a field instance, return as is. User must know what they're doing + if isinstance(field, marshmallow.fields.Field): + return field + + return field(**metadata) + return None + + +def _field_for_union_type( + typ: type, + base_schema: Optional[Type[marshmallow.Schema]], + **metadata: Any, +) -> Optional[marshmallow.fields.Field]: + arguments = typing_extensions.get_args(typ) if typing_inspect.is_union_type(typ): if typing_inspect.is_optional_type(typ): metadata["allow_none"] = metadata.get("allow_none", True) @@ -887,6 +907,14 @@ def _field_for_schema( subtyp = Any return _field_for_schema(subtyp, default, metadata, base_schema) + annotated_field = _field_for_annotated_type(typ, **metadata) + if annotated_field: + return annotated_field + + union_field = _field_for_union_type(typ, base_schema, **metadata) + if union_field: + return union_field + # Generic types generic_field = _field_for_generic_type(typ, base_schema, **metadata) if generic_field: From 7fc3fb7b911bffda557ea61ecd1b33d49ce0a966 Mon Sep 17 00:00:00 2001 From: Michiel <918128+mvanderlee@users.noreply.github.com> Date: Wed, 19 Jun 2024 00:14:12 +0200 Subject: [PATCH 08/12] Remove tox from requirements --- setup.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/setup.py b/setup.py index 39d6351d..9ab3a242 100644 --- a/setup.py +++ b/setup.py @@ -24,8 +24,6 @@ # re: pypy: typed-ast (a dependency of mypy) fails to install on pypy # https://github.com/python/typed_ast/issues/111 "pytest-mypy-plugins>=1.2.0; implementation_name != 'pypy'", - "tox>=4", - "virtualenv-pyenv", ], } EXTRAS_REQUIRE["dev"] = ( From c19b5637270e2139d921cadc5bc83cc89ec9dfc9 Mon Sep 17 00:00:00 2001 From: Michiel <918128+mvanderlee@users.noreply.github.com> Date: Wed, 19 Jun 2024 00:16:07 +0200 Subject: [PATCH 09/12] Add coverage report and remove virtualenv-pyenv --- pyproject.toml | 6 ++++++ tox.ini | 22 +++++++++++++++++++--- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 4112bd14..49e683c8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,3 +6,9 @@ target-version = ['py36', 'py37', 'py38', 'py39', 'py310', 'py310'] filterwarnings = [ "error:::marshmallow_dataclass|test", ] + +[tool.coverage.report] +exclude_also = [ + '^\s*\.\.\.\s*$', + '^\s*pass\s*$', +] \ No newline at end of file diff --git a/tox.ini b/tox.ini index 6bcf7ddd..0ea84049 100644 --- a/tox.ini +++ b/tox.ini @@ -1,11 +1,27 @@ [tox] requires = tox>=4 -env_list = py{38,39,310,311,312} +env_list = + py{38,39,310,311,312} + cover-report +set_env = + VIRTUALENV_DISCOVERY = pyenv [testenv] -deps = pytest -commands = pytest +deps = + coverage + pytest +commands = coverage run -p -m pytest tests extras = dev set_env = VIRTUALENV_DISCOVERY = pyenv +depends = + cover-report: py{38,39,310,311,312} + +[testenv:cover-report] +skip_install = true +deps = coverage +commands = + coverage combine + coverage html + coverage report \ No newline at end of file From dbd909f32eaf85ed2d9e7442e291c193e9a45d30 Mon Sep 17 00:00:00 2001 From: Michiel <918128+mvanderlee@users.noreply.github.com> Date: Wed, 19 Jun 2024 00:34:26 +0200 Subject: [PATCH 10/12] Add warning when multiple Field annotations have bene detected --- marshmallow_dataclass/__init__.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/marshmallow_dataclass/__init__.py b/marshmallow_dataclass/__init__.py index aefa77b6..f82b13a2 100644 --- a/marshmallow_dataclass/__init__.py +++ b/marshmallow_dataclass/__init__.py @@ -733,6 +733,11 @@ def _field_for_annotated_type( or isinstance(arg, marshmallow.fields.Field) ] if marshmallow_annotations: + if len(marshmallow_annotations) > 1: + warnings.warn( + "Multiple marshmallow Field annotations found. Using the last one." + ) + field = marshmallow_annotations[-1] # Got a field instance, return as is. User must know what they're doing if isinstance(field, marshmallow.fields.Field): From 0a43d81d1034d7d1402f14680fe62f7842993a2b Mon Sep 17 00:00:00 2001 From: Michiel <918128+mvanderlee@users.noreply.github.com> Date: Sun, 23 Jun 2024 16:05:55 +0200 Subject: [PATCH 11/12] Remove tox and document Annotated for python 3.8 --- CONTRIBUTING.md | 3 +- README.md | 673 ++++++++++++++++++++++++------------------------ tox.ini | 27 -- 3 files changed, 343 insertions(+), 360 deletions(-) delete mode 100644 tox.ini diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 832feb03..7aad9414 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -21,5 +21,4 @@ Every commit is checked with pre-commit hooks for : - type safety with [mypy](http://mypy-lang.org/) - test conformance by running [tests](./tests) with [pytest](https://docs.pytest.org/en/latest/) - You can run `pytest` from the command line. - - - You can also run `tox` from the command line to test in all supported python versions. Note that this will require you to have all supported python versions installed. \ No newline at end of file + \ No newline at end of file diff --git a/README.md b/README.md index f2e50f22..e70264a4 100644 --- a/README.md +++ b/README.md @@ -1,331 +1,342 @@ -# marshmallow-dataclass - -[![Test Workflow Status (master branch)](https://img.shields.io/github/actions/workflow/status/lovasoa/marshmallow_dataclass/python-package.yml?branch=master&label=tests)](https://github.com/lovasoa/marshmallow_dataclass/actions/workflows/python-package.yml) -[![PyPI version](https://badge.fury.io/py/marshmallow-dataclass.svg)](https://badge.fury.io/py/marshmallow-dataclass) -[![marshmallow 3 compatible](https://badgen.net/badge/marshmallow/3)](https://marshmallow.readthedocs.io/en/latest/upgrading.html) -[![download stats](https://img.shields.io/pypi/dm/marshmallow-dataclass.svg)](https://pypistats.org/packages/marshmallow-dataclass) - -Automatic generation of [marshmallow](https://marshmallow.readthedocs.io/) schemas from dataclasses. - -```python -from dataclasses import dataclass, field -from typing import List, Optional - -import marshmallow_dataclass -import marshmallow.validate - - -@dataclass -class Building: - # field metadata is used to instantiate the marshmallow field - height: float = field(metadata={"validate": marshmallow.validate.Range(min=0)}) - name: str = field(default="anonymous") - - -@dataclass -class City: - name: Optional[str] - buildings: List[Building] = field(default_factory=list) - - -city_schema = marshmallow_dataclass.class_schema(City)() - -city = city_schema.load( - {"name": "Paris", "buildings": [{"name": "Eiffel Tower", "height": 324}]} -) -# => City(name='Paris', buildings=[Building(height=324.0, name='Eiffel Tower')]) - -city_dict = city_schema.dump(city) -# => {'name': 'Paris', 'buildings': [{'name': 'Eiffel Tower', 'height': 324.0}]} -``` - -## Why - -Using schemas in Python often means having both a class to represent your data and a class to represent its schema, which results in duplicated code that could fall out of sync. -As of Python 3.6, types can be defined for class members, which allows libraries to generate schemas automatically. - -Therefore, you can document your APIs in a way that allows you to statically check that the code matches the documentation. - -## Installation - -This package [is hosted on PyPI](https://pypi.org/project/marshmallow-dataclass/). - -```shell -pip3 install marshmallow-dataclass -``` - -```shell -pip3 install "marshmallow-dataclass" -``` - -### marshmallow 2 support - -`marshmallow-dataclass` no longer supports marshmallow 2. -Install `marshmallow_dataclass<6.0` if you need marshmallow 2 compatibility. - -## Usage - -Use the [`class_schema`](https://lovasoa.github.io/marshmallow_dataclass/html/marshmallow_dataclass.html#marshmallow_dataclass.class_schema) -function to generate a marshmallow [Schema](https://marshmallow.readthedocs.io/en/latest/api_reference.html#marshmallow.Schema) -class from a [`dataclass`](https://docs.python.org/3/library/dataclasses.html#dataclasses.dataclass). - -```python -from dataclasses import dataclass -from datetime import date - -import marshmallow_dataclass - - -@dataclass -class Person: - name: str - birth: date - - -PersonSchema = marshmallow_dataclass.class_schema(Person) -``` - -The type of your fields must be either basic -[types supported by marshmallow](https://marshmallow.readthedocs.io/en/stable/api_reference.html#marshmallow.Schema.TYPE_MAPPING) -(such as `float`, `str`, `bytes`, `datetime`, ...), `Union`, or other dataclasses. - -### Union (de)serialization coercion - -Typically the Union type; `Union[X, Y]` means—from a set theory perspective—either `X` or `Y`, i.e., an unordered set, howevever the order of the sub-types defines the precedence when attempting to ether deserialize or serialize the value per [here](https://github.com/lovasoa/marshmallow_dataclass/blob/master/marshmallow_dataclass/union_field.py). - -For example, - -```python -from typing import Union - -from dataclasses import dataclass - - -@dataclass -class Person: - name: str - age: Union[int, float] - - -PersonSchema = marshmallow_dataclass.class_schema(Person) -PersonSchema().load({"name": "jane", "age": 50.0}) -# => Person(name="jane", age=50) -``` - -will first (sucessfully) try to coerce `50.0` to an `int`. If coercion is not desired the `Any` type can be used with the caveat that values will not be type checked without additional [validation](https://marshmallow.readthedocs.io/en/stable/marshmallow.validate.html). - -### Customizing generated fields - -To pass arguments to the generated marshmallow fields (e.g., `validate`, `load_only`, `dump_only`, etc.), -pass them to the `metadata` argument of the -[`field`](https://docs.python.org/3/library/dataclasses.html#dataclasses.field) function. - -Note that starting with version 4, marshmallow will disallow passing arbitrary arguments, so any -additional metadata should itself be put in its own `metadata` dict: - -```python -from dataclasses import dataclass, field -import marshmallow_dataclass -import marshmallow.validate - - -@dataclass -class Person: - name: str = field( - metadata=dict( - load_only=True, metadata=dict(description="The person's first name") - ) - ) - height: float = field(metadata=dict(validate=marshmallow.validate.Range(min=0))) - - -PersonSchema = marshmallow_dataclass.class_schema(Person) -``` - -### `@dataclass` shortcut - -`marshmallow_dataclass` provides a `@dataclass` decorator that behaves like the standard library's -`@dataclasses.dataclass` and adds a `Schema` attribute with the generated marshmallow -[Schema](https://marshmallow.readthedocs.io/en/2.x-line/api_reference.html#marshmallow.Schema). - -```python -# Use marshmallow_dataclass's @dataclass shortcut -from marshmallow_dataclass import dataclass - - -@dataclass -class Point: - x: float - y: float - - -Point.Schema().dump(Point(4, 2)) -# => {'x': 4, 'y': 2} -``` - -Note: Since the `.Schema` property is added dynamically, it can confuse type checkers. -To avoid that, you can declare `Schema` as a [`ClassVar`](https://docs.python.org/3/library/typing.html#typing.ClassVar). - -```python -from typing import ClassVar, Type - -from marshmallow_dataclass import dataclass -from marshmallow import Schema - - -@dataclass -class Point: - x: float - y: float - Schema: ClassVar[Type[Schema]] = Schema -``` - -### Customizing the base Schema - -It is also possible to derive all schemas from your own -base Schema class -(see [marshmallow's documentation about extending `Schema`](https://marshmallow.readthedocs.io/en/stable/extending.html)). -This allows you to implement custom (de)serialization -behavior, for instance specifying a custom mapping between your classes and marshmallow fields, -or renaming fields on serialization. - -#### Custom mapping between classes and fields - -```python -class BaseSchema(marshmallow.Schema): - TYPE_MAPPING = {CustomType: CustomField, List: CustomListField} - - -class Sample: - my_custom: CustomType - my_custom_list: List[int] - - -SampleSchema = marshmallow_dataclass.class_schema(Sample, base_schema=BaseSchema) -# SampleSchema now serializes my_custom using the CustomField marshmallow field -# and serializes my_custom_list using the CustomListField marshmallow field -``` - -#### Renaming fields on serialization - -```python -import marshmallow -import marshmallow_dataclass - - -class UppercaseSchema(marshmallow.Schema): - """A Schema that marshals data with uppercased keys.""" - - def on_bind_field(self, field_name, field_obj): - field_obj.data_key = (field_obj.data_key or field_name).upper() - - -class Sample: - my_text: str - my_int: int - - -SampleSchema = marshmallow_dataclass.class_schema(Sample, base_schema=UppercaseSchema) - -SampleSchema().dump(Sample(my_text="warm words", my_int=1)) -# -> {"MY_TEXT": "warm words", "MY_INT": 1} -``` - -You can also pass `base_schema` to `marshmallow_dataclass.dataclass`. - -```python -@marshmallow_dataclass.dataclass(base_schema=UppercaseSchema) -class Sample: - my_text: str - my_int: int -``` - -See [marshmallow's documentation about extending `Schema`](https://marshmallow.readthedocs.io/en/stable/extending.html). - -### Custom type aliases - -This library allows you to specify [customized marshmallow fields](https://marshmallow.readthedocs.io/en/stable/custom_fields.html#creating-a-field-class) using python's Annoted type [PEP-593](https://peps.python.org/pep-0593/). - -```python -from typing import Annotated -import marshmallow.fields as mf -import marshmallow.validate as mv - -IPv4 = Annotated[str, mf.String(validate=mv.Regexp(r"^([0-9]{1,3}\\.){3}[0-9]{1,3}$"))] -``` - -You can also pass a marshmallow field class. - -```python -import marshmallow -from marshmallow_dataclass import NewType - -Email = Annotated[str, marshmallow.fields.Email] -``` - -For convenience, some custom types are provided: - -```python -from marshmallow_dataclass.typing import Email, Url -``` - -### Custom NewType declarations [__deprecated__] - -> NewType is deprecated in favor or type aliases using Annotated, as described above. - -This library exports a `NewType` function to create types that generate [customized marshmallow fields](https://marshmallow.readthedocs.io/en/stable/custom_fields.html#creating-a-field-class). - -Keyword arguments to `NewType` are passed to the marshmallow field constructor. - -```python -import marshmallow.validate -from marshmallow_dataclass import NewType - -IPv4 = NewType( - "IPv4", str, validate=marshmallow.validate.Regexp(r"^([0-9]{1,3}\\.){3}[0-9]{1,3}$") -) -``` - -You can also pass a marshmallow field to `NewType`. - -```python -import marshmallow -from marshmallow_dataclass import NewType - -Email = NewType("Email", str, field=marshmallow.fields.Email) -``` - -Note: if you are using `mypy`, you will notice that `mypy` throws an error if a variable defined with -`NewType` is used in a type annotation. To resolve this, add the `marshmallow_dataclass.mypy` plugin -to your `mypy` configuration, e.g.: - -```ini -[mypy] -plugins = marshmallow_dataclass.mypy -# ... -``` - -### `Meta` options - -[`Meta` options](https://marshmallow.readthedocs.io/en/stable/api_reference.html#marshmallow.Schema.Meta) are set the same way as a marshmallow `Schema`. - -```python -from marshmallow_dataclass import dataclass - - -@dataclass -class Point: - x: float - y: float - - class Meta: - ordered = True -``` - -## Documentation - -The project documentation is hosted on GitHub Pages: https://lovasoa.github.io/marshmallow_dataclass/ - -## Contributing - -To install this project and make changes to it locally, follow the instructions in [`CONTRIBUTING.md`](./CONTRIBUTING.md). +# marshmallow-dataclass + +[![Test Workflow Status (master branch)](https://img.shields.io/github/actions/workflow/status/lovasoa/marshmallow_dataclass/python-package.yml?branch=master&label=tests)](https://github.com/lovasoa/marshmallow_dataclass/actions/workflows/python-package.yml) +[![PyPI version](https://badge.fury.io/py/marshmallow-dataclass.svg)](https://badge.fury.io/py/marshmallow-dataclass) +[![marshmallow 3 compatible](https://badgen.net/badge/marshmallow/3)](https://marshmallow.readthedocs.io/en/latest/upgrading.html) +[![download stats](https://img.shields.io/pypi/dm/marshmallow-dataclass.svg)](https://pypistats.org/packages/marshmallow-dataclass) + +Automatic generation of [marshmallow](https://marshmallow.readthedocs.io/) schemas from dataclasses. + +```python +from dataclasses import dataclass, field +from typing import List, Optional + +import marshmallow_dataclass +import marshmallow.validate + + +@dataclass +class Building: + # field metadata is used to instantiate the marshmallow field + height: float = field(metadata={"validate": marshmallow.validate.Range(min=0)}) + name: str = field(default="anonymous") + + +@dataclass +class City: + name: Optional[str] + buildings: List[Building] = field(default_factory=list) + + +city_schema = marshmallow_dataclass.class_schema(City)() + +city = city_schema.load( + {"name": "Paris", "buildings": [{"name": "Eiffel Tower", "height": 324}]} +) +# => City(name='Paris', buildings=[Building(height=324.0, name='Eiffel Tower')]) + +city_dict = city_schema.dump(city) +# => {'name': 'Paris', 'buildings': [{'name': 'Eiffel Tower', 'height': 324.0}]} +``` + +## Why + +Using schemas in Python often means having both a class to represent your data and a class to represent its schema, which results in duplicated code that could fall out of sync. +As of Python 3.6, types can be defined for class members, which allows libraries to generate schemas automatically. + +Therefore, you can document your APIs in a way that allows you to statically check that the code matches the documentation. + +## Installation + +This package [is hosted on PyPI](https://pypi.org/project/marshmallow-dataclass/). + +```shell +pip3 install marshmallow-dataclass +``` + +```shell +pip3 install "marshmallow-dataclass" +``` + +### marshmallow 2 support + +`marshmallow-dataclass` no longer supports marshmallow 2. +Install `marshmallow_dataclass<6.0` if you need marshmallow 2 compatibility. + +## Usage + +Use the [`class_schema`](https://lovasoa.github.io/marshmallow_dataclass/html/marshmallow_dataclass.html#marshmallow_dataclass.class_schema) +function to generate a marshmallow [Schema](https://marshmallow.readthedocs.io/en/latest/api_reference.html#marshmallow.Schema) +class from a [`dataclass`](https://docs.python.org/3/library/dataclasses.html#dataclasses.dataclass). + +```python +from dataclasses import dataclass +from datetime import date + +import marshmallow_dataclass + + +@dataclass +class Person: + name: str + birth: date + + +PersonSchema = marshmallow_dataclass.class_schema(Person) +``` + +The type of your fields must be either basic +[types supported by marshmallow](https://marshmallow.readthedocs.io/en/stable/api_reference.html#marshmallow.Schema.TYPE_MAPPING) +(such as `float`, `str`, `bytes`, `datetime`, ...), `Union`, or other dataclasses. + +### Union (de)serialization coercion + +Typically the Union type; `Union[X, Y]` means—from a set theory perspective—either `X` or `Y`, i.e., an unordered set, howevever the order of the sub-types defines the precedence when attempting to ether deserialize or serialize the value per [here](https://github.com/lovasoa/marshmallow_dataclass/blob/master/marshmallow_dataclass/union_field.py). + +For example, + +```python +from typing import Union + +from dataclasses import dataclass + + +@dataclass +class Person: + name: str + age: Union[int, float] + + +PersonSchema = marshmallow_dataclass.class_schema(Person) +PersonSchema().load({"name": "jane", "age": 50.0}) +# => Person(name="jane", age=50) +``` + +will first (sucessfully) try to coerce `50.0` to an `int`. If coercion is not desired the `Any` type can be used with the caveat that values will not be type checked without additional [validation](https://marshmallow.readthedocs.io/en/stable/marshmallow.validate.html). + +### Customizing generated fields + +To pass arguments to the generated marshmallow fields (e.g., `validate`, `load_only`, `dump_only`, etc.), +pass them to the `metadata` argument of the +[`field`](https://docs.python.org/3/library/dataclasses.html#dataclasses.field) function. + +Note that starting with version 4, marshmallow will disallow passing arbitrary arguments, so any +additional metadata should itself be put in its own `metadata` dict: + +```python +from dataclasses import dataclass, field +import marshmallow_dataclass +import marshmallow.validate + + +@dataclass +class Person: + name: str = field( + metadata=dict( + load_only=True, metadata=dict(description="The person's first name") + ) + ) + height: float = field(metadata=dict(validate=marshmallow.validate.Range(min=0))) + + +PersonSchema = marshmallow_dataclass.class_schema(Person) +``` + +### `@dataclass` shortcut + +`marshmallow_dataclass` provides a `@dataclass` decorator that behaves like the standard library's +`@dataclasses.dataclass` and adds a `Schema` attribute with the generated marshmallow +[Schema](https://marshmallow.readthedocs.io/en/2.x-line/api_reference.html#marshmallow.Schema). + +```python +# Use marshmallow_dataclass's @dataclass shortcut +from marshmallow_dataclass import dataclass + + +@dataclass +class Point: + x: float + y: float + + +Point.Schema().dump(Point(4, 2)) +# => {'x': 4, 'y': 2} +``` + +Note: Since the `.Schema` property is added dynamically, it can confuse type checkers. +To avoid that, you can declare `Schema` as a [`ClassVar`](https://docs.python.org/3/library/typing.html#typing.ClassVar). + +```python +from typing import ClassVar, Type + +from marshmallow_dataclass import dataclass +from marshmallow import Schema + + +@dataclass +class Point: + x: float + y: float + Schema: ClassVar[Type[Schema]] = Schema +``` + +### Customizing the base Schema + +It is also possible to derive all schemas from your own +base Schema class +(see [marshmallow's documentation about extending `Schema`](https://marshmallow.readthedocs.io/en/stable/extending.html)). +This allows you to implement custom (de)serialization +behavior, for instance specifying a custom mapping between your classes and marshmallow fields, +or renaming fields on serialization. + +#### Custom mapping between classes and fields + +```python +class BaseSchema(marshmallow.Schema): + TYPE_MAPPING = {CustomType: CustomField, List: CustomListField} + + +class Sample: + my_custom: CustomType + my_custom_list: List[int] + + +SampleSchema = marshmallow_dataclass.class_schema(Sample, base_schema=BaseSchema) +# SampleSchema now serializes my_custom using the CustomField marshmallow field +# and serializes my_custom_list using the CustomListField marshmallow field +``` + +#### Renaming fields on serialization + +```python +import marshmallow +import marshmallow_dataclass + + +class UppercaseSchema(marshmallow.Schema): + """A Schema that marshals data with uppercased keys.""" + + def on_bind_field(self, field_name, field_obj): + field_obj.data_key = (field_obj.data_key or field_name).upper() + + +class Sample: + my_text: str + my_int: int + + +SampleSchema = marshmallow_dataclass.class_schema(Sample, base_schema=UppercaseSchema) + +SampleSchema().dump(Sample(my_text="warm words", my_int=1)) +# -> {"MY_TEXT": "warm words", "MY_INT": 1} +``` + +You can also pass `base_schema` to `marshmallow_dataclass.dataclass`. + +```python +@marshmallow_dataclass.dataclass(base_schema=UppercaseSchema) +class Sample: + my_text: str + my_int: int +``` + +See [marshmallow's documentation about extending `Schema`](https://marshmallow.readthedocs.io/en/stable/extending.html). + +### Custom type aliases + +This library allows you to specify [customized marshmallow fields](https://marshmallow.readthedocs.io/en/stable/custom_fields.html#creating-a-field-class) using python's Annoted type [PEP-593](https://peps.python.org/pep-0593/). + +```python +from typing import Annotated +import marshmallow.fields as mf +import marshmallow.validate as mv + +IPv4 = Annotated[str, mf.String(validate=mv.Regexp(r"^([0-9]{1,3}\\.){3}[0-9]{1,3}$"))] +``` + +You can also pass a marshmallow field class. + +```python +from typing import Annotated +import marshmallow +from marshmallow_dataclass import NewType + +Email = Annotated[str, marshmallow.fields.Email] +``` + +For convenience, some custom types are provided: + +```python +from marshmallow_dataclass.typing import Email, Url +``` + +When using Python 3.8, you must import `Annotated` from the typing_extensions package + +```python +# Version agnostic import code: +if sys.version_info >= (3, 9): + from typing import Annotated +else: + from typing_extensions import Annotated +``` + +### Custom NewType declarations [__deprecated__] + +> NewType is deprecated in favor or type aliases using Annotated, as described above. + +This library exports a `NewType` function to create types that generate [customized marshmallow fields](https://marshmallow.readthedocs.io/en/stable/custom_fields.html#creating-a-field-class). + +Keyword arguments to `NewType` are passed to the marshmallow field constructor. + +```python +import marshmallow.validate +from marshmallow_dataclass import NewType + +IPv4 = NewType( + "IPv4", str, validate=marshmallow.validate.Regexp(r"^([0-9]{1,3}\\.){3}[0-9]{1,3}$") +) +``` + +You can also pass a marshmallow field to `NewType`. + +```python +import marshmallow +from marshmallow_dataclass import NewType + +Email = NewType("Email", str, field=marshmallow.fields.Email) +``` + +Note: if you are using `mypy`, you will notice that `mypy` throws an error if a variable defined with +`NewType` is used in a type annotation. To resolve this, add the `marshmallow_dataclass.mypy` plugin +to your `mypy` configuration, e.g.: + +```ini +[mypy] +plugins = marshmallow_dataclass.mypy +# ... +``` + +### `Meta` options + +[`Meta` options](https://marshmallow.readthedocs.io/en/stable/api_reference.html#marshmallow.Schema.Meta) are set the same way as a marshmallow `Schema`. + +```python +from marshmallow_dataclass import dataclass + + +@dataclass +class Point: + x: float + y: float + + class Meta: + ordered = True +``` + +## Documentation + +The project documentation is hosted on GitHub Pages: https://lovasoa.github.io/marshmallow_dataclass/ + +## Contributing + +To install this project and make changes to it locally, follow the instructions in [`CONTRIBUTING.md`](./CONTRIBUTING.md). diff --git a/tox.ini b/tox.ini deleted file mode 100644 index 0ea84049..00000000 --- a/tox.ini +++ /dev/null @@ -1,27 +0,0 @@ -[tox] -requires = - tox>=4 -env_list = - py{38,39,310,311,312} - cover-report -set_env = - VIRTUALENV_DISCOVERY = pyenv - -[testenv] -deps = - coverage - pytest -commands = coverage run -p -m pytest tests -extras = dev -set_env = - VIRTUALENV_DISCOVERY = pyenv -depends = - cover-report: py{38,39,310,311,312} - -[testenv:cover-report] -skip_install = true -deps = coverage -commands = - coverage combine - coverage html - coverage report \ No newline at end of file From 45e2aff9278f07dca9d89958dee52e7c78da8ec2 Mon Sep 17 00:00:00 2001 From: Jeff Dairiki Date: Sun, 23 Jun 2024 10:37:45 -0700 Subject: [PATCH 12/12] fix: line-endings Somehow, the line-endings in README got switched to DOS (CRLF) rather than UNIX (LF). That resulted in an unreadable diff. --- README.md | 684 +++++++++++++++++++++++++++--------------------------- 1 file changed, 342 insertions(+), 342 deletions(-) diff --git a/README.md b/README.md index e70264a4..9fe68583 100644 --- a/README.md +++ b/README.md @@ -1,342 +1,342 @@ -# marshmallow-dataclass - -[![Test Workflow Status (master branch)](https://img.shields.io/github/actions/workflow/status/lovasoa/marshmallow_dataclass/python-package.yml?branch=master&label=tests)](https://github.com/lovasoa/marshmallow_dataclass/actions/workflows/python-package.yml) -[![PyPI version](https://badge.fury.io/py/marshmallow-dataclass.svg)](https://badge.fury.io/py/marshmallow-dataclass) -[![marshmallow 3 compatible](https://badgen.net/badge/marshmallow/3)](https://marshmallow.readthedocs.io/en/latest/upgrading.html) -[![download stats](https://img.shields.io/pypi/dm/marshmallow-dataclass.svg)](https://pypistats.org/packages/marshmallow-dataclass) - -Automatic generation of [marshmallow](https://marshmallow.readthedocs.io/) schemas from dataclasses. - -```python -from dataclasses import dataclass, field -from typing import List, Optional - -import marshmallow_dataclass -import marshmallow.validate - - -@dataclass -class Building: - # field metadata is used to instantiate the marshmallow field - height: float = field(metadata={"validate": marshmallow.validate.Range(min=0)}) - name: str = field(default="anonymous") - - -@dataclass -class City: - name: Optional[str] - buildings: List[Building] = field(default_factory=list) - - -city_schema = marshmallow_dataclass.class_schema(City)() - -city = city_schema.load( - {"name": "Paris", "buildings": [{"name": "Eiffel Tower", "height": 324}]} -) -# => City(name='Paris', buildings=[Building(height=324.0, name='Eiffel Tower')]) - -city_dict = city_schema.dump(city) -# => {'name': 'Paris', 'buildings': [{'name': 'Eiffel Tower', 'height': 324.0}]} -``` - -## Why - -Using schemas in Python often means having both a class to represent your data and a class to represent its schema, which results in duplicated code that could fall out of sync. -As of Python 3.6, types can be defined for class members, which allows libraries to generate schemas automatically. - -Therefore, you can document your APIs in a way that allows you to statically check that the code matches the documentation. - -## Installation - -This package [is hosted on PyPI](https://pypi.org/project/marshmallow-dataclass/). - -```shell -pip3 install marshmallow-dataclass -``` - -```shell -pip3 install "marshmallow-dataclass" -``` - -### marshmallow 2 support - -`marshmallow-dataclass` no longer supports marshmallow 2. -Install `marshmallow_dataclass<6.0` if you need marshmallow 2 compatibility. - -## Usage - -Use the [`class_schema`](https://lovasoa.github.io/marshmallow_dataclass/html/marshmallow_dataclass.html#marshmallow_dataclass.class_schema) -function to generate a marshmallow [Schema](https://marshmallow.readthedocs.io/en/latest/api_reference.html#marshmallow.Schema) -class from a [`dataclass`](https://docs.python.org/3/library/dataclasses.html#dataclasses.dataclass). - -```python -from dataclasses import dataclass -from datetime import date - -import marshmallow_dataclass - - -@dataclass -class Person: - name: str - birth: date - - -PersonSchema = marshmallow_dataclass.class_schema(Person) -``` - -The type of your fields must be either basic -[types supported by marshmallow](https://marshmallow.readthedocs.io/en/stable/api_reference.html#marshmallow.Schema.TYPE_MAPPING) -(such as `float`, `str`, `bytes`, `datetime`, ...), `Union`, or other dataclasses. - -### Union (de)serialization coercion - -Typically the Union type; `Union[X, Y]` means—from a set theory perspective—either `X` or `Y`, i.e., an unordered set, howevever the order of the sub-types defines the precedence when attempting to ether deserialize or serialize the value per [here](https://github.com/lovasoa/marshmallow_dataclass/blob/master/marshmallow_dataclass/union_field.py). - -For example, - -```python -from typing import Union - -from dataclasses import dataclass - - -@dataclass -class Person: - name: str - age: Union[int, float] - - -PersonSchema = marshmallow_dataclass.class_schema(Person) -PersonSchema().load({"name": "jane", "age": 50.0}) -# => Person(name="jane", age=50) -``` - -will first (sucessfully) try to coerce `50.0` to an `int`. If coercion is not desired the `Any` type can be used with the caveat that values will not be type checked without additional [validation](https://marshmallow.readthedocs.io/en/stable/marshmallow.validate.html). - -### Customizing generated fields - -To pass arguments to the generated marshmallow fields (e.g., `validate`, `load_only`, `dump_only`, etc.), -pass them to the `metadata` argument of the -[`field`](https://docs.python.org/3/library/dataclasses.html#dataclasses.field) function. - -Note that starting with version 4, marshmallow will disallow passing arbitrary arguments, so any -additional metadata should itself be put in its own `metadata` dict: - -```python -from dataclasses import dataclass, field -import marshmallow_dataclass -import marshmallow.validate - - -@dataclass -class Person: - name: str = field( - metadata=dict( - load_only=True, metadata=dict(description="The person's first name") - ) - ) - height: float = field(metadata=dict(validate=marshmallow.validate.Range(min=0))) - - -PersonSchema = marshmallow_dataclass.class_schema(Person) -``` - -### `@dataclass` shortcut - -`marshmallow_dataclass` provides a `@dataclass` decorator that behaves like the standard library's -`@dataclasses.dataclass` and adds a `Schema` attribute with the generated marshmallow -[Schema](https://marshmallow.readthedocs.io/en/2.x-line/api_reference.html#marshmallow.Schema). - -```python -# Use marshmallow_dataclass's @dataclass shortcut -from marshmallow_dataclass import dataclass - - -@dataclass -class Point: - x: float - y: float - - -Point.Schema().dump(Point(4, 2)) -# => {'x': 4, 'y': 2} -``` - -Note: Since the `.Schema` property is added dynamically, it can confuse type checkers. -To avoid that, you can declare `Schema` as a [`ClassVar`](https://docs.python.org/3/library/typing.html#typing.ClassVar). - -```python -from typing import ClassVar, Type - -from marshmallow_dataclass import dataclass -from marshmallow import Schema - - -@dataclass -class Point: - x: float - y: float - Schema: ClassVar[Type[Schema]] = Schema -``` - -### Customizing the base Schema - -It is also possible to derive all schemas from your own -base Schema class -(see [marshmallow's documentation about extending `Schema`](https://marshmallow.readthedocs.io/en/stable/extending.html)). -This allows you to implement custom (de)serialization -behavior, for instance specifying a custom mapping between your classes and marshmallow fields, -or renaming fields on serialization. - -#### Custom mapping between classes and fields - -```python -class BaseSchema(marshmallow.Schema): - TYPE_MAPPING = {CustomType: CustomField, List: CustomListField} - - -class Sample: - my_custom: CustomType - my_custom_list: List[int] - - -SampleSchema = marshmallow_dataclass.class_schema(Sample, base_schema=BaseSchema) -# SampleSchema now serializes my_custom using the CustomField marshmallow field -# and serializes my_custom_list using the CustomListField marshmallow field -``` - -#### Renaming fields on serialization - -```python -import marshmallow -import marshmallow_dataclass - - -class UppercaseSchema(marshmallow.Schema): - """A Schema that marshals data with uppercased keys.""" - - def on_bind_field(self, field_name, field_obj): - field_obj.data_key = (field_obj.data_key or field_name).upper() - - -class Sample: - my_text: str - my_int: int - - -SampleSchema = marshmallow_dataclass.class_schema(Sample, base_schema=UppercaseSchema) - -SampleSchema().dump(Sample(my_text="warm words", my_int=1)) -# -> {"MY_TEXT": "warm words", "MY_INT": 1} -``` - -You can also pass `base_schema` to `marshmallow_dataclass.dataclass`. - -```python -@marshmallow_dataclass.dataclass(base_schema=UppercaseSchema) -class Sample: - my_text: str - my_int: int -``` - -See [marshmallow's documentation about extending `Schema`](https://marshmallow.readthedocs.io/en/stable/extending.html). - -### Custom type aliases - -This library allows you to specify [customized marshmallow fields](https://marshmallow.readthedocs.io/en/stable/custom_fields.html#creating-a-field-class) using python's Annoted type [PEP-593](https://peps.python.org/pep-0593/). - -```python -from typing import Annotated -import marshmallow.fields as mf -import marshmallow.validate as mv - -IPv4 = Annotated[str, mf.String(validate=mv.Regexp(r"^([0-9]{1,3}\\.){3}[0-9]{1,3}$"))] -``` - -You can also pass a marshmallow field class. - -```python -from typing import Annotated -import marshmallow -from marshmallow_dataclass import NewType - -Email = Annotated[str, marshmallow.fields.Email] -``` - -For convenience, some custom types are provided: - -```python -from marshmallow_dataclass.typing import Email, Url -``` - -When using Python 3.8, you must import `Annotated` from the typing_extensions package - -```python -# Version agnostic import code: -if sys.version_info >= (3, 9): - from typing import Annotated -else: - from typing_extensions import Annotated -``` - -### Custom NewType declarations [__deprecated__] - -> NewType is deprecated in favor or type aliases using Annotated, as described above. - -This library exports a `NewType` function to create types that generate [customized marshmallow fields](https://marshmallow.readthedocs.io/en/stable/custom_fields.html#creating-a-field-class). - -Keyword arguments to `NewType` are passed to the marshmallow field constructor. - -```python -import marshmallow.validate -from marshmallow_dataclass import NewType - -IPv4 = NewType( - "IPv4", str, validate=marshmallow.validate.Regexp(r"^([0-9]{1,3}\\.){3}[0-9]{1,3}$") -) -``` - -You can also pass a marshmallow field to `NewType`. - -```python -import marshmallow -from marshmallow_dataclass import NewType - -Email = NewType("Email", str, field=marshmallow.fields.Email) -``` - -Note: if you are using `mypy`, you will notice that `mypy` throws an error if a variable defined with -`NewType` is used in a type annotation. To resolve this, add the `marshmallow_dataclass.mypy` plugin -to your `mypy` configuration, e.g.: - -```ini -[mypy] -plugins = marshmallow_dataclass.mypy -# ... -``` - -### `Meta` options - -[`Meta` options](https://marshmallow.readthedocs.io/en/stable/api_reference.html#marshmallow.Schema.Meta) are set the same way as a marshmallow `Schema`. - -```python -from marshmallow_dataclass import dataclass - - -@dataclass -class Point: - x: float - y: float - - class Meta: - ordered = True -``` - -## Documentation - -The project documentation is hosted on GitHub Pages: https://lovasoa.github.io/marshmallow_dataclass/ - -## Contributing - -To install this project and make changes to it locally, follow the instructions in [`CONTRIBUTING.md`](./CONTRIBUTING.md). +# marshmallow-dataclass + +[![Test Workflow Status (master branch)](https://img.shields.io/github/actions/workflow/status/lovasoa/marshmallow_dataclass/python-package.yml?branch=master&label=tests)](https://github.com/lovasoa/marshmallow_dataclass/actions/workflows/python-package.yml) +[![PyPI version](https://badge.fury.io/py/marshmallow-dataclass.svg)](https://badge.fury.io/py/marshmallow-dataclass) +[![marshmallow 3 compatible](https://badgen.net/badge/marshmallow/3)](https://marshmallow.readthedocs.io/en/latest/upgrading.html) +[![download stats](https://img.shields.io/pypi/dm/marshmallow-dataclass.svg)](https://pypistats.org/packages/marshmallow-dataclass) + +Automatic generation of [marshmallow](https://marshmallow.readthedocs.io/) schemas from dataclasses. + +```python +from dataclasses import dataclass, field +from typing import List, Optional + +import marshmallow_dataclass +import marshmallow.validate + + +@dataclass +class Building: + # field metadata is used to instantiate the marshmallow field + height: float = field(metadata={"validate": marshmallow.validate.Range(min=0)}) + name: str = field(default="anonymous") + + +@dataclass +class City: + name: Optional[str] + buildings: List[Building] = field(default_factory=list) + + +city_schema = marshmallow_dataclass.class_schema(City)() + +city = city_schema.load( + {"name": "Paris", "buildings": [{"name": "Eiffel Tower", "height": 324}]} +) +# => City(name='Paris', buildings=[Building(height=324.0, name='Eiffel Tower')]) + +city_dict = city_schema.dump(city) +# => {'name': 'Paris', 'buildings': [{'name': 'Eiffel Tower', 'height': 324.0}]} +``` + +## Why + +Using schemas in Python often means having both a class to represent your data and a class to represent its schema, which results in duplicated code that could fall out of sync. +As of Python 3.6, types can be defined for class members, which allows libraries to generate schemas automatically. + +Therefore, you can document your APIs in a way that allows you to statically check that the code matches the documentation. + +## Installation + +This package [is hosted on PyPI](https://pypi.org/project/marshmallow-dataclass/). + +```shell +pip3 install marshmallow-dataclass +``` + +```shell +pip3 install "marshmallow-dataclass" +``` + +### marshmallow 2 support + +`marshmallow-dataclass` no longer supports marshmallow 2. +Install `marshmallow_dataclass<6.0` if you need marshmallow 2 compatibility. + +## Usage + +Use the [`class_schema`](https://lovasoa.github.io/marshmallow_dataclass/html/marshmallow_dataclass.html#marshmallow_dataclass.class_schema) +function to generate a marshmallow [Schema](https://marshmallow.readthedocs.io/en/latest/api_reference.html#marshmallow.Schema) +class from a [`dataclass`](https://docs.python.org/3/library/dataclasses.html#dataclasses.dataclass). + +```python +from dataclasses import dataclass +from datetime import date + +import marshmallow_dataclass + + +@dataclass +class Person: + name: str + birth: date + + +PersonSchema = marshmallow_dataclass.class_schema(Person) +``` + +The type of your fields must be either basic +[types supported by marshmallow](https://marshmallow.readthedocs.io/en/stable/api_reference.html#marshmallow.Schema.TYPE_MAPPING) +(such as `float`, `str`, `bytes`, `datetime`, ...), `Union`, or other dataclasses. + +### Union (de)serialization coercion + +Typically the Union type; `Union[X, Y]` means—from a set theory perspective—either `X` or `Y`, i.e., an unordered set, howevever the order of the sub-types defines the precedence when attempting to ether deserialize or serialize the value per [here](https://github.com/lovasoa/marshmallow_dataclass/blob/master/marshmallow_dataclass/union_field.py). + +For example, + +```python +from typing import Union + +from dataclasses import dataclass + + +@dataclass +class Person: + name: str + age: Union[int, float] + + +PersonSchema = marshmallow_dataclass.class_schema(Person) +PersonSchema().load({"name": "jane", "age": 50.0}) +# => Person(name="jane", age=50) +``` + +will first (sucessfully) try to coerce `50.0` to an `int`. If coercion is not desired the `Any` type can be used with the caveat that values will not be type checked without additional [validation](https://marshmallow.readthedocs.io/en/stable/marshmallow.validate.html). + +### Customizing generated fields + +To pass arguments to the generated marshmallow fields (e.g., `validate`, `load_only`, `dump_only`, etc.), +pass them to the `metadata` argument of the +[`field`](https://docs.python.org/3/library/dataclasses.html#dataclasses.field) function. + +Note that starting with version 4, marshmallow will disallow passing arbitrary arguments, so any +additional metadata should itself be put in its own `metadata` dict: + +```python +from dataclasses import dataclass, field +import marshmallow_dataclass +import marshmallow.validate + + +@dataclass +class Person: + name: str = field( + metadata=dict( + load_only=True, metadata=dict(description="The person's first name") + ) + ) + height: float = field(metadata=dict(validate=marshmallow.validate.Range(min=0))) + + +PersonSchema = marshmallow_dataclass.class_schema(Person) +``` + +### `@dataclass` shortcut + +`marshmallow_dataclass` provides a `@dataclass` decorator that behaves like the standard library's +`@dataclasses.dataclass` and adds a `Schema` attribute with the generated marshmallow +[Schema](https://marshmallow.readthedocs.io/en/2.x-line/api_reference.html#marshmallow.Schema). + +```python +# Use marshmallow_dataclass's @dataclass shortcut +from marshmallow_dataclass import dataclass + + +@dataclass +class Point: + x: float + y: float + + +Point.Schema().dump(Point(4, 2)) +# => {'x': 4, 'y': 2} +``` + +Note: Since the `.Schema` property is added dynamically, it can confuse type checkers. +To avoid that, you can declare `Schema` as a [`ClassVar`](https://docs.python.org/3/library/typing.html#typing.ClassVar). + +```python +from typing import ClassVar, Type + +from marshmallow_dataclass import dataclass +from marshmallow import Schema + + +@dataclass +class Point: + x: float + y: float + Schema: ClassVar[Type[Schema]] = Schema +``` + +### Customizing the base Schema + +It is also possible to derive all schemas from your own +base Schema class +(see [marshmallow's documentation about extending `Schema`](https://marshmallow.readthedocs.io/en/stable/extending.html)). +This allows you to implement custom (de)serialization +behavior, for instance specifying a custom mapping between your classes and marshmallow fields, +or renaming fields on serialization. + +#### Custom mapping between classes and fields + +```python +class BaseSchema(marshmallow.Schema): + TYPE_MAPPING = {CustomType: CustomField, List: CustomListField} + + +class Sample: + my_custom: CustomType + my_custom_list: List[int] + + +SampleSchema = marshmallow_dataclass.class_schema(Sample, base_schema=BaseSchema) +# SampleSchema now serializes my_custom using the CustomField marshmallow field +# and serializes my_custom_list using the CustomListField marshmallow field +``` + +#### Renaming fields on serialization + +```python +import marshmallow +import marshmallow_dataclass + + +class UppercaseSchema(marshmallow.Schema): + """A Schema that marshals data with uppercased keys.""" + + def on_bind_field(self, field_name, field_obj): + field_obj.data_key = (field_obj.data_key or field_name).upper() + + +class Sample: + my_text: str + my_int: int + + +SampleSchema = marshmallow_dataclass.class_schema(Sample, base_schema=UppercaseSchema) + +SampleSchema().dump(Sample(my_text="warm words", my_int=1)) +# -> {"MY_TEXT": "warm words", "MY_INT": 1} +``` + +You can also pass `base_schema` to `marshmallow_dataclass.dataclass`. + +```python +@marshmallow_dataclass.dataclass(base_schema=UppercaseSchema) +class Sample: + my_text: str + my_int: int +``` + +See [marshmallow's documentation about extending `Schema`](https://marshmallow.readthedocs.io/en/stable/extending.html). + +### Custom type aliases + +This library allows you to specify [customized marshmallow fields](https://marshmallow.readthedocs.io/en/stable/custom_fields.html#creating-a-field-class) using python's Annoted type [PEP-593](https://peps.python.org/pep-0593/). + +```python +from typing import Annotated +import marshmallow.fields as mf +import marshmallow.validate as mv + +IPv4 = Annotated[str, mf.String(validate=mv.Regexp(r"^([0-9]{1,3}\\.){3}[0-9]{1,3}$"))] +``` + +You can also pass a marshmallow field class. + +```python +from typing import Annotated +import marshmallow +from marshmallow_dataclass import NewType + +Email = Annotated[str, marshmallow.fields.Email] +``` + +For convenience, some custom types are provided: + +```python +from marshmallow_dataclass.typing import Email, Url +``` + +When using Python 3.8, you must import `Annotated` from the typing_extensions package + +```python +# Version agnostic import code: +if sys.version_info >= (3, 9): + from typing import Annotated +else: + from typing_extensions import Annotated +``` + +### Custom NewType declarations [__deprecated__] + +> NewType is deprecated in favor or type aliases using Annotated, as described above. + +This library exports a `NewType` function to create types that generate [customized marshmallow fields](https://marshmallow.readthedocs.io/en/stable/custom_fields.html#creating-a-field-class). + +Keyword arguments to `NewType` are passed to the marshmallow field constructor. + +```python +import marshmallow.validate +from marshmallow_dataclass import NewType + +IPv4 = NewType( + "IPv4", str, validate=marshmallow.validate.Regexp(r"^([0-9]{1,3}\\.){3}[0-9]{1,3}$") +) +``` + +You can also pass a marshmallow field to `NewType`. + +```python +import marshmallow +from marshmallow_dataclass import NewType + +Email = NewType("Email", str, field=marshmallow.fields.Email) +``` + +Note: if you are using `mypy`, you will notice that `mypy` throws an error if a variable defined with +`NewType` is used in a type annotation. To resolve this, add the `marshmallow_dataclass.mypy` plugin +to your `mypy` configuration, e.g.: + +```ini +[mypy] +plugins = marshmallow_dataclass.mypy +# ... +``` + +### `Meta` options + +[`Meta` options](https://marshmallow.readthedocs.io/en/stable/api_reference.html#marshmallow.Schema.Meta) are set the same way as a marshmallow `Schema`. + +```python +from marshmallow_dataclass import dataclass + + +@dataclass +class Point: + x: float + y: float + + class Meta: + ordered = True +``` + +## Documentation + +The project documentation is hosted on GitHub Pages: https://lovasoa.github.io/marshmallow_dataclass/ + +## Contributing + +To install this project and make changes to it locally, follow the instructions in [`CONTRIBUTING.md`](./CONTRIBUTING.md).