From cd81f97c1ed5702f18c826e5b22ee716f41c580d Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Mon, 15 Jan 2024 22:17:16 -0800 Subject: [PATCH 1/2] Conformance tests and spec change for generic type erasure --- conformance/tests/generics_type_erasure.py | 54 ++++++++++++++++++++++ docs/spec/generics.rst | 8 ++-- 2 files changed, 58 insertions(+), 4 deletions(-) create mode 100644 conformance/tests/generics_type_erasure.py diff --git a/conformance/tests/generics_type_erasure.py b/conformance/tests/generics_type_erasure.py new file mode 100644 index 00000000..316ff45c --- /dev/null +++ b/conformance/tests/generics_type_erasure.py @@ -0,0 +1,54 @@ +# Specification: https://typing.readthedocs.io/en/latest/spec/generics.html#instantiating-generic-classes-and-type-erasure + +from typing import Any, TypeVar, Generic, assert_type + +T = TypeVar("T") + +# > If the constructor (__init__ or __new__) uses T in its signature, and a +# > corresponding argument value is passed, the type of the corresponding +# > argument(s) is substituted. Otherwise, Any is assumed. + +class Node(Generic[T]): + label: T + def __init__(self, label: T | None = None) -> None: ... + +assert_type(Node(''), Node[str]) +assert_type(Node(0), Node[int]) +assert_type(Node(), Node[Any]) + +assert_type(Node(0).label, int) +assert_type(Node().label, Any) + +# > In case the inferred type uses [Any] but the intended type is more specific, +# > you can use an annotation to force the type of the variable, e.g.: + +n1: Node[int] = Node() +assert_type(n1, Node[int]) +n2: Node[str] = Node() +assert_type(n2, Node[str]) + +n3 = Node[int]() +assert_type(n3, Node[int]) +n4 = Node[str]() +assert_type(n4, Node[str]) + +n5 = Node[int](0) # OK +n6 = Node[int]("") # Type error +n7 = Node[str]("") # OK +n8 = Node[str](0) # Type error + +Node[int].label = 1 # Type error +Node[int].label # Type error +Node.label = 1 # Type error +Node.label # Type error +type(n1).label # Type error +assert_type(n1.label, int) +assert_type(Node[int]().label, int) +n1.label = 1 # OK + +# > [...] generic versions of concrete collections can be instantiated: + +from typing import DefaultDict + +data = DefaultDict[int, bytes]() +assert_type(data[0], bytes) diff --git a/docs/spec/generics.rst b/docs/spec/generics.rst index 6af51adc..bb23ffb8 100644 --- a/docs/spec/generics.rst +++ b/docs/spec/generics.rst @@ -333,7 +333,7 @@ argument(s) is substituted. Otherwise, ``Any`` is assumed. Example:: class Node(Generic[T]): x: T # Instance attribute (see below) - def __init__(self, label: T = None) -> None: + def __init__(self, label: T | None = None) -> None: ... x = Node('') # Inferred type is Node[str] @@ -341,7 +341,7 @@ argument(s) is substituted. Otherwise, ``Any`` is assumed. Example:: z = Node() # Inferred type is Node[Any] In case the inferred type uses ``[Any]`` but the intended type is more -specific, you can use a type comment (see below) to force the type of +specific, you can use an annotation (see below) to force the type of the variable, e.g.:: # (continued from previous example) @@ -373,8 +373,8 @@ class instance that does not have an instance attribute with the same name:: Node.x = 1 # Error Node.x # Error type(p).x # Error - p.x # Ok (evaluates to None) - Node[int]().x # Ok (evaluates to None) + p.x # Ok (evaluates to int) + Node[int]().x # Ok (evaluates to int) p.x = 1 # Ok, but assigning to instance attribute Generic versions of abstract collections like ``Mapping`` or ``Sequence`` From 7eb65ca814cf73f036980d460e1a2e926e453d2c Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Mon, 15 Jan 2024 22:22:06 -0800 Subject: [PATCH 2/2] grading --- .../results/mypy/generics_type_erasure.toml | 15 +++++++++++++++ .../results/pyre/generics_type_erasure.toml | 11 +++++++++++ .../results/pyright/generics_type_erasure.toml | 18 ++++++++++++++++++ .../results/pytype/generics_type_erasure.toml | 4 ++++ 4 files changed, 48 insertions(+) create mode 100644 conformance/results/mypy/generics_type_erasure.toml create mode 100644 conformance/results/pyre/generics_type_erasure.toml create mode 100644 conformance/results/pyright/generics_type_erasure.toml create mode 100644 conformance/results/pytype/generics_type_erasure.toml diff --git a/conformance/results/mypy/generics_type_erasure.toml b/conformance/results/mypy/generics_type_erasure.toml new file mode 100644 index 00000000..d99efc11 --- /dev/null +++ b/conformance/results/mypy/generics_type_erasure.toml @@ -0,0 +1,15 @@ +conformant = "Partial" +notes = """ +Infers Node[Never] instead of Node[Any] when argument is not provided. +False negative on instance attribute access on type(node). +""" +output = """ +generics_type_erasure.py:17: error: Expression is of type "Node[Never]", not "Node[Any]" [assert-type] +generics_type_erasure.py:20: error: Expression is of type Never, not "Any" [assert-type] +generics_type_erasure.py:36: error: Argument 1 to "Node" has incompatible type "str"; expected "int | None" [arg-type] +generics_type_erasure.py:38: error: Argument 1 to "Node" has incompatible type "int"; expected "str | None" [arg-type] +generics_type_erasure.py:40: error: Access to generic instance variables via class is ambiguous [misc] +generics_type_erasure.py:41: error: Access to generic instance variables via class is ambiguous [misc] +generics_type_erasure.py:42: error: Access to generic instance variables via class is ambiguous [misc] +generics_type_erasure.py:43: error: Access to generic instance variables via class is ambiguous [misc] +""" diff --git a/conformance/results/pyre/generics_type_erasure.toml b/conformance/results/pyre/generics_type_erasure.toml new file mode 100644 index 00000000..9c343f3a --- /dev/null +++ b/conformance/results/pyre/generics_type_erasure.toml @@ -0,0 +1,11 @@ +conformant = "Partial" +notes = """ +Doesn't allow using Node[Any] in assert_type expression. +False negatives on instance attribute access on the type. +""" +output = """ +generics_type_erasure.py:11:0 Uninitialized attribute [13]: Attribute `label` is declared in class `Node` to have type `Variable[T]` but is never initialized. +generics_type_erasure.py:17:25 Incompatible parameter type [6]: In call `typing.GenericMeta.__getitem__`, for 1st positional argument, expected `Type[Variable[T]]` but got `object`. +generics_type_erasure.py:36:15 Incompatible parameter type [6]: In call `Node.__init__`, for 1st positional argument, expected `Optional[int]` but got `str`. +generics_type_erasure.py:38:15 Incompatible parameter type [6]: In call `Node.__init__`, for 1st positional argument, expected `Optional[str]` but got `int`. +""" diff --git a/conformance/results/pyright/generics_type_erasure.toml b/conformance/results/pyright/generics_type_erasure.toml new file mode 100644 index 00000000..93af4864 --- /dev/null +++ b/conformance/results/pyright/generics_type_erasure.toml @@ -0,0 +1,18 @@ +conformant = "Partial" +notes = """ +False negatives on instance attribute access on the type. +""" +output = """ +generics_type_erasure.py:36:16 - error: Argument of type "Literal['']" cannot be assigned to parameter "label" of type "int | None" in function "__init__" +  Type "Literal['']" cannot be assigned to type "int | None" +    "Literal['']" is incompatible with "int" +    "Literal['']" is incompatible with "None" (reportGeneralTypeIssues) +generics_type_erasure.py:38:16 - error: Argument of type "Literal[0]" cannot be assigned to parameter "label" of type "str | None" in function "__init__" +  Type "Literal[0]" cannot be assigned to type "str | None" +    "Literal[0]" is incompatible with "str" +    "Literal[0]" is incompatible with "None" (reportGeneralTypeIssues) +generics_type_erasure.py:42:6 - error: Cannot assign member "label" for type "type[Node[T@Node]]" +  Expression of type "Literal[1]" cannot be assigned to member "label" of class "Node[T@Node]" +    Member "__set__" is unknown +    Type "Literal[1]" cannot be assigned to type "T@Node" (reportGeneralTypeIssues) +""" diff --git a/conformance/results/pytype/generics_type_erasure.toml b/conformance/results/pytype/generics_type_erasure.toml new file mode 100644 index 00000000..889e84f4 --- /dev/null +++ b/conformance/results/pytype/generics_type_erasure.toml @@ -0,0 +1,4 @@ +conformant = "Unsupported" +output = """ +NotImplementedError: ParameterizedClass +"""