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() stack = inspect.stack()
try: try:
# get calling function name (the relation)
relation = stack[1][3]
# Make sure locals contain __module__ # Make sure locals contain __module__
caller_locals = stack[2][0].f_locals caller_locals = stack[2][0].f_locals
finally: finally:
del stack del stack
if not '__module__' in caller_locals: 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__'] module_name = caller_locals['__module__']
base_name = module_name.split('.')[-1] 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 """Returns a class that wraps a dictionary and enables it to be used
like an object.""" like an object."""
class wrapper(object): class wrapper(object):
def __getattr__(self, name): def __getattr__(self, name): return dictionary[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() return wrapper()

View File

@ -41,9 +41,11 @@ class OpenMpi(Package):
* ``provides`` * ``provides``
* ``extends`` * ``extends``
* ``patch`` * ``patch``
* ``variant``
""" """
__all__ = [ 'depends_on', 'extends', 'provides', 'patch', 'version' ] __all__ = [ 'depends_on', 'extends', 'provides', 'patch', 'version',
'variant' ]
import re import re
import inspect import inspect
@ -59,52 +61,125 @@ class OpenMpi(Package):
from spack.spec import Spec, parse_anonymous_spec from spack.spec import Spec, parse_anonymous_spec
def directive(fun): #
"""Decorator that allows a function to be called while a class is # This is a list of all directives, built up as they are defined in
being constructed, and to modify the class. # 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()) def __init__(self, **kwargs):
pkg.name = get_calling_module_name() # dict argument allows directives to have storage on the package.
return fun(pkg, *args, **kwargs) dicts = kwargs.get('dicts', None)
return directive_function
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): def version(pkg, ver, checksum=None, **kwargs):
"""Adds a version and metadata describing how to fetch it. """Adds a version and metadata describing how to fetch it.
Metadata is just stored as a dict in the package's versions Metadata is just stored as a dict in the package's versions
dictionary. Package must turn it into a valid fetch strategy dictionary. Package must turn it into a valid fetch strategy
later. later.
""" """
versions = pkg.setdefault('versions', {})
# special case checksum for backward compatibility # special case checksum for backward compatibility
if checksum: if checksum:
kwargs['md5'] = checksum kwargs['md5'] = checksum
# Store the kwargs for the package to use later when constructing # Store kwargs for the package to later with a fetch_strategy.
# a fetch strategy. pkg.versions[Version(ver)] = kwargs
versions[Version(ver)] = kwargs
@directive @directive(dicts='dependencies')
def depends_on(pkg, *specs): def depends_on(pkg, *specs):
"""Adds a dependencies local variable in the locals of """Adds a dependencies local variable in the locals of
the calling class, based on args. """ the calling class, based on args. """
dependencies = pkg.setdefault('dependencies', {})
for string in specs: for string in specs:
for spec in spack.spec.parse(string): for spec in spack.spec.parse(string):
if pkg.name == spec.name: if pkg.name == spec.name:
raise CircularReferenceError('depends_on', pkg.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): def extends(pkg, spec, **kwargs):
"""Same as depends_on, but dependency is symlinked into parent prefix. """Same as depends_on, but dependency is symlinked into parent prefix.
@ -119,19 +194,17 @@ def extends(pkg, spec, **kwargs):
mechanism. mechanism.
""" """
dependencies = pkg.setdefault('dependencies', {}) if pkg.extendees:
extendees = pkg.setdefault('extendees', {}) raise DirectiveError("Packages can extend at most one other package.")
if extendees:
raise RelationError("Packages can extend at most one other package.")
spec = Spec(spec) spec = Spec(spec)
if pkg.name == spec.name: if pkg.name == spec.name:
raise CircularReferenceError('extends', pkg.name) raise CircularReferenceError('extends', pkg.name)
dependencies[spec.name] = spec pkg.dependencies[spec.name] = spec
extendees[spec.name] = (spec, kwargs) pkg.extendees[spec.name] = (spec, kwargs)
@directive @directive(dicts='provided')
def provides(pkg, *specs, **kwargs): def provides(pkg, *specs, **kwargs):
"""Allows packages to provide a virtual dependency. If a package provides """Allows packages to provide a virtual dependency. If a package provides
'mpi', other packages can declare that they depend on "mpi", and spack '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) spec_string = kwargs.get('when', pkg.name)
provider_spec = parse_anonymous_spec(spec_string, pkg.name) provider_spec = parse_anonymous_spec(spec_string, pkg.name)
provided = pkg.setdefault("provided", {})
for string in specs: for string in specs:
for provided_spec in spack.spec.parse(string): for provided_spec in spack.spec.parse(string):
if pkg.name == provided_spec.name: if pkg.name == provided_spec.name:
raise CircularReferenceError('depends_on', pkg.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): def patch(pkg, url_or_filename, **kwargs):
"""Packages can declare patches to apply to source. You can """Packages can declare patches to apply to source. You can
optionally provide a when spec to indicate that a particular 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) level = kwargs.get('level', 1)
when = kwargs.get('when', pkg.name) when = kwargs.get('when', pkg.name)
patches = pkg.setdefault('patches', {})
when_spec = parse_anonymous_spec(when, pkg.name) when_spec = parse_anonymous_spec(when, pkg.name)
if when_spec not in patches: if when_spec not in pkg.patches:
patches[when_spec] = [Patch(pkg.name, url_or_filename, level)] pkg.patches[when_spec] = [Patch(pkg.name, url_or_filename, level)]
else: else:
# 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.
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): @directive(dicts='variants')
"""This is raised when something is wrong with a package relation.""" def variant(pkg, name, description="", **kwargs):
def __init__(self, relation, message): """Define a variant for the package. Allows the user to supply
super(RelationError, self).__init__(message) +variant/-variant in a spec. You can optionally supply an
self.relation = relation 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): class DirectiveError(spack.error.SpackError):
"""This is raised when a relation is called from outside a spack package.""" """This is raised when something is wrong with a package directive."""
def __init__(self, relation): def __init__(self, directive, message):
super(ScopeError, self).__init__( super(DirectiveError, self).__init__(message)
relation, self.directive = directive
"Must invoke '%s' from inside a class definition!" % relation)
class CircularReferenceError(RelationError): class CircularReferenceError(DirectiveError):
"""This is raised when something depends on itself.""" """This is raised when something depends on itself."""
def __init__(self, relation, package): def __init__(self, directive, package):
super(CircularReferenceError, self).__init__( super(CircularReferenceError, self).__init__(
relation, directive,
"Package '%s' cannot pass itself to %s." % (package, relation)) "Package '%s' cannot pass itself to %s." % (package, directive))
self.package = package self.package = package

View File

@ -55,6 +55,7 @@
import spack.compilers import spack.compilers
import spack.mirror import spack.mirror
import spack.hooks import spack.hooks
import spack.directives
import spack.build_environment as build_env import spack.build_environment as build_env
import spack.url as url import spack.url as url
import spack.fetch_strategy as fs 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. 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. # These are default values for instance variables.
# #
@ -351,20 +325,8 @@ def __init__(self, spec):
if '.' in self.name: if '.' in self.name:
self.name = self.name[self.name.rindex('.') + 1:] self.name = self.name[self.name.rindex('.') + 1:]
# Sanity check some required variables that could be # Sanity check attributes required by Spack directives.
# overridden by package authors. spack.directives.ensure_dicts(type(self))
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')
# Check versions in the versions dict. # Check versions in the versions dict.
for v in self.versions: for v in self.versions: