pre-commit-localupdate 0.1.0__tar.gz → 0.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 (17) hide show
  1. pre_commit_localupdate-0.1.0/README.md → pre_commit_localupdate-0.3.0/PKG-INFO +47 -6
  2. pre_commit_localupdate-0.1.0/PKG-INFO → pre_commit_localupdate-0.3.0/README.md +32 -21
  3. pre_commit_localupdate-0.3.0/pre_commit_localupdate/__init__.py +1 -0
  4. {pre_commit_localupdate-0.1.0 → pre_commit_localupdate-0.3.0}/pre_commit_localupdate/__main__.py +1 -1
  5. {pre_commit_localupdate-0.1.0 → pre_commit_localupdate-0.3.0}/pre_commit_localupdate/cli.py +11 -2
  6. pre_commit_localupdate-0.3.0/pre_commit_localupdate/io.py +43 -0
  7. pre_commit_localupdate-0.3.0/pre_commit_localupdate/packages/__init__.py +15 -0
  8. pre_commit_localupdate-0.3.0/pre_commit_localupdate/packages/julia.py +99 -0
  9. {pre_commit_localupdate-0.1.0/pre_commit_localupdate → pre_commit_localupdate-0.3.0/pre_commit_localupdate/packages}/node.py +1 -1
  10. {pre_commit_localupdate-0.1.0/pre_commit_localupdate → pre_commit_localupdate-0.3.0/pre_commit_localupdate/packages}/python.py +1 -1
  11. pre_commit_localupdate-0.3.0/pre_commit_localupdate/packages/rust.py +103 -0
  12. {pre_commit_localupdate-0.1.0 → pre_commit_localupdate-0.3.0}/pre_commit_localupdate/pre_commit_config.py +37 -67
  13. {pre_commit_localupdate-0.1.0 → pre_commit_localupdate-0.3.0}/pyproject.toml +9 -3
  14. pre_commit_localupdate-0.1.0/pre_commit_localupdate/__init__.py +0 -0
  15. {pre_commit_localupdate-0.1.0 → pre_commit_localupdate-0.3.0}/LICENSE +0 -0
  16. {pre_commit_localupdate-0.1.0 → pre_commit_localupdate-0.3.0}/pre_commit_localupdate/logs.py +0 -0
  17. {pre_commit_localupdate-0.1.0/pre_commit_localupdate → pre_commit_localupdate-0.3.0/pre_commit_localupdate/packages}/package.py +0 -0
@@ -1,10 +1,25 @@
1
+ Metadata-Version: 2.1
2
+ Name: pre-commit-localupdate
3
+ Version: 0.3.0
4
+ Summary: A CLI tool to automatically update additional dependencies within local Python, Julia, Rust, and Node.js hooks in pre-commit config files.
5
+ Author: M. Farzalipour Tabriz
6
+ Classifier: Development Status :: 3 - Alpha
7
+ Classifier: Environment :: Console
8
+ Classifier: Intended Audience :: Developers
9
+ Classifier: Programming Language :: Python :: 3
10
+ Project-URL: source, https://gitlab.mpcdf.mpg.de/tbz/pre-commit-localupdate.git
11
+ Requires-Python: <3.15,>=3.11
12
+ Requires-Dist: requests>=2.32.5
13
+ Requires-Dist: ruamel-yaml>=0.19.1
14
+ Description-Content-Type: text/markdown
15
+
1
16
  # pre-commit-localupdate
2
17
 
3
- A CLI tool to automatically update dependencies in `pre-commit-config.yml` files. It specifically targets `additional_dependencies` within local Python and Node.js hooks to ensure your tooling stays up-to-date. It also adds version to unversioned packages and pins exact version of loosely defined ones.
18
+ A CLI tool to automatically update dependencies in `pre-commit-config.yml` files. It specifically targets `additional_dependencies` within local Python, Julia, Rust, and Node.js hooks to ensure your tooling stays up-to-date. It also adds version to unversioned packages and pins exact version of loosely defined ones.
4
19
 
5
20
  ## Installation
6
21
 
7
- ```bash
22
+ ```shell
8
23
  pip install pre-commit-localupdate
