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

Release 0.12.1 #33

Merged
merged 5 commits into from Jun 8, 2019
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.rst
Expand Up @@ -2,6 +2,12 @@
CHANGELOG
=========

0.12.1
============

* Experimental support for SumType.


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

Expand Down
2 changes: 1 addition & 1 deletion README.rst
Expand Up @@ -19,7 +19,7 @@
:alt: Latest PyPI Release


**Development status: Alpha**
**Development status: Beta**

Typeit
------
Expand Down
2 changes: 1 addition & 1 deletion docs/conf.py
Expand Up @@ -53,7 +53,7 @@
# The short X.Y version.
version = '0.12'
# The full version, including alpha/beta/rc tags.
release = '0.12.0'
release = '0.12.1'

# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
Expand Down
60 changes: 55 additions & 5 deletions docs/quickstart_guide.rst
Expand Up @@ -4,11 +4,11 @@ Quickstart Guide

.. CAUTION::

The project is in an early development status, and a few public
The project is in a beta development status, and a few public
APIs may change in a backward-incompatible manner.


``typeit`` supports Python 3.7+.
``typeit`` supports Python 3.6+.


Installation
Expand Down Expand Up @@ -73,12 +73,20 @@ and rename the whole structure to better indicate the nature of the data:
}


mk_person, dict_person = type_constructor(Person, overrides)
mk_person, dict_person = type_constructor & overrides ^ Person


``typeit`` will handle creation of the constructor ``mk_person :: Dict -> Person`` and the serializer
``dict_person :: Person -> Dict`` for you.

``type_constructor & overrides`` produces a new type constructor that takes overrides into consideration,
and ``type_constructor ^ Person`` reads as "type constructor applied to the Person structure" and essentially
is the same as ``type_constructor(Person)``, but doesn't require parentheses around overrides (and extensions):

.. code-block:: python

(type_constructor & overrides & extension & ...)(Person)


Overrides
---------
Expand All @@ -99,7 +107,7 @@ our ``Person`` type:
}


mk_person, dict_person = type_constructor(Person, overrides)
mk_person, dict_person = type_constructor & overrides ^ Person


This is the way we can indicate that our Python structure has different field
Expand Down Expand Up @@ -137,5 +145,47 @@ any nested types, for instance:
}


mk_person, dict_person = type_constructor(Person, overrides)
mk_person, dict_person = type_constructor & overrides ^ Person


Supported types by default
--------------------------

* ``bool``
* ``int``
* ``float``
* ``str``
* ``dict``
* ``set`` and ``frozenset``
* ``typing.Any`` passes any value as is
* ``typing.Union`` including nested structures
* ``typing.Sequence``, ``typing.List`` including generic collections with ``typing.TypeVar``.
* ``typing.Set`` and ``typing.FrozenSet``
* ``typing.Tuple``
* ``typing.Dict``
* ``typing.Mapping``
* ``enum.Enum`` derivatives
* ``pathlib.Path`` derivatives
* ``typing_extensions.Literal``
* ``pyrsistent.typing.PVector``
* ``pyrsistent.typing.PMap``


Flags
-----

``NON_STRICT_PRIMITIVES`` -
disables strict checking of primitive types. With this flag, a type constructor for a structure
with a ``x: int`` attribute annotation would allow input values of ``x`` to be strings that could be parsed
as integer numbers. Without this flag, the type constructor will reject those values. The same rule is applicable
to combinations of floats, ints, and bools.

Extensions
----------

TODO

Handling errors
---------------

TODO
2 changes: 1 addition & 1 deletion requirements/minimal.txt
@@ -1,5 +1,5 @@
inflection>=0.3.1,<0.4.0
colander>=1.7.0,<1.8.0
pyrsistent>=0.14.11,<0.15
pyrsistent>=0.15.2,<0.16
typing-inspect>=0.4.0,<0.5.0
typing-extensions>=3.7.2,<3.8
4 changes: 2 additions & 2 deletions requirements/test.txt
@@ -1,6 +1,6 @@
-r ./minimal.txt
pytest==4.4.1
pytest>=4.6.1,<4.7
coverage==4.5.3
pytest-cov==2.6.1
pytest-cov>=2.7.1,<2.8
mypy==0.701
py-money==0.4.0
4 changes: 2 additions & 2 deletions setup.py
Expand Up @@ -38,11 +38,11 @@ def requirements(at_path: Path):
# ----------------------------

