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

Validate Only and Exclude #826

Merged
merged 8 commits into from Jul 1, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions AUTHORS.rst
Expand Up @@ -102,4 +102,5 @@ Contributors (chronological)
- Suren Khorenyan `@surik00 <https://github.com/surik00>`_
- Jeffrey Berger `@JeffBerger <https://github.com/JeffBerger>`_
- Felix Yan `@felixonmars <https://github.com/felixonmars>`_
- Prasanjit Prakash `@ikilledthecat <https://github.com/ikilledthecat>`_
- Guillaume Gelin `@ramnes <https://github.com/ramnes>`_
4 changes: 4 additions & 0 deletions CHANGELOG.rst
Expand Up @@ -16,6 +16,10 @@ Other changes:
- *Backwards-incompatible*: Pre/Post-processors MUST return modified data.
Returning ``None`` does not imply data were mutated (:issue:`347`). Thanks
:user:`tdevelioglu` for reporting.
- *Backwards-incompatible*: ``only`` and ``exclude`` are bound by
declared and additional fields. A ``ValueError`` is raised if invalid
fields are passed (:issue:`636`). Thanks :user:`jan-23` for reporting.
Thanks :user:`ikilledthecat` and :user:`deckar01` for the PRs.

Deprecations/Removals:

Expand Down
37 changes: 24 additions & 13 deletions marshmallow/schema.py
Expand Up @@ -247,12 +247,13 @@ class Meta:
data, errors = schema.dump(album)
data # {'release_date': '1968-12-06', 'title': 'Beggars Banquet'}

:param tuple|list only: Whitelist of fields to select when instantiating the Schema.
If None, all fields are used.
Nested fields can be represented with dot delimiters.
:param tuple|list exclude: Blacklist of fields to exclude when instantiating the Schema.
If a field appears in both `only` and `exclude`, it is not used.
Nested fields can be represented with dot delimiters.
:param tuple|list only: Whitelist of the declared fields to select when
instantiating the Schema. If None, all fields are used. Nested fields
can be represented with dot delimiters.
:param tuple|list exclude: Blacklist of the declared fields to exclude
when instantiating the Schema. If a field appears in both `only` and
`exclude`, it is not used. Nested fields can be represented with dot
delimiters.
:param str prefix: Optional prefix that will be prepended to all the
serialized field names.
:param bool many: Should be set to `True` if ``obj`` is a collection
Expand Down Expand Up @@ -723,13 +724,7 @@ def __apply_nested_option(self, option_name, field_names, set_operation):

def _update_fields(self, obj=None, many=False):
"""Update fields based on the passed in object."""
if self.only is not None:
# Return only fields specified in only option
if self.opts.fields:
field_names = self.set_class(self.opts.fields) & self.set_class(self.only)
else:
field_names = self.set_class(self.only)
elif self.opts.fields:
if self.opts.fields:
# Return fields specified in fields option
field_names = self.set_class(self.opts.fields)
elif self.opts.additional:
Expand All @@ -739,10 +734,26 @@ def _update_fields(self, obj=None, many=False):
else:
field_names = self.set_class(self.declared_fields.keys())

declared_fields = field_names
invalid_fields = self.set_class()
if self.only is not None:
# Return only fields specified in only option
only = self.set_class(self.only)
field_names = only
invalid_only = only - declared_fields
invalid_fields |= invalid_only

# If "exclude" option or param is specified, remove those fields
excludes = set(self.opts.exclude) | set(self.exclude)
if excludes:
field_names = field_names - excludes
invalid_excludes = excludes - declared_fields
invalid_fields |= invalid_excludes

if invalid_fields:
message = 'Invalid fields for {0}: {1}.'.format(self, invalid_fields)
raise ValueError(message)

ret = self.__filter_fields(field_names, obj, many=many)
# Set parents
self.__set_field_attrs(ret)
Expand Down
2 changes: 1 addition & 1 deletion tests/base.py
Expand Up @@ -229,7 +229,7 @@ class Meta:

class UserExcludeSchema(UserSchema):
class Meta:
exclude = ('created', 'updated', 'field_not_found_but_thats_ok')
exclude = ('created', 'updated',)


class UserAdditionalSchema(Schema):
Expand Down
79 changes: 68 additions & 11 deletions tests/test_schema.py
Expand Up @@ -671,7 +671,7 @@ def test_only_in_init(SchemaClass, user):
assert 'age' in s

def test_invalid_only_param(user):
with pytest.raises(AttributeError):
with pytest.raises(ValueError):
UserSchema(only=('_invalid', 'name')).dump(user)

def test_can_serialize_uuid(serialized_user, user):
Expand Down Expand Up @@ -1291,32 +1291,89 @@ class MySchema(Schema):
assert 'bar' not in result


def test_exclude_invalid_attribute():
def test_only_and_exclude_with_fields():
class MySchema(Schema):
foo = fields.Field()

class Meta:
fields = ('bar', 'baz')
sch = MySchema(only=('bar', 'baz'), exclude=('bar', ))
data = dict(foo=42, bar=24, baz=242)
result = sch.dump(data)
assert 'baz' in result
assert 'bar' not in result


def test_invalid_only_and_exclude_with_fields():
class MySchema(Schema):
foo = fields.Field()

sch = MySchema(exclude=('bar', ))
assert sch.dump({'foo': 42}) == {'foo': 42}
class Meta:
fields = ('bar', 'baz')

with pytest.raises(ValueError) as excinfo:
MySchema(only=('foo', 'par'), exclude=('ban', ))

def test_only_with_invalid_attribute():
assert 'foo' in str(excinfo)
assert 'par' in str(excinfo)
assert 'ban' in str(excinfo)


def test_only_and_exclude_with_additional():
class MySchema(Schema):
foo = fields.Field()

sch = MySchema(only=('bar', ))
with pytest.raises(KeyError) as excinfo:
sch.dump(dict(foo=42))
assert '"bar" is not a valid field' in str(excinfo.value.args[0])
class Meta:
additional = ('bar', 'baz')
sch = MySchema(only=('foo', 'bar'), exclude=('bar', ))
data = dict(foo=42, bar=24, baz=242)
result = sch.dump(data)
assert 'foo' in result
assert 'bar' not in result


def test_invalid_only_and_exclude_with_additional():
class MySchema(Schema):
foo = fields.Field()

class Meta:
additional = ('bar', 'baz')

with pytest.raises(ValueError) as excinfo:
MySchema(only=('foop', 'par'), exclude=('ban', ))

assert 'foop' in str(excinfo)
assert 'par' in str(excinfo)
assert 'ban' in str(excinfo)


def test_exclude_invalid_attribute():

class MySchema(Schema):
foo = fields.Field()

with pytest.raises(ValueError):
MySchema(exclude=('bar', ))


def test_only_bounded_by_fields():
class MySchema(Schema):

class Meta:
fields = ('foo', )

sch = MySchema(only=('baz', ))
assert sch.dump({'foo': 42}) == {}
with pytest.raises(ValueError):
MySchema(only=('baz', ))


def test_only_bounded_by_additional():
class MySchema(Schema):

class Meta:
additional = ('b', )

with pytest.raises(ValueError):
MySchema(only=('c', )).dump({'c': 3})

def test_only_empty():
class MySchema(Schema):
Expand Down