nested-pandas 0.3.0__tar.gz → 0.3.2__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 (79) hide show
  1. {nested_pandas-0.3.0/src/nested_pandas.egg-info → nested_pandas-0.3.2}/PKG-INFO +1 -1
  2. {nested_pandas-0.3.0 → nested_pandas-0.3.2}/src/nested_pandas/_version.py +2 -2
  3. {nested_pandas-0.3.0 → nested_pandas-0.3.2}/src/nested_pandas/nestedframe/core.py +98 -3
  4. {nested_pandas-0.3.0 → nested_pandas-0.3.2/src/nested_pandas.egg-info}/PKG-INFO +1 -1
  5. {nested_pandas-0.3.0 → nested_pandas-0.3.2}/src/nested_pandas.egg-info/SOURCES.txt +0 -1
  6. {nested_pandas-0.3.0 → nested_pandas-0.3.2}/tests/nested_pandas/nestedframe/test_nestedframe.py +26 -0
  7. {nested_pandas-0.3.0 → nested_pandas-0.3.2}/tests/nested_pandas/utils/test_utils.py +36 -12
  8. nested_pandas-0.3.0/src/nested_pandas/nestedframe/utils.py +0 -61
  9. {nested_pandas-0.3.0 → nested_pandas-0.3.2}/.copier-answers.yml +0 -0
  10. {nested_pandas-0.3.0 → nested_pandas-0.3.2}/.git_archival.txt +0 -0
  11. {nested_pandas-0.3.0 → nested_pandas-0.3.2}/.gitattributes +0 -0
  12. {nested_pandas-0.3.0 → nested_pandas-0.3.2}/.github/ISSUE_TEMPLATE/0-general_issue.md +0 -0
  13. {nested_pandas-0.3.0 → nested_pandas-0.3.2}/.github/ISSUE_TEMPLATE/1-bug_report.md +0 -0
  14. {nested_pandas-0.3.0 → nested_pandas-0.3.2}/.github/ISSUE_TEMPLATE/2-feature_request.md +0 -0
  15. {nested_pandas-0.3.0 → nested_pandas-0.3.2}/.github/dependabot.yml +0 -0
  16. {nested_pandas-0.3.0 → nested_pandas-0.3.2}/.github/pull_request_template.md +0 -0
  17. {nested_pandas-0.3.0 → nested_pandas-0.3.2}/.github/workflows/asv-main.yml +0 -0
  18. {nested_pandas-0.3.0 → nested_pandas-0.3.2}/.github/workflows/asv-nightly.yml +0 -0
  19. {nested_pandas-0.3.0 → nested_pandas-0.3.2}/.github/workflows/asv-pr.yml +0 -0
  20. {nested_pandas-0.3.0 → nested_pandas-0.3.2}/.github/workflows/build-documentation.yml +0 -0
  21. {nested_pandas-0.3.0 → nested_pandas-0.3.2}/.github/workflows/pre-commit-ci.yml +0 -0
  22. {nested_pandas-0.3.0 → nested_pandas-0.3.2}/.github/workflows/publish-benchmarks-pr.yml +0 -0
  23. {nested_pandas-0.3.0 → nested_pandas-0.3.2}/.github/workflows/publish-to-pypi.yml +0 -0
  24. {nested_pandas-0.3.0 → nested_pandas-0.3.2}/.github/workflows/smoke-test.yml +0 -0
  25. {nested_pandas-0.3.0 → nested_pandas-0.3.2}/.github/workflows/testing-and-coverage.yml +0 -0
  26. {nested_pandas-0.3.0 → nested_pandas-0.3.2}/.gitignore +0 -0
  27. {nested_pandas-0.3.0 → nested_pandas-0.3.2}/.pre-commit-config.yaml +0 -0
  28. {nested_pandas-0.3.0 → nested_pandas-0.3.2}/.readthedocs.yml +0 -0
  29. {nested_pandas-0.3.0 → nested_pandas-0.3.2}/.setup_dev.sh +0 -0
  30. {nested_pandas-0.3.0 → nested_pandas-0.3.2}/LICENSE +0 -0
  31. {nested_pandas-0.3.0 → nested_pandas-0.3.2}/README.md +0 -0
  32. {nested_pandas-0.3.0 → nested_pandas-0.3.2}/benchmarks/README.md +0 -0
  33. {nested_pandas-0.3.0 → nested_pandas-0.3.2}/benchmarks/__init__.py +0 -0
  34. {nested_pandas-0.3.0 → nested_pandas-0.3.2}/benchmarks/asv.conf.json +0 -0
  35. {nested_pandas-0.3.0 → nested_pandas-0.3.2}/benchmarks/benchmarks.py +0 -0
  36. {nested_pandas-0.3.0 → nested_pandas-0.3.2}/docs/Makefile +0 -0
  37. {nested_pandas-0.3.0 → nested_pandas-0.3.2}/docs/_static/custom.css +0 -0
  38. {nested_pandas-0.3.0 → nested_pandas-0.3.2}/docs/conf.py +0 -0
  39. {nested_pandas-0.3.0 → nested_pandas-0.3.2}/docs/gettingstarted/contributing.rst +0 -0
  40. {nested_pandas-0.3.0 → nested_pandas-0.3.2}/docs/gettingstarted/installation.rst +0 -0
  41. {nested_pandas-0.3.0 → nested_pandas-0.3.2}/docs/gettingstarted/quickstart.ipynb +0 -0
  42. {nested_pandas-0.3.0 → nested_pandas-0.3.2}/docs/gettingstarted.rst +0 -0
  43. {nested_pandas-0.3.0 → nested_pandas-0.3.2}/docs/index.rst +0 -0
  44. {nested_pandas-0.3.0 → nested_pandas-0.3.2}/docs/requirements.txt +0 -0
  45. {nested_pandas-0.3.0 → nested_pandas-0.3.2}/docs/tutorials/README.md +0 -0
  46. {nested_pandas-0.3.0 → nested_pandas-0.3.2}/docs/tutorials/data_loading_notebook.ipynb +0 -0
  47. {nested_pandas-0.3.0 → nested_pandas-0.3.2}/docs/tutorials/data_manipulation.ipynb +0 -0
  48. {nested_pandas-0.3.0 → nested_pandas-0.3.2}/docs/tutorials/low_level.ipynb +0 -0
  49. {nested_pandas-0.3.0 → nested_pandas-0.3.2}/docs/tutorials/nested_spectra.ipynb +0 -0
  50. {nested_pandas-0.3.0 → nested_pandas-0.3.2}/docs/tutorials.rst +0 -0
  51. {nested_pandas-0.3.0 → nested_pandas-0.3.2}/nestedframe.png +0 -0
  52. {nested_pandas-0.3.0 → nested_pandas-0.3.2}/pyproject.toml +0 -0
  53. {nested_pandas-0.3.0 → nested_pandas-0.3.2}/setup.cfg +0 -0
  54. {nested_pandas-0.3.0 → nested_pandas-0.3.2}/src/nested_pandas/__init__.py +0 -0
  55. {nested_pandas-0.3.0 → nested_pandas-0.3.2}/src/nested_pandas/datasets/__init__.py +0 -0
  56. {nested_pandas-0.3.0 → nested_pandas-0.3.2}/src/nested_pandas/datasets/generation.py +0 -0
  57. {nested_pandas-0.3.0 → nested_pandas-0.3.2}/src/nested_pandas/nestedframe/__init__.py +0 -0
  58. {nested_pandas-0.3.0 → nested_pandas-0.3.2}/src/nested_pandas/nestedframe/io.py +0 -0
  59. {nested_pandas-0.3.0 → nested_pandas-0.3.2}/src/nested_pandas/py.typed +0 -0
  60. {nested_pandas-0.3.0 → nested_pandas-0.3.2}/src/nested_pandas/series/__init__.py +0 -0
  61. {nested_pandas-0.3.0 → nested_pandas-0.3.2}/src/nested_pandas/series/accessor.py +0 -0
  62. {nested_pandas-0.3.0 → nested_pandas-0.3.2}/src/nested_pandas/series/dtype.py +0 -0
  63. {nested_pandas-0.3.0 → nested_pandas-0.3.2}/src/nested_pandas/series/ext_array.py +0 -0
  64. {nested_pandas-0.3.0 → nested_pandas-0.3.2}/src/nested_pandas/series/packer.py +0 -0
  65. {nested_pandas-0.3.0 → nested_pandas-0.3.2}/src/nested_pandas/series/utils.py +0 -0
  66. {nested_pandas-0.3.0 → nested_pandas-0.3.2}/src/nested_pandas/utils/__init__.py +0 -0
  67. {nested_pandas-0.3.0 → nested_pandas-0.3.2}/src/nested_pandas/utils/utils.py +0 -0
  68. {nested_pandas-0.3.0 → nested_pandas-0.3.2}/src/nested_pandas.egg-info/dependency_links.txt +0 -0
  69. {nested_pandas-0.3.0 → nested_pandas-0.3.2}/src/nested_pandas.egg-info/requires.txt +0 -0
  70. {nested_pandas-0.3.0 → nested_pandas-0.3.2}/src/nested_pandas.egg-info/top_level.txt +0 -0
  71. {nested_pandas-0.3.0 → nested_pandas-0.3.2}/tests/nested_pandas/conftest.py +0 -0
  72. {nested_pandas-0.3.0 → nested_pandas-0.3.2}/tests/nested_pandas/datasets/test_generation.py +0 -0
  73. {nested_pandas-0.3.0 → nested_pandas-0.3.2}/tests/nested_pandas/e2e_tests/test_issue89.py +0 -0
  74. {nested_pandas-0.3.0 → nested_pandas-0.3.2}/tests/nested_pandas/nestedframe/test_io.py +0 -0
  75. {nested_pandas-0.3.0 → nested_pandas-0.3.2}/tests/nested_pandas/series/test_accessor.py +0 -0
  76. {nested_pandas-0.3.0 → nested_pandas-0.3.2}/tests/nested_pandas/series/test_dtype.py +0 -0
  77. {nested_pandas-0.3.0 → nested_pandas-0.3.2}/tests/nested_pandas/series/test_ext_array.py +0 -0
  78. {nested_pandas-0.3.0 → nested_pandas-0.3.2}/tests/nested_pandas/series/test_packer.py +0 -0
  79. {nested_pandas-0.3.0 → nested_pandas-0.3.2}/tests/nested_pandas/test_packaging.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: nested-pandas
3
- Version: 0.3.0
3
+ Version: 0.3.2
4
4
  Author-email: LINCC Frameworks <brantd@uw.edu>
5
5
  License: MIT License
6
6
 
@@ -12,5 +12,5 @@ __version__: str
12
12
  __version_tuple__: VERSION_TUPLE
13
13
  version_tuple: VERSION_TUPLE
14
14
 
15
- __version__ = version = '0.3.0'
16
- __version_tuple__ = version_tuple = (0, 3, 0)
15
+ __version__ = version = '0.3.2'
16
+ __version_tuple__ = version_tuple = (0, 3, 2)
@@ -10,9 +10,11 @@ import pyarrow as pa
10
10
  from pandas._libs import lib
11
11
  from pandas._typing import Any, AnyAll, Axis, IndexLabel
12
12
  from pandas.api.extensions import no_default
13
+ from pandas.core.computation import ops
14
+ from pandas.core.computation.eval import Expr, ensure_scope
13
15
  from pandas.core.computation.expr import PARSERS, PandasExprVisitor
16
+ from pandas.core.computation.parsing import clean_column_name
14
17
 
15
- from nested_pandas.nestedframe.utils import extract_nest_names
16
18
  from nested_pandas.series.dtype import NestedDtype
17
19
  from nested_pandas.series.packer import pack, pack_lists, pack_sorted_df_into_struct
