Skip to content

feat: dynamic PyPI version resolution + add clang-include-cleaner & clang-apply-replacements#242

Merged
shenxianpeng merged 6 commits into
mainfrom
feature/support-include-cleaner-apply-replacements
Jun 14, 2026
Merged

feat: dynamic PyPI version resolution + add clang-include-cleaner & clang-apply-replacements#242
shenxianpeng merged 6 commits into
mainfrom
feature/support-include-cleaner-apply-replacements

Conversation

@shenxianpeng

@shenxianpeng shenxianpeng commented Jun 14, 2026

Copy link
Copy Markdown
Member

Summary

Replace the hardcoded version list approach with dynamic PyPI resolution and add wheel installation support for two new clang tools.

Changes

🔄 Dynamic version resolution (util.py)

  • New: _get_pypi_versions() – queries PyPI JSON API at runtime, filters pre-releases, returns (latest, [stable_versions_descending]). Results cached via lru_cache.
  • New: _resolve_version_from_pypi() – resolves user input (exact match → prefix match → friendly error) against live PyPI data.
  • Removed: versions.py (130 lines of hardcoded versions) — no longer needed.
  • Removed: scripts/update_versions.py + .github/workflows/update-versions.yml — the weekly cron job, auto-PR, and manual review process are all obsolete.

🆕 New tools support

  • clang-include-cleaner and clang-apply-replacements are now fully supported through the same dynamic resolution pipeline.
  • Adding a new tool in the future requires zero changes to version infrastructure — just pass the tool name to resolve_install_with_diagnostics().

🧹 Cleanup

  • release-drafter.yml: simplified — versions are always "latest from PyPI", no more extracting from versions.py.
  • .github/copilot-instructions.md: updated to reflect the new architecture.

✅ Tests

  • 45 unit tests covering dynamic resolution, prefix matching, caching, network failure, pre-release filtering, and all four tools.

Why

Before After
Hardcoded copy of PyPI data (versions.py) Real-time PyPI query
Weekly cron → PR → human review to update Instant — new PyPI releases available immediately
Adding a tool = edit 4 places Adding a tool = pass tool name to existing function
Possible inconsistency (PyPI has version X, hooks only know up to Y) Always consistent

Net diff: -383 lines (690 deleted, 307 added).

Depends on: cpp-linter/clang-tools-pip#185

Summary by CodeRabbit

Release Notes

  • New Features

    • Tool versions are now resolved dynamically from PyPI at hook runtime, automatically ensuring access to the latest stable releases.
    • Support for pinning specific versions directly in pre-commit configuration.
  • Chores

    • Removed hardcoded version lists from the repository.
    • Eliminated automated version update workflow and manual version maintenance requirements.

…lacements

- Add version lists for clang-include-cleaner (22.1.7) and
  clang-apply-replacements (16.0.0, 17.0.6) in versions.py
- Extend _versions_for_tool and _default_version_for_tool in util.py
  to handle the new tools via lookup maps
- Add comprehensive tests for version resolution, default versions,
  install dispatch, and diagnostic messages
@coderabbitai

coderabbitai Bot commented Jun 14, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@pre-commit-ci[bot], we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 39 minutes and 4 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 85779576-c9b0-4dde-bc12-17f8e446c6bb

📥 Commits

Reviewing files that changed from the base of the PR and between 2c17f82 and ca55ccc.

📒 Files selected for processing (2)
  • cpp_linter_hooks/util.py
  • tests/test_util.py

Walkthrough

Replaces the static versions.py module, scripts/update_versions.py, and the update-versions.yml workflow with runtime PyPI JSON API resolution in util.py. Two new cached helpers (_get_pypi_versions, _resolve_version_from_pypi) perform stable-release filtering and prefix/exact matching. Tests, the release-drafter workflow, and Copilot instructions are updated to match.

Changes

Dynamic PyPI version resolution

Layer / File(s) Summary
PyPI resolution helpers and install integration
cpp_linter_hooks/util.py
Adds _get_pypi_versions (lru_cache-backed PyPI JSON fetch, prerelease filtering, sorted stable list) and _resolve_version_from_pypi (latest, exact, or prefix match, or error string). Updates resolve_install_with_diagnostics to call the new PyPI resolver and propagates user-facing errors; verbose path now reports "latest" instead of "default".
Reworked test suite for PyPI resolution and install paths
tests/test_util.py
Removes tests for get_version_from_dependency, _resolve_version, and hardcoded version-list constants. Adds mock PyPI JSON fixtures and a _pypi_side_effect mapper; covers _get_pypi_versions success/failure/prerelease-only, _resolve_version_from_pypi exact/prefix/latest/invalid, _is_version_installed match/mismatch, _install_tool pip invocation and failure, and resolve_install/resolve_install_with_diagnostics for all four clang wheel tools.
Remove static versioning artifacts and update CI/docs
cpp_linter_hooks/versions.py, scripts/update_versions.py, .github/workflows/update-versions.yml, .github/workflows/release-drafter.yml, .github/copilot-instructions.md
Removes versions.py (hardcoded CLANG_FORMAT_VERSIONS/CLANG_TIDY_VERSIONS), scripts/update_versions.py, and the update-versions.yml scheduled workflow. Updates the release-drafter step to write a dynamic-resolution note instead of extracting concrete versions. Updates Copilot instructions to remove versions.py references and note that no manual version updates are needed.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • cpp-linter/cpp-linter-hooks#187: Modifies the same util.py version-resolution and install flow (_is_version_installed, resolve_install) and corresponding tests/test_util.py coverage that this PR rewrites for dynamic PyPI resolution.
  • cpp-linter/cpp-linter-hooks#132: Touches the same clang-tool version infrastructure — cpp_linter_hooks/versions.py, util.py, and the automation around updating those versions — which this PR removes and replaces.
  • cpp-linter/cpp-linter-hooks#92: Modifies util.py's version resolution logic to derive defaults from pyproject.toml, the exact approach this PR replaces with PyPI runtime resolution.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: dynamic PyPI version resolution and adding support for two new clang tools (clang-include-cleaner and clang-apply-replacements).
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/support-include-cleaner-apply-replacements

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov

codecov Bot commented Jun 14, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 97.51%. Comparing base (6f66123) to head (ca55ccc).

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #242      +/-   ##
==========================================
+ Coverage   97.07%   97.51%   +0.43%     
==========================================
  Files           4        3       -1     
  Lines         239      241       +2     
==========================================
+ Hits          232      235       +3     
+ Misses          7        6       -1     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@codspeed-hq

codspeed-hq Bot commented Jun 14, 2026

Copy link
Copy Markdown

Merging this PR will degrade performance by 23.12%

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

❌ 3 regressed benchmarks
✅ 62 untouched benchmarks
🆕 41 new benchmarks
⏩ 38 skipped benchmarks1

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Benchmark BASE HEAD Efficiency
test_resolve_install_tool_already_installed_correct_version 933.8 µs 1,256.1 µs -25.65%
test_resolve_install_tool_not_installed 951.1 µs 1,271.5 µs -25.2%
test_resolve_install_invalid_version 1 ms 1.2 ms -18.29%
🆕 test_resolve_install_apply_replacements N/A 1.3 ms N/A
🆕 test_resolve_install_include_cleaner N/A 1.3 ms N/A
🆕 test_resolve_install_with_diagnostics_apply_replacements_invalid N/A 535.3 µs N/A
🆕 test_resolve_install_with_diagnostics_include_cleaner_invalid N/A 532.9 µs N/A
🆕 test_resolve_install_with_diagnostics_include_cleaner_verbose N/A 1.3 ms N/A
🆕 test_get_pypi_versions_all_prerelease N/A 2.1 ms N/A
🆕 test_get_pypi_versions_network_failure N/A 690 µs N/A
🆕 test_get_pypi_versions_success N/A 2.1 ms N/A
🆕 test_resolve_install_apply_replacements_default N/A 1.3 ms N/A
🆕 test_resolve_install_apply_replacements_invalid N/A 1.2 ms N/A
🆕 test_resolve_install_include_cleaner_default N/A 1.3 ms N/A
🆕 test_resolve_install_include_cleaner_invalid N/A 1.2 ms N/A
🆕 test_resolve_install_no_version_uses_latest N/A 1.3 ms N/A
🆕 test_resolve_install_tool_version_mismatch_reinstalls N/A 1.6 ms N/A
🆕 test_resolve_install_with_diagnostics_invalid_version N/A 537.7 µs N/A
🆕 test_resolve_install_with_diagnostics_verbose_latest N/A 1.3 ms N/A
🆕 test_resolve_install_with_diagnostics_verbose_resolved N/A 1.3 ms N/A
... ... ... ... ...

