pre-commit-localupdate 0.4.1__tar.gz → 0.5.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.4.1 → pre_commit_localupdate-0.5.0}/PKG-INFO +38 -22
- {pre_commit_localupdate-0.4.1 → pre_commit_localupdate-0.5.0}/README.md +36 -20
- pre_commit_localupdate-0.5.0/pre_commit_localupdate/__init__.py +1 -0
- {pre_commit_localupdate-0.4.1 → pre_commit_localupdate-0.5.0}/pre_commit_localupdate/__main__.py +9 -3
- {pre_commit_localupdate-0.4.1 → pre_commit_localupdate-0.5.0}/pre_commit_localupdate/cli.py +31 -3
- pre_commit_localupdate-0.5.0/pre_commit_localupdate/io.py +121 -0
- {pre_commit_localupdate-0.4.1 → pre_commit_localupdate-0.5.0}/pre_commit_localupdate/packages/golang.py +14 -32
- {pre_commit_localupdate-0.4.1 → pre_commit_localupdate-0.5.0}/pre_commit_localupdate/packages/julia.py +42 -37
- {pre_commit_localupdate-0.4.1 → pre_commit_localupdate-0.5.0}/pre_commit_localupdate/packages/node.py +22 -38
- {pre_commit_localupdate-0.4.1 → pre_commit_localupdate-0.5.0}/pre_commit_localupdate/packages/package.py +16 -8
- {pre_commit_localupdate-0.4.1 → pre_commit_localupdate-0.5.0}/pre_commit_localupdate/packages/python.py +16 -32
- {pre_commit_localupdate-0.4.1 → pre_commit_localupdate-0.5.0}/pre_commit_localupdate/packages/rust.py +15 -28
- {pre_commit_localupdate-0.4.1 → pre_commit_localupdate-0.5.0}/pre_commit_localupdate/pre_commit_config.py +43 -59
- {pre_commit_localupdate-0.4.1 → pre_commit_localupdate-0.5.0}/pyproject.toml +2 -2
- pre_commit_localupdate-0.4.1/pre_commit_localupdate/__init__.py +0 -1
- pre_commit_localupdate-0.4.1/pre_commit_localupdate/io.py +0 -53
- {pre_commit_localupdate-0.4.1 → pre_commit_localupdate-0.5.0}/LICENSE +0 -0
- {pre_commit_localupdate-0.4.1 → pre_commit_localupdate-0.5.0}/pre_commit_localupdate/error.py +0 -0
- {pre_commit_localupdate-0.4.1 → pre_commit_localupdate-0.5.0}/pre_commit_localupdate/logs.py +0 -0
- {pre_commit_localupdate-0.4.1 → pre_commit_localupdate-0.5.0}/pre_commit_localupdate/packages/__init__.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
|
|
3
|
+
Version: 0.5.0
|
|
4
|
+
Summary: A CLI tool to automatically update additional dependencies within local hooks in pre-commit config files.
|
|
5
5
|
Author: M. Farzalipour Tabriz
|
|
6
6
|
Classifier: Development Status :: 3 - Alpha
|
|
7
7
|
Classifier: Environment :: Console
|
|
@@ -16,7 +16,7 @@ Description-Content-Type: text/markdown
|
|
|
16
16
|
|
|
17
17
|
# pre-commit-localupdate
|
|
18
18
|
|
|
19
|
-
A CLI tool to automatically update dependencies in `pre-commit-config.yml` files. It specifically targets `additional_dependencies` within local
|
|
19
|
+
A CLI tool to automatically update dependencies in `pre-commit-config.yml` files. It specifically targets `additional_dependencies` within local 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.
|
|
20
20
|
|
|
21
21
|
## Installation
|
|
22
22
|
|
|
@@ -34,20 +34,25 @@ pre-commit-localupdate
|
|
|
34
34
|
|
|
35
35
|
All options:
|
|
36
36
|
|
|
37
|
-
```
|
|
38
|
-
usage: pre-commit-localupdate [-h] [--debug] [--dry-run] [-c CONFIG] [--timeout TIMEOUT]
|
|
37
|
+
```text
|
|
38
|
+
usage: pre-commit-localupdate [-h] [--debug] [--dry-run] [-c PRE-COMMIT-CONFIG] [--timeout TIMEOUT]
|
|
39
|
+
[--indent-mapping INDENT_MAPPING] [--indent-sequence INDENT_SEQUENCE]
|
|
40
|
+
[--indent-offset INDENT_OFFSET] [--line-width LINE_WIDTH] [--version]
|
|
39
41
|
|
|
40
|
-
Automatically update additional dependencies within local
|
|
41
|
-
file.
|
|
42
|
+
Automatically update additional dependencies within local hooks in a pre-commit config file.
|
|
42
43
|
|
|
43
44
|
options:
|
|
44
|
-
-h, --help
|
|
45
|
-
--debug
|
|
46
|
-
--dry-run
|
|
47
|
-
|
|
48
|
-
-c, --config CONFIG
|
|
49
|
-
--timeout TIMEOUT
|
|
50
|
-
--
|
|
45
|
+
-h, --help show this help message and exit
|
|
46
|
+
--debug enable debug logging (default: False)
|
|
47
|
+
--dry-run dry run mode. Do not update the file and exit with a non-zero code if the
|
|
48
|
+
configuration file require an update. (default: False)
|
|
49
|
+
-c, --config PRE-COMMIT-CONFIG pre-commit config file path (default: .pre-commit-config.yaml)
|
|
50
|
+
--timeout TIMEOUT connection timeout in seconds (default: 10)
|
|
51
|
+
--indent-mapping INDENT_MAPPING YAML indentation for mappings (default: 2)
|
|
52
|
+
--indent-sequence INDENT_SEQUENCE YAML indentation for sequences (default: 4)
|
|
53
|
+
--indent-offset INDENT_OFFSET YAML indentation offset (default: 2)
|
|
54
|
+
--line-width LINE_WIDTH maximum line width (default: 80)
|
|
55
|
+
--version show program's version number and exit
|
|
51
56
|
```
|
|
52
57
|
|
|
53
58
|
## Example
|
|
@@ -70,7 +75,7 @@ repos:
|
|
|
70
75
|
# All comment are preserved
|
|
71
76
|
- id: black
|
|
72
77
|
name: black
|
|
73
|
-
description: "
|
|
78
|
+
description: "Length of strings is automatically adjusted based on the value of --line-width"
|
|
74
79
|
entry: black
|
|
75
80
|
language: python
|
|
76
81
|
minimum_pre_commit_version: 2.9.2
|
|
@@ -84,7 +89,7 @@ repos:
|
|
|
84
89
|
|
|
85
90
|
- id: julia-format
|
|
86
91
|
name: format julia code
|
|
87
|
-
description:
|
|
92
|
+
description: Unicodeテキストは保持されます。
|
|
88
93
|
language: julia
|
|
89
94
|
types: [julia]
|
|
90
95
|
entry: tools/formatter.jl
|
|
@@ -120,8 +125,8 @@ repos:
|
|
|
120
125
|
# All comment are preserved
|
|
121
126
|
- id: black
|
|
122
127
|
name: black
|
|
123
|
-
description: "
|
|
124
|
-
|
|
128
|
+
description: "Length of strings is automatically adjusted based on the value
|
|
129
|
+
of --line-width"
|
|
125
130
|
entry: black
|
|
126
131
|
language: python
|
|
127
132
|
minimum_pre_commit_version: 2.9.2
|
|
@@ -135,7 +140,7 @@ repos:
|
|
|
135
140
|
|
|
136
141
|
- id: julia-format
|
|
137
142
|
name: format julia code
|
|
138
|
-
description:
|
|
143
|
+
description: Unicodeテキストは保持されます。
|
|
139
144
|
language: julia
|
|
140
145
|
types: [julia]
|
|
141
146
|
entry: tools/formatter.jl
|
|
@@ -153,14 +158,25 @@ repos:
|
|
|
153
158
|
additional_dependencies: ["cli:mdbook-lint:0.14.2"]
|
|
154
159
|
```
|
|
155
160
|
|
|
161
|
+
## Supported Languages
|
|
162
|
+
|
|
163
|
+
Following local hook languages are currently supported:
|
|
164
|
+
|
|
165
|
+
- Go (golang)
|
|
166
|
+
- Julia
|
|
167
|
+
- Node.js (node)
|
|
168
|
+
- Python
|
|
169
|
+
- Rust
|
|
170
|
+
|
|
156
171
|
## Formatting
|
|
157
172
|
|
|
158
|
-
|
|
173
|
+
Significant effort has been made to ensure `pre-commit-localupdate` maintains the original style and preserves comments in pre-commit configuration files. However, since the current version of the underlying library does not perfectly preserve the original YAML file format after processing, `pre-commit-localupdate` automatically folds long string values (like descriptions) and enforces block indentation by default when updating files to enhance readability. Both behaviors can be adjusted using command-line parameters.
|
|
159
174
|
|
|
160
175
|
## Requirements
|
|
161
176
|
|
|
162
|
-
-
|
|
163
|
-
- requests
|
|
177
|
+
- `packaging`
|
|
178
|
+
- `requests`
|
|
179
|
+
- `ruamel.yaml`
|
|
164
180
|
|
|
165
181
|
## License
|
|
166
182
|
|
|
@@ -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
|
|
3
|
+
A CLI tool to automatically update dependencies in `pre-commit-config.yml` files. It specifically targets `additional_dependencies` within local 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
|
|
|
@@ -18,20 +18,25 @@ pre-commit-localupdate
|
|
|
18
18
|
|
|
19
19
|
All options:
|
|
20
20
|
|
|
21
|
-
```
|
|
22
|
-
usage: pre-commit-localupdate [-h] [--debug] [--dry-run] [-c CONFIG] [--timeout TIMEOUT]
|
|
21
|
+
```text
|
|
22
|
+
usage: pre-commit-localupdate [-h] [--debug] [--dry-run] [-c PRE-COMMIT-CONFIG] [--timeout TIMEOUT]
|
|
23
|
+
[--indent-mapping INDENT_MAPPING] [--indent-sequence INDENT_SEQUENCE]
|
|
24
|
+
[--indent-offset INDENT_OFFSET] [--line-width LINE_WIDTH] [--version]
|
|
23
25
|
|
|
24
|
-
Automatically update additional dependencies within local
|
|
25
|
-
file.
|
|
26
|
+
Automatically update additional dependencies within local hooks in a pre-commit config file.
|
|
26
27
|
|
|
27
28
|
options:
|
|
28
|
-
-h, --help
|
|
29
|
-
--debug
|
|
30
|
-
--dry-run
|
|
31
|
-
|
|
32
|
-
-c, --config CONFIG
|
|
33
|
-
--timeout TIMEOUT
|
|
34
|
-
--
|
|
29
|
+
-h, --help show this help message and exit
|
|
30
|
+
--debug enable debug logging (default: False)
|
|
31
|
+
--dry-run dry run mode. Do not update the file and exit with a non-zero code if the
|
|
32
|
+
configuration file require an update. (default: False)
|
|
33
|
+
-c, --config PRE-COMMIT-CONFIG pre-commit config file path (default: .pre-commit-config.yaml)
|
|
34
|
+
--timeout TIMEOUT connection timeout in seconds (default: 10)
|
|
35
|
+
--indent-mapping INDENT_MAPPING YAML indentation for mappings (default: 2)
|
|
36
|
+
--indent-sequence INDENT_SEQUENCE YAML indentation for sequences (default: 4)
|
|
37
|
+
--indent-offset INDENT_OFFSET YAML indentation offset (default: 2)
|
|
38
|
+
--line-width LINE_WIDTH maximum line width (default: 80)
|
|
39
|
+
--version show program's version number and exit
|
|
35
40
|
```
|
|
36
41
|
|
|
37
42
|
## Example
|
|
@@ -54,7 +59,7 @@ repos:
|
|
|
54
59
|
# All comment are preserved
|
|
55
60
|
- id: black
|
|
56
61
|
name: black
|
|
57
|
-
description: "
|
|
62
|
+
description: "Length of strings is automatically adjusted based on the value of --line-width"
|
|
58
63
|
entry: black
|
|
59
64
|
language: python
|
|
60
65
|
minimum_pre_commit_version: 2.9.2
|
|
@@ -68,7 +73,7 @@ repos:
|
|
|
68
73
|
|
|
69
74
|
- id: julia-format
|
|
70
75
|
name: format julia code
|
|
71
|
-
description:
|
|
76
|
+
description: Unicodeテキストは保持されます。
|
|
72
77
|
language: julia
|
|
73
78
|
types: [julia]
|
|
74
79
|
entry: tools/formatter.jl
|
|
@@ -104,8 +109,8 @@ repos:
|
|
|
104
109
|
# All comment are preserved
|
|
105
110
|
- id: black
|
|
106
111
|
name: black
|
|
107
|
-
description: "
|
|
108
|
-
|
|
112
|
+
description: "Length of strings is automatically adjusted based on the value
|
|
113
|
+
of --line-width"
|
|
109
114
|
entry: black
|
|
110
115
|
language: python
|
|
111
116
|
minimum_pre_commit_version: 2.9.2
|
|
@@ -119,7 +124,7 @@ repos:
|
|
|
119
124
|
|
|
120
125
|
- id: julia-format
|
|
121
126
|
name: format julia code
|
|
122
|
-
description:
|
|
127
|
+
description: Unicodeテキストは保持されます。
|
|
123
128
|
language: julia
|
|
124
129
|
types: [julia]
|
|
125
130
|
entry: tools/formatter.jl
|
|
@@ -137,14 +142,25 @@ repos:
|
|
|
137
142
|
additional_dependencies: ["cli:mdbook-lint:0.14.2"]
|
|
138
143
|
```
|
|
139
144
|
|
|
145
|
+
## Supported Languages
|
|
146
|
+
|
|
147
|
+
Following local hook languages are currently supported:
|
|
148
|
+
|
|
149
|
+
- Go (golang)
|
|
150
|
+
- Julia
|
|
151
|
+
- Node.js (node)
|
|
152
|
+
- Python
|
|
153
|
+
- Rust
|
|
154
|
+
|
|
140
155
|
## Formatting
|
|
141
156
|
|
|
142
|
-
|
|
157
|
+
Significant effort has been made to ensure `pre-commit-localupdate` maintains the original style and preserves comments in pre-commit configuration files. However, since the current version of the underlying library does not perfectly preserve the original YAML file format after processing, `pre-commit-localupdate` automatically folds long string values (like descriptions) and enforces block indentation by default when updating files to enhance readability. Both behaviors can be adjusted using command-line parameters.
|
|
143
158
|
|
|
144
159
|
## Requirements
|
|
145
160
|
|
|
146
|
-
-
|
|
147
|
-
- requests
|
|
161
|
+
- `packaging`
|
|
162
|
+
- `requests`
|
|
163
|
+
- `ruamel.yaml`
|
|
148
164
|
|
|
149
165
|
## License
|
|
150
166
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = '0.5.0'
|
{pre_commit_localupdate-0.4.1 → pre_commit_localupdate-0.5.0}/pre_commit_localupdate/__main__.py
RENAMED
|
@@ -11,10 +11,10 @@ to their latest versions.
|
|
|
11
11
|
|
|
12
12
|
import logging
|
|
13
13
|
import sys
|
|
14
|
-
from pathlib import Path
|
|
15
14
|
|
|
16
15
|
from pre_commit_localupdate.cli import parse_args
|
|
17
16
|
from pre_commit_localupdate.error import PreCommitLocalUpdateError
|
|
17
|
+
from pre_commit_localupdate.io import YAMLFormat
|
|
18
18
|
from pre_commit_localupdate.logs import setup_logging
|
|
19
19
|
from pre_commit_localupdate.pre_commit_config import update_additional_dependencies
|
|
20
20
|
|
|
@@ -23,12 +23,18 @@ def main() -> None:
|
|
|
23
23
|
"""Main entry point for the script."""
|
|
24
24
|
args = parse_args()
|
|
25
25
|
setup_logging(debug=args.debug)
|
|
26
|
-
|
|
26
|
+
yaml_format = YAMLFormat(
|
|
27
|
+
indent_mapping=args.indent_mapping,
|
|
28
|
+
indent_sequence=args.indent_sequence,
|
|
29
|
+
indent_offset=args.indent_offset,
|
|
30
|
+
line_width=args.line_width,
|
|
31
|
+
)
|
|
27
32
|
try:
|
|
28
33
|
if update_additional_dependencies(
|
|
29
|
-
|
|
34
|
+
args.config, args.timeout, yaml_format, dry_run=args.dry_run
|
|
30
35
|
):
|
|
31
36
|
if args.dry_run:
|
|
37
|
+
logging.info("Additional dependencies require updating!")
|
|
32
38
|
sys.exit(1)
|
|
33
39
|
logging.info("Done.")
|
|
34
40
|
else:
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
# SPDX-License-Identifier: LGPL-3.0-or-later
|
|
3
3
|
|
|
4
4
|
import argparse
|
|
5
|
+
from pathlib import Path
|
|
5
6
|
|
|
6
7
|
from pre_commit_localupdate import __version__
|
|
7
8
|
|
|
@@ -13,8 +14,10 @@ def parse_args() -> argparse.Namespace:
|
|
|
13
14
|
argparse.Namespace: Parsed arguments.
|
|
14
15
|
"""
|
|
15
16
|
parser = argparse.ArgumentParser(
|
|
16
|
-
description="Automatically update additional dependencies within local
|
|
17
|
-
formatter_class=argparse.ArgumentDefaultsHelpFormatter
|
|
17
|
+
description="Automatically update additional dependencies within local hooks in a pre-commit config file.",
|
|
18
|
+
formatter_class=lambda prog: argparse.ArgumentDefaultsHelpFormatter(
|
|
19
|
+
prog, max_help_position=40, width=120
|
|
20
|
+
),
|
|
18
21
|
)
|
|
19
22
|
parser.add_argument(
|
|
20
23
|
"--debug",
|
|
@@ -29,9 +32,10 @@ def parse_args() -> argparse.Namespace:
|
|
|
29
32
|
parser.add_argument(
|
|
30
33
|
"-c",
|
|
31
34
|
"--config",
|
|
32
|
-
type=
|
|
35
|
+
type=Path,
|
|
33
36
|
help="pre-commit config file path",
|
|
34
37
|
default=".pre-commit-config.yaml",
|
|
38
|
+
metavar="PRE-COMMIT-CONFIG",
|
|
35
39
|
)
|
|
36
40
|
parser.add_argument(
|
|
37
41
|
"--timeout",
|
|
@@ -39,6 +43,30 @@ def parse_args() -> argparse.Namespace:
|
|
|
39
43
|
help="connection timeout in seconds",
|
|
40
44
|
default=10,
|
|
41
45
|
)
|
|
46
|
+
parser.add_argument(
|
|
47
|
+
"--indent-mapping",
|
|
48
|
+
type=int,
|
|
49
|
+
help="YAML indentation for mappings",
|
|
50
|
+
default=2,
|
|
51
|
+
)
|
|
52
|
+
parser.add_argument(
|
|
53
|
+
"--indent-sequence",
|
|
54
|
+
type=int,
|
|
55
|
+
help="YAML indentation for sequences",
|
|
56
|
+
default=4,
|
|
57
|
+
)
|
|
58
|
+
parser.add_argument(
|
|
59
|
+
"--indent-offset",
|
|
60
|
+
type=int,
|
|
61
|
+
help="YAML indentation offset",
|
|
62
|
+
default=2,
|
|
63
|
+
)
|
|
64
|
+
parser.add_argument(
|
|
65
|
+
"--line-width",
|
|
66
|
+
type=int,
|
|
67
|
+
help="maximum line width",
|
|
68
|
+
default=80,
|
|
69
|
+
)
|
|
42
70
|
parser.add_argument(
|
|
43
71
|
"--version",
|
|
44
72
|
action="version",
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 M. Farzalipour Tabriz, Max Planck Institute for Physics
|
|
2
|
+
# SPDX-License-Identifier: LGPL-3.0-or-later
|
|
3
|
+
|
|
4
|
+
import io
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
import tempfile
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from ruamel.yaml import YAML
|
|
12
|
+
from ruamel.yaml.comments import CommentedMap
|
|
13
|
+
|
|
14
|
+
from pre_commit_localupdate.error import PreCommitLocalUpdateError
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True)
|
|
18
|
+
class YAMLFormat:
|
|
19
|
+
"""Configuration for YAML file formatting parameters.
|
|
20
|
+
|
|
21
|
+
Attributes:
|
|
22
|
+
indent_mapping (int): YAML indentation for mappings
|
|
23
|
+
indent_sequence (int): YAML indentation for sequences
|
|
24
|
+
indent_offset (int): YAML indentation offset
|
|
25
|
+
line_width (int): maximum line width
|
|
26
|
+
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
indent_mapping: int
|
|
30
|
+
indent_sequence: int
|
|
31
|
+
indent_offset: int
|
|
32
|
+
line_width: int
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def load_config_file(file_path: Path) -> tuple[list[str], str]:
|
|
36
|
+
"""Reads pre-commit configuration file and returns its header lines and content.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
file_path (Path): Path to config file
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
tuple[list[str], str]: Header lines, config file content
|
|
43
|
+
|
|
44
|
+
Raises:
|
|
45
|
+
PreCommitLocalUpdateError: Error in reading config file
|
|
46
|
+
"""
|
|
47
|
+
logging.debug("Reading configuration file: %s", file_path)
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
file_content = file_path.read_text(encoding="utf-8")
|
|
51
|
+
except FileNotFoundError as exc:
|
|
52
|
+
raise PreCommitLocalUpdateError(f"File not found: {file_path}") from exc
|
|
53
|
+
except OSError as exc:
|
|
54
|
+
raise PreCommitLocalUpdateError(
|
|
55
|
+
f"IOError while reading file {file_path}"
|
|
56
|
+
) from exc
|
|
57
|
+
|
|
58
|
+
raw_lines = file_content.splitlines(keepends=True)
|
|
59
|
+
|
|
60
|
+
content_start_index = 0
|
|
61
|
+
for i, line in enumerate(raw_lines):
|
|
62
|
+
stripped = line.strip()
|
|
63
|
+
if stripped == "---":
|
|
64
|
+
logging.debug("YAML marker found.")
|
|
65
|
+
content_start_index = i + 1
|
|
66
|
+
break
|
|
67
|
+
if stripped and not stripped.startswith("#"):
|
|
68
|
+
content_start_index = i
|
|
69
|
+
break
|
|
70
|
+
|
|
71
|
+
header_lines = raw_lines[:content_start_index]
|
|
72
|
+
content = "".join(raw_lines[content_start_index:])
|
|
73
|
+
|
|
74
|
+
return header_lines, content
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def save_config_file(
|
|
78
|
+
file_path: Path,
|
|
79
|
+
config: CommentedMap,
|
|
80
|
+
header_lines: list[str],
|
|
81
|
+
yaml_dumper: YAML,
|
|
82
|
+
) -> None:
|
|
83
|
+
"""Writes pre-commit configuration file.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
file_path (Path): Path to config file
|
|
87
|
+
config (CommentedMap): Config file content
|
|
88
|
+
header_lines (list[str]): File header lines
|
|
89
|
+
yaml_dumper (YAML): YAML dumper
|
|
90
|
+
|
|
91
|
+
Raises:
|
|
92
|
+
PreCommitLocalUpdateError: Error in writing config file
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
logging.debug("Writing modifications to disk...")
|
|
96
|
+
stream = io.StringIO()
|
|
97
|
+
yaml_dumper.dump(config, stream)
|
|
98
|
+
modified_body = stream.getvalue()
|
|
99
|
+
final_content = "".join(header_lines) + modified_body
|
|
100
|
+
|
|
101
|
+
temp_file_path = None
|
|
102
|
+
try:
|
|
103
|
+
with tempfile.NamedTemporaryFile(
|
|
104
|
+
mode="w",
|
|
105
|
+
encoding="utf-8",
|
|
106
|
+
dir=file_path.parent,
|
|
107
|
+
delete=False,
|
|
108
|
+
) as temp_file:
|
|
109
|
+
temp_file.write(final_content)
|
|
110
|
+
os.fsync(temp_file.fileno())
|
|
111
|
+
temp_file_path = temp_file.name
|
|
112
|
+
|
|
113
|
+
os.replace(temp_file_path, file_path)
|
|
114
|
+
logging.info("Successfully updated %s", file_path)
|
|
115
|
+
|
|
116
|
+
except OSError as exc:
|
|
117
|
+
if temp_file_path and os.path.exists(temp_file_path):
|
|
118
|
+
os.unlink(temp_file_path)
|
|
119
|
+
raise PreCommitLocalUpdateError(
|
|
120
|
+
f"IOError while writing to file {file_path}"
|
|
121
|
+
) from exc
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
|
|
4
4
|
import logging
|
|
5
5
|
import re
|
|
6
|
-
from dataclasses import dataclass
|
|
7
6
|
|
|
8
7
|
import requests
|
|
9
8
|
|
|
@@ -13,51 +12,39 @@ from .package import PackageBase
|
|
|
13
12
|
GO_PKG_LATEST_VERSION_API_URL = "https://proxy.golang.org/{package_path}/@latest"
|
|
14
13
|
|
|
15
14
|
|
|
16
|
-
@dataclass
|
|
17
15
|
class GoPackage(PackageBase):
|
|
18
16
|
"""Represents a Go module."""
|
|
19
17
|
|
|
20
18
|
@classmethod
|
|
21
|
-
def from_specification(
|
|
22
|
-
cls, spec: str, connection_timeout: int
|
|
23
|
-
) -> "GoPackage" | None:
|
|
19
|
+
def from_specification(cls, spec: str) -> "GoPackage" | None:
|
|
24
20
|
"""Parse a Go module specification string into a Package object.
|
|
25
21
|
|
|
26
|
-
|
|
27
|
-
-
|
|
28
|
-
-
|
|
29
|
-
-
|
|
30
|
-
-
|
|
31
|
-
-
|
|
32
|
-
-
|
|
22
|
+
Supported formats:
|
|
23
|
+
- `mvdan.cc/sh` (Module path only)
|
|
24
|
+
- `mvdan.cc/sh@v1.3.0` (Module path with specific version)
|
|
25
|
+
- `mvdan.cc/sh/v3` (Module path with major version suffix)
|
|
26
|
+
- `mvdan.cc/sh/v3@v3.10.0` (Major suffix with version)
|
|
27
|
+
- `mvdan.cc/sh/v3/cmd/shfmt` (Submodule)
|
|
28
|
+
- `mvdan.cc/sh/v3/cmd/shfmt@v3.10.0` (Submodule with version)
|
|
33
29
|
|
|
34
30
|
Args:
|
|
35
|
-
spec (str): The package specification string.
|
|
36
|
-
connection_timeout (int): Connection timeout in seconds.
|
|
31
|
+
spec (str): The package specification string (without quotations).
|
|
37
32
|
|
|
38
33
|
Returns:
|
|
39
34
|
"GoPackage" | None: A Package instance if parsing succeeds, otherwise None.
|
|
40
35
|
"""
|
|
41
|
-
clean_spec = spec.strip().strip('"').strip("'")
|
|
42
36
|
|
|
43
|
-
match = re.match(r"^([a-zA-Z0-9._/-]+)(@.+)?$",
|
|
37
|
+
match = re.match(r"^([a-zA-Z0-9._/-]+)(@.+)?$", spec)
|
|
44
38
|
|
|
45
39
|
if not match:
|
|
46
|
-
logging.warning(
|
|
47
|
-
"Could not parse the Go package specification: %s", clean_spec
|
|
48
|
-
)
|
|
49
40
|
return None
|
|
50
41
|
|
|
51
42
|
name = match.group(1)
|
|
52
43
|
raw_spec = match.group(2)
|
|
53
44
|
|
|
54
|
-
module_path = cls._resolve_module_path(name)
|
|
55
|
-
latest_version = cls._get_latest_version(module_path, connection_timeout)
|
|
56
|
-
|
|
57
45
|
return cls(
|
|
58
46
|
name=name,
|
|
59
47
|
raw_spec=raw_spec,
|
|
60
|
-
latest_version=latest_version,
|
|
61
48
|
)
|
|
62
49
|
|
|
63
50
|
@staticmethod
|
|
@@ -82,17 +69,13 @@ class GoPackage(PackageBase):
|
|
|
82
69
|
|
|
83
70
|
return package_path
|
|
84
71
|
|
|
85
|
-
|
|
86
|
-
def _get_latest_version(package_path: str, connection_timeout: int) -> str | None:
|
|
72
|
+
def fetch_latest_version(self, connection_timeout: int) -> None:
|
|
87
73
|
"""Fetch the latest version of a Go module from proxy.golang.org.
|
|
88
74
|
|
|
89
75
|
Args:
|
|
90
|
-
package_path (str): The module path (e.g., mvdan.cc/sh/v3).
|
|
91
76
|
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
77
|
"""
|
|
78
|
+
package_path = self._resolve_module_path(self.name)
|
|
96
79
|
logging.debug("Fetching latest version for %s", package_path)
|
|
97
80
|
try:
|
|
98
81
|
url = GO_PKG_LATEST_VERSION_API_URL.format(package_path=package_path)
|
|
@@ -103,16 +86,15 @@ class GoPackage(PackageBase):
|
|
|
103
86
|
data = response.json()
|
|
104
87
|
latest_version = data.get("Version")
|
|
105
88
|
|
|
106
|
-
|
|
89
|
+
if isinstance(latest_version, str):
|
|
90
|
+
self.latest_version = latest_version
|
|
107
91
|
|
|
108
92
|
except requests.exceptions.RequestException as exc:
|
|
109
93
|
logging.warning(
|
|
110
94
|
"Could not fetch latest version for %s: %s", package_path, exc
|
|
111
95
|
)
|
|
112
|
-
return None
|
|
113
96
|
except (KeyError, ValueError) as exc:
|
|
114
97
|
logging.warning("Could not parse response for %s: %s", package_path, exc)
|
|
115
|
-
return None
|
|
116
98
|
|
|
117
99
|
def latest_version_specification(self) -> str | None:
|
|
118
100
|
"""Version specification of package pinned to the latest version.
|
|
@@ -17,57 +17,69 @@ JULIA_PKG_API_URL = "https://raw.githubusercontent.com/JuliaRegistries/General/r
|
|
|
17
17
|
|
|
18
18
|
@dataclass
|
|
19
19
|
class JuliaPackage(PackageBase):
|
|
20
|
-
"""Represents a Julia package.
|
|
20
|
+
"""Represents a Julia package.
|
|
21
|
+
|
|
22
|
+
Attributes:
|
|
23
|
+
uuid (str): Package UUID [not validated]
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
uuid: str = ""
|
|
21
27
|
|
|
22
28
|
@classmethod
|
|
23
|
-
def from_specification(
|
|
24
|
-
cls, spec: str, connection_timeout: int
|
|
25
|
-
) -> "JuliaPackage" | None:
|
|
29
|
+
def from_specification(cls, spec: str) -> "JuliaPackage" | None:
|
|
26
30
|
"""Parse a package specification string into a Package object.
|
|
27
31
|
|
|
28
|
-
|
|
32
|
+
Supported formats:
|
|
33
|
+
- `package`
|
|
34
|
+
- `package@1.2.3`
|
|
35
|
+
- `package=2edaba10-b0f1-5616-af89-8c11ac63239a`
|
|
36
|
+
- `package=2edaba10-b0f1-5616-af89-8c11ac63239a@1.2`
|
|
29
37
|
|
|
30
38
|
Args:
|
|
31
|
-
spec (str): The package specification string.
|
|
32
|
-
connection_timeout (int): Connection timeout in seconds.
|
|
39
|
+
spec (str): The package specification string (without quotations).
|
|
33
40
|
|
|
34
41
|
Returns:
|
|
35
42
|
"JuliaPackage" | None: A Package instance if parsing succeeds, otherwise None.
|
|
36
43
|
|
|
37
44
|
"""
|
|
38
|
-
|
|
39
|
-
|
|
45
|
+
|
|
46
|
+
pattern = re.compile(
|
|
47
|
+
r"^(?P<name>[a-zA-Z0-9_]+)"
|
|
48
|
+
r"(?:=(?P<uuid>[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}))?"
|
|
49
|
+
r"(?:@(?P<version>[a-zA-Z0-9._+,-]+))?$"
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
match = pattern.match(spec)
|
|
40
53
|
|
|
41
54
|
if not match:
|
|
42
|
-
logging.warning("Could not parse the package specification: %s", clean_spec)
|
|
43
55
|
return None
|
|
44
56
|
|
|
45
|
-
name = match.group(
|
|
46
|
-
|
|
47
|
-
|
|
57
|
+
name = match.group("name")
|
|
58
|
+
uuid = match.group("uuid")
|
|
59
|
+
version = match.group("version")
|
|
60
|
+
|
|
61
|
+
raw_spec = None
|
|
62
|
+
if uuid:
|
|
63
|
+
raw_spec = f"={uuid}@{version}" if version else f"={uuid}"
|
|
64
|
+
elif version:
|
|
65
|
+
raw_spec = f"@{version}"
|
|
48
66
|
|
|
49
67
|
return cls(
|
|
50
68
|
name=name,
|
|
51
69
|
raw_spec=raw_spec,
|
|
52
|
-
|
|
70
|
+
uuid=uuid,
|
|
53
71
|
)
|
|
54
72
|
|
|
55
|
-
|
|
56
|
-
def _get_latest_version(package_name: str, connection_timeout: int) -> str | None:
|
|
73
|
+
def fetch_latest_version(self, connection_timeout: int) -> None:
|
|
57
74
|
"""Fetch the latest version of a package from the Julia General Registry.
|
|
58
75
|
|
|
59
76
|
Args:
|
|
60
|
-
package_name (str): Name of the package.
|
|
61
77
|
connection_timeout (int): Connection timeout in seconds.
|
|
62
|
-
|
|
63
|
-
Returns:
|
|
64
|
-
str | None: The latest version string if found, otherwise None.
|
|
65
|
-
|
|
66
78
|
"""
|
|
67
|
-
logging.debug("Fetching latest version for %s",
|
|
79
|
+
logging.debug("Fetching latest version for %s", self.name)
|
|
68
80
|
try:
|
|
69
81
|
url = JULIA_PKG_API_URL.format(
|
|
70
|
-
package_name=
|
|
82
|
+
package_name=self.name, package_name_first_letter=self.name[0]
|
|
71
83
|
)
|
|
72
84
|
response = requests.get(url, timeout=connection_timeout)
|
|
73
85
|
response.raise_for_status()
|
|
@@ -75,22 +87,14 @@ class JuliaPackage(PackageBase):
|
|
|
75
87
|
data = tomllib.loads(response.text)
|
|
76
88
|
|
|
77
89
|
versions = list(data.keys())
|
|
78
|
-
if
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
versions.sort(key=lambda v: Version(v), reverse=True)
|
|
82
|
-
latest_version = versions[0]
|
|
83
|
-
|
|
84
|
-
return latest_version if isinstance(latest_version, str) else None
|
|
90
|
+
if versions:
|
|
91
|
+
versions.sort(key=lambda v: Version(v), reverse=True)
|
|
92
|
+
self.latest_version = versions[0]
|
|
85
93
|
|
|
86
94
|
except requests.exceptions.RequestException as exc:
|
|
87
|
-
logging.warning(
|
|
88
|
-
"Could not fetch latest version for %s: %s", package_name, exc
|
|
89
|
-
)
|
|
90
|
-
return None
|
|
95
|
+
logging.warning("Could not fetch latest version for %s: %s", self.name, exc)
|
|
91
96
|
except (KeyError, ValueError, tomllib.TOMLDecodeError, InvalidVersion) as exc:
|
|
92
|
-
logging.warning("Could not parse response for %s: %s",
|
|
93
|
-
return None
|
|
97
|
+
logging.warning("Could not parse response for %s: %s", self.name, exc)
|
|
94
98
|
|
|
95
99
|
def latest_version_specification(self) -> str | None:
|
|
96
100
|
"""Version specification of package pinned to the latest version.
|
|
@@ -102,5 +106,6 @@ class JuliaPackage(PackageBase):
|
|
|
102
106
|
"""
|
|
103
107
|
if not self.latest_version:
|
|
104
108
|
return None
|
|
105
|
-
|
|
109
|
+
if self.uuid:
|
|
110
|
+
return f"{self.name}={self.uuid}@{self.latest_version}"
|
|
106
111
|
return f"{self.name}@{self.latest_version}"
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
|
|
4
4
|
import logging
|
|
5
5
|
import re
|
|
6
|
-
from dataclasses import dataclass
|
|
7
6
|
|
|
8
7
|
import requests
|
|
9
8
|
|
|
@@ -12,81 +11,66 @@ from .package import PackageBase
|
|
|
12
11
|
NPM_API_URL = "https://registry.npmjs.org/{package_name}"
|
|
13
12
|
|
|
14
13
|
|
|
15
|
-
@dataclass
|
|
16
14
|
class NodePackage(PackageBase):
|
|
17
15
|
"""Represents a Node.js package."""
|
|
18
16
|
|
|
19
17
|
@classmethod
|
|
20
|
-
def from_specification(
|
|
21
|
-
cls, spec: str, connection_timeout: int
|
|
22
|
-
) -> "NodePackage" | None:
|
|
18
|
+
def from_specification(cls, spec: str) -> "NodePackage" | None:
|
|
23
19
|
"""Parse a package specification string into a NodePackage object.
|
|
24
20
|
|
|
25
|
-
|
|
21
|
+
Supported formats:
|
|
22
|
+
- `package`
|
|
23
|
+
- `package@1.2.3`
|
|
24
|
+
- `package@>=1.2.3`
|
|
25
|
+
- `@scope/name@^1.2.3`
|
|
26
26
|
|
|
27
27
|
Args:
|
|
28
|
-
spec (str): The package specification string.
|
|
29
|
-
connection_timeout (int): Connection timeout in seconds.
|
|
28
|
+
spec (str): The package specification string (without quotations).
|
|
30
29
|
|
|
31
30
|
Returns:
|
|
32
31
|
"NodePackage" | None: A NodePackage instance if parsing succeeds, otherwise None.
|
|
33
32
|
|
|
34
33
|
"""
|
|
35
|
-
clean_spec = spec.strip().strip('"').strip("'")
|
|
36
34
|
|
|
37
|
-
if not
|
|
35
|
+
if not spec:
|
|
38
36
|
return None
|
|
39
37
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
38
|
+
# (@scope/name|name)(@version)?
|
|
39
|
+
match = re.match(r"^(?P<name>(?:@[^/]+\/)?[^@]+?)(?:@(?P<spec>.*))?$", spec)
|
|
40
|
+
if not match:
|
|
41
|
+
return None
|
|
44
42
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
if re.match(r"^(\d+|[<>=~^])", parts[i]):
|
|
48
|
-
name = "@".join(parts[:i])
|
|
49
|
-
raw_spec = "@" + "@".join(parts[i:])
|
|
50
|
-
break
|
|
43
|
+
name = match.group("name")
|
|
44
|
+
raw_spec = match.group("spec")
|
|
51
45
|
|
|
52
|
-
|
|
46
|
+
if not name:
|
|
47
|
+
return None
|
|
53
48
|
|
|
54
49
|
return cls(
|
|
55
50
|
name=name,
|
|
56
51
|
raw_spec=raw_spec,
|
|
57
|
-
latest_version=latest_version,
|
|
58
52
|
)
|
|
59
53
|
|
|
60
|
-
|
|
61
|
-
def _get_latest_version(package_name: str, connection_timeout: int) -> str | None:
|
|
54
|
+
def fetch_latest_version(self, connection_timeout: int) -> None:
|
|
62
55
|
"""Fetch the latest version of a package from the npm registry.
|
|
63
56
|
|
|
64
57
|
Args:
|
|
65
|
-
package_name (str): Name of the package.
|
|
66
58
|
connection_timeout (int): Connection timeout in seconds.
|
|
67
|
-
|
|
68
|
-
Returns:
|
|
69
|
-
str | None: The latest version string if found, otherwise None.
|
|
70
|
-
|
|
71
59
|
"""
|
|
72
60
|
try:
|
|
73
|
-
url = NPM_API_URL.format(package_name=
|
|
61
|
+
url = NPM_API_URL.format(package_name=self.name)
|
|
74
62
|
response = requests.get(url, timeout=connection_timeout)
|
|
75
63
|
response.raise_for_status()
|
|
76
64
|
data = response.json()
|
|
77
65
|
|
|
78
66
|
latest_version = data.get("dist-tags", {}).get("latest")
|
|
79
|
-
|
|
80
|
-
|
|
67
|
+
if isinstance(latest_version, str):
|
|
68
|
+
self.latest_version = latest_version
|
|
81
69
|
|
|
82
70
|
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
|
|
71
|
+
logging.warning("Could not fetch latest version for %s: %s", self.name, exc)
|
|
87
72
|
except (KeyError, ValueError) as exc:
|
|
88
|
-
logging.warning("Could not parse response for %s: %s",
|
|
89
|
-
return None
|
|
73
|
+
logging.warning("Could not parse response for %s: %s", self.name, exc)
|
|
90
74
|
|
|
91
75
|
def latest_version_specification(self) -> str | None:
|
|
92
76
|
"""Version specification of package pinned to the latest version.
|
|
@@ -11,30 +11,38 @@ class PackageBase(ABC):
|
|
|
11
11
|
|
|
12
12
|
Attributes:
|
|
13
13
|
name (str): The name of the package (e.g., 'requests').
|
|
14
|
-
raw_spec (str | None): The original version specifier string (e.g., '==2.
|
|
15
|
-
latest_version (str | None): Latest version number without operators (e.g., '2.
|
|
14
|
+
raw_spec (str | None): The original version specifier string (e.g., '==1.2.3', '^1.2.3', '>=1.2.3', '@1.2.3').
|
|
15
|
+
latest_version (str | None): Latest version number without operators (e.g., '1.2.3').
|
|
16
16
|
|
|
17
17
|
"""
|
|
18
18
|
|
|
19
19
|
name: str
|
|
20
20
|
raw_spec: str | None
|
|
21
|
-
latest_version: str | None
|
|
21
|
+
latest_version: str | None = None
|
|
22
22
|
|
|
23
23
|
@classmethod
|
|
24
24
|
@abstractmethod
|
|
25
25
|
def from_specification(
|
|
26
|
-
cls,
|
|
26
|
+
cls,
|
|
27
|
+
spec_string: str,
|
|
27
28
|
) -> "PackageBase" | None:
|
|
28
29
|
"""Parse a package specification string into a Package object.
|
|
29
30
|
|
|
30
31
|
Args:
|
|
31
|
-
spec_string (str): The package specification string.
|
|
32
|
-
connection_timeout (int): Connection timeout in seconds.
|
|
32
|
+
spec_string (str): The package specification string (without quotations).
|
|
33
33
|
|
|
34
34
|
Returns:
|
|
35
35
|
"PackageBase" | None: A Package instance if parsing succeeds, otherwise None.
|
|
36
36
|
"""
|
|
37
37
|
|
|
38
|
+
@abstractmethod
|
|
39
|
+
def fetch_latest_version(self, connection_timeout: int) -> None:
|
|
40
|
+
"""Get latest package version and set self.latest_version
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
connection_timeout (int): Connection timeout in seconds.
|
|
44
|
+
"""
|
|
45
|
+
|
|
38
46
|
@abstractmethod
|
|
39
47
|
def latest_version_specification(self) -> str | None:
|
|
40
48
|
"""Version specification of package pinned to the latest version.
|
|
@@ -43,7 +51,7 @@ class PackageBase(ABC):
|
|
|
43
51
|
str | None: The latest version specification string if found, otherwise None.
|
|
44
52
|
"""
|
|
45
53
|
|
|
46
|
-
def
|
|
54
|
+
def original_version_specification(self) -> str | None:
|
|
47
55
|
"""Version specification of package as originally defined.
|
|
48
56
|
|
|
49
57
|
Returns:
|
|
@@ -52,4 +60,4 @@ class PackageBase(ABC):
|
|
|
52
60
|
"""
|
|
53
61
|
if self.raw_spec:
|
|
54
62
|
return f"{self.name}{self.raw_spec}"
|
|
55
|
-
return f"{self.name}
|
|
63
|
+
return f"{self.name}"
|
|
@@ -2,85 +2,69 @@
|
|
|
2
2
|
# SPDX-License-Identifier: LGPL-3.0-or-later
|
|
3
3
|
|
|
4
4
|
import logging
|
|
5
|
-
from dataclasses import dataclass
|
|
6
5
|
|
|
7
6
|
import requests
|
|
8
|
-
from packaging.requirements import Requirement
|
|
7
|
+
from packaging.requirements import InvalidRequirement, Requirement
|
|
9
8
|
|
|
10
9
|
from .package import PackageBase
|
|
11
10
|
|
|
12
11
|
PYPI_API_URL = "https://pypi.org/pypi/{package_name}/json"
|
|
13
12
|
|
|
14
13
|
|
|
15
|
-
@dataclass
|
|
16
14
|
class PyPackage(PackageBase):
|
|
17
15
|
"""Represents a Python package."""
|
|
18
16
|
|
|
19
17
|
@classmethod
|
|
20
|
-
def from_specification(
|
|
21
|
-
cls, spec: str, connection_timeout: int
|
|
22
|
-
) -> "PyPackage" | None:
|
|
18
|
+
def from_specification(cls, spec: str) -> "PyPackage" | None:
|
|
23
19
|
"""Parse a package specification string into a Package object.
|
|
24
20
|
|
|
25
|
-
|
|
21
|
+
Supported formats (and other specifications in PEP 508):
|
|
22
|
+
- `package`
|
|
23
|
+
- `package==1.2.3`
|
|
24
|
+
- `package>=1.2.3`
|
|
26
25
|
|
|
27
26
|
Args:
|
|
28
|
-
spec (str): The package specification string.
|
|
29
|
-
connection_timeout (int): Connection timeout in seconds.
|
|
27
|
+
spec (str): The package specification string (without quotations).
|
|
30
28
|
|
|
31
29
|
Returns:
|
|
32
30
|
"PyPackage" | None: A Package instance if parsing succeeds, otherwise None.
|
|
33
31
|
|
|
34
32
|
"""
|
|
35
|
-
clean_spec = spec.strip().strip('"').strip("'")
|
|
36
33
|
|
|
37
34
|
try:
|
|
38
|
-
requirement = Requirement(
|
|
39
|
-
except
|
|
40
|
-
logging.warning(
|
|
41
|
-
"Could not parse the package specification: %s - %s", clean_spec, exc
|
|
42
|
-
)
|
|
35
|
+
requirement = Requirement(spec)
|
|
36
|
+
except InvalidRequirement:
|
|
43
37
|
return None
|
|
44
38
|
|
|
45
39
|
name = requirement.name
|
|
46
40
|
raw_spec = str(requirement.specifier) if requirement.specifier else None
|
|
47
|
-
latest_version = cls._get_latest_version(name, connection_timeout)
|
|
48
41
|
|
|
49
42
|
return cls(
|
|
50
43
|
name=name,
|
|
51
44
|
raw_spec=raw_spec,
|
|
52
|
-
latest_version=latest_version,
|
|
53
45
|
)
|
|
54
46
|
|
|
55
|
-
|
|
56
|
-
def _get_latest_version(package_name: str, connection_timeout: int) -> str | None:
|
|
47
|
+
def fetch_latest_version(self, connection_timeout: int) -> None:
|
|
57
48
|
"""Fetch the latest version of a package from PyPI.
|
|
58
49
|
|
|
59
50
|
Args:
|
|
60
|
-
package_name (str): Name of the package.
|
|
61
51
|
connection_timeout (int): Connection timeout in seconds.
|
|
62
52
|
|
|
63
|
-
Returns:
|
|
64
|
-
str | None: The latest version string if found, otherwise None.
|
|
65
|
-
|
|
66
53
|
"""
|
|
67
|
-
logging.debug("Fetching latest version for %s",
|
|
54
|
+
logging.debug("Fetching latest version for %s", self.name)
|
|
68
55
|
try:
|
|
69
|
-
url = PYPI_API_URL.format(package_name=
|
|
56
|
+
url = PYPI_API_URL.format(package_name=self.name)
|
|
70
57
|
response = requests.get(url, timeout=connection_timeout)
|
|
71
58
|
response.raise_for_status()
|
|
72
59
|
data = response.json()
|
|
73
60
|
latest_version = data.get("info", {}).get("version")
|
|
74
|
-
|
|
61
|
+
if isinstance(latest_version, str):
|
|
62
|
+
self.latest_version = latest_version
|
|
75
63
|
|
|
76
64
|
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
|
|
65
|
+
logging.warning("Could not fetch latest version for %s: %s", self.name, exc)
|
|
81
66
|
except (KeyError, ValueError) as exc:
|
|
82
|
-
logging.warning("Could not parse response for %s: %s",
|
|
83
|
-
return None
|
|
67
|
+
logging.warning("Could not parse response for %s: %s", self.name, exc)
|
|
84
68
|
|
|
85
69
|
def latest_version_specification(self) -> str | None:
|
|
86
70
|
"""Version specification of package pinned to the latest version.
|
|
@@ -23,29 +23,26 @@ class RustPackage(PackageBase):
|
|
|
23
23
|
cli: bool = False
|
|
24
24
|
|
|
25
25
|
@classmethod
|
|
26
|
-
def from_specification(
|
|
27
|
-
cls, spec: str, connection_timeout: int
|
|
28
|
-
) -> "RustPackage" | None:
|
|
26
|
+
def from_specification(cls, spec: str) -> "RustPackage" | None:
|
|
29
27
|
"""Parse a package specification string into a Package object.
|
|
30
28
|
|
|
31
|
-
|
|
29
|
+
Supported formats:
|
|
30
|
+
- `package`
|
|
31
|
+
- `package:1.2.3`
|
|
32
|
+
- `cli:package:1.2.3`
|
|
32
33
|
|
|
33
34
|
Args:
|
|
34
|
-
spec (str): The package specification string.
|
|
35
|
-
connection_timeout (int): Connection timeout in seconds.
|
|
35
|
+
spec (str): The package specification string (without quotations).
|
|
36
36
|
|
|
37
37
|
Returns:
|
|
38
38
|
"RustPackage" | None: A Package instance if parsing succeeds, otherwise None.
|
|
39
39
|
|
|
40
40
|
"""
|
|
41
|
-
clean_spec = spec.strip().strip('"').strip("'")
|
|
42
|
-
|
|
43
41
|
match = re.match(
|
|
44
42
|
r"^(?:(?P<cli>cli):)?(?P<name>[a-zA-Z0-9_-]+)(?::(?P<version>[^:]+))?$",
|
|
45
|
-
|
|
43
|
+
spec,
|
|
46
44
|
)
|
|
47
45
|
if not match:
|
|
48
|
-
logging.warning("Could not parse the package specification: %s", clean_spec)
|
|
49
46
|
return None
|
|
50
47
|
|
|
51
48
|
name = match.group("name")
|
|
@@ -53,39 +50,29 @@ class RustPackage(PackageBase):
|
|
|
53
50
|
cli = bool(match.group("cli"))
|
|
54
51
|
|
|
55
52
|
raw_spec = f":{version}" if version else None
|
|
56
|
-
latest_version = cls._get_latest_version(name, connection_timeout)
|
|
57
53
|
|
|
58
|
-
return cls(name=name, raw_spec=raw_spec,
|
|
54
|
+
return cls(name=name, raw_spec=raw_spec, cli=cli)
|
|
59
55
|
|
|
60
|
-
|
|
61
|
-
def _get_latest_version(package_name: str, connection_timeout: int) -> str | None:
|
|
56
|
+
def fetch_latest_version(self, connection_timeout: int) -> None:
|
|
62
57
|
"""Fetch the latest version of a package from crates.io.
|
|
63
58
|
|
|
64
59
|
Args:
|
|
65
|
-
package_name (str): Name of the package.
|
|
66
60
|
connection_timeout (int): Connection timeout in seconds.
|
|
67
|
-
|
|
68
|
-
Returns:
|
|
69
|
-
str | None: The latest version string if found, otherwise None.
|
|
70
|
-
|
|
71
61
|
"""
|
|
72
|
-
logging.debug("Fetching latest version for %s",
|
|
62
|
+
logging.debug("Fetching latest version for %s", self.name)
|
|
73
63
|
try:
|
|
74
|
-
url = CRATES_API_URL.format(package_name=
|
|
64
|
+
url = CRATES_API_URL.format(package_name=self.name)
|
|
75
65
|
response = requests.get(url, timeout=connection_timeout)
|
|
76
66
|
response.raise_for_status()
|
|
77
67
|
data = response.json()
|
|
78
68
|
latest_version = data.get("crate", {}).get("max_version")
|
|
79
|
-
|
|
69
|
+
if isinstance(latest_version, str):
|
|
70
|
+
self.latest_version = latest_version
|
|
80
71
|
|
|
81
72
|
except requests.exceptions.RequestException as exc:
|
|
82
|
-
logging.warning(
|
|
83
|
-
"Could not fetch latest version for %s: %s", package_name, exc
|
|
84
|
-
)
|
|
85
|
-
return None
|
|
73
|
+
logging.warning("Could not fetch latest version for %s: %s", self.name, exc)
|
|
86
74
|
except (KeyError, ValueError) as exc:
|
|
87
|
-
logging.warning("Could not parse response for %s: %s",
|
|
88
|
-
return None
|
|
75
|
+
logging.warning("Could not parse response for %s: %s", self.name, exc)
|
|
89
76
|
|
|
90
77
|
def latest_version_specification(self) -> str | None:
|
|
91
78
|
"""Version specification of package pinned to the latest version.
|
|
@@ -1,22 +1,25 @@
|
|
|
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
|
-
import io
|
|
5
4
|
import logging
|
|
6
|
-
import os
|
|
7
5
|
import re
|
|
8
|
-
import tempfile
|
|
9
6
|
from pathlib import Path
|
|
10
7
|
|
|
11
|
-
|
|
8
|
+
from ruamel.yaml import YAML
|
|
9
|
+
from ruamel.yaml.comments import CommentedSeq
|
|
10
|
+
from ruamel.yaml.error import YAMLError
|
|
12
11
|
|
|
13
12
|
from pre_commit_localupdate.error import PreCommitLocalUpdateError
|
|
14
|
-
from pre_commit_localupdate.io import load_config_file
|
|
13
|
+
from pre_commit_localupdate.io import YAMLFormat, load_config_file, save_config_file
|
|
15
14
|
from pre_commit_localupdate.packages import SUPPORTED_PACKAGES
|
|
16
15
|
|
|
17
16
|
|
|
18
17
|
def update_additional_dependencies(
|
|
19
|
-
file_path: Path,
|
|
18
|
+
file_path: Path,
|
|
19
|
+
connection_timeout: int,
|
|
20
|
+
yaml_format: YAMLFormat,
|
|
21
|
+
*,
|
|
22
|
+
dry_run: bool = False,
|
|
20
23
|
) -> bool:
|
|
21
24
|
"""Reads the pre-commit configuration file, identifies outdated packages
|
|
22
25
|
in local hooks, and updates them directly in the file.
|
|
@@ -27,10 +30,11 @@ def update_additional_dependencies(
|
|
|
27
30
|
Args:
|
|
28
31
|
file_path (Path): Path to the pre-commit configuration file.
|
|
29
32
|
connection_timeout (int): Connection timeout in seconds.
|
|
33
|
+
yaml_format (YAMLFormat): YAML file formatting parameters
|
|
30
34
|
dry_run (bool): Do not update the pre-commit configuration file.
|
|
31
35
|
|
|
32
36
|
Returns:
|
|
33
|
-
bool: True if the
|
|
37
|
+
bool: True if the dependencies needed updating, False otherwise.
|
|
34
38
|
|
|
35
39
|
Raises:
|
|
36
40
|
PreCommitLocalUpdateError: Error in parsing or updating pre-commit hooks
|
|
@@ -40,17 +44,20 @@ def update_additional_dependencies(
|
|
|
40
44
|
|
|
41
45
|
logging.debug("Parsing YAML content...")
|
|
42
46
|
|
|
43
|
-
yaml =
|
|
47
|
+
yaml = YAML()
|
|
44
48
|
yaml.preserve_quotes = True
|
|
45
|
-
yaml.indent(
|
|
49
|
+
yaml.indent(
|
|
50
|
+
mapping=yaml_format.indent_mapping,
|
|
51
|
+
sequence=yaml_format.indent_sequence,
|
|
52
|
+
offset=yaml_format.indent_offset,
|
|
53
|
+
)
|
|
46
54
|
yaml.default_flow_style = False
|
|
47
|
-
yaml.width =
|
|
55
|
+
yaml.width = yaml_format.line_width
|
|
48
56
|
yaml.explicit_start = False
|
|
49
57
|
yaml.allow_unicode = True
|
|
50
|
-
|
|
51
58
|
try:
|
|
52
59
|
config = yaml.load(yaml_content)
|
|
53
|
-
except
|
|
60
|
+
except YAMLError as exc:
|
|
54
61
|
raise PreCommitLocalUpdateError("Failed to parse YAML content.") from exc
|
|
55
62
|
|
|
56
63
|
if config is None:
|
|
@@ -59,6 +66,7 @@ def update_additional_dependencies(
|
|
|
59
66
|
)
|
|
60
67
|
|
|
61
68
|
update_required = False
|
|
69
|
+
FREEZE_PATTERN = re.compile(r"^#\s*freeze(\s|$)")
|
|
62
70
|
|
|
63
71
|
if "repos" in config:
|
|
64
72
|
logging.debug("Scanning repositories for local hooks")
|
|
@@ -81,35 +89,38 @@ def update_additional_dependencies(
|
|
|
81
89
|
|
|
82
90
|
package_type = SUPPORTED_PACKAGES[hook.get("language")]
|
|
83
91
|
|
|
84
|
-
for i,
|
|
85
|
-
|
|
92
|
+
for i, dep_str in enumerate(deps_list):
|
|
93
|
+
|
|
94
|
+
comment = None
|
|
95
|
+
if isinstance(deps_list, CommentedSeq):
|
|
96
|
+
comment = deps_list.ca.items.get(i, [None, None])[0]
|
|
86
97
|
|
|
87
|
-
if hasattr(
|
|
88
|
-
|
|
89
|
-
if
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
):
|
|
93
|
-
logging.debug("Skipping frozen dependency: %s", dep_str)
|
|
94
|
-
continue
|
|
98
|
+
if comment and hasattr(comment, "value"):
|
|
99
|
+
comment_text = comment.value.strip().lower()
|
|
100
|
+
if FREEZE_PATTERN.match(comment_text):
|
|
101
|
+
logging.debug("Skipping frozen dependency: %s", dep_str)
|
|
102
|
+
continue
|
|
95
103
|
|
|
96
104
|
logging.debug("Checking dependency: %s", dep_str)
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
)
|
|
105
|
+
clean_dep_str = dep_str.strip().strip('"').strip("'")
|
|
106
|
+
package = package_type.from_specification(clean_dep_str)
|
|
100
107
|
if not package:
|
|
108
|
+
logging.warning(
|
|
109
|
+
"Unsupported package specification: %s", dep_str
|
|
110
|
+
)
|
|
101
111
|
continue
|
|
102
112
|
|
|
113
|
+
package.fetch_latest_version(connection_timeout)
|
|
103
114
|
if not package.latest_version_specification():
|
|
104
115
|
continue
|
|
105
116
|
|
|
106
117
|
if (
|
|
107
118
|
package.latest_version_specification()
|
|
108
|
-
!= package.
|
|
119
|
+
!= package.original_version_specification()
|
|
109
120
|
):
|
|
110
|
-
logging.
|
|
121
|
+
logging.debug(
|
|
111
122
|
"%s needs updating to %s",
|
|
112
|
-
package.
|
|
123
|
+
package.original_version_specification(),
|
|
113
124
|
package.latest_version_specification(),
|
|
114
125
|
)
|
|
115
126
|
hook["additional_dependencies"][
|
|
@@ -121,41 +132,14 @@ def update_additional_dependencies(
|
|
|
121
132
|
logging.debug(
|
|
122
133
|
"%s is already correctly defined and up to date (%s).",
|
|
123
134
|
package.name,
|
|
124
|
-
package.
|
|
135
|
+
package.original_version_specification(),
|
|
125
136
|
)
|
|
126
137
|
else:
|
|
127
138
|
raise PreCommitLocalUpdateError("Failed to parse pre-commit hooks!")
|
|
128
139
|
|
|
129
140
|
if update_required:
|
|
130
|
-
if dry_run:
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
logging.debug("Writing modifications to disk...")
|
|
134
|
-
stream = io.StringIO()
|
|
135
|
-
yaml.dump(config, stream)
|
|
136
|
-
modified_body = stream.getvalue()
|
|
137
|
-
final_content = "".join(header_lines) + modified_body
|
|
138
|
-
|
|
139
|
-
try:
|
|
140
|
-
with tempfile.NamedTemporaryFile(
|
|
141
|
-
mode="w",
|
|
142
|
-
encoding="utf-8",
|
|
143
|
-
dir=file_path.parent,
|
|
144
|
-
delete=False,
|
|
145
|
-
) as tmp_file:
|
|
146
|
-
tmp_file.write(final_content)
|
|
147
|
-
os.fsync(tmp_file.fileno())
|
|
148
|
-
temp_file_path = tmp_file.name
|
|
149
|
-
|
|
150
|
-
os.replace(temp_file_path, file_path)
|
|
151
|
-
logging.info("Successfully updated %s", file_path)
|
|
152
|
-
return True
|
|
153
|
-
|
|
154
|
-
except OSError as exc:
|
|
155
|
-
if "temp_file_path" in locals() and os.path.exists(temp_file_path):
|
|
156
|
-
os.unlink(temp_file_path)
|
|
157
|
-
raise PreCommitLocalUpdateError(
|
|
158
|
-
f"IOError while writing to file {file_path}"
|
|
159
|
-
) from exc
|
|
141
|
+
if not dry_run:
|
|
142
|
+
save_config_file(file_path, config, header_lines, yaml)
|
|
143
|
+
return True
|
|
160
144
|
|
|
161
145
|
return False
|
|
@@ -24,12 +24,12 @@ dependencies = [
|
|
|
24
24
|
"ruamel-yaml>=0.19.1",
|
|
25
25
|
"packaging>=26.0",
|
|
26
26
|
]
|
|
27
|
-
description = "A CLI tool to automatically update additional dependencies within local
|
|
27
|
+
description = "A CLI tool to automatically update additional dependencies within local hooks in pre-commit config files."
|
|
28
28
|
dynamic = []
|
|
29
29
|
name = "pre-commit-localupdate"
|
|
30
30
|
readme = "README.md"
|
|
31
31
|
requires-python = "<3.15,>=3.11"
|
|
32
|
-
version = "0.
|
|
32
|
+
version = "0.5.0"
|
|
33
33
|
|
|
34
34
|
[project.scripts]
|
|
35
35
|
pre-commit-localupdate = "pre_commit_localupdate.__main__:main"
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = '0.4.1'
|
|
@@ -1,53 +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
|
-
from pathlib import Path
|
|
6
|
-
|
|
7
|
-
from pre_commit_localupdate.error import PreCommitLocalUpdateError
|
|
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
|
-
|
|
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
|
-
"""
|
|
22
|
-
logging.debug("Reading configuration file: %s", file_path)
|
|
23
|
-
|
|
24
|
-
raw_lines: list[str] = []
|
|
25
|
-
header_lines: list[str] = []
|
|
26
|
-
content_start_index: int = 0
|
|
27
|
-
|
|
28
|
-
try:
|
|
29
|
-
with file_path.open("r", encoding="utf-8") as f:
|
|
30
|
-
raw_lines = f.readlines()
|
|
31
|
-
except FileNotFoundError as exc:
|
|
32
|
-
error = f"File not found: {file_path}"
|
|
33
|
-
raise PreCommitLocalUpdateError(error) from exc
|
|
34
|
-
except OSError as exc:
|
|
35
|
-
error = f"IOError while reading file {file_path}"
|
|
36
|
-
raise PreCommitLocalUpdateError(error) from exc
|
|
37
|
-
|
|
38
|
-
logging.debug("Parsing file header...")
|
|
39
|
-
content_start_index = 0
|
|
40
|
-
for i, line in enumerate(raw_lines):
|
|
41
|
-
stripped: str = line.strip()
|
|
42
|
-
if stripped == "---":
|
|
43
|
-
logging.debug("YAML marker found.")
|
|
44
|
-
content_start_index = i + 1
|
|
45
|
-
break
|
|
46
|
-
if stripped and not stripped.startswith("#"):
|
|
47
|
-
content_start_index = i
|
|
48
|
-
break
|
|
49
|
-
|
|
50
|
-
header_lines = raw_lines[:content_start_index]
|
|
51
|
-
content: str = "".join(raw_lines[content_start_index:])
|
|
52
|
-
|
|
53
|
-
return header_lines, content
|
|
File without changes
|
{pre_commit_localupdate-0.4.1 → pre_commit_localupdate-0.5.0}/pre_commit_localupdate/error.py
RENAMED
|
File without changes
|
{pre_commit_localupdate-0.4.1 → pre_commit_localupdate-0.5.0}/pre_commit_localupdate/logs.py
RENAMED
|
File without changes
|
|
File without changes
|