diff --git a/pythondistdeps.py b/pythondistdeps.py index 47a40b3..31b4737 100755 --- a/pythondistdeps.py +++ b/pythondistdeps.py @@ -3,6 +3,7 @@ # # Copyright 2010 Per Øyvind Karlsen # Copyright 2015 Neal Gompa +# Copyright 2020 SUSE LLC # # This program is free software. It may be redistributed and/or modified under # the terms of the LGPL version 2.1 (or later). @@ -11,7 +12,7 @@ # from __future__ import print_function -from getopt import getopt +import argparse from os.path import basename, dirname, isdir, sep from sys import argv, stdin, version from distutils.sysconfig import get_python_lib @@ -57,20 +58,22 @@ class RpmVersion(): rpm_suffix = '' return '{}{}{}'.format(rpm_epoch, rpm_version, rpm_suffix) + def convert_compatible(name, operator, version_id): if version_id.endswith('.*'): print('Invalid requirement: {} {} {}'.format(name, operator, version_id)) - exit(65) # os.EX_DATAERR + exit(65) # os.EX_DATAERR version = RpmVersion(version_id) if len(version.version) == 1: print('Invalid requirement: {} {} {}'.format(name, operator, version_id)) - exit(65) # os.EX_DATAERR + exit(65) # os.EX_DATAERR upper_version = RpmVersion(version_id) upper_version.version.pop() upper_version.increment() return '({} >= {} with {} < {})'.format( name, version, name, upper_version) + def convert_equal(name, operator, version_id): if version_id.endswith('.*'): version_id = version_id[:-2] + '.0' @@ -78,13 +81,15 @@ def convert_equal(name, operator, version_id): version = RpmVersion(version_id) return '{} = {}'.format(name, version) + def convert_arbitrary_equal(name, operator, version_id): if version_id.endswith('.*'): print('Invalid requirement: {} {} {}'.format(name, operator, version_id)) - exit(65) # os.EX_DATAERR + exit(65) # os.EX_DATAERR version = RpmVersion(version_id) return '{} = {}'.format(name, version) + def convert_not_equal(name, operator, version_id): if version_id.endswith('.*'): version_id = version_id[:-2] @@ -96,6 +101,7 @@ def convert_not_equal(name, operator, version_id): return '({} < {} or {} > {})'.format( name, version, name, lower_version) + def convert_ordered(name, operator, version_id): if version_id.endswith('.*'): # PEP 440 does not define semantics for prefix matching @@ -112,81 +118,46 @@ def convert_ordered(name, operator, version_id): version = RpmVersion(version_id) return '{} {} {}'.format(name, operator, version) + OPERATORS = {'~=': convert_compatible, '==': convert_equal, '===': convert_arbitrary_equal, '!=': convert_not_equal, '<=': convert_ordered, - '<': convert_ordered, + '<': convert_ordered, '>=': convert_ordered, - '>': convert_ordered} + '>': convert_ordered} + def convert(name, operator, version_id): return OPERATORS[operator](name, operator, version_id) -opts, args = getopt( - argv[1:], 'hPRrCEMmLl:', - ['help', 'provides', 'requires', 'recommends', 'conflicts', 'extras', 'majorver-provides', 'majorver-only', 'legacy-provides' , 'legacy']) - -Provides = False -Requires = False -Recommends = False -Conflicts = False -Extras = False -Provides_PyMajorVer_Variant = False -PyMajorVer_Deps = False -legacy_Provides = False -legacy = False - def normalize_name(name): """https://www.python.org/dev/peps/pep-0503/#normalized-names""" import re return re.sub(r'[-_.]+', '-', name).lower() -for o, a in opts: - if o in ('-h', '--help'): - print('-h, --help\tPrint help') - print('-P, --provides\tPrint Provides') - print('-R, --requires\tPrint Requires') - print('-r, --recommends\tPrint Recommends') - print('-C, --conflicts\tPrint Conflicts') - print('-E, --extras\tPrint Extras ') - print('-M, --majorver-provides\tPrint extra Provides with Python major version only') - print('-m, --majorver-only\tPrint Provides/Requires with Python major version only') - print('-L, --legacy-provides\tPrint extra legacy pythonegg Provides') - print('-l, --legacy\tPrint legacy pythonegg Provides/Requires instead') - exit(1) - elif o in ('-P', '--provides'): - Provides = True - elif o in ('-R', '--requires'): - Requires = True - elif o in ('-r', '--recommends'): - Recommends = True - elif o in ('-C', '--conflicts'): - Conflicts = True - elif o in ('-E', '--extras'): - Extras = True - elif o in ('-M', '--majorver-provides'): - Provides_PyMajorVer_Variant = True - elif o in ('-m', '--majorver-only'): - PyMajorVer_Deps = True - elif o in ('-L', '--legacy-provides'): - legacy_Provides = True - elif o in ('-l', '--legacy'): - legacy = True -if Requires: - py_abi = True -else: - py_abi = False +parser = argparse.ArgumentParser(prog=argv[0]) +group = parser.add_mutually_exclusive_group(required=True) +group.add_argument('-P', '--provides', action='store_true', help='Print Provides') +group.add_argument('-R', '--requires', action='store_true', help='Print Requires') +group.add_argument('-r', '--recommends', action='store_true', help='Print Recommends') +group.add_argument('-C', '--conflicts', action='store_true', help='Print Conflicts') +group.add_argument('-E', '--extras', action='store_true', help='Print Extras') +parser.add_argument('-M', '--majorver-provides', action='store_true', help='Print extra Provides with Python major version only') +parser.add_argument('-m', '--majorver-only', action='store_true', help='Print Provides/Requires with Python major version only') +parser.add_argument('-L', '--legacy-provides', action='store_true', help='Print extra legacy pythonegg Provides') +parser.add_argument('-l', '--legacy', action='store_true', help='Print legacy pythonegg Provides/Requires instead') +parser.add_argument('files', nargs=argparse.REMAINDER) +args = parser.parse_args() + +py_abi = args.requires py_deps = {} -if args: - files = args -else: - files = stdin.readlines() -for f in files: + +for f in (args.files or stdin.readlines()): f = f.strip() lower = f.lower() name = 'python(abi)' @@ -235,7 +206,16 @@ for f in files: warn("Version for {!r} has not been found".format(dist), RuntimeWarning) continue - # XXX: https://github.com/pypa/setuptools/pull/1275 + # pkg_resources use platform.python_version to evaluate if a + # dependency is relevant based on environment markers [1], + # e.g. requirement `argparse;python_version<"2.7"` + # + # Since we're running this script on one Python version while + # possibly evaluating packages for different versions, we mock the + # platform.python_version function. Discussed upstream [2]. + # + # [1] https://www.python.org/dev/peps/pep-0508/#environment-markers + # [2] https://github.com/pypa/setuptools/pull/1275 import platform platform.python_version = lambda: dist.py_version @@ -246,31 +226,31 @@ for f in files: # See https://bugzilla.redhat.com/show_bug.cgi?id=1791530 normalized_name = normalize_name(dist.project_name) - if Provides_PyMajorVer_Variant or PyMajorVer_Deps or legacy_Provides or legacy: + if args.majorver_provides or args.majorver_only or args.legacy_provides or args.legacy: # Get the Python major version pyver_major = dist.py_version.split('.')[0] - if Provides: + if args.provides: # If egg/dist metadata says package name is python, we provide python(abi) if dist.key == 'python': name = 'python(abi)' if name not in py_deps: py_deps[name] = [] py_deps[name].append(('==', dist.py_version)) - if not legacy or not PyMajorVer_Deps: + if not args.legacy or not args.majorver_only: name = 'python{}dist({})'.format(dist.py_version, dist.key) if name not in py_deps: py_deps[name] = [] name_ = 'python{}dist({})'.format(dist.py_version, normalized_name) if name_ not in py_deps: py_deps[name_] = [] - if Provides_PyMajorVer_Variant or PyMajorVer_Deps: + if args.majorver_provides or args.majorver_only: pymajor_name = 'python{}dist({})'.format(pyver_major, dist.key) if pymajor_name not in py_deps: py_deps[pymajor_name] = [] pymajor_name_ = 'python{}dist({})'.format(pyver_major, normalized_name) if pymajor_name_ not in py_deps: py_deps[pymajor_name_] = [] - if legacy or legacy_Provides: + if args.legacy or args.legacy_provides: legacy_name = 'pythonegg({})({})'.format(pyver_major, dist.key) if legacy_name not in py_deps: py_deps[legacy_name] = [] @@ -278,17 +258,17 @@ for f in files: version = dist.version spec = ('==', version) if spec not in py_deps[name]: - if not legacy: + if not args.legacy: py_deps[name].append(spec) if name != name_: py_deps[name_].append(spec) - if Provides_PyMajorVer_Variant: + if args.majorver_provides: py_deps[pymajor_name].append(spec) if pymajor_name != pymajor_name_: py_deps[pymajor_name_].append(spec) - if legacy or legacy_Provides: + if args.legacy or args.legacy_provides: py_deps[legacy_name].append(spec) - if Requires or (Recommends and dist.extras): + if args.requires or (args.recommends and dist.extras): name = 'python(abi)' # If egg/dist metadata says package name is python, we don't add dependency on python(abi) if dist.key == 'python': @@ -302,26 +282,26 @@ for f in files: if spec not in py_deps[name]: py_deps[name].append(spec) deps = dist.requires() - if Recommends: + if args.recommends: depsextras = dist.requires(extras=dist.extras) - if not Requires: + if not args.requires: for dep in reversed(depsextras): if dep in deps: depsextras.remove(dep) deps = depsextras # console_scripts/gui_scripts entry points need pkg_resources from setuptools if ((dist.get_entry_map('console_scripts') or - dist.get_entry_map('gui_scripts')) and - (lower.endswith('.egg') or - lower.endswith('.egg-info'))): + dist.get_entry_map('gui_scripts')) and + (lower.endswith('.egg') or + lower.endswith('.egg-info'))): # stick them first so any more specific requirement overrides it deps.insert(0, Requirement.parse('setuptools')) # add requires/recommends based on egg/dist metadata for dep in deps: - if legacy: + if args.legacy: name = 'pythonegg({})({})'.format(pyver_major, dep.key) else: - if PyMajorVer_Deps: + if args.majorver_only: name = 'python{}dist({})'.format(pyver_major, dep.key) else: name = 'python{}dist({})'.format(dist.py_version, dep.key) @@ -334,7 +314,7 @@ for f in files: py_deps[name] = [] # Unused, for automatic sub-package generation based on 'extras' from egg/dist metadata # TODO: implement in rpm later, or...? - if Extras: + if args.extras: deps = dist.requires() extras = dist.extras print(extras) @@ -356,7 +336,7 @@ for f in files: print('%%description\t{}'.format(extra)) print('{} extra for {} python package'.format(extra, dist.key)) print('%%files\t\textras-{}\n'.format(extra)) - if Conflicts: + if args.conflicts: # Should we really add conflicts for extras? # Creating a meta package per extra with recommends on, which has # the requires/conflicts in stead might be a better solution...