Skip to content

Commit a4b74b6

Browse files
classabbyampahesford
authored andcommitted
common/scripts/parse-py-metadata.py: add script to parse python module metadata
this script uses python3-packaging-bootstrap to extract information about python modules, including their names and dependencies for use by xbps-src hooks
1 parent d5a71b7 commit a4b74b6

File tree

1 file changed

+203
-0
lines changed

1 file changed

+203
-0
lines changed
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
#!/usr/bin/python3
2+
3+
# vim: set ts=4 sw=4 et:
4+
"""
5+
Usage:
6+
7+
./parse-py-metadata.py -S "$DESTDIR/$py3_sitelib" provides -v "$version"
8+
9+
extract the names of top-level packages from:
10+
- $DESTDIR/$py3_sitelib/*.dist-info/METADATA
11+
- $DESTDIR/$py3_sitelib/*.egg-info/PKG-INFO
12+
13+
./parse-py-metadata.py -S "$DESTDIR/$py3_sitelib" [-s] [-C] depends -e "extra1 extra2 ..."
14+
-D "$XBPS_STATEDIR/$pkgname-rdeps" -V <( xbps-query -R -p provides -s "py3:" )
15+
16+
check that the dependencies of a package match what's listed in the python
17+
package metadata, using the virtual package provides entries generated by
18+
`parse-py-metadata.py provides`.
19+
20+
This script requires python3-packaging-bootstrap to be installed in the chroot
21+
to run (which should be taken care of by the python3-module and python3-pep517
22+
build styles).
23+
"""
24+
25+
import argparse
26+
from pathlib import Path
27+
from sys import stderr
28+
from typing import TYPE_CHECKING
29+
30+
if TYPE_CHECKING:
31+
from packaging.metadata import Metadata
32+
from packaging.requirements import Requirement
33+
from packaging.utils import canonicalize_name
34+
35+
36+
def msg_err(msg: str, *, nocolor: bool = False, strict: bool = False):
37+
if nocolor:
38+
print(msg, flush=True)
39+
else:
40+
color = "31" if strict else "33"
41+
print(f"\033[1m\033[{color}m{msg}\033[m", file=stderr, flush=True)
42+
43+
44+
def vpkgname(val: "str | Requirement", *, version: str | None = None) -> str:
45+
sfx = ""
46+
if version is not None:
47+
sfx = f"-{version}"
48+
if isinstance(val, Requirement):
49+
name = val.name
50+
else:
51+
name = val
52+
return f"py3:{canonicalize_name(name)}{sfx}"
53+
54+
55+
def getpkgname(pkgver: str) -> str:
56+
return pkgver.rpartition("-")[0]
57+
58+
59+
def getpkgversion(pkgver: str) -> str:
60+
return pkgver.rpartition("-")[2]
61+
62+
63+
def getpkgdepname(pkgdep: str) -> str:
64+
if "<" in pkgdep:
65+
return pkgdep.partition("<")[0]
66+
elif ">" in pkgdep:
67+
return pkgdep.partition(">")[0]
68+
else:
69+
return pkgdep.rpartition("-")[0]
70+
71+
72+
def match_markers(req: "Requirement", extras: set[str]) -> bool:
73+
# unconditional requirement
74+
if req.marker is None:
75+
return True
76+
77+
# check the requirement for each extra we want and without any extras
78+
if extras:
79+
return req.marker.evaluate() and any(req.marker.evaluate({"extra": e}) for e in extras)
80+
81+
return req.marker.evaluate()
82+
83+
84+
def find_metadata_files(sitepkgs: Path) -> list[Path]:
85+
metafiles = list(sitepkgs.glob("*.dist-info/METADATA"))
86+
metafiles.extend(sitepkgs.glob("*.egg-info/PKG-INFO"))
87+
return metafiles
88+
89+
90+
def parse_provides(args):
91+
out = set()
92+
93+
for metafile in find_metadata_files(args.sitepkgs):
94+
with metafile.open() as f:
95+
raw = f.read()
96+
97+
meta = Metadata.from_email(raw, validate=False)
98+
99+
out.add(vpkgname(meta.name, version=getpkgversion(args.pkgver)))
100+
if meta.provides_dist is not None:
101+
out.update(map(lambda n: vpkgname(n, version=getpkgversion(args.pkgver)), meta.provides_dist))
102+
# deprecated but may be used
103+
if meta.provides is not None:
104+
out.update(map(lambda n: vpkgname(n, version=getpkgversion(args.pkgver)), meta.provides))
105+
106+
print("\n".join(out), flush=True)
107+
108+
109+
def parse_depends(args):
110+
depends = dict()
111+
vpkgs = dict()
112+
extras = set(args.extras.split())
113+
114+
with args.vpkgs.open() as f:
115+
for ln in f.readlines():
116+
if not ln.strip():
117+
continue
118+
pkgver, _, rest = ln.partition(":")
119+
vpkgvers, _, _ = rest.strip().partition("(")
120+
pkg = getpkgname(pkgver)
121+
vpkg = map(getpkgname, vpkgvers.split())
122+
for v in vpkg:
123+
vpkgs[v] = pkg
124+
125+
if args.rdeps.exists():
126+
with args.rdeps.open() as f:
127+
rdeps = list(map(getpkgdepname, f.read().split()))
128+
else:
129+
rdeps = []
130+
131+
for metafile in find_metadata_files(args.sitepkgs):
132+
with metafile.open() as f:
133+
raw = f.read()
134+
135+
meta = Metadata.from_email(raw, validate=False)
136+
137+
if meta.requires_dist is not None:
138+
depends.update(map(lambda p: (vpkgname(p), None),
139+
filter(lambda r: match_markers(r, extras), meta.requires_dist)))
140+
# deprecated but may be used
141+
if meta.requires is not None:
142+
depends.update(map(lambda p: (vpkgname(p), None), meta.requires))
143+
144+
err = False
145+
unknown = False
146+
missing = []
147+
for k in depends.keys():
148+
if k in vpkgs.keys():
149+
pkgname = vpkgs[k]
150+
if pkgname in rdeps:
151+
print(f" PYTHON: {k} <-> {pkgname}", flush=True)
152+
else:
153+
msg_err(f" PYTHON: {k} <-> {pkgname} NOT IN depends PLEASE FIX!",
154+
nocolor=args.nocolor, strict=args.strict)
155+
missing.append(pkgname)
156+
err = True
157+
else:
158+
msg_err(f" PYTHON: {k} <-> UNKNOWN PKG PLEASE FIX!",
159+
nocolor=args.nocolor, strict=args.strict)
160+
unknown = True
161+
err = True
162+
163+
if missing or unknown:
164+
msg_err(f"=> {args.pkgver}: missing dependencies detected!",
165+
nocolor=args.nocolor, strict=args.strict)
166+
if missing:
167+
msg_err(f"=> {args.pkgver}: please add these packages to depends: {' '.join(sorted(missing))}",
168+
nocolor=args.nocolor, strict=args.strict)
169+
170+
if err and args.strict:
171+
exit(1)
172+
173+
174+
if __name__ == "__main__":
175+
parser = argparse.ArgumentParser()
176+
parser.add_argument("-S", dest="sitepkgs", type=Path)
177+
parser.add_argument("-v", dest="pkgver")
178+
parser.add_argument("-s", dest="strict", action="store_true")
179+
parser.add_argument("-C", dest="nocolor", action="store_true")
180+
subparsers = parser.add_subparsers()
181+
182+
prov_parser = subparsers.add_parser("provides")
183+
prov_parser.set_defaults(func=parse_provides)
184+
185+
deps_parser = subparsers.add_parser("depends")
186+
deps_parser.add_argument("-e", dest="extras", default="")
187+
deps_parser.add_argument("-V", dest="vpkgs", type=Path)
188+
deps_parser.add_argument("-D", dest="rdeps", type=Path)
189+
deps_parser.set_defaults(func=parse_depends)
190+
191+
args = parser.parse_args()
192+
193+
try:
194+
from packaging.metadata import Metadata
195+
from packaging.requirements import Requirement
196+
from packaging.utils import canonicalize_name
197+
except ImportError:
198+
msg_err(f"=> WARNING: {args.pkgver}: missing packaging module!\n"
199+
f"=> WARNING: {args.pkgver}: please add python3-packaging-bootstrap to hostmakedepends to run this check",
200+
nocolor=args.nocolor)
201+
exit(0)
202+
203+
args.func(args)

0 commit comments

Comments
 (0)