Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ repos:
hooks:
- id: blacken-docs
additional_dependencies: [black==24.2.0]
exclude: ^design/
- repo: local
hooks:
- id: rst
Expand All @@ -48,4 +49,4 @@ repos:
- id: mypy
files: ^(src/|testing/)
args: []
additional_dependencies: [pytest]
additional_dependencies: [pytest, types-greenlet]
8 changes: 8 additions & 0 deletions changelog/704.removal.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Hook options are now :class:`pluggy.HookspecConfiguration` /
:class:`pluggy.HookimplConfiguration` objects (markers attach these instead of
dicts). ``PluginManager.parse_hookimpl_opts`` /
``parse_hookspec_opts`` remain as a deprecated pytest/support concession that
returns legacy dicts and are only invoked during registration when a subclass
overrides them and no modern configuration attribute was found.
``HookspecOpts`` / ``HookimplOpts`` TypedDicts remain importable for
pytest/typing compatibility.
3 changes: 3 additions & 0 deletions changelog/706.trivial.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
``HookSpec`` now stores its :class:`pluggy.HookspecConfiguration` as
``config``; the old ``opts`` attribute remains as a deprecated alias
property.
9 changes: 9 additions & 0 deletions changelog/707.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
Hook implementations are now represented by dedicated types:
:class:`pluggy.NormalImpl` for normal implementations and
:class:`pluggy.WrapperImpl` for (old- and new-style) wrappers, both
subclasses of :class:`pluggy.HookImpl`.
``HookimplConfiguration.create_hookimpl()`` selects the appropriate
subclass, and ``WrapperImpl.setup_and_get_completion_hook()`` exposes
wrapper setup/teardown as a ``CompletionHook`` callback.
``HookImpl`` now stores its configuration as ``hookimpl_config``; the old
``opts`` attribute remains as a deprecated alias property.
12 changes: 12 additions & 0 deletions changelog/708.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
:class:`pluggy.HookCaller` is now a runtime-checkable
:class:`~typing.Protocol` implemented by the new concrete callers
:class:`pluggy.NormalHookCaller` (split normal/wrapper implementation
lists), :class:`pluggy.HistoricHookCaller` (call memorization and replay,
no wrappers) and :class:`pluggy.SubsetHookCaller`.
``isinstance(caller, HookCaller)`` keeps working for all of them.

The hook execution engine now runs a dual-sequence multicall: wrappers own
their setup/teardown through ``CompletionHook`` callbacks which run LIFO
after the normal implementations, removing all wrapper flag branching from
the hot loop. Hook call monitoring callbacks still receive a single
combined implementation list.
6 changes: 6 additions & 0 deletions changelog/709.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
New :class:`pluggy.ProjectSpec` hub bundles a project's
:class:`~pluggy.HookspecMarker`, :class:`~pluggy.HookimplMarker` and
:meth:`~pluggy.ProjectSpec.create_plugin_manager` under one project name.
:class:`~pluggy.HookspecMarker`, :class:`~pluggy.HookimplMarker` and
:class:`~pluggy.PluginManager` now accept either a project name string or
a ``ProjectSpec`` instance; plain strings keep working unchanged.
9 changes: 9 additions & 0 deletions changelog/710.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
New async bridge for awaitable hook results:
``await pm.run_async(lambda: pm.hook.my_hook())`` runs the hook call in a
greenlet context where awaitable results from hook implementations are
awaited on the current event loop ("await-me-maybe"). Outside
``run_async``, awaitable results are passed through unchanged, as before.
Requires the optional ``greenlet`` dependency, installable via
``pluggy[async]``. The bridge is a persistent per-manager ``Submitter``;
async wrapper generators are not auto-awaited (a manual
``async_generator_to_sync`` helper is provided).
99 changes: 99 additions & 0 deletions design/01-module-reorganization.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# 01 — Module reorganization

**Status:** Foundation step (behavior-preserving move)
**Depends on:** nothing
**Next:** [02-configuration-objects.md](02-configuration-objects.md)

## Problem

On `main`, hook machinery is concentrated in:

| File | Contents today |
|------|----------------|
| [`src/pluggy/_hooks.py`](../src/pluggy/_hooks.py) | TypedDicts, markers, callers, HookImpl, HookSpec, helpers |
| [`src/pluggy/_callers.py`](../src/pluggy/_callers.py) | `_multicall`, old-style wrapper adapter |

Later steps (Protocols, CompletionHook, typed impls) need clear module
boundaries. try-claude already performed this split.

## Goals

- Split by role into focused modules (try-claude layout, clearer names OK).
- Preserve import paths via re-exports from `_hooks.py` / shims.
- Zero behavior change in this step.

## Non-goals

- New types, CompletionHook, Protocol callers (docs 02–05).
- Copying anything from `reiterate-claude`.

## Target design (chosen)

Cross-check summary:

| Branch | Layout | Notes |
|--------|--------|-------|
| `try-claude` | `_hook_config`, `_hook_markers`, `_hook_callers` (callers+impls), `_callers` | Good role split; `_hook_callers` too large; `_hook_*` + `_callers` naming clash |
| `reiterate-claude` | `_config`, `_decorators`, `_caller`, `_implementation`, `_execution` | Cleaner names; **do not copy its logic** |
| **This step** | reiterate **names** + current `main` **content** | Move-only |

```text
src/pluggy/
_config.py # HookspecOpts, HookimplOpts, normalize_hookimpl_opts
_decorators.py # markers, HookSpec, varnames
_caller.py # HookCaller, HookRelay, _SubsetHookCaller
_implementation.py # HookImpl + _Plugin / _HookImplFunction
_execution.py # _multicall + wrapper helpers
_hooks.py # re-exports (compat)
_callers.py # thin re-export of _execution (compat)
```

Name mapping from try-claude: see [DECISIONS.md](DECISIONS.md) D1. For this
step, move **current `main` code** only — do not yet port try-claude’s new
types/Protocols.

## Reference branch / files

```bash
# Layout inspiration only — content for step 01 is current main
git show try-claude:src/pluggy/_hook_config.py
git show try-claude:src/pluggy/_hook_markers.py
git show try-claude:src/pluggy/_hook_callers.py
git show try-claude:src/pluggy/_callers.py
git show try-claude:src/pluggy/_hooks.py
```

## Implementation steps

1. Create modules; cut-and-paste from `main`.
2. Fix relative imports; avoid cycles (`TYPE_CHECKING` where needed).
3. `_hooks.py` re-exports prior public/internal names.
4. Thin-shim or delete `_callers.py` after updating internal imports to
`_execution`.
5. Verify:

```bash
uv run pytest
uv run pre-commit run -a
```

Commit message:

```text
chore(structure): split hook modules into _config, _decorators, _caller, _implementation, _execution
```

## Public API / back-compat

- `from pluggy import ...` unchanged.
- `from pluggy._hooks import ...` unchanged via re-exports.

## Tests

- No new semantic tests; suite must stay green.

## Done when

- [ ] Role modules exist; `_hooks.py` is re-export layer.
- [ ] Move-only diff (no intentional behavior change).
- [ ] pytest + pre-commit green.
149 changes: 149 additions & 0 deletions design/02-configuration-objects.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
# 02 — Configuration objects (TypedDicts removed)

**Status:** Replace live options encoding
**Depends on:** [01-module-reorganization.md](01-module-reorganization.md)
**Next:** [03-markers-attach-config.md](03-markers-attach-config.md)

## Problem

On `main`, options are `HookspecOpts` / `HookimplOpts` TypedDicts — unvalidated
dicts copied around as the live representation. try-claude introduced proper
configuration classes; those are the API.

## Goals

- Add `HookspecConfiguration` and `HookimplConfiguration` (`__slots__`,
`Final`, validation).
- **Remove TypedDicts from the live and public API surface.**
- Provide a **narrow pytest support shim** that accepts mapping/dict-shaped
options and converts them to configuration objects (pytest migration only).
- Export configuration classes from `pluggy`.

## Non-goals

- Keeping TypedDicts “for compatibility” as a dual API ([DECISIONS.md](DECISIONS.md) D4).
- Marker attachment (doc 03) — can land in the same PR if cleaner, but
conceptually next.
- `create_hookimpl` body (needs NormalImpl/WrapperImpl — doc 04); may stub
or add method signature with a TODO.

## Target design

```python
# src/pluggy/_config.py

class HookspecConfiguration:
__slots__ = ("firstresult", "historic", "warn_on_impl", "warn_on_impl_args")
firstresult: Final[bool]
historic: Final[bool]
warn_on_impl: Final[Warning | None]
warn_on_impl_args: Final[Mapping[str, Warning] | None]

def __init__(self, firstresult=False, historic=False, ...):
if historic and firstresult:
raise ValueError("cannot have a historic firstresult hook")
...

class HookimplConfiguration:
__slots__ = ("wrapper", "hookwrapper", "optionalhook",
"tryfirst", "trylast", "specname")
...
# create_hookimpl added in doc 04
```

Pytest support shim (name flexible — e.g. `_pytest_compat.py` or helpers in
`_config.py` marked clearly):

```python
def hookspec_config_from_mapping(opts: Mapping[str, Any]) -> HookspecConfiguration:
"""Pytest/support only: accept legacy dict-shaped options."""
return HookspecConfiguration(
firstresult=opts.get("firstresult", False),
historic=opts.get("historic", False),
warn_on_impl=opts.get("warn_on_impl"),
warn_on_impl_args=opts.get("warn_on_impl_args"),
)

def hookimpl_config_from_mapping(opts: Mapping[str, Any]) -> HookimplConfiguration:
...
```

Do **not** keep `TypedDict` classes in `__all__`. Do not document dicts as
the options API. Markers take kwargs that construct configuration objects
directly (doc 03).

## Reference branch / files

```bash
git show try-claude:src/pluggy/_hook_config.py
```

Port classes from try-claude; then **delete** the TypedDict definitions from
the live module (try still kept them — we go further per D4). Isolate mapping
helpers as the only dict-facing surface.

## Implementation steps

### Step 2.1 — Add configuration classes

Port from try-claude `_hook_config.py` into `_config.py`.

### Step 2.2 — Remove TypedDicts from live path

1. Delete `HookspecOpts` / `HookimplOpts` TypedDict definitions (or move to a
private pytest-shim module if pytest tests in-tree still need the names
during transition — prefer deletion + mapping helpers).
2. Update all internal imports/usages to configuration classes.
3. Update `__init__.py` exports: add configuration classes; drop TypedDict
exports.

### Step 2.3 — Pytest support shim

1. Add `hookspec_config_from_mapping` / `hookimpl_config_from_mapping`.
2. Document in docstring: for pytest/support migration only, not the public
options API.
3. If pytest (downstream) needs a temporary import path, keep it private
(`pluggy._config` or `pluggy._pytest_compat`), not in `__all__`.

### Step 2.4 — Tests

- Validation (`historic` + `firstresult`).
- Mapping shim round-trip.
- No remaining tests that treat TypedDicts as the primary API.

```bash
uv run pytest
uv run pre-commit run -a
```

Commit message:

```text
feat(config): replace TypedDict options with Hook*Configuration classes
```

## Public API / back-compat

| Before | After |
|--------|-------|
| `HookspecOpts` / `HookimplOpts` TypedDicts | **Removed** from public API |
| dict attrs on marked functions | configuration objects (doc 03) |
| — | `HookspecConfiguration` / `HookimplConfiguration` exported |
| — | private mapping shim for pytest support |

Marker **kwargs** (`firstresult=`, `wrapper=`, …) stay — they construct
objects, they are not TypedDict literals.

## Tests to add/update

| File | Coverage |
|------|----------|
| `testing/test_configuration.py` | Class validation; mapping shim |
| Anything importing TypedDicts | Migrate or delete |

## Done when

- [ ] Configuration classes are the only live options encoding.
- [ ] TypedDicts gone from public `__all__` / docs path.
- [ ] Pytest mapping shim exists and is clearly non-API.
- [ ] pytest + pre-commit green.
Loading
Loading