pre-commit-localupdate 0.3.0__tar.gz → 0.4.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.3.0 → pre_commit_localupdate-0.4.0}/PKG-INFO +51 -59
  2. {pre_commit_localupdate-0.3.0 → pre_commit_localupdate-0.4.0}/README.md +49 -57
  3. pre_commit_localupdate-0.4.0/pre_commit_localupdate/__init__.py +1 -0
  4. pre_commit_localupdate-0.4.0/pre_commit_localupdate/__main__.py +42 -0
  5. {pre_commit_localupdate-0.3.0 → pre_commit_localupdate-0.4.0}/pre_commit_localupdate/cli.py +5 -3
  6. pre_commit_localupdate-0.4.0/pre_commit_localupdate/error.py +6 -0
  7. {pre_commit_localupdate-0.3.0 → pre_commit_localupdate-0.4.0}/pre_commit_localupdate/io.py +10 -10
  8. {pre_commit_localupdate-0.3.0 → pre_commit_localupdate-0.4.0}/pre_commit_localupdate/logs.py +1 -1
  9. {pre_commit_localupdate-0.3.0 → pre_commit_localupdate-0.4.0}/pre_commit_localupdate/packages/__init__.py +2 -0
  10. pre_commit_localupdate-0.4.0/pre_commit_localupdate/packages/golang.py +124 -0
  11. {pre_commit_localupdate-0.3.0 → pre_commit_localupdate-0.4.0}/pre_commit_localupdate/pre_commit_config.py +56 -31
  12. {pre_commit_localupdate-0.3.0 → pre_commit_localupdate-0.4.0}/pyproject.toml +2 -2
  13. pre_commit_localupdate-0.3.0/pre_commit_localupdate/__init__.py +0 -1
  14. pre_commit_localupdate-0.3.0/pre_commit_localupdate/__main__.py +0 -34
  15. {pre_commit_localupdate-0.3.0 → pre_commit_localupdate-0.4.0}/LICENSE +0 -0
  16. {pre_commit_localupdate-0.3.0 → pre_commit_localupdate-0.4.0}/pre_commit_localupdate/packages/julia.py +0 -0
  17. {pre_commit_localupdate-0.3.0 → pre_commit_localupdate-0.4.0}/pre_commit_localupdate/packages/node.py +0 -0
  18. {pre_commit_localupdate-0.3.0 → pre_commit_localupdate-0.4.0}/pre_commit_localupdate/packages/package.py +0 -0
  19. {pre_commit_localupdate-0.3.0 → pre_commit_localupdate-0.4.0}/pre_commit_localupdate/packages/python.py +0 -0
  20. {pre_commit_localupdate-0.3.0 → pre_commit_localupdate-0.4.0}/pre_commit_localupdate/packages/rust.py +0 -0
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pre-commit-localupdate
3
- Version: 0.3.0
4
- Summary: A CLI tool to automatically update additional dependencies within local Python, Julia, Rust, and Node.js hooks in pre-commit config files.
3
+ Version: 0.4.0
4
+ Summary: A CLI tool to automatically update additional dependencies within local Python, Julia, Rust, Go, and Node.js hooks in pre-commit config files.
5
5
  Author: M. Farzalipour Tabriz
6
6
  Classifier: Development Status :: 3 - Alpha
7
7
  Classifier: Environment :: Console
@@ -15,7 +15,7 @@ Description-Content-Type: text/markdown
15
15
 
16
16
  # pre-commit-localupdate
17
17
 
18
- A CLI tool to automatically update dependencies in `pre-commit-config.yml` files. It specifically targets `additional_dependencies` within local Python, Julia, Rust, and Node.js hooks to ensure your tooling stays up-to-date. It also adds version to unversioned packages and pins exact version of loosely defined ones.
18
+ 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
19
 
20
20
  ## Installation
21
21
 
@@ -54,19 +54,19 @@ pre-commit-localupdate --debug
54
54
  Given a `.pre-commit-config.yaml` with the following content:
55
55
 
