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
8 changes: 4 additions & 4 deletions jsonmodels/errors.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import List, Tuple, Type
from typing import Any, List, Tuple, Type


class ValidationError(RuntimeError):
Expand Down Expand Up @@ -38,7 +38,7 @@ class FieldValidationError(ValidationError):
Enriches a validator error with the name of the field that caused it.
"""
def __init__(self, model_name: str, field_name: str,
given_value: any, error: ValidatorError):
given_value: Any, error: ValidatorError):
"""
:param model_name: The name of the model.
:param field_name: The name of the field.
Expand Down Expand Up @@ -78,7 +78,7 @@ class BadTypeError(ValidatorError):
expected one
"""

def __init__(self, value: any, types: Tuple, is_list: bool):
def __init__(self, value: Any, types: Tuple, is_list: bool):
"""
:param value: The given value.
:param types: The accepted types.
Expand Down Expand Up @@ -186,7 +186,7 @@ def __init__(self, value, maximum_value, exclusive: bool):
class EnumError(ValidatorError):
""" Error raised by the Enum validator """

def __init__(self, value: any, choices: List[any]):
def __init__(self, value: Any, choices: List[Any]):
"""
:param value: The given value.
:param choices: The allowed choices.
Expand Down
115 changes: 68 additions & 47 deletions jsonmodels/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,15 @@
import re
import six
from dateutil.parser import parse
from typing import List, Optional, Dict, Set, Union, Pattern
from typing import Any, List, Generic, Optional, Dict, Sequence, Set, Tuple, TypeVar, Union, Pattern

from .collections import ModelCollection
from .errors import RequiredFieldError, BadTypeError, AmbiguousTypeError

MYPY = False
if MYPY:
from .models import Base

# unique marker for "no default value specified". None is not good enough since
# it is a completely valid default value.
NotSet = object()
Expand All @@ -21,18 +25,24 @@
]


class BaseField(object):
T = TypeVar("T")


class BaseField(Generic[T]):

"""Base class for all fields."""

types = None
types: Tuple[Any, ...] = ()

validators: List[Any] = []
memory: WeakKeyDictionary

def __init__(
self,
required=False,
nullable=False,
help_text=None,
validators=None,
validators: Optional[List[Any]]=None,
sedwards2009 marked this conversation as resolved.
Show resolved Hide resolved
default=NotSet,
name=None):
self.memory = WeakKeyDictionary()
Expand All @@ -47,31 +57,31 @@ def __init__(
self._default = default

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

def _assign_validators(self, validators):
def _assign_validators(self, validators) -> None:
if validators and not isinstance(validators, list):
validators = [validators]
self.validators = validators or []

def __set__(self, instance, value):
def __set__(self, instance: "Base", value: Optional[T]) -> None:
self._finish_initialization(type(instance))
value = self.parse_value(value)
self.validate(value)
self.memory[instance._cache_key] = value

def __get__(self, instance, owner=None):
def __get__(self, instance: "Base", owner=None) -> T:
if instance is None:
self._finish_initialization(owner)
return self
return self # type: ignore

self._finish_initialization(type(instance))

self._check_value(instance)
return self.memory[instance._cache_key]

def _finish_initialization(self, owner):
def _finish_initialization(self, owner) -> None:
pass

def _check_value(self, obj):
Expand All @@ -82,21 +92,21 @@ def validate_for_object(self, obj):
value = self.__get__(obj)
self.validate(value)

def validate(self, value):
def validate(self, value: Optional[T]) -> None:
self._check_types()
self._validate_against_types(value)
self._check_against_required(value)
self._validate_with_custom_validators(value)

def _check_against_required(self, value):
def _check_against_required(self, value) -> None:
if value is None and self.required:
raise RequiredFieldError()

def _validate_against_types(self, value):
def _validate_against_types(self, value) -> None:
if value is not None and not isinstance(value, self.types):
raise BadTypeError(value, self.types, is_list=False)

def _check_types(self):
def _check_types(self) -> None:
if self.types is None:
tpl = 'Field "{type}" is not usable, try different field type.'
raise ValueError(tpl.format(type=type(self).__name__))
Expand Down Expand Up @@ -131,7 +141,7 @@ def _get_embed_type(value, models):
return matching_models[0]
return models[0]

def toBsonEncodable(self, value: types) -> BsonEncodable:
def toBsonEncodable(self, value) -> BsonEncodable:
"""Optionally return a bson encodable python object.

