Skip to content

Commit

Permalink
Merge branch 'generator'
Browse files Browse the repository at this point in the history
  • Loading branch information
mar10 committed Sep 12, 2024
2 parents 4fd579a + 4ccd631 commit 16ca341
Show file tree
Hide file tree
Showing 9 changed files with 198 additions and 194 deletions.
7 changes: 3 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,17 @@

## 0.9.0 (unreleased)

- Add `Tree.build_random_tree()`
- Add `GenericNodeData`
- Add `Tree.build_random_tree()` (experimental).
- Add `GenericNodeData` as wrapper for `dict` data.
- Fixed #7 Tree.from_dict failing to recreate an arbitrary object tree with a mapper.

## 0.8.0 (2024-03-29)

- BREAKING: Drop Python 3.7 support (EoL 2023-06-27).
- `Tree.save()` accepts a `compression` argument that will enable compression.
`Tree.load()` can detect if the input file has a compression header and will
decompress automatically.
- New traversal methods `LEVEL_ORDER`, `LEVEL_ORDER_RTL`, `ZIGZAG`, `ZIGZAG_RTL`.
decompress transparently.
- New traversal methods `LEVEL_ORDER`, `LEVEL_ORDER_RTL`, `ZIGZAG`, `ZIGZAG_RTL`.
- New compact connector styles `'lines32c'`, `'round43c'`, ...
- Save as mermaid flow diagram.

Expand Down
4 changes: 2 additions & 2 deletions docs/sphinx/reference_guide.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@ Reference Guide
Class Overview
==============

nutree classes
Nutree Classes
--------------

.. inheritance-diagram:: nutree.tree nutree.node nutree.typed_tree nutree.common
:parts: 2
:private-bases:

Random tree generator
Random Tree Generator
---------------------

.. inheritance-diagram:: nutree.tree_generator
Expand Down
9 changes: 9 additions & 0 deletions docs/sphinx/rg_modules.rst
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,12 @@ nutree.common module
:show-inheritance:
:inherited-members:

nutree.tree_generator module
----------------------------

.. automodule:: nutree.tree_generator
:members:
:undoc-members:
:show-inheritance:
:inherited-members:

94 changes: 59 additions & 35 deletions docs/sphinx/ug_objects.rst
Original file line number Diff line number Diff line change
Expand Up @@ -131,26 +131,47 @@ Dictionaries (GenericNodeData)

Python
`dictionaries <https://docs.python.org/3/tutorial/datastructures.html#dictionaries>`_
are unhashable and cannot be used as node data objects. |br|
We can handle this in different ways:
are unhashable and cannot be used as node data objects::

1. Explicitly set the `data_id` when adding the dict: |br|
``tree.add({"name": "Alice", "age": 23, "guid": "{123-456}"}, data_id="{123-456}")``
2. Use a custom `calc_data_id` callback function that returns a unique key for
the data object (see example above).
3. Wrap the dict in :class:`~nutree.common.GenericNodeData`.
d = {"a": 1, "b": 2}
tree.add(d) # ERROR: raises `TypeError: unhashable type: 'dict'`

Adding Native Dictionaries
~~~~~~~~~~~~~~~~~~~~~~~~~~