ℹ️ Only the first 20 benchmarks are displayed. Go to the app to view all benchmarks.

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing feature/support-include-cleaner-apply-replacements (1ddca97) with main (6e33844)2

Open in CodSpeed

Footnotes

  1. 38 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

  2. No successful run was found on main (6f66123) during the generation of this report, so 6e33844 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

Replace the hardcoded version lists (versions.py, update_versions.yml) with
dynamic resolution via PyPI's JSON API at hook runtime.

Why:
- Eliminates the need for weekly cron jobs + manual PR reviews to keep
  version lists current
- New PyPI releases are instantly available — no lag
- Adding a new tool (e.g. clang-include-cleaner) requires zero changes to
  version infrastructure
- The hardcoded approach was a shadow copy of PyPI's ground truth,
  creating inconsistency risks

Changes:
- util.py: Add _get_pypi_versions() (PyPI JSON + stable filtering + lru_cache)
  and _resolve_version_from_pypi() for dynamic prefix matching
- Delete versions.py — no more hardcoded lists
- Delete scripts/update_versions.py and update-versions.yml — obsolete
- Update release-drafter.yml — versions are now always latest from PyPI
- Update copilot-instructions.md to reflect new architecture
- tests/test_util.py: 45 tests covering dynamic resolution, caching,
  network failure, prefix matching, and all four tools
@shenxianpeng shenxianpeng force-pushed the feature/support-include-cleaner-apply-replacements branch from a845e43 to 5027134 Compare June 14, 2026 19:11
@shenxianpeng shenxianpeng changed the title feat: add wheel support for clang-include-cleaner and clang-apply-replacements feat: dynamic PyPI version resolution + add clang-include-cleaner & clang-apply-replacements Jun 14, 2026
@github-actions github-actions Bot added the documentation Improvements or additions to documentation label Jun 14, 2026
Comment thread tests/test_util.py Fixed
Comment thread tests/test_util.py Fixed
Comment thread tests/test_util.py Fixed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cpp_linter_hooks/util.py`:
- Around line 76-80: The prefix matching logic using startswith() can
incorrectly match unintended versions because it performs a simple string prefix
match rather than matching on dot-separated version segments (e.g., "21.1" would
match both "21.1.8" and "21.10.x"). Replace the startswith() check in the list
comprehension with logic that splits both the user_input and each version string
by dots, then verifies that the version segments match the user_input segments
at the beginning. This ensures proper dot-separated segment matching so that
"21.1" only matches versions starting with "21.1" and not "21.10" or higher.

In `@tests/test_util.py`:
- Line 1: Replace all EN DASH characters (`–`) with ASCII hyphens (`-`) in the
docstrings throughout the file. The Ruff RUF002 rule flags these characters as
linting violations. Search for docstrings containing the EN DASH character and
replace each occurrence with a standard ASCII hyphen to resolve the lint
failures.
- Around line 55-68: The cache verification calls are made outside the urlopen
patch context, which means if the cache is broken, the test will attempt a real
network call and become flaky. Move the second _get_pypi_versions call and its
subsequent assertions (latest2, versions2) inside the patch context manager to
ensure the cache test remains deterministic and does not require network access.
- Around line 101-119: The parametrized test cases do not cover all major
versions from 16 through 21 as required. Review the current test cases in the
parametrize decorator and identify which major versions are missing
(specifically check for version 18). Add additional parametrized test cases to
ensure every major version from 16 to 21 is represented, including both exact
version matches and prefix matches where applicable for the tools being tested
in this parametrize block.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5cca0afa-ecef-4ec2-b782-9886d0d4d0aa

📥 Commits

Reviewing files that changed from the base of the PR and between 6f66123 and 2c17f82.

📒 Files selected for processing (7)
  • .github/copilot-instructions.md
  • .github/workflows/release-drafter.yml
  • .github/workflows/update-versions.yml
  • cpp_linter_hooks/util.py
  • cpp_linter_hooks/versions.py
  • scripts/update_versions.py
  • tests/test_util.py
💤 Files with no reviewable changes (3)
  • scripts/update_versions.py
  • cpp_linter_hooks/versions.py
  • .github/workflows/update-versions.yml

Comment thread cpp_linter_hooks/util.py
Comment on lines +76 to +80
# Prefix match (e.g. "20" → "20.1.8"). Versions are newest-first,
# so the first matching entry is the latest for that prefix.
matched = [v for v in versions if v.startswith(user_input)]
if matched:
return matched[0], None

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Prefix matching can resolve the wrong major/minor line.

Using startswith() for partial version matching can select unintended versions (e.g., 21.1 also matches 21.10.x). Match on dot-separated segments instead.

Suggested fix
-    matched = [v for v in versions if v.startswith(user_input)]
+    requested_parts = user_input.split(".")
+    matched = [
+        v
+        for v in versions
+        if v.split(".")[: len(requested_parts)] == requested_parts
+    ]
     if matched:
         return matched[0], None
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cpp_linter_hooks/util.py` around lines 76 - 80, The prefix matching logic
using startswith() can incorrectly match unintended versions because it performs
a simple string prefix match rather than matching on dot-separated version
segments (e.g., "21.1" would match both "21.1.8" and "21.10.x"). Replace the
startswith() check in the list comprehension with logic that splits both the
user_input and each version string by dots, then verifies that the version
segments match the user_input segments at the beginning. This ensures proper
dot-separated segment matching so that "21.1" only matches versions starting
with "21.1" and not "21.10" or higher.

