diff --git a/crates/ruff_linter/resources/test/fixtures/refurb/FURB180.py b/crates/ruff_linter/resources/test/fixtures/refurb/FURB180.py index 33576061fc23a..eca6eb12e5b92 100644 --- a/crates/ruff_linter/resources/test/fixtures/refurb/FURB180.py +++ b/crates/ruff_linter/resources/test/fixtures/refurb/FURB180.py @@ -1,5 +1,9 @@ import abc +import typing +import typing_extensions from abc import abstractmethod, ABCMeta +from typing import Protocol as TProtocol +from typing_extensions import Protocol as TEProtocol # Errors @@ -32,6 +36,14 @@ class A3(B0, before_metaclass=1, metaclass=abc.ABCMeta): pass +class Protocol: + pass + + +class C1(Protocol, metaclass=ABCMeta): + pass + + # OK class Meta(type): @@ -56,3 +68,23 @@ def foo(self): pass class A7(B0, abc.ABC, B1): @abstractmethod def foo(self): pass + + +class A8(typing.Protocol, metaclass=ABCMeta): + @abstractmethod + def foo(self): pass + + +class A9(typing_extensions.Protocol, metaclass=ABCMeta): + @abstractmethod + def foo(self): pass + + +class A10(TProtocol, metaclass=ABCMeta): + @abstractmethod + def foo(self): pass + + +class A11(TEProtocol, metaclass=ABCMeta): + @abstractmethod + def foo(self): pass diff --git a/crates/ruff_linter/resources/test/fixtures/refurb/FURB180_exceptions.py b/crates/ruff_linter/resources/test/fixtures/refurb/FURB180_exceptions.py new file mode 100644 index 0000000000000..e915d5b4cb19f --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/refurb/FURB180_exceptions.py @@ -0,0 +1,83 @@ +import abc +import ast +from abc import abstractmethod, ABCMeta +from ast import Name + + +# marked as exempt base class +class A0: + pass + + +# not exempt base class +class A1: + pass + + +# Errors + +class B0(A1, metaclass=abc.ABCMeta): + @abstractmethod + def foo(self): pass + + +class B1(ast.Param, metaclass=abc.ABCMeta): + @abstractmethod + def foo(self): pass + + +class B2(A1, ast.Param, metaclass=abc.ABCMeta): + @abstractmethod + def foo(self): pass + + +# OK + +class C0(A0, metaclass=abc.ABCMeta): + @abstractmethod + def foo(self): pass + + +class C1(ast.Name, metaclass=abc.ABCMeta): + @abstractmethod + def foo(self): pass + + +class C2(Name, metaclass=abc.ABCMeta): + @abstractmethod + def foo(self): pass + + +class C3(ast.Param, A0, metaclass=abc.ABCMeta): + @abstractmethod + def foo(self): pass + + +class C4(A0, ast.Param, metaclass=abc.ABCMeta): + @abstractmethod + def foo(self): pass + + +class C5(ast.Name, A1, metaclass=abc.ABCMeta): + @abstractmethod + def foo(self): pass + + +class C6(A1, ast.Name, metaclass=abc.ABCMeta): + @abstractmethod + def foo(self): pass + + +class C6(Name, A1, metaclass=abc.ABCMeta): + @abstractmethod + def foo(self): pass + + +class C7(A1, Name, metaclass=abc.ABCMeta): + @abstractmethod + def foo(self): pass + + +class C8(A0, Name, metaclass=abc.ABCMeta): + @abstractmethod + def foo(self): pass diff --git a/crates/ruff_linter/src/rules/refurb/mod.rs b/crates/ruff_linter/src/rules/refurb/mod.rs index a14531e43aa4a..4f5bd5439f165 100644 --- a/crates/ruff_linter/src/rules/refurb/mod.rs +++ b/crates/ruff_linter/src/rules/refurb/mod.rs @@ -2,6 +2,7 @@ mod helpers; pub(crate) mod rules; +pub mod settings; #[cfg(test)] mod tests { @@ -12,6 +13,7 @@ mod tests { use test_case::test_case; use crate::registry::Rule; + use crate::rules::refurb; use crate::test::test_path; use crate::{assert_messages, settings}; @@ -101,4 +103,25 @@ mod tests { assert_messages!(diagnostics); Ok(()) } + + #[test] + fn allowed_abc_meta_bases() -> Result<()> { + let rule_code = Rule::MetaClassABCMeta; + let path = Path::new("FURB180_exceptions.py"); + let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy()); + let diagnostics = test_path( + Path::new("refurb").join(path).as_path(), + &settings::LinterSettings { + refurb: refurb::settings::Settings { + allowed_abc_meta_bases: ["ast.Name", "FURB180_exceptions.A0"] + .into_iter() + .map(String::from) + .collect(), + }, + ..settings::LinterSettings::for_rule(rule_code) + }, + )?; + assert_messages!(snapshot, diagnostics); + Ok(()) + } } diff --git a/crates/ruff_linter/src/rules/refurb/rules/metaclass_abcmeta.rs b/crates/ruff_linter/src/rules/refurb/rules/metaclass_abcmeta.rs index 96016b03514ec..9b4a1f41e90fa 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/metaclass_abcmeta.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/metaclass_abcmeta.rs @@ -37,6 +37,10 @@ use crate::{AlwaysFixableViolation, Edit, Fix}; /// be validating the class's other base classes (e.g., `typing.Protocol` does this) or otherwise /// alter runtime behavior if more base classes are added. /// +/// ## Options +/// - `lint.refurb.allowed-abc-meta-bases` +/// - `lint.refurb.extend-allowed-abc-meta-bases` +/// /// ## References /// - [Python documentation: `abc.ABC`](https://docs.python.org/3/library/abc.html#abc.ABC) /// - [Python documentation: `abc.ABCMeta`](https://docs.python.org/3/library/abc.html#abc.ABCMeta) @@ -75,6 +79,20 @@ pub(crate) fn metaclass_abcmeta(checker: &Checker, class_def: &StmtClassDef) { return; } + // Determine if base classes contain an exempted class per configuration + let allowed_abc_meta_bases = &checker.settings.refurb.allowed_abc_meta_bases; + let has_exempt_base = class_def.bases().iter().any(|base| { + checker + .semantic() + .resolve_qualified_name(base) + .map(|qualified_name| qualified_name.to_string()) + .is_some_and(|name| allowed_abc_meta_bases.contains(&name)) + }); + + if has_exempt_base { + return; + } + let applicability = if class_def.bases().is_empty() { Applicability::Safe } else { diff --git a/crates/ruff_linter/src/rules/refurb/settings.rs b/crates/ruff_linter/src/rules/refurb/settings.rs new file mode 100644 index 0000000000000..c5ab06a3524d7 --- /dev/null +++ b/crates/ruff_linter/src/rules/refurb/settings.rs @@ -0,0 +1,41 @@ +//! Settings for the `refurb` plugin. + +use std::fmt; + +use ruff_macros::CacheKey; +use rustc_hash::FxHashSet; + +use crate::display_settings; + +pub fn default_allowed_abc_meta_bases() -> FxHashSet { + ["typing.Protocol", "typing_extensions.Protocol"] + .into_iter() + .map(ToString::to_string) + .collect() +} + +#[derive(Debug, Clone, CacheKey)] +pub struct Settings { + pub allowed_abc_meta_bases: FxHashSet, +} + +impl Default for Settings { + fn default() -> Self { + Self { + allowed_abc_meta_bases: default_allowed_abc_meta_bases(), + } + } +} + +impl fmt::Display for Settings { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + display_settings! { + formatter = f, + namespace = "linter.refurb", + fields = [ + self.allowed_abc_meta_bases | set + ] + } + Ok(()) + } +} diff --git a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB180_FURB180.py.snap b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB180_FURB180.py.snap index d351d92f93732..186bdaeee290e 100644 --- a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB180_FURB180.py.snap +++ b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB180_FURB180.py.snap @@ -1,79 +1,97 @@ --- source: crates/ruff_linter/src/rules/refurb/mod.rs --- -FURB180.py:7:10: FURB180 [*] Use of `metaclass=abc.ABCMeta` to define abstract base class - | -5 | # Errors -6 | -7 | class A0(metaclass=abc.ABCMeta): - | ^^^^^^^^^^^^^^^^^^^^^ FURB180 -8 | @abstractmethod -9 | def foo(self): pass - | - = help: Replace with `abc.ABC` +FURB180.py:11:10: FURB180 [*] Use of `metaclass=abc.ABCMeta` to define abstract base class + | + 9 | # Errors +10 | +11 | class A0(metaclass=abc.ABCMeta): + | ^^^^^^^^^^^^^^^^^^^^^ FURB180 +12 | @abstractmethod +13 | def foo(self): pass + | + = help: Replace with `abc.ABC` ℹ Safe fix -4 4 | -5 5 | # Errors -6 6 | -7 |-class A0(metaclass=abc.ABCMeta): - 7 |+class A0(abc.ABC): -8 8 | @abstractmethod -9 9 | def foo(self): pass +8 8 | +9 9 | # Errors 10 10 | +11 |-class A0(metaclass=abc.ABCMeta): + 11 |+class A0(abc.ABC): +12 12 | @abstractmethod +13 13 | def foo(self): pass +14 14 | -FURB180.py:12:10: FURB180 [*] Use of `metaclass=abc.ABCMeta` to define abstract base class +FURB180.py:16:10: FURB180 [*] Use of `metaclass=abc.ABCMeta` to define abstract base class | -12 | class A1(metaclass=ABCMeta): +16 | class A1(metaclass=ABCMeta): | ^^^^^^^^^^^^^^^^^ FURB180 -13 | @abstractmethod -14 | def foo(self): pass +17 | @abstractmethod +18 | def foo(self): pass | = help: Replace with `abc.ABC` ℹ Safe fix -9 9 | def foo(self): pass -10 10 | -11 11 | -12 |-class A1(metaclass=ABCMeta): - 12 |+class A1(abc.ABC): -13 13 | @abstractmethod -14 14 | def foo(self): pass +13 13 | def foo(self): pass +14 14 | 15 15 | +16 |-class A1(metaclass=ABCMeta): + 16 |+class A1(abc.ABC): +17 17 | @abstractmethod +18 18 | def foo(self): pass +19 19 | -FURB180.py:26:18: FURB180 [*] Use of `metaclass=abc.ABCMeta` to define abstract base class +FURB180.py:30:18: FURB180 [*] Use of `metaclass=abc.ABCMeta` to define abstract base class | -26 | class A2(B0, B1, metaclass=ABCMeta): +30 | class A2(B0, B1, metaclass=ABCMeta): | ^^^^^^^^^^^^^^^^^ FURB180 -27 | @abstractmethod -28 | def foo(self): pass +31 | @abstractmethod +32 | def foo(self): pass | = help: Replace with `abc.ABC` ℹ Unsafe fix -23 23 | pass -24 24 | -25 25 | -26 |-class A2(B0, B1, metaclass=ABCMeta): - 26 |+class A2(B0, B1, abc.ABC): -27 27 | @abstractmethod -28 28 | def foo(self): pass +27 27 | pass +28 28 | 29 29 | +30 |-class A2(B0, B1, metaclass=ABCMeta): + 30 |+class A2(B0, B1, abc.ABC): +31 31 | @abstractmethod +32 32 | def foo(self): pass +33 33 | -FURB180.py:31:34: FURB180 [*] Use of `metaclass=abc.ABCMeta` to define abstract base class +FURB180.py:35:34: FURB180 [*] Use of `metaclass=abc.ABCMeta` to define abstract base class | -31 | class A3(B0, before_metaclass=1, metaclass=abc.ABCMeta): +35 | class A3(B0, before_metaclass=1, metaclass=abc.ABCMeta): | ^^^^^^^^^^^^^^^^^^^^^ FURB180 -32 | pass +36 | pass | = help: Replace with `abc.ABC` ℹ Unsafe fix -28 28 | def foo(self): pass -29 29 | -30 30 | -31 |-class A3(B0, before_metaclass=1, metaclass=abc.ABCMeta): - 31 |+class A3(B0, abc.ABC, before_metaclass=1): -32 32 | pass +32 32 | def foo(self): pass 33 33 | -34 34 | +34 34 | +35 |-class A3(B0, before_metaclass=1, metaclass=abc.ABCMeta): + 35 |+class A3(B0, abc.ABC, before_metaclass=1): +36 36 | pass +37 37 | +38 38 | + +FURB180.py:43:20: FURB180 [*] Use of `metaclass=abc.ABCMeta` to define abstract base class + | +43 | class C1(Protocol, metaclass=ABCMeta): + | ^^^^^^^^^^^^^^^^^ FURB180 +44 | pass + | + = help: Replace with `abc.ABC` + +ℹ Unsafe fix +40 40 | pass +41 41 | +42 42 | +43 |-class C1(Protocol, metaclass=ABCMeta): + 43 |+class C1(Protocol, abc.ABC): +44 44 | pass +45 45 | +46 46 | diff --git a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB180_FURB180_exceptions.py.snap b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB180_FURB180_exceptions.py.snap new file mode 100644 index 0000000000000..8bacf269e642b --- /dev/null +++ b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB180_FURB180_exceptions.py.snap @@ -0,0 +1,61 @@ +--- +source: crates/ruff_linter/src/rules/refurb/mod.rs +--- +FURB180_exceptions.py:19:14: FURB180 [*] Use of `metaclass=abc.ABCMeta` to define abstract base class + | +17 | # Errors +18 | +19 | class B0(A1, metaclass=abc.ABCMeta): + | ^^^^^^^^^^^^^^^^^^^^^ FURB180 +20 | @abstractmethod +21 | def foo(self): pass + | + = help: Replace with `abc.ABC` + +ℹ Unsafe fix +16 16 | +17 17 | # Errors +18 18 | +19 |-class B0(A1, metaclass=abc.ABCMeta): + 19 |+class B0(A1, abc.ABC): +20 20 | @abstractmethod +21 21 | def foo(self): pass +22 22 | + +FURB180_exceptions.py:24:21: FURB180 [*] Use of `metaclass=abc.ABCMeta` to define abstract base class + | +24 | class B1(ast.Param, metaclass=abc.ABCMeta): + | ^^^^^^^^^^^^^^^^^^^^^ FURB180 +25 | @abstractmethod +26 | def foo(self): pass + | + = help: Replace with `abc.ABC` + +ℹ Unsafe fix +21 21 | def foo(self): pass +22 22 | +23 23 | +24 |-class B1(ast.Param, metaclass=abc.ABCMeta): + 24 |+class B1(ast.Param, abc.ABC): +25 25 | @abstractmethod +26 26 | def foo(self): pass +27 27 | + +FURB180_exceptions.py:29:25: FURB180 [*] Use of `metaclass=abc.ABCMeta` to define abstract base class + | +29 | class B2(A1, ast.Param, metaclass=abc.ABCMeta): + | ^^^^^^^^^^^^^^^^^^^^^ FURB180 +30 | @abstractmethod +31 | def foo(self): pass + | + = help: Replace with `abc.ABC` + +ℹ Unsafe fix +26 26 | def foo(self): pass +27 27 | +28 28 | +29 |-class B2(A1, ast.Param, metaclass=abc.ABCMeta): + 29 |+class B2(A1, ast.Param, abc.ABC): +30 30 | @abstractmethod +31 31 | def foo(self): pass +32 32 | diff --git a/crates/ruff_linter/src/settings/mod.rs b/crates/ruff_linter/src/settings/mod.rs index bfec035a594cd..31f5d62ff0424 100644 --- a/crates/ruff_linter/src/settings/mod.rs +++ b/crates/ruff_linter/src/settings/mod.rs @@ -20,7 +20,7 @@ use crate::rules::{ flake8_comprehensions, flake8_copyright, flake8_errmsg, flake8_gettext, flake8_implicit_str_concat, flake8_import_conventions, flake8_pytest_style, flake8_quotes, flake8_self, flake8_tidy_imports, flake8_type_checking, flake8_unused_arguments, isort, mccabe, - pep8_naming, pycodestyle, pydoclint, pydocstyle, pyflakes, pylint, pyupgrade, ruff, + pep8_naming, pycodestyle, pydoclint, pydocstyle, pyflakes, pylint, pyupgrade, refurb, ruff, }; use crate::settings::types::{CompiledPerFileIgnoreList, ExtensionMapping, FilePatternSet}; use crate::{RuleSelector, codes, fs}; @@ -279,6 +279,7 @@ pub struct LinterSettings { pub pyflakes: pyflakes::settings::Settings, pub pylint: pylint::settings::Settings, pub pyupgrade: pyupgrade::settings::Settings, + pub refurb: refurb::settings::Settings, pub ruff: ruff::settings::Settings, } @@ -448,6 +449,7 @@ impl LinterSettings { pyflakes: pyflakes::settings::Settings::default(), pylint: pylint::settings::Settings::default(), pyupgrade: pyupgrade::settings::Settings::default(), + refurb: refurb::settings::Settings::default(), ruff: ruff::settings::Settings::default(), preview: PreviewMode::default(), explicit_preview_rules: false, diff --git a/crates/ruff_workspace/src/configuration.rs b/crates/ruff_workspace/src/configuration.rs index 684a2b434dfa0..e486ea4c5d626 100644 --- a/crates/ruff_workspace/src/configuration.rs +++ b/crates/ruff_workspace/src/configuration.rs @@ -53,7 +53,8 @@ use crate::options::{ Flake8QuotesOptions, Flake8SelfOptions, Flake8TidyImportsOptions, Flake8TypeCheckingOptions, Flake8UnusedArgumentsOptions, FormatOptions, IsortOptions, LintCommonOptions, LintOptions, McCabeOptions, Options, Pep8NamingOptions, PyUpgradeOptions, PycodestyleOptions, - PydoclintOptions, PydocstyleOptions, PyflakesOptions, PylintOptions, RuffOptions, + PydoclintOptions, PydocstyleOptions, PyflakesOptions, PylintOptions, RefurbOptions, + RuffOptions, }; use crate::settings::{ EXCLUDE, FileResolverSettings, FormatterSettings, INCLUDE, LineEnding, Settings, @@ -428,6 +429,10 @@ impl Configuration { .pyupgrade .map(PyUpgradeOptions::into_settings) .unwrap_or_default(), + refurb: lint + .refurb + .map(RefurbOptions::into_settings) + .unwrap_or_default(), ruff: lint .ruff .map(RuffOptions::into_settings) @@ -665,6 +670,7 @@ pub struct LintConfiguration { pub pyflakes: Option, pub pylint: Option, pub pyupgrade: Option, + pub refurb: Option, pub ruff: Option, } @@ -781,6 +787,7 @@ impl LintConfiguration { pyflakes: options.common.pyflakes, pylint: options.common.pylint, pyupgrade: options.common.pyupgrade, + refurb: options.refurb, ruff: options.ruff, }) } @@ -1178,6 +1185,7 @@ impl LintConfiguration { pyflakes: self.pyflakes.combine(config.pyflakes), pylint: self.pylint.combine(config.pylint), pyupgrade: self.pyupgrade.combine(config.pyupgrade), + refurb: self.refurb.combine(config.refurb), ruff: self.ruff.combine(config.ruff), typing_extensions: self.typing_extensions.or(config.typing_extensions), } diff --git a/crates/ruff_workspace/src/options.rs b/crates/ruff_workspace/src/options.rs index b68fc08ea9682..8712baca52e33 100644 --- a/crates/ruff_workspace/src/options.rs +++ b/crates/ruff_workspace/src/options.rs @@ -24,7 +24,7 @@ use ruff_linter::rules::{ flake8_copyright, flake8_errmsg, flake8_gettext, flake8_implicit_str_concat, flake8_import_conventions, flake8_pytest_style, flake8_quotes, flake8_self, flake8_tidy_imports, flake8_type_checking, flake8_unused_arguments, isort, mccabe, pep8_naming, - pycodestyle, pydoclint, pydocstyle, pyflakes, pylint, pyupgrade, ruff, + pycodestyle, pydoclint, pydocstyle, pyflakes, pylint, pyupgrade, refurb, ruff, }; use ruff_linter::settings::types::{ IdentifierPattern, OutputFormat, PythonVersion, RequiredVersion, @@ -498,6 +498,10 @@ pub struct LintOptions { #[option_group] pub pydoclint: Option, + /// Options for the `refurb` plugin + #[option_group] + pub refurb: Option, + /// Options for the `ruff` plugin #[option_group] pub ruff: Option, @@ -3384,6 +3388,69 @@ impl PyUpgradeOptions { } } +/// Options for the `refurb` plugin +#[derive( + Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize, OptionsMetadata, CombineOptions, +)] +#[serde(deny_unknown_fields, rename_all = "kebab-case")] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +pub struct RefurbOptions { + /// Define a list of classes. If a class `C` is a subclass of at least one class in this list, + /// `refurb` will allow `C` to explicitly set its metaclass to `abc.ABCMeta`, instead of + /// insisting on inheriting from `abc.ABC` instead. + /// + /// This is useful for classes that validate their base-classes at runtime. + /// + /// For example, if `SpecialBaseClass` is in the list, while `OtherBaseClass` + /// is not: + /// + /// ```python + /// class MyClass1(SpecialBaseClass, metaclass=abc.ABCMeta): # this is allowed + /// pass + /// + /// + /// class MyClass2(OtherBaseClass, metaclass=abc.ABCMeta): # this will be linted + /// pass + /// + /// + /// class MyClass2(OtherBaseClass, abc.ABC): # instead this is the way to go + /// pass + /// ``` + /// + /// Expects to receive a list of fully-qualified names (e.g., `typing.Protocol`, rather than + /// `Protocol`). + #[option( + default = r#"["typing.Protocol", "typing_extensions.Protocol"]"#, + value_type = "list[str]", + example = r#"allowed-abc-meta-bases = ["my_package.SpecialBaseClass"]"# + )] + pub allowed_abc_meta_bases: Option>, + + /// A list of additional classes to [`allowed_abc_meta_bases`](#lint_refurb_allowed-abc-meta-bases). + /// + /// Expects to receive a list of fully-qualified names (e.g., `typing.Protocol`, rather than + /// `Protocol`). + #[option( + default = r#"[]"#, + value_type = "list[str]", + example = r#"extend-allowed-abc-meta-bases = ["my_package.SpecialBaseClass"]"# + )] + pub extend_allowed_abc_meta_bases: Option>, +} + +impl RefurbOptions { + pub fn into_settings(self) -> refurb::settings::Settings { + refurb::settings::Settings { + allowed_abc_meta_bases: self + .allowed_abc_meta_bases + .unwrap_or_else(refurb::settings::default_allowed_abc_meta_bases) + .into_iter() + .chain(self.extend_allowed_abc_meta_bases.unwrap_or_default()) + .collect(), + } + } +} + /// Options for the `ruff` plugin #[derive( Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize, OptionsMetadata, CombineOptions, @@ -3892,6 +3959,7 @@ pub struct LintOptionsWire { exclude: Option>, pydoclint: Option, + refurb: Option, ruff: Option, preview: Option, typing_extensions: Option, @@ -3947,6 +4015,7 @@ impl From for LintOptions { extend_per_file_ignores, exclude, pydoclint, + refurb, ruff, preview, typing_extensions, @@ -4003,6 +4072,7 @@ impl From for LintOptions { }, exclude, pydoclint, + refurb, ruff, preview, typing_extensions, diff --git a/ruff.schema.json b/ruff.schema.json index 7bf97547310a7..735c86c5da973 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -2428,6 +2428,17 @@ } ] }, + "refurb": { + "description": "Options for the `refurb` plugin", + "anyOf": [ + { + "$ref": "#/definitions/RefurbOptions" + }, + { + "type": "null" + } + ] + }, "ruff": { "description": "Options for the `ruff` plugin", "anyOf": [ @@ -2878,6 +2889,35 @@ "preserve" ] }, + "RefurbOptions": { + "description": "Options for the `refurb` plugin", + "type": "object", + "properties": { + "allowed-abc-meta-bases": { + "description": "Define a list of classes. If a class `C` is a subclass of at least one class in this list, `refurb` will allow `C` to explicitly set its metaclass to `abc.ABCMeta`, instead of insisting on inheriting from `abc.ABC` instead.\n\nThis is useful for classes that validate their base-classes at runtime.\n\nFor example, if `SpecialBaseClass` is in the list, while `OtherBaseClass` is not:\n\n```python class MyClass1(SpecialBaseClass, metaclass=abc.ABCMeta): # this is allowed pass\n\nclass MyClass2(OtherBaseClass, metaclass=abc.ABCMeta): # this will be linted pass\n\nclass MyClass2(OtherBaseClass, abc.ABC): # instead this is the way to go pass ```\n\nExpects to receive a list of fully-qualified names (e.g., `typing.Protocol`, rather than `Protocol`).", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + }, + "uniqueItems": true + }, + "extend-allowed-abc-meta-bases": { + "description": "A list of additional classes to [`allowed_abc_meta_bases`](#lint_refurb_allowed-abc-meta-bases).\n\nExpects to receive a list of fully-qualified names (e.g., `typing.Protocol`, rather than `Protocol`).", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + }, + "uniqueItems": true + } + }, + "additionalProperties": false + }, "RelativeImportsOrder": { "oneOf": [ {