py-import-checker 0.2.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,35 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.pyo
5
+ *.pyd
6
+ *.so
7
+ *.egg
8
+ *.egg-info/
9
+ dist/
10
+ build/
11
+ .eggs/
12
+
13
+ # Virtual environments
14
+ .venv/
15
+ venv/
16
+ env/
17
+ ENV/
18
+
19
+ # Tools
20
+ .ruff_cache/
21
+ .mypy_cache/
22
+ .pytest_cache/
23
+ .coverage
24
+ htmlcov/
25
+ *.prof
26
+
27
+ # IDEs
28
+ .idea/
29
+ .vscode/
30
+ *.swp
31
+ *.swo
32
+
33
+ # OS
34
+ .DS_Store
35
+ Thumbs.db
@@ -0,0 +1,26 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
6
+ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ---
9
+
10
+ ## [Unreleased]
11
+
12
+ ## [0.1.0] — 2026-06-08
13
+
14
+ ### Added
15
+ - Core `check_directory()` engine using `importlib.util`
16
+ - Zero dependencies — stdlib only
17
+ - CLI (`py-import-checker`) with `--src`, `--glob`, `--verbose` flags
18
+ - src-layout support via `--src` (repeatable)
19
+ - Auto-skip `.venv`, `venv`, `__pycache__`, `dist`, `build`
20
+ - Exit codes: `0` success · `1` broken imports · `2` bad args
21
+ - 9 unit tests (checker + CLI)
22
+ - GitHub Actions CI: Python 3.9–3.12 matrix + self-scan job
23
+ - `pyproject.toml` with Hatchling, ruff, mypy
24
+
25
+ [Unreleased]: https://github.com/matthieugraziani/py-import-checker/compare/v0.1.0...HEAD
26
+ [0.1.0]: https://github.com/matthieugraziani/py-import-checker/releases/tag/v0.1.0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 matthieugraziani
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,207 @@
1
+ Metadata-Version: 2.4
2
+ Name: py-import-checker
3
+ Version: 0.2.0
4
+ Summary: Fast, zero-dependency Python import health scanner
5
+ Project-URL: Homepage, https://github.com/matthieugraziani/py-import-checker
6
+ Project-URL: Repository, https://github.com/matthieugraziani/py-import-checker
7
+ Project-URL: Issues, https://github.com/matthieugraziani/py-import-checker/issues
8
+ Project-URL: Changelog, https://github.com/matthieugraziani/py-import-checker/blob/main/CHANGELOG.md
9
+ Author-email: Matthieu Graziani <matthieu.graziani@proton.me>
10
+ License: MIT
11
+ License-File: LICENSE
12
+ Keywords: cli,debugging,developer-tools,import-checker,imports,linting,static-analysis
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Environment :: Console
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3 :: Only
20
+ Classifier: Programming Language :: Python :: 3.9
21
+ Classifier: Programming Language :: Python :: 3.10
22
+ Classifier: Programming Language :: Python :: 3.11
23
+ Classifier: Programming Language :: Python :: 3.12
24
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
25
+ Classifier: Topic :: Software Development :: Quality Assurance
26
+ Classifier: Topic :: Software Development :: Testing
27
+ Requires-Python: >=3.9
28
+ Description-Content-Type: text/markdown
29
+
30
+ <p align="center">
31
+ <h1 align="center">py-import-checker</h1>
32
+ <p align="center">
33
+ <strong>Fast, zero-dependency Python import health scanner.</strong>
34
+ </p>
35
+
36
+ <p align="center">
37
+ Recursively scan any Python project and instantly surface every broken or missing import —
38
+ <strong>before your tests run, before CI fails, before runtime surprises you.</strong>
39
+ </p>
40
+
41
+ <p align="center">
42
+ <a href="https://pypi.org/project/py-import-checker/">
43
+ <img src="https://img.shields.io/pypi/v/py-import-checker.svg" alt="PyPI version">
44
+ </a>
45
+ <a href="https://github.com/matthieugraziani/py-import-checker/actions">
46
+ <img src="https://github.com/matthieugraziani/py-import-checker/actions/workflows/ci.yml/badge.svg" alt="Tests">
47
+ </a>
48
+ <a href="https://opensource.org/licenses/MIT">
49
+ <img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT">
50
+ </a>
51
+ <img src="https://img.shields.io/badge/Python-3.10%2B-blue" alt="Python 3.10+">
52
+ </p>
53
+ </p>
54
+
55
+ ```text
56
+ py-import-checker src/ --src src/
57
+
58
+ py-import-checker — Python import health scanner
59
+
60
+ Scanning /home/user/myproject/src
61
+
62
+ ──────────────────────────────────────────────────
63
+ ✗ mypackage/broken_module.py
64
+ ModuleNotFoundError: No module named 'nonexistent_lib'
65
+
66
+ ──────────────────────────────────────────────────
67
+ ✗ 1 broken import(s) found in 14 file(s) scanned.
68
+
69
+ ```
70
+
71
+ ## Features
72
+
73
+ - **Zero dependencies** — uses only the Python standard library (`importlib`, `pathlib`, `sys`)
74
+ - **src-layout aware** — pass `--src` to add extra directories to `sys.path`
75
+ - **Noise-free** — only reports `ImportError` / `ModuleNotFoundError`; ignores runtime exceptions
76
+ - **Auto-skips** virtual environments (`.venv`, `venv`) and build artefacts
77
+ - **CI-friendly** — exits with code `1` on any broken import, `0` on success
78
+ - **Self-checking** — the CI pipeline scans itself with `py-import-checker`
79
+
80
+
81
+ ## Installation
82
+
83
+ ```bash
84
+ pip install py-import-checker
85
+ ```
86
+
87
+ Or install from source (editable):
88
+
89
+ ```bash
90
+ git clone https://github.com/matthieugraziani/py-import-checker
91
+ cd py-import-checker
92
+ pip install -e .
93
+ ```
94
+
95
+ ## Usage
96
+
97
+ ### Command line
98
+
99
+ ```bash
100
+ # Scan the current directory
101
+ py-import-checker
102
+
103
+ # Scan a specific directory
104
+ py-import-checker path/to/project
105
+
106
+ # src-layout project (adds src/ to sys.path)
107
+ py-import-checker . --src src/
108
+
109
+ # Multiple extra paths
110
+ py-import-checker . --src src/ --src lib/
111
+
112
+ # Custom file glob
113
+ py-import-checker . --glob "app/**/*.py"
114
+
115
+ # Verbose output (show all files, not just errors)
116
+ py-import-checker . -v
117
+ ```
118
+
119
+ ### Python API
120
+
121
+ ```python
122
+ from pathlib import Path
123
+ from py_import_checker.checker import check_directory
124
+
125
+ result = check_directory(
126
+ root=Path("src/"),
127
+ extra_paths=[Path("src/")],
128
+ )
129
+
130
+ if not result.success:
131
+ for err in result.errors:
132
+ print(f"{err.file}: {err.error_type}: {err.message}")
133
+ ```
134
+
135
+ ### Pre-commit hook
136
+ Ajoutez ceci à votre fichier .pre-commit-config.yaml :
137
+ ```yaml
138
+ # .pre-commit-config.yaml
139
+ repos:
140
+ - repo: https://github.com/matthieugraziani/py-import-checker
141
+ rev: v0.1.0
142
+ hooks:
143
+ - id: py-import-checker
144
+ args: [--src, src/]
145
+ ```
146
+
147
+ ### GitHub Actions
148
+
149
+ ```yaml
150
+ - name: Check imports
151
+ run: |
152
+ pip install py-import-checker
153
+ py-import-checker . --src src/
154
+ ```
155
+
156
+
157
+
158
+ ## How it works
159
+
160
+ py-import-checker utilise importlib.util.spec_from_file_location pour charger chaque fichier .py dans un namespace isolé. Seules les erreurs d’import sont capturées — tout le reste (erreurs runtime, variables manquantes, etc.) est ignoré.
161
+
162
+
163
+ ## Roadmap (suggestions)
164
+
165
+ - Mode --fix (suggestions d’imports)
166
+ - Support des packages namespace (__init__.py moins strict)
167
+ - Intégration VS Code / LSP
168
+ - Rapport HTML / JSON
169
+ - Détection de circular imports (optionnel)
170
+
171
+ ---
172
+
173
+ ## Development
174
+
175
+ ```bash
176
+ # Install with dev extras
177
+ pip install -e ".[dev]"
178
+
179
+ # Run tests
180
+ pytest
181
+
182
+ # Lint
183
+ ruff check src/ tests/
184
+
185
+ # Type-check
186
+ mypy src/
187
+ ```
188
+
189
+ ---
190
+
191
+ ## License - MIT
192
+
193
+ Auteur : Matthieu Graziani
194
+ ```text
195
+ ### Améliorations apportées
196
+ - En-tête centré + badges propres
197
+ - Démo plus visible
198
+ - Sections plus aérées
199
+ - Roadmap ajoutée (pour montrer l’évolution)
200
+ - Meilleure lisibilité
201
+
202
+ ### Actions prioritaires maintenant
203
+ 1. **Publier sur PyPI** (version 0.1.0 ou 0.2.0) :
204
+ ```bash
205
+ hatch build
206
+ hatch publish
207
+ ```
@@ -0,0 +1,178 @@
1
+ <p align="center">
2
+ <h1 align="center">py-import-checker</h1>
3
+ <p align="center">
4
+ <strong>Fast, zero-dependency Python import health scanner.</strong>
5
+ </p>
6
+
7
+ <p align="center">
8
+ Recursively scan any Python project and instantly surface every broken or missing import —
9
+ <strong>before your tests run, before CI fails, before runtime surprises you.</strong>
10
+ </p>
11
+
12
+ <p align="center">
13
+ <a href="https://pypi.org/project/py-import-checker/">
14
+ <img src="https://img.shields.io/pypi/v/py-import-checker.svg" alt="PyPI version">
15
+ </a>
16
+ <a href="https://github.com/matthieugraziani/py-import-checker/actions">
17
+ <img src="https://github.com/matthieugraziani/py-import-checker/actions/workflows/ci.yml/badge.svg" alt="Tests">
18
+ </a>
19
+ <a href="https://opensource.org/licenses/MIT">
20
+ <img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT">
21
+ </a>
22
+ <img src="https://img.shields.io/badge/Python-3.10%2B-blue" alt="Python 3.10+">
23
+ </p>
24
+ </p>
25
+
26
+ ```text
27
+ py-import-checker src/ --src src/
28
+
29
+ py-import-checker — Python import health scanner
30
+
31
+ Scanning /home/user/myproject/src
32
+
33
+ ──────────────────────────────────────────────────
34
+ ✗ mypackage/broken_module.py
35
+ ModuleNotFoundError: No module named 'nonexistent_lib'
36
+
37
+ ──────────────────────────────────────────────────
38
+ ✗ 1 broken import(s) found in 14 file(s) scanned.
39
+
40
+ ```
41
+
42
+ ## Features
43
+
44
+ - **Zero dependencies** — uses only the Python standard library (`importlib`, `pathlib`, `sys`)
45
+ - **src-layout aware** — pass `--src` to add extra directories to `sys.path`
46
+ - **Noise-free** — only reports `ImportError` / `ModuleNotFoundError`; ignores runtime exceptions
47
+ - **Auto-skips** virtual environments (`.venv`, `venv`) and build artefacts
48
+ - **CI-friendly** — exits with code `1` on any broken import, `0` on success
49
+ - **Self-checking** — the CI pipeline scans itself with `py-import-checker`
50
+
51
+
52
+ ## Installation
53
+
54
+ ```bash
55
+ pip install py-import-checker
56
+ ```
57
+
58
+ Or install from source (editable):
59
+
60
+ ```bash
61
+ git clone https://github.com/matthieugraziani/py-import-checker
62
+ cd py-import-checker
63
+ pip install -e .
64
+ ```
65
+
66
+ ## Usage
67
+
68
+ ### Command line
69
+
70
+ ```bash
71
+ # Scan the current directory
72
+ py-import-checker
73
+
74
+ # Scan a specific directory
75
+ py-import-checker path/to/project
76
+
77
+ # src-layout project (adds src/ to sys.path)
78
+ py-import-checker . --src src/
79
+
80
+ # Multiple extra paths
81
+ py-import-checker . --src src/ --src lib/
82
+
83
+ # Custom file glob
84
+ py-import-checker . --glob "app/**/*.py"
85
+
86
+ # Verbose output (show all files, not just errors)
87
+ py-import-checker . -v
88
+ ```
89
+
90
+ ### Python API
91
+
92
+ ```python
93
+ from pathlib import Path
94
+ from py_import_checker.checker import check_directory
95
+
96
+ result = check_directory(
97
+ root=Path("src/"),
98
+ extra_paths=[Path("src/")],
99
+ )
100
+
101
+ if not result.success:
102
+ for err in result.errors:
103
+ print(f"{err.file}: {err.error_type}: {err.message}")
104
+ ```
105
+
106
+ ### Pre-commit hook
107
+ Ajoutez ceci à votre fichier .pre-commit-config.yaml :
108
+ ```yaml
109
+ # .pre-commit-config.yaml
110
+ repos:
111
+ - repo: https://github.com/matthieugraziani/py-import-checker
112
+ rev: v0.1.0
113
+ hooks:
114
+ - id: py-import-checker
115
+ args: [--src, src/]
116
+ ```
117
+
118
+ ### GitHub Actions
119
+
120
+ ```yaml
121
+ - name: Check imports
122
+ run: |
123
+ pip install py-import-checker
124
+ py-import-checker . --src src/
125
+ ```
126
+
127
+
128
+
129
+ ## How it works
130
+
131
+ py-import-checker utilise importlib.util.spec_from_file_location pour charger chaque fichier .py dans un namespace isolé. Seules les erreurs d’import sont capturées — tout le reste (erreurs runtime, variables manquantes, etc.) est ignoré.
132
+
133
+
134
+ ## Roadmap (suggestions)
135
+
136
+ - Mode --fix (suggestions d’imports)
137
+ - Support des packages namespace (__init__.py moins strict)
138
+ - Intégration VS Code / LSP
139
+ - Rapport HTML / JSON
140
+ - Détection de circular imports (optionnel)
141
+
142
+ ---
143
+
144
+ ## Development
145
+
146
+ ```bash
147
+ # Install with dev extras
148
+ pip install -e ".[dev]"
149
+
150
+ # Run tests
151
+ pytest
152
+
153
+ # Lint
154
+ ruff check src/ tests/
155
+
156
+ # Type-check
157
+ mypy src/
158
+ ```
159
+
160
+ ---
161
+
162
+ ## License - MIT
163
+
164
+ Auteur : Matthieu Graziani
165
+ ```text
166
+ ### Améliorations apportées
167
+ - En-tête centré + badges propres
168
+ - Démo plus visible
169
+ - Sections plus aérées
170
+ - Roadmap ajoutée (pour montrer l’évolution)
171
+ - Meilleure lisibilité
172
+
173
+ ### Actions prioritaires maintenant
174
+ 1. **Publier sur PyPI** (version 0.1.0 ou 0.2.0) :
175
+ ```bash
176
+ hatch build
177
+ hatch publish
178
+ ```
@@ -0,0 +1,109 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.27"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "py-import-checker"
7
+ version = "0.2.0" # ← mis à jour
8
+ description = "Fast, zero-dependency Python import health scanner"
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ authors = [{ name = "Matthieu Graziani", email = "matthieu.graziani@proton.me" }] # ← recommandé
12
+ requires-python = ">=3.9"
13
+ keywords = ["imports", "linting", "static-analysis", "debugging", "developer-tools", "cli", "import-checker"]
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta", # ← passé en Beta
16
+ "Environment :: Console",
17
+ "Intended Audience :: Developers",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Operating System :: OS Independent",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.9",
22
+ "Programming Language :: Python :: 3.10",
23
+ "Programming Language :: Python :: 3.11",
24
+ "Programming Language :: Python :: 3.12",
25
+ "Programming Language :: Python :: 3 :: Only",
26
+ "Topic :: Software Development :: Libraries :: Python Modules",
27
+ "Topic :: Software Development :: Quality Assurance",
28
+ "Topic :: Software Development :: Testing",
29
+ ]
30
+
31
+ [project.scripts]
32
+ py-import-checker = "py_import_checker.cli:main"
33
+
34
+ [project.urls]
35
+ Homepage = "https://github.com/matthieugraziani/py-import-checker"
36
+ Repository = "https://github.com/matthieugraziani/py-import-checker"
37
+ Issues = "https://github.com/matthieugraziani/py-import-checker/issues"
38
+ Changelog = "https://github.com/matthieugraziani/py-import-checker/blob/main/CHANGELOG.md"
39
+
40
+ # === Hatch Configuration ===
41
+ [tool.hatch.version]
42
+ path = "src/py_import_checker/__init__.py" # recommande cette approche (version centralisée)
43
+
44
+ [tool.hatch.build.targets.wheel]
45
+ packages = ["src/py_import_checker"]
46
+
47
+ [tool.hatch.build.targets.sdist]
48
+ include = [
49
+ "src/py_import_checker",
50
+ "tests",
51
+ "README.md",
52
+ "LICENSE",
53
+ "CHANGELOG.md",
54
+ ]
55
+
56
+ # === Environments ===
57
+ [tool.hatch.envs.default]
58
+ dependencies = [
59
+ "pytest>=8.0",
60
+ "pytest-cov>=6.0",
61
+ "mypy>=1.10",
62
+ "ruff>=0.9",
63
+ ]
64
+
65
+ [tool.hatch.envs.default.scripts]
66
+ lint = "ruff check src/ tests/"
67
+ format = "ruff format src/ tests/"
68
+ type-check = "mypy src/"
69
+ test = "pytest --cov=py_import_checker --cov-report=term-missing"
70
+ test-all = ["lint", "type-check", "test"]
71
+ all = ["format", "lint", "type-check", "test"]
72
+
73
+ [tool.hatch.envs.dev]
74
+ dependencies = [
75
+ "pytest>=8.0",
76
+ "pytest-cov>=6.0",
77
+ "mypy>=1.10",
78
+ "ruff>=0.9",
79
+ ]
80
+
81
+ # === Tooling Config ===
82
+ [tool.ruff]
83
+ line-length = 100
84
+ target-version = "py39"
85
+
86
+ [tool.ruff.lint]
87
+ select = ["E", "F", "I", "UP", "B", "SIM", "PL", "RUF"]
88
+ ignore = ["E501"]
89
+ fixable = ["ALL"]
90
+
91
+ [tool.ruff.lint.per-file-ignores]
92
+ "tests/**" = ["PLR2004"] # Magic values are common and acceptable in tests
93
+
94
+ [tool.mypy]
95
+ python_version = "3.10"
96
+ strict = true
97
+ ignore_missing_imports = true
98
+ disallow_untyped_defs = true
99
+ disallow_incomplete_defs = true
100
+
101
+ [tool.pytest.ini_options]
102
+ testpaths = ["tests"]
103
+ addopts = "-v --tb=short --cov=py_import_checker --cov-report=term-missing"
104
+ python_files = ["test_*.py", "*_test.py"]
105
+
106
+ # Optionnel : Coverage
107
+ [tool.coverage.run]
108
+ source = ["py_import_checker"]
109
+ omit = ["tests/*", "src/py_import_checker/__main__.py"]
@@ -0,0 +1,4 @@
1
+ """py-import-checker: Fast, zero-dependency Python import health scanner."""
2
+
3
+ __version__ = "0.1.0"
4
+ __author__ = "matthieugraziani"
@@ -0,0 +1,96 @@
1
+ """Core import verification engine."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import importlib.util
6
+ import sys
7
+ from dataclasses import dataclass, field
8
+ from pathlib import Path
9
+
10
+
11
+ @dataclass
12
+ class ImportFailure:
13
+ """Represents a single import failure."""
14
+
15
+ file: Path
16
+ error_type: str
17
+ message: str
18
+
19
+
20
+ @dataclass
21
+ class CheckResult:
22
+ """Aggregated result from a full scan."""
23
+
24
+ checked: int = 0
25
+ errors: list[ImportFailure] = field(default_factory=list)
26
+
27
+ @property
28
+ def success(self) -> bool:
29
+ return len(self.errors) == 0
30
+
31
+
32
+ _SKIP_PARTS = {".venv", "venv", "__pycache__", ".git", "node_modules", "dist", "build"}
33
+
34
+
35
+ def _should_skip(path: Path, script_path: Path) -> bool:
36
+ if path.resolve() == script_path.resolve():
37
+ return True
38
+ return any(part in _SKIP_PARTS for part in path.parts)
39
+
40
+
41
+ def check_directory(
42
+ root: Path,
43
+ extra_paths: list[Path] | None = None,
44
+ glob: str = "**/*.py",
45
+ ) -> CheckResult:
46
+ """
47
+ Scan all Python files under *root* and attempt to import each one.
48
+
49
+ Parameters
50
+ ----------
51
+ root:
52
+ Directory to scan recursively.
53
+ extra_paths:
54
+ Additional paths inserted at the front of sys.path before scanning
55
+ (e.g. the project ``src/`` directory).
56
+ glob:
57
+ Glob pattern used to find Python files (default: ``**/*.py``).
58
+ """
59
+ root = root.resolve()
60
+ script_path = Path(__file__).resolve()
61
+
62
+ paths_to_add = [root] + (extra_paths or [])
63
+ for p in reversed(paths_to_add):
64
+ p_str = str(p)
65
+ if p_str not in sys.path:
66
+ sys.path.insert(0, p_str)
67
+
68
+ result = CheckResult()
69
+
70
+ for file_path in sorted(root.glob(glob)):
71
+ if _should_skip(file_path, script_path):
72
+ continue
73
+
74
+ result.checked += 1
75
+ relative = file_path.relative_to(root)
76
+ module_name = ".".join(relative.with_suffix("").parts)
77
+
78
+ try:
79
+ spec = importlib.util.spec_from_file_location(module_name, file_path)
80
+ if spec and spec.loader:
81
+ module = importlib.util.module_from_spec(spec)
82
+ sys.modules[module_name] = module
83
+ spec.loader.exec_module(module)
84
+ except (ModuleNotFoundError, ImportError) as exc:
85
+ result.errors.append(
86
+ ImportFailure(
87
+ file=relative,
88
+ error_type=type(exc).__name__,
89
+ message=str(exc),
90
+ )
91
+ )
92
+ except Exception: # pylint: disable=broad-exception-caught
93
+ # Ignore pure runtime errors — only structural import issues matter.
94
+ pass
95
+
96
+ return result
@@ -0,0 +1,110 @@
1
+ """Command-line interface for py-import-checker."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ from .checker import CheckResult, check_directory
10
+
11
+ RESET = "\033[0m"
12
+ BOLD = "\033[1m"
13
+ GREEN = "\033[32m"
14
+ RED = "\033[31m"
15
+ YELLOW = "\033[33m"
16
+ CYAN = "\033[36m"
17
+ DIM = "\033[2m"
18
+
19
+
20
+ def _banner() -> None:
21
+ print(f"\n{BOLD}{CYAN}py-import-checker{RESET} — Python import health scanner\n")
22
+
23
+
24
+ def _print_result(result: CheckResult, verbose: bool) -> None:
25
+ if verbose:
26
+ print(f"{DIM}Verbose mode enabled.{RESET}\n")
27
+
28
+ if result.errors:
29
+ print(f"{YELLOW}{'─' * 50}{RESET}")
30
+ for err in result.errors:
31
+ print(f" {RED}✗{RESET} {BOLD}{err.file}{RESET}")
32
+ print(f" {DIM}{err.error_type}: {err.message}{RESET}\n")
33
+
34
+ print(f"{'─' * 50}")
35
+ total = result.checked
36
+ n_err = len(result.errors)
37
+
38
+ if result.success:
39
+ print(
40
+ f"{GREEN}{BOLD}✓ All clear!{RESET}"
41
+ f" {total} file(s) checked — no broken imports.\n"
42
+ )
43
+ else:
44
+ print(
45
+ f"{RED}{BOLD}✗ {n_err} broken import(s){RESET}"
46
+ f" found in {total} file(s) scanned.\n"
47
+ )
48
+
49
+
50
+ def main(argv: list[str] | None = None) -> int:
51
+ parser = argparse.ArgumentParser(
52
+ prog="py-import-checker",
53
+ description="Scan a Python project for broken imports.",
54
+ )
55
+ parser.add_argument(
56
+ "path",
57
+ nargs="?",
58
+ default=".",
59
+ help="Root directory to scan (default: current directory).",
60
+ )
61
+ parser.add_argument(
62
+ "--src",
63
+ metavar="DIR",
64
+ action="append",
65
+ default=[],
66
+ help=(
67
+ "Extra directory to prepend to sys.path (repeatable). "
68
+ "Useful for src-layout projects."
69
+ ),
70
+ )
71
+ parser.add_argument(
72
+ "--glob",
73
+ default="**/*.py",
74
+ help="Glob pattern for file discovery (default: **/*.py).",
75
+ )
76
+ parser.add_argument(
77
+ "-v",
78
+ "--verbose",
79
+ action="store_true",
80
+ help="Show all scanned files, not just errors.",
81
+ )
82
+ parser.add_argument(
83
+ "--version",
84
+ action="version",
85
+ version="%(prog)s 0.1.0",
86
+ )
87
+
88
+ args = parser.parse_args(argv)
89
+ root = Path(args.path).resolve()
90
+
91
+ if not root.exists():
92
+ print(f"{RED}Error: path '{root}' does not exist.{RESET}", file=sys.stderr)
93
+ return 2
94
+
95
+ extra = [Path(p).resolve() for p in args.src]
96
+
97
+ _banner()
98
+ print(f" {DIM}Scanning {root}{RESET}")
99
+ if extra:
100
+ print(f" {DIM}sys.path {', '.join(str(p) for p in extra)}{RESET}")
101
+ print()
102
+
103
+ result = check_directory(root, extra_paths=extra, glob=args.glob)
104
+ _print_result(result, args.verbose)
105
+
106
+ return 0 if result.success else 1
107
+
108
+
109
+ if __name__ == "__main__":
110
+ sys.exit(main())
File without changes
@@ -0,0 +1,125 @@
1
+ """Tests for py-import-checker."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import textwrap
6
+ from pathlib import Path
7
+
8
+ from py_import_checker.checker import check_directory
9
+ from py_import_checker.cli import main
10
+
11
+ # ---------------------------------------------------------------------------
12
+ # Test constants (évite les magic numbers → PLR2004)
13
+ # ---------------------------------------------------------------------------
14
+
15
+ EXPECTED_SUCCESS_CHECKED = 2
16
+ EXPECTED_MULTIPLE_ERRORS = 2
17
+ EXPECTED_MULTIPLE_CHECKED = 3
18
+ EXPECTED_CLI_SUCCESS = 0
19
+ EXPECTED_CLI_FAILURE = 1
20
+ EXPECTED_CLI_ERROR = 2 # Convention courante pour "command line usage error"
21
+
22
+
23
+ # ---------------------------------------------------------------------------
24
+ # Helpers
25
+ # ---------------------------------------------------------------------------
26
+
27
+
28
+ def write_py(tmp_path: Path, name: str, content: str) -> Path:
29
+ """Write a Python file inside *tmp_path* and return its path."""
30
+ p = tmp_path / name
31
+ p.write_text(textwrap.dedent(content))
32
+ return p
33
+
34
+
35
+ # ---------------------------------------------------------------------------
36
+ # checker.check_directory
37
+ # ---------------------------------------------------------------------------
38
+
39
+
40
+ def test_clean_project(tmp_path: Path) -> None:
41
+ write_py(tmp_path, "mod_a.py", "x = 1\n")
42
+ write_py(tmp_path, "mod_b.py", "from mod_a import x\n")
43
+
44
+ result = check_directory(tmp_path)
45
+
46
+ assert result.success
47
+ assert result.checked == EXPECTED_SUCCESS_CHECKED
48
+ assert result.errors == []
49
+
50
+
51
+ def test_broken_import_detected(tmp_path: Path) -> None:
52
+ write_py(tmp_path, "broken.py", "import _nonexistent_pkg_xyz\n")
53
+
54
+ result = check_directory(tmp_path)
55
+
56
+ assert not result.success
57
+ assert len(result.errors) == 1
58
+ assert result.errors[0].error_type == "ModuleNotFoundError"
59
+ assert "_nonexistent_pkg_xyz" in result.errors[0].message
60
+
61
+
62
+ def test_runtime_error_ignored(tmp_path: Path) -> None:
63
+ """Pure runtime errors (NameError, ZeroDivisionError…) must NOT be reported."""
64
+ write_py(tmp_path, "runtime_err.py", "x = 1 / 0\n")
65
+
66
+ result = check_directory(tmp_path)
67
+
68
+ assert result.success
69
+ assert result.checked == 1
70
+
71
+
72
+ def test_venv_skipped(tmp_path: Path) -> None:
73
+ venv_dir = tmp_path / ".venv" / "lib"
74
+ venv_dir.mkdir(parents=True)
75
+ (venv_dir / "something.py").write_text("import _nope\n")
76
+ write_py(tmp_path, "good.py", "pass\n")
77
+
78
+ result = check_directory(tmp_path)
79
+
80
+ assert result.success
81
+ assert result.checked == 1
82
+
83
+
84
+ def test_multiple_errors(tmp_path: Path) -> None:
85
+ write_py(tmp_path, "a.py", "import _nope_a\n")
86
+ write_py(tmp_path, "b.py", "import _nope_b\n")
87
+ write_py(tmp_path, "c.py", "pass\n")
88
+
89
+ result = check_directory(tmp_path)
90
+
91
+ assert not result.success
92
+ assert len(result.errors) == EXPECTED_MULTIPLE_ERRORS
93
+ assert result.checked == EXPECTED_MULTIPLE_CHECKED
94
+
95
+
96
+ # ---------------------------------------------------------------------------
97
+ # cli.main
98
+ # ---------------------------------------------------------------------------
99
+
100
+
101
+ def test_cli_success(tmp_path: Path) -> None:
102
+ write_py(tmp_path, "ok.py", "x = 42\n")
103
+ code = main([str(tmp_path)])
104
+ assert code == EXPECTED_CLI_SUCCESS
105
+
106
+
107
+ def test_cli_failure(tmp_path: Path) -> None:
108
+ write_py(tmp_path, "bad.py", "import _nope_cli\n")
109
+ code = main([str(tmp_path)])
110
+ assert code == EXPECTED_CLI_FAILURE
111
+
112
+
113
+ def test_cli_missing_path(tmp_path: Path) -> None:
114
+ code = main([str(tmp_path / "does_not_exist")])
115
+ assert code == EXPECTED_CLI_ERROR
116
+
117
+
118
+ def test_cli_src_flag(tmp_path: Path) -> None:
119
+ src = tmp_path / "src"
120
+ src.mkdir()
121
+ (src / "mylib.py").write_text("VALUE = 99\n")
122
+ write_py(tmp_path, "consumer.py", "from mylib import VALUE\n")
123
+
124
+ code = main([str(tmp_path), "--src", str(src)])
125
+ assert code == EXPECTED_CLI_SUCCESS