From 34cc3cab980cf5be4d737db1f73bac77b19705fb Mon Sep 17 00:00:00 2001 From: Ankit Chaurasia <8670962+sunank200@users.noreply.github.com> Date: Fri, 24 Jan 2025 11:40:05 +0545 Subject: [PATCH] [`airflow`] Update `AIR302` to check for deprecated context keys (#15144) **Summary** Airflow 3.0 removes a set of deprecated context variables that were phased out in 2.x. This PR introduces lint rules to detect usage of these removed variables in various patterns, helping identify incompatibilities. The removed context variables include: ``` conf execution_date next_ds next_ds_nodash next_execution_date prev_ds prev_ds_nodash prev_execution_date prev_execution_date_success tomorrow_ds yesterday_ds yesterday_ds_nodash ``` **Detected Patterns and Examples** The linter now flags the use of removed context variables in the following scenarios: 1. **Direct Subscript Access** ```python execution_date = context["execution_date"] # Flagged ``` 2. **`.get("key")` Method Calls** ```python print(context.get("execution_date")) # Flagged ``` 3. **Variables Assigned from `get_current_context()`** If a variable is assigned from `get_current_context()` and then used to access a removed key: ```python c = get_current_context() print(c.get("execution_date")) # Flagged ``` 4. **Function Parameters in `@task`-Decorated Functions** Parameters named after removed context variables in functions decorated with `@task` are flagged: ```python from airflow.decorators import task @task def my_task(execution_date, **kwargs): # Parameter 'execution_date' flagged pass ``` 5. **Removed Keys in Task Decorator `kwargs` and Other Scenarios** Other similar patterns where removed context variables appear (e.g., as part of `kwargs` in a `@task` function) are also detected. ``` from airflow.decorators import task @task def process_with_execution_date(**context): execution_date = lambda: context["execution_date"] # flagged print(execution_date) @task(kwargs={"execution_date": "2021-01-01"}) # flagged def task_with_kwargs(**context): pass ``` **Test Plan** Test fixtures covering various patterns of deprecated context usage are included in this PR. For example: ```python from airflow.decorators import task, dag, get_current_context from airflow.models import DAG from airflow.operators.dummy import DummyOperator import pendulum from datetime import datetime @task def access_invalid_key_task(**context): print(context.get("conf")) # 'conf' flagged @task def print_config(**context): execution_date = context["execution_date"] # Flagged prev_ds = context["prev_ds"] # Flagged @task def from_current_context(): context = get_current_context() print(context["execution_date"]) # Flagged # Usage outside of a task decorated function c = get_current_context() print(c.get("execution_date")) # Flagged @task def some_task(execution_date, **kwargs): print("execution date", execution_date) # Parameter flagged @dag( start_date=pendulum.datetime(2021, 1, 1, tz="UTC") ) def my_dag(): task1 = DummyOperator( task_id="task1", params={ "execution_date": "{{ execution_date }}", # Flagged in template context }, ) access_invalid_key_task() print_config() from_current_context() dag = my_dag() class CustomOperator(BaseOperator): def execute(self, context): execution_date = context.get("execution_date") # Flagged next_ds = context.get("next_ds") # Flagged next_execution_date = context["next_execution_date"] # Flagged ``` Ruff will emit `AIR302` diagnostics for each deprecated usage, with suggestions when applicable, aiding in code migration to Airflow 3.0. related: https://github.com/apache/airflow/issues/44409, https://github.com/apache/airflow/issues/41641 --------- Co-authored-by: Wei Lee --- .../test/fixtures/airflow/AIR302_context.py | 127 +++++ .../src/checkers/ast/analyze/expression.rs | 4 +- .../src/checkers/ast/analyze/statement.rs | 3 + crates/ruff_linter/src/rules/airflow/mod.rs | 1 + .../src/rules/airflow/rules/removal_in_3.rs | 297 +++++++++- ...flow__tests__AIR302_AIR302_context.py.snap | 514 ++++++++++++++++++ 6 files changed, 941 insertions(+), 5 deletions(-) create mode 100644 crates/ruff_linter/resources/test/fixtures/airflow/AIR302_context.py create mode 100644 crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_context.py.snap diff --git a/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_context.py b/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_context.py new file mode 100644 index 0000000000000..2d2764f5ddf2c --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_context.py @@ -0,0 +1,127 @@ +from datetime import datetime + +import pendulum + +from airflow.decorators import dag, task +from airflow.models import DAG +from airflow.models.baseoperator import BaseOperator +from airflow.operators.dummy import DummyOperator +from airflow.plugins_manager import AirflowPlugin +from airflow.providers.standard.operators.python import PythonOperator +from airflow.utils.context import get_current_context + + +def access_invalid_key_in_context(**context): + print("access invalid key", context["conf"]) + +@task +def access_invalid_key_task_out_of_dag(**context): + print("access invalid key", context.get("conf")) + +@dag( + schedule=None, + start_date=pendulum.datetime(2021, 1, 1, tz="UTC"), + catchup=False, + tags=[""], +) +def invalid_dag(): + @task() + def access_invalid_key_task(**context): + print("access invalid key", context.get("conf")) + + task1 = PythonOperator( + task_id="task1", + python_callable=access_invalid_key_in_context, + ) + access_invalid_key_task() >> task1 + access_invalid_key_task_out_of_dag() + +invalid_dag() + +@task +def print_config(**context): + # This should not throw an error as logical_date is part of airflow context. + logical_date = context["logical_date"] + + # Removed usage - should trigger violations + execution_date = context["execution_date"] + next_ds = context["next_ds"] + next_ds_nodash = context["next_ds_nodash"] + next_execution_date = context["next_execution_date"] + prev_ds = context["prev_ds"] + prev_ds_nodash = context["prev_ds_nodash"] + prev_execution_date = context["prev_execution_date"] + prev_execution_date_success = context["prev_execution_date_success"] + tomorrow_ds = context["tomorrow_ds"] + yesterday_ds = context["yesterday_ds"] + yesterday_ds_nodash = context["yesterday_ds_nodash"] + +with DAG( + dag_id="example_dag", + schedule_interval="@daily", + start_date=datetime(2023, 1, 1), + template_searchpath=["/templates"], +) as dag: + task1 = DummyOperator( + task_id="task1", + params={ + # Removed variables in template + "execution_date": "{{ execution_date }}", + "next_ds": "{{ next_ds }}", + "prev_ds": "{{ prev_ds }}" + }, + ) + +class CustomMacrosPlugin(AirflowPlugin): + name = "custom_macros" + macros = { + "execution_date_macro": lambda context: context["execution_date"], + "next_ds_macro": lambda context: context["next_ds"] + } + +@task +def print_config(): + context = get_current_context() + execution_date = context["execution_date"] + next_ds = context["next_ds"] + next_ds_nodash = context["next_ds_nodash"] + next_execution_date = context["next_execution_date"] + prev_ds = context["prev_ds"] + prev_ds_nodash = context["prev_ds_nodash"] + prev_execution_date = context["prev_execution_date"] + prev_execution_date_success = context["prev_execution_date_success"] + tomorrow_ds = context["tomorrow_ds"] + yesterday_ds = context["yesterday_ds"] + yesterday_ds_nodash = context["yesterday_ds_nodash"] + +class CustomOperator(BaseOperator): + def execute(self, context): + execution_date = context["execution_date"] + next_ds = context["next_ds"] + next_ds_nodash = context["next_ds_nodash"] + next_execution_date = context["next_execution_date"] + prev_ds = context["prev_ds"] + prev_ds_nodash = context["prev_ds_nodash"] + prev_execution_date = context["prev_execution_date"] + prev_execution_date_success = context["prev_execution_date_success"] + tomorrow_ds = context["tomorrow_ds"] + yesterday_ds = context["yesterday_ds"] + yesterday_ds_nodash = context["yesterday_ds_nodash"] + +@task +def access_invalid_argument_task_out_of_dag(execution_date, tomorrow_ds, logical_date, **context): + print("execution date", execution_date) + print("access invalid key", context.get("conf")) + +@task(task_id="print_the_context") +def print_context(ds=None, **kwargs): + """Print the Airflow context and ds variable from the context.""" + print(ds) + print(kwargs.get("tomorrow_ds")) + c = get_current_context() + c.get("execution_date") + +class CustomOperatorNew(BaseOperator): + def execute(self, context): + execution_date = context.get("execution_date") + next_ds = context.get("next_ds") diff --git a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs index ce5425a2ab95a..74d877f91ba57 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs @@ -175,7 +175,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) { if checker.enabled(Rule::NonPEP646Unpack) { pyupgrade::rules::use_pep646_unpack(checker, subscript); } - + if checker.enabled(Rule::Airflow3Removal) { + airflow::rules::removed_in_3(checker, expr); + } pandas_vet::rules::subscript(checker, value, expr); } Expr::Tuple(ast::ExprTuple { diff --git a/crates/ruff_linter/src/checkers/ast/analyze/statement.rs b/crates/ruff_linter/src/checkers/ast/analyze/statement.rs index 414010e631706..dcb3ceea625ee 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/statement.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/statement.rs @@ -376,6 +376,9 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) { if checker.enabled(Rule::PytestParameterWithDefaultArgument) { flake8_pytest_style::rules::parameter_with_default_argument(checker, function_def); } + if checker.enabled(Rule::Airflow3Removal) { + airflow::rules::removed_in_3_function_def(checker, function_def); + } if checker.enabled(Rule::NonPEP695GenericFunction) { pyupgrade::rules::non_pep695_generic_function(checker, function_def); } diff --git a/crates/ruff_linter/src/rules/airflow/mod.rs b/crates/ruff_linter/src/rules/airflow/mod.rs index 2f2e7dded7bb0..d7020097863ac 100644 --- a/crates/ruff_linter/src/rules/airflow/mod.rs +++ b/crates/ruff_linter/src/rules/airflow/mod.rs @@ -18,6 +18,7 @@ mod tests { #[test_case(Rule::Airflow3Removal, Path::new("AIR302_names.py"))] #[test_case(Rule::Airflow3Removal, Path::new("AIR302_class_attribute.py"))] #[test_case(Rule::Airflow3Removal, Path::new("AIR302_airflow_plugin.py"))] + #[test_case(Rule::Airflow3Removal, Path::new("AIR302_context.py"))] #[test_case(Rule::Airflow3MovedToProvider, Path::new("AIR303.py"))] fn rules(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy()); diff --git a/crates/ruff_linter/src/rules/airflow/rules/removal_in_3.rs b/crates/ruff_linter/src/rules/airflow/rules/removal_in_3.rs index 6c9238b6ad4ff..41b0e932ff5fe 100644 --- a/crates/ruff_linter/src/rules/airflow/rules/removal_in_3.rs +++ b/crates/ruff_linter/src/rules/airflow/rules/removal_in_3.rs @@ -1,17 +1,19 @@ +use crate::checkers::ast::Checker; use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_python_ast::helpers::map_callable; +use ruff_python_ast::AnyParameterRef; use ruff_python_ast::{ name::QualifiedName, Arguments, Expr, ExprAttribute, ExprCall, ExprContext, ExprName, - StmtClassDef, + ExprStringLiteral, ExprSubscript, Stmt, StmtClassDef, StmtFunctionDef, }; use ruff_python_semantic::analyze::typing; use ruff_python_semantic::Modules; use ruff_python_semantic::ScopeKind; +use ruff_python_semantic::SemanticModel; use ruff_text_size::Ranged; use ruff_text_size::TextRange; -use crate::checkers::ast::Checker; - /// ## What it does /// Checks for uses of deprecated Airflow functions and values. /// @@ -71,7 +73,111 @@ impl Violation for Airflow3Removal { } } -/// AIR302 +const REMOVED_CONTEXT_KEYS: [&str; 12] = [ + "conf", + "execution_date", + "next_ds", + "next_ds_nodash", + "next_execution_date", + "prev_ds", + "prev_ds_nodash", + "prev_execution_date", + "prev_execution_date_success", + "tomorrow_ds", + "yesterday_ds", + "yesterday_ds_nodash", +]; + +fn extract_name_from_slice(slice: &Expr) -> Option { + match slice { + Expr::StringLiteral(ExprStringLiteral { value, .. }) => Some(value.to_string()), + _ => None, + } +} + +/// Check if a subscript expression accesses a removed Airflow context variable. +/// If a removed key is found, push a corresponding diagnostic. +fn check_context_variable(checker: &mut Checker, subscript: &ExprSubscript) { + let ExprSubscript { value, slice, .. } = subscript; + + let is_context_arg = if let Expr::Name(ExprName { id, .. }) = &**value { + id.as_str() == "context" || id.as_str().starts_with("**") + } else { + false + }; + + let is_current_context = + if let Some(qualname) = typing::resolve_assignment(value, checker.semantic()) { + matches!( + qualname.segments(), + ["airflow", "utils", "context", "get_current_context"] + ) + } else { + false + }; + + if is_context_arg || is_current_context { + if let Some(key) = extract_name_from_slice(slice) { + if REMOVED_CONTEXT_KEYS.contains(&key.as_str()) { + checker.diagnostics.push(Diagnostic::new( + Airflow3Removal { + deprecated: key, + replacement: Replacement::None, + }, + slice.range(), + )); + } + } + } +} + +// Function to handle `var.get(...)` outside of @task-decorated functions +fn check_removed_context_keys_get_anywhere(checker: &mut Checker, call_expr: &ExprCall) { + let Expr::Attribute(ExprAttribute { attr, value, .. }) = &*call_expr.func else { + return; + }; + + if attr.as_str() != "get" { + return; + } + + // Check if the value is a context argument + let is_context_arg = if let Expr::Name(ExprName { id, .. }) = &**value { + id.as_str() == "context" || id.as_str().starts_with("**") + } else { + false + }; + + let is_current_context = + if let Some(qualname) = typing::resolve_assignment(value, checker.semantic()) { + matches!( + qualname.segments(), + ["airflow", "utils", "context", "get_current_context"] + ) + } else { + false + }; + + if is_context_arg || is_current_context { + for removed_key in REMOVED_CONTEXT_KEYS { + if let Some(argument) = call_expr.arguments.find_positional(0) { + if let Expr::StringLiteral(ExprStringLiteral { value, .. }) = argument { + if value == removed_key { + checker.diagnostics.push(Diagnostic::new( + Airflow3Removal { + deprecated: removed_key.to_string(), + replacement: Replacement::None, + }, + argument.range(), + )); + } + } + } + } + } +} + +// Modify the `removed_in_3` function to call the new check for `var.get(...)` pub(crate) fn removed_in_3(checker: &mut Checker, expr: &Expr) { if !checker.semantic().seen_module(Modules::AIRFLOW) { return; @@ -87,6 +193,8 @@ pub(crate) fn removed_in_3(checker: &mut Checker, expr: &Expr) { check_call_arguments(checker, &qualname, arguments); }; check_method(checker, call_expr); + check_removed_context_keys_usage(checker, call_expr); + check_removed_context_keys_get_anywhere(checker, call_expr); } Expr::Attribute(attribute_expr @ ExprAttribute { attr, .. }) => { check_name(checker, expr, attr.range()); @@ -100,6 +208,9 @@ pub(crate) fn removed_in_3(checker: &mut Checker, expr: &Expr) { } } } + Expr::Subscript(subscript_expr) => { + check_context_variable(checker, subscript_expr); + } _ => {} } } @@ -252,6 +363,133 @@ fn check_class_attribute(checker: &mut Checker, attribute_expr: &ExprAttribute) } } +/// Finds the parameter definition for a given name expression in a function. +fn find_parameter<'a>( + semantic: &'a SemanticModel, + name: &'a ExprName, +) -> Option> { + let binding_id = semantic.only_binding(name)?; + let binding = semantic.binding(binding_id); + let StmtFunctionDef { parameters, .. } = binding.statement(semantic)?.as_function_def_stmt()?; + parameters + .iter() + .find(|parameter| parameter.name().range() == binding.range()) +} + +/// Checks whether an Airflow 3.0–removed context key is used in a function decorated with `@task`. +/// +/// Specifically, it flags two scenarios for task decorated function: +/// 1. A removed context variable passed in as a function parameter name (e.g., `execution_date`). +/// 2. A removed context key accessed via `context.get("...")`. +/// +/// # Examples +/// +/// **Removed key used in `context.get(...)`:** +/// ```python +/// from airflow.decorators import task +/// +/// @task +/// def my_task(**context): +/// # 'conf' is removed in Airflow 3.0 +/// print(context.get("conf")) +/// ``` +/// +/// **Accessing multiple keys:** +/// ```python +/// from airflow.decorators import task +/// +/// @task +/// def more_keys(**context): +/// # 'prev_ds' is also removed in Airflow 3.0 +/// print(context.get("prev_ds")) +/// ``` +fn check_removed_context_keys_usage(checker: &mut Checker, call_expr: &ExprCall) { + if !is_taskflow(checker) { + return; + } + + let Expr::Attribute(ExprAttribute { value, attr, .. }) = &*call_expr.func else { + return; + }; + + let is_named_context = if let Expr::Name(name) = &**value { + if let Some(parameter) = find_parameter(checker.semantic(), name) { + matches!(parameter.name().as_str(), "context" | "kwargs") + || parameter.name().as_str().starts_with("**") + } else { + false + } + } else { + false + }; + + let is_assigned_from_get_current_context = + if let Some(qualname) = typing::resolve_assignment(value, checker.semantic()) { + matches!( + qualname.segments(), + ["airflow", "utils", "context", "get_current_context"] + ) + } else { + false + }; + + if !(is_named_context || is_assigned_from_get_current_context) { + return; + } + + if attr.as_str() != "get" { + return; + } + + for removed_key in REMOVED_CONTEXT_KEYS { + if let Some(argument) = call_expr.arguments.find_positional(0) { + if let Expr::StringLiteral(ExprStringLiteral { value, .. }) = argument { + if value == removed_key { + checker.diagnostics.push(Diagnostic::new( + Airflow3Removal { + deprecated: removed_key.to_string(), + replacement: Replacement::None, + }, + argument.range(), + )); + } + } + } + } +} + +/// Check whether the function is decorated by @task +/// +/// +/// Examples for the above patterns: +/// ```python +/// from airflow.decorators import task +/// +/// +/// @task +/// def access_invalid_key_task_out_of_dag(**context): +/// print("access invalid key", context.get("conf")) +/// ``` +fn is_taskflow(checker: &mut Checker) -> bool { + let mut parents = checker.semantic().current_statements(); + if let Some(Stmt::FunctionDef(StmtFunctionDef { decorator_list, .. })) = + parents.find(|stmt| stmt.is_function_def_stmt()) + { + for decorator in decorator_list { + if checker + .semantic() + .resolve_qualified_name(map_callable(&decorator.expression)) + .is_some_and(|qualified_name| { + matches!(qualified_name.segments(), ["airflow", "decorators", "task"]) + }) + { + return true; + } + } + } + false +} + /// Check whether a removed Airflow class method is called. /// /// For example: @@ -860,3 +1098,54 @@ fn is_airflow_builtin_or_provider(segments: &[&str], module: &str, symbol_suffix _ => false, } } + +/// AIR302 Check the function argument for removed context variable. +/// For example: +/// **Removed context variable as a parameter:** +/// ```python +/// from airflow.decorators import task +/// +/// @task +/// def another_task(execution_date, **kwargs): +/// # 'execution_date' is removed in Airflow 3.0 +/// pass +/// ``` +pub(crate) fn removed_in_3_function_def(checker: &mut Checker, function_def: &StmtFunctionDef) { + if !checker.semantic().seen_module(Modules::AIRFLOW) { + return; + } + + if !is_airflow_task(function_def, checker.semantic()) { + return; + } + + for param in function_def + .parameters + .posonlyargs + .iter() + .chain(function_def.parameters.args.iter()) + .chain(function_def.parameters.kwonlyargs.iter()) + { + let param_name = param.parameter.name.as_str(); + if REMOVED_CONTEXT_KEYS.contains(¶m_name) { + checker.diagnostics.push(Diagnostic::new( + Airflow3Removal { + deprecated: param_name.to_string(), + replacement: Replacement::None, + }, + param.parameter.name.range(), + )); + } + } +} + +/// Returns `true` if the given function is decorated with `@airflow.decorators.task`. +fn is_airflow_task(function_def: &StmtFunctionDef, semantic: &SemanticModel) -> bool { + function_def.decorator_list.iter().any(|decorator| { + semantic + .resolve_qualified_name(map_callable(&decorator.expression)) + .is_some_and(|qualified_name| { + matches!(qualified_name.segments(), ["airflow", "decorators", "task"]) + }) + }) +} diff --git a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_context.py.snap b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_context.py.snap new file mode 100644 index 0000000000000..2ada0b8fdd55f --- /dev/null +++ b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_context.py.snap @@ -0,0 +1,514 @@ +--- +source: crates/ruff_linter/src/rules/airflow/mod.rs +snapshot_kind: text +--- +AIR302_context.py:15:41: AIR302 `conf` is removed in Airflow 3.0 + | +14 | def access_invalid_key_in_context(**context): +15 | print("access invalid key", context["conf"]) + | ^^^^^^ AIR302 +16 | +17 | @task + | + +AIR302_context.py:19:45: AIR302 `conf` is removed in Airflow 3.0 + | +17 | @task +18 | def access_invalid_key_task_out_of_dag(**context): +19 | print("access invalid key", context.get("conf")) + | ^^^^^^ AIR302 +20 | +21 | @dag( + | + +AIR302_context.py:19:45: AIR302 `conf` is removed in Airflow 3.0 + | +17 | @task +18 | def access_invalid_key_task_out_of_dag(**context): +19 | print("access invalid key", context.get("conf")) + | ^^^^^^ AIR302 +20 | +21 | @dag( + | + +AIR302_context.py:30:49: AIR302 `conf` is removed in Airflow 3.0 + | +28 | @task() +29 | def access_invalid_key_task(**context): +30 | print("access invalid key", context.get("conf")) + | ^^^^^^ AIR302 +31 | +32 | task1 = PythonOperator( + | + +AIR302_context.py:30:49: AIR302 `conf` is removed in Airflow 3.0 + | +28 | @task() +29 | def access_invalid_key_task(**context): +30 | print("access invalid key", context.get("conf")) + | ^^^^^^ AIR302 +31 | +32 | task1 = PythonOperator( + | + +AIR302_context.py:47:30: AIR302 `execution_date` is removed in Airflow 3.0 + | +46 | # Removed usage - should trigger violations +47 | execution_date = context["execution_date"] + | ^^^^^^^^^^^^^^^^ AIR302 +48 | next_ds = context["next_ds"] +49 | next_ds_nodash = context["next_ds_nodash"] + | + +AIR302_context.py:48:23: AIR302 `next_ds` is removed in Airflow 3.0 + | +46 | # Removed usage - should trigger violations +47 | execution_date = context["execution_date"] +48 | next_ds = context["next_ds"] + | ^^^^^^^^^ AIR302 +49 | next_ds_nodash = context["next_ds_nodash"] +50 | next_execution_date = context["next_execution_date"] + | + +AIR302_context.py:49:30: AIR302 `next_ds_nodash` is removed in Airflow 3.0 + | +47 | execution_date = context["execution_date"] +48 | next_ds = context["next_ds"] +49 | next_ds_nodash = context["next_ds_nodash"] + | ^^^^^^^^^^^^^^^^ AIR302 +50 | next_execution_date = context["next_execution_date"] +51 | prev_ds = context["prev_ds"] + | + +AIR302_context.py:50:35: AIR302 `next_execution_date` is removed in Airflow 3.0 + | +48 | next_ds = context["next_ds"] +49 | next_ds_nodash = context["next_ds_nodash"] +50 | next_execution_date = context["next_execution_date"] + | ^^^^^^^^^^^^^^^^^^^^^ AIR302 +51 | prev_ds = context["prev_ds"] +52 | prev_ds_nodash = context["prev_ds_nodash"] + | + +AIR302_context.py:51:23: AIR302 `prev_ds` is removed in Airflow 3.0 + | +49 | next_ds_nodash = context["next_ds_nodash"] +50 | next_execution_date = context["next_execution_date"] +51 | prev_ds = context["prev_ds"] + | ^^^^^^^^^ AIR302 +52 | prev_ds_nodash = context["prev_ds_nodash"] +53 | prev_execution_date = context["prev_execution_date"] + | + +AIR302_context.py:52:30: AIR302 `prev_ds_nodash` is removed in Airflow 3.0 + | +50 | next_execution_date = context["next_execution_date"] +51 | prev_ds = context["prev_ds"] +52 | prev_ds_nodash = context["prev_ds_nodash"] + | ^^^^^^^^^^^^^^^^ AIR302 +53 | prev_execution_date = context["prev_execution_date"] +54 | prev_execution_date_success = context["prev_execution_date_success"] + | + +AIR302_context.py:53:35: AIR302 `prev_execution_date` is removed in Airflow 3.0 + | +51 | prev_ds = context["prev_ds"] +52 | prev_ds_nodash = context["prev_ds_nodash"] +53 | prev_execution_date = context["prev_execution_date"] + | ^^^^^^^^^^^^^^^^^^^^^ AIR302 +54 | prev_execution_date_success = context["prev_execution_date_success"] +55 | tomorrow_ds = context["tomorrow_ds"] + | + +AIR302_context.py:54:43: AIR302 `prev_execution_date_success` is removed in Airflow 3.0 + | +52 | prev_ds_nodash = context["prev_ds_nodash"] +53 | prev_execution_date = context["prev_execution_date"] +54 | prev_execution_date_success = context["prev_execution_date_success"] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AIR302 +55 | tomorrow_ds = context["tomorrow_ds"] +56 | yesterday_ds = context["yesterday_ds"] + | + +AIR302_context.py:55:27: AIR302 `tomorrow_ds` is removed in Airflow 3.0 + | +53 | prev_execution_date = context["prev_execution_date"] +54 | prev_execution_date_success = context["prev_execution_date_success"] +55 | tomorrow_ds = context["tomorrow_ds"] + | ^^^^^^^^^^^^^ AIR302 +56 | yesterday_ds = context["yesterday_ds"] +57 | yesterday_ds_nodash = context["yesterday_ds_nodash"] + | + +AIR302_context.py:56:28: AIR302 `yesterday_ds` is removed in Airflow 3.0 + | +54 | prev_execution_date_success = context["prev_execution_date_success"] +55 | tomorrow_ds = context["tomorrow_ds"] +56 | yesterday_ds = context["yesterday_ds"] + | ^^^^^^^^^^^^^^ AIR302 +57 | yesterday_ds_nodash = context["yesterday_ds_nodash"] + | + +AIR302_context.py:57:35: AIR302 `yesterday_ds_nodash` is removed in Airflow 3.0 + | +55 | tomorrow_ds = context["tomorrow_ds"] +56 | yesterday_ds = context["yesterday_ds"] +57 | yesterday_ds_nodash = context["yesterday_ds_nodash"] + | ^^^^^^^^^^^^^^^^^^^^^ AIR302 +58 | +59 | with DAG( + | + +AIR302_context.py:61:5: AIR302 [*] `schedule_interval` is removed in Airflow 3.0 + | +59 | with DAG( +60 | dag_id="example_dag", +61 | schedule_interval="@daily", + | ^^^^^^^^^^^^^^^^^ AIR302 +62 | start_date=datetime(2023, 1, 1), +63 | template_searchpath=["/templates"], + | + = help: Use `schedule` instead + +ℹ Safe fix +58 58 | +59 59 | with DAG( +60 60 | dag_id="example_dag", +61 |- schedule_interval="@daily", + 61 |+ schedule="@daily", +62 62 | start_date=datetime(2023, 1, 1), +63 63 | template_searchpath=["/templates"], +64 64 | ) as dag: + +AIR302_context.py:65:13: AIR302 `airflow.operators.dummy.DummyOperator` is removed in Airflow 3.0 + | +63 | template_searchpath=["/templates"], +64 | ) as dag: +65 | task1 = DummyOperator( + | ^^^^^^^^^^^^^ AIR302 +66 | task_id="task1", +67 | params={ + | + = help: Use `airflow.operators.empty.EmptyOperator` instead + +AIR302_context.py:78:57: AIR302 `execution_date` is removed in Airflow 3.0 + | +76 | name = "custom_macros" +77 | macros = { +78 | "execution_date_macro": lambda context: context["execution_date"], + | ^^^^^^^^^^^^^^^^ AIR302 +79 | "next_ds_macro": lambda context: context["next_ds"] +80 | } + | + +AIR302_context.py:79:50: AIR302 `next_ds` is removed in Airflow 3.0 + | +77 | macros = { +78 | "execution_date_macro": lambda context: context["execution_date"], +79 | "next_ds_macro": lambda context: context["next_ds"] + | ^^^^^^^^^ AIR302 +80 | } + | + +AIR302_context.py:85:30: AIR302 `execution_date` is removed in Airflow 3.0 + | +83 | def print_config(): +84 | context = get_current_context() +85 | execution_date = context["execution_date"] + | ^^^^^^^^^^^^^^^^ AIR302 +86 | next_ds = context["next_ds"] +87 | next_ds_nodash = context["next_ds_nodash"] + | + +AIR302_context.py:86:23: AIR302 `next_ds` is removed in Airflow 3.0 + | +84 | context = get_current_context() +85 | execution_date = context["execution_date"] +86 | next_ds = context["next_ds"] + | ^^^^^^^^^ AIR302 +87 | next_ds_nodash = context["next_ds_nodash"] +88 | next_execution_date = context["next_execution_date"] + | + +AIR302_context.py:87:30: AIR302 `next_ds_nodash` is removed in Airflow 3.0 + | +85 | execution_date = context["execution_date"] +86 | next_ds = context["next_ds"] +87 | next_ds_nodash = context["next_ds_nodash"] + | ^^^^^^^^^^^^^^^^ AIR302 +88 | next_execution_date = context["next_execution_date"] +89 | prev_ds = context["prev_ds"] + | + +AIR302_context.py:88:35: AIR302 `next_execution_date` is removed in Airflow 3.0 + | +86 | next_ds = context["next_ds"] +87 | next_ds_nodash = context["next_ds_nodash"] +88 | next_execution_date = context["next_execution_date"] + | ^^^^^^^^^^^^^^^^^^^^^ AIR302 +89 | prev_ds = context["prev_ds"] +90 | prev_ds_nodash = context["prev_ds_nodash"] + | + +AIR302_context.py:89:23: AIR302 `prev_ds` is removed in Airflow 3.0 + | +87 | next_ds_nodash = context["next_ds_nodash"] +88 | next_execution_date = context["next_execution_date"] +89 | prev_ds = context["prev_ds"] + | ^^^^^^^^^ AIR302 +90 | prev_ds_nodash = context["prev_ds_nodash"] +91 | prev_execution_date = context["prev_execution_date"] + | + +AIR302_context.py:90:30: AIR302 `prev_ds_nodash` is removed in Airflow 3.0 + | +88 | next_execution_date = context["next_execution_date"] +89 | prev_ds = context["prev_ds"] +90 | prev_ds_nodash = context["prev_ds_nodash"] + | ^^^^^^^^^^^^^^^^ AIR302 +91 | prev_execution_date = context["prev_execution_date"] +92 | prev_execution_date_success = context["prev_execution_date_success"] + | + +AIR302_context.py:91:35: AIR302 `prev_execution_date` is removed in Airflow 3.0 + | +89 | prev_ds = context["prev_ds"] +90 | prev_ds_nodash = context["prev_ds_nodash"] +91 | prev_execution_date = context["prev_execution_date"] + | ^^^^^^^^^^^^^^^^^^^^^ AIR302 +92 | prev_execution_date_success = context["prev_execution_date_success"] +93 | tomorrow_ds = context["tomorrow_ds"] + | + +AIR302_context.py:92:43: AIR302 `prev_execution_date_success` is removed in Airflow 3.0 + | +90 | prev_ds_nodash = context["prev_ds_nodash"] +91 | prev_execution_date = context["prev_execution_date"] +92 | prev_execution_date_success = context["prev_execution_date_success"] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AIR302 +93 | tomorrow_ds = context["tomorrow_ds"] +94 | yesterday_ds = context["yesterday_ds"] + | + +AIR302_context.py:93:27: AIR302 `tomorrow_ds` is removed in Airflow 3.0 + | +91 | prev_execution_date = context["prev_execution_date"] +92 | prev_execution_date_success = context["prev_execution_date_success"] +93 | tomorrow_ds = context["tomorrow_ds"] + | ^^^^^^^^^^^^^ AIR302 +94 | yesterday_ds = context["yesterday_ds"] +95 | yesterday_ds_nodash = context["yesterday_ds_nodash"] + | + +AIR302_context.py:94:28: AIR302 `yesterday_ds` is removed in Airflow 3.0 + | +92 | prev_execution_date_success = context["prev_execution_date_success"] +93 | tomorrow_ds = context["tomorrow_ds"] +94 | yesterday_ds = context["yesterday_ds"] + | ^^^^^^^^^^^^^^ AIR302 +95 | yesterday_ds_nodash = context["yesterday_ds_nodash"] + | + +AIR302_context.py:95:35: AIR302 `yesterday_ds_nodash` is removed in Airflow 3.0 + | +93 | tomorrow_ds = context["tomorrow_ds"] +94 | yesterday_ds = context["yesterday_ds"] +95 | yesterday_ds_nodash = context["yesterday_ds_nodash"] + | ^^^^^^^^^^^^^^^^^^^^^ AIR302 +96 | +97 | class CustomOperator(BaseOperator): + | + +AIR302_context.py:99:34: AIR302 `execution_date` is removed in Airflow 3.0 + | + 97 | class CustomOperator(BaseOperator): + 98 | def execute(self, context): + 99 | execution_date = context["execution_date"] + | ^^^^^^^^^^^^^^^^ AIR302 +100 | next_ds = context["next_ds"] +101 | next_ds_nodash = context["next_ds_nodash"] + | + +AIR302_context.py:100:27: AIR302 `next_ds` is removed in Airflow 3.0 + | + 98 | def execute(self, context): + 99 | execution_date = context["execution_date"] +100 | next_ds = context["next_ds"] + | ^^^^^^^^^ AIR302 +101 | next_ds_nodash = context["next_ds_nodash"] +102 | next_execution_date = context["next_execution_date"] + | + +AIR302_context.py:101:34: AIR302 `next_ds_nodash` is removed in Airflow 3.0 + | + 99 | execution_date = context["execution_date"] +100 | next_ds = context["next_ds"] +101 | next_ds_nodash = context["next_ds_nodash"] + | ^^^^^^^^^^^^^^^^ AIR302 +102 | next_execution_date = context["next_execution_date"] +103 | prev_ds = context["prev_ds"] + | + +AIR302_context.py:102:39: AIR302 `next_execution_date` is removed in Airflow 3.0 + | +100 | next_ds = context["next_ds"] +101 | next_ds_nodash = context["next_ds_nodash"] +102 | next_execution_date = context["next_execution_date"] + | ^^^^^^^^^^^^^^^^^^^^^ AIR302 +103 | prev_ds = context["prev_ds"] +104 | prev_ds_nodash = context["prev_ds_nodash"] + | + +AIR302_context.py:103:27: AIR302 `prev_ds` is removed in Airflow 3.0 + | +101 | next_ds_nodash = context["next_ds_nodash"] +102 | next_execution_date = context["next_execution_date"] +103 | prev_ds = context["prev_ds"] + | ^^^^^^^^^ AIR302 +104 | prev_ds_nodash = context["prev_ds_nodash"] +105 | prev_execution_date = context["prev_execution_date"] + | + +AIR302_context.py:104:34: AIR302 `prev_ds_nodash` is removed in Airflow 3.0 + | +102 | next_execution_date = context["next_execution_date"] +103 | prev_ds = context["prev_ds"] +104 | prev_ds_nodash = context["prev_ds_nodash"] + | ^^^^^^^^^^^^^^^^ AIR302 +105 | prev_execution_date = context["prev_execution_date"] +106 | prev_execution_date_success = context["prev_execution_date_success"] + | + +AIR302_context.py:105:39: AIR302 `prev_execution_date` is removed in Airflow 3.0 + | +103 | prev_ds = context["prev_ds"] +104 | prev_ds_nodash = context["prev_ds_nodash"] +105 | prev_execution_date = context["prev_execution_date"] + | ^^^^^^^^^^^^^^^^^^^^^ AIR302 +106 | prev_execution_date_success = context["prev_execution_date_success"] +107 | tomorrow_ds = context["tomorrow_ds"] + | + +AIR302_context.py:106:47: AIR302 `prev_execution_date_success` is removed in Airflow 3.0 + | +104 | prev_ds_nodash = context["prev_ds_nodash"] +105 | prev_execution_date = context["prev_execution_date"] +106 | prev_execution_date_success = context["prev_execution_date_success"] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AIR302 +107 | tomorrow_ds = context["tomorrow_ds"] +108 | yesterday_ds = context["yesterday_ds"] + | + +AIR302_context.py:107:31: AIR302 `tomorrow_ds` is removed in Airflow 3.0 + | +105 | prev_execution_date = context["prev_execution_date"] +106 | prev_execution_date_success = context["prev_execution_date_success"] +107 | tomorrow_ds = context["tomorrow_ds"] + | ^^^^^^^^^^^^^ AIR302 +108 | yesterday_ds = context["yesterday_ds"] +109 | yesterday_ds_nodash = context["yesterday_ds_nodash"] + | + +AIR302_context.py:108:32: AIR302 `yesterday_ds` is removed in Airflow 3.0 + | +106 | prev_execution_date_success = context["prev_execution_date_success"] +107 | tomorrow_ds = context["tomorrow_ds"] +108 | yesterday_ds = context["yesterday_ds"] + | ^^^^^^^^^^^^^^ AIR302 +109 | yesterday_ds_nodash = context["yesterday_ds_nodash"] + | + +AIR302_context.py:109:39: AIR302 `yesterday_ds_nodash` is removed in Airflow 3.0 + | +107 | tomorrow_ds = context["tomorrow_ds"] +108 | yesterday_ds = context["yesterday_ds"] +109 | yesterday_ds_nodash = context["yesterday_ds_nodash"] + | ^^^^^^^^^^^^^^^^^^^^^ AIR302 +110 | +111 | @task + | + +AIR302_context.py:112:45: AIR302 `execution_date` is removed in Airflow 3.0 + | +111 | @task +112 | def access_invalid_argument_task_out_of_dag(execution_date, tomorrow_ds, logical_date, **context): + | ^^^^^^^^^^^^^^ AIR302 +113 | print("execution date", execution_date) +114 | print("access invalid key", context.get("conf")) + | + +AIR302_context.py:112:61: AIR302 `tomorrow_ds` is removed in Airflow 3.0 + | +111 | @task +112 | def access_invalid_argument_task_out_of_dag(execution_date, tomorrow_ds, logical_date, **context): + | ^^^^^^^^^^^ AIR302 +113 | print("execution date", execution_date) +114 | print("access invalid key", context.get("conf")) + | + +AIR302_context.py:114:45: AIR302 `conf` is removed in Airflow 3.0 + | +112 | def access_invalid_argument_task_out_of_dag(execution_date, tomorrow_ds, logical_date, **context): +113 | print("execution date", execution_date) +114 | print("access invalid key", context.get("conf")) + | ^^^^^^ AIR302 +115 | +116 | @task(task_id="print_the_context") + | + +AIR302_context.py:114:45: AIR302 `conf` is removed in Airflow 3.0 + | +112 | def access_invalid_argument_task_out_of_dag(execution_date, tomorrow_ds, logical_date, **context): +113 | print("execution date", execution_date) +114 | print("access invalid key", context.get("conf")) + | ^^^^^^ AIR302 +115 | +116 | @task(task_id="print_the_context") + | + +AIR302_context.py:120:22: AIR302 `tomorrow_ds` is removed in Airflow 3.0 + | +118 | """Print the Airflow context and ds variable from the context.""" +119 | print(ds) +120 | print(kwargs.get("tomorrow_ds")) + | ^^^^^^^^^^^^^ AIR302 +121 | c = get_current_context() +122 | c.get("execution_date") + | + +AIR302_context.py:122:11: AIR302 `execution_date` is removed in Airflow 3.0 + | +120 | print(kwargs.get("tomorrow_ds")) +121 | c = get_current_context() +122 | c.get("execution_date") + | ^^^^^^^^^^^^^^^^ AIR302 +123 | +124 | class CustomOperatorNew(BaseOperator): + | + +AIR302_context.py:122:11: AIR302 `execution_date` is removed in Airflow 3.0 + | +120 | print(kwargs.get("tomorrow_ds")) +121 | c = get_current_context() +122 | c.get("execution_date") + | ^^^^^^^^^^^^^^^^ AIR302 +123 | +124 | class CustomOperatorNew(BaseOperator): + | + +AIR302_context.py:126:38: AIR302 `execution_date` is removed in Airflow 3.0 + | +124 | class CustomOperatorNew(BaseOperator): +125 | def execute(self, context): +126 | execution_date = context.get("execution_date") + | ^^^^^^^^^^^^^^^^ AIR302 +127 | next_ds = context.get("next_ds") + | + +AIR302_context.py:127:31: AIR302 `next_ds` is removed in Airflow 3.0 + | +125 | def execute(self, context): +126 | execution_date = context.get("execution_date") +127 | next_ds = context.get("next_ds") + | ^^^^^^^^^ AIR302 + |