Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix: several nested partial issues (sequence partial, data key, schema initialized partial) #2042

Open
wants to merge 5 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 13 additions & 10 deletions src/marshmallow/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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)]:
Expand Down
46 changes: 35 additions & 11 deletions tests/test_deserialization.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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)
Expand All @@ -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:
Expand All @@ -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
Expand Down