Commit a763ef605d for bind

commit a763ef605d804ae8f88dad640739d0621094abd8
Author: Štěpán Balážik <stepan@isc.org>
Date:   Tue Sep 8 19:34:19 2026 +0200

    Find a leaf matcher inside a combination

    A handler built on `Qname(...) & Qtype(...)` has no way to read the
    names or types it was declared with back, short of repeating them.
    of() finds the one matcher of a given class in a combination, typed
    as that class.

    Assisted-by: Claude:claude-fable-5

diff --git a/bin/tests/system/isctest/asyncserver/matchers.py b/bin/tests/system/isctest/asyncserver/matchers.py
index c9a62f8983..6e9c597a69 100644
--- a/bin/tests/system/isctest/asyncserver/matchers.py
+++ b/bin/tests/system/isctest/asyncserver/matchers.py
@@ -9,6 +9,9 @@
 # See the COPYRIGHT file distributed with this work for additional
 # information regarding copyright ownership.

+from collections.abc import Iterator
+from typing import TypeVar
+
 import abc

 import dns.name
@@ -16,6 +19,8 @@ import dns.rdatatype

 from .context import QueryContext

+M = TypeVar("M", bound="Matcher")
+

 class Matcher(abc.ABC):
     """
@@ -41,6 +46,22 @@ class Matcher(abc.ABC):
     def __invert__(self) -> "Matcher":
         return Not(self)

+    def of(self, cls: type[M]) -> M:
+        """
+        The one matcher of class `cls` this matcher is built from, for a
+        handler to read what it was declared with: `matcher.of(Qname).qnames`.
+        """
+        found = [m for m in self.leaves() if isinstance(m, cls)]
+        assert len(found) == 1, f"{self} has {len(found)} {cls.__name__} matchers"
+        return found[0]
+
+    def leaves(self) -> Iterator["Matcher"]:
+        """
+        The matchers this one is built from; a matcher built from nothing
+        yields itself.
+        """
+        yield self
+
     def __str__(self) -> str:
         return f"{self.__class__.__name__}()"

@@ -55,6 +76,10 @@ class _Combinator(Matcher):
     def __init__(self, *matchers: Matcher) -> None:
         self._matchers = matchers

+    def leaves(self) -> Iterator[Matcher]:
+        for matcher in self._matchers:
+            yield from matcher.leaves()
+
     def __str__(self) -> str:
         return f"({self._SEPARATOR.join(str(m) for m in self._matchers)})"

@@ -95,6 +120,9 @@ class Not(Matcher):
     def __init__(self, matcher: Matcher) -> None:
         self._matcher = matcher

+    def leaves(self) -> Iterator[Matcher]:
+        yield from self._matcher.leaves()
+
     def match(self, qctx: QueryContext) -> bool:
         return not self._matcher.match(qctx)