@ -73,6 +73,14 @@ class Requirement(Requirement_):
class Distribution ( PathDistribution ) :
class Distribution ( PathDistribution ) :
def __init__ ( self , path ) :
def __init__ ( self , path ) :
super ( Distribution , self ) . __init__ ( Path ( 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 . normalized_name = normalize_name ( self . name )
self . legacy_normalized_name = legacy_normalize_name ( self . name )
self . legacy_normalized_name = legacy_normalize_name ( self . name )
self . requirements = [ Requirement ( r ) for r in self . requires or [ ] ]
self . requirements = [ Requirement ( r ) for r in self . requires or [ ] ]
@ -86,8 +94,8 @@ class Distribution(PathDistribution):
# that it works also on previous Python/importlib_metadata versions.
# that it works also on previous Python/importlib_metadata versions.
@property
@property
def name ( self ) :
def name ( self ) :
""" Return the ' Name ' metadata for the distribution package ."""
""" Return the ' Name ' metadata for the distribution package or None ."""
return self . metadata [ ' Name ' ]
return self . metadata . get ( ' Name ' )
def _parse_py_version ( self , path ) :
def _parse_py_version ( self , path ) :
# Try to parse the Python version from the path the metadata
# Try to parse the Python version from the path the metadata
@ -104,10 +112,17 @@ class Distribution(PathDistribution):
def requirements_for_extra ( self , extra ) :
def requirements_for_extra ( self , extra ) :
extra_deps = [ ]
extra_deps = [ ]
# we are only interested in dependencies with extra == 'our_extra' marker
for req in self . requirements :
for req in self . requirements :
# no marker at all, nothing to evaluate
if not req . marker :
if not req . marker :
continue
continue
if req . marker . evaluate ( get_marker_env ( self , extra ) ) :
# 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 )
extra_deps . append ( req )
return extra_deps
return extra_deps
@ -126,6 +141,12 @@ class RpmVersion():
self . pre = version . _version . pre
self . pre = version . _version . pre
self . dev = version . _version . dev
self . dev = version . _version . dev
self . post = version . _version . post
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 ) :
def increment ( self ) :
self . version [ - 1 ] + = 1
self . version [ - 1 ] + = 1
@ -134,8 +155,11 @@ class RpmVersion():
self . post = None
self . post = None
return self
return self
def is_zero ( self ) :
return self . __str__ ( ) == ' 0 '
def __str__ ( self ) :
def __str__ ( self ) :
if isinstance ( self . version , str ) :
if self . is_legacy ( ) :
return self . version
return self . version
if self . epoch :
if self . epoch :
rpm_epoch = str ( self . epoch ) + ' : '
rpm_epoch = str ( self . epoch ) + ' : '
@ -161,6 +185,11 @@ def convert_compatible(name, operator, version_id):
print ( ' Invalid requirement: {} {} {} ' . format ( name , operator , version_id ) , file = stderr )
print ( ' Invalid requirement: {} {} {} ' . format ( name , operator , version_id ) , file = stderr )
exit ( 65 ) # os.EX_DATAERR
exit ( 65 ) # os.EX_DATAERR
version = RpmVersion ( version_id )
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 :
if len ( version . version ) == 1 :
print ( " *** INVALID_REQUIREMENT_ERROR___SEE_STDERR *** " )
print ( " *** INVALID_REQUIREMENT_ERROR___SEE_STDERR *** " )
print ( ' Invalid requirement: {} {} {} ' . format ( name , operator , version_id ) , file = stderr )
print ( ' Invalid requirement: {} {} {} ' . format ( name , operator , version_id ) , file = stderr )
@ -193,18 +222,32 @@ def convert_not_equal(name, operator, version_id):
if version_id . endswith ( ' .* ' ) :
if version_id . endswith ( ' .* ' ) :
version_id = version_id [ : - 2 ]
version_id = version_id [ : - 2 ]
version = RpmVersion ( version_id )
version = RpmVersion ( version_id )
lower_version = RpmVersion ( version_id ) . increment ( )
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 :
else :
version = RpmVersion ( version_id )
version = RpmVersion ( version_id )
lower_version = version
version_gt = version
return ' ( {} < {} or {} > {} ) ' . format (
version_gt_operator = ' > '
name , version , name , lower_version )
return ' ( {} < {} or {} {} {} ) ' . format (
name , version , name , version_gt_operator , version_gt )
def convert_ordered ( name , operator , version_id ) :
def convert_ordered ( name , operator , version_id ) :
if version_id . endswith ( ' .* ' ) :
if version_id . endswith ( ' .* ' ) :
# PEP 440 does not define semantics for prefix matching
# PEP 440 does not define semantics for prefix matching
# with ordered comparisons
# 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_id = version_id [ : - 2 ]
version = RpmVersion ( version_id )
version = RpmVersion ( version_id )
if operator == ' > ' :
if operator == ' > ' :
@ -215,6 +258,14 @@ def convert_ordered(name, operator, version_id):
operator = ' < '
operator = ' < '
else :
else :
version = RpmVersion ( version_id )
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 )
return ' {} {} {} ' . format ( name , operator , version )
@ -252,10 +303,10 @@ def get_marker_env(dist, extra):
" extra " : extra }
" extra " : extra }
if __name__ == " __main__ " :
def main ( ) :
""" To allow this script to be importable (and its classes/functions
""" To allow this script to be importable (and its classes/functions
reused ) , actions are performed only when run as a main script . """
reused ) , actions are defined in the main function and are performed only
when run as a main script . """
parser = argparse . ArgumentParser ( prog = argv [ 0 ] )
parser = argparse . ArgumentParser ( prog = argv [ 0 ] )
group = parser . add_mutually_exclusive_group ( required = True )
group = parser . add_mutually_exclusive_group ( required = True )
group . add_argument ( ' -P ' , ' --provides ' , action = ' store_true ' , help = ' Print Provides ' )
group . add_argument ( ' -P ' , ' --provides ' , action = ' store_true ' , help = ' Print Provides ' )
@ -285,9 +336,14 @@ if __name__ == "__main__":
parser . add_argument ( ' --require-extras-subpackages ' , action = ' store_true ' ,
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 " )
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 ( ' --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 " )
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 ( )
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_abi = args . requires
py_deps = { }
py_deps = { }
@ -333,6 +389,8 @@ if __name__ == "__main__":
package_name_parts = args . package_name . rpartition ( ' + ' )
package_name_parts = args . package_name . rpartition ( ' + ' )
extras_subpackage = package_name_parts [ 2 ] . lower ( ) or None
extras_subpackage = package_name_parts [ 2 ] . lower ( ) or None
namespace = ( args . namespace + " ( {} ) " ) if args . namespace else " {} "
for f in ( args . files or stdin . readlines ( ) ) :
for f in ( args . files or stdin . readlines ( ) ) :
f = f . strip ( )
f = f . strip ( )
lower = f . lower ( )
lower = f . lower ( )
@ -389,36 +447,47 @@ if __name__ == "__main__":
extras_suffix = f " [ { extras_subpackage } ] " if extras_subpackage else " "
extras_suffix = f " [ { extras_subpackage } ] " if extras_subpackage else " "
# If egg/dist metadata says package name is python, we provide python(abi)
# If egg/dist metadata says package name is python, we provide python(abi)
if dist . normalized_name == ' python ' :
if dist . normalized_name == ' python ' :
name = ' python(abi) '
name = namespace . format ( ' python(abi) ' )
if name not in py_deps :
if name not in py_deps :
py_deps [ name ] = [ ]
py_deps [ name ] = [ ]
py_deps [ name ] . append ( ( ' == ' , dist . py_version ) )
py_deps [ name ] . append ( ( ' == ' , dist . py_version ) )
if not args . legacy or not args . majorver_only :
if not args . legacy or not args . majorver_only :
if normalized_names_provide_legacy :
if normalized_names_provide_legacy :
name = ' python {} dist( {} {} ) ' . format ( dist . py_version , dist . legacy_normalized_name , extras_suffix )
name = namespace . format ( ' python {} dist( {} {} ) ' ) . format ( dist . py_version , dist . legacy_normalized_name , extras_suffix )
if name not in py_deps :
if name not in py_deps :
py_deps [ name ] = [ ]
py_deps [ name ] = [ ]
if normalized_names_provide_pep503 :
if normalized_names_provide_pep503 :
name_ = ' python {} dist( {} {} ) ' . format ( dist . py_version , dist . normalized_name , extras_suffix )
name_ = namespace . format ( ' python {} dist( {} {} ) ' ) . format ( dist . py_version , dist . normalized_name , extras_suffix )
if name_ not in py_deps :
if name_ not in py_deps :
py_deps [ name_ ] = [ ]
py_deps [ name_ ] = [ ]
if args . majorver_provides or args . majorver_only or \
if args . majorver_provides or args . majorver_only or \
( args . majorver_provides_versions and dist . py_version in args . majorver_provides_versions ) :
( args . majorver_provides_versions and dist . py_version in args . majorver_provides_versions ) :
if normalized_names_provide_legacy :
if normalized_names_provide_legacy :
pymajor_name = ' python {} dist( {} {} ) ' . format ( pyver_major , dist . legacy_normalized_name , extras_suffix )
pymajor_name = namespace . format ( ' python {} dist( {} {} ) ' ) . format ( pyver_major , dist . legacy_normalized_name , extras_suffix )
if pymajor_name not in py_deps :
if pymajor_name not in py_deps :
py_deps [ pymajor_name ] = [ ]
py_deps [ pymajor_name ] = [ ]
if normalized_names_provide_pep503 :
if normalized_names_provide_pep503 :
pymajor_name_ = ' python {} dist( {} {} ) ' . format ( pyver_major , dist . normalized_name , extras_suffix )
pymajor_name_ = namespace . format ( ' python {} dist( {} {} ) ' ) . format ( pyver_major , dist . normalized_name , extras_suffix )
if pymajor_name_ not in py_deps :
if pymajor_name_ not in py_deps :
py_deps [ pymajor_name_ ] = [ ]
py_deps [ pymajor_name_ ] = [ ]
if args . legacy or args . legacy_provides :
if args . legacy or args . legacy_provides :
legacy_name = ' pythonegg( {} )( {} ) ' . format ( pyver_major , dist . legacy_normalized_name )
legacy_name = namespace . format ( ' pythonegg( {} )( {} ) ' ) . format ( pyver_major , dist . legacy_normalized_name )
if legacy_name not in py_deps :
if legacy_name not in py_deps :
py_deps [ legacy_name ] = [ ]
py_deps [ legacy_name ] = [ ]
if dist . version :
if dist . version :
version = dist . version
version = dist . version
spec = ( ' == ' , version )
spec = ( ' == ' , version )
if args . fail_if_zero :
if RpmVersion ( version ) . is_zero ( ) :
print ( ' *** PYTHON_PROVIDED_VERSION_NORMALIZES_TO_ZERO___SEE_STDERR *** ' )
print ( f ' \n Error: 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 %d efine 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 normalized_names_provide_legacy :
if spec not in py_deps [ name ] :
if spec not in py_deps [ name ] :
@ -436,7 +505,7 @@ if __name__ == "__main__":
if spec not in py_deps [ legacy_name ] :
if spec not in py_deps [ legacy_name ] :
py_deps [ legacy_name ] . append ( spec )
py_deps [ legacy_name ] . append ( spec )
if args . requires or ( args . recommends and dist . extras ) :
if args . requires or ( args . recommends and dist . extras ) :
name = ' python(abi) '
name = namespace . format ( ' python(abi) ' )
# If egg/dist metadata says package name is python, we don't add dependency on python(abi)
# If egg/dist metadata says package name is python, we don't add dependency on python(abi)
if dist . normalized_name == ' python ' :
if dist . normalized_name == ' python ' :
py_abi = False
py_abi = False
@ -483,12 +552,12 @@ if __name__ == "__main__":
dep_normalized_name = dep . legacy_normalized_name
dep_normalized_name = dep . legacy_normalized_name
if args . legacy :
if args . legacy :
name = ' pythonegg( {} )( {} ) ' . format ( pyver_major , dep . legacy_normalized_name )
name = namespace . format ( ' pythonegg( {} )( {} ) ' ) . format ( pyver_major , dep . legacy_normalized_name )
else :
else :
if args . majorver_only :
if args . majorver_only :
name = ' python {} dist( {} {} ) ' . format ( pyver_major , dep_normalized_name , extras_suffix )
name = namespace . format ( ' python {} dist( {} {} ) ' ) . format ( pyver_major , dep_normalized_name , extras_suffix )
else :
else :
name = ' python {} dist( {} {} ) ' . format ( dist . py_version , dep_normalized_name , extras_suffix )
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 dep . marker and not args . recommends and not extras_subpackage :
if not dep . marker . evaluate ( get_marker_env ( dist , ' ' ) ) :
if not dep . marker . evaluate ( get_marker_env ( dist , ' ' ) ) :
@ -544,3 +613,14 @@ if __name__ == "__main__":
else :
else :
# Print out unversioned provides, requires, recommends, conflicts
# Print out unversioned provides, requires, recommends, conflicts
print ( name )
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