1 '''
2 Test cases for pyclbr.py
3 Nick Mathewson
4 '''
5
6 import sys
7 from textwrap import dedent
8 from types import FunctionType, MethodType, BuiltinFunctionType
9 import pyclbr
10 from unittest import TestCase, main as unittest_main
11 from test.test_importlib import util as test_importlib_util
12 import warnings
13
14
15 StaticMethodType = type(staticmethod(lambda: None))
16 ClassMethodType = 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
25 class ESC[4;38;5;81mPyclbrTest(ESC[4;38;5;149mTestCase):
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
241 class ESC[4;38;5;81mReadmoduleTests(ESC[4;38;5;149mTestCase):
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
265 if __name__ == "__main__":
266 unittest_main()