Skip to content

Commit 111ee16

Browse files
committed
Implement Airflow rule for 3.0 removals
Airflow 3.0 removes various deprecated functions, members, modules, and other values. They have been deprecated in 2.x, but the removal causes incompatibilities that we want to detect.
1 parent a90e404 commit 111ee16

File tree

7 files changed

+154
-2
lines changed

7 files changed

+154
-2
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from airflow.utils import dates
2+
from airflow.utils.dates import date_range, datetime_to_nano, days_ago
3+
4+
date_range
5+
days_ago
6+
7+
dates.date_range
8+
dates.days_ago
9+
10+
# This one was not deprecated.
11+
datetime_to_nano
12+
dates.datetime_to_nano

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

+8-2
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ use ruff_text_size::Ranged;
1111
use crate::checkers::ast::Checker;
1212
use crate::registry::Rule;
1313
use crate::rules::{
14-
flake8_2020, flake8_async, flake8_bandit, flake8_boolean_trap, flake8_bugbear, flake8_builtins,
15-
flake8_comprehensions, flake8_datetimez, flake8_debugger, flake8_django,
14+
airflow, flake8_2020, flake8_async, flake8_bandit, flake8_boolean_trap, flake8_bugbear,
15+
flake8_builtins, flake8_comprehensions, flake8_datetimez, flake8_debugger, flake8_django,
1616
flake8_future_annotations, flake8_gettext, flake8_implicit_str_concat, flake8_logging,
1717
flake8_logging_format, flake8_pie, flake8_print, flake8_pyi, flake8_pytest_style, flake8_self,
1818
flake8_simplify, flake8_tidy_imports, flake8_type_checking, flake8_use_pathlib, flynt, numpy,
@@ -216,6 +216,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
216216
if checker.enabled(Rule::RegexFlagAlias) {
217217
refurb::rules::regex_flag_alias(checker, expr);
218218
}
219+
if checker.enabled(Rule::AirflowDeprecatedMembers) {
220+
airflow::rules::deprecated_members(checker, expr);
221+
}
219222

220223
// Ex) List[...]
221224
if checker.any_enabled(&[
@@ -376,6 +379,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
376379
if checker.enabled(Rule::ByteStringUsage) {
377380
flake8_pyi::rules::bytestring_attribute(checker, expr);
378381
}
382+
if checker.enabled(Rule::AirflowDeprecatedMembers) {
383+
airflow::rules::deprecated_members(checker, expr);
384+
}
379385
}
380386
Expr::Call(
381387
call @ ast::ExprCall {

crates/ruff_linter/src/codes.rs

+1
Original file line numberDiff line numberDiff line change
@@ -1031,6 +1031,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
10311031

10321032
// airflow
10331033
(Airflow, "001") => (RuleGroup::Stable, rules::airflow::rules::AirflowVariableNameTaskIdMismatch),
1034+
(Airflow, "302") => (RuleGroup::Preview, rules::airflow::rules::AirflowDeprecatedMembers),
10341035

10351036
// perflint
10361037
(Perflint, "101") => (RuleGroup::Stable, rules::perflint::rules::UnnecessaryListCast),

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

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ mod tests {
1313
use crate::{assert_messages, settings};
1414

1515
#[test_case(Rule::AirflowVariableNameTaskIdMismatch, Path::new("AIR001.py"))]
16+
#[test_case(Rule::AirflowDeprecatedMembers, Path::new("AIR302.py"))]
1617
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
1718
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
1819
let diagnostics = test_path(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
use ruff_diagnostics::{Diagnostic, Violation};
2+
use ruff_macros::{derive_message_formats, violation};
3+
use ruff_python_ast::Expr;
4+
use ruff_text_size::Ranged;
5+
6+
use crate::checkers::ast::Checker;
7+
8+
#[derive(Debug, Eq, PartialEq)]
9+
enum Replacement {
10+
None,
11+
Name(String),
12+
}
13+
14+
impl Replacement {
15+
fn name(name: impl Into<String>) -> Self {
16+
Self::Name(name.into())
17+
}
18+
}
19+
20+
/// ## What it does
21+
/// Checks for uses of deprecated Airflow functions and values.
22+
///
23+
/// ## Why is this bad?
24+
/// Airflow 3.0 removed various deprecated functions, members, and other
25+
/// values. Some have more modern replacements. Others are considered too niche
26+
/// and not worth to be maintained in Airflow.
27+
///
28+
/// ## Example
29+
/// ```python
30+
/// from airflow.utils.dates import days_ago
31+
///
32+
///
33+
/// yesterday = days_ago(today, 1)
34+
/// ```
35+
///
36+
/// Use instead:
37+
/// ```python
38+
/// from datetime import timedelta
39+
///
40+
///
41+
/// yesterday = today - timedelta(days=1)
42+
/// ```
43+
#[violation]
44+
pub struct AirflowDeprecatedMembers {
45+
deprecated: String,
46+
replacement: Replacement,
47+
}
48+
49+
impl Violation for AirflowDeprecatedMembers {
50+
#[derive_message_formats]
51+
fn message(&self) -> String {
52+
let AirflowDeprecatedMembers {
53+
deprecated,
54+
replacement,
55+
} = self;
56+
match replacement {
57+
Replacement::None => format!("`{deprecated}` is removed in Airflow 3.0"),
58+
Replacement::Name(name) => {
59+
format!("`{deprecated}` is removed in Airflow 3.0; use {name} instead")
60+
}
61+
}
62+
}
63+
}
64+
65+
/// AIR302
66+
pub(crate) fn deprecated_members(checker: &mut Checker, expr: &Expr) {
67+
let Some((deprecated, replacement)) =
68+
checker
69+
.semantic()
70+
.resolve_qualified_name(expr)
71+
.and_then(|qualname| match qualname.segments() {
72+
["airflow", "utils", "dates", "date_range"] => {
73+
Some((qualname.to_string(), Replacement::None))
74+
}
75+
["airflow", "utils", "dates", "days_ago"] => Some((
76+
qualname.to_string(),
77+
Replacement::name("datetime.timedelta()"),
78+
)),
79+
_ => None,
80+
})
81+
else {
82+
return;
83+
};
84+
85+
checker.diagnostics.push(Diagnostic::new(
86+
AirflowDeprecatedMembers {
87+
deprecated,
88+
replacement,
89+
},
90+
expr.range(),
91+
));
92+
}
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
pub(crate) use deprecated_members::*;
12
pub(crate) use task_variable_name::*;
23

4+
mod deprecated_members;
35
mod task_variable_name;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
---
2+
source: crates/ruff_linter/src/rules/airflow/mod.rs
3+
---
4+
AIR302.py:4:1: AIR302 `airflow.utils.dates.date_range` is removed in Airflow 3.0
5+
|
6+
2 | from airflow.utils.dates import date_range, datetime_to_nano, days_ago
7+
3 |
8+
4 | date_range
9+
| ^^^^^^^^^^ AIR302
10+
5 | days_ago
11+
|
12+
13+
AIR302.py:5:1: AIR302 `airflow.utils.dates.days_ago` is removed in Airflow 3.0; use datetime.timedelta() instead
14+
|
15+
4 | date_range
16+
5 | days_ago
17+
| ^^^^^^^^ AIR302
18+
6 |
19+
7 | dates.date_range
20+
|
21+
22+
AIR302.py:7:1: AIR302 `airflow.utils.dates.date_range` is removed in Airflow 3.0
23+
|
24+
5 | days_ago
25+
6 |
26+
7 | dates.date_range
27+
| ^^^^^^^^^^^^^^^^ AIR302
28+
8 | dates.days_ago
29+
|
30+
31+
AIR302.py:8:1: AIR302 `airflow.utils.dates.days_ago` is removed in Airflow 3.0; use datetime.timedelta() instead
32+
|
33+
7 | dates.date_range
34+
8 | dates.days_ago
35+
| ^^^^^^^^^^^^^^ AIR302
36+
9 |
37+
10 | # This one was not deprecated.
38+
|

0 commit comments

Comments
 (0)