18
20
 
@@ -79,6 +81,22 @@ class _NestResolver(dict):
79
81
  def __init__(self, outer: NestedFrame):
80
82
  self._outer = outer
81
83
  super().__init__()
84
+ # Pre-load the field resolvers for all columns which are known at present.
85
+ for column in outer.nested_columns:
86
+ self._initialize_field_resolver(column, outer)
87
+
88
+ def _initialize_field_resolver(self, column: str, outer: NestedFrame):
89
+ """
90
+ Initialize a resolver for the given nested column, and also an alias
91
+ for it, in the case of column names that have spaces or are otherwise
92
+ not identifier-like.
93
+ """
94
+ super().__setitem__(column, _NestedFieldResolver(column, outer))
95
+ clean_id = clean_column_name(column)
96
+ # And once more for the cleaned name, if it's different.
97
+ # This allows us to capture references to it from the Pandas evaluator.
98
+ if clean_id != column:
99
+ super().__setitem__(clean_id, _NestedFieldResolver(column, outer))
82
100
 
83
101
  def __contains__(self, item):
84
102
  top_nest = item if "." not in item else item.split(".")[0].strip()
@@ -89,7 +107,7 @@ class _NestResolver(dict):
89
107
  if not super().__contains__(top_nest):
90
108
  if top_nest not in self._outer.nested_columns:
