@@ -81,7 +81,7 @@ class DeprecatedTypingAliasMsg(NamedTuple):
81
81
node : Union [nodes .Name , nodes .Attribute ]
82
82
qname : str
83
83
alias : str
84
- parent_subscript : bool
84
+ parent_subscript : bool = False
85
85
86
86
87
87
class TypingChecker (BaseChecker ):
@@ -118,6 +118,14 @@ class TypingChecker(BaseChecker):
118
118
"use string annotation instead. E.g. "
119
119
"``Callable[..., 'NoReturn']``. https://bugs.python.org/issue34921" ,
120
120
),
121
+ "E6005" : (
122
+ "'collections.abc.Callable' inside Optional and Union is broken in "
123
+ "3.9.0 / 3.9.1 (use 'typing.Callable' instead)" ,
124
+ "broken-collections-callable" ,
125
+ "``collections.abc.Callable`` inside Optional and Union is broken in "
126
+ "Python 3.9.0 and 3.9.1. Use ``typing.Callable`` for these cases instead. "
127
+ "https://bugs.python.org/issue42965" ,
128
+ ),
121
129
}
122
130
options = (
123
131
(
@@ -152,7 +160,9 @@ class TypingChecker(BaseChecker):
152
160
def __init__ (self , linter : "PyLinter" ) -> None :
153
161
"""Initialize checker instance."""
154
162
super ().__init__ (linter = linter )
163
+ self ._found_broken_callable_location : bool = False
155
164
self ._alias_name_collisions : Set [str ] = set ()
165
+ self ._deprecated_typing_alias_msgs : List [DeprecatedTypingAliasMsg ] = []
156
166
self ._consider_using_alias_msgs : List [DeprecatedTypingAliasMsg ] = []
157
167
158
168
def open (self ) -> None :
@@ -169,8 +179,9 @@ def open(self) -> None:
169
179
)
170
180
171
181
self ._should_check_noreturn = py_version < (3 , 7 , 2 )
182
+ self ._should_check_callable = py_version < (3 , 9 , 2 )
172
183
173
- def _msg_postponed_eval_hint (self , node ) -> str :
184
+ def _msg_postponed_eval_hint (self , node : nodes . NodeNG ) -> str :
174
185
"""Message hint if postponed evaluation isn't enabled."""
175
186
if self ._py310_plus or "annotations" in node .root ().future_imports :
176
187
return ""
@@ -181,6 +192,7 @@ def _msg_postponed_eval_hint(self, node) -> str:
181
192
"consider-using-alias" ,
182
193
"consider-alternative-union-syntax" ,
183
194
"broken-noreturn" ,
195
+ "broken-collections-callable" ,
184
196
)
185
197
def visit_name (self , node : nodes .Name ) -> None :
186
198
if self ._should_check_typing_alias and node .name in ALIAS_NAMES :
@@ -189,12 +201,15 @@ def visit_name(self, node: nodes.Name) -> None:
189
201
self ._check_for_alternative_union_syntax (node , node .name )
190
202
if self ._should_check_noreturn and node .name == "NoReturn" :
191
203
self ._check_broken_noreturn (node )
204
+ if self ._should_check_callable and node .name == "Callable" :
205
+ self ._check_broken_callable (node )
192
206
193
207
@check_messages (
194
208
"deprecated-typing-alias" ,
195
209
"consider-using-alias" ,
196
210
"consider-alternative-union-syntax" ,
197
211
"broken-noreturn" ,
212
+ "broken-collections-callable" ,
198
213
)
199
214
def visit_attribute (self , node : nodes .Attribute ) -> None :
200
215
if self ._should_check_typing_alias and node .attrname in ALIAS_NAMES :
@@ -203,6 +218,8 @@ def visit_attribute(self, node: nodes.Attribute) -> None:
203
218
self ._check_for_alternative_union_syntax (node , node .attrname )
204
219
if self ._should_check_noreturn and node .attrname == "NoReturn" :
205
220
self ._check_broken_noreturn (node )
221
+ if self ._should_check_callable and node .attrname == "Callable" :
222
+ self ._check_broken_callable (node )
206
223
207
224
def _check_for_alternative_union_syntax (
208
225
self ,
@@ -255,10 +272,16 @@ def _check_for_typing_alias(
255
272
return
256
273
257
274
if self ._py39_plus :
258
- self .add_message (
259
- "deprecated-typing-alias" ,
260
- node = node ,
261
- args = (inferred .qname (), alias .name ),
275
+ if inferred .qname () == "typing.Callable" and self ._broken_callable_location (
276
+ node
277
+ ):
278
+ self ._found_broken_callable_location = True
279
+ self ._deprecated_typing_alias_msgs .append (
280
+ DeprecatedTypingAliasMsg (
281
+ node ,
282
+ inferred .qname (),
283
+ alias .name ,
284
+ )
262
285
)
263
286
return
264
287
@@ -284,7 +307,20 @@ def leave_module(self, node: nodes.Module) -> None:
284
307
'consider-using-alias' check. Make sure results are safe
285
308
to recommend / collision free.
286
309
"""
287
- if self ._py37_plus and not self ._py39_plus :
310
+ if self ._py39_plus :
311
+ for msg in self ._deprecated_typing_alias_msgs :
312
+ if (
313
+ self ._found_broken_callable_location
314
+ and msg .qname == "typing.Callable"
315
+ ):
316
+ continue
317
+ self .add_message (
318
+ "deprecated-typing-alias" ,
319
+ node = msg .node ,
320
+ args = (msg .qname , msg .alias ),
321
+ )
322
+
323
+ elif self ._py37_plus :
288
324
msg_future_import = self ._msg_postponed_eval_hint (node )
289
325
for msg in self ._consider_using_alias_msgs :
290
326
if msg .qname in self ._alias_name_collisions :
@@ -298,7 +334,10 @@ def leave_module(self, node: nodes.Module) -> None:
298
334
msg_future_import if msg .parent_subscript else "" ,
299
335
),
300
336
)
337
+
301
338
# Clear all module cache variables
339
+ self ._found_broken_callable_location = False
340
+ self ._deprecated_typing_alias_msgs .clear ()
302
341
self ._alias_name_collisions .clear ()
303
342
self ._consider_using_alias_msgs .clear ()
304
343
@@ -328,6 +367,57 @@ def _check_broken_noreturn(self, node: Union[nodes.Name, nodes.Attribute]) -> No
328
367
self .add_message ("broken-noreturn" , node = node , confidence = INFERENCE )
329
368
break
330
369
370
+ def _check_broken_callable (self , node : Union [nodes .Name , nodes .Attribute ]) -> None :
371
+ """Check for 'collections.abc.Callable' inside Optional and Union."""
372
+ inferred = safe_infer (node )
373
+ if not (
374
+ isinstance (inferred , nodes .ClassDef )
375
+ and inferred .qname () == "_collections_abc.Callable"
376
+ and self ._broken_callable_location (node )
377
+ ):
378
+ return
379
+
380
+ self .add_message ("broken-collections-callable" , node = node , confidence = INFERENCE )
381
+
382
+ def _broken_callable_location ( # pylint: disable=no-self-use
383
+ self , node : Union [nodes .Name , nodes .Attribute ]
384
+ ) -> bool :
385
+ """Check if node would be a broken location for collections.abc.Callable."""
386
+ if is_postponed_evaluation_enabled (node ) and is_node_in_type_annotation_context (
387
+ node
388
+ ):
389
+ return False
390
+
391
+ # Check first Callable arg is a list of arguments -> Callable[[int], None]
392
+ if not (
393
+ isinstance (node .parent , nodes .Subscript )
394
+ and isinstance (node .parent .slice , nodes .Tuple )
395
+ and len (node .parent .slice .elts ) == 2
396
+ and isinstance (node .parent .slice .elts [0 ], nodes .List )
397
+ ):
398
+ return False
399
+
400
+ # Check nested inside Optional or Union
401
+ parent_subscript = node .parent .parent
402
+ if isinstance (parent_subscript , nodes .BaseContainer ):
403
+ parent_subscript = parent_subscript .parent
404
+ if not (
405
+ isinstance (parent_subscript , nodes .Subscript )
406
+ and isinstance (parent_subscript .value , (nodes .Name , nodes .Attribute ))
407
+ ):
408
+ return False
409
+
410
+ inferred_parent = safe_infer (parent_subscript .value )
411
+ if not (
412
+ isinstance (inferred_parent , nodes .FunctionDef )
413
+ and inferred_parent .qname () in {"typing.Optional" , "typing.Union" }
414
+ or isinstance (inferred_parent , astroid .bases .Instance )
415
+ and inferred_parent .qname () == "typing._SpecialForm"
416
+ ):
417
+ return False
418
+
419
+ return True
420
+
331
421
332
422
def register (linter : "PyLinter" ) -> None :
333
423
linter .register_checker (TypingChecker (linter ))
0 commit comments