From ed6454fe78c8de2efb08d3c85e41fddd6fe704fb Mon Sep 17 00:00:00 2001 From: Todd Gamblin Date: Sat, 17 May 2014 15:17:40 -0700 Subject: 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. --- lib/spack/spack/multimethod.py | 2 +- lib/spack/spack/packages.py | 1 + lib/spack/spack/spec.py | 14 ++++---- lib/spack/spack/test/versions.py | 45 ++++++++++++++++++++++++++ lib/spack/spack/version.py | 69 +++++++++++++++++++++++++++++++++++++--- 5 files changed, 118 insertions(+), 13 deletions(-) (limited to 'lib') diff --git a/lib/spack/spack/multimethod.py b/lib/spack/spack/multimethod.py index 8d91e4f86d..974401e1aa 100644 --- a/lib/spack/spack/multimethod.py +++ b/lib/spack/spack/multimethod.py @@ -117,7 +117,7 @@ class SpecMultiMethod(object): or if there is none, then raise a NoSuchMethodError. """ 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) if self.default: diff --git a/lib/spack/spack/packages.py b/lib/spack/spack/packages.py index 36f3d4286a..5a31f1fbb9 100644 --- a/lib/spack/spack/packages.py +++ b/lib/spack/spack/packages.py @@ -77,6 +77,7 @@ class PackageDB(object): @_autospec 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)] diff --git a/lib/spack/spack/spec.py b/lib/spack/spack/spec.py index f0244695bc..35a17621b6 100644 --- a/lib/spack/spack/spec.py +++ b/lib/spack/spack/spec.py @@ -214,17 +214,17 @@ class CompilerSpec(object): 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) return (self.name == other.name and - self.versions.overlaps(other.versions)) + self.versions.satisfies(other.versions)) def constrain(self, 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) @@ -866,8 +866,8 @@ class Spec(object): # TODO: might want more detail than this, e.g. specific deps # in violation. if this becomes a priority get rid of this # check and be more specici about what's wrong. - if not self.satisfies_dependencies(other): - raise UnsatisfiableDependencySpecError(self, other) + if not other.satisfies_dependencies(self): + raise UnsatisfiableDependencySpecError(other, self) # Handle common first-order constraints directly for name in self.common_dependencies(other): diff --git a/lib/spack/spack/test/versions.py b/lib/spack/spack/test/versions.py index 37fd28a8e7..e272274a4f 100644 --- a/lib/spack/spack/test/versions.py +++ b/lib/spack/spack/test/versions.py @@ -83,6 +83,14 @@ class VersionsTest(unittest.TestCase): 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): self.assertEqual(ver(expected), ver(a).intersection(ver(b))) @@ -301,3 +309,40 @@ class VersionsTest(unittest.TestCase): self.check_intersection(['2.5:2.7'], ['1.1:2.7'], ['2.5:3.0','1.0']) 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') diff --git a/lib/spack/spack/version.py b/lib/spack/spack/version.py index 0b5125fdf0..ce94303a9c 100644 --- a/lib/spack/spack/version.py +++ b/lib/spack/spack/version.py @@ -143,6 +143,18 @@ class Version(object): 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): """Create a regex that will match variants of this version string.""" def a_or_n(seg): @@ -326,6 +338,37 @@ class VersionRange(object): 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 def overlaps(self, other): return (other in self or self in other or @@ -444,11 +487,6 @@ class VersionList(object): return self[-1].highest() - def satisfies(self, other): - """Synonym for overlaps.""" - return self.overlaps(other) - - @coerced def overlaps(self, other): if not other or not self: @@ -465,6 +503,27 @@ class VersionList(object): 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 def update(self, other): for v in other.versions: -- cgit v1.2.3-60-g2f50