ialdev-core 0.2.3__tar.gz → 0.2.5__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 (52) hide show
  1. {ialdev_core-0.2.3 → ialdev_core-0.2.5}/PKG-INFO +3 -3
  2. {ialdev_core-0.2.3 → ialdev_core-0.2.5}/README.md +1 -1
  3. {ialdev_core-0.2.3 → ialdev_core-0.2.5}/pyproject.toml +3 -2
  4. {ialdev_core-0.2.3 → ialdev_core-0.2.5}/src/iad/core/data/pdtools.py +116 -21
  5. {ialdev_core-0.2.3 → ialdev_core-0.2.5}/tests/test_pdtools.py +49 -2
  6. {ialdev_core-0.2.3 → ialdev_core-0.2.5}/.gitignore +0 -0
  7. {ialdev_core-0.2.3 → ialdev_core-0.2.5}/.pixi/config.toml +0 -0
  8. {ialdev_core-0.2.3 → ialdev_core-0.2.5}/src/iad/core/__init__.py +0 -0
  9. {ialdev_core-0.2.3 → ialdev_core-0.2.5}/src/iad/core/cache.py +0 -0
  10. {ialdev_core-0.2.3 → ialdev_core-0.2.5}/src/iad/core/codetools.py +0 -0
  11. {ialdev_core-0.2.3 → ialdev_core-0.2.5}/src/iad/core/data/__init__.py +0 -0
  12. {ialdev_core-0.2.3 → ialdev_core-0.2.5}/src/iad/core/data/array.py +0 -0
  13. {ialdev_core-0.2.3 → ialdev_core-0.2.5}/src/iad/core/data/binary.py +0 -0
  14. {ialdev_core-0.2.3 → ialdev_core-0.2.5}/src/iad/core/data/label.py +0 -0
  15. {ialdev_core-0.2.3 → ialdev_core-0.2.5}/src/iad/core/data/nptools.py +0 -0
  16. {ialdev_core-0.2.3 → ialdev_core-0.2.5}/src/iad/core/data/unc_panda.py +0 -0
  17. {ialdev_core-0.2.3 → ialdev_core-0.2.5}/src/iad/core/data/units.py +0 -0
  18. {ialdev_core-0.2.3 → ialdev_core-0.2.5}/src/iad/core/datatools.py +0 -0
  19. {ialdev_core-0.2.3 → ialdev_core-0.2.5}/src/iad/core/docs/locators.ipynb +0 -0
  20. {ialdev_core-0.2.3 → ialdev_core-0.2.5}/src/iad/core/events.py +0 -0
  21. {ialdev_core-0.2.3 → ialdev_core-0.2.5}/src/iad/core/fnctools.py +0 -0
  22. {ialdev_core-0.2.3 → ialdev_core-0.2.5}/src/iad/core/fs/__init__.py +0 -0
  23. {ialdev_core-0.2.3 → ialdev_core-0.2.5}/src/iad/core/fs/env.py +0 -0
  24. {ialdev_core-0.2.3 → ialdev_core-0.2.5}/src/iad/core/fs/filesproc.py +0 -0
  25. {ialdev_core-0.2.3 → ialdev_core-0.2.5}/src/iad/core/fs/paths.py +0 -0
  26. {ialdev_core-0.2.3 → ialdev_core-0.2.5}/src/iad/core/logs.py +0 -0
  27. {ialdev_core-0.2.3 → ialdev_core-0.2.5}/src/iad/core/one_dark.puml +0 -0
  28. {ialdev_core-0.2.3 → ialdev_core-0.2.5}/src/iad/core/param/__init__.py +0 -0
  29. {ialdev_core-0.2.3 → ialdev_core-0.2.5}/src/iad/core/param/confargparse.py +0 -0
  30. {ialdev_core-0.2.3 → ialdev_core-0.2.5}/src/iad/core/param/paramaze.py +0 -0
  31. {ialdev_core-0.2.3 → ialdev_core-0.2.5}/src/iad/core/regexp.py +0 -0
  32. {ialdev_core-0.2.3 → ialdev_core-0.2.5}/src/iad/core/short.py +0 -0
  33. {ialdev_core-0.2.3 → ialdev_core-0.2.5}/src/iad/core/strings.py +0 -0
  34. {ialdev_core-0.2.3 → ialdev_core-0.2.5}/src/iad/core/tbox.py +0 -0
  35. {ialdev_core-0.2.3 → ialdev_core-0.2.5}/src/iad/core/wrap.py +0 -0
  36. {ialdev_core-0.2.3 → ialdev_core-0.2.5}/tests/array.ipynb +0 -0
  37. {ialdev_core-0.2.3 → ialdev_core-0.2.5}/tests/conftest.py +0 -0
  38. {ialdev_core-0.2.3 → ialdev_core-0.2.5}/tests/data/.env +0 -0
  39. {ialdev_core-0.2.3 → ialdev_core-0.2.5}/tests/dot_styles_examples.ipynb +0 -0
  40. {ialdev_core-0.2.3 → ialdev_core-0.2.5}/tests/test_array.py +0 -0
  41. {ialdev_core-0.2.3 → ialdev_core-0.2.5}/tests/test_binar.py +0 -0
  42. {ialdev_core-0.2.3 → ialdev_core-0.2.5}/tests/test_cachepipe.py +0 -0
  43. {ialdev_core-0.2.3 → ialdev_core-0.2.5}/tests/test_codetools.py +0 -0
  44. {ialdev_core-0.2.3 → ialdev_core-0.2.5}/tests/test_coltools.py +0 -0
  45. {ialdev_core-0.2.3 → ialdev_core-0.2.5}/tests/test_datatools.py +0 -0
  46. {ialdev_core-0.2.3 → ialdev_core-0.2.5}/tests/test_events.py +0 -0
  47. {ialdev_core-0.2.3 → ialdev_core-0.2.5}/tests/test_filesproc.py +0 -0
  48. {ialdev_core-0.2.3 → ialdev_core-0.2.5}/tests/test_nptools.py +0 -0
  49. {ialdev_core-0.2.3 → ialdev_core-0.2.5}/tests/test_paths.py +0 -0
  50. {ialdev_core-0.2.3 → ialdev_core-0.2.5}/tests/test_regexp.py +0 -0
  51. {ialdev_core-0.2.3 → ialdev_core-0.2.5}/tests/test_shorts.py +0 -0
  52. {ialdev_core-0.2.3 → ialdev_core-0.2.5}/tests/test_tbox.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ialdev-core
