pre-commit-localupdate 0.1.0__tar.gz → 0.2.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.
@@ -1,10 +1,25 @@
1
+ Metadata-Version: 2.1
2
+ Name: pre-commit-localupdate
3
+ Version: 0.2.0
4
+ Summary: A CLI tool to automatically update additional dependencies within local Python and Node.js hooks in pre-commit config files.
5
+ Author: M. Farzalipour Tabriz
6
+ Classifier: Development Status :: 3 - Alpha
7
+ Classifier: Environment :: Console
8
+ Classifier: Intended Audience :: Developers
9
+ Classifier: Programming Language :: Python :: 3
10
+ Project-URL: source, https://gitlab.mpcdf.mpg.de/tbz/pre-commit-localupdate.git
11
+ Requires-Python: <3.15,>=3.11
12
+ Requires-Dist: requests>=2.32.5
13
+ Requires-Dist: ruamel-yaml>=0.19.1
14
+ Description-Content-Type: text/markdown
15
+
1
16
  # pre-commit-localupdate
2
17
 
3
- A CLI tool to automatically update dependencies in `pre-commit-config.yml` files. It specifically targets `additional_dependencies` within local Python and Node.js hooks to ensure your tooling stays up-to-date. It also adds version to unversioned packages and pins exact version of loosely defined ones.
18
+ A CLI tool to automatically update dependencies in `pre-commit-config.yml` files. It specifically targets `additional_dependencies` within local Python, Julia, and Node.js hooks to ensure your tooling stays up-to-date. It also adds version to unversioned packages and pins exact version of loosely defined ones.
4
19
 
5
20
  ## Installation
6
21
 
7
- ```bash
22
+ ```shell
8
23
  pip install pre-commit-localupdate
9
24
  ```
10
25
 
@@ -12,16 +27,28 @@ pip install pre-commit-localupdate
12
27
 
13
28
  To check and update the `additional_dependencies` in your `.pre-commit-config.yaml` file, simply run:
14
29
 
15
- ```bash
30
+ ```shell
16
31
  pre-commit-localupdate
17
32
  ```
18
33
 
19
34
  By default, the tool looks for `.pre-commit-config.yaml` in the current directory. To specify a custom file path, use the `-c` or `--config` argument:
20
35
 
21
- ```bash
36
+ ```shell
22
37
  pre-commit-localupdate --config path/to/.pre-commit-config.yaml
