Skip to content

Commit

Permalink
Merge branch 'master' into add/pyproject
Browse files Browse the repository at this point in the history
  • Loading branch information
beregond authored Jun 14, 2021
2 parents 6284616 + 4a7a4f2 commit ab492ab
Show file tree
Hide file tree
Showing 19 changed files with 278 additions and 43 deletions.
1 change: 1 addition & 0 deletions docs/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ fields classes from :mod:`jsonmodels.fields`).
name = fields.StringField(required=True)
breed = fields.StringField()
extra = fields.DictField()
class Dog(models.Base):
Expand Down
9 changes: 9 additions & 0 deletions jsonmodels/builders.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,13 @@ def _apply_validators_modifications(field_schema, field):
except AttributeError:
pass

if "items" in field_schema:
for validator in field.item_validators:
try:
validator.modify_schema(field_schema["items"])
except AttributeError: # Case when validator is simple function.
pass


class PrimitiveBuilder(Builder):

Expand All @@ -143,6 +150,8 @@ def build(self):
obj_type = 'number'
elif issubclass(self.type, float):
obj_type = 'number'
elif issubclass(self.type, dict):
obj_type = 'object'
else:
raise errors.FieldNotSupported(
"Can't specify value schema!", self.type
Expand Down
28 changes: 20 additions & 8 deletions jsonmodels/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,20 +180,29 @@ def parse_value(self, value):
return bool(parsed) if parsed is not None else None


class DictField(BaseField):

"""Dict field."""

types = (dict, )


class ListField(BaseField):

"""List field."""

types = (list,)

def __init__(self, items_types=None, *args, **kwargs):
def __init__(self, items_types=None, item_validators=(), *args, **kwargs):
"""Init.
`ListField` is **always not required**. If you want to control number
of items use validators.
of items use validators. If you want to validate each individual item,
use `item_validators`.
"""
self._assign_types(items_types)
self.item_validators = item_validators
super().__init__(*args, **kwargs)
self.required = False

Expand Down Expand Up @@ -223,22 +232,25 @@ def _assign_types(self, items_types):
def validate(self, value):
super().validate(value)

if len(self.items_types) == 0:
return

for item in value:
self.validate_single_value(item)

def validate_single_value(self, item):
def validate_single_value(self, value):
for validator in self.item_validators:
try:
validator.validate(value)
except AttributeError: # Case when validator is simple function.
validator(value)

if len(self.items_types) == 0:
return

if not isinstance(item, self.items_types):
if not isinstance(value, self.items_types):
raise ValidationError(
'All items must be instances '
'of "{types}", and not "{type}".'.format(
types=', '.join([t.__name__ for t in self.items_types]),
type=type(item).__name__,
type=type(value).__name__,
))

def parse_value(self, values):
Expand Down
13 changes: 11 additions & 2 deletions jsonmodels/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,11 @@ def populate(self, **values):
fields = list(self.iterate_with_name())
for _, structure_name, field in fields:
if structure_name in values:
field.__set__(self, values.pop(structure_name))
self.set_field(field, structure_name,
values.pop(structure_name))
for name, _, field in fields:
if name in values:
field.__set__(self, values.pop(name))
self.set_field(field, name, values.pop(name))

def get_field(self, field_name):
"""Get field associated with given attribute."""
Expand All @@ -50,6 +51,14 @@ def get_field(self, field_name):

raise errors.FieldNotFound('Field not found', field_name)

def set_field(self, field, field_name, value):
""" Sets the value of a field. """
try:
field.__set__(self, value)
except ValidationError as error:
raise ValidationError("Error for field '{name}': {error}."
.format(name=field_name, error=error))

def __iter__(self):
"""Iterate through fields and values."""
yield from self.iterate_over_fields()
Expand Down
6 changes: 4 additions & 2 deletions jsonmodels/parsers.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,15 +65,15 @@ def _parse_list(field, parent_builder):
parent_builder, field.nullable, default=field._default)
for type in field.items_types:
builder.add_type_schema(build_json_schema(type, builder))
return builder
return builder.build()


def _parse_embedded(field, parent_builder):
builder = builders.EmbeddedBuilder(
parent_builder, field.nullable, default=field._default)
for type in field.types:
builder.add_type_schema(build_json_schema(type, builder))
return builder
return builder.build()


def build_json_schema_primitive(cls, parent_builder):
Expand All @@ -90,6 +90,8 @@ def _create_primitive_field_schema(field):
obj_type = 'float'
elif isinstance(field, fields.BoolField):
obj_type = 'boolean'
elif isinstance(field, fields.DictField):
obj_type = 'object'
else:
raise errors.FieldNotSupported(
'Field {field} is not supported!'.format(
Expand Down
21 changes: 9 additions & 12 deletions jsonmodels/utilities.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import sre_constants

import re
from collections import namedtuple

Expand Down Expand Up @@ -93,19 +95,14 @@ def is_ecma_regex(regex):
:rtype: bool
"""
parts = regex.split('/')

if len(parts) == 1:
return False

if len(parts) < 3:
raise ValueError('Given regex isn\'t ECMA regex nor Python regex.')
parts.pop()
parts.append('')

raw_regex = '/'.join(parts)
if raw_regex.startswith('/') and raw_regex.endswith('/'):
if re.match(r"/[^/]+/[gimuy]*", regex):
return True

try:
re.compile(regex)
except sre_constants.error as err:
raise ValueError("Given regex {} isn't ECMA regex nor "
"Python regex: {}.".format(regex, err))
return False


Expand Down
8 changes: 6 additions & 2 deletions jsonmodels/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,11 +174,15 @@ def validate(self, value):

def modify_schema(self, field_schema):
"""Modify field schema."""
is_array = field_schema.get('type') == 'array'

if self.minimum_value:
field_schema['minLength'] = self.minimum_value
key = 'minItems' if is_array else 'minLength'
field_schema[key] = self.minimum_value

if self.maximum_value:
field_schema['maxLength'] = self.maximum_value
key = 'maxItems' if is_array else 'maxLength'
field_schema[key] = self.maximum_value


class Enum:
Expand Down
3 changes: 2 additions & 1 deletion tests/fixtures/schema1.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
"properties": {
"name": {"type": "string"},
"surname": {"type": "string"},
"age": {"type": "number"}
"age": {"type": "number"},
"extra": {"type": "object"}
},
"required": ["name", "surname"],
"additionalProperties": false
Expand Down
5 changes: 3 additions & 2 deletions tests/fixtures/schema2.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@
"type": "object",
"properties": {
"brand": {"type": "string"},
"registration": {"type": "string"}
"registration": {"type": "string"},
"extra": {"type": "object"}
},
"required": ["brand", "registration"],
"required": ["brand", "registration","extra"],
"additionalProperties": false
},
"kids": {
Expand Down
50 changes: 50 additions & 0 deletions tests/fixtures/schema_length_list.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
{
"type": "object",
"additionalProperties": false,
"properties": {
"min_max_len": {
"type": "array",
"items": {
"type": "string"
},
"minItems": 2,
"maxItems": 4
},
"min_len": {
"type": "array",
"items": {
"type": "string"
},
"minItems": 2
},
"max_len": {
"type": "array",
"items": {
"type": "string"
},
"minItems": 4
},
"item_validator_int": {
"type": "array",
"items": {
"type": "number",
"minimum": 10,
"maximum": 20
}
},
"item_validator_str": {
"type": "array",
"items": {
"type": "string",
"minLength": 10,
"maxLength": 20,
"pattern": "/\\w+/"
},
"minItems": 1,
"maxItems": 2
},
"surname": {
"type": "string"
}
}
}
12 changes: 12 additions & 0 deletions tests/fixtures/schema_list_item_simple.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"type": "object",
"additionalProperties": false,
"properties": {
"lucky_numbers": {
"type": "array",
"items": {
"type": "number"
}
}
}
}
16 changes: 15 additions & 1 deletion tests/test_data_initialization.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@ class Person(models.Base):
surname = fields.StringField()
age = fields.IntField()
cash = fields.FloatField()
extra_data = fields.DictField()

data = dict(
name='Alan',
surname='Wake',
age=24,
cash=2445.45,
extra_data={"location": "Oviedo, Spain", "gender": "Unknown"},
trash='123qwe',
)

Expand All @@ -29,6 +31,8 @@ class Person(models.Base):
assert alan.surname == 'Wake'
assert alan.age == 24
assert alan.cash == 2445.45
assert alan.extra_data == {"location": "Oviedo, Spain",
"gender": "Unknown"}

assert not hasattr(alan, 'trash')

Expand All @@ -38,6 +42,7 @@ def test_deep_initialization():
class Car(models.Base):

brand = fields.StringField()
extra = fields.DictField()

class ParkingPlace(models.Base):

Expand All @@ -47,7 +52,10 @@ class ParkingPlace(models.Base):
data = {
'location': 'somewhere',
'car': {
'brand': 'awesome brand'
'brand': 'awesome brand',
'extra': {"extra_int": 1, "extra_str": "a",
"extra_bool": True,
"extra_dict": {"I am extra": True}}
}
}

Expand All @@ -59,11 +67,17 @@ class ParkingPlace(models.Base):
car = parking.car
assert isinstance(car, Car)
assert car.brand == 'awesome brand'
assert car.extra == {"extra_int": 1, "extra_str": "a",
"extra_bool": True,
"extra_dict": {"I am extra": True}}

assert parking.location == 'somewhere'
car = parking.car
assert isinstance(car, Car)
assert car.brand == 'awesome brand'
assert car.extra == {"extra_int": 1, "extra_str": "a",
"extra_bool": True,
"extra_dict": {"I am extra": True}}


def test_deep_initialization_error_with_multitypes():
Expand Down
27 changes: 26 additions & 1 deletion tests/test_fields.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from jsonmodels import models, fields
from jsonmodels import models, fields, validators


def test_bool_field():
Expand Down Expand Up @@ -28,3 +28,28 @@ class Person(models.Base):
assert field.parse_value(0) is False
assert field.parse_value('') is False
assert field.parse_value([]) is False


def test_dict_field():

field = fields.DictField()
default_field = fields.DictField(default={
"extra_default": "Hello",
"deep_extra": {"spanish": "Hola"}},
validators=[validators.Length(2)])

class Person(models.Base):

extra = field
extra_required = fields.DictField(required=True)
extra_default = default_field
extra_nullable = fields.DictField(nullable=True)

person = Person(extra_required={"required": True})
assert person.extra is None
assert person.extra_required == {"required": True}
assert person.extra_default == {"extra_default": "Hello",
"deep_extra": {"spanish": "Hola"}}

person.extra = {"extra": True}
assert person.extra == {"extra": True}
Loading

0 comments on commit ab492ab

Please sign in to comment.