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