specs: remove parse_anonymous_spec(); use Spec() instead

- `parse_anonymous_spec()` is a vestige of the days when Spack didn't
  support nameless specs.  We don't need it anymore because now we can
  write Spec() for a spec that will match anything, and satisfies()
  semantics work properly for anonymous specs.

- Delete `parse_anonymous_spec()` and replace its uses with simple calls
  to the Spec() constructor.

- make then handling of when='...' specs in directives more consistent.

- clean up Spec.__contains__()

- refactor directives and tests slightly to accommodate the change.
This commit is contained in:
Todd Gamblin 2019-06-29 15:07:07 -07:00 committed by Peter Scheibel
parent 12b9fad7b6
commit 515b4045e9
8 changed files with 224 additions and 144 deletions

View File

@ -54,6 +54,48 @@ class OpenMpi(Package):
_patch_order_index = 0 _patch_order_index = 0
def make_when_spec(value):
"""Create a ``Spec`` that indicates when a directive should be applied.
Directives with ``when`` specs, e.g.:
patch('foo.patch', when='@4.5.1:')
depends_on('mpi', when='+mpi')
depends_on('readline', when=sys.platform() != 'darwin')
are applied conditionally depending on the value of the ``when``
keyword argument. Specifically:
1. If the ``when`` argument is ``True``, the directive is always applied
2. If it is ``False``, the directive is never applied
3. If it is a ``Spec`` string, it is applied when the package's
concrete spec satisfies the ``when`` spec.
The first two conditions are useful for the third example case above.
It allows package authors to include directives that are conditional
at package definition time, in additional to ones that are evaluated
as part of concretization.
Arguments:
value (Spec or bool): a conditional Spec or a constant ``bool``
value indicating when a directive should be applied.
"""
# Unsatisfiable conditions are discarded by the caller, and never
# added to the package class
if value is False:
return False
# If there is no constraint, the directive should always apply;
# represent this by returning the unconstrained `Spec()`, which is
# always satisfied.
if value is None or value is True:
return spack.spec.Spec()
# This is conditional on the spec
return spack.spec.Spec(value)
class DirectiveMeta(type): class DirectiveMeta(type):
"""Flushes the directives that were temporarily stored in the staging """Flushes the directives that were temporarily stored in the staging
area into the package. area into the package.
@ -236,13 +278,9 @@ def _execute_version(pkg):
def _depends_on(pkg, spec, when=None, type=default_deptype, patches=None): def _depends_on(pkg, spec, when=None, type=default_deptype, patches=None):
# If when is False do nothing when_spec = make_when_spec(when)
if when is False: if not when_spec:
return return
# If when is None or True make sure the condition is always satisfied
if when is None or when is True:
when = pkg.name
when_spec = spack.spec.parse_anonymous_spec(when, pkg.name)
dep_spec = spack.spec.Spec(spec) dep_spec = spack.spec.Spec(spec)
if pkg.name == dep_spec.name: if pkg.name == dep_spec.name:
@ -304,8 +342,9 @@ def conflicts(conflict_spec, when=None, msg=None):
""" """
def _execute_conflicts(pkg): def _execute_conflicts(pkg):
# If when is not specified the conflict always holds # If when is not specified the conflict always holds
condition = pkg.name if when is None else when when_spec = make_when_spec(when)
when_spec = spack.spec.parse_anonymous_spec(condition, pkg.name) if not when_spec:
return
# Save in a list the conflicts and the associated custom messages # Save in a list the conflicts and the associated custom messages
when_spec_list = pkg.conflicts.setdefault(conflict_spec, []) when_spec_list = pkg.conflicts.setdefault(conflict_spec, [])
@ -352,12 +391,11 @@ def extends(spec, **kwargs):
""" """
def _execute_extends(pkg): def _execute_extends(pkg):
# if pkg.extendees: when = kwargs.get('when')
# directive = 'extends' when_spec = make_when_spec(when)
# msg = 'Packages can extend at most one other package.' if not when_spec:
# raise DirectiveError(directive, msg) return
when = kwargs.get('when', pkg.name)
_depends_on(pkg, spec, when=when) _depends_on(pkg, spec, when=when)
pkg.extendees[spec] = (spack.spec.Spec(spec), kwargs) pkg.extendees[spec] = (spack.spec.Spec(spec), kwargs)
return _execute_extends return _execute_extends
@ -370,8 +408,14 @@ def provides(*specs, **kwargs):
can use the providing package to satisfy the dependency. can use the providing package to satisfy the dependency.
""" """
def _execute_provides(pkg): def _execute_provides(pkg):
spec_string = kwargs.get('when', pkg.name) when = kwargs.get('when')
provider_spec = spack.spec.parse_anonymous_spec(spec_string, pkg.name) when_spec = make_when_spec(when)
if not when_spec:
return
# ``when`` specs for ``provides()`` need a name, as they are used
# to build the ProviderIndex.
when_spec.name = pkg.name
for string in specs: for string in specs:
for provided_spec in spack.spec.parse(string): for provided_spec in spack.spec.parse(string):
@ -381,7 +425,7 @@ def _execute_provides(pkg):
if provided_spec not in pkg.provided: if provided_spec not in pkg.provided:
pkg.provided[provided_spec] = set() pkg.provided[provided_spec] = set()
pkg.provided[provided_spec].add(provider_spec) pkg.provided[provided_spec].add(when_spec)
return _execute_provides return _execute_provides
@ -407,9 +451,9 @@ def patch(url_or_filename, level=1, when=None, working_dir=".", **kwargs):
""" """
def _execute_patch(pkg_or_dep): def _execute_patch(pkg_or_dep):
constraint = pkg_or_dep.name if when is None else when when_spec = make_when_spec(when)
when_spec = spack.spec.parse_anonymous_spec( if not when_spec:
constraint, pkg_or_dep.name) return
# if this spec is identical to some other, then append this # if this spec is identical to some other, then append this
# patch to the existing list. # patch to the existing list.
@ -551,7 +595,11 @@ def resource(**kwargs):
resource is moved into the main package stage area. resource is moved into the main package stage area.
""" """
def _execute_resource(pkg): def _execute_resource(pkg):
when = kwargs.get('when', pkg.name) when = kwargs.get('when')
when_spec = make_when_spec(when)
if not when_spec:
return
destination = kwargs.get('destination', "") destination = kwargs.get('destination', "")
placement = kwargs.get('placement', None) placement = kwargs.get('placement', None)
@ -574,7 +622,6 @@ def _execute_resource(pkg):
message += "\tdestination : '{dest}'\n".format(dest=destination) message += "\tdestination : '{dest}'\n".format(dest=destination)
raise RuntimeError(message) raise RuntimeError(message)
when_spec = spack.spec.parse_anonymous_spec(when, pkg.name)
resources = pkg.resources.setdefault(when_spec, []) resources = pkg.resources.setdefault(when_spec, [])
name = kwargs.get('name') name = kwargs.get('name')
fetcher = from_kwargs(**kwargs) fetcher = from_kwargs(**kwargs)

