d77d134
#!/usr/bin/python3 -s
ad70cab
# -*- coding: utf-8 -*-
ad70cab
#
ad70cab
# Copyright 2010 Per √ėyvind Karlsen <proyvind@moondrake.org>
ad70cab
# Copyright 2015 Neal Gompa <ngompa13@gmail.com>
1639424
# Copyright 2020 SUSE LLC
ad70cab
#
ad70cab
# This program is free software. It may be redistributed and/or modified under
ad70cab
# the terms of the LGPL version 2.1 (or later).
ad70cab
#
ad70cab
# RPM python dependency generator, using .egg-info/.egg-link/.dist-info data
ad70cab
#
ad70cab
ad70cab
from __future__ import print_function
1639424
import argparse
ad70cab
from distutils.sysconfig import get_python_lib
438d8d3
from os.path import dirname, sep
438d8d3
import re
438d8d3
from sys import argv, stdin, stderr
ad70cab
from warnings import warn
ad70cab
438d8d3
from packaging.requirements import Requirement as Requirement_
438d8d3
from packaging.version import parse
b44c808
import packaging.markers
b44c808
b44c808
# Monkey patching packaging.markers to handle extras names in a
b44c808
# case-insensitive manner:
b44c808
#   pip considers dnspython[DNSSEC] and dnspython[dnssec] to be equal, but
b44c808
#   packaging markers treat extras in a case-sensitive manner. To solve this
b44c808
#   issue, we introduce a comparison operator that compares case-insensitively
b44c808
#   if both sides of the comparison are strings. And then we inject this
b44c808
#   operator into packaging.markers to be used when comparing names of extras.
b44c808
# Fedora BZ: https://bugzilla.redhat.com/show_bug.cgi?id=1936875
b44c808
# Upstream issue: https://discuss.python.org/t/what-extras-names-are-treated-as-equal-and-why/7614
b44c808
# - After it's established upstream what is the canonical form of an extras
b44c808
#   name, we plan to open an issue with packaging to hopefully solve this
b44c808
#   there without having to resort to monkeypatching.
b44c808
def str_lower_eq(a, b):
b44c808
    if isinstance(a, str) and isinstance(b, str):
b44c808
        return a.lower() == b.lower()
b44c808
    else:
b44c808
        return a == b
b44c808
packaging.markers._operators["=="] = str_lower_eq
438d8d3
438d8d3
try:
438d8d3
    from importlib.metadata import PathDistribution
438d8d3
except ImportError:
438d8d3
    from importlib_metadata import PathDistribution
438d8d3
438d8d3
try:
438d8d3
    from pathlib import Path
438d8d3
except ImportError:
438d8d3
    from pathlib2 import Path
438d8d3
438d8d3
438d8d3
def normalize_name(name):
438d8d3
    """https://www.python.org/dev/peps/pep-0503/#normalized-names"""
438d8d3
    return re.sub(r'[-_.]+', '-', name).lower()
438d8d3
438d8d3
438d8d3
def legacy_normalize_name(name):
438d8d3
    """Like pkg_resources Distribution.key property"""
438d8d3
    return re.sub(r'[-_]+', '-', name).lower()
438d8d3
438d8d3
438d8d3
class Requirement(Requirement_):
438d8d3
    def __init__(self, requirement_string):
438d8d3
        super(Requirement, self).__init__(requirement_string)
438d8d3
        self.normalized_name = normalize_name(self.name)
438d8d3
        self.legacy_normalized_name = legacy_normalize_name(self.name)
438d8d3
438d8d3
438d8d3
class Distribution(PathDistribution):
438d8d3
    def __init__(self, path):
438d8d3
        super(Distribution, self).__init__(Path(path))
438d8d3
        self.normalized_name = normalize_name(self.name)
438d8d3
        self.legacy_normalized_name = legacy_normalize_name(self.name)
438d8d3
        self.requirements = [Requirement(r) for r in self.requires or []]
438d8d3
        self.extras = [
3a4efad
            v.lower() for k, v in self.metadata.items() if k == 'Provides-Extra']
438d8d3
        self.py_version = self._parse_py_version(path)
438d8d3
48510ee
    # `name` is defined as a property exactly like this in Python 3.10 in the
