Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion airflow-ctl/src/airflowctl/ctl/cli_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -566,7 +576,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,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we add a test like this?

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)

"dict[str, typing.Any]": json_dict_type,
}
# Default to ``str`` to preserve previous behaviour for any unrecognised
Expand Down
32 changes: 32 additions & 0 deletions airflow-ctl/tests/airflow_ctl/ctl/test_cli_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
CommandFactory,
GroupCommand,
add_auth_token_to_all_commands,
iso_datetime_type,
json_dict_type,
merge_commands,
safe_call_command,
Expand Down Expand Up @@ -364,6 +365,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.
"""
Comment on lines +385 to +391

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"""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.
"""
"""An ISO-8601 datetime string (date-only or full) is parsed into a datetime."""

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.

Expand Down