56
56
  ```yaml
57
- # Comments in the file header are preserved
57
+ # File header is preserved. If there is no document start marker (---), it won't be added
58
58
  ---
59
59
  repos:
60
60
  # External hooks won't be touched. Use 'pre-commit autoupdate' command to update them
61
61
  - repo: https://github.com/pre-commit/pre-commit-hooks
62
62
  rev: v2.3.0
63
63
  hooks:
64
- - id: check-yaml
65
64
  - id: end-of-file-fixer
66
65
  - id: trailing-whitespace
66
+
67
67
  - repo: local
68
68
  hooks:
69
- # Comment about hooks are preserved
69
+ # All comment are preserved
70
70
  - id: black
71
71
  name: black
72
72
  description: "Long strings are automatically folded into multilines by ruamel-yaml library!"
@@ -78,51 +78,45 @@ repos:
78
78
  additional_dependencies:
79
79
  # Loose version definitions are pinned to an exact version
80
80
  - "black>=25.1.0"
81
-
82
- - id: mypy
83
- name: mypy
84
- description: 'Type checking'
85
- entry: mypy
86
- language: python
87
- types_or: [python, pyi]
88
- args: ["--strict"]
89
- require_serial: true
90
- minimum_pre_commit_version: '2.9.2'
81
+ # Updates can be prevented by adding a 'freeze' comment to them as:
82
+ - "white==0.1.0" # freeze
83
+
84
+ - id: julia-format
85
+ name: format julia code
86
+ description: Run `JuliaFormatter.jl` against Julia source files
87
+ language: julia
88
+ types: [julia]
89
+ entry: tools/formatter.jl
91
90
  additional_dependencies:
92
- # Double/single quoting style is preserved
93
- - 'mypy==1.18.1'
94
- # Version is added to packages with no version definition
95
- - "types-requests"
96
-
97
- - id: mypy2
98
- name: mypy
99
- description: 'Type checking'
100
- entry: mypy
101
- language: python
102
- types_or: [python, pyi]
103
- args: ["--strict"]
104
- require_serial: true
105
- minimum_pre_commit_version: '2.9.2'
91
+ # Double/single quoting style is preserved and version is added to packages with no version definition
92
+ - 'JuliaFormatter'
93
+
94
+ - id: mdbook-lint
95
+ name: mdbook-lint
96
+ description: rust package to lint markdown
97
+ entry: mdbook-lint lint --fix
98
+ language: rust
99
+ types: [markdown]
106
100
  # Updating packages defined in flow style is also supported
107
- additional_dependencies: ['mypy==1.18.1', "types-requests"]
101
+ additional_dependencies: ["cli:mdbook-lint"]
108
102
  ```
109
103
 
110
104
  Running `pre-commit-localupdate` will update the file to (hypothetical latest versions):
111
105
 
112
106
  ```yaml
113
- # Comments in the file header are preserved
107
+ # File header is preserved. If there is no document start marker (---), it won't be added
114
108
  ---
115
109
  repos:
116
110
  # External hooks won't be touched. Use 'pre-commit autoupdate' command to update them
117
111
  - repo: https://github.com/pre-commit/pre-commit-hooks
118
112
  rev: v2.3.0
119
113
  hooks:
120
- - id: check-yaml
121
114
  - id: end-of-file-fixer
122
115
  - id: trailing-whitespace
116
+
123
117
  - repo: local
124
118
  hooks:
125
- # Comment about hooks are preserved
119
+ # All comment are preserved
126
120
  - id: black
127
121
  name: black
128
122
  description: "Long strings are automatically folded into multilines by ruamel-yaml
@@ -135,35 +129,33 @@ repos:
135
129
  additional_dependencies:
136
130
  # Loose version definitions are pinned to an exact version
137
131
  - "black==26.1.0"
138
-
139
- - id: mypy
140
- name: mypy
141
- description: 'Type checking'
142
- entry: mypy
143
- language: python
144
- types_or: [python, pyi]
145
- args: ["--strict"]
146
- require_serial: true
147
- minimum_pre_commit_version: '2.9.2'
132
+ # Updates can be prevented by adding a 'freeze' comment to them as:
133
+ - "white==0.1.0" # freeze
134
+
135
+ - id: julia-format
136
+ name: format julia code
137
+ description: Run `JuliaFormatter.jl` against Julia source files
138
+ language: julia
139
+ types: [julia]
140
+ entry: tools/formatter.jl
148
141
  additional_dependencies:
149
- # Double/single quoting style is preserved
150
- - 'mypy==1.19.1'
151
- # Version is added to packages with no version definition
152
- - "types-requests==2.32.4.20260107"
153
-
154
- - id: mypy2
155
- name: mypy
156
- description: 'Type checking'
157
- entry: mypy
158
- language: python
159
- types_or: [python, pyi]
160
- args: ["--strict"]
161
- require_serial: true
162
- minimum_pre_commit_version: '2.9.2'
142
+ # Double/single quoting style is preserved and version is added to packages with no version definition
143
+ - 'JuliaFormatter@2.3.0'
144
+
145
+ - id: mdbook-lint
146
+ name: mdbook-lint
147
+ description: rust package to lint markdown
148
+ entry: mdbook-lint lint --fix
149
+ language: rust
150
+ types: [markdown]
163
151
  # Updating packages defined in flow style is also supported
164
- additional_dependencies: ['mypy==1.19.1', "types-requests==2.32.4.20260107"]
152
+ additional_dependencies: ["cli:mdbook-lint:0.14.2"]
165
153
  ```
