diff --git a/mypy/argmap.py b/mypy/argmap.py index 2f786bf17d28..788a0c744da2 100644 --- a/mypy/argmap.py +++ b/mypy/argmap.py @@ -1,8 +1,8 @@ """Utilities for mapping between actual and formal arguments (and their types).""" -from typing import List, Optional, Sequence, Callable +from typing import List, Optional, Sequence, Callable, Set -from mypy.types import Type, Instance, TupleType, AnyType, TypeOfAny +from mypy.types import Type, Instance, TupleType, AnyType, TypeOfAny, TypedDictType from mypy import nodes @@ -65,13 +65,24 @@ def map_actuals_to_formals(caller_kinds: List[int], map[callee_kinds.index(nodes.ARG_STAR2)].append(i) else: assert kind == nodes.ARG_STAR2 - for j in range(ncallee): - # TODO tuple varargs complicate this - no_certain_match = ( - not map[j] or caller_kinds[map[j][0]] == nodes.ARG_STAR) - if ((callee_names[j] and no_certain_match) - or callee_kinds[j] == nodes.ARG_STAR2): - map[j].append(i) + argt = caller_arg_type(i) + if isinstance(argt, TypedDictType): + for name, value in argt.items.items(): + if name in callee_names: + map[callee_names.index(name)].append(i) + elif nodes.ARG_STAR2 in callee_kinds: + map[callee_kinds.index(nodes.ARG_STAR2)].append(i) + else: + # We don't exactly know which **kwargs are provided by the + # caller. Assume that they will fill the remaining arguments. + for j in range(ncallee): + # TODO: If there are also tuple varargs, we might be missing some potential + # matches if the tuple was short enough to not match everything. + no_certain_match = ( + not map[j] or caller_kinds[map[j][0]] == nodes.ARG_STAR) + if ((callee_names[j] and no_certain_match) + or callee_kinds[j] == nodes.ARG_STAR2): + map[j].append(i) return map @@ -95,35 +106,87 @@ def map_formals_to_actuals(caller_kinds: List[int], return actual_to_formal -def get_actual_type(arg_type: Type, kind: int, - tuple_counter: List[int]) -> Type: - """Return the type of an actual argument with the given kind. +class ArgTypeExpander: + """Utility class for mapping actual argument types to formal arguments. + + One of the main responsibilities is to expand caller tuple *args and TypedDict + **kwargs, and to keep track of which tuple/TypedDict items have already been + consumed. + + Example: + + def f(x: int, *args: str) -> None: ... + f(*(1, 'x', 1.1)) + + We'd call expand_actual_type three times: - If the argument is a *arg, return the individual argument item. + 1. The first call would provide 'int' as the actual type of 'x' (from '1'). + 2. The second call would provide 'str' as one of the actual types for '*args'. + 2. The third call would provide 'float' as one of the actual types for '*args'. + + A single instance can process all the arguments for a single call. Each call + needs a separate instance since instances have per-call state. """ - if kind == nodes.ARG_STAR: - if isinstance(arg_type, Instance): - if arg_type.type.fullname() == 'builtins.list': - # List *arg. - return arg_type.args[0] - elif arg_type.args: - # TODO try to map type arguments to Iterable - return arg_type.args[0] + def __init__(self) -> None: + # Next tuple *args index to use. + self.tuple_index = 0 + # Keyword arguments in TypedDict **kwargs used. + self.kwargs_used = set() # type: Set[str] + + def expand_actual_type(self, + actual_type: Type, + actual_kind: int, + formal_name: Optional[str], + formal_kind: int) -> Type: + """Return the actual (caller) type(s) of a formal argument with the given kinds. + + If the actual argument is a tuple *args, return the next individual tuple item that + maps to the formal arg. + + If the actual argument is a TypedDict **kwargs, return the next matching typed dict + value type based on formal argument name and kind. + + This is supposed to be called for each formal, in order. Call multiple times per + formal if multiple actuals map to a formal. + """ + if actual_kind == nodes.ARG_STAR: + if isinstance(actual_type, Instance): + if actual_type.type.fullname() == 'builtins.list': + # List *arg. + return actual_type.args[0] + elif actual_type.args: + # TODO: Try to map type arguments to Iterable + return actual_type.args[0] + else: + return AnyType(TypeOfAny.from_error) + elif isinstance(actual_type, TupleType): + # Get the next tuple item of a tuple *arg. + if self.tuple_index >= len(actual_type.items): + # Exhausted a tuple -- continue to the next *args. + self.tuple_index = 1 + else: + self.tuple_index += 1 + return actual_type.items[self.tuple_index - 1] + else: + return AnyType(TypeOfAny.from_error) + elif actual_kind == nodes.ARG_STAR2: + if isinstance(actual_type, TypedDictType): + if formal_kind != nodes.ARG_STAR2 and formal_name in actual_type.items: + # Lookup type based on keyword argument name. + assert formal_name is not None + else: + # Pick an arbitrary item if no specified keyword is expected. + formal_name = (set(actual_type.items.keys()) - self.kwargs_used).pop() + self.kwargs_used.add(formal_name) + return actual_type.items[formal_name] + elif (isinstance(actual_type, Instance) + and (actual_type.type.fullname() == 'builtins.dict')): + # Dict **arg. + # TODO: Handle arbitrary Mapping + return actual_type.args[1] else: return AnyType(TypeOfAny.from_error) - elif isinstance(arg_type, TupleType): - # Get the next tuple item of a tuple *arg. - tuple_counter[0] += 1 - return arg_type.items[tuple_counter[0] - 1] - else: - return AnyType(TypeOfAny.from_error) - elif kind == nodes.ARG_STAR2: - if isinstance(arg_type, Instance) and (arg_type.type.fullname() == 'builtins.dict'): - # Dict **arg. TODO more general (Mapping) - return arg_type.args[1] else: - return AnyType(TypeOfAny.from_error) - else: - # No translation for other kinds. - return arg_type + # No translation for other kinds -- 1:1 mapping. + return actual_type diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index d049098ff1bb..45e302f6595b 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -51,7 +51,7 @@ from mypy import applytype from mypy import erasetype from mypy.checkmember import analyze_member_access, type_object_type -from mypy.argmap import get_actual_type, map_actuals_to_formals, map_formals_to_actuals +from mypy.argmap import ArgTypeExpander, map_actuals_to_formals, map_formals_to_actuals from mypy.checkstrformat import StringFormatterChecker from mypy.expandtype import expand_type, expand_type_by_instance, freshen_function_type_vars from mypy.util import split_module_names @@ -704,8 +704,7 @@ def check_call(self, callee: Type, args: List[Expression], self.check_argument_count(callee, arg_types, arg_kinds, arg_names, formal_to_actual, context, self.msg) - self.check_argument_types(arg_types, arg_kinds, callee, - formal_to_actual, context, + self.check_argument_types(arg_types, arg_kinds, callee, formal_to_actual, context, messages=arg_messages) if (callee.is_type_obj() and (len(arg_types) == 1) @@ -1049,7 +1048,9 @@ def apply_inferred_arguments(self, callee_type: CallableType, # arguments. return self.apply_generic_arguments(callee_type, inferred_args, context) - def check_argument_count(self, callee: CallableType, actual_types: List[Type], + def check_argument_count(self, + callee: CallableType, + actual_types: List[Type], actual_kinds: List[int], actual_names: Optional[Sequence[Optional[str]]], formal_to_actual: List[List[int]], @@ -1062,54 +1063,27 @@ def check_argument_count(self, callee: CallableType, actual_types: List[Type], Return False if there were any errors. Otherwise return True """ + if messages: + assert context, "Internal error: messages given without context" + elif context is None: + context = TempNode(AnyType(TypeOfAny.special_form)) # Avoid "is None" checks + # TODO(jukka): We could return as soon as we find an error if messages is None. - formal_kinds = callee.arg_kinds # Collect list of all actual arguments matched to formal arguments. all_actuals = [] # type: List[int] for actuals in formal_to_actual: all_actuals.extend(actuals) - is_unexpected_arg_error = False # Keep track of errors to avoid duplicate errors. - ok = True # False if we've found any error. - for i, kind in enumerate(actual_kinds): - if i not in all_actuals and ( - kind != nodes.ARG_STAR or - not is_empty_tuple(actual_types[i])): - # Extra actual: not matched by a formal argument. - ok = False - if kind != nodes.ARG_NAMED: - if messages: - assert context, "Internal error: messages given without context" - messages.too_many_arguments(callee, context) - else: - if messages: - assert context, "Internal error: messages given without context" - assert actual_names, "Internal error: named kinds without names given" - act_name = actual_names[i] - assert act_name is not None - messages.unexpected_keyword_argument( - callee, act_name, context) - is_unexpected_arg_error = True - elif kind == nodes.ARG_STAR and ( - nodes.ARG_STAR not in formal_kinds): - actual_type = actual_types[i] - if isinstance(actual_type, TupleType): - if all_actuals.count(i) < len(actual_type.items): - # Too many tuple items as some did not match. - if messages: - assert context, "Internal error: messages given without context" - messages.too_many_arguments(callee, context) - ok = False - # *args can be applied even if the function takes a fixed - # number of positional arguments. This may succeed at runtime. + ok, is_unexpected_arg_error = self.check_for_extra_actual_arguments( + callee, actual_types, actual_kinds, actual_names, all_actuals, context, messages) - for i, kind in enumerate(formal_kinds): + # Check for too many or few values for formals. + for i, kind in enumerate(callee.arg_kinds): if kind == nodes.ARG_POS and (not formal_to_actual[i] and not is_unexpected_arg_error): # No actual for a mandatory positional formal. if messages: - assert context, "Internal error: messages given without context" messages.too_few_arguments(callee, context, actual_names) ok = False elif kind == nodes.ARG_NAMED and (not formal_to_actual[i] and @@ -1118,7 +1092,6 @@ def check_argument_count(self, callee: CallableType, actual_types: List[Type], if messages: argname = callee.arg_names[i] assert argname is not None - assert context, "Internal error: messages given without context" messages.missing_named_argument(callee, context, argname) ok = False elif kind in [nodes.ARG_POS, nodes.ARG_OPT, @@ -1127,19 +1100,72 @@ def check_argument_count(self, callee: CallableType, actual_types: List[Type], if (self.chk.in_checked_function() or isinstance(actual_types[formal_to_actual[i][0]], TupleType)): if messages: - assert context, "Internal error: messages given without context" messages.duplicate_argument_value(callee, i, context) ok = False elif (kind in (nodes.ARG_NAMED, nodes.ARG_NAMED_OPT) and formal_to_actual[i] and actual_kinds[formal_to_actual[i][0]] not in [nodes.ARG_NAMED, nodes.ARG_STAR2]): # Positional argument when expecting a keyword argument. if messages: - assert context, "Internal error: messages given without context" messages.too_many_positional_arguments(callee, context) ok = False return ok - def check_argument_types(self, arg_types: List[Type], arg_kinds: List[int], + def check_for_extra_actual_arguments(self, + callee: CallableType, + actual_types: List[Type], + actual_kinds: List[int], + actual_names: Optional[Sequence[Optional[str]]], + all_actuals: List[int], + context: Context, + messages: Optional[MessageBuilder]) -> Tuple[bool, bool]: + """Check for extra actual arguments. + + Return tuple (was everything ok, + was there an extra keyword argument error [used to avoid duplicate errors]). + """ + + is_unexpected_arg_error = False # Keep track of errors to avoid duplicate errors + ok = True # False if we've found any error + + for i, kind in enumerate(actual_kinds): + if i not in all_actuals and ( + kind != nodes.ARG_STAR or + not is_empty_tuple(actual_types[i])): + # Extra actual: not matched by a formal argument. + ok = False + if kind != nodes.ARG_NAMED: + if messages: + messages.too_many_arguments(callee, context) + else: + if messages: + assert actual_names, "Internal error: named kinds without names given" + act_name = actual_names[i] + assert act_name is not None + messages.unexpected_keyword_argument(callee, act_name, context) + is_unexpected_arg_error = True + elif ((kind == nodes.ARG_STAR and nodes.ARG_STAR not in callee.arg_kinds) + or kind == nodes.ARG_STAR2): + actual_type = actual_types[i] + if isinstance(actual_type, (TupleType, TypedDictType)): + if all_actuals.count(i) < len(actual_type.items): + # Too many tuple/dict items as some did not match. + if messages: + if (kind != nodes.ARG_STAR2 + or not isinstance(actual_type, TypedDictType)): + messages.too_many_arguments(callee, context) + else: + messages.too_many_arguments_from_typed_dict(callee, actual_type, + context) + is_unexpected_arg_error = True + ok = False + # *args/**kwargs can be applied even if the function takes a fixed + # number of positional arguments. This may succeed at runtime. + + return ok, is_unexpected_arg_error + + def check_argument_types(self, + arg_types: List[Type], + arg_kinds: List[int], callee: CallableType, formal_to_actual: List[List[int]], context: Context, @@ -1152,47 +1178,28 @@ def check_argument_types(self, arg_types: List[Type], arg_kinds: List[int], messages = messages or self.msg check_arg = check_arg or self.check_arg # Keep track of consumed tuple *arg items. - tuple_counter = [0] + mapper = ArgTypeExpander() for i, actuals in enumerate(formal_to_actual): for actual in actuals: - arg_type = arg_types[actual] - if arg_type is None: + actual_type = arg_types[actual] + if actual_type is None: continue # Some kind of error was already reported. + actual_kind = arg_kinds[actual] # Check that a *arg is valid as varargs. - if (arg_kinds[actual] == nodes.ARG_STAR and - not self.is_valid_var_arg(arg_type)): - messages.invalid_var_arg(arg_type, context) - if (arg_kinds[actual] == nodes.ARG_STAR2 and - not self.is_valid_keyword_var_arg(arg_type)): - is_mapping = is_subtype(arg_type, self.chk.named_type('typing.Mapping')) - messages.invalid_keyword_var_arg(arg_type, is_mapping, context) - # Get the type of an individual actual argument (for *args - # and **args this is the item type, not the collection type). - if (isinstance(arg_type, TupleType) - and tuple_counter[0] >= len(arg_type.items) - and arg_kinds[actual] == nodes.ARG_STAR): - # The tuple is exhausted. Continue with further arguments. - continue - actual_type = get_actual_type(arg_type, arg_kinds[actual], - tuple_counter) - check_arg(actual_type, arg_type, arg_kinds[actual], + if (actual_kind == nodes.ARG_STAR and + not self.is_valid_var_arg(actual_type)): + messages.invalid_var_arg(actual_type, context) + if (actual_kind == nodes.ARG_STAR2 and + not self.is_valid_keyword_var_arg(actual_type)): + is_mapping = is_subtype(actual_type, self.chk.named_type('typing.Mapping')) + messages.invalid_keyword_var_arg(actual_type, is_mapping, context) + expanded_actual = mapper.expand_actual_type( + actual_type, actual_kind, + callee.arg_names[i], callee.arg_kinds[i]) + check_arg(expanded_actual, actual_type, arg_kinds[actual], callee.arg_types[i], actual + 1, i + 1, callee, context, messages) - # There may be some remaining tuple varargs items that haven't - # been checked yet. Handle them. - tuplet = arg_types[actual] - if (callee.arg_kinds[i] == nodes.ARG_STAR and - arg_kinds[actual] == nodes.ARG_STAR and - isinstance(tuplet, TupleType)): - while tuple_counter[0] < len(tuplet.items): - actual_type = get_actual_type(arg_type, - arg_kinds[actual], - tuple_counter) - check_arg(actual_type, arg_type, arg_kinds[actual], - callee.arg_types[i], - actual + 1, i + 1, callee, context, messages) - def check_arg(self, caller_type: Type, original_caller_type: Type, caller_kind: int, callee_type: Type, n: int, m: int, callee: CallableType, @@ -1669,8 +1676,8 @@ def check_arg(caller_type: Type, original_caller_type: Type, caller_kind: int, raise Finished try: - self.check_argument_types(arg_types, arg_kinds, callee, formal_to_actual, - context=context, check_arg=check_arg) + self.check_argument_types(arg_types, arg_kinds, callee, + formal_to_actual, context=context, check_arg=check_arg) return True except Finished: return False diff --git a/mypy/constraints.py b/mypy/constraints.py index 5f6ddf1769b3..f4530f6c79df 100644 --- a/mypy/constraints.py +++ b/mypy/constraints.py @@ -13,7 +13,7 @@ from mypy.sametypes import is_same_type from mypy.erasetype import erase_typevars from mypy.nodes import COVARIANT, CONTRAVARIANT -from mypy.argmap import get_actual_type +from mypy.argmap import ArgTypeExpander MYPY = False if MYPY: @@ -54,7 +54,7 @@ def infer_constraints_for_callable( Return a list of constraints. """ constraints = [] # type: List[Constraint] - tuple_counter = [0] + mapper = ArgTypeExpander() for i, actuals in enumerate(formal_to_actual): for actual in actuals: @@ -62,10 +62,9 @@ def infer_constraints_for_callable( if actual_arg_type is None: continue - actual_type = get_actual_type(actual_arg_type, arg_kinds[actual], - tuple_counter) - c = infer_constraints(callee.arg_types[i], actual_type, - SUPERTYPE_OF) + actual_type = mapper.expand_actual_type(actual_arg_type, arg_kinds[actual], + callee.arg_names[i], callee.arg_kinds[i]) + c = infer_constraints(callee.arg_types[i], actual_type, SUPERTYPE_OF) constraints.extend(c) return constraints diff --git a/mypy/messages.py b/mypy/messages.py index 2162c06acbf0..d2d05e60d228 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -671,6 +671,18 @@ def incompatible_argument(self, n: int, m: int, callee: CallableType, arg_type: if arg_name is not None: arg_label = '"{}"'.format(arg_name) + if (arg_kind == ARG_STAR2 + and isinstance(arg_type, TypedDictType) + and m <= len(callee.arg_names) + and callee.arg_names[m - 1] is not None + and callee.arg_kinds[m - 1] != ARG_STAR2): + arg_name = callee.arg_names[m - 1] + assert arg_name is not None + arg_type_str, expected_type_str = self.format_distinctly( + arg_type.items[arg_name], + expected_type, + bare=True) + arg_label = '"{}"'.format(arg_name) msg = 'Argument {} {}has incompatible type {}; expected {}'.format( arg_label, target, self.quote_type_string(arg_type_str), self.quote_type_string(expected_type_str)) @@ -711,6 +723,20 @@ def too_many_arguments(self, callee: CallableType, context: Context) -> None: msg = 'Too many arguments' + for_function(callee) self.fail(msg, context) + def too_many_arguments_from_typed_dict(self, + callee: CallableType, + arg_type: TypedDictType, + context: Context) -> None: + # Try to determine the name of the extra argument. + for key in arg_type.items: + if key not in callee.arg_names: + msg = 'Extra argument "{}" from **args'.format(key) + for_function(callee) + break + else: + self.too_many_arguments(callee, context) + return + self.fail(msg, context) + def too_many_positional_arguments(self, callee: CallableType, context: Context) -> None: msg = 'Too many positional arguments' + for_function(callee) diff --git a/test-data/unit/check-typeddict.test b/test-data/unit/check-typeddict.test index f1734907e8e3..95bfd4f963b3 100644 --- a/test-data/unit/check-typeddict.test +++ b/test-data/unit/check-typeddict.test @@ -1413,3 +1413,81 @@ from mypy_extensions import TypedDict tp = TypedDict('tp', {'x': int}) [builtins fixtures/dict.pyi] [out] + +[case testTypedDictAsStarStarArg] +from mypy_extensions import TypedDict + +A = TypedDict('A', {'x': int, 'y': str}) +class B: pass + +def f1(x: int, y: str) -> None: ... +def f2(x: int, y: int) -> None: ... +def f3(x: B, y: str) -> None: ... +def f4(x: int) -> None: pass +def f5(x: int, y: str, z: int) -> None: pass +def f6(x: int, z: str) -> None: pass + +a: A +f1(**a) +f2(**a) # E: Argument "y" to "f2" has incompatible type "str"; expected "int" +f3(**a) # E: Argument "x" to "f3" has incompatible type "int"; expected "B" +f4(**a) # E: Extra argument "y" from **args for "f4" +f5(**a) # E: Too few arguments for "f5" +f6(**a) # E: Extra argument "y" from **args for "f6" +f1(1, **a) # E: "f1" gets multiple values for keyword argument "x" + +[case testTypedDictAsStarStarArgConstraints] +from typing import TypeVar, Union +from mypy_extensions import TypedDict + +T = TypeVar('T') +S = TypeVar('S') +def f1(x: T, y: S) -> Union[T, S]: ... + +A = TypedDict('A', {'y': int, 'x': str}) +a: A +reveal_type(f1(**a)) # E: Revealed type is 'Union[builtins.str*, builtins.int*]' + +[case testTypedDictAsStarStarArgCalleeKwargs] +from mypy_extensions import TypedDict + +A = TypedDict('A', {'x': int, 'y': str}) +B = TypedDict('B', {'x': str, 'y': str}) + +def f(**kwargs: str) -> None: ... +def g(x: int, **kwargs: str) -> None: ... + +a: A +b: B +f(**a) # E: Argument 1 to "f" has incompatible type "**A"; expected "str" +f(**b) +g(**a) +g(**b) # E: Argument "x" to "g" has incompatible type "str"; expected "int" +g(1, **a) # E: "g" gets multiple values for keyword argument "x" +g(1, **b) # E: "g" gets multiple values for keyword argument "x" \ + # E: Argument "x" to "g" has incompatible type "str"; expected "int" +[builtins fixtures/dict.pyi] + +[case testTypedDictAsStarStarTwice] +from mypy_extensions import TypedDict + +A = TypedDict('A', {'x': int, 'y': str}) +B = TypedDict('B', {'z': bytes}) +C = TypedDict('C', {'x': str, 'z': bytes}) + +def f1(x: int, y: str, z: bytes) -> None: ... +def f2(x: int, y: float, z: bytes) -> None: ... +def f3(x: int, y: str, z: float) -> None: ... + +a: A +b: B +c: C +f1(**a, **b) +f1(**b, **a) +f2(**a, **b) # E: Argument "y" to "f2" has incompatible type "str"; expected "float" +f3(**a, **b) # E: Argument "z" to "f3" has incompatible type "bytes"; expected "float" +f3(**b, **a) # E: Argument "z" to "f3" has incompatible type "bytes"; expected "float" +f1(**a, **c) # E: "f1" gets multiple values for keyword argument "x" \ + # E: Argument "x" to "f1" has incompatible type "str"; expected "int" +f1(**c, **a) # E: "f1" gets multiple values for keyword argument "x" \ + # E: Argument "x" to "f1" has incompatible type "str"; expected "int" diff --git a/test-data/unit/check-varargs.test b/test-data/unit/check-varargs.test index 81701f8eea01..75d24fad0fb8 100644 --- a/test-data/unit/check-varargs.test +++ b/test-data/unit/check-varargs.test @@ -111,6 +111,20 @@ f(*it1) f(*it2) # E: Argument 1 to "f" has incompatible type "*Iterable[str]"; expected "int" [builtins fixtures/for.pyi] +[case testCallVarargsFunctionWithTwoTupleStarArgs] +from typing import TypeVar, Tuple + +T1 = TypeVar('T1') +T2 = TypeVar('T2') +T3 = TypeVar('T3') +T4 = TypeVar('T4') + +def f(a: T1, b: T2, c: T3, d: T4) -> Tuple[T1, T2, T3, T4]: ... +x: Tuple[int, str] +y: Tuple[float, bool] +reveal_type(f(*x, *y)) # E: Revealed type is 'Tuple[builtins.int*, builtins.str*, builtins.float*, builtins.bool*]' +[builtins fixtures/list.pyi] + [case testCallVarargsFunctionWithIterableAndPositional] from typing import Iterable @@ -125,8 +139,10 @@ f(*it1, '') # E: Argument 2 to "f" has incompatible type "str"; expected "int" def f(*x: int) -> None: pass it1 = (1, 2) +it2 = ('',) f(*it1, 1, 2) f(*it1, 1, *it1, 2) +f(*it1, 1, *it2, 2) # E: Argument 3 to "f" has incompatible type "*Tuple[str]"; expected "int" f(*it1, '') # E: Argument 2 to "f" has incompatible type "str"; expected "int" [builtins fixtures/for.pyi] @@ -551,6 +567,18 @@ main:10: error: Incompatible types in assignment (expression has type "List[]", variable has type "List[A]") main:11: error: Argument 1 to "f" of "G" has incompatible type "*List[A]"; expected "B" +[case testCallerTupleVarArgsAndGenericCalleeVarArg] +# flags: --strict-optional +from typing import TypeVar + +T = TypeVar('T') + +def f(*args: T) -> T: ... +reveal_type(f(*(1, None))) # E: Revealed type is 'Union[builtins.int, None]' +reveal_type(f(1, *(None, 1))) # E: Revealed type is 'Union[builtins.int, None]' +reveal_type(f(1, *(1, None))) # E: Revealed type is 'Union[builtins.int, None]' +[builtins fixtures/tuple.pyi] + -- Comment signatures -- ------------------