gardener


Namegardener JSON
Version 2.0.1 PyPI version JSON
download
home_pagehttps://github.com/courage-tci/gardener
SummaryHook-based tree manipulation library
upload_time2022-12-28 17:10:08
maintainer
docs_urlNone
authorDmitry Gritsenko
requires_python>=3.7,<4.0
licenseMIT
keywords tree
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            [![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/courage-tci/gardener/build.yml?branch=pub)](https://github.com/courage-tci/gardener/actions/workflows/build.yml)
[![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/courage-tci/gardener/test.yml?branch=pub&label=tests)](https://github.com/courage-tci/gardener/actions/workflows/test.yml)
[![PyPI](https://img.shields.io/pypi/v/gardener)](https://pypi.org/project/gardener/)
![PyPI - Downloads](https://pepy.tech/badge/gardener)
![PyPI - Python Version](https://img.shields.io/pypi/pyversions/gardener)
[![Coveralls](https://img.shields.io/coverallsCoverage/github/courage-tci/gardener?label=test%20coverage)](https://coveralls.io/github/courage-tci/gardener?branch=pub)
![License](https://img.shields.io/github/license/courage-tci/gardener)
![Badge Count](https://img.shields.io/badge/badges-8-important)

**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:


```python
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

```python
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:

```python
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:


```python
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:

```python
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:

```python
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>"
  }
}
"""
```
            

Raw data

            {
    "_id": null,
    "home_page": "https://github.com/courage-tci/gardener",
    "name": "gardener",
    "maintainer": "",
    "docs_url": null,
    "requires_python": ">=3.7,<4.0",
    "maintainer_email": "",
    "keywords": "tree",
    "author": "Dmitry Gritsenko",
    "author_email": "k01419q45@ya.ru",
    "download_url": "https://files.pythonhosted.org/packages/96/e1/7c02dd4204b7f635ceca15f05f9f2f52c7f22084c331407fc3dc602a7749/gardener-2.0.1.tar.gz",
    "platform": null,
    "description": "[![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/courage-tci/gardener/build.yml?branch=pub)](https://github.com/courage-tci/gardener/actions/workflows/build.yml)\n[![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/courage-tci/gardener/test.yml?branch=pub&label=tests)](https://github.com/courage-tci/gardener/actions/workflows/test.yml)\n[![PyPI](https://img.shields.io/pypi/v/gardener)](https://pypi.org/project/gardener/)\n![PyPI - Downloads](https://pepy.tech/badge/gardener)\n![PyPI - Python Version](https://img.shields.io/pypi/pyversions/gardener)\n[![Coveralls](https://img.shields.io/coverallsCoverage/github/courage-tci/gardener?label=test%20coverage)](https://coveralls.io/github/courage-tci/gardener?branch=pub)\n![License](https://img.shields.io/github/license/courage-tci/gardener)\n![Badge Count](https://img.shields.io/badge/badges-8-important)\n\n**gardener** is a simple tree manipulation module. It provides a hook-based tree transformation.\n\n## Installation\n\n`gardener` is available on PyPi:    \n\n`python -m pip install gardener` (or any other package manager)\n\n## Basic example\n\nThis is a simple example usage, a hook that transforms every `tag` node into a string:\n\n\n```python\nfrom gardener import make_node, register_hook, Node\n\n\n@register_hook(\"tag\")\ndef tag_hook(node: Node) -> str:\n    return f\"tag:{node['i']}{node['children', []]}\"\n\n\nx: str = make_node(\n    \"tag\",\n    children=[\n        make_node(\"tag\", i=i) \n        for i in range(10)\n    ],\n    i=99\n)\n\nprint(x) # tag:99['tag:0[]', 'tag:1[]', 'tag:2[]', 'tag:3[]', 'tag:4[]', 'tag:5[]', 'tag:6[]', 'tag:7[]', 'tag:8[]', 'tag:9[]']\n```\n\n## Combining hooks and transforming nodes\n\n```python\nfrom gardener import make_node, register_hook, Node\nfrom operator import add, sub, mul, truediv\n\n\noperators = {\n    \"+\": add,\n    \"-\": sub,\n    \"*\": mul,\n    \"/\": truediv\n}\n\n@register_hook(\"+\", \"-\", \"*\", \"/\")\ndef binary_expr(node: Node) -> float:\n    op = node.key[0] # node.key is a 1-element tuple, e.g. ('+', )\n    op_func = operators[op]\n    \n    parts = node[\"parts\", []]\n    \n    if not parts:\n        if op in \"+-\":\n            return 0 # empty sum\n        return 1 # empty product\n    \n    result = parts[0]\n    \n    for i in range(1, len(parts)):\n        result = op_func(result, parts[i])\n    \n    return result\n\n\nx: float = make_node(\n    \"+\",\n    parts=[\n        3,\n        make_node(\"*\", parts=[5, 6]) # 30\n    ]\n)\n\nprint(x) # 33\n```\n\nLet'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`).    \nYou can obviously write a separate hook for that, but we can just combine hooks:\n\n```python\nfrom gardener import make_node, register_hook, Node\nfrom operator import add, sub, mul, truediv\n\n\noperators = {\n    \"+\": add,\n    \"-\": sub,\n    \"*\": mul,\n    \"/\": truediv,\n    \"**\": lambda x, y: pow(y, x) # reversing order there, so that we can reverse the order of all elements\n}\n\n\"\"\"\n\nThis hook, instead of producing a new non-node value, just edits the node contents.\nThis allows the hook chain to continue to `binary_expr` hook\n\nBe aware that the order of hook apply is the same as their registration.\n\n\"\"\"\n@register_hook(\"**\")\ndef power_reverse(node: Node) -> Node:\n    node[\"parts\"] = node[\"parts\", []][::-1]\n    return node\n\n\n@register_hook(\"+\", \"-\", \"*\", \"/\", \"**\")\ndef binary_expr(node: Node) -> float:\n    op = node.key[0] # node.key is a 1-element tuple, e.g. ('+', )\n    op_func = operators[op]\n    \n    parts = node[\"parts\", []]\n    \n    if not parts:\n        if op in \"+-\":\n            return 0 # empty sum\n        return 1 # empty product\n    \n    result = parts[0]\n    \n    for i in range(1, len(parts)):\n        result = op_func(result, parts[i])\n    \n    return result\n\n\nx: float = make_node(\n    \"+\",\n    parts=[\n        3,\n        make_node(\"*\", parts=[5, 6]), # 30\n        make_node(\"**\", parts=[2, 2, 3]) # 256\n    ]\n)\n\nprint(x) # 289\n```\n\nThis, 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\n\n## Node props\n\nExamples above have shown how to set initial props of a `Node`. To get and edit those props, use bracket notation:\n\n\n```python\nfrom gardener import make_node\n\nnode = make_node(\"test\")\n\nnode[\"a\"] = 10 # accepts any type of value, but key must be a string\nprint(node[\"a\"]) # prints 10\nprint(node[\"b\", 0]) # prints 0 (default value)\nprint(node[\"b\"]) # raises KeyError\n\n```\n\n## Hook evaluation order\n\nHook ordering is simple:\n\n1. 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) \n    *except creating a `Node` object directly, which is discouraged*\n2. Hooks are run in registration order, because when you register a hook, it's appended to the end of the list for that key. \n    *you can change the order by editing `scope.hooks[key]` directly (check Scopes below)*\n\n## Scoping\n\nOften it is convenient to have different trees in one project, using different hooks.    \nWhile 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.    \n\n`gardener` provides you with a more convenient approach: `Scope` objects. A scope is an isolated store with hooks:\n\n```python\nfrom gardener import Scope, Node\n\n\nscope1 = Scope(\"scope1\") # key is optional and it doesn't affect scope behaviour\nscope2 = Scope(\"scope2\")\n\n\n@scope1.register_hook(\"i\")\ndef print_stuff_1(node: Node) -> Node:\n    print(\"this is the first scope\")\n    return node\n\n@scope2.register_hook(\"i\")\ndef print_stuff_2(node: Node) -> Node:\n    print(\"this is the second scope\")\n    return node\n\n@scope1.register_hook(\"i\")\n@scope2.register_hook(\"i\")\ndef print_stuff_both(node: Node) -> Node:\n    print(\"this is both scopes\")\n    return node\n\n\n# prints \"this is the first scope\"\n# prints \"this is both scopes\"\nscope1.make_node(\"i\")\n\n\n# prints \"this is the second scope\"\n# prints \"this is both scopes\"\nscope2.make_node(\"i\")\n```\n\nYou can get all of the scope hooks with `scope.hooks`. It has type `dict[tuple[str, ...], list[HookType]]`.    \nTo get the scope of the current node (e.g. in a hook, use `node.scope`)    \n\nGlobal `make_node` and `register_hook` are, in fact, methods of `gardener.core.default_scope`\n\n\n## Applying hooks multiple times\n\nTo apply a hook to a node multiple times, call `node.transform()` \u2014 it would return the result of another chain of transformations.    \n**Be careful about using it in hooks, as this could easily lead to infinite recursion if not handled properly.**    \n\n## Node printing\n\nIf your node props are JSON-serializable, you can run `node.pretty(**dumps_kwargs)` to get a pretty indented JSON representation of the node.    \nNode class itself is JSON-serializable (only with `NodeJSON` as an encoder).\n\nTo represent non-JSON-serializable data, you will need to provide an encoder class:\n\n```python\nfrom gardener import make_node\nfrom gardener.core import NodeJSON\nfrom typing import Any\n\n\nclass SomeCoolDataClass: # your custom class\n    def __init__(self, x: int):\n        self.x = x\n\n\nclass MyNodeJSON(NodeJSON):\n    def default(self, obj: Any):\n        if isinstance(obj, SomeCoolDataClass):\n            return f\"SomeCoolDataClass<{obj.x}>\" # return any serializable data here (can contain nodes or, e.g. SomeCoolDataClass inside)\n        return super().default(obj)\n\n\nnode = make_node(\n    \"cool_data_node\",\n    cool_data=SomeCoolDataClass(6)\n)\n\nprint(\n    node.pretty(cls=MyNodeJSON) # accepts same arguments (keyword-only) as json.dumps\n)\n\"\"\"\n{\n  \"key\": \"cool_data_node\",\n  \"props\": {\n    \"cool_data\": \"SomeCoolDataClass<6>\"\n  }\n}\n\"\"\"\n```",
    "bugtrack_url": null,
    "license": "MIT",
    "summary": "Hook-based tree manipulation library",
    "version": "2.0.1",
    "split_keywords": [
        "tree"
    ],
    "urls": [
        {
            "comment_text": "",
            "digests": {
                "md5": "1b32e4ff11ff836db13ecc52f135e6e6",
                "sha256": "9e08c080648f7e304d0969583c3bd3b18232b90d4f3a7f19eec1e5934cf1181a"
            },
            "downloads": -1,
            "filename": "gardener-2.0.1-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "1b32e4ff11ff836db13ecc52f135e6e6",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": ">=3.7,<4.0",
            "size": 5818,
            "upload_time": "2022-12-28T17:10:07",
            "upload_time_iso_8601": "2022-12-28T17:10:07.426948Z",
            "url": "https://files.pythonhosted.org/packages/22/33/568536b5061d7d17b0cb991e135548acaa675475744cbbe641d78c49c304/gardener-2.0.1-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": "",
            "digests": {
                "md5": "85531b45db93268c8f23506b05d8d7bb",
                "sha256": "6e2b1b90e6a4d3496e4bb457eaec6a408e1fd8bb8d2abf1690f40c43971857f3"
            },
            "downloads": -1,
            "filename": "gardener-2.0.1.tar.gz",
            "has_sig": false,
            "md5_digest": "85531b45db93268c8f23506b05d8d7bb",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": ">=3.7,<4.0",
            "size": 6198,
            "upload_time": "2022-12-28T17:10:08",
            "upload_time_iso_8601": "2022-12-28T17:10:08.499095Z",
            "url": "https://files.pythonhosted.org/packages/96/e1/7c02dd4204b7f635ceca15f05f9f2f52c7f22084c331407fc3dc602a7749/gardener-2.0.1.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2022-12-28 17:10:08",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "github_user": "courage-tci",
    "github_project": "gardener",
    "travis_ci": false,
    "coveralls": false,
    "github_actions": true,
    "lcname": "gardener"
}
        
Elapsed time: 0.09431s