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.
- {nested_pandas-0.3.0/src/nested_pandas.egg-info → nested_pandas-0.3.2}/PKG-INFO +1 -1
- {nested_pandas-0.3.0 → nested_pandas-0.3.2}/src/nested_pandas/_version.py +2 -2
- {nested_pandas-0.3.0 → nested_pandas-0.3.2}/src/nested_pandas/nestedframe/core.py +98 -3
- {nested_pandas-0.3.0 → nested_pandas-0.3.2/src/nested_pandas.egg-info}/PKG-INFO +1 -1
- {nested_pandas-0.3.0 → nested_pandas-0.3.2}/src/nested_pandas.egg-info/SOURCES.txt +0 -1
- {nested_pandas-0.3.0 → nested_pandas-0.3.2}/tests/nested_pandas/nestedframe/test_nestedframe.py +26 -0
- {nested_pandas-0.3.0 → nested_pandas-0.3.2}/tests/nested_pandas/utils/test_utils.py +36 -12
- nested_pandas-0.3.0/src/nested_pandas/nestedframe/utils.py +0 -61
- {nested_pandas-0.3.0 → nested_pandas-0.3.2}/.copier-answers.yml +0 -0
- {nested_pandas-0.3.0 → nested_pandas-0.3.2}/.git_archival.txt +0 -0
- {nested_pandas-0.3.0 → nested_pandas-0.3.2}/.gitattributes +0 -0
- {nested_pandas-0.3.0 → nested_pandas-0.3.2}/.github/ISSUE_TEMPLATE/0-general_issue.md +0 -0
- {nested_pandas-0.3.0 → nested_pandas-0.3.2}/.github/ISSUE_TEMPLATE/1-bug_report.md +0 -0
- {nested_pandas-0.3.0 → nested_pandas-0.3.2}/.github/ISSUE_TEMPLATE/2-feature_request.md +0 -0
- {nested_pandas-0.3.0 → nested_pandas-0.3.2}/.github/dependabot.yml +0 -0
- {nested_pandas-0.3.0 → nested_pandas-0.3.2}/.github/pull_request_template.md +0 -0
- {nested_pandas-0.3.0 → nested_pandas-0.3.2}/.github/workflows/asv-main.yml +0 -0
- {nested_pandas-0.3.0 → nested_pandas-0.3.2}/.github/workflows/asv-nightly.yml +0 -0
- {nested_pandas-0.3.0 → nested_pandas-0.3.2}/.github/workflows/asv-pr.yml +0 -0
- {nested_pandas-0.3.0 → nested_pandas-0.3.2}/.github/workflows/build-documentation.yml +0 -0
- {nested_pandas-0.3.0 → nested_pandas-0.3.2}/.github/workflows/pre-commit-ci.yml +0 -0
- {nested_pandas-0.3.0 → nested_pandas-0.3.2}/.github/workflows/publish-benchmarks-pr.yml +0 -0
- {nested_pandas-0.3.0 → nested_pandas-0.3.2}/.github/workflows/publish-to-pypi.yml +0 -0
- {nested_pandas-0.3.0 → nested_pandas-0.3.2}/.github/workflows/smoke-test.yml +0 -0
- {nested_pandas-0.3.0 → nested_pandas-0.3.2}/.github/workflows/testing-and-coverage.yml +0 -0
- {nested_pandas-0.3.0 → nested_pandas-0.3.2}/.gitignore +0 -0
- {nested_pandas-0.3.0 → nested_pandas-0.3.2}/.pre-commit-config.yaml +0 -0
- {nested_pandas-0.3.0 → nested_pandas-0.3.2}/.readthedocs.yml +0 -0
- {nested_pandas-0.3.0 → nested_pandas-0.3.2}/.setup_dev.sh +0 -0
- {nested_pandas-0.3.0 → nested_pandas-0.3.2}/LICENSE +0 -0
- {nested_pandas-0.3.0 → nested_pandas-0.3.2}/README.md +0 -0
- {nested_pandas-0.3.0 → nested_pandas-0.3.2}/benchmarks/README.md +0 -0
- {nested_pandas-0.3.0 → nested_pandas-0.3.2}/benchmarks/__init__.py +0 -0
- {nested_pandas-0.3.0 → nested_pandas-0.3.2}/benchmarks/asv.conf.json +0 -0
- {nested_pandas-0.3.0 → nested_pandas-0.3.2}/benchmarks/benchmarks.py +0 -0
- {nested_pandas-0.3.0 → nested_pandas-0.3.2}/docs/Makefile +0 -0
- {nested_pandas-0.3.0 → nested_pandas-0.3.2}/docs/_static/custom.css +0 -0
- {nested_pandas-0.3.0 → nested_pandas-0.3.2}/docs/conf.py +0 -0
- {nested_pandas-0.3.0 → nested_pandas-0.3.2}/docs/gettingstarted/contributing.rst +0 -0
- {nested_pandas-0.3.0 → nested_pandas-0.3.2}/docs/gettingstarted/installation.rst +0 -0
- {nested_pandas-0.3.0 → nested_pandas-0.3.2}/docs/gettingstarted/quickstart.ipynb +0 -0
- {nested_pandas-0.3.0 → nested_pandas-0.3.2}/docs/gettingstarted.rst +0 -0
- {nested_pandas-0.3.0 → nested_pandas-0.3.2}/docs/index.rst +0 -0
- {nested_pandas-0.3.0 → nested_pandas-0.3.2}/docs/requirements.txt +0 -0
- {nested_pandas-0.3.0 → nested_pandas-0.3.2}/docs/tutorials/README.md +0 -0
- {nested_pandas-0.3.0 → nested_pandas-0.3.2}/docs/tutorials/data_loading_notebook.ipynb +0 -0
- {nested_pandas-0.3.0 → nested_pandas-0.3.2}/docs/tutorials/data_manipulation.ipynb +0 -0
- {nested_pandas-0.3.0 → nested_pandas-0.3.2}/docs/tutorials/low_level.ipynb +0 -0
- {nested_pandas-0.3.0 → nested_pandas-0.3.2}/docs/tutorials/nested_spectra.ipynb +0 -0
- {nested_pandas-0.3.0 → nested_pandas-0.3.2}/docs/tutorials.rst +0 -0
- {nested_pandas-0.3.0 → nested_pandas-0.3.2}/nestedframe.png +0 -0
- {nested_pandas-0.3.0 → nested_pandas-0.3.2}/pyproject.toml +0 -0
- {nested_pandas-0.3.0 → nested_pandas-0.3.2}/setup.cfg +0 -0
- {nested_pandas-0.3.0 → nested_pandas-0.3.2}/src/nested_pandas/__init__.py +0 -0
- {nested_pandas-0.3.0 → nested_pandas-0.3.2}/src/nested_pandas/datasets/__init__.py +0 -0
- {nested_pandas-0.3.0 → nested_pandas-0.3.2}/src/nested_pandas/datasets/generation.py +0 -0
- {nested_pandas-0.3.0 → nested_pandas-0.3.2}/src/nested_pandas/nestedframe/__init__.py +0 -0
- {nested_pandas-0.3.0 → nested_pandas-0.3.2}/src/nested_pandas/nestedframe/io.py +0 -0
- {nested_pandas-0.3.0 → nested_pandas-0.3.2}/src/nested_pandas/py.typed +0 -0
- {nested_pandas-0.3.0 → nested_pandas-0.3.2}/src/nested_pandas/series/__init__.py +0 -0
- {nested_pandas-0.3.0 → nested_pandas-0.3.2}/src/nested_pandas/series/accessor.py +0 -0
- {nested_pandas-0.3.0 → nested_pandas-0.3.2}/src/nested_pandas/series/dtype.py +0 -0
- {nested_pandas-0.3.0 → nested_pandas-0.3.2}/src/nested_pandas/series/ext_array.py +0 -0
- {nested_pandas-0.3.0 → nested_pandas-0.3.2}/src/nested_pandas/series/packer.py +0 -0
- {nested_pandas-0.3.0 → nested_pandas-0.3.2}/src/nested_pandas/series/utils.py +0 -0
- {nested_pandas-0.3.0 → nested_pandas-0.3.2}/src/nested_pandas/utils/__init__.py +0 -0
- {nested_pandas-0.3.0 → nested_pandas-0.3.2}/src/nested_pandas/utils/utils.py +0 -0
- {nested_pandas-0.3.0 → nested_pandas-0.3.2}/src/nested_pandas.egg-info/dependency_links.txt +0 -0
- {nested_pandas-0.3.0 → nested_pandas-0.3.2}/src/nested_pandas.egg-info/requires.txt +0 -0
- {nested_pandas-0.3.0 → nested_pandas-0.3.2}/src/nested_pandas.egg-info/top_level.txt +0 -0
- {nested_pandas-0.3.0 → nested_pandas-0.3.2}/tests/nested_pandas/conftest.py +0 -0
- {nested_pandas-0.3.0 → nested_pandas-0.3.2}/tests/nested_pandas/datasets/test_generation.py +0 -0
- {nested_pandas-0.3.0 → nested_pandas-0.3.2}/tests/nested_pandas/e2e_tests/test_issue89.py +0 -0
- {nested_pandas-0.3.0 → nested_pandas-0.3.2}/tests/nested_pandas/nestedframe/test_io.py +0 -0
- {nested_pandas-0.3.0 → nested_pandas-0.3.2}/tests/nested_pandas/series/test_accessor.py +0 -0
- {nested_pandas-0.3.0 → nested_pandas-0.3.2}/tests/nested_pandas/series/test_dtype.py +0 -0
- {nested_pandas-0.3.0 → nested_pandas-0.3.2}/tests/nested_pandas/series/test_ext_array.py +0 -0
- {nested_pandas-0.3.0 → nested_pandas-0.3.2}/tests/nested_pandas/series/test_packer.py +0 -0
- {nested_pandas-0.3.0 → nested_pandas-0.3.2}/tests/nested_pandas/test_packaging.py +0 -0
|
@@ -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
|
-
|
|
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)
|
|
@@ -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
|
{nested_pandas-0.3.0 → nested_pandas-0.3.2}/tests/nested_pandas/nestedframe/test_nestedframe.py
RENAMED
|
@@ -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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
|
86
|
+
assert b1.extract_nest_names("a>3") == {""}
|
|
87
|
+
assert b1.extract_nest_names("a > 3") == {""}
|
|
61
88
|
|
|
62
|
-
|
|
63
|
-
assert extract_nest_names("
|
|
64
|
-
assert extract_nest_names("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
|
|
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
|
|
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
|
|
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
|
|
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
|