PackageLoader doesn't depend on setuptools
diff --git a/CHANGES.rst b/CHANGES.rst
index 381f78e..477721f 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -47,6 +47,8 @@
- 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..eb5a879 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,110 @@
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 + os.path.sep:
+ package_path = package_path[2:]
+
+ package_path = os.path.normpath(package_path)
+
+ 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 = os.path.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)
- source = self.provider.get_resource_string(self.manager, p)
- return source.decode(self.encoding), filename, uptodate
+ with open(p, "rb") as f:
+ source = f.read()
+
+ 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).replace(os.path.sep, "/")
+ for name in filenames
+ )
+ else:
+ if not hasattr(self._loader, "_files"):
+ raise TypeError(
+ "This zip import does not have the required"
+ " metadata to list templates."
+ )
+
+ # 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:].replace(os.path.sep, "/"))
+
results.sort()
return results
diff --git a/tests/res/package.zip b/tests/res/package.zip
new file mode 100644
index 0000000..d4c9ce9
--- /dev/null
+++ b/tests/res/package.zip
Binary files differ
diff --git a/tests/test_loader.py b/tests/test_loader.py
index cb30ebe..3445ba8 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 @@
# 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,52 @@
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(*split_template_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(*split_template_path(template)))
+ assert up_to_date is None
+
+
+@pytest.mark.xfail(
+ PYPY,
+ reason="PyPy's zipimporter doesn't have a _files attribute.",
+ raises=TypeError,
+)
+def test_package_zip_list(package_zip_loader):
+ assert package_zip_loader.list_templates() == ["foo/test.html", "test.html"]