23
38
  ```
24
39
 
40
+ To check the dependencies without updating, use the `--dry-run` flag. The command will exit with a non-zero status code if any package needs updating:
41
+
42
+ ```shell
43
+ pre-commit-localupdate --dry-run
44
+ ```
45
+
46
+ To get debug output use `--debug` flag:
47
+
48
+ ```shell
49
+ pre-commit-localupdate --debug
50
+ ```
51
+
25
52
  ## Example
26
53
 
27
54
  Given a `.pre-commit-config.yaml` with the following content:
@@ -30,6 +57,13 @@ Given a `.pre-commit-config.yaml` with the following content:
30
57
  # Comments in the file header are preserved
31
58
  ---
32
59
  repos:
60
+ # External hooks won't be touched. Use 'pre-commit autoupdate' command to update them
61
+ - repo: https://github.com/pre-commit/pre-commit-hooks
62
+ rev: v2.3.0
63
+ hooks:
64
+ - id: check-yaml
65
+ - id: end-of-file-fixer
66
+ - id: trailing-whitespace
33
67
  - repo: local
34
68
  hooks:
35
69
  # Comment about hooks are preserved
@@ -42,7 +76,7 @@ repos:
42
76
  require_serial: true
43
77
  types_or: [python, pyi]
44
78
  additional_dependencies:
45
- # Loose version definitions will be pinned to an exact version
79
+ # Loose version definitions are pinned to an exact version
46
80
  - "black>=25.1.0"
47
81
 
48
82
  - id: mypy
@@ -79,6 +113,13 @@ Running `pre-commit-localupdate` will update the file to (hypothetical latest ve
79
113
  # Comments in the file header are preserved
80
114
  ---
81
115
  repos:
116
+ # External hooks won't be touched. Use 'pre-commit autoupdate' command to update them
117
+ - repo: https://github.com/pre-commit/pre-commit-hooks
118
+ rev: v2.3.0
119
+ hooks:
120
+ - id: check-yaml
121
+ - id: end-of-file-fixer
122
+ - id: trailing-whitespace
82
123
  - repo: local
83
124
  hooks:
84
125
  # Comment about hooks are preserved
@@ -92,7 +133,7 @@ repos:
92
133
  require_serial: true
93
134
  types_or: [python, pyi]
94
135
  additional_dependencies:
95
- # Loose version definitions should be pinned
136
+ # Loose version definitions are pinned to an exact version
96
137
  - "black==26.1.0"
97
138
 
98
139
  - id: mypy
@@ -1,25 +1,10 @@
1
- Metadata-Version: 2.1
2
- Name: pre-commit-localupdate
3
- Version: 0.1.0
4
- Summary: A CLI tool to automatically update `additional_dependencies` within local Python hooks in `pre-commit-config.yml` files.
5
- Author: M. Farzalipour Tabriz
6
- Classifier: Development Status :: 3 - Alpha
7
- Classifier: Environment :: Console
8
- Classifier: Intended Audience :: Developers
9
- Classifier: Programming Language :: Python :: 3
10
- Project-URL: source, https://gitlab.mpcdf.mpg.de/tbz/pre-commit-localupdate.git
11
- Requires-Python: <3.15,>=3.10
12
- Requires-Dist: requests>=2.32.5
13
- Requires-Dist: ruamel-yaml>=0.19.1
14
- Description-Content-Type: text/markdown
15
-
16
1
  # pre-commit-localupdate
17
2
 
18
- A CLI tool to automatically update dependencies in `pre-commit-config.yml` files. It specifically targets `additional_dependencies` within local Python and Node.js hooks to ensure your tooling stays up-to-date. It also adds version to unversioned packages and pins exact version of loosely defined ones.
3
+ A CLI tool to automatically update dependencies in `pre-commit-config.yml` files. It specifically targets `additional_dependencies` within local Python, Julia, and Node.js hooks to ensure your tooling stays up-to-date. It also adds version to unversioned packages and pins exact version of loosely defined ones.
19
4
 
20
5
  ## Installation
21
6
 
22
- ```bash
7
+ ```shell
23
8
  pip install pre-commit-localupdate
24
9
  ```
25
10
 
@@ -27,16 +12,28 @@ pip install pre-commit-localupdate
27
12
 
28
13
  To check and update the `additional_dependencies` in your `.pre-commit-config.yaml` file, simply run:
29
14
 
30
- ```bash
15
+ ```shell
31
16
  pre-commit-localupdate
32
17
  ```
33
18
 
34
19
  By default, the tool looks for `.pre-commit-config.yaml` in the current directory. To specify a custom file path, use the `-c` or `--config` argument:
35
20
 
36
- ```bash
21
+ ```shell
37
22
  pre-commit-localupdate --config path/to/.pre-commit-config.yaml
38
23
  ```
39
24
 
25
+ To check the dependencies without updating, use the `--dry-run` flag. The command will exit with a non-zero status code if any package needs updating:
26
+
27
+ ```shell
28
+ pre-commit-localupdate --dry-run
29
+ ```
30
+
31
+ To get debug output use `--debug` flag:
32
+
33
+ ```shell
34
+ pre-commit-localupdate --debug
35
+ ```
36
+
40
37
  ## Example
41
38
 
42
39
  Given a `.pre-commit-config.yaml` with the following content:
@@ -45,6 +42,13 @@ Given a `.pre-commit-config.yaml` with the following content:
45
42
  # Comments in the file header are preserved
