Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions copier/_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -657,8 +657,14 @@ def jinja_env(self) -> SandboxedEnvironment:

Respects template settings.
"""
paths = [str(self.template.local_abspath)]
loader = FileSystemLoader(paths)
template_path = str(self.template.local_abspath)
loader = FileSystemLoader([template_path])
# Add template directory to sys.path so that template-local Python packages
# are importable as or by Jinja extensions and their transitive imports.
if template_path not in sys.path and (
self.unsafe or is_trusted_repository(self.settings.trust, self.template.url)
):
sys.path.insert(0, template_path)
default_extensions = [
"jinja2_ansible_filters.AnsibleCoreFiltersExtension",
YieldExtension,
Expand Down
6 changes: 6 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,3 +119,9 @@ def settings_path(config_path: Path) -> Path:
config_path.mkdir()
settings_path = config_path / "settings.yml"
return settings_path


@pytest.fixture(autouse=True)
def sys_path_cleanup(monkeypatch: pytest.MonkeyPatch) -> None:
# Ensure that `sys.path` is restored after each test
monkeypatch.setattr("sys.path", [*sys.path])
4 changes: 1 addition & 3 deletions tests/test_context.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import json
import sys
from pathlib import Path
from uuid import uuid4

Expand All @@ -12,7 +11,7 @@


def test_no_path_variables(
tmp_path_factory: pytest.TempPathFactory, monkeypatch: pytest.MonkeyPatch
tmp_path_factory: pytest.TempPathFactory,
) -> None:
"""Test that there are no context variables of type `pathlib.Path`."""
src, dst = map(tmp_path_factory.mktemp, ("src", "dst"))
Expand Down Expand Up @@ -60,7 +59,6 @@ def _assert(self, ctx: Context) -> None:
src / "test.txt.jinja": "{{ __assert() | default('', true) }}",
}
)
monkeypatch.setattr("sys.path", [str(src), *sys.path])
copier.run_copy(str(src), dst, unsafe=True)
assert (dst / "test.txt").read_text("utf-8") == ""

Expand Down
77 changes: 76 additions & 1 deletion tests/test_jinja2_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@
import pytest
from jinja2 import Environment
from jinja2.ext import Extension
from plumbum import local

import copier

from .helpers import PROJECT_TEMPLATE, build_file_tree
from .helpers import PROJECT_TEMPLATE, build_file_tree, git_save


class FilterExtension(Extension):
Expand Down Expand Up @@ -36,6 +37,39 @@ def super_func(argument: Any) -> str:
environment.globals.update(super_var="super var!")


@pytest.fixture
def template_with_extension(tmp_path_factory: pytest.TempPathFactory) -> Path:
src = tmp_path_factory.mktemp("src")
build_file_tree(
{
src / "extensions" / "__init__.py": "",
src / "extensions" / "data.py": """\
DATA_VALUE = "hello from data module"
""",
src / "extensions" / "filters.py": """\
from jinja2 import Environment
from jinja2.ext import Extension

from extensions import data


class DataFilter(Extension):
def __init__(self, environment: Environment) -> None:
super().__init__(environment)
environment.filters["data_value"] = lambda _: data.DATA_VALUE
""",
src / "copier.yml": """\
_jinja_extensions:
- extensions.filters.DataFilter
""",
src
/ "{{ _copier_conf.answers_file }}.jinja": "{{ _copier_answers|to_yaml }}",
src / "result.txt.jinja": "{{ '' | data_value }}",
}
)
return src


def test_default_jinja2_extensions(tmp_path: Path) -> None:
copier.run_copy(str(PROJECT_TEMPLATE) + "_extensions_default", tmp_path)
super_file = tmp_path / "super_file.md"
Expand Down Expand Up @@ -64,3 +98,44 @@ def test_to_json_filter_with_conf(tmp_path_factory: pytest.TempPathFactory) -> N
assert conf_file.exists()
# must not raise an error
assert json.loads(conf_file.read_text())


def test_extension_from_copy_with_vcs_ref(
template_with_extension: Path, tmp_path_factory: pytest.TempPathFactory
) -> None:
dst = tmp_path_factory.mktemp("dst")
with local.cwd(template_with_extension):
git_save(tag="1.0.0")
copier.run_copy(str(template_with_extension), dst, unsafe=True, vcs_ref="HEAD")
result = dst / "result.txt"
assert result.exists()
assert result.read_text() == "hello from data module"


def test_extension_on_update(
template_with_extension: Path, tmp_path_factory: pytest.TempPathFactory
) -> None:
dst = tmp_path_factory.mktemp("dst")
with local.cwd(template_with_extension):
git_save(tag="1.0.0")
# Initial copy
copier.run_copy(
str(template_with_extension),
dst,
unsafe=True,
vcs_ref="1.0.0",
defaults=True,
overwrite=True,
)
assert (dst / "result.txt").read_text() == "hello from data module"
with local.cwd(dst):
git_save()
# Add a new file in v2 (extension stays the same)
build_file_tree({template_with_extension / "v2.txt": "new in v2"})
with local.cwd(template_with_extension):
git_save(tag="2.0.0")
# Run update — extension should work because template is trusted
copier.run_update(dst, unsafe=True, defaults=True, overwrite=True)
# Extension still works during update rendering
assert (dst / "result.txt").read_text() == "hello from data module"
assert (dst / "v2.txt").read_text() == "new in v2"
Loading