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.
Files changed (22) hide show
  1. {pytest_digline-0.2.3 → pytest_digline-0.2.4}/.gitignore +9 -0
  2. {pytest_digline-0.2.3 → pytest_digline-0.2.4}/PKG-INFO +3 -4
  3. {pytest_digline-0.2.3 → pytest_digline-0.2.4}/README.md +1 -2
  4. {pytest_digline-0.2.3 → pytest_digline-0.2.4}/pyproject.toml +9 -2
  5. {pytest_digline-0.2.3 → pytest_digline-0.2.4}/src/pytest_digline/plugin.py +7 -1
  6. pytest_digline-0.2.4/tests/test_digline_run_notes.py +103 -0
  7. pytest_digline-0.2.4/tests/test_plugin_one_declared_path.py +93 -0
  8. {pytest_digline-0.2.3 → pytest_digline-0.2.4}/src/pytest_digline/__init__.py +0 -0
  9. {pytest_digline-0.2.3 → pytest_digline-0.2.4}/src/pytest_digline/py.typed +0 -0
  10. {pytest_digline-0.2.3 → pytest_digline-0.2.4}/tests/_cycle.py +0 -0
  11. {pytest_digline-0.2.3 → pytest_digline-0.2.4}/tests/conftest.py +0 -0
  12. {pytest_digline-0.2.3 → pytest_digline-0.2.4}/tests/test_calibration_rows.py +0 -0
  13. {pytest_digline-0.2.3 → pytest_digline-0.2.4}/tests/test_contract.py +0 -0
  14. {pytest_digline-0.2.3 → pytest_digline-0.2.4}/tests/test_cost.py +0 -0
  15. {pytest_digline-0.2.3 → pytest_digline-0.2.4}/tests/test_escapes.py +0 -0
  16. {pytest_digline-0.2.3 → pytest_digline-0.2.4}/tests/test_exits.py +0 -0
  17. {pytest_digline-0.2.3 → pytest_digline-0.2.4}/tests/test_incomparable_rows.py +0 -0
  18. {pytest_digline-0.2.3 → pytest_digline-0.2.4}/tests/test_inert.py +0 -0
  19. {pytest_digline-0.2.3 → pytest_digline-0.2.4}/tests/test_message.py +0 -0
  20. {pytest_digline-0.2.3 → pytest_digline-0.2.4}/tests/test_no_promote.py +0 -0
  21. {pytest_digline-0.2.3 → pytest_digline-0.2.4}/tests/test_states.py +0 -0
  22. {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
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.30.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
- 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
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
- 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
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"
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
- "digline>=0.30.0",
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)