lf-pollywog 0.1.1__py3-none-any.whl

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.
tests/test_core.py ADDED
@@ -0,0 +1,274 @@
1
+ def test_query_with_external_variable():
2
+ from pollywog.core import CalcSet, Variable, Number
3
+
4
+ a = Variable(name="a", children=["foo"])
5
+ b = Number(name="b1", children=["[a] + 1"])
6
+ c = Number(name="b2", children=["[b1] + 2"])
7
+ cs = CalcSet([a, b, c])
8
+ prefix = "b"
9
+ # Should select items whose name starts with prefix
10
+ result = cs.query("name.startswith(@prefix)")
11
+ names = [item.name for item in result.items]
12
+ assert set(names) == {"b1", "b2"}
13
+
14
+
15
+ def test_query_with_multiple_external_vars():
16
+ from pollywog.core import CalcSet, Variable, Number
17
+
18
+ a = Variable(name="a", children=["foo"])
19
+ b = Number(name="b1", children=["[a] + 1"])
20
+ c = Number(name="b2", children=["[b1] + 2"])
21
+ cs = CalcSet([a, b, c])
22
+ prefix = "b"
23
+ suffix = "2"
24
+ # Should select items whose name starts with prefix and ends with suffix
25
+ result = cs.query("name.startswith(@prefix) and name.endswith(@suffix)")
26
+ names = [item.name for item in result.items]
27
+ assert names == ["b2"]
28
+
29
+
30
+ def test_topological_sort_simple():
31
+ from pollywog.core import Number, Variable, CalcSet
32
+
33
+ a = Variable(name="a", children=["foo"])
34
+ b = Number(name="b", children=["[a] + 1"])
35
+ c = Number(name="c", children=["[b] + 2"])
36
+ cs = CalcSet([c, b, a])
37
+ sorted_cs = cs.topological_sort()
38
+ names = [item.name for item in sorted_cs.items]
39
+ assert names == ["a", "b", "c"]
40
+
41
+
42
+ def test_topological_sort_external_dep():
43
+ from pollywog.core import Number, Variable, CalcSet
44
+
45
+ a = Variable(name="a", children=["foo"])
46
+ b = Number(name="b", children=["[external] + 1"])
47
+ cs = CalcSet([b, a])
48
+ sorted_cs = cs.topological_sort()
49
+ names = [item.name for item in sorted_cs.items]
50
+ assert set(names) == {"a", "b"}
51
+
52
+
53
+ def test_topological_sort_cycle():
54
+ from pollywog.core import Number, CalcSet
55
+
56
+ a = Number(name="a", children=["[b] + 1"])
57
+ b = Number(name="b", children=["[a] + 2"])
58
+ cs = CalcSet([a, b])
59
+ import pytest
60
+
61
+ with pytest.raises(ValueError):
62
+ cs.topological_sort()
63
+
64
+
65
+ def test_item_rename():
66
+ num = Number(name="n1", children=["[x] + 1"])
67
+ # Rename item name only
68
+ num2 = num.rename(name="n2")
69
+ assert num2.name == "n2"
70
+ assert num2.children == ["[x] + 1"]
71
+ # Rename variable inside children
72
+ num3 = num.rename(variables={"x": "y"})
73
+ assert num3.children == ["[y] + 1"]
74
+ # Rename both name and variable
75
+ num4 = num.rename(name="n3", variables={"x": "z"})
76
+ assert num4.name == "n3"
77
+ assert num4.children == ["[z] + 1"]
78
+
79
+
80
+ def test_calcset_rename_items_and_variables():
81
+ num = Number(name="n1", children=["[x] + 1"])
82
+ var = Variable(name="x", children=["foo"])
83
+ filt = Filter(name="f1", children=["[x] > 0"])
84
+ cs = CalcSet([num, var, filt])
85
+ # Rename item names
86
+ cs2 = cs.rename(items={"n1": "n2", "f1": "f2"})
87
+ assert cs2.items[0].name == "n2"
88
+ assert cs2.items[2].name == "f2"
89
+ # Rename variable references in children
90
+ cs3 = cs.rename(variables={"x": "y"})
91
+ assert cs3.items[0].children == ["[y] + 1"]
92
+ assert cs3.items[2].children == ["[y] > 0"]
93
+ # Rename both items and variables
94
+ cs4 = cs.rename(items={"n1": "n3"}, variables={"x": "z"})
95
+ assert cs4.items[0].name == "n3"
96
+ assert cs4.items[0].children == ["[z] + 1"]
97
+ assert cs4.items[2].children == ["[z] > 0"]
98
+
99
+
100
+ def test_rename_with_regex():
101
+ num = Number(name="prefix_n1", children=["[var_x] + 1"])
102
+ var = Variable(name="var_x", children=["foo"])
103
+ cs = CalcSet([num, var])
104
+ # Rename with regex
105
+ cs2 = cs.rename(
106
+ items={r"^prefix_": "renamed_"}, variables={r"^var_": "newvar_"}, regex=True
107
+ )
108
+ assert cs2.items[0].name == "renamed_n1"
109
+ assert cs2.items[0].children == ["[newvar_x] + 1"]
110
+ assert cs2.items[1].name == "newvar_x"
111
+
112
+
113
+ def test_rename_nested_if():
114
+ ifrow = IfRow(condition=["[x] > 0"], value=["[x] + 1"])
115
+ ifexpr = If(rows=[ifrow], otherwise=["[x] - 1"])
116
+ num = Number(name="n1", children=[ifexpr])
117
+ cs = CalcSet([num])
118
+ cs2 = cs.rename(variables={"x": "y"})
119
+ nested_if = cs2.items[0].children[0]
120
+ assert isinstance(nested_if, If)
121
+ assert nested_if.rows[0].condition == ["[y] > 0"]
122
+ assert nested_if.rows[0].value == ["[y] + 1"]
123
+ assert nested_if.otherwise == ["[y] - 1"]
124
+
125
+
126
+ import pytest
127
+ from pollywog.core import CalcSet, Number
128
+
129
+ from pollywog.core import Variable, Filter, If, IfRow, Category
130
+
131
+
132
+ def test_number_to_dict_and_from_dict():
133
+ num = Number(name="n1", children=["1+2"])
134
+ d = num.to_dict()
135
+ num2 = Number.from_dict(d)
136
+ assert num2.name == "n1"
137
+ assert num2.children == ["1+2"]
138
+
139
+
140
+ def test_variable_and_filter():
141
+ var = Variable(name="v1", children=["foo"])
142
+ filt = Filter(name="f1", children=["bar"])
143
+ assert var.to_dict()["type"] == "variable"
144
+ assert filt.to_dict()["type"] == "filter"
145
+
146
+
147
+ def test_category():
148
+ cat = Category(name="cat1", children=["'A'"])
149
+ d = cat.to_dict()
150
+ assert d["calculation_type"] == "string"
151
+
152
+
153
+ def test_ifrow_and_if():
154
+ ifrow = IfRow(condition=["[x] > 0"], value=["1"])
155
+ d = ifrow.to_dict()
156
+ ifrow2 = IfRow.from_dict(d)
157
+ assert ifrow2.condition == ["[x] > 0"]
158
+ assert ifrow2.value == ["1"]
159
+
160
+ ifexpr = If(rows=[ifrow], otherwise=["0"])
161
+ d2 = ifexpr.to_dict()
162
+ ifexpr2 = If.from_dict(d2)
163
+ assert isinstance(ifexpr2, If)
164
+ assert isinstance(ifexpr2.rows[0], IfRow)
165
+ assert ifexpr2.otherwise == ["0"]
166
+
167
+
168
+ def test_calcset_serialization():
169
+ num = Number(name="n1", children=["1+2"])
170
+ var = Variable(name="v1", children=["foo"])
171
+ cs = CalcSet([num, var])
172
+ json_str = cs.to_json()
173
+ cs2 = CalcSet.from_dict(cs.to_dict())
174
+ assert isinstance(cs2, CalcSet)
175
+ assert len(cs2.items) == 2
176
+
177
+
178
+ def test_calcset_repr():
179
+ num = Number(name="n1", children=["1+2"])
180
+ cs = CalcSet([num])
181
+ s = repr(cs)
182
+ assert s.startswith("{")
183
+
184
+
185
+ def test_calcset_add_multiple():
186
+ num1 = Number(name="a", children=["2"])
187
+ num2 = Number(name="b", children=["3"])
188
+ var = Variable(name="v", children=["foo"])
189
+ cs1 = CalcSet([num1])
190
+ cs2 = CalcSet([num2, var])
191
+ cs3 = cs1 + cs2
192
+ assert len(cs3.items) == 3
193
+ assert cs3.items[2].name == "v"
194
+
195
+
196
+ def test_copy_independence():
197
+ num = Number(name="n1", children=["1+2"])
198
+ num_copy = num.copy()
199
+ assert isinstance(num_copy, Number)
200
+ assert num_copy.name == num.name
201
+ assert num_copy.children == num.children
202
+ num_copy.name = "n2"
203
+ num_copy.children[0] = "3+4"
204
+ assert num.name == "n1"
205
+ assert num.children[0] == "1+2"
206
+
207
+ var = Variable(name="v1", children=["foo"])
208
+ var_copy = var.copy()
209
+ var_copy.name = "v2"
210
+ assert var.name == "v1"
211
+
212
+ filt = Filter(name="f1", children=["bar"])
213
+ filt_copy = filt.copy()
214
+ filt_copy.name = "f2"
215
+ assert filt.name == "f1"
216
+
217
+ cat = Category(name="cat1", children=["'A'"])
218
+ cat_copy = cat.copy()
219
+ cat_copy.name = "cat2"
220
+ assert cat.name == "cat1"
221
+
222
+ ifrow = IfRow(condition=["[x] > 0"], value=["1"])
223
+ ifrow_copy = ifrow.copy()
224
+ ifrow_copy.condition[0] = "[x] < 0"
225
+ assert ifrow.condition[0] == "[x] > 0"
226
+
227
+ ifexpr = If(rows=[ifrow], otherwise=["0"])
228
+ ifexpr_copy = ifexpr.copy()
229
+ ifexpr_copy.rows[0].condition[0] = "[x] == 0"
230
+ assert ifexpr.rows[0].condition[0] == "[x] > 0"
231
+
232
+ cs = CalcSet([num, var, filt, cat, ifrow, ifexpr])
233
+ cs_copy = cs.copy()
234
+ cs_copy.items[0].name = "changed"
235
+ assert cs.items[0].name == "n1"
236
+
237
+
238
+ def test_error_handling():
239
+ # Wrong type for CalcSet.from_dict
240
+ with pytest.raises(ValueError):
241
+ CalcSet.from_dict({"type": "not-calcset", "items": []})
242
+ # Unknown item type
243
+ with pytest.raises(ValueError):
244
+ CalcSet.from_dict({"type": "calculation-set", "items": [{"type": "unknown"}]})
245
+
246
+
247
+ def test_ifrow_invalid_type():
248
+ with pytest.raises(ValueError):
249
+ IfRow.from_dict({"type": "not_if_row"})
250
+
251
+
252
+ def test_if_invalid_type():
253
+ with pytest.raises(ValueError):
254
+ If.from_dict({"type": "not_if"})
255
+
256
+
257
+ def test_calcset_to_dict():
258
+ num = Number(name="test_num", children=["1+1"])
259
+ calcset = CalcSet([num])
260
+ d = calcset.to_dict()
261
+ assert d["type"] == "calculation-set"
262
+ assert isinstance(d["items"], list)
263
+ assert d["items"][0]["name"] == "test_num"
264
+
265
+
266
+ def test_calcset_add():
267
+ num1 = Number(name="a", children=["2"])
268
+ num2 = Number(name="b", children=["3"])
269
+ cs1 = CalcSet([num1])
270
+ cs2 = CalcSet([num2])
271
+ cs3 = cs1 + cs2
272
+ assert len(cs3.items) == 2
273
+ assert cs3.items[0].name == "a"
274
+ assert cs3.items[1].name == "b"
tests/test_display.py ADDED
@@ -0,0 +1,60 @@
1
+ import pollywog.display
2
+ import pytest
3
+
4
+ # Dummy CalcSet and Item for testing
5
+ class DummyItem:
6
+ def __init__(self, name, typ, calc_type=None):
7
+ self.name = name
8
+ self.item_type = typ
9
+ self.calculation_type = calc_type
10
+ def to_dict(self):
11
+ d = {"name": self.name, "type": self.item_type}
12
+ if self.calculation_type:
13
+ d["calculation_type"] = self.calculation_type
14
+ return d
15
+
16
+ class DummyCalcSet:
17
+ def __init__(self, items):
18
+ self.items = items
19
+
20
+ # Test set_theme and theme switching
21
+ def test_set_theme():
22
+ pollywog.display.set_theme("dark")
23
+ assert pollywog.display._DISPLAY_THEME == "dark"
24
+ pollywog.display.set_theme("light")
25
+ assert pollywog.display._DISPLAY_THEME == "light"
26
+ with pytest.raises(ValueError):
27
+ pollywog.display.set_theme("unknown")
28
+
29
+ # Test display_calcset does not error and produces HTML
30
+ @pytest.mark.parametrize("theme", ["light", "dark"])
31
+ def test_display_calcset_html(theme):
32
+ pollywog.display.set_theme(theme)
33
+ items = [
34
+ DummyItem("A", "variable"),
35
+ DummyItem("B", "calculation", "number"),
36
+ DummyItem("C", "calculation", "string"),
37
+ DummyItem("D", "filter"),
38
+ ]
39
+ calcset = DummyCalcSet(items)
40
+ # Should not raise
41
+ pollywog.display.display_calcset(calcset)
42
+ # Should produce HTML output (not None)
43
+ # (We can't check the actual rendering, but can check no error)
44
+
45
+ # Optionally, test that the HTML contains expected labels
46
+ @pytest.mark.parametrize("theme,label", [("light", "number"), ("dark", "string")])
47
+ def test_display_calcset_label(theme, label):
48
+ pollywog.display.set_theme(theme)
49
+ items = [DummyItem("B", "calculation", label)]
50
+ calcset = DummyCalcSet(items)
51
+ # Monkeypatch display to capture HTML
52
+ import IPython.display
53
+ captured = {}
54
+ def fake_display(obj):
55
+ captured["html"] = obj.data if hasattr(obj, "data") else str(obj)
56
+ orig_display = IPython.display.display
57
+ IPython.display.display = fake_display
58
+ pollywog.display.display_calcset(calcset)
59
+ IPython.display.display = orig_display
60
+ assert label in captured["html"]
tests/test_helpers.py ADDED
@@ -0,0 +1,72 @@
1
+ from pollywog.helpers import Average, Sum, Product, Normalize, WeightedAverage, Scale, IfElse, CategoryFromThresholds
2
+ from pollywog.core import Number
3
+
4
+ def test_average_helper():
5
+ n = Average("Au", "Ag", name="avg_Au_Ag")
6
+ assert isinstance(n, Number)
7
+ assert n.name == "avg_Au_Ag"
8
+ assert "/ 2" in n.children[0]
9
+ assert "[Au]" in n.children[0] and "[Ag]" in n.children[0]
10
+
11
+ def test_sum_helper():
12
+ n = Sum("Au", "Ag", name="sum_Au_Ag")
13
+ assert isinstance(n, Number)
14
+ assert n.name == "sum_Au_Ag"
15
+ assert "[Au]" in n.children[0] and "[Ag]" in n.children[0]
16
+ assert "+" in n.children[0]
17
+
18
+ def test_product_helper():
19
+ n = Product("Au", "Ag", name="prod_Au_Ag")
20
+ assert isinstance(n, Number)
21
+ assert n.name == "prod_Au_Ag"
22
+ assert "[Au]" in n.children[0] and "[Ag]" in n.children[0]
23
+ assert "*" in n.children[0]
24
+
25
+ def test_normalize_helper():
26
+ n = Normalize("Au", 0, 10, name="norm_Au")
27
+ assert isinstance(n, Number)
28
+ assert n.name == "norm_Au"
29
+ assert "[Au]" in n.children[0]
30
+ assert "/ (10 - 0)" in n.children[0]
31
+
32
+ def test_weighted_average_helper():
33
+ # Test with constant weights
34
+ n = WeightedAverage(["Au", "Ag"], [0.7, 0.3], name="wavg_Au_Ag")
35
+ assert isinstance(n, Number)
36
+ assert n.name == "wavg_Au_Ag"
37
+ assert "[Au] * 0.7" in n.children[0]
38
+ assert "[Ag] * 0.3" in n.children[0]
39
+ assert "/ (0.7 + 0.3)" in n.children[0] or "/ (1.0)" in n.children[0]
40
+
41
+ # Test with variable weights
42
+ n2 = WeightedAverage(["Au", "Ag"], ["w1", "w2"], name="wavg_Au_Ag_varw")
43
+ assert isinstance(n2, Number)
44
+ assert n2.name == "wavg_Au_Ag_varw"
45
+ assert "[Au] * [w1]" in n2.children[0]
46
+ assert "[Ag] * [w2]" in n2.children[0]
47
+ assert "/ ([w1] + [w2])" in n2.children[0]
48
+
49
+ def test_scale_helper():
50
+ n = Scale("Au", 2, name="Au_scaled")
51
+ assert isinstance(n, Number)
52
+ assert n.name == "Au_scaled"
53
+ assert "[Au] * 2" in n.children[0]
54
+
55
+ n2 = Scale("Ag", "factor", name="Ag_scaled")
56
+ assert isinstance(n2, Number)
57
+ assert n2.name == "Ag_scaled"
58
+ assert "[Ag] * [factor]" in n2.children[0]
59
+
60
+ def test_ifelse_helper():
61
+ n = IfElse("[Au] > 1", 2, 0, name="Au_ifelse")
62
+ assert n.name == "Au_ifelse"
63
+ # Should contain If block
64
+ assert hasattr(n, "children")
65
+ assert "If [Au] > 1 then 2 else 0" in n.comment_equation
66
+
67
+ def test_category_from_thresholds_helper():
68
+ n = CategoryFromThresholds("Au", [0.5, 1.0], ["Low", "Medium", "High"], name="Au_class")
69
+ assert n.name == "Au_class"
70
+ # Should contain If block
71
+ assert hasattr(n, "children")
72
+ assert "Classify Au by thresholds [0.5, 1.0]" in n.comment_equation
tests/test_run.py ADDED
@@ -0,0 +1,71 @@
1
+ import pytest
2
+ from pollywog.core import CalcSet, Variable, Number, If, IfRow
3
+ from pollywog.run import run_calcset
4
+
5
+
6
+ def test_import_run():
7
+ # Just test that run.py imports without error
8
+ assert True
9
+
10
+
11
+ def test_run_calcset_with_dict():
12
+ a = Variable(name="a", children=[""])
13
+ b = Number(name="b", children=["[a] + 1"])
14
+ c = Number(name="c", children=["[b] * 2"])
15
+ cs = CalcSet([a, b, c])
16
+ inputs = {"a": 3}
17
+ results = run_calcset(cs, inputs=inputs)
18
+ assert "a" not in results # Variable should not be in output by default
19
+ assert results["b"] == 4
20
+ assert results["c"] == 8
21
+ # Debug mode: output_variables=True
22
+ debug_results = run_calcset(cs, inputs=inputs, output_variables=True)
23
+ assert debug_results["a"] == 3
24
+ assert debug_results["b"] == 4
25
+ assert debug_results["c"] == 8
26
+
27
+
28
+ def test_run_calcset_with_if():
29
+ a = Variable(name="a", children=[""])
30
+ ifrow1 = IfRow(condition=["[a] > 0"], value=["1"])
31
+ ifrow2 = IfRow(condition=["[a] <= 0"], value=["-1"])
32
+ ifexpr = If(rows=[ifrow1, ifrow2], otherwise=["0"])
33
+ b = Number(name="b", children=[ifexpr])
34
+ cs = CalcSet([a, b])
35
+ results = run_calcset(cs, inputs={"a": 2})
36
+ assert "a" not in results
37
+ assert results["b"] == 1
38
+ results = run_calcset(cs, inputs={"a": -2})
39
+ assert results["b"] == -1
40
+ results = run_calcset(cs, inputs={"a": 0})
41
+ assert results["b"] == -1
42
+ # Debug mode
43
+ debug_results = run_calcset(cs, inputs={"a": 2}, output_variables=True)
44
+ assert debug_results["a"] == 2
45
+ assert debug_results["b"] == 1
46
+
47
+
48
+ def test_run_calcset_with_dataframe():
49
+ import pandas as pd
50
+
51
+ a = Variable(name="a", children=[""])
52
+ b = Number(name="b", children=["[a] + 1"])
53
+ cs = CalcSet([a, b])
54
+ df = pd.DataFrame({"a": [1, 2, 3]})
55
+ result_df = run_calcset(cs, dataframe=df)
56
+ assert list(result_df["b"]) == [2, 3, 4]
57
+ assert "a" not in result_df.columns # Variable column should be dropped
58
+ # Debug mode
59
+ debug_df = run_calcset(cs, dataframe=df, output_variables=True)
60
+ assert "a" in debug_df.columns
61
+ assert list(debug_df["a"]) == [1, 2, 3]
62
+
63
+
64
+ def test_pw_accessor():
65
+ import pandas as pd
66
+
67
+ b = Number(name="b", children=["[a] + 1"])
68
+ cs = CalcSet([b])
69
+ df = pd.DataFrame({"a": [10, 20]})
70
+ result_df = df.pw.run(cs)
71
+ assert list(result_df["b"]) == [11, 21]
tests/test_utils.py ADDED
@@ -0,0 +1,32 @@
1
+ from pollywog.utils import ensure_list, ensure_str_list, to_dict
2
+
3
+
4
+ class Dummy:
5
+ def to_dict(self):
6
+ return {"dummy": True}
7
+
8
+
9
+ def test_ensure_list():
10
+ assert ensure_list(1) == [1]
11
+ assert ensure_list([1, 2]) == [1, 2]
12
+
13
+
14
+ def test_ensure_str_list():
15
+ assert ensure_str_list("foo") == ["foo"]
16
+ assert ensure_str_list(["foo"]) == ["foo"]
17
+ assert ensure_str_list([1]) == ["", 1, ""]
18
+ assert ensure_str_list(["foo", 1]) == ["foo", 1, ""]
19
+ assert ensure_str_list(["foo", "bar"]) == ["foo", "bar"]
20
+
21
+
22
+ def test_to_dict():
23
+ d = Dummy()
24
+ assert to_dict(d) == [{"dummy": True}]
25
+ assert to_dict([d, d]) == [{"dummy": True}, {"dummy": True}]
26
+ assert to_dict(["a", "b"]) == ["a", "b"]
27
+ assert to_dict(["a", d]) == ["a", {"dummy": True}]
28
+ # guard_strings True
29
+ assert to_dict([d], guard_strings=True) == ["", {"dummy": True}, ""]
30
+ assert to_dict(["a", d], guard_strings=True) == ["a", {"dummy": True}, ""]
31
+ assert to_dict([d, "a"], guard_strings=True) == ["", {"dummy": True}, "a"]
32
+ assert to_dict(["a", "b"], guard_strings=True) == ["a", "b"]