diff --git a/src/anthropic/lib/_files.py b/src/anthropic/lib/_files.py index ad7b7e57a..c022829d5 100644 --- a/src/anthropic/lib/_files.py +++ b/src/anthropic/lib/_files.py @@ -9,7 +9,9 @@ def files_from_dir(directory: str | os.PathLike[str]) -> list[FileTypes]: - path = Path(directory) + # the names carry the directory itself, so where it sits has to be known: + # `.` has no parent to take the name from, and `..` would put one in a name + path = Path(os.path.abspath(directory)) files: list[FileTypes] = [] _collect_files(path, path.parent, files) @@ -26,7 +28,7 @@ def _collect_files(directory: Path, relative_to: Path, files: list[FileTypes]) - async def async_files_from_dir(directory: str | os.PathLike[str]) -> list[FileTypes]: - path = anyio.Path(directory) + path = anyio.Path(os.path.abspath(directory)) files: list[FileTypes] = [] await _async_collect_files(path, path.parent, files) diff --git a/tests/lib/test_files.py b/tests/lib/test_files.py new file mode 100644 index 000000000..13f49a0a4 --- /dev/null +++ b/tests/lib/test_files.py @@ -0,0 +1,53 @@ +from __future__ import annotations + +import os +from pathlib import Path + +import pytest + +from anthropic.lib import files_from_dir, async_files_from_dir + + +@pytest.fixture +def skill(tmp_path: Path) -> Path: + directory = tmp_path / "greeting" + (directory / "scripts").mkdir(parents=True) + (directory / "SKILL.md").write_text("# hi\n") + (directory / "scripts" / "run.py").write_text("print(1)\n") + return directory + + +def _names(files: list[object]) -> list[str]: + return sorted(name for name, *_ in files) # type: ignore[misc] + + +EXPECTED = ["greeting/SKILL.md", "greeting/scripts/run.py"] + + +def test_names_carry_the_directory(skill: Path) -> None: + assert _names(files_from_dir(skill)) == EXPECTED + + +RELATIVE = [("inside", "."), ("inside", "./"), ("inside", "../greeting"), ("parent", "greeting")] + + +def test_a_relative_root_names_the_same_files(skill: Path, monkeypatch: pytest.MonkeyPatch) -> None: + # the names go to the API as given, so `.` must not drop the directory and + # `..` must not reach a name + for where, spelling in RELATIVE: + monkeypatch.chdir(skill if where == "inside" else skill.parent) + assert _names(files_from_dir(spelling)) == EXPECTED, spelling + + +def test_a_trailing_separator_changes_nothing(skill: Path) -> None: + assert _names(files_from_dir(f"{skill}{os.sep}")) == EXPECTED + + +async def test_async_names_carry_the_directory(skill: Path) -> None: + assert _names(await async_files_from_dir(skill)) == EXPECTED + + +async def test_async_a_relative_root_names_the_same_files(skill: Path, monkeypatch: pytest.MonkeyPatch) -> None: + for where, spelling in RELATIVE: + monkeypatch.chdir(skill if where == "inside" else skill.parent) + assert _names(await async_files_from_dir(spelling)) == EXPECTED, spelling