From 70221d61190d6de0ba1e025ec6a025298ae8f997 Mon Sep 17 00:00:00 2001 From: Jamin Chen Date: Mon, 27 Mar 2023 19:34:40 +0800 Subject: [PATCH] Several minor improvements (#209) * Add github workflow * Replace nose as pytest for py3.7+ * Apply code style to flake8 + black --- .github/workflows/python-package.yml | 59 +++ HISTORY | 3 + README.md | 3 +- examples/family_tree.py | 22 +- examples/folder_tree.py | 97 +++-- examples/recursive_dirtree_generator.py | 16 +- examples/save_tree_2_file.py | 2 +- ...ts-testing.txt => requirements-t-pre37.txt | 2 +- requirements-t.txt | 5 + requirements.txt | 1 + scripts/flake8.sh | 3 + testing.sh => scripts/testing.sh | 0 setup.py | 52 +-- tests/test_node.py | 15 +- tests/test_plugins.py | 63 ++-- tests/test_tree.py | 349 ++++++++++-------- treelib/exceptions.py | 7 + treelib/misc.py | 13 +- treelib/node.py | 36 +- treelib/plugins.py | 4 +- treelib/tree.py | 268 ++++++++++---- 21 files changed, 647 insertions(+), 373 deletions(-) create mode 100644 .github/workflows/python-package.yml rename requirements-testing.txt => requirements-t-pre37.txt (66%) create mode 100644 requirements-t.txt create mode 100644 requirements.txt create mode 100755 scripts/flake8.sh rename testing.sh => scripts/testing.sh (100%) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml new file mode 100644 index 0000000..fc7a708 --- /dev/null +++ b/.github/workflows/python-package.yml @@ -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 \ No newline at end of file diff --git a/HISTORY b/HISTORY index 6e3ac62..b28a57a 100644 --- a/HISTORY +++ b/HISTORY @@ -1,3 +1,6 @@ +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. diff --git a/README.md b/README.md index 77b4cdf..dc6011d 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,8 @@ treelib Tree implementation in python: simple for you to use. -[![Build Status](https://travis-ci.org/caesar0301/treelib.svg?branch=master)](https://travis-ci.org/caesar0301/treelib) +[![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) diff --git a/examples/family_tree.py b/examples/family_tree.py index 0a2ad83..d8b956d 100644 --- a/examples/family_tree.py +++ b/examples/family_tree.py @@ -3,7 +3,7 @@ # # Author: chenxm # -__author__ = 'chenxm' +__author__ = "chenxm" from treelib import Tree, Node @@ -21,28 +21,28 @@ def create_family_tree(): def example(desp): - sep = "-"*20 + '\n' + sep = "-" * 20 + "\n" print(sep + desp) -if __name__ == '__main__': +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') + 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()])) + 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') + tree.show(idhidden=False, filter=lambda x: x.identifier != "diane") example("Let me introduce Diane family only:") - sub_t = tree.subtree('diane') + sub_t = tree.subtree("diane") sub_t.show() example("Children of Diane:") - for child in tree.is_branch('diane'): + for child in tree.is_branch("diane"): print(tree[child].tag) example("New members join Jill's family:") @@ -50,7 +50,7 @@ def example(desp): 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.paste("bill", new_tree) tree.show() example("They leave after a while:") @@ -58,8 +58,8 @@ def example(desp): tree.show() example("Now Mary moves to live with grandfather Harry:") - tree.move_node('mary', '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')])) + print(",".join([tree[node].tag for node in tree.rsearch("mark")])) diff --git a/examples/folder_tree.py b/examples/folder_tree.py index eb572d8..0e942e8 100644 --- a/examples/folder_tree.py +++ b/examples/folder_tree.py @@ -5,7 +5,7 @@ # and pattern variables # -__author__ = 'holger' +__author__ = "holger" from treelib import tree @@ -33,9 +33,13 @@ 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') +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 @@ -44,39 +48,46 @@ folder_blacklist = [] dir_tree = tree.Tree() -dir_tree.create_node('Root', rootPath) # root node +dir_tree.create_node("Root", rootPath) # root node def crc32(data): - data = bytes(data, 'UTF-8') + 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 + 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('/') +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 + """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 '_' + 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 '_' - _+++ - e.g. 2_Folder_XYZ_1+++ + _+++ + e.g. 2_Folder_XYZ_1+++ """ - return str(str(depth) + '_' + dir).replace(" ", "_") + '+++' + crc32(os.path.join(root, dir)) + return ( + str(str(depth) + "_" + dir).replace(" ", "_") + + "+++" + + crc32(os.path.join(root, dir)) + ) + # TODO: Verzeichnistiefe pruefen: Was ist mit sowas /mp3/ @@ -92,20 +103,26 @@ def get_parentid(current_depth, root, dir): # 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] - parentid = str(current_depth - 1) + '_' + parent_dir.replace(" ", "_") + '+++' + crc32(root) + pos2 = search_string.rfind("/") + pos1 = search_string.rfind("/", 0, pos2) + parent_dir = search_string[pos1 + 1 : pos2] + 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) + print("#############################") + print("node created") + print(" dir: " + dir) + print(" note_id: " + node_id) + print(" parent: " + parent_id) def crawler(): @@ -118,10 +135,10 @@ def crawler(): for dir in dirs: # calculating current depth - current_depth = os.path.join(root, dir).count('/') - start_depth + current_depth = os.path.join(root, dir).count("/") - start_depth if DEBUG: - print('current: ' + os.path.join(root, dir)) + print("current: " + os.path.join(root, dir)) node_id = get_noteid(current_depth, root, dir) parent_id = str(get_parentid(current_depth, root, dir)) @@ -143,10 +160,10 @@ def crawler(): continue # calculating current depth - current_depth = os.path.join(root, filename).count('/') - start_depth + current_depth = os.path.join(root, filename).count("/") - start_depth if DEBUG: - print('current: ' + os.path.join(root, filename)) + print("current: " + os.path.join(root, filename)) node_id = get_noteid(current_depth, root, filename) parent_id = str(get_parentid(current_depth, root, filename)) @@ -166,28 +183,28 @@ def crawler(): crawler() if PROFILING == 1: t1 = timeit.Timer("crawler()", "from __main__ import crawler") - print('time: ' + str(t1.timeit(number=1))) + print("time: " + str(t1.timeit(number=1))) if PROFILING == 2: cProfile.run("crawler()") -print('filecount: ' + str(FILECOUNT)) -print('dircount: ' + str(DIRCOUNT)) +print("filecount: " + str(FILECOUNT)) +print("dircount: " + str(DIRCOUNT)) if DIR_ERRORLIST: for item in DIR_ERRORLIST: print(item) else: - print('no directory errors') + print("no directory errors") -print('\n\n\n') +print("\n\n\n") if FILE_ERRORLIST: for item in FILE_ERRORLIST: print(item) else: - print('no file errors') + print("no file errors") -print('nodes: ' + str(len(dir_tree.nodes))) +print("nodes: " + str(len(dir_tree.nodes))) dir_tree.show() diff --git a/examples/recursive_dirtree_generator.py b/examples/recursive_dirtree_generator.py index 50307af..776c193 100644 --- a/examples/recursive_dirtree_generator.py +++ b/examples/recursive_dirtree_generator.py @@ -16,7 +16,7 @@ def get_random_string(length): - return ''.join(random.choice(digits + letters) for _ in range(length)) + return "".join(random.choice(digits + letters) for _ in range(length)) def build_recursive_tree(tree, base, depth, width): @@ -34,8 +34,12 @@ def build_recursive_tree(tree, base, depth, width): depth -= 1 for i in xrange(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 + 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) @@ -47,7 +51,9 @@ def build_recursive_tree(tree, base, depth, width): class Directory(object): def __init__(self): self._name = get_random_string(64) - self._files = [File() for _ in xrange(MAX_FILES_PER_DIR)] # Each directory contains 1000 files + self._files = [ + File() for _ in xrange(MAX_FILES_PER_DIR) + ] # Each directory contains 1000 files @property def name(self): @@ -68,7 +74,7 @@ def name(self): tree = treelib.Tree() -base = tree.create_node('Root', 'root') +base = tree.create_node("Root", "root") build_recursive_tree(tree, base, 2, 10) tree.show() diff --git a/examples/save_tree_2_file.py b/examples/save_tree_2_file.py index 8b46fab..3b564f5 100644 --- a/examples/save_tree_2_file.py +++ b/examples/save_tree_2_file.py @@ -9,4 +9,4 @@ 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') +tree.save2file("tree.txt") diff --git a/requirements-testing.txt b/requirements-t-pre37.txt similarity index 66% rename from requirements-testing.txt rename to requirements-t-pre37.txt index 10f43bd..b50e6c3 100644 --- a/requirements-testing.txt +++ b/requirements-t-pre37.txt @@ -1,3 +1,3 @@ nose>=1.3.7 -coverage>=4.4.1 nose-exclude>=0.5.0 +coverage>=4.4.1 \ No newline at end of file diff --git a/requirements-t.txt b/requirements-t.txt new file mode 100644 index 0000000..eaed303 --- /dev/null +++ b/requirements-t.txt @@ -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 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..63a9655 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +six>=1.13.0 \ No newline at end of file diff --git a/scripts/flake8.sh b/scripts/flake8.sh new file mode 100755 index 0000000..9e05f3e --- /dev/null +++ b/scripts/flake8.sh @@ -0,0 +1,3 @@ +#!/bin/bash +flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics +flake8 . --count --exit-zero --max-line-length=127 --statistics diff --git a/testing.sh b/scripts/testing.sh similarity index 100% rename from testing.sh rename to scripts/testing.sh diff --git a/setup.py b/setup.py index 03d290c..559a1a7 100644 --- a/setup.py +++ b/setup.py @@ -1,35 +1,39 @@ from setuptools import setup -__version__ = '1.6.2' +__version__ = "1.6.3" 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.''', + 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'], + 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', - 'Topic :: Software Development :: Libraries :: Python Modules', - ], + "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", + ], ) diff --git a/tests/test_node.py b/tests/test_node.py index 0a643c5..b67dfe5 100644 --- a/tests/test_node.py +++ b/tests/test_node.py @@ -29,7 +29,7 @@ def test_set_tag(self): def test_object_as_node_tag(self): node = Node(tag=(0, 1)) self.assertEqual(node.tag, (0, 1)) - self.assertTrue(node.__repr__().startswith('Node')) + self.assertTrue(node.__repr__().startswith("Node")) def test_set_identifier(self): self.node1.identifier = "ID1" @@ -39,28 +39,28 @@ def test_set_identifier(self): def test_set_fpointer(self): # retro-compatibility self.node1.update_fpointer("identifier 2") - self.assertEqual(self.node1.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.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.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.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) @@ -77,7 +77,6 @@ def test_tree_wise_is_leaf(self): self.assertEqual(self.node2.is_leaf("tree 1"), True) def test_data(self): - class Flower(object): def __init__(self, color): self.color = color diff --git a/tests/test_plugins.py b/tests/test_plugins.py index 049b6b1..05fde9d 100644 --- a/tests/test_plugins.py +++ b/tests/test_plugins.py @@ -23,14 +23,14 @@ def setUp(self): self.tree = tree def read_generated_output(self, filename): - output = codecs.open(filename, 'r', 'utf-8') + 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') + export_to_dot(self.tree, "tree.dot") expected = """\ digraph tree { \t"hárry" [label="Hárry", shape=circle] @@ -45,52 +45,69 @@ def test_export_to_dot(self): \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.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') + 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') + 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.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') + 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') + 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') + 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') + 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') + 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 diff --git a/tests/test_tree.py b/tests/test_tree.py index ab9e7cc..308b6c5 100644 --- a/tests/test_tree.py +++ b/tests/test_tree.py @@ -18,7 +18,7 @@ def encode(value): if sys.version_info[0] == 2: # Python2.x : - return value.encode('utf-8') + return value.encode("utf-8") else: # Python3.x : return value @@ -27,9 +27,9 @@ def encode(value): class TreeCase(unittest.TestCase): def setUp(self): tree = Tree(identifier="tree 1") - tree.create_node(u"Hárry", u"hárry") - tree.create_node("Jane", "jane", parent=u"hárry") - tree.create_node("Bill", "bill", parent=u"hárry") + 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 @@ -48,11 +48,11 @@ def get_t1(): │ └── 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') + 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 @@ -63,11 +63,11 @@ def get_t2(): └── 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') + 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): @@ -76,23 +76,23 @@ def test_tree(self): def test_is_root(self): # retro-compatibility - self.assertTrue(self.tree._nodes['hárry'].is_root()) - self.assertFalse(self.tree._nodes['jane'].is_root()) + 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) + 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")) + 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) + 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) @@ -112,21 +112,20 @@ def test_getitem(self): try: self.tree[node_id] except NodeIDAbsentError: - self.fail('Node access should be possible via getitem.') + self.fail("Node access should be possible via getitem.") try: - self.tree['root'] + self.tree["root"] except NodeIDAbsentError: pass else: - self.fail('There should be no default fallback value for getitem.') + 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) + self.assertEqual(self.tree.parent(nid) in self.tree.all_nodes(), True) def test_ancestor(self): for nid in self.tree.nodes: @@ -134,15 +133,16 @@ def test_ancestor(self): 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) + 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) + 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) @@ -197,21 +197,26 @@ 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) + 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 + (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 ) - 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") @@ -229,27 +234,27 @@ def test_expand_tree(self): # |-- George # Traverse in depth first mode preserving insertion order nodes = [nid for nid in self.tree.expand_tree(sorting=False)] - self.assertEqual(nodes, [u'h\xe1rry', u'jane', u'diane', u'bill', u'george']) + 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, [u'h\xe1rry', u'bill', u'george', u'jane', u'diane']) + 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, [u'bill', u'george']) + 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, [u'h\xe1rry', u'jane', u'bill', u'diane', u'george']) + 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, [u'h\xe1rry', u'bill', u'jane', u'george', u'diane']) + self.assertEqual(nodes, ["h\xe1rry", "bill", "jane", "george", "diane"]) self.assertEqual(len(nodes), 5) # expanding by filters @@ -257,7 +262,7 @@ def test_expand_tree(self): 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, [u'h\xe1rry', u'jane', u'diane']) + self.assertEqual(nodes, ["h\xe1rry", "jane", "diane"]) self.assertEqual(len(nodes), 3) def test_move_node(self): @@ -275,105 +280,121 @@ def test_paste_tree(self): self.tree.show() self.assertEqual( self.tree._reader, - u'''Hárry + """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.assertNotIn("jill", self.tree.nodes.keys()) + self.assertNotIn("mark", self.tree.nodes.keys()) self.tree.show() self.assertEqual( self.tree._reader, - u'''Hárry + """Hárry ├── Bill │ └── George └── Jane └── Diane -''' +""", ) def test_merge(self): # merge on empty initial tree - t1 = Tree(identifier='t1') + 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 + 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) + 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 + 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) + 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 + 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 + 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 + 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 @@ -381,31 +402,38 @@ def test_paste(self): ├── 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') + 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.') + 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 + 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 @@ -413,20 +441,24 @@ def test_paste(self): ├── C └── D └── D1 -''') +""", + ) # paste empty new_tree (under root) t1 = self.get_t1() - t2 = Tree(identifier='t2') - t1.paste(nid='r', new_tree=t2) + 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 + 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"]: @@ -443,11 +475,11 @@ def test_subtree(self): def test_remove_subtree(self): subtree_shallow = self.tree.remove_subtree("jane") - self.assertEqual("jane" not in self.tree.is_branch(u"hárry"), True) + 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(u"hárry") + self.tree.remove_subtree("hárry") self.assertIsNone(self.tree.root) self.assertEqual(len(self.tree.nodes.keys()), 0) @@ -458,15 +490,14 @@ def test_to_json(self): def test_siblings(self): self.assertEqual(len(self.tree.siblings("hárry")) == 0, True) - self.assertEqual(self.tree.siblings("jane")[0].identifier == "bill", - 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.tree.create_node("Jill", "jill", parent="jane", data=Flower("white")) self.assertEqual(self.tree["jill"].data.color, "white") self.tree.remove_node("jill") @@ -481,6 +512,7 @@ def test_show_data_property(self): 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: @@ -488,12 +520,12 @@ def __init__(self, color): sys.stdout = sys.__stdout__ # stops from printing to console def test_level(self): - self.assertEqual(self.tree.level('hárry'), 0) + 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) + 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) @@ -514,7 +546,7 @@ def test_print_backend(self): def test_show(self): if sys.version_info[0] < 3: reload(sys) - sys.setdefaultencoding('utf-8') + sys.setdefaultencoding("utf-8") sys.stdout = open(os.devnull, "w") # stops from printing to console try: @@ -535,8 +567,8 @@ def test_all_nodes_itr(self): 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)) + 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) @@ -550,22 +582,26 @@ def test_filter_nodes(self): 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)) + 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.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') + 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') + tree.move_node("b", "d") except LoopError: pass @@ -574,33 +610,33 @@ def test_modify_node_identifier_directly_failed(self): tree.create_node("Harry", "harry") tree.create_node("Jane", "jane", parent="harry") n = tree.get_node("jane") - self.assertTrue(n.identifier == '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') + 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') + self.assertTrue(n.identifier == "jane") # Success to modify - tree.update_node(n.identifier, identifier='xyz') + tree.update_node(n.identifier, identifier="xyz") self.assertTrue(tree.get_node("jane") is None) - self.assertTrue(tree.get_node("xyz").identifier == 'xyz') + 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') + 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): @@ -624,48 +660,53 @@ def test_shallow_copy_hermetic_pointers(self): # └── Diane # └── Bill # └── George - tree2 = self.tree.subtree(nid='jane', identifier='tree 2') + 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': u"hárry", 'tree 2': None}) - self.assertEqual(dict(self.tree['jane']._successors), {'tree 1': ['diane'], 'tree 2': ['diane']}) + 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")) + 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') + t1.create_node(identifier="A") t2 = Tree() - t2.create_node(identifier='A') - t2.create_node(identifier='B', parent='A') + t2.create_node(identifier="A") + t2.create_node(identifier="B", parent="A") with self.assertRaises(ValueError) as e: - t1.paste('A', t2) + 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') + n1 = t1.create_node(identifier="A") t2 = Tree() - n2 = t2.create_node(identifier='B') + n2 = t2.create_node(identifier="B") t3 = Tree() - n3 = t3.create_node(identifier='C') + n3 = t3.create_node(identifier="C") t1.paste(n1.identifier, t2) - self.assertEqual(t1.to_dict(), {'A': {'children': ['B']}}) + 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.to_dict(), {"A": {"children": ["B", "C"]}}) self.assertEqual(t1.level(n1.identifier), 0) self.assertEqual(t1.level(n2.identifier), 1) @@ -675,10 +716,10 @@ 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') + 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') + self.assertEqual(t.root, "root-B") diff --git a/treelib/exceptions.py b/treelib/exceptions.py index a9ca429..fc955c5 100644 --- a/treelib/exceptions.py +++ b/treelib/exceptions.py @@ -1,25 +1,30 @@ 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 @@ -28,6 +33,7 @@ class LinkPastRootNodeError(Exception): Exception throwed in Tree.link_past_node() if one attempts to "link past" the root node of a tree. """ + pass @@ -40,4 +46,5 @@ class LoopError(Exception): Exception thrown if trying to move node B to node A's position while A is B's ancestor. """ + pass diff --git a/treelib/misc.py b/treelib/misc.py index b176e4a..37ab7f5 100644 --- a/treelib/misc.py +++ b/treelib/misc.py @@ -31,10 +31,15 @@ def real_deco(func): @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 + 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 diff --git a/treelib/node.py b/treelib/node.py index 6accae1..958c734 100644 --- a/treelib/node.py +++ b/treelib/node.py @@ -86,10 +86,10 @@ def _set_identifier(self, nid): self._identifier = nid @property - @deprecated(alias='node.predecessor') + @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 + 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(): @@ -97,16 +97,16 @@ def bpointer(self): return self._predecessor[self._initial_tree_id] @bpointer.setter - @deprecated(alias='node.set_predecessor') + @deprecated(alias="node.set_predecessor") def bpointer(self, value): self.set_predecessor(value, self._initial_tree_id) - @deprecated(alias='node.set_predecessor') + @deprecated(alias="node.set_predecessor") def update_bpointer(self, nid): self.set_predecessor(nid, self._initial_tree_id) @property - @deprecated(alias='node.successors') + @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 @@ -117,11 +117,11 @@ def fpointer(self): return self._successors[self._initial_tree_id] @fpointer.setter - @deprecated(alias='node.update_successors') + @deprecated(alias="node.update_successors") def fpointer(self, value): self.set_successors(value, tree_id=self._initial_tree_id) - @deprecated(alias='node.update_successors') + @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) @@ -148,10 +148,10 @@ def successors(self, 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) + "NoneType": lambda x: list(), + "list": lambda x: x, + "dict": lambda x: list(x.keys()), + "set": lambda x: list(x), } t = value.__class__.__name__ @@ -159,7 +159,7 @@ def set_successors(self, value, tree_id=None): f_setter = setter_lookup[t] self._successors[tree_id] = f_setter(value) else: - raise NotImplementedError('Unsupported value type %s' % t) + raise NotImplementedError("Unsupported value type %s" % t) def update_successors(self, nid, mode=ADD, replace=None, tree_id=None): """ @@ -176,7 +176,7 @@ 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) + warn("Nid %s wasn't present in fpointer" % nid) def _manipulator_insert(): warn("WARNING: INSERT is deprecated to ADD mode") @@ -191,14 +191,14 @@ def _manipulator_replace(): self.successors(tree_id)[ind] = replace manipulator_lookup = { - self.ADD: '_manipulator_append', - self.DELETE: '_manipulator_delete', - self.INSERT: '_manipulator_insert', - self.REPLACE: '_manipulator_replace' + 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)) + raise NotImplementedError("Unsupported node updating mode %s" % str(mode)) f_name = manipulator_lookup.get(mode) f = locals()[f_name] diff --git a/treelib/plugins.py b/treelib/plugins.py index ff54b11..0736c73 100644 --- a/treelib/plugins.py +++ b/treelib/plugins.py @@ -29,7 +29,7 @@ from .misc import deprecated -@deprecated(alias='tree.to_graphviz()') -def export_to_dot(tree, filename=None, shape='circle', graph='digraph'): +@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) diff --git a/treelib/tree.py b/treelib/tree.py index 3a64779..285b8c0 100644 --- a/treelib/tree.py +++ b/treelib/tree.py @@ -45,10 +45,17 @@ except ImportError: from io import StringIO -from .exceptions import * +from .exceptions import ( + NodeIDAbsentError, + DuplicatedNodeIdError, + MultipleRootError, + InvalidLevelNumber, + LinkPastRootNodeError, + LoopError, +) from .node import Node -__author__ = 'chenxm' +__author__ = "chenxm" @python_2_unicode_compatible @@ -110,7 +117,9 @@ def _clone(self, identifier=None, with_tree=False, deep=False): >>> subtree.tree_description "smart tree" """ - return self.__class__(identifier=identifier, tree=self if with_tree else None, deep=deep) + return self.__class__( + identifier=identifier, tree=self if with_tree else None, deep=deep + ) @property def identifier(self): @@ -138,14 +147,23 @@ def __str__(self): self._reader = "" def write(line): - self._reader += line.decode('utf-8') + "\n" + self._reader += line.decode("utf-8") + "\n" self.__print_backend(func=write) return self._reader - def __print_backend(self, nid=None, level=ROOT, idhidden=True, filter=None, - key=None, reverse=False, line_type='ascii-ex', - data_property=None, func=print): + def __print_backend( + self, + nid=None, + level=ROOT, + idhidden=True, + filter=None, + key=None, + reverse=False, + line_type="ascii-ex", + data_property=None, + func=print, + ): """ Another implementation of printing tree using Stack Print tree structure in hierarchy style. @@ -173,44 +191,55 @@ def __print_backend(self, nid=None, level=ROOT, idhidden=True, filter=None, # Factory for proper get_label() function if data_property: if idhidden: + def get_label(node): return getattr(node.data, data_property) + else: + def get_label(node): - return "%s[%s]" % (getattr(node.data, data_property), node.identifier) + return "%s[%s]" % ( + getattr(node.data, data_property), + node.identifier, + ) + else: if idhidden: + def get_label(node): return node.tag + else: + def get_label(node): return "%s[%s]" % (node.tag, node.identifier) # legacy ordering if key is None: + def key(node): return node # iter with func - for pre, node in self.__get(nid, level, filter, key, reverse, - line_type): + for pre, node in self.__get(nid, level, filter, key, reverse, line_type): label = get_label(node) - func('{0}{1}'.format(pre, label).encode('utf-8')) + func("{0}{1}".format(pre, label).encode("utf-8")) def __get(self, nid, level, filter_, key, reverse, line_type): # default filter if filter_ is None: + def filter_(node): return True # render characters dt = { - 'ascii': ('|', '|-- ', '+-- '), - 'ascii-ex': ('\u2502', '\u251c\u2500\u2500 ', '\u2514\u2500\u2500 '), - 'ascii-exr': ('\u2502', '\u251c\u2500\u2500 ', '\u2570\u2500\u2500 '), - 'ascii-em': ('\u2551', '\u2560\u2550\u2550 ', '\u255a\u2550\u2550 '), - 'ascii-emv': ('\u2551', '\u255f\u2500\u2500 ', '\u2559\u2500\u2500 '), - 'ascii-emh': ('\u2502', '\u255e\u2550\u2550 ', '\u2558\u2550\u2550 '), + "ascii": ("|", "|-- ", "+-- "), + "ascii-ex": ("\u2502", "\u251c\u2500\u2500 ", "\u2514\u2500\u2500 "), + "ascii-exr": ("\u2502", "\u251c\u2500\u2500 ", "\u2570\u2500\u2500 "), + "ascii-em": ("\u2551", "\u2560\u2550\u2550 ", "\u255a\u2550\u2550 "), + "ascii-emv": ("\u2551", "\u255f\u2500\u2500 ", "\u2559\u2500\u2500 "), + "ascii-emh": ("\u2502", "\u255e\u2550\u2550 ", "\u2558\u2550\u2550 "), }[line_type] return self.__get_iter(nid, level, filter_, key, reverse, dt, []) @@ -224,13 +253,19 @@ def __get_iter(self, nid, level, filter_, key, reverse, dt, is_last): if level == self.ROOT: yield "", node else: - leading = ''.join(map(lambda x: dt_vertical_line + ' ' * 3 - if not x else ' ' * 4, is_last[0:-1])) + leading = "".join( + map( + lambda x: dt_vertical_line + " " * 3 if not x else " " * 4, + is_last[0:-1], + ) + ) lasting = dt_line_corner if is_last[-1] else dt_line_box yield leading + lasting, node if filter_(node) and node.expanded: - children = [self[i] for i in node.successors(self._identifier) if filter_(self[i])] + children = [ + self[i] for i in node.successors(self._identifier) if filter_(self[i]) + ] idxlast = len(children) - 1 if key: children.sort(key=key, reverse=reverse) @@ -239,8 +274,9 @@ def __get_iter(self, nid, level, filter_, key, reverse, dt, is_last): level += 1 for idx, child in enumerate(children): is_last.append(idx == idxlast) - for item in self.__get_iter(child.identifier, level, filter_, - key, reverse, dt, is_last): + for item in self.__get_iter( + child.identifier, level, filter_, key, reverse, dt, is_last + ): yield item is_last.pop() @@ -262,14 +298,15 @@ def add_node(self, node, parent=None): """ if not isinstance(node, self.node_class): raise OSError( - "First parameter must be object of {}".format(self.node_class)) + "First parameter must be object of {}".format(self.node_class) + ) if node.identifier in self._nodes: - raise DuplicatedNodeIdError("Can't create node " - "with ID '%s'" % node.identifier) + raise DuplicatedNodeIdError( + "Can't create node " "with ID '%s'" % node.identifier + ) - pid = parent.identifier if isinstance( - parent, self.node_class) else parent + pid = parent.identifier if isinstance(parent, self.node_class) else parent if pid is None: if self.root is not None: @@ -277,8 +314,7 @@ def add_node(self, node, parent=None): else: self.root = node.identifier elif not self.contains(pid): - raise NodeIDAbsentError("Parent node '%s' " - "is not in the tree" % pid) + raise NodeIDAbsentError("Parent node '%s' " "is not in the tree" % pid) self._nodes.update({node.identifier: node}) self.__update_fpointer(pid, node.identifier, self.node_class.ADD) @@ -313,8 +349,11 @@ def ancestor(self, nid, level=None): elif nid == self.root: return self[nid] elif level >= self.level(descendant.identifier): - raise InvalidLevelNumber("Descendant level (level %s) must be greater \ - than its ancestor's level (level %s)" % (str(self.level(descendant.identifier)), level)) + raise InvalidLevelNumber( + "Descendant level (level %s) must be greater \ + than its ancestor's level (level %s)" + % (str(self.level(descendant.identifier)), level) + ) while ascendant is not None: if ascendant_level == level: @@ -371,8 +410,9 @@ def depth(self, node=None): ret = self.level(nid) return ret - def expand_tree(self, nid=None, mode=DEPTH, filter=None, key=None, - reverse=False, sorting=True): + def expand_tree( + self, nid=None, mode=DEPTH, filter=None, key=None, reverse=False, sorting=True + ): """ Python generator to traverse the tree (or a subtree) with optional node filtering and sorting. @@ -402,14 +442,21 @@ def expand_tree(self, nid=None, mode=DEPTH, filter=None, key=None, filter = (lambda x: True) if (filter is None) else filter if filter(self[nid]): yield nid - queue = [self[i] for i in self[nid].successors(self._identifier) if filter(self[i])] + queue = [ + self[i] + for i in self[nid].successors(self._identifier) + if filter(self[i]) + ] if mode in [self.DEPTH, self.WIDTH]: if sorting: queue.sort(key=key, reverse=reverse) while queue: yield queue[0].identifier - expansion = [self[i] for i in queue[0].successors(self._identifier) - if filter(self[i])] + expansion = [ + self[i] + for i in queue[0].successors(self._identifier) + if filter(self[i]) + ] if sorting: expansion.sort(key=key, reverse=reverse) if mode is self.DEPTH: @@ -424,8 +471,11 @@ def expand_tree(self, nid=None, mode=DEPTH, filter=None, key=None, stack = stack_bw = queue direction = False while stack: - expansion = [self[i] for i in stack[0].successors(self._identifier) - if filter(self[i])] + expansion = [ + self[i] + for i in stack[0].successors(self._identifier) + if filter(self[i]) + ] yield stack.pop(0).identifier if direction: expansion.reverse() @@ -437,8 +487,7 @@ def expand_tree(self, nid=None, mode=DEPTH, filter=None, key=None, stack = stack_fw if direction else stack_bw else: - raise ValueError( - "Traversal mode '{}' is not supported".format(mode)) + raise ValueError("Traversal mode '{}' is not supported".format(mode)) def filter_nodes(self, func): """ @@ -512,8 +561,9 @@ def link_past_node(self, nid): if not self.contains(nid): raise NodeIDAbsentError("Node '%s' is not in the tree" % nid) if self.root == nid: - raise LinkPastRootNodeError("Cannot link past the root node, " - "delete it with remove_node()") + raise LinkPastRootNodeError( + "Cannot link past the root node, " "delete it with remove_node()" + ) # Get the parent of the node we are linking past parent = self[self[nid].predecessor(self._identifier)] # Set the children of the node to the parent @@ -633,7 +683,7 @@ def paste(self, nid, new_tree, deep=False): set_joint = set(new_tree._nodes) & set(self._nodes) # joint keys if set_joint: - raise ValueError('Duplicated nodes %s exists.' % list(map(text, set_joint))) + raise ValueError("Duplicated nodes %s exists." % list(map(text, set_joint))) for cid, node in iteritems(new_tree.nodes): if deep: @@ -686,8 +736,7 @@ def remove_node(self, identifier): Return the number of removed nodes. """ if not self.contains(identifier): - raise NodeIDAbsentError("Node '%s' " - "is not in the tree" % identifier) + raise NodeIDAbsentError("Node '%s' " "is not in the tree" % identifier) parent = self[identifier].predecessor(self._identifier) @@ -772,24 +821,58 @@ def rsearch(self, nid, filter=None): if filter(self[current]): yield current # subtree() hasn't update the bpointer - current = self[current].predecessor(self._identifier) if self.root != current else None - - def save2file(self, filename, nid=None, level=ROOT, idhidden=True, - filter=None, key=None, reverse=False, line_type='ascii-ex', data_property=None): + current = ( + self[current].predecessor(self._identifier) + if self.root != current + else None + ) + + def save2file( + self, + filename, + nid=None, + level=ROOT, + idhidden=True, + filter=None, + key=None, + reverse=False, + line_type="ascii-ex", + data_property=None, + ): """ Save the tree into file for offline analysis. """ def _write_line(line, f): - f.write(line + b'\n') - - def handler(x): return _write_line(x, open(filename, 'ab')) - - self.__print_backend(nid, level, idhidden, filter, - key, reverse, line_type, data_property, func=handler) - - def show(self, nid=None, level=ROOT, idhidden=True, filter=None, - key=None, reverse=False, line_type='ascii-ex', data_property=None, stdout=True): + f.write(line + b"\n") + + def handler(x): + return _write_line(x, open(filename, "ab")) + + self.__print_backend( + nid, + level, + idhidden, + filter, + key, + reverse, + line_type, + data_property, + func=handler, + ) + + def show( + self, + nid=None, + level=ROOT, + idhidden=True, + filter=None, + key=None, + reverse=False, + line_type="ascii-ex", + data_property=None, + stdout=True, + ): """ Print the tree structure in hierarchy style. @@ -815,13 +898,22 @@ def show(self, nid=None, level=ROOT, idhidden=True, filter=None, self._reader = "" def write(line): - self._reader += line.decode('utf-8') + "\n" + self._reader += line.decode("utf-8") + "\n" try: - self.__print_backend(nid, level, idhidden, filter, - key, reverse, line_type, data_property, func=write) + self.__print_backend( + nid, + level, + idhidden, + filter, + key, + reverse, + line_type, + data_property, + func=write, + ) except NodeIDAbsentError: - print('Tree is empty') + print("Tree is empty") if stdout: print(self._reader) @@ -838,7 +930,9 @@ def siblings(self, nid): if nid != self.root: pid = self[nid].predecessor(self._identifier) - siblings = [self[i] for i in self[pid].successors(self._identifier) if i != nid] + siblings = [ + self[i] for i in self[pid].successors(self._identifier) if i != nid + ] return siblings @@ -858,10 +952,17 @@ def size(self, level=None): else: try: level = int(level) - return len([node for node in self.all_nodes_itr() if self.level(node.identifier) == level]) + return len( + [ + node + for node in self.all_nodes_itr() + if self.level(node.identifier) == level + ] + ) except: raise TypeError( - "level should be an integer instead of '%s'" % type(level)) + "level should be an integer instead of '%s'" % type(level) + ) def subtree(self, nid, identifier=None): """ @@ -904,7 +1005,7 @@ def update_node(self, nid, **attrs): """ cn = self[nid] for attr, val in iteritems(attrs): - if attr == 'identifier': + if attr == "identifier": # Updating node id meets following contraints: # * Update node identifier property # * Update parent's followers @@ -912,13 +1013,15 @@ def update_node(self, nid, **attrs): # * Update tree registration of var _nodes # * Update tree root if necessary cn = self._nodes.pop(nid) - setattr(cn, 'identifier', val) + setattr(cn, "identifier", val) self._nodes[val] = cn if cn.predecessor(self._identifier) is not None: self[cn.predecessor(self._identifier)].update_successors( - nid, mode=self.node_class.REPLACE, replace=val, - tree_id=self._identifier + nid, + mode=self.node_class.REPLACE, + replace=val, + tree_id=self._identifier, ) for fp in cn.successors(self._identifier): @@ -946,17 +1049,21 @@ def to_dict(self, nid=None, key=None, sort=True, reverse=False, with_data=False) for elem in queue: tree_dict[ntag]["children"].append( - self.to_dict(elem.identifier, with_data=with_data, sort=sort, reverse=reverse)) + self.to_dict( + elem.identifier, with_data=with_data, sort=sort, reverse=reverse + ) + ) if len(tree_dict[ntag]["children"]) == 0: - tree_dict = self[nid].tag if not with_data else \ - {ntag: {"data": self[nid].data}} + tree_dict = ( + self[nid].tag if not with_data else {ntag: {"data": self[nid].data}} + ) return tree_dict def to_json(self, with_data=False, sort=True, reverse=False): """To format the tree in JSON format.""" return json.dumps(self.to_dict(with_data=with_data, sort=sort, reverse=reverse)) - def to_graphviz(self, filename=None, shape='circle', graph='digraph', + def to_graphviz(self, filename=None, shape="circle", graph="digraph", filter=None, key=None, reverse=False, sorting=True): """Exports the tree in the dot format of the graphviz software""" nodes, connections = [], [] @@ -965,8 +1072,7 @@ def to_graphviz(self, filename=None, shape='circle', graph='digraph', for n in self.expand_tree(mode=self.WIDTH, filter=filter, key=key, reverse=reverse, sorting=sorting): nid = self[n].identifier - state = '"{0}" [label="{1}", shape={2}]'.format( - nid, self[n].tag, shape) + state = '"{0}" [label="{1}", shape={2}]'.format(nid, self[n].tag, shape) nodes.append(state) for c in self.children(nid): @@ -977,21 +1083,21 @@ def to_graphviz(self, filename=None, shape='circle', graph='digraph', # write nodes and connections to dot format is_plain_file = filename is not None if is_plain_file: - f = codecs.open(filename, 'w', 'utf-8') + f = codecs.open(filename, "w", "utf-8") else: f = StringIO() - f.write(graph + ' tree {\n') + f.write(graph + " tree {\n") for n in nodes: - f.write('\t' + n + '\n') + f.write("\t" + n + "\n") if len(connections) > 0: - f.write('\n') + f.write("\n") for c in connections: - f.write('\t' + c + '\n') + f.write("\t" + c + "\n") - f.write('}') + f.write("}") if not is_plain_file: print(f.getvalue())