diff --git a/changelog/808.bugfix.rst b/changelog/808.bugfix.rst new file mode 100644 index 00000000..9a80ec2a --- /dev/null +++ b/changelog/808.bugfix.rst @@ -0,0 +1 @@ +Historic hook calls now copy their keyword argument mapping, so later changes to the caller's mapping do not affect calls replayed to newly registered plugins. diff --git a/docs/index.rst b/docs/index.rst index f2aaf957..9eed1bc8 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -995,6 +995,10 @@ using the :py:meth:`~pluggy.HookCaller.call_historic()` method: # historic callback is invoked here pm.register(mylateplugin) +The keyword argument mapping is copied when the call is recorded, so later +changes to the caller's mapping do not affect calls replayed to newly registered +plugins. + Note that if you :py:meth:`~pluggy.HookCaller.call_historic()` the :py:class:`~pluggy.HookCaller` (and thus your calling code) can not receive results back from the underlying *hookimpl* functions. diff --git a/src/pluggy/_caller.py b/src/pluggy/_caller.py index 52b02316..0b0013c7 100644 --- a/src/pluggy/_caller.py +++ b/src/pluggy/_caller.py @@ -215,7 +215,7 @@ def call_historic( kwargs = kwargs or {} kwargs = self._apply_defaults(kwargs) self._verify_all_args_are_provided(kwargs) - self._call_history.append((kwargs, result_callback)) + self._call_history.append((dict(kwargs), result_callback)) # Historizing hooks don't return results. # Remember firstresult isn't compatible with historic. # Copy because plugins may register other plugins during iteration (#438). diff --git a/testing/test_invocations.py b/testing/test_invocations.py index ccae8aea..18e509df 100644 --- a/testing/test_invocations.py +++ b/testing/test_invocations.py @@ -597,6 +597,29 @@ def hello(self, arg, new_arg): assert seen == [(1, "spec"), (2, "call")] + def test_historic_replay_copies_call_kwargs(self, pm: PluginManager) -> None: + class Api: + @hookspec(historic=True) + def hello(self, arg): + pass + + pm.add_hookspecs(Api) + + kwargs = {"arg": "at-call-time"} + pm.hook.hello.call_historic(kwargs=kwargs) + kwargs["arg"] = "mutated" + + seen = [] + + class Plugin: + @hookimpl + def hello(self, arg): + seen.append(arg) + + pm.register(Plugin()) + + assert seen == ["at-call-time"] + def test_call_extra(self, pm: PluginManager) -> None: """Test calls with extras respect defaults."""