From 1c69de23692374af50bd46a49ece6b95320399f1 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Wed, 11 Aug 2021 11:40:37 -0400 Subject: [PATCH 1/6] enable and pass mypy --strict --- mypy.ini | 1 + src/desert/__init__.py | 131 +++++++++++++++++++++++++++---- src/desert/_make.py | 31 +++++--- tests/test_make.py | 173 ++++++++++++++++++++++++----------------- 4 files changed, 237 insertions(+), 99 deletions(-) diff --git a/mypy.ini b/mypy.ini index 976ba02..ea11601 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1,2 +1,3 @@ [mypy] ignore_missing_imports = True +strict = True diff --git a/src/desert/__init__.py b/src/desert/__init__.py index cee2bcb..23f019d 100644 --- a/src/desert/__init__.py +++ b/src/desert/__init__.py @@ -12,8 +12,11 @@ import attr._make +T = t.TypeVar("T") + + def schema( - cls: t.Type, many: bool = False, meta: t.Dict[str, t.Any] = {} + cls: type, many: bool = False, meta: t.Dict[str, t.Any] = {} ) -> marshmallow.Schema: """Build a marshmallow schema *instance* for the class. @@ -29,7 +32,7 @@ def schema( def schema_class( - cls: t.Type, meta: t.Dict[str, t.Any] = {} + cls: type, meta: t.Dict[str, t.Any] = {} ) -> t.Type[marshmallow.Schema]: """Build a marshmallow schema *class* for the class. @@ -46,7 +49,7 @@ def schema_class( def metadata( field: marshmallow.fields.Field, -) -> t.Dict["desert._make._DesertSentinel", t.Dict[t.Any, marshmallow.fields.Field]]: +) -> t.Dict[object, object]: """Specify a marshmallow field in the field metadata. .. code-block:: python @@ -56,7 +59,53 @@ def metadata( return {desert._make._DESERT_SENTINEL: {"marshmallow_field": field}} -def field(marshmallow_field: marshmallow.fields.Field, **kw) -> dataclasses.Field: +# TODO: maybe deprecate and rename metadata() +create_metadata = metadata + + +# These overloads lie about their return type just as both attrs and dataclasses +# do so as to support the normal usage of `attribute: int = field()` +@t.overload +def field( + marshmallow_field: marshmallow.fields.Field, + *, + default: T, + metadata: t.Optional[t.Mapping[object, object]] = None, + **kw: object, +) -> T: + ... + + +@t.overload +def field( + marshmallow_field: marshmallow.fields.Field, + *, + default_factory: t.Callable[[], T], + metadata: t.Optional[t.Mapping[object, object]] = None, + **kw: object, +) -> T: + ... + + +@t.overload +def field( + marshmallow_field: marshmallow.fields.Field, + *, + metadata: t.Optional[t.Mapping[object, object]] = None, + **kw: object, +) -> object: + ... + + +# The return type hint of object is certainly a lie but fits a lot better with +# the normal use of `x: int = desert.field()`. Both dataclasses and attrs +# prioritize hinting for this usage as well. Perhaps someday we'll have a +# plugin that indicates the actual type. +def field( + marshmallow_field: marshmallow.fields.Field, + metadata: t.Optional[t.Mapping[object, object]] = None, + **kw: object, +) -> object: """Specify a marshmallow field in the metadata for a ``dataclasses.dataclass``. .. code-block:: python @@ -65,15 +114,61 @@ def field(marshmallow_field: marshmallow.fields.Field, **kw) -> dataclasses.Fiel class A: x: int = desert.field(marshmallow.fields.Int()) """ - meta = metadata(marshmallow_field) - meta.update(kw.pop("metadata", {})) - # typeshed hints it as Mapping[str, Any] without any obvious reason - # https://github.com/python/typeshed/blob/95a45eb4abd0c25849268983cb614e3bf6b9b264/stdlib/dataclasses.pyi#L81 - # https://github.com/python/typeshed/pull/5823 - return dataclasses.field(**kw, metadata=meta) # type: ignore[arg-type] - - -def ib(marshmallow_field: marshmallow.fields.Field, **kw) -> attr._make._CountingAttr: + if metadata is None: + metadata = {} + + meta: t.Dict[object, object] = create_metadata(marshmallow_field) + meta.update(metadata) + + # call-overload and new_field intermediary: + # https://github.com/python/typeshed/pull/5823 + new_field: dataclasses.Field[object] = dataclasses.field(**kw, metadata=meta) # type: ignore[call-overload] + return new_field + + +# These overloads lie about their return type just as both attrs and dataclasses +# do so as to support the normal usage of `attribute: int = field()` +@t.overload +def ib( + marshmallow_field: marshmallow.fields.Field, + *, + default: t.Union[T, t.Callable[[], T]], + metadata: t.Optional[t.Mapping[object, object]] = None, + **kw: object, +) -> T: + ... + + +@t.overload +def ib( + marshmallow_field: marshmallow.fields.Field, + *, + factory: t.Callable[[], T], + metadata: t.Optional[t.Mapping[object, object]] = None, + **kw: object, +) -> T: + ... + + +@t.overload +def ib( + marshmallow_field: marshmallow.fields.Field, + *, + metadata: t.Optional[t.Mapping[object, object]] = None, + **kw: object, +) -> object: + ... + + +# The return type hint of object is certainly a lie but fits a lot better with +# the normal use of `x: int = desert.ib()`. Both dataclasses and attrs +# prioritize hinting for this usage as well. Perhaps someday we'll have a +# plugin that indicates the actual type. +def ib( + marshmallow_field: marshmallow.fields.Field, + metadata: t.Optional[t.Mapping[object, object]] = None, + **kw: object, +) -> object: """Specify a marshmallow field in the metadata for an ``attr.dataclass``. .. code-block:: python @@ -82,9 +177,13 @@ def ib(marshmallow_field: marshmallow.fields.Field, **kw) -> attr._make._Countin class A: x: int = desert.ib(marshmallow.fields.Int()) """ - meta = metadata(marshmallow_field) - meta.update(kw.pop("metadata", {})) - return attr.ib(**kw, metadata=meta) + if metadata is None: + metadata = {} + + meta: t.Dict[object, object] = create_metadata(marshmallow_field) + meta.update(metadata) + new_field: attr._make._CountingAttr = attr.ib(**kw, metadata=meta) # type: ignore[call-overload] + return new_field __version__ = desert._version.__version__ diff --git a/src/desert/_make.py b/src/desert/_make.py index 13877e1..726cb0c 100644 --- a/src/desert/_make.py +++ b/src/desert/_make.py @@ -97,7 +97,9 @@ def class_schema( ``marshmallow_field`` key in the metadata dictionary. """ - fields: t.Union[t.Tuple[dataclasses.Field, ...], t.Tuple[attr.Attribute, ...]] + fields: t.Union[ + t.Tuple[dataclasses.Field[object], ...], t.Tuple[attr.Attribute[object], ...] + ] if not isinstance(clazz, type): raise desert.exceptions.UnknownType( @@ -161,7 +163,7 @@ def class_schema( class VariadicTuple(marshmallow.fields.List): """Homogenous tuple with variable number of entries.""" - def _deserialize(self, *args, **kwargs): + def _deserialize(self, *args: object, **kwargs: object) -> t.Tuple[object, ...]: # type: ignore[override] return tuple(super()._deserialize(*args, **kwargs)) @@ -172,7 +174,9 @@ def only(items: t.Iterable[T]) -> T: def field_for_schema( - typ: type, default=marshmallow.missing, metadata: t.Mapping[t.Any, t.Any] = None + typ: type, + default: object = marshmallow.missing, + metadata: t.Optional[t.Mapping[t.Any, t.Any]] = None, ) -> marshmallow.fields.Field: """ Get a marshmallow Field corresponding to the given python type. @@ -221,7 +225,9 @@ def field_for_schema( field = None # If the field was already defined by the user - predefined_field = desert_metadata.get("marshmallow_field") + predefined_field = t.cast( + marshmallow.fields.Field, desert_metadata.get("marshmallow_field") + ) if predefined_field: field = predefined_field @@ -242,7 +248,7 @@ def field_for_schema( field = marshmallow.fields.List(field_for_schema(arguments[0])) if origin in (tuple, t.Tuple) and Ellipsis not in arguments: - field = marshmallow.fields.Tuple( + field = marshmallow.fields.Tuple( # type: ignore[no-untyped-call] tuple(field_for_schema(arg) for arg in arguments) ) elif origin in (tuple, t.Tuple) and Ellipsis in arguments: @@ -307,21 +313,24 @@ def field_for_schema( def _base_schema(clazz: type) -> t.Type[marshmallow.Schema]: class BaseSchema(marshmallow.Schema): @marshmallow.post_load - def make_data_class(self, data, **_): + def make_data_class(self, data: t.Mapping[str, object], **_: object) -> object: return clazz(**data) return BaseSchema -def _get_field_default(field: t.Union[dataclasses.Field, attr.Attribute]): +def _get_field_default( + field: t.Union[dataclasses.Field[object], "attr.Attribute[object]"], +) -> object: """ Return a marshmallow default value given a dataclass default value >>> _get_field_default(dataclasses.field()) """ if isinstance(field, dataclasses.Field): - # https://github.com/python/mypy/issues/10750 - if field.default_factory != dataclasses.MISSING: # type: ignore[misc] + # misc: https://github.com/python/mypy/issues/10750 + # comparison-overlap: https://github.com/python/typeshed/pull/5900 + if field.default_factory != dataclasses.MISSING: # type: ignore[misc,comparison-overlap] return dataclasses.MISSING if field.default is dataclasses.MISSING: return marshmallow.missing @@ -333,9 +342,9 @@ def _get_field_default(field: t.Union[dataclasses.Field, attr.Attribute]): # attrs specifically doesn't support this so as to support the # primary use case. # https://github.com/python-attrs/attrs/blob/38580632ceac1cd6e477db71e1d190a4130beed4/src/attr/__init__.pyi#L63-L65 - if field.default.takes_self: # type: ignore[union-attr] + if field.default.takes_self: # type: ignore[attr-defined] return attr.NOTHING - return field.default.factory # type: ignore[union-attr] + return field.default.factory # type: ignore[attr-defined] return field.default else: raise TypeError(field) diff --git a/tests/test_make.py b/tests/test_make.py index a8e00b4..1437830 100644 --- a/tests/test_make.py +++ b/tests/test_make.py @@ -10,18 +10,22 @@ import attr import marshmallow import marshmallow.fields + +# https://github.com/pytest-dev/pytest/issues/7469 +import _pytest.fixtures import pytest +import typing_extensions import desert -@attr.s(frozen=True, order=False) +@attr.frozen(order=False) class DataclassModule: """Implementation of a dataclass module like attr or dataclasses.""" - dataclass = attr.ib() - field = attr.ib() - fields = attr.ib() + dataclass: t.Callable[[type], type] + field: t.Callable[..., t.Any] = attr.ib() + fields: t.Callable[[type], object] = attr.ib() @pytest.fixture( @@ -36,15 +40,17 @@ class DataclassModule: ], ids=["attrs", "dataclasses"], ) -def dataclass_param(request): +def dataclass_param(request: _pytest.fixtures.SubRequest) -> DataclassModule: """Parametrize over both implementations of the @dataclass decorator.""" - return request.param + module = t.cast(DataclassModule, request.param) + return module -def _assert_load( - schema: marshmallow.Schema, loaded: t.Any, dumped: t.Dict[t.Any, t.Any] -) -> None: - assert schema.load(dumped) == loaded +class AssertLoadDumpProtocol(typing_extensions.Protocol): + def __call__( + self, schema: marshmallow.Schema, loaded: t.Any, dumped: t.Dict[t.Any, t.Any] + ) -> None: + ... def _assert_dump( @@ -53,6 +59,12 @@ def _assert_dump( assert schema.dump(loaded) == dumped +def _assert_load( + schema: marshmallow.Schema, loaded: t.Any, dumped: t.Dict[t.Any, t.Any] +) -> None: + assert schema.load(dumped) == loaded + + def _assert_dump_load( schema: marshmallow.Schema, loaded: t.Any, dumped: t.Dict[t.Any, t.Any] ) -> None: @@ -70,7 +82,7 @@ def fixture_from_dict( id_to_value: t.Mapping[ str, t.Callable[[marshmallow.Schema, t.Dict[t.Any, t.Any], t.Any], None] ], -): +) -> _pytest.fixtures._FixtureFunction: """ Create fixture parametrized to yield each value and labeled with the corresponding ID. @@ -84,10 +96,12 @@ def fixture_from_dict( """ @pytest.fixture(name=name, params=id_to_value.values(), ids=id_to_value.keys()) - def fixture(request): + def fixture(request: _pytest.fixtures.SubRequest) -> object: return request.param - return fixture + # This looks right to me but mypy says: + # error: Incompatible return value type (got "Callable[[SubRequest], object]", expected "_pytest.fixtures._FixtureFunction") [return-value] + return fixture # type: ignore[return-value] _assert_dump_load = fixture_from_dict( @@ -101,7 +115,7 @@ def fixture(request): ) -def test_simple(module): +def test_simple(module: DataclassModule) -> None: """Load dict into a dataclass instance.""" @module.dataclass @@ -110,10 +124,10 @@ class A: data = desert.schema_class(A)().load(data={"x": 5}) - assert data == A(x=5) + assert data == A(x=5) # type: ignore[call-arg] -def test_validation(module): +def test_validation(module: DataclassModule) -> None: """Passing the wrong keys will raise ValidationError.""" @module.dataclass @@ -125,7 +139,7 @@ class A: schema.load({"y": 5}) -def test_not_a_dataclass(module): +def test_not_a_dataclass(module: DataclassModule) -> None: """Raises when object is not a dataclass.""" class A: @@ -135,7 +149,7 @@ class A: desert.schema_class(A) -def test_set_default(module): +def test_set_default(module: DataclassModule) -> None: """Setting a default value in the dataclass makes passing it optional.""" @module.dataclass @@ -144,13 +158,13 @@ class A: schema = desert.schema_class(A)() data = schema.load({"x": 1}) - assert data == A(1) + assert data == A(1) # type: ignore[call-arg] data = schema.load({}) - assert data == A(1) + assert data == A(1) # type: ignore[call-arg] -def test_list(module): +def test_list(module: DataclassModule) -> None: """Build a generic list *without* setting a factory on the dataclass.""" @module.dataclass @@ -159,10 +173,10 @@ class A: schema = desert.schema_class(A)() data = schema.load({"y": [1]}) - assert data == A([1]) + assert data == A([1]) # type: ignore[call-arg] -def test_dict(module): +def test_dict(module: DataclassModule) -> None: """Build a dict without setting a factory on the dataclass.""" @module.dataclass @@ -172,10 +186,10 @@ class A: schema = desert.schema_class(A)() data = schema.load({"y": {1: 2, 3: 4}}) - assert data == A({1: 2, 3: 4}) + assert data == A({1: 2, 3: 4}) # type: ignore[call-arg] -def test_nested(module): +def test_nested(module: DataclassModule) -> None: """One object can hold instances of another.""" @module.dataclass @@ -188,10 +202,10 @@ class B: data = desert.schema_class(B)().load({"y": {"x": 5}}) - assert data == B(A(5)) + assert data == B(A(5)) # type: ignore[call-arg] -def test_optional(module): +def test_optional(module: DataclassModule) -> None: """Setting an optional type makes the default None.""" @module.dataclass @@ -199,10 +213,10 @@ class A: x: t.Optional[int] data = desert.schema_class(A)().load({}) - assert data == A(None) + assert data == A(None) # type: ignore[call-arg] -def test_optional_present(module): +def test_optional_present(module: DataclassModule) -> None: """Setting an optional type allows passing None.""" @module.dataclass @@ -210,10 +224,10 @@ class A: x: t.Optional[int] data = desert.schema_class(A)().load({"x": None}) - assert data == A(None) + assert data == A(None) # type: ignore[call-arg] -def test_custom_field(module): +def test_custom_field(module: DataclassModule) -> None: @module.dataclass class A: x: str = module.field( @@ -224,15 +238,15 @@ class A: dt = datetime.datetime(year=2019, month=10, day=21, hour=10, minute=25, second=00) schema = desert.schema(A) - assert schema.load({"x": timestring}) == A(x=dt) + assert schema.load({"x": timestring}) == A(x=dt) # type: ignore[call-arg] -def test_concise_dataclasses_field(): +def test_concise_dataclasses_field() -> None: """Concisely create a dataclasses.Field.""" @dataclasses.dataclass class A: - x: str = desert.field(marshmallow.fields.NaiveDateTime()) + x: datetime.datetime = desert.field(marshmallow.fields.NaiveDateTime()) # type: ignore[assignment] timestring = "2019-10-21T10:25:00" dt = datetime.datetime(year=2019, month=10, day=21, hour=10, minute=25, second=00) @@ -241,12 +255,12 @@ class A: assert schema.load({"x": timestring}) == A(x=dt) -def test_concise_attrib(): +def test_concise_attrib() -> None: """Concisely create an attr.ib()""" @attr.dataclass class A: - x: str = desert.ib(marshmallow.fields.NaiveDateTime()) + x: datetime.datetime = desert.ib(marshmallow.fields.NaiveDateTime()) # type: ignore[assignment] timestring = "2019-10-21T10:25:00" dt = datetime.datetime(year=2019, month=10, day=21, hour=10, minute=25, second=00) @@ -255,12 +269,12 @@ class A: assert schema.load({"x": timestring}) == A(x=dt) -def test_concise_field_metadata(): +def test_concise_field_metadata() -> None: """Concisely create a dataclasses.Field with metadata.""" @dataclasses.dataclass class A: - x: str = desert.field(marshmallow.fields.NaiveDateTime(), metadata={"foo": 1}) + x: datetime.datetime = desert.field(marshmallow.fields.NaiveDateTime(), metadata={"foo": 1}) # type: ignore[assignment] timestring = "2019-10-21T10:25:00" dt = datetime.datetime(year=2019, month=10, day=21, hour=10, minute=25, second=00) @@ -270,12 +284,12 @@ class A: assert dataclasses.fields(A)[0].metadata["foo"] == 1 -def test_concise_attrib_metadata(): +def test_concise_attrib_metadata() -> None: """Concisely create an attr.ib() with metadata.""" @attr.dataclass class A: - x: str = desert.ib(marshmallow.fields.NaiveDateTime(), metadata={"foo": 1}) + x: datetime.datetime = desert.ib(marshmallow.fields.NaiveDateTime(), metadata={"foo": 1}) # type: ignore[assignment] timestring = "2019-10-21T10:25:00" dt = datetime.datetime(year=2019, month=10, day=21, hour=10, minute=25, second=00) @@ -285,7 +299,7 @@ class A: assert attr.fields(A).x.metadata["foo"] == 1 -def test_non_init(module): +def test_non_init(module: DataclassModule) -> None: """Non-init attributes are not included in schema""" @module.dataclass @@ -298,7 +312,7 @@ class A: assert "y" not in schema.fields -def test_metadata_marshmallow_field_loads(module): +def test_metadata_marshmallow_field_loads(module: DataclassModule) -> None: """Marshmallow field can be specified via metadata dict""" @module.dataclass @@ -309,18 +323,22 @@ class A: schema = desert.schema_class(A)() - assert schema.loads('{"x": "1.3"}') == A(decimal.Decimal("1.3")) + assert schema.loads('{"x": "1.3"}') == A(decimal.Decimal("1.3")) # type: ignore[call-arg] -def test_get_field_default_raises_for_non_field(): +def test_get_field_default_raises_for_non_field() -> None: """Not attrs and not dataclasses field raises""" with pytest.raises(TypeError, match=re.escape("None")): - desert._make._get_field_default(field=None) + desert._make._get_field_default(field=None) # type: ignore[arg-type] @pytest.mark.parametrize(argnames=["value"], argvalues=[["X"], [5]]) -def test_union(module, value, assert_dump_load): +def test_union( + module: DataclassModule, + value: t.List[object], + assert_dump_load: AssertLoadDumpProtocol, +) -> None: """Deserialize one of several types.""" @module.dataclass @@ -330,12 +348,15 @@ class A: schema = desert.schema_class(A)() dumped = {"x": value} - loaded = A(value) + loaded = A(value) # type: ignore[call-arg] assert_dump_load(schema=schema, loaded=loaded, dumped=dumped) -def test_enum(module, assert_dump_load): +def test_enum( + module: DataclassModule, + assert_dump_load: AssertLoadDumpProtocol, +) -> None: """Deserialize an enum object.""" class Color(enum.Enum): @@ -348,12 +369,15 @@ class A: schema = desert.schema_class(A)() dumped = {"x": "RED"} - loaded = A(Color.RED) + loaded = A(Color.RED) # type: ignore[call-arg] assert_dump_load(schema=schema, loaded=loaded, dumped=dumped) -def test_tuple(module, assert_dump_load): +def test_tuple( + module: DataclassModule, + assert_dump_load: AssertLoadDumpProtocol, +) -> None: """Round trip a tuple. The tuple is converted to list only for dumps(), not during dump(). @@ -365,12 +389,12 @@ class A: schema = desert.schema_class(A)() dumped = {"x": (1, False)} - loaded = A(x=(1, False)) + loaded = A(x=(1, False)) # type: ignore[call-arg] assert_dump_load(schema=schema, loaded=loaded, dumped=dumped) -def test_attr_factory(): +def test_attr_factory() -> None: """Attrs default factory instantiates the factory type if no value is passed.""" @attr.dataclass @@ -381,7 +405,7 @@ class A: assert data == A([]) -def test_dataclasses_factory(): +def test_dataclasses_factory() -> None: """Dataclasses default factory instantiates the factory type if no value is passed.""" @dataclasses.dataclass @@ -392,7 +416,10 @@ class A: assert data == A([]) -def test_newtype(module, assert_dump_load): +def test_newtype( + module: DataclassModule, + assert_dump_load: AssertLoadDumpProtocol, +) -> None: """An instance of NewType delegates to its supertype.""" MyInt = t.NewType("MyInt", int) @@ -403,7 +430,7 @@ class A: schema = desert.schema_class(A)() dumped = {"x": 1} - loaded = A(x=1) + loaded = A(x=1) # type: ignore[call-arg] assert_dump_load(schema=schema, loaded=loaded, dumped=dumped) @@ -415,7 +442,9 @@ class A: + "See https://github.com/lovasoa/marshmallow_dataclass/issues/13" ), ) -def test_forward_reference(module, assert_dump_load): # pragma: no cover +def test_forward_reference( + module: DataclassModule, assert_dump_load: AssertLoadDumpProtocol, +) -> None: # pragma: no cover """Build schemas from classes that are defined below their containing class.""" @module.dataclass @@ -428,7 +457,7 @@ class B: schema = desert.schema_class(A)() dumped = {"x": {"y": 1}} - loaded = A((B(1))) + loaded = A((B(1))) # type: ignore[call-arg] assert_dump_load(schema=schema, loaded=loaded, dumped=dumped) @@ -439,13 +468,13 @@ class B: reason="Forward references and string annotations are broken in PyPy3 < 7.2", strict=True, ) -def test_forward_reference_module_scope(): +def test_forward_reference_module_scope() -> None: """Run the forward reference test at global scope.""" import tests.cases.forward_reference # pylint disable=unused-import,import-outside-toplevel -def test_non_string_metadata_key(module): +def test_non_string_metadata_key(module: DataclassModule) -> None: """A non-string key in the attrib metadata comes through in the mm field.""" @module.dataclass @@ -456,7 +485,7 @@ class A: assert field.metadata == {1: 2, desert._make._DESERT_SENTINEL: {}} -def test_non_optional_means_required(module): +def test_non_optional_means_required(module: DataclassModule) -> None: """Non-optional fields are required.""" @module.dataclass @@ -469,7 +498,7 @@ class A: schema.load({}) -def test_ignore_unknown_fields(module): +def test_ignore_unknown_fields(module: DataclassModule) -> None: """Enable unknown fields with meta argument.""" @module.dataclass @@ -479,15 +508,15 @@ class A: schema_class = desert.schema_class(A, meta={"unknown": marshmallow.EXCLUDE}) schema = schema_class() data = schema.load({"x": 1, "y": 2}) - assert data == A(x=1) + assert data == A(x=1) # type: ignore[call-arg] -def test_raise_unknown_type(module): +def test_raise_unknown_type(module: DataclassModule) -> None: """Raise UnknownType for failed inferences.""" @module.dataclass class A: - x: list + x: list # type: ignore[type-arg] with pytest.raises(desert.exceptions.UnknownType): desert.schema_class(A) @@ -496,7 +525,7 @@ class A: @pytest.mark.skipif( sys.version_info[:2] <= (3, 6), reason="3.6 has isinstance(t.Sequence[int], type)." ) -def test_raise_unknown_generic(module): +def test_raise_unknown_generic(module: DataclassModule) -> None: """Raise UnknownType for unknown generics.""" @module.dataclass @@ -507,7 +536,7 @@ class A: desert.schema_class(A) -def test_tuple_ellipsis(module): +def test_tuple_ellipsis(module: DataclassModule) -> None: """Tuple with ellipsis allows variable length tuple. See :class:`typing.Tuple`. @@ -519,7 +548,7 @@ class A: schema = desert.schema_class(A)() dumped = {"x": (1, 2, 3)} - loaded = A(x=(1, 2, 3)) + loaded = A(x=(1, 2, 3)) # type: ignore[call-arg] actually_dumped = {"x": [1, 2, 3]} @@ -530,12 +559,12 @@ class A: assert schema.dump(schema.load(actually_dumped)) == actually_dumped -def test_only(): +def test_only() -> None: """only() extracts the only item in an iterable.""" assert desert._make.only([1]) == 1 -def test_only_raises(): +def test_only_raises() -> None: """only() raises if the iterable has an unexpected number of entries.'""" with pytest.raises(ValueError): desert._make.only([]) @@ -544,7 +573,7 @@ def test_only_raises(): desert._make.only([1, 2]) -def test_takes_self(): +def test_takes_self() -> None: """Attrs default factories are constructed after instance creation.""" @attr.s @@ -553,14 +582,14 @@ class C: y: int = attr.ib() @y.default - def _(self): + def _(self) -> int: return self.x + 1 schema = desert.schema(C) assert schema.load({"x": 1}) == C(x=1, y=2) -def test_methods_not_on_schema(module): +def test_methods_not_on_schema(module: DataclassModule) -> None: """Dataclass methods are not copied to the schema.""" @module.dataclass From 8661169583108d1b83404ca9e1b5f215ae8befd2 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Wed, 11 Aug 2021 11:48:50 -0400 Subject: [PATCH 2/6] fixup tests --- src/desert/_make.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/desert/_make.py b/src/desert/_make.py index 726cb0c..6fc36af 100644 --- a/src/desert/_make.py +++ b/src/desert/_make.py @@ -320,7 +320,8 @@ def make_data_class(self, data: t.Mapping[str, object], **_: object) -> object: def _get_field_default( - field: t.Union[dataclasses.Field[object], "attr.Attribute[object]"], + # field: t.Union[dataclasses.Field[object], "attr.Attribute[object]"], + field: t.Union["dataclasses.Field[object]", "attr.Attribute[object]"], ) -> object: """ Return a marshmallow default value given a dataclass default value From fbd0279122c50bec7bfad7a5d0c000e5fe72ff39 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Wed, 11 Aug 2021 11:51:55 -0400 Subject: [PATCH 3/6] black --- tests/test_make.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_make.py b/tests/test_make.py index 1437830..a075bb5 100644 --- a/tests/test_make.py +++ b/tests/test_make.py @@ -443,7 +443,8 @@ class A: ), ) def test_forward_reference( - module: DataclassModule, assert_dump_load: AssertLoadDumpProtocol, + module: DataclassModule, + assert_dump_load: AssertLoadDumpProtocol, ) -> None: # pragma: no cover """Build schemas from classes that are defined below their containing class.""" From e218f421ec09f50a2ede0a98460d35ffe67d2ab8 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Wed, 11 Aug 2021 12:03:31 -0400 Subject: [PATCH 4/6] isort --- tests/test_make.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/test_make.py b/tests/test_make.py index a075bb5..8efdcdb 100644 --- a/tests/test_make.py +++ b/tests/test_make.py @@ -7,12 +7,11 @@ import types import typing as t +# https://github.com/pytest-dev/pytest/issues/7469 +import _pytest.fixtures import attr import marshmallow import marshmallow.fields - -# https://github.com/pytest-dev/pytest/issues/7469 -import _pytest.fixtures import pytest import typing_extensions From 0a5456a5f59ab33600ac21cb6b58de5bf8e8c715 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Wed, 11 Aug 2021 12:11:10 -0400 Subject: [PATCH 5/6] only use ignore_missing_imports for specific libraries --- mypy.ini | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/mypy.ini b/mypy.ini index ea11601..4218146 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1,3 +1,11 @@ [mypy] -ignore_missing_imports = True strict = True + +[mypy-marshmallow_enum.*] +ignore_missing_imports = True + +[mypy-marshmallow_union.*] +ignore_missing_imports = True + +[mypy-typing_inspect.*] +ignore_missing_imports = True From adee10fae50b1767879702f8353fa23b68875fdd Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Tue, 17 Aug 2021 10:35:19 -0400 Subject: [PATCH 6/6] back to {} for metadata default --- src/desert/__init__.py | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/src/desert/__init__.py b/src/desert/__init__.py index 23f019d..c55d5e8 100644 --- a/src/desert/__init__.py +++ b/src/desert/__init__.py @@ -70,7 +70,7 @@ def field( marshmallow_field: marshmallow.fields.Field, *, default: T, - metadata: t.Optional[t.Mapping[object, object]] = None, + metadata: t.Mapping[object, object] = {}, **kw: object, ) -> T: ... @@ -81,7 +81,7 @@ def field( marshmallow_field: marshmallow.fields.Field, *, default_factory: t.Callable[[], T], - metadata: t.Optional[t.Mapping[object, object]] = None, + metadata: t.Mapping[object, object] = {}, **kw: object, ) -> T: ... @@ -91,7 +91,7 @@ def field( def field( marshmallow_field: marshmallow.fields.Field, *, - metadata: t.Optional[t.Mapping[object, object]] = None, + metadata: t.Mapping[object, object] = {}, **kw: object, ) -> object: ... @@ -103,7 +103,7 @@ def field( # plugin that indicates the actual type. def field( marshmallow_field: marshmallow.fields.Field, - metadata: t.Optional[t.Mapping[object, object]] = None, + metadata: t.Mapping[object, object] = {}, **kw: object, ) -> object: """Specify a marshmallow field in the metadata for a ``dataclasses.dataclass``. @@ -114,9 +114,6 @@ def field( class A: x: int = desert.field(marshmallow.fields.Int()) """ - if metadata is None: - metadata = {} - meta: t.Dict[object, object] = create_metadata(marshmallow_field) meta.update(metadata) @@ -133,7 +130,7 @@ def ib( marshmallow_field: marshmallow.fields.Field, *, default: t.Union[T, t.Callable[[], T]], - metadata: t.Optional[t.Mapping[object, object]] = None, + metadata: t.Mapping[object, object] = {}, **kw: object, ) -> T: ... @@ -144,7 +141,7 @@ def ib( marshmallow_field: marshmallow.fields.Field, *, factory: t.Callable[[], T], - metadata: t.Optional[t.Mapping[object, object]] = None, + metadata: t.Mapping[object, object] = {}, **kw: object, ) -> T: ... @@ -154,7 +151,7 @@ def ib( def ib( marshmallow_field: marshmallow.fields.Field, *, - metadata: t.Optional[t.Mapping[object, object]] = None, + metadata: t.Mapping[object, object] = {}, **kw: object, ) -> object: ... @@ -166,7 +163,7 @@ def ib( # plugin that indicates the actual type. def ib( marshmallow_field: marshmallow.fields.Field, - metadata: t.Optional[t.Mapping[object, object]] = None, + metadata: t.Mapping[object, object] = {}, **kw: object, ) -> object: """Specify a marshmallow field in the metadata for an ``attr.dataclass``. @@ -177,9 +174,6 @@ def ib( class A: x: int = desert.ib(marshmallow.fields.Int()) """ - if metadata is None: - metadata = {} - meta: t.Dict[object, object] = create_metadata(marshmallow_field) meta.update(metadata) new_field: attr._make._CountingAttr = attr.ib(**kw, metadata=meta) # type: ignore[call-overload]