truthound-dashboard 1.3.1__py3-none-any.whl → 1.4.1__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.
- truthound_dashboard/api/alerts.py +258 -0
- truthound_dashboard/api/anomaly.py +1302 -0
- truthound_dashboard/api/cross_alerts.py +352 -0
- truthound_dashboard/api/deps.py +143 -0
- truthound_dashboard/api/drift_monitor.py +540 -0
- truthound_dashboard/api/lineage.py +1151 -0
- truthound_dashboard/api/maintenance.py +363 -0
- truthound_dashboard/api/middleware.py +373 -1
- truthound_dashboard/api/model_monitoring.py +805 -0
- truthound_dashboard/api/notifications_advanced.py +2452 -0
- truthound_dashboard/api/plugins.py +2096 -0
- truthound_dashboard/api/profile.py +211 -14
- truthound_dashboard/api/reports.py +853 -0
- truthound_dashboard/api/router.py +147 -0
- truthound_dashboard/api/rule_suggestions.py +310 -0
- truthound_dashboard/api/schema_evolution.py +231 -0
- truthound_dashboard/api/sources.py +47 -3
- truthound_dashboard/api/triggers.py +190 -0
- truthound_dashboard/api/validations.py +13 -0
- truthound_dashboard/api/validators.py +333 -4
- truthound_dashboard/api/versioning.py +309 -0
- truthound_dashboard/api/websocket.py +301 -0
- truthound_dashboard/core/__init__.py +27 -0
- truthound_dashboard/core/anomaly.py +1395 -0
- truthound_dashboard/core/anomaly_explainer.py +633 -0
- truthound_dashboard/core/cache.py +206 -0
- truthound_dashboard/core/cached_services.py +422 -0
- truthound_dashboard/core/charts.py +352 -0
- truthound_dashboard/core/connections.py +1069 -42
- truthound_dashboard/core/cross_alerts.py +837 -0
- truthound_dashboard/core/drift_monitor.py +1477 -0
- truthound_dashboard/core/drift_sampling.py +669 -0
- truthound_dashboard/core/i18n/__init__.py +42 -0
- truthound_dashboard/core/i18n/detector.py +173 -0
- truthound_dashboard/core/i18n/messages.py +564 -0
- truthound_dashboard/core/lineage.py +971 -0
- truthound_dashboard/core/maintenance.py +443 -5
- truthound_dashboard/core/model_monitoring.py +1043 -0
- truthound_dashboard/core/notifications/channels.py +1020 -1
- truthound_dashboard/core/notifications/deduplication/__init__.py +143 -0
- truthound_dashboard/core/notifications/deduplication/policies.py +274 -0
- truthound_dashboard/core/notifications/deduplication/service.py +400 -0
- truthound_dashboard/core/notifications/deduplication/stores.py +2365 -0
- truthound_dashboard/core/notifications/deduplication/strategies.py +422 -0
- truthound_dashboard/core/notifications/dispatcher.py +43 -0
- truthound_dashboard/core/notifications/escalation/__init__.py +149 -0
- truthound_dashboard/core/notifications/escalation/backends.py +1384 -0
- truthound_dashboard/core/notifications/escalation/engine.py +429 -0
- truthound_dashboard/core/notifications/escalation/models.py +336 -0
- truthound_dashboard/core/notifications/escalation/scheduler.py +1187 -0
- truthound_dashboard/core/notifications/escalation/state_machine.py +330 -0
- truthound_dashboard/core/notifications/escalation/stores.py +2896 -0
- truthound_dashboard/core/notifications/events.py +49 -0
- truthound_dashboard/core/notifications/metrics/__init__.py +115 -0
- truthound_dashboard/core/notifications/metrics/base.py +528 -0
- truthound_dashboard/core/notifications/metrics/collectors.py +583 -0
- truthound_dashboard/core/notifications/routing/__init__.py +169 -0
- truthound_dashboard/core/notifications/routing/combinators.py +184 -0
- truthound_dashboard/core/notifications/routing/config.py +375 -0
- truthound_dashboard/core/notifications/routing/config_parser.py +867 -0
- truthound_dashboard/core/notifications/routing/engine.py +382 -0
- truthound_dashboard/core/notifications/routing/expression_engine.py +1269 -0
- truthound_dashboard/core/notifications/routing/jinja2_engine.py +774 -0
- truthound_dashboard/core/notifications/routing/rules.py +625 -0
- truthound_dashboard/core/notifications/routing/validator.py +678 -0
- truthound_dashboard/core/notifications/service.py +2 -0
- truthound_dashboard/core/notifications/stats_aggregator.py +850 -0
- truthound_dashboard/core/notifications/throttling/__init__.py +83 -0
- truthound_dashboard/core/notifications/throttling/builder.py +311 -0
- truthound_dashboard/core/notifications/throttling/stores.py +1859 -0
- truthound_dashboard/core/notifications/throttling/throttlers.py +633 -0
- truthound_dashboard/core/openlineage.py +1028 -0
- truthound_dashboard/core/plugins/__init__.py +39 -0
- truthound_dashboard/core/plugins/docs/__init__.py +39 -0
- truthound_dashboard/core/plugins/docs/extractor.py +703 -0
- truthound_dashboard/core/plugins/docs/renderers.py +804 -0
- truthound_dashboard/core/plugins/hooks/__init__.py +63 -0
- truthound_dashboard/core/plugins/hooks/decorators.py +367 -0
- truthound_dashboard/core/plugins/hooks/manager.py +403 -0
- truthound_dashboard/core/plugins/hooks/protocols.py +265 -0
- truthound_dashboard/core/plugins/lifecycle/__init__.py +41 -0
- truthound_dashboard/core/plugins/lifecycle/hot_reload.py +584 -0
- truthound_dashboard/core/plugins/lifecycle/machine.py +419 -0
- truthound_dashboard/core/plugins/lifecycle/states.py +266 -0
- truthound_dashboard/core/plugins/loader.py +504 -0
- truthound_dashboard/core/plugins/registry.py +810 -0
- truthound_dashboard/core/plugins/reporter_executor.py +588 -0
- truthound_dashboard/core/plugins/sandbox/__init__.py +59 -0
- truthound_dashboard/core/plugins/sandbox/code_validator.py +243 -0
- truthound_dashboard/core/plugins/sandbox/engines.py +770 -0
- truthound_dashboard/core/plugins/sandbox/protocols.py +194 -0
- truthound_dashboard/core/plugins/sandbox.py +617 -0
- truthound_dashboard/core/plugins/security/__init__.py +68 -0
- truthound_dashboard/core/plugins/security/analyzer.py +535 -0
- truthound_dashboard/core/plugins/security/policies.py +311 -0
- truthound_dashboard/core/plugins/security/protocols.py +296 -0
- truthound_dashboard/core/plugins/security/signing.py +842 -0
- truthound_dashboard/core/plugins/security.py +446 -0
- truthound_dashboard/core/plugins/validator_executor.py +401 -0
- truthound_dashboard/core/plugins/versioning/__init__.py +51 -0
- truthound_dashboard/core/plugins/versioning/constraints.py +377 -0
- truthound_dashboard/core/plugins/versioning/dependencies.py +541 -0
- truthound_dashboard/core/plugins/versioning/semver.py +266 -0
- truthound_dashboard/core/profile_comparison.py +601 -0
- truthound_dashboard/core/report_history.py +570 -0
- truthound_dashboard/core/reporters/__init__.py +57 -0
- truthound_dashboard/core/reporters/base.py +296 -0
- truthound_dashboard/core/reporters/csv_reporter.py +155 -0
- truthound_dashboard/core/reporters/html_reporter.py +598 -0
- truthound_dashboard/core/reporters/i18n/__init__.py +65 -0
- truthound_dashboard/core/reporters/i18n/base.py +494 -0
- truthound_dashboard/core/reporters/i18n/catalogs.py +930 -0
- truthound_dashboard/core/reporters/json_reporter.py +160 -0
- truthound_dashboard/core/reporters/junit_reporter.py +233 -0
- truthound_dashboard/core/reporters/markdown_reporter.py +207 -0
- truthound_dashboard/core/reporters/pdf_reporter.py +209 -0
- truthound_dashboard/core/reporters/registry.py +272 -0
- truthound_dashboard/core/rule_generator.py +2088 -0
- truthound_dashboard/core/scheduler.py +822 -12
- truthound_dashboard/core/schema_evolution.py +858 -0
- truthound_dashboard/core/services.py +152 -9
- truthound_dashboard/core/statistics.py +718 -0
- truthound_dashboard/core/streaming_anomaly.py +883 -0
- truthound_dashboard/core/triggers/__init__.py +45 -0
- truthound_dashboard/core/triggers/base.py +226 -0
- truthound_dashboard/core/triggers/evaluators.py +609 -0
- truthound_dashboard/core/triggers/factory.py +363 -0
- truthound_dashboard/core/unified_alerts.py +870 -0
- truthound_dashboard/core/validation_limits.py +509 -0
- truthound_dashboard/core/versioning.py +709 -0
- truthound_dashboard/core/websocket/__init__.py +59 -0
- truthound_dashboard/core/websocket/manager.py +512 -0
- truthound_dashboard/core/websocket/messages.py +130 -0
- truthound_dashboard/db/__init__.py +30 -0
- truthound_dashboard/db/models.py +3375 -3
- truthound_dashboard/main.py +22 -0
- truthound_dashboard/schemas/__init__.py +396 -1
- truthound_dashboard/schemas/anomaly.py +1258 -0
- truthound_dashboard/schemas/base.py +4 -0
- truthound_dashboard/schemas/cross_alerts.py +334 -0
- truthound_dashboard/schemas/drift_monitor.py +890 -0
- truthound_dashboard/schemas/lineage.py +428 -0
- truthound_dashboard/schemas/maintenance.py +154 -0
- truthound_dashboard/schemas/model_monitoring.py +374 -0
- truthound_dashboard/schemas/notifications_advanced.py +1363 -0
- truthound_dashboard/schemas/openlineage.py +704 -0
- truthound_dashboard/schemas/plugins.py +1293 -0
- truthound_dashboard/schemas/profile.py +420 -34
- truthound_dashboard/schemas/profile_comparison.py +242 -0
- truthound_dashboard/schemas/reports.py +285 -0
- truthound_dashboard/schemas/rule_suggestion.py +434 -0
- truthound_dashboard/schemas/schema_evolution.py +164 -0
- truthound_dashboard/schemas/source.py +117 -2
- truthound_dashboard/schemas/triggers.py +511 -0
- truthound_dashboard/schemas/unified_alerts.py +223 -0
- truthound_dashboard/schemas/validation.py +25 -1
- truthound_dashboard/schemas/validators/__init__.py +11 -0
- truthound_dashboard/schemas/validators/base.py +151 -0
- truthound_dashboard/schemas/versioning.py +152 -0
- truthound_dashboard/static/index.html +2 -2
- {truthound_dashboard-1.3.1.dist-info → truthound_dashboard-1.4.1.dist-info}/METADATA +147 -23
- truthound_dashboard-1.4.1.dist-info/RECORD +239 -0
- truthound_dashboard/static/assets/index-BZG20KuF.js +0 -586
- truthound_dashboard/static/assets/index-D_HyZ3pb.css +0 -1
- truthound_dashboard/static/assets/unmerged_dictionaries-CtpqQBm0.js +0 -1
- truthound_dashboard-1.3.1.dist-info/RECORD +0 -110
- {truthound_dashboard-1.3.1.dist-info → truthound_dashboard-1.4.1.dist-info}/WHEEL +0 -0
- {truthound_dashboard-1.3.1.dist-info → truthound_dashboard-1.4.1.dist-info}/entry_points.txt +0 -0
- {truthound_dashboard-1.3.1.dist-info → truthound_dashboard-1.4.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,703 @@
|
|
|
1
|
+
"""Documentation Extractor.
|
|
2
|
+
|
|
3
|
+
This module provides AST-based documentation extraction
|
|
4
|
+
for Python source code.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import ast
|
|
10
|
+
import inspect
|
|
11
|
+
import re
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class ParameterDoc:
|
|
19
|
+
"""Documentation for a function parameter.
|
|
20
|
+
|
|
21
|
+
Attributes:
|
|
22
|
+
name: Parameter name.
|
|
23
|
+
type_hint: Type annotation.
|
|
24
|
+
default: Default value.
|
|
25
|
+
description: Parameter description from docstring.
|
|
26
|
+
required: Whether parameter is required.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
name: str
|
|
30
|
+
type_hint: str = ""
|
|
31
|
+
default: str | None = None
|
|
32
|
+
description: str = ""
|
|
33
|
+
required: bool = True
|
|
34
|
+
|
|
35
|
+
def to_dict(self) -> dict[str, Any]:
|
|
36
|
+
"""Convert to dictionary."""
|
|
37
|
+
return {
|
|
38
|
+
"name": self.name,
|
|
39
|
+
"type_hint": self.type_hint,
|
|
40
|
+
"default": self.default,
|
|
41
|
+
"description": self.description,
|
|
42
|
+
"required": self.required,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class FunctionDoc:
|
|
48
|
+
"""Documentation for a function or method.
|
|
49
|
+
|
|
50
|
+
Attributes:
|
|
51
|
+
name: Function name.
|
|
52
|
+
signature: Full function signature.
|
|
53
|
+
docstring: Docstring content.
|
|
54
|
+
description: Short description (first line).
|
|
55
|
+
long_description: Full description.
|
|
56
|
+
parameters: List of parameters.
|
|
57
|
+
returns: Return type and description.
|
|
58
|
+
raises: Exceptions that may be raised.
|
|
59
|
+
examples: Usage examples.
|
|
60
|
+
decorators: Applied decorators.
|
|
61
|
+
is_async: Whether function is async.
|
|
62
|
+
is_classmethod: Whether it's a classmethod.
|
|
63
|
+
is_staticmethod: Whether it's a staticmethod.
|
|
64
|
+
line_number: Line number in source.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
name: str
|
|
68
|
+
signature: str = ""
|
|
69
|
+
docstring: str = ""
|
|
70
|
+
description: str = ""
|
|
71
|
+
long_description: str = ""
|
|
72
|
+
parameters: list[ParameterDoc] = field(default_factory=list)
|
|
73
|
+
returns: dict[str, str] = field(default_factory=dict)
|
|
74
|
+
raises: list[dict[str, str]] = field(default_factory=list)
|
|
75
|
+
examples: list[str] = field(default_factory=list)
|
|
76
|
+
decorators: list[str] = field(default_factory=list)
|
|
77
|
+
is_async: bool = False
|
|
78
|
+
is_classmethod: bool = False
|
|
79
|
+
is_staticmethod: bool = False
|
|
80
|
+
line_number: int = 0
|
|
81
|
+
|
|
82
|
+
def to_dict(self) -> dict[str, Any]:
|
|
83
|
+
"""Convert to dictionary."""
|
|
84
|
+
return {
|
|
85
|
+
"name": self.name,
|
|
86
|
+
"signature": self.signature,
|
|
87
|
+
"docstring": self.docstring,
|
|
88
|
+
"description": self.description,
|
|
89
|
+
"long_description": self.long_description,
|
|
90
|
+
"parameters": [p.to_dict() for p in self.parameters],
|
|
91
|
+
"returns": self.returns,
|
|
92
|
+
"raises": self.raises,
|
|
93
|
+
"examples": self.examples,
|
|
94
|
+
"decorators": self.decorators,
|
|
95
|
+
"is_async": self.is_async,
|
|
96
|
+
"is_classmethod": self.is_classmethod,
|
|
97
|
+
"is_staticmethod": self.is_staticmethod,
|
|
98
|
+
"line_number": self.line_number,
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@dataclass
|
|
103
|
+
class ClassDoc:
|
|
104
|
+
"""Documentation for a class.
|
|
105
|
+
|
|
106
|
+
Attributes:
|
|
107
|
+
name: Class name.
|
|
108
|
+
docstring: Class docstring.
|
|
109
|
+
description: Short description.
|
|
110
|
+
long_description: Full description.
|
|
111
|
+
bases: Base classes.
|
|
112
|
+
methods: List of method documentation.
|
|
113
|
+
attributes: Class attributes.
|
|
114
|
+
class_attributes: Class-level attributes.
|
|
115
|
+
decorators: Applied decorators.
|
|
116
|
+
is_dataclass: Whether it's a dataclass.
|
|
117
|
+
line_number: Line number in source.
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
name: str
|
|
121
|
+
docstring: str = ""
|
|
122
|
+
description: str = ""
|
|
123
|
+
long_description: str = ""
|
|
124
|
+
bases: list[str] = field(default_factory=list)
|
|
125
|
+
methods: list[FunctionDoc] = field(default_factory=list)
|
|
126
|
+
attributes: list[dict[str, Any]] = field(default_factory=list)
|
|
127
|
+
class_attributes: list[dict[str, Any]] = field(default_factory=list)
|
|
128
|
+
decorators: list[str] = field(default_factory=list)
|
|
129
|
+
is_dataclass: bool = False
|
|
130
|
+
line_number: int = 0
|
|
131
|
+
|
|
132
|
+
def to_dict(self) -> dict[str, Any]:
|
|
133
|
+
"""Convert to dictionary."""
|
|
134
|
+
return {
|
|
135
|
+
"name": self.name,
|
|
136
|
+
"docstring": self.docstring,
|
|
137
|
+
"description": self.description,
|
|
138
|
+
"long_description": self.long_description,
|
|
139
|
+
"bases": self.bases,
|
|
140
|
+
"methods": [m.to_dict() for m in self.methods],
|
|
141
|
+
"attributes": self.attributes,
|
|
142
|
+
"class_attributes": self.class_attributes,
|
|
143
|
+
"decorators": self.decorators,
|
|
144
|
+
"is_dataclass": self.is_dataclass,
|
|
145
|
+
"line_number": self.line_number,
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@dataclass
|
|
150
|
+
class ModuleDoc:
|
|
151
|
+
"""Documentation for a module.
|
|
152
|
+
|
|
153
|
+
Attributes:
|
|
154
|
+
name: Module name.
|
|
155
|
+
path: File path.
|
|
156
|
+
docstring: Module docstring.
|
|
157
|
+
description: Short description.
|
|
158
|
+
long_description: Full description.
|
|
159
|
+
classes: List of class documentation.
|
|
160
|
+
functions: List of function documentation.
|
|
161
|
+
constants: Module-level constants.
|
|
162
|
+
imports: Import statements.
|
|
163
|
+
"""
|
|
164
|
+
|
|
165
|
+
name: str
|
|
166
|
+
path: str = ""
|
|
167
|
+
docstring: str = ""
|
|
168
|
+
description: str = ""
|
|
169
|
+
long_description: str = ""
|
|
170
|
+
classes: list[ClassDoc] = field(default_factory=list)
|
|
171
|
+
functions: list[FunctionDoc] = field(default_factory=list)
|
|
172
|
+
constants: list[dict[str, Any]] = field(default_factory=list)
|
|
173
|
+
imports: list[str] = field(default_factory=list)
|
|
174
|
+
|
|
175
|
+
def to_dict(self) -> dict[str, Any]:
|
|
176
|
+
"""Convert to dictionary."""
|
|
177
|
+
return {
|
|
178
|
+
"name": self.name,
|
|
179
|
+
"path": self.path,
|
|
180
|
+
"docstring": self.docstring,
|
|
181
|
+
"description": self.description,
|
|
182
|
+
"long_description": self.long_description,
|
|
183
|
+
"classes": [c.to_dict() for c in self.classes],
|
|
184
|
+
"functions": [f.to_dict() for f in self.functions],
|
|
185
|
+
"constants": self.constants,
|
|
186
|
+
"imports": self.imports,
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
class DocstringParser:
|
|
191
|
+
"""Parses docstrings in various formats (Google, NumPy, reStructuredText)."""
|
|
192
|
+
|
|
193
|
+
# Patterns for Google-style docstrings
|
|
194
|
+
GOOGLE_SECTIONS = {
|
|
195
|
+
"args": r"Args?:\s*\n",
|
|
196
|
+
"arguments": r"Arguments?:\s*\n",
|
|
197
|
+
"parameters": r"Parameters?:\s*\n",
|
|
198
|
+
"returns": r"Returns?:\s*\n",
|
|
199
|
+
"yields": r"Yields?:\s*\n",
|
|
200
|
+
"raises": r"Raises?:\s*\n",
|
|
201
|
+
"examples": r"Examples?:\s*\n",
|
|
202
|
+
"attributes": r"Attributes?:\s*\n",
|
|
203
|
+
"note": r"Note:\s*\n",
|
|
204
|
+
"notes": r"Notes?:\s*\n",
|
|
205
|
+
"warning": r"Warning:\s*\n",
|
|
206
|
+
"see also": r"See Also:\s*\n",
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
@classmethod
|
|
210
|
+
def parse(cls, docstring: str) -> dict[str, Any]:
|
|
211
|
+
"""Parse a docstring.
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
docstring: Docstring to parse.
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
Parsed docstring sections.
|
|
218
|
+
"""
|
|
219
|
+
if not docstring:
|
|
220
|
+
return {}
|
|
221
|
+
|
|
222
|
+
result: dict[str, Any] = {
|
|
223
|
+
"description": "",
|
|
224
|
+
"long_description": "",
|
|
225
|
+
"params": [],
|
|
226
|
+
"returns": {},
|
|
227
|
+
"raises": [],
|
|
228
|
+
"examples": [],
|
|
229
|
+
"attributes": [],
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
lines = docstring.strip().split("\n")
|
|
233
|
+
if not lines:
|
|
234
|
+
return result
|
|
235
|
+
|
|
236
|
+
# First non-empty line is short description
|
|
237
|
+
for i, line in enumerate(lines):
|
|
238
|
+
line = line.strip()
|
|
239
|
+
if line:
|
|
240
|
+
result["description"] = line
|
|
241
|
+
lines = lines[i + 1:]
|
|
242
|
+
break
|
|
243
|
+
|
|
244
|
+
# Parse remaining content
|
|
245
|
+
content = "\n".join(lines)
|
|
246
|
+
|
|
247
|
+
# Extract sections
|
|
248
|
+
sections = cls._split_sections(content)
|
|
249
|
+
|
|
250
|
+
if "description" in sections:
|
|
251
|
+
result["long_description"] = sections["description"].strip()
|
|
252
|
+
|
|
253
|
+
if "args" in sections or "arguments" in sections or "parameters" in sections:
|
|
254
|
+
section = sections.get("args") or sections.get("arguments") or sections.get("parameters", "")
|
|
255
|
+
result["params"] = cls._parse_params(section)
|
|
256
|
+
|
|
257
|
+
if "returns" in sections:
|
|
258
|
+
result["returns"] = cls._parse_returns(sections["returns"])
|
|
259
|
+
|
|
260
|
+
if "raises" in sections:
|
|
261
|
+
result["raises"] = cls._parse_raises(sections["raises"])
|
|
262
|
+
|
|
263
|
+
if "examples" in sections:
|
|
264
|
+
result["examples"] = cls._parse_examples(sections["examples"])
|
|
265
|
+
|
|
266
|
+
if "attributes" in sections:
|
|
267
|
+
result["attributes"] = cls._parse_params(sections["attributes"])
|
|
268
|
+
|
|
269
|
+
return result
|
|
270
|
+
|
|
271
|
+
@classmethod
|
|
272
|
+
def _split_sections(cls, content: str) -> dict[str, str]:
|
|
273
|
+
"""Split docstring into sections."""
|
|
274
|
+
sections: dict[str, str] = {"description": ""}
|
|
275
|
+
|
|
276
|
+
# Find all section headers
|
|
277
|
+
section_pattern = r"^(Args?|Arguments?|Parameters?|Returns?|Yields?|Raises?|Examples?|Attributes?|Notes?|Warning|See Also):\s*$"
|
|
278
|
+
current_section = "description"
|
|
279
|
+
current_content: list[str] = []
|
|
280
|
+
|
|
281
|
+
for line in content.split("\n"):
|
|
282
|
+
match = re.match(section_pattern, line.strip(), re.IGNORECASE)
|
|
283
|
+
if match:
|
|
284
|
+
# Save previous section
|
|
285
|
+
sections[current_section] = "\n".join(current_content)
|
|
286
|
+
# Start new section
|
|
287
|
+
current_section = match.group(1).lower()
|
|
288
|
+
if current_section.endswith("s") and current_section not in ["notes", "yields", "raises"]:
|
|
289
|
+
current_section = current_section[:-1]
|
|
290
|
+
current_content = []
|
|
291
|
+
else:
|
|
292
|
+
current_content.append(line)
|
|
293
|
+
|
|
294
|
+
# Save last section
|
|
295
|
+
sections[current_section] = "\n".join(current_content)
|
|
296
|
+
|
|
297
|
+
return sections
|
|
298
|
+
|
|
299
|
+
@classmethod
|
|
300
|
+
def _parse_params(cls, section: str) -> list[dict[str, str]]:
|
|
301
|
+
"""Parse parameter section."""
|
|
302
|
+
params = []
|
|
303
|
+
current_param: dict[str, str] | None = None
|
|
304
|
+
|
|
305
|
+
for line in section.split("\n"):
|
|
306
|
+
# Match parameter line: "name (type): description" or "name: description"
|
|
307
|
+
match = re.match(r"^\s*(\w+)(?:\s*\(([^)]+)\))?:\s*(.*)$", line)
|
|
308
|
+
if match:
|
|
309
|
+
if current_param:
|
|
310
|
+
params.append(current_param)
|
|
311
|
+
current_param = {
|
|
312
|
+
"name": match.group(1),
|
|
313
|
+
"type": match.group(2) or "",
|
|
314
|
+
"description": match.group(3),
|
|
315
|
+
}
|
|
316
|
+
elif current_param and line.strip():
|
|
317
|
+
# Continuation of description
|
|
318
|
+
current_param["description"] += " " + line.strip()
|
|
319
|
+
|
|
320
|
+
if current_param:
|
|
321
|
+
params.append(current_param)
|
|
322
|
+
|
|
323
|
+
return params
|
|
324
|
+
|
|
325
|
+
@classmethod
|
|
326
|
+
def _parse_returns(cls, section: str) -> dict[str, str]:
|
|
327
|
+
"""Parse returns section."""
|
|
328
|
+
lines = [l.strip() for l in section.split("\n") if l.strip()]
|
|
329
|
+
if not lines:
|
|
330
|
+
return {}
|
|
331
|
+
|
|
332
|
+
# Try to parse "type: description" format
|
|
333
|
+
match = re.match(r"^([^:]+):\s*(.*)$", lines[0])
|
|
334
|
+
if match:
|
|
335
|
+
return {
|
|
336
|
+
"type": match.group(1).strip(),
|
|
337
|
+
"description": match.group(2) + " ".join(lines[1:]),
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return {"description": " ".join(lines)}
|
|
341
|
+
|
|
342
|
+
@classmethod
|
|
343
|
+
def _parse_raises(cls, section: str) -> list[dict[str, str]]:
|
|
344
|
+
"""Parse raises section."""
|
|
345
|
+
raises = []
|
|
346
|
+
current: dict[str, str] | None = None
|
|
347
|
+
|
|
348
|
+
for line in section.split("\n"):
|
|
349
|
+
match = re.match(r"^\s*(\w+):\s*(.*)$", line)
|
|
350
|
+
if match:
|
|
351
|
+
if current:
|
|
352
|
+
raises.append(current)
|
|
353
|
+
current = {
|
|
354
|
+
"exception": match.group(1),
|
|
355
|
+
"description": match.group(2),
|
|
356
|
+
}
|
|
357
|
+
elif current and line.strip():
|
|
358
|
+
current["description"] += " " + line.strip()
|
|
359
|
+
|
|
360
|
+
if current:
|
|
361
|
+
raises.append(current)
|
|
362
|
+
|
|
363
|
+
return raises
|
|
364
|
+
|
|
365
|
+
@classmethod
|
|
366
|
+
def _parse_examples(cls, section: str) -> list[str]:
|
|
367
|
+
"""Parse examples section."""
|
|
368
|
+
examples = []
|
|
369
|
+
current: list[str] = []
|
|
370
|
+
in_code_block = False
|
|
371
|
+
|
|
372
|
+
for line in section.split("\n"):
|
|
373
|
+
if line.strip().startswith(">>>") or in_code_block:
|
|
374
|
+
in_code_block = True
|
|
375
|
+
current.append(line)
|
|
376
|
+
if not line.strip() and current:
|
|
377
|
+
examples.append("\n".join(current))
|
|
378
|
+
current = []
|
|
379
|
+
in_code_block = False
|
|
380
|
+
elif current:
|
|
381
|
+
examples.append("\n".join(current))
|
|
382
|
+
current = []
|
|
383
|
+
in_code_block = False
|
|
384
|
+
|
|
385
|
+
if current:
|
|
386
|
+
examples.append("\n".join(current))
|
|
387
|
+
|
|
388
|
+
return examples
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
class DocumentationExtractor:
|
|
392
|
+
"""Extracts documentation from Python source code using AST."""
|
|
393
|
+
|
|
394
|
+
def __init__(self, include_private: bool = False) -> None:
|
|
395
|
+
"""Initialize the extractor.
|
|
396
|
+
|
|
397
|
+
Args:
|
|
398
|
+
include_private: Whether to include private members.
|
|
399
|
+
"""
|
|
400
|
+
self.include_private = include_private
|
|
401
|
+
|
|
402
|
+
def extract_from_source(self, source: str, module_name: str = "") -> ModuleDoc:
|
|
403
|
+
"""Extract documentation from source code.
|
|
404
|
+
|
|
405
|
+
Args:
|
|
406
|
+
source: Python source code.
|
|
407
|
+
module_name: Module name.
|
|
408
|
+
|
|
409
|
+
Returns:
|
|
410
|
+
ModuleDoc with extracted documentation.
|
|
411
|
+
"""
|
|
412
|
+
try:
|
|
413
|
+
tree = ast.parse(source)
|
|
414
|
+
except SyntaxError:
|
|
415
|
+
return ModuleDoc(name=module_name)
|
|
416
|
+
|
|
417
|
+
return self._extract_module(tree, module_name)
|
|
418
|
+
|
|
419
|
+
def extract_from_file(self, path: str | Path) -> ModuleDoc:
|
|
420
|
+
"""Extract documentation from a file.
|
|
421
|
+
|
|
422
|
+
Args:
|
|
423
|
+
path: Path to Python file.
|
|
424
|
+
|
|
425
|
+
Returns:
|
|
426
|
+
ModuleDoc with extracted documentation.
|
|
427
|
+
"""
|
|
428
|
+
path = Path(path)
|
|
429
|
+
source = path.read_text(encoding="utf-8")
|
|
430
|
+
module_name = path.stem
|
|
431
|
+
doc = self.extract_from_source(source, module_name)
|
|
432
|
+
doc.path = str(path)
|
|
433
|
+
return doc
|
|
434
|
+
|
|
435
|
+
def extract_from_path(self, path: str | Path) -> list[ModuleDoc]:
|
|
436
|
+
"""Extract documentation from all Python files in a path.
|
|
437
|
+
|
|
438
|
+
Args:
|
|
439
|
+
path: Directory or file path.
|
|
440
|
+
|
|
441
|
+
Returns:
|
|
442
|
+
List of ModuleDoc for each file.
|
|
443
|
+
"""
|
|
444
|
+
path = Path(path)
|
|
445
|
+
docs: list[ModuleDoc] = []
|
|
446
|
+
|
|
447
|
+
if path.is_file():
|
|
448
|
+
docs.append(self.extract_from_file(path))
|
|
449
|
+
elif path.is_dir():
|
|
450
|
+
for py_file in path.rglob("*.py"):
|
|
451
|
+
if py_file.name.startswith("_") and not self.include_private:
|
|
452
|
+
continue
|
|
453
|
+
docs.append(self.extract_from_file(py_file))
|
|
454
|
+
|
|
455
|
+
return docs
|
|
456
|
+
|
|
457
|
+
def _extract_module(self, tree: ast.Module, name: str) -> ModuleDoc:
|
|
458
|
+
"""Extract module documentation from AST."""
|
|
459
|
+
doc = ModuleDoc(name=name)
|
|
460
|
+
|
|
461
|
+
# Get module docstring
|
|
462
|
+
if (
|
|
463
|
+
tree.body
|
|
464
|
+
and isinstance(tree.body[0], ast.Expr)
|
|
465
|
+
and isinstance(tree.body[0].value, ast.Constant)
|
|
466
|
+
and isinstance(tree.body[0].value.value, str)
|
|
467
|
+
):
|
|
468
|
+
doc.docstring = tree.body[0].value.value
|
|
469
|
+
parsed = DocstringParser.parse(doc.docstring)
|
|
470
|
+
doc.description = parsed.get("description", "")
|
|
471
|
+
doc.long_description = parsed.get("long_description", "")
|
|
472
|
+
|
|
473
|
+
# Extract imports
|
|
474
|
+
for node in ast.walk(tree):
|
|
475
|
+
if isinstance(node, ast.Import):
|
|
476
|
+
for alias in node.names:
|
|
477
|
+
doc.imports.append(alias.name)
|
|
478
|
+
elif isinstance(node, ast.ImportFrom):
|
|
479
|
+
if node.module:
|
|
480
|
+
doc.imports.append(f"from {node.module}")
|
|
481
|
+
|
|
482
|
+
# Extract classes and functions at module level
|
|
483
|
+
for node in tree.body:
|
|
484
|
+
if isinstance(node, ast.ClassDef):
|
|
485
|
+
if self._should_include(node.name):
|
|
486
|
+
doc.classes.append(self._extract_class(node))
|
|
487
|
+
elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
488
|
+
if self._should_include(node.name):
|
|
489
|
+
doc.functions.append(self._extract_function(node))
|
|
490
|
+
elif isinstance(node, ast.Assign):
|
|
491
|
+
# Module-level constants
|
|
492
|
+
for target in node.targets:
|
|
493
|
+
if isinstance(target, ast.Name) and target.id.isupper():
|
|
494
|
+
doc.constants.append({
|
|
495
|
+
"name": target.id,
|
|
496
|
+
"value": ast.unparse(node.value) if hasattr(ast, "unparse") else "",
|
|
497
|
+
})
|
|
498
|
+
|
|
499
|
+
return doc
|
|
500
|
+
|
|
501
|
+
def _extract_class(self, node: ast.ClassDef) -> ClassDoc:
|
|
502
|
+
"""Extract class documentation from AST."""
|
|
503
|
+
doc = ClassDoc(
|
|
504
|
+
name=node.name,
|
|
505
|
+
line_number=node.lineno,
|
|
506
|
+
)
|
|
507
|
+
|
|
508
|
+
# Get base classes
|
|
509
|
+
for base in node.bases:
|
|
510
|
+
if isinstance(base, ast.Name):
|
|
511
|
+
doc.bases.append(base.id)
|
|
512
|
+
elif isinstance(base, ast.Attribute):
|
|
513
|
+
doc.bases.append(f"{ast.unparse(base)}" if hasattr(ast, "unparse") else base.attr)
|
|
514
|
+
|
|
515
|
+
# Get decorators
|
|
516
|
+
for decorator in node.decorator_list:
|
|
517
|
+
if isinstance(decorator, ast.Name):
|
|
518
|
+
doc.decorators.append(decorator.id)
|
|
519
|
+
if decorator.id == "dataclass":
|
|
520
|
+
doc.is_dataclass = True
|
|
521
|
+
elif isinstance(decorator, ast.Call) and isinstance(decorator.func, ast.Name):
|
|
522
|
+
doc.decorators.append(decorator.func.id)
|
|
523
|
+
if decorator.func.id == "dataclass":
|
|
524
|
+
doc.is_dataclass = True
|
|
525
|
+
|
|
526
|
+
# Get docstring
|
|
527
|
+
if (
|
|
528
|
+
node.body
|
|
529
|
+
and isinstance(node.body[0], ast.Expr)
|
|
530
|
+
and isinstance(node.body[0].value, ast.Constant)
|
|
531
|
+
and isinstance(node.body[0].value.value, str)
|
|
532
|
+
):
|
|
533
|
+
doc.docstring = node.body[0].value.value
|
|
534
|
+
parsed = DocstringParser.parse(doc.docstring)
|
|
535
|
+
doc.description = parsed.get("description", "")
|
|
536
|
+
doc.long_description = parsed.get("long_description", "")
|
|
537
|
+
doc.attributes = parsed.get("attributes", [])
|
|
538
|
+
|
|
539
|
+
# Extract methods
|
|
540
|
+
for item in node.body:
|
|
541
|
+
if isinstance(item, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
542
|
+
if self._should_include(item.name):
|
|
543
|
+
doc.methods.append(self._extract_function(item))
|
|
544
|
+
elif isinstance(item, ast.AnnAssign) and isinstance(item.target, ast.Name):
|
|
545
|
+
# Class attribute with type annotation
|
|
546
|
+
doc.class_attributes.append({
|
|
547
|
+
"name": item.target.id,
|
|
548
|
+
"type": ast.unparse(item.annotation) if hasattr(ast, "unparse") else "",
|
|
549
|
+
"value": ast.unparse(item.value) if item.value and hasattr(ast, "unparse") else None,
|
|
550
|
+
})
|
|
551
|
+
|
|
552
|
+
return doc
|
|
553
|
+
|
|
554
|
+
def _extract_function(self, node: ast.FunctionDef | ast.AsyncFunctionDef) -> FunctionDoc:
|
|
555
|
+
"""Extract function documentation from AST."""
|
|
556
|
+
doc = FunctionDoc(
|
|
557
|
+
name=node.name,
|
|
558
|
+
is_async=isinstance(node, ast.AsyncFunctionDef),
|
|
559
|
+
line_number=node.lineno,
|
|
560
|
+
)
|
|
561
|
+
|
|
562
|
+
# Get decorators
|
|
563
|
+
for decorator in node.decorator_list:
|
|
564
|
+
if isinstance(decorator, ast.Name):
|
|
565
|
+
doc.decorators.append(decorator.id)
|
|
566
|
+
if decorator.id == "classmethod":
|
|
567
|
+
doc.is_classmethod = True
|
|
568
|
+
elif decorator.id == "staticmethod":
|
|
569
|
+
doc.is_staticmethod = True
|
|
570
|
+
elif isinstance(decorator, ast.Attribute):
|
|
571
|
+
doc.decorators.append(decorator.attr)
|
|
572
|
+
|
|
573
|
+
# Build signature
|
|
574
|
+
doc.signature = self._build_signature(node)
|
|
575
|
+
|
|
576
|
+
# Get docstring
|
|
577
|
+
if (
|
|
578
|
+
node.body
|
|
579
|
+
and isinstance(node.body[0], ast.Expr)
|
|
580
|
+
and isinstance(node.body[0].value, ast.Constant)
|
|
581
|
+
and isinstance(node.body[0].value.value, str)
|
|
582
|
+
):
|
|
583
|
+
doc.docstring = node.body[0].value.value
|
|
584
|
+
parsed = DocstringParser.parse(doc.docstring)
|
|
585
|
+
doc.description = parsed.get("description", "")
|
|
586
|
+
doc.long_description = parsed.get("long_description", "")
|
|
587
|
+
doc.returns = parsed.get("returns", {})
|
|
588
|
+
doc.raises = parsed.get("raises", [])
|
|
589
|
+
doc.examples = parsed.get("examples", [])
|
|
590
|
+
|
|
591
|
+
# Match docstring params to function params
|
|
592
|
+
docstring_params = {p["name"]: p for p in parsed.get("params", [])}
|
|
593
|
+
doc.parameters = self._extract_parameters(node.args, docstring_params)
|
|
594
|
+
else:
|
|
595
|
+
doc.parameters = self._extract_parameters(node.args, {})
|
|
596
|
+
|
|
597
|
+
return doc
|
|
598
|
+
|
|
599
|
+
def _extract_parameters(
|
|
600
|
+
self,
|
|
601
|
+
args: ast.arguments,
|
|
602
|
+
docstring_params: dict[str, dict[str, str]],
|
|
603
|
+
) -> list[ParameterDoc]:
|
|
604
|
+
"""Extract function parameters from AST."""
|
|
605
|
+
params: list[ParameterDoc] = []
|
|
606
|
+
|
|
607
|
+
# Calculate defaults offset
|
|
608
|
+
num_defaults = len(args.defaults)
|
|
609
|
+
num_args = len(args.args)
|
|
610
|
+
defaults_offset = num_args - num_defaults
|
|
611
|
+
|
|
612
|
+
for i, arg in enumerate(args.args):
|
|
613
|
+
if arg.arg == "self" or arg.arg == "cls":
|
|
614
|
+
continue
|
|
615
|
+
|
|
616
|
+
param = ParameterDoc(name=arg.arg)
|
|
617
|
+
|
|
618
|
+
# Type hint
|
|
619
|
+
if arg.annotation:
|
|
620
|
+
param.type_hint = ast.unparse(arg.annotation) if hasattr(ast, "unparse") else ""
|
|
621
|
+
|
|
622
|
+
# Default value
|
|
623
|
+
default_index = i - defaults_offset
|
|
624
|
+
if default_index >= 0:
|
|
625
|
+
param.default = ast.unparse(args.defaults[default_index]) if hasattr(ast, "unparse") else "..."
|
|
626
|
+
param.required = False
|
|
627
|
+
|
|
628
|
+
# Description from docstring
|
|
629
|
+
if arg.arg in docstring_params:
|
|
630
|
+
param.description = docstring_params[arg.arg].get("description", "")
|
|
631
|
+
if not param.type_hint:
|
|
632
|
+
param.type_hint = docstring_params[arg.arg].get("type", "")
|
|
633
|
+
|
|
634
|
+
params.append(param)
|
|
635
|
+
|
|
636
|
+
# Handle *args and **kwargs
|
|
637
|
+
if args.vararg:
|
|
638
|
+
params.append(ParameterDoc(
|
|
639
|
+
name=f"*{args.vararg.arg}",
|
|
640
|
+
type_hint=ast.unparse(args.vararg.annotation) if args.vararg.annotation and hasattr(ast, "unparse") else "",
|
|
641
|
+
required=False,
|
|
642
|
+
))
|
|
643
|
+
|
|
644
|
+
if args.kwarg:
|
|
645
|
+
params.append(ParameterDoc(
|
|
646
|
+
name=f"**{args.kwarg.arg}",
|
|
647
|
+
type_hint=ast.unparse(args.kwarg.annotation) if args.kwarg.annotation and hasattr(ast, "unparse") else "",
|
|
648
|
+
required=False,
|
|
649
|
+
))
|
|
650
|
+
|
|
651
|
+
return params
|
|
652
|
+
|
|
653
|
+
def _build_signature(self, node: ast.FunctionDef | ast.AsyncFunctionDef) -> str:
|
|
654
|
+
"""Build function signature string."""
|
|
655
|
+
parts = []
|
|
656
|
+
|
|
657
|
+
if isinstance(node, ast.AsyncFunctionDef):
|
|
658
|
+
parts.append("async ")
|
|
659
|
+
|
|
660
|
+
parts.append(f"def {node.name}(")
|
|
661
|
+
|
|
662
|
+
# Parameters
|
|
663
|
+
param_strs = []
|
|
664
|
+
args = node.args
|
|
665
|
+
|
|
666
|
+
for i, arg in enumerate(args.args):
|
|
667
|
+
param = arg.arg
|
|
668
|
+
if arg.annotation:
|
|
669
|
+
param += f": {ast.unparse(arg.annotation)}" if hasattr(ast, "unparse") else ""
|
|
670
|
+
|
|
671
|
+
# Default
|
|
672
|
+
default_index = i - (len(args.args) - len(args.defaults))
|
|
673
|
+
if default_index >= 0:
|
|
674
|
+
param += f" = {ast.unparse(args.defaults[default_index])}" if hasattr(ast, "unparse") else " = ..."
|
|
675
|
+
|
|
676
|
+
param_strs.append(param)
|
|
677
|
+
|
|
678
|
+
if args.vararg:
|
|
679
|
+
param = f"*{args.vararg.arg}"
|
|
680
|
+
if args.vararg.annotation:
|
|
681
|
+
param += f": {ast.unparse(args.vararg.annotation)}" if hasattr(ast, "unparse") else ""
|
|
682
|
+
param_strs.append(param)
|
|
683
|
+
|
|
684
|
+
if args.kwarg:
|
|
685
|
+
param = f"**{args.kwarg.arg}"
|
|
686
|
+
if args.kwarg.annotation:
|
|
687
|
+
param += f": {ast.unparse(args.kwarg.annotation)}" if hasattr(ast, "unparse") else ""
|
|
688
|
+
param_strs.append(param)
|
|
689
|
+
|
|
690
|
+
parts.append(", ".join(param_strs))
|
|
691
|
+
parts.append(")")
|
|
692
|
+
|
|
693
|
+
# Return type
|
|
694
|
+
if node.returns:
|
|
695
|
+
parts.append(f" -> {ast.unparse(node.returns)}" if hasattr(ast, "unparse") else "")
|
|
696
|
+
|
|
697
|
+
return "".join(parts)
|
|
698
|
+
|
|
699
|
+
def _should_include(self, name: str) -> bool:
|
|
700
|
+
"""Check if member should be included."""
|
|
701
|
+
if name.startswith("_") and not self.include_private:
|
|
702
|
+
return False
|
|
703
|
+
return True
|