"""Tests for docstring_generator module (Issue #96).""" import ast import sys import tempfile from pathlib import Path import pytest sys.path.insert(0, str(Path(__file__).parent.parent / "scripts")) from docstring_generator import ( name_to_title, extract_body_hint, generate_docstring, process_source, iter_python_files, ) class TestNameToTitle: def test_snake_to_title(self): assert name_to_title("validate_fact") == "Validate Fact" assert name_to_title("docstring_generator") == "Docstring Generator" assert name_to_title("main") == "Main" assert name_to_title("__init__") == "Init" class TestExtractBodyHint: def test_assignment_hint(self): body = [ast.parse("result = compute()").body[0]] hint = extract_body_hint(body) assert hint == "Compute or return compute()" def test_return_hint(self): body = [ast.parse("return data").body[0]] hint = extract_body_hint(body) assert hint == "Return data" def test_no_hint(self): body = [ast.parse("pass").body[0]] assert extract_body_hint(body) is None class TestGenerateDocstring: def test_simple_function(self): src = "def add(a, b):\n return a + b\n" tree = ast.parse(src) func = tree.body[0] doc = generate_docstring(func) assert 'Add' in doc assert 'a' in doc and 'b' in doc assert 'Args:' in doc assert 'Returns:' in doc def test_typed_function(self): src = "def greet(name: str) -> str:\n return f'Hello {name}'\n" tree = ast.parse(src) func = tree.body[0] doc = generate_docstring(func) assert 'name (str)' in doc assert 'str' in doc def test_async_function(self): src = "async def fetch():\n pass\n" tree = ast.parse(src) func = tree.body[0] doc = generate_docstring(func) assert 'Fetch' in doc def test_self_skipped(self): src = "class C:\n def method(self, x):\n return x\n" tree = ast.parse(src) cls = tree.body[0] method = cls.body[0] doc = generate_docstring(method) # 'self' should not appear in Args section args_start = doc.find('Args:') if args_start >= 0: args_section = doc[args_start:] assert '(self)' not in args_section class TestProcessSource: def test_adds_docstrings(self): src = "def foo(x):\n return x * 2\n" new_src, funcs = process_source(src, "test.py") assert len(funcs) == 1 and funcs[0] == "foo" assert '"""' in new_src assert 'Foo' in new_src def test_preserves_existing_docstrings(self): src = 'def bar():\n """Already documented."""\n return 1\n' new_src, funcs = process_source(src, "test.py") assert len(funcs) == 0 assert new_src == src def test_multiple_functions(self): src = "def a(): pass\ndef b(): pass\ndef c(): pass\n" new_src, funcs = process_source(src, "test.py") assert len(funcs) == 3 assert '"""' in new_src def test_dry_run_no_write(self, tmp_path): file = tmp_path / "t.py" file.write_text("def f(): pass\n") original_mtime = file.stat().st_mtime new_src, funcs = process_source(file.read_text(), str(file)) assert funcs # detected # When caller handles write, dry-run leaves file unchanged current_mtime = file.stat().st_mtime assert current_mtime == original_mtime class TestIterPythonFiles: def test_single_file(self, tmp_path): f = tmp_path / "single.py" f.write_text("x = 1") files = iter_python_files([str(f)]) assert len(files) == 1 assert files[0].name == "single.py" def test_directory_recursion(self, tmp_path): (tmp_path / "sub").mkdir() (tmp_path / "sub" / "a.py").write_text("a=1") (tmp_path / "b.py").write_text("b=2") files = iter_python_files([str(tmp_path)]) assert len(files) == 2