Commit cdbfdf0a by Chris Jerdonek

Refactored the Loader class to use the Renderer class's unicode.

As a result, it is no longer necessary to pass decode_errors to
the Loader class.
parent d22cc3b0
...@@ -120,6 +120,9 @@ class View(CustomizedTemplate): ...@@ -120,6 +120,9 @@ class View(CustomizedTemplate):
return renderer.render(template, self.context) return renderer.render(template, self.context)
# TODO: finalize this class's name.
# TODO: get this class fully working with test cases, and then refactor
# and replace the View class.
class CustomLoader(object): class CustomLoader(object):
""" """
......
...@@ -3,8 +3,15 @@ ...@@ -3,8 +3,15 @@
""" """
This module provides a central location for defining default behavior. This module provides a central location for defining default behavior.
Throughout the package, these defaults take effect only when the user
does not otherwise specify a value.
""" """
import cgi
import sys
# How to handle encoding errors when decoding strings from str to unicode. # How to handle encoding errors when decoding strings from str to unicode.
# #
# This value is passed as the "errors" argument to Python's built-in # This value is passed as the "errors" argument to Python's built-in
...@@ -14,5 +21,25 @@ This module provides a central location for defining default behavior. ...@@ -14,5 +21,25 @@ This module provides a central location for defining default behavior.
# #
DECODE_ERRORS = 'strict' DECODE_ERRORS = 'strict'
# The name of the encoding to use when converting to unicode any strings of
# type str encountered during the rendering process.
STRING_ENCODING = sys.getdefaultencoding()
# The name of the encoding to use when converting file contents to unicode.
# This default takes precedence over the STRING_ENCODING default for
# strings that arise from files.
FILE_ENCODING = sys.getdefaultencoding()
# The escape function to apply to strings that require escaping when
# rendering templates (e.g. for tags enclosed in double braces).
# Only unicode strings will be passed to this function.
#
# The quote=True argument causes double quotes to be escaped,
# but not single quotes:
#
# http://docs.python.org/library/cgi.html#cgi.escape
#
TAG_ESCAPE = lambda u: cgi.escape(u, quote=True)
# The default template extension. # The default template extension.
TEMPLATE_EXTENSION = 'mustache' TEMPLATE_EXTENSION = 'mustache'
# coding: utf-8 # coding: utf-8
""" """
This module provides a Reader class to read a template given a path. This module provides a Loader class for locating and reading templates.
""" """
...@@ -14,62 +14,82 @@ from . import defaults ...@@ -14,62 +14,82 @@ from . import defaults
from .locator import Locator from .locator import Locator
def _to_unicode(s, encoding=None):
"""
Raises a TypeError exception if the given string is already unicode.
"""
if encoding is None:
encoding = defaults.STRING_ENCODING
return unicode(s, encoding, defaults.DECODE_ERRORS)
class Loader(object): class Loader(object):
def __init__(self, encoding=None, decode_errors=None, extension=None): """
""" Loads the template associated to a name or user-defined object.
Construct a template reader.
Arguments: """
decode_errors: the string to pass as the errors argument to the def __init__(self, file_encoding=None, extension=None, to_unicode=None):
built-in function unicode() when converting str strings to """
unicode. Defaults to the package default. Construct a template loader instance.
encoding: the file encoding. This is the name of the encoding to Arguments:
use when converting file contents to unicode. This name is
passed as the encoding argument to Python's built-in function
unicode(). Defaults to the encoding name returned by
sys.getdefaultencoding().
extension: the template file extension. Pass False for no extension: the template file extension. Pass False for no
extension (i.e. to use extensionless template files). extension (i.e. to use extensionless template files).
Defaults to the package default. Defaults to the package default.
""" file_encoding: the name of the encoding to use when converting file
if decode_errors is None: contents to unicode. Defaults to the package default.
decode_errors = defaults.DECODE_ERRORS
if encoding is None: to_unicode: the function to use when converting strings of type
encoding = sys.getdefaultencoding() str to unicode. The function should have the signature:
to_unicode(s, encoding=None)
It should accept a string of type str and an optional encoding
name and return a string of type unicode. Defaults to calling
Python's built-in function unicode() using the package encoding
and decode-errors defaults.
"""
if extension is None: if extension is None:
extension = defaults.TEMPLATE_EXTENSION extension = defaults.TEMPLATE_EXTENSION
self.decode_errors = decode_errors if file_encoding is None:
self.encoding = encoding file_encoding = defaults.FILE_ENCODING
if to_unicode is None:
to_unicode = _to_unicode
self.extension = extension self.extension = extension
self.file_encoding = file_encoding
self.to_unicode = to_unicode
# TODO: eliminate redundancy with the Renderer class's unicode code.
def unicode(self, s, encoding=None): def unicode(self, s, encoding=None):
""" """
Call Python's built-in function unicode(), and return the result. Convert a string to unicode using the given encoding, and return it.
This function uses the underlying to_unicode attribute.
Arguments:
For unicode strings (or unicode subclasses), this function calls s: a basestring instance to convert to unicode. Unlike Python's
Python's unicode() without the encoding and errors arguments. built-in unicode() function, it is okay to pass unicode strings
Thus, unlike Python's built-in unicode(), it is okay to pass unicode to this function. (Passing a unicode string to Python's unicode()
strings to this function. (Passing a unicode string to Python's with the encoding argument throws the error, "TypeError: decoding
unicode() with the encoding argument throws the following Unicode is not supported.")
error: "TypeError: decoding Unicode is not supported.")
encoding: the encoding to pass to the to_unicode attribute.
Defaults to None.
""" """
if isinstance(s, unicode): if isinstance(s, unicode):
return unicode(s) return unicode(s)
if encoding is None: return self.to_unicode(s, encoding)
encoding = self.encoding
return unicode(s, encoding, self.decode_errors)
def read(self, path, encoding=None): def read(self, path, encoding=None):
""" """
...@@ -79,6 +99,9 @@ class Loader(object): ...@@ -79,6 +99,9 @@ class Loader(object):
with open(path, 'r') as f: with open(path, 'r') as f:
text = f.read() text = f.read()
if encoding is None:
encoding = self.file_encoding
return self.unicode(text, encoding) return self.unicode(text, encoding)
# TODO: unit-test this method. # TODO: unit-test this method.
......
...@@ -5,9 +5,7 @@ This module provides a Renderer class to render templates. ...@@ -5,9 +5,7 @@ This module provides a Renderer class to render templates.
""" """
import cgi
import os import os
import sys
from . import defaults from . import defaults
from .context import Context from .context import Context
...@@ -15,12 +13,6 @@ from .loader import Loader ...@@ -15,12 +13,6 @@ from .loader import Loader
from .renderengine import RenderEngine from .renderengine import RenderEngine
# The quote=True argument causes double quotes to be escaped,
# but not single quotes:
# http://docs.python.org/library/cgi.html#cgi.escape
DEFAULT_ESCAPE = lambda s: cgi.escape(s, quote=True)
class Renderer(object): class Renderer(object):
""" """
...@@ -40,6 +32,7 @@ class Renderer(object): ...@@ -40,6 +32,7 @@ class Renderer(object):
""" """
# TODO: rename default_encoding to string_encoding.
def __init__(self, file_encoding=None, default_encoding=None, def __init__(self, file_encoding=None, default_encoding=None,
decode_errors=None, search_dirs=None, file_extension=None, decode_errors=None, search_dirs=None, file_extension=None,
escape=None, partials=None): escape=None, partials=None):
...@@ -82,7 +75,7 @@ class Renderer(object): ...@@ -82,7 +75,7 @@ class Renderer(object):
to unicode any strings of type str encountered during the to unicode any strings of type str encountered during the
rendering process. The name will be passed as the encoding rendering process. The name will be passed as the encoding
argument to the built-in function unicode(). Defaults to the argument to the built-in function unicode(). Defaults to the
encoding name returned by sys.getdefaultencoding(). package default.
decode_errors: the string to pass as the errors argument to the decode_errors: the string to pass as the errors argument to the
built-in function unicode() when converting str strings to built-in function unicode() when converting str strings to
...@@ -102,10 +95,10 @@ class Renderer(object): ...@@ -102,10 +95,10 @@ class Renderer(object):
decode_errors = defaults.DECODE_ERRORS decode_errors = defaults.DECODE_ERRORS
if default_encoding is None: if default_encoding is None:
default_encoding = sys.getdefaultencoding() default_encoding = defaults.STRING_ENCODING
if escape is None: if escape is None:
escape = DEFAULT_ESCAPE escape = defaults.TAG_ESCAPE
# This needs to be after we set the default default_encoding. # This needs to be after we set the default default_encoding.
if file_encoding is None: if file_encoding is None:
...@@ -121,6 +114,7 @@ class Renderer(object): ...@@ -121,6 +114,7 @@ class Renderer(object):
search_dirs = [search_dirs] search_dirs = [search_dirs]
self.decode_errors = decode_errors self.decode_errors = decode_errors
# TODO: rename this attribute to string_encoding.
self.default_encoding = default_encoding self.default_encoding = default_encoding
self.escape = escape self.escape = escape
self.file_encoding = file_encoding self.file_encoding = file_encoding
...@@ -152,7 +146,7 @@ class Renderer(object): ...@@ -152,7 +146,7 @@ class Renderer(object):
""" """
return unicode(self.escape(self._to_unicode_soft(s))) return unicode(self.escape(self._to_unicode_soft(s)))
def unicode(self, s): def unicode(self, s, encoding=None):
""" """
Convert a string to unicode, using default_encoding and decode_errors. Convert a string to unicode, using default_encoding and decode_errors.
...@@ -165,17 +159,20 @@ class Renderer(object): ...@@ -165,17 +159,20 @@ class Renderer(object):
TypeError: decoding Unicode is not supported TypeError: decoding Unicode is not supported
""" """
if encoding is None:
encoding = self.default_encoding
# TODO: Wrap UnicodeDecodeErrors with a message about setting # TODO: Wrap UnicodeDecodeErrors with a message about setting
# the default_encoding and decode_errors attributes. # the default_encoding and decode_errors attributes.
return unicode(s, self.default_encoding, self.decode_errors) return unicode(s, encoding, self.decode_errors)
def _make_loader(self): def _make_loader(self):
""" """
Create a Loader instance using current attributes. Create a Loader instance using current attributes.
""" """
return Loader(encoding=self.file_encoding, decode_errors=self.decode_errors, return Loader(file_encoding=self.file_encoding, extension=self.file_extension,
extension=self.file_extension) to_unicode=self.unicode)
def _make_load_template(self): def _make_load_template(self):
""" """
......
...@@ -146,10 +146,11 @@ class CustomLoaderTests(unittest.TestCase, AssertIsMixin, AssertStringMixin): ...@@ -146,10 +146,11 @@ class CustomLoaderTests(unittest.TestCase, AssertIsMixin, AssertStringMixin):
def test_init__defaults(self): def test_init__defaults(self):
custom = CustomLoader() custom = CustomLoader()
# Check the reader attribute. # Check the loader attribute.
loader = custom.loader loader = custom.loader
self.assertEquals(loader.decode_errors, 'strict') self.assertEquals(loader.extension, 'mustache')
self.assertEquals(loader.encoding, sys.getdefaultencoding()) self.assertEquals(loader.file_encoding, sys.getdefaultencoding())
to_unicode = loader.to_unicode
# Check search_dirs. # Check search_dirs.
self.assertEquals(custom.search_dirs, []) self.assertEquals(custom.search_dirs, [])
......
...@@ -10,40 +10,71 @@ import sys ...@@ -10,40 +10,71 @@ import sys
import unittest import unittest
from .common import AssertStringMixin from .common import AssertStringMixin
from pystache import defaults
from pystache.loader import Loader from pystache.loader import Loader
DATA_DIR = 'tests/data' DATA_DIR = 'tests/data'
class LoaderTestCase(unittest.TestCase, AssertStringMixin): class LoaderTests(unittest.TestCase, AssertStringMixin):
def _get_path(self, filename):
return os.path.join(DATA_DIR, filename)
def test_init__decode_errors(self):
# Test the default value.
reader = Loader()
self.assertEquals(reader.decode_errors, 'strict')
reader = Loader(decode_errors='replace')
self.assertEquals(reader.decode_errors, 'replace')
def test_init__encoding(self):
# Test the default value.
reader = Loader()
self.assertEquals(reader.encoding, sys.getdefaultencoding())
reader = Loader(encoding='foo')
self.assertEquals(reader.encoding, 'foo')
def test_init__extension(self): def test_init__extension(self):
loader = Loader(extension='foo')
self.assertEquals(loader.extension, 'foo')
def test_init__extension__default(self):
# Test the default value. # Test the default value.
reader = Loader() loader = Loader()
self.assertEquals(reader.extension, 'mustache') self.assertEquals(loader.extension, 'mustache')
def test_init__file_encoding(self):
loader = Loader(file_encoding='bar')
self.assertEquals(loader.file_encoding, 'bar')
def test_init__file_encoding__default(self):
file_encoding = defaults.FILE_ENCODING
try:
defaults.FILE_ENCODING = 'foo'
loader = Loader()
self.assertEquals(loader.file_encoding, 'foo')
finally:
defaults.FILE_ENCODING = file_encoding
def test_init__to_unicode(self):
to_unicode = lambda x: x
loader = Loader(to_unicode=to_unicode)
self.assertEquals(loader.to_unicode, to_unicode)
def test_init__to_unicode__default(self):
loader = Loader()
self.assertRaises(TypeError, loader.to_unicode, u"abc")
decode_errors = defaults.DECODE_ERRORS
string_encoding = defaults.STRING_ENCODING
nonascii = 'abcdé'
try:
defaults.DECODE_ERRORS = 'strict'
defaults.STRING_ENCODING = 'ascii'
loader = Loader()
self.assertRaises(UnicodeDecodeError, loader.to_unicode, nonascii)
defaults.DECODE_ERRORS = 'ignore'
loader = Loader()
self.assertString(loader.to_unicode(nonascii), u'abcd')
defaults.STRING_ENCODING = 'utf-8'
loader = Loader()
self.assertString(loader.to_unicode(nonascii), u'abcdé')
finally:
defaults.DECODE_ERRORS = decode_errors
defaults.STRING_ENCODING = string_encoding
reader = Loader(extension='foo') def _get_path(self, filename):
self.assertEquals(reader.extension, 'foo') return os.path.join(DATA_DIR, filename)
def test_unicode__basic__input_str(self): def test_unicode__basic__input_str(self):
""" """
...@@ -80,19 +111,24 @@ class LoaderTestCase(unittest.TestCase, AssertStringMixin): ...@@ -80,19 +111,24 @@ class LoaderTestCase(unittest.TestCase, AssertStringMixin):
self.assertString(actual, u"foo") self.assertString(actual, u"foo")
def test_unicode__encoding_attribute(self): def test_unicode__to_unicode__attribute(self):
""" """
Test unicode(): encoding attribute. Test unicode(): encoding attribute.
""" """
reader = Loader() reader = Loader()
non_ascii = u'é'.encode('utf-8') non_ascii = u'abcdé'.encode('utf-8')
self.assertRaises(UnicodeDecodeError, reader.unicode, non_ascii) self.assertRaises(UnicodeDecodeError, reader.unicode, non_ascii)
reader.encoding = 'utf-8' def to_unicode(s, encoding=None):
self.assertEquals(reader.unicode(non_ascii), u"é") if encoding is None:
encoding = 'utf-8'
return unicode(s, encoding)
reader.to_unicode = to_unicode
self.assertString(reader.unicode(non_ascii), u"abcdé")
def test_unicode__encoding_argument(self): def test_unicode__encoding_argument(self):
""" """
...@@ -101,13 +137,14 @@ class LoaderTestCase(unittest.TestCase, AssertStringMixin): ...@@ -101,13 +137,14 @@ class LoaderTestCase(unittest.TestCase, AssertStringMixin):
""" """
reader = Loader() reader = Loader()
non_ascii = u'é'.encode('utf-8') non_ascii = u'abcdé'.encode('utf-8')
self.assertRaises(UnicodeDecodeError, reader.unicode, non_ascii) self.assertRaises(UnicodeDecodeError, reader.unicode, non_ascii)
actual = reader.unicode(non_ascii, encoding='utf-8') actual = reader.unicode(non_ascii, encoding='utf-8')
self.assertEquals(actual, u'é') self.assertString(actual, u'abcdé')
# TODO: check the read() unit tests.
def test_read(self): def test_read(self):
""" """
Test read(). Test read().
...@@ -118,18 +155,18 @@ class LoaderTestCase(unittest.TestCase, AssertStringMixin): ...@@ -118,18 +155,18 @@ class LoaderTestCase(unittest.TestCase, AssertStringMixin):
actual = reader.read(path) actual = reader.read(path)
self.assertString(actual, u'ascii: abc') self.assertString(actual, u'ascii: abc')
def test_read__encoding__attribute(self): def test_read__file_encoding__attribute(self):
""" """
Test read(): encoding attribute respected. Test read(): file_encoding attribute respected.
""" """
reader = Loader() loader = Loader()
path = self._get_path('non_ascii.mustache') path = self._get_path('non_ascii.mustache')
self.assertRaises(UnicodeDecodeError, reader.read, path) self.assertRaises(UnicodeDecodeError, loader.read, path)
reader.encoding = 'utf-8' loader.file_encoding = 'utf-8'
actual = reader.read(path) actual = loader.read(path)
self.assertString(actual, u'non-ascii: é') self.assertString(actual, u'non-ascii: é')
def test_read__encoding__argument(self): def test_read__encoding__argument(self):
...@@ -145,9 +182,9 @@ class LoaderTestCase(unittest.TestCase, AssertStringMixin): ...@@ -145,9 +182,9 @@ class LoaderTestCase(unittest.TestCase, AssertStringMixin):
actual = reader.read(path, encoding='utf-8') actual = reader.read(path, encoding='utf-8')
self.assertString(actual, u'non-ascii: é') self.assertString(actual, u'non-ascii: é')
def test_get__decode_errors(self): def test_reader__to_unicode__attribute(self):
""" """
Test get(): decode_errors attribute. Test read(): to_unicode attribute respected.
""" """
reader = Loader() reader = Loader()
...@@ -155,7 +192,7 @@ class LoaderTestCase(unittest.TestCase, AssertStringMixin): ...@@ -155,7 +192,7 @@ class LoaderTestCase(unittest.TestCase, AssertStringMixin):
self.assertRaises(UnicodeDecodeError, reader.read, path) self.assertRaises(UnicodeDecodeError, reader.read, path)
reader.decode_errors = 'ignore' #reader.decode_errors = 'ignore'
actual = reader.read(path) #actual = reader.read(path)
self.assertString(actual, u'non-ascii: ') #self.assertString(actual, u'non-ascii: ')
...@@ -196,19 +196,21 @@ class RendererTestCase(unittest.TestCase): ...@@ -196,19 +196,21 @@ class RendererTestCase(unittest.TestCase):
def test__make_loader__attributes(self): def test__make_loader__attributes(self):
""" """
Test that _make_locator() sets all attributes correctly.. Test that _make_loader() sets all attributes correctly..
""" """
unicode_ = lambda x: x
renderer = Renderer() renderer = Renderer()
renderer.decode_errors = 'dec'
renderer.file_encoding = 'enc' renderer.file_encoding = 'enc'
renderer.file_extension = 'ext' renderer.file_extension = 'ext'
renderer.unicode = unicode_
loader = renderer._make_loader() loader = renderer._make_loader()
self.assertEquals(loader.decode_errors, 'dec')
self.assertEquals(loader.encoding, 'enc')
self.assertEquals(loader.extension, 'ext') self.assertEquals(loader.extension, 'ext')
self.assertEquals(loader.file_encoding, 'enc')
self.assertEquals(loader.to_unicode, unicode_)
## Test the render() method. ## Test the render() method.
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment