├── .travis.yml
├── CONTRIBUTORS.txt
├── LICENSE
├── MANIFEST.in
├── README.md
├── behold
├── __init__.py
├── logger.py
├── tests
│ ├── __init__.py
│ ├── logger_tests.py
│ └── testing_helpers.py
└── version.py
├── docs
├── Makefile
├── conf.py
├── index.rst
├── ref
│ └── behold.rst
└── toc.rst
├── docs_requirements.txt
├── publish.py
├── setup.cfg
└── setup.py
/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: false
2 | language: python
3 | python:
4 | - '2.7'
5 | - '3.4'
6 | - '3.5'
7 | - '3.6-dev'
8 | install:
9 | - pip install -e .[dev]
10 | before_script:
11 | - flake8 .
12 | script:
13 | - nosetests
14 | - coverage report --fail-under=100
15 | after_success:
16 | - coveralls
17 | notifications:
18 | email: false
19 |
20 |
--------------------------------------------------------------------------------
/CONTRIBUTORS.txt:
--------------------------------------------------------------------------------
1 | Rob deCarvalho (unlisted@unlisted.com)
2 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2017 Rob deCarvalho
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of
6 | this software and associated documentation files (the "Software"), to deal in
7 | the Software without restriction, including without limitation the rights to
8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9 | the Software, and to permit persons to whom the Software is furnished to do so,
10 | subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include README.rst
2 | include CONTRIBUTORS.txt
3 | include LICENSE
4 | prune */tests
5 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Behold: A debugging tool for large Python projects
2 | ===
3 | [](https://travis-ci.org/robdmc/behold)
4 | [](https://coveralls.io/github/robdmc/behold?branch=develop)
5 |
6 | Behold is a package that makes it easier to debug large Python projects. It
7 | enables you to perform [contextual debugging](#contextual-debugging-explained)
8 | over your entire code base. This means that you can use the state inside one
9 | module to control either printing or step-debugging in a completely different
10 | module. Given the stateful nature of many large, multi-file applications (I'm
11 | looking at you, Django), this capability provides valuable control over your
12 | debugging work flow.
13 |
14 | Behold is written in pure Python with no dependencies. It is compatible with
15 | both Python2 and Python3.
16 |
17 | This page shows several examples to get you started. The
18 | API documentation can be found here.
19 |
20 |
21 | Installation
22 | ---
23 | ```bash
24 | pip install behold
25 | ```
26 |
27 | Table of Contents
28 | ---
29 |
30 | * [API Documentation](http://behold.readthedocs.io/en/latest/ref/behold.html)
31 | * [Simple print-style debugging](#simple-print-style-debugging)
32 | * [Conditional printing](#conditional-printing)
33 | * [Tagged printing](#tagged-printing)
34 | * [Contextual debugging](#contextual-debugging-explained)
35 | * [Printing object attributes](#printing-object-attributes)
36 | * [Printing global variables and nested attributes](#printing-global-variables-and-nested-attributes)
37 | * [Stashing results](#stashing-results)
38 | * [Custom attribute extraction](#custom-attribute-extraction)
39 |
40 |
41 | Simple Print-Style Debugging
42 | ---
43 | Behold provides a uniform look to your print-style debugging statements.
44 | ```python
45 | from behold import Behold
46 |
47 | letters = ['a', 'b', 'c', 'd', 'A', 'B', 'C', 'D']
48 |
49 | for index, letter in enumerate(letters):
50 | # The following line is equivalent to
51 | # print('index: {}, letter: {}'.format(index, letter))
52 | Behold().show('index', 'letter')
53 | ```
54 | Output:
55 | ```
56 | index: 0, letter: a
57 | index: 1, letter: b
58 | index: 2, letter: c
59 | index: 3, letter: d
60 | index: 4, letter: A
61 | index: 5, letter: B
62 | index: 6, letter: C
63 | index: 7, letter: D
64 | ```
65 |
66 | Conditional Printing
67 | ---
68 | You can filter your debugging statements based on scope variables.
69 | ```python
70 | from behold import Behold
71 |
72 | letters = ['a', 'b', 'c', 'd', 'A', 'B', 'C', 'D']
73 |
74 | for index, letter in enumerate(letters):
75 | # The following line is equivalent to
76 | # if letter.upper() == letter and index % 2 == 0:
77 | # print('index: {}'.format(index))
78 | Behold().when(letter.upper() == letter and index % 2 == 0).show('index')
79 |
80 | # If you don't like typing, the Behold class is aliased to B
81 | # from behold import B # this also works
82 | ```
83 | Output:
84 | ```
85 | index: 4
86 | index: 6
87 | ```
88 |
89 | Tagged Printing
90 | ---
91 | Each instance of a behold object can be tagged to produce distinguishable
92 | output. This makes it easy to grep for specific output you want to see.
93 | ```python
94 | from behold import Behold
95 |
96 | letters = ['a', 'b', 'c', 'd', 'A', 'B', 'C', 'D']
97 |
98 | for index, letter in enumerate(letters):
99 | # The following two lines of code are equivalent to
100 | # if letter.upper() == letter and index % 2 == 0:
101 | # print('index: {}, letter:, {}, even_uppercase'.format(index, letter))
102 | # if letter.upper() != letter and index % 2 != 0:
103 | # print('index: {}, letter: {} odd_lowercase'.format(index, letter))
104 | Behold(tag='even_uppercase').when(letter.upper() == letter and index % 2 == 0).show('index', 'letter')
105 | Behold(tag='odd_lowercase').when(letter.lower() == letter and index % 2 != 0).show('index', 'letter')
106 |
107 | ```
108 | Output:
109 | ```
110 | index: 1, letter: b, odd_lowercase
111 | index: 3, letter: d, odd_lowercase
112 | index: 4, letter: A, even_uppercase
113 | index: 6, letter: C, even_uppercase
114 | ```
115 |
116 | Contextual Debugging Explained
117 | ---
118 | Let's say you have a complicated code base consisting of many files spread over
119 | many directories. In the course of chasing down bugs, you may want to print out
120 | what is going on inside a particular function. But you only want the printing
121 | to happen when that function is called from some other function defined in a
122 | completely different file. Situations like this frequently arise in Django web
123 | projects where the code can be spread across multiple apps. This is the use
124 | case where Behold really shines. Here is a simple example.
125 |
126 | Say you want to debug a reusable function somewhere in one of your modules.
127 | ```python
128 | from behold import Behold
129 |
130 | # Some function that is used everywhere in your code base
131 | def my_function():
132 | x = 'hello' # your complicated logic goes here
133 |
134 | # This will print the value of x, but only when in 'testing' context
135 | Behold().when_context(what='testing').show('x')
136 |
137 | # This will drop into a step debugger only when in 'debugging' context
138 | if Behold().when_context(what='debugging').is_true():
139 | import pdb; pdb.set_trace()
140 | ```
141 |
142 | Now, from a completely different module somewhere else in your project, you can
143 | control how your function gets debugged.
144 | ```python
145 | from behold import in_context
146 |
147 | # Decorate your testing function to execute in a 'testing' context
148 | @in_context(what='testing')
149 | def test_x():
150 | my_function()
151 | test_x() # This will print 'x: hello' to your console
152 |
153 | # Use a context manager to set a debugging context
154 | with in_context(what='debugging'):
155 | my_function() # This will drop you into the pdb debugger.
156 |
157 | ```
158 |
159 |
160 | Printing Object Attributes
161 | ---
162 | Up to this point, we have only called the `.show()` method with string arguments
163 | holding names of local variables. What if we wanted to show attributes of some
164 | object in our code? The example below uses an instance of the
165 |
166 | Item class
167 |
168 |
169 | ```python
170 | from behold import Behold, Item
171 |
172 | # Define an item with three attributes.
173 | item = Item(a=1, b=2, c=3)
174 |
175 | # The show() method will accept up to one non-string argument. If it detects that
176 | # that a non-string argument has been passed, it will call getattr() on the
177 | # non-string variable to display the str representation of the attributes listed
178 | # in the string arguments.
179 | Behold(tag='with_args').show(item, 'a', 'b')
180 |
181 | # Calling show with an object and no string arguments defaults to printing all
182 | # attributes in the object's __dict__.
183 | Behold(tag='no_args').show(item)
184 | ```
185 | Output:
186 | ```
187 | a: 1, b: 2, with_args
188 | a: 1, b: 2, c: 3, no_args
189 | ```
190 |
191 | Printing Global Variables and Nested Attributes
192 | ---
193 | When providing string arguments to the `.show()` method, the default behavior is
194 | to examine the local variables for names matching the strings. Global variables
195 | can not be accessed in this way. Furthermore, if you have classes with nested
196 | attributes, those will also not be accessible with simple string arguments.
197 | This example illustrates how to use `.show()` to access these types of
198 | variables.
199 |
200 | ```python
201 | from __future__ import print_function
202 | from behold import Behold, Item
203 |
204 | # define a global variable
205 | g = 'global_content'
206 |
207 | # Now set up a nested function to create a new scope
208 | def example_func():
209 | employee = Item(name='Toby')
210 | boss = Item(employee=employee, name='Michael')
211 |
212 | print('# Can\'t see global variable')
213 | Behold().show('boss', 'employee', 'g')
214 |
215 | print('\n# I can see the the boss\'s name, but not employee name')
216 | Behold('no_employee_name').show(boss)
217 |
218 | print('\n# Here is how to show global variables')
219 | Behold().show(global_g=g, boss=boss)
220 |
221 | # Or if you don't like the ordering the dict keys give you,
222 | # you can enforce it with the order of some string arguments
223 | print('\n# You can force variable ordering by supplying string arguments')
224 | Behold().show('global_g', 'boss', global_g=g, boss=boss)
225 |
226 | print('\n# And a similar strategy for nested attributes')
227 | Behold().show(employee_name=boss.employee.name)
228 |
229 | example_func()
230 | ```
231 | Output:
232 | ```bash
233 | # Can't see global variable
234 | boss: Item('employee', 'name'), employee: Item('name'), g: None
235 |
236 | # I can see the the boss's name, but not employee name
237 | employee: Item('name'), name: Michael, no_employee_name
238 |
239 | # Here is how to show global variables
240 | boss: Item('employee', 'name'), global_g: global_content
241 |
242 | # You can force variable ordering by supplying string arguments
243 | global_g: global_content, boss: Item('employee', 'name')
244 |
245 | # And a similar strategy for nested attributes
246 | employee_name: Toby
247 | ```
248 |
249 | Stashing Results
250 | ---
251 | Behold provides a global stash space where you can store observed values for
252 | later use in a top-level summary. The stash space is global, so you need to
253 | carefully manage it in order not to confuse yourself. Here is an example of
254 | using the stash feature to print summary info. The list of dicts returned by the
255 | `.get_stash()` function was specifically designed to be passed directly to a Pandas Dataframe constructor to help
257 | simplify further analysis.
258 |
259 | ```python
260 | from __future__ import print_function
261 | from pprint import pprint
262 | from behold import Behold, in_context, get_stash, clear_stash
263 |
264 | def my_function():
265 | out = []
266 | for nn in range(5):
267 | x, y, z = nn, 2 * nn, 3 * nn
268 | out.append((x, y, z))
269 |
270 | # You must define tags if you want to stash variables. The tag
271 | # names become the keys in the global stash space
272 |
273 | # this will only populate when testing x
274 | Behold(tag='test_x').when_context(what='test_x').stash('y', 'z')
275 |
276 | # this will only populate when testing y
277 | Behold(tag='test_y').when_context(what='test_y').stash('x', 'z')
278 |
279 | # this will only populate when testing z
280 | Behold(tag='test_z').when_context(what='test_z').stash('x', 'y')
281 | return out
282 |
283 |
284 | @in_context(what='test_x')
285 | def test_x():
286 | assert(sum([t[0] for t in my_function()]) == 10)
287 |
288 | @in_context(what='test_y')
289 | def test_y():
290 | assert(sum([t[1] for t in my_function()]) == 20)
291 |
292 | @in_context(what='test_z')
293 | def test_z():
294 | assert(sum([t[2] for t in my_function()]) == 30)
295 |
296 | test_x()
297 | test_y()
298 | test_z()
299 |
300 |
301 | print('\n# contents of test_x stash. Notice only y and z as expected')
302 | pprint(get_stash('test_x'))
303 |
304 | print('\n# contents of test_y stash. Notice only x and z as expected')
305 | pprint(get_stash('test_y'))
306 |
307 | print('\n# contents of test_z stash. Notice only x and y as expected')
308 | pprint(get_stash('test_z'))
309 |
310 | # With no arguments, clear_stash will delete all stashes. You can
311 | # select a specific set of stashes to clear by supplying their names.
312 | clear_stash()
313 | ```
314 | Output:
315 | ```
316 |
317 | # contents of test_x stash. Notice only y and z as expected
318 | [{'y': 0, 'z': 0},
319 | {'y': 2, 'z': 3},
320 | {'y': 4, 'z': 6},
321 | {'y': 6, 'z': 9},
322 | {'y': 8, 'z': 12}]
323 |
324 | # contents of test_y stash. Notice only x and z as expected
325 | [{'x': 0, 'z': 0},
326 | {'x': 1, 'z': 3},
327 | {'x': 2, 'z': 6},
328 | {'x': 3, 'z': 9},
329 | {'x': 4, 'z': 12}]
330 |
331 | # contents of test_z stash. Notice only x and y as expected
332 | [{'x': 0, 'y': 0},
333 | {'x': 1, 'y': 2},
334 | {'x': 2, 'y': 4},
335 | {'x': 3, 'y': 6},
336 | {'x': 4, 'y': 8}]
337 | ```
338 |
339 | Custom Attribute Extraction
340 | ---
341 | When working with database applications, you frequently encounter objects that
342 | are referenced by id numbers. These ids serve as record keys from which you can
343 | extract human-readable information. When you are debugging, it can often get
344 | confusing if your screen dump involves just a bunch of id numbers. What you
345 | would actually like to see is some meaningful name corresponding to that id. By
346 | simply overriding one method of the Behold class, this behavior is quite easy to
347 | implement. This example shows how.
348 | ```python
349 | from __future__ import print_function
350 | from behold import Behold, Item
351 |
352 |
353 | # Subclass Behold to enable custom attribute extraction
354 | class CustomBehold(Behold):
355 | @classmethod
356 | def load_state(cls):
357 | # Notice this is a class method so that the loaded state will be
358 | # available to all instances of CustomBehold. A common use case would
359 | # be to load state like this once from a database and then be able to
360 | # reuse it at will without invoking continual database activity. In
361 | # this example, imagine the numbers are database ids and you have
362 | # constructed a mapping from id to some human-readable description.
363 | cls.name_lookup = {
364 | 1: 'John',
365 | 2: 'Paul',
366 | 3: 'George',
367 | 4: 'Ringo'
368 | }
369 | cls.instrument_lookup = {
370 | 1: 'Rhythm Guitar',
371 | 2: 'Bass Guitar',
372 | 3: 'Lead Guitar',
373 | 4: 'Drums'
374 | }
375 |
376 | def extract(self, item, name):
377 | """
378 | I am overriding the extract() method of the behold class. This method
379 | is responsible for taking an object and turning it into a string. The
380 | default behavior is to simply call str() on the object.
381 | """
382 | # if the lookup state hasn't been loaded, do so now.
383 | if not hasattr(self.__class__, 'name_lookup'):
384 | self.__class__.load_state()
385 |
386 | # extract the value from the behold item
387 | val = getattr(item, name)
388 |
389 | # If this is a Item object, enable name translation
390 | if isinstance(item, Item) and name == 'name':
391 | return self.__class__.name_lookup.get(val, None)
392 |
393 | # If this is a Item object, enable instrument translation
394 | elif isinstance(item, Item) and name == 'instrument':
395 | return self.__class__.instrument_lookup.get(val, None)
396 |
397 | # otherwise, just call the default extractor
398 | else:
399 | return super(CustomBehold, self).extract(item, name)
400 |
401 |
402 | # define a list of items where names and instruments are given by id numbers
403 | items = [Item(name=nn, instrument=nn) for nn in range(1, 5)]
404 |
405 | print('\n# Show items using standard Behold class')
406 | for item in items:
407 | Behold().show(item)
408 |
409 |
410 | print('\n# Show items using CustomBehold class with specialized extractor')
411 | for item in items:
412 | CustomBehold().show(item, 'name', 'instrument')
413 | ```
414 | Output:
415 | ```bash
416 | # Show items using standard Behold class
417 | instrument: 1, name: 1
418 | instrument: 2, name: 2
419 | instrument: 3, name: 3
420 | instrument: 4, name: 4
421 |
422 | # Show items using CustomBehold class with specialized extractor
423 | name: John, instrument: Rhythm Guitar
424 | name: Paul, instrument: Bass Guitar
425 | name: George, instrument: Lead Guitar
426 | name: Ringo, instrument: Drums
427 | ```
428 |
429 | ___
430 | Projects by [robdmc](https://www.linkedin.com/in/robdecarvalho).
431 | * [Pandashells](https://github.com/robdmc/pandashells) Pandas at the bash command line
432 | * [Consecution](https://github.com/robdmc/consecution) Pipeline abstraction for Python
433 | * [Behold](https://github.com/robdmc/behold) Helping debug large Python projects
434 | * [Crontabs](https://github.com/robdmc/crontabs) Simple scheduling library for Python scripts
435 | * [Switchenv](https://github.com/robdmc/switchenv) Manager for bash environments
436 | * [Gistfinder](https://github.com/robdmc/gistfinder) Fuzzy-search your gists
437 |
438 |
439 |
--------------------------------------------------------------------------------
/behold/__init__.py:
--------------------------------------------------------------------------------
1 | # flake8: noqa
2 | from .logger import (
3 | Behold,
4 | Item,
5 | in_context,
6 | set_context,
7 | unset_context,
8 | clear_stash,
9 | get_stash,
10 | )
11 |
12 | # single letter alias
13 | B = Behold
14 |
15 | # single-letter alias's can be hard to find. So make a repeating letter alias
16 | BB = Behold
17 |
18 |
--------------------------------------------------------------------------------
/behold/logger.py:
--------------------------------------------------------------------------------
1 | from collections import defaultdict, OrderedDict
2 | import copy
3 | import functools
4 | import inspect
5 | import operator
6 | import sys
7 |
8 | # TODO: THINK ABOUT CHANGING ALL NON-INTERFACE METHODS TO PRIVATE
9 |
10 | # TODO: Maybe add a strict kwark go Behold that will fail if
11 | # context/values keys aren't found.
12 |
13 | # TODO: make sure you can filter on unshown variables
14 | # TODO: test the inquality operator
15 |
16 |
17 | class _Sentinal(object):
18 | pass
19 |
20 |
21 | class Item(object):
22 | """
23 | Item is a simple container class that sets its attributes from constructor
24 | kwargs. It supports both object and dictionary access to its attributes.
25 | So, for example, all of the following statements are supported.
26 |
27 | .. code-block:: python
28 |
29 | item = Item(a=1, b=2)
30 | item['c'] = 2
31 | a = item['a']
32 |
33 | An instance of this class is created when you ask to show local variables
34 | with a `Behold` object. The local variables you want to show are attached as
35 | attributes to an `Item` object.
36 | """
37 | # I'm using unconventional "_item_self_" name here to avoid
38 | # conflicts when kwargs actually contain a "self" arg.
39 |
40 | def __init__(_item_self, **kwargs):
41 | for key, val in kwargs.items():
42 | _item_self[key] = val
43 |
44 | def __str__(_item_self):
45 | quoted_keys = [
46 | '\'{}\''.format(k) for k in sorted(vars(_item_self).keys())]
47 | att_string = ', '.join(quoted_keys)
48 | return 'Item({})'.format(att_string)
49 |
50 | def __repr__(_item_self):
51 | return _item_self.__str__()
52 |
53 | def __setitem__(_item_self, key, value):
54 | setattr(_item_self, key, value)
55 |
56 | def __getitem__(_item_self, key):
57 | return getattr(_item_self, key)
58 |
59 |
60 | class Behold(object):
61 | """
62 | :type tag: str
63 | :param tag: A tag with which to label all output (default: None)
64 |
65 | :type strict: Bool
66 | :param strict: When set to true, will only only allow existing keys to be
67 | used in the ``when_contex()`` and ``when_values()``
68 | methods.
69 |
70 | :type stream: FileObject
71 | :param stream: Any write-enabled python FileObject (default: sys.stdout)
72 |
73 | :ivar stream: sys.stdout: The stream that will be written to
74 | :ivar tag: None: A string with which to tag output
75 | :ivar strict: False: A Bool that sets whether or not only existing keys
76 | allowed in ``when_contex()`` and ``when_values()``
77 | methods.
78 |
79 | ``Behold`` objects are used to probe state within your code base. They can
80 | be used to log output to the console or to trigger entry points for step
81 | debugging.
82 |
83 | Because it is used so frequently, the behold class has a couple of aliases.
84 | The following three statements are equivalent
85 |
86 | .. code-block:: python
87 |
88 | from behold import Behold # Import using the name of the class
89 |
90 | from behold import B # If you really hate typing
91 |
92 | from behold import BB # If you really hate typing but would
93 | # rather use a name that's easier to
94 | # search for in your editor.
95 |
96 | from behold import * # Although bad practice in general, since
97 | # you'll usually be using behold just for
98 | # debugging, this is pretty convenient.
99 |
100 |
101 | """
102 | # class variable to hold all context values
103 | _context = {}
104 | _stash = defaultdict(list)
105 |
106 | # operators to handle django-style querying
107 | _op_for = {
108 | '__lt': operator.lt,
109 | '__lte': operator.le,
110 | '__le': operator.le,
111 | '__gt': operator.gt,
112 | '__gte': operator.ge,
113 | '__ge': operator.ge,
114 | '__ne': operator.ne,
115 | '__in': lambda value, options: value in options
116 | }
117 | # TODO; maybe add __contains and __startwith
118 | # And if you do, add it to the when*() methods docstrings
119 |
120 | def __init__(self, tag=None, strict=False, stream=None):
121 | self.tag = tag
122 | self.strict = strict
123 |
124 | #: Doc comment for class attribute Foo.bar.
125 | #: It can have multiple lines.
126 | self.stream = None
127 | if stream is None:
128 | self.stream = sys.stdout
129 | else:
130 | self.stream = stream
131 |
132 | # these filters apply to context variables
133 | self.passes = True
134 | self.context_filters = []
135 | self.value_filters = []
136 | self._viewed_context_keys = []
137 |
138 | # a list of fields that will be printed if filters pass
139 | self.print_keys = []
140 |
141 | # holds a string rep for this object
142 | self._str = ''
143 |
144 | # a bool to hold whether or not all filters have passed
145 | self._passes_all = False
146 |
147 | def reset(self):
148 | self.passes = False
149 | self.context_filters = []
150 | self.value_filters = []
151 | self._viewed_context_keys = []
152 |
153 | def _key_to_field_op(self, key):
154 | # this method looks at a key and checks if it ends in any of the
155 | # endings that have special django-like query meanings.
156 | # It translates those into comparision operators and returns the
157 | # name of the actual key.
158 | op = operator.eq
159 | name = key
160 | for op_name, trial_op in self.__class__._op_for.items():
161 | if key.endswith(op_name):
162 | op = trial_op
163 | name = key.split('__')[0]
164 | break
165 | return op, name
166 |
167 | @classmethod
168 | def set_context(cls, **kwargs):
169 | cls._context.update(kwargs)
170 |
171 | @classmethod
172 | def unset_context(cls, *keys):
173 | for key in keys:
174 | if key in cls._context:
175 | cls._context.pop(key)
176 |
177 | def when(self, *bools):
178 | """
179 | :type bools: bool
180 | :param bools: Boolean arguments
181 |
182 | All boolean arguments passed to this method must evaluate to `True` for
183 | printing to be enabled.
184 |
185 | So for example, the following code would print ``x: 1``
186 |
187 | .. code-block:: python
188 |
189 | for x in range(10):
190 | Behold().when(x == 1).show('x')
191 | """
192 | self.passes = self.passes and all(bools)
193 | return self
194 |
195 | def view_context(self, *context_keys):
196 | """
197 | :type context_keys: string arguments
198 | :param context_keys: Strings with context keys
199 |
200 | This method allows you to show values of context variables along with
201 | the local variables you are examining. It is useful for sorting out
202 | which context is active when filtering with "in queries" like this
203 | the ``myvar__in=[1, 2]``
204 | """
205 | self._viewed_context_keys.extend(context_keys)
206 | return self
207 |
208 | def when_context(self, **criteria):
209 | """
210 | :type criteria: kwargs
211 | :param criteria: Key word arguments of var_name=var_value
212 |
213 | The key-word arguments passed to this method specify the context
214 | constraints that must be met in order for printing to occur. The
215 | syntax of these constraints is reminiscent of that used in Django
216 | querysets. All specified criteria must be met for printing to occur.
217 |
218 | The following syntax is supported.
219 |
220 | * ``x__lt=1`` means ``x < 1``
221 | * ``x__lte=1`` means ``x <= 1``
222 | * ``x__le=1`` means ``x <= 1``
223 | * ``x__gt=1`` means ``x > 1``
224 | * ``x__gte=1`` means ``x >= 1``
225 | * ``x__ge=1`` means ``x >= 1``
226 | * ``x__ne=1`` means ``x != 1``
227 | * ``x__in=[1, 2, 3]`` means ``x in [1, 2, 3]``
228 |
229 | The reason this syntax is needed is that the context values being
230 | compared are not available in the local scope. This renders the normal
231 | Python comparison operators useless.
232 | """
233 | self._add_context_filters(**criteria)
234 | return self
235 |
236 | def when_values(self, **criteria):
237 | """
238 | By default, ``Behold`` objects call ``str()`` on all variables before
239 | sending them to the output stream. This method enables you to filter on
240 | those extracted string representations. The syntax is exactly like that
241 | of the ``when_context()`` method. Here is an example.
242 |
243 | .. code-block:: python
244 |
245 | from behold import Behold, Item
246 |
247 | items = [
248 | Item(a=1, b=2),
249 | Item(c=3, d=4),
250 | ]
251 |
252 | for item in items:
253 | # You can filter on the string representation
254 | Behold(tag='first').when_values(a='1').show(item)
255 |
256 | # Behold is smart enough to transform your criteria to strings
257 | # so this also works
258 | Behold(tag='second').when_values(a=1).show(item)
259 |
260 | # Because the string representation is not present in the local
261 | # scope, you must use Django-query-like syntax for logical
262 | # operations.
263 | Behold(tag='third').when_values(a__gte=1).show(item)
264 | """
265 | criteria = {k: str(v) for k, v in criteria.items()}
266 | self._add_value_filters(**criteria)
267 | return self
268 |
269 | def _add_context_filters(self, **criteria):
270 | for key, val in criteria.items():
271 | op, field = self._key_to_field_op(key)
272 | self.context_filters.append((op, field, val))
273 |
274 | def _add_value_filters(self, **criteria):
275 | for key, val in criteria.items():
276 | op, field = self._key_to_field_op(key)
277 | self.value_filters.append((op, field, val))
278 |
279 | def _passes_filter(self, filter_list, value_extractor, default_when_missing=True):
280 | passes = True
281 | for (op, field, filter_val) in filter_list:
282 | # _Sentinal object means current value couldn't be extraced
283 | current_val = value_extractor(field)
284 | no_value_found = isinstance(current_val, _Sentinal)
285 |
286 | # if you couldn't extract a value, do the default thing
287 | if no_value_found:
288 | passes = default_when_missing
289 | # otherwise update whether or not this passes
290 | else:
291 | passes = passes and op(current_val, filter_val)
292 |
293 | if not passes:
294 | return False
295 | return True
296 |
297 | def _passes_value_filter(self, item, name):
298 | if not self.value_filters:
299 | return True
300 |
301 | def value_extractor(field):
302 | return self.extract(item, field)
303 |
304 | return self._passes_filter(self.value_filters, value_extractor)
305 |
306 | def _strict_checker(self, names, item=None):
307 | if self.strict:
308 | names = set(names)
309 | if item is None:
310 | allowed_names = set(self.__class__._context.keys())
311 | else:
312 | allowed_names = set(item.__dict__.keys())
313 | bad_names = names - allowed_names
314 | if bad_names:
315 | msg = (
316 | '\n\nKeys {} not found.\n'
317 | 'Allowed keys: {}'
318 | ).format(
319 | list(sorted(bad_names)),
320 | list(sorted(allowed_names))
321 | )
322 |
323 | raise ValueError(msg)
324 |
325 | def _passes_context_filter(self):
326 | if not self.context_filters:
327 | return True
328 | else:
329 |
330 | def value_extractor(field):
331 | return self.__class__._context.get(field, _Sentinal())
332 |
333 | return self._passes_filter(
334 | self.context_filters, value_extractor,
335 | default_when_missing=False)
336 |
337 | def passes_all(self, item=None, att_names=None):
338 | if not self.passes or not self._passes_context_filter():
339 | self._passes_all = False
340 |
341 | elif item is not None and att_names is not None:
342 | self._passes_all = all([
343 | self._passes_value_filter(item, name)
344 | for name in att_names
345 | ])
346 | else:
347 | self._passes_all = True
348 | return self._passes_all
349 |
350 | def _separate_names_objects(self, values):
351 | att_names = []
352 | objs = []
353 | for val in values:
354 | if isinstance(val, str):
355 | att_names.append(val)
356 | else:
357 | objs.append(val)
358 | return att_names, objs
359 |
360 | def _validate_objs(self, objs):
361 | has_obj = bool(objs)
362 | has_multi_objs = len(objs) > 1
363 |
364 | # only allow at most one object
365 | if has_multi_objs:
366 | raise ValueError(
367 | '\n\nYou can pass at most one non-string argument.'
368 | )
369 |
370 | if has_obj:
371 | # make sure object is useable
372 | if not hasattr(objs[0], '__dict__'):
373 | raise ValueError(
374 | 'Error in Behold() The object you passed has '
375 | 'no __dict__ attribute'
376 | )
377 |
378 | def _get_item_and_att_names(self, *values, **data):
379 | if not self.passes_all():
380 | return None, None
381 |
382 | att_names, objs = self._separate_names_objects(values)
383 | all_att_names = set(att_names)
384 |
385 | # gather information about the inputs
386 | has_data = bool(data)
387 | has_obj = bool(objs)
388 |
389 | # make sure objs are okay
390 | self._validate_objs(objs)
391 |
392 | # If an object was provided, create a dict with its attributes
393 | if has_obj:
394 | att_dict = objs[0].__dict__
395 |
396 | # If no object was provided, construct an item from the calling local
397 | # scope
398 | else:
399 | # this try/else block is needed to breake reference cycles
400 | try:
401 | att_dict = {}
402 | calling_frame = inspect.currentframe().f_back.f_back
403 |
404 | # update with local variables of the calling frame
405 | att_dict.update(calling_frame.f_locals)
406 | finally:
407 | # delete the calling frame to avoid reference cycles
408 | del calling_frame
409 |
410 | # If data was passed, it gets priority
411 | if has_data:
412 | att_dict.update(data)
413 | att_names.extend(sorted(data.keys()))
414 |
415 | # if no attribute names supplied, use all of them
416 | if not att_names:
417 | att_names = sorted(att_dict.keys())
418 | all_att_names = all_att_names.union(set(att_names))
419 |
420 | # do strict check if requested
421 | if self.strict:
422 | self._strict_checker(att_names, item=Item(**att_dict))
423 |
424 | # check for values passing
425 | if not self.passes_all(Item(**att_dict), list(all_att_names)):
426 | return None, None
427 |
428 | # Limit the att_dict to have only requested attributes.
429 | # Using an ordered dict here to preserve attribute order
430 | # while deduplicating
431 | ordered_atts = OrderedDict()
432 | for att_name in att_names:
433 | ordered_atts[att_name] = att_dict.get(att_name, None)
434 |
435 | # Make an item out of the att_dict (might lose order, but don't care)
436 | item = Item(**ordered_atts)
437 |
438 | # make an ordered list of attribute names
439 | ordered_att_names = list(ordered_atts.keys())
440 | return item, ordered_att_names
441 |
442 | @classmethod
443 | def get_stash(cls, stash_name):
444 | if stash_name in cls._stash:
445 | return copy.deepcopy(cls._stash[stash_name])
446 | else:
447 | raise ValueError(
448 | '\n\nRequested name \'{}\' not in {}'.format(
449 | stash_name, list(cls._stash.keys()))
450 | )
451 |
452 | @classmethod
453 | def clear_stash(cls, *names):
454 | if names:
455 | for name in names:
456 | if name in cls._stash:
457 | del cls._stash[name]
458 | else:
459 | raise ValueError(
460 | '\n\nName \'{}\' not in {}'.format(
461 | name, list(cls._stash.keys())
462 | )
463 | )
464 | else:
465 | cls._stash = defaultdict(list)
466 |
467 | def stash(self, *values, **data):
468 | """
469 | The stash method allows you to stash values for later analysis. The
470 | arguments are identical to the ``show()`` method. Instead of writing
471 | outpout, however, the ``stash()`` method populates a global list with
472 | the values that would have been printed. This allows them to be
473 | accessed later in the debugging process.
474 |
475 | Here is an example.
476 |
477 | .. code-block:: python
478 |
479 | from behold import Behold, get_stash
480 |
481 | for nn in range(10):
482 | # You can only invoke ``stash()`` on behold objects that were
483 | # created with tag. The tag becomes the global key for the stash
484 | # list.
485 | behold = Behold(tag='my_stash_key')
486 | two_nn = 2 * nn
487 |
488 | behold.stash('nn' 'two_nn')
489 |
490 | # You can then run this in a completely different file of your code
491 | # base.
492 | my_stashed_list = get_stash('my_stash_key')
493 | """
494 | if not self.tag:
495 | raise ValueError(
496 | 'You must instantiate Behold with a tag name if you want to '
497 | 'use stashing'
498 | )
499 |
500 | item, att_names = self._get_item_and_att_names(*values, **data)
501 | if not item:
502 | self.reset()
503 | return False
504 |
505 | out = {name: item.__dict__.get(name, None) for name in att_names}
506 |
507 | self.__class__._stash[self.tag].append(out)
508 | self.reset()
509 | return True
510 |
511 | def get(self, *values, **data):
512 | item, att_names = self._get_item_and_att_names(*values, **data)
513 | if not item:
514 | self.reset()
515 | return None
516 | out = {name: item.__dict__.get(name, None) for name in att_names}
517 | return out
518 |
519 | def is_true(self, item=None):
520 | """
521 | If you are filtering on object values, you need to pass that object here.
522 | """
523 | if item:
524 | values = [item]
525 | else:
526 | values = []
527 | self._get_item_and_att_names(*values)
528 | return self._passes_all
529 |
530 | def show(self, *values, **data):
531 | """
532 | :type values: str arguments
533 | :param values: A list of variable or attribute names you want to print.
534 | At most one argument can be something other than a
535 | string. Strings are interpreted as the
536 | variable/attribute names you want to print. If a single
537 | non-string argument is provided, it must be an object
538 | having attributes named in the string variables. If no
539 | object is provided, the strings must be the names of
540 | variables in the local scope.
541 |
542 | :type data: keyword args
543 | :param data: A set of keyword arguments. The key provided will be the
544 | name of the printed variables. The value associated with
545 | that key will have its str() representation printed. You
546 | can think of these keyword args as attatching additional
547 | attributes to any object that was passed in args. If no
548 | object was passed, then these kwargs will be used to create
549 | an object.
550 |
551 | This method will return ``True`` if all the filters passed, otherwise it
552 | will return ``False``. This allows you to perform additional logic in
553 | your debugging code if you wish. Here are some examples.
554 |
555 | .. code-block:: python
556 |
557 | from behold import Behold, Item
558 | a, b = 1, 2
559 | my_list = [a, b]
560 |
561 | # show arguments from local scope
562 | Behold().show('a', 'b')
563 |
564 | # show values from local scope using keyword arguments
565 | Behold.show(a=my_list[0], b=my_list[1])
566 |
567 | # show values from local scope using keyword arguments, but
568 | # force them to be printed in a specified order
569 | Behold.show('b', 'a', a=my_list[0], b=my_list[1])
570 |
571 | # show attributes on an object
572 | item = Item(a=1, b=2)
573 | Behold.show(item, 'a', 'b')
574 |
575 | # use the boolean returned by show to control more debugging
576 | a = 1
577 | if Behold.when(a > 1).show('a'):
578 | import pdb; pdb.set_trace()
579 | """
580 | item, att_names = self._get_item_and_att_names(*values, **data)
581 | if not item:
582 | self.reset()
583 | return False
584 |
585 | self._strict_checker(att_names, item=item)
586 |
587 | # set the string value
588 | self._str = self.stringify_item(item, att_names)
589 | self.stream.write(self._str + '\n')
590 |
591 | passes_all = self._passes_all
592 | self.reset()
593 | return passes_all
594 |
595 | def stringify_item(self, item, att_names):
596 | if not att_names:
597 | raise ValueError(
598 | 'Error in Behold. Could not determine attributes/'
599 | 'variables to show.')
600 |
601 | out = []
602 | for ind, key in enumerate(att_names):
603 | out.append(key + ': ')
604 | has_more = ind < len(att_names) - 1
605 | has_more = has_more or self.tag or self._viewed_context_keys
606 | if has_more:
607 | ending = ', '
608 | else:
609 | ending = ''
610 | val = self.extract(item, key)
611 | out.append(val + ending)
612 |
613 | self._strict_checker(self._viewed_context_keys)
614 |
615 | for ind, key in enumerate(self._viewed_context_keys):
616 | has_more = ind < len(self._viewed_context_keys) - 1
617 | has_more = has_more or self.tag
618 | if has_more:
619 | ending = ', '
620 | else:
621 | ending = ''
622 | out.append(
623 | '{}: {}{}'.format(
624 | key,
625 | self.__class__._context.get(key, ''),
626 | ending
627 | )
628 | )
629 |
630 | if self.tag:
631 | out.append(self.tag)
632 | return ''.join(out)
633 |
634 | def extract(self, item, name):
635 | """
636 | You should never need to call this method when you are debugging. It is
637 | an internal method that is nevertheless exposed to allow you to
638 | implement custom extraction logic for variables/attributes.
639 |
640 | This method is responsible for turning attributes into strings for
641 | printing. The default implementation is shown below, but for custom
642 | situations, you can inherit from `Behold` and override this method to
643 | obtain custom behavior you might find useful. A common strategy is to
644 | load up class-level state to help you make the necessary transformation.
645 |
646 | :type item: Object
647 | :param item: The object from which to print attributes. If you didn't
648 | explicitly provide an object to the `.show()` method,
649 | then `Behold` will attach the local variables you
650 | specified as attributes to an :class:`.Item` object.
651 |
652 | :type name: str
653 | :param name: The attribute name to extract from item
654 |
655 | Here is the default implementation.
656 |
657 | .. code-block:: python
658 |
659 | def extract(self, item, name):
660 | val = ''
661 | if hasattr(item, name):
662 | val = getattr(item, name)
663 | return str(val)
664 |
665 | Here is an example of transforming Django model ids to names.
666 |
667 | .. code-block:: python
668 |
669 | class CustomBehold(Behold):
670 | def load_state(self):
671 | # Put logic here to load your lookup dict.
672 | self.lookup = your_lookup_code()
673 |
674 | def extract(self, item, name):
675 | if hasattr(item, name):
676 | val = getattr(item, name)
677 | if isinstance(item, Model) and name == 'client_id':
678 | return self.lookup.get(val, '')
679 | else:
680 | return super(CustomBehold, self).extract(name, item)
681 | else:
682 | return ''
683 | """
684 | val = ''
685 | if hasattr(item, name):
686 | val = getattr(item, name)
687 | return str(val)
688 |
689 | def __str__(self):
690 | return self._str
691 |
692 | def __repr__(self):
693 | return self.__str__()
694 |
695 |
696 | class in_context(object):
697 | """
698 | :type context_vars: key-work arguments
699 | :param context_vars: Key-word arguments specifying the context variables
700 | you would like to set.
701 |
702 | You can define arbitrary context in which to perform your debugging. A
703 | common use case for this is when you have a piece of code that is called
704 | from many different places in your code base, but you are only interested in
705 | what happens when it's called from a particular location. You can just wrap
706 | that location in a context and only debug when in that context. Here is an
707 | example.
708 |
709 | .. code-block:: python
710 |
711 | from behold import BB # this is an alias for Behold
712 | from behold import in_context
713 |
714 | # A function that can get called from anywhere
715 | def my_function():
716 | for nn in range(5):
717 | x, y = nn, 2 * nn
718 |
719 | # this will only print for testing
720 | BB().when_context(what='testing').show('x')
721 |
722 | # this will only print for prodution
723 | BB().when_context(what='production').show('y')
724 |
725 | # Set a a testing context using a decorator
726 | @in_context(what='testing')
727 | def test_x():
728 | my_function()
729 |
730 | # Now run the function under a test
731 | test_x()
732 |
733 | # Set a production context using a context-manager and call the function
734 | with in_context(what='production'):
735 | my_function()
736 | """
737 | _behold_class = Behold
738 |
739 | def __init__(self, **context_vars):
740 | self._context_vars = context_vars
741 |
742 | def __call__(self, f):
743 | @functools.wraps(f)
744 | def decorated(*args, **kwds):
745 | with self:
746 | return f(*args, **kwds)
747 | return decorated
748 |
749 | def __enter__(self):
750 | self.__class__._behold_class.set_context(**self._context_vars)
751 |
752 | def __exit__(self, *args, **kwargs):
753 | self.__class__._behold_class.unset_context(*self._context_vars.keys())
754 |
755 |
756 | def set_context(**kwargs):
757 | """
758 | :type context_vars: key-work arguments
759 | :param context_vars: Key-word arguments specifying the context variables
760 | you would like to set.
761 |
762 | This function lets you manually set context variables without using
763 | decorators or with statements.
764 |
765 | .. code-block:: python
766 |
767 | from behold import Behold
768 | from behold import set_context, unset_context
769 |
770 |
771 | # manually set a context
772 | set_context(what='my_context')
773 |
774 | # print some variables in that context
775 | Behold().when_context(what='my_context').show(x='hello')
776 |
777 | # manually unset the context
778 | unset_context('what')
779 | """
780 | Behold.set_context(**kwargs)
781 |
782 |
783 | def unset_context(*keys):
784 | """
785 | :type keys: string arguments
786 | :param keys: Arguments specifying the names of context variables you
787 | would like to unset.
788 |
789 | See the ``set_context()`` method for an example of how to use this.
790 | """
791 | Behold.unset_context(*keys)
792 |
793 |
794 | def get_stash(name):
795 | """
796 | :type name: str
797 | :param name: The name of the stash you want to retrieve
798 |
799 | :rtype: list
800 | :return: A list of dictionaries holding stashed records for each time the
801 | ``behold.stash()`` method was called.
802 |
803 | For examples, see documentation for ``Behold.stash()`` as well as the stash
804 | `examples on Github `_.
805 | """
806 | return Behold.get_stash(name)
807 |
808 |
809 | def clear_stash(*names):
810 | """
811 | :type names: string arguments
812 | :param name: The names of stashes you would like to clear.
813 |
814 | This method removes all global data associated with a particular stash name.
815 | """
816 | Behold.clear_stash(*names)
817 |
--------------------------------------------------------------------------------
/behold/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/robdmc/behold/bc44199712527277961efa37ec233fa1873391ff/behold/tests/__init__.py
--------------------------------------------------------------------------------
/behold/tests/logger_tests.py:
--------------------------------------------------------------------------------
1 | import sys
2 | from unittest import TestCase
3 |
4 | try: # pragma: no cover
5 | from cStringIO import StringIO
6 | except: # pragma: no cover
7 | from io import StringIO
8 |
9 | from ..logger import (
10 | Behold,
11 | Item,
12 | in_context,
13 | set_context,
14 | unset_context,
15 | get_stash,
16 | clear_stash
17 | )
18 |
19 | from .testing_helpers import print_catcher
20 |
21 | # a global variable to test global inclusion
22 | g = 7
23 |
24 |
25 | class BaseTestCase(TestCase):
26 | def setUp(self):
27 | Behold._context = {}
28 | clear_stash()
29 |
30 |
31 | def module_func():
32 | m, n = 1, 2 # flake8: noqa
33 | Behold().show('m', 'n', 'g')
34 |
35 |
36 | class BeholdCustom(Behold):
37 | """
38 | """
39 | def __init__(self, *args, **kwargs):
40 | super(BeholdCustom, self).__init__(*args, **kwargs)
41 | self.lookup = {
42 | 1: 'manny',
43 | 2: 'moe',
44 | 3: 'jack',
45 | }
46 |
47 | def extract(self, item, name):
48 | """
49 | I am overriding the extract() method of the behold class. This method
50 | is responsible for taking an object and turning it into a string. The
51 | default behavior is to simply call str() on the object.
52 | """
53 | # extract the value from the behold item
54 | val = getattr(item, name)
55 |
56 | # If this is a MyItem object, enable name translation
57 | if isinstance(item, Item) and name == 'name':
58 | return self.lookup.get(val, None)
59 | # otherwise, just call the default extractor
60 | else:
61 | return super(BeholdCustom, self).extract(item, name)
62 |
63 |
64 | class ItemTests(TestCase):
65 | def test_get_item(self):
66 | item = Item(a=1)
67 | self.assertEqual(item['a'], 1)
68 |
69 | def test_str(self):
70 | item1 = Item(a=1)
71 | item2 = Item(a=1, b='bbb')
72 | self.assertEqual(repr(item1), 'Item(\'a\')')
73 | self.assertEqual(repr(item2), 'Item(\'a\', \'b\')')
74 |
75 |
76 | class TestBeholdRepr(BaseTestCase):
77 | def test_repr(self):
78 | x = 1
79 | with print_catcher() as catchter:
80 | behold = Behold()
81 | behold.show('x')
82 | self.assertEqual(repr(behold), 'x: 1')
83 |
84 | class IsTrueTests(BaseTestCase):
85 | def test_when_no_item(self):
86 | self.assertTrue(Behold().when(True).is_true())
87 | self.assertFalse(Behold().when(False).is_true())
88 |
89 | def test_when_context_no_item(self):
90 | with in_context(what='yes'):
91 | self.assertTrue(Behold().when_context(what='yes').is_true())
92 | self.assertFalse(Behold().when_context(what='yes').is_true())
93 |
94 | def test_when_values_no_item(self):
95 | xx = 'xx'
96 | self.assertTrue(Behold().when_values(xx='xx').is_true())
97 | self.assertFalse(Behold().when_values(xx='yy').is_true())
98 |
99 | def test_when_values_item(self):
100 | item = Item(xx='xx')
101 | self.assertTrue(Behold().when_values(xx='xx').is_true(item))
102 | self.assertFalse(Behold().when_values(xx='yy').is_true(item))
103 |
104 | class ViewContextTests(BaseTestCase):
105 | def test_good_view(self):
106 | xx = 1
107 | with print_catcher() as catcher:
108 | with in_context(what='this', where='here'):
109 | Behold().view_context('what', 'where').show('xx')
110 | self.assertEqual(catcher.txt, 'xx: 1, what: this, where: here\n')
111 |
112 | def test_missing_view(self):
113 | xx = 1
114 | with print_catcher() as catcher:
115 | with in_context(where='here'):
116 | behold = Behold()
117 | behold.view_context('what', 'where')
118 | behold.view_context('what', 'where')
119 | behold.show('xx')
120 | self.assertEqual(
121 | catcher.txt, 'xx: 1, what: , where: here, what: , where: here\n')
122 |
123 | def test_strict_missing_view(self):
124 | with self.assertRaises(ValueError):
125 | with in_context(where='here', when='now'):
126 | Behold(strict=True).view_context('what', 'where').show('xx')
127 |
128 | def test_strict_filter_on_missing_view(self):
129 | with in_context(where='here', when='now'):
130 | Behold(strict=True).when_context(
131 | what='this').view_context('where').is_true() #.show('xx')
132 | #Behold(strict=True).view_context('where').show('xx')
133 |
134 | class ValueFilterTests(BaseTestCase):
135 | def test_values_in(self):
136 | items = [
137 | Item(name=nn, value=nn) for nn in range(1, 4)
138 | ]
139 | with print_catcher() as catcher:
140 | for item in items:
141 | BeholdCustom().when_values(
142 | name__in=['manny', 'moe'], value=2
143 | ).show(item, 'name', 'value')
144 |
145 | self.assertTrue('name: moe, value: 2' in catcher.txt)
146 |
147 | def test_lt(self):
148 |
149 | items = [
150 | Item(name=nn, value=nn) for nn in range(1, 4)
151 | ]
152 |
153 | with print_catcher() as catcher:
154 | for item in items:
155 | BeholdCustom().when_values(value__lt=2).show(item)
156 | self.assertEqual(catcher.txt, 'name: manny, value: 1\n')
157 |
158 | def test_lte(self):
159 |
160 | items = [
161 | Item(name=nn, value=nn) for nn in range(1, 4)
162 | ]
163 |
164 | with print_catcher() as catcher:
165 | for item in items:
166 | BeholdCustom().when_values(value__lte=2).show(item)
167 | self.assertTrue('manny' in catcher.txt)
168 | self.assertTrue('moe' in catcher.txt)
169 |
170 | def test_gt(self):
171 |
172 | items = [
173 | Item(name=nn, value=nn) for nn in range(1, 4)
174 | ]
175 |
176 | with print_catcher() as catcher:
177 | for item in items:
178 | BeholdCustom().when_values(value__gt=2).show(item)
179 | self.assertEqual(catcher.txt, 'name: jack, value: 3\n')
180 |
181 | def test_gte(self):
182 |
183 | items = [
184 | Item(name=nn, value=nn) for nn in range(1, 4)
185 | ]
186 |
187 | with print_catcher() as catcher:
188 | for item in items:
189 | BeholdCustom().when_values(value__gte=2).show(item)
190 | self.assertTrue('moe' in catcher.txt)
191 | self.assertTrue('jack' in catcher.txt)
192 |
193 |
194 | class StashTests(BaseTestCase):
195 | def test_full_stash(self):
196 | for nn in range(10):
197 | x = nn
198 | Behold(tag='mystash').when(nn>=2).stash('nn', 'y')
199 | Behold(tag='mystash2').when(nn>=2).stash('nn', 'y')
200 | stash_list = get_stash('mystash')
201 | expected_list = [{'nn': nn, 'y': None} for nn in range(2, 10)]
202 | self.assertEqual(stash_list, expected_list)
203 | clear_stash('mystash')
204 | with self.assertRaises(ValueError):
205 | get_stash('mystash')
206 | stash_list = get_stash('mystash2')
207 | self.assertEqual(stash_list, expected_list)
208 | clear_stash()
209 | with self.assertRaises(ValueError):
210 | get_stash('mystash2')
211 |
212 | def test_stash_no_tag(self):
213 | nn = 1
214 | with self.assertRaises(ValueError):
215 | Behold().stash('nn')
216 |
217 | def test_stash_bad_item(self):
218 | nn = 1
219 | Behold(tag='bad_stash').stash('xx')
220 | results = get_stash('bad_stash')
221 | self.assertEqual(results, [{'xx': None}])
222 |
223 | def test_stash_bad_delete(self):
224 | nn = 1
225 | Behold(tag='bad_stash').stash('xx')
226 | with self.assertRaises(ValueError):
227 | clear_stash('bad_name')
228 |
229 | def test_stash_no_pass(self):
230 | item = Item(nn=1)
231 | #passed = Behold(tag='mytag').when(False).stash('xx')
232 | passed = Behold(tag='mytag').when_values(nn=3).stash(item, 'nn')
233 | self.assertEqual(passed, False)
234 |
235 |
236 | class GetTests(BaseTestCase):
237 | def test_get_okay(self):
238 | a, b, c = 'aaa', 'bbb', 'ccc'
239 | result = Behold().get('a', 'b', 'd')
240 | self.assertEqual({'a': 'aaa', 'b': 'bbb', 'd': None}, result)
241 |
242 | def test_get_all(self):
243 | a, b, c = 'aaa', 'bbb', 'ccc'
244 | self.assertEqual(set(Behold().get()), {'self', 'a', 'b', 'c'})
245 |
246 | def test_get_item(self):
247 | item = Item(a='aaa', b='bbb', c='ccc')
248 | self.assertEqual(set(Behold().get(item)), {'a', 'b', 'c'})
249 |
250 | def test_get_failing(self):
251 | a, b, c = 'aaa', 'bbb', 'ccc'
252 | result = Behold().when(a=='zzz').get('a', 'b', 'd')
253 | self.assertEqual(None, result)
254 |
255 | class UnfilteredTests(BaseTestCase):
256 | def test_strinfigy_no_names(self):
257 | item = Item()
258 | b = Behold()
259 | with self.assertRaises(ValueError):
260 | b.stringify_item(item, [])
261 |
262 | def test_show_item_with_args_no_kwargs(self):
263 | item = Item(a=1, b=2)
264 | with print_catcher() as catcher:
265 | Behold().show(item, 'a', 'b')
266 | self.assertTrue('a: 1, b: 2' in catcher.txt)
267 |
268 | def test_truthiness(self):
269 | item = Item(a=1, b=2)
270 | with print_catcher() as catcher:
271 | behold = Behold()
272 | with print_catcher() as catcher:
273 | passed = behold.show(item, 'a', 'b')
274 |
275 | out = str(behold)
276 | self.assertTrue(passed)
277 | self.assertTrue('a: 1, b: 2' in out)
278 | self.assertEqual('', catcher.txt)
279 |
280 | def test_unkown_local(self):
281 | c = 1
282 | self.assertFalse(Behold().when_values(a=1).show('a', 'c'))
283 |
284 | def test_show_locals_with_args_no_kwargs(self):
285 | a, b = 1, 2 # flake8: noqa
286 |
287 | def nested():
288 | x, y = 3, 4 # flake8: noqa
289 | Behold().show('a', 'b', 'x', 'y',)
290 | with print_catcher() as catcher:
291 | nested()
292 |
293 | self.assertTrue('a: None, b: None, x: 3, y: 4' in catcher.txt)
294 |
295 | def test_show_from_frame_module_func(self):
296 | with print_catcher() as catcher:
297 | module_func()
298 | self.assertTrue('m: 1, n: 2' in catcher.txt)
299 |
300 | def test_show_with_kwargs_no_args(self):
301 | a, b = 1, 2
302 | with print_catcher() as catcher:
303 | Behold().show(B=b, A=a)
304 | self.assertTrue('A: 1, B: 2' in catcher.txt)
305 |
306 | def test_show_with_kwargs_and_args(self):
307 | a, b = 1, 2
308 |
309 | with print_catcher() as catcher:
310 | Behold().show('B', 'A', B=b, A=a)
311 | self.assertTrue('B: 2, A: 1' in catcher.txt)
312 |
313 | with print_catcher() as catcher:
314 | Behold().show('B', B=b, A=a)
315 | self.assertTrue('B: 2, A: 1' in catcher.txt)
316 |
317 | def test_show_obj_and_data(self):
318 | item = Item(first='one', second='two', a=1, b=2)
319 | with print_catcher() as catcher:
320 | Behold().show(item, 'a', 'b', begin=item.first, end=item.second)
321 | self.assertEqual(catcher.txt, 'a: 1, b: 2, begin: one, end: two\n')
322 |
323 | def test_show_obj_and_data_bad_att(self):
324 | item = Item(a=1, b=2)
325 | with print_catcher() as catcher:
326 | Behold().show(item, 'a', 'b', 'c')
327 | self.assertTrue('a: 1, b: 2, c: ' in catcher.txt)
328 |
329 | def test_show_multiple_obj(self):
330 | item = Item(a=1, b=2)
331 | with self.assertRaises(ValueError):
332 | Behold().show(item, 'a', 'b', item)
333 |
334 | def test_show_only_args(self):
335 | x = ['hello']
336 | with self.assertRaises(ValueError):
337 | Behold().show(x)
338 |
339 | def test_show_with_stream(self):
340 | x, y = 1, 2 # flake8: noqa
341 | stream = StringIO()
342 | Behold(stream=stream).show('x')
343 | self.assertEqual('x: 1\n', stream.getvalue())
344 |
345 | def test_show_with_non_existing_attribute(self):
346 | x = 8 # flake8: noqa
347 |
348 | with print_catcher() as catcher:
349 | Behold().show('x', 'y')
350 |
351 | self.assertEqual(catcher.txt, 'x: 8, y: None\n')
352 |
353 |
354 | class FilteredTests(BaseTestCase):
355 | def test_strict_context_filtering(self):
356 | with in_context(what='testing'):
357 | is_true = Behold(strict=True).when_context(
358 | what='testing').is_true()
359 | self.assertTrue(is_true)
360 |
361 | with in_context(what='testing'):
362 | is_false = Behold(strict=True).when_context(where='here').is_true()
363 | self.assertFalse(is_false)
364 |
365 | with self.assertRaises(ValueError):
366 | with in_context(what='testing'):
367 | x = 1
368 | Behold(strict=True).when_context(what='testing').view_context(
369 | 'where').show('x')
370 |
371 | with print_catcher() as catcher:
372 | with in_context(what='testing'):
373 | x = 1
374 | Behold(strict=True).when_context(what='testing').view_context(
375 | 'what').show('x')
376 | self.assertEqual(catcher.txt, 'x: 1, what: testing\n')
377 |
378 | def test_strict_value_filtering(self):
379 | item = Item(a=1, b=2)
380 | with print_catcher() as catcher:
381 | Behold(strict=True).show(item, 'a', 'b')
382 | self.assertEqual(catcher.txt, 'a: 1, b: 2\n')
383 |
384 | with self.assertRaises(ValueError):
385 | Behold(strict=True).show(item, 'c')
386 |
387 | with self.assertRaises(ValueError):
388 | Behold(strict=True).show('w', 'z')
389 |
390 | def test_arg_filtering(self):
391 | a, b = 1, 2 # flake8: noqa
392 | with print_catcher() as catcher:
393 | passed = Behold().when(a == 1).show('a', 'b')
394 | self.assertEqual(catcher.txt, 'a: 1, b: 2\n')
395 | self.assertTrue(passed)
396 |
397 | with print_catcher() as catcher:
398 | behold = Behold()
399 | passed = behold.when(a == 2).show('a', 'b')
400 | self.assertEqual(catcher.txt, '')
401 | self.assertFalse(passed)
402 | self.assertEqual(repr(behold), '')
403 |
404 | def test_context_filtering_equal(self):
405 | var = 'first' # flake8: noqa
406 | with in_context(what=10):
407 | with print_catcher() as catcher:
408 | passed = Behold(tag='tag').when_context(what=10).show('var')
409 |
410 | self.assertTrue(passed)
411 | self.assertEqual('var: first, tag\n', catcher.txt)
412 |
413 | with in_context(what=10):
414 | with print_catcher() as catcher:
415 | passed = Behold(tag='tag').when_context(what=11).show('var')
416 |
417 | self.assertFalse(passed)
418 | self.assertEqual('', catcher.txt)
419 |
420 | with print_catcher() as catcher:
421 | passed = Behold(tag='tag').when_context(what=11).show('var')
422 |
423 | self.assertFalse(passed)
424 | self.assertEqual('', catcher.txt)
425 |
426 | def test_context_filtering_inequality(self):
427 | var = 'first' # flake8: noqa
428 | with in_context(what=10):
429 | with print_catcher() as catcher:
430 | passed = Behold(tag='tag').when_context(what__gt=5).show('var')
431 |
432 | self.assertTrue(passed)
433 | self.assertEqual('var: first, tag\n', catcher.txt)
434 |
435 | with in_context(what=10):
436 | with print_catcher() as catcher:
437 | passed = Behold(tag='tag').when_context(what__lt=5).show('var')
438 |
439 | self.assertFalse(passed)
440 | self.assertEqual('', catcher.txt)
441 |
442 | def test_context_filtering_membership(self):
443 | var = 'first' # flake8: noqa
444 | with in_context(what=10):
445 | with print_catcher() as catcher:
446 | passed = Behold(
447 | tag='tag').when_context(what__in=[5, 10]).show('var')
448 |
449 | self.assertTrue(passed)
450 | self.assertEqual('var: first, tag\n', catcher.txt)
451 |
452 | with in_context(what=10):
453 | with print_catcher() as catcher:
454 | passed = Behold(
455 | tag='tag').when_context(what__in=[7, 11]).show('var')
456 |
457 | self.assertFalse(passed)
458 | self.assertEqual('', catcher.txt)
459 |
460 | def test_context_decorator(self):
461 | @in_context(what='hello')
462 | def my_func():
463 | x = 1 # flake8: noqa
464 | Behold().when_context(what='hello').show('x')
465 |
466 | def my_out_of_context_func():
467 | x = 1 # flake8: noqa
468 | Behold().when_context(what='hello').show('x')
469 |
470 | with print_catcher() as catcher:
471 | my_func()
472 | self.assertEqual(catcher.txt, 'x: 1\n')
473 |
474 | with print_catcher() as catcher:
475 | my_out_of_context_func()
476 | self.assertEqual(catcher.txt, '')
477 |
478 | def test_explicit_context_setting(self):
479 | def printer():
480 | Behold().when_context(what='hello').show(x='yes')
481 |
482 | set_context(what='hello')
483 | with print_catcher() as catcher:
484 | printer()
485 | self.assertEqual(catcher.txt, 'x: yes\n')
486 |
487 | unset_context('what')
488 | with print_catcher() as catcher:
489 | printer()
490 | self.assertEqual(catcher.txt, '')
491 |
492 | set_context(what='not_hello')
493 | with print_catcher() as catcher:
494 | printer()
495 | self.assertEqual(catcher.txt, '')
496 |
497 | def test_unset_non_existing(self):
498 | def printer():
499 | Behold().when_context(what='hello').show(x='yes')
500 |
501 | set_context(what='hello')
502 | unset_context('what_else')
503 |
504 | with print_catcher() as catcher:
505 | printer()
506 | self.assertEqual(catcher.txt, 'x: yes\n')
507 |
--------------------------------------------------------------------------------
/behold/tests/testing_helpers.py:
--------------------------------------------------------------------------------
1 | import sys
2 | from contextlib import contextmanager
3 |
4 |
5 | # These don't need to covered. They are just tesing utilities
6 | @contextmanager
7 | def print_catcher(buff='stdout'): # pragma: no cover
8 | if buff == 'stdout':
9 | sys.stdout = Printer()
10 | yield sys.stdout
11 | sys.stdout = sys.__stdout__
12 | elif buff == 'stderr':
13 | sys.stderr = Printer()
14 | yield sys.stderr
15 | sys.stderr = sys.__stderr__
16 | else: # pragma: no cover This is just to help testing. No need to cover.
17 | raise ValueError('buff must be either \'stdout\' or \'stderr\'')
18 |
19 |
20 | class Printer(object): # pragma: no cover
21 | def __init__(self):
22 | self.txt = ""
23 |
24 | def write(self, txt):
25 | self.txt += txt
26 |
27 | def lines(self):
28 | for line in self.txt.split('\n'):
29 | yield line.strip()
30 |
--------------------------------------------------------------------------------
/behold/version.py:
--------------------------------------------------------------------------------
1 | __version__ = '0.2.0'
2 |
--------------------------------------------------------------------------------
/docs/Makefile:
--------------------------------------------------------------------------------
1 | # Makefile for Sphinx documentation
2 | #
3 |
4 | # You can set these variables from the command line.
5 | SPHINXOPTS =
6 | SPHINXBUILD = sphinx-build
7 | PAPER =
8 | BUILDDIR = _build
9 |
10 | # User-friendly check for sphinx-build
11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)
13 | endif
14 |
15 | # Internal variables.
16 | PAPEROPT_a4 = -D latex_paper_size=a4
17 | PAPEROPT_letter = -D latex_paper_size=letter
18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
19 | # the i18n builder cannot share the environment and doctrees with the others
20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
21 |
22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp epub latex latexpdf text man changes linkcheck doctest gettext
23 |
24 | help:
25 | @echo "Please use \`make ' where is one of"
26 | @echo " html to make standalone HTML files"
27 | @echo " dirhtml to make HTML files named index.html in directories"
28 | @echo " singlehtml to make a single large HTML file"
29 | @echo " pickle to make pickle files"
30 | @echo " json to make JSON files"
31 | @echo " htmlhelp to make HTML files and a HTML help project"
32 | @echo " epub to make an epub"
33 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
34 | @echo " latexpdf to make LaTeX files and run them through pdflatex"
35 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
36 | @echo " text to make text files"
37 | @echo " man to make manual pages"
38 | @echo " texinfo to make Texinfo files"
39 | @echo " info to make Texinfo files and run them through makeinfo"
40 | @echo " gettext to make PO message catalogs"
41 | @echo " changes to make an overview of all changed/added/deprecated items"
42 | @echo " xml to make Docutils-native XML files"
43 | @echo " pseudoxml to make pseudoxml-XML files for display purposes"
44 | @echo " linkcheck to check all external links for integrity"
45 | @echo " doctest to run all doctests embedded in the documentation (if enabled)"
46 |
47 | clean:
48 | rm -rf $(BUILDDIR)/*
49 |
50 | html:
51 | $(SPHINXBUILD) -W -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
52 | @echo
53 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
54 |
55 | dirhtml:
56 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
57 | @echo
58 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
59 |
60 | singlehtml:
61 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
62 | @echo
63 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
64 |
65 | pickle:
66 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
67 | @echo
68 | @echo "Build finished; now you can process the pickle files."
69 |
70 | json:
71 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
72 | @echo
73 | @echo "Build finished; now you can process the JSON files."
74 |
75 | htmlhelp:
76 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
77 | @echo
78 | @echo "Build finished; now you can run HTML Help Workshop with the" \
79 | ".hhp project file in $(BUILDDIR)/htmlhelp."
80 |
81 | epub:
82 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
83 | @echo
84 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub."
85 |
86 | latex:
87 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
88 | @echo
89 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
90 | @echo "Run \`make' in that directory to run these through (pdf)latex" \
91 | "(use \`make latexpdf' here to do that automatically)."
92 |
93 | latexpdf:
94 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
95 | @echo "Running LaTeX files through pdflatex..."
96 | $(MAKE) -C $(BUILDDIR)/latex all-pdf
97 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
98 |
99 | latexpdfja:
100 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
101 | @echo "Running LaTeX files through platex and dvipdfmx..."
102 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
103 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
104 |
105 | text:
106 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
107 | @echo
108 | @echo "Build finished. The text files are in $(BUILDDIR)/text."
109 |
110 | man:
111 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
112 | @echo
113 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man."
114 |
115 | texinfo:
116 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
117 | @echo
118 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
119 | @echo "Run \`make' in that directory to run these through makeinfo" \
120 | "(use \`make info' here to do that automatically)."
121 |
122 | info:
123 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
124 | @echo "Running Texinfo files through makeinfo..."
125 | make -C $(BUILDDIR)/texinfo info
126 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
127 |
128 | gettext:
129 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
130 | @echo
131 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
132 |
133 | changes:
134 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
135 | @echo
136 | @echo "The overview file is in $(BUILDDIR)/changes."
137 |
138 | linkcheck:
139 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
140 | @echo
141 | @echo "Link check complete; look for any errors in the above output " \
142 | "or in $(BUILDDIR)/linkcheck/output.txt."
143 |
144 | doctest:
145 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
146 | @echo "Testing of doctests in the sources finished, look at the " \
147 | "results in $(BUILDDIR)/doctest/output.txt."
148 |
149 | xml:
150 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
151 | @echo
152 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml."
153 |
154 | pseudoxml:
155 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
156 | @echo
157 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
158 |
--------------------------------------------------------------------------------
/docs/conf.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | import inspect
4 | import os
5 | import re
6 | import sys
7 |
8 | file_dir = os.path.realpath(os.path.dirname(__file__))
9 | sys.path.append(os.path.join(file_dir, '..'))
10 |
11 | def get_version():
12 | """Obtain the packge version from a python file e.g. pkg/__init__.py
13 | See .
14 | """
15 | file_dir = os.path.realpath(os.path.dirname(__file__))
16 | with open(
17 | os.path.join(file_dir, '..', 'behold', 'version.py')) as f:
18 | txt = f.read()
19 | version_match = re.search(
20 | r"""^__version__ = ['"]([^'"]*)['"]""", txt, re.M)
21 | if version_match:
22 | return version_match.group(1)
23 | raise RuntimeError("Unable to find version string.")
24 |
25 |
26 | # If extensions (or modules to document with autodoc) are in another directory,
27 | # add these directories to sys.path here. If the directory is relative to the
28 | # documentation root, use os.path.abspath to make it absolute, like shown here.
29 | #sys.path.insert(0, os.path.abspath('.'))
30 |
31 | # -- General configuration ------------------------------------------------
32 |
33 | extensions = [
34 | 'sphinx.ext.autodoc',
35 | #'sphinx.ext.intersphinx',
36 | 'sphinx.ext.viewcode',
37 | #'sphinxcontrib.fulltoc',
38 | ]
39 |
40 | # Add any paths that contain templates here, relative to this directory.
41 | templates_path = ['_templates']
42 |
43 | # The suffix of source filenames.
44 | source_suffix = '.rst'
45 |
46 | # The master toctree document.
47 | master_doc = 'toc'
48 |
49 | # General information about the project.
50 | project = 'behold'
51 | copyright = '2017, Rob deCarvalho'
52 |
53 | # The short X.Y version.
54 | version = get_version()
55 | # The full version, including alpha/beta/rc tags.
56 | release = version
57 |
58 | exclude_patterns = ['_build']
59 |
60 | # The name of the Pygments (syntax highlighting) style to use.
61 | pygments_style = 'sphinx'
62 |
63 | intersphinx_mapping = {
64 | 'python': ('http://docs.python.org/3.4', None),
65 | 'django': ('http://django.readthedocs.org/en/latest/', None),
66 | #'celery': ('http://celery.readthedocs.org/en/latest/', None),
67 | }
68 |
69 | # -- Options for HTML output ----------------------------------------------
70 |
71 | html_theme = 'default'
72 | #html_theme_path = []
73 |
74 | on_rtd = os.environ.get('READTHEDOCS', None) == 'True'
75 | if not on_rtd: # only import and set the theme if we're building docs locally
76 | import sphinx_rtd_theme
77 | html_theme = 'sphinx_rtd_theme'
78 | html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
79 |
80 | # Add any paths that contain custom static files (such as style sheets) here,
81 | # relative to this directory. They are copied after the builtin static files,
82 | # so a file named "default.css" will overwrite the builtin "default.css".
83 | # html_static_path = ['_static']
84 | html_static_path = []
85 |
86 | # Custom sidebar templates, maps document names to template names.
87 | #html_sidebars = {}
88 |
89 | # Additional templates that should be rendered to pages, maps page names to
90 | # template names.
91 | #html_additional_pages = {}
92 |
93 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
94 | html_show_sphinx = False
95 |
96 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
97 | html_show_copyright = True
98 |
99 | # Output file base name for HTML help builder.
100 | htmlhelp_basename = 'beholddoc'
101 |
102 |
103 | ## -- Options for LaTeX output ---------------------------------------------
104 | #
105 | #latex_elements = {
106 | # #The paper size ('letterpaper' or 'a4paper').
107 | #'papersize': 'letterpaper',
108 | #
109 | # #The font size ('10pt', '11pt' or '12pt').
110 | #'pointsize': '10pt',
111 | #
112 | # #Additional stuff for the LaTeX preamble.
113 | #'preamble': '',
114 | #}
115 | #
116 | ## Grouping the document tree into LaTeX files. List of tuples
117 | ## (source start file, target name, title,
118 | ## author, documentclass [howto, manual, or own class]).
119 | #latex_documents = [
120 | # ('index', 'behold.tex', 'behold Documentation',
121 | # 'Rob deCarvalho', 'manual'),
122 | #]
123 | #
124 | ## -- Options for manual page output ---------------------------------------
125 | #
126 | ## One entry per manual page. List of tuples
127 | ## (source start file, name, description, authors, manual section).
128 | #man_pages = [
129 | # ('index', 'behold', 'behold Documentation',
130 | # ['Rob deCarvalho'], 1)
131 | #]
132 | #
133 | ## -- Options for Texinfo output -------------------------------------------
134 | #
135 | ## Grouping the document tree into Texinfo files. List of tuples
136 | ## (source start file, target name, title, author,
137 | ## dir menu entry, description, category)
138 | #texinfo_documents = [
139 | # ('index', 'behold', 'behold Documentation',
140 | # 'Rob deCarvalho', 'behold', 'A short description',
141 | # 'Miscellaneous'),
142 | #]
143 |
--------------------------------------------------------------------------------
/docs/index.rst:
--------------------------------------------------------------------------------
1 | Behold: Python debugging for large projects
2 | ===========================================
3 |
4 | Behold is a package that let's you perform contextual debugging. You can use the
5 | state inside one module to either trigger a step debugger or trigger print
6 | statements in a completely different module. Given the stateful nature of many
7 | large, multi-file applications (I'm looking at you, Django), this capability
8 | provides valuable control over your debugging work flow.
9 |
10 | Behold is written in pure Python with no dependencies. It is compatible with
11 | both Python2 and Python3.
12 |
13 | See the
14 | `Github project page `_.
15 | for examples of how to use `behold`.
16 |
17 |
--------------------------------------------------------------------------------
/docs/ref/behold.rst:
--------------------------------------------------------------------------------
1 | .. _ref-behold:
2 |
3 |
4 | API Documentation
5 | ==================
6 | This is the API documentation for the `behold` package. To see examples
7 | of how to use `behold`, visit the
8 | `Github project page `_.
9 |
10 |
11 | Managing Context
12 | ----------------
13 | .. autoclass:: behold.logger.in_context
14 | .. autofunction:: behold.logger.set_context
15 | .. autofunction:: behold.logger.unset_context
16 | .. autofunction:: behold.logger.get_stash
17 | .. autofunction:: behold.logger.clear_stash
18 |
19 | Printing / Debugging
20 | --------------------
21 | .. autoclass:: behold.logger.Behold
22 |
23 | .. automethod:: behold.logger.Behold.show
24 | .. automethod:: behold.logger.Behold.when
25 | .. automethod:: behold.logger.Behold.when_values
26 | .. automethod:: behold.logger.Behold.when_context
27 | .. automethod:: behold.logger.Behold.view_context
28 | .. automethod:: behold.logger.Behold.stash
29 | .. automethod:: behold.logger.Behold.extract
30 |
31 |
32 | Items
33 | -----
34 | .. autoclass:: behold.logger.Item
35 | :members:
36 |
37 |
--------------------------------------------------------------------------------
/docs/toc.rst:
--------------------------------------------------------------------------------
1 | Table of Contents
2 | =================
3 |
4 | .. toctree::
5 | :maxdepth: 3
6 |
7 | index
8 | ref/behold
9 |
--------------------------------------------------------------------------------
/docs_requirements.txt:
--------------------------------------------------------------------------------
1 | Sphinx
2 | sphinx_rtd_theme
3 |
4 |
5 |
--------------------------------------------------------------------------------
/publish.py:
--------------------------------------------------------------------------------
1 | import subprocess
2 |
3 | subprocess.call('pip install wheel'.split())
4 | subprocess.call('python setup.py clean --all'.split())
5 | subprocess.call('python setup.py sdist'.split())
6 | # subprocess.call('pip wheel --no-index --no-deps --wheel-dir dist dist/*.tar.gz'.split())
7 | subprocess.call('python setup.py register sdist bdist_wheel upload'.split())
8 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [nosetests]
2 | nocapture=1
3 | verbosity=1
4 | with-coverage=1
5 | cover-branches=1
6 | #cover-min-percentage=100
7 | cover-package=behold
8 |
9 | [coverage:report]
10 | show_missing=True
11 | fail_under=100
12 | exclude_lines =
13 | # Have to re-enable the standard pragma
14 | pragma: no cover
15 |
16 | # Don't complain if tests don't hit defensive assertion code:
17 | raise NotImplementedError
18 |
19 | [coverage:run]
20 | omit =
21 | behold/version.py
22 | behold/__init__.py
23 |
24 |
25 | [flake8]
26 | max-line-length = 120
27 | exclude = docs,env,*.egg
28 | max-complexity = 10
29 | ignore = E402
30 |
31 | [build_sphinx]
32 | source-dir = docs/
33 | build-dir = docs/_build
34 | all_files = 1
35 |
36 | [upload_sphinx]
37 | upload-dir = docs/_build/html
38 |
39 | [bdist_wheel]
40 | universal = 1
41 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | # import multiprocessing to avoid this bug (http://bugs.python.org/issue15881#msg170215)
2 | import multiprocessing
3 | assert multiprocessing
4 | import re
5 | from setuptools import setup, find_packages
6 |
7 |
8 | def get_version():
9 | """
10 | Extracts the version number from the version.py file.
11 | """
12 | VERSION_FILE = 'behold/version.py'
13 | mo = re.search(r'^__version__ = [\'"]([^\'"]*)[\'"]', open(VERSION_FILE, 'rt').read(), re.M)
14 | if mo:
15 | return mo.group(1)
16 | else:
17 | raise RuntimeError('Unable to find version string in {0}.'.format(VERSION_FILE))
18 |
19 |
20 | install_requires = [
21 | ]
22 | tests_require = [
23 | 'coverage>=4.0',
24 | 'flake8>=2.2.0',
25 | 'nose>=1.3.0',
26 | 'coveralls',
27 | ]
28 | docs_require = [
29 | 'Sphinx>=1.2.2',
30 | 'sphinx_rtd_theme',
31 | ]
32 |
33 | extras_require = {
34 | 'test': tests_require,
35 | 'packaging': ['wheel'],
36 | 'docs': docs_require,
37 | 'dev': install_requires + tests_require + docs_require
38 | }
39 |
40 | everything = set(install_requires)
41 | for deps in extras_require.values():
42 | everything.update(deps)
43 | extras_require['all'] = list(everything)
44 |
45 | setup(
46 | name='behold',
47 | version=get_version(),
48 | description='',
49 | long_description=open('README.md').read(),
50 | url='https://github.com/robdmc/behold',
51 | author='Rob deCarvalho',
52 | author_email='not_listed@nothing.net',
53 | keywords='',
54 | packages=find_packages(),
55 | classifiers=[
56 | 'Programming Language :: Python :: 2.7',
57 | 'Programming Language :: Python :: 3.4',
58 | 'Programming Language :: Python :: 3.5',
59 | 'Intended Audience :: Developers',
60 | 'License :: OSI Approved :: MIT License',
61 | 'Operating System :: OS Independent',
62 | ],
63 | license='MIT',
64 | include_package_data=True,
65 | test_suite='nose.collector',
66 | install_requires=install_requires,
67 | tests_require=tests_require,
68 | extras_require=extras_require,
69 | zip_safe=False,
70 | )
71 |
--------------------------------------------------------------------------------