pytest-digline 0.2.3__tar.gz → 0.2.4__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.2.3 → pytest_digline-0.2.4}/.gitignore +9 -0
- {pytest_digline-0.2.3 → pytest_digline-0.2.4}/PKG-INFO +3 -4
- {pytest_digline-0.2.3 → pytest_digline-0.2.4}/README.md +1 -2
- {pytest_digline-0.2.3 → pytest_digline-0.2.4}/pyproject.toml +9 -2
- {pytest_digline-0.2.3 → pytest_digline-0.2.4}/src/pytest_digline/plugin.py +7 -1
- pytest_digline-0.2.4/tests/test_digline_run_notes.py +103 -0
- pytest_digline-0.2.4/tests/test_plugin_one_declared_path.py +93 -0
- {pytest_digline-0.2.3 → pytest_digline-0.2.4}/src/pytest_digline/__init__.py +0 -0
- {pytest_digline-0.2.3 → pytest_digline-0.2.4}/src/pytest_digline/py.typed +0 -0
- {pytest_digline-0.2.3 → pytest_digline-0.2.4}/tests/_cycle.py +0 -0
- {pytest_digline-0.2.3 → pytest_digline-0.2.4}/tests/conftest.py +0 -0
- {pytest_digline-0.2.3 → pytest_digline-0.2.4}/tests/test_calibration_rows.py +0 -0
- {pytest_digline-0.2.3 → pytest_digline-0.2.4}/tests/test_contract.py +0 -0
- {pytest_digline-0.2.3 → pytest_digline-0.2.4}/tests/test_cost.py +0 -0
- {pytest_digline-0.2.3 → pytest_digline-0.2.4}/tests/test_escapes.py +0 -0
- {pytest_digline-0.2.3 → pytest_digline-0.2.4}/tests/test_exits.py +0 -0
- {pytest_digline-0.2.3 → pytest_digline-0.2.4}/tests/test_incomparable_rows.py +0 -0
- {pytest_digline-0.2.3 → pytest_digline-0.2.4}/tests/test_inert.py +0 -0
- {pytest_digline-0.2.3 → pytest_digline-0.2.4}/tests/test_message.py +0 -0
- {pytest_digline-0.2.3 → pytest_digline-0.2.4}/tests/test_no_promote.py +0 -0
- {pytest_digline-0.2.3 → pytest_digline-0.2.4}/tests/test_states.py +0 -0
- {pytest_digline-0.2.3 → pytest_digline-0.2.4}/tests/test_summary_note.py +0 -0
|
@@ -22,6 +22,9 @@ dist/
|
|
|
22
22
|
.idea/
|
|
23
23
|
*.iml
|
|
24
24
|
|
|
25
|
+
# VS Code. Not used today; ignored as prevention.
|
|
26
|
+
.vscode/
|
|
27
|
+
|
|
25
28
|
# Local Claude Code settings. `.claude/settings.json`, if it appears, is shared
|
|
26
29
|
# and stays versioned. `CLAUDE.local.md` is the personal working agreement —
|
|
27
30
|
# how I want to be worked with — as against `CLAUDE.md`, which is the project.
|
|
@@ -38,6 +41,12 @@ private/
|
|
|
38
41
|
# artifacts are not — and the split is enforced one level down, by the
|
|
39
42
|
# `.gitignore` the store itself writes into `.digline/` (`*/runs/`).
|
|
40
43
|
# Ignoring `.digline/` here would take the baselines out of git with it.
|
|
44
|
+
|
|
45
|
+
# Output of the release step, like dist/. .github/select_unpublished.py writes
|
|
46
|
+
# into to-publish/ the distributions PyPI and TestPyPI do not already have, and
|
|
47
|
+
# publish.yml uses it as packages-dir. It lands in the working copy of whoever
|
|
48
|
+
# runs that step by hand. The line arrived in b5870a6 without a reason beside
|
|
49
|
+
# it; the reason was read from the code and written here on 2026-10-08.
|
|
41
50
|
to-publish/
|
|
42
51
|
|
|
43
52
|
# The site checkout that ci.yml's `docs` job makes, and that RELEASING
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: pytest-digline
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
4
4
|
Summary: Gate a pytest run on a digline comparison: one row per check, against the baseline committed in your repo.
|
|
5
5
|
Project-URL: Homepage, https://digline.dev/
|
|
6
6
|
Project-URL: Documentation, https://digline.dev/product/pytest/
|
|
@@ -17,7 +17,7 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
17
17
|
Classifier: Topic :: Software Development :: Testing
|
|
18
18
|
Classifier: Typing :: Typed
|
|
19
19
|
Requires-Python: >=3.12
|
|
20
|
-
Requires-Dist: digline>=0.
|
|
20
|
+
Requires-Dist: digline>=0.31.0
|
|
21
21
|
Requires-Dist: pytest>=8.0
|
|
22
22
|
Description-Content-Type: text/markdown
|
|
23
23
|
|
|
@@ -40,8 +40,7 @@ eval/suite.py::gamma s
|
|
|
40
40
|
|
|
41
41
|
=================================== FAILURES ===================================
|
|
42
42
|
digline: alpha · llm_rubric
|
|
43
|
-
|
|
44
|
-
and beyond the 0.880000–0.950000 this check measured across 5 samples
|
|
43
|
+
Went from passing to failing (0.910000 → 0.640000).
|
|
45
44
|
reason: signed=True, concise=False
|
|
46
45
|
|
|
47
46
|
==================================== digline ===================================
|
|
@@ -17,8 +17,7 @@ eval/suite.py::gamma s
|
|
|
17
17
|
|
|
18
18
|
=================================== FAILURES ===================================
|
|
19
19
|
digline: alpha · llm_rubric
|
|
20
|
-
|
|
21
|
-
and beyond the 0.880000–0.950000 this check measured across 5 samples
|
|
20
|
+
Went from passing to failing (0.910000 → 0.640000).
|
|
22
21
|
reason: signed=True, concise=False
|
|
23
22
|
|
|
24
23
|
==================================== digline ===================================
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "pytest-digline"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.4"
|
|
4
4
|
description = "Gate a pytest run on a digline comparison: one row per check, against the baseline committed in your repo."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.12"
|
|
@@ -51,7 +51,14 @@ dependencies = [
|
|
|
51
51
|
# was owed at the cut and raised in this package's own release, after the
|
|
52
52
|
# core was on PyPI, because a floor may not name a release that does not
|
|
53
53
|
# exist yet.
|
|
54
|
-
|
|
54
|
+
#
|
|
55
|
+
# 0.31.0, a sixth time, by name: `--digline-run` imports `suite_notes`,
|
|
56
|
+
# which first ships in 0.31.0, so the lines `digline run` prints about the
|
|
57
|
+
# suite before the first call reach a pytest session too (#396, ADR 0024
|
|
58
|
+
# §6.4). Against 0.30.0 the import raises `ImportError`. It was owed at the
|
|
59
|
+
# cut and raised in this package's own release, after the core was on
|
|
60
|
+
# PyPI, because a floor may not name a release that does not exist yet.
|
|
61
|
+
"digline>=0.31.0",
|
|
55
62
|
# A floor and no cap, unlike `digline-mcp`'s `mcp<3`. That cap records a
|
|
56
63
|
# rename that actually happened; pytest has not moved the `pytest11` entry
|
|
57
64
|
# point in a decade, and a cap here would fight the one dependency the user
|
|
@@ -370,7 +370,7 @@ def _measure(
|
|
|
370
370
|
utc_now_iso,
|
|
371
371
|
)
|
|
372
372
|
from digline.report import visible
|
|
373
|
-
from digline.run import execute, planned_calls
|
|
373
|
+
from digline.run import execute, planned_calls, suite_notes
|
|
374
374
|
|
|
375
375
|
target = load_target(None, loaded, spec)
|
|
376
376
|
plan = planned_calls(suite)
|
|
@@ -380,6 +380,12 @@ def _measure(
|
|
|
380
380
|
# write left in any front end when the rule was widened past `digline.cli`.
|
|
381
381
|
# (from the release delta-pass over 0.15.0)
|
|
382
382
|
print(f"digline: {visible(plan.sentence())}", file=sys.stderr)
|
|
383
|
+
# Beside the count, as `digline run` says them: whoever writes a suite in
|
|
384
|
+
# pytest may never run the CLI, and is the one these lines are for. Until
|
|
385
|
+
# #396 the plugin said neither, under a rule against raising its floor for
|
|
386
|
+
# a line on stderr; that rule was withdrawn on 2026-10-07. (ADR 0024 §6.4)
|
|
387
|
+
for note in suite_notes(suite):
|
|
388
|
+
print(f"digline: {visible(note)}", file=sys.stderr)
|
|
383
389
|
commit = git_commit(root)
|
|
384
390
|
created_at = utc_now_iso()
|
|
385
391
|
artifacts = read_artifacts(suite, target, path.parent, root=root)
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""`--digline-run` says what `digline run` says about the suite. (#396)
|
|
2
|
+
|
|
3
|
+
Before the first call the CLI names a check whose class declares no `KIND` and
|
|
4
|
+
a tolerance that switches its check off. Whoever writes a suite in pytest may
|
|
5
|
+
never run the CLI, and is the one both lines are for. Until #396 the plugin
|
|
6
|
+
said neither, under a rule against raising its floor for a line on stderr; the
|
|
7
|
+
rule was withdrawn on 2026-10-07. (ADR 0024 §6.4)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import pytest
|
|
13
|
+
|
|
14
|
+
NOTED = """
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
|
|
17
|
+
from digline.core import (
|
|
18
|
+
TEXT_ONLY,
|
|
19
|
+
AssertionBase,
|
|
20
|
+
Contains,
|
|
21
|
+
EvaluatorInputs,
|
|
22
|
+
JudgeReply,
|
|
23
|
+
LlmRubric,
|
|
24
|
+
OutputKind,
|
|
25
|
+
Verdict,
|
|
26
|
+
)
|
|
27
|
+
from digline.run import Case, Response, Suite
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True, slots=True)
|
|
31
|
+
class NoKind(AssertionBase):
|
|
32
|
+
name: str = "no_kind"
|
|
33
|
+
threshold: float = 0.5
|
|
34
|
+
tolerance: float = 0.0
|
|
35
|
+
accepts: frozenset[OutputKind] = TEXT_ONLY
|
|
36
|
+
|
|
37
|
+
def __call__(self, inputs: EvaluatorInputs) -> Verdict:
|
|
38
|
+
return self._binary(True, "fine")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def judge(prompt):
|
|
42
|
+
return JudgeReply(score=1.0, reason="looked")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
suite = Suite(
|
|
46
|
+
tenant="acme",
|
|
47
|
+
environment="staging",
|
|
48
|
+
name="noted",
|
|
49
|
+
assertions=[
|
|
50
|
+
Contains(needle="Acme"),
|
|
51
|
+
NoKind(),
|
|
52
|
+
LlmRubric(rubric="ok?", judge=judge, threshold=0.5, tolerance=0.5,
|
|
53
|
+
name="loose"),
|
|
54
|
+
],
|
|
55
|
+
cases=[Case(id="alpha")],
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def target(case):
|
|
60
|
+
return Response(output="Acme", input="who?")
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_the_notes_follow_the_count_on_stderr(pytester: pytest.Pytester) -> None:
|
|
65
|
+
path = pytester.path / "suite.py"
|
|
66
|
+
path.write_text(NOTED, encoding="utf-8")
|
|
67
|
+
|
|
68
|
+
result = pytester.runpytest_subprocess(
|
|
69
|
+
"--digline-suite", str(path), "--digline-run"
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
# The order `digline run` prints: the count, the KIND note, each blind
|
|
73
|
+
# tolerance. The session itself ends on the missing baseline, which is not
|
|
74
|
+
# what is under test here.
|
|
75
|
+
result.stderr.fnmatch_lines(
|
|
76
|
+
[
|
|
77
|
+
"digline: tenant 'acme' * = 1 call to the target",
|
|
78
|
+
"digline: no_kind declares no KIND, so the shape reading leaves it out*",
|
|
79
|
+
"digline: tolerance 0.5 on 'loose' (threshold 0.5) covers every movement*",
|
|
80
|
+
],
|
|
81
|
+
consecutive=True,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_a_suite_with_nothing_to_say_says_only_the_count(
|
|
86
|
+
pytester: pytest.Pytester,
|
|
87
|
+
) -> None:
|
|
88
|
+
"""The control: a note on every run would be a line people learn to skip,
|
|
89
|
+
and would pass the test above."""
|
|
90
|
+
path = pytester.path / "suite.py"
|
|
91
|
+
plain = NOTED.replace(" NoKind(),\n", "").replace(
|
|
92
|
+
"tolerance=0.5", "tolerance=0.05"
|
|
93
|
+
)
|
|
94
|
+
path.write_text(plain, encoding="utf-8")
|
|
95
|
+
|
|
96
|
+
result = pytester.runpytest_subprocess(
|
|
97
|
+
"--digline-suite", str(path), "--digline-run"
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
text = result.stderr.str()
|
|
101
|
+
assert "= 1 call to the target" in text
|
|
102
|
+
assert "KIND" not in text
|
|
103
|
+
assert "covers every movement" not in text
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""ADR 0045 through pytest-digline: test plan entry 9, by its flag and its ini.
|
|
2
|
+
|
|
3
|
+
A real pytest run in a subprocess, started from the working directory under
|
|
4
|
+
test: `pytester` would start it from its own directory, and the directory is
|
|
5
|
+
what decides which prompt a bare relative path reads. The layout is #481's,
|
|
6
|
+
from `tests/_one_path.py`.
|
|
7
|
+
|
|
8
|
+
The run is not promoted, so a run that gets past the files under test ends on
|
|
9
|
+
"no baseline" (exit 4) after its run file is written. What these tests read is
|
|
10
|
+
that file and what the provider was sent, not the gate's verdict.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import os
|
|
16
|
+
import subprocess
|
|
17
|
+
import sys
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
import pytest
|
|
21
|
+
from tests._one_path import RELATIVE, layout, recorded, sent, sha
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _pytest(tmp_path: Path, cwd: str, how: str) -> subprocess.CompletedProcess[str]:
|
|
25
|
+
"""`--digline-suite eval/suite.py` from `R`, or the absolute suite from
|
|
26
|
+
elsewhere; or the `digline_suites` ini, which pytest makes absolute."""
|
|
27
|
+
root = tmp_path / "R"
|
|
28
|
+
args = [
|
|
29
|
+
sys.executable,
|
|
30
|
+
"-m",
|
|
31
|
+
"pytest",
|
|
32
|
+
"-p",
|
|
33
|
+
"no:cacheprovider",
|
|
34
|
+
"--digline-run",
|
|
35
|
+
"--digline-root",
|
|
36
|
+
str(root),
|
|
37
|
+
"--rootdir",
|
|
38
|
+
str(root),
|
|
39
|
+
]
|
|
40
|
+
if how == "ini":
|
|
41
|
+
(root / "pytest.ini").write_text(
|
|
42
|
+
"[pytest]\ndigline_suites =\n eval/suite.py\n", "utf-8"
|
|
43
|
+
)
|
|
44
|
+
args += ["-c", str(root / "pytest.ini")]
|
|
45
|
+
else:
|
|
46
|
+
spec = "eval/suite.py" if cwd == "R" else str(root / "eval" / "suite.py")
|
|
47
|
+
args += ["-c", os.devnull, "--digline-suite", spec]
|
|
48
|
+
return subprocess.run(
|
|
49
|
+
args, cwd=tmp_path / cwd, capture_output=True, text=True, check=False
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _runs(root: Path) -> list[Path]:
|
|
54
|
+
return list((root / ".digline").rglob("runs/*/*.json"))
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@pytest.mark.parametrize("how", ["flag", "ini"])
|
|
58
|
+
def test_1_from_the_root_the_run_records_the_file_it_sent(
|
|
59
|
+
tmp_path: Path, how: str
|
|
60
|
+
) -> None:
|
|
61
|
+
root = layout(tmp_path)
|
|
62
|
+
done = _pytest(tmp_path, "R", how)
|
|
63
|
+
assert "no baseline" in done.stdout + done.stderr, done.stdout + done.stderr
|
|
64
|
+
assert sent(tmp_path) == ["text of R\n"]
|
|
65
|
+
(run,) = _runs(root)
|
|
66
|
+
assert recorded(root, run.stem) == {
|
|
67
|
+
"prompt.txt": {"sha": sha(root / "prompt.txt"), "text": "text of R\n"}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@pytest.mark.parametrize("how", ["flag", "ini"])
|
|
72
|
+
def test_2_from_outside_the_run_is_refused_and_nothing_is_sent(
|
|
73
|
+
tmp_path: Path, how: str
|
|
74
|
+
) -> None:
|
|
75
|
+
root = layout(tmp_path)
|
|
76
|
+
done = _pytest(tmp_path, "OUT", how)
|
|
77
|
+
out = done.stdout + done.stderr
|
|
78
|
+
assert "the target Stub of suite 'qa'" in out, out
|
|
79
|
+
assert "(ADR 0042 §2)" in out
|
|
80
|
+
assert sent(tmp_path) == []
|
|
81
|
+
assert not _runs(root)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@pytest.mark.parametrize("how", ["flag", "ini"])
|
|
85
|
+
def test_4_a_relative_answer_is_refused_naming_the_target(
|
|
86
|
+
tmp_path: Path, how: str
|
|
87
|
+
) -> None:
|
|
88
|
+
root = layout(tmp_path, target=RELATIVE)
|
|
89
|
+
done = _pytest(tmp_path, "R", how)
|
|
90
|
+
out = done.stdout + done.stderr
|
|
91
|
+
assert "the target Naming of suite 'qa'" in out, out
|
|
92
|
+
assert "a relative path" in out
|
|
93
|
+
assert not _runs(root)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|