Skip to content

Commit 9d7bb89

Browse files
committed
Merge branch 'main' into charlie/metaclass
* main: [ty] Split up `types/class.rs` (#23714) [ty] Rework module resolution to be breadth-first instead of depth-first (#22449) [ty] Move tests and type-alias-related code out of `types.rs` (#23711) [ty] Move TypeVar-related code to a `types::typevar` submodule (#23710) Fail CI on new linter ecosystem panics (#23597) [ty] Fix handling of non-Python text documents [ty] Move `CallableType`, and related methods/types, to a new `types::callable` submodule (#23707) [ty] Avoid stack overflow with recursive typevar (#23652) [ty] Add a diagnostic for an unused awaitable (#23650) [ty] Fix union `*args` binding for optional positional parameters (#23124) [`refurb`] Fix `FURB101` and `FURB103` false positives when I/O variable is used later (#23542) [ty] Fix type checking for multi-member enums within in a function block (#23683) [ty] Improve folding for decorators (#23543) [`airflow`] Extract common utilities for use in new rules (#23630)
2 parents a388c8a + 34cee06 commit 9d7bb89

68 files changed

Lines changed: 11061 additions & 9409 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -663,6 +663,12 @@ jobs:
663663
path: ecosystem-result
664664
if-no-files-found: "error"
665665

666+
- name: Fail on ecosystem errors
667+
run: |
668+
if grep -q "project error" ecosystem-result; then
669+
exit 1
670+
fi
671+
666672
fuzz-ty:
667673
name: "Fuzz for new ty panics"
668674
runs-on: ${{ github.repository == 'astral-sh/ruff' && 'depot-ubuntu-22.04-16' || 'ubuntu-latest' }}

crates/ruff_linter/resources/test/fixtures/airflow/AIR301_context.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,3 +205,37 @@ def test_inlet_events_dataset_subscript_ok(**context):
205205

206206
print(context["inlet_events"][Dataset("this://is-url")])
207207
print(context["inlet_events"][Asset("this://is-url")])
208+
209+
210+
# Same context checks with airflow.sdk import
211+
from airflow.sdk import task as sdk_task
212+
213+
214+
@sdk_task
215+
def sdk_access_deprecated_context_key(**context):
216+
execution_date = context["execution_date"]
217+
next_ds = context["next_ds"]
218+
219+
220+
@sdk_task
221+
def sdk_access_valid_context_key(**context):
222+
logical_date = context["logical_date"]
223+
224+
225+
# Test variant decorator forms like @task.branch and @task.short_circuit
226+
@task.branch
227+
def branch_task_with_deprecated_context(**context):
228+
execution_date = context["execution_date"]
229+
return "some_task"
230+
231+
232+
@task.short_circuit
233+
def short_circuit_task_with_deprecated_context(**context):
234+
next_ds = context["next_ds"]
235+
return True
236+
237+
238+
@task.branch()
239+
def branch_task_with_call_and_deprecated_context(**context):
240+
tomorrow_ds = context["tomorrow_ds"]
241+
return "some_task"
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# FURB101
2+
with open("file.txt", encoding="utf-8") as f:
3+
_ = f.read()
4+
f = object()
5+
print(f)
6+
7+
# See: https://github.com/astral-sh/ruff/issues/21483
8+
with open("file.txt", encoding="utf-8") as f:
9+
_ = f.read()
10+
print(f.mode)
11+
12+
# Rebinding in a later `with ... as config_file` should not suppress this one.
13+
with open("config.yaml", encoding="utf-8") as config_file:
14+
config_raw = config_file.read()
15+
16+
if "tts:" in config_raw:
17+
try:
18+
with open("config.yaml", "w", encoding="utf-8") as config_file:
19+
config_file.write(config_raw.replace("tts:", "google_translate:"))
20+
except OSError:
21+
pass
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# FURB103
2+
# should trigger
3+
with open("file.txt", "w", encoding="utf-8") as f:
4+
f.write("\n")
5+
f = object()
6+
print(f)
7+
8+
# See: https://github.com/astral-sh/ruff/issues/21483
9+
with open("file.txt", "w") as f:
10+
f.write("\n")
11+
print(f.encoding)
12+
13+
14+
def _():
15+
# should trigger
16+
with open("file.txt", "w") as f:
17+
f.write("\n")
18+
return (f.name for _ in [0])
19+
20+
21+
def _set():
22+
# should trigger
23+
with open("file.txt", "w") as f:
24+
f.write("\n")
25+
g = {f.name for _ in [0]}
26+
return g
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
use ruff_python_ast::Stmt;
2+
3+
use crate::{checkers::ast::Checker, codes::Rule, rules::refurb};
4+
5+
/// Run lint rules over all deferred with-statements in the [`SemanticModel`].
6+
pub(crate) fn deferred_with_statements(checker: &mut Checker) {
7+
while !checker.analyze.with_statements.is_empty() {
8+
let with_statements = std::mem::take(&mut checker.analyze.with_statements);
9+
for snapshot in with_statements {
10+
checker.semantic.restore(snapshot);
11+
12+
let Stmt::With(stmt_with) = checker.semantic.current_statement() else {
13+
unreachable!("Expected Stmt::With");
14+
};
15+
if checker.is_rule_enabled(Rule::ReadWholeFile) {
16+
refurb::rules::read_whole_file(checker, stmt_with);
17+
}
18+
if checker.is_rule_enabled(Rule::WriteWholeFile) {
19+
refurb::rules::write_whole_file(checker, stmt_with);
20+
}
21+
}
22+
}
23+
}

crates/ruff_linter/src/checkers/ast/analyze/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ pub(super) use deferred_comprehensions::deferred_comprehensions;
44
pub(super) use deferred_for_loops::deferred_for_loops;
55
pub(super) use deferred_lambdas::deferred_lambdas;
66
pub(super) use deferred_scopes::deferred_scopes;
7+
pub(super) use deferred_with_statements::deferred_with_statements;
78
pub(super) use definitions::definitions;
89
pub(super) use except_handler::except_handler;
910
pub(super) use expression::expression;
@@ -21,6 +22,7 @@ mod deferred_comprehensions;
2122
mod deferred_for_loops;
2223
mod deferred_lambdas;
2324
mod deferred_scopes;
25+
mod deferred_with_statements;
2426
mod definitions;
2527
mod except_handler;
2628
mod expression;

crates/ruff_linter/src/checkers/ast/analyze/statement.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1181,11 +1181,11 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
11811181
if checker.is_rule_enabled(Rule::RedefinedLoopName) {
11821182
pylint::rules::redefined_loop_name(checker, stmt);
11831183
}
1184-
if checker.is_rule_enabled(Rule::ReadWholeFile) {
1185-
refurb::rules::read_whole_file(checker, with_stmt);
1186-
}
1187-
if checker.is_rule_enabled(Rule::WriteWholeFile) {
1188-
refurb::rules::write_whole_file(checker, with_stmt);
1184+
if checker.any_rule_enabled(&[Rule::ReadWholeFile, Rule::WriteWholeFile]) {
1185+
checker
1186+
.analyze
1187+
.with_statements
1188+
.push(checker.semantic.snapshot());
11891189
}
11901190
if checker.is_rule_enabled(Rule::UselessWithLock) {
11911191
pylint::rules::useless_with_lock(checker, with_stmt);

crates/ruff_linter/src/checkers/ast/deferred.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,5 +34,6 @@ pub(crate) struct Analyze {
3434
pub(crate) scopes: Vec<ScopeId>,
3535
pub(crate) lambdas: Vec<Snapshot>,
3636
pub(crate) for_loops: Vec<Snapshot>,
37+
pub(crate) with_statements: Vec<Snapshot>,
3738
pub(crate) comprehensions: Vec<Snapshot>,
3839
}

crates/ruff_linter/src/checkers/ast/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3321,6 +3321,7 @@ pub(crate) fn check_ast(
33213321
analyze::definitions(&mut checker);
33223322
analyze::bindings(&checker);
33233323
analyze::unresolved_references(&checker);
3324+
analyze::deferred_with_statements(&mut checker);
33243325

33253326
// Reset the scope to module-level, and check all consumed scopes.
33263327
checker.semantic.scope_id = ScopeId::global();

crates/ruff_linter/src/rules/airflow/helpers.rs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use crate::fix::edits::remove_unused_imports;
33
use crate::importer::ImportRequest;
44
use crate::rules::numpy::helpers::{AttributeSearcher, ImportSearcher};
55
use ruff_diagnostics::{Edit, Fix};
6+
use ruff_python_ast::helpers::map_callable;
67
use ruff_python_ast::name::{QualifiedName, QualifiedNameBuilder};
78
use ruff_python_ast::statement_visitor::StatementVisitor;
89
use ruff_python_ast::visitor::Visitor;
@@ -290,3 +291,38 @@ where
290291

291292
any_qualified_base_class(class_def, semantic, &is_base_class)
292293
}
294+
295+
/// Returns `true` if the current statement hierarchy has a function that's decorated with
296+
/// `@airflow.decorators.task` or `@airflow.sdk.task`.
297+
pub(crate) fn in_airflow_task_function(semantic: &SemanticModel) -> bool {
298+
semantic
299+
.current_statements()
300+
.find_map(|stmt| stmt.as_function_def_stmt())
301+
.is_some_and(|function_def| is_airflow_task(function_def, semantic))
302+
}
303+
304+
/// Returns `true` if the given function is decorated with `@airflow.decorators.task`
305+
/// (or `@airflow.sdk.task`), including variant forms like `@task.branch` and
306+
/// `@task.short_circuit`.
307+
pub(crate) fn is_airflow_task(function_def: &StmtFunctionDef, semantic: &SemanticModel) -> bool {
308+
function_def.decorator_list.iter().any(|decorator| {
309+
let expr = map_callable(&decorator.expression);
310+
311+
// Match `@task` and `@task()` directly.
312+
if semantic
313+
.resolve_qualified_name(expr)
314+
.is_some_and(|qn| matches!(qn.segments(), ["airflow", "decorators" | "sdk", "task"]))
315+
{
316+
return true;
317+
}
318+
319+
// Match `@task.<variant>` (e.g., `@task.branch`, `@task.short_circuit`).
320+
if let Expr::Attribute(ExprAttribute { value, .. }) = expr {
321+
return semantic.resolve_qualified_name(value).is_some_and(|qn| {
322+
matches!(qn.segments(), ["airflow", "decorators" | "sdk", "task"])
323+
});
324+
}
325+
326+
false
327+
})
328+
}

0 commit comments

Comments
 (0)