Package: gardener
gardeneris a simple tree manipulation module. It provides a hook-based tree transformation.Installationgardeneris available on PyPi:python -m pip install gardener(or any other package manager)Basic exampleThis is a simple example usage, a hook that transforms everytagnode into a string:fromgardenerimportmake_node,register_hook,Node@register_hook("tag")deftag_hook(node:Node)->str:returnf"tag:{node['i']}{node['children',[]]}"x:str=make_node("tag",children=[make_node("tag",i=i)foriinrange(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 nodesfromgardenerimportmake_node,register_hook,Nodefromoperatorimportadd,sub,mul,truedivoperators={"+":add,"-":sub,"*":mul,"/":truediv}@register_hook("+","-","*","/")defbinary_expr(node:Node)->float:op=node.key[0]# node.key is a 1-element tuple, e.g. ('+', )op_func=operators[op]parts=node["parts",[]]ifnotparts:ifopin"+-":return0# empty sumreturn1# empty productresult=parts[0]foriinrange(1,len(parts)):result=op_func(result,parts[i])returnresultx:float=make_node("+",parts=[3,make_node("*",parts=[5,6])# 30])print(x)# 33Let's add exponentiation to this calculator. The trick is that power is right-associative (so that2 ** 2 ** 3equals2 ** (2 ** 3), not(2 ** 2) ** 3).You can obviously write a separate hook for that, but we can just combine hooks:fromgardenerimportmake_node,register_hook,Nodefromoperatorimportadd,sub,mul,truedivoperators={"+":add,"-":sub,"*":mul,"/":truediv,"**":lambdax,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` hookBe aware that the order of hook apply is the same as their registration."""@register_hook("**")defpower_reverse(node:Node)->Node:node["parts"]=node["parts",[]][::-1]returnnode@register_hook("+","-","*","/","**")defbinary_expr(node:Node)->float:op=node.key[0]# node.key is a 1-element tuple, e.g. ('+', )op_func=operators[op]parts=node["parts",[]]ifnotparts:ifopin"+-":return0# empty sumreturn1# empty productresult=parts[0]foriinrange(1,len(parts)):result=op_func(result,parts[i])returnresultx:float=make_node("+",parts=[3,make_node("*",parts=[5,6]),# 30make_node("**",parts=[2,2,3])# 256])print(x)# 289This, of course, may not be the most efficient or obvious way, butgardenerdoesn't impose any restrictions on how you might approach a problemNode propsExamples above have shown how to set initial props of aNode. To get and edit those props, use bracket notation:fromgardenerimportmake_nodenode=make_node("test")node["a"]=10# accepts any type of value, but key must be a stringprint(node["a"])# prints 10print(node["b",0])# prints 0 (default value)print(node["b"])# raises KeyErrorHook evaluation orderHook 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 aNodeobject directly, which is discouragedHooks 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 editingscope.hooks[key]directly (check Scopes below)ScopingOften it is convenient to have different trees in one project, using different hooks.While this can be done through namespacing (make_nodeactually also accepts node key as astr | tuple[str, ...]), that approach would force you to write long names in node creating and hook registration.gardenerprovides you with a more convenient approach:Scopeobjects. A scope is an isolated store with hooks:fromgardenerimportScope,Nodescope1=Scope("scope1")# key is optional and it doesn't affect scope behaviourscope2=Scope("scope2")@scope1.register_hook("i")defprint_stuff_1(node:Node)->Node:print("this is the first scope")returnnode@scope2.register_hook("i")defprint_stuff_2(node:Node)->Node:print("this is the second scope")returnnode@scope1.register_hook("i")@scope2.register_hook("i")defprint_stuff_both(node:Node)->Node:print("this is both scopes")returnnode# 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 withscope.hooks. It has typedict[tuple[str, ...], list[HookType]].To get the scope of the current node (e.g. in a hook, usenode.scope)Globalmake_nodeandregister_hookare, in fact, methods ofgardener.core.default_scopeApplying hooks multiple timesTo apply a hook to a node multiple times, callnode.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 printingIf your node props are JSON-serializable, you can runnode.pretty(**dumps_kwargs)to get a pretty indented JSON representation of the node.Node class itself is JSON-serializable (only withNodeJSONas an encoder).To represent non-JSON-serializable data, you will need to provide an encoder class:fromgardenerimportmake_nodefromgardener.coreimportNodeJSONfromtypingimportAnyclassSomeCoolDataClass:# your custom classdef__init__(self,x:int):self.x=xclassMyNodeJSON(NodeJSON):defdefault(self,obj:Any):ifisinstance(obj,SomeCoolDataClass):returnf"SomeCoolDataClass<{obj.x}>"# return any serializable data here (can contain nodes or, e.g. SomeCoolDataClass inside)returnsuper().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>"}}"""