Modularize directives. Now each directive specifies its storage.
This commit is contained in:
parent
0944ba120c
commit
1f8ce403dc
@ -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 __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 __setattr__(self, name, value):
|
||||
dictionary[name] = value
|
||||
return value
|
||||
|
||||
def setdefault(self, *args):
|
||||
return dictionary.setdefault(*args)
|
||||
|
||||
def get(self, *args):
|
||||
return dictionary.get(*args)
|
||||
|
||||
return wrapper()
|
||||
|
||||
|
@ -41,9 +41,11 @@ class OpenMpi(Package):
|
||||
* ``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 @@ class OpenMpi(Package):
|
||||
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))
|
||||
|
||||
self.dicts = dicts
|
||||
|
||||
|
||||
@directive
|
||||
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))
|
||||
|
||||
|
||||
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
|
||||
@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))
|
||||
|
||||
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
|
||||
|
@ -55,6 +55,7 @@
|
||||
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 SomePackage(Package):
|
||||
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 @@ def __init__(self, spec):
|
||||
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:
|
||||
|
Loading…
Reference in New Issue
Block a user