oinspect.py
1283 lines
| 41.7 KiB
| text/x-python
|
PythonLexer
|
r1032 | """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. | ||||
""" | ||||
|
r16579 | # Copyright (c) IPython Development Team. | ||
# Distributed under the terms of the Modified BSD License. | ||||
|
r1032 | __all__ = ['Inspector','InspectColors'] | ||
# stdlib modules | ||||
|
r28195 | from dataclasses import dataclass | ||
|
r23020 | from inspect import signature | ||
|
r28195 | from textwrap import dedent | ||
import ast | ||||
|
r27940 | import html | ||
|
r28195 | import inspect | ||
import io as stdlib_io | ||||
|
r1032 | import linecache | ||
import os | ||||
|
r1413 | import types | ||
|
r28195 | import warnings | ||
|
r25338 | |||
|
r28662 | |||
from typing import ( | ||||
cast, | ||||
Any, | ||||
Optional, | ||||
Dict, | ||||
Union, | ||||
List, | ||||
TypedDict, | ||||
TypeAlias, | ||||
Tuple, | ||||
) | ||||
|
r1413 | |||
|
r28591 | import traitlets | ||
|
r1032 | # IPython's own | ||
|
r2830 | from IPython.core import page | ||
|
r17023 | from IPython.lib.pretty import pretty | ||
|
r22961 | from IPython.testing.skipdoctest import skip_doctest | ||
|
r28662 | from IPython.utils import PyColorize, openpy | ||
|
r12965 | from IPython.utils.dir2 import safe_hasattr | ||
|
r20569 | from IPython.utils.path import compress_user | ||
|
r2498 | from IPython.utils.text import indent | ||
|
r28662 | from IPython.utils.wildcard import list_namespace, typestr2type | ||
|
r28591 | from IPython.utils.coloransi import TermColors | ||
|
r22109 | from IPython.utils.colorable import Colorable | ||
|
r23035 | from IPython.utils.decorators import undoc | ||
|
r1032 | |||
|
r22492 | from pygments import highlight | ||
from pygments.lexers import PythonLexer | ||||
from pygments.formatters import HtmlFormatter | ||||
|
r28195 | HOOK_NAME = "__custom_documentations__" | ||
UnformattedBundle: TypeAlias = Dict[str, List[Tuple[str, str]]] # List of (title, body) | ||||
Bundle: TypeAlias = Dict[str, str] | ||||
|
r28165 | |||
@dataclass | ||||
class OInfo: | ||||
ismagic: bool | ||||
isalias: bool | ||||
found: bool | ||||
|
r28167 | namespace: Optional[str] | ||
|
r28165 | parent: Any | ||
obj: Any | ||||
|
r28252 | 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 | ||||
""" | ||||
# We need to deprecate this at some point, but the warning will show in completion. | ||||
# Let's comment this for now and uncomment end of 2023 ish | ||||
# warnings.warn( | ||||
# f"OInfo dataclass with fields access since IPython 8.12 please use OInfo.{field} instead." | ||||
# "OInfo used to be a dict but a dataclass provide static fields verification with mypy." | ||||
# "This warning and backward compatibility `get()` method were added in 8.13.", | ||||
# DeprecationWarning, | ||||
# stacklevel=2, | ||||
# ) | ||||
return getattr(self, field) | ||||
|
r22492 | def pylight(code): | ||
return highlight(code, PythonLexer(), HtmlFormatter(noclasses=True)) | ||||
|
r14835 | # builtin docstrings to ignore | ||
_func_call_docstring = types.FunctionType.__call__.__doc__ | ||||
_object_init_docstring = object.__init__.__doc__ | ||||
_builtin_type_docstrings = { | ||||
|
r17025 | inspect.getdoc(t) for t in (types.ModuleType, types.MethodType, | ||
types.FunctionType, property) | ||||
|
r14835 | } | ||
|
r15362 | |||
_builtin_func_type = type(all) | ||||
_builtin_meth_type = type(str.upper) # Bound methods have the same type as builtin functions | ||||
|
r1032 | #**************************************************************************** | ||
# Builtin color schemes | ||||
Colors = TermColors # just a shorthand | ||||
|
r21774 | InspectColors = PyColorize.ANSICodeColors | ||
|
r1032 | |||
#**************************************************************************** | ||||
|
r2931 | # Auxiliary functions and objects | ||
|
r28650 | |||
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] | ||||
# These won't be printed but will be used to determine how to | ||||
# format the object | ||||
ismagic: bool | ||||
isalias: bool | ||||
isclass: bool | ||||
found: bool | ||||
name: str | ||||
|
r28662 | _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}") | ||||
|
r28650 | |||
@dataclass | ||||
class InspectorHookData: | ||||
"""Data passed to the mime hook""" | ||||
obj: Any | ||||
info: Optional[OInfo] | ||||
info_dict: InfoDict | ||||
detail_level: int | ||||
omit_sections: list[str] | ||||
|
r2931 | |||
|
r28650 | @undoc | ||
|
r28662 | def object_info( | ||
*, | ||||
name: str, | ||||
found: bool, | ||||
isclass: bool = False, | ||||
isalias: bool = False, | ||||
ismagic: bool = False, | ||||
**kw, | ||||
) -> InfoDict: | ||||
|
r28658 | """Make an object info dict with all fields present.""" | ||
|
r28662 | infodict = kw | ||
infodict = {k: None for k in _info_fields if k not in infodict} | ||||
infodict["name"] = name # type: ignore | ||||
infodict["found"] = found # type: ignore | ||||
infodict["isclass"] = isclass # type: ignore | ||||
infodict["isalias"] = isalias # type: ignore | ||||
infodict["ismagic"] = ismagic # type: ignore | ||||
return InfoDict(**infodict) # type:ignore | ||||
|
r2931 | |||
|
r8322 | 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) | ||||
# run contents of file through pager starting at line where the object | ||||
# is defined, as long as the file isn't binary and is actually on the | ||||
# filesystem. | ||||
if ofile is None: | ||||
return None | ||||
elif ofile.endswith(('.so', '.dll', '.pyd')): | ||||
return None | ||||
elif not os.path.isfile(ofile): | ||||
return None | ||||
else: | ||||
# Print only text files, not extension binaries. Note that | ||||
# getsourcelines returns lineno with 1-offset and page() uses | ||||
# 0-offset, so we must adjust. | ||||
|
r15467 | with stdlib_io.open(ofile, 'rb') as buffer: # Tweaked to use io.open for Python 2 | ||
|
r28591 | encoding, _lines = openpy.detect_encoding(buffer.readline) | ||
|
r8322 | return encoding | ||
|
r28650 | |||
|
r28662 | def getdoc(obj) -> Union[str, None]: | ||
|
r1032 | """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 | ||||
|
r22460 | (like Pyro proxies) to still be inspected by ipython's ? system. | ||
""" | ||||
|
r1032 | # Allow objects to offer customized documentation via a getdoc method: | ||
try: | ||||
|
r5535 | ds = obj.getdoc() | ||
except Exception: | ||||
|
r1032 | pass | ||
else: | ||||
|
r23037 | if isinstance(ds, str): | ||
|
r5536 | return inspect.cleandoc(ds) | ||
|
r24004 | docstr = inspect.getdoc(obj) | ||
|
r25333 | return docstr | ||
|
r1032 | |||
|
r1413 | |||
|
r25333 | def getsource(obj, oname='') -> Union[str,None]: | ||
|
r1032 | """Wrapper around inspect.getsource. | ||
This can be modified by other projects to provide customized source | ||||
extraction. | ||||
|
r17023 | Parameters | ||
---------- | ||||
obj : object | ||||
an object whose source code we will attempt to extract | ||||
oname : str | ||||
(optional) a name under which the object is known | ||||
|
r1032 | |||
|
r17023 | Returns | ||
------- | ||||
src : unicode or None | ||||
|
r1032 | |||
|
r17023 | """ | ||
|
r1032 | |||
|
r17023 | 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 '' | ||||
|
r25333 | sources.append(''.join(('# ', oname_prefix, attrname))) | ||
|
r17023 | if inspect.isfunction(fn): | ||
|
r28167 | _src = getsource(fn) | ||
if _src: | ||||
# assert _src is not None, "please mypy" | ||||
sources.append(dedent(_src)) | ||||
|
r17023 | else: | ||
# Default str/repr only prints function name, | ||||
# pretty.pretty prints module name too. | ||||
|
r25333 | sources.append( | ||
'%s%s = %s\n' % (oname_prefix, attrname, pretty(fn)) | ||||
) | ||||
|
r17023 | if sources: | ||
return '\n'.join(sources) | ||||
else: | ||||
return None | ||||
|
r4266 | |||
|
r1032 | else: | ||
|
r17023 | # Get source for non-property objects. | ||
|
r21505 | obj = _get_wrapped(obj) | ||
|
r17023 | |||
|
r1228 | try: | ||
src = inspect.getsource(obj) | ||||
except TypeError: | ||||
|
r17023 | # The object itself provided no meaningful source, try looking for | ||
# its class definition instead. | ||||
|
r27233 | try: | ||
src = inspect.getsource(obj.__class__) | ||||
|
r27234 | except (OSError, TypeError): | ||
|
r27233 | return None | ||
|
r27234 | except OSError: | ||
return None | ||||
|
r17023 | |||
|
r25333 | return src | ||
|
r1032 | |||
|
r15362 | |||
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)) | ||||
|
r25244 | @undoc | ||
|
r1413 | def getargspec(obj): | ||
|
r25450 | """Wrapper around :func:`inspect.getfullargspec` | ||
|
r24820 | |||
|
r15335 | In addition to functions and methods, this can also handle objects with a | ||
``__call__`` attribute. | ||||
|
r25244 | |||
DEPRECATED: Deprecated since 7.10. Do not use, will be removed. | ||||
|
r15335 | """ | ||
|
r25244 | |||
warnings.warn('`getargspec` function is deprecated as of IPython 7.10' | ||||
'and will be removed in future versions.', DeprecationWarning, stacklevel=2) | ||||
|
r15362 | if safe_hasattr(obj, '__call__') and not is_simple_callable(obj): | ||
|
r15335 | obj = obj.__call__ | ||
|
r23086 | return inspect.getfullargspec(obj) | ||
|
r1413 | |||
|
r25244 | @undoc | ||
|
r3051 | 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. | ||||
|
r25244 | |||
|
r27217 | DEPRECATED (since 7.10): Do not use; will be removed in future versions. | ||
|
r3051 | """ | ||
|
r25244 | |||
warnings.warn('`format_argspec` function is deprecated as of IPython 7.10' | ||||
'and will be removed in future versions.', DeprecationWarning, stacklevel=2) | ||||
|
r3051 | return inspect.formatargspec(argspec['args'], argspec['varargs'], | ||
argspec['varkw'], argspec['defaults']) | ||||
|
r23035 | @undoc | ||
|
r3051 | def call_tip(oinfo, format_call=True): | ||
|
r27217 | """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, | ||||
) | ||||
|
r3051 | # Get call definition | ||
|
r3934 | argspec = oinfo.get('argspec') | ||
|
r3051 | if argspec is None: | ||
call_line = None | ||||
else: | ||||
# Callable objects will have 'self' as their first argument, prune | ||||
# it out if it's there for clarity (since users do *not* pass an | ||||
# extra first argument explicitly). | ||||
try: | ||||
has_self = argspec['args'][0] == 'self' | ||||
except (KeyError, IndexError): | ||||
pass | ||||
else: | ||||
if has_self: | ||||
argspec['args'] = argspec['args'][1:] | ||||
|
r4872 | |||
|
r3051 | call_line = oinfo['name']+format_argspec(argspec) | ||
# Now get docstring. | ||||
# The priority is: call docstring, constructor docstring, main one. | ||||
|
r3934 | doc = oinfo.get('call_docstring') | ||
|
r3051 | if doc is None: | ||
|
r3934 | doc = oinfo.get('init_docstring') | ||
|
r3051 | if doc is None: | ||
|
r3934 | doc = oinfo.get('docstring','') | ||
|
r3051 | |||
return call_line, doc | ||||
|
r1413 | |||
|
r21505 | def _get_wrapped(obj): | ||
|
r21905 | """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 | ||||
|
r21505 | while safe_hasattr(obj, '__wrapped__'): | ||
obj = obj.__wrapped__ | ||||
|
r21905 | i += 1 | ||
if i > 100: | ||||
# __wrapped__ is probably a lie, so return the thing we started with | ||||
return orig_obj | ||||
|
r21505 | return obj | ||
|
r28591 | def find_file(obj) -> Optional[str]: | ||
|
r7290 | """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 | ||||
|
r27289 | The absolute path to the file where the object was defined. | ||
|
r7290 | """ | ||
|
r21505 | obj = _get_wrapped(obj) | ||
|
r7520 | |||
|
r28591 | fname: Optional[str] = None | ||
|
r7290 | try: | ||
fname = inspect.getabsfile(obj) | ||||
|
r27233 | except TypeError: | ||
|
r7290 | # For an instance, the file that matters is where its class was | ||
# declared. | ||||
|
r27233 | try: | ||
fname = inspect.getabsfile(obj.__class__) | ||||
except (OSError, TypeError): | ||||
# Can happen for builtins | ||||
pass | ||||
|
r27234 | except OSError: | ||
|
r7520 | pass | ||
|
r27234 | |||
|
r28591 | return fname | ||
|
r7290 | |||
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 | ||||
|
r27289 | The line number where the object definition starts. | ||
|
r7290 | """ | ||
|
r21505 | obj = _get_wrapped(obj) | ||
|
r24820 | |||
|
r7290 | try: | ||
|
r27234 | lineno = inspect.getsourcelines(obj)[1] | ||
except TypeError: | ||||
# For instances, try the class object like getsource() does | ||||
|
r7290 | try: | ||
|
r27233 | lineno = inspect.getsourcelines(obj.__class__)[1] | ||
|
r27234 | except (OSError, TypeError): | ||
return None | ||||
except OSError: | ||||
|
r7290 | return None | ||
return lineno | ||||
|
r22109 | class Inspector(Colorable): | ||
|
r22460 | |||
|
r28591 | 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, | ||||
): | ||||
|
r22109 | super(Inspector, self).__init__(parent=parent, config=config) | ||
|
r1032 | self.color_table = color_table | ||
|
r22109 | self.parser = PyColorize.Parser(out='str', parent=self, style=scheme) | ||
|
r1032 | self.format = self.parser.format | ||
self.str_detail_level = str_detail_level | ||||
self.set_active_scheme(scheme) | ||||
|
r25333 | def _getdef(self,obj,oname='') -> Union[str,None]: | ||
|
r8707 | """Return the call signature for any callable object. | ||
|
r1032 | |||
If any exception is generated, None is returned instead and the | ||||
exception is suppressed.""" | ||||
|
r28463 | if not callable(obj): | ||
|
r28464 | return None | ||
|
r1032 | try: | ||
|
r25333 | return _render_signature(signature(obj), oname) | ||
|
r1032 | except: | ||
return None | ||||
|
r4872 | |||
|
r25333 | def __head(self,h) -> str: | ||
|
r1032 | """Return a header string with proper colors.""" | ||
return '%s%s%s' % (self.color_table.active_colors.header,h, | ||||
self.color_table.active_colors.normal) | ||||
|
r7290 | def set_active_scheme(self, scheme): | ||
|
r23425 | if scheme is not None: | ||
self.color_table.set_active_scheme(scheme) | ||||
self.parser.color_table.set_active_scheme(scheme) | ||||
|
r4872 | |||
|
r7290 | def noinfo(self, msg, oname): | ||
|
r1032 | """Generic message when no information is found.""" | ||
|
r7817 | print('No %s found' % msg, end=' ') | ||
|
r1032 | if oname: | ||
|
r7817 | print('for %s' % oname) | ||
|
r1032 | else: | ||
|
r7817 | print() | ||
|
r4872 | |||
|
r7290 | def pdef(self, obj, oname=''): | ||
|
r8707 | """Print the call signature for any callable object. | ||
|
r1032 | |||
If the object is a class, print the constructor information.""" | ||||
if not callable(obj): | ||||
|
r7817 | print('Object is not callable.') | ||
|
r1032 | return | ||
header = '' | ||||
if inspect.isclass(obj): | ||||
header = self.__head('Class constructor information:\n') | ||||
|
r23087 | |||
|
r1032 | |||
|
r2929 | output = self._getdef(obj,oname) | ||
|
r1032 | if output is None: | ||
self.noinfo('definition header',oname) | ||||
else: | ||||
|
r22192 | print(header,self.format(output), end=' ') | ||
|
r1032 | |||
|
r22961 | # In Python 3, all classes are new-style, so they all have __init__. | ||
@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 # For convenience | ||||
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)) | ||||
|
r17023 | def psource(self, obj, oname=''): | ||
|
r1032 | """Print the source code for an object.""" | ||
# Flush the source cache because inspect can return out-of-date source | ||||
linecache.checkcache() | ||||
try: | ||||
|
r17023 | src = getsource(obj, oname=oname) | ||
except Exception: | ||||
src = None | ||||
if src is None: | ||||
self.noinfo('source', oname) | ||||
|
r1032 | else: | ||
|
r8312 | page.page(self.format(src)) | ||
|
r1032 | |||
|
r7290 | def pfile(self, obj, oname=''): | ||
|
r1032 | """Show the whole file where an object was defined.""" | ||
|
r22460 | |||
|
r7290 | lineno = find_source_lines(obj) | ||
if lineno is None: | ||||
self.noinfo('file', oname) | ||||
|
r1228 | return | ||
|
r7290 | ofile = find_file(obj) | ||
# run contents of file through pager starting at line where the object | ||||
# is defined, as long as the file isn't binary and is actually on the | ||||
# filesystem. | ||||
|
r28662 | 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) | ||||
|
r1228 | elif not os.path.isfile(ofile): | ||
|
r7817 | print('File %r does not exist, not printing.' % ofile) | ||
|
r1032 | else: | ||
|
r1228 | # Print only text files, not extension binaries. Note that | ||
# getsourcelines returns lineno with 1-offset and page() uses | ||||
# 0-offset, so we must adjust. | ||||
|
r8304 | page.page(self.format(openpy.read_py_file(ofile, skip_encoding_cookie=False)), lineno - 1) | ||
|
r4872 | |||
|
r25338 | |||
def _mime_format(self, text:str, formatter=None) -> dict: | ||||
|
r22460 | """Return a mime bundle representation of the input text. | ||
|
r22459 | |||
|
r22460 | - if `formatter` is None, the returned mime bundle has | ||
|
r27623 | a ``text/plain`` field, with the input text. | ||
a ``text/html`` field with a ``<pre>`` tag containing the input text. | ||||
|
r22460 | |||
|
r27623 | - 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. | ||||
|
r22460 | |||
Note: | ||||
Formatters returning strings are supported but this behavior is deprecated. | ||||
""" | ||||
defaults = { | ||||
|
r27945 | "text/plain": text, | ||
"text/html": f"<pre>{html.escape(text)}</pre>", | ||||
|
r22460 | } | ||
if formatter is None: | ||||
return defaults | ||||
else: | ||||
formatted = formatter(text) | ||||
if not isinstance(formatted, dict): | ||||
# Handle the deprecated behavior of a formatter returning | ||||
# a string instead of a mime bundle. | ||||
|
r27945 | return {"text/plain": formatted, "text/html": f"<pre>{formatted}</pre>"} | ||
|
r22460 | |||
else: | ||||
return dict(defaults, **formatted) | ||||
|
r28195 | def format_mime(self, bundle: UnformattedBundle) -> Bundle: | ||
|
r27936 | """Format a mimebundle being created by _make_info_unformatted into a real mimebundle""" | ||
|
r27937 | # Format text/plain mimetype | ||
|
r28195 | assert isinstance(bundle["text/plain"], list) | ||
for item in bundle["text/plain"]: | ||||
assert isinstance(item, tuple) | ||||
|
r22537 | |||
|
r28195 | new_b: Bundle = {} | ||
lines = [] | ||||
_len = max(len(h) for h, _ in bundle["text/plain"]) | ||||
|
r22537 | |||
|
r28195 | 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}" | ||||
|
r27938 | ) | ||
|
r28195 | |||
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) | ||||
# Format the text/html mimetype | ||||
if isinstance(bundle["text/html"], (list, tuple)): | ||||
# bundle['text/html'] is a list of (head, formatted body) pairs | ||||
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: | ||||
|
r28593 | new_b[k] = bundle[k] # type:ignore | ||
|
r28195 | return new_b | ||
|
r22537 | |||
|
r27938 | def _append_info_field( | ||
|
r28195 | self, | ||
bundle: UnformattedBundle, | ||||
title: str, | ||||
key: str, | ||||
info, | ||||
|
r28662 | omit_sections: List[str], | ||
|
r28195 | formatter, | ||
|
r27938 | ): | ||
|
r27936 | """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) | ||||
|
r27938 | bundle["text/plain"].append((title, formatted_field["text/plain"])) | ||
bundle["text/html"].append((title, formatted_field["text/html"])) | ||||
|
r27936 | |||
|
r28195 | def _make_info_unformatted( | ||
self, obj, info, formatter, detail_level, omit_sections | ||||
) -> UnformattedBundle: | ||||
|
r27936 | """Assemble the mimebundle as unformatted lists of information""" | ||
|
r28195 | bundle: UnformattedBundle = { | ||
|
r27938 | "text/plain": [], | ||
"text/html": [], | ||||
|
r22460 | } | ||
|
r27936 | # A convenience function to simplify calls below | ||
|
r28195 | def append_field( | ||
bundle: UnformattedBundle, title: str, key: str, formatter=None | ||||
): | ||||
|
r27938 | self._append_info_field( | ||
bundle, | ||||
title=title, | ||||
key=key, | ||||
info=info, | ||||
omit_sections=omit_sections, | ||||
formatter=formatter, | ||||
) | ||||
|
r22460 | |||
|
r28195 | def code_formatter(text) -> Bundle: | ||
|
r22460 | return { | ||
'text/plain': self.format(text), | ||||
|
r22469 | 'text/html': pylight(text) | ||
|
r22460 | } | ||
|
r20568 | |||
|
r27938 | if info["isalias"]: | ||
append_field(bundle, "Repr", "string_form") | ||||
|
r20568 | |||
elif info['ismagic']: | ||||
|
r22460 | if detail_level > 0: | ||
|
r27938 | append_field(bundle, "Source", "source", code_formatter) | ||
|
r20697 | else: | ||
|
r27938 | append_field(bundle, "Docstring", "docstring", formatter) | ||
append_field(bundle, "File", "file") | ||||
|
r20568 | |||
elif info['isclass'] or is_simple_callable(obj): | ||||
# Functions, methods, classes | ||||
|
r27938 | 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) | ||||
|
r20568 | else: | ||
|
r27938 | append_field(bundle, "Init docstring", "init_docstring", formatter) | ||
|
r20568 | |||
|
r27938 | append_field(bundle, "File", "file") | ||
append_field(bundle, "Type", "type_name") | ||||
append_field(bundle, "Subclasses", "subclasses") | ||||
|
r20568 | |||
|
r1032 | else: | ||
|
r20568 | # General Python objects | ||
|
r27938 | 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") | ||||
|
r20568 | |||
# Namespace | ||||
|
r27938 | if info["namespace"] != "Interactive": | ||
append_field(bundle, "Namespace", "namespace") | ||||
|
r20568 | |||
|
r27938 | append_field(bundle, "Length", "length") | ||
append_field(bundle, "File", "file") | ||||
|
r24820 | |||
|
r20568 | # Source or docstring, depending on detail level and whether | ||
# source found. | ||||
|
r27938 | if detail_level > 0 and info["source"]: | ||
append_field(bundle, "Source", "source", code_formatter) | ||||
|
r22460 | else: | ||
|
r27938 | append_field(bundle, "Docstring", "docstring", formatter) | ||
|
r27936 | |||
|
r27938 | append_field(bundle, "Class docstring", "class_docstring", formatter) | ||
append_field(bundle, "Init docstring", "init_docstring", formatter) | ||||
append_field(bundle, "Call docstring", "call_docstring", formatter) | ||||
|
r27936 | return bundle | ||
|
r22537 | |||
|
r24820 | |||
|
r27936 | def _get_info( | ||
|
r28195 | self, | ||
obj: Any, | ||||
oname: str = "", | ||||
formatter=None, | ||||
info: Optional[OInfo] = None, | ||||
|
r28662 | detail_level: int = 0, | ||
omit_sections: Union[List[str], Tuple[()]] = (), | ||||
|
r28195 | ) -> Bundle: | ||
|
r27936 | """Retrieve an info dict and format it. | ||
|
r22460 | |||
|
r27936 | 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. | ||||
|
r28662 | omit_sections : list[str] | ||
|
r27936 | Titles or keys to omit from output (can be set, tuple, etc., anything supporting `in`) | ||
""" | ||||
|
r28195 | info_dict = self.info(obj, oname=oname, info=info, detail_level=detail_level) | ||
|
r28662 | omit_sections = list(omit_sections) | ||
|
r28650 | |||
|
r27938 | bundle = self._make_info_unformatted( | ||
|
r28195 | obj, | ||
info_dict, | ||||
formatter, | ||||
detail_level=detail_level, | ||||
omit_sections=omit_sections, | ||||
|
r27938 | ) | ||
|
r28650 | if self.mime_hooks: | ||
hook_data = InspectorHookData( | ||||
obj=obj, | ||||
info=info, | ||||
info_dict=info_dict, | ||||
detail_level=detail_level, | ||||
omit_sections=omit_sections, | ||||
) | ||||
|
r28662 | for key, hook in self.mime_hooks.items(): # type:ignore | ||
|
r28650 | 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 | ||||
|
r27936 | return self.format_mime(bundle) | ||
|
r22460 | |||
|
r27110 | def pinfo( | ||
self, | ||||
obj, | ||||
oname="", | ||||
formatter=None, | ||||
|
r28195 | info: Optional[OInfo] = None, | ||
|
r27110 | detail_level=0, | ||
enable_html_pager=True, | ||||
|
r27187 | omit_sections=(), | ||
|
r27110 | ): | ||
|
r16579 | """Show detailed information about an object. | ||
Optional arguments: | ||||
- oname: name of the variable pointing to the object. | ||||
|
r1032 | |||
|
r22460 | - 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 | ||||
|
r24084 | in the form of a dictionary. | ||
|
r22460 | |||
Although the support of custom formatter returning a string | ||||
instead of a mime type bundle is deprecated. | ||||
|
r16579 | |||
- info: a structure with some information fields which may have been | ||||
precomputed already. | ||||
- detail_level: if set to 1, more information is given. | ||||
|
r27109 | |||
- omit_sections: set of section keys and titles to omit | ||||
|
r16579 | """ | ||
|
r28195 | assert info is not None | ||
info_b: Bundle = self._get_info( | ||||
|
r27110 | obj, oname, formatter, info, detail_level, omit_sections=omit_sections | ||
) | ||||
|
r22472 | if not enable_html_pager: | ||
|
r28195 | del info_b["text/html"] | ||
page.page(info_b) | ||||
|
r22460 | |||
|
r27344 | def _info(self, obj, oname="", info=None, detail_level=0): | ||
""" | ||||
|
r27351 | Inspector.info() was likely improperly marked as deprecated | ||
|
r27344 | while only a parameter was deprecated. We "un-deprecate" it. | ||
""" | ||||
warnings.warn( | ||||
|
r27351 | "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.", | ||||
|
r27344 | DeprecationWarning, | ||
stacklevel=2, | ||||
) | ||||
return self.info(obj, oname=oname, info=info, detail_level=detail_level) | ||||
|
r22460 | |||
|
r28650 | def info(self, obj, oname="", info=None, detail_level=0) -> InfoDict: | ||
|
r2931 | """Compute a dict with detailed information about an object. | ||
|
r23673 | Parameters | ||
|
r27289 | ---------- | ||
obj : any | ||||
|
r23673 | An object to find information about | ||
|
r27289 | oname : str (default: '') | ||
|
r23673 | Name of the variable pointing to `obj`. | ||
|
r27289 | info : (default: None) | ||
|
r23673 | A struct (dict like with attr access) with some information fields | ||
which may have been precomputed already. | ||||
|
r27289 | detail_level : int (default:0) | ||
|
r23673 | If set to 1, more information is given. | ||
Returns | ||||
|
r27289 | ------- | ||
|
r28650 | An object info dict with known fields from `info_fields` (see `InfoDict`). | ||
|
r2931 | """ | ||
if info is None: | ||||
|
r23673 | ismagic = False | ||
isalias = False | ||||
|
r2931 | ospace = '' | ||
else: | ||||
ismagic = info.ismagic | ||||
isalias = info.isalias | ||||
ospace = info.namespace | ||||
|
r3051 | |||
|
r2931 | # Get docstring, special-casing aliases: | ||
|
r28195 | att_name = oname.split(".")[-1] | ||
parents_docs = None | ||||
prelude = "" | ||||
|
r28207 | if info and info.parent is not None and hasattr(info.parent, HOOK_NAME): | ||
|
r28195 | parents_docs_dict = getattr(info.parent, HOOK_NAME) | ||
parents_docs = parents_docs_dict.get(att_name, None) | ||||
|
r28650 | out: InfoDict = cast( | ||
InfoDict, | ||||
{ | ||||
|
r28662 | **{field: None for field in _info_fields}, | ||
|
r28650 | **{ | ||
"name": oname, | ||||
"found": True, | ||||
"isalias": isalias, | ||||
"ismagic": ismagic, | ||||
"subclasses": None, | ||||
}, | ||||
}, | ||||
|
r28195 | ) | ||
if parents_docs: | ||||
ds = parents_docs | ||||
elif isalias: | ||||
|
r2931 | 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: | ||||
|
r28167 | ds_or_None = getdoc(obj) | ||
if ds_or_None is None: | ||||
|
r2931 | ds = '<no docstring>' | ||
|
r28167 | else: | ||
ds = ds_or_None | ||||
|
r2931 | |||
|
r28195 | ds = prelude + ds | ||
|
r3051 | # store output in a dict, we initialize it here and fill it as we go | ||
|
r4872 | |||
|
r2931 | string_max = 200 # max size of strings to show (snipped if longer) | ||
|
r22460 | shalf = int((string_max - 5) / 2) | ||
|
r2931 | |||
if ismagic: | ||||
|
r23673 | out['type_name'] = 'Magic function' | ||
|
r2931 | elif isalias: | ||
|
r23673 | out['type_name'] = 'System alias' | ||
|
r2931 | else: | ||
|
r23673 | out['type_name'] = type(obj).__name__ | ||
|
r2931 | |||
try: | ||||
bclass = obj.__class__ | ||||
out['base_class'] = str(bclass) | ||||
|
r23673 | except: | ||
pass | ||||
|
r2931 | |||
# String form, but snip if too long in ? form (full in ??) | ||||
if detail_level >= self.str_detail_level: | ||||
try: | ||||
ostr = str(obj) | ||||
|
r28650 | if not detail_level and len(ostr) > string_max: | ||
|
r2931 | ostr = ostr[:shalf] + ' <...> ' + ostr[-shalf:] | ||
|
r28650 | # TODO: `'string_form'.expandtabs()` seems wrong, but | ||
# it was (nearly) like this since the first commit ever. | ||||
ostr = ("\n" + " " * len("string_form".expandtabs())).join( | ||||
q.strip() for q in ostr.split("\n") | ||||
) | ||||
out["string_form"] = ostr | ||||
|
r2931 | except: | ||
pass | ||||
if ospace: | ||||
out['namespace'] = ospace | ||||
# Length (for strings and lists) | ||||
try: | ||||
out['length'] = str(len(obj)) | ||||
|
r23673 | except Exception: | ||
pass | ||||
|
r2931 | |||
# Filename where object was defined | ||||
binary_file = False | ||||
|
r7290 | fname = find_file(obj) | ||
if fname is None: | ||||
|
r2931 | # if anything goes wrong, we don't want to show source, so it's as | ||
# if the file was binary | ||||
binary_file = True | ||||
|
r7290 | else: | ||
if fname.endswith(('.so', '.dll', '.pyd')): | ||||
binary_file = True | ||||
elif fname.endswith('<string>'): | ||||
fname = 'Dynamically generated function. No source code available.' | ||||
|
r20569 | out['file'] = compress_user(fname) | ||
|
r4872 | |||
|
r17023 | # Original source code for a callable, class or property. | ||
|
r2931 | if detail_level: | ||
# Flush the source cache because inspect can return out-of-date | ||||
# source | ||||
linecache.checkcache() | ||||
try: | ||||
|
r17023 | if isinstance(obj, property) or not binary_file: | ||
src = getsource(obj, oname) | ||||
if src is not None: | ||||
src = src.rstrip() | ||||
out['source'] = src | ||||
|
r3856 | except Exception: | ||
|
r3857 | pass | ||
|
r4872 | |||
|
r17023 | # Add docstring only if no source is to be shown (avoid repetitions). | ||
|
r24118 | if ds and not self._source_contains_docstring(out.get('source'), ds): | ||
|
r17023 | out['docstring'] = ds | ||
|
r2931 | |||
# Constructor docstring for classes | ||||
if inspect.isclass(obj): | ||||
|
r3856 | out['isclass'] = True | ||
|
r22171 | |||
|
r22176 | # get the init signature: | ||
|
r22171 | try: | ||
init_def = self._getdef(obj, oname) | ||||
except AttributeError: | ||||
init_def = None | ||||
|
r22176 | |||
|
r22171 | # get the __init__ docstring | ||
|
r2931 | try: | ||
|
r22171 | obj_init = obj.__init__ | ||
|
r2931 | except AttributeError: | ||
|
r22176 | init_ds = None | ||
|
r2931 | else: | ||
|
r22539 | if init_def is None: | ||
# Get signature from init if top-level sig failed. | ||||
# Can happen for built-in types (list, etc.). | ||||
try: | ||||
init_def = self._getdef(obj_init, oname) | ||||
except AttributeError: | ||||
pass | ||||
|
r22176 | init_ds = getdoc(obj_init) | ||
|
r2931 | # Skip Python's auto-generated docstrings | ||
|
r14835 | if init_ds == _object_init_docstring: | ||
|
r2931 | init_ds = None | ||
|
r22539 | if init_def: | ||
out['init_definition'] = init_def | ||||
|
r22171 | if init_ds: | ||
out['init_docstring'] = init_ds | ||||
|
r3051 | |||
|
r24946 | names = [sub.__name__ for sub in type.__subclasses__(obj)] | ||
|
r24858 | if len(names) < 10: | ||
all_names = ', '.join(names) | ||||
else: | ||||
all_names = ', '.join(names[:10]+['...']) | ||||
|
r24820 | out['subclasses'] = all_names | ||
|
r2931 | # and class docstring for instances: | ||
|
r3856 | else: | ||
|
r15711 | # reconstruct the function definition and print it: | ||
defln = self._getdef(obj, oname) | ||||
if defln: | ||||
|
r22460 | out['definition'] = defln | ||
|
r15711 | |||
|
r2931 | # First, check whether the instance docstring is identical to the | ||
# class one, and print it separately if they don't coincide. In | ||||
# most cases they will, but it's nice to print all the info for | ||||
# objects which use instance-customized docstrings. | ||||
if ds: | ||||
try: | ||||
cls = getattr(obj,'__class__') | ||||
except: | ||||
class_ds = None | ||||
else: | ||||
class_ds = getdoc(cls) | ||||
# Skip Python's auto-generated docstrings | ||||
|
r14835 | if class_ds in _builtin_type_docstrings: | ||
|
r2931 | class_ds = None | ||
if class_ds and ds != class_ds: | ||||
|
r3051 | out['class_docstring'] = class_ds | ||
|
r2931 | |||
# Next, try to show constructor docstrings | ||||
try: | ||||
init_ds = getdoc(obj.__init__) | ||||
# Skip Python's auto-generated docstrings | ||||
|
r14835 | if init_ds == _object_init_docstring: | ||
|
r2931 | init_ds = None | ||
except AttributeError: | ||||
init_ds = None | ||||
if init_ds: | ||||
|
r3051 | out['init_docstring'] = init_ds | ||
|
r2931 | |||
# Call form docstring for callable instances | ||||
|
r15362 | if safe_hasattr(obj, '__call__') and not is_simple_callable(obj): | ||
|
r3051 | call_def = self._getdef(obj.__call__, oname) | ||
|
r22465 | if call_def and (call_def != out.get('definition')): | ||
|
r15711 | # it may never be the case that call def and definition differ, | ||
# but don't include the same signature twice | ||||
|
r22465 | out['call_def'] = call_def | ||
|
r2931 | call_ds = getdoc(obj.__call__) | ||
# Skip Python's auto-generated docstrings | ||||
|
r14835 | if call_ds == _func_call_docstring: | ||
|
r2931 | call_ds = None | ||
if call_ds: | ||||
|
r3051 | out['call_docstring'] = call_ds | ||
|
r28650 | return out | ||
|
r2931 | |||
|
r24118 | @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: | ||||
|
r28195 | (def_node,) = ast.parse(dedent(src)).body | ||
return ast.get_docstring(def_node) == doc # type: ignore[arg-type] | ||||
|
r24118 | except Exception: | ||
# The source can become invalid or even non-existent (because it | ||||
# is re-fetched from the source file) so the above code fail in | ||||
# arbitrary ways. | ||||
return False | ||||
|
r1032 | def psearch(self,pattern,ns_table,ns_search=[], | ||
|
r25004 | ignore_case=False,show_all=False, *, list_types=False): | ||
|
r1032 | """Search namespaces with wildcards for objects. | ||
Arguments: | ||||
- pattern: string containing shell-like wildcards to use in namespace | ||||
|
r12553 | searches and optionally a type specification to narrow the search to | ||
objects of that type. | ||||
|
r1032 | |||
- ns_table: dict of name->namespaces for search. | ||||
Optional arguments: | ||||
|
r4872 | |||
|
r1032 | - 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 | ||||
|
r12553 | underscores. | ||
|
r27289 | |||
|
r24996 | - list_types(False): list all available object types for object matching. | ||
|
r1032 | """ | ||
|
r28756 | # print('ps pattern:<%r>' % pattern) # dbg | ||
|
r4872 | |||
|
r1032 | # defaults | ||
type_pattern = 'all' | ||||
filter = '' | ||||
|
r24996 | # list all object types | ||
if list_types: | ||||
page.page('\n'.join(sorted(typestr2type))) | ||||
return | ||||
|
r1032 | cmds = pattern.split() | ||
len_cmds = len(cmds) | ||||
if len_cmds == 1: | ||||
# Only filter pattern given | ||||
filter = cmds[0] | ||||
elif len_cmds == 2: | ||||
# Both filter and type specified | ||||
filter,type_pattern = cmds | ||||
else: | ||||
raise ValueError('invalid argument string for psearch: <%s>' % | ||||
pattern) | ||||
# filter search namespaces | ||||
for name in ns_search: | ||||
if name not in ns_table: | ||||
raise ValueError('invalid namespace <%s>. Valid names: %s' % | ||||
(name,ns_table.keys())) | ||||
|
r28756 | # print('type_pattern:',type_pattern) # dbg | ||
|
r5550 | search_result, namespaces_seen = set(), set() | ||
|
r1032 | for ns_name in ns_search: | ||
ns = ns_table[ns_name] | ||||
|
r5550 | # Normally, locals and globals are the same, so we just check one. | ||
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))) | ||||
|
r24841 | |||
|
r25333 | def _render_signature(obj_signature, obj_name) -> str: | ||
|
r24847 | """ | ||
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(): | ||||
|
r28167 | if param.kind == inspect.Parameter.POSITIONAL_ONLY: | ||
|
r24847 | pos_only = True | ||
elif pos_only: | ||||
|
r24841 | result.append('/') | ||
|
r24847 | pos_only = False | ||
|
r24841 | |||
|
r28167 | if param.kind == inspect.Parameter.VAR_POSITIONAL: | ||
|
r24847 | kw_only = False | ||
|
r28167 | elif param.kind == inspect.Parameter.KEYWORD_ONLY and kw_only: | ||
|
r24847 | result.append('*') | ||
kw_only = False | ||||
result.append(str(param)) | ||||
if pos_only: | ||||
result.append('/') | ||||
# add up name, parameters, braces (2), and commas | ||||
if len(obj_name) + sum(len(r) + 2 for r in result) > 75: | ||||
# This doesn’t fit behind “Signature: ” in an inspect window. | ||||
|
r24881 | rendered = '{}(\n{})'.format(obj_name, ''.join( | ||
' {},\n'.format(r) for r in result) | ||||
) | ||||
|
r24847 | else: | ||
rendered = '{}({})'.format(obj_name, ', '.join(result)) | ||||
|
r24841 | |||
|
r24848 | if obj_signature.return_annotation is not inspect._empty: | ||
anno = inspect.formatannotation(obj_signature.return_annotation) | ||||
|
r24847 | rendered += ' -> {}'.format(anno) | ||
|
r24841 | |||
|
r24847 | return rendered | ||