View File

@ -128,7 +128,6 @@
__all__ = [ __all__ = [
'Spec', 'Spec',
'parse', 'parse',
'parse_anonymous_spec',
'SpecError', 'SpecError',
'SpecParseError', 'SpecParseError',
'DuplicateDependencyError', 'DuplicateDependencyError',
@ -2554,17 +2553,9 @@ def _autospec(self, spec_like):
it. If it's a string, tries to parse a string. If that fails, tries it. If it's a string, tries to parse a string. If that fails, tries
to parse a local spec from it (i.e. name is assumed to be self's name). to parse a local spec from it (i.e. name is assumed to be self's name).
""" """
if isinstance(spec_like, spack.spec.Spec): if isinstance(spec_like, Spec):
return spec_like return spec_like
return Spec(spec_like)
try:
spec = spack.spec.Spec(spec_like)
if not spec.name:
raise SpecError(
"anonymous package -- this will always be handled")
return spec
except SpecError:
return parse_anonymous_spec(spec_like, self.name)
def satisfies(self, other, deps=True, strict=False, strict_deps=False): def satisfies(self, other, deps=True, strict=False, strict_deps=False):
"""Determine if this spec satisfies all constraints of another. """Determine if this spec satisfies all constraints of another.
@ -2933,15 +2924,21 @@ def __getitem__(self, name):
return value return value
def __contains__(self, spec): def __contains__(self, spec):
"""True if this spec satisfies the provided spec, or if any dependency """True if this spec or some dependency satisfies the spec.
does. If the spec has no name, then we parse this one first.
Note: If ``spec`` is anonymous, we ONLY check whether the root
satisfies it, NOT dependencies. This is because most anonymous
specs (e.g., ``@1.2``) don't make sense when applied across an
entire DAG -- we limit them to the root.
""" """
spec = self._autospec(spec) spec = self._autospec(spec)
for s in self.traverse():
if s.satisfies(spec, strict=True):
return True
return False # if anonymous or same name, we only have to look at the root
if not spec.name or spec.name == self.name:
return self.satisfies(spec)
else:
return any(s.satisfies(spec) for s in self.traverse(root=False))
def sorted_deps(self): def sorted_deps(self):
"""Return a list of all dependencies sorted by name.""" """Return a list of all dependencies sorted by name."""
@ -3945,41 +3942,6 @@ def parse(string):
return SpecParser().parse(string) return SpecParser().parse(string)
def parse_anonymous_spec(spec_like, pkg_name):
"""Allow the user to omit the package name part of a spec if they
know what it has to be already.
e.g., provides('mpi@2', when='@1.9:') says that this package
provides MPI-3 when its version is higher than 1.9.
"""
if not isinstance(spec_like, (str, Spec)):
raise TypeError('spec must be Spec or spec string. Found %s'
% type(spec_like))
if isinstance(spec_like, str):
try:
anon_spec = Spec(spec_like)
if anon_spec.name != pkg_name:
raise SpecParseError(spack.parse.ParseError(
"",
"",
"Expected anonymous spec for package %s but found spec for"
"package %s" % (pkg_name, anon_spec.name)))
except SpecParseError:
anon_spec = Spec(pkg_name + ' ' + spec_like)
if anon_spec.name != pkg_name:
raise ValueError(
"Invalid spec for package %s: %s" % (pkg_name, spec_like))
else:
anon_spec = spec_like.copy()
if anon_spec.name != pkg_name:
raise ValueError("Spec name '%s' must match package name '%s'"
% (anon_spec.name, pkg_name))
return anon_spec
def save_dependency_spec_yamls( def save_dependency_spec_yamls(
root_spec_as_yaml, output_directory, dependencies=None): root_spec_as_yaml, output_directory, dependencies=None):
"""Given a root spec (represented as a yaml object), index it with a subset """Given a root spec (represented as a yaml object), index it with a subset

View File

@ -0,0 +1,34 @@
# Copyright 2013-2019 Lawrence Livermore National Security, LLC and other
# Spack Project Developers. See the top-level COPYRIGHT file for details.
#
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
import spack.repo
from spack.spec import Spec
def test_false_directives_do_not_exist(mock_packages):
"""Ensure directives that evaluate to False at import time are added to
dicts on packages.
"""
cls = spack.repo.path.get_pkg_class('when-directives-false')
assert not cls.dependencies
assert not cls.resources
assert not cls.patches
def test_true_directives_exist(mock_packages):
"""Ensure directives that evaluate to True at import time are added to
dicts on packages.
"""
cls = spack.repo.path.get_pkg_class('when-directives-true')
assert cls.dependencies
assert Spec() in cls.dependencies['extendee']
assert Spec() in cls.dependencies['b']
assert cls.resources
assert Spec() in cls.resources
assert cls.patches
assert Spec() in cls.patches

View File

@ -143,16 +143,16 @@ def test_nested_directives(mock_packages):
# to Dependency objects. # to Dependency objects.
libelf_dep = next(iter(patcher.dependencies['libelf'].values())) libelf_dep = next(iter(patcher.dependencies['libelf'].values()))
assert len(libelf_dep.patches) == 1 assert len(libelf_dep.patches) == 1
assert len(libelf_dep.patches[Spec('libelf')]) == 1 assert len(libelf_dep.patches[Spec()]) == 1
libdwarf_dep = next(iter(patcher.dependencies['libdwarf'].values())) libdwarf_dep = next(iter(patcher.dependencies['libdwarf'].values()))
assert len(libdwarf_dep.patches) == 2 assert len(libdwarf_dep.patches) == 2
assert len(libdwarf_dep.patches[Spec('libdwarf')]) == 1 assert len(libdwarf_dep.patches[Spec()]) == 1
assert len(libdwarf_dep.patches[Spec('libdwarf@20111030')]) == 1 assert len(libdwarf_dep.patches[Spec('@20111030')]) == 1
fake_dep = next(iter(patcher.dependencies['fake'].values())) fake_dep = next(iter(patcher.dependencies['fake'].values()))
assert len(fake_dep.patches) == 1 assert len(fake_dep.patches) == 1
assert len(fake_dep.patches[Spec('fake')]) == 2 assert len(fake_dep.patches[Spec()]) == 2
def test_patched_dependency( def test_patched_dependency(

View File

@ -7,7 +7,7 @@
import pytest import pytest
from spack.spec import Spec, UnsatisfiableSpecError, SpecError from spack.spec import Spec, UnsatisfiableSpecError, SpecError
from spack.spec import substitute_abstract_variants, parse_anonymous_spec from spack.spec import substitute_abstract_variants
from spack.spec import SpecFormatSigilError, SpecFormatStringError from spack.spec import SpecFormatSigilError, SpecFormatStringError
from spack.variant import InvalidVariantValueError from spack.variant import InvalidVariantValueError
from spack.variant import MultipleValuesInExclusiveVariantError from spack.variant import MultipleValuesInExclusiveVariantError
@ -17,50 +17,46 @@
import spack.error import spack.error
def target_factory(spec_string, target_concrete): def make_spec(spec_like, concrete):
spec = Spec(spec_string) if spec_string else Spec() if isinstance(spec_like, Spec):
return spec_like
if target_concrete: spec = Spec(spec_like)
if concrete:
spec._mark_concrete() spec._mark_concrete()
substitute_abstract_variants(spec) substitute_abstract_variants(spec)
return spec return spec
def argument_factory(argument_spec, left): def _specify(spec_like):
try: if isinstance(spec_like, Spec):
# If it's not anonymous, allow it return spec_like
right = target_factory(argument_spec, False)
except Exception: return Spec(spec_like)
right = parse_anonymous_spec(argument_spec, left.name)
return right
def check_satisfies(target_spec, argument_spec, target_concrete=False): def check_satisfies(target_spec, constraint_spec, target_concrete=False):
left = target_factory(target_spec, target_concrete) target = make_spec(target_spec, target_concrete)
right = argument_factory(argument_spec, left) constraint = _specify(constraint_spec)
# Satisfies is one-directional. # Satisfies is one-directional.
assert left.satisfies(right) assert target.satisfies(constraint)
if argument_spec:
assert left.satisfies(argument_spec)
# If left satisfies right, then we should be able to constrain # If target satisfies constraint, then we should be able to constrain
# right by left. Reverse is not always true. # constraint by target. Reverse is not always true.
right.copy().constrain(left) constraint.copy().constrain(target)
def check_unsatisfiable(target_spec, argument_spec, target_concrete=False): def check_unsatisfiable(target_spec, constraint_spec, target_concrete=False):
left = target_factory(target_spec, target_concrete) target = make_spec(target_spec, target_concrete)
right = argument_factory(argument_spec, left) constraint = _specify(constraint_spec)
assert not left.satisfies(right) assert not target.satisfies(constraint)
assert not left.satisfies(argument_spec)
with pytest.raises(UnsatisfiableSpecError): with pytest.raises(UnsatisfiableSpecError):
right.copy().constrain(left) constraint.copy().constrain(target)
def check_constrain(expected, spec, constraint): def check_constrain(expected, spec, constraint):
@ -99,31 +95,31 @@ def test_satisfies(self):
def test_empty_satisfies(self): def test_empty_satisfies(self):
# Basic satisfaction # Basic satisfaction
check_satisfies('libelf', '') check_satisfies('libelf', Spec())
check_satisfies('libdwarf', '') check_satisfies('libdwarf', Spec())
check_satisfies('%intel', '') check_satisfies('%intel', Spec())
check_satisfies('^mpi', '') check_satisfies('^mpi', Spec())
check_satisfies('+debug', '') check_satisfies('+debug', Spec())
check_satisfies('@3:', '') check_satisfies('@3:', Spec())
# Concrete (strict) satisfaction # Concrete (strict) satisfaction
check_satisfies('libelf', '', True) check_satisfies('libelf', Spec(), True)
check_satisfies('libdwarf', '', True) check_satisfies('libdwarf', Spec(), True)
check_satisfies('%intel', '', True) check_satisfies('%intel', Spec(), True)
check_satisfies('^mpi', '', True) check_satisfies('^mpi', Spec(), True)
# TODO: Variants can't be called concrete while anonymous # TODO: Variants can't be called concrete while anonymous
# check_satisfies('+debug', '', True) # check_satisfies('+debug', Spec(), True)
check_satisfies('@3:', '', True) check_satisfies('@3:', Spec(), True)
# Reverse (non-strict) satisfaction # Reverse (non-strict) satisfaction
check_satisfies('', 'libelf') check_satisfies(Spec(), 'libelf')
check_satisfies('', 'libdwarf') check_satisfies(Spec(), 'libdwarf')
check_satisfies('', '%intel') check_satisfies(Spec(), '%intel')
check_satisfies('', '^mpi') check_satisfies(Spec(), '^mpi')
# TODO: Variant matching is auto-strict # TODO: Variant matching is auto-strict
# we should rethink this # we should rethink this
# check_satisfies('', '+debug') # check_satisfies(Spec(), '+debug')
check_satisfies('', '@3:') check_satisfies(Spec(), '@3:')
def test_satisfies_namespace(self): def test_satisfies_namespace(self):
check_satisfies('builtin.mpich', 'mpich') check_satisfies('builtin.mpich', 'mpich')
@ -343,8 +339,8 @@ def test_unsatisfiable_multi_value_variant(self):
# Semantics for a multi-valued variant is different # Semantics for a multi-valued variant is different
# Depending on whether the spec is concrete or not # Depending on whether the spec is concrete or not
a = target_factory( a = make_spec(
'multivalue_variant foo="bar"', target_concrete=True 'multivalue_variant foo="bar"', concrete=True
) )
spec_str = 'multivalue_variant foo="bar,baz"' spec_str = 'multivalue_variant foo="bar,baz"'
b = Spec(spec_str) b = Spec(spec_str)
@ -363,8 +359,8 @@ def test_unsatisfiable_multi_value_variant(self):
# An abstract spec can instead be constrained # An abstract spec can instead be constrained
assert a.constrain(b) assert a.constrain(b)
a = target_factory( a = make_spec(
'multivalue_variant foo="bar,baz"', target_concrete=True 'multivalue_variant foo="bar,baz"', concrete=True
) )
spec_str = 'multivalue_variant foo="bar,baz,quux"' spec_str = 'multivalue_variant foo="bar,baz,quux"'
b = Spec(spec_str) b = Spec(spec_str)
@ -416,13 +412,13 @@ def test_unsatisfiable_variant_types(self):
check_unsatisfiable( check_unsatisfiable(
target_spec='multivalue_variant foo="bar"', target_spec='multivalue_variant foo="bar"',
argument_spec='multivalue_variant +foo', constraint_spec='multivalue_variant +foo',
target_concrete=True target_concrete=True
) )
check_unsatisfiable( check_unsatisfiable(
target_spec='multivalue_variant foo="bar"', target_spec='multivalue_variant foo="bar"',
argument_spec='multivalue_variant ~foo', constraint_spec='multivalue_variant ~foo',
target_concrete=True target_concrete=True
) )

View File

@ -9,7 +9,7 @@
import spack.store import spack.store
import spack.spec as sp import spack.spec as sp
from spack.parse import Token from spack.parse import Token
from spack.spec import Spec, parse, parse_anonymous_spec from spack.spec import Spec
from spack.spec import SpecParseError, RedundantSpecError from spack.spec import SpecParseError, RedundantSpecError
from spack.spec import AmbiguousHashError, InvalidHashError, NoSuchHashError from spack.spec import AmbiguousHashError, InvalidHashError, NoSuchHashError
from spack.spec import DuplicateArchitectureError, DuplicateVariantError from spack.spec import DuplicateArchitectureError, DuplicateVariantError
@ -532,18 +532,3 @@ def test_kv_with_spaces(self):
"mvapich_foo debug= 4 " "mvapich_foo debug= 4 "
"^ _openmpi @1.2 : 1.4 , 1.6 % intel @ 12.1 : 12.6 + debug - qt_4 " "^ _openmpi @1.2 : 1.4 , 1.6 % intel @ 12.1 : 12.6 + debug - qt_4 "
"^ stackwalker @ 8.1_1e") "^ stackwalker @ 8.1_1e")
@pytest.mark.parametrize('spec,anon_spec,spec_name', [
('openmpi languages=go', 'languages=go', 'openmpi'),
('openmpi @4.6:', '@4.6:', 'openmpi'),
('openmpi languages=go @4.6:', 'languages=go @4.6:', 'openmpi'),
('openmpi @4.6: languages=go', '@4.6: languages=go', 'openmpi'),
])
def test_parse_anonymous_specs(spec, anon_spec, spec_name):
expected = parse(spec)
spec = parse_anonymous_spec(anon_spec, spec_name)
assert len(expected) == 1
assert spec in expected

View File

@ -0,0 +1,28 @@
# Copyright 2013-2019 Lawrence Livermore National Security, LLC and other
# Spack Project Developers. See the top-level COPYRIGHT file for details.
#
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
from spack import *
class WhenDirectivesFalse(Package):
"""Package that tests False when specs on directives."""
homepage = "http://www.example.com"
url = "http://www.example.com/example-1.0.tar.gz"
version('1.0', '0123456789abcdef0123456789abcdef')
patch('https://example.com/foo.patch',
sha256='abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234',
when=False)
extends('extendee', when=False)
depends_on('b', when=False)
conflicts('@1.0', when=False)
resource(url="http://www.example.com/example-1.0-resource.tar.gz",
md5='0123456789abcdef0123456789abcdef',
when=False)
def install(self, spec, prefix):
pass

View File

@ -0,0 +1,28 @@
# Copyright 2013-2019 Lawrence Livermore National Security, LLC and other
# Spack Project Developers. See the top-level COPYRIGHT file for details.
#
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
from spack import *
class WhenDirectivesTrue(Package):
"""Package that tests True when specs on directives."""
homepage = "http://www.example.com"
url = "http://www.example.com/example-1.0.tar.gz"
version('1.0', '0123456789abcdef0123456789abcdef')
patch('https://example.com/foo.patch',
sha256='abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234',
when=True)
extends('extendee', when=True)
depends_on('b', when=True)
conflicts('@1.0', when=True)
resource(url="http://www.example.com/example-1.0-resource.tar.gz",
md5='0123456789abcdef0123456789abcdef',
when=True)
def install(self, spec, prefix):
pass