allow project requirements to diverge at the lower bounds

Replace the exact match check for requirements with one that ensures
that the exclusion set specified is a subset of any exclusions used in
the global requirements list. This allows the lower bounds in a
project to diverge from the value used in other projects (and the
global list) while still ensuring that the allowed set of values is
the same.

We treat a cap (<version) as an exclusion because we want to allow
caps but we don't want them to be different from what we have in
global-requirements.

Change-Id: Ic2355cf6ca15380025d76dacffcf917c3929d40c
Signed-off-by: Doug Hellmann <doug@doughellmann.com>
This commit is contained in:
Doug Hellmann 2018-03-27 15:17:02 -04:00
parent ae899937df
commit f1eca36019
2 changed files with 102 additions and 13 deletions

View File

@ -72,17 +72,37 @@ class RequirementsList(object):
self.reqs_by_file[name] = self.extract_reqs(content, strict)
def _get_exclusions(req):
return set(
spec
for spec in req.specifiers.split(',')
if '!=' in spec or '<' in spec
)
def _is_requirement_in_global_reqs(req, global_reqs):
# Compare all fields except the extras field as the global
# requirements should not have any lines with the extras syntax
# example: oslo.db[xyz]<1.2.3
req_exclusions = _get_exclusions(req)
for req2 in global_reqs:
if (req.package == req2.package and
req.location == req2.location and
req.specifiers == req2.specifiers and
req.markers == req2.markers and
req.comment == req2.comment):
return True
# This matches the right package and other properties, so
# ensure that any exclusions are a subset of the global
# set.
global_exclusions = _get_exclusions(req2)
if req_exclusions.issubset(global_exclusions):
return True
else:
print(
"Requirement for package {} "
"has an exclusion not found in the "
"global list: {} vs. {}".format(
req.package, req_exclusions, global_exclusions)
)
return False
return False
@ -103,7 +123,7 @@ def get_global_reqs(content):
def _validate_one(name, reqs, branch_reqs, blacklist, global_reqs):
"Returns True if there is a failure."
print(name, reqs, branch_reqs, blacklist, global_reqs)
print('_validate_one', name, reqs, branch_reqs, blacklist, global_reqs)
if (name in branch_reqs.reqs and
reqs == branch_reqs.reqs[name]):
# Unchanged [or a change that preserves a current value]
@ -117,8 +137,6 @@ def _validate_one(name, reqs, branch_reqs, blacklist, global_reqs):
print("Requirement %s not in openstack/requirements" %
str(reqs))
return True
if reqs == global_reqs[name]:
return False
counts = {}
for req in reqs:
if req.extras:
@ -141,6 +159,7 @@ def _validate_one(name, reqs, branch_reqs, blacklist, global_reqs):
('[%s]' % extra) if extra else '',
len(global_reqs[name])))
return True
return False
def validate(head_reqs, branch_reqs, blacklist, global_reqs):

View File

@ -66,7 +66,7 @@ class TestIsReqInGlobalReqs(testtools.TestCase):
def test_min_mismatch(self):
req = requirement.parse('name>=1.3,!=1.4')['name'][0][0]
self.assertFalse(
self.assertTrue(
check._is_requirement_in_global_reqs(
req,
self.global_reqs['name'],
@ -84,7 +84,7 @@ class TestIsReqInGlobalReqs(testtools.TestCase):
def test_missing_exclusion(self):
req = requirement.parse('name>=1.2')['name'][0][0]
self.assertFalse(
self.assertTrue(
check._is_requirement_in_global_reqs(
req,
self.global_reqs['name'],
@ -92,6 +92,30 @@ class TestIsReqInGlobalReqs(testtools.TestCase):
)
class TestGetExclusions(testtools.TestCase):
def test_none(self):
req = list(check.get_global_reqs('name>=1.2')['name'])[0]
self.assertEqual(
set(),
check._get_exclusions(req),
)
def test_one(self):
req = list(check.get_global_reqs('name>=1.2,!=1.4')['name'])[0]
self.assertEqual(
set(['!=1.4']),
check._get_exclusions(req),
)
def test_cap(self):
req = list(check.get_global_reqs('name>=1.2,!=1.4,<2.0')['name'])[0]
self.assertEqual(
set(['!=1.4', '<2.0']),
check._get_exclusions(req),
)
class TestValidateOne(testtools.TestCase):
def setUp(self):
@ -212,9 +236,32 @@ class TestValidateOne(testtools.TestCase):
)
)
def test_new_item_mismatches_global_list(self):
# If the new item does not match the global value, that is an
# error.
def test_new_item_lower_min(self):
# If the new item has a lower minimum value than the global
# list, that is OK.
reqs = [
r
for r, line in requirement.parse('name>=1.1,!=1.4')['name']
]
branch_reqs = check.RequirementsList(
'testproj',
{'requirements': {'requirements.txt': ''}},
)
branch_reqs.process(False)
global_reqs = check.get_global_reqs('name>=1.2,!=1.4')
self.assertFalse(
check._validate_one(
'name',
reqs=reqs,
branch_reqs=branch_reqs,
blacklist=requirement.parse(''),
global_reqs=global_reqs,
)
)
def test_new_item_extra_exclusion(self):
# If the new item includes an exclusion that is not present in
# the global list that is not OK.
reqs = [
r
for r, line in requirement.parse('name>=1.2,!=1.4,!=1.5')['name']
@ -235,6 +282,29 @@ class TestValidateOne(testtools.TestCase):
)
)
def test_new_item_missing_exclusion(self):
# If the new item does not include an exclusion that is
# present in the global list that is OK.
reqs = [
r
for r, line in requirement.parse('name>=1.2')['name']
]
branch_reqs = check.RequirementsList(
'testproj',
{'requirements': {'requirements.txt': ''}},
)
branch_reqs.process(False)
global_reqs = check.get_global_reqs('name>=1.2,!=1.4')
self.assertFalse(
check._validate_one(
'name',
reqs=reqs,
branch_reqs=branch_reqs,
blacklist=requirement.parse(''),
global_reqs=global_reqs,
)
)
def test_new_item_matches_global_list_with_extra(self):
# If the global list has multiple entries for an item with
# different "extra" specifiers, the values must all be in the