diff --git a/path_dict/__init__.py b/path_dict/__init__.py index 9a86499..75092c9 100644 --- a/path_dict/__init__.py +++ b/path_dict/__init__.py @@ -1,4 +1,4 @@ -from . path_dict import PathDict +from .path_dict import PathDict pd = PathDict diff --git a/path_dict/multi_path_dict.py b/path_dict/multi_path_dict.py index 3062949..9328354 100644 --- a/path_dict/multi_path_dict.py +++ b/path_dict/multi_path_dict.py @@ -1,29 +1,27 @@ from __future__ import annotations + from typing import Any, Callable -from . path import Path -from . path_dict import PathDict + +from .path import Path +from .path_dict import PathDict class MultiPathDict: path_handle: Path root_data: dict | list - def __init__(self, data: dict | list, path: Path): self.path_handle = path self.root_data = data - def __repr__(self) -> str: return f"MultiPathDict({self.root_data = }, {self.path_handle = })" - ############################################################################ # Setters # Setters ALWAYS return a value ############################################################################ - def gather(self, as_type="list", include_paths=False) -> dict | list: """ Get the actual value at the given path. @@ -49,7 +47,6 @@ def gather(self, as_type="list", include_paths=False) -> dict | list: res[tuple(path.path)] = handle.at(path.path).get() return res - def gather_pd(self, as_type="list", include_paths=False) -> PathDict: data = self.gather(as_type=as_type, include_paths=include_paths) return PathDict.from_data_and_path(data, self.path_handle.copy(replace_path=[])) @@ -59,7 +56,6 @@ def gather_pd(self, as_type="list", include_paths=False) -> PathDict: # Setters ALWAYS return a handle, not the value. ############################################################################ - def map(self, f: Callable) -> PathDict: """ Map the result of f to the value at path previously set by ".at(path)". @@ -70,19 +66,16 @@ def map(self, f: Callable) -> PathDict: PathDict.from_data_and_path(self.root_data, path).map(f) return PathDict.from_data_and_path(self.root_data, self.path_handle) - def reduce(self, f: Callable, aggregate: Any, as_type="list", include_paths=False) -> Any: """ Get all values of the given multi-path, and reduce them using f. """ return self.gather_pd(as_type=as_type, include_paths=include_paths).reduce(f, aggregate) - ############################################################################ #### Filter ############################################################################ - def filter(self, f: Callable, as_type="list", include_paths=False) -> PathDict: """ At the current path only keep the elements for which f(key, value) @@ -90,29 +83,24 @@ def filter(self, f: Callable, as_type="list", include_paths=False) -> PathDict: """ return self.gather_pd(as_type=as_type, include_paths=include_paths).filter(f) - # def filtered(self, f: Callable[[Any], bool], as_type="list", include_paths=False) -> PathDict: # raise NotImplementedError - ############################################################################ #### Useful shorthands ############################################################################ - def sum(self) -> Any: """ Sum all values at the given multi-path. """ return sum(self.gather()) - def set(self, value: Any) -> PathDict: for path in self.path_handle.expand(self.root_data): PathDict.from_data_and_path(self.root_data, path).set(value) return self - ############################################################################ #### Standard dict methods ############################################################################ diff --git a/path_dict/path.py b/path_dict/path.py index 0d92e0d..8aa2d1e 100644 --- a/path_dict/path.py +++ b/path_dict/path.py @@ -1,12 +1,12 @@ from __future__ import annotations -from . utils import get_nested_keys_or_indices + +from .utils import get_nested_keys_or_indices class Path: path: list[str] raw: bool - def __init__(self, *path, raw=False): # Careful, if the kwargs are passed as positional agrs, they are part of the path self.raw = raw @@ -22,35 +22,28 @@ def __init__(self, *path, raw=False): # Clean up empty strings self.path = [x for x in self.path if x != ""] - def __repr__(self) -> str: return f"Path(path={self.path}, raw={self.raw})" - @property def has_wildcards(self): return "*" in self.path - def __iter__(self): - """ Iterate over path keys using a for in loop """ + """Iterate over path keys using a for in loop""" return iter(self.path) - def __len__(self): return len(self.path) - def __getitem__(self, key): return self.path[key] - def copy(self, replace_path=None, replace_raw=None) -> Path: path_copy = list(self.path) if replace_path is None else replace_path raw_copy = self.raw if replace_raw is None else replace_raw return Path(path_copy, raw=raw_copy) - def expand(self, ref: dict | list) -> list[Path]: """ Expand the path to list[Path], using the ref as a reference. diff --git a/path_dict/path_dict.py b/path_dict/path_dict.py index bff3419..b0be5f6 100644 --- a/path_dict/path_dict.py +++ b/path_dict/path_dict.py @@ -1,16 +1,18 @@ from __future__ import annotations -from typing import Any, Callable + import copy +import json +from typing import Any, Callable, Union + from . import utils -from . path import Path +from .path import Path class PathDict: root_data: dict | list | Any - data: dict | list | Any + data: dict | list | Any path_handle: Path - def __init__(self, data: dict | list, raw=False, path: Path = None): """ A PathDict always refers to a dict or list. @@ -18,14 +20,10 @@ def __init__(self, data: dict | list, raw=False, path: Path = None): When initialized, the current path is the root path. """ if not isinstance(data, (dict, list)): - raise TypeError( - f"PathDict init: data must be dict or list but is {type(data)} " - f"({data})" - ) + raise TypeError(f"PathDict init: data must be dict or list but is {type(data)} " f"({data})") self.data = data self.path_handle = Path([], raw=raw) if path is None else path - @classmethod def from_data_and_path(cls, data: dict | list, path: Path) -> PathDict: """ @@ -36,10 +34,8 @@ def from_data_and_path(cls, data: dict | list, path: Path) -> PathDict: """ return cls(data=data, path=path) - def __repr__(self) -> str: - return f"PathDict({self.data = }, {self.path_handle = })" - + return f"PathDict({json.dumps(self.data, indent=4, sort_keys=True)}, {self.path_handle = })" def deepcopy(self, from_root=False, true_deepcopy=False) -> PathDict: """ @@ -57,7 +53,6 @@ def deepcopy(self, from_root=False, true_deepcopy=False) -> PathDict: data_copy = copy.deepcopy(data) if true_deepcopy else utils.fast_deepcopy(data) return PathDict.from_data_and_path(data_copy, path) - def copy(self, from_root=False) -> PathDict: """ Return a shallow copy of the data at the current path or from the root. @@ -72,13 +67,11 @@ def copy(self, from_root=False) -> PathDict: data_copy = copy.copy(self.data if from_root else self.get()) return PathDict.from_data_and_path(data_copy, path) - ############################################################################ # Moving the handle ############################################################################ - - def at(self, *path, raw=None) -> PathDict | MultiPathDict: + def at(self, *path, raw=None) -> Union[PathDict, MultiPathDict]: """ Calling at(path) moves the handle to the given path, and returns the handle. @@ -106,7 +99,6 @@ def at(self, *path, raw=None) -> PathDict | MultiPathDict: return MultiPathDict(self.data, self.path_handle) return self - def at_root(self) -> PathDict: """ Move the handle back to the root of the data and return it. @@ -121,14 +113,12 @@ def at_root(self) -> PathDict: """ return self.at() - def at_parent(self) -> PathDict: """ Move the handle to the parent of the current path and return it. """ return self.at(self.path_handle.path[:-1]) - def at_children(self) -> MultiPathDict: """ Return a MultiPathDict that refers to all the children of the current @@ -136,13 +126,11 @@ def at_children(self) -> MultiPathDict: """ return self.at(self.path_handle.path + ["*"]) - ############################################################################ # Getters # Getters ALWAYS return actual values, not handles. ############################################################################ - def get(self, default=None) -> dict | list | Any: """ Get the actual value at the given path. @@ -174,13 +162,11 @@ def get(self, default=None) -> dict | list | Any: return default return current - ############################################################################ # Setters # Setters ALWAYS return a handle, not the value. ############################################################################ - def set(self, value) -> PathDict: # Setting nothing is a no-op if value is None: @@ -218,7 +204,6 @@ def set(self, value) -> PathDict: return self - def map(self, f: Callable) -> PathDict: """ Map the result of f to the value at path previously set by ".at(path)". @@ -228,7 +213,6 @@ def map(self, f: Callable) -> PathDict: self.set(f(self.get())) return self - def mapped(self, f: Callable) -> PathDict: """ Makes a fast deepcopy of your root data, moves the handle to the previously @@ -237,13 +221,11 @@ def mapped(self, f: Callable) -> PathDict: current_handle = self.path_handle return self.deepcopy(from_root=True).at(current_handle.path).map(f) - ############################################################################ # Filter # Filter ALWAYS return a handle, not the value. ############################################################################ - def filter(self, f: Callable) -> PathDict: """ At the current path only keep the elements for which f(key, value) @@ -256,8 +238,6 @@ def filter(self, f: Callable) -> PathDict: return self.set([x for x in get_at_current if f(x)]) raise TypeError("PathDict filter: must be applied to a dict or list") - - def filtered(self, f: Callable) -> PathDict: """ Shortcut for: @@ -265,12 +245,10 @@ def filtered(self, f: Callable) -> PathDict: """ return self.copy().filter(f) - ############################################################################ # Reduce ############################################################################ - def reduce(self, f: Callable, aggregate=None) -> Any: """ Reduce a value starting with init at the given path. @@ -292,12 +270,10 @@ def reduce(self, f: Callable, aggregate=None) -> Any: return agg raise TypeError("PathDict reduce: must be applied to a dict or list") - ############################################################################ #### Useful Shorthands ############################################################################ - def sum(self) -> Any: """ Sum the elements at the given path. @@ -309,34 +285,28 @@ def sum(self) -> Any: return sum(get_at_current) raise TypeError("PathDict sum: must be applied to a dict or list") - def append(self, value) -> PathDict: """ Append the value to the list at the given path. """ return self.map(lambda l: (l or []) + [value]) - def update(self, value) -> PathDict: """ Update the dict at the given path with the given value. """ return self.map(lambda d: {**d, **value}) - ############################################################################ #### Standard dict methods ############################################################################ - def keys(self): return list(self.get().keys()) - def values(self): return list(self.get().values()) - def items(self): return self.get().items() @@ -346,7 +316,6 @@ def pop(self, key, default=None): def __len__(self): return len(self.get()) - def __getitem__(self, path): at = self.at(*path) if isinstance(path, tuple) else self.at(path) if isinstance(at, MultiPathDict): @@ -355,13 +324,11 @@ def __getitem__(self, path): self.at_root() return res - def __setitem__(self, path, value): at = self.at(*path) if isinstance(path, tuple) else self.at(path) at.map(value) if callable(value) else at.set(value) self.at_root() - def __contains__(self, *path): try: contains = self.at(*path).get() is not None @@ -370,7 +337,6 @@ def __contains__(self, *path): except KeyError: return False - def __iter__(self): return iter(self.keys()) diff --git a/path_dict/utils.py b/path_dict/utils.py index 5dad8a6..f0d7845 100644 --- a/path_dict/utils.py +++ b/path_dict/utils.py @@ -1,4 +1,5 @@ from __future__ import annotations + from typing import Any @@ -44,8 +45,7 @@ def guarded_get(current: dict | list, key: Any): if isinstance(current, list): return safe_list_get(current, key) raise KeyError( - f"PathDict: The path is not a stack of nested dicts and lists " - f"(value at key {key} has type {type(current)})" + f"PathDict: The path is not a stack of nested dicts and lists " f"(value at key {key} has type {type(current)})" ) @@ -89,6 +89,5 @@ def get_nested_keys_or_indices(ref: dict | list, path: list): if isinstance(current, list): return list(range(len(current))) raise KeyError( - f"PathDict: The path is not a stack of nested dicts and lists " - f"(value at key {key} has type {type(current)})" + f"PathDict: The path is not a stack of nested dicts and lists " f"(value at key {key} has type {type(current)})" ) diff --git a/tests/dummy_data.py b/tests/dummy_data.py index 20d914a..3f8ddf8 100644 --- a/tests/dummy_data.py +++ b/tests/dummy_data.py @@ -1,4 +1,3 @@ - def get_users(): return { "total_users": 3, @@ -10,7 +9,7 @@ def get_users(): }, "2": { "name": "Ben", - "age": 49 + "age": 49, }, "3": { "name": "Sue", @@ -21,7 +20,7 @@ def get_users(): ["Ben", "Sue"], ["Joe", "Ben"], ["Ben", "Joe"], - ] + ], } diff --git a/tests/test_PDHandle.py b/tests/test_PDHandle.py index fd0c873..93ceceb 100644 --- a/tests/test_PDHandle.py +++ b/tests/test_PDHandle.py @@ -1,11 +1,10 @@ -from path_dict import pd import pytest -from tests import dummy_data +from path_dict import pd +from tests import dummy_data def test_initialization(): - # Empty assert pd({}).get() == {} @@ -98,12 +97,12 @@ def test_deepcopy(): assert pd(j).at("1").get() is j["1"] # deepcopy at root - assert pd(j).deepcopy().get() == j + assert pd(j).deepcopy().get() == j assert pd(j).deepcopy().get() is not j # deepcopy at path assert pd(j).at("1").deepcopy().get() is not j["1"] - assert pd(j).at("1").deepcopy().get() == j["1"] + assert pd(j).at("1").deepcopy().get() == j["1"] assert pd(j).deepcopy().at("1").get() is not j["1"] # deepcopy from root at path @@ -123,11 +122,11 @@ def test_copy(): j = {"1": {"2": 3}} assert pd(j).at("1").get() is j["1"] - assert pd(j).copy().get() == j + assert pd(j).copy().get() == j assert pd(j).copy().get() is not j assert pd(j).at("1").copy().get() is not j["1"] - assert pd(j).at("1").copy().get() == j["1"] + assert pd(j).at("1").copy().get() == j["1"] assert pd(j).copy().at("1").get() is j["1"] assert pd(j).at("1").copy(from_root=True).at().get() == j @@ -140,9 +139,7 @@ def test_copy(): assert dc_pd is not dc_pd_copy - def test_contains(): - users_dict = dummy_data.get_users() users_pd = pd(users_dict) assert "total_users" in users_pd @@ -158,12 +155,14 @@ def test_contains(): assert ["users", "1", "name", "joe", "Doe"] not in users_pd # too many paths - def test_nested_object_copy(): # Test copy with object - class TestObject(): - def __init__(self, data): self.data = data - def __repr__(self): return f"TestObject({self.data})" + class TestObject: + def __init__(self, data): + self.data = data + + def __repr__(self): + return f"TestObject({self.data})" o = pd({}) o["test", "test"] = TestObject({"1": "2"}) @@ -190,9 +189,6 @@ def __repr__(self): return f"TestObject({self.data})" assert fdc.at("test", "test").get() is o.at("test", "test").get() - - - def test_get_path(): users_dict = dummy_data.get_users() users_pd = pd(users_dict) @@ -215,8 +211,6 @@ def test_get_path(): assert users_pd["users", "1", "*"] == ["Joe", 22] - - def test_set_path(): assert pd(["1", 2]).set([3, "4"]).get() == [3, "4"] @@ -236,8 +230,6 @@ def test_set_path(): with pytest.raises(TypeError): p.at().set("Not Allowed") - - p = pd({"l1": [1, 2, 3]}) with pytest.raises(KeyError): p.at(["l1", "nonexistent"]).set(4) @@ -257,15 +249,11 @@ def test_map(): def test_mapped(): - j = { - "1": {"2": 3}, - "a": {"b": "c"} - } + j = {"1": {"2": 3}, "a": {"b": "c"}} p = pd(j).at("1", "2").mapped(lambda x: x + 1).at().get() p2 = pd(j).deepcopy().at("1", "2").map(lambda x: x + 1).at().get() - assert j["1"]["2"] == 3 assert p["1"]["2"] == 4 assert p2["1"]["2"] == 4 @@ -295,7 +283,7 @@ def test_filter_behavior_spec(): "2": "20", "3": "30", "4": "40", - } + }, } p = pd(j) p.at("1").filter(lambda k, v: int(k) > 3) @@ -308,29 +296,14 @@ def test_filter_behavior_spec(): p.at("a").filter(lambda x: x) - def test_filter(): users_pd = pd(dummy_data.get_users()) users_below_30 = users_pd.deepcopy().at("users").filtered(lambda k, v: v["age"] <= 30) - assert users_below_30.get() == { - "1": { - "age": 22, - "name": "Joe" - } - } + assert users_below_30.get() == {"1": {"age": 22, "name": "Joe"}} premium_users = users_pd.deepcopy().at("users").filtered(lambda k, v: int(k) in users_pd["premium_users"]) - assert premium_users.get() == { - "1": { - "age": 22, - "name": "Joe" - }, - "3": { - "age": 32, - "name": "Sue" - } - } + assert premium_users.get() == {"1": {"age": 22, "name": "Joe"}, "3": {"age": 32, "name": "Sue"}} follows_includes_joe = users_pd.at("follows").filtered(lambda e: "Joe" in e) assert isinstance(follows_includes_joe.get(), list) @@ -340,7 +313,6 @@ def test_filter(): ] - def test_reduce(): users_pd = pd(dummy_data.get_users()) @@ -398,7 +370,6 @@ def test_pop(): def test_iter(): - p = pd({"a": 1, "b": 2, "c": 3}) keys = [] diff --git a/tests/test_PDMultiHandle.py b/tests/test_PDMultiHandle.py index 10a717e..ae979bf 100644 --- a/tests/test_PDMultiHandle.py +++ b/tests/test_PDMultiHandle.py @@ -1,7 +1,7 @@ -from path_dict import pd import pytest -from tests import dummy_data +from path_dict import pd +from tests import dummy_data def test_get_all(): @@ -63,27 +63,26 @@ def test_get_all(): p.at("*", "*").gather(as_type="invalid") - - - def test_get_all_2(): - p = pd({ - "1": { - "name": "Joe", - "age": 22, - "interests": ["Python", "C++", "C#"], - }, - "2": { - "name": "Ben", - "age": 49, - "interests": ["Javascript", "C++", "Haskell"], - }, - "3": { - "name": "Sue", - "age": 36, - "interests": ["Python", "C++", "C#"], - }, - }) + p = pd( + { + "1": { + "name": "Joe", + "age": 22, + "interests": ["Python", "C++", "C#"], + }, + "2": { + "name": "Ben", + "age": 49, + "interests": ["Javascript", "C++", "Haskell"], + }, + "3": { + "name": "Sue", + "age": 36, + "interests": ["Python", "C++", "C#"], + }, + } + ) ages = p.at(["*", "age"]).gather() assert ages == [22, 49, 36] @@ -103,45 +102,43 @@ def test_get_all_2(): # ] - def test_get_all_3(): - p = pd({ - "1": [2, 3, 4], - "2": "3", - }) + p = pd( + { + "1": [2, 3, 4], + "2": "3", + } + ) assert p.at("1", "*").gather() == [2, 3, 4] with pytest.raises(KeyError): p.at("2", "*").gather() - - - def test_gather(): - winners_original = pd({ - "2017": { - "podium": { - "17-place-1": {"name": "Joe", "age": 22}, - "17-place-2": {"name": "Ben", "age": 13}, - "17-place-3": {"name": "Sue", "age": 98}, + winners_original = pd( + { + "2017": { + "podium": { + "17-place-1": {"name": "Joe", "age": 22}, + "17-place-2": {"name": "Ben", "age": 13}, + "17-place-3": {"name": "Sue", "age": 98}, + }, + "prices_list": ["Car", "Bike", "Plane"], }, - "prices_list": ["Car", "Bike", "Plane"], - }, - "2018": { - "podium": { - "18-place-1": {"name": "Bernd", "age": 50}, - "18-place-2": {"name": "Sara", "age": 32}, - "18-place-3": {"name": "Jan", "age": 26}, + "2018": { + "podium": { + "18-place-1": {"name": "Bernd", "age": 50}, + "18-place-2": {"name": "Sara", "age": 32}, + "18-place-3": {"name": "Jan", "age": 26}, + }, + "prices_list": ["Beer", "Coffee", "Cigarette"], }, - "prices_list": ["Beer", "Coffee", "Cigarette"], - }, - }) + } + ) # Get names of all winners winners = winners_original.deepcopy(from_root=True) - assert winners.at("*", "podium", "*", "name").gather() == [ - "Joe", "Ben", "Sue", "Bernd", "Sara", "Jan" - ] + assert winners.at("*", "podium", "*", "name").gather() == ["Joe", "Ben", "Sue", "Bernd", "Sara", "Jan"] # Increment age of all users by 1 winners = winners_original.deepcopy(from_root=True) @@ -157,12 +154,13 @@ def test_gather(): assert names_2017 == ["Joe", "Ben", "Sue"] - def test_sum(): - p = pd({ - "1": {"a": 1, "b": [1]}, - "2": {"a": 3, "b": [1]}, - }) + p = pd( + { + "1": {"a": 1, "b": [1]}, + "2": {"a": 3, "b": [1]}, + } + ) assert p.at("*", "a").sum() == 4 with pytest.raises(TypeError): p.at("*", "b").sum() diff --git a/tests/test_scenarios.py b/tests/test_scenarios.py index 4ee615d..cde7df7 100644 --- a/tests/test_scenarios.py +++ b/tests/test_scenarios.py @@ -1,6 +1,7 @@ -from path_dict import pd import pytest +from path_dict import pd + def test_scenario_1(): d = { @@ -15,7 +16,7 @@ def test_scenario_1(): ["Ben", "Sue"], ["Joe", "Ben"], ["Ben", "Joe"], - ] + ], } o = pd(d) # Getting attributes @@ -24,7 +25,8 @@ def test_scenario_1(): assert o["users"] == { "1": {"name": "Joe", "age": 22}, "2": {"name": "Ben", "age": 49}, - "3": {"name": "Sue", "age": 32}} + "3": {"name": "Sue", "age": 32}, + } assert o["users", "1"] == {"name": "Joe", "age": 22} assert o["users", "3", "name"] == "Sue" assert o["follows"][0] == ["Ben", "Sue"] @@ -39,10 +41,7 @@ def test_scenario_1(): assert o["1", "1", "1"] == {"1": 1} # Apply functions o["follows"] = lambda x: [list(reversed(e)) for e in x] - assert o["follows"] == [ - ["Sue", "Ben"], - ["Ben", "Joe"], - ["Joe", "Ben"]] + assert o["follows"] == [["Sue", "Ben"], ["Ben", "Joe"], ["Joe", "Ben"]] assert o.get() == { "1": {"1": {"1": {"1": 1}}}, @@ -54,27 +53,21 @@ def test_scenario_1(): "3": {"name": "Sue", "age": 99}, "4": {"name": "Ron", "age": 62}, }, - "follows": [ - ["Sue", "Ben"], - ["Ben", "Joe"], - ["Joe", "Ben"]] + "follows": [["Sue", "Ben"], ["Ben", "Joe"], ["Joe", "Ben"]], } def test_scenario_2(): - tr = pd({ - "1": { - "date": "2018-01-01", - "amount": 100, - "currency": "EUR", - }, - "2": { - "date": "2018-01-02", - "amount": 200, - "currency": "CHF", - "related": [5, {"nested": "val"}, 2, 3] - }, - }) + tr = pd( + { + "1": { + "date": "2018-01-01", + "amount": 100, + "currency": "EUR", + }, + "2": {"date": "2018-01-02", "amount": 200, "currency": "CHF", "related": [5, {"nested": "val"}, 2, 3]}, + } + ) assert tr["2", "related", 1, "nested"] == "val" @@ -85,58 +78,70 @@ def test_scenario_2(): def test_scenario_3(): - u = pd({ - "1": { - "name": "Joe", - "currencies": ["EUR", "CHF"], - "expenses": { - "1": {"amount": 100, "currency": "EUR"}, - "2": {"amount": 50, "currency": "CHF"}, - "3": {"amount": 200, "currency": "EUR"}, - } - }, - "2": { - "name": "Ben", - "currencies": ["EUR", "USD"], - "expenses": { - "1": {"amount": 3, "currency": "EUR"}, - "2": {"amount": 40, "currency": "USD"}, - "3": {"amount": 10, "currency": "USD"}, - } - }, - "3": { - "name": "Sue", - "currencies": ["CHF", "USD"], - "expenses": { - "1": {"amount": 500, "currency": "CHF"}, - "2": {"amount": 300, "currency": "CHF"}, - "3": {"amount": 200, "currency": "USD"}, - } - }, - }) + u = pd( + { + "1": { + "name": "Joe", + "currencies": ["EUR", "CHF"], + "expenses": { + "1": {"amount": 100, "currency": "EUR"}, + "2": {"amount": 50, "currency": "CHF"}, + "3": {"amount": 200, "currency": "EUR"}, + }, + }, + "2": { + "name": "Ben", + "currencies": ["EUR", "USD"], + "expenses": { + "1": {"amount": 3, "currency": "EUR"}, + "2": {"amount": 40, "currency": "USD"}, + "3": {"amount": 10, "currency": "USD"}, + }, + }, + "3": { + "name": "Sue", + "currencies": ["CHF", "USD"], + "expenses": { + "1": {"amount": 500, "currency": "CHF"}, + "2": {"amount": 300, "currency": "CHF"}, + "3": {"amount": 200, "currency": "USD"}, + }, + }, + } + ) assert u.at("*", "expenses", "*", "amount").sum() == 1403 assert u.at("2", "expenses", "*", "amount").sum() == 53 assert u.at("*", "expenses", "1", "amount").sum() == 603 # Get sum of all expenses in EUR - - assert u.deepcopy(from_root=True).at("*", "expenses", "*").filter(lambda v: v["currency"] == "EUR").at("*", "amount").sum() == 303 - + assert ( + u.deepcopy(from_root=True) + .at("*", "expenses", "*") + .filter(lambda v: v["currency"] == "EUR") + .at("*", "amount") + .sum() + == 303 + ) # Get all transactions in CHF except for those of sue - assert u.at("*").filter( - lambda x: x["name"] != "Sue" - ).at("*", "expenses", "*").filter( - lambda v: v["currency"] == "CHF" - ).at("*", "amount").sum() == 50 - - - j = pd({ - "a": [1, 2], - "b": {"c": 1, "d": 2}, - "e": 5, - }) + assert ( + u.at("*") + .filter(lambda x: x["name"] != "Sue") + .at("*", "expenses", "*") + .filter(lambda v: v["currency"] == "CHF") + .at("*", "amount") + .sum() + == 50 + ) + + j = pd( + { + "a": [1, 2], + "b": {"c": 1, "d": 2}, + "e": 5, + } + ) assert j.at("a").sum() == 3 assert j.at("b").sum() == 3 with pytest.raises(TypeError): diff --git a/tests_syntax.py b/tests_syntax.py new file mode 100644 index 0000000..4444767 --- /dev/null +++ b/tests_syntax.py @@ -0,0 +1,78 @@ +from path_dict import PathDict as pd + +users ={ + "user_1": { + "name": "John Smith", + "email": "john.smith@example.com", + "age": 32, + "address": { + "street": "123 Main St", + "city": "Anytown", + "state": "CA", + "zip": "12345" + }, + "interests": ["reading", "hiking", "traveling"] + }, + "user_2": { + "name": "Jane Doe", + "email": "jane.doe@example.com", + "age": 28, + "address": { + "street": "456 Oak Ave", + "city": "Somewhere", + "state": "NY", + "zip": "67890" + }, + "interests": ["cooking", "running", "music"], + "job": { + "title": "Software Engineer", + "company": "Example Inc.", + "salary": 80000 + } + }, + "user_3": { + "name": "Bob Johnson", + "email": "bob.johnson@example.com", + "age": 40, + "address": { + "street": "789 Maple Blvd", + "city": "Nowhere", + "state": "TX", + "zip": "54321" + }, + "interests": ["gardening", "fishing", "crafts"], + "job": { + "title": "Marketing Manager", + "company": "Acme Corporation", + "salary": 90000 + } + }, + "user_4": { + "name": "Alice Brown", + "email": "alice.brown@example.com", + "age": 25, + "address": { + "street": "321 Pine St", + "city": "Anywhere", + "state": "FL", + "zip": "13579" + }, + "interests": ["painting", "yoga", "volunteering"], + "job": { + "title": "Graphic Designer", + "company": "Design Co.", + "salary": 65000 + } + } +} + +users_pd = pd(users) + + +users_pd.at("user_4") + + + +print(users_pd.at("user_5").set({"name": "John Smither", "age": 33})) + +# print(users_pd.at("*", "age").gather(as_type="list", include_paths=True))