mirror of
https://github.com/NCBM/plyngent.git
synced 2026-11-07 07:17:54 +08:00
core/tools: rename grep_files to regex_files (regex pattern)
This commit is contained in:
@@ -332,14 +332,14 @@ def _glob_detail(_args_json: str, result: str) -> str:
|
||||
return _pretty_segments((f"({count} {unit})", None))
|
||||
|
||||
|
||||
def _grep_prefix(args_json: str) -> str:
|
||||
def _regex_prefix(args_json: str) -> str:
|
||||
pattern = _json_str_arg(args_json, "pattern") or "?"
|
||||
path = _json_str_arg(args_json, "path") or "."
|
||||
return _pretty_prefix(f"* Grep '{pattern}' in '{path}' ")
|
||||
|
||||
|
||||
def _grep_detail(_args_json: str, result: str) -> str:
|
||||
"""``grep_files`` outcome: matches and file count, or none/error."""
|
||||
def _regex_detail(_args_json: str, result: str) -> str:
|
||||
"""``regex_files`` outcome: matches and file count, or none/error."""
|
||||
if result.startswith("error:"):
|
||||
return _pretty_segments((f"({result})", "red"))
|
||||
if result == "(no matches)":
|
||||
@@ -802,7 +802,7 @@ _PRETTY_LINES: dict[str, PrettyLine] = {
|
||||
"todo_clear": _todo_line("todo_clear"),
|
||||
"listdir": PrettyLine(prefix=_listdir_prefix, detail=_listdir_detail),
|
||||
"glob_paths": PrettyLine(prefix=_glob_prefix, detail=_glob_detail),
|
||||
"grep_files": PrettyLine(prefix=_grep_prefix, detail=_grep_detail),
|
||||
"regex_files": PrettyLine(prefix=_regex_prefix, detail=_regex_detail),
|
||||
"get_truncated": PrettyLine(prefix=_resume_prefix, detail=_resume_detail),
|
||||
"run_argv": PrettyLine(prefix=_run_argv_prefix, detail=_run_argv_detail),
|
||||
"run_argv_batch": PrettyLine(prefix=_run_argv_batch_prefix, detail=_run_argv_batch_detail),
|
||||
|
||||
@@ -37,10 +37,10 @@ from .file import delete_path as delete_path
|
||||
from .file import edit_lineno as edit_lineno
|
||||
from .file import edit_replace as edit_replace
|
||||
from .file import glob_paths as glob_paths
|
||||
from .file import grep_files as grep_files
|
||||
from .file import listdir as listdir
|
||||
from .file import move_path as move_path
|
||||
from .file import read_file as read_file
|
||||
from .file import regex_files as regex_files
|
||||
from .file import tree as tree
|
||||
from .file import write_file as write_file
|
||||
from .net import NET_TOOLS as NET_TOOLS
|
||||
|
||||
@@ -7,9 +7,9 @@ from .fs_ops import delete_path as delete_path
|
||||
from .fs_ops import move_path as move_path
|
||||
from .get_truncated import get_truncated as get_truncated
|
||||
from .glob_paths import glob_paths as glob_paths
|
||||
from .grep_files import grep_files as grep_files
|
||||
from .listdir import listdir as listdir
|
||||
from .read import read_file as read_file
|
||||
from .regex_files import regex_files as regex_files
|
||||
from .tree import tree as tree
|
||||
from .write import write_file as write_file
|
||||
|
||||
@@ -19,7 +19,7 @@ FILE_TOOLS = [
|
||||
listdir,
|
||||
tree,
|
||||
glob_paths,
|
||||
grep_files,
|
||||
regex_files,
|
||||
edit_replace,
|
||||
edit_lineno,
|
||||
copy_path,
|
||||
|
||||
@@ -130,7 +130,7 @@ def _compile_pattern(pattern: str, *, case_insensitive: bool) -> re.Pattern[str]
|
||||
|
||||
|
||||
@tool(tags=ToolTag.LOCAL | ToolTag.INSTANCE_STATE | ToolTag.READ_ONLY)
|
||||
async def grep_files(
|
||||
async def regex_files(
|
||||
pattern: str,
|
||||
path: str = ".",
|
||||
*,
|
||||
@@ -5,7 +5,7 @@ optional bundled files — scripts, references, templates. Discovery, parsing, a
|
||||
path rules live in :mod:`plyngent.skills`; this module is the model-facing shell.
|
||||
|
||||
Reading needs no grant: the host registers the roots as static read-only roots,
|
||||
so ``read_file`` / ``grep_files`` see them too. Writing goes through the skill
|
||||
so ``read_file`` / ``regex_files`` see them too. Writing goes through the skill
|
||||
tools' own grant (a per-call confirm, or ``[skills].allow_write``) and is always
|
||||
confined to a skill root — that is why these tools never inherit the
|
||||
directory-access path of ``request_directory_access``.
|
||||
@@ -30,9 +30,9 @@ from plyngent.skills import (
|
||||
resolve_skill_file,
|
||||
validate_skill_name,
|
||||
)
|
||||
from plyngent.tools.file.grep_files import DEFAULT_MAX_MATCHES
|
||||
from plyngent.tools.file.grep_files import grep_files as _grep_files
|
||||
from plyngent.tools.file.read import read_file as _read_file
|
||||
from plyngent.tools.file.regex_files import DEFAULT_MAX_MATCHES
|
||||
from plyngent.tools.file.regex_files import regex_files as _regex_files
|
||||
from plyngent.tools.workspace import (
|
||||
DEFAULT_POLICY_CONFIRM_TIMEOUT_SECONDS,
|
||||
WorkspaceError,
|
||||
@@ -47,7 +47,7 @@ if TYPE_CHECKING:
|
||||
# Keeps a listing readable when a third-party root holds a pile of skills.
|
||||
_MAX_LISTED_ISSUES = 8
|
||||
_SKILL_WRITE_POLICY_KEY = "skill_write_policy"
|
||||
# ``path:line: content`` as ``grep_files`` formats it (the path is non-greedy so
|
||||
# ``path:line: content`` as ``regex_files`` formats it (the path is non-greedy so
|
||||
# a Windows drive letter or a colon in a file name cannot split it wrongly).
|
||||
_HIT_RE = re.compile(r"^(?P<path>.+?):(?P<line>\d+): ?(?P<content>.*)$")
|
||||
|
||||
@@ -159,9 +159,9 @@ async def skill_read(
|
||||
|
||||
|
||||
def _hit_relative(skill: Skill, path: str) -> str:
|
||||
"""Rebase one ``grep_files`` hit path onto its skill directory.
|
||||
"""Rebase one ``regex_files`` hit path onto its skill directory.
|
||||
|
||||
Grep reports paths relative to the workspace root for files inside it and
|
||||
The search reports paths relative to the workspace root for files inside it and
|
||||
absolute paths for files outside, so a skill directory can arrive either way
|
||||
(a project-scope skill root lives inside the workspace). Normalizing to an
|
||||
absolute path first covers both without guessing from the string alone.
|
||||
@@ -179,7 +179,7 @@ def _hit_relative(skill: Skill, path: str) -> str:
|
||||
|
||||
|
||||
def _skill_hits(skill: Skill, result: str) -> list[str]:
|
||||
"""Attribute one skill's grep lines to that skill."""
|
||||
"""Attribute one skill's search lines to that skill."""
|
||||
hits: list[str] = []
|
||||
for line in result.splitlines():
|
||||
match = _HIT_RE.match(line)
|
||||
@@ -229,7 +229,7 @@ async def skill_search(
|
||||
remaining = max_matches - len(hits)
|
||||
if remaining <= 0:
|
||||
break
|
||||
result = await _grep_files.handler(
|
||||
result = await _regex_files.handler(
|
||||
pattern,
|
||||
str(target.path),
|
||||
case_insensitive=case_insensitive,
|
||||
|
||||
@@ -79,7 +79,7 @@ def test_parallel_tool_outputs_interleaved_after_calls() -> None:
|
||||
),
|
||||
AssistantFunctionToolCall(
|
||||
id="call_02_ABCD",
|
||||
function=AssistantFunctionTool(name="grep_files", arguments='{"pattern":"todo"}'),
|
||||
function=AssistantFunctionTool(name="regex_files", arguments='{"pattern":"todo"}'),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -352,13 +352,13 @@ async def test_pretty_glob_truncated(capsys: pytest.CaptureFixture[str]) -> None
|
||||
|
||||
async def test_pretty_grep(capsys: pytest.CaptureFixture[str]) -> None:
|
||||
result = "src/a.py:3: x = 1\nsrc/a.py:9: x = 2\nsrc/b.py:1: y = 3\n"
|
||||
await render_events(_aiter([_pretty_call("grep_files", '{"pattern": "x"}'), _result(result)]))
|
||||
await render_events(_aiter([_pretty_call("regex_files", '{"pattern": "x"}'), _result(result)]))
|
||||
out = capsys.readouterr().out
|
||||
assert "* Grep 'x' in '.' (3 matches in 2 files)" in out
|
||||
|
||||
|
||||
async def test_pretty_grep_no_matches(capsys: pytest.CaptureFixture[str]) -> None:
|
||||
await render_events(_aiter([_pretty_call("grep_files", '{"pattern": "zzz"}'), _result("(no matches)")]))
|
||||
await render_events(_aiter([_pretty_call("regex_files", '{"pattern": "zzz"}'), _result("(no matches)")]))
|
||||
out = capsys.readouterr().out
|
||||
assert "* Grep 'zzz' in '.' (no matches)" in out
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ def test_yaml_subset_types_lists_and_booleans() -> None:
|
||||
"read_only: true",
|
||||
"confirm: no",
|
||||
"retries: 3",
|
||||
"allowed-tools: [read_file, grep_files]",
|
||||
"allowed-tools: [read_file, regex_files]",
|
||||
"tags:",
|
||||
" - alpha",
|
||||
" - beta",
|
||||
@@ -72,7 +72,7 @@ def test_yaml_subset_types_lists_and_booleans() -> None:
|
||||
"read_only": True,
|
||||
"confirm": False,
|
||||
"retries": 3,
|
||||
"allowed-tools": ["read_file", "grep_files"],
|
||||
"allowed-tools": ["read_file", "regex_files"],
|
||||
"tags": ["alpha", "beta"],
|
||||
}
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ def test_read_only_classification() -> None:
|
||||
"read_file",
|
||||
"get_truncated",
|
||||
"glob_paths",
|
||||
"grep_files",
|
||||
"regex_files",
|
||||
"listdir",
|
||||
"tree",
|
||||
"todo_list",
|
||||
|
||||
@@ -2,46 +2,46 @@ from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from plyngent.tools.file.grep_files import grep_files
|
||||
from plyngent.tools.file.regex_files import regex_files
|
||||
from tests.test_tools.helpers import call_sync
|
||||
|
||||
|
||||
def test_grep_files_basic(workspace: object) -> None:
|
||||
def test_regex_files_basic(workspace: object) -> None:
|
||||
assert isinstance(workspace, Path)
|
||||
_ = (workspace / "a.py").write_text("hello world\nfoo\n", encoding="utf-8")
|
||||
_ = (workspace / "b.py").write_text("nope\n", encoding="utf-8")
|
||||
out = call_sync(grep_files, "hello")
|
||||
out = call_sync(regex_files, "hello")
|
||||
assert "a.py:1:" in out
|
||||
assert "hello world" in out
|
||||
assert "b.py" not in out
|
||||
|
||||
|
||||
def test_grep_files_case_insensitive(workspace: object) -> None:
|
||||
def test_regex_files_case_insensitive(workspace: object) -> None:
|
||||
assert isinstance(workspace, Path)
|
||||
_ = (workspace / "t.txt").write_text("Hello\n", encoding="utf-8")
|
||||
assert "no matches" in call_sync(grep_files, "hello")
|
||||
out = call_sync(grep_files, "hello", case_insensitive=True)
|
||||
assert "no matches" in call_sync(regex_files, "hello")
|
||||
out = call_sync(regex_files, "hello", case_insensitive=True)
|
||||
assert "t.txt:1:" in out
|
||||
|
||||
|
||||
def test_grep_files_invalid_regex(workspace: object) -> None:
|
||||
def test_regex_files_invalid_regex(workspace: object) -> None:
|
||||
del workspace
|
||||
assert "invalid regex" in call_sync(grep_files, "[")
|
||||
assert "invalid regex" in call_sync(regex_files, "[")
|
||||
|
||||
|
||||
def test_grep_files_skip_git(workspace: object) -> None:
|
||||
def test_regex_files_skip_git(workspace: object) -> None:
|
||||
assert isinstance(workspace, Path)
|
||||
(workspace / ".git").mkdir()
|
||||
_ = (workspace / ".git" / "x").write_text("secret\n", encoding="utf-8")
|
||||
_ = (workspace / "ok.txt").write_text("secret\n", encoding="utf-8")
|
||||
out = call_sync(grep_files, "secret")
|
||||
out = call_sync(regex_files, "secret")
|
||||
assert "ok.txt" in out
|
||||
assert ".git" not in out
|
||||
|
||||
|
||||
def test_grep_files_max_matches(workspace: object) -> None:
|
||||
def test_regex_files_max_matches(workspace: object) -> None:
|
||||
assert isinstance(workspace, Path)
|
||||
lines = "\n".join(f"match {i}" for i in range(10))
|
||||
_ = (workspace / "m.txt").write_text(lines + "\n", encoding="utf-8")
|
||||
out = call_sync(grep_files, "match", max_matches=3)
|
||||
out = call_sync(regex_files, "match", max_matches=3)
|
||||
assert "truncated" in out
|
||||
@@ -198,7 +198,7 @@ async def test_skill_search_single_skill_and_errors(tmp_path: Path) -> None:
|
||||
async def test_skill_search_inside_the_workspace_rebases_the_hit(tmp_path: Path) -> None:
|
||||
"""A project-scope skill root lives inside the workspace: one path, once.
|
||||
|
||||
Grep reports in-workspace files relative to the workspace root and outside
|
||||
The search reports in-workspace files relative to the workspace root and outside
|
||||
ones absolutely, so the skill name must never be glued onto a path that
|
||||
already carries the root.
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user