Hook-based tree manipulation library
Project description
gardener is a simple tree manipulation module. It provides a hook-based tree transformation.
Installation
gardener
is available on PyPi:
python -m pip install gardener
(or any other package manager)
Basic example
This is a simple example usage, a hook that transforms every tag
node into a string:
from gardener import make_node, register_hook, Node
@register_hook("tag")
def tag_hook(node: Node) -> str:
return f"tag:{node['i']}{node['children', []]}"
x: str = make_node(
"tag",
children=[
make_node("tag", i=i)
for i in range(10)
],
i=99
)
print(x) # tag:99['tag:0[]', 'tag:1[]', 'tag:2[]', 'tag:3[]', 'tag:4[]', 'tag:5[]', 'tag:6[]', 'tag:7[]', 'tag:8[]', 'tag:9[]']
Combining hooks and transforming nodes
from gardener import make_node, register_hook, Node
from operator import add, sub, mul, truediv
operators = {
"+": add,
"-": sub,
"*": mul,
"/": truediv
}
@register_hook("+", "-", "*", "/")
def binary_expr(node: Node) -> float:
op = node.key[0] # node.key is a 1-element tuple, e.g. ('+', )
op_func = operators[op]
parts = node["parts", []]
if not parts:
if op in "+-":
return 0 # empty sum
return 1 # empty product
result = parts[0]
for i in range(1, len(parts)):
result = op_func(result, parts[i])
return result
x: float = make_node(
"+",
parts=[
3,
make_node("*", parts=[5, 6]) # 30
]
)
print(x) # 33
Let's add exponentiation to this calculator. The trick is that power is right-associative (so that 2 ** 2 ** 3
equals 2 ** (2 ** 3)
, not (2 ** 2) ** 3
).
You can obviously write a separate hook for that, but we can just combine hooks:
from gardener import make_node, register_hook, Node
from operator import add, sub, mul, truediv
operators = {
"+": add,
"-": sub,
"*": mul,
"/": truediv,
"**": lambda x, y: pow(y, x) # reversing order there, so that we can reverse the order of all elements
}
"""
This hook, instead of producing a new non-node value, just edits the node contents.
This allows the hook chain to continue to `binary_expr` hook
Be aware that the order of hook apply is the same as their registration.
"""
@register_hook("**")
def power_reverse(node: Node) -> Node:
node["parts"] = node["parts", []][::-1]
return node
@register_hook("+", "-", "*", "/", "**")
def binary_expr(node: Node) -> float:
op = node.key[0] # node.key is a 1-element tuple, e.g. ('+', )
op_func = operators[op]
parts = node["parts", []]
if not parts:
if op in "+-":
return 0 # empty sum
return 1 # empty product
result = parts[0]
for i in range(1, len(parts)):
result = op_func(result, parts[i])
return result
x: float = make_node(
"+",
parts=[
3,
make_node("*", parts=[5, 6]), # 30
make_node("**", parts=[2, 2, 3]) # 256
]
)
print(x) # 289
This, of course, may not be the most efficient or obvious way, but gardener
doesn't impose any restrictions on how you might approach a problem
Node props
Examples above have shown how to set initial props of a Node
. To get and edit those props, use bracket notation:
from gardener import make_node
node = make_node("test")
node["a"] = 10 # accepts any type of value, but key must be a string
print(node["a"]) # prints 10
print(node["b", 0]) # prints 0 (default value)
print(node["b"]) # raises KeyError
Hook evaluation order
Hook ordering is simple:
- Hooks run at the node creation, there is no way to get a node that wasn't processed with relevant hooks (if there were any)
except creating a
Node
object directly, which is discouraged - Hooks are run in registration order, because when you register a hook, it's appended to the end of the list for that key.
you can change the order by editing
scope.hooks[key]
directly (check Scopes below)
Scoping
Often it is convenient to have different trees in one project, using different hooks.
While this can be done through namespacing (make_node
actually also accepts node key as a str | tuple[str, ...]
), that approach would force you to write long names in node creating and hook registration.
gardener
provides you with a more convenient approach: Scope
objects. A scope is an isolated store with hooks:
from gardener import Scope, Node
scope1 = Scope("scope1") # key is optional and it doesn't affect scope behaviour
scope2 = Scope("scope2")
@scope1.register_hook("i")
def print_stuff_1(node: Node) -> Node:
print("this is the first scope")
return node
@scope2.register_hook("i")
def print_stuff_2(node: Node) -> Node:
print("this is the second scope")
return node
@scope1.register_hook("i")
@scope2.register_hook("i")
def print_stuff_both(node: Node) -> Node:
print("this is both scopes")
return node
# prints "this is the first scope"
# prints "this is both scopes"
scope1.make_node("i")
# prints "this is the second scope"
# prints "this is both scopes"
scope2.make_node("i")
You can get all of the scope hooks with scope.hooks
. It has type dict[tuple[str, ...], list[HookType]]
.
To get the scope of the current node (e.g. in a hook, use node.scope
)
Global make_node
and register_hook
are, in fact, methods of gardener.core.default_scope
Applying hooks multiple times
To apply a hook to a node multiple times, call node.transform()
— it would return the result of another chain of transformations.
Be careful about using it in hooks, as this could easily lead to infinite recursion if not handled properly.
Node printing
If your node props are JSON-serializable, you can run node.pretty(**dumps_kwargs)
to get a pretty indented JSON representation of the node.
Node class itself is JSON-serializable (only with NodeJSON
as an encoder).
To represent non-JSON-serializable data, you will need to provide an encoder class:
from gardener import make_node
from gardener.core import NodeJSON
from typing import Any
class SomeCoolDataClass: # your custom class
def __init__(self, x: int):
self.x = x
class MyNodeJSON(NodeJSON):
def default(self, obj: Any):
if isinstance(obj, SomeCoolDataClass):
return f"SomeCoolDataClass<{obj.x}>" # return any serializable data here (can contain nodes or, e.g. SomeCoolDataClass inside)
return super().default(obj)
node = make_node(
"cool_data_node",
cool_data=SomeCoolDataClass(6)
)
print(
node.pretty(cls=MyNodeJSON) # accepts same arguments (keyword-only) as json.dumps
)
"""
{
"key": "cool_data_node",
"props": {
"cool_data": "SomeCoolDataClass<6>"
}
}
"""
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
File details
Details for the file gardener-2.0.1.tar.gz
.
File metadata
- Download URL: gardener-2.0.1.tar.gz
- Upload date:
- Size: 6.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: poetry/1.3.1 CPython/3.9.16 Linux/5.15.0-1024-azure
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | 6e2b1b90e6a4d3496e4bb457eaec6a408e1fd8bb8d2abf1690f40c43971857f3 |
|
MD5 | 85531b45db93268c8f23506b05d8d7bb |
|
BLAKE2b-256 | 96e17c02dd4204b7f635ceca15f05f9f2f52c7f22084c331407fc3dc602a7749 |
File details
Details for the file gardener-2.0.1-py3-none-any.whl
.
File metadata
- Download URL: gardener-2.0.1-py3-none-any.whl
- Upload date:
- Size: 5.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: poetry/1.3.1 CPython/3.9.16 Linux/5.15.0-1024-azure
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | 9e08c080648f7e304d0969583c3bd3b18232b90d4f3a7f19eec1e5934cf1181a |
|
MD5 | 1b32e4ff11ff836db13ecc52f135e6e6 |
|
BLAKE2b-256 | 2233568536b5061d7d17b0cb991e135548acaa675475744cbbe641d78c49c304 |