Skip to content

Add conformance tests for basic generic spec #1553

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Jan 16, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
17 changes: 17 additions & 0 deletions conformance/results/mypy/generics_basic.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
conformant = "Pass"
output = """
generics_basic.py:36: error: Value of type variable "AnyStr" of "concat" cannot be "Sequence[object]" [type-var]
generics_basic.py:37: error: Value of type variable "AnyStr" of "concat" cannot be "Sequence[object]" [type-var]
generics_basic.py:44: error: TypeVar cannot have only a single constraint [misc]
generics_basic.py:48: error: Type variable "generics_basic.T" is unbound [valid-type]
generics_basic.py:48: note: (Hint: Use "Generic[T]" or "Protocol[T]" base class to bind "T" inside a class)
generics_basic.py:48: note: (Hint: Use "T" in function signature to bind "T" inside a function)
generics_basic.py:59: error: Value of type variable "AnyStr" of "concat" cannot be "Sequence[object]" [type-var]
generics_basic.py:107: error: Duplicate type variables in Generic[...] or Protocol[...] [misc]
generics_basic.py:140: error: Invalid index type "int" for "MyMap1[str, int]"; expected type "str" [index]
generics_basic.py:141: error: Invalid index type "int" for "MyMap2[int, str]"; expected type "str" [index]
generics_basic.py:167: error: Dynamic metaclass not supported for "GenericMetaInstance" [misc]
generics_basic.py:167: error: Type variable "generics_basic.T" is unbound [valid-type]
generics_basic.py:167: note: (Hint: Use "Generic[T]" or "Protocol[T]" base class to bind "T" inside a class)
generics_basic.py:167: note: (Hint: Use "T" in function signature to bind "T" inside a function)
"""
27 changes: 27 additions & 0 deletions conformance/results/mypy/generics_scoping.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
conformant = "Partial"
notes = """
False negative on generic class nested within generic class with same type variable.
"""
output = """
generics_scoping.py:25: error: Argument 1 to "meth_2" of "MyClass" has incompatible type "str"; expected "int" [arg-type]
generics_scoping.py:46: error: Type variable "generics_scoping.S" is unbound [valid-type]
generics_scoping.py:46: note: (Hint: Use "Generic[S]" or "Protocol[S]" base class to bind "S" inside a class)
generics_scoping.py:46: note: (Hint: Use "S" in function signature to bind "S" inside a function)
generics_scoping.py:50: error: Type variable "generics_scoping.S" is unbound [valid-type]
generics_scoping.py:50: note: (Hint: Use "Generic[S]" or "Protocol[S]" base class to bind "S" inside a class)
generics_scoping.py:50: note: (Hint: Use "S" in function signature to bind "S" inside a function)
generics_scoping.py:61: error: Free type variable expected in Generic[...] [misc]
generics_scoping.py:74: error: Type variable "generics_scoping.T" is unbound [valid-type]
generics_scoping.py:74: note: (Hint: Use "Generic[T]" or "Protocol[T]" base class to bind "T" inside a class)
generics_scoping.py:74: note: (Hint: Use "T" in function signature to bind "T" inside a function)
generics_scoping.py:80: error: Can't use bound type variable "T" to define generic alias [valid-type]
generics_scoping.py:84: error: Type variable "generics_scoping.T" is unbound [valid-type]
generics_scoping.py:84: note: (Hint: Use "Generic[T]" or "Protocol[T]" base class to bind "T" inside a class)
generics_scoping.py:84: note: (Hint: Use "T" in function signature to bind "T" inside a function)
generics_scoping.py:85: error: Type variable "generics_scoping.T" is unbound [valid-type]
generics_scoping.py:85: note: (Hint: Use "Generic[T]" or "Protocol[T]" base class to bind "T" inside a class)
generics_scoping.py:85: note: (Hint: Use "T" in function signature to bind "T" inside a function)
generics_scoping.py:86: error: Type variable "generics_scoping.T" is unbound [valid-type]
generics_scoping.py:86: note: (Hint: Use "Generic[T]" or "Protocol[T]" base class to bind "T" inside a class)
generics_scoping.py:86: note: (Hint: Use "T" in function signature to bind "T" inside a function)
"""
18 changes: 18 additions & 0 deletions conformance/results/pyre/generics_basic.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
conformant = "Partial"
notes = """
False positives in examples using constrained type variables.
False negative in custom map example.
False positive using `iter`.
False negative for generic metaclass.
"""
output = """
generics_basic.py:31:4 Incompatible return type [7]: Expected `Variable[AnyStr <: [str, bytes]]` but got `bytes`.
generics_basic.py:31:15 Incompatible parameter type [6]: In call `bytes.__add__`, for 1st positional argument, expected `Union[array[typing.Any], bytearray, bytes, _CData, memoryview, mmap, PickleBuffer]` but got `Variable[AnyStr <: [str, bytes]]`.
generics_basic.py:36:14 Incompatible parameter type [6]: In call `concat`, for 2nd positional argument, expected `Variable[AnyStr <: [str, bytes]]` but got `bytes`.
generics_basic.py:37:14 Incompatible parameter type [6]: In call `concat`, for 2nd positional argument, expected `Variable[AnyStr <: [str, bytes]]` but got `str`.
generics_basic.py:44:0 Invalid type [31]: TypeVar can't have a single explicit constraint. Did you mean `bound=str`?
generics_basic.py:48:0 Invalid type [31]: Expression `Variable[BadConstraint2 <: [str, Variable[generics_basic.T]]]` is not a valid type. Type variables cannot contain other type variables in their constraints.
generics_basic.py:59:14 Incompatible parameter type [6]: In call `concat`, for 2nd positional argument, expected `Variable[AnyStr <: [str, bytes]]` but got `bytes`.
generics_basic.py:107:0 Duplicate type variables [59]: Duplicate type variable `T` in Generic[...].
generics_basic.py:161:25 Undefined attribute [16]: `typing.Iterator` has no attribute `__getitem__`.
"""
18 changes: 18 additions & 0 deletions conformance/results/pyre/generics_scoping.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
conformant = "Partial"
notes = """
False negative on generic class nested within generic function with same type variable.
False negative on generic class nested within generic class with same type variable.
"""
output = """
generics_scoping.py:25:9 Incompatible parameter type [6]: In call `MyClass.meth_2`, for 1st positional argument, expected `int` but got `str`.
generics_scoping.py:46:7 Invalid type variable [34]: The type variable `Variable[S]` isn't present in the function's parameters.
generics_scoping.py:50:13 Invalid type variable [34]: The current class isn't generic with respect to the type variable `Variable[S]`. To reference the type variable, you can modify the class to inherit from `typing.Generic[S]`.
generics_scoping.py:70:0 Uninitialized attribute [13]: Attribute `attr` is declared in class `Outer` to have type `Outer.Inner[Variable[T]]` but is never initialized.
generics_scoping.py:73:4 Uninitialized attribute [13]: Attribute `x` is declared in class `Outer.AlsoBad` to have type `typing.List[Variable[T]]` but is never initialized.
generics_scoping.py:74:11 Invalid type variable [34]: The current class isn't generic with respect to the type variable `Variable[T]`. To reference the type variable, you can modify the class to inherit from `typing.Generic[T]`.
generics_scoping.py:80:4 Incompatible attribute type [8]: Attribute `alias` declared in class `Outer` has type `TypeAlias` but is used as type `Type[List[Variable[_T]]]`.
generics_scoping.py:80:28 Incompatible parameter type [6]: In call `typing.GenericMeta.__getitem__`, for 1st positional argument, expected `Type[Variable[_T]]` but got `TypeVar`.
generics_scoping.py:84:13 Invalid type variable [34]: The type variable `Variable[T]` can only be used to annotate generic classes or functions.
generics_scoping.py:85:13 Invalid type variable [34]: The type variable `Variable[T]` can only be used to annotate generic classes or functions.
generics_scoping.py:86:5 Incompatible parameter type [6]: In call `typing.GenericMeta.__getitem__`, for 1st positional argument, expected `Type[Variable[_T]]` but got `TypeVar`.
"""
18 changes: 18 additions & 0 deletions conformance/results/pyright/generics_basic.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
conformant = "Pass"
output = """
generics_basic.py:36:15 - error: Argument of type "bytes" cannot be assigned to parameter "y" of type "AnyStr@concat" in function "concat"
  "bytes" is incompatible with "str" (reportGeneralTypeIssues)
generics_basic.py:37:15 - error: Argument of type "str" cannot be assigned to parameter "y" of type "AnyStr@concat" in function "concat"
  "str" is incompatible with "bytes" (reportGeneralTypeIssues)
generics_basic.py:44:44 - error: TypeVar must have at least two constrained types (reportGeneralTypeIssues)
generics_basic.py:48:49 - error: Type variable "T" has no meaning in this context (reportGeneralTypeIssues)
generics_basic.py:48:49 - error: TypeVar constraint type cannot be generic
generics_basic.py:59:15 - error: Argument of type "bytes" cannot be assigned to parameter "y" of type "AnyStr@concat" in function "concat"
  "bytes" is incompatible with "str" (reportGeneralTypeIssues)
generics_basic.py:107:24 - error: Type arguments for "Generic" must be unique
generics_basic.py:140:5 - error: Argument of type "Literal[0]" cannot be assigned to parameter "__key" of type "str" in function "__getitem__"
  "Literal[0]" is incompatible with "str" (reportGeneralTypeIssues)
generics_basic.py:141:5 - error: Argument of type "Literal[0]" cannot be assigned to parameter "__key" of type "str" in function "__getitem__"
  "Literal[0]" is incompatible with "str" (reportGeneralTypeIssues)
generics_basic.py:167:37 - error: Metaclass cannot be generic (reportGeneralTypeIssues)
"""
14 changes: 14 additions & 0 deletions conformance/results/pyright/generics_scoping.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
conformant = "Pass"
output = """
generics_scoping.py:25:10 - error: Argument of type "Literal['a']" cannot be assigned to parameter "x" of type "int" in function "meth_2"
  "Literal['a']" is incompatible with "int" (reportGeneralTypeIssues)
generics_scoping.py:46:13 - error: Type variable "S" has no meaning in this context (reportGeneralTypeIssues)
generics_scoping.py:50:19 - error: Type variable "S" has no meaning in this context (reportGeneralTypeIssues)
generics_scoping.py:61:29 - error: TypeVar "T" is already in use by an outer scope (reportGeneralTypeIssues)
generics_scoping.py:71:24 - error: TypeVar "T" is already in use by an outer scope (reportGeneralTypeIssues)
generics_scoping.py:74:17 - error: Type variable "T" has no meaning in this context (reportGeneralTypeIssues)
generics_scoping.py:80:5 - error: Generic type alias within class cannot use bound type variables T
generics_scoping.py:84:14 - error: Type variable "T" has no meaning in this context (reportGeneralTypeIssues)
generics_scoping.py:85:19 - error: Type variable "T" has no meaning in this context (reportGeneralTypeIssues)
generics_scoping.py:86:6 - error: Type variable "T" has no meaning in this context (reportGeneralTypeIssues)
"""
41 changes: 41 additions & 0 deletions conformance/results/pytype/generics_basic.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
conformant = "Partial"
notes = """
False positives in examples using constrained type variables.
False negative for generic metaclass.
"""
output = """
File "generics_basic.py", line 31, in concat: bad return type [bad-return-type]
Expected: MyStr
Actually returned: str
Called from (traceback):
line 57, in test_concat_subtype
File "generics_basic.py", line 36, in test_concat: Function concat was called with the wrong arguments [wrong-arg-types]
Expected: (x, y: str)
Actually passed: (x, y: bytes)
File "generics_basic.py", line 37, in test_concat: Function concat was called with the wrong arguments [wrong-arg-types]
Expected: (x, y: bytes)
Actually passed: (x, y: str)
File "generics_basic.py", line 44, in <module>: Invalid TypeVar: the number of constraints must be 0 or more than 1 [invalid-typevar]
File "generics_basic.py", line 48, in <module>: Invalid TypeVar: constraint cannot contain TypeVars [invalid-typevar]
File "generics_basic.py", line 57, in test_concat_subtype: MyStr [assert-type]
Expected: str
Actual: MyStr
File "generics_basic.py", line 58, in test_concat_subtype: Function concat was called with the wrong arguments [wrong-arg-types]
Expected: (x, y: MyStr)
Actually passed: (x, y: str)
File "generics_basic.py", line 58, in test_concat_subtype: Any [assert-type]
Expected: str
Actual: Any
File "generics_basic.py", line 59, in test_concat_subtype: Function concat was called with the wrong arguments [wrong-arg-types]
Expected: (x, y: MyStr)
Actually passed: (x, y: bytes)
File "generics_basic.py", line 107, in <module>: Invalid type annotation 'Generic' [invalid-annotation]
Parameters to Generic[...] must all be unique
File "generics_basic.py", line 140, in test_my_map: unsupported operand type(s) for item retrieval: MyMap1[str, int] and int [unsupported-operands]
Function __getitem__ on MyMap1[str, int] expects str
File "generics_basic.py", line 141, in test_my_map: unsupported operand type(s) for item retrieval: MyMap2[int, str] and int [unsupported-operands]
Function __getitem__ on MyMap2[int, str] expects str
File "generics_basic.py", line 161, in test_my_iterable_any: Iterator[nothing] [assert-type]
Expected: Iterator
Actual: Iterator[nothing]
"""
45 changes: 45 additions & 0 deletions conformance/results/pytype/generics_scoping.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
conformant = "Pass"
output = """
File "generics_scoping.py", line 9, in fun_1: bad return type [bad-return-type]
Expected: int
Actually returned: None
Called from (traceback):
line 12, in current file
File "generics_scoping.py", line 10, in fun_2: bad return type [bad-return-type]
Expected: str
Actually returned: None
Called from (traceback):
line 13, in current file
File "generics_scoping.py", line 20, in meth_1: bad return type [bad-return-type]
Expected: int
Actually returned: None
Called from (traceback):
line 24, in current file
File "generics_scoping.py", line 25, in <module>: Function MyClass.meth_2 was called with the wrong arguments [wrong-arg-types]
Expected: (self, x: int)
Actually passed: (self, x: str)
File "generics_scoping.py", line 35, in method: bad return type [bad-return-type]
Expected: str
Actually returned: None
Called from (traceback):
line 38, in current file
File "generics_scoping.py", line 35, in method: bad return type [bad-return-type]
Expected: bytes
Actually returned: None
Called from (traceback):
line 39, in current file
File "generics_scoping.py", line 46, in fun_3: Invalid type annotation 'List[S]' for z [invalid-annotation]
TypeVar(s) 'S' not in scope for method 'fun_3'
File "generics_scoping.py", line 50, in Bar: Invalid type annotation 'List[S]' for an_attr [invalid-annotation]
TypeVar(s) 'S' not in scope for class 'Bar'
File "generics_scoping.py", line 59, in fun_4: Invalid type annotation 'T' [invalid-annotation]
Function [fun_4] and its nested generic class [MyGeneric] cannot use the same type variable T
File "generics_scoping.py", line 70, in <module>: Invalid type annotation 'Outer' [invalid-annotation]
Generic class [Outer] and its nested generic class [Bad] cannot use the same type variable T.
File "generics_scoping.py", line 74, in AlsoBad: Invalid type annotation 'List[T]' for x [invalid-annotation]
TypeVar(s) 'T' not in scope for class 'Outer.AlsoBad'
File "generics_scoping.py", line 84, in <module>: Invalid type annotation 'T' for global_var1 [invalid-annotation]
TypeVar(s) 'T' not in scope
File "generics_scoping.py", line 85, in <module>: Invalid type annotation 'List[T]' for global_var2 [invalid-annotation]
TypeVar(s) 'T' not in scope
"""
168 changes: 168 additions & 0 deletions conformance/tests/generics_basic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
"""
Tests for basic usage of generics.
"""

