Skip to content

Commit 22d9392

Browse files
Add conformance results invariant validator (#2205)
1 parent a014f3a commit 22d9392

File tree

7 files changed

+125
-5
lines changed

7 files changed

+125
-5
lines changed

.github/workflows/conformance.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@ jobs:
3232
uv sync --python 3.12 --frozen
3333
uv run --python 3.12 --frozen python src/main.py
3434
35+
- name: Validate conformance invariants
36+
working-directory: conformance
37+
run: |
38+
uv run --python 3.12 --frozen python src/validate_results.py
39+
3540
- name: Assert conformance results are up to date
3641
run: |
3742
if [ -n "$(git status --porcelain -- conformance/results)" ]; then

conformance/results/mypy/constructors_call_metaclass.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
conformant = "Unupported"
1+
conformant = "Unsupported"
22
notes = """
33
Does not honor metaclass __call__ method when evaluating constructor call.
44
Does not skip evaluation of __new__ and __init__ if custom metaclass call returns non-class.

conformance/results/mypy/generics_defaults.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
conformant = "Partial"
2+
notes = """
3+
Does not detect a TypeVar with a default used after a TypeVarTuple.
4+
Does not fully support defaults on TypeVarTuple and ParamSpec.
5+
"""
26
output = """
37
generics_defaults.py:24: error: "T" cannot appear after "DefaultStrT" in type parameter list because it has no default type [misc]
48
generics_defaults.py:66: error: "AllTheDefaults" expects between 2 and 5 type arguments, but 1 given [type-arg]

conformance/results/mypy/generics_defaults_referential.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
conformant = "Partial"
2+
notes = """
3+
Does not correctly handle defaults referencing other TypeVars.
4+
"""
25
output = """
36
generics_defaults_referential.py:23: error: Expression is of type "type[slice[StartT, StopT, StepT]]", not "type[slice[int, int, int | None]]" [assert-type]
47
generics_defaults_referential.py:38: error: Argument 1 to "Foo" has incompatible type "str"; expected "int" [arg-type]

conformance/results/mypy/generics_defaults_specialization.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
conformant = "Partial"
2+
notes = """
3+
Does not correctly resolve defaults when classes are used directly.
4+
"""
25
output = """
36
generics_defaults_specialization.py:30: error: Bad number of arguments for type alias, expected between 0 and 1, given 2 [type-arg]
47
generics_defaults_specialization.py:45: error: Expression is of type "type[Bar[DefaultStrT]]", not "type[Bar[str]]" [assert-type]

conformance/results/results.html

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -277,21 +277,21 @@ <h3>Python Type System Conformance Test Results</h3>
277277
<th class="column col2 partially-conformant"><div class="hover-text">Partial<span class="tooltip-text" id="bottom"><p>Incorrectly allows constrained type variables to be solved to a union of their constraints.</p></span></div></th>
278278
</tr>
279279
<tr><th class="column col1">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;generics_defaults</th>
280-
<th class="column col2 partially-conformant">Partial</th>
280+
<th class="column col2 partially-conformant"><div class="hover-text">Partial<span class="tooltip-text" id="bottom"><p>Does not detect a TypeVar with a default used after a TypeVarTuple.</p><p>Does not fully support defaults on TypeVarTuple and ParamSpec.</p></span></div></th>
281281
<th class="column col2 conformant">Pass</th>
282282
<th class="column col2 conformant">Pass</th>
283283
<th class="column col2 conformant">Pass</th>
284284
<th class="column col2 partially-conformant"><div class="hover-text">Partial<span class="tooltip-text" id="bottom"><p>Does not forbid a `TypeVar` immediately following a `TypeVarTuple` in a parameter list from having a default.</p><p>Does not support `TypeVarTuple`.</p><p>Does not fully support defaults for `ParamSpec`s.</p></span></div></th>
285285
</tr>
286286
<tr><th class="column col1">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;generics_defaults_referential</th>
287-
<th class="column col2 partially-conformant">Partial</th>
287+
<th class="column col2 partially-conformant"><div class="hover-text">Partial<span class="tooltip-text" id="bottom"><p>Does not correctly handle defaults referencing other TypeVars.</p></span></div></th>
288288
<th class="column col2 conformant">Pass</th>
289289
<th class="column col2 conformant">Pass</th>
290290
<th class="column col2 conformant">Pass</th>
291291
<th class="column col2 conformant">Pass</th>
292292
</tr>
293293
<tr><th class="column col1">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;generics_defaults_specialization</th>
294-
<th class="column col2 partially-conformant">Partial</th>
294+
<th class="column col2 partially-conformant"><div class="hover-text">Partial<span class="tooltip-text" id="bottom"><p>Does not correctly resolve defaults when classes are used directly.</p></span></div></th>
295295
<th class="column col2 conformant">Pass</th>
296296
<th class="column col2 conformant">Pass</th>
297297
<th class="column col2 conformant">Pass</th>
@@ -718,7 +718,7 @@ <h3>Python Type System Conformance Test Results</h3>
718718
<th class="column col2 partially-conformant"><div class="hover-text">Partial<span class="tooltip-text" id="bottom"><p>Does not reject invalid argument types to an inherited constructor in a specialized subclass of a generic superclass.</p><p>Does not reject class-scoped type variables used in the `self` annotation.</p><p>Does not support inferring type variables for generic classes where the `__init__` method uses method-scoped type variables.</p></span></div></th>
719719
</tr>
720720
<tr><th class="column col1">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;constructors_call_metaclass</th>
721-
<th class="column col2 not-conformant"><div class="hover-text">Unupported<span class="tooltip-text" id="bottom"><p>Does not honor metaclass __call__ method when evaluating constructor call.</p><p>Does not skip evaluation of __new__ and __init__ if custom metaclass call returns non-class.</p></span></div></th>
721+
<th class="column col2 not-conformant"><div class="hover-text">Unsupported<span class="tooltip-text" id="bottom"><p>Does not honor metaclass __call__ method when evaluating constructor call.</p><p>Does not skip evaluation of __new__ and __init__ if custom metaclass call returns non-class.</p></span></div></th>
722722
<th class="column col2 conformant">Pass</th>
723723
<th class="column col2 conformant">Pass</th>
724724
<th class="column col2 conformant">Pass</th>
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
"""
2+
Validate invariants for conformance result files.
3+
"""
4+
5+
from pathlib import Path
6+
import sys
7+
import tomllib
8+
from typing import Any
9+
10+
ALLOWED_RESULT_KEYS = frozenset(
11+
{
12+
"conformance_automated",
13+
"conformant",
14+
"errors_diff",
15+
"ignore_errors",
16+
"notes",
17+
"output",
18+
}
19+
)
20+
21+
22+
def main() -> int:
23+
results_dir = Path(__file__).resolve().parent.parent / "results"
24+
issues: list[str] = []
25+
checked = 0
26+
27+
for type_checker_dir in sorted(results_dir.iterdir()):
28+
if not type_checker_dir.is_dir():
29+
continue
30+
for file in sorted(type_checker_dir.iterdir()):
31+
if file.name == "version.toml":
32+
continue
33+
checked += 1
34+
try:
35+
with file.open("rb") as f:
36+
info = tomllib.load(f)
37+
except Exception as e:
38+
issues.append(f"{file.relative_to(results_dir)}: failed to parse TOML ({e})")
39+
continue
40+
41+
issues.extend(_validate_result(file, results_dir, info))
42+
43+
if issues:
44+
print(f"Found {len(issues)} invariant violation(s) across {checked} file(s):")
45+
for issue in issues:
46+
print(f"- {issue}")
47+
return 1
48+
49+
print(f"Validated {checked} conformance result file(s); no invariant violations found.")
50+
return 0
51+
52+
53+
def _validate_result(file: Path, results_dir: Path, info: dict[str, Any]) -> list[str]:
54+
issues: list[str] = []
55+
rel_path = file.relative_to(results_dir)
56+
57+
unknown_keys = sorted(set(info) - ALLOWED_RESULT_KEYS)
58+
if unknown_keys:
59+
issues.append(
60+
f"{rel_path}: unrecognized key(s): {', '.join(repr(key) for key in unknown_keys)}"
61+
)
62+
63+
automated = info.get("conformance_automated")
64+
if automated not in {"Pass", "Fail"}:
65+
issues.append(
66+
f"{rel_path}: conformance_automated must be 'Pass' or 'Fail' (got {automated!r})"
67+
)
68+
return issues
69+
automated_is_pass = automated == "Pass"
70+
71+
conformant = info.get("conformant")
72+
if conformant is None:
73+
if automated_is_pass:
74+
conformant_is_pass = True
75+
else:
76+
issues.append(
77+
f"{rel_path}: conformant is required when conformance_automated is 'Fail'"
78+
)
79+
return issues
80+
elif isinstance(conformant, str):
81+
if conformant not in ("Pass", "Partial", "Unsupported"):
82+
issues.append(f"{rel_path}: invalid conformance status {conformant!r}")
83+
conformant_is_pass = conformant == "Pass"
84+
else:
85+
issues.append(f"{rel_path}: conformant must be a string when present")
86+
return issues
87+
88+
if conformant_is_pass != automated_is_pass:
89+
issues.append(
90+
f"{rel_path}: conformant={conformant!r} does not match "
91+
f"conformance_automated={automated!r}"
92+
)
93+
94+
if conformant == "Partial":
95+
notes = info.get("notes", "")
96+
if not isinstance(notes, str) or not notes.strip():
97+
issues.append(
98+
f"{rel_path}: notes must be present when checker is not fully conformant"
99+
)
100+
101+
return issues
102+
103+
104+
if __name__ == "__main__":
105+
raise SystemExit(main())

0 commit comments

Comments
 (0)