2
2
import builtins
3
3
import itertools
4
4
import logging
5
+ import math
5
6
import re
6
- import sys
7
7
from collections import namedtuple
8
8
from contextlib import suppress
9
9
from functools import lru_cache , partial
@@ -354,13 +354,13 @@ def visit_Assert(self, node):
354
354
355
355
def visit_AsyncFunctionDef (self , node ):
356
356
self .check_for_b902 (node )
357
- self .check_for_b006 (node )
357
+ self .check_for_b006_and_b008 (node )
358
358
self .generic_visit (node )
359
359
360
360
def visit_FunctionDef (self , node ):
361
361
self .check_for_b901 (node )
362
362
self .check_for_b902 (node )
363
- self .check_for_b006 (node )
363
+ self .check_for_b006_and_b008 (node )
364
364
self .check_for_b018 (node )
365
365
self .check_for_b019 (node )
366
366
self .check_for_b021 (node )
@@ -390,23 +390,14 @@ def visit_With(self, node):
390
390
self .check_for_b022 (node )
391
391
self .generic_visit (node )
392
392
393
- def compose_call_path (self , node ):
394
- if isinstance (node , ast .Attribute ):
395
- yield from self .compose_call_path (node .value )
396
- yield node .attr
397
- elif isinstance (node , ast .Call ):
398
- yield from self .compose_call_path (node .func )
399
- elif isinstance (node , ast .Name ):
400
- yield node .id
401
-
402
393
def check_for_b005 (self , node ):
403
394
if node .func .attr not in B005 .methods :
404
395
return # method name doesn't match
405
396
406
397
if len (node .args ) != 1 or not isinstance (node .args [0 ], ast .Str ):
407
398
return # used arguments don't match the builtin strip
408
399
409
- call_path = "." .join (self . compose_call_path (node .func .value ))
400
+ call_path = "." .join (compose_call_path (node .func .value ))
410
401
if call_path in B005 .valid_paths :
411
402
return # path is exempt
412
403
@@ -419,48 +410,10 @@ def check_for_b005(self, node):
419
410
420
411
self .errors .append (B005 (node .lineno , node .col_offset ))
421
412
422
- def check_for_b006 (self , node ):
423
- for default in node .args .defaults + node .args .kw_defaults :
424
- if isinstance (
425
- default , (* B006 .mutable_literals , * B006 .mutable_comprehensions )
426
- ):
427
- self .errors .append (B006 (default .lineno , default .col_offset ))
428
- elif isinstance (default , ast .Call ):
429
- call_path = "." .join (self .compose_call_path (default .func ))
430
- if call_path in B006 .mutable_calls :
431
- self .errors .append (B006 (default .lineno , default .col_offset ))
432
- elif (
433
- call_path
434
- not in B008 .immutable_calls | self .b008_extend_immutable_calls
435
- ):
436
- # Check if function call is actually a float infinity/NaN literal
437
- if call_path == "float" and len (default .args ) == 1 :
438
- float_arg = default .args [0 ]
439
- if sys .version_info < (3 , 8 , 0 ):
440
- # NOTE: pre-3.8, string literals are represented with ast.Str
441
- if isinstance (float_arg , ast .Str ):
442
- str_val = float_arg .s
443
- else :
444
- str_val = ""
445
- else :
446
- # NOTE: post-3.8, string literals are represented with ast.Constant
447
- if isinstance (float_arg , ast .Constant ):
448
- str_val = float_arg .value
449
- if not isinstance (str_val , str ):
450
- str_val = ""
451
- else :
452
- str_val = ""
453
-
454
- # NOTE: regex derived from documentation at:
455
- # https://docs.python.org/3/library/functions.html#float
456
- inf_nan_regex = r"^[+-]?(inf|infinity|nan)$"
457
- re_result = re .search (inf_nan_regex , str_val .lower ())
458
- is_float_literal = re_result is not None
459
- else :
460
- is_float_literal = False
461
-
462
- if not is_float_literal :
463
- self .errors .append (B008 (default .lineno , default .col_offset ))
413
+ def check_for_b006_and_b008 (self , node ):
414
+ visitor = FuntionDefDefaultsVisitor (self .b008_extend_immutable_calls )
415
+ visitor .visit (node .args .defaults + node .args .kw_defaults )
416
+ self .errors .extend (visitor .errors )
464
417
465
418
def check_for_b007 (self , node ):
466
419
targets = NameFinder ()
@@ -536,8 +489,7 @@ def check_for_b019(self, node):
536
489
# Preserve decorator order so we can get the lineno from the decorator node
537
490
# rather than the function node (this location definition changes in Python 3.8)
538
491
resolved_decorators = (
539
- "." .join (self .compose_call_path (decorator ))
540
- for decorator in node .decorator_list
492
+ "." .join (compose_call_path (decorator )) for decorator in node .decorator_list
541
493
)
542
494
for idx , decorator in enumerate (resolved_decorators ):
543
495
if decorator in {"classmethod" , "staticmethod" }:
@@ -755,6 +707,16 @@ def check_for_b022(self, node):
755
707
self .errors .append (B022 (node .lineno , node .col_offset ))
756
708
757
709
710
+ def compose_call_path (node ):
711
+ if isinstance (node , ast .Attribute ):
712
+ yield from compose_call_path (node .value )
713
+ yield node .attr
714
+ elif isinstance (node , ast .Call ):
715
+ yield from compose_call_path (node .func )
716
+ elif isinstance (node , ast .Name ):
717
+ yield node .id
718
+
719
+
758
720
@attr .s
759
721
class NameFinder (ast .NodeVisitor ):
760
722
"""Finds a name within a tree of nodes.
@@ -778,6 +740,69 @@ def visit(self, node):
778
740
return node
779
741
780
742
743
+ class FuntionDefDefaultsVisitor (ast .NodeVisitor ):
744
+ def __init__ (self , b008_extend_immutable_calls = None ):
745
+ self .b008_extend_immutable_calls = b008_extend_immutable_calls or set ()
746
+ for node in B006 .mutable_literals + B006 .mutable_comprehensions :
747
+ setattr (self , f"visit_{ node } " , self .visit_mutable_literal_or_comprehension )
748
+ self .errors = []
749
+ self .arg_depth = 0
750
+ super ().__init__ ()
751
+
752
+ def visit_mutable_literal_or_comprehension (self , node ):
753
+ # Flag B006 iff mutable literal/comprehension is not nested.
754
+ # We only flag these at the top level of the expression as we
755
+ # cannot easily guarantee that nested mutable structures are not
756
+ # made immutable by outer operations, so we prefer no false positives.
757
+ # e.g.
758
+ # >>> def this_is_fine(a=frozenset({"a", "b", "c"})): ...
759
+ #
760
+ # >>> def this_is_not_fine_but_hard_to_detect(a=(lambda x: x)([1, 2, 3]))
761
+ #
762
+ # We do still search for cases of B008 within mutable structures though.
763
+ if self .arg_depth == 1 :
764
+ self .errors .append (B006 (node .lineno , node .col_offset ))
765
+ # Check for nested functions.
766
+ self .generic_visit (node )
767
+
768
+ def visit_Call (self , node ):
769
+ call_path = "." .join (compose_call_path (node .func ))
770
+ if call_path in B006 .mutable_calls :
771
+ self .errors .append (B006 (node .lineno , node .col_offset ))
772
+ self .generic_visit (node )
773
+ return
774
+
775
+ if call_path in B008 .immutable_calls | self .b008_extend_immutable_calls :
776
+ self .generic_visit (node )
777
+ return
778
+
779
+ # Check if function call is actually a float infinity/NaN literal
780
+ if call_path == "float" and len (node .args ) == 1 :
781
+ try :
782
+ value = float (ast .literal_eval (node .args [0 ]))
783
+ except Exception :
784
+ pass
785
+ else :
786
+ if math .isfinite (value ):
787
+ self .errors .append (B008 (node .lineno , node .col_offset ))
788
+ else :
789
+ self .errors .append (B008 (node .lineno , node .col_offset ))
790
+
791
+ # Check for nested functions.
792
+ self .generic_visit (node )
793
+
794
+ def visit (self , node ):
795
+ """Like super-visit but supports iteration over lists."""
796
+ self .arg_depth += 1
797
+ if isinstance (node , list ):
798
+ for elem in node :
799
+ if elem is not None :
800
+ super ().visit (elem )
801
+ else :
802
+ super ().visit (node )
803
+ self .arg_depth -= 1
804
+
805
+
781
806
class B020NameFinder (NameFinder ):
782
807
"""Ignore names defined within the local scope of a comprehension."""
783
808
@@ -851,8 +876,8 @@ def visit_comprehension(self, node):
851
876
"between them."
852
877
)
853
878
)
854
- B006 .mutable_literals = (ast . Dict , ast . List , ast . Set )
855
- B006 .mutable_comprehensions = (ast . ListComp , ast . DictComp , ast . SetComp )
879
+ B006 .mutable_literals = (" Dict" , " List" , " Set" )
880
+ B006 .mutable_comprehensions = (" ListComp" , " DictComp" , " SetComp" )
856
881
B006 .mutable_calls = {
857
882
"Counter" ,
858
883
"OrderedDict" ,
0 commit comments