Blame SOURCES/pythondistdeps.py

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