diff options
-rw-r--r-- | CHANGES.rst | 2 | ||||
-rw-r--r-- | jinja2/loaders.py | 128 | ||||
-rw-r--r-- | tests/res/package.zip | bin | 0 -> 1036 bytes | |||
-rw-r--r-- | tests/test_loader.py | 60 |
4 files changed, 136 insertions, 54 deletions
diff --git a/CHANGES.rst b/CHANGES.rst index 381f78e..477721f 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -47,6 +47,8 @@ Unreleased - Fix behavior of ``loop`` control variables such as ``length`` and ``revindex0`` when looping over a generator. :issue:`459, 751, 794`, :pr:`993` +- ``PackageLoader`` doesn't depend on setuptools or pkg_resources. + :issue:`970` Version 2.10.3 diff --git a/jinja2/loaders.py b/jinja2/loaders.py index 4c79793..031fa7a 100644 --- a/jinja2/loaders.py +++ b/jinja2/loaders.py @@ -9,6 +9,7 @@ :license: BSD, see LICENSE for more details. """ import os +import pkgutil import sys import weakref from types import ModuleType @@ -203,66 +204,99 @@ class FileSystemLoader(BaseLoader): class PackageLoader(BaseLoader): - """Load templates from python eggs or packages. It is constructed with - the name of the python package and the path to the templates in that - package:: + """Load templates from a directory in a Python package. - loader = PackageLoader('mypackage', 'views') + :param package_name: Import name of the package that contains the + template directory. + :param package_path: Directory within the imported package that + contains the templates. + :param encoding: Encoding of template files. - If the package path is not given, ``'templates'`` is assumed. + The following example looks up templates in the ``pages`` directory + within the ``project.ui`` package. - Per default the template encoding is ``'utf-8'`` which can be changed - by setting the `encoding` parameter to something else. Due to the nature - of eggs it's only possible to reload templates if the package was loaded - from the file system and not a zip file. + .. code-block:: python + + loader = PackageLoader("project.ui", "pages") + + Only packages installed as directories (standard pip behavior) or + zip/egg files (less common) are supported. The Python API for + introspecting data in packages is too limited to support other + installation methods the way this loader requires. + + .. versionchanged:: 2.11.0 + No longer uses ``setuptools`` as a dependency. """ - def __init__(self, package_name, package_path='templates', - encoding='utf-8'): - from pkg_resources import DefaultProvider, ResourceManager, \ - get_provider - provider = get_provider(package_name) - self.encoding = encoding - self.manager = ResourceManager() - self.filesystem_bound = isinstance(provider, DefaultProvider) - self.provider = provider + def __init__(self, package_name, package_path="templates", encoding="utf-8"): + if package_path == os.path.curdir: + package_path = "" + elif package_path[:2] == os.path.curdir + "/": + package_path = package_path[2:] + + self.package_name = package_name self.package_path = package_path + self.encoding = encoding + + self._loader = pkgutil.get_loader(package_name) + # Zip loader's archive attribute points at the zip. + self._archive = getattr(self._loader, "archive", None) + self._template_root = os.path.join( + os.path.dirname(self._loader.get_filename(package_name)), package_path + ).rstrip(os.path.sep) def get_source(self, environment, template): - pieces = split_template_path(template) - p = '/'.join((self.package_path,) + tuple(pieces)) - if not self.provider.has_resource(p): - raise TemplateNotFound(template) + p = "/".join([self._template_root] + split_template_path(template)) - filename = uptodate = None - if self.filesystem_bound: - filename = self.provider.get_resource_filename(self.manager, p) - mtime = path.getmtime(filename) - def uptodate(): - try: - return path.getmtime(filename) == mtime - except OSError: - return False + if self._archive is None: + # Package is a directory. + if not os.path.isfile(p): + raise TemplateNotFound(template) + + with open(p, "rb") as f: + source = f.read() - source = self.provider.get_resource_string(self.manager, p) - return source.decode(self.encoding), filename, uptodate + mtime = os.path.getmtime(p) + + def up_to_date(): + return os.path.isfile(p) and os.path.getmtime(p) == mtime + else: + # Package is a zip file. + try: + source = self._loader.get_data(p) + except OSError: + raise TemplateNotFound(template) + + # Could use the zip's mtime for all template mtimes, but + # would need to safely reload the module if it's out of + # date, so just report it as always current. + up_to_date = None + + return source.decode(self.encoding), p, up_to_date def list_templates(self): - path = self.package_path - if path[:2] == './': - path = path[2:] - elif path == '.': - path = '' - offset = len(path) results = [] - def _walk(path): - for filename in self.provider.resource_listdir(path): - fullname = path + '/' + filename - if self.provider.resource_isdir(fullname): - _walk(fullname) - else: - results.append(fullname[offset:].lstrip('/')) - _walk(path) + + if self._archive is None: + # Package is a directory. + offset = len(self._template_root) + + for dirpath, _, filenames in os.walk(self._template_root): + dirpath = dirpath[offset:].lstrip(os.path.sep) + results.extend(os.path.join(dirpath, name) for name in filenames) + else: + # Package is a zip file. + prefix = ( + self._template_root[len(self._archive):].lstrip(os.path.sep) + + os.path.sep + ) + offset = len(prefix) + + for name in self._loader._files.keys(): + # Find names under the templates directory that aren't directories. + if name.startswith(prefix) and name[-1] != os.path.sep: + results.append(name[offset:]) + results.sort() return results diff --git a/tests/res/package.zip b/tests/res/package.zip Binary files differnew file mode 100644 index 0000000..d4c9ce9 --- /dev/null +++ b/tests/res/package.zip diff --git a/tests/test_loader.py b/tests/test_loader.py index cb30ebe..13ce836 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -9,21 +9,24 @@ :license: BSD, see LICENSE for more details. """ import os +import shutil import sys import tempfile -import shutil -import pytest import weakref -from jinja2 import Environment, loaders -from jinja2._compat import PYPY, PY2 -from jinja2.loaders import split_template_path +import pytest + +from jinja2 import Environment +from jinja2 import loaders +from jinja2 import PackageLoader +from jinja2._compat import PY2 +from jinja2._compat import PYPY from jinja2.exceptions import TemplateNotFound +from jinja2.loaders import split_template_path @pytest.mark.loaders class TestLoaders(object): - def test_dict_loader(self, dict_loader): env = Environment(loader=dict_loader) tmpl = env.get_template('justdict.html') @@ -54,7 +57,6 @@ class TestLoaders(object): # This would raise NotADirectoryError if "t2/foo" wasn't skipped. e.get_template("foo/test.html") - def test_choice_loader(self, choice_loader): env = Environment(loader=choice_loader) tmpl = env.get_template('justdict.html') @@ -243,3 +245,47 @@ class TestModuleLoader(object): assert tmpl1.render() == 'BAR' tmpl2 = self.mod_env.get_template('DICT/test.html') assert tmpl2.render() == 'DICT_TEMPLATE' + + +@pytest.fixture() +def package_dir_loader(monkeypatch): + monkeypatch.syspath_prepend(os.path.dirname(__file__)) + return PackageLoader("res") + + +@pytest.mark.parametrize( + ("template", "expect"), [("foo/test.html", "FOO"), ("test.html", "BAR")] +) +def test_package_dir_source(package_dir_loader, template, expect): + source, name, up_to_date = package_dir_loader.get_source(None, template) + assert source.rstrip() == expect + assert name.endswith(os.path.join(package_dir_loader.package_path, template)) + assert up_to_date() + + +def test_package_dir_list(package_dir_loader): + templates = package_dir_loader.list_templates() + assert "foo/test.html" in templates + assert "test.html" in templates + + +@pytest.fixture() +def package_zip_loader(monkeypatch): + monkeypatch.syspath_prepend( + os.path.join(os.path.dirname(__file__), "res", "package.zip") + ) + return PackageLoader("t_pack") + + +@pytest.mark.parametrize( + ("template", "expect"), [("foo/test.html", "FOO"), ("test.html", "BAR")] +) +def test_package_zip_source(package_zip_loader, template, expect): + source, name, up_to_date = package_zip_loader.get_source(None, template) + assert source.rstrip() == expect + assert name.endswith(os.path.join(package_zip_loader.package_path, template)) + assert up_to_date is None + + +def test_package_zip_list(package_zip_loader): + assert package_zip_loader.list_templates() == ["foo/test.html", "test.html"] |