Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

added IndentedTextImporter, compatible docstrings, & nose tests #155

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions anytree/importer/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
"""Importer."""

from .dictimporter import DictImporter # noqa
from .indentedtextimporter import IndentedTextImporter
from .indentedtextimporter import IndentedTextImporterError
from .jsonimporter import JsonImporter # noqa
91 changes: 91 additions & 0 deletions anytree/importer/indentedtextimporter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# -*- coding: utf-8 -*-
from anytree import Node


class IndentedTextImporter(object):

def __init__(self, rootname="root"):
r"""
Import Tree from indented text.

Every line of text is converted to an instance of Node.
The text of the lines establish the names of the nodes.
Indentation establishes the hierarchy between the nodes.

White space must be all spaces, and what constitutes
indentation must be consistent -- That is, if you use x2
spaces to establish 1 level of indentation, then all
indentations much be x2 spaces. The first found indentation
sets the model for all further indentation.

(If you wish to use this with tabs, simply replace the input
text string's leading tabs with spaces, before use.
This may be as simple as s.replace("\t", " "), if the only
tabs used are used for indentation.)

The name for a root node must be supplied;
Every line starting at column 0 is a child of this root node.

Keyword Args:
rootname: name for the root node (invisible)

>>> from anytree.importer import IndentedTextImporter
>>> from anytree import RenderTree
>>> importer = IndentedTextImporter("root")
>>> data = '''
... sub0
... sub0A
... sub0B
... sub1
... '''
>>> root = importer.import_(data)
>>> print(RenderTree(root))
Node('/root')
├── Node('/root/sub0')
│ ├── Node('/root/sub0/sub0A')
│ └── Node('/root/sub0/sub0B')
└── Node('/root/sub1')
"""
self.rootname = rootname

def import_(self, text):
"""Import tree from `text`."""
expected_indentation = None
root = Node(self.rootname) # node implied at "column -(INDENTx1)"
n = None # last node's indentation level
parents_at_levels = [root] # parents at indentation levels
for i, line in enumerate(text.splitlines()):
sp = len(line) - len(line.lstrip(" "))
name = line[sp:]
if not name: # blank line
continue
if sp > 0 and expected_indentation is None: # FIRST indent
expected_indentation = sp # imprint from first indent
n = 0 # last indentation was 0
if expected_indentation is None and sp == 0:
node = Node(name, parent=root)
if len(parents_at_levels) == 1:
parents_at_levels.append(node) # first time
elif len(parents_at_levels) == 2:
parents_at_levels[-1] = node # still no expectation
elif expected_indentation is None:
raise IndentedTextImporterError("bad indent at line", i)
elif sp == n+expected_indentation:
node = Node(name, parent=parents_at_levels[-1])
parents_at_levels.append(node)
elif sp == n:
node = Node(name, parent=parents_at_levels[-2])
parents_at_levels[-1] = node # replace prior end
elif (sp < n) and (sp % expected_indentation == 0):
prior_levels = n // expected_indentation
levels = sp // expected_indentation
node = Node(name, parent=parents_at_levels[levels])
parents_at_levels[levels+1:] = [node] # replace dismissed
else:
raise IndentedTextImporterError("bad indent at line", i)
n = sp
return root


class IndentedTextImporterError(RuntimeError):
"""IndentedTextImporter Error."""
107 changes: 107 additions & 0 deletions tests/test_indentedtextimporter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# -*- coding: utf-8 -*-
from anytree.importer import IndentedTextImporter
from anytree.importer import IndentedTextImporterError

docstring_sample = """
sub0
sub0A
sub0B
sub1
"""[1:-1]

check_docstring_sample = ["root", 0, "sub0", 0, "sub0A", -1, 1, "sub0B", -1, -1,
1, "sub1"]


faulty_indent = """
sub0
sub0A
sub0B
sub1
"""[1:-1]


early_bad_indent = """
sub0
sub1
"""[1:-1]


large_example = """
foo
bar
baz
this line has a lot of text on it
so does this one; lots of text to go around here
what if I embed a / in here
these/should/still/work/
/with/many////slashes

and there was a blank line in here too,

and another blank line,
and indentation afterwards
how is it?
"""[1:-1]


check_large_example = ["root", 0, "foo", 0, "bar", 0, "baz", -1, -1, 1,
"this line has a lot of text on it", 0,
"so does this one; lots of text to go around here",
-1, -1, 2, "what if I embed a / in here", 0,
"these/should/still/work/", 0, "/with/many////slashes",
-1, -1, -1, -1, 1,
"and there was a blank line in here too,", 0,
"and another blank line,", -1, 1,
"and indentation afterwards", -1, -1, 2, "how is it?"]


def check(node, instructions):
for cmd in instructions:
if cmd == -1: # "go up"
node = node.parent
elif isinstance(cmd, int): # "go to numbered child"
node = node.children[cmd]
else:
if cmd != node.name: # "verify that this is the text"
raise ValueError("unexpected value located", cmd, node.name)


def test_importer():
"""IndentedTextImporter test"""
importer = IndentedTextImporter()
root = importer.import_(docstring_sample)
check(root, check_docstring_sample)


def test_faulty_indent():
"""IndentedTextImporter: bad indentation test"""
importer = IndentedTextImporter()
try:
root = importer.import_(faulty_indent)
except IndentedTextImporterError as e:
(err_name, err_lineno) = e.args
if err_name == "bad indent at line" and err_lineno == 2:
pass
else:
raise ValueError("expected bad indent error on line 2")


def test_early_bad_indent():
"""IndentedTextImporter: bad indentation test"""
importer = IndentedTextImporter()
try:
root = importer.import_(early_bad_indent)
except IndentedTextImporterError as e:
(err_name, err_lineno) = e.args
if err_name == "bad indent at line" and err_lineno == 0:
pass
else:
raise ValueError("expected bad indent error on line 0")


def test_large_example():
"""IndentedTextImporter: bad indentation test"""
importer = IndentedTextImporter()
root = importer.import_(large_example)
check(root, check_large_example)