The truth is rarely pure and never simple

hierarchical logging for python

Have you ever tried to spot a certain message in screens full of debugging output? Normally, you have a hard time scanning the text in order to find the output section you were looking for. Why not making the life easier for yourself (and your users) and start printing messages organized in a hierarchical structure? Here is how it may look like:

bibsync

For your convenience, here is a small python module that follows this idea. It is a logging facility which mimics file objects and keeps track of proper indentation together with colorized output. The class supports fluent interface calls and detects pipe redirections automatically. In the latter case, the messages are redirected to stderr, so the content your application sends to stdout is unaffected and the piping works as expected.

How to use it

Let’s assume, you have downloaded the source file below and now have a module file in your input path that is called output_tracker. This usage example will help you to get the idea

import output_tracker as ot
o = ot.output_tracker
o.print_info('Loading cached data').add_level()
# do something
o.print_info('Cache content A loaded')
# do something
o.print_info('Cache content B loaded')
o.add_level().print_info('Content outdated. Rebuilding.')

o.del_level(2)

with o:
  # call external library that fills stdout
  # output_tracker will capture it, as long as you use the with statement
  print 'This line will be printed as info output.'

o.print_warning(ot.errors.FOOBAR) # the error messages are static so you can use error codes
o.print_error(ot.errors.FOOBAR) # terminates the application and prints an exception

If you want to disable printing messages in your application, you may use the output_tracker.null_output class which will only print error messages. Please note that you have to define the error codes and their names by yourself as only the info messages can have arbitrary content.

The source

#!/usr/bin/env python
import sys 

class errors(object):
	NO_MODE_AND_INPUT 	= (1, 'Mode and input file missing.')
	NO_OUTPUT 			= (2, 'No output file given.')

# sanity check for error codes
codes = [x[1][0] for x in vars(errors).items() if x[0].upper() == x[0]]
if len(codes) != len(set(codes)):
	raise TypeError('Duplicate error code.')

class null_output(object):
	def __getattribute__(self, name):
		def print_error(error):
			raise Exception(error[1])

		if name == 'print_error':
			return print_error
		return lambda x: self

class output_tracker(object):
	def __enter__(self):
		self._stdout = sys.stdout
		sys.stdout = self

	def __exit__(self, exc_type, exc_value, traceback):
		sys.stdout = self._stdout

	def __init__(self):
		self._indent = 0
		self._indent_steps = 3
		self._stdout = sys.stdout

		# bash color codes
		self.colors = dict([
			('red', '0;31'), 
			('green', '0;32'),
			('brown', '0;33'),
			('default', '0')
			])

	def write(self, text):
		self.print_info(text.strip('\n'))

	def flush(self):
		pass

	def _print_multiline(self, prefix, message, color):
		if len(prefix) not in  [0, 2]:
			raise ValueError('Internal Error: Wrong prefix length.')
		if prefix != '':
			prefix += ' '
		if type(message) != str:
			message = str(message)

		parts = message.split('\n')
		target = self._stdout
		if not target.isatty():
			target = sys.stderr
		target.write('{1}\033[{3}m{0}\033[0m{2}\n'.format(
			prefix.upper(), 
			' ' * self._indent, 
			parts[0], 
			color
			))
		for part in parts[1:]:
			target.write('{0}{1}\n'.format(' ' * (self._indent + 2), part))

	def print_error(self, code):
		if not type(code[0]) is int:
			raise ValueError('Error code is not numeric.')
		self._print_multiline(
			'EE', 
			code[1] + (' (error code %d)' % code[0]), 
			self.colors['red']
			)
		raise Exception('Exception for traceback.')

	def print_warn(self, code):
		if not type(code[0]) is int:
			raise ValueError('Warning code is not numeric.')
		self._print_multiline(
			'WW', 
			code[1] + (' (warning code %d)' % code[0]), 
			self.colors['brown']
			)
		return self

	def print_info(self, message):
		self._print_multiline('II', message, self.colors['green'])
		return self

	def add_level(self):
		self._indent += self._indent_steps
		return self

	def del_level(self, depth=1):
		self._indent -= self._indent_steps*depth
		if self._indent < 0:
			raise ValueError('Leftmost indentation level reached.')
		return self

	def print_plain(self, message):
		self._print_multiline('', message, self.colors['default'])
		return self

Leave a comment

Your email address will not be published.