././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1658606582.9615712 q-2.7/0000775000175000017500000000000000000000000011611 5ustar00stavrosstavros././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1658606518.0 q-2.7/MANIFEST.in0000664000175000017500000000002200000000000013341 0ustar00stavrosstavrosinclude README.md ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1658606582.9615712 q-2.7/PKG-INFO0000664000175000017500000000146500000000000012714 0ustar00stavrosstavrosMetadata-Version: 1.1 Name: q Version: 2.7 Summary: Quick-and-dirty debugging output for tired programmers Home-page: http://github.com/zestyping/q Author: Ka-Ping Yee Author-email: ping@zesty.ca License: Apache License 2.0 Description: UNKNOWN Keywords: debugging Platform: UNKNOWN Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: Programming Language :: Python :: Implementation :: Jython Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: Apache Software License ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1658606518.0 q-2.7/README.md0000664000175000017500000000370300000000000013073 0ustar00stavrosstavros# q [![Code Shelter](https://www.codeshelter.co/static/badges/badge-flat.svg)](https://www.codeshelter.co/) Quick and dirty debugging output for tired programmers. For a short demo, watch the [Lightning Talk](http://pyvideo.org/video/1858/sunday-evening-lightning-talks#t=25m15s) from PyCon 2013. Install q with `pip install -U q`. All output goes to `/tmp/q` (or on Windows, to `$HOME/tmp/q`). You can watch the output with this shell command while your program is running: tail -f /tmp/q To print the value of foo, insert this into your program: import q; q(foo) To print the value of something in the middle of an expression, you can wrap it with `q()`. You can also insert `q/` or `q|` into the expression; `q/` binds tightly whereas `q|` binds loosely. For example, given this statement: file.write(prefix + (sep or '').join(items)) you can print out various values without using any temporary variables: file.write(prefix + q(sep or '').join(items)) # prints (sep or '') file.write(q/prefix + (sep or '').join(items)) # prints prefix file.write(q|prefix + (sep or '').join(items)) # prints the arg to write To trace a function (showing its arguments, return value, and running time), insert this above the def: import q @q To start an interactive console at any point in your code, call q.d(): import q; q.d() By default the output of q is not truncated, but it can be truncated by calling: q.short Truncation can be reversed by: ```python q.long # Truncates output to 1,000,000 q.long = 2000000 # Truncates output to 2,000,000 ``` # Other projects inspired by this one * [`q` for golang](https://github.com/y0ssar1an/q) * [`qq` for elixir](https://github.com/mandarvaze/q) * [`ic` for Python](https://github.com/gruns/icecream) - Similar library for Python, inspired by `q`. The following [Lightning Talk](http://pyvideo.org/video/1858/sunday-evening-lightning-talks#t=25m15s) shows how powerful using q can be. ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1658606582.960571 q-2.7/q.egg-info/0000775000175000017500000000000000000000000013543 5ustar00stavrosstavros././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1658606582.0 q-2.7/q.egg-info/PKG-INFO0000664000175000017500000000146500000000000014646 0ustar00stavrosstavrosMetadata-Version: 1.1 Name: q Version: 2.7 Summary: Quick-and-dirty debugging output for tired programmers Home-page: http://github.com/zestyping/q Author: Ka-Ping Yee Author-email: ping@zesty.ca License: Apache License 2.0 Description: UNKNOWN Keywords: debugging Platform: UNKNOWN Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: Programming Language :: Python :: Implementation :: Jython Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: Apache Software License ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1658606582.0 q-2.7/q.egg-info/SOURCES.txt0000664000175000017500000000024400000000000015427 0ustar00stavrosstavrosMANIFEST.in README.md q.py setup.cfg setup.py q.egg-info/PKG-INFO q.egg-info/SOURCES.txt q.egg-info/dependency_links.txt q.egg-info/top_level.txt test/test_basic.py././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1658606582.0 q-2.7/q.egg-info/dependency_links.txt0000664000175000017500000000000100000000000017611 0ustar00stavrosstavros ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1658606582.0 q-2.7/q.egg-info/top_level.txt0000664000175000017500000000000200000000000016265 0ustar00stavrosstavrosq ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1658606518.0 q-2.7/q.py0000664000175000017500000003632100000000000012430 0ustar00stavrosstavros# Copyright 2012 Google Inc. All Rights Reserved. # vim: set ts=4 sw=4 et sts=4 ai: # # Licensed under the Apache License, Version 2.0 (the "License"); you may not # use this file except in compliance with the License. You may obtain a copy # of the License at: http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software distrib- # uted under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES # OR CONDITIONS OF ANY KIND, either express or implied. See the License for # specific language governing permissions and limitations under the License. """Quick and dirty debugging output for tired programmers. All output goes to /tmp/q, which you can watch with this shell command: tail -f /tmp/q If TMPDIR is set, the output goes to $TMPDIR/q. To print the value of foo, insert this into your program: import q; q(foo) To print the value of something in the middle of an expression, insert "q()", "q/", or "q|". For example, given this statement: file.write(prefix + (sep or '').join(items)) ...you can print out various values without using any temporary variables: file.write(prefix + q(sep or '').join(items)) # prints (sep or '') file.write(q/prefix + (sep or '').join(items)) # prints prefix file.write(q|prefix + (sep or '').join(items)) # prints the arg to write To trace a function's arguments and return value, insert this above the def: import q @q To start an interactive console at any point in your code, call q.d(): import q; q.d() """ from __future__ import print_function import sys __author__ = 'Ka-Ping Yee ' # WARNING: Horrible abuse of sys.modules, __call__, __div__, __or__, inspect, # sys._getframe, and more! q's behaviour changes depending on the text of the # source code near its call site. Don't ever do this in real code! # These are reused below in both Q and Writer. ESCAPE_SEQUENCES = ['\x1b[0m'] + ['\x1b[3%dm' % i for i in range(1, 7)] if sys.version_info >= (3,): BASESTRING_TYPES = (str, bytes) TEXT_TYPES = (str,) else: BASESTRING_TYPES = (basestring,) TEXT_TYPES = (unicode,) # When we insert Q() into sys.modules, all the globals become None, so we # have to keep everything we use inside the Q class. class Q(object): __doc__ = __doc__ # from the module's __doc__ above import ast import code import inspect import os import pydoc import sys import random import re import time import functools import tempfile # The debugging log will go to this file; temporary files will also have # this path as a prefix, followed by a random number. OUTPUT_PATH = os.path.join(tempfile.gettempdir(), 'q') NORMAL, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN = ESCAPE_SEQUENCES TEXT_REPR = pydoc.TextRepr() q_max_length = 1_000_000 @property def short(self): __class__.TEXT_REPR = __class__.pydoc.TextRepr() @property def long(self): __class__.TEXT_REPR = __class__.pydoc.TextRepr() __class__.TEXT_REPR.maxarray = __class__.q_max_length __class__.TEXT_REPR.maxdeque = __class__.q_max_length __class__.TEXT_REPR.maxdict = __class__.q_max_length __class__.TEXT_REPR.maxfrozenset = __class__.q_max_length __class__.TEXT_REPR.maxlevel = __class__.q_max_length __class__.TEXT_REPR.maxlist = __class__.q_max_length __class__.TEXT_REPR.maxlong = __class__.q_max_length __class__.TEXT_REPR.maxother = __class__.q_max_length __class__.TEXT_REPR.maxset = __class__.q_max_length __class__.TEXT_REPR.maxstring = __class__.q_max_length __class__.TEXT_REPR.maxtuple = __class__.q_max_length @long.setter def long(self, value): __class__.q_max_length = value self.long # For portably converting strings between python2 and python3 BASESTRING_TYPES = BASESTRING_TYPES TEXT_TYPES = TEXT_TYPES class FileWriter(object): """An object that appends to or overwrites a single file.""" import sys # For portably converting strings between python2 and python3 BASESTRING_TYPES = BASESTRING_TYPES TEXT_TYPES = TEXT_TYPES def __init__(self, path): self.path = path self.open = open # App Engine's dev_appserver patches 'open' to simulate security # restrictions in production; we circumvent this to write output. if open.__name__ == 'FakeFile': # dev_appserver's patched 'file' self.open = open.__bases__[0] # the original built-in 'file' def write(self, mode, content): if 'b' not in mode: mode = '%sb' % mode if (isinstance(content, self.BASESTRING_TYPES) and isinstance(content, self.TEXT_TYPES)): content = content.encode('utf-8') try: f = self.open(self.path, mode) f.write(content) f.close() except IOError: pass class Writer: """Abstract away the output pipe, timestamping, and color support.""" NORMAL, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN = ESCAPE_SEQUENCES def __init__(self, file_writer, time): self.color = True self.file_writer = file_writer self.gap_seconds = 2 self.time = time # the 'time' module (needed because no globals) self.start_time = self.time.time() self.last_write = 0 def write(self, chunks): """Writes out a list of strings as a single timestamped unit.""" if not self.color: chunks = [x for x in chunks if not x.startswith('\x1b')] content = ''.join(chunks) now = self.time.time() prefix = '%4.1fs ' % ((now - self.start_time) % 100) indent = ' ' * len(prefix) if self.color: prefix = self.YELLOW + prefix + self.NORMAL if now - self.last_write >= self.gap_seconds: prefix = '\n' + prefix self.last_write = now output = prefix + content.replace('\n', '\n' + indent) self.file_writer.write('a', output + '\n') class Stanza: """Abstract away indentation and line-wrapping.""" def __init__(self, indent=0, width=80 - 7): self.chunks = [' ' * indent] self.indent = indent self.column = indent self.width = width def newline(self): if len(self.chunks) > 1: self.column = self.width def add(self, items, sep='', wrap=True): """Adds a list of strings that are to be printed on one line.""" items = list(map(str, items)) size = sum([len(x) for x in items if not x.startswith('\x1b')]) if (wrap and self.column > self.indent and self.column + len(sep) + size > self.width): self.chunks.append(sep.rstrip() + '\n' + ' ' * self.indent) self.column = self.indent else: self.chunks.append(sep) self.column += len(sep) self.chunks.extend(items) self.column += size def __init__(self): self.writer = self.Writer(self.FileWriter(self.OUTPUT_PATH), self.time) self.indent = 0 # in_console tracks whether we're in an interactive console. # We use it to display the caller as "" instead of "". self.in_console = False def unindent(self, lines): """Removes any indentation that is common to all of the given lines.""" indent = min( len(self.re.match(r'^ *', line).group()) for line in lines) return [line[indent:].rstrip() for line in lines] def safe_repr(self, value): # TODO: Use colour to distinguish '...' elision from actual '...' # TODO: Show a nicer repr for SRE.Match objects. # TODO: Show a nicer repr for big multiline strings. result = self.TEXT_REPR.repr(value) if isinstance(value, self.BASESTRING_TYPES) and len(value) > 80: # If the string is big, save it to a file for later examination. if isinstance(value, self.TEXT_TYPES): value = value.encode('utf-8') path = self.OUTPUT_PATH + '%08d.txt' % self.random.randrange(1e8) self.FileWriter(path).write('w', value) result += ' (file://' + path + ')' return result def get_call_exprs(self, line): """Gets the argument expressions from the source of a function call.""" line = line.lstrip() try: tree = self.ast.parse(line) except SyntaxError: return None for node in self.ast.walk(tree): if isinstance(node, self.ast.Call): offsets = [] for arg in node.args: # In Python 3.4 the col_offset is calculated wrong. See # https://bugs.python.org/issue21295 if isinstance(arg, self.ast.Attribute) and ( (3, 4, 0) <= self.sys.version_info <= (3, 4, 3)): offsets.append(arg.col_offset - len(arg.value.id) - 1) else: offsets.append(arg.col_offset) if node.keywords: line = line[:node.keywords[0].value.col_offset] line = self.re.sub(r'\w+\s*=\s*$', '', line) else: line = self.re.sub(r'\s*\)\s*$', '', line) offsets.append(len(line)) args = [] for i in range(len(node.args)): args.append(line[offsets[i]:offsets[i + 1]].rstrip(', ')) return args def show(self, func_name, values, labels=None): """Prints out nice representations of the given values.""" s = self.Stanza(self.indent) if func_name == '' and self.in_console: func_name = '' s.add([func_name + ': ']) reprs = map(self.safe_repr, values) if labels: sep = '' for label, repr in zip(labels, reprs): s.add([label + '=', self.CYAN, repr, self.NORMAL], sep) sep = ', ' else: sep = '' for repr in reprs: s.add([self.CYAN, repr, self.NORMAL], sep) sep = ', ' self.writer.write(s.chunks) def trace(self, func): """Decorator to print out a function's arguments and return value.""" def wrapper(*args, **kwargs): # Print out the call to the function with its arguments. s = self.Stanza(self.indent) s.add([self.GREEN, func.__name__, self.NORMAL, '(']) s.indent += 4 sep = '' for arg in args: s.add([self.CYAN, self.safe_repr(arg), self.NORMAL], sep) sep = ', ' for name, value in sorted(kwargs.items()): s.add([name + '=', self.CYAN, self.safe_repr(value), self.NORMAL], sep) sep = ', ' s.add(')', wrap=False) self.writer.write(s.chunks) # Call the function. self.indent += 2 try: result = func(*args, **kwargs) except: # Display an exception. self.indent -= 2 etype, evalue, etb = self.sys.exc_info() info = self.inspect.getframeinfo(etb.tb_next, context=3) s = self.Stanza(self.indent) s.add([self.RED, '!> ', self.safe_repr(evalue), self.NORMAL]) s.add(['at ', info.filename, ':', info.lineno], ' ') lines = self.unindent(info.code_context) firstlineno = info.lineno - info.index fmt = '%' + str(len(str(firstlineno + len(lines)))) + 'd' for i, line in enumerate(lines): s.newline() s.add([ i == info.index and self.MAGENTA or '', fmt % (i + firstlineno), i == info.index and '> ' or ': ', line, self.NORMAL]) self.writer.write(s.chunks) raise # Display the return value. self.indent -= 2 s = self.Stanza(self.indent) s.add([self.GREEN, '-> ', self.CYAN, self.safe_repr(result), self.NORMAL]) self.writer.write(s.chunks) return result return self.functools.update_wrapper(wrapper, func) def __call__(self, *args): """If invoked as a decorator on a function, adds tracing output to the function; otherwise immediately prints out the arguments.""" info = self.inspect.getframeinfo(self.sys._getframe(1), context=9) # info.index is the index of the line containing the end of the call # expression, so this gets a few lines up to the end of the expression. lines = [''] if info.code_context: lines = info.code_context[:info.index + 1] # If we see "@q" on a single line, behave like a trace decorator. for line in lines: if line.strip() in ('@q', '@q()') and args: return self.trace(args[0]) # Otherwise, search for the beginning of the call expression; once it # parses, use the expressions in the call to label the debugging # output. for i in range(1, len(lines) + 1): labels = self.get_call_exprs(''.join(lines[-i:]).replace('\n', '')) if labels: break self.show(info.function, args, labels) return args and args[0] def __truediv__(self, arg): # a tight-binding operator """Prints out and returns the argument.""" info = self.inspect.getframeinfo(self.sys._getframe(1)) self.show(info.function, [arg]) return arg # Compat for Python 2 without from future import __division__ turned on __div__ = __truediv__ __or__ = __div__ # a loose-binding operator q = __call__ # backward compatibility with @q.q t = trace # backward compatibility with @q.t __name__ = 'Q' # App Engine's import hook dies if this isn't present def d(self, depth=1): """Launches an interactive console at the point where it's called.""" info = self.inspect.getframeinfo(self.sys._getframe(1)) s = self.Stanza(self.indent) s.add([info.function + ': ']) s.add([self.MAGENTA, 'Interactive console opened', self.NORMAL]) self.writer.write(s.chunks) frame = self.sys._getframe(depth) env = frame.f_globals.copy() env.update(frame.f_locals) self.indent += 2 self.in_console = True self.code.interact( 'Python console opened by q.d() in ' + info.function, local=env) self.in_console = False self.indent -= 2 s = self.Stanza(self.indent) s.add([info.function + ': ']) s.add([self.MAGENTA, 'Interactive console closed', self.NORMAL]) self.writer.write(s.chunks) # Install the Q() object in sys.modules so that "import q" gives a callable q. q = Q() q.long sys.modules['q'] = q ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1658606582.9615712 q-2.7/setup.cfg0000664000175000017500000000015400000000000013432 0ustar00stavrosstavros[metadata] description-file = README.md [bdist_wheel] universal = 1 [egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1658606530.0 q-2.7/setup.py0000664000175000017500000000154600000000000013331 0ustar00stavrosstavrosfrom setuptools import setup setup( name='q', version='2.7', py_modules=['q'], description='Quick-and-dirty debugging output for tired programmers', author='Ka-Ping Yee', author_email='ping@zesty.ca', license='Apache License 2.0', classifiers=[ "Programming Language :: Python", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: Implementation :: PyPy", "Programming Language :: Python :: Implementation :: Jython", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", ], keywords=['debugging'], url='http://github.com/zestyping/q' ) ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1658606582.960571 q-2.7/test/0000775000175000017500000000000000000000000012570 5ustar00stavrosstavros././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1658606518.0 q-2.7/test/test_basic.py0000664000175000017500000001204700000000000015266 0ustar00stavrosstavros#!/usr/bin/env python # vim: set ts=4 sw=4 et sts=4 ai: # # Test some basic functionality. # import os import re import sys import unittest qpath = os.path.abspath(os.path.join(os.path.split(__file__)[0], '..')) sys.path.insert(0, qpath) class TestQBasic(unittest.TestCase): def setUp(self): if os.path.exists('/tmp/q'): os.remove('/tmp/q') def tearDown(self): self.setUp() def assertInQLog(self, string): # Check the log file exists. self.assertTrue(os.path.exists('/tmp/q')) # Read in the data. f = open('/tmp/q', 'r') logdata = f.read() f.close() # Check the string is found in the log file. # We can't use self.assertRegexpMatches as we need re.DOTALL expected_regexp = re.compile('.*%s.*' % string, re.DOTALL) if not expected_regexp.search(logdata): msg = '%s: %r not found in\n%s\n%s\n%s' % ( "Regexp didn't match", expected_regexp.pattern, "-"*75, logdata, "-"*75, ) raise self.failureException(msg) def test_q_log_message(self): import q q.q('Test message') self.assertInQLog('Test message') def test_q_function_call(self): import q @q.t def test(arg): return 'RetVal' self.assertEqual('RetVal', test('ArgVal')) self.assertInQLog('ArgVal') self.assertInQLog('RetVal') def test_q_argument_order_arguments(self): import q q.writer.color = False class A: def __init__(self, two, three, four): q(two, three, four) A("ArgVal1", "ArgVal2", "ArgVal3") self.assertInQLog(".*".join([ "__init__:", "two='ArgVal1'", "three='ArgVal2'", "four='ArgVal3'", ])) def test_q_argument_order_attributes1(self): import q q.writer.color = False class A: def __init__(self, two, three, four): self.attrib1 = 'Attrib1' self.attrib2 = 'Attrib2' q(self.attrib1, self.attrib2) A("ArgVal1", "ArgVal2", "ArgVal3") self.assertInQLog(".*".join([ "__init__:", "self.attrib1='Attrib1',", "self.attrib2='Attrib2'", ])) def test_q_argument_order_attributes2(self): import q q.writer.color = False class A: def __init__(s, two, three, four): s.attrib1 = 'Attrib1' s.attrib2 = 'Attrib2' q(s.attrib1, s.attrib2) A("ArgVal1", "ArgVal2", "ArgVal3") self.assertInQLog(".*".join([ "__init__:", "s.attrib1='Attrib1',", "s.attrib2='Attrib2'", ])) def test_q_argument_order_attributes_and_arguments(self): import q q.writer.color = False class A: def __init__(self, two, three, four): self.attrib1 = 'Attrib1' self.attrib2 = 'Attrib2' q(two, three, self.attrib1, four, self.attrib2) A("ArgVal1", "ArgVal2", "ArgVal3") self.assertInQLog(".*".join([ "__init__:", "two='ArgVal1'", "three='ArgVal2'", "self.attrib1='Attrib1'", "four='ArgVal3'", "self.attrib2='Attrib2'", ])) def test_q_trace(self): import q q.writer.color = False @q def log1(msg='default'): return msg @q.t def log2(msg='default'): return msg log1('log1 message') log2('log2 message') self.assertInQLog("log1\\('log1 message'\\)") self.assertInQLog("log2\\('log2 message'\\)") def test_q_nested_bad_wrapper(self): # See http://micheles.googlecode.com/hg/decorator/documentation.html#statement-of-the-problem # noqa import q q.writer.color = False def wrapper(func): def do_nothing(*args, **kwargs): return func(*args, **kwargs) return do_nothing @wrapper @q @wrapper def decorated_log_bad(msg='default'): return msg decorated_log_bad('decorated bad message') self.assertInQLog("do_nothing\\('decorated bad message'\\)") self.assertInQLog("-> 'decorated bad message'") def test_q_nested_good_wrappers(self): import q q.writer.color = False import functools def wrapper(func): def do_nothing(*args, **kwargs): return func(*args, **kwargs) return functools.update_wrapper(do_nothing, func) @wrapper @q @wrapper def decorated_log_good(msg='default'): return msg decorated_log_good('decorated good message') self.assertInQLog("decorated_log_good\\('decorated good message'\\)") self.assertInQLog("-> 'decorated good message'") unittest.main()