Skip to content

Commit

Permalink
fix edge case with namedtuple
Browse files Browse the repository at this point in the history
  • Loading branch information
KotlinIsland committed Jul 21, 2024
1 parent 8fd88ee commit bae241b
Show file tree
Hide file tree
Showing 4 changed files with 102 additions and 60 deletions.
9 changes: 9 additions & 0 deletions docs/source/error_code_list3.rst
Original file line number Diff line number Diff line change
Expand Up @@ -147,4 +147,13 @@ basedmypy has lint rules regarding their validity as ``Callable``\s:
A().a(1) # runtime TypeError: <lambda>() takes 1 positional argument but 2 were given
.. _code-unhandled-scenario:

A scenario isn't being handled correctly by basedmypy itself
------------------------------------------------------------

Sometimes, the implementation of a the type system is not completely sound
and basedmypy needs to be updated.

.. _code-reveal:
136 changes: 76 additions & 60 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -3152,6 +3152,80 @@ def check_type_alias_rvalue(self, s: AssignmentStmt) -> None:
alias_type = self.expr_checker.accept(s.rvalue)
self.store_type(s.lvalues[-1], alias_type)

def check_function_on_class(
self,
lvalue: Lvalue,
rvalue: Expression,
p_lvalue_type: ProperType | None,
p_rvalue_type: ProperType | None,
):
"""Special case: function assigned to an instance var via the class will be methodised"""
type_type = None
expr_type = None
if isinstance(lvalue, MemberExpr):
expr_type = self.expr_checker.accept(lvalue.expr)
proper = get_proper_type(expr_type)
if isinstance(proper, CallableType) and proper.is_type_obj():
type_type = expr_type
if not (
(type_type or self.scope.active_class())
and isinstance(p_rvalue_type, CallableType)
and isinstance(p_lvalue_type, CallableType)
):
return
node = None
if type_type:
assert expr_type
proper = get_proper_type(expr_type)
assert isinstance(proper, CallableType)
proper = get_proper_type(proper.ret_type)
if isinstance(proper, TupleType):
proper = proper.partial_fallback
if not isinstance(proper, Instance):
self.msg.fail(
"This value is some special cased thing, needs to be handled separately",
rvalue,
code=codes.UNHANDLED_SCENARIO,
)
return
assert isinstance(lvalue, (NameExpr, MemberExpr))
table_node = proper.type.get(lvalue.name)
if table_node is None:
# work around https://github.com/python/mypy/issues/17316
return
node = table_node.node
class_var = (isinstance(node, Var) and node.is_classvar) or (
isinstance(lvalue, NameExpr)
and isinstance(lvalue.node, Var)
and lvalue.node.is_classvar
)
if p_rvalue_type.is_function:
if codes.CALLABLE_FUNCTIONTYPE in self.options.enabled_error_codes:
if not (class_var and p_lvalue_type.is_function):
self.msg.fail(
'Assigning a "FunctionType" on the class will become a "MethodType"',
rvalue,
code=codes.CALLABLE_FUNCTIONTYPE,
)
if not class_var:
self.msg.note(
'Consider setting it on the instance, or using "ClassVar"', rvalue
)
elif (
codes.POSSIBLE_FUNCTION in self.options.enabled_error_codes
and type(p_rvalue_type) is CallableType
and p_rvalue_type.is_callable
):
self.msg.fail(
'This "CallableType" could be a "FunctionType", which when assigned via the class, would produce a "MethodType"',
rvalue,
code=codes.POSSIBLE_FUNCTION,
)
if class_var:
self.msg.note('Consider changing the type to "FunctionType"', rvalue)
elif not p_lvalue_type.is_function:
self.msg.note('Consider setting it on the instance, or using "ClassVar"', rvalue)

