Repository: spack/spack

Issue:
Namespace arguments broken in spack.repo API
While coding towards #11158 I tried to use a custom namespace for the temporary repository being used. This caused strange errors, as for repositories that are not in the `spack.pkg` namespace directives won't be executed on their packages.

### Steps to reproduce the issue

```console
$ spack-python
Spack version 0.12.1
Python 2.7.15rc1, Linux x86_64
>>> import spack.repo
>>> import spack.spec
# Any repository path will be fine below
>>> repo = spack.repo.Repo('/tmp/tmp4yoo27fk', namespace='custom')
>>> spack.repo.path.put_first(repo)
# Choose a package in the custom repo
>>> s = spack.spec.Spec('concretizationroot')
>>> s.concretize()
Traceback (most recent call last):
  File "<console>", line 1, in <module>
  File "/home/mculpo/PycharmProjects/spack/lib/spack/spack/spec.py", line 1909, in concretize
    user_spec_deps=user_spec_deps),
  File "/home/mculpo/PycharmProjects/spack/lib/spack/spack/spec.py", line 2322, in normalize
    self.validate_or_raise()
  File "/home/mculpo/PycharmProjects/spack/lib/spack/spack/spec.py", line 2374, in validate_or_raise
    spack.repo.get(spec.fullname)
  File "/home/mculpo/PycharmProjects/spack/lib/spack/spack/repo.py", line 1198, in get
    return path.get(spec)
  File "/home/mculpo/PycharmProjects/spack/lib/spack/spack/repo.py", line 85, in converter
    return function(self, spec_like, *args, **kwargs)
  File "/home/mculpo/PycharmProjects/spack/lib/spack/spack/repo.py", line 644, in get
    return self.repo_for_pkg(spec).get(spec)
  File "/home/mculpo/PycharmProjects/spack/lib/spack/spack/repo.py", line 85, in converter
    return function(self, spec_like, *args, **kwargs)
  File "/home/mculpo/PycharmProjects/spack/lib/spack/spack/repo.py", line 906, in get
    raise FailedConstructorError(spec.fullname, *sys.exc_info())
FailedConstructorError: Class constructor failed for package 'concretizationroot'.
    
Caused by:
AttributeError: 'Concretizationroot' object has no attribute 'versions'
  File "/home/mculpo/PycharmProjects/spack/lib/spack/spack/repo.py", line 897, in get
    return package_class(spec)
  File "/home/mculpo/PycharmProjects/spack/lib/spack/spack/package.py", line 488, in __init__
    for v in self.versions:
```

### Additional information

This bug apparently was introduced in 39c9bbfbb which added this check:

https://github.com/spack/spack/blob/b2611c80862628046556c73e1e9da6ec69060428/lib/spack/spack/directives.py#L102-L104

to `DirectivesMeta`, which results in directives being executed only if the namespace of a repository is the default one. As a solution we could either relax this check, or simplify the repository interface by removing its optional `namespace` argument.


Discussion:

Resolving patch:
diff --git a/lib/spack/spack/repo.py b/lib/spack/spack/repo.py
--- a/lib/spack/spack/repo.py
+++ b/lib/spack/spack/repo.py
@@ -5,15 +5,17 @@
 
 import abc
 import collections
-import os
-import stat
-import shutil
+import contextlib
 import errno
-import sys
+import functools
 import inspect
+import os
 import re
+import shutil
+import stat
+import sys
 import traceback
-from contextlib import contextmanager
+
 from six import string_types, add_metaclass
 
 try:
@@ -44,7 +46,13 @@
 
 #: Super-namespace for all packages.
 #: Package modules are imported as spack.pkg.<namespace>.<pkg-name>.
-repo_namespace     = 'spack.pkg'
+repo_namespace = 'spack.pkg'
+
+
+def get_full_namespace(namespace):
+    """Returns the full namespace of a repository, given its relative one."""
+    return '{0}.{1}'.format(repo_namespace, namespace)
+
 
 #
 # These names describe how repos should be laid out in the filesystem.
@@ -74,10 +82,11 @@
 _package_prepend = 'from spack.pkgkit import *'
 
 