91
109
  raise KeyError(f"Unknown nest {top_nest}")
92
- super().__setitem__(top_nest, _NestedFieldResolver(top_nest, self._outer))
110
+ self._initialize_field_resolver(top_nest, self._outer)
93
111
  return super().__getitem__(top_nest)
94
112
 
95
113
  def __setitem__(self, item, _):
@@ -133,6 +151,48 @@ class _NestedFieldResolver:
133
151
  raise AttributeError(f"No attribute {item_name}")
134
152
 
135
153
 
154
+ def _subexprs_by_nest(parents: list, node) -> dict[str, list]:
155
+ """
156
+ Given an expression which contains references to both base and nested
157
+ columns, return a dictionary of the sub-expressions that should be
158
+ evaluated independently, keyed by nesting context.
159
+
160
+ The key of the dictionary is the name of the nested column, and will
161
+ be a blank string in the case of base columns. The value is a list
162
+ of the parent nodes that lead to sub-expressions that can be evaluated
163
+ successfully.
164
+
165
+ While this is not in use today for automatically splitting expressions,
166
+ it can be used to detect whether an expression is suitably structured
167
+ for evaluation: the returned dictionary should have a single key.
168
+ """
169
+ if isinstance(node, ops.Term) and not isinstance(node, ops.Constant):
170
+ if isinstance(node.value, _SeriesFromNest):
171
+ return {node.value.nest_name: parents}
172
+ return {getattr(node, "upper_name", ""): parents}
173
+ if not isinstance(node, ops.Op):
174
+ return {}
175
+ sources = [getattr(node, "lhs", None), getattr(node, "rhs", None)]
176
+ result: dict[str, list] = {}
177
+ for source in sources:
178
+ child = _subexprs_by_nest(parents, source)
179
+ for k, v in child.items():
180
+ result.setdefault(k, []).append(v)
181
+ # After a complete traversal across sources, check for any necessary splits.
182
+ # If it's homogenous, move the split-node up the tree.
183
+ if len(result) == 1:
184
+ # Let the record of each parent node drift up the tree,
185
+ # and merge the subtrees into a single node, since by definition,
186
+ # this node is homogeneous over all of its children, and can
187
+ # be evaluated in a single step.
188
+ result = {k: [node] for k in result}
189
+ # If the result is either empty or has more than one key, leave the result
190
+ # alone. Each key represents a different nest (with a blank string for the base),
191
+ # and the value is the highest point in the expression tree where the expression
192
+ # was still within a single nest.
193
+ return result
194
+
195
+
136
196
  class NestedFrame(pd.DataFrame):
