|
"""Tools for inspecting Python objects. |
|
|
|
Uses syntax highlighting for presenting the various information elements. |
|
|
|
Similar in spirit to the inspect module, but all calls take a name argument to |
|
reference the name under which an object is being read. |
|
""" |
|
|
|
|
|
|
|
|
|
__all__ = ['Inspector','InspectColors'] |
|
|
|
|
|
from dataclasses import dataclass |
|
from inspect import signature |
|
from textwrap import dedent |
|
import ast |
|
import html |
|
import inspect |
|
import io as stdlib_io |
|
import linecache |
|
import os |
|
import types |
|
import warnings |
|
|
|
|
|
from typing import ( |
|
cast, |
|
Any, |
|
Optional, |
|
Dict, |
|
Union, |
|
List, |
|
TypedDict, |
|
TypeAlias, |
|
Tuple, |
|
) |
|
|
|
import traitlets |
|
|
|
|
|
from IPython.core import page |
|
from IPython.lib.pretty import pretty |
|
from IPython.testing.skipdoctest import skip_doctest |
|
from IPython.utils import PyColorize, openpy |
|
from IPython.utils.dir2 import safe_hasattr |
|
from IPython.utils.path import compress_user |
|
from IPython.utils.text import indent |
|
from IPython.utils.wildcard import list_namespace, typestr2type |
|
from IPython.utils.coloransi import TermColors |
|
from IPython.utils.colorable import Colorable |
|
from IPython.utils.decorators import undoc |
|
|
|
from pygments import highlight |
|
from pygments.lexers import PythonLexer |
|
from pygments.formatters import HtmlFormatter |
|
|
|
HOOK_NAME = "__custom_documentations__" |
|
|
|
|
|
UnformattedBundle: TypeAlias = Dict[str, List[Tuple[str, str]]] |
|
Bundle: TypeAlias = Dict[str, str] |
|
|
|
|
|
@dataclass |
|
class OInfo: |
|
ismagic: bool |
|
isalias: bool |
|
found: bool |
|
namespace: Optional[str] |
|
parent: Any |
|
obj: Any |
|
|
|
def get(self, field): |
|
"""Get a field from the object for backward compatibility with before 8.12 |
|
|
|
see https://github.com/h5py/h5py/issues/2253 |
|
""" |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return getattr(self, field) |
|
|
|
|
|
def pylight(code): |
|
return highlight(code, PythonLexer(), HtmlFormatter(noclasses=True)) |
|
|
|
|
|
_func_call_docstring = types.FunctionType.__call__.__doc__ |
|
_object_init_docstring = object.__init__.__doc__ |
|
_builtin_type_docstrings = { |
|
inspect.getdoc(t) for t in (types.ModuleType, types.MethodType, |
|
types.FunctionType, property) |
|
} |
|
|
|
_builtin_func_type = type(all) |
|
_builtin_meth_type = type(str.upper) |
|
|
|
|
|
|
|
Colors = TermColors |
|
|
|
InspectColors = PyColorize.ANSICodeColors |
|
|
|
|
|
|
|
|
|
|
|
class InfoDict(TypedDict): |
|
type_name: Optional[str] |
|
base_class: Optional[str] |
|
string_form: Optional[str] |
|
namespace: Optional[str] |
|
length: Optional[str] |
|
file: Optional[str] |
|
definition: Optional[str] |
|
docstring: Optional[str] |
|
source: Optional[str] |
|
init_definition: Optional[str] |
|
class_docstring: Optional[str] |
|
init_docstring: Optional[str] |
|
call_def: Optional[str] |
|
call_docstring: Optional[str] |
|
subclasses: Optional[str] |
|
|
|
|
|
ismagic: bool |
|
isalias: bool |
|
isclass: bool |
|
found: bool |
|
name: str |
|
|
|
|
|
_info_fields = list(InfoDict.__annotations__.keys()) |
|
|
|
|
|
def __getattr__(name): |
|
if name == "info_fields": |
|
warnings.warn( |
|
"IPython.core.oinspect's `info_fields` is considered for deprecation and may be removed in the Future. ", |
|
DeprecationWarning, |
|
stacklevel=2, |
|
) |
|
return _info_fields |
|
|
|
raise AttributeError(f"module {__name__!r} has no attribute {name!r}") |
|
|
|
|
|
@dataclass |
|
class InspectorHookData: |
|
"""Data passed to the mime hook""" |
|
|
|
obj: Any |
|
info: Optional[OInfo] |
|
info_dict: InfoDict |
|
detail_level: int |
|
omit_sections: list[str] |
|
|
|
|
|
@undoc |
|
def object_info( |
|
*, |
|
name: str, |
|
found: bool, |
|
isclass: bool = False, |
|
isalias: bool = False, |
|
ismagic: bool = False, |
|
**kw, |
|
) -> InfoDict: |
|
"""Make an object info dict with all fields present.""" |
|
infodict = kw |
|
infodict = {k: None for k in _info_fields if k not in infodict} |
|
infodict["name"] = name |
|
infodict["found"] = found |
|
infodict["isclass"] = isclass |
|
infodict["isalias"] = isalias |
|
infodict["ismagic"] = ismagic |
|
|
|
return InfoDict(**infodict) |
|
|
|
|
|
def get_encoding(obj): |
|
"""Get encoding for python source file defining obj |
|
|
|
Returns None if obj is not defined in a sourcefile. |
|
""" |
|
ofile = find_file(obj) |
|
|
|
|
|
|
|
if ofile is None: |
|
return None |
|
elif ofile.endswith(('.so', '.dll', '.pyd')): |
|
return None |
|
elif not os.path.isfile(ofile): |
|
return None |
|
else: |
|
|
|
|
|
|
|
with stdlib_io.open(ofile, 'rb') as buffer: |
|
encoding, _lines = openpy.detect_encoding(buffer.readline) |
|
return encoding |
|
|
|
|
|
def getdoc(obj) -> Union[str, None]: |
|
"""Stable wrapper around inspect.getdoc. |
|
|
|
This can't crash because of attribute problems. |
|
|
|
It also attempts to call a getdoc() method on the given object. This |
|
allows objects which provide their docstrings via non-standard mechanisms |
|
(like Pyro proxies) to still be inspected by ipython's ? system. |
|
""" |
|
|
|
try: |
|
ds = obj.getdoc() |
|
except Exception: |
|
pass |
|
else: |
|
if isinstance(ds, str): |
|
return inspect.cleandoc(ds) |
|
docstr = inspect.getdoc(obj) |
|
return docstr |
|
|
|
|
|
def getsource(obj, oname='') -> Union[str,None]: |
|
"""Wrapper around inspect.getsource. |
|
|
|
This can be modified by other projects to provide customized source |
|
extraction. |
|
|
|
Parameters |
|
---------- |
|
obj : object |
|
an object whose source code we will attempt to extract |
|
oname : str |
|
(optional) a name under which the object is known |
|
|
|
Returns |
|
------- |
|
src : unicode or None |
|
|
|
""" |
|
|
|
if isinstance(obj, property): |
|
sources = [] |
|
for attrname in ['fget', 'fset', 'fdel']: |
|
fn = getattr(obj, attrname) |
|
if fn is not None: |
|
encoding = get_encoding(fn) |
|
oname_prefix = ('%s.' % oname) if oname else '' |
|
sources.append(''.join(('# ', oname_prefix, attrname))) |
|
if inspect.isfunction(fn): |
|
_src = getsource(fn) |
|
if _src: |
|
|
|
sources.append(dedent(_src)) |
|
else: |
|
|
|
|
|
sources.append( |
|
'%s%s = %s\n' % (oname_prefix, attrname, pretty(fn)) |
|
) |
|
if sources: |
|
return '\n'.join(sources) |
|
else: |
|
return None |
|
|
|
else: |
|
|
|
|
|
obj = _get_wrapped(obj) |
|
|
|
try: |
|
src = inspect.getsource(obj) |
|
except TypeError: |
|
|
|
|
|
try: |
|
src = inspect.getsource(obj.__class__) |
|
except (OSError, TypeError): |
|
return None |
|
except OSError: |
|
return None |
|
|
|
return src |
|
|
|
|
|
def is_simple_callable(obj): |
|
"""True if obj is a function ()""" |
|
return (inspect.isfunction(obj) or inspect.ismethod(obj) or \ |
|
isinstance(obj, _builtin_func_type) or isinstance(obj, _builtin_meth_type)) |
|
|
|
@undoc |
|
def getargspec(obj): |
|
"""Wrapper around :func:`inspect.getfullargspec` |
|
|
|
In addition to functions and methods, this can also handle objects with a |
|
``__call__`` attribute. |
|
|
|
DEPRECATED: Deprecated since 7.10. Do not use, will be removed. |
|
""" |
|
|
|
warnings.warn('`getargspec` function is deprecated as of IPython 7.10' |
|
'and will be removed in future versions.', DeprecationWarning, stacklevel=2) |
|
|
|
if safe_hasattr(obj, '__call__') and not is_simple_callable(obj): |
|
obj = obj.__call__ |
|
|
|
return inspect.getfullargspec(obj) |
|
|
|
@undoc |
|
def format_argspec(argspec): |
|
"""Format argspect, convenience wrapper around inspect's. |
|
|
|
This takes a dict instead of ordered arguments and calls |
|
inspect.format_argspec with the arguments in the necessary order. |
|
|
|
DEPRECATED (since 7.10): Do not use; will be removed in future versions. |
|
""" |
|
|
|
warnings.warn('`format_argspec` function is deprecated as of IPython 7.10' |
|
'and will be removed in future versions.', DeprecationWarning, stacklevel=2) |
|
|
|
|
|
return inspect.formatargspec(argspec['args'], argspec['varargs'], |
|
argspec['varkw'], argspec['defaults']) |
|
|
|
@undoc |
|
def call_tip(oinfo, format_call=True): |
|
"""DEPRECATED since 6.0. Extract call tip data from an oinfo dict.""" |
|
warnings.warn( |
|
"`call_tip` function is deprecated as of IPython 6.0" |
|
"and will be removed in future versions.", |
|
DeprecationWarning, |
|
stacklevel=2, |
|
) |
|
|
|
argspec = oinfo.get('argspec') |
|
if argspec is None: |
|
call_line = None |
|
else: |
|
|
|
|
|
|
|
try: |
|
has_self = argspec['args'][0] == 'self' |
|
except (KeyError, IndexError): |
|
pass |
|
else: |
|
if has_self: |
|
argspec['args'] = argspec['args'][1:] |
|
|
|
call_line = oinfo['name']+format_argspec(argspec) |
|
|
|
|
|
|
|
doc = oinfo.get('call_docstring') |
|
if doc is None: |
|
doc = oinfo.get('init_docstring') |
|
if doc is None: |
|
doc = oinfo.get('docstring','') |
|
|
|
return call_line, doc |
|
|
|
|
|
def _get_wrapped(obj): |
|
"""Get the original object if wrapped in one or more @decorators |
|
|
|
Some objects automatically construct similar objects on any unrecognised |
|
attribute access (e.g. unittest.mock.call). To protect against infinite loops, |
|
this will arbitrarily cut off after 100 levels of obj.__wrapped__ |
|
attribute access. --TK, Jan 2016 |
|
""" |
|
orig_obj = obj |
|
i = 0 |
|
while safe_hasattr(obj, '__wrapped__'): |
|
obj = obj.__wrapped__ |
|
i += 1 |
|
if i > 100: |
|
|
|
return orig_obj |
|
return obj |
|
|
|
def find_file(obj) -> Optional[str]: |
|
"""Find the absolute path to the file where an object was defined. |
|
|
|
This is essentially a robust wrapper around `inspect.getabsfile`. |
|
|
|
Returns None if no file can be found. |
|
|
|
Parameters |
|
---------- |
|
obj : any Python object |
|
|
|
Returns |
|
------- |
|
fname : str |
|
The absolute path to the file where the object was defined. |
|
""" |
|
obj = _get_wrapped(obj) |
|
|
|
fname: Optional[str] = None |
|
try: |
|
fname = inspect.getabsfile(obj) |
|
except TypeError: |
|
|
|
|
|
try: |
|
fname = inspect.getabsfile(obj.__class__) |
|
except (OSError, TypeError): |
|
|
|
pass |
|
except OSError: |
|
pass |
|
|
|
return fname |
|
|
|
|
|
def find_source_lines(obj): |
|
"""Find the line number in a file where an object was defined. |
|
|
|
This is essentially a robust wrapper around `inspect.getsourcelines`. |
|
|
|
Returns None if no file can be found. |
|
|
|
Parameters |
|
---------- |
|
obj : any Python object |
|
|
|
Returns |
|
------- |
|
lineno : int |
|
The line number where the object definition starts. |
|
""" |
|
obj = _get_wrapped(obj) |
|
|
|
try: |
|
lineno = inspect.getsourcelines(obj)[1] |
|
except TypeError: |
|
|
|
try: |
|
lineno = inspect.getsourcelines(obj.__class__)[1] |
|
except (OSError, TypeError): |
|
return None |
|
except OSError: |
|
return None |
|
|
|
return lineno |
|
|
|
class Inspector(Colorable): |
|
|
|
mime_hooks = traitlets.Dict( |
|
config=True, |
|
help="dictionary of mime to callable to add informations into help mimebundle dict", |
|
).tag(config=True) |
|
|
|
def __init__( |
|
self, |
|
color_table=InspectColors, |
|
code_color_table=PyColorize.ANSICodeColors, |
|
scheme=None, |
|
str_detail_level=0, |
|
parent=None, |
|
config=None, |
|
): |
|
super(Inspector, self).__init__(parent=parent, config=config) |
|
self.color_table = color_table |
|
self.parser = PyColorize.Parser(out='str', parent=self, style=scheme) |
|
self.format = self.parser.format |
|
self.str_detail_level = str_detail_level |
|
self.set_active_scheme(scheme) |
|
|
|
def _getdef(self,obj,oname='') -> Union[str,None]: |
|
"""Return the call signature for any callable object. |
|
|
|
If any exception is generated, None is returned instead and the |
|
exception is suppressed.""" |
|
if not callable(obj): |
|
return None |
|
try: |
|
return _render_signature(signature(obj), oname) |
|
except: |
|
return None |
|
|
|
def __head(self,h) -> str: |
|
"""Return a header string with proper colors.""" |
|
return '%s%s%s' % (self.color_table.active_colors.header,h, |
|
self.color_table.active_colors.normal) |
|
|
|
def set_active_scheme(self, scheme): |
|
if scheme is not None: |
|
self.color_table.set_active_scheme(scheme) |
|
self.parser.color_table.set_active_scheme(scheme) |
|
|
|
def noinfo(self, msg, oname): |
|
"""Generic message when no information is found.""" |
|
print('No %s found' % msg, end=' ') |
|
if oname: |
|
print('for %s' % oname) |
|
else: |
|
print() |
|
|
|
def pdef(self, obj, oname=''): |
|
"""Print the call signature for any callable object. |
|
|
|
If the object is a class, print the constructor information.""" |
|
|
|
if not callable(obj): |
|
print('Object is not callable.') |
|
return |
|
|
|
header = '' |
|
|
|
if inspect.isclass(obj): |
|
header = self.__head('Class constructor information:\n') |
|
|
|
|
|
output = self._getdef(obj,oname) |
|
if output is None: |
|
self.noinfo('definition header',oname) |
|
else: |
|
print(header,self.format(output), end=' ') |
|
|
|
|
|
@skip_doctest |
|
def pdoc(self, obj, oname='', formatter=None): |
|
"""Print the docstring for any object. |
|
|
|
Optional: |
|
-formatter: a function to run the docstring through for specially |
|
formatted docstrings. |
|
|
|
Examples |
|
-------- |
|
In [1]: class NoInit: |
|
...: pass |
|
|
|
In [2]: class NoDoc: |
|
...: def __init__(self): |
|
...: pass |
|
|
|
In [3]: %pdoc NoDoc |
|
No documentation found for NoDoc |
|
|
|
In [4]: %pdoc NoInit |
|
No documentation found for NoInit |
|
|
|
In [5]: obj = NoInit() |
|
|
|
In [6]: %pdoc obj |
|
No documentation found for obj |
|
|
|
In [5]: obj2 = NoDoc() |
|
|
|
In [6]: %pdoc obj2 |
|
No documentation found for obj2 |
|
""" |
|
|
|
head = self.__head |
|
lines = [] |
|
ds = getdoc(obj) |
|
if formatter: |
|
ds = formatter(ds).get('plain/text', ds) |
|
if ds: |
|
lines.append(head("Class docstring:")) |
|
lines.append(indent(ds)) |
|
if inspect.isclass(obj) and hasattr(obj, '__init__'): |
|
init_ds = getdoc(obj.__init__) |
|
if init_ds is not None: |
|
lines.append(head("Init docstring:")) |
|
lines.append(indent(init_ds)) |
|
elif hasattr(obj,'__call__'): |
|
call_ds = getdoc(obj.__call__) |
|
if call_ds: |
|
lines.append(head("Call docstring:")) |
|
lines.append(indent(call_ds)) |
|
|
|
if not lines: |
|
self.noinfo('documentation',oname) |
|
else: |
|
page.page('\n'.join(lines)) |
|
|
|
def psource(self, obj, oname=''): |
|
"""Print the source code for an object.""" |
|
|
|
|
|
linecache.checkcache() |
|
try: |
|
src = getsource(obj, oname=oname) |
|
except Exception: |
|
src = None |
|
|
|
if src is None: |
|
self.noinfo('source', oname) |
|
else: |
|
page.page(self.format(src)) |
|
|
|
def pfile(self, obj, oname=''): |
|
"""Show the whole file where an object was defined.""" |
|
|
|
lineno = find_source_lines(obj) |
|
if lineno is None: |
|
self.noinfo('file', oname) |
|
return |
|
|
|
ofile = find_file(obj) |
|
|
|
|
|
|
|
if ofile is None: |
|
print("Could not find file for object") |
|
elif ofile.endswith((".so", ".dll", ".pyd")): |
|
print("File %r is binary, not printing." % ofile) |
|
elif not os.path.isfile(ofile): |
|
print('File %r does not exist, not printing.' % ofile) |
|
else: |
|
|
|
|
|
|
|
page.page(self.format(openpy.read_py_file(ofile, skip_encoding_cookie=False)), lineno - 1) |
|
|
|
|
|
def _mime_format(self, text:str, formatter=None) -> dict: |
|
"""Return a mime bundle representation of the input text. |
|
|
|
- if `formatter` is None, the returned mime bundle has |
|
a ``text/plain`` field, with the input text. |
|
a ``text/html`` field with a ``<pre>`` tag containing the input text. |
|
|
|
- if ``formatter`` is not None, it must be a callable transforming the |
|
input text into a mime bundle. Default values for ``text/plain`` and |
|
``text/html`` representations are the ones described above. |
|
|
|
Note: |
|
|
|
Formatters returning strings are supported but this behavior is deprecated. |
|
|
|
""" |
|
defaults = { |
|
"text/plain": text, |
|
"text/html": f"<pre>{html.escape(text)}</pre>", |
|
} |
|
|
|
if formatter is None: |
|
return defaults |
|
else: |
|
formatted = formatter(text) |
|
|
|
if not isinstance(formatted, dict): |
|
|
|
|
|
return {"text/plain": formatted, "text/html": f"<pre>{formatted}</pre>"} |
|
|
|
else: |
|
return dict(defaults, **formatted) |
|
|
|
def format_mime(self, bundle: UnformattedBundle) -> Bundle: |
|
"""Format a mimebundle being created by _make_info_unformatted into a real mimebundle""" |
|
|
|
assert isinstance(bundle["text/plain"], list) |
|
for item in bundle["text/plain"]: |
|
assert isinstance(item, tuple) |
|
|
|
new_b: Bundle = {} |
|
lines = [] |
|
_len = max(len(h) for h, _ in bundle["text/plain"]) |
|
|
|
for head, body in bundle["text/plain"]: |
|
body = body.strip("\n") |
|
delim = "\n" if "\n" in body else " " |
|
lines.append( |
|
f"{self.__head(head+':')}{(_len - len(head))*' '}{delim}{body}" |
|
) |
|
|
|
new_b["text/plain"] = "\n".join(lines) |
|
|
|
if "text/html" in bundle: |
|
assert isinstance(bundle["text/html"], list) |
|
for item in bundle["text/html"]: |
|
assert isinstance(item, tuple) |
|
|
|
if isinstance(bundle["text/html"], (list, tuple)): |
|
|
|
new_b["text/html"] = "\n".join( |
|
(f"<h1>{head}</h1>\n{body}" for (head, body) in bundle["text/html"]) |
|
) |
|
|
|
for k in bundle.keys(): |
|
if k in ("text/html", "text/plain"): |
|
continue |
|
else: |
|
new_b[k] = bundle[k] |
|
return new_b |
|
|
|
def _append_info_field( |
|
self, |
|
bundle: UnformattedBundle, |
|
title: str, |
|
key: str, |
|
info, |
|
omit_sections: List[str], |
|
formatter, |
|
): |
|
"""Append an info value to the unformatted mimebundle being constructed by _make_info_unformatted""" |
|
if title in omit_sections or key in omit_sections: |
|
return |
|
field = info[key] |
|
if field is not None: |
|
formatted_field = self._mime_format(field, formatter) |
|
bundle["text/plain"].append((title, formatted_field["text/plain"])) |
|
bundle["text/html"].append((title, formatted_field["text/html"])) |
|
|
|
def _make_info_unformatted( |
|
self, obj, info, formatter, detail_level, omit_sections |
|
) -> UnformattedBundle: |
|
"""Assemble the mimebundle as unformatted lists of information""" |
|
bundle: UnformattedBundle = { |
|
"text/plain": [], |
|
"text/html": [], |
|
} |
|
|
|
|
|
def append_field( |
|
bundle: UnformattedBundle, title: str, key: str, formatter=None |
|
): |
|
self._append_info_field( |
|
bundle, |
|
title=title, |
|
key=key, |
|
info=info, |
|
omit_sections=omit_sections, |
|
formatter=formatter, |
|
) |
|
|
|
def code_formatter(text) -> Bundle: |
|
return { |
|
'text/plain': self.format(text), |
|
'text/html': pylight(text) |
|
} |
|
|
|
if info["isalias"]: |
|
append_field(bundle, "Repr", "string_form") |
|
|
|
elif info['ismagic']: |
|
if detail_level > 0: |
|
append_field(bundle, "Source", "source", code_formatter) |
|
else: |
|
append_field(bundle, "Docstring", "docstring", formatter) |
|
append_field(bundle, "File", "file") |
|
|
|
elif info['isclass'] or is_simple_callable(obj): |
|
|
|
append_field(bundle, "Signature", "definition", code_formatter) |
|
append_field(bundle, "Init signature", "init_definition", code_formatter) |
|
append_field(bundle, "Docstring", "docstring", formatter) |
|
if detail_level > 0 and info["source"]: |
|
append_field(bundle, "Source", "source", code_formatter) |
|
else: |
|
append_field(bundle, "Init docstring", "init_docstring", formatter) |
|
|
|
append_field(bundle, "File", "file") |
|
append_field(bundle, "Type", "type_name") |
|
append_field(bundle, "Subclasses", "subclasses") |
|
|
|
else: |
|
|
|
append_field(bundle, "Signature", "definition", code_formatter) |
|
append_field(bundle, "Call signature", "call_def", code_formatter) |
|
append_field(bundle, "Type", "type_name") |
|
append_field(bundle, "String form", "string_form") |
|
|
|
|
|
if info["namespace"] != "Interactive": |
|
append_field(bundle, "Namespace", "namespace") |
|
|
|
append_field(bundle, "Length", "length") |
|
append_field(bundle, "File", "file") |
|
|
|
|
|
|
|
if detail_level > 0 and info["source"]: |
|
append_field(bundle, "Source", "source", code_formatter) |
|
else: |
|
append_field(bundle, "Docstring", "docstring", formatter) |
|
|
|
append_field(bundle, "Class docstring", "class_docstring", formatter) |
|
append_field(bundle, "Init docstring", "init_docstring", formatter) |
|
append_field(bundle, "Call docstring", "call_docstring", formatter) |
|
return bundle |
|
|
|
|
|
def _get_info( |
|
self, |
|
obj: Any, |
|
oname: str = "", |
|
formatter=None, |
|
info: Optional[OInfo] = None, |
|
detail_level: int = 0, |
|
omit_sections: Union[List[str], Tuple[()]] = (), |
|
) -> Bundle: |
|
"""Retrieve an info dict and format it. |
|
|
|
Parameters |
|
---------- |
|
obj : any |
|
Object to inspect and return info from |
|
oname : str (default: ''): |
|
Name of the variable pointing to `obj`. |
|
formatter : callable |
|
info |
|
already computed information |
|
detail_level : integer |
|
Granularity of detail level, if set to 1, give more information. |
|
omit_sections : list[str] |
|
Titles or keys to omit from output (can be set, tuple, etc., anything supporting `in`) |
|
""" |
|
|
|
info_dict = self.info(obj, oname=oname, info=info, detail_level=detail_level) |
|
omit_sections = list(omit_sections) |
|
|
|
bundle = self._make_info_unformatted( |
|
obj, |
|
info_dict, |
|
formatter, |
|
detail_level=detail_level, |
|
omit_sections=omit_sections, |
|
) |
|
if self.mime_hooks: |
|
hook_data = InspectorHookData( |
|
obj=obj, |
|
info=info, |
|
info_dict=info_dict, |
|
detail_level=detail_level, |
|
omit_sections=omit_sections, |
|
) |
|
for key, hook in self.mime_hooks.items(): |
|
required_parameters = [ |
|
parameter |
|
for parameter in inspect.signature(hook).parameters.values() |
|
if parameter.default != inspect.Parameter.default |
|
] |
|
if len(required_parameters) == 1: |
|
res = hook(hook_data) |
|
else: |
|
warnings.warn( |
|
"MIME hook format changed in IPython 8.22; hooks should now accept" |
|
" a single parameter (InspectorHookData); support for hooks requiring" |
|
" two-parameters (obj and info) will be removed in a future version", |
|
DeprecationWarning, |
|
stacklevel=2, |
|
) |
|
res = hook(obj, info) |
|
if res is not None: |
|
bundle[key] = res |
|
return self.format_mime(bundle) |
|
|
|
def pinfo( |
|
self, |
|
obj, |
|
oname="", |
|
formatter=None, |
|
info: Optional[OInfo] = None, |
|
detail_level=0, |
|
enable_html_pager=True, |
|
omit_sections=(), |
|
): |
|
"""Show detailed information about an object. |
|
|
|
Optional arguments: |
|
|
|
- oname: name of the variable pointing to the object. |
|
|
|
- formatter: callable (optional) |
|
A special formatter for docstrings. |
|
|
|
The formatter is a callable that takes a string as an input |
|
and returns either a formatted string or a mime type bundle |
|
in the form of a dictionary. |
|
|
|
Although the support of custom formatter returning a string |
|
instead of a mime type bundle is deprecated. |
|
|
|
- info: a structure with some information fields which may have been |
|
precomputed already. |
|
|
|
- detail_level: if set to 1, more information is given. |
|
|
|
- omit_sections: set of section keys and titles to omit |
|
""" |
|
assert info is not None |
|
info_b: Bundle = self._get_info( |
|
obj, oname, formatter, info, detail_level, omit_sections=omit_sections |
|
) |
|
if not enable_html_pager: |
|
del info_b["text/html"] |
|
page.page(info_b) |
|
|
|
def _info(self, obj, oname="", info=None, detail_level=0): |
|
""" |
|
Inspector.info() was likely improperly marked as deprecated |
|
while only a parameter was deprecated. We "un-deprecate" it. |
|
""" |
|
|
|
warnings.warn( |
|
"The `Inspector.info()` method has been un-deprecated as of 8.0 " |
|
"and the `formatter=` keyword removed. `Inspector._info` is now " |
|
"an alias, and you can just call `.info()` directly.", |
|
DeprecationWarning, |
|
stacklevel=2, |
|
) |
|
return self.info(obj, oname=oname, info=info, detail_level=detail_level) |
|
|
|
def info(self, obj, oname="", info=None, detail_level=0) -> InfoDict: |
|
"""Compute a dict with detailed information about an object. |
|
|
|
Parameters |
|
---------- |
|
obj : any |
|
An object to find information about |
|
oname : str (default: '') |
|
Name of the variable pointing to `obj`. |
|
info : (default: None) |
|
A struct (dict like with attr access) with some information fields |
|
which may have been precomputed already. |
|
detail_level : int (default:0) |
|
If set to 1, more information is given. |
|
|
|
Returns |
|
------- |
|
An object info dict with known fields from `info_fields` (see `InfoDict`). |
|
""" |
|
|
|
if info is None: |
|
ismagic = False |
|
isalias = False |
|
ospace = '' |
|
else: |
|
ismagic = info.ismagic |
|
isalias = info.isalias |
|
ospace = info.namespace |
|
|
|
|
|
att_name = oname.split(".")[-1] |
|
parents_docs = None |
|
prelude = "" |
|
if info and info.parent is not None and hasattr(info.parent, HOOK_NAME): |
|
parents_docs_dict = getattr(info.parent, HOOK_NAME) |
|
parents_docs = parents_docs_dict.get(att_name, None) |
|
out: InfoDict = cast( |
|
InfoDict, |
|
{ |
|
**{field: None for field in _info_fields}, |
|
**{ |
|
"name": oname, |
|
"found": True, |
|
"isalias": isalias, |
|
"ismagic": ismagic, |
|
"subclasses": None, |
|
}, |
|
}, |
|
) |
|
|
|
if parents_docs: |
|
ds = parents_docs |
|
elif isalias: |
|
if not callable(obj): |
|
try: |
|
ds = "Alias to the system command:\n %s" % obj[1] |
|
except: |
|
ds = "Alias: " + str(obj) |
|
else: |
|
ds = "Alias to " + str(obj) |
|
if obj.__doc__: |
|
ds += "\nDocstring:\n" + obj.__doc__ |
|
else: |
|
ds_or_None = getdoc(obj) |
|
if ds_or_None is None: |
|
ds = '<no docstring>' |
|
else: |
|
ds = ds_or_None |
|
|
|
ds = prelude + ds |
|
|
|
|
|
|
|
string_max = 200 |
|
shalf = int((string_max - 5) / 2) |
|
|
|
if ismagic: |
|
out['type_name'] = 'Magic function' |
|
elif isalias: |
|
out['type_name'] = 'System alias' |
|
else: |
|
out['type_name'] = type(obj).__name__ |
|
|
|
try: |
|
bclass = obj.__class__ |
|
out['base_class'] = str(bclass) |
|
except: |
|
pass |
|
|
|
|
|
if detail_level >= self.str_detail_level: |
|
try: |
|
ostr = str(obj) |
|
if not detail_level and len(ostr) > string_max: |
|
ostr = ostr[:shalf] + ' <...> ' + ostr[-shalf:] |
|
|
|
|
|
ostr = ("\n" + " " * len("string_form".expandtabs())).join( |
|
q.strip() for q in ostr.split("\n") |
|
) |
|
out["string_form"] = ostr |
|
except: |
|
pass |
|
|
|
if ospace: |
|
out['namespace'] = ospace |
|
|
|
|
|
try: |
|
out['length'] = str(len(obj)) |
|
except Exception: |
|
pass |
|
|
|
|
|
binary_file = False |
|
fname = find_file(obj) |
|
if fname is None: |
|
|
|
|
|
binary_file = True |
|
else: |
|
if fname.endswith(('.so', '.dll', '.pyd')): |
|
binary_file = True |
|
elif fname.endswith('<string>'): |
|
fname = 'Dynamically generated function. No source code available.' |
|
out['file'] = compress_user(fname) |
|
|
|
|
|
if detail_level: |
|
|
|
|
|
linecache.checkcache() |
|
try: |
|
if isinstance(obj, property) or not binary_file: |
|
src = getsource(obj, oname) |
|
if src is not None: |
|
src = src.rstrip() |
|
out['source'] = src |
|
|
|
except Exception: |
|
pass |
|
|
|
|
|
if ds and not self._source_contains_docstring(out.get('source'), ds): |
|
out['docstring'] = ds |
|
|
|
|
|
if inspect.isclass(obj): |
|
out['isclass'] = True |
|
|
|
|
|
try: |
|
init_def = self._getdef(obj, oname) |
|
except AttributeError: |
|
init_def = None |
|
|
|
|
|
try: |
|
obj_init = obj.__init__ |
|
except AttributeError: |
|
init_ds = None |
|
else: |
|
if init_def is None: |
|
|
|
|
|
try: |
|
init_def = self._getdef(obj_init, oname) |
|
except AttributeError: |
|
pass |
|
init_ds = getdoc(obj_init) |
|
|
|
if init_ds == _object_init_docstring: |
|
init_ds = None |
|
|
|
if init_def: |
|
out['init_definition'] = init_def |
|
|
|
if init_ds: |
|
out['init_docstring'] = init_ds |
|
|
|
names = [sub.__name__ for sub in type.__subclasses__(obj)] |
|
if len(names) < 10: |
|
all_names = ', '.join(names) |
|
else: |
|
all_names = ', '.join(names[:10]+['...']) |
|
out['subclasses'] = all_names |
|
|
|
else: |
|
|
|
defln = self._getdef(obj, oname) |
|
if defln: |
|
out['definition'] = defln |
|
|
|
|
|
|
|
|
|
|
|
if ds: |
|
try: |
|
cls = getattr(obj,'__class__') |
|
except: |
|
class_ds = None |
|
else: |
|
class_ds = getdoc(cls) |
|
|
|
if class_ds in _builtin_type_docstrings: |
|
class_ds = None |
|
if class_ds and ds != class_ds: |
|
out['class_docstring'] = class_ds |
|
|
|
|
|
try: |
|
init_ds = getdoc(obj.__init__) |
|
|
|
if init_ds == _object_init_docstring: |
|
init_ds = None |
|
except AttributeError: |
|
init_ds = None |
|
if init_ds: |
|
out['init_docstring'] = init_ds |
|
|
|
|
|
if safe_hasattr(obj, '__call__') and not is_simple_callable(obj): |
|
call_def = self._getdef(obj.__call__, oname) |
|
if call_def and (call_def != out.get('definition')): |
|
|
|
|
|
out['call_def'] = call_def |
|
call_ds = getdoc(obj.__call__) |
|
|
|
if call_ds == _func_call_docstring: |
|
call_ds = None |
|
if call_ds: |
|
out['call_docstring'] = call_ds |
|
|
|
return out |
|
|
|
@staticmethod |
|
def _source_contains_docstring(src, doc): |
|
""" |
|
Check whether the source *src* contains the docstring *doc*. |
|
|
|
This is is helper function to skip displaying the docstring if the |
|
source already contains it, avoiding repetition of information. |
|
""" |
|
try: |
|
(def_node,) = ast.parse(dedent(src)).body |
|
return ast.get_docstring(def_node) == doc |
|
except Exception: |
|
|
|
|
|
|
|
return False |
|
|
|
def psearch(self,pattern,ns_table,ns_search=[], |
|
ignore_case=False,show_all=False, *, list_types=False): |
|
"""Search namespaces with wildcards for objects. |
|
|
|
Arguments: |
|
|
|
- pattern: string containing shell-like wildcards to use in namespace |
|
searches and optionally a type specification to narrow the search to |
|
objects of that type. |
|
|
|
- ns_table: dict of name->namespaces for search. |
|
|
|
Optional arguments: |
|
|
|
- ns_search: list of namespace names to include in search. |
|
|
|
- ignore_case(False): make the search case-insensitive. |
|
|
|
- show_all(False): show all names, including those starting with |
|
underscores. |
|
|
|
- list_types(False): list all available object types for object matching. |
|
""" |
|
|
|
|
|
|
|
type_pattern = 'all' |
|
filter = '' |
|
|
|
|
|
if list_types: |
|
page.page('\n'.join(sorted(typestr2type))) |
|
return |
|
|
|
cmds = pattern.split() |
|
len_cmds = len(cmds) |
|
if len_cmds == 1: |
|
|
|
filter = cmds[0] |
|
elif len_cmds == 2: |
|
|
|
filter,type_pattern = cmds |
|
else: |
|
raise ValueError('invalid argument string for psearch: <%s>' % |
|
pattern) |
|
|
|
|
|
for name in ns_search: |
|
if name not in ns_table: |
|
raise ValueError('invalid namespace <%s>. Valid names: %s' % |
|
(name,ns_table.keys())) |
|
|
|
|
|
search_result, namespaces_seen = set(), set() |
|
for ns_name in ns_search: |
|
ns = ns_table[ns_name] |
|
|
|
if id(ns) in namespaces_seen: |
|
continue |
|
namespaces_seen.add(id(ns)) |
|
tmp_res = list_namespace(ns, type_pattern, filter, |
|
ignore_case=ignore_case, show_all=show_all) |
|
search_result.update(tmp_res) |
|
|
|
page.page('\n'.join(sorted(search_result))) |
|
|
|
|
|
def _render_signature(obj_signature, obj_name) -> str: |
|
""" |
|
This was mostly taken from inspect.Signature.__str__. |
|
Look there for the comments. |
|
The only change is to add linebreaks when this gets too long. |
|
""" |
|
result = [] |
|
pos_only = False |
|
kw_only = True |
|
for param in obj_signature.parameters.values(): |
|
if param.kind == inspect.Parameter.POSITIONAL_ONLY: |
|
pos_only = True |
|
elif pos_only: |
|
result.append('/') |
|
pos_only = False |
|
|
|
if param.kind == inspect.Parameter.VAR_POSITIONAL: |
|
kw_only = False |
|
elif param.kind == inspect.Parameter.KEYWORD_ONLY and kw_only: |
|
result.append('*') |
|
kw_only = False |
|
|
|
result.append(str(param)) |
|
|
|
if pos_only: |
|
result.append('/') |
|
|
|
|
|
if len(obj_name) + sum(len(r) + 2 for r in result) > 75: |
|
|
|
rendered = '{}(\n{})'.format(obj_name, ''.join( |
|
' {},\n'.format(r) for r in result) |
|
) |
|
else: |
|
rendered = '{}({})'.format(obj_name, ', '.join(result)) |
|
|
|
if obj_signature.return_annotation is not inspect._empty: |
|
anno = inspect.formatannotation(obj_signature.return_annotation) |
|
rendered += ' -> {}'.format(anno) |
|
|
|
return rendered |
|
|