tesserakit-docs 0.4.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.
- tessera_docs/__init__.py +3 -0
- tessera_docs/cli.py +46 -0
- tessera_docs/compiler.py +141 -0
- tessera_docs/loader.py +27 -0
- tessera_docs/pack.py +36 -0
- tessera_docs/scan.py +83 -0
- tessera_docs/schema.py +19 -0
- tessera_docs/validator.py +64 -0
- tesserakit_docs-0.4.0.dist-info/METADATA +59 -0
- tesserakit_docs-0.4.0.dist-info/RECORD +12 -0
- tesserakit_docs-0.4.0.dist-info/WHEEL +4 -0
- tesserakit_docs-0.4.0.dist-info/entry_points.txt +5 -0
tessera_docs/__init__.py
ADDED
tessera_docs/cli.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
from rich.table import Table
|
|
8
|
+
|
|
9
|
+
from tessera_core.models import RunContext
|
|
10
|
+
|
|
11
|
+
from tessera_docs.pack import DocsPack
|
|
12
|
+
|
|
13
|
+
console = Console()
|
|
14
|
+
docs_app = typer.Typer(help="Measure Python docstring coverage for public symbols.")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@docs_app.command("coverage")
|
|
18
|
+
def coverage_cmd(
|
|
19
|
+
input: Path = typer.Option(Path("."), "--input", "-i", exists=True, readable=True, help="Project directory."),
|
|
20
|
+
output: Path = typer.Option(Path("docs_pack"), "--output", "-o", help="Output directory."),
|
|
21
|
+
include_tests: bool = typer.Option(False, "--include-tests", help="Include test files in the scan."),
|
|
22
|
+
) -> None:
|
|
23
|
+
"""Scan Python source and report docstring coverage for public symbols."""
|
|
24
|
+
ctx = RunContext(job_name="docs", output_dir=output)
|
|
25
|
+
pack = DocsPack()
|
|
26
|
+
artifacts = pack.run(input_path=input, ctx=ctx, options={"include_tests": include_tests})
|
|
27
|
+
|
|
28
|
+
table = Table(title="Docs Pack Created")
|
|
29
|
+
table.add_column("Artifact")
|
|
30
|
+
table.add_column("Path")
|
|
31
|
+
table.add_column("Kind")
|
|
32
|
+
for art in artifacts:
|
|
33
|
+
table.add_row(art.name, str(art.path), art.kind)
|
|
34
|
+
console.print(table)
|
|
35
|
+
|
|
36
|
+
summary = Table(title="Run Summary")
|
|
37
|
+
summary.add_column("Metric")
|
|
38
|
+
summary.add_column("Value")
|
|
39
|
+
summary.add_row("run_id", ctx.run_id)
|
|
40
|
+
summary.add_row("symbols", str(ctx.metadata.get("record_count", 0)))
|
|
41
|
+
summary.add_row("findings", str(ctx.metadata.get("finding_count", 0)))
|
|
42
|
+
console.print(summary)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def register(root_app: typer.Typer) -> None:
|
|
46
|
+
root_app.add_typer(docs_app, name="docs")
|
tessera_docs/compiler.py
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections import Counter, defaultdict
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from tessera_core.artifacts import write_jsonl, write_markdown
|
|
8
|
+
from tessera_core.models import Artifact, RunContext, ValidationFinding
|
|
9
|
+
|
|
10
|
+
from tessera_docs.loader import load_docs_records
|
|
11
|
+
from tessera_docs.schema import DocSymbol
|
|
12
|
+
from tessera_docs.validator import validate_docs_records
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def load_records(input_path: Path, options: dict[str, Any]) -> list[DocSymbol]:
|
|
16
|
+
return load_docs_records(input_path, options)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def validate_records(symbols: list[DocSymbol], options: dict[str, Any]) -> list[ValidationFinding]:
|
|
20
|
+
return validate_docs_records(symbols, options)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def write_artifacts(symbols: list[DocSymbol], ctx: RunContext, options: dict[str, Any]) -> list[Artifact]:
|
|
24
|
+
ctx.output_dir.mkdir(parents=True, exist_ok=True)
|
|
25
|
+
findings: list[ValidationFinding] = ctx.metadata.get("findings") or validate_records(symbols, options)
|
|
26
|
+
|
|
27
|
+
symbols_jsonl = ctx.output_dir / "symbols.jsonl"
|
|
28
|
+
index_md = ctx.output_dir / "index.md"
|
|
29
|
+
validation_md = ctx.output_dir / "validation_report.md"
|
|
30
|
+
coverage_md = ctx.output_dir / "coverage_report.md"
|
|
31
|
+
undocumented_md = ctx.output_dir / "undocumented.md"
|
|
32
|
+
|
|
33
|
+
write_jsonl(symbols_jsonl, [s.model_dump() for s in symbols])
|
|
34
|
+
write_markdown(index_md, _render_index(symbols, options))
|
|
35
|
+
write_markdown(validation_md, _render_validation(symbols, findings))
|
|
36
|
+
write_markdown(coverage_md, _render_coverage(symbols))
|
|
37
|
+
write_markdown(undocumented_md, _render_undocumented(symbols))
|
|
38
|
+
|
|
39
|
+
return [
|
|
40
|
+
Artifact(name="symbols.jsonl", path=symbols_jsonl, kind="jsonl"),
|
|
41
|
+
Artifact(name="index.md", path=index_md, kind="markdown"),
|
|
42
|
+
Artifact(name="validation_report.md", path=validation_md, kind="markdown"),
|
|
43
|
+
Artifact(name="coverage_report.md", path=coverage_md, kind="markdown"),
|
|
44
|
+
Artifact(name="undocumented.md", path=undocumented_md, kind="markdown"),
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _coverage(symbols: list[DocSymbol]) -> tuple[int, int, float]:
|
|
49
|
+
public = [s for s in symbols if s.is_public]
|
|
50
|
+
documented = sum(1 for s in public if s.has_docstring)
|
|
51
|
+
pct = (documented / len(public)) if public else 1.0
|
|
52
|
+
return documented, len(public), pct
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _render_index(symbols: list[DocSymbol], options: dict[str, Any]) -> str:
|
|
56
|
+
documented, total, pct = _coverage(symbols)
|
|
57
|
+
lines = ["# Documentation Coverage", ""]
|
|
58
|
+
lines.append(f"- Files scanned: {options.get('_file_count', 0)}")
|
|
59
|
+
lines.append(f"- Public symbols: {total}")
|
|
60
|
+
lines.append(f"- Documented: {documented}")
|
|
61
|
+
lines.append(f"- Coverage: {pct*100:.0f}%")
|
|
62
|
+
return "\n".join(lines) + "\n"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _render_validation(symbols: list[DocSymbol], findings: list[ValidationFinding]) -> str:
|
|
66
|
+
lines = ["# Validation Report", ""]
|
|
67
|
+
lines.append(f"- Symbols: {len(symbols)}")
|
|
68
|
+
lines.append(f"- Findings: {len(findings)}")
|
|
69
|
+
lines.append("")
|
|
70
|
+
by_sev = Counter(f.severity for f in findings)
|
|
71
|
+
lines.append("## Severity Breakdown")
|
|
72
|
+
lines.append("")
|
|
73
|
+
for sev in ("error", "warning", "info"):
|
|
74
|
+
lines.append(f"- {sev}: {by_sev.get(sev, 0)}")
|
|
75
|
+
lines.append("")
|
|
76
|
+
if findings:
|
|
77
|
+
lines.append("## Findings")
|
|
78
|
+
lines.append("")
|
|
79
|
+
for f in findings[:300]:
|
|
80
|
+
lines.append(f"- **{f.severity.upper()}** `{f.code}`: {f.message}")
|
|
81
|
+
if len(findings) > 300:
|
|
82
|
+
lines.append(f"- ... {len(findings) - 300} more findings omitted")
|
|
83
|
+
return "\n".join(lines)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _render_coverage(symbols: list[DocSymbol]) -> str:
|
|
87
|
+
lines = ["# Coverage Report", ""]
|
|
88
|
+
documented, total, pct = _coverage(symbols)
|
|
89
|
+
lines.append(f"- Public symbols: {total}")
|
|
90
|
+
lines.append(f"- Documented: {documented} ({pct*100:.0f}%)")
|
|
91
|
+
lines.append("")
|
|
92
|
+
|
|
93
|
+
# by kind
|
|
94
|
+
lines.append("## By kind")
|
|
95
|
+
lines.append("")
|
|
96
|
+
lines.append("| Kind | Public | Documented | Coverage |")
|
|
97
|
+
lines.append("|---|---:|---:|---:|")
|
|
98
|
+
by_kind: dict[str, list[DocSymbol]] = defaultdict(list)
|
|
99
|
+
for s in symbols:
|
|
100
|
+
if s.is_public:
|
|
101
|
+
by_kind[s.kind].append(s)
|
|
102
|
+
for kind in ("module", "class", "function", "method"):
|
|
103
|
+
items = by_kind.get(kind, [])
|
|
104
|
+
if not items:
|
|
105
|
+
continue
|
|
106
|
+
doc = sum(1 for s in items if s.has_docstring)
|
|
107
|
+
lines.append(f"| {kind} | {len(items)} | {doc} | {100*doc/len(items):.0f}% |")
|
|
108
|
+
lines.append("")
|
|
109
|
+
|
|
110
|
+
# worst files
|
|
111
|
+
by_file: dict[str, list[DocSymbol]] = defaultdict(list)
|
|
112
|
+
for s in symbols:
|
|
113
|
+
if s.is_public:
|
|
114
|
+
by_file[s.path].append(s)
|
|
115
|
+
rows = []
|
|
116
|
+
for path, items in by_file.items():
|
|
117
|
+
doc = sum(1 for s in items if s.has_docstring)
|
|
118
|
+
rows.append((doc / len(items), path, doc, len(items)))
|
|
119
|
+
rows.sort()
|
|
120
|
+
lines.append("## Lowest-coverage files")
|
|
121
|
+
lines.append("")
|
|
122
|
+
lines.append("| File | Documented | Public | Coverage |")
|
|
123
|
+
lines.append("|---|---:|---:|---:|")
|
|
124
|
+
for cov, path, doc, tot in rows[:15]:
|
|
125
|
+
lines.append(f"| `{path}` | {doc} | {tot} | {cov*100:.0f}% |")
|
|
126
|
+
return "\n".join(lines) + "\n"
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _render_undocumented(symbols: list[DocSymbol]) -> str:
|
|
130
|
+
missing = [s for s in symbols if s.is_public and not s.has_docstring]
|
|
131
|
+
lines = ["# Undocumented Public Symbols", ""]
|
|
132
|
+
lines.append(f"- Count: {len(missing)}")
|
|
133
|
+
lines.append("")
|
|
134
|
+
if not missing:
|
|
135
|
+
lines.append("_Everything public is documented._")
|
|
136
|
+
return "\n".join(lines) + "\n"
|
|
137
|
+
lines.append("| File | Symbol | Kind | Line |")
|
|
138
|
+
lines.append("|---|---|---|---:|")
|
|
139
|
+
for s in sorted(missing, key=lambda x: (x.path, x.lineno)):
|
|
140
|
+
lines.append(f"| `{s.path}` | `{s.qualname or s.name}` | {s.kind} | {s.lineno} |")
|
|
141
|
+
return "\n".join(lines) + "\n"
|
tessera_docs/loader.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from tessera_docs.scan import discover_py_files, extract_symbols
|
|
7
|
+
from tessera_docs.schema import DocSymbol
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def load_docs_records(input_path: Path, options: dict[str, Any]) -> list[DocSymbol]:
|
|
11
|
+
"""Extract documentable symbols from every (non-test) Python file."""
|
|
12
|
+
root = input_path if input_path.is_dir() else input_path.parent
|
|
13
|
+
include_tests = bool(options.get("include_tests", False))
|
|
14
|
+
|
|
15
|
+
symbols: list[DocSymbol] = []
|
|
16
|
+
parse_errors: list[str] = []
|
|
17
|
+
files = discover_py_files(root, include_tests=include_tests)
|
|
18
|
+
for f in files:
|
|
19
|
+
syms, err = extract_symbols(root, f)
|
|
20
|
+
symbols.extend(syms)
|
|
21
|
+
if err:
|
|
22
|
+
parse_errors.append(err)
|
|
23
|
+
|
|
24
|
+
options["_parse_errors"] = parse_errors
|
|
25
|
+
options["_file_count"] = len(files)
|
|
26
|
+
options["_root"] = str(root)
|
|
27
|
+
return symbols
|
tessera_docs/pack.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from tessera_core.jobpack import JobPack
|
|
7
|
+
from tessera_core.models import Artifact, RunContext, ValidationFinding
|
|
8
|
+
|
|
9
|
+
from tessera_docs.compiler import load_records, validate_records, write_artifacts
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class DocsPack(JobPack):
|
|
13
|
+
name = "docs"
|
|
14
|
+
version = "0.3.1"
|
|
15
|
+
|
|
16
|
+
def normalize(self, input_path: Path, options: dict[str, Any]) -> list[Any]:
|
|
17
|
+
return load_records(input_path, options)
|
|
18
|
+
|
|
19
|
+
def validate(
|
|
20
|
+
self,
|
|
21
|
+
records: list[Any],
|
|
22
|
+
options: dict[str, Any],
|
|
23
|
+
) -> list[ValidationFinding]:
|
|
24
|
+
return validate_records(records, options)
|
|
25
|
+
|
|
26
|
+
def generate(
|
|
27
|
+
self,
|
|
28
|
+
records: list[Any],
|
|
29
|
+
ctx: RunContext,
|
|
30
|
+
options: dict[str, Any],
|
|
31
|
+
) -> list[Artifact]:
|
|
32
|
+
return write_artifacts(records, ctx, options)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def create_pack() -> DocsPack:
|
|
36
|
+
return DocsPack()
|
tessera_docs/scan.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Extract documentable symbols from Python source via the ast module.
|
|
2
|
+
|
|
3
|
+
Source is parsed, never imported or executed.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import ast
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from tessera_docs.schema import DocSymbol
|
|
12
|
+
|
|
13
|
+
IGNORE_DIRS = {
|
|
14
|
+
".git", ".venv", "venv", "node_modules", "__pycache__", ".pytest_cache",
|
|
15
|
+
"dist", "build", ".tox", "target", ".mypy_cache", ".ruff_cache",
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def is_public(name: str) -> bool:
|
|
20
|
+
return not name.startswith("_")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _is_test_path(rel: Path) -> bool:
|
|
24
|
+
parts = [p.lower() for p in rel.parts]
|
|
25
|
+
if any(p in ("test", "tests", "__tests__") for p in parts[:-1]):
|
|
26
|
+
return True
|
|
27
|
+
n = rel.name.lower()
|
|
28
|
+
return n.startswith("test_") or n.endswith("_test.py")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def discover_py_files(root: Path, include_tests: bool = False) -> list[Path]:
|
|
32
|
+
out: list[Path] = []
|
|
33
|
+
for p in sorted(root.rglob("*.py")):
|
|
34
|
+
rel = p.relative_to(root)
|
|
35
|
+
if any(part in IGNORE_DIRS for part in rel.parts):
|
|
36
|
+
continue
|
|
37
|
+
if not include_tests and _is_test_path(rel):
|
|
38
|
+
continue
|
|
39
|
+
out.append(p)
|
|
40
|
+
return out
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def extract_symbols(root: Path, path: Path) -> tuple[list[DocSymbol], str | None]:
|
|
44
|
+
"""Return (symbols, parse_error). Parse error is a string or None."""
|
|
45
|
+
rel = path.relative_to(root).as_posix()
|
|
46
|
+
try:
|
|
47
|
+
source = path.read_text(encoding="utf-8")
|
|
48
|
+
tree = ast.parse(source)
|
|
49
|
+
except (OSError, UnicodeDecodeError, SyntaxError) as exc:
|
|
50
|
+
return [], f"{rel}: {exc}"
|
|
51
|
+
|
|
52
|
+
symbols: list[DocSymbol] = []
|
|
53
|
+
|
|
54
|
+
# module-level symbol
|
|
55
|
+
mod_doc = ast.get_docstring(tree)
|
|
56
|
+
symbols.append(DocSymbol(
|
|
57
|
+
path=rel, qualname="", kind="module", name=Path(rel).stem, lineno=1,
|
|
58
|
+
is_public=True, has_docstring=mod_doc is not None,
|
|
59
|
+
docstring_len=len(mod_doc or ""),
|
|
60
|
+
))
|
|
61
|
+
|
|
62
|
+
def walk(nodes, prefix: str, inside_class: bool) -> None:
|
|
63
|
+
for node in nodes:
|
|
64
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
65
|
+
kind = "method" if inside_class else "function"
|
|
66
|
+
_add(node, kind, prefix)
|
|
67
|
+
# nested functions/classes
|
|
68
|
+
walk(node.body, f"{prefix}{node.name}.", inside_class=False)
|
|
69
|
+
elif isinstance(node, ast.ClassDef):
|
|
70
|
+
_add(node, "class", prefix)
|
|
71
|
+
walk(node.body, f"{prefix}{node.name}.", inside_class=True)
|
|
72
|
+
|
|
73
|
+
def _add(node, kind: str, prefix: str) -> None:
|
|
74
|
+
doc = ast.get_docstring(node)
|
|
75
|
+
qual = f"{prefix}{node.name}"
|
|
76
|
+
symbols.append(DocSymbol(
|
|
77
|
+
path=rel, qualname=qual, kind=kind, name=node.name, lineno=node.lineno,
|
|
78
|
+
is_public=is_public(node.name), has_docstring=doc is not None,
|
|
79
|
+
docstring_len=len(doc or ""),
|
|
80
|
+
))
|
|
81
|
+
|
|
82
|
+
walk(tree.body, prefix="", inside_class=False)
|
|
83
|
+
return symbols, None
|
tessera_docs/schema.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class DocSymbol(BaseModel):
|
|
9
|
+
"""A documentable Python symbol. Serialized to ``symbols.jsonl``."""
|
|
10
|
+
|
|
11
|
+
path: str # repo-relative source file, POSIX
|
|
12
|
+
qualname: str # dotted name within the module (module-level = "")
|
|
13
|
+
kind: str # module / class / function / method
|
|
14
|
+
name: str
|
|
15
|
+
lineno: int = 0
|
|
16
|
+
is_public: bool = True
|
|
17
|
+
has_docstring: bool = False
|
|
18
|
+
docstring_len: int = 0
|
|
19
|
+
metadata: dict[str, Any] = Field(default_factory=dict)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from tessera_core.models import ValidationFinding
|
|
6
|
+
|
|
7
|
+
from tessera_docs.schema import DocSymbol
|
|
8
|
+
|
|
9
|
+
LOW_COVERAGE_THRESHOLD = 0.80
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def validate_docs_records(symbols: list[DocSymbol], options: dict[str, Any]) -> list[ValidationFinding]:
|
|
13
|
+
findings: list[ValidationFinding] = []
|
|
14
|
+
|
|
15
|
+
for err in options.get("_parse_errors", []):
|
|
16
|
+
findings.append(
|
|
17
|
+
ValidationFinding(severity="error", code="parse_error",
|
|
18
|
+
message=f"could not parse: {err}", field=None)
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
public = [s for s in symbols if s.is_public]
|
|
22
|
+
if not public:
|
|
23
|
+
if not options.get("_parse_errors"):
|
|
24
|
+
findings.append(ValidationFinding(severity="info", code="no_public_symbols",
|
|
25
|
+
message="no public symbols found", field=None))
|
|
26
|
+
return findings
|
|
27
|
+
|
|
28
|
+
code_for_kind = {
|
|
29
|
+
"module": "missing_module_docstring",
|
|
30
|
+
"class": "missing_class_docstring",
|
|
31
|
+
"function": "missing_function_docstring",
|
|
32
|
+
"method": "missing_method_docstring",
|
|
33
|
+
}
|
|
34
|
+
severity_for_kind = {
|
|
35
|
+
"module": "info",
|
|
36
|
+
"class": "warning",
|
|
37
|
+
"function": "warning",
|
|
38
|
+
"method": "warning",
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
for s in public:
|
|
42
|
+
if not s.has_docstring:
|
|
43
|
+
findings.append(
|
|
44
|
+
ValidationFinding(
|
|
45
|
+
severity=severity_for_kind.get(s.kind, "warning"),
|
|
46
|
+
code=code_for_kind.get(s.kind, "missing_docstring"),
|
|
47
|
+
message=f"{s.kind} `{s.qualname or s.name}` ({s.path}:{s.lineno}) has no docstring",
|
|
48
|
+
field="docs",
|
|
49
|
+
metadata={"path": s.path, "qualname": s.qualname or s.name, "kind": s.kind, "lineno": s.lineno},
|
|
50
|
+
)
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
documented = sum(1 for s in public if s.has_docstring)
|
|
54
|
+
coverage = documented / len(public)
|
|
55
|
+
if coverage < LOW_COVERAGE_THRESHOLD:
|
|
56
|
+
findings.append(
|
|
57
|
+
ValidationFinding(
|
|
58
|
+
severity="warning", code="low_doc_coverage",
|
|
59
|
+
message=f"public docstring coverage is {coverage*100:.0f}% (below {int(LOW_COVERAGE_THRESHOLD*100)}%)",
|
|
60
|
+
field="docs", metadata={"coverage": round(coverage, 4)},
|
|
61
|
+
)
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
return findings
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tesserakit-docs
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: Docs job pack for Tessera: measure Python docstring coverage for public symbols.
|
|
5
|
+
Project-URL: Homepage, https://github.com/ShaileshRawat1403/tessera
|
|
6
|
+
Project-URL: Repository, https://github.com/ShaileshRawat1403/tessera
|
|
7
|
+
Project-URL: Issues, https://github.com/ShaileshRawat1403/tessera/issues
|
|
8
|
+
Author: Shailesh Rawat
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Requires-Python: >=3.10
|
|
14
|
+
Requires-Dist: pydantic>=2.7
|
|
15
|
+
Requires-Dist: rich>=13.7
|
|
16
|
+
Requires-Dist: tesserakit-core>=0.1.0
|
|
17
|
+
Requires-Dist: typer>=0.12
|
|
18
|
+
Provides-Extra: dev
|
|
19
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# tesserakit-docs
|
|
23
|
+
|
|
24
|
+
Measure Python docstring coverage for public symbols.
|
|
25
|
+
|
|
26
|
+
`tessera-docs` parses Python source with the standard-library `ast` module (it never imports or runs the code), inventories every documentable symbol (modules, classes, functions, methods), and reports which public ones lack docstrings.
|
|
27
|
+
|
|
28
|
+
## Coverage check
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
tessera docs coverage --input . --output ./out/docs_pack
|
|
32
|
+
tessera docs coverage --input . --include-tests # also scan test files
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Test files are excluded by default; pass `--include-tests` to include them.
|
|
36
|
+
|
|
37
|
+
Artifacts written:
|
|
38
|
+
|
|
39
|
+
```text
|
|
40
|
+
symbols.jsonl one DocSymbol per symbol (kind, public, has_docstring, line)
|
|
41
|
+
index.md coverage headline
|
|
42
|
+
validation_report.md missing-docstring findings + low-coverage warning
|
|
43
|
+
coverage_report.md coverage by kind and lowest-coverage files
|
|
44
|
+
undocumented.md every undocumented public symbol with file:line
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## What counts
|
|
48
|
+
|
|
49
|
+
- **Public** = name does not start with `_` (so `_private` and `__dunder__` are excluded).
|
|
50
|
+
- Symbol kinds: `module`, `class`, `function`, `method`.
|
|
51
|
+
- A symbol is documented if `ast.get_docstring` returns a value.
|
|
52
|
+
|
|
53
|
+
## Findings
|
|
54
|
+
|
|
55
|
+
- `missing_module_docstring` (info)
|
|
56
|
+
- `missing_class_docstring`, `missing_function_docstring`, `missing_method_docstring` (warning)
|
|
57
|
+
- `low_doc_coverage` — overall public coverage below 80%
|
|
58
|
+
- `parse_error` — a file could not be parsed
|
|
59
|
+
- `no_public_symbols` — nothing public found
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
tessera_docs/__init__.py,sha256=lOU_jR1sGVZe8D1lbLDFa_u4C1SGz-s021MYlwke8pc,48
|
|
2
|
+
tessera_docs/cli.py,sha256=Xt5Ci_N3U5hbeth_D2shuT_W4I9aN5Eb8b7CgpWwH2Q,1619
|
|
3
|
+
tessera_docs/compiler.py,sha256=jTk4rbe7HBNVRvCJQNRTuuqvPv7kDj_pvgWfRW9Q24I,5694
|
|
4
|
+
tessera_docs/loader.py,sha256=eF4UXvmGEL7OgyGjJhwYd27Ko-hUrs8fOuH5vP4IjoA,900
|
|
5
|
+
tessera_docs/pack.py,sha256=wgJBaLPrzbOkVqD80GsEnm_RKY5WGGlNZRd-GEgNuKo,910
|
|
6
|
+
tessera_docs/scan.py,sha256=oPDW_8vh33pcSGaeAJ5Bin83bvlJs50k_Wco9DH8VxI,2819
|
|
7
|
+
tessera_docs/schema.py,sha256=_zUGhJodAkHte3M8KQe6bISqMl8HnpnWxq8VHLXuBTc,580
|
|
8
|
+
tessera_docs/validator.py,sha256=vTsB3Q_1B4Orwqq49AMG2yiCMtUU0_E9EEZlrAVGIcY,2293
|
|
9
|
+
tesserakit_docs-0.4.0.dist-info/METADATA,sha256=iIF73PndfuUMAWw7OlMqvJ8x1MCwUrmWWhSvNyvELi4,2259
|
|
10
|
+
tesserakit_docs-0.4.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
11
|
+
tesserakit_docs-0.4.0.dist-info/entry_points.txt,sha256=n6X0ZZpenfXtDacASPce_62cOWcDHKjvuBnuiQdGfMk,109
|
|
12
|
+
tesserakit_docs-0.4.0.dist-info/RECORD,,
|