##// END OF EJS Templates
Add hooking ability for producing help mime bundle. (#14300)...
M Bussonnier -
r28592:6e5ec4db merge
parent child Browse files
Show More
@@ -1,1170 +1,1183
1 # -*- coding: utf-8 -*-
2 1 """Tools for inspecting Python objects.
3 2
4 3 Uses syntax highlighting for presenting the various information elements.
5 4
6 5 Similar in spirit to the inspect module, but all calls take a name argument to
7 6 reference the name under which an object is being read.
8 7 """
9 8
10 9 # Copyright (c) IPython Development Team.
11 10 # Distributed under the terms of the Modified BSD License.
12 11
13 12 __all__ = ['Inspector','InspectColors']
14 13
15 14 # stdlib modules
16 15 from dataclasses import dataclass
17 16 from inspect import signature
18 17 from textwrap import dedent
19 18 import ast
20 19 import html
21 20 import inspect
22 21 import io as stdlib_io
23 22 import linecache
24 23 import os
25 import sys
26 24 import types
27 25 import warnings
28 26
29 27 from typing import Any, Optional, Dict, Union, List, Tuple
30 28
31 29 from typing import TypeAlias
32 30
31 import traitlets
32
33 33 # IPython's own
34 34 from IPython.core import page
35 35 from IPython.lib.pretty import pretty
36 36 from IPython.testing.skipdoctest import skip_doctest
37 37 from IPython.utils import PyColorize
38 38 from IPython.utils import openpy
39 39 from IPython.utils.dir2 import safe_hasattr
40 40 from IPython.utils.path import compress_user
41 41 from IPython.utils.text import indent
42 42 from IPython.utils.wildcard import list_namespace
43 43 from IPython.utils.wildcard import typestr2type
44 from IPython.utils.coloransi import TermColors, ColorScheme, ColorSchemeTable
44 from IPython.utils.coloransi import TermColors
45 45 from IPython.utils.py3compat import cast_unicode
46 46 from IPython.utils.colorable import Colorable
47 47 from IPython.utils.decorators import undoc
48 48
49 49 from pygments import highlight
50 50 from pygments.lexers import PythonLexer
51 51 from pygments.formatters import HtmlFormatter
52 52
53 53 HOOK_NAME = "__custom_documentations__"
54 54
55 55
56 56 UnformattedBundle: TypeAlias = Dict[str, List[Tuple[str, str]]] # List of (title, body)
57 57 Bundle: TypeAlias = Dict[str, str]
58 58
59 59
60 60 @dataclass
61 61 class OInfo:
62 62 ismagic: bool
63 63 isalias: bool
64 64 found: bool
65 65 namespace: Optional[str]
66 66 parent: Any
67 67 obj: Any
68 68
69 69 def get(self, field):
70 70 """Get a field from the object for backward compatibility with before 8.12
71 71
72 72 see https://github.com/h5py/h5py/issues/2253
73 73 """
74 74 # We need to deprecate this at some point, but the warning will show in completion.
75 75 # Let's comment this for now and uncomment end of 2023 ish
76 76 # warnings.warn(
77 77 # f"OInfo dataclass with fields access since IPython 8.12 please use OInfo.{field} instead."
78 78 # "OInfo used to be a dict but a dataclass provide static fields verification with mypy."
79 79 # "This warning and backward compatibility `get()` method were added in 8.13.",
80 80 # DeprecationWarning,
81 81 # stacklevel=2,
82 82 # )
83 83 return getattr(self, field)
84 84
85 85
86 86 def pylight(code):
87 87 return highlight(code, PythonLexer(), HtmlFormatter(noclasses=True))
88 88
89 89 # builtin docstrings to ignore
90 90 _func_call_docstring = types.FunctionType.__call__.__doc__
91 91 _object_init_docstring = object.__init__.__doc__
92 92 _builtin_type_docstrings = {
93 93 inspect.getdoc(t) for t in (types.ModuleType, types.MethodType,
94 94 types.FunctionType, property)
95 95 }
96 96
97 97 _builtin_func_type = type(all)
98 98 _builtin_meth_type = type(str.upper) # Bound methods have the same type as builtin functions
99 99 #****************************************************************************
100 100 # Builtin color schemes
101 101
102 102 Colors = TermColors # just a shorthand
103 103
104 104 InspectColors = PyColorize.ANSICodeColors
105 105
106 106 #****************************************************************************
107 107 # Auxiliary functions and objects
108 108
109 109 # See the messaging spec for the definition of all these fields. This list
110 110 # effectively defines the order of display
111 111 info_fields = ['type_name', 'base_class', 'string_form', 'namespace',
112 112 'length', 'file', 'definition', 'docstring', 'source',
113 113 'init_definition', 'class_docstring', 'init_docstring',
114 114 'call_def', 'call_docstring',
115 115 # These won't be printed but will be used to determine how to
116 116 # format the object
117 117 'ismagic', 'isalias', 'isclass', 'found', 'name'
118 118 ]
119 119
120 120
121 121 def object_info(**kw):
122 122 """Make an object info dict with all fields present."""
123 123 infodict = {k:None for k in info_fields}
124 124 infodict.update(kw)
125 125 return infodict
126 126
127 127
128 128 def get_encoding(obj):
129 129 """Get encoding for python source file defining obj
130 130
131 131 Returns None if obj is not defined in a sourcefile.
132 132 """
133 133 ofile = find_file(obj)
134 134 # run contents of file through pager starting at line where the object
135 135 # is defined, as long as the file isn't binary and is actually on the
136 136 # filesystem.
137 137 if ofile is None:
138 138 return None
139 139 elif ofile.endswith(('.so', '.dll', '.pyd')):
140 140 return None
141 141 elif not os.path.isfile(ofile):
142 142 return None
143 143 else:
144 144 # Print only text files, not extension binaries. Note that
145 145 # getsourcelines returns lineno with 1-offset and page() uses
146 146 # 0-offset, so we must adjust.
147 147 with stdlib_io.open(ofile, 'rb') as buffer: # Tweaked to use io.open for Python 2
148 encoding, lines = openpy.detect_encoding(buffer.readline)
148 encoding, _lines = openpy.detect_encoding(buffer.readline)
149 149 return encoding
150 150
151 151 def getdoc(obj) -> Union[str,None]:
152 152 """Stable wrapper around inspect.getdoc.
153 153
154 154 This can't crash because of attribute problems.
155 155
156 156 It also attempts to call a getdoc() method on the given object. This
157 157 allows objects which provide their docstrings via non-standard mechanisms
158 158 (like Pyro proxies) to still be inspected by ipython's ? system.
159 159 """
160 160 # Allow objects to offer customized documentation via a getdoc method:
161 161 try:
162 162 ds = obj.getdoc()
163 163 except Exception:
164 164 pass
165 165 else:
166 166 if isinstance(ds, str):
167 167 return inspect.cleandoc(ds)
168 168 docstr = inspect.getdoc(obj)
169 169 return docstr
170 170
171 171
172 172 def getsource(obj, oname='') -> Union[str,None]:
173 173 """Wrapper around inspect.getsource.
174 174
175 175 This can be modified by other projects to provide customized source
176 176 extraction.
177 177
178 178 Parameters
179 179 ----------
180 180 obj : object
181 181 an object whose source code we will attempt to extract
182 182 oname : str
183 183 (optional) a name under which the object is known
184 184
185 185 Returns
186 186 -------
187 187 src : unicode or None
188 188
189 189 """
190 190
191 191 if isinstance(obj, property):
192 192 sources = []
193 193 for attrname in ['fget', 'fset', 'fdel']:
194 194 fn = getattr(obj, attrname)
195 195 if fn is not None:
196 196 encoding = get_encoding(fn)
197 197 oname_prefix = ('%s.' % oname) if oname else ''
198 198 sources.append(''.join(('# ', oname_prefix, attrname)))
199 199 if inspect.isfunction(fn):
200 200 _src = getsource(fn)
201 201 if _src:
202 202 # assert _src is not None, "please mypy"
203 203 sources.append(dedent(_src))
204 204 else:
205 205 # Default str/repr only prints function name,
206 206 # pretty.pretty prints module name too.
207 207 sources.append(
208 208 '%s%s = %s\n' % (oname_prefix, attrname, pretty(fn))
209 209 )
210 210 if sources:
211 211 return '\n'.join(sources)
212 212 else:
213 213 return None
214 214
215 215 else:
216 216 # Get source for non-property objects.
217 217
218 218 obj = _get_wrapped(obj)
219 219
220 220 try:
221 221 src = inspect.getsource(obj)
222 222 except TypeError:
223 223 # The object itself provided no meaningful source, try looking for
224 224 # its class definition instead.
225 225 try:
226 226 src = inspect.getsource(obj.__class__)
227 227 except (OSError, TypeError):
228 228 return None
229 229 except OSError:
230 230 return None
231 231
232 232 return src
233 233
234 234
235 235 def is_simple_callable(obj):
236 236 """True if obj is a function ()"""
237 237 return (inspect.isfunction(obj) or inspect.ismethod(obj) or \
238 238 isinstance(obj, _builtin_func_type) or isinstance(obj, _builtin_meth_type))
239 239
240 240 @undoc
241 241 def getargspec(obj):
242 242 """Wrapper around :func:`inspect.getfullargspec`
243 243
244 244 In addition to functions and methods, this can also handle objects with a
245 245 ``__call__`` attribute.
246 246
247 247 DEPRECATED: Deprecated since 7.10. Do not use, will be removed.
248 248 """
249 249
250 250 warnings.warn('`getargspec` function is deprecated as of IPython 7.10'
251 251 'and will be removed in future versions.', DeprecationWarning, stacklevel=2)
252 252
253 253 if safe_hasattr(obj, '__call__') and not is_simple_callable(obj):
254 254 obj = obj.__call__
255 255
256 256 return inspect.getfullargspec(obj)
257 257
258 258 @undoc
259 259 def format_argspec(argspec):
260 260 """Format argspect, convenience wrapper around inspect's.
261 261
262 262 This takes a dict instead of ordered arguments and calls
263 263 inspect.format_argspec with the arguments in the necessary order.
264 264
265 265 DEPRECATED (since 7.10): Do not use; will be removed in future versions.
266 266 """
267 267
268 268 warnings.warn('`format_argspec` function is deprecated as of IPython 7.10'
269 269 'and will be removed in future versions.', DeprecationWarning, stacklevel=2)
270 270
271 271
272 272 return inspect.formatargspec(argspec['args'], argspec['varargs'],
273 273 argspec['varkw'], argspec['defaults'])
274 274
275 275 @undoc
276 276 def call_tip(oinfo, format_call=True):
277 277 """DEPRECATED since 6.0. Extract call tip data from an oinfo dict."""
278 278 warnings.warn(
279 279 "`call_tip` function is deprecated as of IPython 6.0"
280 280 "and will be removed in future versions.",
281 281 DeprecationWarning,
282 282 stacklevel=2,
283 283 )
284 284 # Get call definition
285 285 argspec = oinfo.get('argspec')
286 286 if argspec is None:
287 287 call_line = None
288 288 else:
289 289 # Callable objects will have 'self' as their first argument, prune
290 290 # it out if it's there for clarity (since users do *not* pass an
291 291 # extra first argument explicitly).
292 292 try:
293 293 has_self = argspec['args'][0] == 'self'
294 294 except (KeyError, IndexError):
295 295 pass
296 296 else:
297 297 if has_self:
298 298 argspec['args'] = argspec['args'][1:]
299 299
300 300 call_line = oinfo['name']+format_argspec(argspec)
301 301
302 302 # Now get docstring.
303 303 # The priority is: call docstring, constructor docstring, main one.
304 304 doc = oinfo.get('call_docstring')
305 305 if doc is None:
306 306 doc = oinfo.get('init_docstring')
307 307 if doc is None:
308 308 doc = oinfo.get('docstring','')
309 309
310 310 return call_line, doc
311 311
312 312
313 313 def _get_wrapped(obj):
314 314 """Get the original object if wrapped in one or more @decorators
315 315
316 316 Some objects automatically construct similar objects on any unrecognised
317 317 attribute access (e.g. unittest.mock.call). To protect against infinite loops,
318 318 this will arbitrarily cut off after 100 levels of obj.__wrapped__
319 319 attribute access. --TK, Jan 2016
320 320 """
321 321 orig_obj = obj
322 322 i = 0
323 323 while safe_hasattr(obj, '__wrapped__'):
324 324 obj = obj.__wrapped__
325 325 i += 1
326 326 if i > 100:
327 327 # __wrapped__ is probably a lie, so return the thing we started with
328 328 return orig_obj
329 329 return obj
330 330
331 def find_file(obj) -> str:
331 def find_file(obj) -> Optional[str]:
332 332 """Find the absolute path to the file where an object was defined.
333 333
334 334 This is essentially a robust wrapper around `inspect.getabsfile`.
335 335
336 336 Returns None if no file can be found.
337 337
338 338 Parameters
339 339 ----------
340 340 obj : any Python object
341 341
342 342 Returns
343 343 -------
344 344 fname : str
345 345 The absolute path to the file where the object was defined.
346 346 """
347 347 obj = _get_wrapped(obj)
348 348
349 fname = None
349 fname: Optional[str] = None
350 350 try:
351 351 fname = inspect.getabsfile(obj)
352 352 except TypeError:
353 353 # For an instance, the file that matters is where its class was
354 354 # declared.
355 355 try:
356 356 fname = inspect.getabsfile(obj.__class__)
357 357 except (OSError, TypeError):
358 358 # Can happen for builtins
359 359 pass
360 360 except OSError:
361 361 pass
362 362
363 return cast_unicode(fname)
363 return fname
364 364
365 365
366 366 def find_source_lines(obj):
367 367 """Find the line number in a file where an object was defined.
368 368
369 369 This is essentially a robust wrapper around `inspect.getsourcelines`.
370 370
371 371 Returns None if no file can be found.
372 372
373 373 Parameters
374 374 ----------
375 375 obj : any Python object
376 376
377 377 Returns
378 378 -------
379 379 lineno : int
380 380 The line number where the object definition starts.
381 381 """
382 382 obj = _get_wrapped(obj)
383 383
384 384 try:
385 385 lineno = inspect.getsourcelines(obj)[1]
386 386 except TypeError:
387 387 # For instances, try the class object like getsource() does
388 388 try:
389 389 lineno = inspect.getsourcelines(obj.__class__)[1]
390 390 except (OSError, TypeError):
391 391 return None
392 392 except OSError:
393 393 return None
394 394
395 395 return lineno
396 396
397 397 class Inspector(Colorable):
398 398
399 def __init__(self, color_table=InspectColors,
399 mime_hooks = traitlets.Dict(
400 config=True,
401 help="dictionary of mime to callable to add informations into help mimebundle dict",
402 ).tag(config=True)
403
404 def __init__(
405 self,
406 color_table=InspectColors,
400 407 code_color_table=PyColorize.ANSICodeColors,
401 408 scheme=None,
402 409 str_detail_level=0,
403 parent=None, config=None):
410 parent=None,
411 config=None,
412 ):
404 413 super(Inspector, self).__init__(parent=parent, config=config)
405 414 self.color_table = color_table
406 415 self.parser = PyColorize.Parser(out='str', parent=self, style=scheme)
407 416 self.format = self.parser.format
408 417 self.str_detail_level = str_detail_level
409 418 self.set_active_scheme(scheme)
410 419
411 420 def _getdef(self,obj,oname='') -> Union[str,None]:
412 421 """Return the call signature for any callable object.
413 422
414 423 If any exception is generated, None is returned instead and the
415 424 exception is suppressed."""
416 425 if not callable(obj):
417 426 return None
418 427 try:
419 428 return _render_signature(signature(obj), oname)
420 429 except:
421 430 return None
422 431
423 432 def __head(self,h) -> str:
424 433 """Return a header string with proper colors."""
425 434 return '%s%s%s' % (self.color_table.active_colors.header,h,
426 435 self.color_table.active_colors.normal)
427 436
428 437 def set_active_scheme(self, scheme):
429 438 if scheme is not None:
430 439 self.color_table.set_active_scheme(scheme)
431 440 self.parser.color_table.set_active_scheme(scheme)
432 441
433 442 def noinfo(self, msg, oname):
434 443 """Generic message when no information is found."""
435 444 print('No %s found' % msg, end=' ')
436 445 if oname:
437 446 print('for %s' % oname)
438 447 else:
439 448 print()
440 449
441 450 def pdef(self, obj, oname=''):
442 451 """Print the call signature for any callable object.
443 452
444 453 If the object is a class, print the constructor information."""
445 454
446 455 if not callable(obj):
447 456 print('Object is not callable.')
448 457 return
449 458
450 459 header = ''
451 460
452 461 if inspect.isclass(obj):
453 462 header = self.__head('Class constructor information:\n')
454 463
455 464
456 465 output = self._getdef(obj,oname)
457 466 if output is None:
458 467 self.noinfo('definition header',oname)
459 468 else:
460 469 print(header,self.format(output), end=' ')
461 470
462 471 # In Python 3, all classes are new-style, so they all have __init__.
463 472 @skip_doctest
464 473 def pdoc(self, obj, oname='', formatter=None):
465 474 """Print the docstring for any object.
466 475
467 476 Optional:
468 477 -formatter: a function to run the docstring through for specially
469 478 formatted docstrings.
470 479
471 480 Examples
472 481 --------
473 482 In [1]: class NoInit:
474 483 ...: pass
475 484
476 485 In [2]: class NoDoc:
477 486 ...: def __init__(self):
478 487 ...: pass
479 488
480 489 In [3]: %pdoc NoDoc
481 490 No documentation found for NoDoc
482 491
483 492 In [4]: %pdoc NoInit
484 493 No documentation found for NoInit
485 494
486 495 In [5]: obj = NoInit()
487 496
488 497 In [6]: %pdoc obj
489 498 No documentation found for obj
490 499
491 500 In [5]: obj2 = NoDoc()
492 501
493 502 In [6]: %pdoc obj2
494 503 No documentation found for obj2
495 504 """
496 505
497 506 head = self.__head # For convenience
498 507 lines = []
499 508 ds = getdoc(obj)
500 509 if formatter:
501 510 ds = formatter(ds).get('plain/text', ds)
502 511 if ds:
503 512 lines.append(head("Class docstring:"))
504 513 lines.append(indent(ds))
505 514 if inspect.isclass(obj) and hasattr(obj, '__init__'):
506 515 init_ds = getdoc(obj.__init__)
507 516 if init_ds is not None:
508 517 lines.append(head("Init docstring:"))
509 518 lines.append(indent(init_ds))
510 519 elif hasattr(obj,'__call__'):
511 520 call_ds = getdoc(obj.__call__)
512 521 if call_ds:
513 522 lines.append(head("Call docstring:"))
514 523 lines.append(indent(call_ds))
515 524
516 525 if not lines:
517 526 self.noinfo('documentation',oname)
518 527 else:
519 528 page.page('\n'.join(lines))
520 529
521 530 def psource(self, obj, oname=''):
522 531 """Print the source code for an object."""
523 532
524 533 # Flush the source cache because inspect can return out-of-date source
525 534 linecache.checkcache()
526 535 try:
527 536 src = getsource(obj, oname=oname)
528 537 except Exception:
529 538 src = None
530 539
531 540 if src is None:
532 541 self.noinfo('source', oname)
533 542 else:
534 543 page.page(self.format(src))
535 544
536 545 def pfile(self, obj, oname=''):
537 546 """Show the whole file where an object was defined."""
538 547
539 548 lineno = find_source_lines(obj)
540 549 if lineno is None:
541 550 self.noinfo('file', oname)
542 551 return
543 552
544 553 ofile = find_file(obj)
545 554 # run contents of file through pager starting at line where the object
546 555 # is defined, as long as the file isn't binary and is actually on the
547 556 # filesystem.
548 557 if ofile.endswith(('.so', '.dll', '.pyd')):
549 558 print('File %r is binary, not printing.' % ofile)
550 559 elif not os.path.isfile(ofile):
551 560 print('File %r does not exist, not printing.' % ofile)
552 561 else:
553 562 # Print only text files, not extension binaries. Note that
554 563 # getsourcelines returns lineno with 1-offset and page() uses
555 564 # 0-offset, so we must adjust.
556 565 page.page(self.format(openpy.read_py_file(ofile, skip_encoding_cookie=False)), lineno - 1)
557 566
558 567
559 568 def _mime_format(self, text:str, formatter=None) -> dict:
560 569 """Return a mime bundle representation of the input text.
561 570
562 571 - if `formatter` is None, the returned mime bundle has
563 572 a ``text/plain`` field, with the input text.
564 573 a ``text/html`` field with a ``<pre>`` tag containing the input text.
565 574
566 575 - if ``formatter`` is not None, it must be a callable transforming the
567 576 input text into a mime bundle. Default values for ``text/plain`` and
568 577 ``text/html`` representations are the ones described above.
569 578
570 579 Note:
571 580
572 581 Formatters returning strings are supported but this behavior is deprecated.
573 582
574 583 """
575 584 defaults = {
576 585 "text/plain": text,
577 586 "text/html": f"<pre>{html.escape(text)}</pre>",
578 587 }
579 588
580 589 if formatter is None:
581 590 return defaults
582 591 else:
583 592 formatted = formatter(text)
584 593
585 594 if not isinstance(formatted, dict):
586 595 # Handle the deprecated behavior of a formatter returning
587 596 # a string instead of a mime bundle.
588 597 return {"text/plain": formatted, "text/html": f"<pre>{formatted}</pre>"}
589 598
590 599 else:
591 600 return dict(defaults, **formatted)
592 601
593 602 def format_mime(self, bundle: UnformattedBundle) -> Bundle:
594 603 """Format a mimebundle being created by _make_info_unformatted into a real mimebundle"""
595 604 # Format text/plain mimetype
596 605 assert isinstance(bundle["text/plain"], list)
597 606 for item in bundle["text/plain"]:
598 607 assert isinstance(item, tuple)
599 608
600 609 new_b: Bundle = {}
601 610 lines = []
602 611 _len = max(len(h) for h, _ in bundle["text/plain"])
603 612
604 613 for head, body in bundle["text/plain"]:
605 614 body = body.strip("\n")
606 615 delim = "\n" if "\n" in body else " "
607 616 lines.append(
608 617 f"{self.__head(head+':')}{(_len - len(head))*' '}{delim}{body}"
609 618 )
610 619
611 620 new_b["text/plain"] = "\n".join(lines)
612 621
613 622 if "text/html" in bundle:
614 623 assert isinstance(bundle["text/html"], list)
615 624 for item in bundle["text/html"]:
616 625 assert isinstance(item, tuple)
617 626 # Format the text/html mimetype
618 627 if isinstance(bundle["text/html"], (list, tuple)):
619 628 # bundle['text/html'] is a list of (head, formatted body) pairs
620 629 new_b["text/html"] = "\n".join(
621 630 (f"<h1>{head}</h1>\n{body}" for (head, body) in bundle["text/html"])
622 631 )
623 632
624 633 for k in bundle.keys():
625 634 if k in ("text/html", "text/plain"):
626 635 continue
627 636 else:
628 637 new_b = bundle[k] # type:ignore
629 638 return new_b
630 639
631 640 def _append_info_field(
632 641 self,
633 642 bundle: UnformattedBundle,
634 643 title: str,
635 644 key: str,
636 645 info,
637 646 omit_sections,
638 647 formatter,
639 648 ):
640 649 """Append an info value to the unformatted mimebundle being constructed by _make_info_unformatted"""
641 650 if title in omit_sections or key in omit_sections:
642 651 return
643 652 field = info[key]
644 653 if field is not None:
645 654 formatted_field = self._mime_format(field, formatter)
646 655 bundle["text/plain"].append((title, formatted_field["text/plain"]))
647 656 bundle["text/html"].append((title, formatted_field["text/html"]))
648 657
649 658 def _make_info_unformatted(
650 659 self, obj, info, formatter, detail_level, omit_sections
651 660 ) -> UnformattedBundle:
652 661 """Assemble the mimebundle as unformatted lists of information"""
653 662 bundle: UnformattedBundle = {
654 663 "text/plain": [],
655 664 "text/html": [],
656 665 }
657 666
658 667 # A convenience function to simplify calls below
659 668 def append_field(
660 669 bundle: UnformattedBundle, title: str, key: str, formatter=None
661 670 ):
662 671 self._append_info_field(
663 672 bundle,
664 673 title=title,
665 674 key=key,
666 675 info=info,
667 676 omit_sections=omit_sections,
668 677 formatter=formatter,
669 678 )
670 679
671 680 def code_formatter(text) -> Bundle:
672 681 return {
673 682 'text/plain': self.format(text),
674 683 'text/html': pylight(text)
675 684 }
676 685
677 686 if info["isalias"]:
678 687 append_field(bundle, "Repr", "string_form")
679 688
680 689 elif info['ismagic']:
681 690 if detail_level > 0:
682 691 append_field(bundle, "Source", "source", code_formatter)
683 692 else:
684 693 append_field(bundle, "Docstring", "docstring", formatter)
685 694 append_field(bundle, "File", "file")
686 695
687 696 elif info['isclass'] or is_simple_callable(obj):
688 697 # Functions, methods, classes
689 698 append_field(bundle, "Signature", "definition", code_formatter)
690 699 append_field(bundle, "Init signature", "init_definition", code_formatter)
691 700 append_field(bundle, "Docstring", "docstring", formatter)
692 701 if detail_level > 0 and info["source"]:
693 702 append_field(bundle, "Source", "source", code_formatter)
694 703 else:
695 704 append_field(bundle, "Init docstring", "init_docstring", formatter)
696 705
697 706 append_field(bundle, "File", "file")
698 707 append_field(bundle, "Type", "type_name")
699 708 append_field(bundle, "Subclasses", "subclasses")
700 709
701 710 else:
702 711 # General Python objects
703 712 append_field(bundle, "Signature", "definition", code_formatter)
704 713 append_field(bundle, "Call signature", "call_def", code_formatter)
705 714 append_field(bundle, "Type", "type_name")
706 715 append_field(bundle, "String form", "string_form")
707 716
708 717 # Namespace
709 718 if info["namespace"] != "Interactive":
710 719 append_field(bundle, "Namespace", "namespace")
711 720
712 721 append_field(bundle, "Length", "length")
713 722 append_field(bundle, "File", "file")
714 723
715 724 # Source or docstring, depending on detail level and whether
716 725 # source found.
717 726 if detail_level > 0 and info["source"]:
718 727 append_field(bundle, "Source", "source", code_formatter)
719 728 else:
720 729 append_field(bundle, "Docstring", "docstring", formatter)
721 730
722 731 append_field(bundle, "Class docstring", "class_docstring", formatter)
723 732 append_field(bundle, "Init docstring", "init_docstring", formatter)
724 733 append_field(bundle, "Call docstring", "call_docstring", formatter)
725 734 return bundle
726 735
727 736
728 737 def _get_info(
729 738 self,
730 739 obj: Any,
731 740 oname: str = "",
732 741 formatter=None,
733 742 info: Optional[OInfo] = None,
734 743 detail_level=0,
735 744 omit_sections=(),
736 745 ) -> Bundle:
737 746 """Retrieve an info dict and format it.
738 747
739 748 Parameters
740 749 ----------
741 750 obj : any
742 751 Object to inspect and return info from
743 752 oname : str (default: ''):
744 753 Name of the variable pointing to `obj`.
745 754 formatter : callable
746 755 info
747 756 already computed information
748 757 detail_level : integer
749 758 Granularity of detail level, if set to 1, give more information.
750 759 omit_sections : container[str]
751 760 Titles or keys to omit from output (can be set, tuple, etc., anything supporting `in`)
752 761 """
753 762
754 763 info_dict = self.info(obj, oname=oname, info=info, detail_level=detail_level)
755 764 bundle = self._make_info_unformatted(
756 765 obj,
757 766 info_dict,
758 767 formatter,
759 768 detail_level=detail_level,
760 769 omit_sections=omit_sections,
761 770 )
771 for key, hook in self.mime_hooks.items():
772 res = hook(obj, info)
773 if res is not None:
774 bundle[key] = res
762 775 return self.format_mime(bundle)
763 776
764 777 def pinfo(
765 778 self,
766 779 obj,
767 780 oname="",
768 781 formatter=None,
769 782 info: Optional[OInfo] = None,
770 783 detail_level=0,
771 784 enable_html_pager=True,
772 785 omit_sections=(),
773 786 ):
774 787 """Show detailed information about an object.
775 788
776 789 Optional arguments:
777 790
778 791 - oname: name of the variable pointing to the object.
779 792
780 793 - formatter: callable (optional)
781 794 A special formatter for docstrings.
782 795
783 796 The formatter is a callable that takes a string as an input
784 797 and returns either a formatted string or a mime type bundle
785 798 in the form of a dictionary.
786 799
787 800 Although the support of custom formatter returning a string
788 801 instead of a mime type bundle is deprecated.
789 802
790 803 - info: a structure with some information fields which may have been
791 804 precomputed already.
792 805
793 806 - detail_level: if set to 1, more information is given.
794 807
795 808 - omit_sections: set of section keys and titles to omit
796 809 """
797 810 assert info is not None
798 811 info_b: Bundle = self._get_info(
799 812 obj, oname, formatter, info, detail_level, omit_sections=omit_sections
800 813 )
801 814 if not enable_html_pager:
802 815 del info_b["text/html"]
803 816 page.page(info_b)
804 817
805 818 def _info(self, obj, oname="", info=None, detail_level=0):
806 819 """
807 820 Inspector.info() was likely improperly marked as deprecated
808 821 while only a parameter was deprecated. We "un-deprecate" it.
809 822 """
810 823
811 824 warnings.warn(
812 825 "The `Inspector.info()` method has been un-deprecated as of 8.0 "
813 826 "and the `formatter=` keyword removed. `Inspector._info` is now "
814 827 "an alias, and you can just call `.info()` directly.",
815 828 DeprecationWarning,
816 829 stacklevel=2,
817 830 )
818 831 return self.info(obj, oname=oname, info=info, detail_level=detail_level)
819 832
820 833 def info(self, obj, oname="", info=None, detail_level=0) -> Dict[str, Any]:
821 834 """Compute a dict with detailed information about an object.
822 835
823 836 Parameters
824 837 ----------
825 838 obj : any
826 839 An object to find information about
827 840 oname : str (default: '')
828 841 Name of the variable pointing to `obj`.
829 842 info : (default: None)
830 843 A struct (dict like with attr access) with some information fields
831 844 which may have been precomputed already.
832 845 detail_level : int (default:0)
833 846 If set to 1, more information is given.
834 847
835 848 Returns
836 849 -------
837 850 An object info dict with known fields from `info_fields`. Keys are
838 851 strings, values are string or None.
839 852 """
840 853
841 854 if info is None:
842 855 ismagic = False
843 856 isalias = False
844 857 ospace = ''
845 858 else:
846 859 ismagic = info.ismagic
847 860 isalias = info.isalias
848 861 ospace = info.namespace
849 862
850 863 # Get docstring, special-casing aliases:
851 864 att_name = oname.split(".")[-1]
852 865 parents_docs = None
853 866 prelude = ""
854 867 if info and info.parent is not None and hasattr(info.parent, HOOK_NAME):
855 868 parents_docs_dict = getattr(info.parent, HOOK_NAME)
856 869 parents_docs = parents_docs_dict.get(att_name, None)
857 870 out = dict(
858 871 name=oname, found=True, isalias=isalias, ismagic=ismagic, subclasses=None
859 872 )
860 873
861 874 if parents_docs:
862 875 ds = parents_docs
863 876 elif isalias:
864 877 if not callable(obj):
865 878 try:
866 879 ds = "Alias to the system command:\n %s" % obj[1]
867 880 except:
868 881 ds = "Alias: " + str(obj)
869 882 else:
870 883 ds = "Alias to " + str(obj)
871 884 if obj.__doc__:
872 885 ds += "\nDocstring:\n" + obj.__doc__
873 886 else:
874 887 ds_or_None = getdoc(obj)
875 888 if ds_or_None is None:
876 889 ds = '<no docstring>'
877 890 else:
878 891 ds = ds_or_None
879 892
880 893 ds = prelude + ds
881 894
882 895 # store output in a dict, we initialize it here and fill it as we go
883 896
884 897 string_max = 200 # max size of strings to show (snipped if longer)
885 898 shalf = int((string_max - 5) / 2)
886 899
887 900 if ismagic:
888 901 out['type_name'] = 'Magic function'
889 902 elif isalias:
890 903 out['type_name'] = 'System alias'
891 904 else:
892 905 out['type_name'] = type(obj).__name__
893 906
894 907 try:
895 908 bclass = obj.__class__
896 909 out['base_class'] = str(bclass)
897 910 except:
898 911 pass
899 912
900 913 # String form, but snip if too long in ? form (full in ??)
901 914 if detail_level >= self.str_detail_level:
902 915 try:
903 916 ostr = str(obj)
904 917 str_head = 'string_form'
905 918 if not detail_level and len(ostr)>string_max:
906 919 ostr = ostr[:shalf] + ' <...> ' + ostr[-shalf:]
907 920 ostr = ("\n" + " " * len(str_head.expandtabs())).\
908 921 join(q.strip() for q in ostr.split("\n"))
909 922 out[str_head] = ostr
910 923 except:
911 924 pass
912 925
913 926 if ospace:
914 927 out['namespace'] = ospace
915 928
916 929 # Length (for strings and lists)
917 930 try:
918 931 out['length'] = str(len(obj))
919 932 except Exception:
920 933 pass
921 934
922 935 # Filename where object was defined
923 936 binary_file = False
924 937 fname = find_file(obj)
925 938 if fname is None:
926 939 # if anything goes wrong, we don't want to show source, so it's as
927 940 # if the file was binary
928 941 binary_file = True
929 942 else:
930 943 if fname.endswith(('.so', '.dll', '.pyd')):
931 944 binary_file = True
932 945 elif fname.endswith('<string>'):
933 946 fname = 'Dynamically generated function. No source code available.'
934 947 out['file'] = compress_user(fname)
935 948
936 949 # Original source code for a callable, class or property.
937 950 if detail_level:
938 951 # Flush the source cache because inspect can return out-of-date
939 952 # source
940 953 linecache.checkcache()
941 954 try:
942 955 if isinstance(obj, property) or not binary_file:
943 956 src = getsource(obj, oname)
944 957 if src is not None:
945 958 src = src.rstrip()
946 959 out['source'] = src
947 960
948 961 except Exception:
949 962 pass
950 963
951 964 # Add docstring only if no source is to be shown (avoid repetitions).
952 965 if ds and not self._source_contains_docstring(out.get('source'), ds):
953 966 out['docstring'] = ds
954 967
955 968 # Constructor docstring for classes
956 969 if inspect.isclass(obj):
957 970 out['isclass'] = True
958 971
959 972 # get the init signature:
960 973 try:
961 974 init_def = self._getdef(obj, oname)
962 975 except AttributeError:
963 976 init_def = None
964 977
965 978 # get the __init__ docstring
966 979 try:
967 980 obj_init = obj.__init__
968 981 except AttributeError:
969 982 init_ds = None
970 983 else:
971 984 if init_def is None:
972 985 # Get signature from init if top-level sig failed.
973 986 # Can happen for built-in types (list, etc.).
974 987 try:
975 988 init_def = self._getdef(obj_init, oname)
976 989 except AttributeError:
977 990 pass
978 991 init_ds = getdoc(obj_init)
979 992 # Skip Python's auto-generated docstrings
980 993 if init_ds == _object_init_docstring:
981 994 init_ds = None
982 995
983 996 if init_def:
984 997 out['init_definition'] = init_def
985 998
986 999 if init_ds:
987 1000 out['init_docstring'] = init_ds
988 1001
989 1002 names = [sub.__name__ for sub in type.__subclasses__(obj)]
990 1003 if len(names) < 10:
991 1004 all_names = ', '.join(names)
992 1005 else:
993 1006 all_names = ', '.join(names[:10]+['...'])
994 1007 out['subclasses'] = all_names
995 1008 # and class docstring for instances:
996 1009 else:
997 1010 # reconstruct the function definition and print it:
998 1011 defln = self._getdef(obj, oname)
999 1012 if defln:
1000 1013 out['definition'] = defln
1001 1014
1002 1015 # First, check whether the instance docstring is identical to the
1003 1016 # class one, and print it separately if they don't coincide. In
1004 1017 # most cases they will, but it's nice to print all the info for
1005 1018 # objects which use instance-customized docstrings.
1006 1019 if ds:
1007 1020 try:
1008 1021 cls = getattr(obj,'__class__')
1009 1022 except:
1010 1023 class_ds = None
1011 1024 else:
1012 1025 class_ds = getdoc(cls)
1013 1026 # Skip Python's auto-generated docstrings
1014 1027 if class_ds in _builtin_type_docstrings:
1015 1028 class_ds = None
1016 1029 if class_ds and ds != class_ds:
1017 1030 out['class_docstring'] = class_ds
1018 1031
1019 1032 # Next, try to show constructor docstrings
1020 1033 try:
1021 1034 init_ds = getdoc(obj.__init__)
1022 1035 # Skip Python's auto-generated docstrings
1023 1036 if init_ds == _object_init_docstring:
1024 1037 init_ds = None
1025 1038 except AttributeError:
1026 1039 init_ds = None
1027 1040 if init_ds:
1028 1041 out['init_docstring'] = init_ds
1029 1042
1030 1043 # Call form docstring for callable instances
1031 1044 if safe_hasattr(obj, '__call__') and not is_simple_callable(obj):
1032 1045 call_def = self._getdef(obj.__call__, oname)
1033 1046 if call_def and (call_def != out.get('definition')):
1034 1047 # it may never be the case that call def and definition differ,
1035 1048 # but don't include the same signature twice
1036 1049 out['call_def'] = call_def
1037 1050 call_ds = getdoc(obj.__call__)
1038 1051 # Skip Python's auto-generated docstrings
1039 1052 if call_ds == _func_call_docstring:
1040 1053 call_ds = None
1041 1054 if call_ds:
1042 1055 out['call_docstring'] = call_ds
1043 1056
1044 1057 return object_info(**out)
1045 1058
1046 1059 @staticmethod
1047 1060 def _source_contains_docstring(src, doc):
1048 1061 """
1049 1062 Check whether the source *src* contains the docstring *doc*.
1050 1063
1051 1064 This is is helper function to skip displaying the docstring if the
1052 1065 source already contains it, avoiding repetition of information.
1053 1066 """
1054 1067 try:
1055 1068 (def_node,) = ast.parse(dedent(src)).body
1056 1069 return ast.get_docstring(def_node) == doc # type: ignore[arg-type]
1057 1070 except Exception:
1058 1071 # The source can become invalid or even non-existent (because it
1059 1072 # is re-fetched from the source file) so the above code fail in
1060 1073 # arbitrary ways.
1061 1074 return False
1062 1075
1063 1076 def psearch(self,pattern,ns_table,ns_search=[],
1064 1077 ignore_case=False,show_all=False, *, list_types=False):
1065 1078 """Search namespaces with wildcards for objects.
1066 1079
1067 1080 Arguments:
1068 1081
1069 1082 - pattern: string containing shell-like wildcards to use in namespace
1070 1083 searches and optionally a type specification to narrow the search to
1071 1084 objects of that type.
1072 1085
1073 1086 - ns_table: dict of name->namespaces for search.
1074 1087
1075 1088 Optional arguments:
1076 1089
1077 1090 - ns_search: list of namespace names to include in search.
1078 1091
1079 1092 - ignore_case(False): make the search case-insensitive.
1080 1093
1081 1094 - show_all(False): show all names, including those starting with
1082 1095 underscores.
1083 1096
1084 1097 - list_types(False): list all available object types for object matching.
1085 1098 """
1086 1099 #print 'ps pattern:<%r>' % pattern # dbg
1087 1100
1088 1101 # defaults
1089 1102 type_pattern = 'all'
1090 1103 filter = ''
1091 1104
1092 1105 # list all object types
1093 1106 if list_types:
1094 1107 page.page('\n'.join(sorted(typestr2type)))
1095 1108 return
1096 1109
1097 1110 cmds = pattern.split()
1098 1111 len_cmds = len(cmds)
1099 1112 if len_cmds == 1:
1100 1113 # Only filter pattern given
1101 1114 filter = cmds[0]
1102 1115 elif len_cmds == 2:
1103 1116 # Both filter and type specified
1104 1117 filter,type_pattern = cmds
1105 1118 else:
1106 1119 raise ValueError('invalid argument string for psearch: <%s>' %
1107 1120 pattern)
1108 1121
1109 1122 # filter search namespaces
1110 1123 for name in ns_search:
1111 1124 if name not in ns_table:
1112 1125 raise ValueError('invalid namespace <%s>. Valid names: %s' %
1113 1126 (name,ns_table.keys()))
1114 1127
1115 1128 #print 'type_pattern:',type_pattern # dbg
1116 1129 search_result, namespaces_seen = set(), set()
1117 1130 for ns_name in ns_search:
1118 1131 ns = ns_table[ns_name]
1119 1132 # Normally, locals and globals are the same, so we just check one.
1120 1133 if id(ns) in namespaces_seen:
1121 1134 continue
1122 1135 namespaces_seen.add(id(ns))
1123 1136 tmp_res = list_namespace(ns, type_pattern, filter,
1124 1137 ignore_case=ignore_case, show_all=show_all)
1125 1138 search_result.update(tmp_res)
1126 1139
1127 1140 page.page('\n'.join(sorted(search_result)))
1128 1141
1129 1142
1130 1143 def _render_signature(obj_signature, obj_name) -> str:
1131 1144 """
1132 1145 This was mostly taken from inspect.Signature.__str__.
1133 1146 Look there for the comments.
1134 1147 The only change is to add linebreaks when this gets too long.
1135 1148 """
1136 1149 result = []
1137 1150 pos_only = False
1138 1151 kw_only = True
1139 1152 for param in obj_signature.parameters.values():
1140 1153 if param.kind == inspect.Parameter.POSITIONAL_ONLY:
1141 1154 pos_only = True
1142 1155 elif pos_only:
1143 1156 result.append('/')
1144 1157 pos_only = False
1145 1158
1146 1159 if param.kind == inspect.Parameter.VAR_POSITIONAL:
1147 1160 kw_only = False
1148 1161 elif param.kind == inspect.Parameter.KEYWORD_ONLY and kw_only:
1149 1162 result.append('*')
1150 1163 kw_only = False
1151 1164
1152 1165 result.append(str(param))
1153 1166
1154 1167 if pos_only:
1155 1168 result.append('/')
1156 1169
1157 1170 # add up name, parameters, braces (2), and commas
1158 1171 if len(obj_name) + sum(len(r) + 2 for r in result) > 75:
1159 1172 # This doesn’t fit behind “Signature: ” in an inspect window.
1160 1173 rendered = '{}(\n{})'.format(obj_name, ''.join(
1161 1174 ' {},\n'.format(r) for r in result)
1162 1175 )
1163 1176 else:
1164 1177 rendered = '{}({})'.format(obj_name, ', '.join(result))
1165 1178
1166 1179 if obj_signature.return_annotation is not inspect._empty:
1167 1180 anno = inspect.formatannotation(obj_signature.return_annotation)
1168 1181 rendered += ' -> {}'.format(anno)
1169 1182
1170 1183 return rendered
General Comments 0
You need to be logged in to leave comments. Login now