Skip to content

Comments

[airflow] Add ruff rules to catch deprecated Airflow imports for Airflow 3.1 (AIR321)#22376

Merged
ntBre merged 18 commits intoastral-sh:mainfrom
sjyangkevin:catch-deprecated-imports-airflow-3_1
Feb 4, 2026
Merged

[airflow] Add ruff rules to catch deprecated Airflow imports for Airflow 3.1 (AIR321)#22376
ntBre merged 18 commits intoastral-sh:mainfrom
sjyangkevin:catch-deprecated-imports-airflow-3_1

Conversation

@sjyangkevin
Copy link
Contributor

Summary

This PR is related to the discussion: apache/airflow#54714

This change creates a new code (AIR321) and implement ruff rules to catch, and/or fix deprecated imports in Airflow for Airflow 3.1. The rules are implemented by following the structure of AIR301. The rules check whether a removed Airflow name is used, and match on Expr::Name and Expr::Attribute.

Test Plan

The following two test files are added:

  1. crates/ruff_linter/resources/test/fixtures/airflow/AIR321_names.py
  2. crates/ruff_linter/resources/test/fixtures/airflow/AIR321_names_fix.py

AIR321_names.py
All the test cases in this file should raise violations and fixes should be suggested when running the test with --unsafe-fixes. The test results shown in the snapshot are expected.

cargo run -p ruff -- check crates/ruff_linter/resources/test/fixtures/airflow/AIR321_names.py --no-cache --preview --select AIR321 --unsafe-fixes

AIR321_names_fix.py
All the test cases in this file raise NO violation (i.e., all checks should pass). The snapshot file is empty.

Screenshot from 2026-01-04 16-37-51

Document Update

Screenshot from 2026-01-04 16-37-02

@astral-sh-bot
Copy link

astral-sh-bot bot commented Jan 4, 2026

ruff-ecosystem results

Linter (stable)

✅ ecosystem check detected no linter changes.

Linter (preview)

ℹ️ ecosystem check detected linter changes. (+462 -0 violations, +0 -0 fixes in 1 projects; 54 projects unchanged)

apache/airflow (+462 -0 violations, +0 -0 fixes)

ruff check --no-cache --exit-zero --no-fix --output-format concise --preview --select ALL

