astral-sh / ruff

An extremely fast Python linter and code formatter, written in Rust.
https://docs.astral.sh/ruff
MIT License
33k stars 1.1k forks source link

B039 considers `frozenset` to be mutable #14525

Open layday opened 11 hours ago

layday commented 11 hours ago
$ 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
layday commented 11 hours ago

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

layday commented 10 hours ago

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.
layday commented 10 hours ago

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]())
harupy commented 8 hours ago

@AlexWaygood can I work on this?

AlexWaygood commented 8 hours ago

@harupy sure!

AlexWaygood commented 8 hours ago

@harupy this function will probably be helpful in fixing some of the issues here: https://github.com/astral-sh/ruff/blob/b80de52592f7b4c9854a454e2b7a0121fe2862e4/crates/ruff_python_ast/src/helpers.rs#L660-L670

harupy commented 8 hours ago

@AlexWaygood Thanks!

harupy commented 8 hours ago

Filed #14532