48510ee
    # PathDistribution class. Due to that we can't redefine `name` as a normal
48510ee
    # attribute. So we copied the Python 3.10 definition here into the code so
48510ee
    # that it works also on previous Python/importlib_metadata versions.
48510ee
    @property
48510ee
    def name(self):
48510ee
        """Return the 'Name' metadata for the distribution package."""
48510ee
        return self.metadata['Name']
48510ee
438d8d3
    def _parse_py_version(self, path):
438d8d3
        # Try to parse the Python version from the path the metadata
438d8d3
        # resides at (e.g. /usr/lib/pythonX.Y/site-packages/...)
438d8d3
        res = re.search(r"/python(?P<pyver>\d+\.\d+)/", path)
438d8d3
        if res:
438d8d3
            return res.group('pyver')
438d8d3
        # If that hasn't worked, attempt to parse it from the metadata
438d8d3
        # directory name
438d8d3
        res = re.search(r"-py(?P<pyver>\d+.\d+)[.-]egg-info$", path)
438d8d3
        if res:
438d8d3
            return res.group('pyver')
438d8d3
        return None
438d8d3
438d8d3
    def requirements_for_extra(self, extra):
438d8d3
        extra_deps = []
438d8d3
        for req in self.requirements:
438d8d3
            if not req.marker:
438d8d3
                continue
438d8d3
            if req.marker.evaluate(get_marker_env(self, extra)):
438d8d3
                extra_deps.append(req)
438d8d3
        return extra_deps
438d8d3
438d8d3
    def __repr__(self):
438d8d3
        return '{} from {}'.format(self.name, self._path)
438d8d3
ad70cab
783dcc7
class RpmVersion():
783dcc7
    def __init__(self, version_id):
438d8d3
        version = parse(version_id)
783dcc7
        if isinstance(version._version, str):
783dcc7
            self.version = version._version
783dcc7
        else:
783dcc7
            self.epoch = version._version.epoch
783dcc7
            self.version = list(version._version.release)
783dcc7
            self.pre = version._version.pre
783dcc7
            self.dev = version._version.dev
783dcc7
            self.post = version._version.post
783dcc7
783dcc7
    def increment(self):
783dcc7
        self.version[-1] += 1
783dcc7
        self.pre = None
783dcc7
        self.dev = None
783dcc7
        self.post = None
783dcc7
        return self
783dcc7
783dcc7
    def __str__(self):
783dcc7
        if isinstance(self.version, str):
783dcc7
            return self.version
783dcc7
        if self.epoch:
783dcc7
            rpm_epoch = str(self.epoch) + ':'
783dcc7
        else:
783dcc7
            rpm_epoch = ''
0ec8581
        while len(self.version) > 1 and self.version[-1] == 0:
783dcc7
            self.version.pop()
783dcc7
        rpm_version = '.'.join(str(x) for x in self.version)
783dcc7
        if self.pre:
783dcc7
            rpm_suffix = '~{}'.format(''.join(str(x) for x in self.pre))
783dcc7
        elif self.dev:
79790d1
            rpm_suffix = '~~{}'.format(''.join(str(x) for x in self.dev))
783dcc7
        elif self.post:
783dcc7
            rpm_suffix = '^post{}'.format(self.post[1])
783dcc7
        else:
783dcc7
            rpm_suffix = ''
783dcc7
        return '{}{}{}'.format(rpm_epoch, rpm_version, rpm_suffix)
783dcc7
1639424
783dcc7
def convert_compatible(name, operator, version_id):
783dcc7
    if version_id.endswith('.*'):
098c48d
        print("*** INVALID_REQUIREMENT_ERROR___SEE_STDERR ***")
098c48d
        print('Invalid requirement: {} {} {}'.format(name, operator, version_id), file=stderr)
1639424
        exit(65)  # os.EX_DATAERR
783dcc7
    version = RpmVersion(version_id)
783dcc7
    if len(version.version) == 1:
098c48d
        print("*** INVALID_REQUIREMENT_ERROR___SEE_STDERR ***")
098c48d
        print('Invalid requirement: {} {} {}'.format(name, operator, version_id), file=stderr)
1639424
        exit(65)  # os.EX_DATAERR
783dcc7
    upper_version = RpmVersion(version_id)
