Skip to content

[ruff] add fix safety section (RUF033)#17760

Merged
dylwil3 merged 3 commits intoastral-sh:mainfrom
VascoSch92:fix-safety-section-post-init-defaults
May 11, 2025
Merged

[ruff] add fix safety section (RUF033)#17760
dylwil3 merged 3 commits intoastral-sh:mainfrom
VascoSch92:fix-safety-section-post-init-defaults

Conversation

@VascoSch92
Copy link
Contributor

@VascoSch92 VascoSch92 commented May 1, 2025

The PR add the fix safety section for rule RUF033 (#15584 ).

It seems that the fix was always unsafe, see #13192. An example of unsafely can be found in the PR description of #13192 or in the next section.

Unsafety example

I took as an example the case

from dataclasses import dataclass, InitVar

@dataclass
class User:
    name: str

    def __post_init__(self, greeting: str = "Hello"):
        print(f"{greeting}, {self.name}!")

@dataclass
class UserAfterFix:
    name: str
    greeting: InitVar[str] = "Hello"

    def __post_init__(self, greeting: str):
        print(f"{greeting}, {self.name}!")

User('Alice')  # print `Hello, Alice`
UserAfterFix('Alice') # raise an error

@VascoSch92 VascoSch92 mentioned this pull request May 1, 2025
71 tasks
@github-actions
Copy link
Contributor

github-actions bot commented May 1, 2025

ruff-ecosystem results

Linter (stable)

✅ ecosystem check detected no linter changes.

Linter (preview)

✅ ecosystem check detected no linter changes.

@MichaReiser MichaReiser added the documentation Improvements or additions to documentation label May 1, 2025
@VascoSch92
Copy link
Contributor Author

Hey @dylwil3

I believe the PR is ready for review.

Copy link
Collaborator

@dylwil3 dylwil3 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could not reproduce the exception being raised in the example in your PR summary (but maybe I've done something wrong?), nor could I see why the example fix in the original PR was unsafe. The example in the docs for the rule of a suggested change would be an unsafe fix since it changes runtime behavior, but we don't actually offer an autofix in the situation where we would be overwriting a default like that. So I don't really see yet why this fix is unsafe (is it?).

Comment on lines 66 to 67
/// This fix is always marked as unsafe because it assumes that the user always
/// wants `__post_init__` args to be `InitVar`s.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't really follow this as a justification - if the user didn't want this, they wouldn't select this lint rule to begin with. Does the fix ever cause a change in runtime behavior? Is it too likely that there are false positives, or are there known false positives? That's the sort of thing we're looking for here.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fix is unsafe because, although switching to InitVar is usually correct, it is incorrect when the parameter is not meant to be a field exposed in the public API, or when the value should be shared between all instances. The examples below use a defaulted parameter as a class-level cache; the correct fix would be to switch to ClassVar.

Here is an example where the fix changes runtime behavior:

$ cat >ruf033_1.py <<'# EOF'
from dataclasses import dataclass, field
@dataclass
class Foo:
    name: str
    serial_number: int = field(init=False)
    def __post_init__(self, cache: dict[str, int] = {}) -> None:
        self.serial_number = cache.setdefault(self.name, len(cache))
print(Foo("A").serial_number)
print(Foo("B").serial_number)
print(Foo("A").serial_number)
try:
    print(Foo("A", cache={"A": 10}).serial_number)
except TypeError:
    pass
else:
    print("Error: `cache` should not be accessible in the API")
# EOF

$ python ruf033_1.py
0
1
0

$ ruff --isolated check ruf033_1.py --select RUF033 --unsafe-fixes --fix
Found 1 error (1 fixed, 0 remaining).

$ python ruf033_1.py                                                    
0
1
0
10
Error: `cache` should not be accessible in the API

Here is another:

$ cat >ruf033_2.py <<'# EOF'
from dataclasses import dataclass, field
@dataclass
class Foo:
    name: str
    serial_number: int = field(init=False)
    def __post_init__(self, *, cache: dict[str, int] = {}) -> None:
        self.serial_number = cache.setdefault(self.name, len(cache))
print(Foo("A").serial_number)
print(Foo("B").serial_number)
print(Foo("A").serial_number)
# EOF

$ python ruf033_2.py
0
1
0

$ ruff --isolated check ruf033_2.py --select RUF033 --unsafe-fixes --fix
Found 1 error (1 fixed, 0 remaining).

$ python ruf033_2.py
Traceback (most recent call last):
  File "ruf033.py", line 9, in <module>
    print(Foo("A").serial_number)
          ^^^^^^^^
  File "<string>", line 4, in __init__
TypeError: Foo.__post_init__() takes 1 positional argument but 2 were given

@VascoSch92
Copy link
Contributor Author

I could not reproduce the exception being raised in the example in your PR summary (but maybe I've done something wrong?), nor could I see why the example fix in the original PR was unsafe. The example in the docs for the rule of a suggested change would be an unsafe fix since it changes runtime behavior, but we don't actually offer an autofix in the situation where we would be overwriting a default like that. So I don't really see yet why this fix is unsafe (is it?).

Ok, strange... It works on ruff playground, i.e., the rule is triggered.

Screenshot 2025-05-05 at 20 38 53

I had in mind the same reason of @dscorbett but I could not explain it so clear ;-)

I will change the section accordingly.

@VascoSch92 VascoSch92 requested a review from dylwil3 May 10, 2025 16:53
@dylwil3
Copy link
Collaborator

dylwil3 commented May 11, 2025

Ok, strange... It works on ruff playground, i.e., the rule is triggered.

I agree that the rule is triggered, but when you actually run the code it does not raise an exception (both before and after the fix).

Copy link
Collaborator

@dylwil3 dylwil3 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you! (and thanks @dscorbett !)

@dylwil3 dylwil3 merged commit 5792ed1 into astral-sh:main May 11, 2025
34 checks passed
dcreager added a commit that referenced this pull request May 12, 2025
* main:
  disable jemalloc on android (#18033)
  [ty] Fix incorrect type of `src.root` in documentation (#18040)
  [ty] Refine message for why a rule is enabled (#18038)
  [ty] Remove brackets around option names (#18037)
  Update pre-commit dependencies (#18025)
  Update docker/build-push-action action to v6.16.0 (#18030)
  Update docker/login-action action to v3.4.0 (#18031)
  Update taiki-e/install-action digest to 83254c5 (#18022)
  Update cargo-bins/cargo-binstall action to v1.12.4 (#18023)
  Update Rust crate ctrlc to v3.4.7 (#18027)
  Update Rust crate clap to v4.5.38 (#18026)
  Update Rust crate jiff to v0.2.13 (#18029)
  Update Rust crate getrandom to v0.3.3 (#18028)
  Update dependency ruff to v0.11.9 (#18024)
  [`pylint`] add fix safety section (`PLW1514`) (#17932)
  python_stdlib: update for 3.14 (#18014)
  [`ruff`] add fix safety section (`RUF033`) (#17760)
  [`pylint`] add fix safety section (`PLC0414`) (#17802)
Glyphack pushed a commit to Glyphack/ruff that referenced this pull request May 21, 2025
This PR adds the fix safety section for rule `RUF033`
(astral-sh#15584 ).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants