Skip to content

Commit 16ca341

Browse files
committed
Merge branch 'generator'
2 parents 4fd579a + 4ccd631 commit 16ca341

File tree

9 files changed

+198
-194
lines changed

9 files changed

+198
-194
lines changed

CHANGELOG.md

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,17 @@
22

33
## 0.9.0 (unreleased)
44

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

99
## 0.8.0 (2024-03-29)
1010

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

docs/sphinx/reference_guide.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,14 @@ Reference Guide
55
Class Overview
66
==============
77

8-
nutree classes
8+
Nutree Classes
99
--------------
1010

1111
.. inheritance-diagram:: nutree.tree nutree.node nutree.typed_tree nutree.common
1212
:parts: 2
1313
:private-bases:
1414

15-
Random tree generator
15+
Random Tree Generator
1616
---------------------
1717

1818
.. inheritance-diagram:: nutree.tree_generator

docs/sphinx/rg_modules.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,12 @@ nutree.common module
4242
:show-inheritance:
4343
:inherited-members:
4444

45+
nutree.tree_generator module
46+
----------------------------
47+
48+
.. automodule:: nutree.tree_generator
49+
:members:
50+
:undoc-members:
51+
:show-inheritance:
52+
:inherited-members:
53+

docs/sphinx/ug_objects.rst

Lines changed: 59 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -131,26 +131,47 @@ Dictionaries (GenericNodeData)
131131

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

