Skip to content

Commit

Permalink
Allow non-final __match_args__ and overriding (#12415)
Browse files Browse the repository at this point in the history
Allow subclasses to override `__match_args__` freely, and don't require
`__match_args__` to be final. 

This matches runtime behavior. For example, if `B` subclasses `A`, 
`case A(...)` also matches instances of `B`, using the `__match_args__` 
from `A`.

The issue was brough up by @AlexWaygood in 
#12411 (comment).
  • Loading branch information
JukkaL committed Mar 22, 2022
1 parent 626147a commit 6606dbe
Show file tree
Hide file tree
Showing 3 changed files with 47 additions and 11 deletions.
17 changes: 9 additions & 8 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -1439,12 +1439,7 @@ def check_setattr_method(self, typ: Type, context: Context) -> None:
self.msg.invalid_signature_for_special_method(typ, context, '__setattr__')

def check_match_args(self, var: Var, typ: Type, context: Context) -> None:
"""Check that __match_args__ is final and contains literal strings"""

if not var.is_final:
self.note("__match_args__ must be final for checking of match statements to work",
context, code=codes.LITERAL_REQ)

"""Check that __match_args__ contains literal strings"""
typ = get_proper_type(typ)
if not isinstance(typ, TupleType) or \
not all([is_string_literal(item) for item in typ.items]):
Expand Down Expand Up @@ -2276,11 +2271,16 @@ def check_assignment(self, lvalue: Lvalue, rvalue: Expression, infer_lvalue_type

# Defer PartialType's super type checking.
if (isinstance(lvalue, RefExpr) and
not (isinstance(lvalue_type, PartialType) and lvalue_type.type is None)):
not (isinstance(lvalue_type, PartialType) and
lvalue_type.type is None) and
not (isinstance(lvalue, NameExpr) and lvalue.name == '__match_args__')):
if self.check_compatibility_all_supers(lvalue, lvalue_type, rvalue):
# We hit an error on this line; don't check for any others
return

if isinstance(lvalue, MemberExpr) and lvalue.name == '__match_args__':
self.fail(message_registry.CANNOT_MODIFY_MATCH_ARGS, lvalue)

if lvalue_type:
if isinstance(lvalue_type, PartialType) and lvalue_type.type is None:
# Try to infer a proper type for a variable with a partial None type.
Expand Down Expand Up @@ -2377,7 +2377,8 @@ def check_assignment(self, lvalue: Lvalue, rvalue: Expression, infer_lvalue_type

if inferred:
rvalue_type = self.expr_checker.accept(rvalue)
if not inferred.is_final:
if not (inferred.is_final or (isinstance(lvalue, NameExpr) and
lvalue.name == '__match_args__')):
rvalue_type = remove_instance_last_known_values(rvalue_type)
self.infer_variable_type(inferred, lvalue, rvalue_type, rvalue)
self.check_assignment_to_slots(lvalue)
Expand Down
1 change: 1 addition & 0 deletions mypy/message_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,3 +245,4 @@ def format(self, *args: object, **kwargs: object) -> "ErrorMessage":
CLASS_PATTERN_DUPLICATE_KEYWORD_PATTERN: Final = 'Duplicate keyword pattern "{}"'
CLASS_PATTERN_UNKNOWN_KEYWORD: Final = 'Class "{}" has no attribute "{}"'
MULTIPLE_ASSIGNMENTS_IN_PATTERN: Final = 'Multiple assignments to name "{}" in pattern'
CANNOT_MODIFY_MATCH_ARGS: Final = 'Cannot assign to "__match_args__"'
40 changes: 37 additions & 3 deletions test-data/unit/check-python310.test
Original file line number Diff line number Diff line change
Expand Up @@ -881,16 +881,16 @@ reveal_type(z) # N: Revealed type is "builtins.int*"

[case testMatchNonFinalMatchArgs]
class A:
__match_args__ = ("a", "b") # N: __match_args__ must be final for checking of match statements to work
__match_args__ = ("a", "b")
a: str
b: int

m: object

match m:
case A(i, j):
reveal_type(i) # N: Revealed type is "Any"
reveal_type(j) # N: Revealed type is "Any"
reveal_type(i) # N: Revealed type is "builtins.str"
reveal_type(j) # N: Revealed type is "builtins.int"
[builtins fixtures/tuple.pyi]

[case testMatchAnyTupleMatchArgs]
Expand Down Expand Up @@ -1528,3 +1528,37 @@ class A:
class B:
def __enter__(self) -> B: ...
def __exit__(self, x, y, z) -> None: ...

[case testOverrideMatchArgs]
class AST:
__match_args__ = ()

class stmt(AST): ...

class AnnAssign(stmt):
__match_args__ = ('target', 'annotation', 'value', 'simple')
target: str
annotation: int
value: str
simple: int

reveal_type(AST.__match_args__) # N: Revealed type is "Tuple[]"
reveal_type(stmt.__match_args__) # N: Revealed type is "Tuple[]"
reveal_type(AnnAssign.__match_args__) # N: Revealed type is "Tuple[Literal['target']?, Literal['annotation']?, Literal['value']?, Literal['simple']?]"

AnnAssign.__match_args__ = ('a', 'b', 'c', 'd') # E: Cannot assign to "__match_args__"
__match_args__ = 0

def f(x: AST) -> None:
match x:
case AST():
reveal_type(x) # N: Revealed type is "__main__.AST"
match x:
case stmt():
reveal_type(x) # N: Revealed type is "__main__.stmt"
match x:
case AnnAssign(a, b, c, d):
reveal_type(a) # N: Revealed type is "builtins.str"
reveal_type(b) # N: Revealed type is "builtins.int"
reveal_type(c) # N: Revealed type is "builtins.str"
[builtins fixtures/tuple.pyi]

0 comments on commit 6606dbe

Please sign in to comment.