From 1f8ce403dcc84a741bdef8dc08db1b8182690386 Mon Sep 17 00:00:00 2001
From: Todd Gamblin <tgamblin@llnl.gov>
Date: Tue, 17 Mar 2015 23:23:56 -0400
Subject: Modularize directives.  Now each directive specifies its storage.

---
 lib/spack/llnl/util/lang.py   |  25 +++---
 lib/spack/spack/directives.py | 182 ++++++++++++++++++++++++++++++------------
 lib/spack/spack/package.py    |  44 +---------
 3 files changed, 143 insertions(+), 108 deletions(-)

(limited to 'lib')

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:
-- 
cgit v1.2.3-70-g09d2