# -*- coding: utf-8 -*-
"""Tools for inspecting Python objects.
This file was forked from the IPython project:
* Copyright (c) 2008-2014, IPython Development Team
* Copyright (C) 2001-2007 Fernando Perez <fperez@colorado.edu>
* Copyright (c) 2001, Janko Hauser <jhauser@zscout.de>
* Copyright (c) 2001, Nathaniel Gray <n8gray@caltech.edu>
"""
from collections import namedtuple
import inspect
import io as stdlib_io
from itertools import zip_longest
import linecache
import os
import sys
import types
from xonsh import openpy
from xonsh.tools import (cast_unicode, safe_hasattr, indent,
print_color, format_color)
from xonsh.platform import HAS_PYGMENTS, PYTHON_VERSION_INFO
if HAS_PYGMENTS:
import pygments
from xonsh import pyghooks
# builtin docstrings to ignore
_func_call_docstring = types.FunctionType.__call__.__doc__
_object_init_docstring = object.__init__.__doc__
_builtin_type_docstrings = {
t.__doc__
for t in (types.ModuleType, types.MethodType, types.FunctionType)
}
_builtin_func_type = type(all)
_builtin_meth_type = type(
str.upper) # Bound methods have the same type as builtin functions
info_fields = [
'type_name', 'base_class', 'string_form', 'namespace', 'length', 'file',
'definition', 'docstring', 'source', 'init_definition', 'class_docstring',
'init_docstring', 'call_def', 'call_docstring',
# These won't be printed but will be used to determine how to
# format the object
'ismagic', 'isalias', 'isclass', 'argspec', 'found', 'name'
]
[docs]def object_info(**kw):
"""Make an object info dict with all fields present."""
infodict = dict(zip_longest(info_fields, [None]))
infodict.update(kw)
return infodict
[docs]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.
with stdlib_io.open(ofile, 'rb') as buf: # Tweaked to use io.open for Python 2
encoding, _ = openpy.detect_encoding(buf.readline)
return encoding
[docs]def getdoc(obj):
"""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."""
# Allow objects to offer customized documentation via a getdoc method:
try:
ds = obj.getdoc()
except Exception: # pylint:disable=broad-except
pass
else:
# if we get extra info, we add it to the normal docstring.
if isinstance(ds, str):
return inspect.cleandoc(ds)
try:
docstr = inspect.getdoc(obj)
encoding = get_encoding(obj)
return cast_unicode(docstr, encoding=encoding)
except Exception: # pylint:disable=broad-except
# Harden against an inspect failure, which can occur with
# SWIG-wrapped extensions.
raise
[docs]def getsource(obj, is_binary=False):
"""Wrapper around inspect.getsource.
This can be modified by other projects to provide customized source
extraction.
Inputs:
- obj: an object whose source code we will attempt to extract.
Optional inputs:
- is_binary: whether the object is known to come from a binary source.
This implementation will skip returning any output for binary objects,
but custom extractors may know how to meaningfully process them."""
if is_binary:
return None
else:
# get source if obj was decorated with @decorator
if hasattr(obj, "__wrapped__"):
obj = obj.__wrapped__
try:
src = inspect.getsource(obj)
except TypeError:
if hasattr(obj, '__class__'):
src = inspect.getsource(obj.__class__)
encoding = get_encoding(obj)
return cast_unicode(src, encoding=encoding)
[docs]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))
[docs]def getargspec(obj):
"""Wrapper around :func:`inspect.getfullargspec` on Python 3, and
:func:inspect.getargspec` on Python 2.
In addition to functions and methods, this can also handle objects with a
``__call__`` attribute.
"""
if safe_hasattr(obj, '__call__') and not is_simple_callable(obj):
obj = obj.__call__
return inspect.getfullargspec(obj)
[docs]def call_tip(oinfo, format_call=True):
"""Extract call tip data from an oinfo dict.
Parameters
----------
oinfo : dict
format_call : bool, optional
If True, the call line is formatted and returned as a string. If not, a
tuple of (name, argspec) is returned.
Returns
-------
call_info : None, str or (str, dict) tuple.
When format_call is True, the whole call information is formattted as a
single string. Otherwise, the object's name and its argspec dict are
returned. If no call information is available, None is returned.
docstring : str or None
The most relevant docstring for calling purposes is returned, if
available. The priority is: call docstring for callable instances, then
constructor docstring for classes, then main object's docstring otherwise
(regular functions).
"""
# Get call definition
argspec = oinfo.get('argspec')
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:]
call_line = oinfo['name'] + format_argspec(argspec)
# Now get docstring.
# The priority is: call docstring, constructor docstring, main one.
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
[docs]def find_file(obj):
"""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.
"""
# get source if obj was decorated with @decorator
if safe_hasattr(obj, '__wrapped__'):
obj = obj.__wrapped__
fname = None
try:
fname = inspect.getabsfile(obj)
except TypeError:
# For an instance, the file that matters is where its class was
# declared.
if hasattr(obj, '__class__'):
try:
fname = inspect.getabsfile(obj.__class__)
except TypeError:
# Can happen for builtins
pass
except: # pylint:disable=bare-except
pass
return cast_unicode(fname)
[docs]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.
"""
# get source if obj was decorated with @decorator
if safe_hasattr(obj, '__wrapped__'):
obj = obj.__wrapped__
try:
try:
lineno = inspect.getsourcelines(obj)[1]
except TypeError:
# For instances, try the class object like getsource() does
if hasattr(obj, '__class__'):
lineno = inspect.getsourcelines(obj.__class__)[1]
else:
lineno = None
except: # pylint:disable=bare-except
return None
return lineno
if PYTHON_VERSION_INFO < (3, 5, 0):
FrameInfo = namedtuple('FrameInfo', ['frame', 'filename', 'lineno', 'function',
'code_context', 'index'])
def getouterframes(frame, context=1):
"""Wrapper for getouterframes so that it acts like the Python v3.5 version."""
return [FrameInfo(*f) for f in inspect.getouterframes(frame, context=context)]
else:
getouterframes = inspect.getouterframes
[docs]class Inspector(object):
"""Inspects objects."""
def __init__(self, str_detail_level=0):
self.str_detail_level = str_detail_level
def _getdef(self, obj, oname=''):
"""Return the call signature for any callable object.
If any exception is generated, None is returned instead and the
exception is suppressed.
"""
try:
hdef = oname + inspect.formatargspec(*getargspec(obj))
return cast_unicode(hdef)
except: # pylint:disable=bare-except
return None
[docs] 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()
[docs] 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')
obj = obj.__init__
output = self._getdef(obj, oname)
if output is None:
self.noinfo('definition header', oname)
else:
print(header, output, end=' ', file=sys.stdout)
[docs] def pdoc(self, obj, oname=''):
"""Print the docstring for any object.
Optional
-formatter: a function to run the docstring through for specially
formatted docstrings.
"""
head = self.__head # For convenience
lines = []
ds = getdoc(obj)
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:
print('\n'.join(lines))
[docs] def psource(self, obj, oname=''):
"""Print the source code for an object."""
# Flush the source cache because inspect can return out-of-date source
linecache.checkcache()
try:
src = getsource(obj)
except: # pylint:disable=bare-except
self.noinfo('source', oname)
else:
print(src)
[docs] 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)
# 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.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:
# Print only text files, not extension binaries. Note that
# getsourcelines returns lineno with 1-offset and page() uses
# 0-offset, so we must adjust.
o = openpy.read_py_file(ofile, skip_encoding_cookie=False)
print(o, lineno - 1)
def _format_fields_str(self, fields, title_width=0):
"""Formats a list of fields for display using color strings.
Parameters
----------
fields : list
A list of 2-tuples: (field_title, field_content)
title_width : int
How many characters to pad titles to. Default to longest title.
"""
out = []
if title_width == 0:
title_width = max(len(title) + 2 for title, _ in fields)
for title, content in fields:
title_len = len(title)
title = '{BOLD_RED}' + title + ':{NO_COLOR}'
if len(content.splitlines()) > 1:
title += '\n'
else:
title += " ".ljust(title_width - title_len)
out.append(cast_unicode(title) + cast_unicode(content))
return format_color("\n".join(out) + '\n')
def _format_fields_tokens(self, fields, title_width=0):
"""Formats a list of fields for display using color tokens from
pygments.
Parameters
----------
fields : list
A list of 2-tuples: (field_title, field_content)
title_width : int
How many characters to pad titles to. Default to longest title.
"""
out = []
if title_width == 0:
title_width = max(len(title) + 2 for title, _ in fields)
for title, content in fields:
title_len = len(title)
title = '{BOLD_RED}' + title + ':{NO_COLOR}'
if not isinstance(content, str) or len(content.splitlines()) > 1:
title += '\n'
else:
title += " ".ljust(title_width - title_len)
out += pyghooks.partial_color_tokenize(title)
if isinstance(content, str):
out[-1] = (out[-1][0], out[-1][1] + content + '\n')
else:
out += content
out[-1] = (out[-1][0], out[-1][1] + '\n')
out[-1] = (out[-1][0], out[-1][1] + '\n')
return out
def _format_fields(self, fields, title_width=0):
"""Formats a list of fields for display using color tokens from
pygments.
Parameters
----------
fields : list
A list of 2-tuples: (field_title, field_content)
title_width : int
How many characters to pad titles to. Default to longest title.
"""
if HAS_PYGMENTS:
rtn = self._format_fields_tokens(fields, title_width=title_width)
else:
rtn = self._format_fields_str(fields, title_width=title_width)
return rtn
# The fields to be displayed by pinfo: (fancy_name, key_in_info_dict)
pinfo_fields1 = [("Type", "type_name")]
pinfo_fields2 = [("String form", "string_form")]
pinfo_fields3 = [("Length", "length"),
("File", "file"),
("Definition", "definition"), ]
pinfo_fields_obj = [("Class docstring", "class_docstring"),
("Init docstring", "init_docstring"),
("Call def", "call_def"),
("Call docstring", "call_docstring"), ]
[docs] def pinfo(self, obj, oname='', info=None, detail_level=0):
"""Show detailed information about an object.
Parameters
----------
obj : object
oname : str, optional
name of the variable pointing to the object.
info : dict, optional
a structure with some information fields which may have been
precomputed already.
detail_level : int, optional
if set to 1, more information is given.
"""
info = self.info(obj,
oname=oname,
info=info,
detail_level=detail_level)
displayfields = []
def add_fields(fields):
for title, key in fields:
field = info[key]
if field is not None:
displayfields.append((title, field.rstrip()))
add_fields(self.pinfo_fields1)
add_fields(self.pinfo_fields2)
# Namespace
if (info['namespace'] is not None and
info['namespace'] != 'Interactive'):
displayfields.append(("Namespace", info['namespace'].rstrip()))
add_fields(self.pinfo_fields3)
if info['isclass'] and info['init_definition']:
displayfields.append(("Init definition",
info['init_definition'].rstrip()))
# Source or docstring, depending on detail level and whether
# source found.
if detail_level > 0 and info['source'] is not None:
displayfields.append(("Source", cast_unicode(info['source'])))
elif info['docstring'] is not None:
displayfields.append(("Docstring", info["docstring"]))
# Constructor info for classes
if info['isclass']:
if info['init_docstring'] is not None:
displayfields.append(("Init docstring",
info['init_docstring']))
# Info for objects:
else:
add_fields(self.pinfo_fields_obj)
# Finally send to printer/pager:
if displayfields:
print_color(self._format_fields(displayfields))
[docs] def info(self, obj, oname='', info=None, detail_level=0):
"""Compute a dict with detailed information about an object.
Optional arguments:
- oname: name of the variable pointing to the object.
- info: a structure with some information fields which may have been
precomputed already.
- detail_level: if set to 1, more information is given.
"""
obj_type = type(obj)
if info is None:
ismagic = 0
isalias = 0
ospace = ''
else:
ismagic = info.ismagic
isalias = info.isalias
ospace = info.namespace
# Get docstring, special-casing aliases:
if isalias:
if not callable(obj):
if len(obj) >= 2 and isinstance(obj[1], str):
ds = "Alias to the system command:\n {0}".format(obj[1])
else: # pylint:disable=bare-except
ds = "Alias: " + str(obj)
else:
ds = "Alias to " + str(obj)
if obj.__doc__:
ds += "\nDocstring:\n" + obj.__doc__
else:
ds = getdoc(obj)
if ds is None:
ds = '<no docstring>'
# store output in a dict, we initialize it here and fill it as we go
out = dict(name=oname, found=True, isalias=isalias, ismagic=ismagic)
string_max = 200 # max size of strings to show (snipped if longer)
shalf = int((string_max - 5) / 2)
if ismagic:
obj_type_name = 'Magic function'
elif isalias:
obj_type_name = 'System alias'
else:
obj_type_name = obj_type.__name__
out['type_name'] = obj_type_name
try:
bclass = obj.__class__
out['base_class'] = str(bclass)
except: # pylint:disable=bare-except
pass
# String form, but snip if too long in ? form (full in ??)
if detail_level >= self.str_detail_level:
try:
ostr = str(obj)
str_head = 'string_form'
if not detail_level and len(ostr) > string_max:
ostr = ostr[:shalf] + ' <...> ' + ostr[-shalf:]
ostr = ("\n" + " " * len(str_head.expandtabs())).\
join(q.strip() for q in ostr.split("\n"))
out[str_head] = ostr
except: # pylint:disable=bare-except
pass
if ospace:
out['namespace'] = ospace
# Length (for strings and lists)
try:
out['length'] = str(len(obj))
except: # pylint:disable=bare-except
pass
# Filename where object was defined
binary_file = False
fname = find_file(obj)
if fname is None:
# if anything goes wrong, we don't want to show source, so it's as
# if the file was binary
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'] = fname
# Docstrings only in detail 0 mode, since source contains them (we
# avoid repetitions). If source fails, we add them back, see below.
if ds and detail_level == 0:
out['docstring'] = ds
# Original source code for any callable
if detail_level:
# Flush the source cache because inspect can return out-of-date
# source
linecache.checkcache()
source = None
try:
try:
source = getsource(obj, binary_file)
except TypeError:
if hasattr(obj, '__class__'):
source = getsource(obj.__class__, binary_file)
if source is not None:
source = source.rstrip()
if HAS_PYGMENTS:
lexer = pyghooks.XonshLexer()
source = list(pygments.lex(source, lexer=lexer))
out['source'] = source
except Exception: # pylint:disable=broad-except
pass
if ds and source is None:
out['docstring'] = ds
# Constructor docstring for classes
if inspect.isclass(obj):
out['isclass'] = True
# reconstruct the function definition and print it:
try:
obj_init = obj.__init__
except AttributeError:
init_def = init_ds = None
else:
init_def = self._getdef(obj_init, oname)
init_ds = getdoc(obj_init)
# Skip Python's auto-generated docstrings
if init_ds == _object_init_docstring:
init_ds = None
if init_def or init_ds:
if init_def:
out['init_definition'] = init_def
if init_ds:
out['init_docstring'] = init_ds
# and class docstring for instances:
else:
# reconstruct the function definition and print it:
defln = self._getdef(obj, oname)
if defln:
out['definition'] = defln
# 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: # pylint:disable=bare-except
class_ds = None
else:
class_ds = getdoc(cls)
# Skip Python's auto-generated docstrings
if class_ds in _builtin_type_docstrings:
class_ds = None
if class_ds and ds != class_ds:
out['class_docstring'] = class_ds
# Next, try to show constructor docstrings
try:
init_ds = getdoc(obj.__init__)
# Skip Python's auto-generated docstrings
if init_ds == _object_init_docstring:
init_ds = None
except AttributeError:
init_ds = None
if init_ds:
out['init_docstring'] = init_ds
# Call form docstring for callable instances
if safe_hasattr(obj, '__call__') and not is_simple_callable(obj):
call_def = self._getdef(obj.__call__, oname)
if call_def:
call_def = call_def
# it may never be the case that call def and definition
# differ, but don't include the same signature twice
if call_def != out.get('definition'):
out['call_def'] = call_def
call_ds = getdoc(obj.__call__)
# Skip Python's auto-generated docstrings
if call_ds == _func_call_docstring:
call_ds = None
if call_ds:
out['call_docstring'] = call_ds
# Compute the object's argspec as a callable. The key is to decide
# whether to pull it from the object itself, from its __init__ or
# from its __call__ method.
if inspect.isclass(obj):
# Old-style classes need not have an __init__
callable_obj = getattr(obj, "__init__", None)
elif callable(obj):
callable_obj = obj
else:
callable_obj = None
if callable_obj:
try:
argspec = getargspec(callable_obj)
except (TypeError, AttributeError):
# For extensions/builtins we can't retrieve the argspec
pass
else:
# named tuples' _asdict() method returns an OrderedDict, but we
# we want a normal
out['argspec'] = argspec_dict = dict(argspec._asdict())
# We called this varkw before argspec became a named tuple.
# With getfullargspec it's also called varkw.
if 'varkw' not in argspec_dict:
argspec_dict['varkw'] = argspec_dict.pop('keywords')
return object_info(**out)