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

Lee-W 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 fa9c935035d airflowctl: fix datetime.datetime CLI parameters rejecting 
all input (#70249)
fa9c935035d is described below

commit fa9c935035dca69b3319469869976b56e5d790d8
Author: Bramhanand Lingala <[email protected]>
AuthorDate: Tue Oct 27 05:37:37 2026 -0400

    airflowctl: fix datetime.datetime CLI parameters rejecting all input 
(#70249)
    
    Co-authored-by: Wei Lee <[email protected]>
---
 airflow-ctl/src/airflowctl/ctl/cli_config.py       | 31 +++++++++-
 .../tests/airflow_ctl/ctl/test_cli_config.py       | 66 ++++++++++++++++++++++
 2 files changed, 95 insertions(+), 2 deletions(-)

diff --git a/airflow-ctl/src/airflowctl/ctl/cli_config.py 
b/airflow-ctl/src/airflowctl/ctl/cli_config.py
index 95979dbe503..b0f229be2ff 100755
--- a/airflow-ctl/src/airflowctl/ctl/cli_config.py
+++ b/airflow-ctl/src/airflowctl/ctl/cli_config.py
@@ -216,6 +216,33 @@ def json_dict_type(val: str | dict[str, Any]) -> dict[str, 
Any]:
     return parsed
 
 
+def is_iso_datetime_type(val: str | datetime.datetime) -> datetime.datetime:
+    """Parse an ISO-8601 datetime argument."""
+    if isinstance(val, datetime.datetime):
+        return val
+    try:
+        if val.endswith("Z"):
+            val = val[:-1] + "+00:00"
+        return datetime.datetime.fromisoformat(val)
+    except ValueError as e:
+        raise argparse.ArgumentTypeError(f"invalid datetime value: {val!r}") 
from e
+
+
+def is_iso_date_type(val: str | datetime.date) -> datetime.date:
+    """
+    Parse an ISO-8601 date string into a datetime.date.
+
+    Used as the argparse ``type=`` callable for datetime.date-typed CLI
+    parameters, mirroring is_iso_datetime_type() above.
+    """
+    if isinstance(val, datetime.date):
+        return val
+    try:
+        return datetime.date.fromisoformat(val)
+    except ValueError as e:
+        raise argparse.ArgumentTypeError(f"Invalid ISO-8601 date: {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"
@@ -662,8 +689,8 @@ class CommandFactory:
             "dict": json_dict_type,
             "tuple": tuple,
             "set": set,
-            "datetime.date": datetime.date,
-            "datetime.datetime": datetime.datetime,
+            "datetime.date": is_iso_date_type,
+            "datetime.datetime": is_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 8417860fcc4..4e8bf00c906 100644
--- a/airflow-ctl/tests/airflow_ctl/ctl/test_cli_config.py
+++ b/airflow-ctl/tests/airflow_ctl/ctl/test_cli_config.py
@@ -18,6 +18,7 @@
 from __future__ import annotations
 
 import argparse
+import datetime
 import json
 from argparse import BooleanOptionalAction
 from datetime import UTC
@@ -41,6 +42,8 @@ from airflowctl.ctl.cli_config import (
     CommandFactory,
     GroupCommand,
     add_auth_token_to_all_commands,
+    is_iso_date_type,
+    is_iso_datetime_type,
     json_dict_type,
     merge_commands,
     safe_call_command,
@@ -435,6 +438,51 @@ class TestCommandFactory:
         with pytest.raises(argparse.ArgumentTypeError, match="expected JSON 
object"):
             json_dict_type(value)
 
+    def test_is_iso_datetime_type_returns_datetime_input_unchanged(self):
+        """A datetime.datetime input is returned as-is without re-parsing."""
+
+        value = datetime.datetime(2026, 7, 1, tzinfo=UTC)
+
+        assert is_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"),
+            ("2026-07-01T12:34:56Z", "2026-07-01T12:34:56+00:00"),
+        ],
+    )
+    def test_is_iso_datetime_type_parses_iso_string(self, value, expected):
+        """An ISO-8601 datetime string (date-only or full) is parsed into a 
datetime."""
+        assert is_iso_datetime_type(value).isoformat() == expected
+
+    def test_is_iso_datetime_type_rejects_invalid_value(self):
+        """A non-parseable string raises an ArgumentTypeError."""
+        with pytest.raises(argparse.ArgumentTypeError, match="invalid datetime 
value"):
+            is_iso_datetime_type("not-a-date")
+
+    def test_is_iso_datetime_type_accepts_trailing_z(self):
+        """A trailing Z (as emitted by the API) parses on all supported Python 
versions."""
+        assert is_iso_datetime_type("2026-07-01T12:00:00Z") == 
datetime.datetime(
+            2026, 7, 1, 12, 0, 0, tzinfo=UTC
+        )
+
+    def test_is_iso_date_type_parses_iso_date(self):
+        """An ISO-8601 date string is parsed into a datetime.date."""
+        assert is_iso_date_type("2026-07-01") == datetime.date(2026, 7, 1)
+
+    def test_is_iso_date_type_returns_date_input_unchanged(self):
+        """A datetime.date input is returned as-is without re-parsing."""
+        d = datetime.date(2026, 7, 1)
+        assert is_iso_date_type(d) is d
+
+    def test_is_iso_date_type_rejects_invalid_value(self):
+        """A non-parseable string raises an ArgumentTypeError."""
+        with pytest.raises(argparse.ArgumentTypeError, match="Invalid ISO-8601 
date"):
+            is_iso_date_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.
 
@@ -1013,6 +1061,24 @@ class TestCliConfigMethods:
                         return
         pytest.fail(f"Auto-generated command not found: {group_name} 
{subcommand_name}")
 
+    def test_command_factory_wires_iso_parser_to_datetime_params(self):
+        """A generated datetime CLI arg parses an ISO date end to end."""
+        command_factory = CommandFactory()
+        dagrun_list_args = []
+
+        for group in command_factory.group_commands:
+            if group.name != "dagrun":
+                continue
+
+            for sub in group.subcommands:
+                if sub.name == "list":
+                    dagrun_list_args = list(sub.args)
+                    break
+
+        start_date_arg = next(a for a in dagrun_list_args if a.flags == 
("--start-date",))
+
+        assert start_date_arg.kwargs["type"]("2026-07-01") == 
datetime.datetime(2026, 7, 1)
+
     @staticmethod
     def _call_generated_command(monkeypatch, operations_class, method_name: 
str, **parsed_args):
         """Run the auto-generated command for ``operations_class.method_name`` 
and return its call kwargs."""

Reply via email to