Skip to content

squashed updates, including support for 3.9 #52

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

Merged
merged 1 commit into from
Nov 7, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ python:
- "3.6"
- "3.7"
- "3.8"
- "3.9"
install:
- pip install tox-travis
script:
Expand Down
15 changes: 14 additions & 1 deletion docs/tutorial-basics,rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,19 @@
Tutorial - Basics
=================

Let's start with a scenario: You created a function that processes trades. A trade has many parameters:
Let's start with a scenario: You created a system that processes trades. A trade has many parameters. Some of them:
* price - a positive integer
* quantity
* Participants details
* identifiers of instrument
* date and time
* venue
The trades are passed around by throughout the system. Typically every function/component validates the content of the
parameters. This can result in a a lot of boilerplate code, inconsistencies in the expectations from the values of the
properties. For example, Different functions can expect different date format.
Ideally, the specification should be expressed decleratively, and the trade object will be guranteed to conform to the specs.
This means that the validation is self-contained.
This where Typedpy shines. We could define something like the following:



8 changes: 7 additions & 1 deletion docs/tutorial.rst
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ Next, will look at immutability.
With dataclass, although the class is "frozen" (i.e. supposed to be immutable), we can do the following:

.. code-block:: python

@dataclass(frozen=True)
class FooDataClass:
a: dict
Expand Down Expand Up @@ -144,7 +145,12 @@ Let's examine inheritance. In the following code:
We forgot to add the dataclass decorator to Bar, but it inherits from FooDataClass. So is it a dataclass or not?

It is, but probably not what we intended. Its constructor looks exactly like FooDataClass, and it ignores the fields \
in its own body. So it is a dataclass, but ignores its own spec.
in its own body. So it is a dataclass, but ignores its own spec. So the valid instantiation of Bar looks like:

.. code-block:: python

Bar(a=[5], i=5, t=[5])

This is an unintuitive outcome (if we add the dataclass decorator to it, and then Bar will behave as expected).

In Typedpy, inheritance works the way we expect:
Expand Down
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
]

setup(
Expand Down
16 changes: 15 additions & 1 deletion tests/test_Set.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from pytest import raises

from typedpy import Structure, Number, String, Integer, Set, AnyOf, Map, PositiveInt
from typedpy import Structure, Number, String, Integer, Set, AnyOf, Map, PositiveInt, ImmutableSet


class Example(Structure):
Expand All @@ -12,6 +12,7 @@ class Example(Structure):
f = Set[Integer]
g = Set[AnyOf(fields=[String(minLength=3), Number(minimum=10)])]
h = Set
frozen = ImmutableSet[Integer]


def test_invalid_items_definitions_err1():
Expand Down Expand Up @@ -142,3 +143,16 @@ def test_simple_set_invalid():
with raises(TypeError) as excinfo:
Example(h=[1, 2, 3])
assert "h: Got [1, 2, 3]; Expected <class 'set'>" in str(excinfo.value)


def test_immutable_no_update():
e = Example(frozen={1,2,3})
with raises(AttributeError) as excinfo:
e.frozen.add(4)
assert "'frozenset' object has no attribute 'add'" in str(excinfo.value)


def test_immutable_content():
e = Example(frozen={1,2,3})
assert 1 in e.frozen

60 changes: 55 additions & 5 deletions tests/test_dataclass_style.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
import sys
from dataclasses import dataclass, FrozenInstanceError
from typing import List, FrozenSet, Dict

import pytest
from pytest import raises

from typedpy import AllOf, Enum, Number, Float, Structure, ImmutableStructure
Expand Down Expand Up @@ -29,8 +34,8 @@ class MixedTypesExample(Structure):

def test_partially_use_annotation():
print(Example)
assert Example(i=9, s="xyz", all=4).i==9
assert Example(i=9, s="xyz", all=4).all==4
assert Example(i=9, s="xyz", all=4).i == 9
assert Example(i=9, s="xyz", all=4).all == 4

with raises(ValueError):
Example(i=20, s="xyz")
Expand Down Expand Up @@ -61,7 +66,7 @@ def test_type_conversion_to_typedpy_str_representation():

def test_type_conversion_to_typedpy_validation_err_for_converted_type():
with raises(TypeError) as excinfo:
MixedTypesExample(i=5, s="xyz", s1="asd", simple=SimpleStruct(name="John"), a="a")
MixedTypesExample(i=5, s="xyz", s1="asd", simple=SimpleStruct(name="John"), a="a")
assert "a: Expected <class 'dict'>" in str(excinfo.value)


Expand Down Expand Up @@ -99,14 +104,14 @@ class ExampleOfImmutable(ImmutableStructure):
def test_default_values():
class Example(Structure):
i: int = 5
mylist: list = [1,2,3]
mylist: list = [1, 2, 3]
map: dict
f: Float = 0.5
f2 = Float(default=1.5)

e = Example(map={'x': 'y'})
assert e.i == 5
assert e.mylist == [1,2,3]
assert e.mylist == [1, 2, 3]
assert e.map == {'x': 'y'}
assert e.f == 0.5
assert e.f2 == 1.5
Expand Down Expand Up @@ -161,3 +166,48 @@ class ExampleOfImmutable(Structure):

assert ExampleOfImmutable(map={'x': 'y'}).f == 0.5


@pytest.mark.skipif(sys.version_info < (3, 7), reason="requires python3.7 or higher")
def test_valid_typing_valid1():
class ExampleWithTyping(Structure):
mylist: List[List[int]]
i: Integer(minimum=50)
myset: FrozenSet

e = ExampleWithTyping(myset={1, 2, 3}, i=100, mylist=[[1, 2, 3]])
assert e.mylist[0] == [1, 2, 3]


@pytest.mark.skipif(sys.version_info < (3, 7), reason="requires python3.7 or higher")
def test_valid_typing_valid2():
class ExampleWithTyping(Structure):
mymap: Dict[str, List]
myset: FrozenSet[int]

assert str(ExampleWithTyping) == '<Structure: ExampleWithTyping. Properties: mymap = <Map. Properties: items' \
' = [<String>, <Array>]>, myset = <ImmutableSet. Properties: items = <Integer>>>'
e = ExampleWithTyping(myset={1, 2, 3}, mymap={"x": [1, 2, 3]})
assert e.mymap["x"] == [1, 2, 3]


@pytest.mark.skipif(sys.version_info < (3, 7), reason="requires python3.7 or higher")
def test_typing_error():
class ExampleWithTyping(Structure):
mymap: Dict[str, List]

with raises(TypeError) as exc_info:
ExampleWithTyping(mymap={"x": 5})
assert "mymap_value: Got 5; Expected <class 'list'>" in str(exc_info.value)


@pytest.mark.skipif(sys.version_info < (3, 7), reason="requires python3.7 or higher")
def test_valid_typing_and_dataclass():
@dataclass(frozen=True)
class ExampleWithTyping(Structure):
mylist: List[List[int]]
i: Integer(minimum=50)
myset: FrozenSet

e = ExampleWithTyping(myset=frozenset({2, 3}), i=100, mylist=[[1, 2, 3]])
with raises(FrozenInstanceError):
e.mylist = frozenset()
2 changes: 1 addition & 1 deletion tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
# and then run "tox" from this directory.

[tox]
envlist = py{36,37, 38}
envlist = py{36,37, 38, 39}
skip_missing_interpreters = true

[testenv]
Expand Down
7 changes: 7 additions & 0 deletions typedpy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,13 @@
Anything,
SerializableField,
Function,
ImmutableMap,
ImmutableArray,
ImmutableSet,
ImmutableFloat,
ImmutableString,
ImmutableInteger,
ImmutableNumber,
)

