Skip to content

Commit bcbee87

Browse files
authored
Merge pull request #199 from Nobatek/dev_jl_field2property_rework
field2property rework
2 parents 66c8e78 + 6169811 commit bcbee87

3 files changed

Lines changed: 98 additions & 32 deletions

File tree

apispec/ext/marshmallow/swagger.py

Lines changed: 72 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -110,29 +110,69 @@ def _get_json_type_for_field(field):
110110
return json_type, fmt
111111

112112

113-
def field2choices(field):
114-
"""Return the set of valid choices for a :class:`Field <marshmallow.fields.Field>`,
115-
or ``None`` if no choices are specified.
113+
def field2choices(field, **kwargs):
114+
"""Return the dictionary of swagger field attributes for valid choices definition
116115
117116
:param Field field: A marshmallow field.
118-
:rtype: set
117+
:rtype: dict
119118
"""
120-
comparable = {
119+
attributes = {}
120+
121+
comparable = [
121122
validator.comparable for validator in field.validators
122123
if hasattr(validator, 'comparable')
123-
}
124+
]
124125
if comparable:
125-
return comparable
126+
attributes['enum'] = comparable
127+
else:
128+
choices = [
129+
OrderedSet(validator.choices) for validator in field.validators
130+
if hasattr(validator, 'choices')
131+
]
132+
if choices:
133+
attributes['enum'] = list(functools.reduce(operator.and_, choices))
134+
135+
return attributes
126136

127-
choices = [
128-
OrderedSet(validator.choices) for validator in field.validators
129-
if hasattr(validator, 'choices')
130-
]
131-
if choices:
132-
return functools.reduce(operator.and_, choices)
133137

138+
def field2read_only(field, **kwargs):
139+
"""Return the dictionary of swagger field attributes for a dump_only field.
134140
135-
def field2range(field):
141+
:param Field field: A marshmallow field.
142+
:rtype: dict
143+
"""
144+
attributes = {}
145+
if field.dump_only:
146+
attributes['readOnly'] = True
147+
return attributes
148+
149+
150+
def field2write_only(field, **kwargs):
151+
"""Return the dictionary of swagger field attributes for a load_only field.
152+
153+
:param Field field: A marshmallow field.
154+
:rtype: dict
155+
"""
156+
attributes = {}
157+
if field.load_only and kwargs['openapi_major_version'] >= 3:
158+
attributes['writeOnly'] = True
159+
return attributes
160+
161+
162+
def field2nullable(field, **kwargs):
163+
"""Return the dictionary of swagger field attributes for a nullable field.
164+
165+
:param Field field: A marshmallow field.
166+
:rtype: dict
167+
"""
168+
attributes = {}
169+
if field.allow_none:
170+
omv = kwargs['openapi_major_version']
171+
attributes['x-nullable' if omv < 3 else 'nullable'] = True
172+
return attributes
173+
174+
175+
def field2range(field, **kwargs):
136176
"""Return the dictionary of swagger field attributes for a set of
137177
:class:`Range <marshmallow.validators.Range>` validators.
138178
@@ -168,7 +208,7 @@ def field2range(field):
168208
attributes['maximum'] = validator.max
169209
return attributes
170210

171-
def field2length(field):
211+
def field2length(field, **kwargs):
172212
"""Return the dictionary of swagger field attributes for a set of
173213
:class:`Length <marshmallow.validators.Length>` validators.
174214
@@ -269,6 +309,12 @@ def field2property(field, spec=None, use_refs=True, dump=True, name=None):
269309
:param str name: The definition name, if applicable, used to construct the $ref value.
270310
:rtype: dict, a Property Object
271311
"""
312+
if spec:
313+
openapi_major_version = spec.openapi_version.version[0]
314+
else:
315+
# Default to 2 for backward compatibility
316+
openapi_major_version = 2
317+
272318
from apispec.ext.marshmallow import resolve_schema_dict
273319
type_, fmt = _get_json_type_for_field(field)
274320

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

