generate-constraints: Make use of ranges

Currently, if we run with generate-constraints with a large number of
Python versions, we will get a large number of Python version specific
markers for dependencies: one for each passed Python version. For
example:

  $ generate-constraints \
      -b blacklist.txt \
      -p /usr/bin/python3.8 \
      -p /usr/bin/python3.9 \
      -p /usr/bin/python3.10 \
      -r global-requirements.txt

Will yield versions like:

  networkx===3.1;python_version=='3.8'
  networkx===3.2.1;python_version=='3.10'
  networkx===3.2.1;python_version=='3.9'

What has happened here is that the given dependency (networkx in this
case) has dropped support for an older Python version (Python 3.8).
However, the way that we've specified this limits our constraints to the
versions of Python we ran (-p <python>) or the versions we mapped/mocked
(--version map <real:mapped[:mapped...]>). This isn't ideal. Instead, it
would be better to think in terms of upper and lower limits. That is, if
we generated a map like so:

  networkx===3.1;python_version<='3.8'
  networkx===3.2.1;python_version>='3.9'

This has the benefit of being simpler and potentially allowing us to
generate constraints for more Python versions than we currently check
for.

Change-Id: Ibfc6a79624e5591baf945a578f9d265071e57f73
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
This commit is contained in:
Stephen Finucane
2023-12-21 12:05:53 +00:00
committed by Tim Burke
parent 4a006431f6
commit 9da6fcd609
2 changed files with 23 additions and 11 deletions

View File

@@ -127,6 +127,7 @@ def _combine_freezes(freezes, denylist=None):
for py_version, freeze in freezes:
if py_version in reference_versions:
raise Exception("Duplicate python %s" % py_version)
reference_versions.append(py_version)
for package, version in freeze:
packages.setdefault(
@@ -135,18 +136,29 @@ def _combine_freezes(freezes, denylist=None):
for package, versions in sorted(packages.items()):
if package.lower() in excludes:
continue
if (len(versions) != 1 or
list(versions.values())[0] != reference_versions):
# markers
for version, py_versions in sorted(versions.items()):
# Once the ecosystem matures, we can consider using OR.
if len(versions) > 1:
# markers for packages with multiple versions - we use python
# version ranges for these
for idx, (version, py_versions) in enumerate(sorted(versions.items())): # noqa: E501
if idx == 0: # lower-bound
marker = f"python_version<='{py_versions[-1]}'"
elif idx + 1 != len(versions): # intermediate version(s)
marker = f"python_version>='{py_versions[0]}',<={py_versions[-1]}" # noqa: E501
else: # upper-bound
marker = f"python_version>='{py_versions[0]}'"
yield f'{package}==={version};{marker}\n'
elif list(versions.values())[0] != reference_versions:
# markers for packages with a single version - these are usually
# version specific so we use strict python versions for these
for idx, (version, py_versions) in enumerate(sorted(versions.items())): # noqa: E501
for py_version in sorted(py_versions):
yield (
"%s===%s;python_version=='%s'\n" %
(package, version, py_version))
marker = f"python_version=='{py_version}'"
yield f'{package}==={version};{marker}\n'
else:
# no markers
yield '%s===%s\n' % (package, list(versions.keys())[0])
yield f'{package}==={list(versions)[0]}\n'
def _clone_versions(freezes, options):

View File

@@ -89,8 +89,8 @@ class TestCombine(testtools.TestCase):
freeze_27 = ('2.7', [('fixtures', '1.2.0')])
freeze_34 = ('3.4', [('fixtures', '1.5.0')])
self.assertEqual(
["fixtures===1.2.0;python_version=='2.7'\n",
"fixtures===1.5.0;python_version=='3.4'\n"],
["fixtures===1.2.0;python_version<='2.7'\n",
"fixtures===1.5.0;python_version>='3.4'\n"],
list(generate._combine_freezes([freeze_27, freeze_34])))
def test_duplicate_pythons(self):