Skip to main content

Generic tree with visitors and nice markdown/mermaid representation

This project has been archived.

The maintainers of this project have marked this project as archived. No new releases are expected.

Project description

gentry

Python Test Status Coverage PyPI - Version

gentry  logo

gentry is a Python package for representing and manipulating generic tree structures with support for extensible child groups, visitor patterns, and convenient representations in the form of Markdown / Mermaid diagrams.

The name gentry is a weak pun on 'generic tree' that is too lame to explain 😄 (See Wikipedia)


Table of contents


Features

The gentry package centers around a core gentry.tree.Tree class and a reusable gentry.tree.Vistor class. A mixin class gentry.mermaid.Mermaid is also provided.

The Tree class implements a node in a tree. It features

  • a label

    that identifies a node, but that may be empty

  • any number of children, organized in groups

    those children are implemented as a dict[str,list] and can be accessed via the _children atrribute, that will be initialized when instancing a node, but those groups of children will normally be accessed via group attributes (see next point). The Visitor class will iterate over all objects in _children, regardless which group they are in.

  • group attributes

    a particular group of children can be accessed by using there name as an attribute to a node, by defining a class variable _groups with a set of group names when inheriting from Tree. See the section Example Usage for some examples.

  • a properties attribute

    that should be a dict[str,Any] and can hold node specific arbitrary information.

The Mermaid mixin class can be added to the base classes when inheriting from Tree.

It will add a __str__() method that will return Markdown containing a Mermaid block that presents the node and all its children a graph.

It is a cooperative mixin, i.e. it depends on the existance of the label, _children, and properties attributes, and its __str__() method will recursively create nodes and connectors for a Tree node and all its children. It will put each group of children in its own frame. An example:

graph TD
        classDef keyword fill:#dFd
        classDef function fill:#bff,font-size:20px,stroke-width:2px,font-weight:bold
        classDef loop fill:#fdd
        classDef operator font-weight:bold,font-size:20px
        classDef constant color:#3a3
        classDef variable color:#a33
        classDef subgraph_even fill:#eff
        classDef subgraph_odd fill:#eee

    subgraph0:::subgraph_odd
    Family0@{shape: rounded, label: "The Andersons"} --> subgraph0
    subgraph subgraph0[matriarch]
            direction TB

        subgraph2:::subgraph_even
        GrandMother1:::function@{shape: braces, label: "Granny"} --> subgraph2
        subgraph subgraph2[children]
                direction TB

            subgraph4:::subgraph_odd
            Mother2@{shape: rounded, label: "Anna"} --> subgraph4
            subgraph subgraph4[girls]
                    direction TB

            Child6@{shape: rounded, label: "Alice\n()"}
            Child7@{shape: rounded, label: "Cherryl\n(chess master=ELO 2235)"}
            end
            subgraph7:::subgraph_odd
            Mother2@{shape: rounded, label: "Anna"} --> subgraph7
            subgraph subgraph7[boys]
                    direction TB

            Child9@{shape: rounded, label: "Bob\n()"}
            Child10@{shape: rounded, label: "Dick\n()"}
            end
            subgraph11:::subgraph_odd
            Mother2@{shape: rounded, label: "Beatrice"} --> subgraph11
            subgraph subgraph11[girls]
                    direction TB

            Child13@{shape: rounded, label: "Ellen\n()"}
            Child14@{shape: rounded, label: "Gladys\n(drivers license=2023,\nnose piercing=2024)"}
            end
            subgraph14:::subgraph_odd
            Mother2@{shape: rounded, label: "Beatrice"} --> subgraph14
            subgraph subgraph14[boys]
                    direction TB

            Child16@{shape: rounded, label: "Fergal\n()"}
            Child17@{shape: rounded, label: "Hank\n()"}
            end
        end
    end

Individual nodes can be given distict shapes and styles, and the alternating colors of the frames can be configured as well. See Example Usage for more.

Finally we have a Visitor class that can be inherited from to implement a visitor pattern. It can be given a Tree node and its children will be iterated over in depth-first fashion, after which the type of the node will be used to find a specific vistor method for that node type (a tree can have nodes of different types as long as the inherit from Tree), or default to a general visit method.

Example Usage

This is the code that was used to generate the example diagram. It can be found in main.py

It creates different subclasses of Tree, each with their own way of grouping children: a Person in general may simple group all their children using the attribute children, while Mothers are the exceptions and have different attributes for boys and girls.

A GrandMother also has a different node shape when rendered as a Mermaid graph, and a different style (Styles reflect to original purpose of the gentry package, i.e. Abstract Syntax Trees, so the have names like Shape.function)

For a Child we enable the display of properties in a Mermaid graph, but these are only set on some children (Cherryl and Gladys)

We also define a FamilyCount class that inherits from the Count class (provided in the package) that in turn inherits from the base Visitor class. It simply counts all nodes in a tree, but because we don't want to count a Family node as we would a Person, we provide a specialized _do_count_Family() method that simply returns 0.

The Mermaid diagram is produced in the final print statement. That's a one lines because it is implement in the __str__() method.

from .tree import Tree, Count
from .mermaid import Mermaid, Shape, Style

class Family(Tree, Mermaid):...

class Person(Tree, Mermaid):
    _groups = { "children"}
    
class GrandMother(Person):
    _style = Style.function
    _shape = Shape.braces

class Mother(Person):
    _groups = {"girls", "boys"}
 
class Child(Person):
    _include_properties = True

class FamilyCount(Count):
    """
    We only count real persons, so we define a specialized member function
    for any Family node that returns 0 and will not be counted.
    """
    def _do_count_Family(self, tree:Family):
        return 0

# define a few children  
c1 = Child("Alice")
c2 = Child("Bob")
c3 = Child("Cherryl", properties={"chess master": "ELO 2235"})
c4 = Child("Dick")
c5 = Child("Ellen")
c6 = Child("Fergal")
c7 = Child("Gladys", properties={"drivers license": 2023, "nose piercing": 2024})
c8 = Child("Hank")

# Mothers have girls and boys attributes defined, so those can be assigned directly
m1 = Mother("Anna")
m1.girls = [c1,c3]
m1.boys = [c2,c4]

m2 = Mother("Beatrice")
m2.girls.append(c5)     # under the hood they are all items in a defaultdict(list) so we can append directly
m2.girls.append(c7)
m2.boys = [c6,c8]

# Grandmothers do not make the distinction
g1 = GrandMother("Granny")
g1.children = [m1, m2]
# so this would fail:
# g1.girls = [m1, m2]

# Family does not have any direct access attributes defined, but anything
# that is passed as the children argument (and can be converted to a defaultdict(list))
# will be added to the _children attribute, so will still be automatically discovered
# by Visitor derived classes.
f1 = Family("The Andersons", children={"matriarch":[g1]})

counter = FamilyCount(f1, strict=False)

assert counter.count() == 11

print(f1)

Installation

pip install gentry

License

This project is licensed under the GNU GPLv3, with the exception of any artwork, including the logo, which are licensed under CC BY-NC-ND 4.0.

Documentation

The API documentation is a work in progress and can be found on https://varkenvarken.github.io/gentrypy/

Project Structure

The repository is a reflection of my Vscode environment and contains:

To package the gentry package setup.py is provided, which is used by a GitHub action to package and publish to pypi when a new release is tagged.

Contributions

Contributions are wellcome, either by suggesting improvements in Issues, or even better, submitting a PR.

Please make sure that any PR:

  • is suitable for inclusion under a GPLv3 license
  • passes all tests
  • provides new unit tests if new functionality is implemented
  • contains only code that is formatted by ruff (with the default settings, import sorting enabled)

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

gentry-0.1.5.tar.gz (30.2 kB view details)

Uploaded Source

File details

Details for the file gentry-0.1.5.tar.gz.

File metadata

  • Download URL: gentry-0.1.5.tar.gz
  • Upload date:
  • Size: 30.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.12.9

File hashes

Hashes for gentry-0.1.5.tar.gz
Algorithm Hash digest
SHA256 c1526500721db75784916b17542b231c0cf8e0fadcfeefea7f0c56df58f6c7a5
MD5 ff6cb32c32bd23090b92e01c11bd5fab
BLAKE2b-256 ff9f4eb4b510959b5d998c14a1f7cbff70dd226e9d64762636ffd16f76ef547f

See more details on using hashes here.

Provenance

The following attestation bundles were made for gentry-0.1.5.tar.gz:

Publisher: publish_package.yml on varkenvarken/gentrypy

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

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