specfact-cli 0.4.2__py3-none-any.whl → 0.6.8__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.
- specfact_cli/__init__.py +1 -1
- specfact_cli/agents/analyze_agent.py +2 -3
- specfact_cli/analyzers/__init__.py +2 -1
- specfact_cli/analyzers/ambiguity_scanner.py +601 -0
- specfact_cli/analyzers/code_analyzer.py +462 -30
- specfact_cli/analyzers/constitution_evidence_extractor.py +491 -0
- specfact_cli/analyzers/contract_extractor.py +419 -0
- specfact_cli/analyzers/control_flow_analyzer.py +281 -0
- specfact_cli/analyzers/requirement_extractor.py +337 -0
- specfact_cli/analyzers/test_pattern_extractor.py +330 -0
- specfact_cli/cli.py +151 -206
- specfact_cli/commands/constitution.py +281 -0
- specfact_cli/commands/enforce.py +42 -34
- specfact_cli/commands/import_cmd.py +481 -152
- specfact_cli/commands/init.py +224 -55
- specfact_cli/commands/plan.py +2133 -547
- specfact_cli/commands/repro.py +100 -78
- specfact_cli/commands/sync.py +701 -186
- specfact_cli/enrichers/constitution_enricher.py +765 -0
- specfact_cli/enrichers/plan_enricher.py +294 -0
- specfact_cli/importers/speckit_converter.py +364 -48
- specfact_cli/importers/speckit_scanner.py +65 -0
- specfact_cli/models/plan.py +42 -0
- specfact_cli/resources/mappings/node-async.yaml +49 -0
- specfact_cli/resources/mappings/python-async.yaml +47 -0
- specfact_cli/resources/mappings/speckit-default.yaml +82 -0
- specfact_cli/resources/prompts/specfact-enforce.md +185 -0
- specfact_cli/resources/prompts/specfact-import-from-code.md +626 -0
- specfact_cli/resources/prompts/specfact-plan-add-feature.md +188 -0
- specfact_cli/resources/prompts/specfact-plan-add-story.md +212 -0
- specfact_cli/resources/prompts/specfact-plan-compare.md +571 -0
- specfact_cli/resources/prompts/specfact-plan-init.md +531 -0
- specfact_cli/resources/prompts/specfact-plan-promote.md +352 -0
- specfact_cli/resources/prompts/specfact-plan-review.md +1276 -0
- specfact_cli/resources/prompts/specfact-plan-select.md +401 -0
- specfact_cli/resources/prompts/specfact-plan-update-feature.md +242 -0
- specfact_cli/resources/prompts/specfact-plan-update-idea.md +211 -0
- specfact_cli/resources/prompts/specfact-repro.md +268 -0
- specfact_cli/resources/prompts/specfact-sync.md +497 -0
- specfact_cli/resources/schemas/deviation.schema.json +61 -0
- specfact_cli/resources/schemas/plan.schema.json +204 -0
- specfact_cli/resources/schemas/protocol.schema.json +53 -0
- specfact_cli/resources/templates/github-action.yml.j2 +140 -0
- specfact_cli/resources/templates/plan.bundle.yaml.j2 +141 -0
- specfact_cli/resources/templates/pr-template.md.j2 +58 -0
- specfact_cli/resources/templates/protocol.yaml.j2 +24 -0
- specfact_cli/resources/templates/telemetry.yaml.example +35 -0
- specfact_cli/sync/__init__.py +10 -1
- specfact_cli/sync/watcher.py +268 -0
- specfact_cli/telemetry.py +440 -0
- specfact_cli/utils/acceptance_criteria.py +127 -0
- specfact_cli/utils/enrichment_parser.py +445 -0
- specfact_cli/utils/feature_keys.py +12 -3
- specfact_cli/utils/ide_setup.py +170 -0
- specfact_cli/utils/structure.py +179 -2
- specfact_cli/utils/yaml_utils.py +33 -0
- specfact_cli/validators/repro_checker.py +22 -1
- specfact_cli/validators/schema.py +15 -4
- specfact_cli-0.6.8.dist-info/METADATA +456 -0
- specfact_cli-0.6.8.dist-info/RECORD +99 -0
- {specfact_cli-0.4.2.dist-info → specfact_cli-0.6.8.dist-info}/entry_points.txt +1 -0
- specfact_cli-0.6.8.dist-info/licenses/LICENSE.md +202 -0
- specfact_cli-0.4.2.dist-info/METADATA +0 -370
- specfact_cli-0.4.2.dist-info/RECORD +0 -62
- specfact_cli-0.4.2.dist-info/licenses/LICENSE.md +0 -61
- {specfact_cli-0.4.2.dist-info → specfact_cli-0.6.8.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
"""Contract extractor for extracting API contracts from code signatures and validation logic.
|
|
2
|
+
|
|
3
|
+
Extracts contracts from function signatures, type hints, and validation logic,
|
|
4
|
+
generating OpenAPI/JSON Schema, icontract decorators, and contract test templates.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import ast
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from beartype import beartype
|
|
13
|
+
from icontract import ensure, require
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ContractExtractor:
|
|
17
|
+
"""
|
|
18
|
+
Extracts API contracts from function signatures, type hints, and validation logic.
|
|
19
|
+
|
|
20
|
+
Generates:
|
|
21
|
+
- Request/Response schemas from type hints
|
|
22
|
+
- Preconditions from input validation
|
|
23
|
+
- Postconditions from output validation
|
|
24
|
+
- Error contracts from exception handling
|
|
25
|
+
- OpenAPI/JSON Schema definitions
|
|
26
|
+
- icontract decorators
|
|
27
|
+
- Contract test templates
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
@beartype
|
|
31
|
+
def __init__(self) -> None:
|
|
32
|
+
"""Initialize contract extractor."""
|
|
33
|
+
|
|
34
|
+
@beartype
|
|
35
|
+
@require(
|
|
36
|
+
lambda method_node: isinstance(method_node, (ast.FunctionDef, ast.AsyncFunctionDef)),
|
|
37
|
+
"Method must be function node",
|
|
38
|
+
)
|
|
39
|
+
@ensure(lambda result: isinstance(result, dict), "Must return dict")
|
|
40
|
+
def extract_function_contracts(self, method_node: ast.FunctionDef | ast.AsyncFunctionDef) -> dict[str, Any]:
|
|
41
|
+
"""
|
|
42
|
+
Extract contracts from a function signature.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
method_node: AST node for the function/method
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
Dictionary containing:
|
|
49
|
+
- parameters: List of parameter schemas
|
|
50
|
+
- return_type: Return type schema
|
|
51
|
+
- preconditions: List of preconditions
|
|
52
|
+
- postconditions: List of postconditions
|
|
53
|
+
- error_contracts: List of error contracts
|
|
54
|
+
"""
|
|
55
|
+
contracts: dict[str, Any] = {
|
|
56
|
+
"parameters": [],
|
|
57
|
+
"return_type": None,
|
|
58
|
+
"preconditions": [],
|
|
59
|
+
"postconditions": [],
|
|
60
|
+
"error_contracts": [],
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
# Extract parameters
|
|
64
|
+
contracts["parameters"] = self._extract_parameters(method_node)
|
|
65
|
+
|
|
66
|
+
# Extract return type
|
|
67
|
+
contracts["return_type"] = self._extract_return_type(method_node)
|
|
68
|
+
|
|
69
|
+
# Extract validation logic
|
|
70
|
+
contracts["preconditions"] = self._extract_preconditions(method_node)
|
|
71
|
+
contracts["postconditions"] = self._extract_postconditions(method_node)
|
|
72
|
+
contracts["error_contracts"] = self._extract_error_contracts(method_node)
|
|
73
|
+
|
|
74
|
+
return contracts
|
|
75
|
+
|
|
76
|
+
@beartype
|
|
77
|
+
@ensure(lambda result: isinstance(result, list), "Must return list")
|
|
78
|
+
def _extract_parameters(self, method_node: ast.FunctionDef | ast.AsyncFunctionDef) -> list[dict[str, Any]]:
|
|
79
|
+
"""Extract parameter schemas from function signature."""
|
|
80
|
+
parameters: list[dict[str, Any]] = []
|
|
81
|
+
|
|
82
|
+
for arg in method_node.args.args:
|
|
83
|
+
param: dict[str, Any] = {
|
|
84
|
+
"name": arg.arg,
|
|
85
|
+
"type": self._ast_to_type_string(arg.annotation) if arg.annotation else "Any",
|
|
86
|
+
"required": True,
|
|
87
|
+
"default": None,
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
# Check if parameter has default value
|
|
91
|
+
# Default args are in method_node.args.defaults, aligned with last N args
|
|
92
|
+
arg_index = method_node.args.args.index(arg)
|
|
93
|
+
defaults_start = len(method_node.args.args) - len(method_node.args.defaults)
|
|
94
|
+
if arg_index >= defaults_start:
|
|
95
|
+
default_index = arg_index - defaults_start
|
|
96
|
+
if default_index < len(method_node.args.defaults):
|
|
97
|
+
param["required"] = False
|
|
98
|
+
param["default"] = self._ast_to_value_string(method_node.args.defaults[default_index])
|
|
99
|
+
|
|
100
|
+
parameters.append(param)
|
|
101
|
+
|
|
102
|
+
# Handle *args
|
|
103
|
+
if method_node.args.vararg:
|
|
104
|
+
parameters.append(
|
|
105
|
+
{
|
|
106
|
+
"name": method_node.args.vararg.arg,
|
|
107
|
+
"type": "list[Any]",
|
|
108
|
+
"required": False,
|
|
109
|
+
"variadic": True,
|
|
110
|
+
}
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
# Handle **kwargs
|
|
114
|
+
if method_node.args.kwarg:
|
|
115
|
+
parameters.append(
|
|
116
|
+
{
|
|
117
|
+
"name": method_node.args.kwarg.arg,
|
|
118
|
+
"type": "dict[str, Any]",
|
|
119
|
+
"required": False,
|
|
120
|
+
"keyword_variadic": True,
|
|
121
|
+
}
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
return parameters
|
|
125
|
+
|
|
126
|
+
@beartype
|
|
127
|
+
@ensure(lambda result: result is None or isinstance(result, dict), "Must return None or dict")
|
|
128
|
+
def _extract_return_type(self, method_node: ast.FunctionDef | ast.AsyncFunctionDef) -> dict[str, Any] | None:
|
|
129
|
+
"""Extract return type schema from function signature."""
|
|
130
|
+
if not method_node.returns:
|
|
131
|
+
return {"type": "None", "nullable": False}
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
"type": self._ast_to_type_string(method_node.returns),
|
|
135
|
+
"nullable": False,
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
@beartype
|
|
139
|
+
@ensure(lambda result: isinstance(result, list), "Must return list")
|
|
140
|
+
def _extract_preconditions(self, method_node: ast.FunctionDef | ast.AsyncFunctionDef) -> list[str]:
|
|
141
|
+
"""Extract preconditions from validation logic in function body."""
|
|
142
|
+
preconditions: list[str] = []
|
|
143
|
+
|
|
144
|
+
if not method_node.body:
|
|
145
|
+
return preconditions
|
|
146
|
+
|
|
147
|
+
for node in method_node.body:
|
|
148
|
+
# Check for assertion statements
|
|
149
|
+
if isinstance(node, ast.Assert):
|
|
150
|
+
condition = self._ast_to_condition_string(node.test)
|
|
151
|
+
preconditions.append(f"Requires: {condition}")
|
|
152
|
+
|
|
153
|
+
# Check for validation decorators (would need to check decorator_list)
|
|
154
|
+
# For now, we'll extract from docstrings and assertions
|
|
155
|
+
|
|
156
|
+
# Check for isinstance checks
|
|
157
|
+
if isinstance(node, ast.If):
|
|
158
|
+
condition = self._ast_to_condition_string(node.test)
|
|
159
|
+
# Check if it's a validation check (isinstance, type check, etc.)
|
|
160
|
+
if "isinstance" in condition or "type" in condition.lower():
|
|
161
|
+
preconditions.append(f"Requires: {condition}")
|
|
162
|
+
|
|
163
|
+
return preconditions
|
|
164
|
+
|
|
165
|
+
@beartype
|
|
166
|
+
@ensure(lambda result: isinstance(result, list), "Must return list")
|
|
167
|
+
def _extract_postconditions(self, method_node: ast.FunctionDef | ast.AsyncFunctionDef) -> list[str]:
|
|
168
|
+
"""Extract postconditions from return value validation."""
|
|
169
|
+
postconditions: list[str] = []
|
|
170
|
+
|
|
171
|
+
if not method_node.body:
|
|
172
|
+
return postconditions
|
|
173
|
+
|
|
174
|
+
# Check for return statements with validation
|
|
175
|
+
for node in ast.walk(ast.Module(body=list(method_node.body), type_ignores=[])):
|
|
176
|
+
if isinstance(node, ast.Return) and node.value:
|
|
177
|
+
return_type = self._ast_to_type_string(method_node.returns) if method_node.returns else "Any"
|
|
178
|
+
postconditions.append(f"Ensures: returns {return_type}")
|
|
179
|
+
|
|
180
|
+
return postconditions
|
|
181
|
+
|
|
182
|
+
@beartype
|
|
183
|
+
@ensure(lambda result: isinstance(result, list), "Must return list")
|
|
184
|
+
def _extract_error_contracts(self, method_node: ast.FunctionDef | ast.AsyncFunctionDef) -> list[dict[str, Any]]:
|
|
185
|
+
"""Extract error contracts from exception handling."""
|
|
186
|
+
error_contracts: list[dict[str, Any]] = []
|
|
187
|
+
|
|
188
|
+
if not method_node.body:
|
|
189
|
+
return error_contracts
|
|
190
|
+
|
|
191
|
+
for node in method_node.body:
|
|
192
|
+
if isinstance(node, ast.Try):
|
|
193
|
+
for handler in node.handlers:
|
|
194
|
+
exception_type = "Exception"
|
|
195
|
+
if handler.type:
|
|
196
|
+
exception_type = self._ast_to_type_string(handler.type)
|
|
197
|
+
|
|
198
|
+
error_contracts.append(
|
|
199
|
+
{
|
|
200
|
+
"exception_type": exception_type,
|
|
201
|
+
"condition": self._ast_to_condition_string(handler.type)
|
|
202
|
+
if handler.type
|
|
203
|
+
else "Any exception",
|
|
204
|
+
}
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
# Check for raise statements
|
|
208
|
+
for child in ast.walk(node):
|
|
209
|
+
if (
|
|
210
|
+
isinstance(child, ast.Raise)
|
|
211
|
+
and child.exc
|
|
212
|
+
and isinstance(child.exc, ast.Call)
|
|
213
|
+
and isinstance(child.exc.func, ast.Name)
|
|
214
|
+
):
|
|
215
|
+
error_contracts.append(
|
|
216
|
+
{
|
|
217
|
+
"exception_type": child.exc.func.id,
|
|
218
|
+
"condition": "Error condition",
|
|
219
|
+
}
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
return error_contracts
|
|
223
|
+
|
|
224
|
+
@beartype
|
|
225
|
+
@ensure(lambda result: isinstance(result, str), "Must return string")
|
|
226
|
+
def _ast_to_type_string(self, node: ast.AST | None) -> str:
|
|
227
|
+
"""Convert AST type annotation node to string representation."""
|
|
228
|
+
if node is None:
|
|
229
|
+
return "Any"
|
|
230
|
+
|
|
231
|
+
# Use ast.unparse if available (Python 3.9+)
|
|
232
|
+
if hasattr(ast, "unparse"):
|
|
233
|
+
try:
|
|
234
|
+
return ast.unparse(node)
|
|
235
|
+
except Exception:
|
|
236
|
+
pass
|
|
237
|
+
|
|
238
|
+
# Fallback: manual conversion
|
|
239
|
+
if isinstance(node, ast.Name):
|
|
240
|
+
return node.id
|
|
241
|
+
if isinstance(node, ast.Subscript) and isinstance(node.value, ast.Name):
|
|
242
|
+
# Handle generics like List[str], Dict[str, int], Optional[str]
|
|
243
|
+
container = node.value.id
|
|
244
|
+
if isinstance(node.slice, ast.Tuple):
|
|
245
|
+
args = [self._ast_to_type_string(el) for el in node.slice.elts]
|
|
246
|
+
return f"{container}[{', '.join(args)}]"
|
|
247
|
+
if isinstance(node.slice, ast.Name):
|
|
248
|
+
return f"{container}[{node.slice.id}]"
|
|
249
|
+
return f"{container}[...]"
|
|
250
|
+
if isinstance(node, ast.Constant):
|
|
251
|
+
return str(node.value)
|
|
252
|
+
|
|
253
|
+
return "Any"
|
|
254
|
+
|
|
255
|
+
@beartype
|
|
256
|
+
@ensure(lambda result: isinstance(result, str), "Must return string")
|
|
257
|
+
def _ast_to_value_string(self, node: ast.AST) -> str:
|
|
258
|
+
"""Convert AST value node to string representation."""
|
|
259
|
+
if isinstance(node, ast.Constant):
|
|
260
|
+
return repr(node.value)
|
|
261
|
+
if isinstance(node, ast.Name):
|
|
262
|
+
return node.id
|
|
263
|
+
if isinstance(node, ast.NameConstant): # Python < 3.8
|
|
264
|
+
return str(node.value)
|
|
265
|
+
|
|
266
|
+
# Use ast.unparse if available
|
|
267
|
+
if hasattr(ast, "unparse"):
|
|
268
|
+
try:
|
|
269
|
+
return ast.unparse(node)
|
|
270
|
+
except Exception:
|
|
271
|
+
pass
|
|
272
|
+
|
|
273
|
+
return "..."
|
|
274
|
+
|
|
275
|
+
@beartype
|
|
276
|
+
@ensure(lambda result: isinstance(result, str), "Must return string")
|
|
277
|
+
def _ast_to_condition_string(self, node: ast.AST) -> str:
|
|
278
|
+
"""Convert AST condition node to string representation."""
|
|
279
|
+
# Use ast.unparse if available
|
|
280
|
+
if hasattr(ast, "unparse"):
|
|
281
|
+
try:
|
|
282
|
+
return ast.unparse(node)
|
|
283
|
+
except Exception:
|
|
284
|
+
pass
|
|
285
|
+
|
|
286
|
+
# Fallback: basic conversion
|
|
287
|
+
if isinstance(node, ast.Compare):
|
|
288
|
+
left = self._ast_to_condition_string(node.left) if hasattr(node, "left") else "..."
|
|
289
|
+
ops = [self._op_to_string(op) for op in node.ops]
|
|
290
|
+
comparators = [self._ast_to_condition_string(comp) for comp in node.comparators]
|
|
291
|
+
return f"{left} {' '.join(ops)} {' '.join(comparators)}"
|
|
292
|
+
if isinstance(node, ast.Call) and isinstance(node.func, ast.Name):
|
|
293
|
+
args = [self._ast_to_condition_string(arg) for arg in node.args]
|
|
294
|
+
return f"{node.func.id}({', '.join(args)})"
|
|
295
|
+
if isinstance(node, ast.Name):
|
|
296
|
+
return node.id
|
|
297
|
+
if isinstance(node, ast.Constant):
|
|
298
|
+
return repr(node.value)
|
|
299
|
+
|
|
300
|
+
return "..."
|
|
301
|
+
|
|
302
|
+
@beartype
|
|
303
|
+
@ensure(lambda result: isinstance(result, str), "Must return string")
|
|
304
|
+
def _op_to_string(self, op: ast.cmpop) -> str:
|
|
305
|
+
"""Convert AST comparison operator to string."""
|
|
306
|
+
op_map = {
|
|
307
|
+
ast.Eq: "==",
|
|
308
|
+
ast.NotEq: "!=",
|
|
309
|
+
ast.Lt: "<",
|
|
310
|
+
ast.LtE: "<=",
|
|
311
|
+
ast.Gt: ">",
|
|
312
|
+
ast.GtE: ">=",
|
|
313
|
+
ast.Is: "is",
|
|
314
|
+
ast.IsNot: "is not",
|
|
315
|
+
ast.In: "in",
|
|
316
|
+
ast.NotIn: "not in",
|
|
317
|
+
}
|
|
318
|
+
return op_map.get(type(op), "??")
|
|
319
|
+
|
|
320
|
+
@beartype
|
|
321
|
+
@require(lambda contracts: isinstance(contracts, dict), "Contracts must be dict")
|
|
322
|
+
@ensure(lambda result: isinstance(result, dict), "Must return dict")
|
|
323
|
+
def generate_json_schema(self, contracts: dict[str, Any]) -> dict[str, Any]:
|
|
324
|
+
"""
|
|
325
|
+
Generate JSON Schema from contracts.
|
|
326
|
+
|
|
327
|
+
Args:
|
|
328
|
+
contracts: Contract dictionary from extract_function_contracts()
|
|
329
|
+
|
|
330
|
+
Returns:
|
|
331
|
+
JSON Schema dictionary
|
|
332
|
+
"""
|
|
333
|
+
schema: dict[str, Any] = {
|
|
334
|
+
"type": "object",
|
|
335
|
+
"properties": {},
|
|
336
|
+
"required": [],
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
# Add parameter properties
|
|
340
|
+
for param in contracts.get("parameters", []):
|
|
341
|
+
param_name = param["name"]
|
|
342
|
+
param_type = param.get("type", "Any")
|
|
343
|
+
schema["properties"][param_name] = self._type_to_json_schema(param_type)
|
|
344
|
+
|
|
345
|
+
if param.get("required", True):
|
|
346
|
+
schema["required"].append(param_name)
|
|
347
|
+
|
|
348
|
+
return schema
|
|
349
|
+
|
|
350
|
+
@beartype
|
|
351
|
+
@ensure(lambda result: isinstance(result, dict), "Must return dict")
|
|
352
|
+
def _type_to_json_schema(self, type_str: str) -> dict[str, Any]:
|
|
353
|
+
"""Convert Python type string to JSON Schema type."""
|
|
354
|
+
type_str = type_str.strip()
|
|
355
|
+
|
|
356
|
+
# Basic types
|
|
357
|
+
if type_str == "str":
|
|
358
|
+
return {"type": "string"}
|
|
359
|
+
if type_str == "int":
|
|
360
|
+
return {"type": "integer"}
|
|
361
|
+
if type_str == "float":
|
|
362
|
+
return {"type": "number"}
|
|
363
|
+
if type_str == "bool":
|
|
364
|
+
return {"type": "boolean"}
|
|
365
|
+
if type_str == "None" or type_str == "NoneType":
|
|
366
|
+
return {"type": "null"}
|
|
367
|
+
|
|
368
|
+
# Optional types
|
|
369
|
+
if type_str.startswith("Optional[") or (type_str.startswith("Union[") and "None" in type_str):
|
|
370
|
+
inner_type = type_str.split("[")[1].rstrip("]").split(",")[0].strip()
|
|
371
|
+
if "None" in inner_type:
|
|
372
|
+
inner_type = next(
|
|
373
|
+
(t.strip() for t in type_str.split("[")[1].rstrip("]").split(",") if "None" not in t),
|
|
374
|
+
inner_type,
|
|
375
|
+
)
|
|
376
|
+
return {"anyOf": [self._type_to_json_schema(inner_type), {"type": "null"}]}
|
|
377
|
+
|
|
378
|
+
# List types
|
|
379
|
+
if type_str.startswith(("list[", "List[")):
|
|
380
|
+
inner_type = type_str.split("[")[1].rstrip("]")
|
|
381
|
+
return {"type": "array", "items": self._type_to_json_schema(inner_type)}
|
|
382
|
+
|
|
383
|
+
# Dict types
|
|
384
|
+
if type_str.startswith(("dict[", "Dict[")):
|
|
385
|
+
parts = type_str.split("[")[1].rstrip("]").split(",")
|
|
386
|
+
if len(parts) >= 2:
|
|
387
|
+
value_type = parts[1].strip()
|
|
388
|
+
return {"type": "object", "additionalProperties": self._type_to_json_schema(value_type)}
|
|
389
|
+
|
|
390
|
+
# Default: any type
|
|
391
|
+
return {"type": "object"}
|
|
392
|
+
|
|
393
|
+
@beartype
|
|
394
|
+
@require(lambda contracts: isinstance(contracts, dict), "Contracts must be dict")
|
|
395
|
+
@ensure(lambda result: isinstance(result, str), "Must return string")
|
|
396
|
+
def generate_icontract_decorator(self, contracts: dict[str, Any], function_name: str) -> str:
|
|
397
|
+
"""
|
|
398
|
+
Generate icontract decorator code from contracts.
|
|
399
|
+
|
|
400
|
+
Args:
|
|
401
|
+
contracts: Contract dictionary from extract_function_contracts()
|
|
402
|
+
function_name: Name of the function
|
|
403
|
+
|
|
404
|
+
Returns:
|
|
405
|
+
Python code string with icontract decorators
|
|
406
|
+
"""
|
|
407
|
+
decorators: list[str] = []
|
|
408
|
+
|
|
409
|
+
# Generate @require decorators from preconditions
|
|
410
|
+
for precondition in contracts.get("preconditions", []):
|
|
411
|
+
condition = precondition.replace("Requires: ", "")
|
|
412
|
+
decorators.append(f'@require(lambda: {condition}, "{precondition}")')
|
|
413
|
+
|
|
414
|
+
# Generate @ensure decorators from postconditions
|
|
415
|
+
for postcondition in contracts.get("postconditions", []):
|
|
416
|
+
condition = postcondition.replace("Ensures: ", "")
|
|
417
|
+
decorators.append(f'@ensure(lambda result: {condition}, "{postcondition}")')
|
|
418
|
+
|
|
419
|
+
return "\n".join(decorators) if decorators else ""
|