Commits

Jonathan Eunice committed ea1a709

added show() for debug printing

  • Participants
  • Parent commits d188529

Comments (0)

Files changed (4)

  *  Indentation and wrapping (to help stucture output)
  *  Convenience printing functions for horizontal rules (lines), titles, and
     vertical whitespace.
+ *  A parallel function, ``show()``,  that displays the current value of
+    variables.
 
 Usage
 =====
 
 ::
 
-    from say import say, fmt
+    from say import say, fmt, show
     
     x = 12
     nums = list(range(4))
     
     say("There are {x} things.")
     say("Nums has {len(nums)} items: {nums}")
-
+    
 yields::
 
     There are 12 things.
-    Nums has 4 items: [1, 2, 3, 4]
-    
+    Nums has 4 items: [0, 1, 2, 3]
+
 ``say`` is basically a simpler, nicer recasting of::
     
     print "There are {} things.".format(x)
 invocation, the more valuable having it stated in-line becomes. Note that full
 expressions are are supported. They are evaluated in the context of the caller.
 
+There is also a built-in facility, ``show()``, for outputing the current state of
+variables. It is designed for "debugging print statements" that don't require the
+craptastic repetition of ``print "x = {x}".format(x)``. Instead, just::
+
+    show(x)
+    show(nums)
+    show(x, nums, len(nums))
+    
+yields:
+
+    x: 12
+    nums: [0, 1, 2, 3]
+    x: 12  nums: [0, 1, 2, 3]  len(nums): 4
+
+All of the standard keyword options for ``say()`` work for ``show()`` as well.
+It's a very quick way of adding in debug printing statements.
+
 Printing Where You Like
 =======================
 
     from codecs import getencoder
 from textwrap import fill, dedent
 
+from linecache import getline
+import os, re
+
 
 ### Convenience and compatibility functions and names
 
 
     def __call__(self, *args, **kwargs): 
         
-        opts = self.options.push(kwargs)  
-        opts._callframe = inspect.currentframe().f_back
+        opts = self.options.push(kwargs)
+        if not opts._callframe:
+            opts._callframe = inspect.currentframe().f_back
         
         formatted = [ _sprintf(arg, opts._callframe) if is_string(arg) else str(arg)
                       for arg in args ]
 
 fmt = say.clone(encoding=None, retvalue=True, silent=True)
 fmt.setfiles([])
