Skip to content

[ruff] Abstract methods defined in normal classes (RUF044)#14688

Open
InSyncWithFoo wants to merge 12 commits intoastral-sh:mainfrom
InSyncWithFoo:RUF044
Open

[ruff] Abstract methods defined in normal classes (RUF044)#14688
InSyncWithFoo wants to merge 12 commits intoastral-sh:mainfrom
InSyncWithFoo:RUF044

Conversation

@InSyncWithFoo
Copy link
Contributor

@InSyncWithFoo InSyncWithFoo commented Nov 30, 2024

Summary

Resolves #12861.

This change introduces RUF044 along with a new shared logic for detecting whether a given class is abstract or not. The details on how that works can be found in the doc comments.

Test Plan

cargo nextest run and cargo insta test.

@github-actions
Copy link
Contributor

github-actions bot commented Nov 30, 2024

ruff-ecosystem results

Linter (stable)

✅ ecosystem check detected no linter changes.

Linter (preview)

ℹ️ ecosystem check detected linter changes. (+102 -0 violations, +0 -0 fixes in 12 projects; 43 projects unchanged)

RasaHQ/rasa (+3 -0 violations, +0 -0 fixes)

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

+ rasa/core/featurizers/tracker_featurizers.py:253:9: RUF044 Abstract method defined in non-abstract class
+ rasa/core/policies/policy.py:333:9: RUF044 Abstract method defined in non-abstract class
+ rasa/core/policies/policy.py:354:9: RUF044 Abstract method defined in non-abstract class

apache/airflow (+26 -0 violations, +0 -0 fixes)

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

+ airflow-core/src/airflow/api_fastapi/auth/managers/models/base_user.py:27:9: RUF044 Abstract method defined in non-abstract class
+ airflow-core/src/airflow/api_fastapi/auth/managers/models/base_user.py:30:9: RUF044 Abstract method defined in non-abstract class
+ airflow-core/src/airflow/utils/log/logging_mixin.py:137:9: RUF044 Abstract method defined in non-abstract class
+ airflow-core/src/airflow/utils/log/logging_mixin.py:141:9: RUF044 Abstract method defined in non-abstract class
+ airflow-core/src/airflow/utils/log/logging_mixin.py:146:9: RUF044 Abstract method defined in non-abstract class
+ dev/breeze/src/airflow_breeze/utils/cdxgen.py:331:9: RUF044 Abstract method defined in non-abstract class
+ dev/breeze/src/airflow_breeze/utils/cdxgen.py:335:9: RUF044 Abstract method defined in non-abstract class
+ providers/amazon/src/airflow/providers/amazon/aws/sensors/bedrock.py:88:9: RUF044 Abstract method defined in non-abstract class
+ providers/amazon/src/airflow/providers/amazon/aws/sensors/comprehend.py:80:9: RUF044 Abstract method defined in non-abstract class
+ providers/amazon/src/airflow/providers/amazon/aws/sensors/eks.py:110:9: RUF044 Abstract method defined in non-abstract class
+ providers/amazon/src/airflow/providers/amazon/aws/sensors/eks.py:113:9: RUF044 Abstract method defined in non-abstract class
+ providers/amazon/src/airflow/providers/amazon/aws/triggers/base.py:141:9: RUF044 Abstract method defined in non-abstract class
... 14 additional changes omitted for project

apache/superset (+2 -0 violations, +0 -0 fixes)

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

+ superset/commands/database/uploaders/base.py:81:9: RUF044 Abstract method defined in non-abstract class
+ superset/commands/database/uploaders/base.py:84:9: RUF044 Abstract method defined in non-abstract class

bokeh/bokeh (+1 -0 violations, +0 -0 fixes)

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

+ src/bokeh/command/subcommands/file_output.py:216:9: RUF044 Abstract method defined in non-abstract class

ibis-project/ibis (+30 -0 violations, +0 -0 fixes)

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

+ ibis/backends/sql/__init__.py:183:9: RUF044 Abstract method defined in non-abstract class
+ ibis/common/collections.py:123:9: RUF044 Abstract method defined in non-abstract class
+ ibis/common/collections.py:31:9: RUF044 Abstract method defined in non-abstract class
+ ibis/common/collections.py:39:9: RUF044 Abstract method defined in non-abstract class
+ ibis/common/collections.py:47:9: RUF044 Abstract method defined in non-abstract class
+ ibis/common/collections.py:58:9: RUF044 Abstract method defined in non-abstract class
+ ibis/common/collections.py:66:9: RUF044 Abstract method defined in non-abstract class
+ ibis/common/collections.py:79:9: RUF044 Abstract method defined in non-abstract class
+ ibis/common/deferred.py:29:9: RUF044 Abstract method defined in non-abstract class
+ ibis/common/deferred.py:45:9: RUF044 Abstract method defined in non-abstract class
+ ibis/common/graph.py:258:9: RUF044 Abstract method defined in non-abstract class
+ ibis/common/graph.py:263:9: RUF044 Abstract method defined in non-abstract class
+ ibis/common/patterns.py:201:9: RUF044 Abstract method defined in non-abstract class
+ ibis/common/patterns.py:223:9: RUF044 Abstract method defined in non-abstract class
... 16 additional changes omitted for project

langchain-ai/langchain (+6 -0 violations, +0 -0 fixes)

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

+ libs/core/langchain_core/indexing/base.py:519:9: RUF044 Abstract method defined in non-abstract class
+ libs/core/langchain_core/indexing/base.py:570:9: RUF044 Abstract method defined in non-abstract class
+ libs/core/langchain_core/indexing/base.py:610:9: RUF044 Abstract method defined in non-abstract class
+ libs/core/langchain_core/output_parsers/list.py:51:9: RUF044 Abstract method defined in non-abstract class
+ libs/core/langchain_core/runnables/configurable.py:138:9: RUF044 Abstract method defined in non-abstract class
+ libs/core/langchain_core/tools/base.py:621:9: RUF044 Abstract method defined in non-abstract class

milvus-io/pymilvus (+5 -0 violations, +0 -0 fixes)

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

+ pymilvus/client/abstract.py:499:9: RUF044 Abstract method defined in non-abstract class
+ pymilvus/client/asynch.py:30:9: RUF044 Abstract method defined in non-abstract class
+ pymilvus/client/asynch.py:41:9: RUF044 Abstract method defined in non-abstract class
+ pymilvus/client/asynch.py:49:9: RUF044 Abstract method defined in non-abstract class
+ pymilvus/client/asynch.py:86:9: RUF044 Abstract method defined in non-abstract class

pandas-dev/pandas (+1 -0 violations, +0 -0 fixes)

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

+ pandas/io/formats/info.py:594:9: RUF044 Abstract method defined in non-abstract class

python-poetry/poetry (+2 -0 violations, +0 -0 fixes)

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

+ src/poetry/plugins/base_plugin.py:17:9: RUF044 Abstract method defined in non-abstract class
+ src/poetry/plugins/plugin.py:23:9: RUF044 Abstract method defined in non-abstract class

rotki/rotki (+6 -0 violations, +0 -0 fixes)

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

+ rotkehlchen/chain/ethereum/oracles/uniswap.py:117:9: RUF044 Abstract method defined in non-abstract class
+ rotkehlchen/chain/ethereum/oracles/uniswap.py:125:9: RUF044 Abstract method defined in non-abstract class
+ rotkehlchen/chain/ethereum/oracles/uniswap.py:138:9: RUF044 Abstract method defined in non-abstract class
+ rotkehlchen/chain/evm/decoding/aave/common.py:74:9: RUF044 Abstract method defined in non-abstract class
+ rotkehlchen/exchanges/exchange.py:58:9: RUF044 Abstract method defined in non-abstract class
+ rotkehlchen/utils/interfaces.py:107:9: RUF044 Abstract method defined in non-abstract class

zulip/zulip (+1 -0 violations, +0 -0 fixes)

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

+ zproject/backends.py:2634:9: RUF044 Abstract method defined in non-abstract class

astropy/astropy (+19 -0 violations, +0 -0 fixes)

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

