invar-tools 1.17.26__py3-none-any.whl → 1.17.27__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 (35) hide show
  1. invar/core/contracts.py +38 -6
  2. invar/core/doc_edit.py +22 -9
  3. invar/core/doc_parser.py +17 -11
  4. invar/core/entry_points.py +48 -42
  5. invar/core/extraction.py +25 -9
  6. invar/core/format_specs.py +7 -2
  7. invar/core/format_strategies.py +6 -4
  8. invar/core/formatter.py +9 -6
  9. invar/core/hypothesis_strategies.py +24 -5
  10. invar/core/lambda_helpers.py +2 -2
  11. invar/core/parser.py +40 -21
  12. invar/core/patterns/detector.py +25 -6
  13. invar/core/patterns/p0_exhaustive.py +5 -5
  14. invar/core/patterns/p0_literal.py +8 -8
  15. invar/core/patterns/p0_newtype.py +2 -2
  16. invar/core/patterns/p0_nonempty.py +12 -7
  17. invar/core/patterns/p0_validation.py +4 -8
  18. invar/core/patterns/registry.py +12 -2
  19. invar/core/property_gen.py +47 -23
  20. invar/core/shell_analysis.py +70 -66
  21. invar/core/strategies.py +14 -3
  22. invar/core/suggestions.py +12 -4
  23. invar/core/tautology.py +33 -10
  24. invar/core/template_parser.py +23 -15
  25. invar/core/ts_parsers.py +6 -2
  26. invar/core/ts_sig_parser.py +18 -10
  27. invar/core/utils.py +20 -9
  28. invar/templates/protocol/python/tools.md +3 -0
  29. {invar_tools-1.17.26.dist-info → invar_tools-1.17.27.dist-info}/METADATA +1 -1
  30. {invar_tools-1.17.26.dist-info → invar_tools-1.17.27.dist-info}/RECORD +35 -35
  31. {invar_tools-1.17.26.dist-info → invar_tools-1.17.27.dist-info}/WHEEL +0 -0
  32. {invar_tools-1.17.26.dist-info → invar_tools-1.17.27.dist-info}/entry_points.txt +0 -0
  33. {invar_tools-1.17.26.dist-info → invar_tools-1.17.27.dist-info}/licenses/LICENSE +0 -0
  34. {invar_tools-1.17.26.dist-info → invar_tools-1.17.27.dist-info}/licenses/LICENSE-GPL +0 -0
  35. {invar_tools-1.17.26.dist-info → invar_tools-1.17.27.dist-info}/licenses/NOTICE +0 -0
invar/core/parser.py CHANGED
@@ -21,7 +21,11 @@ from invar.core.purity import (
21
21
  )
22
22
 
23
23
 
24
- @pre(lambda source, path="<string>": isinstance(source, str) and len(source.strip()) > 0)
24
+ @pre(
25
+ lambda source, path="<string>": isinstance(source, str)
26
+ and len(source.strip()) > 0
27
+ and isinstance(path, str)
28
+ )
25
29
  def parse_source(source: str, path: str = "<string>") -> FileInfo | None:
26
30
  """
27
31
  Parse Python source code and extract symbols.
@@ -64,7 +68,7 @@ def parse_source(source: str, path: str = "<string>") -> FileInfo | None:
64
68
  )
65
69
 
66
70
 
67
- @pre(lambda tree: isinstance(tree, ast.Module) and hasattr(tree, 'body'))
71
+ @pre(lambda tree: isinstance(tree, ast.Module) and hasattr(tree, "body"))
68
72
  def _extract_symbols(tree: ast.Module) -> list[Symbol]:
69
73
  """
70
74
  Extract function, class, and method symbols from AST.
@@ -95,10 +99,14 @@ def _extract_symbols(tree: ast.Module) -> list[Symbol]:
95
99
  return symbols
96
100
 
97
101
 
98
- @pre(lambda node: (
99
- isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef) and
100
- hasattr(node, 'name') and hasattr(node, 'args') and hasattr(node, 'lineno')
101
- ))
102
+ @pre(
103
+ lambda node: (
104
+ isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef)
105
+ and hasattr(node, "name")
106
+ and hasattr(node, "args")
107
+ and hasattr(node, "lineno")
108
+ )
109
+ )
102
110
  @post(lambda result: result.kind == SymbolKind.FUNCTION)
