[flake8-bugbear] Mark the fix for unreliable-callable-check as always unsafe (B004) (#20318)

## Summary
Resolves #20282

Makes the rule fix always unsafe, because the replacement may not be
semantically equivalent to the original expression, potentially changing
the behavior of the code.

Updated docstring with examples.

## Test Plan
- Added two tests from issue and regenerated the snapshot

---------

Co-authored-by: Igor Drokin <drokinii1017@gmail.com>
Co-authored-by: Brent Westbrook <36778786+ntBre@users.noreply.github.com>
This commit is contained in:
Igor Drokin 2025-09-12 22:27:17 +03:00 committed by GitHub
parent ff677a96e4
commit dfec94608c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 80 additions and 8 deletions

View file

@ -39,3 +39,16 @@ hasattr(
"__call__", # comment 4 "__call__", # comment 4
# comment 5 # comment 5
) )
import operator
assert hasattr(operator, "__call__")
assert callable(operator) is False
class A:
def __init__(self): self.__call__ = None
assert hasattr(A(), "__call__")
assert callable(A()) is False

View file

@ -28,10 +28,29 @@ use crate::{Edit, Fix, FixAvailability, Violation};
/// ``` /// ```
/// ///
/// ## Fix safety /// ## Fix safety
/// This rule's fix is marked as unsafe if there's comments in the `hasattr` call /// This rule's fix is marked as unsafe because the replacement may not be semantically
/// expression, as comments may be removed. /// equivalent to the original expression, potentially changing the behavior of the code.
/// ///
/// For example, the fix would be marked as unsafe in the following case: /// For example, an imported module may have a `__call__` attribute but is not considered
/// a callable object:
/// ```python
/// import operator
///
/// assert hasattr(operator, "__call__")
/// assert callable(operator) is False
/// ```
/// Additionally, `__call__` may be defined only as an instance method:
/// ```python
/// class A:
/// def __init__(self):
/// self.__call__ = None
///
///
/// assert hasattr(A(), "__call__")
/// assert callable(A()) is False
/// ```
///
/// Additionally, if there are comments in the `hasattr` call expression, they may be removed:
/// ```python /// ```python
/// hasattr( /// hasattr(
/// # comment 1 /// # comment 1
@ -103,11 +122,7 @@ pub(crate) fn unreliable_callable_check(
Ok(Fix::applicable_edits( Ok(Fix::applicable_edits(
binding_edit, binding_edit,
import_edit, import_edit,
if checker.comment_ranges().intersects(expr.range()) { Applicability::Unsafe,
Applicability::Unsafe
} else {
Applicability::Safe
},
)) ))
}); });
} }

View file

@ -19,6 +19,7 @@ help: Replace with `callable()`
4 | print("Ooh, callable! Or is it?") 4 | print("Ooh, callable! Or is it?")
5 | if getattr(o, "__call__", False): 5 | if getattr(o, "__call__", False):
6 | print("Ooh, callable! Or is it?") 6 | print("Ooh, callable! Or is it?")
note: This is an unsafe fix and may change runtime behavior
B004 Using `hasattr(x, "__call__")` to test if x is callable is unreliable. Use `callable(x)` for consistent results. B004 Using `hasattr(x, "__call__")` to test if x is callable is unreliable. Use `callable(x)` for consistent results.
--> B004.py:5:8 --> B004.py:5:8
@ -50,6 +51,7 @@ help: Replace with `callable()`
13 | print("B U G") 13 | print("B U G")
14 | if builtins.getattr(o, "__call__", False): 14 | if builtins.getattr(o, "__call__", False):
15 | print("B U G") 15 | print("B U G")
note: This is an unsafe fix and may change runtime behavior
B004 Using `hasattr(x, "__call__")` to test if x is callable is unreliable. Use `callable(x)` for consistent results. B004 Using `hasattr(x, "__call__")` to test if x is callable is unreliable. Use `callable(x)` for consistent results.
--> B004.py:14:8 --> B004.py:14:8
@ -85,6 +87,7 @@ help: Replace with `callable()`
26 | print("STILL a bug!") 26 | print("STILL a bug!")
27 | 27 |
28 | 28 |
note: This is an unsafe fix and may change runtime behavior
B004 [*] Using `hasattr(x, "__call__")` to test if x is callable is unreliable. Use `callable(x)` for consistent results. B004 [*] Using `hasattr(x, "__call__")` to test if x is callable is unreliable. Use `callable(x)` for consistent results.
--> B004.py:35:1 --> B004.py:35:1
@ -99,6 +102,8 @@ B004 [*] Using `hasattr(x, "__call__")` to test if x is callable is unreliable.
40 | | # comment 5 40 | | # comment 5
41 | | ) 41 | | )
| |_^ | |_^
42 |
43 | import operator
| |
help: Replace with `callable()` help: Replace with `callable()`
32 | 32 |
@ -112,4 +117,43 @@ help: Replace with `callable()`
- # comment 5 - # comment 5
- ) - )
35 + callable(obj) 35 + callable(obj)
36 |
37 | import operator
38 |
note: This is an unsafe fix and may change runtime behavior
B004 [*] Using `hasattr(x, "__call__")` to test if x is callable is unreliable. Use `callable(x)` for consistent results.
--> B004.py:45:8
|
43 | import operator
44 |
45 | assert hasattr(operator, "__call__")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
46 | assert callable(operator) is False
|
help: Replace with `callable()`
42 |
43 | import operator
44 |
- assert hasattr(operator, "__call__")
45 + assert callable(operator)
46 | assert callable(operator) is False
47 |
48 |
note: This is an unsafe fix and may change runtime behavior
B004 [*] Using `hasattr(x, "__call__")` to test if x is callable is unreliable. Use `callable(x)` for consistent results.
--> B004.py:53:8
|
53 | assert hasattr(A(), "__call__")
| ^^^^^^^^^^^^^^^^^^^^^^^^
54 | assert callable(A()) is False
|
help: Replace with `callable()`
50 | def __init__(self): self.__call__ = None
51 |
52 |
- assert hasattr(A(), "__call__")
53 + assert callable(A())
54 | assert callable(A()) is False
note: This is an unsafe fix and may change runtime behavior note: This is an unsafe fix and may change runtime behavior