From 4b6077a8c0a0f56bb8dbac37f8f9027130b4091c Mon Sep 17 00:00:00 2001 From: David Lord Date: Wed, 16 Oct 2019 13:23:36 -0700 Subject: [PATCH] PackageLoader doesn't depend on setuptools --- CHANGES.rst | 2 + jinja2/loaders.py | 139 ++++++++++++++++++++++++++++-------------- tests/res/package.zip | Bin 0 -> 1036 bytes tests/test_loader.py | 65 +++++++++++++++++--- 4 files changed, 152 insertions(+), 54 deletions(-) create mode 100644 tests/res/package.zip diff --git a/CHANGES.rst b/CHANGES.rst index 381f78e8..477721f2 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 4c797937..eb5a879d 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 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) + + 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) - source = self.provider.get_resource_string(self.manager, p) - return source.decode(self.encoding), filename, uptodate + # 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 0000000000000000000000000000000000000000..d4c9ce9cea96284a040c61f78e373062436bc4ad GIT binary patch literal 1036 zc-jl_W@h1H00G0S0DlG~z|J7UP!eB|n4GO28p6rIe6MPG>|-D9*0WO>2NzyKx! z;09Qs86b!-pd>Z7ASbaTwHRX52cR(@@EHYi%hb9^P8J{=gr%?<1;izK86~+naOd5_ zG}Fm3h${f6A4Rd5o0gxikIzU{iX z?7?O%Qn(U!HfH1_JNpSX^C3xrIHOS$0