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

Add a new declare-non-slot error code #9564

Open
wants to merge 15 commits into
base: main
Choose a base branch
from

Conversation

adamtuft
Copy link

@adamtuft adamtuft commented Apr 22, 2024

Type of Changes

Type
✨ New feature

Description

Refs #9499

Implement declare-non-slot which reports an error when a class has a __slots__ member and a type hint on the class is not present in __slots__.

Details

Tests:

  • Add test case which should report the error code.

  • Add test case where the error is suppressed due to __dict__ in __slots__.

  • Add test to detect annotation not in any base class's __slots__.

  • Add test against false-positive when base doesn't have __slots__ (since there is still then an instance __dict__ thanks to base)

  • Add test against false-positive when base has __slots__ with __dict__ entry.

  • Ignore declare-non-slots for regression_5479.py which is for assigning-non-slot.

Implementation:

Implement check for declare-non-slot in class_checker.py and 2 new helper methods:

  • Add method _check_declare_non_slot to report declare-non-slot error. This method checks node and all bases for a valid __slots__, gathering the names in all __slots__ found. If node has an annotation not in any __slots__, then declare-non-slot is reported.

  • Add helper method _has_valid_slots which returns True if a valid __slots__ is found on a ClassDef (re-use logic from _check_slots).

  • Refactor _check_redefined_slots to split out logic for getting __slots__ names into _get_classdef_slots_names helper, since the same logic can be used by both functions.

Some thoughts:

  • I thought this check belonged in the class checker instead of the dataclass checker since the problematic code is problematic even without the dataclass decorator. There may be other cases where the class annotations and slots items should match, and making this check dataclass-specific would miss those cases.
  • I went with declare-non-slot over define-non-slot as annotating a class feels more like a declaration that such a member exists, rather than a definition of that member.
  • I made some changes to existing code to factor out some logic into a helper function _get_classdef_slots_names as this seemed the cleanest way to re-use existing logic. I hope this is ok.
  • I've checked that the existing tests still pass and have run the primer tests with python3 -m pytest -m primer_stdlib --primer-stdlib.

To Do:

adamtuft and others added 2 commits April 22, 2024 22:26
- Add test case which should report the error code.

- Add test case where the error is suppressed due to __dict__
  in __slots__.

- Add test to detect annotation not in any base class' __slots__

- Add test against false-positive when base doesn't have __slots__
  (since there is still then an instance __dict__ thanks to base)

- Add test against false-positive when base has __slots__ with
  __dict__ entry

Ignore declare-non-slots for regression_5479.py

- This regression test is for "assigning-non-slot" so ignore
  "declare-non-slots" which also matches this example.

Implement check for "declare-non-slot" and 2 new helper methods:

- Add method `_check_declare_non_slot` to report "declare-non-slot" error.
  This method checks `node` and all bases for a valid `__slots__`, gathering
  the names in all `__slots__` found. If `node` has an annotation not in any
  `__slots__`, then "declare-non-slot" is reported.

- Add helper method `_has_valid_slots` which returns True if a valid `__slots__`
  is found on a ClassDef (re-use logic from `_check_slots`).

- Refactor `_check_redefined_slots` to split out logic for getting `__slots__`
  names into `_get_classdef_slots_names` helper.
Copy link

codecov bot commented Apr 22, 2024

Codecov Report

Attention: Patch coverage is 98.03922% with 1 lines in your changes are missing coverage. Please review.

Project coverage is 95.82%. Comparing base (67bfab4) to head (67b5d50).
Report is 13 commits behind head on main.

Additional details and impacted files

Impacted file tree graph

@@           Coverage Diff           @@
##             main    #9564   +/-   ##
=======================================
  Coverage   95.81%   95.82%           
=======================================
  Files         173      173           
  Lines       18825    18875   +50     
=======================================
+ Hits        18038    18087   +49     
- Misses        787      788    +1     
Files Coverage Δ
pylint/checkers/classes/class_checker.py 93.60% <98.03%> (+0.22%) ⬆️

This comment has been minimized.

@adamtuft
Copy link
Author

adamtuft commented Apr 22, 2024

🤖 Effect of this PR on checked open source code: 🤖

Effect on pandas:
The following messages are now emitted:

  1. declare-non-slot:
    No such name 'values' in slots
    https://github.com/pandas-dev/pandas/blob/bb0fcc23eed9f6a1a6506c6e27b98fb397ce747e/pandas/core/internals/blocks.py#L2097
  2. declare-non-slot:
    No such name 'values' in slots
    https://github.com/pandas-dev/pandas/blob/bb0fcc23eed9f6a1a6506c6e27b98fb397ce747e/pandas/core/internals/blocks.py#L2147

This comment was generated for commit 245be58

Based on these messages, the error should probably exclude any annotation that looks like a descriptor. I'm assuming that's how such members are implemented but I'll need to look into it.

EDIT: upon further reading, an empty __slots__ is actually required when multiple inheritance is involved [link]. I suspect this is what is happening here.

@Pierre-Sassoulas Pierre-Sassoulas added the Enhancement ✨ Improvement to a component label Apr 23, 2024
@Pierre-Sassoulas Pierre-Sassoulas changed the title Add "declare-non-slot" error code (Refs https://github.com/pylint-dev/pylint/issues/9499) Add a new declare-non-slot error code Apr 23, 2024
Copy link
Member

@Pierre-Sassoulas Pierre-Sassoulas left a comment

Choose a reason for hiding this comment

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

Looks great already, thank you. I don't have the time to really review the change at the moment but regarding the CI fail: You can create an example in the doc (see for example bad-chained-comparison : https://github.com/pylint-dev/pylint/tree/main/doc/data/messages/b/bad-chained-comparison) and a changelog entry for towncrier (Some doc here, but you can copy paste an existing fragment of course: https://pylint.readthedocs.io/en/stable/development_guide/contributor_guide/contribute.html#creating-a-pull-request)

Copy link
Collaborator

@DanielNoord DanielNoord left a comment

Choose a reason for hiding this comment

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

Left some comments. Code looks really good, thanks for opening the PR. I intended to have a look at your fork, but this is much easier as I can make in-line comments. Nice job on your first PR!

pylint/checkers/classes/class_checker.py Outdated Show resolved Hide resolved
pylint/checkers/classes/class_checker.py Outdated Show resolved Hide resolved
pylint/checkers/classes/class_checker.py Show resolved Hide resolved
pylint/checkers/classes/class_checker.py Outdated Show resolved Hide resolved
pylint/checkers/classes/class_checker.py Show resolved Hide resolved
@adamtuft
Copy link
Author

Thanks very much for the feedback! I'll incorporate suggestions when I have a little more time, hopefully in the next week or so.

Co-authored-by: Daniël van Noord <13665637+DanielNoord@users.noreply.github.com>
adamtuft and others added 2 commits April 24, 2024 21:48
Co-authored-by: Pierre Sassoulas <pierre.sassoulas@gmail.com>
Avoid unnecessary inference if any ancestor has __dict__ in __slots__

Co-authored-by: Pierre Sassoulas <pierre.sassoulas@gmail.com>
@adamtuft
Copy link
Author

adamtuft commented Apr 24, 2024

Thanks so much for the review comments! I've reflected on my implementation and I think it needs a bit more work 🤔 - just to share a few thoughts:

  • Could we just return early if _get_classdef_slots_names returns []? Add a new declare-non-slot error code #9564 (comment)
  • What to do about empty __slots__, which is required if the class takes part in multiple inheritance [link] (I suspect this is the cause of the messages in this comment).
  • Make it clearer under what conditions the check should return early, and whether to be permissive or conservative when trying to gather names from __slots__ across base classes (I don't want to cause lots of false-positives!)
  • Given these challenges, should this message be restricted just to classes decorated with @dataclass as suggested in the original issue Check "__slots__" and "@dataclass" attributes #9499

This comment has been minimized.

adamtuft and others added 10 commits May 2, 2024 20:10
- An empty __slots__ is required if a class takes part in multiple
  inheritance, so assume this is the case if the base class has an
  empty __slots__ and abort the check.

- Abort early if __dict__ found in any __slots__.

- No need to call self._has_valid_slots in
  self._get_classdef_slots_names since we already call it at the start
  of the check.

- Move definition to below E0244 for consistency, remove "'" around %r
  as this adds extra quotes when formatted.

This comment has been minimized.

@adamtuft
Copy link
Author

adamtuft commented May 2, 2024

Re. reporting an error for an empty __slots__, I think the best thing to do in this case is abort the check. An empty __slots__ is required if a class takes part in multiple inheritance with other classes that may use __slots__, so it is probably best to assume this is the intention if a class has an empty __slots__. This will prevent false positives like those seen above in the internals of Pandas.

Copy link
Contributor

github-actions bot commented May 3, 2024

🤖 According to the primer, this change has no effect on the checked open source code. 🤖🎉

This comment was generated for commit 67b5d50

@DanielNoord
Copy link
Collaborator

Just letting you know that I do want to review this but haven't found the time. I hope to get to this in the next 7 days.

@@ -1485,6 +1536,24 @@ def _check_functools_or_not(self, decorator: nodes.Attribute) -> bool:

return "functools" in dict(import_node.names)

def _has_valid_slots(self, node: nodes.ClassDef) -> bool:
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm on mobile but this seems very similar to _check_slots. I'm a bit worried that we're duplicating checks and run the risk of code drift.

Have you considered refactoring the other method to serve both purposes?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Enhancement ✨ Improvement to a component
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants