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.
Files changed (20) hide show
  1. {pre_commit_localupdate-0.4.1 → pre_commit_localupdate-0.5.0}/PKG-INFO +38 -22
  2. {pre_commit_localupdate-0.4.1 → pre_commit_localupdate-0.5.0}/README.md +36 -20
  3. pre_commit_localupdate-0.5.0/pre_commit_localupdate/__init__.py +1 -0
  4. {pre_commit_localupdate-0.4.1 → pre_commit_localupdate-0.5.0}/pre_commit_localupdate/__main__.py +9 -3
  5. {pre_commit_localupdate-0.4.1 → pre_commit_localupdate-0.5.0}/pre_commit_localupdate/cli.py +31 -3
  6. pre_commit_localupdate-0.5.0/pre_commit_localupdate/io.py +121 -0
  7. {pre_commit_localupdate-0.4.1 → pre_commit_localupdate-0.5.0}/pre_commit_localupdate/packages/golang.py +14 -32
  8. {pre_commit_localupdate-0.4.1 → pre_commit_localupdate-0.5.0}/pre_commit_localupdate/packages/julia.py +42 -37
  9. {pre_commit_localupdate-0.4.1 → pre_commit_localupdate-0.5.0}/pre_commit_localupdate/packages/node.py +22 -38
  10. {pre_commit_localupdate-0.4.1 → pre_commit_localupdate-0.5.0}/pre_commit_localupdate/packages/package.py +16 -8
  11. {pre_commit_localupdate-0.4.1 → pre_commit_localupdate-0.5.0}/pre_commit_localupdate/packages/python.py +16 -32
  12. {pre_commit_localupdate-0.4.1 → pre_commit_localupdate-0.5.0}/pre_commit_localupdate/packages/rust.py +15 -28
  13. {pre_commit_localupdate-0.4.1 → pre_commit_localupdate-0.5.0}/pre_commit_localupdate/pre_commit_config.py +43 -59
  14. {pre_commit_localupdate-0.4.1 → pre_commit_localupdate-0.5.0}/pyproject.toml +2 -2
  15. pre_commit_localupdate-0.4.1/pre_commit_localupdate/__init__.py +0 -1
  16. pre_commit_localupdate-0.4.1/pre_commit_localupdate/io.py +0 -53
  17. {pre_commit_localupdate-0.4.1 → pre_commit_localupdate-0.5.0}/LICENSE +0 -0
  18. {pre_commit_localupdate-0.4.1 → pre_commit_localupdate-0.5.0}/pre_commit_localupdate/error.py +0 -0
  19. {pre_commit_localupdate-0.4.1 → pre_commit_localupdate-0.5.0}/pre_commit_localupdate/logs.py +0 -0
  20. {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.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.
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 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
+ 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
- ```shell
38
- usage: pre-commit-localupdate [-h] [--debug] [--dry-run] [-c CONFIG] [--timeout TIMEOUT] [--version]
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 Python, Julia, Rust, Go, and Node.js hooks in a pre-commit config
41
- file.
42
+ Automatically update additional dependencies within local hooks in a pre-commit config file.
42
43
 
43
44
  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
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: "Long strings are automatically folded into multilines by ruamel-yaml library!"
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: Run `JuliaFormatter.jl` against Julia source files
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: "Long strings are automatically folded into multilines by ruamel-yaml
124
- library!"
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: Run `JuliaFormatter.jl` against Julia source files
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
- A considerable amount of effort has been put into making `pre-commit-localupdate` respect the original style and preserve the comments in the pre-commit configuration files. Although it is currently folding long strings (for example, in description lines) and enforcing block indentation for better readability.
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
- - ruamel.yaml
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 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.
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
- ```shell
22
- usage: pre-commit-localupdate [-h] [--debug] [--dry-run] [-c CONFIG] [--timeout TIMEOUT] [--version]
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 Python, Julia, Rust, Go, and Node.js hooks in a pre-commit config
25
- file.
26
+ Automatically update additional dependencies within local hooks in a pre-commit config file.
26
27
 
27
28
  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
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: "Long strings are automatically folded into multilines by ruamel-yaml library!"
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: Run `JuliaFormatter.jl` against Julia source files
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: "Long strings are automatically folded into multilines by ruamel-yaml
108
- library!"
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: Run `JuliaFormatter.jl` against Julia source files
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
- A considerable amount of effort has been put into making `pre-commit-localupdate` respect the original style and preserve the comments in the pre-commit configuration files. Although it is currently folding long strings (for example, in description lines) and enforcing block indentation for better readability.
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
- - ruamel.yaml
147
- - requests
161
+ - `packaging`
162
+ - `requests`
163
+ - `ruamel.yaml`
148
164
 
149
165
  ## License
150
166
 
@@ -0,0 +1 @@
1
+ __version__ = '0.5.0'
@@ -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
- Path(args.config), args.timeout, dry_run=args.dry_run
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 Python, Julia, Rust, Go, and Node.js hooks in a pre-commit config file.",
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=str,
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
- 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)
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._/-]+)(@.+)?$", clean_spec)
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
- @staticmethod
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
- return latest_version if isinstance(latest_version, str) else None
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
- Handles formats like 'Example@0.4.1', 'Example@0.4', or just 'Example'.
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
- clean_spec = spec.strip().strip('"').strip("'")
39
- match = re.match(r"^([a-zA-Z0-9_]+)(?:@([a-zA-Z0-9._+,-]+))?$", clean_spec)
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(1)
46
- raw_spec = match.group(2)
47
- latest_version = cls._get_latest_version(name, connection_timeout)
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
- latest_version=latest_version,
70
+ uuid=uuid,
53
71
  )
