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

[pep8-naming] Avoid false positive for class Bar(type(foo)) (N804) #14683

Merged
merged 8 commits into from
Nov 30, 2024

Conversation

ntBre
Copy link
Contributor

@ntBre ntBre commented Nov 29, 2024

Summary

Fixes #14675, which reports N804 (invalid-first-argument-name-for-class-method) for this code:

foo = {}

class Bar(type(foo)):
    def foo_method(self):
        pass

I don't really love having to capture maybe in the closure, but this was the best way I could come up with to avoid propagating the new IsMetaclass return type through any_base_class. Another option along these lines, which at least avoids changing the Fns to FnMut, is to let maybe be a RefCell<bool> and mutate it that way.

Test Plan

cargo test with the example above added to N804.py.

@ntBre ntBre requested a review from AlexWaygood as a code owner November 29, 2024 18:34
Copy link
Contributor

github-actions bot commented Nov 29, 2024

ruff-ecosystem results

Linter (stable)

✅ ecosystem check detected no linter changes.

Linter (preview)

✅ ecosystem check detected no linter changes.

/// `IsMetaclass::No` if it's definitely *not* a metaclass, and
/// `IsMetaclass::Maybe` otherwise.
pub fn is_metaclass(class_def: &ast::StmtClassDef, semantic: &SemanticModel) -> IsMetaclass {
let mut maybe = false;
Copy link
Member

Choose a reason for hiding this comment

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

I don't really love having to capture maybe in the closure

hmm, yeah, this does feel a bit... icky 😄

I wonder if a better solution (though it's a bit more of a refactor) would be to split up any_base_class() into two functions: one that just handles iterating through all superclasses recursively (iter_base_classes() or iter_superclasses()), and one that checks if a condition holds true for any of the superclasses (that one could still be called any_base_class()). That way is_metaclass() could use the lower-level iter_base_classes() function directly rather than having to awkwardly work around the fact that the quite high-level any_base_class function doesn't really have quite the API we'd like it to have here.

WDYT?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think that's a great idea, I'll give it a try!

Copy link
Member

Choose a reason for hiding this comment

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

Just some extra data point if it helps to avoid a lot of work: I don't mind that it it captures the variable

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This was a little harder than I thought. I've added an iter_base_classes function, but it's not truly an iterator now since I build up a Vec of base classes and then call into_iter. I'll keep thinking about this, but at least the API has the shape I was going for.

Copy link
Member

Choose a reason for hiding this comment

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

I kind of prefer the old solution. It overall felt simpler and avoiding the extra allocation is nice too

Copy link
Member

Choose a reason for hiding this comment

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

Yeah, writing a recursive iterator seems annoyingly hard :(

Sorry for leading you down the wrong track here @ntBre. My bad -- your first solution probably was best!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No worries, it was interesting to try! I've reverted to the original.

One other thing I just noticed, should maybe also be true for the subscript case? I only handled the type(foo) example directly from the issue, but should type[foo] work as well? That's just a one-line code change, but I can update the tests too.

Copy link
Member

@AlexWaygood AlexWaygood Nov 30, 2024

Choose a reason for hiding this comment

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

should maybe also be true for the subscript case

I don't think so -- type[str], type[int], etc. always behaves the same as bare type in the context of a class's bases, and if a class inherits from type then it's always a metaclass

Copy link
Member

Choose a reason for hiding this comment

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

Even subscripts that are "invalid" from a typing perspective behave the same way:

>>> class bar(type[int, str]): pass
... 
>>> bar.__bases__
(<class 'type'>,)

@AlexWaygood
Copy link
Member

Also thanks for jumping on this! :D

@AlexWaygood AlexWaygood added the bug Something isn't working label Nov 29, 2024
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.

Thanks!

@AlexWaygood AlexWaygood merged commit 9e01763 into astral-sh:main Nov 30, 2024
21 checks passed
@ntBre ntBre deleted the brent/fix-14675 branch November 30, 2024 22:43
@alexhenman
Copy link

Thanks all for your very speedy work getting this fixed!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

Successfully merging this pull request may close these issues.

N804 false positive for class Bar(type(foo))
4 participants