##// END OF EJS Templates
explict python version check
Matthias Bussonnier -
Show More
@@ -1,541 +1,533 b''
1 1 """Tests for the object inspection functionality.
2 2 """
3 3
4 4 # Copyright (c) IPython Development Team.
5 5 # Distributed under the terms of the Modified BSD License.
6 6
7 7
8 8 from contextlib import contextmanager
9 9 from inspect import signature, Signature, Parameter
10 10 import inspect
11 11 import os
12 12 import pytest
13 13 import re
14 14 import sys
15 15
16 16 from .. import oinspect
17 17
18 18 from decorator import decorator
19 19
20 20 from IPython.testing.tools import AssertPrints, AssertNotPrints
21 21 from IPython.utils.path import compress_user
22 22
23 23
24 24 #-----------------------------------------------------------------------------
25 25 # Globals and constants
26 26 #-----------------------------------------------------------------------------
27 27
28 28 inspector = None
29 29
30 30 def setup_module():
31 31 global inspector
32 32 inspector = oinspect.Inspector()
33 33
34 34
35 35 class SourceModuleMainTest:
36 36 __module__ = "__main__"
37 37
38 38
39 39 #-----------------------------------------------------------------------------
40 40 # Local utilities
41 41 #-----------------------------------------------------------------------------
42 42
43 43 # WARNING: since this test checks the line number where a function is
44 44 # defined, if any code is inserted above, the following line will need to be
45 45 # updated. Do NOT insert any whitespace between the next line and the function
46 46 # definition below.
47 47 THIS_LINE_NUMBER = 47 # Put here the actual number of this line
48 48
49 49
50 50 def test_find_source_lines():
51 51 assert oinspect.find_source_lines(test_find_source_lines) == THIS_LINE_NUMBER + 3
52 52 assert oinspect.find_source_lines(type) is None
53 53 assert oinspect.find_source_lines(SourceModuleMainTest) is None
54 54 assert oinspect.find_source_lines(SourceModuleMainTest()) is None
55 55
56 56
57 57 def test_getsource():
58 58 assert oinspect.getsource(type) is None
59 59 assert oinspect.getsource(SourceModuleMainTest) is None
60 60 assert oinspect.getsource(SourceModuleMainTest()) is None
61 61
62 62
63 63 def test_inspect_getfile_raises_exception():
64 64 """Check oinspect.find_file/getsource/find_source_lines expectations"""
65 65 with pytest.raises(TypeError):
66 66 inspect.getfile(type)
67 67 with pytest.raises(OSError if sys.version_info >= (3, 10) else TypeError):
68 68 inspect.getfile(SourceModuleMainTest)
69 69
70 70
71 71 # A couple of utilities to ensure these tests work the same from a source or a
72 72 # binary install
73 73 def pyfile(fname):
74 74 return os.path.normcase(re.sub('.py[co]$', '.py', fname))
75 75
76 76
77 77 def match_pyfiles(f1, f2):
78 78 assert pyfile(f1) == pyfile(f2)
79 79
80 80
81 81 def test_find_file():
82 82 match_pyfiles(oinspect.find_file(test_find_file), os.path.abspath(__file__))
83 83 assert oinspect.find_file(type) is None
84 84 assert oinspect.find_file(SourceModuleMainTest) is None
85 85 assert oinspect.find_file(SourceModuleMainTest()) is None
86 86
87 87
88 88 def test_find_file_decorated1():
89 89
90 90 @decorator
91 91 def noop1(f):
92 92 def wrapper(*a, **kw):
93 93 return f(*a, **kw)
94 94 return wrapper
95 95
96 96 @noop1
97 97 def f(x):
98 98 "My docstring"
99 99
100 100 match_pyfiles(oinspect.find_file(f), os.path.abspath(__file__))
101 101 assert f.__doc__ == "My docstring"
102 102
103 103
104 104 def test_find_file_decorated2():
105 105
106 106 @decorator
107 107 def noop2(f, *a, **kw):
108 108 return f(*a, **kw)
109 109
110 110 @noop2
111 111 @noop2
112 112 @noop2
113 113 def f(x):
114 114 "My docstring 2"
115 115
116 116 match_pyfiles(oinspect.find_file(f), os.path.abspath(__file__))
117 117 assert f.__doc__ == "My docstring 2"
118 118
119 119
120 120 def test_find_file_magic():
121 121 run = ip.find_line_magic('run')
122 122 assert oinspect.find_file(run) is not None
123 123
124 124
125 125 # A few generic objects we can then inspect in the tests below
126 126
127 127 class Call(object):
128 128 """This is the class docstring."""
129 129
130 130 def __init__(self, x, y=1):
131 131 """This is the constructor docstring."""
132 132
133 133 def __call__(self, *a, **kw):
134 134 """This is the call docstring."""
135 135
136 136 def method(self, x, z=2):
137 137 """Some method's docstring"""
138 138
139 139 class HasSignature(object):
140 140 """This is the class docstring."""
141 141 __signature__ = Signature([Parameter('test', Parameter.POSITIONAL_OR_KEYWORD)])
142 142
143 143 def __init__(self, *args):
144 144 """This is the init docstring"""
145 145
146 146
147 147 class SimpleClass(object):
148 148 def method(self, x, z=2):
149 149 """Some method's docstring"""
150 150
151 151
152 152 class Awkward(object):
153 153 def __getattr__(self, name):
154 154 raise Exception(name)
155 155
156 156 class NoBoolCall:
157 157 """
158 158 callable with `__bool__` raising should still be inspect-able.
159 159 """
160 160
161 161 def __call__(self):
162 162 """does nothing"""
163 163 pass
164 164
165 165 def __bool__(self):
166 166 """just raise NotImplemented"""
167 167 raise NotImplementedError('Must be implemented')
168 168
169 169
170 170 class SerialLiar(object):
171 171 """Attribute accesses always get another copy of the same class.
172 172
173 173 unittest.mock.call does something similar, but it's not ideal for testing
174 174 as the failure mode is to eat all your RAM. This gives up after 10k levels.
175 175 """
176 176 def __init__(self, max_fibbing_twig, lies_told=0):
177 177 if lies_told > 10000:
178 178 raise RuntimeError('Nose too long, honesty is the best policy')
179 179 self.max_fibbing_twig = max_fibbing_twig
180 180 self.lies_told = lies_told
181 181 max_fibbing_twig[0] = max(max_fibbing_twig[0], lies_told)
182 182
183 183 def __getattr__(self, item):
184 184 return SerialLiar(self.max_fibbing_twig, self.lies_told + 1)
185 185
186 186 #-----------------------------------------------------------------------------
187 187 # Tests
188 188 #-----------------------------------------------------------------------------
189 189
190 190 def test_info():
191 191 "Check that Inspector.info fills out various fields as expected."
192 192 i = inspector.info(Call, oname="Call")
193 193 assert i["type_name"] == "type"
194 194 expected_class = str(type(type)) # <class 'type'> (Python 3) or <type 'type'>
195 195 assert i["base_class"] == expected_class
196 196 assert re.search(
197 197 "<class 'IPython.core.tests.test_oinspect.Call'( at 0x[0-9a-f]{1,9})?>",
198 198 i["string_form"],
199 199 )
200 200 fname = __file__
201 201 if fname.endswith(".pyc"):
202 202 fname = fname[:-1]
203 203 # case-insensitive comparison needed on some filesystems
204 204 # e.g. Windows:
205 205 assert i["file"].lower() == compress_user(fname).lower()
206 206 assert i["definition"] == None
207 207 assert i["docstring"] == Call.__doc__
208 208 assert i["source"] == None
209 209 assert i["isclass"] is True
210 210 assert i["init_definition"] == "Call(x, y=1)"
211 211 assert i["init_docstring"] == Call.__init__.__doc__
212 212
213 213 i = inspector.info(Call, detail_level=1)
214 214 assert i["source"] is not None
215 215 assert i["docstring"] == None
216 216
217 217 c = Call(1)
218 218 c.__doc__ = "Modified instance docstring"
219 219 i = inspector.info(c)
220 220 assert i["type_name"] == "Call"
221 221 assert i["docstring"] == "Modified instance docstring"
222 222 assert i["class_docstring"] == Call.__doc__
223 223 assert i["init_docstring"] == Call.__init__.__doc__
224 224 assert i["call_docstring"] == Call.__call__.__doc__
225 225
226 226
227 227 def test_class_signature():
228 228 info = inspector.info(HasSignature, "HasSignature")
229 229 assert info["init_definition"] == "HasSignature(test)"
230 230 assert info["init_docstring"] == HasSignature.__init__.__doc__
231 231
232 232
233 233 def test_info_awkward():
234 234 # Just test that this doesn't throw an error.
235 235 inspector.info(Awkward())
236 236
237 237 def test_bool_raise():
238 238 inspector.info(NoBoolCall())
239 239
240 240 def test_info_serialliar():
241 241 fib_tracker = [0]
242 242 inspector.info(SerialLiar(fib_tracker))
243 243
244 244 # Nested attribute access should be cut off at 100 levels deep to avoid
245 245 # infinite loops: https://github.com/ipython/ipython/issues/9122
246 246 assert fib_tracker[0] < 9000
247 247
248 248 def support_function_one(x, y=2, *a, **kw):
249 249 """A simple function."""
250 250
251 251 def test_calldef_none():
252 252 # We should ignore __call__ for all of these.
253 253 for obj in [support_function_one, SimpleClass().method, any, str.upper]:
254 254 i = inspector.info(obj)
255 255 assert i["call_def"] is None
256 256
257 257
258 258 def f_kwarg(pos, *, kwonly):
259 259 pass
260 260
261 261 def test_definition_kwonlyargs():
262 262 i = inspector.info(f_kwarg, oname="f_kwarg") # analysis:ignore
263 263 assert i["definition"] == "f_kwarg(pos, *, kwonly)"
264 264
265 265
266 266 def test_getdoc():
267 267 class A(object):
268 268 """standard docstring"""
269 269 pass
270 270
271 271 class B(object):
272 272 """standard docstring"""
273 273 def getdoc(self):
274 274 return "custom docstring"
275 275
276 276 class C(object):
277 277 """standard docstring"""
278 278 def getdoc(self):
279 279 return None
280 280
281 281 a = A()
282 282 b = B()
283 283 c = C()
284 284
285 285 assert oinspect.getdoc(a) == "standard docstring"
286 286 assert oinspect.getdoc(b) == "custom docstring"
287 287 assert oinspect.getdoc(c) == "standard docstring"
288 288
289 289
290 290 def test_empty_property_has_no_source():
291 291 i = inspector.info(property(), detail_level=1)
292 292 assert i["source"] is None
293 293
294 294
295 295 def test_property_sources():
296 296 # A simple adder whose source and signature stays
297 297 # the same across Python distributions
298 298 def simple_add(a, b):
299 299 "Adds two numbers"
300 300 return a + b
301 301
302 302 class A(object):
303 303 @property
304 304 def foo(self):
305 305 return 'bar'
306 306
307 307 foo = foo.setter(lambda self, v: setattr(self, 'bar', v))
308 308
309 309 dname = property(oinspect.getdoc)
310 310 adder = property(simple_add)
311 311
312 312 i = inspector.info(A.foo, detail_level=1)
313 313 assert "def foo(self):" in i["source"]
314 314 assert "lambda self, v:" in i["source"]
315 315
316 316 i = inspector.info(A.dname, detail_level=1)
317 317 assert "def getdoc(obj)" in i["source"]
318 318
319 319 i = inspector.info(A.adder, detail_level=1)
320 320 assert "def simple_add(a, b)" in i["source"]
321 321
322 322
323 323 def test_property_docstring_is_in_info_for_detail_level_0():
324 324 class A(object):
325 325 @property
326 326 def foobar(self):
327 327 """This is `foobar` property."""
328 328 pass
329 329
330 330 ip.user_ns["a_obj"] = A()
331 331 assert (
332 332 "This is `foobar` property."
333 333 == ip.object_inspect("a_obj.foobar", detail_level=0)["docstring"]
334 334 )
335 335
336 336 ip.user_ns["a_cls"] = A
337 337 assert (
338 338 "This is `foobar` property."
339 339 == ip.object_inspect("a_cls.foobar", detail_level=0)["docstring"]
340 340 )
341 341
342 342
343 343 def test_pdef():
344 344 # See gh-1914
345 345 def foo(): pass
346 346 inspector.pdef(foo, 'foo')
347 347
348 348
349 349 @contextmanager
350 350 def cleanup_user_ns(**kwargs):
351 351 """
352 352 On exit delete all the keys that were not in user_ns before entering.
353 353
354 354 It does not restore old values !
355 355
356 356 Parameters
357 357 ----------
358 358
359 359 **kwargs
360 360 used to update ip.user_ns
361 361
362 362 """
363 363 try:
364 364 known = set(ip.user_ns.keys())
365 365 ip.user_ns.update(kwargs)
366 366 yield
367 367 finally:
368 368 added = set(ip.user_ns.keys()) - known
369 369 for k in added:
370 370 del ip.user_ns[k]
371 371
372 372
373 373 def test_pinfo_getindex():
374 374 def dummy():
375 375 """
376 376 MARKER
377 377 """
378 378
379 379 container = [dummy]
380 380 with cleanup_user_ns(container=container):
381 381 with AssertPrints("MARKER"):
382 382 ip._inspect("pinfo", "container[0]", detail_level=0)
383 383 assert "container" not in ip.user_ns.keys()
384 384
385 385
386 386 def test_qmark_getindex():
387 387 def dummy():
388 388 """
389 389 MARKER 2
390 390 """
391 391
392 392 container = [dummy]
393 393 with cleanup_user_ns(container=container):
394 394 with AssertPrints("MARKER 2"):
395 395 ip.run_cell("container[0]?")
396 396 assert "container" not in ip.user_ns.keys()
397 397
398 398
399 399 def test_qmark_getindex_negatif():
400 400 def dummy():
401 401 """
402 402 MARKER 3
403 403 """
404 404
405 405 container = [dummy]
406 406 with cleanup_user_ns(container=container):
407 407 with AssertPrints("MARKER 3"):
408 408 ip.run_cell("container[-1]?")
409 409 assert "container" not in ip.user_ns.keys()
410 410
411 411
412 412
413 413 def test_pinfo_nonascii():
414 414 # See gh-1177
415 415 from . import nonascii2
416 416 ip.user_ns['nonascii2'] = nonascii2
417 417 ip._inspect('pinfo', 'nonascii2', detail_level=1)
418 418
419 419 def test_pinfo_type():
420 420 """
421 421 type can fail in various edge case, for example `type.__subclass__()`
422 422 """
423 423 ip._inspect('pinfo', 'type')
424 424
425 425
426 426 def test_pinfo_docstring_no_source():
427 427 """Docstring should be included with detail_level=1 if there is no source"""
428 428 with AssertPrints('Docstring:'):
429 429 ip._inspect('pinfo', 'str.format', detail_level=0)
430 430 with AssertPrints('Docstring:'):
431 431 ip._inspect('pinfo', 'str.format', detail_level=1)
432 432
433 433
434 434 def test_pinfo_no_docstring_if_source():
435 435 """Docstring should not be included with detail_level=1 if source is found"""
436 436 def foo():
437 437 """foo has a docstring"""
438 438
439 439 ip.user_ns['foo'] = foo
440 440
441 441 with AssertPrints('Docstring:'):
442 442 ip._inspect('pinfo', 'foo', detail_level=0)
443 443 with AssertPrints('Source:'):
444 444 ip._inspect('pinfo', 'foo', detail_level=1)
445 445 with AssertNotPrints('Docstring:'):
446 446 ip._inspect('pinfo', 'foo', detail_level=1)
447 447
448 448
449 449 def test_pinfo_docstring_if_detail_and_no_source():
450 450 """ Docstring should be displayed if source info not available """
451 451 obj_def = '''class Foo(object):
452 452 """ This is a docstring for Foo """
453 453 def bar(self):
454 454 """ This is a docstring for Foo.bar """
455 455 pass
456 456 '''
457 457
458 458 ip.run_cell(obj_def)
459 459 ip.run_cell('foo = Foo()')
460 460
461 461 with AssertNotPrints("Source:"):
462 462 with AssertPrints('Docstring:'):
463 463 ip._inspect('pinfo', 'foo', detail_level=0)
464 464 with AssertPrints('Docstring:'):
465 465 ip._inspect('pinfo', 'foo', detail_level=1)
466 466 with AssertPrints('Docstring:'):
467 467 ip._inspect('pinfo', 'foo.bar', detail_level=0)
468 468
469 469 with AssertNotPrints('Docstring:'):
470 470 with AssertPrints('Source:'):
471 471 ip._inspect('pinfo', 'foo.bar', detail_level=1)
472 472
473 473
474 474 def test_pinfo_magic():
475 475 with AssertPrints('Docstring:'):
476 476 ip._inspect('pinfo', 'lsmagic', detail_level=0)
477 477
478 478 with AssertPrints('Source:'):
479 479 ip._inspect('pinfo', 'lsmagic', detail_level=1)
480 480
481 481
482 482 def test_init_colors():
483 483 # ensure colors are not present in signature info
484 484 info = inspector.info(HasSignature)
485 485 init_def = info["init_definition"]
486 486 assert "[0m" not in init_def
487 487
488 488
489 489 def test_builtin_init():
490 490 info = inspector.info(list)
491 491 init_def = info['init_definition']
492 492 assert init_def is not None
493 493
494 494
495 495 def test_render_signature_short():
496 496 def short_fun(a=1): pass
497 497 sig = oinspect._render_signature(
498 498 signature(short_fun),
499 499 short_fun.__name__,
500 500 )
501 501 assert sig == "short_fun(a=1)"
502 502
503 503
504 504 def test_render_signature_long():
505 505 from typing import Optional
506 506
507 507 def long_function(
508 508 a_really_long_parameter: int,
509 509 and_another_long_one: bool = False,
510 510 let_us_make_sure_this_is_looong: Optional[str] = None,
511 511 ) -> bool: pass
512 512
513 513 sig = oinspect._render_signature(
514 514 signature(long_function),
515 515 long_function.__name__,
516 516 )
517 assert sig in [
518 # Python >=3.9
519 '''\
517 if sys.version_info >= (3, 9):
518 expected = """\
520 519 long_function(
521 520 a_really_long_parameter: int,
522 521 and_another_long_one: bool = False,
523 522 let_us_make_sure_this_is_looong: Optional[str] = None,
524 523 ) -> bool\
525 ''',
526 # Python >=3.7
527 '''\
528 long_function(
529 a_really_long_parameter: int,
530 and_another_long_one: bool = False,
531 let_us_make_sure_this_is_looong: Union[str, NoneType] = None,
532 ) -> bool\
533 ''', # Python <=3.6
534 '''\
524 """
525 else:
526 expected = """\
535 527 long_function(
536 528 a_really_long_parameter:int,
537 529 and_another_long_one:bool=False,
538 530 let_us_make_sure_this_is_looong:Union[str, NoneType]=None,
539 531 ) -> bool\
540 ''',
541 ]
532 """
533 assert sig == expected
General Comments 0
You need to be logged in to leave comments. Login now