Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/conformance.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ jobs:
uv sync --python 3.12 --frozen
uv run --python 3.12 --frozen python src/main.py

- name: Validate conformance invariants
working-directory: conformance
run: |
uv run --python 3.12 --frozen python src/validate_results.py

- name: Assert conformance results are up to date
run: |
if [ -n "$(git status --porcelain -- conformance/results)" ]; then
Expand Down
2 changes: 1 addition & 1 deletion conformance/results/mypy/constructors_call_metaclass.toml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
conformant = "Unupported"
conformant = "Unsupported"
notes = """
Does not honor metaclass __call__ method when evaluating constructor call.
Does not skip evaluation of __new__ and __init__ if custom metaclass call returns non-class.
Expand Down
4 changes: 4 additions & 0 deletions conformance/results/mypy/generics_defaults.toml
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
conformant = "Partial"
notes = """
Does not detect a TypeVar with a default used after a TypeVarTuple.
Does not fully support defaults on TypeVarTuple and ParamSpec.
"""
output = """
generics_defaults.py:24: error: "T" cannot appear after "DefaultStrT" in type parameter list because it has no default type [misc]
generics_defaults.py:66: error: "AllTheDefaults" expects between 2 and 5 type arguments, but 1 given [type-arg]
Expand Down
3 changes: 3 additions & 0 deletions conformance/results/mypy/generics_defaults_referential.toml
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
conformant = "Partial"
notes = """
Does not correctly handle defaults referencing other TypeVars.
"""
output = """
generics_defaults_referential.py:23: error: Expression is of type "type[slice[StartT, StopT, StepT]]", not "type[slice[int, int, int | None]]" [assert-type]
generics_defaults_referential.py:37: error: Argument 1 to "Foo" has incompatible type "str"; expected "int" [arg-type]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
conformant = "Partial"
notes = """
Does not correctly resolve defaults when classes are used directly.
"""
output = """
generics_defaults_specialization.py:30: error: Bad number of arguments for type alias, expected between 0 and 1, given 2 [type-arg]
generics_defaults_specialization.py:45: error: Expression is of type "type[Bar[DefaultStrT]]", not "type[Bar[str]]" [assert-type]
Expand Down
8 changes: 4 additions & 4 deletions conformance/results/results.html
Original file line number Diff line number Diff line change
Expand Up @@ -263,19 +263,19 @@ <h3>Python Type System Conformance Test Results</h3>
<th class="column col2 partially-conformant"><div class="hover-text">Partial<span class="tooltip-text" id="bottom"><p>Incorrect rejects + between two AnyStr</p><p>Constrained type var resolves to subtype instead of explcitly listed constraint</p></span></div></th>
</tr>
<tr><th class="column col1">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;generics_defaults</th>
<th class="column col2 partially-conformant">Partial</th>
<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>
<th class="column col2 conformant">Pass</th>
<th class="column col2 conformant">Pass</th>
<th class="column col2 conformant">Pass</th>
</tr>
<tr><th class="column col1">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;generics_defaults_referential</th>
<th class="column col2 partially-conformant">Partial</th>
<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>
<th class="column col2 conformant">Pass</th>
<th class="column col2 conformant">Pass</th>
<th class="column col2 conformant">Pass</th>
</tr>
<tr><th class="column col1">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;generics_defaults_specialization</th>
<th class="column col2 partially-conformant">Partial</th>
<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>
<th class="column col2 conformant">Pass</th>
<th class="column col2 conformant">Pass</th>
<th class="column col2 conformant">Pass</th>
Expand Down Expand Up @@ -644,7 +644,7 @@ <h3>Python Type System Conformance Test Results</h3>
<th class="column col2 conformant">Pass</th>
</tr>
<tr><th class="column col1">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;constructors_call_metaclass</th>
<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>
<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>
<th class="column col2 conformant">Pass</th>
<th class="column col2 conformant">Pass</th>
<th class="column col2 conformant">Pass</th>
Expand Down
105 changes: 105 additions & 0 deletions conformance/src/validate_results.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
"""
Validate invariants for conformance result files.
"""

from pathlib import Path
import sys
import tomllib
from typing import Any

ALLOWED_RESULT_KEYS = frozenset(
{
"conformance_automated",
"conformant",
"errors_diff",
"ignore_errors",
"notes",
"output",
}
)


def main() -> int:
results_dir = Path(__file__).resolve().parent.parent / "results"
issues: list[str] = []
checked = 0

for type_checker_dir in sorted(results_dir.iterdir()):
if not type_checker_dir.is_dir():
continue
for file in sorted(type_checker_dir.iterdir()):
if file.name == "version.toml":
continue
checked += 1
try:
with file.open("rb") as f:
info = tomllib.load(f)
except Exception as e:
issues.append(f"{file.relative_to(results_dir)}: failed to parse TOML ({e})")
continue

issues.extend(_validate_result(file, results_dir, info))

if issues:
print(f"Found {len(issues)} invariant violation(s) across {checked} file(s):")
for issue in issues:
print(f"- {issue}")
return 1

print(f"Validated {checked} conformance result file(s); no invariant violations found.")
return 0


def _validate_result(file: Path, results_dir: Path, info: dict[str, Any]) -> list[str]:
issues: list[str] = []
rel_path = file.relative_to(results_dir)

unknown_keys = sorted(set(info) - ALLOWED_RESULT_KEYS)
if unknown_keys:
issues.append(
f"{rel_path}: unrecognized key(s): {', '.join(repr(key) for key in unknown_keys)}"
)

automated = info.get("conformance_automated")
if automated not in {"Pass", "Fail"}:
issues.append(
f"{rel_path}: conformance_automated must be 'Pass' or 'Fail' (got {automated!r})"
)
return issues
automated_is_pass = automated == "Pass"

conformant = info.get("conformant")
if conformant is None:
if automated_is_pass:
conformant_is_pass = True
else:
issues.append(
f"{rel_path}: conformant is required when conformance_automated is 'Fail'"
)
return issues
elif isinstance(conformant, str):
if conformant not in ("Pass", "Partial", "Unsupported"):
issues.append(f"{rel_path}: invalid conformance status {conformant!r}")
conformant_is_pass = conformant == "Pass"
else:
issues.append(f"{rel_path}: conformant must be a string when present")
return issues

if conformant_is_pass != automated_is_pass:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the only one I wonder about. Conformance is human-scored for a reason. In cases where a conformance test accidentally tests something unspecified and unrelated to the topic of the test, is it reasonable for a human scorer to mark conformance as "Pass" even if conformance_automated is "Fail"? (Ideally this would be temporary, pending a PR to improve the conformance suite.) Or do we want to prohibit this, in order to better motivate improving the conformance suite? It seems misleading to mark a test file as "Partial" due to an automated-scoring mismatch not actually related to the topic of the test.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think in that case, we should fix the test so the mismatch goes away.

issues.append(
f"{rel_path}: conformant={conformant!r} does not match "
f"conformance_automated={automated!r}"
)

if not conformant_is_pass:
notes = info.get("notes", "")
if not isinstance(notes, str) or not notes.strip():
issues.append(
f"{rel_path}: notes must be present when checker is not fully conformant"
)

return issues


if __name__ == "__main__":
raise SystemExit(main())