This is an automated email from the ASF dual-hosted git repository.
bugraoz pushed a commit to branch v3-2-test
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/v3-2-test by this push:
new eb3896d59f5 [v3-2-test] fix(cli): change is_alive default to None in
jobs list (#65065) (#65091)
eb3896d59f5 is described below
commit eb3896d59f5192df72c0039cbc7a982c2bd28553
Author: Bugra Ozturk <[email protected]>
AuthorDate: Sun Apr 12 17:15:24 2026 +0200
[v3-2-test] fix(cli): change is_alive default to None in jobs list (#65065)
(#65091)
(cherry picked from commit 6ef5165)
Co-authored-by: rjgoyln <[email protected]>
---
airflow-ctl/src/airflowctl/api/operations.py | 14 ++++++++--
airflow-ctl/src/airflowctl/ctl/cli_config.py | 5 ++--
.../tests/airflow_ctl/api/test_operations.py | 31 ++++++++++++++++++++++
.../tests/airflow_ctl/ctl/test_cli_config.py | 30 +++++++++++++++++++++
4 files changed, 76 insertions(+), 4 deletions(-)
diff --git a/airflow-ctl/src/airflowctl/api/operations.py
b/airflow-ctl/src/airflowctl/api/operations.py
index 8c7b4a1408d..2890304743b 100644
--- a/airflow-ctl/src/airflowctl/api/operations.py
+++ b/airflow-ctl/src/airflowctl/api/operations.py
@@ -642,10 +642,20 @@ class JobsOperations(BaseOperations):
"""Job operations."""
def list(
- self, job_type: str, hostname: str, is_alive: bool
+ self,
+ job_type: str | None = None,
+ hostname: str | None = None,
+ is_alive: bool | None = None,
) -> JobCollectionResponse | ServerResponseError:
"""List all jobs."""
- params = {"job_type": job_type, "hostname": hostname, "is_alive":
is_alive}
+ params: dict[str, Any] = {}
+ if job_type:
+ params["job_type"] = job_type
+ if hostname:
+ params["hostname"] = hostname
+ if is_alive is not None:
+ params["is_alive"] = is_alive
+
return super().execute_list(path="jobs",
data_model=JobCollectionResponse, params=params)
diff --git a/airflow-ctl/src/airflowctl/ctl/cli_config.py
b/airflow-ctl/src/airflowctl/ctl/cli_config.py
index 95fb11d03ce..2ad55b91a0f 100755
--- a/airflow-ctl/src/airflowctl/ctl/cli_config.py
+++ b/airflow-ctl/src/airflowctl/ctl/cli_config.py
@@ -578,14 +578,15 @@ class CommandFactory:
for parameter in operation.get("parameters"):
for parameter_key, parameter_type in parameter.items():
if self._is_primitive_type(type_name=parameter_type):
- is_bool = parameter_type == "bool"
+ base_parameter_type = parameter_type.replace(" |
None", "").strip()
+ is_bool = base_parameter_type == "bool"
args.append(
self._create_arg(
arg_flags=("--" +
self._sanitize_arg_parameter_key(parameter_key),),
arg_type=self._python_type_from_string(parameter_type),
arg_action=argparse.BooleanOptionalAction if
is_bool else None,
arg_help=f"{parameter_key} for
{operation.get('name')} operation in {operation.get('parent').name}",
- arg_default=False if is_bool else None,
+ arg_default=None,
)
)
else:
diff --git a/airflow-ctl/tests/airflow_ctl/api/test_operations.py
b/airflow-ctl/tests/airflow_ctl/api/test_operations.py
index 7d875054c3f..2f2e0b0f547 100644
--- a/airflow-ctl/tests/airflow_ctl/api/test_operations.py
+++ b/airflow-ctl/tests/airflow_ctl/api/test_operations.py
@@ -1129,6 +1129,11 @@ class TestJobsOperations:
def test_list(self):
def handle_request(request: httpx.Request) -> httpx.Response:
assert request.url.path == "/api/v2/jobs"
+ params = dict(request.url.params)
+ assert params["job_type"] == "job_type"
+ assert params["hostname"] == "hostname"
+ assert params["is_alive"] == "true"
+ assert params["limit"] == "50"
return httpx.Response(200,
json=json.loads(self.job_collection_response.model_dump_json()))
client = make_api_client(transport=httpx.MockTransport(handle_request))
@@ -1139,6 +1144,32 @@ class TestJobsOperations:
)
assert response == self.job_collection_response
+ @pytest.mark.parametrize(
+ ("job_type", "hostname", "is_alive", "expected_subset"),
+ [
+ (None, None, None, {}),
+ ("scheduler", None, None, {"job_type": "scheduler"}),
+ (None, "host-a", None, {"hostname": "host-a"}),
+ (None, None, False, {"is_alive": "false"}),
+ ],
+ )
+ def test_list_omits_empty_filters(self, job_type, hostname, is_alive,
expected_subset):
+ def handle_request(request: httpx.Request) -> httpx.Response:
+ assert request.url.path == "/api/v2/jobs"
+ params = dict(request.url.params)
+ assert params["limit"] == "50"
+ for key, value in expected_subset.items():
+ assert params[key] == value
+
+ assert ("job_type" in params) is ("job_type" in expected_subset)
+ assert ("hostname" in params) is ("hostname" in expected_subset)
+ assert ("is_alive" in params) is ("is_alive" in expected_subset)
+ return httpx.Response(200,
json=json.loads(self.job_collection_response.model_dump_json()))
+
+ client = make_api_client(transport=httpx.MockTransport(handle_request))
+ response = client.jobs.list(job_type=job_type, hostname=hostname,
is_alive=is_alive)
+ assert response == self.job_collection_response
+
class TestPoolsOperations:
pool_name = "pool_name"
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 56d3ddcd64c..2525b85e49f 100644
--- a/airflow-ctl/tests/airflow_ctl/ctl/test_cli_config.py
+++ b/airflow-ctl/tests/airflow_ctl/ctl/test_cli_config.py
@@ -287,6 +287,36 @@ class TestCommandFactory:
assert arg.kwargs["default"] == test_arg[1]["default"]
assert arg.kwargs["type"] == test_arg[1]["type"]
+ def test_command_factory_optional_bool_uses_boolean_optional_action(self):
+ """Optional bool parameters should support --flag and --no-flag
forms."""
+ temp_file = "test_command.py"
+ self._save_temp_operations_py(
+ temp_file=temp_file,
+ file_content="""
+ class JobsOperations(BaseOperations):
+ def list(self, is_alive: bool | None = None) ->
JobCollectionResponse | ServerResponseError:
+ self.response = self.client.get("jobs")
+ return
JobCollectionResponse.model_validate_json(self.response.content)
+ """,
+ )
+
+ command_factory = CommandFactory(file_path=temp_file)
+ generated_group_commands = command_factory.group_commands
+
+ jobs_list_args = []
+ for generated_group_command in generated_group_commands:
+ if generated_group_command.name != "jobs":
+ continue
+ for sub_command in generated_group_command.subcommands:
+ if sub_command.name == "list":
+ jobs_list_args = list(sub_command.args)
+ break
+
+ is_alive_arg = next(arg for arg in jobs_list_args if arg.flags ==
("--is-alive",))
+ assert is_alive_arg.kwargs["action"] == BooleanOptionalAction
+ assert is_alive_arg.kwargs["default"] is None
+ assert is_alive_arg.kwargs["type"] is bool
+
class TestCliConfigMethods:
def test_add_to_parser_drops_type_for_boolean_optional_action(self):