Skip to content

warn on ambiguous -p plugin module usage#8

Open
yashwant86 wants to merge 6 commits intomainfrom
pr-14270
Open

warn on ambiguous -p plugin module usage#8
yashwant86 wants to merge 6 commits intomainfrom
pr-14270

Conversation

@yashwant86
Copy link
Copy Markdown

@yashwant86 yashwant86 commented Apr 7, 2026

Mirror of pytest-dev#14270


Summary by MergeMonkey

  • Docs & Guides:
    • Added changelog entry documenting new warning for ambiguous plugin module usage with -p flag
  • Feature Drops:
    • Warns users when -p loads a module with no pytest hooks but a pytest11 entry-point exists for a submodule

@mergemonkeyhq
Copy link
Copy Markdown

mergemonkeyhq Bot commented Apr 11, 2026

Risk AssessmentNEEDS-TESTING · ~15 min review

Focus areas: Entry-point value parsing and prefix matching · Hook detection via dir() and parse_hookimpl_opts · Test coverage of warning conditions

Assessment: Adds warning detection for plugin loading ambiguity; requires testing of entry-point scanning logic.

Walkthrough

When a user runs pytest with -p <module_name>, the plugin loader attempts to import the module. If the module has no pytest hooks but a pytest11 entry-point exists for a submodule (e.g., -p pytest_recording when the entry-point is pytest_recording.plugin), the new code detects this ambiguity and warns the user with a suggestion to use the correct submodule entry-point name instead.

Changes

Files Summary
Plugin Loading Ambiguity Detection
src/_pytest/config/__init__.py
testing/test_config.py
changelog/14135.bugfix.rst
Detects and warns when -p loads a module with no pytest hooks but a pytest11 entry-point exists for a submodule, helping users identify the correct plugin to load.

Sequence Diagram

sequenceDiagram
    participant User
    participant PytestConfig
    participant PluginManager
    participant EntryPointScanner
    User->>PytestConfig: pytest -p pytest_recording
    PytestConfig->>PluginManager: import_plugin("pytest_recording", consider_entry_points=True)
    PluginManager->>PluginManager: load_setuptools_entrypoints("pytest11", name="pytest_recording")
    PluginManager->>PluginManager: loaded = False (no direct entry-point)
    PluginManager->>PluginManager: import pytest_recording module
    PluginManager->>PluginManager: register(mod, "pytest_recording")
    PluginManager->>EntryPointScanner: _warn_about_submodule_entrypoint_plugin("pytest_recording", mod)
    EntryPointScanner->>EntryPointScanner: _plugin_has_pytest_hooks(mod) = False
    EntryPointScanner->>EntryPointScanner: scan distributions for pytest11 entry-points
    EntryPointScanner->>EntryPointScanner: find entry-points with value="pytest_recording.*"
    EntryPointScanner->>User: warn with suggestion: -p recording
Loading

Dig Deeper With Commands

  • /review <file-path> <function-optional>
  • /chat <file-path> "<question>"
  • /roast <file-path>

Runs only when explicitly triggered.

if ep.group == "pytest11"
and ep.name != modname
and isinstance(getattr(ep, "value", None), str)
and getattr(ep, "value").split(":", 1)[0].startswith(modname_prefix)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Iterates all installed distributions on every ambiguous check

importlib.metadata.distributions() scans all installed packages. This runs every time a -p loaded module has no hooks. Consider caching the result or using importlib.metadata.entry_points(group='pytest11') which is more targeted and faster on Python 3.12+.

if ep.group == "pytest11"
and ep.name != modname
and isinstance(getattr(ep, "value", None), str)
and getattr(ep, "value").split(":", 1)[0].startswith(modname_prefix)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Double getattr call risks AttributeError on race

Line 920 checks getattr(ep, 'value', None) but line 921 calls getattr(ep, 'value') without default. If value is a property that could change or raise, this could throw. Use a single assignment: val = getattr(ep, 'value', None) then check isinstance(val, str) and val.split(...)....

@mergemonkeyhq
Copy link
Copy Markdown

mergemonkeyhq Bot commented Apr 11, 2026

Actionable Comments Posted: 2

🧹 Nitpick comments (1)
\`\_plugin\_has\_pytest\_hooks\` has no explicit return type path - src/_pytest/config/__init__.py (940)
The function relies on \`any\(\)\` which always returns bool, so it's correct. However, \`parse\_hookimpl\_opts\` may raise exceptions for certain attributes \(e.g., descriptors\). Consider wrapping with try/except to avoid unexpected failures during introspection.
🧾 Coverage Summary
✔️ Covered (3 files)
- changelog/14135.bugfix.rst
- src/_pytest/config/__init__.py
- testing/test_config.py

@yashwant86
Copy link
Copy Markdown
Author

/review --force

@mergemonkeyhq
Copy link
Copy Markdown

mergemonkeyhq Bot commented Apr 11, 2026

Actionable Comments Posted: 0

👀 Worth a Look (1)
[MEDIUM] Hook detection can still raise on exotic plugin attributes - src/_pytest/config/__init__.py (938)
The new warning path calls \`\_plugin\_has\_pytest\_hooks\(\)\` on every \`-p\` module import that didn't resolve through entry points. That helper walks \`dir\(plugin\)\` and then \`parse\_hookimpl\_opts\(\)\` does an unguarded \`getattr\(plugin, name\)\` for every \`pytest\_\*\` attribute, so a plugin exposing a problematic descriptor/property can now fail during warning detection instead of just being imported.

Wrap the hook probe in a small \`try/except Exception\` around \`parse\_hookimpl\_opts\(\)\`/\`getattr\`, or limit detection to safe attribute sources for modules so the warning path can't break plugin import.
🧹 Nitpick comments (1)
Entry-point \`value\` is read twice in the new suggestion filter - src/_pytest/config/__init__.py (920)
The suggestion scan guards \`getattr\(ep, "value", None\)\` and then immediately reads \`getattr\(ep, "value"\)\` again. For normal \`importlib.metadata.EntryPoint\` objects that's fine, but the code now assumes repeated access is stable; a custom entry-point-like object with a property or side effect can pass the first check and fail or change on the second.

Store \`value = getattr\(ep, "value", None\)\` once inside the loop/comprehension and use that cached value for both the type check and split.
🧾 Coverage Summary
✔️ Covered (3 files)
- changelog/14135.bugfix.rst
- src/_pytest/config/__init__.py
- testing/test_config.py

@yashwant86
Copy link
Copy Markdown
Author

/review --force

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants