• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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                if stripped_typename := oclass.__name__.lstrip('_'):
82                    objname = f"_{stripped_typename}{objname}"
83            return objname == name
84
85        # Make sure the toplevel functions and classes are the same.
86        for name, value in dict.items():
87            if name in ignore:
88                continue
89            self.assertHasattr(module, name, ignore)
90            py_item = getattr(module, name)
91            if isinstance(value, pyclbr.Function):
92                self.assertIsInstance(py_item, (FunctionType, BuiltinFunctionType))
93                if py_item.__module__ != moduleName:
94                    continue   # skip functions that came from somewhere else
95                self.assertEqual(py_item.__module__, value.module)
96            else:
97                self.assertIsInstance(py_item, type)
98                if py_item.__module__ != moduleName:
99                    continue   # skip classes that came from somewhere else
100
101                real_bases = [base.__name__ for base in py_item.__bases__]
102                pyclbr_bases = [ getattr(base, 'name', base)
103                                 for base in value.super ]
104
105                try:
106                    self.assertListEq(real_bases, pyclbr_bases, ignore)
107                except:
108                    print("class=%s" % py_item, file=sys.stderr)
109                    raise
110
111                actualMethods = []
112                for m in py_item.__dict__.keys():
113                    if ismethod(py_item, getattr(py_item, m), m):
114                        actualMethods.append(m)
115
116                if stripped_typename := name.lstrip('_'):
117                    foundMethods = []
118                    for m in value.methods.keys():
119                        if m.startswith('__') and not m.endswith('__'):
120                            foundMethods.append(f"_{stripped_typename}{m}")
121                        else:
122                            foundMethods.append(m)
123                else:
124                    foundMethods = list(value.methods.keys())
125
126                try:
127                    self.assertListEq(foundMethods, actualMethods, ignore)
128                    self.assertEqual(py_item.__module__, value.module)
129
130                    self.assertEqualsOrIgnored(py_item.__name__, value.name,
131                                               ignore)
132                    # can't check file or lineno
133                except:
134                    print("class=%s" % py_item, file=sys.stderr)
135                    raise
136
137        # Now check for missing stuff.
138        def defined_in(item, module):
139            if isinstance(item, type):
140                return item.__module__ == module.__name__
141            if isinstance(item, FunctionType):
142                return item.__globals__ is module.__dict__
143            return False
144        for name in dir(module):
145            item = getattr(module, name)
146            if isinstance(item,  (type, FunctionType)):
147                if defined_in(item, module):
148                    self.assertHaskey(dict, name, ignore)
149
150    def test_easy(self):
151        self.checkModule('pyclbr')
152        # XXX: Metaclasses are not supported
153        # self.checkModule('ast')
154        self.checkModule('doctest', ignore=("TestResults", "_SpoofOut",
155                                            "DocTestCase", '_DocTestSuite'))
156        self.checkModule('difflib', ignore=("Match",))
157
158    def test_cases(self):
159        # see test.pyclbr_input for the rationale behind the ignored symbols
160        self.checkModule('test.pyclbr_input', ignore=['om', 'f'])
161
162    def test_nested(self):
163        mb = pyclbr
164        # Set arguments for descriptor creation and _creat_tree call.
165        m, p, f, t, i = 'test', '', 'test.py', {}, None
166        source = dedent("""\
167        def f0():
168            def f1(a,b,c):
169                def f2(a=1, b=2, c=3): pass
170                return f1(a,b,d)
171            class c1: pass
172        class C0:
173            "Test class."
174            def F1():
175                "Method."
176                return 'return'
177            class C1():
178                class C2:
179                    "Class nested within nested class."
180                    def F3(): return 1+1
181
182        """)
183        actual = mb._create_tree(m, p, f, source, t, i)
184
185        # Create descriptors, linked together, and expected dict.
186        f0 = mb.Function(m, 'f0', f, 1, end_lineno=5)
187        f1 = mb._nest_function(f0, 'f1', 2, 4)
188        f2 = mb._nest_function(f1, 'f2', 3, 3)
189        c1 = mb._nest_class(f0, 'c1', 5, 5)
190        C0 = mb.Class(m, 'C0', None, f, 6, end_lineno=14)
191        F1 = mb._nest_function(C0, 'F1', 8, 10)
192        C1 = mb._nest_class(C0, 'C1', 11, 14)
193        C2 = mb._nest_class(C1, 'C2', 12, 14)
194        F3 = mb._nest_function(C2, 'F3', 14, 14)
195        expected = {'f0':f0, 'C0':C0}
196
197        def compare(parent1, children1, parent2, children2):
198            """Return equality of tree pairs.
199
200            Each parent,children pair define a tree.  The parents are
201            assumed equal.  Comparing the children dictionaries as such
202            does not work due to comparison by identity and double
203            linkage.  We separate comparing string and number attributes
204            from comparing the children of input children.
205            """
206            self.assertEqual(children1.keys(), children2.keys())
207            for ob in children1.values():
208                self.assertIs(ob.parent, parent1)
209            for ob in children2.values():
210                self.assertIs(ob.parent, parent2)
211            for key in children1.keys():
212                o1, o2 = children1[key], children2[key]
213                t1 = type(o1), o1.name, o1.file, o1.module, o1.lineno, o1.end_lineno
214                t2 = type(o2), o2.name, o2.file, o2.module, o2.lineno, o2.end_lineno
215                self.assertEqual(t1, t2)
216                if type(o1) is mb.Class:
217                    self.assertEqual(o1.methods, o2.methods)
218                # Skip superclasses for now as not part of example
219                compare(o1, o1.children, o2, o2.children)
220
221        compare(None, actual, None, expected)
222
223    def test_others(self):
224        cm = self.checkModule
225
226        # These were once some of the longest modules.
227        cm('random', ignore=('Random',))  # from _random import Random as CoreGenerator
228        cm('pickle', ignore=('partial', 'PickleBuffer'))
229        with warnings.catch_warnings():
230            warnings.simplefilter('ignore', DeprecationWarning)
231            cm('sre_parse', ignore=('dump', 'groups', 'pos')) # from sre_constants import *; property
232        cm(
233            'pdb',
234            # pyclbr does not handle elegantly `typing` or properties
235            ignore=('Union', '_ModuleTarget', '_ScriptTarget', '_ZipTarget'),
236        )
237        cm('pydoc', ignore=('input', 'output',)) # properties
238
239        # Tests for modules inside packages
240        cm('email.parser')
241        cm('test.test_pyclbr')
242
243
244class ReadmoduleTests(TestCase):
245
246    def setUp(self):
247        self._modules = pyclbr._modules.copy()
248
249    def tearDown(self):
250        pyclbr._modules = self._modules
251
252
253    def test_dotted_name_not_a_package(self):
254        # test ImportError is raised when the first part of a dotted name is
255        # not a package.
256        #
257        # Issue #14798.
258        self.assertRaises(ImportError, pyclbr.readmodule_ex, 'asyncio.foo')
259
260    def test_module_has_no_spec(self):
261        module_name = "doesnotexist"
262        assert module_name not in pyclbr._modules
263        with test_importlib_util.uncache(module_name):
264            with self.assertRaises(ModuleNotFoundError):
265                pyclbr.readmodule_ex(module_name)
266
267
268if __name__ == "__main__":
269    unittest_main()
270