Skip to content

Commit 2d5a708

Browse files
committed
Fix brittle imports and cycles after moving atomic_write_text to filesystem.py
- callers (Edit/Insert/Write/diff applier) import atomic_write_text lazily from .filesystem to avoid the filesystem<->tool-module cycle - drop now-unused imports from base.py, add stat/uuid/contextlib to filesystem.py where the helper now lives - update the helper docstring and filesystem module doc to match the new home; re-export via __all__ - tests import atomic_write_text from tools.filesystem
1 parent 321afa0 commit 2d5a708

7 files changed

Lines changed: 33 additions & 24 deletions

File tree

‎python_agent_harness/tools/base.py‎

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,8 @@
22

33
from __future__ import annotations
44

5-
import contextlib
6-
import os
7-
import stat
85
import threading
9-
import uuid
106
from abc import ABC, abstractmethod
11-
from pathlib import Path
127
from typing import Any, Protocol, runtime_checkable
138

149
from ..core.models import ToolSpec

‎python_agent_harness/tools/diffapply.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,6 @@
2626
import os
2727
import re
2828

29-
from .base import atomic_write_text
30-
3129
_HUNK_HEADER_RE = re.compile(r"^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@")
3230
_FILE_OLD_RE = re.compile(r"^---[ \t]")
3331
_FILE_NEW_RE = re.compile(r"^\+\+\+[ \t]")
@@ -257,6 +255,8 @@ def _apply_section(section: _Section, cwd: str, fallback_path: str | None) -> tu
257255
ending = _file_line_ending(file_lines)
258256
for hunk, pos in reversed(plan): # bottom-up: earlier positions stay valid
259257
_apply_hunk(hunk, pos, new_lines, ending)
258+
from .filesystem import atomic_write_text
259+
260260
try:
261261
atomic_write_text(target, "".join(new_lines))
262262
except OSError as e:

‎python_agent_harness/tools/edit.py‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
import subprocess
1717

1818
from ..io.diffrender import unified_diff
19-
from .base import Tool, ToolContext, atomic_write_text
19+
from .base import Tool, ToolContext
2020

2121
# Any line ending, as a single compiled pattern (CRLF first so a CRLF is
2222
# never rewritten as two endings).
@@ -168,6 +168,8 @@ def _string_replace(self, path: str, old: str | None, new_str: str, ctx: ToolCon
168168
"for the replacement, or a unified diff"
169169
)
170170
new = content.replace(old, new_str, 1)
171+
from .filesystem import atomic_write_text
172+
171173
try:
172174
atomic_write_text(path, new)
173175
except OSError as e:

