Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Tree.build_random_tree() #9

Merged
merged 5 commits into from
Sep 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
# Changelog

## 0.8.1 (unreleased)
## 0.9.0 (unreleased)

- Add `Tree.build_random_tree()`
- Add `GenericNodeData`
- Fixed #7 Tree.from_dict failing to recreate an arbitrary object tree with a mapper.

## 0.8.0 (2024-03-29)
Expand Down
8 changes: 3 additions & 5 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,9 @@ name = "pypi"

[dev-packages]
black = { version = "~=24.3", extras = ["jupyter"] }
# coverage = "*"
fabulist="*"
isort = "*"
# pylint = "*"
# TODO: remove this line:
# pytest-html = "!=3.2.0" # wait for https://github.com/pytest-dev/pytest-html/pull/583
pytest = "*"
pytest-cov = "*"
PyYAML = "*"
Expand All @@ -22,14 +21,13 @@ tox = "*"
twine = "*"
wheel = "*"
yabs = "*"
nutree = {path = ".",editable = true}
ipykernel = "*"
notebook = "*"
nutree = {path = ".",editable = true}

[packages]

[requires]
python_version = "3.12"

[pipenv]
# allow_prereleases = true
1,567 changes: 796 additions & 771 deletions Pipfile.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ Nodes can be plain strings or objects <br>
(De)Serialize to (compressed) JSON <br>
Save as Mermaid flow diagram <br>
Different traversal methods <br>
Generate random trees <br>
Fully type annotated <br>
Convert to RDF graph <br>
Typed child nodes <br>
Expand Down
24 changes: 15 additions & 9 deletions docs/sphinx/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@

# General information about the project.
project = u'nutree'
copyright = u'2021-2023, <a href="https://wwwendt.de">Martin Wendt</a>'
copyright = u'2021-2024, <a href="https://wwwendt.de">Martin Wendt</a>'
author = u'Martin Wendt'

# The version info for the project you're documenting, acts as replacement for
Expand All @@ -130,19 +130,25 @@
#version = '1.0'
# The full version, including alpha/beta/rc tags.
#release = '1.0'
import pkg_resources
import importlib

try:
release = pkg_resources.get_distribution('nutree').version
# print( "release", release)
del pkg_resources
except pkg_resources.DistributionNotFound:
print('To build the documentation, The distribution information')
print('Has to be available. Either install the package into your')
# release = pkg_resources.get_distribution("nutree").version
release = importlib.metadata.version("nutree")
except importlib.metadata.PackageNotFoundError:
print("To build the documentation, The distribution information")
print("has to be available. Either install the package into your")
print('development environment or run "setup.py develop" to setup the')
print('metadata. A virtualenv is recommended!')
print("metadata. A virtualenv is recommended!")

print(f"sys.path: {sys.path}")
print(f"package_root: {package_root}")
for fn in os.listdir(package_root):
print("-", fn)
sys.exit(1)

del importlib.metadata

version = '.'.join(release.split('.')[:2])

# The language for content autogenerated by Sphinx. Refer to documentation
Expand Down
3 changes: 2 additions & 1 deletion docs/sphinx/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,9 @@ Nutree Facts
* :ref:`(De)Serialize to (compressed) JSON <serialize>`
* :ref:`Save as Mermaid flow diagram <save-mermaid>`
* :ref:`Different traversal methods <traversal>`
* :ref:`Fully type annotated <api-reference>`
* :ref:`Generate random trees <randomize>`
* :ref:`Convert to RDF graph <save-rdf>`
* :ref:`Fully type annotated <api-reference>`
* :ref:`Typed child nodes <typed-tree>`
* :ref:`Pretty print <pretty-print>`
* :ref:`Navigation <navigate>`
Expand Down
10 changes: 9 additions & 1 deletion docs/sphinx/reference_guide.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,18 @@ Reference Guide
Class Overview
==============

nutree classes
--------------

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

Random tree generator
---------------------

.. inheritance-diagram:: nutree.tree_generator
:parts: 2


.. API
Expand Down
10 changes: 8 additions & 2 deletions docs/sphinx/ug_basics.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ Basics

.. py:currentmodule:: nutree

.. admonition:: TL;DR

Nutree is a Python library for managing hierarchical data structures.
It stores arbitrary data objects in nodes and provides methods for
navigation, searching, and iteration.


Adding Nodes
------------
Expand Down Expand Up @@ -35,8 +41,8 @@ Nodes are usually created by adding a new data instance to a parent::

.. seealso::

See :doc:`ug_objects` for details on how to manage arbitrary objects instead
of plain strings.
See :doc:`ug_objects` for details on how to manage arbitrary objects, dicts,
etc. instead of plain strings.


Info and Navigation
Expand Down
4 changes: 4 additions & 0 deletions docs/sphinx/ug_clones.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ Multiple Instances ('Clones')

.. py:currentmodule:: nutree

.. admonition:: TL;DR

Nutree allows to store multiple references to the same data object in a tree.

Every :class:`~nutree.node.Node` instance is unique within the tree and
also has a unique `node.node_id` value.

Expand Down
4 changes: 4 additions & 0 deletions docs/sphinx/ug_diff.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ Diff and Merge

.. py:currentmodule:: nutree

.. admonition:: TL;DR

Nutree provides a `diff` method to compare two trees and calculate the differences.

The :meth:`~nutree.tree.Tree.diff` method compares a tree (`T0`) against another
one (`T1`) and returns a merged, annotated copy.

Expand Down
9 changes: 9 additions & 0 deletions docs/sphinx/ug_graphs.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,15 @@ Graphs

.. py:currentmodule:: nutree

.. admonition:: TL;DR

Nutree implements conversion to `DOT <https://en.wikipedia.org/wiki/DOT_(graph_description_language)>`_
and `Mermaid <https://mermaid.js.org>`_ formats. |br|
This allows to visualize trees as graphs in various formats like `png`, `svg`, etc. |br|
The :class:`~nutree.typed_tree.TypedTree` class introduces the concept of
`typed nodes`, which allows to generate labelled edges in the graph representation.


.. note::
:class:`~nutree.tree.Tree` (and :class:`~nutree.typed_tree.TypedTree` even
more so) has features that make mapping to a graph easy.
Expand Down
6 changes: 6 additions & 0 deletions docs/sphinx/ug_mutation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ Mutation

.. py:currentmodule:: nutree

.. admonition:: TL;DR

Nutree provides methods to modify the tree structure in-place. |br|
This includes adding, moving, and deleting nodes, as well as filtering and sorting.


Some in-place modifications are available::

# Tree
Expand Down
144 changes: 134 additions & 10 deletions docs/sphinx/ug_objects.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ Working with Objects

.. py:currentmodule:: nutree

.. admonition:: TL;DR

Nutree allows to store arbitrary objects in its nodes without the
need to modify them or derive from a common base class. |br|
It also supports shadow attributes for direct access to object attributes. |br|
Some objects like *dicts* or *dataclasses* are unhashable and require special
handling.

The previous examples used plain strings as data objects. However, any Python
object can be stored, as long as it is `hashable`.

Expand Down Expand Up @@ -37,7 +45,7 @@ We can add instances of these classes to our tree::
...

For bookkeeping, lookups, and serialization, every data object needs a `data_id`.
This value defaults to ``hash(data)``, which is good enough in many cases. ::
This value defaults to ``hash(data)``, which is good enough in many cases::

assert tree[alice].data_id == hash(alice)

Expand All @@ -46,7 +54,7 @@ to be useful for persistence. In our example, we already have object GUIDs, whic
we want to use instead. This can be achieved by passing a callback to the tree::

def _calc_id(tree, data):
if isinstance(data, fixture.Person):
if isinstance(data, Person):
return data.guid
return hash(data)

Expand Down Expand Up @@ -79,13 +87,12 @@ Shadow Attributes (Attribute Aliasing)

When storing arbitrary objects within a tree node, all its attributes must be
accessed through the ``node.data`` attribute. |br|
This can be simplified by using the ``shadow_attrs`` argument, which allow to
This can be simplified by using the ``shadow_attrs`` argument, which allows to
access ``node.data.age`` as ``node.age`` for example::

tree = Tree("Persons", shadow_attrs=True)
dev = tree.add(Department("Development"))
alice = Person("Alice", age=23, guid="{123-456}")
alice_node = dev.add(alice)
alice_node = tree.add(alice)

# Standard access using `node.data`:
assert alice_node.data is alice
Expand All @@ -96,15 +103,17 @@ access ``node.data.age`` as ``node.age`` for example::
assert alice_node.guid == "{123-456}"
assert alice_node.age == 23

# Note also: shadow attributes are readonly:
alice_node.age = 24 # ERROR: raises AttributeError

# But we can still modify the data object directly:
alice_node.data.age = 24 # OK!

# Note caveat: `node.name` is not shadowed, but a native property:
assert alice.data.name == "Alice"
assert alice.name == "Person<Alice, 23>"

# Note also: shadow attributes are readonly:
alice.age = 24 # ERROR: raises AttributeError
alice.data.age = 24 # OK!

.. note::
.. warning::

Aliasing only works for attribute names that are **not** part of the native
:class:`~nutree.node.Node` data model. So these attributes will always return
Expand All @@ -114,3 +123,118 @@ access ``node.data.age`` as ``node.age`` for example::

Note also that shadow attributes are readonly.


.. _generic-node-data:

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:

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

The :class:`~nutree.common.GenericNodeData` class is a simple wrapper around a
dictionary that

- is hashable, so it can be used 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"]``

Examples ::

from nutree import Tree, GenericNodeData

tree = Tree(shadow_attrs=True)

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

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

node = tree.add_child(obj)

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

assert node.data.a == 1, "accessible as data attribute"
assert node.data["a"] == 1, "accessible by index"

# Since we enabled shadow_attrs, this is also possible:
assert node.a == 1, "accessible as node attribute"

# Note: shadow attributes are readonly:
node.a = 99 # ERROR: raises AttributeError
node.data["a"] = 99 # ERROR: raises TypeError

# We need to access the dict directly to modify it
node.data._dict["a"] = 99
assert node.a == 99, "should reflect changes in dict"


GenericNodeData can also be initialized with keyword args like this::

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


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

from dataclasses import dataclass

@dataclass
class Person:
name: str
age: int
guid: str = None

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``).

Example: Explicitly 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

def __hash__(self):
return hash(self.guid)

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

tree.add(alice)

Example: Use a frozen dataclass instead, which is immutable and hashable by default::

@dataclass(frozen=True)
class Person:
name: str
age: int
guid: str = None

Loading
Loading