3
- Version: 0.2.3
3
+ Version: 0.2.5
4
4
  Summary: iad.core — algorithmic utilities: data tools, parameters, paths, caching, and more
5
5
  Author: ipcoder
6
6
  Requires-Python: >=3.10
@@ -16,7 +16,7 @@ Classifier: Programming Language :: Python :: 3.11
16
16
  Classifier: Programming Language :: Python :: 3.12
17
17
  Requires-Dist: pydantic>=2.0
18
18
  Requires-Dist: pandas>=2.1.0
19
- Requires-Dist: numpy>=1.26.0
19
+ Requires-Dist: numpy>=1.26.4,<2
20
20
  Requires-Dist: pyyaml>=5.4.0
21
21
  Requires-Dist: ruamel.yaml>=0.15.0
22
22
  Requires-Dist: regex>=2021.0.0
@@ -54,7 +54,7 @@ Use this package when you need the shared building blocks used by the other `ial
54
54
  pip install ialdev-core
55
55
  ```
56
56
 
57
- Requires Python `>=3.10`, NumPy `>=2.0`, and pandas `>=2.1.0`.
57
+ Requires Python `>=3.10`, NumPy `>=1.26.4,<2`, and pandas `>=2.1.0`.
58
58
 
59
59
  ## Highlights
60
60
 
@@ -10,7 +10,7 @@ Use this package when you need the shared building blocks used by the other `ial
10
10
  pip install ialdev-core
11
11
  ```
12
12
 
13
- Requires Python `>=3.10`, NumPy `>=2.0`, and pandas `>=2.1.0`.
13
+ Requires Python `>=3.10`, NumPy `>=1.26.4,<2`, and pandas `>=2.1.0`.
14
14
 
15
15
  ## Highlights
16
16
 
@@ -4,7 +4,7 @@ build-backend = "flit_core.buildapi"
4
4
 
5
5
  [project]
6
6
  name = "ialdev-core"
7
- version = "0.2.3"
7
+ version = "0.2.5"
8
8
  description = "iad.core — algorithmic utilities: data tools, parameters, paths, caching, and more"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -27,7 +27,7 @@ classifiers = [
27
27
  dependencies = [
28
28
  "pydantic>=2.0",
29
29
  "pandas>=2.1.0",
30
- "numpy>=1.26.0",
30
+ "numpy>=1.26.4,<2",
31
31
  "pyyaml>=5.4.0",
32
32
  "ruamel.yaml>=0.15.0",
33
33
  "regex>=2021.0.0",
@@ -63,6 +63,7 @@ name = "iad.core"
63
63
 
64
64
  [tool.pytest.ini_options]
65
65
  testpaths = ["tests"]
66
+ addopts = "--import-mode=importlib"
66
67
  python_files = ["test_*.py"]
67
68
  python_classes = ["Test*"]
68
69
  python_functions = ["test_*"]
@@ -4,6 +4,7 @@ import logging
4
4
  import re
5
5
  import warnings
6
6
  from collections import namedtuple
7
+ from collections.abc import Mapping
7
8
  from dataclasses import dataclass
8
9
  from functools import cmp_to_key
9
10
  from io import StringIO
@@ -767,7 +768,7 @@ class TablePainter:
767
768
  styled.set_caption(self.cap)
768
769
 
769
770
  if self.prec is not None:
770
- styled.set_precision(self.prec)
771
+ styled.format(precision=self.prec)
771
772
 
772
773
  if not self.disp:
773
774
  return styled
@@ -817,8 +818,8 @@ def side_tables(*tables, caps=None, hide_index=None, painter=None, **kws):
817
818
  index.names = [None] * len(index.names)
818
819
  columns = table.columns[[0]]
819
820
  t = pd.DataFrame(data=np.arange(len(table.index)), index=index, columns=columns).style
820
- yield t.hide_columns([*columns]), None
821
- yield table.hide_index(), cap
821
+ yield t.hide([*columns], axis="columns"), None
822
+ yield table.hide(axis="index"), cap
822
823
  else:
823
824
  yield table, cap
824
825
 
@@ -1320,6 +1321,24 @@ def trans(s, max_col_width=20):
1320
1321
  return ss
1321
1322
 
1322
1323
 
1324
+ def _format_display_cell(value, fnc):
1325
+ """Apply a ``TableFormats`` cell formatter, while leaving null cells empty.
1326
+
1327
+ NA detection is guarded to scalars so array-like cells (the very ones we
1328
+ want to compact, e.g. ndarrays) never reach the ambiguous ``pd.isna``
1329
+ truth-value check.
1330
+ """
1331
+ if value is None:
1332
+ return ""
1333
+ if np.ndim(value) == 0:
1334
+ try:
1335
+ if pd.isna(value):
1336
+ return ""
1337
+ except (TypeError, ValueError):
1338
+ pass
1339
+ return fnc(value)
1340
+
1341
+
1323
1342
  def _invert_levels(levels, names):
1324
1343
  if isinstance(levels, (str, int)):
1325
1344
  levels = [levels]
@@ -1329,8 +1348,71 @@ def _invert_levels(levels, names):
1329
1348
  return names.difference(levels)
1330
1349
 
1331
1350
 
1351
+ def _reduce(obj: PTable, op: str, *, axis=0, level=None, numeric_only=None):
1352
+ """Apply a reduction (``'mean'`` | ``'sum'`` | ``'count'``) over an axis, or,
1353
+ if ``level`` is given, by grouping that index level.
1354
+
1355
+ This replaces the ``level=`` argument of the aggregation methods, which was
1356
+ removed in pandas 2.0 in favour of ``groupby(level=...)``.
1357
+ """
1358
+ num_kws = {} if (numeric_only is None or op == 'count') else dict(numeric_only=numeric_only)
1359
+ if level is not None:
1360
+ # GroupBy methods are the stock pandas implementations, so this also
1361
+ # bypasses any aggregation overrides on the table subclasses.
1362
+ return getattr(obj.groupby(level=level), op)(**num_kws)
1363
+ # Call the unbound pandas method explicitly to bypass subclass overrides
1364
+ # (e.g. _TableMixIn.mean), matching the pre-pandas-2 behaviour.
1365
+ base = pd.Series if isinstance(obj, pd.Series) else pd.DataFrame
1366
+ if base is pd.Series:
1367
+ return getattr(base, op)(obj, **num_kws)
1368
+ return getattr(base, op)(obj, axis=axis, **num_kws)
1369
+
1370
+
1332
1371
  class _TableMixIn:
1333
1372
 
1373
+ def _display_frame(self) -> pd.DataFrame:
1374
+ """Build a plain ``pd.DataFrame`` copy for marimo's interactive table.
1375
+
1376
+ Object columns (which may hold large ndarrays, dicts, paths, ...) are
1377
+ replaced by their compact ``TableFormats`` string representation, the
1378
+ same one used by ``_repr_html_``. Numeric / bool / datetime / string
1379
+ columns are left native so marimo keeps real sorting and filtering.
1380
+
1381
+ A plain ``pd.DataFrame`` (not a ``DataTable``) is returned on purpose:
1382
+ it has no ``_display_`` of its own, so marimo applies its built-in
1383
+ opinionated formatter and renders an interactive, sortable, filterable
1384
+ table whose cells are already short.
1385
+ """
1386
+ src = self if self.ndim == 2 else self._prep_repr()
1387
+ plain = pd.DataFrame(src)
1388
+ formatters = TableFormats.formatters(plain)
1389
+ for i, fnc in enumerate(formatters):
1390
+ if fnc is None or plain.dtypes.iloc[i] != object:
1391
+ continue
1392
+ col = plain.iloc[:, i]
1393
+ plain.isetitem(i, col.map(lambda v, _f=fnc: _format_display_cell(v, _f)))
1394
+ return plain
1395
+
1396
+ def _display_(self):
1397
+ """marimo display protocol.
1398
+
1399
+ Has the highest precedence in marimo's formatter resolution, so it
1400
+ overrides marimo's built-in opinionated formatter for ``pd.DataFrame``
1401
+ / ``pd.Series`` (which would otherwise dump full ndarray contents for
1402
+ the ``data`` column and similar).
1403
+
1404
+ We return a plain ``pd.DataFrame`` with object columns pre-rendered to
1405
+ compact strings, so marimo still shows its interactive table (sorting,
1406
+ filtering, pagination) but without dumping large arrays. On any failure
1407
+ we fall back to the static rich HTML used by Jupyter.
1408
+ """
1409
+ try:
1410
+ return self._display_frame()
1411
+ except Exception:
1412
+ import marimo as mo
1413
+ html = self._repr_html_()
1414
+ return mo.plain(self) if html is None else mo.Html(html)
1415
+
1334
1416
  def as_labels(self, index=True, data=False, squeeze=False) -> Labels | list[Labels]:
1335
1417
  """
1336
1418
  Convert into list of datacast.labels.Labels objects.
@@ -1436,7 +1518,7 @@ class _TableMixIn:
1436
1518
  if strict:
1437
1519
  missing = set(levels).difference(index_names)
1438
1520
  if missing:
1439
- tbl = tbl.stack([*missing]) # will raise her if not found in columns
1521
+ tbl = tbl.stack([*missing], future_stack=True) # will raise her if not found in columns
1440
1522
  index_names.update(missing)
1441
1523
 
1442
1524
  tbl = tbl.unstack([*index_names.difference(levels)], **kws)
@@ -1469,7 +1551,13 @@ class _TableMixIn:
1469
1551
  raise KeyError(f"Levels {missing} not found in index {tbl.index.names}")
1470
1552
  col_names.update(missing)
1471
1553
 
1472
- tbl = tbl.stack([*col_names.difference(levels)], **kws) # Need remove NULL rows?
1554
+ # pandas 2: the new stack (future_stack=True) no longer accepts/handles
1555
+ # `dropna`/`sort` and never drops NaN rows, so emulate the old default.
1556
+ dropna = kws.pop('dropna', True)
1557
+ kws.pop('sort', None)
1558
+ tbl = tbl.stack([*col_names.difference(levels)], future_stack=True, **kws)
1559
+ if dropna:
1560
+ tbl = tbl.dropna(how='all') if tbl.ndim > 1 else tbl.dropna()
1473
1561
 
1474
1562
  if tbl.ndim > 1 and tbl.columns.nlevels > 1: # if there are col levels
1475
1563
  levels = [lvl for lvl in levels if lvl in col_names]
@@ -1730,8 +1818,6 @@ class _TableMixIn:
1730
1818
  Tutorial:
1731
1819
  Using this DataTable:
1732
1820
 
1733
- >>> from iad.core.tests.test_pdtools import sdt_general
1734
- >>> dt= sdt_general()
1735
1821
  >>> dt # doctest: +NORMALIZE_WHITESPACE
1736
1822
  z data1 data2
1737
1823
  w a b c d a b c d
@@ -1896,15 +1982,13 @@ class _TableMixIn:
1896
1982
  Include only float, int, boolean columns. If None, will attempt to use
1897
1983
  everything, then use only numeric data. Not implemented for Series.
1898
1984
  """
1899
- series = isinstance(self, pd.Series)
1900
- kws = dict(axis=axis, level=level, numeric_only=numeric_only)
1901
- mean = (pd.Series if series else pd.DataFrame).mean(self.nominal_value, **kws)
1985
+ mean = _reduce(self.nominal_value, 'mean', axis=axis, level=level, numeric_only=numeric_only)
1902
1986
  if not (hasattr(mean, 'index') and hasattr(self, 'nominal_value')) or self.std_dev.sum().sum() == 0:
1903
1987
  return mean
1904
1988
 
1905
- kws.update(level=level)
1906
- num = self.std_dev.count(**(dict(level=level) if series else kws))
1907
- sd = ((self.std_dev ** 2).sum(**kws) / num) ** 0.5
1989
+ num = _reduce(self.std_dev, 'count', axis=axis, level=level)
1990
+ sd = (_reduce(self.std_dev ** 2, 'sum', axis=axis, level=level,
1991
+ numeric_only=numeric_only) / num) ** 0.5
1908
1992
  from uncertainties.unumpy import uarray
1909
1993
  return mean.__class__(uarray(mean, sd), index=mean.index,
1910
1994
  **({} if mean.ndim == 1 else {'columns': mean.columns}))
@@ -1930,11 +2014,14 @@ class _TableMixIn:
1930
2014
  self = self.unstack_but(ws.index.names) # align indices
1931
2015
  assert ws.shape[0] == self.shape[0], f"{ws.shape=} vs {self.shape=}"
1932
2016
 
1933
- ws = ws / ws.sum(level=level)
1934
- avr = (self.multiply(ws, 0)).sum(level=level).unstack_but(level)
2017
+ if level is None:
2018
+ ws = ws / ws.sum()
2019
+ else:
2020
+ ws = ws / ws.groupby(level=level).transform('sum')
2021
+ avr = _reduce(self.multiply(ws, 0), 'sum', level=level).unstack_but(level)
1935
2022
  if unc:
1936
2023
  from uncertainties.unumpy import uarray
1937
- sd = ((self.sub(avr) ** 2).multiply(ws, 0).sum(level=level)) ** 0.5
2024
+ sd = (_reduce((self.sub(avr) ** 2).multiply(ws, 0), 'sum', level=level)) ** 0.5
1938
2025
  if unc is True:
1939
2026
  data = uarray(avr.values, sd.values)
1940
2027
  return DataSeries(data, index=avr.index) if avr.ndim == 1 else \
@@ -1948,11 +2035,17 @@ class _TableMixIn:
1948
2035
  index_names = self.index.names
1949
2036
  return pd.concat([self.reset_index(), other.reset_index()]).set_index(list(index_names))
1950
2037
 
1951
- def __or__(self: DTable, other: dict):
2038
+ def __or__(self: DTable, other):
2039
+ # Only the dict form adds index levels; delegate everything else
2040
+ # (e.g. boolean masks ``mask_a | mask_b``) to the pandas operator.
2041
+ if not isinstance(other, Mapping):
2042
+ return super().__or__(other)
1952
2043
  (out := self.copy()).__ior__(other)
1953
2044
  return out
1954
2045
 
1955
- def __ior__(self: DTable, other: dict):
2046
+ def __ior__(self: DTable, other):
2047
+ if not isinstance(other, Mapping):
2048
+ return super().__ior__(other)
1956
2049
  new_levels = list(other)
1957
2050
  self[new_levels] = [*other.values()]
1958
2051
  self.set_index(new_levels, append=True, inplace=True)
@@ -2021,7 +2114,8 @@ def path_fixer(root, fixer_name, cols='path') -> Callable[[pd.DataFrame], pd.Dat
2021
2114
 
2022
2115
  def fix_path(df):
2023
2116
  if columns := [c for c in df.columns if c in cols]:
2024
- df[columns] = df[columns].applymap(fixer(root))
2117
+ df = df.copy()
2118
+ df[columns] = df[columns].map(fixer(root))
2025
2119
  return df
2026
2120
 
2027
2121
  return fix_path
@@ -2262,8 +2356,9 @@ def append_col(df: pd.DataFrame,
2262
2356
  by cols and the added into `df`
2263
2357
  :param levels: optionally provide names for the levels,
2264
2358
  must be same number of them as the final levels
2265
- :return: return the input df (the operations are inplace)
2359
+ :return: a new data frame with the column(s) added (the input is not modified)
2266
2360
  """
2361
+ df = df.copy()
2267
2362
 
2268
2363
  def add_levels(n):
2269
2364
  df.columns = pd.MultiIndex.from_product([df.columns, *[*[['']] * n]])
@@ -2516,7 +2611,7 @@ def filter_full_groups(df, group) -> (pd.DataFrame, int):
2516
2611
  group = as_list(group)
2517
2612
  mv_idx = df.index.names.difference(group)
2518
2613
  df = df.reset_index(mv_idx) # leave only 'scene' in the index
2519
- full_groups = df.groupby(group).apply(lambda x: issubset_report(x.kind, ))
2614
+ full_groups = df.groupby(group).apply(lambda x: issubset_report(x.kind, ), include_groups=False)
2520
2615
  df = df.loc[full_groups[full_groups].index]
2521
2616
  bad_scenes = (full_groups == False).sum()
2522
2617
  groups_num = len(full_groups) - bad_scenes
@@ -373,11 +373,58 @@ def test_append_col():
373
373
  [4, 5, 6],
374
374
  [7, 8, 9]], index=pd.MultiIndex.from_tuples([(1, 1), (1, 2), (1, 3)]))
375
375
  dt.columns = pd.MultiIndex.from_tuples([(1, 1), (1, 2), (1, 3)])
376
- append_col(dt, col='4', values=[10, 11, 12])
377
- append_col(dt, col='4', values=[3, 1, 11])
376
+ dt = append_col(dt, col='4', values=[10, 11, 12])
377
+ dt = append_col(dt, col='4', values=[3, 1, 11])
378
378
  assert int(dt.loc[(1, 3), '4']) == 11
379
379
 
380
380
 
381
+ def test_append_col_does_not_mutate_input():
382
+ """append_col must return a new frame and leave the input untouched (pandas 2 / CoW)."""
383
+ base = DT(data=[[1], [2]], columns=['a'])
384
+ out = pdt.append_col(base, col='b', values=[10, 20])
385
+ assert 'b' not in base.columns # input untouched
386
+ assert out['b'].tolist() == [10, 20] # result carries the new column
387
+
388
+
389
+ def test_reduce_level_aggregation():
390
+ """`_reduce` replaces the removed pandas-1 `level=` aggregation argument."""
391
+ idx = pd.MultiIndex.from_product([['a', 'b'], [1, 2]], names=['g', 'i'])
392
+ s = DS([1.0, 3.0, 10.0, 20.0], index=idx)
393
+ df = DT({'v': [1.0, 3.0, 10.0, 20.0], 'w': [2.0, 4.0, 6.0, 8.0]}, index=idx)
394
+
395
+ # by level -> grouped result indexed by that level
396
+ assert pdt._reduce(s, 'mean', level='g').to_dict() == {'a': 2.0, 'b': 15.0}
397
+ assert pdt._reduce(s, 'sum', level='g').to_dict() == {'a': 4.0, 'b': 30.0}
398
+ assert pdt._reduce(s, 'count', level='g').to_dict() == {'a': 2, 'b': 2}
399
+ assert pdt._reduce(df, 'sum', level='g').loc['a'].to_dict() == {'v': 4.0, 'w': 6.0}
400
+
401
+ # no level -> plain reduction
402
+ assert pdt._reduce(s, 'sum') == 34.0
403
+ assert pdt._reduce(df, 'mean')['v'] == 8.5
404
+
405
+
406
+ def test_bitwise_or_is_elementwise_for_masks():
407
+ """The custom __or__ must not hijack boolean-mask combination (`mask | mask`)."""
408
+ idx = pd.MultiIndex.from_product([['a', 'b'], [1, 2]], names=['g', 'i'])
409
+ s = DS([1.0, np.nan, np.inf, -np.inf], index=idx)
410
+ assert (s.isnull() | np.isinf(s)).tolist() == [False, True, True, True]
411
+
412
+ df = DT({'x': [1.0, np.nan], 'y': [np.inf, 2.0]})
413
+ assert (df.isnull() | ~np.isfinite(df)).values.tolist() == [[False, True], [True, False]]
414
+
415
+
416
+ def test_or_dict_still_adds_index_level():
417
+ """The dict form of `|` keeps its index-level-appending behaviour."""
418
+ dt = DT({'v': [1, 2]})
419
+ out = dt | dict(mode='X', fid='001')
420
+ assert 'mode' in out.index.names and 'fid' in out.index.names
421
+ assert out.index.get_level_values('mode').tolist() == ['X', 'X']
422
+
423
+ dt2 = DT({'v': [1, 2]})
424
+ dt2 |= dict(mode='Y')
425
+ assert dt2.index.get_level_values('mode').tolist() == ['Y', 'Y']
426
+
427
+
381
428
  def test_sample(sdt):
382
429
  sample = pdt.sample
383
430
  assert len(sample(sdt, 2)) == 2
File without changes
File without changes