Adding upstream version 1.6.4.
Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
parent
a5555eb4a1
commit
d5b8e0af0d
42 changed files with 3857 additions and 0 deletions
2
.github/CODEOWNERS
vendored
Normal file
2
.github/CODEOWNERS
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
* @caesar0301
|
||||||
|
* @liamlundy
|
39
.github/workflows/pypi-publish.yml
vendored
Normal file
39
.github/workflows/pypi-publish.yml
vendored
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
# This workflow will upload a Python Package using Twine when a release is created
|
||||||
|
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries
|
||||||
|
|
||||||
|
# This workflow uses actions that are not certified by GitHub.
|
||||||
|
# They are provided by a third-party and are governed by
|
||||||
|
# separate terms of service, privacy policy, and support
|
||||||
|
# documentation.
|
||||||
|
|
||||||
|
name: Upload Python Package
|
||||||
|
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [published]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v3
|
||||||
|
with:
|
||||||
|
python-version: '3.x'
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install build
|
||||||
|
- name: Build package
|
||||||
|
run: python -m build
|
||||||
|
- name: Publish package
|
||||||
|
uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29
|
||||||
|
with:
|
||||||
|
user: __token__
|
||||||
|
password: ${{ secrets.PYPI_API_TOKEN }}
|
59
.github/workflows/python-package.yml
vendored
Normal file
59
.github/workflows/python-package.yml
vendored
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
# This workflow will install Python dependencies, run tests and lint with a variety of Python versions
|
||||||
|
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python
|
||||||
|
name: Package testing
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ "master", "dev" ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ "master" ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-pre37:
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
python-version: ["2.7"]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
|
uses: actions/setup-python@v3
|
||||||
|
with:
|
||||||
|
python-version: ${{ matrix.python-version }}
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
|
||||||
|
if [ -f requirements-t-pre37.txt ]; then pip install -r requirements-t-pre37.txt; fi
|
||||||
|
- name: Test with nosetests
|
||||||
|
run: |
|
||||||
|
./scripts/testing.sh
|
||||||
|
|
||||||
|
build:
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
|
uses: actions/setup-python@v3
|
||||||
|
with:
|
||||||
|
python-version: ${{ matrix.python-version }}
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
|
||||||
|
if [ -f requirements-t.txt ]; then pip install -r requirements-t.txt; fi
|
||||||
|
- name: Lint with flake8
|
||||||
|
run: |
|
||||||
|
./scripts/flake8.sh
|
||||||
|
- name: Test with pytest
|
||||||
|
run: |
|
||||||
|
pytest
|
41
.gitignore
vendored
Normal file
41
.gitignore
vendored
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
*.py[cod]
|
||||||
|
.idea
|
||||||
|
env/*
|
||||||
|
.python-version
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Packages
|
||||||
|
*.egg
|
||||||
|
*.egg-info
|
||||||
|
dist
|
||||||
|
build
|
||||||
|
eggs
|
||||||
|
parts
|
||||||
|
bin
|
||||||
|
var
|
||||||
|
sdist
|
||||||
|
develop-eggs
|
||||||
|
.installed.cfg
|
||||||
|
lib
|
||||||
|
lib64
|
||||||
|
__pycache__
|
||||||
|
\.vscode/*
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
.coverage
|
||||||
|
.tox
|
||||||
|
nosetests.xml
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
|
||||||
|
# Mr Developer
|
||||||
|
.mr.developer.cfg
|
||||||
|
.project
|
||||||
|
.pydevproject
|
||||||
|
*.swp
|
15
.travis.yml
Normal file
15
.travis.yml
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
language: python
|
||||||
|
python:
|
||||||
|
- "2.7"
|
||||||
|
- "3.4"
|
||||||
|
- "3.5"
|
||||||
|
- "3.6"
|
||||||
|
- "3.7"
|
||||||
|
- "3.8"
|
||||||
|
|
||||||
|
install:
|
||||||
|
- "python setup.py install"
|
||||||
|
|
||||||
|
script:
|
||||||
|
- pip install -r requirements-testing.txt
|
||||||
|
- sh testing.sh
|
10
AUTHORS
Normal file
10
AUTHORS
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
# Contributors
|
||||||
|
|
||||||
|
* Brett Alistair Kromkamp (brettkromkamp@gmail.com): Post basic idea online.
|
||||||
|
* Xiaming Chen (chenxm35@gmail.com): Finished primary parts and made the library freely public.
|
||||||
|
* Holger Bast (holgerbast@gmx.de): Replaced list with `dict` for fast node index and optimized the performance.
|
||||||
|
* Ilya Kuprik (ilya-spy@ynadex.ru): Added ZIGZAG tree-walk algorithm to tree traversal.
|
||||||
|
|
||||||
|
and other committers etc.
|
||||||
|
|
||||||
|
Thank you all.
|
53
HISTORY
Normal file
53
HISTORY
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
# History
|
||||||
|
|
||||||
|
Mar 27, 2023 V1.6.3
|
||||||
|
Apply to black code style. Migrate future to six
|
||||||
|
|
||||||
|
|
||||||
|
Oct 13, 2019 V1.5.6
|
||||||
|
Add ability to independently mutate multiple shallow copies of same initial tree.
|
||||||
|
|
||||||
|
|
||||||
|
Aug 10, 2017 V1.4.0
|
||||||
|
Abandon supporting Python 3.2 since v1.4.0.
|
||||||
|
|
||||||
|
|
||||||
|
Mar 27, 2014 V1.2.6
|
||||||
|
Node identifier supports multiple data types beyond int and str.
|
||||||
|
Finish depth() and leaves() routines.
|
||||||
|
Add siblings() routine.
|
||||||
|
Add unit test to the whole module.
|
||||||
|
|
||||||
|
|
||||||
|
Dec 21, 2013 V1.2.5
|
||||||
|
Add root() routine to Tree class.
|
||||||
|
Add all_nodes() routine to Tree class.
|
||||||
|
|
||||||
|
|
||||||
|
May 1, 2013
|
||||||
|
Pulish the module on PyPI with version 1.2 under GNU General Pulic License
|
||||||
|
version 3.
|
||||||
|
|
||||||
|
|
||||||
|
Mar 21, 2013
|
||||||
|
Replace the list implementation with dict to improve performance to a
|
||||||
|
large extend. Thanks to @aronadaal
|
||||||
|
|
||||||
|
|
||||||
|
Dec 30, 2012
|
||||||
|
Make a pretty print routine to the tree and give some examples in README
|
||||||
|
|
||||||
|
|
||||||
|
Dec 19, 2012
|
||||||
|
Little modification and add get_node routine by @pgebhard.
|
||||||
|
|
||||||
|
|
||||||
|
Jul 13, 2012
|
||||||
|
Add rsearch routines to implement reversed searching from leaf to tree root.
|
||||||
|
|
||||||
|
|
||||||
|
Jul 07, 2012
|
||||||
|
Initiate the project on GitHub.
|
||||||
|
For my research paper, I need the python implementation of tree structure.
|
||||||
|
Based on the imcompleted code of Brett A. Kromkamp, I finished all methods
|
||||||
|
and added subtree and paste routines.
|
16
INSTALL
Normal file
16
INSTALL
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
Thanks for downloading treelib.
|
||||||
|
|
||||||
|
To install it, make sure you have Python 2.6 or greater installed. Then run
|
||||||
|
this command from the command prompt:
|
||||||
|
|
||||||
|
python setup.py install
|
||||||
|
|
||||||
|
If you're upgrading from a previous version, you need to remove it first.
|
||||||
|
|
||||||
|
AS AN ALTERNATIVE, you can just copy the entire "treelib" directory to Python's
|
||||||
|
site-packages directory, which is located wherever your Python installation
|
||||||
|
lives. Some places you might check are:
|
||||||
|
|
||||||
|
/usr/lib/python2.7/site-packages (Unix, Python 2.7)
|
||||||
|
/usr/lib/python2.6/site-packages (Unix, Python 2.6)
|
||||||
|
C:\\PYTHON\site-packages (Windows)
|
21
LICENSE
Normal file
21
LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
treelib - Simple to use for you.
|
||||||
|
Python 2/3 Tree Implementation
|
||||||
|
|
||||||
|
Copyright (C) 2011
|
||||||
|
Brett Alistair Kromkamp - brettkromkamp@gmail.com
|
||||||
|
Copyright (C) 2012-2017
|
||||||
|
Xiaming Chen - chenxm35@gmail.com
|
||||||
|
and other contributors.
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
8
MANIFEST.in
Normal file
8
MANIFEST.in
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
include README*
|
||||||
|
include HISTORY*
|
||||||
|
include LICENSE*
|
||||||
|
include MANIFEST*
|
||||||
|
include *.py
|
||||||
|
recursive-include treelib *.py
|
||||||
|
recursive-include tests *.py
|
||||||
|
recursive-include examples *.py
|
38
README.md
Normal file
38
README.md
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
# treelib
|
||||||
|
|
||||||
|
Tree implementation in python: simple for you to use.
|
||||||
|
|
||||||
|
[![Build Status](https://github.com/caesar0301/treelib/actions/workflows/python-package.yml/badge.svg)](https://github.com/caesar0301/treelib/actions)
|
||||||
|
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
|
||||||
|
[![Documentation Status](https://readthedocs.org/projects/treelib/badge/?version=latest)](http://treelib.readthedocs.io/en/latest/?badge=latest)
|
||||||
|
[![Status](https://img.shields.io/pypi/status/treelib.svg)](https://pypi.python.org/pypi/treelib)
|
||||||
|
[![Latest](https://img.shields.io/pypi/v/treelib.svg)](https://pypi.python.org/pypi/treelib)
|
||||||
|
[![PyV](https://img.shields.io/pypi/pyversions/treelib.svg)](https://pypi.python.org/pypi/treelib)
|
||||||
|
[![PyPI download month](https://img.shields.io/pypi/dm/treelib.svg)](https://pypi.python.org/pypi/treelib/)
|
||||||
|
[![GitHub contributors](https://img.shields.io/github/contributors/caesar0301/treelib.svg)](https://GitHub.com/caesar0301/treelib/graphs/contributors/)
|
||||||
|
|
||||||
|
[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com)
|
||||||
|
[![GitHub pull-requests](https://img.shields.io/github/issues-pr/caesar0301/treelib.svg)](https://GitHub.com/caesar0301/treelib/pulls)
|
||||||
|
[![GitHub pull-requests closed](https://img.shields.io/github/issues-pr-closed/caesar0301/treelib.svg)](https://GitHub.com/caesar0301/treelib/pulls?q=is%3Apr+is%3Aclosed)
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
pip install -U treelib
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
For installation, APIs and examples, see http://treelib.readthedocs.io/en/latest/
|
||||||
|
|
||||||
|
## Code Style
|
||||||
|
|
||||||
|
`treelib` complies with [black](https://github.com/psf/black) formatter and
|
||||||
|
specific [flake8 validations](https://github.com/caesar0301/treelib/blob/master/scripts/flake8.sh).
|
||||||
|
Before creating a pull request, please make sure you pass the local validation
|
||||||
|
with `scripts/flake8.sh`.
|
||||||
|
|
||||||
|
## Contributors
|
||||||
|
|
||||||
|
Thank you all,
|
||||||
|
[committers](https://github.com/caesar0301/treelib/graphs/contributors).
|
||||||
|
|
||||||
|
[![ForTheBadge built-with-love](http://ForTheBadge.com/images/badges/built-with-love.svg)](https://GitHub.com/Naereen/)
|
19
docs/Makefile
Normal file
19
docs/Makefile
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
# Minimal makefile for Sphinx documentation
|
||||||
|
#
|
||||||
|
|
||||||
|
# You can set these variables from the command line.
|
||||||
|
SPHINXOPTS =
|
||||||
|
SPHINXBUILD = sphinx-build
|
||||||
|
SOURCEDIR = source
|
||||||
|
BUILDDIR = build
|
||||||
|
|
||||||
|
# Put it first so that "make" without argument is like "make help".
|
||||||
|
help:
|
||||||
|
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||||
|
|
||||||
|
.PHONY: help Makefile
|
||||||
|
|
||||||
|
# Catch-all target: route all unknown targets to Sphinx using the new
|
||||||
|
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
|
||||||
|
%: Makefile
|
||||||
|
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
35
docs/make.bat
Normal file
35
docs/make.bat
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
@ECHO OFF
|
||||||
|
|
||||||
|
pushd %~dp0
|
||||||
|
|
||||||
|
REM Command file for Sphinx documentation
|
||||||
|
|
||||||
|
if "%SPHINXBUILD%" == "" (
|
||||||
|
set SPHINXBUILD=sphinx-build
|
||||||
|
)
|
||||||
|
set SOURCEDIR=source
|
||||||
|
set BUILDDIR=build
|
||||||
|
|
||||||
|
if "%1" == "" goto help
|
||||||
|
|
||||||
|
%SPHINXBUILD% >NUL 2>NUL
|
||||||
|
if errorlevel 9009 (
|
||||||
|
echo.
|
||||||
|
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
|
||||||
|
echo.installed, then set the SPHINXBUILD environment variable to point
|
||||||
|
echo.to the full path of the 'sphinx-build' executable. Alternatively you
|
||||||
|
echo.may add the Sphinx directory to PATH.
|
||||||
|
echo.
|
||||||
|
echo.If you don't have Sphinx installed, grab it from
|
||||||
|
echo.http://sphinx-doc.org/
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
|
||||||
|
goto end
|
||||||
|
|
||||||
|
:help
|
||||||
|
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
|
||||||
|
|
||||||
|
:end
|
||||||
|
popd
|
12
docs/publish.sh
Executable file
12
docs/publish.sh
Executable file
|
@ -0,0 +1,12 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
SOURCE=source
|
||||||
|
PYLIB=../treelib
|
||||||
|
TARGET=html
|
||||||
|
BUILT=build
|
||||||
|
|
||||||
|
rm -rf $BUILD
|
||||||
|
sphinx-apidoc -o $SOURCE $PYLIB
|
||||||
|
make $TARGET
|
||||||
|
touch $BUILT/$TARGET/.nojekyll
|
||||||
|
ghp-import -p $BUILT/$TARGET
|
2
docs/requirements.txt
Normal file
2
docs/requirements.txt
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
ghp-import==0.5.5
|
||||||
|
sphinx==1.8.1
|
187
docs/source/conf.py
Normal file
187
docs/source/conf.py
Normal file
|
@ -0,0 +1,187 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# Configuration file for the Sphinx documentation builder.
|
||||||
|
#
|
||||||
|
# This file does only contain a selection of the most common options. For a
|
||||||
|
# full list see the documentation:
|
||||||
|
# http://www.sphinx-doc.org/en/master/config
|
||||||
|
|
||||||
|
# -- Path setup --------------------------------------------------------------
|
||||||
|
|
||||||
|
# If extensions (or modules to document with autodoc) are in another directory,
|
||||||
|
# add these directories to sys.path here. If the directory is relative to the
|
||||||
|
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||||
|
#
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
sys.path.insert(0, os.path.abspath('../..'))
|
||||||
|
|
||||||
|
from setup import __version__
|
||||||
|
|
||||||
|
# -- Project information -----------------------------------------------------
|
||||||
|
|
||||||
|
project = u'treelib'
|
||||||
|
copyright = u'2018, Xiaming Chen'
|
||||||
|
author = u'Xiaming Chen'
|
||||||
|
|
||||||
|
# The short X.Y version
|
||||||
|
version = __version__
|
||||||
|
# The full version, including alpha/beta/rc tags
|
||||||
|
release = __version__
|
||||||
|
|
||||||
|
|
||||||
|
# -- General configuration ---------------------------------------------------
|
||||||
|
|
||||||
|
# If your documentation needs a minimal Sphinx version, state it here.
|
||||||
|
#
|
||||||
|
# needs_sphinx = '1.0'
|
||||||
|
|
||||||
|
# Add any Sphinx extension module names here, as strings. They can be
|
||||||
|
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||||
|
# ones.
|
||||||
|
extensions = [
|
||||||
|
'sphinx.ext.autodoc',
|
||||||
|
'sphinx.ext.coverage',
|
||||||
|
'sphinx.ext.mathjax',
|
||||||
|
'sphinx.ext.viewcode',
|
||||||
|
'sphinx.ext.githubpages',
|
||||||
|
]
|
||||||
|
|
||||||
|
# Add any paths that contain templates here, relative to this directory.
|
||||||
|
templates_path = ['_templates']
|
||||||
|
|
||||||
|
# The suffix(es) of source filenames.
|
||||||
|
# You can specify multiple suffix as a list of string:
|
||||||
|
#
|
||||||
|
# source_suffix = ['.rst', '.md']
|
||||||
|
source_suffix = '.rst'
|
||||||
|
|
||||||
|
# The master toctree document.
|
||||||
|
master_doc = 'index'
|
||||||
|
|
||||||
|
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||||
|
# for a list of supported languages.
|
||||||
|
#
|
||||||
|
# This is also used if you do content translation via gettext catalogs.
|
||||||
|
# Usually you set "language" from the command line for these cases.
|
||||||
|
language = None
|
||||||
|
|
||||||
|
# List of patterns, relative to source directory, that match files and
|
||||||
|
# directories to ignore when looking for source files.
|
||||||
|
# This pattern also affects html_static_path and html_extra_path.
|
||||||
|
exclude_patterns = []
|
||||||
|
|
||||||
|
# The name of the Pygments (syntax highlighting) style to use.
|
||||||
|
pygments_style = None
|
||||||
|
|
||||||
|
|
||||||
|
# -- Options for HTML output -------------------------------------------------
|
||||||
|
|
||||||
|
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||||
|
# a list of builtin themes.
|
||||||
|
#
|
||||||
|
html_theme = 'alabaster'
|
||||||
|
|
||||||
|
# Theme options are theme-specific and customize the look and feel of a theme
|
||||||
|
# further. For a list of options available for each theme, see the
|
||||||
|
# documentation.
|
||||||
|
#
|
||||||
|
# html_theme_options = {}
|
||||||
|
|
||||||
|
# Add any paths that contain custom static files (such as style sheets) here,
|
||||||
|
# relative to this directory. They are copied after the builtin static files,
|
||||||
|
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||||
|
html_static_path = ['_static']
|
||||||
|
|
||||||
|
# Custom sidebar templates, must be a dictionary that maps document names
|
||||||
|
# to template names.
|
||||||
|
#
|
||||||
|
# The default sidebars (for documents that don't match any pattern) are
|
||||||
|
# defined by theme itself. Builtin themes are using these templates by
|
||||||
|
# default: ``['localtoc.html', 'relations.html', 'sourcelink.html',
|
||||||
|
# 'searchbox.html']``.
|
||||||
|
#
|
||||||
|
# html_sidebars = {}
|
||||||
|
|
||||||
|
|
||||||
|
# -- Options for HTMLHelp output ---------------------------------------------
|
||||||
|
|
||||||
|
# Output file base name for HTML help builder.
|
||||||
|
htmlhelp_basename = 'treelibdoc'
|
||||||
|
|
||||||
|
|
||||||
|
# -- Options for LaTeX output ------------------------------------------------
|
||||||
|
|
||||||
|
latex_elements = {
|
||||||
|
# The paper size ('letterpaper' or 'a4paper').
|
||||||
|
#
|
||||||
|
# 'papersize': 'letterpaper',
|
||||||
|
|
||||||
|
# The font size ('10pt', '11pt' or '12pt').
|
||||||
|
#
|
||||||
|
# 'pointsize': '10pt',
|
||||||
|
|
||||||
|
# Additional stuff for the LaTeX preamble.
|
||||||
|
#
|
||||||
|
# 'preamble': '',
|
||||||
|
|
||||||
|
# Latex figure (float) alignment
|
||||||
|
#
|
||||||
|
# 'figure_align': 'htbp',
|
||||||
|
}
|
||||||
|
|
||||||
|
# Grouping the document tree into LaTeX files. List of tuples
|
||||||
|
# (source start file, target name, title,
|
||||||
|
# author, documentclass [howto, manual, or own class]).
|
||||||
|
latex_documents = [
|
||||||
|
(master_doc, 'treelib.tex', u'treelib Documentation',
|
||||||
|
u'Xiaming Chen', 'manual'),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# -- Options for manual page output ------------------------------------------
|
||||||
|
|
||||||
|
# One entry per manual page. List of tuples
|
||||||
|
# (source start file, name, description, authors, manual section).
|
||||||
|
man_pages = [
|
||||||
|
(master_doc, 'treelib', u'treelib Documentation',
|
||||||
|
[author], 1)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# -- Options for Texinfo output ----------------------------------------------
|
||||||
|
|
||||||
|
# Grouping the document tree into Texinfo files. List of tuples
|
||||||
|
# (source start file, target name, title, author,
|
||||||
|
# dir menu entry, description, category)
|
||||||
|
texinfo_documents = [
|
||||||
|
(master_doc, 'treelib', u'treelib Documentation',
|
||||||
|
author, 'treelib', 'One line description of project.',
|
||||||
|
'Miscellaneous'),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# -- Options for Epub output -------------------------------------------------
|
||||||
|
|
||||||
|
# Bibliographic Dublin Core info.
|
||||||
|
epub_title = project
|
||||||
|
|
||||||
|
# The unique identifier of the text. This can be a ISBN number
|
||||||
|
# or the project homepage.
|
||||||
|
#
|
||||||
|
# epub_identifier = ''
|
||||||
|
|
||||||
|
# A unique identification for the text.
|
||||||
|
#
|
||||||
|
# epub_uid = ''
|
||||||
|
|
||||||
|
# A list of files that should not be packed into the epub file.
|
||||||
|
epub_exclude_files = ['search.html']
|
||||||
|
|
||||||
|
|
||||||
|
# -- Extension configuration -------------------------------------------------
|
||||||
|
|
||||||
|
# -- Options for intersphinx extension ---------------------------------------
|
||||||
|
|
||||||
|
# Example configuration for intersphinx: refer to the Python standard library.
|
||||||
|
intersphinx_mapping = {'https://docs.python.org/': None}
|
241
docs/source/index.rst
Normal file
241
docs/source/index.rst
Normal file
|
@ -0,0 +1,241 @@
|
||||||
|
.. treelib documentation master file, created by
|
||||||
|
sphinx-quickstart on Thu Dec 20 16:30:18 2018.
|
||||||
|
You can adapt this file completely to your liking, but it should at least
|
||||||
|
contain the root `toctree` directive.
|
||||||
|
|
||||||
|
Welcome to treelib's documentation!
|
||||||
|
***********************************
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 2
|
||||||
|
:caption: Contents:
|
||||||
|
|
||||||
|
Introduction
|
||||||
|
============
|
||||||
|
|
||||||
|
`Tree <http://en.wikipedia.org/wiki/Tree_%28data_structure%29>`_ is an
|
||||||
|
important data structure in computer science. Examples are shown in ML algorithm designs such as random forest tree and software engineering such as file system index. `treelib <https://github.com/caesar0301/pyTree>`_ is created to provide an efficient implementation of tree data structure in Python.
|
||||||
|
|
||||||
|
The main features of `treelib` includes:
|
||||||
|
* Efficient operation of node searching, O(1).
|
||||||
|
* Support common tree operations like **traversing**, **insertion**, **deletion**, **node moving**, **shallow/deep copying**, **subtree cutting** etc.
|
||||||
|
* Support user-defined data payload to accelerate your model construction.
|
||||||
|
* Pretty tree showing and text/json dump for pretty show and offline analysis.
|
||||||
|
* Compatible with Python 2 and 3.
|
||||||
|
|
||||||
|
Installation
|
||||||
|
============
|
||||||
|
|
||||||
|
The rapidest way to install treelib is using the package management tools like
|
||||||
|
``easy_install`` or ``pip`` with command
|
||||||
|
|
||||||
|
.. code-block:: sh
|
||||||
|
|
||||||
|
$ sudo easy_install -U treelib
|
||||||
|
|
||||||
|
or the setup script
|
||||||
|
|
||||||
|
.. code-block:: sh
|
||||||
|
|
||||||
|
$ sudo python setup.py install
|
||||||
|
|
||||||
|
**Note**: With the package management tools, the hosted version may be falling
|
||||||
|
behind current development branch on `Github
|
||||||
|
<https://github.com/caesar0301/pyTree>`_. If you encounter some problems, try
|
||||||
|
the freshest version on Github or open `issues
|
||||||
|
<https://github.com/caesar0301/pyTree/issues>`_ to let me know your problem.
|
||||||
|
|
||||||
|
Examples
|
||||||
|
========
|
||||||
|
|
||||||
|
Basic Usage
|
||||||
|
-------------
|
||||||
|
|
||||||
|
.. code-block:: sh
|
||||||
|
|
||||||
|
>>> from treelib import Node, Tree
|
||||||
|
>>> tree = Tree()
|
||||||
|
>>> tree.create_node("Harry", "harry") # root node
|
||||||
|
>>> tree.create_node("Jane", "jane", parent="harry")
|
||||||
|
>>> tree.create_node("Bill", "bill", parent="harry")
|
||||||
|
>>> tree.create_node("Diane", "diane", parent="jane")
|
||||||
|
>>> tree.create_node("Mary", "mary", parent="diane")
|
||||||
|
>>> tree.create_node("Mark", "mark", parent="jane")
|
||||||
|
>>> tree.show()
|
||||||
|
Harry
|
||||||
|
├── Bill
|
||||||
|
└── Jane
|
||||||
|
├── Diane
|
||||||
|
│ └── Mary
|
||||||
|
└── Mark
|
||||||
|
|
||||||
|
|
||||||
|
API Examples
|
||||||
|
--------------
|
||||||
|
|
||||||
|
**Example 1**: Expand a tree with specific mode (Tree.DEPTH [default],
|
||||||
|
Tree.WIDTH, Tree.ZIGZAG).
|
||||||
|
|
||||||
|
.. code-block:: sh
|
||||||
|
|
||||||
|
>>> print(','.join([tree[node].tag for node in \
|
||||||
|
tree.expand_tree(mode=Tree.DEPTH)]))
|
||||||
|
Harry,Bill,Jane,Diane,Mary,Mark
|
||||||
|
|
||||||
|
**Example 2**: Expand tree with custom filter.
|
||||||
|
|
||||||
|
.. code-block:: sh
|
||||||
|
|
||||||
|
>>> print(','.join([tree[node].tag for node in \
|
||||||
|
tree.expand_tree(filter = lambda x: \
|
||||||
|
x.identifier != 'diane')]))
|
||||||
|
Harry,Bill,Jane,Mark
|
||||||
|
|
||||||
|
**Example 3**: Get a subtree with the root of 'diane'.
|
||||||
|
|
||||||
|
.. code-block:: sh
|
||||||
|
|
||||||
|
>>> sub_t = tree.subtree('diane')
|
||||||
|
>>> sub_t.show()
|
||||||
|
Diane
|
||||||
|
└── Mary
|
||||||
|
|
||||||
|
**Example 4**: Paste a new tree to the original one.
|
||||||
|
|
||||||
|
.. code-block:: sh
|
||||||
|
|
||||||
|
>>> new_tree = Tree()
|
||||||
|
>>> new_tree.create_node("n1", 1) # root node
|
||||||
|
>>> new_tree.create_node("n2", 2, parent=1)
|
||||||
|
>>> new_tree.create_node("n3", 3, parent=1)
|
||||||
|
>>> tree.paste('bill', new_tree)
|
||||||
|
>>> tree.show()
|
||||||
|
Harry
|
||||||
|
├── Bill
|
||||||
|
│ └── n1
|
||||||
|
│ ├── n2
|
||||||
|
│ └── n3
|
||||||
|
└── Jane
|
||||||
|
├── Diane
|
||||||
|
│ └── Mary
|
||||||
|
└── Mark
|
||||||
|
|
||||||
|
**Example 5**: Remove the existing node from the tree
|
||||||
|
|
||||||
|
.. code-block:: sh
|
||||||
|
|
||||||
|
>>> tree.remove_node(1)
|
||||||
|
>>> tree.show()
|
||||||
|
Harry
|
||||||
|
├── Bill
|
||||||
|
└── Jane
|
||||||
|
├── Diane
|
||||||
|
│ └── Mary
|
||||||
|
└── Mark
|
||||||
|
|
||||||
|
**Example 6**: Move a node to another parent.
|
||||||
|
|
||||||
|
.. code-block:: sh
|
||||||
|
|
||||||
|
>>> tree.move_node('mary', 'harry')
|
||||||
|
>>> tree.show()
|
||||||
|
Harry
|
||||||
|
├── Bill
|
||||||
|
├── Jane
|
||||||
|
│ ├── Diane
|
||||||
|
│ └── Mark
|
||||||
|
└── Mary
|
||||||
|
|
||||||
|
**Example 7**: Get the height of the tree.
|
||||||
|
|
||||||
|
.. code-block:: sh
|
||||||
|
|
||||||
|
>>> tree.depth()
|
||||||
|
2
|
||||||
|
|
||||||
|
**Example 8**: Get the level of a node.
|
||||||
|
|
||||||
|
.. code-block:: sh
|
||||||
|
|
||||||
|
>>> node = tree.get_node("bill")
|
||||||
|
>>> tree.depth(node)
|
||||||
|
1
|
||||||
|
|
||||||
|
**Example 9**: Print or dump tree structure. For example, the same tree in
|
||||||
|
basic example can be printed with 'ascii-em':
|
||||||
|
|
||||||
|
.. code-block:: sh
|
||||||
|
|
||||||
|
>>> tree.show(line_type="ascii-em")
|
||||||
|
Harry
|
||||||
|
╠══ Bill
|
||||||
|
╠══ Jane
|
||||||
|
║ ╠══ Diane
|
||||||
|
║ ╚══ Mark
|
||||||
|
╚══ Mary
|
||||||
|
|
||||||
|
In the JSON form, to_json() takes optional parameter with_data to trigger if
|
||||||
|
the data field is appended into JSON string. For example,
|
||||||
|
|
||||||
|
.. code-block:: sh
|
||||||
|
|
||||||
|
>>> print(tree.to_json(with_data=True))
|
||||||
|
{"Harry": {"data": null, "children": [{"Bill": {"data": null}}, {"Jane": {"data": null, "children": [{"Diane": {"data": null}}, {"Mark": {"data": null}}]}}, {"Mary": {"data": null}}]}}
|
||||||
|
|
||||||
|
**Example 10**: Save tree into file
|
||||||
|
The function save2file require a filename.
|
||||||
|
The file is opened to write using mode 'ab'.
|
||||||
|
|
||||||
|
.. code-block:: sh
|
||||||
|
|
||||||
|
>>> tree.save2file('tree.txt')
|
||||||
|
|
||||||
|
|
||||||
|
Advanced Usage
|
||||||
|
----------------
|
||||||
|
|
||||||
|
Sometimes, you need trees to store your own data. The newest version of
|
||||||
|
:mod:`treelib` supports ``.data`` variable to store whatever you want. For
|
||||||
|
example, to define a flower tree with your own data:
|
||||||
|
|
||||||
|
.. code-block:: sh
|
||||||
|
|
||||||
|
>>> class Flower(object): \
|
||||||
|
def __init__(self, color): \
|
||||||
|
self.color = color
|
||||||
|
|
||||||
|
You can create a flower tree now:
|
||||||
|
|
||||||
|
.. code-block:: sh
|
||||||
|
|
||||||
|
>>> ftree = Tree()
|
||||||
|
>>> ftree.create_node("Root", "root", data=Flower("black"))
|
||||||
|
>>> ftree.create_node("F1", "f1", parent='root', data=Flower("white"))
|
||||||
|
>>> ftree.create_node("F2", "f2", parent='root', data=Flower("red"))
|
||||||
|
|
||||||
|
Printing the colors of the tree:
|
||||||
|
|
||||||
|
.. code-block:: sh
|
||||||
|
|
||||||
|
>>> ftree.show(data_property="color")
|
||||||
|
black
|
||||||
|
├── white
|
||||||
|
└── red
|
||||||
|
|
||||||
|
**Notes:** Before version 1.2.5, you may need to inherit and modify the behaviors of tree. Both are supported since then. For flower example,
|
||||||
|
|
||||||
|
.. code-block:: sh
|
||||||
|
|
||||||
|
>>> class FlowerNode(treelib.Node): \
|
||||||
|
def __init__(self, color): \
|
||||||
|
self.color = color
|
||||||
|
>>> # create a new node
|
||||||
|
>>> fnode = FlowerNode("white")
|
||||||
|
|
||||||
|
|
||||||
|
Indices and tables
|
||||||
|
==================
|
||||||
|
|
||||||
|
* :ref:`genindex`
|
||||||
|
* :ref:`modindex`
|
||||||
|
* :ref:`search`
|
7
docs/source/modules.rst
Normal file
7
docs/source/modules.rst
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
treelib
|
||||||
|
=======
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 4
|
||||||
|
|
||||||
|
treelib
|
45
docs/source/treelib.rst
Normal file
45
docs/source/treelib.rst
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
treelib package
|
||||||
|
===============
|
||||||
|
|
||||||
|
Module contents
|
||||||
|
---------------
|
||||||
|
|
||||||
|
.. automodule:: treelib
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
Submodules
|
||||||
|
----------
|
||||||
|
|
||||||
|
treelib.node module
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
.. automodule:: treelib.node
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
treelib.tree module
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
.. automodule:: treelib.tree
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
treelib.plugins module
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
.. automodule:: treelib.plugins
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
treelib.exceptions module
|
||||||
|
-------------------------
|
||||||
|
|
||||||
|
.. automodule:: treelib.exceptions
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
65
examples/family_tree.py
Normal file
65
examples/family_tree.py
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# Example usage of treelib
|
||||||
|
#
|
||||||
|
# Author: chenxm
|
||||||
|
#
|
||||||
|
__author__ = "chenxm"
|
||||||
|
|
||||||
|
from treelib import Tree
|
||||||
|
|
||||||
|
|
||||||
|
def create_family_tree():
|
||||||
|
# Create the family tree
|
||||||
|
tree = Tree()
|
||||||
|
tree.create_node("Harry", "harry") # root node
|
||||||
|
tree.create_node("Jane", "jane", parent="harry")
|
||||||
|
tree.create_node("Bill", "bill", parent="harry")
|
||||||
|
tree.create_node("Diane", "diane", parent="jane")
|
||||||
|
tree.create_node("Mary", "mary", parent="diane")
|
||||||
|
tree.create_node("Mark", "mark", parent="jane")
|
||||||
|
return tree
|
||||||
|
|
||||||
|
|
||||||
|
def example(desp):
|
||||||
|
sep = "-" * 20 + "\n"
|
||||||
|
print(sep + desp)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
tree = create_family_tree()
|
||||||
|
|
||||||
|
example("Tree of the whole family:")
|
||||||
|
tree.show(key=lambda x: x.tag, reverse=True, line_type="ascii-em")
|
||||||
|
|
||||||
|
example("All family members in DEPTH mode:")
|
||||||
|
print(",".join([tree[node].tag for node in tree.expand_tree()]))
|
||||||
|
|
||||||
|
example("All family members (with identifiers) but Diane's sub-family:")
|
||||||
|
tree.show(idhidden=False, filter=lambda x: x.identifier != "diane")
|
||||||
|
|
||||||
|
example("Let me introduce Diane family only:")
|
||||||
|
sub_t = tree.subtree("diane")
|
||||||
|
sub_t.show()
|
||||||
|
|
||||||
|
example("Children of Diane:")
|
||||||
|
for child in tree.is_branch("diane"):
|
||||||
|
print(tree[child].tag)
|
||||||
|
|
||||||
|
example("New members join Jill's family:")
|
||||||
|
new_tree = Tree()
|
||||||
|
new_tree.create_node("n1", 1) # root node
|
||||||
|
new_tree.create_node("n2", 2, parent=1)
|
||||||
|
new_tree.create_node("n3", 3, parent=1)
|
||||||
|
tree.paste("bill", new_tree)
|
||||||
|
tree.show()
|
||||||
|
|
||||||
|
example("They leave after a while:")
|
||||||
|
tree.remove_node(1)
|
||||||
|
tree.show()
|
||||||
|
|
||||||
|
example("Now Mary moves to live with grandfather Harry:")
|
||||||
|
tree.move_node("mary", "harry")
|
||||||
|
tree.show()
|
||||||
|
|
||||||
|
example("A big family for Mark to send message to the oldest Harry:")
|
||||||
|
print(",".join([tree[node].tag for node in tree.rsearch("mark")]))
|
207
examples/folder_tree.py
Normal file
207
examples/folder_tree.py
Normal file
|
@ -0,0 +1,207 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# A file folder scanner contributed by @holger
|
||||||
|
#
|
||||||
|
# You can spicify the scanned folder and file pattern by changing rootPath
|
||||||
|
# and pattern variables
|
||||||
|
#
|
||||||
|
|
||||||
|
__author__ = "holger"
|
||||||
|
|
||||||
|
from treelib import tree
|
||||||
|
|
||||||
|
import fnmatch
|
||||||
|
import os
|
||||||
|
import zlib
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
DEBUG = 0
|
||||||
|
FILECOUNT = 0
|
||||||
|
DIRCOUNT = 0
|
||||||
|
DIR_ERRORLIST = []
|
||||||
|
FILE_ERRORLIST = []
|
||||||
|
|
||||||
|
|
||||||
|
# Time Profiling
|
||||||
|
PROFILING = 0
|
||||||
|
# 0 - nothing
|
||||||
|
# 1 - time
|
||||||
|
# 2 - cProfile
|
||||||
|
|
||||||
|
if PROFILING == 1:
|
||||||
|
import timeit
|
||||||
|
if PROFILING == 2:
|
||||||
|
import cProfile
|
||||||
|
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Scan the given folder and print its structure in a tree."
|
||||||
|
)
|
||||||
|
parser.add_argument("abspath", type=str, help="An absolute path to be scanned.")
|
||||||
|
parser.add_argument(
|
||||||
|
"pattern", type=str, help="File name pattern to filtered, e.g. *.pdf"
|
||||||
|
)
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
rootPath = args.abspath
|
||||||
|
pattern = args.pattern
|
||||||
|
|
||||||
|
folder_blacklist = []
|
||||||
|
|
||||||
|
dir_tree = tree.Tree()
|
||||||
|
dir_tree.create_node("Root", rootPath) # root node
|
||||||
|
|
||||||
|
|
||||||
|
def crc32(data):
|
||||||
|
data = bytes(data, "UTF-8")
|
||||||
|
|
||||||
|
if DEBUG:
|
||||||
|
print("++++++ CRC32 ++++++")
|
||||||
|
print("input: " + str(data))
|
||||||
|
print("crc32: " + hex(zlib.crc32(data) & 0xFFFFFFFF))
|
||||||
|
print("+++++++++++++++++++")
|
||||||
|
return hex(
|
||||||
|
zlib.crc32(data) & 0xFFFFFFFF
|
||||||
|
) # crc32 returns a signed value, &-ing it will match py3k
|
||||||
|
|
||||||
|
|
||||||
|
parent = rootPath
|
||||||
|
i = 1
|
||||||
|
|
||||||
|
# calculating start depth
|
||||||
|
start_depth = rootPath.count("/")
|
||||||
|
|
||||||
|
|
||||||
|
def get_noteid(depth, root, dir):
|
||||||
|
"""get_noteid returns
|
||||||
|
- depth contains the current depth of the folder hierarchy
|
||||||
|
- dir contains the current directory
|
||||||
|
|
||||||
|
Function returns a string containing the current depth, the folder name and unique ID build by hashing the
|
||||||
|
absolute path of the directory. All spaces are replaced by '_'
|
||||||
|
|
||||||
|
<depth>_<dirname>+++<crc32>
|
||||||
|
e.g. 2_Folder_XYZ_1+++<crc32>
|
||||||
|
"""
|
||||||
|
return (
|
||||||
|
str(str(depth) + "_" + dir).replace(" ", "_")
|
||||||
|
+ "+++"
|
||||||
|
+ crc32(os.path.join(root, dir))
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: Verzeichnistiefe pruefen: Was ist mit sowas /mp3/
|
||||||
|
|
||||||
|
|
||||||
|
def get_parentid(current_depth, root, dir):
|
||||||
|
# special case for the 'root' of the tree
|
||||||
|
# because we don't want a cryptic root-name
|
||||||
|
if current_depth == 0:
|
||||||
|
return root
|
||||||
|
|
||||||
|
# looking for parent directory
|
||||||
|
# e.g. /home/user1/mp3/folder1/parent_folder/current_folder
|
||||||
|
# get 'parent_folder'
|
||||||
|
|
||||||
|
search_string = os.path.join(root, dir)
|
||||||
|
pos2 = search_string.rfind("/")
|
||||||
|
pos1 = search_string.rfind("/", 0, pos2)
|
||||||
|
parent_dir = search_string[pos1 + 1 : pos2] # noqa: E203
|
||||||
|
parentid = (
|
||||||
|
str(current_depth - 1)
|
||||||
|
+ "_"
|
||||||
|
+ parent_dir.replace(" ", "_")
|
||||||
|
+ "+++"
|
||||||
|
+ crc32(root)
|
||||||
|
)
|
||||||
|
return parentid
|
||||||
|
# TODO: catch error
|
||||||
|
|
||||||
|
|
||||||
|
def print_node(dir, node_id, parent_id):
|
||||||
|
print("#############################")
|
||||||
|
print("node created")
|
||||||
|
print(" dir: " + dir)
|
||||||
|
print(" note_id: " + node_id)
|
||||||
|
print(" parent: " + parent_id)
|
||||||
|
|
||||||
|
|
||||||
|
def crawler():
|
||||||
|
global DIRCOUNT
|
||||||
|
global FILECOUNT
|
||||||
|
|
||||||
|
for root, dirs, files in os.walk(rootPath):
|
||||||
|
# +++ DIRECTORIES +++
|
||||||
|
for dir in dirs:
|
||||||
|
# calculating current depth
|
||||||
|
current_depth = os.path.join(root, dir).count("/") - start_depth
|
||||||
|
|
||||||
|
if DEBUG:
|
||||||
|
print("current: " + os.path.join(root, dir))
|
||||||
|
|
||||||
|
node_id = get_noteid(current_depth, root, dir)
|
||||||
|
parent_id = str(get_parentid(current_depth, root, dir))
|
||||||
|
|
||||||
|
if parent_id == str(None):
|
||||||
|
DIR_ERRORLIST.append(os.path.join(root, dir))
|
||||||
|
|
||||||
|
if DEBUG:
|
||||||
|
print_node(dir, node_id, parent_id)
|
||||||
|
|
||||||
|
# create node
|
||||||
|
dir_tree.create_node(dir, node_id, parent_id)
|
||||||
|
DIRCOUNT += 1
|
||||||
|
|
||||||
|
# +++ FILES +++
|
||||||
|
for filename in fnmatch.filter(files, pattern):
|
||||||
|
if dir in folder_blacklist:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# calculating current depth
|
||||||
|
current_depth = os.path.join(root, filename).count("/") - start_depth
|
||||||
|
|
||||||
|
if DEBUG:
|
||||||
|
print("current: " + os.path.join(root, filename))
|
||||||
|
|
||||||
|
node_id = get_noteid(current_depth, root, filename)
|
||||||
|
parent_id = str(get_parentid(current_depth, root, filename))
|
||||||
|
|
||||||
|
if parent_id == str(None):
|
||||||
|
FILE_ERRORLIST.append(os.path.join(root, dir))
|
||||||
|
|
||||||
|
if DEBUG:
|
||||||
|
print_node(filename, node_id, parent_id)
|
||||||
|
|
||||||
|
# create node
|
||||||
|
dir_tree.create_node(filename, node_id, parent_id)
|
||||||
|
FILECOUNT += 1
|
||||||
|
|
||||||
|
|
||||||
|
if PROFILING == 0:
|
||||||
|
crawler()
|
||||||
|
if PROFILING == 1:
|
||||||
|
t1 = timeit.Timer("crawler()", "from __main__ import crawler")
|
||||||
|
print("time: " + str(t1.timeit(number=1)))
|
||||||
|
if PROFILING == 2:
|
||||||
|
cProfile.run("crawler()")
|
||||||
|
|
||||||
|
|
||||||
|
print("filecount: " + str(FILECOUNT))
|
||||||
|
print("dircount: " + str(DIRCOUNT))
|
||||||
|
|
||||||
|
if DIR_ERRORLIST:
|
||||||
|
for item in DIR_ERRORLIST:
|
||||||
|
print(item)
|
||||||
|
else:
|
||||||
|
print("no directory errors")
|
||||||
|
|
||||||
|
print("\n\n\n")
|
||||||
|
|
||||||
|
if FILE_ERRORLIST:
|
||||||
|
for item in FILE_ERRORLIST:
|
||||||
|
print(item)
|
||||||
|
else:
|
||||||
|
print("no file errors")
|
||||||
|
|
||||||
|
print("nodes: " + str(len(dir_tree.nodes)))
|
||||||
|
|
||||||
|
dir_tree.show()
|
89
examples/recursive_dirtree_generator.py
Normal file
89
examples/recursive_dirtree_generator.py
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
"""
|
||||||
|
Example of treelib usage to generate recursive tree of directories.
|
||||||
|
It could be useful to implement Directory Tree data structure
|
||||||
|
|
||||||
|
2016 samuelsh
|
||||||
|
"""
|
||||||
|
|
||||||
|
import treelib
|
||||||
|
import random
|
||||||
|
import hashlib
|
||||||
|
from string import digits, letters
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
MAX_FILES_PER_DIR = 10
|
||||||
|
|
||||||
|
|
||||||
|
def range2(stop):
|
||||||
|
if sys.version_info[0] < 3:
|
||||||
|
return xrange(stop) # noqa: F821
|
||||||
|
else:
|
||||||
|
return range(stop)
|
||||||
|
|
||||||
|
|
||||||
|
def get_random_string(length):
|
||||||
|
return "".join(random.choice(digits + letters) for _ in range2(length))
|
||||||
|
|
||||||
|
|
||||||
|
def build_recursive_tree(tree, base, depth, width):
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
tree: Tree
|
||||||
|
base: Node
|
||||||
|
depth: int
|
||||||
|
width: int
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
"""
|
||||||
|
if depth >= 0:
|
||||||
|
depth -= 1
|
||||||
|
for i in range2(width):
|
||||||
|
directory = Directory()
|
||||||
|
tree.create_node(
|
||||||
|
"{0}".format(directory.name),
|
||||||
|
"{0}".format(hashlib.md5(directory.name)),
|
||||||
|
parent=base.identifier,
|
||||||
|
data=directory,
|
||||||
|
) # node identifier is md5 hash of it's name
|
||||||
|
dirs_nodes = tree.children(base.identifier)
|
||||||
|
for dir in dirs_nodes:
|
||||||
|
newbase = tree.get_node(dir.identifier)
|
||||||
|
build_recursive_tree(tree, newbase, depth, width)
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
class Directory(object):
|
||||||
|
def __init__(self):
|
||||||
|
self._name = get_random_string(64)
|
||||||
|
self._files = [
|
||||||
|
File() for _ in range2(MAX_FILES_PER_DIR)
|
||||||
|
] # Each directory contains 1000 files
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
return self._name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def files(self):
|
||||||
|
return self._files
|
||||||
|
|
||||||
|
|
||||||
|
class File(object):
|
||||||
|
def __init__(self):
|
||||||
|
self._name = get_random_string(64)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
return self._name
|
||||||
|
|
||||||
|
|
||||||
|
tree = treelib.Tree()
|
||||||
|
base = tree.create_node("Root", "root")
|
||||||
|
build_recursive_tree(tree, base, 2, 10)
|
||||||
|
|
||||||
|
tree.show()
|
12
examples/save_tree_2_file.py
Normal file
12
examples/save_tree_2_file.py
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from treelib import Tree
|
||||||
|
|
||||||
|
tree = Tree()
|
||||||
|
tree.create_node("Harry", "harry") # root node
|
||||||
|
tree.create_node("Jane", "jane", parent="harry")
|
||||||
|
tree.create_node("Bill", "bill", parent="harry")
|
||||||
|
tree.create_node("Diane", "diane", parent="jane")
|
||||||
|
tree.create_node("Mary", "mary", parent="diane")
|
||||||
|
tree.create_node("Mark", "mark", parent="jane")
|
||||||
|
tree.save2file("tree.txt")
|
33
recipe/conda_build.sh
Normal file
33
recipe/conda_build.sh
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
export VERSION="1.5.3"
|
||||||
|
export BUILD_NAME="0"
|
||||||
|
export CONDA_BLD_PATH=~/conda-bld
|
||||||
|
USER="e3sm"
|
||||||
|
PLATFORM="noarch"
|
||||||
|
PKG="treelib"
|
||||||
|
|
||||||
|
if [ -d $CONDA_BLD_PATH ]; then
|
||||||
|
rm -rf $CONDA_BLD_PATH
|
||||||
|
fi
|
||||||
|
echo "Creating build dir at" $CONDA_BLD_PATH
|
||||||
|
mkdir $CONDA_BLD_PATH
|
||||||
|
|
||||||
|
conda config --set anaconda_upload no
|
||||||
|
if [ ! -z "$1" ]; then
|
||||||
|
export TAG="$1"
|
||||||
|
else
|
||||||
|
export TAG="master"
|
||||||
|
fi
|
||||||
|
echo "Building" $VERSION"-"$BUILD_NAME "for label:" $TAG
|
||||||
|
|
||||||
|
conda build .
|
||||||
|
|
||||||
|
if [ $? -eq 1 ]; then
|
||||||
|
echo "conda build failed"
|
||||||
|
exit
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -z "$1" ]; then
|
||||||
|
anaconda upload -u $USER -l "$1" $CONDA_BLD_PATH/$PLATFORM/$PKG-$VERSION-$BUILD_NAME.tar.bz2
|
||||||
|
else
|
||||||
|
anaconda upload -u $USER $CONDA_BLD_PATH/$PLATFORM/$PKG-$VERSION-$BUILD_NAME.tar.bz2
|
||||||
|
fi
|
25
recipe/meta.yaml
Normal file
25
recipe/meta.yaml
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
package:
|
||||||
|
name: treelib
|
||||||
|
version: {{ environ['VERSION'] }}
|
||||||
|
|
||||||
|
source:
|
||||||
|
git_url: git://github.com/caesar0301/treelib.git
|
||||||
|
git_tag: {{ environ['TAG'] }}
|
||||||
|
|
||||||
|
build:
|
||||||
|
script: python setup.py install
|
||||||
|
string: {{ environ['BUILD_NAME'] }}
|
||||||
|
noarch: python
|
||||||
|
|
||||||
|
about:
|
||||||
|
home: https://treelib.readthedocs.io/en/latest/
|
||||||
|
summary: An efficient implementation of tree data structure in python 2/3.
|
||||||
|
|
||||||
|
requirements:
|
||||||
|
build:
|
||||||
|
- python
|
||||||
|
- setuptools
|
||||||
|
- pip
|
||||||
|
|
||||||
|
run:
|
||||||
|
- python
|
3
requirements-t-pre37.txt
Normal file
3
requirements-t-pre37.txt
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
nose>=1.3.7
|
||||||
|
nose-exclude>=0.5.0
|
||||||
|
coverage>=4.4.1
|
5
requirements-t.txt
Normal file
5
requirements-t.txt
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
pytest>=7.0.0
|
||||||
|
coverage>=4.4.1
|
||||||
|
flake8==5.0.0
|
||||||
|
flake8-black==0.3.6
|
||||||
|
black==23.1.0
|
1
requirements.txt
Normal file
1
requirements.txt
Normal file
|
@ -0,0 +1 @@
|
||||||
|
six>=1.13.0
|
3
scripts/flake8.sh
Executable file
3
scripts/flake8.sh
Executable file
|
@ -0,0 +1,3 @@
|
||||||
|
#!/bin/bash
|
||||||
|
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
|
||||||
|
flake8 . --count --max-line-length=127 --exclude docs/source/conf.py --statistics
|
6
scripts/testing.sh
Executable file
6
scripts/testing.sh
Executable file
|
@ -0,0 +1,6 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
nosetests --with-coverage --cover-package=treelib \
|
||||||
|
--cover-erase --cover-tests \
|
||||||
|
-d --all-modules \
|
||||||
|
tests
|
2
setup.cfg
Normal file
2
setup.cfg
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
[metadata]
|
||||||
|
description-file = README.md
|
39
setup.py
Normal file
39
setup.py
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
from setuptools import setup
|
||||||
|
|
||||||
|
__version__ = "1.6.4"
|
||||||
|
|
||||||
|
|
||||||
|
setup(
|
||||||
|
name="treelib",
|
||||||
|
version=__version__,
|
||||||
|
url="https://github.com/caesar0301/treelib",
|
||||||
|
author="Xiaming Chen",
|
||||||
|
author_email="chenxm35@gmail.com",
|
||||||
|
description="A Python 2/3 implementation of tree structure.",
|
||||||
|
long_description="""This is a simple tree data structure implementation in python.""",
|
||||||
|
license="Apache License, Version 2.0",
|
||||||
|
packages=["treelib"],
|
||||||
|
keywords=["data structure", "tree", "tools"],
|
||||||
|
install_requires=["six"],
|
||||||
|
classifiers=[
|
||||||
|
"Development Status :: 4 - Beta",
|
||||||
|
"Environment :: Console",
|
||||||
|
"Intended Audience :: Developers",
|
||||||
|
"License :: OSI Approved :: Apache Software License",
|
||||||
|
"Operating System :: OS Independent",
|
||||||
|
"Programming Language :: Python",
|
||||||
|
"Programming Language :: Python :: 2",
|
||||||
|
"Programming Language :: Python :: 2.7",
|
||||||
|
"Programming Language :: Python :: 3",
|
||||||
|
"Programming Language :: Python :: 3.4",
|
||||||
|
"Programming Language :: Python :: 3.5",
|
||||||
|
"Programming Language :: Python :: 3.6",
|
||||||
|
"Programming Language :: Python :: 3.7",
|
||||||
|
"Programming Language :: Python :: 3.8",
|
||||||
|
"Programming Language :: Python :: 3.9",
|
||||||
|
"Programming Language :: Python :: 3.10",
|
||||||
|
"Programming Language :: Python :: 3.11",
|
||||||
|
"Programming Language :: Python :: 3.12",
|
||||||
|
"Topic :: Software Development :: Libraries :: Python Modules",
|
||||||
|
],
|
||||||
|
)
|
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
88
tests/test_node.py
Normal file
88
tests/test_node.py
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from collections import defaultdict
|
||||||
|
from treelib import Node
|
||||||
|
|
||||||
|
|
||||||
|
class NodeCase(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.node1 = Node("Test One", "identifier 1")
|
||||||
|
self.node2 = Node("Test Two", "identifier 2")
|
||||||
|
|
||||||
|
def test_initialization(self):
|
||||||
|
self.assertEqual(self.node1.tag, "Test One")
|
||||||
|
self.assertEqual(self.node1.identifier, "identifier 1")
|
||||||
|
# retro-compatibility
|
||||||
|
self.assertEqual(self.node1.bpointer, None)
|
||||||
|
self.assertEqual(self.node1.fpointer, [])
|
||||||
|
|
||||||
|
self.assertEqual(self.node1.expanded, True)
|
||||||
|
self.assertEqual(self.node1._predecessor, {})
|
||||||
|
self.assertEqual(self.node1._successors, defaultdict(list))
|
||||||
|
self.assertEqual(self.node1.data, None)
|
||||||
|
|
||||||
|
def test_set_tag(self):
|
||||||
|
self.node1.tag = "Test 1"
|
||||||
|
self.assertEqual(self.node1.tag, "Test 1")
|
||||||
|
self.node1.tag = "Test One"
|
||||||
|
|
||||||
|
def test_object_as_node_tag(self):
|
||||||
|
node = Node(tag=(0, 1))
|
||||||
|
self.assertEqual(node.tag, (0, 1))
|
||||||
|
self.assertTrue(node.__repr__().startswith("Node"))
|
||||||
|
|
||||||
|
def test_set_identifier(self):
|
||||||
|
self.node1.identifier = "ID1"
|
||||||
|
self.assertEqual(self.node1.identifier, "ID1")
|
||||||
|
self.node1.identifier = "identifier 1"
|
||||||
|
|
||||||
|
def test_set_fpointer(self):
|
||||||
|
# retro-compatibility
|
||||||
|
self.node1.update_fpointer("identifier 2")
|
||||||
|
self.assertEqual(self.node1.fpointer, ["identifier 2"])
|
||||||
|
self.node1.fpointer = []
|
||||||
|
self.assertEqual(self.node1.fpointer, [])
|
||||||
|
|
||||||
|
def test_update_successors(self):
|
||||||
|
self.node1.update_successors("identifier 2", tree_id="tree 1")
|
||||||
|
self.assertEqual(self.node1.successors("tree 1"), ["identifier 2"])
|
||||||
|
self.assertEqual(self.node1._successors["tree 1"], ["identifier 2"])
|
||||||
|
self.node1.set_successors([], tree_id="tree 1")
|
||||||
|
self.assertEqual(self.node1._successors["tree 1"], [])
|
||||||
|
|
||||||
|
def test_set_bpointer(self):
|
||||||
|
# retro-compatibility
|
||||||
|
self.node2.update_bpointer("identifier 1")
|
||||||
|
self.assertEqual(self.node2.bpointer, "identifier 1")
|
||||||
|
self.node2.bpointer = None
|
||||||
|
self.assertEqual(self.node2.bpointer, None)
|
||||||
|
|
||||||
|
def test_set_predecessor(self):
|
||||||
|
self.node2.set_predecessor("identifier 1", "tree 1")
|
||||||
|
self.assertEqual(self.node2.predecessor("tree 1"), "identifier 1")
|
||||||
|
self.assertEqual(self.node2._predecessor["tree 1"], "identifier 1")
|
||||||
|
self.node2.set_predecessor(None, "tree 1")
|
||||||
|
self.assertEqual(self.node2.predecessor("tree 1"), None)
|
||||||
|
|
||||||
|
def test_set_is_leaf(self):
|
||||||
|
self.node1.update_fpointer("identifier 2")
|
||||||
|
self.node2.update_bpointer("identifier 1")
|
||||||
|
self.assertEqual(self.node1.is_leaf(), False)
|
||||||
|
self.assertEqual(self.node2.is_leaf(), True)
|
||||||
|
|
||||||
|
def test_tree_wise_is_leaf(self):
|
||||||
|
self.node1.update_successors("identifier 2", tree_id="tree 1")
|
||||||
|
self.node2.set_predecessor("identifier 1", "tree 1")
|
||||||
|
self.assertEqual(self.node1.is_leaf("tree 1"), False)
|
||||||
|
self.assertEqual(self.node2.is_leaf("tree 1"), True)
|
||||||
|
|
||||||
|
def test_data(self):
|
||||||
|
class Flower(object):
|
||||||
|
def __init__(self, color):
|
||||||
|
self.color = color
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "%s" % self.color
|
||||||
|
|
||||||
|
self.node1.data = Flower("red")
|
||||||
|
self.assertEqual(self.node1.data.color, "red")
|
113
tests/test_plugins.py
Normal file
113
tests/test_plugins.py
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import codecs
|
||||||
|
import os
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from treelib import Tree
|
||||||
|
from treelib.plugins import export_to_dot
|
||||||
|
|
||||||
|
|
||||||
|
class DotExportCase(unittest.TestCase):
|
||||||
|
"""Test class for the export to dot format function"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
tree = Tree()
|
||||||
|
tree.create_node("Hárry", "hárry")
|
||||||
|
tree.create_node("Jane", "jane", parent="hárry")
|
||||||
|
tree.create_node("Bill", "bill", parent="hárry")
|
||||||
|
tree.create_node("Diane", "diane", parent="jane")
|
||||||
|
tree.create_node("George", "george", parent="bill")
|
||||||
|
self.tree = tree
|
||||||
|
|
||||||
|
def read_generated_output(self, filename):
|
||||||
|
output = codecs.open(filename, "r", "utf-8")
|
||||||
|
generated = output.read()
|
||||||
|
output.close()
|
||||||
|
|
||||||
|
return generated
|
||||||
|
|
||||||
|
def test_export_to_dot(self):
|
||||||
|
export_to_dot(self.tree, "tree.dot")
|
||||||
|
expected = """\
|
||||||
|
digraph tree {
|
||||||
|
\t"hárry" [label="Hárry", shape=circle]
|
||||||
|
\t"bill" [label="Bill", shape=circle]
|
||||||
|
\t"jane" [label="Jane", shape=circle]
|
||||||
|
\t"george" [label="George", shape=circle]
|
||||||
|
\t"diane" [label="Diane", shape=circle]
|
||||||
|
|
||||||
|
\t"hárry" -> "jane"
|
||||||
|
\t"hárry" -> "bill"
|
||||||
|
\t"bill" -> "george"
|
||||||
|
\t"jane" -> "diane"
|
||||||
|
}"""
|
||||||
|
|
||||||
|
self.assertTrue(
|
||||||
|
os.path.isfile("tree.dot"), "The file tree.dot could not be found."
|
||||||
|
)
|
||||||
|
generated = self.read_generated_output("tree.dot")
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
generated, expected, "Generated dot tree is not the expected one"
|
||||||
|
)
|
||||||
|
os.remove("tree.dot")
|
||||||
|
|
||||||
|
def test_export_to_dot_empty_tree(self):
|
||||||
|
empty_tree = Tree()
|
||||||
|
export_to_dot(empty_tree, "tree.dot")
|
||||||
|
|
||||||
|
expected = """\
|
||||||
|
digraph tree {
|
||||||
|
}"""
|
||||||
|
self.assertTrue(
|
||||||
|
os.path.isfile("tree.dot"), "The file tree.dot could not be found."
|
||||||
|
)
|
||||||
|
generated = self.read_generated_output("tree.dot")
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
expected, generated, "The generated output for an empty tree is not empty"
|
||||||
|
)
|
||||||
|
os.remove("tree.dot")
|
||||||
|
|
||||||
|
def test_unicode_filename(self):
|
||||||
|
tree = Tree()
|
||||||
|
tree.create_node("Node 1", "node_1")
|
||||||
|
export_to_dot(tree, "ŕʩϢ.dot")
|
||||||
|
|
||||||
|
expected = """\
|
||||||
|
digraph tree {
|
||||||
|
\t"node_1" [label="Node 1", shape=circle]
|
||||||
|
}"""
|
||||||
|
self.assertTrue(
|
||||||
|
os.path.isfile("ŕʩϢ.dot"), "The file ŕʩϢ.dot could not be found."
|
||||||
|
)
|
||||||
|
generated = self.read_generated_output("ŕʩϢ.dot")
|
||||||
|
self.assertEqual(
|
||||||
|
expected, generated, "The generated file content is not the expected one"
|
||||||
|
)
|
||||||
|
os.remove("ŕʩϢ.dot")
|
||||||
|
|
||||||
|
def test_export_with_minus_in_filename(self):
|
||||||
|
tree = Tree()
|
||||||
|
tree.create_node("Example Node", "example-node")
|
||||||
|
expected = """\
|
||||||
|
digraph tree {
|
||||||
|
\t"example-node" [label="Example Node", shape=circle]
|
||||||
|
}"""
|
||||||
|
|
||||||
|
export_to_dot(tree, "id_with_minus.dot")
|
||||||
|
self.assertTrue(
|
||||||
|
os.path.isfile("id_with_minus.dot"),
|
||||||
|
"The file id_with_minus.dot could not be found.",
|
||||||
|
)
|
||||||
|
generated = self.read_generated_output("id_with_minus.dot")
|
||||||
|
self.assertEqual(
|
||||||
|
expected, generated, "The generated file content is not the expected one"
|
||||||
|
)
|
||||||
|
os.remove("id_with_minus.dot")
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
self.tree = None
|
736
tests/test_tree.py
Normal file
736
tests/test_tree.py
Normal file
|
@ -0,0 +1,736 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
from treelib import Tree, Node
|
||||||
|
from treelib.tree import NodeIDAbsentError, LoopError
|
||||||
|
|
||||||
|
|
||||||
|
def encode(value):
|
||||||
|
if sys.version_info[0] == 2:
|
||||||
|
# Python2.x :
|
||||||
|
return value.encode("utf-8")
|
||||||
|
else:
|
||||||
|
# Python3.x :
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
class TreeCase(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
tree = Tree(identifier="tree 1")
|
||||||
|
tree.create_node("Hárry", "hárry")
|
||||||
|
tree.create_node("Jane", "jane", parent="hárry")
|
||||||
|
tree.create_node("Bill", "bill", parent="hárry")
|
||||||
|
tree.create_node("Diane", "diane", parent="jane")
|
||||||
|
tree.create_node("George", "george", parent="bill")
|
||||||
|
# Hárry
|
||||||
|
# |-- Jane
|
||||||
|
# |-- Diane
|
||||||
|
# |-- Bill
|
||||||
|
# |-- George
|
||||||
|
self.tree = tree
|
||||||
|
self.copytree = Tree(self.tree, deep=True)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_t1():
|
||||||
|
"""
|
||||||
|
root
|
||||||
|
├── A
|
||||||
|
│ └── A1
|
||||||
|
└── B
|
||||||
|
"""
|
||||||
|
t = Tree(identifier="t1")
|
||||||
|
t.create_node(tag="root", identifier="r")
|
||||||
|
t.create_node(tag="A", identifier="a", parent="r")
|
||||||
|
t.create_node(tag="B", identifier="b", parent="r")
|
||||||
|
t.create_node(tag="A1", identifier="a1", parent="a")
|
||||||
|
return t
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_t2():
|
||||||
|
"""
|
||||||
|
root2
|
||||||
|
├── C
|
||||||
|
└── D
|
||||||
|
└── D1
|
||||||
|
"""
|
||||||
|
t = Tree(identifier="t2")
|
||||||
|
t.create_node(tag="root2", identifier="r2")
|
||||||
|
t.create_node(tag="C", identifier="c", parent="r2")
|
||||||
|
t.create_node(tag="D", identifier="d", parent="r2")
|
||||||
|
t.create_node(tag="D1", identifier="d1", parent="d")
|
||||||
|
return t
|
||||||
|
|
||||||
|
def test_tree(self):
|
||||||
|
self.assertEqual(isinstance(self.tree, Tree), True)
|
||||||
|
self.assertEqual(isinstance(self.copytree, Tree), True)
|
||||||
|
|
||||||
|
def test_is_root(self):
|
||||||
|
# retro-compatibility
|
||||||
|
self.assertTrue(self.tree._nodes["hárry"].is_root())
|
||||||
|
self.assertFalse(self.tree._nodes["jane"].is_root())
|
||||||
|
|
||||||
|
def test_tree_wise_is_root(self):
|
||||||
|
subtree = self.tree.subtree("jane", identifier="subtree 2")
|
||||||
|
# harry is root of tree 1 but not present in subtree 2
|
||||||
|
self.assertTrue(self.tree._nodes["hárry"].is_root("tree 1"))
|
||||||
|
self.assertNotIn("hárry", subtree._nodes)
|
||||||
|
# jane is not root of tree 1 but is root of subtree 2
|
||||||
|
self.assertFalse(self.tree._nodes["jane"].is_root("tree 1"))
|
||||||
|
self.assertTrue(subtree._nodes["jane"].is_root("subtree 2"))
|
||||||
|
|
||||||
|
def test_paths_to_leaves(self):
|
||||||
|
paths = self.tree.paths_to_leaves()
|
||||||
|
self.assertEqual(len(paths), 2)
|
||||||
|
self.assertTrue(["hárry", "jane", "diane"] in paths)
|
||||||
|
self.assertTrue(["hárry", "bill", "george"] in paths)
|
||||||
|
|
||||||
|
def test_nodes(self):
|
||||||
|
self.assertEqual(len(self.tree.nodes), 5)
|
||||||
|
self.assertEqual(len(self.tree.all_nodes()), 5)
|
||||||
|
self.assertEqual(self.tree.size(), 5)
|
||||||
|
self.assertEqual(self.tree.get_node("jane").tag, "Jane")
|
||||||
|
self.assertEqual(self.tree.contains("jane"), True)
|
||||||
|
self.assertEqual("jane" in self.tree, True)
|
||||||
|
self.assertEqual(self.tree.contains("alien"), False)
|
||||||
|
self.tree.create_node("Alien", "alien", parent="jane")
|
||||||
|
self.assertEqual(self.tree.contains("alien"), True)
|
||||||
|
self.tree.remove_node("alien")
|
||||||
|
|
||||||
|
def test_getitem(self):
|
||||||
|
"""Nodes can be accessed via getitem."""
|
||||||
|
for node_id in self.tree.nodes:
|
||||||
|
try:
|
||||||
|
self.tree[node_id]
|
||||||
|
except NodeIDAbsentError:
|
||||||
|
self.fail("Node access should be possible via getitem.")
|
||||||
|
try:
|
||||||
|
self.tree["root"]
|
||||||
|
except NodeIDAbsentError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
self.fail("There should be no default fallback value for getitem.")
|
||||||
|
|
||||||
|
def test_parent(self):
|
||||||
|
for nid in self.tree.nodes:
|
||||||
|
if nid == self.tree.root:
|
||||||
|
self.assertEqual(self.tree.parent(nid), None)
|
||||||
|
else:
|
||||||
|
self.assertEqual(self.tree.parent(nid) in self.tree.all_nodes(), True)
|
||||||
|
|
||||||
|
def test_ancestor(self):
|
||||||
|
for nid in self.tree.nodes:
|
||||||
|
if nid == self.tree.root:
|
||||||
|
self.assertEqual(self.tree.ancestor(nid), None)
|
||||||
|
else:
|
||||||
|
for level in range(self.tree.level(nid) - 1, 0, -1):
|
||||||
|
self.assertEqual(
|
||||||
|
self.tree.ancestor(nid, level=level) in self.tree.all_nodes(),
|
||||||
|
True,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_children(self):
|
||||||
|
for nid in self.tree.nodes:
|
||||||
|
children = self.tree.is_branch(nid)
|
||||||
|
for child in children:
|
||||||
|
self.assertEqual(self.tree[child] in self.tree.all_nodes(), True)
|
||||||
|
children = self.tree.children(nid)
|
||||||
|
for child in children:
|
||||||
|
self.assertEqual(child in self.tree.all_nodes(), True)
|
||||||
|
try:
|
||||||
|
self.tree.is_branch("alien")
|
||||||
|
except NodeIDAbsentError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
self.fail("The absent node should be declaimed.")
|
||||||
|
|
||||||
|
def test_remove_node(self):
|
||||||
|
self.tree.create_node("Jill", "jill", parent="george")
|
||||||
|
self.tree.create_node("Mark", "mark", parent="jill")
|
||||||
|
self.assertEqual(self.tree.remove_node("jill"), 2)
|
||||||
|
self.assertEqual(self.tree.get_node("jill") is None, True)
|
||||||
|
self.assertEqual(self.tree.get_node("mark") is None, True)
|
||||||
|
|
||||||
|
def test_tree_wise_depth(self):
|
||||||
|
# Try getting the level of this tree
|
||||||
|
self.assertEqual(self.tree.depth(), 2)
|
||||||
|
self.tree.create_node("Jill", "jill", parent="george")
|
||||||
|
self.assertEqual(self.tree.depth(), 3)
|
||||||
|
self.tree.create_node("Mark", "mark", parent="jill")
|
||||||
|
self.assertEqual(self.tree.depth(), 4)
|
||||||
|
|
||||||
|
# Try getting the level of the node
|
||||||
|
"""
|
||||||
|
self.tree.show()
|
||||||
|
Hárry
|
||||||
|
|___ Bill
|
||||||
|
| |___ George
|
||||||
|
| |___ Jill
|
||||||
|
| |___ Mark
|
||||||
|
|___ Jane
|
||||||
|
| |___ Diane
|
||||||
|
"""
|
||||||
|
self.assertEqual(self.tree.depth(self.tree.get_node("mark")), 4)
|
||||||
|
self.assertEqual(self.tree.depth(self.tree.get_node("jill")), 3)
|
||||||
|
self.assertEqual(self.tree.depth(self.tree.get_node("george")), 2)
|
||||||
|
self.assertEqual(self.tree.depth("jane"), 1)
|
||||||
|
self.assertEqual(self.tree.depth("bill"), 1)
|
||||||
|
self.assertEqual(self.tree.depth("hárry"), 0)
|
||||||
|
|
||||||
|
# Try getting Exception
|
||||||
|
node = Node("Test One", "identifier 1")
|
||||||
|
self.assertRaises(NodeIDAbsentError, self.tree.depth, node)
|
||||||
|
|
||||||
|
# Reset the test case
|
||||||
|
self.tree.remove_node("jill")
|
||||||
|
|
||||||
|
def test_leaves(self):
|
||||||
|
# retro-compatibility
|
||||||
|
leaves = self.tree.leaves()
|
||||||
|
for nid in self.tree.expand_tree():
|
||||||
|
self.assertEqual(
|
||||||
|
(self.tree[nid].is_leaf()) == (self.tree[nid] in leaves), True
|
||||||
|
)
|
||||||
|
leaves = self.tree.leaves(nid="jane")
|
||||||
|
for nid in self.tree.expand_tree(nid="jane"):
|
||||||
|
self.assertEqual(
|
||||||
|
self.tree[nid].is_leaf() == (self.tree[nid] in leaves), True
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_tree_wise_leaves(self):
|
||||||
|
leaves = self.tree.leaves()
|
||||||
|
for nid in self.tree.expand_tree():
|
||||||
|
self.assertEqual(
|
||||||
|
(self.tree[nid].is_leaf("tree 1")) == (self.tree[nid] in leaves), True
|
||||||
|
)
|
||||||
|
leaves = self.tree.leaves(nid="jane")
|
||||||
|
for nid in self.tree.expand_tree(nid="jane"):
|
||||||
|
self.assertEqual(
|
||||||
|
self.tree[nid].is_leaf("tree 1") == (self.tree[nid] in leaves), True
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_link_past_node(self):
|
||||||
|
self.tree.create_node("Jill", "jill", parent="hárry")
|
||||||
|
self.tree.create_node("Mark", "mark", parent="jill")
|
||||||
|
self.assertEqual("mark" not in self.tree.is_branch("hárry"), True)
|
||||||
|
self.tree.link_past_node("jill")
|
||||||
|
self.assertEqual("mark" in self.tree.is_branch("hárry"), True)
|
||||||
|
|
||||||
|
def test_expand_tree(self):
|
||||||
|
# default config
|
||||||
|
# Hárry
|
||||||
|
# |-- Jane
|
||||||
|
# |-- Diane
|
||||||
|
# |-- Bill
|
||||||
|
# |-- George
|
||||||
|
# Traverse in depth first mode preserving insertion order
|
||||||
|
nodes = [nid for nid in self.tree.expand_tree(sorting=False)]
|
||||||
|
self.assertEqual(nodes, ["h\xe1rry", "jane", "diane", "bill", "george"])
|
||||||
|
self.assertEqual(len(nodes), 5)
|
||||||
|
|
||||||
|
# By default traverse depth first and sort child nodes by node tag
|
||||||
|
nodes = [nid for nid in self.tree.expand_tree()]
|
||||||
|
self.assertEqual(nodes, ["h\xe1rry", "bill", "george", "jane", "diane"])
|
||||||
|
self.assertEqual(len(nodes), 5)
|
||||||
|
|
||||||
|
# expanding from specific node
|
||||||
|
nodes = [nid for nid in self.tree.expand_tree(nid="bill")]
|
||||||
|
self.assertEqual(nodes, ["bill", "george"])
|
||||||
|
self.assertEqual(len(nodes), 2)
|
||||||
|
|
||||||
|
# changing into width mode preserving insertion order
|
||||||
|
nodes = [nid for nid in self.tree.expand_tree(mode=Tree.WIDTH, sorting=False)]
|
||||||
|
self.assertEqual(nodes, ["h\xe1rry", "jane", "bill", "diane", "george"])
|
||||||
|
self.assertEqual(len(nodes), 5)
|
||||||
|
|
||||||
|
# Breadth first mode, child nodes sorting by tag
|
||||||
|
nodes = [nid for nid in self.tree.expand_tree(mode=Tree.WIDTH)]
|
||||||
|
self.assertEqual(nodes, ["h\xe1rry", "bill", "jane", "george", "diane"])
|
||||||
|
self.assertEqual(len(nodes), 5)
|
||||||
|
|
||||||
|
# expanding by filters
|
||||||
|
# Stops at root
|
||||||
|
nodes = [nid for nid in self.tree.expand_tree(filter=lambda x: x.tag == "Bill")]
|
||||||
|
self.assertEqual(len(nodes), 0)
|
||||||
|
nodes = [nid for nid in self.tree.expand_tree(filter=lambda x: x.tag != "Bill")]
|
||||||
|
self.assertEqual(nodes, ["h\xe1rry", "jane", "diane"])
|
||||||
|
self.assertEqual(len(nodes), 3)
|
||||||
|
|
||||||
|
def test_move_node(self):
|
||||||
|
diane_parent = self.tree.parent("diane")
|
||||||
|
self.tree.move_node("diane", "bill")
|
||||||
|
self.assertEqual("diane" in self.tree.is_branch("bill"), True)
|
||||||
|
self.tree.move_node("diane", diane_parent.identifier)
|
||||||
|
|
||||||
|
def test_paste_tree(self):
|
||||||
|
new_tree = Tree()
|
||||||
|
new_tree.create_node("Jill", "jill")
|
||||||
|
new_tree.create_node("Mark", "mark", parent="jill")
|
||||||
|
self.tree.paste("jane", new_tree)
|
||||||
|
self.assertEqual("jill" in self.tree.is_branch("jane"), True)
|
||||||
|
self.tree.show()
|
||||||
|
self.assertEqual(
|
||||||
|
self.tree._reader,
|
||||||
|
"""Hárry
|
||||||
|
├── Bill
|
||||||
|
│ └── George
|
||||||
|
└── Jane
|
||||||
|
├── Diane
|
||||||
|
└── Jill
|
||||||
|
└── Mark
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
self.tree.remove_node("jill")
|
||||||
|
self.assertNotIn("jill", self.tree.nodes.keys())
|
||||||
|
self.assertNotIn("mark", self.tree.nodes.keys())
|
||||||
|
self.tree.show()
|
||||||
|
self.assertEqual(
|
||||||
|
self.tree._reader,
|
||||||
|
"""Hárry
|
||||||
|
├── Bill
|
||||||
|
│ └── George
|
||||||
|
└── Jane
|
||||||
|
└── Diane
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_merge(self):
|
||||||
|
# merge on empty initial tree
|
||||||
|
t1 = Tree(identifier="t1")
|
||||||
|
t2 = self.get_t2()
|
||||||
|
t1.merge(nid=None, new_tree=t2)
|
||||||
|
|
||||||
|
self.assertEqual(t1.identifier, "t1")
|
||||||
|
self.assertEqual(t1.root, "r2")
|
||||||
|
self.assertEqual(set(t1._nodes.keys()), {"r2", "c", "d", "d1"})
|
||||||
|
self.assertEqual(
|
||||||
|
t1.show(stdout=False),
|
||||||
|
"""root2
|
||||||
|
├── C
|
||||||
|
└── D
|
||||||
|
└── D1
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
# merge empty new_tree (on root)
|
||||||
|
t1 = self.get_t1()
|
||||||
|
t2 = Tree(identifier="t2")
|
||||||
|
t1.merge(nid="r", new_tree=t2)
|
||||||
|
|
||||||
|
self.assertEqual(t1.identifier, "t1")
|
||||||
|
self.assertEqual(t1.root, "r")
|
||||||
|
self.assertEqual(set(t1._nodes.keys()), {"r", "a", "a1", "b"})
|
||||||
|
self.assertEqual(
|
||||||
|
t1.show(stdout=False),
|
||||||
|
"""root
|
||||||
|
├── A
|
||||||
|
│ └── A1
|
||||||
|
└── B
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
# merge at root
|
||||||
|
t1 = self.get_t1()
|
||||||
|
t2 = self.get_t2()
|
||||||
|
t1.merge(nid="r", new_tree=t2)
|
||||||
|
|
||||||
|
self.assertEqual(t1.identifier, "t1")
|
||||||
|
self.assertEqual(t1.root, "r")
|
||||||
|
self.assertNotIn("r2", t1._nodes.keys())
|
||||||
|
self.assertEqual(set(t1._nodes.keys()), {"r", "a", "a1", "b", "c", "d", "d1"})
|
||||||
|
self.assertEqual(
|
||||||
|
t1.show(stdout=False),
|
||||||
|
"""root
|
||||||
|
├── A
|
||||||
|
│ └── A1
|
||||||
|
├── B
|
||||||
|
├── C
|
||||||
|
└── D
|
||||||
|
└── D1
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
# merge on node
|
||||||
|
t1 = self.get_t1()
|
||||||
|
t2 = self.get_t2()
|
||||||
|
t1.merge(nid="b", new_tree=t2)
|
||||||
|
self.assertEqual(t1.identifier, "t1")
|
||||||
|
self.assertEqual(t1.root, "r")
|
||||||
|
self.assertNotIn("r2", t1._nodes.keys())
|
||||||
|
self.assertEqual(set(t1._nodes.keys()), {"r", "a", "a1", "b", "c", "d", "d1"})
|
||||||
|
self.assertEqual(
|
||||||
|
t1.show(stdout=False),
|
||||||
|
"""root
|
||||||
|
├── A
|
||||||
|
│ └── A1
|
||||||
|
└── B
|
||||||
|
├── C
|
||||||
|
└── D
|
||||||
|
└── D1
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_paste(self):
|
||||||
|
# paste under root
|
||||||
|
t1 = self.get_t1()
|
||||||
|
t2 = self.get_t2()
|
||||||
|
t1.paste(nid="r", new_tree=t2)
|
||||||
|
self.assertEqual(t1.identifier, "t1")
|
||||||
|
self.assertEqual(t1.root, "r")
|
||||||
|
self.assertEqual(t1.parent("r2").identifier, "r")
|
||||||
|
self.assertEqual(
|
||||||
|
set(t1._nodes.keys()), {"r", "r2", "a", "a1", "b", "c", "d", "d1"}
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
t1.show(stdout=False),
|
||||||
|
"""root
|
||||||
|
├── A
|
||||||
|
│ └── A1
|
||||||
|
├── B
|
||||||
|
└── root2
|
||||||
|
├── C
|
||||||
|
└── D
|
||||||
|
└── D1
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
# paste under non-existing node
|
||||||
|
t1 = self.get_t1()
|
||||||
|
t2 = self.get_t2()
|
||||||
|
with self.assertRaises(NodeIDAbsentError) as e:
|
||||||
|
t1.paste(nid="not_existing", new_tree=t2)
|
||||||
|
self.assertEqual(e.exception.args[0], "Node 'not_existing' is not in the tree")
|
||||||
|
|
||||||
|
# paste under None nid
|
||||||
|
t1 = self.get_t1()
|
||||||
|
t2 = self.get_t2()
|
||||||
|
with self.assertRaises(ValueError) as e:
|
||||||
|
t1.paste(nid=None, new_tree=t2)
|
||||||
|
self.assertEqual(
|
||||||
|
e.exception.args[0], 'Must define "nid" under which new tree is pasted.'
|
||||||
|
)
|
||||||
|
|
||||||
|
# paste under node
|
||||||
|
t1 = self.get_t1()
|
||||||
|
t2 = self.get_t2()
|
||||||
|
t1.paste(nid="b", new_tree=t2)
|
||||||
|
self.assertEqual(t1.identifier, "t1")
|
||||||
|
self.assertEqual(t1.root, "r")
|
||||||
|
self.assertEqual(t1.parent("b").identifier, "r")
|
||||||
|
self.assertEqual(
|
||||||
|
set(t1._nodes.keys()), {"r", "a", "a1", "b", "c", "d", "d1", "r2"}
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
t1.show(stdout=False),
|
||||||
|
"""root
|
||||||
|
├── A
|
||||||
|
│ └── A1
|
||||||
|
└── B
|
||||||
|
└── root2
|
||||||
|
├── C
|
||||||
|
└── D
|
||||||
|
└── D1
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
# paste empty new_tree (under root)
|
||||||
|
t1 = self.get_t1()
|
||||||
|
t2 = Tree(identifier="t2")
|
||||||
|
t1.paste(nid="r", new_tree=t2)
|
||||||
|
|
||||||
|
self.assertEqual(t1.identifier, "t1")
|
||||||
|
self.assertEqual(t1.root, "r")
|
||||||
|
self.assertEqual(set(t1._nodes.keys()), {"r", "a", "a1", "b"})
|
||||||
|
self.assertEqual(
|
||||||
|
t1.show(stdout=False),
|
||||||
|
"""root
|
||||||
|
├── A
|
||||||
|
│ └── A1
|
||||||
|
└── B
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_rsearch(self):
|
||||||
|
for nid in ["hárry", "jane", "diane"]:
|
||||||
|
self.assertEqual(nid in self.tree.rsearch("diane"), True)
|
||||||
|
|
||||||
|
def test_subtree(self):
|
||||||
|
subtree_copy = Tree(self.tree.subtree("jane"), deep=True)
|
||||||
|
self.assertEqual(subtree_copy.parent("jane") is None, True)
|
||||||
|
subtree_copy["jane"].tag = "Sweeti"
|
||||||
|
self.assertEqual(self.tree["jane"].tag == "Jane", True)
|
||||||
|
self.assertEqual(subtree_copy.level("diane"), 1)
|
||||||
|
self.assertEqual(subtree_copy.level("jane"), 0)
|
||||||
|
self.assertEqual(self.tree.level("jane"), 1)
|
||||||
|
|
||||||
|
def test_remove_subtree(self):
|
||||||
|
subtree_shallow = self.tree.remove_subtree("jane")
|
||||||
|
self.assertEqual("jane" not in self.tree.is_branch("hárry"), True)
|
||||||
|
self.tree.paste("hárry", subtree_shallow)
|
||||||
|
|
||||||
|
def test_remove_subtree_whole_tree(self):
|
||||||
|
self.tree.remove_subtree("hárry")
|
||||||
|
self.assertIsNone(self.tree.root)
|
||||||
|
self.assertEqual(len(self.tree.nodes.keys()), 0)
|
||||||
|
|
||||||
|
def test_to_json(self):
|
||||||
|
self.assertEqual.__self__.maxDiff = None
|
||||||
|
self.tree.to_json()
|
||||||
|
self.tree.to_json(True)
|
||||||
|
|
||||||
|
def test_siblings(self):
|
||||||
|
self.assertEqual(len(self.tree.siblings("hárry")) == 0, True)
|
||||||
|
self.assertEqual(self.tree.siblings("jane")[0].identifier == "bill", True)
|
||||||
|
|
||||||
|
def test_tree_data(self):
|
||||||
|
class Flower(object):
|
||||||
|
def __init__(self, color):
|
||||||
|
self.color = color
|
||||||
|
|
||||||
|
self.tree.create_node("Jill", "jill", parent="jane", data=Flower("white"))
|
||||||
|
self.assertEqual(self.tree["jill"].data.color, "white")
|
||||||
|
self.tree.remove_node("jill")
|
||||||
|
|
||||||
|
def test_show_data_property(self):
|
||||||
|
new_tree = Tree()
|
||||||
|
|
||||||
|
sys.stdout = open(os.devnull, "w") # stops from printing to console
|
||||||
|
|
||||||
|
try:
|
||||||
|
new_tree.show()
|
||||||
|
|
||||||
|
class Flower(object):
|
||||||
|
def __init__(self, color):
|
||||||
|
self.color = color
|
||||||
|
|
||||||
|
new_tree.create_node("Jill", "jill", data=Flower("white"))
|
||||||
|
new_tree.show(data_property="color")
|
||||||
|
finally:
|
||||||
|
sys.stdout.close()
|
||||||
|
sys.stdout = sys.__stdout__ # stops from printing to console
|
||||||
|
|
||||||
|
def test_level(self):
|
||||||
|
self.assertEqual(self.tree.level("hárry"), 0)
|
||||||
|
depth = self.tree.depth()
|
||||||
|
self.assertEqual(self.tree.level("diane"), depth)
|
||||||
|
self.assertEqual(
|
||||||
|
self.tree.level("diane", lambda x: x.identifier != "jane"), depth - 1
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_size(self):
|
||||||
|
self.assertEqual(self.tree.size(level=2), 2)
|
||||||
|
self.assertEqual(self.tree.size(level=1), 2)
|
||||||
|
self.assertEqual(self.tree.size(level=0), 1)
|
||||||
|
|
||||||
|
def test_print_backend(self):
|
||||||
|
expected_result = """\
|
||||||
|
Hárry
|
||||||
|
├── Bill
|
||||||
|
│ └── George
|
||||||
|
└── Jane
|
||||||
|
└── Diane
|
||||||
|
"""
|
||||||
|
|
||||||
|
assert str(self.tree) == encode(expected_result)
|
||||||
|
|
||||||
|
def test_show(self):
|
||||||
|
if sys.version_info[0] < 3:
|
||||||
|
reload(sys) # noqa: F821
|
||||||
|
sys.setdefaultencoding("utf-8")
|
||||||
|
sys.stdout = open(os.devnull, "w") # stops from printing to console
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.tree.show()
|
||||||
|
finally:
|
||||||
|
sys.stdout.close()
|
||||||
|
sys.stdout = sys.__stdout__ # stops from printing to console
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
self.tree = None
|
||||||
|
self.copytree = None
|
||||||
|
|
||||||
|
def test_show_without_sorting(self):
|
||||||
|
t = Tree()
|
||||||
|
t.create_node("Students", "Students", parent=None)
|
||||||
|
Node(tag="Students", identifier="Students", data=None)
|
||||||
|
t.create_node("Ben", "Ben", parent="Students")
|
||||||
|
Node(tag="Ben", identifier="Ben", data=None)
|
||||||
|
t.create_node("Annie", "Annie", parent="Students")
|
||||||
|
Node(tag="Annie", identifier="Annie", data=None)
|
||||||
|
t.show()
|
||||||
|
self.assertEqual(
|
||||||
|
t.show(sorting=False, stdout=False),
|
||||||
|
"""Students
|
||||||
|
├── Ben
|
||||||
|
└── Annie
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_all_nodes_itr(self):
|
||||||
|
"""
|
||||||
|
tests: Tree.all_nodes_iter
|
||||||
|
Added by: William Rusnack
|
||||||
|
"""
|
||||||
|
new_tree = Tree()
|
||||||
|
self.assertEqual(len(new_tree.all_nodes_itr()), 0)
|
||||||
|
nodes = list()
|
||||||
|
nodes.append(new_tree.create_node("root_node"))
|
||||||
|
nodes.append(new_tree.create_node("second", parent=new_tree.root))
|
||||||
|
for nd in new_tree.all_nodes_itr():
|
||||||
|
self.assertTrue(nd in nodes)
|
||||||
|
|
||||||
|
def test_filter_nodes(self):
|
||||||
|
"""
|
||||||
|
tests: Tree.filter_nodes
|
||||||
|
Added by: William Rusnack
|
||||||
|
"""
|
||||||
|
new_tree = Tree(identifier="tree 1")
|
||||||
|
|
||||||
|
self.assertEqual(tuple(new_tree.filter_nodes(lambda n: True)), ())
|
||||||
|
|
||||||
|
nodes = list()
|
||||||
|
nodes.append(new_tree.create_node("root_node"))
|
||||||
|
nodes.append(new_tree.create_node("second", parent=new_tree.root))
|
||||||
|
|
||||||
|
self.assertEqual(tuple(new_tree.filter_nodes(lambda n: False)), ())
|
||||||
|
self.assertEqual(
|
||||||
|
tuple(new_tree.filter_nodes(lambda n: n.is_root("tree 1"))), (nodes[0],)
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
tuple(new_tree.filter_nodes(lambda n: not n.is_root("tree 1"))), (nodes[1],)
|
||||||
|
)
|
||||||
|
self.assertTrue(set(new_tree.filter_nodes(lambda n: True)), set(nodes))
|
||||||
|
|
||||||
|
def test_loop(self):
|
||||||
|
tree = Tree()
|
||||||
|
tree.create_node("a", "a")
|
||||||
|
tree.create_node("b", "b", parent="a")
|
||||||
|
tree.create_node("c", "c", parent="b")
|
||||||
|
tree.create_node("d", "d", parent="c")
|
||||||
|
try:
|
||||||
|
tree.move_node("b", "d")
|
||||||
|
except LoopError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_modify_node_identifier_directly_failed(self):
|
||||||
|
tree = Tree()
|
||||||
|
tree.create_node("Harry", "harry")
|
||||||
|
tree.create_node("Jane", "jane", parent="harry")
|
||||||
|
n = tree.get_node("jane")
|
||||||
|
self.assertTrue(n.identifier == "jane")
|
||||||
|
|
||||||
|
# Failed to modify
|
||||||
|
n.identifier = "xyz"
|
||||||
|
self.assertTrue(tree.get_node("xyz") is None)
|
||||||
|
self.assertTrue(tree.get_node("jane").identifier == "xyz")
|
||||||
|
|
||||||
|
def test_modify_node_identifier_recursively(self):
|
||||||
|
tree = Tree()
|
||||||
|
tree.create_node("Harry", "harry")
|
||||||
|
tree.create_node("Jane", "jane", parent="harry")
|
||||||
|
n = tree.get_node("jane")
|
||||||
|
self.assertTrue(n.identifier == "jane")
|
||||||
|
|
||||||
|
# Success to modify
|
||||||
|
tree.update_node(n.identifier, identifier="xyz")
|
||||||
|
self.assertTrue(tree.get_node("jane") is None)
|
||||||
|
self.assertTrue(tree.get_node("xyz").identifier == "xyz")
|
||||||
|
|
||||||
|
def test_modify_node_identifier_root(self):
|
||||||
|
tree = Tree(identifier="tree 3")
|
||||||
|
tree.create_node("Harry", "harry")
|
||||||
|
tree.create_node("Jane", "jane", parent="harry")
|
||||||
|
tree.update_node(tree["harry"].identifier, identifier="xyz", tag="XYZ")
|
||||||
|
self.assertTrue(tree.root == "xyz")
|
||||||
|
self.assertTrue(tree["xyz"].tag == "XYZ")
|
||||||
|
self.assertEqual(tree.parent("jane").identifier, "xyz")
|
||||||
|
|
||||||
|
def test_subclassing(self):
|
||||||
|
class SubNode(Node):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class SubTree(Tree):
|
||||||
|
node_class = SubNode
|
||||||
|
|
||||||
|
tree = SubTree()
|
||||||
|
node = tree.create_node()
|
||||||
|
self.assertTrue(isinstance(node, SubNode))
|
||||||
|
|
||||||
|
tree = Tree(node_class=SubNode)
|
||||||
|
node = tree.create_node()
|
||||||
|
self.assertTrue(isinstance(node, SubNode))
|
||||||
|
|
||||||
|
def test_shallow_copy_hermetic_pointers(self):
|
||||||
|
# tree 1
|
||||||
|
# Hárry
|
||||||
|
# └── Jane
|
||||||
|
# └── Diane
|
||||||
|
# └── Bill
|
||||||
|
# └── George
|
||||||
|
tree2 = self.tree.subtree(nid="jane", identifier="tree 2")
|
||||||
|
# tree 2
|
||||||
|
# Jane
|
||||||
|
# └── Diane
|
||||||
|
|
||||||
|
# check that in shallow copy, instances are the same
|
||||||
|
self.assertIs(self.tree["jane"], tree2["jane"])
|
||||||
|
self.assertEqual(
|
||||||
|
self.tree["jane"]._predecessor, {"tree 1": "hárry", "tree 2": None}
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
dict(self.tree["jane"]._successors),
|
||||||
|
{"tree 1": ["diane"], "tree 2": ["diane"]},
|
||||||
|
)
|
||||||
|
|
||||||
|
# when creating new node on subtree, check that it has no impact on initial tree
|
||||||
|
tree2.create_node("Jill", "jill", parent="diane")
|
||||||
|
self.assertIn("jill", tree2)
|
||||||
|
self.assertIn("jill", tree2.is_branch("diane"))
|
||||||
|
self.assertNotIn("jill", self.tree)
|
||||||
|
self.assertNotIn("jill", self.tree.is_branch("diane"))
|
||||||
|
|
||||||
|
def test_paste_duplicate_nodes(self):
|
||||||
|
t1 = Tree()
|
||||||
|
t1.create_node(identifier="A")
|
||||||
|
t2 = Tree()
|
||||||
|
t2.create_node(identifier="A")
|
||||||
|
t2.create_node(identifier="B", parent="A")
|
||||||
|
|
||||||
|
with self.assertRaises(ValueError) as e:
|
||||||
|
t1.paste("A", t2)
|
||||||
|
self.assertEqual(e.exception.args, ("Duplicated nodes ['A'] exists.",))
|
||||||
|
|
||||||
|
def test_shallow_paste(self):
|
||||||
|
t1 = Tree()
|
||||||
|
n1 = t1.create_node(identifier="A")
|
||||||
|
|
||||||
|
t2 = Tree()
|
||||||
|
n2 = t2.create_node(identifier="B")
|
||||||
|
|
||||||
|
t3 = Tree()
|
||||||
|
n3 = t3.create_node(identifier="C")
|
||||||
|
|
||||||
|
t1.paste(n1.identifier, t2)
|
||||||
|
self.assertEqual(t1.to_dict(), {"A": {"children": ["B"]}})
|
||||||
|
t1.paste(n1.identifier, t3)
|
||||||
|
self.assertEqual(t1.to_dict(), {"A": {"children": ["B", "C"]}})
|
||||||
|
|
||||||
|
self.assertEqual(t1.level(n1.identifier), 0)
|
||||||
|
self.assertEqual(t1.level(n2.identifier), 1)
|
||||||
|
self.assertEqual(t1.level(n3.identifier), 1)
|
||||||
|
|
||||||
|
def test_root_removal(self):
|
||||||
|
t = Tree()
|
||||||
|
t.create_node(identifier="root-A")
|
||||||
|
self.assertEqual(len(t.nodes.keys()), 1)
|
||||||
|
self.assertEqual(t.root, "root-A")
|
||||||
|
t.remove_node(identifier="root-A")
|
||||||
|
self.assertEqual(len(t.nodes.keys()), 0)
|
||||||
|
self.assertEqual(t.root, None)
|
||||||
|
t.create_node(identifier="root-B")
|
||||||
|
self.assertEqual(len(t.nodes.keys()), 1)
|
||||||
|
self.assertEqual(t.root, "root-B")
|
39
treelib/__init__.py
Normal file
39
treelib/__init__.py
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
# Copyright (C) 2011
|
||||||
|
# Brett Alistair Kromkamp - brettkromkamp@gmail.com
|
||||||
|
# Copyright (C) 2012-2017
|
||||||
|
# Xiaming Chen - chenxm35@gmail.com
|
||||||
|
# and other contributors.
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
"""
|
||||||
|
treelib - Python 2/3 Tree Implementation
|
||||||
|
|
||||||
|
`treelib` is a Python module with two primary classes: Node and Tree.
|
||||||
|
Tree is a self-contained structure with some nodes and connected by branches.
|
||||||
|
A tree owns merely a root, while a node (except root) has some children and one parent.
|
||||||
|
|
||||||
|
Note: To solve string compatibility between Python 2.x and 3.x, treelib follows
|
||||||
|
the way of porting Python 3.x to 2/3. That means, all strings are manipulated as
|
||||||
|
unicode and you do not need u'' prefix anymore. The impacted functions include `str()`,
|
||||||
|
`show()` and `save2file()` routines.
|
||||||
|
But if your data contains non-ascii characters and Python 2.x is used,
|
||||||
|
you have to trigger the compatibility by declaring `unicode_literals` in the code:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
>>> from __future__ import unicode_literals
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .tree import Tree # noqa: F401
|
||||||
|
from .node import Node # noqa: F401
|
50
treelib/exceptions.py
Normal file
50
treelib/exceptions.py
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
class NodePropertyError(Exception):
|
||||||
|
"""Basic Node attribute error"""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class NodeIDAbsentError(NodePropertyError):
|
||||||
|
"""Exception throwed if a node's identifier is unknown"""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class NodePropertyAbsentError(NodePropertyError):
|
||||||
|
"""Exception throwed if a node's data property is not specified"""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class MultipleRootError(Exception):
|
||||||
|
"""Exception throwed if more than one root exists in a tree."""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class DuplicatedNodeIdError(Exception):
|
||||||
|
"""Exception throwed if an identifier already exists in a tree."""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class LinkPastRootNodeError(Exception):
|
||||||
|
"""
|
||||||
|
Exception throwed in Tree.link_past_node() if one attempts
|
||||||
|
to "link past" the root node of a tree.
|
||||||
|
"""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidLevelNumber(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class LoopError(Exception):
|
||||||
|
"""
|
||||||
|
Exception thrown if trying to move node B to node A's position
|
||||||
|
while A is B's ancestor.
|
||||||
|
"""
|
||||||
|
|
||||||
|
pass
|
47
treelib/misc.py
Normal file
47
treelib/misc.py
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# Copyright (C) 2011
|
||||||
|
# Brett Alistair Kromkamp - brettkromkamp@gmail.com
|
||||||
|
# Copyright (C) 2012-2017
|
||||||
|
# Xiaming Chen - chenxm35@gmail.com
|
||||||
|
# and other contributors.
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
import functools
|
||||||
|
from warnings import warn, simplefilter
|
||||||
|
|
||||||
|
|
||||||
|
def deprecated(alias):
|
||||||
|
def real_deco(func):
|
||||||
|
"""This is a decorator which can be used to mark functions
|
||||||
|
as deprecated. It will result in a warning being emmitted
|
||||||
|
when the function is used.
|
||||||
|
Derived from answer by Leando: https://stackoverflow.com/a/30253848
|
||||||
|
"""
|
||||||
|
|
||||||
|
@functools.wraps(func)
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
simplefilter("always", DeprecationWarning) # turn off filter
|
||||||
|
warn(
|
||||||
|
'Call to deprecated function "{}"; use "{}" instead.'.format(
|
||||||
|
func.__name__, alias
|
||||||
|
),
|
||||||
|
category=DeprecationWarning,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
|
simplefilter("default", DeprecationWarning) # reset filter
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
return real_deco
|
279
treelib/node.py
Normal file
279
treelib/node.py
Normal file
|
@ -0,0 +1,279 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# Copyright (C) 2011
|
||||||
|
# Brett Alistair Kromkamp - brettkromkamp@gmail.com
|
||||||
|
# Copyright (C) 2012-2017
|
||||||
|
# Xiaming Chen - chenxm35@gmail.com
|
||||||
|
# and other contributors.
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
"""
|
||||||
|
Node structure in treelib.
|
||||||
|
|
||||||
|
A :class:`Node` object contains basic properties such as node identifier,
|
||||||
|
node tag, parent node, children nodes etc., and some operations for a node.
|
||||||
|
"""
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import copy
|
||||||
|
import uuid
|
||||||
|
from collections import defaultdict
|
||||||
|
from warnings import warn
|
||||||
|
|
||||||
|
from .exceptions import NodePropertyError
|
||||||
|
from .misc import deprecated
|
||||||
|
|
||||||
|
|
||||||
|
class Node(object):
|
||||||
|
"""
|
||||||
|
Nodes are elementary objects that are stored in the `_nodes` dictionary of a Tree.
|
||||||
|
Use `data` attribute to store node-specific data.
|
||||||
|
"""
|
||||||
|
|
||||||
|
#: Mode constants for routine `update_fpointer()`.
|
||||||
|
(ADD, DELETE, INSERT, REPLACE) = list(range(4))
|
||||||
|
|
||||||
|
def __init__(self, tag=None, identifier=None, expanded=True, data=None):
|
||||||
|
"""Create a new Node object to be placed inside a Tree object"""
|
||||||
|
|
||||||
|
#: if given as a parameter, must be unique
|
||||||
|
self._identifier = None
|
||||||
|
self._set_identifier(identifier)
|
||||||
|
|
||||||
|
#: None or something else
|
||||||
|
#: if None, self._identifier will be set to the identifier's value.
|
||||||
|
if tag is None:
|
||||||
|
self._tag = self._identifier
|
||||||
|
else:
|
||||||
|
self._tag = tag
|
||||||
|
|
||||||
|
#: boolean
|
||||||
|
self.expanded = expanded
|
||||||
|
|
||||||
|
#: identifier of the parent's node :
|
||||||
|
self._predecessor = {}
|
||||||
|
#: identifier(s) of the soons' node(s) :
|
||||||
|
self._successors = defaultdict(list)
|
||||||
|
|
||||||
|
#: User payload associated with this node.
|
||||||
|
self.data = data
|
||||||
|
|
||||||
|
# for retro-compatibility on bpointer/fpointer
|
||||||
|
self._initial_tree_id = None
|
||||||
|
|
||||||
|
def __lt__(self, other):
|
||||||
|
return self.tag < other.tag
|
||||||
|
|
||||||
|
def set_initial_tree_id(self, tree_id):
|
||||||
|
if self._initial_tree_id is None:
|
||||||
|
self._initial_tree_id = tree_id
|
||||||
|
|
||||||
|
def _set_identifier(self, nid):
|
||||||
|
"""Initialize self._set_identifier"""
|
||||||
|
if nid is None:
|
||||||
|
self._identifier = str(uuid.uuid1())
|
||||||
|
else:
|
||||||
|
self._identifier = nid
|
||||||
|
|
||||||
|
@property
|
||||||
|
@deprecated(alias="node.predecessor")
|
||||||
|
def bpointer(self):
|
||||||
|
"""Use predecessor method, this property is deprecated and only kept for retro-compatilibity. Parents of
|
||||||
|
a node are dependant on a given tree. This implementation keeps the previous behavior by keeping returning
|
||||||
|
bpointer of first declared tree.
|
||||||
|
"""
|
||||||
|
if self._initial_tree_id not in self._predecessor.keys():
|
||||||
|
return None
|
||||||
|
return self._predecessor[self._initial_tree_id]
|
||||||
|
|
||||||
|
@bpointer.setter
|
||||||
|
@deprecated(alias="node.set_predecessor")
|
||||||
|
def bpointer(self, value):
|
||||||
|
self.set_predecessor(value, self._initial_tree_id)
|
||||||
|
|
||||||
|
@deprecated(alias="node.set_predecessor")
|
||||||
|
def update_bpointer(self, nid):
|
||||||
|
self.set_predecessor(nid, self._initial_tree_id)
|
||||||
|
|
||||||
|
@property
|
||||||
|
@deprecated(alias="node.successors")
|
||||||
|
def fpointer(self):
|
||||||
|
"""Use successors method, this property is deprecated and only kept for retro-compatilibity. Children of
|
||||||
|
a node are dependant on a given tree. This implementation keeps the previous behavior by keeping returning
|
||||||
|
fpointer of first declared tree.
|
||||||
|
"""
|
||||||
|
if self._initial_tree_id not in self._successors:
|
||||||
|
return []
|
||||||
|
return self._successors[self._initial_tree_id]
|
||||||
|
|
||||||
|
@fpointer.setter
|
||||||
|
@deprecated(alias="node.update_successors")
|
||||||
|
def fpointer(self, value):
|
||||||
|
self.set_successors(value, tree_id=self._initial_tree_id)
|
||||||
|
|
||||||
|
@deprecated(alias="node.update_successors")
|
||||||
|
def update_fpointer(self, nid, mode=ADD, replace=None):
|
||||||
|
"""Deprecated"""
|
||||||
|
self.update_successors(nid, mode, replace, self._initial_tree_id)
|
||||||
|
|
||||||
|
def predecessor(self, tree_id):
|
||||||
|
"""
|
||||||
|
The parent ID of a node in a given tree.
|
||||||
|
"""
|
||||||
|
return self._predecessor[tree_id]
|
||||||
|
|
||||||
|
def set_predecessor(self, nid, tree_id):
|
||||||
|
"""Set the value of `_predecessor`."""
|
||||||
|
self._predecessor[tree_id] = nid
|
||||||
|
|
||||||
|
def successors(self, tree_id):
|
||||||
|
"""
|
||||||
|
With a getting operator, a list of IDs of node's children is obtained. With
|
||||||
|
a setting operator, the value can be list, set, or dict. For list or set,
|
||||||
|
it is converted to a list type by the package; for dict, the keys are
|
||||||
|
treated as the node IDs.
|
||||||
|
"""
|
||||||
|
return self._successors[tree_id]
|
||||||
|
|
||||||
|
def set_successors(self, value, tree_id=None):
|
||||||
|
"""Set the value of `_successors`."""
|
||||||
|
setter_lookup = {
|
||||||
|
"NoneType": lambda x: list(),
|
||||||
|
"list": lambda x: x,
|
||||||
|
"dict": lambda x: list(x.keys()),
|
||||||
|
"set": lambda x: list(x),
|
||||||
|
}
|
||||||
|
|
||||||
|
t = value.__class__.__name__
|
||||||
|
if t in setter_lookup:
|
||||||
|
f_setter = setter_lookup[t]
|
||||||
|
self._successors[tree_id] = f_setter(value)
|
||||||
|
else:
|
||||||
|
raise NotImplementedError("Unsupported value type %s" % t)
|
||||||
|
|
||||||
|
def update_successors(self, nid, mode=ADD, replace=None, tree_id=None):
|
||||||
|
"""
|
||||||
|
Update the children list with different modes: addition (Node.ADD or
|
||||||
|
Node.INSERT) and deletion (Node.DELETE).
|
||||||
|
"""
|
||||||
|
if nid is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
def _manipulator_append():
|
||||||
|
self.successors(tree_id).append(nid)
|
||||||
|
|
||||||
|
def _manipulator_delete():
|
||||||
|
if nid in self.successors(tree_id):
|
||||||
|
self.successors(tree_id).remove(nid)
|
||||||
|
else:
|
||||||
|
warn("Nid %s wasn't present in fpointer" % nid)
|
||||||
|
|
||||||
|
def _manipulator_insert():
|
||||||
|
warn("WARNING: INSERT is deprecated to ADD mode")
|
||||||
|
self.update_successors(nid, tree_id=tree_id)
|
||||||
|
|
||||||
|
def _manipulator_replace():
|
||||||
|
if replace is None:
|
||||||
|
raise NodePropertyError(
|
||||||
|
'Argument "repalce" should be provided when mode is {}'.format(mode)
|
||||||
|
)
|
||||||
|
ind = self.successors(tree_id).index(nid)
|
||||||
|
self.successors(tree_id)[ind] = replace
|
||||||
|
|
||||||
|
manipulator_lookup = {
|
||||||
|
self.ADD: "_manipulator_append",
|
||||||
|
self.DELETE: "_manipulator_delete",
|
||||||
|
self.INSERT: "_manipulator_insert",
|
||||||
|
self.REPLACE: "_manipulator_replace",
|
||||||
|
}
|
||||||
|
|
||||||
|
if mode not in manipulator_lookup:
|
||||||
|
raise NotImplementedError("Unsupported node updating mode %s" % str(mode))
|
||||||
|
|
||||||
|
f_name = manipulator_lookup.get(mode)
|
||||||
|
f = locals()[f_name]
|
||||||
|
return f()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def identifier(self):
|
||||||
|
"""
|
||||||
|
The unique ID of a node within the scope of a tree. This attribute can be
|
||||||
|
accessed and modified with ``.`` and ``=`` operator respectively.
|
||||||
|
"""
|
||||||
|
return self._identifier
|
||||||
|
|
||||||
|
def clone_pointers(self, former_tree_id, new_tree_id):
|
||||||
|
former_bpointer = self.predecessor(former_tree_id)
|
||||||
|
self.set_predecessor(former_bpointer, new_tree_id)
|
||||||
|
former_fpointer = self.successors(former_tree_id)
|
||||||
|
# fpointer is a list and would be copied by reference without deepcopy
|
||||||
|
self.set_successors(copy.deepcopy(former_fpointer), tree_id=new_tree_id)
|
||||||
|
|
||||||
|
def reset_pointers(self, tree_id):
|
||||||
|
self.set_predecessor(None, tree_id)
|
||||||
|
self.set_successors([], tree_id=tree_id)
|
||||||
|
|
||||||
|
@identifier.setter
|
||||||
|
def identifier(self, value):
|
||||||
|
"""Set the value of `_identifier`."""
|
||||||
|
if value is None:
|
||||||
|
print("WARNING: node ID can not be None")
|
||||||
|
else:
|
||||||
|
self._set_identifier(value)
|
||||||
|
|
||||||
|
def is_leaf(self, tree_id=None):
|
||||||
|
"""Return true if current node has no children."""
|
||||||
|
if tree_id is None:
|
||||||
|
# for retro-compatilibity
|
||||||
|
if self._initial_tree_id not in self._successors.keys():
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
tree_id = self._initial_tree_id
|
||||||
|
|
||||||
|
if len(self.successors(tree_id)) == 0:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def is_root(self, tree_id=None):
|
||||||
|
"""Return true if self has no parent, i.e. as root."""
|
||||||
|
if tree_id is None:
|
||||||
|
# for retro-compatilibity
|
||||||
|
if self._initial_tree_id not in self._predecessor.keys():
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
tree_id = self._initial_tree_id
|
||||||
|
|
||||||
|
return self.predecessor(tree_id) is None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def tag(self):
|
||||||
|
"""
|
||||||
|
The readable node name for human. This attribute can be accessed and
|
||||||
|
modified with ``.`` and ``=`` operator respectively.
|
||||||
|
"""
|
||||||
|
return self._tag
|
||||||
|
|
||||||
|
@tag.setter
|
||||||
|
def tag(self, value):
|
||||||
|
"""Set the value of `_tag`."""
|
||||||
|
self._tag = value if value is not None else None
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
name = self.__class__.__name__
|
||||||
|
kwargs = [
|
||||||
|
"tag={0}".format(self.tag),
|
||||||
|
"identifier={0}".format(self.identifier),
|
||||||
|
"data={0}".format(self.data),
|
||||||
|
]
|
||||||
|
return "%s(%s)" % (name, ", ".join(kwargs))
|
35
treelib/plugins.py
Normal file
35
treelib/plugins.py
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright (C) 2011
|
||||||
|
# Brett Alistair Kromkamp - brettkromkamp@gmail.com
|
||||||
|
# Copyright (C) 2012-2017
|
||||||
|
# Xiaming Chen - chenxm35@gmail.com
|
||||||
|
# and other contributors.
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
"""
|
||||||
|
This is a public location to maintain contributed
|
||||||
|
utilities to extend the basic Tree class.
|
||||||
|
|
||||||
|
Deprecated! We prefer a unified processing of Tree object.
|
||||||
|
"""
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from .misc import deprecated
|
||||||
|
|
||||||
|
|
||||||
|
@deprecated(alias="tree.to_graphviz()")
|
||||||
|
def export_to_dot(tree, filename=None, shape="circle", graph="digraph"):
|
||||||
|
"""Exports the tree in the dot format of the graphviz software"""
|
||||||
|
tree.to_graphviz(filename=filename, shape=shape, graph=graph)
|
1130
treelib/tree.py
Normal file
1130
treelib/tree.py
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Add table
Reference in a new issue