invar-tools 1.0.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.
Files changed (64) hide show
  1. invar/__init__.py +68 -0
  2. invar/contracts.py +152 -0
  3. invar/core/__init__.py +8 -0
  4. invar/core/contracts.py +375 -0
  5. invar/core/extraction.py +172 -0
  6. invar/core/formatter.py +281 -0
  7. invar/core/hypothesis_strategies.py +454 -0
  8. invar/core/inspect.py +154 -0
  9. invar/core/lambda_helpers.py +190 -0
  10. invar/core/models.py +289 -0
  11. invar/core/must_use.py +172 -0
  12. invar/core/parser.py +276 -0
  13. invar/core/property_gen.py +383 -0
  14. invar/core/purity.py +369 -0
  15. invar/core/purity_heuristics.py +184 -0
  16. invar/core/references.py +180 -0
  17. invar/core/rule_meta.py +203 -0
  18. invar/core/rules.py +435 -0
  19. invar/core/strategies.py +267 -0
  20. invar/core/suggestions.py +324 -0
  21. invar/core/tautology.py +137 -0
  22. invar/core/timeout_inference.py +114 -0
  23. invar/core/utils.py +364 -0
  24. invar/decorators.py +94 -0
  25. invar/invariant.py +57 -0
  26. invar/mcp/__init__.py +10 -0
  27. invar/mcp/__main__.py +13 -0
  28. invar/mcp/server.py +251 -0
  29. invar/py.typed +0 -0
  30. invar/resource.py +99 -0
  31. invar/shell/__init__.py +8 -0
  32. invar/shell/cli.py +358 -0
  33. invar/shell/config.py +248 -0
  34. invar/shell/fs.py +112 -0
  35. invar/shell/git.py +85 -0
  36. invar/shell/guard_helpers.py +324 -0
  37. invar/shell/guard_output.py +235 -0
  38. invar/shell/init_cmd.py +289 -0
  39. invar/shell/mcp_config.py +171 -0
  40. invar/shell/perception.py +125 -0
  41. invar/shell/property_tests.py +227 -0
  42. invar/shell/prove.py +460 -0
  43. invar/shell/prove_cache.py +133 -0
  44. invar/shell/prove_fallback.py +183 -0
  45. invar/shell/templates.py +443 -0
  46. invar/shell/test_cmd.py +117 -0
  47. invar/shell/testing.py +297 -0
  48. invar/shell/update_cmd.py +191 -0
  49. invar/templates/CLAUDE.md.template +58 -0
  50. invar/templates/INVAR.md +134 -0
  51. invar/templates/__init__.py +1 -0
  52. invar/templates/aider.conf.yml.template +29 -0
  53. invar/templates/context.md.template +51 -0
  54. invar/templates/cursorrules.template +28 -0
  55. invar/templates/examples/README.md +21 -0
  56. invar/templates/examples/contracts.py +111 -0
  57. invar/templates/examples/core_shell.py +121 -0
  58. invar/templates/pre-commit-config.yaml.template +44 -0
  59. invar/templates/proposal.md.template +93 -0
  60. invar_tools-1.0.0.dist-info/METADATA +321 -0
  61. invar_tools-1.0.0.dist-info/RECORD +64 -0
  62. invar_tools-1.0.0.dist-info/WHEEL +4 -0
  63. invar_tools-1.0.0.dist-info/entry_points.txt +2 -0
  64. invar_tools-1.0.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,125 @@
1
+ """
2
+ Perception CLI implementation (Phase 4).
3
+
4
+ Shell module: handles file I/O for map and sig commands.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ from pathlib import Path
11
+ from typing import TYPE_CHECKING
12
+
13
+ from returns.result import Failure, Result, Success
14
+ from rich.console import Console
15
+
16
+ from invar.core.formatter import (
17
+ format_map_json,
18
+ format_map_text,
19
+ format_signatures_json,
20
+ format_signatures_text,
21
+ )
22
+ from invar.core.models import FileInfo
23
+ from invar.core.parser import parse_source
24
+ from invar.core.references import build_perception_map
25
+ from invar.shell.fs import discover_python_files
26
+
27
+ if TYPE_CHECKING:
28
+ from invar.core.models import Symbol
29
+
30
+ console = Console()
31
+
32
+
33
+ def run_map(path: Path, top_n: int, json_output: bool) -> Result[None, str]:
34
+ """
35
+ Run the map command.
36
+
37
+ Scans project and generates perception map with reference counts.
38
+ """
39
+ if not path.exists():
40
+ return Failure(f"Path does not exist: {path}")
41
+
42
+ # Collect all files and their sources
43
+ file_infos: list[FileInfo] = []
44
+ sources: dict[str, str] = {}
45
+
46
+ for py_file in discover_python_files(path):
47
+ try:
48
+ content = py_file.read_text(encoding="utf-8")
49
+ rel_path = str(py_file.relative_to(path))
50
+ # Skip empty files (e.g., __init__.py)
51
+ if not content.strip():
52
+ continue
53
+ file_info = parse_source(content, rel_path)
54
+ if file_info:
55
+ file_infos.append(file_info)
56
+ sources[rel_path] = content
57
+ except (OSError, UnicodeDecodeError) as e:
58
+ console.print(f"[yellow]Warning:[/yellow] {py_file}: {e}")
59
+ continue
60
+
61
+ if not file_infos:
62
+ return Failure("No Python files found")
63
+
64
+ # Build perception map
65
+ perception_map = build_perception_map(file_infos, sources, str(path.absolute()))
66
+
67
+ # Output
68
+ if json_output:
69
+ output = format_map_json(perception_map, top_n)
70
+ console.print(json.dumps(output, indent=2))
71
+ else:
72
+ output = format_map_text(perception_map, top_n)
73
+ console.print(output)
74
+
75
+ return Success(None)
76
+
77
+
78
+ def run_sig(target: str, json_output: bool) -> Result[None, str]:
79
+ """
80
+ Run the sig command.
81
+
82
+ Extracts signatures from a file or specific symbol.
83
+ Target format: "path/to/file.py" or "path/to/file.py::symbol_name"
84
+ """
85
+ # Parse target
86
+ if "::" in target:
87
+ file_path_str, symbol_name = target.split("::", 1)
88
+ else:
89
+ file_path_str = target
90
+ symbol_name = None
91
+
92
+ file_path = Path(file_path_str)
93
+ if not file_path.exists():
94
+ return Failure(f"File not found: {file_path}")
95
+
96
+ # Read and parse
97
+ try:
98
+ content = file_path.read_text(encoding="utf-8")
99
+ except (OSError, UnicodeDecodeError) as e:
100
+ return Failure(f"Failed to read {file_path}: {e}")
101
+
102
+ # Handle empty files
103
+ if not content.strip():
104
+ file_info = FileInfo(path=str(file_path), lines=0, symbols=[], imports=[], source="")
105
+ else:
106
+ file_info = parse_source(content, str(file_path))
107
+ if file_info is None:
108
+ return Failure(f"Syntax error in {file_path}")
109
+
110
+ # Filter symbols
111
+ symbols: list[Symbol] = file_info.symbols
112
+ if symbol_name:
113
+ symbols = [s for s in symbols if s.name == symbol_name]
114
+ if not symbols:
115
+ return Failure(f"Symbol '{symbol_name}' not found in {file_path}")
116
+
117
+ # Output
118
+ if json_output:
119
+ output = format_signatures_json(symbols, str(file_path))
120
+ console.print(json.dumps(output, indent=2))
121
+ else:
122
+ output = format_signatures_text(symbols, str(file_path))
123
+ console.print(output)
124
+
125
+ return Success(None)
@@ -0,0 +1,227 @@
1
+ """
2
+ Property test runner for files and directories.
3
+
4
+ DX-08: Shell module for running auto-generated property tests.
5
+ Handles I/O and file scanning, returns Result[T, E].
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import importlib.util
11
+ import sys
12
+ from typing import TYPE_CHECKING
13
+
14
+ from returns.result import Failure, Result, Success
15
+ from rich.console import Console
16
+
17
+ from invar.core.property_gen import (
18
+ PropertyTestReport,
19
+ find_contracted_functions,
20
+ run_property_test,
21
+ )
22
+
23
+ if TYPE_CHECKING:
24
+ from pathlib import Path
25
+
26
+ console = Console()
27
+
28
+
29
+ def run_property_tests_on_file(
30
+ file_path: Path,
31
+ max_examples: int = 100,
32
+ verbose: bool = False,
33
+ ) -> Result[PropertyTestReport, str]:
34
+ """
35
+ Run property tests on all contracted functions in a file.
36
+
37
+ Scans file for @pre/@post decorated functions, generates
38
+ Hypothesis tests, and runs them.
39
+
40
+ Args:
41
+ file_path: Path to Python file
42
+ max_examples: Maximum Hypothesis examples per function
43
+ verbose: Show detailed output
44
+
45
+ Returns:
46
+ Success with PropertyTestReport or Failure with error
47
+ """
48
+ if not file_path.exists():
49
+ return Failure(f"File not found: {file_path}")
50
+
51
+ if file_path.suffix != ".py":
52
+ return Failure(f"Not a Python file: {file_path}")
53
+
54
+ # Read and find contracted functions
55
+ try:
56
+ source = file_path.read_text()
57
+ except OSError as e:
58
+ return Failure(f"Could not read file: {e}")
59
+
60
+ # Handle empty files gracefully
61
+ if not source.strip():
62
+ return Success(PropertyTestReport())
63
+
64
+ contracted = find_contracted_functions(source)
65
+ if not contracted:
66
+ return Success(PropertyTestReport()) # No contracted functions, skip
67
+
68
+ # Import the module to get actual function objects
69
+ module = _import_module_from_path(file_path)
70
+ if module is None:
71
+ return Failure(f"Could not import module: {file_path}")
72
+
73
+ # Run tests on each contracted function
74
+ report = PropertyTestReport()
75
+
76
+ for func_info in contracted:
77
+ func_name = func_info["name"]
78
+ func = getattr(module, func_name, None)
79
+
80
+ if func is None or not callable(func):
81
+ report.functions_skipped += 1
82
+ continue
83
+
84
+ # Run property test
85
+ result = run_property_test(func, max_examples)
86
+ report.results.append(result)
87
+ report.functions_tested += 1
88
+ report.total_examples += result.examples_run
89
+
90
+ if result.passed:
91
+ report.functions_passed += 1
92
+ else:
93
+ report.functions_failed += 1
94
+
95
+ return Success(report)
96
+
97
+
98
+ def run_property_tests_on_files(
99
+ files: list[Path],
100
+ max_examples: int = 100,
101
+ verbose: bool = False,
102
+ ) -> Result[PropertyTestReport, str]:
103
+ """
104
+ Run property tests on multiple files.
105
+
106
+ Args:
107
+ files: List of Python file paths
108
+ max_examples: Maximum Hypothesis examples per function
109
+ verbose: Show detailed output
110
+
111
+ Returns:
112
+ Combined PropertyTestReport
113
+ """
114
+ # Check hypothesis availability first
115
+ try:
116
+ import hypothesis # noqa: F401
117
+ except ImportError:
118
+ return Success(PropertyTestReport(
119
+ errors=["Hypothesis not installed (pip install hypothesis)"]
120
+ ))
121
+
122
+ combined_report = PropertyTestReport()
123
+
124
+ for file_path in files:
125
+ result = run_property_tests_on_file(file_path, max_examples, verbose)
126
+
127
+ if isinstance(result, Failure):
128
+ combined_report.errors.append(result.failure())
129
+ continue
130
+
131
+ file_report = result.unwrap()
132
+ combined_report.functions_tested += file_report.functions_tested
133
+ combined_report.functions_passed += file_report.functions_passed
134
+ combined_report.functions_failed += file_report.functions_failed
135
+ combined_report.functions_skipped += file_report.functions_skipped
136
+ combined_report.total_examples += file_report.total_examples
137
+ combined_report.results.extend(file_report.results)
138
+ combined_report.errors.extend(file_report.errors)
139
+
140
+ return Success(combined_report)
141
+
142
+
143
+ def _import_module_from_path(file_path: Path) -> object | None:
144
+ """
145
+ Import a Python module from a file path.
146
+
147
+ Returns None if import fails.
148
+ """
149
+ try:
150
+ module_name = file_path.stem
151
+ spec = importlib.util.spec_from_file_location(module_name, file_path)
152
+ if spec is None or spec.loader is None:
153
+ return None
154
+
155
+ module = importlib.util.module_from_spec(spec)
156
+ sys.modules[module_name] = module
157
+
158
+ # Suppress output during import
159
+ spec.loader.exec_module(module)
160
+ return module
161
+
162
+ except Exception:
163
+ return None
164
+
165
+
166
+ def format_property_test_report(
167
+ report: PropertyTestReport,
168
+ json_output: bool = False,
169
+ ) -> str:
170
+ """
171
+ Format property test report for display.
172
+
173
+ Args:
174
+ report: The test report
175
+ json_output: Output as JSON
176
+
177
+ Returns:
178
+ Formatted string
179
+ """
180
+ import json
181
+
182
+ if json_output:
183
+ return json.dumps({
184
+ "functions_tested": report.functions_tested,
185
+ "functions_passed": report.functions_passed,
186
+ "functions_failed": report.functions_failed,
187
+ "functions_skipped": report.functions_skipped,
188
+ "total_examples": report.total_examples,
189
+ "all_passed": report.all_passed(),
190
+ "results": [
191
+ {
192
+ "function": r.function_name,
193
+ "passed": r.passed,
194
+ "examples": r.examples_run,
195
+ "error": r.error,
196
+ }
197
+ for r in report.results
198
+ ],
199
+ "errors": report.errors,
200
+ }, indent=2)
201
+
202
+ # Human-readable format
203
+ lines = []
204
+
205
+ if report.functions_tested == 0:
206
+ lines.append("No contracted functions found for property testing.")
207
+ return "\n".join(lines)
208
+
209
+ status = "✓" if report.all_passed() else "✗"
210
+ color = "green" if report.all_passed() else "red"
211
+
212
+ lines.append(
213
+ f"[{color}]{status}[/{color}] Property tests: "
214
+ f"{report.functions_passed}/{report.functions_tested} passed, "
215
+ f"{report.total_examples} examples"
216
+ )
217
+
218
+ # Show failures
219
+ for result in report.results:
220
+ if not result.passed:
221
+ lines.append(f" [red]✗[/red] {result.function_name}: {result.error}")
222
+
223
+ # Show errors
224
+ for error in report.errors:
225
+ lines.append(f" [yellow]![/yellow] {error}")
226
+
227
+ return "\n".join(lines)