Commits

Jonathan Eunice committed 6d406be

initial commit

  • Participants

Comments (0)

Files changed (10)

+syntax: glob
+*.swp.{py,txt,html,css,js}
+*.pyc
+.DS_Store
+build/*
+dist/*
+*.egg-info
+setup.cfg
+PKG-INFO
+.tox
+Simple, effective debug printing. 
+
+Usage
+=====
+
+::
+
+    from show import show
+    
+    x = 12
+    nums = list(range(4))
+    
+    show(x, nums)
+    
+yields::
+
+    x: 12  nums: [0, 1, 2, 3]
+
+Debug Printing
+==============
+
+Sometimes programs print so that users can see things, and sometimes they print
+so that develpopers can. ``show()`` is for developers, helping
+rapidly print the current state of variables. It replaces require the craptastic
+repetitiveness of::
+
+    print "x: {0}".format(x)
+    
+with::
+
+    show(x)
+
+If you'd like to see where the data is being produced,::
+
+    show.set(where=True)
+    
+will turn on location reporting.
+
+``show.props(x)`` shows the properties of object x. An optional second
+parameter can determine which properties are shown. E.g.::
+
+    show.props(x, 'name,age')
+    
+Or if you prefer the keyword syntax, this is equivalent to::
+
+    show(x, props='name,age')
+    
+Interactive Limitations
+=======================
+
+Sadly, because Python provides weaker introspection during
+interactive operation, (see e.g. `this <http://stackoverflow.com/questions/13204161/how-to-access-the-calling-source-line-from-interactive-shell>`_)
+``show()`` is somewhat limited in interactive use. It works at the main
+level and usually in imported modules, but does work within the body of functions defined
+interactively.
+
+Notes
+=====
+
+ *  ``show`` is the companion to, and built on the output management of,
+    `say <http://pypi.python.org/pypi/say>`_.
+   
+ *  Automated multi-version testing is managed with the wonderful
+    `pytest <http://pypi.python.org/pypi/pytest>`_
+    and `tox <http://pypi.python.org/pypi/tox>`_ modules. ``show`` is 
+    successfully packaged for, and tested against, all late-model verions of
+    Python: 2.6, 2.7, 3.2, and 3.3, as well as PyPy 1.9 (based on 2.7.2).
+ 
+ *  ``show`` has 
+    draft support for interactive Python and iPython. Works well at the
+    interactive prompt, and within imported modules; it cannot, however, be used
+    within interactively defined functions and classes. There may be a hard limit
+    on Python introspection of interactive code. Or it could be something
+    that can be worked around over time.
+ 
+ *  The author, `Jonathan Eunice <mailto:jonathan.eunice@gmail.com>`_ or
+    `@jeunice on Twitter <http://twitter.com/jeunice>`_
+    welcomes your comments and suggestions.
+
+Installation
+============
+
+::
+
+    pip install show
+
+To ``easy_install`` under a specific Python version (3.3 in this example)::
+
+    python3.3 -m easy_install show
+    
+(You may need to prefix these with "sudo " to authorize installation.)
+# Test stuff for interactive use
+
+# Not a real part of the module.  More a snippet
+# for exploring the behavior of things under
+# interactive use
+
+
+from say import show
+
+def f(x):
+    show(x)
+    return 4
+
+isInteractive = hasattr(sys, 'ps1') or hasattr(sys, 'ipcompleter')
+# other methods of determining interactivity mentioned at:
+# http://stackoverflow.com/questions/967369/python-find-out-if-running-in-shell-or-not-e-g-sun-grid-engine-queue
+
+def call():
+    print "__name__", __name__
+    print "__package__", __package__
+    caller = inspect.currentframe().f_back
+    print caller
+    print dir(caller)
+    print "lineno:", caller.f_lineno
+    print "f_code", caller.f_code
+    print "f_code.co_filename", caller.f_code.co_filename
+    if caller.f_code.co_filename == '<stdin>':
+        print history.line(-1)
+
+[pytest]
+python_files = test/*.py
+#! /usr/bin/env python
+from setuptools import setup
+import sys
+
+def verno(s):
+    """
+    Update the version number passed in by extending it to the 
+    thousands place and adding 1/1000, then returning that result
+    and as a side-effect updating setup.py
+
+    Dangerous, self-modifying, and also, helps keep version numbers
+    ascending without human intervention.
+    """
+    
+    from decimal import Decimal
+    import re
+    d = Decimal(s)
+    increment = Decimal('0.001')
+    d = d.quantize(increment) + increment
+    dstr = str(d)
+    setup = open('setup.py', 'r').read()
+    setup = re.sub('verno\(\w*[\'"]([\d\.]+)[\'"]', 'verno("' + dstr + '"', setup)
+    open('setup.py', 'w').write(setup)
+    return dstr
+
+def linelist(text):
+    """
+    Returns each non-blank line in text enclosed in a list.
+    """
+    return [ l.strip() for l in text.strip().splitlines() if l.split() ]
+    
+    # The double-mention of l.strip() is yet another fine example of why
+    # Python needs en passant aliasing.
+
+setup(
+    name='show',
+    version=verno("0.414"),
+    author='Jonathan Eunice',
+    author_email='jonathan.eunice@gmail.com',
+    description='Debug print statements, done right. E.g. show(x)',
+    long_description=open('README.rst').read(),
+    url='https://bitbucket.org/jeunice/show',
+    packages=['show'],
+    install_requires=['six', 'options>=0.4', 'stuf>=0.9.10', 'mementos>=0.5', 'codegen>=1.0', 'say>0.8'],
+    tests_require = ['tox', 'pytest', 'six'],
+    zip_safe = True,
+    keywords='debug print display show',
+    classifiers=linelist("""
+        Development Status :: 4 - Beta
+        Operating System :: OS Independent
+        License :: OSI Approved :: BSD License
+        Intended Audience :: Developers
+        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
+        Topic :: Software Development :: Debuggers
+    """)
+)

File show/__init__.py

+from show.core import *
+from show.linecacher import *

File show/core.py

+"""Debugging print features. """
+
+import string, inspect, sys, os, re, collections
+from options import Options, OptionsContext
+from say import *
+from show.linecacher import *
+import linecache, ast, codegen
+from mementos import MementoMetaclass, with_metaclass
+import textwrap
+
+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 = ('"', "'", '"""')
+
+class CallArgs(with_metaclass(MementoMetaclass, ast.NodeVisitor)):
+    """
+    An ``ast.NodeVisitor`` that parses a Python function call and determines its
+    arguments from the corresponding AST. Memoized so that parsing and
+    traversing the AST is done once and only once; subseqnet requests are
+    delivered via cache lookup.
+    """
+
+    TARGET_FUNCS = set(['show', 'show.items', 'show.props'])  # functions we care about
+
+    def __init__(self, filepath, lineno):
+        ast.NodeVisitor.__init__(self)
+        self.filepath = filepath
+        self.lineno   = lineno
+        self.source   = None  # placeholder
+        self.ast      = None  # placeholder
+        self.args     = None  # placeholder
+        self.get_ast()
+        self.visit(self.ast)
+    
+    def get_ast(self):
+        """
+        Find the AST. Easy if single source line contains whole line. May
+        need a bit of trial-and-error if over multiple lines.
+        """
+        src = ""
+        for lastlineno in range(self.lineno, self.lineno+10):
+            src += getline(self.filepath, lastlineno)
+            try:
+                srcleft = textwrap.dedent(src)
+                self.ast = ast.parse(srcleft)
+                self.source = src
+                return
+            except IndentationError:
+                pass
+            except SyntaxError:
+                pass
+        raise RuntimeError('Failed to parse:\n{}\n'.format(src))
+        
+    def visit_Call(self, node):
+        """
+        Called for all ``ast.Call`` nodes. Collects source of each argument.
+        Note that AST ``starargs`` and ``keywords`` properties are not
+        considered. Because ``CallArgs`` parses just one line of source code out
+        of its module's context, ``ast.parse`` assumes that arguments are
+        normal, not ``*args``. And ``**kwargs`` we can ignore, because those are
+        pragmas, not data.
+        """
+        
+        def call_name(n):
+            """
+            Given an ast.Call node, return the name of the called function, if
+            discoverable. Only attempts to decode the common ``func()`` and
+            ``object.method()`` cases that we care about for the ``show``
+            module. Others return ``None``.
+            """
+            if isinstance(n.func, ast.Name):
+                return n.func.id
+            elif isinstance(n.func, ast.Attribute):
+                a = n.func
+                if isinstance(a.value, ast.Name):
+                    return '.'.join([a.value.id, a.attr])
+                else:
+                    return None # could be an attribute of a call, but for those, we don't much care
+            else:
+                raise ValueError("Uh...I'm confused!")
+        
+        name = call_name(node)
+        if name in self.TARGET_FUNCS:
+            self.args = [ codegen.to_source(arg) for arg in node.args ]
+        else:
+            # visit its children
+            ast.NodeVisitor.generic_visit(self, node)
+
+# probably cannot make this work from interactive Python
+# http://stackoverflow.com/questions/13204161/how-to-access-the-calling-source-line-from-interactive-shell
+
+class Show(object):
+    """Show 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=False,        # show the call location of each call
+        sep="  ",           # separate items with two spaces, by default
+        retvalue=False,     # return the value printed?
+        props=None,         # placeholder for show.props
+    )
+
+    def __init__(self, **kwargs):
+        self.options = Show.options.push(kwargs)
+        self.say = Say(retvalue=self.options.retvalue)
+        self.opts = None  # per call options, set on each call to reflect transient state
+    
+    def call_location(self, caller):
+        """
+        Create a call location string indicating where a show() was called.
+        """
+        if isInteractive:
+            return "<stdin>:{0}".format(len(history.lines))
+        else:
+            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 ':'.join([func_location, str(lineno)])
+    
+    @staticmethod
+    def value_repr(value):
+        """
+        Return a ``repr()`` string for value that has any brace characters (e.g.
+        for ``dict``--and in Python 3, ``set`--literals) doubled so that they
+        are not interpreted as format template characters when the composed string
+        is eventually output by ``say``.
+        """
+        fvalue = repr(value)
+        if isinstance(value, (dict, set)):
+            fvalue = fvalue.replace('{', '{{').replace('}', '}}') # escape { and }
+        return fvalue
+
+    def arg_format(self, name, value, caller):
+        """
+        Format a single argument. Strings returned formatted.
+        """
+        if name.startswith(QUOTE_CHARS):
+            return fmt(value, **{'_callframe': caller})
+        else:
+            return ': '.join( [ name, self.value_repr(value) ] )
+
+    def arg_format_items(self, name, value, caller):
+        """
+        Format a single argument to show items of a collection.
+        """
+        if name.startswith(QUOTE_CHARS):
+            ret = fmt(value, **{'_callframe': caller})
+            return ret
+        else:                
+            if isinstance(value, (list, dict, set, six.string_types)):  # weak test
+                length = len(value)
+                itemname = 'char' if isinstance(value, six.string_types) else 'item'
+                s_or_nothing = '' if length == 1 else 's'
+                fvalue = self.value_repr(value)
+                return "{0} ({1} {2}{3}): {4}".format(name, length, itemname, s_or_nothing, fvalue)
+            else:
+                return "{0}: {1!r}".format(name, value)
+
+    def arg_format_props(self, name, value, caller):
+        """
+        Format a single argument to show properties.
+        """
+        if name.startswith(QUOTE_CHARS):
+            ret = fmt(value, **{'_callframe': caller})
+            return ret
+        else:
+            try:
+                props = self.opts.props
+                if props:
+                    proplist = props.split(',')
+                else:
+                    proplist = sorted([ k for k in value.__dict__.keys() if not k.startswith('_') ])
+                propvals = [ "{0}={1}".format(p, self.value_repr(getattr(value, p))) for p in proplist ]
+                return "{0}: {1}".format(name, ' '.join(propvals))
+            except Exception:
+                return "{0}: {1}".format(name, self.value_repr(value))
+       
+    def get_arg_tuples(self, caller, values):
+        """
+        Return a list of argument name, value tuples with the given values.
+        """
+        filename, lineno = frame_to_source_info(caller)
+        argnames = CallArgs(filename, lineno).args
+        argtuples = list(zip(list(argnames), list(values)))
+        return argtuples
+    
+    def settings(self, **kwargs):
+        """
+        Open a context manager for a `with` statement. Temporarily change settings
+        for the duration of the with.
+        """
+        return ShowContext(self, kwargs)
+    
+    def set(self, **kwargs):
+        """
+        Open a context manager for a `with` statement. Temporarily change settings
+        for the duration of the with.
+        """
+        self.options.set(**kwargs)
+        if kwargs:
+            self.say.set(**kwargs)
+    
+    def clone(self, **kwargs):
+        """
+        Create a new Say instance whose options are chained to this instance's
+        options (and thence to Say.options). kwargs become the cloned instance's
+        overlay options.
+        """
+        cloned = Show()
+        cloned.options = self.options.push(kwargs)
+        return cloned
+    
+    def _showcore(self, args, kwargs, caller, formatter, opts):
+        """
+        Do core work of showing the args.
+        """
+        self.opts = opts
+        argtuples = self.get_arg_tuples(caller, args)
+        
+        # Construct the result string
+        valstr = opts.sep.join([ formatter(name, value, caller) for name, value in argtuples ])
+        locval = [ self.call_location(caller) + ":  ", valstr ] if opts.where else [ valstr ]
+
+        # Emit the result string, and optionally return it
+        retval = self.say(*locval, **kwargs)
+        if opts.retvalue:
+            return retval
+        
+    def __call__(self, *args, **kwargs):
+        """
+        Main entry point for Show objects.
+        """
+        opts = self.options.push(kwargs)
+        caller = inspect.currentframe().f_back
+        formatter = self.arg_format if not opts.props else self.arg_format_props
+        return self._showcore(args, kwargs, caller, formatter, opts)
+        
+    def items(self, *args, **kwargs):
+        """
+        Show items of a collection.
+        """
+        opts = self.options.push(kwargs)
+        caller = inspect.currentframe().f_back
+        return self._showcore(args, kwargs, caller, self.arg_format_items, opts)
+        
+    def props(self, *args, **kwargs):
+        """
+        Show properties of objects.
+        """
+        opts = self.options.push(kwargs)
+        if len(args) > 1 and isinstance(args[-1], str):
+            used = opts.addflat([ args[-1] ], ['props'])
+            args = args[:-1]        
+        caller = inspect.currentframe().f_back
+        return self._showcore(args, kwargs, caller, self.arg_format_props, opts)
+    
+class ShowContext(OptionsContext):
+    """
+    Context helper to support Python's with statement.  Generally called
+    from ``with show.settings(...):``
+    """
+    pass
+
+show = Show()

File show/linecacher.py

+"""Like linecache, but with support for getting lines from
+interactive Python and iPython use, too."""
+
+import sys, linecache
+
+isInteractive = hasattr(sys, 'ps1') or hasattr(sys, 'ipcompleter')
+# http://stackoverflow.com/questions/967369/python-find-out-if-running-in-shell-or-not-e-g-sun-grid-engine-queue
+
+if isInteractive:
+    import readline as rl
+    
+    class History(object):
+        """
+        Singleton proxy for readline
+        """
+        
+        def __init__(self):
+            self._lines  = [ None ] # first item is None to compensate: 0-based arrays
+                                    # but 1-based line numbers
+            self._lines.append(rl.get_history_item(rl.get_current_history_length()))
+            rl.clear_history()
+            self._lastseen = rl.get_current_history_length()  # have we seen it all?
+            
+        @property
+        def lines(self):
+            """
+            The magically self-updating lines property.
+            """
+            self._update()
+            return self._lines
+            
+        def _update(self):
+            """
+            If the lines have not been recently updated (readlines knows more lines than
+            we do), import those lines.
+            """
+            cur_hist_len = rl.get_current_history_length()
+            if cur_hist_len > self._lastseen:
+                for i in range(self._lastseen+1, cur_hist_len+1):
+                    self._lines.extend(rl.get_history_item(i).splitlines())
+                self._lastseen = cur_hist_len
+                
+            # Fancier splitlines() thing required because iPython stores history
+            # lines for multi-line strings with embedded newlines. Iteractive Python
+            # stores them individually.
+        
+        def prev(self, offset=0):
+            """
+            Show the previous line. Or can go back a few, with offset
+            """
+            return self.lines[-2-abs(offset)]
+        
+        def clear(self):
+            """
+            Obliviate! Clear the history.
+            """
+            rl.clear_history()
+            self._lines  = [ None ] # first item is None to compensate: 0-based arrays
+                                    # but 1-based line numbers
+            self._lines.append(rl.get_history_item(rl.get_current_history_length()))
+        
+        #def show(self):
+        #    """
+        #    Show last items.
+        #    """
+        #    for lineno, line in enumerate(self.lines[1:], start=1):
+        #        print "{0:3}: {1}".format(lineno, line)
+                
+    history = History()
+    
+    def frame_to_source_info(frame):
+        """
+        Given a Python call frame, e.g. from ``inspect.currentframe()`` or any of
+        its ``f_back`` predecessors, return the best filename and lineno combination.
+        """
+        filename = frame.f_code.co_filename
+        lineno   = frame.f_lineno
+        # print "lineno", lineno
+        if filename.startswith( ('<stdin>', '<ipython-input') ):
+            if lineno == 1:
+                lineno = len(history.lines)
+            return ('<stdin>', lineno)
+        return (filename, lineno)
+
+    def getline(filename, lineno):
+        """
+        Replace ``linecache.getline()`` with function that first determines if
+        we need to get from history, or from a regular file.
+        """
+        # print "getline: filename = ", filename, "lineno = ", lineno
+        if filename == '<stdin>':
+            index = -1 if lineno == 1 else lineno - 1
+            # for interactive Python, lineno == 1 a lot
+            return history.lines[index]
+        else:
+            return linecache.getline(filename, lineno)
+        
+else:
+    history = None
+    getline = linecache.getline
+    
+    def frame_to_source_info(frame):
+        """
+        Given a Python call frame, e.g. from ``inspect.currentframe()`` or any of
+        its ``f_back`` predecessors, return the best filename and lineno combination.
+        If not running interactively, just use the Python data structures.
+        """
+        return (frame.f_code.co_filename, frame.f_lineno)

File test/test_show.py

+
+import pytest
+from show import Show
+import sys
+
+PY3 = sys.version_info[0] == 3
+
+show = Show(where=False, retvalue=True)
+
+def nospaces(s):
+    return s.replace(' ', '')
+    
+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 nospaces(show(1+1)) == nospaces('1 + 1: 2')
+        # may have extra spaces in it, based on codegen.to_source() output
+        
+    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_literals():
+    assert show(1 + 1) == '1 + 1: 2'
+    assert show(1+1) == '1 + 1: 2'
+    
+    # assert nospaces(show(1+1)) == nospaces('1 + 1: 2')
+    
+    # NB output may have more or fewer spaces than actual parameter, based on codegen.to_source() 
+    # creating its 'idealized' code output
+
+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'
+
+def test_strings():
+    assert show('this') == "this"
+    x = 44
+    assert show("x = {x}") == 'x = 44'
+    
+def test_set():
+    
+    s = set([1,2,99])
+    
+    # native literals for sets different in py2 and py3
+    if PY3:
+        assert show(s) == "s: {1, 2, 99}"
+    else:
+        assert show(s) == "s: set([1, 2, 99])"
+    
+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'
+
+def test_show_items():
+    nums = list(range(4))
+    assert show.items(nums) == \
+        'nums (4 items): [0, 1, 2, 3]'
+    
+    astring = 'this'
+    assert show.items(astring) == \
+        "astring (4 chars): 'this'"
+    
+    d = {'a': 1, 'b': 2}
+    fstr =  show.items(d)
+    assert fstr == "d (2 items): {'a': 1, 'b': 2}" or fstr == "d (2 items): {'b': 2, 'a': 1}" 
+    
+def test_show_props():
+    
+    class O(object):
+        name    = None
+        age     = None
+        address = None
+    o = O()
+    o.name = 'An Object'
+    o.age  = 2
+    o.address = 'RAM'
+    assert show.props(o) == "o: address='RAM' age=2 name='An Object'"
+    assert show.props(o, 'name,age,address') == "o: name='An Object' age=2 address='RAM'"
+    assert show.props(o, 'name,age') == "o: name='An Object' age=2"
+    
+    assert show.props(o, 'name,age') == show(o, props='name,age')
+        
+    # Other, not (yet, at any rate) API options
+    
+    # v0 = show.props(o)
+    # assert show(props(o)) == v0
+    
+    # v1 = show.props(o, 'name,age')
+    # assert show(props(o, 'name,age')) == v1
+    
+def test_show_props2():
+    
+    class OO(object):
+        def __init__(self, name, age, address):
+            self.name = name
+            self.age  = age
+            self.address = address
+        def test(self):
+            return self.age > 17
+        def best(self):
+            return self.name.capitalize()
+    
+    oo = OO('Joe', 36, 'Waverly Place')
+
+    assert show.props(oo) == "oo: address='Waverly Place' age=36 name='Joe'"
+    
+[tox]
+envlist = py26, py27, py32, py33, pypy
+
+[testenv]
+changedir=test
+deps=
+    pytest 
+commands=py.test {posargs}