From a524e77eec22589e4e657550233a5c3bcf24d046 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 10 Mar 2020 10:02:47 -0700 Subject: Use importlib machinery to fix PEP 451 import hooks --- src/jinja2/loaders.py | 27 +++++++++++++-------------- tests/test_loader.py | 41 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 14 deletions(-) diff --git a/src/jinja2/loaders.py b/src/jinja2/loaders.py index d5c45c4..ca42b7f 100644 --- a/src/jinja2/loaders.py +++ b/src/jinja2/loaders.py @@ -1,10 +1,11 @@ """API and implementations for loading templates from different data sources. """ +import importlib.util import os -import pkgutil import sys import weakref +import zipimport from collections import abc from hashlib import sha1 from importlib import import_module @@ -253,21 +254,19 @@ class PackageLoader(BaseLoader): # Make sure the package exists. This also makes namespace # packages work, otherwise get_loader returns None. import_module(package_name) - self._loader = loader = pkgutil.get_loader(package_name) + spec = importlib.util.find_spec(package_name) + self._loader = spec.loader - # Zip loader's archive attribute points at the zip. - self._archive = getattr(loader, "archive", None) + self._archive = None self._template_root = None - - if hasattr(loader, "get_filename"): - # A standard directory package, or a zip package. - self._template_root = os.path.join( - os.path.dirname(loader.get_filename(package_name)), package_path - ) - elif hasattr(loader, "_path"): - # A namespace package, limited support. Find the first - # contributor with the template directory. - for root in loader._path: + if isinstance(spec.loader, zipimport.zipimporter): + self._archive = spec.loader.archive + pkgdir = next(iter(spec.submodule_search_locations)) + self._template_root = os.path.join(pkgdir, package_path) + elif spec.submodule_search_locations: + # this will be one element for "packages" and multiple for + # namespace packages + for root in spec.submodule_search_locations: root = os.path.join(root, package_path) if os.path.isdir(root): diff --git a/tests/test_loader.py b/tests/test_loader.py index 1e08133..d3fe61e 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -1,3 +1,6 @@ +import importlib.abc +import importlib.machinery +import importlib.util import os import platform import shutil @@ -347,3 +350,41 @@ def test_package_zip_source(package_zip_loader, template, expect): ) def test_package_zip_list(package_zip_loader): assert package_zip_loader.list_templates() == ["foo/test.html", "test.html"] + + +def test_pep_451_import_hook(tmp_path): + package_name = "_my_pep451_pkg" + pkg = tmp_path.joinpath(package_name) + pkg.mkdir() + pkg.joinpath("__init__.py").touch() + templates = pkg.joinpath("templates") + templates.mkdir() + templates.joinpath("foo.html").write_text("hello world") + + class ImportHook(importlib.abc.MetaPathFinder, importlib.abc.Loader): + def find_spec(self, name, path=None, target=None): + if name != package_name: + return None + path = [str(tmp_path)] + spec = importlib.machinery.PathFinder.find_spec(name, path=path) + return importlib.util.spec_from_file_location( + name, + spec.origin, + loader=self, + submodule_search_locations=spec.submodule_search_locations, + ) + + def create_module(self, spec): + return None # default behaviour is fine + + def exec_module(self, module): + return None # we need this to satisfy the interface, it's wrong + + # ensure we restore `sys.meta_path` after putting in our loader + before = sys.meta_path[:] + try: + sys.meta_path.insert(0, ImportHook()) + package_loader = PackageLoader(package_name) + assert package_loader.list_templates() == ["foo.html"] + finally: + sys.meta_path[:] = before -- cgit v1.2.1 From c0675781a569b4d54f23ecbfcc6d4730d11ded01 Mon Sep 17 00:00:00 2001 From: David Lord Date: Mon, 30 Mar 2020 12:55:40 -0700 Subject: add changelog and cleanup for pep 451 support --- CHANGES.rst | 2 ++ src/jinja2/loaders.py | 12 ++++++------ tests/test_loader.py | 21 +++++++-------------- 3 files changed, 15 insertions(+), 20 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 580d411..1911ab4 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -9,6 +9,8 @@ Unreleased - Bump MarkupSafe dependency to >=1.1. - Bump Babel optional dependency to >=2.1. - Remove code that was marked deprecated. +- Use :pep:`451` API to load templates with + :class:`~loaders.PackageLoader`. :issue:`1168` 2.11.2 diff --git a/src/jinja2/loaders.py b/src/jinja2/loaders.py index ca42b7f..6e546c0 100644 --- a/src/jinja2/loaders.py +++ b/src/jinja2/loaders.py @@ -255,17 +255,17 @@ class PackageLoader(BaseLoader): # packages work, otherwise get_loader returns None. import_module(package_name) spec = importlib.util.find_spec(package_name) - self._loader = spec.loader - + self._loader = loader = spec.loader self._archive = None self._template_root = None - if isinstance(spec.loader, zipimport.zipimporter): - self._archive = spec.loader.archive + + if isinstance(loader, zipimport.zipimporter): + self._archive = loader.archive pkgdir = next(iter(spec.submodule_search_locations)) self._template_root = os.path.join(pkgdir, package_path) elif spec.submodule_search_locations: - # this will be one element for "packages" and multiple for - # namespace packages + # This will be one element for regular packages and multiple + # for namespace packages. for root in spec.submodule_search_locations: root = os.path.join(root, package_path) diff --git a/tests/test_loader.py b/tests/test_loader.py index d3fe61e..8ca1289 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -352,21 +352,13 @@ def test_package_zip_list(package_zip_loader): assert package_zip_loader.list_templates() == ["foo/test.html", "test.html"] -def test_pep_451_import_hook(tmp_path): - package_name = "_my_pep451_pkg" - pkg = tmp_path.joinpath(package_name) - pkg.mkdir() - pkg.joinpath("__init__.py").touch() - templates = pkg.joinpath("templates") - templates.mkdir() - templates.joinpath("foo.html").write_text("hello world") - +def test_pep_451_import_hook(): class ImportHook(importlib.abc.MetaPathFinder, importlib.abc.Loader): def find_spec(self, name, path=None, target=None): - if name != package_name: + if name != "res": return None - path = [str(tmp_path)] - spec = importlib.machinery.PathFinder.find_spec(name, path=path) + + spec = importlib.machinery.PathFinder.find_spec(name) return importlib.util.spec_from_file_location( name, spec.origin, @@ -382,9 +374,10 @@ def test_pep_451_import_hook(tmp_path): # ensure we restore `sys.meta_path` after putting in our loader before = sys.meta_path[:] + try: sys.meta_path.insert(0, ImportHook()) - package_loader = PackageLoader(package_name) - assert package_loader.list_templates() == ["foo.html"] + package_loader = PackageLoader("res") + assert "test.html" in package_loader.list_templates() finally: sys.meta_path[:] = before -- cgit v1.2.1