From 7fd9de10b0eedda42b9b973a91eb74b1dcc3282f Mon Sep 17 00:00:00 2001 From: Edgar Arakelyan Date: Sun, 11 Oct 2026 19:04:14 -0700 Subject: [PATCH 1/2] fix(typing): annotate the __init_subclass__ hooks, which strict type checkers refuse to call untyped agent_env ships py.typed, so a downstream package's type checker checks its subclasses of agent_env's classes, and defining a subclass calls the base's __init_subclass__. SandboxProvider's (new in 0.9.1312) and Universe's had no annotations, so under mypy --strict every subclass of them fails with no-untyped-call; TaskStep's left **kwargs unannotated. All three are now `__init_subclass__(cls, **kwargs: Any) -> None`, and a unit test keeps every such hook annotated. Co-Authored-By: Claude Opus 5.5 (1M context) --- src/agent_env/artifact/universe.py | 4 ++-- .../sandbox_providers/sandbox_provider.py | 2 +- src/agent_env/task_step/task_step.py | 2 +- tst/unit/test_subclass_hooks_are_annotated.py | 24 +++++++++++++++++++ 4 files changed, 28 insertions(+), 4 deletions(-) create mode 100644 tst/unit/test_subclass_hooks_are_annotated.py 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..52b508b8 --- /dev/null +++ b/tst/unit/test_subclass_hooks_are_annotated.py @@ -0,0 +1,24 @@ +"""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 + params = [*args.posonlyargs, *args.args[1:], *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 == {} From bcfb353148172bf38c9883a7e2b21efc6b744591 Mon Sep 17 00:00:00 2001 From: Edgar Arakelyan Date: Sun, 11 Oct 2026 19:15:27 -0700 Subject: [PATCH 2/2] test(typing): the hook test skips cls whether or not it's positional-only Co-Authored-By: Claude Opus 5.5 (1M context) --- tst/unit/test_subclass_hooks_are_annotated.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tst/unit/test_subclass_hooks_are_annotated.py b/tst/unit/test_subclass_hooks_are_annotated.py index 52b508b8..2e521f51 100644 --- a/tst/unit/test_subclass_hooks_are_annotated.py +++ b/tst/unit/test_subclass_hooks_are_annotated.py @@ -10,7 +10,8 @@ def _unannotated(hook: ast.FunctionDef) -> list[str]: args = hook.args - params = [*args.posonlyargs, *args.args[1:], *args.kwonlyargs, *filter(None, (args.vararg, args.kwarg))] + 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 [])