From cce97b3550af479882b8ef9a672a2f23ed3cfc18 Mon Sep 17 00:00:00 2001 From: Artemonim Date: Tue, 1 Jul 2025 11:13:52 +0300 Subject: [PATCH 01/12] Remove redundant tests for version bump without changes in `test_core.py` --- tests/test_core.py | 39 --------------------------------------- 1 file changed, 39 deletions(-) diff --git a/tests/test_core.py b/tests/test_core.py index ebb81ab..96845db 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -32,8 +32,6 @@ - test_process_file_read_error(mock_read, tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None (line 403) - test_process_file_write_error(mock_write, tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None (line 414) - test_process_file_parser_error(source_processor) -> None (line 425) - - test_process_file_version_bump_no_changes(source_processor) -> None (line 430) - - test_process_file_version_bump_no_changes_verbose(source_processor, capsys: pytest.CaptureFixture[str]) -> None (line 449) - TestDiscoverAndProcessFiles (line 468): - test_discover_single_directory(tmp_path: Path) -> None (line 471) - test_discover_multiple_directories(tmp_path: Path) -> None (line 486) @@ -408,43 +406,6 @@ def test_process_file_parser_error(self, source_processor) -> None: # It should not crash source_processor("malformed.py", "def func(a,:", verbose=True) - def test_process_file_version_bump_no_changes(self, source_processor) -> None: - """Test that version bump in header without code changes updates the docstring to current version.""" - content = '''""" - --- AUTO-GENERATED DOCSTRING --- - This docstring is automatically generated by Agent Docstrings v1.2.0 - Do not modify this block directly. - - Classes/Functions: - - foo() (line 1) - --- END AUTO-GENERATED DOCSTRING --- -""" - def foo(): - pass''' - processed_content, _, _ = source_processor("test.py", content, verbose=False) - # The header version should be updated to the current version - assert "generated by Agent Docstrings v" in processed_content - assert "v1.3.0" in processed_content - assert "v1.2.0" not in processed_content - - def test_process_file_version_bump_no_changes_verbose(self, source_processor, capsys: pytest.CaptureFixture[str]) -> None: - """Test that version-only header changes produce a processed message when verbose.""" - content = '''""" - --- AUTO-GENERATED DOCSTRING --- - This docstring is automatically generated by Agent Docstrings v1.2.0 - Do not modify this block directly. - - Classes/Functions: - - foo() (line 1) - --- END AUTO-GENERATED DOCSTRING --- -""" - def foo(): - pass''' - source_processor("test.py", content, verbose=True) - captured = capsys.readouterr() - # Should indicate that the file was processed - assert "Processed Python:" in captured.out - class TestDiscoverAndProcessFiles: """Tests for discover_and_process_files function.""" From 69a6ae6462e330ace2840be411435e9cde27c809 Mon Sep 17 00:00:00 2001 From: Artemonim Date: Tue, 1 Jul 2025 11:40:06 +0300 Subject: [PATCH 02/12] ci optimization --- .github/workflows/ci.yml | 211 +++++++++++++++++++++------------------ 1 file changed, 112 insertions(+), 99 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e07969a..b976939 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,99 +1,112 @@ -name: CI - -on: - push: - branches: [master, dev] - pull_request_target: - branches: [master, dev] - -jobs: - test: - name: Test on Python ${{ matrix.python-version }} (beta=${{ matrix.beta }}) - runs-on: ubuntu-latest - strategy: - matrix: - python-version: ["3.10", "3.11", "3.12", "3.13"] - beta: [false, true] - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - ref: ${{ github.event.pull_request.head.sha }} - - - name: Set up Go - uses: actions/setup-go@v5 - with: - go-version: "1.22" - - - name: Build Go parsers - run: pwsh -File ./build_goparser.ps1 - shell: bash - - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install .[dev] - - - name: CLI smoke test - run: | - agent-docstrings --version - if [ "${{ matrix.beta }}" = "true" ]; then - agent-docstrings --beta --version - fi - - - name: Run tests with coverage - run: | - pytest --cov=agent_docstrings --cov-report=xml --cov-report=term-missing - - - name: Upload coverage artifact - uses: actions/upload-artifact@v4 - with: - name: coverage-${{ matrix.python-version }}-${{ matrix.beta }} - path: coverage.xml - - report: - name: Report Coverage - if: github.ref == 'refs/heads/master' - runs-on: ubuntu-latest - needs: test - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Download all coverage artifacts - uses: actions/download-artifact@v4 - with: - path: coverage-artifacts - pattern: coverage-* - merge-multiple: true - - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v5 - with: - token: ${{ secrets.CODECOV_TOKEN }} - directory: ./coverage-artifacts/ - fail_ci_if_error: false - - check-version: - name: Check for accidental version bump - if: github.base_ref == 'dev' - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Verify that version was not bumped - run: | - if ! git diff --quiet origin/dev HEAD -- pyproject.toml; then - echo "::error::Version in pyproject.toml was changed in a PR to dev." - echo "Version bumping should only happen in a release PR to master." - exit 1 - fi - echo "Version check passed for pyproject.toml" +name: CI + +on: + # Run on pull requests into dev or master. + pull_request: + branches: [master, dev] + # After a PR is merged, the merge commit is pushed to master; we still want tests + coverage once on the resulting commit. + push: + branches: [master] + +jobs: + test: + # * Runs unit-test matrix: + # - Always on pull_request (dev or master) + # - On push to master (after merge) + if: | + github.event_name == 'pull_request' || + (github.event_name == 'push' && github.ref == 'refs/heads/master') + name: Test on Python ${{ matrix.python-version }} (beta=${{ matrix.beta }}) + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.10", "3.11", "3.12", "3.13"] + beta: [false, true] + steps: + - name: Checkout repository + uses: actions/checkout@v4 + # For pull_request we check out the PR commit; for push we stay on the pushed ref (master). + with: + ref: ${{ github.event.pull_request.head.sha }} + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: "1.22" + + - name: Build Go parsers + run: pwsh -File ./build_goparser.ps1 + shell: bash + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install .[dev] + + - name: CLI smoke test + run: | + agent-docstrings --version + if [ "${{ matrix.beta }}" = "true" ]; then + agent-docstrings --beta --version + fi + + - name: Run tests with coverage + run: | + pytest --cov=agent_docstrings --cov-report=xml --cov-report=term-missing + + - name: Upload coverage artifact + uses: actions/upload-artifact@v4 + with: + name: coverage-${{ matrix.python-version }}-${{ matrix.beta }} + path: coverage.xml + + report: + # * Only for master: either in PR to master (so reviewers see comment) or after merge push to master. + if: | + (github.event_name == 'pull_request' && github.base_ref == 'master') || + (github.event_name == 'push' && github.ref == 'refs/heads/master') + name: Report Coverage + runs-on: ubuntu-latest + needs: test + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Download all coverage artifacts + uses: actions/download-artifact@v4 + with: + path: coverage-artifacts + pattern: coverage-* + merge-multiple: true + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} + directory: ./coverage-artifacts/ + fail_ci_if_error: false + + check-version: + # * Only on PRs into dev: prevent accidental version bumps. + if: github.event_name == 'pull_request' && github.base_ref == 'dev' + name: Check for accidental version bump + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Verify that version was not bumped + run: | + if ! git diff --quiet origin/dev HEAD -- pyproject.toml; then + echo "::error::Version in pyproject.toml was changed in a PR to dev." + echo "Version bumping should only happen in a release PR to master." + exit 1 + fi + echo "Version check passed for pyproject.toml" From 549499814b1514bea14f052b01285e01fe501906 Mon Sep 17 00:00:00 2001 From: Artemonim Date: Wed, 2 Jul 2025 12:15:14 +0300 Subject: [PATCH 03/12] Enhance header preservation for Kotlin ### Fixed - **Kotlin Header Preservation**: Fixed a bug where multi-line block comments (`/** ... */`) in Kotlin files were not correctly preserved, leading to malformed docstrings. The header parsing logic now correctly identifies and preserves these comment blocks. (Fixes #9) ### Added - Added a new test to verify the preservation of multi-line comments in Kotlin files. --- CHANGELOG.md | 4 +++ agent_docstrings/core.py | 30 ++++++++++++++----- .../fixtures/kotlin_with_multiline_comment.kt | 11 +++++++ tests/test_header_preservation.py | 24 ++++++++++++++- 4 files changed, 61 insertions(+), 8 deletions(-) create mode 100644 tests/fixtures/kotlin_with_multiline_comment.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index d604f4f..e3bfe00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [NextRelease] +### Fixed + +- **Kotlin Header Preservation**: Fixed a bug where multi-line block comments (`/** ... */`) in Kotlin files were not correctly preserved, leading to malformed docstrings. The header parsing logic now correctly identifies and preserves these comment blocks. (Fixes #9) + ## [1.3.1] ### Added diff --git a/agent_docstrings/core.py b/agent_docstrings/core.py index 8744083..591f497 100644 --- a/agent_docstrings/core.py +++ b/agent_docstrings/core.py @@ -338,17 +338,33 @@ def get_preserved_header_end_line(lines: List[str], language: str) -> int: if line.strip().startswith("package "): return i + 1 return 0 - # General check for JS, TS, C# + # General check for JS, TS, C#, C++, Java, Kotlin + in_block_comment = False for i, line in enumerate(lines): stripped = line.strip() - if not ( - stripped.startswith(tuple(["//", "/*", "*/"])) # Allow comment blocks - or stripped.startswith("using ") + + if in_block_comment: + if "*/" in stripped: + in_block_comment = False + continue + + if stripped.startswith("/*"): + if "*/" not in stripped: + in_block_comment = True + continue + + if ( + stripped.startswith("//") or stripped.startswith("import ") + or stripped.startswith("using ") + or stripped.startswith("package ") ): - # ! Stop at the first non-header line (including empty lines) - # * Empty lines are not considered preserved headers - return i + continue + + # If we're not in a block comment and the line is not a recognized + # header element, then the header is over. This includes empty lines. + return i + return len(lines) diff --git a/tests/fixtures/kotlin_with_multiline_comment.kt b/tests/fixtures/kotlin_with_multiline_comment.kt new file mode 100644 index 0000000..2c3008c --- /dev/null +++ b/tests/fixtures/kotlin_with_multiline_comment.kt @@ -0,0 +1,11 @@ +/** + * This is a multi-line comment that should be preserved. + * It exists to test the header preservation logic. + */ +package com.example + +class MyClass { + fun myMethod() { + // method body + } +} \ No newline at end of file diff --git a/tests/test_header_preservation.py b/tests/test_header_preservation.py index 7f6e72f..c51ff41 100644 --- a/tests/test_header_preservation.py +++ b/tests/test_header_preservation.py @@ -5,6 +5,7 @@ Classes/Functions: - test_header_preservation(source_processor, ext, header_lines, lang) (line 25) - test_future_import_preservation(source_processor) -> None (line 59) + - test_kotlin_multiline_comment_preservation(source_processor) -> None (line 79) --- END AUTO-GENERATED DOCSTRING --- """ import pytest @@ -76,4 +77,25 @@ def test_future_import_preservation(source_processor) -> None: # The second line should be empty (preserved) assert result_lines[1] == "" # The third line should be the start of our docstring - assert result_lines[2] == '"""' \ No newline at end of file + assert result_lines[2] == '"""' + +def test_kotlin_multiline_comment_preservation(source_processor) -> None: + """ + Verifies that a multi-line block comment at the start of a Kotlin file is preserved. + This is to address the bug reported in issue #9. + """ + fixture_path = Path(__file__).parent / "fixtures" / "kotlin_with_multiline_comment.kt" + original_content = fixture_path.read_text(encoding="utf-8") + + file_path, result_lines, _ = source_processor("test.kt", original_content) + + processed_content = '\n'.join(result_lines) + + # The original multiline comment should be at the start of the file + assert processed_content.strip().startswith("/**") + assert "This is a multi-line comment that should be preserved." in processed_content + + # The autogenerated docstring should come *after* the manual comment + # and package declaration. + assert "--- AUTO-GENERATED DOCSTRING ---" in processed_content + assert processed_content.find("*/") < processed_content.find("--- AUTO-GENERATED DOCSTRING ---") \ No newline at end of file From d0390badb5a546d800c6d9b3791adfdab2829d05 Mon Sep 17 00:00:00 2001 From: Artemonim Date: Sat, 5 Jul 2025 23:58:30 +0300 Subject: [PATCH 04/12] Update version to 1.3.2 ### Fixed - **Kotlin Header Preservation** --- CHANGELOG.md | 4 ++++ agent_docstrings/__init__.py | 2 +- pyproject.toml | 6 +++--- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e3bfe00..7afd2b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [NextRelease] +- **Something great**: for sure + +## [1.3.2] + ### Fixed - **Kotlin Header Preservation**: Fixed a bug where multi-line block comments (`/** ... */`) in Kotlin files were not correctly preserved, leading to malformed docstrings. The header parsing logic now correctly identifies and preserves these comment blocks. (Fixes #9) diff --git a/agent_docstrings/__init__.py b/agent_docstrings/__init__.py index e58120e..cc3defe 100644 --- a/agent_docstrings/__init__.py +++ b/agent_docstrings/__init__.py @@ -7,4 +7,4 @@ Attributes: __version__ (str): Current version of the *agent-docstrings* package. """ -__version__ = "1.3.1" \ No newline at end of file +__version__ = "1.3.2" \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index a600663..ffc4d56 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "agent-docstrings" -version = "1.3.1" +version = "1.3.2" 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." readme = { file = "README.md", content-type = "text/markdown" } license = { file = "LICENSE" } @@ -148,7 +148,7 @@ exclude_lines = [ ] [tool.bumpversion] -current_version = "1.3.1" +current_version = "1.3.2" commit = false tag = false @@ -165,4 +165,4 @@ replace = '__version__ = "{new_version}"' [[tool.bumpversion.files]] filename = "CHANGELOG.md" search = "## [{current_version}]" -replace = "## [{new_version}]\n\n### Header\n\n- **subtitle**:\n - text\n\n## [{current_version}]" \ No newline at end of file +replace = "## [{new_version}]\n\n### Header\n\n- **subtitle**: describtion\n\n## [{current_version}]" \ No newline at end of file From b50433149a590201f27ed4206d44f98b7b232fc8 Mon Sep 17 00:00:00 2001 From: Artemonim Date: Sun, 6 Jul 2025 01:23:09 +0300 Subject: [PATCH 05/12] Enhance README.md for Agent Docstrings ### Changed - Updated the description of Agent Docstrings to clarify its role in solving the "cold start" problem for AI agents navigating large codebases. - Added a new section detailing the advantages of using Agent Docstrings, contrasting the "Blind" approach with the "Map-First" approach. - Introduced a "Support the Project" section to encourage contributions and support for the ongoing development of the tool. ### Improved - Enhanced clarity and engagement in the README to better communicate the tool's benefits and encourage community involvement. --- README.md | 39 ++++++++++++++++++++++++++++++++++----- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index d172691..25274de 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ -This is especially useful for AI-Agents: quickly understanding large files, navigating unfamiliar codebases, etc. +This is especially useful for AI-Agents, helping them solve the "cold start" problem of quickly understanding and navigating large, unfamiliar codebases. --- @@ -63,10 +63,25 @@ This is especially useful for AI-Agents: quickly understanding large files, navi ## Why Use Agent Docstrings? -**Agent Docstrings** providing a scannable "Table of Contents" at the beginning of each file. This offers several key advantages: +Imagine an AI agent tasked with modifying a large, unfamiliar codebase. Its first step is to read a file to get its bearings. What if the first thing it saw was a perfect summary? -- **Faster Onboarding**: AI Agents can familiarize themselves with the codebase much faster. The generated docstring acts as a map to the file's contents. -- **Improved Code Navigation**: AI get an immediate high-level overview of any file's structure without reading its entire content. Jump directly to the code it need. +#### Without Agent Docstrings: The "Blind" Approach + +An AI agent opens a file and has no initial context. To understand the file's structure, it must: +1. Read a large chunk of the file. +2. Use tools like `grep_tool` or other search methods to find function and class definitions. +3. Analyze and piece together the results to build a mental map of the file. +This process is slow, api-intensive, and prone to error. + +#### With Agent Docstrings: The "Map-First" Approach + +The agent opens the same file. The very first thing it reads is a "Table of Contents" generated by this tool. This provides immediate, critical advantages: + +- **Solves the "Cold Start" Problem**: The agent instantly understands the file's layout, classes, and functions without any prior knowledge. The docstring acts as a "map" for the new territory, providing an immediate entry point for analysis. +- **Dramatically Boosts Efficiency**: Gaining this structural overview is a single `read_tool` operation. This is far more efficient than performing multiple searches and analyses to build the same context from scratch. +- **Enhances Situational Awareness**: With a clear overview from the start, the agent's subsequent actions (like targeted code searches or modifications) become more precise and intelligent. Knowing that a function `integrate_user_data` exists allows for a much more focused approach than a broad search for "user data". + +In short, **Agent Docstrings** gives an AI a crucial head start, turning a slow, investigative process into a quick, informed action. ## Features @@ -304,6 +319,20 @@ The tool is configured in `pyproject.toml` to automatically update the version s **Note**: Per project configuration, this tool only modifies the files. You will need to commit and tag the changes manually after bumping the version. +## Support the Project + +Agent Docstrings is an independent open-source project. If you find this tool useful and want to support its ongoing development, your help would be greatly appreciated. + +Here are a few ways you can contribute: + +- **Give a Star:** The simplest way to show your support is to star the project on [GitHub](https://github.com/Artemonim/AgentDocstrings)! It increases the project's visibility. +- **Support My Work:** Your financial contribution helps me dedicate more time to improving this tool and creating other open-source projects. On my [**Boosty page**](https://boosty.to/artemonim), you can: + - Make a **one-time donation** to thank me for this specific project. + - Become a **monthly supporter** to help all of my creative endeavors. +- **Try a Recommended Tool:** This project was inspired by my work with LLMs. If you're looking for a great service to work with multiple neural networks, check out [**Syntx AI**](https://t.me/syntxaibot?start=aff_157453205). Using my referral link is another way to support my work at no extra cost to you. + +Thank you for your support! + ## Contributing 1. Fork the repository @@ -324,4 +353,4 @@ See [CHANGELOG.md](CHANGELOG.md) for a list of changes and version history. - **Issues**: [GitHub Issues](https://github.com/Artemonim/agent-docstrings/issues) - **Documentation**: [GitHub README](https://github.com/Artemonim/agent-docstrings#readme) -- **Source Code**: [GitHub Repository](https://github.com/Artemonim/agent-docstrings) +- **Source Code**: [GitHub Repository](https://github.com/Artemonim/agent-docstrings) \ No newline at end of file From 76d45e0fe6e2ac952300806f95740353b3c451e4 Mon Sep 17 00:00:00 2001 From: Artemonim Date: Sun, 6 Jul 2025 01:25:07 +0300 Subject: [PATCH 06/12] fix: README ToC --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 25274de..0a17f2d 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ This is especially useful for AI-Agents, helping them solve the "cold start" pro - [Limitations and Nuances](#limitations-and-nuances) - [Integration with Development Workflow](#integration-with-development-workflow) - [Development](#development) +- [Support the Project](#Support) - [Contributing](#contributing) - [License](#license) - [Changelog](#changelog) From 78737c6fd9dbd4b23f541563f6e4cd1ea75d5277 Mon Sep 17 00:00:00 2001 From: Artemonim Date: Sun, 6 Jul 2025 06:42:44 +0300 Subject: [PATCH 07/12] fix README.md another day, another fix --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0a17f2d..61f3a94 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ This is especially useful for AI-Agents, helping them solve the "cold start" pro - [Limitations and Nuances](#limitations-and-nuances) - [Integration with Development Workflow](#integration-with-development-workflow) - [Development](#development) -- [Support the Project](#Support) +- [Support the Project](#support-the-project) - [Contributing](#contributing) - [License](#license) - [Changelog](#changelog) @@ -354,4 +354,4 @@ See [CHANGELOG.md](CHANGELOG.md) for a list of changes and version history. - **Issues**: [GitHub Issues](https://github.com/Artemonim/agent-docstrings/issues) - **Documentation**: [GitHub README](https://github.com/Artemonim/agent-docstrings#readme) -- **Source Code**: [GitHub Repository](https://github.com/Artemonim/agent-docstrings) \ No newline at end of file +- **Source Code**: [GitHub Repository](https://github.com/Artemonim/agent-docstrings) From e65c91de4b1d8acd630ba8a22622862e56450fc1 Mon Sep 17 00:00:00 2001 From: Artemonim Date: Sun, 6 Jul 2025 12:20:38 +0300 Subject: [PATCH 08/12] fix: docstring duplication ### Fixed - **Python Docstring Cleaning**: (fixes #11) - **C-Style Comment Handling**: --- CHANGELOG.md | 5 +- agent_docstrings/languages/common.py | 82 +++++-------- tests/test_common.py | 44 +++---- tests/test_docstring_duplication.py | 170 ++++++++++++++++++++++++++ tests/test_whitespace_preservation.py | 35 ++++++ 5 files changed, 257 insertions(+), 79 deletions(-) create mode 100644 tests/test_docstring_duplication.py create mode 100644 tests/test_whitespace_preservation.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 7afd2b0..533fd7f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [NextRelease] -- **Something great**: for sure +### Fixed + +- **Python Docstring Cleaning**: Improved the `remove_agent_docstring` function to better handle Python docstrings by preserving manual content while removing auto-generated table of contents. The function now correctly identifies and removes only the auto-generated content while maintaining the structure of existing manual docstrings. (fixes #11) +- **C-Style Comment Handling**: Enhanced the docstring removal logic for C-style languages (Kotlin, Java, Go, etc.) to be more flexible with comment formatting variations, ensuring proper detection and removal of auto-generated content across different comment styles. ## [1.3.2] diff --git a/agent_docstrings/languages/common.py b/agent_docstrings/languages/common.py index ef987a2..80ae3b2 100644 --- a/agent_docstrings/languages/common.py +++ b/agent_docstrings/languages/common.py @@ -1,16 +1,15 @@ """ --- AUTO-GENERATED DOCSTRING --- - Table of content is automatically generated by Agent Docstrings v1.3.0 + Table of content is automatically generated by Agent Docstrings v1.3.2 Classes/Functions: - - SignatureInfo (line 20): - - ClassInfo (line 26): - - CommentStyle (line 34): - - remove_agent_docstring(text: str, language: str) -> str (line 57) + - SignatureInfo (line 17): + - ClassInfo (line 21): + - CommentStyle (line 27): + - remove_agent_docstring(text: str, language: str) -> str (line 46) --- END AUTO-GENERATED DOCSTRING --- """ from __future__ import annotations - import re from typing import List, Tuple, Dict, NamedTuple @@ -22,7 +21,6 @@ class SignatureInfo(NamedTuple): signature: str line: int - class ClassInfo(NamedTuple): """Stores information about a parsed class, including its methods.""" name: str @@ -30,7 +28,6 @@ class ClassInfo(NamedTuple): methods: List[SignatureInfo] inner_classes: List["ClassInfo"] - class CommentStyle(NamedTuple): """Stores language-specific comment formatting information.""" start: str @@ -38,7 +35,6 @@ class CommentStyle(NamedTuple): prefix: str indent: str - COMMENT_STYLES: Dict[str, CommentStyle] = { "python": CommentStyle('"""', '"""', " ", " "), "kotlin": CommentStyle('/**', ' */', ' * ', " "), @@ -53,63 +49,43 @@ class CommentStyle(NamedTuple): "delphi": CommentStyle('(*', '*)', ' * ', " "), } - def remove_agent_docstring(text: str, language: str) -> str: - """Remove a previously generated docstring from *text*. - - The search uses language-specific comment patterns to find a block - containing DOCSTRING_START_MARKER and DOCSTRING_END_MARKER at the - beginning of the file, and removes it. - - Args: - text (str): Full contents of the source file. - language (str): Canonical language name (e.g. ``"python"``) used - to pick the correct comment delimiters from - :data:`COMMENT_STYLES`. - - Returns: - str: *text* without the agent docstring block. If no such - docstring is detected, *text* is returned unchanged. - """ + """Remove a previously generated docstring from *text*.""" style = COMMENT_STYLES[language] - - # ! Create a more flexible pattern that can match various formats start_marker_escaped = re.escape(DOCSTRING_START_MARKER) end_marker_escaped = re.escape(DOCSTRING_END_MARKER) - if language == "python": - # * Python uses triple quotes - check for new format first - pattern = re.compile( - rf'^\s*"""\s*{start_marker_escaped}.*?{end_marker_escaped}\s*"""\s*\n?', - re.DOTALL - ) - match = pattern.search(text) - if match: - return text[match.end():] - - # * Also check for old format (without proper markers) - old_format_pattern = re.compile( - rf'^\s*"""\s*Classes/Functions:.*?"""\s*\n?', - re.DOTALL - ) - match = old_format_pattern.search(text) - if match: - return text[match.end():] + def replacer(match): + docstring_content = match.group(0) + auto_content_pattern = re.compile( + rf'\s*{start_marker_escaped}[\s\S]*?{end_marker_escaped}\s*?\n?', + re.DOTALL + ) + cleaned_docstring = auto_content_pattern.sub('', docstring_content) + temp_cleaned = cleaned_docstring.replace('"""', '').replace("'''", '').strip() + if not temp_cleaned: + return '' # Remove empty docstring + # Ensure single newline padding for non-empty manual comments + return f'"""\n{temp_cleaned}\n"""' + docstring_pattern = re.compile(r'^\s*("""[\s\S]*?"""|'r"'''[\s\S]*?''')") + # Iteratively clean the text + cleaned_text = docstring_pattern.sub(replacer, text) + cleaned_text = docstring_pattern.sub(replacer, cleaned_text) # Run again to handle adjacent blocks + # Collapse whitespace and return + return cleaned_text.strip() else: - # * For C-style comments, be more flexible with the format - # * Handle both compact (/**---...---*/) and expanded formats + # For C-style comments, be more flexible with the format + # Handle both compact (/**---...---*/) and expanded formats start_escaped = re.escape(style.start.rstrip()) # Remove trailing spaces - - # * Handle different possible endings (with or without space before *) + # Handle different possible endings (with or without space before *) end_patterns = [ re.escape(style.end), # Original format with space re.escape(style.end.strip()), # Without space ] - - # * Try each possible end pattern + # Try each possible end pattern for end_pattern in end_patterns: pattern = re.compile( - rf'^\s*{start_escaped}.*?{start_marker_escaped}.*?{end_marker_escaped}.*?{end_pattern}\s*\n?', + rf'^\s*{start_escaped}[\s\S]*?{start_marker_escaped}[\s\S]*?{end_marker_escaped}[\s\S]*?{end_pattern}\s*\n?', re.DOTALL ) match = pattern.search(text) diff --git a/tests/test_common.py b/tests/test_common.py index 5945644..e0a176e 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -2,34 +2,31 @@ """ --- AUTO-GENERATED DOCSTRING --- - Table of content is automatically generated by Agent Docstrings v1.3.1 + Table of content is automatically generated by Agent Docstrings v1.3.2 Classes/Functions: - - TestDataClasses (line 40): - - test_signature_info_creation() -> None (line 43) - - test_class_info_creation() -> None (line 49) - - test_comment_style_creation() -> None (line 68) - - TestCommentStyles (line 77): - - test_all_supported_languages_have_styles() -> None (line 80) - - test_comment_style_values(language: str, expected_start: str, expected_end: str, expected_prefix: str, expected_indent: str) -> None (line 97) - - TestHeaderStripping (line 113): - - test_strip_python_header() -> None (line 116) - - test_strip_block_comment_header() -> None (line 141) - - test_strip_c_style_comment_header() -> None (line 159) - - test_no_header_to_strip() -> None (line 177) - - test_preserve_shebang_when_stripping() -> None (line 186) - - test_strip_header_with_various_whitespace() -> None (line 199) - - test_strip_only_first_matching_header() -> None (line 207) - - test_strip_header_edge_cases() -> None (line 223) - - test_header_not_at_start() -> None (line 235) - - test_invalid_language_patterns(language: str) -> None (line 249) + - TestDataClasses (line 37): + - test_signature_info_creation() -> None (line 39) + - test_class_info_creation() -> None (line 44) + - test_comment_style_creation() -> None (line 60) + - TestCommentStyles (line 67): + - test_all_supported_languages_have_styles() -> None (line 69) + - test_comment_style_values(language: str, expected_start: str, expected_end: str, expected_prefix: str, expected_indent: str) -> None (line 85) + - TestHeaderStripping (line 99): + - test_strip_python_header() -> None (line 101) + - test_strip_block_comment_header() -> None (line 121) + - test_strip_c_style_comment_header() -> None (line 136) + - test_no_header_to_strip() -> None (line 151) + - test_preserve_shebang_when_stripping() -> None (line 158) + - test_strip_header_with_various_whitespace() -> None (line 169) + - test_strip_only_first_matching_header() -> None (line 175) + - test_strip_header_edge_cases() -> None (line 189) + - test_header_not_at_start() -> None (line 198) + - test_invalid_language_patterns(language: str) -> None (line 209) --- END AUTO-GENERATED DOCSTRING --- Tests for agent_docstrings.languages.common module. """ - - import pytest - from agent_docstrings.languages.common import ( COMMENT_STYLES, ClassInfo, @@ -40,7 +37,6 @@ DOCSTRING_END_MARKER, ) - class TestDataClasses: """Tests for data classes used in parsing.""" @@ -77,7 +73,6 @@ def test_comment_style_creation(self) -> None: assert style.prefix == " * " assert style.indent == " " - class TestCommentStyles: """Tests for comment style definitions.""" @@ -113,7 +108,6 @@ def test_comment_style_values( assert style.prefix == expected_prefix assert style.indent == expected_indent - class TestHeaderStripping: """Tests for remove_agent_docstring function.""" diff --git a/tests/test_docstring_duplication.py b/tests/test_docstring_duplication.py new file mode 100644 index 0000000..00f67c9 --- /dev/null +++ b/tests/test_docstring_duplication.py @@ -0,0 +1,170 @@ +""" + --- AUTO-GENERATED DOCSTRING --- + Table of content is automatically generated by Agent Docstrings v1.3.2 + + Classes/Functions: + - test_no_docstring_duplication_on_repeated_runs(source_processor) -> None (line 15) + - test_manual_docstring_preservation_with_auto_generation(source_processor) -> None (line 56) + - test_existing_auto_docstring_replacement(source_processor) -> None (line 100) + - test_multiple_auto_docstring_removal(source_processor) -> None (line 135) + --- END AUTO-GENERATED DOCSTRING --- +""" +import pytest +import re +from textwrap import dedent + +def test_no_docstring_duplication_on_repeated_runs(source_processor) -> None: + """ + Test that running the docstring generator multiple times on the same file + does not create duplicate auto-generated docstrings. + This test simulates the scenario where a file with manual docstring + gets processed multiple times, ensuring no double docstrings are created. + """ + # * Initial file with manual docstring + initial_content = dedent(''' + """ + Human comments + This is a manual docstring that should be preserved. + """ + def test_function(): + """This is a function docstring.""" + return "test" + class TestClass: + def method(self): + return "method" + ''').strip() + # * First run - should generate auto docstring and merge with manual + result_content_1, lines_1, _ = source_processor("test_duplication.py", initial_content) + # * Verify that auto-generated docstring was added + assert "--- AUTO-GENERATED DOCSTRING ---" in result_content_1 + assert "Human comments" in result_content_1 # Manual content preserved + assert "test_function()" in result_content_1 # Auto-generated content added + # * Count auto-generated docstring markers + auto_markers_1 = result_content_1.count("--- AUTO-GENERATED DOCSTRING ---") + assert auto_markers_1 == 1, f"Expected 1 auto docstring marker, found {auto_markers_1}" + # * Second run - should not create duplicate auto docstrings + result_content_2, lines_2, _ = source_processor("test_duplication.py", result_content_1) + # * Verify no duplication occurred + auto_markers_2 = result_content_2.count("--- AUTO-GENERATED DOCSTRING ---") + assert auto_markers_2 == 1, f"Expected 1 auto docstring marker after second run, found {auto_markers_2}" + # * Verify manual content is still preserved + assert "Human comments" in result_content_2 + assert "This is a manual docstring that should be preserved." in result_content_2 + # * Verify auto-generated content is still present + assert "test_function()" in result_content_2 + assert "TestClass" in result_content_2 + assert "method()" in result_content_2 + +def test_manual_docstring_preservation_with_auto_generation(source_processor) -> None: + """ + Test that manual docstrings are properly preserved when auto-generating + docstrings, and that the structure is correct. + """ + # * File with manual docstring only + initial_content = dedent(''' + """ + This is a manual module docstring. + It should be preserved and merged with auto-generated content. + """ + def function_one(): + pass + def function_two(): + pass + ''').strip() + result_content, lines, _ = source_processor("test_manual_preservation.py", initial_content) + # * Verify structure: manual content should come after auto-generated content + lines_list = result_content.split('\n') + # * Find the docstring boundaries + docstring_start = None + docstring_end = None + manual_content_found = False + for i, line in enumerate(lines_list): + if line.strip() == '"""' and docstring_start is None: + docstring_start = i + elif line.strip() == '"""' and docstring_start is not None: + docstring_end = i + break + assert docstring_start is not None, "Docstring start not found" + assert docstring_end is not None, "Docstring end not found" + # * Extract docstring content + docstring_content = lines_list[docstring_start:docstring_end + 1] + docstring_text = '\n'.join(docstring_content) + # * Verify auto-generated content is first + assert "--- AUTO-GENERATED DOCSTRING ---" in docstring_text + assert "function_one()" in docstring_text + assert "function_two()" in docstring_text + # * Verify manual content is preserved + assert "This is a manual module docstring." in docstring_text + assert "It should be preserved and merged with auto-generated content." in docstring_text + # * Verify only one docstring block exists + docstring_blocks = result_content.count('"""') + assert docstring_blocks == 2, f"Expected 2 triple quotes (start and end), found {docstring_blocks}" + +def test_existing_auto_docstring_replacement(source_processor) -> None: + """ + Test that existing auto-generated docstrings are properly replaced + when the file is processed again. + """ + # * File with existing auto-generated docstring + initial_content = dedent(''' + """ + --- AUTO-GENERATED DOCSTRING --- + Table of content is automatically generated by Agent Docstrings v1.3.1 + Classes/Functions: + - old_function() (line 8) + --- END AUTO-GENERATED DOCSTRING --- + """ + def old_function(): + pass + def new_function(): + pass + ''').strip() + result_content, lines, _ = source_processor("test_replacement.py", initial_content) + # * Find the docstring in the result + docstring_match = re.search(r'"""[\s\S]*?"""', result_content) + assert docstring_match, "Could not find docstring in processed file" + docstring_text = docstring_match.group(0) + # * Verify new content is in the docstring + assert "old_function()" in docstring_text + assert "new_function()" in docstring_text + # * Verify only one auto-generated docstring exists in the whole file + auto_markers = result_content.count("--- AUTO-GENERATED DOCSTRING ---") + assert auto_markers == 1, f"Expected 1 auto docstring marker, found {auto_markers}" + # * Verify the version is updated in the docstring + assert "Agent Docstrings v1.3.2" in docstring_text + # * Verify that old_function is mentioned only once *within the docstring* + assert docstring_text.count("old_function()") == 1, "Function should appear only once in docstring" + assert docstring_text.count("new_function()") == 1, "Function should appear only once in docstring" + +def test_multiple_auto_docstring_removal(source_processor) -> None: + """ + Test that multiple auto-generated docstrings are properly removed + and replaced with a single one. + """ + # * File with multiple auto-generated docstrings (simulating a bug) + initial_content = dedent(''' + """ + --- AUTO-GENERATED DOCSTRING --- + Table of content is automatically generated by Agent Docstrings v1.3.1 + --- END AUTO-GENERATED DOCSTRING --- + """ + """ + --- AUTO-GENERATED DOCSTRING --- + Table of content is automatically generated by Agent Docstrings v1.3.2 + --- END AUTO-GENERATED DOCSTRING --- + Human comments + """ + def test_function(): + return "test" + ''').strip() + result_content, lines, _ = source_processor("test_multiple_removal.py", initial_content) + # * Verify only one auto-generated docstring exists + auto_markers = result_content.count("--- AUTO-GENERATED DOCSTRING ---") + assert auto_markers == 1, f"Expected 1 auto docstring marker, found {auto_markers}" + # * Verify manual content is preserved + assert "Human comments" in result_content + # * Verify function is documented + assert "test_function()" in result_content + # * Verify that there is only one docstring block in the final output + docstring_blocks = re.findall(r'"""[\s\S]*?"""', result_content) + assert len(docstring_blocks) == 1, f"Expected 1 docstring block, found {len(docstring_blocks)}" \ No newline at end of file diff --git a/tests/test_whitespace_preservation.py b/tests/test_whitespace_preservation.py new file mode 100644 index 0000000..f4b1c64 --- /dev/null +++ b/tests/test_whitespace_preservation.py @@ -0,0 +1,35 @@ +""" +Tests to ensure that the docstring generator preserves important whitespace. +""" +from textwrap import dedent + +def test_blank_lines_are_preserved(source_processor) -> None: + """ + Verifies that blank lines between functions and classes are not removed. + """ + initial_content = dedent(''' + class FirstClass: + pass + + + def top_level_function(): + pass + + ''').strip() + + result_content, _, _ = source_processor("whitespace_test.py", initial_content) + + # After processing, there should still be a blank line between the class and function + # The docstring generation will add its own lines, so we can't do a direct + # line-by-line comparison, but we can check for the pattern. + expected_pattern = "class FirstClass:\n pass\n\n\ndef top_level_function():" + + # We normalize the result content by removing the docstring to make the test reliable + from agent_docstrings.languages.common import remove_agent_docstring + cleaned_result = remove_agent_docstring(result_content, 'python') + + # The cleaned result should have the preserved blank lines. + # Note: The exact number of newlines might differ slightly based on how the + # docstring is inserted, so we check for at least two newlines. + assert "pass\n\n\ndef" in cleaned_result, \ + f"Expected blank lines to be preserved. Cleaned result:\n{cleaned_result}" \ No newline at end of file From 0bce370f9999f0da8b908ff4b4593da6701d792c Mon Sep 17 00:00:00 2001 From: Artemonim Date: Sun, 6 Jul 2025 12:36:29 +0300 Subject: [PATCH 09/12] Patch Update 1.3.3 ### Fixed - **Python Docstring Cleaning**: (fixes #11) - **C-Style Comment Handling**: --- CHANGELOG.md | 2 ++ agent_docstrings/__init__.py | 2 +- pyproject.toml | 4 ++-- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 533fd7f..68e0bd4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [NextRelease] +## [1.3.3] + ### Fixed - **Python Docstring Cleaning**: Improved the `remove_agent_docstring` function to better handle Python docstrings by preserving manual content while removing auto-generated table of contents. The function now correctly identifies and removes only the auto-generated content while maintaining the structure of existing manual docstrings. (fixes #11) diff --git a/agent_docstrings/__init__.py b/agent_docstrings/__init__.py index cc3defe..f48ede6 100644 --- a/agent_docstrings/__init__.py +++ b/agent_docstrings/__init__.py @@ -7,4 +7,4 @@ Attributes: __version__ (str): Current version of the *agent-docstrings* package. """ -__version__ = "1.3.2" \ No newline at end of file +__version__ = "1.3.3" \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index ffc4d56..7b8d562 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "agent-docstrings" -version = "1.3.2" +version = "1.3.3" 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." readme = { file = "README.md", content-type = "text/markdown" } license = { file = "LICENSE" } @@ -148,7 +148,7 @@ exclude_lines = [ ] [tool.bumpversion] -current_version = "1.3.2" +current_version = "1.3.3" commit = false tag = false From 3aa5337f5cffad9db5fc91fc5c07c72f5fad58c5 Mon Sep 17 00:00:00 2001 From: Artemonim Date: Sun, 6 Jul 2025 13:04:26 +0300 Subject: [PATCH 10/12] fix: update docstring versioning in tests ### Changed - Updated the test for existing auto docstring replacement to dynamically check the version of Agent Docstrings using the `__version__` variable instead of a hardcoded string. --- tests/test_docstring_duplication.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_docstring_duplication.py b/tests/test_docstring_duplication.py index 00f67c9..e8440e2 100644 --- a/tests/test_docstring_duplication.py +++ b/tests/test_docstring_duplication.py @@ -12,6 +12,7 @@ import pytest import re from textwrap import dedent +from agent_docstrings import __version__ def test_no_docstring_duplication_on_repeated_runs(source_processor) -> None: """ @@ -131,7 +132,7 @@ def new_function(): auto_markers = result_content.count("--- AUTO-GENERATED DOCSTRING ---") assert auto_markers == 1, f"Expected 1 auto docstring marker, found {auto_markers}" # * Verify the version is updated in the docstring - assert "Agent Docstrings v1.3.2" in docstring_text + assert f"Agent Docstrings v{__version__}" in docstring_text # * Verify that old_function is mentioned only once *within the docstring* assert docstring_text.count("old_function()") == 1, "Function should appear only once in docstring" assert docstring_text.count("new_function()") == 1, "Function should appear only once in docstring" From a86260d962e2c6d7acad2691f92dc3b4ac3625a1 Mon Sep 17 00:00:00 2001 From: Artemonim Date: Tue, 8 Jul 2025 01:03:12 +0300 Subject: [PATCH 11/12] chore: update version to 1.3.4 --- CHANGELOG.md | 6 ++++++ agent_docstrings/__init__.py | 2 +- pyproject.toml | 4 ++-- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f17e5f..5ab8d46 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [NextRelease] +### Header + +- **subtitle**: describtion + +## [1.3.4] + ### Fixed - **Deterministic Processing**: Fixed a critical bug that caused line numbers in the table of contents to change on every run. This was due to inconsistent newline handling after removing an existing agent docstring. The process is now fully idempotent. diff --git a/agent_docstrings/__init__.py b/agent_docstrings/__init__.py index f48ede6..5f5575c 100644 --- a/agent_docstrings/__init__.py +++ b/agent_docstrings/__init__.py @@ -7,4 +7,4 @@ Attributes: __version__ (str): Current version of the *agent-docstrings* package. """ -__version__ = "1.3.3" \ No newline at end of file +__version__ = "1.3.4" \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 04d4b66..b2808cd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "agent-docstrings" -version = "1.3.3" +version = "1.3.4" 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." readme = { file = "README.md", content-type = "text/markdown" } license = { file = "LICENSE" } @@ -148,7 +148,7 @@ exclude_lines = [ ] [tool.bumpversion] -current_version = "1.3.3" +current_version = "1.3.4" commit = false tag = false From 5a11ed87552e1ba5caa17e3b860f7da72b9b21e4 Mon Sep 17 00:00:00 2001 From: Artemonim Date: Tue, 8 Jul 2025 01:52:43 +0300 Subject: [PATCH 12/12] chore: update release automation workflow to trigger on any `release/*` merged pull requests --- .github/workflows/release-automation.yml | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/.github/workflows/release-automation.yml b/.github/workflows/release-automation.yml index 42b2d3e..186f0e5 100644 --- a/.github/workflows/release-automation.yml +++ b/.github/workflows/release-automation.yml @@ -1,14 +1,16 @@ name: Release Automation on: - push: - branches: - - master + pull_request: + types: [closed] jobs: create_release: - # We only run it for merge commits from branches release/* - if: startsWith(github.event.head_commit.message, 'Merge pull request') && contains(github.event.head_commit.message, 'from release/') + # We only run it for merged PRs from a release/* branch into master. + if: | + github.event.pull_request.merged == true && + github.event.pull_request.base.ref == 'master' && + startsWith(github.event.pull_request.head.ref, 'release/') runs-on: ubuntu-latest permissions: contents: write # to create tags and releases @@ -17,7 +19,8 @@ jobs: - name: Checkout code uses: actions/checkout@v4 with: - # I need a complete history to read the tags and create a PR + # We check out the master branch, which is the state after the merge. + ref: 'master' fetch-depth: 0 - name: Get Version