# Specification: https://typing.readthedocs.io/en/latest/spec/generics.html#introduction

from __future__ import annotations

from collections.abc import Sequence
from typing import Any, Generic, TypeVar, assert_type

T = TypeVar('T')

# > Generics can be parameterized by using a factory available in
# > ``typing`` called ``TypeVar``.

def first(l: Sequence[T]) -> T:
return l[0]


def test_first(seq_int: Sequence[int], seq_str: Sequence[str]) -> None:
assert_type(first(seq_int), int)
assert_type(first(seq_str), str)

# > ``TypeVar`` supports constraining parametric types to a fixed set of
# > possible types

AnyStr = TypeVar('AnyStr', str, bytes)

def concat(x: AnyStr, y: AnyStr) -> AnyStr:
return x + y

def test_concat(s: str, b: bytes, a: Any) -> None:
concat(s, s) # OK
concat(b, b) # OK
concat(s, b) # Type error
concat(b, s) # Type error

concat(s, a) # OK
concat(a, b) # OK

# > Specifying a single constraint is disallowed.

BadConstraint1 = TypeVar('BadConstraint1', str) # Type error

# > Note: those types cannot be parameterized by type variables

BadConstraint2 = TypeVar('BadConstraint2', str, T) # Type error

# > Subtypes of types constrained by a type variable should be treated
# > as their respective explicitly listed base types in the context of the
# > type variable.

