Compare commits

..

1 Commits

Author SHA1 Message Date
David Peter
e935bc5578 [ty] enum.Flag 2025-07-29 16:52:42 +02:00
28 changed files with 196 additions and 867 deletions

View File

@@ -1,35 +1,5 @@
# Changelog
## 0.12.7
This is a follow-up release to 0.12.6. Because of an issue in the package metadata, 0.12.6 failed to publish fully to PyPI and has been yanked. Similarly, there is no GitHub release or Git tag for 0.12.6. The contents of the 0.12.7 release are identical to 0.12.6, except for the updated metadata.
## 0.12.6
### Preview features
- \[`flake8-commas`\] Add support for trailing comma checks in type parameter lists (`COM812`, `COM819`) ([#19390](https://github.com/astral-sh/ruff/pull/19390))
- \[`pylint`\] Implement auto-fix for `missing-maxsplit-arg` (`PLC0207`) ([#19387](https://github.com/astral-sh/ruff/pull/19387))
- \[`ruff`\] Offer fixes for `RUF039` in more cases ([#19065](https://github.com/astral-sh/ruff/pull/19065))
### Bug fixes
- Support `.pyi` files in ruff analyze graph ([#19611](https://github.com/astral-sh/ruff/pull/19611))
- \[`flake8-pyi`\] Preserve inline comment in ellipsis removal (`PYI013`) ([#19399](https://github.com/astral-sh/ruff/pull/19399))
- \[`perflint`\] Ignore rule if target is `global` or `nonlocal` (`PERF401`) ([#19539](https://github.com/astral-sh/ruff/pull/19539))
- \[`pyupgrade`\] Fix `UP030` to avoid modifying double curly braces in format strings ([#19378](https://github.com/astral-sh/ruff/pull/19378))
- \[`refurb`\] Ignore decorated functions for `FURB118` ([#19339](https://github.com/astral-sh/ruff/pull/19339))
- \[`refurb`\] Mark `int` and `bool` cases for `Decimal.from_float` as safe fixes (`FURB164`) ([#19468](https://github.com/astral-sh/ruff/pull/19468))
- \[`ruff`\] Fix `RUF033` for named default expressions ([#19115](https://github.com/astral-sh/ruff/pull/19115))
### Rule changes
- \[`flake8-blind-except`\] Change `BLE001` to permit `logging.critical(..., exc_info=True)` ([#19520](https://github.com/astral-sh/ruff/pull/19520))
### Performance
- Add support for specifying minimum dots in detected string imports ([#19538](https://github.com/astral-sh/ruff/pull/19538))
## 0.12.5
### Preview features

6
Cargo.lock generated
View File

@@ -2744,7 +2744,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.12.7"
version = "0.12.5"
dependencies = [
"anyhow",
"argfile",
@@ -2997,7 +2997,7 @@ dependencies = [
[[package]]
name = "ruff_linter"
version = "0.12.7"
version = "0.12.5"
dependencies = [
"aho-corasick",
"anyhow",
@@ -3329,7 +3329,7 @@ dependencies = [
[[package]]
name = "ruff_wasm"
version = "0.12.7"
version = "0.12.5"
dependencies = [
"console_error_panic_hook",
"console_log",

View File

@@ -148,8 +148,8 @@ curl -LsSf https://astral.sh/ruff/install.sh | sh
powershell -c "irm https://astral.sh/ruff/install.ps1 | iex"
# For a specific version.
curl -LsSf https://astral.sh/ruff/0.12.7/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.12.7/install.ps1 | iex"
curl -LsSf https://astral.sh/ruff/0.12.5/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.12.5/install.ps1 | iex"
```
You can also install Ruff via [Homebrew](https://formulae.brew.sh/formula/ruff), [Conda](https://anaconda.org/conda-forge/ruff),
@@ -182,7 +182,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com/) hook via [`ruff
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.12.7
rev: v0.12.5
hooks:
# Run the linter.
- id: ruff-check

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff"
version = "0.12.7"
version = "0.12.5"
publish = true
authors = { workspace = true }
edition = { workspace = true }

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_linter"
version = "0.12.7"
version = "0.12.5"
publish = false
authors = { workspace = true }
edition = { workspace = true }

View File

@@ -39,11 +39,6 @@ class NonEmptyWithInit:
pass
class NonEmptyChildWithInlineComment:
value: int
... # preserve me
class EmptyClass:
...

View File

@@ -38,10 +38,6 @@ class NonEmptyWithInit:
def __init__():
pass
class NonEmptyChildWithInlineComment:
value: int
... # preserve me
# Not violations
class EmptyClass: ...

View File

@@ -59,7 +59,3 @@ kwargs = {x: x for x in range(10)}
"{1}_{0}".format(1, 2, *args)
"{1}_{0}".format(1, 2)
r"\d{{1,2}} {0}".format(42)
"{{{0}}}".format(123)

View File

@@ -590,16 +590,6 @@ impl<'a> Checker<'a> {
member,
})
}
/// Return the [`LintContext`] for the current analysis.
///
/// Note that you should always prefer calling methods like `settings`, `report_diagnostic`, or
/// `is_rule_enabled` directly on [`Checker`] when possible. This method exists only for the
/// rare cases where rules or helper functions need to be accessed by both a `Checker` and a
/// `LintContext` in different analysis phases.
pub(crate) const fn context(&self) -> &'a LintContext<'a> {
self.context
}
}
pub(crate) struct TypingImporter<'a, 'b> {

View File

@@ -1,11 +1,10 @@
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::whitespace::trailing_comment_start_offset;
use ruff_python_ast::{Stmt, StmtExpr};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::fix;
use crate::{Edit, Fix, FixAvailability, Violation};
use crate::{Fix, FixAvailability, Violation};
/// ## What it does
/// Removes ellipses (`...`) in otherwise non-empty class bodies.
@@ -51,21 +50,15 @@ pub(crate) fn ellipsis_in_non_empty_class_body(checker: &Checker, body: &[Stmt])
}
for stmt in body {
let Stmt::Expr(StmtExpr { value, .. }) = stmt else {
let Stmt::Expr(StmtExpr { value, .. }) = &stmt else {
continue;
};
if value.is_ellipsis_literal_expr() {
let mut diagnostic =
checker.report_diagnostic(EllipsisInNonEmptyClassBody, stmt.range());
// Try to preserve trailing comment if it exists
let edit = if let Some(index) = trailing_comment_start_offset(stmt, checker.source()) {
Edit::range_deletion(stmt.range().add_end(index))
} else {
fix::edits::delete_stmt(stmt, Some(stmt), checker.locator(), checker.indexer())
};
let edit =
fix::edits::delete_stmt(stmt, Some(stmt), checker.locator(), checker.indexer());
diagnostic.set_fix(Fix::safe_edit(edit).isolate(Checker::isolation(
checker.semantic().current_statement_id(),
)));

View File

@@ -145,22 +145,3 @@ PYI013.py:36:5: PYI013 [*] Non-empty class body must not contain `...`
37 36 |
38 37 | def __init__():
39 38 | pass
PYI013.py:44:5: PYI013 [*] Non-empty class body must not contain `...`
|
42 | class NonEmptyChildWithInlineComment:
43 | value: int
44 | ... # preserve me
| ^^^ PYI013
|
= help: Remove unnecessary `...`
Safe fix
41 41 |
42 42 | class NonEmptyChildWithInlineComment:
43 43 | value: int
44 |- ... # preserve me
44 |+ # preserve me
45 45 |
46 46 |
47 47 | class EmptyClass:

View File

@@ -17,10 +17,9 @@ PYI013.pyi:5:5: PYI013 [*] Non-empty class body must not contain `...`
3 3 | class OneAttributeClass:
4 4 | value: int
5 |- ... # Error
5 |+ # Error
6 6 |
7 7 | class OneAttributeClass2:
8 8 | ... # Error
6 5 |
7 6 | class OneAttributeClass2:
8 7 | ... # Error
PYI013.pyi:8:5: PYI013 [*] Non-empty class body must not contain `...`
|
@@ -36,10 +35,9 @@ PYI013.pyi:8:5: PYI013 [*] Non-empty class body must not contain `...`
6 6 |
7 7 | class OneAttributeClass2:
8 |- ... # Error
8 |+ # Error
9 9 | value: int
10 10 |
11 11 | class MyClass:
9 8 | value: int
10 9 |
11 10 | class MyClass:
PYI013.pyi:12:5: PYI013 [*] Non-empty class body must not contain `...`
|
@@ -93,10 +91,9 @@ PYI013.pyi:17:5: PYI013 [*] Non-empty class body must not contain `...`
15 15 | class TwoEllipsesClass:
16 16 | ...
17 |- ... # Error
17 |+ # Error
18 18 |
19 19 | class DocstringClass:
20 20 | """
18 17 |
19 18 | class DocstringClass:
20 19 | """
PYI013.pyi:24:5: PYI013 [*] Non-empty class body must not contain `...`
|
@@ -114,10 +111,9 @@ PYI013.pyi:24:5: PYI013 [*] Non-empty class body must not contain `...`
22 22 | """
23 23 |
24 |- ... # Error
24 |+ # Error
25 25 |
26 26 | class NonEmptyChild(Exception):
27 27 | value: int
25 24 |
26 25 | class NonEmptyChild(Exception):
27 26 | value: int
PYI013.pyi:28:5: PYI013 [*] Non-empty class body must not contain `...`
|
@@ -135,10 +131,9 @@ PYI013.pyi:28:5: PYI013 [*] Non-empty class body must not contain `...`
26 26 | class NonEmptyChild(Exception):
27 27 | value: int
28 |- ... # Error
28 |+ # Error
29 29 |
30 30 | class NonEmptyChild2(Exception):
31 31 | ... # Error
29 28 |
30 29 | class NonEmptyChild2(Exception):
31 30 | ... # Error
PYI013.pyi:31:5: PYI013 [*] Non-empty class body must not contain `...`
|
@@ -154,10 +149,9 @@ PYI013.pyi:31:5: PYI013 [*] Non-empty class body must not contain `...`
29 29 |
30 30 | class NonEmptyChild2(Exception):
31 |- ... # Error
31 |+ # Error
32 32 | value: int
33 33 |
34 34 | class NonEmptyWithInit:
32 31 | value: int
33 32 |
34 33 | class NonEmptyWithInit:
PYI013.pyi:36:5: PYI013 [*] Non-empty class body must not contain `...`
|
@@ -175,28 +169,6 @@ PYI013.pyi:36:5: PYI013 [*] Non-empty class body must not contain `...`
34 34 | class NonEmptyWithInit:
35 35 | value: int
36 |- ... # Error
36 |+ # Error
37 37 |
38 38 | def __init__():
39 39 | pass
PYI013.pyi:43:5: PYI013 [*] Non-empty class body must not contain `...`
|
41 | class NonEmptyChildWithInlineComment:
42 | value: int
43 | ... # preserve me
| ^^^ PYI013
44 |
45 | # Not violations
|
= help: Remove unnecessary `...`
Safe fix
40 40 |
41 41 | class NonEmptyChildWithInlineComment:
42 42 | value: int
43 |- ... # preserve me
43 |+ # preserve me
44 44 |
45 45 | # Not violations
46 46 |
37 36 |
38 37 | def __init__():
39 38 | pass

View File

@@ -124,20 +124,10 @@ fn is_sequential(indices: &[usize]) -> bool {
indices.iter().enumerate().all(|(idx, value)| idx == *value)
}
static FORMAT_SPECIFIER: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(
r"(?x)
(?P<prefix>
^|[^{]|(?:\{{2})+ # preceded by nothing, a non-brace, or an even number of braces
)
\{ # opening curly brace
(?P<int>\d+) # followed by any integer
(?P<fmt>.*?) # followed by any text
} # followed by a closing brace
",
)
.unwrap()
});
// An opening curly brace, followed by any integer, followed by any text,
// followed by a closing brace.
static FORMAT_SPECIFIER: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"\{(?P<int>\d+)(?P<fmt>.*?)}").unwrap());
/// Remove the explicit positional indices from a format string.
fn remove_specifiers<'a>(value: &mut Expression<'a>, arena: &'a typed_arena::Arena<String>) {
@@ -145,7 +135,7 @@ fn remove_specifiers<'a>(value: &mut Expression<'a>, arena: &'a typed_arena::Are
Expression::SimpleString(expr) => {
expr.value = arena.alloc(
FORMAT_SPECIFIER
.replace_all(expr.value, "$prefix{$fmt}")
.replace_all(expr.value, "{$fmt}")
.to_string(),
);
}
@@ -156,7 +146,7 @@ fn remove_specifiers<'a>(value: &mut Expression<'a>, arena: &'a typed_arena::Are
libcst_native::String::Simple(string) => {
string.value = arena.alloc(
FORMAT_SPECIFIER
.replace_all(string.value, "$prefix{$fmt}")
.replace_all(string.value, "{$fmt}")
.to_string(),
);
}

View File

@@ -481,7 +481,6 @@ UP030_0.py:59:1: UP030 [*] Use implicit references for positional format fields
59 |+"{}_{}".format(2, 1, )
60 60 |
61 61 | "{1}_{0}".format(1, 2)
62 62 |
UP030_0.py:61:1: UP030 [*] Use implicit references for positional format fields
|
@@ -489,8 +488,6 @@ UP030_0.py:61:1: UP030 [*] Use implicit references for positional format fields
60 |
61 | "{1}_{0}".format(1, 2)
| ^^^^^^^^^^^^^^^^^^^^^^ UP030
62 |
63 | r"\d{{1,2}} {0}".format(42)
|
= help: Remove explicit positional indices
@@ -500,42 +497,3 @@ UP030_0.py:61:1: UP030 [*] Use implicit references for positional format fields
60 60 |
61 |-"{1}_{0}".format(1, 2)
61 |+"{}_{}".format(2, 1)
62 62 |
63 63 | r"\d{{1,2}} {0}".format(42)
64 64 |
UP030_0.py:63:1: UP030 [*] Use implicit references for positional format fields
|
61 | "{1}_{0}".format(1, 2)
62 |
63 | r"\d{{1,2}} {0}".format(42)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP030
64 |
65 | "{{{0}}}".format(123)
|
= help: Remove explicit positional indices
Unsafe fix
60 60 |
61 61 | "{1}_{0}".format(1, 2)
62 62 |
63 |-r"\d{{1,2}} {0}".format(42)
63 |+r"\d{{1,2}} {}".format(42)
64 64 |
65 65 | "{{{0}}}".format(123)
UP030_0.py:65:1: UP030 [*] Use implicit references for positional format fields
|
63 | r"\d{{1,2}} {0}".format(42)
64 |
65 | "{{{0}}}".format(123)
| ^^^^^^^^^^^^^^^^^^^^^ UP030
|
= help: Remove explicit positional indices
Unsafe fix
62 62 |
63 63 | r"\d{{1,2}} {0}".format(42)
64 64 |
65 |-"{{{0}}}".format(123)
65 |+"{{{}}}".format(123)

View File

@@ -12,6 +12,7 @@ use crate::checkers::ast::{Checker, LintContext};
use crate::preview::is_unicode_to_unicode_confusables_enabled;
use crate::rules::ruff::rules::Context;
use crate::rules::ruff::rules::confusables::confusable;
use crate::settings::LinterSettings;
/// ## What it does
/// Checks for ambiguous Unicode characters in strings.
@@ -179,7 +180,9 @@ pub(crate) fn ambiguous_unicode_character_comment(
range: TextRange,
) {
let text = locator.slice(range);
ambiguous_unicode_character(text, range, Context::Comment, context);
for candidate in ambiguous_unicode_character(text, range, context.settings()) {
candidate.into_diagnostic(Context::Comment, context);
}
}
/// RUF001, RUF002
@@ -200,19 +203,22 @@ pub(crate) fn ambiguous_unicode_character_string(checker: &Checker, string_like:
match part {
ast::StringLikePart::String(string_literal) => {
let text = checker.locator().slice(string_literal);
ambiguous_unicode_character(
text,
string_literal.range(),
context,
checker.context(),
);
for candidate in
ambiguous_unicode_character(text, string_literal.range(), checker.settings())
{
candidate.report_diagnostic(checker, context);
}
}
ast::StringLikePart::Bytes(_) => {}
ast::StringLikePart::FString(FString { elements, .. })
| ast::StringLikePart::TString(TString { elements, .. }) => {
for literal in elements.literals() {
let text = checker.locator().slice(literal);
ambiguous_unicode_character(text, literal.range(), context, checker.context());
for candidate in
ambiguous_unicode_character(text, literal.range(), checker.settings())
{
candidate.report_diagnostic(checker, context);
}
}
}
}
@@ -222,12 +228,13 @@ pub(crate) fn ambiguous_unicode_character_string(checker: &Checker, string_like:
fn ambiguous_unicode_character(
text: &str,
range: TextRange,
context: Context,
lint_context: &LintContext,
) {
settings: &LinterSettings,
) -> Vec<Candidate> {
let mut candidates = Vec::new();
// Most of the time, we don't need to check for ambiguous unicode characters at all.
if text.is_ascii() {
return;
return candidates;
}
// Iterate over the "words" in the text.
@@ -239,7 +246,7 @@ fn ambiguous_unicode_character(
if !word_candidates.is_empty() {
if word_flags.is_candidate_word() {
for candidate in word_candidates.drain(..) {
candidate.into_diagnostic(context, lint_context);
candidates.push(candidate);
}
}
word_candidates.clear();
@@ -250,23 +257,21 @@ fn ambiguous_unicode_character(
// case, it's always included as a diagnostic.
if !current_char.is_ascii() {
if let Some(representant) = confusable(current_char as u32).filter(|representant| {
is_unicode_to_unicode_confusables_enabled(lint_context.settings())
|| representant.is_ascii()
is_unicode_to_unicode_confusables_enabled(settings) || representant.is_ascii()
}) {
let candidate = Candidate::new(
TextSize::try_from(relative_offset).unwrap() + range.start(),
current_char,
representant,
);
candidate.into_diagnostic(context, lint_context);
candidates.push(candidate);
}
}
} else if current_char.is_ascii() {
// The current word contains at least one ASCII character.
word_flags |= WordFlags::ASCII;
} else if let Some(representant) = confusable(current_char as u32).filter(|representant| {
is_unicode_to_unicode_confusables_enabled(lint_context.settings())
|| representant.is_ascii()
is_unicode_to_unicode_confusables_enabled(settings) || representant.is_ascii()
}) {
// The current word contains an ambiguous unicode character.
word_candidates.push(Candidate::new(
@@ -284,11 +289,13 @@ fn ambiguous_unicode_character(
if !word_candidates.is_empty() {
if word_flags.is_candidate_word() {
for candidate in word_candidates.drain(..) {
candidate.into_diagnostic(context, lint_context);
candidates.push(candidate);
}
}
word_candidates.clear();
}
candidates
}
bitflags! {
@@ -366,6 +373,39 @@ impl Candidate {
};
}
}
fn report_diagnostic(self, checker: &Checker, context: Context) {
if !checker
.settings()
.allowed_confusables
.contains(&self.confusable)
{
let char_range = TextRange::at(self.offset, self.confusable.text_len());
match context {
Context::String => checker.report_diagnostic_if_enabled(
AmbiguousUnicodeCharacterString {
confusable: self.confusable,
representant: self.representant,
},
char_range,
),
Context::Docstring => checker.report_diagnostic_if_enabled(
AmbiguousUnicodeCharacterDocstring {
confusable: self.confusable,
representant: self.representant,
},
char_range,
),
Context::Comment => checker.report_diagnostic_if_enabled(
AmbiguousUnicodeCharacterComment {
confusable: self.confusable,
representant: self.representant,
},
char_range,
),
};
}
}
}
struct NamedUnicode(char);

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_wasm"
version = "0.12.7"
version = "0.12.5"
publish = false
authors = { workspace = true }
edition = { workspace = true }

View File

@@ -66,9 +66,9 @@ enum CompletionTargetTokens<'t> {
/// A token was found under the cursor, but it didn't
/// match any of our anticipated token patterns.
Generic { token: &'t Token },
/// No token was found. We generally treat this like
/// `Generic` (i.e., offer scope based completions).
Unknown,
/// No token was found, but we have the offset of the
/// cursor.
Unknown { offset: TextSize },
}
impl<'t> CompletionTargetTokens<'t> {
@@ -78,7 +78,7 @@ impl<'t> CompletionTargetTokens<'t> {
static OBJECT_DOT_NON_EMPTY: [TokenKind; 2] = [TokenKind::Dot, TokenKind::Name];
let offset = match parsed.tokens().at_offset(offset) {
TokenAt::None => return Some(CompletionTargetTokens::Unknown),
TokenAt::None => return Some(CompletionTargetTokens::Unknown { offset }),
TokenAt::Single(tok) => tok.end(),
TokenAt::Between(_, tok) => tok.start(),
};
@@ -122,7 +122,7 @@ impl<'t> CompletionTargetTokens<'t> {
return None;
} else {
let Some(last) = before.last() else {
return Some(CompletionTargetTokens::Unknown);
return Some(CompletionTargetTokens::Unknown { offset });
};
CompletionTargetTokens::Generic { token: last }
},
@@ -171,7 +171,7 @@ impl<'t> CompletionTargetTokens<'t> {
node: covering_node.node(),
})
}
CompletionTargetTokens::Unknown => {
CompletionTargetTokens::Unknown { offset } => {
let range = TextRange::empty(offset);
let covering_node = covering_node(parsed.syntax().into(), range);
Some(CompletionTargetAst::Scoped {

View File

@@ -1,70 +0,0 @@
# `replace`
The `replace` function and the `replace` protocol were added in Python 3.13:
<https://docs.python.org/3/whatsnew/3.13.html#copy>
```toml
[environment]
python-version = "3.13"
```
## Basic
```py
from copy import replace
from datetime import time
t = time(12, 0, 0)
t = replace(t, minute=30)
reveal_type(t) # revealed: time
```
## The `__replace__` protocol
### Dataclasses
Dataclasses support the `__replace__` protocol:
```py
from dataclasses import dataclass
from copy import replace
@dataclass
class Point:
x: int
y: int
reveal_type(Point.__replace__) # revealed: (self: Point, *, x: int = int, y: int = int) -> Point
```
The `__replace__` method can either be called directly or through the `replace` function:
```py
a = Point(1, 2)
b = a.__replace__(x=3, y=4)
reveal_type(b) # revealed: Point
b = replace(a, x=3, y=4)
reveal_type(b) # revealed: Point
```
A call to `replace` does not require all keyword arguments:
```py
c = a.__replace__(y=4)
reveal_type(c) # revealed: Point
d = replace(a, y=4)
reveal_type(d) # revealed: Point
```
Invalid calls to `__replace__` or `replace` will raise an error:
```py
e = a.__replace__(x="wrong") # error: [invalid-argument-type]
# TODO: this should ideally also be emit an error
e = replace(a, x="wrong")
```

View File

@@ -559,6 +559,22 @@ class Answer(Enum):
reveal_type(enum_members(Answer))
```
## Subclasses of `enum.Flag`
```py
from enum import Flag, auto
class KeyModifier(Flag):
SHIFT = auto()
CTRL = auto()
ALT = auto()
reveal_type(KeyModifier.SHIFT) # revealed: Literal[KeyModifier.SHIFT]
# TODO: this should be `KeyModifier`
reveal_type(KeyModifier.SHIFT | KeyModifier.CTRL) # revealed: Literal[KeyModifier.CTRL]
```
## Custom enum types
Enum classes can also be defined using a subclass of `enum.Enum` or any class that uses

View File

@@ -1,141 +0,0 @@
// This is a Dot representation of a flow diagram meant to describe Python's
// import resolution rules. This particular diagram starts with one particular
// search path and one particular module name. (Typical import resolution
// implementation will try multiple search paths.)
//
// This diagram also assumes that stubs are allowed. The ty implementation
// of import resolution makes this a configurable parameter, but it should
// be straight-forward to adapt this flow diagram to one where no stubs
// are allowed. (i.e., Remove `.pyi` checks and remove the `package-stubs`
// handling.)
//
// This flow diagram exists to act as a sort of specification. At the time
// of writing (2025-07-29), it was written to capture the implementation of
// resolving a *particular* module name. We wanted to add another code path for
// *listing* available module names. Since code reuse is somewhat difficult
// between these two access patterns, I wrote this flow diagram as a way of 1)
// learning how module resolution works and 2) to provide a "source of truth"
// that we can compare implementations to.
//
// To convert this file into an actual image, you'll need the `dot` program
// (which is typically part of a `graphviz` package in a Linux distro):
//
// dot -Tsvg import-resolution-diagram.dot > import-resolution-diagram.svg
//
// And then view it in a web browser (or some other svg viewer):
//
// firefox ./import-resolution-diagram.svg
//
// [Dot]: https://graphviz.org/doc/info/lang.html
digraph python_import_resolution {
labelloc="t";
label=<
<b>Python import resolution flow diagram for a single module name in a single "search path"</b>
<br/>(assumes that the module name is valid and that stubs are allowed)
>;
// These are the final affirmative states we can end up in. A
// module is a regular `foo.py` file module. A package is a
// directory containing an `__init__.py`. A namespace package is a
// directory that does *not* contain an `__init__.py`.
module [label="Single-file Module",peripheries=2];
package [label="Package",peripheries=2];
namespace_package [label="Namespace Package",peripheries=2];
not_found [label="Module Not Found",peripheries=2];
// The final states are wrapped in a subgraph with invisible edges
// to convince GraphViz to give a more human digestible rendering.
// Without this, the nodes are scattered every which way and the
// flow diagram is pretty hard to follow. This encourages (but does
// not guarantee) GraphViz to put these nodes "close" together, and
// this generally gets us something grokable.
subgraph final {
rank = same;
module -> package -> namespace_package -> not_found [style=invis];
}
START [label=<<b>START</b>>];
START -> non_shadowable;
non_shadowable [label=<
Is the search path not the standard library and<br/>
the module name is `types` or some other built-in?
>];
non_shadowable -> not_found [label="Yes"];
non_shadowable -> stub_package_check [label="No"];
stub_package_check [label=<
Is the search path in the standard library?
>];
stub_package_check -> stub_package_set [label="No"];
stub_package_check -> determine_parent_kind [label="Yes"];
stub_package_set [label=<
Set `module_name` to `{top-package}-stubs.{rest}`
>];
stub_package_set -> determine_parent_kind;
determine_parent_kind [label=<
Does every parent package of `module_name`<br/>
correspond to a directory that contains an<br/>
`__init__.py` or an `__init__.pyi`?
>];
determine_parent_kind -> maybe_package [label="Yes"];
determine_parent_kind -> namespace_parent1 [label="No"];
namespace_parent1 [label=<
Is the direct parent package<br/>
a directory that contains<br/>
an `__init__.py` or `__init__.pyi`?
>];
namespace_parent1 -> bail [label="Yes"];
namespace_parent1 -> namespace_parent2 [label="No"];
namespace_parent2 [label=<
Does the direct parent package<br/>
have a sibling file with the same<br/>
basename and a `py` or `pyi` extension?<br/>
>];
namespace_parent2 -> bail [label="Yes"];
namespace_parent2 -> namespace_parent3 [label="No"];
namespace_parent3 [label=<
Is every parent above the direct<br/>
parent package a normal package or<br/>
otherwise satisfy the previous two<br/>
namespace package requirements?
>];
namespace_parent3 -> bail [label="No"];
namespace_parent3 -> maybe_package [label="Yes"];
maybe_package [label=<
After replacing `.` with `/` in module name,<br/>
does `{path}/__init__.py` or `{path}/__init__.pyi` exist?
>];
maybe_package -> package [label="Yes"];
maybe_package -> maybe_module [label="No"];
maybe_module [label=<
Does `{path}.py` or `{path}.pyi` exist?
>];
maybe_module -> module [label="Yes"];
maybe_module -> maybe_namespace [label="No"];
maybe_namespace [label=<
Is `{path}` a directory?
>];
maybe_namespace -> namespace_package [label="Yes"];
maybe_namespace -> bail [label="No"];
bail [label=<
Is `module_name` set to a stub package candidate?
>];
bail -> not_found [label="No"];
bail -> retry [label="Yes"];
retry [label=<
Reset `module_name` to original
>];
retry -> determine_parent_kind;
}

View File

@@ -1,296 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz version 13.0.1 (0)
-->
<!-- Title: python_import_resolution Pages: 1 -->
<svg width="1463pt" height="1417pt"
viewBox="0.00 0.00 1463.00 1417.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 1413.5)">
<title>python_import_resolution</title>
<polygon fill="white" stroke="none" points="-4,4 -4,-1413.5 1458.95,-1413.5 1458.95,4 -4,4"/>
<text xml:space="preserve" text-anchor="start" x="337.85" y="-1393.2" font-family="Times,serif" font-size="14.00"> &#160;&#160;&#160;&#160;&#160;&#160;&#160;</text>
<text xml:space="preserve" text-anchor="start" x="373.85" y="-1393.2" font-family="Times,serif" font-weight="bold" font-size="14.00">Python import resolution flow diagram for a single module name in a single &quot;search path&quot;</text>
<text xml:space="preserve" text-anchor="start" x="1081.1" y="-1393.2" font-family="Times,serif" font-size="14.00"> &#160;&#160;&#160;&#160;&#160;&#160;&#160;</text>
<text xml:space="preserve" text-anchor="start" x="476.23" y="-1379.2" font-family="Times,serif" font-size="14.00">(assumes that the module name is valid and that stubs are allowed) &#160;&#160;&#160;</text>
<!-- module -->
<g id="node1" class="node">
<title>module</title>
<ellipse fill="none" stroke="black" cx="105.71" cy="-22" rx="101.71" ry="18"/>
<ellipse fill="none" stroke="black" cx="105.71" cy="-22" rx="105.71" ry="22"/>
<text xml:space="preserve" text-anchor="middle" x="105.71" y="-17.32" font-family="Times,serif" font-size="14.00">Single&#45;file Module</text>
</g>
<!-- package -->
<g id="node2" class="node">
<title>package</title>
<ellipse fill="none" stroke="black" cx="302.71" cy="-22" rx="52.26" ry="18"/>
<ellipse fill="none" stroke="black" cx="302.71" cy="-22" rx="56.26" ry="22"/>
<text xml:space="preserve" text-anchor="middle" x="302.71" y="-17.32" font-family="Times,serif" font-size="14.00">Package</text>
</g>
<!-- module&#45;&gt;package -->
<!-- namespace_package -->
<g id="node3" class="node">
<title>namespace_package</title>
<ellipse fill="none" stroke="black" cx="511.71" cy="-22" rx="113.29" ry="18"/>
<ellipse fill="none" stroke="black" cx="511.71" cy="-22" rx="117.29" ry="22"/>
<text xml:space="preserve" text-anchor="middle" x="511.71" y="-17.32" font-family="Times,serif" font-size="14.00">Namespace Package</text>
</g>
<!-- package&#45;&gt;namespace_package -->
<!-- not_found -->
<g id="node4" class="node">
<title>not_found</title>
<ellipse fill="none" stroke="black" cx="1200.71" cy="-22" rx="104.35" ry="18"/>
<ellipse fill="none" stroke="black" cx="1200.71" cy="-22" rx="108.35" ry="22"/>
<text xml:space="preserve" text-anchor="middle" x="1200.71" y="-17.32" font-family="Times,serif" font-size="14.00">Module Not Found</text>
</g>
<!-- namespace_package&#45;&gt;not_found -->
<!-- START -->
<g id="node5" class="node">
<title>START</title>
<ellipse fill="none" stroke="black" cx="1147.71" cy="-1355.5" rx="47.53" ry="18"/>
<text xml:space="preserve" text-anchor="start" x="1121.84" y="-1351.82" font-family="Times,serif" font-weight="bold" font-size="14.00">START</text>
</g>
<!-- non_shadowable -->
<g id="node6" class="node">
<title>non_shadowable</title>
<ellipse fill="none" stroke="black" cx="1147.71" cy="-1270.44" rx="307.24" ry="30.05"/>
<text xml:space="preserve" text-anchor="start" x="961.71" y="-1274.39" font-family="Times,serif" font-size="14.00"> &#160;&#160;&#160;&#160;&#160;&#160;&#160;Is the search path not the standard library and</text>
<text xml:space="preserve" text-anchor="start" x="938.46" y="-1257.14" font-family="Times,serif" font-size="14.00"> &#160;&#160;&#160;&#160;&#160;&#160;&#160;the module name is `types` or some other built&#45;in? &#160;&#160;&#160;</text>
</g>
<!-- START&#45;&gt;non_shadowable -->
<g id="edge4" class="edge">
<title>START&#45;&gt;non_shadowable</title>
<path fill="none" stroke="black" d="M1147.71,-1337.29C1147.71,-1329.95 1147.71,-1321.06 1147.71,-1312.21"/>
<polygon fill="black" stroke="black" points="1151.21,-1312.25 1147.71,-1302.25 1144.21,-1312.25 1151.21,-1312.25"/>
</g>
<!-- non_shadowable&#45;&gt;not_found -->
<g id="edge5" class="edge">
<title>non_shadowable&#45;&gt;not_found</title>
<path fill="none" stroke="black" d="M1192.48,-1240.23C1213.37,-1222.8 1233.71,-1198.54 1233.71,-1170.14 1233.71,-1170.14 1233.71,-1170.14 1233.71,-114.25 1233.71,-93.46 1226.05,-71.52 1218.11,-54.36"/>
<polygon fill="black" stroke="black" points="1221.41,-53.14 1213.86,-45.7 1215.12,-56.22 1221.41,-53.14"/>
<text xml:space="preserve" text-anchor="middle" x="1245.71" y="-609.2" font-family="Times,serif" font-size="14.00">Yes</text>
</g>
<!-- stub_package_check -->
<g id="node7" class="node">
<title>stub_package_check</title>
<ellipse fill="none" stroke="black" cx="892.71" cy="-1169.14" rx="261.65" ry="18"/>
<text xml:space="preserve" text-anchor="start" x="714.21" y="-1164.47" font-family="Times,serif" font-size="14.00"> &#160;&#160;&#160;&#160;&#160;&#160;&#160;Is the search path in the standard library? &#160;&#160;&#160;</text>
</g>
<!-- non_shadowable&#45;&gt;stub_package_check -->
<g id="edge6" class="edge">
<title>non_shadowable&#45;&gt;stub_package_check</title>
<path fill="none" stroke="black" d="M1074.3,-1240.85C1033.86,-1225.11 984.5,-1205.89 947.46,-1191.46"/>
<polygon fill="black" stroke="black" points="948.87,-1188.25 938.28,-1187.89 946.33,-1194.78 948.87,-1188.25"/>
<text xml:space="preserve" text-anchor="middle" x="1030.34" y="-1209.09" font-family="Times,serif" font-size="14.00">No</text>
</g>
<!-- stub_package_set -->
<g id="node8" class="node">
<title>stub_package_set</title>
<ellipse fill="none" stroke="black" cx="892.71" cy="-1079.89" rx="312.68" ry="18"/>
<text xml:space="preserve" text-anchor="start" x="677.84" y="-1075.22" font-family="Times,serif" font-size="14.00"> &#160;&#160;&#160;&#160;&#160;&#160;&#160;Set `module_name` to `{top&#45;package}&#45;stubs.{rest}` &#160;&#160;&#160;</text>
</g>
<!-- stub_package_check&#45;&gt;stub_package_set -->
<g id="edge7" class="edge">
<title>stub_package_check&#45;&gt;stub_package_set</title>
<path fill="none" stroke="black" d="M892.71,-1150.9C892.71,-1139.07 892.71,-1123.1 892.71,-1109.39"/>
<polygon fill="black" stroke="black" points="896.21,-1109.61 892.71,-1099.61 889.21,-1109.61 896.21,-1109.61"/>
<text xml:space="preserve" text-anchor="middle" x="902.84" y="-1119.84" font-family="Times,serif" font-size="14.00">No</text>
</g>
<!-- determine_parent_kind -->
<g id="node9" class="node">
<title>determine_parent_kind</title>
<ellipse fill="none" stroke="black" cx="833.71" cy="-982.64" rx="269.05" ry="42.25"/>
<text xml:space="preserve" text-anchor="start" x="651.46" y="-995.22" font-family="Times,serif" font-size="14.00"> &#160;&#160;&#160;&#160;&#160;&#160;&#160;Does every parent package of `module_name`</text>
<text xml:space="preserve" text-anchor="start" x="664.96" y="-977.97" font-family="Times,serif" font-size="14.00"> &#160;&#160;&#160;&#160;&#160;&#160;&#160;correspond to a directory that contains an</text>
<text xml:space="preserve" text-anchor="start" x="691.59" y="-960.72" font-family="Times,serif" font-size="14.00"> &#160;&#160;&#160;&#160;&#160;&#160;&#160;`__init__.py` or an `__init__.pyi`? &#160;&#160;&#160;</text>
</g>
<!-- stub_package_check&#45;&gt;determine_parent_kind -->
<g id="edge8" class="edge">
<title>stub_package_check&#45;&gt;determine_parent_kind</title>
<path fill="none" stroke="black" d="M729.85,-1154.72C652.04,-1144.58 570.67,-1127.2 546.71,-1097.89 511.98,-1055.39 550.89,-1028.61 610.12,-1011.78"/>
<polygon fill="black" stroke="black" points="610.79,-1015.22 619.54,-1009.24 608.97,-1008.46 610.79,-1015.22"/>
<text xml:space="preserve" text-anchor="middle" x="558.71" y="-1075.22" font-family="Times,serif" font-size="14.00">Yes</text>
</g>
<!-- stub_package_set&#45;&gt;determine_parent_kind -->
<g id="edge9" class="edge">
<title>stub_package_set&#45;&gt;determine_parent_kind</title>
<path fill="none" stroke="black" d="M881.89,-1061.42C877.15,-1053.77 871.32,-1044.35 865.37,-1034.74"/>
<polygon fill="black" stroke="black" points="868.45,-1033.07 860.21,-1026.42 862.5,-1036.76 868.45,-1033.07"/>
</g>
<!-- maybe_package -->
<g id="node10" class="node">
<title>maybe_package</title>
<ellipse fill="none" stroke="black" cx="427.71" cy="-395.05" rx="328.45" ry="30.05"/>
<text xml:space="preserve" text-anchor="start" x="254.09" y="-399" font-family="Times,serif" font-size="14.00"> &#160;&#160;&#160;&#160;&#160;&#160;&#160;After replacing `.` with `/` in module name,</text>
<text xml:space="preserve" text-anchor="start" x="203.46" y="-381.75" font-family="Times,serif" font-size="14.00"> &#160;&#160;&#160;&#160;&#160;&#160;&#160;does `{path}/__init__.py` or `{path}/__init__.pyi` exist? &#160;&#160;&#160;</text>
</g>
<!-- determine_parent_kind&#45;&gt;maybe_package -->
<g id="edge10" class="edge">
<title>determine_parent_kind&#45;&gt;maybe_package</title>
<path fill="none" stroke="black" d="M620.67,-956.53C500.71,-936.22 374.71,-901.62 374.71,-845.89 374.71,-845.89 374.71,-845.89 374.71,-531.8 374.71,-497.63 389.47,-461.73 403.41,-435.43"/>
<polygon fill="black" stroke="black" points="406.39,-437.27 408.15,-426.82 400.26,-433.89 406.39,-437.27"/>
<text xml:space="preserve" text-anchor="middle" x="386.71" y="-690.27" font-family="Times,serif" font-size="14.00">Yes</text>
</g>
<!-- namespace_parent1 -->
<g id="node11" class="node">
<title>namespace_parent1</title>
<ellipse fill="none" stroke="black" cx="833.71" cy="-844.89" rx="212.31" ry="42.25"/>
<text xml:space="preserve" text-anchor="start" x="714.84" y="-857.47" font-family="Times,serif" font-size="14.00"> &#160;&#160;&#160;&#160;&#160;&#160;&#160;Is the direct parent package</text>
<text xml:space="preserve" text-anchor="start" x="727.59" y="-840.22" font-family="Times,serif" font-size="14.00"> &#160;&#160;&#160;&#160;&#160;&#160;&#160;a directory that contains</text>
<text xml:space="preserve" text-anchor="start" x="691.59" y="-822.97" font-family="Times,serif" font-size="14.00"> &#160;&#160;&#160;&#160;&#160;&#160;&#160;an `__init__.py` or `__init__.pyi`? &#160;&#160;&#160;</text>
</g>
<!-- determine_parent_kind&#45;&gt;namespace_parent1 -->
<g id="edge11" class="edge">
<title>determine_parent_kind&#45;&gt;namespace_parent1</title>
<path fill="none" stroke="black" d="M833.71,-940.17C833.71,-927.14 833.71,-912.56 833.71,-898.84"/>
<polygon fill="black" stroke="black" points="837.21,-899.09 833.71,-889.09 830.21,-899.09 837.21,-899.09"/>
<text xml:space="preserve" text-anchor="middle" x="843.84" y="-909.09" font-family="Times,serif" font-size="14.00">No</text>
</g>
<!-- maybe_package&#45;&gt;package -->
<g id="edge18" class="edge">
<title>maybe_package&#45;&gt;package</title>
<path fill="none" stroke="black" d="M301.68,-366.89C269.45,-352.16 243.71,-329.51 243.71,-294.75 243.71,-294.75 243.71,-294.75 243.71,-114.25 243.71,-90.15 258.4,-67.38 273.12,-50.62"/>
<polygon fill="black" stroke="black" points="275.47,-53.24 279.71,-43.53 270.34,-48.47 275.47,-53.24"/>
<text xml:space="preserve" text-anchor="middle" x="255.71" y="-199.82" font-family="Times,serif" font-size="14.00">Yes</text>
</g>
<!-- maybe_module -->
<g id="node15" class="node">
<title>maybe_module</title>
<ellipse fill="none" stroke="black" cx="521.71" cy="-293.75" rx="249.55" ry="18"/>
<text xml:space="preserve" text-anchor="start" x="351.84" y="-289.07" font-family="Times,serif" font-size="14.00"> &#160;&#160;&#160;&#160;&#160;&#160;&#160;Does `{path}.py` or `{path}.pyi` exist? &#160;&#160;&#160;</text>
</g>
<!-- maybe_package&#45;&gt;maybe_module -->
<g id="edge19" class="edge">
<title>maybe_package&#45;&gt;maybe_module</title>
<path fill="none" stroke="black" d="M455.41,-364.8C468.86,-350.58 484.87,-333.67 497.79,-320.03"/>
<polygon fill="black" stroke="black" points="500.01,-322.77 504.34,-313.1 494.92,-317.96 500.01,-322.77"/>
<text xml:space="preserve" text-anchor="middle" x="496.02" y="-333.7" font-family="Times,serif" font-size="14.00">No</text>
</g>
<!-- bail -->
<g id="node12" class="node">
<title>bail</title>
<ellipse fill="none" stroke="black" cx="860.71" cy="-115.25" rx="306.9" ry="18"/>
<text xml:space="preserve" text-anchor="start" x="649.96" y="-110.58" font-family="Times,serif" font-size="14.00"> &#160;&#160;&#160;&#160;&#160;&#160;&#160;Is `module_name` set to a stub package candidate? &#160;&#160;&#160;</text>
</g>
<!-- namespace_parent1&#45;&gt;bail -->
<g id="edge12" class="edge">
<title>namespace_parent1&#45;&gt;bail</title>
<path fill="none" stroke="black" d="M878.81,-803.16C904.14,-775.71 930.71,-737.1 930.71,-695.95 930.71,-695.95 930.71,-695.95 930.71,-203.5 930.71,-178.3 912.73,-156.23 894.99,-140.59"/>
<polygon fill="black" stroke="black" points="897.48,-138.11 887.55,-134.41 893.01,-143.49 897.48,-138.11"/>
<text xml:space="preserve" text-anchor="middle" x="942.71" y="-390.38" font-family="Times,serif" font-size="14.00">Yes</text>
</g>
<!-- namespace_parent2 -->
<g id="node13" class="node">
<title>namespace_parent2</title>
<ellipse fill="none" stroke="black" cx="659.71" cy="-694.95" rx="242.54" ry="54.45"/>
<text xml:space="preserve" text-anchor="start" x="529.59" y="-716.15" font-family="Times,serif" font-size="14.00"> &#160;&#160;&#160;&#160;&#160;&#160;&#160;Does the direct parent package</text>
<text xml:space="preserve" text-anchor="start" x="526.21" y="-698.9" font-family="Times,serif" font-size="14.00"> &#160;&#160;&#160;&#160;&#160;&#160;&#160;have a sibling file with the same</text>
<text xml:space="preserve" text-anchor="start" x="496.21" y="-681.65" font-family="Times,serif" font-size="14.00"> &#160;&#160;&#160;&#160;&#160;&#160;&#160;basename and a `py` or `pyi` extension?</text>
<text xml:space="preserve" text-anchor="start" x="650.71" y="-664.4" font-family="Times,serif" font-size="14.00"> &#160;&#160;&#160;</text>
</g>
<!-- namespace_parent1&#45;&gt;namespace_parent2 -->
<g id="edge13" class="edge">
<title>namespace_parent1&#45;&gt;namespace_parent2</title>
<path fill="none" stroke="black" d="M786.17,-803.47C768.72,-788.63 748.58,-771.5 729.58,-755.35"/>
<polygon fill="black" stroke="black" points="731.98,-752.79 722.09,-748.98 727.44,-758.13 731.98,-752.79"/>
<text xml:space="preserve" text-anchor="middle" x="772.42" y="-771.34" font-family="Times,serif" font-size="14.00">No</text>
</g>
<!-- bail&#45;&gt;not_found -->
<g id="edge24" class="edge">
<title>bail&#45;&gt;not_found</title>
<path fill="none" stroke="black" d="M924.27,-97.19C981.44,-81.85 1065.5,-59.29 1125.91,-43.08"/>
<polygon fill="black" stroke="black" points="1126.68,-46.49 1135.44,-40.52 1124.87,-39.73 1126.68,-46.49"/>
<text xml:space="preserve" text-anchor="middle" x="1061.2" y="-65.95" font-family="Times,serif" font-size="14.00">No</text>
</g>
<!-- retry -->
<g id="node17" class="node">
<title>retry</title>
<ellipse fill="none" stroke="black" cx="860.71" cy="-22" rx="213.78" ry="18"/>
<text xml:space="preserve" text-anchor="start" x="716.34" y="-17.32" font-family="Times,serif" font-size="14.00"> &#160;&#160;&#160;&#160;&#160;&#160;&#160;Reset `module_name` to original &#160;&#160;&#160;</text>
</g>
<!-- bail&#45;&gt;retry -->
<g id="edge25" class="edge">
<title>bail&#45;&gt;retry</title>
<path fill="none" stroke="black" d="M860.71,-97.09C860.71,-84.3 860.71,-66.53 860.71,-51.61"/>
<polygon fill="black" stroke="black" points="864.21,-51.91 860.71,-41.91 857.21,-51.91 864.21,-51.91"/>
<text xml:space="preserve" text-anchor="middle" x="872.71" y="-65.95" font-family="Times,serif" font-size="14.00">Yes</text>
</g>
<!-- namespace_parent2&#45;&gt;bail -->
<g id="edge14" class="edge">
<title>namespace_parent2&#45;&gt;bail</title>
<path fill="none" stroke="black" d="M794.49,-649.24C842.41,-624.2 884.71,-587.02 884.71,-533.8 884.71,-533.8 884.71,-533.8 884.71,-203.5 884.71,-183.2 878.6,-161.17 872.55,-144.33"/>
<polygon fill="black" stroke="black" points="875.82,-143.08 868.98,-134.99 869.28,-145.58 875.82,-143.08"/>
<text xml:space="preserve" text-anchor="middle" x="896.71" y="-333.7" font-family="Times,serif" font-size="14.00">Yes</text>
</g>
<!-- namespace_parent3 -->
<g id="node14" class="node">
<title>namespace_parent3</title>
<ellipse fill="none" stroke="black" cx="629.71" cy="-532.8" rx="227.16" ry="54.45"/>
<text xml:space="preserve" text-anchor="start" x="496.96" y="-554" font-family="Times,serif" font-size="14.00"> &#160;&#160;&#160;&#160;&#160;&#160;&#160;Is every parent above the direct</text>
<text xml:space="preserve" text-anchor="start" x="481.59" y="-536.75" font-family="Times,serif" font-size="14.00"> &#160;&#160;&#160;&#160;&#160;&#160;&#160;parent package a normal package or</text>
<text xml:space="preserve" text-anchor="start" x="488.34" y="-519.5" font-family="Times,serif" font-size="14.00"> &#160;&#160;&#160;&#160;&#160;&#160;&#160;otherwise satisfy the previous two</text>
<text xml:space="preserve" text-anchor="start" x="477.09" y="-502.25" font-family="Times,serif" font-size="14.00"> &#160;&#160;&#160;&#160;&#160;&#160;&#160;namespace package requirements? &#160;&#160;&#160;</text>
</g>
<!-- namespace_parent2&#45;&gt;namespace_parent3 -->
<g id="edge15" class="edge">
<title>namespace_parent2&#45;&gt;namespace_parent3</title>
<path fill="none" stroke="black" d="M649.64,-640.17C647.13,-626.77 644.41,-612.25 641.81,-598.38"/>
<polygon fill="black" stroke="black" points="645.32,-598.1 640.04,-588.92 638.44,-599.39 645.32,-598.1"/>
<text xml:space="preserve" text-anchor="middle" x="655.74" y="-609.2" font-family="Times,serif" font-size="14.00">No</text>
</g>
<!-- namespace_parent3&#45;&gt;maybe_package -->
<g id="edge17" class="edge">
<title>namespace_parent3&#45;&gt;maybe_package</title>
<path fill="none" stroke="black" d="M554.21,-481.06C529.7,-464.59 503.04,-446.67 480.54,-431.56"/>
<polygon fill="black" stroke="black" points="482.69,-428.78 472.43,-426.1 478.78,-434.59 482.69,-428.78"/>
<text xml:space="preserve" text-anchor="middle" x="534.66" y="-447.05" font-family="Times,serif" font-size="14.00">Yes</text>
</g>
<!-- namespace_parent3&#45;&gt;bail -->
<g id="edge16" class="edge">
<title>namespace_parent3&#45;&gt;bail</title>
<path fill="none" stroke="black" d="M754.07,-486.86C789.03,-465.83 817.71,-436.31 817.71,-396.05 817.71,-396.05 817.71,-396.05 817.71,-203.5 817.71,-181.34 829.02,-158.97 840.03,-142.43"/>
<polygon fill="black" stroke="black" points="842.62,-144.83 845.52,-134.64 836.9,-140.8 842.62,-144.83"/>
<text xml:space="preserve" text-anchor="middle" x="827.84" y="-289.07" font-family="Times,serif" font-size="14.00">No</text>
</g>
<!-- maybe_module&#45;&gt;module -->
<g id="edge20" class="edge">
<title>maybe_module&#45;&gt;module</title>
<path fill="none" stroke="black" d="M465.44,-275.74C429.3,-263.58 382.11,-245.35 343.71,-222.5 338.06,-219.13 202.5,-104.78 138.54,-50.75"/>
<polygon fill="black" stroke="black" points="141.07,-48.3 131.17,-44.52 136.55,-53.65 141.07,-48.3"/>
<text xml:space="preserve" text-anchor="middle" x="289.08" y="-155.2" font-family="Times,serif" font-size="14.00">Yes</text>
</g>
<!-- maybe_namespace -->
<g id="node16" class="node">
<title>maybe_namespace</title>
<ellipse fill="none" stroke="black" cx="521.71" cy="-204.5" rx="169.06" ry="18"/>
<text xml:space="preserve" text-anchor="start" x="409.21" y="-199.82" font-family="Times,serif" font-size="14.00"> &#160;&#160;&#160;&#160;&#160;&#160;&#160;Is `{path}` a directory? &#160;&#160;&#160;</text>
</g>
<!-- maybe_module&#45;&gt;maybe_namespace -->
<g id="edge21" class="edge">
<title>maybe_module&#45;&gt;maybe_namespace</title>
<path fill="none" stroke="black" d="M521.71,-275.51C521.71,-263.68 521.71,-247.7 521.71,-234"/>
<polygon fill="black" stroke="black" points="525.21,-234.22 521.71,-224.22 518.21,-234.22 525.21,-234.22"/>
<text xml:space="preserve" text-anchor="middle" x="531.84" y="-244.45" font-family="Times,serif" font-size="14.00">No</text>
</g>
<!-- maybe_namespace&#45;&gt;namespace_package -->
<g id="edge22" class="edge">
<title>maybe_namespace&#45;&gt;namespace_package</title>
<path fill="none" stroke="black" d="M520.76,-186.19C519.09,-156.13 515.64,-93.8 513.51,-55.47"/>
<polygon fill="black" stroke="black" points="517.03,-55.71 512.98,-45.91 510.04,-56.09 517.03,-55.71"/>
<text xml:space="preserve" text-anchor="middle" x="529.71" y="-110.58" font-family="Times,serif" font-size="14.00">Yes</text>
</g>
<!-- maybe_namespace&#45;&gt;bail -->
<g id="edge23" class="edge">
<title>maybe_namespace&#45;&gt;bail</title>
<path fill="none" stroke="black" d="M584.29,-187.4C640.48,-172.93 723.17,-151.65 783.47,-136.13"/>
<polygon fill="black" stroke="black" points="784.17,-139.56 792.99,-133.68 782.43,-132.78 784.17,-139.56"/>
<text xml:space="preserve" text-anchor="middle" x="731.7" y="-155.2" font-family="Times,serif" font-size="14.00">No</text>
</g>
<!-- retry&#45;&gt;determine_parent_kind -->
<g id="edge26" class="edge">
<title>retry&#45;&gt;determine_parent_kind</title>
<path fill="none" stroke="black" d="M1001.81,-35.84C1093.37,-48.22 1195.71,-71.6 1195.71,-114.25 1195.71,-845.89 1195.71,-845.89 1195.71,-845.89 1195.71,-889.15 1103.96,-923.26 1012.84,-946.47"/>
<polygon fill="black" stroke="black" points="1012.05,-943.05 1003.2,-948.87 1013.75,-949.84 1012.05,-943.05"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 21 KiB

View File

@@ -1,13 +1,3 @@
/*!
This module principally provides two routines for resolving a particular module
name to a `Module`: [`resolve_module`] and [`resolve_real_module`]. You'll
usually want the former, unless you're certain you want to forbid stubs, in
which case, use the latter.
For implementors, see `import-resolution-diagram.svg` for a flow diagram that
specifies ty's implementation of Python's import resolution algorithm.
*/
use std::borrow::Cow;
use std::fmt;
use std::iter::FusedIterator;
@@ -62,7 +52,7 @@ impl ModuleResolveMode {
/// This query should not be called directly. Instead, use [`resolve_module`]. It only exists
/// because Salsa requires the module name to be an ingredient.
#[salsa::tracked(heap_size=get_size2::GetSize::get_heap_size)]
fn resolve_module_query<'db>(
pub(crate) fn resolve_module_query<'db>(
db: &'db dyn Db,
module_name: ModuleNameIngredient<'db>,
) -> Option<Module<'db>> {
@@ -159,9 +149,8 @@ pub(crate) fn search_paths(db: &dyn Db) -> SearchPathIterator {
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SearchPaths {
/// Search paths that have been statically determined purely from reading
/// ty's configuration settings. These shouldn't ever change unless the
/// config settings themselves change.
/// Search paths that have been statically determined purely from reading ty's configuration settings.
/// These shouldn't ever change unless the config settings themselves change.
static_paths: Vec<SearchPath>,
/// site-packages paths are not included in the above field:
@@ -204,19 +193,19 @@ impl SearchPaths {
for path in extra_paths {
let path = canonicalize(path, system);
tracing::debug!("Adding extra search-path `{path}`");
tracing::debug!("Adding extra search-path '{path}'");
static_paths.push(SearchPath::extra(system, path)?);
}
for src_root in src_roots {
tracing::debug!("Adding first-party search path `{src_root}`");
tracing::debug!("Adding first-party search path '{src_root}'");
static_paths.push(SearchPath::first_party(system, src_root.to_path_buf())?);
}
let (typeshed_versions, stdlib_path) = if let Some(typeshed) = typeshed {
let typeshed = canonicalize(typeshed, system);
tracing::debug!("Adding custom-stdlib search path `{typeshed}`");
tracing::debug!("Adding custom-stdlib search path '{typeshed}'");
let versions_path = typeshed.join("stdlib/VERSIONS");
@@ -245,22 +234,17 @@ impl SearchPaths {
let mut site_packages: Vec<_> = Vec::with_capacity(site_packages_paths.len());
for path in site_packages_paths {
tracing::debug!("Adding site-packages search path `{path}`");
tracing::debug!("Adding site-packages search path '{path}'");
site_packages.push(SearchPath::site_packages(system, path.clone())?);
}
// TODO vendor typeshed's third-party stubs as well as the stdlib and
// fallback to them as a final step?
//
// See: <https://github.com/astral-sh/ruff/pull/19620#discussion_r2240609135>
// TODO vendor typeshed's third-party stubs as well as the stdlib and fallback to them as a final step
// Filter out module resolution paths that point to the same directory
// on disk (the same invariant maintained by [`sys.path` at runtime]).
// (Paths may, however, *overlap* -- e.g. you could have both `src/`
// and `src/foo` as module resolution paths simultaneously.)
// Filter out module resolution paths that point to the same directory on disk (the same invariant maintained by [`sys.path` at runtime]).
// (Paths may, however, *overlap* -- e.g. you could have both `src/` and `src/foo`
// as module resolution paths simultaneously.)
//
// This code doesn't use an `IndexSet` because the key is the system
// path and not the search root.
// This code doesn't use an `IndexSet` because the key is the system path and not the search root.
//
// [`sys.path` at runtime]: https://docs.python.org/3/library/site.html#module-site
let mut seen_paths = FxHashSet::with_capacity_and_hasher(static_paths.len(), FxBuildHasher);
@@ -537,7 +521,7 @@ impl<'db> Iterator for PthFileIterator<'db> {
let contents = match system.read_to_string(&path) {
Ok(contents) => contents,
Err(error) => {
tracing::warn!("Failed to read .pth file `{path}`: {error}");
tracing::warn!("Failed to read .pth file '{path}': {error}");
continue;
}
};
@@ -598,8 +582,7 @@ fn resolve_name(db: &dyn Db, name: &ModuleName, mode: ModuleResolveMode) -> Opti
Ok((package_kind, ResolvedName::FileModule(module))) => {
if package_kind.is_root() && module.kind.is_module() {
tracing::trace!(
"Search path `{search_path}` contains a module \
named `{stub_name}` but a standalone module isn't a valid stub."
"Search path '{search_path} contains a module named `{stub_name}` but a standalone module isn't a valid stub."
);
} else {
return Some(ResolvedName::FileModule(module));
@@ -610,12 +593,12 @@ fn resolve_name(db: &dyn Db, name: &ModuleName, mode: ModuleResolveMode) -> Opti
}
Err(PackageKind::Root) => {
tracing::trace!(
"Search path `{search_path}` contains no stub package named `{stub_name}`."
"Search path '{search_path}' contains no stub package named `{stub_name}`."
);
}
Err(PackageKind::Regular) => {
tracing::trace!(
"Stub-package in `{search_path}` doesn't contain module: `{name}`"
"Stub-package in `{search_path} doesn't contain module: `{name}`"
);
// stub exists, but the module doesn't.
// TODO: Support partial packages.
@@ -623,8 +606,7 @@ fn resolve_name(db: &dyn Db, name: &ModuleName, mode: ModuleResolveMode) -> Opti
}
Err(PackageKind::Namespace) => {
tracing::trace!(
"Stub-package in `{search_path}` doesn't contain module: \
`{name}` but it is a namespace package, keep going."
"Stub-package in `{search_path} doesn't contain module: `{name}` but it is a namespace package, keep going."
);
// stub exists, but the module doesn't. But this is a namespace package,
// keep searching the next search path for a stub package with the same name.
@@ -643,19 +625,18 @@ fn resolve_name(db: &dyn Db, name: &ModuleName, mode: ModuleResolveMode) -> Opti
Err(kind) => match kind {
PackageKind::Root => {
tracing::trace!(
"Search path `{search_path}` contains no package named `{name}`."
"Search path '{search_path}' contains no package named `{name}`."
);
}
PackageKind::Regular => {
// For regular packages, don't search the next search path. All files of that
// package must be in the same location
tracing::trace!("Package in `{search_path}` doesn't contain module: `{name}`");
tracing::trace!("Package in `{search_path} doesn't contain module: `{name}`");
return None;
}
PackageKind::Namespace => {
tracing::trace!(
"Package in `{search_path}` doesn't contain module: \
`{name}` but it is a namespace package, keep going."
"Package in `{search_path} doesn't contain module: `{name}` but it is a namespace package, keep going."
);
}
},
@@ -676,9 +657,7 @@ enum ResolvedName {
/// The module name resolved to a namespace package.
///
/// For example, `from opentelemetry import trace, metrics` where
/// `opentelemetry` is a namespace package (and `trace` and `metrics` are
/// sub packages).
/// For example, `from opentelemetry import trace, metrics` where `opentelemetry` is a namespace package (and `trace` and `metrics` are sub packages).
NamespacePackage,
}
@@ -689,20 +668,6 @@ struct ResolvedFileModule {
file: File,
}
/// Attempts to resolve a module name in a particular search path.
///
/// `search_path` should be the directory to start looking for the module.
///
/// `name` should be a complete non-empty module name, e.g, `foo` or
/// `foo.bar.baz`.
///
/// Upon success, this returns the kind of the parent package (root, regular
/// package or namespace package) along with the resolved details of the
/// module: its kind (single-file module or package), the search path in
/// which it was found (guaranteed to be equal to the one given) and the
/// corresponding `File`.
///
/// Upon error, the kind of the parent package is returned.
fn resolve_name_in_search_path(
context: &ResolverContext,
name: &RelaxedModuleName,
@@ -744,23 +709,19 @@ fn resolve_name_in_search_path(
));
}
// Last resort, check if a folder with the given name exists. If so,
// then this is a namespace package. We need to skip this check for
// typeshed because the `resolve_file_module` can also return `None` if the
// `__init__.py` exists but isn't available for the current Python version.
// Let's assume that the `xml` module is only available on Python 3.11+ and
// we're resolving for Python 3.10:
// Last resort, check if a folder with the given name exists.
// If so, then this is a namespace package.
// We need to skip this check for typeshed because the `resolve_file_module` can also return `None`
// if the `__init__.py` exists but isn't available for the current Python version.
// Let's assume that the `xml` module is only available on Python 3.11+ and we're resolving for Python 3.10:
// * `resolve_file_module("xml/__init__.pyi")` returns `None` even though the file exists but the
// module isn't available for the current Python version.
// * The check here would now return `true` because the `xml` directory exists, resulting
// in a false positive for a namespace package.
//
// * `resolve_file_module("xml/__init__.pyi")` returns `None` even though
// the file exists but the module isn't available for the current Python
// version.
// * The check here would now return `true` because the `xml` directory
// exists, resulting in a false positive for a namespace package.
//
// Since typeshed doesn't use any namespace packages today (May 2025),
// simply skip this check which also helps performance. If typeshed
// ever uses namespace packages, ensure that this check also takes the
// `VERSIONS` file into consideration.
// Since typeshed doesn't use any namespace packages today (May 2025), simply skip this
// check which also helps performance. If typeshed ever uses namespace packages, ensure that
// this check also takes the `VERSIONS` file into consideration.
if !search_path.is_standard_library() && package_path.is_directory(context) {
if let Some(path) = package_path.to_system_path() {
let system = context.db.system();
@@ -812,20 +773,6 @@ fn resolve_file_module(module: &ModulePath, resolver_state: &ResolverContext) ->
Some(file)
}
/// Attempt to resolve the parent package of a module.
///
/// `module_search_path` should be the directory to start looking for the
/// parent package.
///
/// `components` should be the full module name of the parent package. This
/// specifically should not include the basename of the module. So e.g.,
/// for `foo.bar.baz`, `components` should be `[foo, bar]`. It follows that
/// `components` may be empty (in which case, the parent package is the root).
///
/// Upon success, the path to the package and its "kind" (root, regular or
/// namespace) is returned. Upon error, the kind of the package is still
/// returned based on how many components were found and whether `__init__.py`
/// is present.
fn resolve_package<'a, 'db, I>(
module_search_path: &SearchPath,
components: I,
@@ -844,7 +791,7 @@ where
// `true` if resolving a sub-package. For example, `true` when resolving `bar` of `foo.bar`.
let mut in_sub_package = false;
// For `foo.bar.baz`, test that `foo` and `bar` both contain a `__init__.py`.
// For `foo.bar.baz`, test that `foo` and `baz` both contain a `__init__.py`.
for folder in components {
package_path.push(folder);
@@ -856,8 +803,7 @@ where
// Pure modules hide namespace packages with the same name
&& resolve_file_module(&package_path, resolver_state).is_none()
{
// A directory without an `__init__.py(i)` is a namespace package,
// continue with the next folder.
// A directory without an `__init__.py(i)` is a namespace package, continue with the next folder.
in_namespace_package = true;
} else if in_namespace_package {
// Package not found but it is part of a namespace package.
@@ -903,11 +849,9 @@ enum PackageKind {
/// For example, `bar` in `foo.bar` when the `foo` directory contains an `__init__.py`.
Regular,
/// A sub-package in a namespace package. A namespace package is a package
/// without an `__init__.py`.
/// A sub-package in a namespace package. A namespace package is a package without an `__init__.py`.
///
/// For example, `bar` in `foo.bar` if the `foo` directory contains no
/// `__init__.py`.
/// For example, `bar` in `foo.bar` if the `foo` directory contains no `__init__.py`.
Namespace,
}
@@ -1544,8 +1488,8 @@ mod tests {
db.memory_file_system().remove_file(&bar_path).unwrap();
bar.sync(&mut db);
// Re-query the foo module. The foo module should still be cached
// because `bar.py` isn't relevant for resolving `foo`.
// Re-query the foo module. The foo module should still be cached because `bar.py` isn't relevant
// for resolving `foo`.
let foo_module2 = resolve_module(&db, &foo_module_name);
let foo_pieces2 = foo_module2.map(|foo_module2| {
@@ -2079,9 +2023,8 @@ not_a_directory
db.write_file(src.join("main.py"), "print('Hy')")
.context("Failed to write `main.py`")?;
// The symlink triggers the slow-path in the `OsSystem`'s
// `exists_path_case_sensitive` code because canonicalizing the path
// for `a/__init__.py` results in `a-package/__init__.py`
// The symlink triggers the slow-path in the `OsSystem`'s `exists_path_case_sensitive`
// code because canonicalizing the path for `a/__init__.py` results in `a-package/__init__.py`
std::os::unix::fs::symlink(a_package_target.as_std_path(), a_src.as_std_path())
.context("Failed to symlink `src/a` to `a-package`")?;
@@ -2100,8 +2043,7 @@ not_a_directory
let a_module_name = ModuleName::new_static("A").unwrap();
assert_eq!(resolve_module(&db, &a_module_name), None);
// Now lookup the same module using the lowercase `a` and it should
// resolve to the file in the system site-packages
// Now lookup the same module using the lowercase `a` and it should resolve to the file in the system site-packages
let a_module_name = ModuleName::new_static("a").unwrap();
let a_module = resolve_module(&db, &a_module_name).expect("a.py to resolve");
assert!(

View File

@@ -1596,10 +1596,7 @@ impl<'db> ClassLiteral<'db> {
let field_policy = CodeGeneratorKind::from_class(db, self)?;
let instance_ty =
Type::instance(db, self.apply_optional_specialization(db, specialization));
let signature_from_fields = |mut parameters: Vec<_>, return_ty: Option<Type<'db>>| {
let signature_from_fields = |mut parameters: Vec<_>| {
let mut kw_only_field_seen = false;
for (
field_name,
@@ -1672,26 +1669,21 @@ impl<'db> ClassLiteral<'db> {
}
}
let mut parameter = if kw_only_field_seen || name == "__replace__" {
let mut parameter = if kw_only_field_seen {
Parameter::keyword_only(field_name)
} else {
Parameter::positional_or_keyword(field_name)
}
.with_annotated_type(field_ty);
if name == "__replace__" {
// When replacing, we know there is a default value for the field
// (the value that is currently assigned to the field)
// assume this to be the declared type of the field
parameter = parameter.with_default_type(field_ty);
} else if let Some(default_ty) = default_ty {
if let Some(default_ty) = default_ty {
parameter = parameter.with_default_type(default_ty);
}
parameters.push(parameter);
}
let mut signature = Signature::new(Parameters::new(parameters), return_ty);
let mut signature = Signature::new(Parameters::new(parameters), Some(Type::none(db)));
signature.inherited_generic_context = self.generic_context(db);
Some(CallableType::function_like(db, signature))
};
@@ -1709,13 +1701,16 @@ impl<'db> ClassLiteral<'db> {
let self_parameter = Parameter::positional_or_keyword(Name::new_static("self"))
// TODO: could be `Self`.
.with_annotated_type(instance_ty);
signature_from_fields(vec![self_parameter], Some(Type::none(db)))
.with_annotated_type(Type::instance(
db,
self.apply_optional_specialization(db, specialization),
));
signature_from_fields(vec![self_parameter])
}
(CodeGeneratorKind::NamedTuple, "__new__") => {
let cls_parameter = Parameter::positional_or_keyword(Name::new_static("cls"))
.with_annotated_type(KnownClass::Type.to_instance(db));
signature_from_fields(vec![cls_parameter], Some(Type::none(db)))
signature_from_fields(vec![cls_parameter])
}
(CodeGeneratorKind::DataclassLike, "__lt__" | "__le__" | "__gt__" | "__ge__") => {
if !has_dataclass_param(DataclassParams::ORDER) {
@@ -1726,10 +1721,16 @@ impl<'db> ClassLiteral<'db> {
Parameters::new([
Parameter::positional_or_keyword(Name::new_static("self"))
// TODO: could be `Self`.
.with_annotated_type(instance_ty),
.with_annotated_type(Type::instance(
db,
self.apply_optional_specialization(db, specialization),
)),
Parameter::positional_or_keyword(Name::new_static("other"))
// TODO: could be `Self`.
.with_annotated_type(instance_ty),
.with_annotated_type(Type::instance(
db,
self.apply_optional_specialization(db, specialization),
)),
]),
Some(KnownClass::Bool.to_instance(db)),
);
@@ -1744,20 +1745,15 @@ impl<'db> ClassLiteral<'db> {
.place
.ignore_possibly_unbound()
}
(CodeGeneratorKind::DataclassLike, "__replace__")
if Program::get(db).python_version(db) >= PythonVersion::PY313 =>
{
let self_parameter = Parameter::positional_or_keyword(Name::new_static("self"))
.with_annotated_type(instance_ty);
signature_from_fields(vec![self_parameter], Some(instance_ty))
}
(CodeGeneratorKind::DataclassLike, "__setattr__") => {
if has_dataclass_param(DataclassParams::FROZEN) {
let signature = Signature::new(
Parameters::new([
Parameter::positional_or_keyword(Name::new_static("self"))
.with_annotated_type(instance_ty),
.with_annotated_type(Type::instance(
db,
self.apply_optional_specialization(db, specialization),
)),
Parameter::positional_or_keyword(Name::new_static("name")),
Parameter::positional_or_keyword(Name::new_static("value")),
]),

View File

@@ -10905,8 +10905,8 @@ fn contains_string_literal(expr: &ast::Expr) -> bool {
/// Map based on a `Vec`. It doesn't enforce
/// uniqueness on insertion. Instead, it relies on the caller
/// that elements are unique. For example, the way we visit definitions
/// in the `TypeInference` builder already implicitly guarantees that each definition
/// that elements are uniuqe. For example, the way we visit definitions
/// in the `TypeInference` builder make already implicitly guarantees that each definition
/// is only visited once.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct VecMap<K, V>(Vec<(K, V)>);

View File

@@ -80,7 +80,7 @@ You can add the following configuration to `.gitlab-ci.yml` to run a `ruff forma
stage: build
interruptible: true
image:
name: ghcr.io/astral-sh/ruff:0.12.7-alpine
name: ghcr.io/astral-sh/ruff:0.12.5-alpine
before_script:
- cd $CI_PROJECT_DIR
- ruff --version
@@ -106,7 +106,7 @@ Ruff can be used as a [pre-commit](https://pre-commit.com) hook via [`ruff-pre-c
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.12.7
rev: v0.12.5
hooks:
# Run the linter.
- id: ruff
@@ -119,7 +119,7 @@ To enable lint fixes, add the `--fix` argument to the lint hook:
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.12.7
rev: v0.12.5
hooks:
# Run the linter.
- id: ruff
@@ -133,7 +133,7 @@ To avoid running on Jupyter Notebooks, remove `jupyter` from the list of allowed
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.12.7
rev: v0.12.5
hooks:
# Run the linter.
- id: ruff

View File

@@ -369,7 +369,7 @@ This tutorial has focused on Ruff's command-line interface, but Ruff can also be
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.12.7
rev: v0.12.5
hooks:
# Run the linter.
- id: ruff

View File

@@ -4,12 +4,13 @@ build-backend = "maturin"
[project]
name = "ruff"
version = "0.12.7"
version = "0.12.5"
description = "An extremely fast Python linter and code formatter, written in Rust."
authors = [{ name = "Astral Software Inc.", email = "hey@astral.sh" }]
readme = "README.md"
requires-python = ">=3.7"
license = { file = "LICENSE" }
license = "MIT"
license-files = ["LICENSE"]
keywords = [
"automation",
"flake8",

View File

@@ -1,6 +1,6 @@
[project]
name = "scripts"
version = "0.12.7"
version = "0.12.5"
description = ""
authors = ["Charles Marsh <charlie.r.marsh@gmail.com>"]