783dcc7
    upper_version.version.pop()
783dcc7
    upper_version.increment()
783dcc7
    return '({} >= {} with {} < {})'.format(
783dcc7
        name, version, name, upper_version)
783dcc7
1639424
783dcc7
def convert_equal(name, operator, version_id):
783dcc7
    if version_id.endswith('.*'):
783dcc7
        version_id = version_id[:-2] + '.0'
783dcc7
        return convert_compatible(name, '~=', version_id)
783dcc7
    version = RpmVersion(version_id)
783dcc7
    return '{} = {}'.format(name, version)
783dcc7
1639424
783dcc7
def convert_arbitrary_equal(name, operator, version_id):
783dcc7
    if version_id.endswith('.*'):
098c48d
        print("*** INVALID_REQUIREMENT_ERROR___SEE_STDERR ***")
098c48d
        print('Invalid requirement: {} {} {}'.format(name, operator, version_id), file=stderr)
1639424
        exit(65)  # os.EX_DATAERR
783dcc7
    version = RpmVersion(version_id)
783dcc7
    return '{} = {}'.format(name, version)
783dcc7
1639424
783dcc7
def convert_not_equal(name, operator, version_id):
783dcc7
    if version_id.endswith('.*'):
783dcc7
        version_id = version_id[:-2]
783dcc7
        version = RpmVersion(version_id)
783dcc7
        lower_version = RpmVersion(version_id).increment()
783dcc7
    else:
783dcc7
        version = RpmVersion(version_id)
783dcc7
        lower_version = version
783dcc7
    return '({} < {} or {} > {})'.format(
783dcc7
        name, version, name, lower_version)
783dcc7
1639424
783dcc7
def convert_ordered(name, operator, version_id):
783dcc7
    if version_id.endswith('.*'):
783dcc7
        # PEP 440 does not define semantics for prefix matching
783dcc7
        # with ordered comparisons
783dcc7
        version_id = version_id[:-2]
783dcc7
        version = RpmVersion(version_id)
fbe1c77
        if operator == '>':
fbe1c77
            # distutils will allow a prefix match with '>'
783dcc7
            operator = '>='
fbe1c77
        if operator == '<=':
fbe1c77
            # distutils will not allow a prefix match with '<='
fbe1c77
            operator = '<'
783dcc7
    else:
783dcc7
        version = RpmVersion(version_id)
783dcc7
    return '{} {} {}'.format(name, operator, version)
783dcc7
1639424
783dcc7
OPERATORS = {'~=': convert_compatible,
783dcc7
             '==': convert_equal,
783dcc7
             '===': convert_arbitrary_equal,
783dcc7
             '!=': convert_not_equal,
783dcc7
             '<=': convert_ordered,
1639424
             '<': convert_ordered,
783dcc7
             '>=': convert_ordered,
1639424
             '>': convert_ordered}
1639424
783dcc7
783dcc7
def convert(name, operator, version_id):
972beac
    try:
972beac
        return OPERATORS[operator](name, operator, version_id)
972beac
    except Exception as exc:
972beac
        raise RuntimeError("Cannot process Python package version `{}` for name `{}`".
972beac
                           format(version_id, name)) from exc
783dcc7
783dcc7
438d8d3
def get_marker_env(dist, extra):
438d8d3
    # packaging uses a default environment using
438d8d3
    # platform.python_version to evaluate if a dependency is relevant
438d8d3
    # based on environment markers [1],
438d8d3
    # e.g. requirement `argparse;python_version<"2.7"`
438d8d3
    #
438d8d3
    # Since we're running this script on one Python version while
438d8d3
    # possibly evaluating packages for different versions, we
438d8d3
    # set up an environment with the version we want to evaluate.
438d8d3
    #
438d8d3
    # [1] https://www.python.org/dev/peps/pep-0508/#environment-markers
438d8d3
    return {"python_full_version": dist.py_version,
438d8d3
            "python_version": dist.py_version,
438d8d3
            "extra": extra}
7d819e0
ad70cab
d48f350
if __name__ == "__main__":
d48f350
    """To allow this script to be importable (and its classes/functions
d48f350
       reused), actions are performed only when run as a main script."""
d48f350
d48f350
    parser = argparse.ArgumentParser(prog=argv[0])
