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.
- py_import_checker-0.2.0/.gitignore +35 -0
- py_import_checker-0.2.0/CHANGELOG.md +26 -0
- py_import_checker-0.2.0/LICENSE +21 -0
- py_import_checker-0.2.0/PKG-INFO +207 -0
- py_import_checker-0.2.0/README.md +178 -0
- py_import_checker-0.2.0/pyproject.toml +109 -0
- py_import_checker-0.2.0/src/py_import_checker/__init__.py +4 -0
- py_import_checker-0.2.0/src/py_import_checker/checker.py +96 -0
- py_import_checker-0.2.0/src/py_import_checker/cli.py +110 -0
- py_import_checker-0.2.0/tests/__init__.py +0 -0
- py_import_checker-0.2.0/tests/test_checker.py +125 -0
|
@@ -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,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
|