166
154
 
155
+ ## Formatting
156
+
157
+ 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.
158
+
167
159
  ## Requirements
168
160
 
169
161
  - ruamel.yaml
@@ -1,6 +1,6 @@
1
1
  # pre-commit-localupdate
2
2
 
3
- A CLI tool to automatically update dependencies in `pre-commit-config.yml` files. It specifically targets `additional_dependencies` within local Python, Julia, Rust, and Node.js hooks to ensure your tooling stays up-to-date. It also adds version to unversioned packages and pins exact version of loosely defined ones.
3
+ A CLI tool to automatically update dependencies in `pre-commit-config.yml` files. It specifically targets `additional_dependencies` within local Python, Julia, Rust, Go, and Node.js hooks to ensure your tooling stays up-to-date. It also adds version to unversioned packages and pins exact version of loosely defined ones. You can also pin a specific package to an older version by adding a `# freeze` comment.
4
4
 
5
5
  ## Installation
6
6
 
@@ -39,19 +39,19 @@ pre-commit-localupdate --debug
39
39
  Given a `.pre-commit-config.yaml` with the following content:
40
40
 
41
41
  ```yaml
42
- # Comments in the file header are preserved
42
+ # File header is preserved. If there is no document start marker (---), it won't be added
43
43
  ---
44
44
  repos:
45
45
  # External hooks won't be touched. Use 'pre-commit autoupdate' command to update them
46
46
  - repo: https://github.com/pre-commit/pre-commit-hooks
47
47
  rev: v2.3.0
48
48
  hooks:
49
- - id: check-yaml
50
49
  - id: end-of-file-fixer
51
50
  - id: trailing-whitespace
51
+
52
52
  - repo: local
53
53
  hooks:
54
- # Comment about hooks are preserved
54
+ # All comment are preserved
55
55
  - id: black
56
56
  name: black
57
57
  description: "Long strings are automatically folded into multilines by ruamel-yaml library!"
@@ -63,51 +63,45 @@ repos:
63
63
  additional_dependencies:
64
64
  # Loose version definitions are pinned to an exact version
65
65
  - "black>=25.1.0"
66
-
67
- - id: mypy
68
- name: mypy
69
- description: 'Type checking'
70
- entry: mypy
71
- language: python
72
- types_or: [python, pyi]
73
- args: ["--strict"]
74
- require_serial: true
75
- minimum_pre_commit_version: '2.9.2'
66
+ # Updates can be prevented by adding a 'freeze' comment to them as:
67
+ - "white==0.1.0" # freeze
68
+
69
+ - id: julia-format
70
+ name: format julia code
71
+ description: Run `JuliaFormatter.jl` against Julia source files
72
+ language: julia
73
+ types: [julia]
74
+ entry: tools/formatter.jl
76
75
  additional_dependencies:
77
- # Double/single quoting style is preserved
78
- - 'mypy==1.18.1'
79
- # Version is added to packages with no version definition
80
- - "types-requests"
81
-
82
- - id: mypy2
83
- name: mypy
84
- description: 'Type checking'
85
- entry: mypy
86
- language: python
87
- types_or: [python, pyi]
88
- args: ["--strict"]
89
- require_serial: true
90
- minimum_pre_commit_version: '2.9.2'
76
+ # Double/single quoting style is preserved and version is added to packages with no version definition
77
+ - 'JuliaFormatter'
78
+
79
+ - id: mdbook-lint
80
+ name: mdbook-lint
81
+ description: rust package to lint markdown
82
+ entry: mdbook-lint lint --fix
83
+ language: rust
84
+ types: [markdown]
91
85
  # Updating packages defined in flow style is also supported
92
- additional_dependencies: ['mypy==1.18.1', "types-requests"]
86
+ additional_dependencies: ["cli:mdbook-lint"]
93
87
  ```
