from collections import defaultdict, OrderedDict
import copy
import functools
import inspect
import operator
import sys
# TODO: THINK ABOUT CHANGING ALL NON-INTERFACE METHODS TO PRIVATE
# TODO: Maybe add a strict kwark go Behold that will fail if
# context/values keys aren't found.
# TODO: make sure you can filter on unshown variables
# TODO: test the inquality operator
class _Sentinal(object):
pass
[docs]class Item(object):
"""
Item is a simple container class that sets its attributes from constructor
kwargs. It supports both object and dictionary access to its attributes.
So, for example, all of the following statements are supported.
.. code-block:: python
item = Item(a=1, b=2)
item['c'] = 2
a = item['a']
An instance of this class is created when you ask to show local variables
with a `Behold` object. The local variables you want to show are attached as
attributes to an `Item` object.
"""
# I'm using unconventional "_item_self_" name here to avoid
# conflicts when kwargs actually contain a "self" arg.
def __init__(_item_self, **kwargs):
for key, val in kwargs.items():
_item_self[key] = val
def __str__(_item_self):
quoted_keys = [
'\'{}\''.format(k) for k in sorted(vars(_item_self).keys())]
att_string = ', '.join(quoted_keys)
return 'Item({})'.format(att_string)
def __repr__(_item_self):
return _item_self.__str__()
def __setitem__(_item_self, key, value):
setattr(_item_self, key, value)
def __getitem__(_item_self, key):
return getattr(_item_self, key)
[docs]class Behold(object):
"""
:type tag: str
:param tag: A tag with which to label all output (default: None)
:type strict: Bool
:param strict: When set to true, will only only allow existing keys to be
used in the ``when_contex()`` and ``when_values()``
methods.
:type stream: FileObject
:param stream: Any write-enabled python FileObject (default: sys.stdout)
:ivar stream: sys.stdout: The stream that will be written to
:ivar tag: None: A string with which to tag output
:ivar strict: False: A Bool that sets whether or not only existing keys
allowed in ``when_contex()`` and ``when_values()``
methods.
``Behold`` objects are used to probe state within your code base. They can
be used to log output to the console or to trigger entry points for step
debugging.
Because it is used so frequently, the behold class has a couple of aliases.
The following three statements are equivalent
.. code-block:: python
from behold import Behold # Import using the name of the class
from behold import B # If you really hate typing
from behold import BB # If you really hate typing but would
# rather use a name that's easier to
# search for in your editor.
from behold import * # Although bad practice in general, since
# you'll usually be using behold just for
# debugging, this is pretty convenient.
"""
# class variable to hold all context values
_context = {}
_stash = defaultdict(list)
# operators to handle django-style querying
_op_for = {
'__lt': operator.lt,
'__lte': operator.le,
'__le': operator.le,
'__gt': operator.gt,
'__gte': operator.ge,
'__ge': operator.ge,
'__ne': operator.ne,
'__in': lambda value, options: value in options
}
# TODO; maybe add __contains and __startwith
# And if you do, add it to the when*() methods docstrings
def __init__(self, tag=None, strict=False, stream=None):
self.tag = tag
self.strict = strict
#: Doc comment for class attribute Foo.bar.
#: It can have multiple lines.
self.stream = None
if stream is None:
self.stream = sys.stdout
else:
self.stream = stream
# these filters apply to context variables
self.passes = True
self.context_filters = []
self.value_filters = []
self._viewed_context_keys = []
# a list of fields that will be printed if filters pass
self.print_keys = []
# holds a string rep for this object
self._str = ''
# a bool to hold whether or not all filters have passed
self._passes_all = False
def reset(self):
self.passes = False
self.context_filters = []
self.value_filters = []
self._viewed_context_keys = []
def _key_to_field_op(self, key):
# this method looks at a key and checks if it ends in any of the
# endings that have special django-like query meanings.
# It translates those into comparision operators and returns the
# name of the actual key.
op = operator.eq
name = key
for op_name, trial_op in self.__class__._op_for.items():
if key.endswith(op_name):
op = trial_op
name = key.split('__')[0]
break
return op, name
@classmethod
def set_context(cls, **kwargs):
cls._context.update(kwargs)
@classmethod
def unset_context(cls, *keys):
for key in keys:
if key in cls._context:
cls._context.pop(key)
[docs] def when(self, *bools):
"""
:type bools: bool
:param bools: Boolean arguments
All boolean arguments passed to this method must evaluate to `True` for
printing to be enabled.
So for example, the following code would print ``x: 1``
.. code-block:: python
for x in range(10):
Behold().when(x == 1).show('x')
"""
self.passes = self.passes and all(bools)
return self
[docs] def view_context(self, *context_keys):
"""
:type context_keys: string arguments
:param context_keys: Strings with context keys
This method allows you to show values of context variables along with
the local variables you are examining. It is useful for sorting out
which context is active when filtering with "in queries" like this
the ``myvar__in=[1, 2]``
"""
self._viewed_context_keys.extend(context_keys)
return self
[docs] def when_context(self, **criteria):
"""
:type criteria: kwargs
:param criteria: Key word arguments of var_name=var_value
The key-word arguments passed to this method specify the context
constraints that must be met in order for printing to occur. The
syntax of these constraints is reminiscent of that used in Django
querysets. All specified criteria must be met for printing to occur.
The following syntax is supported.
* ``x__lt=1`` means ``x < 1``
* ``x__lte=1`` means ``x <= 1``
* ``x__le=1`` means ``x <= 1``
* ``x__gt=1`` means ``x > 1``
* ``x__gte=1`` means ``x >= 1``
* ``x__ge=1`` means ``x >= 1``
* ``x__ne=1`` means ``x != 1``
* ``x__in=[1, 2, 3]`` means ``x in [1, 2, 3]``
The reason this syntax is needed is that the context values being
compared are not available in the local scope. This renders the normal
Python comparison operators useless.
"""
self._add_context_filters(**criteria)
return self
[docs] def when_values(self, **criteria):
"""
By default, ``Behold`` objects call ``str()`` on all variables before
sending them to the output stream. This method enables you to filter on
those extracted string representations. The syntax is exactly like that
of the ``when_context()`` method. Here is an example.
.. code-block:: python
from behold import Behold, Item
items = [
Item(a=1, b=2),
Item(c=3, d=4),
]
for item in items:
# You can filter on the string representation
Behold(tag='first').when_values(a='1').show(item)
# Behold is smart enough to transform your criteria to strings
# so this also works
Behold(tag='second').when_values(a=1).show(item)
# Because the string representation is not present in the local
# scope, you must use Django-query-like syntax for logical
# operations.
Behold(tag='third').when_values(a__gte=1).show(item)
"""
criteria = {k: str(v) for k, v in criteria.items()}
self._add_value_filters(**criteria)
return self
def _add_context_filters(self, **criteria):
for key, val in criteria.items():
op, field = self._key_to_field_op(key)
self.context_filters.append((op, field, val))
def _add_value_filters(self, **criteria):
for key, val in criteria.items():
op, field = self._key_to_field_op(key)
self.value_filters.append((op, field, val))
def _passes_filter(self, filter_list, value_extractor, default_when_missing=True):
passes = True
for (op, field, filter_val) in filter_list:
# _Sentinal object means current value couldn't be extraced
current_val = value_extractor(field)
no_value_found = isinstance(current_val, _Sentinal)
# if you couldn't extract a value, do the default thing
if no_value_found:
passes = default_when_missing
# otherwise update whether or not this passes
else:
passes = passes and op(current_val, filter_val)
if not passes:
return False
return True
def _passes_value_filter(self, item, name):
if not self.value_filters:
return True
def value_extractor(field):
return self.extract(item, field)
return self._passes_filter(self.value_filters, value_extractor)
def _strict_checker(self, names, item=None):
if self.strict:
names = set(names)
if item is None:
allowed_names = set(self.__class__._context.keys())
else:
allowed_names = set(item.__dict__.keys())
bad_names = names - allowed_names
if bad_names:
msg = (
'\n\nKeys {} not found.\n'
'Allowed keys: {}'
).format(
list(sorted(bad_names)),
list(sorted(allowed_names))
)
raise ValueError(msg)
def _passes_context_filter(self):
if not self.context_filters:
return True
else:
def value_extractor(field):
return self.__class__._context.get(field, _Sentinal())
return self._passes_filter(
self.context_filters, value_extractor,
default_when_missing=False)
def passes_all(self, item=None, att_names=None):
if not self.passes or not self._passes_context_filter():
self._passes_all = False
elif item is not None and att_names is not None:
self._passes_all = all([
self._passes_value_filter(item, name)
for name in att_names
])
else:
self._passes_all = True
return self._passes_all
def _separate_names_objects(self, values):
att_names = []
objs = []
for val in values:
if isinstance(val, str):
att_names.append(val)
else:
objs.append(val)
return att_names, objs
def _validate_objs(self, objs):
has_obj = bool(objs)
has_multi_objs = len(objs) > 1
# only allow at most one object
if has_multi_objs:
raise ValueError(
'\n\nYou can pass at most one non-string argument.'
)
if has_obj:
# make sure object is useable
if not hasattr(objs[0], '__dict__'):
raise ValueError(
'Error in Behold() The object you passed has '
'no __dict__ attribute'
)
def _get_item_and_att_names(self, *values, **data):
if not self.passes_all():
return None, None
att_names, objs = self._separate_names_objects(values)
all_att_names = set(att_names)
# gather information about the inputs
has_data = bool(data)
has_obj = bool(objs)
# make sure objs are okay
self._validate_objs(objs)
# If an object was provided, create a dict with its attributes
if has_obj:
att_dict = objs[0].__dict__
# If no object was provided, construct an item from the calling local
# scope
else:
# this try/else block is needed to breake reference cycles
try:
att_dict = {}
calling_frame = inspect.currentframe().f_back.f_back
# update with local variables of the calling frame
att_dict.update(calling_frame.f_locals)
finally:
# delete the calling frame to avoid reference cycles
del calling_frame
# If data was passed, it gets priority
if has_data:
att_dict.update(data)
att_names.extend(sorted(data.keys()))
# if no attribute names supplied, use all of them
if not att_names:
att_names = sorted(att_dict.keys())
all_att_names = all_att_names.union(set(att_names))
# do strict check if requested
if self.strict:
self._strict_checker(att_names, item=Item(**att_dict))
# check for values passing
if not self.passes_all(Item(**att_dict), list(all_att_names)):
return None, None
# Limit the att_dict to have only requested attributes.
# Using an ordered dict here to preserve attribute order
# while deduplicating
ordered_atts = OrderedDict()
for att_name in att_names:
ordered_atts[att_name] = att_dict.get(att_name, None)
# Make an item out of the att_dict (might lose order, but don't care)
item = Item(**ordered_atts)
# make an ordered list of attribute names
ordered_att_names = list(ordered_atts.keys())
return item, ordered_att_names
@classmethod
def get_stash(cls, stash_name):
if stash_name in cls._stash:
return copy.deepcopy(cls._stash[stash_name])
else:
raise ValueError(
'\n\nRequested name \'{}\' not in {}'.format(
stash_name, list(cls._stash.keys()))
)
@classmethod
def clear_stash(cls, *names):
if names:
for name in names:
if name in cls._stash:
del cls._stash[name]
else:
raise ValueError(
'\n\nName \'{}\' not in {}'.format(
name, list(cls._stash.keys())
)
)
else:
cls._stash = defaultdict(list)
[docs] def stash(self, *values, **data):
"""
The stash method allows you to stash values for later analysis. The
arguments are identical to the ``show()`` method. Instead of writing
outpout, however, the ``stash()`` method populates a global list with
the values that would have been printed. This allows them to be
accessed later in the debugging process.
Here is an example.
.. code-block:: python
from behold import Behold, get_stash
for nn in range(10):
# You can only invoke ``stash()`` on behold objects that were
# created with tag. The tag becomes the global key for the stash
# list.
behold = Behold(tag='my_stash_key')
two_nn = 2 * nn
behold.stash('nn' 'two_nn')
# You can then run this in a completely different file of your code
# base.
my_stashed_list = get_stash('my_stash_key')
"""
if not self.tag:
raise ValueError(
'You must instantiate Behold with a tag name if you want to '
'use stashing'
)
item, att_names = self._get_item_and_att_names(*values, **data)
if not item:
self.reset()
return False
out = {name: item.__dict__.get(name, None) for name in att_names}
self.__class__._stash[self.tag].append(out)
self.reset()
return True
def get(self, *values, **data):
item, att_names = self._get_item_and_att_names(*values, **data)
if not item:
self.reset()
return None
out = {name: item.__dict__.get(name, None) for name in att_names}
return out
def is_true(self, item=None):
"""
If you are filtering on object values, you need to pass that object here.
"""
if item:
values = [item]
else:
values = []
self._get_item_and_att_names(*values)
return self._passes_all
[docs] def show(self, *values, **data):
"""
:type values: str arguments
:param values: A list of variable or attribute names you want to print.
At most one argument can be something other than a
string. Strings are interpreted as the
variable/attribute names you want to print. If a single
non-string argument is provided, it must be an object
having attributes named in the string variables. If no
object is provided, the strings must be the names of
variables in the local scope.
:type data: keyword args
:param data: A set of keyword arguments. The key provided will be the
name of the printed variables. The value associated with
that key will have its str() representation printed. You
can think of these keyword args as attatching additional
attributes to any object that was passed in args. If no
object was passed, then these kwargs will be used to create
an object.
This method will return ``True`` if all the filters passed, otherwise it
will return ``False``. This allows you to perform additional logic in
your debugging code if you wish. Here are some examples.
.. code-block:: python
from behold import Behold, Item
a, b = 1, 2
my_list = [a, b]
# show arguments from local scope
Behold().show('a', 'b')
# show values from local scope using keyword arguments
Behold.show(a=my_list[0], b=my_list[1])
# show values from local scope using keyword arguments, but
# force them to be printed in a specified order
Behold.show('b', 'a', a=my_list[0], b=my_list[1])
# show attributes on an object
item = Item(a=1, b=2)
Behold.show(item, 'a', 'b')
# use the boolean returned by show to control more debugging
a = 1
if Behold.when(a > 1).show('a'):
import pdb; pdb.set_trace()
"""
item, att_names = self._get_item_and_att_names(*values, **data)
if not item:
self.reset()
return False
self._strict_checker(att_names, item=item)
# set the string value
self._str = self.stringify_item(item, att_names)
self.stream.write(self._str + '\n')
passes_all = self._passes_all
self.reset()
return passes_all
def stringify_item(self, item, att_names):
if not att_names:
raise ValueError(
'Error in Behold. Could not determine attributes/'
'variables to show.')
out = []
for ind, key in enumerate(att_names):
out.append(key + ': ')
has_more = ind < len(att_names) - 1
has_more = has_more or self.tag or self._viewed_context_keys
if has_more:
ending = ', '
else:
ending = ''
val = self.extract(item, key)
out.append(val + ending)
self._strict_checker(self._viewed_context_keys)
for ind, key in enumerate(self._viewed_context_keys):
has_more = ind < len(self._viewed_context_keys) - 1
has_more = has_more or self.tag
if has_more:
ending = ', '
else:
ending = ''
out.append(
'{}: {}{}'.format(
key,
self.__class__._context.get(key, ''),
ending
)
)
if self.tag:
out.append(self.tag)
return ''.join(out)
def __str__(self):
return self._str
def __repr__(self):
return self.__str__()
[docs]class in_context(object):
"""
:type context_vars: key-work arguments
:param context_vars: Key-word arguments specifying the context variables
you would like to set.
You can define arbitrary context in which to perform your debugging. A
common use case for this is when you have a piece of code that is called
from many different places in your code base, but you are only interested in
what happens when it's called from a particular location. You can just wrap
that location in a context and only debug when in that context. Here is an
example.
.. code-block:: python
from behold import BB # this is an alias for Behold
from behold import in_context
# A function that can get called from anywhere
def my_function():
for nn in range(5):
x, y = nn, 2 * nn
# this will only print for testing
BB().when_context(what='testing').show('x')
# this will only print for prodution
BB().when_context(what='production').show('y')
# Set a a testing context using a decorator
@in_context(what='testing')
def test_x():
my_function()
# Now run the function under a test
test_x()
# Set a production context using a context-manager and call the function
with in_context(what='production'):
my_function()
"""
_behold_class = Behold
def __init__(self, **context_vars):
self._context_vars = context_vars
def __call__(self, f):
@functools.wraps(f)
def decorated(*args, **kwds):
with self:
return f(*args, **kwds)
return decorated
def __enter__(self):
self.__class__._behold_class.set_context(**self._context_vars)
def __exit__(self, *args, **kwargs):
self.__class__._behold_class.unset_context(*self._context_vars.keys())
[docs]def set_context(**kwargs):
"""
:type context_vars: key-work arguments
:param context_vars: Key-word arguments specifying the context variables
you would like to set.
This function lets you manually set context variables without using
decorators or with statements.
.. code-block:: python
from behold import Behold
from behold import set_context, unset_context
# manually set a context
set_context(what='my_context')
# print some variables in that context
Behold().when_context(what='my_context').show(x='hello')
# manually unset the context
unset_context('what')
"""
Behold.set_context(**kwargs)
[docs]def unset_context(*keys):
"""
:type keys: string arguments
:param keys: Arguments specifying the names of context variables you
would like to unset.
See the ``set_context()`` method for an example of how to use this.
"""
Behold.unset_context(*keys)
[docs]def get_stash(name):
"""
:type name: str
:param name: The name of the stash you want to retrieve
:rtype: list
:return: A list of dictionaries holding stashed records for each time the
``behold.stash()`` method was called.
For examples, see documentation for ``Behold.stash()`` as well as the stash
`examples on Github <https://github.com/robdmc/behold#stashing-results>`_.
"""
return Behold.get_stash(name)
[docs]def clear_stash(*names):
"""
:type names: string arguments
:param name: The names of stashes you would like to clear.
This method removes all global data associated with a particular stash name.
"""
Behold.clear_stash(*names)