137
197
  """A Pandas Dataframe extension with support for nested structure.
138
198
 
@@ -202,6 +262,8 @@ class NestedFrame(pd.DataFrame):
202
262
  nested = item.split(".")[0]
203
263
  col = ".".join(item.split(".")[1:])
204
264
  return self[nested].nest.get_flat_series(col)
265
+ else:
266
+ raise KeyError(f"Column '{item}' not found in nested columns or base columns")
205
267
  else:
206
268
  return super().__getitem__(item)
207
269
 
@@ -455,6 +517,39 @@ class NestedFrame(pd.DataFrame):
455
517
  kwargs["parser"] = "nested-pandas"
456
518
  return super().eval(expr, **kwargs)
457
519
 
520
+ def extract_nest_names(
521
+ self,
522
+ expr: str,
523
+ local_dict=None,
524
+ global_dict=None,
525
+ resolvers=(),
526
+ level: int = 0,
527
+ target=None,
528
+ **kwargs,
529
+ ) -> set[str]:
530
+ """
531
+ Given a string expression, parse it and visit the resulting expression tree,
532
+ surfacing the nesting types. The purpose is to identify expressions that attempt
533
+ to mix base and nested columns, or columns from two different nests.
534
+ """
535
+ index_resolvers = self._get_index_resolvers()
536
+ column_resolvers = self._get_cleaned_column_resolvers()
537
+ resolvers = resolvers + (_NestResolver(self), column_resolvers, index_resolvers)
538
+ # Parser needs to be the "nested-pandas" parser.
539
+ # We also need the same variable context that eval() will have, so that
540
+ # backtick-quoted names are substituted as expected.
541
+ env = ensure_scope(
542
+ level + 1,
543
+ global_dict=global_dict,
544
+ local_dict=local_dict,
545
+ resolvers=resolvers,
546
+ target=target,
547
+ )
548
+ parsed_expr = Expr(expr, parser="nested-pandas", env=env)
549
+ expr_tree = parsed_expr.terms
550
+ separable = _subexprs_by_nest([], expr_tree)
551
+ return set(separable.keys())
552
+
458
553
  def query(self, expr: str, *, inplace: bool = False, **kwargs) -> NestedFrame | None:
459
554
  """
460
555
  Query the columns of a NestedFrame with a boolean expression. Specified
@@ -512,7 +607,7 @@ class NestedFrame(pd.DataFrame):
512
607
  # At present, the query expression must be either entirely within a
513
608
  # single nest, or have nothing but base columns. Mixed structures are not
514
609
  # supported, so preflight the expression.
515
- nest_names = extract_nest_names(expr)
610
+ nest_names = self.extract_nest_names(expr, **kwargs)
516
611
  if len(nest_names) > 1:
517
612
  raise ValueError("Queries cannot target multiple structs/layers, write a separate query for each")
518
613
  result = self.eval(expr, **kwargs)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: nested-pandas
3
- Version: 0.3.0
3
+ Version: 0.3.2
4
4
  Author-email: LINCC Frameworks <brantd@uw.edu>
5
5
  License: MIT License
6
6
 
@@ -55,7 +55,6 @@ src/nested_pandas/datasets/generation.py
55
55
  src/nested_pandas/nestedframe/__init__.py
56
56
  src/nested_pandas/nestedframe/core.py
57
57
  src/nested_pandas/nestedframe/io.py
58
- src/nested_pandas/nestedframe/utils.py
59
58
  src/nested_pandas/series/__init__.py
60
59
  src/nested_pandas/series/accessor.py
61
60
  src/nested_pandas/series/dtype.py
@@ -594,6 +594,25 @@ def test_query():
594
594
  assert base["nested.d"].shape == (2,)
595
595
 
596
596
 