103
111
  def _parse_function(node: ast.FunctionDef | ast.AsyncFunctionDef) -> Symbol:
104
112
  """Parse a function definition into a Symbol."""
@@ -129,10 +137,15 @@ def _parse_function(node: ast.FunctionDef | ast.AsyncFunctionDef) -> Symbol:
129
137
  )
130
138
 
131
139
 
132
- @pre(lambda node, class_name: (
133
- isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef) and
134
- hasattr(node, 'name') and hasattr(node, 'args') and hasattr(node, 'lineno')
135
- ))
140
+ @pre(
141
+ lambda node, class_name: (
142
+ isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef)
143
+ and hasattr(node, "name")
144
+ and hasattr(node, "args")
145
+ and hasattr(node, "lineno")
146
+ and len(class_name) > 0
147
+ )
148
+ )
136
149
  @post(lambda result: result.kind == SymbolKind.METHOD)
137
150
  def _parse_method(node: ast.FunctionDef | ast.AsyncFunctionDef, class_name: str) -> Symbol:
138
151
  """
@@ -175,7 +188,11 @@ def _parse_method(node: ast.FunctionDef | ast.AsyncFunctionDef, class_name: str)
175
188
  )
176
189
 
177
190
 
178
- @pre(lambda node: isinstance(node, ast.ClassDef) and hasattr(node, 'name') and hasattr(node, 'lineno'))
191
+ @pre(
192
+ lambda node: isinstance(node, ast.ClassDef)
193
+ and hasattr(node, "name")
194
+ and hasattr(node, "lineno")
195
+ )
179
196
  @post(lambda result: result.kind == SymbolKind.CLASS)
180
197
  def _parse_class(node: ast.ClassDef) -> Symbol:
181
198
  """Parse a class definition into a Symbol."""
@@ -190,10 +207,11 @@ def _parse_class(node: ast.ClassDef) -> Symbol:
190
207
  )
191
208
 
192
209
 
193
- @pre(lambda node: (
194
- isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef) and
195
- hasattr(node, 'decorator_list')
196
- ))
210
+ @pre(
211
+ lambda node: (
212
+ isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef) and hasattr(node, "decorator_list")
213
+ )
214
+ )
197
215
  @post(lambda result: all(c.kind in ("pre", "post") for c in result))
198
216
  def _extract_contracts(node: ast.FunctionDef | ast.AsyncFunctionDef) -> list[Contract]:
199
217
  """Extract @pre and @post contracts from function decorators."""
@@ -233,7 +251,7 @@ def _parse_decorator_as_contract(decorator: ast.expr) -> Contract | None:
233
251
  return None
234
252
 
235
253
 
236
- @pre(lambda call: isinstance(call, ast.Call) and hasattr(call, 'args'))
254
+ @pre(lambda call: isinstance(call, ast.Call) and hasattr(call, "args"))
237
255
  def _get_contract_expression(call: ast.Call) -> str:
238
256
  """Extract the expression string from a contract decorator call."""
239
257
  if call.args:
@@ -241,10 +259,11 @@ def _get_contract_expression(call: ast.Call) -> str:
241
259
  return ""
242
260
 
243
261
 
244
- @pre(lambda node: (
245
- isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef) and
246
- hasattr(node, 'args')
247
- ))
262
+ @pre(
263
+ lambda node: (
264
+ isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef) and hasattr(node, "args")
265
+ )
266
+ )
248
267
  @post(lambda result: result.startswith("(") and ")" in result)
249
268
  def _build_signature(node: ast.FunctionDef | ast.AsyncFunctionDef) -> str:
250
269
  """Build a signature string from function arguments."""
@@ -267,7 +286,7 @@ def _build_signature(node: ast.FunctionDef | ast.AsyncFunctionDef) -> str:
267
286
  return sig
268
287
 
269
288
 
270
- @pre(lambda tree: isinstance(tree, ast.Module) and hasattr(tree, 'body'))
289
+ @pre(lambda tree: isinstance(tree, ast.Module) and hasattr(tree, "body"))
271
290
  @post(lambda result: all(isinstance(s, str) and s for s in result))
272
291
  def _extract_imports(tree: ast.Module) -> list[str]:
273
292
  """Extract imported module names from AST (top-level only)."""
@@ -147,11 +147,9 @@ class BaseDetector:
147
147
  result = ast.unparse(annotation)
148
148
  return result if result else "<unknown>"
149
149
 
150
- @pre(lambda self, params, type_name: len(type_name) > 0)
150
+ @pre(lambda self, params, type_name: isinstance(params, list) and len(type_name) > 0)
151
151
  @post(lambda result: result >= 0)
152
- def count_type_occurrences(
153
- self, params: list[tuple[str, str | None]], type_name: str
154
- ) -> int:
152
+ def count_type_occurrences(self, params: list[tuple[str, str | None]], type_name: str) -> int:
155
153
  """
156
154
  Count how many parameters have a specific type.
157
155
 
@@ -203,12 +201,33 @@ class BaseDetector:
203
201
  for case in match_node.cases:
204
202
  pattern = case.pattern
205
203
  if isinstance(pattern, ast.MatchValue):
206
- cases.append(ast.unparse(pattern.value) if hasattr(ast, "unparse") else str(pattern.value))
204
+ cases.append(
205
+ ast.unparse(pattern.value) if hasattr(ast, "unparse") else str(pattern.value)
206
+ )
207
207
  elif isinstance(pattern, ast.MatchAs) and pattern.pattern is None:
208
208
  cases.append("_") # Wildcard
209
209
  return cases
210
210
 
211
- @pre(lambda self, pattern_id, priority, file_path, line, message, current_code, suggested_pattern, confidence, reference_pattern: line > 0)
211
+ @pre(
212
+ lambda self,
213
+ pattern_id,
214
+ priority,
215
+ file_path,
216
+ line,
217
+ message,
218
+ current_code,
219
+ suggested_pattern,
220
+ confidence,
221
+ reference_pattern: pattern_id in PatternID
222
+ and priority in Priority
223
+ and confidence in Confidence
224
+ and line > 0
225
+ and len(file_path) > 0
226
+ and len(message) > 0
227
+ and len(current_code) > 0
228
+ and len(suggested_pattern) > 0
229
+ and len(reference_pattern) > 0
230
+ )
212
231
  @post(lambda result: result.reference_file == ".invar/examples/functional.py")
