Better satisfies: e.g., v4.7.3 now satisfies v4.7

- Changed how satisfies() is defined for the various version classes
- Can't just use overlaps() with version lists -- need to account for
  more and less specific versions.

If the version is more specific than the constriant (e.g., 4.7.3 is more
specific than 4.7), then it should satisfy the constraint, because if
a user asks for 4.7 they likely do not care about the minor version.  If they
do, they can specify it.  New Version.satisfies() takes this into account.
This commit is contained in:
Todd Gamblin 2014-05-17 15:17:40 -07:00
parent 285c5444ab
commit ed6454fe78
5 changed files with 118 additions and 13 deletions

View file

@ -117,7 +117,7 @@ def __call__(self, package_self, *args, **kwargs):
or if there is none, then raise a NoSuchMethodError. or if there is none, then raise a NoSuchMethodError.
""" """
for spec, method in self.method_list: for spec, method in self.method_list:
if spec.satisfies(package_self.spec): if package_self.spec.satisfies(spec):
return method(package_self, *args, **kwargs) return method(package_self, *args, **kwargs)
if self.default: if self.default:

View file

@ -77,6 +77,7 @@ def get(self, spec):
@_autospec @_autospec
def get_installed(self, spec): def get_installed(self, spec):
"""Get all the installed specs that satisfy the provided spec constraint."""
return [s for s in self.installed_package_specs() if s.satisfies(spec)] return [s for s in self.installed_package_specs() if s.satisfies(spec)]

View file

@ -214,17 +214,17 @@ def _autospec(self, compiler_spec_like):
def satisfies(self, other): def satisfies(self, other):
# TODO: This should not just look for overlapping versions.
# TODO: e.g., 4.7.3 should satisfy a requirement for 4.7.
other = self._autospec(other) other = self._autospec(other)
return (self.name == other.name and return (self.name == other.name and
self.versions.overlaps(other.versions)) self.versions.satisfies(other.versions))
def constrain(self, other): def constrain(self, other):
other = self._autospec(other) other = self._autospec(other)
if not self.satisfies(other):
raise UnsatisfiableCompilerSpecError(self, other) # ensure that other will actually constrain this spec.
if not other.satisfies(self):
raise UnsatisfiableCompilerSpecError(other, self)
self.versions.intersect(other.versions) self.versions.intersect(other.versions)
@ -866,8 +866,8 @@ def _constrain_dependencies(self, other):
# TODO: might want more detail than this, e.g. specific deps # TODO: might want more detail than this, e.g. specific deps
# in violation. if this becomes a priority get rid of this # in violation. if this becomes a priority get rid of this
# check and be more specici about what's wrong. # check and be more specici about what's wrong.
if not self.satisfies_dependencies(other): if not other.satisfies_dependencies(self):
raise UnsatisfiableDependencySpecError(self, other) raise UnsatisfiableDependencySpecError(other, self)
# Handle common first-order constraints directly # Handle common first-order constraints directly
for name in self.common_dependencies(other): for name in self.common_dependencies(other):

View file