+ astropy/coordinates/representation/base.py:282:9: RUF044 Abstract method defined in non-abstract class
+ astropy/coordinates/representation/base.py:299:9: RUF044 Abstract method defined in non-abstract class
+ astropy/coordinates/representation/base.py:517:9: RUF044 Abstract method defined in non-abstract class
+ astropy/coordinates/representation/base.py:539:9: RUF044 Abstract method defined in non-abstract class
+ astropy/coordinates/transformations/affine.py:210:9: RUF044 Abstract method defined in non-abstract class
+ astropy/cosmology/_src/flrw/base.py:377:9: RUF044 Abstract method defined in non-abstract class
+ astropy/cosmology/_src/io/builtin/model.py:74:9: RUF044 Abstract method defined in non-abstract class
+ astropy/cosmology/_src/io/builtin/model.py:81:9: RUF044 Abstract method defined in non-abstract class
+ astropy/cosmology/_src/tests/flrw/test_base.py:267:9: RUF044 Abstract method defined in non-abstract class
+ astropy/cosmology/_src/tests/flrw/test_base.py:72:9: RUF044 Abstract method defined in non-abstract class
... 9 additional changes omitted for project

Changes by rule (1 rules affected)

code total + violation - violation + fix - fix
RUF044 102 102 0 0 0

@dhruvmanila dhruvmanila added rule Implementing or modifying a lint rule preview Related to preview mode features labels Dec 2, 2024
@InSyncWithFoo
Copy link
Contributor Author

@MichaReiser The function cannot be called is_metaclass_abcmeta because we do not have access to enough information to determine if that is the case. We can only tell if a class "might be" or "is definitely not" abc.ABCMeta. Same for is_abstract_class. I'm sure I have explained this somewhat in the doc comments. See also typing::might_be_generic.

@MichaReiser
Copy link
Member

@InSyncWithFoo what's an example where is_metaclass_abcmeta or is_abstract_class returns true where the class isn't a abcmeta or an abstract class?

if there are indeed cases where some of those methods return false when the class isn't abstract, then it's important that we document these as part of the rule.

@InSyncWithFoo
Copy link
Contributor Author

A few examples:

from abc import ABCMeta
from somewhere import SomeMetaclass  # Could be subclass of ABCMeta, for all we know.

# `B` might be abstract (methods are not checked).
class A(ABCMeta): ...
class B(metaclass=A): ...

# Is `C` abstract or not?
class C(metaclass=SomeMetaclass): ...

# `D` and `E` definitely aren't abstract.
class D: ...
class E(D): ...

@MichaReiser
Copy link
Member

Thanks for the example. I feel like we can keep using is_metacalss_abcmeta and add a documentation that clarifies that:

  • It only returns true for classes that are guaranteed to be meta classes
  • It may return false for classes that are meta classes but Ruff can't detect

@InSyncWithFoo
Copy link
Contributor Author

InSyncWithFoo commented Dec 19, 2024

Here's a table:

Classes Is abstract? might_be_abstract()
class A(ABC) Yes true
class B(A), class A(ABC) Yes true
class A(metaclass=ABCMeta) Yes true
class A(Unknown) Maybe true
class A(metaclass=Unknown) Maybe true
class A No false
class B(A), class A No false
class B(metaclass=A), class A No false

might_be_abstract() returns false when it is possible to determine that the given class is not abstract. It returns true otherwise (when the class is known to be abstract, or when there are not enough information). Same for metaclass_might_be_abcmeta().

@MichaReiser
Copy link
Member

Thanks for the table. I see. I'm then leaning towards simply calling it is_not_abstract.

I also noticed that we have B024 which checks the opposite of this rule. We should make sure that the two rules are consistent in how they detect abstract classes.

fn is_abc_class(bases: &[Expr], keywords: &[Keyword], semantic: &SemanticModel) -> bool {
keywords.iter().any(|keyword| {
keyword.arg.as_ref().is_some_and(|arg| arg == "metaclass")
&& semantic
.resolve_qualified_name(&keyword.value)
.is_some_and(|qualified_name| {
matches!(qualified_name.segments(), ["abc", "ABCMeta"])
})
}) || bases.iter().any(|base| {
semantic
.resolve_qualified_name(base)
.is_some_and(|qualified_name| matches!(qualified_name.segments(), ["abc", "ABC"]))
})
}

@InSyncWithFoo
Copy link
Contributor Author

@MichaReiser is_not_abstract has the "double negative" problem:

// This doesn't read well in English: "If *not* `class` is *not* abstract, bail."
if !is_not_abstract(class, checker.semantic()) {
    return;
}

// This reads better: "If `class` might be abstract, bail."
if might_be_abstract(class, checker.semantic()) {
    return;
}

As for B024 (and B027), I can submit another PR, but I don't think that alone should block this PR, especially when the existing logic was taken dirrectly from upstream.

@MichaReiser
Copy link
Member

MichaReiser commented Dec 21, 2024

@MichaReiser is_not_abstract has the "double negative" problem:

That's fair. My only concern is that might is a bit hand-wavey. It's unclear if it classifies classes as abstract that aren't and/or classes as not abstract that are. The name suggests to me that it could have false positives as well as false negatives. The is_not_abstract is more explicit about it. The name suggests that it never returns true for a class that might be abstract. We can also try to address this in another way. For example by returning an enum with variant names, although I'm not sure how to name the variants to make it clear.

As for B024 (and B027), I can submit another PR, but I don't think that alone should block this PR, especially when the existing logic was taken dirrectly from upstream.

I'm not sure if we want to update B024, instead I think it would be better to align this rule with what's done in B024 and, if we can, avoid any unnecessary code duplication.

@InSyncWithFoo
Copy link
Contributor Author

@MichaReiser

  • B024 does not check for subclasses of ABCMeta; RUF044 currently does.
  • B024 only reports when the class is definitely known to be abstract; RUF044 currently only reports when the class is known to not be abstract.
  • B024 wants to avoid classes that do not inherit directly from ABC or has metaclass=ABCMeta, as such a class might not be abstract.
  • RUF044 wants to avoid classes that have an ABC base or an ABCMeta metaclass, as such a class might be abstract.

Visualization:

         `is_abc_class() == true`
                    |             `might_be_abstract() == true`
                    /-------------------------------------------------------\
Abstractness:   Abstract        Likely abstract        Unknown      Likely not abstract    Not abstract
                    |------------------|------------------|------------------|------------------|
Requirements:  B024, B027                                                                     RUF044

Are you sure that you want RUF044 to use existing logic from B024 when the requirements are so significantly different?

@MichaReiser
Copy link
Member

To me the two rules must be consistent in their behavior. That means they should both respect ABCMeta or not. For now, I suggest to ignore ABCMeta and to create a separate PR that adds support for ABCMeta to both rules. Unless there are specific reasons why B024 doesn't support ABCMeta that I'm overlooking.

Whether both rules use the exact same code is less important but it would be nice if they can share most of the: Does this class implement ABCMeta logic.

@InSyncWithFoo
Copy link
Contributor Author

That B024 does not recognize subclasses of ABCMeta is a different matter, to be addressed in another PR. I can submit that PR tomorrow if you want (reusing all the code from this PR), but I don't see why I have to worsen RUF044's logic to match that, especially when it will be readded again anyway.

B024 wants only direct subclasses of ABC or classes that have (a subclass of) ABCMeta as the metaclass. RUF044 wants classes with no ABC or ABCMeta anywhere in their and their metaclasses' MROs.

Copy link
Member

@MichaReiser MichaReiser left a comment

Choose a reason for hiding this comment

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

My main concern is that this PR adds more code to identify abstract classes that are hidden away in the rule implementation and isn't shared with the logic that other rules use. I'm okay looking over the ABCMeta difference if we plan on addressing that later, as long as my main concern is addressed

@InSyncWithFoo InSyncWithFoo changed the title [ruff] Abstract method defined in normal class (RUF044) [ruff] Abstract methods defined in normal class (RUF044) Feb 15, 2025
@InSyncWithFoo InSyncWithFoo changed the title [ruff] Abstract methods defined in normal class (RUF044) [ruff] Abstract methods defined in normal classes (RUF044) Feb 15, 2025
@InSyncWithFoo
Copy link
Contributor Author

This is ready for a re-review now.

@liammcdevitt73
Copy link

Did this ever get implemented "Abstract methods defined in normal classes (RUF044)" or are there plans to? https://docs.astral.sh/ruff/rules/#ruff-specific-rules-ruf

@amaslenn
Copy link

amaslenn commented Sep 5, 2025

@InSyncWithFoo what is the state of this feature? I find it useful and ready to help if needed.

@InSyncWithFoo
Copy link
Contributor Author

@amaslenn This PR is outdated due to not having a reviewer. I'm willing to update it as long as there's one.

@amaslenn
Copy link

amaslenn commented Sep 8, 2025

@amaslenn This PR is outdated due to not having a reviewer. I'm willing to update it as long as there's one.

@MichaReiser could you please review this PR?

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.

Rule request: abstractmethod appears on normal class

6 participants