python-update-checker 0.5__py3-none-any.whl
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.
- puc/__init__.py +3 -0
- puc/cli.py +214 -0
- puc/dependencies.py +187 -0
- puc/logging.py +76 -0
- puc/pyprojecttoml.py +179 -0
- puc/requirementstxt.py +116 -0
- puc/uvlock.py +133 -0
- python_update_checker-0.5.dist-info/METADATA +122 -0
- python_update_checker-0.5.dist-info/RECORD +11 -0
- python_update_checker-0.5.dist-info/WHEEL +4 -0
- python_update_checker-0.5.dist-info/entry_points.txt +3 -0
puc/__init__.py
ADDED
puc/cli.py
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
# Author: Bastian Kleineidam
|
|
2
|
+
# Copyright: GPL-v3
|
|
3
|
+
"""CLI to update pinned dependencies in pyproject.toml or requirements.txt files.
|
|
4
|
+
Needs uv (https://docs.astral.sh/uv/).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import subprocess
|
|
8
|
+
import argparse
|
|
9
|
+
import sys
|
|
10
|
+
import logging
|
|
11
|
+
import os
|
|
12
|
+
import tempfile
|
|
13
|
+
from packaging.utils import canonicalize_name
|
|
14
|
+
from typing import TextIO, Any
|
|
15
|
+
|
|
16
|
+
from .dependencies import (
|
|
17
|
+
get_latest_version,
|
|
18
|
+
)
|
|
19
|
+
from .logging import logger
|
|
20
|
+
from .pyprojecttoml import handle_pyproject_toml
|
|
21
|
+
from .requirementstxt import handle_requirements_txt
|
|
22
|
+
from .uvlock import handle_uv_lock
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def usage(msg: str | None = None) -> None:
|
|
26
|
+
"""Print usage info"""
|
|
27
|
+
if msg:
|
|
28
|
+
logger.error(msg)
|
|
29
|
+
p = get_option_parser()
|
|
30
|
+
logger.info(p.format_usage())
|
|
31
|
+
sys.exit(-1)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def handle_latest(package, optargs, constraint_file):
|
|
35
|
+
"""Print latest version of package."""
|
|
36
|
+
exclude_newer = optargs.exclude_newer
|
|
37
|
+
try:
|
|
38
|
+
latest_version = get_latest_version(
|
|
39
|
+
package,
|
|
40
|
+
exclude_newer=exclude_newer,
|
|
41
|
+
constraint_file=constraint_file,
|
|
42
|
+
)
|
|
43
|
+
logger.info(f"{package}=={latest_version}")
|
|
44
|
+
except subprocess.CalledProcessError as exc:
|
|
45
|
+
# error getting latest version
|
|
46
|
+
err = f"{exc}, output={exc.output}, stderr={exc.stderr}"
|
|
47
|
+
logger.warning(f"error getting latest version for '{package}': {err}")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def supports_color(handle: TextIO | Any) -> bool:
|
|
51
|
+
"""Determine if given file handle is a TTY suitable for color output."""
|
|
52
|
+
return hasattr(handle, "isatty") and handle.isatty()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def get_option_parser() -> argparse.ArgumentParser:
|
|
56
|
+
"""Initialize and return the option parser.
|
|
57
|
+
@return: parser
|
|
58
|
+
@rtype: argparse.ArgumentParser
|
|
59
|
+
"""
|
|
60
|
+
parser = argparse.ArgumentParser()
|
|
61
|
+
parser.add_argument(
|
|
62
|
+
"--exclude-newer",
|
|
63
|
+
dest="exclude_newer",
|
|
64
|
+
help="Limit package versions to those that were uploaded prior to the given date",
|
|
65
|
+
)
|
|
66
|
+
parser.add_argument(
|
|
67
|
+
"--constraints",
|
|
68
|
+
dest="constraints",
|
|
69
|
+
help="Constrain versions using the given requirements file or string",
|
|
70
|
+
)
|
|
71
|
+
parser.add_argument(
|
|
72
|
+
"--package",
|
|
73
|
+
dest="packages",
|
|
74
|
+
action="append",
|
|
75
|
+
help="Only update the given package, can be given multiple times",
|
|
76
|
+
)
|
|
77
|
+
parser.add_argument(
|
|
78
|
+
"--no-color",
|
|
79
|
+
action="store_false",
|
|
80
|
+
dest="color",
|
|
81
|
+
default=supports_color(sys.stdout),
|
|
82
|
+
help="Do not print colored updated versions.",
|
|
83
|
+
)
|
|
84
|
+
parser.add_argument(
|
|
85
|
+
"--debug",
|
|
86
|
+
action="store_true",
|
|
87
|
+
dest="debug",
|
|
88
|
+
default=False,
|
|
89
|
+
help="Print debug messages.",
|
|
90
|
+
)
|
|
91
|
+
subparsers = parser.add_subparsers(help='commands', dest='command')
|
|
92
|
+
parser_check = subparsers.add_parser('check', help='check for updated versions')
|
|
93
|
+
parser_check.add_argument(
|
|
94
|
+
"dep_files",
|
|
95
|
+
nargs="+",
|
|
96
|
+
help="pyproject.toml or requirements.txt file",
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
parser_update = subparsers.add_parser('update', help='update to latest versions')
|
|
100
|
+
parser_update.add_argument(
|
|
101
|
+
"dep_files",
|
|
102
|
+
nargs="+",
|
|
103
|
+
help="pyproject.toml or requirements.txt file",
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
parser_latest = subparsers.add_parser(
|
|
107
|
+
'latest', help='get latest version of packages'
|
|
108
|
+
)
|
|
109
|
+
parser_latest.add_argument(
|
|
110
|
+
"packages",
|
|
111
|
+
nargs="+",
|
|
112
|
+
help="package names",
|
|
113
|
+
)
|
|
114
|
+
return parser
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def handle_dependency_file(dep_file: str, optargs, constraint_file):
|
|
118
|
+
"""Check a dependency file for updates."""
|
|
119
|
+
if not os.path.isfile(dep_file):
|
|
120
|
+
usage(f"file {dep_file} not found or not a regular file")
|
|
121
|
+
# limit to 1MB to prevent denial-of-service
|
|
122
|
+
if os.stat(dep_file).st_size > 1024 * 1014:
|
|
123
|
+
usage(f"file {dep_file} is >1 MB")
|
|
124
|
+
dep_file_normalized = os.path.basename(dep_file).lower()
|
|
125
|
+
if optargs.packages:
|
|
126
|
+
packages = [canonicalize_name(name) for name in optargs.packages]
|
|
127
|
+
else:
|
|
128
|
+
packages = None
|
|
129
|
+
if dep_file_normalized == "pyproject.toml":
|
|
130
|
+
# pyproject.toml format
|
|
131
|
+
updatable = handle_pyproject_toml(
|
|
132
|
+
dep_file,
|
|
133
|
+
packages=packages,
|
|
134
|
+
command=optargs.command,
|
|
135
|
+
exclude_newer=optargs.exclude_newer,
|
|
136
|
+
constraint_file=constraint_file,
|
|
137
|
+
color=optargs.color,
|
|
138
|
+
)
|
|
139
|
+
elif dep_file_normalized == "uv.lock":
|
|
140
|
+
# pyproject.toml format
|
|
141
|
+
updatable = handle_uv_lock(
|
|
142
|
+
dep_file,
|
|
143
|
+
packages=packages,
|
|
144
|
+
command=optargs.command,
|
|
145
|
+
exclude_newer=optargs.exclude_newer,
|
|
146
|
+
constraint_file=constraint_file,
|
|
147
|
+
color=optargs.color,
|
|
148
|
+
)
|
|
149
|
+
elif dep_file_normalized.endswith((".txt", ".in")):
|
|
150
|
+
# requirements.txt format
|
|
151
|
+
updatable = handle_requirements_txt(
|
|
152
|
+
dep_file,
|
|
153
|
+
packages=packages,
|
|
154
|
+
command=optargs.command,
|
|
155
|
+
exclude_newer=optargs.exclude_newer,
|
|
156
|
+
constraint_file=constraint_file,
|
|
157
|
+
color=optargs.color,
|
|
158
|
+
)
|
|
159
|
+
else:
|
|
160
|
+
usage(
|
|
161
|
+
f"no pyproject.toml or requirements.txt format detected for file {dep_file!r}"
|
|
162
|
+
)
|
|
163
|
+
return updatable
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def main() -> int:
|
|
167
|
+
"""Parse options and check or update dependencies."""
|
|
168
|
+
# parse options
|
|
169
|
+
try:
|
|
170
|
+
optargs = get_option_parser().parse_args(sys.argv[1:])
|
|
171
|
+
except argparse.ArgumentError as exc:
|
|
172
|
+
logger.exception(exc)
|
|
173
|
+
usage()
|
|
174
|
+
# handle options
|
|
175
|
+
if optargs.debug:
|
|
176
|
+
logger.setLevel(logging.DEBUG)
|
|
177
|
+
remove_constraint_file = False
|
|
178
|
+
constraints = optargs.constraints
|
|
179
|
+
constraint_file = None
|
|
180
|
+
# if constraints is a string write it in a temporary constraint file
|
|
181
|
+
if constraints:
|
|
182
|
+
if os.path.isfile(constraints):
|
|
183
|
+
constraint_file = constraints
|
|
184
|
+
else:
|
|
185
|
+
_fd, constraint_file = tempfile.mkstemp(
|
|
186
|
+
suffix=".txt", prefix="puc-constraints-"
|
|
187
|
+
)
|
|
188
|
+
with open(constraint_file, "w") as f:
|
|
189
|
+
f.write(constraints)
|
|
190
|
+
remove_constraint_file = True
|
|
191
|
+
# handle all given dependency files
|
|
192
|
+
try:
|
|
193
|
+
if optargs.command in ("check", "update"):
|
|
194
|
+
for dep_file in optargs.dep_files:
|
|
195
|
+
updatable = handle_dependency_file(dep_file, optargs, constraint_file)
|
|
196
|
+
elif optargs.command == "latest":
|
|
197
|
+
for package in optargs.packages:
|
|
198
|
+
handle_latest(package, optargs, constraint_file)
|
|
199
|
+
elif not optargs.command:
|
|
200
|
+
usage("missing command")
|
|
201
|
+
else:
|
|
202
|
+
usage(f"unknown command {optargs.command}")
|
|
203
|
+
except Exception as exc:
|
|
204
|
+
logger.error(f"error handling command {optargs.command}: {exc}")
|
|
205
|
+
return -1
|
|
206
|
+
finally:
|
|
207
|
+
if remove_constraint_file and constraint_file:
|
|
208
|
+
os.unlink(constraint_file)
|
|
209
|
+
# check command returns non-zero exit code when updates are available
|
|
210
|
+
return 1 if optargs.command == "check" and updatable > 0 else 0
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
# if __name__ == "__main__":
|
|
214
|
+
# sys.exit(main(sys.argv[1:]))
|
puc/dependencies.py
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# Author: Bastian Kleineidam
|
|
2
|
+
# Copyright: GPL-v3
|
|
3
|
+
"""Dependency helper functions."""
|
|
4
|
+
|
|
5
|
+
import subprocess
|
|
6
|
+
import re
|
|
7
|
+
from packaging.requirements import Requirement
|
|
8
|
+
from packaging.markers import Variable, MarkerList
|
|
9
|
+
from packaging.version import parse as parse_version
|
|
10
|
+
from .logging import logger
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def get_latest_version(
|
|
14
|
+
package: str,
|
|
15
|
+
exclude_newer: None | str = None,
|
|
16
|
+
constraint_file: None | str = None,
|
|
17
|
+
python_platform: str | None = None,
|
|
18
|
+
python_version: str | None = None,
|
|
19
|
+
) -> str:
|
|
20
|
+
"""Get the latest version of a package.
|
|
21
|
+
`python_platform` defines the platform for which requirements should be resolved.
|
|
22
|
+
`python_version` defines the minimum Python version that must be supported by the resolved requirements. If a patch version is omitted, the minimum patch version is assumed. For example, 3.8 is mapped to 3.8.0.
|
|
23
|
+
"""
|
|
24
|
+
cmd = [
|
|
25
|
+
"uv",
|
|
26
|
+
"pip",
|
|
27
|
+
"compile",
|
|
28
|
+
"-",
|
|
29
|
+
"--color=never",
|
|
30
|
+
"--quiet",
|
|
31
|
+
"--no-deps",
|
|
32
|
+
"--no-header",
|
|
33
|
+
"--no-annotate",
|
|
34
|
+
"--no-progress",
|
|
35
|
+
]
|
|
36
|
+
if exclude_newer:
|
|
37
|
+
cmd.extend(("--exclude-newer", exclude_newer))
|
|
38
|
+
if constraint_file:
|
|
39
|
+
cmd.extend(("--constraints", constraint_file))
|
|
40
|
+
if python_platform:
|
|
41
|
+
cmd.extend(("--python-platform", python_platform))
|
|
42
|
+
if python_version:
|
|
43
|
+
cmd.extend(("--python-version", python_version))
|
|
44
|
+
logger.debug(f"running '{' '.join(cmd)}' with input {package!r}")
|
|
45
|
+
result = subprocess.run(
|
|
46
|
+
cmd, check=True, text=True, input=package, capture_output=True
|
|
47
|
+
)
|
|
48
|
+
package_spec = result.stdout.strip()
|
|
49
|
+
return package_spec.split("==", 1)[1]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def get_python_platform(
|
|
53
|
+
os_name: str | None = None, sys_platform: str | None = None
|
|
54
|
+
) -> str | None:
|
|
55
|
+
"""Translate os_name or sys_platform values into python uv --python-platform values.
|
|
56
|
+
The translation is very coarse and not complete, but should be suitable for common
|
|
57
|
+
cases.
|
|
58
|
+
See https://peps.python.org/pep-0508/#environment-markers and
|
|
59
|
+
https://docs.astral.sh/uv/reference/cli/#uv-pip-compile--python-platform
|
|
60
|
+
"""
|
|
61
|
+
if os_name == "nt":
|
|
62
|
+
return "windows"
|
|
63
|
+
if os_name == "posix":
|
|
64
|
+
return "linux"
|
|
65
|
+
if sys_platform == "win32":
|
|
66
|
+
return "windows"
|
|
67
|
+
if sys_platform == "linux":
|
|
68
|
+
return "linux"
|
|
69
|
+
if sys_platform == "darwin":
|
|
70
|
+
return "macos"
|
|
71
|
+
return None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def check_requirement(
|
|
75
|
+
pkg_req: Requirement, projectname: str | None = None
|
|
76
|
+
) -> Requirement | None:
|
|
77
|
+
"""Check if requirement is pinned, else log and return None."""
|
|
78
|
+
if projectname and pkg_req.name == projectname:
|
|
79
|
+
logger.info(f"skip project name dependency {pkg_req}")
|
|
80
|
+
return None
|
|
81
|
+
if pkg_req.url:
|
|
82
|
+
logger.info(f"skip URL-pinned package dependency '{pkg_req}'")
|
|
83
|
+
return None
|
|
84
|
+
if len(pkg_req.specifier) < 1:
|
|
85
|
+
logger.info(f"skip non-versioned dependency '{pkg_req}'")
|
|
86
|
+
return None
|
|
87
|
+
if len(pkg_req.specifier) > 1:
|
|
88
|
+
logger.info(f"skip multi-versioned dependency '{pkg_req}'")
|
|
89
|
+
return None
|
|
90
|
+
for spec in pkg_req.specifier:
|
|
91
|
+
if spec.operator not in ('==', '==='):
|
|
92
|
+
logger.info(f"skip unpinned dependency '{pkg_req}'")
|
|
93
|
+
return None
|
|
94
|
+
if "*" in spec.version:
|
|
95
|
+
logger.info(f"skip unpinned *-patterned version dependency '{pkg_req}'")
|
|
96
|
+
return None
|
|
97
|
+
return pkg_req
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def get_python_platform_from_req(pkg_req: Requirement) -> str | None:
|
|
102
|
+
"""Determine value to use for 'uv pip compile --python-platform'"""
|
|
103
|
+
if not pkg_req.marker:
|
|
104
|
+
return None
|
|
105
|
+
markerlist = pkg_req.marker._markers
|
|
106
|
+
return get_python_platform(
|
|
107
|
+
os_name=get_marker_value(markerlist, "os_name", opfilter=("==",)),
|
|
108
|
+
sys_platform=get_marker_value(markerlist, "sys_platform", opfilter=("==",)),
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def get_min_python_version_from_req(pkg_req: Requirement) -> str | None:
|
|
113
|
+
"""Determine value to use for 'uv pip compile --python-version'"""
|
|
114
|
+
if not pkg_req.marker:
|
|
115
|
+
return None
|
|
116
|
+
markerlist = pkg_req.marker._markers
|
|
117
|
+
return get_marker_value(markerlist, "python_version", opfilter=(">=", ""))
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def parse_requirement(
|
|
121
|
+
line: str,
|
|
122
|
+
exclude_newer: None | str = None,
|
|
123
|
+
constraint_file: None | str = None,
|
|
124
|
+
) -> None | str | Requirement:
|
|
125
|
+
"""Parse one line of a requirements.txt file."""
|
|
126
|
+
line = line.strip()
|
|
127
|
+
if not line or line.startswith("#"):
|
|
128
|
+
# ignore comments
|
|
129
|
+
return None
|
|
130
|
+
if line.endswith("\\"):
|
|
131
|
+
line = line[:-1]
|
|
132
|
+
if line.startswith("--hash"):
|
|
133
|
+
logger.info(f"ignore requirements hash {line!r}")
|
|
134
|
+
return None
|
|
135
|
+
if re.search(r"^-r\s+", line):
|
|
136
|
+
# recursion
|
|
137
|
+
return line.split(maxsplit=1)[1].strip()
|
|
138
|
+
if re.search(r"^-c\s+", line):
|
|
139
|
+
logger.info(
|
|
140
|
+
f"ignore constraints reference {line!r}, use puc --constraints instead"
|
|
141
|
+
)
|
|
142
|
+
return None
|
|
143
|
+
if line.startswith("./"):
|
|
144
|
+
# ignore local file references
|
|
145
|
+
logger.info(f"skip local-file pinned dependency {line!r}")
|
|
146
|
+
return None
|
|
147
|
+
if line.lower().startswith(("http://", "https://")):
|
|
148
|
+
# ignore local file references
|
|
149
|
+
logger.info(f"skip URL-pinned dependency {line!r}")
|
|
150
|
+
return None
|
|
151
|
+
# remove trailing comment
|
|
152
|
+
line = re.sub("#.*$", "", line)
|
|
153
|
+
try:
|
|
154
|
+
pkg_req = Requirement(line)
|
|
155
|
+
except Exception as exc:
|
|
156
|
+
logger.debug(f"error parsing exception: {exc}")
|
|
157
|
+
logger.info(f"skip unsupported dependency {line!r}")
|
|
158
|
+
return None
|
|
159
|
+
return check_requirement(pkg_req)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def get_marker_value(
|
|
163
|
+
markerlist: MarkerList, varname: str, opfilter: tuple[str, ...] | None = None
|
|
164
|
+
) -> str | None:
|
|
165
|
+
"""Search variable definitions in markerlist.
|
|
166
|
+
`varname`: variable name to match
|
|
167
|
+
`opfilter`: optional operator string to match.
|
|
168
|
+
"""
|
|
169
|
+
for marker in markerlist:
|
|
170
|
+
if isinstance(marker, tuple):
|
|
171
|
+
left, op, right = marker
|
|
172
|
+
if opfilter and op.serialize() not in opfilter:
|
|
173
|
+
continue
|
|
174
|
+
if isinstance(left, Variable):
|
|
175
|
+
var = left.value
|
|
176
|
+
value = right.value
|
|
177
|
+
else:
|
|
178
|
+
var = right.value
|
|
179
|
+
value = left.value
|
|
180
|
+
if var == varname:
|
|
181
|
+
return value
|
|
182
|
+
return None
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def is_newer_version(old_version, new_version):
|
|
186
|
+
"""Check that new_version is newer than old_version."""
|
|
187
|
+
return parse_version(new_version) > parse_version(old_version)
|
puc/logging.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# Author: Bastian Kleineidam
|
|
2
|
+
# Copyright: GPL-v3
|
|
3
|
+
"""Logging initialization."""
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger("puc")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def init_logging(stream=sys.stdout) -> None:
|
|
12
|
+
"""Configure the global logger.
|
|
13
|
+
All log messages will be sent to the given stream, default is sys.stdout.
|
|
14
|
+
"""
|
|
15
|
+
# do not propagate log message to higher log level handlers (in our case the root level)
|
|
16
|
+
logger.propagate = False
|
|
17
|
+
handler = logging.StreamHandler(stream=stream)
|
|
18
|
+
format = "%(name)s %(levelname)s: %(message)s"
|
|
19
|
+
handler.setFormatter(logging.Formatter(format))
|
|
20
|
+
logger.addHandler(handler)
|
|
21
|
+
# set the log level to INFO, and change to DEBUG with --verbose
|
|
22
|
+
logger.setLevel(logging.INFO)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
init_logging()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# ANSI color codes
|
|
29
|
+
ansi_colors = {
|
|
30
|
+
'red': '\033[31m',
|
|
31
|
+
'cyan': '\033[36m',
|
|
32
|
+
'green': '\033[32m',
|
|
33
|
+
'reset': '\033[0m',
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def colorize_updated_version(from_ver: str, to_ver: str) -> str:
|
|
38
|
+
"""Colorize an updated version `to_ver` (`from_ver` is the old version).
|
|
39
|
+
Assumes both versions are semver strings.
|
|
40
|
+
Logic for coloring:
|
|
41
|
+
- red: major version change or any change before 1.0.0
|
|
42
|
+
- cyan: minor version change
|
|
43
|
+
- green: patch version change
|
|
44
|
+
"""
|
|
45
|
+
# split into parts for comparing
|
|
46
|
+
parts_to_ver = to_ver.split('.')
|
|
47
|
+
parts_from_ver = from_ver.split('.')
|
|
48
|
+
|
|
49
|
+
# find the index of the first difference
|
|
50
|
+
index = len(parts_to_ver)
|
|
51
|
+
for i, part in enumerate(parts_to_ver):
|
|
52
|
+
if i >= len(parts_from_ver):
|
|
53
|
+
# '1' --> '1.1'
|
|
54
|
+
index = i
|
|
55
|
+
break
|
|
56
|
+
if part != parts_from_ver[i]:
|
|
57
|
+
# '1.0' --> '1.1'
|
|
58
|
+
index = i
|
|
59
|
+
break
|
|
60
|
+
|
|
61
|
+
# coloring
|
|
62
|
+
if index == 0 or (len(parts_to_ver) > 0 and parts_to_ver[0] == '0'):
|
|
63
|
+
color = 'red'
|
|
64
|
+
elif index == 1:
|
|
65
|
+
color = 'cyan'
|
|
66
|
+
else:
|
|
67
|
+
color = 'green'
|
|
68
|
+
|
|
69
|
+
# construct the final string
|
|
70
|
+
first_part = ".".join(parts_to_ver[:index])
|
|
71
|
+
second_part = ".".join(parts_to_ver[index:])
|
|
72
|
+
middle_dot = '.' if 0 < index < len(parts_to_ver) else ""
|
|
73
|
+
if second_part:
|
|
74
|
+
# add color
|
|
75
|
+
second_part = f"{ansi_colors[color]}{second_part}{ansi_colors['reset']}"
|
|
76
|
+
return f"{first_part}{middle_dot}{second_part}"
|
puc/pyprojecttoml.py
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# Author: Bastian Kleineidam
|
|
2
|
+
# Copyright: GPL-v3
|
|
3
|
+
"""Handle pyproject.toml files."""
|
|
4
|
+
|
|
5
|
+
import subprocess
|
|
6
|
+
import os
|
|
7
|
+
import tomllib
|
|
8
|
+
from packaging.utils import canonicalize_name
|
|
9
|
+
from packaging.requirements import Requirement
|
|
10
|
+
|
|
11
|
+
from .logging import logger, colorize_updated_version
|
|
12
|
+
from .dependencies import (
|
|
13
|
+
get_latest_version,
|
|
14
|
+
get_python_platform_from_req,
|
|
15
|
+
get_min_python_version_from_req,
|
|
16
|
+
check_requirement,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def handle_pyproject_toml(
|
|
21
|
+
pyproject_path: str,
|
|
22
|
+
command: None | str = None,
|
|
23
|
+
packages=None,
|
|
24
|
+
exclude_newer: None | str = None,
|
|
25
|
+
constraint_file: None | str = None,
|
|
26
|
+
color: bool = True,
|
|
27
|
+
) -> int:
|
|
28
|
+
"""Check or update pinned dependencies of a pyproject.toml file.
|
|
29
|
+
Specification: https://packaging.python.org/en/latest/specifications/pyproject-toml/
|
|
30
|
+
Friendly guide: https://packaging.python.org/en/latest/guides/writing-pyproject-toml/
|
|
31
|
+
"""
|
|
32
|
+
logger.info(f"{command} pyproject file {pyproject_path}")
|
|
33
|
+
updatable = 0
|
|
34
|
+
project_dir = os.path.abspath(os.path.dirname(pyproject_path))
|
|
35
|
+
# parse pyproject.toml
|
|
36
|
+
with open(pyproject_path, "rb") as f:
|
|
37
|
+
try:
|
|
38
|
+
pyproject = tomllib.load(f)
|
|
39
|
+
except Exception as exc:
|
|
40
|
+
logger.error(f"error parsing {pyproject_path}: {exc}")
|
|
41
|
+
return updatable
|
|
42
|
+
project = pyproject.get("project", dict())
|
|
43
|
+
if not project:
|
|
44
|
+
logger.warning(f"no project defined in {pyproject_path}")
|
|
45
|
+
return updatable
|
|
46
|
+
projectname = project.get("name", None)
|
|
47
|
+
# project dependencies
|
|
48
|
+
if "dependencies" in project:
|
|
49
|
+
updatable += update_pyproject_dependencies(
|
|
50
|
+
project["dependencies"],
|
|
51
|
+
project_dir,
|
|
52
|
+
projectname,
|
|
53
|
+
command=command,
|
|
54
|
+
packages=packages,
|
|
55
|
+
exclude_newer=exclude_newer,
|
|
56
|
+
constraint_file=constraint_file,
|
|
57
|
+
color=color,
|
|
58
|
+
)
|
|
59
|
+
# update optional dependencies
|
|
60
|
+
for group, deps in project.get("optional-dependencies", {}).items():
|
|
61
|
+
updatable += update_pyproject_dependencies(
|
|
62
|
+
deps,
|
|
63
|
+
project_dir,
|
|
64
|
+
projectname,
|
|
65
|
+
group=group,
|
|
66
|
+
optional=True,
|
|
67
|
+
command=command,
|
|
68
|
+
packages=packages,
|
|
69
|
+
exclude_newer=exclude_newer,
|
|
70
|
+
constraint_file=constraint_file,
|
|
71
|
+
color=color,
|
|
72
|
+
)
|
|
73
|
+
# update dependency groups
|
|
74
|
+
for group, deps in pyproject.get("dependency-groups", {}).items():
|
|
75
|
+
updatable += update_pyproject_dependencies(
|
|
76
|
+
deps,
|
|
77
|
+
project_dir,
|
|
78
|
+
projectname,
|
|
79
|
+
group=group,
|
|
80
|
+
command=command,
|
|
81
|
+
packages=packages,
|
|
82
|
+
exclude_newer=exclude_newer,
|
|
83
|
+
constraint_file=constraint_file,
|
|
84
|
+
color=color,
|
|
85
|
+
)
|
|
86
|
+
if command == "update":
|
|
87
|
+
logger.info(f"updated {updatable} package version(s) in {pyproject_path}")
|
|
88
|
+
return updatable
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def update_pyproject_dependencies(
|
|
92
|
+
dependencies: list[str | dict],
|
|
93
|
+
project_dir: str,
|
|
94
|
+
projectname: str,
|
|
95
|
+
group: None | str = None,
|
|
96
|
+
optional=False,
|
|
97
|
+
command: None | str = None,
|
|
98
|
+
packages=None,
|
|
99
|
+
exclude_newer: None | str = None,
|
|
100
|
+
constraint_file: None | str = None,
|
|
101
|
+
color: bool = True,
|
|
102
|
+
) -> int:
|
|
103
|
+
"""Update given dependency list of a pyproject.toml file."""
|
|
104
|
+
updatable = 0
|
|
105
|
+
for dep in dependencies:
|
|
106
|
+
if isinstance(dep, dict):
|
|
107
|
+
logger.debug(f"skip include-group dependency {dep!r} in group {group}")
|
|
108
|
+
continue
|
|
109
|
+
try:
|
|
110
|
+
pkg_req = Requirement(dep)
|
|
111
|
+
except Exception as exc:
|
|
112
|
+
logger.debug(f"error parsing requirement: {exc}")
|
|
113
|
+
logger.info(f"skip unsupported dependency {dep!r}")
|
|
114
|
+
continue
|
|
115
|
+
if check_requirement(pkg_req, projectname=projectname) is None:
|
|
116
|
+
continue
|
|
117
|
+
|
|
118
|
+
# respect optional package filter
|
|
119
|
+
if packages and canonicalize_name(pkg_req.name) not in packages:
|
|
120
|
+
continue
|
|
121
|
+
try:
|
|
122
|
+
latest_version = get_latest_version(
|
|
123
|
+
pkg_req.name,
|
|
124
|
+
exclude_newer=exclude_newer,
|
|
125
|
+
constraint_file=constraint_file,
|
|
126
|
+
python_platform=get_python_platform_from_req(pkg_req),
|
|
127
|
+
python_version=get_min_python_version_from_req(pkg_req),
|
|
128
|
+
)
|
|
129
|
+
except subprocess.CalledProcessError as exc:
|
|
130
|
+
# error getting latest version
|
|
131
|
+
err = f"{exc}, output={exc.output}, stderr={exc.stderr}"
|
|
132
|
+
logger.warning(f"error getting latest version for '{pkg_req}': {err}")
|
|
133
|
+
latest_version = None
|
|
134
|
+
spec = next(s for s in pkg_req.specifier)
|
|
135
|
+
if latest_version is not None and latest_version != spec.version:
|
|
136
|
+
updatable += 1
|
|
137
|
+
version = (
|
|
138
|
+
colorize_updated_version(spec.version, latest_version)
|
|
139
|
+
if color
|
|
140
|
+
else latest_version
|
|
141
|
+
)
|
|
142
|
+
if command == "check":
|
|
143
|
+
logger.warning(f"found update '{dep}' --> {version}")
|
|
144
|
+
else:
|
|
145
|
+
logger.info(f"updating '{dep}' --> {version}")
|
|
146
|
+
newdep = dep.replace(spec.version, latest_version, 1)
|
|
147
|
+
update_pyproject_pkg(
|
|
148
|
+
newdep, pkg_req.name, project_dir, group=group, optional=optional
|
|
149
|
+
)
|
|
150
|
+
return updatable
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def update_pyproject_pkg(
|
|
154
|
+
dependency: str,
|
|
155
|
+
package: str,
|
|
156
|
+
projectdir: str,
|
|
157
|
+
group: None | str = None,
|
|
158
|
+
optional: bool = False,
|
|
159
|
+
) -> None:
|
|
160
|
+
"""Update one package in pyproject.toml."""
|
|
161
|
+
command = [
|
|
162
|
+
"uv",
|
|
163
|
+
"add",
|
|
164
|
+
"--project",
|
|
165
|
+
projectdir,
|
|
166
|
+
"--quiet",
|
|
167
|
+
"--frozen",
|
|
168
|
+
"--color=never",
|
|
169
|
+
f"--upgrade-package={package}",
|
|
170
|
+
]
|
|
171
|
+
if optional and group:
|
|
172
|
+
command.append("--optional")
|
|
173
|
+
command.append(group)
|
|
174
|
+
elif group:
|
|
175
|
+
command.append("--group")
|
|
176
|
+
command.append(group)
|
|
177
|
+
command.append(f"{dependency}")
|
|
178
|
+
logger.debug(f"running {' '.join(command)}")
|
|
179
|
+
subprocess.check_call(command)
|
puc/requirementstxt.py
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# Author: Bastian Kleineidam
|
|
2
|
+
# Copyright: GPL-v3
|
|
3
|
+
"""Handle requirements.txt files."""
|
|
4
|
+
|
|
5
|
+
import subprocess
|
|
6
|
+
import os
|
|
7
|
+
import re
|
|
8
|
+
import io
|
|
9
|
+
from packaging.utils import canonicalize_name
|
|
10
|
+
|
|
11
|
+
from .logging import logger, colorize_updated_version
|
|
12
|
+
from .dependencies import (
|
|
13
|
+
get_latest_version,
|
|
14
|
+
get_python_platform_from_req,
|
|
15
|
+
get_min_python_version_from_req,
|
|
16
|
+
parse_requirement,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# maximum recursion level for requirements.txt
|
|
21
|
+
max_rec_level = 5
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def handle_requirements_txt(
|
|
25
|
+
requirements_txt_path: str,
|
|
26
|
+
command: None | str = None,
|
|
27
|
+
packages=None,
|
|
28
|
+
exclude_newer: None | str = None,
|
|
29
|
+
constraint_file: None | str = None,
|
|
30
|
+
color: bool = True,
|
|
31
|
+
rec_level: int = 0,
|
|
32
|
+
handled_files: list[str] | None = None,
|
|
33
|
+
) -> int:
|
|
34
|
+
"""Check or update pinned dependencies of a requirements.txt file."""
|
|
35
|
+
msg = f"{command} requirements file {requirements_txt_path}"
|
|
36
|
+
if rec_level > 0:
|
|
37
|
+
msg += f", recursion level {rec_level}"
|
|
38
|
+
logger.info(msg)
|
|
39
|
+
if rec_level > max_rec_level:
|
|
40
|
+
logger.error(f"recursion level greater than maximum {max_rec_level}, ignoring")
|
|
41
|
+
return 0
|
|
42
|
+
if handled_files is None:
|
|
43
|
+
handled_files = [os.path.abspath(requirements_txt_path)]
|
|
44
|
+
else:
|
|
45
|
+
handled_files.append(os.path.abspath(requirements_txt_path))
|
|
46
|
+
output = io.StringIO()
|
|
47
|
+
updatable = 0
|
|
48
|
+
with open(requirements_txt_path) as f:
|
|
49
|
+
for line in f:
|
|
50
|
+
pkg_req = parse_requirement(
|
|
51
|
+
line, exclude_newer=exclude_newer, constraint_file=constraint_file
|
|
52
|
+
)
|
|
53
|
+
if pkg_req is None:
|
|
54
|
+
output.write(line)
|
|
55
|
+
elif isinstance(pkg_req, str):
|
|
56
|
+
output.write(line)
|
|
57
|
+
base_dir = os.path.dirname(requirements_txt_path)
|
|
58
|
+
requirements_txt_child = os.path.join(base_dir, pkg_req)
|
|
59
|
+
if os.path.abspath(requirements_txt_child) not in handled_files:
|
|
60
|
+
updatable += handle_requirements_txt(
|
|
61
|
+
requirements_txt_child,
|
|
62
|
+
command,
|
|
63
|
+
packages=packages,
|
|
64
|
+
exclude_newer=exclude_newer,
|
|
65
|
+
constraint_file=constraint_file,
|
|
66
|
+
color=color,
|
|
67
|
+
rec_level=rec_level + 1,
|
|
68
|
+
handled_files=handled_files,
|
|
69
|
+
)
|
|
70
|
+
else:
|
|
71
|
+
try:
|
|
72
|
+
latest_version = get_latest_version(
|
|
73
|
+
pkg_req.name,
|
|
74
|
+
exclude_newer=exclude_newer,
|
|
75
|
+
constraint_file=constraint_file,
|
|
76
|
+
python_platform=get_python_platform_from_req(pkg_req),
|
|
77
|
+
python_version=get_min_python_version_from_req(pkg_req),
|
|
78
|
+
)
|
|
79
|
+
except subprocess.CalledProcessError as exc:
|
|
80
|
+
# error getting latest version
|
|
81
|
+
err = f"{exc}, output={exc.output}, stderr={exc.stderr}"
|
|
82
|
+
logger.warning(
|
|
83
|
+
f"error getting latest version for {pkg_req.name!r}: {err}"
|
|
84
|
+
)
|
|
85
|
+
latest_version = None
|
|
86
|
+
spec = next(s for s in pkg_req.specifier)
|
|
87
|
+
if packages and canonicalize_name(pkg_req.name) not in packages:
|
|
88
|
+
output.write(line)
|
|
89
|
+
elif latest_version is not None and latest_version != spec.version:
|
|
90
|
+
updatable += 1
|
|
91
|
+
version = (
|
|
92
|
+
colorize_updated_version(spec.version, latest_version)
|
|
93
|
+
if color
|
|
94
|
+
else latest_version
|
|
95
|
+
)
|
|
96
|
+
if command == "check":
|
|
97
|
+
logger.warning(f"found update '{line.strip()}' --> {version}")
|
|
98
|
+
output.write(line)
|
|
99
|
+
else:
|
|
100
|
+
logger.info(f"updating '{line.strip()}' --> {version}")
|
|
101
|
+
output.write(
|
|
102
|
+
re.sub(
|
|
103
|
+
rf"(===?\s*){re.escape(spec.version)}",
|
|
104
|
+
rf"\g<1>{latest_version}",
|
|
105
|
+
line,
|
|
106
|
+
count=1,
|
|
107
|
+
)
|
|
108
|
+
)
|
|
109
|
+
else:
|
|
110
|
+
output.write(line)
|
|
111
|
+
if command == "update":
|
|
112
|
+
if updatable > 0:
|
|
113
|
+
with open(requirements_txt_path, "w") as f:
|
|
114
|
+
f.write(output.getvalue())
|
|
115
|
+
logger.info("updated {updatable} package version(s) in {requirements_txt_path}")
|
|
116
|
+
return updatable
|
puc/uvlock.py
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# Author: Bastian Kleineidam
|
|
2
|
+
# Copyright: GPL-v3
|
|
3
|
+
"""Handle uv.lock files."""
|
|
4
|
+
|
|
5
|
+
import subprocess
|
|
6
|
+
import os
|
|
7
|
+
import tomllib
|
|
8
|
+
from packaging.utils import canonicalize_name
|
|
9
|
+
|
|
10
|
+
from .logging import logger, colorize_updated_version
|
|
11
|
+
from .dependencies import (
|
|
12
|
+
get_latest_version,
|
|
13
|
+
is_newer_version,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def handle_uv_lock(
|
|
18
|
+
uvlock_path: str,
|
|
19
|
+
command: None | str = None,
|
|
20
|
+
packages=None,
|
|
21
|
+
exclude_newer: None | str = None,
|
|
22
|
+
constraint_file: None | str = None,
|
|
23
|
+
color: bool = True,
|
|
24
|
+
) -> int:
|
|
25
|
+
"""Check or update pinned dependencies of a uv.lock file."""
|
|
26
|
+
# warn about constraint_file?
|
|
27
|
+
logger.info(f"{command} lock file {uvlock_path}")
|
|
28
|
+
updatable = 0
|
|
29
|
+
project_dir = os.path.abspath(os.path.dirname(uvlock_path))
|
|
30
|
+
# parse uv.lock
|
|
31
|
+
with open(uvlock_path, "rb") as f:
|
|
32
|
+
try:
|
|
33
|
+
uvlock = tomllib.load(f)
|
|
34
|
+
except Exception as exc:
|
|
35
|
+
logger.error(f"error parsing {uvlock_path}: {exc}")
|
|
36
|
+
return updatable
|
|
37
|
+
uvpackages = uvlock.get("package", [])
|
|
38
|
+
if not uvpackages:
|
|
39
|
+
logger.warning(f"no packages defined in {uvlock_path}")
|
|
40
|
+
return updatable
|
|
41
|
+
|
|
42
|
+
for uvpackage in uvpackages:
|
|
43
|
+
name = uvpackage.get("name", None)
|
|
44
|
+
if name is None:
|
|
45
|
+
logger.warning(f"missing name in package {uvpackage}")
|
|
46
|
+
continue
|
|
47
|
+
version = uvpackage.get("version", None)
|
|
48
|
+
if version is None:
|
|
49
|
+
logger.warning(f"missing version in package {uvpackage}")
|
|
50
|
+
continue
|
|
51
|
+
updatable += update_uvlock_dependency(
|
|
52
|
+
name,
|
|
53
|
+
version,
|
|
54
|
+
project_dir,
|
|
55
|
+
command=command,
|
|
56
|
+
packages=packages,
|
|
57
|
+
exclude_newer=exclude_newer,
|
|
58
|
+
constraint_file=constraint_file,
|
|
59
|
+
color=color,
|
|
60
|
+
)
|
|
61
|
+
if command == "update":
|
|
62
|
+
logger.info(f"updated {updatable} package version(s) in {uvlock_path}")
|
|
63
|
+
return updatable
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def update_uvlock_dependency(
|
|
67
|
+
package,
|
|
68
|
+
version,
|
|
69
|
+
project_dir: str,
|
|
70
|
+
command: None | str = None,
|
|
71
|
+
packages=None,
|
|
72
|
+
exclude_newer: None | str = None,
|
|
73
|
+
constraint_file: None | str = None,
|
|
74
|
+
color: bool = True,
|
|
75
|
+
) -> int:
|
|
76
|
+
"""Update uv lock dependency."""
|
|
77
|
+
# respect optional package filter
|
|
78
|
+
if packages and canonicalize_name(package) not in packages:
|
|
79
|
+
return 0
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
latest_version = get_latest_version(
|
|
83
|
+
package,
|
|
84
|
+
exclude_newer=exclude_newer,
|
|
85
|
+
constraint_file=constraint_file,
|
|
86
|
+
)
|
|
87
|
+
except subprocess.CalledProcessError as exc:
|
|
88
|
+
# error getting latest version
|
|
89
|
+
err = f"{exc}, output={exc.output}, stderr={exc.stderr}"
|
|
90
|
+
logger.warning(f"error getting latest version for '{package}': {err}")
|
|
91
|
+
return 0
|
|
92
|
+
if latest_version == version:
|
|
93
|
+
return 0
|
|
94
|
+
if not is_newer_version(version, latest_version):
|
|
95
|
+
logger.warning(
|
|
96
|
+
f"{package} latest version {latest_version} is older than specified version {version}"
|
|
97
|
+
)
|
|
98
|
+
return 0
|
|
99
|
+
newversion = (
|
|
100
|
+
colorize_updated_version(version, latest_version) if color else latest_version
|
|
101
|
+
)
|
|
102
|
+
if command == "check":
|
|
103
|
+
logger.warning(f"found update '{package}=={version}' --> {newversion}")
|
|
104
|
+
else:
|
|
105
|
+
logger.info(f"updating '{package}=={version}' --> {newversion}")
|
|
106
|
+
update_uvlock_pkg(package, project_dir, exclude_newer=exclude_newer)
|
|
107
|
+
return 1
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def update_uvlock_pkg(
|
|
111
|
+
package: str,
|
|
112
|
+
projectdir: str,
|
|
113
|
+
exclude_newer: None | str = None,
|
|
114
|
+
) -> None:
|
|
115
|
+
"""Update one package in pyproject.toml."""
|
|
116
|
+
command = [
|
|
117
|
+
"uv",
|
|
118
|
+
"lock",
|
|
119
|
+
"--project",
|
|
120
|
+
projectdir,
|
|
121
|
+
"--quiet",
|
|
122
|
+
"--color=never",
|
|
123
|
+
f"--upgrade-package={package}",
|
|
124
|
+
]
|
|
125
|
+
if exclude_newer:
|
|
126
|
+
command.extend(
|
|
127
|
+
(
|
|
128
|
+
"--exclude-newer",
|
|
129
|
+
exclude_newer,
|
|
130
|
+
)
|
|
131
|
+
)
|
|
132
|
+
logger.debug(f"running {' '.join(command)}")
|
|
133
|
+
subprocess.check_call(command)
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: python-update-checker
|
|
3
|
+
Version: 0.5
|
|
4
|
+
License-Expression: GPL-3.0-only
|
|
5
|
+
Classifier: Environment :: Console
|
|
6
|
+
Classifier: Intended Audience :: Developers
|
|
7
|
+
Classifier: Development Status :: 4 - Beta
|
|
8
|
+
Classifier: Operating System :: OS Independent
|
|
9
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
14
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
15
|
+
Requires-Dist: packaging>=26.1
|
|
16
|
+
Requires-Python: >=3.11
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
python-update-checker
|
|
20
|
+
======================
|
|
21
|
+
Python-update-checker (puc) checks or updates pinned dependencies
|
|
22
|
+
in `pyproject.toml`, `requirements.txt` or `uv.lock` files.
|
|
23
|
+
|
|
24
|
+
See https://github.com/astral-sh/uv/issues/6794 for a discussion
|
|
25
|
+
about different pinning strategies.
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
Features
|
|
29
|
+
---------
|
|
30
|
+
|
|
31
|
+
* updates pinned dependencies, ignores unpinned dependencies
|
|
32
|
+
* supports pyproject.toml, uv.lock and requirements.txt formats
|
|
33
|
+
* supports `[project.dependencies]`, `[project.optional-dependencies]` and `[dependency-groups]` in pyproject.toml
|
|
34
|
+
* supports recursive references (-r) in requirements.txt formats
|
|
35
|
+
* can run in check only mode, ie. it checks if updates are available
|
|
36
|
+
* limit updates to specific packages
|
|
37
|
+
* limit updates with package version constraints (ie. "django<6")
|
|
38
|
+
* limit updates to versions that were uploaded prior to a given date
|
|
39
|
+
* (limited) support for environment markers, ie. `"pywin32==311; os_name=='nt'"`
|
|
40
|
+
* runs on Linux, MacOS and Windows platforms
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
Examples
|
|
44
|
+
---------
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
$ # check all pinned packages for updates in pyproject.toml
|
|
48
|
+
$ puc check pyproject.toml
|
|
49
|
+
puc INFO: check pyproject file pyproject.toml
|
|
50
|
+
puc WARNING: found update 'ty==0.0.29' --> 0.0.32
|
|
51
|
+
|
|
52
|
+
$ # update all pinned packages in pyproject.toml
|
|
53
|
+
$ # limit updates to versions that are at least 7 days old
|
|
54
|
+
$ puc --exclude-newer="7 days" update pyproject.toml
|
|
55
|
+
puc INFO: update pyproject file pyproject.toml
|
|
56
|
+
puc INFO: updating 'ty==0.0.29' --> 0.0.31
|
|
57
|
+
puc INFO: Wrote 1 updated package version(s) to pyproject.toml
|
|
58
|
+
|
|
59
|
+
$ # check all pinned packages for updates in requirements.txt
|
|
60
|
+
$ puc check requirements.txt
|
|
61
|
+
puc INFO: check requirements file requirements.txt
|
|
62
|
+
puc WARNING: found update 'argcomplete==3.6.1' --> 3.6.3
|
|
63
|
+
puc WARNING: found update 'Django==5.2.0' --> 6.0.4
|
|
64
|
+
|
|
65
|
+
$ # update only the django package version in requirements.txt
|
|
66
|
+
$ # limit updates to django versions less than 6
|
|
67
|
+
$ puc --package="Django" --constraints="Django<6" update requirements.txt
|
|
68
|
+
INFO: update requirements file requirements.txt
|
|
69
|
+
INFO: updating 'Django==5.2.0' --> 5.2.13
|
|
70
|
+
INFO: Wrote 1 updated package version(s) to requirements.txt
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Script behaviour
|
|
74
|
+
-----------------
|
|
75
|
+
|
|
76
|
+
The exit code of `puc check` is non-zero when updates are available.
|
|
77
|
+
|
|
78
|
+
Checking a `pyproject.toml` or `uv.lock`file with `puc` should be done
|
|
79
|
+
from the project of the `pyproject.toml` file,
|
|
80
|
+
especially if your project relies on a [project directory](https://docs.astral.sh/uv/concepts/projects/layout/) (for example to define additional packages index in pyproject.toml).
|
|
81
|
+
|
|
82
|
+
After updating versions in pyproject.toml, run `uv lock --upgrade` to update
|
|
83
|
+
the transitive dependencies in `uv.lock`.
|
|
84
|
+
|
|
85
|
+
Pinned dependencies are packages with `==` or `===` constraints and no wildcards in the version.
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
Installation
|
|
89
|
+
-------------
|
|
90
|
+
|
|
91
|
+
1) Install [python uv](https://docs.astral.sh/uv/getting-started/installation/)
|
|
92
|
+
2) Install puc with `uv pip install python-update-checker`.
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
Architecture
|
|
97
|
+
-------------
|
|
98
|
+
|
|
99
|
+
Dependencies are
|
|
100
|
+
|
|
101
|
+
* [uv](https://docs.astral.sh/uv/):
|
|
102
|
+
The uv binary must be available for the script to call.
|
|
103
|
+
puc uses `echo "package" | uv pip compile -` to get latest package versions.
|
|
104
|
+
puc uses `uv add "package==<version>"` to update pyproject.toml dependencies.
|
|
105
|
+
|
|
106
|
+
* [packaging](https://packaging.pypa.io/):
|
|
107
|
+
Parses dependencies with the packaging.requirements.Requirement class.
|
|
108
|
+
|
|
109
|
+
puc needs Python >= 3.11 since it uses the tomllib Python module.
|
|
110
|
+
|
|
111
|
+
puc consists of a single python script. The script uses [inline script metadata](https://peps.python.org/pep-0723/) to be executed directly with `uv run --script`.
|
|
112
|
+
This enables simple packaging and installation.
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
Limitations
|
|
116
|
+
------------
|
|
117
|
+
|
|
118
|
+
* No support for custom dependency formats in pyproject.toml
|
|
119
|
+
(eg. `[tool.poetry.dependencies]`).
|
|
120
|
+
* puc has limited support for environment markers.
|
|
121
|
+
* Constraint references (`-c`) inside requirements.txt are not supported.
|
|
122
|
+
Use the `--constraints` option instead.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
puc/__init__.py,sha256=QCJeo6rsztgAeIuufaaId2f-x_XYLjs4UywOPp4w7ks,67
|
|
2
|
+
puc/cli.py,sha256=aLQ52sA59lc8xAK7gjR36nTqUYxZvkvh467i0cx8Zso,6956
|
|
3
|
+
puc/dependencies.py,sha256=7MY8fyGGGayztpQfPAyCKXnlPUABXJDF9mwqmeMEjZs,6450
|
|
4
|
+
puc/logging.py,sha256=VBojYqr7-l-aLeNqrBqTJyHuEINzdBx1CObQg5XiC-k,2222
|
|
5
|
+
puc/pyprojecttoml.py,sha256=TMhNEWqlRfjichuU0nVGHbBHFhYALu12DuOSyobT5oE,6179
|
|
6
|
+
puc/requirementstxt.py,sha256=V075kYSuD35cGJ8XGdjq_24_aSUcJi43KH9ALov4rSQ,4604
|
|
7
|
+
puc/uvlock.py,sha256=mKc3x2RhcmHzYe8PHfUQZBkJI0DzN4qpDCmOQzrYnAE,3948
|
|
8
|
+
python_update_checker-0.5.dist-info/WHEEL,sha256=iCTolw4aw2dP3yfM-EQCGTDsFCXL_ymmbYnBRVH7plA,81
|
|
9
|
+
python_update_checker-0.5.dist-info/entry_points.txt,sha256=mQuGfBukyjyo7ssOHSIycAlRdQkrA1ewAth6tsV_T60,38
|
|
10
|
+
python_update_checker-0.5.dist-info/METADATA,sha256=RgatXY5cl_Oy8z2NisqECNj5msIVOv2VlEsBKdQA5Eo,4562
|
|
11
|
+
python_update_checker-0.5.dist-info/RECORD,,
|