@ -83,6 +83,14 @@ def assert_no_overlap(self, v1, v2):
self.assertFalse(ver(v1).overlaps(ver(v2))) self.assertFalse(ver(v1).overlaps(ver(v2)))
def assert_satisfies(self, v1, v2):
self.assertTrue(ver(v1).satisfies(ver(v2)))
def assert_does_not_satisfy(self, v1, v2):
self.assertFalse(ver(v1).satisfies(ver(v2)))
def check_intersection(self, expected, a, b): def check_intersection(self, expected, a, b):
self.assertEqual(ver(expected), ver(a).intersection(ver(b))) self.assertEqual(ver(expected), ver(a).intersection(ver(b)))
@ -301,3 +309,40 @@ def test_intersection(self):
self.check_intersection(['2.5:2.7'], self.check_intersection(['2.5:2.7'],
['1.1:2.7'], ['2.5:3.0','1.0']) ['1.1:2.7'], ['2.5:3.0','1.0'])
self.check_intersection(['0:1'], [':'], ['0:1']) self.check_intersection(['0:1'], [':'], ['0:1'])
def test_satisfaction(self):
self.assert_satisfies('4.7.3', '4.7.3')
self.assert_satisfies('4.7.3', '4.7')
self.assert_satisfies('4.7.3b2', '4.7')
self.assert_satisfies('4.7b6', '4.7')
self.assert_satisfies('4.7.3', '4')
self.assert_satisfies('4.7.3b2', '4')
self.assert_satisfies('4.7b6', '4')
self.assert_does_not_satisfy('4.8.0', '4.9')
self.assert_does_not_satisfy('4.8', '4.9')
self.assert_does_not_satisfy('4', '4.9')
self.assert_satisfies('4.7b6', '4.3:4.7')
self.assert_satisfies('4.3.0', '4.3:4.7')
self.assert_satisfies('4.3.2', '4.3:4.7')
self.assert_does_not_satisfy('4.8.0', '4.3:4.7')
self.assert_does_not_satisfy('4.3', '4.4:4.7')
self.assert_satisfies('4.7b6', '4.3:4.7')
self.assert_does_not_satisfy('4.8.0', '4.3:4.7')
self.assert_satisfies('4.7', '4.3, 4.6, 4.7')
self.assert_satisfies('4.7.3', '4.3, 4.6, 4.7')
self.assert_satisfies('4.6.5', '4.3, 4.6, 4.7')
self.assert_satisfies('4.6.5.2', '4.3, 4.6, 4.7')
self.assert_does_not_satisfy('4', '4.3, 4.6, 4.7')
self.assert_does_not_satisfy('4.8.0', '4.2, 4.3:4.7')
self.assert_satisfies('4.8.0', '4.2, 4.3:4.8')
self.assert_satisfies('4.8.2', '4.2, 4.3:4.8')

View file

@ -143,6 +143,18 @@ def highest(self):
return self return self
@coerced
def satisfies(self, other):
"""A Version 'satisfies' another if it is at least as specific and has a
common prefix. e.g., we want gcc@4.7.3 to satisfy a request for
gcc@4.7 so that when a user asks to build with gcc@4.7, we can find
a suitable compiler.
"""
nself = len(self.version)
nother = len(other.version)
return nother <= nself and self.version[:nother] == other.version
def wildcard(self): def wildcard(self):
"""Create a regex that will match variants of this version string.""" """Create a regex that will match variants of this version string."""
def a_or_n(seg): def a_or_n(seg):
@ -326,6 +338,37 @@ def __contains__(self, other):
none_high.le(other.end, self.end)) none_high.le(other.end, self.end))
@coerced
def satisfies(self, other):
"""A VersionRange satisfies another if some version in this range
would satisfy some version in the other range. To do this it must
either:
a) Overlap with the other range
b) The start of this range satisfies the end of the other range.
This is essentially the same as overlaps(), but overlaps assumes
that its arguments are specific. That is, 4.7 is interpreted as
4.7.0.0.0.0... . This funciton assumes that 4.7 woudl be satisfied
by 4.7.3.5, etc.
Rationale:
If a user asks for gcc@4.5:4.7, and a package is only compatible with
gcc@4.7.3:4.8, then that package should be able to build under the
constraints. Just using overlaps() would not work here.
Note that we don't need to check whether the end of this range
would satisfy the start of the other range, because overlaps()
already covers that case.
Note further that overlaps() is a symmetric operation, while
satisfies() is not.
"""
return (self.overlaps(other) or
# if either self.start or other.end are None, then this can't
# satisfy, or overlaps() would've taken care of it.
self.start and other.end and self.start.satisfies(other.end))
@coerced @coerced
def overlaps(self, other): def overlaps(self, other):
return (other in self or self in other or return (other in self or self in other or
@ -444,11 +487,6 @@ def highest(self):
return self[-1].highest() return self[-1].highest()
def satisfies(self, other):
"""Synonym for overlaps."""
return self.overlaps(other)
@coerced @coerced
def overlaps(self, other): def overlaps(self, other):
if not other or not self: if not other or not self:
@ -465,6 +503,27 @@ def overlaps(self, other):
return False return False
@coerced
def satisfies(self, other):
"""A VersionList satisfies another if some version in the list would
would satisfy some version in the other list. This uses essentially
the same algorithm as overlaps() does for VersionList, but it calls
satisfies() on member Versions and VersionRanges.
"""
if not other or not self:
return False
s = o = 0
while s < len(self) and o < len(other):
if self[s].satisfies(other[o]):
return True
elif self[s] < other[o]:
s += 1
else:
o += 1
return False
@coerced @coerced
def update(self, other): def update(self, other):
for v in other.versions: for v in other.versions: