Repository: copier-org/copier
Pull request #1835 (2024-10-27 01:45:40)

build(flake): update lock
Automated changes by the [update-flake-lock](https://github.com/DeterminateSystems/update-flake-lock) GitHub Action.

```
Flake lock file updates:

• Updated input 'devenv':
    'github:cachix/devenv/e6464200390e502e3cff45f82b6dbe8c6760fab5?narHash=sha256-FhlknassIb3rKEucqnfFAzgny1ANmenJcTyRaXYwbA0%3D' (2024-10-17)
  → 'github:cachix/devenv/6c987a8795eedea872afe4d1c1ac518d0c7f6db1?narHash=sha256-ax0264nOyPcTJvIJAnPKGfkfXQ8Oe8ZVFziKf3UV26o%3D' (2025-02-11)
• Updated input 'devenv/cachix':
    'github:cachix/cachix/695525f9086542dfb09fde0871dbf4174abbf634?narHash=sha256-jOsaBmJ/EtX5t/vbylCdS7pWYcKGmWOKg4QKUzKr6dA%3D' (2024-09-16)
  → 'github:cachix/cachix/f65a3cd5e339c223471e64c051434616e18cc4f5?narHash=sha256-8HFvG7fvIFbgtaYAY2628Tb89fA55nPm2jSiNs0/Cws%3D' (2025-01-23)
• Updated input 'devenv/cachix/devenv':
    'github:cachix/devenv/ff5eb4f2accbcda963af67f1a1159e3f6c7f5f91?narHash=sha256-0JrfahRMJ37Rf1i0iOOn%2B8Z4CLvbcGNwa2ChOAVrp/8%3D' (2024-08-08)
  → follows 'devenv'
• Removed input 'devenv/cachix/devenv/cachix'
• Removed input 'devenv/cachix/devenv/cachix/devenv'
• Removed input 'devenv/cachix/devenv/cachix/devenv/flake-compat'
• Removed input 'devenv/cachix/devenv/cachix/devenv/nix'
• Removed input 'devenv/cachix/devenv/cachix/devenv/nix/flake-compat'
• Removed input 'devenv/cachix/devenv/cachix/devenv/nix/nixpkgs'
• Removed input 'devenv/cachix/devenv/cachix/devenv/nix/nixpkgs-regression'
• Removed input 'devenv/cachix/devenv/cachix/devenv/nixpkgs'
• Removed input 'devenv/cachix/devenv/cachix/devenv/poetry2nix'
• Removed input 'devenv/cachix/devenv/cachix/devenv/poetry2nix/flake-utils'
• Removed input 'devenv/cachix/devenv/cachix/devenv/poetry2nix/flake-utils/systems'
• Removed input 'devenv/cachix/devenv/cachix/devenv/poetry2nix/nix-github-actions'
• Removed input 'devenv/cachix/devenv/cachix/devenv/poetry2nix/nix-github-actions/nixpkgs'
• Removed input 'devenv/cachix/devenv/cachix/devenv/poetry2nix/nixpkgs'
• Removed input 'devenv/cachix/devenv/cachix/devenv/pre-commit-hooks'
• Removed input 'devenv/cachix/devenv/cachix/flake-compat'
• Removed input 'devenv/cachix/devenv/cachix/nixpkgs'
• Removed input 'devenv/cachix/devenv/cachix/pre-commit-hooks'
• Removed input 'devenv/cachix/devenv/flake-compat'
• Removed input 'devenv/cachix/devenv/nix'
• Removed input 'devenv/cachix/devenv/nix/flake-compat'
• Removed input 'devenv/cachix/devenv/nix/nixpkgs'
• Removed input 'devenv/cachix/devenv/nix/nixpkgs-regression'
• Removed input 'devenv/cachix/devenv/nixpkgs'
• Removed input 'devenv/cachix/devenv/pre-commit-hooks'
• Updated input 'devenv/cachix/flake-compat':
    follows 'devenv/flake-compat'
  → follows 'devenv'
• Updated input 'devenv/cachix/git-hooks':
    follows 'devenv/pre-commit-hooks'
  → follows 'devenv'
• Updated input 'devenv/cachix/nixpkgs':
    follows 'devenv/nixpkgs'
  → 'github:NixOS/nixpkgs/55d15ad12a74eb7d4646254e13638ad0c4128776?narHash=sha256-M1%2BuCoV5igihRfcUKrr1riygbe73/dzNnzPsmaLCmpo%3D' (2024-12-03)
• Updated input 'devenv/flake-compat':
    'github:edolstra/flake-compat/0f9255e01c2351cc7d116c072cb317785dd33b33?narHash=sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U%3D' (2023-10-04)
  → 'github:edolstra/flake-compat/ff81ac966bb2cae68946d5ed5fc4994f96d0ffec?narHash=sha256-NeCCThCEP3eCl2l/%2B27kNNK7QrwZB1IJCrXfrbv5oqU%3D' (2024-12-04)
• Added input 'devenv/git-hooks':
    'github:cachix/git-hooks.nix/9364dc02281ce2d37a1f55b6e51f7c0f65a75f17?narHash=sha256-R10v2hoJRLq8jcL4syVFag7nIGE7m13qO48wRIukWNg%3D' (2025-01-21)
• Added input 'devenv/git-hooks/flake-compat':
    follows 'devenv'
• Added input 'devenv/git-hooks/gitignore':
    'github:hercules-ci/gitignore.nix/637db329424fd7e46cf4185293b9cc8c88c95394?narHash=sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs%3D' (2024-02-28)
• Added input 'devenv/git-hooks/gitignore/nixpkgs':
    follows 'devenv/git-hooks/nixpkgs'
• Added input 'devenv/git-hooks/nixpkgs':
    follows 'devenv/nixpkgs'
• Updated input 'devenv/nix':
    'github:domenkozar/nix/f6c5ae4c1b2e411e6b1e6a8181cc84363d6a7546?narHash=sha256-X8ES7I1cfNhR9oKp06F6ir4Np70WGZU5sfCOuNBEwMg%3D' (2024-09-27)
  → 'github:domenkozar/nix/bde6a1a0d1f2af86caa4d20d23eca019f3d57eee?narHash=sha256-n52PUzub5jZWc8nI/sR7UICOheU8rNA%2BYZ73YaHeCBg%3D' (2024-12-13)
• Updated input 'devenv/nix/flake-compat':
    follows 'devenv/flake-compat'
  → follows 'devenv'
• Updated input 'devenv/nix/nixpkgs-23-11':
    'github:NixOS/nixpkgs/a62e6edd6d5e1fa0329b8653c801147986f8d446?narHash=sha256-oamiKNfr2MS6yH64rUn99mIZjc45nGJlj9eGth/3Xuw%3D' (2024-05-31)
  → follows 'devenv'
• Updated input 'devenv/nix/nixpkgs-regression':
    'github:NixOS/nixpkgs/215d4d0fd80ca5163643b03a33fde804a29cc1e2?narHash=sha256-uGJ0VXIhWKGXxkeNnq4TvV3CIOkUJ3PAoLZ3HMzNVMw%3D' (2022-01-24)
  → follows 'devenv'
• Updated input 'devenv/nix/pre-commit-hooks':
    'github:cachix/pre-commit-hooks.nix/40e6053ecb65fcbf12863338a6dcefb3f55f1bf8?narHash=sha256-nMirxrGteNAl9sWiOhoN5tIHyjBbVi5e2tgZUgZlK3Y%3D' (2024-04-12)
  → follows 'devenv'
• Removed input 'devenv/nix/pre-commit-hooks/flake-compat'
• Removed input 'devenv/nix/pre-commit-hooks/flake-utils'
• Removed input 'devenv/nix/pre-commit-hooks/gitignore'
• Removed input 'devenv/nix/pre-commit-hooks/nixpkgs'
• Removed input 'devenv/nix/pre-commit-hooks/nixpkgs-stable'
• Updated input 'devenv/nixpkgs':
    'github:cachix/devenv-nixpkgs/4267e705586473d3e5c8d50299e71503f16a6fb6?narHash=sha256-Q1UQzYcMJH4RscmpTkjlgqQDX5yi1tZL0O345Ri6vXQ%3D' (2024-05-29)
  → 'github:cachix/devenv-nixpkgs/7bd9e84d0452f6d2e63b6e6da29fe73fac951857?narHash=sha256-qamMCz5mNpQmgBwc8SB5tVMlD5sbwVIToVZtSxMph9s%3D' (2024-12-06)
• Removed input 'devenv/pre-commit-hooks'
• Removed input 'devenv/pre-commit-hooks/flake-compat'
• Removed input 'devenv/pre-commit-hooks/gitignore'
• Removed input 'devenv/pre-commit-hooks/gitignore/nixpkgs'
• Removed input 'devenv/pre-commit-hooks/nixpkgs'
• Removed input 'devenv/pre-commit-hooks/nixpkgs-stable'
• Updated input 'flake-compat':
    'https://api.flakehub.com/f/pinned/edolstra/flake-compat/1.0.1/018afb31-abd1-7bff-a5e4-cff7e18efb7a/source.tar.gz?narHash=sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U%3D' (2023-10-04)
  → 'https://api.flakehub.com/f/pinned/edolstra/flake-compat/1.1.0/01948eb7-9cba-704f-bbf3-3fa956735b52/source.tar.gz?narHash=sha256-NeCCThCEP3eCl2l/%2B27kNNK7QrwZB1IJCrXfrbv5oqU%3D' (2024-12-04)
• Updated input 'flake-utils':
    'https://api.flakehub.com/f/pinned/numtide/flake-utils/0.1.101%2Brev-c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a/0191ff0c-3a46-7ad4-b5e7-9f6806881886/source.tar.gz?narHash=sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ%3D' (2024-09-17)
  → 'https://api.flakehub.com/f/pinned/numtide/flake-utils/0.1.102%2Brev-11707dc2f618dd54ca8739b309ec4fc024de578b/0193276d-5b8f-7dbc-acf1-41cb7b54ad2e/source.tar.gz?narHash=sha256-l0KFg5HjrsfsO/JpG%2Br7fRrqm12kzFHyUHqHCVpMMbI%3D' (2024-11-13)
• Updated input 'nixpkgs':
    'https://api.flakehub.com/f/pinned/NixOS/nixpkgs/0.2405.635979%2Brev-4eb33fe664af7b41a4c446f87d20c9a0a6321fa3/0192a20e-216f-78c5-8821-f030d9a83220/source.tar.gz?narHash=sha256-LDiPhQ3l%2BfBjRATNtnuDZsBS7hqoBtPkKBkhpoBHv3I%3D' (2024-10-17)
  → 'https://api.flakehub.com/f/pinned/NixOS/nixpkgs/0.2411.714486%2Brev-0b73e36b1962620a8ac551a37229dd8662dac5c8/01950977-be43-7a95-9723-843510bebafd/source.tar.gz?narHash=sha256-wjWLzdM7PIq4ZAe7k3vyjtgVJn6b0UeodtRFlM/6W5U%3D' (2025-02-13)
• Updated input 'poetry2nix':
    'github:nix-community/poetry2nix/e67cc181b5bdf5aa46f1340fa9acd3910f6fb122?narHash=sha256-rT2Vz1kFSj%2BNKHetVY7PJUACX%2BqBETdnuPSklzFKCsc%3D' (2024-10-17)
  → 'github:nix-community/poetry2nix/be1fe795035d3d36359ca9135b26dcc5321b31fb?narHash=sha256-UiTOA89yQV5YNlO1ZAp4IqJUGWOnTyBC83netvt8rQE%3D' (2025-02-05)
• Updated input 'poetry2nix/nix-github-actions':
    'github:nix-community/nix-github-actions/622f829f5fe69310a866c8a6cd07e747c44ef820?narHash=sha256-uPlLYH2S0ACj0IcgaK9Lsf4spmJoGejR9DotXiXSBZQ%3D' (2024-07-04)
  → 'github:nix-community/nix-github-actions/e04df33f62cdcf93d73e9a04142464753a16db67?narHash=sha256-B4mzTcQ0FZHdpeWcpDYPERtyjJd/NIuaQ9%2BBV1h%2BMpA%3D' (2024-10-24)
• Updated input 'poetry2nix/treefmt-nix':
    'github:numtide/treefmt-nix/4446c7a6fc0775df028c5a3f6727945ba8400e64?narHash=sha256-xpRqITAoD8rHlXQafYZOLvUXCF6cnZkPfoq67ThN0Hc%3D' (2024-10-03)
  → 'github:numtide/treefmt-nix/9ef337e492a5555d8e17a51c911ff1f02635be15?narHash=sha256-LqHYIxMrl/1p3/kvm2ir925tZ8DkI0KA10djk8wecSk%3D' (2024-10-28)
```

### Running GitHub Actions on this PR

GitHub Actions will not run workflows on pull requests which are opened by a GitHub Action.

To run GitHub Actions workflows on this PR, run:

```sh
git branch -D update_flake_lock_action
git fetch origin
git checkout update_flake_lock_action
git commit --amend --no-edit
git push origin update_flake_lock_action --force
```


Patch:
diff --git a/copier/errors.py b/copier/errors.py
index f1bbf8a..01e6f1a 100644
--- a/copier/errors.py
+++ b/copier/errors.py
@@ -143,3 +143,7 @@ class ShallowCloneWarning(UserWarning, CopierWarning):
 
 class MissingSettingsWarning(UserWarning, CopierWarning):
     """Settings path has been defined but file is missing."""
+
+
+class MissingFileWarning(UserWarning, CopierWarning):
+    """I still couldn't find what I'm looking for."""
diff --git a/copier/main.py b/copier/main.py
index 426314a..cbacde8 100644
--- a/copier/main.py
+++ b/copier/main.py
@@ -59,13 +59,34 @@ from .tools import (
     scantree,
     set_git_alternates,
 )
-from .types import MISSING, AnyByStrDict, JSONSerializable, RelativePath, StrOrPath
-from .user_data import DEFAULT_DATA, AnswersMap, Question
+from .types import (
+    MISSING,
+    AnyByStrDict,
+    AnyByStrMutableMapping,
+    JSONSerializable,
+    RelativePath,
+    StrOrPath,
+)
+from .user_data import AnswersMap, Question, load_answersfile_data
 from .vcs import get_git
 
 _T = TypeVar("_T")
 
 
+# HACK https://github.com/copier-org/copier/pull/1880#discussion_r1887491497
+class _LazyDict:
+    """A dict where values are functions that get evaluated only once when requested."""
+
+    def __init__(self, **kwargs: Callable[[], Any]):
+        self.pending = kwargs
+        self.done: dict[str, Any] = {}
+
+    def __getitem__(self, key: str) -> Any:
+        if key not in self.done:
+            self.done[key] = self.pending[key]()
+        return self.done[key]
+
+
 @dataclass(config=ConfigDict(extra="forbid"))
 class Worker:
     """Copier process state manager.
@@ -260,7 +281,27 @@ class Worker:
         if features:
             raise UnsafeTemplateError(sorted(features))
 
+    def _external_data(self) -> _LazyDict:
+        """Load external data lazily.
+
+        Result keys are used for rendering, and values are the parsed contents
+        of the YAML files specified in [external_data][].
+
+        Files will only be parsed lazily on 1st access. This helps avoiding
+        circular dependencies when the file name also comes from a variable.
+        """
+        return _LazyDict(
+            **{
+                name: lambda path=path: load_answersfile_data(
+                    self.dst_path, self._render_string(path)
+                )
+                for name, path in self.template.external_data.items()
+            }
+        )
+
     def _print_message(self, message: str) -> None:
+        # On first use, at least we need the system render context
+        self.answers.system = self._system_render_context()
         if message and not self.quiet:
             print(self._render_string(message), file=sys.stderr)
 
@@ -327,12 +368,18 @@ class Worker:
             with local.cwd(working_directory), local.env(**extra_env):
                 subprocess.run(task_cmd, shell=use_shell, check=True, env=local.env)
 
-    def _render_context(self) -> Mapping[str, Any]:
-        """Produce render context for Jinja."""
+    def _system_render_context(self) -> AnyByStrMutableMapping:
+        """System reserved render context.
+
+        Most keys start with `_` because they're reserved.
+
+        Resolution of computed values is deferred until used for the 1st time.
+        """
         # Backwards compatibility
         # FIXME Remove it?
         conf = asdict(self)
         conf.pop("_cleanup_hooks")
+        conf.pop("answers")
         conf.update(
             {
                 "answers_file": self.answers_relpath,
@@ -342,12 +389,10 @@ class Worker:
                 "os": OS,
             }
         )
-
         return dict(
-            DEFAULT_DATA,
-            **self.answers.combined,
             _copier_answers=self._answers_to_remember(),
             _copier_conf=conf,
+            _external_data=self._external_data(),
             _folder_name=self.subproject.local_abspath.name,
             _copier_python=sys.executable,
         )
@@ -452,16 +497,17 @@ class Worker:
 
     def _ask(self) -> None:  # noqa: C901
         """Ask the questions of the questionnaire and record their answers."""
-        result = AnswersMap(
+        self.answers = AnswersMap(
             user_defaults=self.user_defaults,
             init=self.data,
             last=self.subproject.last_answers,
             metadata=self.template.metadata,
+            system=self._system_render_context(),
         )
 
         for var_name, details in self.template.questions_data.items():
             question = Question(
-                answers=result,
+                answers=self.answers,
                 jinja_env=self.jinja_env,
                 settings=self.settings,
                 var_name=var_name,
@@ -469,25 +515,25 @@ class Worker:
             )
             # Delete last answer if it cannot be parsed or validated, so a new
             # valid answer can be provided.
-            if var_name in result.last:
+            if var_name in self.answers.last:
                 try:
-                    answer = question.parse_answer(result.last[var_name])
+                    answer = question.parse_answer(self.answers.last[var_name])
                 except Exception:
-                    del result.last[var_name]
+                    del self.answers.last[var_name]
                 else:
                     if question.validate_answer(answer):
-                        del result.last[var_name]
+                        del self.answers.last[var_name]
             # Skip a question when the skip condition is met.
             if not question.get_when():
                 # Omit its answer from the answers file.
-                result.hide(var_name)
+                self.answers.hide(var_name)
                 # Skip immediately to the next question when it has no default
                 # value.
                 if question.default is MISSING:
                     continue
-            if var_name in result.init:
+            if var_name in self.answers.init:
                 # Try to parse the answer value.
-                answer = question.parse_answer(result.init[var_name])
+                answer = question.parse_answer(self.answers.init[var_name])
                 # Try to validate the answer value if the question has a
                 # validator.
                 if err_msg := question.validate_answer(answer):
@@ -496,10 +542,10 @@ class Worker:
                     )
                 # At this point, the answer value is valid. Do not ask the
                 # question again, but set answer as the user's answer instead.
-                result.user[var_name] = answer
+                self.answers.user[var_name] = answer
                 continue
             # Skip a question when the user already answered it.
-            if self.skip_answered and var_name in result.last:
+            if self.skip_answered and var_name in self.answers.last:
                 continue
 
             # Display TUI and ask user interactively only without --defaults
@@ -514,10 +560,12 @@ class Worker:
                         answers={question.var_name: question.get_default()},
                     )[question.var_name]
             except KeyboardInterrupt as err:
-                raise CopierAnswersInterrupt(result, question, self.template) from err
-            result.user[var_name] = new_answer
-
-        self.answers = result
+                raise CopierAnswersInterrupt(
+                    self.answers, question, self.template
+                ) from err
+            self.answers.user[var_name] = new_answer
+        # Update system render context, which may depend on answers
+        self.answers.system = self._system_render_context()
 
     @property
     def answers_relpath(self) -> Path:
@@ -648,7 +696,7 @@ class Worker:
                 new_content = src_abspath.read_bytes()
             else:
                 new_content = tpl.render(
-                    **self._render_context(), **(extra_context or {})
+                    **self.answers.combined, **(extra_context or {})
                 ).encode()
                 if self.jinja_env.yield_name:
                     raise YieldTagInFileError(
@@ -834,7 +882,7 @@ class Worker:
                 Additional variables to use for rendering the template.
         """
         tpl = self.jinja_env.from_string(string)
-        return tpl.render(**self._render_context(), **(extra_context or {}))
+        return tpl.render(**self.answers.combined, **(extra_context or {}))
 
     def _render_value(
         self, value: _T, extra_context: AnyByStrDict | None = None
@@ -1055,7 +1103,7 @@ class Worker:
                 )
             # Clear last answers cache to load possible answers migration, if skip_answered flag is not set
             if self.skip_answered is False:
-                self.answers = AnswersMap()
+                self.answers = AnswersMap(system=self._system_render_context())
                 with suppress(AttributeError):
                     del self.subproject.last_answers
             # Do a normal update in final destination
@@ -1071,6 +1119,7 @@ class Worker:
             ) as current_worker:
                 current_worker.run_copy()
                 self.answers = current_worker.answers
+                self.answers.system = self._system_render_context()
             # Render with the same answers in an empty dir to avoid pollution
             with replace(
                 self,
diff --git a/copier/template.py b/copier/template.py
index 6e39fea..e47b1a3 100644
--- a/copier/template.py
+++ b/copier/template.py
@@ -329,6 +329,14 @@ class Template:
             )
         )
 
+    @cached_property
+    def external_data(self) -> dict[str, str]:
+        """Get external data files specified in the template.
+
+        See [external_data][].
+        """
+        return self.config_data.get("external_data", {})
+
     @cached_property
     def jinja_extensions(self) -> tuple[str, ...]:
         """Get Jinja2 extensions specified in the template, or `()`.
diff --git a/copier/types.py b/copier/types.py
index be9a5de..740cf7d 100644
--- a/copier/types.py
+++ b/copier/types.py
@@ -7,6 +7,7 @@ from typing import (
     Dict,
     Literal,
     Mapping,
+    MutableMapping,
     NewType,
     Optional,
     Sequence,
@@ -19,6 +20,7 @@ from pydantic import AfterValidator
 # simple types
 StrOrPath = Union[str, Path]
 AnyByStrDict = Dict[str, Any]
+AnyByStrMutableMapping = MutableMapping[str, Any]
 
 # sequences
 IntSeq = Sequence[int]
diff --git a/copier/user_data.py b/copier/user_data.py
index 3cb870a..2be65b5 100644
--- a/copier/user_data.py
+++ b/copier/user_data.py
@@ -25,9 +25,9 @@ from questionary.prompts.common import Choice
 
 from copier.settings import Settings
 
-from .errors import InvalidTypeError, UserMessageError
+from .errors import InvalidTypeError, MissingFileWarning, UserMessageError
 from .tools import cast_to_bool, cast_to_str, force_str_end
-from .types import MISSING, AnyByStrDict, MissingType, OptStrOrPath, StrOrPath
+from .types import MISSING, AnyByStrDict, AnyByStrMutableMapping, MissingType, StrOrPath
 
 
 # TODO Remove these two functions as well as DEFAULT_DATA in a future release
@@ -85,17 +85,21 @@ class AnswersMap:
             Default data from the user e.g. previously completed and restored data.
 
             See [copier.main.Worker][].
+
+        system:
+            Automatic context generated by the [Worker][copier.main.Worker].
     """
 
     # Private
     hidden: set[str] = field(default_factory=set, init=False)
 
     # Public
-    user: AnyByStrDict = field(default_factory=dict)
-    init: AnyByStrDict = field(default_factory=dict)
-    metadata: AnyByStrDict = field(default_factory=dict)
-    last: AnyByStrDict = field(default_factory=dict)
-    user_defaults: AnyByStrDict = field(default_factory=dict)
+    user: AnyByStrMutableMapping = field(default_factory=dict)
+    init: AnyByStrMutableMapping = field(default_factory=dict)
+    metadata: AnyByStrMutableMapping = field(default_factory=dict)
+    last: AnyByStrMutableMapping = field(default_factory=dict)
+    user_defaults: AnyByStrMutableMapping = field(default_factory=dict)
+    system: AnyByStrMutableMapping = field(default_factory=dict)
 
     @property
     def combined(self) -> Mapping[str, Any]:
@@ -107,6 +111,7 @@ class AnswersMap:
                 self.metadata,
                 self.last,
                 self.user_defaults,
+                self.system,
                 DEFAULT_DATA,
             )
         )
@@ -127,6 +132,15 @@ class Question:
     All attributes are init kwargs.
 
     Attributes:
+        var_name:
+            Question name in the answers dict.
+
+        answers:
+            A map containing the answers provided by the user.
+
+        jinja_env:
+            The Jinja environment used to rendering answers.
+
         choices:
             Selections available for the user if the question requires them.
             Can be templated.