def check_assignment(
self,
lvalue: Lvalue,
Expand Down Expand Up @@ -3306,66 +3380,8 @@ def check_assignment(
self.msg.concrete_only_assign(p_lvalue_type, rvalue)
return

# Special case: function assigned to an instance var via the class will be methodised
type_type = None
expr_type = None
if isinstance(lvalue, MemberExpr):
expr_type = self.expr_checker.accept(lvalue.expr)
proper = get_proper_type(expr_type)
if isinstance(proper, CallableType) and proper.is_type_obj():
type_type = expr_type
if (
(type_type or self.scope.active_class())
and isinstance(p_rvalue_type, CallableType)
and isinstance(p_lvalue_type, CallableType)
):
node = None
if type_type:
assert expr_type
proper = get_proper_type(expr_type)
assert isinstance(proper, CallableType)
proper = get_proper_type(proper.ret_type)
assert isinstance(proper, Instance)
assert isinstance(lvalue, (NameExpr, MemberExpr))
table_node = proper.type.get(lvalue.name)
if table_node is None:
# work around https://github.com/python/mypy/issues/17316
return
node = table_node.node
class_var = (isinstance(node, Var) and node.is_classvar) or (
isinstance(lvalue, NameExpr)
and isinstance(lvalue.node, Var)
and lvalue.node.is_classvar
)
if p_rvalue_type.is_function:
if codes.CALLABLE_FUNCTIONTYPE in self.options.enabled_error_codes:
if not (class_var and p_lvalue_type.is_function):
self.msg.fail(
'Assigning a "FunctionType" on the class will become a "MethodType"',
rvalue,
code=codes.CALLABLE_FUNCTIONTYPE,
)
if not class_var:
self.msg.note(
'Consider setting it on the instance, or using "ClassVar"',
rvalue,
)
elif (
codes.POSSIBLE_FUNCTION in self.options.enabled_error_codes
and type(p_rvalue_type) is CallableType
and p_rvalue_type.is_callable
):
self.msg.fail(
'This "CallableType" could be a "FunctionType", which when assigned via the class, would produce a "MethodType"',
rvalue,
code=codes.POSSIBLE_FUNCTION,
)
if class_var:
self.msg.note('Consider changing the type to "FunctionType"', rvalue)
elif not p_lvalue_type.is_function:
self.msg.note(
'Consider setting it on the instance, or using "ClassVar"', rvalue
)
self.check_function_on_class(lvalue, rvalue, p_lvalue_type, p_rvalue_type)

proper_right = get_proper_type(rvalue_type)
if (
isinstance(lvalue, RefExpr)
Expand Down
6 changes: 6 additions & 0 deletions mypy/errorcodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,12 @@ def __hash__(self) -> int:
POSSIBLE_FUNCTION: Final[ErrorCode] = ErrorCode(
"possible-function", "possible FunctionType on class", "General", default_enabled=False
)
UNHANDLED_SCENARIO: Final[ErrorCode] = ErrorCode(
"unhandled-scenario",
"an unknown error occurred. raise an issue",
"General",
default_enabled=False,
)

REGEX: Final = ErrorCode("regex", "Regex related errors", "General")
REVEAL: Final = ErrorCode("reveal", "Reveal types at check time", "General")
Expand Down
11 changes: 11 additions & 0 deletions test-data/unit/check-based-callable.test
Original file line number Diff line number Diff line change
Expand Up @@ -271,3 +271,14 @@ reveal_type(A().f) # N: Revealed type is "_NamedCallable & () -> None"
reveal_type(A().c) # N: Revealed type is "_NamedCallable & () -> None"
reveal_type(A().s) # N: Revealed type is "_NamedCallable & () -> None"
[builtins fixtures/callable.pyi]


[case testNonInstanceThing]
# test a crash when the subject isn't an instance
from collections import namedtuple

f = namedtuple("f", "")
f.__repr__ = lambda _: "" # E: "type[f]" has no attribute "__repr__" [attr-defined] \
# E: Expression type contains "Any" (has type "def (_: Untyped) -> str") [no-any-expr]
reveal_type(f) # N: Revealed type is "() -> tuple[(), fallback=__main__.f]"
[builtins fixtures/tuple.pyi]

0 comments on commit bae241b

Please sign in to comment.