Comment thread tests/test_util.py Outdated
Comment thread tests/test_util.py
Comment on lines +55 to +68
with patch("urllib.request.urlopen") as mock_urlopen:
mock_urlopen.return_value.__enter__.return_value.read.return_value = (
__import__("json").dumps(mock_data).encode()
)
latest, versions = _get_pypi_versions("clang-format")

assert latest == "22.1.5"
assert "22.1.0-rc1" not in versions
assert "22.1.0a3" not in versions
assert versions == ["22.1.5", "22.1.4", "20.1.8"]
# Verify cache works
latest2, versions2 = _get_pypi_versions("clang-format")
assert latest2 == latest
assert versions2 == versions

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keep the cache test deterministic and offline-safe.

The cache verification makes the second call after the urlopen patch context exits, so a cache regression would trigger a real network call and make this unit test flaky.

Suggested fix
 with patch("urllib.request.urlopen") as mock_urlopen:
     mock_urlopen.return_value.__enter__.return_value.read.return_value = (
         __import__("json").dumps(mock_data).encode()
     )
     latest, versions = _get_pypi_versions("clang-format")
+    # Verify cache works without leaving the mocked network context
+    latest2, versions2 = _get_pypi_versions("clang-format")
 
 assert latest == "22.1.5"
 assert "22.1.0-rc1" not in versions
 assert "22.1.0a3" not in versions
 assert versions == ["22.1.5", "22.1.4", "20.1.8"]
-# Verify cache works
-latest2, versions2 = _get_pypi_versions("clang-format")
 assert latest2 == latest
 assert versions2 == versions
+assert mock_urlopen.call_count == 1
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_util.py` around lines 55 - 68, The cache verification calls are
made outside the urlopen patch context, which means if the cache is broken, the
test will attempt a real network call and become flaky. Move the second
_get_pypi_versions call and its subsequent assertions (latest2, versions2)
inside the patch context manager to ensure the cache test remains deterministic
and does not require network access.

Comment thread tests/test_util.py
shenxianpeng and others added 3 commits June 14, 2026 22:28
When no explicit --version is requested and PyPI is unreachable,
fall back to whatever version of the tool is already installed
locally instead of failing immediately.  This preserves the
pre-existing behavior where pre-installed tools worked offline.

Only applies when version=None (auto-detect).  When the user
specifies a specific --version, PyPI must be reachable to verify
it exists and resolve prefix matches.

Also fix EN DASH characters in test docstrings (Ruff RUF002).

New:
- _detect_installed_version() helper with version regex extraction
- 5 new tests: offline fallback (happy path, no tool, with version),
  detect installed version tests
@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
1 Security Hotspot

See analysis details on SonarQube Cloud

@shenxianpeng shenxianpeng merged commit d8299d6 into main Jun 14, 2026
30 of 32 checks passed
@shenxianpeng shenxianpeng deleted the feature/support-include-cleaner-apply-replacements branch June 14, 2026 19:49
@shenxianpeng shenxianpeng removed the documentation Improvements or additions to documentation label Jun 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants