Skip to content

Commit

Permalink
Merge a4f3121 into f1db64e
Browse files Browse the repository at this point in the history
  • Loading branch information
avanov committed Jun 23, 2019
2 parents f1db64e + a4f3121 commit ee3fe80
Show file tree
Hide file tree
Showing 16 changed files with 322 additions and 103 deletions.
6 changes: 5 additions & 1 deletion CHANGELOG.rst
Expand Up @@ -2,12 +2,16 @@
CHANGELOG
=========

0.13.0
==============

* Added support for regular classes with annotated ``__init__``.

0.12.1, 0.12.2
==============

* Experimental support for SumType.


0.12.0
============

Expand Down
4 changes: 2 additions & 2 deletions docs/conf.py
Expand Up @@ -51,9 +51,9 @@
# built documents.
#
# The short X.Y version.
version = '0.12'
version = '0.13'
# The full version, including alpha/beta/rc tags.
release = '0.12.2'
release = '0.13.0'

# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
Expand Down
125 changes: 123 additions & 2 deletions docs/quickstart_guide.rst
Expand Up @@ -148,8 +148,8 @@ any nested types, for instance:
mk_person, dict_person = type_constructor & overrides ^ Person
Supported types by default
--------------------------
Supported types
---------------

* ``bool``
* ``int``
Expand All @@ -164,11 +164,132 @@ Supported types by default
* ``typing.Tuple``
* ``typing.Dict``
* ``typing.Mapping``
* ``typeit.sums.SumType``
* ``enum.Enum`` derivatives
* ``pathlib.Path`` derivatives
* ``typing_extensions.Literal``
* ``pyrsistent.typing.PVector``
* ``pyrsistent.typing.PMap``
* Regular classes with annotated ``__init__`` method.


Sum Type
--------

There are many ways to describe what a Sum Type (Tagged Union) is. Here's just a few of them:

* `Wikipedia <https://en.wikipedia.org/wiki/Tagged_union>`_ describes it as "a data structure used
to hold a value that could take on several different, but fixed, types.
Only one of the types can be in use at any one time, and a tag explicitly indicates which one
is in use. It can be thought of as a type that has several “cases”, each of which should be handled
correctly when that type is manipulated";

* or you can think of Sum Types as data types that have more than one constructor, where each constructor
accepts its own set of input data;

* or even simpler, as a generalized version of Enums, with some extra features.

``typeit`` provides a limited implementation of Sum Types, that have functionality similar to default Python Enums,
plus the ability of each tag to hold a value.

A new SumType is defined with the following signature:

.. code-block:: python
from typeit.sums import SumType
class Payment(SumType):
class Cash:
amount: Money
class Card:
amount: Money
card: CardCredentials
class Phone:
amount: Money
provider: MobilePaymentProvider
class JustThankYou:
pass
``Payment`` is a new Tagged Union (which is another name for a Sum Type, remember), that consists
of four distinct possibilities: ``Cash``, ``Card``, ``Phone``, and ``JustThankYou``.
These possibilities are called tags (or variants, or constructors) of ``Payment``.
In other words, any instance of ``Payment`` is either ``Cash`` or ``Card`` or ``Phone`` or ``JustThankYou``,
and is never two or more of them at the same time.

Now, let's observe the properties of this new type:

.. code-block:: python
>>> adam_paid = Payment.Cash(amount=Money('USD', 10))
>>> jane_paid = Payment.Card(amount=Money('GBP', 8),
... card=CardCredentials(number='1234 5678 9012 3456',
... holder='Jane Austen',
... validity='12/24',
... secret='***'))
>>> fred_paid = Payment.JustThankYou()
>>>
>>> assert type(adam_paid) is type(jane_paid) is type(fred_paid) is Payment
>>>
>>> assert isinstance(adam_paid, Payment)
>>> assert isinstance(jane_paid, Payment)
>>> assert isinstance(fred_paid, Payment)
>>>
>>> assert isinstance(adam_paid, Payment.Cash)
>>> assert isinstance(jane_paid, Payment.Card)
>>> assert isinstance(fred_paid, Payment.JustThankYou)
>>>
>>> assert not isinstance(adam_paid, Payment.Card)
>>> assert not isinstance(adam_paid, Payment.JustThankYou)
>>>
>>> assert not isinstance(jane_paid, Payment.Cash)
>>> assert not isinstance(jane_paid, Payment.JustThankYou)
>>>
>>> assert not isinstance(fred_paid, Payment.Cash)
>>> assert not isinstance(fred_paid, Payment.Card)
>>>
>>> assert not isinstance(adam_paid, Payment.Phone)
>>> assert not isinstance(jane_paid, Payment.Phone)
>>> assert not isinstance(fred_paid, Payment.Phone)
>>>
>>> assert Payment('Phone') is Payment.Phone
>>> assert Payment('phone') is Payment.Phone
>>> assert Payment(Payment.Phone) is Payment.Phone
>>>
>>> paid = Payment(adam_paid)
>>> assert paid is adam_paid
As you can see, every variant constructs an instance of the same type ``Payment``,
and yet, every instance is identified with its own tag. You can use this tag to branch
your business logic, like in a function below:

.. code-block:: python
def notify_restaurant_owner(channel: Broadcaster, payment: Payment):
if isinstance(payment, Payment.JustThankYou):
channel.push(f'A customer said Big Thank You!')
else: # Cash, Card, Phone instances have the `payment.amount` attribute
channel.push(f'A customer left {payment.amount}!')
And, of course, you can use Sum Types in signatures of your serializable data:

.. code-block:: python
from typing import NamedTuple, Sequence
from typeit import type_constructor
class Payments(NamedTuple):
latest: Sequence[Payment]
mk_payments, dict_payments = type_constructor ^ Payments
json_ready = dict_payments(Payments(latest=[adam_paid, jane_paid, fred_paid]))
payments = mk_payments(json_ready)
Flags
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Expand Up @@ -38,7 +38,7 @@ def requirements(at_path: Path):
# ----------------------------

setup(name='typeit',
version='0.12.2',
version='0.13.0',
description='typeit brings typed data into your project',
long_description=README,
classifiers=[
Expand Down
Empty file added tests/parser/__init__.py
Empty file.
25 changes: 25 additions & 0 deletions tests/parser/test_dataclasses.py
@@ -0,0 +1,25 @@
from typeit.compat import PY_VERSION

if PY_VERSION >= (3, 7):
from dataclasses import dataclass
from typeit import parser as p


def test_dataclasses():

@dataclass
class InventoryItem:
name: str
unit_price: float
quantity_on_hand: int = 0

mk_inv, dict_inv = p.type_constructor(InventoryItem)

serialized = {
'name': 'test',
'unit_price': 1.0,
'quantity_on_hand': 5,
}
x = mk_inv(serialized)
assert isinstance(x, InventoryItem)
assert dict_inv(x) == serialized
51 changes: 51 additions & 0 deletions tests/parser/test_extending.py
@@ -0,0 +1,51 @@
from typing import NamedTuple, Optional

from money.currency import Currency
from money.money import Money

import typeit
from typeit import schema, parser as p


def test_extending():
class X(NamedTuple):
x: Money

class MoneySchema(schema.types.Tuple):
def deserialize(self, node, cstruct):
r = super().deserialize(node, cstruct)
if r in (schema.types.Null, None):
return r
try:
currency = Currency(r[0])
except ValueError:
raise typeit.Invalid(node, f'Invalid currency token in {r}', cstruct)

try:
rv = Money(r[1], currency)
except:
raise typeit.Invalid(node, f'Invalid amount in {r}', cstruct)

return rv

def serialize(self, node, appstruct: Optional[Money]):
if appstruct is None or appstruct is schema.types.Null:
# if appstruct is None or appstruct is schema.types.Null:
return appstruct

r = (appstruct.currency, appstruct.amount)
return super().serialize(node, r)

mk_x, dict_x = (
p.type_constructor
& MoneySchema[Money] << schema.types.Enum(Currency) << schema.primitives.NonStrictStr()
^ X
)

serialized = {
'x': ('GBP', '10')
}

x = mk_x(serialized)
assert isinstance(x.x, Money)
assert dict_x(x) == serialized
60 changes: 9 additions & 51 deletions tests/test_parser.py → tests/parser/test_parser.py
Expand Up @@ -2,10 +2,7 @@
from enum import Enum
from typing import NamedTuple, Dict, Any, Sequence, Union, Tuple, Optional, Set, List, FrozenSet, get_type_hints

import colander
import pytest
from money.currency import Currency
from money.money import Money

import typeit
from typeit import codegen as cg
Expand Down Expand Up @@ -288,6 +285,15 @@ class X(NamedTuple):
assert x.x.name == 'Name'
assert dict_x(x) == x_data

with pytest.raises(typeit.Invalid):
# version is missing
x = mk_x({
'x': ('right', {
'data': {'value': 'Value'},
'name': 'Name',
})
})


def test_enum_unions_serialization():
class E0(Enum):
Expand Down Expand Up @@ -424,54 +430,6 @@ class X(NamedTuple):
assert dict_x(x) == data


def test_extending():
class X(NamedTuple):
x: Money

class MoneySchema(schema.types.Tuple):
def deserialize(self, node, cstruct):
r = super().deserialize(node, cstruct)
if r in (colander.null, None):
return r
try:
currency = Currency(r[0])
except ValueError:
raise typeit.Invalid(node, f'Invalid currency token in {r}', cstruct)

try:
rv = Money(r[1], currency)
except:
raise typeit.Invalid(node, f'Invalid amount in {r}', cstruct)

return rv

def serialize(self, node, appstruct: Optional[Money]):
if appstruct is None or appstruct is colander.null:
return appstruct

r = (appstruct.currency, appstruct.amount)
return super().serialize(node, r)

with pytest.raises(TypeError):
# type ``Money`` is not defined in overrides
__ = p.type_constructor ^ X

mk_x, dict_x = (
p.type_constructor
& MoneySchema[Money] << schema.types.Enum(Currency) << schema.primitives.NonStrictStr()
^ X
)

serialized = {
'x': ('GBP', '10')
}

x = mk_x(serialized)
assert isinstance(x.x, Money)
assert dict_x(x) == serialized



GITHUB_PR_PAYLOAD_JSON = """
{
"action": "closed",
Expand Down
18 changes: 18 additions & 0 deletions tests/parser/test_regular_classes.py
@@ -0,0 +1,18 @@
from money.money import Money

from typeit import parser as p
from typeit import flags


def test_regular_classes():

mk_x, dict_x = p.type_constructor & flags.NON_STRICT_PRIMITIVES ^ Money

serialized = {
'amount': '10',
'currency': 'GBP',
}

x = mk_x(serialized)
assert isinstance(x, Money)
assert dict_x(x) == serialized
2 changes: 0 additions & 2 deletions tests/test_sums.py
Expand Up @@ -125,5 +125,3 @@ class Right: ...

assert x.errmsg == 'Error'
assert y.payload == {'success': True}


0 comments on commit ee3fe80

Please sign in to comment.