-def _autospec(function):
-    """Decorator that automatically converts the argument of a single-arg
-       function to a Spec."""
-
+def autospec(function):
+    """Decorator that automatically converts the first argument of a
+    function to a Spec.
+    """
+    @functools.wraps(function)
     def converter(self, spec_like, *args, **kwargs):
         if not isinstance(spec_like, spack.spec.Spec):
             spec_like = spack.spec.Spec(spec_like)
@@ -332,7 +341,7 @@ class RepoIndex(object):
     updates (using ``FastPackageChecker``) and for regenerating indexes
     when they're needed.
 
-    ``Indexers`` shoudl be added to the ``RepoIndex`` using
+    ``Indexers`` should be added to the ``RepoIndex`` using
     ``add_index(name, indexer)``, and they should support the interface
     defined by ``Indexer``, so that the ``RepoIndex`` can read, generate,
     and update stored indices.
@@ -428,15 +437,9 @@ class RepoPath(object):
 
     Args:
         repos (list): list Repo objects or paths to put in this RepoPath
-
-    Optional Args:
-        repo_namespace (str): super-namespace for all packages in this
-            RepoPath (used when importing repos as modules)
     """
 
-    def __init__(self, *repos, **kwargs):
-        self.super_namespace = kwargs.get('namespace', repo_namespace)
-
+    def __init__(self, *repos):
         self.repos = []
         self.by_namespace = NamespaceTrie()
 
@@ -448,7 +451,7 @@ def __init__(self, *repos, **kwargs):
         for repo in repos:
             try:
                 if isinstance(repo, string_types):
-                    repo = Repo(repo, self.super_namespace)
+                    repo = Repo(repo)
                 self.put_last(repo)
             except RepoError as e:
                 tty.warn("Failed to initialize repository: '%s'." % repo,
@@ -500,12 +503,12 @@ def get_repo(self, namespace, default=NOT_PROVIDED):
                 If default is provided, return it when the namespace
                 isn't found.  If not, raise an UnknownNamespaceError.
         """
-        fullspace = '%s.%s' % (self.super_namespace, namespace)
-        if fullspace not in self.by_namespace:
+        full_namespace = get_full_namespace(namespace)
+        if full_namespace not in self.by_namespace:
             if default == NOT_PROVIDED:
                 raise UnknownNamespaceError(namespace)
             return default
-        return self.by_namespace[fullspace]
+        return self.by_namespace[full_namespace]
 
     def first_repo(self):
         """Get the first repo in precedence order."""
@@ -551,14 +554,14 @@ def patch_index(self):
 
         return self._patch_index
 
-    @_autospec
+    @autospec
     def providers_for(self, vpkg_spec):
         providers = self.provider_index.providers_for(vpkg_spec)
         if not providers:
             raise UnknownPackageError(vpkg_spec.name)
         return providers
 
-    @_autospec
+    @autospec
     def extensions_for(self, extendee_spec):
         return [p for p in self.all_packages() if p.extends(extendee_spec)]
 
@@ -619,7 +622,7 @@ def repo_for_pkg(self, spec):
         # If the spec already has a namespace, then return the
         # corresponding repo if we know about it.
         if namespace:
-            fullspace = '%s.%s' % (self.super_namespace, namespace)
+            fullspace = get_full_namespace(namespace)
             if fullspace not in self.by_namespace:
                 raise UnknownNamespaceError(spec.namespace)
             return self.by_namespace[fullspace]
@@ -634,19 +637,16 @@ def repo_for_pkg(self, spec):
         # that can operate on packages that don't exist yet.
         return self.first_repo()
 
-    @_autospec
-    def get(self, spec, new=False):
-        """Find a repo that contains the supplied spec's package.
-
-           Raises UnknownPackageError if not found.
-        """
+    @autospec
+    def get(self, spec):
+        """Returns the package associated with the supplied spec."""
         return self.repo_for_pkg(spec).get(spec)
 
     def get_pkg_class(self, pkg_name):
         """Find a class for the spec's package and return the class object."""
         return self.repo_for_pkg(pkg_name).get_pkg_class(pkg_name)
 
-    @_autospec
+    @autospec
     def dump_provenance(self, spec, path):
         """Dump provenance information for a spec to a particular path.
 
@@ -689,25 +689,16 @@ class Repo(object):
 
     """
 