213
232
  def make_suggestion(
214
233
  self,
@@ -106,10 +106,8 @@ class ExhaustiveMatchDetector(BaseDetector):
106
106
 
107
107
  return suggestions
108
108
 
109
- @pre(lambda self, node, file_path: len(file_path) > 0)
110
- def _check_match(
111
- self, node: ast.Match, file_path: str
112
- ) -> PatternSuggestion | None:
109
+ @pre(lambda self, node, file_path: node is not None and len(file_path) > 0)
110
+ def _check_match(self, node: ast.Match, file_path: str) -> PatternSuggestion | None:
113
111
  """
114
112
  Check if match statement could benefit from assert_never.
115
113
 
@@ -144,7 +142,9 @@ class ExhaustiveMatchDetector(BaseDetector):
144
142
  elif isinstance(pattern, ast.MatchValue):
145
143
  if isinstance(pattern.value, ast.Attribute):
146
144
  has_enum_patterns = True
147
- enum_cases.append(ast.unparse(pattern.value) if hasattr(ast, "unparse") else "...")
145
+ enum_cases.append(
146
+ ast.unparse(pattern.value) if hasattr(ast, "unparse") else "..."
147
+ )
148
148
 
149
149
  # Suggest if: has enum patterns + has wildcard + doesn't use assert_never
150
150
  if has_enum_patterns and has_wildcard and not uses_assert_never:
@@ -101,7 +101,7 @@ class LiteralDetector(BaseDetector):
101
101
 
102
102
  return suggestions
103
103
 
104
- @pre(lambda self, node, file_path: len(file_path) > 0)
104
+ @pre(lambda self, node, file_path: node is not None and len(file_path) > 0)
105
105
  @post(lambda result: all(isinstance(s, PatternSuggestion) for s in result))
106
106
  def _check_function(
107
107
  self, node: ast.FunctionDef | ast.AsyncFunctionDef, file_path: str
@@ -145,7 +145,11 @@ class LiteralDetector(BaseDetector):
145
145
 
146
146
  return suggestions
147
147
 
148
- @post(lambda result: all(len(name) > 0 and len(vals) > 0 and line > 0 for name, vals, line in result))
148
+ @post(
149
+ lambda result: all(
150
+ len(name) > 0 and len(vals) > 0 and line > 0 for name, vals, line in result
151
+ )
152
+ )
149
153
  def _find_membership_checks(
150
154
  self, node: ast.FunctionDef | ast.AsyncFunctionDef
151
155
  ) -> list[tuple[str, list[str | int], int]]:
@@ -206,9 +210,7 @@ class LiteralDetector(BaseDetector):
206
210
  self._collect_membership_checks(stmt.orelse, checks)
207
211
 
208
212
  @post(lambda result: result is None or (len(result[0]) > 0 and len(result[1]) > 0))
209
- def _extract_membership_check(
210
- self, test: ast.expr
211
- ) -> tuple[str, list[str | int]] | None:
213
+ def _extract_membership_check(self, test: ast.expr) -> tuple[str, list[str | int]] | None:
212
214
  """
213
215
  Extract variable and values from membership check.
214
216
 
@@ -259,9 +261,7 @@ class LiteralDetector(BaseDetector):
259
261
  if isinstance(node, (ast.Tuple, ast.List, ast.Set)):
260
262
  values = []
261
263
  for elt in node.elts:
262
- if isinstance(elt, ast.Constant) and isinstance(
263
- elt.value, (str, int)
264
- ):
264
+ if isinstance(elt, ast.Constant) and isinstance(elt.value, (str, int)):
265
265
  values.append(elt.value)
266
266
  else:
267
267
  return None # Non-literal value
@@ -107,7 +107,7 @@ class NewTypeDetector(BaseDetector):
107
107
 
108
108
  return suggestions
109
109
 
110
- @pre(lambda self, node, file_path: len(file_path) > 0)
110
+ @pre(lambda self, node, file_path: node is not None and len(file_path) > 0)
111
111
  def _check_function(
112
112
  self, node: ast.FunctionDef | ast.AsyncFunctionDef, file_path: str
113
113
  ) -> PatternSuggestion | None:
@@ -153,7 +153,7 @@ class NewTypeDetector(BaseDetector):
153
153
 
154
154
  return None
155
155
 
156
- @pre(lambda self, param_names, _node: len(param_names) > 0)
156
+ @pre(lambda self, param_names, _node: len(param_names) > 0 and _node is not None)
157
157
  @post(lambda result: result in Confidence)
158
158
  def _calculate_confidence(
159
159
  self, param_names: list[str], _node: ast.FunctionDef | ast.AsyncFunctionDef
@@ -102,7 +102,7 @@ class NonEmptyDetector(BaseDetector):
102
102
 
103
103
  return suggestions
104
104
 
105
- @pre(lambda self, node, file_path: len(file_path) > 0)
105
+ @pre(lambda self, node, file_path: node is not None and len(file_path) > 0)
106
106
  def _check_function(
107
107
  self, node: ast.FunctionDef | ast.AsyncFunctionDef, file_path: str
108
108
  ) -> PatternSuggestion | None:
@@ -173,9 +173,7 @@ class NonEmptyDetector(BaseDetector):
173
173
  return checks
174
174
 
175
175
  @pre(lambda self, stmts, checks: stmts is not None and checks is not None)
176
- def _collect_empty_checks(
177
- self, stmts: list[ast.stmt], checks: list[tuple[str, int]]
178
- ) -> None:
176
+ def _collect_empty_checks(self, stmts: list[ast.stmt], checks: list[tuple[str, int]]) -> None:
179
177
  """
180
178
  Recursively collect empty checks, avoiding nested functions.
181
179
 
@@ -260,7 +258,7 @@ class NonEmptyDetector(BaseDetector):
260
258
  """
261
259
  return any(isinstance(stmt, (ast.Raise, ast.Return)) for stmt in body)
262
260
 
263
- @pre(lambda self, node, var_name: len(var_name) > 0)
261
+ @pre(lambda self, node, var_name: node is not None and len(var_name) > 0)
264
262
  def _get_param_type(
265
263
  self, node: ast.FunctionDef | ast.AsyncFunctionDef, var_name: str
266
264
  ) -> str | None:
@@ -278,7 +276,11 @@ class NonEmptyDetector(BaseDetector):
278
276
  return self._annotation_to_str(arg.annotation)
279
277
  return None
280
278
 
281
- @pre(lambda self, _node, var_name, param_type: len(var_name) > 0)
279
+ @pre(
280
+ lambda self, _node, var_name, param_type=None: _node is not None
281
+ and len(var_name) > 0
282
+ and (param_type is None or isinstance(param_type, str))
283
+ )
282
284
  @post(lambda result: result in Confidence)
283
285
  def _calculate_confidence(
284
286
  self,
@@ -305,7 +307,10 @@ class NonEmptyDetector(BaseDetector):
305
307
 
306
308
  return Confidence.LOW
307
309
 
308
- @pre(lambda self, var_name, param_type: len(var_name) > 0)
310
+ @pre(
311
+ lambda self, var_name, param_type=None: len(var_name) > 0
312
+ and (param_type is None or isinstance(param_type, str))
313
+ )
309
314
  @post(lambda result: len(result) > 0 and "if not" in result)
310
315
  def _format_check(self, var_name: str, param_type: str | None) -> str:
311
316
  """
@@ -114,7 +114,7 @@ class ValidationDetector(BaseDetector):
114
114
 
115
115
  return suggestions
116
116
 
117
- @pre(lambda self, node, file_path: len(file_path) > 0)
117
+ @pre(lambda self, node, file_path: node is not None and len(file_path) > 0)
118
118
  def _check_function(
119
119
  self, node: ast.FunctionDef | ast.AsyncFunctionDef, file_path: str
120
120
  ) -> PatternSuggestion | None:
@@ -156,9 +156,7 @@ class ValidationDetector(BaseDetector):
156
156
  return None
157
157
 
158
158
  @post(lambda result: result >= 0)
159
- def _count_early_error_returns(
160
- self, node: ast.FunctionDef | ast.AsyncFunctionDef
161
- ) -> int:
159
+ def _count_early_error_returns(self, node: ast.FunctionDef | ast.AsyncFunctionDef) -> int:
162
160
  """
163
161
  Count early returns with error-like values inside if statements.
164
162
 
@@ -239,7 +237,7 @@ class ValidationDetector(BaseDetector):
239
237
 
240
238
  return False
241
239
 
242
- @pre(lambda self, node, early_returns: early_returns >= 0)
240
+ @pre(lambda self, node, early_returns: node is not None and early_returns >= 0)
243
241
  @post(lambda result: result in Confidence)
244
242
  def _calculate_confidence(
245
243
  self, node: ast.FunctionDef | ast.AsyncFunctionDef, early_returns: int
@@ -269,9 +267,7 @@ class ValidationDetector(BaseDetector):
269
267
  return Confidence.LOW
270
268
 
271
269
  @post(lambda result: len(result) > 0 and "def " in result)
272
- def _format_function_preview(
273
- self, node: ast.FunctionDef | ast.AsyncFunctionDef
274
- ) -> str:
270
+ def _format_function_preview(self, node: ast.FunctionDef | ast.AsyncFunctionDef) -> str:
275
271
  """
276
272
  Format function preview for display.
277
273
 
@@ -72,7 +72,13 @@ class PatternRegistry:
72
72
  """
73
73
  return [d for d in self._detectors if d.priority == priority]
74
74
 
75
- @pre(lambda self, file_path, source, min_confidence=None, priority_filter=None: len(file_path) > 0)
75
+ @pre(
76
+ lambda self, file_path, source, min_confidence=None, priority_filter=None: len(file_path)
77
+ > 0
78
+ and isinstance(source, str)
79
+ and (min_confidence is None or min_confidence in Confidence)
80
+ and (priority_filter is None or priority_filter in Priority)
81
+ )
76
82
  @post(lambda result: result is not None)
77
83
  def detect_file(
78
84
  self,
@@ -220,7 +226,11 @@ def get_registry() -> PatternRegistry:
220
226
  return PatternRegistry()
221
227
 
222
228
 
223
- @pre(lambda file_path, source, min_confidence=None: len(file_path) > 0)
229
+ @pre(
230
+ lambda file_path, source, min_confidence=None: len(file_path) > 0
231
+ and isinstance(source, str)
232
+ and (min_confidence is None or min_confidence in Confidence)
233
+ )
224
234
  @post(lambda result: result is not None)
225
235
  def detect_patterns(
226
236
  file_path: str,
@@ -194,10 +194,12 @@ def _check_decorator_contracts(dec: ast.Call) -> tuple[bool, bool]:
194
194
  return has_pre, has_post
195
195
 
196
196
 
197
- @pre(lambda node: (
198
- isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) and
199
- hasattr(node, 'decorator_list')
200
- ))
197
+ @pre(
198
+ lambda node: (
199
+ isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
200
+ and hasattr(node, "decorator_list")
201
+ )
202
+ )
201
203
  @post(lambda result: isinstance(result, tuple) and len(result) == 2)
202
204
  def _get_function_contracts(node: ast.FunctionDef | ast.AsyncFunctionDef) -> tuple[bool, bool]:
203
205
  """Check function decorators for contracts, return (has_pre, has_post).
@@ -247,18 +249,22 @@ def find_contracted_functions(source: str) -> list[dict[str, Any]]:
247
249
  if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
248
250
  has_pre, has_post = _get_function_contracts(node)
249
251
  if has_pre or has_post:
250
- functions.append({
251
- "name": node.name,
252
- "lineno": node.lineno,
253
- "has_pre": has_pre,
254
- "has_post": has_post,
255
- "params": _extract_params(node),
256
- "return_type": _extract_return_type(node),
257
- })
252
+ functions.append(
253
+ {
254
+ "name": node.name,
255
+ "lineno": node.lineno,
256
+ "has_pre": has_pre,
257
+ "has_post": has_post,
258
+ "params": _extract_params(node),
259
+ "return_type": _extract_return_type(node),
260
+ }
261
+ )
258
262
  return functions
259
263
 
260
264
 
261
- @pre(lambda node: isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) and hasattr(node, "args"))
265
+ @pre(
266
+ lambda node: isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) and hasattr(node, "args")
267
+ )
262
268
  @post(lambda result: isinstance(result, list))
263
269
  def _extract_params(node: ast.FunctionDef | ast.AsyncFunctionDef) -> list[dict[str, str]]:
264
270
  """Extract parameter names and type annotations from function node."""
@@ -280,7 +286,11 @@ def _extract_return_type(node: ast.FunctionDef | ast.AsyncFunctionDef) -> str |
280
286
  return None
281
287
 
282
288
 
283
- @pre(lambda func, strategies, max_examples=100: callable(func) and isinstance(strategies, dict))
289
+ @pre(
290
+ lambda func, strategies, max_examples=100: callable(func)
291
+ and isinstance(strategies, dict)
292
+ and max_examples > 0
293
+ )
284
294
  @post(lambda result: result is None or callable(result))
285
295
  def build_test_function(
286
296
  func: Callable,
@@ -366,25 +376,37 @@ def _skip_result(name: str, reason: str) -> PropertyTestResult:
366
376
 
367
377
  # Skip patterns for untestable error detection
368
378
  _SKIP_PATTERNS = (
369
- "Nothing", "NoSuchExample", "filter_too_much", "Could not resolve",
370
- "validation error", "missing", "positional argument", "Unable to satisfy",
379
+ "Nothing",
380
+ "NoSuchExample",
381
+ "filter_too_much",
382
+ "Could not resolve",
383
+ "validation error",
384
+ "missing",
385
+ "positional argument",
386
+ "Unable to satisfy",
371
387
  "has no attribute 'check'", # invar_runtime contracts, not deal contracts
372
388
  )
373
389
 
374
390
 
375
- @pre(lambda err_str, func_name, max_examples: len(err_str) > 0 and len(func_name) > 0 and max_examples > 0)
391
+ @pre(
392
+ lambda err_str, func_name, max_examples: len(err_str) > 0
393
+ and len(func_name) > 0
394
+ and max_examples > 0
395
+ )
376
396
  @post(lambda result: isinstance(result, PropertyTestResult))
377
- def _handle_test_exception(
378
- err_str: str, func_name: str, max_examples: int
379
- ) -> PropertyTestResult:
397
+ def _handle_test_exception(err_str: str, func_name: str, max_examples: int) -> PropertyTestResult:
380
398
  """Handle exception from property test, returning skip or failure result."""
381
399
  # Check for invar_runtime contracts (deal.cases requires deal contracts)
382
400
  if "has no attribute 'check'" in err_str:
383
- return _skip_result(func_name, "Skipped: uses invar_runtime (deal.cases requires deal contracts)")
401
+ return _skip_result(
402
+ func_name, "Skipped: uses invar_runtime (deal.cases requires deal contracts)"
403
+ )
384
404
  if any(p in err_str for p in _SKIP_PATTERNS):
385
405
  return _skip_result(func_name, "Skipped: untestable types")
386
406
  seed = _extract_hypothesis_seed(err_str)
387
- return PropertyTestResult(func_name, passed=False, examples_run=max_examples, error=err_str, seed=seed)
407
+ return PropertyTestResult(
408
+ func_name, passed=False, examples_run=max_examples, error=err_str, seed=seed
409
+ )
388
410
 
389
411
 
390
412
  @pre(lambda func, max_examples: callable(func) and max_examples > 0)
@@ -424,7 +446,9 @@ def run_property_test(func: Callable, max_examples: int = 100) -> PropertyTestRe
424
446
  except deal.PostContractError as e:
425
447
  err_str = str(e)
426
448
  seed = _extract_hypothesis_seed(err_str)
427
- return PropertyTestResult(func_name, passed=False, examples_run=max_examples, error=err_str, seed=seed)
449
+ return PropertyTestResult(
450
+ func_name, passed=False, examples_run=max_examples, error=err_str, seed=seed
451
+ )
428
452
  except ImportError:
429
453
  pass # Fall through to custom strategy approach
430
454
  except Exception as e:
@@ -22,69 +22,71 @@ if TYPE_CHECKING:
22
22
  from invar.core.models import Symbol
23
23
 
24
24
  # I/O indicators that mark a function as legitimately in Shell
25
- IO_INDICATORS: frozenset[str] = frozenset([
26
- # File operations
27
- ".read(",
28
- ".write(",
29
- ".read_text(",
30
- ".write_text(",
31
- ".read_bytes(",
32
- ".write_bytes(",
33
- "open(",
34
- "Path(",
35
- ".exists()",
36
- ".is_file()",
37
- ".is_dir()",
38
- ".rglob(",
39
- ".glob(",
40
- ".iterdir(",
41
- ".mkdir(",
42
- ".unlink(",
43
- "shutil.",
44
- "tempfile.",
45
- # Process operations
46
- "subprocess.",
47
- "os.system(",
48
- "os.popen(",
49
- "os.getenv(",
50
- "os.environ",
51
- # Terminal/System
52
- "sys.stdout",
53
- "sys.stderr",
54
- "sys.stdin",
55
- ".isatty()",
56
- # Module loading
57
- "importlib.",
58
- "exec_module(",
59
- # Network operations
60
- "requests.",
61
- "aiohttp.",
62
- "httpx.",
63
- "urllib.",
64
- # Console output
65
- "print(",
66
- "console.",
67
- "Console(",
68
- "typer.",
69
- "click.",
70
- "rich.",
71
- # Result wrapping (Shell's primary job)
72
- "Success(",
73
- "Failure(",
74
- "Result[",
75
- # Database
76
- "cursor.",
77
- "connection.",
78
- "session.",
79
- # Logging
80
- "logger.",
81
- "logging.",
82
- # Serialization (often to files)
83
- "json.dump(",
84
- "json.load(",
85
- "toml.load(",
86
- "yaml.load(",
87
- ])
25
+ IO_INDICATORS: frozenset[str] = frozenset(
26
+ [
27
+ # File operations
28
+ ".read(",
29
+ ".write(",
30
+ ".read_text(",
31
+ ".write_text(",
32
+ ".read_bytes(",
33
+ ".write_bytes(",
34
+ "open(",
35
+ "Path(",
36
+ ".exists()",
37
+ ".is_file()",
38
+ ".is_dir()",
39
+ ".rglob(",
40
+ ".glob(",
41
+ ".iterdir(",
42
+ ".mkdir(",
43
+ ".unlink(",
44
+ "shutil.",
45
+ "tempfile.",
46
+ # Process operations
47
+ "subprocess.",
48
+ "os.system(",
49
+ "os.popen(",
50
+ "os.getenv(",
51
+ "os.environ",
52
+ # Terminal/System
53
+ "sys.stdout",
54
+ "sys.stderr",
55
+ "sys.stdin",
56
+ ".isatty()",
57
+ # Module loading
58
+ "importlib.",
59
+ "exec_module(",
60
+ # Network operations
61
+ "requests.",
62
+ "aiohttp.",
63
+ "httpx.",
64
+ "urllib.",
65
+ # Console output
66
+ "print(",
67
+ "console.",
68
+ "Console(",
69
+ "typer.",
70
+ "click.",
71
+ "rich.",
72
+ # Result wrapping (Shell's primary job)
73
+ "Success(",
74
+ "Failure(",
75
+ "Result[",
76
+ # Database
77
+ "cursor.",
78
+ "connection.",
79
+ "session.",
80
+ # Logging
81
+ "logger.",
82
+ "logging.",
83
+ # Serialization (often to files)
84
+ "json.dump(",
85
+ "json.load(",
86
+ "toml.load(",
87
+ "yaml.load(",
88
+ ]
89
+ )
88
90
 
89
91
  # Marker pattern to exempt functions from complexity check
90
92
  COMPLEXITY_MARKER_PATTERN = re.compile(r"#\s*@shell_complexity\s*:")
@@ -111,7 +113,7 @@ def has_io_operations(source: str) -> bool:
111
113
  return any(indicator in source for indicator in IO_INDICATORS)
112
114
 
113
115
 
114
- @pre(lambda symbol, source: symbol is not None) # Symbol must exist
116
+ @pre(lambda symbol, source: symbol is not None and isinstance(source, str)) # Symbol must exist
115
117
  def has_orchestration_marker(symbol: Symbol, source: str) -> bool:
116
118
  """
117
119
  Check if symbol has @shell_orchestration marker comment.
@@ -144,7 +146,7 @@ def has_orchestration_marker(symbol: Symbol, source: str) -> bool:
144
146
  return bool(ORCHESTRATION_MARKER_PATTERN.search(context))
145
147
 
146
148
 
147
- @pre(lambda symbol, source: symbol is not None) # Symbol must exist
149
+ @pre(lambda symbol, source: symbol is not None and isinstance(source, str)) # Symbol must exist
148
150
  def has_complexity_marker(symbol: Symbol, source: str) -> bool:
149
151
  """
150
152
  Check if symbol has @shell_complexity marker comment.
@@ -221,7 +223,9 @@ def count_branches(source: str) -> int:
221
223
  return count
222
224
 
223
225
 
224
- @pre(lambda symbol, file_source: symbol is not None) # Symbol must exist
226
+ @pre(
227
+ lambda symbol, file_source: symbol is not None and isinstance(file_source, str)
228
+ ) # Symbol must exist
225
229
  def get_symbol_source(symbol: Symbol, file_source: str) -> str:
226
230
  """
227
231
  Extract the source code for a specific symbol.