typedload/setup.py 0000744 0001750 0001750 00000010750 14170244730 013615 0 ustar salvo salvo #!/usr/bin/python3
# This file is auto generated. Do not modify
from distutils.core import setup
setup(
name='typedload',
version='2.15',
description='Load and dump data from json-like format into typed data structures',
long_description="=========\ntypedload\n=========\n\nLoad and dump json-like data into typed data structures in Python3, enforcing\na schema on the data.\n\nThis module provides an API to load dictionaries and lists (usually loaded\nfrom json) into Python's NamedTuples, dataclass, sets, enums, and various\nother typed data structures; respecting all the type-hints and performing\ntype checks or casts when needed.\n\nIt can also dump from typed data structures to json-like dictionaries and lists.\n\nIt is very useful for projects that use Mypy and deal with untyped data\nlike json, because it guarantees that the data will follow the specified schema.\n\nIt is released with a GPLv3 license.\n\n\n=======\nExample\n=======\n\nFor example this dictionary, loaded from a json:\n\n\n>>>\ndata = {\n 'users': [\n {\n 'username': 'salvo',\n 'shell': 'bash',\n 'sessions': ['pts/4', 'tty7', 'pts/6']\n },\n {\n 'username': 'lop'\n }\n ],\n}\n\n\n\nCan be treated more easily if loaded into this type:\n\n\n>>>\n@dataclasses.dataclass\nclass User:\n username: str\n shell: str = 'bash'\n sessions: List[str] = dataclasses.field(default_factory=list)\n>>>\nclass Logins(NamedTuple):\n users: List[User]\n\n\nAnd the data can be loaded into the structure with this:\n\n\n>>>\nt_data = typedload.load(data, Logins)\n\n\nAnd then converted back:\n\n\n>>>\ndata = typedload.dump(t_data)\n\n\n===============\nSupported types\n===============\n\nSince this is not magic, not all types are supported.\n\nThe following things are supported:\n\n * Basic python types (int, str, bool, float, NoneType)\n * NamedTuple\n * Enum\n * Optional[SomeType]\n * List[SomeType]\n * Dict[TypeA, TypeB]\n * Tuple[TypeA, TypeB, TypeC] and Tuple[SomeType, ...]\n * Set[SomeType]\n * Union[TypeA, TypeB]\n * dataclass (requires Python 3.7)\n * attr.s\n * ForwardRef (Refer to the type in its own definition)\n * Literal (requires Python 3.8)\n * TypedDict (requires Python 3.8)\n * datetime.date, datetime.time, datetime.datetime\n * Path\n * IPv4Address, IPv6Address\n * typing.Any\n * typing.NewType\n\n==========\nUsing Mypy\n==========\n\nMypy and similar tools work without requiring any plugins.\n\n\n>>>\n# This is treated as Any, no checks done.\ndata = json.load(f)\n>>>\n# This is treated as Dict[str, int]\n# but there will be runtime errors if the data does not\n# match the expected format\ndata = json.load(f) # type: Dict[str, int]\n>>>\n# This is treated as Dict[str, int] and an exception is\n# raised if the actual data is not Dict[str, int]\ndata = typedload.load(json.load(f), Dict[str, int])\n\n\nSo when using Mypy, it makes sense to make sure that the type is correct,\nrather than hoping the data will respect the format.\n\n=========\nExtending\n=========\n\nType handlers can easily be added, and existing ones can be replaced, so the library is fully cusomizable and can work with any type.\n\nInheriting a base class is not required.\n\n=======\nInstall\n=======\n\n* `pip install typedload`\n* `apt install python3-typedload`\n* Latest and greatest .deb file is in [releases](https://github.com/ltworf/typedload/releases)\n\n=============\nDocumentation\n=============\n\n* [Online documentation](https://ltworf.github.io/typedload/)\n* In the docs/ directory\n\nThe tests are hard to read but provide more in depth examples of\nthe capabilities of this module.\n\n=======\nUsed by\n=======\n\nAs dependency, typedload is used by those entities. Feel free to add to the list.\n\n* Several universities around the world\n",
url='https://ltworf.github.io/typedload/',
author="Salvo 'LtWorf' Tomaselli",
author_email='tiposchi@tiscali.it',
license='GPLv3',
classifiers=[
'Development Status :: 5 - Production/Stable',
'Intended Audience :: Developers',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10',
'Programming Language :: Python :: 3.11',
],
keywords='typing types mypy json',
packages=['typedload'],
package_data={"typedload": ["py.typed"]},
)
typedload/Makefile 0000644 0001750 0001750 00000004174 14166525334 013554 0 ustar salvo salvo MINIMUM_PYTHON_VERSION=3.5
all: pypi
.PHONY: test
test:
python3 -m tests
.PHONY: mypy
mypy:
mypy --python-version=$(MINIMUM_PYTHON_VERSION) --config-file mypy.conf typedload
mypy --python-version=3.7 example.py
setup.py:
./gensetup.py > setup.py
chmod u+x setup.py
pypi: setup.py typedload
mkdir -p dist pypi
./setup.py sdist
mv dist/typedload-`head -1 CHANGELOG`.tar.gz pypi
rmdir dist
gpg --detach-sign -a pypi/typedload-`head -1 CHANGELOG`.tar.gz
.PHONY: clean
clean:
$(RM) -r pypi
$(RM) -r .mypy_cache
$(RM) MANIFEST
$(RM) -r `find . -name __pycache__`
$(RM) typedload_`head -1 CHANGELOG`.orig.tar.gz
$(RM) typedload_`head -1 CHANGELOG`.orig.tar.gz.asc
$(RM) -r deb-pkg
$(RM) setup.py
$(RM) -r html
$(RM) -r perftest.output
.PHONY: dist
dist: clean setup.py
cd ..; tar -czvvf typedload.tar.gz \
typedload/setup.py \
typedload/Makefile \
typedload/tests \
typedload/docs \
typedload/mkdocs.yml \
typedload/LICENSE \
typedload/CONTRIBUTING.md \
typedload/CHANGELOG \
typedload/README.md \
typedload/example.py \
typedload/mypy.conf \
typedload/typedload
mv ../typedload.tar.gz typedload_`./setup.py --version`.orig.tar.gz
gpg --detach-sign -a *.orig.tar.gz
.PHONY: upload
upload: pypi
twine upload pypi/typedload-`./setup.py --version`.tar.gz
deb-pkg: dist
mv typedload_`./setup.py --version`.orig.tar.gz* /tmp
cd /tmp; tar -xf typedload_*.orig.tar.gz
cp -r debian /tmp/typedload/
cd /tmp/typedload/; dpkg-buildpackage --changes-option=-S
mkdir deb-pkg
mv /tmp/typedload_* /tmp/python3-typedload*.deb deb-pkg
$(RM) -r /tmp/typedload
lintian --pedantic -E --color auto -i -I deb-pkg/*.changes deb-pkg/*.deb
html: \
mkdocs.yml \
docs/CHANGELOG.md \
docs/comparisons.md \
docs/errors.md \
docs/CONTRIBUTING.md \
docs/gpl3logo.png \
docs/CODE_OF_CONDUCT.md \
docs/examples.md \
docs/README.md \
docs/supported_types.md \
docs/SECURITY.md \
docs/origin_story.md
mkdocs build
.PHONY: publish_html
publish_html: html
mkdocs gh-deploy
perftest.output/perf.p:
perftest/performance.py
.PHONY: gnuplot
gnuplot: perftest.output/perf.p
cd "perftest.output"; gnuplot -persist -c perf.p
typedload/tests/ 0000755 0001750 0001750 00000000000 14170056714 013244 5 ustar salvo salvo typedload/tests/test_attrload.py 0000644 0001750 0001750 00000016171 14166525311 016474 0 ustar salvo salvo # typedload
# Copyright (C) 2018-2021 Salvo "LtWorf" Tomaselli
#
# typedload is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
# author Salvo "LtWorf" Tomaselli
from enum import Enum
from typing import Dict, List, NamedTuple, Optional, Set, Tuple, Union
import unittest
from attr import attrs, attrib
from typedload import load, dump, exceptions, typechecks
from typedload import datadumper
class Hair(Enum):
BROWN = 'brown'
BLACK = 'black'
BLONDE = 'blonde'
WHITE = 'white'
@attrs
class Person:
name = attrib(default='Turiddu', type=str)
address = attrib(type=Optional[str], default=None)
@attrs
class DetailedPerson(Person):
hair = attrib(type=Hair, default=Hair.BLACK)
@attrs
class Students:
course = attrib(type=str)
students = attrib(type=List[Person])
@attrs
class Mangle:
value = attrib(type=int, metadata={'name': 'va.lue'})
class TestAttrDump(unittest.TestCase):
def test_basicdump(self):
assert dump(Person()) == {}
assert dump(Person('Alfio')) == {'name': 'Alfio'}
assert dump(Person('Alfio', '33')) == {'name': 'Alfio', 'address': '33'}
def test_norepr(self):
@attrs
class A:
i = attrib(type=int)
j = attrib(type=int, repr=False)
assert dump(A(1,1)) == {'i': 1}
def test_dumpdefault(self):
dumper = datadumper.Dumper()
dumper.hidedefault = False
assert dumper.dump(Person()) == {'name': 'Turiddu', 'address': None}
def test_factory_dump(self):
@attrs
class A:
a = attrib(factory=list, metadata={'ciao': 'ciao'}, type=List[int])
assert dump(A()) == {}
assert dump(A(), hidedefault=False) == {'a': []}
def test_nesteddump(self):
assert dump(
Students('advanced coursing', [
Person('Alfio'),
Person('Carmelo', 'via mulino'),
])) == {
'course': 'advanced coursing',
'students': [
{'name': 'Alfio'},
{'name': 'Carmelo', 'address': 'via mulino'},
]
}
class TestAttrload(unittest.TestCase):
def test_condition(self):
assert typechecks.is_attrs(Person)
assert typechecks.is_attrs(Students)
assert typechecks.is_attrs(Mangle)
assert typechecks.is_attrs(DetailedPerson)
assert not typechecks.is_attrs(int)
assert not typechecks.is_attrs(List[int])
assert not typechecks.is_attrs(Union[str, int])
assert not typechecks.is_attrs(Tuple[str, int])
def test_basicload(self):
assert load({'name': 'gino'}, Person) == Person('gino')
assert load({}, Person) == Person('Turiddu')
def test_nestenum(self):
assert load({'hair': 'white'}, DetailedPerson) == DetailedPerson(hair=Hair.WHITE)
def test_nested(self):
assert load(
{
'course': 'advanced coursing',
'students': [
{'name': 'Alfio'},
{'name': 'Carmelo', 'address': 'via mulino'},
]
},
Students,
) == Students('advanced coursing', [
Person('Alfio'),
Person('Carmelo', 'via mulino'),
])
def test_uuid(self):
import uuid
@attrs
class A:
a = attrib(type=int)
uuid_value = attrib(type=str, init=False)
def __attrs_post_init__(self):
self.uuid_value = str(uuid.uuid4())
assert type(load({'a': 1}, A).uuid_value) == str
assert load({'a': 1}, A) != load({'a': 1}, A)
class TestMangling(unittest.TestCase):
def test_mangle_extra(self):
@attrs
class Mangle:
value = attrib(metadata={'name': 'Value'}, type=int)
assert load({'value': 12, 'Value': 12}, Mangle) == Mangle(12)
with self.assertRaises(exceptions.TypedloadValueError):
load({'value': 12, 'Value': 12}, Mangle, failonextra=True)
def test_load_metanames(self):
a = {'va.lue': 12}
b = a.copy()
assert load(a, Mangle) == Mangle(12)
assert a == b
def test_case(self):
@attrs
class Mangle:
value = attrib(type = int, metadata={'name': 'Value'})
assert load({'Value': 1}, Mangle) == Mangle(1)
assert 'Value' in dump(Mangle(1))
with self.assertRaises(TypeError):
load({'value': 1}, Mangle)
def test_dump_metanames(self):
assert dump(Mangle(12)) == {'va.lue': 12}
def test_mangle_rename(self):
@attrs
class Mangle:
a = attrib(type=int, metadata={'name': 'b'})
b = attrib(type=str, metadata={'name': 'a'})
assert load({'b': 1, 'a': 'ciao'}, Mangle) == Mangle(1, 'ciao')
assert dump(Mangle(1, 'ciao')) == {'b': 1, 'a': 'ciao'}
def test_weird_mangle(self):
@attrs
class Mangle:
a = attrib(type=int, metadata={'name': 'b', 'alt': 'q'})
b = attrib(type=str, metadata={'name': 'a'})
assert load({'b': 1, 'a': 'ciao'}, Mangle) == Mangle(1, 'ciao')
assert load({'q': 1, 'b': 'ciao'}, Mangle, mangle_key='alt') == Mangle(1, 'ciao')
assert dump(Mangle(1, 'ciao')) == {'b': 1, 'a': 'ciao'}
assert dump(Mangle(1, 'ciao'), mangle_key='alt') == {'q': 1, 'b': 'ciao'}
def test_correct_exception_when_mangling(self):
@attrs
class A:
a = attrib(type=str, metadata={'name': 'q'})
with self.assertRaises(exceptions.TypedloadAttributeError):
load(1, A)
class TestAttrExceptions(unittest.TestCase):
def test_wrongtype_simple(self):
try:
load(3, Person)
except exceptions.TypedloadAttributeError:
pass
def test_wrongtype_nested(self):
data = {
'course': 'how to be a corsair',
'students': [
{'name': 'Alfio'},
3
]
}
try:
load(data, Students)
except exceptions.TypedloadAttributeError as e:
assert e.trace[-1].annotation[1] == 1
def test_index(self):
try:
load(
{
'course': 'advanced coursing',
'students': [
{'name': 'Alfio'},
{'name': 'Carmelo', 'address': 'via mulino'},
[],
]
},
Students,
)
except Exception as e:
assert e.trace[-2].annotation[1] == 'students'
assert e.trace[-1].annotation[1] == 2
typedload/tests/test_dataloader.py 0000644 0001750 0001750 00000037403 14170056714 016764 0 ustar salvo salvo # typedload
# Copyright (C) 2018-2021 Salvo "LtWorf" Tomaselli
#
# typedload is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
# author Salvo "LtWorf" Tomaselli
import argparse
import datetime
from enum import Enum
from ipaddress import IPv4Address, IPv6Address, IPv6Network, IPv4Network, IPv4Interface, IPv6Interface
from pathlib import Path
from typing import Dict, List, NamedTuple, Optional, Set, Tuple, Union, Any, NewType
import unittest
from typedload import dataloader, load, exceptions
class TestRealCase(unittest.TestCase):
def test_stopboard(self):
class VehicleType(Enum):
ST = 'ST'
TRAM = 'TRAM'
BUS = 'BUS'
WALK = 'WALK'
BOAT = 'BOAT'
class BoardItem(NamedTuple):
name: str
type: VehicleType
date: str
time: str
stop: str
stopid: str
journeyid: str
sname: Optional[str] = None
track: str = ''
rtDate: Optional[str] = None
rtTime: Optional[str] = None
direction: Optional[str] = None
accessibility: str = ''
bgColor: str = '#0000ff'
fgColor: str = '#ffffff'
stroke: Optional[str] = None
night: bool = False
c = {
'JourneyDetailRef': {'ref': 'https://api.vasttrafik.se/bin/rest.exe/v2/journeyDetail?ref=859464%2F301885%2F523070%2F24954%2F80%3Fdate%3D2018-04-08%26station_evaId%3D5862002%26station_type%3Ddep%26format%3Djson%26'},
'accessibility': 'wheelChair',
'bgColor': '#00394d',
'date': '2018-04-08',
'direction': 'Kortedala',
'fgColor': '#fa8719',
'journeyid': '9015014500604285',
'name': 'Spårvagn 6',
'rtDate': '2018-04-08',
'rtTime': '12:27',
'sname': '6',
'stop': 'SKF, Göteborg',
'stopid': '9022014005862002',
'stroke': 'Solid',
'time': '12:17',
'track': 'B',
'type': 'TRAM'
}
loader = dataloader.Loader()
loader.load(c, BoardItem)
class TestStrconstructed(unittest.TestCase):
def test_load_strconstructed(self):
loader = dataloader.Loader()
class Q:
def __init__(self, p):
self.param = p
loader.strconstructed.add(Q)
data = loader.load('42', Q)
assert data.param == '42'
class TestUnion(unittest.TestCase):
def test_json(self):
'''
This test would normally be flaky, but with the scoring of
types in union, it should always work.
'''
Json = Union[int, float, str, bool, None, List['Json'], Dict[str, 'Json']]
data = [{},[]]
loader = dataloader.Loader()
loader.basiccast = False
loader.frefs = {'Json' : Json}
assert loader.load(data, Json) == data
def test_str_obj(self):
'''
Possibly flaky test. Testing automatic type sorting in Union
It depends on python internal magic of sorting the union types
'''
loader = dataloader.Loader()
class Q(NamedTuple):
a: int
expected = Q(12)
for _ in range(5000):
t = eval('Union[str, Q]')
assert loader.load({'a': 12}, t) == expected
def test_ComplicatedUnion(self):
class A(NamedTuple):
a: int
class B(NamedTuple):
a: str
class C(NamedTuple):
val: Union[A, B]
loader = dataloader.Loader()
loader.basiccast = False
assert type(loader.load({'val': {'a': 1}}, C).val) == A
assert type(loader.load({'val': {'a': '1'}}, C).val) == B
def test_optional(self):
loader = dataloader.Loader()
assert loader.load(1, Optional[int]) == 1
assert loader.load(None, Optional[int]) == None
assert loader.load('1', Optional[int]) == 1
with self.assertRaises(ValueError):
loader.load('ciao', Optional[int])
loader.basiccast = False
loader.load('1', Optional[int])
def test_union(self):
loader = dataloader.Loader()
loader.basiccast = False
assert loader.load(1, Optional[Union[int, str]]) == 1
assert loader.load('a', Optional[Union[int, str]]) == 'a'
assert loader.load(None, Optional[Union[int, str]]) == None
assert type(loader.load(1, Optional[Union[int, float]])) == int
assert type(loader.load(1.0, Optional[Union[int, float]])) == float
with self.assertRaises(ValueError):
loader.load('', Optional[Union[int, float]])
loader.basiccast = True
assert type(loader.load(1, Optional[Union[int, float]])) == int
assert type(loader.load(1.0, Optional[Union[int, float]])) == float
assert loader.load(None, Optional[str]) is None
def test_debug_union(self):
loader = dataloader.Loader()
class A(NamedTuple):
a: int
class B(NamedTuple):
a: int
assert isinstance(loader.load({'a': 1}, Union[A, B]), (A, B))
loader.uniondebugconflict = True
with self.assertRaises(TypeError):
loader.load({'a': 1}, Union[A, B])
class TestTupleLoad(unittest.TestCase):
def test_ellipsis(self):
loader = dataloader.Loader()
l = list(range(33))
t = tuple(l)
assert loader.load(l, Tuple[int, ...]) == t
assert loader.load('abc', Tuple[str, ...]) == ('a', 'b', 'c')
assert loader.load('a', Tuple[str, ...]) == ('a', )
def test_tuple(self):
loader = dataloader.Loader()
with self.assertRaises(ValueError):
assert loader.load([1], Tuple[int, int]) == (1, 2)
assert loader.load([1, 2, 3], Tuple[int, int]) == (1, 2)
loader.failonextra = True
# Now the same will fail
with self.assertRaises(ValueError):
loader.load([1, 2, 3], Tuple[int, int]) == (1, 2)
class TestNamedTuple(unittest.TestCase):
def test_simple(self):
class A(NamedTuple):
a: int
b: str
loader = dataloader.Loader()
r = A(1,'1')
assert loader.load({'a': 1, 'b': 1}, A) == r
assert loader.load({'a': 1, 'b': 1, 'c': 3}, A) == r
loader.failonextra = True
with self.assertRaises(ValueError):
loader.load({'a': 1, 'b': 1, 'c': 3}, A)
def test_simple_defaults(self):
class A(NamedTuple):
a: int = 1
b: str = '1'
loader = dataloader.Loader()
r = A(1,'1')
assert loader.load({}, A) == r
def test_nested(self):
class A(NamedTuple):
a: int
class B(NamedTuple):
a: A
loader = dataloader.Loader()
r = B(A(1))
assert loader.load({'a': {'a': 1}}, B) == r
with self.assertRaises(TypeError):
loader.load({'a': {'a': 1}}, A)
def test_fail(self):
class A(NamedTuple):
a: int
q: str
loader = dataloader.Loader()
with self.assertRaises(ValueError):
loader.load({'a': 3}, A)
class TestEnum(unittest.TestCase):
def test_load_difficult_enum(self):
class TestEnum(Enum):
A: int = 1
B: Tuple[int,int,int] = (1, 2, 3)
loader = dataloader.Loader()
assert loader.load(1, TestEnum) == TestEnum.A
assert loader.load((1, 2, 3), TestEnum) == TestEnum.B
assert loader.load([1, 2, 3], TestEnum) == TestEnum.B
assert loader.load([1, 2, 3, 4], TestEnum) == TestEnum.B
loader.failonextra = True
with self.assertRaises(ValueError):
loader.load([1, 2, 3, 4], TestEnum)
def test_load_enum(self):
loader = dataloader.Loader()
class TestEnum(Enum):
LABEL1 = 1
LABEL2 = '2'
assert loader.load(1, TestEnum) == TestEnum.LABEL1
assert loader.load('2', TestEnum) == TestEnum.LABEL2
with self.assertRaises(ValueError):
loader.load(2, TestEnum)
assert loader.load(['2', 1], Tuple[TestEnum, TestEnum]) == (TestEnum.LABEL2, TestEnum.LABEL1)
class TestForwardRef(unittest.TestCase):
def test_known_refs(self):
class Node(NamedTuple):
value: int = 1
next: Optional['Node'] = None
l = {'next': {}, 'value': 12}
loader = dataloader.Loader()
assert loader.load(l, Node) == Node(value=12,next=Node())
def test_disable(self):
class A(NamedTuple):
i: 'int'
loader = dataloader.Loader(frefs=None)
with self.assertRaises(Exception):
loader.load(3, A)
def test_add_fref(self):
class A(NamedTuple):
i: 'alfio'
loader = dataloader.Loader()
with self.assertRaises(ValueError):
loader.load({'i': 3}, A)
loader.frefs['alfio'] = int
assert loader.load({'i': 3}, A) == A(3)
class TestLoaderIndex(unittest.TestCase):
def test_removal(self):
loader = dataloader.Loader()
assert loader.load(3, int) == 3
loader = dataloader.Loader()
loader.handlers.pop(loader.index(int))
with self.assertRaises(TypeError):
loader.load(3, int)
class TestExceptions(unittest.TestCase):
def test_dict_is_not_list(self):
loader = dataloader.Loader()
with self.assertRaises(exceptions.TypedloadTypeError):
loader.load({1: 1}, List[int])
with self.assertRaises(exceptions.TypedloadTypeError):
loader.load({1: 1}, Tuple[int, ...])
with self.assertRaises(exceptions.TypedloadTypeError):
loader.load({1: 1}, Set[int])
def test_list_exception(self):
loader = dataloader.Loader()
with self.assertRaises(exceptions.TypedloadTypeError):
loader.load(None, List[int])
def test_dict_exception(self):
loader = dataloader.Loader()
with self.assertRaises(exceptions.TypedloadAttributeError):
loader.load(None, Dict[int, int])
def test_index(self):
loader = dataloader.Loader()
try:
loader.load([1, 2, 3, 'q'], List[int])
except Exception as e:
assert e.trace[-1].annotation[1] == 3
try:
loader.load(['q', 2], Tuple[int,int])
except Exception as e:
assert e.trace[-1].annotation[1] == 0
try:
loader.load({'q': 1}, Dict[int,int])
except Exception as e:
assert e.trace[-1].annotation[1] == 'q'
def test_attrname(self):
class A(NamedTuple):
a: int
class B(NamedTuple):
a: A
b: int
loader = dataloader.Loader()
try:
loader.load({'a': 'q'}, A)
except Exception as e:
assert e.trace[-1].annotation[1] == 'a'
try:
loader.load({'a':'q','b': {'a': 1}}, B)
except Exception as e:
assert e.trace[-1].annotation[1] == 'a'
try:
loader.load({'a':3,'b': {'a': 'q'}}, B)
except Exception as e:
assert e.trace[-1].annotation[1] == 'a'
def test_typevalue(self):
loader = dataloader.Loader()
try:
loader.load([1, 2, 3, 'q'], List[int])
except Exception as e:
assert e.value == 'q'
assert e.type_ == int
class TestDatetime(unittest.TestCase):
def test_date(self):
loader = dataloader.Loader()
assert loader.load((2011, 1, 1), datetime.date) == datetime.date(2011, 1, 1)
assert loader.load((15, 33), datetime.time) == datetime.time(15, 33)
assert loader.load((15, 33, 0), datetime.time) == datetime.time(15, 33)
assert loader.load((2011, 1, 1), datetime.datetime) == datetime.datetime(2011, 1, 1)
assert loader.load((2011, 1, 1, 22), datetime.datetime) == datetime.datetime(2011, 1, 1, 22)
# Same but with lists
assert loader.load([2011, 1, 1], datetime.date) == datetime.date(2011, 1, 1)
assert loader.load([15, 33], datetime.time) == datetime.time(15, 33)
assert loader.load([15, 33, 0], datetime.time) == datetime.time(15, 33)
assert loader.load([2011, 1, 1], datetime.datetime) == datetime.datetime(2011, 1, 1)
assert loader.load([2011, 1, 1, 22], datetime.datetime) == datetime.datetime(2011, 1, 1, 22)
def test_exception(self):
loader = dataloader.Loader()
with self.assertRaises(TypeError):
loader.load((2011, ), datetime.datetime)
loader.load(33, datetime.datetime)
class TestDictEquivalence(unittest.TestCase):
def test_namespace(self):
loader = dataloader.Loader()
data = argparse.Namespace(a=12, b='33')
class A(NamedTuple):
a: int
b: int
c: int = 1
assert loader.load(data, A) == A(12, 33, 1)
assert loader.load(data, Dict[str, int]) == {'a': 12, 'b': 33}
def test_nonamespace(self):
loader = dataloader.Loader(dictequivalence=False)
data = argparse.Namespace(a=1)
with self.assertRaises(AttributeError):
loader.load(data, Dict[str, int])
class TestCommonTypes(unittest.TestCase):
def test_path(self):
loader = dataloader.Loader()
assert loader.load('/', Path) == Path('/')
def test_ipaddress(self):
loader = dataloader.Loader()
assert loader.load('10.10.10.1', IPv4Address) == IPv4Address('10.10.10.1')
assert loader.load('10.10.10.1', IPv4Network) == IPv4Network('10.10.10.1/32')
assert loader.load('10.10.10.1', IPv4Interface) == IPv4Interface('10.10.10.1/32')
assert loader.load('fe80::123', IPv6Address) == IPv6Address('fe80::123')
assert loader.load('10.10.10.0/24', IPv4Network) == IPv4Network('10.10.10.0/24')
assert loader.load('fe80::/64', IPv6Network) == IPv6Network('fe80::/64')
assert loader.load('10.10.10.1/24', IPv4Interface) == IPv4Interface('10.10.10.1/24')
assert loader.load('fe80::123/64', IPv6Interface) == IPv6Interface('fe80::123/64')
# Wrong IP version
with self.assertRaises(ValueError):
loader.load('10.10.10.1', IPv6Address)
with self.assertRaises(ValueError):
loader.load('fe80::123', IPv4Address)
with self.assertRaises(ValueError):
loader.load('10.10.10.0/24', IPv6Network)
with self.assertRaises(ValueError):
loader.load('fe80::123', IPv4Network)
with self.assertRaises(ValueError):
loader.load('10.10.10.1/24', IPv6Interface)
with self.assertRaises(ValueError):
loader.load('fe80::123/64', IPv4Interface)
# Wrong ipaddress type
with self.assertRaises(ValueError):
loader.load('10.10.10.1/24', IPv4Address)
with self.assertRaises(ValueError):
loader.load('10.10.10.1/24', IPv4Network)
class TestAny(unittest.TestCase):
def test_any(self):
loader = dataloader.Loader()
o = object()
assert loader.load(o, Any) is o
class TestNewType(unittest.TestCase):
def test_newtype(self):
loader = dataloader.Loader()
Foo = NewType("Foo", str)
bar = loader.load("bar", Foo)
assert bar == "bar"
assert type(bar) is str
typedload/tests/test_legacytuples_dataloader.py 0000644 0001750 0001750 00000021014 14142203534 021525 0 ustar salvo salvo # typedload
# Copyright (C) 2018 Salvo "LtWorf" Tomaselli
#
# typedload is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
# author Salvo "LtWorf" Tomaselli
from enum import Enum
from typing import Dict, FrozenSet, List, NamedTuple, Optional, Set, Tuple, Union
import unittest
from typedload import dataloader, load
class TestLegacy_oldsyntax(unittest.TestCase):
def test_legacyload(self):
A = NamedTuple('A', [('a', int), ('b', str)])
assert load({'a': 101, 'b': 'ciao'}, A) == A(101, 'ciao')
def test_nestedlegacyload(self):
A = NamedTuple('A', [('a', int), ('b', str)])
B = NamedTuple('B', [('a', A), ('b', List[A])])
assert load({'a': {'a': 101, 'b': 'ciao'}, 'b': []}, B) == B(A(101, 'ciao'), [])
assert load(
{'a': {'a': 101, 'b': 'ciao'}, 'b': [{'a': 1, 'b': 'a'},{'a': 0, 'b': 'b'}]},
B
) == B(A(101, 'ciao'), [A(1, 'a'),A(0, 'b')])
class TestUnion_oldsyntax(unittest.TestCase):
def test_ComplicatedUnion(self):
A = NamedTuple('A', [('a', int)])
B = NamedTuple('B', [('a', str)])
C = NamedTuple('C', [('val', Union[A, B])])
loader = dataloader.Loader()
loader.basiccast = False
assert type(loader.load({'val': {'a': 1}}, C).val) == A
assert type(loader.load({'val': {'a': '1'}}, C).val) == B
def test_optional(self):
loader = dataloader.Loader()
assert loader.load(1, Optional[int]) == 1
assert loader.load(None, Optional[int]) == None
assert loader.load('1', Optional[int]) == 1
with self.assertRaises(ValueError):
loader.load('ciao', Optional[int])
loader.basiccast = False
loader.load('1', Optional[int])
def test_union(self):
loader = dataloader.Loader()
loader.basiccast = False
assert loader.load(1, Optional[Union[int, str]]) == 1
assert loader.load('a', Optional[Union[int, str]]) == 'a'
assert loader.load(None, Optional[Union[int, str]]) == None
assert type(loader.load(1, Optional[Union[int, float]])) == int
assert type(loader.load(1.0, Optional[Union[int, float]])) == float
with self.assertRaises(ValueError):
loader.load('', Optional[Union[int, float]])
loader.basiccast = True
assert type(loader.load(1, Optional[Union[int, float]])) == int
assert type(loader.load(1.0, Optional[Union[int, float]])) == float
assert loader.load(None, Optional[str]) is None
class TestNamedTuple_oldsyntax(unittest.TestCase):
def test_simple(self):
A = NamedTuple('A', [('a', int), ('b', str)])
loader = dataloader.Loader()
r = A(1,'1')
assert loader.load({'a': 1, 'b': 1}, A) == r
assert loader.load({'a': 1, 'b': 1, 'c': 3}, A) == r
loader.failonextra = True
with self.assertRaises(ValueError):
loader.load({'a': 1, 'b': 1, 'c': 3}, A)
def test_nested(self):
A = NamedTuple('A', [('a', int)])
B = NamedTuple('B', [('a', A)])
loader = dataloader.Loader()
r = B(A(1))
assert loader.load({'a': {'a': 1}}, B) == r
with self.assertRaises(TypeError):
loader.load({'a': {'a': 1}}, A)
def test_fail(self):
A = NamedTuple('A', [('a', int), ('q', str)])
loader = dataloader.Loader()
with self.assertRaises(ValueError):
loader.load({'a': 3}, A)
class TestSet(unittest.TestCase):
def test_load_set(self):
loader = dataloader.Loader()
r = {(1, 1), (2, 2), (0, 0)}
assert loader.load(zip(range(3), range(3)), Set[Tuple[int,int]]) == r
assert loader.load([1, '2', 2], Set[int]) == {1, 2}
def test_load_frozen_set(self):
loader = dataloader.Loader()
assert loader.load(range(4), FrozenSet[float]) == frozenset((0.0, 1.0, 2.0, 3.0))
class TestDict(unittest.TestCase):
def test_load_dict(self):
loader = dataloader.Loader()
class State(Enum):
OK = 'ok'
FAILED = 'failed'
v = {'1': 'ok', '15': 'failed'}
r = {1: State.OK, 15: State.FAILED}
assert loader.load(v, Dict[int, State]) == r
def test_load_nondict(self):
class SimDict():
def items(self):
return zip(range(12), range(12))
loader = dataloader.Loader()
assert loader.load(SimDict(), Dict[str, int]) == {str(k): v for k,v in zip(range(12), range(12))}
with self.assertRaises(AttributeError):
loader.load(33, Dict[int, str])
class TestTuple(unittest.TestCase):
def test_load_list_of_tuples(self):
t = List[Tuple[str, int, Tuple[int, int]]]
v = [
['a', 12, [1, 1]],
['b', 15, [3, 2]],
]
r = [
('a', 12, (1, 1)),
('b', 15, (3, 2)),
]
loader = dataloader.Loader()
assert loader.load(v, t) == r
def test_load_nested_tuple(self):
loader = dataloader.Loader()
assert loader.load([1, 2, 3, [1, 2]], Tuple[int,int,int,Tuple[str,str]]) == (1, 2, 3, ('1', '2'))
def test_load_tuple(self):
loader = dataloader.Loader()
assert loader.load([1, 2, 3], Tuple[int,int,int]) == (1, 2, 3)
assert loader.load(['2', False, False], Tuple[int, bool]) == (2, False)
with self.assertRaises(ValueError):
loader.load(['2', False], Tuple[int, bool, bool])
loader.failonextra = True
assert loader.load(['2', False, False], Tuple[int, bool]) == (2, False)
class TestLoader(unittest.TestCase):
def test_kwargs(self):
with self.assertRaises(ValueError):
load(1, str, basiccast=False)
load(1, int, handlers=[])
class TestBasicTypes(unittest.TestCase):
def test_basic_casting(self):
# Casting enabled, by default
loader = dataloader.Loader()
assert loader.load(1, int) == 1
assert loader.load(1.1, int) == 1
assert loader.load(False, int) == 0
assert loader.load('ciao', str) == 'ciao'
assert loader.load('1', float) == 1.0
with self.assertRaises(ValueError):
loader.load('ciao', float)
def test_list_basic(self):
loader = dataloader.Loader()
assert loader.load(range(12), List[int]) == list(range(12))
assert loader.load(range(12), List[str]) == [str(i) for i in range(12)]
def test_extra_basic(self):
# Add more basic types
loader = dataloader.Loader()
with self.assertRaises(TypeError):
assert loader.load(b'ciao', bytes) == b'ciao'
loader.basictypes.add(bytes)
assert loader.load(b'ciao', bytes) == b'ciao'
def test_none_basic(self):
loader = dataloader.Loader()
loader.load(None, type(None))
with self.assertRaises(ValueError):
loader.load(12, type(None))
def test_basic_nocasting(self):
# Casting enabled, by default
loader = dataloader.Loader()
loader.basiccast = False
assert loader.load(1, int) == 1
assert loader.load(True, bool) == True
assert loader.load(1.5, float) == 1.5
with self.assertRaises(ValueError):
loader.load(1.1, int)
loader.load(False, int)
loader.load('ciao', str)
loader.load('1', float)
class TestHandlers(unittest.TestCase):
def test_custom_handler(self):
class Q:
def __eq__(self, other):
return isinstance(other, Q)
loader = dataloader.Loader()
loader.handlers.append((
lambda t: t == Q,
lambda l, v, t: Q()
))
assert loader.load('test', Q) == Q()
def test_broken_handler(self):
loader = dataloader.Loader()
loader.handlers.insert(0, (lambda t: 33 + t is None, lambda l, v, t: None))
with self.assertRaises(TypeError):
loader.load(1, int)
loader.raiseconditionerrors = False
assert loader.load(1, int) == 1
typedload/tests/test_dumpload.py 0000644 0001750 0001750 00000002721 14142203534 016455 0 ustar salvo salvo # typedload
# Copyright (C) 2018 Salvo "LtWorf" Tomaselli
#
# typedload is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
# author Salvo "LtWorf" Tomaselli
from enum import Enum
from typing import Dict, List, NamedTuple, Optional, Set, Tuple, Union
import unittest
from typedload import dump, load
class Result(Enum):
PASS = True
FAIL = False
class Student(NamedTuple):
name: str
id: int
email: Optional[str] = None
class ExamResults(NamedTuple):
results: List[Tuple[Student, Result]]
class TestDumpLoad(unittest.TestCase):
def test_dump_load_results(self):
results = ExamResults(
[
(Student('Anna', 1), Result.PASS),
(Student('Alfio', 2), Result.PASS),
(Student('Iano', 3, 'iano@iano.it'), Result.PASS),
]
)
assert load(dump(results), ExamResults) == results
typedload/tests/__main__.py 0000644 0001750 0001750 00000003102 14142506151 015324 0 ustar salvo salvo # typedload
# Copyright (C) 2018-2019 Salvo "LtWorf" Tomaselli
#
# typedload is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
# author Salvo "LtWorf" Tomaselli
import unittest
import sys
print('Running tests using %s' % sys.version)
if sys.version_info.major != 3 or sys.version_info.minor < 5:
raise Exception('Only version 3.5 and above supported')
if sys.version_info.minor > 5:
from .test_dataloader import *
from .test_datadumper import *
from .test_dumpload import *
from .test_exceptions import *
if sys.version_info.minor >= 7:
from .test_dataclass import *
if sys.version_info.minor >= 8:
from .test_literal import *
from .test_typeddict import *
from .test_legacytuples_dataloader import *
from .test_typechecks import *
# Run tests for the attr plugin only if it is loaded
try:
import attr
attr_module = True
except ImportError:
attr_module = False
if attr_module:
from .test_attrload import *
if __name__ == '__main__':
unittest.main()
typedload/tests/test_typechecks.py 0000644 0001750 0001750 00000013360 14170056714 017022 0 ustar salvo salvo # typedload
# Copyright (C) 2018-2021 Salvo "LtWorf" Tomaselli
#
# typedload is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
# author Salvo "LtWorf" Tomaselli
from enum import Enum
from typing import Dict, FrozenSet, List, NamedTuple, Optional, Set, Tuple, Union, Any, NewType
import unittest
import sys
if sys.version_info.minor >= 8 :
from typing import Literal
from typedload import typechecks
class TestChecks(unittest.TestCase):
def test_is_literal(self):
if sys.version_info.minor >= 8:
l = Literal[1, 2, 3]
assert typechecks.is_literal(l)
assert not typechecks.is_literal(3)
assert not typechecks.is_literal(int)
assert not typechecks.is_literal(str)
assert not typechecks.is_literal(None)
assert not typechecks.is_literal(List[int])
def test_is_not_typeddict(self):
assert not typechecks.is_typeddict(int)
assert not typechecks.is_typeddict(3)
assert not typechecks.is_typeddict(str)
assert not typechecks.is_typeddict({})
assert not typechecks.is_typeddict(dict)
assert not typechecks.is_typeddict(set)
assert not typechecks.is_typeddict(None)
assert not typechecks.is_typeddict(List[str])
def test_is_list(self):
assert typechecks.is_list(List)
assert typechecks.is_list(List[int])
assert typechecks.is_list(List[str])
assert not typechecks.is_list(list)
assert not typechecks.is_list(Tuple[int, str])
assert not typechecks.is_list(Dict[int, str])
assert not typechecks.is_list([])
if sys.version_info.minor >= 9:
assert typechecks.is_list(list[str])
assert not typechecks.is_list(tuple[str])
def test_is_dict(self):
assert typechecks.is_dict(Dict[int, int])
assert typechecks.is_dict(Dict)
assert typechecks.is_dict(Dict[str, str])
assert not typechecks.is_dict(Tuple[str, str])
assert not typechecks.is_dict(Set[str])
if sys.version_info.minor >= 9:
assert typechecks.is_dict(dict[str, str])
assert not typechecks.is_dict(tuple[str])
def test_is_set(self):
assert typechecks.is_set(Set[int])
assert typechecks.is_set(Set)
if sys.version_info.minor >= 9:
assert typechecks.is_set(set[str])
assert not typechecks.is_set(tuple[str])
def test_is_frozenset_(self):
assert not typechecks.is_frozenset(Set[int])
assert typechecks.is_frozenset(FrozenSet[int])
assert typechecks.is_frozenset(FrozenSet)
if sys.version_info.minor >= 9:
assert typechecks.is_frozenset(frozenset[str])
assert not typechecks.is_frozenset(tuple[str])
def test_is_tuple(self):
assert typechecks.is_tuple(Tuple[str, int, int])
assert typechecks.is_tuple(Tuple)
assert not typechecks.is_tuple(tuple)
assert not typechecks.is_tuple((1,2))
if sys.version_info.minor >= 9:
assert typechecks.is_tuple(tuple[str])
assert not typechecks.is_tuple(list[str])
def test_is_union(self):
assert typechecks.is_union(Optional[int])
assert typechecks.is_union(Optional[str])
assert typechecks.is_union(Union[bytes, str])
assert typechecks.is_union(Union[str, int, float])
def test_is_nonetype(self):
assert typechecks.is_nonetype(type(None))
assert not typechecks.is_nonetype(List[int])
def test_is_enum(self):
class A(Enum):
BB = 3
assert typechecks.is_enum(A)
assert not typechecks.is_enum(Set[int])
def test_is_namedtuple(self):
A = NamedTuple('A', [
('val', int),
])
assert typechecks.is_namedtuple(A)
assert not typechecks.is_namedtuple(Tuple)
assert not typechecks.is_namedtuple(tuple)
assert not typechecks.is_namedtuple(Tuple[int, int])
def test_is_forwardref(self):
try:
# Since 3.7
from typing import ForwardRef # type: ignore
except ImportError:
from typing import _ForwardRef as ForwardRef # type: ignore
assert typechecks.is_forwardref(ForwardRef('SomeType'))
def test_uniontypes(self):
assert set(typechecks.uniontypes(Optional[bool])) == {typechecks.NONETYPE, bool}
assert set(typechecks.uniontypes(Optional[int])) == {typechecks.NONETYPE, int}
assert set(typechecks.uniontypes(Optional[Union[int, float]])) == {typechecks.NONETYPE, float, int}
assert set(typechecks.uniontypes(Optional[Union[int, str, Optional[float]]])) == {typechecks.NONETYPE, str, int, float}
with self.assertRaises(AttributeError):
typechecks.uniontypes(Union[int])
def test_any(self):
assert typechecks.is_any(Any)
assert not typechecks.is_any(str)
assert not typechecks.is_any(Tuple[int, ...])
assert not typechecks.is_any(int)
assert not typechecks.is_any(List[float])
def test_isnewtype(self):
assert typechecks.is_newtype(NewType("foo", str))
assert not typechecks.is_newtype(type(NewType("foo", str)("bar")))
assert not typechecks.is_typeddict(str)
typedload/tests/test_datadumper.py 0000644 0001750 0001750 00000012335 14142506151 017001 0 ustar salvo salvo # typedload
# Copyright (C) 2018-2021 Salvo "LtWorf" Tomaselli
#
# typedload is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
# author Salvo "LtWorf" Tomaselli
import datetime
from enum import Enum
from ipaddress import IPv4Address, IPv4Network, IPv4Interface, IPv6Address, IPv6Network, IPv6Interface
from pathlib import Path
from typing import Dict, List, NamedTuple, Optional, Set, Tuple, Union
import unittest
from typedload import datadumper, dump, load
class EnumA(Enum):
A: int = 1
B: str = '2'
C: Tuple[int, int] = (1, 2)
class NamedA(NamedTuple):
a: int
b: str
c: str = 'no'
class TestDumpLoad(unittest.TestCase):
def test_enum(self):
assert load(dump(EnumA.C), EnumA) == EnumA.C
class TestLegacyDump(unittest.TestCase):
def test_dump(self):
A = NamedTuple('A',[('a', int), ('b', str)])
assert dump(A(1, '12')) == {'a': 1, 'b': '12'}
class TestStrconstructed(unittest.TestCase):
def test_dump_strconstructed(self):
dumper = datadumper.Dumper()
class Q:
def __str__(self):
return '42'
dumper.strconstructed.add(Q)
assert dumper.dump(Q()) == '42'
class TestBasicDump(unittest.TestCase):
def test_dump_namedtuple(self):
dumper = datadumper.Dumper()
assert dumper.dump(NamedA(1, 'a')) == {'a': 1, 'b': 'a'}
assert dumper.dump(NamedA(1, 'a', 'yes')) == {'a': 1, 'b': 'a', 'c': 'yes'}
dumper.hidedefault = False
assert dumper.dump(NamedA(1, 'a')) == {'a': 1, 'b': 'a', 'c': 'no'}
def test_dump_dict(self):
dumper = datadumper.Dumper()
assert dumper.dump({EnumA.B: 'ciao'}) == {'2': 'ciao'}
def test_dump_set(self):
dumper = datadumper.Dumper()
assert dumper.dump(set(range(3))) == [0, 1, 2]
assert dumper.dump(frozenset(range(3))) == [0, 1, 2]
def test_dump_enums(self):
dumper = datadumper.Dumper()
assert dumper.dump(EnumA.A) == 1
assert dumper.dump(EnumA.B) == '2'
assert dumper.dump(EnumA.C) == [1, 2]
def test_dump_iterables(self):
dumper = datadumper.Dumper()
assert dumper.dump([1]) == [1]
assert dumper.dump((1, 2)) == [1, 2]
assert dumper.dump([(1, 1), (0, 0)]) == [[1, 1], [0, 0]]
assert dumper.dump({1, 2}) == [1, 2]
def test_basic_types(self):
# Casting enabled, by default
dumper = datadumper.Dumper()
assert dumper.dump(1) == 1
assert dumper.dump('1') == '1'
assert dumper.dump(None) == None
dumper.basictypes = {int, str}
assert dumper.dump('1') == '1'
assert dumper.dump(1) == 1
with self.assertRaises(ValueError):
assert dumper.dump(None) == None
assert dumper.dump(True) == True
def test_datetime(self):
dumper = datadumper.Dumper()
assert dumper.dump(datetime.date(2011, 12, 12)) == [2011, 12, 12]
assert dumper.dump(datetime.time(15, 41)) == [15, 41, 0, 0]
assert dumper.dump(datetime.datetime(2019, 5, 31, 12, 44, 22)) == [2019, 5, 31, 12, 44, 22, 0]
class TestHandlersDumper(unittest.TestCase):
def test_custom_handler(self):
class Q:
def __eq__(self, other):
return isinstance(other, Q)
dumper = datadumper.Dumper()
dumper.handlers.append((
lambda v: isinstance(v, Q),
lambda l, v: 12
))
assert dumper.dump(Q()) == 12
def test_broken_handler(self):
dumper = datadumper.Dumper()
dumper.handlers.insert(0, (lambda v: 'a' + v is None, lambda l, v: None))
with self.assertRaises(TypeError):
dumper.dump(1)
dumper.raiseconditionerrors = False
assert dumper.dump(1) == 1
def test_replace_handler(self):
dumper = datadumper.Dumper()
index = dumper.index([])
assert dumper.dump([11]) == [11]
dumper.handlers[index] = (dumper.handlers[index][0], lambda *args: 3)
assert dumper.dump([11]) == 3
class TestDumper(unittest.TestCase):
def test_kwargs(self):
with self.assertRaises(ValueError):
dump(1, handlers=[])
class TestDumpCommonTypes(unittest.TestCase):
def test_path(self):
assert dump(Path('/')) == '/'
def test_ipaddress(self):
assert dump(IPv4Address('10.10.10.1')) == '10.10.10.1'
assert dump(IPv4Network('10.10.10.0/24')) == '10.10.10.0/24'
assert dump(IPv4Interface('10.10.10.1/24')) == '10.10.10.1/24'
assert dump(IPv6Address('fe80::123')) == 'fe80::123'
assert dump(IPv6Network('fe80::/64')) == 'fe80::/64'
assert dump(IPv6Interface('fe80::123/64')) == 'fe80::123/64'
typedload/tests/test_literal.py 0000644 0001750 0001750 00000002670 14142203534 016307 0 ustar salvo salvo # typedload
# Copyright (C) 2019 Salvo "LtWorf" Tomaselli
#
# typedload is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
# author Salvo "LtWorf" Tomaselli
from typing import Literal
import unittest
from typedload import dataloader, load, dump, typechecks
class TestLiteralLoad(unittest.TestCase):
def test_literalvalues(self):
assert isinstance(typechecks.literalvalues(Literal[1]), set)
assert typechecks.literalvalues(Literal[1]) == {1}
assert typechecks.literalvalues(Literal[1, 1]) == {1}
assert typechecks.literalvalues(Literal[1, 2]) == {1, 2}
def test_load(self):
l = Literal[1, 2, 'a']
assert load(1, l) == 1
assert load(2, l) == 2
assert load('a', l) == 'a'
def test_fail(self):
l = Literal[1, 2, 'a']
with self.assertRaises(ValueError):
load(3, l)
typedload/tests/test_exceptions.py 0000644 0001750 0001750 00000006511 14142506200 017026 0 ustar salvo salvo # typedload
# Copyright (C) 2021 Salvo "LtWorf" Tomaselli
#
# typedload is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
# author Salvo "LtWorf" Tomaselli
import enum
from typing import List, NamedTuple, Optional, Tuple
import unittest
from typedload import dataloader, load, dump, typechecks, exceptions
class Firewall(NamedTuple):
open_ports: List[int]
class Networking(NamedTuple):
nic: Optional[str]
firewall: Firewall
class Remote(NamedTuple):
networking: Networking
class Config(NamedTuple):
remote: Optional[Remote]
class Enumeration(enum.Enum):
A = 1
B = '2'
C = 3.0
class TestExceptionsStr(unittest.TestCase):
def test_exceptions_str(self):
incorrect = [
{'remote': {'networking': {'nic': "eth0", "firewall": {"open_ports":[1,2,3, 'a']}}}},
{'remote': {'networking': {'nic': "eth0", "firewall": {"closed_ports": [], "open_ports":[1,2,3]}}}},
{'remote': {'networking': {'noc': "eth0", "firewall": {"open_ports":[2,3]}}}},
{'romote': {'networking': {'nic': "eth0", "firewall": {"open_ports":[2,3]}}}},
{'remote': {'nitworking': {'nic': "eth0", "firewall": {"open_ports":[2,3]}}}},
]
paths = []
for i in incorrect:
try:
load(i, Config, basiccast=False, failonextra=True)
assert False
except exceptions.TypedloadException as e:
for i in e.exceptions:
paths.append(e._path(e.trace) + '.' + i._path(i.trace[1:]))
#1st object
assert paths[0] == '.remote.networking.firewall.open_ports.[3]'
assert paths[1] == '.remote.'
#2nd object
assert paths[2] == '.remote.networking.firewall'
assert paths[3] == '.remote.'
#3rd object
assert paths[4] == '.remote.networking'
assert paths[5] == '.remote.'
#4th object
# Nothing because of no sub-exceptions, fails before the union
#5th object
assert paths[6] == '.remote.'
assert paths[7] == '.remote.'
assert len(paths) == 8
def test_tuple_exceptions_str(self):
incorrect = [
[1, 1],
[1, 1, 1],
[1],
[1, 1.2],
[1, None],
[1, None, 1],
]
for i in incorrect:
try:
load(i, Tuple[int, int], basiccast=False, failonextra=True)
except Exception as e:
str(e)
def test_enum_exceptions_str(self):
incorrect = [
[1, 1],
'3',
12,
]
for i in incorrect:
try:
load(i, Enumeration, basiccast=False, failonextra=True)
except Exception as e:
str(e)
typedload/tests/test_typeddict.py 0000644 0001750 0001750 00000005153 14142506226 016647 0 ustar salvo salvo # typedload
# Copyright (C) 2019-2021 Salvo "LtWorf" Tomaselli
#
# typedload is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
# author Salvo "LtWorf" Tomaselli
from typing import TypedDict
import unittest
import sys
from typedload import dataloader, load, dump, typechecks
class Person(TypedDict):
name: str
age: float
class A(TypedDict):
val: str
class B(TypedDict, total=False):
val: str
class C(A, total=False):
vel: int
class TestTypeddictLoad(unittest.TestCase):
def test_mixed_totality(self):
if sys.version_info.minor == 8:
# This only works from 3.9
return
with self.assertRaises(ValueError):
load({}, C)
assert load({'val': 'a'}, C) == {'val': 'a'}
with self.assertRaises(ValueError):
load({'val': 'a', 'vel': 'q'}, C)
assert load({'val': 'a', 'vel': 1}, C) == {'val': 'a', 'vel': 1}
assert load({'val': 'a', 'vel': '1'}, C) == {'val': 'a', 'vel': 1}
assert load({'val': 'a','vil': 2}, C) == {'val': 'a'}
with self.assertRaises(ValueError):
load({'val': 'a','vil': 2}, C, failonextra=True)
def test_totality(self):
with self.assertRaises(ValueError):
load({}, A)
assert load({}, B) == {}
assert load({'val': 'a'}, B) == {'val': 'a'}
assert load({'vel': 'q'}, B) == {}
with self.assertRaises(ValueError):
load({'vel': 'q'}, B, failonextra=True)
def test_loadperson(self):
o = {'name': 'pino', 'age': 1.1}
assert load(o, Person) == o
assert load({'val': 3}, A) == {'val': '3'}
assert load({'val': 3, 'vil': 4}, A) == {'val': '3'}
with self.assertRaises(ValueError):
o.pop('age')
load(o, Person)
with self.assertRaises(ValueError):
load({'val': 3, 'vil': 4}, A, failonextra=True)
def test_is_typeddict(self):
assert typechecks.is_typeddict(A)
assert typechecks.is_typeddict(Person)
assert typechecks.is_typeddict(B)
typedload/tests/test_dataclass.py 0000644 0001750 0001750 00000013352 14166525311 016617 0 ustar salvo salvo # typedload
# Copyright (C) 2018-2021 Salvo "LtWorf" Tomaselli
#
# typedload is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
# author Salvo "LtWorf" Tomaselli
from dataclasses import dataclass, field
from enum import Enum
from typing import Dict, List, NamedTuple, Optional, Set, Tuple, Union
import unittest
from typedload import dataloader, load, dump, typechecks, exceptions
class TestDataclassLoad(unittest.TestCase):
def test_do_not_init(self):
@dataclass
class Q:
a: int
b: int
c: int = field(init=False)
def __post_init__(self):
self.c = self.a + self.b
assert typechecks.is_dataclass(Q)
assert load({'a': 1, 'b': 2}, Q).c == 3
a = load({'a': 12, 'b': 30}, Q)
assert a.c == 42
a.c = 1
assert a.c == 1
assert a.a == 12
assert a.b == 30
def test_is_dataclass(self):
@dataclass
class A:
pass
class B(NamedTuple):
pass
assert typechecks.is_dataclass(A)
assert not typechecks.is_dataclass(List[int])
assert not typechecks.is_dataclass(Tuple[int, int])
assert not typechecks.is_dataclass(B)
def test_factory_load(self):
@dataclass
class A:
a: List[int] = field(default_factory=list)
assert load({'a': [1, 2, 3]}, A) == A([1, 2, 3])
assert load({'a': []}, A) == A()
assert load({}, A) == A()
def test_load(self):
@dataclass
class A:
a: int
b: str
assert load({'a': 101, 'b': 'ciao'}, A) == A(101, 'ciao')
def test_nestedload(self):
@dataclass
class A:
a: int
b: str
@dataclass
class B:
a: A
b: List[A]
assert load({'a': {'a': 101, 'b': 'ciao'}, 'b': []}, B) == B(A(101, 'ciao'), [])
assert load(
{'a': {'a': 101, 'b': 'ciao'}, 'b': [{'a': 1, 'b': 'a'},{'a': 0, 'b': 'b'}]},
B
) == B(A(101, 'ciao'), [A(1, 'a'),A(0, 'b')])
def test_defaultvalue(self):
@dataclass
class A:
a: int
b: Optional[str] = None
assert load({'a': 1}, A) == A(1)
assert load({'a': 1, 'b': 'io'}, A) == A(1, 'io')
class TestDataclassUnion(unittest.TestCase):
def test_ComplicatedUnion(self):
@dataclass
class A:
a: int
@dataclass
class B:
a: str
@dataclass
class C:
val: Union[A, B]
loader = dataloader.Loader()
loader.basiccast = False
assert type(loader.load({'val': {'a': 1}}, C).val) == A
assert type(loader.load({'val': {'a': '1'}}, C).val) == B
class TestDataclassDump(unittest.TestCase):
def test_dump(self):
@dataclass
class A:
a: int
b: int = 0
assert dump(A(12)) == {'a': 12}
assert dump(A(12), hidedefault=False) == {'a': 12, 'b': 0}
def test_factory_dump(self):
@dataclass
class A:
a: int
b: List[int] = field(default_factory=list)
assert dump(A(3)) == {'a': 3}
assert dump(A(12), hidedefault=False) == {'a': 12, 'b': []}
class TestDataclassMangle(unittest.TestCase):
def test_mangle_extra(self):
@dataclass
class Mangle:
value: int = field(metadata={'name': 'Value'})
assert load({'value': 12, 'Value': 12}, Mangle) == Mangle(12)
with self.assertRaises(exceptions.TypedloadValueError):
load({'value': 12, 'Value': 12}, Mangle, failonextra=True)
def test_mangle_load(self):
@dataclass
class Mangle:
value: int = field(metadata={'name': 'va.lue'})
assert load({'va.lue': 1}, Mangle) == Mangle(1)
assert dump(Mangle(1)) == {'va.lue': 1}
def test_case(self):
@dataclass
class Mangle:
value: int = field(metadata={'name': 'Value'})
assert load({'Value': 1}, Mangle) == Mangle(1)
assert 'Value' in dump(Mangle(1))
with self.assertRaises(ValueError):
load({'value': 1}, Mangle)
def test_mangle_rename(self):
@dataclass
class Mangle:
a: int = field(metadata={'name': 'b'})
b: str = field(metadata={'name': 'a'})
assert load({'b': 1, 'a': 'ciao'}, Mangle) == Mangle(1, 'ciao')
assert dump(Mangle(1, 'ciao')) == {'b': 1, 'a': 'ciao'}
def test_weird_mangle(self):
@dataclass
class Mangle:
a: int = field(metadata={'name': 'b', 'alt': 'q'})
b: str = field(metadata={'name': 'a'})
assert load({'b': 1, 'a': 'ciao'}, Mangle) == Mangle(1, 'ciao')
assert load({'q': 1, 'b': 'ciao'}, Mangle, mangle_key='alt') == Mangle(1, 'ciao')
assert dump(Mangle(1, 'ciao')) == {'b': 1, 'a': 'ciao'}
assert dump(Mangle(1, 'ciao'), mangle_key='alt') == {'q': 1, 'b': 'ciao'}
def test_correct_exception_when_mangling(self):
@dataclass
class A:
a: str = field(metadata={'name': 'q'})
with self.assertRaises(exceptions.TypedloadAttributeError):
load(1, A)
typedload/docs/ 0000755 0001750 0001750 00000000000 14170244670 013032 5 ustar salvo salvo typedload/docs/CHANGELOG.md 0000644 0001750 0001750 00000010605 14170244670 014645 0 ustar salvo salvo 2.15
====
* Union fails immediately when a non typedload exception is found
* New `make html` target to generate the website
* Updated CONTRIBUTING file, with details about new licenses from the FSF
* Handle typing.NewType
2.14
====
* Fix bug where AttributeError from name mangling caused an AssertionError
2.13
====
* Separate and simpler handlers for NamedTuple, dataclass, attrs, TypedDict
* Allow duck typing when loading attr (allow any dict-like class to be used)
* Minor performance improvements
2.12
====
* Add `uniondebugconflict` flag to detect unions with conflicts.
2.11
====
* Make newer mypy happy
2.10
====
* Fix setup.py referring to a non-existing file when installing with pip
2.9
===
* Use README on pypi.org
* Tiny speed improvement
* Expanded and improved documentation
2.8
===
* Better report errors for `Enum`
* Improve support for inheritance with mixed totality of `TypedDict` (requires Python 3.9)
2.7
===
* failonextra triggers failure when dropping fields in mangling
* Support for `total=False` in `TypedDict`
* Support `init=False` in `dataclass` field
2.6
===
* Handle `Any` types as passthrough
* Easy way to handle types loaded from and dumped to `str`
* Improve how exceptions are displayed
2.5
===
* Fix dump for attr classes with factory
* Let name mangling use arbitrary metadata fields rather than just `name`
2.4
===
* Support for `ipaddress.IPv4Address`, `ipaddress.IPv6Address`,
`ipaddress.IPv4Network`, `ipaddress.IPv6Network`,
`ipaddress.IPv4Interface`, `ipaddress.IPv6Interface`.
2.3
===
* Better type sorting in `Union`
This helps when using `Union[dataclass, str]`
2.2
===
* Add Python3.9 to the supported versions
* Prevent loading dict as `List`, `Tuple`, `Set`
This helps when using `Union[Dict, List]` to take the correct
type.
2.1
===
* Written new usage example
* typechecks internals now pass with more mypy configurations
* Fix `import *`
2.0
===
* Breaking API change: handlers can only be modified before the first load
* Breaking API change: plugins removed (attr support is by default)
* Exceptions contain more information
* Greatly improve performances with iterables types
* Support for `pathlib.Path`
1.20
====
* Drop support for Python 3.5.2 (3.5 series is still supported)
* Support `TypedDict`
* More precise type annotation of `TypedloadException` and `Annotation` fields
* Deprecate the plugin to handle `attr.s` and make it always supported.
This means that there will be no need for special code.
* Fix datetime loader raising exceptions with the wrong type
1.19
====
* Add support for `Literal`.
1.18
====
* Improved documentation
* Debian builds are now done source only
1.17
====
* Prefer the same type in union loading
1.16
====
* New `uniontypes()` function.
* Make list and dictionary loaders raise the correct exceptions
* Able to load from `argparse.Namespace`
1.15
====
* Add support for `FrozenSet[T]`.
* Define `__all__` for typechecks.
* Add name mangling support in dataclass, to match attrs.
* Add support for `datetime.date`, `datetime.time`, `datetime.datetime`
1.14
====
* Add support for `Tuple[t, ...]`
1.13
====
* Fix bug in loading attr classes and passing random crap.
Now the proper exception is raised.
* New module to expose the internal type checks functions
1.12
====
* Support fields with factory for dataclass
1.11
====
* Fixed problem when printing sub-exceptions of failed unions
* Improve documentation
1.10
====
* Make mypy happy again
1.9
===
* Support `ForwardRef`
* Add a new Exception type with more details on the error (no breaking API changes)
1.8
===
* Make mypy happy again
1.7
===
* Make mypy happy again
1.6
===
* Run tests on older python as well
* Support for dataclass (Since python 3.7)
* Added methods to find the appropriate handlers
1.5
===
* Improve handling of unions
* Better continuous integration
* Support python 3.7
1.4
===
* Add support for name mangling in attr plugin
* Parameters can be passed as kwargs
* Improved exception message for `NamedTuple` loading
1.3
===
* Add support for Python < 3.5.3
1.2
===
* Ship the plugins in pypy
1.1
===
* Able to load and dump old style `NamedTuple`
* Support for Python 3.5
* Target to run mypy in makefile
* Refactor to support plugins. The API is still compatible.
* Plugin for the attr module, seems useful in Python 3.5
1.0
===
* Has a setting to hide default fields or not, in dumps
* Better error reporting
* Add file for PEP 561
0.9
===
* Initial release
typedload/docs/gpl3logo.png 0000644 0001750 0001750 00000014730 14142506226 015270 0 ustar salvo salvo PNG
IHDR D Z} bKGD pHYs
B(x tIMEQs eIDATxyTՙU(F-"ƅ>K&.-f5qщF8:3Q$$&ƠE
+,QiSoujSշz{snmI ;]=~@W ,^;X?Wnbl@988J>9
#~f*]o
[d@-A'Ig,P78qXMy[}/f 9x}?AumVxfzo
Tm`)N
Q8$)TgVW~(fvO 'ˇy3HLŐN4( aVǀO6M6>*[gR yx=2}x:O,lr_9ϴQd`upmjFŶ-_v.rӐ9o@g}`cm>mp۴ # *<5c|P9;(
9K@..rB=W_)
>8J+00lC&{rrSr#;X1 Ecc3 i \z 7pk2HcNJSW#j34ׅd#<z%IGD`LM;A9h۞ze;1C@} 0uT3|%vz?t%"0/,_}G