summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorTodd Gamblin <tgamblin@llnl.gov>2015-03-17 23:23:56 -0400
committerTodd Gamblin <tgamblin@llnl.gov>2015-05-10 12:24:03 -0700
commit1f8ce403dcc84a741bdef8dc08db1b8182690386 (patch)
treeaf1c1ba2eca511542e486538885cce5cdb44db42 /lib
parent0944ba120cad3de7b84b15e19b9c889f5bea6241 (diff)
downloadspack-1f8ce403dcc84a741bdef8dc08db1b8182690386.tar.gz
spack-1f8ce403dcc84a741bdef8dc08db1b8182690386.tar.bz2
spack-1f8ce403dcc84a741bdef8dc08db1b8182690386.tar.xz
spack-1f8ce403dcc84a741bdef8dc08db1b8182690386.zip
Modularize directives. Now each directive specifies its storage.
Diffstat (limited to 'lib')
-rw-r--r--lib/spack/llnl/util/lang.py25
-rw-r--r--lib/spack/spack/directives.py182
-rw-r--r--lib/spack/spack/package.py44
3 files changed, 143 insertions, 108 deletions
diff --git a/lib/spack/llnl/util/lang.py b/lib/spack/llnl/util/lang.py
index 13453c20ed..9e1bef18ca 100644
--- a/lib/spack/llnl/util/lang.py
+++ b/lib/spack/llnl/util/lang.py
@@ -132,16 +132,14 @@ def get_calling_module_name():
"""
stack = inspect.stack()
try:
- # get calling function name (the relation)
- relation = stack[1][3]
-
# Make sure locals contain __module__
caller_locals = stack[2][0].f_locals
finally:
del stack
if not '__module__' in caller_locals:
- raise ScopeError(relation)
+ raise RuntimeError("Must invoke get_calling_module_name() "
+ "from inside a class definition!")
module_name = caller_locals['__module__']
base_name = module_name.split('.')[-1]
@@ -327,18 +325,15 @@ def DictWrapper(dictionary):
"""Returns a class that wraps a dictionary and enables it to be used
like an object."""
class wrapper(object):
- def __getattr__(self, name):
- return dictionary[name]
-
- def __setattr__(self, name, value):
- dictionary[name] = value
- return value
-
- def setdefault(self, *args):
- return dictionary.setdefault(*args)
+ def __getattr__(self, name): return dictionary[name]
+ def __setattr__(self, name, value): dictionary[name] = value
+ def setdefault(self, *args): return dictionary.setdefault(*args)
+ def get(self, *args): return dictionary.get(*args)
+ def keys(self): return dictionary.keys()
+ def values(self): return dictionary.values()
+ def items(self): return dictionary.items()
+ def __iter__(self): return iter(dictionary)
- def get(self, *args):
- return dictionary.get(*args)
return wrapper()
diff --git a/lib/spack/spack/directives.py b/lib/spack/spack/directives.py
index e1589c019f..a45edecad1 100644
--- a/lib/spack/spack/directives.py
+++ b/lib/spack/spack/directives.py
@@ -41,9 +41,11 @@ The available directives are:
* ``provides``
* ``extends``
* ``patch``
+ * ``variant``
"""
-__all__ = [ 'depends_on', 'extends', 'provides', 'patch', 'version' ]
+__all__ = [ 'depends_on', 'extends', 'provides', 'patch', 'version',
+ 'variant' ]
import re
import inspect
@@ -59,52 +61,125 @@ from spack.patch import Patch
from spack.spec import Spec, parse_anonymous_spec
-def directive(fun):
- """Decorator that allows a function to be called while a class is
- being constructed, and to modify the class.
+#
+# This is a list of all directives, built up as they are defined in
+# this file.
+#
+directives = {}
+
+
+def ensure_dicts(pkg):
+ """Ensure that a package has all the dicts required by directives."""
+ for name, d in directives.items():
+ d.ensure_dicts(pkg)
+
+
+class directive(object):
+ """Decorator for Spack directives.
+
+ Spack directives allow you to modify a package while it is being
+ defined, e.g. to add version or depenency information. Directives
+ are one of the key pieces of Spack's package "langauge", which is
+ embedded in python.
+
+ Here's an example directive:
+
+ @directive(dicts='versions')
+ version(pkg, ...):
+ ...
+
+ This directive allows you write:
+
+ class Foo(Package):
+ version(...)
+
+ The ``@directive`` decorator handles a couple things for you:
+
+ 1. Adds the class scope (pkg) as an initial parameter when
+ called, like a class method would. This allows you to modify
+ a package from within a directive, while the package is still
+ being defined.
+
+ 2. It automatically adds a dictionary called "versions" to the
+ package so that you can refer to pkg.versions.
+
+ The ``(dicts='versions')`` part ensures that ALL packages in Spack
+ will have a ``versions`` attribute after they're constructed, and
+ that if no directive actually modified it, it will just be an
+ empty dict.
+
+ This is just a modular way to add storage attributes to the
+ Package class, and it's how Spack gets information from the
+ packages to the core.
- Adds the class scope as an initial parameter when called, like
- a class method would.
"""
- def directive_function(*args, **kwargs):
- pkg = DictWrapper(caller_locals())
- pkg.name = get_calling_module_name()
- return fun(pkg, *args, **kwargs)
- return directive_function
+ def __init__(self, **kwargs):
+ # dict argument allows directives to have storage on the package.
+ dicts = kwargs.get('dicts', None)
+
+ if isinstance(dicts, basestring):
+ dicts = (dicts,)
+ elif type(dicts) not in (list, tuple):
+ raise TypeError(
+ "dicts arg must be list, tuple, or string. Found %s."
+ % type(dicts))
-@directive
+ self.dicts = dicts
+
+
+ def ensure_dicts(self, pkg):
+ """Ensure that a package has the dicts required by this directive."""
+ for d in self.dicts:
+ if not hasattr(pkg, d):
+ setattr(pkg, d, {})
+
+ attr = getattr(pkg, d)
+ if not isinstance(attr, dict):
+ raise spack.error.SpackError(
+ "Package %s has non-dict %s attribute!" % (pkg, d))
+
+
+ def __call__(self, directive_function):
+ directives[directive_function.__name__] = self
+
+ def wrapped(*args, **kwargs):
+ pkg = DictWrapper(caller_locals())
+ self.ensure_dicts(pkg)
+
+ pkg.name = get_calling_module_name()
+ return directive_function(pkg, *args, **kwargs)
+
+ return wrapped
+
+
+@directive(dicts='versions')
def version(pkg, ver, checksum=None, **kwargs):
"""Adds a version and metadata describing how to fetch it.
Metadata is just stored as a dict in the package's versions
dictionary. Package must turn it into a valid fetch strategy
later.
"""
- versions = pkg.setdefault('versions', {})
-
# special case checksum for backward compatibility
if checksum:
kwargs['md5'] = checksum
- # Store the kwargs for the package to use later when constructing
- # a fetch strategy.
- versions[Version(ver)] = kwargs
+ # Store kwargs for the package to later with a fetch_strategy.
+ pkg.versions[Version(ver)] = kwargs
-@directive
+@directive(dicts='dependencies')
def depends_on(pkg, *specs):
"""Adds a dependencies local variable in the locals of
the calling class, based on args. """
- dependencies = pkg.setdefault('dependencies', {})
-
for string in specs:
for spec in spack.spec.parse(string):
if pkg.name == spec.name:
raise CircularReferenceError('depends_on', pkg.name)
- dependencies[spec.name] = spec
+ pkg.dependencies[spec.name] = spec
-@directive
+@directive(dicts=('extendees', 'dependencies'))
def extends(pkg, spec, **kwargs):
"""Same as depends_on, but dependency is symlinked into parent prefix.
@@ -119,19 +194,17 @@ def extends(pkg, spec, **kwargs):
mechanism.
"""
- dependencies = pkg.setdefault('dependencies', {})
- extendees = pkg.setdefault('extendees', {})
- if extendees:
- raise RelationError("Packages can extend at most one other package.")
+ if pkg.extendees:
+ raise DirectiveError("Packages can extend at most one other package.")
spec = Spec(spec)
if pkg.name == spec.name:
raise CircularReferenceError('extends', pkg.name)
- dependencies[spec.name] = spec
- extendees[spec.name] = (spec, kwargs)
+ pkg.dependencies[spec.name] = spec
+ pkg.extendees[spec.name] = (spec, kwargs)
-@directive
+@directive(dicts='provided')
def provides(pkg, *specs, **kwargs):
"""Allows packages to provide a virtual dependency. If a package provides
'mpi', other packages can declare that they depend on "mpi", and spack
@@ -140,15 +213,14 @@ def provides(pkg, *specs, **kwargs):
spec_string = kwargs.get('when', pkg.name)
provider_spec = parse_anonymous_spec(spec_string, pkg.name)
- provided = pkg.setdefault("provided", {})
for string in specs:
for provided_spec in spack.spec.parse(string):
if pkg.name == provided_spec.name:
raise CircularReferenceError('depends_on', pkg.name)
- provided[provided_spec] = provider_spec
+ pkg.provided[provided_spec] = provider_spec
-@directive
+@directive(dicts='patches')
def patch(pkg, url_or_filename, **kwargs):
"""Packages can declare patches to apply to source. You can
optionally provide a when spec to indicate that a particular
@@ -158,36 +230,42 @@ def patch(pkg, url_or_filename, **kwargs):
level = kwargs.get('level', 1)
when = kwargs.get('when', pkg.name)
- patches = pkg.setdefault('patches', {})
-
when_spec = parse_anonymous_spec(when, pkg.name)
- if when_spec not in patches:
- patches[when_spec] = [Patch(pkg.name, url_or_filename, level)]
+ if when_spec not in pkg.patches:
+ pkg.patches[when_spec] = [Patch(pkg.name, url_or_filename, level)]
else:
# if this spec is identical to some other, then append this
# patch to the existing list.
- patches[when_spec].append(Patch(pkg.name, url_or_filename, level))
+ pkg.patches[when_spec].append(Patch(pkg.name, url_or_filename, level))
+
+
+@directive(dicts='variants')
+def variant(pkg, name, description="", **kwargs):
+ """Define a variant for the package. Allows the user to supply
+ +variant/-variant in a spec. You can optionally supply an
+ initial + or - to make the variant enabled or disabled by defaut.
+ """
+ return
+ if not re.match(r'[-~+]?[A-Za-z0-9_][A-Za-z0-9_.-]*', name):
+ raise DirectiveError("Invalid variant name in %s: '%s'"
+ % (pkg.name, name))
-class RelationError(spack.error.SpackError):
- """This is raised when something is wrong with a package relation."""
- def __init__(self, relation, message):
- super(RelationError, self).__init__(message)
- self.relation = relation
+ enabled = re.match(r'+', name)
+ pkg.variants[name] = enabled
-class ScopeError(RelationError):
- """This is raised when a relation is called from outside a spack package."""
- def __init__(self, relation):
- super(ScopeError, self).__init__(
- relation,
- "Must invoke '%s' from inside a class definition!" % relation)
+class DirectiveError(spack.error.SpackError):
+ """This is raised when something is wrong with a package directive."""
+ def __init__(self, directive, message):
+ super(DirectiveError, self).__init__(message)
+ self.directive = directive
-class CircularReferenceError(RelationError):
+class CircularReferenceError(DirectiveError):
"""This is raised when something depends on itself."""
- def __init__(self, relation, package):
+ def __init__(self, directive, package):
super(CircularReferenceError, self).__init__(
- relation,
- "Package '%s' cannot pass itself to %s." % (package, relation))
+ directive,
+ "Package '%s' cannot pass itself to %s." % (package, directive))
self.package = package
diff --git a/lib/spack/spack/package.py b/lib/spack/spack/package.py
index 7f2b53ceed..2891791339 100644
--- a/lib/spack/spack/package.py
+++ b/lib/spack/spack/package.py
@@ -55,6 +55,7 @@ import spack.error
import spack.compilers
import spack.mirror
import spack.hooks
+import spack.directives
import spack.build_environment as build_env
import spack.url as url
import spack.fetch_strategy as fs
@@ -301,33 +302,6 @@ class Package(object):
clean() (some of them do this), and others to provide custom behavior.
"""
-
- #
- # These variables are defaults for Spack's various package
- # directives.
- #
- """Map of information about Versions of this package.
- Map goes: Version -> dict of attributes"""
- versions = {}
-
- """Specs of dependency packages, keyed by name."""
- dependencies = {}
-
- """Specs of virtual packages provided by this package, keyed by name."""
- provided = {}
-
- """Specs of conflicting packages, keyed by name. """
- conflicted = {}
-
- """Patches to apply to newly expanded source, if any."""
- patches = {}
-
- """Specs of package this one extends, or None.
-
- Currently, ppackages can extend at most one other package.
- """
- extendees = {}
-
#
# These are default values for instance variables.
#
@@ -351,20 +325,8 @@ class Package(object):
if '.' in self.name:
self.name = self.name[self.name.rindex('.') + 1:]
- # Sanity check some required variables that could be
- # overridden by package authors.
- def ensure_has_dict(attr_name):
- if not hasattr(self, attr_name):
- raise PackageError("Package %s must define %s" % attr_name)
-
- attr = getattr(self, attr_name)
- if not isinstance(attr, dict):
- raise PackageError("Package %s has non-dict %s attribute!"
- % (self.name, attr_name))
- ensure_has_dict('versions')
- ensure_has_dict('dependencies')
- ensure_has_dict('conflicted')
- ensure_has_dict('patches')
+ # Sanity check attributes required by Spack directives.
+ spack.directives.ensure_dicts(type(self))
# Check versions in the versions dict.
for v in self.versions: