Skip to content

field2property rework #199

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

Merged
merged 5 commits into from
Apr 11, 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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 72 additions & 28 deletions apispec/ext/marshmallow/swagger.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,29 +110,69 @@ def _get_json_type_for_field(field):
return json_type, fmt


def field2choices(field):
"""Return the set of valid choices for a :class:`Field <marshmallow.fields.Field>`,
or ``None`` if no choices are specified.
def field2choices(field, **kwargs):
"""Return the dictionary of swagger field attributes for valid choices definition

:param Field field: A marshmallow field.
:rtype: set
:rtype: dict
"""
comparable = {
attributes = {}

comparable = [
validator.comparable for validator in field.validators
if hasattr(validator, 'comparable')
}
]
if comparable:
return comparable
attributes['enum'] = comparable
else:
choices = [
OrderedSet(validator.choices) for validator in field.validators
if hasattr(validator, 'choices')
]
if choices:
attributes['enum'] = list(functools.reduce(operator.and_, choices))

return attributes

choices = [
OrderedSet(validator.choices) for validator in field.validators
if hasattr(validator, 'choices')
]
if choices:
return functools.reduce(operator.and_, choices)

def field2read_only(field, **kwargs):
"""Return the dictionary of swagger field attributes for a dump_only field.

def field2range(field):
:param Field field: A marshmallow field.
:rtype: dict
"""
attributes = {}
if field.dump_only:
attributes['readOnly'] = True
return attributes


def field2write_only(field, **kwargs):
Copy link
Member Author

@lafrech lafrech Apr 10, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this was Python 3 only, I'd write

def field2write_only(field, *, openapi_major_version, **kwargs):

"""Return the dictionary of swagger field attributes for a load_only field.

:param Field field: A marshmallow field.
:rtype: dict
"""
attributes = {}
if field.load_only and kwargs['openapi_major_version'] >= 3:
attributes['writeOnly'] = True
return attributes


def field2nullable(field, **kwargs):
"""Return the dictionary of swagger field attributes for a nullable field.

:param Field field: A marshmallow field.
:rtype: dict
"""
attributes = {}
if field.allow_none:
omv = kwargs['openapi_major_version']
attributes['x-nullable' if omv < 3 else 'nullable'] = True
return attributes


def field2range(field, **kwargs):
"""Return the dictionary of swagger field attributes for a set of
:class:`Range <marshmallow.validators.Range>` validators.

Expand Down Expand Up @@ -168,7 +208,7 @@ def field2range(field):
attributes['maximum'] = validator.max
return attributes

def field2length(field):
def field2length(field, **kwargs):
"""Return the dictionary of swagger field attributes for a set of
:class:`Length <marshmallow.validators.Length>` validators.

Expand Down Expand Up @@ -269,6 +309,12 @@ def field2property(field, spec=None, use_refs=True, dump=True, name=None):
:param str name: The definition name, if applicable, used to construct the $ref value.
:rtype: dict, a Property Object
"""
if spec:
openapi_major_version = spec.openapi_version.version[0]
else:
# Default to 2 for backward compatibility
openapi_major_version = 2

from apispec.ext.marshmallow import resolve_schema_dict
type_, fmt = _get_json_type_for_field(field)

Expand All @@ -286,18 +332,16 @@ def field2property(field, spec=None, use_refs=True, dump=True, name=None):
else:
ret['default'] = default

choices = field2choices(field)
if choices:
ret['enum'] = list(choices)

if field.dump_only:
ret['readOnly'] = True

if field.allow_none:
ret['x-nullable'] = True

ret.update(field2range(field))
ret.update(field2length(field))
for attr_func in (
field2choices,
field2read_only,
field2write_only,
field2nullable,
field2range,
field2length,
):
ret.update(attr_func(
field, openapi_major_version=openapi_major_version))

if isinstance(field, marshmallow.fields.Nested):
del ret['type']
Expand All @@ -313,7 +357,7 @@ def field2property(field, spec=None, use_refs=True, dump=True, name=None):
raise ValueError('Must pass `name` argument for self-referencing Nested fields.')
# We need to use the `name` argument when the field is self-referencing and
# unbound (doesn't have `parent` set) because we can't access field.schema
ref_path = get_ref_path(spec.openapi_version.version[0])
ref_path = get_ref_path(openapi_major_version)
ref_name = '#/{ref_path}/{name}'.format(ref_path=ref_path,
name=name)
ref_schema = {'$ref': ref_name}
Expand Down
1 change: 0 additions & 1 deletion tests/test_ext_order.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ def confirm_ext_order_independency(web_framework, **kwargs_for_add_path):
extensions = [web_framework, 'marshmallow']
specs = []
for reverse in (False, True):
print(reverse)
if reverse:
spec = create_spec(reversed(extensions))
else:
Expand Down
29 changes: 26 additions & 3 deletions tests/test_swagger.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class TestMarshmallowFieldToSwagger:
def test_field2choices_preserving_order(self):
choices = ['a', 'b', 'c', 'aa', '0', 'cc']
field = fields.String(validate=validate.OneOf(choices))
assert swagger.field2choices(field) == choices
assert swagger.field2choices(field) == {'enum': choices}

@mark.parametrize(('FieldClass', 'jsontype'), [
(fields.Integer, 'integer'),
Expand Down Expand Up @@ -203,6 +203,17 @@ def test_field_with_allow_none(self):
res = swagger.field2property(field)
assert res['x-nullable'] is True

def test_field_with_allow_none_v3(self):
spec = APISpec(
title='Pets',
version='0.1',
plugins=['apispec.ext.marshmallow'],
openapi_version='3.0.0'
)
field = fields.Str(allow_none=True)
res = swagger.field2property(field, spec)
assert res['nullable'] is True

class TestMarshmallowSchemaToModelDefinition:

def test_invalid_schema(self):
Expand Down Expand Up @@ -376,20 +387,32 @@ class NotASchema(object):
assert excinfo.value.args[0] == ("{0!r} doesn't have either `fields` "
"or `_declared_fields`".format(NotASchema))

def test_dump_only_load_only_fields(self):
@pytest.mark.parametrize('openapi_version', ['2.0.0', '3.0.0'])
def test_dump_only_load_only_fields(self, openapi_version):
spec = APISpec(
title='Pets',
version='0.1',
plugins=['apispec.ext.marshmallow'],
openapi_version=openapi_version
)

class UserSchema(Schema):
_id = fields.Str(dump_only=True)
name = fields.Str()
password = fields.Str(load_only=True)

res = swagger.schema2jsonschema(UserSchema())
res = swagger.schema2jsonschema(UserSchema(), spec)
props = res['properties']
assert 'name' in props
# dump_only field appears with readOnly attribute
assert '_id' in props
assert 'readOnly' in props['_id']
# load_only field appears (writeOnly attribute does not exist)
assert 'password' in props
if openapi_version == '2.0.0':
assert 'writeOnly' not in props['password']
else:
assert 'writeOnly' in props['password']


class TestMarshmallowSchemaToParameters:
Expand Down