typedload/setup.py 0000764 0001750 0001750 00000002022 14644307575 013625 0 ustar salvo salvo #!/usr/bin/python3
# This file is auto generated. Do not modify
from setuptools import setup
setup(
name='typedload',
version='2.34',
description='Load and dump data from json-like format into typed data structures',
readme='README.md',
url='https://ltworf.codeberg.page/typedload/',
author="Salvo 'LtWorf' Tomaselli",
author_email='tiposchi@tiscali.it',
license='GPL-3.0-only',
classifiers=['Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)', 'Typing :: Typed', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12', 'Programming Language :: Python :: 3.13'],
keywords='typing types mypy json schema json-schema python3 namedtuple enums dataclass pydantic',
packages=['typedload'],
package_data={"typedload": ["py.typed", "__init__.pyi"]},
)
typedload/Makefile 0000664 0001750 0001750 00000010463 14630621362 013546 0 ustar salvo salvo MINIMUM_PYTHON_VERSION=3.8
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=$(MINIMUM_PYTHON_VERSION) example.py
pyproject.toml: docs/CHANGELOG.md
./gensetup.py --$@
setup.py: docs/CHANGELOG.md README.md
./gensetup.py --$@
chmod u+x setup.py
pypi: pyproject.toml setup.py typedload
mkdir -p dist pypi
./setup.py sdist
./setup.py bdist_wheel
mv dist/typedload-`head -1 CHANGELOG`.tar.gz pypi
mv dist/*whl pypi
rmdir dist
gpg --detach-sign -a pypi/typedload-`head -1 CHANGELOG`.tar.gz
gpg --detach-sign -a pypi/typedload-`head -1 CHANGELOG`-py3-none-any.whl
# Debian needs setup and pyproject to be kept since they are in the
# dist file. However I want to clean them or they will become outdated
# and not regenerated
.PHONY: debian_clean
debian_clean:
$(RM) -r pypi
$(RM) -r build
$(RM) -r .mypy_cache
$(RM) -r typedload.egg-info/
$(RM) -r .pybuild
$(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) -r html
$(RM) -r perftest.output
$(RM) docs/*_docgen.md
.PHONY: clean
clean: debian_clean
$(RM) setup.py
$(RM) pyproject.toml
.PHONY: dist
dist: clean setup.py pyproject.toml
cd ..; tar -czvvf typedload.tar.gz \
typedload/setup.py \
typedload/Makefile \
typedload/tests \
typedload/docs \
typedload/docgen \
typedload/mkdocs.yml \
typedload/LICENSE \
typedload/CONTRIBUTING.md \
typedload/CHANGELOG \
typedload/README.md \
typedload/example.py \
typedload/mypy.conf \
typedload/pyproject.toml \
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 --username __token__ --password `cat .token` pypi/*
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
docs/typedload_docgen.md: typedload/__init__.py
./docgen $@
docs/typedload.dataloader_docgen.md: typedload/dataloader.py
./docgen $@
docs/typedload.datadumper_docgen.md: typedload/datadumper.py
./docgen $@
docs/typedload.exceptions_docgen.md: typedload/exceptions.py
./docgen $@
docs/typedload.typechecks_docgen.md: typedload/typechecks.py
./docgen $@
html: \
docs/*.svg \
docs/CHANGELOG.md \
docs/CODE_OF_CONDUCT.md \
docs/comparisons.md \
docs/CONTRIBUTING.md \
docs/deferred_evaluation.md \
docs/docs \
docs/docs/gpl3logo.png \
docs/errors.md \
docs/examples.md \
docs/gpl3logo.png \
docs/origin_story.md \
docs/performance.md \
docs/README.md \
docs/SECURITY.md \
docs/supported_types.md \
docs/typedload.datadumper_docgen.md \
docs/typedload.dataloader_docgen.md \
docs/typedload_docgen.md \
docs/typedload.exceptions_docgen.md \
docs/typedload.typechecks_docgen.md \
mkdocs.yml
mkdocs build
# Download cloudflare crap
mkdir -p html/cdn
cd html/cdn; wget --continue `cat ../*html | grep cloudflare | grep min.css | sort | uniq | cut -d\" -f4`
cd html/cdn; wget --continue `cat ../*html | grep cloudflare | grep min.js | sort | uniq | cut -d\" -f2`
# Fix html pages
for page in html/*.html; do \
sed -i 's,https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.5.0/styles/github.min.css,cdn/github.min.css,g' $${page}; \
sed -i 's,https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.5.0/highlight.min.js,cdn/highlight.min.js,g' $${page}; \
echo "" >> $${page}; \
done
.PHONY: publish_html
publish_html: html
git checkout pages
rm -rf cdn css docs fonts img js search
mv html/* .
git add cdn css docs fonts img js search
git add `git status --porcelain | grep '^ M' | cut -d\ -f3`
git commit -m "Deployed manually to workaround MkDocs"
git push
git checkout -
perftest.output/perf.p:
@echo export MOREVERSIONS=1 to compare more versions
perftest/performance.py
.PHONY: gnuplot
gnuplot: perftest.output/perf.p
cd "perftest.output"; gnuplot -persist -c perf.p
typedload/tests/ 0000775 0001750 0001750 00000000000 14644274452 013255 5 ustar salvo salvo typedload/tests/test_literal.py 0000664 0001750 0001750 00000011253 14630621362 016313 0 ustar salvo salvo # typedload
# Copyright (C) 2019-2022 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
from typing import Literal, NamedTuple, TypedDict, Union
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)
def test_discriminatorliterals_wrong(self):
assert typechecks.discriminatorliterals(int) == {}
def test_discriminatorliterals_namedtuple(self):
class A(NamedTuple):
t: Literal['a', 'b']
i: int
q: str
class B(NamedTuple):
t: Literal[33]
q: Literal[12]
i: int
class C(NamedTuple):
t: Literal['a']
i: int
assert typechecks.discriminatorliterals(A) == {'t': {'a', 'b'}}
assert typechecks.discriminatorliterals(B) == {'t': {33,}, 'q': {12,}}
assert typechecks.discriminatorliterals(C) == {'t': {'a', }}
def test_discriminatorliterals_typeddict(self):
class A(TypedDict):
t: Literal['a', 'b']
i: int
q: str
class B(TypedDict):
t: Literal[33]
q: Literal[12]
i: int
class C(TypedDict):
t: Literal['a']
i: int
assert typechecks.discriminatorliterals(A) == {'t': {'a', 'b'}}
assert typechecks.discriminatorliterals(B) == {'t': {33,}, 'q': {12,}}
assert typechecks.discriminatorliterals(C) == {'t': {'a', }}
def test_discriminatorliterals_dataclass(self):
@dataclass
class A:
t: Literal['a', 'b']
i: int
q: str
@dataclass
class B:
t: Literal[33]
q: Literal[12]
i: int
@dataclass
class C:
t: Literal['a']
i: int
assert typechecks.discriminatorliterals(A) == {'t': {'a', 'b'}}
assert typechecks.discriminatorliterals(B) == {'t': {33,}, 'q': {12,}}
assert typechecks.discriminatorliterals(C) == {'t': {'a', }}
def test_discriminatorliterals_attr(self):
try:
from attr import attrs, attrib
except ImportError:
return
@attrs
class A:
t: Literal['a', 'b'] = attrib()
i: int = attrib()
q: str = attrib()
@attrs
class B:
t: Literal[33] = attrib()
q: Literal[12] = attrib()
i: int = attrib()
@attrs
class C:
t: Literal['a'] = attrib()
i: int = attrib()
assert typechecks.discriminatorliterals(A) == {'t': {'a', 'b'}}
assert typechecks.discriminatorliterals(B) == {'t': {33,}, 'q': {12,}}
assert typechecks.discriminatorliterals(C) == {'t': {'a', }}
def test_literal_sorting(self):
class A(NamedTuple):
t: Literal[1]
i: int
class B(NamedTuple):
t: Literal[2, 3]
i: int
assert load({'t': 1, 'i': 12}, Union[A, B]) == A(1, 12)
assert load({'t': 2, 'i': 12}, Union[A, B]) == B(2, 12)
assert load({'t': 3, 'i': 12}, Union[A, B]) == B(3, 12)
def test_multiple_literal_sorting(self):
class A(NamedTuple):
t: Literal[1]
u: Literal[1]
class B(NamedTuple):
t: Literal[2]
i: int
assert load({'t': 1, 'u': 1}, Union[A, B]) == A(1, 1)
assert load({'t': 2, 'i': 12}, Union[A, B]) == B(2, 12)
typedload/tests/test_dumpload.py 0000664 0001750 0001750 00000002721 14630621362 016464 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/test_typeddict.py 0000664 0001750 0001750 00000012620 14644274436 016662 0 ustar salvo salvo # typedload
# Copyright (C) 2019-2024 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(TypeError):
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(TypeError):
load({'val': 'a','vil': 2}, C, failonextra=True)
def test_totality(self):
with self.assertRaises(TypeError):
load({}, A)
assert load({}, B) == {}
assert load({'val': 'a'}, B) == {'val': 'a'}
assert load({'vel': 'q'}, B) == {}
with self.assertRaises(TypeError):
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(TypeError):
o.pop('age')
load(o, Person)
with self.assertRaises(TypeError):
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)
if sys.version_info.minor >= 11:
# NotRequired is present from 3.11
from typing import NotRequired, Required
class TestRequired(unittest.TestCase):
def test_normal(self):
class A(TypedDict, total=False):
a: int
b: Required[int]
assert load({'a': 1, 'b': 1}, A) == {'a': 1, 'b': 1}
assert load({'b': 1}, A) == {'b': 1}
with self.assertRaises(TypeError):
load({}, A)
def test_abnormal(self):
class A(TypedDict, total=True):
a: int
b: Required[int]
assert load({'a': 1, 'b': 1}, A) == {'a': 1, 'b': 1}
with self.assertRaises(TypeError):
load({}, A)
with self.assertRaises(TypeError):
load({'a': 1}, A)
with self.assertRaises(TypeError):
load({'b': 1}, A)
def test_many(self):
class A(TypedDict, total=True):
a: Required[int]
b: Required[int]
c: NotRequired[int]
d: NotRequired[int]
class B(TypedDict, total=False):
a: Required[int]
b: Required[int]
c: NotRequired[int]
d: NotRequired[int]
with self.assertRaises(TypeError):
load({}, A)
with self.assertRaises(TypeError):
load({}, B)
with self.assertRaises(TypeError):
load({'c': 1}, A)
with self.assertRaises(TypeError):
load({'c': 1}, B)
assert load({'a': 1, 'b': 1}, A) == {'a': 1, 'b': 1}
assert load({'a': 1, 'b': 1}, B) == {'a': 1, 'b': 1}
with self.assertRaises(ValueError):
load({'a': 1, 'b': 'qqq'}, A)
class TestNotRequired(unittest.TestCase):
def test_standard(self):
class A(TypedDict):
i: int
o: NotRequired[int]
assert load({'i': 1}, A) == {'i': 1}
assert load({'i': 1, 'o': 2}, A) == {'i': 1, 'o': 2}
def test_nontotal(self):
class A(TypedDict, total = False):
i: int
o: NotRequired[int]
assert load({}, A) == {}
assert load({'i': 1}, A) == {'i': 1}
assert load({'i': 1, 'o': 2}, A) == {'i': 1, 'o': 2}
def test_mixtotal(self):
class A(TypedDict):
a: int
b: NotRequired[int]
class B(A, total=False):
c: int
d: NotRequired[int]
with self.assertRaises(TypeError):
load({}, B)
assert load({'a': 1}, B) == {'a': 1}
assert load({'a': 1, 'd':12}, B) == {'a': 1, 'd': 12}
typedload/tests/test_orunion.py 0000664 0001750 0001750 00000003143 14630621362 016347 0 ustar salvo salvo # typedload
# Copyright (C) 2022 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
from typedload import dataloader, load, dump, typechecks
class TestOrUnion(unittest.TestCase):
'''
From Python3.10 unions can be written as A | B.
That is a completely different internal than Union[A, B]
'''
def test_typechecker(self):
assert typechecks.is_union(int | str)
assert not typechecks.is_union(2 | 1)
def test_uniontypes(self):
u = int | str | float
assert int in typechecks.uniontypes(u)
assert str in typechecks.uniontypes(u)
assert float in typechecks.uniontypes(u)
assert bytes not in typechecks.uniontypes(u)
assert bool not in typechecks.uniontypes(u)
def test_loadnewunion(self):
t = list[int] | str
assert load('ciao', t) == 'ciao'
assert load(['1', 1.0, 0], t) == [1, 1, 0]
assert load(('1', 1.0, 0), t) == [1, 1, 0]
typedload/tests/test_datadumper.py 0000664 0001750 0001750 00000012301 14630621362 017000 0 ustar salvo salvo # typedload
# Copyright (C) 2018-2023 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 ipaddress import IPv4Address, IPv4Network, IPv4Interface, IPv6Address, IPv6Network, IPv6Interface
from pathlib import Path
import re
from typing import Dict, List, NamedTuple, Optional, Set, Tuple, Union
import unittest
from uuid import UUID
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
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([])
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'
def test_uuid(self):
assert dump(UUID('631b09cb-016e-11ef-97ce-000000000001')) == '631b09cb-016e-11ef-97ce-000000000001'
def test_pattern(self):
assert dump(re.compile(r'[bc](at|ot)\d+')) == r'[bc](at|ot)\d+'
assert dump(re.compile(br'[bc](at|ot)\d+')) == br'[bc](at|ot)\d+'
typedload/tests/test_datetime.py 0000664 0001750 0001750 00000010256 14630621362 016455 0 ustar salvo salvo # typedload
# Copyright (C) 2023 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
import unittest
from typedload import load, dump, dataloader, datadumper
class TestDatetimedump(unittest.TestCase):
def test_isodatetime(self):
dumper = datadumper.Dumper(isodates=True)
assert dumper.dump(datetime.date(2011, 12, 12)) == '2011-12-12'
assert dumper.dump(datetime.time(15, 41)) == '15:41:00'
assert dumper.dump(datetime.datetime(2019, 5, 31, 12, 44, 22)) == '2019-05-31T12:44:22'
assert dumper.dump(datetime.datetime(2023, 3, 20, 7, 43, 19, 906439, tzinfo=datetime.timezone.utc)) == '2023-03-20T07:43:19.906439+00:00'
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 TestDatetimeLoad(unittest.TestCase):
def test_isoload(self):
now = datetime.datetime.now()
assert load(now.isoformat(), datetime.datetime) == now
withtz = datetime.datetime(2023, 3, 20, 7, 43, 19, 906439, tzinfo=datetime.timezone.utc)
assert load(withtz.isoformat(), datetime.datetime) == withtz
date = datetime.date(2023, 4, 1)
assert load(date.isoformat(), datetime.date) == date
time = datetime.time(23, 44, 12)
assert load(time.isoformat(), datetime.time) == time
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 TestTimedelta(unittest.TestCase):
def test_findhandlers(self):
l = dataloader.Loader()
d = datadumper.Dumper()
l.index(datetime.timedelta)
d.index(datetime.timedelta(1))
def test_dumpdelta(self):
assert dump(datetime.timedelta(0, 1)) == 1.0
assert dump(datetime.timedelta(1, 1)) == 86400 + 1
assert dump(datetime.timedelta(3, 0.1)) == 86400 * 3 + 0.1
def test_loaddelta(self):
assert load(1.0, datetime.timedelta) == datetime.timedelta(0, 1)
assert load(86400, datetime.timedelta) == datetime.timedelta(1, 0)
assert load(86400.0, datetime.timedelta) == datetime.timedelta(1, 0)
def test_loaddump(self):
for i in [(0, 1), (2,12), (9, 50), (600, 0.4), (1000, 501)]:
delta = datetime.timedelta(*i)
assert load(dump(delta), datetime.timedelta) == delta
typedload/tests/test_exceptions.py 0000664 0001750 0001750 00000012512 14630621362 017037 0 ustar salvo salvo # typedload
# Copyright (C) 2021-2023 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, Set, FrozenSet
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_tuple_exceptions(self):
try:
load(('1',), Tuple[int, ...], basiccast=False)
except exceptions.TypedloadException as e:
assert e._path(e.trace) == '.[0]'
try:
load((1, '1',), Tuple[int, ...], basiccast=False)
except exceptions.TypedloadException as e:
assert e._path(e.trace) == '.[1]'
try:
load(('1'), Tuple[int], basiccast=False)
except exceptions.TypedloadException as e:
assert e._path(e.trace) == '.[0]'
try:
load(('1', 1), Tuple[int, int], basiccast=False)
except exceptions.TypedloadException as e:
assert e._path(e.trace) == '.[0]'
try:
load((1, '1'), Tuple[int, int], basiccast=False)
except exceptions.TypedloadException as e:
assert e._path(e.trace) == '.[1]'
try:
load((1,), Tuple[int, int], basiccast=False)
except exceptions.TypedloadException as e:
assert e._path(e.trace) == '.'
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)
def test_nested_wrong_type(self):
with self.assertRaises(exceptions.TypedloadException):
load([[1]], List[List[bytes]])
with self.assertRaises(exceptions.TypedloadException):
load([[1]], List[Tuple[bytes, ...]])
with self.assertRaises(exceptions.TypedloadException):
load([[1]], List[Set[bytes]])
with self.assertRaises(exceptions.TypedloadException):
load([[1]], List[FrozenSet[bytes]])
def test_notiterable_exception(self):
loader = dataloader.Loader()
with self.assertRaises(exceptions.TypedloadTypeError):
loader.load(None, List[int])
with self.assertRaises(exceptions.TypedloadTypeError):
loader.load(None, Tuple[int, ...])
with self.assertRaises(exceptions.TypedloadTypeError):
loader.load(None, Set[int])
with self.assertRaises(exceptions.TypedloadTypeError):
loader.load(None, FrozenSet[int])
typedload/tests/test_dataloader.py 0000664 0001750 0001750 00000050543 14635254646 017000 0 ustar salvo salvo # typedload
# Copyright (C) 2018-2024 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
from enum import Enum
from ipaddress import IPv4Address, IPv6Address, IPv6Network, IPv4Network, IPv4Interface, IPv6Interface
from pathlib import Path
import re
import sys
import typing
from typing import Dict, List, NamedTuple, Optional, Set, Tuple, Union, Any, NewType, FrozenSet
import unittest
from uuid import UUID
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 TestFastIterableLoad(unittest.TestCase):
def yielder(self):
yield from range(2)
yield "1"
def test_tupleload_from_generator_with_exception(self):
loader = dataloader.Loader(basiccast=False)
with self.assertRaises(exceptions.TypedloadValueError):
a = loader.load(self.yielder(), Tuple[int, ...])
with self.assertRaises(exceptions.TypedloadValueError):
a = loader.load(self.yielder(), Tuple[Union[float, int], ...])
loader = dataloader.Loader(basiccast=True)
assert loader.load(self.yielder(), Tuple[int, ...]) == (0, 1, 1)
assert loader.load(self.yielder(), Tuple[Union[float, int], ...]) == (0, 1, 1)
assert loader.load(self.yielder(), Tuple[Union[str, int], ...]) == (0, 1, '1')
def test_listload_from_generator_with_exception(self):
loader = dataloader.Loader(basiccast=False)
with self.assertRaises(exceptions.TypedloadValueError):
a = loader.load(self.yielder(), List[int])
with self.assertRaises(exceptions.TypedloadValueError):
a = loader.load(self.yielder(), List[Union[int, float]])
loader = dataloader.Loader(basiccast=True)
assert loader.load(self.yielder(), List[int]) == [0, 1, 1]
assert loader.load(self.yielder(), List[Union[float, int]]) == [0, 1, 1]
assert loader.load(self.yielder(), List[Union[int, str]]) == [0, 1, "1"]
def test_frozensetload_from_generator_with_exception(self):
loader = dataloader.Loader(basiccast=False)
with self.assertRaises(exceptions.TypedloadValueError):
a = loader.load(self.yielder(), FrozenSet[int])
loader = dataloader.Loader(basiccast=True)
assert loader.load(self.yielder(), FrozenSet[int]) == frozenset((0, 1, 1))
def test_setload_from_generator_with_exception(self):
loader = dataloader.Loader(basiccast=False)
with self.assertRaises(exceptions.TypedloadValueError):
a = loader.load(self.yielder(), Set[int])
with self.assertRaises(exceptions.TypedloadValueError):
a = loader.load(self.yielder(), Set[Union[int, float]])
loader = dataloader.Loader(basiccast=True)
assert loader.load(self.yielder(), Set[int]) == {0, 1, 1}
assert loader.load(self.yielder(), Set[Union[float, int]]) == {0, 1, 1}
assert loader.load(self.yielder(), Set[Union[int, str]]) == {0, 1, "1"}
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(TypeError):
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(TypeError):
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_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': 1}, B)
except Exception as e:
print(e.trace)
assert e.trace[-1].annotation[1] == 'a'
try:
loader.load({'a':3,'b': 1}, 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 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_pattern_str(self):
loader = dataloader.Loader()
if sys.version_info[:2] <= (3, 8):
with self.assertRaises(TypeError):
assert loader.load(r'[bc](at|ot)\d+', re.Pattern[str])
else:
assert loader.load(r'[bc](at|ot)\d+', re.Pattern[str]) == re.compile(r'[bc](at|ot)\d+')
assert loader.load(r'[bc](at|ot)\d+', typing.Pattern[str]) == re.compile(r'[bc](at|ot)\d+')
def test_pattern_bytes(self):
loader = dataloader.Loader()
if sys.version_info[:2] <= (3, 8):
with self.assertRaises(TypeError):
assert loader.load(br'[bc](at|ot)\d+', re.Pattern[bytes])
else:
assert loader.load(br'[bc](at|ot)\d+', re.Pattern[bytes]) == re.compile(br'[bc](at|ot)\d+')
assert loader.load(br'[bc](at|ot)\d+', typing.Pattern[bytes]) == re.compile(br'[bc](at|ot)\d+')
def test_pattern(self):
loader = dataloader.Loader()
assert loader.load(r'[bc](at|ot)\d+', re.Pattern) == re.compile(r'[bc](at|ot)\d+')
assert loader.load(br'[bc](at|ot)\d+', re.Pattern) == re.compile(br'[bc](at|ot)\d+')
assert loader.load(r'[bc](at|ot)\d+', typing.Pattern) == re.compile(r'[bc](at|ot)\d+')
assert loader.load(br'[bc](at|ot)\d+', typing.Pattern) == re.compile(br'[bc](at|ot)\d+')
# Right type, invalid value
with self.assertRaises(exceptions.TypedloadException) as e:
assert loader.load(r'((((((', re.Pattern)
with self.assertRaises(exceptions.TypedloadException) as e:
assert loader.load(br'((((((', re.Pattern)
with self.assertRaises(exceptions.TypedloadException) as e:
assert loader.load(r'((((((', typing.Pattern)
with self.assertRaises(exceptions.TypedloadException) as e:
assert loader.load(br'((((((', typing.Pattern)
with self.assertRaises(exceptions.TypedloadException) as e:
assert loader.load(r'(?P[bc])(?P(at|ot))\d+', re.Pattern)
with self.assertRaises(exceptions.TypedloadException) as e:
assert loader.load(br'(?P[bc])(?P(at|ot))\d+', re.Pattern)
with self.assertRaises(exceptions.TypedloadException) as e:
assert loader.load(r'(?P[bc])(?P(at|ot))\d+', typing.Pattern)
with self.assertRaises(exceptions.TypedloadException) as e:
assert loader.load(br'(?P[bc])(?P(at|ot))\d+', typing.Pattern)
# Wrong type
with self.assertRaises(exceptions.TypedloadTypeError) as e:
assert loader.load(33, re.Pattern)
with self.assertRaises(exceptions.TypedloadTypeError) as e:
assert loader.load(33, typing.Pattern)
with self.assertRaises(exceptions.TypedloadTypeError) as e:
assert loader.load(False, re.Pattern)
with self.assertRaises(exceptions.TypedloadTypeError) as e:
assert loader.load(False, typing.Pattern)
with self.assertRaises(exceptions.TypedloadTypeError) as e:
assert loader.load(None, re.Pattern)
with self.assertRaises(exceptions.TypedloadTypeError) as e:
assert loader.load(None, typing.Pattern)
def test_uuid(self):
loader = dataloader.Loader()
assert loader.load('631b09cb-016e-11ef-97ce-000000000001', UUID) == UUID('631b09cb-016e-11ef-97ce-000000000001')
# Invalid UUID
with self.assertRaises(ValueError):
loader.load('631b09cb-016e-11ef-97ce-00000000000', UUID)
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/__main__.py 0000664 0001750 0001750 00000003072 14635254652 015351 0 ustar salvo salvo # typedload
# Copyright (C) 2018-2024 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 < 8:
raise Exception('Only version 3.8 and above supported')
from .test_dataloader import *
from .test_datadumper import *
from .test_dumpload import *
from .test_exceptions import *
from .test_dataclass import *
from .test_deferred import *
from .test_legacytuples_dataloader import *
from .test_typechecks import *
from .test_datetime import *
from .test_literal import *
from .test_typeddict import *
if sys.version_info.minor >= 10:
from .test_orunion import *
# Run tests for attr only if it is installed
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_legacytuples_dataloader.py 0000664 0001750 0001750 00000021012 14630621362 021532 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(TypeError):
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(TypeError):
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_deferred.py 0000664 0001750 0001750 00000003276 14630621362 016445 0 ustar salvo salvo # typedload
# Copyright (C) 2022 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 __future__ import annotations
from dataclasses import dataclass
from typing import NamedTuple, Optional
import unittest
from typedload import load
class A(NamedTuple):
a: Optional[int]
@dataclass
class B:
a: Optional[int]
class TestDeferred(unittest.TestCase):
'''
This test should entirely be deleted when the PEP is superseeded.
'''
def test_deferred_named_tuple(self):
assert load({'a': None}, A, pep563=True) == A(None)
assert load({'a': 3}, A, pep563=True) == A(3)
with self.assertRaises(ValueError):
load({'a': None}, A)
with self.assertRaises(ValueError):
load({'a': 3}, A)
def test_deferred_dataclass(self):
assert load({'a': None}, B, pep563=True) == B(None)
assert load({'a': 3}, B, pep563=True) == B(3)
with self.assertRaises(TypeError):
load({'a': None}, B)
with self.assertRaises(TypeError):
load({'a': 3}, B)
typedload/tests/test_typechecks.py 0000664 0001750 0001750 00000017373 14644274436 017045 0 ustar salvo salvo # typedload
# Copyright (C) 2018-2024 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_not_required(self):
if sys.version_info.minor >= 11:
from typing import NotRequired, Required
assert typechecks.is_notrequired(NotRequired[int])
assert typechecks.is_notrequired(NotRequired[str])
assert typechecks.is_notrequired(NotRequired[Union[int, str]])
assert not typechecks.is_notrequired(Required[int])
assert not typechecks.is_notrequired(Required[str])
assert not typechecks.is_notrequired(Required[Union[int, str]])
assert (not typechecks.is_notrequired(None))
def test_is_required(self):
if sys.version_info.minor >= 11:
from typing import NotRequired, Required
assert typechecks.is_required(Required[int])
assert typechecks.is_required(Required[str])
assert typechecks.is_required(Required[Union[int, str]])
assert not typechecks.is_required(NotRequired[int])
assert not typechecks.is_required(NotRequired[str])
assert not typechecks.is_required(NotRequired[Union[int, str]])
assert (not typechecks.is_required(None))
def test_not_required(self):
if sys.version_info.minor < 11:
# Only from 3.11
return
from typing import NotRequired
assert int == typechecks.notrequiredtype(NotRequired[int])
def test_required(self):
if sys.version_info.minor < 11:
# Only from 3.11
return
from typing import Required
assert int == typechecks.requiredtype(Required[int])
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])
assert not typechecks.is_union(FrozenSet[int])
assert not typechecks.is_union(int)
def test_is_optional(self):
assert typechecks.is_optional(Optional[int])
assert typechecks.is_optional(Optional[str])
assert not typechecks.is_optional(Union[bytes, str])
assert not typechecks.is_optional(Union[str, int, float])
assert not typechecks.is_union(FrozenSet[int])
assert not typechecks.is_union(int)
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_dataclass.py 0000664 0001750 0001750 00000015311 14635254652 016626 0 ustar salvo salvo # typedload
# Copyright (C) 2018-2024 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
import sys
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_missing(self):
@dataclass
class A:
a: int
with self.assertRaises(TypeError):
load({}, A)
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')
if sys.version_info.minor >= 10:
def test_loadslotted(self):
@dataclass(slots=True)
class A:
a: int
assert load({'a': 1}, A) == A(1)
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
# This class has to be defined at the top-level of the module to be visible by get_type_hints
# when it resolves the "Node" string annotation.
@dataclass
class Node:
name: str
child: Optional["Node"] = None
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}
if sys.version_info.minor >= 10:
def test_dump_slots(self):
@dataclass(slots=True)
class A:
a: int
assert dump(A(1)) == {'a': 1}
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': []}
def test_cyclic_dump(self):
assert dump(Node("foo")) == {"name": "foo"}
assert dump(Node("foo", child=Node("bar"))) == {
"name": "foo",
"child": {"name": "bar"},
}
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(TypeError):
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/tests/test_attrload.py 0000664 0001750 0001750 00000021656 14630621362 016501 0 ustar salvo salvo # typedload
# Copyright (C) 2018-2024 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
import sys
from attr import attrs, attrib, define, field
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
def test_needed_missing(self):
@attrs
class A:
a: int = attrib()
b: int = attrib()
load({'a':1, 'b': 2}, A)
with self.assertRaises(exceptions.TypedloadTypeError):
load({}, A)
with self.assertRaises(exceptions.TypedloadTypeError):
load({'a':1}, A)
class TestAttrConverter(unittest.TestCase):
def test_old_style_int_conversion_any(self):
@attrs
class C:
a: int = attrib(converter=int)
b: int = attrib()
assert load({'a': '1', 'b': 1}, C) == C(1, 1)
with self.assertRaises(ValueError):
load({'a': 'a', 'b': 1}, C)
def test_new_style_int_conversion_any(self):
@define
class C:
a: int = field(converter=int)
b: int
assert load({'a': '1', 'b': 1}, C) == C(1, 1)
with self.assertRaises(ValueError):
load({'a': 'a', 'b': 1}, C)
def test_typed_conversion(self):
if sys.version_info.minor < 8:
# Skip for older than 3.8
return
from typing import Literal
@define
class A:
type: Literal['A']
value: int
@define
class B:
type: Literal['B']
value: str
def conv(param: Union[A, B]) -> B:
if isinstance(param, B):
return param
return B('B', str(param.value))
@define
class Outer:
inner: B = field(converter=conv)
v = load({'inner': {'type': 'A', 'value': 33}}, Outer)
assert v.inner.type == 'B'
assert v.inner.value == '33'
v = load({'inner': {'type': 'B', 'value': '33'}}, Outer)
assert v.inner.type == 'B'
assert v.inner.value == '33'
typedload/docs/ 0000775 0001750 0001750 00000000000 14644306633 013040 5 ustar salvo salvo typedload/docs/3.9_tagged_union_of_objects.svg 0000664 0001750 0001750 00000025600 14644306633 021015 0 ustar salvo salvo
typedload/docs/comparisons.md 0000664 0001750 0001750 00000014665 14630624137 015730 0 ustar salvo salvo Comparisons
===========
In this section we compare typedload to other similar libraries.
In general, the advantages of typedload over competing libraries are:
* Easy to use
* Very fast when Unions are involved
* Works with existing codebase and uses standard types. No inheritance or decorators
* Easy to extend, even with objects from 3rd party libraries
* Stable API, breaking changes only happen on major releases (it has happened once since 2018 and most users didn't notice)
* Mypy and similar work without plugins
* Can use and convert camelCase and snake_case
* Functional approach
* Pure Python, no compiling
* Very small, it's fast for automated tests to download, extract and install compared to huge binary libraries
### It works with existing codebase
Most libraries require your classes to extend or use decorators from the library itself.
This means that types from other libraries or non supported stdlib classes can never be used.
It also means that mypy will just work out of the box, rather than requiring plugins.
Instead, typedload works fine with the type annotations from the `typing` module and will work without requiring any changes to the datatypes.
### It is easy to extend
Since there can be situations that are highly domain specific, typedload allows to extend its functionality to support more types or replace the existing code to handle special cases.
### Support of Union
Other libraries tend to either be very [slow](https://pydantic-docs.helpmanual.io/) or just give completely wrong results when Union are involved. Typedload works without having to manually do annoying annotations.
# Functional approach
You can load a `list[YourType]`, without having to create a loader object or a useless container object.
apischema
---------
Found [here](https://github.com/wyfo/apischema)
It's the only viable alternative to typedload that I've encountered.
* Settings are global, a submodule changing a setting will affect the entire application
* Type checks are disabled by default
* It reuses the same objects in the output, so changing the data might result in subtle bugs if the input data is used again
* No native support for attrs (but can be manually added by the user)
pydantic
--------
Found [here](https://pydantic-docs.helpmanual.io/)
* Complicated API
* [The author calls you a liar if your pure python library is faster](https://news.ycombinator.com/item?id=36639943)
* [Breaks API all the time, between minor releases.](https://docs.pydantic.dev/latest/changelog/) (43 times in 2 major versions so far)
* [They hate](https://github.com/pydantic/pydantic/pull/3264) [benchmarks](https://github.com/pydantic/pydantic/pull/3881) [that show](https://github.com/pydantic/pydantic/pull/1810) [it's slow](https://github.com/pydantic/pydantic/pull/1525). [So they removed them altogether](https://github.com/pydantic/pydantic/pull/3973)
* It needs a mypy plugin, and for some kinds of classes it does no static checks whatsoever.
* Is now VC funded, so eventually some draconian monetizing plan might appear.
#### Version 1
* One of the slowest libraries that exist in this space
* `int | float` might decide to cast a `float` to `int`, or an `int` to `float`
#### Version 2
* Despite the rewrite in rust, and [taking inspiration from typedload's autotagging of unions](https://github.com/pydantic/pydantic/issues/5163#issuecomment-1619203179) somehow manages to be slower than pure python to load unions.
* Took them several years to make a version 2 where types on BaseModel finally mean the same thing that they mean in the rest of python
* Took them several years to implement unions that don't cast types at random
jsons
-----
Found [here](https://github.com/ramonhagenaars/jsons)
* Type safety is not a goal of this project
* It is buggy:
* This returns an int `jsons.load(1.1, Union[int, float])`
* This returns a string `jsons.load(1, Union[str, int])`
* This raises an exception `jsons.load(1.0, int | float)`
* It is incredibly slow (40x slower in certain cases)
For this reason it has been removed from the benchmarks.
* [Does not support `Literal`](https://github.com/ramonhagenaars/jsons/issues/170)
* Can't load iterables as lists
* Exceptions do not have information to find the incorrect data
#### Quick test
```python
# Needed because jsons can't load directly from range()
data = [i for i in range(3000000)]
# This took 2.5s with jsons and 200ms with typedload
load(data, list[int])
# This took 20s with jsons and 500ms with typedload
# And it converted all the ints to float!
load(data, list[Union[float,int]])
```
dataclasses-json
----------------
Found [here](https://github.com/lidatong/dataclasses-json)
*It is completely useless for type safety and very slow. I can't understand why it has users.*
* It is incredibly slow (20x slower in certain cases)
For this reason it has been removed from the benchmarks.
* It doesn't enforce type safety (it will be happy to load whatever inside an int field)
* Requires to decorate all the classes
* It is not extensible
* Has dependencies (marshmallow, marshmallow-enum, typing-inspect)
* Very complicated way for doing lists
#### Quick test
```python
# Just to show how unreliable this is
@dataclass_json
@dataclass
class Data:
data: list[int]
Data.from_dict({'data': ["4", None, ..., ('qwe',), 1.1]})
# This returns:
# Data(data=['4', None, Ellipsis, ('qwe',), 1.1])
# despite data is supposed to be a list of int
```
msgspec
-------
* Very fast, but unions don't work
* [The author will send you a PR to add his project to the benchmarks](https://github.com/ltworf/typedload/pull/390) [but will refuse to add your project to his benchmarks when you do the same](https://github.com/jcrist/msgspec/pull/333), saying that your project is not popular enough (despite it having many more downloads)
In theory unions do work, but you need to refactor your entire project around using msgspec's peculiar idea of unions to use them, and even then they are very limited in scope, compared to what other projects support and python users normally use.
* Implemented in C, won't run on PyPy
* Supports tagged Unions partially only when inheriting from its Struct type
Mypy will not typecheck those classes.
To use unions you must give up static typechecking.
* Doesn't support unions between regular dataclass/NamedTuple/Attrs/TypedDict
* Doesn't support untagged Unions
* Doesn't support multiple tags (e.g. `tag=Literal[1, 2]`)
* Extended using a single function that must handle all cases
* Can't replace type handlers
typedload/docs/errors.md 0000664 0001750 0001750 00000005572 14630621362 014701 0 ustar salvo salvo Errors in typedload
===================
All exceptions are subclasses of `TypedloadException`.
To make sure of that, there is an assertion in place that will fail if a type handler is misbehaving and raising the wrong type of exception.
The exceptions have a clear user message, but they offer an API to expose precise knowledge of the problem.
String trace
------------
By default when an error occurrs the path within the data structure is shown.
```python
from typing import *
import typedload
class Thing(NamedTuple):
value: int
class Data(NamedTuple):
field1: List[Thing]
field2: Tuple[Thing, ...]
typedload.load({'field1': [{'value': 12}, {'value': 'a'}], 'field2': []}, Data)
```
```python
TypedloadValueError: invalid literal for int() with base 10: 'a'
Path: .field1.[1].value
```
The path in the string description tells where the wrong value was found.
Trace
-----
To be able to locate where in the data an exception happened, `TypedloadException` has the `trace` property, which contains a list `TraceItem`, which help to track where the exception happened.
This can be useful to do more clever error handling.
For example:
```python
try:
typedload.load([1, 2, 'a'], List[int])
except Exception as e:
print(e.trace[-1])
```
Will raise an exception and print the last element in the trace
```python
TraceItem(value='a', type_=, annotation=Annotation(annotation_type=, value=2))
```
Another example, with an object:
```python
class O(NamedTuple):
data: List[int]
try:
typedload.load({'data': [1, 2, 'a']}, O)
except Exception as e:
for i in e.trace:
print(i)
```
Will print the entire trace:
```python
TraceItem(value={'data': [1, 2, 'a']}, type_=, annotation=None)
TraceItem(value=[1, 2, 'a'], type_=typing.List[int], annotation=Annotation(annotation_type=, value='data'))
TraceItem(value='a', type_=, annotation=Annotation(annotation_type=, value=2))
```
And checking the `annotation` field it is possible to find out that the issue happened in *data* at index *2*.
Union
-----
Because it is normal for a union of n types to generate n-1 exceptions, a union which fails generated n exceptions.
Typedload has no way of knowing which of those is the important exception that was expected to succeed and instead puts all the exceptions inside the `exception` field of the parent exception.
So all the sub exceptions can be investigated to decide which one is the most relevant one.
Raise exceptions in custom handlers
-----------------------------------
To find the path where the wrong value was found, typedload needs to trace the execution by using annotations.
This is used in handlers that do recursive calls to the loader.
See the source code of the handlers for Union and NamedTuple to see how this is done.
typedload/docs/3.9_load_list_of_lists.svg 0000664 0001750 0001750 00000027222 14644306633 020033 0 ustar salvo salvo
typedload/docs/3.9_load_list_of_ints.svg 0000664 0001750 0001750 00000024132 14644306633 017647 0 ustar salvo salvo
typedload/docs/3.11_fail_tagged_union_of_objects.svg 0000664 0001750 0001750 00000026424 14644306633 022066 0 ustar salvo salvo
typedload/docs/3.11_load_list_of_ints.svg 0000664 0001750 0001750 00000024131 14644306633 017717 0 ustar salvo salvo
typedload/docs/README.md 0000777 0001750 0001750 00000000000 14630621362 015776 2../README.md ustar salvo salvo typedload/docs/examples.md 0000664 0001750 0001750 00000031757 14630621362 015207 0 ustar salvo salvo Examples
========
Objects
-------
Three different kinds of objects are supported to be loaded and dumped back.
* NamedTuple (stdlib)
* dataclass (stdlib, since 3.7)
* attrs (3rd party module)
More or less they all work in the same way: the object is defined, types are assigned for the fields and typedload can inspect the class and create an instance from a dictionary, or go the other way to a dictionary from an instance.
```python
from typing import NamedTuple, List
from pathlib import Path
import typedload
from attr import attrs, attrib
class File(NamedTuple):
path: Union[str, Path]
size: int
@attrs
class Directory:
name = str
files: List[File] = attrib(factory=list) # mutable objects require a factory, not a default value
dir = {
'name': 'home',
'files': [
{'path': '/asd.txt', 'size': 0},
{'path': '/tmp/test.txt', 'size': 30},
]
}
# Load the dictionary into objects
d = typedload.load(dir, Directory)
# Out: Directory(files=[File(path='/asd.txt', size=0), File(path='/tmp/test.txt', size=30)])
# Dump the objects into a dictionary
typedload.dump(d)
```
Please see the other sections for more advanced usage.
Optional values
---------------
Python typing is a bit confusing about `Optional`. An `Optional[T]` means that the field can assume `None` as value, but the value must still be specified, and can't be omitted.
If, on the other hand, a variable has a default value, then when it's not explicitly specified, the default value is assumed.
Typedload follows exactly the normal behaviour of python and mypy.
```python
import typedload
from typing import Optional, NamedTuple
class User(NamedTuple):
username: str # Must be assigned
nickname: Optional[str] # Must be assigned and can be None
last_login: Optional[int] = None # Not required.
# This fails, as nickname is not present
typedload.load({'username': 'ltworf'}, User)
# TypedloadValueError: Value does not contain fields: {'nickname'} which are necessary for type User
# Those 2 work fine
typedload.load({'username': 'ltworf', 'nickname': None}, User)
# Out: User(username='ltworf', nickname=None, last_login=None)
typedload.load({'username': 'ltworf', 'nickname': 'LtWorf'}, User)
# Out: User(username='ltworf', nickname='LtWorf', last_login=None)
# Those 2 work fine too
typedload.load({'username': 'ltworf', 'nickname': None, 'last_login': None}, User)
# Out: User(username='ltworf', nickname=None, last_login=None)
typedload.load({'username': 'ltworf', 'nickname': None, 'last_login': 666}, User)
# Out: User(username='ltworf', nickname=None, last_login=666)
```
There is of course no relationship between a default value and `Optional`, so a default can be anything.
```python
class Coordinates(NamedTuple):
x: int = 0
y: int = 0
```
When dumping values, the fields which match with their default value are omitted.
```python
# Returns an empty dictionary
typedload.dump(Coordinates())
# Out: {}
# Returns only the x value
typedload.dump(Coordinates(x=42, y=0))
# Out: {'x': 42}
# Returns both coordinates
typedload.dump(Coordinates(), hidedefault=False)
# Out: {'x': 0, 'y': 0}
```
Unions
------
### Disable cast
Many times it is beneficial to disable casting when loading.
For example, if a value can be an object of a certain kind or a string, not disabling casting will cast any invalid object to a string, which might not be desired.
```python
import typedload
from typing import NamedTuple, Union
class Data(NamedTuple):
data: int
# This loads "{'date': 33}", since the object is not a valid Data object.
typedload.load({'date': 33}, Union[str, Data])
# Out: "{'date': 33}"
# This fails, because the dictionary is not cast to str
typedload.load({'date': 33}, Union[str, Data], basiccast=False)
# TypedloadValueError: Value of dict could not be loaded into typing.Union[str, __main__.Data]
```
### List or single object
Some terribly evil programmers use json in this way:
* A list in case they have multiple values
* A single object in case they have one value
* Nothing at all in case they have zero values
Let's see how typedload can help us survive the situation without having to handle all the cases every time.
```python
import typedload
from typing import NamedTuple, Union, List
import dataclasses
# Multiple data points, a list is used
data0 = {
"data_points": [{"x": 1.4, "y": 4.1}, {"x": 5.2, "y": 6.13}]
}
# A single data point. Instead of a list of 1 element, the element is passed directly
data1 = {
"data_points": {"x": 1.4, "y": 4.1}
}
# No data points. Instead of an empty list, the object is empty
data2 = {}
# Now we make our objects
class Point(NamedTuple):
x: float
y: float
@dataclasses.dataclass
class Data:
# We make an hidden field to load the data_points field from the json
# If the value is absent it will default to an empty list
# The hidden field can either be a List[Point] or directly a Point object
_data_points: Union[Point, List[Point]] = dataclasses.field(default_factory=list, metadata={'name': 'data_points'})
@property
def data_points(self) -> List[Point]:
# We make a property called data_points, that always returns a list
if not isinstance(self._data_points, list):
return [self._data_points]
return self._data_points
# Now we can load our data, and they will all be lists of Point
typedload.load(data0, Data).data_points
# Out: [Point(x=1.4, y=4.1), Point(x=5.2, y=6.13)]
typedload.load(data1, Data).data_points
# Out: [Point(x=1.4, y=4.1)]
typedload.load(data2, Data).data_points
# Out: []
```
### Objects
Loading different objects with a `Union` is of course possible, but some care is needed to avoid unexpected results.
For example, using objects with default values is a bad idea:
```python
import typedload
from typing import NamedTuple, Union, Optional
class Person(NamedTuple):
name: str = ''
class Data(NamedTuple):
data: Optional[str] = None
# WARNING: This might return either a Person or a Data. It's random
typedload.load({}, Union[Person, Data])
# Out: Data(data=None)
# Out: Person(name='')
```
This happens because in the union the order of the type is random, and either object works fine.
So you want to use union on objects that have at least one non default non colliding field.
You might want to use `failonextra` for objects whose fields are subset of other objects.
```python
import typedload
from typing import NamedTuple, Union
class Person(NamedTuple):
name: str
class Car(NamedTuple):
name: str
model: str
# This should be a Car, not a Person
data = {'name': 'macchina', 'model': 'TP21'}
# WARNING: This can return either a Person or a Car
typedload.load(data, Union[Person, Car])
# Out: Person(name='macchina')
# Out: Car(name='macchina', model='TP21')
# This can be explained by checking that both of these work
typedload.load(data, Person)
# Out: Person(name='macchina')
typedload.load(data, Car)
# Out: Car(name='macchina', model='TP21')
# The data we have works for both objects, and the union
# picks the first one (python sorts them randomly)
# We want to avoid that dictionary to be loaded as Person, so we use failonextra
# This fails
typedload.load(data, Person, failonextra=True)
# TypedloadValueError: Dictionary has unrecognized fields: model and cannot be loaded into Person
# This works
typedload.load(data, Car, failonextra=True)
# Out: Car(name='macchina', model='TP21')
# At this point the union will reliably pick the class that we want
typedload.load(data, Union[Person, Car], failonextra=True)
# Out: Car(name='macchina', model='TP21')
```
### Object type in value
Let's assume that our json objects contain a *type* field that names the object itself.
This makes conflicts impossible and so in the union the correct type will always be picked.
*This is very fast, because typedload will internally use the `Literal` values to try the best type in the union first.*
Slack sends events in this way.
```python
import typedload
from typing import List, Literal, Union, NamedTuple
events = [
{
"type": "message",
"text": "hello"
},
{
"type": "user-joined",
"username": "giufà"
}
]
# We have events that can be of many types
class Message(NamedTuple):
type: Literal['message']
text: str
class UserJoined(NamedTuple):
type: Literal['user-joined']
username: str
# Now to load our event list
typedload.load(events, List[Union[Message, UserJoined]])
# Out: [Message(type='message', text='hello'), UserJoined(type='user-joined', username='giufà')]
```
Name mangling
-------------
Name mangling is primarily used to deal with camel-case in codebases that use snake_case.
It is supported using `dataclass` and `attrs`, which provide metadata for the fields.
Let's assume that our original data uses camel case.
Since we are not maniacs, we want the fields in python to use snake_case, we do the following:
```python
from dataclasses import dataclass, field
import typedload
@dataclass
class Character:
first_name: str = field(metadata={'name': 'firstName'})
last_name: str = field(metadata={'name': 'lastName'})
data = {"firstName": "Paolino", "lastName": "Paperino"}
character = typedload.load(data, Character)
# Out: Character(first_name='Paolino', last_name='Paperino')
```
When dumping back the data
```python
typedload.dump(character)
# Out: {'lastName': 'Paperino', 'firstName': 'Paolino'}
```
the names will be converted back to camel case.
### Multiple name mangling schemes
If we want to load from a source and dump to another source that uses a different convention, we can use `mangle_key`
```python
from dataclasses import dataclass, field
import typedload
@dataclass
class Character:
first_name: str = field(metadata={'name': 'firstName', 'alt_name': 'first-name'})
last_name: str = field(metadata={'name': 'lastName', 'alt_name': 'last-name'})
data = {"firstName": "Paolino", "lastName": "Paperino"}
character = typedload.load(data, Character)
# Out: Character(first_name='Paolino', last_name='Paperino')
typedload.dump(character, mangle_key='alt_name')
# Out: {'last-name': 'Paperino', 'first-name': 'Paolino'}
```
Load and dump types from str
----------------------------
Some classes are easy to load and dump from `str`. For example this is done for `Path`.
Let's assume we want to have a class that is called `SerialNumber` that we load from a string and dump back to a string.
Here's how it can be done:
```python
from typing import List
import typedload.datadumper
import typedload.dataloader
class SerialNumber:
def __init__(self, sn: str) -> None:
# Some validation
if ' ' in sn:
raise Exception('Invalid serial number')
self.sn = sn
def __str__(self):
return self.sn
l = typedload.dataloader.Loader()
d = typedload.datadumper.Dumper()
l.strconstructed.add(SerialNumber)
d.strconstructed.add(SerialNumber)
serials = l.load(['1', '2', '3'], List[SerialNumber])
d.dump(serials)
```
Custom handlers
---------------
Let's assume that our codebase uses methods `from_json()` and `to_json()` as custom methods, and we want to use those.
```python
from typing import NamedTuple
import typedload.datadumper
import typedload.dataloader
import typedload.exceptions
# This is a NamedTuple, but we want to give priority to the from/to json methods
class Point(NamedTuple):
x: int
y: int
@staticmethod
def from_json(data):
# Checks on the data
# Typedload handlers must raise subclasses of TypedloadException to work properly
if not isinstance(data, list):
raise typedload.exceptions.TypedloadTypeError('List expected')
if len(data) != 2:
raise typedload.exceptions.TypedloadTypeError('Only 2 items')
if not all(isinstance(i, int) for i in data):
raise typedload.exceptions.TypedloadValueError('Values must be int')
# Return the data
return Point(*data)
def to_json(self):
return [self.x, self.y]
# We get a loader
l = typedload.dataloader.Loader()
# We find which handler handles NamedTuple
nt_handler = l.index(Point)
# We prepare a new handler
load_handler = (
lambda x: hasattr(x, 'from_json'), # Anything that has a from_json
lambda loader, value, type_: type_.from_json(value) # Call the from_json and return its value
)
# We add the new handler
l.handlers.insert(nt_handler, load_handler)
# Ready to try it!
l.load([1, 2], Point)
# Out: Point(x=1, y=2)
# Now we do the dumper
d = typedload.datadumper.Dumper()
nt_handler = d.index(Point(1,2)) # We need to use a real object to find the handler
dump_handler = (
lambda x: hasattr(x, 'from_json'), # Anything that has a from_json
lambda dumper, value, value_type: value.to_json() # Call the from_json and return its value
)
d.handlers.insert(nt_handler, dump_handler)
d.dump(Point(5, 5))
# Out: [5, 5]
```
Handlers basically permit doing anything, replacing current handlers or adding more to deal with more types.
You can just append them to the list if you are extending.
Remember to always use typedload exceptions, implement checks, and never modify the handler list after loading or dumping something.
typedload/docs/docs/ 0000775 0001750 0001750 00000000000 14630621362 013762 5 ustar salvo salvo typedload/docs/docs/donate.svg 0000777 0001750 0001750 00000000000 14630621362 020156 2../donate.svg ustar salvo salvo typedload/docs/docs/gpl3logo.png 0000777 0001750 0001750 00000000000 14630621362 020660 2../gpl3logo.png ustar salvo salvo typedload/docs/donate.svg 0000664 0001750 0001750 00000002661 14630620150 015024 0 ustar salvo salvo