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