Commits

Jonathan Eunice committed c354679

repackaged as Python package; successfully restested

  • Participants
  • Parent commits 8b1a283

Comments (0)

Files changed (4)

say.py

-"""Interpolating string formatter. """
-
-import string, inspect, sys, itertools
-from stuf import chainstuf  
-from options import Options, OptionsContext
-import six
-if six.PY3:
-    from codecs import getencoder
-from textwrap import fill, dedent
-
-from linecache import getline
-import os, re
-
-
-### Convenience and compatibility functions and names
-
-stdout = sys.stdout
-stderr = sys.stderr
-
-def is_string(v):
-    """
-    Is the value v a string? Useful especially in making a test that works on
-    both Python 2.x and Python 3.x
-    """
-    return isinstance(v, six.string_types)
-
-def encoded(u, encoding):
-    """
-    Encode string u (denoting it is expected to be in Unicode) if there's
-    encoding to be done. Tries to mask the difference between Python 2 and 3,
-    which have highly different models of string processing, and different
-    codec APIs and quirks. Some Python 3 encoders further require ``bytes``
-    in, not ``str``. These are first encoded into utf-8, encoded, then
-    decoded.
-    """
-    if not encoding:
-        return u
-    elif six.PY3:
-        try:
-            return getencoder(encoding)(u)[0]
-        except LookupError:
-            name = encoding.replace('-','')  # base-64 becomes base64 e.g.
-            bytesout = getencoder(name + '_codec')(u.encode('utf-8'))[0]
-            if name in set(['base64', 'hex', 'quopri', 'uu']):
-                return bytesout.decode('utf-8')
-            else:
-                return bytesout
-            
-        # NB PY3 requires lower-level interface for many codecs. s.encode('utf-8')
-        # works fine, but others do not. Some codecs convert bytes to bytes,
-        # and are not properly looked up by nickname (e.g. 'base64'). These are
-        # managed by first encoding into utf-8, then if it makes sense decoding
-        # back into a string. The others are things like bz2 and zlib--binary
-        # encodings that have little use to us here.
-        
-        # There are also be some slight variations in results that will make
-        # testing more fun. Possibly related to adding or not adding a terminal
-        # newline.
-    else:
-        return u.encode(encoding)
-        
-def flatten(*args):
-    """
-    Like itertools.chain(), but will pretend that single scalar values are singleton lists.
-    Convenient for iterating over values whether they're lists or singletons.
-    """
-    flattened = [ x if isinstance(x, (list, tuple)) else [x] for x in args ]
-    return itertools.chain(*flattened)
-
-    # would use ``hasattr(x, '__iter__')`` rather than ``isinstance(x, (list, tuple))``,
-    # but other objects like file have ``__iter__``, which screws things up
-
-
-### Workhorse functions
-
-sformatter = string.Formatter()  # piggyback Python's format() template parser
-
-def _sprintf(arg, caller, override=None):
-    """
-    Format the template string (arg) with the values seen in the context of the caller.
-    If override is defined, it is a dictionary like object mapping additional
-    values atop those of the local context.
-    """
-
-    def seval(s):
-        """
-        Evaluate the string s in the caller's context. Return its value.
-        """
-        try:
-            localvars = caller.f_locals if override is None \
-                                        else chainstuf(override, caller.f_locals)
-            return eval(s, caller.f_globals, localvars)
-        except SyntaxError:
-            raise SyntaxError("syntax error when formatting '{}'".format(s))
-
-
-    if is_string(arg):
-        parts = []
-        for (literal_text, field_name, format_spec, conversion) in sformatter.parse(arg):
-            parts.append(literal_text)
-            if field_name is not None:
-                format_str = six.u("{0") + ("!" + conversion if conversion else "") + \
-                                          (":" + format_spec if format_spec else "") + "}"
-                field_value = seval(field_name)
-                formatted = format_str.format(field_value)
-                parts.append(formatted)
-        return ''.join(parts)
-    else:
-        return str(seval(str(arg)))
-
-### Core Say class
-
-
-class Say(object):
-    """
-    Say encapsulates printing functions. Instances are configurable, and callable.
-    """
-    
-    options = Options(  
-        indent=0,           # indent level (if set to None, indentation is turned off)
-        indent_str='    ',  # indent string for each level
-        encoding=None if six.PY3 else 'utf-8',
-                            # character encoding for output (needed on Python 2, not 3)
-        encoded=None,       # character encoding to return
-        files= [stdout],    # where is output headed? a list of write() able objects
-                            # NB Set to [stdout] but in way that gets around stuf bug
-        wrap=None,          # column to wrap text to, if any
-        sep=' ',            # separate args with this (Python print function compatible)
-        end='\n',           # end output with this (Python print function compatible)
-        silent=False,       # do the formatting and return the result, but don't write the output
-        retvalue=False,     # should return the value of the formatted string?
-        _callframe=None,    # frome from which the caller was calling
-    )
-    
-    options.magic(
-        indent = lambda v, cur: cur.indent + int(v) if isinstance(v, str) else v
-    )
-    
-  
-    def __init__(self, **kwargs):
-        """
-        Make a say object with the given options.
-        """
-        self.options = Say.options.push(kwargs)
-    
-    def hr(self, sep=six.u('\u2500'), width=40, vsep=0, **kwargs):
-        """
-        Print a horizontal line. Like the HTML hr tag. Optionally
-        specify the width, character repeated to make the line, and vertical separation.
-        
-        Good options for the separator may be '-', '=', or parts of the Unicode 
-        box drawing character set. http://en.wikipedia.org/wiki/Box-drawing_character
-        """
-        opts = self.options.push(kwargs)
-
-        self.blank_lines(vsep, **opts)
-        self._output([sep * width], opts)
-        self.blank_lines(vsep, **opts)
-
-    def title(self, name, sep=six.u('\u2500'), width=15, vsep=0, **kwargs):
-        """
-        Print a horizontal line with an embedded title. 
-        """
-        opts = self.options.push(kwargs)
-
-        self.blank_lines(vsep, **opts)
-        line = sep * width
-        self._output([ ' '.join([line, name, line]) ], opts)
-        self.blank_lines(vsep, **opts)
-    
-    def blank_lines(self, n, **kwargs):
-        """
-        Output N blank lines ("vertical separation")
-        """
-        if n > 0:
-            opts = self.options.push(kwargs)
-            self._write("\n" * n, opts)
-
-    def set(self, **kwargs):
-        """
-        Permanently change the reciver's settings to those defined in the kwargs.
-        An update-like function.
-        """
-        self.options.set(**kwargs)
-        
-    def setfiles(self, files):
-        """
-        Set the list of output files. ``files`` is a list. For each item, if
-        it's a real file like ``sys.stdout``, use it. If it's a string, assume
-        it's a filename and open it for writing. 
-        """
-        def opened(f):
-            """
-            If f is a string, consider it a file name and return it, ready for writing.
-            Else, assume it's an open file. Just return it.
-            """
-            return open(f, "w") if is_string(f) else f
-            
-        self.options.files = [ opened(f) for f in files ]
-        return self
-    
-        # TBD: Turn this into 'magical' attribute set
-    
-    def settings(self, **kwargs):
-        """
-        Open a context manager for a `with` statement. Temporarily change settings
-        for the duration of the with.
-        """
-        return SayContext(self, 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 = Say()
-        cloned.options = self.options.push(kwargs)
-        return cloned
-
-    def __call__(self, *args, **kwargs): 
-        
-        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 ]
-        return self._output(opts.sep.join(formatted), opts)
-        
-    def _output(self, data, opts):
-        """
-        Do the actual formatting and outputting work. ``data`` may be either a
-        list of lines, or a composed string.
-        """
-        
-        if opts.indent or opts.wrap:
-            indent_str = opts.indent_str * opts.indent
-            if opts.wrap:
-                datastr = '\n'.join(data) if isinstance(data, list) else data
-                outstr = fill(datastr, width=opts.wrap, initial_indent=indent_str, subsequent_indent=indent_str)
-            else:
-                datalines = data if isinstance(data, list) else data.splitlines()
-                outstr = '\n'.join([ ''.join([ indent_str, line ]) for line in datalines ])
-        else:
-            outstr = '\n'.join(data) if isinstance(data, list) else data
-        # by end of indenting, dealing with string only
-
-        # prepare and emit output
-        if opts.end is not None:
-            outstr += opts.end
-        if not opts.silent:
-            to_write = encoded(outstr, opts.encoding)
-            self._write(to_write, opts)
-            
-        # prepare and return return value
-        if opts.retvalue:
-            retencoding = opts.encoding if opts.encoded is True else opts.encoded
-            retencoded = encoded(outstr, retencoding)
-            rettrimmed = retencoded[:-1] if retencoded.endswith('\n') else retencoded
-            return rettrimmed
-
-    def _write(self, s, opts):
-        """
-        Write s to all associated file objects. 
-        """
-        # print(opts)
-        for f in opts.files:
-            f.write(s)
-
-class SayContext(OptionsContext):
-    """
-    Context helper to support Python's with statement.  Generally called
-    from ``with say.settings(...):``
-    """
-    pass
-    
-
-### Define default ``say`` and ``fmt`` callables
-
-say = Say()
-
-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()
+"""Interpolating string formatter. """
+
+import string, inspect, sys, itertools, os, re
+from stuf import chainstuf  
+from options import Options, OptionsContext
+import six
+if six.PY3:
+    from codecs import getencoder
+from textwrap import fill, dedent
+
+### Convenience and compatibility functions and names
+
+stdout = sys.stdout
+stderr = sys.stderr
+
+def is_string(v):
+    """
+    Is the value v a string? Useful especially in making a test that works on
+    both Python 2.x and Python 3.x
+    """
+    return isinstance(v, six.string_types)
+
+def encoded(u, encoding):
+    """
+    Encode string u (denoting it is expected to be in Unicode) if there's
+    encoding to be done. Tries to mask the difference between Python 2 and 3,
+    which have highly different models of string processing, and different
+    codec APIs and quirks. Some Python 3 encoders further require ``bytes``
+    in, not ``str``. These are first encoded into utf-8, encoded, then
+    decoded.
+    """
+    if not encoding:
+        return u
+    elif six.PY3:
+        try:
+            return getencoder(encoding)(u)[0]
+        except LookupError:
+            name = encoding.replace('-','')  # base-64 becomes base64 e.g.
+            bytesout = getencoder(name + '_codec')(u.encode('utf-8'))[0]
+            if name in set(['base64', 'hex', 'quopri', 'uu']):
+                return bytesout.decode('utf-8')
+            else:
+                return bytesout
+            
+        # NB PY3 requires lower-level interface for many codecs. s.encode('utf-8')
+        # works fine, but others do not. Some codecs convert bytes to bytes,
+        # and are not properly looked up by nickname (e.g. 'base64'). These are
+        # managed by first encoding into utf-8, then if it makes sense decoding
+        # back into a string. The others are things like bz2 and zlib--binary
+        # encodings that have little use to us here.
+        
+        # There are also be some slight variations in results that will make
+        # testing more fun. Possibly related to adding or not adding a terminal
+        # newline.
+    else:
+        return u.encode(encoding)
+        
+def flatten(*args):
+    """
+    Like itertools.chain(), but will pretend that single scalar values are singleton lists.
+    Convenient for iterating over values whether they're lists or singletons.
+    """
+    flattened = [ x if isinstance(x, (list, tuple)) else [x] for x in args ]
+    return itertools.chain(*flattened)
+
+    # would use ``hasattr(x, '__iter__')`` rather than ``isinstance(x, (list, tuple))``,
+    # but other objects like file have ``__iter__``, which screws things up
+
+
+### Workhorse functions
+
+sformatter = string.Formatter()  # piggyback Python's format() template parser
+
+def _sprintf(arg, caller, override=None):
+    """
+    Format the template string (arg) with the values seen in the context of the caller.
+    If override is defined, it is a dictionary like object mapping additional
+    values atop those of the local context.
+    """
+
+    def seval(s):
+        """
+        Evaluate the string s in the caller's context. Return its value.
+        """
+        try:
+            localvars = caller.f_locals if override is None \
+                                        else chainstuf(override, caller.f_locals)
+            return eval(s, caller.f_globals, localvars)
+        except SyntaxError:
+            raise SyntaxError("syntax error when formatting '{}'".format(s))
+
+
+    if is_string(arg):
+        parts = []
+        for (literal_text, field_name, format_spec, conversion) in sformatter.parse(arg):
+            parts.append(literal_text)
+            if field_name is not None:
+                format_str = six.u("{0") + ("!" + conversion if conversion else "") + \
+                                          (":" + format_spec if format_spec else "") + "}"
+                field_value = seval(field_name)
+                formatted = format_str.format(field_value)
+                parts.append(formatted)
+        return ''.join(parts)
+    else:
+        return str(seval(str(arg)))
+
+### Core Say class
+
+
+class Say(object):
+    """
+    Say encapsulates printing functions. Instances are configurable, and callable.
+    """
+    
+    options = Options(  
+        indent=0,           # indent level (if set to None, indentation is turned off)
+        indent_str='    ',  # indent string for each level
+        encoding=None if six.PY3 else 'utf-8',
+                            # character encoding for output (needed on Python 2, not 3)
+        encoded=None,       # character encoding to return
+        files= [stdout],    # where is output headed? a list of write() able objects
+                            # NB Set to [stdout] but in way that gets around stuf bug
+        wrap=None,          # column to wrap text to, if any
+        sep=' ',            # separate args with this (Python print function compatible)
+        end='\n',           # end output with this (Python print function compatible)
+        silent=False,       # do the formatting and return the result, but don't write the output
+        retvalue=False,     # should return the value of the formatted string?
+        _callframe=None,    # frome from which the caller was calling
+    )
+    
+    options.magic(
+        indent = lambda v, cur: cur.indent + int(v) if isinstance(v, str) else v
+    )
+    
+  
+    def __init__(self, **kwargs):
+        """
+        Make a say object with the given options.
+        """
+        self.options = Say.options.push(kwargs)
+    
+    def hr(self, sep=six.u('\u2500'), width=40, vsep=0, **kwargs):
+        """
+        Print a horizontal line. Like the HTML hr tag. Optionally
+        specify the width, character repeated to make the line, and vertical separation.
+        
+        Good options for the separator may be '-', '=', or parts of the Unicode 
+        box drawing character set. http://en.wikipedia.org/wiki/Box-drawing_character
+        """
+        opts = self.options.push(kwargs)
+
+        self.blank_lines(vsep, **opts)
+        self._output([sep * width], opts)
+        self.blank_lines(vsep, **opts)
+
+    def title(self, name, sep=six.u('\u2500'), width=15, vsep=0, **kwargs):
+        """
+        Print a horizontal line with an embedded title. 
+        """
+        opts = self.options.push(kwargs)
+
+        self.blank_lines(vsep, **opts)
+        line = sep * width
+        self._output([ ' '.join([line, name, line]) ], opts)
+        self.blank_lines(vsep, **opts)
+    
+    def blank_lines(self, n, **kwargs):
+        """
+        Output N blank lines ("vertical separation")
+        """
+        if n > 0:
+            opts = self.options.push(kwargs)
+            self._write("\n" * n, opts)
+
+    def set(self, **kwargs):
+        """
+        Permanently change the reciver's settings to those defined in the kwargs.
+        An update-like function.
+        """
+        self.options.set(**kwargs)
+        
+    def setfiles(self, files):
+        """
+        Set the list of output files. ``files`` is a list. For each item, if
+        it's a real file like ``sys.stdout``, use it. If it's a string, assume
+        it's a filename and open it for writing. 
+        """
+        def opened(f):
+            """
+            If f is a string, consider it a file name and return it, ready for writing.
+            Else, assume it's an open file. Just return it.
+            """
+            return open(f, "w") if is_string(f) else f
+            
+        self.options.files = [ opened(f) for f in files ]
+        return self
+    
+        # TBD: Turn this into 'magical' attribute set
+    
+    def settings(self, **kwargs):
+        """
+        Open a context manager for a `with` statement. Temporarily change settings
+        for the duration of the with.
+        """
+        return SayContext(self, 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 = Say()
+        cloned.options = self.options.push(kwargs)
+        return cloned
+
+    def __call__(self, *args, **kwargs): 
+        
+        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 ]
+        return self._output(opts.sep.join(formatted), opts)
+        
+    def _output(self, data, opts):
+        """
+        Do the actual formatting and outputting work. ``data`` may be either a
+        list of lines, or a composed string.
+        """
+        
+        if opts.indent or opts.wrap:
+            indent_str = opts.indent_str * opts.indent
+            if opts.wrap:
+                datastr = '\n'.join(data) if isinstance(data, list) else data
+                outstr = fill(datastr, width=opts.wrap, initial_indent=indent_str, subsequent_indent=indent_str)
+            else:
+                datalines = data if isinstance(data, list) else data.splitlines()
+                outstr = '\n'.join([ ''.join([ indent_str, line ]) for line in datalines ])
+        else:
+            outstr = '\n'.join(data) if isinstance(data, list) else data
+        # by end of indenting, dealing with string only
+
+        # prepare and emit output
+        if opts.end is not None:
+            outstr += opts.end
+        if not opts.silent:
+            to_write = encoded(outstr, opts.encoding)
+            self._write(to_write, opts)
+            
+        # prepare and return return value
+        if opts.retvalue:
+            retencoding = opts.encoding if opts.encoded is True else opts.encoded
+            retencoded = encoded(outstr, retencoding)
+            rettrimmed = retencoded[:-1] if retencoded.endswith('\n') else retencoded
+            return rettrimmed
+
+    def _write(self, s, opts):
+        """
+        Write s to all associated file objects. 
+        """
+        # print(opts)
+        for f in opts.files:
+            f.write(s)
+
+class SayContext(OptionsContext):
+    """
+    Context helper to support Python's with statement.  Generally called
+    from ``with say.settings(...):``
+    """
+    pass
+    
+
+### Define default ``say`` and ``fmt`` callables
+
+say = Say()
+
+fmt = say.clone(encoding=None, retvalue=True, silent=True)
+fmt.setfiles([])
 
 setup(
     name='say',
-    version=verno("0.606"),
+    version=verno("0.620"),
     author='Jonathan Eunice',
     author_email='jonathan.eunice@gmail.com',
     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'],
+    packages=['say'],
     install_requires=['six','options>=0.325', 'stuf>=0.9.10'],
     tests_require = ['tox', 'pytest', 'six'],
     zip_safe = True,

test/test_show.py

 
-import pytest
-from say import Say, fmt, show, DebugPrinter
+from say import Show, Say
 
-say = Say(retvalue=True)
-show = DebugPrinter(where=False)
-show.say = say
+show = Show(where=False, retvalue=True)
 
 def test_basic(param = 'Yo'):