From 67a3f370fffbd38b5d074cd229cd17f4a6fcdc4f Mon Sep 17 00:00:00 2001 From: bugraoz93 Date: Thu, 18 Jun 2026 20:44:22 +0200 Subject: [PATCH 1/6] Not warn user for CLI Deprecations --- airflow-core/src/airflow/cli/utils.py | 35 +++++------ .../cli/commands/test_command_deprecations.py | 63 ++++++++----------- airflow-core/tests/unit/cli/test_utils.py | 24 ++++--- 3 files changed, 56 insertions(+), 66 deletions(-) diff --git a/airflow-core/src/airflow/cli/utils.py b/airflow-core/src/airflow/cli/utils.py index a19d761c30485..28c560def8b90 100644 --- a/airflow-core/src/airflow/cli/utils.py +++ b/airflow-core/src/airflow/cli/utils.py @@ -17,14 +17,10 @@ from __future__ import annotations -import functools import sys -import warnings from collections.abc import Callable from typing import TYPE_CHECKING, TypeVar -from airflow.exceptions import RemovedInAirflow4Warning - # Placeholder for masking sensitive values in CLI output SENSITIVE_PLACEHOLDER = "***" @@ -42,27 +38,28 @@ def deprecated_for_airflowctl(replacement: str) -> Callable[[F], F]: """ - Mark an ``airflow`` CLI command as deprecated in favour of an ``airflowctl`` equivalent. + Mark an ``airflow`` CLI command as migrated to its ``airflowctl`` equivalent. - These commands now reach Airflow through the API server via the ``airflowctl`` client. They - are kept for backwards compatibility but will be removed in a future Airflow release; users - should switch to ``airflowctl`` directly. + The decorated command now reaches Airflow through the API server via the ``airflowctl`` + client. It is intentionally kept in the ``airflow`` CLI as a supported entry point, so it + emits **no user-facing deprecation warning** at runtime. Instead, the migration is recorded + for maintainers: a note is appended to the command's docstring and the equivalent + ``airflowctl`` command is stored on the ``_migrated_to_airflowctl`` attribute (the migration + registry test in ``test_command_deprecations.py`` reads it). :param replacement: The equivalent ``airflowctl`` command, e.g. ``airflowctl dags trigger``. """ def decorator(func: F) -> F: - @functools.wraps(func) - def wrapper(*args, **kwargs): - warnings.warn( - f"This `airflow` CLI command is deprecated and will be removed in a future " - f"Airflow release. Use `{replacement}` instead.", - RemovedInAirflow4Warning, - stacklevel=2, - ) - return func(*args, **kwargs) - - return wrapper # type: ignore[return-value] + maintainer_note = ( + "\n\n.. note::\n\n" + " **Maintainers:** this command has been migrated to the ``airflowctl`` HTTP API " + "client and now reaches Airflow through the API server. The equivalent ``airflowctl`` " + f"command is ``{replacement}``." + ) + func.__doc__ = (func.__doc__ or "") + maintainer_note + func._migrated_to_airflowctl = replacement # type: ignore[attr-defined] + return func return decorator diff --git a/airflow-core/tests/unit/cli/commands/test_command_deprecations.py b/airflow-core/tests/unit/cli/commands/test_command_deprecations.py index b4eb6840c9069..cd685acb90573 100644 --- a/airflow-core/tests/unit/cli/commands/test_command_deprecations.py +++ b/airflow-core/tests/unit/cli/commands/test_command_deprecations.py @@ -16,57 +16,46 @@ # specific language governing permissions and limitations # under the License. """ -Single source of truth for the ``airflow`` CLI commands deprecated in favour of ``airflowctl``. +Single source of truth for the ``airflow`` CLI commands migrated to ``airflowctl``. Every command decorated with ``deprecated_for_airflowctl`` must have one entry below. When a new -command is migrated and deprecated, add a row to ``DEPRECATED_CLI_COMMANDS`` -- the test then -verifies it emits ``RemovedInAirflow4Warning`` pointing at the right ``airflowctl`` command. +command is migrated, add a row to ``MIGRATED_CLI_COMMANDS`` -- the test then verifies the decorator +recorded the right ``airflowctl`` replacement for maintainers. The commands stay in the ``airflow`` +CLI as supported entry points, so they emit no user-facing deprecation warning. """ from __future__ import annotations -import contextlib -import re - import pytest from airflow.cli.commands import asset_command, dag_command, pool_command -from airflow.exceptions import RemovedInAirflow4Warning -# (command callable, argv to parse, expected airflowctl replacement named in the warning) -DEPRECATED_CLI_COMMANDS = [ - (dag_command.dag_trigger, ["dags", "trigger", "example_dag", "--run-id=x"], "airflowctl dags trigger"), - (dag_command.dag_delete, ["dags", "delete", "example_dag", "--yes"], "airflowctl dags delete"), - (pool_command.pool_list, ["pools", "list"], "airflowctl pools list"), - (pool_command.pool_get, ["pools", "get", "foo"], "airflowctl pools get"), - (pool_command.pool_set, ["pools", "set", "foo", "1", "desc"], "airflowctl pools create"), - (pool_command.pool_delete, ["pools", "delete", "foo"], "airflowctl pools delete"), - (pool_command.pool_import, ["pools", "import", "/nonexistent.json"], "airflowctl pools import"), - ( - pool_command.pool_export, - ["pools", "export", "/tmp/airflow_pools_export.json"], - "airflowctl pools export", - ), - ( - asset_command.asset_materialize, - ["assets", "materialize", "--name=foo"], - "airflowctl assets materialize", - ), +# (command callable, expected airflowctl replacement recorded by the decorator) +MIGRATED_CLI_COMMANDS = [ + (dag_command.dag_trigger, "airflowctl dags trigger"), + (dag_command.dag_delete, "airflowctl dags delete"), + (pool_command.pool_list, "airflowctl pools list"), + (pool_command.pool_get, "airflowctl pools get"), + (pool_command.pool_set, "airflowctl pools create"), + (pool_command.pool_delete, "airflowctl pools delete"), + (pool_command.pool_import, "airflowctl pools import"), + (pool_command.pool_export, "airflowctl pools export"), + (asset_command.asset_materialize, "airflowctl assets materialize"), ] @pytest.mark.parametrize( - ("command", "argv", "replacement"), - DEPRECATED_CLI_COMMANDS, - ids=[argv[0] + "-" + argv[1] for _, argv, _ in DEPRECATED_CLI_COMMANDS], + ("command", "replacement"), + MIGRATED_CLI_COMMANDS, + ids=[replacement for _, replacement in MIGRATED_CLI_COMMANDS], ) -def test_deprecated_cli_command_points_to_airflowctl(command, argv, replacement, parser, mock_cli_api_client): - """Each migrated command warns it will become an alias for its ``airflowctl`` counterpart. +def test_migrated_cli_command_records_airflowctl_replacement(command, replacement): + """Each migrated command records its ``airflowctl`` counterpart for maintainers. - We only assert the deprecation warning fires (and names the right replacement); the command - body itself is exercised by the per-command test modules, so any error it raises against the - bare mocked client is irrelevant here and suppressed. + The marker (and the appended docstring note) is the maintainer-facing trace of the migration; + users see no runtime deprecation warning. The command body itself is exercised by the + per-command test modules. ``functools.wraps`` on the outer ``action_cli`` decorator propagates + both the attribute and the docstring up to the command object imported here. """ - with pytest.warns(RemovedInAirflow4Warning, match=re.escape(replacement)): - with contextlib.suppress(Exception, SystemExit): - command(parser.parse_args(argv)) + assert getattr(command, "_migrated_to_airflowctl", None) == replacement + assert replacement in (command.__doc__ or "") diff --git a/airflow-core/tests/unit/cli/test_utils.py b/airflow-core/tests/unit/cli/test_utils.py index 4fb137ad48201..783cbb38069d9 100644 --- a/airflow-core/tests/unit/cli/test_utils.py +++ b/airflow-core/tests/unit/cli/test_utils.py @@ -17,32 +17,36 @@ # under the License. from __future__ import annotations -import pytest +import warnings from airflow.cli.utils import deprecated_for_airflowctl -from airflow.exceptions import RemovedInAirflow4Warning class TestDeprecatedForAirflowctl: - def test_emits_warning_naming_replacement(self): + def test_records_replacement_without_emitting_a_user_warning(self): @deprecated_for_airflowctl("airflowctl dags trigger") def command(args): return "result" - with pytest.warns(RemovedInAirflow4Warning, match="airflowctl dags trigger"): + # Calling the command emits nothing to users (any warning would become an error here). + with warnings.catch_warnings(): + warnings.simplefilter("error") result = command(args=None) - # The wrapped command still runs and returns its value. assert result == "result" + # The replacement is recorded for maintainers, not shown to users. + assert command._migrated_to_airflowctl == "airflowctl dags trigger" + assert "airflowctl dags trigger" in command.__doc__ - def test_passes_through_args_and_preserves_metadata(self): + def test_passes_through_args_and_appends_maintainer_note(self): @deprecated_for_airflowctl("airflowctl pools create") def command(a, b, *, c): """Original docstring.""" return (a, b, c) - with pytest.warns(RemovedInAirflow4Warning): - assert command(1, 2, c=3) == (1, 2, 3) - + assert command(1, 2, c=3) == (1, 2, 3) + # The decorator returns the original function untouched apart from the metadata it records. assert command.__name__ == "command" - assert command.__doc__ == "Original docstring." + assert command.__doc__.startswith("Original docstring.") + assert "airflowctl pools create" in command.__doc__ + assert command._migrated_to_airflowctl == "airflowctl pools create" From 1eb0a837fd8ebf55abeb2f283d5579ac58ece6f5 Mon Sep 17 00:00:00 2001 From: bugraoz93 Date: Thu, 18 Jun 2026 20:49:09 +0200 Subject: [PATCH 2/6] Update significant to reflect direction --- airflow-core/newsfragments/68175.significant.rst | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/airflow-core/newsfragments/68175.significant.rst b/airflow-core/newsfragments/68175.significant.rst index 4c9f7b4dffc18..684f85f1be1de 100644 --- a/airflow-core/newsfragments/68175.significant.rst +++ b/airflow-core/newsfragments/68175.significant.rst @@ -1,16 +1,8 @@ -Airflow CLI commands are moving to talk to the API server +``airflow.api.client`` is replaced by the ``airflowctl`` client -The CLI is being migrated to reach Airflow through the API server (via the ``airflowctl`` -client) instead of the metadata database directly. Migrated so far: ``dags trigger``, -``dags delete``, ``pools`` (list/get/set/delete/import/export), and ``assets materialize``; -this fragment is updated as more commands migrate rather than adding new ones. - -These commands now require a reachable API server and mint a short-lived token in memory -(set ``AIRFLOW_CLI_TOKEN`` for auth managers that cannot mint locally, or for remote servers). -``airflow.api.client`` is removed — use ``airflow.cli.api_client.get_cli_api_client``. - -Each migrated command emits a ``RemovedInAirflow4Warning`` and will be removed in a future -Airflow release; use the equivalent ``airflowctl`` command instead. +The legacy ``airflow.api.client`` module is removed. The ``airflow`` CLI now reaches the API +server through the same typed client that ``airflowctl`` uses. Replace any use of +``airflow.api.client`` with ``airflow.cli.api_client.get_cli_api_client``. * Types of change From 2b77d6bfc53fc6db0d110eae3323fcb0adbf99d3 Mon Sep 17 00:00:00 2001 From: bugraoz93 Date: Thu, 18 Jun 2026 21:10:44 +0200 Subject: [PATCH 3/6] Rename significant to reflect warning changes --- .../{68175.significant.rst => 68726.significant.rst} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename airflow-core/newsfragments/{68175.significant.rst => 68726.significant.rst} (100%) diff --git a/airflow-core/newsfragments/68175.significant.rst b/airflow-core/newsfragments/68726.significant.rst similarity index 100% rename from airflow-core/newsfragments/68175.significant.rst rename to airflow-core/newsfragments/68726.significant.rst From 890607c897ce84d3b993f992b4915df07fa7e1ca Mon Sep 17 00:00:00 2001 From: bugraoz93 Date: Mon, 22 Jun 2026 20:03:54 +0200 Subject: [PATCH 4/6] Remove not used method docs and remove significant file --- airflow-core/newsfragments/68726.significant.rst | 16 ---------------- airflow-core/src/airflow/cli/utils.py | 16 +++++----------- .../cli/commands/test_command_deprecations.py | 9 ++++----- airflow-core/tests/unit/cli/test_utils.py | 6 ++---- 4 files changed, 11 insertions(+), 36 deletions(-) delete mode 100644 airflow-core/newsfragments/68726.significant.rst diff --git a/airflow-core/newsfragments/68726.significant.rst b/airflow-core/newsfragments/68726.significant.rst deleted file mode 100644 index 684f85f1be1de..0000000000000 --- a/airflow-core/newsfragments/68726.significant.rst +++ /dev/null @@ -1,16 +0,0 @@ -``airflow.api.client`` is replaced by the ``airflowctl`` client - -The legacy ``airflow.api.client`` module is removed. The ``airflow`` CLI now reaches the API -server through the same typed client that ``airflowctl`` uses. Replace any use of -``airflow.api.client`` with ``airflow.cli.api_client.get_cli_api_client``. - -* Types of change - - * [ ] Dag changes - * [ ] Config changes - * [ ] API changes - * [ ] CLI changes - * [x] Behaviour changes - * [ ] Plugin changes - * [ ] Dependency changes - * [x] Code interface changes diff --git a/airflow-core/src/airflow/cli/utils.py b/airflow-core/src/airflow/cli/utils.py index 28c560def8b90..2a3f362e77a81 100644 --- a/airflow-core/src/airflow/cli/utils.py +++ b/airflow-core/src/airflow/cli/utils.py @@ -42,22 +42,16 @@ def deprecated_for_airflowctl(replacement: str) -> Callable[[F], F]: The decorated command now reaches Airflow through the API server via the ``airflowctl`` client. It is intentionally kept in the ``airflow`` CLI as a supported entry point, so it - emits **no user-facing deprecation warning** at runtime. Instead, the migration is recorded - for maintainers: a note is appended to the command's docstring and the equivalent - ``airflowctl`` command is stored on the ``_migrated_to_airflowctl`` attribute (the migration - registry test in ``test_command_deprecations.py`` reads it). + emits **no user-facing deprecation warning** at runtime. The migration is recorded for + maintainers only: the equivalent ``airflowctl`` command is stored on the + ``_migrated_to_airflowctl`` attribute (the migration registry test in + ``test_command_deprecations.py`` reads it). The decorator at the command's definition site is + the developer-facing trace -- it is source-only and never rendered to users. :param replacement: The equivalent ``airflowctl`` command, e.g. ``airflowctl dags trigger``. """ def decorator(func: F) -> F: - maintainer_note = ( - "\n\n.. note::\n\n" - " **Maintainers:** this command has been migrated to the ``airflowctl`` HTTP API " - "client and now reaches Airflow through the API server. The equivalent ``airflowctl`` " - f"command is ``{replacement}``." - ) - func.__doc__ = (func.__doc__ or "") + maintainer_note func._migrated_to_airflowctl = replacement # type: ignore[attr-defined] return func diff --git a/airflow-core/tests/unit/cli/commands/test_command_deprecations.py b/airflow-core/tests/unit/cli/commands/test_command_deprecations.py index cd685acb90573..bf675265afa09 100644 --- a/airflow-core/tests/unit/cli/commands/test_command_deprecations.py +++ b/airflow-core/tests/unit/cli/commands/test_command_deprecations.py @@ -52,10 +52,9 @@ def test_migrated_cli_command_records_airflowctl_replacement(command, replacement): """Each migrated command records its ``airflowctl`` counterpart for maintainers. - The marker (and the appended docstring note) is the maintainer-facing trace of the migration; - users see no runtime deprecation warning. The command body itself is exercised by the - per-command test modules. ``functools.wraps`` on the outer ``action_cli`` decorator propagates - both the attribute and the docstring up to the command object imported here. + The marker is the maintainer-facing trace of the migration; users see no runtime deprecation + warning. The command body itself is exercised by the per-command test modules. + ``functools.wraps`` on the outer ``action_cli`` decorator propagates the attribute up to the + command object imported here. """ assert getattr(command, "_migrated_to_airflowctl", None) == replacement - assert replacement in (command.__doc__ or "") diff --git a/airflow-core/tests/unit/cli/test_utils.py b/airflow-core/tests/unit/cli/test_utils.py index 783cbb38069d9..f98a9ef6b193d 100644 --- a/airflow-core/tests/unit/cli/test_utils.py +++ b/airflow-core/tests/unit/cli/test_utils.py @@ -36,9 +36,8 @@ def command(args): assert result == "result" # The replacement is recorded for maintainers, not shown to users. assert command._migrated_to_airflowctl == "airflowctl dags trigger" - assert "airflowctl dags trigger" in command.__doc__ - def test_passes_through_args_and_appends_maintainer_note(self): + def test_passes_through_args_and_leaves_function_untouched(self): @deprecated_for_airflowctl("airflowctl pools create") def command(a, b, *, c): """Original docstring.""" @@ -47,6 +46,5 @@ def command(a, b, *, c): assert command(1, 2, c=3) == (1, 2, 3) # The decorator returns the original function untouched apart from the metadata it records. assert command.__name__ == "command" - assert command.__doc__.startswith("Original docstring.") - assert "airflowctl pools create" in command.__doc__ + assert command.__doc__ == "Original docstring." assert command._migrated_to_airflowctl == "airflowctl pools create" From 8534b2a93a5904f77777d3cc673a086604a5be66 Mon Sep 17 00:00:00 2001 From: bugraoz93 Date: Mon, 22 Jun 2026 20:11:59 +0200 Subject: [PATCH 5/6] Update documentation to explain CLI CTL relation better --- airflow-core/src/airflow/cli/utils.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/airflow-core/src/airflow/cli/utils.py b/airflow-core/src/airflow/cli/utils.py index 2a3f362e77a81..35a62c9df9135 100644 --- a/airflow-core/src/airflow/cli/utils.py +++ b/airflow-core/src/airflow/cli/utils.py @@ -38,16 +38,18 @@ def deprecated_for_airflowctl(replacement: str) -> Callable[[F], F]: """ - Mark an ``airflow`` CLI command as migrated to its ``airflowctl`` equivalent. - - The decorated command now reaches Airflow through the API server via the ``airflowctl`` - client. It is intentionally kept in the ``airflow`` CLI as a supported entry point, so it - emits **no user-facing deprecation warning** at runtime. The migration is recorded for - maintainers only: the equivalent ``airflowctl`` command is stored on the - ``_migrated_to_airflowctl`` attribute (the migration registry test in - ``test_command_deprecations.py`` reads it). The decorator at the command's definition site is + Mark an ``airflow`` CLI command as deprecated in favour of an ``airflowctl`` equivalent. + + The command keeps its existing implementation and stays in the ``airflow`` CLI as a supported + entry point, so it emits **no user-facing deprecation warning** at runtime. The intent is to + point future development at ``airflowctl``: the equivalent ``airflowctl`` command is recorded + for maintainers only, on the ``_migrated_to_airflowctl`` attribute (the migration registry test + in ``test_command_deprecations.py`` reads it). The decorator at the command's definition site is the developer-facing trace -- it is source-only and never rendered to users. + See ``contributing-docs/27_cli_implementation_guide.rst`` for the CLI / ``airflowctl`` + development guidance. + :param replacement: The equivalent ``airflowctl`` command, e.g. ``airflowctl dags trigger``. """ From 2f6c1d46ce76d4f1fab60a30f537f3dfa21d055d Mon Sep 17 00:00:00 2001 From: bugraoz93 Date: Mon, 22 Jun 2026 20:43:41 +0200 Subject: [PATCH 6/6] Update documentation to explain CLI CTL relation better --- .../unit/cli/commands/test_command_deprecations.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/airflow-core/tests/unit/cli/commands/test_command_deprecations.py b/airflow-core/tests/unit/cli/commands/test_command_deprecations.py index bf675265afa09..9300219fe5a1e 100644 --- a/airflow-core/tests/unit/cli/commands/test_command_deprecations.py +++ b/airflow-core/tests/unit/cli/commands/test_command_deprecations.py @@ -16,12 +16,14 @@ # specific language governing permissions and limitations # under the License. """ -Single source of truth for the ``airflow`` CLI commands migrated to ``airflowctl``. +Single source of truth for the ``airflow`` CLI commands deprecated in favour of ``airflowctl``. -Every command decorated with ``deprecated_for_airflowctl`` must have one entry below. When a new -command is migrated, add a row to ``MIGRATED_CLI_COMMANDS`` -- the test then verifies the decorator +Every command decorated with ``deprecated_for_airflowctl`` must have one entry below. When a +command is deprecated, add a row to ``MIGRATED_CLI_COMMANDS`` -- the test then verifies the decorator recorded the right ``airflowctl`` replacement for maintainers. The commands stay in the ``airflow`` -CLI as supported entry points, so they emit no user-facing deprecation warning. +CLI as supported entry points, so they emit no user-facing deprecation warning; they are simply no +longer developed here -- new work belongs in ``airflowctl``. See +``contributing-docs/27_cli_implementation_guide.rst`` for the CLI / ``airflowctl`` direction. """ from __future__ import annotations