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

Make Mypy & JSON models friends #18

Draft
wants to merge 12 commits into
base: master
Choose a base branch
from
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Install pytest python library as well as add all files in current directory
FROM python:3.7 AS base
FROM python:3.11 AS base
WORKDIR /usr/src/app
RUN apt-get update \
&& apt-get install -y enchant \
Expand Down
154 changes: 84 additions & 70 deletions jsonmodels/builders.py
Original file line number Diff line number Diff line change
@@ -1,79 +1,95 @@
"""Builders to generate in memory representation of model and fields tree."""

from __future__ import absolute_import

from collections import defaultdict

from typing import Any, Dict, List, Optional, Set
import six

from . import errors
from .fields import NotSet

from .fields import NotSet, Value
from .types import Builder, Field, JSONSchemaProperty, JSONSchemaTypeName, Model

class Builder(object):

def __init__(self, parent=None, nullable=False, default=NotSet):
class BaseBuilder:
def __init__(
self,
parent: Optional[Builder] = None,
nullable: bool = False,
default: Any = NotSet,
) -> None:
self.parent = parent
self.types_builders = {}
self.types_count = defaultdict(int)
self.definitions = set()
self.types_builders: Dict[type[Model], Builder] = {}
self.types_count: Dict[type[Model], int] = defaultdict(int)
self.definitions: Set[Builder] = set()
self.nullable = nullable
self.default = default

@property
def has_default(self):
def has_default(self) -> bool:
return self.default is not NotSet

def register_type(self, type, builder):
def register_type(self, model_type: type[Model], builder: Builder) -> None:
if self.parent:
return self.parent.register_type(type, builder)
self.parent.register_type(model_type, builder)
return

self.types_count[type] += 1
if type not in self.types_builders:
self.types_builders[type] = builder
self.types_count[model_type] += 1
if model_type not in self.types_builders:
self.types_builders[model_type] = builder

def get_builder(self, type):
def get_builder(self, model_type: type[Model]) -> Builder:
if self.parent:
return self.parent.get_builder(type)
return self.parent.get_builder(model_type)

return self.types_builders[type]
return self.types_builders[model_type]

def count_type(self, type):
def count_type(self, model_type: type[Model]) -> int:
if self.parent:
return self.parent.count_type(type)
return self.parent.count_type(model_type)

return self.types_count[type]
return self.types_count[model_type]

@staticmethod
def maybe_build(value):
def maybe_build(value: Value) -> JSONSchemaProperty | Value:
return value.build() if isinstance(value, Builder) else value

def add_definition(self, builder):
def add_definition(self, builder: Builder) -> None:
if self.parent:
return self.parent.add_definition(builder)

self.definitions.add(builder)

def build_definition(self, add_definitions: bool = True) -> JSONSchemaProperty:
raise NotImplementedError()

@property
def is_definition(self) -> bool:
raise NotImplementedError()

@property
def type_name(self) -> str:
raise NotImplementedError()

class ObjectBuilder(Builder):

def __init__(self, model_type, *args, **kwargs):
super(ObjectBuilder, self).__init__(*args, **kwargs)
self.properties = {}
self.required = []
class ObjectBuilder(BaseBuilder):
def __init__(self, model_type: type[Model], *args: Any, **kwargs: Any):
super().__init__(*args, **kwargs)
self.properties: Dict[str, str | JSONSchemaProperty] = {}
self.required: List[str] = []
self.type = model_type

self.register_type(self.type, self)

def add_field(self, name, field, schema):
_apply_validators_modifications(schema, field)
def add_field(self, name: str, field: Field, schema: str | JSONSchemaProperty) -> None:
if not isinstance(schema, str):
_apply_validators_modifications(schema, field)
if isinstance(schema, dict) and field.help_text:
schema["description"] = field.help_text
self.properties[name] = schema
if field.required:
self.required.append(name)

def build(self):
def build(self) -> str | JSONSchemaProperty:
builder = self.get_builder(self.type)
if self.is_definition and not self.is_root:
self.add_definition(builder)
Expand All @@ -83,27 +99,27 @@ def build(self):
return builder.build_definition()

@property
def type_name(self):
def type_name(self) -> str:
module_name = '{module}.{name}'.format(
module=self.type.__module__,
name=self.type.__name__,
)
return module_name.replace('.', '_').lower()

