autodocstring-tool 0.1.0__py3-none-any.whl

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 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,4 @@
1
+ from autodocstring.cli import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
autodocstring/cli.py ADDED
@@ -0,0 +1,91 @@
1
+ import sys
2
+ import argparse
3
+ from pathlib import Path
4
+
5
+ from autodocstring.parser import parse_file
6
+ from autodocstring.coverage import coverage_report
7
+
8
+
9
+ def main():
10
+ parser = argparse.ArgumentParser(
11
+ description="Docstring coverage checker and reporter",
12
+ epilog="Example: python cli.py samples/ --style Google --min-coverage 80 --verbose",
13
+ )
14
+ parser.add_argument(
15
+ "paths",
16
+ nargs="*",
17
+ default=["samples"],
18
+ help="Path(s) to check (folder or files). Default: samples",
19
+ )
20
+ parser.add_argument(
21
+ "--min-coverage",
22
+ type=float,
23
+ default=80.0,
24
+ help="Minimum coverage per file (default: 80.0)",
25
+ )
26
+ parser.add_argument(
27
+ "--style",
28
+ default="Google",
29
+ choices=["Google", "NumPy", "reST"],
30
+ help="Docstring style (default: Google)",
31
+ )
32
+ parser.add_argument(
33
+ "--verbose",
34
+ action="store_true",
35
+ help="Print detailed output for each file",
36
+ )
37
+
38
+ args = parser.parse_args()
39
+
40
+ files = []
41
+ for p in args.paths:
42
+ path = Path(p)
43
+ if path.is_dir():
44
+ files.extend(path.rglob("*.py"))
45
+ elif path.is_file() and path.suffix.lower() == ".py":
46
+ files.append(path)
47
+ else:
48
+ if args.verbose:
49
+ print(f"Skipping non-Python path: {path}")
50
+
51
+ if not files:
52
+ print("No Python files found.")
53
+ sys.exit(1)
54
+
55
+ failed = []
56
+ print(f"Checking {len(files)} file(s)")
57
+
58
+ for file_path in files:
59
+ try:
60
+ parsed = parse_file(str(file_path))
61
+ report = coverage_report(parsed)
62
+
63
+ # Safe access to keys with defaults
64
+ coverage_pct = report.get("Coverage (%)", 0.0)
65
+ missing_count = report.get("Missing", 0)
66
+
67
+ if coverage_pct < args.min_coverage:
68
+ failed.append((file_path, coverage_pct, missing_count))
69
+ print(
70
+ f" {file_path}: {coverage_pct:.2f}% - missing {missing_count} items"
71
+ )
72
+ else:
73
+ if args.verbose:
74
+ print(f" {file_path}: {coverage_pct:.2f}% OK")
75
+
76
+ except Exception as e:
77
+ print(f"Error processing {file_path}: {type(e).__name__}: {str(e)}")
78
+ failed.append((file_path, 0.0, "error"))
79
+
80
+ if failed:
81
+ print(
82
+ f"\nFailed: {len(failed)} file(s) below {args.min_coverage}% or had errors"
83
+ )
84
+ sys.exit(1)
85
+
86
+ print("All checked files passed coverage check.")
87
+ sys.exit(0)
88
+
89
+
90
+ if __name__ == "__main__":
91
+ main()
@@ -0,0 +1,53 @@
1
+ def analyze_docstring(func, style=None):
2
+ raw_doc = func.get("docstring") or ""
3
+ doc = raw_doc.lower()
4
+ missing = []
5
+ formatting_issues = []
6
+ warnings = []
7
+ # ---------- SECTION COMPLETENESS ----------
8
+ if func["params"] and not any(k in doc for k in ["param", "args", "parameters"]):
9
+ missing.append("Parameters")
10
+
11
+ if func["returns"] and "return" not in doc:
12
+ missing.append("Returns")
13
+
14
+ if func["raises"] and "raise" not in doc:
15
+ missing.append("Raises")
16
+
17
+ if func["is_generator"] and "yield" not in doc:
18
+ missing.append("Yields")
19
+
20
+ if func.get("class") and func.get("attributes") and "attribute" not in doc:
21
+ missing.append("Attributes")
22
+ # ---------- SUMMARY LINE CHECK ----------
23
+ if raw_doc:
24
+ first_line = raw_doc.strip().split("\n")[0].strip()
25
+
26
+ if not first_line:
27
+ formatting_issues.append("Empty summary line")
28
+ elif not first_line.endswith("."):
29
+ warnings.append("Summary line should end with a period")
30
+ elif len(first_line) > 72:
31
+ warnings.append("Summary line is too long")
32
+ elif first_line.lower().startswith(
33
+ ("returns", "does", "divides", "calculates")
34
+ ):
35
+ warnings.append("Summary line not in imperative mood")
36
+ else:
37
+ formatting_issues.append("Missing summary line")
38
+ # ---------- STYLE-SPECIFIC CHECK ----------
39
+ if style and raw_doc:
40
+ if style == "Google" and "args:" not in doc:
41
+ warnings.append("Google style expects 'Args:' section")
42
+
43
+ if style == "NumPy" and "parameters" not in doc:
44
+ warnings.append("NumPy style expects 'Parameters' section")
45
+
46
+ if style == "reST" and ":param" not in doc:
47
+ warnings.append("reST style expects ':param:' fields")
48
+ return {
49
+ "missing_sections": missing,
50
+ "formatting_issues": formatting_issues,
51
+ "warnings": warnings,
52
+ "pep257_compliant": len(missing) == 0 and len(formatting_issues) == 0,
53
+ }
@@ -0,0 +1,20 @@
1
+ import tomllib
2
+ from pathlib import Path
3
+
4
+
5
+ def load_config():
6
+ path = Path("pyproject.toml")
7
+
8
+ defaults = {
9
+ "style": "Google",
10
+ "min_coverage": 80,
11
+ "enforce_pep257": True,
12
+ }
13
+
14
+ if not path.exists():
15
+ return defaults
16
+
17
+ with open(path, "rb") as f:
18
+ data = tomllib.load(f)
19
+
20
+ return {**defaults, **data.get("tool", {}).get("autodoc", {})}
@@ -0,0 +1,70 @@
1
+ from autodocstring.compliance import analyze_docstring
2
+
3
+
4
+ def coverage_report(parsed_data):
5
+ functions = parsed_data["functions"]
6
+ classes = parsed_data["classes"]
7
+ methods = [m for c in classes for m in c["methods"]]
8
+ all_items = functions + methods
9
+ missing = {"Parameters": 0, "Returns": 0, "Raises": 0, "Yields": 0, "Attributes": 0}
10
+ compliant = 0
11
+ # ✅ NEW: collect non-compliant details
12
+ non_compliant_items = []
13
+
14
+ for item in all_items:
15
+ result = analyze_docstring(item)
16
+
17
+ if result["pep257_compliant"]:
18
+ compliant += 1
19
+ else:
20
+ non_compliant_items.append(
21
+ {
22
+ "Name": (
23
+ item["name"]
24
+ if not item.get("class")
25
+ else f"{item['class']}.{item['name']}"
26
+ ),
27
+ "Type": "Method" if item.get("class") else "Function",
28
+ "Missing Sections": ", ".join(result["missing_sections"]) or "None",
29
+ "Formatting Issues": ", ".join(result.get("formatting_issues", []))
30
+ or "None",
31
+ }
32
+ )
33
+
34
+ for sec in result["missing_sections"]:
35
+ missing[sec] += 1
36
+
37
+ total = len(all_items)
38
+ documented = sum(i["has_docstring"] for i in all_items)
39
+ # ---------- TYPE-WISE COUNTS ----------
40
+ documented_functions = sum(f["has_docstring"] for f in functions)
41
+ documented_methods = sum(m["has_docstring"] for m in methods)
42
+ documented_classes = sum(c["has_docstring"] for c in classes)
43
+ return {
44
+ # ---------- COUNTS ----------
45
+ "Functions": len(functions),
46
+ "Classes": len(classes),
47
+ "Methods": len(methods),
48
+ "Total": total,
49
+ # ---------- OVERALL COVERAGE ----------
50
+ "Documented": documented,
51
+ "Missing": total - documented,
52
+ "Coverage (%)": (documented / total * 100) if total else 0,
53
+ # ---------- TYPE-WISE COVERAGE ----------
54
+ "Function Coverage (%)": (
55
+ documented_functions / len(functions) * 100 if functions else 0
56
+ ),
57
+ "Method Coverage (%)": (
58
+ documented_methods / len(methods) * 100 if methods else 0
59
+ ),
60
+ "Class Coverage (%)": (
61
+ documented_classes / len(classes) * 100 if classes else 0
62
+ ),
63
+ # ---------- COMPLIANCE ----------
64
+ "PEP-257 Compliant": compliant,
65
+ "PEP-257 Compliance (%)": (compliant / total * 100) if total else 0,
66
+ # ---------- MISSING SECTIONS ----------
67
+ **{f"Missing {k}": v for k, v in missing.items()},
68
+ # ---------- ✅ NEW ADDITION ----------
69
+ "Non-Compliant Items": non_compliant_items,
70
+ }
@@ -0,0 +1,78 @@
1
+ def _summary(item, class_name=None):
2
+ if "name" in item:
3
+ name = f"{class_name}.{item['name']}" if class_name else item["name"]
4
+ else:
5
+ name = "Object"
6
+
7
+ return f"Generate documentation for {name}."
8
+
9
+
10
+ def generate_google(func, class_name=None):
11
+ doc = f"{_summary(func, class_name)}\n\n"
12
+
13
+ if func.get("params"):
14
+ doc += "Args:\n"
15
+ for p in func["params"]:
16
+ doc += f" {p['name']} ({p['type'] or 'Any'}): Description.\n"
17
+
18
+ if func.get("returns"):
19
+ doc += f"\nReturns:\n {func['returns']}: Description.\n"
20
+
21
+ if func.get("raises"):
22
+ doc += "\nRaises:\n"
23
+ for r in func["raises"]:
24
+ doc += f" {r}: Description.\n"
25
+
26
+ if func.get("is_generator"):
27
+ doc += "\nYields:\n value: Generated value.\n"
28
+
29
+ return doc.strip()
30
+
31
+
32
+ def generate_numpy(func, class_name=None):
33
+ doc = f"{_summary(func, class_name)}\n\n"
34
+
35
+ if func.get("params"):
36
+ doc += "Parameters\n----------\n"
37
+ for p in func["params"]:
38
+ doc += f"{p['name']} : {p['type'] or 'Any'}\n Description.\n"
39
+
40
+ if func.get("returns"):
41
+ doc += f"\nReturns\n-------\n{func['returns']}\n Description.\n"
42
+
43
+ if func.get("raises"):
44
+ doc += "\nRaises\n------\n"
45
+ for r in func["raises"]:
46
+ doc += f"{r}\n Description.\n"
47
+
48
+ if func.get("is_generator"):
49
+ doc += "\nYields\n------\nvalue\n Generated value.\n"
50
+
51
+ return doc.strip()
52
+
53
+
54
+ def generate_rest(func, class_name=None):
55
+ doc = f"{_summary(func, class_name)}\n\n"
56
+
57
+ for p in func.get("params", []):
58
+ doc += f":param {p['name']}: Description.\n"
59
+ doc += f":type {p['name']}: {p['type'] or 'Any'}\n"
60
+
61
+ if func.get("returns"):
62
+ doc += f":return: {func['returns']}.\n"
63
+
64
+ for r in func.get("raises", []):
65
+ doc += f":raises {r}: Description.\n"
66
+
67
+ if func.get("is_generator"):
68
+ doc += ":yields: Generated value.\n"
69
+
70
+ return doc.strip()
71
+
72
+
73
+ def generate_docstring(func, class_name=None, style="Google"):
74
+ if style == "NumPy":
75
+ return generate_numpy(func, class_name)
76
+ if style == "reST":
77
+ return generate_rest(func, class_name)
78
+ return generate_google(func, class_name)
@@ -0,0 +1,64 @@
1
+ import ast
2
+ from autodocstring.llm_generator import generate_docstring_llm
3
+
4
+
5
+ class DocstringInjector(ast.NodeTransformer):
6
+ """Injects clean PEP 257 docstrings into functions and methods."""
7
+
8
+ def __init__(self, style: str = "Google"):
9
+ self.style = style
10
+
11
+ def visit_FunctionDef(self, node):
12
+ self.generic_visit(node)
13
+
14
+ try:
15
+ source = ast.unparse(node)
16
+ raw_body = generate_docstring_llm(source, self.style)
17
+
18
+ # Strip any triple quotes the LLM might have added
19
+ body = raw_body.strip()
20
+ for q in ['"""', "'''"]:
21
+ if body.startswith(q) and body.endswith(q):
22
+ body = body[len(q) : -len(q)].strip()
23
+
24
+ # Remove any leading/trailing junk
25
+ body = body.strip()
26
+
27
+ # Build clean docstring (we add the quotes here only once)
28
+ docstring = f"\n{ body}\n "
29
+
30
+ doc_node = ast.Expr(value=ast.Constant(value=docstring))
31
+
32
+ # Replace existing docstring or insert at the beginning
33
+ if node.body and isinstance(node.body[0], ast.Expr):
34
+ if isinstance(node.body[0].value, ast.Constant) and isinstance(
35
+ node.body[0].value.value, str
36
+ ):
37
+ # Replace old docstring
38
+ node.body[0] = doc_node
39
+ else:
40
+ # Insert new one
41
+ node.body.insert(0, doc_node)
42
+ else:
43
+ node.body.insert(0, doc_node)
44
+
45
+ except Exception as e:
46
+ print(f"Docstring generation failed for {node.name}: {e}")
47
+
48
+ return node
49
+
50
+
51
+ def inject_docstrings(source_code: str, style: str = "Google") -> str:
52
+ """
53
+ Parse source code and inject docstrings into all functions/methods.
54
+ """
55
+ try:
56
+ tree = ast.parse(source_code)
57
+ except SyntaxError as e:
58
+ raise SyntaxError(f"Invalid Python code: {str(e)}")
59
+
60
+ transformer = DocstringInjector(style=style)
61
+ new_tree = transformer.visit(tree)
62
+
63
+ ast.fix_missing_locations(new_tree)
64
+ return ast.unparse(new_tree)
@@ -0,0 +1,173 @@
1
+ import requests
2
+ import re
3
+ import textwrap
4
+
5
+ OLLAMA_URL = "http://localhost:11434/api/generate"
6
+ MODEL = "qwen2.5-coder:3b"
7
+
8
+
9
+ def build_prompt(code: str, style: str) -> str:
10
+ """
11
+ Build a strong, clear prompt optimized for qwen2.5-coder:3b
12
+ """
13
+ style = style.lower()
14
+ if style in ["rst", "restructuredtext"]:
15
+ style = "reST"
16
+
17
+ return f"""You are an expert Python docstring generator.
18
+
19
+ Generate EXACTLY ONE PEP 257 compliant docstring for the given function/method.
20
+
21
+ MANDATORY RULES - YOU MUST FOLLOW ALL:
22
+ - Output ONLY the docstring content (text that goes between triple quotes)
23
+ - Start with one short imperative summary sentence. End with a period.
24
+ - Then exactly one blank line.
25
+ - Then only needed sections: Args/Parameters, Returns, Raises, Yields
26
+ - Follow the selected style strictly: {style}
27
+ - Use exactly 4 spaces for indentation under sections
28
+ - Do NOT include ```python
29
+ - Do NOT repeat the function definition
30
+ - Do NOT add extra triple quotes around your output
31
+ - Do NOT add examples, notes, explanations, or any extra text
32
+ - Do NOT include the words "python" or "code" unless in type hints
33
+ - Return ONLY the pure docstring text — nothing before or after
34
+
35
+ Style reference:
36
+ - google: Args:, Returns:, Raises:
37
+ - numpy: Parameters\n----------, Returns\n-------, Raises\n------
38
+ - reST: :param name:, :returns:, :raises:
39
+
40
+ STRICT OUTPUT RULES - YOU MUST OBEY:
41
+ - Output ONLY the docstring content (the text INSIDE the triple quotes)
42
+ - Start directly with the summary sentence
43
+ - Then one blank line
44
+ - Then only needed sections
45
+ - Do NOT output triple quotes at all
46
+ - Do NOT repeat the function definition
47
+ - Do NOT include ```python or any fences
48
+ - Do NOT add extra text, examples, explanations or markup
49
+ - Return pure text only
50
+
51
+
52
+
53
+ Code:
54
+ {code}
55
+
56
+ Style: {style}
57
+
58
+ Respond with ONLY the docstring content. No other text.
59
+ """
60
+
61
+
62
+ def clean_docstring(raw: str) -> str:
63
+ """
64
+ Clean LLM output and ensure it becomes a valid docstring.
65
+ """
66
+ raw = raw.strip()
67
+
68
+ # Remove common LLM mistakes
69
+ raw = re.sub(r"^```(?:python|py)?\s*", "", raw, flags=re.IGNORECASE)
70
+ raw = re.sub(r"\s*```$", "", raw)
71
+ raw = re.sub(r'^("""|\'\'\')+', "", raw)
72
+ raw = re.sub(r'("""|\'\'\')+$', "", raw)
73
+
74
+ # Split into lines and clean
75
+ lines = [line.rstrip() for line in raw.splitlines()]
76
+
77
+ # Remove leading/trailing empty lines
78
+ while lines and not lines[0].strip():
79
+ lines.pop(0)
80
+ while lines and not lines[-1].strip():
81
+ lines.pop()
82
+
83
+ if not lines:
84
+ return '"""\nNo description available.\n"""'
85
+
86
+ # Force PEP 257 structure: summary + blank line
87
+ summary = lines[0].strip()
88
+ body = lines[1:]
89
+
90
+ result = [summary, ""] # summary + blank line
91
+
92
+ for line in body:
93
+ stripped = line.rstrip()
94
+ if stripped:
95
+ # Keep section headers clean, indent descriptions
96
+ if re.match(r"^(Args|Parameters|Returns|Raises|Yields):", stripped.strip()):
97
+ result.append(stripped)
98
+ else:
99
+ result.append(" " + stripped.lstrip())
100
+ else:
101
+ result.append(" ")
102
+
103
+ # Add triple quotes
104
+ result = ['"""'] + result + ['"""']
105
+
106
+ return "\n".join(result)
107
+
108
+
109
+ def generate_docstring_llm(code: str, style: str = "Google") -> str:
110
+ """
111
+ Generate a PEP 257 compliant docstring using Ollama + qwen2.5-coder:3b
112
+
113
+ Args:
114
+ code: Function/method code as string
115
+ style: "Google", "NumPy", "reST"
116
+
117
+ Returns:
118
+ Properly formatted docstring string (with triple quotes)
119
+ """
120
+ prompt = build_prompt(code, style)
121
+
122
+ payload = {
123
+ "model": MODEL,
124
+ "prompt": prompt,
125
+ "stream": False,
126
+ "options": {
127
+ "temperature": 0.0,
128
+ "top_p": 0.9,
129
+ "repeat_penalty": 1.1,
130
+ "num_predict": 512, # enough for most docstrings
131
+ },
132
+ }
133
+
134
+ try:
135
+ response = requests.post(OLLAMA_URL, json=payload, timeout=120)
136
+ response.raise_for_status()
137
+
138
+ raw = response.json().get("response", "").strip()
139
+
140
+ if not raw:
141
+ return '"""\nGenerated docstring.\n"""'
142
+
143
+ cleaned = clean_docstring(raw)
144
+
145
+ # Final safety: ensure triple quotes
146
+ if not cleaned.startswith('"""'):
147
+ cleaned = '"""\n' + cleaned.lstrip()
148
+ if not cleaned.endswith('"""'):
149
+ cleaned = cleaned.rstrip() + '\n"""'
150
+
151
+ return cleaned
152
+
153
+ except requests.exceptions.RequestException as e:
154
+ raise RuntimeError(f"Ollama connection failed: {e}") from e
155
+ except Exception as e:
156
+ raise RuntimeError(f"Docstring generation failed: {e}") from e
157
+
158
+
159
+ # ────────────────────────────────────────────────
160
+ # Optional test
161
+ # ────────────────────────────────────────────────
162
+ if __name__ == "__main__":
163
+ sample = """
164
+ def add(a: int, b: int = 0) -> int:
165
+ return a + b
166
+ """
167
+
168
+ try:
169
+ doc = generate_docstring_llm(sample, style="Google")
170
+ print("Generated docstring:\n")
171
+ print(doc)
172
+ except Exception as e:
173
+ print(f"Error: {e}")
@@ -0,0 +1,89 @@
1
+ import ast
2
+
3
+
4
+ class CodeParser(ast.NodeVisitor):
5
+ def __init__(self):
6
+ self.functions = []
7
+ self.classes = []
8
+ self.current_class = None
9
+
10
+ # ---------- TOP-LEVEL FUNCTIONS ----------
11
+ def visit_FunctionDef(self, node):
12
+ if self.current_class is None:
13
+ self.functions.append(self.extract_function(node))
14
+ self.generic_visit(node)
15
+
16
+ # ---------- CLASSES ----------
17
+ def visit_ClassDef(self, node):
18
+ class_docstring = ast.get_docstring(node)
19
+
20
+ class_info = {
21
+ "class_name": node.name,
22
+ "has_docstring": class_docstring is not None,
23
+ "docstring": class_docstring,
24
+ "attributes": [],
25
+ "methods": [],
26
+ }
27
+
28
+ self.current_class = node.name
29
+
30
+ for item in node.body:
31
+ if isinstance(item, ast.Assign):
32
+ for target in item.targets:
33
+ if isinstance(target, ast.Name):
34
+ class_info["attributes"].append(target.id)
35
+
36
+ if isinstance(item, ast.FunctionDef):
37
+ class_info["methods"].append(self.extract_function(item, node.name))
38
+
39
+ self.classes.append(class_info)
40
+ self.current_class = None
41
+
42
+ # ---------- FUNCTION EXTRACTION ----------
43
+ def extract_function(self, node, class_name=None):
44
+ docstring = ast.get_docstring(node)
45
+
46
+ params = [
47
+ {
48
+ "name": arg.arg,
49
+ "type": ast.unparse(arg.annotation) if arg.annotation else None,
50
+ }
51
+ for arg in node.args.args
52
+ ]
53
+
54
+ param_names = [p["name"] for p in params]
55
+ signature = f"{node.name}({', '.join(param_names)})"
56
+
57
+ returns = ast.unparse(node.returns) if node.returns else None
58
+
59
+ raises = []
60
+ is_generator = False
61
+
62
+ for child in ast.walk(node):
63
+ if isinstance(child, ast.Raise):
64
+ raises.append(ast.unparse(child.exc) if child.exc else "Exception")
65
+ if isinstance(child, (ast.Yield, ast.YieldFrom)):
66
+ is_generator = True
67
+
68
+ return {
69
+ "name": node.name,
70
+ "class": class_name,
71
+ "signature": signature, # ⭐ ADDED
72
+ "params": params,
73
+ "returns": returns,
74
+ "has_docstring": docstring is not None,
75
+ "lineno": node.lineno,
76
+ "docstring": docstring,
77
+ "raises": list(set(raises)),
78
+ "is_generator": is_generator,
79
+ }
80
+
81
+
82
+ def parse_file(file_path):
83
+ with open(file_path, "r", encoding="utf-8") as f:
84
+ tree = ast.parse(f.read())
85
+
86
+ parser = CodeParser()
87
+ parser.visit(tree)
88
+
89
+ return {"functions": parser.functions, "classes": parser.classes}
@@ -0,0 +1,60 @@
1
+ import subprocess
2
+ import re
3
+
4
+ CATEGORY_MAP = {
5
+ "D10": "Missing Docstrings",
6
+ "D20": "Whitespace Issues",
7
+ "D30": "Quotes Issues",
8
+ "D40": "Docstring Content Issues",
9
+ }
10
+
11
+
12
+ def categorize(code):
13
+ for k in CATEGORY_MAP:
14
+ if code.startswith(k):
15
+ return CATEGORY_MAP[k]
16
+ return "Other"
17
+
18
+
19
+ def run_full_pep257(file_path):
20
+ result = subprocess.run(["pydocstyle", file_path], capture_output=True, text=True)
21
+
22
+ issues = []
23
+
24
+ for line in result.stdout.splitlines():
25
+
26
+ match = re.match(r"(.*?):(\d+)\s+(D\d+)\s+(.*)", line)
27
+
28
+ if match:
29
+ code = match.group(3)
30
+
31
+ issues.append(
32
+ {
33
+ "File": match.group(1),
34
+ "Line": int(match.group(2)),
35
+ "Code": code,
36
+ "Category": categorize(code),
37
+ "Message": match.group(4),
38
+ "Fix": suggest_fix(code),
39
+ }
40
+ )
41
+
42
+ return issues
43
+
44
+
45
+ # Only SAFE automatic fixes
46
+ def suggest_fix(code):
47
+
48
+ fixes = {
49
+ "D100": "Add module docstring at top of file",
50
+ "D101": "Add class docstring",
51
+ "D102": "Add method docstring",
52
+ "D103": "Add function docstring",
53
+ "D205": "Add blank line after summary",
54
+ "D400": "End summary with period",
55
+ "D401": "Use imperative mood",
56
+ "D403": "Capitalize first word",
57
+ "D404": "Avoid starting with 'This'",
58
+ }
59
+
60
+ return fixes.get(code, "Manual fix required")
@@ -0,0 +1,31 @@
1
+ import subprocess
2
+
3
+
4
+ def run_pydocstyle(file_path):
5
+ """
6
+ Runs pydocstyle and returns RAW violations.
7
+ This version is FAIL-SAFE.
8
+ """
9
+
10
+ try:
11
+ result = subprocess.run(
12
+ ["pydocstyle", file_path],
13
+ stdout=subprocess.PIPE,
14
+ stderr=subprocess.PIPE,
15
+ text=True,
16
+ )
17
+
18
+ output = result.stdout + result.stderr
19
+
20
+ if not output.strip():
21
+ return []
22
+
23
+ issues = []
24
+
25
+ for line in output.splitlines():
26
+ issues.append({"Violation": line})
27
+
28
+ return issues
29
+
30
+ except Exception as e:
31
+ return [{"Violation": str(e)}]
@@ -0,0 +1,29 @@
1
+ import subprocess
2
+ import tempfile
3
+ import os
4
+
5
+
6
+ def pep257_report(file_path):
7
+ """
8
+ Runs pydocstyle on a Python file and returns:
9
+ - Total errors
10
+ - Error messages
11
+ - Compliance %
12
+ """
13
+
14
+ try:
15
+ result = subprocess.run(
16
+ ["pydocstyle", file_path], capture_output=True, text=True
17
+ )
18
+
19
+ errors = result.stdout.strip().split("\n") if result.stdout else []
20
+ errors = [e for e in errors if e.strip()]
21
+
22
+ return {
23
+ "total_errors": len(errors),
24
+ "messages": errors,
25
+ "compliance": 0 if errors else 100,
26
+ }
27
+
28
+ except Exception as e:
29
+ return {"total_errors": 0, "messages": [str(e)], "compliance": 0}
@@ -0,0 +1,10 @@
1
+ Metadata-Version: 2.4
2
+ Name: autodocstring-tool
3
+ Version: 0.1.0
4
+ Summary: Automated Python docstring generator and enforcer
5
+ Requires-Python: >=3.9
6
+ License-File: LICENSE
7
+ Requires-Dist: streamlit
8
+ Requires-Dist: pandas
9
+ Requires-Dist: pydocstyle
10
+ Dynamic: license-file
@@ -0,0 +1,19 @@
1
+ autodocstring/__init__.py,sha256=kUR5RAFc7HCeiqdlX36dZOHkUI5wI6V_43RpEcD8b-0,22
2
+ autodocstring/__main__.py,sha256=xzXQLgKVJ0b_xAWbzwNaeK6kGZaN0r0EIODKY1hAC0M,77
3
+ autodocstring/cli.py,sha256=g1M0_FpILS_vZvnOrnTpfSmeRh0XvQJjvRkFlB5vEds,2676
4
+ autodocstring/compliance.py,sha256=GKevPx2ciwtoi6aBUXUMh-1cKdeDGObX8FdPVoO2csc,2081
5
+ autodocstring/config.py,sha256=XhCGQs3sJo1q-xk9A7O0RRu0GLmhKrwqHNFNYzDTvh8,409
6
+ autodocstring/coverage.py,sha256=BEls7x1HPacmekNWkM5GbssLWPul1tYfC4VQEd6L1dk,2795
7
+ autodocstring/generator.py,sha256=uh1izp0JNuM4Yw_CpP2BJKelt1Fbh_TufyTtAmN-3Gs,2305
8
+ autodocstring/injector.py,sha256=XpTC2pMhqUytjeejVKYdw_c3gcXTGyKP_qJ40ov5IXc,2175
9
+ autodocstring/llm_generator.py,sha256=tUZ7ofYrIp223aCm0RfAtWYEjxMcO3G0kRD5B0QvJeg,5392
10
+ autodocstring/parser.py,sha256=uxECK0SnmZL0dSUA3mYIwpvz57ksi0kAwR_wsprjRWI,2819
11
+ autodocstring/pep257_fixer.py,sha256=x9rbONRllE28cnbg6xOo0wC5bx0l_EDECheu50ebnZQ,1561
12
+ autodocstring/pydoc_report.py,sha256=0OPX4AZP7d65NEQoJGv5BFOsvNDSUKm3GPYWiD_ouIU,666
13
+ autodocstring/quality.py,sha256=DRcXOsI4_CiH53-kYyllZuqG4VSo7p7Poqs_oi5CDDM,731
14
+ autodocstring_tool-0.1.0.dist-info/licenses/LICENSE,sha256=wNekdRt3aqWWk7_mI-LKOqoh8i_kSEcYsk_7hp_nbZs,1094
15
+ autodocstring_tool-0.1.0.dist-info/METADATA,sha256=V-RmBF_5kENCmRHZNAdwCXSq46EfS0X8nV704GJElEY,271
16
+ autodocstring_tool-0.1.0.dist-info/WHEEL,sha256=YCfwYGOYMi5Jhw2fU4yNgwErybb2IX5PEwBKV4ZbdBo,91
17
+ autodocstring_tool-0.1.0.dist-info/entry_points.txt,sha256=84sJzO0Ap7fEMbOJxfjZjYJfNZ2HsSBnVzJ8F_8WaWY,57
18
+ autodocstring_tool-0.1.0.dist-info/top_level.txt,sha256=so2wqryBeazxmDEbIO1vC1ma_wnCkiB0jaNjpL0RAmc,14
19
+ autodocstring_tool-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ autodocstring = autodocstring.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 keerthireddy2006
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ autodocstring