From 6a772d8d7c411fad08f62871ae1542ddd24a63ee Mon Sep 17 00:00:00 2001 From: bramhanandlingala Date: Wed, 22 Jul 2026 21:54:38 +0530 Subject: [PATCH] airflowctl: fix datetime.datetime CLI parameters rejecting all input --- airflow-ctl/src/airflowctl/ctl/cli_config.py | 12 ++++++- .../tests/airflow_ctl/ctl/test_cli_config.py | 32 +++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/airflow-ctl/src/airflowctl/ctl/cli_config.py b/airflow-ctl/src/airflowctl/ctl/cli_config.py index 47270d9c29c10..efd1e67669a8e 100755 --- a/airflow-ctl/src/airflowctl/ctl/cli_config.py +++ b/airflow-ctl/src/airflowctl/ctl/cli_config.py @@ -209,6 +209,16 @@ def json_dict_type(val: str | dict[str, Any]) -> dict[str, Any]: return parsed +def iso_datetime_type(val: str | datetime.datetime) -> datetime.datetime: + """Parse an ISO-8601 datetime argument.""" + if isinstance(val, datetime.datetime): + return val + try: + return datetime.datetime.fromisoformat(val) + except ValueError as e: + raise argparse.ArgumentTypeError(f"invalid datetime value: {val!r}") from e + + def _load_help_texts_yaml() -> dict[str, dict[str, str]]: """Load the help texts yaml for the auto-generated commands.""" help_texts_path = Path(__file__).parent / "help_texts.yaml" @@ -546,7 +556,7 @@ def _python_type_from_string(type_name: str | type) -> type | Callable: "dict": json_dict_type, "tuple": tuple, "set": set, - "datetime.datetime": datetime.datetime, + "datetime.datetime": iso_datetime_type, "dict[str, typing.Any]": json_dict_type, } # Default to ``str`` to preserve previous behaviour for any unrecognised diff --git a/airflow-ctl/tests/airflow_ctl/ctl/test_cli_config.py b/airflow-ctl/tests/airflow_ctl/ctl/test_cli_config.py index b843b3b75682f..1d6c8fcb47eb4 100644 --- a/airflow-ctl/tests/airflow_ctl/ctl/test_cli_config.py +++ b/airflow-ctl/tests/airflow_ctl/ctl/test_cli_config.py @@ -33,6 +33,7 @@ CommandFactory, GroupCommand, add_auth_token_to_all_commands, + iso_datetime_type, json_dict_type, merge_commands, safe_call_command, @@ -363,6 +364,37 @@ def test_json_dict_type_rejects_non_object_json(self, value): with pytest.raises(argparse.ArgumentTypeError, match="expected JSON object"): json_dict_type(value) + def test_iso_datetime_type_returns_datetime_input_unchanged(self): + """A datetime.datetime input is returned as-is without re-parsing.""" + import datetime + + value = datetime.datetime(2026, 7, 1, tzinfo=datetime.timezone.utc) + + assert iso_datetime_type(value) is value + + @pytest.mark.parametrize( + ("value", "expected"), + [ + ("2026-07-01", "2026-07-01T00:00:00"), + ("2026-07-01T00:00:00", "2026-07-01T00:00:00"), + ("2026-07-01T12:34:56+00:00", "2026-07-01T12:34:56+00:00"), + ], + ) + def test_iso_datetime_type_parses_iso_string(self, value, expected): + """An ISO-8601 datetime string (date-only or full) is parsed into a datetime. + + Regression test for https://github.com/apache/airflow/issues/70232: previously the + bare ``datetime.datetime`` class was used as the argparse ``type=`` callable, so + argparse called ``datetime.datetime(value)`` on the raw string, which always raised + a ``TypeError`` regardless of the input. + """ + assert iso_datetime_type(value).isoformat() == expected + + def test_iso_datetime_type_rejects_invalid_value(self): + """A non-parseable string raises an ArgumentTypeError.""" + with pytest.raises(argparse.ArgumentTypeError, match="invalid datetime value"): + iso_datetime_type("not-a-date") + def test_command_factory_required_primitive_param_is_positional(self, tmp_path): """Required primitive parameters (no default, not Optional) become positional arguments.