pre-commit-localupdate 0.5.1__tar.gz → 0.6.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.5.1 → pre_commit_localupdate-0.6.0}/PKG-INFO +6 -4
  2. {pre_commit_localupdate-0.5.1 → pre_commit_localupdate-0.6.0}/README.md +5 -3
  3. pre_commit_localupdate-0.6.0/pre_commit_localupdate/__init__.py +1 -0
  4. {pre_commit_localupdate-0.5.1 → pre_commit_localupdate-0.6.0}/pre_commit_localupdate/__main__.py +5 -1
  5. {pre_commit_localupdate-0.5.1 → pre_commit_localupdate-0.6.0}/pre_commit_localupdate/cli.py +29 -1
  6. pre_commit_localupdate-0.6.0/pre_commit_localupdate/logs.py +64 -0
  7. {pre_commit_localupdate-0.5.1 → pre_commit_localupdate-0.6.0}/pre_commit_localupdate/packages/golang.py +34 -10
  8. {pre_commit_localupdate-0.5.1 → pre_commit_localupdate-0.6.0}/pre_commit_localupdate/packages/julia.py +7 -3
  9. {pre_commit_localupdate-0.5.1 → pre_commit_localupdate-0.6.0}/pre_commit_localupdate/packages/node.py +14 -7
  10. {pre_commit_localupdate-0.5.1 → pre_commit_localupdate-0.6.0}/pre_commit_localupdate/packages/package.py +6 -2
  11. {pre_commit_localupdate-0.5.1 → pre_commit_localupdate-0.6.0}/pre_commit_localupdate/packages/python.py +14 -6
  12. {pre_commit_localupdate-0.5.1 → pre_commit_localupdate-0.6.0}/pre_commit_localupdate/packages/rust.py +14 -6
  13. {pre_commit_localupdate-0.5.1 → pre_commit_localupdate-0.6.0}/pre_commit_localupdate/pre_commit_config.py +29 -13
  14. {pre_commit_localupdate-0.5.1 → pre_commit_localupdate-0.6.0}/pyproject.toml +1 -1
  15. pre_commit_localupdate-0.5.1/pre_commit_localupdate/__init__.py +0 -1
  16. pre_commit_localupdate-0.5.1/pre_commit_localupdate/logs.py +0 -19
  17. {pre_commit_localupdate-0.5.1 → pre_commit_localupdate-0.6.0}/LICENSE +0 -0
  18. {pre_commit_localupdate-0.5.1 → pre_commit_localupdate-0.6.0}/pre_commit_localupdate/error.py +0 -0
  19. {pre_commit_localupdate-0.5.1 → pre_commit_localupdate-0.6.0}/pre_commit_localupdate/io.py +0 -0
  20. {pre_commit_localupdate-0.5.1 → pre_commit_localupdate-0.6.0}/pre_commit_localupdate/packages/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pre-commit-localupdate
3
- Version: 0.5.1
3
+ Version: 0.6.0
4
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
@@ -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 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 to the absolute latest. You can also pin a specific package to an older version by adding a `# freeze` comment.
20
20
 
21
21
  ## Installation
22
22
 
@@ -36,8 +36,9 @@ All options:
36
36
 
37
37
  ```text
38
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
+ [--request-delay REQUEST_DELAY] [--indent-mapping INDENT_MAPPING]
40
+ [--indent-sequence INDENT_SEQUENCE] [--indent-offset INDENT_OFFSET]
41
+ [--line-width LINE_WIDTH] [--version]
41
42
 
42
43
  Automatically update additional dependencies within local hooks in a pre-commit config file.
43
44
 
@@ -48,6 +49,7 @@ options:
48
49
  configuration file require an update. (default: False)
49
50
  -c, --config PRE-COMMIT-CONFIG pre-commit config file path (default: .pre-commit-config.yaml)
50
51
  --timeout TIMEOUT connection timeout in seconds (default: 10)
52
+ --request-delay REQUEST_DELAY minimum deplay between network requests in seconds (default: 1.0)
51
53
  --indent-mapping INDENT_MAPPING YAML indentation for mappings (default: 2)
52
54
  --indent-sequence INDENT_SEQUENCE YAML indentation for sequences (default: 4)
53
55
  --indent-offset INDENT_OFFSET YAML indentation offset (default: 2)
@@ -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 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 to the absolute latest. You can also pin a specific package to an older version by adding a `# freeze` comment.
4
4
 
