From a629df3f7ef4e36573671018a25e2e9aa0889dbf Mon Sep 17 00:00:00 2001 From: Mike Bayer <mike_mp@zzzcomputing.com> Date: Thu, 4 Mar 2010 23:45:40 +0000 Subject: [PATCH] - merged -r481:499 of py3k branch. - Python 3 support is added ! See README.py3k for installation and testing notes. [ticket:119] --- CHANGES | 4 + README.py3k | 50 +++ distribute_setup.py | 477 +++++++++++++++++++++ mako/ast.py | 36 +- mako/codegen.py | 13 +- mako/exceptions.py | 37 +- mako/filters.py | 7 +- mako/lexer.py | 112 +++-- mako/parsetree.py | 98 ++++- mako/pygen.py | 7 +- mako/pyparser.py | 49 ++- mako/runtime.py | 41 +- mako/template.py | 26 +- mako/util.py | 35 +- setup.py | 10 +- test/__init__.py | 13 +- test/templates/chs_unicode_py3k.html | 11 + test/templates/read_unicode_py3k.html | 10 + test/templates/unicode_arguments_py3k.html | 10 + test/templates/unicode_code_py3k.html | 7 + test/templates/unicode_expr_py3k.html | 2 + test/test_ast.py | 79 ++-- test/test_def.py | 33 +- test/test_exceptions.py | 59 ++- test/test_inheritance.py | 27 +- test/test_lexer.py | 259 ++++++++--- test/test_template.py | 223 ++++++---- 27 files changed, 1400 insertions(+), 335 deletions(-) create mode 100644 README.py3k create mode 100644 distribute_setup.py create mode 100644 test/templates/chs_unicode_py3k.html create mode 100644 test/templates/read_unicode_py3k.html create mode 100644 test/templates/unicode_arguments_py3k.html create mode 100644 test/templates/unicode_code_py3k.html create mode 100644 test/templates/unicode_expr_py3k.html diff --git a/CHANGES b/CHANGES index f048aed..d3ed5ea 100644 --- a/CHANGES +++ b/CHANGES @@ -1,5 +1,9 @@ 0.3 - Python 2.3 support is dropped. [ticket:123] + +- Python 3 support is added ! See README.py3k + for installation and testing notes. + [ticket:119] - Unit tests now run with nose. [ticket:127] diff --git a/README.py3k b/README.py3k new file mode 100644 index 0000000..3e3c8ab --- /dev/null +++ b/README.py3k @@ -0,0 +1,50 @@ +================= +PYTHON 3 SUPPORT +================= + +Python 3 support in Mako is provided by the Python 2to3 script. + +Installing Distribute +--------------------- + +Distribute should be installed with the Python3 installation. The +distribute bootloader is included. + +Running as a user with permission to modify the Python distribution, +install Distribute: + + python3 distribute_setup.py + + +Installing Mako in Python 3 +--------------------------------- + +Once Distribute is installed, Mako can be installed directly. +The 2to3 process will kick in which takes several minutes: + + python3 setup.py install + +Converting Tests, Examples, Source to Python 3 +---------------------------------------------- + +To convert all files in the source distribution, run +the 2to3 script: + + 2to3 --no-diffs -w lib test examples + +The above will rewrite all files in-place in Python 3 format. + +Running Tests +------------- + +To run the unit tests, ensure Distribute is installed as above, +and also that at least the ./lib/ and ./test/ directories have been converted +to Python 3 using the source tool above. A Python 3 version of Nose +can be acquired from Bitbucket using Mercurial: + + hg clone http://bitbucket.org/jpellerin/nose3/ + cd nose3 + python3 setup.py install + +The tests can then be run using the "nosetests3" script installed by the above. + diff --git a/distribute_setup.py b/distribute_setup.py new file mode 100644 index 0000000..0021336 --- /dev/null +++ b/distribute_setup.py @@ -0,0 +1,477 @@ +#!python +"""Bootstrap distribute installation + +If you want to use setuptools in your package's setup.py, just include this +file in the same directory with it, and add this to the top of your setup.py:: + + from distribute_setup import use_setuptools + use_setuptools() + +If you want to require a specific version of setuptools, set a download +mirror, or use an alternate download directory, you can do so by supplying +the appropriate options to ``use_setuptools()``. + +This file can also be run as a script to install or upgrade setuptools. +""" +import os +import sys +import time +import fnmatch +import tempfile +import tarfile +from distutils import log + +try: + from site import USER_SITE +except ImportError: + USER_SITE = None + +try: + import subprocess + + def _python_cmd(*args): + args = (sys.executable,) + args + return subprocess.call(args) == 0 + +except ImportError: + # will be used for python 2.3 + def _python_cmd(*args): + args = (sys.executable,) + args + # quoting arguments if windows + if sys.platform == 'win32': + def quote(arg): + if ' ' in arg: + return '"%s"' % arg + return arg + args = [quote(arg) for arg in args] + return os.spawnl(os.P_WAIT, sys.executable, *args) == 0 + +DEFAULT_VERSION = "0.6.10" +DEFAULT_URL = "http://pypi.python.org/packages/source/d/distribute/" +SETUPTOOLS_FAKED_VERSION = "0.6c11" + +SETUPTOOLS_PKG_INFO = """\ +Metadata-Version: 1.0 +Name: setuptools +Version: %s +Summary: xxxx +Home-page: xxx +Author: xxx +Author-email: xxx +License: xxx +Description: xxx +""" % SETUPTOOLS_FAKED_VERSION + + +def _install(tarball): + # extracting the tarball + tmpdir = tempfile.mkdtemp() + log.warn('Extracting in %s', tmpdir) + old_wd = os.getcwd() + try: + os.chdir(tmpdir) + tar = tarfile.open(tarball) + _extractall(tar) + tar.close() + + # going in the directory + subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0]) + os.chdir(subdir) + log.warn('Now working in %s', subdir) + + # installing + log.warn('Installing Distribute') + if not _python_cmd('setup.py', 'install'): + log.warn('Something went wrong during the installation.') + log.warn('See the error message above.') + finally: + os.chdir(old_wd) + + +def _build_egg(egg, tarball, to_dir): + # extracting the tarball + tmpdir = tempfile.mkdtemp() + log.warn('Extracting in %s', tmpdir) + old_wd = os.getcwd() + try: + os.chdir(tmpdir) + tar = tarfile.open(tarball) + _extractall(tar) + tar.close() + + # going in the directory + subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0]) + os.chdir(subdir) + log.warn('Now working in %s', subdir) + + # building an egg + log.warn('Building a Distribute egg in %s', to_dir) + _python_cmd('setup.py', '-q', 'bdist_egg', '--dist-dir', to_dir) + + finally: + os.chdir(old_wd) + # returning the result + log.warn(egg) + if not os.path.exists(egg): + raise IOError('Could not build the egg.') + + +def _do_download(version, download_base, to_dir, download_delay): + egg = os.path.join(to_dir, 'distribute-%s-py%d.%d.egg' + % (version, sys.version_info[0], sys.version_info[1])) + if not os.path.exists(egg): + tarball = download_setuptools(version, download_base, + to_dir, download_delay) + _build_egg(egg, tarball, to_dir) + sys.path.insert(0, egg) + import setuptools + setuptools.bootstrap_install_from = egg + + +def use_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL, + to_dir=os.curdir, download_delay=15, no_fake=True): + # making sure we use the absolute path + to_dir = os.path.abspath(to_dir) + was_imported = 'pkg_resources' in sys.modules or \ + 'setuptools' in sys.modules + try: + try: + import pkg_resources + if not hasattr(pkg_resources, '_distribute'): + if not no_fake: + _fake_setuptools() + raise ImportError + except ImportError: + return _do_download(version, download_base, to_dir, download_delay) + try: + pkg_resources.require("distribute>="+version) + return + except pkg_resources.VersionConflict: + e = sys.exc_info()[1] + if was_imported: + sys.stderr.write( + "The required version of distribute (>=%s) is not available,\n" + "and can't be installed while this script is running. Please\n" + "install a more recent version first, using\n" + "'easy_install -U distribute'." + "\n\n(Currently using %r)\n" % (version, e.args[0])) + sys.exit(2) + else: + del pkg_resources, sys.modules['pkg_resources'] # reload ok + return _do_download(version, download_base, to_dir, + download_delay) + except pkg_resources.DistributionNotFound: + return _do_download(version, download_base, to_dir, + download_delay) + finally: + if not no_fake: + _create_fake_setuptools_pkg_info(to_dir) + +def download_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL, + to_dir=os.curdir, delay=15): + """Download distribute from a specified location and return its filename + + `version` should be a valid distribute version number that is available + as an egg for download under the `download_base` URL (which should end + with a '/'). `to_dir` is the directory where the egg will be downloaded. + `delay` is the number of seconds to pause before an actual download + attempt. + """ + # making sure we use the absolute path + to_dir = os.path.abspath(to_dir) + try: + from urllib.request import urlopen + except ImportError: + from urllib2 import urlopen + tgz_name = "distribute-%s.tar.gz" % version + url = download_base + tgz_name + saveto = os.path.join(to_dir, tgz_name) + src = dst = None + if not os.path.exists(saveto): # Avoid repeated downloads + try: + log.warn("Downloading %s", url) + src = urlopen(url) + # Read/write all in one block, so we don't create a corrupt file + # if the download is interrupted. + data = src.read() + dst = open(saveto, "wb") + dst.write(data) + finally: + if src: + src.close() + if dst: + dst.close() + return os.path.realpath(saveto) + + +def _patch_file(path, content): + """Will backup the file then patch it""" + existing_content = open(path).read() + if existing_content == content: + # already patched + log.warn('Already patched.') + return False + log.warn('Patching...') + _rename_path(path) + f = open(path, 'w') + try: + f.write(content) + finally: + f.close() + return True + + +def _same_content(path, content): + return open(path).read() == content + +def _no_sandbox(function): + def __no_sandbox(*args, **kw): + try: + from setuptools.sandbox import DirectorySandbox + def violation(*args): + pass + DirectorySandbox._old = DirectorySandbox._violation + DirectorySandbox._violation = violation + patched = True + except ImportError: + patched = False + + try: + return function(*args, **kw) + finally: + if patched: + DirectorySandbox._violation = DirectorySandbox._old + del DirectorySandbox._old + + return __no_sandbox + +@_no_sandbox +def _rename_path(path): + new_name = path + '.OLD.%s' % time.time() + log.warn('Renaming %s into %s', path, new_name) + os.rename(path, new_name) + return new_name + +def _remove_flat_installation(placeholder): + if not os.path.isdir(placeholder): + log.warn('Unkown installation at %s', placeholder) + return False + found = False + for file in os.listdir(placeholder): + if fnmatch.fnmatch(file, 'setuptools*.egg-info'): + found = True + break + if not found: + log.warn('Could not locate setuptools*.egg-info') + return + + log.warn('Removing elements out of the way...') + pkg_info = os.path.join(placeholder, file) + if os.path.isdir(pkg_info): + patched = _patch_egg_dir(pkg_info) + else: + patched = _patch_file(pkg_info, SETUPTOOLS_PKG_INFO) + + if not patched: + log.warn('%s already patched.', pkg_info) + return False + # now let's move the files out of the way + for element in ('setuptools', 'pkg_resources.py', 'site.py'): + element = os.path.join(placeholder, element) + if os.path.exists(element): + _rename_path(element) + else: + log.warn('Could not find the %s element of the ' + 'Setuptools distribution', element) + return True + + +def _after_install(dist): + log.warn('After install bootstrap.') + placeholder = dist.get_command_obj('install').install_purelib + _create_fake_setuptools_pkg_info(placeholder) + +@_no_sandbox +def _create_fake_setuptools_pkg_info(placeholder): + if not placeholder or not os.path.exists(placeholder): + log.warn('Could not find the install location') + return + pyver = '%s.%s' % (sys.version_info[0], sys.version_info[1]) + setuptools_file = 'setuptools-%s-py%s.egg-info' % \ + (SETUPTOOLS_FAKED_VERSION, pyver) + pkg_info = os.path.join(placeholder, setuptools_file) + if os.path.exists(pkg_info): + log.warn('%s already exists', pkg_info) + return + + log.warn('Creating %s', pkg_info) + f = open(pkg_info, 'w') + try: + f.write(SETUPTOOLS_PKG_INFO) + finally: + f.close() + + pth_file = os.path.join(placeholder, 'setuptools.pth') + log.warn('Creating %s', pth_file) + f = open(pth_file, 'w') + try: + f.write(os.path.join(os.curdir, setuptools_file)) + finally: + f.close() + +def _patch_egg_dir(path): + # let's check if it's already patched + pkg_info = os.path.join(path, 'EGG-INFO', 'PKG-INFO') + if os.path.exists(pkg_info): + if _same_content(pkg_info, SETUPTOOLS_PKG_INFO): + log.warn('%s already patched.', pkg_info) + return False + _rename_path(path) + os.mkdir(path) + os.mkdir(os.path.join(path, 'EGG-INFO')) + pkg_info = os.path.join(path, 'EGG-INFO', 'PKG-INFO') + f = open(pkg_info, 'w') + try: + f.write(SETUPTOOLS_PKG_INFO) + finally: + f.close() + return True + + +def _before_install(): + log.warn('Before install bootstrap.') + _fake_setuptools() + + +def _under_prefix(location): + if 'install' not in sys.argv: + return True + args = sys.argv[sys.argv.index('install')+1:] + for index, arg in enumerate(args): + for option in ('--root', '--prefix'): + if arg.startswith('%s=' % option): + top_dir = arg.split('root=')[-1] + return location.startswith(top_dir) + elif arg == option: + if len(args) > index: + top_dir = args[index+1] + return location.startswith(top_dir) + elif option == '--user' and USER_SITE is not None: + return location.startswith(USER_SITE) + return True + + +def _fake_setuptools(): + log.warn('Scanning installed packages') + try: + import pkg_resources + except ImportError: + # we're cool + log.warn('Setuptools or Distribute does not seem to be installed.') + return + ws = pkg_resources.working_set + try: + setuptools_dist = ws.find(pkg_resources.Requirement.parse('setuptools', + replacement=False)) + except TypeError: + # old distribute API + setuptools_dist = ws.find(pkg_resources.Requirement.parse('setuptools')) + + if setuptools_dist is None: + log.warn('No setuptools distribution found') + return + # detecting if it was already faked + setuptools_location = setuptools_dist.location + log.warn('Setuptools installation detected at %s', setuptools_location) + + # if --root or --preix was provided, and if + # setuptools is not located in them, we don't patch it + if not _under_prefix(setuptools_location): + log.warn('Not patching, --root or --prefix is installing Distribute' + ' in another location') + return + + # let's see if its an egg + if not setuptools_location.endswith('.egg'): + log.warn('Non-egg installation') + res = _remove_flat_installation(setuptools_location) + if not res: + return + else: + log.warn('Egg installation') + pkg_info = os.path.join(setuptools_location, 'EGG-INFO', 'PKG-INFO') + if (os.path.exists(pkg_info) and + _same_content(pkg_info, SETUPTOOLS_PKG_INFO)): + log.warn('Already patched.') + return + log.warn('Patching...') + # let's create a fake egg replacing setuptools one + res = _patch_egg_dir(setuptools_location) + if not res: + return + log.warn('Patched done.') + _relaunch() + + +def _relaunch(): + log.warn('Relaunching...') + # we have to relaunch the process + args = [sys.executable] + sys.argv + sys.exit(subprocess.call(args)) + + +def _extractall(self, path=".", members=None): + """Extract all members from the archive to the current working + directory and set owner, modification time and permissions on + directories afterwards. `path' specifies a different directory + to extract to. `members' is optional and must be a subset of the + list returned by getmembers(). + """ + import copy + import operator + from tarfile import ExtractError + directories = [] + + if members is None: + members = self + + for tarinfo in members: + if tarinfo.isdir(): + # Extract directories with a safe mode. + directories.append(tarinfo) + tarinfo = copy.copy(tarinfo) + tarinfo.mode = 448 # decimal for oct 0700 + self.extract(tarinfo, path) + + # Reverse sort directories. + if sys.version_info < (2, 4): + def sorter(dir1, dir2): + return cmp(dir1.name, dir2.name) + directories.sort(sorter) + directories.reverse() + else: + directories.sort(key=operator.attrgetter('name'), reverse=True) + + # Set correct owner, mtime and filemode on directories. + for tarinfo in directories: + dirpath = os.path.join(path, tarinfo.name) + try: + self.chown(tarinfo, dirpath) + self.utime(tarinfo, dirpath) + self.chmod(tarinfo, dirpath) + except ExtractError: + e = sys.exc_info()[1] + if self.errorlevel > 1: + raise + else: + self._dbg(1, "tarfile: %s" % e) + + +def main(argv, version=DEFAULT_VERSION): + """Install or upgrade setuptools and EasyInstall""" + tarball = download_setuptools() + _install(tarball) + + +if __name__ == '__main__': + main(sys.argv[1:]) diff --git a/mako/ast.py b/mako/ast.py index 8d5b1d7..242b6ee 100644 --- a/mako/ast.py +++ b/mako/ast.py @@ -4,7 +4,8 @@ # This module is part of Mako and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php -"""utilities for analyzing expressions and blocks of Python code, as well as generating Python from AST nodes""" +"""utilities for analyzing expressions and blocks of Python +code, as well as generating Python from AST nodes""" from mako import exceptions, pyparser, util import re @@ -22,9 +23,12 @@ class PythonCode(object): # note that an identifier can be in both the undeclared and declared lists. - # using AST to parse instead of using code.co_varnames, code.co_names has several advantages: - # - we can locate an identifier as "undeclared" even if its declared later in the same block of code - # - AST is less likely to break with version changes (for example, the behavior of co_names changed a little bit + # using AST to parse instead of using code.co_varnames, + # code.co_names has several advantages: + # - we can locate an identifier as "undeclared" even if + # its declared later in the same block of code + # - AST is less likely to break with version changes + # (for example, the behavior of co_names changed a little bit # in python version 2.5) if isinstance(code, basestring): expr = pyparser.parse(code.lstrip(), "exec", **exception_kwargs) @@ -65,7 +69,9 @@ class PythonFragment(PythonCode): def __init__(self, code, **exception_kwargs): m = re.match(r'^(\w+)(?:\s+(.*?))?:\s*(#|$)', code.strip(), re.S) if not m: - raise exceptions.CompileException("Fragment '%s' is not a partial control statement" % code, **exception_kwargs) + raise exceptions.CompileException( + "Fragment '%s' is not a partial control statement" % + code, **exception_kwargs) if m.group(3): code = code[:m.start(3)] (keyword, expr) = m.group(1,2) @@ -78,7 +84,9 @@ class PythonFragment(PythonCode): elif keyword == 'except': code = "try:pass\n" + code + "pass" else: - raise exceptions.CompileException("Unsupported control keyword: '%s'" % keyword, **exception_kwargs) + raise exceptions.CompileException( + "Unsupported control keyword: '%s'" % + keyword, **exception_kwargs) super(PythonFragment, self).__init__(code, **exception_kwargs) @@ -91,12 +99,17 @@ class FunctionDecl(object): f = pyparser.ParseFunc(self, **exception_kwargs) f.visit(expr) if not hasattr(self, 'funcname'): - raise exceptions.CompileException("Code '%s' is not a function declaration" % code, **exception_kwargs) + raise exceptions.CompileException( + "Code '%s' is not a function declaration" % code, + **exception_kwargs) if not allow_kwargs and self.kwargs: - raise exceptions.CompileException("'**%s' keyword argument not allowed here" % self.argnames[-1], **exception_kwargs) + raise exceptions.CompileException( + "'**%s' keyword argument not allowed here" % + self.argnames[-1], **exception_kwargs) def get_argument_expressions(self, include_defaults=True): """return the argument declarations of this FunctionDecl as a printable list.""" + namedecls = [] defaults = [d for d in self.defaults] kwargs = self.kwargs @@ -114,12 +127,17 @@ class FunctionDecl(object): else: default = len(defaults) and defaults.pop() or None if include_defaults and default: - namedecls.insert(0, "%s=%s" % (arg, pyparser.ExpressionGenerator(default).value())) + namedecls.insert(0, "%s=%s" % + (arg, + pyparser.ExpressionGenerator(default).value() + ) + ) else: namedecls.insert(0, arg) return namedecls class FunctionArgs(FunctionDecl): """the argument portion of a function declaration""" + def __init__(self, code, **kwargs): super(FunctionArgs, self).__init__("def ANON(%s):pass" % code, **kwargs) diff --git a/mako/codegen.py b/mako/codegen.py index 41ab7fe..ded9396 100644 --- a/mako/codegen.py +++ b/mako/codegen.py @@ -25,6 +25,14 @@ def compile(node, """Generate module source code given a parsetree node, uri, and optional source filename""" + # if on Py2K, push the "source_encoding" string to be + # a bytestring itself, as we will be embedding it into + # the generated source and we don't want to coerce the + # result into a unicode object, in "disable_unicode" mode + if not util.py3k and isinstance(source_encoding, unicode): + source_encoding = source_encoding.encode(source_encoding) + + buf = util.FastEncodingBuffer() printer = PythonPrinter(buf) @@ -571,8 +579,9 @@ class _GenerateRenderMethod(object): if not self.in_def and len(self.identifiers.locally_assigned) > 0: # if we are the "template" def, fudge locally declared/modified variables into the "__M_locals" dictionary, # which is used for def calls within the same template, to simulate "enclosing scope" - self.printer.writeline('__M_locals.update(__M_dict_builtin([(__M_key, __M_locals_builtin()[__M_key]) for __M_key in [%s] if __M_key in __M_locals_builtin()]))' % ','.join([repr(x) for x in node.declared_identifiers()])) - + self.printer.writeline('__M_locals_builtin_stored = __M_locals_builtin()') + self.printer.writeline('__M_locals.update(__M_dict_builtin([(__M_key, __M_locals_builtin_stored[__M_key]) for __M_key in [%s] if __M_key in __M_locals_builtin_stored]))' % ','.join([repr(x) for x in node.declared_identifiers()])) + def visitIncludeTag(self, node): self.write_source_comment(node) args = node.attributes.get('args') diff --git a/mako/exceptions.py b/mako/exceptions.py index dcd6a64..b8b7747 100644 --- a/mako/exceptions.py +++ b/mako/exceptions.py @@ -19,7 +19,9 @@ def _format_filepos(lineno, pos, filename): if filename is None: return " at line: %d char: %d" % (lineno, pos) else: - return " in file '%s' at line: %d char: %d" % (filename, lineno, pos) + return " in file '%s' at line: %d char: %d" % (filename, lineno, pos) + + class CompileException(MakoException): def __init__(self, message, source, lineno, pos, filename): MakoException.__init__(self, message + _format_filepos(lineno, pos, filename)) @@ -35,6 +37,9 @@ class SyntaxException(MakoException): self.pos = pos self.filename = filename self.source = source + +class UnsupportedError(MakoException): + """raised when a retired feature is used.""" class TemplateLookupException(MakoException): pass @@ -130,17 +135,19 @@ class RichTraceback(object): template_filename = info.template_filename or filename except KeyError: # A normal .py file (not a Template) - try: - fp = open(filename) - encoding = util.parse_encoding(fp) - fp.close() - except IOError: - encoding = None - if encoding: - line = line.decode(encoding) - else: - line = line.decode('ascii', 'replace') - new_trcback.append((filename, lineno, function, line, None, None, None, None)) + if not util.py3k: + try: + fp = open(filename, 'rb') + encoding = util.parse_encoding(fp) + fp.close() + except IOError: + encoding = None + if encoding: + line = line.decode(encoding) + else: + line = line.decode('ascii', 'replace') + new_trcback.append((filename, lineno, function, line, + None, None, None, None)) continue template_ln = module_ln = 1 @@ -161,7 +168,9 @@ class RichTraceback(object): template_line = template_lines[template_ln - 1] else: template_line = None - new_trcback.append((filename, lineno, function, line, template_filename, template_ln, template_line, template_source)) + new_trcback.append((filename, lineno, function, + line, template_filename, template_ln, + template_line, template_source)) if not self.source: for l in range(len(new_trcback)-1, 0, -1): if new_trcback[l][5]: @@ -171,7 +180,7 @@ class RichTraceback(object): else: try: # A normal .py file (not a Template) - fp = open(new_trcback[-1][0]) + fp = open(new_trcback[-1][0], 'rb') encoding = util.parse_encoding(fp) fp.seek(0) self.source = fp.read() diff --git a/mako/filters.py b/mako/filters.py index 9a5b21d..5f35714 100644 --- a/mako/filters.py +++ b/mako/filters.py @@ -7,6 +7,7 @@ import re, cgi, urllib, htmlentitydefs, codecs from StringIO import StringIO +from mako import util xml_escapes = { '&' : '&', @@ -166,5 +167,9 @@ DEFAULT_ESCAPES = { 'str':'str', 'n':'n' } - + +if util.py3k: + DEFAULT_ESCAPES.update({ + 'unicode':'str' + }) diff --git a/mako/lexer.py b/mako/lexer.py index caf295b..5e4a3bc 100644 --- a/mako/lexer.py +++ b/mako/lexer.py @@ -7,7 +7,7 @@ """provides the Lexer class for parsing template strings into parse trees.""" import re, codecs -from mako import parsetree, exceptions +from mako import parsetree, exceptions, util from mako.pygen import adjust_whitespace _regexp_cache = {} @@ -27,6 +27,12 @@ class Lexer(object): self.control_line = [] self.disable_unicode = disable_unicode self.encoding = input_encoding + + if util.py3k and disable_unicode: + raise exceptions.UnsupportedError( + "Mako for Python 3 does not " + "support disabling Unicode") + if preprocessor is None: self.preprocessor = [] elif not hasattr(preprocessor, '__iter__'): @@ -42,10 +48,8 @@ class Lexer(object): 'filename':self.filename} def match(self, regexp, flags=None): - """match the given regular expression string and flags to the current text position. + """compile the given regexp, cache the reg, and call match_reg().""" - if a match occurs, update the current text and line position.""" - mp = self.match_position try: reg = _regexp_cache[(regexp, flags)] except KeyError: @@ -54,6 +58,17 @@ class Lexer(object): else: reg = re.compile(regexp) _regexp_cache[(regexp, flags)] = reg + + return self.match_reg(reg) + + def match_reg(self, reg): + """match the given regular expression object to the current text position. + + if a match occurs, update the current text and line position. + + """ + + mp = self.match_position match = reg.match(self.text, self.match_position) if match: @@ -128,45 +143,61 @@ class Lexer(object): (node.keyword, self.control_line[-1].keyword), **self.exception_kwargs) - def parse(self): - for preproc in self.preprocessor: - self.text = preproc(self.text) - - if not isinstance(self.text, unicode) and self.text.startswith(codecs.BOM_UTF8): - self.text = self.text[len(codecs.BOM_UTF8):] + _coding_re = re.compile(r'#.*coding[:=]\s*([-\w.]+).*\r?\n') + + def decode_raw_stream(self, text, decode_raw, known_encoding, filename): + """given string/unicode or bytes/string, determine encoding + from magic encoding comment, return body as unicode + or raw if decode_raw=False + + """ + if isinstance(text, unicode): + m = self._coding_re.match(text) + encoding = m and m.group(1) or known_encoding or 'ascii' + return encoding, text + + if text.startswith(codecs.BOM_UTF8): + text = text[len(codecs.BOM_UTF8):] parsed_encoding = 'utf-8' - me = self.match_encoding() - if me is not None and me != 'utf-8': + m = self._coding_re.match(text.decode('utf-8', 'ignore')) + if m is not None and m.group(1) != 'utf-8': raise exceptions.CompileException( "Found utf-8 BOM in file, with conflicting " - "magic encoding comment of '%s'" % me, - self.text.decode('utf-8', 'ignore'), - 0, 0, self.filename) + "magic encoding comment of '%s'" % m.group(1), + text.decode('utf-8', 'ignore'), + 0, 0, filename) else: - parsed_encoding = self.match_encoding() - - if parsed_encoding: - self.encoding = parsed_encoding - - if not self.disable_unicode and not isinstance(self.text, unicode): - if self.encoding: - try: - self.text = self.text.decode(self.encoding) - except UnicodeDecodeError, e: - raise exceptions.CompileException( - "Unicode decode operation of encoding '%s' failed" % - self.encoding, - self.text.decode('utf-8', 'ignore'), - 0, 0, self.filename) + m = self._coding_re.match(text.decode('utf-8', 'ignore')) + if m: + parsed_encoding = m.group(1) else: - try: - self.text = self.text.decode() - except UnicodeDecodeError, e: - raise exceptions.CompileException( - "Could not read template using encoding of 'ascii'. " - "Did you forget a magic encoding comment?", - self.text.decode('utf-8', 'ignore'), 0, 0, self.filename) + parsed_encoding = known_encoding or 'ascii' + + if decode_raw: + try: + text = text.decode(parsed_encoding) + except UnicodeDecodeError, e: + raise exceptions.CompileException( + "Unicode decode operation of encoding '%s' failed" % + parsed_encoding, + text.decode('utf-8', 'ignore'), + 0, 0, filename) + + return parsed_encoding, text + + def parse(self): + self.encoding, self.text = self.decode_raw_stream(self.text, + not self.disable_unicode, + self.encoding, + self.filename,) + for preproc in self.preprocessor: + self.text = preproc(self.text) + + # push the match marker past the + # encoding comment. + self.match_reg(self._coding_re) + self.textlength = len(self.text) while (True): @@ -206,13 +237,6 @@ class Lexer(object): self.control_line[-1].pos, self.filename) return self.template - def match_encoding(self): - match = self.match(r'#.*coding[:=]\s*([-\w.]+).*\r?\n') - if match: - return match.group(1) - else: - return None - def match_tag_start(self): match = self.match(r''' \<% # opening tag diff --git a/mako/parsetree.py b/mako/parsetree.py index 48e2d10..3a273ac 100644 --- a/mako/parsetree.py +++ b/mako/parsetree.py @@ -235,11 +235,13 @@ class Tag(Node): attributes - raw dictionary of attribute key/value pairs - expressions - a set of identifiers that are legal attributes, which can also contain embedded expressions + expressions - a set of identifiers that are legal attributes, + which can also contain embedded expressions - nonexpressions - a set of identifiers that are legal attributes, which cannot contain embedded expressions + nonexpressions - a set of identifiers that are legal attributes, + which cannot contain embedded expressions - **kwargs - other arguments passed to the Node superclass (lineno, pos) + \**kwargs - other arguments passed to the Node superclass (lineno, pos) """ super(Tag, self).__init__(**kwargs) @@ -270,7 +272,9 @@ class Tag(Node): m = re.match(r'^\${(.+?)}$', x) if m: code = ast.PythonCode(m.group(1), **self.exception_kwargs) - undeclared_identifiers = undeclared_identifiers.union(code.undeclared_identifiers) + undeclared_identifiers = undeclared_identifiers.union( + code.undeclared_identifiers + ) expr.append("(%s)" % m.group(1)) else: if x: @@ -279,11 +283,15 @@ class Tag(Node): elif key in nonexpressions: if re.search(r'${.+?}', self.attributes[key]): raise exceptions.CompileException( - "Attibute '%s' in tag '%s' does not allow embedded expressions" % (key, self.keyword), + "Attibute '%s' in tag '%s' does not allow embedded " + "expressions" % (key, self.keyword), **self.exception_kwargs) self.parsed_attributes[key] = repr(self.attributes[key]) else: - raise exceptions.CompileException("Invalid attribute for tag '%s': '%s'" %(self.keyword, key), **self.exception_kwargs) + raise exceptions.CompileException( + "Invalid attribute for tag '%s': '%s'" % + (self.keyword, key), + **self.exception_kwargs) self.expression_undeclared_identifiers = undeclared_identifiers def declared_identifiers(self): @@ -297,15 +305,21 @@ class Tag(Node): self.keyword, util.sorted_dict_repr(self.attributes), (self.lineno, self.pos), - [repr(x) for x in self.nodes] + self.nodes ) class IncludeTag(Tag): __keyword__ = 'include' def __init__(self, keyword, attributes, **kwargs): - super(IncludeTag, self).__init__(keyword, attributes, ('file', 'import', 'args'), (), ('file',), **kwargs) - self.page_args = ast.PythonCode("__DUMMY(%s)" % attributes.get('args', ''), **self.exception_kwargs) + super(IncludeTag, self).__init__( + keyword, + attributes, + ('file', 'import', 'args'), + (), ('file',), **kwargs) + self.page_args = ast.PythonCode( + "__DUMMY(%s)" % attributes.get('args', ''), + **self.exception_kwargs) def declared_identifiers(self): return [] @@ -318,10 +332,19 @@ class NamespaceTag(Tag): __keyword__ = 'namespace' def __init__(self, keyword, attributes, **kwargs): - super(NamespaceTag, self).__init__(keyword, attributes, (), ('name','inheritable','file','import','module'), (), **kwargs) + super(NamespaceTag, self).__init__( + keyword, attributes, + (), + ('name','inheritable', + 'file','import','module'), + (), **kwargs) + self.name = attributes.get('name', '__anon_%s' % hex(abs(id(self)))) if not 'name' in attributes and not 'import' in attributes: - raise exceptions.CompileException("'name' and/or 'import' attributes are required for <%namespace>", **self.exception_kwargs) + raise exceptions.CompileException( + "'name' and/or 'import' attributes are required " + "for <%namespace>", + **self.exception_kwargs) def declared_identifiers(self): return [] @@ -330,8 +353,13 @@ class TextTag(Tag): __keyword__ = 'text' def __init__(self, keyword, attributes, **kwargs): - super(TextTag, self).__init__(keyword, attributes, (), ('filter'), (), **kwargs) - self.filter_args = ast.ArgumentList(attributes.get('filter', ''), **self.exception_kwargs) + super(TextTag, self).__init__( + keyword, + attributes, (), + ('filter'), (), **kwargs) + self.filter_args = ast.ArgumentList( + attributes.get('filter', ''), + **self.exception_kwargs) class DefTag(Tag): __keyword__ = 'def' @@ -340,17 +368,22 @@ class DefTag(Tag): super(DefTag, self).__init__( keyword, attributes, - ('buffered', 'cached', 'cache_key', 'cache_timeout', 'cache_type', 'cache_dir', 'cache_url'), + ('buffered', 'cached', 'cache_key', 'cache_timeout', + 'cache_type', 'cache_dir', 'cache_url'), ('name','filter', 'decorator'), ('name',), **kwargs) name = attributes['name'] if re.match(r'^[\w_]+$',name): - raise exceptions.CompileException("Missing parenthesis in %def", **self.exception_kwargs) + raise exceptions.CompileException( + "Missing parenthesis in %def", + **self.exception_kwargs) self.function_decl = ast.FunctionDecl("def " + name + ":pass", **self.exception_kwargs) self.name = self.function_decl.funcname self.decorator = attributes.get('decorator', '') - self.filter_args = ast.ArgumentList(attributes.get('filter', ''), **self.exception_kwargs) + self.filter_args = ast.ArgumentList( + attributes.get('filter', ''), + **self.exception_kwargs) def declared_identifiers(self): return self.function_decl.argnames @@ -359,13 +392,17 @@ class DefTag(Tag): res = [] for c in self.function_decl.defaults: res += list(ast.PythonCode(c, **self.exception_kwargs).undeclared_identifiers) - return res + list(self.filter_args.undeclared_identifiers.difference(set(filters.DEFAULT_ESCAPES.keys()))) + return res + list(self.filter_args.\ + undeclared_identifiers.\ + difference(filters.DEFAULT_ESCAPES.keys()) + ) class CallTag(Tag): __keyword__ = 'call' def __init__(self, keyword, attributes, **kwargs): - super(CallTag, self).__init__(keyword, attributes, ('args'), ('expr',), ('expr',), **kwargs) + super(CallTag, self).__init__(keyword, attributes, + ('args'), ('expr',), ('expr',), **kwargs) self.expression = attributes['expr'] self.code = ast.PythonCode(self.expression, **self.exception_kwargs) self.body_decl = ast.FunctionArgs(attributes.get('args', ''), **self.exception_kwargs) @@ -386,9 +423,18 @@ class CallNamespaceTag(Tag): (), (), **kwargs) - self.expression = "%s.%s(%s)" % (namespace, defname, ",".join(["%s=%s" % (k, v) for k, v in self.parsed_attributes.iteritems() if k != 'args'])) + + self.expression = "%s.%s(%s)" % ( + namespace, + defname, + ",".join(["%s=%s" % (k, v) for k, v in + self.parsed_attributes.iteritems() + if k != 'args']) + ) self.code = ast.PythonCode(self.expression, **self.exception_kwargs) - self.body_decl = ast.FunctionArgs(attributes.get('args', ''), **self.exception_kwargs) + self.body_decl = ast.FunctionArgs( + attributes.get('args', ''), + **self.exception_kwargs) def declared_identifiers(self): return self.code.declared_identifiers.union(self.body_decl.argnames) @@ -400,7 +446,9 @@ class InheritTag(Tag): __keyword__ = 'inherit' def __init__(self, keyword, attributes, **kwargs): - super(InheritTag, self).__init__(keyword, attributes, ('file',), (), ('file',), **kwargs) + super(InheritTag, self).__init__( + keyword, attributes, + ('file',), (), ('file',), **kwargs) class PageTag(Tag): __keyword__ = 'page' @@ -409,12 +457,16 @@ class PageTag(Tag): super(PageTag, self).__init__( keyword, attributes, - ('cached', 'cache_key', 'cache_timeout', 'cache_type', 'cache_dir', 'cache_url', 'args', 'expression_filter'), + ('cached', 'cache_key', 'cache_timeout', + 'cache_type', 'cache_dir', 'cache_url', + 'args', 'expression_filter'), (), (), **kwargs) self.body_decl = ast.FunctionArgs(attributes.get('args', ''), **self.exception_kwargs) - self.filter_args = ast.ArgumentList(attributes.get('expression_filter', ''), **self.exception_kwargs) + self.filter_args = ast.ArgumentList( + attributes.get('expression_filter', ''), + **self.exception_kwargs) def declared_identifiers(self): return self.body_decl.argnames diff --git a/mako/pygen.py b/mako/pygen.py index 914443b..aada94d 100644 --- a/mako/pygen.py +++ b/mako/pygen.py @@ -8,6 +8,7 @@ import re, string from StringIO import StringIO +from mako import exceptions class PythonPrinter(object): def __init__(self, stream): @@ -84,7 +85,7 @@ class PythonPrinter(object): # probably put extra closures - the resulting # module wont compile. if len(self.indent_detail) == 0: - raise "Too many whitespace closures" + raise exceptions.SyntaxException("Too many whitespace closures") self.indent_detail.pop() if line is None: @@ -200,7 +201,7 @@ class PythonPrinter(object): if self._in_multi_line(entry): self.stream.write(entry + "\n") else: - entry = string.expandtabs(entry) + entry = entry.expandtabs() if stripspace is None and re.search(r"^[ \t]*[^# \t]", entry): stripspace = re.match(r"^([ \t]*)", entry).group(1) self.stream.write(self._indent_line(entry, stripspace) + "\n") @@ -260,7 +261,7 @@ def adjust_whitespace(text): if in_multi_line(line): lines.append(line) else: - line = string.expandtabs(line) + line = line.expandtabs() if stripspace is None and re.search(r"^[ \t]*[^# \t]", line): stripspace = re.match(r"^([ \t]*)", line).group(1) lines.append(_indent_line(line, stripspace)) diff --git a/mako/pyparser.py b/mako/pyparser.py index 7ae3a4f..b90278e 100644 --- a/mako/pyparser.py +++ b/mako/pyparser.py @@ -12,9 +12,23 @@ module is used. from StringIO import StringIO from mako import exceptions, util +import operator + +if util.py3k: + # words that cannot be assigned to (notably + # smaller than the total keys in __builtins__) + reserved = set(['True', 'False', 'None', 'print']) + + # the "id" attribute on a function node + arg_id = operator.attrgetter('arg') +else: + # words that cannot be assigned to (notably + # smaller than the total keys in __builtins__) + reserved = set(['True', 'False', 'None']) + + # the "id" attribute on a function node + arg_id = operator.attrgetter('id') -# words that cannot be assigned to (notably smaller than the total keys in __builtins__) -reserved = set(['True', 'False', 'None']) try: import _ast @@ -63,6 +77,18 @@ if _ast: for n in node.targets: self.visit(n) self.in_assign_targets = in_a + + if util.py3k: + # ExceptHandler is in Python 2, but this + # block only works in Python 3 (and is required there) + def visit_ExceptHandler(self, node): + if node.name is not None: + self._add_declared(node.name) + if node.type is not None: + self.listener.undeclared_identifiers.add(node.type.id) + for statement in node.body: + self.visit(statement) + def visit_FunctionDef(self, node): self._add_declared(node.name) # push function state onto stack. dont log any @@ -72,17 +98,19 @@ if _ast: saved = {} inf = self.in_function self.in_function = True + for arg in node.args.args: - if arg.id in self.local_ident_stack: - saved[arg.id] = True + if arg_id(arg) in self.local_ident_stack: + saved[arg_id(arg)] = True else: - self.local_ident_stack[arg.id] = True + self.local_ident_stack[arg_id(arg)] = True for n in node.body: self.visit(n) self.in_function = inf for arg in node.args.args: - if arg.id not in saved: - del self.local_ident_stack[arg.id] + if arg_id(arg) not in saved: + del self.local_ident_stack[arg_id(arg)] + def visit_For(self, node): # flip around visit self.visit(node.iter) @@ -94,7 +122,9 @@ if _ast: def visit_Name(self, node): if isinstance(node.ctx, _ast.Store): self._add_declared(node.id) - if node.id not in reserved and node.id not in self.listener.declared_identifiers and node.id not in self.local_ident_stack: + if node.id not in reserved and \ + node.id not in self.listener.declared_identifiers and \ + node.id not in self.local_ident_stack: self.listener.undeclared_identifiers.add(node.id) def visit_Import(self, node): for name in node.names: @@ -128,9 +158,10 @@ if _ast: def __init__(self, listener, **exception_kwargs): self.listener = listener self.exception_kwargs = exception_kwargs + def visit_FunctionDef(self, node): self.listener.funcname = node.name - argnames = [arg.id for arg in node.args.args] + argnames = [arg_id(arg) for arg in node.args.args] if node.args.vararg: argnames.append(node.args.vararg) if node.args.kwarg: diff --git a/mako/runtime.py b/mako/runtime.py index 583b79e..95828b4 100644 --- a/mako/runtime.py +++ b/mako/runtime.py @@ -18,6 +18,7 @@ class Context(object): self._data.update(data) self._kwargs = data.copy() self._with_template = None + self._outputting_as_unicode = None self.namespaces = {} # "capture" function which proxies to the generic "capture" function @@ -26,8 +27,13 @@ class Context(object): # "caller" stack used by def calls with content self.caller_stack = self._data['caller'] = CallerStack() - lookup = property(lambda self:self._with_template.lookup) - kwargs = property(lambda self:self._kwargs.copy()) + @property + def lookup(self): + return self._with_template.lookup + + @property + def kwargs(self): + return self._kwargs.copy() def push_caller(self, caller): self.caller_stack.append(caller) @@ -87,6 +93,7 @@ class Context(object): c._orig = self._orig c._kwargs = self._kwargs c._with_template = self._with_template + c._outputting_as_unicode = self._outputting_as_unicode c.namespaces = self.namespaces c.caller_stack = self.caller_stack return c @@ -368,6 +375,7 @@ def _render(template, callable_, args, data, as_unicode=False): else: buf = util.StringIO() context = Context(buf, **data) + context._outputting_as_unicode = as_unicode context._with_template = template _render_context(template, callable_, context, *args, **_kwargs_for_callable(callable_, data)) return context._pop_buffer().getvalue() @@ -404,19 +412,24 @@ def _exec_template(callable_, context, args=None, kwargs=None): try: callable_(context, *args, **kwargs) except Exception, e: - error = e + _render_error(template, context, e) except: e = sys.exc_info()[0] - error = e - if error: - if template.error_handler: - result = template.error_handler(context, error) - if not result: - raise error - else: - error_template = exceptions.html_error_template() - context._buffer_stack[:] = [util.FastEncodingBuffer(error_template.output_encoding, error_template.encoding_errors)] - context._with_template = error_template - error_template.render_context(context, error=error) + _render_error(template, context, e) else: callable_(context, *args, **kwargs) + + +def _render_error(template, context, error): + if template.error_handler: + result = template.error_handler(context, error) + if not result: + raise error + else: + error_template = exceptions.html_error_template() + if context._outputting_as_unicode: + context._buffer_stack[:] = [util.FastEncodingBuffer(unicode=True)] + else: + context._buffer_stack[:] = [util.FastEncodingBuffer(error_template.output_encoding, error_template.encoding_errors)] + context._with_template = error_template + error_template.render_context(context, error=error) diff --git a/mako/template.py b/mako/template.py index 87f6898..9104683 100644 --- a/mako/template.py +++ b/mako/template.py @@ -66,8 +66,14 @@ class Template(object): self.output_encoding = output_encoding self.encoding_errors = encoding_errors self.disable_unicode = disable_unicode + + if util.py3k and disable_unicode: + raise exceptions.UnsupportedError( + "Mako for Python 3 does not " + "support disabling Unicode") + if default_filters is None: - if self.disable_unicode: + if util.py3k or self.disable_unicode: self.default_filters = ['str'] else: self.default_filters = ['unicode'] @@ -108,18 +114,18 @@ class Template(object): os.stat(path)[stat.ST_MTIME] < filemtime: _compile_module_file( self, - file(filename).read(), + open(filename, 'rb').read(), filename, path) - module = imp.load_source(self.module_id, path, file(path)) + module = imp.load_source(self.module_id, path, open(path, 'rb')) del sys.modules[self.module_id] if module._magic_number != codegen.MAGIC_NUMBER: _compile_module_file( self, - file(filename).read(), + open(filename, 'rb').read(), filename, path) - module = imp.load_source(self.module_id, path, file(path)) + module = imp.load_source(self.module_id, path, open(path, 'rb')) del sys.modules[self.module_id] ModuleInfo(module, path, self, filename, None, None) else: @@ -127,7 +133,7 @@ class Template(object): # in memory (code, module) = _compile_text( self, - file(filename).read(), + open(filename, 'rb').read(), filename) self._source = None self._code = code @@ -318,7 +324,7 @@ class ModuleInfo(object): if self.module_source is not None: return self.module_source else: - return file(self.module_filename).read() + return open(self.module_filename).read() @property def source(self): @@ -331,10 +337,10 @@ class ModuleInfo(object): return self.template_source else: if self.module._source_encoding: - return file(self.template_filename).read().\ + return open(self.template_filename, 'rb').read().\ decode(self.module._source_encoding) else: - return file(self.template_filename).read() + return open(self.template_filename).read() def _compile_text(template, text, filename): identifier = template.module_id @@ -355,7 +361,7 @@ def _compile_text(template, text, filename): generate_magic_comment=template.disable_unicode) cid = identifier - if isinstance(cid, unicode): + if not util.py3k and isinstance(cid, unicode): cid = cid.encode() module = types.ModuleType(cid) code = compile(source, cid, 'exec') diff --git a/mako/util.py b/mako/util.py index 88076c3..dcac5d7 100644 --- a/mako/util.py +++ b/mako/util.py @@ -6,16 +6,20 @@ import sys -try: - from cStringIO import StringIO -except: - from StringIO import StringIO py3k = getattr(sys, 'py3kwarning', False) or sys.version_info >= (3, 0) jython = sys.platform.startswith('java') win32 = sys.platform.startswith('win') -import codecs, re, weakref, os, time +if py3k: + from io import StringIO +else: + try: + from cStringIO import StringIO + except: + from StringIO import StringIO + +import codecs, re, weakref, os, time, operator try: import threading @@ -60,6 +64,10 @@ def to_list(x, default=None): else: return x + + + + class SetLikeDict(dict): """a dictionary that has some setlike methods on it""" def union(self, other): @@ -84,6 +92,9 @@ class FastEncodingBuffer(object): self.unicode = unicode self.errors = errors self.write = self.data.append + + def truncate(self): + self.data =[] def getvalue(self): if self.encoding: @@ -138,8 +149,8 @@ class LRUCache(dict): def _manage_size(self): while len(self) > self.capacity + self.capacity * self.threshold: - bytime = dict.values(self) - bytime.sort(lambda a, b: cmp(b.timestamp, a.timestamp)) + bytime = sorted(dict.values(self), + key=operator.attrgetter('timestamp'), reverse=True) for item in bytime[self.capacity:]: try: del self[item.key] @@ -154,13 +165,13 @@ _PYTHON_MAGIC_COMMENT_re = re.compile( re.VERBOSE) def parse_encoding(fp): - """Deduce the encoding of a source file from magic comment. + """Deduce the encoding of a Python source file (binary mode) from magic comment. It does this in the same way as the `Python interpreter`__ .. __: http://docs.python.org/ref/encodings.html - The ``fp`` argument should be a seekable file object. + The ``fp`` argument should be a seekable file object in binary mode. """ pos = fp.tell() fp.seek(0) @@ -170,11 +181,11 @@ def parse_encoding(fp): if has_bom: line1 = line1[len(codecs.BOM_UTF8):] - m = _PYTHON_MAGIC_COMMENT_re.match(line1) + m = _PYTHON_MAGIC_COMMENT_re.match(line1.decode('ascii', 'ignore')) if not m: try: import parser - parser.suite(line1) + parser.suite(line1.decode('ascii', 'ignore')) except (ImportError, SyntaxError): # Either it's a real syntax error, in which case the source # is not valid python source, or line2 is a continuation of @@ -183,7 +194,7 @@ def parse_encoding(fp): pass else: line2 = fp.readline() - m = _PYTHON_MAGIC_COMMENT_re.match(line2) + m = _PYTHON_MAGIC_COMMENT_re.match(line2.decode('ascii', 'ignore')) if has_bom: if m: diff --git a/setup.py b/setup.py index caed701..dda072f 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,13 @@ from setuptools import setup, find_packages import os import re +import sys + +extra = {} +if sys.version_info >= (3, 0): + extra.update( + use_2to3=True, + ) v = file(os.path.join(os.path.dirname(__file__), 'mako', '__init__.py')) VERSION = re.compile(r".*__version__ = '(.*?)'", re.S).match(v.read()).group(1) @@ -25,10 +32,11 @@ SVN version: """, classifiers=[ - "Development Status :: 5 - Production/Stable", + 'Development Status :: 5 - Production/Stable', 'Environment :: Web Environment', 'Intended Audience :: Developers', 'Programming Language :: Python', + 'Programming Language :: Python :: 3', 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', ], keywords='wsgi myghty mako', diff --git a/test/__init__.py b/test/__init__.py index c8c2a4d..1bad221 100644 --- a/test/__init__.py +++ b/test/__init__.py @@ -15,17 +15,26 @@ class TemplateTest(unittest.TestCase): return Template(uri=filename, filename=filepath, module_directory=module_base, **kw) def _file_path(self, filename): + name, ext = os.path.splitext(filename) + + if py3k: + py3k_path = os.path.join(template_base, name + "_py3k" + ext) + if os.path.exists(py3k_path): + return py3k_path + return os.path.join(template_base, filename) def _do_file_test(self, filename, expected, filters=None, unicode_=True, template_args=None, **kw): t1 = self._file_template(filename, **kw) - self._do_test(t1, expected, filters=filters, unicode_=unicode_, template_args=template_args) + self._do_test(t1, expected, filters=filters, + unicode_=unicode_, template_args=template_args) def _do_memory_test(self, source, expected, filters=None, unicode_=True, template_args=None, **kw): t1 = Template(text=source, **kw) - self._do_test(t1, expected, filters=filters, unicode_=unicode_, template_args=template_args) + self._do_test(t1, expected, filters=filters, + unicode_=unicode_, template_args=template_args) def _do_test(self, template, expected, filters=None, template_args=None, unicode_=True): if template_args is None: diff --git a/test/templates/chs_unicode_py3k.html b/test/templates/chs_unicode_py3k.html new file mode 100644 index 0000000..35b888d --- /dev/null +++ b/test/templates/chs_unicode_py3k.html @@ -0,0 +1,11 @@ +## -*- encoding:utf8 -*- +<% + msg = 'æ–°ä¸å›½çš„主å¸' +%> + +<%def name="welcome(who, place='北京')"> +Welcome ${who} to ${place}. +</%def> + +${name} 是 ${msg}<br/> +${welcome('ä½ ')} diff --git a/test/templates/read_unicode_py3k.html b/test/templates/read_unicode_py3k.html new file mode 100644 index 0000000..380d356 --- /dev/null +++ b/test/templates/read_unicode_py3k.html @@ -0,0 +1,10 @@ +<% +try: + file_content = open(path) +except: + raise "Should never execute here" +doc_content = ''.join(file_content.readlines()) +file_content.close() +%> + +${bytes(doc_content, encoding='utf-8')} diff --git a/test/templates/unicode_arguments_py3k.html b/test/templates/unicode_arguments_py3k.html new file mode 100644 index 0000000..97e6d3a --- /dev/null +++ b/test/templates/unicode_arguments_py3k.html @@ -0,0 +1,10 @@ +# coding: utf-8 + +<%def name="my_def(x)"> + x is: ${x} +</%def> + +${my_def('drôle de petit voix m’a réveillé')} +<%self:my_def x='drôle de petit voix m’a réveillé'/> +<%self:my_def x="${'drôle de petit voix m’a réveillé'}"/> +<%call expr="my_def('drôle de petit voix m’a réveillé')"/> diff --git a/test/templates/unicode_code_py3k.html b/test/templates/unicode_code_py3k.html new file mode 100644 index 0000000..76ed9cc --- /dev/null +++ b/test/templates/unicode_code_py3k.html @@ -0,0 +1,7 @@ +## -*- coding: utf-8 -*- +<% + x = "drôle de petit voix m’a réveillé." +%> +% if x=="drôle de petit voix m’a réveillé.": + hi, ${x} +% endif diff --git a/test/templates/unicode_expr_py3k.html b/test/templates/unicode_expr_py3k.html new file mode 100644 index 0000000..4898257 --- /dev/null +++ b/test/templates/unicode_expr_py3k.html @@ -0,0 +1,2 @@ +## -*- coding: utf-8 -*- +${"Alors vous imaginez ma surprise, au lever du jour, quand une drôle de petit voix m’a réveillé. Elle disait: « S’il vous plaît… dessine-moi un mouton! »"} diff --git a/test/test_ast.py b/test/test_ast.py index ed21456..bfdfd90 100644 --- a/test/test_ast.py +++ b/test/test_ast.py @@ -1,14 +1,12 @@ import unittest from mako import ast, exceptions, pyparser, util +from test import eq_ exception_kwargs = {'source':'', 'lineno':0, 'pos':0, 'filename':''} class AstParseTest(unittest.TestCase): - def setUp(self): - pass - def tearDown(self): - pass + def test_locate_identifiers(self): """test the location of identifiers in a python code string""" code = """ @@ -21,8 +19,8 @@ foo.hoho.lala.bar = 7 + gah.blah + u + blah for lar in (1,2,3): gh = 5 x = 12 -print "hello world, ", a, b -print "Another expr", c +("hello world, ", a, b) +("Another expr", c) """ parsed = ast.PythonCode(code, **exception_kwargs) assert parsed.declared_identifiers == set(['a','b','c', 'g', 'h', 'i', 'u', 'k', 'j', 'gh', 'lar', 'x']) @@ -51,7 +49,7 @@ for x in data: code = """ x = x + 5 for y in range(1, y): - print "hi" + ("hi",) [z for z in range(1, z)] (q for q in range (1, q)) """ @@ -59,9 +57,17 @@ for y in range(1, y): assert parsed.undeclared_identifiers == set(['x', 'y', 'z', 'q', 'range']) def test_locate_identifiers_4(self): - code = """ + if util.py3k: + code = """ +x = 5 +(y, ) +def mydef(mydefarg): + print("mda is", mydefarg) +""" + else: + code = """ x = 5 -print y +(y, ) def mydef(mydefarg): print "mda is", mydefarg """ @@ -70,7 +76,16 @@ def mydef(mydefarg): assert parsed.declared_identifiers == set(['mydef', 'x']) def test_locate_identifiers_5(self): - code = """ + if util.py3k: + code = """ +try: + print(x) +except: + print(y) +""" + else: + + code = """ try: print x except: @@ -86,8 +101,15 @@ def foo(): """ parsed = ast.PythonCode(code, **exception_kwargs) assert parsed.undeclared_identifiers == set(['bar']) - - code = """ + + if util.py3k: + code = """ +def lala(x, y): + return x, y, z +print(x) +""" + else: + code = """ def lala(x, y): return x, y, z print x @@ -95,8 +117,17 @@ print x parsed = ast.PythonCode(code, **exception_kwargs) assert parsed.undeclared_identifiers == set(['z', 'x']) assert parsed.declared_identifiers == set(['lala']) - - code = """ + + if util.py3k: + code = """ +def lala(x, y): + def hoho(): + def bar(): + z = 7 +print(z) +""" + else: + code = """ def lala(x, y): def hoho(): def bar(): @@ -131,11 +162,7 @@ class Hi(object): from foo import * import x as bar """ - try: - parsed = ast.PythonCode(code, **exception_kwargs) - assert False - except exceptions.CompileException, e: - assert str(e).startswith("'import *' is not supported") + self.assertRaises(exceptions.CompileException, ast.PythonCode, code, **exception_kwargs) def test_python_fragment(self): parsed = ast.PythonFragment("for x in foo:", **exception_kwargs) @@ -144,9 +171,12 @@ import x as bar parsed = ast.PythonFragment("try:", **exception_kwargs) - parsed = ast.PythonFragment("except MyException, e:", **exception_kwargs) - assert parsed.declared_identifiers == set(['e']) - assert parsed.undeclared_identifiers == set(['MyException']) + if util.py3k: + parsed = ast.PythonFragment("except MyException as e:", **exception_kwargs) + else: + parsed = ast.PythonFragment("except MyException, e:", **exception_kwargs) + eq_(parsed.declared_identifiers, set(['e'])) + eq_(parsed.undeclared_identifiers, set(['MyException'])) def test_argument_list(self): parsed = ast.ArgumentList("3, 5, 'hi', x+5, context.get('lala')", **exception_kwargs) @@ -184,8 +214,6 @@ import x as bar code = "str((x+7*y) / foo.bar(5,6)) + lala('ho')" astnode = pyparser.parse(code) newcode = pyparser.ExpressionGenerator(astnode).value() - #print "newcode:" + newcode - #print "result:" + eval(code, local_dict) assert (eval(code, local_dict) == eval(newcode, local_dict)) a = ["one", "two", "three"] @@ -195,8 +223,6 @@ import x as bar code = "a[2] + hoho['somevalue'] + repr(g[3:5]) + repr(g[3:]) + repr(g[:5])" astnode = pyparser.parse(code) newcode = pyparser.ExpressionGenerator(astnode).value() - #print newcode - #print "result:", eval(code, local_dict) assert(eval(code, local_dict) == eval(newcode, local_dict)) local_dict={'f':lambda :9, 'x':7} @@ -209,7 +235,6 @@ import x as bar local_dict={} astnode = pyparser.parse(code) newcode = pyparser.ExpressionGenerator(astnode).value() - #print code, newcode assert(eval(code, local_dict)) == eval(newcode, local_dict), "%s != %s" % (code, newcode) diff --git a/test/test_def.py b/test/test_def.py index 6cb1fdf..1f7a39a 100644 --- a/test/test_def.py +++ b/test/test_def.py @@ -1,9 +1,9 @@ from mako.template import Template from mako import lookup -import unittest +from test import TemplateTest from util import flatten_result, result_lines -class DefTest(unittest.TestCase): +class DefTest(TemplateTest): def test_def_noargs(self): template = Template(""" @@ -61,6 +61,7 @@ class DefTest(unittest.TestCase): def test_toplevel(self): """test calling a def from the top level""" + template = Template(""" this is the body @@ -73,15 +74,20 @@ class DefTest(unittest.TestCase): this is b, ${x} ${y} </%def> - """, output_encoding='utf-8') - assert flatten_result(template.get_def("a").render()) == "this is a" - assert flatten_result(template.get_def("b").render(x=10, y=15)) == "this is b, 10 15" - assert flatten_result(template.get_def("body").render()) == "this is the body" + """) + + self._do_test(template.get_def("a"), "this is a", filters=flatten_result) + self._do_test(template.get_def("b"), "this is b, 10 15", + template_args={'x':10, 'y':15}, + filters=flatten_result) + self._do_test(template.get_def("body"), "this is the body", filters=flatten_result) + -class ScopeTest(unittest.TestCase): +class ScopeTest(TemplateTest): """test scoping rules. The key is, enclosing scope always takes precedence over contextual scope.""" + def test_scope_one(self): - t = Template(""" + self._do_memory_test(""" <%def name="a()"> this is a, and y is ${y} </%def> @@ -94,8 +100,11 @@ class ScopeTest(unittest.TestCase): ${a()} -""") - assert flatten_result(t.render(y=None)) == "this is a, and y is None this is a, and y is 7" +""", + "this is a, and y is None this is a, and y is 7", + filters=flatten_result, + template_args={'y':None} + ) def test_scope_two(self): t = Template(""" @@ -372,7 +381,7 @@ class ScopeTest(unittest.TestCase): "this is a, x is 15" ] -class NestedDefTest(unittest.TestCase): +class NestedDefTest(TemplateTest): def test_nested_def(self): t = Template(""" @@ -512,7 +521,7 @@ class NestedDefTest(unittest.TestCase): """) assert flatten_result(t.render(x=5)) == "b. c. x is 10. a: x is 5 x is 5" -class ExceptionTest(unittest.TestCase): +class ExceptionTest(TemplateTest): def test_raise(self): template = Template(""" <% diff --git a/test/test_exceptions.py b/test/test_exceptions.py index 52c9544..57b3bac 100644 --- a/test/test_exceptions.py +++ b/test/test_exceptions.py @@ -2,7 +2,7 @@ import sys import unittest -from mako import exceptions +from mako import exceptions, util from mako.template import Template from mako.lookup import TemplateLookup from util import result_lines @@ -15,9 +15,9 @@ class ExceptionsTest(unittest.TestCase): """ try: template = Template(code) - template.render() + template.render_unicode() except exceptions.CompileException, ce: - html_error = exceptions.html_error_template().render() + html_error = exceptions.html_error_template().render_unicode() assert ("CompileException: Fragment 'i = 0' is not a partial " "control statement") in html_error assert '<style>' in html_error @@ -26,13 +26,13 @@ class ExceptionsTest(unittest.TestCase): assert html_error_stripped.startswith('<html>') assert html_error_stripped.endswith('</html>') - not_full = exceptions.html_error_template().render(full=False) + not_full = exceptions.html_error_template().render_unicode(full=False) assert '<html>' not in not_full assert '</html>' not in not_full assert '<style>' in not_full assert '</style>' in not_full - no_css = exceptions.html_error_template().render(css=False) + no_css = exceptions.html_error_template().render_unicode(css=False) assert '<style>' not in no_css assert '</style>' not in no_css else: @@ -41,20 +41,33 @@ class ExceptionsTest(unittest.TestCase): def test_utf8_html_error_template(self): """test the html_error_template with a Template containing utf8 chars""" - code = """# -*- coding: utf-8 -*- + + if util.py3k: + code = """# -*- coding: utf-8 -*- +% if 2 == 2: /an error +${'привет'} +% endif +""" + else: + code = """# -*- coding: utf-8 -*- % if 2 == 2: /an error ${u'привет'} % endif """ try: template = Template(code) - template.render() + template.render_unicode() except exceptions.CompileException, ce: html_error = exceptions.html_error_template().render() assert ("CompileException: Fragment 'if 2 == 2: /an " "error' is not a partial control " - "statement at line: 2 char: 1") in html_error - assert u"3 ${u'привет'}".encode(sys.getdefaultencoding(), + "statement at line: 2 char: 1") in html_error.decode('utf-8') + + if util.py3k: + assert u"3 ${'привет'}".encode(sys.getdefaultencoding(), + 'htmlentityreplace') in html_error + else: + assert u"3 ${u'привет'}".encode(sys.getdefaultencoding(), 'htmlentityreplace') in html_error else: assert False, ("This function should trigger a CompileException, " @@ -66,8 +79,12 @@ ${u'привет'} raise RuntimeError('test') except: html_error = exceptions.html_error_template().render() - assert 'RuntimeError: test' in html_error - assert "foo = u'日本'" in html_error + if util.py3k: + assert 'RuntimeError: test' in html_error.decode('utf-8') + assert u"foo = '日本'" in html_error.decode('utf-8') + else: + assert 'RuntimeError: test' in html_error + assert "foo = u'日本'" in html_error def test_py_unicode_error_html_error_template(self): @@ -75,8 +92,7 @@ ${u'привет'} raise RuntimeError(u'日本') except: html_error = exceptions.html_error_template().render() - assert 'RuntimeError: 日本' in html_error - assert "RuntimeError(u'日本')" in html_error + assert u"RuntimeError: 日本".encode('ascii', 'ignore') in html_error def test_format_exceptions(self): l = TemplateLookup(format_exceptions=True) @@ -90,16 +106,21 @@ ${foobar} ${self.body()} """) - assert '<div class="sourceline">${foobar}</div>' in result_lines(l.get_template("foo.html").render()) + assert '<div class="sourceline">${foobar}</div>' in result_lines(l.get_template("foo.html").render_unicode()) def test_utf8_format_exceptions(self): """test that htmlentityreplace formatting is applied to exceptions reported with format_exceptions=True""" l = TemplateLookup(format_exceptions=True) + if util.py3k: + l.put_string("foo.html", """# -*- coding: utf-8 -*-\n${'привет' + foobar}""") + else: + l.put_string("foo.html", """# -*- coding: utf-8 -*-\n${u'привет' + foobar}""") - l.put_string("foo.html", """# -*- coding: utf-8 -*- -${u'привет' + foobar} -""") - - assert '''<div class="highlight">2 ${u\'привет\' + foobar}</div>''' in result_lines(l.get_template("foo.html").render()) + if util.py3k: + assert u'<div class="sourceline">${\'привет\' + foobar}</div>'\ + in result_lines(l.get_template("foo.html").render().decode('utf-8')) + else: + assert '<div class="highlight">2 ${u\'привет\' + foobar}</div>' \ + in result_lines(l.get_template("foo.html").render().decode('utf-8')) diff --git a/test/test_inheritance.py b/test/test_inheritance.py index c9c6990..9f978d2 100644 --- a/test/test_inheritance.py +++ b/test/test_inheritance.py @@ -1,5 +1,5 @@ from mako.template import Template -from mako import lookup +from mako import lookup, util import unittest from util import flatten_result, result_lines @@ -187,10 +187,10 @@ ${next.body()} this is the base. <% - sorted = pageargs.items() - sorted.sort() + sorted_ = pageargs.items() + sorted_ = sorted(sorted_) %> - pageargs: (type: ${type(pageargs)}) ${sorted} + pageargs: (type: ${type(pageargs)}) ${sorted_} <%def name="foo()"> ${next.body(**context.kwargs)} </%def> @@ -202,11 +202,20 @@ ${next.body()} <%page args="x, y, z=7"/> print ${x}, ${y}, ${z} """) - assert result_lines(collection.get_template('index').render(x=5,y=10)) == [ - "this is the base.", - "pageargs: (type: <type 'dict'>) [('x', 5), ('y', 10)]", - "print 5, 10, 7" - ] + + if util.py3k: + assert result_lines(collection.get_template('index').render_unicode(x=5,y=10)) == [ + "this is the base.", + "pageargs: (type: <class 'dict'>) [('x', 5), ('y', 10)]", + "print 5, 10, 7" + ] + else: + assert result_lines(collection.get_template('index').render_unicode(x=5,y=10)) == [ + "this is the base.", + "pageargs: (type: <type 'dict'>) [('x', 5), ('y', 10)]", + "print 5, 10, 7" + ] + def test_pageargs_2(self): collection = lookup.TemplateLookup() collection.put_string("base", """ diff --git a/test/test_lexer.py b/test/test_lexer.py index d934860..00d16af 100644 --- a/test/test_lexer.py +++ b/test/test_lexer.py @@ -1,14 +1,45 @@ import unittest from mako.lexer import Lexer -from mako import exceptions +from mako import exceptions, util from util import flatten_result, result_lines from mako.template import Template import re -from test import TemplateTest, template_base, skip_if - - +from test import TemplateTest, template_base, skip_if, eq_ + +# create fake parsetree classes which are constructed +# exactly as the repr() of a real parsetree object. +# this allows us to use a Python construct as the source +# of a comparable repr(), which is also hit by the 2to3 tool. + +def repr_arg(x): + if isinstance(x, dict): + return util.sorted_dict_repr(x) + else: + return repr(x) + +from mako import parsetree +for cls in parsetree.__dict__.values(): + if isinstance(cls, type) and \ + issubclass(cls, parsetree.Node): + clsname = cls.__name__ + exec (""" +class %s(object): + def __init__(self, *args): + self.args = args + def __repr__(self): + return "%%s(%%s)" %% ( + self.__class__.__name__, + ", ".join(repr_arg(x) for x in self.args) + ) +""" % clsname) in locals() + + class LexerTest(TemplateTest): + + def _compare(self, node, expected): + eq_(repr(node), repr(expected)) + def test_text_and_tag(self): template = """ <b>Hello world</b> @@ -19,7 +50,10 @@ class LexerTest(TemplateTest): and some more text. """ node = Lexer(template).parse() - assert repr(node) == r"""TemplateNode({}, [Text(u'\n<b>Hello world</b>\n ', (1, 1)), DefTag(u'def', {u'name': u'foo()'}, (3, 9), ["Text(u'\\n this is a def.\\n ', (3, 28))"]), Text(u'\n \n and some more text.\n', (5, 16))])""" + self._compare( + node, + TemplateNode({}, [Text(u'\n<b>Hello world</b>\n ', (1, 1)), DefTag(u'def', {u'name': u'foo()'}, (3, 9), [Text(u'\n this is a def.\n ', (3, 28))]), Text(u'\n \n and some more text.\n', (5, 16))]) + ) def test_unclosed_tag(self): template = """ @@ -96,7 +130,10 @@ class LexerTest(TemplateTest): % endif """ node = Lexer(template).parse() - assert repr(node) == r"""TemplateNode({}, [Text(u'\n', (1, 1)), Comment(u'comment', (2, 1)), ControlLine(u'if', u'if foo:', False, (3, 1)), Text(u' hi\n', (4, 1)), ControlLine(u'if', u'endif', True, (5, 1)), Text(u' ', (6, 1)), TextTag(u'text', {}, (6, 9), ['Text(u\'\\n # more code\\n \\n % more code\\n <%illegal compionent>/></>\\n <%def name="laal()">def</%def>\\n \\n \\n \', (6, 16))']), Text(u'\n\n ', (14, 17)), DefTag(u'def', {u'name': u'foo()'}, (16, 9), ["Text(u'this is foo', (16, 28))"]), Text(u'\n \n', (16, 46)), ControlLine(u'if', u'if bar:', False, (18, 1)), Text(u' code\n', (19, 1)), ControlLine(u'if', u'endif', True, (20, 1)), Text(u' ', (21, 1))])""" + self._compare( + node, + TemplateNode({}, [Text(u'\n', (1, 1)), Comment(u'comment', (2, 1)), ControlLine(u'if', u'if foo:', False, (3, 1)), Text(u' hi\n', (4, 1)), ControlLine(u'if', u'endif', True, (5, 1)), Text(u' ', (6, 1)), TextTag(u'text', {}, (6, 9), [Text(u'\n # more code\n \n % more code\n <%illegal compionent>/></>\n <%def name="laal()">def</%def>\n \n \n ', (6, 16))]), Text(u'\n\n ', (14, 17)), DefTag(u'def', {u'name': u'foo()'}, (16, 9), [Text(u'this is foo', (16, 28))]), Text(u'\n \n', (16, 46)), ControlLine(u'if', u'if bar:', False, (18, 1)), Text(u' code\n', (19, 1)), ControlLine(u'if', u'endif', True, (20, 1)), Text(u' ', (21, 1))]) + ) def test_def_syntax(self): template = """ @@ -122,7 +159,10 @@ class LexerTest(TemplateTest): """ node = Lexer(template).parse() - assert repr(node) == r"""TemplateNode({}, [Text(u'\n ', (1, 1)), DefTag(u'def', {u'name': u'adef()'}, (2, 13), ["Text(u'\\n adef\\n ', (2, 36))"]), Text(u'\n ', (4, 20))])""" + self._compare( + node, + TemplateNode({}, [Text(u'\n ', (1, 1)), DefTag(u'def', {u'name': u'adef()'}, (2, 13), [Text(u'\n adef\n ', (2, 36))]), Text(u'\n ', (4, 20))]) + ) def test_ns_tag_closed(self): template = """ @@ -130,14 +170,20 @@ class LexerTest(TemplateTest): <%self:go x="1" y="2" z="${'hi' + ' ' + 'there'}"/> """ nodes = Lexer(template).parse() - assert repr(nodes) == r"""TemplateNode({}, [Text(u'\n \n ', (1, 1)), CallNamespaceTag(u'self:go', {u'x': u'1', u'y': u'2', u'z': u"${'hi' + ' ' + 'there'}"}, (3, 13), []), Text(u'\n ', (3, 64))])""" + self._compare( + nodes, + TemplateNode({}, [Text(u'\n \n ', (1, 1)), CallNamespaceTag(u'self:go', {u'x': u'1', u'y': u'2', u'z': u"${'hi' + ' ' + 'there'}"}, (3, 13), []), Text(u'\n ', (3, 64))]) + ) def test_ns_tag_empty(self): template = """ <%form:option value=""></%form:option> """ nodes = Lexer(template).parse() - assert repr(nodes) == r"""TemplateNode({}, [Text(u'\n ', (1, 1)), CallNamespaceTag(u'form:option', {u'value': u''}, (2, 13), []), Text(u'\n ', (2, 51))])""" + self._compare( + nodes, + TemplateNode({}, [Text(u'\n ', (1, 1)), CallNamespaceTag(u'form:option', {u'value': u''}, (2, 13), []), Text(u'\n ', (2, 51))]) + ) def test_ns_tag_open(self): template = """ @@ -147,19 +193,26 @@ class LexerTest(TemplateTest): </%self:go> """ nodes = Lexer(template).parse() - assert repr(nodes) == r"""TemplateNode({}, [Text(u'\n \n ', (1, 1)), CallNamespaceTag(u'self:go', {u'x': u'1', u'y': u'${process()}'}, (3, 13), ["Text(u'\\n this is the body\\n ', (3, 46))"]), Text(u'\n ', (5, 24))])""" + self._compare( + nodes, + TemplateNode({}, [Text(u'\n \n ', (1, 1)), CallNamespaceTag(u'self:go', {u'x': u'1', u'y': u'${process()}'}, (3, 13), [Text(u'\n this is the body\n ', (3, 46))]), Text(u'\n ', (5, 24))]) + ) def test_expr_in_attribute(self): """test some slightly trickier expressions. - you can still trip up the expression parsing, though, unless we integrated really deeply somehow with AST.""" + you can still trip up the expression parsing, + though, unless we integrated really deeply somehow with AST.""" + template = """ <%call expr="foo>bar and 'lala' or 'hoho'"/> <%call expr='foo<bar and hoho>lala and "x" + "y"'/> """ nodes = Lexer(template).parse() - #print nodes - assert repr(nodes) == r"""TemplateNode({}, [Text(u'\n ', (1, 1)), CallTag(u'call', {u'expr': u"foo>bar and 'lala' or 'hoho'"}, (2, 13), []), Text(u'\n ', (2, 57)), CallTag(u'call', {u'expr': u'foo<bar and hoho>lala and "x" + "y"'}, (3, 13), []), Text(u'\n ', (3, 64))])""" + self._compare( + nodes, + TemplateNode({}, [Text(u'\n ', (1, 1)), CallTag(u'call', {u'expr': u"foo>bar and 'lala' or 'hoho'"}, (2, 13), []), Text(u'\n ', (2, 57)), CallTag(u'call', {u'expr': u'foo<bar and hoho>lala and "x" + "y"'}, (3, 13), []), Text(u'\n ', (3, 64))]) + ) def test_pagetag(self): @@ -169,7 +222,10 @@ class LexerTest(TemplateTest): some template """ nodes = Lexer(template).parse() - assert repr(nodes) == r"""TemplateNode({}, [Text(u'\n ', (1, 1)), PageTag(u'page', {u'args': u'a, b', u'cached': u'True'}, (2, 13), []), Text(u'\n \n some template\n ', (2, 48))])""" + self._compare( + nodes, + TemplateNode({}, [Text(u'\n ', (1, 1)), PageTag(u'page', {u'args': u'a, b', u'cached': u'True'}, (2, 13), []), Text(u'\n \n some template\n ', (2, 48))]) + ) def test_nesting(self): template = """ @@ -182,10 +238,38 @@ class LexerTest(TemplateTest): """ nodes = Lexer(template).parse() - assert repr(nodes) == r"""TemplateNode({}, [Text(u'\n \n ', (1, 1)), NamespaceTag(u'namespace', {u'name': u'ns'}, (3, 9), ["Text(u'\\n ', (3, 31))", 'DefTag(u\'def\', {u\'name\': u\'lala(hi, there)\'}, (4, 13), ["Text(u\'\\\\n \', (4, 42))", "CallTag(u\'call\', {u\'expr\': u\'something()\'}, (5, 17), [])", "Text(u\'\\\\n \', (5, 44))"])', "Text(u'\\n ', (6, 20))"]), Text(u'\n \n ', (7, 22))])""" + self._compare( + nodes, + TemplateNode({}, [Text(u'\n \n ', (1, 1)), NamespaceTag(u'namespace', {u'name': u'ns'}, (3, 9), [Text(u'\n ', (3, 31)), DefTag(u'def', {u'name': u'lala(hi, there)'}, (4, 13), [Text(u'\n ', (4, 42)), CallTag(u'call', {u'expr': u'something()'}, (5, 17), []), Text(u'\n ', (5, 44))]), Text(u'\n ', (6, 20))]), Text(u'\n \n ', (7, 22))]) + ) - def test_code(self): - template = """ + if util.py3k: + def test_code(self): + template = \ + """ + some text + + <% + print("hi") + for x in range(1,5): + print(x) + %> + + more text + + <%! + import foo + %> + """ + nodes = Lexer(template).parse() + self._compare( + nodes, + TemplateNode({}, [Text(u'\n some text\n \n ', (1, 1)), Code(u'\nprint("hi")\nfor x in range(1,5):\n print(x)\n \n', False, (4, 9)), Text(u'\n \n more text\n \n ', (8, 11)), Code(u'\nimport foo\n \n', True, (12, 9)), Text(u'\n ', (14, 11))]) + ) + else: + def test_code(self): + template = \ + """ some text <% @@ -200,9 +284,11 @@ class LexerTest(TemplateTest): import foo %> """ - nodes = Lexer(template).parse() - #print nodes - assert repr(nodes) == r"""TemplateNode({}, [Text(u'\n some text\n \n ', (1, 1)), Code(u'\nprint "hi"\nfor x in range(1,5):\n print x\n \n', False, (4, 9)), Text(u'\n \n more text\n \n ', (8, 11)), Code(u'\nimport foo\n \n', True, (12, 9)), Text(u'\n ', (14, 11))])""" + nodes = Lexer(template).parse() + self._compare( + nodes, + TemplateNode({}, [Text(u'\n some text\n \n ', (1, 1)), Code(u'\nprint "hi"\nfor x in range(1,5):\n print x\n \n', False, (4, 9)), Text(u'\n \n more text\n \n ', (8, 11)), Code(u'\nimport foo\n \n', True, (12, 9)), Text(u'\n ', (14, 11))]) + ) def test_code_and_tags(self): template = """ @@ -225,7 +311,10 @@ class LexerTest(TemplateTest): result: <%call expr="foo.x(result)"/> """ nodes = Lexer(template).parse() - assert repr(nodes) == r"""TemplateNode({}, [Text(u'\n', (1, 1)), NamespaceTag(u'namespace', {u'name': u'foo'}, (2, 1), ["Text(u'\\n ', (2, 24))", 'DefTag(u\'def\', {u\'name\': u\'x()\'}, (3, 5), ["Text(u\'\\\\n this is x\\\\n \', (3, 22))"])', "Text(u'\\n ', (5, 12))", 'DefTag(u\'def\', {u\'name\': u\'y()\'}, (6, 5), ["Text(u\'\\\\n this is y\\\\n \', (6, 22))"])', "Text(u'\\n', (8, 12))"]), Text(u'\n\n', (9, 14)), Code(u'\nresult = []\ndata = get_data()\nfor x in data:\n result.append(x+7)\n\n', False, (11, 1)), Text(u'\n\n result: ', (16, 3)), CallTag(u'call', {u'expr': u'foo.x(result)'}, (18, 13), []), Text(u'\n', (18, 42))])""" + self._compare( + nodes, + TemplateNode({}, [Text(u'\n', (1, 1)), NamespaceTag(u'namespace', {u'name': u'foo'}, (2, 1), [Text(u'\n ', (2, 24)), DefTag(u'def', {u'name': u'x()'}, (3, 5), [Text(u'\n this is x\n ', (3, 22))]), Text(u'\n ', (5, 12)), DefTag(u'def', {u'name': u'y()'}, (6, 5), [Text(u'\n this is y\n ', (6, 22))]), Text(u'\n', (8, 12))]), Text(u'\n\n', (9, 14)), Code(u'\nresult = []\ndata = get_data()\nfor x in data:\n result.append(x+7)\n\n', False, (11, 1)), Text(u'\n\n result: ', (16, 3)), CallTag(u'call', {u'expr': u'foo.x(result)'}, (18, 13), []), Text(u'\n', (18, 42))]) + ) def test_expression(self): template = """ @@ -236,7 +325,10 @@ class LexerTest(TemplateTest): ${hi()} """ nodes = Lexer(template).parse() - assert repr(nodes) == r"""TemplateNode({}, [Text(u'\n this is some ', (1, 1)), Expression(u'text', [], (2, 22)), Text(u' and this is ', (2, 29)), Expression(u'textwith ', ['escapes', 'moreescapes'], (2, 42)), Text(u'\n ', (2, 76)), DefTag(u'def', {u'name': u'hi()'}, (3, 9), ["Text(u'\\n give me ', (3, 27))", "Expression(u'foo()', [], (4, 21))", "Text(u' and ', (4, 29))", "Expression(u'bar()', [], (4, 34))", "Text(u'\\n ', (4, 42))"]), Text(u'\n ', (5, 16)), Expression(u'hi()', [], (6, 9)), Text(u'\n', (6, 16))])""" + self._compare( + nodes, + TemplateNode({}, [Text(u'\n this is some ', (1, 1)), Expression(u'text', [], (2, 22)), Text(u' and this is ', (2, 29)), Expression(u'textwith ', ['escapes', 'moreescapes'], (2, 42)), Text(u'\n ', (2, 76)), DefTag(u'def', {u'name': u'hi()'}, (3, 9), [Text(u'\n give me ', (3, 27)), Expression(u'foo()', [], (4, 21)), Text(u' and ', (4, 29)), Expression(u'bar()', [], (4, 34)), Text(u'\n ', (4, 42))]), Text(u'\n ', (5, 16)), Expression(u'hi()', [], (6, 9)), Text(u'\n', (6, 16))]) + ) def test_tricky_expression(self): @@ -245,36 +337,68 @@ class LexerTest(TemplateTest): ${x and "|" or "hi"} """ nodes = Lexer(template).parse() - assert repr(nodes) == r"""TemplateNode({}, [Text(u'\n \n ', (1, 1)), Expression(u'x and "|" or "hi"', [], (3, 13)), Text(u'\n ', (3, 33))])""" + self._compare( + nodes, + TemplateNode({}, [Text(u'\n \n ', (1, 1)), Expression(u'x and "|" or "hi"', [], (3, 13)), Text(u'\n ', (3, 33))]) + ) template = """ ${hello + '''heres '{|}' text | | }''' | escape1} """ nodes = Lexer(template).parse() - assert repr(nodes) == r"""TemplateNode({}, [Text(u'\n \n ', (1, 1)), Expression(u"hello + '''heres '{|}' text | | }''' ", ['escape1'], (3, 13)), Text(u'\n ', (3, 62))])""" + self._compare( + nodes, + TemplateNode({}, [Text(u'\n \n ', (1, 1)), Expression(u"hello + '''heres '{|}' text | | }''' ", ['escape1'], (3, 13)), Text(u'\n ', (3, 62))]) + ) def test_tricky_code(self): - template = """<% print 'hi %>' %>""" - nodes = Lexer(template).parse() - assert repr(nodes) == r"""TemplateNode({}, [Code(u"print 'hi %>' \n", False, (1, 1))])""" + if util.py3k: + template = """<% print('hi %>') %>""" + nodes = Lexer(template).parse() + self._compare( + nodes, + TemplateNode({}, [Code(u"print('hi %>') \n", False, (1, 1))]) + ) + else: + template = """<% print 'hi %>' %>""" + nodes = Lexer(template).parse() + self._compare( + nodes, + TemplateNode({}, [Code(u"print 'hi %>' \n", False, (1, 1))]) + ) - template = r""" - <% - lines = src.split('\n') - %> -""" - nodes = Lexer(template).parse() - def test_tricky_code_2(self): template = """<% # someone's comment %> """ nodes = Lexer(template).parse() - assert repr(nodes) == r"""TemplateNode({}, [Code(u" \n # someone's comment\n \n", False, (1, 1)), Text(u'\n ', (3, 11))])""" - - template= """<% + self._compare( + nodes, + TemplateNode({}, [Code(u" \n # someone's comment\n \n", False, (1, 1)), Text(u'\n ', (3, 11))]) + ) + + if util.py3k: + def test_tricky_code_3(self): + template= """<% + print('hi') + # this is a comment + # another comment + x = 7 # someone's '''comment + print(''' + there + ''') + # someone else's comment + %> '''and now some text '''""" + nodes = Lexer(template).parse() + self._compare( + nodes, + TemplateNode({}, [Code(u"\nprint('hi')\n# this is a comment\n# another comment\nx = 7 # someone's '''comment\nprint('''\n there\n ''')\n# someone else's comment\n \n", False, (1, 1)), Text(u" '''and now some text '''", (10, 11))]) + ) + else: + def test_tricky_code_3(self): + template= """<% print 'hi' # this is a comment # another comment @@ -284,8 +408,11 @@ class LexerTest(TemplateTest): ''' # someone else's comment %> '''and now some text '''""" - nodes = Lexer(template).parse() - assert repr(nodes) == r"""TemplateNode({}, [Code(u"\nprint 'hi'\n# this is a comment\n# another comment\nx = 7 # someone's '''comment\nprint '''\n there\n '''\n# someone else's comment\n \n", False, (1, 1)), Text(u" '''and now some text '''", (10, 11))])""" + nodes = Lexer(template).parse() + self._compare( + nodes, + TemplateNode({}, [Code(u"\nprint 'hi'\n# this is a comment\n# another comment\nx = 7 # someone's '''comment\nprint '''\n there\n '''\n# someone else's comment\n \n", False, (1, 1)), Text(u" '''and now some text '''", (10, 11))]) + ) def test_control_lines(self): template = """ @@ -302,8 +429,10 @@ text text la la """ nodes = Lexer(template).parse() - #print nodes - assert repr(nodes) == r"""TemplateNode({}, [Text(u'\ntext text la la\n', (1, 1)), ControlLine(u'if', u'if foo():', False, (3, 1)), Text(u' mroe text la la blah blah\n', (4, 1)), ControlLine(u'if', u'endif', True, (5, 1)), Text(u'\n and osme more stuff\n', (6, 1)), ControlLine(u'for', u'for l in range(1,5):', False, (8, 1)), Text(u' tex tesl asdl l is ', (9, 1)), Expression(u'l', [], (9, 24)), Text(u' kfmas d\n', (9, 28)), ControlLine(u'for', u'endfor', True, (10, 1)), Text(u' tetx text\n \n', (11, 1))])""" + self._compare( + nodes, + TemplateNode({}, [Text(u'\ntext text la la\n', (1, 1)), ControlLine(u'if', u'if foo():', False, (3, 1)), Text(u' mroe text la la blah blah\n', (4, 1)), ControlLine(u'if', u'endif', True, (5, 1)), Text(u'\n and osme more stuff\n', (6, 1)), ControlLine(u'for', u'for l in range(1,5):', False, (8, 1)), Text(u' tex tesl asdl l is ', (9, 1)), Expression(u'l', [], (9, 24)), Text(u' kfmas d\n', (9, 28)), ControlLine(u'for', u'endfor', True, (10, 1)), Text(u' tetx text\n \n', (11, 1))]) + ) def test_control_lines_2(self): template = \ @@ -315,7 +444,10 @@ text text la la % endfor """ nodes = Lexer(template).parse() - assert repr(nodes) == r"""TemplateNode({}, [Text(u'\n\n\n', (1, 1)), ControlLine(u'for', u"for file in requestattr['toc'].filenames:", False, (4, 1)), Text(u' x\n', (5, 1)), ControlLine(u'for', u'endfor', True, (6, 1))])""" + self._compare( + nodes, + TemplateNode({}, [Text(u'\n\n\n', (1, 1)), ControlLine(u'for', u"for file in requestattr['toc'].filenames:", False, (4, 1)), Text(u' x\n', (5, 1)), ControlLine(u'for', u'endfor', True, (6, 1))]) + ) def test_long_control_lines(self): template = \ @@ -326,7 +458,10 @@ text text la la % endfor """ nodes = Lexer(template).parse() - assert repr(nodes) == r"""TemplateNode({}, [Text(u'\n', (1, 1)), ControlLine(u'for', u"for file in \\\n requestattr['toc'].filenames:", False, (2, 1)), Text(u' x\n', (4, 1)), ControlLine(u'for', u'endfor', True, (5, 1)), Text(u' ', (6, 1))])""" + self._compare( + nodes, + TemplateNode({}, [Text(u'\n', (1, 1)), ControlLine(u'for', u"for file in \\\n requestattr['toc'].filenames:", False, (2, 1)), Text(u' x\n', (4, 1)), ControlLine(u'for', u'endfor', True, (5, 1)), Text(u' ', (6, 1))]) + ) def test_unmatched_control(self): template = """ @@ -381,7 +516,10 @@ text text la la % endif """ nodes = Lexer(template).parse() - assert repr(nodes) == r"""TemplateNode({}, [Text(u'\n', (1, 1)), ControlLine(u'if', u'if x:', False, (2, 1)), Text(u' hi\n', (3, 1)), ControlLine(u'elif', u'elif y+7==10:', False, (4, 1)), Text(u' there\n', (5, 1)), ControlLine(u'elif', u'elif lala:', False, (6, 1)), Text(u' lala\n', (7, 1)), ControlLine(u'else', u'else:', False, (8, 1)), Text(u' hi\n', (9, 1)), ControlLine(u'if', u'endif', True, (10, 1))])""" + self._compare( + nodes, + TemplateNode({}, [Text(u'\n', (1, 1)), ControlLine(u'if', u'if x:', False, (2, 1)), Text(u' hi\n', (3, 1)), ControlLine(u'elif', u'elif y+7==10:', False, (4, 1)), Text(u' there\n', (5, 1)), ControlLine(u'elif', u'elif lala:', False, (6, 1)), Text(u' lala\n', (7, 1)), ControlLine(u'else', u'else:', False, (8, 1)), Text(u' hi\n', (9, 1)), ControlLine(u'if', u'endif', True, (10, 1))]) + ) def test_integration(self): template = """<%namespace name="foo" file="somefile.html"/> @@ -406,8 +544,10 @@ text text la la </table> """ nodes = Lexer(template).parse() - expected = r"""TemplateNode({}, [NamespaceTag(u'namespace', {u'file': u'somefile.html', u'name': u'foo'}, (1, 1), []), Text(u'\n', (1, 46)), Comment(u'inherit from foobar.html', (2, 1)), InheritTag(u'inherit', {u'file': u'foobar.html'}, (3, 1), []), Text(u'\n\n', (3, 31)), DefTag(u'def', {u'name': u'header()'}, (5, 1), ["Text(u'\\n <div>header</div>\\n', (5, 23))"]), Text(u'\n', (7, 8)), DefTag(u'def', {u'name': u'footer()'}, (8, 1), ["Text(u'\\n <div> footer</div>\\n', (8, 23))"]), Text(u'\n\n<table>\n', (10, 8)), ControlLine(u'for', u'for j in data():', False, (13, 1)), Text(u' <tr>\n', (14, 1)), ControlLine(u'for', u'for x in j:', False, (15, 1)), Text(u' <td>Hello ', (16, 1)), Expression(u'x', ['h'], (16, 23)), Text(u'</td>\n', (16, 30)), ControlLine(u'for', u'endfor', True, (17, 1)), Text(u' </tr>\n', (18, 1)), ControlLine(u'for', u'endfor', True, (19, 1)), Text(u'</table>\n', (20, 1))])""" - assert repr(nodes) == expected + self._compare( + nodes, + TemplateNode({}, [NamespaceTag(u'namespace', {u'file': u'somefile.html', u'name': u'foo'}, (1, 1), []), Text(u'\n', (1, 46)), Comment(u'inherit from foobar.html', (2, 1)), InheritTag(u'inherit', {u'file': u'foobar.html'}, (3, 1), []), Text(u'\n\n', (3, 31)), DefTag(u'def', {u'name': u'header()'}, (5, 1), [Text(u'\n <div>header</div>\n', (5, 23))]), Text(u'\n', (7, 8)), DefTag(u'def', {u'name': u'footer()'}, (8, 1), [Text(u'\n <div> footer</div>\n', (8, 23))]), Text(u'\n\n<table>\n', (10, 8)), ControlLine(u'for', u'for j in data():', False, (13, 1)), Text(u' <tr>\n', (14, 1)), ControlLine(u'for', u'for x in j:', False, (15, 1)), Text(u' <td>Hello ', (16, 1)), Expression(u'x', ['h'], (16, 23)), Text(u'</td>\n', (16, 30)), ControlLine(u'for', u'endfor', True, (17, 1)), Text(u' </tr>\n', (18, 1)), ControlLine(u'for', u'endfor', True, (19, 1)), Text(u'</table>\n', (20, 1))]) + ) def test_comment_after_statement(self): template = """ @@ -418,12 +558,18 @@ text text la la % endif #end """ nodes = Lexer(template).parse() - assert repr(nodes) == r"""TemplateNode({}, [Text(u'\n', (1, 1)), ControlLine(u'if', u'if x: #comment', False, (2, 1)), Text(u' hi\n', (3, 1)), ControlLine(u'else', u'else: #next', False, (4, 1)), Text(u' hi\n', (5, 1)), ControlLine(u'if', u'endif #end', True, (6, 1))])""" + self._compare( + nodes, + TemplateNode({}, [Text(u'\n', (1, 1)), ControlLine(u'if', u'if x: #comment', False, (2, 1)), Text(u' hi\n', (3, 1)), ControlLine(u'else', u'else: #next', False, (4, 1)), Text(u' hi\n', (5, 1)), ControlLine(u'if', u'endif #end', True, (6, 1))]) + ) def test_crlf(self): - template = file(self._file_path("crlf.html")).read() + template = open(self._file_path("crlf.html"), 'rb').read() nodes = Lexer(template).parse() - assert repr(nodes) == r"""TemplateNode({}, [Text(u'<html>\r\n\r\n', (1, 1)), PageTag(u'page', {u'args': u"a=['foo',\n 'bar']"}, (3, 1), []), Text(u'\r\n\r\nlike the name says.\r\n\r\n', (4, 26)), ControlLine(u'for', u'for x in [1,2,3]:', False, (8, 1)), Text(u' ', (9, 1)), Expression(u'x', [], (9, 9)), Text(u'', (9, 13)), ControlLine(u'for', u'endfor', True, (10, 1)), Text(u'\r\n', (11, 1)), Expression(u"trumpeter == 'Miles' and trumpeter or \\\n 'Dizzy'", [], (12, 1)), Text(u'\r\n\r\n', (13, 15)), DefTag(u'def', {u'name': u'hi()'}, (15, 1), ["Text(u'\\r\\n hi!\\r\\n', (15, 19))"]), Text(u'\r\n\r\n</html>\r\n', (17, 8))])""" + self._compare( + nodes, + TemplateNode({}, [Text(u'<html>\r\n\r\n', (1, 1)), PageTag(u'page', {u'args': u"a=['foo',\n 'bar']"}, (3, 1), []), Text(u'\r\n\r\nlike the name says.\r\n\r\n', (4, 26)), ControlLine(u'for', u'for x in [1,2,3]:', False, (8, 1)), Text(u' ', (9, 1)), Expression(u'x', [], (9, 9)), Text(u'', (9, 13)), ControlLine(u'for', u'endfor', True, (10, 1)), Text(u'\r\n', (11, 1)), Expression(u"trumpeter == 'Miles' and trumpeter or \\\n 'Dizzy'", [], (12, 1)), Text(u'\r\n\r\n', (13, 15)), DefTag(u'def', {u'name': u'hi()'}, (15, 1), [Text(u'\r\n hi!\r\n', (15, 19))]), Text(u'\r\n\r\n</html>\r\n', (17, 8))]) + ) assert flatten_result(Template(template).render()) == """<html> like the name says. 1 2 3 Dizzy </html>""" def test_comments(self): @@ -447,7 +593,10 @@ comment hi """ nodes = Lexer(template).parse() - assert repr(nodes) == r"""TemplateNode({}, [Text(u'\n<style>\n #someselector\n # other non comment stuff\n</style>\n', (1, 1)), Comment(u'a comment', (6, 1)), Text(u'\n# also not a comment\n\n', (7, 1)), Comment(u'this is a comment', (10, 1)), Text(u' \nthis is ## not a comment\n\n', (11, 1)), Comment(u' multiline\ncomment\n', (14, 1)), Text(u'\n\nhi\n', (16, 8))])""" + self._compare( + nodes, + TemplateNode({}, [Text(u'\n<style>\n #someselector\n # other non comment stuff\n</style>\n', (1, 1)), Comment(u'a comment', (6, 1)), Text(u'\n# also not a comment\n\n', (7, 1)), Comment(u'this is a comment', (10, 1)), Text(u' \nthis is ## not a comment\n\n', (11, 1)), Comment(u' multiline\ncomment\n', (14, 1)), Text(u'\n\nhi\n', (16, 8))]) + ) def test_docs(self): template = """ @@ -461,7 +610,10 @@ hi </%def> """ nodes = Lexer(template).parse() - assert repr(nodes) == r"""TemplateNode({}, [Text(u'\n ', (1, 1)), Comment(u'\n this is a comment\n ', (2, 9)), Text(u'\n ', (4, 16)), DefTag(u'def', {u'name': u'foo()'}, (5, 9), ["Text(u'\\n ', (5, 28))", "Comment(u'\\n this is the foo func\\n ', (6, 13))", "Text(u'\\n ', (8, 20))"]), Text(u'\n ', (9, 16))])""" + self._compare( + nodes, + TemplateNode({}, [Text(u'\n ', (1, 1)), Comment(u'\n this is a comment\n ', (2, 9)), Text(u'\n ', (4, 16)), DefTag(u'def', {u'name': u'foo()'}, (5, 9), [Text(u'\n ', (5, 28)), Comment(u'\n this is the foo func\n ', (6, 13)), Text(u'\n ', (8, 20))]), Text(u'\n ', (9, 16))]) + ) def test_preprocess(self): def preproc(text): @@ -472,5 +624,8 @@ hi # another comment """ nodes = Lexer(template, preprocessor=preproc).parse() - assert repr(nodes) == r"""TemplateNode({}, [Text(u'\n hi\n', (1, 1)), Comment(u'old style comment', (3, 1)), Comment(u'another comment', (4, 1))])""" + self._compare( + nodes, + TemplateNode({}, [Text(u'\n hi\n', (1, 1)), Comment(u'old style comment', (3, 1)), Comment(u'another comment', (4, 1))]) + ) diff --git a/test/test_template.py b/test/test_template.py index f06ab5b..970565a 100644 --- a/test/test_template.py +++ b/test/test_template.py @@ -3,7 +3,7 @@ from mako.template import Template, ModuleTemplate from mako.lookup import TemplateLookup from mako.ext.preprocessors import convert_comments -from mako import exceptions +from mako import exceptions, util import re, os from util import flatten_result, result_lines import codecs @@ -50,10 +50,13 @@ class EncodingTest(TemplateTest): directories=[template_base], output_encoding='utf-8', default_filters=['decode.utf8']) - template = lookup.get_template('/chs_unicode.html') + if util.py3k: + template = lookup.get_template('/chs_unicode_py3k.html') + else: + template = lookup.get_template('/chs_unicode.html') eq_( - flatten_result(template.render(name='毛泽东')), - '毛泽东 是 æ–°ä¸å›½çš„主å¸<br/> Welcome ä½ to 北京.' + flatten_result(template.render_unicode(name='毛泽东')), + u'毛泽东 是 æ–°ä¸å›½çš„主å¸<br/> Welcome ä½ to 北京.' ) def test_unicode_bom(self): @@ -76,14 +79,14 @@ class EncodingTest(TemplateTest): def test_unicode_memory(self): val = u"""Alors vous imaginez ma surprise, au lever du jour, quand une drôle de petit voix m’a réveillé. Elle disait: « S’il vous plaît… dessine-moi un mouton! »""" self._do_memory_test( - "## -*- coding: utf-8 -*-\n" + val.encode('utf-8'), + ("## -*- coding: utf-8 -*-\n" + val).encode('utf-8'), u"""Alors vous imaginez ma surprise, au lever du jour, quand une drôle de petit voix m’a réveillé. Elle disait: « S’il vous plaît… dessine-moi un mouton! »""" ) def test_unicode_text(self): val = u"""<%text>Alors vous imaginez ma surprise, au lever du jour, quand une drôle de petit voix m’a réveillé. Elle disait: « S’il vous plaît… dessine-moi un mouton! »</%text>""" self._do_memory_test( - "## -*- coding: utf-8 -*-\n" + val.encode('utf-8'), + ("## -*- coding: utf-8 -*-\n" + val).encode('utf-8'), u"""Alors vous imaginez ma surprise, au lever du jour, quand une drôle de petit voix m’a réveillé. Elle disait: « S’il vous plaît… dessine-moi un mouton! »""" ) @@ -96,19 +99,28 @@ class EncodingTest(TemplateTest): <%text>Alors vous imaginez ma surprise, au lever du jour, quand une drôle de petit voix m’a réveillé. Elle disait: « S’il vous plaît… dessine-moi un mouton! »</%text> </%call>""" self._do_memory_test( - "## -*- coding: utf-8 -*-\n" + val.encode('utf-8'), + ("## -*- coding: utf-8 -*-\n" + val).encode('utf-8'), u"""Alors vous imaginez ma surprise, au lever du jour, quand une drôle de petit voix m’a réveillé. Elle disait: « S’il vous plaît… dessine-moi un mouton! »""", filters=flatten_result ) def test_unicode_literal_in_expr(self): - self._do_memory_test( - u"""## -*- coding: utf-8 -*- - ${u"Alors vous imaginez ma surprise, au lever du jour, quand une drôle de petit voix m’a réveillé. Elle disait: « S’il vous plaît… dessine-moi un mouton! »"} - """.encode('utf-8'), - u"""Alors vous imaginez ma surprise, au lever du jour, quand une drôle de petit voix m’a réveillé. Elle disait: « S’il vous plaît… dessine-moi un mouton! »""", - filters = lambda s:s.strip() - ) + if util.py3k: + self._do_memory_test( + u"""## -*- coding: utf-8 -*- + ${"Alors vous imaginez ma surprise, au lever du jour, quand une drôle de petit voix m’a réveillé. Elle disait: « S’il vous plaît… dessine-moi un mouton! »"} + """.encode('utf-8'), + u"""Alors vous imaginez ma surprise, au lever du jour, quand une drôle de petit voix m’a réveillé. Elle disait: « S’il vous plaît… dessine-moi un mouton! »""", + filters = lambda s:s.strip() + ) + else: + self._do_memory_test( + u"""## -*- coding: utf-8 -*- + ${u"Alors vous imaginez ma surprise, au lever du jour, quand une drôle de petit voix m’a réveillé. Elle disait: « S’il vous plaît… dessine-moi un mouton! »"} + """.encode('utf-8'), + u"""Alors vous imaginez ma surprise, au lever du jour, quand une drôle de petit voix m’a réveillé. Elle disait: « S’il vous plaît… dessine-moi un mouton! »""", + filters = lambda s:s.strip() + ) def test_unicode_literal_in_expr_file(self): self._do_file_test( @@ -118,29 +130,54 @@ class EncodingTest(TemplateTest): ) def test_unicode_literal_in_code(self): - self._do_memory_test( - u"""## -*- coding: utf-8 -*- - <% - context.write(u"Alors vous imaginez ma surprise, au lever du jour, quand une drôle de petit voix m’a réveillé. Elle disait: « S’il vous plaît… dessine-moi un mouton! »") - %> - """.encode('utf-8'), - u"""Alors vous imaginez ma surprise, au lever du jour, quand une drôle de petit voix m’a réveillé. Elle disait: « S’il vous plaît… dessine-moi un mouton! »""", - filters=lambda s:s.strip() - ) + if util.py3k: + self._do_memory_test( + u"""## -*- coding: utf-8 -*- + <% + context.write("Alors vous imaginez ma surprise, au lever du jour, quand une drôle de petit voix m’a réveillé. Elle disait: « S’il vous plaît… dessine-moi un mouton! »") + %> + """.encode('utf-8'), + u"""Alors vous imaginez ma surprise, au lever du jour, quand une drôle de petit voix m’a réveillé. Elle disait: « S’il vous plaît… dessine-moi un mouton! »""", + filters=lambda s:s.strip() + ) + else: + self._do_memory_test( + u"""## -*- coding: utf-8 -*- + <% + context.write(u"Alors vous imaginez ma surprise, au lever du jour, quand une drôle de petit voix m’a réveillé. Elle disait: « S’il vous plaît… dessine-moi un mouton! »") + %> + """.encode('utf-8'), + u"""Alors vous imaginez ma surprise, au lever du jour, quand une drôle de petit voix m’a réveillé. Elle disait: « S’il vous plaît… dessine-moi un mouton! »""", + filters=lambda s:s.strip() + ) def test_unicode_literal_in_controlline(self): - self._do_memory_test( - u"""## -*- coding: utf-8 -*- - <% - x = u"drôle de petit voix m’a réveillé." - %> - % if x==u"drôle de petit voix m’a réveillé.": - hi, ${x} - % endif - """.encode('utf-8'), - u"""hi, drôle de petit voix m’a réveillé.""", - filters=lambda s:s.strip(), - ) + if util.py3k: + self._do_memory_test( + u"""## -*- coding: utf-8 -*- + <% + x = "drôle de petit voix m’a réveillé." + %> + % if x=="drôle de petit voix m’a réveillé.": + hi, ${x} + % endif + """.encode('utf-8'), + u"""hi, drôle de petit voix m’a réveillé.""", + filters=lambda s:s.strip(), + ) + else: + self._do_memory_test( + u"""## -*- coding: utf-8 -*- + <% + x = u"drôle de petit voix m’a réveillé." + %> + % if x==u"drôle de petit voix m’a réveillé.": + hi, ${x} + % endif + """.encode('utf-8'), + u"""hi, drôle de petit voix m’a réveillé.""", + filters=lambda s:s.strip(), + ) def test_unicode_literal_in_tag(self): self._do_file_test( @@ -155,7 +192,7 @@ class EncodingTest(TemplateTest): ) self._do_memory_test( - file(self._file_path("unicode_arguments.html")).read(), + open(self._file_path("unicode_arguments.html"), 'rb').read(), [ u'x is: drôle de petit voix m’a réveillé', u'x is: drôle de petit voix m’a réveillé', @@ -166,45 +203,83 @@ class EncodingTest(TemplateTest): ) def test_unicode_literal_in_def(self): - self._do_memory_test( - u"""## -*- coding: utf-8 -*- - <%def name="bello(foo, bar)"> - Foo: ${ foo } - Bar: ${ bar } - </%def> - <%call expr="bello(foo=u'árvÃztűrÅ‘ tükörfúrógép', bar=u'ÃRVÃZTÅ°RÅ TÃœKÖRFÚRÓGÉP')"> - </%call>""".encode('utf-8'), - u"""Foo: árvÃztűrÅ‘ tükörfúrógép Bar: ÃRVÃZTÅ°RÅ TÃœKÖRFÚRÓGÉP""", - filters=flatten_result - ) + if util.py3k: + self._do_memory_test( + u"""## -*- coding: utf-8 -*- + <%def name="bello(foo, bar)"> + Foo: ${ foo } + Bar: ${ bar } + </%def> + <%call expr="bello(foo='árvÃztűrÅ‘ tükörfúrógép', bar='ÃRVÃZTÅ°RÅ TÃœKÖRFÚRÓGÉP')"> + </%call>""".encode('utf-8'), + u"""Foo: árvÃztűrÅ‘ tükörfúrógép Bar: ÃRVÃZTÅ°RÅ TÃœKÖRFÚRÓGÉP""", + filters=flatten_result + ) + + self._do_memory_test( + u"""## -*- coding: utf-8 -*- + <%def name="hello(foo='árvÃztűrÅ‘ tükörfúrógép', bar='ÃRVÃZTÅ°RÅ TÃœKÖRFÚRÓGÉP')"> + Foo: ${ foo } + Bar: ${ bar } + </%def> + ${ hello() }""".encode('utf-8'), + u"""Foo: árvÃztűrÅ‘ tükörfúrógép Bar: ÃRVÃZTÅ°RÅ TÃœKÖRFÚRÓGÉP""", + filters=flatten_result + ) + else: + self._do_memory_test( + u"""## -*- coding: utf-8 -*- + <%def name="bello(foo, bar)"> + Foo: ${ foo } + Bar: ${ bar } + </%def> + <%call expr="bello(foo=u'árvÃztűrÅ‘ tükörfúrógép', bar=u'ÃRVÃZTÅ°RÅ TÃœKÖRFÚRÓGÉP')"> + </%call>""".encode('utf-8'), + u"""Foo: árvÃztűrÅ‘ tükörfúrógép Bar: ÃRVÃZTÅ°RÅ TÃœKÖRFÚRÓGÉP""", + filters=flatten_result + ) - self._do_memory_test( - u"""## -*- coding: utf-8 -*- - <%def name="hello(foo=u'árvÃztűrÅ‘ tükörfúrógép', bar=u'ÃRVÃZTÅ°RÅ TÃœKÖRFÚRÓGÉP')"> - Foo: ${ foo } - Bar: ${ bar } - </%def> - ${ hello() }""".encode('utf-8'), - u"""Foo: árvÃztűrÅ‘ tükörfúrógép Bar: ÃRVÃZTÅ°RÅ TÃœKÖRFÚRÓGÉP""", - filters=flatten_result - ) + self._do_memory_test( + u"""## -*- coding: utf-8 -*- + <%def name="hello(foo=u'árvÃztűrÅ‘ tükörfúrógép', bar=u'ÃRVÃZTÅ°RÅ TÃœKÖRFÚRÓGÉP')"> + Foo: ${ foo } + Bar: ${ bar } + </%def> + ${ hello() }""".encode('utf-8'), + u"""Foo: árvÃztűrÅ‘ tükörfúrógép Bar: ÃRVÃZTÅ°RÅ TÃœKÖRFÚRÓGÉP""", + filters=flatten_result + ) def test_input_encoding(self): """test the 'input_encoding' flag on Template, and that unicode objects arent double-decoded""" - self._do_memory_test( - u"hello ${f(u'Å›lÄ…sk')}", - u"hello Å›lÄ…sk", - input_encoding='utf-8', - template_args={'f':lambda x:x} - ) - - self._do_memory_test( - u"## -*- coding: utf-8 -*-\nhello ${f(u'Å›lÄ…sk')}", - u"hello Å›lÄ…sk", - template_args={'f':lambda x:x} - ) + if util.py3k: + self._do_memory_test( + u"hello ${f('Å›lÄ…sk')}", + u"hello Å›lÄ…sk", + input_encoding='utf-8', + template_args={'f':lambda x:x} + ) + + self._do_memory_test( + u"## -*- coding: utf-8 -*-\nhello ${f('Å›lÄ…sk')}", + u"hello Å›lÄ…sk", + template_args={'f':lambda x:x} + ) + else: + self._do_memory_test( + u"hello ${f(u'Å›lÄ…sk')}", + u"hello Å›lÄ…sk", + input_encoding='utf-8', + template_args={'f':lambda x:x} + ) + + self._do_memory_test( + u"## -*- coding: utf-8 -*-\nhello ${f(u'Å›lÄ…sk')}", + u"hello Å›lÄ…sk", + template_args={'f':lambda x:x} + ) def test_raw_strings(self): """test that raw strings go straight thru with default_filters turned off""" @@ -243,9 +318,13 @@ class EncodingTest(TemplateTest): def test_read_unicode(self): lookup = TemplateLookup(directories=[template_base], filesystem_checks=True, output_encoding='utf-8') - template = lookup.get_template('/read_unicode.html') + if util.py3k: + template = lookup.get_template('/read_unicode_py3k.html') + else: + template = lookup.get_template('/read_unicode.html') data = template.render(path=self._file_path('internationalization.html')) + @skip_if(lambda: util.py3k) def test_bytestring_passthru(self): self._do_file_test( 'chs_utf8.html', @@ -418,7 +497,7 @@ class ControlTest(TemplateTest): t = Template(""" ## this is a template. % for x in y: - % if x.has_key('test'): + % if 'test' in x: yes x has test % else: no x does not have test @@ -474,7 +553,7 @@ class RichTracebackTest(TemplateTest): filename = 'unicode_syntax_error.html' else: filename = 'unicode_runtime_error.html' - source = file(self._file_path(filename)).read() + source = open(self._file_path(filename), 'rb').read() if not utf8: source = source.decode('utf-8') templateargs = {'filename':self._file_path(filename)} -- GitLab