fix(workflow): declare abc.ABC on BaseNode so abstract subclasses type-check
Co-authored-by: George Weale <gweale@google.com> PiperOrigin-RevId: 967414592
This commit is contained in:
committed by
Copybara-Service
parent
51231cd4ac
commit
f3b59fd62e
@@ -14,6 +14,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import abc
|
||||
from collections.abc import AsyncGenerator
|
||||
from typing import Any
|
||||
from typing import final
|
||||
@@ -33,7 +34,13 @@ if TYPE_CHECKING:
|
||||
from ..events.event import Event
|
||||
|
||||
|
||||
class BaseNode(BaseModel):
|
||||
# The `abc.ABC` base is load-bearing and must not be dropped. Subclasses
|
||||
# declare `@abc.abstractmethod` without inheriting `abc.ABC` themselves, relying
|
||||
# on this class for the metaclass. Static type checkers do not see `ABCMeta`
|
||||
# through pydantic's `ModelMetaclass`, so without an explicit base they treat
|
||||
# those subclasses as concrete and report the abstract methods as returning
|
||||
# `None`.
|
||||
class BaseNode(BaseModel, abc.ABC):
|
||||
"""A base class for all nodes in the workflow graph."""
|
||||
|
||||
model_config = ConfigDict(arbitrary_types_allowed=True)
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
"""Testings for the BaseAgent."""
|
||||
|
||||
import abc
|
||||
from enum import Enum
|
||||
from functools import partial
|
||||
import logging
|
||||
@@ -27,11 +28,13 @@ from google.adk.agents.base_agent import BaseAgent
|
||||
from google.adk.agents.base_agent import BaseAgentState
|
||||
from google.adk.agents.callback_context import CallbackContext
|
||||
from google.adk.agents.invocation_context import InvocationContext
|
||||
from google.adk.agents.llm_agent import LlmAgent
|
||||
from google.adk.apps.app import ResumabilityConfig
|
||||
from google.adk.events.event import Event
|
||||
from google.adk.plugins.base_plugin import BasePlugin
|
||||
from google.adk.plugins.plugin_manager import PluginManager
|
||||
from google.adk.sessions.in_memory_session_service import InMemorySessionService
|
||||
from google.adk.workflow import BaseNode
|
||||
from google.genai import types
|
||||
import pytest
|
||||
import pytest_mock
|
||||
@@ -973,6 +976,32 @@ def test_validate_sub_agents_unique_names_empty_list(
|
||||
assert len(parent.sub_agents) == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize('agent_class', [BaseNode, BaseAgent, LlmAgent])
|
||||
def test_agent_classes_are_abstract(agent_class: type[BaseNode]):
|
||||
"""Each class reaches `abc.ABC` through an explicitly declared base.
|
||||
|
||||
Subclasses declare `@abc.abstractmethod` without inheriting `abc.ABC`
|
||||
themselves. Static type checkers only honor that when a base says `abc.ABC`
|
||||
in source, because they do not see `ABCMeta` through pydantic's
|
||||
`ModelMetaclass`. Losing it makes those subclasses look concrete and their
|
||||
abstract methods look like they return `None`.
|
||||
"""
|
||||
assert issubclass(agent_class, abc.ABC)
|
||||
|
||||
|
||||
def test_abstract_subclass_cannot_be_instantiated():
|
||||
"""A subclass that leaves an abstract method unimplemented still raises."""
|
||||
|
||||
class _Incomplete(LlmAgent):
|
||||
|
||||
@abc.abstractmethod
|
||||
def summarize(self) -> str:
|
||||
"""Left unimplemented on purpose."""
|
||||
|
||||
with pytest.raises(TypeError, match='abstract'):
|
||||
_Incomplete(name='incomplete')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
pytest.main([__file__])
|
||||
|
||||
|
||||
Reference in New Issue
Block a user