5
5
  ## Installation
6
6
 
@@ -20,8 +20,9 @@ All options:
20
20
 
21
21
  ```text
22
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
+ [--request-delay REQUEST_DELAY] [--indent-mapping INDENT_MAPPING]
24
+ [--indent-sequence INDENT_SEQUENCE] [--indent-offset INDENT_OFFSET]
25
+ [--line-width LINE_WIDTH] [--version]
25
26
 
26
27
  Automatically update additional dependencies within local hooks in a pre-commit config file.
27
28
 
@@ -32,6 +33,7 @@ options:
32
33
  configuration file require an update. (default: False)
33
34
  -c, --config PRE-COMMIT-CONFIG pre-commit config file path (default: .pre-commit-config.yaml)
34
35
  --timeout TIMEOUT connection timeout in seconds (default: 10)
36
+ --request-delay REQUEST_DELAY minimum deplay between network requests in seconds (default: 1.0)
35
37
  --indent-mapping INDENT_MAPPING YAML indentation for mappings (default: 2)
36
38
  --indent-sequence INDENT_SEQUENCE YAML indentation for sequences (default: 4)
37
39
  --indent-offset INDENT_OFFSET YAML indentation offset (default: 2)
@@ -0,0 +1 @@
1
+ __version__ = '0.6.0'
@@ -31,7 +31,11 @@ def main() -> None:
31
31
  )
32
32
  try:
33
33
  if update_additional_dependencies(
34
- args.config, args.timeout, yaml_format, dry_run=args.dry_run
34
+ args.config,
35
+ args.timeout,
36
+ args.request_delay,
37
+ yaml_format,
38
+ dry_run=args.dry_run,
35
39
  ):
36
40
  if args.dry_run:
37
41
  logging.info("Additional dependencies require updating!")
@@ -7,6 +7,28 @@ from pathlib import Path
7
7
  from pre_commit_localupdate import __version__
8
8
 
9
9
 
10
+ def positive_int(value: str) -> int:
11
+ """Convert a string argument to a positive integer.
12
+
13
+ Args:
14
+ value(str): input value
15
+ Returns:
16
+ int: positive integer equivalent
17
+ Raises:
18
+ argparse.ArgumentTypeError: if ``value`` cannot be converted to an integer
19
+ or is not a positive integer
20
+ """
21
+ try:
22
+ int_value = int(value)
23
+ except ValueError as err:
24
+ msg = f"'{value}' cannot be converted to integer"
25
+ raise argparse.ArgumentTypeError(msg) from err
26
+ if int_value <= 0:
27
+ msg = f"'{value}' is not a positive integer"
28
+ raise argparse.ArgumentTypeError(msg)
29
+ return int_value
30
+
31
+
10
32
  def parse_args() -> argparse.Namespace:
11
33
  """Parse command line arguments.
12
34
 
@@ -39,10 +61,16 @@ def parse_args() -> argparse.Namespace:
39
61
  )
40
62
  parser.add_argument(
41
63
  "--timeout",
42
- type=int,
64
+ type=positive_int,
43
65
  help="connection timeout in seconds",
44
66
  default=10,
45
67
  )
68
+ parser.add_argument(
69
+ "--request-delay",
70
+ type=float,
71
+ help="minimum deplay between network requests in seconds",
72
+ default=1.0,
73
+ )
46
74
  parser.add_argument(
47
75
  "--indent-mapping",
48
76
  type=int,
@@ -0,0 +1,64 @@
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
+
6
+
7
+ class SafeFormatter(logging.Formatter):
8
+ """Custom formatter that escapes newlines and carriage returns to prevent log injection."""
9
+
10
+ def format(self, record: logging.LogRecord) -> str:
11
+ """Sanitize logs and encode non-printable characters.
12
+
13
+ Args:
14
+ record (logging.LogRecord): log record to sanitize
15
+ Returns:
16
+ str: sanitized log message
17
+ """
18
+ processed_record = super().format(record)
19
+ if processed_record.isprintable():
20
+ return processed_record
21
+
22
+ max_8bit_uint: int = 0xFF
23
+ max_16bit_uint: int = 0xFFFF
24
+ sanitized_record_parts = []
25
+ for character in processed_record:
26
+ if character.isprintable():
27
+ sanitized_record_parts.append(character)
28
+ else:
29
+ character_code = ord(character)
30
+ if character_code <= max_8bit_uint:
31
+ sanitized_record_parts.append(f"\\x{character_code:02x}")
32
+ elif character_code <= max_16bit_uint:
33
+ sanitized_record_parts.append(f"\\u{character_code:04x}")
34
+ else:
35
+ sanitized_record_parts.append(f"\\U{character_code:08x}")
36
+
37
+ return "".join(sanitized_record_parts)
38
+
39
+
40
+ def setup_logging(*, debug: bool = False) -> None:
41
+ """Configure logging based on the debug flag.
42
+
43
+ Args:
44
+ debug (bool): activate debug logs
45
+ """
46
+ logger = logging.getLogger()
47
+ for handler in logger.handlers:
48
+ if not isinstance(handler, logging.NullHandler):
49
+ logger.warning("Logger has already been initialized!")
50
+ return
51
+
52
+ ch = logging.StreamHandler()
53
+ if debug:
54
+ log_format = "%(asctime)s - %(levelname)s - %(message)s"
55
+ logger.setLevel(logging.DEBUG)
56
+ ch.setLevel(logging.DEBUG)
57
+ else:
58
+ log_format = "%(levelname)s - %(message)s"
59
+ logger.setLevel(logging.INFO)
60
+ ch.setLevel(logging.INFO)
61
+
62
+ formatter = SafeFormatter(log_format)
63
+ ch.setFormatter(formatter)
64
+ logger.addHandler(ch)
@@ -8,7 +8,9 @@ import re
8
8
 
9
9
  import requests
10
10
 
11
- from .package import PackageBase
11
+ from urllib.parse import quote as url_quote
12
+
13
+ from .package import PackageBase, QUERY_AGENT_STRING
12
14
 
13
15
  # Go Proxy API endpoints
14
16
  GO_PKG_LATEST_VERSION_API_URL = "https://proxy.golang.org/{package_path}/@latest"
@@ -71,6 +73,21 @@ class GoPackage(PackageBase):
71
73
 
72
74
  return package_path
73
75
 
76
+ @staticmethod
77
+ def _escape_module_path(package_path: str) -> str:
78
+ """Escape a module path per the Go module proxy protocol.
79
+
80
+ Args:
81
+ package_path (str): The module path.
82
+
83
+ Returns:
84
+ str: The URL escaped and encoded module path.
85
+ """
86
+ escaped = re.sub(
87
+ r"[A-Z]", lambda cap_char: "!" + cap_char.group(0).lower(), package_path
88
+ )
89
+ return url_quote(escaped, safe="/")
90
+
74
91
  def fetch_latest_version(self, connection_timeout: int) -> None:
75
92
  """Fetch the latest version of a Go module from proxy.golang.org.
76
93
 
@@ -80,17 +97,24 @@ class GoPackage(PackageBase):
80
97
  package_path = self._resolve_module_path(self.name)
81
98
  logging.debug("Fetching latest version for %s", package_path)
82
99
  try:
83
- url = GO_PKG_LATEST_VERSION_API_URL.format(package_path=package_path)
84
-
85
- response = requests.get(url, timeout=connection_timeout)
100
+ safe_package_path = self._escape_module_path(package_path)
101
+ url = GO_PKG_LATEST_VERSION_API_URL.format(package_path=safe_package_path)
102
+ headers = {"User-Agent": QUERY_AGENT_STRING}
103
+ response = requests.get(url, headers=headers, timeout=connection_timeout)
86
104
  response.raise_for_status()
87
105
 
88
- data = response.json()
89
- latest_version = data.get("Version")
90
-
91
- if isinstance(latest_version, str):
92
- self.latest_version = latest_version
93
-
106
+ if response.headers.get("Content-Type", "").startswith("application/json"):
107
+ data = response.json()
108
+ latest_version = data.get("Version")
109
+
110
+ if isinstance(latest_version, str):
111
+ self.latest_version = latest_version
112
+ else:
113
+ logging.warning(
114
+ "Unexpected content type for %s: %s",
115
+ package_path,
116
+ response.headers.get("Content-Type", "<missing>"),
117
+ )
94
118
  except requests.exceptions.RequestException as exc:
95
119
  logging.warning(
96
120
  "Could not fetch latest version for %s: %s", package_path, exc
@@ -11,7 +11,7 @@ from dataclasses import dataclass
11
11
  import requests
12
12
  from packaging.version import InvalidVersion, Version
13
13
 
14
- from .package import PackageBase
14
+ from .package import PackageBase, QUERY_AGENT_STRING
15
15
 
16
16
  # Julia General Registry Source
17
17
  JULIA_PKG_API_URL = "https://raw.githubusercontent.com/JuliaRegistries/General/refs/heads/master/{package_name_first_letter}/{package_name}/Versions.toml"
@@ -75,6 +75,8 @@ class JuliaPackage(PackageBase):
75
75
  def fetch_latest_version(self, connection_timeout: int) -> None:
76
76
  """Fetch the latest version of a package from the Julia General Registry.
77
77
 
78
+ Ignores pre-release versions.
79
+
78
80
  Args:
79
81
  connection_timeout (int): Connection timeout in seconds.
80
82
  """
@@ -83,12 +85,14 @@ class JuliaPackage(PackageBase):
83
85
  url = JULIA_PKG_API_URL.format(
84
86
  package_name=self.name, package_name_first_letter=self.name[0]
85
87
  )
86
- response = requests.get(url, timeout=connection_timeout)
88
+ headers = {"User-Agent": QUERY_AGENT_STRING}
89
+ response = requests.get(url, headers=headers, timeout=connection_timeout)
87
90
  response.raise_for_status()
88
91
 
89
92
  data = tomllib.loads(response.text)
90
93
 
91
- versions = list(data.keys())
94
+ versions = [ver for ver in data.keys() if "-" not in ver.split("+", 1)[0]]
95
+
92
96
  if versions:
93
97
  versions.sort(key=lambda v: Version(v), reverse=True)
94
98
  self.latest_version = versions[0]
@@ -8,7 +8,7 @@ import re
8
8
 
9
9
  import requests
10
10
 
11
- from .package import PackageBase
11
+ from .package import PackageBase, QUERY_AGENT_STRING
12
12
 
13
13
  NPM_API_URL = "https://registry.npmjs.org/{package_name}"
14
14
 
@@ -61,14 +61,21 @@ class NodePackage(PackageBase):
61
61
  """
62
62
  try:
63
63
  url = NPM_API_URL.format(package_name=self.name)
64
- response = requests.get(url, timeout=connection_timeout)
64
+ headers = {"User-Agent": QUERY_AGENT_STRING}
65
+ response = requests.get(url, headers=headers, timeout=connection_timeout)
65
66
  response.raise_for_status()
66
- data = response.json()
67
-
68
- latest_version = data.get("dist-tags", {}).get("latest")
69
- if isinstance(latest_version, str):
70
- self.latest_version = latest_version
71
67
 
68
+ if response.headers.get("Content-Type", "").startswith("application/json"):
69
+ data = response.json()
70
+ latest_version = data.get("dist-tags", {}).get("latest")
71
+ if isinstance(latest_version, str):
72
+ self.latest_version = latest_version
73
+ else:
74
+ logging.warning(
75
+ "Unexpected content type for %s: %s",
76
+ self.name,
77
+ response.headers.get("Content-Type", "<missing>"),
78
+ )
72
79
  except requests.exceptions.RequestException as exc:
73
80
  logging.warning("Could not fetch latest version for %s: %s", self.name, exc)
74
81
  except (KeyError, ValueError) as exc:
@@ -6,6 +6,10 @@ from __future__ import annotations
6
6
  from abc import ABC, abstractmethod
7
7
  from dataclasses import dataclass
8
8
 
9
+ from pre_commit_localupdate import __version__
10
+
11
+ QUERY_AGENT_STRING = f"pre-commit-localupdate/{__version__} (https://pypi.org/project/pre-commit-localupdate/)"
12
+
9
13
 
10
14
  @dataclass
11
15
  class PackageBase(ABC):
@@ -26,12 +30,12 @@ class PackageBase(ABC):
26
30
  @abstractmethod
27
31
  def from_specification(
28
32
  cls,
29
- spec_string: str,
33
+ spec: str,
30
34
  ) -> "PackageBase" | None:
31
35
  """Parse a package specification string into a Package object.
32
36
 
33
37
  Args:
34
- spec_string (str): The package specification string (without quotations).
38
+ spec (str): The package specification string (without quotations).
35
39
 
36
40
  Returns:
37
41
  "PackageBase" | None: A Package instance if parsing succeeds, otherwise None.
@@ -8,7 +8,7 @@ import logging
8
8
  import requests
9
9
  from packaging.requirements import InvalidRequirement, Requirement
10
10
 
11
- from .package import PackageBase
11
+ from .package import PackageBase, QUERY_AGENT_STRING
12
12
 
13
13
  PYPI_API_URL = "https://pypi.org/pypi/{package_name}/json"
14
14
 
@@ -56,13 +56,21 @@ class PyPackage(PackageBase):
56
56
  logging.debug("Fetching latest version for %s", self.name)
57
57
  try:
58
58
  url = PYPI_API_URL.format(package_name=self.name)
59
- response = requests.get(url, timeout=connection_timeout)
59
+ headers = {"User-Agent": QUERY_AGENT_STRING}
60
+ response = requests.get(url, headers=headers, timeout=connection_timeout)
60
61
  response.raise_for_status()
61
- data = response.json()
62
- latest_version = data.get("info", {}).get("version")
63
- if isinstance(latest_version, str):
64
- self.latest_version = latest_version
65
62
 
63
+ if response.headers.get("Content-Type", "").startswith("application/json"):
64
+ data = response.json()
65
+ latest_version = data.get("info", {}).get("version")
66
+ if isinstance(latest_version, str):
67
+ self.latest_version = latest_version
68
+ else:
69
+ logging.warning(
70
+ "Unexpected content type for %s: %s",
71
+ self.name,
72
+ response.headers.get("Content-Type", "<missing>"),
73
+ )
66
74
  except requests.exceptions.RequestException as exc:
67
75
  logging.warning("Could not fetch latest version for %s: %s", self.name, exc)
68
76
  except (KeyError, ValueError) as exc:
@@ -9,7 +9,7 @@ from dataclasses import dataclass
9
9
 
10
10
  import requests
11
11
 
12
- from .package import PackageBase
12
+ from .package import PackageBase, QUERY_AGENT_STRING
13
13
 
14
14
  CRATES_API_URL = "https://crates.io/api/v1/crates/{package_name}"
15
15
 
@@ -64,13 +64,21 @@ class RustPackage(PackageBase):
64
64
  logging.debug("Fetching latest version for %s", self.name)
65
65
  try:
66
66
  url = CRATES_API_URL.format(package_name=self.name)
67
- response = requests.get(url, timeout=connection_timeout)
67
+ headers = {"User-Agent": QUERY_AGENT_STRING}
68
+ response = requests.get(url, headers=headers, timeout=connection_timeout)
68
69
  response.raise_for_status()
69
- data = response.json()
70
- latest_version = data.get("crate", {}).get("max_version")
71
- if isinstance(latest_version, str):
72
- self.latest_version = latest_version
73
70
 
71
+ if response.headers.get("Content-Type", "").startswith("application/json"):
72
+ data = response.json()
73
+ latest_version = data.get("crate", {}).get("max_version")
74
+ if isinstance(latest_version, str):
75
+ self.latest_version = latest_version
76
+ else:
77
+ logging.warning(
78
+ "Unexpected content type for %s: %s",
79
+ self.name,
80
+ response.headers.get("Content-Type", "<missing>"),
81
+ )
74
82
  except requests.exceptions.RequestException as exc:
75
83
  logging.warning("Could not fetch latest version for %s: %s", self.name, exc)
76
84
  except (KeyError, ValueError) as exc:
@@ -2,6 +2,7 @@
2
2
  # SPDX-License-Identifier: LGPL-3.0-or-later
3
3
 
4
4
  import logging
5
+ import time
5
6
  import re
6
7
  from pathlib import Path
7
8
 
@@ -13,10 +14,13 @@ from pre_commit_localupdate.error import PreCommitLocalUpdateError
13
14
  from pre_commit_localupdate.io import YAMLFormat, load_config_file, save_config_file
14
15
  from pre_commit_localupdate.packages import SUPPORTED_PACKAGES
15
16
 
17
+ FREEZE_PATTERN = re.compile(r"^#\s*freeze(\s|$)")
18
+
16
19
 
17
20
  def update_additional_dependencies(
18
21
  file_path: Path,
19
22
  connection_timeout: int,
23
+ request_delay: float,
20
24
  yaml_format: YAMLFormat,
21
25
  *,
22
26
  dry_run: bool = False,
@@ -30,6 +34,7 @@ def update_additional_dependencies(
30
34
  Args:
31
35
  file_path (Path): Path to the pre-commit configuration file.
32
36
  connection_timeout (int): Connection timeout in seconds.
37
+ request_delay (float): Minimum delay between network requests in seconds.
33
38
  yaml_format (YAMLFormat): YAML file formatting parameters
34
39
  dry_run (bool): Do not update the pre-commit configuration file.
35
40
 
@@ -66,7 +71,9 @@ def update_additional_dependencies(
66
71
  )
67
72
 
68
73
  update_required = False
69
- FREEZE_PATTERN = re.compile(r"^#\s*freeze(\s|$)")
74
+ latest_request_time: float = 0.0
75
+ # (package type, package name) -> latest version specification
76
+ queried_packages: dict[tuple[str, str], str | None] = {}
70
77
 
71
78
  if "repos" in config:
72
79
  logging.debug("Scanning repositories for local hooks")
@@ -90,7 +97,6 @@ def update_additional_dependencies(
90
97
  package_type = SUPPORTED_PACKAGES[hook.get("language")]
91
98
 
92
99
  for i, dep_str in enumerate(deps_list):
93
-
94
100
  comment = None
95
101
  if isinstance(deps_list, CommentedSeq):
96
102
  comment = deps_list.ca.items.get(i, [None, None])[0]
@@ -101,7 +107,7 @@ def update_additional_dependencies(
101
107
  logging.debug("Skipping frozen dependency: %s", dep_str)
102
108
  continue
103
109
 
104
- logging.debug("Checking dependency: %s", dep_str)
110
+ logging.debug("Processing dependency: %s", dep_str)
105
111
  clean_dep_str = dep_str.strip().strip('"').strip("'")
106
112
  package = package_type.from_specification(clean_dep_str)
107
113
  if not package:
@@ -110,22 +116,32 @@ def update_additional_dependencies(
110
116
  )
111
117
  continue
112
118
 
113
- package.fetch_latest_version(connection_timeout)
114
- if not package.latest_version_specification():
119
+ cache_key = (package_type.__name__, package.name)
120
+ if cache_key in queried_packages:
121
+ logging.debug(
122
+ "Using cached latest version for package: %s",
123
+ package.name,
124
+ )
125
+ latest_spec = queried_packages[cache_key]
126
+ else:
127
+ elapsed = time.monotonic() - latest_request_time
128
+ if elapsed < request_delay:
129
+ time.sleep(request_delay - elapsed)
130
+ latest_request_time = time.monotonic()
131
+ package.fetch_latest_version(connection_timeout)
132
+ latest_spec = package.latest_version_specification()
133
+ queried_packages[cache_key] = latest_spec
134
+
135
+ if latest_spec is None:
115
136
  continue
116
137
 
117
- if (
118
- package.latest_version_specification()
119
- != package.original_version_specification()
120
- ):
138
+ if latest_spec != package.original_version_specification():
121
139
  logging.debug(
122
140
  "%s needs updating to %s",
123
141
  package.original_version_specification(),
124
- package.latest_version_specification(),
142
+ latest_spec,
125
143
  )
126
- hook["additional_dependencies"][
127
- i
128
- ] = package.latest_version_specification()
144
+ hook["additional_dependencies"][i] = latest_spec
129
145
 
130
146
  update_required = True
131
147
  else:
@@ -29,7 +29,7 @@ dynamic = []
29
29
  name = "pre-commit-localupdate"
30
30
  readme = "README.md"
31
31
  requires-python = "<3.15,>=3.11"
32
- version = "0.5.1"
32
+ version = "0.6.0"
33
33
 
34
34
  [project.scripts]
35
35
  pre-commit-localupdate = "pre_commit_localupdate.__main__:main"
@@ -1 +0,0 @@
1
- __version__ = '0.5.1'
@@ -1,19 +0,0 @@
1
- # SPDX-FileCopyrightText: 2026 M. Farzalipour Tabriz, Max Planck Institute for Physics
2
- # SPDX-License-Identifier: LGPL-3.0-or-later
3
-
4
- import logging
5
- import sys
6
-
7
-
8
- def setup_logging(*, debug: bool = False) -> None:
9
- """Configure logging based on the debug flag.
10
-
11
- Args:
12
- debug (bool): activate debug logs
13
- """
14
- level = logging.DEBUG if debug else logging.INFO
15
- logging.basicConfig(
16
- level=level,
17
- format="%(asctime)s - %(levelname)s - %(message)s",
18
- stream=sys.stdout,
19
- )