[ruff] Add non-empty-init-module (RUF067) (#22143)

Summary
--

This PR adds a new rule, `non-empty-init-module`, which restricts the
kind of
code that can be included in an `__init__.py` file. By default,
docstrings,
imports, and assignments to `__all__` are allowed. When the new
configuration
option `lint.ruff.strictly-empty-init-modules` is enabled, no code at
all is
allowed.

This closes #9848, where these two variants correspond to different
rules in the

[`flake8-empty-init-modules`](https://github.com/samueljsb/flake8-empty-init-modules/)
linter. The upstream rules are EIM001, which bans all code, and EIM002,
which
bans non-import/docstring/`__all__` code. Since we discussed folding
these into
one rule on [Discord], I just added the rule to the `RUF` group instead
of
adding a new `EIM` plugin.

I'm not really sure we need to flag docstrings even when the strict
setting is
enabled, but I just followed upstream for now. Similarly, as I noted in
a TODO
comment, we could also allow more statements involving `__all__`, such
as
`__all__.append(...)` or `__all__.extend(...)`. The current version only
allows
assignments, like upstream, as well as annotated and augmented
assignments,
unlike upstream.

I think when we discussed this previously, we considered flagging the
module
itself as containing code, but for now I followed the upstream
implementation of
flagging each statement in the module that breaks the rule (actually the
upstream linter flags each _line_, including comments). This will
obviously be a
bit noisier, emitting many diagnostics for the same module. But this
also seems
preferable because it flags every statement that needs to be fixed up
front
instead of only emitting one diagnostic for the whole file that persists
as you
keep removing more lines. It was also easy to implement in
`analyze::statement`
without a separate visitor.

The first commit adds the rule and baseline tests, the second commit
adds the
option and a diff test showing the additional diagnostics when the
setting is
enabled.

I noticed a small (~2%) performance regression on our largest benchmark,
so I also added a cached `Checker::in_init_module` field and method
instead of the `Checker::path` method. This was almost the only reason
for the `Checker::path` field at all, but there's one remaining
reference in a `warn_user!`
[call](https://github.com/astral-sh/ruff/blob/main/crates/ruff_linter/src/checkers/ast/analyze/definitions.rs#L188).

Test Plan
--

New tests adapted from the upstream linter

## Ecosystem Report

I've spot-checked the ecosystem report, and the results look "correct."
This is obviously a very noisy rule if you do include code in
`__init__.py` files. We could make it less noisy by adding more
exceptions (e.g. allowing `if TYPE_CHECKING` blocks, allowing
`__getattr__` functions, allowing imports from `importlib` assignments),
but I'm sort of inclined just to start simple and see what users need.

[Discord]:
https://discord.com/channels/1039017663004942429/1082324250112823306/1440086001035771985

---------

Co-authored-by: Micha Reiser <micha@reiser.io>
This commit is contained in:
Brent Westbrook
2025-12-30 11:32:10 -05:00
committed by GitHub
parent 57218753be
commit c483b59ddd
27 changed files with 843 additions and 9 deletions

View File

@@ -261,6 +261,7 @@ linter.pylint.max_locals = 15
linter.pylint.max_nested_blocks = 5
linter.pyupgrade.keep_runtime_typing = false
linter.ruff.parenthesize_tuple_in_subscript = false
linter.ruff.strictly_empty_init_modules = false
# Formatter Settings
formatter.exclude = []

View File

@@ -263,6 +263,7 @@ linter.pylint.max_locals = 15
linter.pylint.max_nested_blocks = 5
linter.pyupgrade.keep_runtime_typing = false
linter.ruff.parenthesize_tuple_in_subscript = false
linter.ruff.strictly_empty_init_modules = false
# Formatter Settings
formatter.exclude = []

View File

@@ -265,6 +265,7 @@ linter.pylint.max_locals = 15
linter.pylint.max_nested_blocks = 5
linter.pyupgrade.keep_runtime_typing = false
linter.ruff.parenthesize_tuple_in_subscript = false
linter.ruff.strictly_empty_init_modules = false
# Formatter Settings
formatter.exclude = []

View File

@@ -265,6 +265,7 @@ linter.pylint.max_locals = 15
linter.pylint.max_nested_blocks = 5
linter.pyupgrade.keep_runtime_typing = false
linter.ruff.parenthesize_tuple_in_subscript = false
linter.ruff.strictly_empty_init_modules = false
# Formatter Settings
formatter.exclude = []

View File

@@ -262,6 +262,7 @@ linter.pylint.max_locals = 15
linter.pylint.max_nested_blocks = 5
linter.pyupgrade.keep_runtime_typing = false
linter.ruff.parenthesize_tuple_in_subscript = false
linter.ruff.strictly_empty_init_modules = false
# Formatter Settings
formatter.exclude = []

View File

@@ -262,6 +262,7 @@ linter.pylint.max_locals = 15
linter.pylint.max_nested_blocks = 5
linter.pyupgrade.keep_runtime_typing = false
linter.ruff.parenthesize_tuple_in_subscript = false
linter.ruff.strictly_empty_init_modules = false
# Formatter Settings
formatter.exclude = []

View File

@@ -261,6 +261,7 @@ linter.pylint.max_locals = 15
linter.pylint.max_nested_blocks = 5
linter.pyupgrade.keep_runtime_typing = false
linter.ruff.parenthesize_tuple_in_subscript = false
linter.ruff.strictly_empty_init_modules = false
# Formatter Settings
formatter.exclude = []

View File

@@ -261,6 +261,7 @@ linter.pylint.max_locals = 15
linter.pylint.max_nested_blocks = 5
linter.pyupgrade.keep_runtime_typing = false
linter.ruff.parenthesize_tuple_in_subscript = false
linter.ruff.strictly_empty_init_modules = false
# Formatter Settings
formatter.exclude = []

View File

@@ -261,6 +261,7 @@ linter.pylint.max_locals = 15
linter.pylint.max_nested_blocks = 5
linter.pyupgrade.keep_runtime_typing = false
linter.ruff.parenthesize_tuple_in_subscript = false
linter.ruff.strictly_empty_init_modules = false
# Formatter Settings
formatter.exclude = []

View File

@@ -374,6 +374,7 @@ linter.pylint.max_locals = 15
linter.pylint.max_nested_blocks = 5
linter.pyupgrade.keep_runtime_typing = false
linter.ruff.parenthesize_tuple_in_subscript = false
linter.ruff.strictly_empty_init_modules = false
# Formatter Settings
formatter.exclude = []

View File

@@ -0,0 +1,51 @@
"""This is the module docstring."""
# convenience imports:
import os
from pathlib import Path
__all__ = ["MY_CONSTANT"]
__all__ += ["foo"]
__all__: list[str] = __all__
__all__ = __all__ = __all__
MY_CONSTANT = 5
"""This is an important constant."""
os.environ["FOO"] = 1
def foo():
return Path("foo.py")
def __getattr__(name): # ok
return name
__path__ = __import__('pkgutil').extend_path(__path__, __name__) # ok
if os.environ["FOO"] != "1": # RUF067
MY_CONSTANT = 4 # ok, don't flag nested statements
if TYPE_CHECKING: # ok
MY_CONSTANT = 3
import typing
if typing.TYPE_CHECKING: # ok
MY_CONSTANT = 2
__version__ = "1.2.3" # ok
def __dir__(): # ok
return ["foo"]
import pkgutil
__path__ = pkgutil.extend_path(__path__, __name__) # ok
__path__ = unknown.extend_path(__path__, __name__) # also ok
# non-`extend_path` assignments are not allowed
__path__ = 5 # RUF067
# also allow `__author__`
__author__ = "The Author" # ok

View File

@@ -0,0 +1,54 @@
"""
The code here is not in an `__init__.py` file and should not trigger the
lint.
"""
# convenience imports:
import os
from pathlib import Path
__all__ = ["MY_CONSTANT"]
__all__ += ["foo"]
__all__: list[str] = __all__
__all__ = __all__ = __all__
MY_CONSTANT = 5
"""This is an important constant."""
os.environ["FOO"] = 1
def foo():
return Path("foo.py")
def __getattr__(name): # ok
return name
__path__ = __import__('pkgutil').extend_path(__path__, __name__) # ok
if os.environ["FOO"] != "1": # RUF067
MY_CONSTANT = 4 # ok, don't flag nested statements
if TYPE_CHECKING: # ok
MY_CONSTANT = 3
import typing
if typing.TYPE_CHECKING: # ok
MY_CONSTANT = 2
__version__ = "1.2.3" # ok
def __dir__(): # ok
return ["foo"]
import pkgutil
__path__ = pkgutil.extend_path(__path__, __name__) # ok
__path__ = unknown.extend_path(__path__, __name__) # also ok
# non-`extend_path` assignments are not allowed
__path__ = 5 # RUF067
# also allow `__author__`
__author__ = "The Author" # ok

View File

@@ -1630,4 +1630,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
}
_ => {}
}
if checker.is_rule_enabled(Rule::NonEmptyInitModule) {
ruff::rules::non_empty_init_module(checker, stmt);
}
}

