@@ -17,6 +17,54 @@ def is_mapping_proxy_type(annotation: ast.expr | None) -> bool:
1717 return False
1818
1919
20+ def _get_base_name (annotation_value : ast .expr ) -> str :
21+ """Extract the base name from an annotation value."""
22+ if isinstance (annotation_value , ast .Name ):
23+ return annotation_value .id
24+ if isinstance (annotation_value , ast .Attribute ):
25+ return annotation_value .attr
26+ return ""
27+
28+
29+ def is_dict_type_annotation (annotation : ast .expr | None ) -> bool :
30+ """Check if annotation represents a dict type that should trigger COP013.
31+
32+ Returns True for:
33+ - dict
34+ - Final[dict]
35+ - dict[key, value]
36+ - Final[dict[key, value]]
37+
38+ Returns False for TypedDict and other non-dict annotations.
39+ """
40+ is_dict_annotation = False
41+
42+ if annotation is not None :
43+ # Handle simple name annotations like 'dict'
44+ if isinstance (annotation , ast .Name ):
45+ is_dict_annotation = annotation .id == "dict"
46+ # Handle attribute annotations like 'typing.Final'
47+ elif isinstance (annotation , ast .Attribute ):
48+ is_dict_annotation = annotation .attr == "dict"
49+ # Handle subscript annotations like 'dict[str, int]' or 'Final[dict]'
50+ elif isinstance (annotation , ast .Subscript ):
51+ base_name : typing .Final = _get_base_name (annotation .value )
52+ if base_name :
53+ # Check for Final[...] annotations
54+ if base_name == "Final" :
55+ # Extract the inner type from Final[inner_type]
56+ inner_type = annotation .slice
57+ # Handle Python 3.8 vs 3.9+ differences
58+ if hasattr (inner_type , "value" ): # Python 3.8
59+ inner_type = inner_type .value
60+ is_dict_annotation = is_dict_type_annotation (inner_type )
61+ # Check for dict[...] annotations
62+ elif base_name == "dict" :
63+ is_dict_annotation = True
64+
65+ return is_dict_annotation
66+
67+
2068@typing .final
2169class MappingProxyCheck (ast .NodeVisitor ):
2270 def __init__ (self , syntax_tree : ast .AST ) -> None : # noqa: ARG002
@@ -33,6 +81,10 @@ def _check_mapping_assignment(self, ast_node: ast.Assign | ast.AnnAssign) -> Non
3381 if isinstance (ast_node , ast .AnnAssign ) and is_mapping_proxy_type (ast_node .annotation ):
3482 return
3583
84+ # Skip annotated assignments that are not dict-like types
85+ if isinstance (ast_node , ast .AnnAssign ) and not is_dict_type_annotation (ast_node .annotation ):
86+ return
87+
3688 # Check for dictionary literals assigned to module-level variables
3789 assigned_value : ast .expr | None
3890 assignment_targets : list [ast .expr ]
0 commit comments