Metadata-Version: 2.5
Name: hook-atlas
Version: 0.2.0
Summary: Trace and draw the hook execution flow of any pluggy-based application
Project-URL: Source, https://github.com/zy1o/hook-atlas
Project-URL: Issues, https://github.com/zy1o/hook-atlas/issues
Author-email: Andrzej Ostrowski <a.ostrowski@outlook.com>
License-Expression: MIT
License-File: LICENSE
Keywords: documentation,hooks,pluggy,tracing,visualisation
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Software Development :: Documentation
Classifier: Topic :: Software Development :: Libraries
Requires-Python: >=3.11
Requires-Dist: graphviz>=0.20
Requires-Dist: packaging>=23
Requires-Dist: pluggy>=1.0
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: ruff>=0.6; extra == 'dev'
Provides-Extra: docs
Requires-Dist: mkdocs-material<10,>=9.5; extra == 'docs'
Requires-Dist: mkdocs<2,>=1.6; extra == 'docs'
Requires-Dist: pymdown-extensions>=10.8; extra == 'docs'
Description-Content-Type: text/markdown

# hook-atlas

Trace and draw the hook execution flow of any [pluggy](https://pluggy.readthedocs.io/)-based
application — pytest, tox, devpi, datasette, or something nobody has written yet.

pluggy powers a lot of Python's plugin systems, and every one of them has the
same problem: the order hooks run in, what nests inside what, and which plugin
actually supplied each implementation are all facts about a *run*, not about the
documentation. This library captures them from a real run and draws the result.

## Status

First release, `0.1.0`. Early but not raw: the library and its
`trace`/`draw`/`check` commands work, and CI runs them against
tox, datasette, devpi-client and pytest on every change - including tracing
pytest while it runs datasette's own test suite - so "it works on things nobody
described to it" is checked rather than hoped for.

Expect the interfaces to move while the version starts with a zero. What is
unlikely to move is the trace format and the shape of a config, both of which
something else already depends on. The first consumer is
[pytest-hook-atlas](https://github.com/zy1o/pytest-hook-atlas), which publishes
[an atlas of pytest's hooks](https://zy1o.github.io/pytest-hook-atlas/).

## See your own project's hook flow

Three commands. Nothing to configure, and nothing to set up first.

```bash
pip install hook-atlas

# 1. run whatever you normally run, with the tracer watching
hook-atlas trace -- pytest -q tests/

# 2. draw it
hook-atlas draw

# 3. open hook-flow.html
```

That is the whole thing. Step 1 writes `hook-atlas-trace.json` beside you and
prints where; step 2 turns it into a standalone `hook-flow.html` you can open in
a browser, with the diagram inline and no site, server or stylesheet needed.

Everything after the `--` is your command, untouched. Your flags, your plugins,
your `conftest.py`, your exit code:

```bash
hook-atlas trace -- pytest -q -k "not slow" --maxfail=2
hook-atlas trace -- tox r -e py312
hook-atlas trace -- datasette serve mydata.db     # ctrl-c: the trace is still written
```

A second `--` belongs to your command, not to us, so `tox r -e py -- --lf`
arrives at tox whole.

### Options worth knowing

```bash
hook-atlas trace -o run.json -- pytest -q   # name the trace
hook-atlas draw run.json -o flow.svg        # bare SVG instead of a page
hook-atlas check run.json                   # is the trace sound?
```

`check` is worth running if something looks wrong: it reports a tracer that lost
its place, a tree whose counts do not add up, absolute paths baked into plugin
names, and anything that fails to draw.

### What you get, and what you do not

The diagram is **one flow of the whole run**, because this tool has not been
told what your application's phases are. Every hook it saw, in the order it was
called, with nesting - and for a large test suite that is a very tall picture.
That is the honest default rather than a guess.

Hooks render unlinked unless the application's documentation is described to the
tool, because a guessed URL is a dead link.

If you want the run sliced into named phases with hooks linked to their
documentation, that is what a wrapper supplies -
[pytest-hook-atlas](https://github.com/zy1o/pytest-hook-atlas) does it for
pytest, in about a hundred lines of configuration.

### If nothing gets traced

```
hook-atlas: pytest created no plugin manager, so there was nothing to trace
```

means the command answered before building one - `pytest --version` does - or
that the program is not built on pluggy at all. Try the command you actually
run, rather than `--help` or `--version`.

## How it works

pluggy has offered hook monitoring for years —
`PluginManager.add_hookcall_monitoring(before, after)` is public API. What it has
never offered is a way to *reach a manager somebody else built*: there is no
registry of live managers, and no notification when one appears.

So `watch()` wraps `PluginManager.__init__`. Every manager constructed after
that point is recorded, which means the one rule that matters is an ordering
one: **`watch()` has to run before the application builds its manager**, and in
practice that means before the application is imported. `hook-atlas trace` does
exactly that — patch, then import and run your command.

Recording from the constructor turns out to be earlier than any plugin can
manage, which is why the trace includes hooks that fire while plugins are still
being loaded.

## Driving it from Python

Two cases, and they are different.

**Tracing a program you are not modifying** — the CLI is this, and calling it
directly is usually easier than reproducing it:

```python
from hook_atlas.cli import run

exit_code = run(["pytest", "-q", "tests/"])   # patches, then imports and runs
```

**An application tracing itself**, where you already hold the manager and no
patching is needed:

```python
import pluggy

from hook_atlas import tracer

pm = pluggy.PluginManager("myapp")
tracer.attach(pm)        # record every hook call from here on
...
tracer.write_trace()     # JSON: the call tree, in order, with provenance
```

`attach` returns `None` if a manager is already being recorded — two interleaved
into one tree would be nonsense, and applications that build several are common
enough to matter.

## What a trace gives you

The call tree with nesting and ordering preserved, every implementation
attributed to the plugin that supplied it in pluggy's real call order, and
hookspec semantics (`firstresult`, `historic`) read from the live manager rather
than guessed. Diagrams are drawn from that; the JSON is the more useful half if
you want to answer something no view covers.

The program being traced is left alone — its stdout, its exit code, its
behaviour. A wrapper that turned a failing build green would be worse than no
wrapper, so that is a test.

## Configuring it for an application

Nothing is required. With no configuration an application's whole run is drawn
as a single flow, hooks render unlinked, and phases are not mentioned — which is
the honest output for an application nobody has described.

Describing one means supplying `Phase` objects to slice the run into diagrams,
and a `DocLinks` saying where its hook documentation lives. pytest's four phases
— startup, collection, the run-test protocol, session finish — live in
`pytest-hook-atlas`, not here.

## Licence

MIT.
