Skip to content

Commit 782796f

Browse files
google-genai-botcopybara-github
authored andcommitted
fix: read_file/write_file path type mismatch in BaseEnvironment and LocalEnvironment
Widen the path parameter from Path to str | Path in BaseEnvironment.read_file and BaseEnvironment.write_file so that LocalEnvironment's str override no longer violates Liskov substitution. Update LocalEnvironment to match, normalizing via str() in _resolve_path. PiperOrigin-RevId: 901415944
1 parent b580891 commit 782796f

3 files changed

Lines changed: 104 additions & 7 deletions

File tree

src/google/adk/environment/_local_environment.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -123,23 +123,24 @@ async def execute(
123123
)
124124

125125
@override
126-
async def read_file(self, path: str) -> bytes:
126+
async def read_file(self, path: str | Path) -> bytes:
127127
if self._working_dir is None:
128128
raise RuntimeError('`working_dir` is not set. Call initialize() first.')
129129

130-
path = self._resolve_path(path)
131-
return await asyncio.to_thread(self._sync_read, path)
130+
resolved = self._resolve_path(path)
131+
return await asyncio.to_thread(self._sync_read, resolved)
132132

133133
@override
134-
async def write_file(self, path: str, content: str | bytes) -> None:
134+
async def write_file(self, path: str | Path, content: str | bytes) -> None:
135135
if self._working_dir is None:
136136
raise RuntimeError('`working_dir` is not set. Call initialize() first.')
137137

138-
path = self._resolve_path(path)
139-
return await asyncio.to_thread(self._sync_write, path, content)
138+
resolved = self._resolve_path(path)
139+
return await asyncio.to_thread(self._sync_write, resolved, content)
140140

141-
def _resolve_path(self, path: str) -> str:
141+
def _resolve_path(self, path: str | Path) -> str:
142142
"""Resolve a relative path against the working directory."""
143+
path = str(path)
143144
if os.path.isabs(path):
144145
return path
145146
return os.path.join(self._working_dir, path)

tests/unittests/tools/BUILD

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,19 @@ package(
55
default_visibility = ["//visibility:private"],
66
)
77

8+
pytest_test(
9+
name = "test_local_environment",
10+
srcs = ["test_local_environment.py"],
11+
args = [
12+
"-p",
13+
"pytest_asyncio.plugin",
14+
],
15+
deps = [
16+
"//third_party/py/google/adk",
17+
"//third_party/py/pytest_asyncio",
18+
],
19+
)
20+
821
pytest_test(
922
name = "test_skill_toolset",
1023
srcs = ["test_skill_toolset.py"],
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# Copyright 2026 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Tests for LocalEnvironment.read_file and write_file."""
16+
17+
from pathlib import Path
18+
19+
from google.adk.environment._local_environment import LocalEnvironment
20+
import pytest
21+
import pytest_asyncio
22+
23+
24+
@pytest_asyncio.fixture(name="env")
25+
async def _env(tmp_path: Path):
26+
"""Create and initialize a LocalEnvironment backed by a temp directory."""
27+
environment = LocalEnvironment(working_dir=tmp_path)
28+
await environment.initialize()
29+
yield environment
30+
await environment.close()
31+
32+
33+
class TestReadFileWriteFile:
34+
"""Verify read_file and write_file accept both str and Path arguments."""
35+
36+
@pytest.mark.asyncio
37+
async def test_write_and_read_with_str(self, env: LocalEnvironment):
38+
"""Round-trip a file using str paths."""
39+
await env.write_file("hello.txt", "hello world")
40+
data = await env.read_file("hello.txt")
41+
assert data == b"hello world"
42+
43+
@pytest.mark.asyncio
44+
async def test_write_and_read_with_path(self, env: LocalEnvironment):
45+
"""Round-trip a file using Path objects."""
46+
await env.write_file(Path("path_obj.txt"), "path content")
47+
data = await env.read_file(Path("path_obj.txt"))
48+
assert data == b"path content"
49+
50+
@pytest.mark.asyncio
51+
async def test_write_str_read_path(self, env: LocalEnvironment):
52+
"""Write with str, read with Path."""
53+
await env.write_file("mixed.txt", "mixed")
54+
data = await env.read_file(Path("mixed.txt"))
55+
assert data == b"mixed"
56+
57+
@pytest.mark.asyncio
58+
async def test_write_path_read_str(self, env: LocalEnvironment):
59+
"""Write with Path, read with str."""
60+
await env.write_file(Path("mixed2.txt"), "mixed2")
61+
data = await env.read_file("mixed2.txt")
62+
assert data == b"mixed2"
63+
64+
@pytest.mark.asyncio
65+
async def test_write_bytes_content(self, env: LocalEnvironment):
66+
"""Write raw bytes and read them back."""
67+
raw = b"\x00\x01\x02\xff"
68+
await env.write_file(Path("binary.bin"), raw)
69+
data = await env.read_file("binary.bin")
70+
assert data == raw
71+
72+
@pytest.mark.asyncio
73+
async def test_write_creates_parent_dirs(self, env: LocalEnvironment):
74+
"""Parent directories are created automatically."""
75+
await env.write_file(Path("sub/dir/file.txt"), "nested")
76+
data = await env.read_file("sub/dir/file.txt")
77+
assert data == b"nested"
78+
79+
@pytest.mark.asyncio
80+
async def test_read_nonexistent_raises(self, env: LocalEnvironment):
81+
"""Reading a missing file raises FileNotFoundError."""
82+
with pytest.raises(FileNotFoundError):
83+
await env.read_file(Path("does_not_exist.txt"))

0 commit comments

Comments
 (0)