Skip to content

Commit 595bedf

Browse files
bokelleyclaude
andcommitted
chore(server): apply prep-PR review feedback (P1 polish)
Code-reviewer feedback on PR #318 surfaced four items, all P1 polish (no functional changes): - _BROAD_SURFACE_BASES named set in serve.py replacing inline base.__name__ != "ADCPHandler" check. Future broad bases get added in one place. - stacklevel=4 documented with the explicit call chain (warn → _warn_if_unregistered_subclass → _log_advertised_tools → _register_handler_tools → operator's serve()). - 3 missing edge-case tests added: - test_init_subclass_three_level_chain_with_intermediate_declaration - test_init_subclass_forwards_kwargs_to_super (covers super() chain integrity in multi-inheritance — bystander base receives its __init_subclass__ kwargs through ADCPHandler's chain) - test_register_handler_tools_accepts_empty_iterable (pins the intended "zero AdCP tool surface" semantics) Test count: 16 in test_register_handler_tools.py; 29 across the prep PR test surface. Full suite: 2396 passed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 2026242 commit 595bedf

2 files changed

Lines changed: 88 additions & 12 deletions

File tree

src/adcp/server/serve.py

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -235,18 +235,29 @@ class introduces a new specialism (a custom subclass that's not in
235235
_warn_if_unregistered_subclass(handler, advertise_all=advertise_all)
236236

237237

238+
#: Bases whose tool set is broad-by-design — when an adopter subclass
239+
#: lands on one of these via MRO without registering its own
240+
#: ``advertised_tools``, the result is over-advertisement (the broad
241+
#: base's full set inherited unintentionally). Naming the rule rather
242+
#: than checking ``base.__name__ != "ADCPHandler"`` inline so future
243+
#: broad bases (a hypothetical ``UniversalHandler``) get added to one
244+
#: place — and a reviewer's first question becomes "is this base
245+
#: broad-by-design?" not "what's special about ADCPHandler?".
246+
_BROAD_SURFACE_BASES: frozenset[str] = frozenset({"ADCPHandler"})
247+
248+
238249
def _warn_if_unregistered_subclass(handler: ADCPHandler[Any], *, advertise_all: bool) -> None:
239250
"""Emit a one-time ``UserWarning`` when a custom handler base bypasses
240251
the tool-discovery registry.
241252
242253
The trigger: the concrete handler class itself isn't in
243254
``_HANDLER_TOOLS``, has no ``advertised_tools`` declaration of its
244-
own, and inherits its tool set from ``ADCPHandler`` (the broadest
245-
base) rather than a specialized base like ``GovernanceHandler``.
246-
That combination almost always means the adopter meant to declare a
247-
focused tool set but forgot to register it; the framework
248-
over-advertises by silently falling through to ADCPHandler's full
249-
surface.
255+
own, and inherits its tool set from a broad-surface base (see
256+
:data:`_BROAD_SURFACE_BASES`) rather than a specialized base like
257+
``GovernanceHandler``. That combination almost always means the
258+
adopter meant to declare a focused tool set but forgot to register
259+
it; the framework over-advertises by silently falling through to
260+
the broad base's full surface.
250261
251262
Suppressed when ``advertise_all=True`` — that's the explicit "yes,
252263
advertise everything" opt-in.
@@ -260,15 +271,21 @@ def _warn_if_unregistered_subclass(handler: ADCPHandler[Any], *, advertise_all:
260271
# Should already have been auto-registered via __init_subclass__,
261272
# but defensively skip the warning if the attribute exists.
262273
return
263-
# Walk MRO looking for a specialized SDK base (anything in the
264-
# registry other than ADCPHandler itself). If one is found, the
265-
# adopter is subclassing a focused base and inheriting its tool set
266-
# — that's the documented pattern, no warning needed.
274+
# Walk MRO looking for a specialized (non-broad-surface) SDK base.
275+
# If one is found, the adopter is subclassing a focused base and
276+
# inheriting its tool set — that's the documented pattern, no
277+
# warning needed.
267278
has_specialized_parent = any(
268-
base.__name__ in _HANDLER_TOOLS and base.__name__ != "ADCPHandler" for base in cls.__mro__
279+
base.__name__ in _HANDLER_TOOLS and base.__name__ not in _BROAD_SURFACE_BASES
280+
for base in cls.__mro__
269281
)
270282
if has_specialized_parent:
271283
return
284+
# stacklevel=4 walks: warnings.warn → _warn_if_unregistered_subclass
285+
# → _log_advertised_tools → _register_handler_tools → operator's
286+
# serve()/create_mcp_server()/create_a2a_server() call site. If any
287+
# of those frames are added or removed, recompute or refactor to
288+
# use stacklevel=2 from a thin wrapper.
272289
warnings.warn(
273290
f"Handler class {cls.__name__!r} subclasses ADCPHandler directly "
274291
f"but isn't registered in the framework's tool-discovery "

tests/test_register_handler_tools.py

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
from __future__ import annotations
1919

2020
import warnings
21-
from typing import ClassVar
21+
from typing import Any, ClassVar
2222

2323
import pytest
2424

@@ -149,6 +149,65 @@ class _ChildBase(_ParentBase):
149149
assert "_ChildBase" not in _HANDLER_TOOLS
150150

151151

152+
def test_init_subclass_three_level_chain_with_intermediate_declaration(
153+
cleanup_handler_registry,
154+
):
155+
"""Three-level inheritance where the *intermediate* base declares
156+
``advertised_tools`` registers AT the intermediate level. The leaf
157+
inherits via MRO and isn't separately registered. Regression for
158+
middle-of-the-chain registration."""
159+
160+
class _Root(ADCPHandler):
161+
# No advertised_tools — root level.
162+
pass
163+
164+
class _Intermediate(_Root):
165+
# Declares its own — registers at this level.
166+
advertised_tools: ClassVar[set[str]] = {"get_products", "create_media_buy"}
167+
168+
class _Leaf(_Intermediate):
169+
# Inherits from intermediate; no own declaration.
170+
pass
171+
172+
assert "_Root" not in _HANDLER_TOOLS
173+
assert "_Intermediate" in _HANDLER_TOOLS
174+
assert _HANDLER_TOOLS["_Intermediate"] == {"get_products", "create_media_buy"}
175+
assert "_Leaf" not in _HANDLER_TOOLS
176+
177+
178+
def test_init_subclass_forwards_kwargs_to_super(cleanup_handler_registry):
179+
"""``__init_subclass__`` calls ``super().__init_subclass__(**kwargs)``
180+
so PEP 487-style metaclass kwargs (e.g. from
181+
``__init_subclass__`` declared on a custom metaclass) flow through.
182+
Without this, a future subclass using ``class X(ADCPHandler, mixin_kw=...)``
183+
would lose the kwarg silently. Regression coverage by declaring a
184+
bystander base that inspects ``cls`` on subclass creation."""
185+
seen: list[type] = []
186+
187+
class _Bystander:
188+
def __init_subclass__(cls, **kwargs: Any) -> None:
189+
super().__init_subclass__(**kwargs)
190+
seen.append(cls)
191+
192+
class _MultiInheritHandler(ADCPHandler, _Bystander):
193+
advertised_tools: ClassVar[set[str]] = {"get_products"}
194+
195+
# Bystander's __init_subclass__ saw the new class — proves the
196+
# super() chain is intact even with ADCPHandler.__init_subclass__
197+
# in the chain. Without `super().__init_subclass__(**kwargs)`, the
198+
# bystander would never fire.
199+
assert _MultiInheritHandler in seen
200+
201+
202+
def test_register_handler_tools_accepts_empty_iterable(cleanup_handler_registry):
203+
"""An empty tool set is allowed — represents "this handler claims
204+
zero tool surface." Registering it makes the handler-name known to
205+
the registry without claiming any AdCP verbs. Useful for handlers
206+
that exist purely for typed test-context plumbing."""
207+
register_handler_tools("_EmptySetHandler", set())
208+
assert _HANDLER_TOOLS["_EmptySetHandler"] == set()
209+
210+
152211
def test_init_subclass_idempotent_on_module_reload(cleanup_handler_registry):
153212
"""Re-evaluating the same class body (test reload, ipython rerun)
154213
re-triggers ``__init_subclass__`` with the same tool set — must not

0 commit comments

Comments
 (0)