import-mend 0.1.0__tar.gz

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.
@@ -0,0 +1,11 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ dist/
5
+ build/
6
+ .venv/
7
+ .env
8
+ *.egg
9
+ .pytest_cache/
10
+ .coverage
11
+ htmlcov/
@@ -0,0 +1,105 @@
1
+ Metadata-Version: 2.4
2
+ Name: import-mend
3
+ Version: 0.1.0
4
+ Summary: Detect and repair broken Python imports after refactoring, with zero runtime cost
5
+ License: MIT
6
+ Keywords: ast,developer-tools,imports,refactoring,static-analysis
7
+ Classifier: Development Status :: 3 - Alpha
8
+ Classifier: Intended Audience :: Developers
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
15
+ Classifier: Topic :: Software Development :: Quality Assurance
16
+ Requires-Python: >=3.10
17
+ Requires-Dist: libcst>=1.0.0
18
+ Provides-Extra: dev
19
+ Requires-Dist: pytest-cov; extra == 'dev'
20
+ Requires-Dist: pytest>=7.0; extra == 'dev'
21
+ Description-Content-Type: text/markdown
22
+
23
+ # import-check
24
+
25
+ Detect and repair broken Python imports after refactoring — with zero runtime cost.
26
+
27
+ ## What it does
28
+
29
+ After a codebase refactoring (manual or autonomous), module paths shift. Every file that
30
+ imported moved symbols now references a path that no longer exists. `import-check` fixes
31
+ this retroactively:
32
+
33
+ 1. **Inventories** symbols before and after using git history + AST (no code executed)
34
+ 2. **Detects** moves, renames, splits, and merges in the migration map
35
+ 3. **Rewrites** all broken imports in-place using `libcst` (format-preserving)
36
+ 4. **Verifies** every import via filesystem + AST checks (zero runtime loading)
37
+
38
+ ## Installation
39
+
40
+ ```bash
41
+ pip install import-check
42
+ ```
43
+
44
+ ## Usage
45
+
46
+ ```bash
47
+ # Full pipeline: fix then verify
48
+ python -m import_check run
49
+
50
+ # Fix broken imports only
51
+ python -m import_check fix
52
+
53
+ # Verify imports only (CI gate)
54
+ python -m import_check check
55
+
56
+ # With options
57
+ python -m import_check run \
58
+ --source-dirs src lib \
59
+ --git-ref HEAD~3 \
60
+ --format json \
61
+ --log-level DEBUG
62
+ ```
63
+
64
+ ## Programmatic API
65
+
66
+ ```python
67
+ from import_check import fix, check, run
68
+
69
+ # Full pipeline
70
+ result = run(root="/path/to/project", source_dirs=["src"], git_ref="HEAD~1")
71
+
72
+ # Fix only
73
+ fix_results = fix(root=".", git_ref="abc123")
74
+
75
+ # Check only
76
+ errors = check(root=".", encapsulation_check=True)
77
+ ```
78
+
79
+ ## Configuration
80
+
81
+ Add to `pyproject.toml`:
82
+
83
+ ```toml
84
+ [tool.import_check]
85
+ source_dirs = ["src", "lib"]
86
+ exclude_patterns = [".venv", "__pycache__", "node_modules"]
87
+ git_ref = "HEAD"
88
+ encapsulation_check = true
89
+ output_format = "human" # or "json"
90
+ log_level = "INFO" # or "DEBUG", "ERROR"
91
+ check_stubs = true # .pyi stub fallback
92
+ check_getattr = true # __getattr__ suppression
93
+ ```
94
+
95
+ ## Design
96
+
97
+ - **Deterministic-first:** AST-based analysis handles the common case. LLM integration
98
+ for residuals is out of scope — the structured error list is the handoff contract.
99
+ - **Zero runtime cost:** The checker never loads a module. Safe in any environment.
100
+ - **Generic:** Works on any Python project with git. No project-specific config required.
101
+ - **Single dependency:** Only `libcst` beyond the standard library.
102
+
103
+ ## License
104
+
105
+ MIT
@@ -0,0 +1,83 @@
1
+ # import-check
2
+
3
+ Detect and repair broken Python imports after refactoring — with zero runtime cost.
4
+
5
+ ## What it does
6
+
7
+ After a codebase refactoring (manual or autonomous), module paths shift. Every file that
8
+ imported moved symbols now references a path that no longer exists. `import-check` fixes
9
+ this retroactively:
10
+
11
+ 1. **Inventories** symbols before and after using git history + AST (no code executed)
12
+ 2. **Detects** moves, renames, splits, and merges in the migration map
13
+ 3. **Rewrites** all broken imports in-place using `libcst` (format-preserving)
14
+ 4. **Verifies** every import via filesystem + AST checks (zero runtime loading)
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ pip install import-check
20
+ ```
21
+
22
+ ## Usage
23
+
24
+ ```bash
25
+ # Full pipeline: fix then verify
26
+ python -m import_check run
27
+
28
+ # Fix broken imports only
29
+ python -m import_check fix
30
+
31
+ # Verify imports only (CI gate)
32
+ python -m import_check check
33
+
34
+ # With options
35
+ python -m import_check run \
36
+ --source-dirs src lib \
37
+ --git-ref HEAD~3 \
38
+ --format json \
39
+ --log-level DEBUG
40
+ ```
41
+
42
+ ## Programmatic API
43
+
44
+ ```python
45
+ from import_check import fix, check, run
46
+
47
+ # Full pipeline
48
+ result = run(root="/path/to/project", source_dirs=["src"], git_ref="HEAD~1")
49
+
50
+ # Fix only
51
+ fix_results = fix(root=".", git_ref="abc123")
52
+
53
+ # Check only
54
+ errors = check(root=".", encapsulation_check=True)
55
+ ```
56
+
57
+ ## Configuration
58
+
59
+ Add to `pyproject.toml`:
60
+
61
+ ```toml
62
+ [tool.import_check]
63
+ source_dirs = ["src", "lib"]
64
+ exclude_patterns = [".venv", "__pycache__", "node_modules"]
65
+ git_ref = "HEAD"
66
+ encapsulation_check = true
67
+ output_format = "human" # or "json"
68
+ log_level = "INFO" # or "DEBUG", "ERROR"
69
+ check_stubs = true # .pyi stub fallback
70
+ check_getattr = true # __getattr__ suppression
71
+ ```
72
+
73
+ ## Design
74
+
75
+ - **Deterministic-first:** AST-based analysis handles the common case. LLM integration
76
+ for residuals is out of scope — the structured error list is the handoff contract.
77
+ - **Zero runtime cost:** The checker never loads a module. Safe in any environment.
78
+ - **Generic:** Works on any Python project with git. No project-specific config required.
79
+ - **Single dependency:** Only `libcst` beyond the standard library.
80
+
81
+ ## License
82
+
83
+ MIT
@@ -0,0 +1,52 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "import-mend"
7
+ version = "0.1.0"
8
+ description = "Detect and repair broken Python imports after refactoring, with zero runtime cost"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = {text = "MIT"}
12
+ keywords = ["imports", "refactoring", "ast", "static-analysis", "developer-tools"]
13
+ classifiers = [
14
+ "Development Status :: 3 - Alpha",
15
+ "Intended Audience :: Developers",
16
+ "License :: OSI Approved :: MIT License",
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3.10",
19
+ "Programming Language :: Python :: 3.11",
20
+ "Programming Language :: Python :: 3.12",
21
+ "Topic :: Software Development :: Libraries :: Python Modules",
22
+ "Topic :: Software Development :: Quality Assurance",
23
+ ]
24
+ dependencies = [
25
+ "libcst>=1.0.0",
26
+ ]
27
+
28
+ [project.optional-dependencies]
29
+ dev = [
30
+ "pytest>=7.0",
31
+ "pytest-cov",
32
+ ]
33
+
34
+ [project.scripts]
35
+ import-mend = "import_mend.__main__:main"
36
+
37
+ [tool.hatch.build.targets.wheel]
38
+ packages = ["src/import_mend"]
39
+
40
+ [tool.pytest.ini_options]
41
+ testpaths = ["tests"]
42
+ addopts = "-v"
43
+
44
+ [tool.import_mend]
45
+ source_dirs = ["src"]
46
+ exclude_patterns = [".venv", "__pycache__", "node_modules", ".git"]
47
+ git_ref = "HEAD"
48
+ encapsulation_check = true
49
+ output_format = "human"
50
+ log_level = "INFO"
51
+ check_stubs = true
52
+ check_getattr = true
@@ -0,0 +1,262 @@
1
+ # @summary
2
+ # Public API facade for the import_mend tool.
3
+ # Provides three entry points: fix() for deterministic import rewriting,
4
+ # check() for smoke-testing all imports, and run() for fix-then-check.
5
+ # Loads configuration from pyproject.toml [tool.import_mend] with overrides.
6
+ # Exports: fix, check, run, ImportCheckConfig, RunResult, FixResult, ImportError
7
+ # Deps: tomllib/tomli, logging, pathlib, import_mend.schemas,
8
+ # import_mend.inventory, import_mend.differ, import_mend.fixer,
9
+ # import_mend.checker
10
+ # @end-summary
11
+
12
+ """Public API facade for the import_mend tool.
13
+
14
+ Provides three entry points:
15
+
16
+ - :func:`fix` — deterministic import rewriting based on git-diff inventory.
17
+ - :func:`check` — smoke-test all imports for resolution and encapsulation.
18
+ - :func:`run` — fix then check (the full pipeline).
19
+
20
+ Configuration is loaded from ``pyproject.toml`` ``[tool.import_mend]`` section
21
+ with programmatic overrides applied on top.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import logging
27
+ from dataclasses import fields
28
+ from pathlib import Path
29
+
30
+ from .schemas import (
31
+ FixResult,
32
+ ImportCheckConfig,
33
+ ImportError,
34
+ RunResult,
35
+ )
36
+
37
+ __all__ = [
38
+ "fix",
39
+ "check",
40
+ "run",
41
+ "ImportCheckConfig",
42
+ "RunResult",
43
+ "FixResult",
44
+ "ImportError",
45
+ ]
46
+
47
+
48
+ # ---------------------------------------------------------------------------
49
+ # Internal helpers
50
+ # ---------------------------------------------------------------------------
51
+
52
+
53
+ def _load_config(root: Path, **overrides: object) -> ImportCheckConfig:
54
+ """Load configuration with resolution order: defaults < pyproject.toml < overrides.
55
+
56
+ Reads ``[tool.import_mend]`` from ``pyproject.toml`` at *root* (if it exists),
57
+ merges with dataclass defaults, then applies any keyword overrides.
58
+
59
+ Args:
60
+ root: Project root directory containing ``pyproject.toml``.
61
+ **overrides: Keyword arguments that override both defaults and file values.
62
+
63
+ Returns:
64
+ Fully resolved :class:`ImportCheckConfig`.
65
+ """
66
+ # --- 1. Start with dataclass defaults (implicit via constructor) ---
67
+ defaults: dict[str, object] = {}
68
+
69
+ # --- 2. Read pyproject.toml if present ---
70
+ pyproject_path = root / "pyproject.toml"
71
+ file_values: dict[str, object] = {}
72
+
73
+ if pyproject_path.is_file():
74
+ try:
75
+ import tomllib # Python 3.11+
76
+ except ModuleNotFoundError:
77
+ import tomli as tomllib # type: ignore[no-redef]
78
+
79
+ try:
80
+ with open(pyproject_path, "rb") as f:
81
+ data = tomllib.load(f)
82
+ file_values = data.get("tool", {}).get("import_mend", {})
83
+ except Exception: # noqa: BLE001
84
+ logging.getLogger("import_mend").warning(
85
+ "Failed to parse pyproject.toml at %s, using defaults", pyproject_path,
86
+ )
87
+
88
+ # --- 3. Merge: defaults < file < overrides ---
89
+ # Collect valid field names from the dataclass.
90
+ valid_fields = {f.name for f in fields(ImportCheckConfig)}
91
+
92
+ merged: dict[str, object] = {}
93
+
94
+ # Apply file values (only recognised keys).
95
+ for key, value in file_values.items():
96
+ if key in valid_fields:
97
+ merged[key] = value
98
+
99
+ # Apply programmatic overrides (highest priority).
100
+ for key, value in overrides.items():
101
+ if key in valid_fields:
102
+ merged[key] = value
103
+
104
+ # Always set root.
105
+ merged["root"] = root
106
+
107
+ return ImportCheckConfig(**merged) # type: ignore[arg-type]
108
+
109
+
110
+ def _setup_logging(config: ImportCheckConfig) -> logging.Logger:
111
+ """Configure the ``import_mend`` logger with the level from *config*.
112
+
113
+ Format: ``%(levelname)s: %(message)s``.
114
+
115
+ Args:
116
+ config: Configuration with ``log_level`` field.
117
+
118
+ Returns:
119
+ The configured :class:`logging.Logger`.
120
+ """
121
+ logger = logging.getLogger("import_mend")
122
+ logger.setLevel(config.log_level.upper())
123
+
124
+ # Avoid duplicate handlers on repeated calls.
125
+ if not logger.handlers:
126
+ handler = logging.StreamHandler()
127
+ handler.setFormatter(logging.Formatter("%(levelname)s: %(message)s"))
128
+ logger.addHandler(handler)
129
+
130
+ return logger
131
+
132
+
133
+ # ---------------------------------------------------------------------------
134
+ # Public entry points
135
+ # ---------------------------------------------------------------------------
136
+
137
+
138
+ def fix(root: str | Path | None = None, **config_overrides: object) -> FixResult:
139
+ """Deterministic fix pipeline: diff git inventories, then rewrite imports.
140
+
141
+ Steps:
142
+ 1. Resolve *root* (defaults to cwd).
143
+ 2. Load configuration.
144
+ 3. Setup logging.
145
+ 4. Get changed files since ``git_ref``.
146
+ 5. Build old (git) and new (filesystem) symbol inventories.
147
+ 6. Diff inventories to produce a migration map.
148
+ 7. If no migrations, return an empty :class:`FixResult`.
149
+ 8. Collect all Python files across configured source directories.
150
+ 9. Apply import fixes using the migration map.
151
+ 10. Return the :class:`FixResult`.
152
+
153
+ Args:
154
+ root: Project root directory. Defaults to the current working directory.
155
+ **config_overrides: Overrides forwarded to :func:`_load_config`.
156
+
157
+ Returns:
158
+ :class:`FixResult` summarising files modified and fixes applied.
159
+ """
160
+ from . import differ, fixer, inventory
161
+
162
+ # 1. Resolve root.
163
+ resolved_root = Path(root).resolve() if root is not None else Path.cwd().resolve()
164
+
165
+ # 2. Load config.
166
+ config = _load_config(resolved_root, **config_overrides)
167
+
168
+ # 3. Setup logging.
169
+ logger = _setup_logging(config)
170
+
171
+ # 4. Get changed files.
172
+ changed_files = inventory.get_changed_files(config.git_ref, resolved_root)
173
+ logger.info("Found %d changed files", len(changed_files))
174
+
175
+ if not changed_files:
176
+ return FixResult()
177
+
178
+ # 5. Build old and new inventories.
179
+ old_inv = inventory.build_old_inventory(changed_files, config.git_ref, resolved_root)
180
+ logger.info("Built old inventory: %d symbols", sum(len(v) for v in old_inv.values()))
181
+
182
+ new_inv = inventory.build_inventory(changed_files, resolved_root)
183
+ logger.info("Built new inventory: %d symbols", sum(len(v) for v in new_inv.values()))
184
+
185
+ # 6. Diff inventories.
186
+ migration_map = differ.diff_inventories(old_inv, new_inv)
187
+ logger.info("Migration map: %d entries", len(migration_map))
188
+
189
+ # 7. If no migrations, return empty result.
190
+ if not migration_map:
191
+ return FixResult()
192
+
193
+ # 8. Collect all Python files.
194
+ all_files = inventory.collect_python_files(
195
+ config.source_dirs, resolved_root, config.exclude_patterns,
196
+ )
197
+ logger.info("Applying fixes to %d files", len(all_files))
198
+
199
+ # 9. Apply fixes.
200
+ result = fixer.apply_fixes(migration_map, all_files, resolved_root)
201
+
202
+ return result
203
+
204
+
205
+ def check(
206
+ root: str | Path | None = None, **config_overrides: object
207
+ ) -> list[ImportError]:
208
+ """Smoke-test all imports for resolution and encapsulation errors.
209
+
210
+ Steps:
211
+ 1. Resolve *root* (defaults to cwd).
212
+ 2. Load configuration.
213
+ 3. Setup logging.
214
+ 4. Collect all Python files across configured source directories.
215
+ 5. Run import checks.
216
+
217
+ Args:
218
+ root: Project root directory. Defaults to the current working directory.
219
+ **config_overrides: Overrides forwarded to :func:`_load_config`.
220
+
221
+ Returns:
222
+ List of :class:`ImportError` records. Empty if all imports are clean.
223
+ """
224
+ from . import checker, inventory
225
+
226
+ # 1. Resolve root.
227
+ resolved_root = Path(root).resolve() if root is not None else Path.cwd().resolve()
228
+
229
+ # 2. Load config.
230
+ config = _load_config(resolved_root, **config_overrides)
231
+
232
+ # 3. Setup logging.
233
+ logger = _setup_logging(config)
234
+
235
+ # 4. Collect all Python files.
236
+ all_files = inventory.collect_python_files(
237
+ config.source_dirs, resolved_root, config.exclude_patterns,
238
+ )
239
+ logger.info("Checking imports in %d files", len(all_files))
240
+
241
+ # 5. Run checker.
242
+ errors = checker.check_imports(all_files, resolved_root, config)
243
+
244
+ logger.info("Found %d import errors", len(errors))
245
+
246
+ return errors
247
+
248
+
249
+ def run(root: str | Path | None = None, **config_overrides: object) -> RunResult:
250
+ """Run the full pipeline: fix imports, then check for remaining errors.
251
+
252
+ Args:
253
+ root: Project root directory. Defaults to the current working directory.
254
+ **config_overrides: Overrides forwarded to :func:`_load_config`.
255
+
256
+ Returns:
257
+ :class:`RunResult` containing the fix result and any remaining errors.
258
+ """
259
+ fix_result = fix(root=root, **config_overrides)
260
+ remaining_errors = check(root=root, **config_overrides)
261
+
262
+ return RunResult(fix_result=fix_result, remaining_errors=remaining_errors)