class MyStr(str): ...

def test_concat_subtype(s: str, b: bytes, a: Any, m: MyStr) -> None:
assert_type(concat(m, m), str)
assert_type(concat(m, s), str)
concat(m, b) # Type error

# TODO: should these be str or Any?
# reveal_type(concat(m, a))
Copy link
Collaborator

Choose a reason for hiding this comment

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

This gets into constraint-solving behavior which is underspecified, but I'm still surprised to see that mypy generates an Any in this case. That's not what I would expect, and it seems inconsistent with mypy's handling of non-constrained TypeVars.

# reveal_type(concat(a, m))

# Specification: https://typing.readthedocs.io/en/latest/spec/generics.html#user-defined-generic-classes

# > You can include a ``Generic`` base class to define a user-defined class
# > as generic.

from logging import Logger
from collections.abc import Iterable

class LoggedVar(Generic[T]):
def __init__(self, value: T, name: str, logger: Logger) -> None:
self.name = name
self.logger = logger
self.value = value

def set(self, new: T) -> None:
self.log('Set ' + repr(self.value))
self.value = new

def get(self) -> T:
self.log('Get ' + repr(self.value))
return self.value

def log(self, message: str) -> None:
self.logger.info('{}: {}'.format(self.name, message))


def zero_all_vars(vars: Iterable[LoggedVar[int]]) -> None:
for var in vars:
var.set(0)
assert_type(var.get(), int)


# > A generic type can have any number of type variables, and type variables
# > may be constrained.

S = TypeVar('S')

class Pair1(Generic[T, S]):
...

# > Each type variable argument to ``Generic`` must be distinct.

class Pair2(Generic[T, T]): # Type error
...

# > The ``Generic[T]`` base class is redundant in simple cases where you
# > subclass some other generic class and specify type variables for its
# > parameters.

from collections.abc import Iterator, Mapping

class MyIter1(Iterator[T]):
...

class MyIter2(Iterator[T], Generic[T]):
...

def test_my_iter(m1: MyIter1[int], m2: MyIter2[int]):
assert_type(next(m1), int)
assert_type(next(m2), int)


K = TypeVar("K")
V = TypeVar("V")

class MyMap1(Mapping[K, V], Generic[K, V]):
...

class MyMap2(Mapping[K, V], Generic[V, K]):
...

def test_my_map(m1: MyMap1[str, int], m2: MyMap2[int, str]):
assert_type(m1["key"], int)
assert_type(m2["key"], int)

m1[0] # Type error
m2[0] # Type error

# > You can use multiple inheritance with ``Generic``

from collections.abc import Sized, Container

class LinkedList(Sized, Generic[T]):
...

class MyMapping(Iterable[tuple[K, V]], Container[tuple[K, V]], Generic[K, V]):
...

# > Subclassing a generic class without specifying type parameters assumes
# > ``Any`` for each position. In the following example, ``MyIterable``
# > is not generic but implicitly inherits from ``Iterable[Any]``::

class MyIterableAny(Iterable): # Same as Iterable[Any]
...

def test_my_iterable_any(m: MyIterableAny):
assert_type(iter(m), Iterator[Any])

# > Generic metaclasses are not supported

class GenericMeta(type, Generic[T]): ...

class GenericMetaInstance(metaclass=GenericMeta[T]): # Type error
...
Copy link
Collaborator

Choose a reason for hiding this comment

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

It may also be useful to add tests for accessing a TypeVar as a runtime object — to make sure that this isn't disallowed.

assert_type(T.__name__, str)
assert_type(T.__bound__, Any | None)
T.other # Type error

runtime_typevar: TypeVar = T # OK

Loading