diff --git a/airflow-core/newsfragments/68175.significant.rst b/airflow-core/newsfragments/68175.significant.rst deleted file mode 100644 index 4c9f7b4dffc18..0000000000000 --- a/airflow-core/newsfragments/68175.significant.rst +++ /dev/null @@ -1,24 +0,0 @@ -Airflow CLI commands are moving to talk to the API server - -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. - -* 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 a19d761c30485..35a62c9df9135 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 = "***" @@ -44,25 +40,22 @@ def deprecated_for_airflowctl(replacement: str) -> Callable[[F], F]: """ Mark an ``airflow`` CLI command as deprecated in favour of an ``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 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``. """ 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] + 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..9300219fe5a1e 100644 --- a/airflow-core/tests/unit/cli/commands/test_command_deprecations.py +++ b/airflow-core/tests/unit/cli/commands/test_command_deprecations.py @@ -18,55 +18,45 @@ """ 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 and deprecated, add a row to ``DEPRECATED_CLI_COMMANDS`` -- the test then -verifies it emits ``RemovedInAirflow4Warning`` pointing at the right ``airflowctl`` command. +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; 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 -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 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. """ - 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 diff --git a/airflow-core/tests/unit/cli/test_utils.py b/airflow-core/tests/unit/cli/test_utils.py index 4fb137ad48201..f98a9ef6b193d 100644 --- a/airflow-core/tests/unit/cli/test_utils.py +++ b/airflow-core/tests/unit/cli/test_utils.py @@ -17,32 +17,34 @@ # 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" - def test_passes_through_args_and_preserves_metadata(self): + def test_passes_through_args_and_leaves_function_untouched(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._migrated_to_airflowctl == "airflowctl pools create"