Skip to content

Commit 30d1910

Browse files
google-genai-botcopybara-github
authored andcommitted
fix(utils): Preserve decorated type for @experimental and @working_in_progress
Merge google#6030 ## Summary The feature decorators (`@experimental`, `@working_in_progress`) were typed as returning `Any`, so any class or function they decorated (e.g. `BaseEnvironment`) was seen as `Any` by type checkers. This erased the base class, producing spurious `"no base method present"` errors on every `@override` in subclasses (`LocalEnvironment`, and any future subclass). This adds a `_FeatureDecorator` `Protocol` with `@overload` signatures so the decorators preserve the decorated object's type across all three call forms — `@experimental`, `@experimental()`, `@experimental("msg")` — for both classes and functions. ## Impact - `BaseEnvironment` now resolves as `type[BaseEnvironment]` instead of `Any`. - `LocalEnvironment`'s 6 spurious `@override` errors drop to 0 (verified with pyright). - Runtime behavior is unchanged: warnings still fire, class names and callability are preserved. ## Test plan - [x] `pytest tests/unittests/utils/test_feature_decorator.py` (17 passed) - [x] `pytest tests/unittests/features/test_feature_decorator.py` (11 passed) - [x] `pyright src/google/adk/utils/feature_decorator.py` — 0 errors - [x] Verified `LocalEnvironment` override errors went 6 → 0 PiperOrigin-RevId: 929389871
1 parent d72bb7d commit 30d1910

3 files changed

Lines changed: 10 additions & 47 deletions

File tree

src/google/adk/environment/_base_environment.py

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -58,17 +58,6 @@ class BaseEnvironment(ABC):
5858
4. Call ``close()`` when done.
5959
"""
6060

61-
_is_initialized: bool = False
62-
63-
@property
64-
def is_initialized(self) -> bool:
65-
"""Whether the environment has been initialized."""
66-
return self._is_initialized
67-
68-
@is_initialized.setter
69-
def is_initialized(self, value: bool) -> None:
70-
self._is_initialized = value
71-
7261
async def initialize(self) -> None:
7362
"""Initialize the environment (e.g. create working directory).
7463

src/google/adk/environment/_local_environment.py

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,6 @@ def __init__(
5959
self._working_dir = working_dir
6060
self._env_vars = env_vars
6161
self._auto_created = False
62-
self._is_initialized = False
6362

6463
@property
6564
@override
@@ -76,15 +75,13 @@ async def initialize(self) -> None:
7675
logger.debug('Created temporary folder: %s', self._working_dir)
7776
else:
7877
os.makedirs(self._working_dir, exist_ok=True)
79-
self._is_initialized = True
8078

8179
@override
8280
async def close(self) -> None:
8381
if self._auto_created and self._working_dir:
8482
shutil.rmtree(self._working_dir, ignore_errors=True)
8583
logger.debug('Removed temporary workspace: %s', self._working_dir)
8684
self._working_dir = None
87-
self._is_initialized = False
8885

8986
@override
9087
async def execute(
@@ -141,21 +138,21 @@ async def write_file(self, path: str | Path, content: str | bytes) -> None:
141138
resolved = self._resolve_path(path)
142139
return await asyncio.to_thread(self._sync_write, resolved, content)
143140

144-
def _resolve_path(self, path: str | Path) -> Path:
141+
def _resolve_path(self, path: str | Path) -> str:
145142
"""Resolve a relative path against the working directory."""
146-
path_obj = Path(path)
147-
if path_obj.is_absolute():
148-
return path_obj
149-
return self.working_dir / path_obj
143+
path = str(path)
144+
if os.path.isabs(path):
145+
return path
146+
return os.path.join(self._working_dir, path)
150147

151148
@staticmethod
152-
def _sync_read(path: Path) -> bytes:
149+
def _sync_read(path: str) -> bytes:
153150
with open(path, 'rb') as f:
154151
return f.read()
155152

156153
@staticmethod
157-
def _sync_write(path: Path, content: str | bytes) -> None:
158-
os.makedirs(path.parent, exist_ok=True)
154+
def _sync_write(path: str, content: str | bytes) -> None:
155+
os.makedirs(os.path.dirname(path), exist_ok=True)
159156
mode = 'w' if isinstance(content, str) else 'wb'
160157
kwargs = {'encoding': 'utf-8'} if isinstance(content, str) else {}
161158
with open(path, mode, **kwargs) as f:

src/google/adk/utils/feature_decorator.py

Lines changed: 2 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -20,35 +20,12 @@
2020
from typing import Any
2121
from typing import cast
2222
from typing import Optional
23-
from typing import overload
24-
from typing import Protocol
2523
from typing import TypeVar
2624
import warnings
2725

2826
T = TypeVar("T")
2927

3028

31-
class _FeatureDecorator(Protocol):
32-
"""A feature decorator usable with or without a message argument.
33-
34-
Preserves the decorated object's type so that subclasses and type
35-
checkers continue to see the real class/function rather than ``Any``.
36-
"""
37-
38-
# @decorator (bare, on a class or function)
39-
@overload
40-
def __call__(self, message_or_obj: T) -> T:
41-
...
42-
43-
# @decorator() or @decorator("message")
44-
@overload
45-
def __call__(self, message_or_obj: Optional[str] = ...) -> Callable[[T], T]:
46-
...
47-
48-
def __call__(self, message_or_obj: Any = None) -> Any:
49-
...
50-
51-
5229
def _is_truthy_env(var_name: str) -> bool:
5330
value = os.environ.get(var_name)
5431
if value is None:
@@ -62,7 +39,7 @@ def _make_feature_decorator(
6239
default_message: str,
6340
block_usage: bool = False,
6441
bypass_env_var: Optional[str] = None,
65-
) -> _FeatureDecorator:
42+
) -> Callable[..., Any]:
6643
def decorator_factory(message_or_obj: Any = None) -> Any:
6744
# Case 1: Used as @decorator without parentheses
6845
# message_or_obj is the decorated class/function
@@ -80,7 +57,7 @@ def decorator_factory(message_or_obj: Any = None) -> Any:
8057
)
8158
return _create_decorator(message, label, block_usage, bypass_env_var)
8259

83-
return cast(_FeatureDecorator, decorator_factory)
60+
return decorator_factory
8461

8562

8663
def _create_decorator(

0 commit comments

Comments
 (0)