@@ -157,13 +171,10 @@ class Question:
             If the question type is str, it will hide user input on the screen
             by displaying asterisks: `****`.
 
-        type_name:
+        type:
             The type of question. Affects the rendering, validation and filtering.
             Can be templated.
 
-        var_name:
-            Question name in the answers dict.
-
         validator:
             Jinja template with which to validate the user input. This template
             will be rendered with the combined answers as variables; it should
@@ -492,13 +503,16 @@ def parse_yaml_string(string: str) -> Any:
 
 def load_answersfile_data(
     dst_path: StrOrPath,
-    answers_file: OptStrOrPath = None,
+    answers_file: StrOrPath = ".copier-answers.yml",
 ) -> AnyByStrDict:
     """Load answers data from a `$dst_path/$answers_file` file if it exists."""
     try:
-        with open(Path(dst_path) / (answers_file or ".copier-answers.yml")) as fd:
+        with open(Path(dst_path) / answers_file) as fd:
             return yaml.safe_load(fd)
-    except FileNotFoundError:
+    except (FileNotFoundError, IsADirectoryError):
+        warnings.warn(
+            f"File not found; returning empty dict: {answers_file}", MissingFileWarning
+        )
         return {}
 
 
diff --git a/docs/comparisons.md b/docs/comparisons.md
index cd8417c..7034506 100644
--- a/docs/comparisons.md
+++ b/docs/comparisons.md
@@ -37,6 +37,7 @@ docs! We don't want to be biased, but it's easy that we tend to be:
 [ejs]: https://ejs.co/
 
 [^1]: The file itself can [include other YAML files][include-other-yaml-files].
+
 [^2]:
     Git repo is recommended to be able to use advanced features such as template tagging
     and smart updates.
@@ -50,6 +51,7 @@ docs! We don't want to be biased, but it's easy that we tend to be:
     and extract smart diffs between them.
 
 [^5]: Context hooks are provided through the [`ContextHook` extension][context-hook].
+
 [^6]: Updates are possible through [Cruft][cruft].
 
 [context-hook]:
diff --git a/docs/configuring.md b/docs/configuring.md
index c79d2a5..9d3c5e5 100644
--- a/docs/configuring.md
+++ b/docs/configuring.md
@@ -841,6 +841,75 @@ contains your data.
 
     Command line arguments passed via `--data` always take precedence over the data file.
 
+### `external_data`
+
+-   Format: `dict[str, str]`
+-   CLI flags: N/A
+-   Default value: `{}`
+
+This allows using preexisting data inside the rendering context. The format is a dict of
+strings, where:
+
+-   The dict key will be the namespace of the data under [`_external_data`][].
+-   The dict value is the relative path (from the subproject destination) where the YAML
+    data file should be found.
+
+!!! example "Template composition"
+
+    If your template is
+    [a complement of another template][applying-multiple-templates-to-the-same-subproject],
+    you can access the other template's answers with a pattern similar to this:
+
+    ```yaml title="copier.yml"
+    # Child template defaults to a different answers file, to avoid conflicts
+    _answers_file: .copier-answers.child-tpl.yml
+
+    # Child template loads parent answers
+    _external_data_files:
+        # A dynamic path. Make sure you answer that question
+        # before the first access to the data (with `_external_data.parent_tpl`)
+        parent_tpl: "{{ parent_tpl_answers_file }}"
+
+    # Ask user where they stored parent answers
+    parent_tpl_answers_file:
+        help: Where did you store answers of the parent template?
+        default: .copier-answers.yml
+
+    # Use a parent answer as the default value for a child question
+    target_version:
+        help: What version are you deploying?
+        # We already answered the `parent_tpl_answers_file` question, so we can
+        # now correctly access the external data from `_external_data.parent_tpl`
+        default: "{{ _external_data.parent_tpl.target_version }}"
+    ```
+
+!!! example "Loading secrets"
+
+    If your template has [secret questions][secret_questions], you can load the secrets
+    and use them, e.g., as default answers with a pattern similar to this:
+
+    ```yaml
+    # Template loads secrets from Git-ignored file
+    _external_data_files:
+        # A static path. If missing, it will return an empty dict
+        secrets: .secrets.yaml
+
+    # Use a secret answers as the default value for a secret question
+    password:
+        help: What is the password?
+        secret: true
+        # If `.secrets.yaml` exists, it has been loaded at this point and we can
+        # now correctly access the external data from `_external_data.secrets`
+        default: "{{ _external_data.secrets.password }}"
+    ```
+
+    A template might even render `.secrets.yaml` with the answers to secret questions
+    similar to this:
+
+    ```yaml title=".secrets.yaml.jinja"
+    password: "{{ password }}"
+    ```
+
 ### `envops`
 
 -   Format: `dict`
@@ -1191,8 +1260,8 @@ Migrations are like [tasks][tasks], but each item can have additional keys:
 
 -   **command**: The migration command to run
 -   **version** (optional): Indicates the version that the template update has to go
-    through to trigger this migration. It is evaluated using [PEP 440][]. If no version
-    is specified the migration will run on every update.
+    through to trigger this migration. It is evaluated using [PEP 440][]. If no version is
+    specified the migration will run on every update.
 -   **when** (optional): Specifies a condition that needs to hold for the task to run.
     By default, a migration will run in the after upgrade stage.
 -   **working_directory** (optional): Specifies the directory in which the command will
@@ -1226,12 +1295,11 @@ processes will receive these variables:
 -   `_version_pep440_from`/`$VERSION_PEP440_FROM`,
     `_version_pep440_to`/`$VERSION_PEP440_TO`,
     `_version_pep440_current`/`$VERSION_PEP440_CURRENT`: Same as the above, but
