Skip to content

feat(config): replace TypedDict options with Hook*Configuration - #5

Open
RonnyPfannschmidt wants to merge 1 commit into
refactor/split-hook-modulesfrom
refactor/configuration-objects
Open

feat(config): replace TypedDict options with Hook*Configuration#5
RonnyPfannschmidt wants to merge 1 commit into
refactor/split-hook-modulesfrom
refactor/configuration-objects

Conversation

@RonnyPfannschmidt

@RonnyPfannschmidt RonnyPfannschmidt commented Jul 22, 2026

Copy link
Copy Markdown
Owner

Review PR — step 2 of 7.

This PR targets the previous step's branch, so its diff is only this step's change. Review happens here. The corresponding upstream PR, which is the one that actually merges, is pytest-dev#704.

Merges happen upstream one step at a time, bottom-up. When step 2 lands upstream, this PR is closed and the rest of the stack is rebased onto the new main.

Step Branch Review (downstream) Merge (upstream)
1 refactor/split-hook-modules #6 pytest-dev#703
2 refactor/configuration-objects #5 pytest-dev#704
3 refactor/markers-attach-config #7 pytest-dev#706
4 refactor/hookimpl-wrapper-types #8 pytest-dev#707
5 refactor/hookcaller-and-execution #9 pytest-dev#708
6 refactor/project-spec #10 pytest-dev#709
7 refactor/async-submitter #11 pytest-dev#710

Summary

  • Add HookspecConfiguration / HookimplConfiguration; markers attach these objects
  • Private _discover_* / _read_* own registration discovery
  • parse_hookimpl_opts / parse_hookspec_opts kept as a deprecated pytest concession (legacy dicts), only invoked when a subclass overrides them and no modern config attribute was found
  • TypedDicts remain importable via _pytest_compat for pytest typing

Stacked on refactor/split-hook-modules for reviewable incremental diff.

Test plan

  • uv run pytest (164 passed)
  • uv run pre-commit run -a

Made with Cursor

Summary by Sourcery

Replace legacy dict-based hook option encodings with configuration objects and preserve compatibility for pytest and other legacy callers.

New Features:

  • Introduce HookspecConfiguration and HookimplConfiguration classes as the primary configuration carriers for hook specifications and implementations.
  • Expose the new HookspecConfiguration and HookimplConfiguration types from the public pluggy package API.

Enhancements:

  • Update decorators, hook calling, and plugin manager registration to use configuration objects instead of TypedDict mappings, with internal helpers to convert to and from legacy mappings.
  • Adjust tests and hookcaller behavior to work with configuration objects, including attribute-style access to hook implementation flags.

Tests:

  • Add a dedicated test suite for configuration classes, mapping shims, marker integration, and historic hook behavior.
  • Adapt existing tests to validate the new configuration-based API and the behavior of overridden parse_hookimpl_opts implementations.

Chores:

  • Introduce a _pytest_compat module housing legacy TypedDict definitions and mapping helpers to support pytest and other callers during migration.

@sourcery-ai

sourcery-ai Bot commented Jul 22, 2026

Copy link
Copy Markdown

Reviewer's Guide

Introduce HookspecConfiguration and HookimplConfiguration as the primary hook config objects, migrate internal registration/calling logic and markers to use them, and keep legacy dict/TypedDict options available via a pytest compatibility module and deprecated shims.

File-Level Changes

Change Details Files
Replace dict-based hook specification/implementation options with configuration classes and mapping shims.
  • Remove HookspecOpts and HookimplOpts TypedDicts and normalize_hookimpl_opts from the core config module.
  • Add final, slot-based HookspecConfiguration and HookimplConfiguration classes with validation and repr implementations.
  • Introduce helpers to convert between Mapping[str, Any] and configuration objects and to serialize back to legacy mappings.
src/pluggy/_config.py
Update PluginManager to discover and use configuration objects while preserving deprecated parse_*_opts overrides for legacy subclasses.
  • Change register/add_hookspecs to pass HookimplConfiguration/HookspecConfiguration instead of dicts into HookImpl and HookCaller.
  • Add private read_configuration helpers that read marker-attached attributes, accepting both new configuration objects and legacy dicts.
  • Add discover_configuration methods that prefer modern configuration; only call parse_hookimpl_opts/parse_hookspec_opts when a subclass overrides them and no modern config is found.
  • Redefine parse_hookimpl_opts/parse_hookspec_opts to be deprecated, returning legacy dict-shaped options via config-to-mapping conversion.
src/pluggy/_manager.py
Change hook markers and hook calling machinery to work with configuration objects instead of dicts.
  • Update HookspecMarker and HookimplMarker to attach HookspecConfiguration/HookimplConfiguration instances to functions instead of dicts.
  • Change HookSpec to accept HookspecConfiguration and access attributes rather than dict keys.
  • Update HookCaller to use HookspecConfiguration in constructor/set_specification, check historic/firstresult via attributes, and construct HookimplConfiguration in call_extra.
  • Update HookImpl to store HookimplConfiguration and read wrapper/ordering flags via attributes.
src/pluggy/_decorators.py
src/pluggy/_caller.py
src/pluggy/_implementation.py
Adjust public exports and hook module to reflect the new configuration types and pytest compatibility layer.
  • Update _hooks to export HookspecConfiguration and HookimplConfiguration instead of TypedDict-based opts and remove normalize_hookimpl_opts usage.
  • Expose HookspecConfiguration and HookimplConfiguration from pluggy.init while moving HookspecOpts/HookimplOpts imports to the new pytest compatibility module.
src/pluggy/_hooks.py
src/pluggy/__init__.py
Introduce a pytest compatibility module that retains legacy TypedDict option shapes for typing-only use.
  • Add _pytest_compat.py defining HookspecOpts and HookimplOpts TypedDicts with the original fields.
  • Re-export configuration classes and mapping conversion helpers from _pytest_compat for pytest callers.
  • Update imports across the codebase so only pytest-facing surfaces use HookspecOpts/HookimplOpts from this module.
src/pluggy/_pytest_compat.py
src/pluggy/_manager.py
src/pluggy/__init__.py
Add and adjust tests to cover configuration behavior, legacy mapping shims, and discovery semantics.
  • Add test_configuration.py to validate configuration defaults, field behaviors, mapping conversions, marker attachment of configuration objects, integration with PluginManager hooks, and historic hook behavior.
  • Update tests that previously inspected dict-based opts to read configuration attributes instead (e.g., firstresult and hookimpl flags).
  • Adapt test_parse_hookimpl_override to the new parse_hookimpl_opts behavior and discovery mechanics.
testing/test_configuration.py
testing/test_hookcaller.py
testing/test_details.py

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - I've found 3 issues, and left some high level feedback:

  • The __repr__ implementations for HookspecConfiguration and HookimplConfiguration skip falsy values, which makes it hard to see flags explicitly set to False or None; consider including all slots so configuration state is fully inspectable.
  • The broad except Exception around getattr in _read_hookimpl_configuration and _read_hookspec_configuration may hide real bugs in plugin/spec code; tightening this to specific exception types or at least logging unexpected errors would make configuration discovery easier to debug.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `__repr__` implementations for `HookspecConfiguration` and `HookimplConfiguration` skip falsy values, which makes it hard to see flags explicitly set to `False` or `None`; consider including all slots so configuration state is fully inspectable.
- The broad `except Exception` around `getattr` in `_read_hookimpl_configuration` and `_read_hookspec_configuration` may hide real bugs in plugin/spec code; tightening this to specific exception types or at least logging unexpected errors would make configuration discovery easier to debug.

## Individual Comments

### Comment 1
<location path="src/pluggy/_manager.py" line_range="219" />
<code_context>
+        config = self._read_hookimpl_configuration(plugin, name)
+        if config is None:
+            return None
+        return cast(HookimplOpts, hookimpl_config_to_mapping(config))

     def unregister(
</code_context>
<issue_to_address>
**issue (bug_risk):** Using `cast` here will raise at runtime unless `cast` is imported in this module.

`cast` must be imported (e.g., `from typing import cast`) for this call to work; otherwise `parse_hookimpl_opts` will raise a `NameError` at runtime in this compatibility path.
</issue_to_address>

### Comment 2
<location path="src/pluggy/_manager.py" line_range="350" />
<code_context>
+        config = self._read_hookspec_configuration(module_or_class, name)
+        if config is None:
+            return None
+        return cast(HookspecOpts, hookspec_config_to_mapping(config))

     def get_plugins(self) -> set[Any]:
</code_context>
<issue_to_address>
**issue (bug_risk):** Same `cast` runtime issue in `parse_hookspec_opts` as in `parse_hookimpl_opts`.

This deprecated path also calls `cast(...)` without defining it, so it will raise `NameError` when invoked. Please apply the same local `cast` definition here as in `parse_hookimpl_opts` to preserve hookspec compatibility.
</issue_to_address>

### Comment 3
<location path="src/pluggy/_manager.py" line_range="165" />
<code_context>
-        options for items decorated with :class:`HookimplMarker`.
-        """
-        method: object = getattr(plugin, name)
+    def _read_hookimpl_configuration(
+        self, plugin: _Plugin, name: str
+    ) -> HookimplConfiguration | None:
</code_context>
<issue_to_address>
**issue (complexity):** Consider collapsing the new `_read_*` and `_discover_*` helpers into a single `_get_*_configuration` per type and reworking `parse_*_opts` as pure legacy mapping helpers to simplify the discovery flow and avoid redundant config↔mapping round-trips.

You can remove a layer of indirection and the config↔mapping round‑trip by collapsing `_read_*` + `_discover_*` into a single internal helper per type and making the deprecated `parse_*_opts` a pure legacy mapping helper.

### 1. Collapse `_read_*` and `_discover_*` into a single configuration helper

Instead of `_read_hookimpl_configuration` + `_discover_hookimpl_configuration`, use a single `_get_hookimpl_configuration` that:

- Reads the marker attribute.
- Accepts both `HookimplConfiguration` and mapping markers.
- Only calls legacy `parse_hookimpl_opts` if a subclass overrides it.

```python
def _get_hookimpl_configuration(
    self, plugin: _Plugin, name: str
) -> HookimplConfiguration | None:
    try:
        method: object = getattr(plugin, name)
    except Exception:
        return None

    if not inspect.isroutine(method):
        return None

    try:
        attr: object = getattr(method, self.project_name + "_impl", None)
    except Exception:  # pragma: no cover
        attr = None

    if isinstance(attr, HookimplConfiguration):
        return attr
    if isinstance(attr, Mapping):
        return hookimpl_config_from_mapping(attr)

    # Legacy path: only if subclass overrides parse_hookimpl_opts
    if type(self).parse_hookimpl_opts is not PluginManager.parse_hookimpl_opts:
        legacy = self.parse_hookimpl_opts(plugin, name)
        if isinstance(legacy, Mapping):
            return hookimpl_config_from_mapping(legacy)

    return None
```

Then `register` becomes:

```python
for name in dir(plugin):
    hookimpl_config = self._get_hookimpl_configuration(plugin, name)
    if hookimpl_config is not None:
        method: _HookImplFunction[object] = getattr(plugin, name)
        hookimpl = HookImpl(plugin, plugin_name, method, hookimpl_config)
        name = hookimpl_config.specname or name
        ...
```

Apply the same pattern for hookspecs:

```python
def _get_hookspec_configuration(
    self, module_or_class: _Namespace, name: str
) -> HookspecConfiguration | None:
    try:
        method = getattr(module_or_class, name)
    except Exception:
        return None

    try:
        attr: object = getattr(method, self.project_name + "_spec", None)
    except Exception:  # pragma: no cover
        attr = None

    if isinstance(attr, HookspecConfiguration):
        return attr
    if isinstance(attr, Mapping):
        return hookspec_config_from_mapping(attr)

    if type(self).parse_hookspec_opts is not PluginManager.parse_hookspec_opts:
        legacy = self.parse_hookspec_opts(module_or_class, name)
        if isinstance(legacy, Mapping):
            return hookspec_config_from_mapping(legacy)

    return None
```

And `add_hookspecs` uses `_get_hookspec_configuration` directly.

This keeps all behavior (including the override check) but removes the tight coupling between `_read_*` and `_discover_*` and makes the registration/spec discovery path a single function per type.

### 2. Make `parse_*_opts` purely legacy mapping helpers (no round‑trip)

The deprecated `parse_*_opts` can operate directly on the marker attribute as mappings, without calling the internal configuration helper. That removes the config→mapping→config round‑trip when subclasses override them.

```python
def parse_hookimpl_opts(self, plugin: _Plugin, name: str) -> HookimplOpts | None:
    """Return legacy dict-shaped hookimpl options, if any.

    .. deprecated::
       Thin pytest/support concession. Prefer marker-attached configuration
       objects; core registration uses `_get_hookimpl_configuration`.
    """
    try:
        method: object = getattr(plugin, name)
    except Exception:
        return None
    if not inspect.isroutine(method):
        return None
    try:
        attr: object = getattr(method, self.project_name + "_impl", None)
    except Exception:  # pragma: no cover
        return None
    if isinstance(attr, Mapping):
        return cast(HookimplOpts, attr)
    if isinstance(attr, HookimplConfiguration):
        # Still support config objects when someone calls this explicitly.
        return cast(HookimplOpts, hookimpl_config_to_mapping(attr))
    return None
```

```python
def parse_hookspec_opts(
    self, module_or_class: _Namespace, name: str
) -> HookspecOpts | None:
    """Return legacy dict-shaped hookspec options, if any.

    .. deprecated::
       Thin pytest/support concession. Prefer marker-attached configuration
       objects; core discovery uses `_get_hookspec_configuration`.
    """
    try:
        method = getattr(module_or_class, name)
    except Exception:
        return None
    try:
        attr: object = getattr(method, self.project_name + "_spec", None)
    except Exception:  # pragma: no cover
        return None
    if isinstance(attr, Mapping):
        return cast(HookspecOpts, attr)
    if isinstance(attr, HookspecConfiguration):
        return cast(HookspecOpts, hookspec_config_to_mapping(attr))
    return None
```

With this:

- Core discovery/registration uses a single `_get_*_configuration` per type.
- Legacy `parse_*_opts` stays available for pytest/support and subclass overrides, but is clearly separated from the main path and doesn’t participate in config↔mapping round‑trips.
- The override detection is still there, but the control flow is simpler: one internal function per type, one public legacy helper.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread src/pluggy/_manager.py
config = self._read_hookimpl_configuration(plugin, name)
if config is None:
return None
return cast(HookimplOpts, hookimpl_config_to_mapping(config))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (bug_risk): Using cast here will raise at runtime unless cast is imported in this module.

cast must be imported (e.g., from typing import cast) for this call to work; otherwise parse_hookimpl_opts will raise a NameError at runtime in this compatibility path.

Comment thread src/pluggy/_manager.py
config = self._read_hookspec_configuration(module_or_class, name)
if config is None:
return None
return cast(HookspecOpts, hookspec_config_to_mapping(config))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (bug_risk): Same cast runtime issue in parse_hookspec_opts as in parse_hookimpl_opts.

This deprecated path also calls cast(...) without defining it, so it will raise NameError when invoked. Please apply the same local cast definition here as in parse_hookimpl_opts to preserve hookspec compatibility.

Comment thread src/pluggy/_manager.py
options for items decorated with :class:`HookimplMarker`.
"""
method: object = getattr(plugin, name)
def _read_hookimpl_configuration(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (complexity): Consider collapsing the new _read_* and _discover_* helpers into a single _get_*_configuration per type and reworking parse_*_opts as pure legacy mapping helpers to simplify the discovery flow and avoid redundant config↔mapping round-trips.

You can remove a layer of indirection and the config↔mapping round‑trip by collapsing _read_* + _discover_* into a single internal helper per type and making the deprecated parse_*_opts a pure legacy mapping helper.

1. Collapse _read_* and _discover_* into a single configuration helper

Instead of _read_hookimpl_configuration + _discover_hookimpl_configuration, use a single _get_hookimpl_configuration that:

  • Reads the marker attribute.
  • Accepts both HookimplConfiguration and mapping markers.
  • Only calls legacy parse_hookimpl_opts if a subclass overrides it.
def _get_hookimpl_configuration(
    self, plugin: _Plugin, name: str
) -> HookimplConfiguration | None:
    try:
        method: object = getattr(plugin, name)
    except Exception:
        return None

    if not inspect.isroutine(method):
        return None

    try:
        attr: object = getattr(method, self.project_name + "_impl", None)
    except Exception:  # pragma: no cover
        attr = None

    if isinstance(attr, HookimplConfiguration):
        return attr
    if isinstance(attr, Mapping):
        return hookimpl_config_from_mapping(attr)

    # Legacy path: only if subclass overrides parse_hookimpl_opts
    if type(self).parse_hookimpl_opts is not PluginManager.parse_hookimpl_opts:
        legacy = self.parse_hookimpl_opts(plugin, name)
        if isinstance(legacy, Mapping):
            return hookimpl_config_from_mapping(legacy)

    return None

Then register becomes:

for name in dir(plugin):
    hookimpl_config = self._get_hookimpl_configuration(plugin, name)
    if hookimpl_config is not None:
        method: _HookImplFunction[object] = getattr(plugin, name)
        hookimpl = HookImpl(plugin, plugin_name, method, hookimpl_config)
        name = hookimpl_config.specname or name
        ...

Apply the same pattern for hookspecs:

def _get_hookspec_configuration(
    self, module_or_class: _Namespace, name: str
) -> HookspecConfiguration | None:
    try:
        method = getattr(module_or_class, name)
    except Exception:
        return None

    try:
        attr: object = getattr(method, self.project_name + "_spec", None)
    except Exception:  # pragma: no cover
        attr = None

    if isinstance(attr, HookspecConfiguration):
        return attr
    if isinstance(attr, Mapping):
        return hookspec_config_from_mapping(attr)

    if type(self).parse_hookspec_opts is not PluginManager.parse_hookspec_opts:
        legacy = self.parse_hookspec_opts(module_or_class, name)
        if isinstance(legacy, Mapping):
            return hookspec_config_from_mapping(legacy)

    return None

And add_hookspecs uses _get_hookspec_configuration directly.

This keeps all behavior (including the override check) but removes the tight coupling between _read_* and _discover_* and makes the registration/spec discovery path a single function per type.

2. Make parse_*_opts purely legacy mapping helpers (no round‑trip)

The deprecated parse_*_opts can operate directly on the marker attribute as mappings, without calling the internal configuration helper. That removes the config→mapping→config round‑trip when subclasses override them.

def parse_hookimpl_opts(self, plugin: _Plugin, name: str) -> HookimplOpts | None:
    """Return legacy dict-shaped hookimpl options, if any.

    .. deprecated::
       Thin pytest/support concession. Prefer marker-attached configuration
       objects; core registration uses `_get_hookimpl_configuration`.
    """
    try:
        method: object = getattr(plugin, name)
    except Exception:
        return None
    if not inspect.isroutine(method):
        return None
    try:
        attr: object = getattr(method, self.project_name + "_impl", None)
    except Exception:  # pragma: no cover
        return None
    if isinstance(attr, Mapping):
        return cast(HookimplOpts, attr)
    if isinstance(attr, HookimplConfiguration):
        # Still support config objects when someone calls this explicitly.
        return cast(HookimplOpts, hookimpl_config_to_mapping(attr))
    return None
def parse_hookspec_opts(
    self, module_or_class: _Namespace, name: str
) -> HookspecOpts | None:
    """Return legacy dict-shaped hookspec options, if any.

    .. deprecated::
       Thin pytest/support concession. Prefer marker-attached configuration
       objects; core discovery uses `_get_hookspec_configuration`.
    """
    try:
        method = getattr(module_or_class, name)
    except Exception:
        return None
    try:
        attr: object = getattr(method, self.project_name + "_spec", None)
    except Exception:  # pragma: no cover
        return None
    if isinstance(attr, Mapping):
        return cast(HookspecOpts, attr)
    if isinstance(attr, HookspecConfiguration):
        return cast(HookspecOpts, hookspec_config_to_mapping(attr))
    return None

With this:

  • Core discovery/registration uses a single _get_*_configuration per type.
  • Legacy parse_*_opts stays available for pytest/support and subclass overrides, but is clearly separated from the main path and doesn’t participate in config↔mapping round‑trips.
  • The override detection is still there, but the control flow is simpler: one internal function per type, one public legacy helper.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR migrates pluggy’s hook option representation from legacy dict/TypedDict shapes to dedicated configuration objects, while preserving compatibility paths needed by pytest and other legacy integrations.

Changes:

  • Introduces HookspecConfiguration / HookimplConfiguration and updates markers to attach these objects to functions.
  • Updates core registration/discovery paths (PluginManager.register / add_hookspecs, HookCaller, HookImpl, HookSpec) to consume configuration objects, with mapping shims for legacy encodings.
  • Adds a pytest-compat module for legacy TypedDict typing and expands tests/docs to cover the new configuration-based API and migration behavior.

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
testing/test_hookcaller.py Updates tests to assert configuration via attribute-style access instead of dict indexing.
testing/test_details.py Adjusts a subclass override test for legacy parse_hookimpl_opts behavior.
testing/test_configuration.py Adds dedicated tests for configuration classes, mapping shims, and discovery/override behavior.
src/pluggy/_pytest_compat.py Adds legacy TypedDict definitions and mapping conversion helpers for pytest/support.
src/pluggy/_manager.py Switches hook registration/spec discovery to configuration objects with private _read_*/_discover_* helpers and legacy fallback.
src/pluggy/_implementation.py Updates HookImpl to store and expose configuration via object attributes.
src/pluggy/_hooks.py Updates exports/imports to reflect configuration-object API surface.
src/pluggy/_decorators.py Updates markers to attach configuration objects; updates HookSpec to read config attributes.
src/pluggy/_config.py Replaces TypedDict option containers with final configuration classes plus mapping shims.
src/pluggy/_caller.py Updates HookCaller to use configuration objects for historic/firstresult and temp hookimpl opts.
src/pluggy/init.py Exposes the new configuration classes publicly; keeps legacy TypedDicts importable.
docs/index.rst Updates documentation to describe configuration objects and deprecated legacy parsing methods.
docs/api_reference.rst Updates API docs to reference configuration classes instead of TypedDict option types.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 334 to 337
if val:
assert he_myhook1.example_impl.get(name)
assert getattr(he_myhook1.example_impl, name)
else:
assert not hasattr(he_myhook1, name)
Markers attach HookspecConfiguration/HookimplConfiguration objects.
Registration discovers those privately; parse_hookimpl_opts and
parse_hookspec_opts remain a deprecated pytest concession that returns
legacy dicts and is only called when a subclass overrides them and no
modern configuration attribute was found.

Co-authored-by: Cursor AI <ai@cursor.sh>
Co-authored-by: Cursor Grok 4.5 <grok@cursor.com>
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