We can handle this by explicitly setting the `data_id` when adding the dict::
node = tree.add({d, data_id="{123-456}")

The :class:`~nutree.common.GenericNodeData` class is a simple wrapper around a
dictionary that
assert node.data is d
assert node.data["a"] == 1

Alternatively, we can implement a custom `calc_data_id` callback function that
returns a unique key for the data object::

def _calc_id(tree, data):
if isinstance(data, dict):
return hash(data["guid"])
return hash(data)

- is hashable, so it can be used added to the tree as ``node.data``
tree = Tree(calc_data_id=_calc_id)

d = {"a": 1, "b": 2, "guid": "{123-456}"}
tree.add(d)

Wrapping Dictionaries
~~~~~~~~~~~~~~~~~~~~~

Finally, we can use the :class:`~nutree.common.GenericNodeData` which is a simple
wrapper around a dictionary that

- is hashable, so it can be added to the tree as ``node.data``
- stores a reference to the original dict internally as ``node.data._dict``
- allows readonly access to dict keys as shadow attributes, i.e.
``node.data._dict["name"]`` can be accessed as ``node.data.name``. |br|
If ``shadow_attrs=True`` is passed to the tree constructor, it can also be
accessed as ``node.name``. |br|
Note that shadow attributes are readonly.
- allows access to dict keys by index, i.e. ``node.data["name"]``
accessed as ``node.name``
- allows readonly access to dict keys by index, i.e. ``node.data["name"]``

Examples ::

Expand All @@ -160,11 +181,10 @@ Examples ::

d = {"a": 1, "b": 2}
obj = GenericNodeData(d)

We can now add the wrapped `dict` to the tree::

node = tree.add_child(obj)

We can now access the dict keys as attributes::

assert node.data._dict is d, "stored as reference"
assert node.data._dict["a"] == 1

Expand All @@ -187,13 +207,22 @@ GenericNodeData can also be initialized with keyword args like this::

obj = GenericNodeData(a=1, b=2)

.. warning::
The :class:`~nutree.common.GenericNodeData` provides a hash value because
any class that is hashable, so it can be used as a data object. However, the
hash value is NOT based on the internal dict but on the object itself. |br|
This means that two instances of GenericNodeData with the same dict content
will have different hash values.

.. warning::
The `shadow_attrs` feature is readonly, so you cannot modify the dict
through the shadow attributes. You need to access the dict directly for that.

Dataclasses
-----------

`Dataclasses <https://docs.python.org/3/library/dataclasses.html>`_ are a great way
to define simple classes that hold data. However, they are not hashable by default. |br|
We can handle this in different ways::
to define simple classes that hold data. However, they are not hashable by default::

from dataclasses import dataclass

Expand All @@ -205,32 +234,27 @@ We can handle this in different ways::

alice = Person("Alice", age=23, guid="{123-456}")

.. 1. Explicitly set the `data_id` when adding the dataclass instance.
.. ``tree.add(, data_id="{123-456}")``
.. 2. Use a custom `calc_data_id` function that returns a unique key for the data object.
.. 3. Make the dataclass hashable by adding a `__hash__` method.
.. 4. Make the dataclass ``frozen=True`` (or ``unsafe_hash=True``).
tree.add(alice) # ERROR: raises `TypeError: unhashable type: 'dict'`

Example: Explicitly set the `data_id` when adding the dataclass instance::
We can handle this in different ways byexplicitly set the `data_id` when adding
the dataclass instance::

tree.add(alice, data_id=alice.guid)

Example: make the dataclass hashable by adding a `__hash__` method::

@dataclass
class Person:
name: str
age: int
guid: str = None
Alternatively, we can implement a custom `calc_data_id` callback function that
returns a unique key for the data object::

def __hash__(self):
return hash(self.guid)
def _calc_id(tree, data):
if hasattr(data, "guid"):
return hash(data.guid)
return hash(data)

alice = Person("Alice", age=23, guid="{123-456}")
tree = Tree(calc_data_id=_calc_id)

tree.add(alice)

Example: Use a frozen dataclass instead, which is immutable and hashable by default::
Finally, we can use a frozen dataclass instead, which is immutable and hashable by
default (or pass ``unsafe_hash=True``)::

@dataclass(frozen=True)
class Person:
Expand Down
155 changes: 87 additions & 68 deletions docs/sphinx/ug_randomize.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ Generate Random Trees

Nutree can generate random tree structures from a structure definition.

.. warning::

This feature is experimental and may change in future versions.

Nutree can generate random tree structures from a structure definition.
This can be used to create hierarchical data for test, demo, or benchmarking of
*nutree* itself.
Expand Down Expand Up @@ -55,99 +59,114 @@ This definition is then passed to :meth:`tree.Tree.build_random_tree`::
Example::

structure_def = {
#: Name of the new tree (str, optiona)
# Name of the generated tree (optional)
"name": "fmea",
#: Types define the default properties of the nodes
# Types define the default properties of the gernated nodes
"types": {
#: Default properties for all node types
"*": { ... },
#: Specific default properties for each node type (optional)
"TYPE_1": { ... },
"TYPE_2": { ... },
...
},
#: Relations define the possible parent / child relationships between
#: node types and optionally override the default properties.
"relations": {
"__root__": {
"TYPE_1": {
":count": 10,
"ATTR_1": "Function {hier_idx}",
"expanded": True,
},
},
"function": {
"failure": {
":count": RangeRandomizer(1, 3),
"title": "Failure {hier_idx}",
},
},
"failure": {
"cause": {
":count": RangeRandomizer(1, 3),
"title": "Cause {hier_idx}",
},
"effect": {
":count": RangeRandomizer(1, 3),
"title": "Effect {hier_idx}",
},
# '*' Defines default properties for all node types (optional)
"*": {
":factory": GenericNodeData, # Default node class (optional)
},
# Specific default properties for each node type
"function": {"icon": "gear"},
"failure": {"icon": "exclamation"},
"cause": {"icon": "tools"},
"effect": {"icon": "lightning"},
},
}
tree = Tree.build_random_tree(structure_def)
tree.print()
assert type(tree) is Tree
assert tree.calc_height() == 3

Example::

structure_def = {
"name": "fmea",
#: Types define the default properties of the nodes
"types": {
#: Default properties for all node types
"*": {":factory": GenericNodeData},
#: Specific default properties for each node type
"function": {"icon": "bi bi-gear"},
"failure": {"icon": "bi bi-exclamation-triangle"},
"cause": {"icon": "bi bi-tools"},
"effect": {"icon": "bi bi-lightning"},
},
#: Relations define the possible parent / child relationships between
#: node types and optionally override the default properties.
# Relations define the possible parent / child relationships between
# node types and optionally override the default properties.
"relations": {
"__root__": {
"function": {
":count": 10,
"title": "Function {hier_idx}",
":count": 3,
"title": TextRandomizer(("{idx}: Provide $(Noun:plural)",)),
"details": BlindTextRandomizer(dialect="ipsum"),
"expanded": True,
},
},
"function": {
"failure": {
":count": RangeRandomizer(1, 3),
"title": "Failure {hier_idx}",
"title": TextRandomizer("$(Noun:plural) not provided"),
},
},
"failure": {
"cause": {
":count": RangeRandomizer(1, 3),
"title": "Cause {hier_idx}",
"title": TextRandomizer("$(Noun:plural) not provided"),
},
"effect": {
":count": RangeRandomizer(1, 3),
"title": "Effect {hier_idx}",
"title": TextRandomizer("$(Noun:plural) not provided"),
},
},
},
}
tree = Tree.build_random_tree(structure_def)
tree.print()
assert type(tree) is Tree
tree = TypedTree.build_random_tree(structure_def)
assert type(tree) is TypedTree
assert tree.calc_height() == 3
tree.print()

May produce::

TypedTree<'fmea'>
├── function → GenericNodeData<{'icon': 'gear', 'title': '1: Provide Seniors', 'details': 'Quis aute iure reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquid ex ea commodi consequat. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat.', 'expanded': True}>
│ ├── failure → GenericNodeData<{'icon': 'exclamation', 'title': 'Streets not provided'}>
│ │ ├── cause → GenericNodeData<{'icon': 'tools', 'title': 'Decisions not provided'}>
│ │ ├── effect → GenericNodeData<{'icon': 'lightning', 'title': 'Spaces not provided'}>
│ │ ╰── effect → GenericNodeData<{'icon': 'lightning', 'title': 'Kings not provided'}>
│ ╰── failure → GenericNodeData<{'icon': 'exclamation', 'title': 'Entertainments not provided'}>
│ ├── cause → GenericNodeData<{'icon': 'tools', 'title': 'Programs not provided'}>
│ ├── effect → GenericNodeData<{'icon': 'lightning', 'title': 'Dirts not provided'}>
│ ╰── effect → GenericNodeData<{'icon': 'lightning', 'title': 'Dimensions not provided'}>
├── function → GenericNodeData<{'icon': 'gear', 'title': '2: Provide Shots', 'details': 'Quis aute iure reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Quis aute iure reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum.', 'expanded': True}>
│ ├── failure → GenericNodeData<{'icon': 'exclamation', 'title': 'Trainers not provided'}>
│ │ ├── cause → GenericNodeData<{'icon': 'tools', 'title': 'Girlfriends not provided'}>
│ │ ├── cause → GenericNodeData<{'icon': 'tools', 'title': 'Noses not provided'}>
│ │ ├── effect → GenericNodeData<{'icon': 'lightning', 'title': 'Closets not provided'}>
│ │ ╰── effect → GenericNodeData<{'icon': 'lightning', 'title': 'Potentials not provided'}>
│ ╰── failure → GenericNodeData<{'icon': 'exclamation', 'title': 'Punches not provided'}>
│ ├── cause → GenericNodeData<{'icon': 'tools', 'title': 'Inevitables not provided'}>
│ ├── cause → GenericNodeData<{'icon': 'tools', 'title': 'Fronts not provided'}>
│ ╰── effect → GenericNodeData<{'icon': 'lightning', 'title': 'Worths not provided'}>
╰── function → GenericNodeData<{'icon': 'gear', 'title': '3: Provide Shots', 'details': 'Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Quis aute iure reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.', 'expanded': True}>
╰── failure → GenericNodeData<{'icon': 'exclamation', 'title': 'Recovers not provided'}>
├── cause → GenericNodeData<{'icon': 'tools', 'title': 'Viruses not provided'}>
├── effect → GenericNodeData<{'icon': 'lightning', 'title': 'Dirts not provided'}>
╰── effect → GenericNodeData<{'icon': 'lightning', 'title': 'Readings not provided'}>


**A few things to note**

- The generated tree contains nodes :class:`~common.GenericNodeData` as ``node.data``
value..

- Every ``node.data`` contains items from the structure definition except for
the ones starting with a colon, for example ``":count"``. |br|
The node items are merged with the default properties defined in the `types`
section.

- Randomizers are used to generate random data for each instance.
They derive from the :class:`~tree_generator.Randomizer` base class.

- The :class:`~tree_generator.TextRandomizer` and
:class:`~tree_generator.BlindTextRandomizer` classes are used to generate
random text using the `Fabulist <https://fabulist.readthedocs.io/>`_ library.

- :meth:`tree.Tree.build_random_tree` creates instances of :class:`~tree.Tree`, while
:meth:`typed_tree.TypedTree.build_random_tree` creates instances of
:class:`~typed_tree.TypedTree`.

- The generated tree contains instances of the :class:`~common.GenericNodeData`
class by default, but can be overridden for each node type by adding a
``":factory": CLASS`` entry.

tree2 = TypedTree.build_random_tree(structure_def)
tree2.print()
assert type(tree2) is TypedTree
assert tree2.calc_height() == 3
.. note::

The random text generator is based on the `Fabulist <https://fabulist.readthedocs.io/>`_
library and can use any of its providers to generate random data. |br|
Make sure to install the `fabulist` package to use the text randomizers
:class:`~tree_generator.TextRandomizer` and :class:`~tree_generator.BlindTextRandomizer`.
Loading

0 comments on commit 16ca341

Please sign in to comment.