diff --git a/src/marshmallow/schema.py b/src/marshmallow/schema.py index 92152696f..f2b3bf0e3 100644 --- a/src/marshmallow/schema.py +++ b/src/marshmallow/schema.py @@ -373,7 +373,7 @@ def __init__( context: dict | None = None, load_only: types.StrSequenceOrSet = (), dump_only: types.StrSequenceOrSet = (), - partial: bool | types.StrSequenceOrSet = False, + partial: bool | types.StrSequenceOrSet | None = None, unknown: str | None = None, ): # Raise error if only or exclude is passed as string, not list of strings @@ -647,15 +647,18 @@ def _deserialize( partial_is_collection and attr_name in partial ): continue - d_kwargs = {} + d_kwargs = {} # type: typing.Dict[str, typing.Any] # Allow partial loading of nested schemas. if partial_is_collection: - prefix = field_name + "." - len_prefix = len(prefix) - sub_partial = [ - f[len_prefix:] for f in partial if f.startswith(prefix) - ] - d_kwargs["partial"] = sub_partial + if attr_name in partial: + d_kwargs["partial"] = True + else: + prefix = f"{attr_name}." + len_prefix = len(prefix) + sub_partial = [ + f[len_prefix:] for f in partial if f.startswith(prefix) + ] + d_kwargs["partial"] = sub_partial else: d_kwargs["partial"] = partial getter = lambda val: field_obj.deserialize( @@ -1080,7 +1083,7 @@ def _invoke_load_processors( *, many: bool, original_data, - partial: bool | types.StrSequenceOrSet, + partial: bool | types.StrSequenceOrSet | None, ): # This has to invert the order of the dump processors, so run the pass_many # processors first. @@ -1157,7 +1160,7 @@ def _invoke_schema_validators( data, original_data, many: bool, - partial: bool | types.StrSequenceOrSet, + partial: bool | types.StrSequenceOrSet | None, field_errors: bool = False, ): for attr_name in self._hooks[(VALIDATES_SCHEMA, pass_many)]: diff --git a/tests/test_deserialization.py b/tests/test_deserialization.py index 4ee5ba5b1..08d872edd 100644 --- a/tests/test_deserialization.py +++ b/tests/test_deserialization.py @@ -2019,17 +2019,18 @@ class Sch(Schema): assert "equal" in errors assert errors["equal"] == ["Must be equal to False."] - def test_nested_partial_load(self): + @pytest.mark.parametrize("partial", (True, {"z.x"}, {"z"})) + def test_nested_partial(self, partial): class SchemaA(Schema): x = fields.Integer(required=True) - y = fields.Integer() + y = fields.Integer(required=True) class SchemaB(Schema): z = fields.Nested(SchemaA) b_dict = {"z": {"y": 42}} # Partial loading shouldn't generate any errors. - result = SchemaB().load(b_dict, partial=True) + result = SchemaB().load(b_dict, partial=partial) assert result["z"]["y"] == 42 # Non partial loading should complain about missing values. with pytest.raises(ValidationError) as excinfo: @@ -2039,10 +2040,11 @@ class SchemaB(Schema): assert "z" in errors assert "x" in errors["z"] - def test_deeply_nested_partial_load(self): + @pytest.mark.parametrize("partial", (True, {"b.c.x"}, {"b.c"}, {"b"})) + def test_deeply_nested_partial(self, partial): class SchemaC(Schema): x = fields.Integer(required=True) - y = fields.Integer() + y = fields.Integer(required=True) class SchemaB(Schema): c = fields.Nested(SchemaC) @@ -2052,7 +2054,7 @@ class SchemaA(Schema): a_dict = {"b": {"c": {"y": 42}}} # Partial loading shouldn't generate any errors. - result = SchemaA().load(a_dict, partial=True) + result = SchemaA().load(a_dict, partial=partial) assert result["b"]["c"]["y"] == 42 # Non partial loading should complain about missing values. with pytest.raises(ValidationError) as excinfo: @@ -2063,22 +2065,44 @@ class SchemaA(Schema): assert "c" in errors["b"] assert "x" in errors["b"]["c"] - def test_nested_partial_tuple(self): + @pytest.mark.parametrize("partial", (True, {"x"})) + def test_nested_partial_from_schema_init(self, partial): class SchemaA(Schema): x = fields.Integer(required=True) y = fields.Integer(required=True) class SchemaB(Schema): - z = fields.Nested(SchemaA) + z = fields.Nested(SchemaA(partial=partial)) b_dict = {"z": {"y": 42}} - # If we ignore the missing z.x, z.y should still load. + + result = SchemaB().load(b_dict) + assert "z" in result + assert "y" in result["z"] + assert "x" not in result["z"] + assert result["z"]["y"] == 42 + + def test_nested_with_data_keys_partial_using_tuple(self): + class SchemaA(Schema): + x = fields.Integer(required=True, data_key="x_key") + y = fields.Integer(required=True, data_key="y_key") + + class SchemaB(Schema): + z = fields.Nested(SchemaA, data_key="z_key") + + b_dict = {"z_key": {"y_key": 42}} + result = SchemaB().load(b_dict, partial=("z.x",)) assert result["z"]["y"] == 42 - # If we ignore a missing z.y we should get a validation error. - with pytest.raises(ValidationError): + + with pytest.raises(ValidationError) as exc: SchemaB().load(b_dict, partial=("z.y",)) + data, errors = exc.value.valid_data, exc.value.messages + assert data["z"]["y"] == 42 + assert "z_key" in errors + assert "x_key" in errors["z_key"] + @pytest.mark.parametrize("FieldClass", ALL_FIELDS) def test_required_field_failure(FieldClass): # noqa