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        # XXX: See comment in pyclbr_input.py for a test that would fail
154        #      if it were not commented out.
155        #
156        self.checkModule('test.pyclbr_input', ignore=['om'])
157
158    def test_nested(self):
159        mb = pyclbr
160        # Set arguments for descriptor creation and _creat_tree call.
161        m, p, f, t, i = 'test', '', 'test.py', {}, None
162        source = dedent("""\
163        def f0:
164            def f1(a,b,c):
165                def f2(a=1, b=2, c=3): pass
166                    return f1(a,b,d)
167            class c1: pass
168        class C0:
169            "Test class."
170            def F1():
171                "Method."
172                return 'return'
173            class C1():
174                class C2:
175                    "Class nested within nested class."
176                    def F3(): return 1+1
177
178        """)
179        actual = mb._create_tree(m, p, f, source, t, i)
180
181        # Create descriptors, linked together, and expected dict.
182        f0 = mb.Function(m, 'f0', f, 1)
183        f1 = mb._nest_function(f0, 'f1', 2)
184        f2 = mb._nest_function(f1, 'f2', 3)
185        c1 = mb._nest_class(f0, 'c1', 5)
186        C0 = mb.Class(m, 'C0', None, f, 6)
187        F1 = mb._nest_function(C0, 'F1', 8)
188        C1 = mb._nest_class(C0, 'C1', 11)
189        C2 = mb._nest_class(C1, 'C2', 12)
190        F3 = mb._nest_function(C2, 'F3', 14)
191        expected = {'f0':f0, 'C0':C0}
192
193        def compare(parent1, children1, parent2, children2):
194            """Return equality of tree pairs.
195
196            Each parent,children pair define a tree.  The parents are
197            assumed equal.  Comparing the children dictionaries as such
198            does not work due to comparison by identity and double
199            linkage.  We separate comparing string and number attributes
200            from comparing the children of input children.
201            """
202            self.assertEqual(children1.keys(), children2.keys())
203            for ob in children1.values():
204                self.assertIs(ob.parent, parent1)
205            for ob in children2.values():
206                self.assertIs(ob.parent, parent2)
207            for key in children1.keys():
208                o1, o2 = children1[key], children2[key]
209                t1 = type(o1), o1.name, o1.file, o1.module, o1.lineno
210                t2 = type(o2), o2.name, o2.file, o2.module, o2.lineno
211                self.assertEqual(t1, t2)
212                if type(o1) is mb.Class:
213                    self.assertEqual(o1.methods, o2.methods)
214                # Skip superclasses for now as not part of example
215                compare(o1, o1.children, o2, o2.children)
216
217        compare(None, actual, None, expected)
218
219    def test_others(self):
220        cm = self.checkModule
221
222        # These were once about the 10 longest modules
223        cm('random', ignore=('Random',))  # from _random import Random as CoreGenerator
224        cm('cgi', ignore=('log',))      # set with = in module
225        cm('pickle', ignore=('partial', 'PickleBuffer'))
226        # TODO(briancurtin): openfp is deprecated as of 3.7.
227        # Update this once it has been removed.
228        cm('aifc', ignore=('openfp', '_aifc_params'))  # set with = in module
229        cm('sre_parse', ignore=('dump', 'groups', 'pos')) # from sre_constants import *; property
230        cm('pdb')
231        cm('pydoc', ignore=('input', 'output',)) # properties
232
233        # Tests for modules inside packages
234        cm('email.parser')
235        cm('test.test_pyclbr')
236
237
238class ReadmoduleTests(TestCase):
239
240    def setUp(self):
241        self._modules = pyclbr._modules.copy()
242
243    def tearDown(self):
244        pyclbr._modules = self._modules
245
246
247    def test_dotted_name_not_a_package(self):
248        # test ImportError is raised when the first part of a dotted name is
249        # not a package.
250        #
251        # Issue #14798.
252        self.assertRaises(ImportError, pyclbr.readmodule_ex, 'asyncore.foo')
253
254    def test_module_has_no_spec(self):
255        module_name = "doesnotexist"
256        assert module_name not in pyclbr._modules
257        with test_importlib_util.uncache(module_name):
258            with self.assertRaises(ModuleNotFoundError):
259                pyclbr.readmodule_ex(module_name)
260
261
262if __name__ == "__main__":
263    unittest_main()
264