+
+
+
+def wrapped_if(value, prefix="", suffix="", transform=None):
+    """
+    If a string has a value, then transform it (optinally) and add the prefix and
+    suffix. Else, return empty string. Handy for formatting operations, where
+    one often wants to add decoration iff the value exists.
+    """
+    
+    if not value:
+        return ""
+    s = transform(str(value)) if transform else str(value)
+    return (prefix or "") + s + (suffix or "")
+
+QUOTE_CHARS = ('"', "'", '"""')
+
+def is_kwarg(s):
+    """
+    Rough test as to whether s is a kwarg to show()
+    """
+    return not s.startswith(QUOTE_CHARS) and '=' in s
+
+class DebugPrinter(object):
+    """DebugPrinter objects print debug output in a 'name: value' format that
+    is convenient for discovering what's going on as a program runs."""
+    
+    options = Options(
+        show_module=False,  # Show the module name in the call location
+        where=True,         # show the call location of each dprint() (None => not explicitly)
+        sep="  ",           # separate items with two spaces, by default
+    )
+
+    def __init__(self, **kwargs):
+        self.options = DebugPrinter.options.push(kwargs)
+        self.say = Say(retval=True)
+        self.opts = None  # per call options, set on each call to reflect transient state
+
+    def caller_arg_names(self, srcline):
+        """
+        Get the variable names the display function was called with.
+        """
+        
+        # Get the contents in show(CONTENTS)
+        contents = re.sub('^[^(]*\(', '', srcline)
+        contents = re.sub('\)[^)]*$', '', contents)
+        # really need to fix this to match out balanced parens, at least
+        
+        # While this successfully parses 95% of what you might typically want,
+        # it's insufficient for show(CONTENTS) where CONTENTS contains commas
+        # that don't separate the arguments. show, while called like a
+        # function, must be on its own line as well (as though it were a statement)
+
+        # Get the names of any non-keyword args. May be an easier and/or more
+        # systematic and reliable way to do this via inspect or bytecode inspection
+        return [ a.strip() for a in contents.split(',') if a and not is_kwarg(a) ]
+    
+    def call_location(self, caller):
+        """
+        Create a call location string indicating where a show() was called.
+        """
+        
+        module_name = ""
+        if self.opts.show_module:
+            filepath = caller.f_locals.get('__file__', caller.f_globals.get('__file__', 'UNKNOWN'))
+            filename = os.path.basename(filepath)
+            module_name = re.sub(r'.py', '', filename)
+            
+        lineno = caller.f_lineno
+        co_name = caller.f_code.co_name
+        if co_name == '<module>':
+            co_name = '__main__'
+        func_location = wrapped_if(module_name, ":") + wrapped_if(co_name, "", "()")
+        return "{}:{}".format(func_location, lineno)
+        
+    
+    def arg_format(self, name, value, caller):
+        """Format a single argument. Strings returned as-is."""
+        if name.startswith(QUOTE_CHARS):
+            ret = fmt(value, **{'_callframe': caller})
+            return ret
+        else:
+            return "{0}: {1!r}".format(name, value)
+
+    def get_arg_tuples(self, caller, *args):
+        """
+        Get arg name, value tuples uses to format.
+        """
+        
+        srcline = getline(caller.f_code.co_filename, caller.f_lineno)
+        arg_names  = self.caller_arg_names(srcline)
+        return zip(arg_names, list(args))
+        
+    def __call__(self, *args, **kwargs):
+        """Main entry point for DebugPrinter."""
+
+        opts = self.opts = self.options.push(kwargs)
+        
+        # Determine argument names and values
+        caller = inspect.currentframe().f_back
+        argtuples = self.get_arg_tuples(caller, *args)
+        
+        valstr = opts.sep.join([ self.arg_format(name, value, caller) for name, value in argtuples ])
+        locval = [ self.call_location(caller) + ":  ", valstr ] if opts.where else [ valstr ]
+
+        return self.say(*locval, **kwargs)
+
+show = DebugPrinter()
 
 setup(
     name='say',
-    version=verno("0.512"),
+    version=verno("0.6"),
     author='Jonathan Eunice',
     author_email='jonathan.eunice@gmail.com',
-    description='Simple printing with templates. E.g.: say("Hello, {whoever}!", indent=1)',
+    description='Super-simple templated printing. E.g.: say("Hello, {whoever}!", indent=1) or show(x)',
     long_description=open('README.rst').read(),
     url='https://bitbucket.org/jeunice/say',
     py_modules=['say'],
     install_requires=['six','options>=0.325', 'stuf>=0.9.10'],
-    tests_require = ['tox', 'pytest'],
+    tests_require = ['tox', 'pytest', 'six'],
     zip_safe = True,
-    keywords='print format template interpolate say',
+    keywords='print format template interpolate say show',
     classifiers=linelist("""
         Development Status :: 4 - Beta
         Operating System :: OS Independent
         Programming Language :: Python
         Programming Language :: Python :: 2.6
         Programming Language :: Python :: 2.7
+        Programming Language :: Python :: 3
         Programming Language :: Python :: 3.2
         Programming Language :: Python :: 3.3
         Programming Language :: Python :: Implementation :: CPython
         Programming Language :: Python :: Implementation :: PyPy
         Topic :: Software Development :: Libraries :: Python Modules
+        Topic :: Printing
     """)
 )

File test/test_show.py

+
+import pytest
+from say import Say, fmt, show, DebugPrinter
+
+say = Say(retvalue=True)
+show = DebugPrinter(where=False)
+show.say = say
+
+def test_basic(param = 'Yo'):
+
+    a = 1
+    b = 3.141
+    c = "something else!"
+    
+    assert show(a) == 'a: 1'
+    assert show(a, b) == 'a: 1  b: 3.141'
+    assert show(a, b, c) == "a: 1  b: 3.141  c: 'something else!'"
+    assert show(1+1) == '1+1: 2'
+    assert show(len(c), c) == \
+                "len(c): 15  c: 'something else!'"
+    
+            # anything with paraens in it must be on separate line, given weak parser
+            # of show parameters
+            
+def test_say_params():
+    a = 1
+    b = 3.141
+    c = "something else!"
+    
+    assert show(a, indent='+1') == '    a: 1'
+    assert show(a, b, sep='\n') == 'a: 1\nb: 3.141'
+
+@pytest.mark.skipif('True')
+def test_strings():
+    assert show('this') == "this"
+    
+    # known bug
+    
+def test_strings2():
+    x = 44
+    assert show("x = {x}") == 'x = 44'
+    
+def test_show_example():
+    
+    x = 12
+    nums = list(range(4))
+
+    assert show(x) == 'x: 12'
+    assert show(nums) == 'nums: [0, 1, 2, 3]'
+    assert show(x, nums, len(nums)) == \
+        'x: 12  nums: [0, 1, 2, 3]  len(nums): 4'
+    assert show(x, nums, len(nums), indent='+1') == \
+        '    x: 12  nums: [0, 1, 2, 3]  len(nums): 4'
+    assert show(x, nums, len(nums), sep='\n') == \
+        'x: 12\nnums: [0, 1, 2, 3]\nlen(nums): 4'
+    assert show(x, nums, len(nums), sep='\n', indent=1) == \
+        '    x: 12\n    nums: [0, 1, 2, 3]\n    len(nums): 4'