dataiter 1.3.1__tar.gz → 1.4__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 (28) hide show
  1. {dataiter-1.3.1 → dataiter-1.4}/.gitignore +1 -4
  2. {dataiter-1.3.1 → dataiter-1.4}/PKG-INFO +13 -6
  3. {dataiter-1.3.1 → dataiter-1.4}/README.md +9 -2
  4. {dataiter-1.3.1 → dataiter-1.4}/dataiter/__init__.py +32 -32
  5. {dataiter-1.3.1 → dataiter-1.4}/dataiter/aggregate.py +0 -1
  6. {dataiter-1.3.1 → dataiter-1.4}/dataiter/data_frame.py +51 -82
  7. {dataiter-1.3.1 → dataiter-1.4}/dataiter/io.py +1 -1
  8. {dataiter-1.3.1 → dataiter-1.4}/dataiter/list_of_dicts.py +1 -1
  9. {dataiter-1.3.1 → dataiter-1.4}/dataiter/regex.py +21 -45
  10. {dataiter-1.3.1 → dataiter-1.4}/dataiter/test/test_aggregate.py +1 -1
  11. {dataiter-1.3.1 → dataiter-1.4}/dataiter/test/test_data_frame.py +44 -43
  12. {dataiter-1.3.1 → dataiter-1.4}/dataiter/test/test_dt.py +1 -1
  13. {dataiter-1.3.1 → dataiter-1.4}/dataiter/test/test_geojson.py +4 -6
  14. {dataiter-1.3.1 → dataiter-1.4}/dataiter/test/test_list_of_dicts.py +16 -17
  15. {dataiter-1.3.1 → dataiter-1.4}/dataiter/test/test_util.py +8 -9
  16. {dataiter-1.3.1 → dataiter-1.4}/dataiter/test/test_vector.py +1 -1
  17. {dataiter-1.3.1 → dataiter-1.4}/dataiter/util.py +4 -5
  18. {dataiter-1.3.1 → dataiter-1.4}/dataiter/vector.py +16 -7
  19. {dataiter-1.3.1 → dataiter-1.4}/pyproject.toml +3 -3
  20. {dataiter-1.3.1 → dataiter-1.4}/AUTHORS.md +0 -0
  21. {dataiter-1.3.1 → dataiter-1.4}/COPYING +0 -0
  22. {dataiter-1.3.1 → dataiter-1.4}/dataiter/deco.py +0 -0
  23. {dataiter-1.3.1 → dataiter-1.4}/dataiter/dt.py +0 -0
  24. {dataiter-1.3.1 → dataiter-1.4}/dataiter/dtypes.py +0 -0
  25. {dataiter-1.3.1 → dataiter-1.4}/dataiter/geojson.py +0 -0
  26. {dataiter-1.3.1 → dataiter-1.4}/dataiter/test/__init__.py +0 -0
  27. {dataiter-1.3.1 → dataiter-1.4}/dataiter/test/test_io.py +0 -0
  28. {dataiter-1.3.1 → dataiter-1.4}/dataiter/test/test_regex.py +0 -0
@@ -1,6 +1,4 @@
1
- *.lprof
2
- *.prof
3
- .claude
1
+ .claude/settings.local.json
4
2
  .env
5
3
  .envrc
6
4
  .pytest_cache
@@ -9,7 +7,6 @@ benchmark-head.py
9
7
  benchmark-versions.csv
10
8
  benchmark-versions.ods
11
9
  build
12
- dataiter.egg-info
13
10
  dist
14
11
  doc/_build
15
12
  doc/comparison/comparison.html
@@ -1,21 +1,20 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.5
2
2
  Name: dataiter
3
- Version: 1.3.1
3
+ Version: 1.4
4
4
  Summary: Simple, light-weight data frames for Python
5
5
  Project-URL: Homepage, https://github.com/otsaloma/dataiter
6
6
  Author-email: Osmo Salomaa <otsaloma@iki.fi>
7
- License: MIT
7
+ License-Expression: MIT
8
8
  License-File: AUTHORS.md
9
9
  License-File: COPYING
10
10
  Requires-Python: >=3.11.0
11
11
  Requires-Dist: attd>=0.3
12
12
  Requires-Dist: numpy<3.0,>=2.4.0
13
- Requires-Dist: pyarrow>=2.0
13
+ Requires-Dist: pyarrow>=16.0
14
14
  Requires-Dist: wcwidth>=0.1
15
15
  Description-Content-Type: text/markdown
16
16
 
17
- Simple, Light-Weight Data Frames for Python
18
- ===========================================
17
+ # Simple, Light-Weight Data Frames for Python
19
18
 