Returned object should be BSON compatible. By default uses the
Expand All @@ -153,7 +163,7 @@ def to_struct(self, value):
"""Cast value to Python dict."""
return value

def parse_value(self, value):
def parse_value(self, value: Optional[Any]) -> Optional[T]:
"""Parse value from primitive to desired format.

Each field can parse value to form it wants it to be (like string or
Expand Down Expand Up @@ -195,18 +205,18 @@ def structue_name(self, default):
return self.structure_name(default)


class StringField(BaseField):
class StringField(BaseField[str]):

"""String field."""

types = six.string_types
types: Tuple[Any, ...] = six.string_types


class IntField(BaseField):
class IntField(BaseField[int]):

"""Integer field."""

types = (int,)
types: Tuple[Any, ...] = (int,)

def parse_value(self, value):
"""Cast value to `int`, e.g. from string or long"""
Expand All @@ -219,32 +229,37 @@ def parse_value(self, value):
raise BadTypeError(value, types=(int,), is_list=False)


class FloatField(BaseField):
class FloatField(BaseField[float]):

"""Float field."""

types = (float, int)
types: Tuple[Any, ...] = (float, int)


class BoolField(BaseField):
class BoolField(BaseField[bool]):

"""Bool field."""

types = (bool,)
types: Tuple[Any, ...] = (bool,)

def parse_value(self, value):
"""Cast value to `bool`."""
parsed = super(BoolField, self).parse_value(value)
return bool(parsed) if parsed is not None else None


class ListField(BaseField):
I = TypeVar("I")


class ListField(BaseField[List[I]]):

"""List field."""

types = (list, tuple)
types: Tuple[Any, ...] = (list, tuple)
items_types: Tuple[Any, ...]
item_validators: List[Any]

def __init__(self, items_types=None, item_validators=(), omit_empty=False,
def __init__(self, items_types: Optional[List[Any]]=None, item_validators: Union[Any, List[Any]]=[], omit_empty=False,
*args, **kwargs):
"""Init.

Expand Down Expand Up @@ -359,13 +374,15 @@ def __init__(self, field: BaseField, *args, **kwargs):
:param validators: The validators for the list field.
"""
self._field = field

fixed_kwargs = kwargs.copy()
fixed_kwargs["items_types"] = field.types
fixed_kwargs["item_validators"] = field.validators
super(DerivedListField, self).__init__(
items_types=field.types,
item_validators=field.validators,
*args, **kwargs,
*args, **fixed_kwargs,
)

def to_struct(self, values: List[any]) -> List[any]:
def to_struct(self, values: List[Any]) -> Optional[List[Any]]:
"""
Converts the list to its output format.
:param values: The values in the list.
Expand All @@ -374,18 +391,22 @@ def to_struct(self, values: List[any]) -> List[any]:
return [self._field.to_struct(value) for value in values] \
if values or not self._omit_empty else None

def parse_value(self, values: List[any]) -> List[any]:
def parse_value(self, values: Optional[Any]) -> Optional[List[Any]]:
"""
Converts the list to its internal format.
:param values: The values in the list.
:return: The converted values.
"""
if values is None:
return None

try:
return [self._field.parse_value(value) for value in values]
except TypeError:
raise BadTypeError(values, self._field.types, is_list=True)
return None

def validate_single_value(self, value: any) -> None:
def validate_single_value(self, value: Any) -> None:
"""
Validates a single value in the list.
:param value: One of the values in the list.
Expand Down Expand Up @@ -443,7 +464,7 @@ def to_struct(self, value):
return value.to_struct()


class MapField(BaseField):
class MapField(BaseField[Dict[Any, Any]]):
"""
Model field that keeps a mapping between two other fields.
It is basically a dictionary with key and values being separate fields.
Expand All @@ -453,7 +474,7 @@ class MapField(BaseField):
included in the to_struct method.

