diff --git a/airflow-core/src/airflow/configuration.py b/airflow-core/src/airflow/configuration.py index 1bfbbcf62b445..361b5765fd44f 100644 --- a/airflow-core/src/airflow/configuration.py +++ b/airflow-core/src/airflow/configuration.py @@ -589,6 +589,14 @@ def _write_value( value = "\n# ".join(value_lines) file.write(f"# {option} = {value}\n") else: + if "\n" in value: + try: + value = json.dumps(json.loads(value), indent=4) + value = value.replace( + "\n", "\n " + ) # indent multi-line JSON to satisfy configparser format + except JSONDecodeError: + pass file.write(f"{option} = {value}\n") if needs_separation: file.write("\n") diff --git a/airflow-core/tests/unit/core/test_configuration.py b/airflow-core/tests/unit/core/test_configuration.py index 0735245f5098d..bcce279d08618 100644 --- a/airflow-core/tests/unit/core/test_configuration.py +++ b/airflow-core/tests/unit/core/test_configuration.py @@ -1028,6 +1028,52 @@ def test_order_of_secrets_backends_and_kwargs_on_workers( for key, value in expected_backend_kwargs.items(): assert getattr(secrets_backend, key) == value + def test_write_pretty_prints_multiline_json(self): + """ + Tests that the `write` method correctly pretty-prints + a config value that is a valid multi-line JSON string. + """ + json_string = '[\n{\n"name": "dags-folder",\n"classpath": "test.class"\n}\n]' + + test_conf = AirflowConfigParser() + test_conf.add_section("test_json") + test_conf.set("test_json", "my_json_config", json_string) + + with StringIO() as string_file: + test_conf.write(string_file, include_descriptions=False, include_env_vars=False) + content = string_file.getvalue() + + expected_formatted_string = ( + "my_json_config = [\n" + " {\n" + ' "name": "dags-folder",\n' + ' "classpath": "test.class"\n' + " }\n" + " ]\n" + ) + + assert expected_formatted_string in content + assert json_string not in content + + def test_write_handles_multiline_non_json_string(self): + """ + Tests that `write` does not crash when encountering a multi-line string + that is NOT valid JSON. + """ + multiline_string = "This is the first line.\nThis is the second line." + + test_conf = AirflowConfigParser() + test_conf.add_section("test_multiline") + test_conf.set("test_multiline", "my_string_config", multiline_string) + + with StringIO() as string_file: + test_conf.write(string_file, include_descriptions=False, include_env_vars=False) + content = string_file.getvalue() + + expected_raw_output = "my_string_config = This is the first line.\nThis is the second line.\n" + + assert expected_raw_output in content + @mock.patch.dict( "os.environ",