From a92263b715bc3548a585f75b7e735a4a665fff7c Mon Sep 17 00:00:00 2001 From: Henry Chen Date: Sun, 14 Jun 2026 10:23:48 +0800 Subject: [PATCH] Use the airflowctl API client to list installed providers while preserving the legacy command output format. --- .../airflow/cli/commands/provider_command.py | 12 ++-- .../cli/commands/test_command_deprecations.py | 3 +- .../cli/commands/test_provider_command.py | 61 +++++++++++++++++++ 3 files changed, 71 insertions(+), 5 deletions(-) create mode 100644 airflow-core/tests/unit/cli/commands/test_provider_command.py diff --git a/airflow-core/src/airflow/cli/commands/provider_command.py b/airflow-core/src/airflow/cli/commands/provider_command.py index 645618fd852cc..797a5a559e67a 100644 --- a/airflow-core/src/airflow/cli/commands/provider_command.py +++ b/airflow-core/src/airflow/cli/commands/provider_command.py @@ -21,7 +21,9 @@ import re import sys +from airflow.cli.api_client import NEW_API_CLIENT, Client, provide_api_client from airflow.cli.simple_table import AirflowConsole +from airflow.cli.utils import deprecated_for_airflowctl from airflow.providers_manager import ProvidersManager from airflow.utils.cli import suppress_logs_and_warning from airflow.utils.providers_configuration_loader import providers_configuration_loaded @@ -55,16 +57,18 @@ def provider_get(args): raise SystemExit(f"No such provider installed: {args.provider_name}") +@deprecated_for_airflowctl("airflowctl providers list") @suppress_logs_and_warning @providers_configuration_loaded -def providers_list(args): +@provide_api_client +def providers_list(args, api_client: Client = NEW_API_CLIENT): """List all providers at the command line.""" AirflowConsole().print_as( - data=list(ProvidersManager().providers.values()), + data=api_client.providers.list().providers, output=args.output, mapper=lambda x: { - "package_name": x.data["package-name"], - "description": _remove_rst_syntax(x.data["description"]), + "package_name": x.package_name, + "description": x.description, "version": x.version, }, ) 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..f1cc5bbc6793b 100644 --- a/airflow-core/tests/unit/cli/commands/test_command_deprecations.py +++ b/airflow-core/tests/unit/cli/commands/test_command_deprecations.py @@ -30,7 +30,7 @@ import pytest -from airflow.cli.commands import asset_command, dag_command, pool_command +from airflow.cli.commands import asset_command, dag_command, pool_command, provider_command from airflow.exceptions import RemovedInAirflow4Warning # (command callable, argv to parse, expected airflowctl replacement named in the warning) @@ -42,6 +42,7 @@ (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"), + (provider_command.providers_list, ["providers", "list"], "airflowctl providers list"), ( pool_command.pool_export, ["pools", "export", "/tmp/airflow_pools_export.json"], diff --git a/airflow-core/tests/unit/cli/commands/test_provider_command.py b/airflow-core/tests/unit/cli/commands/test_provider_command.py new file mode 100644 index 0000000000000..b2b323472f1d9 --- /dev/null +++ b/airflow-core/tests/unit/cli/commands/test_provider_command.py @@ -0,0 +1,61 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +from __future__ import annotations + +import json + +from airflowctl.api.datamodels.generated import ProviderResponse + +from airflow.cli import cli_parser +from airflow.cli.commands import provider_command + + +class TestCliProviders: + @classmethod + def setup_class(cls): + cls.parser = cli_parser.get_parser() + + def test_providers_list(self, mock_cli_api_client, stdout_capture): + mock_cli_api_client.providers.list.return_value.providers = [ + ProviderResponse( + package_name="apache-airflow-providers-test", + description="Test provider", + version="1.0.0", + documentation_url="https://example.com", + ) + ] + + with stdout_capture as stdout: + provider_command.providers_list(self.parser.parse_args(["providers", "list", "--output", "json"])) + + assert json.loads(stdout.getvalue()) == [ + { + "package_name": "apache-airflow-providers-test", + "description": "Test provider", + "version": "1.0.0", + } + ] + mock_cli_api_client.providers.list.assert_called_once_with() + + def test_providers_list_empty(self, mock_cli_api_client, stdout_capture): + mock_cli_api_client.providers.list.return_value.providers = [] + + with stdout_capture as stdout: + provider_command.providers_list(self.parser.parse_args(["providers", "list"])) + + assert "No data found" in stdout.getvalue()