Skip to content

Commit ae1afb0

Browse files
committed
Merge remote-tracking branch 'origin/master' into feature/mypy
2 parents 77f9093 + 48c5270 commit ae1afb0

File tree

12 files changed

+1014
-142
lines changed

12 files changed

+1014
-142
lines changed

requirements.txt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
1-
six
2-
appdirs
1+
# Updated by depupdate.py on 2025-05-29T00:19:36 by user
2+
# Do not edit this section manually.
3+
six==1.17.0
4+
appdirs==1.4.4

scripts/depupdate.py

Lines changed: 390 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,390 @@
1+
#!/usr/bin/env python3
2+
# coding=utf8
3+
"""depupdate.py
4+
5+
Update or check Python project dependencies.
6+
"""
7+
from __future__ import absolute_import, print_function, unicode_literals
8+
9+
import getpass
10+
import json
11+
import logging
12+
import os
13+
import re
14+
import sys
15+
from datetime import datetime
16+
from subprocess import CalledProcessError, PIPE, check_call, run
17+
18+
# script version
19+
__version__ = "0.0.1"
20+
21+
_SCRIPT_NAME = os.path.splitext(os.path.basename(__file__))[0]
22+
23+
LOGGER = logging.getLogger(_SCRIPT_NAME)
24+
25+
26+
def _serialize_args(args):
27+
def _serialize_value(v):
28+
# If it's a basic type, return as is
29+
if isinstance(v, (str, int, float, bool, type(None))):
30+
return v
31+
# If it's a file-like object, show its class and name
32+
if hasattr(v, 'name') and hasattr(v, 'mode'):
33+
return {
34+
"class": v.__class__.__name__,
35+
"name": v.name,
36+
"mode": v.mode
37+
}
38+
# If it's a type, return its name
39+
if isinstance(v, type):
40+
return v.__name__
41+
# If it's an object with __dict__, show class and attributes
42+
if hasattr(v, '__dict__'):
43+
return {
44+
"class": v.__class__.__name__,
45+
"attributes": {
46+
k: _serialize_value(val)
47+
for k, val in vars(v).items()
48+
}
49+
}
50+
# Fallback to string
51+
return str(v)
52+
53+
return {
54+
k: _serialize_value(v)
55+
for k, v in vars(args).items()
56+
if v is not None
57+
}
58+
59+
60+
def argument_parser(**kwargs):
61+
"""Construct Argument Parser."""
62+
from argparse import (
63+
ArgumentParser,
64+
ArgumentDefaultsHelpFormatter,
65+
FileType,
66+
SUPPRESS
67+
)
68+
69+
_file_mode_suffix = "b" if sys.version_info[0] == 2 else ""
70+
_filetype_read = FileType("r+{0}".format(_file_mode_suffix))
71+
_filetype_write = FileType("w+{0}".format(_file_mode_suffix))
72+
73+
parser = ArgumentParser(**kwargs)
74+
parser.set_defaults(
75+
argument_default=SUPPRESS,
76+
conflict_handler="resolve",
77+
formatter_class=ArgumentDefaultsHelpFormatter,
78+
add_help=False
79+
)
80+
parser.add_argument(
81+
"-V", "--version",
82+
action="version",
83+
version=__version__
84+
)
85+
parser.add_argument(
86+
"-d", "--debug",
87+
"-v", "--verbose",
88+
dest="verbose",
89+
action="store_true",
90+
help="enable debug/verbose logging"
91+
)
92+
parser.add_argument(
93+
"--logfile",
94+
action="store",
95+
dest="logfile",
96+
required=False,
97+
help="path to log file"
98+
)
99+
parser.add_argument(
100+
"input",
101+
nargs="?",
102+
default="-",
103+
type=_filetype_read,
104+
help="program input"
105+
)
106+
parser.add_argument(
107+
"-o", "--output",
108+
action="store",
109+
nargs="?",
110+
default="-",
111+
required=False,
112+
type=_filetype_write,
113+
help="program output"
114+
)
115+
parser.add_argument(
116+
"requirements_file",
117+
nargs="?",
118+
default="requirements.txt",
119+
type=str,
120+
help="path to requirements.txt file (default: requirements.txt)"
121+
)
122+
parser.add_argument(
123+
"--update",
124+
action="store_true",
125+
dest="update",
126+
default=False,
127+
help="update dependencies to latest versions"
128+
)
129+
parser.add_argument(
130+
"--check",
131+
action="store_true",
132+
dest="check",
133+
default=False,
134+
help="check if dependencies are up to date"
135+
)
136+
parser.add_argument(
137+
"--write",
138+
action="store_true",
139+
dest="write",
140+
default=False,
141+
help="write latest versions to requirements file"
142+
)
143+
return parser
144+
145+
146+
def parse_requirements(file_path):
147+
"""Parse the requirements.txt file and return a list of dependencies."""
148+
# noinspection PyArgumentEqualDefault
149+
with open(file_path, 'r') as f:
150+
lines = f.readlines()
151+
dependencies = [
152+
line.strip()
153+
for line in lines
154+
if line.strip() and not line.startswith('#')
155+
]
156+
return dependencies
157+
158+
159+
def get_latest_version(package):
160+
"""Get the latest version of a package using pip.
161+
"""
162+
try:
163+
result = run(
164+
['pip', 'index', 'versions', package],
165+
stdout=PIPE,
166+
stderr=PIPE,
167+
text=True
168+
)
169+
if result.returncode == 0 and "Available versions:" in result.stdout:
170+
match = re.search(r'Available versions: (.+)', result.stdout)
171+
if match:
172+
versions = match.group(1).split(', ')
173+
return versions[0] # Return the latest version
174+
else:
175+
LOGGER.error(
176+
"failed to fetch versions for package: %s - %s",
177+
package, result.stderr
178+
)
179+
except Exception as e:
180+
LOGGER.error(
181+
"error fetching version for package: %s - %s",
182+
package, e
183+
)
184+
return None
185+
186+
187+
def update_requirements(file_path):
188+
"""Update the requirements.txt file with the latest versions.
189+
"""
190+
dependencies = parse_requirements(file_path)
191+
updated_dependencies = []
192+
for dep in dependencies:
193+
package, _, current_version = dep.partition('==')
194+
latest_version = get_latest_version(package)
195+
if latest_version and latest_version != current_version:
196+
print(f"updating {package} from {current_version} to {latest_version}")
197+
updated_dependencies.append(f"{package}=={latest_version}")
198+
else:
199+
updated_dependencies.append(dep)
200+
with open(file_path, 'w') as f:
201+
f.write('\n'.join(updated_dependencies) + '\n')
202+
203+
204+
def update_dependencies(requirements_file="requirements.txt"):
205+
"""Update all dependencies listed in requirements.txt.
206+
"""
207+
if not os.path.isfile(requirements_file):
208+
LOGGER.error("Requirements file not found: %s", requirements_file)
209+
return 1
210+
try:
211+
check_call([
212+
"python3", "-m", "pip", "install", "--upgrade", "-r",
213+
requirements_file
214+
])
215+
LOGGER.info(
216+
"successfully updated dependencies from requirements file: %s",
217+
requirements_file
218+
)
219+
return 0
220+
except CalledProcessError as e:
221+
LOGGER.error(
222+
"failed to update dependencies from requirements file: %s - %s",
223+
requirements_file, e
224+
)
225+
return e.returncode
226+
227+
228+
def write_latest_versions(requirements_file="requirements.txt"):
229+
"""Write the latest versions of dependencies to the requirements file.
230+
231+
Args:
232+
requirements_file: Path to the requirements.txt file.
233+
"""
234+
dependencies = parse_requirements(requirements_file)
235+
236+
updated = []
237+
for dep in dependencies:
238+
LOGGER.info("checking-dependency: %s", dep)
239+
240+
package, _, current_version = dep.partition('==')
241+
242+
latest_version = get_latest_version(package)
243+
244+
if latest_version:
245+
LOGGER.info(
246+
"updating-package: %s from %s to %s",
247+
package, current_version or "not specified", latest_version
248+
)
249+
250+
updated.append("{package}=={latest_version}".format(
251+
package=package, latest_version=latest_version
252+
))
253+
else:
254+
LOGGER.warning(
255+
"no-latest-version-found: %s (current version: %s)",
256+
package, current_version or "not specified"
257+
)
258+
updated.append(dep)
259+
260+
now = datetime.now().isoformat(timespec="seconds")
261+
user = getpass.getuser()
262+
comment = (
263+
"# Updated by depupdate.py on {now} by {user}\n"
264+
"# Do not edit this section manually.\n"
265+
).format(now=now, user=user)
266+
267+
LOGGER.info("writing-updated-dependencies: %s", requirements_file)
268+
269+
with open(requirements_file, 'w') as f:
270+
f.write(comment)
271+
for n, dep in enumerate(updated, start=1):
272+
f.write(dep + '\n')
273+
LOGGER.debug("updated-dependency-%d: %s", n, dep)
274+
275+
LOGGER.info(
276+
"requirements file updated with latest versions: %s",
277+
requirements_file
278+
)
279+
return 0
280+
281+
282+
def check_dependencies(requirements_file="requirements.txt"):
283+
"""Check if dependencies are up to date.
284+
"""
285+
if not os.path.isfile(requirements_file):
286+
LOGGER.error("requirements-file-not-found: %s", requirements_file)
287+
return 1
288+
289+
dependencies = parse_requirements(requirements_file)
290+
291+
up_to_date = 0
292+
outdated = 0
293+
missing_version = 0
294+
295+
for dep in dependencies:
296+
LOGGER.info("checking-dependency: %s", dep)
297+
298+
package, _, current_version = dep.partition('==')
299+
latest_version = get_latest_version(package)
300+
301+
if not current_version:
302+
LOGGER.warning(
303+
"no-package-version-specified: '%s' (latest: '%s')",
304+
package, latest_version or "unknown"
305+
)
306+
missing_version += 1
307+
elif latest_version and latest_version != current_version:
308+
LOGGER.warning(
309+
"package-is-outdated: name='%s', current-version='%s', "
310+
"latest-version='%s'",
311+
package, current_version, latest_version
312+
)
313+
outdated += 1
314+
else:
315+
LOGGER.info(
316+
"package-is-up-to-date: name='%s', version='%s'",
317+
package, current_version
318+
)
319+
up_to_date += 1
320+
321+
LOGGER.info(
322+
"dependency-check-summary: up-to-date=%d outdated=%d missing-version=%d",
323+
up_to_date, outdated, missing_version
324+
)
325+
326+
if outdated > 0 or missing_version > 0:
327+
LOGGER.error(
328+
"some dependencies are outdated or missing versions - "
329+
"please update your requirements file"
330+
)
331+
return 1
332+
333+
LOGGER.info("all dependencies are up-to-date")
334+
return 0
335+
336+
337+
def main(*args):
338+
"""CLI Entry Point."""
339+
path = os.path.abspath(__file__)
340+
prog = os.path.splitext(os.path.basename(path))[0]
341+
342+
parser = argument_parser(prog=prog, description=__doc__)
343+
args = parser.parse_args(args if args else None)
344+
345+
# setup logging
346+
level = logging.DEBUG if args.verbose else logging.INFO
347+
logging.basicConfig(level=level, filename=args.logfile)
348+
LOGGER.setLevel(level)
349+
350+
LOGGER.info("starting: %s (v%s)", prog, __version__)
351+
LOGGER.debug(
352+
"parsed arguments: %s",
353+
json.dumps(_serialize_args(args), indent=2, sort_keys=True)
354+
)
355+
356+
if args.update:
357+
LOGGER.info("updating-dependencies: %s", args.requirements_file)
358+
return_code = update_dependencies(args.requirements_file)
359+
360+
elif args.check:
361+
if args.write:
362+
LOGGER.info(
363+
"checking and updating requirements file: %s",
364+
args.requirements_file
365+
)
366+
return_code = write_latest_versions(args.requirements_file)
367+
else:
368+
LOGGER.info("checking-dependencies: %s", args.requirements_file)
369+
return_code = check_dependencies(args.requirements_file)
370+
371+
else:
372+
LOGGER.error("no-action-specified: use --update or --check")
373+
parser.print_help()
374+
return_code = 1
375+
376+
LOGGER.info(
377+
"finished: %s (v%s) exiting with return code: %s",
378+
prog, __version__, return_code
379+
)
380+
381+
if args.input and not args.input.closed:
382+
args.input.close()
383+
if args.output and not args.output.closed:
384+
args.output.close()
385+
386+
return return_code
387+
388+
389+
if __name__ == "__main__":
390+
sys.exit(main())

0 commit comments

Comments
 (0)