597
+ def test_query_on_non_identifier_columns():
598
+ """
599
+ Column names very often follow the same rules as Python identifiers, but
600
+ they are not required to. Test that query() can handle such names.
601
+ """
602
+ # Taken from GH#174
603
+ nf = NestedFrame(data={"dog": [1, 2, 3], "good dog": [2, 4, 6]}, index=[0, 1, 2])
604
+ nested = pd.DataFrame(
605
+ data={"a": [0, 2, 4, 1, 4, 3, 1, 4, 1], "b": [5, 4, 7, 5, 3, 1, 9, 3, 4]},
606
+ index=[0, 0, 0, 1, 1, 1, 2, 2, 2],
607
+ )
608
+ nf = nf.add_nested(nested, "bad dog")
609
+ nf2 = nf.query("`good dog` > 3")
610
+ assert nf.shape == (3, 3)
611
+ assert nf2.shape == (2, 3)
612
+ nf3 = nf.query("`bad dog`.a > 2")
613
+ assert nf3["bad dog"].nest["a"].size == 4
614
+
615
+
597
616
  def test_dropna():
598
617
  """Test that dropna works on all layers"""
599
618
 
@@ -1014,3 +1033,10 @@ def test_eval_assignment():
1014
1033
  assert set(nf.p2.nest.fields) == {"e", "f"}
1015
1034
  assert (nf["p2.e"] == nf["packed.d"] * 2 + nf.c).all()
1016
1035
  assert (nf["p2.f"] == nf["p2.e"] + nf.b).all()
1036
+
1037
+
1038
+ def test_access_non_existing_column():
1039
+ """Test that accessing a non-existing column raises a KeyError"""
1040
+ nf = NestedFrame()
1041
+ with pytest.raises(KeyError):
1042
+ _ = nf["non_existing_column"]
@@ -2,7 +2,6 @@ import numpy as np
2
2
  import pandas as pd
3
3
  import pytest
4
4
  from nested_pandas import NestedFrame
5
- from nested_pandas.nestedframe.utils import extract_nest_names
6
5
  from nested_pandas.utils import count_nested
7
6
 
8
7
 
@@ -52,16 +51,41 @@ def test_check_expr_nesting():
52
51
  used to ensure that an expression-based query does not try to combine base and nested
53
52
  sub-expressions.
