aboutsummaryrefslogtreecommitdiff
path: root/tools/update_coverage_deps.py
diff options
context:
space:
mode:
Diffstat (limited to 'tools/update_coverage_deps.py')
-rwxr-xr-xtools/update_coverage_deps.py248
1 files changed, 248 insertions, 0 deletions
diff --git a/tools/update_coverage_deps.py b/tools/update_coverage_deps.py
new file mode 100755
index 0000000..57b7850
--- /dev/null
+++ b/tools/update_coverage_deps.py
@@ -0,0 +1,248 @@
+#!/usr/bin/python3 -B
+# Copyright 2023 The Bazel Authors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""A small script to update bazel files within the repo.
+
+We are not running this with 'bazel run' to keep the dependencies minimal
+"""
+
+# NOTE @aignas 2023-01-09: We should only depend on core Python 3 packages.
+import argparse
+import difflib
+import json
+import pathlib
+import sys
+import textwrap
+from collections import defaultdict
+from dataclasses import dataclass
+from typing import Any
+from urllib import request
+
+# This should be kept in sync with //python:versions.bzl
+_supported_platforms = {
+ # Windows is unsupported right now
+ # "win_amd64": "x86_64-pc-windows-msvc",
+ "manylinux2014_x86_64": "x86_64-unknown-linux-gnu",
+ "manylinux2014_aarch64": "aarch64-unknown-linux-gnu",
+ "macosx_11_0_arm64": "aarch64-apple-darwin",
+ "macosx_10_9_x86_64": "x86_64-apple-darwin",
+}
+
+
+@dataclass
+class Dep:
+ name: str
+ platform: str
+ python: str
+ url: str
+ sha256: str
+
+ @property
+ def repo_name(self):
+ return f"pypi__{self.name}_{self.python}_{self.platform}"
+
+ def __repr__(self):
+ return "\n".join(
+ [
+ "(",
+ f' "{self.url}",',
+ f' "{self.sha256}",',
+ ")",
+ ]
+ )
+
+
+@dataclass
+class Deps:
+ deps: list[Dep]
+
+ def __repr__(self):
+ deps = defaultdict(dict)
+ for d in self.deps:
+ deps[d.python][d.platform] = d
+
+ parts = []
+ for python, contents in deps.items():
+ inner = textwrap.indent(
+ "\n".join([f'"{platform}": {d},' for platform, d in contents.items()]),
+ prefix=" ",
+ )
+ parts.append('"{}": {{\n{}\n}},'.format(python, inner))
+ return "{{\n{}\n}}".format(textwrap.indent("\n".join(parts), prefix=" "))
+
+
+def _get_platforms(filename: str, name: str, version: str, python_version: str):
+ return filename[
+ len(f"{name}-{version}-{python_version}-{python_version}-") : -len(".whl")
+ ].split(".")
+
+
+def _map(
+ name: str,
+ filename: str,
+ python_version: str,
+ url: str,
+ digests: list,
+ platform: str,
+ **kwargs: Any,
+):
+ if platform not in _supported_platforms:
+ return None
+
+ return Dep(
+ name=name,
+ platform=_supported_platforms[platform],
+ python=python_version,
+ url=url,
+ sha256=digests["sha256"],
+ )
+
+
+def _writelines(path: pathlib.Path, lines: list[str]):
+ with open(path, "w") as f:
+ f.writelines(lines)
+
+
+def _difflines(path: pathlib.Path, lines: list[str]):
+ with open(path) as f:
+ input = f.readlines()
+
+ rules_python = pathlib.Path(__file__).parent.parent
+ p = path.relative_to(rules_python)
+
+ print(f"Diff of the changes that would be made to '{p}':")
+ for line in difflib.unified_diff(
+ input,
+ lines,
+ fromfile=f"a/{p}",
+ tofile=f"b/{p}",
+ ):
+ print(line, end="")
+
+ # Add an empty line at the end of the diff
+ print()
+
+
+def _update_file(
+ path: pathlib.Path,
+ snippet: str,
+ start_marker: str,
+ end_marker: str,
+ dry_run: bool = True,
+):
+ with open(path) as f:
+ input = f.readlines()
+
+ out = []
+ skip = False
+ for line in input:
+ if skip:
+ if not line.startswith(end_marker):
+ continue
+
+ skip = False
+
+ out.append(line)
+
+ if not line.startswith(start_marker):
+ continue
+
+ skip = True
+ out.extend([f"{line}\n" for line in snippet.splitlines()])
+
+ if dry_run:
+ _difflines(path, out)
+ else:
+ _writelines(path, out)
+
+
+def _parse_args() -> argparse.Namespace:
+ parser = argparse.ArgumentParser(__doc__)
+ parser.add_argument(
+ "--name",
+ default="coverage",
+ type=str,
+ help="The name of the package",
+ )
+ parser.add_argument(
+ "version",
+ type=str,
+ help="The version of the package to download",
+ )
+ parser.add_argument(
+ "--py",
+ nargs="+",
+ type=str,
+ default=["cp38", "cp39", "cp310", "cp311"],
+ help="Supported python versions",
+ )
+ parser.add_argument(
+ "--dry-run",
+ action="store_true",
+ help="Wether to write to files",
+ )
+ return parser.parse_args()
+
+
+def main():
+ args = _parse_args()
+
+ api_url = f"https://pypi.python.org/pypi/{args.name}/{args.version}/json"
+ req = request.Request(api_url)
+ with request.urlopen(req) as response:
+ data = json.loads(response.read().decode("utf-8"))
+
+ urls = []
+ for u in data["urls"]:
+ if u["yanked"]:
+ continue
+
+ if not u["filename"].endswith(".whl"):
+ continue
+
+ if u["python_version"] not in args.py:
+ continue
+
+ if f'_{u["python_version"]}m_' in u["filename"]:
+ continue
+
+ platforms = _get_platforms(
+ u["filename"],
+ args.name,
+ args.version,
+ u["python_version"],
+ )
+
+ result = [_map(name=args.name, platform=p, **u) for p in platforms]
+ urls.extend(filter(None, result))
+
+ urls.sort(key=lambda x: f"{x.python}_{x.platform}")
+
+ rules_python = pathlib.Path(__file__).parent.parent
+
+ # Update the coverage_deps, which are used to register deps
+ _update_file(
+ path=rules_python / "python" / "private" / "coverage_deps.bzl",
+ snippet=f"_coverage_deps = {repr(Deps(urls))}\n",
+ start_marker="#START: managed by update_coverage_deps.py script",
+ end_marker="#END: managed by update_coverage_deps.py script",
+ dry_run=args.dry_run,
+ )
+
+ return
+
+
+if __name__ == "__main__":
+ main()