Skip to content

Raise when unregister's plugin and name disagree - #747

Open
feiiiiii5 wants to merge 3 commits into
pytest-dev:mainfrom
feiiiiii5:fix/unregister-plugin-name-mismatch
Open

feiiiiii5 wants to merge 3 commits into
pytest-dev:mainfrom
feiiiiii5:fix/unregister-plugin-name-mismatch

Conversation

@feiiiiii5

Copy link
Copy Markdown
Contributor

PluginManager.unregister() documents that its two ways of naming a plugin are interchangeable, and that they must agree:

"""Unregister a plugin and all of its hook implementations.
The plugin can be specified either by the plugin object or the plugin
name. If both are specified, they must agree.

They do not have to agree today, and the mismatch does not fail — it produces a state that is wrong in two directions at once, because unregister looks hook callers up by plugin but deletes the registry entry by name. So pm.unregister(plugin=first, name="second") strips first's implementations and drops second from the registry. Running that on 012fd24:

registry after unregister(first, name='second')  : ['first']
is_registered(first)  : True           # still registered, now with zero impls
is_registered(second) : False          # not registered...
hook still calls      : ['second:1']   # ...but its impl is still called

second is simultaneously not registered and still being called, and first is registered with nothing left to run.

The fix is an early guard, before anything is mutated:

        elif plugin is not None:
            # Both given: the hookcallers are looked up by plugin but the
            # registry entry is deleted by name, so a mismatch would strip
            # one plugin's impls while dropping an unrelated registry entry.
            registered_name = self.get_name(plugin)
            if registered_name is not None and registered_name != name:
                raise ValueError(
                    f"Plugin {plugin!r} is registered under name "
                    f"{registered_name!r}, not {name!r}"
                )

It raises rather than silently preferring one of the two, because the docstring already requires agreement and pluggy already raises ValueError from register() on the analogous mismatch. It converts a silent wrong state into a loud error, not the reverse. The registered_name is not None clause keeps the existing behavior for an unregistered plugin, which test_unregister_blocked relies on when it unregisters a blocked plugin twice.

Tests

test_unregister_plugin_and_name_must_agree registers two plugins under distinct names, asserts the ValueError, and then asserts that the rejected call changed no state — both plugins still registered, both implementations still on the hook caller.

On 012fd24 it fails with Failed: DID NOT RAISE ValueError; testing/test_pluginmanager.py is 74 passed here against 73 there, and the full suite is 203 passed against 202 passed on 012fd24 — the new test, no regressions.

Gates, matching what pre-commit runs: ruff check and ruff format --check clean on both files, and mypy src/pluggy testing reports Success: no issues found in 24 source files both before and after the change. I did not run pre-commit itself, since it installs its own environments.

I found no prior art: searching for unregister plus plugin/name/must agree/mismatch/ValueError turns up #52, #648, #740, #734, #431, #718 and #122, none about the two arguments disagreeing, and none of the 7 open PRs touches this. git log on the path shows unregister untouched by any recent commit, so this reads as long-standing rather than a regression — the deeper git log -S/blame archaeology is limited because this is a --filter=blob:none partial clone whose older promisor blobs are not local.

Left alone: docs/index.rst:598-602 says a hookimpl argument with a default is "not passed by pluggy, even when the hook caller provides a value", contradicting both the 1.7 "Hookimpl argument defaults" section and what _multicall does. That is a stale-doc bug, worth its own change.

unregister() looks hookcallers up by plugin but deletes the registry
entry by name, and never checked that the two agree. Passing a plugin
registered as "first" together with name="second" therefore stripped the
plugin's own implementations while dropping an unrelated plugin's
registry entry — leaving that plugin reported as unregistered with its
implementations still on the hook caller.

The docstring already says "If both are specified, they must agree", so
raise before mutating anything.
The spec body is never executed, so it was the one uncovered line this
diff added, and it dragged Codecov's patch coverage to 96%. The
.coveragerc already excludes '`- `...`' spec bodies and three other
tests in this file use one.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants