From e65976f9f23c387d0ceece13340f5405f07a2a32 Mon Sep 17 00:00:00 2001 From: aranega Date: Fri, 25 Oct 2024 10:15:31 -0600 Subject: [PATCH] Add wildcard and excluding paths concept --- README.md | 4 ++++ iguala/paths.py | 41 ++++++++++++++++++++++++++++++++++------- tests/test_paths.py | 12 ++++++++++++ 3 files changed, 50 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index b223a7c..d3ad73e 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,9 @@ There is different kind of paths: * creating a named rec. path is done by using the `*` or `+` operator after a name, e.g: `foo*` expresses that `foo` needs to be followed 0 or many times and `foo+` expresses that `foo` needs to be followed 1 or many times. * **children recursive paths**: they express the recursive navigation of all "instance variable" of an object. * creating a children rec. path is done by using `*` alone, e.g: `*` means all the "children" (the instance variable of the object/keys of the dict) and their children. +* **wildcard direct paths**: they express a direct connection between objects without naming explicitally the relationship (e.g: "any of the direct connection from this object to another") +* **excluding paths**: they express a path towards any direct relationship excluding a specific relation from them (e.g: "any of the direct connection from this object to another excluding this connection"). + * creatin an excluding path is done by using `!` in front of the relationship to exclude, e.g: `!parent` means "all the direct paths excluding `parent`. Those operators can be composed with `>`. For examples: @@ -93,6 +96,7 @@ For examples: * `bar>foo>*` means "`bar` then `foo` then all the children recursively" * `*>foo` means "all the children recursively then `foo`" (if `foo` exists for each object) * `child*>name` means "follow `child` recursively and get `name` each time" +* `!parent*` means "follow all the direct relationship from object to object recursively, but excluding `parent` each time * ... ### Wildcards/variables diff --git a/iguala/paths.py b/iguala/paths.py index 7ab3877..f953710 100644 --- a/iguala/paths.py +++ b/iguala/paths.py @@ -1,6 +1,4 @@ # from types import LambdaType -from functools import lru_cache - from .helpers import IdentitySet, flat @@ -32,6 +30,29 @@ def resolve_from(self, obj): return flat(getattr(obj, self.path, [])) +class WildcardPath(ObjectPath): + def __init__(self, excluding=None): + self.excluding = excluding or [] + + def resolve_from(self, obj): + direct_objects = [] + try: + visit = vars(obj).items() + except TypeError: + try: + visit = {k: getattr(obj, k) for k in obj.__class__.__slots__}.items() + except AttributeError: + try: + visit = obj.items() + except AttributeError: + return [] + for k, v in visit: + if k in self.excluding: + continue + direct_objects.extend(flat(v)) + return direct_objects + + # class LambdaPath(ObjectPath): # def __init__(self, func): # self.func = func @@ -75,12 +96,15 @@ def _resolve_from(self, obj, seen, resolved): if obj not in seen: seen.add(obj) direct_objects.extend( - flat([self._resolve_from(x, seen) for x in direct_objects]) + flat([self._inner_resolve_from(x, seen) for x in direct_objects]) ) return direct_objects + def _inner_resolve_from(self, obj, seen): + return [] + def resolve_from(self, obj): - res = self._resolve_from(obj, IdentitySet()) + res = self._inner_resolve_from(obj, IdentitySet()) return res @property @@ -92,13 +116,13 @@ class NamedRecursivePath(RecursivePath): def __init__(self, path): self.path = path - def _resolve_from(self, obj, seen): + def _inner_resolve_from(self, obj, seen): o = self.path.resolve_from(obj) return super()._resolve_from(obj, seen, o) class ChildrenRecursivePath(RecursivePath): - def _resolve_from(self, obj, seen): + def _inner_resolve_from(self, obj, seen): direct_objects = [] try: visit = vars(obj).items() @@ -124,6 +148,8 @@ def as_path(s, dictkey=False): # return LambdaPath(s) if not isinstance(s, str): return s.as_path() + if s == '_': + return WildcardPath() if s == "*": return ChildrenRecursivePath() if isinstance(s, str): @@ -136,5 +162,6 @@ def as_path(s, dictkey=False): NamedRecursivePath(as_path(s[:-1], dictkey=dictkey)), ) ) - + if s.startswith('!'): + return WildcardPath(excluding=[s[1:]]) return dict_cls(s) diff --git a/tests/test_paths.py b/tests/test_paths.py index 315e9dd..21a271f 100644 --- a/tests/test_paths.py +++ b/tests/test_paths.py @@ -466,3 +466,15 @@ def test_resolve_unexisting(): p = as_path("name>unexisting") assert p.resolve_from(obj_test) == [] + + +def test_resolve_wildcard_path(): + p = as_path("_") + + assert p.resolve_from(obj_test) == [4, 8, obj_test.name, obj_test.inner, *obj_test.inner_list] + + +def test_resolve_named_path_excluding(): + p = as_path("!inner_list") + + assert p.resolve_from(obj_test) == [4, 8, obj_test.name, obj_test.inner] \ No newline at end of file