From 4878d550fe56f34bbdd80055a557253c53977eec Mon Sep 17 00:00:00 2001 From: Daniil Fajnberg Date: Mon, 13 Mar 2023 15:48:05 +0100 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Move=20the=20schema-wide?= =?UTF-8?q?=20`many`=20warning=20to=20`=5F=5Fsetattr=5F=5F`=20instead?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/marshmallow_generic/schema.py | 82 ++++++++++++++++++------------- tests/test_schema.py | 14 ++++-- 2 files changed, 56 insertions(+), 40 deletions(-) diff --git a/src/marshmallow_generic/schema.py b/src/marshmallow_generic/schema.py index 9572c27..9fb9822 100644 --- a/src/marshmallow_generic/schema.py +++ b/src/marshmallow_generic/schema.py @@ -57,7 +57,7 @@ class GenericSchema(GenericInsightMixin1[Model], Schema): many: bool = False, # usage discouraged ) -> None: """ - Emits a warning, if the `many` argument is not `None`. + Emits a warning, if the `many` argument is not `False`. Otherwise the same as in [`marshmallow.Schema`][marshmallow.Schema]. @@ -72,19 +72,20 @@ class GenericSchema(GenericInsightMixin1[Model], Schema): it is not used. Nested fields can be represented with dot delimiters. context: - Optional context passed to [`Method`] - [marshmallow.fields.Method] and [`Function`] - [marshmallow.fields.Function] fields. + Optional context passed to + [`Method`][marshmallow.fields.Method] and + [`Function`][marshmallow.fields.Function] fields. load_only: Fields to skip during serialization (write-only fields) dump_only: Fields to skip during deserialization (read-only fields) partial: Whether to ignore missing fields and not require any fields - declared. Propagates down to [`Nested`] - [marshmallow.fields.Nested] fields as well. If its value is an - iterable, only missing fields listed in that iterable will be - ignored. Use dot delimiters to specify nested fields. + declared. Propagates down to + [`Nested`][marshmallow.fields.Nested] fields as well. If its + value is an iterable, only missing fields listed in that + iterable will be ignored. Use dot delimiters to specify nested + fields. unknown: Whether to exclude, include, or raise an error for unknown fields in the data. Use `EXCLUDE`, `INCLUDE` or `RAISE`. @@ -99,11 +100,6 @@ class GenericSchema(GenericInsightMixin1[Model], Schema): [`load`][marshmallow_generic.GenericSchema.load]/ [`loads`][marshmallow_generic.GenericSchema.loads]. """ - if many: - warn( - "Setting `many` schema-wide breaks type safety. Use the the " - "`many` parameter of specific methods (like `load`) instead." - ) super().__init__( only=only, exclude=exclude, @@ -115,13 +111,28 @@ class GenericSchema(GenericInsightMixin1[Model], Schema): unknown=unknown, ) + def __setattr__(self, name: str, value: Any) -> None: + """ + Warns, when trying to set `many` to anything other than `False`. + + Otherwise the same the normal + [`object.__setattr__`](https://docs.python.org/3/reference/datamodel.html#object.__setattr__). + """ + if name == "many" and value is not False: + warn( + "Changing `many` schema-wide breaks type safety. Use the the " + "`many` parameter of specific methods (like `load`) instead." + ) + super().__setattr__(name, value) + @post_load def instantiate(self, data: dict[str, Any], **_kwargs: Any) -> Model: """ Unpacks `data` into the constructor of the specified **`Model`**. - Registered as a [`@post_load`] - [marshmallow_generic.decorators.post_load] hook for the schema. + Registered as a + [`@post_load`][marshmallow_generic.decorators.post_load] + hook for the schema. !!! warning You should probably not use this method directly. No parsing, @@ -167,8 +178,9 @@ class GenericSchema(GenericInsightMixin1[Model], Schema): """ Serializes **`Model`** objects to native Python data types. - Same as [`marshmallow.Schema.dump`] - [marshmallow.schema.Schema.dump] at runtime. + Same as + [`marshmallow.Schema.dump`][marshmallow.schema.Schema.dump] + at runtime. Annotations ensure that type checkers will infer the return type correctly based on the `many` argument, and also enforce the `obj` @@ -252,10 +264,10 @@ class GenericSchema(GenericInsightMixin1[Model], Schema): """ Deserializes data to objects of the specified **`Model`** class. - Same as [`marshmallow.Schema.load`] - [marshmallow.schema.Schema.load] at runtime, but data will always - pass through the [`instantiate`] - [marshmallow_generic.schema.GenericSchema.instantiate] + Same as + [`marshmallow.Schema.load`][marshmallow.schema.Schema.load] at + runtime, but data will always pass through the + [`instantiate`][marshmallow_generic.schema.GenericSchema.instantiate] hook after deserialization. Annotations ensure that type checkers will infer the return type @@ -269,11 +281,11 @@ class GenericSchema(GenericInsightMixin1[Model], Schema): the value for `self.many` is used. partial: Whether to ignore missing fields and not require any - fields declared. Propagates down to [`Nested`] - [marshmallow.fields.Nested] fields as well. If its value - is an iterable, only missing fields listed in that - iterable will be ignored. Use dot delimiters to specify - nested fields. + fields declared. Propagates down to + [`Nested`][marshmallow.fields.Nested] fields as well. If + its value is an iterable, only missing fields listed in + that iterable will be ignored. Use dot delimiters to + specify nested fields. unknown: Whether to exclude, include, or raise an error for unknown fields in the data. Use `EXCLUDE`, `INCLUDE` or `RAISE`. @@ -321,10 +333,10 @@ class GenericSchema(GenericInsightMixin1[Model], Schema): """ Deserializes data to objects of the specified **`Model`** class. - Same as [`marshmallow.Schema.loads`] - [marshmallow.schema.Schema.loads] at runtime, but data will always - pass through the [`instantiate`] - [marshmallow_generic.schema.GenericSchema.instantiate] + Same as + [`marshmallow.Schema.loads`][marshmallow.schema.Schema.loads] at + runtime, but data will always pass through the + [`instantiate`][marshmallow_generic.schema.GenericSchema.instantiate] hook after deserialization. Annotations ensure that type checkers will infer the return type @@ -338,11 +350,11 @@ class GenericSchema(GenericInsightMixin1[Model], Schema): the value for `self.many` is used. partial: Whether to ignore missing fields and not require any - fields declared. Propagates down to [`Nested`] - [marshmallow.fields.Nested] fields as well. If its value - is an iterable, only missing fields listed in that - iterable will be ignored. Use dot delimiters to specify - nested fields. + fields declared. Propagates down to + [`Nested`][marshmallow.fields.Nested] fields as well. If + its value is an iterable, only missing fields listed in + that iterable will be ignored. Use dot delimiters to + specify nested fields. unknown: Whether to exclude, include, or raise an error for unknown fields in the data. Use `EXCLUDE`, `INCLUDE` or `RAISE`. diff --git a/tests/test_schema.py b/tests/test_schema.py index 5dfa763..076e88a 100644 --- a/tests/test_schema.py +++ b/tests/test_schema.py @@ -19,15 +19,19 @@ class GenericSchemaTestCase(TestCase): "dump_only": object(), "partial": object(), "unknown": object(), - "many": False, + "many": object(), } schema.GenericSchema[Foo](**kwargs) mock_super_init.assert_called_once_with(**kwargs) - mock_super_init.reset_mock() - kwargs["many"] = True + + def test___setattr__(self) -> None: + class Foo: + pass + + obj = schema.GenericSchema[Foo]() with self.assertWarns(UserWarning): - schema.GenericSchema[Foo](**kwargs) - mock_super_init.assert_called_once_with(**kwargs) + obj.many = new = MagicMock() + self.assertIs(new, obj.many) @patch.object(_util.GenericInsightMixin, "_get_type_arg") def test_instantiate(self, mock__get_type_arg: MagicMock) -> None: