arraykit 1.11.0__tar.gz → 1.12.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. {arraykit-1.11.0/arraykit.egg-info → arraykit-1.12.0}/PKG-INFO +7 -1
  2. {arraykit-1.11.0 → arraykit-1.12.0}/README.rst +6 -0
  3. arraykit-1.12.0/VERSION +2 -0
  4. {arraykit-1.11.0 → arraykit-1.12.0/arraykit.egg-info}/PKG-INFO +7 -1
  5. {arraykit-1.11.0 → arraykit-1.12.0}/arraykit.egg-info/SOURCES.txt +2 -0
  6. {arraykit-1.11.0 → arraykit-1.12.0}/src/__init__.py +2 -0
  7. {arraykit-1.11.0 → arraykit-1.12.0}/src/__init__.pyi +4 -0
  8. {arraykit-1.11.0 → arraykit-1.12.0}/src/_arraykit.c +8 -0
  9. {arraykit-1.11.0 → arraykit-1.12.0}/src/methods.c +288 -0
  10. {arraykit-1.11.0 → arraykit-1.12.0}/src/methods.h +6 -0
  11. arraykit-1.12.0/test/test_map_object.py +153 -0
  12. arraykit-1.12.0/test/test_prepare_iter_for_array.py +190 -0
  13. arraykit-1.11.0/VERSION +0 -2
  14. {arraykit-1.11.0 → arraykit-1.12.0}/LICENSE.txt +0 -0
  15. {arraykit-1.11.0 → arraykit-1.12.0}/MANIFEST.in +0 -0
  16. {arraykit-1.11.0 → arraykit-1.12.0}/arraykit.egg-info/dependency_links.txt +0 -0
  17. {arraykit-1.11.0 → arraykit-1.12.0}/arraykit.egg-info/requires.txt +0 -0
  18. {arraykit-1.11.0 → arraykit-1.12.0}/arraykit.egg-info/top_level.txt +0 -0
  19. {arraykit-1.11.0 → arraykit-1.12.0}/pyproject.toml +0 -0
  20. {arraykit-1.11.0 → arraykit-1.12.0}/setup.cfg +0 -0
  21. {arraykit-1.11.0 → arraykit-1.12.0}/setup.py +0 -0
  22. {arraykit-1.11.0 → arraykit-1.12.0}/src/array_go.c +0 -0
  23. {arraykit-1.11.0 → arraykit-1.12.0}/src/array_go.h +0 -0
  24. {arraykit-1.11.0 → arraykit-1.12.0}/src/array_to_tuple.c +0 -0
  25. {arraykit-1.11.0 → arraykit-1.12.0}/src/array_to_tuple.h +0 -0
  26. {arraykit-1.11.0 → arraykit-1.12.0}/src/auto_map.c +0 -0
  27. {arraykit-1.11.0 → arraykit-1.12.0}/src/auto_map.h +0 -0
  28. {arraykit-1.11.0 → arraykit-1.12.0}/src/block_index.c +0 -0
  29. {arraykit-1.11.0 → arraykit-1.12.0}/src/block_index.h +0 -0
  30. {arraykit-1.11.0 → arraykit-1.12.0}/src/delimited_to_arrays.c +0 -0
  31. {arraykit-1.11.0 → arraykit-1.12.0}/src/delimited_to_arrays.h +0 -0
  32. {arraykit-1.11.0 → arraykit-1.12.0}/src/py.typed +0 -0
  33. {arraykit-1.11.0 → arraykit-1.12.0}/src/tri_map.c +0 -0
  34. {arraykit-1.11.0 → arraykit-1.12.0}/src/tri_map.h +0 -0
  35. {arraykit-1.11.0 → arraykit-1.12.0}/src/utilities.h +0 -0
  36. {arraykit-1.11.0 → arraykit-1.12.0}/test/test_array_go.py +0 -0
  37. {arraykit-1.11.0 → arraykit-1.12.0}/test/test_astype_array.py +0 -0
  38. {arraykit-1.11.0 → arraykit-1.12.0}/test/test_auto_map.py +0 -0
  39. {arraykit-1.11.0 → arraykit-1.12.0}/test/test_auto_map_property.py +0 -0
  40. {arraykit-1.11.0 → arraykit-1.12.0}/test/test_block_index.py +0 -0
  41. {arraykit-1.11.0 → arraykit-1.12.0}/test/test_delimited_to_arrays.py +0 -0
  42. {arraykit-1.11.0 → arraykit-1.12.0}/test/test_delimited_to_arrays_integration.py +0 -0
  43. {arraykit-1.11.0 → arraykit-1.12.0}/test/test_delimited_to_arrays_property.py +0 -0
  44. {arraykit-1.11.0 → arraykit-1.12.0}/test/test_factorize.py +0 -0
  45. {arraykit-1.11.0 → arraykit-1.12.0}/test/test_fill_directional.py +0 -0
  46. {arraykit-1.11.0 → arraykit-1.12.0}/test/test_group_ordering.py +0 -0
  47. {arraykit-1.11.0 → arraykit-1.12.0}/test/test_group_reduce.py +0 -0
  48. {arraykit-1.11.0 → arraykit-1.12.0}/test/test_nonzero_1d.py +0 -0
  49. {arraykit-1.11.0 → arraykit-1.12.0}/test/test_nonzero_1d_property.py +0 -0
  50. {arraykit-1.11.0 → arraykit-1.12.0}/test/test_objectable.py +0 -0
  51. {arraykit-1.11.0 → arraykit-1.12.0}/test/test_pyi.py +0 -0
  52. {arraykit-1.11.0 → arraykit-1.12.0}/test/test_split_after_count.py +0 -0
  53. {arraykit-1.11.0 → arraykit-1.12.0}/test/test_transition_slices_from_group.py +0 -0
  54. {arraykit-1.11.0 → arraykit-1.12.0}/test/test_tri_map.py +0 -0
  55. {arraykit-1.11.0 → arraykit-1.12.0}/test/test_type_discovery.py +0 -0
  56. {arraykit-1.11.0 → arraykit-1.12.0}/test/test_util.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: arraykit
3
- Version: 1.11.0
3
+ Version: 1.12.0
4
4
  Summary: Array utilities for StaticFrame
5
5
  Author: Christopher Ariza, Brandt Bucher, Charles Burkland
6
6
  License: MIT
@@ -64,6 +64,12 @@ ArrayKit requires the following:
64
64
  What is New in ArrayKit
65
65
  -------------------------
66
66
 
67
+ 1.12.0
68
+ ............
69
+
70
+ Added ``map_object()`` and ``prepare_iter_for_array()``.
71
+
72
+
67
73
  1.11.0
68
74
  ............
69
75
 
@@ -35,6 +35,12 @@ ArrayKit requires the following:
35
35
  What is New in ArrayKit
36
36
  -------------------------
37
37
 
38
+ 1.12.0
39
+ ............
40
+
41
+ Added ``map_object()`` and ``prepare_iter_for_array()``.
42
+
43
+
38
44
  1.11.0
39
45
  ............
40
46
 
@@ -0,0 +1,2 @@
1
+ 1.12.0
2
+
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: arraykit
3
- Version: 1.11.0
3
+ Version: 1.12.0
4
4
  Summary: Array utilities for StaticFrame
5
5
  Author: Christopher Ariza, Brandt Bucher, Charles Burkland
6
6
  License: MIT
@@ -64,6 +64,12 @@ ArrayKit requires the following:
64
64
  What is New in ArrayKit
65
65
  -------------------------
66
66
 
67
+ 1.12.0
68
+ ............
69
+
70
+ Added ``map_object()`` and ``prepare_iter_for_array()``.
71
+
72
+
67
73
  1.11.0
68
74
  ............
69
75
 
@@ -40,9 +40,11 @@ test/test_factorize.py
40
40
  test/test_fill_directional.py
41
41
  test/test_group_ordering.py
42
42
  test/test_group_reduce.py
43
+ test/test_map_object.py
43
44
  test/test_nonzero_1d.py
44
45
  test/test_nonzero_1d_property.py
45
46
  test/test_objectable.py
47
+ test/test_prepare_iter_for_array.py
46
48
  test/test_pyi.py
47
49
  test/test_split_after_count.py
48
50
  test/test_transition_slices_from_group.py
@@ -28,6 +28,8 @@ from ._arraykit import write_array_to_file as write_array_to_file
28
28
  from ._arraykit import factorize as factorize
29
29
  from ._arraykit import group_ordering as group_ordering
30
30
  from ._arraykit import group_reduce as group_reduce
31
+ from ._arraykit import map_object as map_object
32
+ from ._arraykit import prepare_iter_for_array as prepare_iter_for_array
31
33
  from ._arraykit import fill_directional as fill_directional
32
34
  from ._arraykit import count_iteration as count_iteration
33
35
  from ._arraykit import first_true_1d as first_true_1d
@@ -236,6 +236,10 @@ def group_ordering(
236
236
  def group_reduce(
237
237
  codes: np.ndarray, size: int, values: np.ndarray, op: str
238
238
  ) -> np.ndarray: ...
239
+ def map_object(array: np.ndarray, func: tp.Callable[[tp.Any], tp.Any]) -> np.ndarray: ...
240
+ def prepare_iter_for_array(
241
+ values: tp.Iterable[tp.Any], copy: bool = ...
242
+ ) -> tp.Tuple[tp.Optional[type], bool, tp.Sequence[tp.Any]]: ...
239
243
  def fill_directional(
240
244
  array: np.ndarray,
241
245
  target: np.ndarray,
@@ -82,6 +82,14 @@ static PyMethodDef arraykit_methods[] = {
82
82
  (PyCFunction)group_reduce,
83
83
  METH_VARARGS | METH_KEYWORDS,
84
84
  NULL},
85
+ {"map_object",
86
+ (PyCFunction)map_object,
87
+ METH_VARARGS | METH_KEYWORDS,
88
+ NULL},
89
+ {"prepare_iter_for_array",
90
+ (PyCFunction)prepare_iter_for_array,
91
+ METH_VARARGS | METH_KEYWORDS,
92
+ NULL},
85
93
  {"fill_directional",
86
94
  (PyCFunction)fill_directional,
87
95
  METH_VARARGS | METH_KEYWORDS,
@@ -1403,6 +1403,294 @@ group_reduce(PyObject *Py_UNUSED(m), PyObject *args, PyObject *kwargs)
1403
1403
  return out_arr;
1404
1404
  }
1405
1405
 
1406
+ //------------------------------------------------------------------------------
1407
+ // int magnitude beyond which a Python int is no longer losslessly coercible to float;
1408
+ // mirrors static_frame.core.util.INT_MAX_COERCIBLE_TO_FLOAT
1409
+ #define AK_INT_MAX_COERCIBLE_TO_FLOAT 1000000000000000LL
1410
+
1411
+ // running state of the prepare_iter_for_array dtype inference over a series of values
1412
+ typedef struct AK_InferState {
1413
+ int has_str;
1414
+ int has_non_str;
1415
+ int has_inexact;
1416
+ int has_big_int;
1417
+ int has_tuple; // a sized object was seen (tuple/list/array/...)
1418
+ int needs_object; // the resolved dtype must be object rather than auto-detected
1419
+ } AK_InferState;
1420
+
1421
+ // Classify one value per static_frame.core.util.prepare_iter_for_array's rules, updating
1422
+ // `s`. Once `s->needs_object` is set the caller can stop inspecting. `enum_type` is
1423
+ // enum.Enum (may be NULL to skip the Enum check).
1424
+ static inline void
1425
+ AK_infer_value(PyObject *v, PyObject *enum_type, AK_InferState *s)
1426
+ {
1427
+ PyTypeObject *vt = Py_TYPE(v);
1428
+ // exact str/bytes (Python or numpy scalar) -> string; subclasses fall through
1429
+ if (PyUnicode_CheckExact(v) || PyBytes_CheckExact(v)
1430
+ || PyArray_IsScalar(v, Unicode) || PyArray_IsScalar(v, String)) {
1431
+ s->has_str = 1;
1432
+ }
1433
+ // a sized object (tuple, list, array, SF container, str subclass) -> object
1434
+ else if ((vt->tp_as_sequence && vt->tp_as_sequence->sq_length)
1435
+ || (vt->tp_as_mapping && vt->tp_as_mapping->mp_length)) {
1436
+ s->has_tuple = 1;
1437
+ s->needs_object = 1;
1438
+ }
1439
+ else {
1440
+ s->has_non_str = 1;
1441
+ if (vt == &PyFloat_Type || vt == &PyComplex_Type) {
1442
+ s->has_inexact = 1;
1443
+ }
1444
+ else if (vt == &PyLong_Type) {
1445
+ int overflow = 0;
1446
+ long long lv = PyLong_AsLongLongAndOverflow(v, &overflow);
1447
+ if (overflow || llabs(lv) > AK_INT_MAX_COERCIBLE_TO_FLOAT) {
1448
+ s->has_big_int = 1;
1449
+ }
1450
+ }
1451
+ else if (PyArray_IsScalar(v, Generic)) {
1452
+ ; // any other numpy scalar: non-str, no inexact/big-int, not an Enum
1453
+ }
1454
+ else if (enum_type != NULL && PyObject_IsInstance(v, enum_type) == 1) {
1455
+ s->needs_object = 1;
1456
+ }
1457
+ }
1458
+ if ((s->has_str && s->has_non_str) || (s->has_big_int && s->has_inexact)) {
1459
+ s->needs_object = 1;
1460
+ }
1461
+ }
1462
+
1463
+ // Import enum.Enum for the Enum inference case; returns a new reference or NULL
1464
+ // (clearing the error), in which case the Enum check is skipped.
1465
+ static PyObject *
1466
+ AK_import_enum(void)
1467
+ {
1468
+ PyObject *enum_type = NULL;
1469
+ PyObject *enum_mod = PyImport_ImportModule("enum");
1470
+ if (enum_mod != NULL) {
1471
+ enum_type = PyObject_GetAttrString(enum_mod, "Enum");
1472
+ Py_DECREF(enum_mod);
1473
+ }
1474
+ if (enum_type == NULL) {
1475
+ PyErr_Clear();
1476
+ }
1477
+ return enum_type;
1478
+ }
1479
+
1480
+ static char *map_object_kwarg_names[] = {
1481
+ "array",
1482
+ "func",
1483
+ NULL
1484
+ };
1485
+
1486
+ // Apply a Python callable to each element of a 1D array (elements boxed as numpy scalars,
1487
+ // matching NumPy/Series iteration) and return a new 1D array, inferring the result dtype
1488
+ // with the same rules as static_frame.core.util.prepare_iter_for_array: the result is an
1489
+ // object array when the applied values mix strings and non-strings, include a sized object
1490
+ // (tuple/list/array), an Enum, or mix a large Python int with a Python float/complex;
1491
+ // otherwise NumPy auto-detects the dtype (e.g. str -> '<U', float -> float64). This fuses
1492
+ // the per-element apply, the type inspection, and the array build into one C pass.
1493
+ PyObject *
1494
+ map_object(PyObject *Py_UNUSED(m), PyObject *args, PyObject *kwargs)
1495
+ {
1496
+ PyArrayObject *array = NULL;
1497
+ PyObject *func = NULL;
1498
+ if (!PyArg_ParseTupleAndKeywords(args, kwargs,
1499
+ "O!O:map_object",
1500
+ map_object_kwarg_names,
1501
+ &PyArray_Type, &array,
1502
+ &func
1503
+ )) {
1504
+ return NULL;
1505
+ }
1506
+ if (PyArray_NDIM(array) != 1) {
1507
+ PyErr_SetString(PyExc_ValueError, "array must be 1-dimensional");
1508
+ return NULL;
1509
+ }
1510
+ if (!PyCallable_Check(func)) {
1511
+ PyErr_SetString(PyExc_TypeError, "func must be callable");
1512
+ return NULL;
1513
+ }
1514
+ npy_intp n = PyArray_SIZE(array);
1515
+ int is_object = PyArray_TYPE(array) == NPY_OBJECT;
1516
+
1517
+ PyObject *values = PyList_New(n); // collected results; owns references
1518
+ if (values == NULL) {
1519
+ return NULL;
1520
+ }
1521
+ // enum.Enum for the rare Enum-result case; on failure proceed without the check
1522
+ PyObject *enum_type = AK_import_enum();
1523
+ AK_InferState state = {0, 0, 0, 0, 0, 0};
1524
+
1525
+ // 1D: hoist the base pointer and element stride and walk a running pointer, rather
1526
+ // than recomputing PyArray_GETPTR1 each iteration. For a contiguous array the stride
1527
+ // is the itemsize (direct indexing into the flat buffer); a strided slice still works.
1528
+ char *p = (char*)PyArray_DATA(array);
1529
+ npy_intp stride = PyArray_STRIDES(array)[0];
1530
+
1531
+ for (npy_intp i = 0; i < n; i++, p += stride) {
1532
+ PyObject *elem;
1533
+ if (is_object) {
1534
+ elem = *(PyObject**)p;
1535
+ Py_INCREF(elem);
1536
+ }
1537
+ else {
1538
+ elem = PyArray_ToScalar(p, array);
1539
+ if (elem == NULL) {
1540
+ goto fail;
1541
+ }
1542
+ }
1543
+ PyObject *r = PyObject_CallOneArg(func, elem);
1544
+ Py_DECREF(elem);
1545
+ if (r == NULL) {
1546
+ goto fail;
1547
+ }
1548
+ PyList_SET_ITEM(values, i, r); // steals reference to r
1549
+ if (!state.needs_object) {
1550
+ AK_infer_value(r, enum_type, &state);
1551
+ }
1552
+ }
1553
+ Py_XDECREF(enum_type);
1554
+
1555
+ PyObject *result;
1556
+ if (state.needs_object) {
1557
+ // build an object array of the collected values
1558
+ result = PyArray_FROM_OTF(values, NPY_OBJECT, NPY_ARRAY_C_CONTIGUOUS);
1559
+ }
1560
+ else {
1561
+ // let NumPy auto-detect the dtype from the values (str -> '<U', float -> f8, ...)
1562
+ result = PyArray_FromAny(values, NULL, 1, 1, NPY_ARRAY_C_CONTIGUOUS, NULL);
1563
+ }
1564
+ Py_DECREF(values);
1565
+ if (result == NULL) {
1566
+ return NULL;
1567
+ }
1568
+ PyArray_CLEARFLAGS((PyArrayObject*)result, NPY_ARRAY_WRITEABLE);
1569
+ return result;
1570
+
1571
+ fail:
1572
+ Py_XDECREF(enum_type);
1573
+ Py_DECREF(values);
1574
+ return NULL;
1575
+ }
1576
+
1577
+ static char *prepare_iter_for_array_kwarg_names[] = {
1578
+ "values",
1579
+ "copy",
1580
+ NULL
1581
+ };
1582
+
1583
+ // Infer a dtype specifier for the elements of an iterable, matching
1584
+ // static_frame.core.util.prepare_iter_for_array: return ``(resolved, has_tuple, values)``
1585
+ // where `resolved` is None (let NumPy auto-detect) or the ``object`` type, `has_tuple`
1586
+ // marks that a sized object was seen, and `values` is a newly materialized list when
1587
+ // `copy` is true (e.g. a generator/dict/set) else the original iterable. The caller
1588
+ // decides `copy` (via is_gen_copy_values), keeping the SF-specific type policy out of C.
1589
+ PyObject *
1590
+ prepare_iter_for_array(PyObject *Py_UNUSED(m), PyObject *args, PyObject *kwargs)
1591
+ {
1592
+ PyObject *values = NULL;
1593
+ int copy = 0;
1594
+ if (!PyArg_ParseTupleAndKeywords(args, kwargs,
1595
+ "O|p:prepare_iter_for_array",
1596
+ prepare_iter_for_array_kwarg_names,
1597
+ &values,
1598
+ &copy
1599
+ )) {
1600
+ return NULL;
1601
+ }
1602
+ PyObject *enum_type = AK_import_enum();
1603
+ AK_InferState state = {0, 0, 0, 0, 0, 0};
1604
+ PyObject *values_out = NULL; // new list when copy, else a new ref to the original
1605
+
1606
+ if (copy) {
1607
+ // materialize into a list while inspecting (generator/dict/set input). Pre-size
1608
+ // the list from a length hint when one is available (sets, dicts, sized
1609
+ // iterators) to avoid repeated reallocation; fall back to append growth for a
1610
+ // bare generator (hint 0), and guard against an inexact hint over/under-shooting.
1611
+ Py_ssize_t hint = PyObject_LengthHint(values, 0);
1612
+ if (hint < 0) {
1613
+ goto fail;
1614
+ }
1615
+ values_out = PyList_New(hint);
1616
+ if (values_out == NULL) {
1617
+ goto fail;
1618
+ }
1619
+ PyObject *iter = PyObject_GetIter(values);
1620
+ if (iter == NULL) {
1621
+ goto fail;
1622
+ }
1623
+ Py_ssize_t i = 0;
1624
+ PyObject *item;
1625
+ while ((item = PyIter_Next(iter)) != NULL) {
1626
+ if (!state.needs_object) {
1627
+ AK_infer_value(item, enum_type, &state);
1628
+ }
1629
+ if (i < hint) {
1630
+ PyList_SET_ITEM(values_out, i, item); // steals reference
1631
+ }
1632
+ else { // hint underestimated the length
1633
+ int rc = PyList_Append(values_out, item);
1634
+ Py_DECREF(item);
1635
+ if (rc != 0) {
1636
+ Py_DECREF(iter);
1637
+ goto fail;
1638
+ }
1639
+ }
1640
+ i++;
1641
+ }
1642
+ Py_DECREF(iter);
1643
+ if (PyErr_Occurred()) {
1644
+ goto fail;
1645
+ }
1646
+ if (i < hint) { // hint overestimated: drop the trailing (NULL) slots
1647
+ Py_SET_SIZE(values_out, i);
1648
+ }
1649
+ }
1650
+ else {
1651
+ // inspect only; direct indexing for list/tuple, else the iterator protocol
1652
+ Py_INCREF(values);
1653
+ values_out = values;
1654
+ if (PyList_CheckExact(values) || PyTuple_CheckExact(values)) {
1655
+ Py_ssize_t sz = PySequence_Fast_GET_SIZE(values);
1656
+ for (Py_ssize_t i = 0; i < sz && !state.needs_object; i++) {
1657
+ AK_infer_value(PySequence_Fast_GET_ITEM(values, i), enum_type, &state);
1658
+ }
1659
+ }
1660
+ else {
1661
+ PyObject *iter = PyObject_GetIter(values);
1662
+ if (iter == NULL) {
1663
+ goto fail;
1664
+ }
1665
+ PyObject *item;
1666
+ while (!state.needs_object && (item = PyIter_Next(iter)) != NULL) {
1667
+ AK_infer_value(item, enum_type, &state);
1668
+ Py_DECREF(item);
1669
+ }
1670
+ Py_DECREF(iter);
1671
+ if (PyErr_Occurred()) {
1672
+ goto fail;
1673
+ }
1674
+ }
1675
+ }
1676
+ Py_XDECREF(enum_type);
1677
+
1678
+ PyObject *resolved = state.needs_object
1679
+ ? (PyObject*)&PyBaseObject_Type // the ``object`` builtin -> object dtype
1680
+ : Py_None;
1681
+ PyObject *has_tuple = state.has_tuple ? Py_True : Py_False;
1682
+ PyObject *result = PyTuple_Pack(3, resolved, has_tuple, values_out);
1683
+ Py_DECREF(values_out);
1684
+ return result;
1685
+
1686
+ fail:
1687
+ Py_XDECREF(enum_type);
1688
+ Py_XDECREF(values_out);
1689
+ return NULL;
1690
+ }
1691
+
1692
+ //------------------------------------------------------------------------------
1693
+
1406
1694
  // Fill one strided lane in place: walk positions in the fill direction, carrying
1407
1695
  // the most recent non-target value into each target position (subject to `limit`
1408
1696
  // consecutive fills per run). `elem_base`/`elem_stride` address elements in bytes;
@@ -75,6 +75,12 @@ group_ordering(PyObject *Py_UNUSED(m), PyObject *args, PyObject *kwargs);
75
75
  PyObject *
76
76
  group_reduce(PyObject *Py_UNUSED(m), PyObject *args, PyObject *kwargs);
77
77
 
78
+ PyObject *
79
+ map_object(PyObject *Py_UNUSED(m), PyObject *args, PyObject *kwargs);
80
+
81
+ PyObject *
82
+ prepare_iter_for_array(PyObject *Py_UNUSED(m), PyObject *args, PyObject *kwargs);
83
+
78
84
  PyObject *
79
85
  fill_directional(PyObject *Py_UNUSED(m), PyObject *args, PyObject *kwargs);
80
86
 
@@ -0,0 +1,153 @@
1
+ import unittest
2
+ from enum import Enum
3
+
4
+ import numpy as np
5
+ from arraykit import map_object
6
+
7
+
8
+ class Color(Enum):
9
+ R = 1
10
+ G = 2
11
+
12
+
13
+ # reference: prepare_iter_for_array's inference + build, as static_frame applies it
14
+ _INEXACT = (float, complex, np.inexact)
15
+ _BIG = 1_000_000_000_000_000
16
+
17
+
18
+ def _reference(arr, func):
19
+ vals = [func(v) for v in arr]
20
+ resolved = None
21
+ has_str = has_non = has_inx = has_big = False
22
+ for v in vals:
23
+ vt = v.__class__
24
+ if vt is str or vt is np.str_ or vt is bytes or vt is np.bytes_:
25
+ has_str = True
26
+ elif hasattr(v, '__len__') or isinstance(v, Enum):
27
+ resolved = object
28
+ break
29
+ else:
30
+ has_non = True
31
+ if vt in _INEXACT:
32
+ has_inx = True
33
+ elif vt is int and abs(v) > _BIG:
34
+ has_big = True
35
+ if (has_str and has_non) or (has_big and has_inx):
36
+ resolved = object
37
+ break
38
+ return np.array(vals) if resolved is None else np.array(vals, dtype=object)
39
+
40
+
41
+ class TestUnit(unittest.TestCase):
42
+ def _check(self, arr, func):
43
+ post = map_object(arr, func)
44
+ exp = _reference(arr, func)
45
+ self.assertEqual(post.dtype, exp.dtype, (arr.dtype, exp.dtype))
46
+ self.assertTrue(np.array_equal(post, exp))
47
+ self.assertFalse(post.flags.writeable)
48
+ return post
49
+
50
+ def test_map_object_str_from_float(self) -> None:
51
+ post = self._check(np.array([1.5, 2.25, 3.0]), lambda x: str(x))
52
+ self.assertEqual(post.dtype.kind, 'U')
53
+
54
+ def test_map_object_str_from_bool(self) -> None:
55
+ post = self._check(np.array([True, False, True]), lambda x: str(x))
56
+ self.assertEqual(post.tolist(), ['True', 'False', 'True'])
57
+ self.assertEqual(post.dtype.kind, 'U')
58
+
59
+ def test_map_object_str_from_int(self) -> None:
60
+ self._check(np.array([1, 2, 3], dtype=np.int64), lambda x: str(x))
61
+
62
+ def test_map_object_native_float(self) -> None:
63
+ post = self._check(np.array([1.5, 2.5]), lambda x: float(x) * 2)
64
+ self.assertEqual(post.dtype, np.dtype(np.float64))
65
+
66
+ def test_map_object_native_int(self) -> None:
67
+ # python-int results auto-detect to the platform default int (int32 on Windows)
68
+ post = self._check(np.array([1, 2, 3]), lambda x: int(x) + 1)
69
+ self.assertEqual(post.dtype, np.dtype(np.int_))
70
+
71
+ def test_map_object_tuple_result(self) -> None:
72
+ post = self._check(np.array([1, 2]), lambda x: (int(x), int(x)))
73
+ self.assertEqual(post.dtype, np.dtype(object))
74
+
75
+ def test_map_object_list_result(self) -> None:
76
+ post = self._check(np.array([1, 2]), lambda x: [int(x)])
77
+ self.assertEqual(post.dtype, np.dtype(object))
78
+
79
+ def test_map_object_mixed_str_nonstr(self) -> None:
80
+ post = self._check(np.array([1, 2, 3]), lambda x: str(x) if x > 1 else int(x))
81
+ self.assertEqual(post.dtype, np.dtype(object))
82
+
83
+ def test_map_object_python_float(self) -> None:
84
+ self._check(np.array([1, 2, 3]), lambda x: 1.5)
85
+
86
+ def test_map_object_bigint_and_inexact(self) -> None:
87
+ # a large python int mixed with a python float -> object
88
+ post = self._check(np.array([1, 2]), lambda x: 10**18 if x == 1 else 1.5)
89
+ self.assertEqual(post.dtype, np.dtype(object))
90
+
91
+ def test_map_object_bigint_only(self) -> None:
92
+ # big ints alone (no inexact) do not force object
93
+ post = self._check(np.array([1, 2]), lambda x: 10**18)
94
+ self.assertNotEqual(post.dtype, np.dtype(object))
95
+
96
+ def test_map_object_enum_result(self) -> None:
97
+ post = self._check(np.array([1, 2]), lambda x: Color.R)
98
+ self.assertEqual(post.dtype, np.dtype(object))
99
+
100
+ def test_map_object_object_input(self) -> None:
101
+ arr = np.array(['a', 'bb', 'ccc'], dtype=object)
102
+ post = self._check(arr, lambda x: len(x))
103
+ self.assertEqual(post.tolist(), [1, 2, 3])
104
+
105
+ def test_map_object_receives_numpy_scalar(self) -> None:
106
+ # elements are boxed as numpy scalars, matching Series/array iteration
107
+ seen = []
108
+ map_object(np.array([1.5, 2.5]), lambda x: seen.append(type(x)) or x)
109
+ self.assertTrue(all(t is np.float64 for t in seen))
110
+
111
+ def test_map_object_str_subclass_is_object(self) -> None:
112
+ # a str subclass is not an exact str -> sized object -> object array
113
+ class S(str):
114
+ pass
115
+
116
+ post = map_object(np.array([1, 2]), lambda x: S(str(x)))
117
+ self.assertEqual(post.dtype, np.dtype(object))
118
+
119
+ def test_map_object_empty(self) -> None:
120
+ post = self._check(np.array([], dtype=np.float64), lambda x: str(x))
121
+ self.assertEqual(len(post), 0)
122
+
123
+ def test_map_object_strided_non_contiguous(self) -> None:
124
+ # a strided slice (non-contiguous) must be walked correctly by the running pointer
125
+ base = np.array([1.0, 99.0, 2.0, 99.0, 3.0])
126
+ strided = base[::2]
127
+ self.assertFalse(strided.flags['C_CONTIGUOUS'])
128
+ post = self._check(strided, lambda x: str(x))
129
+ self.assertEqual(post.tolist(), ['1.0', '2.0', '3.0'])
130
+
131
+ def test_map_object_strided_object(self) -> None:
132
+ arr = np.array(['a', 'X', 'bb', 'X', 'ccc'], dtype=object)[::2]
133
+ post = self._check(arr, lambda x: len(x))
134
+ self.assertEqual(post.tolist(), [1, 2, 3])
135
+
136
+ def test_map_object_propagates_exception(self) -> None:
137
+ def bad(x):
138
+ raise ValueError('boom')
139
+
140
+ with self.assertRaises(ValueError):
141
+ map_object(np.array([1, 2]), bad)
142
+
143
+ def test_map_object_errors(self) -> None:
144
+ with self.assertRaises(ValueError): # 2d
145
+ map_object(np.array([[1, 2]]), lambda x: x)
146
+ with self.assertRaises(TypeError): # not callable
147
+ map_object(np.array([1, 2]), 3)
148
+ with self.assertRaises(TypeError): # not an array
149
+ map_object([1, 2], lambda x: x)
150
+
151
+
152
+ if __name__ == '__main__':
153
+ unittest.main()
@@ -0,0 +1,190 @@
1
+ import unittest
2
+ from enum import Enum
3
+
4
+ import numpy as np
5
+ from arraykit import prepare_iter_for_array
6
+
7
+
8
+ class Color(Enum):
9
+ R = 1
10
+ G = 2
11
+
12
+
13
+ class _Hinted:
14
+ """An iterable exposing a (possibly inexact) __length_hint__."""
15
+
16
+ def __init__(self, items, hint):
17
+ self._items = list(items)
18
+ self._hint = hint
19
+
20
+ def __iter__(self):
21
+ return iter(self._items)
22
+
23
+ def __length_hint__(self):
24
+ return self._hint
25
+
26
+
27
+ # faithful reference: static_frame.core.util.prepare_iter_for_array, given a precomputed
28
+ # copy flag (SF's is_gen_copy_values decision lives in the SF wrapper)
29
+ _INEXACT = (float, complex, np.inexact)
30
+ _BIG = 1_000_000_000_000_000
31
+
32
+
33
+ def _reference(values, copy):
34
+ if copy:
35
+ vpost = []
36
+ resolved = None
37
+ has_tuple = False
38
+ has_str = has_non = has_inx = has_big = False
39
+ it = iter(values)
40
+ for v in it:
41
+ if copy:
42
+ vpost.append(v)
43
+ vt = v.__class__
44
+ if vt is str or vt is np.str_ or vt is bytes or vt is np.bytes_:
45
+ has_str = True
46
+ elif hasattr(v, '__len__'):
47
+ has_tuple = True
48
+ resolved = object
49
+ break
50
+ elif isinstance(v, Enum):
51
+ resolved = object
52
+ break
53
+ else:
54
+ has_non = True
55
+ if vt in _INEXACT:
56
+ has_inx = True
57
+ elif vt is int and abs(v) > _BIG:
58
+ has_big = True
59
+ if (has_str and has_non) or (has_big and has_inx):
60
+ resolved = object
61
+ break
62
+ if copy:
63
+ vpost.extend(it)
64
+ return resolved, has_tuple, vpost
65
+ return resolved, has_tuple, values
66
+
67
+
68
+ class TestUnit(unittest.TestCase):
69
+ def _check(self, make, copy):
70
+ # make() returns a fresh iterable so the two runs are independent
71
+ r_ak = prepare_iter_for_array(make(), copy)
72
+ r_ref = _reference(make(), copy)
73
+ self.assertIs(r_ak[0], r_ref[0]) # None or the object type, by identity
74
+ self.assertEqual(r_ak[1], r_ref[1]) # has_tuple
75
+ self.assertEqual(list(r_ak[2]), list(r_ref[2])) # values
76
+ return r_ak
77
+
78
+ def test_list_str(self) -> None:
79
+ r = self._check(lambda: ['a', 'b', 'c'], False)
80
+ self.assertIsNone(r[0])
81
+
82
+ def test_list_float(self) -> None:
83
+ self._check(lambda: [1.0, 2.0, 3.0], False)
84
+
85
+ def test_tuple_int(self) -> None:
86
+ self._check(lambda: (1, 2, 3), False)
87
+
88
+ def test_mixed_str_nonstr(self) -> None:
89
+ r = self._check(lambda: [1, 'a', 2.0], False)
90
+ self.assertIs(r[0], object)
91
+
92
+ def test_sized_object_has_tuple(self) -> None:
93
+ r = self._check(lambda: [1, (2, 3)], False)
94
+ self.assertIs(r[0], object)
95
+ self.assertTrue(r[1]) # has_tuple
96
+
97
+ def test_enum(self) -> None:
98
+ r = self._check(lambda: [Color.R, Color.G], False)
99
+ self.assertIs(r[0], object)
100
+ self.assertFalse(r[1]) # not has_tuple
101
+
102
+ def test_bigint_and_inexact(self) -> None:
103
+ r = self._check(lambda: [10**18, 1.5], False)
104
+ self.assertIs(r[0], object)
105
+
106
+ def test_bigint_only(self) -> None:
107
+ r = self._check(lambda: [10**18, 2], False)
108
+ self.assertIsNone(r[0])
109
+
110
+ def test_numpy_float_scalars(self) -> None:
111
+ r = self._check(lambda: [np.float64(1.5), np.float64(2.5)], False)
112
+ self.assertIsNone(r[0])
113
+
114
+ def test_bytes(self) -> None:
115
+ self._check(lambda: [b'x', b'y'], False)
116
+
117
+ def test_empty_list(self) -> None:
118
+ r = self._check(list, False)
119
+ self.assertIsNone(r[0])
120
+
121
+ def test_generator_copy(self) -> None:
122
+ # a generator is materialized when copy=True; the returned list is the values
123
+ r = self._check(lambda: (str(i) for i in range(4)), True)
124
+ self.assertEqual(list(r[2]), ['0', '1', '2', '3'])
125
+ self.assertIsInstance(r[2], list)
126
+
127
+ def test_generator_copy_mixed(self) -> None:
128
+ r = self._check(lambda: (i if i < 2 else str(i) for i in range(4)), True)
129
+ self.assertIs(r[0], object)
130
+ self.assertEqual(list(r[2]), [0, 1, '2', '3'])
131
+
132
+ def test_generator_empty_copy(self) -> None:
133
+ self._check(lambda: (x for x in []), True)
134
+
135
+ def test_set_copy(self) -> None:
136
+ # a set is materialized (order-independent check of contents); it has __len__,
137
+ # so the list is pre-sized
138
+ r = prepare_iter_for_array({1, 2, 3}, True)
139
+ self.assertIsNone(r[0])
140
+ self.assertEqual(sorted(r[2]), [1, 2, 3])
141
+
142
+ def test_dict_copy(self) -> None:
143
+ r = prepare_iter_for_array({'a': 1, 'b': 2}, True)
144
+ self.assertEqual(sorted(r[2]), ['a', 'b'])
145
+
146
+ def test_length_hint_exact(self) -> None:
147
+ r = self._check(lambda: _Hinted([1, 2, 3], 3), True)
148
+ self.assertEqual(list(r[2]), [1, 2, 3])
149
+
150
+ def test_length_hint_overestimate(self) -> None:
151
+ # a hint larger than the actual length -> trailing slots dropped
152
+ r = prepare_iter_for_array(_Hinted([1, 2, 3], 10), True)
153
+ self.assertEqual(list(r[2]), [1, 2, 3])
154
+ self.assertEqual(len(r[2]), 3)
155
+
156
+ def test_length_hint_underestimate(self) -> None:
157
+ # a hint smaller than the actual length -> remaining items appended
158
+ r = prepare_iter_for_array(_Hinted([1, 2, 3, 4, 5], 1), True)
159
+ self.assertEqual(list(r[2]), [1, 2, 3, 4, 5])
160
+
161
+ def test_length_hint_inference_preserved(self) -> None:
162
+ # inference still resolves object through the pre-sized path
163
+ r = prepare_iter_for_array(_Hinted([1, (2,)], 5), True)
164
+ self.assertIs(r[0], object)
165
+ self.assertTrue(r[1])
166
+ self.assertEqual(list(r[2]), [1, (2,)])
167
+
168
+ def test_no_copy_returns_original(self) -> None:
169
+ src = [1, 2, 3]
170
+ r = prepare_iter_for_array(src, False)
171
+ self.assertIs(r[2], src) # original object, not a copy
172
+
173
+ def test_early_stop_does_not_over_iterate(self) -> None:
174
+ # once object is resolved, inspection stops; a later error-raising element in a
175
+ # non-copy list is never inspected
176
+ r = prepare_iter_for_array([1, (2,), object()], False)
177
+ self.assertIs(r[0], object)
178
+ self.assertTrue(r[1])
179
+
180
+ def test_generator_copy_propagates_exception(self) -> None:
181
+ def gen():
182
+ yield 1
183
+ raise ValueError('boom')
184
+
185
+ with self.assertRaises(ValueError):
186
+ prepare_iter_for_array(gen(), True)
187
+
188
+
189
+ if __name__ == '__main__':
190
+ unittest.main()
arraykit-1.11.0/VERSION DELETED
@@ -1,2 +0,0 @@
1
- 1.11.0
2
-
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes