Skip to content

Commit 9bee651

Browse files
committed
v1.2.1
### Added - **Python Docstring Merging**: Implemented a feature to merge the auto-generated header with existing manual module-level docstrings in Python files, preserving user-written content. ### Changed - **Test Suite Refactoring**: Significantly refactored the test suite by introducing a `source_processor` fixture. This simplifies test code, removes boilerplate for file creation, and improves readability across all test files. ### Documentation - Updated the repository URL in `README.md`. - Reorganized `README.md` for better readability by moving the "Supported Languages" section to the top.
1 parent d3f8371 commit 9bee651

11 files changed

Lines changed: 298 additions & 281 deletions

CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1313
- Configuration file format validation
1414
- Switching to the Abstract Syntax Tree (AST)
1515

16+
## [1.2.1] - 2025-06-30
17+
18+
### Added
19+
20+
- **Python Docstring Merging**: Implemented a feature to merge the auto-generated header with existing manual module-level docstrings in Python files, preserving user-written content.
21+
22+
### Changed
23+
24+
- **Test Suite Refactoring**: Significantly refactored the test suite by introducing a `source_processor` fixture. This simplifies test code, removes boilerplate for file creation, and improves readability across all test files.
25+
26+
### Documentation
27+
28+
- Updated the repository URL in `README.md`.
29+
- Reorganized `README.md` for better readability by moving the "Supported Languages" section to the top.
30+
1631
## [1.2.0] - 2025-06-29
1732

1833
### Added
@@ -21,6 +36,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2136
- **Header Preservation**: Implemented intelligent detection to preserve file headers (e.g., shebangs, encoding declarations, Go package definitions, leading comments/imports) across all supported languages.
2237
- **Expanded Language Support**: Added initial processing support and type mappings for Java, PowerShell, Delphi, and C.
2338
- **Enhanced Testing**: Introduced new test suites for determinism, header preservation, and line number accuracy to ensure core feature reliability.
39+
- **Initial release of `agent-docstrings`**
2440

2541
### Changed
2642

README.md

Lines changed: 17 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,22 @@
1717

1818
A command-line tool to auto-generate and update file-level docstrings summarizing classes and functions. Useful for maintaining a high-level overview of your files, especially in projects with code generated or modified by AI assistants.
1919

20+
## Supported Languages
21+
22+
| Language | File Extensions | Features |
23+
| ---------- | ----------------------------------- | ------------------------------ |
24+
| Python | `.py` | Classes, functions, methods |
25+
| Java | `.java` | Classes, methods |
26+
| Kotlin | `.kt` | Classes, functions |
27+
| Go | `.go` | Functions, methods |
28+
| PowerShell | `.ps1`, `.psm1` | Functions |
29+
| Delphi | `.pas` | Classes, procedures, functions |
30+
| C | `.c`, `.h` | Functions |
31+
| C++ | `.cpp`, `.hpp`, `.cc`, `.cxx`, `.h` | Functions, classes |
32+
| C# | `.cs` | Classes, methods |
33+
| JavaScript | `.js`, `.jsx` | Functions, classes |
34+
| TypeScript | `.ts`, `.tsx` | Functions, classes |
35+
2036
## Why?
2137

2238
When working in Cursor and similar IDEs, Agents often start reading files from the beginning. And regarding Cursor's behavior during the script's creation, in normal mode, the model reads 250 lines of code per call, and in MAX mode, 750 lines. However, I have projects with files over 1000 lines of code, which are not very appropriate to divide into smaller files. And anyway, Agent still have to call reading tools for each individual file.
@@ -38,11 +54,6 @@ In addition to the advantage of quick navigation, the initial docstring also ser
3854

3955
This tool is compatible with **Python 3.8, 3.9, 3.10, 3.11, 3.12, and 3.13**.
4056

41-
### Key compatibility features:
42-
43-
- Uses `typing.Union` instead of `|` syntax for Python 3.8/3.9 compatibility
44-
- Uses `typing.Tuple` instead of built-in `tuple` for type hints
45-
- Compatible with `from __future__ import annotations`
4657
- No dependency on external libraries
4758

4859
## Installation
@@ -56,7 +67,7 @@ pip install agent-docstrings
5667
### From source
5768

5869
```bash
59-
git clone https://github.com/yourname/agent-docstrings.git
70+
git clone https://github.com/Artemonim/agent-docstrings.git
6071
cd agent-docstrings
6172
pip install -e .
6273
```
@@ -152,22 +163,6 @@ It is important to understand the nuances of this tool to use it effectively. Th
152163

153164
- **In-Place File Modification**: The tool modifies files directly. It is designed to correctly remove its own previously generated headers, but it might struggle with files that have very complex, pre-existing header comments, potentially leading to incorrect placement of the new header.
154165

155-
## Supported Languages
156-
157-
| Language | File Extensions | Features |
158-
| ---------- | ----------------------------------- | ------------------------------ |
159-
| Python | `.py` | Classes, functions, methods |
160-
| Java | `.java` | Classes, methods |
161-
| Kotlin | `.kt` | Classes, functions |
162-
| Go | `.go` | Functions, methods |
163-
| PowerShell | `.ps1`, `.psm1` | Functions |
164-
| Delphi | `.pas` | Classes, procedures, functions |
165-
| C | `.c`, `.h` | Functions |
166-
| C++ | `.cpp`, `.hpp`, `.cc`, `.cxx`, `.h` | Functions, classes |
167-
| C# | `.cs` | Classes, methods |
168-
| JavaScript | `.js`, `.jsx` | Functions, classes |
169-
| TypeScript | `.ts`, `.tsx` | Functions, classes |
170-
171166
## Examples
172167

173168
### Python Example

agent_docstrings/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,4 @@
77
Attributes:
88
__version__ (str): Current version of the *agent-docstrings* package.
99
"""
10-
__version__ = "1.2.0"
10+
__version__ = "1.2.1"

agent_docstrings/core.py

Lines changed: 64 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
- _format_header(classes: List[ClassInfo], functions: List[SignatureInfo], language: str, line_offset: int) -> str (line 242)
1414
- get_preserved_header_end_line(lines: List[str], language: str) -> int (line 262)
1515
- process_file(path: Path, verbose: bool = False) -> None (line 344)
16-
- discover_and_process_files(directories: List[str], verbose: bool = False) -> None (line 414)
16+
- discover_and_process_files(directories: List[str], verbose: bool = False) -> None (line 468)
1717
--- END AUTO-GENERATED DOCSTRING ---
1818
"""
1919
from __future__ import annotations
@@ -390,15 +390,69 @@ def process_file(path: Path, verbose: bool = False) -> None:
390390
# * Now create the final header with correct line numbers
391391
final_header = _format_header(classes, functions, language, line_offset)
392392

393-
new_content_parts = []
394-
if file_prefix:
395-
new_content_parts.append(file_prefix)
396-
397-
new_content_parts.append(final_header)
398-
new_content_parts.append(cleaned_body.strip())
399-
400-
# Use single newlines to test composition theory
401-
new_content = "\n".join(filter(None, new_content_parts))
393+
# Attempt to merge auto-generated header into existing manual docstring for Python
394+
merged_body = None
395+
if language == "python":
396+
# Split cleaned body into lines
397+
body_lines = cleaned_body.splitlines()
398+
# Find first non-empty line
399+
idx = 0
400+
while idx < len(body_lines) and body_lines[idx].strip() == "":
401+
idx += 1
402+
# Check for manual docstring start
403+
if idx < len(body_lines) and body_lines[idx].strip().startswith(('"""', "'''")):
404+
delim = body_lines[idx].strip()
405+
# Ensure it's not an existing auto-generated docstring
406+
marker_present = False
407+
for i in range(idx, min(idx + 5, len(body_lines))):
408+
if DOCSTRING_START_MARKER in body_lines[i]:
409+
marker_present = True
410+
break
411+
if not marker_present:
412+
# Find end of manual docstring
413+
end_idx = None
414+
for j in range(idx + 1, len(body_lines)):
415+
if body_lines[j].strip() == delim:
416+
end_idx = j
417+
break
418+
if end_idx is not None:
419+
manual_inner = body_lines[idx + 1:end_idx]
420+
# Compute auto header content lines with correct offset for merge
421+
# temp_header_lines holds the auto header lines including delimiters
422+
# content_lines length is temp_header_lines minus start/end markers
423+
offset_override = len(temp_header_lines) - 2
424+
# Generate only the header content lines (without triple-quote delimiters)
425+
header_inner = _get_header_content_lines(
426+
classes, functions, language, offset_override
427+
)
428+
merged_lines = []
429+
# Preserve leading blank lines before manual docstring
430+
merged_lines.extend(body_lines[:idx])
431+
# Start merged docstring with manual delimiter
432+
merged_lines.append(delim)
433+
# Insert auto-generated header content
434+
merged_lines.extend(header_inner)
435+
# Insert original manual docstring content
436+
merged_lines.extend(manual_inner)
437+
# Close merged docstring with manual delimiter
438+
merged_lines.append(delim)
439+
# Append rest of body after original docstring
440+
merged_lines.extend(body_lines[end_idx + 1:])
441+
merged_body = "\n".join(merged_lines)
442+
if merged_body is not None:
443+
if file_prefix:
444+
new_content = file_prefix + "\n" + merged_body.lstrip("\n")
445+
else:
446+
new_content = merged_body.lstrip("\n")
447+
else:
448+
# Default behavior: insert separate docstring
449+
new_content_parts = []
450+
if file_prefix:
451+
new_content_parts.append(file_prefix)
452+
new_content_parts.append(final_header)
453+
new_content_parts.append(cleaned_body.strip())
454+
# Use single newlines to test composition theory
455+
new_content = "\n".join(filter(None, new_content_parts))
402456

403457
if new_content.strip() != original_content.strip():
404458
path.write_text(new_content, encoding="utf-8", newline="\n")

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "agent-docstrings"
7-
version = "1.2.0"
7+
version = "1.2.1"
88
description = "A command-line tool to auto-generate and update file-level docstrings summarizing classes and functions. Useful for maintaining a high-level overview of your files, especially in projects with code generated or modified by AI assistants."
99
readme = { file = "README.md", content-type = "text/markdown" }
1010
license = { file = "LICENSE" }
@@ -124,7 +124,7 @@ exclude_lines = [
124124
]
125125

126126
[tool.bumpversion]
127-
current_version = "1.2.0"
127+
current_version = "1.2.1"
128128
commit = false
129129
tag = false
130130

tests/conftest.py

Lines changed: 44 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,20 @@
55
66
Classes/Functions:
77
- Functions:
8-
- fixtures_dir() -> Path (line 33)
9-
- sample_python_file(tmp_path: Path) -> Iterator[Path] (line 39)
10-
- sample_kotlin_file(tmp_path: Path) -> Iterator[Path] (line 61)
11-
- sample_javascript_file(tmp_path: Path) -> Iterator[Path] (line 87)
12-
- sample_typescript_file(tmp_path: Path) -> Iterator[Path] (line 120)
13-
- sample_csharp_file(tmp_path: Path) -> Iterator[Path] (line 155)
14-
- sample_cpp_file(tmp_path: Path) -> Iterator[Path] (line 198)
15-
- complex_python_file(tmp_path: Path) -> Iterator[Path] (line 249)
16-
- python_file_with_existing_header(tmp_path: Path) -> Iterator[Path] (line 357)
17-
- multilanguage_project(tmp_path: Path) -> Iterator[Path] (line 390)
18-
- empty_files_project(tmp_path: Path) -> Iterator[Path] (line 427)
19-
- sample_files_by_language(tmp_path: Path) -> Iterator[Dict[str, Path]] (line 447)
20-
- malformed_files_project(tmp_path: Path) -> Iterator[Path] (line 479)
8+
- source_processor(tmp_path: Path) (line 36)
9+
- fixtures_dir() -> Path (line 64)
10+
- sample_python_file(tmp_path: Path) -> Iterator[Path] (line 70)
11+
- sample_kotlin_file(tmp_path: Path) -> Iterator[Path] (line 92)
12+
- sample_javascript_file(tmp_path: Path) -> Iterator[Path] (line 118)
13+
- sample_typescript_file(tmp_path: Path) -> Iterator[Path] (line 151)
14+
- sample_csharp_file(tmp_path: Path) -> Iterator[Path] (line 186)
15+
- sample_cpp_file(tmp_path: Path) -> Iterator[Path] (line 229)
16+
- complex_python_file(tmp_path: Path) -> Iterator[Path] (line 280)
17+
- python_file_with_existing_header(tmp_path: Path) -> Iterator[Path] (line 388)
18+
- multilanguage_project(tmp_path: Path) -> Iterator[Path] (line 421)
19+
- empty_files_project(tmp_path: Path) -> Iterator[Path] (line 458)
20+
- sample_files_by_language(tmp_path: Path) -> Iterator[Dict[str, Path]] (line 478)
21+
- malformed_files_project(tmp_path: Path) -> Iterator[Path] (line 510)
2122
--- END AUTO-GENERATED DOCSTRING ---
2223
"""
2324
from __future__ import annotations
@@ -28,6 +29,36 @@
2829

2930
import pytest
3031

32+
from agent_docstrings.core import process_file
33+
34+
35+
@pytest.fixture
36+
def source_processor(tmp_path: Path):
37+
"""A factory fixture that returns a helper function to process source code.
38+
39+
The helper function creates a temporary file with the given source code,
40+
runs the main `process_file` logic on it, and returns the results.
41+
42+
Returns:
43+
A callable that takes a filename and source code string, and returns
44+
a tuple containing:
45+
- The processed file content (str)
46+
- The processed file content as a list of lines (list[str])
47+
- The path to the processed file (Path)
48+
"""
49+
50+
def _process(
51+
filename: str, source_code: str, verbose: bool = False
52+
) -> tuple[str, list[str], Path]:
53+
"""Creates a file with source_code, runs process_file, and returns content."""
54+
source_path = tmp_path / filename
55+
source_path.write_text(source_code, encoding="utf-8")
56+
process_file(source_path, verbose=verbose)
57+
content = source_path.read_text(encoding="utf-8")
58+
return content, content.splitlines(), source_path
59+
60+
return _process
61+
3162

3263
@pytest.fixture(scope="session")
3364
def fixtures_dir() -> Path:

0 commit comments

Comments
 (0)