94
88
 
95
89
  Running `pre-commit-localupdate` will update the file to (hypothetical latest versions):
96
90
 
97
91
  ```yaml
98
- # Comments in the file header are preserved
92
+ # File header is preserved. If there is no document start marker (---), it won't be added
99
93
  ---
100
94
  repos:
101
95
  # External hooks won't be touched. Use 'pre-commit autoupdate' command to update them
102
96
  - repo: https://github.com/pre-commit/pre-commit-hooks
103
97
  rev: v2.3.0
104
98
  hooks:
105
- - id: check-yaml
106
99
  - id: end-of-file-fixer
107
100
  - id: trailing-whitespace
101
+
108
102
  - repo: local
109
103
  hooks:
110
- # Comment about hooks are preserved
104
+ # All comment are preserved
111
105
  - id: black
112
106
  name: black
113
107
  description: "Long strings are automatically folded into multilines by ruamel-yaml
@@ -120,35 +114,33 @@ repos:
120
114
  additional_dependencies:
121
115
  # Loose version definitions are pinned to an exact version
122
116
  - "black==26.1.0"
123
-
124
- - id: mypy
125
- name: mypy
126
- description: 'Type checking'
127
- entry: mypy
128
- language: python
129
- types_or: [python, pyi]
130
- args: ["--strict"]
131
- require_serial: true
132
- minimum_pre_commit_version: '2.9.2'
117
+ # Updates can be prevented by adding a 'freeze' comment to them as:
118
+ - "white==0.1.0" # freeze
119
+
120
+ - id: julia-format
121
+ name: format julia code
122
+ description: Run `JuliaFormatter.jl` against Julia source files
123
+ language: julia
124
+ types: [julia]
125
+ entry: tools/formatter.jl
133
126
  additional_dependencies:
134
- # Double/single quoting style is preserved
135
- - 'mypy==1.19.1'
136
- # Version is added to packages with no version definition
137
- - "types-requests==2.32.4.20260107"
138
-
139
- - id: mypy2
140
- name: mypy
141
- description: 'Type checking'
142
- entry: mypy
143
- language: python
144
- types_or: [python, pyi]
145
- args: ["--strict"]
146
- require_serial: true
147
- minimum_pre_commit_version: '2.9.2'
127
+ # Double/single quoting style is preserved and version is added to packages with no version definition
128
+ - 'JuliaFormatter@2.3.0'
129
+
130
+ - id: mdbook-lint
131
+ name: mdbook-lint
132
+ description: rust package to lint markdown
133
+ entry: mdbook-lint lint --fix
134
+ language: rust
135
+ types: [markdown]
148
136
  # Updating packages defined in flow style is also supported
149
- additional_dependencies: ['mypy==1.19.1', "types-requests==2.32.4.20260107"]
137
+ additional_dependencies: ["cli:mdbook-lint:0.14.2"]
150
138
  ```
151
139
 
140
+ ## Formatting
141
+
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.
143
+
152
144
  ## Requirements
153
145
 
154
146
  - ruamel.yaml
@@ -0,0 +1 @@
1
+ __version__ = '0.4.0'
@@ -0,0 +1,42 @@
1
+ # SPDX-FileCopyrightText: 2026 M. Farzalipour Tabriz, Max Planck Institute for Physics
2
+ # SPDX-License-Identifier: LGPL-3.0-or-later
3
+
4
+ """Module to update additional_dependencies in .pre-commit-config.yaml.
5
+
6
+ This script reads the .pre-commit-config.yaml file, identifies hooks defined
7
+ under 'repo: local' and updates the packages listed in 'additional_dependencies'
8
+ to their latest versions.
9
+
10
+ """
11
+
12
+ import logging
13
+ import sys
14
+ from pathlib import Path
15
+
16
+ from pre_commit_localupdate.cli import parse_args
17
+ from pre_commit_localupdate.error import PreCommitLocalUpdateError
18
+ from pre_commit_localupdate.logs import setup_logging
19
+ from pre_commit_localupdate.pre_commit_config import update_additional_dependencies
20
+
21
+
22
+ def main() -> None:
23
+ """Main entry point for the script."""
24
+ args = parse_args()
25
+ setup_logging(debug=args.debug)
26
+
27
+ try:
28
+ if update_additional_dependencies(Path(args.config), dry_run=args.dry_run):
29
+ if args.dry_run:
30
+ sys.exit(1)
31
+ logging.info("Done.")
32
+ else:
33
+ logging.info(
34
+ "Additional dependencies of local hooks are already up to date.",
35
+ )
36
+ except PreCommitLocalUpdateError as exc:
37
+ logging.exception(exc)
38
+ sys.exit(2)
39
+
40
+
41
+ if __name__ == "__main__":
42
+ main()
@@ -9,7 +9,7 @@ from pre_commit_localupdate import __version__
9
9
  def parse_args() -> argparse.Namespace:
10
10
  """Parse command line arguments."""
11
11
  parser = argparse.ArgumentParser(
12
- description="Automatically update additional dependencies within local Python, Julia, Rust, and Node.js hooks in pre-commit config files."
12
+ description="Automatically update additional dependencies within local Python, Julia, Rust, Go, and Node.js hooks in a pre-commit config file.",
13
13
  )
14
14
  parser.add_argument(
15
15
  "--debug",
@@ -19,7 +19,7 @@ def parse_args() -> argparse.Namespace:
19
19
  parser.add_argument(
20
20
  "--dry-run",
21
21
  action="store_true",
22
- help="Dry run mode. Do not update the file and exit with a non-zero code if the configuration files require an update.",
22
+ help="Dry run mode. Do not update the file and exit with a non-zero code if the configuration file require an update.",
23
23
  )
24
24
  parser.add_argument(
25
25
  "-c",
@@ -29,6 +29,8 @@ def parse_args() -> argparse.Namespace:
29
29
  default=".pre-commit-config.yaml",
30
30
  )
31
31
  parser.add_argument(
32
- "--version", action="version", version=f"pre-commit-localupdate {__version__}"
32
+ "--version",
33
+ action="version",
34
+ version=f"pre-commit-localupdate {__version__}",
33
35
  )
34
36
  return parser.parse_args()
@@ -0,0 +1,6 @@
1
+ # SPDX-FileCopyrightText: 2026 M. Farzalipour Tabriz, Max Planck Institute for Physics
2
+ # SPDX-License-Identifier: LGPL-3.0-or-later
3
+
4
+
5
+ class PreCommitLocalUpdateError(Exception):
6
+ """Exceptions for errors encountered during the pre-commit local update process."""
@@ -2,28 +2,28 @@
2
2
  # SPDX-License-Identifier: LGPL-3.0-or-later
3
3
 
4
4
  import logging
5
- import sys
6
5
  from pathlib import Path
7
- from typing import List, Tuple
8
6
 
7
+ from pre_commit_localupdate.error import PreCommitLocalUpdateError
9
8
 
10
- def load_config_file(file_path: Path) -> Tuple[List[str], str]:
9
+
10
+ def load_config_file(file_path: Path) -> tuple[list[str], str]:
11
11
  """Reads pre-commit configuration file and returns its header lines and content."""
12
12
  logging.debug("Reading configuration file: %s", file_path)
13
13
 
14
- raw_lines: List[str] = []
15
- header_lines: List[str] = []
14
+ raw_lines: list[str] = []
15
+ header_lines: list[str] = []
16
16
  content_start_index: int = 0
17
17
 
18
18
  try:
19
19
  with file_path.open("r", encoding="utf-8") as f:
20
20
  raw_lines = f.readlines()
21
- except FileNotFoundError:
22
- logging.exception("File not found: %s", file_path)
23
- sys.exit(2)
21
+ except FileNotFoundError as exc:
22
+ error = f"File not found: {file_path}"
23
+ raise PreCommitLocalUpdateError(error) from exc
24
24
  except OSError as exc:
25
- logging.exception("IOError while reading file %s: %s", file_path, exc)
26
- sys.exit(2)
25
+ error = f"IOError while reading file {file_path}"
26
+ raise PreCommitLocalUpdateError(error) from exc
27
27
 
28
28
  logging.debug("Parsing file header...")
29
29
  content_start_index = 0
@@ -5,7 +5,7 @@ import logging
5
5
  import sys
6
6
 
7
7
 
8
- def setup_logging(debug: bool = False) -> None:
8
+ def setup_logging(*, debug: bool = False) -> None:
9
9
  """Configure logging based on the debug flag."""
10
10
  level = logging.DEBUG if debug else logging.INFO
11
11
  logging.basicConfig(
@@ -1,6 +1,7 @@
1
1
  # SPDX-FileCopyrightText: 2026 M. Farzalipour Tabriz, Max Planck Institute for Physics
2
2
  # SPDX-License-Identifier: LGPL-3.0-or-later
3
3
 
4
+ from .golang import GoPackage
4
5
  from .julia import JuliaPackage
5
6
  from .node import NodePackage
6
7
  from .package import PackageBase
@@ -8,6 +9,7 @@ from .python import PyPackage
8
9
  from .rust import RustPackage
9
10
 
10
11
  SUPPORTED_PACKAGES: dict[str, type[PackageBase]] = {
12
+ "golang": GoPackage,
11
13
  "julia": JuliaPackage,
12
14
  "node": NodePackage,
13
15
  "python": PyPackage,
@@ -0,0 +1,124 @@
1
+ # SPDX-FileCopyrightText: 2026 M. Farzalipour Tabriz, Max Planck Institute for Physics
2
+ # SPDX-License-Identifier: LGPL-3.0-or-later
3
+
4
+ import logging
5
+ import re
6
+
7
+ import requests
8
+
9
+ from .package import PackageBase
10
+
11
+ # Go Proxy API endpoints
12
+ GO_PKG_LATEST_VERSION_API_URL = "https://proxy.golang.org/{package_path}/@latest"
13
+
14
+ REQUEST_TIMEOUT = 10
15
+
16
+
17
+ class GoPackage(PackageBase):
18
+ """Represents a Go module."""
19
+
20
+ @classmethod
21
+ def from_specification(package, spec: str) -> "GoPackage" | None:
22
+ """Parse a Go module specification string into a Package object.
23
+
24
+ Handles formats like:
25
+ - 'mvdan.cc/sh' (Module path only)
26
+ - 'mvdan.cc/sh@v1.3.0' (Module path with specific version)
27
+ - 'mvdan.cc/sh/v3' (Module path with major version suffix)
28
+ - 'mvdan.cc/sh/v3@v3.10.0' (Major suffix with version)
29
+ - 'mvdan.cc/sh/v3/cmd/shfmt' (Submodule)
30
+ - 'mvdan.cc/sh/v3/cmd/shfmt@v3.10.0' (Submodule with version)
31
+
32
+ Args:
33
+ package: The package specification string.
34
+
35
+ Returns:
36
+ A Package instance if parsing succeeds, otherwise None.
37
+ """
38
+ clean_spec = spec.strip().strip('"').strip("'")
39
+
40
+ match = re.match(r"^([a-zA-Z0-9._/-]+)(@.+)?$", clean_spec)
41
+
42
+ if not match:
43
+ logging.warning(
44
+ "Could not parse the Go package specification: %s", clean_spec
45
+ )
46
+ return None
47
+
48
+ name = match.group(1)
49
+ raw_spec = match.group(2)
50
+
51
+ module_path = package._resolve_module_path(name)
52
+ latest_version = package._fetch_latest_version(module_path)
53
+
54
+ return package(
55
+ name=name,
56
+ raw_spec=raw_spec,
57
+ latest_version=latest_version,
58
+ )
59
+
60
+ @staticmethod
61
+ def _resolve_module_path(package_path: str) -> str:
62
+ """
63
+ Resolve a submodule path to a module path.
64
+
65
+ If the path contains a major version suffix (e.g., /v2, /v3) followed by
66
+ additional path segments, it trims the path at the suffix.
67
+
68
+ Args:
69
+ package_path: The full submodule path.
70
+
71
+ Returns:
72
+ The resolved module path.
73
+ """
74
+
75
+ match = re.search(r"/v([2-9]|[1-9][0-9]+)(?=/)", package_path)
76
+
77
+ if match:
78
+ return package_path[: match.end()]
79
+
80
+ return package_path
81
+
82
+ @staticmethod
83
+ def _fetch_latest_version(package_path: str) -> str | None:
84
+ """Fetch the latest version of a Go module from proxy.golang.org.
85
+
86
+ Args:
87
+ package_path: The module path (e.g., mvdan.cc/sh/v3).
88
+
89
+ Returns:
90
+ The latest version string (e.g., v3.10.0) if found, otherwise None.
91
+ """
92
+ logging.debug("Fetching latest version for %s", package_path)
93
+ try:
94
+ url = GO_PKG_LATEST_VERSION_API_URL.format(package_path=package_path)
95
+
96
+ response = requests.get(url, timeout=REQUEST_TIMEOUT)
97
+ response.raise_for_status()
98
+
99
+ data = response.json()
100
+ version = data.get("Version")
101
+
102
+ if isinstance(version, str):
103
+ return version
104
+
105
+ return None
106
+ except requests.exceptions.RequestException as exc:
107
+ logging.warning(
108
+ "Could not fetch latest version for %s: %s", package_path, exc
109
+ )
110
+ return None
111
+ except (KeyError, ValueError) as exc:
112
+ logging.warning("Could not parse response for %s: %s", package_path, exc)
113
+ return None
114
+
115
+ def latest_version_specification(self) -> str | None:
116
+ """Version specification of package pinned to the latest version.
117
+
118
+ Returns:
119
+ The latest version specification string if found, otherwise None.
120
+ """
121
+ if not self.latest_version:
122
+ return None
123
+
124
+ return f"{self.name}@{self.latest_version}"
@@ -3,11 +3,14 @@
3
3
 
4
4
  import io
5
5
  import logging
6
- import sys
6
+ import os
7
+ import re
8
+ import tempfile
7
9
  from pathlib import Path
8
10
 
9
11
  import ruamel.yaml
10
12
 
13
+ from pre_commit_localupdate.error import PreCommitLocalUpdateError
11
14
  from pre_commit_localupdate.io import load_config_file
12
15
  from pre_commit_localupdate.packages import SUPPORTED_PACKAGES
13
16
 
@@ -21,12 +24,12 @@ def update_additional_dependencies(file_path: Path, *, dry_run: bool = False) ->
21
24
 
22
25
  Args:
23
26
  file_path: Path to the pre-commit configuration file.
27
+ dry_run: Do not update the pre-commit configuration file.
24
28
 
25
29
  Returns:
26
- True if the file was modified and successfully written, False otherwise.
30
+ True if the file needed updating, False otherwise.
27
31
 
28
32
  """
29
-
30
33
  header_lines, yaml_content = load_config_file(file_path)
31
34
 
32
35
  logging.debug("Parsing YAML content...")
@@ -41,13 +44,13 @@ def update_additional_dependencies(file_path: Path, *, dry_run: bool = False) ->
41
44
 
42
45
  try:
43
46
  config = yaml.load(yaml_content)
44
- except Exception as exc:
45
- logging.exception("Failed to parse YAML content from %s: %s", file_path, exc)
46
- sys.exit(2)
47
+ except ruamel.yaml.error.YAMLError as exc:
48
+ raise PreCommitLocalUpdateError("Failed to parse YAML content.") from exc
47
49
 
48
50
  if config is None:
49
- logging.warning("Configuration file is empty or could not be parsed.")
50
- sys.exit(2)
51
+ raise PreCommitLocalUpdateError(
52
+ "Configuration file is empty or could not be parsed.",
53
+ )
51
54
 
52
55
  update_required = False
53
56
 
@@ -65,18 +68,27 @@ def update_additional_dependencies(file_path: Path, *, dry_run: bool = False) ->
65
68
  if hook.get("language") not in SUPPORTED_PACKAGES:
66
69
  continue
67
70
 
68
- if "additional_dependencies" not in hook:
69
- continue
71
+ deps_list = hook.get("additional_dependencies")
70
72
 
71
- deps_list = list(hook["additional_dependencies"])
73
+ if not deps_list:
74
+ continue
72
75
 
73
- Package = SUPPORTED_PACKAGES[hook.get("language")]
76
+ package_type = SUPPORTED_PACKAGES[hook.get("language")]
74
77
 
75
78
  for i, dep_spec in enumerate(deps_list):
76
79
  dep_str = str(dep_spec)
77
80
 
81
+ if hasattr(deps_list, "ca") and deps_list.ca.items:
82
+ comment_data = deps_list.ca.items.get(i)
83
+ if comment_data:
84
+ if re.match(
85
+ r"^#\s*freeze(\s|$)", comment_data[0].value.lower()
86
+ ):
87
+ logging.debug("Skipping frozen dependency: %s", dep_str)
88
+ continue
89
+
78
90
  logging.debug("Checking dependency: %s", dep_str)
79
- package = Package.from_specification(dep_str)
91
+ package = package_type.from_specification(dep_str)
80
92
  if not package:
81
93
  continue
82
94
 
@@ -103,26 +115,39 @@ def update_additional_dependencies(file_path: Path, *, dry_run: bool = False) ->
103
115
  package.name,
104
116
  package.version_specification(),
105
117
  )
118
+ else:
119
+ raise PreCommitLocalUpdateError("Failed to parse pre-commit hooks!")
106
120
 
107
121
  if update_required:
108
122
  if dry_run:
109
- sys.exit(1)
110
- else:
111
- logging.debug("Writing modifications to disk")
112
- try:
113
- stream = io.StringIO()
114
- yaml.dump(config, stream)
115
- modified_body = stream.getvalue()
116
- final_content = "".join(header_lines) + modified_body
117
-
118
- with file_path.open("w", encoding="utf-8") as f:
119
- f.write(final_content)
120
- logging.info("Successfully updated %s", file_path)
121
- return True
122
- except OSError as exc:
123
- logging.exception(
124
- "IOError while writing to file %s: %s", file_path, exc
125
- )
126
- return False
123
+ return True
124
+
125
+ logging.debug("Writing modifications to disk...")
126
+ stream = io.StringIO()
127
+ yaml.dump(config, stream)
128
+ modified_body = stream.getvalue()
129
+ final_content = "".join(header_lines) + modified_body
130
+
131
+ try:
132
+ with tempfile.NamedTemporaryFile(
133
+ mode="w",
134
+ encoding="utf-8",
135
+ dir=file_path.parent,
136
+ delete=False,
137
+ ) as tmp_file:
138
+ tmp_file.write(final_content)
139
+ os.fsync(tmp_file.fileno())
140
+ temp_file_path = tmp_file.name
141
+
142
+ os.replace(temp_file_path, file_path)
143
+ logging.info("Successfully updated %s", file_path)
144
+ return True
145
+
146
+ except OSError as exc:
147
+ if "temp_file_path" in locals() and os.path.exists(temp_file_path):
148
+ os.unlink(temp_file_path)
149
+ raise PreCommitLocalUpdateError(
150
+ f"IOError while writing to file {file_path}"
151
+ ) from exc
127
152
 
128
153
  return False
@@ -23,12 +23,12 @@ dependencies = [
23
23
  "requests>=2.32.5",
24
24
  "ruamel-yaml>=0.19.1",
25
25
  ]
26
- description = "A CLI tool to automatically update additional dependencies within local Python, Julia, Rust, and Node.js hooks in pre-commit config files."
26
+ 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
27
  dynamic = []
28
28
  name = "pre-commit-localupdate"
29
29
  readme = "README.md"
30
30
  requires-python = "<3.15,>=3.11"
31
- version = "0.3.0"
31
+ version = "0.4.0"
32
32
 
33
33
  [project.scripts]
34
34
  pre-commit-localupdate = "pre_commit_localupdate.__main__:main"
@@ -1 +0,0 @@
1
- __version__ = '0.3.0'
@@ -1,34 +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
- """Module to update Python additional_dependencies in .pre-commit-config.yaml.
5
-
6
- This script reads the .pre-commit-config.yaml file, identifies hooks defined
7
- under 'repo: local' that use 'language: python', and updates the packages
8
- listed in 'additional_dependencies' to their latest versions available on PyPI.
9
-
10
- It preserves the original file formatting, comments, and structure, only
11
- modifying the version strings within the dependency lines.
12
- """
13
-
14
- import logging
15
- from pathlib import Path
16
-
17
- from pre_commit_localupdate.cli import parse_args
18
- from pre_commit_localupdate.logs import setup_logging
19
- from pre_commit_localupdate.pre_commit_config import update_additional_dependencies
20
-
21
-
22
- def main() -> None:
23
- """Main entry point for the script."""
24
- args = parse_args()
25
- setup_logging(args.debug)
26
-
27
- if update_additional_dependencies(Path(args.config), dry_run=args.dry_run):
28
- logging.info("Done.")
29
- else:
30
- logging.info("File content is already up to date.")
31
-
32
-
33
- if __name__ == "__main__":
34
- main()