-    def __init__(self, root, namespace=repo_namespace):
+    def __init__(self, root):
         """Instantiate a package repository from a filesystem path.
 
-        Arguments:
-        root        The root directory of the repository.
-
-        namespace   A super-namespace that will contain the repo-defined
-                    namespace (this is generally jsut `spack.pkg`). The
-                    super-namespace is Spack's way of separating repositories
-                    from other python namespaces.
-
+        Args:
+            root: the root directory of the repository
         """
         # Root directory, containing _repo.yaml and package dirs
         # Allow roots to by spack-relative by starting with '$spack'
         self.root = canonicalize_path(root)
 
-        # super-namespace for all packages in the Repo
-        self.super_namespace = namespace
-
         # check and raise BadRepoError on fail.
         def check(condition, msg):
             if not condition:
@@ -734,11 +725,7 @@ def check(condition, msg):
               "Namespaces must be valid python identifiers separated by '.'")
 
         # Set up 'full_namespace' to include the super-namespace
-        if self.super_namespace:
-            self.full_namespace = "%s.%s" % (
-                self.super_namespace, self.namespace)
-        else:
-            self.full_namespace = self.namespace
+        self.full_namespace = get_full_namespace(self.namespace)
 
         # Keep name components around for checking prefixes.
         self._names = self.full_namespace.split('.')
@@ -881,8 +868,9 @@ def _read_config(self):
             tty.die("Error reading %s when opening %s"
                     % (self.config_file, self.root))
 
-    @_autospec
+    @autospec
     def get(self, spec):
+        """Returns the package associated with the supplied spec."""
         if not self.exists(spec.name):
             raise UnknownPackageError(spec.name)
 
@@ -904,7 +892,7 @@ def get(self, spec):
                 sys.excepthook(*sys.exc_info())
             raise FailedConstructorError(spec.fullname, *sys.exc_info())
 
-    @_autospec
+    @autospec
     def dump_provenance(self, spec, path):
         """Dump provenance information for a spec to a particular path.
 
@@ -961,14 +949,14 @@ def patch_index(self):
         """Index of patches and packages they're defined on."""
         return self.index['patches']
 
-    @_autospec
+    @autospec
     def providers_for(self, vpkg_spec):
         providers = self.provider_index.providers_for(vpkg_spec)
         if not providers:
             raise UnknownPackageError(vpkg_spec.name)
         return providers
 
-    @_autospec
+    @autospec
     def extensions_for(self, extendee_spec):
         return [p for p in self.all_packages() if p.extends(extendee_spec)]
 
@@ -977,14 +965,14 @@ def _check_namespace(self, spec):
         if spec.namespace and spec.namespace != self.namespace:
             raise UnknownNamespaceError(spec.namespace)
 
-    @_autospec
+    @autospec
     def dirname_for_package_name(self, spec):
         """Get the directory name for a particular package.  This is the
            directory that contains its package.py file."""
         self._check_namespace(spec)
         return os.path.join(self.packages_path, spec.name)
 
-    @_autospec
+    @autospec
     def filename_for_package_name(self, spec):
         """Get the filename for the module we should load for a particular
            package.  Packages for a Repo live in
@@ -1150,9 +1138,6 @@ def create_repo(root, namespace=None):
             config.write("  namespace: '%s'\n" % namespace)
 
     except (IOError, OSError) as e:
-        raise BadRepoError('Failed to create new repository in %s.' % root,
-                           "Caused by %s: %s" % (type(e), e))
-
         # try to clean up.
         if existed:
             shutil.rmtree(config_path, ignore_errors=True)
@@ -1160,6 +1145,9 @@ def create_repo(root, namespace=None):
         else:
             shutil.rmtree(root, ignore_errors=True)
 
+        raise BadRepoError('Failed to create new repository in %s.' % root,
+                           "Caused by %s: %s" % (type(e), e))
+
     return full_path, namespace
 
 
@@ -1168,7 +1156,7 @@ def create_or_construct(path, namespace=None):
     if not os.path.exists(path):
         mkdirp(path)
         create_repo(path, namespace)
-    return Repo(path, namespace)
+    return Repo(path)
 
 
 def _path():
@@ -1218,7 +1206,7 @@ def set_path(repo):
     return append
 
 
-@contextmanager
+@contextlib.contextmanager
 def swap(repo_path):
     """Temporarily use another RepoPath."""
     global path


Test patch: