PackageLoader doesn't depend on setuptools

This commit is contained in:
David Lord 2019-10-16 13:23:36 -07:00
parent 81b32242e5
commit 4b6077a8c0
No known key found for this signature in database
GPG Key ID: 7A1C87E3F5BC42A8
4 changed files with 152 additions and 54 deletions

View File

@ -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

View File

@ -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

Binary file not shown.

View File

@ -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"]