46
43
  ---
47
44
  repos:
45
+ # External hooks won't be touched. Use 'pre-commit autoupdate' command to update them
46
+ - repo: https://github.com/pre-commit/pre-commit-hooks
47
+ rev: v2.3.0
48
+ hooks:
49
+ - id: check-yaml
50
+ - id: end-of-file-fixer
51
+ - id: trailing-whitespace
48
52
  - repo: local
49
53
  hooks:
50
54
  # Comment about hooks are preserved
@@ -57,7 +61,7 @@ repos:
57
61
  require_serial: true
58
62
  types_or: [python, pyi]
59
63
  additional_dependencies:
60
- # Loose version definitions will be pinned to an exact version
64
+ # Loose version definitions are pinned to an exact version
61
65
  - "black>=25.1.0"
62
66
 
63
67
  - id: mypy
@@ -94,6 +98,13 @@ Running `pre-commit-localupdate` will update the file to (hypothetical latest ve
94
98
  # Comments in the file header are preserved
95
99
  ---
96
100
  repos:
101
+ # External hooks won't be touched. Use 'pre-commit autoupdate' command to update them
102
+ - repo: https://github.com/pre-commit/pre-commit-hooks
103
+ rev: v2.3.0
104
+ hooks:
105
+ - id: check-yaml
106
+ - id: end-of-file-fixer
107
+ - id: trailing-whitespace
97
108
  - repo: local
98
109
  hooks:
99
110
  # Comment about hooks are preserved
@@ -107,7 +118,7 @@ repos:
107
118
  require_serial: true
108
119
  types_or: [python, pyi]
109
120
  additional_dependencies:
110
- # Loose version definitions should be pinned
121
+ # Loose version definitions are pinned to an exact version
111
122
  - "black==26.1.0"
112
123
 
113
124
  - id: mypy
@@ -0,0 +1 @@
1
+ __version__ = '0.2.0'
@@ -24,7 +24,7 @@ def main() -> None:
24
24
  args = parse_args()
25
25
  setup_logging(args.debug)
26
26
 
27
- if update_additional_dependencies(Path(args.config)):
27
+ if update_additional_dependencies(Path(args.config), dry_run=args.dry_run):
28
28
  logging.info("Done.")
29
29
  else:
30
30
  logging.info("File content is already up to date.")
@@ -2,18 +2,24 @@
2
2
  # SPDX-License-Identifier: LGPL-3.0-or-later
3
3
 
4
4
  import argparse
5
+ from pre_commit_localupdate import __version__
5
6
 
6
7
 
7
8
  def parse_args() -> argparse.Namespace:
8
9
  """Parse command line arguments."""
9
10
  parser = argparse.ArgumentParser(
10
- description="Update Python and Node.js additional_dependencies in .pre-commit-config.yaml"
11
+ description="Automatically update additional dependencies within local Python and Node.js hooks in pre-commit config files."
11
12
  )
12
13
  parser.add_argument(
13
14
  "--debug",
14
15
  action="store_true",
15
16
  help="Enable debug logging",
16
17
  )
18
+ parser.add_argument(
19
+ "--dry-run",
20
+ action="store_true",
21
+ help="Dry run mode. Do not update the file and exit with a non-zero code if the configuration files require an update.",
22
+ )
17
23
  parser.add_argument(
18
24
  "-c",
19
25
  "--config",
@@ -21,5 +27,7 @@ def parse_args() -> argparse.Namespace:
21
27
  help="pre-commit config file path",
22
28
  default=".pre-commit-config.yaml",
23
29
  )
24
-
30
+ parser.add_argument(
31
+ "--version", action="version", version=f"pre-commit-localupdate {__version__}"
32
+ )
25
33
  return parser.parse_args()
@@ -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
+ import logging
5
+ from pathlib import Path
6
+ import sys
7
+ from typing import List, Tuple
8
+
9
+
10
+ def load_config_file(file_path: Path) -> Tuple[List[str], str]:
11
+ logging.debug("Reading configuration file: %s", file_path)
12
+
13
+ raw_lines: List[str] = []
14
+ header_lines: List[str] = []
15
+ yaml_start_index: int = 0
16
+
17
+ try:
18
+ with file_path.open("r", encoding="utf-8") as f:
19
+ raw_lines = f.readlines()
20
+ except FileNotFoundError:
21
+ logging.exception("File not found: %s", file_path)
22
+ sys.exit(2)
23
+ except OSError as exc:
24
+ logging.exception("IOError while reading file %s: %s", file_path, exc)
25
+ sys.exit(2)
26
+
27
+ logging.debug("Parsing document header...")
28
+ yaml_start_index = 0
29
+ for i, line in enumerate(raw_lines):
30
+ stripped: str = line.strip()
31
+ if stripped == "---":
32
+ logging.debug("YAML marker found.")
33
+ yaml_start_index = i + 1
34
+ break
35
+ if stripped and not stripped.startswith("#"):
36
+ yaml_start_index = i
37
+ break
38
+
39
+ header_lines = raw_lines[:yaml_start_index]
40
+ yaml_content: str = "".join(raw_lines[yaml_start_index:])
41
+
42
+ return header_lines, yaml_content
@@ -0,0 +1,98 @@
1
+ # SPDX-FileCopyrightText: 2026 M. Farzalipour Tabriz, Max Planck Institute for Physics
2
+ # SPDX-License-Identifier: LGPL-3.0-or-later
3
+
4
+ import logging
5
+ import re
6
+ import tomllib
7
+ import requests
8
+
9
+ from pre_commit_localupdate.package import PackageBase
10
+
11
+ # Julia General Registry Source
12
+ JULIA_PKG_API_URL = "https://raw.githubusercontent.com/JuliaRegistries/General/refs/heads/master/{package_name_first_letter}/{package_name}/Versions.toml"
13
+ REQUEST_TIMEOUT = 10
14
+
15
+
16
+ class JuliaPackage(PackageBase):
17
+ """Represents a Julia package."""
18
+
19
+ @classmethod
20
+ def from_specification(package, spec: str) -> "JuliaPackage" | None:
21
+ """Parse a package specification string into a Package object.
22
+
23
+ Handles formats like 'Example@0.4.1', 'Example@0.4', or just 'Example'.
24
+
25
+ Args:
26
+ spec: The package specification string.
27
+
28
+ Returns:
29
+ A Package instance if parsing succeeds, otherwise None.
30
+
31
+ """
32
+ clean_spec = spec.strip().strip('"').strip("'")
33
+ match = re.match(r"^([a-zA-Z0-9_]+)(?:@([a-zA-Z0-9._+,-]+))?$", clean_spec)
34
+
35
+ if not match:
36
+ logging.warning("Could not parse the package specification: %s", clean_spec)
37
+ return None
38
+
39
+ name = match.group(1)
40
+ raw_spec = match.group(2)
41
+ latest_version = package._get_latest_version(name)
42
+
43
+ return package(
44
+ name=name,
45
+ raw_spec=raw_spec,
46
+ latest_version=latest_version,
47
+ )
48
+
49
+ @staticmethod
50
+ def _get_latest_version(package_name: str) -> str | None:
51
+ """Fetch the latest version of a package from the Julia General Registry.
52
+
53
+ Returns:
54
+ The latest version string if found, otherwise None.
55
+
56
+ """
57
+ logging.debug("Fetching latest version for %s", package_name)
58
+ try:
59
+ url = JULIA_PKG_API_URL.format(
60
+ package_name=package_name, package_name_first_letter=package_name[0]
61
+ )
62
+ response = requests.get(url, timeout=REQUEST_TIMEOUT)
63
+ response.raise_for_status()
64
+
65
+ data = tomllib.loads(response.text)
66
+
67
+ versions = list(data.keys())
68
+ if not versions:
69
+ return None
70
+
71
+ versions.sort(reverse=True)
72
+ latest = versions[0]
73
+
74
+ if latest and isinstance(latest, str):
75
+ return latest
76
+
77
+ return None
78
+ except requests.exceptions.RequestException as exc:
79
+ logging.warning(
80
+ "Could not fetch latest version for %s: %s", package_name, exc
81
+ )
82
+ return None
83
+ except (KeyError, ValueError, tomllib.TOMLDecodeError) as exc:
84
+ logging.warning("Could not parse response for %s: %s", package_name, exc)
85
+ return None
86
+
87
+ def latest_version_specification(package) -> str | None:
88
+ """Version specification of package pinned to the latest version.
89
+
90
+ Returns:
91
+ The latest version specification string if found, otherwise None.
92
+ Format: PackageName@version
93
+
94
+ """
95
+ if not package.latest_version:
96
+ return None
97
+
98
+ return f"{package.name}@{package.latest_version}"
@@ -4,20 +4,24 @@
4
4
  import io
5
5
  import logging
6
6
  from pathlib import Path
7
+ import sys
7
8
 
8
9
  import ruamel.yaml
9
10
 
10
11
  from pre_commit_localupdate.node import NodePackage
11
12
  from pre_commit_localupdate.package import PackageBase
12
13
  from pre_commit_localupdate.python import PyPackage
14
+ from pre_commit_localupdate.julia import JuliaPackage
15
+ from pre_commit_localupdate.io import load_config_file
13
16
 
14
17
  LANGUAGE_PACKAGE_MAP: dict[str, type[PackageBase]] = {
15
18
  "python": PyPackage,
16
19
  "node": NodePackage,
20
+ "julia": JuliaPackage,
17
21
  }
18
22
 
19
23
 
20
- def update_additional_dependencies(file_path: Path) -> bool:
24
+ def update_additional_dependencies(file_path: Path, *, dry_run: bool = False) -> bool:
21
25
  """Reads the configuration file, identifies outdated Python packages in
22
26
  local hooks, and updates them directly in the file.
23
27
 
@@ -31,38 +35,10 @@ def update_additional_dependencies(file_path: Path) -> bool:
31
35
  True if the file was modified and successfully written, False otherwise.
32
36
 
33
37
  """
34
- logging.debug("Reading configuration file: %s", file_path)
35
38
 
36
- raw_lines = []
37
- header_lines = []
38
- yaml_start_index = 0
39
-
40
- try:
41
- with file_path.open("r", encoding="utf-8") as f:
42
- raw_lines = f.readlines()
43
- except FileNotFoundError:
44
- logging.exception("File not found: %s", file_path)
45
- return False
46
- except OSError as exc:
47
- logging.exception("IOError while reading file %s: %s", file_path, exc)
48
- return False
49
-
50
- logging.debug("Parsing document header...")
51
- yaml_start_index = 0
52
- for i, line in enumerate(raw_lines):
53
- stripped = line.strip()
54
- if stripped == "---":
55
- logging.debug("YAML marker found.")
56
- yaml_start_index = i + 1
57
- break
58
- if stripped and not stripped.startswith("#"):
59
- yaml_start_index = i
60
- break
61
-
62
- header_lines = raw_lines[:yaml_start_index]
39
+ header_lines, yaml_content = load_config_file(file_path)
63
40
 
64
41
  logging.debug("Parsing YAML content...")
65
- yaml_content = "".join(raw_lines[yaml_start_index:])
66
42
 
67
43
  yaml = ruamel.yaml.YAML()
68
44
  yaml.preserve_quotes = True
@@ -76,13 +52,13 @@ def update_additional_dependencies(file_path: Path) -> bool:
76
52
  config = yaml.load(yaml_content)
77
53
  except Exception as exc:
78
54
  logging.exception("Failed to parse YAML content from %s: %s", file_path, exc)
79
- return False
55
+ sys.exit(2)
80
56
 
81
57
  if config is None:
82
58
  logging.warning("Configuration file is empty or could not be parsed.")
83
- return False
59
+ sys.exit(2)
84
60
 
85
- file_modified = False
61
+ update_required = False
86
62
 
87
63
  if "repos" in config:
88
64
  logging.debug("Scanning repositories for local hooks")
@@ -123,7 +99,7 @@ def update_additional_dependencies(file_path: Path) -> bool:
123
99
  != package.version_specification()
124
100
  ):
125
101
  logging.info(
126
- "Updating %s to %s",
102
+ "%s needs updating to %s",
127
103
  package.version_specification(),
128
104
  package.latest_version_specification(),
129
105
  )
@@ -131,7 +107,7 @@ def update_additional_dependencies(file_path: Path) -> bool:
131
107
  i
132
108
  ] = package.latest_version_specification()
133
109
 
134
- file_modified = True
110
+ update_required = True
135
111
  else:
136
112
  logging.debug(
137
113
  "%s is already correctly defined and up to date (%s).",
@@ -139,20 +115,25 @@ def update_additional_dependencies(file_path: Path) -> bool:
139
115
  package.version_specification(),
140
116
  )
141
117
 
142
- if file_modified:
143
- logging.debug("Writing modified content to disk")
144
- try:
145
- stream = io.StringIO()
146
- yaml.dump(config, stream)
147
- modified_body = stream.getvalue()
148
- final_content = "".join(header_lines) + modified_body
149
-
150
- with file_path.open("w", encoding="utf-8") as f:
151
- f.write(final_content)
152
- logging.info("Successfully updated %s", file_path)
153
- return True
154
- except OSError as exc:
155
- logging.exception("IOError while writing to file %s: %s", file_path, exc)
156
- return False
118
+ if update_required:
119
+ if dry_run:
120
+ sys.exit(1)
121
+ else:
122
+ logging.debug("Writing modified content to disk")
123
+ try:
124
+ stream = io.StringIO()
125
+ yaml.dump(config, stream)
126
+ modified_body = stream.getvalue()
127
+ final_content = "".join(header_lines) + modified_body
128
+
129
+ with file_path.open("w", encoding="utf-8") as f:
130
+ f.write(final_content)
131
+ logging.info("Successfully updated %s", file_path)
132
+ return True
133
+ except OSError as exc:
134
+ logging.exception(
135
+ "IOError while writing to file %s: %s", file_path, exc
136
+ )
137
+ return False
157
138
 
158
139
  return False
@@ -23,11 +23,12 @@ dependencies = [
23
23
  "requests>=2.32.5",
24
24
  "ruamel-yaml>=0.19.1",
25
25
  ]
26
- description = "A CLI tool to automatically update `additional_dependencies` within local Python hooks in `pre-commit-config.yml` files."
26
+ description = "A CLI tool to automatically update additional dependencies within local Python and Node.js hooks in pre-commit config files."
27
+ dynamic = []
27
28
  name = "pre-commit-localupdate"
28
29
  readme = "README.md"
29
- requires-python = "<3.15,>=3.10"
30
- version = "0.1.0"
30
+ requires-python = "<3.15,>=3.11"
31
+ version = "0.2.0"
31
32
 
32
33
  [project.scripts]
33
34
  pre-commit-localupdate = "pre_commit_localupdate.__main__:main"
@@ -35,6 +36,11 @@ pre-commit-localupdate = "pre_commit_localupdate.__main__:main"
35
36
  [project.urls]
36
37
  source = "https://gitlab.mpcdf.mpg.de/tbz/pre-commit-localupdate.git"
37
38
 
39
+ [tool.pdm.version]
40
+ source = "scm"
41
+ write_template = "__version__ = '{}'"
42
+ write_to = "pre_commit_localupdate/__init__.py"
43
+
38
44
  [tool.tomlsort]
39
45
  sort_table_keys = true
40
46
  trailing_comma_inline_array = true