d48f350
    group = parser.add_mutually_exclusive_group(required=True)
d48f350
    group.add_argument('-P', '--provides', action='store_true', help='Print Provides')
d48f350
    group.add_argument('-R', '--requires', action='store_true', help='Print Requires')
d48f350
    group.add_argument('-r', '--recommends', action='store_true', help='Print Recommends')
d48f350
    group.add_argument('-C', '--conflicts', action='store_true', help='Print Conflicts')
0c96654
    group.add_argument('-E', '--extras', action='store_true', help='[Unused] Generate spec file snippets for extras subpackages')
d48f350
    group_majorver = parser.add_mutually_exclusive_group()
d48f350
    group_majorver.add_argument('-M', '--majorver-provides', action='store_true', help='Print extra Provides with Python major version only')
d48f350
    group_majorver.add_argument('--majorver-provides-versions', action='append',
d48f350
                                help='Print extra Provides with Python major version only for listed '
d48f350
                                     'Python VERSIONS (appended or comma separated without spaces, e.g. 2.7,3.9)')
d48f350
    parser.add_argument('-m', '--majorver-only', action='store_true', help='Print Provides/Requires with Python major version only')
d48f350
    parser.add_argument('-n', '--normalized-names-format', action='store',
d48f350
                        default="legacy-dots", choices=["pep503", "legacy-dots"],
d48f350
                        help='Format of normalized names according to pep503 or legacy format that allows dots [default]')
d48f350
    parser.add_argument('--normalized-names-provide-both', action='store_true',
d48f350
                        help='Provide both `pep503` and `legacy-dots` format of normalized names (useful for a transition period)')
d48f350
    parser.add_argument('-L', '--legacy-provides', action='store_true', help='Print extra legacy pythonegg Provides')
d48f350
    parser.add_argument('-l', '--legacy', action='store_true', help='Print legacy pythonegg Provides/Requires instead')
0c96654
    parser.add_argument('--require-extras-subpackages', action='store_true',
0c96654
                        help="If there is a dependency on a package with extras functionality, require the extras subpackage")
0c96654
    parser.add_argument('--package-name', action='store', help="Name of the RPM package that's being inspected. Required for extras requires/provides to work.")
3b1100b
    parser.add_argument('files', nargs=argparse.REMAINDER, help="Files from the RPM package that are to be inspected, can also be supplied on stdin")
d48f350
    args = parser.parse_args()
d48f350
d48f350
    py_abi = args.requires
d48f350
    py_deps = {}
d48f350
d48f350
    if args.majorver_provides_versions:
d48f350
        # Go through the arguments (can be specified multiple times),
d48f350
        # and parse individual versions (can be comma-separated)
d48f350
        args.majorver_provides_versions = [v for vstring in args.majorver_provides_versions
d48f350
                                             for v in vstring.split(",")]
d48f350
d48f350
    # If normalized_names_require_pep503 is True we require the pep503
d48f350
    # normalized name, if it is False we provide the legacy normalized name
d48f350
    normalized_names_require_pep503 = args.normalized_names_format == "pep503"
d48f350
d48f350
    # If normalized_names_provide_pep503/legacy is True we provide the
d48f350
    #   pep503/legacy normalized name, if it is False we don't
d48f350
    normalized_names_provide_pep503 = \
d48f350
        args.normalized_names_format == "pep503" or args.normalized_names_provide_both
d48f350
    normalized_names_provide_legacy = \
d48f350
        args.normalized_names_format == "legacy-dots" or args.normalized_names_provide_both
d48f350
d48f350
    # At least one type of normalization must be provided
d48f350
    assert normalized_names_provide_pep503 or normalized_names_provide_legacy
d48f350
0c96654
    # Is this script being run for an extras subpackage?
0c96654
    extras_subpackage = None
7398b71
    if args.package_name and '+' in args.package_name:
7398b71
        # The extras names are encoded in the package names after the + sign.
7398b71
        # We take the part after the rightmost +, ignoring when empty,
7398b71
        # this allows packages like nicotine+ or c++ to work fine.
7398b71
        # While packages with names like +spam or foo+bar would break,
7398b71
        # names started with the plus sign are not very common
7398b71
        # and pluses in the middle can be easily replaced with dashes.
