Python API Reference¶
PyCodeCommenter exposes a clean Python API for all three of its core capabilities — generation, validation, and coverage analysis. This page documents every public class exported from the package.
All classes listed here are importable directly from the top-level package:
from PyCodeCommenter import (
PyCodeCommenter,
DocstringValidator,
ValidationReport,
ValidationIssue,
Severity,
CoverageAnalyzer,
FileCoverage,
ProjectCoverage,
TypeAnalyzer,
DocstringParser,
)
PyCodeCommenter¶
The main class for loading Python code, generating docstrings, and patching the source. All methods return self or a computed value, enabling a fluent interface.
Constructor¶
PyCodeCommenter(description_provider=None, include_module_docstrings=False, progress=None, budget=None)
| Parameter | Type | Default | Description |
|---|---|---|---|
description_provider |
DescriptionProvider or None |
None |
Opt-in AI drafting of the parts the code can't state (see AI drafting providers). None keeps generation fully deterministic, with no network calls |
include_module_docstrings |
bool |
False |
Also write a module docstring for a file that has none |
progress |
object with drafting(name) and clear(), or None |
None |
Told when a draft is requested and when it ends, so a slow request can be shown on screen (the CLI passes one; see progress.py) |
budget |
object with take(), or None |
None |
Called before each AI request; a False answer skips it. Caps the requests of a whole run (the CLI's --max-drafts) |
After construction, call from_string() or from_file() to load code.
| Attribute | Type | Description |
|---|---|---|
code |
str |
The raw source code (set by from_string / from_file) |
parsed_code |
ast.Module or None |
The parsed AST; None if parsing failed |
report |
GenerationReport |
Counts from the latest run: new, updated, unchanged docstrings, facts, ai_lines, todos, from_comments; summary_lines() gives the CLI's summary text |
comment_docstrings |
list |
Definitions whose docstring came from the # comment above them (the comment is left in place) |
drafting_stopped |
DraftingStopped or None |
Set if the description provider stopped drafting partway (for example, its daily allowance ran out) |
comments |
list |
Populated by generate_docstrings() |
tokenized_comments |
list |
Raw comment tokens extracted during load |
type_analyzer |
TypeAnalyzer |
Instance used internally for type inference |
Methods¶
from_string(code_string) -> PyCodeCommenter¶
Load code from a string. Returns self so calls can be chained.
Parameters:
| Parameter | Type | Description |
|---|---|---|
code_string |
str |
A string containing valid Python source code |
Returns: PyCodeCommenter (the same instance)
Raises: Does not raise. Logs a warning on empty input; logs an error on SyntaxError and sets parsed_code = None.
Example:
from PyCodeCommenter import PyCodeCommenter
code = """
def greet(name: str) -> str:
return f"Hello, {name}"
"""
commenter = PyCodeCommenter().from_string(code)
Runnable example (checked by the test suite):
>>> from PyCodeCommenter import PyCodeCommenter
>>> code = 'def greet(name: str) -> str:\n return f"Hello, {name}"\n'
>>> commenter = PyCodeCommenter().from_string(code)
>>> commenter.parsed_code is not None
True
>>> PyCodeCommenter().from_string("def broken(:").parsed_code is None
True
from_file(file_path) -> PyCodeCommenter¶
Load code from a file on disk. Returns self.
Parameters:
| Parameter | Type | Description |
|---|---|---|
file_path |
str |
Absolute or relative path to a Python .py file |
Returns: PyCodeCommenter (the same instance)
Raises: Does not raise. Logs an error and sets parsed_code = None on FileNotFoundError, IOError, or SyntaxError.
Example:
commenter = PyCodeCommenter().from_file("src/api.py")
if not commenter.parsed_code:
print("Could not parse the file")
Runnable example (checked by the test suite):
>>> import os, tempfile
>>> with tempfile.TemporaryDirectory() as folder:
... path = os.path.join(folder, "api.py")
... with open(path, "w", encoding="utf-8") as handle:
... _ = handle.write(code)
... loaded = PyCodeCommenter().from_file(path)
>>> loaded.parsed_code is not None
True
>>> PyCodeCommenter().from_file("no/such/file.py").parsed_code is None
True
generate_docstrings() -> list¶
Walk the AST and generate docstring strings for every function and class. Returns a flat list of generated docstring strings (one per node). Does not modify the source.
Parameters: None
Returns: list of str — one entry per function/class found (including module docstring if present)
Example:
commenter = PyCodeCommenter().from_string(code)
docstrings = commenter.generate_docstrings()
for doc in docstrings:
print(doc)
Runnable example (checked by the test suite):
>>> docstrings = PyCodeCommenter().from_string(code).generate_docstrings()
>>> len(docstrings)
1
>>> docstrings[0].splitlines()[0]
'"""Greet.'
get_patched_code() -> str¶
Return the full source code with all docstrings inserted or replaced. If a function or class already has a docstring, it is updated (the existing summary, parameter descriptions, and return text are preserved). New docstrings are inserted at the correct indentation level.
Parameters: None
Returns: str — the complete patched source code
Example:
commenter = PyCodeCommenter().from_string(code)
patched = commenter.get_patched_code()
print(patched)
# Write to disk yourself:
with open("output.py", "w", encoding="utf-8") as f:
f.write(patched)
Runnable example (checked by the test suite):
>>> patched = PyCodeCommenter().from_string(code).get_patched_code()
>>> patched.splitlines()[:3]
['def greet(name: str) -> str:', ' """Greet.', '']
>>> 'name (str): The name.' in patched
True
>>> patched.endswith('return f"Hello, {name}"\n')
True
validate(strict=False) -> ValidationReport¶
Validate all docstrings in the loaded code against the actual AST signatures. Returns a full ValidationReport.
Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
strict |
bool |
False |
Parameter is accepted but does not yet affect behaviour. Reserved for future use |
Returns: ValidationReport
Raises: Does not raise. Returns an empty ValidationReport if parsed_code is None.
Example:
import sys
from PyCodeCommenter import PyCodeCommenter
commenter = PyCodeCommenter().from_file("src/api.py")
report = commenter.validate()
report.print_summary()
if report.stats.errors > 0:
sys.exit(1)
Runnable example (checked by the test suite):
>>> report = PyCodeCommenter().from_string(code).validate()
>>> (report.stats.errors, report.stats.total_functions)
(1, 1)
>>> report.issues[0].message
"Function 'greet' has no docstring"
check_coverage() -> FileCoverage¶
Calculate documentation coverage for the loaded code.
Parameters: None
Returns: FileCoverage — coverage stats for the in-memory code (path is set to "<string>")
Example:
commenter = PyCodeCommenter().from_string(code)
coverage = commenter.check_coverage()
print(f"{coverage.coverage_percentage:.1f}%")
print(f"Functions: {coverage.documented_functions}/{coverage.total_functions}")
print(f"Classes: {coverage.documented_classes}/{coverage.total_classes}")
Runnable example (checked by the test suite):
>>> coverage = PyCodeCommenter().from_string(code).check_coverage()
>>> (coverage.path, coverage.total_functions, coverage.documented_functions)
('<string>', 1, 0)
>>> coverage.coverage_percentage
0.0
DocstringValidator¶
Validates an existing Python source file or code string for documentation quality.
Constructor¶
Provide exactly one of the two parameters.
| Parameter | Type | Default | Description |
|---|---|---|---|
code_string |
str or None |
None |
Python source code as a string |
file_path |
str or None |
None |
Path to a .py file to read and validate |
If both are provided, code_string takes precedence.
Example:
from PyCodeCommenter import DocstringValidator
# From string
validator = DocstringValidator(code_string=my_code)
# From file
validator = DocstringValidator(file_path="src/models.py")
Methods¶
validate_all() -> ValidationReport¶
Run all six validation checks across all functions and classes in the parsed code.
Returns: ValidationReport
Example:
validator = DocstringValidator(file_path="src/models.py")
report = validator.validate_all()
report.print_summary()
Runnable example (checked by the test suite):
>>> from PyCodeCommenter import DocstringValidator
>>> validator = DocstringValidator(code_string=code)
>>> report = validator.validate_all()
>>> (report.stats.errors, report.stats.warnings, report.stats.info)
(1, 0, 0)
>>> DocstringValidator(file_path="no/such/file.py").validate_all().issues
[]
check_signature_match(func_node, docstring, location) -> List[ValidationIssue]¶
Check that every parameter in the function signature is documented and every documented parameter exists in the signature.
Parameters:
| Parameter | Type | Description |
|---|---|---|
func_node |
ast.FunctionDef or ast.AsyncFunctionDef |
The function AST node |
docstring |
str |
The raw docstring text |
location |
str |
Location string for issue reporting, e.g. "file.py:10:my_func" |
Returns: List[ValidationIssue]
Runnable example (checked by the test suite):
>>> import ast
>>> source = '''
... def f(a: int, b: str = "x") -> int:
... if a < 0:
... raise ValueError("negative")
... return a
... '''
>>> func = ast.parse(source).body[0]
>>> validator = DocstringValidator(code_string=source)
>>> issues = validator.check_signature_match(
... func, "Do f.\n\nArgs:\n a (int): The a.\n", "demo.py:2:f"
... )
>>> [(issue.severity.name, issue.message) for issue in issues]
[('ERROR', "Parameter 'b' is not documented in docstring")]
check_type_consistency(func_node, docstring, location) -> List[ValidationIssue]¶
Check that annotated parameters and return types are reflected in the docstring.
Parameters: Same structure as check_signature_match.
Returns: List[ValidationIssue]
Runnable example (checked by the test suite):
>>> issues = validator.check_type_consistency(
... func, "Do f.\n\nArgs:\n a (int): The a.\n", "demo.py:2:f"
... )
>>> [(issue.severity.name, issue.message) for issue in issues]
[('WARNING', "Function has return type hint 'int' but no Returns section in docstring"), ('INFO', "Parameter 'b' has type hint 'str' but is not documented")]
check_exception_documentation(func_node, docstring, location) -> List[ValidationIssue]¶
Check that any raise statements in the function body are matched by a recognised Raises section in the docstring — Google-style Raises:, a bare Raises header, or Sphinx-style :raises.
Parameters: Same structure as check_signature_match.
Returns: List[ValidationIssue]
Runnable example (checked by the test suite):
>>> issues = validator.check_exception_documentation(func, "Do f.", "demo.py:2:f")
>>> [issue.message for issue in issues]
["Function raises exceptions {'ValueError'} but has no Raises section"]
check_return_documentation(func_node, docstring, location) -> List[ValidationIssue]¶
Check that return <value> statements are matched by a Returns: section and vice versa.
Parameters: Same structure as check_signature_match.
Returns: List[ValidationIssue]
Runnable example (checked by the test suite):
>>> issues = validator.check_return_documentation(func, "Do f.", "demo.py:2:f")
>>> [issue.message for issue in issues]
['Function returns a value but has no Returns section in docstring']
check_format_compliance(docstring, location) -> List[ValidationIssue]¶
Check that the docstring has a summary line and only uses recognised Google-style section headers.
Parameters:
| Parameter | Type | Description |
|---|---|---|
docstring |
str |
The raw docstring text |
location |
str |
Location string for issue reporting |
Returns: List[ValidationIssue]
Runnable example (checked by the test suite):
>>> [issue.message for issue in validator.check_format_compliance("", "demo.py:2:f")]
['Docstring is empty']
>>> validator.check_format_compliance("Do f.", "demo.py:2:f")
[]
check_content_quality(docstring, location) -> List[ValidationIssue]¶
Check for placeholder text, very short summaries, and duplicate parameter descriptions.
Parameters: Same as check_format_compliance.
Returns: List[ValidationIssue]
Runnable example (checked by the test suite):
>>> issues = validator.check_content_quality("TODO: write this.", "demo.py:2:f")
>>> [issue.message for issue in issues]
["Placeholder text 'TODO' found in docstring"]
ValidationReport¶
Container for the results of a validate_all() run.
Attributes¶
| Attribute | Type | Description |
|---|---|---|
issues |
List[ValidationIssue] |
All issues found |
stats |
ValidationStats |
Aggregated statistics |
file_path |
str or None |
Source file path, if applicable |
Methods¶
add_issue(issue) -> None¶
Add a ValidationIssue to the report and update stats counters.
print_summary() -> None¶
Print a human-readable summary of the report to stdout, including all issues and suggestions.
to_dict() -> dict¶
Return the report as a Python dictionary, suitable for JSON serialisation.
Example:
import json
report_dict = report.to_dict()
with open("report.json", "w") as f:
json.dump(report_dict, f, indent=2)
to_markdown() -> str¶
Return the report as a Markdown-formatted string.
Example:
count_placeholders() -> int¶
Count the issues that report placeholder text (TODO, FIXME, this tool's own TODO(pycodecommenter) marker and the like). This is what the --fail-on-todo flag of validate checks.
count_ai_drafts() -> int¶
Count the docstrings that still hold an unreviewed (AI-drafted, unreviewed) line. This is what --fail-on-ai-draft checks.
Runnable example (checked by the test suite):
>>> import json
>>> from PyCodeCommenter import Severity, ValidationIssue, ValidationReport
>>> report = ValidationReport()
>>> report.add_issue(
... ValidationIssue(Severity.WARNING, "quality", "f.py:1:f", "Placeholder text", "Replace it")
... )
>>> (report.stats.total_issues, report.stats.warnings)
(1, 1)
>>> report.to_dict()["issues"]
[{'line': 1, 'severity': 'WARNING', 'check': 'quality', 'message': 'Placeholder text'}]
>>> json.loads(json.dumps(report.to_dict()))["stats"]["warnings"]
1
>>> report.to_markdown().splitlines()[0]
'# Validation Report'
>>> (report.count_placeholders(), report.count_ai_drafts())
(1, 0)
ValidationStats¶
Dataclass holding aggregated counts from a validation run. Accessed via report.stats.
| Field | Type | Description |
|---|---|---|
total_functions |
int |
Total functions encountered |
total_classes |
int |
Total classes encountered |
documented_functions |
int |
Functions with a docstring |
documented_classes |
int |
Classes with a docstring |
total_issues |
int |
Total issues found |
errors |
int |
ERROR-level issue count |
warnings |
int |
WARNING-level issue count |
info |
int |
INFO-level issue count (renamed from infos in v2.3.0 to match the "info" key in JSON/Markdown output; .infos still works as a backward-compatible property alias) |
coverage_percentage |
float (property) |
(documented / total) * 100 |
Runnable example (checked by the test suite):
ValidationIssue¶
Dataclass representing a single documentation problem.
| Field | Type | Description |
|---|---|---|
severity |
Severity |
Severity.ERROR, Severity.WARNING, or Severity.INFO |
category |
str |
One of "missing", "signature", "types", "exceptions", "returns", "format", "quality", "ai_draft" |
location |
str |
"file.py:line:function_name" |
message |
str |
Human-readable description of the issue |
suggestion |
str or None |
How to fix the issue |
Runnable example (checked by the test suite):
>>> issue = report.issues[0]
>>> (issue.severity, issue.category, issue.location)
(<Severity.WARNING: 'warning'>, 'quality', 'f.py:1:f')
>>> str(issue)
'[WARNING] f.py:1:f: Placeholder text'
Severity¶
Enum for issue severity levels.
from PyCodeCommenter import Severity
Severity.ERROR # value: "error" — Must fix; causes exit code 1
Severity.WARNING # value: "warning" — Should fix; does not affect exit code
Severity.INFO # value: "info" — Nice to have; does not affect exit code
Runnable example (checked by the test suite):
CoverageAnalyzer¶
Analyses documentation coverage for individual files or entire directory trees.
Constructor¶
| Parameter | Type | Default | Description |
|---|---|---|---|
strict |
bool |
False |
Do not count a docstring that still holds a TODO(pycodecommenter) placeholder or an unreviewed AI-drafted line (the CLI's coverage --strict) |
Methods¶
analyze_file(file_path) -> FileCoverage¶
Analyse a single Python source file.
Parameters:
| Parameter | Type | Description |
|---|---|---|
file_path |
str |
Path to the .py file |
Returns: FileCoverage
Example:
from PyCodeCommenter import CoverageAnalyzer
analyzer = CoverageAnalyzer()
coverage = analyzer.analyze_file("src/api.py")
print(f"{coverage.coverage_percentage:.1f}%")
analyze_directory(directory, exclude_patterns=None) -> ProjectCoverage¶
Recursively analyse all .py files in a directory.
Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
directory |
str |
— | Path to the directory to analyse |
exclude_patterns |
List[str] or None |
None |
Extra patterns to exclude, added to the built-in defaults (__pycache__, .git, .venv, venv, env, .tox, .nox, __pypackages__, site-packages, build, dist, .eggs, .egg-info, .mypy_cache, .pytest_cache, node_modules, tests, test_) — never a replacement for them. A pattern matches a path component exactly, plus a dot-prefixed pattern (e.g. .egg-info) also matches a component it's a suffix of, and an underscore-suffixed pattern (e.g. test_) also matches a component it's a prefix of; it is not a substring match against the whole path |
Returns: ProjectCoverage
Example:
analyzer = CoverageAnalyzer()
project = analyzer.analyze_directory("./src", exclude_patterns=["migrations"])
project.print_report()
if project.total_coverage < 80.0:
print(f"Coverage {project.total_coverage:.1f}% is below threshold")
Runnable example (checked by the test suite):
>>> import os, tempfile
>>> from PyCodeCommenter import CoverageAnalyzer
>>> folder = tempfile.mkdtemp()
>>> path = os.path.join(folder, "module.py")
>>> with open(path, "w", encoding="utf-8") as handle:
... _ = handle.write('def documented():\n """Doc."""\n\n\ndef bare():\n pass\n')
>>> analyzer = CoverageAnalyzer()
>>> analyzer.analyze_file(path).coverage_percentage
50.0
>>> analyzer.analyze_directory(folder).total_coverage
50.0
>>> analyzer.analyze_directory(folder, exclude_patterns=["module.py"]).files
{}
FileCoverage¶
Dataclass holding coverage statistics for one file.
| Field | Type | Description |
|---|---|---|
path |
str |
Path to the file |
total_functions |
int |
Total functions found |
documented_functions |
int |
Functions with docstrings |
total_classes |
int |
Total classes found |
documented_classes |
int |
Classes with docstrings |
coverage_percentage |
float (property) |
(documented / total) * 100 |
Runnable example (checked by the test suite):
>>> file_coverage = analyzer.analyze_file(path)
>>> (file_coverage.total_functions, file_coverage.documented_functions)
(2, 1)
>>> (file_coverage.total_classes, file_coverage.documented_classes)
(0, 0)
ProjectCoverage¶
Dataclass holding coverage statistics across all analysed files.
| Field | Type | Description |
|---|---|---|
files |
Dict[str, FileCoverage] |
Mapping of file path to its FileCoverage |
total_coverage |
float (property) |
Aggregate coverage across all files |
Methods¶
print_report() -> None¶
Print a formatted coverage table to stdout.
to_json() -> dict¶
Export coverage data as a dictionary.
import json
data = project.to_json()
with open("coverage.json", "w") as f:
json.dump(data, f, indent=2)
Runnable example (checked by the test suite):
>>> project = analyzer.analyze_directory(folder)
>>> list(project.files) == [path]
True
>>> project.to_json()["total_coverage"]
50.0
>>> project.to_json()["files"][path]["functions"]
'1/2'
shields_badge_dict¶
A standalone function, not a class — imported from the coverage submodule directly (it isn't in the top-level PyCodeCommenter package __all__):
shields_badge_dict(percentage, label="docs coverage") -> dict¶
Build a shields.io endpoint-badge dict for a coverage percentage. This is what backs the coverage CLI command's --badge-output flag.
Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
percentage |
float |
— | Coverage percentage, 0-100 |
label |
str |
"docs coverage" |
Badge label text |
Returns: dict with schemaVersion, label, message, and color keys. color is brightgreen at ≥90%, green at ≥75%, yellow at ≥50%, and red below that.
Example:
import json
from PyCodeCommenter import CoverageAnalyzer
from PyCodeCommenter.coverage import shields_badge_dict
project = CoverageAnalyzer().analyze_directory("./src")
badge = shields_badge_dict(project.total_coverage)
with open("coverage_badge.json", "w") as f:
json.dump(badge, f, indent=2)
Runnable example (checked by the test suite):
>>> from PyCodeCommenter.coverage import shields_badge_dict
>>> shields_badge_dict(95)
{'schemaVersion': 1, 'label': 'docs coverage', 'message': '95%', 'color': 'brightgreen'}
>>> shields_badge_dict(50.0, label="docs")["color"]
'yellow'
TypeAnalyzer¶
Infers Python types from AST nodes. Used internally by PyCodeCommenter during docstring generation. Exposed publicly for advanced programmatic use.
Constructor¶
| Parameter | Type | Default | Description |
|---|---|---|---|
local_types |
Dict[str, str] or None |
{} |
Pre-known variable-to-type mappings for the current scope |
Key Methods¶
infer_type(node) -> str¶
Infer type for an ast.arg node (with annotation) or any expression node.
infer_expr_type(expr) -> str¶
Infer type from an expression node (literal, list, dict, binary op, call, etc.).
get_annotation_type(annotation) -> str¶
Translate an annotation AST node (supports PEP 604 | and PEP 585 generics) into a readable string such as "list[str]" or "Union[int, None]".
Runnable example (checked by the test suite):
>>> import ast
>>> from PyCodeCommenter import TypeAnalyzer
>>> tree = ast.parse("def k(x: int | None, y: list[str]): ...\nz = 3")
>>> function, assignment = tree.body
>>> analyzer = TypeAnalyzer()
>>> analyzer.infer_type(function.args.args[0])
'Union[int, None]'
>>> analyzer.infer_type(function.args.args[1])
'list[str]'
>>> analyzer.infer_expr_type(assignment.value)
'int'
>>> analyzer.get_annotation_type(function.args.args[0].annotation)
'Union[int, None]'
DocstringParser¶
Parses existing Google-, Sphinx- or NumPy-style docstrings into structured components and records which style they use. Used internally during the merge step of get_patched_code(), so that author text is kept and the style is preserved.
Constructor¶
| Parameter | Type | Default | Description |
|---|---|---|---|
docstring |
str or None |
"" |
The raw docstring text to parse. Parsing happens immediately on construction |
Attributes (after construction)¶
| Attribute | Type | Description |
|---|---|---|
summary |
str |
The first paragraph of the docstring |
description |
str |
Body text before the first section |
params |
Dict[str, str] |
Parameter name → description |
param_types |
Dict[str, str] |
Parameter name → type, where the docstring declares one |
returns |
str |
The returns (or yields) text |
raises |
Dict[str, str] |
Exception name → when it's raised |
attributes |
Dict[str, str] |
Class attribute name → description |
attribute_types |
Dict[str, str] |
Class attribute name → type, where declared |
methods |
str |
An author's Google-style Methods: section, verbatim |
style |
str |
"google", "numpy" or "sphinx" |
Methods¶
get_info() -> dict¶
Return all parsed components as a dictionary.
{
"summary": "...",
"description": "...",
"params": {"name": "description", ...},
"param_types": {"name": "type", ...},
"returns": "...",
"raises": {"ValueError": "...", ...},
"attributes": {"name": "description", ...},
"attribute_types": {"name": "type", ...},
"methods": "...",
"style": "google",
}
Example:
from PyCodeCommenter import DocstringParser
docstring = """Process the data.
Args:
data (str): The data to process.
Returns:
str: The processed result.
"""
parser = DocstringParser(docstring)
info = parser.get_info()
print(info["summary"]) # "Process the data."
print(info["params"]) # {"data": "The data to process."}
print(info["returns"]) # "str: The processed result."
Runnable example (checked by the test suite):
>>> from PyCodeCommenter import DocstringParser
>>> parser = DocstringParser("Do it.\n\nArgs:\n x (int): The x.\n")
>>> parser.get_info()["params"]
{'x': 'The x.'}
>>> (parser.style, parser.param_types)
('google', {'x': 'int'})
>>> DocstringParser().summary
''
AI drafting providers¶
Generation is deterministic unless you pass a description_provider. A provider is asked to draft only the parts the code can't state — the name-derived summary, a missing description, parameters with a TODO or type-only description, an undescribed return value, and exceptions without a readable condition. It is never asked to replace author text or facts read from the code. Every drafted line is written with the marker (AI-drafted, unreviewed), and each value is checked before it's written (no triple quotes, backslashes, placeholder or marker text).
Built-in providers¶
from PyCodeCommenter import PyCodeCommenter
from PyCodeCommenter.remote_provider import RemoteDescriptionProvider, DEFAULT_BACKEND_URL
from PyCodeCommenter.direct_providers import make_provider
# PyCodeCommenter's hosted service: no key, daily limit per user.
hosted = RemoteDescriptionProvider(backend_url=DEFAULT_BACKEND_URL)
# Your own key: "gemini", "openai", "anthropic", "deepseek" or "openai-compatible".
# Needs the matching extra, e.g. pip install "pycodecommenter[anthropic]".
own_key = make_provider("anthropic", api_key="...", model="claude-opus-5-5")
patched = PyCodeCommenter(description_provider=own_key).from_file("app.py").get_patched_code()
The CLI's --ai-draft also asks for consent before sending code anywhere; when you use a provider from the API, that decision is yours.
Runnable example (checked by the test suite):
>>> from PyCodeCommenter.description_provider import DescriptionProvider, DocstringDraft
>>> class OfflineProvider(DescriptionProvider):
... def draft_docstring(self, context, known, slots):
... return DocstringDraft(
... summary="Compute the total." if slots.summary else None,
... params={name: "The value." for name in slots.params},
... returns="The total." if slots.returns else None,
... )
>>> source = "def total(amount, tax):\n return amount + tax\n"
>>> commenter = PyCodeCommenter(description_provider=OfflineProvider())
>>> patched = commenter.from_string(source).get_patched_code()
>>> patched.splitlines()[1]
' """Compute the total. (AI-drafted, unreviewed)'
>>> commenter.report.ai_lines
4
Writing your own provider¶
Subclass DescriptionProvider (in PyCodeCommenter.description_provider) and implement draft_docstring:
from PyCodeCommenter.description_provider import (
DescriptionProvider,
DocstringDraft,
)
class MyProvider(DescriptionProvider):
def draft_docstring(self, context, known, slots):
# context: FunctionContext — name, parameters, return_type,
# is_generator, raised_exceptions, source (comments included)
# known: KnownText — text already settled, to stay consistent with
# slots: DraftSlots — what to draft: summary, description,
# params (names), returns, raises (names)
return DocstringDraft(
summary="Compute the total." if slots.summary else None,
params={name: "..." for name in slots.params},
)
| Class | Purpose |
|---|---|
DraftSlots |
The parts requested: summary, description (bools), params, raises (tuples of names), returns (bool) |
KnownText |
Settled params, returns and raises text, for consistency |
DocstringDraft |
Your answer; leave anything you can't draft as None or out of the dicts. Unrequested parts are ignored |
DraftingStopped(reason, message) |
Raise it to stop drafting for the rest of the run (for example, a rejected key or spent quota). Any other exception skips just that function |
Providers written before draft_docstring existed, which implement only draft_function_description(context), still work: they fill the description paragraph.
Classes are drafted with draft_class_docstring(context, known, slots), which returns a ClassDraft(summary, attributes). context is a ClassContext (name, bases, attributes, and a source outline: header, class-level fields, __init__ in full, other methods as signatures), known maps attribute names to settled text, and slots is a ClassSlots (summary, and the attributes names to draft). The default declines everything, so a provider that only implements draft_docstring leaves class docstrings as they are. Set failed=True on a DocstringDraft or ClassDraft when the request itself failed, so the run summary can tell a failure from a decline.
Related¶
- CLI Reference — the command-line interface for all the same operations
- Validation Checks — detailed descriptions of every check run by
DocstringValidator - Recipes — complete CI/CD examples using the Python API