invar-tools 1.17.25__py3-none-any.whl → 1.17.26__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/utils.py CHANGED
@@ -37,7 +37,13 @@ def get_exit_code(report: GuardReport, strict: bool) -> int:
37
37
  return 0
38
38
 
39
39
 
40
- @pre(lambda report, strict, doctest_passed=True, crosshair_passed=True, property_passed=True: report.files_checked >= 0)
40
+ @pre(
41
+ lambda report,
42
+ strict,
43
+ doctest_passed=True,
44
+ crosshair_passed=True,
45
+ property_passed=True: report.files_checked >= 0
46
+ )
41
47
  @post(lambda result: result in ("passed", "failed"))
42
48
  def get_combined_status(
43
49
  report: GuardReport,
@@ -204,7 +210,9 @@ def _parse_rule_exclusions(config: dict[str, Any]) -> list[RuleExclusion] | None
204
210
  if isinstance(excl, dict) and "pattern" in excl and "rules" in excl:
205
211
  pattern, rules = excl["pattern"], excl["rules"]
206
212
  if isinstance(pattern, str) and isinstance(rules, list):
207
- exclusions.append(RuleExclusion(pattern=str(pattern), rules=[str(r) for r in rules]))
213
+ exclusions.append(
214
+ RuleExclusion(pattern=str(pattern), rules=[str(r) for r in rules])
215
+ )
208
216
  return exclusions if exclusions else None
209
217
 
210
218
 
@@ -253,7 +261,14 @@ def parse_guard_config(guard_config: dict[str, Any]) -> RuleConfig:
253
261
  kwargs: dict[str, Any] = {}
254
262
 
255
263
  # Int fields
256
- for key in ("max_file_lines", "max_function_lines"):
264
+ for key in (
265
+ "max_file_lines",
266
+ "max_function_lines",
267
+ "timeout_doctest",
268
+ "timeout_hypothesis",
269
+ "timeout_crosshair",
270
+ "timeout_crosshair_per_condition",
271
+ ):
257
272
  if (val := _get_int(guard_config, key)) is not None:
258
273
  kwargs[key] = val
259
274
 
@@ -7,13 +7,10 @@ Handles I/O and file scanning, returns Result[T, E].
7
7
 
8
8
  from __future__ import annotations
9
9
 
10
- import importlib.util
11
10
  import sys
11
+ import tomllib
12
12
  from contextlib import contextmanager, suppress
13
- from typing import TYPE_CHECKING
14
-
15
- if TYPE_CHECKING:
16
- from pathlib import Path
13
+ from pathlib import Path
17
14
 
18
15
  from returns.result import Failure, Result, Success
19
16
  from rich.console import Console
@@ -24,9 +21,98 @@ from invar.shell.subprocess_env import detect_project_venv, find_site_packages
24
21
  console = Console()
25
22
 
26
23
 
24
+ def _extract_src_dirs_from_paths(project_root: Path, paths: list[str]) -> set[str]:
25
+ """
26
+ Extract unique src directories from configured paths.
27
+
28
+ For monorepo structures like 'packages/pkg-a/src/pkg_a/core',
29
+ extracts 'packages/pkg-a/src' as the directory to add to sys.path.
30
+
31
+ Examples:
32
+ >>> from pathlib import Path
33
+ >>> root = Path("/project")
34
+ >>> paths = ["packages/a/src/pkg/core", "packages/b/src/pkg/shell"]
35
+ >>> # Returns src dirs (would check existence in real use)
36
+ >>> sorted(_extract_src_dirs_from_paths(root, paths)) # doctest: +SKIP
37
+ ['/project/packages/a/src', '/project/packages/b/src']
38
+ """
39
+ src_dirs: set[str] = set()
40
+ for p in paths:
41
+ parts = Path(p).parts
42
+ if "src" in parts:
43
+ idx = parts.index("src")
44
+ src_dir = project_root / Path(*parts[: idx + 1])
45
+ if src_dir.exists():
46
+ src_dirs.add(str(src_dir))
47
+ return src_dirs
48
+
49
+
50
+ # @shell_complexity: Config fallthrough requires checking 3 sources with error handling
51
+ def _get_invar_paths_from_config(project_root: Path) -> list[str]:
52
+ """
53
+ Get 'paths' from [tool.invar] config across all config locations.
54
+
55
+ Checks in priority order:
56
+ 1. pyproject.toml [tool.invar].paths
57
+ 2. invar.toml [invar].paths (root level)
58
+ 3. .invar/config.toml [invar].paths (root level)
59
+
60
+ Returns first found, or empty list.
61
+ """
62
+ # 1. pyproject.toml [tool.invar].paths
63
+ pyproject = project_root / "pyproject.toml"
64
+ if pyproject.exists():
65
+ try:
66
+ with pyproject.open("rb") as f:
67
+ data = tomllib.load(f)
68
+ paths = data.get("tool", {}).get("invar", {}).get("paths", [])
69
+ if paths:
70
+ return paths
71
+ except Exception:
72
+ pass
73
+
74
+ # 2. invar.toml [invar].paths (root level, since no [tool] wrapper)
75
+ invar_toml = project_root / "invar.toml"
76
+ if invar_toml.exists():
77
+ try:
78
+ with invar_toml.open("rb") as f:
79
+ data = tomllib.load(f)
80
+ # In invar.toml, paths could be at root or under [invar]
81
+ paths = data.get("paths", []) or data.get("invar", {}).get("paths", [])
82
+ if paths:
83
+ return paths
84
+ except Exception:
85
+ pass
86
+
87
+ # 3. .invar/config.toml
88
+ invar_config = project_root / ".invar" / "config.toml"
89
+ if invar_config.exists():
90
+ try:
91
+ with invar_config.open("rb") as f:
92
+ data = tomllib.load(f)
93
+ paths = data.get("paths", []) or data.get("invar", {}).get("paths", [])
94
+ if paths:
95
+ return paths
96
+ except Exception:
97
+ pass
98
+
99
+ return []
100
+
101
+
27
102
  # @shell_orchestration: Temporarily inject venv site-packages for module imports
103
+ # @shell_complexity: Monorepo support requires reading config and extracting src dirs
28
104
  @contextmanager
29
105
  def _inject_project_site_packages(project_root: Path):
106
+ """
107
+ Context manager that temporarily injects project dependencies into sys.path.
108
+
109
+ Supports:
110
+ - Standard layout: project_root/src
111
+ - Monorepo layout: paths from config (pyproject.toml, invar.toml, .invar/config.toml)
112
+ - Configured paths: extracted from core_paths/shell_paths in [tool.invar.guard]
113
+ """
114
+ from invar.shell.config import get_path_classification
115
+
30
116
  venv = detect_project_venv(project_root)
31
117
  site_packages = find_site_packages(venv) if venv is not None else None
32
118
 
@@ -34,13 +120,35 @@ def _inject_project_site_packages(project_root: Path):
34
120
  yield
35
121
  return
36
122
 
37
- src_dir = project_root / "src"
38
-
39
123
  added: list[str] = []
124
+
125
+ # 1. Read 'paths' from config (supports pyproject.toml, invar.toml, .invar/config.toml)
126
+ invar_paths = _get_invar_paths_from_config(project_root)
127
+ for p in invar_paths:
128
+ src_dir = project_root / p
129
+ if src_dir.exists():
130
+ src_dir_str = str(src_dir)
131
+ if src_dir_str not in added:
132
+ sys.path.insert(0, src_dir_str)
133
+ added.append(src_dir_str)
134
+
135
+ # 2. Extract src dirs from core_paths and shell_paths config
136
+ path_result = get_path_classification(project_root)
137
+ if isinstance(path_result, Success):
138
+ core_paths, shell_paths = path_result.unwrap()
139
+ src_dirs = _extract_src_dirs_from_paths(project_root, core_paths + shell_paths)
140
+ for src_dir_str in src_dirs:
141
+ if src_dir_str not in added:
142
+ sys.path.insert(0, src_dir_str)
143
+ added.append(src_dir_str)
144
+
145
+ # 3. Fallback to project_root/src (standard layout)
146
+ src_dir = project_root / "src"
40
147
  if src_dir.exists():
41
148
  src_dir_str = str(src_dir)
42
- sys.path.insert(0, src_dir_str)
43
- added.append(src_dir_str)
149
+ if src_dir_str not in added:
150
+ sys.path.insert(0, src_dir_str)
151
+ added.append(src_dir_str)
44
152
 
45
153
  site_packages_str = str(site_packages)
46
154
  sys.path.insert(0, site_packages_str)
@@ -218,64 +326,84 @@ def _accumulate_report(
218
326
  combined_report.errors.extend(file_report.errors)
219
327
 
220
328
 
329
+ # @shell_complexity: Path traversal logic for monorepo src detection
330
+ def _find_module_root(file_path: Path, project_root: Path | None) -> Path | None:
331
+ """
332
+ Find the module root directory (the directory that should be in sys.path).
333
+
334
+ For monorepo structures, finds the 'src' directory containing the file.
335
+ Falls back to project_root for standard layouts.
336
+
337
+ Examples:
338
+ >>> from pathlib import Path
339
+ >>> # Standard layout: project/src/pkg/module.py -> project/src
340
+ >>> # Monorepo: project/packages/a/src/pkg/module.py -> project/packages/a/src
341
+ """
342
+ if project_root is None:
343
+ return None
344
+
345
+ # Check if file is under a 'src' directory
346
+ try:
347
+ relative = file_path.relative_to(project_root)
348
+ parts = relative.parts
349
+
350
+ if "src" in parts:
351
+ idx = parts.index("src")
352
+ # Return the src directory itself
353
+ return project_root / Path(*parts[: idx + 1])
354
+ except ValueError:
355
+ pass
356
+
357
+ # Fallback: check if project_root/src exists and contains the file
358
+ src_dir = project_root / "src"
359
+ if src_dir.exists():
360
+ try:
361
+ file_path.relative_to(src_dir)
362
+ return src_dir
363
+ except ValueError:
364
+ pass
365
+
366
+ return project_root
367
+
368
+
221
369
  # @shell_complexity: BUG-57 fix requires package hierarchy setup for relative imports
222
370
  def _import_module_from_path(file_path: Path, project_root: Path | None = None) -> object | None:
223
371
  """
224
372
  Import a Python module from a file path.
225
373
 
226
374
  BUG-57: Properly handles relative imports by setting up package context.
375
+ Monorepo fix: Calculates module name relative to src directory, not project root.
227
376
 
228
377
  Returns None if import fails.
229
378
  """
379
+ import importlib
380
+
230
381
  try:
231
- # Calculate the full module name from project root
232
- if project_root and file_path.is_relative_to(project_root):
233
- # Convert path to module name: my_package/main.py -> my_package.main
382
+ # Find the module root (src directory) for this file
383
+ module_root = _find_module_root(file_path, project_root)
384
+
385
+ # Calculate module name relative to module_root (not project_root!)
386
+ if module_root and file_path.is_relative_to(module_root):
387
+ relative = file_path.relative_to(module_root)
388
+ parts = list(relative.with_suffix("").parts)
389
+ module_name = ".".join(parts)
390
+ elif project_root and file_path.is_relative_to(project_root):
391
+ # Fallback to project_root relative path
234
392
  relative = file_path.relative_to(project_root)
235
393
  parts = list(relative.with_suffix("").parts)
236
394
  module_name = ".".join(parts)
237
395
  else:
238
396
  module_name = file_path.stem
239
397
 
240
- # Ensure project root is in sys.path for relative imports
241
- if project_root:
242
- root_str = str(project_root)
398
+ # Ensure module root is in sys.path
399
+ if module_root:
400
+ root_str = str(module_root)
243
401
  if root_str not in sys.path:
244
402
  sys.path.insert(0, root_str)
245
403
 
246
- # For packages with relative imports, we need to set up parent packages first
247
- if "." in module_name:
248
- # Import parent packages first
249
- parts = module_name.split(".")
250
- for i in range(1, len(parts)):
251
- parent_name = ".".join(parts[:i])
252
- if parent_name not in sys.modules:
253
- parent_path = project_root / "/".join(parts[:i]) if project_root else None
254
- if parent_path and (parent_path / "__init__.py").exists():
255
- parent_spec = importlib.util.spec_from_file_location(
256
- parent_name,
257
- parent_path / "__init__.py",
258
- submodule_search_locations=[str(parent_path)],
259
- )
260
- if parent_spec and parent_spec.loader:
261
- parent_module = importlib.util.module_from_spec(parent_spec)
262
- sys.modules[parent_name] = parent_module
263
- parent_spec.loader.exec_module(parent_module)
264
-
265
- # Now import the target module
266
- spec = importlib.util.spec_from_file_location(
267
- module_name,
268
- file_path,
269
- submodule_search_locations=[str(file_path.parent)],
270
- )
271
- if spec is None or spec.loader is None:
272
- return None
273
-
274
- module = importlib.util.module_from_spec(spec)
275
- sys.modules[module_name] = module
276
-
277
- # Suppress output during import
278
- spec.loader.exec_module(module)
404
+ # Use importlib.import_module which correctly handles relative imports
405
+ # This is simpler and more reliable than manual spec loading
406
+ module = importlib.import_module(module_name)
279
407
  return module
280
408
 
281
409
  except Exception:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: invar-tools
3
- Version: 1.17.25
3
+ Version: 1.17.26
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
@@ -37,7 +37,7 @@ invar/core/timeout_inference.py,sha256=BS2fJGmwOrLpYZUku4qrizgNDSIXVLFBslW-6sRAv
37
37
  invar/core/trivial_detection.py,sha256=KYP8jJb7QDeusAxFdX5NAML_H0NL5wLgMeBWDQmNqfU,6086
38
38
  invar/core/ts_parsers.py,sha256=gXvLgb141gD8VAtiW4T1aaRnaSv10BEm0XYqArYIN00,8743
39
39
  invar/core/ts_sig_parser.py,sha256=_lUNfArFPILpENo-1dqmPY1qoVmcfAehv2Tva3r6dzw,9931
40
- invar/core/utils.py,sha256=PyW8dcTLUEFD81xcvkz-LNnCwjIQefn08OUh23fM_Po,14266
40
+ invar/core/utils.py,sha256=BMzMOJuCZ7I74AdM0PImzHypnIMO5OqsO9ms84x-fH0,14478
41
41
  invar/core/verification_routing.py,sha256=_jXi1txFCcUdnB3-Yavtuyk8N-XhEO_Vu_051Vuz27Y,5020
42
42
  invar/core/patterns/__init__.py,sha256=79a3ucN0BI54RnIOe49lngKASpADygs1hll9ROCrP6s,1429
43
43
  invar/core/patterns/detector.py,sha256=lUfED7qk2VWOAjHoGWgSqll5ynhhdzd6CjCiol-7kV8,8690
@@ -2670,7 +2670,7 @@ invar/shell/mutation.py,sha256=Lfyk2b8j8-hxAq-iwAgQeOhr7Ci6c5tRF1TXe3CxQCs,8914
2670
2670
  invar/shell/pattern_integration.py,sha256=pRcjfq3NvMW_tvQCnaXZnD1k5AVEWK8CYOE2jN6VTro,7842
2671
2671
  invar/shell/pi_hooks.py,sha256=ulZc1sP8mTRJTBsjwFHQzUgg-h8ajRIMp7iF1Y4UUtw,6885
2672
2672
  invar/shell/pi_tools.py,sha256=a3ACDDXykFV8fUB5UpBmgMvppwkmLvT1k_BWm0IY47k,4068
2673
- invar/shell/property_tests.py,sha256=qt0CP5RH9Md2ZZV64ziNsjQ_-x0onCYtZwbQfqw9gbY,12586
2673
+ invar/shell/property_tests.py,sha256=zKkCh03MaWTLnz_gfdTMeCGb2RMK_sz0L8PAcD8-iVU,17031
2674
2674
  invar/shell/py_refs.py,sha256=Vjz50lmt9prDBcBv4nkkODdiJ7_DKu5zO4UPZBjAfmM,4638
2675
2675
  invar/shell/skill_manager.py,sha256=Mr7Mh9rxPSKSAOTJCAM5ZHiG5nfUf6KQVCuD4LBNHSI,12440
2676
2676
  invar/shell/subprocess_env.py,sha256=hendEERSyAG4a8UFhYfPtOAlfspVRB03aVCYpj3uqk4,12745
@@ -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.25.dist-info/METADATA,sha256=t7Kc8ideD0woPDbz-PQiXfyQzlyBOPis0VZjDQHHCPU,28582
2782
- invar_tools-1.17.25.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
2783
- invar_tools-1.17.25.dist-info/entry_points.txt,sha256=RwH_EhqgtFPsnO6RcrwrAb70Zyfb8Mh6uUtztWnUxGk,102
2784
- invar_tools-1.17.25.dist-info/licenses/LICENSE,sha256=qeFksp4H4kfTgQxPCIu3OdagXyiZcgBlVfsQ6M5oFyk,10767
2785
- invar_tools-1.17.25.dist-info/licenses/LICENSE-GPL,sha256=IvZfC6ZbP7CLjytoHVzvpDZpD-Z3R_qa1GdMdWlWQ6Q,35157
2786
- invar_tools-1.17.25.dist-info/licenses/NOTICE,sha256=joEyMyFhFY8Vd8tTJ-a3SirI0m2Sd0WjzqYt3sdcglc,2561
2787
- invar_tools-1.17.25.dist-info/RECORD,,
2781
+ invar_tools-1.17.26.dist-info/METADATA,sha256=3iy3Ed3UYlIp66dIVkA4DvQZbADVG_IzGkYrmgLu7dI,28582
2782
+ invar_tools-1.17.26.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
2783
+ invar_tools-1.17.26.dist-info/entry_points.txt,sha256=RwH_EhqgtFPsnO6RcrwrAb70Zyfb8Mh6uUtztWnUxGk,102
2784
+ invar_tools-1.17.26.dist-info/licenses/LICENSE,sha256=qeFksp4H4kfTgQxPCIu3OdagXyiZcgBlVfsQ6M5oFyk,10767
2785
+ invar_tools-1.17.26.dist-info/licenses/LICENSE-GPL,sha256=IvZfC6ZbP7CLjytoHVzvpDZpD-Z3R_qa1GdMdWlWQ6Q,35157
2786
+ invar_tools-1.17.26.dist-info/licenses/NOTICE,sha256=joEyMyFhFY8Vd8tTJ-a3SirI0m2Sd0WjzqYt3sdcglc,2561
2787
+ invar_tools-1.17.26.dist-info/RECORD,,