Skip to content
Draft
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
1 change: 1 addition & 0 deletions .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 Down
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.
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.
102 changes: 102 additions & 0 deletions design/03-markers-attach-config.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# 03 — Markers attach configuration objects

**Status:** Markers emit new types only
**Depends on:** [02-configuration-objects.md](02-configuration-objects.md)
**Next:** [04-hookimpl-wrapper-types.md](04-hookimpl-wrapper-types.md)
**Also unlocks:** [06-project-spec.md](06-project-spec.md)

## Problem

Markers on `main` attach dicts. After doc 02, configuration classes exist;
markers and discovery must use them exclusively.

## Goals

- `HookspecMarker` / `HookimplMarker` attach `HookspecConfiguration` /
`HookimplConfiguration` as `<project>_spec` / `<project>_impl`.
- `HookSpec` stores `config: HookspecConfiguration`.
- Manager parsing reads configuration objects only (no dict branch except
calling the pytest mapping shim if an explicit compat path is required).

## Non-goals

- ProjectSpec on markers (doc 06) — optional to combine.
- Impl subclass factory (doc 04).

## Target design

```python
def setattr_hookspec_opts(func: _F) -> _F:
config = HookspecConfiguration(
firstresult=firstresult,
historic=historic,
warn_on_impl=warn_on_impl,
warn_on_impl_args=warn_on_impl_args,
)
setattr(func, self.project_name + "_spec", config)
return func


def setattr_hookimpl_opts(func: _F) -> _F:
config = HookimplConfiguration(...)
setattr(func, self.project_name + "_impl", config)
return func
```

Decorator kwargs unchanged. Attribute **value type** is configuration object.

```mermaid
sequenceDiagram
participant Dev
participant Marker as HookimplMarker
participant Func as function
participant PM as PluginManager
Dev->>Marker: "@hookimpl(tryfirst=True)"
Marker->>Func: "project_impl = HookimplConfiguration"
Dev->>PM: register(plugin)
PM->>Func: getattr config
Note over PM: no dict path
```

## Reference branch / files

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

## Implementation steps

1. Update markers to construct/attach configuration objects.
2. Update `HookSpec` and manager discovery to use `.firstresult` etc.
3. Remove any `normalize_hookimpl_opts` dict mutation on the live path.
4. Tests: attached attr is configuration instance; historic+firstresult
raises at decoration time.

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

Commit message:

```text
feat(decorators): attach Hook*Configuration objects on marked functions
```

## Public API / back-compat

- Decorator kwargs stable.
- Private attribute payload is now a configuration object (not a dict).
- Callers that poked dict attrs must use the pytest mapping shim or migrate.

## Tests

| File | Coverage |
|------|----------|
| `testing/test_configuration.py` | Attachment + validation |
| Registration / marker tests | Adapt off dict assumptions |

## Done when

- [ ] No marker or manager live path attaches/reads TypedDicts.
- [ ] Suite green.
Loading