From 8ff5054cc85bd30e046a51dd89dfb8830c038435 Mon Sep 17 00:00:00 2001 From: Lion Kimbro Date: Sat, 23 Jan 2021 17:46:34 -0800 Subject: [PATCH 1/4] added IndentedTextImporter, compatible docstrings, & nose tests --- anytree/importer/__init__.py | 3 + anytree/importer/indentedtextimporter.py | 92 ++++++++++++++++++ tests/test_indentedtextimporter.py | 113 +++++++++++++++++++++++ 3 files changed, 208 insertions(+) create mode 100644 anytree/importer/indentedtextimporter.py create mode 100644 tests/test_indentedtextimporter.py diff --git a/anytree/importer/__init__.py b/anytree/importer/__init__.py index 15a3f70..9d23d85 100644 --- a/anytree/importer/__init__.py +++ b/anytree/importer/__init__.py @@ -2,3 +2,6 @@ from .dictimporter import DictImporter # noqa from .jsonimporter import JsonImporter # noqa +from .indentedtextimporter import IndentedTextImporter +from .indentedtextimporter import IndentedTextImporterError + diff --git a/anytree/importer/indentedtextimporter.py b/anytree/importer/indentedtextimporter.py new file mode 100644 index 0000000..8def7e3 --- /dev/null +++ b/anytree/importer/indentedtextimporter.py @@ -0,0 +1,92 @@ +# -*- coding: utf-8 -*- +from anytree import Node + + +class IndentedTextImporter(object): + + def __init__(self, rootname="root"): + u""" + 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.""" + diff --git a/tests/test_indentedtextimporter.py b/tests/test_indentedtextimporter.py new file mode 100644 index 0000000..ad91391 --- /dev/null +++ b/tests/test_indentedtextimporter.py @@ -0,0 +1,113 @@ +# -*- coding: utf-8 -*- +from nose.tools import eq_ + +from anytree import Node +from anytree import RenderTree +from anytree.importer import IndentedTextImporter, IndentedTextImporterError +from helper import eq_str + + +docstring_sample = """ +sub0 + sub0A + sub0B +sub1 +"""[1:-1] + +docstring_sample_expected = """ +Node('/root') +├── Node('/root/sub0') +│ ├── Node('/root/sub0/sub0A') +│ └── Node('/root/sub0/sub0B') +└── Node('/root/sub1') +"""[1:-1] + +faulty_indent = """ +sub0 + sub0A + sub0B +sub1 +"""[1:-1] + +early_bad_indent = """ + + sub0 - (note: only whitespace in line above) + 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/ + even/though/we're/filling/them/with////slashes// + +and there was a blank line in here too, + + including blank lines with white space, + and indentation afterwards +how is it? +"""[1:-1] + +large_example_expected = """ +Node('/root') +├── Node('/root/foo') +│ ├── Node('/root/foo/bar') +│ │ └── Node('/root/foo/bar/baz') +│ ├── Node('/root/foo/this line has a lot of text on it') +│ │ └── Node('/root/foo/this line has a lot of text on it/so does this one; lots of text to go around here') +│ └── Node('/root/foo/what if I embed a / in here') +│ └── Node('/root/foo/what if I embed a / in here/these/should/still/work/') +│ └── Node("/root/foo/what if I embed a / in here/these/should/still/work//even/though/we're/filling/them/with////slashes//") +├── Node('/root/and there was a blank line in here too,') +│ ├── Node('/root/and there was a blank line in here too,/including blank lines with white space,') +│ └── Node('/root/and there was a blank line in here too,/and indentation afterwards') +└── Node('/root/how is it?') +"""[1:-1] + + +def test_importer(): + """IndentedTextImporter test""" + importer = IndentedTextImporter() + root = importer.import_(docstring_sample) + r = RenderTree(root) + eq_str(str(r), docstring_sample_expected) + + +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 == 1: + pass + else: + raise ValueError("expected bad indent error on line 1") + + +def test_large_example(): + """IndentedTextImporter: bad indentation test""" + importer = IndentedTextImporter() + root = importer.import_(large_example) + r = RenderTree(root) + eq_str(str(r), large_example_expected) + + From d56ba624b6f13caf54537480eb44b5900a29688c Mon Sep 17 00:00:00 2001 From: Lion Kimbro Date: Sat, 23 Jan 2021 19:11:58 -0800 Subject: [PATCH 2/4] whitespace & test fixes The tests are now shorter, more direct, and do not rely on the presentation system. --- anytree/importer/__init__.py | 1 - anytree/importer/indentedtextimporter.py | 11 +++-- tests/test_indentedtextimporter.py | 57 ++++++++++++------------ 3 files changed, 34 insertions(+), 35 deletions(-) diff --git a/anytree/importer/__init__.py b/anytree/importer/__init__.py index 9d23d85..9245276 100644 --- a/anytree/importer/__init__.py +++ b/anytree/importer/__init__.py @@ -4,4 +4,3 @@ from .jsonimporter import JsonImporter # noqa from .indentedtextimporter import IndentedTextImporter from .indentedtextimporter import IndentedTextImporterError - diff --git a/anytree/importer/indentedtextimporter.py b/anytree/importer/indentedtextimporter.py index 8def7e3..12e8769 100644 --- a/anytree/importer/indentedtextimporter.py +++ b/anytree/importer/indentedtextimporter.py @@ -3,7 +3,7 @@ class IndentedTextImporter(object): - + def __init__(self, rootname="root"): u""" Import Tree from indented text. @@ -11,18 +11,18 @@ def __init__(self, rootname="root"): 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. @@ -47,7 +47,7 @@ def __init__(self, rootname="root"): └── Node('/root/sub1') """ self.rootname = rootname - + def import_(self, text): """Import tree from `text`.""" expected_indentation = None @@ -89,4 +89,3 @@ def import_(self, text): class IndentedTextImporterError(RuntimeError): """IndentedTextImporter Error.""" - diff --git a/tests/test_indentedtextimporter.py b/tests/test_indentedtextimporter.py index ad91391..06cbc79 100644 --- a/tests/test_indentedtextimporter.py +++ b/tests/test_indentedtextimporter.py @@ -14,13 +14,9 @@ sub1 """[1:-1] -docstring_sample_expected = """ -Node('/root') -├── Node('/root/sub0') -│ ├── Node('/root/sub0/sub0A') -│ └── Node('/root/sub0/sub0B') -└── Node('/root/sub1') -"""[1:-1] +check_docstring_sample = ["root", 0, "sub0", 0, "sub0A", -1, 1, "sub0B", -1, -1, + 1, "sub1"] + faulty_indent = """ sub0 @@ -29,12 +25,14 @@ sub1 """[1:-1] + early_bad_indent = """ sub0 - (note: only whitespace in line above) sub1 """[1:-1] + large_example = """ foo bar @@ -43,7 +41,7 @@ so does this one; lots of text to go around here what if I embed a / in here these/should/still/work/ - even/though/we're/filling/them/with////slashes// + /with/many////slashes and there was a blank line in here too, @@ -52,21 +50,27 @@ how is it? """[1:-1] -large_example_expected = """ -Node('/root') -├── Node('/root/foo') -│ ├── Node('/root/foo/bar') -│ │ └── Node('/root/foo/bar/baz') -│ ├── Node('/root/foo/this line has a lot of text on it') -│ │ └── Node('/root/foo/this line has a lot of text on it/so does this one; lots of text to go around here') -│ └── Node('/root/foo/what if I embed a / in here') -│ └── Node('/root/foo/what if I embed a / in here/these/should/still/work/') -│ └── Node("/root/foo/what if I embed a / in here/these/should/still/work//even/though/we're/filling/them/with////slashes//") -├── Node('/root/and there was a blank line in here too,') -│ ├── Node('/root/and there was a blank line in here too,/including blank lines with white space,') -│ └── Node('/root/and there was a blank line in here too,/and indentation afterwards') -└── Node('/root/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, + "including blank lines with white space,", -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(): @@ -74,7 +78,7 @@ def test_importer(): importer = IndentedTextImporter() root = importer.import_(docstring_sample) r = RenderTree(root) - eq_str(str(r), docstring_sample_expected) + check(root, check_docstring_sample) def test_faulty_indent(): @@ -107,7 +111,4 @@ def test_large_example(): """IndentedTextImporter: bad indentation test""" importer = IndentedTextImporter() root = importer.import_(large_example) - r = RenderTree(root) - eq_str(str(r), large_example_expected) - - + check(root, check_large_example) From 1b5da490e0d3f411c80de0ef96b0a9ad53ea23a4 Mon Sep 17 00:00:00 2001 From: Lion Kimbro Date: Sat, 23 Jan 2021 19:29:51 -0800 Subject: [PATCH 3/4] pydocstyle & isort compliance --- anytree/importer/__init__.py | 2 +- anytree/importer/indentedtextimporter.py | 2 +- tests/test_indentedtextimporter.py | 19 +++++++++---------- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/anytree/importer/__init__.py b/anytree/importer/__init__.py index 9245276..095a6c1 100644 --- a/anytree/importer/__init__.py +++ b/anytree/importer/__init__.py @@ -1,6 +1,6 @@ """Importer.""" from .dictimporter import DictImporter # noqa -from .jsonimporter import JsonImporter # noqa from .indentedtextimporter import IndentedTextImporter from .indentedtextimporter import IndentedTextImporterError +from .jsonimporter import JsonImporter # noqa diff --git a/anytree/importer/indentedtextimporter.py b/anytree/importer/indentedtextimporter.py index 12e8769..d0f09e3 100644 --- a/anytree/importer/indentedtextimporter.py +++ b/anytree/importer/indentedtextimporter.py @@ -5,7 +5,7 @@ class IndentedTextImporter(object): def __init__(self, rootname="root"): - u""" + r""" Import Tree from indented text. Every line of text is converted to an instance of Node. diff --git a/tests/test_indentedtextimporter.py b/tests/test_indentedtextimporter.py index 06cbc79..b4ca49f 100644 --- a/tests/test_indentedtextimporter.py +++ b/tests/test_indentedtextimporter.py @@ -1,11 +1,11 @@ # -*- coding: utf-8 -*- +from helper import eq_str from nose.tools import eq_ from anytree import Node from anytree import RenderTree -from anytree.importer import IndentedTextImporter, IndentedTextImporterError -from helper import eq_str - +from anytree.importer import IndentedTextImporter +from anytree.importer import IndentedTextImporterError docstring_sample = """ sub0 @@ -27,8 +27,7 @@ early_bad_indent = """ - - sub0 - (note: only whitespace in line above) + sub0 sub1 """[1:-1] @@ -44,8 +43,8 @@ /with/many////slashes and there was a blank line in here too, - - including blank lines with white space, + + and another blank line, and indentation afterwards how is it? """[1:-1] @@ -58,7 +57,7 @@ "these/should/still/work/", 0, "/with/many////slashes", -1, -1, -1, -1, 1, "and there was a blank line in here too,", 0, - "including blank lines with white space,", -1, 1, + "and another blank line,", -1, 1, "and indentation afterwards", -1, -1, 2, "how is it?"] @@ -101,10 +100,10 @@ def test_early_bad_indent(): 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 == 1: + if err_name == "bad indent at line" and err_lineno == 0: pass else: - raise ValueError("expected bad indent error on line 1") + raise ValueError("expected bad indent error on line 0") def test_large_example(): From 4db02fe127a2e0f16be87a42520500d11de8978f Mon Sep 17 00:00:00 2001 From: Lion Kimbro Date: Sat, 23 Jan 2021 19:45:34 -0800 Subject: [PATCH 4/4] removed unnecessary imports --- tests/test_indentedtextimporter.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/tests/test_indentedtextimporter.py b/tests/test_indentedtextimporter.py index b4ca49f..5eac4ce 100644 --- a/tests/test_indentedtextimporter.py +++ b/tests/test_indentedtextimporter.py @@ -1,9 +1,4 @@ # -*- coding: utf-8 -*- -from helper import eq_str -from nose.tools import eq_ - -from anytree import Node -from anytree import RenderTree from anytree.importer import IndentedTextImporter from anytree.importer import IndentedTextImporterError @@ -76,7 +71,6 @@ def test_importer(): """IndentedTextImporter test""" importer = IndentedTextImporter() root = importer.import_(docstring_sample) - r = RenderTree(root) check(root, check_docstring_sample)