289-
choices = field2choices(field)
290-
if choices:
291-
ret['enum'] = list(choices)
292-
293-
if field.dump_only:
294-
ret['readOnly'] = True
295-
296-
if field.allow_none:
297-
ret['x-nullable'] = True
298-
299-
ret.update(field2range(field))
300-
ret.update(field2length(field))
335+
for attr_func in (
336+
field2choices,
337+
field2read_only,
338+
field2write_only,
339+
field2nullable,
340+
field2range,
341+
field2length,
342+
):
343+
ret.update(attr_func(
344+
field, openapi_major_version=openapi_major_version))
301345

302346
if isinstance(field, marshmallow.fields.Nested):
303347
del ret['type']
@@ -313,7 +357,7 @@ def field2property(field, spec=None, use_refs=True, dump=True, name=None):
313357
raise ValueError('Must pass `name` argument for self-referencing Nested fields.')
314358
# We need to use the `name` argument when the field is self-referencing and
315359
# unbound (doesn't have `parent` set) because we can't access field.schema
316-
ref_path = get_ref_path(spec.openapi_version.version[0])
360+
ref_path = get_ref_path(openapi_major_version)
317361
ref_name = '#/{ref_path}/{name}'.format(ref_path=ref_path,
318362
name=name)
319363
ref_schema = {'$ref': ref_name}

tests/test_ext_order.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ def confirm_ext_order_independency(web_framework, **kwargs_for_add_path):
2020
extensions = [web_framework, 'marshmallow']
2121
specs = []
2222
for reverse in (False, True):
23-
print(reverse)
2423
if reverse:
2524
spec = create_spec(reversed(extensions))
2625
else:

tests/test_swagger.py

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ class TestMarshmallowFieldToSwagger:
1515
def test_field2choices_preserving_order(self):
1616
choices = ['a', 'b', 'c', 'aa', '0', 'cc']
1717
field = fields.String(validate=validate.OneOf(choices))
18-
assert swagger.field2choices(field) == choices
18+
assert swagger.field2choices(field) == {'enum': choices}
1919

2020
@mark.parametrize(('FieldClass', 'jsontype'), [
2121
(fields.Integer, 'integer'),
@@ -202,6 +202,17 @@ def test_field_with_allow_none(self):
202202
res = swagger.field2property(field)
203203
assert res['x-nullable'] is True
204204

205+
def test_field_with_allow_none_v3(self):
206+
spec = APISpec(
207+
title='Pets',
208+
version='0.1',
209+
plugins=['apispec.ext.marshmallow'],
210+
openapi_version='3.0.0'
211+
)
212+
field = fields.Str(allow_none=True)
213+
res = swagger.field2property(field, spec)
214+
assert res['nullable'] is True
215+
205216
class TestMarshmallowSchemaToModelDefinition:
206217

207218
def test_invalid_schema(self):
@@ -375,20 +386,32 @@ class NotASchema(object):
375386
assert excinfo.value.args[0] == ("{0!r} doesn't have either `fields` "
376387
"or `_declared_fields`".format(NotASchema))
377388

378-
def test_dump_only_load_only_fields(self):
389+
@pytest.mark.parametrize('openapi_version', ['2.0.0', '3.0.0'])
390+
def test_dump_only_load_only_fields(self, openapi_version):
391+
spec = APISpec(
392+
title='Pets',
393+
version='0.1',
394+
plugins=['apispec.ext.marshmallow'],
395+
openapi_version=openapi_version
396+
)
397+
379398
class UserSchema(Schema):
380399
_id = fields.Str(dump_only=True)
381400
name = fields.Str()
382401
password = fields.Str(load_only=True)
383402

384-
res = swagger.schema2jsonschema(UserSchema())
403+
res = swagger.schema2jsonschema(UserSchema(), spec)
385404
props = res['properties']
386405
assert 'name' in props
387406
# dump_only field appears with readOnly attribute
388407
assert '_id' in props
389408
assert 'readOnly' in props['_id']
390409
# load_only field appears (writeOnly attribute does not exist)
391410
assert 'password' in props
411+
if openapi_version == '2.0.0':
412+
assert 'writeOnly' not in props['password']
413+
else:
414+
assert 'writeOnly' in props['password']
392415

393416

394417
class TestMarshmallowSchemaToParameters:

0 commit comments

Comments
 (0)