Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/agent_env/artifact/universe.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from __future__ import annotations

from typing import ClassVar
from typing import Any, ClassVar

from agent_env.artifact.artifact import Artifact

Expand All @@ -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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
Expand Down
2 changes: 1 addition & 1 deletion src/agent_env/task_step/task_step.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
25 changes: 25 additions & 0 deletions tst/unit/test_subclass_hooks_are_annotated.py
Original file line number Diff line number Diff line change
@@ -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 == {}
Loading