invar-tools 1.0.0__py3-none-any.whl → 1.3.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.
- invar/__init__.py +1 -0
- invar/core/contracts.py +80 -10
- invar/core/entry_points.py +367 -0
- invar/core/extraction.py +5 -6
- invar/core/format_specs.py +195 -0
- invar/core/format_strategies.py +197 -0
- invar/core/formatter.py +32 -10
- invar/core/hypothesis_strategies.py +50 -10
- invar/core/inspect.py +1 -1
- invar/core/lambda_helpers.py +3 -2
- invar/core/models.py +30 -18
- invar/core/must_use.py +2 -1
- invar/core/parser.py +13 -6
- invar/core/postcondition_scope.py +128 -0
- invar/core/property_gen.py +86 -42
- invar/core/purity.py +13 -7
- invar/core/purity_heuristics.py +5 -9
- invar/core/references.py +8 -6
- invar/core/review_trigger.py +370 -0
- invar/core/rule_meta.py +69 -2
- invar/core/rules.py +91 -28
- invar/core/shell_analysis.py +247 -0
- invar/core/shell_architecture.py +171 -0
- invar/core/strategies.py +7 -14
- invar/core/suggestions.py +92 -0
- invar/core/sync_helpers.py +238 -0
- invar/core/tautology.py +103 -37
- invar/core/template_parser.py +467 -0
- invar/core/timeout_inference.py +4 -7
- invar/core/utils.py +63 -18
- invar/core/verification_routing.py +155 -0
- invar/mcp/server.py +113 -13
- invar/shell/commands/__init__.py +11 -0
- invar/shell/{cli.py → commands/guard.py} +152 -44
- invar/shell/{init_cmd.py → commands/init.py} +200 -28
- invar/shell/commands/merge.py +256 -0
- invar/shell/commands/mutate.py +184 -0
- invar/shell/{perception.py → commands/perception.py} +2 -0
- invar/shell/commands/sync_self.py +113 -0
- invar/shell/commands/template_sync.py +366 -0
- invar/shell/{test_cmd.py → commands/test.py} +3 -1
- invar/shell/commands/update.py +48 -0
- invar/shell/config.py +247 -10
- invar/shell/coverage.py +351 -0
- invar/shell/fs.py +5 -2
- invar/shell/git.py +2 -0
- invar/shell/guard_helpers.py +116 -20
- invar/shell/guard_output.py +106 -24
- invar/shell/mcp_config.py +3 -0
- invar/shell/mutation.py +314 -0
- invar/shell/property_tests.py +75 -24
- invar/shell/prove/__init__.py +9 -0
- invar/shell/prove/accept.py +113 -0
- invar/shell/{prove.py → prove/crosshair.py} +69 -30
- invar/shell/prove/hypothesis.py +293 -0
- invar/shell/subprocess_env.py +393 -0
- invar/shell/template_engine.py +345 -0
- invar/shell/templates.py +53 -0
- invar/shell/testing.py +77 -37
- invar/templates/CLAUDE.md.template +86 -9
- invar/templates/aider.conf.yml.template +16 -14
- invar/templates/commands/audit.md +138 -0
- invar/templates/commands/guard.md +77 -0
- invar/templates/config/CLAUDE.md.jinja +206 -0
- invar/templates/config/context.md.jinja +92 -0
- invar/templates/config/pre-commit.yaml.jinja +44 -0
- invar/templates/context.md.template +33 -0
- invar/templates/cursorrules.template +25 -13
- invar/templates/examples/README.md +2 -0
- invar/templates/examples/conftest.py +3 -0
- invar/templates/examples/contracts.py +4 -2
- invar/templates/examples/core_shell.py +10 -4
- invar/templates/examples/workflow.md +81 -0
- invar/templates/manifest.toml +137 -0
- invar/templates/protocol/INVAR.md +210 -0
- invar/templates/skills/develop/SKILL.md.jinja +318 -0
- invar/templates/skills/investigate/SKILL.md.jinja +106 -0
- invar/templates/skills/propose/SKILL.md.jinja +104 -0
- invar/templates/skills/review/SKILL.md.jinja +125 -0
- invar_tools-1.3.0.dist-info/METADATA +377 -0
- invar_tools-1.3.0.dist-info/RECORD +95 -0
- invar_tools-1.3.0.dist-info/entry_points.txt +2 -0
- invar_tools-1.3.0.dist-info/licenses/LICENSE +190 -0
- invar_tools-1.3.0.dist-info/licenses/LICENSE-GPL +674 -0
- invar_tools-1.3.0.dist-info/licenses/NOTICE +63 -0
- invar/contracts.py +0 -152
- invar/decorators.py +0 -94
- invar/invariant.py +0 -57
- invar/resource.py +0 -99
- invar/shell/prove_fallback.py +0 -183
- invar/shell/update_cmd.py +0 -191
- invar/templates/INVAR.md +0 -134
- invar_tools-1.0.0.dist-info/METADATA +0 -321
- invar_tools-1.0.0.dist-info/RECORD +0 -64
- invar_tools-1.0.0.dist-info/entry_points.txt +0 -2
- invar_tools-1.0.0.dist-info/licenses/LICENSE +0 -21
- /invar/shell/{prove_cache.py → prove/cache.py} +0 -0
- {invar_tools-1.0.0.dist-info → invar_tools-1.3.0.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
Invar
|
|
2
|
+
Copyright 2025 Invar Contributors
|
|
3
|
+
|
|
4
|
+
This product includes software developed by the Invar project
|
|
5
|
+
(https://github.com/Tefx/Invar).
|
|
6
|
+
|
|
7
|
+
================================================================================
|
|
8
|
+
|
|
9
|
+
This project uses the following third-party libraries:
|
|
10
|
+
|
|
11
|
+
--------------------------------------------------------------------------------
|
|
12
|
+
deal - Design by Contract library
|
|
13
|
+
License: LGPL-3.0-or-later
|
|
14
|
+
https://github.com/life4/deal
|
|
15
|
+
--------------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
--------------------------------------------------------------------------------
|
|
18
|
+
hypothesis - Property-based testing library
|
|
19
|
+
License: MPL-2.0
|
|
20
|
+
https://github.com/HypothesisWorks/hypothesis
|
|
21
|
+
--------------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
--------------------------------------------------------------------------------
|
|
24
|
+
crosshair-tool - Symbolic execution analysis
|
|
25
|
+
License: MIT
|
|
26
|
+
https://github.com/pschanely/CrossHair
|
|
27
|
+
--------------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
--------------------------------------------------------------------------------
|
|
30
|
+
typer - CLI framework
|
|
31
|
+
License: MIT
|
|
32
|
+
https://github.com/tiangolo/typer
|
|
33
|
+
--------------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
--------------------------------------------------------------------------------
|
|
36
|
+
rich - Terminal formatting library
|
|
37
|
+
License: MIT
|
|
38
|
+
https://github.com/Textualize/rich
|
|
39
|
+
--------------------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
--------------------------------------------------------------------------------
|
|
42
|
+
pydantic - Data validation library
|
|
43
|
+
License: MIT
|
|
44
|
+
https://github.com/pydantic/pydantic
|
|
45
|
+
--------------------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
--------------------------------------------------------------------------------
|
|
48
|
+
returns - Functional programming primitives
|
|
49
|
+
License: BSD-2-Clause
|
|
50
|
+
https://github.com/dry-python/returns
|
|
51
|
+
--------------------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
--------------------------------------------------------------------------------
|
|
54
|
+
mcp - Model Context Protocol SDK
|
|
55
|
+
License: MIT
|
|
56
|
+
https://github.com/modelcontextprotocol/python-sdk
|
|
57
|
+
--------------------------------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
--------------------------------------------------------------------------------
|
|
60
|
+
pre-commit - Git hooks framework
|
|
61
|
+
License: MIT
|
|
62
|
+
https://github.com/pre-commit/pre-commit
|
|
63
|
+
--------------------------------------------------------------------------------
|
invar/contracts.py
DELETED
|
@@ -1,152 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Composable contracts for Invar.
|
|
3
|
-
|
|
4
|
-
Provides Contract class with &, |, ~ operators for combining conditions,
|
|
5
|
-
and a standard library of common predicates. Works with deal decorators.
|
|
6
|
-
|
|
7
|
-
Inspired by Idris' dependent types.
|
|
8
|
-
"""
|
|
9
|
-
|
|
10
|
-
from __future__ import annotations
|
|
11
|
-
|
|
12
|
-
from dataclasses import dataclass
|
|
13
|
-
from typing import TYPE_CHECKING, Any
|
|
14
|
-
|
|
15
|
-
import deal
|
|
16
|
-
|
|
17
|
-
if TYPE_CHECKING:
|
|
18
|
-
from collections.abc import Callable
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
@dataclass
|
|
22
|
-
class Contract:
|
|
23
|
-
"""
|
|
24
|
-
Composable contract with &, |, ~ operators.
|
|
25
|
-
|
|
26
|
-
Contracts encapsulate predicates that can be combined and reused.
|
|
27
|
-
Works with deal.pre for runtime checking.
|
|
28
|
-
|
|
29
|
-
Examples:
|
|
30
|
-
>>> NonEmpty = Contract(lambda x: len(x) > 0, "non-empty")
|
|
31
|
-
>>> Sorted = Contract(lambda x: list(x) == sorted(x), "sorted")
|
|
32
|
-
>>> combined = NonEmpty & Sorted
|
|
33
|
-
>>> combined.check([1, 2, 3])
|
|
34
|
-
True
|
|
35
|
-
>>> combined.check([])
|
|
36
|
-
False
|
|
37
|
-
>>> combined.check([3, 1, 2])
|
|
38
|
-
False
|
|
39
|
-
>>> (NonEmpty | Sorted).check([]) # Empty but sorted
|
|
40
|
-
True
|
|
41
|
-
>>> (~NonEmpty).check([]) # NOT non-empty
|
|
42
|
-
True
|
|
43
|
-
"""
|
|
44
|
-
|
|
45
|
-
predicate: Callable[[Any], bool]
|
|
46
|
-
description: str
|
|
47
|
-
|
|
48
|
-
def check(self, value: Any) -> bool:
|
|
49
|
-
"""Check if value satisfies the contract."""
|
|
50
|
-
return self.predicate(value)
|
|
51
|
-
|
|
52
|
-
def __and__(self, other: Contract) -> Contract:
|
|
53
|
-
"""Combine contracts with AND."""
|
|
54
|
-
return Contract(
|
|
55
|
-
predicate=lambda x: self.check(x) and other.check(x),
|
|
56
|
-
description=f"({self.description} AND {other.description})",
|
|
57
|
-
)
|
|
58
|
-
|
|
59
|
-
def __or__(self, other: Contract) -> Contract:
|
|
60
|
-
"""Combine contracts with OR."""
|
|
61
|
-
return Contract(
|
|
62
|
-
predicate=lambda x: self.check(x) or other.check(x),
|
|
63
|
-
description=f"({self.description} OR {other.description})",
|
|
64
|
-
)
|
|
65
|
-
|
|
66
|
-
def __invert__(self) -> Contract:
|
|
67
|
-
"""Negate the contract."""
|
|
68
|
-
return Contract(
|
|
69
|
-
predicate=lambda x: not self.check(x),
|
|
70
|
-
description=f"NOT({self.description})",
|
|
71
|
-
)
|
|
72
|
-
|
|
73
|
-
def __call__(self, *args: Any, **kwargs: Any) -> bool:
|
|
74
|
-
"""Allow using as deal.pre predicate directly."""
|
|
75
|
-
value = args[0] if args else next(iter(kwargs.values()))
|
|
76
|
-
return self.check(value)
|
|
77
|
-
|
|
78
|
-
def __repr__(self) -> str:
|
|
79
|
-
return f"Contract({self.description!r})"
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
def pre(*contracts: Contract) -> Callable[[Callable], Callable]:
|
|
83
|
-
"""
|
|
84
|
-
Decorator accepting Contract objects for preconditions.
|
|
85
|
-
|
|
86
|
-
Works with deal.pre under the hood.
|
|
87
|
-
|
|
88
|
-
Examples:
|
|
89
|
-
>>> from invar.contracts import pre, NonEmpty
|
|
90
|
-
>>> @pre(NonEmpty)
|
|
91
|
-
... def first(xs): return xs[0]
|
|
92
|
-
>>> first([1, 2, 3])
|
|
93
|
-
1
|
|
94
|
-
"""
|
|
95
|
-
|
|
96
|
-
def combined(*args: Any, **kwargs: Any) -> bool:
|
|
97
|
-
value = args[0] if args else next(iter(kwargs.values()))
|
|
98
|
-
return all(c.check(value) for c in contracts)
|
|
99
|
-
|
|
100
|
-
return deal.pre(combined)
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
def post(*contracts: Contract) -> Callable[[Callable], Callable]:
|
|
104
|
-
"""
|
|
105
|
-
Decorator accepting Contract objects for postconditions.
|
|
106
|
-
|
|
107
|
-
Works with deal.post under the hood.
|
|
108
|
-
|
|
109
|
-
Examples:
|
|
110
|
-
>>> from invar.contracts import post, NonEmpty
|
|
111
|
-
>>> @post(NonEmpty)
|
|
112
|
-
... def get_list(): return [1]
|
|
113
|
-
>>> get_list()
|
|
114
|
-
[1]
|
|
115
|
-
"""
|
|
116
|
-
|
|
117
|
-
def combined(result: Any) -> bool:
|
|
118
|
-
return all(c.check(result) for c in contracts)
|
|
119
|
-
|
|
120
|
-
return deal.post(combined)
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
# =============================================================================
|
|
124
|
-
# Standard Library of Contracts
|
|
125
|
-
# =============================================================================
|
|
126
|
-
|
|
127
|
-
# --- Collections ---
|
|
128
|
-
NonEmpty: Contract = Contract(lambda x: len(x) > 0, "non-empty")
|
|
129
|
-
Sorted: Contract = Contract(lambda x: list(x) == sorted(x), "sorted")
|
|
130
|
-
Unique: Contract = Contract(lambda x: len(x) == len(set(x)), "unique")
|
|
131
|
-
SortedNonEmpty: Contract = NonEmpty & Sorted
|
|
132
|
-
|
|
133
|
-
# --- Numbers ---
|
|
134
|
-
Positive: Contract = Contract(lambda x: x > 0, "positive")
|
|
135
|
-
NonNegative: Contract = Contract(lambda x: x >= 0, "non-negative")
|
|
136
|
-
Negative: Contract = Contract(lambda x: x < 0, "negative")
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
def InRange(lo: float, hi: float) -> Contract:
|
|
140
|
-
"""Create a contract checking value is in [lo, hi]."""
|
|
141
|
-
return Contract(lambda x: lo <= x <= hi, f"[{lo},{hi}]")
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
Percentage: Contract = InRange(0, 100)
|
|
145
|
-
|
|
146
|
-
# --- Strings ---
|
|
147
|
-
NonBlank: Contract = Contract(lambda s: bool(s and s.strip()), "non-blank")
|
|
148
|
-
|
|
149
|
-
# --- Lists with elements ---
|
|
150
|
-
AllPositive: Contract = Contract(lambda xs: all(x > 0 for x in xs), "all positive")
|
|
151
|
-
AllNonNegative: Contract = Contract(lambda xs: all(x >= 0 for x in xs), "all non-negative")
|
|
152
|
-
NoNone: Contract = Contract(lambda xs: None not in xs, "no None")
|
invar/decorators.py
DELETED
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Invar contract decorators.
|
|
3
|
-
|
|
4
|
-
Provides decorators that extend deal's contract system for additional
|
|
5
|
-
static analysis by Guard.
|
|
6
|
-
|
|
7
|
-
DX-12-B: Added @strategy for custom Hypothesis strategies.
|
|
8
|
-
"""
|
|
9
|
-
|
|
10
|
-
from __future__ import annotations
|
|
11
|
-
|
|
12
|
-
from collections.abc import Callable
|
|
13
|
-
from typing import Any, TypeVar
|
|
14
|
-
|
|
15
|
-
F = TypeVar("F", bound=Callable)
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def must_use(reason: str | None = None) -> Callable[[F], F]:
|
|
19
|
-
"""
|
|
20
|
-
Mark a function's return value as must-use.
|
|
21
|
-
|
|
22
|
-
Guard will warn if calls to this function discard the return value.
|
|
23
|
-
Inspired by Move's lack of drop ability and Rust's #[must_use].
|
|
24
|
-
|
|
25
|
-
Args:
|
|
26
|
-
reason: Explanation of why the return value must be used.
|
|
27
|
-
|
|
28
|
-
Returns:
|
|
29
|
-
A decorator that marks the function.
|
|
30
|
-
|
|
31
|
-
>>> @must_use("Error must be handled")
|
|
32
|
-
... def may_fail() -> int:
|
|
33
|
-
... return 42
|
|
34
|
-
>>> may_fail.__invar_must_use__
|
|
35
|
-
'Error must be handled'
|
|
36
|
-
|
|
37
|
-
>>> @must_use()
|
|
38
|
-
... def important() -> str:
|
|
39
|
-
... return "result"
|
|
40
|
-
>>> important.__invar_must_use__
|
|
41
|
-
'Return value must be used'
|
|
42
|
-
"""
|
|
43
|
-
|
|
44
|
-
def decorator(func: F) -> F:
|
|
45
|
-
func.__invar_must_use__ = reason or "Return value must be used" # type: ignore[attr-defined]
|
|
46
|
-
return func
|
|
47
|
-
|
|
48
|
-
return decorator
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
def strategy(**param_strategies: Any) -> Callable[[F], F]:
|
|
52
|
-
"""
|
|
53
|
-
Specify custom Hypothesis strategies for function parameters.
|
|
54
|
-
|
|
55
|
-
DX-12-B: Escape hatch when automatic strategy inference fails or
|
|
56
|
-
when you need precise control over generated values.
|
|
57
|
-
|
|
58
|
-
Args:
|
|
59
|
-
**param_strategies: Mapping of parameter names to Hypothesis strategies.
|
|
60
|
-
|
|
61
|
-
Returns:
|
|
62
|
-
A decorator that attaches strategies to the function.
|
|
63
|
-
|
|
64
|
-
Examples:
|
|
65
|
-
>>> from hypothesis import strategies as st
|
|
66
|
-
>>> @strategy(x=st.floats(min_value=1e-10, max_value=1e10))
|
|
67
|
-
... def sqrt(x: float) -> float:
|
|
68
|
-
... return x ** 0.5
|
|
69
|
-
>>> hasattr(sqrt, '__invar_strategies__')
|
|
70
|
-
True
|
|
71
|
-
>>> 'x' in sqrt.__invar_strategies__
|
|
72
|
-
True
|
|
73
|
-
|
|
74
|
-
>>> # NumPy array with specific shape
|
|
75
|
-
>>> @strategy(arr="arrays(dtype=float64, shape=(10,))")
|
|
76
|
-
... def normalize(arr):
|
|
77
|
-
... return arr / arr.sum()
|
|
78
|
-
>>> 'arr' in normalize.__invar_strategies__
|
|
79
|
-
True
|
|
80
|
-
|
|
81
|
-
Note:
|
|
82
|
-
Strategies can be either:
|
|
83
|
-
- Hypothesis strategy objects (e.g., st.floats())
|
|
84
|
-
- String representations (e.g., "floats(min_value=0)")
|
|
85
|
-
|
|
86
|
-
String representations are useful when you don't want to import
|
|
87
|
-
Hypothesis at module load time.
|
|
88
|
-
"""
|
|
89
|
-
|
|
90
|
-
def decorator(func: F) -> F:
|
|
91
|
-
func.__invar_strategies__ = param_strategies # type: ignore[attr-defined]
|
|
92
|
-
return func
|
|
93
|
-
|
|
94
|
-
return decorator
|
invar/invariant.py
DELETED
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Loop invariant support for Invar.
|
|
3
|
-
|
|
4
|
-
Provides runtime-checked loop invariants inspired by Dafny.
|
|
5
|
-
Checking controlled by INVAR_CHECK environment variable (default: ON).
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
from __future__ import annotations
|
|
9
|
-
|
|
10
|
-
import os
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
class InvariantViolation(Exception):
|
|
14
|
-
"""Raised when a loop invariant is violated."""
|
|
15
|
-
|
|
16
|
-
pass
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
# Read once at module load time - effectively a constant
|
|
20
|
-
_INVAR_CHECK = os.environ.get("INVAR_CHECK", "1") == "1"
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def invariant(condition: bool, message: str = "") -> None:
|
|
24
|
-
"""
|
|
25
|
-
Assert loop invariant. Checked at runtime when INVAR_CHECK=1.
|
|
26
|
-
|
|
27
|
-
Place at the START of loop body to check condition each iteration.
|
|
28
|
-
Invariants document what must remain true throughout loop execution.
|
|
29
|
-
|
|
30
|
-
Args:
|
|
31
|
-
condition: Boolean condition that must hold
|
|
32
|
-
message: Optional message describing the invariant
|
|
33
|
-
|
|
34
|
-
Raises:
|
|
35
|
-
InvariantViolation: When condition is False and INVAR_CHECK=1
|
|
36
|
-
|
|
37
|
-
Examples:
|
|
38
|
-
>>> invariant(True) # OK
|
|
39
|
-
|
|
40
|
-
>>> invariant(True, "x is positive") # OK with message
|
|
41
|
-
|
|
42
|
-
>>> try:
|
|
43
|
-
... invariant(False, "x must be positive")
|
|
44
|
-
... except InvariantViolation as e:
|
|
45
|
-
... print(str(e))
|
|
46
|
-
Loop invariant violated: x must be positive
|
|
47
|
-
|
|
48
|
-
Typical usage in a binary search:
|
|
49
|
-
|
|
50
|
-
while lo < hi:
|
|
51
|
-
invariant(0 <= lo <= hi <= len(arr))
|
|
52
|
-
invariant(target not in arr[:lo]) # Already searched
|
|
53
|
-
...
|
|
54
|
-
"""
|
|
55
|
-
if _INVAR_CHECK and not condition:
|
|
56
|
-
msg = f"Loop invariant violated: {message}" if message else "Loop invariant violated"
|
|
57
|
-
raise InvariantViolation(msg)
|
invar/resource.py
DELETED
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Resource management decorators for Invar.
|
|
3
|
-
|
|
4
|
-
Provides @must_close for marking classes that require explicit cleanup.
|
|
5
|
-
Inspired by Move language's resource semantics.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
from __future__ import annotations
|
|
9
|
-
|
|
10
|
-
from typing import Any, TypeVar
|
|
11
|
-
|
|
12
|
-
T = TypeVar("T")
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
class ResourceWarning(UserWarning):
|
|
16
|
-
"""Warning raised when a resource may not be properly closed."""
|
|
17
|
-
|
|
18
|
-
pass
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
class MustCloseViolation(Exception):
|
|
22
|
-
"""Raised when a @must_close resource is not properly managed."""
|
|
23
|
-
|
|
24
|
-
pass
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def must_close(cls: type[T]) -> type[T]:
|
|
28
|
-
"""
|
|
29
|
-
Mark a class as a resource that must be explicitly closed.
|
|
30
|
-
|
|
31
|
-
The decorated class should have a `close()` method. The decorator:
|
|
32
|
-
1. Adds __invar_must_close__ marker for Guard detection
|
|
33
|
-
2. Adds context manager protocol if not present
|
|
34
|
-
|
|
35
|
-
Examples:
|
|
36
|
-
>>> @must_close
|
|
37
|
-
... class TempFile:
|
|
38
|
-
... def __init__(self, path: str):
|
|
39
|
-
... self.path = path
|
|
40
|
-
... self.closed = False
|
|
41
|
-
... def write(self, data: str) -> None:
|
|
42
|
-
... if self.closed:
|
|
43
|
-
... raise ValueError("File is closed")
|
|
44
|
-
... def close(self) -> None:
|
|
45
|
-
... self.closed = True
|
|
46
|
-
|
|
47
|
-
>>> # Preferred: use as context manager
|
|
48
|
-
>>> with TempFile("test.txt") as f:
|
|
49
|
-
... f.write("hello")
|
|
50
|
-
>>> f.closed
|
|
51
|
-
True
|
|
52
|
-
|
|
53
|
-
>>> # Also works: explicit close
|
|
54
|
-
>>> f2 = TempFile("test2.txt")
|
|
55
|
-
>>> f2.write("world")
|
|
56
|
-
>>> f2.close()
|
|
57
|
-
>>> f2.closed
|
|
58
|
-
True
|
|
59
|
-
"""
|
|
60
|
-
# Mark for Guard detection
|
|
61
|
-
cls.__invar_must_close__ = True # type: ignore[attr-defined]
|
|
62
|
-
|
|
63
|
-
# Add context manager protocol if not present
|
|
64
|
-
if not hasattr(cls, "__enter__"):
|
|
65
|
-
|
|
66
|
-
def __enter__(self: Any) -> Any:
|
|
67
|
-
return self
|
|
68
|
-
|
|
69
|
-
cls.__enter__ = __enter__ # type: ignore[attr-defined]
|
|
70
|
-
|
|
71
|
-
if not hasattr(cls, "__exit__"):
|
|
72
|
-
|
|
73
|
-
def __exit__(self: Any, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
|
74
|
-
if hasattr(self, "close") and callable(self.close):
|
|
75
|
-
self.close()
|
|
76
|
-
|
|
77
|
-
cls.__exit__ = __exit__ # type: ignore[attr-defined]
|
|
78
|
-
|
|
79
|
-
return cls
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
def is_must_close(cls_or_obj: Any) -> bool:
|
|
83
|
-
"""
|
|
84
|
-
Check if a class or instance is marked with @must_close.
|
|
85
|
-
|
|
86
|
-
>>> @must_close
|
|
87
|
-
... class Resource:
|
|
88
|
-
... def close(self): pass
|
|
89
|
-
>>> is_must_close(Resource)
|
|
90
|
-
True
|
|
91
|
-
>>> is_must_close(Resource())
|
|
92
|
-
True
|
|
93
|
-
>>> class Plain: pass
|
|
94
|
-
>>> is_must_close(Plain)
|
|
95
|
-
False
|
|
96
|
-
"""
|
|
97
|
-
if isinstance(cls_or_obj, type):
|
|
98
|
-
return getattr(cls_or_obj, "__invar_must_close__", False)
|
|
99
|
-
return getattr(type(cls_or_obj), "__invar_must_close__", False)
|
invar/shell/prove_fallback.py
DELETED
|
@@ -1,183 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Hypothesis fallback for proof verification.
|
|
3
|
-
|
|
4
|
-
DX-12: Provides Hypothesis as automatic fallback when CrossHair
|
|
5
|
-
is unavailable, times out, or skips files.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
from __future__ import annotations
|
|
9
|
-
|
|
10
|
-
import subprocess
|
|
11
|
-
import sys
|
|
12
|
-
from pathlib import Path
|
|
13
|
-
|
|
14
|
-
from returns.result import Failure, Result, Success
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
def run_hypothesis_fallback(
|
|
18
|
-
files: list[Path],
|
|
19
|
-
max_examples: int = 100,
|
|
20
|
-
) -> Result[dict, str]:
|
|
21
|
-
"""
|
|
22
|
-
Run Hypothesis property tests as fallback when CrossHair skips/times out.
|
|
23
|
-
|
|
24
|
-
DX-12: Uses inferred strategies from type hints and @pre contracts.
|
|
25
|
-
|
|
26
|
-
Args:
|
|
27
|
-
files: List of Python file paths to test
|
|
28
|
-
max_examples: Maximum examples per test
|
|
29
|
-
|
|
30
|
-
Returns:
|
|
31
|
-
Success with test results or Failure with error message
|
|
32
|
-
"""
|
|
33
|
-
# Import CrossHairStatus here to avoid circular import
|
|
34
|
-
from invar.shell.prove import CrossHairStatus
|
|
35
|
-
|
|
36
|
-
# Check if hypothesis is available
|
|
37
|
-
try:
|
|
38
|
-
import hypothesis # noqa: F401
|
|
39
|
-
except ImportError:
|
|
40
|
-
return Success(
|
|
41
|
-
{
|
|
42
|
-
"status": CrossHairStatus.SKIPPED,
|
|
43
|
-
"reason": "Hypothesis not installed (pip install hypothesis)",
|
|
44
|
-
"files": [],
|
|
45
|
-
"tool": "hypothesis",
|
|
46
|
-
}
|
|
47
|
-
)
|
|
48
|
-
|
|
49
|
-
if not files:
|
|
50
|
-
return Success(
|
|
51
|
-
{
|
|
52
|
-
"status": CrossHairStatus.SKIPPED,
|
|
53
|
-
"reason": "no files",
|
|
54
|
-
"files": [],
|
|
55
|
-
"tool": "hypothesis",
|
|
56
|
-
}
|
|
57
|
-
)
|
|
58
|
-
|
|
59
|
-
# Filter to Python files only
|
|
60
|
-
py_files = [f for f in files if f.suffix == ".py" and f.exists()]
|
|
61
|
-
if not py_files:
|
|
62
|
-
return Success(
|
|
63
|
-
{
|
|
64
|
-
"status": CrossHairStatus.SKIPPED,
|
|
65
|
-
"reason": "no Python files",
|
|
66
|
-
"files": [],
|
|
67
|
-
"tool": "hypothesis",
|
|
68
|
-
}
|
|
69
|
-
)
|
|
70
|
-
|
|
71
|
-
# Use pytest with hypothesis
|
|
72
|
-
cmd = [
|
|
73
|
-
sys.executable,
|
|
74
|
-
"-m",
|
|
75
|
-
"pytest",
|
|
76
|
-
"--hypothesis-show-statistics",
|
|
77
|
-
"--hypothesis-seed=0", # Reproducible
|
|
78
|
-
"-x", # Stop on first failure
|
|
79
|
-
"--tb=short",
|
|
80
|
-
]
|
|
81
|
-
cmd.extend(str(f) for f in py_files)
|
|
82
|
-
|
|
83
|
-
try:
|
|
84
|
-
result = subprocess.run(cmd, capture_output=True, text=True, timeout=300)
|
|
85
|
-
# Pytest exit codes: 0=passed, 5=no tests collected
|
|
86
|
-
is_passed = result.returncode in (0, 5)
|
|
87
|
-
return Success(
|
|
88
|
-
{
|
|
89
|
-
"status": "passed" if is_passed else "failed",
|
|
90
|
-
"files": [str(f) for f in py_files],
|
|
91
|
-
"exit_code": result.returncode,
|
|
92
|
-
"stdout": result.stdout,
|
|
93
|
-
"stderr": result.stderr,
|
|
94
|
-
"tool": "hypothesis",
|
|
95
|
-
"note": "Fallback from CrossHair",
|
|
96
|
-
}
|
|
97
|
-
)
|
|
98
|
-
except subprocess.TimeoutExpired:
|
|
99
|
-
return Failure("Hypothesis timeout (300s)")
|
|
100
|
-
except Exception as e:
|
|
101
|
-
return Failure(f"Hypothesis error: {e}")
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
def run_prove_with_fallback(
|
|
105
|
-
files: list[Path],
|
|
106
|
-
crosshair_timeout: int = 10,
|
|
107
|
-
hypothesis_max_examples: int = 100,
|
|
108
|
-
use_cache: bool = True,
|
|
109
|
-
cache_dir: Path | None = None,
|
|
110
|
-
) -> Result[dict, str]:
|
|
111
|
-
"""
|
|
112
|
-
Run proof verification with automatic Hypothesis fallback.
|
|
113
|
-
|
|
114
|
-
DX-12 + DX-13: Tries CrossHair first with optimizations, falls back to Hypothesis.
|
|
115
|
-
|
|
116
|
-
Args:
|
|
117
|
-
files: List of Python file paths to verify
|
|
118
|
-
crosshair_timeout: Ignored (kept for backwards compatibility)
|
|
119
|
-
hypothesis_max_examples: Maximum Hypothesis examples
|
|
120
|
-
use_cache: Whether to use verification cache (DX-13)
|
|
121
|
-
cache_dir: Cache directory (default: .invar/cache/prove)
|
|
122
|
-
|
|
123
|
-
Returns:
|
|
124
|
-
Success with verification results or Failure with error message
|
|
125
|
-
"""
|
|
126
|
-
# Import here to avoid circular import
|
|
127
|
-
from invar.shell.prove import CrossHairStatus, run_crosshair_parallel
|
|
128
|
-
from invar.shell.prove_cache import ProveCache
|
|
129
|
-
|
|
130
|
-
# DX-13: Initialize cache
|
|
131
|
-
cache = None
|
|
132
|
-
if use_cache:
|
|
133
|
-
if cache_dir is None:
|
|
134
|
-
cache_dir = Path(".invar/cache/prove")
|
|
135
|
-
cache = ProveCache(cache_dir=cache_dir)
|
|
136
|
-
|
|
137
|
-
# DX-13: Use parallel CrossHair with caching
|
|
138
|
-
crosshair_result = run_crosshair_parallel(
|
|
139
|
-
files,
|
|
140
|
-
max_iterations=5, # Fast mode
|
|
141
|
-
max_workers=None, # Auto-detect
|
|
142
|
-
cache=cache,
|
|
143
|
-
)
|
|
144
|
-
|
|
145
|
-
if isinstance(crosshair_result, Failure):
|
|
146
|
-
# CrossHair failed, try Hypothesis
|
|
147
|
-
return run_hypothesis_fallback(files, max_examples=hypothesis_max_examples)
|
|
148
|
-
|
|
149
|
-
result_data = crosshair_result.unwrap()
|
|
150
|
-
status = result_data.get("status", "")
|
|
151
|
-
|
|
152
|
-
# Check if we need fallback
|
|
153
|
-
needs_fallback = (
|
|
154
|
-
status == CrossHairStatus.SKIPPED
|
|
155
|
-
or status == CrossHairStatus.TIMEOUT
|
|
156
|
-
or "not installed" in result_data.get("reason", "")
|
|
157
|
-
)
|
|
158
|
-
|
|
159
|
-
if needs_fallback:
|
|
160
|
-
# Run Hypothesis as fallback
|
|
161
|
-
hypothesis_result = run_hypothesis_fallback(
|
|
162
|
-
files, max_examples=hypothesis_max_examples
|
|
163
|
-
)
|
|
164
|
-
|
|
165
|
-
if isinstance(hypothesis_result, Success):
|
|
166
|
-
hyp_data = hypothesis_result.unwrap()
|
|
167
|
-
# Merge results
|
|
168
|
-
return Success(
|
|
169
|
-
{
|
|
170
|
-
"status": hyp_data.get("status", "unknown"),
|
|
171
|
-
"primary_tool": "hypothesis",
|
|
172
|
-
"crosshair_status": status,
|
|
173
|
-
"crosshair_reason": result_data.get("reason", ""),
|
|
174
|
-
"hypothesis_result": hyp_data,
|
|
175
|
-
"files": [str(f) for f in files],
|
|
176
|
-
"note": "CrossHair skipped/unavailable, used Hypothesis fallback",
|
|
177
|
-
}
|
|
178
|
-
)
|
|
179
|
-
return hypothesis_result
|
|
180
|
-
|
|
181
|
-
# CrossHair succeeded (verified or found counterexample)
|
|
182
|
-
result_data["primary_tool"] = "crosshair"
|
|
183
|
-
return Success(result_data)
|