invar-tools 1.17.20__py3-none-any.whl → 1.17.22__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.
- invar/core/models.py +7 -3
- invar/core/rules.py +50 -15
- invar/shell/commands/guard.py +15 -0
- invar/shell/prove/crosshair.py +128 -1
- invar/shell/subprocess_env.py +49 -0
- invar/shell/testing.py +2 -1
- {invar_tools-1.17.20.dist-info → invar_tools-1.17.22.dist-info}/METADATA +1 -1
- {invar_tools-1.17.20.dist-info → invar_tools-1.17.22.dist-info}/RECORD +13 -13
- {invar_tools-1.17.20.dist-info → invar_tools-1.17.22.dist-info}/WHEEL +0 -0
- {invar_tools-1.17.20.dist-info → invar_tools-1.17.22.dist-info}/entry_points.txt +0 -0
- {invar_tools-1.17.20.dist-info → invar_tools-1.17.22.dist-info}/licenses/LICENSE +0 -0
- {invar_tools-1.17.20.dist-info → invar_tools-1.17.22.dist-info}/licenses/LICENSE-GPL +0 -0
- {invar_tools-1.17.20.dist-info → invar_tools-1.17.22.dist-info}/licenses/NOTICE +0 -0
invar/core/models.py
CHANGED
|
@@ -160,7 +160,10 @@ def get_layer(file_info: FileInfo) -> CodeLayer:
|
|
|
160
160
|
return CodeLayer.DEFAULT
|
|
161
161
|
|
|
162
162
|
|
|
163
|
-
@pre(
|
|
163
|
+
@pre(
|
|
164
|
+
lambda layer, language="python": isinstance(layer, CodeLayer)
|
|
165
|
+
and language in ("python", "typescript")
|
|
166
|
+
)
|
|
164
167
|
@post(lambda result: result.max_file_lines > 0 and result.max_function_lines > 0)
|
|
165
168
|
def get_limits(layer: CodeLayer, language: str = "python") -> LayerLimits:
|
|
166
169
|
"""
|
|
@@ -424,8 +427,9 @@ class RuleConfig(BaseModel):
|
|
|
424
427
|
"""
|
|
425
428
|
|
|
426
429
|
# MINOR-6: Added ge=1 constraints for numeric fields
|
|
427
|
-
|
|
428
|
-
|
|
430
|
+
# BUG-55: These override layer-based limits when set to non-default values
|
|
431
|
+
max_file_lines: int = Field(default=500, ge=1) # Override all layers if != 500
|
|
432
|
+
max_function_lines: int = Field(default=50, ge=1) # Override all layers if != 50
|
|
429
433
|
entry_max_lines: int = Field(default=15, ge=1) # DX-23: Entry point max lines
|
|
430
434
|
shell_max_branches: int = Field(default=3, ge=1) # DX-22: Shell function max branches
|
|
431
435
|
shell_complexity_debt_limit: int = Field(default=5, ge=0) # DX-22: 0 = no limit
|
invar/core/rules.py
CHANGED
|
@@ -71,7 +71,11 @@ def _build_size_suggestion(base: str, extraction_hint: str, func_hint: str) -> s
|
|
|
71
71
|
def _get_func_hint(file_info: FileInfo) -> str:
|
|
72
72
|
"""Get top 5 largest functions as hint string."""
|
|
73
73
|
funcs = sorted(
|
|
74
|
-
[
|
|
74
|
+
[
|
|
75
|
+
(s.name, s.end_line - s.line + 1)
|
|
76
|
+
for s in file_info.symbols
|
|
77
|
+
if s.kind in (SymbolKind.FUNCTION, SymbolKind.METHOD)
|
|
78
|
+
],
|
|
75
79
|
key=lambda x: -x[1],
|
|
76
80
|
)[:5]
|
|
77
81
|
return f" Functions: {', '.join(f'{n}({sz}L)' for n, sz in funcs)}" if funcs else ""
|
|
@@ -104,6 +108,7 @@ def check_file_size(file_info: FileInfo, config: RuleConfig) -> list[Violation]:
|
|
|
104
108
|
Check if file exceeds maximum line count or warning threshold.
|
|
105
109
|
|
|
106
110
|
LX-10: Uses layer-based limits (Core/Shell/Tests/Default).
|
|
111
|
+
BUG-55: Config override - if max_file_lines is set to non-default, use it.
|
|
107
112
|
P18: Shows function groups in size warnings to help agents decide what to extract.
|
|
108
113
|
P25: Shows extractable groups with dependencies for warnings.
|
|
109
114
|
|
|
@@ -121,6 +126,10 @@ def check_file_size(file_info: FileInfo, config: RuleConfig) -> list[Violation]:
|
|
|
121
126
|
>>> # Core layer: 500 lines max (strict)
|
|
122
127
|
>>> len(check_file_size(FileInfo(path="core/calc.py", lines=550, is_core=True), RuleConfig()))
|
|
123
128
|
1
|
|
129
|
+
>>> # BUG-55: Config override allows larger files (no error at 550 with max 600)
|
|
130
|
+
>>> vs = check_file_size(FileInfo(path="core/calc.py", lines=550, is_core=True), RuleConfig(max_file_lines=600, size_warning_threshold=0))
|
|
131
|
+
>>> any(v.rule == "file_size" for v in vs)
|
|
132
|
+
False
|
|
124
133
|
"""
|
|
125
134
|
# Check for escape hatch
|
|
126
135
|
if _has_file_escape(file_info, "file_size"):
|
|
@@ -133,23 +142,38 @@ def check_file_size(file_info: FileInfo, config: RuleConfig) -> list[Violation]:
|
|
|
133
142
|
# LX-10: Get layer-based limits
|
|
134
143
|
layer = get_layer(file_info)
|
|
135
144
|
limits = get_limits(layer)
|
|
136
|
-
|
|
145
|
+
# BUG-55: Allow config override if user sets non-default value
|
|
146
|
+
max_lines = config.max_file_lines if config.max_file_lines != 500 else limits.max_file_lines
|
|
137
147
|
|
|
138
148
|
if file_info.lines > max_lines:
|
|
139
|
-
violations.append(
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
149
|
+
violations.append(
|
|
150
|
+
Violation(
|
|
151
|
+
rule="file_size",
|
|
152
|
+
severity=Severity.ERROR,
|
|
153
|
+
file=file_info.path,
|
|
154
|
+
line=None,
|
|
155
|
+
message=f"File has {file_info.lines} lines (max: {max_lines} for {layer.value})",
|
|
156
|
+
suggestion=_build_size_suggestion(
|
|
157
|
+
"Split into smaller modules.", extraction_hint, func_hint
|
|
158
|
+
),
|
|
159
|
+
)
|
|
160
|
+
)
|
|
144
161
|
elif config.size_warning_threshold > 0:
|
|
145
162
|
threshold = int(max_lines * config.size_warning_threshold)
|
|
146
163
|
if file_info.lines >= threshold:
|
|
147
164
|
pct = int(file_info.lines / max_lines * 100)
|
|
148
|
-
violations.append(
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
165
|
+
violations.append(
|
|
166
|
+
Violation(
|
|
167
|
+
rule="file_size_warning",
|
|
168
|
+
severity=Severity.WARNING,
|
|
169
|
+
file=file_info.path,
|
|
170
|
+
line=None,
|
|
171
|
+
message=f"File has {file_info.lines} lines ({pct}% of {max_lines} limit)",
|
|
172
|
+
suggestion=_build_size_suggestion(
|
|
173
|
+
"Consider splitting before reaching limit.", extraction_hint, func_hint
|
|
174
|
+
),
|
|
175
|
+
)
|
|
176
|
+
)
|
|
153
177
|
return violations
|
|
154
178
|
|
|
155
179
|
|
|
@@ -159,6 +183,7 @@ def check_function_size(file_info: FileInfo, config: RuleConfig) -> list[Violati
|
|
|
159
183
|
Check if any function exceeds maximum line count.
|
|
160
184
|
|
|
161
185
|
LX-10: Uses layer-based limits (Core/Shell/Tests/Default).
|
|
186
|
+
BUG-55: Config override - if max_function_lines is set to non-default, use it.
|
|
162
187
|
DX-22: Always uses code_lines (excluding docstring) and excludes doctest lines.
|
|
163
188
|
|
|
164
189
|
Examples:
|
|
@@ -177,13 +202,19 @@ def check_function_size(file_info: FileInfo, config: RuleConfig) -> list[Violati
|
|
|
177
202
|
>>> info3 = FileInfo(path="core/calc.py", lines=100, symbols=[sym3], is_core=True)
|
|
178
203
|
>>> len(check_function_size(info3, RuleConfig()))
|
|
179
204
|
1
|
|
205
|
+
>>> # BUG-55: Config override allows larger functions
|
|
206
|
+
>>> len(check_function_size(info3, RuleConfig(max_function_lines=70)))
|
|
207
|
+
0
|
|
180
208
|
"""
|
|
181
209
|
violations: list[Violation] = []
|
|
182
210
|
|
|
183
211
|
# LX-10: Get layer-based limits
|
|
184
212
|
layer = get_layer(file_info)
|
|
185
213
|
limits = get_limits(layer)
|
|
186
|
-
|
|
214
|
+
# BUG-55: Allow config override if user sets non-default value
|
|
215
|
+
max_func_lines = (
|
|
216
|
+
config.max_function_lines if config.max_function_lines != 50 else limits.max_function_lines
|
|
217
|
+
)
|
|
187
218
|
|
|
188
219
|
for symbol in file_info.symbols:
|
|
189
220
|
if symbol.kind in (SymbolKind.FUNCTION, SymbolKind.METHOD):
|
|
@@ -392,7 +423,9 @@ def check_shell_result(file_info: FileInfo, config: RuleConfig) -> list[Violatio
|
|
|
392
423
|
):
|
|
393
424
|
continue
|
|
394
425
|
# DX-23: Skip entry points; DX-22: Skip if @invar:allow marker
|
|
395
|
-
if is_entry_point(symbol, file_info.source) or has_allow_marker(
|
|
426
|
+
if is_entry_point(symbol, file_info.source) or has_allow_marker(
|
|
427
|
+
symbol, file_info.source, "shell_result"
|
|
428
|
+
):
|
|
396
429
|
continue
|
|
397
430
|
if "Result[" not in symbol.signature:
|
|
398
431
|
violations.append(
|
|
@@ -435,7 +468,9 @@ def check_entry_point_thin(file_info: FileInfo, config: RuleConfig) -> list[Viol
|
|
|
435
468
|
continue
|
|
436
469
|
|
|
437
470
|
# Only check entry points; DX-22: Skip if @invar:allow marker
|
|
438
|
-
if not is_entry_point(symbol, file_info.source) or has_allow_marker(
|
|
471
|
+
if not is_entry_point(symbol, file_info.source) or has_allow_marker(
|
|
472
|
+
symbol, file_info.source, "entry_point_too_thick"
|
|
473
|
+
):
|
|
439
474
|
continue
|
|
440
475
|
lines = get_symbol_lines(symbol)
|
|
441
476
|
if lines > max_lines:
|
invar/shell/commands/guard.py
CHANGED
|
@@ -6,6 +6,8 @@ Shell module: handles user interaction and file I/O.
|
|
|
6
6
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
9
11
|
from pathlib import Path
|
|
10
12
|
|
|
11
13
|
import typer
|
|
@@ -234,6 +236,19 @@ def guard(
|
|
|
234
236
|
raise typer.Exit(1)
|
|
235
237
|
path = pyproject_root
|
|
236
238
|
|
|
239
|
+
from invar.shell.subprocess_env import get_uvx_respawn_command
|
|
240
|
+
|
|
241
|
+
cmd = get_uvx_respawn_command(
|
|
242
|
+
project_root=path,
|
|
243
|
+
argv=sys.argv[1:],
|
|
244
|
+
tool_name=Path(sys.argv[0]).name,
|
|
245
|
+
invar_tools_version=__version__,
|
|
246
|
+
)
|
|
247
|
+
if cmd is not None:
|
|
248
|
+
env = os.environ.copy()
|
|
249
|
+
env["INVAR_UVX_RESPAWNED"] = "1"
|
|
250
|
+
os.execvpe(cmd[0], cmd, env)
|
|
251
|
+
|
|
237
252
|
# Load and configure
|
|
238
253
|
config_result = load_config(path)
|
|
239
254
|
if isinstance(config_result, Failure):
|
invar/shell/prove/crosshair.py
CHANGED
|
@@ -88,6 +88,88 @@ def has_verifiable_contracts(source: str) -> bool:
|
|
|
88
88
|
return False
|
|
89
89
|
|
|
90
90
|
|
|
91
|
+
# BUG-56: Detect Literal types that CrossHair cannot handle
|
|
92
|
+
# @shell_orchestration: Pre-filter for CrossHair verification
|
|
93
|
+
# @shell_complexity: AST traversal for type annotation analysis
|
|
94
|
+
def has_literal_in_contracted_functions(source: str) -> bool:
|
|
95
|
+
"""Check if any contracted function uses Literal types in parameters.
|
|
96
|
+
|
|
97
|
+
CrossHair cannot symbolically execute Literal types and silently skips them.
|
|
98
|
+
This function detects such cases for pre-filtering and warning.
|
|
99
|
+
"""
|
|
100
|
+
# Fast path: no Literal import
|
|
101
|
+
if "Literal" not in source:
|
|
102
|
+
return False
|
|
103
|
+
|
|
104
|
+
# Fast path: no contracts
|
|
105
|
+
if "@pre" not in source and "@post" not in source:
|
|
106
|
+
return False
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
import ast
|
|
110
|
+
|
|
111
|
+
tree = ast.parse(source)
|
|
112
|
+
except SyntaxError:
|
|
113
|
+
return False # Can't analyze, don't skip
|
|
114
|
+
|
|
115
|
+
contract_decorators = {"pre", "post"}
|
|
116
|
+
|
|
117
|
+
def has_contract(node: ast.FunctionDef | ast.AsyncFunctionDef) -> bool:
|
|
118
|
+
for dec in node.decorator_list:
|
|
119
|
+
if isinstance(dec, ast.Call):
|
|
120
|
+
func = dec.func
|
|
121
|
+
if isinstance(func, ast.Name) and func.id in contract_decorators:
|
|
122
|
+
return True
|
|
123
|
+
if isinstance(func, ast.Attribute) and func.attr in contract_decorators:
|
|
124
|
+
return True
|
|
125
|
+
return False
|
|
126
|
+
|
|
127
|
+
def annotation_uses_literal(annotation: ast.expr | None) -> bool:
|
|
128
|
+
"""Check if annotation contains Literal type."""
|
|
129
|
+
if annotation is None:
|
|
130
|
+
return False
|
|
131
|
+
|
|
132
|
+
# Direct Literal["..."]
|
|
133
|
+
if isinstance(annotation, ast.Subscript):
|
|
134
|
+
value = annotation.value
|
|
135
|
+
if isinstance(value, ast.Name) and value.id == "Literal":
|
|
136
|
+
return True
|
|
137
|
+
if isinstance(value, ast.Attribute) and value.attr == "Literal":
|
|
138
|
+
return True
|
|
139
|
+
# Check nested (e.g., Optional[Literal["..."]])
|
|
140
|
+
if annotation_uses_literal(annotation.slice):
|
|
141
|
+
return True
|
|
142
|
+
if annotation_uses_literal(value):
|
|
143
|
+
return True
|
|
144
|
+
|
|
145
|
+
# Union types
|
|
146
|
+
if isinstance(annotation, ast.BinOp): # X | Y syntax
|
|
147
|
+
return annotation_uses_literal(annotation.left) or annotation_uses_literal(
|
|
148
|
+
annotation.right
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
# Tuple of types (for Subscript.slice)
|
|
152
|
+
if isinstance(annotation, ast.Tuple):
|
|
153
|
+
return any(annotation_uses_literal(elt) for elt in annotation.elts)
|
|
154
|
+
|
|
155
|
+
return False
|
|
156
|
+
|
|
157
|
+
for node in ast.walk(tree):
|
|
158
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
159
|
+
if has_contract(node):
|
|
160
|
+
# Check all parameter annotations
|
|
161
|
+
for arg in node.args.args + node.args.posonlyargs + node.args.kwonlyargs:
|
|
162
|
+
if annotation_uses_literal(arg.annotation):
|
|
163
|
+
return True
|
|
164
|
+
# Check *args and **kwargs
|
|
165
|
+
if node.args.vararg and annotation_uses_literal(node.args.vararg.annotation):
|
|
166
|
+
return True
|
|
167
|
+
if node.args.kwarg and annotation_uses_literal(node.args.kwarg.annotation):
|
|
168
|
+
return True
|
|
169
|
+
|
|
170
|
+
return False
|
|
171
|
+
|
|
172
|
+
|
|
91
173
|
# ============================================================
|
|
92
174
|
# DX-13: Single File Verification (for parallel execution)
|
|
93
175
|
# ============================================================
|
|
@@ -156,6 +238,23 @@ def _verify_single_file(
|
|
|
156
238
|
# symbolically execute C extensions like ast.parse()
|
|
157
239
|
# Check both stdout and stderr for error patterns
|
|
158
240
|
output = result.stdout + "\n" + result.stderr
|
|
241
|
+
|
|
242
|
+
# BUG-56: Detect Literal type errors (CrossHair limitation)
|
|
243
|
+
literal_errors = [
|
|
244
|
+
"Cannot instantiate typing.Literal",
|
|
245
|
+
"CrosshairUnsupported",
|
|
246
|
+
]
|
|
247
|
+
is_literal_error = any(err in output for err in literal_errors)
|
|
248
|
+
|
|
249
|
+
if is_literal_error:
|
|
250
|
+
return {
|
|
251
|
+
"file": file_path,
|
|
252
|
+
"status": CrossHairStatus.SKIPPED,
|
|
253
|
+
"time_ms": elapsed_ms,
|
|
254
|
+
"reason": "Literal type not supported (tested by Hypothesis)",
|
|
255
|
+
"stdout": output,
|
|
256
|
+
}
|
|
257
|
+
|
|
159
258
|
execution_errors = [
|
|
160
259
|
"TypeError:",
|
|
161
260
|
"AttributeError:",
|
|
@@ -298,6 +397,17 @@ def run_crosshair_parallel(
|
|
|
298
397
|
}
|
|
299
398
|
)
|
|
300
399
|
continue
|
|
400
|
+
|
|
401
|
+
# BUG-56: Check for Literal types that CrossHair cannot handle
|
|
402
|
+
if has_literal_in_contracted_functions(source):
|
|
403
|
+
cached_results.append(
|
|
404
|
+
{
|
|
405
|
+
"file": str(py_file),
|
|
406
|
+
"status": CrossHairStatus.SKIPPED,
|
|
407
|
+
"reason": "Literal type not supported (tested by Hypothesis)",
|
|
408
|
+
}
|
|
409
|
+
)
|
|
410
|
+
continue
|
|
301
411
|
except OSError:
|
|
302
412
|
pass # Include file anyway
|
|
303
413
|
|
|
@@ -324,6 +434,7 @@ def run_crosshair_parallel(
|
|
|
324
434
|
verified_files: list[str] = []
|
|
325
435
|
failed_files: list[str] = []
|
|
326
436
|
all_counterexamples: list[str] = []
|
|
437
|
+
skipped_at_runtime: list[dict] = [] # BUG-56: Track runtime skips with reasons
|
|
327
438
|
total_time_ms = 0
|
|
328
439
|
|
|
329
440
|
if max_workers > 1 and len(files_to_verify) > 1:
|
|
@@ -351,6 +462,7 @@ def run_crosshair_parallel(
|
|
|
351
462
|
verified_files,
|
|
352
463
|
failed_files,
|
|
353
464
|
all_counterexamples,
|
|
465
|
+
skipped_at_runtime,
|
|
354
466
|
cache,
|
|
355
467
|
)
|
|
356
468
|
total_time_ms += result.get("time_ms", 0)
|
|
@@ -372,6 +484,7 @@ def run_crosshair_parallel(
|
|
|
372
484
|
verified_files,
|
|
373
485
|
failed_files,
|
|
374
486
|
all_counterexamples,
|
|
487
|
+
skipped_at_runtime,
|
|
375
488
|
cache,
|
|
376
489
|
)
|
|
377
490
|
total_time_ms += result.get("time_ms", 0)
|
|
@@ -379,13 +492,18 @@ def run_crosshair_parallel(
|
|
|
379
492
|
# Determine overall status
|
|
380
493
|
status = CrossHairStatus.VERIFIED if not failed_files else CrossHairStatus.COUNTEREXAMPLE
|
|
381
494
|
|
|
495
|
+
# BUG-56: Combine pre-filtered skipped and runtime skipped
|
|
496
|
+
all_skipped = [r["file"] for r in cached_results if r.get("status") == "skipped"]
|
|
497
|
+
all_skipped.extend([r["file"] for r in skipped_at_runtime])
|
|
498
|
+
|
|
382
499
|
return Success(
|
|
383
500
|
{
|
|
384
501
|
"status": status,
|
|
385
502
|
"verified": verified_files,
|
|
386
503
|
"failed": failed_files,
|
|
387
504
|
"cached": [r["file"] for r in cached_results if r.get("status") == "cached"],
|
|
388
|
-
"skipped":
|
|
505
|
+
"skipped": all_skipped,
|
|
506
|
+
"skipped_reasons": skipped_at_runtime, # BUG-56: Include reasons
|
|
389
507
|
"counterexamples": all_counterexamples,
|
|
390
508
|
"files": [str(f) for f in py_files],
|
|
391
509
|
"files_verified": len(files_to_verify),
|
|
@@ -404,6 +522,7 @@ def _process_verification_result(
|
|
|
404
522
|
verified_files: list[str],
|
|
405
523
|
failed_files: list[str],
|
|
406
524
|
all_counterexamples: list[str],
|
|
525
|
+
skipped_at_runtime: list[dict], # BUG-56: Track runtime skips
|
|
407
526
|
cache: ProveCache | None,
|
|
408
527
|
) -> None:
|
|
409
528
|
"""Process a single verification result."""
|
|
@@ -425,6 +544,14 @@ def _process_verification_result(
|
|
|
425
544
|
failed_files.append(f"{file_path} (timeout)")
|
|
426
545
|
elif status == CrossHairStatus.ERROR:
|
|
427
546
|
failed_files.append(f"{file_path} ({result.get('error', 'unknown error')})")
|
|
547
|
+
elif status == CrossHairStatus.SKIPPED:
|
|
548
|
+
# BUG-56: Track skipped files with reasons (e.g., Literal type)
|
|
549
|
+
skipped_at_runtime.append(
|
|
550
|
+
{
|
|
551
|
+
"file": str(file_path),
|
|
552
|
+
"reason": result.get("reason", "unsupported"),
|
|
553
|
+
}
|
|
554
|
+
)
|
|
428
555
|
|
|
429
556
|
|
|
430
557
|
# ============================================================
|
invar/shell/subprocess_env.py
CHANGED
|
@@ -11,6 +11,7 @@ This module provides three phases of dependency injection:
|
|
|
11
11
|
from __future__ import annotations
|
|
12
12
|
|
|
13
13
|
import os
|
|
14
|
+
import shutil
|
|
14
15
|
import subprocess
|
|
15
16
|
import sys
|
|
16
17
|
from datetime import datetime, timedelta
|
|
@@ -24,6 +25,7 @@ __all__ = [
|
|
|
24
25
|
"detect_project_python_with_invar",
|
|
25
26
|
"detect_project_venv",
|
|
26
27
|
"find_site_packages",
|
|
28
|
+
"get_uvx_respawn_command",
|
|
27
29
|
"get_venv_python_version",
|
|
28
30
|
"maybe_show_upgrade_prompt",
|
|
29
31
|
"should_respawn",
|
|
@@ -200,6 +202,53 @@ def detect_project_python_with_invar(cwd: Path) -> Path | None:
|
|
|
200
202
|
return None
|
|
201
203
|
|
|
202
204
|
|
|
205
|
+
def _detect_venv_python(venv: Path) -> Path | None:
|
|
206
|
+
python_path = venv / "bin" / "python"
|
|
207
|
+
if not python_path.exists():
|
|
208
|
+
python_path = venv / "Scripts" / "python.exe"
|
|
209
|
+
return python_path if python_path.exists() else None
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def get_uvx_respawn_command(
|
|
213
|
+
project_root: Path,
|
|
214
|
+
argv: list[str],
|
|
215
|
+
tool_name: str,
|
|
216
|
+
invar_tools_version: str,
|
|
217
|
+
) -> list[str] | None:
|
|
218
|
+
if os.environ.get("INVAR_UVX_RESPAWNED") == "1":
|
|
219
|
+
return None
|
|
220
|
+
|
|
221
|
+
venv = detect_project_venv(project_root)
|
|
222
|
+
if venv is None:
|
|
223
|
+
return None
|
|
224
|
+
|
|
225
|
+
venv_version = get_venv_python_version(venv)
|
|
226
|
+
if venv_version is None:
|
|
227
|
+
return None
|
|
228
|
+
|
|
229
|
+
current_version = (sys.version_info.major, sys.version_info.minor)
|
|
230
|
+
if venv_version == current_version:
|
|
231
|
+
return None
|
|
232
|
+
|
|
233
|
+
uvx_path = shutil.which("uvx")
|
|
234
|
+
if uvx_path is None:
|
|
235
|
+
return None
|
|
236
|
+
|
|
237
|
+
project_python = _detect_venv_python(venv)
|
|
238
|
+
if project_python is None:
|
|
239
|
+
return None
|
|
240
|
+
|
|
241
|
+
return [
|
|
242
|
+
uvx_path,
|
|
243
|
+
"--python",
|
|
244
|
+
str(project_python),
|
|
245
|
+
"--from",
|
|
246
|
+
f"invar-tools=={invar_tools_version}",
|
|
247
|
+
tool_name,
|
|
248
|
+
*argv,
|
|
249
|
+
]
|
|
250
|
+
|
|
251
|
+
|
|
203
252
|
@pre(lambda cwd: isinstance(cwd, Path))
|
|
204
253
|
def should_respawn(cwd: Path) -> tuple[bool, Path | None]:
|
|
205
254
|
"""Check if MCP server should re-spawn with project Python.
|
invar/shell/testing.py
CHANGED
|
@@ -194,7 +194,8 @@ def run_doctests_on_files(
|
|
|
194
194
|
capture_output=True,
|
|
195
195
|
text=True,
|
|
196
196
|
timeout=timeout,
|
|
197
|
-
|
|
197
|
+
cwd=str(cwd) if cwd is not None else None,
|
|
198
|
+
env=build_subprocess_env(cwd=cwd),
|
|
198
199
|
)
|
|
199
200
|
# Pytest exit codes: 0=passed, 5=no tests collected (also OK)
|
|
200
201
|
is_passed = result.returncode in (0, 5)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: invar-tools
|
|
3
|
-
Version: 1.17.
|
|
3
|
+
Version: 1.17.22
|
|
4
4
|
Summary: AI-native software engineering tools with design-by-contract verification
|
|
5
5
|
Project-URL: Homepage, https://github.com/tefx/invar
|
|
6
6
|
Project-URL: Documentation, https://github.com/tefx/invar#readme
|
|
@@ -14,7 +14,7 @@ invar/core/hypothesis_strategies.py,sha256=_MfjG7KxkmJvuPsczr_1JayR_YmiDzU2jJ8fQ
|
|
|
14
14
|
invar/core/inspect.py,sha256=l1knohwpLRHSNySPUjyeBHJusnU0vYiQGj4dMVgQZIo,4381
|
|
15
15
|
invar/core/lambda_helpers.py,sha256=Ap1y7N0wpgCgPHwrs2pd7zD9Qq4Ptfd2iTliprXIkME,6457
|
|
16
16
|
invar/core/language.py,sha256=aGUcrq--eQtAjb5bYE40eFmDhRs_EbBNGQ1sBYgTdt0,2637
|
|
17
|
-
invar/core/models.py,sha256=
|
|
17
|
+
invar/core/models.py,sha256=WLHCd9M8anw5mALTMboTjxt5PcXO7eJajiYA8b6FGHk,16881
|
|
18
18
|
invar/core/must_use.py,sha256=7HnnbT53lb4dOT-1mL64pz0JbQYytuw4eejNVe7iWKY,5496
|
|
19
19
|
invar/core/parser.py,sha256=ucVpGziVzUvbkXT1n_SgOrYdStDEcNBqLuRGqK3_M5g,9205
|
|
20
20
|
invar/core/postcondition_scope.py,sha256=ykjVNqZZ1zItBmI7ebgmLW5vFGE-vpaLRTvSgWaJMgM,5245
|
|
@@ -24,7 +24,7 @@ invar/core/purity_heuristics.py,sha256=vsgphC1XPIFtsoLB0xvp--AyaJHqlh83LyKXYda4p
|
|
|
24
24
|
invar/core/references.py,sha256=64yGIdj9vL72Y4uUhJsi9pztZkuMnLN-7OcOziyxYMo,6339
|
|
25
25
|
invar/core/review_trigger.py,sha256=4GGHUmgbVsQJAob4OO6A8G7KrLcNMwNOuqHiT6Jc7cs,14085
|
|
26
26
|
invar/core/rule_meta.py,sha256=il_KUTjSlW1MOVgLguuLDS9wEdyqUe3CDvUx4gQjACo,10180
|
|
27
|
-
invar/core/rules.py,sha256=
|
|
27
|
+
invar/core/rules.py,sha256=y_NIpO8GAcw8WNPZjbJniu9FU7ofgfn3kZ_keexTqA8,23481
|
|
28
28
|
invar/core/shell_analysis.py,sha256=i2A9SMqBI3Rb4Ai0QNTM7awIkSJIY6yZJVWS72lv0bY,6457
|
|
29
29
|
invar/core/shell_architecture.py,sha256=98EVdBFIs8tO-i9jKuzdmv7fLB4PKnyI-vKh5lxnB98,6538
|
|
30
30
|
invar/core/strategies.py,sha256=2DPl0z2p_CBNd4RlSbZzTeAy6Dq6cpCiBCB2p5qHHkk,8798
|
|
@@ -2673,15 +2673,15 @@ invar/shell/pi_tools.py,sha256=a3ACDDXykFV8fUB5UpBmgMvppwkmLvT1k_BWm0IY47k,4068
|
|
|
2673
2673
|
invar/shell/property_tests.py,sha256=SB-3tS5TupYQP1jpEUNpQs6fWCwLWxck0akNsgVwGGM,10533
|
|
2674
2674
|
invar/shell/py_refs.py,sha256=Vjz50lmt9prDBcBv4nkkODdiJ7_DKu5zO4UPZBjAfmM,4638
|
|
2675
2675
|
invar/shell/skill_manager.py,sha256=Mr7Mh9rxPSKSAOTJCAM5ZHiG5nfUf6KQVCuD4LBNHSI,12440
|
|
2676
|
-
invar/shell/subprocess_env.py,sha256=
|
|
2676
|
+
invar/shell/subprocess_env.py,sha256=hendEERSyAG4a8UFhYfPtOAlfspVRB03aVCYpj3uqk4,12745
|
|
2677
2677
|
invar/shell/template_engine.py,sha256=eNKMz7R8g9Xp3_1TGx-QH137jf52E0u3KaVcnotu1Tg,12056
|
|
2678
2678
|
invar/shell/templates.py,sha256=ilhGysbUcdkUFqPgv6ySVmKI3imS_cwYNCWTCdyb5cY,15407
|
|
2679
|
-
invar/shell/testing.py,sha256=
|
|
2679
|
+
invar/shell/testing.py,sha256=mRk22-B3pkNWqPxQ_kjcnK8S6X9G6BQQd3bgdxS0zl8,11534
|
|
2680
2680
|
invar/shell/ts_compiler.py,sha256=nA8brnOhThj9J_J3vAEGjDsM4NjbWQ_eX8Yf4pHPOgk,6672
|
|
2681
2681
|
invar/shell/commands/__init__.py,sha256=MEkKwVyjI9DmkvBpJcuumXo2Pg_FFkfEr-Rr3nrAt7A,284
|
|
2682
2682
|
invar/shell/commands/doc.py,sha256=SOLDoCXXGxx_JU0PKXlAIGEF36PzconHmmAtL-rM6D4,13819
|
|
2683
2683
|
invar/shell/commands/feedback.py,sha256=lLxEeWW_71US_vlmorFrGXS8IARB9nbV6D0zruLs660,7640
|
|
2684
|
-
invar/shell/commands/guard.py,sha256=
|
|
2684
|
+
invar/shell/commands/guard.py,sha256=iGqy6F41ojTYVk8EvGCbRMHDBEcS0tEu3kV0DRjdyqg,26105
|
|
2685
2685
|
invar/shell/commands/hooks.py,sha256=W-SOnT4VQyUvXwipozkJwgEYfiOJGz7wksrbcdWegUg,2356
|
|
2686
2686
|
invar/shell/commands/init.py,sha256=rtoPFsfq7xRZ6lfTipWT1OejNK5wfzqu1ncXi1kizU0,23634
|
|
2687
2687
|
invar/shell/commands/merge.py,sha256=nuvKo8m32-OL-SCQlS4SLKmOZxQ3qj-1nGCx1Pgzifw,8183
|
|
@@ -2696,7 +2696,7 @@ invar/shell/commands/update.py,sha256=-NNQQScEb_0bV1B8YFxTjpUECk9c8dGAiNVRc64nWh
|
|
|
2696
2696
|
invar/shell/prove/__init__.py,sha256=ZqlbmyMFJf6yAle8634jFuPRv8wNvHps8loMlOJyf8A,240
|
|
2697
2697
|
invar/shell/prove/accept.py,sha256=cnY_6jzU1EBnpLF8-zWUWcXiSXtCwxPsXEYXsSVPG38,3717
|
|
2698
2698
|
invar/shell/prove/cache.py,sha256=jbNdrvfLjvK7S0iqugErqeabb4YIbQuwIlcSRyCKbcg,4105
|
|
2699
|
-
invar/shell/prove/crosshair.py,sha256=
|
|
2699
|
+
invar/shell/prove/crosshair.py,sha256=w0p7aT_TscHpAOKKZtlbta46O9EUWRfvQ2qG0OMXjCE,21970
|
|
2700
2700
|
invar/shell/prove/guard_ts.py,sha256=8InEWFd6oqFxBUEaAdlqOvPi43p0VZKxJIrAaF0eReU,37936
|
|
2701
2701
|
invar/shell/prove/hypothesis.py,sha256=QUclOOUg_VB6wbmHw8O2EPiL5qBOeBRqQeM04AVuLw0,9880
|
|
2702
2702
|
invar/templates/CLAUDE.md.template,sha256=eaGU3SyRO_NEifw5b26k3srgQH4jyeujjCJ-HbM36_w,4913
|
|
@@ -2778,10 +2778,10 @@ invar/templates/skills/invar-reflect/template.md,sha256=Rr5hvbllvmd8jSLf_0ZjyKt6
|
|
|
2778
2778
|
invar/templates/skills/investigate/SKILL.md.jinja,sha256=cp6TBEixBYh1rLeeHOR1yqEnFqv1NZYePORMnavLkQI,3231
|
|
2779
2779
|
invar/templates/skills/propose/SKILL.md.jinja,sha256=6BuKiCqO1AEu3VtzMHy1QWGqr_xqG9eJlhbsKT4jev4,3463
|
|
2780
2780
|
invar/templates/skills/review/SKILL.md.jinja,sha256=ET5mbdSe_eKgJbi2LbgFC-z1aviKcHOBw7J5Q28fr4U,14105
|
|
2781
|
-
invar_tools-1.17.
|
|
2782
|
-
invar_tools-1.17.
|
|
2783
|
-
invar_tools-1.17.
|
|
2784
|
-
invar_tools-1.17.
|
|
2785
|
-
invar_tools-1.17.
|
|
2786
|
-
invar_tools-1.17.
|
|
2787
|
-
invar_tools-1.17.
|
|
2781
|
+
invar_tools-1.17.22.dist-info/METADATA,sha256=LcLYpawRN5vHSOQWJkxa1MH8TIMhTsROH0XBK1J5PCQ,28582
|
|
2782
|
+
invar_tools-1.17.22.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
2783
|
+
invar_tools-1.17.22.dist-info/entry_points.txt,sha256=RwH_EhqgtFPsnO6RcrwrAb70Zyfb8Mh6uUtztWnUxGk,102
|
|
2784
|
+
invar_tools-1.17.22.dist-info/licenses/LICENSE,sha256=qeFksp4H4kfTgQxPCIu3OdagXyiZcgBlVfsQ6M5oFyk,10767
|
|
2785
|
+
invar_tools-1.17.22.dist-info/licenses/LICENSE-GPL,sha256=IvZfC6ZbP7CLjytoHVzvpDZpD-Z3R_qa1GdMdWlWQ6Q,35157
|
|
2786
|
+
invar_tools-1.17.22.dist-info/licenses/NOTICE,sha256=joEyMyFhFY8Vd8tTJ-a3SirI0m2Sd0WjzqYt3sdcglc,2561
|
|
2787
|
+
invar_tools-1.17.22.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|