diff --git a/changelog/806.bugfix.rst b/changelog/806.bugfix.rst new file mode 100644 index 00000000..b23affe0 --- /dev/null +++ b/changelog/806.bugfix.rst @@ -0,0 +1 @@ +A historic hook call now remembers the arguments it was made with instead of the mapping the caller passed in, so mutating that mapping afterwards no longer changes what plugins registered later, or the result callback, receive (`#806 `_). diff --git a/src/pluggy/_caller.py b/src/pluggy/_caller.py index 52b02316..e31d8928 100644 --- a/src/pluggy/_caller.py +++ b/src/pluggy/_caller.py @@ -214,6 +214,10 @@ def call_historic( assert self._call_history is not None kwargs = kwargs or {} kwargs = self._apply_defaults(kwargs) + # ``kwargs`` may be a mapping the caller keeps and mutates; the call is + # memorized to be replayed, so it has to keep the values it was made + # with. + kwargs = dict(kwargs) self._verify_all_args_are_provided(kwargs) self._call_history.append((kwargs, result_callback)) # Historizing hooks don't return results. diff --git a/testing/test_pluginmanager.py b/testing/test_pluginmanager.py index 7de6c6de..1e293e7a 100644 --- a/testing/test_pluginmanager.py +++ b/testing/test_pluginmanager.py @@ -540,6 +540,37 @@ def he_method1(self, arg): assert out == [1, 10, 120, 12] +def test_call_historic_keeps_the_values_the_call_was_made_with( + pm: PluginManager, +) -> None: + """The memorized call is replayed with the values it was made with, even + when the caller's mapping is mutated after the call.""" + + class Hooks: + @hookspec(historic=True) + def he_method1(self, arg): ... + + pm.add_hookspecs(Hooks) + + out = [] + kwargs = {"arg": "value-at-call-time"} + pm.hook.he_method1.call_historic(lambda res: out.append(res), kwargs=kwargs) + kwargs["arg"] = "mutated-after-the-call" + + seen = [] + + class Plugin: + @hookimpl + def he_method1(self, arg): + seen.append(arg) + return arg + + pm.register(Plugin()) + + assert seen == ["value-at-call-time"] + assert out == ["value-at-call-time"] + + def test_historic_with_subset_hook_caller(pm: PluginManager) -> None: class Hooks: @hookspec(historic=True)