Skip to content

Commit

Permalink
remove str sep due to side effect danger
Browse files Browse the repository at this point in the history
  • Loading branch information
mkrd committed Apr 3, 2023
1 parent 2007aae commit 5dbc41e
Show file tree
Hide file tree
Showing 9 changed files with 61 additions and 87 deletions.
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -294,13 +294,12 @@ If you try to serialize a PathDict object itself, the operation will fail.
# Reference


### pd(data: dict | list, str_sep="/", raw=False) -> PathDict
### pd(data: dict | list, raw=False) -> PathDict

Creates and returns a handle on the given data.

Args:
- `data` - Must be a list or dict.
- `str_sep` - Look within path strings for this separator and use it to split the path.
- `raw` - If `True`, do not interpret paths. So wildcards (`*`) are interpreted as a usual key, and tuples will be interpreted as keys as well.

Returns:
Expand Down
24 changes: 5 additions & 19 deletions path_dict/path.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,11 @@

class Path:
path: list[str]
str_sep: str
raw: bool


def __init__(self, *path, str_sep="/", raw=False):
def __init__(self, *path, raw=False):
# Careful, if the kwargs are passed as positional agrs, they are part of the path
self.str_sep = str_sep
self.raw = raw

# path is, whitout exceptions, always a tuple
Expand All @@ -21,23 +19,12 @@ def __init__(self, *path, str_sep="/", raw=False):
else:
self.path = list(path)


# If the contains strings with str_sep, split them up if not in raw mode
if not self.raw:
new_path = []
for key in self.path:
if isinstance(key, str) and str_sep in key:
new_path.extend(key.split(str_sep))
else:
new_path.append(key)
self.path = new_path

# Clean up empty strings
self.path = [x for x in self.path if x != ""]


def __repr__(self) -> str:
return f"Path(path={self.path}, str_sep={self.str_sep}, raw={self.raw})"
return f"Path(path={self.path}, raw={self.raw})"


@property
Expand All @@ -58,11 +45,10 @@ def __getitem__(self, key):
return self.path[key]


def copy(self, replace_path=None, replace_str_sep=None, replace_raw=None) -> Path:
def copy(self, replace_path=None, replace_raw=None) -> Path:
path_copy = list(self.path) if replace_path is None else replace_path
str_sep_copy = str(self.str_sep) if replace_str_sep is None else replace_str_sep
raw_copy = self.raw if replace_raw is None else replace_raw
return Path(path_copy, str_sep=str_sep_copy, raw=raw_copy)
return Path(path_copy, raw=raw_copy)


def expand(self, ref: dict | list) -> list[Path]:
Expand All @@ -84,4 +70,4 @@ def expand(self, ref: dict | list) -> list[Path]:
# Return empty list if no paths were found
if paths == [[]]:
return []
return [Path(p, str_sep=self.str_sep, raw=self.raw) for p in paths]
return [Path(p, raw=self.raw) for p in paths]
23 changes: 10 additions & 13 deletions path_dict/path_dict.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ class PathDict:
path_handle: Path


def __init__(self, data: dict | list, str_sep="/", raw=False, path: Path = None):
def __init__(self, data: dict | list, raw=False, path: Path = None):
"""
A PathDict always refers to a dict or list.
It is used to get data or perform operations at a given path.
Expand All @@ -23,7 +23,7 @@ def __init__(self, data: dict | list, str_sep="/", raw=False, path: Path = None)
f"({data})"
)
self.data = data
self.path_handle = Path([], str_sep=str_sep, raw=raw) if path is None else path
self.path_handle = Path([], raw=raw) if path is None else path


@classmethod
Expand Down Expand Up @@ -78,15 +78,14 @@ def copy(self, from_root=False) -> PathDict:
############################################################################


def at(self, *path, str_sep=None, raw=None) -> PathDict | MultiPathDict:
def at(self, *path, raw=None) -> PathDict | MultiPathDict:
"""
Calling at(path) moves the handle to the given path, and returns the
handle.
A path can be a string, a list or a tuple. For example, the following
are equivalent:
>>> d = {"a": {"b": {"c": 1}}}
>>> pd(d).at("a/b/c").get() # -> 1
>>> pd(d).at(["a", "b", "c"]).get() # -> 1
>>> pd(d).at("a", "b", "c").get() # -> 1
Expand All @@ -96,14 +95,12 @@ def at(self, *path, str_sep=None, raw=None) -> PathDict | MultiPathDict:
operations on all the selected elements at once.
:param path: The path to move to.
:param str_sep: The separator to use if there are separators in the path.
:param raw: If True, the path is not parsed, and is used as is. For
example, "*" will not be interpreted as a wildcard, but as a usual key.
"""

str_sep = self.path_handle.str_sep if str_sep is None else str_sep
raw = self.path_handle.raw if raw is None else raw
self.path_handle = Path(*path, str_sep=str_sep, raw=raw)
self.path_handle = Path(*path, raw=raw)

if self.path_handle.has_wildcards:
return MultiPathDict(self.data, self.path_handle)
Expand All @@ -120,7 +117,7 @@ def at_root(self) -> PathDict:
Example:
>>> d = {"a": {"b": {"c": 1}}}
>>> pd(d).at("a/b").filter(lambda k,v: v > 1).root().filter(lambda k,v: k == "a").get()
>>> pd(d).at("a", "b").filter(lambda k,v: v > 1).root().filter(lambda k,v: k == "a").get()
"""
return self.at()

Expand Down Expand Up @@ -155,13 +152,13 @@ def get(self, default=None) -> dict | list | Any:
Example:
>>> d = {"a": {"b": {"c": [1]}}}
>>> pd(d).at("a/b/c").get() # -> [1] (Valid path)
>>> pd(d).at("a/b/d").get() # -> None (Valid path, but does not exist)
>>> pd(d).at("a/b/c/d").get() # -> KeyError (Invalid path - cannot get key "d" on a list)
>>> pd(d).at("a/b/c/0").get() # -> 1 (Valid path)
>>> pd(d).at("a", "b", "c").get() # -> [1] (Valid path)
>>> pd(d).at("a", "b", "d").get() # -> None (Valid path, but does not exist)
>>> pd(d).at("a", "b", "c", "d").get() # -> KeyError (Invalid path - cannot get key "d" on a list)
>>> pd(d).at("a", "b", "c", "0").get() # -> 1 (Valid path)
Shorthand syntax:
>>> pd(d).at("a/b/c")[:]
>>> pd(d).at("a", "b", "c")[:]
You can use [:] to get the value at the current path.
Beware: using the subscript [...] will move the handle back to the root
of the data.
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "path_dict"
version = "3.0.5"
version = "4.0.0"
repository = "https://github.com/mkrd/PathDict"
description = "Extends Python's dict with useful extras"
authors = ["Marcel Kröker <[email protected]>"]
Expand Down
10 changes: 2 additions & 8 deletions test_docs_examples.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
print(users)

# Remove all interests of Ben which do not start with "a" ("cooking is removed")
users.at("u2/interests").filter(lambda i: not i.startswith("a"))
users.at("u2", "interests").filter(lambda i: not i.startswith("a"))
print(users)

# Remove users that are younger than 30
Expand Down Expand Up @@ -115,11 +115,5 @@


last_week = datetime.now() - timedelta(weeks=1)
pd(posts).at("*/ts").map(lambda ts: datetime.fromtimestamp(ts))
pd(posts).at("*", "ts").map(lambda ts: datetime.fromtimestamp(ts))
pd(posts).filter(lambda id, post: post["ts"] > last_week)


# print(posts)

# pd(tasks).at(task_id, "annotation/entry_list").append(new_entry)
# tasks[task_id, "annotation/entry_list"] = lambda el: (el or []) + new_entry
44 changes: 21 additions & 23 deletions tests/test_PDHandle.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,18 +29,16 @@ def test_at():
db = dummy_data.get_db()
assert pd(db).at().path_handle.path == []
assert pd(db).at("").path_handle.path == []
assert pd(db).at("/").path_handle.path == []
assert pd(db).at([]).path_handle.path == []
assert pd(db).at("users").path_handle.path == ["users"]
assert pd(db).at(["users"]).path_handle.path == ["users"]
assert pd(db).at("users", "1").path_handle.path == ["users", "1"]
assert pd(db).at("users/1", "friends").path_handle.path == ["users", "1", "friends"]
assert pd(db, str_sep="-").at("users-1", "friends").path_handle.path == ["users", "1", "friends"]
assert pd(db).at(["users/1", "friends"]).path_handle.path == ["users", "1", "friends"]
assert pd(db).at("users", "1", "friends").path_handle.path == ["users", "1", "friends"]
assert pd(db).at(["users", "1", "friends"]).path_handle.path == ["users", "1", "friends"]


def test_at_parent():
assert pd(dummy_data.get_db()).at("users/1").at_parent().path_handle.path == ["users"]
assert pd(dummy_data.get_db()).at("users", "1").at_parent().path_handle.path == ["users"]
pd(dummy_data.get_db()).at_parent().get() is None


