python-qv 0.1.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.
- python_qv-0.1.0.dist-info/METADATA +281 -0
- python_qv-0.1.0.dist-info/RECORD +33 -0
- python_qv-0.1.0.dist-info/WHEEL +4 -0
- python_qv-0.1.0.dist-info/entry_points.txt +4 -0
- qv/__init__.py +3 -0
- qv/analyzers/__init__.py +1 -0
- qv/analyzers/ci/__init__.py +1 -0
- qv/analyzers/dependencies/__init__.py +5 -0
- qv/analyzers/dependencies/analyzer.py +347 -0
- qv/analyzers/docker/__init__.py +1 -0
- qv/analyzers/environment/__init__.py +5 -0
- qv/analyzers/environment/drift.py +164 -0
- qv/analyzers/imports/__init__.py +5 -0
- qv/analyzers/imports/analyzer.py +160 -0
- qv/analyzers/packaging/__init__.py +5 -0
- qv/analyzers/packaging/analyzer.py +117 -0
- qv/analyzers/security/__init__.py +1 -0
- qv/cli/__init__.py +5 -0
- qv/cli/main.py +253 -0
- qv/core/analyzer.py +21 -0
- qv/core/config.py +122 -0
- qv/core/context.py +105 -0
- qv/core/engine.py +116 -0
- qv/core/models.py +132 -0
- qv/core/project.py +330 -0
- qv/frameworks/__init__.py +1 -0
- qv/integrations/__init__.py +1 -0
- qv/reporters/__init__.py +13 -0
- qv/reporters/base.py +15 -0
- qv/reporters/json_reporter.py +16 -0
- qv/reporters/sarif.py +85 -0
- qv/reporters/terminal.py +126 -0
- qv/rules/registry.py +151 -0
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: python-qv
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Diagnose why a Python project is unhealthy — explain root cause and suggest safe fixes.
|
|
5
|
+
Project-URL: Homepage, https://github.com/inzamol/qv
|
|
6
|
+
Project-URL: Repository, https://github.com/inzamol/qv
|
|
7
|
+
Project-URL: Documentation, https://github.com/inzamol/qv#readme
|
|
8
|
+
Project-URL: Issues, https://github.com/inzamol/qv/issues
|
|
9
|
+
Author: Inzamul Hoque
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Software Development :: Bug Tracking
|
|
20
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Requires-Dist: click>=8.1.0
|
|
23
|
+
Requires-Dist: packaging>=23.2
|
|
24
|
+
Requires-Dist: pydantic>=2.5.0
|
|
25
|
+
Requires-Dist: rich>=13.7.0
|
|
26
|
+
Requires-Dist: tomli>=2.0.1; python_version < '3.11'
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: pyright>=1.1.350; extra == 'dev'
|
|
29
|
+
Requires-Dist: pytest-cov>=4.1.0; extra == 'dev'
|
|
30
|
+
Requires-Dist: pytest>=8.0.0; extra == 'dev'
|
|
31
|
+
Requires-Dist: ruff>=0.4.0; extra == 'dev'
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
|
|
34
|
+
# 🔍 qv
|
|
35
|
+
|
|
36
|
+
<div align="center">
|
|
37
|
+
|
|
38
|
+
**Diagnose why your Python project is unhealthy — understand the root cause and get safe, actionable fixes.**
|
|
39
|
+
|
|
40
|
+
[](https://pypi.org/project/python-qv/)
|
|
41
|
+
[](https://pypi.org/project/python-qv/)
|
|
42
|
+
[](https://github.com/inzamol/qv/actions/workflows/ci.yml)
|
|
43
|
+
[](https://opensource.org/licenses/MIT)
|
|
44
|
+
|
|
45
|
+
[Installation](#-installation) • [Quick Start](#-quick-start) • [Features](#-what-it-detects) • [CLI Commands](#-cli-commands) • [CI/CD Integration](#-cicd-integration) • [Documentation](docs/getting_started.md)
|
|
46
|
+
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## 💡 Why qv?
|
|
52
|
+
|
|
53
|
+
Python projects rarely fail because of Python syntax. They fail because of **ecosystem friction**:
|
|
54
|
+
- Incompatible transitive dependency constraints that break your resolver.
|
|
55
|
+
- Missing dependencies you forgot to add to `pyproject.toml`.
|
|
56
|
+
- Docker containers running Python 3.10 while your team develops on 3.12.
|
|
57
|
+
- Silent circular imports that only crash at runtime when certain modules load.
|
|
58
|
+
|
|
59
|
+
Instead of parsing hundreds of lines of cryptic resolver logs, **`qv`** scans your project in milliseconds, pinpoints the root cause, shows the exact evidence, and gives you a copy-paste command to fix it.
|
|
60
|
+
|
|
61
|
+
```text
|
|
62
|
+
🔍 qv
|
|
63
|
+
Project: payment-service
|
|
64
|
+
Python: 3.12.7
|
|
65
|
+
Package Manager: uv
|
|
66
|
+
|
|
67
|
+
🔴 1 Errors 🟡 1 Warnings 🟢 48 Checks Passed
|
|
68
|
+
|
|
69
|
+
┌────────────────── 🔴 DEP-001 Dependency constraint conflict ─────────────────┐
|
|
70
|
+
│ celery 5.4.0 requires kombu<5.4.0,>=5.3.0, but installed is kombu 5.5.2. │
|
|
71
|
+
│ │
|
|
72
|
+
│ Evidence: │
|
|
73
|
+
│ • celery declared requirement: kombu<5.4.0,>=5.3.0 │
|
|
74
|
+
│ • Installed kombu version: 5.5.2 in active environment │
|
|
75
|
+
│ │
|
|
76
|
+
│ Suggested fix: │
|
|
77
|
+
│ Upgrade celery or pin kombu to <5.4.0,>=5.3.0. │
|
|
78
|
+
│ $ uv add 'kombu<5.4.0,>=5.3.0' │
|
|
79
|
+
└──────────────────────────────────────────────────────────────────────────────┘
|
|
80
|
+
|
|
81
|
+
Health Score: 85/100
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## 📦 Installation
|
|
87
|
+
|
|
88
|
+
Install `python-qv` into your virtual environment (provides the `qv` CLI):
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
# Using pip
|
|
92
|
+
pip install python-qv
|
|
93
|
+
|
|
94
|
+
# Using uv
|
|
95
|
+
uv add python-qv --dev
|
|
96
|
+
|
|
97
|
+
# Run directly without installing (via uvx or pipx)
|
|
98
|
+
uvx python-qv scan
|
|
99
|
+
# or
|
|
100
|
+
pipx run python-qv scan
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## 🚀 Quick Start
|
|
106
|
+
|
|
107
|
+
### 1. Run a Health Scan
|
|
108
|
+
|
|
109
|
+
Run `qv scan` inside any Python repository:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
qv scan
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### 2. Understand Any Flagged Issue
|
|
116
|
+
|
|
117
|
+
Need more context on why a rule triggered? Run `explain`:
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
qv explain DEP-001
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### 3. Add Project Configuration
|
|
124
|
+
|
|
125
|
+
To add default configuration to your `pyproject.toml`:
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
qv init
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## 🔍 What It Detects
|
|
134
|
+
|
|
135
|
+
| Category | Rule ID | Description | Default Severity |
|
|
136
|
+
|---|---|---|---|
|
|
137
|
+
| **Dependencies** | `DEP-001` | Incompatible package version constraints across dependency tree | `ERROR` |
|
|
138
|
+
| | `DEP-002` | Third-party packages imported in code but missing from `pyproject.toml` | `ERROR` |
|
|
139
|
+
| | `DEP-003` | Declared dependencies that are never imported anywhere in project | `WARNING` |
|
|
140
|
+
| | `DEP-004` | Package requires a Python version incompatible with target runtime | `WARNING` |
|
|
141
|
+
| | `DEP-005` | Installed virtualenv version does not match declared manifest pin | `WARNING` |
|
|
142
|
+
| **Environment** | `ENV-001` | Active interpreter version differs from project `requires-python` | `WARNING` |
|
|
143
|
+
| | `ENV-002` | Dockerfile base image Python version differs from project runtime | `WARNING` |
|
|
144
|
+
| | `ENV-003` | CI matrix does not cover the Python versions declared in project | `WARNING` |
|
|
145
|
+
| **Architecture** | `IMP-001` | Circular import cycles across local modules | `ERROR` |
|
|
146
|
+
| | `IMP-002` | Unresolved relative or internal module imports | `ERROR` |
|
|
147
|
+
| **Packaging** | `PKG-001` | Missing PEP 621 metadata (name, version, etc.) | `WARNING` |
|
|
148
|
+
| | `PKG-002` | Invalid syntax or malformed keys in `pyproject.toml` | `ERROR` |
|
|
149
|
+
|
|
150
|
+
👉 *See full explanations and remediation steps in the [Rules Catalog](docs/rules.md).*
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## 🛠️ CLI Commands
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
# Full project diagnostic scan
|
|
158
|
+
qv scan
|
|
159
|
+
|
|
160
|
+
# Scan another folder
|
|
161
|
+
qv scan ./services/billing
|
|
162
|
+
|
|
163
|
+
# Strict mode: fail CI on warnings as well as errors
|
|
164
|
+
qv scan --strict
|
|
165
|
+
|
|
166
|
+
# Output machine-readable formats
|
|
167
|
+
qv scan --json
|
|
168
|
+
qv scan --sarif -o results.sarif
|
|
169
|
+
|
|
170
|
+
# Run focused subsystem scans
|
|
171
|
+
qv dependency # Check package constraints & imports
|
|
172
|
+
qv environment # Check Python, Docker & CI version drift
|
|
173
|
+
qv architecture # Check for circular imports & dead paths
|
|
174
|
+
|
|
175
|
+
# Explain a rule
|
|
176
|
+
qv explain DEP-002
|
|
177
|
+
|
|
178
|
+
# Check installed version
|
|
179
|
+
qv version
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
👉 *See detailed options in the [CLI Reference](docs/cli_reference.md).*
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## ⚙️ Configuration
|
|
187
|
+
|
|
188
|
+
Configure `qv` in your `pyproject.toml`:
|
|
189
|
+
|
|
190
|
+
```toml
|
|
191
|
+
[tool.qv]
|
|
192
|
+
|
|
193
|
+
# Override severity for any rule (error, warning, info, off)
|
|
194
|
+
[tool.qv.rules]
|
|
195
|
+
DEP-001 = "error"
|
|
196
|
+
DEP-003 = "warning"
|
|
197
|
+
ENV-002 = "info"
|
|
198
|
+
|
|
199
|
+
# Ignore specific rules
|
|
200
|
+
[tool.qv.ignore]
|
|
201
|
+
rules = ["DEP-004"]
|
|
202
|
+
|
|
203
|
+
# Exclude directories from scanning
|
|
204
|
+
[tool.qv.paths]
|
|
205
|
+
exclude = [
|
|
206
|
+
".venv",
|
|
207
|
+
"build",
|
|
208
|
+
"dist",
|
|
209
|
+
"legacy_scripts",
|
|
210
|
+
]
|
|
211
|
+
|
|
212
|
+
# Set expected target Python version
|
|
213
|
+
[tool.qv.runtime]
|
|
214
|
+
python = "3.12"
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
👉 *Learn more in the [Configuration Guide](docs/configuration.md).*
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
## 🤖 CI/CD Integration
|
|
222
|
+
|
|
223
|
+
### GitHub Actions (with SARIF code scanning)
|
|
224
|
+
|
|
225
|
+
Add this step to your GitHub Actions workflow (`.github/workflows/ci.yml`):
|
|
226
|
+
|
|
227
|
+
```yaml
|
|
228
|
+
name: Health & Dependency Scan
|
|
229
|
+
|
|
230
|
+
on: [push, pull_request]
|
|
231
|
+
|
|
232
|
+
jobs:
|
|
233
|
+
qv:
|
|
234
|
+
runs-on: ubuntu-latest
|
|
235
|
+
steps:
|
|
236
|
+
- uses: actions/checkout@v4
|
|
237
|
+
- uses: astral-sh/setup-uv@v3
|
|
238
|
+
|
|
239
|
+
# Run scan in CI mode
|
|
240
|
+
- name: Run qv
|
|
241
|
+
run: uv run qv scan --ci
|
|
242
|
+
|
|
243
|
+
# Optional: Generate SARIF report for GitHub Code Scanning
|
|
244
|
+
- name: Generate SARIF report
|
|
245
|
+
run: uv run qv scan --sarif -o qv.sarif
|
|
246
|
+
if: always()
|
|
247
|
+
|
|
248
|
+
- name: Upload SARIF to GitHub Security
|
|
249
|
+
uses: github/codeql-action/upload-sarif@v3
|
|
250
|
+
with:
|
|
251
|
+
sarif_file: qv.sarif
|
|
252
|
+
if: always()
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
👉 *See full details in [CI/CD Integration](docs/ci_integration.md).*
|
|
256
|
+
|
|
257
|
+
---
|
|
258
|
+
|
|
259
|
+
## 🎯 Design Principles
|
|
260
|
+
|
|
261
|
+
- **Diagnose first. Explain second. Fix safely.**
|
|
262
|
+
- **No destructive auto-mutations**: Remediations provide the exact commands/diffs for you to review and apply.
|
|
263
|
+
- **Local-first & Blazing fast**: Zero network calls required; scans complete in under a second.
|
|
264
|
+
- **Package-manager agnostic**: Works out of the box with `uv`, Poetry, `pip`, PDM, and Pipenv.
|
|
265
|
+
|
|
266
|
+
---
|
|
267
|
+
|
|
268
|
+
## 📚 Complete Documentation
|
|
269
|
+
|
|
270
|
+
- 🚀 [Getting Started Guide](docs/getting_started.md)
|
|
271
|
+
- 📖 [CLI Reference](docs/cli_reference.md)
|
|
272
|
+
- 📋 [Diagnostic Rules Catalog](docs/rules.md)
|
|
273
|
+
- ⚙️ [Configuration Guide](docs/configuration.md)
|
|
274
|
+
- 🤖 [CI/CD & SARIF Integration](docs/ci_integration.md)
|
|
275
|
+
- 🤝 [Contributing Guidelines](docs/contributing.md)
|
|
276
|
+
|
|
277
|
+
---
|
|
278
|
+
|
|
279
|
+
## 📄 License
|
|
280
|
+
|
|
281
|
+
Distributed under the [MIT License](LICENSE).
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
qv/__init__.py,sha256=v4AKHyYLemGctPfnaZ7qLHlpL1dgam5uAkTk1qzXUQw,100
|
|
2
|
+
qv/analyzers/__init__.py,sha256=_d-1_NpNFMDu1cT-SqTKRBBeipboWZlRt5zZ0tsVPk0,25
|
|
3
|
+
qv/analyzers/ci/__init__.py,sha256=W0Kr4IJBwjysEJPY-D3wNeDMWVFOFQzZt71LIzRDGfY,20
|
|
4
|
+
qv/analyzers/dependencies/__init__.py,sha256=AfzPubOgDrmbfPVe8I5egJ6tswolYzWsDwXjW-Maqhg,129
|
|
5
|
+
qv/analyzers/dependencies/analyzer.py,sha256=gtQeMuhUXs29cEn3RcRkWCXQzVHTs1PbE4_tVzSHhUQ,14075
|
|
6
|
+
qv/analyzers/docker/__init__.py,sha256=zQsRHV9IiRbjpb9AKmKzaMPtlXp1iSQCBSvyKDo-2q8,24
|
|
7
|
+
qv/analyzers/environment/__init__.py,sha256=5EtSq3sQIJJxKAfpwWiCul3VHWMA94y3-QBi5pjHVb8,128
|
|
8
|
+
qv/analyzers/environment/drift.py,sha256=tsu3Oi5Bng8e69Cvd6SBWsdyuK3Ut72saFvRpEjY-2I,7193
|
|
9
|
+
qv/analyzers/imports/__init__.py,sha256=bShRWSwJunl5mbkHI738-jTut0RGStVQhtdzCae-fuo,112
|
|
10
|
+
qv/analyzers/imports/analyzer.py,sha256=Vw0VKulyHcwONg9hkXEI2LfbMUxF4MZrNZVnaJnzSKw,6335
|
|
11
|
+
qv/analyzers/packaging/__init__.py,sha256=owgIeiZUNwhn-gPLbho1jCqA72cg1f8u_6wsqDqsgdk,123
|
|
12
|
+
qv/analyzers/packaging/analyzer.py,sha256=-57OYQPsS6uIN8u_1LR2CQRT8sOcDmYYCtGkgFo4jo0,4776
|
|
13
|
+
qv/analyzers/security/__init__.py,sha256=LsUd6Rerxa9qt3U0bP1uiNyEbs0doVtN8zvrXkjZOJY,26
|
|
14
|
+
qv/cli/__init__.py,sha256=EUnPKm0L_oVbU1jAO_Icle4TXMCVGpd_aFTQ4iaueik,88
|
|
15
|
+
qv/cli/main.py,sha256=eewmxZF9dpcUTITY2qwVpAFa8NRF2JW7LhmBJzW4ppo,8112
|
|
16
|
+
qv/core/analyzer.py,sha256=FPiEjghPbi-RVCOxODE_BRlkvtMsnrgbakhq7-YORuc,503
|
|
17
|
+
qv/core/config.py,sha256=LW8zjnuNlosQdqumbvycBu8fGe_Neg2fWpB_vdhNHVA,3629
|
|
18
|
+
qv/core/context.py,sha256=kAI_QENd6c9kRiIE5Ejt2Um4MZsQ3TL9ubkl3w5AJF0,2575
|
|
19
|
+
qv/core/engine.py,sha256=p4Lg8teRj8EgoVqc26TaK1SrijOKRHtBKrDnsE8BZ7w,4400
|
|
20
|
+
qv/core/models.py,sha256=9TDMASdnS_vaayjZSAu2kLUk-KPOKU6ikZlQr6KFSM0,3728
|
|
21
|
+
qv/core/project.py,sha256=1aSdCrlp1EbygAHhW3ptGx3_GwJKL-FwJrhQhO5mozc,12906
|
|
22
|
+
qv/frameworks/__init__.py,sha256=5XnOOPd2Kyb9uG3Z7MjqZO1emDZs5F1sCAmI4yjWMPI,25
|
|
23
|
+
qv/integrations/__init__.py,sha256=xBWG_BBOAn4l43MnjXmhbu4qjiMdYq32pVjmDO0Q8iM,37
|
|
24
|
+
qv/reporters/__init__.py,sha256=bTNB9uOshW9cgKv2Oja2uy4UrY5HJufUa0TzcLwO2fY,309
|
|
25
|
+
qv/reporters/base.py,sha256=LvtGAy5xocwB8gqYuAvFEIO73ov7BwXa06Azj_Pnuu4,329
|
|
26
|
+
qv/reporters/json_reporter.py,sha256=RRzEZAMpOw8HGEVbYV8n_PCJ0PrIvFlOgAGptUxvtvY,446
|
|
27
|
+
qv/reporters/sarif.py,sha256=CZEtjmqB7sNCc4qZJBLtm_7cT_gYa8frI1jl9JHzuyU,3062
|
|
28
|
+
qv/reporters/terminal.py,sha256=H5c5-HMKSZ2PIHx-ZE4XA5bhMH826JHzBav0EKFqU44,4549
|
|
29
|
+
qv/rules/registry.py,sha256=EkH_1CcmGwUWkldU2D9ZZATEZx6vxaWqs9Sf9LzVDKA,7410
|
|
30
|
+
python_qv-0.1.0.dist-info/METADATA,sha256=RmVlo5zNtDYwxBkpoZBY46m9uAfoJ3iPP64KfwepbAE,9046
|
|
31
|
+
python_qv-0.1.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
32
|
+
python_qv-0.1.0.dist-info/entry_points.txt,sha256=y0aZqfDPndkJ2yXxBM2RLL1qriH72zWEKSj4nJtc2q4,94
|
|
33
|
+
python_qv-0.1.0.dist-info/RECORD,,
|
qv/__init__.py
ADDED
qv/analyzers/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Analyzers package."""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""CI analyzers."""
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
"""Dependency analyzer implementing DEP-001, DEP-002, DEP-003, DEP-004, DEP-005."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
from packaging.requirements import Requirement
|
|
8
|
+
from packaging.specifiers import SpecifierSet
|
|
9
|
+
from packaging.version import Version
|
|
10
|
+
|
|
11
|
+
from qv.core.context import ProjectContext
|
|
12
|
+
from qv.core.models import Diagnostic, Evidence, Suggestion
|
|
13
|
+
from qv.rules.registry import get_rule_definition
|
|
14
|
+
|
|
15
|
+
# Known Python standard library modules (Python 3.10+)
|
|
16
|
+
STDLIB_MODULES = (
|
|
17
|
+
set(sys.stdlib_module_names)
|
|
18
|
+
if hasattr(sys, "stdlib_module_names")
|
|
19
|
+
else {
|
|
20
|
+
"abc",
|
|
21
|
+
"argparse",
|
|
22
|
+
"ast",
|
|
23
|
+
"asyncio",
|
|
24
|
+
"base64",
|
|
25
|
+
"collections",
|
|
26
|
+
"contextlib",
|
|
27
|
+
"csv",
|
|
28
|
+
"dataclasses",
|
|
29
|
+
"datetime",
|
|
30
|
+
"decimal",
|
|
31
|
+
"difflib",
|
|
32
|
+
"dis",
|
|
33
|
+
"enum",
|
|
34
|
+
"functools",
|
|
35
|
+
"glob",
|
|
36
|
+
"hashlib",
|
|
37
|
+
"http",
|
|
38
|
+
"importlib",
|
|
39
|
+
"inspect",
|
|
40
|
+
"io",
|
|
41
|
+
"itertools",
|
|
42
|
+
"json",
|
|
43
|
+
"logging",
|
|
44
|
+
"math",
|
|
45
|
+
"multiprocessing",
|
|
46
|
+
"os",
|
|
47
|
+
"pathlib",
|
|
48
|
+
"pickle",
|
|
49
|
+
"pprint",
|
|
50
|
+
"random",
|
|
51
|
+
"re",
|
|
52
|
+
"shutil",
|
|
53
|
+
"socket",
|
|
54
|
+
"sqlite3",
|
|
55
|
+
"ssl",
|
|
56
|
+
"string",
|
|
57
|
+
"subprocess",
|
|
58
|
+
"sys",
|
|
59
|
+
"tempfile",
|
|
60
|
+
"threading",
|
|
61
|
+
"time",
|
|
62
|
+
"traceback",
|
|
63
|
+
"typing",
|
|
64
|
+
"unittest",
|
|
65
|
+
"urllib",
|
|
66
|
+
"uuid",
|
|
67
|
+
"warnings",
|
|
68
|
+
"weakref",
|
|
69
|
+
"xml",
|
|
70
|
+
"zipfile",
|
|
71
|
+
"__future__",
|
|
72
|
+
}
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
# Known tools/packages that may not be imported directly in application code
|
|
76
|
+
IGNORED_UNUSED = {
|
|
77
|
+
"pytest",
|
|
78
|
+
"pytest-cov",
|
|
79
|
+
"ruff",
|
|
80
|
+
"black",
|
|
81
|
+
"mypy",
|
|
82
|
+
"pyright",
|
|
83
|
+
"flake8",
|
|
84
|
+
"isort",
|
|
85
|
+
"hatchling",
|
|
86
|
+
"flit",
|
|
87
|
+
"setuptools",
|
|
88
|
+
"wheel",
|
|
89
|
+
"gunicorn",
|
|
90
|
+
"uvicorn",
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class DependencyAnalyzer:
|
|
95
|
+
"""Analyzes declared and installed dependencies for conflicts and anomalies."""
|
|
96
|
+
|
|
97
|
+
id = "dependencies"
|
|
98
|
+
name = "Dependency Analyzer"
|
|
99
|
+
description = (
|
|
100
|
+
"Checks for dependency conflicts, missing imports, unused packages, and version mismatches."
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
def analyze(self, context: ProjectContext) -> list[Diagnostic]:
|
|
104
|
+
diagnostics: list[Diagnostic] = []
|
|
105
|
+
|
|
106
|
+
# Run individual sub-checks
|
|
107
|
+
diagnostics.extend(self._check_installed_conflicts(context))
|
|
108
|
+
diagnostics.extend(self._check_missing_dependencies(context))
|
|
109
|
+
diagnostics.extend(self._check_unused_dependencies(context))
|
|
110
|
+
diagnostics.extend(self._check_declaration_mismatch(context))
|
|
111
|
+
|
|
112
|
+
return diagnostics
|
|
113
|
+
|
|
114
|
+
def _check_installed_conflicts(self, context: ProjectContext) -> list[Diagnostic]:
|
|
115
|
+
"""DEP-001: Check if any installed package's dependencies conflict with other installed packages."""
|
|
116
|
+
diagnostics: list[Diagnostic] = []
|
|
117
|
+
rule = get_rule_definition("DEP-001")
|
|
118
|
+
if not rule:
|
|
119
|
+
return diagnostics
|
|
120
|
+
|
|
121
|
+
for dist in context.installed_packages.values():
|
|
122
|
+
for req_str in dist.requires:
|
|
123
|
+
try:
|
|
124
|
+
req = Requirement(req_str)
|
|
125
|
+
# Ignore marker evaluation failure or unfulfilled markers for other OS/Python
|
|
126
|
+
if req.marker and not req.marker.evaluate():
|
|
127
|
+
continue
|
|
128
|
+
|
|
129
|
+
dep_name = req.name.lower()
|
|
130
|
+
if dep_name in context.installed_packages:
|
|
131
|
+
installed_dep = context.installed_packages[dep_name]
|
|
132
|
+
try:
|
|
133
|
+
dep_ver = Version(installed_dep.version)
|
|
134
|
+
if req.specifier and not req.specifier.contains(
|
|
135
|
+
dep_ver, prereleases=True
|
|
136
|
+
):
|
|
137
|
+
diag = Diagnostic(
|
|
138
|
+
id=rule.id,
|
|
139
|
+
severity=rule.default_severity,
|
|
140
|
+
category=rule.category,
|
|
141
|
+
title=rule.title,
|
|
142
|
+
message=f"{dist.name} requires {req.name}{req.specifier}, but installed is {installed_dep.name} {installed_dep.version}.",
|
|
143
|
+
evidence=[
|
|
144
|
+
Evidence(
|
|
145
|
+
fact=f"{dist.name} declared requirement: {req_str}",
|
|
146
|
+
source=f"{dist.name} metadata",
|
|
147
|
+
),
|
|
148
|
+
Evidence(
|
|
149
|
+
fact=f"Installed {installed_dep.name} version: {installed_dep.version}",
|
|
150
|
+
source="Active environment",
|
|
151
|
+
),
|
|
152
|
+
],
|
|
153
|
+
suggestions=[
|
|
154
|
+
Suggestion(
|
|
155
|
+
description=f"Upgrade {dist.name} or pin {installed_dep.name} to {req.specifier}.",
|
|
156
|
+
command=f"{context.package_manager} add '{installed_dep.name}{req.specifier}'"
|
|
157
|
+
if context.package_manager in ("uv", "poetry")
|
|
158
|
+
else f"pip install '{installed_dep.name}{req.specifier}'",
|
|
159
|
+
)
|
|
160
|
+
],
|
|
161
|
+
affected_packages=[dist.name, installed_dep.name],
|
|
162
|
+
doc_url=rule.doc_url,
|
|
163
|
+
)
|
|
164
|
+
diagnostics.append(diag)
|
|
165
|
+
except Exception:
|
|
166
|
+
pass
|
|
167
|
+
except Exception:
|
|
168
|
+
pass
|
|
169
|
+
|
|
170
|
+
return diagnostics
|
|
171
|
+
|
|
172
|
+
def _check_missing_dependencies(self, context: ProjectContext) -> list[Diagnostic]:
|
|
173
|
+
"""DEP-002: Check for imported packages not declared in dependencies."""
|
|
174
|
+
diagnostics: list[Diagnostic] = []
|
|
175
|
+
rule = get_rule_definition("DEP-002")
|
|
176
|
+
if not rule:
|
|
177
|
+
return diagnostics
|
|
178
|
+
|
|
179
|
+
declared_names = {d.name.lower().replace("-", "_"): d for d in context.dependencies}
|
|
180
|
+
declared_raw_names = {d.name.lower(): d for d in context.dependencies}
|
|
181
|
+
|
|
182
|
+
# Collect local top-level module names
|
|
183
|
+
local_modules = {
|
|
184
|
+
context.project_name.lower().replace("-", "_"),
|
|
185
|
+
context.project_name.lower(),
|
|
186
|
+
}
|
|
187
|
+
for sf in context.source_files:
|
|
188
|
+
parts = sf.relative_path.parts
|
|
189
|
+
if parts:
|
|
190
|
+
if parts[0] in ("src", "lib") and len(parts) > 1:
|
|
191
|
+
local_modules.add(parts[1].replace(".py", "").lower().replace("-", "_"))
|
|
192
|
+
local_modules.add(parts[0].replace(".py", "").lower().replace("-", "_"))
|
|
193
|
+
|
|
194
|
+
seen_missing: set[str] = set()
|
|
195
|
+
|
|
196
|
+
for imp in context.imports:
|
|
197
|
+
if imp.is_relative or not imp.module_name:
|
|
198
|
+
continue
|
|
199
|
+
|
|
200
|
+
top_level = imp.module_name.split(".")[0]
|
|
201
|
+
top_level_norm = top_level.lower().replace("-", "_")
|
|
202
|
+
|
|
203
|
+
if (
|
|
204
|
+
top_level in STDLIB_MODULES
|
|
205
|
+
or top_level in local_modules
|
|
206
|
+
or top_level_norm in local_modules
|
|
207
|
+
or top_level_norm in declared_names
|
|
208
|
+
or top_level.lower() in declared_raw_names
|
|
209
|
+
):
|
|
210
|
+
continue
|
|
211
|
+
|
|
212
|
+
# If it's missing from declared dependencies
|
|
213
|
+
if top_level_norm not in seen_missing:
|
|
214
|
+
seen_missing.add(top_level_norm)
|
|
215
|
+
rel_path = (
|
|
216
|
+
imp.source_file.relative_to(context.project_root)
|
|
217
|
+
if imp.source_file.is_relative_to(context.project_root)
|
|
218
|
+
else imp.source_file
|
|
219
|
+
)
|
|
220
|
+
diag = Diagnostic(
|
|
221
|
+
id=rule.id,
|
|
222
|
+
severity=rule.default_severity,
|
|
223
|
+
category=rule.category,
|
|
224
|
+
title=f"Missing dependency: {top_level}",
|
|
225
|
+
message=f"Module '{top_level}' is imported in {rel_path}:{imp.line_number} but is not declared in project dependencies.",
|
|
226
|
+
evidence=[
|
|
227
|
+
Evidence(
|
|
228
|
+
fact=f"Import statement: `import {imp.module_name}` in {rel_path}:{imp.line_number}",
|
|
229
|
+
source=str(rel_path),
|
|
230
|
+
)
|
|
231
|
+
],
|
|
232
|
+
suggestions=[
|
|
233
|
+
Suggestion(
|
|
234
|
+
description=f"Add '{top_level}' to project dependencies in pyproject.toml.",
|
|
235
|
+
command=f"{context.package_manager} add {top_level}"
|
|
236
|
+
if context.package_manager in ("uv", "poetry")
|
|
237
|
+
else f"pip install {top_level}",
|
|
238
|
+
)
|
|
239
|
+
],
|
|
240
|
+
affected_packages=[top_level],
|
|
241
|
+
file=str(rel_path),
|
|
242
|
+
line=imp.line_number,
|
|
243
|
+
doc_url=rule.doc_url,
|
|
244
|
+
)
|
|
245
|
+
diagnostics.append(diag)
|
|
246
|
+
|
|
247
|
+
return diagnostics
|
|
248
|
+
|
|
249
|
+
def _check_unused_dependencies(self, context: ProjectContext) -> list[Diagnostic]:
|
|
250
|
+
"""DEP-003: Check for declared direct dependencies that have no detected imports."""
|
|
251
|
+
diagnostics: list[Diagnostic] = []
|
|
252
|
+
rule = get_rule_definition("DEP-003")
|
|
253
|
+
if not rule or not context.source_files:
|
|
254
|
+
return diagnostics
|
|
255
|
+
|
|
256
|
+
imported_top_levels = {
|
|
257
|
+
imp.module_name.split(".")[0].lower().replace("-", "_")
|
|
258
|
+
for imp in context.imports
|
|
259
|
+
if imp.module_name
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
for dep in context.dependencies:
|
|
263
|
+
if dep.is_dev:
|
|
264
|
+
continue
|
|
265
|
+
|
|
266
|
+
dep_norm = dep.name.lower().replace("-", "_")
|
|
267
|
+
if dep.name.lower() in IGNORED_UNUSED or dep_norm in IGNORED_UNUSED:
|
|
268
|
+
continue
|
|
269
|
+
|
|
270
|
+
if dep_norm not in imported_top_levels and dep.name.lower() not in imported_top_levels:
|
|
271
|
+
diag = Diagnostic(
|
|
272
|
+
id=rule.id,
|
|
273
|
+
severity=rule.default_severity,
|
|
274
|
+
category=rule.category,
|
|
275
|
+
title=f"Unused declared dependency: {dep.name}",
|
|
276
|
+
message=f"Package '{dep.name}' is declared in {dep.source_file.name} but no imports were detected in project source files.",
|
|
277
|
+
evidence=[
|
|
278
|
+
Evidence(
|
|
279
|
+
fact=f"Declared requirement: {dep.name}{dep.specifier} in {dep.source_file.name}",
|
|
280
|
+
source=str(dep.source_file),
|
|
281
|
+
)
|
|
282
|
+
],
|
|
283
|
+
suggestions=[
|
|
284
|
+
Suggestion(
|
|
285
|
+
description=f"Remove '{dep.name}' from dependencies if not needed at runtime.",
|
|
286
|
+
command=f"{context.package_manager} remove {dep.name}"
|
|
287
|
+
if context.package_manager in ("uv", "poetry")
|
|
288
|
+
else None,
|
|
289
|
+
)
|
|
290
|
+
],
|
|
291
|
+
affected_packages=[dep.name],
|
|
292
|
+
file=str(dep.source_file),
|
|
293
|
+
line=dep.line_number,
|
|
294
|
+
confidence=0.85,
|
|
295
|
+
doc_url=rule.doc_url,
|
|
296
|
+
)
|
|
297
|
+
diagnostics.append(diag)
|
|
298
|
+
|
|
299
|
+
return diagnostics
|
|
300
|
+
|
|
301
|
+
def _check_declaration_mismatch(self, context: ProjectContext) -> list[Diagnostic]:
|
|
302
|
+
"""DEP-005: Check for declared versions that do not match installed versions in active environment."""
|
|
303
|
+
diagnostics: list[Diagnostic] = []
|
|
304
|
+
rule = get_rule_definition("DEP-005")
|
|
305
|
+
if not rule or not context.installed_packages:
|
|
306
|
+
return diagnostics
|
|
307
|
+
|
|
308
|
+
for dep in context.dependencies:
|
|
309
|
+
dep_name_lower = dep.name.lower()
|
|
310
|
+
if dep_name_lower in context.installed_packages and dep.specifier:
|
|
311
|
+
installed = context.installed_packages[dep_name_lower]
|
|
312
|
+
try:
|
|
313
|
+
spec = SpecifierSet(dep.specifier)
|
|
314
|
+
installed_ver = Version(installed.version)
|
|
315
|
+
if not spec.contains(installed_ver, prereleases=True):
|
|
316
|
+
diag = Diagnostic(
|
|
317
|
+
id=rule.id,
|
|
318
|
+
severity=rule.default_severity,
|
|
319
|
+
category=rule.category,
|
|
320
|
+
title=f"Installed/declaration mismatch for {dep.name}",
|
|
321
|
+
message=f"Declared requirement is '{dep.name}{dep.specifier}', but environment has '{installed.name} {installed.version}'.",
|
|
322
|
+
evidence=[
|
|
323
|
+
Evidence(
|
|
324
|
+
fact=f"Declared specifier: {dep.specifier} in {dep.source_file.name}",
|
|
325
|
+
source=str(dep.source_file),
|
|
326
|
+
),
|
|
327
|
+
Evidence(
|
|
328
|
+
fact=f"Installed version in environment: {installed.version}",
|
|
329
|
+
source="Active environment",
|
|
330
|
+
),
|
|
331
|
+
],
|
|
332
|
+
suggestions=[
|
|
333
|
+
Suggestion(
|
|
334
|
+
description="Synchronize the virtual environment with project manifest.",
|
|
335
|
+
command=f"{context.package_manager} sync"
|
|
336
|
+
if context.package_manager in ("uv", "poetry")
|
|
337
|
+
else f"pip install -r {dep.source_file.name}",
|
|
338
|
+
)
|
|
339
|
+
],
|
|
340
|
+
affected_packages=[dep.name],
|
|
341
|
+
doc_url=rule.doc_url,
|
|
342
|
+
)
|
|
343
|
+
diagnostics.append(diag)
|
|
344
|
+
except Exception:
|
|
345
|
+
pass
|
|
346
|
+
|
|
347
|
+
return diagnostics
|