9
24
  ```
10
25
 
@@ -12,16 +27,28 @@ pip install pre-commit-localupdate
12
27
 
13
28
  To check and update the `additional_dependencies` in your `.pre-commit-config.yaml` file, simply run:
14
29
 
15
- ```bash
30
+ ```shell
16
31
  pre-commit-localupdate
17
32
  ```
18
33
 
19
34
  By default, the tool looks for `.pre-commit-config.yaml` in the current directory. To specify a custom file path, use the `-c` or `--config` argument:
20
35
 
21
- ```bash
36
+ ```shell
22
37
  pre-commit-localupdate --config path/to/.pre-commit-config.yaml
23
38
  ```
24
39
 
40
+ To check the dependencies without updating, use the `--dry-run` flag. The command will exit with a non-zero status code if any package needs updating:
41
+
42
+ ```shell
43
+ pre-commit-localupdate --dry-run
44
+ ```
45
+
46
+ To get debug output use `--debug` flag:
47
+
48
+ ```shell
49
+ pre-commit-localupdate --debug
50
+ ```
51
+
25
52
  ## Example
26
53
 
27
54
  Given a `.pre-commit-config.yaml` with the following content:
@@ -30,6 +57,13 @@ Given a `.pre-commit-config.yaml` with the following content:
30
57
  # Comments in the file header are preserved
31
58
  ---
32
59
  repos:
60
+ # External hooks won't be touched. Use 'pre-commit autoupdate' command to update them
61
+ - repo: https://github.com/pre-commit/pre-commit-hooks
62
+ rev: v2.3.0
63
+ hooks:
64
+ - id: check-yaml
65
+ - id: end-of-file-fixer
66
+ - id: trailing-whitespace
33
67
  - repo: local
34
68
  hooks:
35
69
  # Comment about hooks are preserved
@@ -42,7 +76,7 @@ repos:
42
76
  require_serial: true
43
77
  types_or: [python, pyi]
44
78
  additional_dependencies:
45
- # Loose version definitions will be pinned to an exact version
79
+ # Loose version definitions are pinned to an exact version
46
80
  - "black>=25.1.0"
47
81
 
48
82
  - id: mypy
@@ -79,6 +113,13 @@ Running `pre-commit-localupdate` will update the file to (hypothetical latest ve
79
113
  # Comments in the file header are preserved
80
114
  ---
81
115
  repos:
116
+ # External hooks won't be touched. Use 'pre-commit autoupdate' command to update them
117
+ - repo: https://github.com/pre-commit/pre-commit-hooks
118
+ rev: v2.3.0
119
+ hooks:
120
+ - id: check-yaml
121
+ - id: end-of-file-fixer
122
+ - id: trailing-whitespace
82
123
  - repo: local
83
124
  hooks:
84
125
  # Comment about hooks are preserved
@@ -92,7 +133,7 @@ repos:
92
133
  require_serial: true
93
134
  types_or: [python, pyi]
94
135
  additional_dependencies:
95
- # Loose version definitions should be pinned
136
+ # Loose version definitions are pinned to an exact version
96
137
  - "black==26.1.0"
97
138
 
98
139
  - id: mypy
@@ -1,25 +1,10 @@
1
- Metadata-Version: 2.1
2
- Name: pre-commit-localupdate
3
- Version: 0.1.0
4
- Summary: A CLI tool to automatically update `additional_dependencies` within local Python hooks in `pre-commit-config.yml` files.
5
- Author: M. Farzalipour Tabriz
6
- Classifier: Development Status :: 3 - Alpha
7
- Classifier: Environment :: Console
8
- Classifier: Intended Audience :: Developers
9
- Classifier: Programming Language :: Python :: 3
10
- Project-URL: source, https://gitlab.mpcdf.mpg.de/tbz/pre-commit-localupdate.git
11
- Requires-Python: <3.15,>=3.10
12
- Requires-Dist: requests>=2.32.5
13
- Requires-Dist: ruamel-yaml>=0.19.1
14
- Description-Content-Type: text/markdown
15
-
16
1
  # pre-commit-localupdate
17
2
 
18
- A CLI tool to automatically update dependencies in `pre-commit-config.yml` files. It specifically targets `additional_dependencies` within local Python and Node.js hooks to ensure your tooling stays up-to-date. It also adds version to unversioned packages and pins exact version of loosely defined ones.
3
+ A CLI tool to automatically update dependencies in `pre-commit-config.yml` files. It specifically targets `additional_dependencies` within local Python, Julia, Rust, and Node.js hooks to ensure your tooling stays up-to-date. It also adds version to unversioned packages and pins exact version of loosely defined ones.
19
4
 
20
5
  ## Installation
21
6
 
22
- ```bash
7
+ ```shell
23
8
  pip install pre-commit-localupdate
24
9
  ```
25
10
 
@@ -27,16 +12,28 @@ pip install pre-commit-localupdate
27
12
 
28
13
  To check and update the `additional_dependencies` in your `.pre-commit-config.yaml` file, simply run:
29
14
 
30
- ```bash
15
+ ```shell
31
16
  pre-commit-localupdate
32
17
  ```
33
18
 
34
19
  By default, the tool looks for `.pre-commit-config.yaml` in the current directory. To specify a custom file path, use the `-c` or `--config` argument:
35
20
 
36
- ```bash
21
+ ```shell
37
22
  pre-commit-localupdate --config path/to/.pre-commit-config.yaml
38
23
  ```
39
24
 
25
+ To check the dependencies without updating, use the `--dry-run` flag. The command will exit with a non-zero status code if any package needs updating:
26
+
27
+ ```shell
28
+ pre-commit-localupdate --dry-run
29
+ ```
30
+
31
+ To get debug output use `--debug` flag:
32
+
33
+ ```shell
34
+ pre-commit-localupdate --debug
35
+ ```
36
+
40
37
  ## Example
41
38
 
42
39
  Given a `.pre-commit-config.yaml` with the following content:
@@ -45,6 +42,13 @@ Given a `.pre-commit-config.yaml` with the following content:
45
42
  # Comments in the file header are preserved
46
43
  ---
47
44
  repos:
45
+ # External hooks won't be touched. Use 'pre-commit autoupdate' command to update them
46
+ - repo: https://github.com/pre-commit/pre-commit-hooks
47
+ rev: v2.3.0
48
+ hooks:
49
+ - id: check-yaml
50
+ - id: end-of-file-fixer
51
+ - id: trailing-whitespace
48
52
  - repo: local
49
53
  hooks:
50
54
  # Comment about hooks are preserved
@@ -57,7 +61,7 @@ repos:
57
61
  require_serial: true
58
62
  types_or: [python, pyi]
59
63
  additional_dependencies:
60
- # Loose version definitions will be pinned to an exact version
64
+ # Loose version definitions are pinned to an exact version
61
65
  - "black>=25.1.0"
62
66
 
63
67
  - id: mypy
@@ -94,6 +98,13 @@ Running `pre-commit-localupdate` will update the file to (hypothetical latest ve
94
98
  # Comments in the file header are preserved
95
99
  ---
96
100
  repos:
101
+ # External hooks won't be touched. Use 'pre-commit autoupdate' command to update them
102
+ - repo: https://github.com/pre-commit/pre-commit-hooks
103
+ rev: v2.3.0
104
+ hooks:
105
+ - id: check-yaml
106
+ - id: end-of-file-fixer
107
+ - id: trailing-whitespace
97
108
  - repo: local
98
109
  hooks:
99
110
  # Comment about hooks are preserved
@@ -107,7 +118,7 @@ repos:
107
118
  require_serial: true
108
119
  types_or: [python, pyi]
109
120
  additional_dependencies:
110
- # Loose version definitions should be pinned
121
+ # Loose version definitions are pinned to an exact version
111
122
  - "black==26.1.0"
112
123
 
113
124
  - id: mypy
@@ -0,0 +1 @@
1
+ __version__ = '0.3.0'
@@ -24,7 +24,7 @@ def main() -> None:
24
24
  args = parse_args()
25
25
  setup_logging(args.debug)
26
26
 
27
- if update_additional_dependencies(Path(args.config)):
27
+ if update_additional_dependencies(Path(args.config), dry_run=args.dry_run):
28
28
  logging.info("Done.")
29
29
  else:
30
30
  logging.info("File content is already up to date.")
@@ -3,17 +3,24 @@
3
3
 
4
4
  import argparse
5
5
 
6
+ from pre_commit_localupdate import __version__
7
+
6
8
 
7
9
  def parse_args() -> argparse.Namespace:
8
10
  """Parse command line arguments."""
9
11
  parser = argparse.ArgumentParser(
10
- description="Update Python and Node.js additional_dependencies in .pre-commit-config.yaml"
12
+ description="Automatically update additional dependencies within local Python, Julia, Rust, and Node.js hooks in pre-commit config files."
11
13
  )
12
14
  parser.add_argument(
13
15
  "--debug",
14
16
  action="store_true",
15
17
  help="Enable debug logging",
16
18
  )
19
+ parser.add_argument(
20
+ "--dry-run",
21
+ action="store_true",
22
+ help="Dry run mode. Do not update the file and exit with a non-zero code if the configuration files require an update.",
23
+ )
17
24
  parser.add_argument(
18
25
  "-c",
19
26
  "--config",
@@ -21,5 +28,7 @@ def parse_args() -> argparse.Namespace:
21
28
  help="pre-commit config file path",
22
29
  default=".pre-commit-config.yaml",
23
30
  )
24
-
31
+ parser.add_argument(
32
+ "--version", action="version", version=f"pre-commit-localupdate {__version__}"
33
+ )
25
34
  return parser.parse_args()
@@ -0,0 +1,43 @@
1
+ # SPDX-FileCopyrightText: 2026 M. Farzalipour Tabriz, Max Planck Institute for Physics
2
+ # SPDX-License-Identifier: LGPL-3.0-or-later
3
+
4
+ import logging
5
+ import sys
6
+ from pathlib import Path
7
+ from typing import List, Tuple
8
+
9
+
10
+ def load_config_file(file_path: Path) -> Tuple[List[str], str]:
11
+ """Reads pre-commit configuration file and returns its header lines and content."""
12
+ logging.debug("Reading configuration file: %s", file_path)
13
+
14
+ raw_lines: List[str] = []
15
+ header_lines: List[str] = []
16
+ content_start_index: int = 0
17
+
18
+ try:
19
+ with file_path.open("r", encoding="utf-8") as f:
20
+ raw_lines = f.readlines()
21
+ except FileNotFoundError:
22
+ logging.exception("File not found: %s", file_path)
23
+ sys.exit(2)
24
+ except OSError as exc:
25
+ logging.exception("IOError while reading file %s: %s", file_path, exc)
26
+ sys.exit(2)
27
+
28
+ logging.debug("Parsing file header...")
29
+ content_start_index = 0
30
+ for i, line in enumerate(raw_lines):
31
+ stripped: str = line.strip()
32
+ if stripped == "---":
33
+ logging.debug("YAML marker found.")
34
+ content_start_index = i + 1
35
+ break
36
+ if stripped and not stripped.startswith("#"):
37
+ content_start_index = i
38
+ break
39
+
40
+ header_lines = raw_lines[:content_start_index]
41
+ content: str = "".join(raw_lines[content_start_index:])
42
+
43
+ return header_lines, content
@@ -0,0 +1,15 @@
1
+ # SPDX-FileCopyrightText: 2026 M. Farzalipour Tabriz, Max Planck Institute for Physics
2
+ # SPDX-License-Identifier: LGPL-3.0-or-later
3
+
4
+ from .julia import JuliaPackage
5
+ from .node import NodePackage
6
+ from .package import PackageBase
7
+ from .python import PyPackage
8
+ from .rust import RustPackage
9
+
10
+ SUPPORTED_PACKAGES: dict[str, type[PackageBase]] = {
11
+ "julia": JuliaPackage,
12
+ "node": NodePackage,
13
+ "python": PyPackage,
14
+ "rust": RustPackage,
15
+ }
@@ -0,0 +1,99 @@
1
+ # SPDX-FileCopyrightText: 2026 M. Farzalipour Tabriz, Max Planck Institute for Physics
2
+ # SPDX-License-Identifier: LGPL-3.0-or-later
3
+
4
+ import logging
5
+ import re
6
+ import tomllib
7
+
8
+ import requests
9
+
10
+ from .package import PackageBase
11
+
12
+ # Julia General Registry Source
13
+ JULIA_PKG_API_URL = "https://raw.githubusercontent.com/JuliaRegistries/General/refs/heads/master/{package_name_first_letter}/{package_name}/Versions.toml"
14
+ REQUEST_TIMEOUT = 10
15
+
16
+
17
+ class JuliaPackage(PackageBase):
18
+ """Represents a Julia package."""
19
+
20
+ @classmethod
21
+ def from_specification(package, spec: str) -> "JuliaPackage" | None:
22
+ """Parse a package specification string into a Package object.
23
+
24
+ Handles formats like 'Example@0.4.1', 'Example@0.4', or just 'Example'.
25
+
26
+ Args:
27
+ spec: The package specification string.
28
+
29
+ Returns:
30
+ A Package instance if parsing succeeds, otherwise None.
31
+
32
+ """
33
+ clean_spec = spec.strip().strip('"').strip("'")
34
+ match = re.match(r"^([a-zA-Z0-9_]+)(?:@([a-zA-Z0-9._+,-]+))?$", clean_spec)
35
+
36
+ if not match:
37
+ logging.warning("Could not parse the package specification: %s", clean_spec)
38
+ return None
39
+
40
+ name = match.group(1)
41
+ raw_spec = match.group(2)
42
+ latest_version = package._get_latest_version(name)
43
+
44
+ return package(
45
+ name=name,
46
+ raw_spec=raw_spec,
47
+ latest_version=latest_version,
48
+ )
49
+
50
+ @staticmethod
51
+ def _get_latest_version(package_name: str) -> str | None:
52
+ """Fetch the latest version of a package from the Julia General Registry.
53
+
54
+ Returns:
55
+ The latest version string if found, otherwise None.
56
+
57
+ """
58
+ logging.debug("Fetching latest version for %s", package_name)
59
+ try:
60
+ url = JULIA_PKG_API_URL.format(
61
+ package_name=package_name, package_name_first_letter=package_name[0]
62
+ )
63
+ response = requests.get(url, timeout=REQUEST_TIMEOUT)
64
+ response.raise_for_status()
65
+
66
+ data = tomllib.loads(response.text)
67
+
68
+ versions = list(data.keys())
69
+ if not versions:
70
+ return None
71
+
72
+ versions.sort(reverse=True)
73
+ latest = versions[0]
74
+
75
+ if latest and isinstance(latest, str):
76
+ return latest
77
+
78
+ return None
79
+ except requests.exceptions.RequestException as exc:
80
+ logging.warning(
81
+ "Could not fetch latest version for %s: %s", package_name, exc
82
+ )
83
+ return None
84
+ except (KeyError, ValueError, tomllib.TOMLDecodeError) as exc:
85
+ logging.warning("Could not parse response for %s: %s", package_name, exc)
86
+ return None
87
+
88
+ def latest_version_specification(package) -> str | None:
89
+ """Version specification of package pinned to the latest version.
90
+
91
+ Returns:
92
+ The latest version specification string if found, otherwise None.
93
+ Format: PackageName@version
94
+
95
+ """
96
+ if not package.latest_version:
97
+ return None
98
+
99
+ return f"{package.name}@{package.latest_version}"
@@ -8,7 +8,7 @@ from typing import cast
8
8
 
9
9
  import requests
10
10
 
11
- from pre_commit_localupdate.package import PackageBase
11
+ from .package import PackageBase
12
12
 
13
13
  NPM_API_URL = "https://registry.npmjs.org/{package_name}"
14
14
  REQUEST_TIMEOUT = 10
@@ -7,7 +7,7 @@ from typing import cast
7
7
 
8
8
  import requests
9
9
 
10
- from pre_commit_localupdate.package import PackageBase
10
+ from .package import PackageBase
11
11
 
12
12
  PYPI_API_URL = "https://pypi.org/pypi/{package_name}/json"
13
13
  REQUEST_TIMEOUT = 10
@@ -0,0 +1,103 @@
1
+ # SPDX-FileCopyrightText: 2026 M. Farzalipour Tabriz, Max Planck Institute for Physics
2
+ # SPDX-License-Identifier: LGPL-3.0-or-later
3
+
4
+ import logging
5
+ import re
6
+ from typing import Any, cast
7
+
8
+ import requests
9
+
10
+ from .package import PackageBase
11
+
12
+ CRATES_API_URL = "https://crates.io/api/v1/crates/{package_name}"
13
+ REQUEST_TIMEOUT = 10
14
+
15
+
16
+ class RustPackage(PackageBase):
17
+ """Represents a Rust package.
18
+
19
+ Attributes:
20
+ cli: Package is a CLI tool.
21
+ """
22
+
23
+ cli: bool = False
24
+
25
+ def __init__(self, **kwargs: Any) -> None:
26
+ self.cli = kwargs.pop("cli", False)
27
+ super().__init__(**kwargs)
28
+
29
+ @classmethod
30
+ def from_specification(package, spec: str) -> "RustPackage" | None:
31
+ """Parse a package specification string into a Package object.
32
+
33
+ Handles formats like 'package', 'package:1.2.3', or 'cli:package:1.2.3'.
34
+
35
+ Args:
36
+ spec: The package specification string.
37
+
38
+ Returns:
39
+ A Package instance if parsing succeeds, otherwise None.
40
+
41
+ """
42
+ clean_spec = spec.strip().strip('"').strip("'")
43
+
44
+ match = re.match(
45
+ r"^(?:(?P<cli>cli):)?(?P<name>[a-zA-Z0-9_-]+)(?::(?P<version>[^:]+))?$",
46
+ clean_spec,
47
+ )
48
+ if not match:
49
+ logging.warning("Could not parse the package specification: %s", clean_spec)
50
+ return None
51
+
52
+ name = match.group("name")
53
+ version = match.group("version")
54
+ cli = True if match.group("cli") else False
55
+
56
+ raw_spec = f":{version}" if version else None
57
+ latest_version = package._get_latest_version(name)
58
+
59
+ return package(
60
+ name=name, raw_spec=raw_spec, latest_version=latest_version, cli=cli
61
+ )
62
+
63
+ @staticmethod
64
+ def _get_latest_version(package_name: str) -> str | None:
65
+ """Fetch the latest version of a package from crates.io.
66
+
67
+ Returns:
68
+ The latest version string if found, otherwise None.
69
+
70
+ """
71
+ logging.debug("Fetching latest version for %s", package_name)
72
+ try:
73
+ url = CRATES_API_URL.format(package_name=package_name)
74
+ response = requests.get(url, timeout=REQUEST_TIMEOUT)
75
+ response.raise_for_status()
76
+ data = response.json()
77
+ version = data.get("crate", {}).get("max_version")
78
+ if version and isinstance(version, str):
79
+ return cast("str", version)
80
+
81
+ return None
82
+ except requests.exceptions.RequestException as exc:
83
+ logging.warning(
84
+ "Could not fetch latest version for %s: %s", package_name, exc
85
+ )
86
+ return None
87
+ except (KeyError, ValueError) as exc:
88
+ logging.warning("Could not parse response for %s: %s", package_name, exc)
89
+ return None
90
+
91
+ def latest_version_specification(package) -> str | None:
92
+ """Version specification of package pinned to the latest version.
93
+
94
+ Returns:
95
+ The latest version specification string if found, otherwise None.
96
+
97
+ """
98
+ if not package.latest_version:
99
+ return None
100
+ if package.cli:
101
+ return f"cli:{package.name}:{package.latest_version}"
102
+ else:
103
+ return f"{package.name}:{package.latest_version}"
@@ -3,66 +3,33 @@
3
3
 
4
4
  import io
5
5
  import logging
6
+ import sys
6
7
  from pathlib import Path
7
8
 
8
9
  import ruamel.yaml
9
10
 
10
- from pre_commit_localupdate.node import NodePackage
11
- from pre_commit_localupdate.package import PackageBase
12
- from pre_commit_localupdate.python import PyPackage
11
+ from pre_commit_localupdate.io import load_config_file
12
+ from pre_commit_localupdate.packages import SUPPORTED_PACKAGES
13
13
 
14
- LANGUAGE_PACKAGE_MAP: dict[str, type[PackageBase]] = {
15
- "python": PyPackage,
16
- "node": NodePackage,
17
- }
18
14
 
19
-
20
- def update_additional_dependencies(file_path: Path) -> bool:
21
- """Reads the configuration file, identifies outdated Python packages in
22
- local hooks, and updates them directly in the file.
15
+ def update_additional_dependencies(file_path: Path, *, dry_run: bool = False) -> bool:
16
+ """Reads the pre-commit configuration file, identifies outdated packages
17
+ in local hooks, and updates them directly in the file.
23
18
 
24
19
  Preserves structure, quote styles, the document start marker (---),
25
20
  and comments preceding it.
26
21
 
27
22
  Args:
28
- file_path: Path to the YAML configuration file.
23
+ file_path: Path to the pre-commit configuration file.
29
24
 
30
25
  Returns:
31
26
  True if the file was modified and successfully written, False otherwise.
32
27
 
33
28
  """
34
- logging.debug("Reading configuration file: %s", file_path)
35
-
36
- raw_lines = []
37
- header_lines = []
38
- yaml_start_index = 0
39
29
 
40
- try:
41
- with file_path.open("r", encoding="utf-8") as f:
42
- raw_lines = f.readlines()
43
- except FileNotFoundError:
44
- logging.exception("File not found: %s", file_path)
45
- return False
46
- except OSError as exc:
47
- logging.exception("IOError while reading file %s: %s", file_path, exc)
48
- return False
49
-
50
- logging.debug("Parsing document header...")
51
- yaml_start_index = 0
52
- for i, line in enumerate(raw_lines):
53
- stripped = line.strip()
54
- if stripped == "---":
55
- logging.debug("YAML marker found.")
56
- yaml_start_index = i + 1
57
- break
58
- if stripped and not stripped.startswith("#"):
59
- yaml_start_index = i
60
- break
61
-
62
- header_lines = raw_lines[:yaml_start_index]
30
+ header_lines, yaml_content = load_config_file(file_path)
63
31
 
64
32
  logging.debug("Parsing YAML content...")
65
- yaml_content = "".join(raw_lines[yaml_start_index:])
66
33
 
67
34
  yaml = ruamel.yaml.YAML()
68
35
  yaml.preserve_quotes = True
@@ -76,13 +43,13 @@ def update_additional_dependencies(file_path: Path) -> bool:
76
43
  config = yaml.load(yaml_content)
77
44
  except Exception as exc:
78
45
  logging.exception("Failed to parse YAML content from %s: %s", file_path, exc)
79
- return False
46
+ sys.exit(2)
80
47
 
81
48
  if config is None:
82
49
  logging.warning("Configuration file is empty or could not be parsed.")
83
- return False
50
+ sys.exit(2)
84
51
 
85
- file_modified = False
52
+ update_required = False
86
53
 
87
54
  if "repos" in config:
88
55
  logging.debug("Scanning repositories for local hooks")
@@ -95,23 +62,21 @@ def update_additional_dependencies(file_path: Path) -> bool:
95
62
  hook_id = hook.get("id")
96
63
  logging.debug("Processing hook with ID: %s", hook_id)
97
64
 
98
- if hook.get("language") not in LANGUAGE_PACKAGE_MAP:
65
+ if hook.get("language") not in SUPPORTED_PACKAGES:
99
66
  continue
100
67
 
101
- PackageClass = LANGUAGE_PACKAGE_MAP[hook.get("language")]
102
-
103
68
  if "additional_dependencies" not in hook:
104
69
  continue
105
70
 
106
71
  deps_list = list(hook["additional_dependencies"])
107
72
 
73
+ Package = SUPPORTED_PACKAGES[hook.get("language")]
74
+
108
75
  for i, dep_spec in enumerate(deps_list):
109
76
  dep_str = str(dep_spec)
110
77
 
111
78
  logging.debug("Checking dependency: %s", dep_str)
112
- package: PackageBase | None = PackageClass.from_specification(
113
- dep_str
114
- )
79
+ package = Package.from_specification(dep_str)
115
80
  if not package:
116
81
  continue
117
82
 
@@ -123,7 +88,7 @@ def update_additional_dependencies(file_path: Path) -> bool:
123
88
  != package.version_specification()
124
89
  ):
125
90
  logging.info(
126
- "Updating %s to %s",
91
+ "%s needs updating to %s",
127
92
  package.version_specification(),
128
93
  package.latest_version_specification(),
129
94
  )
@@ -131,7 +96,7 @@ def update_additional_dependencies(file_path: Path) -> bool:
131
96
  i
132
97
  ] = package.latest_version_specification()
133
98
 
134
- file_modified = True
99
+ update_required = True
135
100
  else:
136
101
  logging.debug(
137
102
  "%s is already correctly defined and up to date (%s).",
@@ -139,20 +104,25 @@ def update_additional_dependencies(file_path: Path) -> bool:
139
104
  package.version_specification(),
140
105
  )
141
106
 
142
- if file_modified:
143
- logging.debug("Writing modified content to disk")
144
- try:
145
- stream = io.StringIO()
146
- yaml.dump(config, stream)
147
- modified_body = stream.getvalue()
148
- final_content = "".join(header_lines) + modified_body
149
-
150
- with file_path.open("w", encoding="utf-8") as f:
151
- f.write(final_content)
152
- logging.info("Successfully updated %s", file_path)
153
- return True
154
- except OSError as exc:
155
- logging.exception("IOError while writing to file %s: %s", file_path, exc)
156
- return False
107
+ if update_required:
108
+ if dry_run:
109
+ sys.exit(1)
110
+ else:
111
+ logging.debug("Writing modifications to disk")
112
+ try:
113
+ stream = io.StringIO()
114
+ yaml.dump(config, stream)
115
+ modified_body = stream.getvalue()
116
+ final_content = "".join(header_lines) + modified_body
117
+
118
+ with file_path.open("w", encoding="utf-8") as f:
119
+ f.write(final_content)
120
+ logging.info("Successfully updated %s", file_path)
121
+ return True
122
+ except OSError as exc:
123
+ logging.exception(
124
+ "IOError while writing to file %s: %s", file_path, exc
125
+ )
126
+ return False
157
127
 
158
128
  return False
@@ -23,11 +23,12 @@ dependencies = [
23
23
  "requests>=2.32.5",
24
24
  "ruamel-yaml>=0.19.1",
25
25
  ]
26
- description = "A CLI tool to automatically update `additional_dependencies` within local Python hooks in `pre-commit-config.yml` files."
26
+ description = "A CLI tool to automatically update additional dependencies within local Python, Julia, Rust, and Node.js hooks in pre-commit config files."
27
+ dynamic = []
27
28
  name = "pre-commit-localupdate"
28
29
  readme = "README.md"
29
- requires-python = "<3.15,>=3.10"
30
- version = "0.1.0"
30
+ requires-python = "<3.15,>=3.11"
31
+ version = "0.3.0"
31
32
 
32
33
  [project.scripts]
33
34
  pre-commit-localupdate = "pre_commit_localupdate.__main__:main"
@@ -35,6 +36,11 @@ pre-commit-localupdate = "pre_commit_localupdate.__main__:main"
35
36
  [project.urls]
36
37
  source = "https://gitlab.mpcdf.mpg.de/tbz/pre-commit-localupdate.git"
37
38
 
39
+ [tool.pdm.version]
40
+ source = "scm"
41
+ write_template = "__version__ = '{}'"
42
+ write_to = "pre_commit_localupdate/__init__.py"
43
+
38
44
  [tool.tomlsort]
39
45
  sort_table_keys = true
40
46
  trailing_comma_inline_array = true