Skip to content

Commit

Permalink
Merge pull request #48 from Sup3rGeo/master
Browse files Browse the repository at this point in the history
Added IndentedStringImporter
  • Loading branch information
c0fec0de committed Jun 24, 2018
2 parents a20b47b + 6028ea2 commit 30dae57
Show file tree
Hide file tree
Showing 5 changed files with 204 additions and 0 deletions.
1 change: 1 addition & 0 deletions anytree/importer/__init__.py
Expand Up @@ -2,3 +2,4 @@

from .dictimporter import DictImporter # noqa
from .jsonimporter import JsonImporter # noqa
from .indentedstringimporter import IndentedStringImporter
106 changes: 106 additions & 0 deletions anytree/importer/indentedstringimporter.py
@@ -0,0 +1,106 @@
# -*- coding: utf-8 -*-
from anytree import AnyNode


def _get_indentation(line):
content = line.lstrip(' ')
# Split string using version without indentation; First item of result is the indentation itself.
indentation_length = len(line.split(content)[0])
return indentation_length, content


class IndentedStringImporter(object):

def __init__(self, nodecls=AnyNode):
u"""
Import Tree from a single string (with all the lines) or list of strings (lines) with indentation.
Every indented line is converted to an instance of `nodecls`. The string (without indentation) found on the lines are set as the respective node name.
This importer do not constrain indented data to have a definite number of whitespaces (multiple of any number). Nodes are considered child of a parent simply if its indentation is bigger than its parent.
This means that the tree can have siblings with different indentations, as long as the siblings indentations are bigger than the respective parent (but not necessarily the same considering each other).
Keyword Args:
nodecls: class used for nodes.
Example using a string list:
>>> from anytree.importer import IndentedStringImporter
>>> from anytree import RenderTree
>>> importer = IndentedStringImporter()
>>> lines = [
... 'Node1',
... 'Node2',
... ' Node3',
... 'Node5',
... ' Node6',
... ' Node7',
... ' Node8',
... ' Node9',
... ' Node10',
... ' Node11',
... ' Node12',
... 'Node13',
...]
>>> root = importer.import_(lines)
>>> print(RenderTree(root))
AnyNode(name='root')
├── AnyNode(name='Node1')
├── AnyNode(name='Node2')
│ └── AnyNode(name='Node3')
├── AnyNode(name='Node5')
│ ├── AnyNode(name='Node6')
│ │ └── AnyNode(name='Node7')
│ ├── AnyNode(name='Node8')
│ │ ├── AnyNode(name='Node9')
│ │ └── AnyNode(name='Node10')
│ ├── AnyNode(name='Node11')
│ └── AnyNode(name='Node12')
└── AnyNode(name='Node13')
Example using a string:
>>> string = "Node1\n Node2\n Node3\n Node4"
>>> root = importer.import_(string)
>>> print(RenderTree(root))
AnyNode(name='root')
└── AnyNode(name='Node1')
├── AnyNode(name='Node2')
└── AnyNode(name='Node3')
└── AnyNode(name='Node4')
"""
self.nodecls = nodecls

def _tree_from_indented_str(self, data):
if isinstance(data, str):
lines = data.splitlines()
else:
lines = data
root = self.nodecls(name="root")
indentations = {}
for line in lines:
current_indentation, name = _get_indentation(line)

if len(indentations) == 0:
parent = root
elif current_indentation not in indentations:
# parent is the next lower indentation
keys = [key for key in indentations.keys() if key < current_indentation]
parent = indentations[max(keys)]
else:
# current line uses the parent of the last line with same indentation and replaces
# it as the last line with this given indentation
parent = indentations[current_indentation].parent

indentations[current_indentation] = self.nodecls(name=name, parent=parent)

# delete all higher indentations
keys = [key for key in indentations.keys() if key > current_indentation]
for key in keys:
indentations.pop(key)
return root

def import_(self, data):
"""Import tree from `data`, which can be a single string or a list of lines."""
return self._tree_from_indented_str(data)
1 change: 1 addition & 0 deletions docs/importer.rst
Expand Up @@ -9,6 +9,7 @@ Available importers:
.. toctree::
importer/dictimporter
importer/jsonimporter
importer/indentedstringimporter

Importer missing? File a request here: Issues_.

Expand Down
4 changes: 4 additions & 0 deletions docs/importer/indentedstringimporter.rst
@@ -0,0 +1,4 @@
Indented String Importer
========================

.. automodule:: anytree.importer.indentedstringimporter
92 changes: 92 additions & 0 deletions tests/test_indentedstringimporter.py
@@ -0,0 +1,92 @@
# -*- coding: utf-8 -*-
from anytree import Node
from anytree import RenderTree
from anytree.importer import IndentedStringImporter


linelist = [
"Node1",
"Node2",
" Node3",
" Node4",
"Node5",
" Node6",
" Node7",
" Node8",
" Node9",
" Node10",
" Node11",
" Node12",
" Node13",
" Node14",
" Node15",
" Node16",
"Node17"
]

expected_anynode = "\n".join([
"AnyNode(name='root')",
"├── AnyNode(name='Node1')",
"├── AnyNode(name='Node2')",
"│ ├── AnyNode(name='Node3')",
"│ └── AnyNode(name='Node4')",
"├── AnyNode(name='Node5')",
"│ ├── AnyNode(name='Node6')",
"│ │ └── AnyNode(name='Node7')",
"│ ├── AnyNode(name='Node8')",
"│ │ ├── AnyNode(name='Node9')",
"│ │ ├── AnyNode(name='Node10')",
"│ │ ├── AnyNode(name='Node11')",
"│ │ └── AnyNode(name='Node12')",
"│ ├── AnyNode(name='Node13')",
"│ ├── AnyNode(name='Node14')",
"│ ├── AnyNode(name='Node15')",
"│ └── AnyNode(name='Node16')",
"└── AnyNode(name='Node17')",
])


def test_indentedstring_importer_string():
"""Indented String Importer with whole string."""
importer = IndentedStringImporter()
data_string = "\n".join(linelist)
root = importer.import_(data_string)
r = RenderTree(root)
assert str(r) == expected_anynode


def test_indentedstring_importer_list():
"""Indented String Importer with line list."""
importer = IndentedStringImporter()
root = importer.import_(linelist)
r = RenderTree(root)
assert str(r) == expected_anynode


def test_indentedstring_importer_node():
"""Indented String Importer using Node class."""
importer = IndentedStringImporter(Node)
data_string = "\n".join(linelist)
root = importer.import_(data_string)
r = RenderTree(root)
expected = "\n".join([
"Node('/root')",
"├── Node('/root/Node1')",
"├── Node('/root/Node2')",
"│ ├── Node('/root/Node2/Node3')",
"│ └── Node('/root/Node2/Node4')",
"├── Node('/root/Node5')",
"│ ├── Node('/root/Node5/Node6')",
"│ │ └── Node('/root/Node5/Node6/Node7')",
"│ ├── Node('/root/Node5/Node8')",
"│ │ ├── Node('/root/Node5/Node8/Node9')",
"│ │ ├── Node('/root/Node5/Node8/Node10')",
"│ │ ├── Node('/root/Node5/Node8/Node11')",
"│ │ └── Node('/root/Node5/Node8/Node12')",
"│ ├── Node('/root/Node5/Node13')",
"│ ├── Node('/root/Node5/Node14')",
"│ ├── Node('/root/Node5/Node15')",
"│ └── Node('/root/Node5/Node16')",
"└── Node('/root/Node17')",
])
assert str(r) == expected

0 comments on commit 30dae57

Please sign in to comment.