#!/usr/bin/env python
# -*- coding: utf-8 -*-
# ---------------------------------------------------------------------
# Copyright (c) Merchise Autrement [~º/~] and Contributors
# All rights reserved.
#
# This is free software; you can do what the LICENCE file allows you to.
#
"""Utilities to inspect the CPython's stack."""
import inspect
__all__ = (
"MAX_DEEP",
"getargvalues",
"error_info",
"object_info_finder",
"object_finder",
"track_value",
"iter_stack",
)
MAX_DEEP = 25
[docs]def getargvalues(frame):
"""Inspects the given frame for arguments and returns a dictionary that
maps parameters names to arguments values. If an `*` argument was passed
then the key on the returning dictionary would be formatted as
`<name-of-*-param>[index]`.
For example in the function::
>>> def autocontained(a, limit, *margs, **ks):
... import sys
... return getargvalues(sys._getframe())
>>> autocontained(1, 12)['limit']
12
>>> autocontained(1, 2, -10, -11)['margs[0]']
-10
"""
from xotl.tools.future.itertools import flatten
from xotl.tools.values.simple import force_sequence_coerce as array
pos, args, kwds, values = inspect.getargvalues(frame)
res = {}
for keys in pos:
if keys:
res.update({key: values[key] for key in flatten(array(keys))})
if args:
i = 0
for item in values[args]:
res["%s[%s]" % (args, i)] = item
i += 1
if kwds:
res.update(values[kwds])
return res
def __error_info(tb, *args, **kwargs):
"""Internal function used by `error_info`:func: and
`printable_error_info`:func:.
"""
# TODO: Formalize tests for this
ALL = True
res = []
kwargs.update(dict.fromkeys(args, ALL))
if kwargs:
deep = 0
processed = set()
while tb and (deep < MAX_DEEP):
frame = tb.tb_frame
func_name = frame.f_code.co_name
attrs1 = kwargs.get(func_name, None)
attrs2 = kwargs.get(deep, None)
if attrs1 or attrs2:
processed.add(func_name)
processed.add(deep)
if (attrs1 is ALL) or (attrs2 is ALL):
attrs = ALL
else:
attrs = list(attrs1) if attrs1 else []
if attrs2:
attrs.extend(attrs2)
if attrs is ALL:
item = frame.f_locals.copy()
else:
item = {key: frame.f_locals.get(key) for key in attrs}
item["function-name"] = func_name
item["traceback-deep"] = deep
item["line-number"] = tb.tb_lineno
item["file-name"] = frame.f_code.co_filename
res.append(item)
tb = tb.tb_next
deep += 1
for item in processed:
if item in kwargs:
del kwargs[item]
if kwargs:
res["unprocessed-items"] = kwargs
return res
[docs]def error_info(*args, **kwargs):
"""Get error information in current trace-back.
No all trace-back are returned, to select which are returned use:
- ``args``: Positional parameters
- If string, represent the name of a function.
- If an integer, a trace-back level.
Return all values.
- ``kwargs``: The same as ``args`` but each value is a list of local
names to return. If a value is ``True``, means all local variables.
Return a list with a dict in each item.
Example::
>>> def foo(x):
... x += 1//x
... if x % 2:
... bar(x - 1)
... else:
... bar(x - 2)
>>> def bar(x):
... x -= 1//x
... if x % 2:
... foo(x//2)
... else:
... foo(x//3)
>>> try: # doctest: +SKIP
... foo(20)
... except:
... print(printable_error_info('Example', foo=['x'], bar=['x']))
Example
ERROR: integer division or modulo by zero
...
"""
import sys
_error_type, _error, tb = sys.exc_info()
return __error_info(tb, *args, **kwargs)
def printable_error_info(base, *args, **kwargs):
"""Get error information in current trace-back.
No all trace-back are returned, to select which are returned use:
- ``args``: Positional parameters
- If string, represent the name of a function.
- If an integer, a trace-back level.
Return all values.
- ``kwargs``: The same as ``args`` but each value is a list of local
names to return. If a value is ``True``, means all local variables.
Return a formatted string with all information.
See `error_info`:func: for an example.
"""
import sys
_error_type, error, tb = sys.exc_info()
if tb:
res = "%s\n\tERROR: %s\n\t" % (base, error)
info = __error_info(tb, *args, **kwargs)
return res + "\n\t".join(str(item) for item in info)
else:
return ""
[docs]def object_info_finder(obj_type, arg_name=None, max_deep=MAX_DEEP):
"""Find an object of the given type through all arguments in stack frames.
Returns a tuple with the following values:
(arg-value, arg-name, deep, frame).
When no object is found
None is returned.
Arguments:
object_type: a type or a tuple of types as in "isinstance".
arg_name: the arg_name to find; if None find in all arguments
max_deep: the max deep to enter in the stack frames.
"""
frame = inspect.currentframe()
try:
deep = 0
res = None
while (res is None) and (deep < max_deep) and (frame is not None):
ctx = getargvalues(frame)
d = {arg_name: ctx.get(arg_name)} if arg_name is not None else ctx
for key in d:
value = d[key]
if isinstance(value, obj_type):
res = (value, key, deep, frame)
frame = frame.f_back
deep += 1
return res
finally:
del frame # As recommended in the Python's doc to avoid memory leaks
[docs]def object_finder(obj_type, arg_name=None, max_deep=MAX_DEEP):
"""Find an object of the given type through all arguments in stack frames.
The difference with `object_info_finder`:func: is that this function
returns the object directly, not a tuple.
"""
finder = object_info_finder(obj_type, arg_name, max_deep)
info = finder()
return info[0] if info else None
[docs]def track_value(value, max_deep=MAX_DEEP):
"""Find a value through all arguments in stack frames.
Returns a dictionary with the full-context in the same level as "value".
"""
frame = inspect.currentframe().f_back.f_back
deep = 0
res = None
while (res is None) and (deep < max_deep) and (frame is not None):
ctx = getargvalues(frame)
for _key in ctx:
_value = ctx[_key]
if (type(value) == type(_value)) and (value == _value):
res = (ctx, _key)
frame = frame.f_back
deep += 1
return res
[docs]def iter_stack(max_deep=MAX_DEEP):
"""Iterates through stack frames until exhausted or `max_deep` is reached.
To find a frame fulfilling a condition use::
frame = next(f for f in iter_stack() if condition(f))
.. versionadded:: 1.6.8
"""
# Using the previous pattern, functions `object_info_finder`,
# `object_finder` and `track_value` can be reprogrammed or deprecated.
frame = inspect.currentframe()
try:
deep = 0
while (deep < max_deep) and (frame is not None):
yield frame
frame = frame.f_back
deep += 1
finally:
del frame # As recommended in the Python's doc to avoid memory leaks