#!/usr/bin/env python3 # Copyright (C) The Arvados Authors. All rights reserved. # # SPDX-License-Identifier: AGPL-3.0 """pypkg_info.py - Introspect installed Python packages This tool can read metadata about any Python package installed in the current environment and report it out in various formats. We use this mainly to pass information through when building distribution packages. """ import argparse import enum import importlib.metadata import os import sys from pathlib import PurePath class RawFormat: def format_metadata(self, key, value): return value def format_path(self, path): return str(path) class FPMFormat(RawFormat): PYTHON_METADATA_MAP = { 'summary': 'description', } def format_metadata(self, key, value): key = key.lower() key = self.PYTHON_METADATA_MAP.get(key, key) return f'--{key}={value}' class Formats(enum.Enum): RAW = RawFormat FPM = FPMFormat @classmethod def from_arg(cls, arg): try: return cls[arg.upper()] except KeyError: raise ValueError(f"unknown format {arg!r}") from None def report_binfiles(args): bin_names = [ PurePath('bin', path.name) for pkg_name in args.package_names for path in importlib.metadata.distribution(pkg_name).files if path.parts[-3:-1] == ('..', 'bin') ] fmt = args.format.value().format_path return (fmt(path) for path in bin_names) def report_metadata(args): dist = importlib.metadata.distribution(args.package_name) fmt = args.format.value().format_metadata for key in args.metadata_key: yield fmt(key, dist.metadata.get(key, '')) def unescape_str(arg): arg = arg.replace('\'', '\\\'') return eval(f"'''{arg}'''", {}) def parse_arguments(arglist=None): parser = argparse.ArgumentParser() parser.set_defaults(action=None) format_names = ', '.join(fmt.name.lower() for fmt in Formats) parser.add_argument( '--format', '-f', choices=list(Formats), default=Formats.RAW, type=Formats.from_arg, help=f"Output format. Choices are: {format_names}", ) parser.add_argument( '--delimiter', '-d', default='\n', type=unescape_str, help="Line ending. Python backslash escapes are supported. Default newline.", ) subparsers = parser.add_subparsers() binfiles = subparsers.add_parser('binfiles') binfiles.set_defaults(action=report_binfiles) binfiles.add_argument( 'package_names', nargs=argparse.ONE_OR_MORE, ) metadata = subparsers.add_parser('metadata') metadata.set_defaults(action=report_metadata) metadata.add_argument( 'package_name', ) metadata.add_argument( 'metadata_key', nargs=argparse.ONE_OR_MORE, ) args = parser.parse_args() if args.action is None: parser.error("subcommand is required") return args def main(arglist=None): args = parse_arguments(arglist) try: for line in args.action(args): print(line, end=args.delimiter) except importlib.metadata.PackageNotFoundError as error: print(f"error: package not found: {error.args[0]}", file=sys.stderr) return os.EX_NOTFOUND else: return os.EX_OK if __name__ == '__main__': exit(main())