Skip to content

Commit 6624e74

Browse files
committed
Abstract filesystem operations in snippets.py
- Add fs parameter to collect_snippets, find_snippets, and collect_snippet_files functions - Replace direct filesystem calls with Fs interface methods: - Use fs.readlines() instead of open().readlines() - Use fs.stat() instead of path.exists() - Fix PathFs to use UTF-8 encoding for all file operations (read, readlines, write) - Add comprehensive tests using RecordFs for deterministic testing - Test snippet collection, file handling, and error cases with filesystem abstraction
1 parent 1c9c123 commit 6624e74

File tree

3 files changed

+185
-24
lines changed

3 files changed

+185
-24
lines changed

aws_doc_sdk_examples_tools/fs.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,11 +57,11 @@ def read(self, path: Path) -> str:
5757
return file.read()
5858

5959
def readlines(self, path: Path) -> List[str]:
60-
with path.open("r") as file:
60+
with path.open("r", encoding="utf-8") as file:
6161
return file.readlines()
6262

6363
def write(self, path: Path, content: str):
64-
with path.open("w") as file:
64+
with path.open("w", encoding="utf-8") as file:
6565
file.write(content)
6666

6767
def stat(self, path: Path) -> Stat:

aws_doc_sdk_examples_tools/snippets.py

