diff --git a/src/agent_env/artifact/universe.py b/src/agent_env/artifact/universe.py index e9cef1bb..a8fc89ee 100644 --- a/src/agent_env/artifact/universe.py +++ b/src/agent_env/artifact/universe.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import ClassVar +from typing import Any, ClassVar from agent_env.artifact.artifact import Artifact @@ -21,7 +21,7 @@ class Universe(Artifact): # ``type`` literal default (e.g. ``type: Literal["environment_universe"] = "environment_universe"``). __is_abstract__: ClassVar[bool] = True - def __init_subclass__(cls, **kwargs): + def __init_subclass__(cls, **kwargs: Any) -> None: super().__init_subclass__(**kwargs) # A subclass is concrete if its ``type`` field has a non-empty default. type_field = cls.model_fields.get("type") diff --git a/src/agent_env/providers/sandbox_providers/sandbox_provider.py b/src/agent_env/providers/sandbox_providers/sandbox_provider.py index 39854700..32a71655 100644 --- a/src/agent_env/providers/sandbox_providers/sandbox_provider.py +++ b/src/agent_env/providers/sandbox_providers/sandbox_provider.py @@ -187,7 +187,7 @@ def from_config(cls, **config: Any) -> Self: # Whether create_container(private_network=True) puts containers on a network they share. PRIVATE_NETWORK: ClassVar[bool] = False - def __init_subclass__(cls, **kwargs): + def __init_subclass__(cls, **kwargs: Any) -> None: super().__init_subclass__(**kwargs) cls.SUPPORTED_SANDBOX_MODES = frozenset( mode for mode, method in ((SANDBOX_MODE_CONTAINER, "create_sandbox"), (SANDBOX_MODE_VM, "create_vm")) diff --git a/src/agent_env/task_step/task_step.py b/src/agent_env/task_step/task_step.py index 5dae12e8..eb544bbf 100644 --- a/src/agent_env/task_step/task_step.py +++ b/src/agent_env/task_step/task_step.py @@ -90,7 +90,7 @@ class TaskStep(ABC): # Fields holding env, A2A agent or artifact ids; None means undeclared. entity_refs: ClassVar[tuple[EntityRef, ...] | None] = None - def __init_subclass__(cls, **kwargs) -> None: + def __init_subclass__(cls, **kwargs: Any) -> None: super().__init_subclass__(**kwargs) # Never inherited: a subclass may add id fields its parent's declaration misses. if "entity_refs" not in vars(cls): diff --git a/tst/unit/test_subclass_hooks_are_annotated.py b/tst/unit/test_subclass_hooks_are_annotated.py new file mode 100644 index 00000000..2e521f51 --- /dev/null +++ b/tst/unit/test_subclass_hooks_are_annotated.py @@ -0,0 +1,25 @@ +"""agent_env ships py.typed, so a consumer's type checker checks its subclasses of agent_env's classes, and defining one calls +the base's ``__init_subclass__``, which a strict checker refuses to call when it's untyped (``no-untyped-call``). Every such +hook is annotated.""" + +import ast +from pathlib import Path + +SRC = Path(__file__).resolve().parents[2] / "src" / "agent_env" + + +def _unannotated(hook: ast.FunctionDef) -> list[str]: + args = hook.args + after_cls = [*args.posonlyargs, *args.args][1:] + params = [*after_cls, *args.kwonlyargs, *filter(None, (args.vararg, args.kwarg))] + return [param.arg for param in params if param.annotation is None] + (["return"] if hook.returns is None else []) + + +def test_every_init_subclass_hook_is_annotated(): + unannotated = { + f"{path.relative_to(SRC)}:{node.lineno}": missing + for path in sorted(SRC.rglob("*.py")) + for node in ast.walk(ast.parse(path.read_text())) + if isinstance(node, ast.FunctionDef) and node.name == "__init_subclass__" and (missing := _unannotated(node)) + } + assert unannotated == {}