diff --git a/performance/__main__.py b/performance/__main__.py index e784ca35..51cec8e8 100644 --- a/performance/__main__.py +++ b/performance/__main__.py @@ -1,8 +1,11 @@ +import argparse import collections import datetime import timeit -import argparse +import typing as tp +from enum import Enum +from automap import FrozenAutoMap import numpy as np from performance.reference.util import mloc as mloc_ref @@ -17,6 +20,8 @@ from performance.reference.util import dtype_from_element as dtype_from_element_ref from performance.reference.util import array_deepcopy as array_deepcopy_ref from performance.reference.util import isna_element as isna_element_ref +from performance.reference.util import is_gen_copy_values as is_gen_copy_values_ref +from performance.reference.util import prepare_iter_for_array as prepare_iter_for_array_ref from performance.reference.array_go import ArrayGO as ArrayGOREF @@ -32,6 +37,8 @@ from arraykit import dtype_from_element as dtype_from_element_ak from arraykit import array_deepcopy as array_deepcopy_ak from arraykit import isna_element as isna_element_ak +from arraykit import is_gen_copy_values as is_gen_copy_values_ak +from arraykit import prepare_iter_for_array as prepare_iter_for_array_ak from arraykit import ArrayGO as ArrayGOAK @@ -360,6 +367,120 @@ class IsNaElementPerfREF(IsNaElementPerf): #------------------------------------------------------------------------------- +class IsGenCopyValues(Perf): + NUMBER = 2500 + + def pre(self): + self.objects = [ + [1, 2, 3], + (1, 2, 3), + FrozenAutoMap((1, 2, 3)), + {1, 2, 3}, + {1:1, 2:2, 3:3}, + ] + + def main(self): + for _ in range(200): + for obj in self.objects: + self.entry(obj) + +class IsGenCopyValuesAK(IsGenCopyValues): + entry = staticmethod(is_gen_copy_values_ak) + +class IsGenCopyValuesREF(IsGenCopyValues): + entry = staticmethod(is_gen_copy_values_ref) + + +#------------------------------------------------------------------------------- +class PrepareIterForArray(Perf): + NUMBER = 5 + FUNCTIONS = ('iter_small', 'iter_large') + + def pre(self): + def a() -> tp.Iterator[tp.Any]: + for i in range(3): + yield i + yield None + + def b() -> tp.Iterator[tp.Any]: + yield None + for i in range(3): + yield i + + def c() -> tp.Iterator[tp.Any]: + yield 10 + yield None + for i in range(3): + yield i + yield (3,4) + + class E(Enum): + A = 1 + B = 2 + C = 3 + + self.small_iterables = [ + ('a', 'b', 'c'), + ('a', 'b', 3), + ('a', 'b', (1, 2)), + [True, False, True], + (1, 2, 4.3, 2), + (1, 2, 4.3, 2, None), + (1, 2, 4.3, 2, 'g'), + range(4), + [3, 2, (3,4)], + [300000000000000002, 5000000000000000001], + range(3, 7), + [0.0, 36_028_797_018_963_969], + (x for x in ()), + list(), + tuple(), + dict(), + set(), + FrozenAutoMap((1, 2, 3, 4, 5, 6)), + [E.A, E.B, E.C], + ] + + self.small_iterables.extend([iter(iterable) for iterable in self.small_iterables]) + self.small_iterables.extend((a(), b(), c())) + + self.large_iterables = [ + ('a', 'b', 'c') * 10000, + ('a', 'b', 'c') * 10000 + (1, ), + ('a', 'b', 'c') * 10000 + ((1, 2), ), + [True, False, True] * 10000, + (1, 2, 4.3, 2) * 10000, + (1, 2, 4.3, 2) * 10000 + (None, ), + (1, 2, 4.3, 2) * 10000 + ('g', ), + range(10000), + [3, 2, 1] * 10000 + [(3,4)], + [300000000000000002] * 20000 + [5000000000000000001], + range(30000, 40000), + [0.0] * 20000 + [36_028_797_018_963_969], + FrozenAutoMap(range(10000)), + [E.A, E.B, E.C] * 10000, + ] + self.large_iterables.extend([iter(iterable) for iterable in self.large_iterables]) + + def iter_small(self): + for _ in range(2000): + for restrict_copy in (True, False): + for iterable in self.small_iterables: + self.entry(iterable, restrict_copy=restrict_copy) + + def iter_large(self): + for restrict_copy in (True, False): + for iterable in self.large_iterables: + self.entry(iterable, restrict_copy=restrict_copy) + +class PrepareIterForArrayAK(PrepareIterForArray): + entry = staticmethod(prepare_iter_for_array_ak) + +class PrepareIterForArrayREF(PrepareIterForArray): + entry = staticmethod(prepare_iter_for_array_ref) + +#------------------------------------------------------------------------------- + def get_arg_parser(): @@ -398,7 +519,7 @@ def main(): number=cls_runner.NUMBER) records.append((cls_perf.__name__, func_attr, results['ak'], results['ref'], results['ref'] / results['ak'])) - width = 24 + width = 32 for record in records: print(''.join( (r.ljust(width) if isinstance(r, str) else str(round(r, 8)).ljust(width)) for r in record diff --git a/performance/reference/util.py b/performance/reference/util.py index 0f2d0efc..fc47af13 100644 --- a/performance/reference/util.py +++ b/performance/reference/util.py @@ -1,8 +1,12 @@ import typing as tp from copy import deepcopy +from collections import abc +from automap import FrozenAutoMap # pylint: disable = E0611 import numpy as np +DtypeSpecifier = tp.Optional[tp.Union[str, np.dtype, type]] + DTYPE_DATETIME_KIND = 'M' DTYPE_TIMEDELTA_KIND = 'm' DTYPE_COMPLEX_KIND = 'c' @@ -25,6 +29,17 @@ DTYPES_BOOL = (DTYPE_BOOL,) DTYPES_INEXACT = (DTYPE_FLOAT_DEFAULT, DTYPE_COMPLEX_DEFAULT) +INEXACT_TYPES = (float, complex, np.inexact) # inexact matches floating, complexfloating + +DICTLIKE_TYPES = (abc.Set, dict, FrozenAutoMap) + +# iterables that cannot be used in NP array constructors; asumes that dictlike +# types have already been identified +INVALID_ITERABLE_FOR_ARRAY = (abc.ValuesView, abc.KeysView) + +# integers above this value will lose precision when coerced to a float +INT_MAX_COERCIBLE_TO_FLOAT = 9_007_199_256_349_108 + def mloc(array: np.ndarray) -> int: '''Return the memory location of an array. @@ -216,3 +231,94 @@ def dtype_from_element(value: tp.Optional[tp.Hashable]) -> np.dtype: # NOTE: calling array and getting dtype on np.nan is faster than combining isinstance, isnan calls return np.array(value).dtype + +def is_gen_copy_values(values: tp.Iterable[tp.Any]) -> tp.Tuple[bool, bool]: + ''' + Returns: + copy_values: True if values cannot be used in an np.array constructor.` + ''' + if hasattr(values, '__len__'): + if isinstance(values, DICTLIKE_TYPES + INVALID_ITERABLE_FOR_ARRAY): + # Dict-like iterables need copies + return False, True + + return False, False + + # We are a generator and all generators need copies + return True, True + + +def prepare_iter_for_array( + values: tp.Iterable[tp.Any], + restrict_copy: bool = False + ) -> tp.Tuple[DtypeSpecifier, bool, tp.Sequence[tp.Any]]: + ''' + Determine an appropriate DtypeSpecifier for values in an iterable. + This does not try to determine the actual dtype, but instead, if the DtypeSpecifier needs to be + object rather than None (which lets NumPy auto detect). + This is expected to only operate on 1D data. + + Args: + values: can be a generator that will be exhausted in processing; + if a generator, a copy will be made and returned as values. + restrict_copy: if True, reject making a copy, even if a generator is given + + Returns: + resolved_dtype, has_tuple, values + ''' + is_gen, copy_values = is_gen_copy_values(values) + + if not is_gen and len(values) == 0: #type: ignore + return None, False, values #type: ignore + + if restrict_copy: + copy_values = False + + v_iter = values if is_gen else iter(values) + + if copy_values: + values_post = [] + + resolved = None # None is valid specifier if the type is not ambiguous + + has_tuple = False + has_str = False + has_non_str = False + has_inexact = False + has_big_int = False + + for v in v_iter: + if copy_values: + # if a generator, have to make a copy while iterating + values_post.append(v) + + value_type = type(v) + + if (value_type is str + or value_type is np.str_ + or value_type is bytes + or value_type is np.bytes_): + # must compare to both string types + has_str = True + elif hasattr(v, '__len__'): + # identify SF types by if they have STATIC attr they also must be assigned after array creation, so we treat them like tuples + has_tuple = True + resolved = object + break + else: + has_non_str = True + if value_type in INEXACT_TYPES: + has_inexact = True + elif value_type is int and abs(v) > INT_MAX_COERCIBLE_TO_FLOAT: + has_big_int = True + + if (has_str and has_non_str) or (has_big_int and has_inexact): + resolved = object + break + + if copy_values: + # v_iter is an iter, we need to finish it + values_post.extend(v_iter) + return resolved, has_tuple, values_post + + return resolved, has_tuple, values #type: ignore diff --git a/requirements-test.txt b/requirements-test.txt index 3e97c395..5cac43b3 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -3,4 +3,4 @@ numpy==1.17.4 pytest==3.8.0 pylint==2.7.4 invoke==1.4.0 - +automap==0.4.8 diff --git a/requirements.txt b/requirements.txt index 86cb1ef2..00d5cc07 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,3 @@ numpy==1.17.4 +automap==0.4.8 + diff --git a/src/__init__.py b/src/__init__.py index 988ca110..9a3b33b8 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -3,6 +3,7 @@ # pylint: disable=C0414 from ._arraykit import __version__ +from ._arraykit import FrozenAutoMap as FrozenAutoMap from ._arraykit import ArrayGO as ArrayGO from ._arraykit import immutable_filter as immutable_filter from ._arraykit import mloc as mloc @@ -16,3 +17,6 @@ from ._arraykit import resolve_dtype_iter as resolve_dtype_iter from ._arraykit import isna_element as isna_element from ._arraykit import dtype_from_element as dtype_from_element +from ._arraykit import is_gen_copy_values as is_gen_copy_values +from ._arraykit import prepare_iter_for_array as prepare_iter_for_array + diff --git a/src/__init__.pyi b/src/__init__.pyi index 4ff12eb9..e8c79e41 100644 --- a/src/__init__.pyi +++ b/src/__init__.pyi @@ -1,8 +1,10 @@ import typing as tp +from automap import FrozenAutoMap import numpy as np # type: ignore _T = tp.TypeVar('_T') +_DtypeSpecifier = tp.Optional[tp.Union[str, np.dtype, type]] __version__: str @@ -32,4 +34,9 @@ def resolve_dtype(__d1: np.dtype, __d2: np.dtype) -> np.dtype: ... def resolve_dtype_iter(__dtypes: tp.Iterable[np.dtype]) -> np.dtype: ... def isna_element(__value: tp.Any) -> bool: ... def dtype_from_element(__value: tp.Optional[tp.Hashable]) -> np.dtype: ... +def is_gen_copy_values(__values: tp.Iterable[tp.Any]) -> tp.Tuple[bool, bool]: ... +def prepare_iter_for_array( + __values: tp.Iterable[tp.Any], + restrict_copy: bool = ..., + ) -> tp.Tuple[_DtypeSpecifier, bool, tp.Sequence[tp.Any]]: ... diff --git a/src/_arraykit.c b/src/_arraykit.c index f8906a5c..3606d240 100644 --- a/src/_arraykit.c +++ b/src/_arraykit.c @@ -1,5 +1,6 @@ # include "Python.h" # include "structmember.h" +# include # define PY_ARRAY_UNIQUE_SYMBOL AK_ARRAY_API # define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION @@ -63,6 +64,8 @@ fprintf(stderr, #msg); \ _AK_DEBUG_END() +# define AK_INT_MAX_COERCIBLE_TO_FLOAT 9007199256349108l + # if defined __GNUC__ || defined __clang__ # define AK_LIKELY(X) __builtin_expect(!!(X), 1) # define AK_UNLIKELY(X) __builtin_expect(!!(X), 0) @@ -362,6 +365,239 @@ resolve_dtype_iter(PyObject *Py_UNUSED(m), PyObject *arg) return (PyObject *)AK_ResolveDTypeIter(arg); } +typedef enum IsGenCopyValues { + ERR, + IS_GEN, + NOT_GEN_COPY, + NOT_GEN_NO_COPY +} IsGenCopyValues; + +static IsGenCopyValues +AK_is_gen_copy_values(PyObject *values, PyObject *frozenAutoMap) +{ + if(PyObject_HasAttrString(values, "__len__")) { + if (PySet_Check(values) || PyDict_Check(values) || + PyDictValues_Check(values) || PyDictKeys_Check(values)) + { + return NOT_GEN_COPY; + } + + switch (PyObject_IsInstance(values, frozenAutoMap)) + { + case -1: + return ERR; + + case 0: + return NOT_GEN_NO_COPY; + + default: // 1 + return NOT_GEN_COPY; + } + } + + return IS_GEN; +} + +static PyObject* +is_gen_copy_values(PyObject *m, PyObject *arg) +{ + // This only exists to allow `AK_is_gen_copy_values` to be tested + + PyObject *frozenAutoMap = PyObject_GetAttrString(m, "FrozenAutoMap"); + if (!frozenAutoMap) { + return NULL; + } + + PyObject *is_gen = Py_False; + PyObject *copy_values = Py_False; + + IsGenCopyValues is_gen_ret = AK_is_gen_copy_values(arg, frozenAutoMap); + Py_DECREF(frozenAutoMap); + + if (is_gen_ret == IS_GEN) { + is_gen = Py_True; + copy_values = Py_True; + } + else if (is_gen_ret == NOT_GEN_COPY) { + copy_values = Py_True; + } + else if (is_gen_ret == ERR) { + return NULL; + } + + return PyTuple_Pack(2, is_gen, copy_values); +} + +static PyObject* +prepare_iter_for_array(PyObject *m, PyObject *args, PyObject *kwargs) +{ + PyObject *values; + int restrict_copy = 0; // False by default + + static char *kwlist[] = {"values", "restrict_copy", NULL}; + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O|p:prepare_iter_for_array", + kwlist, + &values, + &restrict_copy)) + { + return NULL; + } + + PyObject *frozenAutoMap = PyObject_GetAttrString(m, "FrozenAutoMap"); + if (!frozenAutoMap) { + return NULL; + } + + bool is_gen = false; + bool copy_values = false; + + IsGenCopyValues is_gen_ret = AK_is_gen_copy_values(values, frozenAutoMap); + Py_DECREF(frozenAutoMap); + + if (is_gen_ret == IS_GEN) { + is_gen = true; + copy_values = true; + } + else if (is_gen_ret == NOT_GEN_COPY) { + copy_values = true; + } + else if (is_gen_ret == ERR) { + return NULL; + } + + Py_ssize_t len; + + if (!is_gen) { + len = PyObject_Size(values); + if (len == -1) { + return NULL; + } + if (len == 0) { + return PyTuple_Pack(3, Py_None, Py_False, values); + } + } + + PyObject *copied_values = NULL; + + if (restrict_copy) { + copy_values = false; + } + else if (copy_values) { + copied_values = PyList_New(0); + if (!copied_values) { + return NULL; + } + } + + len = 0; + bool is_object = false; + bool has_tuple = false; + bool has_str = false; + bool has_non_str = false; + bool has_inexact = false; + bool has_big_int = false; + + PyObject *iterator = PyObject_GetIter(values); + if (!iterator) { + goto failure; + } + PyObject *item = NULL; + + while ((item = PyIter_Next(iterator))) { + if (copy_values) { + if (-1 == PyList_Append(copied_values, item)) { + goto failure_during_iteration; + } + ++len; + } + + // This API call always succeeds. + if (PyUnicode_Check(item)) { + has_str = true; + } + else if (PyObject_HasAttrString(item, "__len__")) { + has_tuple = true; + is_object = true; + } + else { + has_non_str = true; + + // These two API calls always succeed. 96% sure this catches np types + if (!has_inexact && (PyFloat_Check(item) || PyComplex_Check(item))) { + has_inexact = true; + } + else if (!has_big_int && PyLong_Check(item)) { + int overflow; + npy_int64 item_val = PyLong_AsLongLongAndOverflow(item, &overflow); + if (-1 == item_val) { + if (PyErr_Occurred()) { + goto failure_during_iteration; + } + } + else { + has_big_int |= (overflow != 0 || Py_ABS(item_val) > AK_INT_MAX_COERCIBLE_TO_FLOAT); + } + } + } + + is_object |= (has_str && has_non_str) || (has_big_int && has_inexact); + + if (is_object) { + if (copy_values) { + if (-1 == PyList_SetSlice(copied_values, len, len, iterator)) { + goto failure_during_iteration; + } + } + Py_DECREF(item); + break; + } + + Py_DECREF(item); + } + + Py_DECREF(iterator); + + if (PyErr_Occurred()) { + goto failure; + } + + PyObject *resolved = NULL; + if (is_object) { + resolved = (PyObject*)PyArray_DescrFromType(NPY_OBJECT); + } else { + resolved = Py_None; + } + Py_INCREF(resolved); + + PyObject *is_tuple_obj = PyBool_FromLong(has_tuple); + if (!is_tuple_obj) { + Py_DECREF(resolved); + goto failure; + } + + if (copy_values) { + PyObject *ret = PyTuple_Pack(3, resolved, is_tuple_obj, copied_values); + Py_DECREF(resolved); + Py_DECREF(is_tuple_obj); + Py_DECREF(copied_values); + return ret; + } + + PyObject *ret = PyTuple_Pack(3, resolved, is_tuple_obj, values); + Py_DECREF(resolved); + Py_DECREF(is_tuple_obj); + return ret; + +failure_during_iteration: + Py_DECREF(iterator); + Py_DECREF(item); + +failure: + Py_XDECREF(copied_values); + return NULL; +} + //------------------------------------------------------------------------------ // general utility @@ -490,6 +726,7 @@ isna_element(PyObject *Py_UNUSED(m), PyObject *arg) Py_RETURN_FALSE; } + //------------------------------------------------------------------------------ // ArrayGO //------------------------------------------------------------------------------ @@ -770,6 +1007,8 @@ static PyMethodDef arraykit_methods[] = { NULL}, {"resolve_dtype", resolve_dtype, METH_VARARGS, NULL}, {"resolve_dtype_iter", resolve_dtype_iter, METH_O, NULL}, + {"is_gen_copy_values", is_gen_copy_values, METH_O, NULL}, + {"prepare_iter_for_array", (PyCFunction)prepare_iter_for_array, METH_VARARGS | METH_KEYWORDS, NULL}, {"isna_element", isna_element, METH_O, NULL}, {"dtype_from_element", dtype_from_element, METH_O, NULL}, {NULL}, @@ -784,12 +1023,30 @@ PyInit__arraykit(void) { import_array(); PyObject *m = PyModule_Create(&arraykit_module); - if (!m || - PyModule_AddStringConstant(m, "__version__", Py_STRINGIFY(AK_VERSION)) || + if (!m) { + return NULL; + } + + PyObject *automap_module = PyImport_ImportModule("automap"); + if (!automap_module) { + Py_DECREF(m); + return NULL; + } + + PyObject *frozenAutoMapClass = PyObject_GetAttrString(automap_module, "FrozenAutoMap"); + Py_DECREF(automap_module); + if (!frozenAutoMapClass) { + Py_DECREF(m); + return NULL; + } + + if (PyModule_AddStringConstant(m, "__version__", Py_STRINGIFY(AK_VERSION)) || PyType_Ready(&ArrayGOType) || - PyModule_AddObject(m, "ArrayGO", (PyObject *) &ArrayGOType)) + PyModule_AddObject(m, "ArrayGO", (PyObject *) &ArrayGOType) || + PyModule_AddObject(m, "FrozenAutoMap", frozenAutoMapClass)) { - Py_XDECREF(m); + Py_DECREF(frozenAutoMapClass); + Py_DECREF(m); return NULL; } return m; diff --git a/test/test_util.py b/test/test_util.py index dcdc1c24..c19d9413 100644 --- a/test/test_util.py +++ b/test/test_util.py @@ -1,9 +1,11 @@ import collections import datetime -import unittest import itertools +import typing as tp +import unittest import numpy as np # type: ignore +from automap import FrozenAutoMap from arraykit import resolve_dtype from arraykit import resolve_dtype_iter @@ -14,11 +16,14 @@ from arraykit import mloc from arraykit import immutable_filter from arraykit import array_deepcopy +from arraykit import is_gen_copy_values from arraykit import isna_element from arraykit import dtype_from_element from performance.reference.util import mloc as mloc_ref +from arraykit import prepare_iter_for_array + class TestUnit(unittest.TestCase): @@ -368,6 +373,170 @@ def test_dtype_from_element_str_and_bytes_dtypes(self) -> None: self.assertEqual(np.dtype(f'|S{size}'), dtype_from_element(bytes(size))) self.assertEqual(np.dtype(f' None: + self.assertEqual((True, True), is_gen_copy_values((x for x in range(3)))) + + l = [1, 2, 3] + t = (1, 2, 3) + fam = FrozenAutoMap((1, 2, 3)) + s = {1, 2, 3} + d = {1:1, 2:2, 3:3} + + self.assertEqual((False, False), is_gen_copy_values(l)) + self.assertEqual((False, False), is_gen_copy_values(t)) + + self.assertEqual((False, True), is_gen_copy_values(fam)) + self.assertEqual((False, True), is_gen_copy_values(d.keys())) + self.assertEqual((False, True), is_gen_copy_values(d.values())) + self.assertEqual((False, True), is_gen_copy_values(d)) + self.assertEqual((False, True), is_gen_copy_values(s)) + + +class TestPrepareIterUnit(unittest.TestCase): + def test_resolve_type_iter_a(self) -> None: + resolved, has_tuple, values = prepare_iter_for_array(('a', 'b', 'c')) + self.assertIsNone(resolved) + + resolved, has_tuple, values = prepare_iter_for_array(('a', 'b', 3)) + self.assertIsNotNone(resolved) + + resolved, has_tuple, values = prepare_iter_for_array(('a', 'b', (1, 2))) + self.assertIsNotNone(resolved) + self.assertTrue(has_tuple) + + resolved, has_tuple, values = prepare_iter_for_array((1, 2, 4.3, 2)) + self.assertIsNone(resolved) + + resolved, has_tuple, values = prepare_iter_for_array((1, 2, 4.3, 2, None)) + self.assertIsNone(resolved) + + resolved, has_tuple, values = prepare_iter_for_array((1, 2, 4.3, 2, 'g')) + self.assertIsNotNone(resolved) + + resolved, has_tuple, values = prepare_iter_for_array(()) + self.assertIsNone(resolved) + + def test_resolve_type_iter_b(self) -> None: + resolved, has_tuple, values = prepare_iter_for_array(iter(('a', 'b', 'c'))) + self.assertIsNone(resolved) + + resolved, has_tuple, values = prepare_iter_for_array(iter(('a', 'b', 3))) + self.assertIsNotNone(resolved) + + resolved, has_tuple, values = prepare_iter_for_array(iter(('a', 'b', (1, 2)))) + self.assertIsNotNone(resolved) + self.assertTrue(has_tuple) + + resolved, has_tuple, values = prepare_iter_for_array(range(4)) + self.assertIsNone(resolved) + + def test_resolve_type_iter_c(self) -> None: + a = [True, False, True] + resolved, has_tuple, values = prepare_iter_for_array(a) + self.assertEqual(id(a), id(values)) + + resolved, has_tuple, values = prepare_iter_for_array(iter(a)) + self.assertNotEqual(id(a), id(values)) + + self.assertIsNone(resolved) + self.assertEqual(has_tuple, False) + + def test_resolve_type_iter_d(self) -> None: + a = [3, 2, (3,4)] + resolved, has_tuple, values = prepare_iter_for_array(a) + self.assertEqual(id(a), id(values)) + self.assertTrue(has_tuple) + + resolved, has_tuple, values = prepare_iter_for_array(iter(a)) + self.assertNotEqual(id(a), id(values)) + + self.assertIsNotNone(resolved) + self.assertEqual(has_tuple, True) + + def test_resolve_type_iter_e(self) -> None: + a = [300000000000000002, 5000000000000000001] + resolved, has_tuple, values = prepare_iter_for_array(a) + self.assertEqual(id(a), id(values)) + + resolved, has_tuple, values = prepare_iter_for_array(iter(a)) + self.assertNotEqual(id(a), id(values)) + self.assertIsNone(resolved) + self.assertEqual(has_tuple, False) + + def test_resolve_type_iter_f(self) -> None: + + def a() -> tp.Iterator[tp.Any]: + for i in range(3): + yield i + yield None + + resolved, has_tuple, values = prepare_iter_for_array(a()) + self.assertEqual(values, [0, 1, 2, None]) + self.assertIsNone(resolved) + self.assertEqual(has_tuple, False) + + def test_resolve_type_iter_g(self) -> None: + + def a() -> tp.Iterator[tp.Any]: + yield None + for i in range(3): + yield i + + resolved, has_tuple, values = prepare_iter_for_array(a()) + self.assertEqual(values, [None, 0, 1, 2]) + self.assertIsNone(resolved) + self.assertEqual(has_tuple, False) + + def test_resolve_type_iter_h(self) -> None: + + def a() -> tp.Iterator[tp.Any]: + yield 10 + yield None + for i in range(3): + yield i + yield (3,4) + + resolved, has_tuple, values = prepare_iter_for_array(a()) + self.assertEqual(values, [10, None, 0, 1, 2, (3,4)]) + self.assertIsNotNone(resolved) + # we stop evaluation after finding object + self.assertEqual(has_tuple, True) + + def test_resolve_type_iter_i(self) -> None: + a0 = range(3, 7) + resolved, has_tuple, values = prepare_iter_for_array(a0) + # a copy is not made + self.assertEqual(id(a0), id(values)) + self.assertIsNone(resolved) + + def test_resolve_type_iter_j(self) -> None: + # this case was found through hypothesis + a0 = [0.0, 36_028_797_018_963_969] + resolved, has_tuple, values = prepare_iter_for_array(a0) + self.assertIsNotNone(resolved) + + a1 = [0.0, 9_007_199_256_349_109] + resolved, has_tuple, values = prepare_iter_for_array(a1) + self.assertIsNotNone(resolved) + + a2 = [0.0, 9_007_199_256_349_108] + resolved, has_tuple, values = prepare_iter_for_array(a2) + self.assertIsNone(resolved) + + def test_resolve_type_iter_k(self) -> None: + resolved, has_tuple, values = prepare_iter_for_array((x for x in ())) #type: ignore + self.assertIsNone(resolved) + self.assertEqual(len(values), 0) + self.assertEqual(has_tuple, False) + + def test_resolve_type_iter_l(self) -> None: + self.assertEqual((None, False, []), prepare_iter_for_array([], True)) + self.assertEqual((None, False, ()), prepare_iter_for_array((), True)) + self.assertEqual((None, False, {}), prepare_iter_for_array({}, True)) + self.assertEqual((None, False, set()), prepare_iter_for_array(set(), True)) + + if __name__ == '__main__': unittest.main() +