Lines changed: 24 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
from .validator_config import skip
1010
from .file_utils import get_files, clear
11+
from .fs import Fs, PathFs
1112
from .metadata import Example
1213
from .metadata_errors import MetadataErrors, MetadataError
1314
from .project_validator import (
@@ -145,16 +146,17 @@ def parse_snippets(
145146
return snippets, errors
146147

147148

148-
def find_snippets(file: Path, prefix: str) -> Tuple[Dict[str, Snippet], MetadataErrors]:
149+
def find_snippets(
150+
file: Path, prefix: str, fs: Fs = PathFs()
151+
) -> Tuple[Dict[str, Snippet], MetadataErrors]:
149152
errors = MetadataErrors()
150153
snippets: Dict[str, Snippet] = {}
151154
try:
152-
with open(file, encoding="utf-8") as snippet_file:
153-
try:
154-
snippets, errs = parse_snippets(snippet_file.readlines(), file, prefix)
155-
errors.extend(errs)
156-
except UnicodeDecodeError as err:
157-
errors.append(MetadataUnicodeError(file=file, err=err))
155+
lines = fs.readlines(file)
156+
snippets, errs = parse_snippets(lines, file, prefix)
157+
errors.extend(errs)
158+
except UnicodeDecodeError as err:
159+
errors.append(MetadataUnicodeError(file=file, err=err))
158160
except FileNotFoundError:
159161
pass
160162
except Exception as e:
@@ -163,12 +165,12 @@ def find_snippets(file: Path, prefix: str) -> Tuple[Dict[str, Snippet], Metadata
163165

164166

165167
def collect_snippets(
166-
root: Path, prefix: str = ""
168+
root: Path, prefix: str = "", fs: Fs = PathFs()
167169
) -> Tuple[Dict[str, Snippet], MetadataErrors]:
168170
snippets: Dict[str, Snippet] = {}
169171
errors = MetadataErrors()
170-
for file in get_files(root, skip):
171-
snips, errs = find_snippets(file, prefix)
172+
for file in get_files(root, skip, fs=fs):
173+
snips, errs = find_snippets(file, prefix, fs=fs)
172174
snippets.update(snips)
173175
errors.extend(errs)
174176
return snippets, errors
@@ -180,14 +182,17 @@ def collect_snippet_files(
180182
prefix: str,
181183
errors: MetadataErrors,
182184
root: Path,
185+
fs: Fs = PathFs(),
183186
):
184187
for example in examples:
185188
for lang in example.languages:
186189
language = example.languages[lang]
187190
for version in language.versions:
188191
for excerpt in version.excerpts:
189192
for snippet_file in excerpt.snippet_files:
190-
if not (root / snippet_file).exists():
193+
snippet_path = root / snippet_file
194+
snippet_stat = fs.stat(snippet_path)
195+
if not snippet_stat.exists:
191196
# Ensure all snippet_files exist
192197
errors.append(
193198
MissingSnippetFile(
@@ -207,17 +212,14 @@ def collect_snippet_files(
207212
)
208213
continue
209214
name = prefix + str(snippet_file).replace("/", ".")
210-
with open(root / snippet_file, encoding="utf-8") as file:
211-
code = file.readlines()
212-
snippets[name] = Snippet(
213-
id=name,
214-
file=snippet_file,
215-
line_start=0,
216-
line_end=len(code),
217-
code="".join(
218-
strip_snippet_tags(strip_spdx_header(code))
219-
),
220-
)
215+
code = fs.readlines(snippet_path)
216+
snippets[name] = Snippet(
217+
id=name,
218+
file=snippet_file,
219+
line_start=0,
220+
line_end=len(code),
221+
code="".join(strip_snippet_tags(strip_spdx_header(code))),
222+
)
221223

222224

223225
def strip_snippet_tags(lines: List[str]) -> List[str]:

aws_doc_sdk_examples_tools/snippets_test.py

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
from pathlib import Path
66

77
from aws_doc_sdk_examples_tools import snippets
8+
from aws_doc_sdk_examples_tools.fs import RecordFs
9+
from aws_doc_sdk_examples_tools.metadata import Example, Language, Version, Excerpt
10+
from aws_doc_sdk_examples_tools.metadata_errors import MetadataErrors
811

912

1013
@pytest.mark.parametrize(
@@ -102,3 +105,159 @@ def test_strip_spdx_header():
102105
)
103106

104107
assert [] == snippets.strip_spdx_header([])
108+
109+
110+
class TestFindSnippetsFs:
111+
"""Test find_snippets with filesystem abstraction."""
112+
113+
def test_find_snippets_with_recordfs(self):
114+
"""Test find_snippets using RecordFs."""
115+
fs = RecordFs(
116+
{
117+
Path(
118+
"/project/test.py"
119+
): """# snippet-start:[example.hello]
120+
def hello():
121+
print("Hello, World!")
122+
# snippet-end:[example.hello]
123+
"""
124+
}
125+
)
126+
127+
snippet_dict, errors = snippets.find_snippets(
128+
Path("/project/test.py"), "", fs=fs
129+
)
130+
131+
assert len(errors) == 0
132+
assert len(snippet_dict) == 1
133+
assert "example.hello" in snippet_dict
134+
snippet = snippet_dict["example.hello"]
135+
assert snippet.id == "example.hello"
136+
assert "def hello():" in snippet.code
137+
138+
def test_find_snippets_missing_file_graceful(self):
139+
"""Test find_snippets behavior with missing files."""
140+
fs = RecordFs({})
141+
142+
snippet_dict, errors = snippets.find_snippets(
143+
Path("/project/missing.py"), "", fs=fs
144+
)
145+
146+
# Missing files generate errors (not handled gracefully)
147+
assert len(snippet_dict) == 0
148+
assert len(errors) == 1 # Should have a FileReadError
149+
150+
151+
class TestCollectSnippetsFs:
152+
"""Test collect_snippets with filesystem abstraction."""
153+
154+
def test_collect_snippets_with_recordfs(self):
155+
"""Test collect_snippets using RecordFs."""
156+
fs = RecordFs(
157+
{
158+
Path(
159+
"/project/src/file1.py"
160+
): """# snippet-start:[example1]
161+
def example1():
162+
pass
163+
# snippet-end:[example1]
164+
""",
165+
Path(
166+
"/project/src/file2.py"
167+
): """# snippet-start:[example2]
168+
def example2():
169+
pass
170+
# snippet-end:[example2]
171+
""",
172+
}
173+
)
174+
175+
snippet_dict, errors = snippets.collect_snippets(Path("/project/src"), fs=fs)
176+
177+
assert len(errors) == 0
178+
assert len(snippet_dict) == 2
179+
assert "example1" in snippet_dict
180+
assert "example2" in snippet_dict
181+
182+
183+
class TestCollectSnippetFilesFs:
184+
"""Test collect_snippet_files with filesystem abstraction."""
185+
186+
def test_collect_snippet_files_with_recordfs(self):
187+
"""Test collect_snippet_files using RecordFs."""
188+
fs = RecordFs({Path("/project/example.py"): "print('Hello, World!')\n"})
189+
190+
example = Example(
191+
id="test_example",
192+
file=None,
193+
languages={
194+
"python": Language(
195+
name="python",
196+
property="python",
197+
versions=[
198+
Version(
199+
sdk_version="3",
200+
excerpts=[
201+
Excerpt(
202+
description="Test excerpt",
203+
snippet_tags=[],
204+
snippet_files=["example.py"],
205+
)
206+
],
207+
)
208+
],
209+
)
210+
},
211+
)
212+
213+
snippet_dict = {}
214+
errors = MetadataErrors()
215+
216+
snippets.collect_snippet_files(
217+
[example], snippet_dict, "", errors, Path("/project"), fs=fs
218+
)
219+
220+
assert len(errors) == 0
221+
assert len(snippet_dict) == 1
222+
assert "example.py" in snippet_dict
223+
snippet = snippet_dict["example.py"]
224+
assert snippet.file == "example.py"
225+
assert "Hello, World!" in snippet.code
226+
227+
def test_collect_snippet_files_missing_file_error(self):
228+
"""Test collect_snippet_files properly reports missing files as errors."""
229+
fs = RecordFs({}) # Empty filesystem
230+
231+
example = Example(
232+
id="test_example",
233+
file=None,
234+
languages={
235+
"python": Language(
236+
name="python",
237+
property="python",
238+
versions=[
239+
Version(
240+
sdk_version="3",
241+
excerpts=[
242+
Excerpt(
243+
description="Test excerpt",
244+
snippet_tags=[],
245+
snippet_files=["missing.py"],
246+
)
247+
],
248+
)
249+
],
250+
)
251+
},
252+
)
253+
254+
snippet_dict = {}
255+
errors = MetadataErrors()
256+
257+
snippets.collect_snippet_files(
258+
[example], snippet_dict, "", errors, Path("/project"), fs=fs
259+
)
260+
261+
# Missing snippet files should generate errors (unlike find_snippets)
262+
assert len(errors) == 1
263+
assert len(snippet_dict) == 0

0 commit comments

Comments
 (0)