From df860be1b931352f307234717234459efcbf3427 Mon Sep 17 00:00:00 2001 From: "Stephen J. Fuhry" Date: Wed, 25 May 2016 07:29:40 -0400 Subject: [PATCH 01/14] add support for descriptions & titles --- marshmallow_jsonschema/base.py | 10 +++++++++- tests/test_dump.py | 20 ++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/marshmallow_jsonschema/base.py b/marshmallow_jsonschema/base.py index c0a388e..4e39e54 100644 --- a/marshmallow_jsonschema/base.py +++ b/marshmallow_jsonschema/base.py @@ -109,14 +109,22 @@ def _from_python_type(field, pytype): if field.default is not missing: json_schema['default'] = field.default + if field.metadata.get('metadata', {}).get('description'): + json_schema['description'] = field.metadata['metadata'].get('description') + if field.metadata.get('metadata', {}).get('title'): + json_schema['title'] = field.metadata['metadata'].get('title') return json_schema def _from_nested_schema(field): schema = JSONSchema().dump(field.nested()).data + if field.metadata.get('metadata', {}).get('description'): + schema['description'] = field.metadata['metadata'].get('description') + if field.metadata.get('metadata', {}).get('title'): + schema['title'] = field.metadata['metadata'].get('title') if field.many: schema = { 'type': ["array"] if field.required else ['array', 'null'], - 'items': schema + 'items': schema, } return schema diff --git a/tests/test_dump.py b/tests/test_dump.py index 131966d..89f02fe 100644 --- a/tests/test_dump.py +++ b/tests/test_dump.py @@ -29,6 +29,26 @@ def test_default(self): self._validate_schema(dumped) self.assertEqual(dumped['properties']['id']['default'], 'no-id') + def test_descriptions(self): + class TestSchema(Schema): + myfield = fields.String(metadata={'description': 'Brown Cow'}) + yourfield = fields.Integer(required=True) + schema = TestSchema() + json_schema = JSONSchema() + dumped = json_schema.dump(schema).data + self._validate_schema(dumped) + assert dumped['properties']['myfield']['description'] == 'Brown Cow' + + def test_title(self): + class TestSchema(Schema): + myfield = fields.String(metadata={'title': 'Brown Cowzz'}) + yourfield = fields.Integer(required=True) + schema = TestSchema() + json_schema = JSONSchema() + dumped = json_schema.dump(schema).data + self._validate_schema(dumped) + assert dumped['properties']['myfield']['title'] == 'Brown Cowzz' + def test_unknown_typed_field_throws_valueerror(self): class Invalid(fields.Field): From 2c9d88a5cb4f9af825866e8a8b73c12bef0dac3c Mon Sep 17 00:00:00 2001 From: "Stephen J. Fuhry" Date: Wed, 25 May 2016 07:38:52 -0400 Subject: [PATCH 02/14] bump version --- CHANGES | 2 ++ setup.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 CHANGES diff --git a/CHANGES b/CHANGES new file mode 100644 index 0000000..ea93f7f --- /dev/null +++ b/CHANGES @@ -0,0 +1,2 @@ +0.2.0 (2016-05-25) + - add support for titles & descriptions in metadata diff --git a/setup.py b/setup.py index bb07e1c..bc4b999 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ def read(fname): setup( name='marshmallow-jsonschema', - version='0.1.7', + version='0.2.0', description='JSON Schema Draft v4 (http://json-schema.org/) formatting with marshmallow', long_description=long_description, author='Stephen Fuhry', From 13264c045ff827f101c0b71c85d917c89140b8dd Mon Sep 17 00:00:00 2001 From: "Stephen J. Fuhry" Date: Wed, 25 May 2016 07:58:30 -0400 Subject: [PATCH 03/14] fix documentation --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index bc4b999..5157b1a 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ def read(fname): setup( name='marshmallow-jsonschema', - version='0.2.0', + version='0.2.1', description='JSON Schema Draft v4 (http://json-schema.org/) formatting with marshmallow', long_description=long_description, author='Stephen Fuhry', From 884a7c9ce59a829abb07b9367d53598c8544ec29 Mon Sep 17 00:00:00 2001 From: Eli Gundry Date: Fri, 8 Jul 2016 13:21:06 -0400 Subject: [PATCH 04/14] Make methods called by fields.Method classmethods In our usage of this library, we have sorely needed to overload these methods to make them work for us (i.e. required fields in nested fields) so that's why they have been moved. I also made the code less claustrophobic and let it breathe a little bit. --- marshmallow_jsonschema/base.py | 72 ++++++++++++++++++++-------------- 1 file changed, 42 insertions(+), 30 deletions(-) diff --git a/marshmallow_jsonschema/base.py b/marshmallow_jsonschema/base.py index 4e39e54..9ea8556 100644 --- a/marshmallow_jsonschema/base.py +++ b/marshmallow_jsonschema/base.py @@ -84,9 +84,9 @@ def get_properties(self, obj): schema = field._jsonschema_type_mapping() elif field.__class__ in mapping: pytype = mapping[field.__class__] - schema = _from_python_type(field, pytype) + schema = self._from_python_type(field, pytype) elif isinstance(field, fields.Nested): - schema = _from_nested_schema(field) + schema = self._from_nested_schema(field) else: raise ValueError('unsupported field type %s' % field) properties[field.name] = schema @@ -99,32 +99,44 @@ def get_required(self, obj): required.append(field.name) return required - -def _from_python_type(field, pytype): - json_schema = { - 'title': field.attribute or field.name, - } - for key, val in TYPE_MAP[pytype].items(): - json_schema[key] = val - if field.default is not missing: - json_schema['default'] = field.default - - if field.metadata.get('metadata', {}).get('description'): - json_schema['description'] = field.metadata['metadata'].get('description') - if field.metadata.get('metadata', {}).get('title'): - json_schema['title'] = field.metadata['metadata'].get('title') - return json_schema - - -def _from_nested_schema(field): - schema = JSONSchema().dump(field.nested()).data - if field.metadata.get('metadata', {}).get('description'): - schema['description'] = field.metadata['metadata'].get('description') - if field.metadata.get('metadata', {}).get('title'): - schema['title'] = field.metadata['metadata'].get('title') - if field.many: - schema = { - 'type': ["array"] if field.required else ['array', 'null'], - 'items': schema, + @classmethod + def _from_python_type(cls, field, pytype): + json_schema = { + 'title': field.attribute or field.name, } - return schema + + for key, val in TYPE_MAP[pytype].items(): + json_schema[key] = val + + if field.default is not missing: + json_schema['default'] = field.default + + if field.metadata.get('metadata', {}).get('description'): + json_schema['description'] = ( + field.metadata['metadata'].get('description') + ) + + if field.metadata.get('metadata', {}).get('title'): + json_schema['title'] = field.metadata['metadata'].get('title') + + return json_schema + + @classmethod + def _from_nested_schema(cls, field): + schema = cls().dump(field.nested()).data + + if field.metadata.get('metadata', {}).get('description'): + schema['description'] = ( + field.metadata['metadata'].get('description') + ) + + if field.metadata.get('metadata', {}).get('title'): + schema['title'] = field.metadata['metadata'].get('title') + + if field.many: + schema = { + 'type': ["array"] if field.required else ['array', 'null'], + 'items': schema, + } + + return schema From a4b3d2bddc5509649aad52f6eb5f377329fb9c03 Mon Sep 17 00:00:00 2001 From: Eli Gundry Date: Fri, 8 Jul 2016 13:35:13 -0400 Subject: [PATCH 05/14] Bring marshmallow validation support for schemas --- marshmallow_jsonschema/base.py | 23 +++++- marshmallow_jsonschema/validation.py | 103 +++++++++++++++++++++++++++ 2 files changed, 125 insertions(+), 1 deletion(-) create mode 100644 marshmallow_jsonschema/validation.py diff --git a/marshmallow_jsonschema/base.py b/marshmallow_jsonschema/base.py index 9ea8556..03bb17b 100644 --- a/marshmallow_jsonschema/base.py +++ b/marshmallow_jsonschema/base.py @@ -2,9 +2,11 @@ import uuid import decimal -from marshmallow import fields, missing, Schema +from marshmallow import fields, missing, Schema, validate from marshmallow.compat import text_type, binary_type +from .validation import handle_length, handle_one_of, handle_range + __all__ = ['JSONSchema'] @@ -66,6 +68,13 @@ } +FIELD_VALIDATORS = { + validate.Length: handle_length, + validate.OneOf: handle_one_of, + validate.Range: handle_range, +} + + class JSONSchema(Schema): properties = fields.Method('get_properties') type = fields.Constant('object') @@ -79,6 +88,7 @@ def get_properties(self, obj): mapping[fields.Url] = text_type mapping[fields.LocalDateTime] = datetime.datetime properties = {} + for field_name, field in sorted(obj.fields.items()): if hasattr(field, '_jsonschema_type_mapping'): schema = field._jsonschema_type_mapping() @@ -89,14 +99,25 @@ def get_properties(self, obj): schema = self._from_nested_schema(field) else: raise ValueError('unsupported field type %s' % field) + + # Apply any and all validators that field may have + for validator in field.validators: + if validator.__class__ in FIELD_VALIDATORS: + schema = FIELD_VALIDATORS[validator.__class__]( + schema, field, validator, obj + ) + properties[field.name] = schema + return properties def get_required(self, obj): required = [] + for field_name, field in sorted(obj.fields.items()): if field.required: required.append(field.name) + return required @classmethod diff --git a/marshmallow_jsonschema/validation.py b/marshmallow_jsonschema/validation.py new file mode 100644 index 0000000..418f78c --- /dev/null +++ b/marshmallow_jsonschema/validation.py @@ -0,0 +1,103 @@ +from marshmallow import fields + + +def handle_length(schema, field, validator, parent_schema): + """Adds validation logic for ``marshmallow.validate.Length``, setting the + values appropriately for ``fields.List`` and ``fields.String``. + + Args: + schema (dict): The original JSON schema we generated. This is what we + want to post-process. + field (fields.Field): The field that generated the original schema and + who this post-processor belongs to. + validator (marshmallow.validate.Length): The validator attached to the + passed in field. + parent_schema (marshmallow.Schema): The Schema instance that the field + belongs to. + + Returns: + dict: A, possibly, new JSON Schema that has been post processed and + altered. + + Raises: + ValueError: Raised if the `field` is something other than `fields.List` + or `fields.String` + """ + if isinstance(field, fields.String): + minKey = 'minLength' + maxKey = 'maxLength' + elif isinstance(field, fields.List): + minKey = 'minItems' + maxKey = 'maxItems' + else: + raise ValueError("In order to set the Length validator for JSON " + "schema, the field must be either a List or a String") + + if validator.min: + schema[minKey] = validator.min + + if validator.max: + schema[maxKey] = validator.max + + if validator.equal: + schema[minKey] = validator.equal + schema[maxKey] = validator.equal + + return schema + + +def handle_one_of(schema, field, validator, parent_schema): + """Adds the validation logic for ``marshmallow.validate.OneOf`` by setting + the JSONSchema `enum` property to the allowed choices in the validator. + + Args: + schema (dict): The original JSON schema we generated. This is what we + want to post-process. + field (fields.Field): The field that generated the original schema and + who this post-processor belongs to. + validator (marshmallow.validate.OneOf): The validator attached to the + passed in field. + parent_schema (marshmallow.Schema): The Schema instance that the field + belongs to. + + Returns: + dict: A, possibly, new JSON Schema that has been post processed and + altered. + """ + if validator.choices: + schema['enum'] = validator.choices + + return schema + + +def handle_range(schema, field, validator, parent_schema): + """Adds validation logic for ``marshmallow.validate.Range``, setting the + values appropriately ``fields.Number`` and it's subclasses. + + Args: + schema (dict): The original JSON schema we generated. This is what we + want to post-process. + field (fields.Field): The field that generated the original schema and + who this post-processor belongs to. + validator (marshmallow.validate.Length): The validator attached to the + passed in field. + parent_schema (marshmallow.Schema): The Schema instance that the field + belongs to. + + Returns: + dict: A, possibly, new JSON Schema that has been post processed and + altered. + """ + if not isinstance(field, fields.Number): + return schema + + if validator.min: + schema['minimum'] = validator.min + schema['exclusiveMinimum'] = True + else: + schema['minimum'] = 0 + schema['exclusiveMinimum'] = False + + if validator.max: + schema['maximum'] = validator.max + schema['exclusiveMaximum'] = True From 15d912509dc20ce9f1277f9e72d53ef75507156b Mon Sep 17 00:00:00 2001 From: Eli Gundry Date: Fri, 8 Jul 2016 14:12:12 -0400 Subject: [PATCH 06/14] Add tests for new validators --- marshmallow_jsonschema/validation.py | 11 +++++---- tests/__init__.py | 9 ++++--- tests/test_dump.py | 36 ++++++++++++++++++++++++---- 3 files changed, 45 insertions(+), 11 deletions(-) diff --git a/marshmallow_jsonschema/validation.py b/marshmallow_jsonschema/validation.py index 418f78c..4a40b61 100644 --- a/marshmallow_jsonschema/validation.py +++ b/marshmallow_jsonschema/validation.py @@ -3,7 +3,8 @@ def handle_length(schema, field, validator, parent_schema): """Adds validation logic for ``marshmallow.validate.Length``, setting the - values appropriately for ``fields.List`` and ``fields.String``. + values appropriately for ``fields.List``, ``fields.Nested``, and + ``fields.String``. Args: schema (dict): The original JSON schema we generated. This is what we @@ -20,13 +21,13 @@ def handle_length(schema, field, validator, parent_schema): altered. Raises: - ValueError: Raised if the `field` is something other than `fields.List` - or `fields.String` + ValueError: Raised if the `field` is something other than + `fields.List`, `fields.Nested`, or `fields.String` """ if isinstance(field, fields.String): minKey = 'minLength' maxKey = 'maxLength' - elif isinstance(field, fields.List): + elif isinstance(field, (fields.List, fields.Nested)): minKey = 'minItems' maxKey = 'maxItems' else: @@ -101,3 +102,5 @@ def handle_range(schema, field, validator, parent_schema): if validator.max: schema['maximum'] = validator.max schema['exclusiveMaximum'] = True + + return schema diff --git a/tests/__init__.py b/tests/__init__.py index 84e99e9..cdaae84 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -8,7 +8,7 @@ class Address(Schema): street = fields.String(required=True) number = fields.String(required=True) city = fields.String(required=True) - floor = fields.String() + floor = fields.Integer(validate=validate.Range(min=1, max=4)) class GithubProfile(Schema): @@ -16,7 +16,8 @@ class GithubProfile(Schema): class UserSchema(Schema): - name = fields.String(required=True) + name = fields.String(required=True, + validate=validate.Length(min=1, max=255)) age = fields.Float() created = fields.DateTime() created_formatted = fields.DateTime(format="%Y-%m-%d", attribute="created") @@ -38,8 +39,10 @@ class UserSchema(Schema): since_created = fields.TimeDelta() sex = fields.Str(validate=validate.OneOf(['male', 'female'])) various_data = fields.Dict() - addresses = fields.Nested(Address, many=True) + addresses = fields.Nested(Address, many=True, + validate=validate.Length(min=1, max=3)) github = fields.Nested(GithubProfile) + const = fields.String(validate=validate.Length(equal=50)) class BaseTest(unittest.TestCase): diff --git a/tests/test_dump.py b/tests/test_dump.py index 89f02fe..36d3523 100644 --- a/tests/test_dump.py +++ b/tests/test_dump.py @@ -2,7 +2,7 @@ from marshmallow_jsonschema import JSONSchema from jsonschema import Draft4Validator -from . import BaseTest, UserSchema +from . import BaseTest, UserSchema, Address class TestDumpSchema(BaseTest): @@ -39,6 +39,34 @@ class TestSchema(Schema): self._validate_schema(dumped) assert dumped['properties']['myfield']['description'] == 'Brown Cow' + def test_one_of_validator(self): + schema = UserSchema() + json_schema = JSONSchema() + dumped = json_schema.dump(schema).data + self._validate_schema(dumped) + self.assertEqual(dumped['properties']['sex']['enum'], + ['male', 'female']) + + def test_range_validator(self): + schema = Address() + json_schema = JSONSchema() + dumped = json_schema.dump(schema).data + self._validate_schema(dumped) + self.assertEqual(dumped['properties']['floor']['minimum'], 1) + self.assertEqual(dumped['properties']['floor']['maximum'], 4) + + def test_length_validator(self): + schema = UserSchema() + json_schema = JSONSchema() + dumped = json_schema.dump(schema).data + self._validate_schema(dumped) + self.assertEqual(dumped['properties']['name']['minLength'], 1) + self.assertEqual(dumped['properties']['name']['maxLength'], 255) + self.assertEqual(dumped['properties']['addresses']['minItems'], 1) + self.assertEqual(dumped['properties']['addresses']['maxItems'], 3) + self.assertEqual(dumped['properties']['const']['minLength'], 50) + self.assertEqual(dumped['properties']['const']['maxLength'], 50) + def test_title(self): class TestSchema(Schema): myfield = fields.String(metadata={'title': 'Brown Cowzz'}) @@ -47,7 +75,7 @@ class TestSchema(Schema): json_schema = JSONSchema() dumped = json_schema.dump(schema).data self._validate_schema(dumped) - assert dumped['properties']['myfield']['title'] == 'Brown Cowzz' + dumped['properties']['myfield']['title'] == 'Brown Cowzz' def test_unknown_typed_field_throws_valueerror(self): @@ -61,7 +89,7 @@ class UserSchema(Schema): schema = UserSchema() json_schema = JSONSchema() with self.assertRaises(ValueError): - dumped = json_schema.dump(schema).data + json_schema.dump(schema).data def test_unknown_typed_field(self): @@ -87,4 +115,4 @@ class UserSchema(Schema): json_schema = JSONSchema() dumped = json_schema.dump(schema).data self.assertEqual(dumped['properties']['favourite_colour'], - {'type': 'string'}) + {'type': 'string'}) From 79912469d491a02121867ee30d2c35a8c57dcb10 Mon Sep 17 00:00:00 2001 From: Eli Gundry Date: Mon, 11 Jul 2016 10:17:16 -0400 Subject: [PATCH 07/14] Restores test removed accidently --- tests/test_dump.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_dump.py b/tests/test_dump.py index 36d3523..d37add6 100644 --- a/tests/test_dump.py +++ b/tests/test_dump.py @@ -75,7 +75,8 @@ class TestSchema(Schema): json_schema = JSONSchema() dumped = json_schema.dump(schema).data self._validate_schema(dumped) - dumped['properties']['myfield']['title'] == 'Brown Cowzz' + self.assertEqual(dumped['properties']['myfield']['title'], + 'Brown Cowzz') def test_unknown_typed_field_throws_valueerror(self): From 6fb666fd9a6e8cda6ba0806b8609a0a64ee17e2f Mon Sep 17 00:00:00 2001 From: "Stephen J. Fuhry" Date: Mon, 11 Jul 2016 11:15:50 -0400 Subject: [PATCH 08/14] add tests, use pytest instead of unittest.TestCase get to 100% coverage --- .travis.yml | 3 +- Makefile | 8 ++ setup.py | 4 +- tests/test_dump.py | 284 +++++++++++++++++++++++++++------------------ 4 files changed, 182 insertions(+), 117 deletions(-) create mode 100644 Makefile diff --git a/.travis.yml b/.travis.yml index 51ab910..4e5b910 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,8 +13,7 @@ before_install: - pip install -U pip install: - - pip install -U .[reco] - - pip install strict-rfc3339 jsonschema coveralls + - make installcheck script: coverage run --source marshmallow_jsonschema -m py.test after_success: coveralls diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..8d91c44 --- /dev/null +++ b/Makefile @@ -0,0 +1,8 @@ + + +installcheck: + pip install -U .[reco] + pip install strict-rfc3339 jsonschema coveralls + +check: + py.test -v diff --git a/setup.py b/setup.py index 5157b1a..469924b 100644 --- a/setup.py +++ b/setup.py @@ -22,8 +22,8 @@ def read(fname): packages=find_packages(exclude=("test*", )), package_dir={'marshmallow-jsonschema': 'marshmallow-jsonschema'}, include_package_data=True, - install_requires=['marshmallow>=2.3.0'], - tests_require=['pytest>=2.1', 'jsonschema', 'strict-rfc3339'], + install_requires=['marshmallow>=2.9.0'], + tests_require=['pytest>=2.9.2', 'jsonschema', 'strict-rfc3339', 'coverage>=4.1'], license=read('LICENSE'), zip_safe=False, keywords=('marshmallow-jsonschema marshmallow schema serialization ' diff --git a/tests/test_dump.py b/tests/test_dump.py index d37add6..4476fe9 100644 --- a/tests/test_dump.py +++ b/tests/test_dump.py @@ -1,119 +1,177 @@ -from marshmallow import Schema, fields +from marshmallow import Schema, fields, validate from marshmallow_jsonschema import JSONSchema from jsonschema import Draft4Validator +import pytest from . import BaseTest, UserSchema, Address -class TestDumpSchema(BaseTest): - - def _validate_schema(self, schema): - ''' - raises jsonschema.exceptions.SchemaError - ''' - Draft4Validator.check_schema(schema) - - def test_dump_schema(self): - schema = UserSchema() - json_schema = JSONSchema() - dumped = json_schema.dump(schema).data - self._validate_schema(dumped) - self.assertGreater(len(schema.fields), 1) - for field_name, field in schema.fields.items(): - self.assertIn(field_name, dumped['properties']) - - def test_default(self): - schema = UserSchema() - json_schema = JSONSchema() - dumped = json_schema.dump(schema).data - self._validate_schema(dumped) - self.assertEqual(dumped['properties']['id']['default'], 'no-id') - - def test_descriptions(self): - class TestSchema(Schema): - myfield = fields.String(metadata={'description': 'Brown Cow'}) - yourfield = fields.Integer(required=True) - schema = TestSchema() - json_schema = JSONSchema() - dumped = json_schema.dump(schema).data - self._validate_schema(dumped) - assert dumped['properties']['myfield']['description'] == 'Brown Cow' - - def test_one_of_validator(self): - schema = UserSchema() - json_schema = JSONSchema() - dumped = json_schema.dump(schema).data - self._validate_schema(dumped) - self.assertEqual(dumped['properties']['sex']['enum'], - ['male', 'female']) - - def test_range_validator(self): - schema = Address() - json_schema = JSONSchema() - dumped = json_schema.dump(schema).data - self._validate_schema(dumped) - self.assertEqual(dumped['properties']['floor']['minimum'], 1) - self.assertEqual(dumped['properties']['floor']['maximum'], 4) - - def test_length_validator(self): - schema = UserSchema() - json_schema = JSONSchema() - dumped = json_schema.dump(schema).data - self._validate_schema(dumped) - self.assertEqual(dumped['properties']['name']['minLength'], 1) - self.assertEqual(dumped['properties']['name']['maxLength'], 255) - self.assertEqual(dumped['properties']['addresses']['minItems'], 1) - self.assertEqual(dumped['properties']['addresses']['maxItems'], 3) - self.assertEqual(dumped['properties']['const']['minLength'], 50) - self.assertEqual(dumped['properties']['const']['maxLength'], 50) - - def test_title(self): - class TestSchema(Schema): - myfield = fields.String(metadata={'title': 'Brown Cowzz'}) - yourfield = fields.Integer(required=True) - schema = TestSchema() - json_schema = JSONSchema() - dumped = json_schema.dump(schema).data - self._validate_schema(dumped) - self.assertEqual(dumped['properties']['myfield']['title'], - 'Brown Cowzz') - - def test_unknown_typed_field_throws_valueerror(self): - - class Invalid(fields.Field): - def _serialize(self, value, attr, obj): - return value - - class UserSchema(Schema): - favourite_colour = Invalid() - - schema = UserSchema() - json_schema = JSONSchema() - with self.assertRaises(ValueError): - json_schema.dump(schema).data - - def test_unknown_typed_field(self): - - class Colour(fields.Field): - - def _jsonschema_type_mapping(self): - return { - 'type': 'string', - } - - def _serialize(self, value, attr, obj): - r, g, b = value - r = hex(r)[2:] - g = hex(g)[2:] - b = hex(b)[2:] - return '#' + r + g + b - - class UserSchema(Schema): - name = fields.String(required=True) - favourite_colour = Colour() - - schema = UserSchema() - json_schema = JSONSchema() - dumped = json_schema.dump(schema).data - self.assertEqual(dumped['properties']['favourite_colour'], - {'type': 'string'}) +def _validate_schema(schema): + ''' + raises jsonschema.exceptions.SchemaError + ''' + Draft4Validator.check_schema(schema) + +def test_dump_schema(): + schema = UserSchema() + json_schema = JSONSchema() + dumped = json_schema.dump(schema).data + _validate_schema(dumped) + assert len(schema.fields) > 1 + for field_name, field in schema.fields.items(): + assert field_name in dumped['properties'] + +def test_default(): + schema = UserSchema() + json_schema = JSONSchema() + dumped = json_schema.dump(schema).data + _validate_schema(dumped) + assert dumped['properties']['id']['default'] == 'no-id' + +def test_descriptions(): + class TestSchema(Schema): + myfield = fields.String(metadata={'description': 'Brown Cow'}) + yourfield = fields.Integer(required=True) + schema = TestSchema() + json_schema = JSONSchema() + dumped = json_schema.dump(schema).data + _validate_schema(dumped) + assert dumped['properties']['myfield']['description'] == 'Brown Cow' + +def test_nested_descriptions(): + class TestSchema(Schema): + myfield = fields.String(metadata={'description': 'Brown Cow'}) + yourfield = fields.Integer(required=True) + class TestNestedSchema(Schema): + nested = fields.Nested( + TestSchema, metadata={'description': 'Nested 1', 'title': 'Title1'}) + yourfield_nested = fields.Integer(required=True) + + schema = TestNestedSchema() + json_schema = JSONSchema() + dumped = json_schema.dump(schema).data + _validate_schema(dumped) + nested_dmp = dumped['properties']['nested'] + assert nested_dmp['properties']['myfield']['description'] == 'Brown Cow' + assert nested_dmp['description'] == 'Nested 1' + assert nested_dmp['title'] == 'Title1' + + +def test_one_of_validator(): + schema = UserSchema() + json_schema = JSONSchema() + dumped = json_schema.dump(schema).data + _validate_schema(dumped) + assert dumped['properties']['sex']['enum'] == ['male', 'female'] + +def test_range_validator(): + schema = Address() + json_schema = JSONSchema() + dumped = json_schema.dump(schema).data + _validate_schema(dumped) + assert dumped['properties']['floor']['minimum'] == 1 + assert dumped['properties']['floor']['maximum'] == 4 + +def test_length_validator(): + schema = UserSchema() + json_schema = JSONSchema() + dumped = json_schema.dump(schema).data + _validate_schema(dumped) + assert dumped['properties']['name']['minLength'] == 1 + assert dumped['properties']['name']['maxLength'] == 255 + assert dumped['properties']['addresses']['minItems'] == 1 + assert dumped['properties']['addresses']['maxItems'] == 3 + assert dumped['properties']['const']['minLength'] == 50 + assert dumped['properties']['const']['maxLength'] == 50 + +def test_length_validator_value_error(): + class BadSchema(Schema): + bob = fields.Integer(validate=validate.Length(min=1, max=3)) + schema = BadSchema(strict=True) + json_schema = JSONSchema() + with pytest.raises(ValueError): + json_schema.dump(schema) + + +def test_handle_range_not_number_returns_same_instance(): + class SchemaWithStringRange(Schema): + floor = fields.String(validate=validate.Range(min=1, max=4)) + class SchemaWithNoRange(Schema): + floor = fields.String() + class SchemaWithIntRangeValidate(Schema): + floor = fields.Integer(validate=validate.Range(min=1, max=4)) + class SchemaWithIntRangeNoValidate(Schema): + floor = fields.Integer() + schema1 = SchemaWithStringRange(strict=True) + schema2 = SchemaWithNoRange(strict=True) + schema3 = SchemaWithIntRangeValidate(strict=True) + schema4 = SchemaWithIntRangeNoValidate(strict=True) + json_schema = JSONSchema() + json_schema.dump(schema1) == json_schema.dump(schema2) + json_schema.dump(schema3) != json_schema.dump(schema4) + + +def test_handle_range_no_minimum(): + class SchemaMin(Schema): + floor = fields.Integer(validate=validate.Range(min=1, max=4)) + class SchemaNoMin(Schema): + floor = fields.Integer(validate=validate.Range(max=4)) + schema1 = SchemaMin(strict=True) + schema2 = SchemaNoMin(strict=True) + json_schema = JSONSchema() + dumped1 = json_schema.dump(schema1) + dumped2 = json_schema.dump(schema2) + dumped1.data['properties']['floor']['minimum'] == 1 + dumped1.data['properties']['floor']['exclusiveMinimum'] is True + dumped2.data['properties']['floor']['minimum'] == 0 + dumped2.data['properties']['floor']['exclusiveMinimum'] is False + + +def test_title(): + class TestSchema(Schema): + myfield = fields.String(metadata={'title': 'Brown Cowzz'}) + yourfield = fields.Integer(required=True) + schema = TestSchema() + json_schema = JSONSchema() + dumped = json_schema.dump(schema).data + _validate_schema(dumped) + assert dumped['properties']['myfield']['title'] == 'Brown Cowzz' + +def test_unknown_typed_field_throws_valueerror(): + + class Invalid(fields.Field): + def _serialize(self, value, attr, obj): + return value + + class UserSchema(Schema): + favourite_colour = Invalid() + + schema = UserSchema() + json_schema = JSONSchema() + with pytest.raises(ValueError): + json_schema.dump(schema).data + +def test_unknown_typed_field(): + + class Colour(fields.Field): + + def _jsonschema_type_mapping(self): + return { + 'type': 'string', + } + + def _serialize(self, value, attr, obj): + r, g, b = value + r = hex(r)[2:] + g = hex(g)[2:] + b = hex(b)[2:] + return '#' + r + g + b + + class UserSchema(Schema): + name = fields.String(required=True) + favourite_colour = Colour() + + schema = UserSchema() + json_schema = JSONSchema() + dumped = json_schema.dump(schema).data + assert dumped['properties']['favourite_colour'] == {'type': 'string'} From 8cfb86ecfa291cb34c4ea64481d36ff88101f9e5 Mon Sep 17 00:00:00 2001 From: "Stephen J. Fuhry" Date: Tue, 12 Jul 2016 14:26:33 -0400 Subject: [PATCH 09/14] self.__class__ for classmethod --- Makefile | 6 +++++- marshmallow_jsonschema/base.py | 4 ++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 8d91c44..5aefef2 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,11 @@ installcheck: pip install -U .[reco] - pip install strict-rfc3339 jsonschema coveralls + pip install strict-rfc3339 jsonschema coveralls coverage check: py.test -v + +coverage: + coverage erase + coverage run --source marshmallow_jsonschema -m py.test -v diff --git a/marshmallow_jsonschema/base.py b/marshmallow_jsonschema/base.py index 03bb17b..ffae465 100644 --- a/marshmallow_jsonschema/base.py +++ b/marshmallow_jsonschema/base.py @@ -94,9 +94,9 @@ def get_properties(self, obj): schema = field._jsonschema_type_mapping() elif field.__class__ in mapping: pytype = mapping[field.__class__] - schema = self._from_python_type(field, pytype) + schema = self.__class__._from_python_type(field, pytype) elif isinstance(field, fields.Nested): - schema = self._from_nested_schema(field) + schema = self.__class__._from_nested_schema(field) else: raise ValueError('unsupported field type %s' % field) From 30d8065461a0bf88618a2349b61e0b2441ad6e80 Mon Sep 17 00:00:00 2001 From: "Stephen J. Fuhry" Date: Tue, 12 Jul 2016 14:38:33 -0400 Subject: [PATCH 10/14] tag 0.3.0 --- Makefile | 6 ++++++ setup.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 5aefef2..13b5f0a 100644 --- a/Makefile +++ b/Makefile @@ -10,3 +10,9 @@ check: coverage: coverage erase coverage run --source marshmallow_jsonschema -m py.test -v + +pypitest: + python setup.py sdist upload -r pypitest + +pypi: + python setup.py sdist upload -r pypi diff --git a/setup.py b/setup.py index 469924b..fa0f403 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ def read(fname): setup( name='marshmallow-jsonschema', - version='0.2.1', + version='0.3.0', description='JSON Schema Draft v4 (http://json-schema.org/) formatting with marshmallow', long_description=long_description, author='Stephen Fuhry', From c587028682391f31d090332d8958fa199d956b7d Mon Sep 17 00:00:00 2001 From: "Stephen J. Fuhry" Date: Tue, 12 Jul 2016 16:58:33 -0400 Subject: [PATCH 11/14] 0.3.0 changelog --- CHANGES | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES b/CHANGES index ea93f7f..1efdab0 100644 --- a/CHANGES +++ b/CHANGES @@ -1,2 +1,5 @@ +0.3.0 (2016-06-12) + - add support for marshmallow validators (see #14) + 0.2.0 (2016-05-25) - add support for titles & descriptions in metadata From bb241e404f2abf782d96e4ceb5a98dff53e17aac Mon Sep 17 00:00:00 2001 From: Hayden Chudy Date: Mon, 18 Jul 2016 11:37:31 -0400 Subject: [PATCH 12/14] Prevent PyPandoc failing install. If PyPandoc is installed, but Pandoc is NOT, then this conversion fails with an `OSError` and nothing gets installed! Should fix the whole thing. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index fa0f403..ff0ba0f 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ def read(fname): try: import pypandoc long_description = pypandoc.convert('README.md', 'rst') -except (IOError, ImportError): +except (IOError, ImportError. OSError): long_description = read('README.md') setup( From 64e98fff9975c2255f844cd9a047ca80fae7ceca Mon Sep 17 00:00:00 2001 From: Hayden Chudy Date: Mon, 18 Jul 2016 11:59:36 -0400 Subject: [PATCH 13/14] SO glad I tested this... --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index ff0ba0f..6d828ac 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ def read(fname): try: import pypandoc long_description = pypandoc.convert('README.md', 'rst') -except (IOError, ImportError. OSError): +except (IOError, ImportError, OSError): long_description = read('README.md') setup( From 6f51771fc809a0762b3d98f4d246b70eb653d802 Mon Sep 17 00:00:00 2001 From: "Stephen J. Fuhry" Date: Sat, 6 Aug 2016 10:44:36 -0400 Subject: [PATCH 14/14] support for strings in Nested columns --- Makefile | 1 + marshmallow_jsonschema/base.py | 9 +++++++-- tests/test_dump.py | 17 +++++++++++++++++ 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 13b5f0a..2605c39 100644 --- a/Makefile +++ b/Makefile @@ -10,6 +10,7 @@ check: coverage: coverage erase coverage run --source marshmallow_jsonschema -m py.test -v + coverage report -m pypitest: python setup.py sdist upload -r pypitest diff --git a/marshmallow_jsonschema/base.py b/marshmallow_jsonschema/base.py index ffae465..fbda2aa 100644 --- a/marshmallow_jsonschema/base.py +++ b/marshmallow_jsonschema/base.py @@ -3,7 +3,8 @@ import decimal from marshmallow import fields, missing, Schema, validate -from marshmallow.compat import text_type, binary_type +from marshmallow.class_registry import get_class +from marshmallow.compat import text_type, binary_type, basestring from .validation import handle_length, handle_one_of, handle_range @@ -144,7 +145,11 @@ def _from_python_type(cls, field, pytype): @classmethod def _from_nested_schema(cls, field): - schema = cls().dump(field.nested()).data + if isinstance(field.nested, basestring): + nested = get_class(field.nested) + else: + nested = field.nested + schema = cls().dump(nested()).data if field.metadata.get('metadata', {}).get('description'): schema['description'] = ( diff --git a/tests/test_dump.py b/tests/test_dump.py index 4476fe9..039c72f 100644 --- a/tests/test_dump.py +++ b/tests/test_dump.py @@ -57,6 +57,22 @@ class TestNestedSchema(Schema): assert nested_dmp['title'] == 'Title1' +def test_nested_string_to_cls(): + class TestSchema(Schema): + foo = fields.Integer(required=True) + + class TestNestedSchema(Schema): + foo2 = fields.Integer(required=True) + nested = fields.Nested('TestSchema') + schema = TestNestedSchema() + json_schema = JSONSchema() + dumped = json_schema.dump(schema).data + _validate_schema(dumped) + nested_json = dumped['properties']['nested'] + assert nested_json['properties']['foo']['format'] == 'integer' + assert nested_json['type'] == 'object' + + def test_one_of_validator(): schema = UserSchema() json_schema = JSONSchema() @@ -64,6 +80,7 @@ def test_one_of_validator(): _validate_schema(dumped) assert dumped['properties']['sex']['enum'] == ['male', 'female'] + def test_range_validator(): schema = Address() json_schema = JSONSchema()