Skip to content
Open
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
92 changes: 64 additions & 28 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -540,6 +540,14 @@ def __init__(
# example when type-checking class decorators.
self.allow_abstract_call = False

# When checking one of the copies `expand_typevars` makes of a method for a
# class with a value-restricted type variable, this holds the self/cls type
# with that substitution already applied. `super()` (see `_super_arg_types`
# in checkexpr.py) consults this instead of recomputing an unsubstituted self
# type from the class's TypeInfo, so inherited members are checked against
# the same value substitution as the rest of the expanded copy.
self.expanding_self_type: ProperType | None = None

# Child checker objects for specific AST node types
self._expr_checker = mypy.checkexpr.ExpressionChecker(
self, self.msg, self.plugin, per_line_checking_time_ns
Expand Down Expand Up @@ -1454,7 +1462,7 @@ def check_func_def(
self.check_typevar_defaults(typ.variables)
expanded = self.expand_typevars(defn, typ)
original_typ = typ
for item, typ in expanded:
for item, typ, typevar_mapping in expanded:
old_binder = self.binder
self.binder = ConditionalTypeBinder(self.options)
with self.binder.top_frame_context():
Expand Down Expand Up @@ -1541,29 +1549,33 @@ def check_func_def(
n.node = v
self.binder.assign_type(n, v.type, v.type)

with self.scope.push_function(defn):
# We suppress reachability warnings for empty generator functions
# (return; yield) which have a "yield" that's unreachable by definition
# since it's only there to promote the function into a generator function.
#
# We also suppress reachability warnings when we use TypeVars with value
# restrictions: we only want to report a warning if a certain statement is
# marked as being suppressed in *all* of the expansions, but we currently
# have no good way of doing this.
#
# TODO: Find a way of working around this limitation
if _is_empty_generator_function(item) or len(expanded) >= 2:
self.binder.suppress_unreachable_warnings()
# When checking a third-party library, we can skip function body,
# if during semantic analysis we found that there are no attributes
# defined via self here.
if (
not self.can_skip_diagnostics
or self.options.preserve_asts
or not isinstance(defn, FuncDef)
or defn.def_or_infer_vars
):
self.accept(item.body)
self.expanding_self_type = self.self_type_for_expansion(defn, typevar_mapping)
try:
with self.scope.push_function(defn):
# We suppress reachability warnings for empty generator functions
# (return; yield) which have a "yield" that's unreachable by definition
# since it's only there to promote the function into a generator function.
#
# We also suppress reachability warnings when we use TypeVars with value
# restrictions: we only want to report a warning if a certain statement is
# marked as being suppressed in *all* of the expansions, but we currently
# have no good way of doing this.
#
# TODO: Find a way of working around this limitation
if _is_empty_generator_function(item) or len(expanded) >= 2:
self.binder.suppress_unreachable_warnings()
# When checking a third-party library, we can skip function body,
# if during semantic analysis we found that there are no attributes
# defined via self here.
if (
not self.can_skip_diagnostics
or self.options.preserve_asts
or not isinstance(defn, FuncDef)
or defn.def_or_infer_vars
):
self.accept(item.body)
finally:
self.expanding_self_type = None
unreachable = self.binder.is_unreachable()
if new_frame is not None:
self.binder.pop_frame(True, 0)
Expand Down Expand Up @@ -2275,7 +2287,7 @@ def check_match_args(self, var: Var, typ: Type, context: Context) -> None:

def expand_typevars(
self, defn: FuncItem, typ: CallableType
) -> list[tuple[FuncItem, CallableType]]:
) -> list[tuple[FuncItem, CallableType, dict[TypeVarId, Type]]]:
# TODO use generator
subst: list[list[tuple[TypeVarId, Type]]] = []
tvars = list(typ.variables) or []
Expand All @@ -2289,13 +2301,37 @@ def expand_typevars(
# value restricted type variables. (Except when running mypyc,
# where we need one canonical version of the function.)
if subst and not (self.options.mypyc or self.options.inspections):
result: list[tuple[FuncItem, CallableType]] = []
result: list[tuple[FuncItem, CallableType, dict[TypeVarId, Type]]] = []
for substitutions in itertools.product(*subst):
mapping = dict(substitutions)
result.append((expand_func(defn, mapping), expand_type(typ, mapping)))
result.append((expand_func(defn, mapping), expand_type(typ, mapping), mapping))
return result
else:
return [(defn, typ)]
return [(defn, typ, {})]

def self_type_for_expansion(
self, defn: FuncItem, mapping: dict[TypeVarId, Type]
) -> ProperType | None:
"""Compute self/cls's type for one of `expand_typevars`'s substituted copies.

`expand_func` only rewrites types already present somewhere in `defn`'s
AST, so an implicit (unannotated) self/cls argument is left with no
type at all, and even an explicitly annotated one is read from `defn`
-- not the substituted copy -- by callers like `_super_arg_types` in
checkexpr.py, since `check_func_def` pushes `defn`, not the copy, onto
the checker scope. Returns `None` when there's no substitution to
apply (`mapping` empty) or no self/cls argument to compute a type for.
"""
if not mapping or not defn.info or not defn.has_self_or_cls_argument:
return None
if not defn.arguments:
return None
self_type: ProperType | None = get_proper_type(defn.arguments[0].variable.type)
if self_type is None:
self_type = fill_typevars(defn.info)
if defn.is_class or defn.name == "__new__":
self_type = TypeType.make_normalized(self_type)
return expand_type(self_type, mapping)

def check_explicit_override_decorator(
self,
Expand Down
11 changes: 10 additions & 1 deletion mypy/checkexpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -5850,7 +5850,16 @@ def _super_arg_types(self, e: SuperExpr) -> Type | tuple[Type, Type]:
method = self.chk.scope.current_function()
assert method is not None
if method.arguments:
instance_type: Type = method.arguments[0].variable.type or current_type
# If we're currently checking one of the copies `expand_typevars`
# makes for a class with a value-restricted type variable, use the
# already-substituted self type computed for this copy -- the
# scope always holds the original (unexpanded) function, so its
# own self argument's type (annotated or not) is never substituted.
instance_type: Type = (
self.chk.expanding_self_type
or method.arguments[0].variable.type
or current_type
)
else:
self.chk.fail(message_registry.SUPER_ENCLOSING_POSITIONAL_ARGS_REQUIRED, e)
return AnyType(TypeOfAny.from_error)
Expand Down
52 changes: 50 additions & 2 deletions test-data/unit/check-generics.test
Original file line number Diff line number Diff line change
Expand Up @@ -2631,8 +2631,56 @@ class Bar(Foo[AnyStr]):
def method1(self, s: AnyStr, t: AnyStr) -> None:
super().method1('x', b'y') # Should be an error
[out]
main:10: error: Argument 1 to "method1" of "Foo" has incompatible type "str"; expected "AnyStr"
main:10: error: Argument 2 to "method1" of "Foo" has incompatible type "bytes"; expected "AnyStr"
main:10: error: Argument 1 to "method1" of "Foo" has incompatible type "str"; expected "bytes"
main:10: error: Argument 2 to "method1" of "Foo" has incompatible type "bytes"; expected "str"

[case testConstrainedGenericSuperNoFalsePositiveSameTypeVar]
# https://github.com/python/mypy/issues/14774
from typing import Generic, TypeVar

T = TypeVar("T", float, int)

class C(Generic[T]):
def __init__(self, i: T) -> None:
self.i: T = i

class B(C[T]):
def __init__(self, i: T) -> None:
super().__init__(i)
[out]

[case testConstrainedGenericSuperNoFalsePositiveDistinctTypeVar]
# https://github.com/python/mypy/issues/17757
from typing import Generic, TypeVar

T = TypeVar("T")
N = TypeVar("N", int, float)

class C(Generic[T]):
def __init__(self, c: T):
self.c = c

class C2(C[N]):
def __init__(self, c: N):
super().__init__(c)
[out]

[case testConstrainedGenericSuperClassmethodNoFalsePositive]
from typing import Generic, TypeVar

N = TypeVar("N", int, float)

class C(Generic[N]):
@classmethod
def make(cls, c: N) -> None:
pass

class C2(C[N]):
@classmethod
def make(cls, c: N) -> None:
super().make(c)
[builtins fixtures/classmethod.pyi]
[out]

[case testTypeVariableClashVar]
from typing import Generic, TypeVar, Callable
Expand Down
Loading