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)
예제 #4
0
    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
예제 #8
0
 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)
예제 #9
0
 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)
예제 #10
0
 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)
예제 #11
0
 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)
예제 #15
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
예제 #17
0
    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)
예제 #19
0
 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)
예제 #23
0
    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