20
19
  [![PyPI](https://img.shields.io/pypi/v/dataiter.svg)](https://pypi.org/project/dataiter)
21
20
  [![Downloads](https://pepy.tech/badge/dataiter/month)](https://pepy.tech/project/dataiter)
@@ -81,6 +80,14 @@ the differences and similarities in common operations.
81
80
 
82
81
  https://dataiter.readthedocs.io/en/stable/comparison.html
83
82
 
83
+ For coding agents and LLMs, each documentation page is also available as
84
+ Markdown by replacing `.html` with `.md` in the URL. There's also an
85
+ `llms.txt` index and complete documentation as `llms-full.txt`. See
86
+
87
+ - https://dataiter.readthedocs.io/en/stable/data-frame.md
88
+ - https://dataiter.readthedocs.io/en/stable/llms.txt
89
+ - https://dataiter.readthedocs.io/en/stable/llms-full.txt
90
+
84
91
  ## Development
85
92
 
86
93
  To install a virtualenv for development, use
@@ -1,5 +1,4 @@
1
- Simple, Light-Weight Data Frames for Python
2
- ===========================================
1
+ # Simple, Light-Weight Data Frames for Python
3
2
 
4
3
  [![PyPI](https://img.shields.io/pypi/v/dataiter.svg)](https://pypi.org/project/dataiter)
5
4
  [![Downloads](https://pepy.tech/badge/dataiter/month)](https://pepy.tech/project/dataiter)
@@ -65,6 +64,14 @@ the differences and similarities in common operations.
65
64
 
66
65
  https://dataiter.readthedocs.io/en/stable/comparison.html
67
66
 
67
+ For coding agents and LLMs, each documentation page is also available as
68
+ Markdown by replacing `.html` with `.md` in the URL. There's also an
69
+ `llms.txt` index and complete documentation as `llms-full.txt`. See
70
+
71
+ - https://dataiter.readthedocs.io/en/stable/data-frame.md
72
+ - https://dataiter.readthedocs.io/en/stable/llms.txt
73
+ - https://dataiter.readthedocs.io/en/stable/llms-full.txt
74
+
68
75
  ## Development
69
76
 
70
77
  To install a virtualenv for development, use
@@ -25,7 +25,7 @@ import numpy as np
25
25
 
26
26
  from dataiter import util
27
27
 
28
- __version__ = "1.3.1"
28
+ __version__ = "1.4"
29
29
 
30
30
  DEFAULT_PEEK_ELEMENTS = 10
31
31
  DEFAULT_PEEK_ITEMS = 3
@@ -87,34 +87,34 @@ globals().pop("np", None)
87
87
  globals().pop("numba", None)
88
88
  globals().pop("util", None)
89
89
 
90
- from dataiter import dtypes # noqa
91
- from dataiter.vector import Vector # noqa
92
- from dataiter.data_frame import DataFrame # noqa
93
- from dataiter.data_frame import DataFrameColumn # noqa
94
- from dataiter.geojson import GeoJSON # noqa
95
- from dataiter.list_of_dicts import ListOfDicts # noqa
96
- from dataiter import dt # noqa
97
- from dataiter import regex # noqa
98
-
99
- from dataiter.aggregate import all # noqa
100
- from dataiter.aggregate import any # noqa
101
- from dataiter.aggregate import count # noqa
102
- from dataiter.aggregate import count_unique # noqa
103
- from dataiter.aggregate import first # noqa
104
- from dataiter.aggregate import last # noqa
105
- from dataiter.aggregate import max # noqa
106
- from dataiter.aggregate import mean # noqa
107
- from dataiter.aggregate import median # noqa
108
- from dataiter.aggregate import min # noqa
109
- from dataiter.aggregate import mode # noqa
110
- from dataiter.aggregate import nth # noqa
111
- from dataiter.aggregate import quantile # noqa
112
- from dataiter.aggregate import std # noqa
113
- from dataiter.aggregate import sum # noqa
114
- from dataiter.aggregate import var # noqa
115
-
116
- from dataiter.io import read_csv # noqa
117
- from dataiter.io import read_geojson # noqa
118
- from dataiter.io import read_json # noqa
119
- from dataiter.io import read_npz # noqa
120
- from dataiter.io import read_parquet # noqa
90
+ from dataiter import dtypes
91
+ from dataiter.vector import Vector
92
+ from dataiter.data_frame import DataFrame
93
+ from dataiter.data_frame import DataFrameColumn
94
+ from dataiter.geojson import GeoJSON
95
+ from dataiter.list_of_dicts import ListOfDicts
96
+ from dataiter import dt
97
+ from dataiter import regex
98
+
99
+ from dataiter.aggregate import all
100
+ from dataiter.aggregate import any
101
+ from dataiter.aggregate import count
102
+ from dataiter.aggregate import count_unique
103
+ from dataiter.aggregate import first
104
+ from dataiter.aggregate import last
105
+ from dataiter.aggregate import max
106
+ from dataiter.aggregate import mean
107
+ from dataiter.aggregate import median
108
+ from dataiter.aggregate import min
109
+ from dataiter.aggregate import mode
110
+ from dataiter.aggregate import nth
111
+ from dataiter.aggregate import quantile
112
+ from dataiter.aggregate import std
113
+ from dataiter.aggregate import sum
114
+ from dataiter.aggregate import var
115
+
116
+ from dataiter.io import read_csv
117
+ from dataiter.io import read_geojson
118
+ from dataiter.io import read_json
119
+ from dataiter.io import read_npz
120
+ from dataiter.io import read_parquet
@@ -242,7 +242,6 @@ def is_na_numba(x):
242
242
  na[i] = is_na_item_numba(x[i])
243
243
  return na
244
244
 
245
- @composite
246
245
  def last(x, *, drop_na=False):
247
246
  """
248
247
  Return the last element of `x`.
@@ -22,7 +22,6 @@
22
22
 
23
23
  import codecs
24
24
  import dataiter
25
- import functools
26
25
  import itertools
27
26
  import json
28
27
  import numpy as np
@@ -103,10 +102,6 @@ class DataFrame(dict):
103
102
  # List of names that are actual attributes, not columns
104
103
  ATTRIBUTES = ["colnames", "_group_colnames"]
105
104
 
106
- # Use dummy attributes corresponding to dictionary keys so that
107
- # Tab completion of column names at a Python shell would work.
108
- COLUMN_PLACEHOLDER = type("COLUMN_PLACEHOLDER", (), {})
109
-
110
105
  def __init__(self, *args, **kwargs):
111
106
  """
112
107
  Return a new data frame.
@@ -122,9 +117,6 @@ class DataFrame(dict):
122
117
  value.nrow == nrow): continue
123
118
  column = DataFrameColumn(value, nrow=nrow)
124
119
  super().__setitem__(key, column)
125
- for key in self:
126
- if not self.__hasattr(key) and key.isidentifier():
127
- super().__setattr__(key, self.COLUMN_PLACEHOLDER)
128
120
  # Check that we have a uniform table.
129
121
  self._check_dimensions()
130
122
  self._group_colnames = ()
@@ -140,12 +132,13 @@ class DataFrame(dict):
140
132
  return self.__delitem__(name)
141
133
  return super().__delattr__(name)
142
134
 
143
- def __delitem__(self, key):
144
- # Note that this is not called for some methods,
145
- # at least pop, popitem and clear.
146
- if self[key] is self.COLUMN_PLACEHOLDER:
147
- super().__delattr__(key)
148
- return super().__delitem__(key)
135
+ def __dir__(self):
136
+ # Used for tab completion of column names at a Python shell.
137
+ names = super().__dir__()
138
+ return names + [x for x in self if
139
+ isinstance(x, str) and
140
+ x.isidentifier() and
141
+ x not in names]
149
142
 
150
143
  def __eq__(self, other):
151
144
  return (isinstance(other, DataFrame) and
@@ -159,27 +152,6 @@ class DataFrame(dict):
159
152
  return self.__getitem__(name)
160
153
  raise AttributeError(name)
161
154
 
162
- def __getattribute__(self, name):
163
- value = super().__getattribute__(name)
164
- if name == "COLUMN_PLACEHOLDER":
165
- return value
166
- if value is self.COLUMN_PLACEHOLDER and name in self:
167
- return self[name]
168
- return value
169
-
170
- def __hasattr(self, name):
171
- # Return True if attribute exists and is not a column.
172
- return hasattr(self, name) and not isinstance(getattr(self, name), DataFrameColumn)
173
-
174
- @classmethod
175
- def __is_builtin_attr(cls, name):
176
- return name in cls.__list_builtin_attrs()
177
-
178
- @classmethod
179
- @functools.lru_cache(None)
180
- def __list_builtin_attrs(cls):
181
- return set(dir(cls()))
182
-
183
155
  def __setattr__(self, name, value):
184
156
  if name in self.ATTRIBUTES:
185
157
  return super().__setattr__(name, value)
@@ -187,8 +159,6 @@ class DataFrame(dict):
187
159
 
188
160
  def __setitem__(self, key, value):
189
161
  value = self._reconcile_column(value)
190
- if not self.__hasattr(key) and key.isidentifier():
191
- super().__setattr__(key, self.COLUMN_PLACEHOLDER)
192
162
  return super().__setitem__(key, value)
193
163
 
194
164
  def __repr__(self):
@@ -220,13 +190,12 @@ class DataFrame(dict):
220
190
  group_colnames = self._group_colnames
221
191
  data = self.sort(**dict.fromkeys(group_colnames, 1))
222
192
  data._index_ = np.arange(data.nrow)
223
- stat = data.unique(*group_colnames).select("_index_", *group_colnames)
224
- indices = np.split(data._index_, stat._index_[1:])
193
+ starts = data._get_group_starts(*group_colnames)
194
+ stat = data.select("_index_", *group_colnames).slice(starts)
225
195
  group_aware = [getattr(x, "group_aware", False) for x in colname_function_pairs.values()]
226
196
  if any(group_aware):
227
- groups = Vector.fast(range(len(indices)), int)
228
- n = Vector.fast(map(len, indices), int)
229
- data._group_ = np.repeat(groups, n)
197
+ n = np.diff(starts, append=data.nrow)
198
+ data._group_ = np.repeat(np.arange(len(starts)), n)
230
199
  slices = None
231
200
  for colname, function in colname_function_pairs.items():
232
201
  if getattr(function, "group_aware", False):
@@ -246,6 +215,7 @@ class DataFrame(dict):
246
215
  # what special values to expect and thus we end up
247
216
  # needing to use the slow Vector.__init__.
248
217
  if slices is None:
218
+ indices = np.split(data._index_, starts[1:])
249
219
  slices = [data._view_rows(x) for x in indices]
250
220
  stat[colname] = [function(x) for x in slices]
251
221
  return stat.unselect("_index_", "_group_")
@@ -589,6 +559,16 @@ class DataFrame(dict):
589
559
  ba[item[0]] = ba.pop(item[1])
590
560
  return ab.rbind(ba).sort(_aid_=1, _bid_=1).unselect("_aid_", "_bid_")
591
561
 
562
+ def _get_group_starts(self, *colnames):
563
+ # Assumes data is sorted by colnames, i.e. groups are contiguous.
564
+ change = np.zeros(self.nrow, bool)
565
+ change[:1] = True
566
+ for colname in colnames:
567
+ column = self[colname]
568
+ na = column.is_na()
569
+ change[1:] |= (column[1:] != column[:-1]) & ~(na[1:] & na[:-1])
570
+ return np.flatnonzero(change)
571
+
592
572
  def _get_join_indices(self, other, by1, by2):
593
573
  other_ids = list(zip(*[other[x] for x in by2]))
594
574
  other_by_id = {other_ids[i]: i for i in range(other.nrow)}
@@ -688,7 +668,7 @@ class DataFrame(dict):
688
668
  vectorized form.
689
669
 
690
670
  >>> data = di.read_csv("data/listings-reviews.csv")
691
- >>> data.map(lambda x, i: (x.reviews[i], x.rating[i]))
671
+ >>> data.map(lambda x, i: (x.reviews[i], x.rating[i]))[:3]
692
672
  """
693
673
  return [function(self, i) for i in range(self.nrow)]
694
674
 
@@ -844,22 +824,6 @@ class DataFrame(dict):
844
824
  wide = wide.left_join(part, *ids)
845
825
  return wide
846
826
 
847
- def pop(self, key, *args, **kwargs):
848
- """"""
849
- value = super().pop(key, *args, **kwargs)
850
- if hasattr(self, key):
851
- if not self.__is_builtin_attr(key):
852
- super().__delattr__(key)
853
- return value
854
-
855
- def popitem(self):
856
- """"""
857
- key, value = super().popitem()
858
- if hasattr(self, key):
859
- if not self.__is_builtin_attr(key):
860
- super().__delattr__(key)
861
- return key, value
862
-
863
827
  def print_(self, *, max_rows=None, max_width=None, truncate_width=None):
864
828
  """
865
829
  Print data frame to ``sys.stdout``.
@@ -1066,7 +1030,7 @@ class DataFrame(dict):
1066
1030
  >>> listings.semi_join(reviews, "id")
1067
1031
  """
1068
1032
  by1, by2 = self._split_join_by(*by)
1069
- other = other.unique(*by2)
1033
+ other = other.drop_na(*by2).unique(*by2)
1070
1034
  found, src = self._get_join_indices(other, by1, by2)
1071
1035
  for colname, column in self.items():
1072
1036
  yield colname, column[found].copy()
@@ -1129,18 +1093,8 @@ class DataFrame(dict):
1129
1093
  column = self[colname]
1130
1094
  if column._is_string_fixed():
1131
1095
  column[column.is_na()] = "\uffff"
1132
- if dir > 0 and any((
1133
- column._is_string_fixed(),
1134
- column.is_boolean(),
1135
- column.is_bytes(),
1136
- column.is_datetime(),
1137
- column.is_float(),
1138
- column.is_integer(),
1139
- # Segfaults on NumPy < 2.2.1!
1140
- # https://github.com/numpy/numpy/issues/27984
1141
- column.is_string(),
1142
- column.is_timedelta(),
1143
- )): return column
1096
+ if dir > 0 and not column.is_object():
1097
+ return column
1144
1098
  if not column.is_number():
1145
1099
  column = column.rank(method="min")
1146
1100
  return column if dir > 0 else -column
@@ -1159,9 +1113,8 @@ class DataFrame(dict):
1159
1113
  data = self.select(*by)
1160
1114
  data._index_ = np.arange(data.nrow)
1161
1115
  data = data.sort(**dict.fromkeys(by, 1))
1162
- data._sorted_index_ = np.arange(data.nrow)
1163
- stat = data.unique(*by)
1164
- return np.split(data._index_, stat._sorted_index_[1:])
1116
+ starts = data._get_group_starts(*by)
1117
+ return np.split(data._index_, starts[1:])
1165
1118
 
1166
1119
  def _split_join_by(self, *by):
1167
1120
  by1 = [x if isinstance(x, str) else x[0] for x in by]
@@ -1277,28 +1230,44 @@ class DataFrame(dict):
1277
1230
  >>> data = di.read_csv("data/listings.csv")
1278
1231
  >>> data.unique("hood")
1279
1232
  """
1280
- # Strangely, standard Python is a lot faster here than np.unique
1281
- # with all the extra needed across columns of different type.
1282
- # We just need to avoid NaN and NaT for the 'in' checks to work.
1283
1233
  colnames = colnames or self.colnames
1284
1234
  columns = [self[x] for x in colnames]
1235
+ if all(x.is_number() or
1236
+ x.is_boolean() or
1237
+ x.is_datetime() or
1238
+ x.is_timedelta() for x in columns):
1239
+ # np.lexsort is stable, so the first row of each group
1240
+ # in sorted order is the first occurrence in self.
1241
+ order = np.lexsort(columns[::-1])
1242
+ starts = self.select(*colnames).slice(order)._get_group_starts(*colnames)
1243
+ keep = np.sort(order[starts])
1244
+ for colname, column in self.items():
1245
+ yield colname, column[keep].copy()
1246
+ return
1247
+ # For strings and objects, a Python set is faster than sorting.
1248
+ # We just need to avoid NaN and NaT for the 'in' checks to work.
1285
1249
  for i, column in enumerate(columns):
1286
1250
  if column.is_datetime():
1287
1251
  flag = np.nanmin(column) - np.timedelta64(1, "D")
1288
1252
  if np.isnat(flag):
1289
1253
  flag = np.datetime64("-001-01-01")
1290
1254
  columns[i] = column.replace_na(flag)
1255
+ if column.is_timedelta():
1256
+ unit = np.datetime_data(column.dtype)[0]
1257
+ flag = np.nanmin(column) - np.timedelta64(1, unit)
1258
+ if np.isnat(flag):
1259
+ flag = np.timedelta64(0, unit)
1260
+ columns[i] = column.replace_na(flag)
1291
1261
  if column.is_float():
1292
1262
  flag = np.nanmin(column) - 1
1293
1263
  if np.isnan(flag):
1294
1264
  flag = -1
1295
1265
  columns[i] = column.replace_na(flag)
1296
- rows = list(zip(*columns))
1297
1266
  seen = set()
1298
1267
  keep = []
1299
- for i in range(self.nrow):
1300
- if rows[i] not in seen:
1301
- seen.add(rows[i])
1268
+ for i, row in enumerate(zip(*columns)):
1269
+ if row not in seen:
1270
+ seen.add(row)
1302
1271
  keep.append(i)
1303
1272
  for colname, column in self.items():
1304
1273
  yield colname, column[keep].copy()
@@ -51,7 +51,7 @@ def read_npz(path, *, allow_pickle=True):
51
51
  return DataFrame.read_npz(path, allow_pickle=allow_pickle)
52
52
 
53
53
  def read_parquet(path, *, columns=[], dtypes={}):
54
- return DataFrame.read_parquet(path, columns=[], dtypes={})
54
+ return DataFrame.read_parquet(path, columns=columns, dtypes=dtypes)
55
55
 
56
56
  read_csv.__doc__ = util.format_alias_doc(read_csv, DataFrame.read_csv)
57
57
  read_geojson.__doc__ = util.format_alias_doc(read_geojson, GeoJSON.read)
@@ -479,7 +479,7 @@ class ListOfDicts(list):
479
479
  list of whatever `function` returns.
480
480
 
481
481
  >>> data = di.read_json("data/listings.json")
482
- >>> data.map(lambda x: (x.guests, x.price))
482
+ >>> data.map(lambda x: (x.guests, x.price))[:3]
483
483
  """
484
484
  new = list(map(function, self))
485
485
  coerce = all(isinstance(x, dict) for x in new)
@@ -28,12 +28,16 @@ from dataiter import util
28
28
  from dataiter import Vector
29
29
  from numpy.dtypes import StringDType
30
30
 
31
- def _prep(string, dtype, default):
31
+ def _apply(function, string, dtype=object, na_value=None):
32
+ if util.is_scalar(string):
33
+ return function(string)
32
34
  assert isinstance(string, np.ndarray)
33
35
  assert isinstance(string.dtype, StringDType)
34
- out = np.full_like(string, default, dtype)
36
+ out = np.full_like(string, na_value, dtype)
35
37
  na = string == dtypes.string.na_object
36
- return out, na
38
+ for i in np.flatnonzero(~na):
39
+ out[i] = function(string[i])
40
+ return Vector.fast(out, dtype)
37
41
 
38
42
  def findall(pattern, string, flags=0):
39
43
  """
@@ -44,12 +48,8 @@ def findall(pattern, string, flags=0):
44
48
  >>> x = di.Vector(["asdf", "1234"])
45
49
  >>> regex.findall(r"[a-z]", x)
46
50
  """
47
- if util.is_scalar(string):
48
- return re.findall(pattern, string, flags=flags)
49
- out, na = _prep(string, object, None)
50
- for i in np.flatnonzero(~na):
51
- out[i] = re.findall(pattern, string[i], flags=flags)
52
- return Vector.fast(out, object)
51
+ f = lambda x: re.findall(pattern, x, flags=flags)
52
+ return _apply(f, string)
53
53
 
54
54
  def fullmatch(pattern, string, flags=0):
55
55
  """
@@ -60,12 +60,8 @@ def fullmatch(pattern, string, flags=0):
60
60
  >>> x = di.Vector(["asdf", "1234"])
61
61
  >>> regex.fullmatch(r"[a-z]+", x)
62
62
  """
63
- if util.is_scalar(string):
64
- return re.fullmatch(pattern, string, flags=flags)
65
- out, na = _prep(string, object, None)
66
- for i in np.flatnonzero(~na):
67
- out[i] = re.fullmatch(pattern, string[i], flags=flags)
68
- return Vector.fast(out, object)
63
+ f = lambda x: re.fullmatch(pattern, x, flags=flags)
64
+ return _apply(f, string)
69
65
 
70
66
  def match(pattern, string, flags=0):
71
67
  """
@@ -76,12 +72,8 @@ def match(pattern, string, flags=0):
76
72
  >>> x = di.Vector(["asdf", "1234"])
77
73
  >>> regex.match(r"[a-z]", x)
78
74
  """
79
- if util.is_scalar(string):
80
- return re.match(pattern, string, flags=flags)
81
- out, na = _prep(string, object, None)
82
- for i in np.flatnonzero(~na):
83
- out[i] = re.match(pattern, string[i], flags=flags)
84
- return Vector.fast(out, object)
75
+ f = lambda x: re.match(pattern, x, flags=flags)
76
+ return _apply(f, string)
85
77
 
86
78
  def search(pattern, string, flags=0):
87
79
  """
@@ -92,12 +84,8 @@ def search(pattern, string, flags=0):
92
84
  >>> x = di.Vector(["asdf", "1234"])
93
85
  >>> regex.search(r"[a-z]", x)
94
86
  """
95
- if util.is_scalar(string):
96
- return re.search(pattern, string, flags=flags)
97
- out, na = _prep(string, object, None)
98
- for i in np.flatnonzero(~na):
99
- out[i] = re.search(pattern, string[i], flags=flags)
100
- return Vector.fast(out, object)
87
+ f = lambda x: re.search(pattern, x, flags=flags)
88
+ return _apply(f, string)
101
89
 
102
90
  def split(pattern, string, maxsplit=0, flags=0):
103
91
  """
@@ -108,12 +96,8 @@ def split(pattern, string, maxsplit=0, flags=0):
108
96
  >>> x = di.Vector(["one two three", "four"])
109
97
  >>> regex.split(r" +", x)
110
98
  """
111
- if util.is_scalar(string):
112
- return re.split(pattern, string, maxsplit=maxsplit, flags=flags)
113
- out, na = _prep(string, object, None)
114
- for i in np.flatnonzero(~na):
115
- out[i] = re.split(pattern, string[i], maxsplit=maxsplit, flags=flags)
116
- return Vector.fast(out, object)
99
+ f = lambda x: re.split(pattern, x, maxsplit=maxsplit, flags=flags)
100
+ return _apply(f, string)
117
101
 
118
102
  def sub(pattern, repl, string, count=0, flags=0):
119
103
  """
@@ -124,12 +108,8 @@ def sub(pattern, repl, string, count=0, flags=0):
124
108
  >>> x = di.Vector(["great", "fantastic"])
125
109
  >>> regex.sub(r"$", r"!", x)
126
110
  """
127
- if util.is_scalar(string):
128
- return re.sub(pattern, repl, string, count=count, flags=flags)
129
- out, na = _prep(string, dtypes.string, dtypes.string.na_object)
130
- for i in np.flatnonzero(~na):
131
- out[i] = re.sub(pattern, repl, string[i], count=count, flags=flags)
132
- return Vector.fast(out, str)
111
+ f = lambda x: re.sub(pattern, repl, x, count=count, flags=flags)
112
+ return _apply(f, string, dtypes.string, dtypes.string.na_object)
133
113
 
134
114
  def subn(pattern, repl, string, count=0, flags=0):
135
115
  """
@@ -140,9 +120,5 @@ def subn(pattern, repl, string, count=0, flags=0):
140
120
  >>> x = di.Vector(["great", "fantastic"])
141
121
  >>> regex.subn(r"$", r"!", x)
142
122
  """
143
- if util.is_scalar(string):
144
- return re.subn(pattern, repl, string, count=count, flags=flags)
145
- out, na = _prep(string, object, None)
146
- for i in np.flatnonzero(~na):
147
- out[i] = re.subn(pattern, repl, string[i], count=count, flags=flags)
148
- return Vector.fast(out, object)
123
+ f = lambda x: re.subn(pattern, repl, x, count=count, flags=flags)
124
+ return _apply(f, string)
@@ -57,7 +57,7 @@ D6 = D1 + datetime.timedelta(days=5)
57
57
  D7 = D1 + datetime.timedelta(days=6)
58
58
 
59
59
  NaN = np.nan
60
- NaT = np.datetime64("NaT")
60
+ NaT = np.datetime64("NaT", "D")
61
61
 
62
62
  EMPTY_VECTOR = Vector([], float)
63
63
  GROUPS = [1, 1, 2, 2, 3, 3, 4, 4, 5, 5]
@@ -23,7 +23,6 @@
23
23
  import dataiter as di
24
24
  import numpy as np
25
25
  import pytest
26
- import tempfile
27
26
 
28
27
  from dataiter import DataFrame
29
28
  from dataiter import DataFrameColumn
@@ -112,12 +111,14 @@ class TestDataFrame:
112
111
  data = test.data_frame("vehicles.csv")
113
112
  assert data.make is data["make"]
114
113
 
115
- def test___hasattr(self):
116
- data = DataFrame(a=[1, 2, 3])
117
- data._DataFrame__hasattr("left_join")
118
- data._DataFrame__hasattr("items")
119
- assert not data._DataFrame__hasattr("a")
120
- assert not data._DataFrame__hasattr("xxx")
114
+ def test___dir__(self):
115
+ data = DataFrame(a=[1, 2, 3], items=[1, 2, 3])
116
+ data["b c"] = 1
117
+ names = dir(data)
118
+ assert "a" in names
119
+ assert "b c" not in names
120
+ assert names.count("items") == 1
121
+ assert "left_join" in names
121
122
 
122
123
  def test___setattr__(self):
123
124
  data = test.data_frame("vehicles.csv")
@@ -482,7 +483,7 @@ class TestDataFrame:
482
483
  assert data.make.is_object()
483
484
  assert data.model.is_object()
484
485
 
485
- def test_read_csv_missing_blank(self):
486
+ def test_read_csv_missing_blank(self, tmp_path):
486
487
  text = """
487
488
  int,float,string,bool,date
488
489
  ,2.5,"test",TRUE,2025-01-01
@@ -491,14 +492,14 @@ int,float,string,bool,date
491
492
  1,2.5,"test",,2025-01-01
492
493
  1,2.5,"test",TRUE,
493
494
  """.strip()
494
- handle, path = tempfile.mkstemp(".csv")
495
+ path = str(tmp_path / "test.csv")
495
496
  Path(path).write_text(text, "utf-8")
496
497
  data = DataFrame.read_csv(path)
497
498
  for i, column in enumerate(data.columns):
498
499
  assert column.is_na().sum() == 1
499
500
  assert column.is_na()[i]
500
501
 
501
- def test_read_csv_missing_excel(self):
502
+ def test_read_csv_missing_excel(self, tmp_path):
502
503
  text = """
503
504
  int,float,string,bool,date
504
505
  #N/A,2.5,"test",TRUE,2025-01-01
@@ -507,14 +508,14 @@ int,float,string,bool,date
507
508
  1,2.5,"test",#N/A,2025-01-01
508
509
  1,2.5,"test",TRUE,#N/A
509
510
  """.strip()
510
- handle, path = tempfile.mkstemp(".csv")
511
+ path = str(tmp_path / "test.csv")
511
512
  Path(path).write_text(text, "utf-8")
512
513
  data = DataFrame.read_csv(path)
513
514
  for i, column in enumerate(data.columns):
514
515
  assert column.is_na().sum() == 1
515
516
  assert column.is_na()[i]
516
517
 
517
- def test_read_csv_missing_na(self):
518
+ def test_read_csv_missing_na(self, tmp_path):
518
519
  text = """
519
520
  int,float,string,bool,date
520
521
  NA,2.5,"test",TRUE,2025-01-01
@@ -523,14 +524,14 @@ NA,2.5,"test",TRUE,2025-01-01
523
524
  1,2.5,"test",NA,2025-01-01
524
525
  1,2.5,"test",TRUE,NA
525
526
  """.strip()
526
- handle, path = tempfile.mkstemp(".csv")
527
+ path = str(tmp_path / "test.csv")
527
528
  Path(path).write_text(text, "utf-8")
528
529
  data = DataFrame.read_csv(path)
529
530
  for i, column in enumerate(data.columns):
530
531
  assert column.is_na().sum() == 1
531
532
  assert column.is_na()[i]
532
533
 
533
- def test_read_csv_missing_null(self):
534
+ def test_read_csv_missing_null(self, tmp_path):
534
535
  text = """
535
536
  int,float,string,bool,date
536
537
  NULL,2.5,"test",TRUE,2025-01-01
@@ -539,7 +540,7 @@ NULL,2.5,"test",TRUE,2025-01-01
539
540
  1,2.5,"test",NULL,2025-01-01
540
541
  1,2.5,"test",TRUE,NULL
541
542
  """.strip()
542
- handle, path = tempfile.mkstemp(".csv")
543
+ path = str(tmp_path / "test.csv")
543
544
  Path(path).write_text(text, "utf-8")
544
545
  data = DataFrame.read_csv(path)
545
546
  for i, column in enumerate(data.columns):
@@ -568,37 +569,37 @@ NULL,2.5,"test",TRUE,2025-01-01
568
569
  DataFrame.read_json(test.get_data_path("vehicles.json"))
569
570
 
570
571
  @pytest.mark.filterwarnings(IGNORE_NPZ_PICKLE_WARNING)
571
- def test_read_npz(self):
572
+ def test_read_npz(self, tmp_path):
572
573
  orig = test.data_frame("vehicles.csv")
573
- handle, path = tempfile.mkstemp(".npz")
574
+ path = str(tmp_path / "test.npz")
574
575
  orig.write_npz(path)
575
576
  data = DataFrame.read_npz(path)
576
577
  assert data == orig
577
578
 
578
579
  @pytest.mark.filterwarnings(IGNORE_NPZ_PICKLE_WARNING)
579
- def test_read_npz_path(self):
580
+ def test_read_npz_path(self, tmp_path):
580
581
  orig = test.data_frame("vehicles.csv")
581
- handle, path = tempfile.mkstemp(".npz")
582
+ path = str(tmp_path / "test.npz")
582
583
  orig.write_npz(path)
583
584
  DataFrame.read_npz(Path(path))
584
585
 
585
- def test_read_parquet(self):
586
+ def test_read_parquet(self, tmp_path):
586
587
  orig = test.data_frame("vehicles.csv")
587
- handle, path = tempfile.mkstemp(".parquet")
588
+ path = str(tmp_path / "test.parquet")
588
589
  orig.write_parquet(path)
589
590
  data = DataFrame.read_parquet(path)
590
591
  assert data == orig
591
592
 
592
- def test_read_pickle(self):
593
+ def test_read_pickle(self, tmp_path):
593
594
  orig = test.data_frame("vehicles.csv")
594
- handle, path = tempfile.mkstemp(".pkl")
595
+ path = str(tmp_path / "test.pkl")
595
596
  orig.write_pickle(path)
596
597
  data = DataFrame.read_pickle(path)
597
598
  assert data == orig
598
599
 
599
- def test_read_pickle_path(self):
600
+ def test_read_pickle_path(self, tmp_path):
600
601
  orig = test.data_frame("vehicles.csv")
601
- handle, path = tempfile.mkstemp(".pkl")
602
+ path = str(tmp_path / "test.pkl")
602
603
  orig.write_pickle(path)
603
604
  DataFrame.read_pickle(Path(path))
604
605
 
@@ -785,59 +786,59 @@ NULL,2.5,"test",TRUE,2025-01-01
785
786
  assert np.all(data.make == "Talbot")
786
787
  assert np.all(data.test == 1)
787
788
 
788
- def test_write_csv(self):
789
+ def test_write_csv(self, tmp_path):
789
790
  orig = test.data_frame("vehicles.csv")
790
- handle, path = tempfile.mkstemp(".csv")
791
+ path = str(tmp_path / "test.csv")
791
792
  orig.write_csv(path)
792
793
  data = DataFrame.read_csv(path)
793
794
  assert data == orig
794
795
 
795
- def test_write_csv_path(self):
796
+ def test_write_csv_path(self, tmp_path):
796
797
  orig = test.data_frame("vehicles.csv")
797
- handle, path = tempfile.mkstemp(".csv")
798
+ path = str(tmp_path / "test.csv")
798
799
  orig.write_csv(Path(path))
799
800
 
800
- def test_write_json(self):
801
+ def test_write_json(self, tmp_path):
801
802
  orig = test.data_frame("downloads.json")
802
- handle, path = tempfile.mkstemp(".json")
803
+ path = str(tmp_path / "test.json")
803
804
  orig.write_json(path)
804
805
  data = DataFrame.read_json(path)
805
806
  assert data == orig
806
807
 
807
- def test_write_json_path(self):
808
+ def test_write_json_path(self, tmp_path):
808
809
  orig = test.data_frame("downloads.json")
809
- handle, path = tempfile.mkstemp(".json")
810
+ path = str(tmp_path / "test.json")
810
811
  orig.write_json(Path(path))
811
812
 
812
813
  @pytest.mark.filterwarnings(IGNORE_NPZ_PICKLE_WARNING)
813
- def test_write_npz(self):
814
+ def test_write_npz(self, tmp_path):
814
815
  orig = test.data_frame("vehicles.csv")
815
- handle, path = tempfile.mkstemp(".npz")
816
+ path = str(tmp_path / "test.npz")
816
817
  orig.write_npz(path)
817
818
  data = DataFrame.read_npz(path)
818
819
  assert data == orig
819
820
 
820
821
  @pytest.mark.filterwarnings(IGNORE_NPZ_PICKLE_WARNING)
821
- def test_write_npz_path(self):
822
+ def test_write_npz_path(self, tmp_path):
822
823
  orig = test.data_frame("vehicles.csv")
823
- handle, path = tempfile.mkstemp(".npz")
824
+ path = str(tmp_path / "test.npz")
824
825
  orig.write_npz(Path(path))
825
826
 
826
- def test_write_parquet(self):
827
+ def test_write_parquet(self, tmp_path):
827
828
  orig = test.data_frame("vehicles.csv")
828
- handle, path = tempfile.mkstemp(".parquet")
829
+ path = str(tmp_path / "test.parquet")
829
830
  orig.write_parquet(path)
830
831
  data = DataFrame.read_parquet(path)
831
832
  assert data == orig
832
833
 
833
- def test_write_pickle(self):
834
+ def test_write_pickle(self, tmp_path):
834
835
  orig = test.data_frame("vehicles.csv")
835
- handle, path = tempfile.mkstemp(".pkl")
836
+ path = str(tmp_path / "test.pkl")
836
837
  orig.write_pickle(path)
837
838
  data = DataFrame.read_pickle(path)
838
839
  assert data == orig
839
840
 
840
- def test_write_pickle_path(self):
841
+ def test_write_pickle_path(self, tmp_path):
841
842
  orig = test.data_frame("vehicles.csv")
842
- handle, path = tempfile.mkstemp(".pkl")
843
+ path = str(tmp_path / "test.pkl")
843
844
  orig.write_pickle(Path(path))
@@ -25,7 +25,7 @@ import numpy as np
25
25
  from dataiter import dt
26
26
  from dataiter import Vector
27
27
 
28
- NaT = np.datetime64("NaT")
28
+ NaT = np.datetime64("NaT", "D")
29
29
 
30
30
  class TestDT:
31
31
 
@@ -20,8 +20,6 @@
20
20
  # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
21
  # THE SOFTWARE.
22
22
 
23
- import tempfile
24
-
25
23
  from dataiter import GeoJSON
26
24
  from dataiter import test
27
25
  from pathlib import Path
@@ -74,15 +72,15 @@ class TestGeoJSON:
74
72
  assert data.head(0).to_string()
75
73
  assert data.head(5).to_string()
76
74
 
77
- def test_write(self):
75
+ def test_write(self, tmp_path):
78
76
  orig = test.geojson(self.path)
79
- handle, path = tempfile.mkstemp(".geojson")
77
+ path = str(tmp_path / "test.geojson")
80
78
  orig.write(path)
81
79
  data = GeoJSON.read(path)
82
80
  assert data == orig
83
81
  assert data.metadata == orig.metadata
84
82
 
85
- def test_write_path(self):
83
+ def test_write_path(self, tmp_path):
86
84
  orig = test.geojson(self.path)
87
- handle, path = tempfile.mkstemp(".geojson")
85
+ path = str(tmp_path / "test.geojson")
88
86
  orig.write(Path(path))
@@ -21,7 +21,6 @@
21
21
  # THE SOFTWARE.
22
22
 
23
23
  import datetime
24
- import tempfile
25
24
 
26
25
  from attd import AttributeDict
27
26
  from dataiter import ListOfDicts
@@ -393,16 +392,16 @@ class TestListOfDicts:
393
392
  assert isinstance(data[100].date, datetime.date)
394
393
  assert isinstance(data[100].downloads, int)
395
394
 
396
- def test_read_pickle(self):
395
+ def test_read_pickle(self, tmp_path):
397
396
  orig = test.list_of_dicts("downloads.json")
398
- handle, path = tempfile.mkstemp(".pkl")
397
+ path = str(tmp_path / "test.pkl")
399
398
  orig.write_pickle(path)
400
399
  data = ListOfDicts.read_pickle(path)
401
400
  assert data == orig
402
401
 
403
- def test_read_pickle_path(self):
402
+ def test_read_pickle_path(self, tmp_path):
404
403
  orig = test.list_of_dicts("downloads.json")
405
- handle, path = tempfile.mkstemp(".pkl")
404
+ path = str(tmp_path / "test.pkl")
406
405
  orig.write_pickle(path)
407
406
  ListOfDicts.read_pickle(Path(path))
408
407
 
@@ -565,38 +564,38 @@ class TestListOfDicts:
565
564
  assert all("downloads" not in x for x in data)
566
565
  assert orig._obsolete
567
566
 
568
- def test_write_csv(self):
567
+ def test_write_csv(self, tmp_path):
569
568
  orig = test.list_of_dicts("vehicles.csv")
570
- handle, path = tempfile.mkstemp(".csv")
569
+ path = str(tmp_path / "test.csv")
571
570
  orig.write_csv(path)
572
571
  data = ListOfDicts.read_csv(path)
573
572
  assert data == orig
574
573
 
575
- def test_write_csv_path(self):
574
+ def test_write_csv_path(self, tmp_path):
576
575
  orig = test.list_of_dicts("vehicles.csv")
577
- handle, path = tempfile.mkstemp(".csv")
576
+ path = str(tmp_path / "test.csv")
578
577
  orig.write_csv(Path(path))
579
578
 
580
- def test_write_json(self):
579
+ def test_write_json(self, tmp_path):
581
580
  orig = test.list_of_dicts("downloads.json")
582
- handle, path = tempfile.mkstemp(".json")
581
+ path = str(tmp_path / "test.json")
583
582
  orig.write_json(path)
584
583
  data = ListOfDicts.read_json(path)
585
584
  assert data == orig
586
585
 
587
- def test_write_json_path(self):
586
+ def test_write_json_path(self, tmp_path):
588
587
  orig = test.list_of_dicts("downloads.json")
589
- handle, path = tempfile.mkstemp(".json")
588
+ path = str(tmp_path / "test.json")
590
589
  orig.write_json(Path(path))
591
590
 
592
- def test_write_pickle(self):
591
+ def test_write_pickle(self, tmp_path):
593
592
  orig = test.list_of_dicts("downloads.json")
594
- handle, path = tempfile.mkstemp(".pkl")
593
+ path = str(tmp_path / "test.pkl")
595
594
  orig.write_pickle(path)
596
595
  data = ListOfDicts.read_pickle(path)
597
596
  assert data == orig
598
597
 
599
- def test_write_pickle_path(self):
598
+ def test_write_pickle_path(self, tmp_path):
600
599
  orig = test.list_of_dicts("downloads.json")
601
- handle, path = tempfile.mkstemp(".pkl")
600
+ path = str(tmp_path / "test.pkl")
602
601
  orig.write_pickle(Path(path))
@@ -23,7 +23,6 @@
23
23
  import datetime
24
24
  import math
25
25
  import numpy as np
26
- import tempfile
27
26
 
28
27
  from dataiter import util
29
28
 
@@ -145,33 +144,33 @@ class TestUtil:
145
144
  assert util.utruncate("abc\u200bdef", 4) == "abc\u200bd"
146
145
  assert util.utruncate("abc\u200bdef\u200b", 4) == "abc\u200bd"
147
146
 
148
- def test_xopen_bz2(self):
147
+ def test_xopen_bz2(self, tmp_path):
149
148
  text = "test åäö"
150
- handle, path = tempfile.mkstemp(".bz2")
149
+ path = str(tmp_path / "test.bz2")
151
150
  with util.xopen(path, "wt") as f:
152
151
  f.write(text)
153
152
  with util.xopen(path, "rt") as f:
154
153
  assert f.read() == text
155
154
 
156
- def test_xopen_gz(self):
155
+ def test_xopen_gz(self, tmp_path):
157
156
  text = "test åäö"
158
- handle, path = tempfile.mkstemp(".gz")
157
+ path = str(tmp_path / "test.gz")
159
158
  with util.xopen(path, "wt") as f:
160
159
  f.write(text)
161
160
  with util.xopen(path, "rt") as f:
162
161
  assert f.read() == text
163
162
 
164
- def test_xopen_txt(self):
163
+ def test_xopen_txt(self, tmp_path):
165
164
  text = "test åäö"
166
- handle, path = tempfile.mkstemp(".txt")
165
+ path = str(tmp_path / "test.txt")
167
166
  with util.xopen(path, "wt") as f:
168
167
  f.write(text)
169
168
  with util.xopen(path, "rt") as f:
170
169
  assert f.read() == text
171
170
 
172
- def test_xopen_xz(self):
171
+ def test_xopen_xz(self, tmp_path):
173
172
  text = "test åäö"
174
- handle, path = tempfile.mkstemp(".xz")
173
+ path = str(tmp_path / "test.xz")
175
174
  with util.xopen(path, "wt") as f:
176
175
  f.write(text)
177
176
  with util.xopen(path, "rt") as f:
@@ -28,7 +28,7 @@ from dataiter import Vector
28
28
  from numpy.dtypes import StringDType
29
29
 
30
30
  NaN = np.nan
31
- NaT = np.datetime64("NaT")
31
+ NaT = np.datetime64("NaT", "D")
32
32
  DATE = datetime.date.today()
33
33
  DATETIME = datetime.datetime.now()
34
34
 
@@ -45,9 +45,9 @@ def count_digits(value):
45
45
  return n, m
46
46
 
47
47
  def format_alias_doc(alias, target):
48
- return f"{target.__doc__}\n\n{' '*8}" + (
49
- ".. note:: :func:`{}` is a convenience alias for :meth:`{}`."
50
- .format(alias.__name__, target.__qualname__))
48
+ return (f"{target.__doc__}\n\n{' '*8}"
49
+ f".. note:: :func:`{alias.__name__}` is a convenience "
50
+ f"alias for :meth:`{target.__qualname__}`.")
51
51
 
52
52
  def format_floats(seq, ksep=None):
53
53
  precision = dataiter.PRINT_FLOAT_PRECISION
@@ -62,8 +62,7 @@ def format_floats(seq, ksep=None):
62
62
  n = max(x[0] for x in digits)
63
63
  m = max(x[1] for x in digits)
64
64
  precision = min(m, max(0, precision - n))
65
- return [f"{{:,.{precision}f}}".format(x).replace(",", ksep)
66
- for x in seq]
65
+ return [f"{x:,.{precision}f}".replace(",", ksep) for x in seq]
67
66
 
68
67
  def generate_colnames(n):
69
68
  return list(itertools.islice(yield_colnames(), n))
@@ -186,6 +186,10 @@ class Vector(np.ndarray):
186
186
  return array.dtype.type(array)
187
187
  return array.view(self.__class__)
188
188
 
189
+ def __iter__(self):
190
+ # NumPy iterates subclasses ~2x slower than base arrays.
191
+ return iter(self.view(np.ndarray))
192
+
189
193
  def __repr__(self):
190
194
  return self.to_string()
191
195
 
@@ -427,6 +431,8 @@ class Vector(np.ndarray):
427
431
  return np.isnan(self)
428
432
  if self.is_string() or self._is_string_fixed():
429
433
  return self == dtypes.string.na_object
434
+ if self.is_integer() or self.is_boolean():
435
+ return self.fast(np.zeros(len(self), bool))
430
436
  # Can't use np.isin here since elements can be arrays.
431
437
  return self.fast([x is None for x in self], bool)
432
438
 
@@ -542,10 +548,13 @@ class Vector(np.ndarray):
542
548
  false and 1.0 for true. Depending on how you use the data, that might
543
549
  work as well as an object vector of ``True``, ``False`` and ``None``.
544
550
  """
545
- if self.is_datetime():
546
- return np.datetime64("NaT")
547
- if self.is_timedelta():
548
- return np.timedelta64("NaT")
551
+ if self.is_datetime() or self.is_timedelta():
552
+ unit = np.datetime_data(self.dtype)
553
+ # Use a coarse unit for generic dtype so that
554
+ # a finer unit from actual data wins in upcasting.
555
+ if unit[0] == "generic":
556
+ unit = "D"
557
+ return self.dtype.type("NaT", unit)
549
558
  if self.is_float():
550
559
  return np.nan
551
560
  if self.is_integer():
@@ -701,7 +710,7 @@ class Vector(np.ndarray):
701
710
  elif len(types) == 1 and types.copy().pop().__module__ == "numpy":
702
711
  # If we have a regular Python list of NumPy scalars,
703
712
  # infer type. This should be rare, but can happen.
704
- dtype = types.copy().pop()().dtype
713
+ dtype = np.dtype(types.copy().pop())
705
714
  na = Vector.fast([], dtype).na_value
706
715
  else:
707
716
  # Guess the missing value based on types in seq.
@@ -736,7 +745,7 @@ class Vector(np.ndarray):
736
745
  return np.nan
737
746
  datetimes = [datetime.date, datetime.datetime, np.datetime64]
738
747
  if all(x in datetimes for x in types):
739
- return np.datetime64("NaT")
748
+ return np.datetime64("NaT", "D")
740
749
  # Usually causes dtype to be object!
741
750
  return None
742
751
 
@@ -815,7 +824,7 @@ class Vector(np.ndarray):
815
824
  strings = util.format_floats(self, ksep=ksep)
816
825
  return self.__class__.fast(pad(strings), str)
817
826
  if self.is_integer() and not self.is_timedelta():
818
- strings = ["{:,d}".format(x).replace(",", ksep) for x in self]
827
+ strings = [f"{x:,d}".replace(",", ksep) for x in self]
819
828
  return self.__class__.fast(pad(strings), str)
820
829
  if self.is_object():
821
830
  strings = [str(x) for x in self]
@@ -1,5 +1,5 @@
1
1
  [build-system]
2
- requires = ["build==1.2.2.post1", "hatchling==1.21.1"]
2
+ requires = ["hatchling==1.32.4"]
3
3
  build-backend = "hatchling.build"
4
4
 
5
5
  [project]
@@ -7,10 +7,10 @@ name = "dataiter"
7
7
  dynamic = ["version"]
8
8
  description = "Simple, light-weight data frames for Python"
9
9
  readme = "README.md"
10
- license = { text = "MIT" }
10
+ license = "MIT"
11
11
  requires-python = ">=3.11.0"
12
12
  authors = [{ name = "Osmo Salomaa", email = "otsaloma@iki.fi" }]
13
- dependencies = ["attd>=0.3", "numpy>=2.4.0,<3.0", "pyarrow>=2.0", "wcwidth>=0.1"]
13
+ dependencies = ["attd>=0.3", "numpy>=2.4.0,<3.0", "pyarrow>=16.0", "wcwidth>=0.1"]
14
14
 
15
15
  [project.urls]
16
16
  Homepage = "https://github.com/otsaloma/dataiter"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes