This is an automated email from the ASF dual-hosted git repository.

potiuk pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/main by this push:
     new ff3f61af405 Respect deprecated options in default config parser 
(#61289)
ff3f61af405 is described below

commit ff3f61af4055a8b10c9e3227118517d17411a762
Author: Jason(Zhe-You) Liu <[email protected]>
AuthorDate: Tue Feb 3 21:32:58 2026 +0800

    Respect deprecated options in default config parser (#61289)
    
    * Refactor configuration parser to respect deprecated options in default 
parser
    
    * Add tests to ensure deprecated options are skipped in configuration parser
    
    * Add test for handling default values of deprecated options in 
AirflowConfigParser
    
    * Add test case without fallback param
---
 airflow-core/src/airflow/configuration.py          |  13 +--
 airflow-core/tests/unit/core/test_configuration.py |  53 +++++++++++
 .../src/airflow_shared/configuration/parser.py     |  29 ++++++
 .../tests/configuration/test_parser.py             | 100 ++++++++++++++++++++-
 task-sdk/src/airflow/sdk/configuration.py          |  20 ++---
 5 files changed, 188 insertions(+), 27 deletions(-)

diff --git a/airflow-core/src/airflow/configuration.py 
b/airflow-core/src/airflow/configuration.py
index 7bc34bec47a..82483b61794 100644
--- a/airflow-core/src/airflow/configuration.py
+++ b/airflow-core/src/airflow/configuration.py
@@ -43,6 +43,7 @@ from airflow._shared.configuration.parser import (
     VALUE_NOT_FOUND_SENTINEL,
     AirflowConfigParser as _SharedAirflowConfigParser,
     ValueNotFound,
+    configure_parser_from_configuration_description,
 )
 from airflow._shared.module_loading import import_string
 from airflow.exceptions import AirflowConfigException, RemovedInAirflow4Warning
@@ -684,17 +685,7 @@ def 
create_default_config_parser(configuration_description: dict[str, dict[str,
     """
     parser = ConfigParser()
     all_vars = get_all_expansion_variables()
-    for section, section_desc in configuration_description.items():
-        parser.add_section(section)
-        options = section_desc["options"]
-        for key in options:
-            default_value = options[key]["default"]
-            is_template = options[key].get("is_template", False)
-            if default_value is not None:
-                if is_template or not isinstance(default_value, str):
-                    parser.set(section, key, default_value)
-                else:
-                    parser.set(section, key, default_value.format(**all_vars))
+    configure_parser_from_configuration_description(parser, 
configuration_description, all_vars)
     return parser
 
 
diff --git a/airflow-core/tests/unit/core/test_configuration.py 
b/airflow-core/tests/unit/core/test_configuration.py
index a3d02d070fa..9c7ec9a3d16 100644
--- a/airflow-core/tests/unit/core/test_configuration.py
+++ b/airflow-core/tests/unit/core/test_configuration.py
@@ -1743,6 +1743,59 @@ sql_alchemy_conn=sqlite://test
         assert airflow_cfg.get("core", "hostname_callable") == "test-fn"
         assert sum(1 for option in all_core_options_including_defaults if 
option == "hostname_callable") == 1
 
+    def test_get_options_including_deprecated_defaults_should_skip(self, 
tmp_path):
+        # 1. Import the real function before patching
+        from airflow.configuration import _default_config_file_path as 
real_default_config_file_path
+
+        config_yaml_with_deprecated = textwrap.dedent(
+            """
+        test_deprecated_section:
+          description: Test section with deprecated options
+          options:
+            key_without_deprecation:
+              type: string
+              default: value1
+            key_with_version_deprecated:
+              type: string
+              default: value2
+              version_deprecated: 3.0.0
+            key_with_deprecation_reason:
+              type: string
+              default: value3
+              deprecation_reason: This key is deprecated.
+            key_with_both_deprecation:
+              type: string
+              default: value4
+              version_deprecated: 3.0.0
+              deprecation_reason: This key is deprecated.
+        """
+        )
+
+        # mock _default_config_file_path with tmp file containing above yaml
+        config_file = tmp_path / "config.yml"
+        config_file.write_text(config_yaml_with_deprecated)
+
+        # 2. Define the side effect using the captured 'real' function
+        def custom_default_config_file_path(file_name):
+            if file_name == "config.yml":
+                return os.fspath(config_file)
+            return real_default_config_file_path(file_name)
+
+        # 3. Apply the patch via context manager using the side_effect
+        with mock.patch(
+            "airflow.configuration._default_config_file_path", 
side_effect=custom_default_config_file_path
+        ):
+            # act
+            airflow_cfg = AirflowConfigParser()
+
+            assert airflow_cfg.has_option("test_deprecated_section", 
"key_without_deprecation")
+            for deprecated_key in [
+                "key_with_version_deprecated",
+                "key_with_deprecation_reason",
+                "key_with_both_deprecation",
+            ]:
+                assert not airflow_cfg.has_option("test_deprecated_section", 
deprecated_key)
+
 
 @skip_if_force_lowest_dependencies_marker
 def test_sensitive_values():
diff --git a/shared/configuration/src/airflow_shared/configuration/parser.py 
b/shared/configuration/src/airflow_shared/configuration/parser.py
index baf695daaa8..f7e3755c076 100644
--- a/shared/configuration/src/airflow_shared/configuration/parser.py
+++ b/shared/configuration/src/airflow_shared/configuration/parser.py
@@ -109,6 +109,35 @@ def _is_template(configuration_description: dict[str, 
dict[str, Any]], section:
     return configuration_description.get(section, {}).get(key, 
{}).get("is_template", False)
 
 
+def configure_parser_from_configuration_description(
+    parser: ConfigParser,
+    configuration_description: dict[str, dict[str, Any]],
+    all_vars: dict[str, Any],
+) -> None:
+    """
+    Configure a ConfigParser based on configuration description.
+
+    :param parser: ConfigParser to configure
+    :param configuration_description: configuration description from config.yml
+    """
+    for section, section_desc in configuration_description.items():
+        parser.add_section(section)
+        options = section_desc["options"]
+        for key in options:
+            default_value = options[key]["default"]
+            is_template = options[key].get("is_template", False)
+            if (default_value is not None) and not (
+                options[key].get("version_deprecated") or 
options[key].get("deprecation_reason")
+            ):
+                if is_template or not isinstance(default_value, str):
+                    parser.set(section, key, str(default_value))
+                else:
+                    try:
+                        parser.set(section, key, 
default_value.format(**all_vars))
+                    except (KeyError, ValueError):
+                        parser.set(section, key, default_value)
+
+
 class AirflowConfigParser(ConfigParser):
     """
     Base configuration parser with pure parsing logic.
diff --git a/shared/configuration/tests/configuration/test_parser.py 
b/shared/configuration/tests/configuration/test_parser.py
index 484e6373f0e..16c8667dfad 100644
--- a/shared/configuration/tests/configuration/test_parser.py
+++ b/shared/configuration/tests/configuration/test_parser.py
@@ -31,7 +31,10 @@ from unittest.mock import patch
 import pytest
 
 from airflow_shared.configuration.exceptions import AirflowConfigException
-from airflow_shared.configuration.parser import AirflowConfigParser as 
_SharedAirflowConfigParser
+from airflow_shared.configuration.parser import (
+    AirflowConfigParser as _SharedAirflowConfigParser,
+    configure_parser_from_configuration_description,
+)
 
 
 class AirflowConfigParser(_SharedAirflowConfigParser):
@@ -790,3 +793,98 @@ existing_list = one,two,three
         test_conf.set("Test", "NewKey", "new_value")
         assert test_conf.get("test", "newkey") == "new_value"
         assert test_conf.get("TEST", "NEWKEY") == "new_value"
+
+    def 
test_configure_parser_from_configuration_description_with_deprecated_options(self):
+        """
+        Test that configure_parser_from_configuration_description respects 
deprecated options.
+        """
+        configuration_description = {
+            "test_section": {
+                "options": {
+                    "non_deprecated_key": {"default": "non_deprecated_value"},
+                    "deprecated_key_version": {
+                        "default": "deprecated_value_version",
+                        "version_deprecated": "3.0.0",
+                    },
+                    "deprecated_key_reason": {
+                        "default": "deprecated_value_reason",
+                        "deprecation_reason": "Some reason",
+                    },
+                    "none_default_deprecated": {"default": None, 
"deprecation_reason": "No default"},
+                }
+            }
+        }
+        parser = ConfigParser()
+        all_vars = {}
+
+        configure_parser_from_configuration_description(parser, 
configuration_description, all_vars)
+
+        # Assert that the non-deprecated key is present
+        assert parser.has_option("test_section", "non_deprecated_key")
+        assert parser.get("test_section", "non_deprecated_key") == 
"non_deprecated_value"
+
+        # Assert that the deprecated keys are NOT present
+        assert not parser.has_option("test_section", "deprecated_key_version")
+        assert not parser.has_option("test_section", "deprecated_key_reason")
+
+        # Assert that options with default None are still not set
+        assert not parser.has_option("test_section", "none_default_deprecated")
+
+    def test_get_default_value_deprecated(self):
+        """Test 'conf.get' for deprecated options and should not return 
default value."""
+
+        class TestConfigParser(AirflowConfigParser):
+            def __init__(self):
+                configuration_description = {
+                    "test_section": {
+                        "options": {
+                            "deprecated_key": {
+                                "default": "some_value",
+                                "deprecation_reason": "deprecated",
+                            },
+                            "deprecated_key2": {
+                                "default": "some_value",
+                                "version_deprecated": "2.0.0",
+                            },
+                            "deprecated_key3": {
+                                "default": None,
+                                "version_deprecated": "2.0.0",
+                            },
+                            "active_key": {
+                                "default": "active_value",
+                            },
+                        }
+                    }
+                }
+                _default_values = ConfigParser()
+                # verify configure_parser_from_configuration_description logic 
of skipping
+                configure_parser_from_configuration_description(
+                    _default_values, configuration_description, {}
+                )
+                _SharedAirflowConfigParser.__init__(self, 
configuration_description, _default_values)
+
+        test_conf = TestConfigParser()
+        deprecated_conf_list = [
+            ("test_section", "deprecated_key"),
+            ("test_section", "deprecated_key2"),
+            ("test_section", "deprecated_key3"),
+        ]
+
+        # case 1: using `get` with fallback
+        # should return fallback if not found
+        expected_sentinel = object()
+        for section, key in deprecated_conf_list:
+            assert test_conf.get(section, key, fallback=expected_sentinel) is 
expected_sentinel
+
+        # case 2: using `get` without fallback
+        # should raise AirflowConfigException
+        for section, key in deprecated_conf_list:
+            with pytest.raises(
+                AirflowConfigException,
+                match=re.escape(f"section/key [{section}/{key}] not found in 
config"),
+            ):
+                test_conf.get(section, key)
+
+        # case 3: active (non-deprecated) key
+        # Active key should be present
+        assert test_conf.get("test_section", "active_key") == "active_value"
diff --git a/task-sdk/src/airflow/sdk/configuration.py 
b/task-sdk/src/airflow/sdk/configuration.py
index 61c1fb5686d..9d0da594b4f 100644
--- a/task-sdk/src/airflow/sdk/configuration.py
+++ b/task-sdk/src/airflow/sdk/configuration.py
@@ -27,7 +27,10 @@ from io import StringIO
 from typing import Any
 
 from airflow.sdk import yaml
-from airflow.sdk._shared.configuration.parser import AirflowConfigParser as 
_SharedAirflowConfigParser
+from airflow.sdk._shared.configuration.parser import (
+    AirflowConfigParser as _SharedAirflowConfigParser,
+    configure_parser_from_configuration_description,
+)
 from airflow.sdk.execution_time.secrets import 
_SERVER_DEFAULT_SECRETS_SEARCH_PATH
 
 log = logging.getLogger(__name__)
@@ -85,20 +88,7 @@ def create_default_config_parser(configuration_description: 
dict[str, dict[str,
     """
     parser = ConfigParser()
     all_vars = get_sdk_expansion_variables()
-    for section, section_desc in configuration_description.items():
-        parser.add_section(section)
-        options = section_desc["options"]
-        for key in options:
-            default_value = options[key]["default"]
-            is_template = options[key].get("is_template", False)
-            if default_value is not None:
-                if is_template or not isinstance(default_value, str):
-                    parser.set(section, key, str(default_value))
-                else:
-                    try:
-                        parser.set(section, key, 
default_value.format(**all_vars))
-                    except (KeyError, ValueError):
-                        parser.set(section, key, default_value)
+    configure_parser_from_configuration_description(parser, 
configuration_description, all_vars)
     return parser
 
 

Reply via email to