Remove lower-constraint management

We stopped doing this nearly 3½ years ago [1]. We no longer use this
tooling in any of our jobs either. It is therefore time to delete all
this code.

[1] https://governance.openstack.org/tc/resolutions/20220414-drop-lower-constraints

Change-Id: I64ba0cec3eeac3f7ded1354bd768f7022d863dc4
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
This commit is contained in:
Stephen Finucane
2025-09-18 13:55:47 +01:00
parent c73d45fac9
commit 8a63c624ba
5 changed files with 2 additions and 393 deletions

View File

@@ -18,7 +18,6 @@ import collections
import re
from packaging import markers
from packaging import specifiers
from openstack_requirements import project
from openstack_requirements import requirement
@@ -303,130 +302,3 @@ def validate(
)
return failed
def _find_constraint(req, constraints):
"""Return the constraint matching the markers for req.
Given a requirement, find the constraint with matching markers.
If none match, find a constraint without any markers at all.
Otherwise return None.
"""
if req.markers:
req_markers = markers.Marker(req.markers)
for constraint_setting, _ in constraints:
if constraint_setting.markers == req.markers:
return constraint_setting
if not constraint_setting.markers:
# There is no point in performing the complex
# comparison for a constraint that has no markers, so
# we skip it here. If we find no closer match then the
# loop at the end of the function will look for a
# constraint without a marker and use that.
continue
# NOTE(dhellmann): This is a very naive attempt to check
# marker compatibility that relies on internal
# implementation details of the packaging library. The
# best way to ensure the constraint and requirements match
# is to use the same marker string in the corresponding
# lines.
c_markers = markers.Marker(constraint_setting.markers)
env = {
str(var): str(val)
for var, op, val in c_markers._markers # WARNING: internals
}
if req_markers.evaluate(env):
return constraint_setting
# Try looking for a constraint without any markers.
for constraint_setting, _ in constraints:
if not constraint_setting.markers:
return constraint_setting
return None
def validate_lower_constraints(req_list, constraints, denylist):
"""Return True if there is an error.
:param reqs: RequirementsList for the head of the branch
:param constraints: Parsed lower-constraints.txt or None
"""
if constraints is None:
return False
parsed_constraints = requirement.parse(constraints)
failed = False
for fname, freqs in req_list.reqs_by_file.items():
if fname == 'doc/requirements.txt':
# Skip things that are not needed for unit or functional
# tests.
continue
print("Validating lower constraints of {}".format(fname))
for name, reqs in freqs.items():
if name in denylist:
continue
if name not in parsed_constraints:
print('ERROR: Package {!r} is used in {} '
'but not in lower-constraints.txt'.format(
name, fname))
failed = True
continue
for req in reqs:
spec = specifiers.SpecifierSet(req.specifiers)
# FIXME(dhellmann): This will only find constraints
# where the markers match the requirements list
# exactly, so we can't do things like use different
# constrained versions for different versions of
# python 3 if the requirement range is expressed as
# python_version>3.0. We can support different
# versions if there is a different requirement
# specification for each version of python. I don't
# really know how smart we want this to be, because
# I'm not sure we want to support extremely
# complicated dependency sets.
constraint_setting = _find_constraint(
req,
parsed_constraints[name],
)
if not constraint_setting:
print('ERROR: Unable to find constraint for {} '
'matching {!r} or without any markers.'.format(
name, req.markers))
failed = True
continue
version = constraint_setting.specifiers.lstrip('=')
if not spec.contains(version):
print('ERROR: Package {!r} is constrained to {} '
'which is incompatible with the settings {} '
'from {}.'.format(
name, version, req, fname))
failed = True
min = [
s
for s in req.specifiers.split(',')
if '>' in s
]
if not min:
# No minimum specified. Ignore this and let some
# other validation trap the error.
continue
expected = min[0].lstrip('>=')
if version != expected:
print('ERROR: Package {!r} is constrained to {} '
'which does not match '
'the minimum version specifier {} in {}'.format(
name, version, expected, fname))
failed = True
return failed

View File

