Skip to content

Commit

Permalink
Support type hints for specifying property types
Browse files Browse the repository at this point in the history
  • Loading branch information
eblade committed Apr 23, 2019
1 parent 5a508b7 commit 9a72900
Show file tree
Hide file tree
Showing 8 changed files with 147 additions and 8 deletions.
43 changes: 43 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,49 @@ Example
}
Type Hinting
------------

You can also specify types for properties with Type Hinting, if available:

.. code-block:: python
>>> from json import dumps
>>> from typing import List
>>> from jsonobject import Property, PropertySet, EnumProperty
>>> class Wheel(PropertySet):
... diameter: float = Property(default=1.)
>>> class Rating(EnumProperty):
... ok = 'ok'
... bad = 'bad'
... good = 'good'
>>> class Car(PropertySet):
... wheels: List[Wheel] = Property()
... brand = Property()
... model = Property()
... rating: Rating = Property(default=Rating.ok)
>>> volvo = Car(brand='Volvo', model='V90', rating=Rating.good, wheels=[])
>>> volvo.wheels.append(Wheel(diameter=3.))
>>> print(volvo.to_json())
{
"*schema": "Car",
"brand": "Volvo",
"model": "V90",
"rating": "good",
"wheels": [
{
"*schema": "Wheel",
"diameter": 3.0
}
]
}
Schema-Less
-----------

Expand Down
47 changes: 39 additions & 8 deletions jsonobject/types.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import json
import urllib

try:
import typing
has_typing = True
except ImportError:
has_typing = False

from enum import Enum
from .schema import wrap_dict, wrap_raw_json

Expand All @@ -9,14 +16,7 @@ def __init__(self, type=str, name=None, default=None, enum=None,
required=False, validator=None, wrap=False, none=None,
is_list=False, calculated=False):

if type not in (str, int, float, bool) or is_list:
if default is not None:
raise ValueError('Can only use default values for simple types')
elif none is not None:
raise ValueError('None value only makes sense for simple types')

if is_list and not issubclass(type, PropertySet):
raise ValueError('Please use is_list only with ProperySet')
self._check_type(type, is_list, default, none)

self.serialized_name = name
self.type = enum if enum else type
Expand All @@ -30,12 +30,43 @@ def __init__(self, type=str, name=None, default=None, enum=None,
self._property_name = None
self.calculated = calculated

@staticmethod
def _check_type(type, is_list, default, none):
if type not in (str, int, float, bool) or is_list:
if default is not None:
raise ValueError('Can only use default values for simple types')
elif none is not None:
raise ValueError('None value only makes sense for simple types')

if is_list and not issubclass(type, PropertySet):
raise ValueError('Please use is_list only with ProperySet')

def __property_config__(self, model_class, property_name):
self.model_class = model_class
self._property_name = property_name
if self.serialized_name is None:
self.serialized_name = property_name

if has_typing and hasattr(model_class, '__annotations__'):
annotation = model_class.__annotations__.get(property_name)
if annotation is not None:
if issubclass(annotation, typing.List):
is_list = True
if len(annotation.__args__) != 1:
raise ValueError('List annotations must be typed')
type = annotation.__args__[0]
elif issubclass(annotation, EnumProperty):
is_list = False
type = str
self.enum = annotation
else:
is_list = False
type = annotation

self._check_type(type, is_list, self.default, self.none)
self.is_list = is_list
self.type = type

def __get__(self, model_instance, model_class):
if model_instance is None:
return self
Expand Down
1 change: 1 addition & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
norecursedirs = .git .cache scripts build dist conda-recipe benchmarks __pycache__ doc
pep8ignore =
doc/conf.py ALL
tests/*.py E701
pep8maxlinelength = 119
flakes-ignore =
__init__.py UnusedImport
Expand Down
9 changes: 9 additions & 0 deletions tests/test_booleans.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,3 +153,12 @@ class O(PropertySet):
del o.s

assert o.s is None


def test_type_via_hint():
class O(PropertySet):
s: bool = Property()

o = O(s='yes')

assert type(o.s) is bool
9 changes: 9 additions & 0 deletions tests/test_dicts.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,3 +109,12 @@ class O(PropertySet):
o = O()

assert type(o.s) is dict


def test_via_hint():
class O(PropertySet):
s: dict = Property()

o = O()

assert type(o.s) is dict
18 changes: 18 additions & 0 deletions tests/test_is_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import pytest
from jsonobject import PropertySet, Property
from typing import List


def test_list_of_property_sets():
Expand Down Expand Up @@ -165,3 +166,20 @@ class P(PropertySet):
with pytest.raises(ValueError):
class O(PropertySet):
s = Property(P, is_list=True, default=1)


def test_list_via_hint():
class P(PropertySet):
s: str = Property()

class O(PropertySet):
s: List[P] = Property()

p1 = P()
p2 = P()
p3 = P()
o = O(s=[p1, p2, p3])

assert len(o.s) == 3
assert o.s == [p1, p2, p3]
assert o.to_dict() == O(o.to_dict()).to_dict()
19 changes: 19 additions & 0 deletions tests/test_numbers.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,15 @@ class O(PropertySet):
assert o.s == 1


def test_type_hint_set_via_constructor():
class O(PropertySet):
s: int = Property()

o = O(s=1)

assert o.s == 1


def test_set_via_attribute():
class O(PropertySet):
s = Property(int)
Expand All @@ -23,6 +32,16 @@ class O(PropertySet):
assert o.s == 1


def test_type_hint_set_via_attribute():
class O(PropertySet):
s: int = Property()

o = O()
o.s = 1

assert o.s == 1


def test_set_default():
class O(PropertySet):
s = Property(int, default=1)
Expand Down
9 changes: 9 additions & 0 deletions tests/test_strings.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,15 @@ class O(PropertySet):
assert o.s == 'test'


def test_type_hint_set_via_constructor():
class O(PropertySet):
s: str = Property()

o = O(s='test')

assert o.s == 'test'


def test_set_via_attribute():
class O(PropertySet):
s = Property()
Expand Down

0 comments on commit 9a72900

Please sign in to comment.