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

potiuk 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 17d887c8722 Make airflowctl command generation safe to read more than 
once (#73097)
17d887c8722 is described below

commit 17d887c87229f3fbae8dcca33b266ae24af514e1
Author: Y-C <[email protected]>
AuthorDate: Mon Sep 14 22:04:52 2026 +0800

    Make airflowctl command generation safe to read more than once (#73097)
    
    The generated command tree is exposed through a module-level CommandFactory
    singleton that anyone can import, so a second read of the property is a
    reachable state rather than a hypothetical one. Today only the single
    import-time access exists, which is why nothing is visibly broken; the most
    likely way in is a routine test refactor that shares one factory instance
    across cases, which would then fail in a way that points nowhere near the
    cause.
    
    Co-authored-by: Eason09053360 
<[email protected]>
---
 airflow-ctl/src/airflowctl/ctl/cli_config.py         | 12 +++++++++---
 airflow-ctl/tests/airflow_ctl/ctl/test_cli_config.py | 13 +++++++++++++
 2 files changed, 22 insertions(+), 3 deletions(-)

diff --git a/airflow-ctl/src/airflowctl/ctl/cli_config.py 
b/airflow-ctl/src/airflowctl/ctl/cli_config.py
index 4a385537eb1..3603e0cc71d 100755
--- a/airflow-ctl/src/airflowctl/ctl/cli_config.py
+++ b/airflow-ctl/src/airflowctl/ctl/cli_config.py
@@ -30,7 +30,7 @@ import sys
 from argparse import Namespace
 from collections.abc import Callable, Iterable
 from enum import Enum
-from functools import partial
+from functools import cached_property, partial
 from pathlib import Path
 from typing import Any, NamedTuple
 
@@ -959,9 +959,15 @@ class CommandFactory:
                 )
             )
 
-    @property
+    @cached_property
     def group_commands(self) -> list[CLICommand]:
-        """List of GroupCommands generated for airflowctl."""
+        """
+        List of GroupCommands generated for airflowctl.
+
+        Cached because the builders below append to ``self.operations`` /
+        ``self.commands_map`` / ``self.group_commands_list``: recomputing would
+        duplicate every group and subcommand instead of replacing them.
+        """
         self._inspect_operations()
         self._create_args_map_from_operation()
         self._create_func_map_from_operation()
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 8eed0b8108f..6cb2fb19192 100644
--- a/airflow-ctl/tests/airflow_ctl/ctl/test_cli_config.py
+++ b/airflow-ctl/tests/airflow_ctl/ctl/test_cli_config.py
@@ -341,6 +341,19 @@ class TestCommandFactory:
 
         assert parsed_conf == {"my-key": "my-value"}
 
+    def test_group_commands_is_stable_across_repeated_access(self):
+        """Reading ``group_commands`` twice must not duplicate groups or 
subcommands."""
+        command_factory = CommandFactory()
+
+        # Snapshot the names and sizes rather than the list itself: both 
accesses
+        # hand back the same object, so only values captured before the second
+        # access can witness it mutating them.
+        first = [(group.name, len(group.subcommands)) for group in 
command_factory.group_commands]
+        second = [(group.name, len(group.subcommands)) for group in 
command_factory.group_commands]
+
+        assert second == first
+        assert len(second) == len({name for name, _ in second})
+
     def test_command_factory_parses_comma_separated_list_fields(self):
         """List fields should parse comma-separated CLI values as whole 
items."""
         command_factory = CommandFactory()

Reply via email to