54
72
 
55
- @staticmethod
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", package_name)
79
+ logging.debug("Fetching latest version for %s", self.name)
68
80
  try:
69
81
  url = JULIA_PKG_API_URL.format(
70
- package_name=package_name, package_name_first_letter=package_name[0]
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 not versions:
79
- return None
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", package_name, exc)
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
- Handles formats like 'react@4.17.21', 'react@>=18.0.0', '@scope/name@^1.2.3', or just 'react'.
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 clean_spec:
35
+ if not spec:
38
36
  return None
39
37
 
40
- parts = clean_spec.split("@")
41
-
42
- name = clean_spec
43
- raw_spec = None
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
- # Look for version from the end (handles scoped packages correctly).
46
- for i in range(len(parts) - 1, 0, -1):
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
- latest_version = cls._get_latest_version(name, connection_timeout)
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
- @staticmethod
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=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
- return latest_version if isinstance(latest_version, str) else None
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", package_name, exc)
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.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').
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, spec_string: str, connection_timeout: int
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 version_specification(self) -> str | None:
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}[unspecified version]"
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
- Handles formats like 'ansible==13.3.0', 'ansible>=1.0', or just 'ansible'.
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(clean_spec)
39
- except Exception as exc:
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
- @staticmethod
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", package_name)
54
+ logging.debug("Fetching latest version for %s", self.name)
68
55
  try:
69
- url = PYPI_API_URL.format(package_name=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
- return latest_version if isinstance(latest_version, str) else None
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", package_name, exc)
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
- Handles formats like 'package', 'package:1.2.3', or 'cli:package:1.2.3'.
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
- clean_spec,
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, latest_version=latest_version, cli=cli)
54
+ return cls(name=name, raw_spec=raw_spec, cli=cli)
59
55
 
60
- @staticmethod
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", package_name)
62
+ logging.debug("Fetching latest version for %s", self.name)
73
63
  try:
74
- url = CRATES_API_URL.format(package_name=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
- return latest_version if isinstance(latest_version, str) else None
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", package_name, exc)
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
- import ruamel.yaml
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, connection_timeout: int, *, dry_run: bool = False
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 file needed updating, False otherwise.
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 = ruamel.yaml.YAML()
47
+ yaml = YAML()
44
48
  yaml.preserve_quotes = True
45
- yaml.indent(mapping=2, sequence=4, offset=2)
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 = None
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 ruamel.yaml.error.YAMLError as exc:
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, dep_spec in enumerate(deps_list):
85
- dep_str = str(dep_spec)
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(deps_list, "ca") and deps_list.ca.items:
88
- comment_data = deps_list.ca.items.get(i)
89
- if comment_data:
90
- if re.match(
91
- r"^#\s*freeze(\s|$)", comment_data[0].value.lower()
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
- package = package_type.from_specification(
98
- dep_str, connection_timeout
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.version_specification()
119
+ != package.original_version_specification()
109
120
  ):
110
- logging.info(
121
+ logging.debug(
111
122
  "%s needs updating to %s",
112
- package.version_specification(),
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.version_specification(),
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
- return True
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 Python, Julia, Rust, Go, and Node.js hooks in pre-commit config files."
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.4.1"
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