summaryrefslogtreecommitdiff
path: root/pythondistdeps.py
diff options
context:
space:
mode:
Diffstat (limited to 'pythondistdeps.py')
-rw-r--r--pythondistdeps.py626
1 files changed, 626 insertions, 0 deletions
diff --git a/pythondistdeps.py b/pythondistdeps.py
new file mode 100644
index 0000000..b43ed39
--- /dev/null
+++ b/pythondistdeps.py
@@ -0,0 +1,626 @@
+#!/usr/bin/python3 -s
+# -*- coding: utf-8 -*-
+#
+# Copyright 2010 Per Øyvind Karlsen <proyvind@moondrake.org>
+# Copyright 2015 Neal Gompa <ngompa13@gmail.com>
+# 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).
+#
+# RPM python dependency generator, using .egg-info/.egg-link/.dist-info data
+#
+
+from __future__ import print_function
+import argparse
+from os.path import dirname, sep
+import re
+from sys import argv, stdin, stderr, version_info
+from sysconfig import get_path
+from warnings import warn
+
+from packaging.requirements import Requirement as Requirement_
+from packaging.version import parse
+import packaging.markers
+
+# Monkey patching packaging.markers to handle extras names in a
+# case-insensitive manner:
+# pip considers dnspython[DNSSEC] and dnspython[dnssec] to be equal, but
+# packaging markers treat extras in a case-sensitive manner. To solve this
+# issue, we introduce a comparison operator that compares case-insensitively
+# if both sides of the comparison are strings. And then we inject this
+# operator into packaging.markers to be used when comparing names of extras.
+# Fedora BZ: https://bugzilla.redhat.com/show_bug.cgi?id=1936875
+# Upstream issue: https://discuss.python.org/t/what-extras-names-are-treated-as-equal-and-why/7614
+# - After it's established upstream what is the canonical form of an extras
+# name, we plan to open an issue with packaging to hopefully solve this
+# there without having to resort to monkeypatching.
+def str_lower_eq(a, b):
+ if isinstance(a, str) and isinstance(b, str):
+ return a.lower() == b.lower()
+ else:
+ return a == b
+packaging.markers._operators["=="] = str_lower_eq
+
+try:
+ from importlib.metadata import PathDistribution
+except ImportError:
+ from importlib_metadata import PathDistribution
+
+try:
+ from pathlib import Path
+except ImportError:
+ from pathlib2 import Path
+
+
+def normalize_name(name):
+ """https://www.python.org/dev/peps/pep-0503/#normalized-names"""
+ return re.sub(r'[-_.]+', '-', name).lower()
+
+
+def legacy_normalize_name(name):
+ """Like pkg_resources Distribution.key property"""
+ return re.sub(r'[-_]+', '-', name).lower()
+
+
+class Requirement(Requirement_):
+ def __init__(self, requirement_string):
+ super(Requirement, self).__init__(requirement_string)
+ self.normalized_name = normalize_name(self.name)
+ self.legacy_normalized_name = legacy_normalize_name(self.name)
+
+
+class Distribution(PathDistribution):
+ def __init__(self, path):
+ super(Distribution, self).__init__(Path(path))
+
+ # Check that the initialization went well and metadata are not missing or corrupted
+ # name is the most important attribute, if it doesn't exist, import failed
+ if not self.name or not isinstance(self.name, str):
+ print("*** PYTHON_METADATA_FAILED_TO_PARSE_ERROR___SEE_STDERR ***")
+ print('Error: Python metadata at `{}` are missing or corrupted.'.format(path), file=stderr)
+ exit(65) # os.EX_DATAERR
+
+ self.normalized_name = normalize_name(self.name)
+ self.legacy_normalized_name = legacy_normalize_name(self.name)
+ self.requirements = [Requirement(r) for r in self.requires or []]
+ self.extras = [
+ v.lower() for k, v in self.metadata.items() if k == 'Provides-Extra']
+ self.py_version = self._parse_py_version(path)
+
+ # `name` is defined as a property exactly like this in Python 3.10 in the
+ # PathDistribution class. Due to that we can't redefine `name` as a normal
+ # attribute. So we copied the Python 3.10 definition here into the code so
+ # that it works also on previous Python/importlib_metadata versions.
+ @property
+ def name(self):
+ """Return the 'Name' metadata for the distribution package or None."""
+ return self.metadata.get('Name')
+
+ def _parse_py_version(self, path):
+ # Try to parse the Python version from the path the metadata
+ # resides at (e.g. /usr/lib/pythonX.Y/site-packages/...)
+ res = re.search(r"/python(?P<pyver>\d+\.\d+)/", path)
+ if res:
+ return res.group('pyver')
+ # If that hasn't worked, attempt to parse it from the metadata
+ # directory name
+ res = re.search(r"-py(?P<pyver>\d+.\d+)[.-]egg-info$", path)
+ if res:
+ return res.group('pyver')
+ return None
+
+ def requirements_for_extra(self, extra):
+ extra_deps = []
+ # we are only interested in dependencies with extra == 'our_extra' marker
+ for req in self.requirements:
+ # no marker at all, nothing to evaluate
+ if not req.marker:
+ continue
+ # does the marker include extra == 'our_extra'?
+ # we can only evaluate the marker as a whole,
+ # so we evaluate it twice (using 2 different marker_envs)
+ # and see if it only evaluates to True with our extra
+ if (req.marker.evaluate(get_marker_env(self, extra)) and
+ not req.marker.evaluate(get_marker_env(self, None))):
+ extra_deps.append(req)
+ return extra_deps
+
+ def __repr__(self):
+ return '{} from {}'.format(self.name, self._path)
+
+
+class RpmVersion():
+ def __init__(self, version_id):
+ version = parse(version_id)
+ if isinstance(version._version, str):
+ self.version = version._version
+ else:
+ self.epoch = version._version.epoch
+ self.version = list(version._version.release)
+ self.pre = version._version.pre
+ self.dev = version._version.dev
+ self.post = version._version.post
+ # version.local is ignored as it is not expected to appear
+ # in public releases
+ # https://www.python.org/dev/peps/pep-0440/#local-version-identifiers
+
+ def is_legacy(self):
+ return isinstance(self.version, str)
+
+ def increment(self):
+ self.version[-1] += 1
+ self.pre = None
+ self.dev = None
+ self.post = None
+ return self
+
+ def is_zero(self):
+ return self.__str__() == '0'
+
+ def __str__(self):
+ if self.is_legacy():
+ return self.version
+ if self.epoch:
+ rpm_epoch = str(self.epoch) + ':'
+ else:
+ rpm_epoch = ''
+ while len(self.version) > 1 and self.version[-1] == 0:
+ self.version.pop()
+ rpm_version = '.'.join(str(x) for x in self.version)
+ if self.pre:
+ rpm_suffix = '~{}'.format(''.join(str(x) for x in self.pre))
+ elif self.dev:
+ rpm_suffix = '~~{}'.format(''.join(str(x) for x in self.dev))
+ elif self.post:
+ rpm_suffix = '^post{}'.format(self.post[1])
+ else:
+ rpm_suffix = ''
+ return '{}{}{}'.format(rpm_epoch, rpm_version, rpm_suffix)
+
+
+def convert_compatible(name, operator, version_id):
+ if version_id.endswith('.*'):
+ print("*** INVALID_REQUIREMENT_ERROR___SEE_STDERR ***")
+ print('Invalid requirement: {} {} {}'.format(name, operator, version_id), file=stderr)
+ exit(65) # os.EX_DATAERR
+ version = RpmVersion(version_id)
+ if version.is_legacy():
+ # LegacyVersions are not supported in this context
+ print("*** INVALID_REQUIREMENT_ERROR___SEE_STDERR ***")
+ print('Invalid requirement: {} {} {}'.format(name, operator, version_id), file=stderr)
+ exit(65) # os.EX_DATAERR
+ if len(version.version) == 1:
+ print("*** INVALID_REQUIREMENT_ERROR___SEE_STDERR ***")
+ print('Invalid requirement: {} {} {}'.format(name, operator, version_id), file=stderr)
+ 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'
+ return convert_compatible(name, '~=', 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_ERROR___SEE_STDERR ***")
+ print('Invalid requirement: {} {} {}'.format(name, operator, version_id), file=stderr)
+ 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]
+ version = RpmVersion(version_id)
+ if version.is_legacy():
+ # LegacyVersions are not supported in this context
+ print("*** INVALID_REQUIREMENT_ERROR___SEE_STDERR ***")
+ print('Invalid requirement: {} {} {}'.format(name, operator, version_id), file=stderr)
+ exit(65) # os.EX_DATAERR
+ version_gt = RpmVersion(version_id).increment()
+ version_gt_operator = '>='
+ # Prevent dev and pre-releases from satisfying a < requirement
+ version = '{}~~'.format(version)
+ else:
+ version = RpmVersion(version_id)
+ version_gt = version
+ version_gt_operator = '>'
+ return '({} < {} or {} {} {})'.format(
+ name, version, name, version_gt_operator, version_gt)
+
+
+def convert_ordered(name, operator, version_id):
+ if version_id.endswith('.*'):
+ # PEP 440 does not define semantics for prefix matching
+ # with ordered comparisons
+ # see: https://github.com/pypa/packaging/issues/320
+ # and: https://github.com/pypa/packaging/issues/321
+ # This style of specifier is officially "unsupported",
+ # even though it is processed. Support may be removed
+ # in version 21.0.
+ version_id = version_id[:-2]
+ version = RpmVersion(version_id)
+ if operator == '>':
+ # distutils will allow a prefix match with '>'
+ operator = '>='
+ if operator == '<=':
+ # distutils will not allow a prefix match with '<='
+ operator = '<'
+ else:
+ version = RpmVersion(version_id)
+ # For backwards compatibility, fallback to previous behavior with LegacyVersions
+ if not version.is_legacy():
+ # Prevent dev and pre-releases from satisfying a < requirement
+ if operator == '<' and not version.pre and not version.dev and not version.post:
+ version = '{}~~'.format(version)
+ # Prevent post-releases from satisfying a > requirement
+ if operator == '>' and not version.pre and not version.dev and not version.post:
+ version = '{}.0'.format(version)
+ return '{} {} {}'.format(name, operator, version)
+
+
+OPERATORS = {'~=': convert_compatible,
+ '==': convert_equal,
+ '===': convert_arbitrary_equal,
+ '!=': convert_not_equal,
+ '<=': convert_ordered,
+ '<': convert_ordered,
+ '>=': convert_ordered,
+ '>': convert_ordered}
+
+
+def convert(name, operator, version_id):
+ try:
+ return OPERATORS[operator](name, operator, version_id)
+ except Exception as exc:
+ raise RuntimeError("Cannot process Python package version `{}` for name `{}`".
+ format(version_id, name)) from exc
+
+
+def get_marker_env(dist, extra):
+ # packaging uses a default environment using
+ # 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
+ # set up an environment with the version we want to evaluate.
+ #
+ # [1] https://www.python.org/dev/peps/pep-0508/#environment-markers
+ return {"python_full_version": dist.py_version,
+ "python_version": dist.py_version,
+ "extra": extra}
+
+
+def main():
+ """To allow this script to be importable (and its classes/functions
+ reused), actions are defined in the main function and are performed only
+ when run as a main script."""
+ 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='[Unused] Generate spec file snippets for extras subpackages')
+ group_majorver = parser.add_mutually_exclusive_group()
+ group_majorver.add_argument('-M', '--majorver-provides', action='store_true', help='Print extra Provides with Python major version only')
+ group_majorver.add_argument('--majorver-provides-versions', action='append',
+ help='Print extra Provides with Python major version only for listed '
+ 'Python VERSIONS (appended or comma separated without spaces, e.g. 2.7,3.9)')
+ parser.add_argument('-m', '--majorver-only', action='store_true', help='Print Provides/Requires with Python major version only')
+ parser.add_argument('-n', '--normalized-names-format', action='store',
+ default="legacy-dots", choices=["pep503", "legacy-dots"],
+ help='Format of normalized names according to pep503 or legacy format that allows dots [default]')
+ parser.add_argument('--normalized-names-provide-both', action='store_true',
+ help='Provide both `pep503` and `legacy-dots` format of normalized names (useful for a transition period)')
+ 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('--console-scripts-nodep-setuptools-since', action='store',
+ help='An optional Python version (X.Y), at least 3.8. '
+ 'For that version and any newer version, '
+ 'a dependency on "setuptools" WILL NOT be generated for packages with console_scripts/gui_scripts entry points. '
+ 'By setting this flag, you guarantee that setuptools >= 47.2.0 is used '
+ 'during the build of packages for this and any newer Python version.')
+ parser.add_argument('--require-extras-subpackages', action='store_true',
+ help="If there is a dependency on a package with extras functionality, require the extras subpackage")
+ parser.add_argument('--package-name', action='store', help="Name of the RPM package that's being inspected. Required for extras requires/provides to work.")
+ parser.add_argument('--namespace', action='store', help="Namespace for the printed Requires, Provides, Recommends and Conflicts")
+ parser.add_argument('--fail-if-zero', action='store_true', help='Fail the script if the automatically generated Provides version was 0, which usually indicates a packaging error.')
+ parser.add_argument('files', nargs=argparse.REMAINDER, help="Files from the RPM package that are to be inspected, can also be supplied on stdin")
+ args = parser.parse_args()
+
+ if args.fail_if_zero and not args.provides:
+ raise parser.error('--fail-if-zero only works with --provides')
+
+ py_abi = args.requires
+ py_deps = {}
+
+ if args.majorver_provides_versions:
+ # Go through the arguments (can be specified multiple times),
+ # and parse individual versions (can be comma-separated)
+ args.majorver_provides_versions = [v for vstring in args.majorver_provides_versions
+ for v in vstring.split(",")]
+
+ # If normalized_names_require_pep503 is True we require the pep503
+ # normalized name, if it is False we provide the legacy normalized name
+ normalized_names_require_pep503 = args.normalized_names_format == "pep503"
+
+ # If normalized_names_provide_pep503/legacy is True we provide the
+ # pep503/legacy normalized name, if it is False we don't
+ normalized_names_provide_pep503 = \
+ args.normalized_names_format == "pep503" or args.normalized_names_provide_both
+ normalized_names_provide_legacy = \
+ args.normalized_names_format == "legacy-dots" or args.normalized_names_provide_both
+
+ # At least one type of normalization must be provided
+ assert normalized_names_provide_pep503 or normalized_names_provide_legacy
+
+ if args.console_scripts_nodep_setuptools_since:
+ nodep_setuptools_pyversion = parse(args.console_scripts_nodep_setuptools_since)
+ if nodep_setuptools_pyversion < parse("3.8"):
+ print("Only version 3.8+ is supported in --console-scripts-nodep-setuptools-since", file=stderr)
+ print("*** PYTHON_EXTRAS_ARGUMENT_ERROR___SEE_STDERR ***")
+ exit(65) # os.EX_DATAERR
+ else:
+ nodep_setuptools_pyversion = None
+
+ # Is this script being run for an extras subpackage?
+ extras_subpackage = None
+ if args.package_name and '+' in args.package_name:
+ # The extras names are encoded in the package names after the + sign.
+ # We take the part after the rightmost +, ignoring when empty,
+ # this allows packages like nicotine+ or c++ to work fine.
+ # While packages with names like +spam or foo+bar would break,
+ # names started with the plus sign are not very common
+ # and pluses in the middle can be easily replaced with dashes.
+ # Python extras names don't contain pluses according to PEP 508.
+ package_name_parts = args.package_name.rpartition('+')
+ extras_subpackage = package_name_parts[2].lower() or None
+
+ namespace = (args.namespace + "({})") if args.namespace else "{}"
+
+ for f in (args.files or stdin.readlines()):
+ f = f.strip()
+ lower = f.lower()
+ name = 'python(abi)'
+ # add dependency based on path, versioned if within versioned python directory
+ if py_abi and (lower.endswith('.py') or lower.endswith('.pyc') or lower.endswith('.pyo')):
+ if name not in py_deps:
+ py_deps[name] = []
+ running_python_version = '{}.{}'.format(*version_info[:2])
+ purelib = get_path('purelib').split(running_python_version)[0]
+ platlib = get_path('platlib').split(running_python_version)[0]
+ for lib in (purelib, platlib):
+ if lib in f:
+ spec = ('==', f.split(lib)[1].split(sep)[0])
+ if spec not in py_deps[name]:
+ py_deps[name].append(spec)
+
+ # XXX: hack to workaround RPM internal dependency generator not passing directories
+ lower_dir = dirname(lower)
+ if lower_dir.endswith('.egg') or \
+ lower_dir.endswith('.egg-info') or \
+ lower_dir.endswith('.dist-info'):
+ lower = lower_dir
+ f = dirname(f)
+ # Determine provide, requires, conflicts & recommends based on egg/dist metadata
+ if lower.endswith('.egg') or \
+ lower.endswith('.egg-info') or \
+ lower.endswith('.dist-info'):
+ dist = Distribution(f)
+ if not dist.py_version:
+ warn("Version for {!r} has not been found".format(dist), RuntimeWarning)
+ continue
+
+ # If processing an extras subpackage:
+ # Check that the extras name is declared in the metadata, or
+ # that there are some dependencies associated with the extras
+ # name in the requires.txt (this is an outdated way to declare
+ # extras packages).
+ # - If there is an extras package declared only in requires.txt
+ # without any dependencies, this check will fail. In that case
+ # make sure to use updated metadata and declare the extras
+ # package there.
+ if extras_subpackage and extras_subpackage not in dist.extras and not dist.requirements_for_extra(extras_subpackage):
+ print("*** PYTHON_EXTRAS_NOT_FOUND_ERROR___SEE_STDERR ***")
+ print(f"\nError: The package name contains an extras name `{extras_subpackage}` that was not found in the metadata.\n"
+ "Check if the extras were removed from the project. If so, consider removing the subpackage and obsoleting it from another.\n", file=stderr)
+ exit(65) # os.EX_DATAERR
+
+ if args.majorver_provides or args.majorver_provides_versions or \
+ args.majorver_only or args.legacy_provides or args.legacy:
+ # Get the Python major version
+ pyver_major = dist.py_version.split('.')[0]
+ if args.provides:
+ extras_suffix = f"[{extras_subpackage}]" if extras_subpackage else ""
+ # If egg/dist metadata says package name is python, we provide python(abi)
+ if dist.normalized_name == 'python':
+ name = namespace.format('python(abi)')
+ if name not in py_deps:
+ py_deps[name] = []
+ py_deps[name].append(('==', dist.py_version))
+ if not args.legacy or not args.majorver_only:
+ if normalized_names_provide_legacy:
+ name = namespace.format('python{}dist({}{})').format(dist.py_version, dist.legacy_normalized_name, extras_suffix)
+ if name not in py_deps:
+ py_deps[name] = []
+ if normalized_names_provide_pep503:
+ name_ = namespace.format('python{}dist({}{})').format(dist.py_version, dist.normalized_name, extras_suffix)
+ if name_ not in py_deps:
+ py_deps[name_] = []
+ if args.majorver_provides or args.majorver_only or \
+ (args.majorver_provides_versions and dist.py_version in args.majorver_provides_versions):
+ if normalized_names_provide_legacy:
+ pymajor_name = namespace.format('python{}dist({}{})').format(pyver_major, dist.legacy_normalized_name, extras_suffix)
+ if pymajor_name not in py_deps:
+ py_deps[pymajor_name] = []
+ if normalized_names_provide_pep503:
+ pymajor_name_ = namespace.format('python{}dist({}{})').format(pyver_major, dist.normalized_name, extras_suffix)
+ if pymajor_name_ not in py_deps:
+ py_deps[pymajor_name_] = []
+ if args.legacy or args.legacy_provides:
+ legacy_name = namespace.format('pythonegg({})({})').format(pyver_major, dist.legacy_normalized_name)
+ if legacy_name not in py_deps:
+ py_deps[legacy_name] = []
+ if dist.version:
+ version = dist.version
+ spec = ('==', version)
+ if args.fail_if_zero:
+ if RpmVersion(version).is_zero():
+ print('*** PYTHON_PROVIDED_VERSION_NORMALIZES_TO_ZERO___SEE_STDERR ***')
+ print(f'\nError: The version in the Python package metadata {version} normalizes to zero.\n'
+ 'It\'s likely a packaging error caused by missing version information\n'
+ '(e.g. when using a version control system snapshot as a source).\n'
+ 'Try providing the version information manually when building the Python package,\n'
+ 'for example by setting the SETUPTOOLS_SCM_PRETEND_VERSION environment variable if the package uses setuptools_scm.\n'
+ 'If you are confident that the version of the Python package is intentionally zero,\n'
+ 'you may %define the _python_dist_allow_version_zero macro in the spec file to disable this check.\n', file=stderr)
+ exit(65) # os.EX_DATAERR
+
+ if normalized_names_provide_legacy:
+ if spec not in py_deps[name]:
+ py_deps[name].append(spec)
+ if args.majorver_provides or \
+ (args.majorver_provides_versions and dist.py_version in args.majorver_provides_versions):
+ py_deps[pymajor_name].append(spec)
+ if normalized_names_provide_pep503:
+ if spec not in py_deps[name_]:
+ py_deps[name_].append(spec)
+ if args.majorver_provides or \
+ (args.majorver_provides_versions and dist.py_version in args.majorver_provides_versions):
+ py_deps[pymajor_name_].append(spec)
+ if args.legacy or args.legacy_provides:
+ if spec not in py_deps[legacy_name]:
+ py_deps[legacy_name].append(spec)
+ if args.requires or (args.recommends and dist.extras):
+ name = namespace.format('python(abi)')
+ # If egg/dist metadata says package name is python, we don't add dependency on python(abi)
+ if dist.normalized_name == 'python':
+ py_abi = False
+ if name in py_deps:
+ py_deps.pop(name)
+ elif py_abi and dist.py_version:
+ if name not in py_deps:
+ py_deps[name] = []
+ spec = ('==', dist.py_version)
+ if spec not in py_deps[name]:
+ py_deps[name].append(spec)
+
+ if extras_subpackage:
+ deps = [d for d in dist.requirements_for_extra(extras_subpackage)]
+ else:
+ deps = dist.requirements
+
+ # console_scripts/gui_scripts entry points needed pkg_resources from setuptools
+ # on new Python/setuptools versions, this is no longer required
+ if nodep_setuptools_pyversion is None or parse(dist.py_version) < nodep_setuptools_pyversion:
+ if (dist.entry_points and
+ (lower.endswith('.egg') or
+ lower.endswith('.egg-info'))):
+ groups = {ep.group for ep in dist.entry_points}
+ if {"console_scripts", "gui_scripts"} & groups:
+ # stick them first so any more specific requirement
+ # overrides it
+ deps.insert(0, Requirement('setuptools'))
+ # add requires/recommends based on egg/dist metadata
+ for dep in deps:
+ # Even if we're requiring `foo[bar]`, also require `foo`
+ # to be safe, and to make it discoverable through
+ # `repoquery --whatrequires`
+ extras_suffixes = [""]
+ if args.require_extras_subpackages and dep.extras:
+ # A dependency can have more than one extras,
+ # i.e. foo[bar,baz], so let's go through all of them
+ extras_suffixes += [f"[{e.lower()}]" for e in dep.extras]
+
+ for extras_suffix in extras_suffixes:
+ if normalized_names_require_pep503:
+ dep_normalized_name = dep.normalized_name
+ else:
+ dep_normalized_name = dep.legacy_normalized_name
+
+ if args.legacy:
+ name = namespace.format('pythonegg({})({})').format(pyver_major, dep.legacy_normalized_name)
+ else:
+ if args.majorver_only:
+ name = namespace.format('python{}dist({}{})').format(pyver_major, dep_normalized_name, extras_suffix)
+ else:
+ name = namespace.format('python{}dist({}{})').format(dist.py_version, dep_normalized_name, extras_suffix)
+
+ if dep.marker and not args.recommends and not extras_subpackage:
+ if not dep.marker.evaluate(get_marker_env(dist, '')):
+ continue
+
+ if name not in py_deps:
+ py_deps[name] = []
+ for spec in dep.specifier:
+ if (spec.operator, spec.version) not in py_deps[name]:
+ py_deps[name].append((spec.operator, spec.version))
+
+ # Unused, for automatic sub-package generation based on 'extras' from egg/dist metadata
+ # TODO: implement in rpm later, or...?
+ if args.extras:
+ print(dist.extras)
+ for extra in dist.extras:
+ print('%%package\textras-{}'.format(extra))
+ print('Summary:\t{} extra for {} python package'.format(extra, dist.legacy_normalized_name))
+ print('Group:\t\tDevelopment/Python')
+ for dep in dist.requirements_for_extra(extra):
+ for spec in dep.specifier:
+ if spec.operator == '!=':
+ print('Conflicts:\t{} {} {}'.format(dep.legacy_normalized_name, '==', spec.version))
+ else:
+ print('Requires:\t{} {} {}'.format(dep.legacy_normalized_name, spec.operator, spec.version))
+ print('%%description\t{}'.format(extra))
+ print('{} extra for {} python package'.format(extra, dist.legacy_normalized_name))
+ print('%%files\t\textras-{}\n'.format(extra))
+ 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...
+ for dep in dist.requirements:
+ for spec in dep.specifier:
+ if spec.operator == '!=':
+ if dep.legacy_normalized_name not in py_deps:
+ py_deps[dep.legacy_normalized_name] = []
+ spec = ('==', spec.version)
+ if spec not in py_deps[dep.legacy_normalized_name]:
+ py_deps[dep.legacy_normalized_name].append(spec)
+
+ for name in sorted(py_deps):
+ if py_deps[name]:
+ # Print out versioned provides, requires, recommends, conflicts
+ spec_list = []
+ for spec in py_deps[name]:
+ spec_list.append(convert(name, spec[0], spec[1]))
+ if len(spec_list) == 1:
+ print(spec_list[0])
+ else:
+ # Sort spec_list so that the results can be tested easily
+ print('({})'.format(' with '.join(sorted(spec_list))))
+ else:
+ # Print out unversioned provides, requires, recommends, conflicts
+ print(name)
+
+
+if __name__ == "__main__":
+ """To allow this script to be importable (and its classes/functions
+ reused), actions are performed only when run as a main script."""
+ try:
+ main()
+ except Exception as exc:
+ print("*** PYTHONDISTDEPS_GENERATORS_FAILED ***", flush=True)
+ raise RuntimeError("Error: pythondistdeps.py generator encountered an unhandled exception and was terminated.") from exc
+