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.
- pre_commit_localupdate-0.1.0/README.md → pre_commit_localupdate-0.3.0/PKG-INFO +47 -6
- pre_commit_localupdate-0.1.0/PKG-INFO → pre_commit_localupdate-0.3.0/README.md +32 -21
- pre_commit_localupdate-0.3.0/pre_commit_localupdate/__init__.py +1 -0
- {pre_commit_localupdate-0.1.0 → pre_commit_localupdate-0.3.0}/pre_commit_localupdate/__main__.py +1 -1
- {pre_commit_localupdate-0.1.0 → pre_commit_localupdate-0.3.0}/pre_commit_localupdate/cli.py +11 -2
- pre_commit_localupdate-0.3.0/pre_commit_localupdate/io.py +43 -0
- pre_commit_localupdate-0.3.0/pre_commit_localupdate/packages/__init__.py +15 -0
- pre_commit_localupdate-0.3.0/pre_commit_localupdate/packages/julia.py +99 -0
- {pre_commit_localupdate-0.1.0/pre_commit_localupdate → pre_commit_localupdate-0.3.0/pre_commit_localupdate/packages}/node.py +1 -1
- {pre_commit_localupdate-0.1.0/pre_commit_localupdate → pre_commit_localupdate-0.3.0/pre_commit_localupdate/packages}/python.py +1 -1
- pre_commit_localupdate-0.3.0/pre_commit_localupdate/packages/rust.py +103 -0
- {pre_commit_localupdate-0.1.0 → pre_commit_localupdate-0.3.0}/pre_commit_localupdate/pre_commit_config.py +37 -67
- {pre_commit_localupdate-0.1.0 → pre_commit_localupdate-0.3.0}/pyproject.toml +9 -3
- pre_commit_localupdate-0.1.0/pre_commit_localupdate/__init__.py +0 -0
- {pre_commit_localupdate-0.1.0 → pre_commit_localupdate-0.3.0}/LICENSE +0 -0
- {pre_commit_localupdate-0.1.0 → pre_commit_localupdate-0.3.0}/pre_commit_localupdate/logs.py +0 -0
- {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
|
-
```
|
|
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
|
-
```
|
|
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
|
-
```
|
|
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
|
|
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
|
|
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
|
-
```
|
|
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
|
-
```
|
|
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
|
-
```
|
|
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
|
|
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
|
|
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'
|
{pre_commit_localupdate-0.1.0 → pre_commit_localupdate-0.3.0}/pre_commit_localupdate/__main__.py
RENAMED
|
@@ -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="
|
|
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}"
|
|
@@ -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.
|
|
11
|
-
from pre_commit_localupdate.
|
|
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
|
-
|
|
21
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
50
|
+
sys.exit(2)
|
|
84
51
|
|
|
85
|
-
|
|
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
|
|
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
|
|
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
|
-
"
|
|
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
|
-
|
|
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
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
|
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.
|
|
30
|
-
version = "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
|
|
File without changes
|
|
File without changes
|
{pre_commit_localupdate-0.1.0 → pre_commit_localupdate-0.3.0}/pre_commit_localupdate/logs.py
RENAMED
|
File without changes
|