137-
1. Explicitly set the `data_id` when adding the dict: |br|
138-
``tree.add({"name": "Alice", "age": 23, "guid": "{123-456}"}, data_id="{123-456}")``
139-
2. Use a custom `calc_data_id` callback function that returns a unique key for
140-
the data object (see example above).
141-
3. Wrap the dict in :class:`~nutree.common.GenericNodeData`.
136+
d = {"a": 1, "b": 2}
137+
tree.add(d) # ERROR: raises `TypeError: unhashable type: 'dict'`
138+
139+
Adding Native Dictionaries
140+
~~~~~~~~~~~~~~~~~~~~~~~~~~
141+
142+
We can handle this by explicitly setting the `data_id` when adding the dict::
143+
144+
node = tree.add({d, data_id="{123-456}")
142145

143-
The :class:`~nutree.common.GenericNodeData` class is a simple wrapper around a
144-
dictionary that
146+
assert node.data is d
147+
assert node.data["a"] == 1
148+
149+
Alternatively, we can implement a custom `calc_data_id` callback function that
150+
returns a unique key for the data object::
151+
152+
def _calc_id(tree, data):
153+
if isinstance(data, dict):
154+
return hash(data["guid"])
155+
return hash(data)
145156

146-
- is hashable, so it can be used added to the tree as ``node.data``
157+
tree = Tree(calc_data_id=_calc_id)
158+
159+
d = {"a": 1, "b": 2, "guid": "{123-456}"}
160+
tree.add(d)
161+
162+
Wrapping Dictionaries
163+
~~~~~~~~~~~~~~~~~~~~~
164+
165+
Finally, we can use the :class:`~nutree.common.GenericNodeData` which is a simple
166+
wrapper around a dictionary that
167+
168+
- is hashable, so it can be added to the tree as ``node.data``
147169
- stores a reference to the original dict internally as ``node.data._dict``
148170
- allows readonly access to dict keys as shadow attributes, i.e.
149171
``node.data._dict["name"]`` can be accessed as ``node.data.name``. |br|
150172
If ``shadow_attrs=True`` is passed to the tree constructor, it can also be
151-
accessed as ``node.name``. |br|
152-
Note that shadow attributes are readonly.
153-
- allows access to dict keys by index, i.e. ``node.data["name"]``
173+
accessed as ``node.name``
174+
- allows readonly access to dict keys by index, i.e. ``node.data["name"]``
154175

155176
Examples ::
156177

@@ -160,11 +181,10 @@ Examples ::
160181

161182
d = {"a": 1, "b": 2}
162183
obj = GenericNodeData(d)
163-
164-
We can now add the wrapped `dict` to the tree::
165-
166184
node = tree.add_child(obj)
167185

186+
We can now access the dict keys as attributes::
187+
168188
assert node.data._dict is d, "stored as reference"
169189
assert node.data._dict["a"] == 1
170190

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

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

210+
.. warning::
211+
The :class:`~nutree.common.GenericNodeData` provides a hash value because
212+
any class that is hashable, so it can be used as a data object. However, the
213+
hash value is NOT based on the internal dict but on the object itself. |br|
214+
This means that two instances of GenericNodeData with the same dict content
215+
will have different hash values.
216+
217+
.. warning::
218+
The `shadow_attrs` feature is readonly, so you cannot modify the dict
219+
through the shadow attributes. You need to access the dict directly for that.
190220

191221
Dataclasses
192222
-----------
193223

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

198227
from dataclasses import dataclass
199228

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

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

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

214-
Example: Explicitly set the `data_id` when adding the dataclass instance::
239+
We can handle this in different ways byexplicitly set the `data_id` when adding
240+
the dataclass instance::
215241

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

218-
Example: make the dataclass hashable by adding a `__hash__` method::
219-
220-
@dataclass
221-
class Person:
222-
name: str
223-
age: int
224-
guid: str = None
244+
Alternatively, we can implement a custom `calc_data_id` callback function that
245+
returns a unique key for the data object::
225246

226-
def __hash__(self):
227-
return hash(self.guid)
247+
def _calc_id(tree, data):
248+
if hasattr(data, "guid"):
249+
return hash(data.guid)
250+
return hash(data)
228251

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

231254
tree.add(alice)
232255

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

235259
@dataclass(frozen=True)
236260
class Person:

docs/sphinx/ug_randomize.rst

Lines changed: 87 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ Generate Random Trees
1010

1111
Nutree can generate random tree structures from a structure definition.
1212

13+
.. warning::
14+
15+
This feature is experimental and may change in future versions.
16+
1317
Nutree can generate random tree structures from a structure definition.
1418
This can be used to create hierarchical data for test, demo, or benchmarking of
1519
*nutree* itself.
@@ -55,99 +59,114 @@ This definition is then passed to :meth:`tree.Tree.build_random_tree`::
5559
Example::
5660

5761
structure_def = {
58-
#: Name of the new tree (str, optiona)
62+
# Name of the generated tree (optional)
5963
"name": "fmea",
60-
#: Types define the default properties of the nodes
64+
# Types define the default properties of the gernated nodes
6165
"types": {
62-
#: Default properties for all node types
63-
"*": { ... },
64-
#: Specific default properties for each node type (optional)
65-
"TYPE_1": { ... },
66-
"TYPE_2": { ... },
67-
...
68-
},
69-
#: Relations define the possible parent / child relationships between
70-
#: node types and optionally override the default properties.
71-
"relations": {
72-
"__root__": {
73-
"TYPE_1": {
74-
":count": 10,
75-
"ATTR_1": "Function {hier_idx}",
76-
"expanded": True,
77-
},
78-
},
79-
"function": {
80-
"failure": {
81-
":count": RangeRandomizer(1, 3),
82-
"title": "Failure {hier_idx}",
83-
},
84-
},
85-
"failure": {
86-
"cause": {
87-
":count": RangeRandomizer(1, 3),
88-
"title": "Cause {hier_idx}",
89-
},
90-
"effect": {
91-
":count": RangeRandomizer(1, 3),
92-
"title": "Effect {hier_idx}",
93-
},
66+
# '*' Defines default properties for all node types (optional)
67+
"*": {
68+
":factory": GenericNodeData, # Default node class (optional)
9469
},
70+
# Specific default properties for each node type
71+
"function": {"icon": "gear"},
72+
"failure": {"icon": "exclamation"},
73+
"cause": {"icon": "tools"},
74+
"effect": {"icon": "lightning"},
9575
},
96-
}
97-
tree = Tree.build_random_tree(structure_def)
98-
tree.print()
99-
assert type(tree) is Tree
100-
assert tree.calc_height() == 3
101-
102-
Example::
103-
104-
structure_def = {
105-
"name": "fmea",
106-
#: Types define the default properties of the nodes
107-
"types": {
108-
#: Default properties for all node types
109-
"*": {":factory": GenericNodeData},
110-
#: Specific default properties for each node type
111-
"function": {"icon": "bi bi-gear"},
112-
"failure": {"icon": "bi bi-exclamation-triangle"},
113-
"cause": {"icon": "bi bi-tools"},
114-
"effect": {"icon": "bi bi-lightning"},
115-
},
116-
#: Relations define the possible parent / child relationships between
117-
#: node types and optionally override the default properties.
76+
# Relations define the possible parent / child relationships between
77+
# node types and optionally override the default properties.
11878
"relations": {
11979
"__root__": {
12080
"function": {
121-
":count": 10,
122-
"title": "Function {hier_idx}",
81+
":count": 3,
82+
"title": TextRandomizer(("{idx}: Provide $(Noun:plural)",)),
83+
"details": BlindTextRandomizer(dialect="ipsum"),
12384
"expanded": True,
12485
},
12586
},
12687
"function": {
12788
"failure": {
12889
":count": RangeRandomizer(1, 3),
129-
"title": "Failure {hier_idx}",
90+
"title": TextRandomizer("$(Noun:plural) not provided"),
13091
},
13192
},
13293
"failure": {
13394
"cause": {
13495
":count": RangeRandomizer(1, 3),
135-
"title": "Cause {hier_idx}",
96+
"title": TextRandomizer("$(Noun:plural) not provided"),
13697
},
13798
"effect": {
13899
":count": RangeRandomizer(1, 3),
139-
"title": "Effect {hier_idx}",
100+
"title": TextRandomizer("$(Noun:plural) not provided"),
140101
},
141102
},
142103
},
143104
}
144-
tree = Tree.build_random_tree(structure_def)
145-
tree.print()
146-
assert type(tree) is Tree
105+
106+
tree = TypedTree.build_random_tree(structure_def)
107+
108+
assert type(tree) is TypedTree
147109
assert tree.calc_height() == 3
110+
111+
tree.print()
112+
113+
May produce::
114+
115+
TypedTree<'fmea'>
116+
├── 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}>
117+
│ ├── failure → GenericNodeData<{'icon': 'exclamation', 'title': 'Streets not provided'}>
118+
│ │ ├── cause → GenericNodeData<{'icon': 'tools', 'title': 'Decisions not provided'}>
119+
│ │ ├── effect → GenericNodeData<{'icon': 'lightning', 'title': 'Spaces not provided'}>
120+
│ │ ╰── effect → GenericNodeData<{'icon': 'lightning', 'title': 'Kings not provided'}>
121+
│ ╰── failure → GenericNodeData<{'icon': 'exclamation', 'title': 'Entertainments not provided'}>
122+
│ ├── cause → GenericNodeData<{'icon': 'tools', 'title': 'Programs not provided'}>
123+
│ ├── effect → GenericNodeData<{'icon': 'lightning', 'title': 'Dirts not provided'}>
124+
│ ╰── effect → GenericNodeData<{'icon': 'lightning', 'title': 'Dimensions not provided'}>
125+
├── 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}>
126+
│ ├── failure → GenericNodeData<{'icon': 'exclamation', 'title': 'Trainers not provided'}>
127+
│ │ ├── cause → GenericNodeData<{'icon': 'tools', 'title': 'Girlfriends not provided'}>
128+
│ │ ├── cause → GenericNodeData<{'icon': 'tools', 'title': 'Noses not provided'}>
129+
│ │ ├── effect → GenericNodeData<{'icon': 'lightning', 'title': 'Closets not provided'}>
130+
│ │ ╰── effect → GenericNodeData<{'icon': 'lightning', 'title': 'Potentials not provided'}>
131+
│ ╰── failure → GenericNodeData<{'icon': 'exclamation', 'title': 'Punches not provided'}>
132+
│ ├── cause → GenericNodeData<{'icon': 'tools', 'title': 'Inevitables not provided'}>
133+
│ ├── cause → GenericNodeData<{'icon': 'tools', 'title': 'Fronts not provided'}>
134+
│ ╰── effect → GenericNodeData<{'icon': 'lightning', 'title': 'Worths not provided'}>
135+
╰── 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}>
136+
╰── failure → GenericNodeData<{'icon': 'exclamation', 'title': 'Recovers not provided'}>
137+
├── cause → GenericNodeData<{'icon': 'tools', 'title': 'Viruses not provided'}>
138+
├── effect → GenericNodeData<{'icon': 'lightning', 'title': 'Dirts not provided'}>
139+
╰── effect → GenericNodeData<{'icon': 'lightning', 'title': 'Readings not provided'}>
140+
141+
142+
**A few things to note**
143+
144+
- The generated tree contains nodes :class:`~common.GenericNodeData` as ``node.data``
145+
value..
146+
147+
- Every ``node.data`` contains items from the structure definition except for
148+
the ones starting with a colon, for example ``":count"``. |br|
149+
The node items are merged with the default properties defined in the `types`
150+
section.
151+
152+
- Randomizers are used to generate random data for each instance.
153+
They derive from the :class:`~tree_generator.Randomizer` base class.
154+
155+
- The :class:`~tree_generator.TextRandomizer` and
156+
:class:`~tree_generator.BlindTextRandomizer` classes are used to generate
157+
random text using the `Fabulist <https://fabulist.readthedocs.io/>`_ library.
158+
159+
- :meth:`tree.Tree.build_random_tree` creates instances of :class:`~tree.Tree`, while
160+
:meth:`typed_tree.TypedTree.build_random_tree` creates instances of
161+
:class:`~typed_tree.TypedTree`.
162+
163+
- The generated tree contains instances of the :class:`~common.GenericNodeData`
164+
class by default, but can be overridden for each node type by adding a
165+
``":factory": CLASS`` entry.
148166

149-
tree2 = TypedTree.build_random_tree(structure_def)
150-
tree2.print()
151-
assert type(tree2) is TypedTree
152-
assert tree2.calc_height() == 3
167+
.. note::
153168

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

0 commit comments

Comments
 (0)