setup(name='typeit',
version='0.12.0',
version='0.12.1',
description='typeit brings typed data into your project',
long_description=README,
classifiers=[
'Development Status :: 3 - Alpha',
'Development Status :: 4 - Beta',
'Intended Audience :: Developers',
'License :: OSI Approved',
'License :: OSI Approved :: MIT License',
Expand Down
67 changes: 56 additions & 11 deletions tests/test_parser.py
Expand Up @@ -12,7 +12,7 @@
from typeit import parser as p
from typeit import flags
from typeit import schema
from typeit.sums import SumType, Variant
from typeit.sums import SumType, Either


def test_parser_empty_struct():
Expand Down Expand Up @@ -222,25 +222,71 @@ class Enums(Enum):
A = 'a'
B = 'b'

class Sums(SumType):
A: Variant[str]
B: Variant[str]

class X(NamedTuple):
e: Enums
s: Sums

mk_x, dict_x = p.type_constructor(X)

data = {'e': 'a', 's': 'b'}
data = {'e': 'a'}
x = mk_x(data)
assert isinstance(x.e, Enums)
assert isinstance(x.s, Sums)
assert isinstance(x.s('value'), Sums)
assert data == dict_x(x)

with pytest.raises(typeit.Invalid):
x = mk_x({'e': 'a', 's': None})
x = mk_x({'e': None})


def test_sum_types_as_union():
class Data(NamedTuple):
value: str

class MyEither(Either):
class Left:
err: str

class Right:
data: Data
version: str
name: str

class X(NamedTuple):
x: MyEither

mk_x, dict_x = p.type_constructor ^ X
x_data = {
'x': ('left', {'err': 'Error'})
}
x = mk_x(x_data)
assert isinstance(x.x, Either)
assert isinstance(x.x, MyEither)
assert isinstance(x.x, MyEither.Left)
assert not isinstance(x.x, Either.Left)
assert not isinstance(x.x, Either.Right)
assert not isinstance(x.x, MyEither.Right)
assert isinstance(x.x.err, str)
assert x.x.err == 'Error'
assert dict_x(x) == x_data

x_data = {
'x': ('right', {
'data': {'value': 'Value'},
'version': '1',
'name': 'Name',
})
}
x = mk_x(x_data)
assert isinstance(x.x, Either)
assert isinstance(x.x, MyEither)
assert isinstance(x.x, MyEither.Right)
assert not isinstance(x.x, Either.Right)
assert not isinstance(x.x, Either.Left)
assert not isinstance(x.x, MyEither.Left)
assert isinstance(x.x.data, Data)
assert isinstance(x.x.version, str)
assert x.x.data == Data(value='Value')
assert x.x.version == '1'
assert x.x.name == 'Name'
assert dict_x(x) == x_data


def test_enum_unions_serialization():
Expand Down Expand Up @@ -378,7 +424,6 @@ class X(NamedTuple):
assert dict_x(x) == data



def test_extending():
class X(NamedTuple):
x: Money
Expand Down
102 changes: 80 additions & 22 deletions tests/test_sums.py
@@ -1,22 +1,22 @@
from typing import Dict

import pytest
import pickle
from typing import NamedTuple
from typeit.sums import SumType, Variant
from typeit.sums import SumType


# These types are defined outside test cases
# because pickle requires classes to be defined in a module scope.
class X(SumType):
VARIANT_A: Variant[str] = 'variant_a'


class Y(SumType):
VARIANT_A: Variant[str] = 'variant_a'
class VARIANT_A(str): ...


def test_enum_like_api():
""" SumType should support the same usage patterns as Enum.
"""
class Y(SumType):
class VARIANT_A(str): ...

assert X.VARIANT_A != Y.VARIANT_A
assert X.VARIANT_A is not Y.VARIANT_A

Expand All @@ -38,19 +38,30 @@ def test_enum_like_api():
assert X(pickle.loads(pickle.dumps(X.VARIANT_A))) is X.VARIANT_A


class Z(SumType):
A: Variant[str]
def test_sum_variant_data_is_typed():
class X(SumType):
class VARIANT_A(str): ...

class _BData(NamedTuple):
x: str
y: int
z: float
class VARIANT_B(str): ...

B: Variant[_BData]
C: Variant[None]
assert X.VARIANT_A is not X
a_inst = X.VARIANT_A('111')
assert isinstance(a_inst, X)
assert isinstance(a_inst, X.VARIANT_A)
assert not isinstance(a_inst, X.VARIANT_B)


def test_sum_variants():
class Z(SumType):
class A(str): ...

class B:
x: str
y: int
z: float

class C: ...

x = Z.A('111')
y = Z.B(x='1', y=2, z=3.0)
c = Z.C()
Expand All @@ -60,12 +71,59 @@ def test_sum_variants():
assert isinstance(x, Z)
assert isinstance(y, Z)

assert x.value == 'a'
assert x.data == '111'
assert y.x == '1'
assert y.y == 2
assert isinstance(y.z, float)


def test_sum_variant_subclass_positional():
class X(SumType):
class A(str): ...

B: str

x = X.A(5)
assert type(x) is X
assert isinstance(x, X)
assert isinstance(x, X.A)


def test_generic_either():
class Either(SumType):
class Left: ...

class Right: ...

# User-defined Sums should adhere base Sum
with pytest.raises(TypeError):
class BrokenEither(Either):
class Left: ...


class ServiceResponse(Either):
class Left:
errmsg: str

class Right:
payload: Dict

x = ServiceResponse.Left(errmsg='Error')
y = ServiceResponse.Right(payload={'success': True})
assert type(x) is ServiceResponse
assert isinstance(x, ServiceResponse)
assert isinstance(x, ServiceResponse.Left)
assert isinstance(x, Either)
assert not isinstance(x, Either.Left)

class AlternativeEither(SumType):
class Left: ...

class Right: ...

assert not isinstance(x, AlternativeEither)
assert not isinstance(x, int)

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

assert y.data.x == '1'
assert y.data.y == 2
assert isinstance(y.data.z, float)
assert isinstance(y.data, Z._BData)

assert c.data is None