def build_definition(self, add_definitions=True):
properties = dict(
def build_definition(self, add_definitions: bool = True) -> JSONSchemaProperty:
properties: Dict[str, str | JSONSchemaProperty] = dict(
(name, self.maybe_build(value))
for name, value
in self.properties.items()
)
schema = {
schema: JSONSchemaProperty = {
'type': 'object',
'additionalProperties': False,
'properties': properties,
}

if self.required:
schema['required'] = self.required
schema['required'] = list(self.required)

if self.definitions and add_definitions:
schema['definitions'] = dict(
Expand All @@ -114,7 +130,7 @@ def build_definition(self, add_definitions=True):
return schema

@property
def is_definition(self):
def is_definition(self) -> bool:
if self.count_type(self.type) > 1:
return True
elif self.parent:
Expand All @@ -123,35 +139,30 @@ def is_definition(self):
return False

@property
def is_root(self):
def is_root(self) -> bool:
return not bool(self.parent)


def _apply_validators_modifications(field_schema, field):
def _apply_validators_modifications(field_schema: JSONSchemaProperty, field: Field) -> None:
for validator in field.validators:
try:
if hasattr(validator, "modify_schema"):
validator.modify_schema(field_schema)
except AttributeError:
pass

# arrays may have separate validators for each item.
# we should also add those validators to the schema.
if "items" in field_schema:
for validator in field.item_validators:
try:
if hasattr(validator, "modify_schema"):
validator.modify_schema(field_schema["items"])
except AttributeError:
pass


class PrimitiveBuilder(Builder):

def __init__(self, type, *args, **kwargs):
super(PrimitiveBuilder, self).__init__(*args, **kwargs)
self.type = type
class PrimitiveBuilder(BaseBuilder):
def __init__(self, value_type: type, *args: Any, **kwargs: Any):
super().__init__(*args, **kwargs)
self.type = value_type

def build(self):
schema = {}
def build(self) -> JSONSchemaProperty:
obj_type: JSONSchemaTypeName
schema: JSONSchemaProperty = {}
if issubclass(self.type, six.string_types):
obj_type = 'string'
elif issubclass(self.type, bool):
Expand All @@ -174,19 +185,21 @@ def build(self):
return schema


class ListBuilder(Builder):
class ListBuilder(BaseBuilder):

def __init__(self, *args, **kwargs):
super(ListBuilder, self).__init__(*args, **kwargs)
self.schemas = []
parent: Builder

def add_type_schema(self, schema):
def __init__(self, *args: Any, **kwargs: Any):
super().__init__(*args, **kwargs)
self.schemas: list[Builder | JSONSchemaProperty] = []

def add_type_schema(self, schema: Builder | JSONSchemaProperty) -> None:
self.schemas.append(schema)

def build(self):
schema = {'type': 'array'}
def build(self) -> str | JSONSchemaProperty:
schema: JSONSchemaProperty = {'type': 'array'}
if self.nullable:
self.add_type_schema({'type': 'null'})
self.add_type_schema({'type': 'null'}) # <- probably a bug

if self.has_default:
schema["default"] = [self.to_struct(i) for i in self.default]
Expand All @@ -201,27 +214,28 @@ def build(self):
return schema

@property
def is_definition(self):
def is_definition(self) -> bool:
return self.parent.is_definition

@staticmethod
def to_struct(item):
def to_struct(item: Value) -> Value:
from .models import Base
if isinstance(item, Base):
return item.to_struct()
return item


class EmbeddedBuilder(Builder):
class EmbeddedBuilder(BaseBuilder):
parent: Builder

def __init__(self, *args, **kwargs):
super(EmbeddedBuilder, self).__init__(*args, **kwargs)
self.schemas = []
def __init__(self, *args: Any, **kwargs: Any):
super().__init__(*args, **kwargs)
self.schemas: list[Builder | JSONSchemaProperty] = []

def add_type_schema(self, schema):
def add_type_schema(self, schema: Builder | JSONSchemaProperty) -> None:
self.schemas.append(schema)

def build(self):
def build(self) -> JSONSchemaProperty:
if self.nullable:
self.add_type_schema({'type': 'null'})

Expand All @@ -239,5 +253,5 @@ def build(self):
return schema

@property
def is_definition(self):
def is_definition(self) -> bool:
return self.parent.is_definition
14 changes: 9 additions & 5 deletions jsonmodels/collections.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@

from typing import Any, Iterable
from .types import CollectionField
from typing_extensions import override

class ModelCollection(list):

Expand All @@ -9,14 +11,16 @@ class ModelCollection(list):

"""

def __init__(self, field):
def __init__(self, field: CollectionField) -> None:
super(ModelCollection, self).__init__()
self.field = field

def append(self, value):
@override
def append(self, value: Any) -> None:
self.field.validate_single_value(value)
super(ModelCollection, self).append(value)

def __setitem__(self, key, value):
@override
def __setitem__(self, index: Any, value: Any, /) -> None:
self.field.validate_single_value(value)
super(ModelCollection, self).__setitem__(key, value)
super(ModelCollection, self).__setitem__(index, value)
Loading