Skip to main content

Hook-based tree manipulation library

Project description

GitHub Workflow Status GitHub Workflow Status PyPI PyPI - Downloads PyPI - Python Version Coveralls License Badge Count

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:

  1. 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
  2. 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

gardener-2.0.1.tar.gz (6.2 kB view details)

Uploaded Source

Built Distribution

gardener-2.0.1-py3-none-any.whl (5.8 kB view details)

Uploaded Python 3

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

Hashes for gardener-2.0.1.tar.gz
Algorithm Hash digest
SHA256 6e2b1b90e6a4d3496e4bb457eaec6a408e1fd8bb8d2abf1690f40c43971857f3
MD5 85531b45db93268c8f23506b05d8d7bb
BLAKE2b-256 96e17c02dd4204b7f635ceca15f05f9f2f52c7f22084c331407fc3dc602a7749

See more details on using hashes here.

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

Hashes for gardener-2.0.1-py3-none-any.whl
Algorithm Hash digest
SHA256 9e08c080648f7e304d0969583c3bd3b18232b90d4f3a7f19eec1e5934cf1181a
MD5 1b32e4ff11ff836db13ecc52f135e6e6
BLAKE2b-256 2233568536b5061d7d17b0cb991e135548acaa675475744cbbe641d78c49c304

See more details on using hashes here.

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page