Skip to content

Commit

Permalink
Initial check-in of type specification conformance suite. (#1552)
Browse files Browse the repository at this point in the history
  • Loading branch information
erictraut authored Dec 28, 2023
1 parent ec4903b commit 8aa8f8b
Show file tree
Hide file tree
Showing 226 changed files with 7,961 additions and 0 deletions.
1 change: 1 addition & 0 deletions .flake8
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
[flake8]

max-line-length = 90
exclude = conformance
ignore =
# irrelevant plugins
B3,
Expand Down
47 changes: 47 additions & 0 deletions conformance/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# C extensions
*.so

# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST

# Environments
.env
.venv

# Tools
.mypy_cache
.pyre_configuration
.pyre
.coverage
htmlcov

# General
.DS_Store

# Editor temp files
.*.swp

# Workspace configurations
.vscode
71 changes: 71 additions & 0 deletions conformance/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Type System Conformance

## Motivation

[PEP 729](https://peps.python.org/pep-0729/) provides a structured and documented way to specify and evolve the Python type system. In support of this effort, an official [Python typing spec](https://github.com/python/typing/tree/main/docs/spec) has been drafted. This spec consolidates details from various historical typing-related PEPs. The spec will be modified over time to clarify unspecified and under-specified parts of the type system. It will also be extended to cover new features of the type system.

Accompanying the typing specification is this conformance test suite which validates the behavior of static type checkers against the specification.

## Structure & Name

This project contains test cases for behaviors defined in the Python typing spec. Tests are structured and grouped in accordance with the specification's chapter headings.

* [concepts](https://typing.readthedocs.io/en/latest/spec/concepts.html)
* [annotations](https://typing.readthedocs.io/en/latest/spec/annotations.html)
* [specialtypes](https://typing.readthedocs.io/en/latest/spec/special-types.html)
* [generics](https://typing.readthedocs.io/en/latest/spec/generics.html)
* [qualifiers](https://typing.readthedocs.io/en/latest/spec/qualifiers.html)
* [classes](https://typing.readthedocs.io/en/latest/spec/class-compat.html)
* [aliases](https://typing.readthedocs.io/en/latest/spec/aliases.html)
* [literals](https://typing.readthedocs.io/en/latest/spec/literal.html)
* [protocols](https://typing.readthedocs.io/en/latest/spec/protocol.html)
* [callables](https://typing.readthedocs.io/en/latest/spec/callables.html)
* [overloads](https://typing.readthedocs.io/en/latest/spec/overload.html)
* [dataclasses](https://typing.readthedocs.io/en/latest/spec/dataclasses.html)
* [typeddicts](https://typing.readthedocs.io/en/latest/spec/typeddict.html)
* [narrowing](https://typing.readthedocs.io/en/latest/spec/narrowing.html)
* [directives](https://typing.readthedocs.io/en/latest/spec/directives.html)
* [distribution](https://typing.readthedocs.io/en/latest/spec/distributing.html)
* [historical](https://typing.readthedocs.io/en/latest/spec/historical.html)

A test file is a ".py" file. The file name should start with one of the above names followed by a description of the test (with words separated by underscores). For example, `generics_paramspec_basic_usage.py` would contain the basic usage tests for `ParamSpec`. Each test file can contain multiple individual unit tests, but these tests should be related to each other. If the number of unit tests in a single test file exceeds ten, it may be desirable to split it into separate test files. This will help maintain a consistent level of granularity across tests.

## Notes About Tests

Tests are designed to run on all current and future static type checkers. They must therefore be type-checker agnostic and should not rely on functionality or behaviors that are specific to one type checker or another.

Test cases are meant to be human readable. They should include comments that help explain their purpose (what is being tested, whether an error should be generated, etc.). They should also contain links to the typing spec, discussions, and issue trackers.

The test suite focuses on static type checking not general Python semantics. Tests should therefore focus on static analysis behaviors, not runtime behaviors.

## Running the Conformance Test Tool

To run the conformance test suite:
* Clone the https://github.com/python/typing repo.
* Create and activate a Python 3.12 virtual environment.
* Switch to the `conformance` subdirectory and install all dependencies (`pip install -r requirements.txt`).
* Switch to the `src` subdirectory and run `python main.py`.

Note that some type checkers may not run on some platforms. For example, pytype cannot be installed on Windows. If a type checker fails to install, tests will be skipped for that type checker.

## Reporting Conformance Results

Different type checkers report errors in different ways (with different wording in error messages and different line numbers or character ranges for errors). This variation makes it difficult to fully automate test validation given that tests will want to check for both false positive and false negative type errors. Some level of manual inspection will therefore be needed to determine whether a type checker is fully conformant with all tests in any given test file. This "scoring" process is required only when the output of a test changes — e.g. when a new version of that type checker is released and the tests are rerun. We assume that the output of a type checker will be the same from one run to the next unless/until a new version is released that fixes or introduces a bug. In this case, the output will need to be manually inspected and the conformance results re-scored for those tests whose output has changed.

Conformance results are reported and summarized for each supported type checker. Initially, results will be reported for mypy and pyright. It is the goal and desire to add additional type checkers over time.

## Adding a New Test Case

To add a new test, create a new ".py" file in the `tests` directory. Its name must begin with one of the above test group names followed by an underscore. Write the contents of the test including a module docstring describing the purpose of the test. Next, run the conformance test tool. This will generate a new `.toml` file in the `results` subdirectory corresponding each type checker. Manually review the output from each type checker and determine whether it conforms to the specification. If so, add `conformant = "Pass"` to the `.toml` file. If it does not fully comply, add `conformant = "Partial"` and a `notes` section detailing where it is not compliant. If the type checker doesn't support the feature in the test add `conformant = "Unsupported"`. Once the conformance status has been updated, rerun the conformance test tool to regenerate the summary report.

## Updating a Test Case

If a test is updated (augmented or fixed), the process is similar to when adding a new test. Run the conformance test tool to generate new results and manually examine the output of each supported type checker. Then update the conformance status accordingly. Once the conformance status has been updated, rerun the conformance test tool to regenerate the summary report.

## Updating a Type Checker

If a new version of a type checker is released, re-run the test tool with the new version. If the type checker output has changed for any test cases, the tool will supply the old and new outputs. Examine these to determine whether the conformance status has changed. Once the conformance status has been updated, re-run the test tool again to regenerate the summary report.

## Contributing

Contributions are welcome!
7 changes: 7 additions & 0 deletions conformance/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
tomli
tomlkit
pyright
mypy
pyre-check
pytype; platform_system != "Windows"

28 changes: 28 additions & 0 deletions conformance/results/mypy/aliases_explicit.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
conformant = "Partial"
notes = """
Does not reject specialization of type alias that has already been implicitly specialized.
"""
output = """
aliases_explicit.py:67: error: Bad number of arguments for type alias, expected: 0, given: 1 [type-arg]
aliases_explicit.py:68: error: Bad number of arguments for type alias, expected: 0, given: 1 [type-arg]
aliases_explicit.py:69: error: Bad number of arguments for type alias, expected: 1, given: 2 [type-arg]
aliases_explicit.py:70: error: Bad number of arguments for type alias, expected: 1, given: 2 [type-arg]
aliases_explicit.py:71: error: Can only replace ParamSpec with a parameter types list or another ParamSpec, got "int" [misc]
aliases_explicit.py:79: error: Invalid type alias: expression is not a valid type [valid-type]
aliases_explicit.py:80: error: Bracketed expression "[...]" is not valid as a type [valid-type]
aliases_explicit.py:81: error: Invalid type alias: expression is not a valid type [valid-type]
aliases_explicit.py:82: error: Invalid type alias: expression is not a valid type [valid-type]
aliases_explicit.py:83: error: Invalid type alias: expression is not a valid type [valid-type]
aliases_explicit.py:84: error: Invalid type alias: expression is not a valid type [valid-type]
aliases_explicit.py:85: error: Invalid type alias: expression is not a valid type [valid-type]
aliases_explicit.py:86: error: Invalid type alias: expression is not a valid type [valid-type]
aliases_explicit.py:87: error: Variable "aliases_explicit.var1" is not valid as a type [valid-type]
aliases_explicit.py:87: note: See https://mypy.readthedocs.io/en/stable/common_issues.html#variables-vs-type-aliases
aliases_explicit.py:88: error: Invalid type: try using Literal[True] instead? [valid-type]
aliases_explicit.py:89: error: Invalid type: try using Literal[1] instead? [valid-type]
aliases_explicit.py:90: error: Invalid type alias: expression is not a valid type [valid-type]
aliases_explicit.py:90: error: Function "list" could always be true in boolean context [truthy-function]
aliases_explicit.py:91: error: Invalid type alias: expression is not a valid type [valid-type]
aliases_explicit.py:101: error: "<typing special form>" not callable [operator]
aliases_explicit.py:102: error: Bad number of arguments for type alias, expected: 0, given: 1 [type-arg]
"""
40 changes: 40 additions & 0 deletions conformance/results/mypy/aliases_implicit.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
conformant = "Pass"
output = """
aliases_implicit.py:76: error: Bad number of arguments for type alias, expected: 0, given: 1 [type-arg]
aliases_implicit.py:77: error: Bad number of arguments for type alias, expected: 0, given: 1 [type-arg]
aliases_implicit.py:78: error: Bad number of arguments for type alias, expected: 1, given: 2 [type-arg]
aliases_implicit.py:79: error: Bad number of arguments for type alias, expected: 1, given: 2 [type-arg]
aliases_implicit.py:80: error: Can only replace ParamSpec with a parameter types list or another ParamSpec, got "int" [misc]
aliases_implicit.py:81: error: Type argument "str" of "GoodTypeAlias12" must be a subtype of "float" [type-var]
aliases_implicit.py:100: error: Function "list" could always be true in boolean context [truthy-function]
aliases_implicit.py:106: error: Variable "aliases_implicit.BadTypeAlias1" is not valid as a type [valid-type]
aliases_implicit.py:106: note: See https://mypy.readthedocs.io/en/stable/common_issues.html#variables-vs-type-aliases
aliases_implicit.py:107: error: Variable "aliases_implicit.BadTypeAlias2" is not valid as a type [valid-type]
aliases_implicit.py:107: note: See https://mypy.readthedocs.io/en/stable/common_issues.html#variables-vs-type-aliases
aliases_implicit.py:108: error: Variable "aliases_implicit.BadTypeAlias3" is not valid as a type [valid-type]
aliases_implicit.py:108: note: See https://mypy.readthedocs.io/en/stable/common_issues.html#variables-vs-type-aliases
aliases_implicit.py:109: error: Variable "aliases_implicit.BadTypeAlias4" is not valid as a type [valid-type]
aliases_implicit.py:109: note: See https://mypy.readthedocs.io/en/stable/common_issues.html#variables-vs-type-aliases
aliases_implicit.py:110: error: Variable "aliases_implicit.BadTypeAlias5" is not valid as a type [valid-type]
aliases_implicit.py:110: note: See https://mypy.readthedocs.io/en/stable/common_issues.html#variables-vs-type-aliases
aliases_implicit.py:111: error: Variable "aliases_implicit.BadTypeAlias6" is not valid as a type [valid-type]
aliases_implicit.py:111: note: See https://mypy.readthedocs.io/en/stable/common_issues.html#variables-vs-type-aliases
aliases_implicit.py:112: error: Variable "aliases_implicit.BadTypeAlias7" is not valid as a type [valid-type]
aliases_implicit.py:112: note: See https://mypy.readthedocs.io/en/stable/common_issues.html#variables-vs-type-aliases
aliases_implicit.py:113: error: Variable "aliases_implicit.BadTypeAlias8" is not valid as a type [valid-type]
aliases_implicit.py:113: note: See https://mypy.readthedocs.io/en/stable/common_issues.html#variables-vs-type-aliases
aliases_implicit.py:114: error: Variable "aliases_implicit.BadTypeAlias9" is not valid as a type [valid-type]
aliases_implicit.py:114: note: See https://mypy.readthedocs.io/en/stable/common_issues.html#variables-vs-type-aliases
aliases_implicit.py:115: error: Variable "aliases_implicit.BadTypeAlias10" is not valid as a type [valid-type]
aliases_implicit.py:115: note: See https://mypy.readthedocs.io/en/stable/common_issues.html#variables-vs-type-aliases
aliases_implicit.py:116: error: Variable "aliases_implicit.BadTypeAlias11" is not valid as a type [valid-type]
aliases_implicit.py:116: note: See https://mypy.readthedocs.io/en/stable/common_issues.html#variables-vs-type-aliases
aliases_implicit.py:117: error: Variable "aliases_implicit.BadTypeAlias12" is not valid as a type [valid-type]
aliases_implicit.py:117: note: See https://mypy.readthedocs.io/en/stable/common_issues.html#variables-vs-type-aliases
aliases_implicit.py:118: error: Variable "aliases_implicit.BadTypeAlias13" is not valid as a type [valid-type]
aliases_implicit.py:118: note: See https://mypy.readthedocs.io/en/stable/common_issues.html#variables-vs-type-aliases
aliases_implicit.py:119: error: Variable "aliases_implicit.BadTypeAlias14" is not valid as a type [valid-type]
aliases_implicit.py:119: note: See https://mypy.readthedocs.io/en/stable/common_issues.html#variables-vs-type-aliases
aliases_implicit.py:133: error: "<typing special form>" not callable [operator]
aliases_implicit.py:135: error: Bad number of arguments for type alias, expected: 0, given: 1 [type-arg]
"""
18 changes: 18 additions & 0 deletions conformance/results/mypy/aliases_newtype.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
conformant = "Pass"
output = """
aliases_newtype.py:11: error: Argument 1 to "UserId" has incompatible type "str"; expected "int" [arg-type]
aliases_newtype.py:12: error: Incompatible types in assignment (expression has type "int", variable has type "UserId") [assignment]
aliases_newtype.py:20: error: Cannot use isinstance() with NewType type [misc]
aliases_newtype.py:23: error: Cannot subclass "NewType" [misc]
aliases_newtype.py:32: error: String argument 1 "BadName" to NewType(...) does not match variable name "GoodName" [misc]
aliases_newtype.py:36: error: "GoodNewType1" expects no type arguments, but 1 given [type-arg]
aliases_newtype.py:42: error: Argument 2 to NewType(...) must be subclassable (got "int | str") [valid-newtype]
aliases_newtype.py:45: error: Type variable "aliases_newtype.T" is unbound [valid-type]
aliases_newtype.py:45: note: (Hint: Use "Generic[T]" or "Protocol[T]" base class to bind "T" inside a class)
aliases_newtype.py:45: note: (Hint: Use "T" in function signature to bind "T" inside a function)
aliases_newtype.py:47: error: NewType cannot be used with protocol classes [misc]
aliases_newtype.py:49: error: Argument 2 to NewType(...) must be subclassable (got "Literal[7]") [valid-newtype]
aliases_newtype.py:56: error: Argument 2 to NewType(...) must be subclassable (got "TD1") [valid-newtype]
aliases_newtype.py:60: error: NewType(...) expects exactly two positional arguments [misc]
aliases_newtype.py:62: error: Argument 2 to NewType(...) must be subclassable (got "Any") [valid-newtype]
"""
14 changes: 14 additions & 0 deletions conformance/results/mypy/aliases_recursive.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
conformant = "Pass"
output = """
aliases_recursive.py:19: error: Dict entry 1 has incompatible type "str": "complex"; expected "str": "int | str | float | list[Json] | dict[str, Json] | None" [dict-item]
aliases_recursive.py:20: error: List item 1 has incompatible type "complex"; expected "int | str | float | list[Json] | dict[str, Json] | None" [list-item]
aliases_recursive.py:38: error: Incompatible types in assignment (expression has type "tuple[int, tuple[str, int], tuple[int, tuple[int, list[int]]]]", variable has type "RecursiveTuple") [assignment]
aliases_recursive.py:39: error: Name "t6" already defined on line 38 [no-redef]
aliases_recursive.py:50: error: Dict entry 0 has incompatible type "str": "list[int]"; expected "str": "str | int | Mapping[str, RecursiveMapping]" [dict-item]
aliases_recursive.py:51: error: Dict entry 2 has incompatible type "str": "list[int]"; expected "str": "str | int | Mapping[str, RecursiveMapping]" [dict-item]
aliases_recursive.py:55: error: Dict entry 2 has incompatible type "str": "list[int]"; expected "str": "str | int | Mapping[str, RecursiveMapping]" [dict-item]
aliases_recursive.py:67: error: List item 0 has incompatible type "float"; expected "GenericTypeAlias1[str] | str" [list-item]
aliases_recursive.py:73: error: List item 0 has incompatible type "float"; expected "GenericTypeAlias2[str, int] | str | int" [list-item]
aliases_recursive.py:76: error: Invalid recursive alias: a union item of itself [misc]
aliases_recursive.py:81: error: Invalid recursive alias: a union item of itself [misc]
"""
Loading

0 comments on commit 8aa8f8b

Please sign in to comment.