‎python_agent_harness/tools/filesystem.py‎

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
"""Filesystem tool helpers and compatibility re-exports.
22
33
This module hosts the SHARED helper machinery for the filesystem
4-
tools — spooling oversized tool results to temp files
5-
(`gptel-agent--truncate-buffer` parity), git-root detection, and the
6-
``natnump`` predicate — and re-exports the tool classes that live in
7-
per-tool modules (`read.py`, `glob.py`, `grep.py`, `edit.py`,
8-
`write.py`, `insert.py`, `mkdir.py`), so existing imports such as
9-
``from .tools.filesystem import Read`` keep working.
4+
tools — atomic file replacement (``atomic_write_text``), spooling
5+
oversized tool results to temp files (`gptel-agent--truncate-buffer`
6+
parity), git-root detection, and the ``natnump`` predicate — and
7+
re-exports the tool classes that live in per-tool modules (`read.py`,
8+
`glob.py`, `grep.py`, `edit.py`, `write.py`, `insert.py`, `mkdir.py`),
9+
so existing imports such as ``from .tools.filesystem import Read``
10+
keep working.
1011
1112
The helpers must stay defined HERE (not in a separate ``_common``
1213
module): tests monkey-patch ``filesystem._spool_dir`` and read
@@ -31,13 +32,16 @@
3132

3233
from __future__ import annotations
3334

35+
import contextlib
3436
import os
3537
import re
3638
import shutil # noqa: F401 (mock target for tests)
39+
import stat
3740
import subprocess # noqa: F401 (mock target for tests)
3841
import tempfile
3942
import threading
4043
import time
44+
import uuid
4145
from collections.abc import Callable, Iterator
4246
from pathlib import Path
4347
from typing import TypeGuard
@@ -232,13 +236,15 @@ def atomic_write_text(path: str, content: str) -> None:
232236
"""Replace PATH's contents with CONTENT atomically.
233237
234238
THE single write path for every tool that rewrites a file (``Edit``,
235-
``Insert``, ``Write``, and the pure-Python diff applier). It lives
236-
here, in the module every tool already imports, so the four callers
237-
cannot drift apart -- and because ``base`` imports nothing from
238-
``tools``, putting it here is also the only placement that avoids a
239-
circular import: ``filesystem.py`` re-imports ``edit``/``write``/
240-
``insert`` at its bottom, so a helper defined *there* would make
241-
``edit`` import a half-initialised ``filesystem``.
239+
``Insert``, ``Write``, and the pure-Python diff applier), so the four
240+
callers cannot drift apart. It lives in this module with the other
241+
shared helpers, and the four callers import it LAZILY (inside the
242+
method that writes) rather than at module level, because this module
243+
re-imports ``edit``/``write``/``insert``/``diffapply`` at its bottom
244+
for the compatibility re-exports: a module-level import here would
245+
close the cycle on a half-initialised module (``tools/__init__``
246+
loads ``edit`` first, whose import of this module would then ask the
247+
half-initialised ``edit`` for ``Edit``).
242248
243249
A plain ``open(path, "w")`` TRUNCATES the file before writing, so a
244250
write that fails partway through -- ENOSPC, a quota, an I/O error,
@@ -345,6 +351,7 @@ def atomic_write_text(path: str, content: str) -> None:
345351
"MAX_OUTPUT",
346352
"READ_SIZE_LIMIT",
347353
"SPOOL_LINES",
354+
"atomic_write_text",
348355
"cleanup_spooled_files",
349356
"_fix_patch_headers",
350357
"_git_glob_results",

‎python_agent_harness/tools/insert.py‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import os
1010

1111
from ..io.diffrender import unified_diff
12-
from .base import Tool, ToolContext, atomic_write_text
12+
from .base import Tool, ToolContext
1313
from .edit import _to_crlf, _uses_crlf
1414

1515

@@ -77,6 +77,8 @@ def run(self, args: dict, ctx: ToolContext) -> str:
7777
else:
7878
lines.insert(ln, new_str)
7979
new_content = "".join(lines)
80+
from .filesystem import atomic_write_text
81+
8082
try:
8183
atomic_write_text(path, new_content)
8284
except OSError as e:

‎python_agent_harness/tools/write.py‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import os
66

77
from ..io.diffrender import unified_diff
8-
from .base import Tool, ToolContext, atomic_write_text
8+
from .base import Tool, ToolContext
99

1010

1111
class Write(Tool):
@@ -82,6 +82,8 @@ def run(self, args: dict, ctx: ToolContext) -> str:
8282
# mirrors the read side; both together keep Write's behavior
8383
# identical on every platform. Both are handled by
8484
# atomic_write_text, which also makes the overwrite crash-safe.
85+
from .filesystem import atomic_write_text
86+
8587
atomic_write_text(path, content)
8688
except OSError as e:
8789
return f"Error: {e}"

‎tests/tools/test_filesystem.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from pathlib import Path
1515
from unittest import mock
1616

17-
from python_agent_harness.tools.base import ToolContext, ToolRuntime, atomic_write_text
17+
from python_agent_harness.tools.base import ToolContext, ToolRuntime
1818
from python_agent_harness.tools.diffapply import apply_unified_diff, diff_targets
1919
from python_agent_harness.tools.edit_mac import EditMac
2020
from python_agent_harness.tools.edit_win import EditWindows
@@ -28,6 +28,7 @@
2828
Write,
2929
_fix_patch_headers,
3030
_strip_diff_fence,
31+
atomic_write_text,
3132
)
3233
from python_agent_harness.tools.glob_mac import GlobMac
3334
from python_agent_harness.tools.glob_win import GlobWindows

0 commit comments

Comments
 (0)