A Python docstring linter that checks arguments, returns, yields, and raises sections
—
Comprehensive violation reporting system with specific error codes (DOC0xx-DOC6xx) covering all categories of docstring issues, from formatting to argument mismatches.
Core class representing individual docstring violations with detailed error information.
class Violation:
"""
Represents a docstring violation with detailed error information.
Provides comprehensive violation reporting including error codes,
line numbers, and formatted messages for both CLI and flake8 output.
"""
code: int # Violation code number (1-6xx range)
line: int # Line number where violation occurs
msg: str # Complete violation message
msgPostfix: str # Additional message content
def __init__(
self,
line: int,
code: int,
msgPrefix: str = '',
msgPostfix: str = '',
) -> None:
"""
Initialize violation with error details.
Parameters:
- line: Line number of violation (0 for file-level issues)
- code: Violation code number
- msgPrefix: Prefix for violation message (deprecated)
- msgPostfix: Additional information to append to standard message
"""
@property
def fullErrorCode(self) -> str:
"""
Get full error code with DOC prefix.
Returns:
str: Full error code (e.g., "DOC101", "DOC201")
"""
def getInfoForFlake8(self) -> tuple[int, int, str]:
"""
Format violation information for flake8 output.
Returns:
tuple containing:
- int: Line number
- int: Column offset (always 0)
- str: Formatted error message with DOC code
"""
def __str__(self) -> str:
"""
String representation of violation.
Returns:
str: Formatted violation message
"""Complete mapping of violation codes to their standard error messages.
VIOLATION_CODES: dict[int, str] = {
# General and formatting issues (DOC0xx)
1: "Potential formatting errors in docstring. Error message:",
2: "Syntax errors; cannot parse this Python file. Error message:",
3: "Docstring style mismatch. (Please read more at https://jsh9.github.io/pydoclint/style_mismatch.html ).",
# Argument-related violations (DOC1xx)
101: "Docstring contains fewer arguments than in function signature.",
102: "Docstring contains more arguments than in function signature.",
103: "Docstring arguments are different from function arguments. (Or could be other formatting issues: https://jsh9.github.io/pydoclint/violation_codes.html#notes-on-doc103 ).",
104: "Arguments are the same in the docstring and the function signature, but are in a different order.",
105: "Argument names match, but type hints in these args do not match:",
106: "The option `--arg-type-hints-in-signature` is `True` but there are no argument type hints in the signature",
107: "The option `--arg-type-hints-in-signature` is `True` but not all args in the signature have type hints",
108: "The option `--arg-type-hints-in-signature` is `False` but there are argument type hints in the signature",
109: "The option `--arg-type-hints-in-docstring` is `True` but there are no type hints in the docstring arg list",
110: "The option `--arg-type-hints-in-docstring` is `True` but not all args in the docstring arg list have type hints",
111: "The option `--arg-type-hints-in-docstring` is `False` but there are type hints in the docstring arg list",
# Return-related violations (DOC2xx)
201: "does not have a return section in docstring",
202: "has a return section in docstring, but there are no return statements or annotations",
203: "return type(s) in docstring not consistent with the return annotation.",
# Class and __init__ violations (DOC3xx)
301: "__init__() should not have a docstring; please combine it with the docstring of the class",
302: "The class docstring does not need a \"Returns\" section, because __init__() cannot return anything",
303: "The __init__() docstring does not need a \"Returns\" section, because it cannot return anything",
304: "Class docstring has an argument/parameter section; please put it in the __init__() docstring",
305: "Class docstring has a \"Raises\" section; please put it in the __init__() docstring",
306: "The class docstring does not need a \"Yields\" section, because __init__() cannot yield anything",
307: "The __init__() docstring does not need a \"Yields\" section, because __init__() cannot yield anything",
# Yield-related violations (DOC4xx)
402: "has \"yield\" statements, but the docstring does not have a \"Yields\" section",
403: "has a \"Yields\" section in the docstring, but there are no \"yield\" statements, or the return annotation is not a Generator/Iterator/Iterable. (Or it could be because the function lacks a return annotation.)",
404: "yield type(s) in docstring not consistent with the return annotation.",
405: "has both \"return\" and \"yield\" statements. Please use Generator[YieldType, SendType, ReturnType] as the return type annotation, and put your yield type in YieldType and return type in ReturnType. More details in https://jsh9.github.io/pydoclint/notes_generator_vs_iterator.html",
# Raises-related violations (DOC5xx)
501: "has raise statements, but the docstring does not have a \"Raises\" section",
502: "has a \"Raises\" section in the docstring, but there are not \"raise\" statements in the body",
503: "exceptions in the \"Raises\" section in the docstring do not match those in the function body.",
504: "has assert statements, but the docstring does not have a \"Raises\" section. (Assert statements could raise \"AssertError\".)",
# Class attribute violations (DOC6xx)
601: "Class docstring contains fewer class attributes than actual class attributes.",
602: "Class docstring contains more class attributes than in actual class attributes.",
603: "Class docstring attributes are different from actual class attributes. (Or could be other formatting issues: https://jsh9.github.io/pydoclint/violation_codes.html#notes-on-doc103 ).",
604: "Attributes are the same in docstring and class def, but are in a different order",
605: "Attribute names match, but type hints in these attributes do not match",
}from pydoclint.utils.violation import Violation, VIOLATION_CODES
# Create a violation for missing arguments
violation = Violation(
code=101,
line=15,
msgPostfix="Missing arguments: x, y"
)
print(violation.fullErrorCode) # "DOC101"
print(str(violation)) # Full formatted message
print(violation.getInfoForFlake8()) # (15, 0, "DOC101: Docstring contains fewer arguments...")
# Check violation code meaning
print(VIOLATION_CODES[101]) # "Docstring contains fewer arguments than in function signature."from pydoclint.main import _checkFile
from pathlib import Path
# Analyze file and process violations
violations = _checkFile(Path("example.py"), style="numpy")
for violation in violations:
print(f"Line {violation.line}: {violation.fullErrorCode}")
print(f" {violation.msg}")
# Handle specific violation types
if violation.code in [101, 102, 103]:
print(" -> Argument mismatch issue")
elif violation.code in [201, 202, 203]:
print(" -> Return section issue")
elif violation.code in [402, 403, 404]:
print(" -> Yield section issue")def categorize_violations(violations: list[Violation]) -> dict[str, list[Violation]]:
"""Categorize violations by type."""
categories = {
"general": [], # DOC001-DOC003
"arguments": [], # DOC101-DOC111
"returns": [], # DOC201-DOC203
"class_init": [], # DOC301-DOC307
"yields": [], # DOC402-DOC405
"raises": [], # DOC501-DOC502
"class_attrs": [], # DOC601-DOC603
}
for violation in violations:
if 1 <= violation.code <= 3:
categories["general"].append(violation)
elif 101 <= violation.code <= 111:
categories["arguments"].append(violation)
elif 201 <= violation.code <= 203:
categories["returns"].append(violation)
elif 301 <= violation.code <= 307:
categories["class_init"].append(violation)
elif 402 <= violation.code <= 405:
categories["yields"].append(violation)
elif 501 <= violation.code <= 502:
categories["raises"].append(violation)
elif 601 <= violation.code <= 603:
categories["class_attrs"].append(violation)
return categories
# Usage
violations = _checkFile(Path("example.py"))
categorized = categorize_violations(violations)
print(f"Argument issues: {len(categorized['arguments'])}")
print(f"Return issues: {len(categorized['returns'])}")class ViolationProcessor:
"""Process and format violations for different outputs."""
def __init__(self, show_filenames: bool = False):
self.show_filenames = show_filenames
def format_for_console(self, filename: str, violations: list[Violation]) -> str:
"""Format violations for console output."""
if not violations:
return ""
output = []
if not self.show_filenames:
output.append(f"{filename}:")
for violation in violations:
prefix = f"{filename}:" if self.show_filenames else " "
output.append(f"{prefix}{violation.line}: {violation.fullErrorCode}: {violation.msg}")
return "\n".join(output)
def format_for_json(self, filename: str, violations: list[Violation]) -> dict:
"""Format violations for JSON output."""
return {
"file": filename,
"violations": [
{
"line": v.line,
"code": v.fullErrorCode,
"message": v.msg,
"severity": self._get_severity(v.code)
}
for v in violations
]
}
def _get_severity(self, code: int) -> str:
"""Determine violation severity."""
if code in [1, 2]:
return "error"
elif code in [101, 102, 201, 402, 501]:
return "warning"
else:
return "info"
# Usage
processor = ViolationProcessor(show_filenames=True)
violations = _checkFile(Path("example.py"))
# Console output
console_output = processor.format_for_console("example.py", violations)
print(console_output)
# JSON output
json_output = processor.format_for_json("example.py", violations)
import json
print(json.dumps(json_output, indent=2))Install with Tessl CLI
npx tessl i tessl/pypi-pydoclint