"""
types = (dict,)
types: Tuple[Any, ...] = (dict,)

def __init__(self, key_field: BaseField, value_field: BaseField,
**kwargs):
Expand All @@ -476,26 +497,26 @@ def _finish_initialization(self, owner):
self._key_field._finish_initialization(owner)
self._value_field._finish_initialization(owner)

def get_default_value(self) -> any:
def get_default_value(self) -> Any:
""" Gets the default value for this field """
default = super(MapField, self).get_default_value()
if default is None and self.required:
return dict()
return default

def parse_value(self, values: Optional[dict]) -> Optional[dict]:
def parse_value(self, values: Optional[Any]) -> Optional[Dict[Any, Any]]:
""" Parses the given values into a new dict. """
values = super().parse_value(values)
if values is None:
return
return None
items = [
(self._key_field.parse_value(key),
self._value_field.parse_value(value))
for key, value in values.items()
]
return type(values)(items) # Preserves OrderedDict

def to_struct(self, values: Optional[dict]) -> Optional[dict]:
def to_struct(self, values: Dict[Any, Any]) -> Dict[Any, Any]:
""" Casts the field values into a dict. """
items = [
(self._key_field.to_struct(key),
Expand All @@ -504,7 +525,7 @@ def to_struct(self, values: Optional[dict]) -> Optional[dict]:
]
return type(values)(items) # Preserves OrderedDict

def validate(self, values: Optional[dict]) -> Optional[dict]:
def validate(self, values: Optional[Dict[Any, Any]]) -> None:
"""
Validates all keys and values in the map field.
:param values: The values in the mapping.
Expand Down Expand Up @@ -567,7 +588,7 @@ class TimeField(StringField):

"""Time field."""

types = (datetime.time,)
types: Tuple[Any, ...] = (datetime.time,)

def __init__(self, str_format=None, *args, **kwargs):
"""Init.
Expand Down Expand Up @@ -598,7 +619,7 @@ class DateField(StringField):

"""Date field."""

types = (datetime.date,)
types: Tuple[Any, ...] = (datetime.date,)
default_format = '%Y-%m-%d'

def __init__(self, str_format=None, *args, **kwargs):
Expand Down Expand Up @@ -630,7 +651,7 @@ class DateTimeField(StringField):

"""Datetime field."""

types = (datetime.datetime,)
types: Tuple[Any, ...] = (datetime.datetime,)

def __init__(self, str_format=None, *args, **kwargs):
"""Init.
Expand All @@ -648,7 +669,7 @@ def to_struct(self, value):
return value.strftime(self.str_format)
return value.isoformat()

def toBsonEncodable(self, value: datetime) -> datetime:
def toBsonEncodable(self, value: datetime.datetime) -> datetime.datetime:
"""
Keep datetime object a datetime object, since pymongo supports that.
"""
Expand All @@ -666,17 +687,17 @@ def parse_value(self, value):
return None


class GenericField(BaseField):
class GenericField(BaseField[Any]):
"""
Field that supports any kind of value, converting models to their correct
struct, keeping ordered dictionaries in their original order.
"""
types = (any,)
types: Tuple[Any, ...] = (any,)

def _validate_against_types(self, value) -> None:
pass

def to_struct(self, values: any) -> any:
def to_struct(self, values: Any) -> Any:
""" Casts value to Python structure. """
from .models import Base
if isinstance(values, Base):
Expand Down
4 changes: 3 additions & 1 deletion jsonmodels/utilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
import six
import re
from collections import namedtuple
from typing import cast, Any, List, Tuple

SCALAR_TYPES = tuple(list(six.string_types) + [int, float, bool])
six_string_types: List[Any] = list(six.string_types)
SCALAR_TYPES = cast(Tuple[Any], tuple(six_string_types + [int, float, bool]))

ECMA_TO_PYTHON_FLAGS = {
'i': re.I,
Expand Down
Loading