7398b71
        # Python extras names don't contain pluses according to PEP 508.
7398b71
        package_name_parts = args.package_name.rpartition('+')
3a4efad
        extras_subpackage = package_name_parts[2].lower() or None
0c96654
d48f350
    for f in (args.files or stdin.readlines()):
d48f350
        f = f.strip()
d48f350
        lower = f.lower()
d48f350
        name = 'python(abi)'
d48f350
        # add dependency based on path, versioned if within versioned python directory
d48f350
        if py_abi and (lower.endswith('.py') or lower.endswith('.pyc') or lower.endswith('.pyo')):
d48f350
            if name not in py_deps:
d48f350
                py_deps[name] = []
d48f350
            purelib = get_python_lib(standard_lib=0, plat_specific=0).split(version[:3])[0]
d48f350
            platlib = get_python_lib(standard_lib=0, plat_specific=1).split(version[:3])[0]
d48f350
            for lib in (purelib, platlib):
d48f350
                if lib in f:
d48f350
                    spec = ('==', f.split(lib)[1].split(sep)[0])
1523def
                    if spec not in py_deps[name]:
ad70cab
                        py_deps[name].append(spec)
1523def
d48f350
        # XXX: hack to workaround RPM internal dependency generator not passing directories
d48f350
        lower_dir = dirname(lower)
d48f350
        if lower_dir.endswith('.egg') or \
d48f350
                lower_dir.endswith('.egg-info') or \
d48f350
                lower_dir.endswith('.dist-info'):
d48f350
            lower = lower_dir
d48f350
            f = dirname(f)
d48f350
        # Determine provide, requires, conflicts & recommends based on egg/dist metadata
d48f350
        if lower.endswith('.egg') or \
d48f350
                lower.endswith('.egg-info') or \
d48f350
                lower.endswith('.dist-info'):
438d8d3
            dist = Distribution(f)
d48f350
            if not dist.py_version:
438d8d3
                warn("Version for {!r} has not been found".format(dist), RuntimeWarning)
438d8d3
                continue
438d8d3
438d8d3
            # If processing an extras subpackage:
438d8d3
            #   Check that the extras name is declared in the metadata, or
438d8d3
            #   that there are some dependencies associated with the extras
438d8d3
            #   name in the requires.txt (this is an outdated way to declare
438d8d3
            #   extras packages).
438d8d3
            # - If there is an extras package declared only in requires.txt
438d8d3
            #   without any dependencies, this check will fail. In that case
438d8d3
            #   make sure to use updated metadata and declare the extras
438d8d3
            #   package there.
438d8d3
            if extras_subpackage and extras_subpackage not in dist.extras and not dist.requirements_for_extra(extras_subpackage):
098c48d
                print("*** PYTHON_EXTRAS_NOT_FOUND_ERROR___SEE_STDERR ***")
0c96654
                print(f"\nError: The package name contains an extras name `{extras_subpackage}` that was not found in the metadata.\n"
098c48d
                      "Check if the extras were removed from the project. If so, consider removing the subpackage and obsoleting it from another.\n", file=stderr)
0c96654
                exit(65)  # os.EX_DATAERR
0c96654
d48f350
            if args.majorver_provides or args.majorver_provides_versions or \
d48f350
                    args.majorver_only or args.legacy_provides or args.legacy:
d48f350
                # Get the Python major version
d48f350
                pyver_major = dist.py_version.split('.')[0]
d48f350
            if args.provides:
0c96654
                extras_suffix = f"[{extras_subpackage}]" if extras_subpackage else ""
d48f350
                # If egg/dist metadata says package name is python, we provide python(abi)
438d8d3
                if dist.normalized_name == 'python':
d48f350
                    name = 'python(abi)'
ff085a0
                    if name not in py_deps:
ff085a0
                        py_deps[name] = []
d48f350
                    py_deps[name].append(('==', dist.py_version))
d48f350
                if not args.legacy or not args.majorver_only:
d48f350
                    if normalized_names_provide_legacy:
438d8d3
                        name = 'python{}dist({}{})'.format(dist.py_version, dist.legacy_normalized_name, extras_suffix)
d48f350
                        if name not in py_deps:
d48f350
                            py_deps[name] = []
d48f350
                    if normalized_names_provide_pep503:
438d8d3
                        name_ = 'python{}dist({}{})'.format(dist.py_version, dist.normalized_name, extras_suffix)
d48f350
                        if name_ not in py_deps:
d48f350
                            py_deps[name_] = []
d48f350
                if args.majorver_provides or args.majorver_only or \
d48f350
                        (args.majorver_provides_versions and dist.py_version in args.majorver_provides_versions):
d48f350
                    if normalized_names_provide_legacy:
438d8d3
                        pymajor_name = 'python{}dist({}{})'.format(pyver_major, dist.legacy_normalized_name, extras_suffix)
d48f350
                        if pymajor_name not in py_deps:
d48f350
                            py_deps[pymajor_name] = []
d48f350
                    if normalized_names_provide_pep503:
438d8d3
                        pymajor_name_ = 'python{}dist({}{})'.format(pyver_major, dist.normalized_name, extras_suffix)
d48f350
                        if pymajor_name_ not in py_deps:
d48f350
                            py_deps[pymajor_name_] = []
d48f350
                if args.legacy or args.legacy_provides:
438d8d3
                    legacy_name = 'pythonegg({})({})'.format(pyver_major, dist.legacy_normalized_name)
d48f350
                    if legacy_name not in py_deps:
d48f350
                        py_deps[legacy_name] = []
d48f350
                if dist.version:
d48f350
                    version = dist.version
d48f350
                    spec = ('==', version)
d48f350
d48f350
                    if normalized_names_provide_legacy:
d48f350
                        if spec not in py_deps[name]:
d48f350
                            py_deps[name].append(spec)
d48f350
                            if args.majorver_provides or \
d48f350
                                    (args.majorver_provides_versions and dist.py_version in args.majorver_provides_versions):
d48f350
                                py_deps[pymajor_name].append(spec)
d48f350
                    if normalized_names_provide_pep503:
d48f350
                        if spec not in py_deps[name_]:
d48f350
                            py_deps[name_].append(spec)
d48f350
                            if args.majorver_provides or \
d48f350
                                    (args.majorver_provides_versions and dist.py_version in args.majorver_provides_versions):
d48f350
                                py_deps[pymajor_name_].append(spec)
d48f350
                    if args.legacy or args.legacy_provides:
d48f350
                        if spec not in py_deps[legacy_name]:
d48f350
                            py_deps[legacy_name].append(spec)
d48f350
            if args.requires or (args.recommends and dist.extras):
d48f350
                name = 'python(abi)'
d48f350
                # If egg/dist metadata says package name is python, we don't add dependency on python(abi)
438d8d3
                if dist.normalized_name == 'python':
d48f350
                    py_abi = False
d48f350
                    if name in py_deps:
d48f350
                        py_deps.pop(name)
d48f350
                elif py_abi and dist.py_version:
d48f350
                    if name not in py_deps:
d48f350
                        py_deps[name] = []
d48f350
                    spec = ('==', dist.py_version)
ff085a0
                    if spec not in py_deps[name]:
ff085a0
                        py_deps[name].append(spec)
438d8d3
438d8d3
                if extras_subpackage:
438d8d3
                    deps = [d for d in dist.requirements_for_extra(extras_subpackage)]
438d8d3
                else:
438d8d3
                    deps = dist.requirements
438d8d3
d48f350
                # console_scripts/gui_scripts entry points need pkg_resources from setuptools
438d8d3
                if (dist.entry_points and
d48f350
                    (lower.endswith('.egg') or
d48f350
                     lower.endswith('.egg-info'))):
438d8d3
                    groups = {ep.group for ep in dist.entry_points}
438d8d3
                    if {"console_scripts", "gui_scripts"} & groups:
438d8d3
                        # stick them first so any more specific requirement
438d8d3
                        # overrides it
438d8d3
                        deps.insert(0, Requirement('setuptools'))
d48f350
                # add requires/recommends based on egg/dist metadata
ad70cab
                for dep in deps:
0c96654
                    # Even if we're requiring `foo[bar]`, also require `foo`
0c96654
                    # to be safe, and to make it discoverable through
0c96654
                    # `repoquery --whatrequires`
0c96654
                    extras_suffixes = [""]
