def test_assignment_has_type_annotation(self): """ 'AnnAssign' node instead of traditional 'Assign' - copy the Assign logic but adjust since can't have multiple lhs when using type annotations. AST: body=[ AnnAssign( lineno=4, col_offset=8, target=Attribute( lineno=4, col_offset=8, value=Name(lineno=4, col_offset=8, id='self', ctx=Load()), attr='restaurant', ctx=Store(), ), annotation=Name(lineno=4, col_offset=25, id='Restaurant', ctx=Load()), value=Name(lineno=4, col_offset=38, id='restaurant', ctx=Load()), simple=0, ), """ source_code = dedent(""" class Customer: def __init__(self, restaurant): self.restaurant: Restaurant = restaurant self.fred: Fred self.xx: Mary = None """) pmodel, debuginfo = parse_source( source_code, options={"mode": 3}, html_debug_root_name="test_assignment_has_type_annotation") self._ensure_attrs_created(pmodel)
def test_assignment_rhs_missing(self): """ When rhs entirely missing, the AST contains an 'Expr' node not an 'Assign' node. AST: body=[ Expr( lineno=4, col_offset=8, value=Attribute( lineno=4, col_offset=8, value=Name(lineno=4, col_offset=8, id='self', ctx=Load()), attr='restaurant', ctx=Load(), ), ), """ source_code = dedent(""" class Customer: def __init__(self, restaurant): self.restaurant self.fred self.xx """) pmodel, debuginfo = parse_source( source_code, options={"mode": 3}, html_debug_root_name="test_assignment_rhs_missing") self._ensure_attrs_created(pmodel)
def test_assignment_rhs_is_None(self): """ Traditional 'Assign' node, attribute create via old rhs/lhs/flush logic. AST: body=[ Assign( lineno=4, col_offset=8, targets=[ Attribute( lineno=4, col_offset=8, value=Name(lineno=4, col_offset=8, id='self', ctx=Load()), attr='restaurant', ctx=Store(), ), ], value=NameConstant(lineno=4, col_offset=26, value=None), type_comment=None, ), """ source_code = dedent(""" class Customer: def __init__(self, restaurant): self.restaurant = None self.fred = None self.xx = None """) pmodel, debuginfo = parse_source( source_code, options={"mode": 3}, html_debug_root_name="test_assignment_rhs_is_None") self._ensure_attrs_created(pmodel)
def test_no_duplicate_edges(self): """ 1. Ensure no duplicate edges when add to displaymodel from same parsemodel twice """ source_code = dedent(""" class Fred(Mary, Sam): pass """) pmodel, debuginfo = parse_source(source_code, options={}) # print(pmodel.classlist) # print((dump_old_structure(pmodel))) self.assertEqual(list(pmodel.classlist.keys()), ["Fred"]) self.assertEqual(pmodel.classlist["Fred"].defs, []) self.assertEqual(pmodel.classlist["Fred"].classesinheritsfrom, ["Mary", "Sam"]) # Now convert to a display model dmodel = DisplayModel() dmodel.build_graphmodel(pmodel) # dmodel.Dump() self.assertEqual(len(dmodel.graph.nodes), 3) self.assertEqual(len(dmodel.graph.edges), 2) # print("display model", dmodel) # again - should not cause extra edges to be created dmodel.build_graphmodel(pmodel) # dmodel.Dump() self.assertEqual(len(dmodel.graph.nodes), 3) self.assertEqual(len(dmodel.graph.edges), 2)
def test_type_annotation_in_attr_assignment(self): # Ensure attr assignment in class, with type annotation, works source_code = dedent(""" class Customer: def __init__(self, restaurant): self.restaurant: Restaurant = restaurant """) pmodel, debuginfo = parse_source( source_code, options={"mode": 3}, html_debug_root_name="test_type_annotation_in_attr_assignment") self.assertEqual(pmodel.errors, "") classNames = [ classname for classname, classentry in pmodel.classlist.items() ] # print(classNames) # print(dump_pmodel(pmodel)) # very pretty table dump of the parse model self.assertIn("Customer", classNames) classentry = pmodel.classlist["Customer"] # print(classentry) self.assertEqual(len(classentry.defs), 1) self.assertIn("__init__", classentry.defs) # ensure the type annotation dependencies have been detected self.assertEqual(len(classentry.classdependencytuples), 1) self.assertEqual(classentry.classdependencytuples[0], ('restaurant', 'Restaurant')) # make sure the attributes are being created as well attrnames = [attr_tuple.attrname for attr_tuple in classentry.attrs] assert "restaurant" in attrnames
def test_yield(self): source_code = dedent(""" class Test(): def gen(self): yield 20 yield """) pmodel, debuginfo = parse_source(source_code, options={"mode": 3}) self.assertEqual(pmodel.errors, "") print(pmodel)
def test_type_annotation_outside_class(self): # Outside of a class Pynsource can't make a class to class dependency but should parse ok. # (but GitUML can make a module dependency since it supports module dependencies and Pynsource currently does not) source_code = dedent(""" def func(varin: dict): pass """) pmodel, debuginfo = parse_source( source_code, options={"mode": 3}, html_debug_root_name="test_type_annotation_outside_class") self.assertIn("had no classes", pmodel.errors) # not really an error
def test_exception_simple(self): """Parse simple """ source_code = dedent(""" def fred(): try: blah() except TypeError as e: raise e """) pmodel, debuginfo = parse_source( source_code, options={"mode": 3}, html_debug_root_name="test_exception_simple") self.assertNoPmodelErrors(pmodel)
def test_exception_from_none(self): """Parse python 3 specific""" source_code = dedent(""" def fred(): try: blah() except ImportError: raise BuildFailed('blah') from None """) pmodel, debuginfo = parse_source( source_code, options={"mode": 3}, html_debug_root_name="test_exception_from_none") self.assertNoPmodelErrors(pmodel)
def test_exception_bug_2018(self): """Parse python 3 specific""" source_code = dedent(""" def fred(): try: blah() except Exception as e: raise e """) pmodel, debuginfo = parse_source( source_code, options={"mode": 3}, html_debug_root_name="test_exception_bug_2018") self.assertNoPmodelErrors(pmodel)
def test_exception_complex(self): """Parse complex multiple exceptions """ source_code = dedent(""" def fred(): try: blah() except (TypeError, ValueError) as e: blah2() raise e """) pmodel, debuginfo = parse_source( source_code, options={"mode": 3}, html_debug_root_name="test_exception_complex") self.assertNoPmodelErrors(pmodel)
def test_issue_80(self): # https://github.com/abulka/pynsource/issues/80 source_code = dedent(""" class Foo(object): @classmethod def create(cls,VAR1): self = Foo() # <-- this is needed to cause the error self.var1 = VAR1 # <- error here return self """) pmodel, debuginfo = parse_source(source_code, options={"mode": 3}, html_debug_root_name="test_issue_80") self.assertEqual(pmodel.errors, "")
def test_sorted_attributes(self): # see also 'test_plantuml_sorted' in src/tests/test_parse_plantuml.py source_code = dedent(""" class ParseMeTest: def __init__(self): self.z = 1 self.a = 1 def aa(self): pass def zz(self): pass def bb(self): pass """) pmodel, debuginfo = parse_source(source_code, options={"mode": 3}) self.assertEqual(pmodel.errors, "") t = dump_pmodel(pmodel) print(t)
def test_type_annotation_builtin_types_skipped(self): # Skip creating references to built in types like 'bool' etc source_code = dedent(""" class Customer: def __init__(self): self.a: bool self.b: int self.c: str """) pmodel, debuginfo = parse_source( source_code, options={"mode": 3}, html_debug_root_name="test_type_annotation_builtin_types_skipped") self.assertEqual(pmodel.errors, "") classentry = pmodel.classlist["Customer"] self.assertEqual(len(classentry.classdependencytuples), 0)
def test_display_model_simplification(self): """ Ensure old display model is no more. It is now merely the graph, and graph nodes point to shapes - node shapes are attached to nodes as node.shape - edge shapes are attached to each edge mapping dictionary """ source_code = dedent(""" class Fred(Mary, Sam): pass """) pmodel, debuginfo = parse_source(source_code, options={}) dmodel = DisplayModel() dmodel.build_graphmodel(pmodel) # old extra data structures we don't need self.assertIsNone(getattr(dmodel, "classnametoshape", None)) self.assertIsNone(getattr(dmodel, "associations_generalisation", None)) self.assertIsNone(getattr(dmodel, "associations_composition", None)) self.assertIsNone(getattr(dmodel, "associations", None)) # instead we have graph nodes pointing to shapes fred: GraphNode = dmodel.graph.FindNodeById("Fred") mary: GraphNode = dmodel.graph.FindNodeById("Mary") sam: GraphNode = dmodel.graph.FindNodeById("Sam") self.node_check(fred) self.node_check(mary) self.node_check(sam) # and we have graph edges dicts pointing to shapes fred_mary = dmodel.graph.FindEdge(fred, mary, "generalisation") fred_sam = dmodel.graph.FindEdge(fred, sam, "generalisation") self.assertIsNotNone(fred_mary) self.assertIsNotNone(fred_sam) umlcanvas = self.create_mock_umlcanvas(dmodel) dmodel.build_view( purge_existing_shapes=True) # doesn't matter t/f cos first build self.assertTrue(umlcanvas.CreateUmlShape.called) self.assertFalse(umlcanvas.createCommentShape.called) self.assertTrue(umlcanvas.CreateUmlEdgeShape.called) self.assertEqual(umlcanvas.CreateUmlShape.call_count, 3) self.assertEqual(umlcanvas.CreateUmlEdgeShape.call_count, 2)
def test_type_annotations_in_method_args(self): """ Detect type annotations in method arguments https://github.com/abulka/pynsource/issues/75 """ source_code = dedent(""" # Ironically, declaring the Restaurant class will trigger Pynsource to treat # self.restaurant as a implicit reference to the Restaurant class - without needing type annotations # simply due to the convention that it is the same name with the first letter in uppercase. # But let's not rely on this here, so comment out this 'trick' # # class Restaurant: # pass class Customer: def __init__(self, restaurant: Restaurant): self.restaurant = restaurant """) pmodel, debuginfo = parse_source( source_code, options={"mode": 3}, html_debug_root_name="test_type_annotations_in_method_args") self.assertEqual(pmodel.errors, "") classNames = [ classname for classname, classentry in pmodel.classlist.items() ] # print(classNames) # print(dump_pmodel(pmodel)) # very pretty table dump of the parse model self.assertIn("Customer", classNames) classentry = pmodel.classlist["Customer"] # print(classentry) self.assertEqual(len(classentry.defs), 1) self.assertIn("__init__", classentry.defs) # ensure the type annotation dependencies have been detected self.assertEqual(len(classentry.classdependencytuples), 1) self.assertEqual(classentry.classdependencytuples[0], ('restaurant', 'Restaurant')) # make sure the attributes are being created as well attrnames = [attr_tuple.attrname for attr_tuple in classentry.attrs] assert "restaurant" in attrnames
def test_display_model_general1(self): source_code = dedent(""" class Fred(Big): self.a = A() self.a2 = A() """) pmodel, debuginfo = parse_source(source_code, options={}) dmodel = DisplayModel() dmodel.build_graphmodel(pmodel) # dmodel.Dump() fred: GraphNode = dmodel.graph.FindNodeById("Fred") big: GraphNode = dmodel.graph.FindNodeById("Big") a: GraphNode = dmodel.graph.FindNodeById("A") self.assertIsNotNone(fred) self.assertIsNotNone(big) self.assertIsNotNone(a) self.assertIsNotNone(dmodel.graph.FindEdge(fred, big, "generalisation")) self.assertIsNotNone(dmodel.graph.FindEdge(a, fred, "composition"))
def test_function_annotation_2(self): source_code = dedent(""" class Customer: def meth1(self, param: Exception.ArithmeticError) -> None: pass def meth2(self, param: Exception) -> SomeOtherClass: pass """) pmodel, debuginfo = parse_source( source_code, options={"mode": 3}, html_debug_root_name="test_function_annotation_2") self.assertEqual(pmodel.errors, "") classentry = pmodel.classlist["Customer"] self.assertEqual( len(classentry.classdependencytuples), 2 ) # Ideally should also find 'SomeOtherClass' dependency? thus 3 dependencies self.assertIn(('param', 'Exception'), classentry.classdependencytuples) self.assertIn(('param', 'Exception.ArithmeticError'), classentry.classdependencytuples)
def test_staticmethod(self): # https://github.com/abulka/pynsource/issues/74 source_code = dedent( """ class Test(): @staticmethod def hi(): pass """ ) pmodel, debuginfo = parse_source(source_code, options={"mode": 3}, html_debug_root_name="test_staticmethod") self.assertEqual(pmodel.errors, "") classNames = [classname for classname, classentry in pmodel.classlist.items()] # print(classNames) # print(dump_pmodel(pmodel)) assert "Test" in classNames assert classNames == ["Test"] classentry = pmodel.classlist["Test"] # print(classentry) assert len(classentry.defs) == 1 assert "hi" in classentry.defs
def test_function_annotation_1(self): # Ensure can parse function annotation return types - see https://github.com/abulka/pynsource/issues/79 """ The reported error was caused by type having a '.' in the type e.g. Exception.ArithmeticError where we got annotation=Attribute( value=Name(lineno=6, col_offset=27, id='Exception', ctx=Load()), attr='ArithmeticError', rather than annotation=Name(lineno=7, col_offset=27, id='Exception', ctx=Load()), The solution is for the parser to check if the annotation is an ast.Attribute or ast.Name (see line 1425 in src/parsing/core_parser_ast.py) """ source_code = dedent(""" def func1(x: Exception) -> None: pass def func1(x: Exception.ArithmeticError) -> float: pass """) pmodel, debuginfo = parse_source( source_code, options={"mode": 3}, html_debug_root_name="test_function_annotation_1") self.assertIn("had no classes.", pmodel.errors)
def test_issue_81(self): # https://github.com/abulka/pynsource/issues/81 """""" # avoid test message grabbing first line of the docstring below """ This ability to specify an = sign after a variable in an fstring is a Python 3.8 feature. See "f-strings support = for self-documenting expressions and debugging" in https://docs.python.org/3/whatsnew/3.8.html Pynsource will have to be running under Python 3.8 to handle this syntax. Pynsource release binaries currently run under Python 3.7. Running the latest master of Pynsource from source under Python 3.8 will allow you to parse this 3.8 syntax. I hope to update the Pynsource release binaries to Python 3.8 in the next release. """ source_code = dedent(""" variable = 'a' print(f'{variable=}') """) pmodel, debuginfo = parse_source(source_code, options={"mode": 3}, html_debug_root_name="test_issue_81") self.assertNotIn("error", pmodel.errors) self.assertIn("had no classes", pmodel.errors) print(sys.version_info.minor)
def test_type_annotation_attr_tricky_rhs(self): # Handle type annotation parsing where no rhs. expression given, and where rhs. is None source_code = dedent(""" class Customer: def __init__(self, restaurant): self.restaurant: Restaurant = restaurant self.fred: Fred self.xx: Mary = None """) pmodel, debuginfo = parse_source( source_code, options={"mode": 3}, html_debug_root_name="test_type_annotation_attr_tricky_rhs") self.assertEqual(pmodel.errors, "") classNames = [ classname for classname, classentry in pmodel.classlist.items() ] # print(classNames) # print(dump_pmodel(pmodel)) # very pretty table dump of the parse model self.assertIn("Customer", classNames) classentry = pmodel.classlist["Customer"] # print(classentry) self.assertEqual(len(classentry.defs), 1) self.assertIn("__init__", classentry.defs) # make sure the attributes are being created attrnames = [attr_tuple.attrname for attr_tuple in classentry.attrs] assert "restaurant" in attrnames assert "fred" in attrnames assert "xx" in attrnames # ensure the type annotation dependencies have been detected self.assertEqual(len(classentry.classdependencytuples), 3) self.assertIn(('restaurant', 'Restaurant'), classentry.classdependencytuples) self.assertIn(('fred', 'Fred'), classentry.classdependencytuples) self.assertIn(('xx', 'Mary'), classentry.classdependencytuples)
def test_merge_attrs(self): """ 2. when add multiple paresemodels to the displaymodel classes in both pmodels can miss out on their full set of attrs/methods depending on the order of pmodels. (cos attrs/methods might not be merging) """ source_code1 = dedent(""" class Fred(Mary): pass """) pmodel1, debuginfo = parse_source(source_code1, options={}) source_code2 = dedent(""" class Fred(Mary): def __init__(self): self.attr1 = None def method1(self): pass """) pmodel2, debuginfo = parse_source(source_code2, options={}) # now add both pmodels to the same display model - hopefully dmodel = DisplayModel() # first parse of class Fred - no attributes or methods, but inherits from Mary dmodel.build_graphmodel(pmodel1) # dmodel.Dump() # check the parsemodel # self.assertEqual(list(pmodel1.classlist.keys()), ["Fred", "Mary"]) # seems that parent doesn't get officially created self.assertEqual(pmodel1.classlist["Fred"].attrs, []) self.assertEqual(pmodel1.classlist["Fred"].defs, []) # check the displaymodel self.assertEqual(len(dmodel.graph.nodes), 2) self.assertEqual(len(dmodel.graph.edges), 1) node = dmodel.graph.FindNodeById("Fred") self.assertEqual(node.attrs, []) self.assertEqual(node.meths, []) # second parse of class Fred - one attributes one method, and still inherits from Mary dmodel.build_graphmodel(pmodel2) # dmodel.Dump() # check the parsemodel # self.assertEqual(list(pmodel1.classlist.keys()), ["Fred", "Mary"]) # seems that parent doesn't get officially created # the main point of this test self.assertEqual(pmodel2.classlist["Fred"].attrs[0].attrname, "attr1") self.assertEqual(pmodel2.classlist["Fred"].defs, ["__init__", "method1"]) # check the displaymodel self.assertEqual(len(dmodel.graph.nodes), 2) self.assertEqual(len(dmodel.graph.edges), 1) # relies on edge duplicate protection fix node = dmodel.graph.FindNodeById("Fred") # test the merging has occurred self.assertEqual(node.attrs, ["attr1"]) self.assertCountEqual( node.meths, ["__init__", "method1"]) # relies on edge duplicate fix
def test_decorators(self): """ Ensure we create class attributes from methods marked with property decorators, and create methods for all other decorator types. """ source_code = dedent(""" # staticmethod and classmthod class Test(): @staticmethod def hi(): pass @classmethod def there(cls): pass # static method, can call directly on class (no instance needed) Test.hi() # create instance t = Test() t.hi() t.myval = 100 t.myval # Abstract methods - technique 1 from abc import ABC, abstractmethod class AbstractAnimal(ABC): @abstractmethod def move(self): pass try: animal = AbstractAnimal() # illegal except TypeError: print("Yep, can't instantiate an abstract class.") animal = Animal() print(animal) animal.move() # Abstract methods - technique 2 import abc class Crop(metaclass=abc.ABCMeta): '''Abstract class declaring that its subclasses must implement that the sow() & harvest() methods.''' @abc.abstractmethod def sow(self): pass def irrigate(self): pass @abc.abstractmethod def harvest(self): pass # Properties class PropsClass(): @staticmethod def hi(): print("hi from static method") @property def myval(self): print(f"getting val") return 999 @myval.setter def myval(self, val): print(f"setting val to {val}") """) pmodel, debuginfo = parse_source( source_code, options={"mode": 3}, html_debug_root_name="test_decorators") self.assertEqual(pmodel.errors, "") classNames = [ classname for classname, classentry in pmodel.classlist.items() ] # print(classNames) # print(dump_pmodel(pmodel)) assert "Test" in classNames assert "AbstractAnimal" in classNames assert "Crop" in classNames assert "PropsClass" in classNames classentry = pmodel.classlist["Test"] assert len(classentry.defs) == 2 assert "hi" in classentry.defs # staticmethod assert "there" in classentry.defs # classmethod classentry = pmodel.classlist["AbstractAnimal"] assert len(classentry.defs) == 1 assert "move" in classentry.defs classentry = pmodel.classlist["Crop"] assert len(classentry.defs) == 3 assert "sow" in classentry.defs assert "irrigate" in classentry.defs assert "harvest" in classentry.defs classentry = pmodel.classlist["PropsClass"] assert len(classentry.defs) == 1 assert "hi" in classentry.defs attrnames = [attr_tuple.attrname for attr_tuple in classentry.attrs] assert "myval" in attrnames