-    normalized into a standard [PEP 440][] version. In Jinja templates these are
-    represented as
-    [packaging.version.Version](https://packaging.pypa.io/en/stable/version.html#packaging.version.Version)
-    objects and allow access to their attributes. As environment variables they are
-    represented as strings. If you use variables to perform migrations, you probably
-    will prefer to use these variables.
+    normalized into a standard [PEP 440][] version. In Jinja templates these are represented
+    as [packaging.version.Version](https://packaging.pypa.io/en/stable/version.html#packaging.version.Version)
+    objects and allow access to their attributes. As environment variables they are represented
+    as strings. If you use variables to perform migrations, you probably will prefer to use
+    these variables.
 
 [git describe]: https://git-scm.com/docs/git-describe
 [pep 440]: https://www.python.org/dev/peps/pep-0440/
@@ -1271,8 +1339,8 @@ can't be used in jinja templates.
 
 Specifies the minimum required version of Copier to generate a project from this
 template. The version must be follow the [PEP 440][] syntax. Upon generating or updating
-a project, if the installed version of Copier is less than the required one, the
-generation will be aborted and an error will be shown to the user.
+a project, if the installed version of Copier is less than the required one, the generation
+will be aborted and an error will be shown to the user.
 
 !!! info
 
diff --git a/docs/creating.md b/docs/creating.md
index 75c07a8..174ba0b 100644
--- a/docs/creating.md
+++ b/docs/creating.md
@@ -121,6 +121,17 @@ The current commit hash from the template.
 
 The absolute path of the Python interpreter running Copier.
 
+### `_external_data`
+
+A dict of the data contained in [external_data][].
+
+When rendering the template, that data will be exposed in the special `_external_data`
+variable:
+
+-   Keys will be the same as in [external_data][].
+-   Values will be the files contents parsed as YAML. JSON is also compatible.
+-   Parsing is done lazily on first use.
+
 ### `_folder_name`
 
 The name of the project root directory.
diff --git a/flake.lock b/flake.lock
index dd6d018..318c93e 100644
--- a/flake.lock
+++ b/flake.lock
@@ -2,42 +2,22 @@
   "nodes": {
     "cachix": {
       "inputs": {
-        "devenv": "devenv_2",
-        "flake-compat": ["devenv", "flake-compat"],
-        "git-hooks": ["devenv", "pre-commit-hooks"],
-        "nixpkgs": ["devenv", "nixpkgs"]
-      },
-      "locked": {
-        "lastModified": 1726520618,
-        "narHash": "sha256-jOsaBmJ/EtX5t/vbylCdS7pWYcKGmWOKg4QKUzKr6dA=",
-        "owner": "cachix",
-        "repo": "cachix",
-        "rev": "695525f9086542dfb09fde0871dbf4174abbf634",
-        "type": "github"
-      },
-      "original": {
-        "owner": "cachix",
-        "repo": "cachix",
-        "type": "github"
-      }
-    },
-    "cachix_2": {
-      "inputs": {
-        "devenv": "devenv_3",
-        "flake-compat": ["devenv", "cachix", "devenv", "flake-compat"],
-        "nixpkgs": ["devenv", "cachix", "devenv", "nixpkgs"],
-        "pre-commit-hooks": ["devenv", "cachix", "devenv", "pre-commit-hooks"]
+        "devenv": ["devenv"],
+        "flake-compat": ["devenv"],
+        "git-hooks": ["devenv"],
+        "nixpkgs": "nixpkgs"
       },
       "locked": {
-        "lastModified": 1712055811,
-        "narHash": "sha256-7FcfMm5A/f02yyzuavJe06zLa9hcMHsagE28ADcmQvk=",
+        "lastModified": 1737621947,
+        "narHash": "sha256-8HFvG7fvIFbgtaYAY2628Tb89fA55nPm2jSiNs0/Cws=",
         "owner": "cachix",
         "repo": "cachix",
-        "rev": "02e38da89851ec7fec3356a5c04bc8349cae0e30",
+        "rev": "f65a3cd5e339c223471e64c051434616e18cc4f5",
         "type": "github"
       },
       "original": {
         "owner": "cachix",
+        "ref": "latest",
         "repo": "cachix",
         "type": "github"
       }
@@ -45,67 +25,22 @@
     "devenv": {
       "inputs": {
         "cachix": "cachix",
-        "flake-compat": "flake-compat_2",
-        "nix": "nix_3",
-        "nixpkgs": "nixpkgs_3",
-        "pre-commit-hooks": "pre-commit-hooks_2"
-      },
-      "locked": {
-        "lastModified": 1729190222,
-        "narHash": "sha256-FhlknassIb3rKEucqnfFAzgny1ANmenJcTyRaXYwbA0=",
-        "owner": "cachix",
-        "repo": "devenv",
-        "rev": "e6464200390e502e3cff45f82b6dbe8c6760fab5",
-        "type": "github"
-      },
-      "original": {
-        "owner": "cachix",
-        "ref": "latest",
-        "repo": "devenv",
-        "type": "github"
-      }
-    },
-    "devenv_2": {
-      "inputs": {
-        "cachix": "cachix_2",
-        "flake-compat": ["devenv", "cachix", "flake-compat"],
-        "nix": "nix_2",
-        "nixpkgs": ["devenv", "cachix", "nixpkgs"],
-        "pre-commit-hooks": ["devenv", "cachix", "git-hooks"]
-      },
-      "locked": {
-        "lastModified": 1723156315,
-        "narHash": "sha256-0JrfahRMJ37Rf1i0iOOn+8Z4CLvbcGNwa2ChOAVrp/8=",
-        "owner": "cachix",
-        "repo": "devenv",
-        "rev": "ff5eb4f2accbcda963af67f1a1159e3f6c7f5f91",
-        "type": "github"
-      },
-      "original": {
-        "owner": "cachix",
-        "repo": "devenv",
-        "type": "github"
-      }
-    },
-    "devenv_3": {
-      "inputs": {
-        "flake-compat": ["devenv", "cachix", "devenv", "cachix", "flake-compat"],
+        "flake-compat": "flake-compat",
+        "git-hooks": "git-hooks",
         "nix": "nix",
-        "nixpkgs": "nixpkgs",
-        "poetry2nix": "poetry2nix",
-        "pre-commit-hooks": ["devenv", "cachix", "devenv", "cachix", "pre-commit-hooks"]
+        "nixpkgs": "nixpkgs_3"
       },
       "locked": {
-        "lastModified": 1708704632,
-        "narHash": "sha256-w+dOIW60FKMaHI1q5714CSibk99JfYxm0CzTinYWr+Q=",
+        "lastModified": 1739300456,
+        "narHash": "sha256-ax0264nOyPcTJvIJAnPKGfkfXQ8Oe8ZVFziKf3UV26o=",
         "owner": "cachix",
         "repo": "devenv",
-        "rev": "2ee4450b0f4b95a1b90f2eb5ffea98b90e48c196",
+        "rev": "6c987a8795eedea872afe4d1c1ac518d0c7f6db1",
         "type": "github"
       },
       "original": {
         "owner": "cachix",
-        "ref": "python-rewrite",
+        "ref": "latest",
         "repo": "devenv",
         "type": "github"
       }
@@ -113,11 +48,11 @@
     "flake-compat": {
       "flake": false,
       "locked": {
-        "lastModified": 1673956053,
-        "narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=",
+        "lastModified": 1733328505,
+        "narHash": "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU=",
         "owner": "edolstra",
         "repo": "flake-compat",
-        "rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9",
+        "rev": "ff81ac966bb2cae68946d5ed5fc4994f96d0ffec",
         "type": "github"
       },
       "original": {
@@ -127,29 +62,13 @@
       }
     },
     "flake-compat_2": {
-      "flake": false,
       "locked": {
-        "lastModified": 1696426674,
-        "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
-        "owner": "edolstra",
-        "repo": "flake-compat",
-        "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
-        "type": "github"
-      },
-      "original": {
-        "owner": "edolstra",
-        "repo": "flake-compat",
-        "type": "github"
-      }
-    },
-    "flake-compat_3": {
-      "locked": {
-        "lastModified": 1696426674,
-        "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
-        "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
-        "revCount": 57,
+        "lastModified": 1733328505,
+        "narHash": "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU=",
+        "rev": "ff81ac966bb2cae68946d5ed5fc4994f96d0ffec",
+        "revCount": 69,
         "type": "tarball",
-        "url": "https://api.flakehub.com/f/pinned/edolstra/flake-compat/1.0.1/018afb31-abd1-7bff-a5e4-cff7e18efb7a/source.tar.gz"
+        "url": "https://api.flakehub.com/f/pinned/edolstra/flake-compat/1.1.0/01948eb7-9cba-704f-bbf3-3fa956735b52/source.tar.gz"
       },
       "original": {
         "type": "tarball",
@@ -179,72 +98,59 @@
         "systems": "systems"
       },
       "locked": {
-        "lastModified": 1689068808,
-        "narHash": "sha256-6ixXo3wt24N/melDWjq70UuHQLxGV8jZvooRanIHXw0=",
-        "owner": "numtide",
-        "repo": "flake-utils",
-        "rev": "919d646de7be200f3bf08cb76ae1f09402b6f9b4",
-        "type": "github"
+        "lastModified": 1731533236,
+        "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
+        "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
+        "revCount": 102,
+        "type": "tarball",
+        "url": "https://api.flakehub.com/f/pinned/numtide/flake-utils/0.1.102%2Brev-11707dc2f618dd54ca8739b309ec4fc024de578b/0193276d-5b8f-7dbc-acf1-41cb7b54ad2e/source.tar.gz"
       },
       "original": {
-        "owner": "numtide",
-        "repo": "flake-utils",
-        "type": "github"
+        "type": "tarball",
+        "url": "https://flakehub.com/f/numtide/flake-utils/0.1.%2A.tar.gz"
       }
     },
     "flake-utils_2": {
-      "locked": {
-        "lastModified": 1667395993,
-        "narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=",
-        "owner": "numtide",
-        "repo": "flake-utils",
-        "rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f",
-        "type": "github"
-      },
-      "original": {
-        "owner": "numtide",
-        "repo": "flake-utils",
-        "type": "github"
-      }
-    },
-    "flake-utils_3": {
       "inputs": {
         "systems": "systems_2"
       },
       "locked": {
         "lastModified": 1726560853,
         "narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=",
+        "owner": "numtide",
+        "repo": "flake-utils",
         "rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a",
-        "revCount": 101,
-        "type": "tarball",
-        "url": "https://api.flakehub.com/f/pinned/numtide/flake-utils/0.1.101%2Brev-c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a/0191ff0c-3a46-7ad4-b5e7-9f6806881886/source.tar.gz"
+        "type": "github"
       },
       "original": {
-        "type": "tarball",
-        "url": "https://flakehub.com/f/numtide/flake-utils/0.1.%2A.tar.gz"
+        "owner": "numtide",
+        "repo": "flake-utils",
+        "type": "github"
       }
     },
-    "flake-utils_4": {
+    "git-hooks": {
       "inputs": {
-        "systems": "systems_3"
+        "flake-compat": ["devenv"],
+        "gitignore": "gitignore",
+        "nixpkgs": ["devenv", "nixpkgs"]
       },
       "locked": {
-        "lastModified": 1726560853,
-        "narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=",
-        "owner": "numtide",
-        "repo": "flake-utils",
-        "rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a",
+        "lastModified": 1737465171,
+        "narHash": "sha256-R10v2hoJRLq8jcL4syVFag7nIGE7m13qO48wRIukWNg=",
+        "owner": "cachix",
+        "repo": "git-hooks.nix",
+        "rev": "9364dc02281ce2d37a1f55b6e51f7c0f65a75f17",
         "type": "github"
       },
       "original": {
-        "owner": "numtide",
-        "repo": "flake-utils",
+        "owner": "cachix",
+        "repo": "git-hooks.nix",
         "type": "github"
       }
     },
     "gitignore": {
       "inputs": {
-        "nixpkgs": ["devenv", "pre-commit-hooks", "nixpkgs"]
+        "nixpkgs": ["devenv", "git-hooks", "nixpkgs"]
       },
       "locked": {
         "lastModified": 1709087332,
@@ -278,61 +184,39 @@
     },
     "nix": {
       "inputs": {
-        "flake-compat": "flake-compat",
-        "nixpkgs": ["devenv", "cachix", "devenv", "cachix", "devenv", "nixpkgs"],
-        "nixpkgs-regression": "nixpkgs-regression"
+        "flake-compat": ["devenv"],
+        "flake-parts": "flake-parts",
+        "libgit2": "libgit2",
+        "nixpkgs": "nixpkgs_2",
+        "nixpkgs-23-11": ["devenv"],
+        "nixpkgs-regression": ["devenv"],
+        "pre-commit-hooks": ["devenv"]
       },
       "locked": {
-        "lastModified": 1712911606,
-        "narHash": "sha256-BGvBhepCufsjcUkXnEEXhEVjwdJAwPglCC2+bInc794=",
+        "lastModified": 1734114420,
+        "narHash": "sha256-n52PUzub5jZWc8nI/sR7UICOheU8rNA+YZ73YaHeCBg=",
         "owner": "domenkozar",
         "repo": "nix",
-        "rev": "b24a9318ea3f3600c1e24b4a00691ee912d4de12",
+        "rev": "bde6a1a0d1f2af86caa4d20d23eca019f3d57eee",
         "type": "github"
       },
       "original": {
         "owner": "domenkozar",
-        "ref": "devenv-2.21",
+        "ref": "devenv-2.24",
         "repo": "nix",
         "type": "github"
       }
     },
     "nix-github-actions": {
-      "inputs": {
-        "nixpkgs": [
-          "devenv",
-          "cachix",
-          "devenv",
-          "cachix",
-          "devenv",
-          "poetry2nix",
-          "nixpkgs"
-        ]
-      },
-      "locked": {
-        "lastModified": 1688870561,
-        "narHash": "sha256-4UYkifnPEw1nAzqqPOTL2MvWtm3sNGw1UTYTalkTcGY=",
-        "owner": "nix-community",
-        "repo": "nix-github-actions",
-        "rev": "165b1650b753316aa7f1787f3005a8d2da0f5301",
-        "type": "github"
-      },
-      "original": {
-        "owner": "nix-community",
-        "repo": "nix-github-actions",
-        "type": "github"
-      }
-    },
-    "nix-github-actions_2": {
       "inputs": {
         "nixpkgs": ["poetry2nix", "nixpkgs"]
       },
       "locked": {
-        "lastModified": 1720066371,
-        "narHash": "sha256-uPlLYH2S0ACj0IcgaK9Lsf4spmJoGejR9DotXiXSBZQ=",
+        "lastModified": 1729742964,
+        "narHash": "sha256-B4mzTcQ0FZHdpeWcpDYPERtyjJd/NIuaQ9+BV1h+MpA=",
         "owner": "nix-community",
         "repo": "nix-github-actions",
-        "rev": "622f829f5fe69310a866c8a6cd07e747c44ef820",
+        "rev": "e04df33f62cdcf93d73e9a04142464753a16db67",
         "type": "github"
       },
       "original": {
@@ -341,144 +225,18 @@
         "type": "github"
       }
     },
-    "nix_2": {
-      "inputs": {
-        "flake-compat": ["devenv", "cachix", "devenv", "flake-compat"],
-        "nixpkgs": ["devenv", "cachix", "devenv", "nixpkgs"],
-        "nixpkgs-regression": "nixpkgs-regression_2"
-      },
-      "locked": {
-        "lastModified": 1712911606,
-        "narHash": "sha256-BGvBhepCufsjcUkXnEEXhEVjwdJAwPglCC2+bInc794=",
-        "owner": "domenkozar",
-        "repo": "nix",
-        "rev": "b24a9318ea3f3600c1e24b4a00691ee912d4de12",
-        "type": "github"
-      },
-      "original": {
-        "owner": "domenkozar",
-        "ref": "devenv-2.21",
-        "repo": "nix",
-        "type": "github"
-      }
-    },
-    "nix_3": {
-      "inputs": {
-        "flake-compat": ["devenv", "flake-compat"],
-        "flake-parts": "flake-parts",
-        "libgit2": "libgit2",
-        "nixpkgs": "nixpkgs_2",
-        "nixpkgs-23-11": "nixpkgs-23-11",
-        "nixpkgs-regression": "nixpkgs-regression_3",
-        "pre-commit-hooks": "pre-commit-hooks"
-      },
-      "locked": {
-        "lastModified": 1727438425,
-        "narHash": "sha256-X8ES7I1cfNhR9oKp06F6ir4Np70WGZU5sfCOuNBEwMg=",
-        "owner": "domenkozar",
-        "repo": "nix",
-        "rev": "f6c5ae4c1b2e411e6b1e6a8181cc84363d6a7546",
-        "type": "github"
-      },
-      "original": {
-        "owner": "domenkozar",
-        "ref": "devenv-2.24",
-        "repo": "nix",
-        "type": "github"
-      }
-    },
     "nixpkgs": {
       "locked": {
-        "lastModified": 1692808169,
-        "narHash": "sha256-x9Opq06rIiwdwGeK2Ykj69dNc2IvUH1fY55Wm7atwrE=",
-        "owner": "NixOS",
-        "repo": "nixpkgs",
-        "rev": "9201b5ff357e781bf014d0330d18555695df7ba8",
-        "type": "github"
-      },
-      "original": {
-        "owner": "NixOS",
-        "ref": "nixpkgs-unstable",
-        "repo": "nixpkgs",
-        "type": "github"
-      }
-    },
-    "nixpkgs-23-11": {
-      "locked": {
-        "lastModified": 1717159533,
-        "narHash": "sha256-oamiKNfr2MS6yH64rUn99mIZjc45nGJlj9eGth/3Xuw=",
-        "owner": "NixOS",
-        "repo": "nixpkgs",
-        "rev": "a62e6edd6d5e1fa0329b8653c801147986f8d446",
-        "type": "github"
-      },
-      "original": {
-        "owner": "NixOS",
-        "repo": "nixpkgs",
-        "rev": "a62e6edd6d5e1fa0329b8653c801147986f8d446",
-        "type": "github"
-      }
-    },
-    "nixpkgs-regression": {
-      "locked": {
-        "lastModified": 1643052045,
-        "narHash": "sha256-uGJ0VXIhWKGXxkeNnq4TvV3CIOkUJ3PAoLZ3HMzNVMw=",
-        "owner": "NixOS",
-        "repo": "nixpkgs",
-        "rev": "215d4d0fd80ca5163643b03a33fde804a29cc1e2",
-        "type": "github"
-      },
-      "original": {
-        "owner": "NixOS",
-        "repo": "nixpkgs",
-        "rev": "215d4d0fd80ca5163643b03a33fde804a29cc1e2",
-        "type": "github"
-      }
-    },
-    "nixpkgs-regression_2": {
-      "locked": {
-        "lastModified": 1643052045,
-        "narHash": "sha256-uGJ0VXIhWKGXxkeNnq4TvV3CIOkUJ3PAoLZ3HMzNVMw=",
-        "owner": "NixOS",
-        "repo": "nixpkgs",
-        "rev": "215d4d0fd80ca5163643b03a33fde804a29cc1e2",
-        "type": "github"
-      },
-      "original": {
-        "owner": "NixOS",
-        "repo": "nixpkgs",
-        "rev": "215d4d0fd80ca5163643b03a33fde804a29cc1e2",
-        "type": "github"
-      }
-    },
-    "nixpkgs-regression_3": {
-      "locked": {
-        "lastModified": 1643052045,
-        "narHash": "sha256-uGJ0VXIhWKGXxkeNnq4TvV3CIOkUJ3PAoLZ3HMzNVMw=",
+        "lastModified": 1733212471,
+        "narHash": "sha256-M1+uCoV5igihRfcUKrr1riygbe73/dzNnzPsmaLCmpo=",
         "owner": "NixOS",
         "repo": "nixpkgs",
-        "rev": "215d4d0fd80ca5163643b03a33fde804a29cc1e2",
+        "rev": "55d15ad12a74eb7d4646254e13638ad0c4128776",
         "type": "github"
       },
       "original": {
         "owner": "NixOS",
-        "repo": "nixpkgs",
-        "rev": "215d4d0fd80ca5163643b03a33fde804a29cc1e2",
-        "type": "github"
-      }
-    },
-    "nixpkgs-stable": {
-      "locked": {
-        "lastModified": 1720386169,
-        "narHash": "sha256-NGKVY4PjzwAa4upkGtAMz1npHGoRzWotlSnVlqI40mo=",
-        "owner": "NixOS",
-        "repo": "nixpkgs",
-        "rev": "194846768975b7ad2c4988bdb82572c00222c0d7",
-        "type": "github"
-      },
-      "original": {
-        "owner": "NixOS",
-        "ref": "nixos-24.05",
+        "ref": "nixos-unstable",
         "repo": "nixpkgs",
         "type": "github"
       }
@@ -501,11 +259,11 @@
     },
     "nixpkgs_3": {
       "locked": {
-        "lastModified": 1716977621,
-        "narHash": "sha256-Q1UQzYcMJH4RscmpTkjlgqQDX5yi1tZL0O345Ri6vXQ=",
+        "lastModified": 1733477122,
+        "narHash": "sha256-qamMCz5mNpQmgBwc8SB5tVMlD5sbwVIToVZtSxMph9s=",
         "owner": "cachix",
         "repo": "devenv-nixpkgs",
-        "rev": "4267e705586473d3e5c8d50299e71503f16a6fb6",
+        "rev": "7bd9e84d0452f6d2e63b6e6da29fe73fac951857",
         "type": "github"
       },
       "original": {
@@ -517,12 +275,12 @@
     },
     "nixpkgs_4": {
       "locked": {
-        "lastModified": 1729181673,
-        "narHash": "sha256-LDiPhQ3l+fBjRATNtnuDZsBS7hqoBtPkKBkhpoBHv3I=",
-        "rev": "4eb33fe664af7b41a4c446f87d20c9a0a6321fa3",
-        "revCount": 635979,
+        "lastModified": 1739484910,
+        "narHash": "sha256-wjWLzdM7PIq4ZAe7k3vyjtgVJn6b0UeodtRFlM/6W5U=",
+        "rev": "0b73e36b1962620a8ac551a37229dd8662dac5c8",
+        "revCount": 714486,
         "type": "tarball",
-        "url": "https://api.flakehub.com/f/pinned/NixOS/nixpkgs/0.2405.635979%2Brev-4eb33fe664af7b41a4c446f87d20c9a0a6321fa3/0192a20e-216f-78c5-8821-f030d9a83220/source.tar.gz"
+        "url": "https://api.flakehub.com/f/pinned/NixOS/nixpkgs/0.2411.714486%2Brev-0b73e36b1962620a8ac551a37229dd8662dac5c8/01950977-be43-7a95-9723-843510bebafd/source.tar.gz"
       },
       "original": {
         "type": "tarball",
@@ -531,38 +289,18 @@
     },
     "poetry2nix": {
       "inputs": {
-        "flake-utils": "flake-utils",
+        "flake-utils": "flake-utils_2",
         "nix-github-actions": "nix-github-actions",
-        "nixpkgs": ["devenv", "cachix", "devenv", "cachix", "devenv", "nixpkgs"]
-      },
-      "locked": {
-        "lastModified": 1692876271,
-        "narHash": "sha256-IXfZEkI0Mal5y1jr6IRWMqK8GW2/f28xJenZIPQqkY0=",
-        "owner": "nix-community",
-        "repo": "poetry2nix",
-        "rev": "d5006be9c2c2417dafb2e2e5034d83fabd207ee3",
-        "type": "github"
-      },
-      "original": {
-        "owner": "nix-community",
-        "repo": "poetry2nix",
-        "type": "github"
-      }
-    },
-    "poetry2nix_2": {
-      "inputs": {
-        "flake-utils": "flake-utils_4",
-        "nix-github-actions": "nix-github-actions_2",
         "nixpkgs": ["nixpkgs"],
-        "systems": "systems_4",
+        "systems": "systems_3",
         "treefmt-nix": "treefmt-nix"
       },
       "locked": {
-        "lastModified": 1729173771,
-        "narHash": "sha256-rT2Vz1kFSj+NKHetVY7PJUACX+qBETdnuPSklzFKCsc=",
+        "lastModified": 1738741221,
+        "narHash": "sha256-UiTOA89yQV5YNlO1ZAp4IqJUGWOnTyBC83netvt8rQE=",
         "owner": "nix-community",
         "repo": "poetry2nix",
-        "rev": "e67cc181b5bdf5aa46f1340fa9acd3910f6fb122",
+        "rev": "be1fe795035d3d36359ca9135b26dcc5321b31fb",
         "type": "github"
       },
       "original": {
@@ -571,56 +309,13 @@
         "type": "github"
       }
     },
-    "pre-commit-hooks": {
-      "inputs": {
-        "flake-compat": ["devenv", "nix"],
-        "flake-utils": "flake-utils_2",
-        "gitignore": ["devenv", "nix"],
-        "nixpkgs": ["devenv", "nix", "nixpkgs"],
-        "nixpkgs-stable": ["devenv", "nix", "nixpkgs"]
-      },
-      "locked": {
-        "lastModified": 1712897695,
-        "narHash": "sha256-nMirxrGteNAl9sWiOhoN5tIHyjBbVi5e2tgZUgZlK3Y=",
-        "owner": "cachix",
-        "repo": "pre-commit-hooks.nix",
-        "rev": "40e6053ecb65fcbf12863338a6dcefb3f55f1bf8",
-        "type": "github"
-      },
-      "original": {
-        "owner": "cachix",
-        "repo": "pre-commit-hooks.nix",
-        "type": "github"
-      }
-    },
-    "pre-commit-hooks_2": {
-      "inputs": {
-        "flake-compat": ["devenv", "flake-compat"],
-        "gitignore": "gitignore",
-        "nixpkgs": ["devenv", "nixpkgs"],
-        "nixpkgs-stable": "nixpkgs-stable"
-      },
-      "locked": {
-        "lastModified": 1726745158,
-        "narHash": "sha256-D5AegvGoEjt4rkKedmxlSEmC+nNLMBPWFxvmYnVLhjk=",
-        "owner": "cachix",
-        "repo": "pre-commit-hooks.nix",
-        "rev": "4e743a6920eab45e8ba0fbe49dc459f1423a4b74",
-        "type": "github"
-      },
-      "original": {
-        "owner": "cachix",
-        "repo": "pre-commit-hooks.nix",
-        "type": "github"
-      }
-    },
     "root": {
       "inputs": {
         "devenv": "devenv",
-        "flake-compat": "flake-compat_3",
-        "flake-utils": "flake-utils_3",
+        "flake-compat": "flake-compat_2",
+        "flake-utils": "flake-utils",
         "nixpkgs": "nixpkgs_4",
-        "poetry2nix": "poetry2nix_2"
+        "poetry2nix": "poetry2nix"
       }
     },
     "systems": {
@@ -668,30 +363,16 @@
         "type": "github"
       }
     },
-    "systems_4": {
-      "locked": {
-        "lastModified": 1681028828,
-        "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
-        "owner": "nix-systems",
-        "repo": "default",
-        "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
-        "type": "github"
-      },
-      "original": {
-        "id": "systems",
-        "type": "indirect"
-      }
-    },
     "treefmt-nix": {
       "inputs": {
         "nixpkgs": ["poetry2nix", "nixpkgs"]
       },
       "locked": {
-        "lastModified": 1727984844,
-        "narHash": "sha256-xpRqITAoD8rHlXQafYZOLvUXCF6cnZkPfoq67ThN0Hc=",
+        "lastModified": 1730120726,
+        "narHash": "sha256-LqHYIxMrl/1p3/kvm2ir925tZ8DkI0KA10djk8wecSk=",
         "owner": "numtide",
         "repo": "treefmt-nix",
-        "rev": "4446c7a6fc0775df028c5a3f6727945ba8400e64",
+        "rev": "9ef337e492a5555d8e17a51c911ff1f02635be15",
         "type": "github"
       },
       "original": {


Test patch:
diff --git a/tests/test_answersfile.py b/tests/test_answersfile.py
index e09cf09..c3c4056 100644
--- a/tests/test_answersfile.py
+++ b/tests/test_answersfile.py
@@ -1,5 +1,6 @@
 from __future__ import annotations
 
+import json
 from pathlib import Path
 from textwrap import dedent
 
@@ -8,7 +9,7 @@ import pytest
 import copier
 from copier.user_data import load_answersfile_data
 
-from .helpers import BRACKET_ENVOPS_JSON, SUFFIX_TMPL, build_file_tree
+from .helpers import BRACKET_ENVOPS_JSON, SUFFIX_TMPL, build_file_tree, git_save
 
 
 @pytest.fixture(scope="module")
@@ -129,3 +130,95 @@ def test_answersfile(
     assert log["round"] == "2nd"
     assert "password_1" not in log
     assert "password_2" not in log
+
+
+def test_external_data(tmp_path_factory: pytest.TempPathFactory) -> None:
+    parent1, parent2, child, dst = map(
+        tmp_path_factory.mktemp, ("parent1", "parent2", "child", "dst")
+    )
+    build_file_tree(
+        {
+            (parent1 / "copier.yaml"): "{name: P1, child: C1}",
+            (parent1 / "parent1.txt.jinja"): "{{ name }}",
+            (
+                parent1 / "{{ _copier_conf.answers_file }}.jinja"
+            ): "{{ _copier_answers|to_nice_yaml -}}",
+            (parent2 / "copier.yaml"): "name: P2",
+            (parent2 / "parent2.txt.jinja"): "{{ name }}",
+            (
+                parent2 / "{{ _copier_conf.answers_file }}.jinja"
+            ): "{{ _copier_answers|to_nice_yaml -}}",
+            (child / "copier.yml"): (
+                """\
+                _external_data:
+                    parent1: .copier-answers.yml
+                    parent2: "{{ parent2_answers }}"
+                parent2_answers: .parent2-answers.yml
+                name: "{{ _external_data.parent2.child | d(_external_data.parent1.child) }}"
+                """
+            ),
+            (child / "combined.json.jinja"): """\
+                {
+                    "parent1": {{ _external_data.parent1.name | tojson }},
+                    "parent2": {{ _external_data.parent2.name | tojson }},
+                    "child": {{ name | tojson }}
+                }
+            """,
+            (
+                child / "{{ _copier_conf.answers_file }}.jinja"
+            ): "{{ _copier_answers|to_nice_yaml -}}",
+        }
+    )
+    git_save(parent1, tag="v1.0+parent1")
+    git_save(parent2, tag="v1.0+parent2")
+    git_save(child, tag="v1.0+child")
+    # Apply parent 1. At this point we don't know we'll want more than 1
+    # template in the same subproject, so we leave the default answers file.
+    copier.run_copy(str(parent1), dst, defaults=True, overwrite=True)
+    git_save(dst)
+    assert (dst / "parent1.txt").read_text() == "P1"
+    expected_parent1_answers = {
+        "_src_path": str(parent1),
+        "_commit": "v1.0+parent1",
+        "name": "P1",
+        "child": "C1",
+    }
+    assert load_answersfile_data(dst, ".copier-answers.yml") == expected_parent1_answers
+    # Apply parent 2. It uses a different answers file.
+    copier.run_copy(
+        str(parent2),
+        dst,
+        defaults=True,
+        overwrite=True,
+        answers_file=".parent2-answers.yml",
+    )
+    git_save(dst)
+    assert (dst / "parent2.txt").read_text() == "P2"
+    expected_parent2_answers = {
+        "_commit": "v1.0+parent2",
+        "_src_path": str(parent2),
+        "name": "P2",
+    }
+    assert (
+        load_answersfile_data(dst, ".parent2-answers.yml") == expected_parent2_answers
+    )
+    # Apply child. It can access answers from both parents.
+    copier.run_copy(
+        str(child),
+        dst,
+        defaults=True,
+        overwrite=True,
+        answers_file=".child-answers.yml",
+    )
+    git_save(dst)
+    assert load_answersfile_data(dst, ".child-answers.yml") == {
+        "_commit": "v1.0+child",
+        "_src_path": str(child),
+        "name": "C1",
+        "parent2_answers": ".parent2-answers.yml",
+    }
+    assert json.loads((dst / "combined.json").read_text()) == {
+        "parent1": "P1",
+        "parent2": "P2",
+        "child": "C1",
+    }
diff --git a/tests/test_config.py b/tests/test_config.py
index 03b66b3..1546cd4 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -346,7 +346,7 @@ def is_subdict(small: dict[Any, Any], big: dict[Any, Any]) -> bool:
 def test_worker_good_data(tmp_path: Path) -> None:
     # This test is probably useless, as it tests the what and not the how
     conf = copier.Worker("./tests/demo_data", tmp_path)
-    assert conf._render_context()["_folder_name"] == tmp_path.name
+    assert conf._system_render_context()["_folder_name"] == tmp_path.name
     assert conf.all_exclusions == ("exclude1", "exclude2")
     assert conf.template.skip_if_exists == ["skip_if_exists1", "skip_if_exists2"]
     assert conf.template.tasks == [