0c96654
                    if args.require_extras_subpackages and dep.extras:
0c96654
                        # A dependency can have more than one extras,
0c96654
                        # i.e. foo[bar,baz], so let's go through all of them
3a4efad
                        extras_suffixes += [f"[{e.lower()}]" for e in dep.extras]
438d8d3
0c96654
                    for extras_suffix in extras_suffixes:
0c96654
                        if normalized_names_require_pep503:
438d8d3
                            dep_normalized_name = dep.normalized_name
ad70cab
                        else:
438d8d3
                            dep_normalized_name = dep.legacy_normalized_name
0c96654
0c96654
                        if args.legacy:
438d8d3
                            name = 'pythonegg({})({})'.format(pyver_major, dep.legacy_normalized_name)
0c96654
                        else:
0c96654
                            if args.majorver_only:
0c96654
                                name = 'python{}dist({}{})'.format(pyver_major, dep_normalized_name, extras_suffix)
0c96654
                            else:
0c96654
                                name = 'python{}dist({}{})'.format(dist.py_version, dep_normalized_name, extras_suffix)
438d8d3
438d8d3
                        if dep.marker and not args.recommends and not extras_subpackage:
438d8d3
                            if not dep.marker.evaluate(get_marker_env(dist, '')):
438d8d3
                                continue
438d8d3
438d8d3
                        if name not in py_deps:
ad70cab
                            py_deps[name] = []
438d8d3
                        for spec in dep.specifier:
438d8d3
                            if (spec.operator, spec.version) not in py_deps[name]:
438d8d3
                                py_deps[name].append((spec.operator, spec.version))
438d8d3
d48f350
            # Unused, for automatic sub-package generation based on 'extras' from egg/dist metadata
d48f350
            # TODO: implement in rpm later, or...?
d48f350
            if args.extras:
438d8d3
                print(dist.extras)
438d8d3
                for extra in dist.extras:
d48f350
                    print('%%package\textras-{}'.format(extra))
438d8d3
                    print('Summary:\t{} extra for {} python package'.format(extra, dist.legacy_normalized_name))
d48f350
                    print('Group:\t\tDevelopment/Python')
438d8d3
                    for dep in dist.requirements_for_extra(extra):
438d8d3
                        for spec in dep.specifier:
438d8d3
                            if spec.operator == '!=':
438d8d3
                                print('Conflicts:\t{} {} {}'.format(dep.legacy_normalized_name, '==', spec.version))
d48f350
                            else:
438d8d3
                                print('Requires:\t{} {} {}'.format(dep.legacy_normalized_name, spec.operator, spec.version))
d48f350
                    print('%%description\t{}'.format(extra))
438d8d3
                    print('{} extra for {} python package'.format(extra, dist.legacy_normalized_name))
d48f350
                    print('%%files\t\textras-{}\n'.format(extra))
d48f350
            if args.conflicts:
d48f350
                # Should we really add conflicts for extras?
d48f350
                # Creating a meta package per extra with recommends on, which has
d48f350
                # the requires/conflicts in stead might be a better solution...
438d8d3
                for dep in dist.requirements:
438d8d3
                    for spec in dep.specifier:
438d8d3
                        if spec.operator == '!=':
438d8d3
                            if dep.legacy_normalized_name not in py_deps:
438d8d3
                                py_deps[dep.legacy_normalized_name] = []
438d8d3
                            spec = ('==', spec.version)
438d8d3
                            if spec not in py_deps[dep.legacy_normalized_name]:
438d8d3
                                py_deps[dep.legacy_normalized_name].append(spec)
438d8d3
438d8d3
    for name in sorted(py_deps):
d48f350
        if py_deps[name]:
d48f350
            # Print out versioned provides, requires, recommends, conflicts
d48f350
            spec_list = []
d48f350
            for spec in py_deps[name]:
d48f350
                spec_list.append(convert(name, spec[0], spec[1]))
d48f350
            if len(spec_list) == 1:
d48f350
                print(spec_list[0])
d48f350
            else:
d48f350
                # Sort spec_list so that the results can be tested easily
d48f350
                print('({})'.format(' with '.join(sorted(spec_list))))
ca811db
        else:
d48f350
            # Print out unversioned provides, requires, recommends, conflicts
d48f350
            print(name)