View File

@@ -33,7 +33,7 @@ pub(crate) fn unresolved_references(checker: &Checker) {
}
// Allow __path__.
if checker.path.ends_with("__init__.py") {
if checker.in_init_module() {
if reference.name(checker.source()) == "__path__" {
continue;
}

View File

@@ -21,7 +21,7 @@
//! represents the lint-rule analysis phase. In the future, these steps may be separated into
//! distinct passes over the AST.
use std::cell::RefCell;
use std::cell::{OnceCell, RefCell};
use std::path::Path;
use itertools::Itertools;
@@ -198,6 +198,8 @@ pub(crate) struct Checker<'a> {
parsed_type_annotation: Option<&'a ParsedAnnotation>,
/// The [`Path`] to the file under analysis.
path: &'a Path,
/// Whether `path` points to an `__init__.py` file.
in_init_module: OnceCell<bool>,
/// The [`Path`] to the package containing the current file.
package: Option<PackageRoot<'a>>,
/// The module representation of the current file (e.g., `foo.bar`).
@@ -274,6 +276,7 @@ impl<'a> Checker<'a> {
noqa_line_for,
noqa,
path,
in_init_module: OnceCell::new(),
package,
module,
source_type,
@@ -482,9 +485,11 @@ impl<'a> Checker<'a> {
self.context.settings
}
/// The [`Path`] to the file under analysis.
pub(crate) const fn path(&self) -> &'a Path {
self.path
/// Returns whether the file under analysis is an `__init__.py` file.
pub(crate) fn in_init_module(&self) -> bool {
*self
.in_init_module
.get_or_init(|| self.path.ends_with("__init__.py"))
}
/// The [`Path`] to the package containing the current file.
@@ -3171,7 +3176,7 @@ impl<'a> Checker<'a> {
// F822
if self.is_rule_enabled(Rule::UndefinedExport) {
if is_undefined_export_in_dunder_init_enabled(self.settings())
|| !self.path.ends_with("__init__.py")
|| !self.in_init_module()
{
self.report_diagnostic(
pyflakes::rules::UndefinedExport {

View File

@@ -1060,6 +1060,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Ruff, "064") => rules::ruff::rules::NonOctalPermissions,
(Ruff, "065") => rules::ruff::rules::LoggingEagerConversion,
(Ruff, "066") => rules::ruff::rules::PropertyWithoutReturn,
(Ruff, "067") => rules::ruff::rules::NonEmptyInitModule,
(Ruff, "100") => rules::ruff::rules::UnusedNOQA,
(Ruff, "101") => rules::ruff::rules::RedirectedNOQA,

View File

@@ -389,7 +389,7 @@ pub(crate) fn unused_import(checker: &Checker, scope: &Scope) {
}
}
let in_init = checker.path().ends_with("__init__.py");
let in_init = checker.in_init_module();
let fix_init = !checker.settings().ignore_init_module_imports;
let preview_mode = is_dunder_init_fix_unused_import_enabled(checker.settings());
let dunder_all_exprs = find_dunder_all_exprs(checker.semantic());

View File

@@ -77,7 +77,7 @@ pub(crate) fn useless_import_alias(checker: &Checker, alias: &Alias) {
}
// A re-export in __init__.py is probably intentional.
if checker.path().ends_with("__init__.py") {
if checker.in_init_module() {
return;
}
@@ -116,7 +116,7 @@ pub(crate) fn useless_import_from_alias(
}
// A re-export in __init__.py is probably intentional.
if checker.path().ends_with("__init__.py") {
if checker.in_init_module() {
return;
}

View File

@@ -119,6 +119,8 @@ mod tests {
#[test_case(Rule::RedirectedNOQA, Path::new("RUF101_0.py"))]
#[test_case(Rule::RedirectedNOQA, Path::new("RUF101_1.py"))]
#[test_case(Rule::InvalidRuleCode, Path::new("RUF102.py"))]
#[test_case(Rule::NonEmptyInitModule, Path::new("RUF067/modules/__init__.py"))]
#[test_case(Rule::NonEmptyInitModule, Path::new("RUF067/modules/okay.py"))]
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
let diagnostics = test_path(
@@ -136,6 +138,7 @@ mod tests {
&LinterSettings {
ruff: super::settings::Settings {
parenthesize_tuple_in_subscript: true,
..super::settings::Settings::default()
},
..LinterSettings::for_rule(Rule::IncorrectlyParenthesizedTupleInSubscript)
},
@@ -151,6 +154,7 @@ mod tests {
&LinterSettings {
ruff: super::settings::Settings {
parenthesize_tuple_in_subscript: false,
..super::settings::Settings::default()
},
unresolved_target_version: PythonVersion::PY310.into(),
..LinterSettings::for_rule(Rule::IncorrectlyParenthesizedTupleInSubscript)
@@ -714,4 +718,26 @@ mod tests {
assert_diagnostics!(snapshot, diagnostics);
Ok(())
}
#[test]
fn strictly_empty_init_modules_ruf067() -> Result<()> {
assert_diagnostics_diff!(
Path::new("ruff/RUF067/modules/__init__.py"),
&LinterSettings {
ruff: super::settings::Settings {
strictly_empty_init_modules: false,
..super::settings::Settings::default()
},
..LinterSettings::for_rule(Rule::NonEmptyInitModule)
},
&LinterSettings {
ruff: super::settings::Settings {
strictly_empty_init_modules: true,
..super::settings::Settings::default()
},
..LinterSettings::for_rule(Rule::NonEmptyInitModule)
},
);
Ok(())
}
}

View File

@@ -32,6 +32,7 @@ pub(crate) use mutable_dataclass_default::*;
pub(crate) use mutable_fromkeys_value::*;
pub(crate) use needless_else::*;
pub(crate) use never_union::*;
pub(crate) use non_empty_init_module::*;
pub(crate) use non_octal_permissions::*;
pub(crate) use none_not_at_end_of_union::*;
pub(crate) use parenthesize_chained_operators::*;
@@ -99,6 +100,7 @@ mod mutable_dataclass_default;
mod mutable_fromkeys_value;
mod needless_else;
mod never_union;
mod non_empty_init_module;
mod non_octal_permissions;
mod none_not_at_end_of_union;
mod parenthesize_chained_operators;

View File

@@ -0,0 +1,259 @@
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::{self as ast, Expr, Stmt};
use ruff_python_semantic::analyze::typing::is_type_checking_block;
use ruff_text_size::Ranged;
use crate::{Violation, checkers::ast::Checker};
/// ## What it does
///
/// Detects the presence of code in `__init__.py` files.
///
/// ## Why is this bad?
///
/// `__init__.py` files are often empty or only contain simple code to modify a module's API. As
/// such, it's easy to overlook them and their possible side effects when debugging.
///
/// ## Example
///
/// Instead of defining `MyClass` directly in `__init__.py`:
///
/// ```python
/// """My module docstring."""
///
///
/// class MyClass:
/// def my_method(self): ...
/// ```
///
/// move the definition to another file, import it, and include it in `__all__`:
///
/// ```python
/// """My module docstring."""
///
/// from submodule import MyClass
///
/// __all__ = ["MyClass"]
/// ```
///
/// Code in `__init__.py` files is also run at import time and can cause surprising slowdowns. To
/// disallow any code in `__init__.py` files, you can enable the
/// [`lint.ruff.strictly-empty-init-modules`] setting. In this case:
///
/// ```python
/// from submodule import MyClass
///
/// __all__ = ["MyClass"]
/// ```
///
/// the only fix is entirely emptying the file:
///
/// ```python
/// ```
///
/// ## Details
///
/// In non-strict mode, this rule allows several common patterns in `__init__.py` files:
///
/// - Imports
/// - Assignments to `__all__`, `__path__`, `__version__`, and `__author__`
/// - Module-level and attribute docstrings
/// - `if TYPE_CHECKING` blocks
/// - [PEP-562] module-level `__getattr__` and `__dir__` functions
///
/// ## Options
///
/// - [`lint.ruff.strictly-empty-init-modules`]
///
/// ## References
///
/// - [`flake8-empty-init-modules`](https://github.com/samueljsb/flake8-empty-init-modules/)
///
/// [PEP-562]: https://peps.python.org/pep-0562/
#[derive(ViolationMetadata)]
#[violation_metadata(preview_since = "0.14.11")]
pub(crate) struct NonEmptyInitModule {
strictly_empty_init_modules: bool,
}
impl Violation for NonEmptyInitModule {
#[derive_message_formats]
fn message(&self) -> String {
if self.strictly_empty_init_modules {
"`__init__` module should not contain any code".to_string()
} else {
"`__init__` module should only contain docstrings and re-exports".to_string()
}
}
}
/// RUF067
pub(crate) fn non_empty_init_module(checker: &Checker, stmt: &Stmt) {
if !checker.in_init_module() {
return;
}
let semantic = checker.semantic();
// Only flag top-level statements
if !semantic.at_top_level() {
return;
}
let strictly_empty_init_modules = checker.settings().ruff.strictly_empty_init_modules;
if !strictly_empty_init_modules {
// Even though module-level attributes are disallowed, we still allow attribute docstrings
// to avoid needing two `noqa` comments in a case like:
//
// ```py
// MY_CONSTANT = 1 # noqa: RUF067
// "A very important constant"
// ```
if semantic.in_pep_257_docstring() || semantic.in_attribute_docstring() {
return;
}
match stmt {
// Allow imports
Stmt::Import(_) | Stmt::ImportFrom(_) => return,
// Allow PEP-562 module `__getattr__` and `__dir__`
Stmt::FunctionDef(func) if matches!(&*func.name, "__getattr__" | "__dir__") => return,
// Allow `TYPE_CHECKING` blocks
Stmt::If(stmt_if) if is_type_checking_block(stmt_if, semantic) => return,
_ => {}
}
if let Some(assignment) = Assignment::from_stmt(stmt) {
// Allow assignments to `__all__`.
//
// TODO(brent) should we allow additional cases here? Beyond simple assignments, you could
// also append or extend `__all__`.
//
// This is actually going slightly beyond the upstream rule already, which only checks for
// `Stmt::Assign`.
if assignment.is_assignment_to("__all__") {
return;
}
// Allow legacy namespace packages with assignments like:
//
// ```py
// __path__ = __import__('pkgutil').extend_path(__path__, __name__)
// ```
if assignment.is_assignment_to("__path__") && assignment.is_pkgutil_extend_path() {
return;
}
// Allow assignments to `__version__`.
if assignment.is_assignment_to("__version__") {
return;
}
// Allow assignments to `__author__`.
if assignment.is_assignment_to("__author__") {
return;
}
}
}
checker.report_diagnostic(
NonEmptyInitModule {
strictly_empty_init_modules,
},
stmt.range(),
);
}
/// Any assignment statement, including plain assignment, annotated assignments, and augmented
/// assignments.
struct Assignment<'a> {
targets: &'a [Expr],
value: Option<&'a Expr>,
}
impl<'a> Assignment<'a> {
fn from_stmt(stmt: &'a Stmt) -> Option<Self> {
let (targets, value) = match stmt {
Stmt::Assign(ast::StmtAssign { targets, value, .. }) => {
(targets.as_slice(), Some(&**value))
}
Stmt::AnnAssign(ast::StmtAnnAssign { target, value, .. }) => {
(std::slice::from_ref(&**target), value.as_deref())
}
Stmt::AugAssign(ast::StmtAugAssign { target, value, .. }) => {
(std::slice::from_ref(&**target), Some(&**value))
}
_ => return None,
};
Some(Self { targets, value })
}
/// Returns whether all of the assignment targets match `name`.
///
/// For example, both of the following would be allowed for a `name` of `__all__`:
///
/// ```py
/// __all__ = ["foo"]
/// __all__ = __all__ = ["foo"]
/// ```
///
/// but not:
///
/// ```py
/// __all__ = another_list = ["foo"]
/// ```
fn is_assignment_to(&self, name: &str) -> bool {
self.targets
.iter()
.all(|target| target.as_name_expr().is_some_and(|expr| expr.id == name))
}
/// Returns `true` if the value being assigned is a call to `pkgutil.extend_path`.
///
/// For example, both of the following would return true:
///
/// ```py
/// __path__ = __import__('pkgutil').extend_path(__path__, __name__)
/// __path__ = other.extend_path(__path__, __name__)
/// ```
///
/// We're intentionally a bit less strict here, not requiring that the receiver of the
/// `extend_path` call is the typical `__import__('pkgutil')` or `pkgutil`.
fn is_pkgutil_extend_path(&self) -> bool {
let Some(Expr::Call(ast::ExprCall {
func: extend_func,
arguments: extend_arguments,
..
})) = self.value
else {
return false;
};
let Expr::Attribute(ast::ExprAttribute {
attr: maybe_extend_path,
..
}) = &**extend_func
else {
return false;
};
// Test that this is an `extend_path(__path__, __name__)` call
if maybe_extend_path != "extend_path" {
return false;
}
let Some(Expr::Name(path)) = extend_arguments.find_argument_value("path", 0) else {
return false;
};
let Some(Expr::Name(name)) = extend_arguments.find_argument_value("name", 1) else {
return false;
};
path.id() == "__path__" && name.id() == "__name__"
}
}

View File

@@ -7,6 +7,7 @@ use std::fmt;
#[derive(Debug, Clone, CacheKey, Default)]
pub struct Settings {
pub parenthesize_tuple_in_subscript: bool,
pub strictly_empty_init_modules: bool,
}
impl fmt::Display for Settings {
@@ -16,6 +17,7 @@ impl fmt::Display for Settings {
namespace = "linter.ruff",
fields = [
self.parenthesize_tuple_in_subscript,
self.strictly_empty_init_modules,
]
}
Ok(())

View File

@@ -0,0 +1,53 @@
---
source: crates/ruff_linter/src/rules/ruff/mod.rs
---
RUF067 `__init__` module should only contain docstrings and re-exports
--> __init__.py:12:1
|
10 | __all__ = __all__ = __all__
11 |
12 | MY_CONSTANT = 5
| ^^^^^^^^^^^^^^^
13 | """This is an important constant."""
|
RUF067 `__init__` module should only contain docstrings and re-exports
--> __init__.py:15:1
|
13 | """This is an important constant."""
14 |
15 | os.environ["FOO"] = 1
| ^^^^^^^^^^^^^^^^^^^^^
|
RUF067 `__init__` module should only contain docstrings and re-exports
--> __init__.py:18:1
|
18 | / def foo():
19 | | return Path("foo.py")
| |_________________________^
20 |
21 | def __getattr__(name): # ok
|
RUF067 `__init__` module should only contain docstrings and re-exports
--> __init__.py:26:1
|
24 | __path__ = __import__('pkgutil').extend_path(__path__, __name__) # ok
25 |
26 | / if os.environ["FOO"] != "1": # RUF067
27 | | MY_CONSTANT = 4 # ok, don't flag nested statements
| |___________________^
28 |
29 | if TYPE_CHECKING: # ok
|
RUF067 `__init__` module should only contain docstrings and re-exports
--> __init__.py:48:1
|
47 | # non-`extend_path` assignments are not allowed
48 | __path__ = 5 # RUF067
| ^^^^^^^^^^^^
49 |
50 | # also allow `__author__`
|

View File

@@ -0,0 +1,4 @@
---
source: crates/ruff_linter/src/rules/ruff/mod.rs
---

View File

@@ -0,0 +1,344 @@
---
source: crates/ruff_linter/src/rules/ruff/mod.rs
---
--- Linter settings ---
-linter.ruff.strictly_empty_init_modules = false
+linter.ruff.strictly_empty_init_modules = true
--- Summary ---
Removed: 5
Added: 24
--- Removed ---
RUF067 `__init__` module should only contain docstrings and re-exports
--> __init__.py:12:1
|
10 | __all__ = __all__ = __all__
11 |
12 | MY_CONSTANT = 5
| ^^^^^^^^^^^^^^^
13 | """This is an important constant."""
|
RUF067 `__init__` module should only contain docstrings and re-exports
--> __init__.py:15:1
|
13 | """This is an important constant."""
14 |
15 | os.environ["FOO"] = 1
| ^^^^^^^^^^^^^^^^^^^^^
|
RUF067 `__init__` module should only contain docstrings and re-exports
--> __init__.py:18:1
|
18 | / def foo():
19 | | return Path("foo.py")
| |_________________________^
20 |
21 | def __getattr__(name): # ok
|
RUF067 `__init__` module should only contain docstrings and re-exports
--> __init__.py:26:1
|
24 | __path__ = __import__('pkgutil').extend_path(__path__, __name__) # ok
25 |
26 | / if os.environ["FOO"] != "1": # RUF067
27 | | MY_CONSTANT = 4 # ok, don't flag nested statements
| |___________________^
28 |
29 | if TYPE_CHECKING: # ok
|
RUF067 `__init__` module should only contain docstrings and re-exports
--> __init__.py:48:1
|
47 | # non-`extend_path` assignments are not allowed
48 | __path__ = 5 # RUF067
| ^^^^^^^^^^^^
49 |
50 | # also allow `__author__`
|
--- Added ---
RUF067 `__init__` module should not contain any code
--> __init__.py:1:1
|
1 | """This is the module docstring."""
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
2 |
3 | # convenience imports:
|
RUF067 `__init__` module should not contain any code
--> __init__.py:4:1
|
3 | # convenience imports:
4 | import os
| ^^^^^^^^^
5 | from pathlib import Path
|
RUF067 `__init__` module should not contain any code
--> __init__.py:5:1
|
3 | # convenience imports:
4 | import os
5 | from pathlib import Path
| ^^^^^^^^^^^^^^^^^^^^^^^^
6 |
7 | __all__ = ["MY_CONSTANT"]
|
RUF067 `__init__` module should not contain any code
--> __init__.py:7:1
|
5 | from pathlib import Path
6 |
7 | __all__ = ["MY_CONSTANT"]
| ^^^^^^^^^^^^^^^^^^^^^^^^^
8 | __all__ += ["foo"]
9 | __all__: list[str] = __all__
|
RUF067 `__init__` module should not contain any code
--> __init__.py:8:1
|
7 | __all__ = ["MY_CONSTANT"]
8 | __all__ += ["foo"]
| ^^^^^^^^^^^^^^^^^^
9 | __all__: list[str] = __all__
10 | __all__ = __all__ = __all__
|
RUF067 `__init__` module should not contain any code
--> __init__.py:9:1
|
7 | __all__ = ["MY_CONSTANT"]
8 | __all__ += ["foo"]
9 | __all__: list[str] = __all__
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
10 | __all__ = __all__ = __all__
|
RUF067 `__init__` module should not contain any code
--> __init__.py:10:1
|
8 | __all__ += ["foo"]
9 | __all__: list[str] = __all__
10 | __all__ = __all__ = __all__
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
11 |
12 | MY_CONSTANT = 5
|
RUF067 `__init__` module should not contain any code
--> __init__.py:12:1
|
10 | __all__ = __all__ = __all__
11 |
12 | MY_CONSTANT = 5
| ^^^^^^^^^^^^^^^
13 | """This is an important constant."""
|
RUF067 `__init__` module should not contain any code
--> __init__.py:13:1
|
12 | MY_CONSTANT = 5
13 | """This is an important constant."""
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
14 |
15 | os.environ["FOO"] = 1
|
RUF067 `__init__` module should not contain any code
--> __init__.py:15:1
|
13 | """This is an important constant."""
14 |
15 | os.environ["FOO"] = 1
| ^^^^^^^^^^^^^^^^^^^^^
|
RUF067 `__init__` module should not contain any code
--> __init__.py:18:1
|
18 | / def foo():
19 | | return Path("foo.py")
| |_________________________^
20 |
21 | def __getattr__(name): # ok
|
RUF067 `__init__` module should not contain any code
--> __init__.py:21:1
|
19 | return Path("foo.py")
20 |
21 | / def __getattr__(name): # ok
22 | | return name
| |_______________^
23 |
24 | __path__ = __import__('pkgutil').extend_path(__path__, __name__) # ok
|
RUF067 `__init__` module should not contain any code
--> __init__.py:24:1
|
22 | return name
23 |
24 | __path__ = __import__('pkgutil').extend_path(__path__, __name__) # ok
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
25 |
26 | if os.environ["FOO"] != "1": # RUF067
|
RUF067 `__init__` module should not contain any code
--> __init__.py:26:1
|
24 | __path__ = __import__('pkgutil').extend_path(__path__, __name__) # ok
25 |
26 | / if os.environ["FOO"] != "1": # RUF067
27 | | MY_CONSTANT = 4 # ok, don't flag nested statements
| |___________________^
28 |
29 | if TYPE_CHECKING: # ok
|
RUF067 `__init__` module should not contain any code
--> __init__.py:29:1
|
27 | MY_CONSTANT = 4 # ok, don't flag nested statements
28 |
29 | / if TYPE_CHECKING: # ok
30 | | MY_CONSTANT = 3
| |___________________^
31 |
32 | import typing
|
RUF067 `__init__` module should not contain any code
--> __init__.py:32:1
|
30 | MY_CONSTANT = 3
31 |
32 | import typing
| ^^^^^^^^^^^^^
33 |
34 | if typing.TYPE_CHECKING: # ok
|
RUF067 `__init__` module should not contain any code
--> __init__.py:34:1
|
32 | import typing
33 |
34 | / if typing.TYPE_CHECKING: # ok
35 | | MY_CONSTANT = 2
| |___________________^
36 |
37 | __version__ = "1.2.3" # ok
|
RUF067 `__init__` module should not contain any code
--> __init__.py:37:1
|
35 | MY_CONSTANT = 2
36 |
37 | __version__ = "1.2.3" # ok
| ^^^^^^^^^^^^^^^^^^^^^
38 |
39 | def __dir__(): # ok
|
RUF067 `__init__` module should not contain any code
--> __init__.py:39:1
|
37 | __version__ = "1.2.3" # ok
38 |
39 | / def __dir__(): # ok
40 | | return ["foo"]
| |__________________^
41 |
42 | import pkgutil
|
RUF067 `__init__` module should not contain any code
--> __init__.py:42:1
|
40 | return ["foo"]
41 |
42 | import pkgutil
| ^^^^^^^^^^^^^^
43 |
44 | __path__ = pkgutil.extend_path(__path__, __name__) # ok
|
RUF067 `__init__` module should not contain any code
--> __init__.py:44:1
|
42 | import pkgutil
43 |
44 | __path__ = pkgutil.extend_path(__path__, __name__) # ok
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
45 | __path__ = unknown.extend_path(__path__, __name__) # also ok
|
RUF067 `__init__` module should not contain any code
--> __init__.py:45:1
|
44 | __path__ = pkgutil.extend_path(__path__, __name__) # ok
45 | __path__ = unknown.extend_path(__path__, __name__) # also ok
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
46 |
47 | # non-`extend_path` assignments are not allowed
|
RUF067 `__init__` module should not contain any code
--> __init__.py:48:1
|
47 | # non-`extend_path` assignments are not allowed
48 | __path__ = 5 # RUF067
| ^^^^^^^^^^^^
49 |
50 | # also allow `__author__`
|
RUF067 `__init__` module should not contain any code
--> __init__.py:51:1
|
50 | # also allow `__author__`
51 | __author__ = "The Author" # ok
| ^^^^^^^^^^^^^^^^^^^^^^^^^
|

View File

@@ -3497,6 +3497,17 @@ pub struct RuffOptions {
note = "The `allowed-markup-names` option has been moved to the `flake8-bandit` section of the configuration."
)]
pub allowed_markup_calls: Option<Vec<String>>,
/// Whether to require `__init__.py` files to contain no code at all, including imports and
/// docstrings (see `RUF067`).
#[option(
default = r#"false"#,
value_type = "bool",
example = r#"
# Make it a violation to include any code, including imports and docstrings in `__init__.py`
strictly-empty-init-modules = true
"#
)]
pub strictly_empty_init_modules: Option<bool>,
}
impl RuffOptions {
@@ -3505,6 +3516,7 @@ impl RuffOptions {
parenthesize_tuple_in_subscript: self
.parenthesize_tuple_in_subscript
.unwrap_or_default(),
strictly_empty_init_modules: self.strictly_empty_init_modules.unwrap_or_default(),
}
}
}

8
ruff.schema.json generated
View File

@@ -2926,6 +2926,13 @@
"boolean",
"null"
]
},
"strictly-empty-init-modules": {
"description": "Whether to require `__init__.py` files to contain no code at all, including imports and\ndocstrings (see `RUF067`).",
"type": [
"boolean",
"null"
]
}
},
"additionalProperties": false
@@ -4046,6 +4053,7 @@
"RUF064",
"RUF065",
"RUF066",
"RUF067",
"RUF1",
"RUF10",
"RUF100",