From 4e91a0e061fc34235039eef72e3927615f798914 Mon Sep 17 00:00:00 2001 From: Oliver Eyton-Williams Date: Wed, 24 Jan 2024 15:32:12 +0100 Subject: [PATCH] refactor: Chainable -> Node (#122) * feat: construct with string or AST * fix: always construct with AST or string * refactor: Chainable -> ASTExplorer * feat: remove parse() * test: improve test coverage * fix: handle if_bodies on non-If node * test: improve coverage * test: more precise test * test: more demostrations of is_equivalent * refactor: ASTExplorer -> Node --- python/py_helpers.py | 74 ++++----- python/py_helpers.test.py | 305 ++++++++++++++++++++++---------------- 2 files changed, 219 insertions(+), 160 deletions(-) diff --git a/python/py_helpers.py b/python/py_helpers.py index 00d508a..18536ec 100644 --- a/python/py_helpers.py +++ b/python/py_helpers.py @@ -3,16 +3,21 @@ # A chainable class that allows us to call functions on the result of parsing a string -class Chainable: +class Node: # TODO: allow initialization with a string def __init__(self, tree=None): - self.tree = tree + if isinstance(tree, str): + self.tree = ast.parse(tree) + elif isinstance(tree, ast.AST) or tree == None: + self.tree = tree + else: + raise TypeError("Node must be initialized with a string or AST") def __getitem__(self, i): if getattr(self.tree, "__getitem__", False): - return Chainable(self.tree[i]) + return Node(self.tree[i]) else: - return Chainable(self.tree.body[i]) + return Node(self.tree.body[i]) def __len__(self): if getattr(self.tree, "__len__", False): @@ -21,7 +26,7 @@ def __len__(self): return len(self.tree.body) def __eq__(self, other): - if not isinstance(other, Chainable): + if not isinstance(other, Node): return False if self.tree == None: return other.tree == None @@ -33,54 +38,51 @@ def __eq__(self, other): def __repr__(self): if self.tree == None: - return "Chainable:\nNone" - return "Chainable:\n" + ast.dump(self.tree, indent=2) - - def parse(self, string): - return Chainable(ast.parse(string)) + return "Node:\nNone" + return "Node:\n" + ast.dump(self.tree, indent=2) def _has_body(self): return bool(getattr(self.tree, "body", False)) - # "find" functions return a new chainable with the result of the find - # function. In this case, it returns a new chainable with the function + # "find" functions return a new node with the result of the find + # function. In this case, it returns a new node with the function # definition (if it exists) def find_function(self, func): if not self._has_body(): - return Chainable() + return Node() for node in self.tree.body: if isinstance(node, ast.FunctionDef): if node.name == func: - return Chainable(node) - return Chainable() + return Node(node) + return Node() # "has" functions return a boolean indicating whether whatever is being # searched for exists. In this case, it returns True if the variable exists. def has_variable(self, name): - return self.find_variable(name) != Chainable() + return self.find_variable(name) != Node() def find_variable(self, name): if not self._has_body(): - return Chainable() + return Node() for node in self.tree.body: if isinstance(node, ast.Assign): for target in node.targets: if isinstance(target, ast.Name): if target.id == name: - return Chainable(node) - return Chainable() + return Node(node) + return Node() def get_variable(self, name): var = self.find_variable(name) - if var != Chainable(): + if var != Node(): return var.tree.value.value else: return None def has_function(self, name): - return self.find_function(name) != Chainable() + return self.find_function(name) != Node() # Checks the variable, name, is in the current scope and is an integer @@ -97,7 +99,7 @@ def value_is_call(self, name): return call.func.id == name return False - # Takes an string and checks if is equivalent to the chainable's AST. This + # Takes an string and checks if is equivalent to the node's AST. This # is a loose comparison that tries to find out if the code is essentially # the same. For example, the string "True" is not represented by the same # AST as the test in "if True:" (the string could be wrapped in Module, @@ -116,12 +118,12 @@ def is_equivalent(self, target_str): def find_class(self, class_name): if not self._has_body(): - return Chainable() + return Node() for node in self.tree.body: if isinstance(node, ast.ClassDef): if node.name == class_name: - return Chainable(node) - return Chainable() + return Node(node) + return Node() # Find an array of conditions in an if statement @@ -130,7 +132,7 @@ def find_ifs(self): def _find_all(self, ast_type): return [ - Chainable(node) for node in self.tree.body if isinstance(node, ast_type) + Node(node) for node in self.tree.body if isinstance(node, ast_type) ] def find_conditions(self): @@ -140,22 +142,26 @@ def _find_conditions(tree): test = tree.test if self.tree.orelse == []: return [test] - elif isinstance(tree.orelse[0], ast.If): + if isinstance(tree.orelse[0], ast.If): return [test] + _find_conditions(tree.orelse[0]) - else: - return [test, None] - return [Chainable(test) for test in _find_conditions(self.tree)] + return [test, None] + + return [Node(test) for test in _find_conditions(self.tree)] # Find an array of bodies in an elif statement def find_if_bodies(self): def _find_if_bodies(tree): + if not isinstance(tree, ast.If): + return [] if self.tree.orelse == []: return [tree.body] - elif isinstance(tree.orelse[0], ast.If): + if isinstance(tree.orelse[0], ast.If): return [tree.body] + _find_if_bodies(tree.orelse[0]) - else: - return [tree.body] + [tree.orelse] - return [Chainable(body) for body in _find_if_bodies(self.tree)] + return [tree.body] + [tree.orelse] + + return [ + Node(ast.Module(body, [])) for body in _find_if_bodies(self.tree) + ] diff --git a/python/py_helpers.test.py b/python/py_helpers.test.py index e79e88f..1e2f47e 100644 --- a/python/py_helpers.test.py +++ b/python/py_helpers.test.py @@ -1,15 +1,37 @@ import unittest import ast -from py_helpers import Chainable +from py_helpers import Node + + +class TestConstructor(unittest.TestCase): + def test_constructor(self): + node = Node() + + self.assertIsNone(node.tree) + + def test_constructor_with_tree(self): + tree = ast.parse("def foo():\n pass") + node = Node(tree) + + self.assertEqual(node.tree, tree) + + def test_constructor_with_string(self): + with_string = Node("def foo():\n pass") + with_tree = Node(ast.parse("def foo():\n pass")) + + self.assertEqual(with_string, with_tree) + + def test_constructor_with_anything_else(self): + self.assertRaises(TypeError, lambda: Node(1)) class TestVariableHelpers(unittest.TestCase): def test_find_variable_can_handle_all_asts(self): - chainable = Chainable().parse("x = 1") + node = Node("x = 1") # First find_variable, so know that the AST has no body and we can be # sure find_class handles this. - self.assertEqual(chainable.find_variable("x").find_variable("x"), Chainable()) + self.assertEqual(node.find_variable("x").find_variable("x"), Node()) def test_has_local_variable_in_function(self): func_str = """def foo(): @@ -18,18 +40,18 @@ def test_has_local_variable_in_function(self): x = 2 """ - chainable = Chainable().parse(func_str) + node = Node(func_str) - self.assertTrue(chainable.find_function("foo").has_variable("x")) + self.assertTrue(node.find_function("foo").has_variable("x")) def test_has_global_variable(self): globals_str = """a = 1 x = 2 """ - chainable = Chainable().parse(globals_str) + node = Node(globals_str) - self.assertTrue(chainable.has_variable("x")) + self.assertTrue(node.has_variable("x")) def test_does_not_see_local_variables_out_of_scope(self): scopes_str = """def foo(): @@ -37,8 +59,8 @@ def test_does_not_see_local_variables_out_of_scope(self): b = 2 """ - chainable = Chainable().parse(scopes_str) - self.assertFalse(chainable.has_variable("a")) + node = Node(scopes_str) + self.assertFalse(node.has_variable("a")) def test_is_integer(self): two_locals = """ @@ -48,93 +70,95 @@ def foo(): x = 2 y = 3 """ - chainable = Chainable().parse(two_locals) - self.assertTrue(chainable.find_function("foo").find_variable("x").is_integer()) - self.assertFalse(chainable.find_function("foo").find_variable("y").is_integer()) + node = Node(two_locals) + self.assertTrue(node.find_function("foo").find_variable("x").is_integer()) + self.assertFalse(node.find_function("foo").find_variable("y").is_integer()) def test_none_assignment(self): none_str = """ x = None """ - chainable = Chainable().parse(none_str) + node = Node(none_str) - self.assertTrue(chainable.has_variable("x")) - self.assertTrue(chainable.find_variable("x").is_equivalent("x = None")) + self.assertTrue(node.has_variable("x")) + self.assertTrue(node.find_variable("x").is_equivalent("x = None")) def test_local_variable_is_integer_with_string(self): - chainable = Chainable().parse('def foo():\n x = "1"') + node = Node('def foo():\n x = "1"') - self.assertFalse(chainable.find_function("foo").find_variable("x").is_integer()) + self.assertFalse(node.find_function("foo").find_variable("x").is_integer()) def test_variable_has_constant_value(self): - chainable = Chainable().parse('def foo():\n x = "1"') + node = Node('def foo():\n x = "1"') - self.assertEqual(chainable.find_function("foo").get_variable("x"), "1") + self.assertEqual(node.find_function("foo").get_variable("x"), "1") def test_find_variable(self): - chainable = Chainable().parse('def foo():\n x = "1"') + node = Node('def foo():\n x = "1"') self.assertTrue( - chainable.find_function("foo").find_variable("x").is_equivalent('x = "1"'), + node.find_function("foo").find_variable("x").is_equivalent('x = "1"'), ) def test_find_variable_not_found(self): - chainable = Chainable().parse('def foo():\n x = "1"') + node = Node('def foo():\n x = "1"') - self.assertEqual(chainable.find_variable("y").tree, None) + self.assertEqual(node.find_variable("y"), Node()) def test_function_call_assigned_to_variable(self): - chainable = Chainable().parse("def foo():\n x = bar()") + node = Node("def foo():\n x = bar()") self.assertTrue( - chainable.find_function("foo").find_variable("x").value_is_call("bar") + node.find_function("foo").find_variable("x").value_is_call("bar") ) def test_function_call_not_assigned_to_variable(self): - chainable = Chainable().parse("def foo():\n bar()") + node = Node("def foo():\n bar()") - self.assertFalse(chainable.find_function("foo").value_is_call("bar")) + self.assertFalse(node.find_function("foo").value_is_call("bar")) class TestFunctionAndClassHelpers(unittest.TestCase): - def test_find_function_returns_chainable(self): + def test_find_function_returns_node(self): func_str = """def foo(): pass """ - chainable = Chainable().parse(func_str) + node = Node(func_str) - self.assertIsInstance(chainable.find_function("foo"), Chainable) - self.assertIsInstance(chainable.find_function("bar"), Chainable) + self.assertIsInstance(node.find_function("foo"), Node) + self.assertIsInstance(node.find_function("bar"), Node) def test_find_function_can_handle_all_asts(self): - chainable = Chainable().parse("x = 1") + node = Node("x = 1") # First find_variable, so know that the AST has no body and we can be # sure find_function handles this. - self.assertEqual(chainable.find_variable("x").find_function("foo"), Chainable()) + self.assertEqual( + node.find_variable("x").find_function("foo"), Node() + ) - def test_parse_creates_chainable(self): - chainable = Chainable().parse("def foo():\n pass") + def test_parse_creates_node(self): + node = Node("def foo():\n pass") - self.assertIsInstance(chainable.tree, ast.Module) + self.assertIsInstance(node.tree, ast.Module) self.assertEqual( - ast.dump(chainable.tree), ast.dump(ast.parse("def foo():\n pass")) + ast.dump(node.tree), ast.dump(ast.parse("def foo():\n pass")) ) def test_find_function_returns_function_ast(self): - chainable = Chainable().parse("def foo():\n pass") + node = Node("def foo():\n pass") - func = chainable.find_function("foo") + func = node.find_function("foo") self.assertIsInstance(func.tree, ast.FunctionDef) self.assertEqual(func.tree.name, "foo") - def test_find_function_returns_chainable_none(self): - chainable = Chainable().parse("def foo():\n pass") + def test_find_function_returns_node_none(self): + node = Node("def foo():\n pass") - func = chainable.find_function("bar") + func = node.find_function("bar") - self.assertIsInstance(func, Chainable) + self.assertIsInstance(func, Node) self.assertEqual(func.tree, None) def test_nested_function(self): @@ -144,12 +168,12 @@ def bar(): y = 2 """ - chainable = Chainable().parse(nested_str) + node = Node(nested_str) - self.assertTrue(chainable.find_function("foo").has_variable("y")) - self.assertFalse(chainable.find_function("foo").has_variable("x")) + self.assertTrue(node.find_function("foo").has_variable("y")) + self.assertFalse(node.find_function("foo").has_variable("x")) self.assertTrue( - chainable.find_function("foo").find_function("bar").has_variable("x") + node.find_function("foo").find_function("bar").has_variable("x") ) def test_find_class(self): @@ -159,20 +183,20 @@ def __init__(self): pass """ - chainable = Chainable().parse(class_str) + node = Node(class_str) - self.assertIsNotNone(chainable.find_class("Foo")) - self.assertIsInstance(chainable.find_class("Foo"), Chainable) + self.assertIsNotNone(node.find_class("Foo")) + self.assertIsInstance(node.find_class("Foo"), Node) - self.assertIsInstance(chainable.find_class("Bar"), Chainable) - self.assertEqual(chainable.find_class("Bar"), Chainable()) + self.assertIsInstance(node.find_class("Bar"), Node) + self.assertEqual(node.find_class("Bar"), Node()) def test_find_class_can_handle_all_asts(self): - chainable = Chainable().parse("x = 1") + node = Node("x = 1") # First find_variable, so know that the AST has no body and we can be # sure find_class handles this. - self.assertEqual(chainable.find_variable("x").find_class("Foo"), Chainable()) + self.assertEqual(node.find_variable("x").find_class("Foo"), Node()) def test_method_exists(self): class_str = """ @@ -182,14 +206,26 @@ def __init__(self): def bar(self): pass """ - chainable = Chainable().parse(class_str) + node = Node(class_str) - self.assertTrue(chainable.find_class("Foo").has_function("bar")) + self.assertTrue(node.find_class("Foo").has_function("bar")) + + def test_dunder_method_exists(self): + class_str = """ +class Foo: + def __init__(self): + self.x = 1 + def bar(self): + pass +""" + node = Node(class_str) + + self.assertTrue(node.find_class("Foo").has_function("__init__")) def test_not_has_function(self): - chainable = Chainable().parse("def foo():\n pass") + node = Node("def foo():\n pass") - self.assertFalse(chainable.has_function("bar")) + self.assertFalse(node.has_function("bar")) class TestEquivalenceHelpers(unittest.TestCase): @@ -202,18 +238,18 @@ def bar(): print(x) """ - chainable = Chainable().parse(full_str) + node = Node(full_str) expected = """def bar(): x = "1" print(x) """ - self.assertTrue(chainable.find_function("bar").is_equivalent(expected)) + self.assertTrue(node.find_function("bar").is_equivalent(expected)) # Obviously, it should be equivalent to itself self.assertTrue( - chainable.find_function("bar").is_equivalent( - ast.unparse(chainable.find_function("bar").tree) + node.find_function("bar").is_equivalent( + ast.unparse(node.find_function("bar").tree) ) ) @@ -225,7 +261,7 @@ def bar(): x = "1" print(x) """ - chainable = Chainable().parse(full_str) + node = Node(full_str) # this should not be equivalent because it contains an extra function expected = """def bar(): @@ -236,7 +272,7 @@ def foo(): a = 1 """ - self.assertFalse(chainable.find_function("bar").is_equivalent(expected)) + self.assertFalse(node.find_function("bar").is_equivalent(expected)) def test_is_equivalent_with_conditional(self): cond_str = """ @@ -244,17 +280,29 @@ def test_is_equivalent_with_conditional(self): pass """ - chainable = Chainable().parse(cond_str) - self.assertTrue(chainable[0].find_conditions()[0].is_equivalent("True")) + node = Node(cond_str) + self.assertTrue(node[0].find_conditions()[0].is_equivalent("True")) def test_none_equivalence(self): none_str = """ x = None """ - chainable = Chainable().parse(none_str) - self.assertIsNone(chainable.get_variable("x")) - self.assertFalse(chainable.find_variable("y").is_equivalent("None")) + node = Node(none_str) + self.assertIsNone(node.get_variable("x")) + self.assertFalse(node.find_variable("y").is_equivalent("None")) + + def test_whitespace_equivalence(self): + str_with_whitespace = """ + +x = 1 +""" + str_with_different_whitespace = """x = 1""" + self.assertTrue( + Node(str_with_whitespace).is_equivalent( + str_with_different_whitespace + ) + ) class TestConditionalHelpers(unittest.TestCase): @@ -269,15 +317,15 @@ def test_find_if_statements(self): pass """ - chainable = Chainable().parse(if_str) - # it should return an array of Chainables, not a Chainable of an array - for if_chainable in chainable.find_ifs(): - self.assertIsInstance(if_chainable, Chainable) - self.assertNotIsInstance(chainable.find_ifs(), Chainable) - self.assertEqual(len(chainable.find_ifs()), 2) + node = Node(if_str) + # it should return an array of nodes, not a node of an array + for if_node in node.find_ifs(): + self.assertIsInstance(if_node, Node) + self.assertNotIsInstance(node.find_ifs(), Node) + self.assertEqual(len(node.find_ifs()), 2) - self.assertTrue(chainable.find_ifs()[0].is_equivalent("if x == 1:\n x = 2")) - self.assertTrue(chainable.find_ifs()[1].is_equivalent("if True:\n pass")) + self.assertTrue(node.find_ifs()[0].is_equivalent("if x == 1:\n x = 2")) + self.assertTrue(node.find_ifs()[1].is_equivalent("if True:\n pass")) def test_find_conditions(self): if_str = """ @@ -286,29 +334,29 @@ def test_find_conditions(self): else: x = 4 """ - chainable = Chainable().parse(if_str) + node = Node(if_str) - # it should return an array of Chainables, not a Chainable of an array - for if_cond in chainable.find_ifs()[0].find_conditions(): - self.assertIsInstance(if_cond, Chainable) - self.assertNotIsInstance(chainable.find_ifs()[0].find_conditions(), Chainable) - self.assertEqual(len(chainable.find_ifs()[0].find_conditions()), 2) + # it should return an array of nodes, not a node of an array + for if_cond in node.find_ifs()[0].find_conditions(): + self.assertIsInstance(if_cond, Node) + self.assertNotIsInstance(node.find_ifs()[0].find_conditions(), Node) + self.assertEqual(len(node.find_ifs()[0].find_conditions()), 2) - self.assertIsNone(chainable.find_ifs()[0].find_conditions()[1].tree) + self.assertIsNone(node.find_ifs()[0].find_conditions()[1].tree) def test_find_conditions_without_if(self): - chainable = Chainable().parse("x = 1") + node = Node("x = 1") - self.assertEqual(len(chainable.find_conditions()), 0) + self.assertEqual(node.find_conditions(), []) def test_find_conditions_only_if(self): if_str = """ if True: x = 1 """ - chainable = Chainable().parse(if_str) + node = Node(if_str) - self.assertEqual(len(chainable.find_ifs()[0].find_conditions()), 1) + self.assertEqual(len(node.find_ifs()[0].find_conditions()), 1) def test_find_conditions_elif(self): if_str = """ @@ -321,23 +369,22 @@ def test_find_conditions_elif(self): else: x = 4 """ - chainable = Chainable().parse(if_str) + node = Node(if_str) - self.assertEqual(len(chainable.find_ifs()[0].find_conditions()), 4) + self.assertEqual(len(node.find_ifs()[0].find_conditions()), 4) self.assertTrue( - chainable.find_ifs()[0].find_conditions()[0].is_equivalent("True") + node.find_ifs()[0].find_conditions()[0].is_equivalent("True") ) self.assertTrue( - chainable.find_ifs()[0].find_conditions()[1].is_equivalent("y == 2") + node.find_ifs()[0].find_conditions()[1].is_equivalent("y == 2") ) self.assertTrue( - chainable.find_ifs()[0].find_conditions()[2].is_equivalent("not x < 3") + node.find_ifs()[0].find_conditions()[2].is_equivalent("not x < 3") ) - self.assertEqual(chainable.find_ifs()[0].find_conditions()[3].tree, None) - self.assertFalse( - chainable.find_ifs()[0] - .find_conditions()[3] - .is_equivalent("This can be anything") + self.assertEqual(node.find_ifs()[0].find_conditions()[3].tree, None) + self.assertRaises( + IndexError, + lambda: node.find_ifs()[0].find_conditions()[4], ) def test_find_if_bodies(self): @@ -345,11 +392,11 @@ def test_find_if_bodies(self): if True: x = 1 """ - chainable = Chainable().parse(if_str) + node = Node(if_str) - self.assertEqual(len(chainable.find_ifs()[0].find_if_bodies()), 1) + self.assertEqual(len(node.find_ifs()[0].find_if_bodies()), 1) self.assertTrue( - chainable.find_ifs()[0].find_if_bodies()[0].is_equivalent("x = 1") + node.find_ifs()[0].find_if_bodies()[0].is_equivalent("x = 1") ) def test_find_if_bodies_elif(self): @@ -363,46 +410,51 @@ def test_find_if_bodies_elif(self): else: x = 4 """ - chainable = Chainable().parse(if_str) + node = Node(if_str) - self.assertEqual(len(chainable.find_ifs()[0].find_if_bodies()), 4) + self.assertEqual(len(node.find_ifs()[0].find_if_bodies()), 4) self.assertTrue( - chainable.find_ifs()[0].find_if_bodies()[0].is_equivalent("x = 1") + node.find_ifs()[0].find_if_bodies()[0].is_equivalent("x = 1") ) self.assertTrue( - chainable.find_ifs()[0].find_if_bodies()[1].is_equivalent("x = 2") + node.find_ifs()[0].find_if_bodies()[1].is_equivalent("x = 2") ) self.assertTrue( - chainable.find_ifs()[0].find_if_bodies()[2].is_equivalent("x = 3") + node.find_ifs()[0].find_if_bodies()[2].is_equivalent("x = 3") ) self.assertTrue( - chainable.find_ifs()[0].find_if_bodies()[3].is_equivalent("x = 4") + node.find_ifs()[0].find_if_bodies()[3].is_equivalent("x = 4") ) self.assertRaises( - IndexError, lambda: chainable.find_ifs()[0].find_if_bodies()[4] + IndexError, lambda: node.find_ifs()[0].find_if_bodies()[4] ) + def test_find_if_bodies_without_if(self): + node = Node("x = 1") + + self.assertEqual(len(node.find_if_bodies()), 0) + class TestGenericHelpers(unittest.TestCase): def test_equality(self): self.assertEqual( - Chainable().parse("def foo():\n pass"), - Chainable().parse("def foo():\n pass"), + Node("def foo():\n pass"), + Node("def foo():\n pass"), ) self.assertNotEqual( - Chainable().parse("def foo():\n pass"), - Chainable().parse("def bar():\n pass"), + Node("def foo():\n pass"), + Node("def bar():\n pass"), ) def test_strict_equality(self): self.assertNotEqual( - Chainable().parse("def foo():\n pass"), - Chainable().parse("def foo():\n pass"), + Node("def foo():\n pass"), + Node("def foo():\n pass"), ) - def test_not_equal_to_non_chainable(self): - self.assertIsNotNone(Chainable().parse("def foo():\n pass")) - self.assertNotEqual(Chainable(), 1) + def test_not_equal_to_non_node(self): + self.assertIsNotNone(Node("def foo():\n pass")) + self.assertNotEqual(Node(), 1) def test_find_nth_statement(self): func_str = """ @@ -411,10 +463,10 @@ def test_find_nth_statement(self): x = 1 """ - chainable = Chainable().parse(func_str) + node = Node(func_str) - self.assertTrue(chainable[0].is_equivalent("if True:\n pass")) - self.assertTrue(chainable[1].is_equivalent("x = 1")) + self.assertTrue(node[0].is_equivalent("if True:\n pass")) + self.assertTrue(node[1].is_equivalent("x = 1")) def test_raise_exception_if_out_of_bounds(self): one_stmt_str = """ @@ -422,8 +474,8 @@ def test_raise_exception_if_out_of_bounds(self): pass """ - chainable = Chainable().parse(one_stmt_str) - self.assertRaises(IndexError, lambda: chainable[1]) + node = Node(one_stmt_str) + self.assertRaises(IndexError, lambda: node[1]) def test_len_of_body(self): func_str = """ @@ -431,9 +483,9 @@ def test_len_of_body(self): pass """ - chainable = Chainable().parse(func_str) + node = Node(func_str) - self.assertEqual(len(chainable), 1) + self.assertEqual(len(node), 1) def test_len(self): ifs_str = """ @@ -444,13 +496,14 @@ def test_len(self): pass """ - chainable = Chainable().parse(ifs_str) + node = Node(ifs_str) - self.assertEqual(len(chainable.find_ifs()), 2) + self.assertEqual(len(node.find_ifs()), 2) def suite(): suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(TestConstructor)) suite.addTest(unittest.makeSuite(TestVariableHelpers)) suite.addTest(unittest.makeSuite(TestFunctionAndClassHelpers)) suite.addTest(unittest.makeSuite(TestEquivalenceHelpers))