Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Consider __new__ methods as special function type for enforcing class method or static method rules #13305

Merged
merged 21 commits into from
Feb 16, 2025

Conversation

cake-monotone
Copy link
Contributor

@cake-monotone cake-monotone commented Sep 10, 2024

Summary

__new__ methods are technically static methods, with cls as their first argument. However, Ruff currently classifies them as classmethod, which causes two issues:

  • It conveys incorrect information, leading to confusion. For example, in cases like ARG003, __new__ is explicitly treated as a classmethod.
  • Future rules that should apply to staticmethod may not be applied correctly due to this misclassification.

Motivated by this, the current PR makes the following adjustments:

  1. Introduces FunctionType::NewMethod as an enum variant, since, for the purposes of lint rules, __new__ sometimes behaves like a static method and other times like a class method. This is an internal change.

  2. The following rule behaviors and messages are totally unchanged:

  3. The following rule behaviors are unchanged, but the messages have been changed for correctness to use "__new__ method" instead of "class method":

  4. The following rules are changed unconditionally (not gated behind preview) because their current behavior is an honest bug: it just isn't true that __new__ is a class method, and it is true that __new__ is a static method:

  5. The only changes which differ based on preview are the following:

Closes #13154

Copy link
Contributor

github-actions bot commented Sep 10, 2024

ruff-ecosystem results

Linter (stable)

ℹ️ ecosystem check detected linter changes. (+2 -2 violations, +0 -0 fixes in 1 projects; 54 projects unchanged)

latchbio/latch (+2 -2 violations, +0 -0 fixes)

- src/latch_cli/centromere/utils.py:67:34: ARG003 Unused class method argument: `args`
+ src/latch_cli/centromere/utils.py:67:34: ARG004 Unused static method argument: `args`
- src/latch_cli/centromere/utils.py:67:42: ARG003 Unused class method argument: `kwargs`
+ src/latch_cli/centromere/utils.py:67:42: ARG004 Unused static method argument: `kwargs`

Changes by rule (2 rules affected)

code total + violation - violation + fix - fix
ARG004 2 2 0 0 0
ARG003 2 0 2 0 0

Linter (preview)

ℹ️ ecosystem check detected linter changes. (+2 -2 violations, +0 -0 fixes in 1 projects; 54 projects unchanged)

latchbio/latch (+2 -2 violations, +0 -0 fixes)

ruff check --no-cache --exit-zero --ignore RUF9 --no-fix --output-format concise --preview

- src/latch_cli/centromere/utils.py:67:34: ARG003 Unused class method argument: `args`
+ src/latch_cli/centromere/utils.py:67:34: ARG004 Unused static method argument: `args`
- src/latch_cli/centromere/utils.py:67:42: ARG003 Unused class method argument: `kwargs`
+ src/latch_cli/centromere/utils.py:67:42: ARG004 Unused static method argument: `kwargs`

Changes by rule (2 rules affected)

code total + violation - violation + fix - fix
ARG004 2 2 0 0 0
ARG003 2 0 2 0 0

@cake-monotone
Copy link
Contributor Author

I'm sure there are other breaking changes besides the ruff-ecosystem results
I'll try to summarize them.

@AlexWaygood AlexWaygood self-assigned this Sep 10, 2024
@cake-monotone
Copy link
Contributor Author

Affected Rules

  • ARG003: unused-class-method-argument
  • ARG004: unused-static-method-argument
  • PLW0642: self-or-cls-assignment
  • PLW0211: bad-staticmethod-argument
  • N804: invalid-first-argument-name-for-class-method
  • PLR0913: too-many-arguments
  • PLR0917: too-many-positional-arguments

ARG003, ARG004

ARG003 (unused-class-method-argument) -> ARG004 (unused-static-method-argument)

PLW0642

__new__ methods will no longer be checked by PLW0642. This rule only applies to methods and class methods, not static methods.

N804, PLW0211

N804 (invalid-first-argument-name-for-class-method) -> PLW0211 (bad-staticmethod-argument)

PLR0913, PLR0917

PLR0913 (too-many-arguments) and PLR0917 (too-many-positional-arguments) behave differently for classmethod and staticmethod. In classmethod, the first argument cls is excluded from the argument count. In staticmethod, cls is not excluded, so the argument limit applies more strictly.

Example

You can view an example of how these rules are applied here:
https://play.ruff.rs/2df5f2b6-093f-4988-a562-c6101a1c71cc

class ClassDunderNew:
    @classmethod
    def __new__(
        cls,
        x,  # ARG003: Unused class method argument: `x`
    ):
        cls = 1  # PLW0642: Reassigned `cls` variable in class method

    @classmethod
    def __new__(  # PLR0913: Too many arguments in function definition (6 > 5)  # PLR0917: Too many positional arguments (6/5)
        self,  # N804: First argument of a class method should be named `cls`
        a1, a2, a3, a4, a5,
        a6,
    ):
        ...


class StaticDunderNew:
    @staticmethod
    def __new__(
        cls,
        x,  # ARG004: Unused static method argument: `x`
    ):
        cls = 1

    @staticmethod
    def __new__(  # PLR0913: Too many arguments in function definition (6 > 5)  # PLR0917: Too many positional arguments (6/5)
        self,  # PLW0211: First argument of a static method should not be named `self`
        a1, a2, a3, a4, a5,
    ):
        ...

@cake-monotone
Copy link
Contributor Author

I reviewed all the code affected by function_type::classify. Let me know if I missed any affected rules.

Please check if these affected rules are acceptable. :)

@AlexWaygood AlexWaygood removed their assignment Sep 19, 2024
@dylwil3 dylwil3 force-pushed the dunder-new-as-staticmethod branch from 1865ecb to 0e23922 Compare December 25, 2024 05:02
@MichaReiser MichaReiser added rule Implementing or modifying a lint rule needs-decision Awaiting a decision from a maintainer labels Dec 26, 2024
@dylwil3 dylwil3 force-pushed the dunder-new-as-staticmethod branch from 0e23922 to b7931ce Compare February 15, 2025 18:49
@dylwil3 dylwil3 removed the needs-decision Awaiting a decision from a maintainer label Feb 15, 2025
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.

Thanks so much!

I think this is pretty much good to go - had to tweak some things to rebase, and I decided we should special case the counting of arguments for the "too-many-(positional-)arguments" rules to treat __new__ like a classmethod, since it seems more intuitive there. Also added more test fixtures to track changes if we decide to further modify the treatment of __new__ later.

My only hesitation on merging is whether this should be considered breaking and wait until the next minor release - thoughts @AlexWaygood / @MichaReiser ?

Also to Alex: It'd be great if you could spot check the conflict resolution for PYI019 to make sure it's in line with what you had in mind for the recent changes to that rule. https://github.com/astral-sh/ruff/pull/13305/files#diff-bfce927fd2c71368a7fc9c045c1f51f03f4ac25e398ed8b3a571caf01f4efe06

@MichaReiser
Copy link
Member

MichaReiser commented Feb 15, 2025

I'd prefer if this is a preview only change just because of the scope of it. It's definitely a breaking change otherwise

@dylwil3
Copy link
Collaborator

dylwil3 commented Feb 16, 2025

I don't think making the change preview only makes sense in many cases, since I consider the old behavior a bug/incorrect. Here's what I ended up doing. Even though this is a long list, there are actually not so many behavior changes, so I wouldn't consider this breaking, personally. What do you think?

  1. As suggested by Alex, I introduced FunctionType::NewMethod as an enum variant, since, for the purposes of lint rules, __new__ sometimes behaves like a static method and other times like a class method. This is an internal change.

  2. The following rule behaviors and messages are totally unchanged:

  3. The following rule behaviors are unchanged, but the messages have been changed for correctness to use "__new__ method" instead of "class method":

  4. The following rules are changed unconditionally (not gated behind preview) because I think their current behavior is an honest bug: it just isn't true that __new__ is a class method, and it is true that __new__ is a static method:

  5. The only changes which differ based on preview are the following:

Copy link
Member

@AlexWaygood AlexWaygood left a comment

Choose a reason for hiding this comment

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

This looks excellent! Thanks @cake-monotone for the PR, and thanks @dylwil3 for driving it over the line!

@dylwil3 dylwil3 added the preview Related to preview mode features label Feb 16, 2025
@dylwil3 dylwil3 changed the title Refactor method classification logic: __new__ methods are now classified as staticmethod Consider __new__ methods as special function type for enforcing class method or static method rules Feb 16, 2025
@dylwil3 dylwil3 merged commit 96dd1b1 into astral-sh:main Feb 16, 2025
21 checks passed
Copy link
Member

@AlexWaygood AlexWaygood left a comment

Choose a reason for hiding this comment

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

argh sorry, a couple of tiny nits I missed pre-merge that might be good to address as a followup

Comment on lines +239 to +242
// In preview, this violation is caught by `PLW0211` instead
function_type::FunctionType::NewMethod if checker.settings.preview.is_enabled() => {
return;
}
Copy link
Member

Choose a reason for hiding this comment

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

Oh, I guess it might be good to add a note about this to the rule's docs?

&& !visibility::is_overload(decorator_list, checker.semantic())
{
// we use `method()` here rather than `function()`, as although `__new__` is
// an implicit staticmethod, `__new__` methods must always have >= parameter
Copy link
Member

Choose a reason for hiding this comment

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

Ah shoot, my previous suggestion had a typo -- it should have been this :-(

Suggested change
// an implicit staticmethod, `__new__` methods must always have >= parameter
// an implicit staticmethod, `__new__` methods must always have >=1 parameters

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
preview Related to preview mode features rule Implementing or modifying a lint rule
Projects
None yet
Development

Successfully merging this pull request may close these issues.

A __new__ method should be considered a staticmethod, not a classmethod.
4 participants