+ airflow-core/tests/unit/ti_deps/deps/test_task_concurrency.py:35:16: AIR321 `airflow.models.baseoperator.BaseOperator` is moved in Airflow 3.1
+ dev/airflow_perf/scheduler_dag_execution_timing.py:181:24: AIR321 [*] `airflow.utils.timezone.utcnow` is moved in Airflow 3.1
+ kubernetes-tests/tests/kubernetes_tests/test_kubernetes_pod_operator.py:59:20: AIR321 [*] `airflow.utils.timezone.datetime` is moved in Airflow 3.1
+ performance/src/performance_dags/performance_dag/performance_dag_utils.py:83:27: AIR321 [*] `airflow.utils.timezone.utcnow` is moved in Airflow 3.1
+ providers/alibaba/tests/unit/alibaba/cloud/log/test_oss_task_handler.py:59:26: AIR321 `airflow.utils.timezone.datetime` is moved in Airflow 3.1
+ providers/alibaba/tests/unit/alibaba/cloud/sensors/test_analyticdb_spark.py:26:16: AIR321 [*] `airflow.utils.timezone.datetime` is moved in Airflow 3.1
+ providers/amazon/src/airflow/providers/amazon/aws/executors/utils/exponential_backoff_retry.py:72:20: AIR321 [*] `airflow.utils.timezone.utcnow` is moved in Airflow 3.1
+ providers/amazon/tests/system/amazon/aws/example_mongo_to_s3.py:45:16: AIR321 `airflow.utils.timezone.datetime` is moved in Airflow 3.1
+ providers/amazon/tests/system/amazon/aws/example_mwaa.py:121:35: AIR321 [*] `airflow.utils.timezone.utc` is moved in Airflow 3.1
+ providers/amazon/tests/system/amazon/aws/example_mwaa.py:151:35: AIR321 [*] `airflow.utils.timezone.utc` is moved in Airflow 3.1
+ providers/amazon/tests/unit/amazon/aws/executors/ecs/test_ecs_executor.py:881:77: AIR321 `airflow.utils.timezone.utcnow` is moved in Airflow 3.1
+ providers/amazon/tests/unit/amazon/aws/hooks/test_s3.py:325:25: AIR321 `airflow.utils.timezone.datetime` is moved in Airflow 3.1
+ providers/amazon/tests/unit/amazon/aws/hooks/test_s3.py:326:23: AIR321 `airflow.utils.timezone.datetime` is moved in Airflow 3.1
+ providers/amazon/tests/unit/amazon/aws/hooks/test_s3.py:585:74: AIR321 `airflow.utils.timezone.datetime` is moved in Airflow 3.1
+ providers/amazon/tests/unit/amazon/aws/hooks/test_s3.py:586:75: AIR321 `airflow.utils.timezone.datetime` is moved in Airflow 3.1
+ providers/amazon/tests/unit/amazon/aws/hooks/test_s3.py:601:66: AIR321 `airflow.utils.timezone.datetime` is moved in Airflow 3.1
+ providers/amazon/tests/unit/amazon/aws/hooks/test_s3.py:602:67: AIR321 `airflow.utils.timezone.datetime` is moved in Airflow 3.1
+ providers/amazon/tests/unit/amazon/aws/hooks/test_s3.py:694:74: AIR321 `airflow.utils.timezone.datetime` is moved in Airflow 3.1
+ providers/amazon/tests/unit/amazon/aws/hooks/test_s3.py:695:75: AIR321 `airflow.utils.timezone.datetime` is moved in Airflow 3.1
+ providers/amazon/tests/unit/amazon/aws/hooks/test_s3.py:725:74: AIR321 `airflow.utils.timezone.datetime` is moved in Airflow 3.1
+ providers/amazon/tests/unit/amazon/aws/hooks/test_s3.py:726:75: AIR321 `airflow.utils.timezone.datetime` is moved in Airflow 3.1
+ providers/amazon/tests/unit/amazon/aws/hooks/test_s3.py:762:41: AIR321 `airflow.utils.timezone.datetime` is moved in Airflow 3.1
+ providers/amazon/tests/unit/amazon/aws/hooks/test_s3.py:800:74: AIR321 `airflow.utils.timezone.datetime` is moved in Airflow 3.1
+ providers/amazon/tests/unit/amazon/aws/hooks/test_s3.py:801:75: AIR321 `airflow.utils.timezone.datetime` is moved in Airflow 3.1
+ providers/amazon/tests/unit/amazon/aws/hooks/test_s3.py:928:41: AIR321 `airflow.utils.timezone.datetime` is moved in Airflow 3.1
+ providers/amazon/tests/unit/amazon/aws/hooks/test_s3.py:934:41: AIR321 `airflow.utils.timezone.datetime` is moved in Airflow 3.1
+ providers/amazon/tests/unit/amazon/aws/hooks/test_s3.py:945:41: AIR321 `airflow.utils.timezone.datetime` is moved in Airflow 3.1
+ providers/amazon/tests/unit/amazon/aws/hooks/test_s3.py:951:41: AIR321 `airflow.utils.timezone.datetime` is moved in Airflow 3.1
+ providers/amazon/tests/unit/amazon/aws/hooks/test_s3.py:995:41: AIR321 `airflow.utils.timezone.datetime` is moved in Airflow 3.1
+ providers/amazon/tests/unit/amazon/aws/log/test_cloudwatch_task_handler.py:141:26: AIR321 `airflow.utils.timezone.datetime` is moved in Airflow 3.1
+ providers/amazon/tests/unit/amazon/aws/log/test_cloudwatch_task_handler.py:195:16: AIR321 `airflow.utils.timezone.datetime` is moved in Airflow 3.1
+ providers/amazon/tests/unit/amazon/aws/log/test_cloudwatch_task_handler.py:261:26: AIR321 `airflow.utils.timezone.datetime` is moved in Airflow 3.1
+ providers/amazon/tests/unit/amazon/aws/log/test_cloudwatch_task_handler.py:334:17: AIR321 `airflow.utils.timezone.datetime` is moved in Airflow 3.1
+ providers/amazon/tests/unit/amazon/aws/log/test_cloudwatch_task_handler.py:335:42: AIR321 `airflow.utils.timezone.datetime` is moved in Airflow 3.1
+ providers/amazon/tests/unit/amazon/aws/log/test_cloudwatch_task_handler.py:389:33: AIR321 `airflow.utils.timezone.datetime` is moved in Airflow 3.1
+ providers/amazon/tests/unit/amazon/aws/operators/test_s3.py:651:24: AIR321 `airflow.utils.timezone.utcnow` is moved in Airflow 3.1
+ providers/amazon/tests/unit/amazon/aws/operators/test_s3.py:652:42: AIR321 `airflow.utils.timezone.datetime` is moved in Airflow 3.1
+ providers/amazon/tests/unit/amazon/aws/operators/test_s3.py:676:27: AIR321 `airflow.utils.timezone.utcnow` is moved in Airflow 3.1
+ providers/amazon/tests/unit/amazon/aws/operators/test_s3.py:707:15: AIR321 `airflow.utils.timezone.utcnow` is moved in Airflow 3.1
+ providers/amazon/tests/unit/amazon/aws/operators/test_s3.py:796:17: AIR321 `airflow.utils.timezone.datetime` is moved in Airflow 3.1
+ providers/amazon/tests/unit/amazon/aws/operators/test_s3.py:803:17: AIR321 `airflow.utils.timezone.datetime` is moved in Airflow 3.1
+ providers/amazon/tests/unit/amazon/aws/operators/test_s3.py:804:17: AIR321 `airflow.utils.timezone.datetime` is moved in Airflow 3.1
+ providers/amazon/tests/unit/amazon/aws/operators/test_s3.py:832:17: AIR321 `airflow.utils.timezone.datetime` is moved in Airflow 3.1
+ providers/amazon/tests/unit/amazon/aws/operators/test_s3.py:839:17: AIR321 `airflow.utils.timezone.datetime` is moved in Airflow 3.1
+ providers/amazon/tests/unit/amazon/aws/operators/test_s3.py:840:17: AIR321 `airflow.utils.timezone.datetime` is moved in Airflow 3.1
+ providers/amazon/tests/unit/amazon/aws/transfers/test_s3_to_sftp.py:47:16: AIR321 `airflow.utils.timezone.datetime` is moved in Airflow 3.1
+ providers/amazon/tests/unit/amazon/aws/transfers/test_sftp_to_s3.py:45:16: AIR321 `airflow.utils.timezone.datetime` is moved in Airflow 3.1
+ providers/apache/flink/tests/unit/apache/flink/operators/test_flink_kubernetes.py:200:51: AIR321 [*] `airflow.utils.timezone.datetime` is moved in Airflow 3.1
+ providers/apache/flink/tests/unit/apache/flink/sensors/test_flink_kubernetes.py:886:51: AIR321 [*] `airflow.utils.timezone.datetime` is moved in Airflow 3.1
+ providers/apache/hdfs/tests/unit/apache/hdfs/log/test_hdfs_task_handler.py:37:16: AIR321 `airflow.utils.timezone.datetime` is moved in Airflow 3.1
... 412 additional changes omitted for project

Changes by rule (1 rules affected)

code total + violation - violation + fix - fix
AIR321 462 462 0 0 0

@MichaReiser
Copy link
Member

@Lee-W could you take a look at the PR if it catches the semantics you want.

@MichaReiser MichaReiser added the rule Implementing or modifying a lint rule label Jan 5, 2026
@Lee-W
Copy link
Contributor

Lee-W commented Jan 5, 2026

@Lee-W could you take a look at the PR if it catches the semantics you want.

At a high level, yes, but some details will need some polish. Will let you know when it's at least ok from my side. Thanks!

@sjyangkevin sjyangkevin force-pushed the catch-deprecated-imports-airflow-3_1 branch from 5f3ef77 to 1d8cbe3 Compare January 6, 2026 03:52
@sjyangkevin sjyangkevin requested a review from Lee-W January 6, 2026 04:05
@Lee-W
Copy link
Contributor

Lee-W commented Jan 6, 2026

The logic looks solid, but we’ll need to revisit the list. @sjyangkevin, could you please make this a draft? We can discuss the list further in relation to the Airflow issue. Thanks!

@sjyangkevin sjyangkevin marked this pull request as draft January 6, 2026 13:16
@sjyangkevin
Copy link
Contributor Author

@Lee-W and @amoghrajesh , thanks for the feedback! I have converted it into a draft. I will also happy to work on the fix after the further discussion.

@sjyangkevin sjyangkevin force-pushed the catch-deprecated-imports-airflow-3_1 branch from 1d8cbe3 to 0a88188 Compare January 7, 2026 04:02
@sjyangkevin sjyangkevin force-pushed the catch-deprecated-imports-airflow-3_1 branch from 6e876dc to e786ba2 Compare January 14, 2026 04:11
Copy link
Contributor Author

@sjyangkevin sjyangkevin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @Lee-W and @amoghrajesh ,

I've refined the rules based on the feedback. I not sure if we can make a rule a "warning" in ruff; currently, I introduce a new Replacement SourceModuleMovedToSDK which we can use to embed a warning message. Let me know if it is the right approach.

Below is the summary of changes.

  1. Exclude the followings

airflow.utils.task_group.get_task_group_children_getter
airflow.utils.task_group.task_group_to_dict

  1. Fix Import Path in AIR311

airflow.sensors.base.poke_mode_only → airflow.sdk.bases.sensor.poke_mode_only

  1. Move from AIR301 to AIR321

airflow.secrets.cache.SecretCache → from airflow.sdk import SecretCache

  1. Keep _internal modules and show a warning message to indicate these are internal API that may change without notice.
  2. Update the other rules to adapt for the new Replacement SourceModuleMovedToSDK

Thanks!!

@Lee-W
Copy link
Contributor

Lee-W commented Jan 14, 2026

I not sure if we can make a rule a "warning" in ruff

A Diagnotic without fix is basically a warning

@sjyangkevin
Copy link
Contributor Author

sjyangkevin commented Jan 14, 2026

I not sure if we can make a rule a "warning" in ruff

A Diagnotic without fix is basically a warning

Thanks! The current implementation proposes a fix and include a warning message. I can refactor this to use a simple message to notify the module move, and include a message to tell these APIs are subject to change (don't encourage using these). So, we don't suggest fix for it. Which one do you think could be a better idea?

Current Implementation suggest fixes and show warning message.
Screenshot from 2026-01-14 11-17-28

@sjyangkevin sjyangkevin force-pushed the catch-deprecated-imports-airflow-3_1 branch from e786ba2 to 75e00c6 Compare January 14, 2026 18:00
Copy link
Contributor Author

@sjyangkevin sjyangkevin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @Lee-W and @amoghrajesh , I made a few adjustments to the PR and I think it is almost ready for open it again for review. Let me know if you have any feedback before opening it again. Below is the summary of changes made.

  1. Move SecretCache from AIR301 to AIR321 (tested in Airflow 3.0.6 and the original import is still valid)
  2. Move the test case for SecretCache from AIR301 to AIR321
  3. For _internal modules, the rule is updated to use Message to show a warning. Hence, the warning_message field is removed from the SourceModuleMovedToSDK. This field is only used for the internal module cases, but will be None for majority.
  4. Update the fix_title for SourceModuleMovedToSDK to "{name} has been moved to {module} since Airflow 3.x (with apache-airflow-task-sdk>={version}).", where version is 1.0.6 for rules in AIR301, and 1.1.6 for rules in AIR321.
  5. For AIR321, update preview_since to 0.14.12
  6. Test snapshots are all updated.
  7. Comments in apache/airflow#54714 (comment) are resolved.

Thanks!

Screenshot from 2026-01-15 10-22-16 Screenshot from 2026-01-15 10-21-47

@sjyangkevin sjyangkevin force-pushed the catch-deprecated-imports-airflow-3_1 branch from 8791001 to 6ef68e7 Compare January 16, 2026 20:42
Comment on lines 43 to 48
// Symbols moved to internal module in Airflow 3. Used when we want to raise a warning.
// e.g., `airflow.utils.setup_teardown.BaseSetupTeardownContext` to `airflow.sdk.definitions._internal.setup_teardown.BaseSetupTeardownContext`
InternalModule {
module: &'static str,
name: String,
},
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add a new item in the enum which used to show warning message for internal module

@sjyangkevin sjyangkevin requested a review from Lee-W January 16, 2026 20:48
@sjyangkevin sjyangkevin force-pushed the catch-deprecated-imports-airflow-3_1 branch from 6ef68e7 to 332b175 Compare January 17, 2026 17:45
@sjyangkevin sjyangkevin force-pushed the catch-deprecated-imports-airflow-3_1 branch from 332b175 to 3e093db Compare January 19, 2026 04:07
name,
suggest_fix,
..
} if *suggest_fix => (module, name.as_str()),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here, we leveraged the suggest_fix parameter from SourceModuleMovedWithMessage to handle when we would like to show a warning and when we would like to suggest a fix with custom message.

Comment on lines +43 to +49
// Symbols updated in Airflow 3 with only module changed. Used when we want to include custom message and optionally report diagnostics.
SourceModuleMovedWithMessage {
module: &'static str,
name: String,
message: &'static str,
suggest_fix: bool,
},
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Create this new enum item which can be used as follow:

  1. The fix_title format is: "{name} has been moved to {module} since Airflow 3.1. {message}"; we can use the message parameter to include a static string as additional message that we want to show to users.
  2. The suggest_fix parameter is used to optionally report diagnostics. In some cases, we might want to just show a warning (e.g., internal module usage), but in some case, we might want to report diagnostic and suggest fix (e.g., BaseHook is moved from one module to another)

Why message is a static string. Use the following example to explain. Making message a String allow us to use format! to templating the string and render it at runtime. However, only rest or variables defined in the accessible scope can be used to render the string. As module and name will be handled in fix_title. It might not be necessary to use those values to render the message. So, here use static string.

[
            "airflow",
            "utils",
            "setup_teardown",
            rest @ ("BaseSetupTeardownContext" | "SetupTeardownContext"),
        ] => Replacement::SourceModuleMovedWithMessage {
            module: "airflow.sdk.definitions._internal.setup_teardown",
            name: rest.to_string(),
            message: "This is an internal module which is not suggested to be used and is subject to change without notice.",
            suggest_fix: false,
        },

@sjyangkevin sjyangkevin force-pushed the catch-deprecated-imports-airflow-3_1 branch from c948314 to a513e55 Compare January 28, 2026 05:41
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also found modules moved to sdk here. Update the Rename/SourceModuleMoved to SourceModuleMovedToSDK, and the task-sdk version is 1.0.0.

Test Command:

breeze shell --use-airflow-version 3.0.0 --python 3.12 --backend postgres --db-reset
Screenshot from 2026-01-28 00-44-03

78 | ds_format("2026-01-01", "%Y-%m-%d", "%m-%d-%y")
79 | datetime_diff_for_humans(
|
help: `ds_add` has been moved to `airflow.sdk.execution_time.macros` since Airflow 3.1. Requires `apache-airflow-task-sdk>=1.1.0,<=1.1.6`. For `apache-airflow-task-sdk>=1.1.7`, import from `airflow.sdk` instead.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the message is refined a bit, hope it is better than before. the version is updated to >=1.1.7

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's better :)

@sjyangkevin sjyangkevin requested a review from Lee-W January 28, 2026 05:49
Copy link
Contributor

@Lee-W Lee-W left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM :)

78 | ds_format("2026-01-01", "%Y-%m-%d", "%m-%d-%y")
79 | datetime_diff_for_humans(
|
help: `ds_add` has been moved to `airflow.sdk.execution_time.macros` since Airflow 3.1. Requires `apache-airflow-task-sdk>=1.1.0,<=1.1.6`. For `apache-airflow-task-sdk>=1.1.7`, import from `airflow.sdk` instead.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's better :)

@sjyangkevin
Copy link
Contributor Author

Hi @MichaReiser , @ntBre , would appreciate if we could get your help to review again. Thanks!

@ntBre
Copy link
Contributor

ntBre commented Feb 2, 2026

Will do! I should be able to take a look this week :)

Copy link
Contributor

@ntBre ntBre left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, thank you!

@ntBre ntBre merged commit f055f39 into astral-sh:main Feb 4, 2026
41 checks passed
ntBre pushed a commit that referenced this pull request Feb 9, 2026
…context key for Airflow 3.0 (`AIR301`) (#22850)

## Summary

<!-- What's the purpose of the change? What does it do, and why? -->
Context:
apache/airflow#41641

1. apache/airflow#45961
* <strike>create_dagrun removed from airflow...DAG</strike> (This has
already been implemented in AIR301)
* context key dag_run.external_trigger removed
2. apache/airflow#45960
* context["inlet_events"]["url"] → context["inlet_events"][Asset("url")]
3. apache/airflow#41348
* context key triggering_dataset_events → triggering_asset_events

The existing AIR301 rules can detect when users access a removed key
such as `execution_date` through Airflow's `context`. For example,
`context["execution_date"]`, or `context["dag_run"]`. However, if an
attribute is deprecated from a context key, such as
`context["dag_run"].external_trigger`, the current implement will not
flag it.

This PR adds the logic for such check, and add two rules to flag the
deprecated attribute for the `"dag_run"` and `"inlet_events"` context
key. In addition to this, `"triggering_dataset_events"` is a deprecated
context key which can be handled by the existing rule. However, the
existing rule doesn't raise a diagnostic. Hence, the rule logic is
refactored a little bit, such that we can add this check and suggest a
`Replacement::Rename`.

## Test Plan

<!-- How was it tested? -->
The test cases have been added to `AIR301_context.py`, and all the tests
have been run locally and success. @Lee-W , could you please review it
when you have time, thanks!

## Notes

In #22376, we introduced some
improvements to the AIR301 code. I will re-base this PR when we are all
good on that, so it can pick up those code structure improvements, and
updated rules.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

preview Related to preview mode features rule Implementing or modifying a lint rule

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants