pre-commit-localupdate 0.3.1__tar.gz → 0.4.1__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.3.1 → pre_commit_localupdate-0.4.1}/PKG-INFO +18 -17
- {pre_commit_localupdate-0.3.1 → pre_commit_localupdate-0.4.1}/README.md +15 -15
- pre_commit_localupdate-0.4.1/pre_commit_localupdate/__init__.py +1 -0
- {pre_commit_localupdate-0.3.1 → pre_commit_localupdate-0.4.1}/pre_commit_localupdate/__main__.py +3 -1
- {pre_commit_localupdate-0.3.1 → pre_commit_localupdate-0.4.1}/pre_commit_localupdate/cli.py +15 -4
- {pre_commit_localupdate-0.3.1 → pre_commit_localupdate-0.4.1}/pre_commit_localupdate/io.py +11 -1
- {pre_commit_localupdate-0.3.1 → pre_commit_localupdate-0.4.1}/pre_commit_localupdate/logs.py +5 -1
- {pre_commit_localupdate-0.3.1 → pre_commit_localupdate-0.4.1}/pre_commit_localupdate/packages/__init__.py +2 -0
- pre_commit_localupdate-0.4.1/pre_commit_localupdate/packages/golang.py +126 -0
- {pre_commit_localupdate-0.3.1 → pre_commit_localupdate-0.4.1}/pre_commit_localupdate/packages/julia.py +26 -19
- {pre_commit_localupdate-0.3.1 → pre_commit_localupdate-0.4.1}/pre_commit_localupdate/packages/node.py +21 -18
- pre_commit_localupdate-0.4.1/pre_commit_localupdate/packages/package.py +55 -0
- pre_commit_localupdate-0.4.1/pre_commit_localupdate/packages/python.py +95 -0
- {pre_commit_localupdate-0.3.1 → pre_commit_localupdate-0.4.1}/pre_commit_localupdate/packages/rust.py +27 -28
- {pre_commit_localupdate-0.3.1 → pre_commit_localupdate-0.4.1}/pre_commit_localupdate/pre_commit_config.py +13 -5
- {pre_commit_localupdate-0.3.1 → pre_commit_localupdate-0.4.1}/pyproject.toml +3 -2
- pre_commit_localupdate-0.3.1/pre_commit_localupdate/__init__.py +0 -1
- pre_commit_localupdate-0.3.1/pre_commit_localupdate/packages/package.py +0 -41
- pre_commit_localupdate-0.3.1/pre_commit_localupdate/packages/python.py +0 -86
- {pre_commit_localupdate-0.3.1 → pre_commit_localupdate-0.4.1}/LICENSE +0 -0
- {pre_commit_localupdate-0.3.1 → pre_commit_localupdate-0.4.1}/pre_commit_localupdate/error.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: pre-commit-localupdate
|
|
3
|
-
Version: 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.
|
|
3
|
+
Version: 0.4.1
|
|
4
|
+
Summary: A CLI tool to automatically update additional dependencies within local Python, Julia, Rust, Go, and Node.js hooks in pre-commit config files.
|
|
5
5
|
Author: M. Farzalipour Tabriz
|
|
6
6
|
Classifier: Development Status :: 3 - Alpha
|
|
7
7
|
Classifier: Environment :: Console
|
|
@@ -11,11 +11,12 @@ Project-URL: source, https://gitlab.mpcdf.mpg.de/tbz/pre-commit-localupdate.git
|
|
|
11
11
|
Requires-Python: <3.15,>=3.11
|
|
12
12
|
Requires-Dist: requests>=2.32.5
|
|
13
13
|
Requires-Dist: ruamel-yaml>=0.19.1
|
|
14
|
+
Requires-Dist: packaging>=26.0
|
|
14
15
|
Description-Content-Type: text/markdown
|
|
15
16
|
|
|
16
17
|
# pre-commit-localupdate
|
|
17
18
|
|
|
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. You can also pin a specific package to an older version by adding a `# freeze` comment.
|
|
19
|
+
A CLI tool to automatically update dependencies in `pre-commit-config.yml` files. It specifically targets `additional_dependencies` within local Python, Julia, Rust, Go, 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. You can also pin a specific package to an older version by adding a `# freeze` comment.
|
|
19
20
|
|
|
20
21
|
## Installation
|
|
21
22
|
|
|
@@ -31,22 +32,22 @@ To check and update the `additional_dependencies` in your `.pre-commit-config.ya
|
|
|
31
32
|
pre-commit-localupdate
|
|
32
33
|
```
|
|
33
34
|
|
|
34
|
-
|
|
35
|
+
All options:
|
|
35
36
|
|
|
36
37
|
```shell
|
|
37
|
-
pre-commit-localupdate --
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
38
|
+
usage: pre-commit-localupdate [-h] [--debug] [--dry-run] [-c CONFIG] [--timeout TIMEOUT] [--version]
|
|
39
|
+
|
|
40
|
+
Automatically update additional dependencies within local Python, Julia, Rust, Go, and Node.js hooks in a pre-commit config
|
|
41
|
+
file.
|
|
42
|
+
|
|
43
|
+
options:
|
|
44
|
+
-h, --help show this help message and exit
|
|
45
|
+
--debug enable debug logging (default: False)
|
|
46
|
+
--dry-run dry run mode. Do not update the file and exit with a non-zero code if the configuration file require an
|
|
47
|
+
update. (default: False)
|
|
48
|
+
-c, --config CONFIG pre-commit config file path (default: .pre-commit-config.yaml)
|
|
49
|
+
--timeout TIMEOUT connection timeout in seconds (default: 10)
|
|
50
|
+
--version show program's version number and exit
|
|
50
51
|
```
|
|
51
52
|
|
|
52
53
|
## Example
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# pre-commit-localupdate
|
|
2
2
|
|
|
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. You can also pin a specific package to an older version by adding a `# freeze` comment.
|
|
3
|
+
A CLI tool to automatically update dependencies in `pre-commit-config.yml` files. It specifically targets `additional_dependencies` within local Python, Julia, Rust, Go, 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. You can also pin a specific package to an older version by adding a `# freeze` comment.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -16,22 +16,22 @@ To check and update the `additional_dependencies` in your `.pre-commit-config.ya
|
|
|
16
16
|
pre-commit-localupdate
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
All options:
|
|
20
20
|
|
|
21
21
|
```shell
|
|
22
|
-
pre-commit-localupdate --
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
22
|
+
usage: pre-commit-localupdate [-h] [--debug] [--dry-run] [-c CONFIG] [--timeout TIMEOUT] [--version]
|
|
23
|
+
|
|
24
|
+
Automatically update additional dependencies within local Python, Julia, Rust, Go, and Node.js hooks in a pre-commit config
|
|
25
|
+
file.
|
|
26
|
+
|
|
27
|
+
options:
|
|
28
|
+
-h, --help show this help message and exit
|
|
29
|
+
--debug enable debug logging (default: False)
|
|
30
|
+
--dry-run dry run mode. Do not update the file and exit with a non-zero code if the configuration file require an
|
|
31
|
+
update. (default: False)
|
|
32
|
+
-c, --config CONFIG pre-commit config file path (default: .pre-commit-config.yaml)
|
|
33
|
+
--timeout TIMEOUT connection timeout in seconds (default: 10)
|
|
34
|
+
--version show program's version number and exit
|
|
35
35
|
```
|
|
36
36
|
|
|
37
37
|
## Example
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = '0.4.1'
|
{pre_commit_localupdate-0.3.1 → pre_commit_localupdate-0.4.1}/pre_commit_localupdate/__main__.py
RENAMED
|
@@ -25,7 +25,9 @@ def main() -> None:
|
|
|
25
25
|
setup_logging(debug=args.debug)
|
|
26
26
|
|
|
27
27
|
try:
|
|
28
|
-
if update_additional_dependencies(
|
|
28
|
+
if update_additional_dependencies(
|
|
29
|
+
Path(args.config), args.timeout, dry_run=args.dry_run
|
|
30
|
+
):
|
|
29
31
|
if args.dry_run:
|
|
30
32
|
sys.exit(1)
|
|
31
33
|
logging.info("Done.")
|
|
@@ -7,19 +7,24 @@ from pre_commit_localupdate import __version__
|
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
def parse_args() -> argparse.Namespace:
|
|
10
|
-
"""Parse command line arguments.
|
|
10
|
+
"""Parse command line arguments.
|
|
11
|
+
|
|
12
|
+
Returns:
|
|
13
|
+
argparse.Namespace: Parsed arguments.
|
|
14
|
+
"""
|
|
11
15
|
parser = argparse.ArgumentParser(
|
|
12
|
-
description="Automatically update additional dependencies within local Python, Julia, Rust, and Node.js hooks in a pre-commit config file.",
|
|
16
|
+
description="Automatically update additional dependencies within local Python, Julia, Rust, Go, and Node.js hooks in a pre-commit config file.",
|
|
17
|
+
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
13
18
|
)
|
|
14
19
|
parser.add_argument(
|
|
15
20
|
"--debug",
|
|
16
21
|
action="store_true",
|
|
17
|
-
help="
|
|
22
|
+
help="enable debug logging",
|
|
18
23
|
)
|
|
19
24
|
parser.add_argument(
|
|
20
25
|
"--dry-run",
|
|
21
26
|
action="store_true",
|
|
22
|
-
help="
|
|
27
|
+
help="dry run mode. Do not update the file and exit with a non-zero code if the configuration file require an update.",
|
|
23
28
|
)
|
|
24
29
|
parser.add_argument(
|
|
25
30
|
"-c",
|
|
@@ -28,6 +33,12 @@ def parse_args() -> argparse.Namespace:
|
|
|
28
33
|
help="pre-commit config file path",
|
|
29
34
|
default=".pre-commit-config.yaml",
|
|
30
35
|
)
|
|
36
|
+
parser.add_argument(
|
|
37
|
+
"--timeout",
|
|
38
|
+
type=int,
|
|
39
|
+
help="connection timeout in seconds",
|
|
40
|
+
default=10,
|
|
41
|
+
)
|
|
31
42
|
parser.add_argument(
|
|
32
43
|
"--version",
|
|
33
44
|
action="version",
|
|
@@ -8,7 +8,17 @@ from pre_commit_localupdate.error import PreCommitLocalUpdateError
|
|
|
8
8
|
|
|
9
9
|
|
|
10
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.
|
|
11
|
+
"""Reads pre-commit configuration file and returns its header lines and content.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
file_path (Path): Path to config file
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
tuple[list[str], str]: Header lines, config file content
|
|
18
|
+
|
|
19
|
+
Raises:
|
|
20
|
+
PreCommitLocalUpdateError: Error in reading config file
|
|
21
|
+
"""
|
|
12
22
|
logging.debug("Reading configuration file: %s", file_path)
|
|
13
23
|
|
|
14
24
|
raw_lines: list[str] = []
|
{pre_commit_localupdate-0.3.1 → pre_commit_localupdate-0.4.1}/pre_commit_localupdate/logs.py
RENAMED
|
@@ -6,7 +6,11 @@ import sys
|
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
def setup_logging(*, debug: bool = False) -> None:
|
|
9
|
-
"""Configure logging based on the debug flag.
|
|
9
|
+
"""Configure logging based on the debug flag.
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
debug (bool): activate debug logs
|
|
13
|
+
"""
|
|
10
14
|
level = logging.DEBUG if debug else logging.INFO
|
|
11
15
|
logging.basicConfig(
|
|
12
16
|
level=level,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# SPDX-FileCopyrightText: 2026 M. Farzalipour Tabriz, Max Planck Institute for Physics
|
|
2
2
|
# SPDX-License-Identifier: LGPL-3.0-or-later
|
|
3
3
|
|
|
4
|
+
from .golang import GoPackage
|
|
4
5
|
from .julia import JuliaPackage
|
|
5
6
|
from .node import NodePackage
|
|
6
7
|
from .package import PackageBase
|
|
@@ -8,6 +9,7 @@ from .python import PyPackage
|
|
|
8
9
|
from .rust import RustPackage
|
|
9
10
|
|
|
10
11
|
SUPPORTED_PACKAGES: dict[str, type[PackageBase]] = {
|
|
12
|
+
"golang": GoPackage,
|
|
11
13
|
"julia": JuliaPackage,
|
|
12
14
|
"node": NodePackage,
|
|
13
15
|
"python": PyPackage,
|
|
@@ -0,0 +1,126 @@
|
|
|
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 dataclasses import dataclass
|
|
7
|
+
|
|
8
|
+
import requests
|
|
9
|
+
|
|
10
|
+
from .package import PackageBase
|
|
11
|
+
|
|
12
|
+
# Go Proxy API endpoints
|
|
13
|
+
GO_PKG_LATEST_VERSION_API_URL = "https://proxy.golang.org/{package_path}/@latest"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class GoPackage(PackageBase):
|
|
18
|
+
"""Represents a Go module."""
|
|
19
|
+
|
|
20
|
+
@classmethod
|
|
21
|
+
def from_specification(
|
|
22
|
+
cls, spec: str, connection_timeout: int
|
|
23
|
+
) -> "GoPackage" | None:
|
|
24
|
+
"""Parse a Go module specification string into a Package object.
|
|
25
|
+
|
|
26
|
+
Handles formats like:
|
|
27
|
+
- 'mvdan.cc/sh' (Module path only)
|
|
28
|
+
- 'mvdan.cc/sh@v1.3.0' (Module path with specific version)
|
|
29
|
+
- 'mvdan.cc/sh/v3' (Module path with major version suffix)
|
|
30
|
+
- 'mvdan.cc/sh/v3@v3.10.0' (Major suffix with version)
|
|
31
|
+
- 'mvdan.cc/sh/v3/cmd/shfmt' (Submodule)
|
|
32
|
+
- 'mvdan.cc/sh/v3/cmd/shfmt@v3.10.0' (Submodule with version)
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
spec (str): The package specification string.
|
|
36
|
+
connection_timeout (int): Connection timeout in seconds.
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
"GoPackage" | None: A Package instance if parsing succeeds, otherwise None.
|
|
40
|
+
"""
|
|
41
|
+
clean_spec = spec.strip().strip('"').strip("'")
|
|
42
|
+
|
|
43
|
+
match = re.match(r"^([a-zA-Z0-9._/-]+)(@.+)?$", clean_spec)
|
|
44
|
+
|
|
45
|
+
if not match:
|
|
46
|
+
logging.warning(
|
|
47
|
+
"Could not parse the Go package specification: %s", clean_spec
|
|
48
|
+
)
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
name = match.group(1)
|
|
52
|
+
raw_spec = match.group(2)
|
|
53
|
+
|
|
54
|
+
module_path = cls._resolve_module_path(name)
|
|
55
|
+
latest_version = cls._get_latest_version(module_path, connection_timeout)
|
|
56
|
+
|
|
57
|
+
return cls(
|
|
58
|
+
name=name,
|
|
59
|
+
raw_spec=raw_spec,
|
|
60
|
+
latest_version=latest_version,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
@staticmethod
|
|
64
|
+
def _resolve_module_path(package_path: str) -> str:
|
|
65
|
+
"""
|
|
66
|
+
Resolve a submodule path to a module path.
|
|
67
|
+
|
|
68
|
+
If the path contains a major version suffix (e.g., /v2, /v3) followed by
|
|
69
|
+
additional path segments, it trims the path at the suffix.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
package_path (str): The full submodule path.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
str: The resolved module path.
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
match = re.search(r"/v([2-9]|[1-9][0-9]+)(?=/)", package_path)
|
|
79
|
+
|
|
80
|
+
if match:
|
|
81
|
+
return package_path[: match.end()]
|
|
82
|
+
|
|
83
|
+
return package_path
|
|
84
|
+
|
|
85
|
+
@staticmethod
|
|
86
|
+
def _get_latest_version(package_path: str, connection_timeout: int) -> str | None:
|
|
87
|
+
"""Fetch the latest version of a Go module from proxy.golang.org.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
package_path (str): The module path (e.g., mvdan.cc/sh/v3).
|
|
91
|
+
connection_timeout (int): Connection timeout in seconds.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
str | None: The latest version string (e.g., v3.10.0) if found, otherwise None.
|
|
95
|
+
"""
|
|
96
|
+
logging.debug("Fetching latest version for %s", package_path)
|
|
97
|
+
try:
|
|
98
|
+
url = GO_PKG_LATEST_VERSION_API_URL.format(package_path=package_path)
|
|
99
|
+
|
|
100
|
+
response = requests.get(url, timeout=connection_timeout)
|
|
101
|
+
response.raise_for_status()
|
|
102
|
+
|
|
103
|
+
data = response.json()
|
|
104
|
+
latest_version = data.get("Version")
|
|
105
|
+
|
|
106
|
+
return latest_version if isinstance(latest_version, str) else None
|
|
107
|
+
|
|
108
|
+
except requests.exceptions.RequestException as exc:
|
|
109
|
+
logging.warning(
|
|
110
|
+
"Could not fetch latest version for %s: %s", package_path, exc
|
|
111
|
+
)
|
|
112
|
+
return None
|
|
113
|
+
except (KeyError, ValueError) as exc:
|
|
114
|
+
logging.warning("Could not parse response for %s: %s", package_path, exc)
|
|
115
|
+
return None
|
|
116
|
+
|
|
117
|
+
def latest_version_specification(self) -> str | None:
|
|
118
|
+
"""Version specification of package pinned to the latest version.
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
str | None: The latest version specification string if found, otherwise None.
|
|
122
|
+
"""
|
|
123
|
+
if not self.latest_version:
|
|
124
|
+
return None
|
|
125
|
+
|
|
126
|
+
return f"{self.name}@{self.latest_version}"
|
|
@@ -4,30 +4,35 @@
|
|
|
4
4
|
import logging
|
|
5
5
|
import re
|
|
6
6
|
import tomllib
|
|
7
|
+
from dataclasses import dataclass
|
|
7
8
|
|
|
8
9
|
import requests
|
|
10
|
+
from packaging.version import InvalidVersion, Version
|
|
9
11
|
|
|
10
12
|
from .package import PackageBase
|
|
11
13
|
|
|
12
14
|
# Julia General Registry Source
|
|
13
15
|
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
|
|
|
16
17
|
|
|
18
|
+
@dataclass
|
|
17
19
|
class JuliaPackage(PackageBase):
|
|
18
20
|
"""Represents a Julia package."""
|
|
19
21
|
|
|
20
22
|
@classmethod
|
|
21
|
-
def from_specification(
|
|
23
|
+
def from_specification(
|
|
24
|
+
cls, spec: str, connection_timeout: int
|
|
25
|
+
) -> "JuliaPackage" | None:
|
|
22
26
|
"""Parse a package specification string into a Package object.
|
|
23
27
|
|
|
24
28
|
Handles formats like 'Example@0.4.1', 'Example@0.4', or just 'Example'.
|
|
25
29
|
|
|
26
30
|
Args:
|
|
27
|
-
spec: The package specification string.
|
|
31
|
+
spec (str): The package specification string.
|
|
32
|
+
connection_timeout (int): Connection timeout in seconds.
|
|
28
33
|
|
|
29
34
|
Returns:
|
|
30
|
-
A Package instance if parsing succeeds, otherwise None.
|
|
35
|
+
"JuliaPackage" | None: A Package instance if parsing succeeds, otherwise None.
|
|
31
36
|
|
|
32
37
|
"""
|
|
33
38
|
clean_spec = spec.strip().strip('"').strip("'")
|
|
@@ -39,20 +44,24 @@ class JuliaPackage(PackageBase):
|
|
|
39
44
|
|
|
40
45
|
name = match.group(1)
|
|
41
46
|
raw_spec = match.group(2)
|
|
42
|
-
latest_version =
|
|
47
|
+
latest_version = cls._get_latest_version(name, connection_timeout)
|
|
43
48
|
|
|
44
|
-
return
|
|
49
|
+
return cls(
|
|
45
50
|
name=name,
|
|
46
51
|
raw_spec=raw_spec,
|
|
47
52
|
latest_version=latest_version,
|
|
48
53
|
)
|
|
49
54
|
|
|
50
55
|
@staticmethod
|
|
51
|
-
def _get_latest_version(package_name: str) -> str | None:
|
|
56
|
+
def _get_latest_version(package_name: str, connection_timeout: int) -> str | None:
|
|
52
57
|
"""Fetch the latest version of a package from the Julia General Registry.
|
|
53
58
|
|
|
59
|
+
Args:
|
|
60
|
+
package_name (str): Name of the package.
|
|
61
|
+
connection_timeout (int): Connection timeout in seconds.
|
|
62
|
+
|
|
54
63
|
Returns:
|
|
55
|
-
The latest version string if found, otherwise None.
|
|
64
|
+
str | None: The latest version string if found, otherwise None.
|
|
56
65
|
|
|
57
66
|
"""
|
|
58
67
|
logging.debug("Fetching latest version for %s", package_name)
|
|
@@ -60,7 +69,7 @@ class JuliaPackage(PackageBase):
|
|
|
60
69
|
url = JULIA_PKG_API_URL.format(
|
|
61
70
|
package_name=package_name, package_name_first_letter=package_name[0]
|
|
62
71
|
)
|
|
63
|
-
response = requests.get(url, timeout=
|
|
72
|
+
response = requests.get(url, timeout=connection_timeout)
|
|
64
73
|
response.raise_for_status()
|
|
65
74
|
|
|
66
75
|
data = tomllib.loads(response.text)
|
|
@@ -69,31 +78,29 @@ class JuliaPackage(PackageBase):
|
|
|
69
78
|
if not versions:
|
|
70
79
|
return None
|
|
71
80
|
|
|
72
|
-
versions.sort(reverse=True)
|
|
73
|
-
|
|
81
|
+
versions.sort(key=lambda v: Version(v), reverse=True)
|
|
82
|
+
latest_version = versions[0]
|
|
74
83
|
|
|
75
|
-
|
|
76
|
-
return latest
|
|
84
|
+
return latest_version if isinstance(latest_version, str) else None
|
|
77
85
|
|
|
78
|
-
return None
|
|
79
86
|
except requests.exceptions.RequestException as exc:
|
|
80
87
|
logging.warning(
|
|
81
88
|
"Could not fetch latest version for %s: %s", package_name, exc
|
|
82
89
|
)
|
|
83
90
|
return None
|
|
84
|
-
except (KeyError, ValueError, tomllib.TOMLDecodeError) as exc:
|
|
91
|
+
except (KeyError, ValueError, tomllib.TOMLDecodeError, InvalidVersion) as exc:
|
|
85
92
|
logging.warning("Could not parse response for %s: %s", package_name, exc)
|
|
86
93
|
return None
|
|
87
94
|
|
|
88
|
-
def latest_version_specification(
|
|
95
|
+
def latest_version_specification(self) -> str | None:
|
|
89
96
|
"""Version specification of package pinned to the latest version.
|
|
90
97
|
|
|
91
98
|
Returns:
|
|
92
|
-
The latest version specification string if found, otherwise None.
|
|
99
|
+
str | None: The latest version specification string if found, otherwise None.
|
|
93
100
|
Format: PackageName@version
|
|
94
101
|
|
|
95
102
|
"""
|
|
96
|
-
if not
|
|
103
|
+
if not self.latest_version:
|
|
97
104
|
return None
|
|
98
105
|
|
|
99
|
-
return f"{
|
|
106
|
+
return f"{self.name}@{self.latest_version}"
|
|
@@ -4,14 +4,12 @@
|
|
|
4
4
|
import logging
|
|
5
5
|
import re
|
|
6
6
|
from dataclasses import dataclass
|
|
7
|
-
from typing import cast
|
|
8
7
|
|
|
9
8
|
import requests
|
|
10
9
|
|
|
11
10
|
from .package import PackageBase
|
|
12
11
|
|
|
13
12
|
NPM_API_URL = "https://registry.npmjs.org/{package_name}"
|
|
14
|
-
REQUEST_TIMEOUT = 10
|
|
15
13
|
|
|
16
14
|
|
|
17
15
|
@dataclass
|
|
@@ -19,16 +17,19 @@ class NodePackage(PackageBase):
|
|
|
19
17
|
"""Represents a Node.js package."""
|
|
20
18
|
|
|
21
19
|
@classmethod
|
|
22
|
-
def from_specification(
|
|
20
|
+
def from_specification(
|
|
21
|
+
cls, spec: str, connection_timeout: int
|
|
22
|
+
) -> "NodePackage" | None:
|
|
23
23
|
"""Parse a package specification string into a NodePackage object.
|
|
24
24
|
|
|
25
25
|
Handles formats like 'react@4.17.21', 'react@>=18.0.0', '@scope/name@^1.2.3', or just 'react'.
|
|
26
26
|
|
|
27
27
|
Args:
|
|
28
|
-
spec: The package specification string.
|
|
28
|
+
spec (str): The package specification string.
|
|
29
|
+
connection_timeout (int): Connection timeout in seconds.
|
|
29
30
|
|
|
30
31
|
Returns:
|
|
31
|
-
A NodePackage instance if parsing succeeds, otherwise None.
|
|
32
|
+
"NodePackage" | None: A NodePackage instance if parsing succeeds, otherwise None.
|
|
32
33
|
|
|
33
34
|
"""
|
|
34
35
|
clean_spec = spec.strip().strip('"').strip("'")
|
|
@@ -48,34 +49,36 @@ class NodePackage(PackageBase):
|
|
|
48
49
|
raw_spec = "@" + "@".join(parts[i:])
|
|
49
50
|
break
|
|
50
51
|
|
|
51
|
-
latest_version =
|
|
52
|
+
latest_version = cls._get_latest_version(name, connection_timeout)
|
|
52
53
|
|
|
53
|
-
return
|
|
54
|
+
return cls(
|
|
54
55
|
name=name,
|
|
55
56
|
raw_spec=raw_spec,
|
|
56
57
|
latest_version=latest_version,
|
|
57
58
|
)
|
|
58
59
|
|
|
59
60
|
@staticmethod
|
|
60
|
-
def _get_latest_version(package_name: str) -> str | None:
|
|
61
|
+
def _get_latest_version(package_name: str, connection_timeout: int) -> str | None:
|
|
61
62
|
"""Fetch the latest version of a package from the npm registry.
|
|
62
63
|
|
|
64
|
+
Args:
|
|
65
|
+
package_name (str): Name of the package.
|
|
66
|
+
connection_timeout (int): Connection timeout in seconds.
|
|
67
|
+
|
|
63
68
|
Returns:
|
|
64
|
-
The latest version string if found, otherwise None.
|
|
69
|
+
str | None: The latest version string if found, otherwise None.
|
|
65
70
|
|
|
66
71
|
"""
|
|
67
72
|
try:
|
|
68
73
|
url = NPM_API_URL.format(package_name=package_name)
|
|
69
|
-
response = requests.get(url, timeout=
|
|
74
|
+
response = requests.get(url, timeout=connection_timeout)
|
|
70
75
|
response.raise_for_status()
|
|
71
76
|
data = response.json()
|
|
72
77
|
|
|
73
|
-
|
|
78
|
+
latest_version = data.get("dist-tags", {}).get("latest")
|
|
74
79
|
|
|
75
|
-
|
|
76
|
-
return cast("str", version)
|
|
80
|
+
return latest_version if isinstance(latest_version, str) else None
|
|
77
81
|
|
|
78
|
-
return None
|
|
79
82
|
except requests.exceptions.RequestException as exc:
|
|
80
83
|
logging.warning(
|
|
81
84
|
"Could not fetch latest version for %s: %s", package_name, exc
|
|
@@ -85,14 +88,14 @@ class NodePackage(PackageBase):
|
|
|
85
88
|
logging.warning("Could not parse response for %s: %s", package_name, exc)
|
|
86
89
|
return None
|
|
87
90
|
|
|
88
|
-
def latest_version_specification(
|
|
91
|
+
def latest_version_specification(self) -> str | None:
|
|
89
92
|
"""Version specification of package pinned to the latest version.
|
|
90
93
|
|
|
91
94
|
Returns:
|
|
92
|
-
The latest version specification string if found, otherwise None.
|
|
95
|
+
str | None: The latest version specification string if found, otherwise None.
|
|
93
96
|
|
|
94
97
|
"""
|
|
95
|
-
if not
|
|
98
|
+
if not self.latest_version:
|
|
96
99
|
return None
|
|
97
100
|
|
|
98
|
-
return f"{
|
|
101
|
+
return f"{self.name}@{self.latest_version}"
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 M. Farzalipour Tabriz, Max Planck Institute for Physics
|
|
2
|
+
# SPDX-License-Identifier: LGPL-3.0-or-later
|
|
3
|
+
|
|
4
|
+
from abc import ABC, abstractmethod
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class PackageBase(ABC):
|
|
10
|
+
"""Abstract base class defining the interface for all package types.
|
|
11
|
+
|
|
12
|
+
Attributes:
|
|
13
|
+
name (str): The name of the package (e.g., 'requests').
|
|
14
|
+
raw_spec (str | None): The original version specifier string (e.g., '==2.25.1', '^4.17.21', '>=1.0.0', '@1.2.3').
|
|
15
|
+
latest_version (str | None): Latest version number without operators (e.g., '2.25.1').
|
|
16
|
+
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
name: str
|
|
20
|
+
raw_spec: str | None
|
|
21
|
+
latest_version: str | None
|
|
22
|
+
|
|
23
|
+
@classmethod
|
|
24
|
+
@abstractmethod
|
|
25
|
+
def from_specification(
|
|
26
|
+
cls, spec_string: str, connection_timeout: int
|
|
27
|
+
) -> "PackageBase" | None:
|
|
28
|
+
"""Parse a package specification string into a Package object.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
spec_string (str): The package specification string.
|
|
32
|
+
connection_timeout (int): Connection timeout in seconds.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
"PackageBase" | None: A Package instance if parsing succeeds, otherwise None.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
@abstractmethod
|
|
39
|
+
def latest_version_specification(self) -> str | None:
|
|
40
|
+
"""Version specification of package pinned to the latest version.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
str | None: The latest version specification string if found, otherwise None.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def version_specification(self) -> str | None:
|
|
47
|
+
"""Version specification of package as originally defined.
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
str | None: The version specification string.
|
|
51
|
+
|
|
52
|
+
"""
|
|
53
|
+
if self.raw_spec:
|
|
54
|
+
return f"{self.name}{self.raw_spec}"
|
|
55
|
+
return f"{self.name}[unspecified version]"
|
|
@@ -0,0 +1,95 @@
|
|
|
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
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
import requests
|
|
8
|
+
from packaging.requirements import Requirement
|
|
9
|
+
|
|
10
|
+
from .package import PackageBase
|
|
11
|
+
|
|
12
|
+
PYPI_API_URL = "https://pypi.org/pypi/{package_name}/json"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class PyPackage(PackageBase):
|
|
17
|
+
"""Represents a Python package."""
|
|
18
|
+
|
|
19
|
+
@classmethod
|
|
20
|
+
def from_specification(
|
|
21
|
+
cls, spec: str, connection_timeout: int
|
|
22
|
+
) -> "PyPackage" | None:
|
|
23
|
+
"""Parse a package specification string into a Package object.
|
|
24
|
+
|
|
25
|
+
Handles formats like 'ansible==13.3.0', 'ansible>=1.0', or just 'ansible'.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
spec (str): The package specification string.
|
|
29
|
+
connection_timeout (int): Connection timeout in seconds.
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
"PyPackage" | None: A Package instance if parsing succeeds, otherwise None.
|
|
33
|
+
|
|
34
|
+
"""
|
|
35
|
+
clean_spec = spec.strip().strip('"').strip("'")
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
requirement = Requirement(clean_spec)
|
|
39
|
+
except Exception as exc:
|
|
40
|
+
logging.warning(
|
|
41
|
+
"Could not parse the package specification: %s - %s", clean_spec, exc
|
|
42
|
+
)
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
name = requirement.name
|
|
46
|
+
raw_spec = str(requirement.specifier) if requirement.specifier else None
|
|
47
|
+
latest_version = cls._get_latest_version(name, connection_timeout)
|
|
48
|
+
|
|
49
|
+
return cls(
|
|
50
|
+
name=name,
|
|
51
|
+
raw_spec=raw_spec,
|
|
52
|
+
latest_version=latest_version,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
@staticmethod
|
|
56
|
+
def _get_latest_version(package_name: str, connection_timeout: int) -> str | None:
|
|
57
|
+
"""Fetch the latest version of a package from PyPI.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
package_name (str): Name of the package.
|
|
61
|
+
connection_timeout (int): Connection timeout in seconds.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
str | None: The latest version string if found, otherwise None.
|
|
65
|
+
|
|
66
|
+
"""
|
|
67
|
+
logging.debug("Fetching latest version for %s", package_name)
|
|
68
|
+
try:
|
|
69
|
+
url = PYPI_API_URL.format(package_name=package_name)
|
|
70
|
+
response = requests.get(url, timeout=connection_timeout)
|
|
71
|
+
response.raise_for_status()
|
|
72
|
+
data = response.json()
|
|
73
|
+
latest_version = data.get("info", {}).get("version")
|
|
74
|
+
return latest_version if isinstance(latest_version, str) else None
|
|
75
|
+
|
|
76
|
+
except requests.exceptions.RequestException as exc:
|
|
77
|
+
logging.warning(
|
|
78
|
+
"Could not fetch latest version for %s: %s", package_name, exc
|
|
79
|
+
)
|
|
80
|
+
return None
|
|
81
|
+
except (KeyError, ValueError) as exc:
|
|
82
|
+
logging.warning("Could not parse response for %s: %s", package_name, exc)
|
|
83
|
+
return None
|
|
84
|
+
|
|
85
|
+
def latest_version_specification(self) -> str | None:
|
|
86
|
+
"""Version specification of package pinned to the latest version.
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
str | None: The latest version specification string if found, otherwise None.
|
|
90
|
+
|
|
91
|
+
"""
|
|
92
|
+
if not self.latest_version:
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
return f"{self.name}=={self.latest_version}"
|
|
@@ -3,40 +3,39 @@
|
|
|
3
3
|
|
|
4
4
|
import logging
|
|
5
5
|
import re
|
|
6
|
-
from
|
|
6
|
+
from dataclasses import dataclass
|
|
7
7
|
|
|
8
8
|
import requests
|
|
9
9
|
|
|
10
10
|
from .package import PackageBase
|
|
11
11
|
|
|
12
12
|
CRATES_API_URL = "https://crates.io/api/v1/crates/{package_name}"
|
|
13
|
-
REQUEST_TIMEOUT = 10
|
|
14
13
|
|
|
15
14
|
|
|
15
|
+
@dataclass
|
|
16
16
|
class RustPackage(PackageBase):
|
|
17
17
|
"""Represents a Rust package.
|
|
18
18
|
|
|
19
19
|
Attributes:
|
|
20
|
-
cli: Package is a CLI tool.
|
|
20
|
+
cli (bool): Package is a CLI tool.
|
|
21
21
|
"""
|
|
22
22
|
|
|
23
23
|
cli: bool = False
|
|
24
24
|
|
|
25
|
-
def __init__(self, **kwargs: Any) -> None:
|
|
26
|
-
self.cli = kwargs.pop("cli", False)
|
|
27
|
-
super().__init__(**kwargs)
|
|
28
|
-
|
|
29
25
|
@classmethod
|
|
30
|
-
def from_specification(
|
|
26
|
+
def from_specification(
|
|
27
|
+
cls, spec: str, connection_timeout: int
|
|
28
|
+
) -> "RustPackage" | None:
|
|
31
29
|
"""Parse a package specification string into a Package object.
|
|
32
30
|
|
|
33
31
|
Handles formats like 'package', 'package:1.2.3', or 'cli:package:1.2.3'.
|
|
34
32
|
|
|
35
33
|
Args:
|
|
36
|
-
spec: The package specification string.
|
|
34
|
+
spec (str): The package specification string.
|
|
35
|
+
connection_timeout (int): Connection timeout in seconds.
|
|
37
36
|
|
|
38
37
|
Returns:
|
|
39
|
-
A Package instance if parsing succeeds, otherwise None.
|
|
38
|
+
"RustPackage" | None: A Package instance if parsing succeeds, otherwise None.
|
|
40
39
|
|
|
41
40
|
"""
|
|
42
41
|
clean_spec = spec.strip().strip('"').strip("'")
|
|
@@ -51,34 +50,34 @@ class RustPackage(PackageBase):
|
|
|
51
50
|
|
|
52
51
|
name = match.group("name")
|
|
53
52
|
version = match.group("version")
|
|
54
|
-
cli =
|
|
53
|
+
cli = bool(match.group("cli"))
|
|
55
54
|
|
|
56
55
|
raw_spec = f":{version}" if version else None
|
|
57
|
-
latest_version =
|
|
56
|
+
latest_version = cls._get_latest_version(name, connection_timeout)
|
|
58
57
|
|
|
59
|
-
return
|
|
60
|
-
name=name, raw_spec=raw_spec, latest_version=latest_version, cli=cli
|
|
61
|
-
)
|
|
58
|
+
return cls(name=name, raw_spec=raw_spec, latest_version=latest_version, cli=cli)
|
|
62
59
|
|
|
63
60
|
@staticmethod
|
|
64
|
-
def _get_latest_version(package_name: str) -> str | None:
|
|
61
|
+
def _get_latest_version(package_name: str, connection_timeout: int) -> str | None:
|
|
65
62
|
"""Fetch the latest version of a package from crates.io.
|
|
66
63
|
|
|
64
|
+
Args:
|
|
65
|
+
package_name (str): Name of the package.
|
|
66
|
+
connection_timeout (int): Connection timeout in seconds.
|
|
67
|
+
|
|
67
68
|
Returns:
|
|
68
|
-
The latest version string if found, otherwise None.
|
|
69
|
+
str | None: The latest version string if found, otherwise None.
|
|
69
70
|
|
|
70
71
|
"""
|
|
71
72
|
logging.debug("Fetching latest version for %s", package_name)
|
|
72
73
|
try:
|
|
73
74
|
url = CRATES_API_URL.format(package_name=package_name)
|
|
74
|
-
response = requests.get(url, timeout=
|
|
75
|
+
response = requests.get(url, timeout=connection_timeout)
|
|
75
76
|
response.raise_for_status()
|
|
76
77
|
data = response.json()
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
return cast("str", version)
|
|
78
|
+
latest_version = data.get("crate", {}).get("max_version")
|
|
79
|
+
return latest_version if isinstance(latest_version, str) else None
|
|
80
80
|
|
|
81
|
-
return None
|
|
82
81
|
except requests.exceptions.RequestException as exc:
|
|
83
82
|
logging.warning(
|
|
84
83
|
"Could not fetch latest version for %s: %s", package_name, exc
|
|
@@ -88,16 +87,16 @@ class RustPackage(PackageBase):
|
|
|
88
87
|
logging.warning("Could not parse response for %s: %s", package_name, exc)
|
|
89
88
|
return None
|
|
90
89
|
|
|
91
|
-
def latest_version_specification(
|
|
90
|
+
def latest_version_specification(self) -> str | None:
|
|
92
91
|
"""Version specification of package pinned to the latest version.
|
|
93
92
|
|
|
94
93
|
Returns:
|
|
95
|
-
The latest version specification string if found, otherwise None.
|
|
94
|
+
str | None: The latest version specification string if found, otherwise None.
|
|
96
95
|
|
|
97
96
|
"""
|
|
98
|
-
if not
|
|
97
|
+
if not self.latest_version:
|
|
99
98
|
return None
|
|
100
|
-
if
|
|
101
|
-
return f"cli:{
|
|
99
|
+
if self.cli:
|
|
100
|
+
return f"cli:{self.name}:{self.latest_version}"
|
|
102
101
|
else:
|
|
103
|
-
return f"{
|
|
102
|
+
return f"{self.name}:{self.latest_version}"
|
|
@@ -15,7 +15,9 @@ from pre_commit_localupdate.io import load_config_file
|
|
|
15
15
|
from pre_commit_localupdate.packages import SUPPORTED_PACKAGES
|
|
16
16
|
|
|
17
17
|
|
|
18
|
-
def update_additional_dependencies(
|
|
18
|
+
def update_additional_dependencies(
|
|
19
|
+
file_path: Path, connection_timeout: int, *, dry_run: bool = False
|
|
20
|
+
) -> bool:
|
|
19
21
|
"""Reads the pre-commit configuration file, identifies outdated packages
|
|
20
22
|
in local hooks, and updates them directly in the file.
|
|
21
23
|
|
|
@@ -23,11 +25,15 @@ def update_additional_dependencies(file_path: Path, *, dry_run: bool = False) ->
|
|
|
23
25
|
and comments preceding it.
|
|
24
26
|
|
|
25
27
|
Args:
|
|
26
|
-
file_path: Path to the pre-commit configuration file.
|
|
27
|
-
|
|
28
|
+
file_path (Path): Path to the pre-commit configuration file.
|
|
29
|
+
connection_timeout (int): Connection timeout in seconds.
|
|
30
|
+
dry_run (bool): Do not update the pre-commit configuration file.
|
|
28
31
|
|
|
29
32
|
Returns:
|
|
30
|
-
True if the file needed updating, False otherwise.
|
|
33
|
+
bool: True if the file needed updating, False otherwise.
|
|
34
|
+
|
|
35
|
+
Raises:
|
|
36
|
+
PreCommitLocalUpdateError: Error in parsing or updating pre-commit hooks
|
|
31
37
|
|
|
32
38
|
"""
|
|
33
39
|
header_lines, yaml_content = load_config_file(file_path)
|
|
@@ -88,7 +94,9 @@ def update_additional_dependencies(file_path: Path, *, dry_run: bool = False) ->
|
|
|
88
94
|
continue
|
|
89
95
|
|
|
90
96
|
logging.debug("Checking dependency: %s", dep_str)
|
|
91
|
-
package = package_type.from_specification(
|
|
97
|
+
package = package_type.from_specification(
|
|
98
|
+
dep_str, connection_timeout
|
|
99
|
+
)
|
|
92
100
|
if not package:
|
|
93
101
|
continue
|
|
94
102
|
|
|
@@ -22,13 +22,14 @@ classifiers = [
|
|
|
22
22
|
dependencies = [
|
|
23
23
|
"requests>=2.32.5",
|
|
24
24
|
"ruamel-yaml>=0.19.1",
|
|
25
|
+
"packaging>=26.0",
|
|
25
26
|
]
|
|
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
|
+
description = "A CLI tool to automatically update additional dependencies within local Python, Julia, Rust, Go, and Node.js hooks in pre-commit config files."
|
|
27
28
|
dynamic = []
|
|
28
29
|
name = "pre-commit-localupdate"
|
|
29
30
|
readme = "README.md"
|
|
30
31
|
requires-python = "<3.15,>=3.11"
|
|
31
|
-
version = "0.
|
|
32
|
+
version = "0.4.1"
|
|
32
33
|
|
|
33
34
|
[project.scripts]
|
|
34
35
|
pre-commit-localupdate = "pre_commit_localupdate.__main__:main"
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = '0.3.1'
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
# SPDX-FileCopyrightText: 2026 M. Farzalipour Tabriz, Max Planck Institute for Physics
|
|
2
|
-
# SPDX-License-Identifier: LGPL-3.0-or-later
|
|
3
|
-
|
|
4
|
-
from abc import ABC, abstractmethod
|
|
5
|
-
from dataclasses import dataclass
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
@dataclass
|
|
9
|
-
class PackageBase(ABC):
|
|
10
|
-
"""Abstract base class defining the interface for all package types.
|
|
11
|
-
|
|
12
|
-
Attributes:
|
|
13
|
-
name: The name of the package (e.g., 'requests').
|
|
14
|
-
raw_spec: The original version specifier string (e.g., '==2.25.1', '^4.17.21', '>=1.0.0', '@1.2.3').
|
|
15
|
-
latest_version: Latest version number without operators (e.g., '2.25.1').
|
|
16
|
-
|
|
17
|
-
"""
|
|
18
|
-
|
|
19
|
-
name: str
|
|
20
|
-
raw_spec: str | None
|
|
21
|
-
latest_version: str | None
|
|
22
|
-
|
|
23
|
-
@classmethod
|
|
24
|
-
@abstractmethod
|
|
25
|
-
def from_specification(package, spec_string: str) -> "PackageBase" | None:
|
|
26
|
-
"""Parse a package specification string into a Package object."""
|
|
27
|
-
|
|
28
|
-
@abstractmethod
|
|
29
|
-
def latest_version_specification(package) -> str | None:
|
|
30
|
-
"""Version specification of package pinned to the latest version."""
|
|
31
|
-
|
|
32
|
-
def version_specification(package) -> str | None:
|
|
33
|
-
"""Version specification of package as originally defined.
|
|
34
|
-
|
|
35
|
-
Returns:
|
|
36
|
-
The version specification string.
|
|
37
|
-
|
|
38
|
-
"""
|
|
39
|
-
if package.raw_spec:
|
|
40
|
-
return f"{package.name}{package.raw_spec}"
|
|
41
|
-
return f"{package.name}[unspecified version]"
|
|
@@ -1,86 +0,0 @@
|
|
|
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 cast
|
|
7
|
-
|
|
8
|
-
import requests
|
|
9
|
-
|
|
10
|
-
from .package import PackageBase
|
|
11
|
-
|
|
12
|
-
PYPI_API_URL = "https://pypi.org/pypi/{package_name}/json"
|
|
13
|
-
REQUEST_TIMEOUT = 10
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
class PyPackage(PackageBase):
|
|
17
|
-
"""Represents a Python package."""
|
|
18
|
-
|
|
19
|
-
@classmethod
|
|
20
|
-
def from_specification(package, spec: str) -> "PyPackage" | None:
|
|
21
|
-
"""Parse a package specification string into a Package object.
|
|
22
|
-
|
|
23
|
-
Handles formats like 'ansible==13.3.0', 'ansible>=1.0', or just 'ansible'.
|
|
24
|
-
|
|
25
|
-
Args:
|
|
26
|
-
spec: The package specification string.
|
|
27
|
-
|
|
28
|
-
Returns:
|
|
29
|
-
A Package instance if parsing succeeds, otherwise None.
|
|
30
|
-
|
|
31
|
-
"""
|
|
32
|
-
clean_spec = spec.strip().strip('"').strip("'")
|
|
33
|
-
match = re.match(r"^([a-zA-Z0-9._-]+)([=<>!~]+.*)?$", clean_spec)
|
|
34
|
-
if not match:
|
|
35
|
-
logging.warning("Could not parse the package specification: %s", clean_spec)
|
|
36
|
-
return None
|
|
37
|
-
|
|
38
|
-
name = match.group(1)
|
|
39
|
-
raw_spec = match.group(2)
|
|
40
|
-
latest_version = package._get_latest_version(name)
|
|
41
|
-
|
|
42
|
-
return package(
|
|
43
|
-
name=name,
|
|
44
|
-
raw_spec=raw_spec,
|
|
45
|
-
latest_version=latest_version,
|
|
46
|
-
)
|
|
47
|
-
|
|
48
|
-
@staticmethod
|
|
49
|
-
def _get_latest_version(package_name: str) -> str | None:
|
|
50
|
-
"""Fetch the latest version of a package from PyPI.
|
|
51
|
-
|
|
52
|
-
Returns:
|
|
53
|
-
The latest version string if found, otherwise None.
|
|
54
|
-
|
|
55
|
-
"""
|
|
56
|
-
logging.debug("Fetching latest version for %s", package_name)
|
|
57
|
-
try:
|
|
58
|
-
url = PYPI_API_URL.format(package_name=package_name)
|
|
59
|
-
response = requests.get(url, timeout=REQUEST_TIMEOUT)
|
|
60
|
-
response.raise_for_status()
|
|
61
|
-
data = response.json()
|
|
62
|
-
version = data.get("info", {}).get("version")
|
|
63
|
-
if version and isinstance(version, str):
|
|
64
|
-
return cast("str", version)
|
|
65
|
-
|
|
66
|
-
return None
|
|
67
|
-
except requests.exceptions.RequestException as exc:
|
|
68
|
-
logging.warning(
|
|
69
|
-
"Could not fetch latest version for %s: %s", package_name, exc
|
|
70
|
-
)
|
|
71
|
-
return None
|
|
72
|
-
except (KeyError, ValueError) as exc:
|
|
73
|
-
logging.warning("Could not parse response for %s: %s", package_name, exc)
|
|
74
|
-
return None
|
|
75
|
-
|
|
76
|
-
def latest_version_specification(package) -> str | None:
|
|
77
|
-
"""Version specification of package pinned to the latest version.
|
|
78
|
-
|
|
79
|
-
Returns:
|
|
80
|
-
The latest version specification string if found, otherwise None.
|
|
81
|
-
|
|
82
|
-
"""
|
|
83
|
-
if not package.latest_version:
|
|
84
|
-
return None
|
|
85
|
-
|
|
86
|
-
return f"{package.name}=={package.latest_version}"
|
|
File without changes
|
{pre_commit_localupdate-0.3.1 → pre_commit_localupdate-0.4.1}/pre_commit_localupdate/error.py
RENAMED
|
File without changes
|