Skip to content

Commit

Permalink
Fix new style union syntax in type aliases (#14008)
Browse files Browse the repository at this point in the history
Fix Python 3.10 `|` union syntax in type aliases, when one of
the operands is a type alias or a type with an overloaded `__init__`.

We can now infer `typing._SpecialForm` for type aliases in a runtime 
context.

Also create a bunch of minimal test-only stubs for stdlib modules 
to fix some test failures caused by the missing `typing._SpecialForm`
in the default test stubs. This is generally what we want in any case, 
since using typeshed stubs with minimal builtins/typing stubs can
result in unpredictable behavior and slow tests.

Fixes #12368. Fixes #12005. Fixes #11426.
  • Loading branch information
JukkaL authored Nov 7, 2022
1 parent d2a3e66 commit 39d35cd
Show file tree
Hide file tree
Showing 21 changed files with 186 additions and 27 deletions.
8 changes: 4 additions & 4 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -6048,11 +6048,11 @@ def lookup_qualified(self, name: str) -> SymbolTableNode:
last = parts[-1]
if last in n.names:
return n.names[last]
elif len(parts) == 2 and parts[0] == "builtins":
fullname = "builtins." + last
elif len(parts) == 2 and parts[0] in ("builtins", "typing"):
fullname = ".".join(parts)
if fullname in SUGGESTED_TEST_FIXTURES:
suggestion = ", e.g. add '[builtins fixtures/{}]' to your test".format(
SUGGESTED_TEST_FIXTURES[fullname]
suggestion = ", e.g. add '[{} fixtures/{}]' to your test".format(
parts[0], SUGGESTED_TEST_FIXTURES[fullname]
)
else:
suggestion = ""
Expand Down
7 changes: 3 additions & 4 deletions mypy/checkexpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -3870,9 +3870,8 @@ class LongName(Generic[T]): ...
else:
if alias_definition:
return AnyType(TypeOfAny.special_form)
# This type is invalid in most runtime contexts, give it an 'object' type.
# TODO: Use typing._SpecialForm instead?
return self.named_type("builtins.object")
# The _SpecialForm type can be used in some runtime contexts (e.g. it may have __or__).
return self.named_type("typing._SpecialForm")

def apply_type_arguments_to_callable(
self, tp: Type, args: Sequence[Type], ctx: Context
Expand Down Expand Up @@ -4742,7 +4741,7 @@ def has_member(self, typ: Type, member: str) -> bool:
typ = typ.fallback
if isinstance(typ, Instance):
return typ.type.has_readable_member(member)
if isinstance(typ, CallableType) and typ.is_type_obj():
if isinstance(typ, FunctionLike) and typ.is_type_obj():
return typ.fallback.type.has_readable_member(member)
elif isinstance(typ, AnyType):
return True
Expand Down
4 changes: 4 additions & 0 deletions mypy/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@
"builtins.isinstance": "isinstancelist.pyi",
"builtins.property": "property.pyi",
"builtins.classmethod": "classmethod.pyi",
"typing._SpecialForm": "typing-medium.pyi",
}


Expand Down Expand Up @@ -2253,6 +2254,9 @@ def format_literal_value(typ: LiteralType) -> str:
if itype.extra_attrs and itype.extra_attrs.mod_name and module_names:
return f"{base_str} {itype.extra_attrs.mod_name}"
return base_str
if itype.type.fullname == "typing._SpecialForm":
# This is not a real type but used for some typing-related constructs.
return "<typing special form>"
if verbosity >= 2 or (fullnames and itype.type.fullname in fullnames):
base_str = itype.type.fullname
else:
Expand Down
3 changes: 3 additions & 0 deletions mypyc/test-data/run-async.test
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ async def g() -> int:
async def f() -> int:
return await g()

[file asyncio/__init__.pyi]
async def sleep(t: float) -> None: ...

[typing fixtures/typing-full.pyi]

[file driver.py]
Expand Down
13 changes: 13 additions & 0 deletions test-data/unit/check-ctypes.test
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ a[3] = b"bytes" # E: No overload variant of "__setitem__" of "Array" matches ar
for x in a:
reveal_type(x) # N: Revealed type is "builtins.int"
[builtins fixtures/floatdict.pyi]
[typing fixtures/typing-medium.pyi]

[case testCtypesArrayCustomElementType]
import ctypes
Expand Down Expand Up @@ -52,6 +53,7 @@ myu: Union[ctypes.Array[ctypes.c_int], List[str]]
for myi in myu:
reveal_type(myi) # N: Revealed type is "Union[builtins.int, builtins.str]"
[builtins fixtures/floatdict.pyi]
[typing fixtures/typing-medium.pyi]

[case testCtypesArrayUnionElementType]
import ctypes
Expand All @@ -76,6 +78,7 @@ mya[3] = b"bytes" # E: No overload variant of "__setitem__" of "Array" matches
for myx in mya:
reveal_type(myx) # N: Revealed type is "Union[__main__.MyCInt, builtins.int]"
[builtins fixtures/floatdict.pyi]
[typing fixtures/typing-medium.pyi]

[case testCtypesCharArrayAttrs]
import ctypes
Expand All @@ -84,13 +87,15 @@ ca = (ctypes.c_char * 4)(b'a', b'b', b'c', b'\x00')
reveal_type(ca.value) # N: Revealed type is "builtins.bytes"
reveal_type(ca.raw) # N: Revealed type is "builtins.bytes"
[builtins fixtures/floatdict.pyi]
[typing fixtures/typing-medium.pyi]

[case testCtypesCharPArrayDoesNotCrash]
import ctypes

# The following line used to crash with "Could not find builtin symbol 'NoneType'"
ca = (ctypes.c_char_p * 0)()
[builtins fixtures/floatdict.pyi]
[typing fixtures/typing-medium.pyi]

[case testCtypesWcharArrayAttrs]
import ctypes
Expand All @@ -99,6 +104,7 @@ wca = (ctypes.c_wchar * 4)('a', 'b', 'c', '\x00')
reveal_type(wca.value) # N: Revealed type is "builtins.str"
wca.raw # E: Array attribute "raw" is only available with element type "c_char", not "c_wchar"
[builtins fixtures/floatdict.pyi]
[typing fixtures/typing-medium.pyi]

[case testCtypesCharUnionArrayAttrs]
import ctypes
Expand All @@ -108,6 +114,7 @@ cua: ctypes.Array[Union[ctypes.c_char, ctypes.c_wchar]]
reveal_type(cua.value) # N: Revealed type is "Union[builtins.bytes, builtins.str]"
cua.raw # E: Array attribute "raw" is only available with element type "c_char", not "Union[c_char, c_wchar]"
[builtins fixtures/floatdict.pyi]
[typing fixtures/typing-medium.pyi]

[case testCtypesAnyUnionArrayAttrs]
import ctypes
Expand All @@ -117,6 +124,7 @@ caa: ctypes.Array[Union[ctypes.c_char, Any]]
reveal_type(caa.value) # N: Revealed type is "Union[builtins.bytes, Any]"
reveal_type(caa.raw) # N: Revealed type is "builtins.bytes"
[builtins fixtures/floatdict.pyi]
[typing fixtures/typing-medium.pyi]

[case testCtypesOtherUnionArrayAttrs]
import ctypes
Expand All @@ -126,6 +134,7 @@ cua: ctypes.Array[Union[ctypes.c_char, ctypes.c_int]]
cua.value # E: Array attribute "value" is only available with element type "c_char" or "c_wchar", not "Union[c_char, c_int]"
cua.raw # E: Array attribute "raw" is only available with element type "c_char", not "Union[c_char, c_int]"
[builtins fixtures/floatdict.pyi]
[typing fixtures/typing-medium.pyi]

[case testCtypesAnyArrayAttrs]
import ctypes
Expand All @@ -134,6 +143,7 @@ aa: ctypes.Array[Any]
reveal_type(aa.value) # N: Revealed type is "Any"
reveal_type(aa.raw) # N: Revealed type is "builtins.bytes"
[builtins fixtures/floatdict.pyi]
[typing fixtures/typing-medium.pyi]

[case testCtypesOtherArrayAttrs]
import ctypes
Expand All @@ -142,6 +152,7 @@ oa = (ctypes.c_int * 4)(1, 2, 3, 4)
oa.value # E: Array attribute "value" is only available with element type "c_char" or "c_wchar", not "c_int"
oa.raw # E: Array attribute "raw" is only available with element type "c_char", not "c_int"
[builtins fixtures/floatdict.pyi]
[typing fixtures/typing-medium.pyi]

[case testCtypesArrayConstructorStarargs]
import ctypes
Expand All @@ -154,6 +165,7 @@ reveal_type(intarr4(*int_values)) # N: Revealed type is "ctypes.Array[ctypes.c_
reveal_type(intarr4(*c_int_values)) # N: Revealed type is "ctypes.Array[ctypes.c_int]"
reveal_type(intarr6(1, ctypes.c_int(2), *int_values)) # N: Revealed type is "ctypes.Array[ctypes.c_int]"
reveal_type(intarr6(1, ctypes.c_int(2), *c_int_values)) # N: Revealed type is "ctypes.Array[ctypes.c_int]"
[typing fixtures/typing-medium.pyi]

float_values = [1.0, 2.0, 3.0, 4.0]
intarr4(*float_values) # E: Array constructor argument 1 of type "List[float]" is not convertible to the array element type "Iterable[c_int]"
Expand All @@ -167,3 +179,4 @@ x = {"a": 1, "b": 2}
intarr4(**x)

[builtins fixtures/floatdict.pyi]
[typing fixtures/typing-medium.pyi]
3 changes: 2 additions & 1 deletion test-data/unit/check-dataclasses.test
Original file line number Diff line number Diff line change
Expand Up @@ -633,8 +633,9 @@ class Two:

c = Two()
x = c.S
reveal_type(x) # N: Revealed type is "builtins.object"
reveal_type(x) # N: Revealed type is "typing._SpecialForm"
[builtins fixtures/dataclasses.pyi]
[typing fixtures/typing-medium.pyi]

[case testDataclassOrdering]
# flags: --python-version 3.7
Expand Down
8 changes: 4 additions & 4 deletions test-data/unit/check-generics.test
Original file line number Diff line number Diff line change
Expand Up @@ -1049,20 +1049,20 @@ CA = Callable[[T], int]
TA = Tuple[T, int]
UA = Union[T, int]

cs = CA + 1 # E: Unsupported left operand type for + ("object")
cs = CA + 1 # E: Unsupported left operand type for + ("<typing special form>")
reveal_type(cs) # N: Revealed type is "Any"

ts = TA() # E: "object" not callable
ts = TA() # E: "<typing special form>" not callable
reveal_type(ts) # N: Revealed type is "Any"

us = UA.x # E: "object" has no attribute "x"
us = UA.x # E: "<typing special form>" has no attribute "x"
reveal_type(us) # N: Revealed type is "Any"

xx = CA[str] + 1 # E: Type application is only supported for generic classes
yy = TA[str]() # E: Type application is only supported for generic classes
zz = UA[str].x # E: Type application is only supported for generic classes
[builtins fixtures/tuple.pyi]

[typing fixtures/typing-medium.pyi]
[out]

[case testGenericTypeAliasesTypeVarBinding]
Expand Down
5 changes: 3 additions & 2 deletions test-data/unit/check-literal.test
Original file line number Diff line number Diff line change
Expand Up @@ -1484,16 +1484,17 @@ Alias = Literal[3]

isinstance(3, Literal[3]) # E: Cannot use isinstance() with Literal type
isinstance(3, Alias) # E: Cannot use isinstance() with Literal type \
# E: Argument 2 to "isinstance" has incompatible type "object"; expected "Union[type, Tuple[Any, ...]]"
# E: Argument 2 to "isinstance" has incompatible type "<typing special form>"; expected "Union[type, Tuple[Any, ...]]"
isinstance(3, Renamed[3]) # E: Cannot use isinstance() with Literal type
isinstance(3, indirect.Literal[3]) # E: Cannot use isinstance() with Literal type

issubclass(int, Literal[3]) # E: Cannot use issubclass() with Literal type
issubclass(int, Alias) # E: Cannot use issubclass() with Literal type \
# E: Argument 2 to "issubclass" has incompatible type "object"; expected "Union[type, Tuple[Any, ...]]"
# E: Argument 2 to "issubclass" has incompatible type "<typing special form>"; expected "Union[type, Tuple[Any, ...]]"
issubclass(int, Renamed[3]) # E: Cannot use issubclass() with Literal type
issubclass(int, indirect.Literal[3]) # E: Cannot use issubclass() with Literal type
[builtins fixtures/isinstancelist.pyi]
[typing fixtures/typing-medium.pyi]
[out]

[case testLiteralErrorsWhenSubclassed]
Expand Down
16 changes: 16 additions & 0 deletions test-data/unit/check-python310.test
Original file line number Diff line number Diff line change
Expand Up @@ -1788,3 +1788,19 @@ def f6(a: object) -> None:
case _ if y is not None: # E: Name "y" may be undefined
pass
[builtins fixtures/tuple.pyi]

[case testTypeAliasWithNewUnionSyntaxAndNoneLeftOperand]
from typing import overload
class C:
@overload
def __init__(self) -> None: pass
@overload
def __init__(self, x: int) -> None: pass
def __init__(self, x=0):
pass

class D: pass

X = None | C
Y = None | D
[builtins fixtures/type.pyi]
26 changes: 14 additions & 12 deletions test-data/unit/check-type-aliases.test
Original file line number Diff line number Diff line change
Expand Up @@ -821,28 +821,28 @@ c = Child()

reveal_type(NormalImplicit) # N: Revealed type is "def () -> __main__.Foo"
reveal_type(NormalExplicit) # N: Revealed type is "def () -> __main__.Foo"
reveal_type(SpecialImplicit) # N: Revealed type is "builtins.object"
reveal_type(SpecialExplicit) # N: Revealed type is "builtins.object"
reveal_type(SpecialImplicit) # N: Revealed type is "typing._SpecialForm"
reveal_type(SpecialExplicit) # N: Revealed type is "typing._SpecialForm"

reveal_type(Parent.NormalImplicit) # N: Revealed type is "def () -> __main__.Foo"
reveal_type(Parent.NormalExplicit) # N: Revealed type is "def () -> __main__.Foo"
reveal_type(Parent.SpecialImplicit) # N: Revealed type is "builtins.object"
reveal_type(Parent.SpecialExplicit) # N: Revealed type is "builtins.object"
reveal_type(Parent.SpecialImplicit) # N: Revealed type is "typing._SpecialForm"
reveal_type(Parent.SpecialExplicit) # N: Revealed type is "typing._SpecialForm"

reveal_type(Child.NormalImplicit) # N: Revealed type is "def () -> __main__.Foo"
reveal_type(Child.NormalExplicit) # N: Revealed type is "def () -> __main__.Foo"
reveal_type(Child.SpecialImplicit) # N: Revealed type is "builtins.object"
reveal_type(Child.SpecialExplicit) # N: Revealed type is "builtins.object"
reveal_type(Child.SpecialImplicit) # N: Revealed type is "typing._SpecialForm"
reveal_type(Child.SpecialExplicit) # N: Revealed type is "typing._SpecialForm"

reveal_type(p.NormalImplicit) # N: Revealed type is "def () -> __main__.Foo"
reveal_type(p.NormalExplicit) # N: Revealed type is "def () -> __main__.Foo"
reveal_type(p.SpecialImplicit) # N: Revealed type is "builtins.object"
reveal_type(p.SpecialExplicit) # N: Revealed type is "builtins.object"
reveal_type(p.SpecialImplicit) # N: Revealed type is "typing._SpecialForm"
reveal_type(p.SpecialExplicit) # N: Revealed type is "typing._SpecialForm"

reveal_type(c.NormalImplicit) # N: Revealed type is "def () -> __main__.Foo"
reveal_type(p.NormalExplicit) # N: Revealed type is "def () -> __main__.Foo"
reveal_type(c.SpecialImplicit) # N: Revealed type is "builtins.object"
reveal_type(c.SpecialExplicit) # N: Revealed type is "builtins.object"
reveal_type(c.SpecialImplicit) # N: Revealed type is "typing._SpecialForm"
reveal_type(c.SpecialExplicit) # N: Revealed type is "typing._SpecialForm"

# Use type aliases in a type alias context in a plausible way

Expand Down Expand Up @@ -895,6 +895,7 @@ reveal_type(weird_child_2) # N: Revealed type is "def () -> Any"
reveal_type(weird_child_3) # N: Revealed type is "def () -> Any"
reveal_type(weird_child_4) # N: Revealed type is "def () -> Any"
[builtins fixtures/tuple.pyi]
[typing fixtures/typing-medium.pyi]

[case testMalformedTypeAliasRuntimeReassignments]
from typing import Union
Expand Down Expand Up @@ -927,8 +928,8 @@ SpecialExplicit = 4 # E: Cannot assign multiple types to name "SpecialExplicit"

Parent.NormalImplicit = 4 # E: Incompatible types in assignment (expression has type "int", variable has type "Type[Foo]")
Parent.NormalExplicit = 4 # E: Incompatible types in assignment (expression has type "int", variable has type "Type[Foo]")
Parent.SpecialImplicit = 4
Parent.SpecialExplicit = 4
Parent.SpecialImplicit = 4 # E: Incompatible types in assignment (expression has type "int", variable has type "<typing special form>")
Parent.SpecialExplicit = 4 # E: Incompatible types in assignment (expression has type "int", variable has type "<typing special form>")

Child.NormalImplicit = 4 # E: Incompatible types in assignment (expression has type "int", variable has type "Type[Foo]")
Child.NormalExplicit = 4 # E: Incompatible types in assignment (expression has type "int", variable has type "Type[Foo]")
Expand All @@ -945,3 +946,4 @@ c.NormalExplicit = 4 # E: Incompatible types in assignment (expression has type
c.SpecialImplicit = 4
c.SpecialExplicit = 4
[builtins fixtures/tuple.pyi]
[typing fixtures/typing-medium.pyi]
1 change: 1 addition & 0 deletions test-data/unit/fixtures/args.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ class list(Sequence[T], Generic[T]): pass

class int:
def __eq__(self, o: object) -> bool: pass
class float: pass
class str: pass
class bytes: pass
class bool: pass
Expand Down
1 change: 1 addition & 0 deletions test-data/unit/fixtures/type.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ class list(Generic[T]): pass
class type(Generic[T]):
__name__: str
def __or__(self, other: Union[type, None]) -> type: pass
def __ror__(self, other: Union[type, None]) -> type: pass
def mro(self) -> List['type']: pass

class tuple(Generic[T]): pass
Expand Down
2 changes: 2 additions & 0 deletions test-data/unit/fixtures/typing-full.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -179,3 +179,5 @@ class _TypedDict(Mapping[str, object]):
def pop(self, k: NoReturn, default: T = ...) -> object: ...
def update(self: T, __m: T) -> None: ...
def __delitem__(self, k: NoReturn) -> None: ...

class _SpecialForm: pass
2 changes: 2 additions & 0 deletions test-data/unit/fixtures/typing-medium.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -68,4 +68,6 @@ class ContextManager(Generic[T]):
# Use Any because not all the precise types are in the fixtures.
def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> Any: pass

class _SpecialForm: pass

TYPE_CHECKING = 1
4 changes: 4 additions & 0 deletions test-data/unit/lib-stub/_decimal.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Very simplified decimal stubs for use in tests

class Decimal:
def __new__(cls, value: str = ...) -> Decimal: ...
16 changes: 16 additions & 0 deletions test-data/unit/lib-stub/datetime.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Very simplified datetime stubs for use in tests

class datetime:
def __new__(
cls,
year: int,
month: int,
day: int,
hour: int = ...,
minute: int = ...,
second: int = ...,
microsecond: int = ...,
*,
fold: int = ...,
) -> datetime: ...
def __format__(self, __fmt: str) -> str: ...
3 changes: 3 additions & 0 deletions test-data/unit/lib-stub/decimal.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Very simplified decimal stubs for use in tests

from _decimal import *
35 changes: 35 additions & 0 deletions test-data/unit/lib-stub/functools.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
from typing import Generic, TypeVar, Callable, Any, Mapping

_T = TypeVar("_T")

class _SingleDispatchCallable(Generic[_T]):
registry: Mapping[Any, Callable[..., _T]]
def dispatch(self, cls: Any) -> Callable[..., _T]: ...
# @fun.register(complex)
# def _(arg, verbose=False): ...
@overload
def register(self, cls: type[Any], func: None = ...) -> Callable[[Callable[..., _T]], Callable[..., _T]]: ...
# @fun.register
# def _(arg: int, verbose=False):
@overload
def register(self, cls: Callable[..., _T], func: None = ...) -> Callable[..., _T]: ...
# fun.register(int, lambda x: x)
@overload
def register(self, cls: type[Any], func: Callable[..., _T]) -> Callable[..., _T]: ...
def _clear_cache(self) -> None: ...
def __call__(__self, *args: Any, **kwargs: Any) -> _T: ...

def singledispatch(func: Callable[..., _T]) -> _SingleDispatchCallable[_T]: ...

def total_ordering(cls: type[_T]) -> type[_T]: ...

class cached_property(Generic[_T]):
func: Callable[[Any], _T]
attrname: str | None
def __init__(self, func: Callable[[Any], _T]) -> None: ...
@overload
def __get__(self, instance: None, owner: type[Any] | None = ...) -> cached_property[_T]: ...
@overload
def __get__(self, instance: object, owner: type[Any] | None = ...) -> _T: ...
def __set_name__(self, owner: type[Any], name: str) -> None: ...
def __class_getitem__(cls, item: Any) -> Any: ...
3 changes: 3 additions & 0 deletions test-data/unit/lib-stub/traceback.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Very simplified traceback stubs for use in tests

def print_tb(*args, **kwargs) -> None: ...
Loading

0 comments on commit 39d35cd

Please sign in to comment.