diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml index 4e30e38..1f40fdb 100644 --- a/.pre-commit-hooks.yaml +++ b/.pre-commit-hooks.yaml @@ -3,7 +3,7 @@ description: Automatically install any specific version of clang-format and format C/C++ code entry: clang-format-hook language: python - files: \.(h\+\+|h|hh|hxx|hpp|c|cc|cpp|c\+\+|cxx)$ + types_or: [c++, c] require_serial: false - id: clang-tidy @@ -11,5 +11,5 @@ description: Automatically install any specific version of clang-tidy and diagnose/fix typical programming errors entry: clang-tidy-hook language: python - files: \.(h\+\+|h|hh|hxx|hpp|c|cc|cpp|c\+\+|cxx)$ + types_or: [c++, c] require_serial: false diff --git a/README.md b/README.md index 193f308..628172c 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ Add this configuration to your `.pre-commit-config.yaml` file: ```yaml repos: - repo: https://github.com/cpp-linter/cpp-linter-hooks - rev: v1.1.0 # Use the tag or commit you want + rev: v1.1.2 # Use the tag or commit you want hooks: - id: clang-format args: [--style=Google] # Other coding style: LLVM, GNU, Chromium, Microsoft, Mozilla, WebKit. @@ -46,7 +46,7 @@ To use custom configurations like `.clang-format` and `.clang-tidy`: ```yaml repos: - repo: https://github.com/cpp-linter/cpp-linter-hooks - rev: v1.1.0 + rev: v1.1.2 hooks: - id: clang-format args: [--style=file] # Loads style from .clang-format file @@ -54,6 +54,9 @@ repos: args: [--checks=.clang-tidy] # Loads checks from .clang-tidy file ``` +> [!TIP] +> By default, the latest version of [`clang-format`](https://pypi.org/project/clang-format/#history) and [`clang-tidy`](https://pypi.org/project/clang-tidy/#history) will be installed if not specified. You can specify the version using the `--version` argument in the `args` list as shown below. + ### Custom Clang Tool Version To use specific versions of clang-format and clang-tidy (using Python wheel packages): @@ -61,7 +64,7 @@ To use specific versions of clang-format and clang-tidy (using Python wheel pack ```yaml repos: - repo: https://github.com/cpp-linter/cpp-linter-hooks - rev: v1.1.0 + rev: v1.1.2 hooks: - id: clang-format args: [--style=file, --version=21] # Specifies version @@ -69,9 +72,6 @@ repos: args: [--checks=.clang-tidy, --version=21] # Specifies version ``` -> [!NOTE] -> Starting from version **v1.0.0**, this pre-commit hook now relies on Python wheel packages — [clang-format](https://pypi.org/project/clang-format/) and [clang-tidy](https://pypi.org/project/clang-tidy/) — instead of the [clang-tools binaries](https://github.com/cpp-linter/clang-tools-static-binaries). The wheel packages are lighter, easier to install, and offer better cross-platform compatibility. For more information, see the [detailed migration notes](docs/migration-notes.md). - ## Output ### clang-format Output @@ -151,7 +151,7 @@ Use -header-filter=.* to display errors from all non-system headers. Use -system ```yaml - repo: https://github.com/cpp-linter/cpp-linter-hooks - rev: v1.1.0 + rev: v1.1.2 hooks: - id: clang-format args: [--style=file, --version=21] @@ -177,7 +177,7 @@ This approach ensures that only modified files are checked, further speeding up ```yaml repos: - repo: https://github.com/cpp-linter/cpp-linter-hooks - rev: v1.1.0 + rev: v1.1.2 hooks: - id: clang-format args: [--style=file, --version=21, --verbose] # Add -v or --verbose for detailed output diff --git a/cpp_linter_hooks/clang_format.py b/cpp_linter_hooks/clang_format.py index ae732f6..0cd0d34 100644 --- a/cpp_linter_hooks/clang_format.py +++ b/cpp_linter_hooks/clang_format.py @@ -3,7 +3,7 @@ from argparse import ArgumentParser from typing import Tuple -from .util import ensure_installed, DEFAULT_CLANG_FORMAT_VERSION +from cpp_linter_hooks.util import _resolve_install, DEFAULT_CLANG_FORMAT_VERSION parser = ArgumentParser() @@ -15,8 +15,9 @@ def run_clang_format(args=None) -> Tuple[int, str]: hook_args, other_args = parser.parse_known_args(args) - tool_name = ensure_installed("clang-format", hook_args.version) - command = [tool_name, "-i"] + if hook_args.version: + _resolve_install("clang-format", hook_args.version) + command = ["clang-format", "-i"] # Add verbose flag if requested if hook_args.verbose: diff --git a/cpp_linter_hooks/clang_tidy.py b/cpp_linter_hooks/clang_tidy.py index 7f5dd5c..f1cb163 100644 --- a/cpp_linter_hooks/clang_tidy.py +++ b/cpp_linter_hooks/clang_tidy.py @@ -2,7 +2,7 @@ from argparse import ArgumentParser from typing import Tuple -from .util import ensure_installed, DEFAULT_CLANG_TIDY_VERSION +from cpp_linter_hooks.util import _resolve_install, DEFAULT_CLANG_TIDY_VERSION parser = ArgumentParser() @@ -11,9 +11,9 @@ def run_clang_tidy(args=None) -> Tuple[int, str]: hook_args, other_args = parser.parse_known_args(args) - tool_name = ensure_installed("clang-tidy", hook_args.version) - command = [tool_name] - command.extend(other_args) + if hook_args.version: + _resolve_install("clang-tidy", hook_args.version) + command = ["clang-tidy"] + other_args retval = 0 output = "" diff --git a/cpp_linter_hooks/util.py b/cpp_linter_hooks/util.py index 80f9b34..7e652d0 100644 --- a/cpp_linter_hooks/util.py +++ b/cpp_linter_hooks/util.py @@ -20,16 +20,10 @@ def get_version_from_dependency(tool: str) -> Optional[str]: return None with open(pyproject_path, "rb") as f: data = tomllib.load(f) - # First try project.optional-dependencies.tools - optional_deps = data.get("project", {}).get("optional-dependencies", {}) - tools_deps = optional_deps.get("tools", []) - for dep in tools_deps: - if dep.startswith(f"{tool}=="): - return dep.split("==")[1] - - # Fallback to project.dependencies for backward compatibility - dependencies = data.get("project", {}).get("dependencies", []) - for dep in dependencies: + # Check build-system.requires + build_system = data.get("build-system", {}) + requires = build_system.get("requires", []) + for dep in requires: if dep.startswith(f"{tool}=="): return dep.split("==")[1] return None @@ -148,29 +142,16 @@ def parse_version(v: str): return None -def _get_runtime_version(tool: str) -> Optional[str]: - """Get the runtime version of a tool.""" - try: - output = subprocess.check_output([tool, "--version"], text=True) - if tool == "clang-tidy": - lines = output.strip().splitlines() - if len(lines) > 1: - return lines[1].split()[-1] - elif tool == "clang-format": - return output.strip().split()[-1] - except Exception: - return None - - def _install_tool(tool: str, version: str) -> Optional[Path]: - """Install a tool using pip.""" + """Install a tool using pip, suppressing output.""" try: subprocess.check_call( - [sys.executable, "-m", "pip", "install", f"{tool}=={version}"] + [sys.executable, "-m", "pip", "install", f"{tool}=={version}"], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, ) return shutil.which(tool) except subprocess.CalledProcessError: - LOG.error("Failed to install %s==%s", tool, version) return None @@ -187,44 +168,4 @@ def _resolve_install(tool: str, version: Optional[str]) -> Optional[Path]: else DEFAULT_CLANG_TIDY_VERSION ) - # Additional safety check in case DEFAULT versions are None - if user_version is None: - user_version = ( - DEFAULT_CLANG_FORMAT_VERSION - if tool == "clang-format" - else DEFAULT_CLANG_TIDY_VERSION - ) - - path = shutil.which(tool) - if path: - runtime_version = _get_runtime_version(tool) - if runtime_version and user_version not in runtime_version: - LOG.info( - "%s version mismatch (%s != %s), reinstalling...", - tool, - runtime_version, - user_version, - ) - return _install_tool(tool, user_version) - return Path(path) - return _install_tool(tool, user_version) - - -def is_installed(tool: str) -> Optional[Path]: - """Check if a tool is installed and return its path.""" - path = shutil.which(tool) - if path: - return Path(path) - return None - - -def ensure_installed(tool: str, version: Optional[str] = None) -> str: - """Ensure a tool is installed, resolving its version if necessary.""" - LOG.info("Ensuring %s is installed", tool) - tool_path = _resolve_install(tool, version) - if tool_path: - LOG.info("%s available at %s", tool, tool_path) - return tool - LOG.warning("%s not found and could not be installed", tool) - return tool diff --git a/pyproject.toml b/pyproject.toml index 5665823..2e66bcd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools>=45", "setuptools-scm"] +requires = ["setuptools>=45", "setuptools-scm", "clang-format==21.1.0", "clang-tidy==21.1.0"] build-backend = "setuptools.build_meta" requires-python = ">=3.9" @@ -46,12 +46,6 @@ source = "https://github.com/cpp-linter/cpp-linter-hooks" tracker = "https://github.com/cpp-linter/cpp-linter-hooks/issues" [project.optional-dependencies] -# only clang tools can added to this section to make hooks work -tools = [ - "clang-format==21.1.0", - "clang-tidy==21.1.0", -] - dev = [ "coverage", "pre-commit", diff --git a/testing/benchmark_hook_1.yaml b/testing/benchmark_hook_1.yaml index 7cac53f..b8ba6da 100644 --- a/testing/benchmark_hook_1.yaml +++ b/testing/benchmark_hook_1.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/cpp-linter/cpp-linter-hooks - rev: v1.1.0 + rev: v1.1.2 hooks: - id: clang-format args: [--style=file, --version=21] diff --git a/tests/test_util.py b/tests/test_util.py index 6b3d806..0c73714 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -1,17 +1,12 @@ -import logging import pytest -from itertools import product from unittest.mock import patch from pathlib import Path import subprocess import sys from cpp_linter_hooks.util import ( - ensure_installed, - is_installed, get_version_from_dependency, _resolve_version, - _get_runtime_version, _install_tool, _resolve_install, CLANG_FORMAT_VERSIONS, @@ -25,91 +20,19 @@ TOOLS = ["clang-format", "clang-tidy"] -@pytest.mark.benchmark -@pytest.mark.parametrize(("tool", "version"), list(product(TOOLS, VERSIONS))) -def test_ensure_installed(tool, version, tmp_path, monkeypatch, caplog): - """Test that ensure_installed returns the tool name for wheel packages.""" - with monkeypatch.context(): - # Mock shutil.which to simulate the tool being available - with patch("shutil.which", return_value=str(tmp_path / tool)): - # Mock _get_runtime_version to return a matching version - mock_version = "20.1.7" if tool == "clang-format" else "20.1.0" - with patch( - "cpp_linter_hooks.util._get_runtime_version", return_value=mock_version - ): - caplog.clear() - caplog.set_level(logging.INFO, logger="cpp_linter_hooks.util") - - if version is None: - result = ensure_installed(tool) - else: - result = ensure_installed(tool, version=version) - - # Should return the tool name for direct execution - assert result == tool - - # Check that we logged ensuring the tool is installed - assert any("Ensuring" in record.message for record in caplog.records) - - -@pytest.mark.benchmark -def test_is_installed_with_shutil_which(tmp_path): - """Test is_installed when tool is found via shutil.which.""" - tool_path = tmp_path / "clang-format" - tool_path.touch() - - with patch("shutil.which", return_value=str(tool_path)): - result = is_installed("clang-format") - assert result == tool_path - - -@pytest.mark.benchmark -def test_is_installed_not_found(): - """Test is_installed when tool is not found anywhere.""" - with ( - patch("shutil.which", return_value=None), - patch("sys.executable", "/nonexistent/python"), - ): - result = is_installed("clang-format") - assert result is None - - -@pytest.mark.benchmark -def test_ensure_installed_tool_not_found(caplog): - """Test ensure_installed when tool is not found.""" - with ( - patch("shutil.which", return_value=None), - patch("cpp_linter_hooks.util._install_tool", return_value=None), - ): - caplog.clear() - caplog.set_level(logging.WARNING, logger="cpp_linter_hooks.util") - - result = ensure_installed("clang-format") - - # Should still return the tool name - assert result == "clang-format" - - # Should log a warning - assert any( - "not found and could not be installed" in record.message - for record in caplog.records - ) - - # Tests for get_version_from_dependency @pytest.mark.benchmark def test_get_version_from_dependency_success(): """Test get_version_from_dependency with valid pyproject.toml.""" mock_toml_content = { - "project": { - "optional-dependencies": { - "tools": [ - "clang-format==20.1.7", - "clang-tidy==20.1.0", - "other-package==1.0.0", - ] - } - } + "build-system": { + "requires": [ + "clang-format==20.1.7", + "clang-tidy==20.1.0", + "other-package==1.0.0", + ] + }, + "project": {}, } with ( @@ -134,9 +57,7 @@ def test_get_version_from_dependency_missing_file(): @pytest.mark.benchmark def test_get_version_from_dependency_missing_dependency(): """Test get_version_from_dependency with missing dependency.""" - mock_toml_content = { - "project": {"optional-dependencies": {"tools": ["other-package==1.0.0"]}} - } + mock_toml_content = {"build-system": {"requires": ["other-package==1.0.0"]}} with ( patch("pathlib.Path.exists", return_value=True), @@ -198,48 +119,6 @@ def test_resolve_version_clang_tidy(user_input, expected): assert result == expected -# Tests for _get_runtime_version -@pytest.mark.benchmark -def test_get_runtime_version_clang_format(): - """Test _get_runtime_version for clang-format.""" - mock_output = "Ubuntu clang-format version 20.1.7-1ubuntu1\n" - - with patch("subprocess.check_output", return_value=mock_output): - result = _get_runtime_version("clang-format") - assert result == "20.1.7-1ubuntu1" - - -@pytest.mark.benchmark -def test_get_runtime_version_clang_tidy(): - """Test _get_runtime_version for clang-tidy.""" - mock_output = "LLVM (http://llvm.org/):\n LLVM version 20.1.0\n" - - with patch("subprocess.check_output", return_value=mock_output): - result = _get_runtime_version("clang-tidy") - assert result == "20.1.0" - - -@pytest.mark.benchmark -def test_get_runtime_version_exception(): - """Test _get_runtime_version when subprocess fails.""" - with patch( - "subprocess.check_output", - side_effect=subprocess.CalledProcessError(1, ["clang-format"]), - ): - result = _get_runtime_version("clang-format") - assert result is None - - -@pytest.mark.benchmark -def test_get_runtime_version_clang_tidy_single_line(): - """Test _get_runtime_version for clang-tidy with single line output.""" - mock_output = "LLVM version 20.1.0\n" - - with patch("subprocess.check_output", return_value=mock_output): - result = _get_runtime_version("clang-tidy") - assert result is None # Should return None for single line - - # Tests for _install_tool @pytest.mark.benchmark def test_install_tool_success(): @@ -254,7 +133,9 @@ def test_install_tool_success(): assert result == mock_path mock_check_call.assert_called_once_with( - [sys.executable, "-m", "pip", "install", "clang-format==20.1.7"] + [sys.executable, "-m", "pip", "install", "clang-format==20.1.7"], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, ) @@ -266,15 +147,11 @@ def test_install_tool_failure(): "subprocess.check_call", side_effect=subprocess.CalledProcessError(1, ["pip"]), ), - patch("cpp_linter_hooks.util.LOG") as mock_log, + patch("cpp_linter_hooks.util.LOG"), ): result = _install_tool("clang-format", "20.1.7") assert result is None - mock_log.error.assert_called_once_with( - "Failed to install %s==%s", "clang-format", "20.1.7" - ) - @pytest.mark.benchmark def test_install_tool_success_but_not_found(): @@ -292,10 +169,9 @@ def test_resolve_install_tool_already_installed_correct_version(): with ( patch("shutil.which", return_value=mock_path), - patch("cpp_linter_hooks.util._get_runtime_version", return_value="20.1.7"), ): result = _resolve_install("clang-format", "20.1.7") - assert result == Path(mock_path) + assert Path(result) == Path(mock_path) @pytest.mark.benchmark @@ -305,22 +181,14 @@ def test_resolve_install_tool_version_mismatch(): with ( patch("shutil.which", return_value=mock_path), - patch("cpp_linter_hooks.util._get_runtime_version", return_value="18.1.8"), patch( "cpp_linter_hooks.util._install_tool", return_value=Path(mock_path) ) as mock_install, - patch("cpp_linter_hooks.util.LOG") as mock_log, ): result = _resolve_install("clang-format", "20.1.7") assert result == Path(mock_path) mock_install.assert_called_once_with("clang-format", "20.1.7") - mock_log.info.assert_called_once_with( - "%s version mismatch (%s != %s), reinstalling...", - "clang-format", - "18.1.8", - "20.1.7", - ) @pytest.mark.benchmark @@ -376,40 +244,6 @@ def test_resolve_install_invalid_version(): ) -# Tests for ensure_installed edge cases -@pytest.mark.benchmark -def test_ensure_installed_version_mismatch(caplog): - """Test ensure_installed with version mismatch scenario.""" - mock_path = "/usr/bin/clang-format" - - with ( - patch("shutil.which", return_value=mock_path), - patch("cpp_linter_hooks.util._get_runtime_version", return_value="18.1.8"), - patch("cpp_linter_hooks.util._install_tool", return_value=Path(mock_path)), - ): - caplog.clear() - caplog.set_level(logging.INFO, logger="cpp_linter_hooks.util") - - result = ensure_installed("clang-format", "20.1.7") - assert result == "clang-format" - - # Should log version mismatch - assert any("version mismatch" in record.message for record in caplog.records) - - -@pytest.mark.benchmark -def test_ensure_installed_no_runtime_version(): - """Test ensure_installed when runtime version cannot be determined.""" - mock_path = "/usr/bin/clang-format" - - with ( - patch("shutil.which", return_value=mock_path), - patch("cpp_linter_hooks.util._get_runtime_version", return_value=None), - ): - result = ensure_installed("clang-format", "20.1.7") - assert result == "clang-format" - - # Tests for constants and defaults @pytest.mark.benchmark def test_default_versions():