├── .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 | [![Build Status](https://travis-ci.org/robdmc/behold.svg?branch=develop)](https://travis-ci.org/robdmc/behold) 4 | [![Coverage Status](https://coveralls.io/repos/github/robdmc/behold/badge.svg?branch=develop)](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 | --------------------------------------------------------------------------------