"""pytp.py -- A very simple Python Template Processor


USAGE

pytp.py infile outfile

For use in a Python script, create a PyTP instance::

    pytp = PyTP()

Then you can process strings with the process() method,
like that::

    input = open(infile).read()
    output = pytp.process(input)
    open(outfile, 'w').write(output)

You can also pass a scope to the process() method that will
be used by the Python code in the template.

The template processor evaluates instruction of this form::

    <%...%>

The starting and ending tag can be customized by setting
the tags parameter when creating the PyTP instance.
The default tags are those used by PSP. Other templating
languages use tags like::

    ('<?py', '?>') or ('<!--python', '-->') or ('[[' , ']]')

If the directive inside the tags starts with an equals sign,
it is considered as a Python expression to be evaluated. If
if the equals sign is missing, pytp will automatically find
out whether it is a Python expression or a Python statement.

* If it is a Python expression:
  - The expression will be evaluated and processed
    recursively as follows:
    * It if is a dictionary,
      use the sorted list of items on separate lines.
    * If it is any other iterable,
      use the list of items on separate lines.
    * Otherwise,
      use the conversion of the result to a string.
  - The processed result will be inserted in the output
    instead of the processing instruction.
* If it is a block of Python statements:
  - The statements will be executed.
  - Everything that is printed to standard output during
    execution will be inserted in the output instead of
    the processing instruction.


DOWNLOAD

This script is part of Webware for Python.
You can download the latest version from the SVN repository
(http://svn.w4py.org/Webware/trunk/DocSupport/pytp.py).

Note: Similar template processors have been written by:
* Christopher A. Craig (http://www.ccraig.org/software/pyhp/)
* David McNab (http://www.freenet.org.nz/python/pyweb/docs/pyhp.html)
* Alex Martelli (http://aspn.activestate.com/ASPN/Python/Cookbook/Recipe/52305)


COPYRIGHT

Copyright (c) 2005 by Christoph Zwerschke.
Licensed under the Open Software License version 2.1.

"""

__version__ = '0.1'
__revision__ = "$Revision: 7626 $"
__date__ = "$Date: 2008-11-13 14:48:43 -0700 (Thu, 13 Nov 2008) $"


import sys, re
try:
	from cStringIO import StringIO
except ImportError:
	from StringIO import StringIO


class PyTP:
	"""A very simple Python Template Processor.

	Provides only one method process().
	"""

	def __init__(self, tags=None):
		"""Initialize the Python template processor.

		You may define your own start and end tags here.
		"""
		if tags is None:
			tags = ('<%', '%>')
		pattern = '%s(.*?)%s' % tuple(map(re.escape, tags))
		self._tags = re.compile(pattern, re.DOTALL)

	def process(self, input, scope=None):
		"""Process a Python template.

		The input must be a string that will be returned
		with all tagged processing instructions expanded.

		You may also pass a variable scope for the
		processing instructions that must be a directory.

		"""
		if scope is None:
			scope = {}
		stdout = sys.stdout
		output = []
		pos = 0
		while pos < len(input):
			m = self._tags.search(input, pos)
			if m is None:
				break
			pi = m.groups()[0].strip()
			isexpr = pi.startswith('=')
			if isexpr:
				pi = pi[1:].lstrip()
			try: # try to evaluate as Python expression
				out = eval(pi, scope)
				if out is None:
					out = ''
			except SyntaxError:
				if isexpr:
					line = input[:m.start()].count('\n') + 1
					self._errmsg('expression syntax', line, pi)
					raise
				out = None
			except Exception:
				line = input[:m.start()].count('\n') + 1
				self._errmsg('expression', line, pi)
				raise
			if out:
				try:
					out = self._output(out)
				except Exception:
					line = input[:m.start()].count('\n') + 1
					self._errmsg('expression output', line, pi)
					raise
			elif out is None:
				try: # try to evaluate as Python block
					tempout = StringIO()
					sys.stdout = tempout
					try:
						pi = self._adjust_block(pi)
						exec pi in scope
						out = tempout.getvalue()
					finally:
						sys.stdout = stdout
						tempout.close()
				except Exception:
					line = input[:m.start()].count('\n') + 1
					self._errmsg('statement', line, pi)
					raise
			output.append(input[pos:m.start()])
			if out:
				output.append(out)
			pos = m.end()
		output.append(input[pos:])
		return ''.join(output)

	# Auxiliary functions

	def _output(self, something):
		"""Output a Python object reasonably as string."""
		output = []
		if hasattr(something, 'items'):
			items = something.items
			try:
				items.sort()
			except Exception:
				pass
			output.append(self._output(items))
		elif hasattr(something, '__iter__'):
			for s in something:
				output.append(self._output(s))
		else:
			if something is not None:
				output.append(str(something))
		while hasattr(something, 'next'):
			something = something.next
			if something is None:
				break
			output.append(self._output(something))
		return '\n'.join(output)

	def _errmsg(self, error, line, code):
		"""Print an error message."""
		print 'PyTP %s error in line %d:' % (error, line)
		print code

	def _adjust_block(self, block, tab='\t'):
		"""Adjust the indentation of a Python block."""
		lines = block.splitlines()
		lines = [lines[0].strip()] + [line.rstrip() for line in lines[1:]]
		ind = None # find least index
		for line in lines[1:]:
			if line != '':
				s = line.lstrip()
				if s[0] != '#':
					i = len(line) - len(s)
					if ind is None or i < ind:
						ind = i
						if i == 0:
							break
		if ind is not None or ind != 0: # remove indentation
			lines[1:] = [line[:ind].lstrip() + line[ind:]
				for line in lines[1:]]
		block = '\n'.join(lines) + '\n'
		if lines[0] and not lines[0][0] == '#':
			# the first line contains code
			try: # try to compile it
				compile(lines[0], '<string>', 'exec')
				# if it works, line does not start new block
			except SyntaxError: # unexpected EOF while parsing?
				try: # try to compile the whole block
					compile(block, '<string>', 'exec')
					# if it works, line does not start new block
				except IndentationError: # expected an indented block?
					# so try to add some indentation:
					lines2 = lines[:1] + [tab + line for line in lines[1:]]
					block2 = '\n'.join(lines2) + '\n'
					# try again to compile the whole block:
					compile(block2, '<string>', 'exec')
					block = block2 # if it works, keep the indentation
				except Exception:
					pass # leave it as it is
			except Exception:
				pass # leave it as it is
		return block


def main(args):
	try:
		infile, outfile = args
	except Exception:
		print __doc__
		sys.exit(2)
	pytp = PyTP()
	open(outfile, 'w').write(pytp.process(open(infile).read()))


if __name__ == '__main__':
	main(sys.argv[1:])