from typedpy.json_schema_mapping import (
Expand Down
1 change: 0 additions & 1 deletion typedpy/commons.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@

def wrap_val(v):
return "'{}'".format(v) if isinstance(v, str) else v
57 changes: 52 additions & 5 deletions typedpy/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@


def _map_to_field(item):
item = item[0] if isinstance(item, (list, tuple)) and len(item) == 1 else item
if isinstance(item, StructMeta) and not isinstance(item, Field):
return ClassReference(item)
if item in [None, ""] or isinstance(item, Field):
Expand Down Expand Up @@ -573,19 +574,20 @@ def __init__(self, *args, items=None, **kwargs):
super().__init__(*args, **kwargs)

def __set__(self, instance, value):
if not isinstance(value, set):
cls = self.__class__._ty
if not isinstance(value, cls):
raise TypeError(
"{}: Got {}; Expected {}".format(self._name, wrap_val(value), set)
"{}: Got {}; Expected {}".format(self._name, wrap_val(value), cls)
)
self.validate_size(value, self._name)
if self.items is not None:
temp_st = Structure()
setattr(self.items, "_name", self._name)
res = set()
res = []
for val in value:
self.items.__set__(temp_st, val)
res.add(getattr(temp_st, getattr(self.items, "_name")))
value = res
res.append(getattr(temp_st, getattr(self.items, "_name")))
value = cls(res)
super().__set__(instance, value)


Expand Down Expand Up @@ -1203,6 +1205,51 @@ def validate_wrapper(cls, value):
)


class ImmutableSet(Set, ImmutableField):
_ty = frozenset

def __set__(self, instance, value):
if not isinstance(value, (set, frozenset)):
raise TypeError(
"{}: Got {}; Expected {}".format(self._name, wrap_val(value), set)
)
self.validate_size(value, self._name)
if self.items is not None:
temp_st = Structure()
setattr(self.items, "_name", self._name)
res = set()
for val in value:
self.items.__set__(temp_st, val)
res.add(getattr(temp_st, getattr(self.items, "_name")))
value = res
corrected_value = value if isinstance(value, frozenset) else frozenset(value)
super().__set__(instance, corrected_value)


class ImmutableMap(ImmutableField, Map):
pass


class ImmutableArray(ImmutableField, Array):
pass


class ImmutableString(ImmutableField, String):
pass


class ImmutableNumber(ImmutableField, Number):
pass


class ImmutableInteger(ImmutableField, Integer):
pass


class ImmutableFloat(ImmutableField, Float):
pass


class SerializableField(ABC):
"""
An abstract class for a field that has custom serialization or deserialization.
Expand Down
Loading