astral-sh/ruff

B039 considers `frozenset` to be mutable

Closed this issue · 8 comments

$ cat foo.py 
from contextvars import ContextVar

foo = ContextVar('foo', default=frozenset[str]())
$ ruff check --select B039 foo.py
foo.py:3:33: B039 Do not use mutable data structures for `ContextVar` defaults
  |
1 | from contextvars import ContextVar
2 | 
3 | foo = ContextVar('foo', default=frozenset[str]())
  |                                 ^^^^^^^^^^^^^^^^ B039
  |
  = help: Replace with `None`; initialize with `.set()``

Found 1 error.
$ ruff --version                 
ruff 0.8.0

Also appears to be the case for tuple, and only if they are parametrised, i.e. frozenset() is accepted but not frozenset[type]().

Conversely, the error is silenced if the context var is an annotated mutable type:

$ cat foo.py
from contextvars import ContextVar

foo = ContextVar[set[str]]('foo', default=set())  # No error
bar = ContextVar('bar', default=set())
$ ruff check --select B039 foo.py
foo.py:4:33: B039 Do not use mutable data structures for `ContextVar` defaults
  |
3 | foo = ContextVar[set[str]]('foo', default=set())  # No error
4 | bar = ContextVar('bar', default=set())
  |                                 ^^^^^ B039
  |
  = help: Replace with `None`; initialize with `.set()``

Found 1 error.

To recap:

from contextvars import ContextVar


# All of these should produce an error but foo3 and foo4 don't.

foo1 = ContextVar('foo1', default=set())
foo2 = ContextVar('foo2', default=set[object]())
foo3 = ContextVar[set[object]]('foo3', default=set())
foo4 = ContextVar[set[object]]('foo4', default=set[object]())
foo5: ContextVar[set[object]] = ContextVar('foo5', default=set())
foo6: ContextVar[set[object]] = ContextVar('foo6', default=set[object]())


# None of these should produce an error but bar2 and bar6 do.

bar1 = ContextVar('bar1', default=frozenset())
bar2 = ContextVar('bar2', default=frozenset[object]())
bar3 = ContextVar[frozenset[object]]('bar3', default=frozenset())
bar4 = ContextVar[frozenset[object]]('bar4', default=frozenset[object]())
bar5: ContextVar[frozenset[object]] = ContextVar('bar5', default=frozenset())
bar6: ContextVar[frozenset[object]] = ContextVar('bar6', default=frozenset[object]())

@AlexWaygood can I work on this?

@harupy this function will probably be helpful in fixing some of the issues here:

/// Given an [`Expr`] that can be a [`ExprSubscript`][ast::ExprSubscript] or not
/// (like an annotation that may be generic or not), return the underlying expr.
pub fn map_subscript(expr: &Expr) -> &Expr {
if let Expr::Subscript(ast::ExprSubscript { value, .. }) = expr {
// Ex) `Iterable[T]` => return `Iterable`
value
} else {
// Ex) `Iterable` => return `Iterable`
expr
}
}

@AlexWaygood Thanks!

Filed #14532