@@ -71,8 +71,4 @@ def read(root):
target_files.append('test-requirements-py%s.txt' % py_version)
for target_file in target_files:
_safe_read(result, target_file, output=requirements)
# Read lower-constraints.txt and ensure the key is always present
# in case the file is missing.
result['lower-constraints.txt'] = None
_safe_read(result, 'lower-constraints.txt')
return result

View File

@@ -541,256 +541,6 @@ class TestValidateOne(testtools.TestCase):
)
class TestValidateLowerConstraints(testtools.TestCase):
def setUp(self):
super(TestValidateLowerConstraints, self).setUp()
self._stdout_fixture = fixtures.StringStream('stdout')
self.stdout = self.useFixture(self._stdout_fixture).stream
self.useFixture(fixtures.MonkeyPatch('sys.stdout', self.stdout))
def test_no_constraints_file(self):
constraints_content = None
project_data = {
'requirements': {'requirements.txt': 'name>=1.2,!=1.4'},
'lower-constraints.txt': constraints_content,
}
head_reqs = check.RequirementsList('testproj', project_data)
head_reqs.process(False)
self.assertFalse(
check.validate_lower_constraints(
req_list=head_reqs,
constraints=project_data['lower-constraints.txt'],
denylist=requirement.parse(''),
)
)
def test_no_min(self):
constraints_content = textwrap.dedent("""
name==1.2
""")
project_data = {
'requirements': {'requirements.txt': 'name!=1.4'},
'lower-constraints.txt': constraints_content,
}
head_reqs = check.RequirementsList('testproj', project_data)
head_reqs.process(False)
self.assertFalse(
check.validate_lower_constraints(
req_list=head_reqs,
constraints=project_data['lower-constraints.txt'],
denylist=requirement.parse(''),
)
)
def test_matches(self):
constraints_content = textwrap.dedent("""
name==1.2
""")
project_data = {
'requirements': {'requirements.txt': 'name>=1.2,!=1.4'},
'lower-constraints.txt': constraints_content,
}
head_reqs = check.RequirementsList('testproj', project_data)
head_reqs.process(False)
self.assertFalse(
check.validate_lower_constraints(
req_list=head_reqs,
constraints=project_data['lower-constraints.txt'],
denylist=requirement.parse(''),
)
)
def test_not_constrained(self):
constraints_content = textwrap.dedent("""
""")
project_data = {
'requirements': {'requirements.txt': 'name>=1.2,!=1.4'},
'lower-constraints.txt': constraints_content,
}
head_reqs = check.RequirementsList('testproj', project_data)
head_reqs.process(False)
self.assertTrue(
check.validate_lower_constraints(
req_list=head_reqs,
constraints=project_data['lower-constraints.txt'],
denylist=requirement.parse(''),
)
)
def test_mismatch_denylisted(self):
constraints_content = textwrap.dedent("""
name==1.2
""")
project_data = {
'requirements': {'requirements.txt': 'name>=1.3,!=1.4'},
'lower-constraints.txt': constraints_content,
}
head_reqs = check.RequirementsList('testproj', project_data)
head_reqs.process(False)
self.assertFalse(
check.validate_lower_constraints(
req_list=head_reqs,
constraints=project_data['lower-constraints.txt'],
denylist=requirement.parse('name'),
)
)
def test_lower_bound_lower(self):
constraints_content = textwrap.dedent("""
name==1.2
""")
project_data = {
'requirements': {'requirements.txt': 'name>=1.1,!=1.4'},
'lower-constraints.txt': constraints_content,
}
head_reqs = check.RequirementsList('testproj', project_data)
head_reqs.process(False)
self.assertTrue(
check.validate_lower_constraints(
req_list=head_reqs,
constraints=project_data['lower-constraints.txt'],
denylist=requirement.parse(''),
)
)
def test_lower_bound_higher(self):
constraints_content = textwrap.dedent("""
name==1.2
""")
project_data = {
'requirements': {'requirements.txt': 'name>=1.3,!=1.4'},
'lower-constraints.txt': constraints_content,
}
head_reqs = check.RequirementsList('testproj', project_data)
head_reqs.process(False)
self.assertTrue(
check.validate_lower_constraints(
req_list=head_reqs,
constraints=project_data['lower-constraints.txt'],
denylist=requirement.parse(''),
)
)
def test_constrained_version_excluded(self):
constraints_content = textwrap.dedent("""
name==1.2
""")
project_data = {
'requirements': {'requirements.txt': 'name>=1.1,!=1.2'},
'lower-constraints.txt': constraints_content,
}
head_reqs = check.RequirementsList('testproj', project_data)
head_reqs.process(False)
self.assertTrue(
check.validate_lower_constraints(
req_list=head_reqs,
constraints=project_data['lower-constraints.txt'],
denylist=requirement.parse(''),
)
)
def test_constraints_with_markers(self):
constraints_content = textwrap.dedent("""
name==1.1;python_version=='2.7'
name==2.0;python_version=='3.5'
name==2.0;python_version=='3.6'
""")
project_data = {
'requirements': {
'requirements.txt': textwrap.dedent("""
name>=1.1,!=1.2;python_version=='2.7'
name>=2.0;python_version=='3.5'
name>=2.0;python_version=='3.6'
"""),
},
'lower-constraints.txt': constraints_content,
}
head_reqs = check.RequirementsList('testproj', project_data)
head_reqs.process(False)
self.assertFalse(
check.validate_lower_constraints(
req_list=head_reqs,
constraints=project_data['lower-constraints.txt'],
denylist=requirement.parse(''),
)
)
def test_constraints_with_markers_missing_one_req(self):
constraints_content = textwrap.dedent("""
name==1.1;python_version=='2.7'
name==2.0;python_version=='3.5'
name==2.0;python_version=='3.6'
""")
project_data = {
'requirements': {
'requirements.txt': textwrap.dedent("""
name>=1.1,!=1.2;python_version=='2.7'
name>=2.0;python_version=='3.5'
"""),
},
'lower-constraints.txt': constraints_content,
}
head_reqs = check.RequirementsList('testproj', project_data)
head_reqs.process(False)
self.assertFalse(
check.validate_lower_constraints(
req_list=head_reqs,
constraints=project_data['lower-constraints.txt'],
denylist=requirement.parse(''),
)
)
def test_constraints_with_markers_missing_one_marker(self):
constraints_content = textwrap.dedent("""
name==1.1;python_version=='2.7'
name==2.0;python_version=='3.5'
""")
project_data = {
'requirements': {
'requirements.txt': textwrap.dedent("""
name>=1.1,!=1.2;python_version=='2.7'
name>=2.0;python_version=='3.5'
name>=2.0;python_version=='3.6'
"""),
},
'lower-constraints.txt': constraints_content,
}
head_reqs = check.RequirementsList('testproj', project_data)
head_reqs.process(False)
self.assertTrue(
check.validate_lower_constraints(
req_list=head_reqs,
constraints=project_data['lower-constraints.txt'],
denylist=requirement.parse(''),
)
)
def test_complex_marker_evaluation(self):
constraints_content = textwrap.dedent("""
name===0.8.0;python_version=='2.7'
name===1.0.0;python_version>='3.0'
""")
project_data = {
'requirements': {
'requirements.txt': textwrap.dedent("""
name>=0.8.0;python_version<'3.0' # BSD
name>=1.0.0;python_version>='3.0' # BSD
"""),
},
'lower-constraints.txt': constraints_content,
}
head_reqs = check.RequirementsList('testproj', project_data)
head_reqs.process(False)
self.assertFalse(
check.validate_lower_constraints(
req_list=head_reqs,
constraints=project_data['lower-constraints.txt'],
denylist=requirement.parse(''),
)
)
class TestBackportPythonMarkers(testtools.TestCase):
def setUp(self):

View File

@@ -42,8 +42,8 @@ class TestReadProject(testtools.TestCase):
root = self.useFixture(fixtures.TempDir()).path
proj = project.read(root)
self.expectThat(
proj, matchers.Equals({'root': root, 'requirements': {},
'lower-constraints.txt': None}))
proj, matchers.Equals({'root': root, 'requirements': {}})
)
class TestProjectExtras(testtools.TestCase):

View File

@@ -145,15 +145,6 @@ def main():
allow_3_only=python_3_branch,
)
failed = (
check.validate_lower_constraints(
head_reqs,
head_proj['lower-constraints.txt'],
denylist,
)
or failed
)
# report the results
if failed or head_reqs.failed:
print("*** Incompatible requirement found!")