pytest-digline 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pytest_digline-0.1.0/.gitignore +45 -0
- pytest_digline-0.1.0/PKG-INFO +124 -0
- pytest_digline-0.1.0/README.md +101 -0
- pytest_digline-0.1.0/pyproject.toml +57 -0
- pytest_digline-0.1.0/src/pytest_digline/__init__.py +15 -0
- pytest_digline-0.1.0/src/pytest_digline/plugin.py +553 -0
- pytest_digline-0.1.0/tests/_cycle.py +40 -0
- pytest_digline-0.1.0/tests/conftest.py +101 -0
- pytest_digline-0.1.0/tests/test_contract.py +118 -0
- pytest_digline-0.1.0/tests/test_cost.py +118 -0
- pytest_digline-0.1.0/tests/test_inert.py +70 -0
- pytest_digline-0.1.0/tests/test_message.py +143 -0
- pytest_digline-0.1.0/tests/test_no_promote.py +82 -0
- pytest_digline-0.1.0/tests/test_states.py +129 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Python bytecode and build output
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
build/
|
|
5
|
+
dist/
|
|
6
|
+
*.egg-info/
|
|
7
|
+
|
|
8
|
+
# Environment. Recreated by `uv sync`; it pins absolute paths, so it must
|
|
9
|
+
# never be committed.
|
|
10
|
+
.venv/
|
|
11
|
+
.env
|
|
12
|
+
|
|
13
|
+
# Tool caches. Each already drops its own `.gitignore`; listed here so a
|
|
14
|
+
# fresh clone is clean before the tools have run once.
|
|
15
|
+
.pytest_cache/
|
|
16
|
+
.ruff_cache/
|
|
17
|
+
.mypy_cache/
|
|
18
|
+
|
|
19
|
+
# IDE. Excluded because the project files carry machine-specific SDK paths
|
|
20
|
+
# (`digline.iml` names the interpreter by absolute path). Drop these two lines
|
|
21
|
+
# to version the shared part, and keep ignoring `.idea/workspace.xml`.
|
|
22
|
+
.idea/
|
|
23
|
+
*.iml
|
|
24
|
+
|
|
25
|
+
# Local Claude Code settings. `.claude/settings.json`, if it appears, is shared
|
|
26
|
+
# and stays versioned. `CLAUDE.local.md` is the personal working agreement —
|
|
27
|
+
# how I want to be worked with — as against `CLAUDE.md`, which is the project.
|
|
28
|
+
.claude/settings.local.json
|
|
29
|
+
CLAUDE.local.md
|
|
30
|
+
|
|
31
|
+
# macOS
|
|
32
|
+
.DS_Store
|
|
33
|
+
|
|
34
|
+
# Working material that stays local and is not part of the package.
|
|
35
|
+
private/
|
|
36
|
+
|
|
37
|
+
# NOT ignored: `.digline/`. Decision 2 — baselines are versioned, run
|
|
38
|
+
# artifacts are not — and the split is enforced one level down, by the
|
|
39
|
+
# `.gitignore` the store itself writes into `.digline/` (`*/runs/`).
|
|
40
|
+
# Ignoring `.digline/` here would take the baselines out of git with it.
|
|
41
|
+
to-publish/
|
|
42
|
+
|
|
43
|
+
# The site checkout that ci.yml's `docs` job makes, and that RELEASING
|
|
44
|
+
# tells you to make to reproduce it. Never committed here.
|
|
45
|
+
_site/
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: pytest-digline
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Gate a pytest run on a digline comparison: one row per check, against the baseline committed in your repo.
|
|
5
|
+
Project-URL: Homepage, https://digline.dev/
|
|
6
|
+
Project-URL: Documentation, https://digline.dev/product/pytest/
|
|
7
|
+
Project-URL: Changelog, https://digline.dev/product/changelog/
|
|
8
|
+
Project-URL: Repository, https://github.com/digline/digline
|
|
9
|
+
Project-URL: Issues, https://github.com/digline/digline/issues
|
|
10
|
+
Author-email: Alessandro Prandini <alessandro.prandini@ict-group.it>
|
|
11
|
+
License-Expression: Apache-2.0
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Framework :: Pytest
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Topic :: Software Development :: Testing
|
|
18
|
+
Classifier: Typing :: Typed
|
|
19
|
+
Requires-Python: >=3.12
|
|
20
|
+
Requires-Dist: digline>=0.9.0
|
|
21
|
+
Requires-Dist: pytest>=8.0
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# pytest-digline
|
|
25
|
+
|
|
26
|
+
The digline comparison, as rows in pytest's own report.
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
pip install pytest-digline
|
|
30
|
+
pytest --digline-suite eval/suite.py
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
digline: comparing 1 suite(s) against the committed baseline
|
|
35
|
+
|
|
36
|
+
eval/suite.py::alpha::contains .
|
|
37
|
+
eval/suite.py::alpha::llm_rubric F
|
|
38
|
+
eval/suite.py::beta::contains .
|
|
39
|
+
eval/suite.py::gamma s
|
|
40
|
+
|
|
41
|
+
=================================== FAILURES ===================================
|
|
42
|
+
digline: alpha · llm_rubric
|
|
43
|
+
dropped from 0.910000 to 0.640000, below its threshold of 0.700000,
|
|
44
|
+
and beyond the 0.880000–0.950000 this check measured across 5 samples
|
|
45
|
+
reason: signed=True, concise=False
|
|
46
|
+
|
|
47
|
+
==================================== digline ===================================
|
|
48
|
+
1 check got worse; every case could be judged; 1 case is suspended; the rules
|
|
49
|
+
are unchanged.
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
One row per **check** — one assertion on one case — because that is digline's
|
|
53
|
+
unit of verdict. `-k` and `--last-failed` select them like any other test.
|
|
54
|
+
|
|
55
|
+
## What it does, and what it costs
|
|
56
|
+
|
|
57
|
+
By default it **compares and never runs**: it reads the latest stored run of
|
|
58
|
+
each suite, holds it against the baseline committed in your repository, and
|
|
59
|
+
makes no network call at all. Producing a run stays a separate act —
|
|
60
|
+
`digline run`, the official image, the GitHub Action.
|
|
61
|
+
|
|
62
|
+
`--digline-run` runs each suite first. That calls your provider and spends
|
|
63
|
+
money, so it is a flag on the command line where the cost is visible, it prints
|
|
64
|
+
the planned call count before the first call, and it **refuses under
|
|
65
|
+
`--collect-only`**: a command whose job is to list test names must never be able
|
|
66
|
+
to spend a hundred model calls.
|
|
67
|
+
|
|
68
|
+
## The four states
|
|
69
|
+
|
|
70
|
+
| digline | pytest |
|
|
71
|
+
|---|---|
|
|
72
|
+
| fine | passed |
|
|
73
|
+
| got worse | **FAILED** |
|
|
74
|
+
| could not be judged | **ERROR** — an error is neither green nor a regression |
|
|
75
|
+
| suspended | **SKIPPED**, with the reason the suite declared |
|
|
76
|
+
|
|
77
|
+
The suspension is the one digline state an exit code cannot express: it never
|
|
78
|
+
fails, so it disappears into `0`. pytest has had a state for *a decision rather
|
|
79
|
+
than an outcome* since it was written, and this is it.
|
|
80
|
+
|
|
81
|
+
**What pytest cannot carry**: the process exit code. `digline compare` exits `1`
|
|
82
|
+
for a regression and `2` for a run it could not judge; a pytest run exits `1`
|
|
83
|
+
for either. The distinction survives in the report — `F` and `E` are counted
|
|
84
|
+
separately, `-rE` lists the errored rows — and in `--junit-xml`. A job that
|
|
85
|
+
needs `1` versus `2` runs `digline compare`, which is one command away.
|
|
86
|
+
|
|
87
|
+
## Naming a suite
|
|
88
|
+
|
|
89
|
+
Nothing is discovered by convention: a suite is a file that *executes*, and a
|
|
90
|
+
file found by convention is a file that runs by accident. Name it, either way:
|
|
91
|
+
|
|
92
|
+
```sh
|
|
93
|
+
pytest --digline-suite eval/suite.py --digline-suite eval/billing.toml
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
```ini
|
|
97
|
+
# pyproject.toml
|
|
98
|
+
[tool.pytest.ini_options]
|
|
99
|
+
digline_suites = ["eval/suite.py"]
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
The command line replaces the ini list rather than adding to it, so narrowing a
|
|
103
|
+
run to one suite is expressible. `--digline-root` names the perimeter holding
|
|
104
|
+
`.digline/` and defaults to pytest's rootdir.
|
|
105
|
+
|
|
106
|
+
Select rows with `-k`; a node id passed as an argument is not supported.
|
|
107
|
+
|
|
108
|
+
## What it will not do
|
|
109
|
+
|
|
110
|
+
**It does not promote.** There is no flag, no fixture and no marker that makes
|
|
111
|
+
a run the new baseline — not refused, *absent*, and a test in this package
|
|
112
|
+
sweeps the sources to keep it that way. A baseline is an approved reference;
|
|
113
|
+
the approval is a person's, and a green pytest run is the single most likely
|
|
114
|
+
place for a promotion to happen by accident.
|
|
115
|
+
|
|
116
|
+
**It has no thresholds of its own.** Every bar it reports against was declared
|
|
117
|
+
in your suite and frozen in the baseline somebody promoted and committed.
|
|
118
|
+
|
|
119
|
+
**It depends on digline and pytest, and nothing else.**
|
|
120
|
+
|
|
121
|
+
Turn it off for one run with `-p no:digline`. With no suite named it collects
|
|
122
|
+
nothing, prints nothing and adds no header.
|
|
123
|
+
|
|
124
|
+
The reasoning is [ADR 0013](https://digline.dev/product/adr/0013-the-pytest-plugin/).
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# pytest-digline
|
|
2
|
+
|
|
3
|
+
The digline comparison, as rows in pytest's own report.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
pip install pytest-digline
|
|
7
|
+
pytest --digline-suite eval/suite.py
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
digline: comparing 1 suite(s) against the committed baseline
|
|
12
|
+
|
|
13
|
+
eval/suite.py::alpha::contains .
|
|
14
|
+
eval/suite.py::alpha::llm_rubric F
|
|
15
|
+
eval/suite.py::beta::contains .
|
|
16
|
+
eval/suite.py::gamma s
|
|
17
|
+
|
|
18
|
+
=================================== FAILURES ===================================
|
|
19
|
+
digline: alpha · llm_rubric
|
|
20
|
+
dropped from 0.910000 to 0.640000, below its threshold of 0.700000,
|
|
21
|
+
and beyond the 0.880000–0.950000 this check measured across 5 samples
|
|
22
|
+
reason: signed=True, concise=False
|
|
23
|
+
|
|
24
|
+
==================================== digline ===================================
|
|
25
|
+
1 check got worse; every case could be judged; 1 case is suspended; the rules
|
|
26
|
+
are unchanged.
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
One row per **check** — one assertion on one case — because that is digline's
|
|
30
|
+
unit of verdict. `-k` and `--last-failed` select them like any other test.
|
|
31
|
+
|
|
32
|
+
## What it does, and what it costs
|
|
33
|
+
|
|
34
|
+
By default it **compares and never runs**: it reads the latest stored run of
|
|
35
|
+
each suite, holds it against the baseline committed in your repository, and
|
|
36
|
+
makes no network call at all. Producing a run stays a separate act —
|
|
37
|
+
`digline run`, the official image, the GitHub Action.
|
|
38
|
+
|
|
39
|
+
`--digline-run` runs each suite first. That calls your provider and spends
|
|
40
|
+
money, so it is a flag on the command line where the cost is visible, it prints
|
|
41
|
+
the planned call count before the first call, and it **refuses under
|
|
42
|
+
`--collect-only`**: a command whose job is to list test names must never be able
|
|
43
|
+
to spend a hundred model calls.
|
|
44
|
+
|
|
45
|
+
## The four states
|
|
46
|
+
|
|
47
|
+
| digline | pytest |
|
|
48
|
+
|---|---|
|
|
49
|
+
| fine | passed |
|
|
50
|
+
| got worse | **FAILED** |
|
|
51
|
+
| could not be judged | **ERROR** — an error is neither green nor a regression |
|
|
52
|
+
| suspended | **SKIPPED**, with the reason the suite declared |
|
|
53
|
+
|
|
54
|
+
The suspension is the one digline state an exit code cannot express: it never
|
|
55
|
+
fails, so it disappears into `0`. pytest has had a state for *a decision rather
|
|
56
|
+
than an outcome* since it was written, and this is it.
|
|
57
|
+
|
|
58
|
+
**What pytest cannot carry**: the process exit code. `digline compare` exits `1`
|
|
59
|
+
for a regression and `2` for a run it could not judge; a pytest run exits `1`
|
|
60
|
+
for either. The distinction survives in the report — `F` and `E` are counted
|
|
61
|
+
separately, `-rE` lists the errored rows — and in `--junit-xml`. A job that
|
|
62
|
+
needs `1` versus `2` runs `digline compare`, which is one command away.
|
|
63
|
+
|
|
64
|
+
## Naming a suite
|
|
65
|
+
|
|
66
|
+
Nothing is discovered by convention: a suite is a file that *executes*, and a
|
|
67
|
+
file found by convention is a file that runs by accident. Name it, either way:
|
|
68
|
+
|
|
69
|
+
```sh
|
|
70
|
+
pytest --digline-suite eval/suite.py --digline-suite eval/billing.toml
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
```ini
|
|
74
|
+
# pyproject.toml
|
|
75
|
+
[tool.pytest.ini_options]
|
|
76
|
+
digline_suites = ["eval/suite.py"]
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
The command line replaces the ini list rather than adding to it, so narrowing a
|
|
80
|
+
run to one suite is expressible. `--digline-root` names the perimeter holding
|
|
81
|
+
`.digline/` and defaults to pytest's rootdir.
|
|
82
|
+
|
|
83
|
+
Select rows with `-k`; a node id passed as an argument is not supported.
|
|
84
|
+
|
|
85
|
+
## What it will not do
|
|
86
|
+
|
|
87
|
+
**It does not promote.** There is no flag, no fixture and no marker that makes
|
|
88
|
+
a run the new baseline — not refused, *absent*, and a test in this package
|
|
89
|
+
sweeps the sources to keep it that way. A baseline is an approved reference;
|
|
90
|
+
the approval is a person's, and a green pytest run is the single most likely
|
|
91
|
+
place for a promotion to happen by accident.
|
|
92
|
+
|
|
93
|
+
**It has no thresholds of its own.** Every bar it reports against was declared
|
|
94
|
+
in your suite and frozen in the baseline somebody promoted and committed.
|
|
95
|
+
|
|
96
|
+
**It depends on digline and pytest, and nothing else.**
|
|
97
|
+
|
|
98
|
+
Turn it off for one run with `-p no:digline`. With no suite named it collects
|
|
99
|
+
nothing, prints nothing and adds no header.
|
|
100
|
+
|
|
101
|
+
The reasoning is [ADR 0013](https://digline.dev/product/adr/0013-the-pytest-plugin/).
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "pytest-digline"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Gate a pytest run on a digline comparison: one row per check, against the baseline committed in your repo."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.12"
|
|
7
|
+
license = "Apache-2.0"
|
|
8
|
+
authors = [
|
|
9
|
+
{ name = "Alessandro Prandini", email = "alessandro.prandini@ict-group.it" },
|
|
10
|
+
]
|
|
11
|
+
classifiers = [
|
|
12
|
+
"Development Status :: 3 - Alpha",
|
|
13
|
+
"Framework :: Pytest",
|
|
14
|
+
"Intended Audience :: Developers",
|
|
15
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
16
|
+
"Programming Language :: Python :: 3.12",
|
|
17
|
+
"Topic :: Software Development :: Testing",
|
|
18
|
+
"Typing :: Typed",
|
|
19
|
+
]
|
|
20
|
+
dependencies = [
|
|
21
|
+
# 0.9.0 is the release that first publishes `report.check_line`, the
|
|
22
|
+
# per-delta sentence this package prints on a failing row (ADR 0013 §6).
|
|
23
|
+
# Held there by `tests/test_plugin_floors.py`, which computes the floor
|
|
24
|
+
# from the newest name these sources import rather than taking it on trust.
|
|
25
|
+
"digline>=0.9.0",
|
|
26
|
+
# A floor and no cap, unlike `digline-mcp`'s `mcp<3`. That cap records a
|
|
27
|
+
# rename that actually happened; pytest has not moved the `pytest11` entry
|
|
28
|
+
# point in a decade, and a cap here would fight the one dependency the user
|
|
29
|
+
# certainly already has. 8.0 is where `@pytest.hookimpl(wrapper=True)` — the
|
|
30
|
+
# hook that keeps an ERROR row free of a traceback — became available.
|
|
31
|
+
"pytest>=8.0",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
# The same storefront as `digline` itself: a plugin's PyPI page is where
|
|
35
|
+
# somebody meets it first, so it points at the same site and the same repo
|
|
36
|
+
# rather than being a page of no links at all.
|
|
37
|
+
[project.urls]
|
|
38
|
+
Homepage = "https://digline.dev/"
|
|
39
|
+
Documentation = "https://digline.dev/product/pytest/"
|
|
40
|
+
Changelog = "https://digline.dev/product/changelog/"
|
|
41
|
+
Repository = "https://github.com/digline/digline"
|
|
42
|
+
Issues = "https://github.com/digline/digline/issues"
|
|
43
|
+
|
|
44
|
+
# Named `digline` and not `pytest_digline`: this is the name a user types to
|
|
45
|
+
# switch it off — `pytest -p no:digline` — and it is what the header prints.
|
|
46
|
+
[project.entry-points.pytest11]
|
|
47
|
+
digline = "pytest_digline.plugin"
|
|
48
|
+
|
|
49
|
+
[build-system]
|
|
50
|
+
requires = ["hatchling>=1.27"]
|
|
51
|
+
build-backend = "hatchling.build"
|
|
52
|
+
|
|
53
|
+
[tool.hatch.build.targets.wheel]
|
|
54
|
+
packages = ["src/pytest_digline"]
|
|
55
|
+
|
|
56
|
+
[tool.uv.sources]
|
|
57
|
+
digline = { workspace = true }
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""`pytest-digline`: the comparison, as rows in pytest's own report.
|
|
2
|
+
|
|
3
|
+
The plugin lives in `pytest_digline.plugin`, which is what the `pytest11` entry
|
|
4
|
+
point names. Nothing is exported here: a pytest plugin is not a library, and a
|
|
5
|
+
name importable from the package root is a surface somebody would write against.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from importlib.metadata import version as _distribution_version
|
|
9
|
+
|
|
10
|
+
# Read from the installed distribution rather than written here, for the reason
|
|
11
|
+
# `digline.__version__` is: a hand-written copy is neither derived nor gated,
|
|
12
|
+
# and `tests/test_versions.py` sweeps these sources for exactly that.
|
|
13
|
+
__version__ = _distribution_version("pytest-digline")
|
|
14
|
+
|
|
15
|
+
__all__ = ["__version__"]
|