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

bugraoz 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 6ef5165a2b6 fix(cli): change is_alive default to None in jobs list 
(#65065)
6ef5165a2b6 is described below

commit 6ef5165a2b6dbd6e06f33dee6ffc953331955c7c
Author: rjgoyln <[email protected]>
AuthorDate: Sun Apr 12 23:09:39 2026 +0800

    fix(cli): change is_alive default to None in jobs list (#65065)
---
 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 a2b2b43ab2f..3ce196c10cb 100644
--- a/airflow-ctl/src/airflowctl/api/operations.py
+++ b/airflow-ctl/src/airflowctl/api/operations.py
@@ -646,10 +646,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 3e3d6a4b3c4..466ee671b61 100755
--- a/airflow-ctl/src/airflowctl/ctl/cli_config.py
+++ b/airflow-ctl/src/airflowctl/ctl/cli_config.py
@@ -580,14 +580,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 c7b60be3c5c..aa559f17421 100644
--- a/airflow-ctl/tests/airflow_ctl/api/test_operations.py
+++ b/airflow-ctl/tests/airflow_ctl/api/test_operations.py
@@ -1158,6 +1158,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))
@@ -1168,6 +1173,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 ebcbe1c0c77..e0278cd7c53 100644
--- a/airflow-ctl/tests/airflow_ctl/ctl/test_cli_config.py
+++ b/airflow-ctl/tests/airflow_ctl/ctl/test_cli_config.py
@@ -296,6 +296,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:
     @pytest.mark.parametrize(

Reply via email to