docgenpranay 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,8 @@
1
+ Metadata-Version: 2.4
2
+ Name: docgenpranay
3
+ Version: 0.1.0
4
+ Summary: Automated Python Docstring Generator
5
+ Author: Muddam Pranay
6
+ Requires-Python: >=3.8
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: pydocstyle
@@ -0,0 +1,8 @@
1
+ Metadata-Version: 2.4
2
+ Name: docgenpranay
3
+ Version: 0.1.0
4
+ Summary: Automated Python Docstring Generator
5
+ Author: Muddam Pranay
6
+ Requires-Python: >=3.8
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: pydocstyle
@@ -0,0 +1,11 @@
1
+ pyproject.toml
2
+ docgenpranay.egg-info/PKG-INFO
3
+ docgenpranay.egg-info/SOURCES.txt
4
+ docgenpranay.egg-info/dependency_links.txt
5
+ docgenpranay.egg-info/entry_points.txt
6
+ docgenpranay.egg-info/requires.txt
7
+ docgenpranay.egg-info/top_level.txt
8
+ docugenius/__init__.py
9
+ docugenius/cli.py
10
+ docugenius/core.py
11
+ tests/test_edge_cases.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ docgenpranay = docugenius.cli:main
@@ -0,0 +1 @@
1
+ pydocstyle
@@ -0,0 +1,3 @@
1
+ dist
2
+ docugenius
3
+ tests
File without changes
@@ -0,0 +1,117 @@
1
+ import argparse
2
+ import sys
3
+ import ast
4
+ from pathlib import Path
5
+
6
+ from docugenius.core import (
7
+ extract_definitions,
8
+ get_function_metadata,
9
+ generate_coverage_report,
10
+ instrument_tree,
11
+ generate_instrumented_code,
12
+ validate_file,
13
+ )
14
+
15
+
16
+ def process_file(file_path: Path, style: str):
17
+ """
18
+ Process a single Python file.
19
+ Returns (instrumented_code, coverage_percent, compliance_percent)
20
+ """
21
+ source = file_path.read_text(encoding="utf-8")
22
+
23
+ tree = ast.parse(source)
24
+ functions, _ = extract_definitions(tree)
25
+
26
+ metadata = [get_function_metadata(func) for func in functions]
27
+
28
+ coverage = generate_coverage_report(metadata)
29
+ violations = validate_file(source)
30
+
31
+ total = coverage["total_functions"]
32
+ compliance = (
33
+ round(((total - len(violations)) / total) * 100, 2)
34
+ if total > 0 else 100.0
35
+ )
36
+
37
+ # Instrument
38
+ instrumented_tree = instrument_tree(tree, style=style)
39
+ instrumented_code = generate_instrumented_code(instrumented_tree)
40
+
41
+ return instrumented_code, coverage["coverage_percent"], compliance
42
+
43
+
44
+ def main():
45
+ parser = argparse.ArgumentParser(
46
+ description="Automated Python Docstring Generator"
47
+ )
48
+
49
+ parser.add_argument(
50
+ "files",
51
+ nargs="+",
52
+ help="Path(s) to Python file(s)"
53
+ )
54
+
55
+ parser.add_argument(
56
+ "--style",
57
+ choices=["google", "numpy", "reST"],
58
+ default="google",
59
+ help="Docstring style"
60
+ )
61
+
62
+ parser.add_argument(
63
+ "--min-coverage",
64
+ type=float,
65
+ default=0,
66
+ help="Minimum required documentation coverage percentage"
67
+ )
68
+
69
+ parser.add_argument(
70
+ "--min-compliance",
71
+ type=float,
72
+ default=0,
73
+ help="Minimum required PEP-257 compliance percentage"
74
+ )
75
+
76
+ args = parser.parse_args()
77
+
78
+ overall_success = True
79
+
80
+ for file_str in args.files:
81
+ file_path = Path(file_str)
82
+
83
+ if not file_path.exists():
84
+ print(f"❌ File not found: {file_str}")
85
+ overall_success = False
86
+ continue
87
+
88
+ try:
89
+ instrumented_code, coverage, compliance = process_file(
90
+ file_path, args.style
91
+ )
92
+
93
+ print(f"\n📄 File: {file_path}")
94
+ print(f"Coverage: {coverage}%")
95
+ print(f"Compliance: {compliance}%")
96
+
97
+ if coverage < args.min_coverage:
98
+ print(f"❌ Coverage below minimum ({args.min_coverage}%)")
99
+ overall_success = False
100
+
101
+ if compliance < args.min_compliance:
102
+ print(f"❌ Compliance below minimum ({args.min_compliance}%)")
103
+ overall_success = False
104
+
105
+ # Print generated code
106
+ print("\n--- Generated Code ---\n")
107
+ print(instrumented_code)
108
+
109
+ except Exception as e:
110
+ print(f"❌ Error processing {file_str}: {e}")
111
+ overall_success = False
112
+
113
+ sys.exit(0 if overall_success else 1)
114
+
115
+
116
+ if __name__ == "__main__":
117
+ main()
@@ -0,0 +1,215 @@
1
+ import ast
2
+ import re
3
+ import tempfile
4
+ import os
5
+ from typing import Dict, List
6
+ import pydocstyle
7
+
8
+
9
+ # =====================================================
10
+ # PARSER
11
+ # =====================================================
12
+
13
+ def parse_file(file_path):
14
+ with open(file_path, "r") as f:
15
+ source_code = f.read()
16
+ return ast.parse(source_code)
17
+
18
+
19
+ # =====================================================
20
+ # EXTRACTION
21
+ # =====================================================
22
+
23
+ def extract_definitions(tree: ast.AST):
24
+ functions = []
25
+ classes = []
26
+
27
+ for node in ast.walk(tree):
28
+ if isinstance(node, ast.FunctionDef):
29
+ functions.append(node)
30
+ elif isinstance(node, ast.ClassDef):
31
+ classes.append(node)
32
+
33
+ return functions, classes
34
+
35
+
36
+ def get_function_metadata(func: ast.FunctionDef) -> Dict:
37
+ params = [arg.arg for arg in func.args.args]
38
+ docstring = ast.get_docstring(func)
39
+ has_return = any(isinstance(n, ast.Return) for n in ast.walk(func))
40
+
41
+ raises = set()
42
+ for n in ast.walk(func):
43
+ if isinstance(n, ast.Raise) and n.exc:
44
+ try:
45
+ exc = ast.unparse(n.exc)
46
+ exc = re.sub(r"\(.*\)", "", exc)
47
+ raises.add(exc)
48
+ except Exception:
49
+ pass
50
+
51
+ is_generator = any(
52
+ isinstance(n, (ast.Yield, ast.YieldFrom))
53
+ for n in ast.walk(func)
54
+ )
55
+
56
+ return {
57
+ "name": func.name,
58
+ "params": params,
59
+ "has_docstring": docstring is not None,
60
+ "docstring": docstring,
61
+ "has_return": has_return,
62
+ "raises": list(raises),
63
+ "is_generator": is_generator,
64
+ }
65
+
66
+
67
+ # =====================================================
68
+ # COVERAGE
69
+ # =====================================================
70
+
71
+ def generate_coverage_report(metadata):
72
+ total_functions = len(metadata)
73
+
74
+ documented = [m for m in metadata if m.get("has_docstring")]
75
+ undocumented = [m for m in metadata if not m.get("has_docstring")]
76
+
77
+ coverage_percent = (
78
+ round((len(documented) / total_functions) * 100, 2)
79
+ if total_functions > 0 else 0
80
+ )
81
+
82
+ return {
83
+ "total_functions": total_functions,
84
+ "documented_functions": len(documented),
85
+ "undocumented_functions": len(undocumented),
86
+ "coverage_percent": coverage_percent
87
+ }
88
+
89
+
90
+ # =====================================================
91
+ # DOCSTRING GENERATION
92
+ # =====================================================
93
+
94
+ def generate_docstring(meta: dict, style: str = "google") -> str:
95
+ """
96
+ Generate clean PEP-257 compliant docstring.
97
+ """
98
+
99
+ name = meta["name"].replace("_", " ").capitalize()
100
+ params = meta["params"]
101
+ raises = meta["raises"]
102
+ is_generator = meta["is_generator"]
103
+ has_return = meta["has_return"]
104
+
105
+ lines = [f"{name}.", ""]
106
+
107
+ # ---------------- PARAMETERS ----------------
108
+ if params:
109
+ lines.append("Args:")
110
+ for p in params:
111
+ lines.append(f" {p}: Description.")
112
+ lines.append("")
113
+
114
+ # ---------------- RETURNS / YIELDS ----------------
115
+ if is_generator:
116
+ lines.append("Yields:")
117
+ lines.append(" value: Description.")
118
+ lines.append("")
119
+ elif has_return:
120
+ lines.append("Returns:")
121
+ lines.append(" value: Description.")
122
+ lines.append("")
123
+
124
+ # ---------------- RAISES ----------------
125
+ if raises:
126
+ lines.append("Raises:")
127
+ for r in raises:
128
+ lines.append(f" {r}: Description.")
129
+ lines.append("")
130
+
131
+ # Remove trailing blank lines
132
+ while lines and lines[-1] == "":
133
+ lines.pop()
134
+
135
+ return "\n".join(lines)
136
+
137
+
138
+ # =====================================================
139
+ # INSTRUMENTATION
140
+ # =====================================================
141
+
142
+ def instrument_ast(tree: ast.AST, style: str = "google") -> ast.AST:
143
+ """
144
+ Insert generated docstrings into module, classes, and functions.
145
+ """
146
+
147
+ # ---------------- MODULE DOCSTRING ----------------
148
+ if not ast.get_docstring(tree):
149
+ module_doc = ast.Expr(
150
+ value=ast.Constant("Module description.")
151
+ )
152
+ tree.body.insert(0, module_doc)
153
+
154
+ for node in ast.walk(tree):
155
+
156
+ # ---------------- CLASS DOCSTRING ----------------
157
+ if isinstance(node, ast.ClassDef):
158
+ if not ast.get_docstring(node):
159
+ class_doc = ast.Expr(
160
+ value=ast.Constant(f"{node.name} class.")
161
+ )
162
+ node.body.insert(0, class_doc)
163
+
164
+ # ---------------- FUNCTION DOCSTRING ----------------
165
+ if isinstance(node, ast.FunctionDef):
166
+ if not ast.get_docstring(node):
167
+ meta = get_function_metadata(node)
168
+ docstring = generate_docstring(meta, style)
169
+
170
+ node.body.insert(
171
+ 0,
172
+ ast.Expr(value=ast.Constant(docstring))
173
+ )
174
+
175
+ return tree
176
+
177
+
178
+ def instrument_tree(tree, metadata=None, style="google"):
179
+ return instrument_ast(tree, style)
180
+
181
+
182
+ def generate_instrumented_code(tree):
183
+ return ast.unparse(tree)
184
+
185
+
186
+ # =====================================================
187
+ # VALIDATOR
188
+ # =====================================================
189
+
190
+ IGNORED_RULES = [
191
+ "D100", "D101", "D102",
192
+ "D205", "D400", "D401", "D212", "D213",
193
+ "D406", "D407", "D413",
194
+ ]
195
+
196
+
197
+ def validate_file(source_code: str) -> List[Dict]:
198
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as tmp:
199
+ tmp.write(source_code)
200
+ tmp_path = tmp.name
201
+
202
+ violations = []
203
+
204
+ try:
205
+ for error in pydocstyle.check([tmp_path], ignore=IGNORED_RULES):
206
+ violations.append({
207
+ "line": error.line,
208
+ "code": error.code,
209
+ "message": error.message,
210
+ "source": error.source,
211
+ })
212
+ finally:
213
+ os.unlink(tmp_path)
214
+
215
+ return violations
@@ -0,0 +1,22 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "docgenpranay"
7
+ version = "0.1.0"
8
+ description = "Automated Python Docstring Generator"
9
+ readme = "README.md"
10
+ requires-python = ">=3.8"
11
+ authors = [
12
+ { name = "Muddam Pranay" }
13
+ ]
14
+ dependencies = [
15
+ "pydocstyle"
16
+ ]
17
+
18
+ [project.scripts]
19
+ docgenpranay = "docugenius.cli:main"
20
+
21
+ [tool.setuptools.packages.find]
22
+ where = ["."]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,123 @@
1
+ import ast
2
+ import pytest
3
+
4
+ from docugenius.core import (
5
+ extract_definitions,
6
+ get_function_metadata,
7
+ instrument_tree,
8
+ generate_instrumented_code,
9
+ validate_file,
10
+ )
11
+
12
+
13
+ # ---------------------------------------------------
14
+ # HELPER
15
+ # ---------------------------------------------------
16
+
17
+ def _parse_and_instrument(source: str):
18
+ tree = ast.parse(source)
19
+ instrumented_tree = instrument_tree(tree, style="google")
20
+ return generate_instrumented_code(instrumented_tree)
21
+
22
+
23
+ # ---------------------------------------------------
24
+ # TESTS
25
+ # ---------------------------------------------------
26
+
27
+ def test_empty_python_file():
28
+ """Empty file should be handled safely."""
29
+ source = ""
30
+ tree = ast.parse(source)
31
+ instrumented_tree = instrument_tree(tree, style="google")
32
+ code = generate_instrumented_code(instrumented_tree)
33
+
34
+ # Should at least be valid Python
35
+ ast.parse(code)
36
+
37
+
38
+ def test_module_docstring_added_if_missing():
39
+ """Missing module docstring should be inserted."""
40
+ source = "x = 10"
41
+ instrumented = _parse_and_instrument(source)
42
+
43
+ assert 'Module description.' in instrumented
44
+
45
+
46
+ def test_nested_function_only_outer_documented():
47
+ """Nested functions should not necessarily be documented separately."""
48
+ source = """
49
+ def outer():
50
+ def inner():
51
+ return 5
52
+ return inner()
53
+ """
54
+ instrumented = _parse_and_instrument(source)
55
+ tree = ast.parse(instrumented)
56
+
57
+ functions, _ = extract_definitions(tree)
58
+
59
+ outer = next(f for f in functions if f.name == "outer")
60
+ inner = next(f for f in functions if f.name == "inner")
61
+
62
+ assert ast.get_docstring(outer) is not None
63
+ # Depending on your logic, inner may or may not be documented.
64
+ # If your instrumentor documents all, change this to assert is not None.
65
+ assert ast.get_docstring(inner) is not None
66
+
67
+
68
+ def test_class_without_methods_gets_docstring():
69
+ """Classes without methods should receive docstrings."""
70
+ source = """
71
+ class Empty:
72
+ pass
73
+ """
74
+ instrumented = _parse_and_instrument(source)
75
+ tree = ast.parse(instrumented)
76
+ _, classes = extract_definitions(tree)
77
+
78
+ empty_class = next(c for c in classes if c.name == "Empty")
79
+ assert ast.get_docstring(empty_class) is not None
80
+
81
+
82
+ def test_existing_docstring_not_overwritten():
83
+ """Existing function docstrings should not be replaced."""
84
+ source = '''
85
+ def multiply(x, y):
86
+ """Existing docstring."""
87
+ return x * y
88
+ '''
89
+ instrumented = _parse_and_instrument(source)
90
+ tree = ast.parse(instrumented)
91
+ functions, _ = extract_definitions(tree)
92
+
93
+ multiply = next(f for f in functions if f.name == "multiply")
94
+ doc = ast.get_docstring(multiply)
95
+
96
+ assert "Existing docstring." in doc
97
+
98
+
99
+ def test_syntax_error_raises():
100
+ """Invalid Python input should raise SyntaxError."""
101
+ bad_source = "def broken(:"
102
+ with pytest.raises(SyntaxError):
103
+ ast.parse(bad_source)
104
+
105
+
106
+ def test_generated_code_removes_missing_docstring_violation():
107
+ """Instrumentation should remove missing-docstring violations."""
108
+ source = """
109
+ def add(x, y):
110
+ return x + y
111
+ """
112
+
113
+ before_violations = validate_file(source)
114
+ instrumented = _parse_and_instrument(source)
115
+ after_violations = validate_file(instrumented)
116
+
117
+ before_codes = {v["code"] for v in before_violations}
118
+ after_codes = {v["code"] for v in after_violations}
119
+
120
+ # Ensure D103 (missing function docstring) is removed
121
+ assert "D103" in before_codes
122
+ assert "D103" not in after_codes
123
+