##// END OF EJS Templates
Relax string checks in test_pinfo_docstring_dynamic...
Thomas Kluyver -
Show More
@@ -1,589 +1,589 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):
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_bool_raise():
374 374 """
375 375 Test that bool method is not called on parent.
376 376 """
377 377
378 378 class RaiseBool:
379 379 attr = None
380 380
381 381 def __bool__(self):
382 382 raise ValueError("pinfo should not access this method")
383 383
384 384 raise_bool = RaiseBool()
385 385
386 386 with cleanup_user_ns(raise_bool=raise_bool):
387 387 ip._inspect("pinfo", "raise_bool.attr", detail_level=0)
388 388
389 389
390 390 def test_pinfo_getindex():
391 391 def dummy():
392 392 """
393 393 MARKER
394 394 """
395 395
396 396 container = [dummy]
397 397 with cleanup_user_ns(container=container):
398 398 with AssertPrints("MARKER"):
399 399 ip._inspect("pinfo", "container[0]", detail_level=0)
400 400 assert "container" not in ip.user_ns.keys()
401 401
402 402
403 403 def test_qmark_getindex():
404 404 def dummy():
405 405 """
406 406 MARKER 2
407 407 """
408 408
409 409 container = [dummy]
410 410 with cleanup_user_ns(container=container):
411 411 with AssertPrints("MARKER 2"):
412 412 ip.run_cell("container[0]?")
413 413 assert "container" not in ip.user_ns.keys()
414 414
415 415
416 416 def test_qmark_getindex_negatif():
417 417 def dummy():
418 418 """
419 419 MARKER 3
420 420 """
421 421
422 422 container = [dummy]
423 423 with cleanup_user_ns(container=container):
424 424 with AssertPrints("MARKER 3"):
425 425 ip.run_cell("container[-1]?")
426 426 assert "container" not in ip.user_ns.keys()
427 427
428 428
429 429
430 430 def test_pinfo_nonascii():
431 431 # See gh-1177
432 432 from . import nonascii2
433 433 ip.user_ns['nonascii2'] = nonascii2
434 434 ip._inspect('pinfo', 'nonascii2', detail_level=1)
435 435
436 436 def test_pinfo_type():
437 437 """
438 438 type can fail in various edge case, for example `type.__subclass__()`
439 439 """
440 440 ip._inspect('pinfo', 'type')
441 441
442 442
443 443 def test_pinfo_docstring_no_source():
444 444 """Docstring should be included with detail_level=1 if there is no source"""
445 445 with AssertPrints('Docstring:'):
446 446 ip._inspect('pinfo', 'str.format', detail_level=0)
447 447 with AssertPrints('Docstring:'):
448 448 ip._inspect('pinfo', 'str.format', detail_level=1)
449 449
450 450
451 451 def test_pinfo_no_docstring_if_source():
452 452 """Docstring should not be included with detail_level=1 if source is found"""
453 453 def foo():
454 454 """foo has a docstring"""
455 455
456 456 ip.user_ns['foo'] = foo
457 457
458 458 with AssertPrints('Docstring:'):
459 459 ip._inspect('pinfo', 'foo', detail_level=0)
460 460 with AssertPrints('Source:'):
461 461 ip._inspect('pinfo', 'foo', detail_level=1)
462 462 with AssertNotPrints('Docstring:'):
463 463 ip._inspect('pinfo', 'foo', detail_level=1)
464 464
465 465
466 466 def test_pinfo_docstring_if_detail_and_no_source():
467 467 """ Docstring should be displayed if source info not available """
468 468 obj_def = '''class Foo(object):
469 469 """ This is a docstring for Foo """
470 470 def bar(self):
471 471 """ This is a docstring for Foo.bar """
472 472 pass
473 473 '''
474 474
475 475 ip.run_cell(obj_def)
476 476 ip.run_cell('foo = Foo()')
477 477
478 478 with AssertNotPrints("Source:"):
479 479 with AssertPrints('Docstring:'):
480 480 ip._inspect('pinfo', 'foo', detail_level=0)
481 481 with AssertPrints('Docstring:'):
482 482 ip._inspect('pinfo', 'foo', detail_level=1)
483 483 with AssertPrints('Docstring:'):
484 484 ip._inspect('pinfo', 'foo.bar', detail_level=0)
485 485
486 486 with AssertNotPrints('Docstring:'):
487 487 with AssertPrints('Source:'):
488 488 ip._inspect('pinfo', 'foo.bar', detail_level=1)
489 489
490 490
491 491 @pytest.mark.xfail(
492 492 sys.version_info.releaselevel not in ("final", "candidate"),
493 493 reason="fails on 3.13.dev",
494 494 strict=True,
495 495 )
496 496 def test_pinfo_docstring_dynamic(capsys):
497 497 obj_def = """class Bar:
498 498 __custom_documentations__ = {
499 499 "prop" : "cdoc for prop",
500 500 "non_exist" : "cdoc for non_exist",
501 501 }
502 502 @property
503 503 def prop(self):
504 504 '''
505 505 Docstring for prop
506 506 '''
507 507 return self._prop
508 508
509 509 @prop.setter
510 510 def prop(self, v):
511 511 self._prop = v
512 512 """
513 513 ip.run_cell(obj_def)
514 514
515 515 ip.run_cell("b = Bar()")
516 516
517 517 ip.run_line_magic("pinfo", "b.prop")
518 518 captured = capsys.readouterr()
519 assert "Docstring: cdoc for prop" in captured.out
519 assert re.search(r"Docstring:\s+cdoc for prop", captured.out)
520 520
521 521 ip.run_line_magic("pinfo", "b.non_exist")
522 522 captured = capsys.readouterr()
523 assert "Docstring: cdoc for non_exist" in captured.out
523 assert re.search(r"Docstring:\s+cdoc for non_exist", captured.out)
524 524
525 525 ip.run_cell("b.prop?")
526 526 captured = capsys.readouterr()
527 assert "Docstring: cdoc for prop" in captured.out
527 assert re.search(r"Docstring:\s+cdoc for prop", captured.out)
528 528
529 529 ip.run_cell("b.non_exist?")
530 530 captured = capsys.readouterr()
531 assert "Docstring: cdoc for non_exist" in captured.out
531 assert re.search(r"Docstring:\s+cdoc for non_exist", captured.out)
532 532
533 533 ip.run_cell("b.undefined?")
534 534 captured = capsys.readouterr()
535 assert "Docstring: <no docstring>" in captured.out
535 assert re.search(r"Type:\s+NoneType", captured.out)
536 536
537 537
538 538 def test_pinfo_magic():
539 539 with AssertPrints("Docstring:"):
540 540 ip._inspect("pinfo", "lsmagic", detail_level=0)
541 541
542 542 with AssertPrints("Source:"):
543 543 ip._inspect("pinfo", "lsmagic", detail_level=1)
544 544
545 545
546 546 def test_init_colors():
547 547 # ensure colors are not present in signature info
548 548 info = inspector.info(HasSignature)
549 549 init_def = info["init_definition"]
550 550 assert "[0m" not in init_def
551 551
552 552
553 553 def test_builtin_init():
554 554 info = inspector.info(list)
555 555 init_def = info['init_definition']
556 556 assert init_def is not None
557 557
558 558
559 559 def test_render_signature_short():
560 560 def short_fun(a=1): pass
561 561 sig = oinspect._render_signature(
562 562 signature(short_fun),
563 563 short_fun.__name__,
564 564 )
565 565 assert sig == "short_fun(a=1)"
566 566
567 567
568 568 def test_render_signature_long():
569 569 from typing import Optional
570 570
571 571 def long_function(
572 572 a_really_long_parameter: int,
573 573 and_another_long_one: bool = False,
574 574 let_us_make_sure_this_is_looong: Optional[str] = None,
575 575 ) -> bool: pass
576 576
577 577 sig = oinspect._render_signature(
578 578 signature(long_function),
579 579 long_function.__name__,
580 580 )
581 581 expected = """\
582 582 long_function(
583 583 a_really_long_parameter: int,
584 584 and_another_long_one: bool = False,
585 585 let_us_make_sure_this_is_looong: Optional[str] = None,
586 586 ) -> bool\
587 587 """
588 588
589 589 assert sig == expected
General Comments 0
You need to be logged in to leave comments. Login now