From 27c9ca4dfcfc35b57052c3df1fb0bc4416c255ed Mon Sep 17 00:00:00 2001 From: M1ha Date: Sat, 2 Feb 2019 23:02:22 +0500 Subject: [PATCH] Added namedtuple with defaults compatible. --- src/django_clickhouse/compatibility.py | 47 ++++++++++++++++++++++++++ tests/test_compatibility.py | 36 ++++++++++++++++++++ 2 files changed, 83 insertions(+) create mode 100644 src/django_clickhouse/compatibility.py create mode 100644 tests/test_compatibility.py diff --git a/src/django_clickhouse/compatibility.py b/src/django_clickhouse/compatibility.py new file mode 100644 index 0000000..e95605c --- /dev/null +++ b/src/django_clickhouse/compatibility.py @@ -0,0 +1,47 @@ +import sys +from collections import namedtuple as basenamedtuple +from functools import lru_cache + +from copy import deepcopy + + +class NamedTuple: + __slots__ = ('_data', '_data_iterator') + _defaults = {} + _data_cls = None + + @classmethod + @lru_cache(maxsize=32) + def _get_defaults(cls, exclude): + res = cls._defaults + for k in exclude: + res.pop(k, None) + return res + + def __init__(self, *args, **kwargs): + new_kwargs = deepcopy(self._get_defaults(self._data_cls._fields[:len(args)])) + new_kwargs.update(kwargs) + self._data = self._data_cls(*args, **new_kwargs) + + def __getattr__(self, item): + return getattr(self._data, item) + + def __iter__(self): + self._data_iterator = iter(self._data) + return self + + def __next__(self): + return next(self._data_iterator) + + +def namedtuple(*args, **kwargs): + """ + Changes namedtuple to support defaults parameter as python 3.7 does + https://docs.python.org/3.7/library/collections.html#collections.namedtuple + :return: namedtuple class + """ + if sys.version_info < (3, 7): + defaults = kwargs.pop('defaults', {}) + return type('namedtuple', (NamedTuple,), {'_defaults': defaults, '_data_cls': basenamedtuple(*args, **kwargs)}) + else: + return basenamedtuple(*args, **kwargs) diff --git a/tests/test_compatibility.py b/tests/test_compatibility.py new file mode 100644 index 0000000..5b113db --- /dev/null +++ b/tests/test_compatibility.py @@ -0,0 +1,36 @@ +from unittest import TestCase + +from django_clickhouse.compatibility import namedtuple + + +class NamedTupleTest(TestCase): + def test_defaults(self): + TestTuple = namedtuple('TestTuple', ('a', 'b', 'c'), defaults={'c': 3}) + self.assertTupleEqual((1, 2, 3), tuple(TestTuple(1, b=2))) + self.assertTupleEqual((1, 2, 4), tuple(TestTuple(1, 2, 4))) + self.assertTupleEqual((1, 2, 4), tuple(TestTuple(a=1, b=2, c=4))) + + def test_exceptions(self): + TestTuple = namedtuple('TestTuple', ('a', 'b', 'c'), defaults={'c': 3}) + with self.assertRaises(TypeError): + TestTuple(b=1, c=4) + + with self.assertRaises(TypeError): + TestTuple(1, 2, 3, c=4) + + def test_different_defaults(self): + # Test that 2 tuple type defaults don't affect each other + TestTuple = namedtuple('TestTuple', ('a', 'b', 'c'), defaults={'c': 3}) + OtherTuple = namedtuple('TestTuple', ('a', 'b', 'c'), defaults={'c': 4}) + t1 = TestTuple(a=1, b=2) + t2 = OtherTuple(a=3, b=4) + self.assertTupleEqual((1, 2, 3), tuple(t1)) + self.assertTupleEqual((3, 4, 4), tuple(t2)) + + def test_defaults_cache(self): + # Test that 2 tuple instances don't affect each other's defaults + TestTuple = namedtuple('TestTuple', ('a', 'b', 'c'), defaults={'c': 3}) + self.assertTupleEqual((1, 2, 4), tuple(TestTuple(a=1, b=2, c=4))) + self.assertTupleEqual((1, 2, 3), tuple(TestTuple(a=1, b=2))) + +