Modularize directives. Now each directive specifies its storage.

This commit is contained in:
Todd Gamblin 2015-03-17 23:23:56 -04:00
parent 0944ba120c
commit 1f8ce403dc
3 changed files with 143 additions and 108 deletions

View File

@ -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()

View File

@ -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

View File

@ -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: