From 7b1d10972bc711de74ecb97d66665a8c2d99fc3c Mon Sep 17 00:00:00 2001 From: PoAn Yang Date: Sun, 14 Jun 2026 13:19:56 +0900 Subject: [PATCH] refactor: [AIP-94] airflowctl config: add get command Signed-off-by: PoAn Yang --- .../airflow/cli/commands/config_command.py | 26 ++++++----- .../cli/commands/test_command_deprecations.py | 3 +- .../unit/cli/commands/test_config_command.py | 44 ++++++++++++------- 3 files changed, 45 insertions(+), 28 deletions(-) diff --git a/airflow-core/src/airflow/cli/commands/config_command.py b/airflow-core/src/airflow/cli/commands/config_command.py index 21f1dcfc4a9fa..85f3b44bcc85f 100644 --- a/airflow-core/src/airflow/cli/commands/config_command.py +++ b/airflow-core/src/airflow/cli/commands/config_command.py @@ -25,12 +25,15 @@ from typing import Any, NamedTuple import pygments +from airflowctl.api.operations import ServerResponseError from pygments.lexers.configs import IniLexer +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.configuration import AIRFLOW_CONFIG, ConfigModifications, conf from airflow.exceptions import AirflowConfigException -from airflow.utils.cli import should_use_colors +from airflow.utils.cli import should_use_colors, suppress_logs_and_warning from airflow.utils.code_utils import get_terminal_formatter from airflow.utils.providers_configuration_loader import providers_configuration_loaded @@ -63,19 +66,20 @@ def show_config(args): print(code) +@deprecated_for_airflowctl("airflowctl config get") +@suppress_logs_and_warning @providers_configuration_loaded -def get_value(args): +@provide_api_client +def get_value(args, api_client: Client = NEW_API_CLIENT): """Get one value from configuration.""" - # while this will make get_value quite a bit slower we must initialize configuration - # for providers because we do not know what sections and options will be available after - # providers are initialized. Theoretically Providers might add new sections and options - # but also override defaults for existing options, so without loading all providers we - # cannot be sure what is the final value of the option. try: - value = conf.get(args.section, args.option) - print(value) - except AirflowConfigException: - pass + config = api_client.configs.get(section=args.section, option=args.option) + except ServerResponseError as e: + # Preserve the historical behaviour of staying silent when the option is unknown. + if e.response.status_code == 404: + return + raise + print(config.sections[0].options[0].value) class ConfigParameter(NamedTuple): 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..0602c01747e55 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, config_command, dag_command, pool_command from airflow.exceptions import RemovedInAirflow4Warning # (command callable, argv to parse, expected airflowctl replacement named in the warning) @@ -52,6 +52,7 @@ ["assets", "materialize", "--name=foo"], "airflowctl assets materialize", ), + (config_command.get_value, ["config", "get-value", "core", "executor"], "airflowctl config get"), ] diff --git a/airflow-core/tests/unit/cli/commands/test_config_command.py b/airflow-core/tests/unit/cli/commands/test_config_command.py index 008554422624b..822d6c06cf40f 100644 --- a/airflow-core/tests/unit/cli/commands/test_config_command.py +++ b/airflow-core/tests/unit/cli/commands/test_config_command.py @@ -21,7 +21,10 @@ import shutil from unittest import mock +import httpx import pytest +from airflowctl.api.datamodels.generated import Config, ConfigOption, ConfigSection +from airflowctl.api.operations import ServerResponseError from airflow.cli import cli_parser from airflow.cli.commands import config_command @@ -33,6 +36,12 @@ STATSD_CONFIG_BEGIN_WITH = "# `StatsD `" +def _server_error(status_code: int) -> ServerResponseError: + request = httpx.Request("GET", "http://testserver/api/v2/config/section/core/option/executor") + response = httpx.Response(status_code, request=request, json={"detail": "boom"}) + return ServerResponseError(message="boom", request=request, response=response) + + class TestCliConfigList: @classmethod def setup_class(cls): @@ -245,29 +254,32 @@ class TestCliConfigGetValue: def setup_class(cls): cls.parser = cli_parser.get_parser() - @conf_vars({("core", "test_key"): "test_value"}) - def test_should_display_value(self, stdout_capture): + def test_should_display_value(self, mock_cli_api_client, stdout_capture): + mock_cli_api_client.configs.get.return_value = Config( + sections=[ConfigSection(name="core", options=[ConfigOption(key="test_key", value="test_value")])] + ) + with stdout_capture as temp_stdout: config_command.get_value(self.parser.parse_args(["config", "get-value", "core", "test_key"])) assert temp_stdout.getvalue().strip() == "test_value" + mock_cli_api_client.configs.get.assert_called_once_with(section="core", option="test_key") - @mock.patch("airflow.cli.commands.config_command.conf") - def test_should_not_raise_exception_when_section_for_config_with_value_defined_elsewhere_is_missing( - self, mock_conf - ): - # no section in config - mock_conf.has_section.return_value = False - # pretend that the option is defined by other means - mock_conf.has_option.return_value = True + def test_should_be_silent_when_option_is_missing(self, mock_cli_api_client, stdout_capture): + mock_cli_api_client.configs.get.side_effect = _server_error(404) - config_command.get_value(self.parser.parse_args(["config", "get-value", "some_section", "value"])) + with stdout_capture as temp_stdout: + config_command.get_value( + self.parser.parse_args(["config", "get-value", "missing-section", "dags_folder"]) + ) - def test_should_raise_exception_when_option_is_missing(self, caplog): - config_command.get_value( - self.parser.parse_args(["config", "get-value", "missing-section", "dags_folder"]) - ) - assert "section/key [missing-section/dags_folder] not found in config" in caplog.text + assert temp_stdout.getvalue() == "" + + def test_should_reraise_non_404_error(self, mock_cli_api_client): + mock_cli_api_client.configs.get.side_effect = _server_error(403) + + with pytest.raises(ServerResponseError): + config_command.get_value(self.parser.parse_args(["config", "get-value", "core", "executor"])) class TestConfigLint: