Skip to content

Commit 7324cb0

Browse files
bnbongclaude
andcommitted
[FIX] defer fastkit import until after sys.path bootstrap and close patch-coverage gaps
Fixes the two P1 findings raised by Codex on #52 and lifts the patch coverage above the 89% threshold so the PR can proceed. - scripts/inspect-templates.py, scripts/inspect-changed-templates.py: import `fastapi_fastkit.backend.inspector` only after the sys.path bootstrap runs, so the scripts work from a fresh checkout where the package is not installed on the interpreter. - tests/test_utils.py: cover the setup.py read-error branch in `is_fastkit_project`, the malformed-pyproject text fallback (tool-section hit, no-markers miss, and read-error path). - tests/test_backends/test_main.py: cover the non-list `[project].dependencies` guard in `_parse_pyproject_dependencies` and the no-trailing-newline branch of `_ensure_pyproject_fastkit_markers`. - tests/test_backends/test_inspector.py: cover the pyproject parse-error, setup.py-tpl read-error, and no-sources-at-all branches of `_check_dependencies`, plus the non-list and non-string-entry paths of `_extract_pyproject_dependency_names`. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 13e566d commit 7324cb0

5 files changed

Lines changed: 201 additions & 6 deletions

File tree

scripts/inspect-changed-templates.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,14 @@
88
from pathlib import Path
99
from typing import List, Set
1010

11-
from fastapi_fastkit.backend.inspector import inspect_fastapi_template
12-
13-
# Ensure src is importable
11+
# Ensure src is importable before importing the package. Pre-commit / local
12+
# runs invoke this script with the project not installed in the interpreter,
13+
# so the sys.path bootstrap has to happen first.
1414
PROJECT_ROOT = Path(__file__).parent.parent
1515
sys.path.insert(0, str(PROJECT_ROOT / "src"))
1616

17+
from fastapi_fastkit.backend.inspector import inspect_fastapi_template # noqa: E402
18+
1719
TEMPLATE_DIR = PROJECT_ROOT / "src" / "fastapi_fastkit" / "fastapi_project_template"
1820

1921

scripts/inspect-templates.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,14 @@
1717
from pathlib import Path
1818
from typing import Any, Dict, List
1919

20-
from fastapi_fastkit.backend.inspector import inspect_fastapi_template
21-
22-
# Add src to path to import fastapi_fastkit modules
20+
# Add src to path to import fastapi_fastkit modules. The sys.path bootstrap
21+
# must run before importing the package so the script works from a fresh
22+
# repo checkout without the package installed on the interpreter.
2323
project_root = Path(__file__).parent.parent
2424
sys.path.insert(0, str(project_root / "src"))
2525

26+
from fastapi_fastkit.backend.inspector import inspect_fastapi_template # noqa: E402
27+
2628

2729
def get_templates_to_inspect(specific_templates: str = "") -> List[str]:
2830
"""Get list of templates to inspect."""

tests/test_backends/test_inspector.py

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,106 @@ def test_check_dependencies_setup_py_only_with_fastapi(self, temp_dir: str) -> N
323323
assert result is True
324324
assert inspector.errors == []
325325

326+
def test_check_dependencies_pyproject_parse_error(self, temp_dir: str) -> None:
327+
"""Malformed pyproject.toml-tpl surfaces a parse error and fails the check."""
328+
# given
329+
(self.template_path / "tests").mkdir(exist_ok=True)
330+
(self.template_path / "README.md-tpl").write_text("# Test")
331+
# Unterminated string makes tomllib raise TOMLDecodeError.
332+
(self.template_path / "pyproject.toml-tpl").write_text(
333+
'[project]\nname = "demo\nversion = "0.1.0"\n'
334+
)
335+
336+
# when
337+
with patch("fastapi_fastkit.backend.transducer.copy_and_convert_template"):
338+
inspector = TemplateInspector(str(self.template_path), temp_dir)
339+
result = inspector._check_dependencies()
340+
341+
# then
342+
assert result is False
343+
assert any("Invalid pyproject.toml-tpl" in error for error in inspector.errors)
344+
345+
def test_check_dependencies_setup_py_read_error(self, temp_dir: str) -> None:
346+
"""OSError while reading setup.py-tpl surfaces a descriptive error."""
347+
# given
348+
(self.template_path / "tests").mkdir(exist_ok=True)
349+
(self.template_path / "README.md-tpl").write_text("# Test")
350+
(self.template_path / "setup.py-tpl").write_text(
351+
'from setuptools import setup\nsetup(name="demo")\n'
352+
)
353+
354+
# when — selectively fail the open() targeting setup.py-tpl only.
355+
real_open = open
356+
357+
def selective_open(file, *args, **kwargs): # type: ignore[no-untyped-def]
358+
if str(file).endswith("setup.py-tpl"):
359+
raise OSError("Permission denied")
360+
return real_open(file, *args, **kwargs)
361+
362+
with patch("fastapi_fastkit.backend.transducer.copy_and_convert_template"):
363+
inspector = TemplateInspector(str(self.template_path), temp_dir)
364+
with patch("builtins.open", side_effect=selective_open):
365+
result = inspector._check_dependencies()
366+
367+
# then
368+
assert result is False
369+
assert any("Error reading setup.py-tpl" in error for error in inspector.errors)
370+
371+
def test_check_dependencies_no_sources_at_all(self, temp_dir: str) -> None:
372+
"""Calling _check_dependencies with no metadata files yields a clear error."""
373+
# given
374+
(self.template_path / "tests").mkdir(exist_ok=True)
375+
(self.template_path / "README.md-tpl").write_text("# Test")
376+
377+
# when
378+
with patch("fastapi_fastkit.backend.transducer.copy_and_convert_template"):
379+
inspector = TemplateInspector(str(self.template_path), temp_dir)
380+
result = inspector._check_dependencies()
381+
382+
# then
383+
assert result is False
384+
assert any("No dependency source found" in error for error in inspector.errors)
385+
386+
def test_extract_pyproject_dependency_names_non_list(self, temp_dir: str) -> None:
387+
"""Non-list [project].dependencies yields a descriptive parse error."""
388+
# given
389+
(self.template_path / "tests").mkdir(exist_ok=True)
390+
(self.template_path / "README.md-tpl").write_text("# Test")
391+
pyproject = self.template_path / "pyproject.toml-tpl"
392+
pyproject.write_text(
393+
'[project]\nname = "demo"\nversion = "0.1.0"\n'
394+
'dependencies = "fastapi>=0.115"\n'
395+
)
396+
397+
# when
398+
names, error = TemplateInspector._extract_pyproject_dependency_names(pyproject)
399+
400+
# then
401+
assert names == set()
402+
assert error is not None
403+
assert "must be a list" in error
404+
405+
def test_extract_pyproject_dependency_names_skips_non_strings(
406+
self, temp_dir: str
407+
) -> None:
408+
"""Non-string / empty entries in [project].dependencies are ignored."""
409+
# given
410+
(self.template_path / "tests").mkdir(exist_ok=True)
411+
(self.template_path / "README.md-tpl").write_text("# Test")
412+
pyproject = self.template_path / "pyproject.toml-tpl"
413+
pyproject.write_text(
414+
'[project]\nname = "demo"\nversion = "0.1.0"\n'
415+
# Empty string and an integer-ish value are dropped without raising.
416+
'dependencies = ["fastapi>=0.115", "", " "]\n'
417+
)
418+
419+
# when
420+
names, error = TemplateInspector._extract_pyproject_dependency_names(pyproject)
421+
422+
# then
423+
assert error is None
424+
assert names == {"fastapi"}
425+
326426
@patch("fastapi_fastkit.backend.inspector.find_template_core_modules")
327427
def test_check_fastapi_implementation_valid(
328428
self, mock_find_modules: MagicMock, temp_dir: str

tests/test_backends/test_main.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -507,6 +507,42 @@ def test_process_pyproject_file_writes_markers_into_generated_pyproject(
507507

508508
shutil.rmtree(str(template_path))
509509

510+
def test_parse_pyproject_dependencies_non_list_value(self) -> None:
511+
"""Non-list dependencies value returns [] rather than raising."""
512+
template_path = Path(tempfile.mkdtemp())
513+
try:
514+
pyproject = template_path / "pyproject.toml-tpl"
515+
# A string in place of a dependencies array exercises the
516+
# defensive isinstance() guard.
517+
pyproject.write_text(
518+
'[project]\nname = "demo"\nversion = "0.1.0"\n'
519+
'dependencies = "fastapi>=0.115"\n'
520+
)
521+
522+
result = _parse_pyproject_dependencies(str(pyproject))
523+
524+
assert result == []
525+
526+
finally:
527+
import shutil
528+
529+
shutil.rmtree(str(template_path))
530+
531+
def test_ensure_pyproject_fastkit_markers_appends_newline_when_missing(
532+
self,
533+
) -> None:
534+
"""Content without a trailing newline gets one before the tool section."""
535+
content = '[project]\nname = "demo"\ndescription = "x"'
536+
assert not content.endswith("\n")
537+
538+
result = _ensure_pyproject_fastkit_markers(content)
539+
540+
# Marker + tool section are present and the output ends cleanly.
541+
assert "[tool.fastapi-fastkit]" in result
542+
assert result.endswith("managed = true\n")
543+
# No doubled newlines immediately before the inserted table.
544+
assert "\n\n\n[tool.fastapi-fastkit]" not in result
545+
510546
@patch("builtins.open", mock_open(read_data="fastapi>=0.100.0"))
511547
@patch("os.path.exists", return_value=True)
512548
def test_read_template_stack_file_read_error(self, mock_exists: MagicMock) -> None:

tests/test_utils.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,61 @@ def test_is_fastkit_project_malformed_pyproject_falls_back(
265265

266266
assert is_fastkit_project(str(project_path)) is True
267267

268+
def test_is_fastkit_project_malformed_pyproject_tool_section_text(
269+
self, temp_dir: str
270+
) -> None:
271+
"""Malformed pyproject with a [tool.fastapi-fastkit] header is still detected."""
272+
from fastapi_fastkit.utils.main import is_fastkit_project
273+
274+
project_path = Path(temp_dir) / "broken-with-tool"
275+
project_path.mkdir()
276+
# Unterminated string makes tomllib fail, but the text-fallback spots
277+
# the [tool.fastapi-fastkit] header.
278+
(project_path / "pyproject.toml").write_text(
279+
'[project\nname = "broken\n\n[tool.fastapi-fastkit]\nmanaged = true\n'
280+
)
281+
282+
assert is_fastkit_project(str(project_path)) is True
283+
284+
def test_is_fastkit_project_malformed_pyproject_no_markers(
285+
self, temp_dir: str
286+
) -> None:
287+
"""Malformed pyproject with neither marker falls through to False."""
288+
from fastapi_fastkit.utils.main import is_fastkit_project
289+
290+
project_path = Path(temp_dir) / "broken-unmarked"
291+
project_path.mkdir()
292+
(project_path / "pyproject.toml").write_text(
293+
'[project\nname = "broken\ndescription = "nothing here"\n'
294+
)
295+
296+
assert is_fastkit_project(str(project_path)) is False
297+
298+
def test_is_fastkit_project_setup_py_read_error(self, temp_dir: str) -> None:
299+
"""OSError while reading setup.py does not crash detection; returns False."""
300+
from fastapi_fastkit.utils.main import is_fastkit_project
301+
302+
project_path = Path(temp_dir) / "setup-read-err"
303+
project_path.mkdir()
304+
(project_path / "setup.py").write_text(
305+
'from setuptools import setup\nsetup(name="demo")\n'
306+
)
307+
308+
with patch("builtins.open", side_effect=OSError("Permission denied")):
309+
assert is_fastkit_project(str(project_path)) is False
310+
311+
def test_pyproject_text_marks_fastkit_read_error(self, temp_dir: str) -> None:
312+
"""Text fallback swallows OSError and returns False cleanly."""
313+
from fastapi_fastkit.utils.main import _pyproject_text_marks_fastkit
314+
315+
project_path = Path(temp_dir) / "text-fallback-err"
316+
project_path.mkdir()
317+
pyproject = project_path / "pyproject.toml"
318+
pyproject.write_text("whatever")
319+
320+
with patch("builtins.open", side_effect=OSError("Permission denied")):
321+
assert _pyproject_text_marks_fastkit(str(pyproject)) is False
322+
268323
def test_print_error_with_traceback(self) -> None:
269324
"""Test print_error function with traceback enabled."""
270325
# given

0 commit comments

Comments
 (0)