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