PackageLoader doesn't depend on setuptools
This commit is contained in:
parent
81b32242e5
commit
4b6077a8c0
@ -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
|
||||
|
||||
@ -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 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 + 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
|
||||
|
||||
|
||||
BIN
tests/res/package.zip
Normal file
BIN
tests/res/package.zip
Normal file
Binary file not shown.
@ -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,52 @@ 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(*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"]
|
||||
|
||||
Loading…
Reference in New Issue
Block a user