Expand All @@ -54,10 +52,10 @@ def test_simple_get():
assert pd(db).get() == db
assert pd(db).at("").get() == db
assert pd(db).at().get() == db
assert pd(db).at("users/1/name").get() == "John"
assert pd(db).at("users/9/name").get() is None
assert pd(db).at("users", "1", "name").get() == "John"
assert pd(db).at("users", "9", "name").get() is None
assert pd(db).at(2).get() is None
assert pd(db).at("users/9/name").get("default") == "default"
assert pd(db).at("users", "9", "name").get("default") == "default"


def test_referencing():
Expand Down Expand Up @@ -85,7 +83,7 @@ def test_referencing():

def test__repr__():
j = {"1": 2}
assert str(pd(j)) == "PathDict(self.data = {'1': 2}, self.path_handle = Path(path=[], str_sep=/, raw=False))"
assert str(pd(j)) == "PathDict(self.data = {'1': 2}, self.path_handle = Path(path=[], raw=False))"


def test_reset_at_after_in():
Expand Down Expand Up @@ -149,7 +147,7 @@ def test_contains():
users_pd = pd(users_dict)
assert "total_users" in users_pd
assert ["premium_users", 1] in users_pd
assert "premium_users/1" in users_pd
assert "premium_users", "1" in users_pd
assert "premium_users", "1" in users_pd
assert ["premium_users", "44"] not in users_pd
assert ["users", "1"] in users_pd
Expand Down Expand Up @@ -233,7 +231,7 @@ def test_set_path():

# Cover specific KeyError
with pytest.raises(KeyError):
p.at("u3/meta/age").set(22)
p.at("u3", "meta", "age").set(22)

with pytest.raises(TypeError):
p.at().set("Not Allowed")
Expand All @@ -249,13 +247,13 @@ def test_set_path():

def test_map():
j = {"1": {"2": 3}}
assert pd(j).at("1/2").map(lambda x: x + 1).get() == 4
assert pd(j).at("1/6/7").map(lambda x: (x or 0) + 1).get() == 1
assert pd(j).at("1/6/7").map(lambda x: (x or 0) + 1).get() == 2
assert pd(j).at("1", "2").map(lambda x: x + 1).get() == 4
assert pd(j).at("1", "6", "7").map(lambda x: (x or 0) + 1).get() == 1
assert pd(j).at("1", "6", "7").map(lambda x: (x or 0) + 1).get() == 2
assert j["1"]["2"] == 4
assert j["1"]["6"]["7"] == 2
with pytest.raises(TypeError):
pd(j).at("1/99/99").map(lambda x: x + 1)
pd(j).at("1", "99", "99").map(lambda x: x + 1)


def test_mapped():
Expand All @@ -264,8 +262,8 @@ def test_mapped():
"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()
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
Expand All @@ -278,7 +276,7 @@ def test_mapped():

def test_append():
p = pd({})
p.at("1/2").append(3)
p.at("1", "2").append(3)
assert p.at().get() == {"1": {"2": [3]}}
with pytest.raises(TypeError):
p.at("1").append(2)
Expand Down Expand Up @@ -357,38 +355,38 @@ def test_reduce():
p.at("l1").reduce(lambda v, a: a + v, aggregate=0)

p = pd(dummy_data.get_users())
assert p.at("users/*/name").reduce(lambda v, a: a + [v], aggregate=[]) == ["Joe", "Ben", "Sue"]
assert p.at("users", "*", "name").reduce(lambda v, a: a + [v], aggregate=[]) == ["Joe", "Ben", "Sue"]


def test_keys():
p = pd({"1": {"2": [3]}})
assert p.keys() == ["1"]
assert p.at("1").keys() == ["2"]
with pytest.raises(AttributeError):
p.at("1/2").keys()
p.at("1", "2").keys()


def test_values():
p = pd({"1": {"2": [3]}})
assert p.values() == [{"2": [3]}]
assert p.at("1").values() == [[3]]
with pytest.raises(AttributeError):
p.at("1/2").values()
p.at("1", "2").values()


def test_items():
p = pd({"1": {"2": [3]}})
assert list(p.items()) == [("1", {"2": [3]})]
assert list(p.at("1").items()) == [("2", [3])]
with pytest.raises(AttributeError):
p.at("1/2").items()
p.at("1", "2").items()


def test__len__():
p = pd({"1": {"2": [3, 1]}})
assert len(p) == 1
assert len(p.at("1")) == 1
assert len(p.at("1/2")) == 2
assert len(p.at("1", "2")) == 2


def test_pop():
Expand Down
Loading

0 comments on commit 5dbc41e

Please sign in to comment.