54
53
  """
55
- assert extract_nest_names("a > 2 & nested.c > 1") == {"", "nested"}
56
- assert extract_nest_names("(nested.c > 1) and (nested.d>2)") == {"nested"}
57
- assert extract_nest_names("-1.52e-5 < abc < 35.2e2") == {""}
58
- assert extract_nest_names("(n.a > 1) and ((b + c) > (d - 1e-8)) or n.q > c") == {"n", ""}
54
+ base = NestedFrame(data={"a": [1, 2, 3], "b": [2, np.nan, 6]}, index=[0, 1, 2])
55
+ nested = pd.DataFrame(
56
+ data={
57
+ "c": [0, 2, 4, 1, np.nan, 3, 1, 4, 1],
58
+ "d": [5, 4, 7, 5, 3, 1, 9, 3, 4],
59
+ "label": ["b", "a", "b", "b", "a", "a", "b", "a", "b"],
60
+ },
61
+ index=[0, 0, 0, 1, 1, 1, 2, 2, 2],
62
+ )
63
+ b1 = base.add_nested(nested, "nested")
64
+ assert b1.extract_nest_names("a > 2 & nested.c > 1") == {"", "nested"}
65
+ assert b1.extract_nest_names("(nested.c > 1) and (nested.d>2)") == {"nested"}
66
+ assert b1.extract_nest_names("-1.52e-5 < b < 35.2e2") == {""}
67
+
68
+ b2 = base.add_nested(nested.copy(), "n")
69
+ assert b2.extract_nest_names("(n.c > 1) and ((b + a) > (b - 1e-8)) or n.d > a") == {"n", ""}
70
+
71
+ abc = pd.DataFrame(
72
+ data={
73
+ "c": [3, 1, 4, 1, 5, 9, 2, 6, 5],
74
+ "d": [1, 4, 1, 2, 1, 3, 5, 6, 2],
75
+ "g": ["a", "b", "c", "d", "e", "f", "g", "h", "i"],
76
+ },
77
+ index=[0, 0, 0, 1, 1, 1, 2, 2, 2],
78
+ )
79
+ b3 = base.add_nested(abc, "abc").add_nested(abc, "c")
80
+ assert b3.extract_nest_names("abc.c > 2 & c.d < 5") == {"abc", "c"}
81
+
82
+ assert b3.extract_nest_names("(abc.d > 3) & (abc.c == [2, 5])") == {"abc"}
83
+ assert b3.extract_nest_names("(abc.d > 3)&(abc.g == 'f')") == {"abc"}
84
+ assert b3.extract_nest_names("(abc.d > 3) & (abc.g == 'f')") == {"abc"}
59
85
 
60
- assert extract_nest_names("a.b > 2 & c.d < 5") == {"a", "c"}
86
+ assert b1.extract_nest_names("a>3") == {""}
87
+ assert b1.extract_nest_names("a > 3") == {""}
61
88
 
62
- assert extract_nest_names("a>3") == {""}
63
- assert extract_nest_names("a > 3") == {""}
64
- assert extract_nest_names("test.a>5&b==2") == {"test", ""}
65
- assert extract_nest_names("test.a > 5 & b == 2") == {"test", ""}
66
- assert extract_nest_names("(a.b > 3)&(a.c == 'f')") == {"a"}
67
- assert extract_nest_names("(a.b > 3) & (a.c == 'f')") == {"a"}
89
+ b4 = base.add_nested(nested, "test")
90
+ assert b4.extract_nest_names("test.c>5&b==2") == {"test", ""}
91
+ assert b4.extract_nest_names("test.c > 5 & b == 2") == {"test", ""}
@@ -1,61 +0,0 @@
1
- # typing.Self and "|" union syntax don't exist in Python 3.9
2
- from __future__ import annotations
3
-
4
- import ast
5
-
6
-
7
- def _subexprs_by_nest(parents: list[ast.expr], node: ast.expr | None) -> dict[str, list]:
8
- """
9
- Given an expression which contains references to both base and nested
10
- columns, return a dictionary of the sub-expressions that should be
11
- evaluated independently, keyed by nesting context.
12
-
13
- The key of the dictionary is the name of the nested column, and will
14
- be a blank string in the case of base columns. The value is a list
15
- of the parent nodes that lead to sub-expressions that can be evaluated
16
- successfully.
17
-
18
- While this is not in use today for automatically splitting expressions,
19
- it can be used to detect whether an expression is suitably structured
20
- for evaluation: the returned dictionary should have a single key.
21
- """
22
- if not isinstance(node, ast.expr):
23
- return {}
24
- if isinstance(node, ast.Name):
25
- return {"": parents}
26
- if isinstance(node, ast.Attribute) and isinstance(node.value, ast.Name):
27
- return {node.value.id: parents}
28
- sources = (
29
- [getattr(node, "left", None), getattr(node, "right", None)]
30
- + getattr(node, "values", [])
31
- + getattr(node, "comparators", [])
32
- )
33
- result: dict[str, list] = {}
34
- for source in sources:
35
- child = _subexprs_by_nest(parents, source)
36
- for k, v in child.items():
37
- result.setdefault(k, []).append(v)
38
- # After a complete traversal across sources, check for any necessary splits.
39
- # If it's homogenous, move the split-node up the tree.
40
- if len(result) == 1:
41
- # Let the record of each parent node drift up the tree,
42
- # and merge the subtrees into a single node, since by definition,
43
- # this node is homogeneous over all of its children, and can
44
- # be evaluated in a single step.
45
- result = {k: [node] for k in result}
46
- # If the result is either empty or has more than one key, leave the result
47
- # alone. Each key represents a different nest (with a blank string for the base),
48
- # and the value is the highest point in the expression tree where the expression
49
- # was still within a single nest.
50
- return result
51
-
52
-
53
- def extract_nest_names(expr: str) -> set[str]:
54
- """
55
- Given a string expression, parse it and visit the resulting AST, surfacing
56
- the nesting types. The purpose is to identify expressions that attempt
57
- to mix base and nested columns, or columns from two different nests.
58
- """
59
- expr_tree = ast.parse(expr, mode="eval").body
60
- separable = _subexprs_by_nest([], expr_tree)
61
- return set(separable.keys())
File without changes
File without changes
File without changes
File without changes