Skip to content

ContextHook called in more phases after #2023 #2123

Open
@ChristianRothQC

Description

@ChristianRothQC

Describe the problem

The changes introduced in #2023 seem to lead to additional jinja rendering - which are also triggering copier_templates_extensions.ContextHook. In my particular case this lead to v.9.7.x breaking my setup because my hook did not distinguish between calls from different phases.

extensions/context_hooks.py

from copier_templates_extensions import ContextHook


class ContextUpdater(ContextHook):
    def hook(self, context: dict) -> dict:
        context["hello_world"] = context["hello"] + ", " + context["world"] + '!'
        return context

copier.yml

_subdirectory: template
_preserve_symlinks: True
_min_copier_version: "9.0.1"
_jinja_extensions:
  - copier_templates_extensions.TemplateExtensionLoader
  - extensions/context_hooks.py:ContextUpdater

hello:
  type: str
  help: "Say hello, please!"
  default: "Hello"

world:
  when: false
  default: "world"

The behavior under <9.7.x can be restored by

from copier import Phase
from copier_templates_extensions import ContextHook


class ContextUpdater(ContextHook):
    def hook(self, context: dict) -> dict:
        if context['_copier_phase'] == Phase.RENDER:
            context["hello_world"] = context["hello"] + ", " + context["world"] + '!'
        return context

... but it seems a bit of a breaking change which the docs also do not seem to cover yet: https://copier.readthedocs.io/en/stable/faq/#how-can-i-alter-the-context-before-rendering-the-project

Template

Not relevant for this one :)

To Reproduce

copier copy --trust . /tmp/foo

Traceback (most recent call last):
  File "/Users/christianroth/repos/copier-repro/.pixi/envs/default/lib/python3.13/runpy.py", line 198, in _run_module_as_main
    return _run_code(code, main_globals, None,
                     "__main__", mod_spec)
  File "/Users/christianroth/repos/copier-repro/.pixi/envs/default/lib/python3.13/runpy.py", line 88, in _run_code
    exec(code, run_globals)
    ~~~~^^^^^^^^^^^^^^^^^^^
  File "/Users/christianroth/.vscode/extensions/ms-python.debugpy-2025.6.0-darwin-arm64/bundled/libs/debugpy/adapter/../../debugpy/launcher/../../debugpy/__main__.py", line 71, in <module>
    cli.main()
    ~~~~~~~~^^
  File "/Users/christianroth/.vscode/extensions/ms-python.debugpy-2025.6.0-darwin-arm64/bundled/libs/debugpy/adapter/../../debugpy/launcher/../../debugpy/../debugpy/server/cli.py", line 501, in main
    run()
    ~~~^^
  File "/Users/christianroth/.vscode/extensions/ms-python.debugpy-2025.6.0-darwin-arm64/bundled/libs/debugpy/adapter/../../debugpy/launcher/../../debugpy/../debugpy/server/cli.py", line 384, in run_module
    run_module_as_main(options.target, alter_argv=True)
    ~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/christianroth/.vscode/extensions/ms-python.debugpy-2025.6.0-darwin-arm64/bundled/libs/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_runpy.py", line 228, in _run_module_as_main
    return _run_code(code, main_globals, None, "__main__", mod_spec)
  File "/Users/christianroth/.vscode/extensions/ms-python.debugpy-2025.6.0-darwin-arm64/bundled/libs/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_runpy.py", line 118, in _run_code
    exec(code, run_globals)
    ~~~~^^^^^^^^^^^^^^^^^^^
  File "/Users/christianroth/repos/copier/copier/__main__.py", line 6, in <module>
    CopierApp.run()
    ~~~~~~~~~~~~~^^
  File "/Users/christianroth/repos/copier-repro/.pixi/envs/default/lib/python3.13/site-packages/plumbum/cli/application.py", line 640, in run
    inst, retcode = subapp.run(argv, exit=False)
                    ~~~~~~~~~~^^^^^^^^^^^^^^^^^^
  File "/Users/christianroth/repos/copier-repro/.pixi/envs/default/lib/python3.13/site-packages/plumbum/cli/application.py", line 635, in run
    retcode = inst.main(*tailargs)
  File "/Users/christianroth/repos/copier/copier/_cli.py", line 282, in main
    return _handle_exceptions(inner)
  File "/Users/christianroth/repos/copier/copier/_cli.py", line 71, in _handle_exceptions
    method()
    ~~~~~~^^
  File "/Users/christianroth/repos/copier/copier/_cli.py", line 273, in inner
    with self._worker(
         ~~~~~~~~~~~~^
        template_src,
        ^^^^^^^^^^^^^
    ...<3 lines>...
        overwrite=self.force or self.overwrite,
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    ) as worker:
    ^
  File "/Users/christianroth/repos/copier/copier/_main.py", line 267, in __exit__
    raise value
  File "/Users/christianroth/repos/copier/copier/_cli.py", line 280, in inner
    worker.run_copy()
    ~~~~~~~~~~~~~~~^^
  File "/Users/christianroth/repos/copier/copier/_main.py", line 94, in _wrapper
    return func(*args, **kwargs)
  File "/Users/christianroth/repos/copier/copier/_main.py", line 1015, in run_copy
    self._ask()
    ~~~~~~~~~^^
  File "/Users/christianroth/repos/copier/copier/_main.py", line 601, in _ask
    [question.get_questionary_structure()],
     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^
  File "/Users/christianroth/repos/copier/copier/_user_data.py", line 390, in get_questionary_structure
    "message": self.get_message(),
               ~~~~~~~~~~~~~~~~^^
  File "/Users/christianroth/repos/copier/copier/_user_data.py", line 365, in get_message
    if rendered_help := self.render_value(self.help):
                        ~~~~~~~~~~~~~~~~~^^^^^^^^^^^
  File "/Users/christianroth/repos/copier/copier/_user_data.py", line 472, in render_value
    return template.render({**self.context, **(extra_answers or {})})
           ~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/christianroth/repos/copier-repro/.pixi/envs/default/lib/python3.13/site-packages/jinja2/environment.py", line 1290, in render
    ctx = self.new_context(dict(*args, **kwargs))
  File "/Users/christianroth/repos/copier-repro/.pixi/envs/default/lib/python3.13/site-packages/jinja2/environment.py", line 1388, in new_context
    return new_context(
        self.environment, self.name, self.blocks, vars, shared, self.globals, locals
    )
  File "/Users/christianroth/repos/copier-repro/.pixi/envs/default/lib/python3.13/site-packages/jinja2/runtime.py", line 117, in new_context
    return environment.context_class(
           ~~~~~~~~~~~~~~~~~~~~~~~~~^
        environment, parent, template_name, blocks, globals=globals
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    )
    ^
  File "/Users/christianroth/repos/copier-repro/.pixi/envs/default/lib/python3.13/site-packages/copier_templates_extensions/extensions/context.py", line 23, in __init__
    parent.update(extension_self.hook(parent))
                  ~~~~~~~~~~~~~~~~~~~^^^^^^^^
  File "/Users/christianroth/repos/copier-repro/extensions/context_hooks.py", line 8, in hook
    context["hello_world"] = context["hello"] + ", " + context["world"] + '!'
                             ~~~~~~~^^^^^^^^^
KeyError: 'hello'

Logs

Expected behavior

Do not break my workflow

Screenshots/screencasts/logs

No response

Operating system

macOS

Operating system distribution and version

15.4.1

Copier version

copier 9.7.2.dev21+g711b32b

Python version

3.13

Installation method

pip+git

Additional context

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugtriageTrying to make sure if this is valid or not

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions