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