1'''
2   Test cases for pyclbr.py
3   Nick Mathewson
4'''
5
6import sys
7from textwrap import dedent
8from types import FunctionType, MethodType, BuiltinFunctionType
9import pyclbr
10from unittest import TestCase, main as unittest_main
11from test.test_importlib import util as test_importlib_util
12
13
14StaticMethodType = type(staticmethod(lambda: None))
15ClassMethodType = type(classmethod(lambda c: None))
16
17# Here we test the python class browser code.
18#
19# The main function in this suite, 'testModule', compares the output
20# of pyclbr with the introspected members of a module.  Because pyclbr
21# is imperfect (as designed), testModule is called with a set of
22# members to ignore.
23
24class PyclbrTest(TestCase):
25
26    def assertListEq(self, l1, l2, ignore):
27        ''' succeed iff {l1} - {ignore} == {l2} - {ignore} '''
28        missing = (set(l1) ^ set(l2)) - set(ignore)
29        if missing:
30            print("l1=%r\nl2=%r\nignore=%r" % (l1, l2, ignore), file=sys.stderr)
31            self.fail("%r missing" % missing.pop())
32
33    def assertHasattr(self, obj, attr, ignore):
34        ''' succeed iff hasattr(obj,attr) or attr in ignore. '''
35        if attr in ignore: return
36        if not hasattr(obj, attr): print("???", attr)
37        self.assertTrue(hasattr(obj, attr),
38                        'expected hasattr(%r, %r)' % (obj, attr))
39
40
41    def assertHaskey(self, obj, key, ignore):
42        ''' succeed iff key in obj or key in ignore. '''
43        if key in ignore: return
44        if key not in obj:
45            print("***",key, file=sys.stderr)
46        self.assertIn(key, obj)
47
48    def assertEqualsOrIgnored(self, a, b, ignore):
49        ''' succeed iff a == b or a in ignore or b in ignore '''
50        if a not in ignore and b not in ignore:
51            self.assertEqual(a, b)
52
53    def checkModule(self, moduleName, module=None, ignore=()):
54        ''' succeed iff pyclbr.readmodule_ex(modulename) corresponds
55            to the actual module object, module.  Any identifiers in
56            ignore are ignored.   If no module is provided, the appropriate
57            module is loaded with __import__.'''
58
59        ignore = set(ignore) | set(['object'])
60
61        if module is None:
62            # Import it.
63            # ('<silly>' is to work around an API silliness in __import__)
64            module = __import__(moduleName, globals(), {}, ['<silly>'])
65
66        dict = pyclbr.readmodule_ex(moduleName)
67
68        def ismethod(oclass, obj, name):
69            classdict = oclass.__dict__
70            if isinstance(obj, MethodType):
71                # could be a classmethod
72                if (not isinstance(classdict[name], ClassMethodType) or
73                    obj.__self__ is not oclass):
74                    return False
75            elif not isinstance(obj, FunctionType):
76                return False
77
78            objname = obj.__name__
79            if objname.startswith("__") and not objname.endswith("__"):
80                objname = "_%s%s" % (oclass.__name__, objname)
81            return objname == name
82
83        # Make sure the toplevel functions and classes are the same.
84        for name, value in dict.items():
85            if name in ignore:
86                continue
87            self.assertHasattr(module, name, ignore)
88            py_item = getattr(module, name)
89            if isinstance(value, pyclbr.Function):
90                self.assertIsInstance(py_item, (FunctionType, BuiltinFunctionType))
91                if py_item.__module__ != moduleName:
92                    continue   # skip functions that came from somewhere else
93                self.assertEqual(py_item.__module__, value.module)
94            else:
95                self.assertIsInstance(py_item, type)
96                if py_item.__module__ != moduleName:
97                    continue   # skip classes that came from somewhere else
98
99                real_bases = [base.__name__ for base in py_item.__bases__]
100                pyclbr_bases = [ getattr(base, 'name', base)
101                                 for base in value.super ]
102
103                try:
104                    self.assertListEq(real_bases, pyclbr_bases, ignore)
105                except:
106                    print("class=%s" % py_item, file=sys.stderr)
107                    raise
108
109                actualMethods = []
110                for m in py_item.__dict__.keys():
111                    if ismethod(py_item, getattr(py_item, m), m):
112                        actualMethods.append(m)
113                foundMethods = []
114                for m in value.methods.keys():
115                    if m[:2] == '__' and m[-2:] != '__':
116                        foundMethods.append('_'+name+m)
117                    else:
118                        foundMethods.append(m)
119
120                try:
121                    self.assertListEq(foundMethods, actualMethods, ignore)
122                    self.assertEqual(py_item.__module__, value.module)
123
124                    self.assertEqualsOrIgnored(py_item.__name__, value.name,
125                                               ignore)
126                    # can't check file or lineno
127                except:
128                    print("class=%s" % py_item, file=sys.stderr)
129                    raise
130
131        # Now check for missing stuff.
132        def defined_in(item, module):
133            if isinstance(item, type):
134                return item.__module__ == module.__name__
135            if isinstance(item, FunctionType):
136                return item.__globals__ is module.__dict__
137            return False
138        for name in dir(module):
139            item = getattr(module, name)
140            if isinstance(item,  (type, FunctionType)):
141                if defined_in(item, module):
142                    self.assertHaskey(dict, name, ignore)
143
144    def test_easy(self):
145        self.checkModule('pyclbr')
146        # XXX: Metaclasses are not supported
147        # self.checkModule('ast')
148        self.checkModule('doctest', ignore=("TestResults", "_SpoofOut",
149                                            "DocTestCase", '_DocTestSuite'))
150        self.checkModule('difflib', ignore=("Match",))
151
152    def test_decorators(self):
153        self.checkModule('test.pyclbr_input', ignore=['om'])
154
155    def test_nested(self):
156        mb = pyclbr
157        # Set arguments for descriptor creation and _creat_tree call.
158        m, p, f, t, i = 'test', '', 'test.py', {}, None
159        source = dedent("""\
160        def f0():
161            def f1(a,b,c):
162                def f2(a=1, b=2, c=3): pass
163                return f1(a,b,d)
164            class c1: pass
165        class C0:
166            "Test class."
167            def F1():
168                "Method."
169                return 'return'
170            class C1():
171                class C2:
172                    "Class nested within nested class."
173                    def F3(): return 1+1
174
175        """)
176        actual = mb._create_tree(m, p, f, source, t, i)
177
178        # Create descriptors, linked together, and expected dict.
179        f0 = mb.Function(m, 'f0', f, 1, end_lineno=5)
180        f1 = mb._nest_function(f0, 'f1', 2, 4)
181        f2 = mb._nest_function(f1, 'f2', 3, 3)
182        c1 = mb._nest_class(f0, 'c1', 5, 5)
183        C0 = mb.Class(m, 'C0', None, f, 6, end_lineno=14)
184        F1 = mb._nest_function(C0, 'F1', 8, 10)
185        C1 = mb._nest_class(C0, 'C1', 11, 14)
186        C2 = mb._nest_class(C1, 'C2', 12, 14)
187        F3 = mb._nest_function(C2, 'F3', 14, 14)
188        expected = {'f0':f0, 'C0':C0}
189
190        def compare(parent1, children1, parent2, children2):
191            """Return equality of tree pairs.
192
193            Each parent,children pair define a tree.  The parents are
194            assumed equal.  Comparing the children dictionaries as such
195            does not work due to comparison by identity and double
196            linkage.  We separate comparing string and number attributes
197            from comparing the children of input children.
198            """
199            self.assertEqual(children1.keys(), children2.keys())
200            for ob in children1.values():
201                self.assertIs(ob.parent, parent1)
202            for ob in children2.values():
203                self.assertIs(ob.parent, parent2)
204            for key in children1.keys():
205                o1, o2 = children1[key], children2[key]
206                t1 = type(o1), o1.name, o1.file, o1.module, o1.lineno, o1.end_lineno
207                t2 = type(o2), o2.name, o2.file, o2.module, o2.lineno, o2.end_lineno
208                self.assertEqual(t1, t2)
209                if type(o1) is mb.Class:
210                    self.assertEqual(o1.methods, o2.methods)
211                # Skip superclasses for now as not part of example
212                compare(o1, o1.children, o2, o2.children)
213
214        compare(None, actual, None, expected)
215
216    def test_others(self):
217        cm = self.checkModule
218
219        # These were once about the 10 longest modules
220        cm('random', ignore=('Random',))  # from _random import Random as CoreGenerator
221        cm('cgi', ignore=('log',))      # set with = in module
222        cm('pickle', ignore=('partial', 'PickleBuffer'))
223        cm('aifc', ignore=('_aifc_params',))  # set with = in module
224        cm('sre_parse', ignore=('dump', 'groups', 'pos')) # from sre_constants import *; property
225        cm(
226            'pdb',
227            # pyclbr does not handle elegantly `typing` or properties
228            ignore=('Union', 'ModuleTarget', 'ScriptTarget'),
229        )
230        cm('pydoc', ignore=('input', 'output',)) # properties
231
232        # Tests for modules inside packages
233        cm('email.parser')
234        cm('test.test_pyclbr')
235
236
237class ReadmoduleTests(TestCase):
238
239    def setUp(self):
240        self._modules = pyclbr._modules.copy()
241
242    def tearDown(self):
243        pyclbr._modules = self._modules
244
245
246    def test_dotted_name_not_a_package(self):
247        # test ImportError is raised when the first part of a dotted name is
248        # not a package.
249        #
250        # Issue #14798.
251        self.assertRaises(ImportError, pyclbr.readmodule_ex, 'asyncio.foo')
252
253    def test_module_has_no_spec(self):
254        module_name = "doesnotexist"
255        assert module_name not in pyclbr._modules
256        with test_importlib_util.uncache(module_name):
257            with self.assertRaises(ModuleNotFoundError):
258                pyclbr.readmodule_ex(module_name)
259
260
261if __name__ == "__main__":
262    unittest_main()
263