py-import-checker 0.2.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.
@@ -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())
@@ -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,8 @@
1
+ py_import_checker/__init__.py,sha256=_Kqs_c_gOezQLrrUtJzqCIPzAUhvptLTiNpTQL0ATbA,132
2
+ py_import_checker/checker.py,sha256=ufMx73ln1izcXDaVCFmzLFRkAGoE9Wlg59rkRg2fYig,2636
3
+ py_import_checker/cli.py,sha256=Bp-lIjN0PvDOYG3copfT9Yk1mKlwhQkaQf7FDzcX-_I,2837
4
+ py_import_checker-0.2.0.dist-info/METADATA,sha256=THXNzQFaxLhGw_KCNGOIidXMVESLUEULqSQ6NPpkZ7I,5933
5
+ py_import_checker-0.2.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
6
+ py_import_checker-0.2.0.dist-info/entry_points.txt,sha256=v3aRM_0I1dsUqInJORiGMAolzWrl-lUsEMR_UAb3-uU,65
7
+ py_import_checker-0.2.0.dist-info/licenses/LICENSE,sha256=T1pyG9Juy8e2d-i9ga0XhleFuGWgDqu4sWjeUIUaXIM,1073
8
+ py_import_checker-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ py-import-checker = py_import_checker.cli:main
@@ -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.