diff options
author | Todd Gamblin <tgamblin@llnl.gov> | 2015-03-17 23:23:56 -0400 |
---|---|---|
committer | Todd Gamblin <tgamblin@llnl.gov> | 2015-05-10 12:24:03 -0700 |
commit | 1f8ce403dcc84a741bdef8dc08db1b8182690386 (patch) | |
tree | af1c1ba2eca511542e486538885cce5cdb44db42 /lib | |
parent | 0944ba120cad3de7b84b15e19b9c889f5bea6241 (diff) | |
download | spack-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.py | 25 | ||||
-rw-r--r-- | lib/spack/spack/directives.py | 182 | ||||
-rw-r--r-- | lib/spack/spack/package.py | 44 |
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: |