pydoctor-cli 2.1.0__tar.gz → 2.3.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.
Files changed (70) hide show
  1. {pydoctor_cli-2.1.0 → pydoctor_cli-2.3.0}/PKG-INFO +74 -3
  2. {pydoctor_cli-2.1.0 → pydoctor_cli-2.3.0}/README.md +71 -0
  3. {pydoctor_cli-2.1.0 → pydoctor_cli-2.3.0}/pydoctor/__init__.py +1 -1
  4. {pydoctor_cli-2.1.0 → pydoctor_cli-2.3.0}/pydoctor/analysis/health_score.py +48 -5
  5. {pydoctor_cli-2.1.0 → pydoctor_cli-2.3.0}/pydoctor/cache/cache_manager.py +8 -1
  6. {pydoctor_cli-2.1.0 → pydoctor_cli-2.3.0}/pydoctor/cli/main.py +228 -37
  7. pydoctor_cli-2.3.0/pydoctor/core/__init__.py +17 -0
  8. {pydoctor_cli-2.1.0 → pydoctor_cli-2.3.0}/pydoctor/core/analyzer.py +68 -1
  9. pydoctor_cli-2.3.0/pydoctor/core/baseline.py +153 -0
  10. {pydoctor_cli-2.1.0 → pydoctor_cli-2.3.0}/pydoctor/core/project.py +105 -78
  11. {pydoctor_cli-2.1.0 → pydoctor_cli-2.3.0}/pydoctor/core/report.py +8 -0
  12. pydoctor_cli-2.3.0/pydoctor/reports/docs_links.py +52 -0
  13. pydoctor_cli-2.3.0/pydoctor/reports/fingerprint.py +57 -0
  14. {pydoctor_cli-2.1.0 → pydoctor_cli-2.3.0}/pydoctor/reports/json_formatter.py +45 -15
  15. pydoctor_cli-2.3.0/pydoctor/reports/sarif_formatter.py +187 -0
  16. {pydoctor_cli-2.1.0 → pydoctor_cli-2.3.0}/pydoctor/reports/table_formatter.py +28 -2
  17. pydoctor_cli-2.3.0/pydoctor/scanners/ci_scanner.py +319 -0
  18. {pydoctor_cli-2.1.0 → pydoctor_cli-2.3.0}/pydoctor/scanners/dependency_scanner.py +54 -9
  19. {pydoctor_cli-2.1.0 → pydoctor_cli-2.3.0}/pydoctor/scanners/env_scanner.py +30 -12
  20. {pydoctor_cli-2.1.0 → pydoctor_cli-2.3.0}/pydoctor/scanners/outdated_package_scanner.py +40 -14
  21. {pydoctor_cli-2.1.0 → pydoctor_cli-2.3.0}/pydoctor/scanners/unused_package_scanner.py +57 -8
  22. {pydoctor_cli-2.1.0 → pydoctor_cli-2.3.0}/pydoctor/scanners/vulnerability_scanner.py +10 -2
  23. pydoctor_cli-2.3.0/pydoctor/security/cvss.py +77 -0
  24. {pydoctor_cli-2.1.0 → pydoctor_cli-2.3.0}/pydoctor/security/osv_client.py +140 -61
  25. pydoctor_cli-2.3.0/pydoctor/security/secrets.py +296 -0
  26. pydoctor_cli-2.3.0/pydoctor/utils/env_probe.py +145 -0
  27. {pydoctor_cli-2.1.0 → pydoctor_cli-2.3.0}/pydoctor/utils/file_utils.py +4 -2
  28. {pydoctor_cli-2.1.0 → pydoctor_cli-2.3.0}/pydoctor/utils/parser_utils.py +24 -14
  29. {pydoctor_cli-2.1.0 → pydoctor_cli-2.3.0}/pydoctor/utils/pip_utils.py +40 -29
  30. {pydoctor_cli-2.1.0 → pydoctor_cli-2.3.0}/pydoctor_cli.egg-info/PKG-INFO +74 -3
  31. {pydoctor_cli-2.1.0 → pydoctor_cli-2.3.0}/pydoctor_cli.egg-info/SOURCES.txt +13 -3
  32. {pydoctor_cli-2.1.0 → pydoctor_cli-2.3.0}/pydoctor_cli.egg-info/top_level.txt +1 -0
  33. {pydoctor_cli-2.1.0 → pydoctor_cli-2.3.0}/pyproject.toml +3 -3
  34. pydoctor_cli-2.3.0/tests/test_baseline.py +165 -0
  35. pydoctor_cli-2.3.0/tests/test_ci_scanner.py +196 -0
  36. pydoctor_cli-2.3.0/tests/test_cli_ux.py +72 -0
  37. {pydoctor_cli-2.1.0 → pydoctor_cli-2.3.0}/tests/test_env_scanner.py +27 -14
  38. pydoctor_cli-2.3.0/tests/test_environment.py +197 -0
  39. pydoctor_cli-2.3.0/tests/test_github_action.py +86 -0
  40. pydoctor_cli-2.3.0/tests/test_osv_parsing.py +181 -0
  41. pydoctor_cli-2.3.0/tests/test_report_output.py +72 -0
  42. pydoctor_cli-2.3.0/tests/test_sarif.py +117 -0
  43. {pydoctor_cli-2.1.0 → pydoctor_cli-2.3.0}/tests/test_unused_scanner.py +19 -0
  44. pydoctor_cli-2.1.0/pydoctor/core/__init__.py +0 -7
  45. pydoctor_cli-2.1.0/pydoctor/performance/__init__.py +0 -5
  46. pydoctor_cli-2.1.0/pydoctor/performance/executor.py +0 -132
  47. pydoctor_cli-2.1.0/pydoctor/performance/parallel.py +0 -40
  48. pydoctor_cli-2.1.0/pydoctor/scanners/ci_scanner.py +0 -141
  49. pydoctor_cli-2.1.0/tests/test_ci_scanner.py +0 -39
  50. pydoctor_cli-2.1.0/tests/test_cli_ux.py +0 -38
  51. {pydoctor_cli-2.1.0 → pydoctor_cli-2.3.0}/LICENSE +0 -0
  52. {pydoctor_cli-2.1.0 → pydoctor_cli-2.3.0}/pydoctor/analysis/__init__.py +0 -0
  53. {pydoctor_cli-2.1.0 → pydoctor_cli-2.3.0}/pydoctor/cache/__init__.py +0 -0
  54. {pydoctor_cli-2.1.0 → pydoctor_cli-2.3.0}/pydoctor/cli/__init__.py +0 -0
  55. {pydoctor_cli-2.1.0 → pydoctor_cli-2.3.0}/pydoctor/config/__init__.py +0 -0
  56. {pydoctor_cli-2.1.0 → pydoctor_cli-2.3.0}/pydoctor/config/settings.py +0 -0
  57. {pydoctor_cli-2.1.0 → pydoctor_cli-2.3.0}/pydoctor/reports/__init__.py +0 -0
  58. {pydoctor_cli-2.1.0 → pydoctor_cli-2.3.0}/pydoctor/reports/terminal_colors.py +0 -0
  59. {pydoctor_cli-2.1.0 → pydoctor_cli-2.3.0}/pydoctor/scanners/__init__.py +0 -0
  60. {pydoctor_cli-2.1.0 → pydoctor_cli-2.3.0}/pydoctor/security/__init__.py +0 -0
  61. {pydoctor_cli-2.1.0 → pydoctor_cli-2.3.0}/pydoctor/utils/__init__.py +0 -0
  62. {pydoctor_cli-2.1.0 → pydoctor_cli-2.3.0}/pydoctor/utils/subprocess_utils.py +0 -0
  63. {pydoctor_cli-2.1.0 → pydoctor_cli-2.3.0}/pydoctor_cli.egg-info/dependency_links.txt +0 -0
  64. {pydoctor_cli-2.1.0 → pydoctor_cli-2.3.0}/pydoctor_cli.egg-info/entry_points.txt +0 -0
  65. {pydoctor_cli-2.1.0 → pydoctor_cli-2.3.0}/pydoctor_cli.egg-info/requires.txt +0 -0
  66. {pydoctor_cli-2.1.0 → pydoctor_cli-2.3.0}/setup.cfg +0 -0
  67. {pydoctor_cli-2.1.0 → pydoctor_cli-2.3.0}/tests/conftest.py +0 -0
  68. {pydoctor_cli-2.1.0 → pydoctor_cli-2.3.0}/tests/test_dependency_scanner.py +0 -0
  69. {pydoctor_cli-2.1.0 → pydoctor_cli-2.3.0}/tests/test_fix_command.py +0 -0
  70. {pydoctor_cli-2.1.0 → pydoctor_cli-2.3.0}/tests/test_vulnerability_scanner.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pydoctor-cli
3
- Version: 2.1.0
3
+ Version: 2.3.0
4
4
  Summary: Premium Python environment diagnostic CLI — your project's personal physician.
5
5
  Author-email: Favour Bamgboye <demiladebamgboye@gmail.com>
6
6
  Maintainer-email: Favour Bamgboye <demiladebamgboye@gmail.com>
@@ -206,9 +206,9 @@ License: Apache License
206
206
  See the License for the specific language governing permissions and
207
207
  limitations under the License.
208
208
 
209
- Project-URL: Homepage, https://pydoctor.vercel.app
209
+ Project-URL: Homepage, https://pydoctor-docs.thally.app
210
210
  Project-URL: Repository, https://github.com/iamAgbaCoder/pydoctor-cli
211
- Project-URL: Documentation, https://pydoctor.vercel.app/docs
211
+ Project-URL: Documentation, https://pydoctor-docs.thally.app
212
212
  Project-URL: Bug Tracker, https://github.com/iamAgbaCoder/pydoctor-cli/issues
213
213
  Project-URL: Changelog, https://github.com/iamAgbaCoder/pydoctor-cli/releases
214
214
  Keywords: pydoctor,diagnosis,dependency-checker,vulnerability-scanner,python-health,cli-tool,security-audit,static-analysis
@@ -253,6 +253,7 @@ Dynamic: license-file
253
253
 
254
254
  **PyDoctor** is a standalone, developer-friendly diagnostic assistant for Python environments. It acts like a personal physician for your code — scanning your project and environment to prescribe fixes for misconfigurations, dependencies, and security vulnerabilities.
255
255
 
256
+ [![PyPI Downloads](https://static.pepy.tech/personalized-badge/pydoctor-cli?period=total&units=INTERNATIONAL_SYSTEM&left_color=GREY&right_color=GREEN&left_text=downloads)](https://pepy.tech/projects/pydoctor-cli)
256
257
  [![CI Status](https://github.com/iamAgbaCoder/pydoctor-cli/actions/workflows/publish.yml/badge.svg)](https://github.com/iamAgbaCoder/pydoctor-cli/actions)
257
258
  [![PyPI version](https://img.shields.io/pypi/v/pydoctor-cli.svg?color=blue&style=flat-square)](https://pypi.org/project/pydoctor-cli/)
258
259
  [![Python versions](https://img.shields.io/pypi/pyversions/pydoctor-cli.svg?style=flat-square)](https://pypi.org/project/pydoctor-cli/)
@@ -299,6 +300,16 @@ pipx install pydoctor-cli
299
300
  pip install pydoctor-cli
300
301
  ```
301
302
 
303
+ > Install `pydoctor-cli`, not `pydoctor`: that is an unrelated project (Twisted's API doc generator) that installs a command with the same name.
304
+
305
+ PyDoctor scans your **project's** Python, wherever PyDoctor itself is installed. It uses the active virtualenv, or a `.venv`/`venv` in the project, and tells you which interpreter it chose. To choose one yourself:
306
+
307
+ ```bash
308
+ pydoctor diagnose --python .venv/bin/python # or set PYDOCTOR_PYTHON
309
+ ```
310
+
311
+ 📖 Full documentation: **https://pydoctor-docs.thally.app**
312
+
302
313
  ---
303
314
 
304
315
  ## ⚡ Usage
@@ -311,6 +322,44 @@ Run a comprehensive check-up on your project.
311
322
  pydoctor diagnose
312
323
  ```
313
324
 
325
+ ### In CI: GitHub Action
326
+
327
+ Findings appear as code-scanning alerts on pull requests, and the job fails on new problems:
328
+
329
+ ```yaml
330
+ permissions:
331
+ contents: read
332
+ security-events: write
333
+
334
+ steps:
335
+ - uses: actions/checkout@v4
336
+ - uses: actions/setup-python@v5
337
+ with: { python-version: "3.12" }
338
+ - run: pip install -r requirements.txt
339
+ - uses: iamAgbaCoder/pydoctor-cli@v2
340
+ ```
341
+
342
+ Set `ci-only: "true"` to run only the secrets and workflow checks. Any CI system can use `pydoctor check --sarif pydoctor.sarif` directly.
343
+
344
+ ### Adopting it in an existing project: baselines
345
+
346
+ ```bash
347
+ pydoctor baseline # accept today's issues; commit .pydoctor-baseline.json
348
+ pydoctor check # now fails only on new issues
349
+ ```
350
+
351
+ Problems with the scan itself (unreadable environment, unreachable vulnerability database) are never baselined.
352
+
353
+ ### pre-commit
354
+
355
+ ```yaml
356
+ repos:
357
+ - repo: https://github.com/iamAgbaCoder/pydoctor-cli
358
+ rev: v2.3.0
359
+ hooks:
360
+ - id: pydoctor-ci # blocks commits that add secrets or risky workflows
361
+ ```
362
+
314
363
  ### Automated Fixes
315
364
 
316
365
  Let PyDoctor perform the heavy lifting. It will offer to install missing packages, upgrade outdated ones, and clean up your environment.
@@ -323,6 +372,28 @@ pydoctor fix
323
372
 
324
373
  ## Release History
325
374
 
375
+ ### v2.3.0
376
+ Built for CI: adopt pydoctor in an existing project without a wall of failures, and see findings where you review code.
377
+
378
+ - **Baselines:** `pydoctor baseline` records today's issues in `.pydoctor-baseline.json`; scans then fail only on new ones and tell you when baselined issues are fixed. Fingerprints survive unrelated edits (a secret moved down by new lines stays known) and package upgrades. `--ci` updates only the CI-guard entries. `--baseline FILE` / `--no-baseline` on every scan command. Broken scans are never baselined.
379
+ - **SARIF output:** `--sarif FILE` on every scan command (SARIF 2.1.0, schema-validated). Vulnerabilities carry their CVSS score as security severity and point at the line that declares the package; secrets and workflow findings point at their file and line.
380
+ - **GitHub Action:** `uses: iamAgbaCoder/pydoctor-cli@v2` installs pydoctor in isolation, uploads findings to code scanning, and fails the job on new problems.
381
+ - **pre-commit hooks:** `pydoctor-ci` blocks commits that add secrets, `.env` files or risky workflows; `pydoctor-check` runs the full scan at pre-push.
382
+ - **Docs links:** every issue links to its page on pydoctor-docs.thally.app (`docs_url` in JSON; shown in the terminal for errors).
383
+
384
+ ### v2.2.0
385
+ Results you can trust: every fix in this release removes a case where PyDoctor reported a problem as fine, or got a severity wrong.
386
+
387
+ - **Scans the right environment:** facts come from your project's interpreter, not PyDoctor's. Under pipx or `uv tool`, and in uv environments (which have no pip), 2.1.0 saw 0 packages and reported everything as OK. Works without pip (package metadata, `uv pip list --outdated`). New `--python` / `PYDOCTOR_PYTHON`; the report shows which interpreter was scanned and why.
388
+ - **Fails loudly:** `ENV_PACKAGES_UNAVAILABLE` (error) when the environment can't be read, `ENV_DEPS_NOT_INSTALLED` when the scanned Python has none of your dependencies, and outdated checks that can't run say so instead of "all up to date".
389
+ - **Virtualenv detection works:** it was always "active". The `diagnose` no-venv prompt now appears, and never blocks CI or piped runs.
390
+ - **Real CVSS scores:** severity is computed from the CVSS v3 vector. Previously a 3.1 (Low) and a 9.8 (Critical) vulnerability were both shown as critical. GitHub's `MODERATE` is now medium.
391
+ - **Right fix version:** for advisories with one fix per release line, `fix` targets your line (Django 5.1.2 → 5.1.4, not 4.2.17).
392
+ - **CI guard finds real leaks:** scans every committed text file, adds Stripe, OpenAI, Anthropic, Slack, Google, private-key, database-URL and `SECRET_KEY` rules, flags committed `.env` files, and checks workflows for `pull_request_target` "pwn requests", script injection and `write-all` permissions. Findings have line numbers; add `# pydoctor: ignore` to silence one. Zero findings on Flask, Django, requests, httpx, FastAPI, DRF and pip.
393
+ - **A score that moves:** a package's advisories count once, categories have diminishing returns, and any critical issue keeps the score below "Good". Expect different (usually higher) scores than 2.1.0.
394
+ - **JSON:** includes `health`; `scan-security --json` reports advisory IDs, aliases, fixed version and CVSS score.
395
+ - Smaller: database drivers, hashers and cache clients picked by Django settings count as used; faster advisory fetching; `--no-cache` no longer wipes the cache; circular import fixed; `github <repo>` and `docker` describe what they actually do.
396
+
326
397
  ### v2.1.0
327
398
  - **`pydoctor fix` reports what really happened:** upgrades are verified against the installed version. If a pin in `pyproject.toml` blocks an upgrade, pydoctor says so and shows the constraint instead of printing "fixed".
328
399
  - **One prompt per package:** vulnerable packages are offered once, with the advisory count, and upgraded to the lowest version that fixes every advisory.
@@ -4,6 +4,7 @@
4
4
 
5
5
  **PyDoctor** is a standalone, developer-friendly diagnostic assistant for Python environments. It acts like a personal physician for your code — scanning your project and environment to prescribe fixes for misconfigurations, dependencies, and security vulnerabilities.
6
6
 
7
+ [![PyPI Downloads](https://static.pepy.tech/personalized-badge/pydoctor-cli?period=total&units=INTERNATIONAL_SYSTEM&left_color=GREY&right_color=GREEN&left_text=downloads)](https://pepy.tech/projects/pydoctor-cli)
7
8
  [![CI Status](https://github.com/iamAgbaCoder/pydoctor-cli/actions/workflows/publish.yml/badge.svg)](https://github.com/iamAgbaCoder/pydoctor-cli/actions)
8
9
  [![PyPI version](https://img.shields.io/pypi/v/pydoctor-cli.svg?color=blue&style=flat-square)](https://pypi.org/project/pydoctor-cli/)
9
10
  [![Python versions](https://img.shields.io/pypi/pyversions/pydoctor-cli.svg?style=flat-square)](https://pypi.org/project/pydoctor-cli/)
@@ -50,6 +51,16 @@ pipx install pydoctor-cli
50
51
  pip install pydoctor-cli
51
52
  ```
52
53
 
54
+ > Install `pydoctor-cli`, not `pydoctor`: that is an unrelated project (Twisted's API doc generator) that installs a command with the same name.
55
+
56
+ PyDoctor scans your **project's** Python, wherever PyDoctor itself is installed. It uses the active virtualenv, or a `.venv`/`venv` in the project, and tells you which interpreter it chose. To choose one yourself:
57
+
58
+ ```bash
59
+ pydoctor diagnose --python .venv/bin/python # or set PYDOCTOR_PYTHON
60
+ ```
61
+
62
+ 📖 Full documentation: **https://pydoctor-docs.thally.app**
63
+
53
64
  ---
54
65
 
55
66
  ## ⚡ Usage
@@ -62,6 +73,44 @@ Run a comprehensive check-up on your project.
62
73
  pydoctor diagnose
63
74
  ```
64
75
 
76
+ ### In CI: GitHub Action
77
+
78
+ Findings appear as code-scanning alerts on pull requests, and the job fails on new problems:
79
+
80
+ ```yaml
81
+ permissions:
82
+ contents: read
83
+ security-events: write
84
+
85
+ steps:
86
+ - uses: actions/checkout@v4
87
+ - uses: actions/setup-python@v5
88
+ with: { python-version: "3.12" }
89
+ - run: pip install -r requirements.txt
90
+ - uses: iamAgbaCoder/pydoctor-cli@v2
91
+ ```
92
+
93
+ Set `ci-only: "true"` to run only the secrets and workflow checks. Any CI system can use `pydoctor check --sarif pydoctor.sarif` directly.
94
+
95
+ ### Adopting it in an existing project: baselines
96
+
97
+ ```bash
98
+ pydoctor baseline # accept today's issues; commit .pydoctor-baseline.json
99
+ pydoctor check # now fails only on new issues
100
+ ```
101
+
102
+ Problems with the scan itself (unreadable environment, unreachable vulnerability database) are never baselined.
103
+
104
+ ### pre-commit
105
+
106
+ ```yaml
107
+ repos:
108
+ - repo: https://github.com/iamAgbaCoder/pydoctor-cli
109
+ rev: v2.3.0
110
+ hooks:
111
+ - id: pydoctor-ci # blocks commits that add secrets or risky workflows
112
+ ```
113
+
65
114
  ### Automated Fixes
66
115
 
67
116
  Let PyDoctor perform the heavy lifting. It will offer to install missing packages, upgrade outdated ones, and clean up your environment.
@@ -74,6 +123,28 @@ pydoctor fix
74
123
 
75
124
  ## Release History
76
125
 
126
+ ### v2.3.0
127
+ Built for CI: adopt pydoctor in an existing project without a wall of failures, and see findings where you review code.
128
+
129
+ - **Baselines:** `pydoctor baseline` records today's issues in `.pydoctor-baseline.json`; scans then fail only on new ones and tell you when baselined issues are fixed. Fingerprints survive unrelated edits (a secret moved down by new lines stays known) and package upgrades. `--ci` updates only the CI-guard entries. `--baseline FILE` / `--no-baseline` on every scan command. Broken scans are never baselined.
130
+ - **SARIF output:** `--sarif FILE` on every scan command (SARIF 2.1.0, schema-validated). Vulnerabilities carry their CVSS score as security severity and point at the line that declares the package; secrets and workflow findings point at their file and line.
131
+ - **GitHub Action:** `uses: iamAgbaCoder/pydoctor-cli@v2` installs pydoctor in isolation, uploads findings to code scanning, and fails the job on new problems.
132
+ - **pre-commit hooks:** `pydoctor-ci` blocks commits that add secrets, `.env` files or risky workflows; `pydoctor-check` runs the full scan at pre-push.
133
+ - **Docs links:** every issue links to its page on pydoctor-docs.thally.app (`docs_url` in JSON; shown in the terminal for errors).
134
+
135
+ ### v2.2.0
136
+ Results you can trust: every fix in this release removes a case where PyDoctor reported a problem as fine, or got a severity wrong.
137
+
138
+ - **Scans the right environment:** facts come from your project's interpreter, not PyDoctor's. Under pipx or `uv tool`, and in uv environments (which have no pip), 2.1.0 saw 0 packages and reported everything as OK. Works without pip (package metadata, `uv pip list --outdated`). New `--python` / `PYDOCTOR_PYTHON`; the report shows which interpreter was scanned and why.
139
+ - **Fails loudly:** `ENV_PACKAGES_UNAVAILABLE` (error) when the environment can't be read, `ENV_DEPS_NOT_INSTALLED` when the scanned Python has none of your dependencies, and outdated checks that can't run say so instead of "all up to date".
140
+ - **Virtualenv detection works:** it was always "active". The `diagnose` no-venv prompt now appears, and never blocks CI or piped runs.
141
+ - **Real CVSS scores:** severity is computed from the CVSS v3 vector. Previously a 3.1 (Low) and a 9.8 (Critical) vulnerability were both shown as critical. GitHub's `MODERATE` is now medium.
142
+ - **Right fix version:** for advisories with one fix per release line, `fix` targets your line (Django 5.1.2 → 5.1.4, not 4.2.17).
143
+ - **CI guard finds real leaks:** scans every committed text file, adds Stripe, OpenAI, Anthropic, Slack, Google, private-key, database-URL and `SECRET_KEY` rules, flags committed `.env` files, and checks workflows for `pull_request_target` "pwn requests", script injection and `write-all` permissions. Findings have line numbers; add `# pydoctor: ignore` to silence one. Zero findings on Flask, Django, requests, httpx, FastAPI, DRF and pip.
144
+ - **A score that moves:** a package's advisories count once, categories have diminishing returns, and any critical issue keeps the score below "Good". Expect different (usually higher) scores than 2.1.0.
145
+ - **JSON:** includes `health`; `scan-security --json` reports advisory IDs, aliases, fixed version and CVSS score.
146
+ - Smaller: database drivers, hashers and cache clients picked by Django settings count as used; faster advisory fetching; `--no-cache` no longer wipes the cache; circular import fixed; `github <repo>` and `docker` describe what they actually do.
147
+
77
148
  ### v2.1.0
78
149
  - **`pydoctor fix` reports what really happened:** upgrades are verified against the installed version. If a pin in `pyproject.toml` blocks an upgrade, pydoctor says so and shows the constraint instead of printing "fixed".
79
150
  - **One prompt per package:** vulnerable packages are offered once, with the advisory count, and upgraded to the lowest version that fixes every advisory.
@@ -9,6 +9,6 @@ Author: Favour Bamgboye
9
9
  License: MIT
10
10
  """
11
11
 
12
- __version__ = "2.1.0"
12
+ __version__ = "2.3.0"
13
13
  __author__ = "Favour Bamgboye"
14
14
  __description__ = "Python environment diagnostic CLI — your project's doctor."
@@ -6,6 +6,7 @@ Calculates the numerical project health score and verdict.
6
6
 
7
7
  from __future__ import annotations
8
8
 
9
+ import math
9
10
  from dataclasses import dataclass
10
11
 
11
12
  from pydoctor.core.report import DiagnosisReport, Issue
@@ -18,20 +19,62 @@ class HealthMetrics:
18
19
  message: str
19
20
 
20
21
 
22
+ # Each category can cost at most this many points. Within a category every
23
+ # extra issue still lowers the score, but by less (diminishing returns), so
24
+ # fixing issues in a project with dozens of them visibly moves the score.
25
+ CATEGORY_CAPS: dict[str, float] = {
26
+ "security": 60,
27
+ "ci": 50,
28
+ "dependencies": 40,
29
+ "environment": 30,
30
+ "outdated": 15,
31
+ "unused": 10,
32
+ }
33
+
34
+ # A project with any critical issue can't be rated Good or Excellent.
35
+ CRITICAL_CEILING = 69
36
+
37
+ # Issues that mean part of the scan didn't run.
38
+ INCOMPLETE_CODES = {"ENV_PACKAGES_UNAVAILABLE", "SEC_SCAN_INCOMPLETE"}
39
+
40
+
21
41
  def calculate_health(report: DiagnosisReport) -> HealthMetrics:
22
42
  """
23
43
  Calculate project health score (0-100) based on identified issues.
24
44
  """
25
45
  score = _compute_score(report)
26
- categories = list({i.category for i in report.issues})
27
- return _get_verdict(score, categories)
46
+ categories = list({i.category for i in report.issues if i.code not in INCOMPLETE_CODES})
47
+ metrics = _get_verdict(score, categories)
48
+ if any(i.code in INCOMPLETE_CODES for i in report.issues):
49
+ metrics.verdict = "Incomplete"
50
+ metrics.message = (
51
+ "Some checks couldn't run, so this score only covers what was checked. "
52
+ "See the errors above."
53
+ )
54
+ return metrics
28
55
 
29
56
 
30
57
  def _compute_score(report: DiagnosisReport) -> int:
31
- score = 100
58
+ raw: dict[str, float] = {}
59
+ # One upgrade usually fixes every advisory for a package, so a package
60
+ # costs the penalty of its worst advisory, not one penalty per advisory.
61
+ per_package: dict[str, int] = {}
32
62
  for issue in report.issues:
33
- score -= _calculate_issue_penalty(issue)
34
- return max(0, score)
63
+ penalty = _calculate_issue_penalty(issue)
64
+ if issue.code == "SEC_VULNERABILITY" and issue.package:
65
+ per_package[issue.package] = max(per_package.get(issue.package, 0), penalty)
66
+ continue
67
+ raw[issue.category] = raw.get(issue.category, 0) + penalty
68
+ if per_package:
69
+ raw["security"] = raw.get("security", 0) + sum(per_package.values())
70
+ lost = 0.0
71
+ for category, points in raw.items():
72
+ cap = CATEGORY_CAPS.get(category)
73
+ lost += cap * (1 - math.exp(-points / cap)) if cap else points
74
+ score = max(0, round(100 - lost))
75
+ if any(i.severity.lower() == "critical" for i in report.issues):
76
+ score = min(score, CRITICAL_CEILING)
77
+ return score
35
78
 
36
79
 
37
80
  def _calculate_issue_penalty(issue: Issue) -> int:
@@ -79,7 +79,7 @@ class CacheManager:
79
79
 
80
80
  return entry.get("value")
81
81
 
82
- def set(self, key: str, value: Any) -> None:
82
+ def set(self, key: str, value: Any, save: bool = True) -> None:
83
83
  """
84
84
  Store a value in the cache with the current timestamp.
85
85
 
@@ -87,8 +87,15 @@ class CacheManager:
87
87
  ----------
88
88
  key: Cache key.
89
89
  value: Any JSON-serialisable value.
90
+ save: Write to disk now. Pass False when setting many keys, then call
91
+ ``save()`` once (writing the whole file per key is quadratic).
90
92
  """
91
93
  self._data[key] = {"_ts": time.time(), "value": value}
94
+ if save:
95
+ self._save()
96
+
97
+ def save(self) -> None:
98
+ """Persist the cache to disk."""
92
99
  self._save()
93
100
 
94
101
  def invalidate(self, key: str) -> None: