Use the non-deprecated MetaPathFinder
interface (#29745)
* Extract the MetaPathFinder and Loaders for packages in their own classes https://peps.python.org/pep-0451/ Currently, RepoPath and Repo implement the (deprecated) interface of MetaPathFinder (find_module) and of Loader (load_module). This commit extracts both of them and places the code in their own classes. The MetaPathFinder interface is updated to contain both the deprecated "find_module" (for Python 2.7 support) and the recommended "find_spec". Update of the Loader interface is deferred at a subsequent commit. * Move the lines to be prepended inside "RepoLoader" Also adjust the naming of a few variables too * Remove spack.util.imp, since code is only used in spack.repo * Remove support from loading Python modules Python > 3 but < 3.5 * Remove `Repo._create_namespace` This function was interacting badly with the MetaPathFinder and causing issues with "normal" imports. Removing the function allows to do things like: ```python import spack.pkg.builtin.mpich cls = spack.pkg.builtin.mpich.Mpich ``` * Remove code needed to trigger the Singleton evaluation The finder is coded in a way to trigger the Singleton, so we don't need external code now that we register it at module level into `sys.meta_path`. * Add unit tests
This commit is contained in:
parent
48b222c36b
commit
ff04d1bfc1
@ -180,6 +180,7 @@ def setup(sphinx):
|
|||||||
('py:class', '_frozen_importlib_external.SourceFileLoader'),
|
('py:class', '_frozen_importlib_external.SourceFileLoader'),
|
||||||
# Spack classes that are private and we don't want to expose
|
# Spack classes that are private and we don't want to expose
|
||||||
('py:class', 'spack.provider_index._IndexBase'),
|
('py:class', 'spack.provider_index._IndexBase'),
|
||||||
|
('py:class', 'spack.repo._PrependFileLoader'),
|
||||||
]
|
]
|
||||||
|
|
||||||
# The reST default role (used for this markup: `text`) to use for all documents.
|
# The reST default role (used for this markup: `text`) to use for all documents.
|
||||||
|
@ -889,11 +889,6 @@ def load_module_from_file(module_name, module_path):
|
|||||||
except KeyError:
|
except KeyError:
|
||||||
pass
|
pass
|
||||||
raise
|
raise
|
||||||
elif sys.version_info[0] == 3 and sys.version_info[1] < 5:
|
|
||||||
import importlib.machinery
|
|
||||||
loader = importlib.machinery.SourceFileLoader( # novm
|
|
||||||
module_name, module_path)
|
|
||||||
module = loader.load_module()
|
|
||||||
elif sys.version_info[0] == 2:
|
elif sys.version_info[0] == 2:
|
||||||
import imp
|
import imp
|
||||||
module = imp.load_source(module_name, module_path)
|
module = imp.load_source(module_name, module_path)
|
||||||
|
@ -512,8 +512,7 @@ def setup_main_options(args):
|
|||||||
spack.config.set('config:locks', args.locks, scope='command_line')
|
spack.config.set('config:locks', args.locks, scope='command_line')
|
||||||
|
|
||||||
if args.mock:
|
if args.mock:
|
||||||
rp = spack.repo.RepoPath(spack.paths.mock_packages_path)
|
spack.repo.path = spack.repo.RepoPath(spack.paths.mock_packages_path)
|
||||||
spack.repo.set_path(rp)
|
|
||||||
|
|
||||||
# If the user asked for it, don't check ssl certs.
|
# If the user asked for it, don't check ssl certs.
|
||||||
if args.insecure:
|
if args.insecure:
|
||||||
|
@ -399,11 +399,7 @@ def module(self):
|
|||||||
@property
|
@property
|
||||||
def namespace(self):
|
def namespace(self):
|
||||||
"""Spack namespace for the package, which identifies its repo."""
|
"""Spack namespace for the package, which identifies its repo."""
|
||||||
namespace, dot, module = self.__module__.rpartition('.')
|
return spack.repo.namespace_from_fullname(self.__module__)
|
||||||
prefix = '%s.' % spack.repo.repo_namespace
|
|
||||||
if namespace.startswith(prefix):
|
|
||||||
namespace = namespace[len(prefix):]
|
|
||||||
return namespace
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def fullname(self):
|
def fullname(self):
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
import shutil
|
import shutil
|
||||||
import stat
|
import stat
|
||||||
import sys
|
import sys
|
||||||
|
import tempfile
|
||||||
import traceback
|
import traceback
|
||||||
import types
|
import types
|
||||||
from typing import Dict # novm
|
from typing import Dict # novm
|
||||||
@ -36,19 +37,269 @@
|
|||||||
import spack.provider_index
|
import spack.provider_index
|
||||||
import spack.spec
|
import spack.spec
|
||||||
import spack.tag
|
import spack.tag
|
||||||
import spack.util.imp as simp
|
|
||||||
import spack.util.naming as nm
|
import spack.util.naming as nm
|
||||||
import spack.util.path
|
import spack.util.path
|
||||||
from spack.util.executable import which
|
from spack.util.executable import which
|
||||||
|
|
||||||
#: Super-namespace for all packages.
|
#: Package modules are imported as spack.pkg.<repo-namespace>.<pkg-name>
|
||||||
#: Package modules are imported as spack.pkg.<namespace>.<pkg-name>.
|
ROOT_PYTHON_NAMESPACE = 'spack.pkg'
|
||||||
repo_namespace = 'spack.pkg'
|
|
||||||
|
|
||||||
|
|
||||||
def get_full_namespace(namespace):
|
def python_package_for_repo(namespace):
|
||||||
"""Returns the full namespace of a repository, given its relative one."""
|
"""Returns the full namespace of a repository, given its relative one
|
||||||
return '{0}.{1}'.format(repo_namespace, namespace)
|
|
||||||
|
For instance:
|
||||||
|
|
||||||
|
python_package_for_repo('builtin') == 'spack.pkg.builtin'
|
||||||
|
|
||||||
|
Args:
|
||||||
|
namespace (str): repo namespace
|
||||||
|
"""
|
||||||
|
return '{0}.{1}'.format(ROOT_PYTHON_NAMESPACE, namespace)
|
||||||
|
|
||||||
|
|
||||||
|
def namespace_from_fullname(fullname):
|
||||||
|
"""Return the repository namespace only for the full module name.
|
||||||
|
|
||||||
|
For instance:
|
||||||
|
|
||||||
|
namespace_from_fullname('spack.pkg.builtin.hdf5') == 'builtin'
|
||||||
|
|
||||||
|
Args:
|
||||||
|
fullname (str): full name for the Python module
|
||||||
|
"""
|
||||||
|
namespace, dot, module = fullname.rpartition('.')
|
||||||
|
prefix_and_dot = '{0}.'.format(ROOT_PYTHON_NAMESPACE)
|
||||||
|
if namespace.startswith(prefix_and_dot):
|
||||||
|
namespace = namespace[len(prefix_and_dot):]
|
||||||
|
return namespace
|
||||||
|
|
||||||
|
|
||||||
|
# The code below is needed to have a uniform Loader interface that could cover both
|
||||||
|
# Python 2.7 and Python 3.X when we load Spack packages as Python modules, e.g. when
|
||||||
|
# we do "import spack.pkg.builtin.mpich" in package recipes.
|
||||||
|
if sys.version_info[0] == 2:
|
||||||
|
import imp
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def import_lock():
|
||||||
|
try:
|
||||||
|
imp.acquire_lock()
|
||||||
|
yield
|
||||||
|
finally:
|
||||||
|
imp.release_lock()
|
||||||
|
|
||||||
|
def load_source(fullname, path, prepend=None):
|
||||||
|
"""Import a Python module from source.
|
||||||
|
|
||||||
|
Load the source file and add it to ``sys.modules``.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
fullname (str): full name of the module to be loaded
|
||||||
|
path (str): path to the file that should be loaded
|
||||||
|
prepend (str or None): some optional code to prepend to the
|
||||||
|
loaded module; e.g., can be used to inject import statements
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
the loaded module
|
||||||
|
"""
|
||||||
|
with import_lock():
|
||||||
|
with prepend_open(path, text=prepend) as f:
|
||||||
|
return imp.load_source(fullname, path, f)
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def prepend_open(f, *args, **kwargs):
|
||||||
|
"""Open a file for reading, but prepend with some text prepended
|
||||||
|
|
||||||
|
Arguments are same as for ``open()``, with one keyword argument,
|
||||||
|
``text``, specifying the text to prepend.
|
||||||
|
|
||||||
|
We have to write and read a tempfile for the ``imp``-based importer,
|
||||||
|
as the ``file`` argument to ``imp.load_source()`` requires a
|
||||||
|
low-level file handle.
|
||||||
|
|
||||||
|
See the ``importlib``-based importer for a faster way to do this in
|
||||||
|
later versions of python.
|
||||||
|
"""
|
||||||
|
text = kwargs.get('text', None)
|
||||||
|
|
||||||
|
with open(f, *args) as f:
|
||||||
|
with tempfile.NamedTemporaryFile(mode='w+') as tf:
|
||||||
|
if text:
|
||||||
|
tf.write(text + '\n')
|
||||||
|
tf.write(f.read())
|
||||||
|
tf.seek(0)
|
||||||
|
yield tf.file
|
||||||
|
|
||||||
|
class _PrependFileLoader(object):
|
||||||
|
def __init__(self, fullname, path, prepend=None):
|
||||||
|
# Done to have a compatible interface with Python 3
|
||||||
|
#
|
||||||
|
# All the object attributes used in this method must be defined
|
||||||
|
# by a derived class
|
||||||
|
pass
|
||||||
|
|
||||||
|
def package_module(self):
|
||||||
|
try:
|
||||||
|
module = load_source(
|
||||||
|
self.fullname, self.package_py, prepend=self._package_prepend
|
||||||
|
)
|
||||||
|
except SyntaxError as e:
|
||||||
|
# SyntaxError strips the path from the filename, so we need to
|
||||||
|
# manually construct the error message in order to give the
|
||||||
|
# user the correct package.py where the syntax error is located
|
||||||
|
msg = 'invalid syntax in {0:}, line {1:}'
|
||||||
|
raise SyntaxError(msg.format(self.package_py, e.lineno))
|
||||||
|
|
||||||
|
module.__package__ = self.repo.full_namespace
|
||||||
|
module.__loader__ = self
|
||||||
|
return module
|
||||||
|
|
||||||
|
def load_module(self, fullname):
|
||||||
|
# Compatibility method to support Python 2.7
|
||||||
|
if fullname in sys.modules:
|
||||||
|
return sys.modules[fullname]
|
||||||
|
|
||||||
|
namespace, dot, module_name = fullname.rpartition('.')
|
||||||
|
|
||||||
|
try:
|
||||||
|
module = self.package_module()
|
||||||
|
except Exception as e:
|
||||||
|
raise ImportError(str(e))
|
||||||
|
|
||||||
|
module.__loader__ = self
|
||||||
|
sys.modules[fullname] = module
|
||||||
|
if namespace != fullname:
|
||||||
|
parent = sys.modules[namespace]
|
||||||
|
if not hasattr(parent, module_name):
|
||||||
|
setattr(parent, module_name, module)
|
||||||
|
|
||||||
|
return module
|
||||||
|
|
||||||
|
else:
|
||||||
|
import importlib.machinery # novm
|
||||||
|
|
||||||
|
class _PrependFileLoader(importlib.machinery.SourceFileLoader): # novm
|
||||||
|
def __init__(self, fullname, path, prepend=None):
|
||||||
|
super(_PrependFileLoader, self).__init__(fullname, path)
|
||||||
|
self.prepend = prepend
|
||||||
|
|
||||||
|
def path_stats(self, path):
|
||||||
|
stats = super(_PrependFileLoader, self).path_stats(path)
|
||||||
|
if self.prepend:
|
||||||
|
stats["size"] += len(self.prepend) + 1
|
||||||
|
return stats
|
||||||
|
|
||||||
|
def get_data(self, path):
|
||||||
|
data = super(_PrependFileLoader, self).get_data(path)
|
||||||
|
if path != self.path or self.prepend is None:
|
||||||
|
return data
|
||||||
|
else:
|
||||||
|
return self.prepend.encode() + b"\n" + data
|
||||||
|
|
||||||
|
|
||||||
|
class RepoLoader(_PrependFileLoader):
|
||||||
|
"""Loads a Python module associated with a package in specific repository"""
|
||||||
|
#: Code in ``_package_prepend`` is prepended to imported packages.
|
||||||
|
#:
|
||||||
|
#: Spack packages were originally expected to call `from spack import *`
|
||||||
|
#: themselves, but it became difficult to manage and imports in the Spack
|
||||||
|
#: core the top-level namespace polluted by package symbols this way. To
|
||||||
|
#: solve this, the top-level ``spack`` package contains very few symbols
|
||||||
|
#: of its own, and importing ``*`` is essentially a no-op. The common
|
||||||
|
#: routines and directives that packages need are now in ``spack.pkgkit``,
|
||||||
|
#: and the import system forces packages to automatically include
|
||||||
|
#: this. This way, old packages that call ``from spack import *`` will
|
||||||
|
#: continue to work without modification, but it's no longer required.
|
||||||
|
_package_prepend = ('from __future__ import absolute_import;'
|
||||||
|
'from spack.pkgkit import *')
|
||||||
|
|
||||||
|
def __init__(self, fullname, repo, package_name):
|
||||||
|
self.repo = repo
|
||||||
|
self.package_name = package_name
|
||||||
|
self.package_py = repo.filename_for_package_name(package_name)
|
||||||
|
self.fullname = fullname
|
||||||
|
super(RepoLoader, self).__init__(
|
||||||
|
self.fullname, self.package_py, prepend=self._package_prepend
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SpackNamespaceLoader(object):
|
||||||
|
def create_module(self, spec):
|
||||||
|
return SpackNamespace(spec.name)
|
||||||
|
|
||||||
|
def exec_module(self, module):
|
||||||
|
module.__loader__ = self
|
||||||
|
|
||||||
|
def load_module(self, fullname):
|
||||||
|
# Compatibility method to support Python 2.7
|
||||||
|
if fullname in sys.modules:
|
||||||
|
return sys.modules[fullname]
|
||||||
|
module = SpackNamespace(fullname)
|
||||||
|
self.exec_module(module)
|
||||||
|
|
||||||
|
namespace, dot, module_name = fullname.rpartition('.')
|
||||||
|
sys.modules[fullname] = module
|
||||||
|
if namespace != fullname:
|
||||||
|
parent = sys.modules[namespace]
|
||||||
|
if not hasattr(parent, module_name):
|
||||||
|
setattr(parent, module_name, module)
|
||||||
|
|
||||||
|
return module
|
||||||
|
|
||||||
|
|
||||||
|
class ReposFinder(object):
|
||||||
|
"""MetaPathFinder class that loads a Python module corresponding to a Spack package
|
||||||
|
|
||||||
|
Return a loader based on the inspection of the current global repository list.
|
||||||
|
"""
|
||||||
|
def find_spec(self, fullname, python_path, target=None):
|
||||||
|
# This function is Python 3 only and will not be called by Python 2.7
|
||||||
|
import importlib.util
|
||||||
|
|
||||||
|
# "target" is not None only when calling importlib.reload()
|
||||||
|
if target is not None:
|
||||||
|
raise RuntimeError('cannot reload module "{0}"'.format(fullname))
|
||||||
|
|
||||||
|
# Preferred API from https://peps.python.org/pep-0451/
|
||||||
|
if not fullname.startswith(ROOT_PYTHON_NAMESPACE):
|
||||||
|
return None
|
||||||
|
|
||||||
|
loader = self.compute_loader(fullname)
|
||||||
|
if loader is None:
|
||||||
|
return None
|
||||||
|
return importlib.util.spec_from_loader(fullname, loader) # novm
|
||||||
|
|
||||||
|
def compute_loader(self, fullname):
|
||||||
|
# namespaces are added to repo, and package modules are leaves.
|
||||||
|
namespace, dot, module_name = fullname.rpartition('.')
|
||||||
|
|
||||||
|
# If it's a module in some repo, or if it is the repo's
|
||||||
|
# namespace, let the repo handle it.
|
||||||
|
for repo in path.repos:
|
||||||
|
# We are using the namespace of the repo and the repo contains the package
|
||||||
|
if namespace == repo.full_namespace:
|
||||||
|
# With 2 nested conditionals we can call "repo.real_name" only once
|
||||||
|
package_name = repo.real_name(module_name)
|
||||||
|
if package_name:
|
||||||
|
return RepoLoader(fullname, repo, package_name)
|
||||||
|
|
||||||
|
# We are importing a full namespace like 'spack.pkg.builtin'
|
||||||
|
if fullname == repo.full_namespace:
|
||||||
|
return SpackNamespaceLoader()
|
||||||
|
|
||||||
|
# No repo provides the namespace, but it is a valid prefix of
|
||||||
|
# something in the RepoPath.
|
||||||
|
if path.by_namespace.is_prefix(fullname):
|
||||||
|
return SpackNamespaceLoader()
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def find_module(self, fullname, python_path=None):
|
||||||
|
# Compatibility method to support Python 2.7
|
||||||
|
if not fullname.startswith(ROOT_PYTHON_NAMESPACE):
|
||||||
|
return None
|
||||||
|
return self.compute_loader(fullname)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -62,22 +313,6 @@ def get_full_namespace(namespace):
|
|||||||
#: Guaranteed unused default value for some functions.
|
#: Guaranteed unused default value for some functions.
|
||||||
NOT_PROVIDED = object()
|
NOT_PROVIDED = object()
|
||||||
|
|
||||||
#: Code in ``_package_prepend`` is prepended to imported packages.
|
|
||||||
#:
|
|
||||||
#: Spack packages were originally expected to call `from spack import *`
|
|
||||||
#: themselves, but it became difficult to manage and imports in the Spack
|
|
||||||
#: core the top-level namespace polluted by package symbols this way. To
|
|
||||||
#: solve this, the top-level ``spack`` package contains very few symbols
|
|
||||||
#: of its own, and importing ``*`` is essentially a no-op. The common
|
|
||||||
#: routines and directives that packages need are now in ``spack.pkgkit``,
|
|
||||||
#: and the import system forces packages to automatically include
|
|
||||||
#: this. This way, old packages that call ``from spack import *`` will
|
|
||||||
#: continue to work without modification, but it's no longer required.
|
|
||||||
#:
|
|
||||||
#: TODO: At some point in the future, consider removing ``from spack import *``
|
|
||||||
#: TODO: from packages and shifting to from ``spack.pkgkit import *``
|
|
||||||
_package_prepend = 'from __future__ import absolute_import; from spack.pkgkit import *'
|
|
||||||
|
|
||||||
|
|
||||||
def packages_path():
|
def packages_path():
|
||||||
"""Get the test repo if it is active, otherwise the builtin repo."""
|
"""Get the test repo if it is active, otherwise the builtin repo."""
|
||||||
@ -596,7 +831,7 @@ def get_repo(self, namespace, default=NOT_PROVIDED):
|
|||||||
If default is provided, return it when the namespace
|
If default is provided, return it when the namespace
|
||||||
isn't found. If not, raise an UnknownNamespaceError.
|
isn't found. If not, raise an UnknownNamespaceError.
|
||||||
"""
|
"""
|
||||||
full_namespace = get_full_namespace(namespace)
|
full_namespace = python_package_for_repo(namespace)
|
||||||
if full_namespace not in self.by_namespace:
|
if full_namespace not in self.by_namespace:
|
||||||
if default == NOT_PROVIDED:
|
if default == NOT_PROVIDED:
|
||||||
raise UnknownNamespaceError(namespace)
|
raise UnknownNamespaceError(namespace)
|
||||||
@ -674,48 +909,6 @@ def providers_for(self, vpkg_spec):
|
|||||||
def extensions_for(self, extendee_spec):
|
def extensions_for(self, extendee_spec):
|
||||||
return [p for p in self.all_packages() if p.extends(extendee_spec)]
|
return [p for p in self.all_packages() if p.extends(extendee_spec)]
|
||||||
|
|
||||||
def find_module(self, fullname, path=None):
|
|
||||||
"""Implements precedence for overlaid namespaces.
|
|
||||||
|
|
||||||
Loop checks each namespace in self.repos for packages, and
|
|
||||||
also handles loading empty containing namespaces.
|
|
||||||
|
|
||||||
"""
|
|
||||||
# namespaces are added to repo, and package modules are leaves.
|
|
||||||
namespace, dot, module_name = fullname.rpartition('.')
|
|
||||||
|
|
||||||
# If it's a module in some repo, or if it is the repo's
|
|
||||||
# namespace, let the repo handle it.
|
|
||||||
for repo in self.repos:
|
|
||||||
if namespace == repo.full_namespace:
|
|
||||||
if repo.real_name(module_name):
|
|
||||||
return repo
|
|
||||||
elif fullname == repo.full_namespace:
|
|
||||||
return repo
|
|
||||||
|
|
||||||
# No repo provides the namespace, but it is a valid prefix of
|
|
||||||
# something in the RepoPath.
|
|
||||||
if self.by_namespace.is_prefix(fullname):
|
|
||||||
return self
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
def load_module(self, fullname):
|
|
||||||
"""Handles loading container namespaces when necessary.
|
|
||||||
|
|
||||||
See ``Repo`` for how actual package modules are loaded.
|
|
||||||
"""
|
|
||||||
if fullname in sys.modules:
|
|
||||||
return sys.modules[fullname]
|
|
||||||
|
|
||||||
if not self.by_namespace.is_prefix(fullname):
|
|
||||||
raise ImportError("No such Spack repo: %s" % fullname)
|
|
||||||
|
|
||||||
module = SpackNamespace(fullname)
|
|
||||||
module.__loader__ = self
|
|
||||||
sys.modules[fullname] = module
|
|
||||||
return module
|
|
||||||
|
|
||||||
def last_mtime(self):
|
def last_mtime(self):
|
||||||
"""Time a package file in this repo was last updated."""
|
"""Time a package file in this repo was last updated."""
|
||||||
return max(repo.last_mtime() for repo in self.repos)
|
return max(repo.last_mtime() for repo in self.repos)
|
||||||
@ -735,7 +928,7 @@ def repo_for_pkg(self, spec):
|
|||||||
# If the spec already has a namespace, then return the
|
# If the spec already has a namespace, then return the
|
||||||
# corresponding repo if we know about it.
|
# corresponding repo if we know about it.
|
||||||
if namespace:
|
if namespace:
|
||||||
fullspace = get_full_namespace(namespace)
|
fullspace = python_package_for_repo(namespace)
|
||||||
if fullspace not in self.by_namespace:
|
if fullspace not in self.by_namespace:
|
||||||
raise UnknownNamespaceError(namespace)
|
raise UnknownNamespaceError(namespace)
|
||||||
return self.by_namespace[fullspace]
|
return self.by_namespace[fullspace]
|
||||||
@ -849,7 +1042,7 @@ def check(condition, msg):
|
|||||||
"Namespaces must be valid python identifiers separated by '.'")
|
"Namespaces must be valid python identifiers separated by '.'")
|
||||||
|
|
||||||
# Set up 'full_namespace' to include the super-namespace
|
# Set up 'full_namespace' to include the super-namespace
|
||||||
self.full_namespace = get_full_namespace(self.namespace)
|
self.full_namespace = python_package_for_repo(self.namespace)
|
||||||
|
|
||||||
# Keep name components around for checking prefixes.
|
# Keep name components around for checking prefixes.
|
||||||
self._names = self.full_namespace.split('.')
|
self._names = self.full_namespace.split('.')
|
||||||
@ -865,40 +1058,6 @@ def check(condition, msg):
|
|||||||
# Indexes for this repository, computed lazily
|
# Indexes for this repository, computed lazily
|
||||||
self._repo_index = None
|
self._repo_index = None
|
||||||
|
|
||||||
# make sure the namespace for packages in this repo exists.
|
|
||||||
self._create_namespace()
|
|
||||||
|
|
||||||
def _create_namespace(self):
|
|
||||||
"""Create this repo's namespace module and insert it into sys.modules.
|
|
||||||
|
|
||||||
Ensures that modules loaded via the repo have a home, and that
|
|
||||||
we don't get runtime warnings from Python's module system.
|
|
||||||
|
|
||||||
"""
|
|
||||||
parent = None
|
|
||||||
for i in range(1, len(self._names) + 1):
|
|
||||||
ns = '.'.join(self._names[:i])
|
|
||||||
|
|
||||||
if ns not in sys.modules:
|
|
||||||
module = SpackNamespace(ns)
|
|
||||||
module.__loader__ = self
|
|
||||||
sys.modules[ns] = module
|
|
||||||
|
|
||||||
# Ensure the namespace is an atrribute of its parent,
|
|
||||||
# if it has not been set by something else already.
|
|
||||||
#
|
|
||||||
# This ensures that we can do things like:
|
|
||||||
# import spack.pkg.builtin.mpich as mpich
|
|
||||||
if parent:
|
|
||||||
modname = self._names[i - 1]
|
|
||||||
setattr(parent, modname, module)
|
|
||||||
else:
|
|
||||||
# no need to set up a module
|
|
||||||
module = sys.modules[ns]
|
|
||||||
|
|
||||||
# but keep track of the parent in this loop
|
|
||||||
parent = module
|
|
||||||
|
|
||||||
def real_name(self, import_name):
|
def real_name(self, import_name):
|
||||||
"""Allow users to import Spack packages using Python identifiers.
|
"""Allow users to import Spack packages using Python identifiers.
|
||||||
|
|
||||||
@ -929,52 +1088,6 @@ def is_prefix(self, fullname):
|
|||||||
parts = fullname.split('.')
|
parts = fullname.split('.')
|
||||||
return self._names[:len(parts)] == parts
|
return self._names[:len(parts)] == parts
|
||||||
|
|
||||||
def find_module(self, fullname, path=None):
|
|
||||||
"""Python find_module import hook.
|
|
||||||
|
|
||||||
Returns this Repo if it can load the module; None if not.
|
|
||||||
"""
|
|
||||||
if self.is_prefix(fullname):
|
|
||||||
return self
|
|
||||||
|
|
||||||
namespace, dot, module_name = fullname.rpartition('.')
|
|
||||||
if namespace == self.full_namespace:
|
|
||||||
if self.real_name(module_name):
|
|
||||||
return self
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
def load_module(self, fullname):
|
|
||||||
"""Python importer load hook.
|
|
||||||
|
|
||||||
Tries to load the module; raises an ImportError if it can't.
|
|
||||||
"""
|
|
||||||
if fullname in sys.modules:
|
|
||||||
return sys.modules[fullname]
|
|
||||||
|
|
||||||
namespace, dot, module_name = fullname.rpartition('.')
|
|
||||||
|
|
||||||
if self.is_prefix(fullname):
|
|
||||||
module = SpackNamespace(fullname)
|
|
||||||
|
|
||||||
elif namespace == self.full_namespace:
|
|
||||||
real_name = self.real_name(module_name)
|
|
||||||
if not real_name:
|
|
||||||
raise ImportError("No module %s in %s" % (module_name, self))
|
|
||||||
module = self._get_pkg_module(real_name)
|
|
||||||
|
|
||||||
else:
|
|
||||||
raise ImportError("No module %s in %s" % (fullname, self))
|
|
||||||
|
|
||||||
module.__loader__ = self
|
|
||||||
sys.modules[fullname] = module
|
|
||||||
if namespace != fullname:
|
|
||||||
parent = sys.modules[namespace]
|
|
||||||
if not hasattr(parent, module_name):
|
|
||||||
setattr(parent, module_name, module)
|
|
||||||
|
|
||||||
return module
|
|
||||||
|
|
||||||
def _read_config(self):
|
def _read_config(self):
|
||||||
"""Check for a YAML config file in this db's root directory."""
|
"""Check for a YAML config file in this db's root directory."""
|
||||||
try:
|
try:
|
||||||
@ -1164,46 +1277,6 @@ def is_virtual(self, pkg_name):
|
|||||||
"""True if the package with this name is virtual, False otherwise."""
|
"""True if the package with this name is virtual, False otherwise."""
|
||||||
return pkg_name in self.provider_index
|
return pkg_name in self.provider_index
|
||||||
|
|
||||||
def _get_pkg_module(self, pkg_name):
|
|
||||||
"""Create a module for a particular package.
|
|
||||||
|
|
||||||
This caches the module within this Repo *instance*. It does
|
|
||||||
*not* add it to ``sys.modules``. So, you can construct
|
|
||||||
multiple Repos for testing and ensure that the module will be
|
|
||||||
loaded once per repo.
|
|
||||||
|
|
||||||
"""
|
|
||||||
if pkg_name not in self._modules:
|
|
||||||
file_path = self.filename_for_package_name(pkg_name)
|
|
||||||
|
|
||||||
if not os.path.exists(file_path):
|
|
||||||
raise UnknownPackageError(pkg_name, self)
|
|
||||||
|
|
||||||
if not os.path.isfile(file_path):
|
|
||||||
tty.die("Something's wrong. '%s' is not a file!" % file_path)
|
|
||||||
|
|
||||||
if not os.access(file_path, os.R_OK):
|
|
||||||
tty.die("Cannot read '%s'!" % file_path)
|
|
||||||
|
|
||||||
# e.g., spack.pkg.builtin.mpich
|
|
||||||
fullname = "%s.%s" % (self.full_namespace, pkg_name)
|
|
||||||
|
|
||||||
try:
|
|
||||||
module = simp.load_source(fullname, file_path,
|
|
||||||
prepend=_package_prepend)
|
|
||||||
except SyntaxError as e:
|
|
||||||
# SyntaxError strips the path from the filename so we need to
|
|
||||||
# manually construct the error message in order to give the
|
|
||||||
# user the correct package.py where the syntax error is located
|
|
||||||
raise SyntaxError('invalid syntax in {0:}, line {1:}'
|
|
||||||
.format(file_path, e.lineno))
|
|
||||||
|
|
||||||
module.__package__ = self.full_namespace
|
|
||||||
module.__loader__ = self
|
|
||||||
self._modules[pkg_name] = module
|
|
||||||
|
|
||||||
return self._modules[pkg_name]
|
|
||||||
|
|
||||||
def get_pkg_class(self, pkg_name):
|
def get_pkg_class(self, pkg_name):
|
||||||
"""Get the class for the package out of its module.
|
"""Get the class for the package out of its module.
|
||||||
|
|
||||||
@ -1308,25 +1381,20 @@ def create_or_construct(path, namespace=None):
|
|||||||
|
|
||||||
|
|
||||||
def _path(repo_dirs=None):
|
def _path(repo_dirs=None):
|
||||||
"""Get the singleton RepoPath instance for Spack.
|
"""Get the singleton RepoPath instance for Spack."""
|
||||||
|
|
||||||
Create a RepoPath, add it to sys.meta_path, and return it.
|
|
||||||
|
|
||||||
TODO: consider not making this a singleton.
|
|
||||||
"""
|
|
||||||
repo_dirs = repo_dirs or spack.config.get('repos')
|
repo_dirs = repo_dirs or spack.config.get('repos')
|
||||||
if not repo_dirs:
|
if not repo_dirs:
|
||||||
raise NoRepoConfiguredError(
|
raise NoRepoConfiguredError(
|
||||||
"Spack configuration contains no package repositories.")
|
"Spack configuration contains no package repositories.")
|
||||||
|
return RepoPath(*repo_dirs)
|
||||||
path = RepoPath(*repo_dirs)
|
|
||||||
sys.meta_path.append(path)
|
|
||||||
return path
|
|
||||||
|
|
||||||
|
|
||||||
#: Singleton repo path instance
|
#: Singleton repo path instance
|
||||||
path = llnl.util.lang.Singleton(_path)
|
path = llnl.util.lang.Singleton(_path)
|
||||||
|
|
||||||
|
# Add the finder to sys.meta_path
|
||||||
|
sys.meta_path.append(ReposFinder())
|
||||||
|
|
||||||
|
|
||||||
def get(spec):
|
def get(spec):
|
||||||
"""Convenience wrapper around ``spack.repo.get()``."""
|
"""Convenience wrapper around ``spack.repo.get()``."""
|
||||||
@ -1338,22 +1406,6 @@ def all_package_names(include_virtuals=False):
|
|||||||
return path.all_package_names(include_virtuals)
|
return path.all_package_names(include_virtuals)
|
||||||
|
|
||||||
|
|
||||||
def set_path(repo):
|
|
||||||
"""Set the path singleton to a specific value.
|
|
||||||
|
|
||||||
Overwrite ``path`` and register it as an importer in
|
|
||||||
``sys.meta_path`` if it is a ``Repo`` or ``RepoPath``.
|
|
||||||
"""
|
|
||||||
global path
|
|
||||||
path = repo
|
|
||||||
|
|
||||||
# make the new repo_path an importer if needed
|
|
||||||
append = isinstance(repo, (Repo, RepoPath))
|
|
||||||
if append:
|
|
||||||
sys.meta_path.append(repo)
|
|
||||||
return append
|
|
||||||
|
|
||||||
|
|
||||||
@contextlib.contextmanager
|
@contextlib.contextmanager
|
||||||
def additional_repository(repository):
|
def additional_repository(repository):
|
||||||
"""Adds temporarily a repository to the default one.
|
"""Adds temporarily a repository to the default one.
|
||||||
@ -1378,24 +1430,10 @@ def use_repositories(*paths_and_repos):
|
|||||||
Corresponding RepoPath object
|
Corresponding RepoPath object
|
||||||
"""
|
"""
|
||||||
global path
|
global path
|
||||||
|
path, saved = RepoPath(*paths_and_repos), path
|
||||||
remove_from_meta = None
|
|
||||||
|
|
||||||
# Construct a temporary RepoPath object from
|
|
||||||
temporary_repositories = RepoPath(*paths_and_repos)
|
|
||||||
|
|
||||||
# Swap the current repository out
|
|
||||||
saved = path
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
remove_from_meta = set_path(temporary_repositories)
|
yield path
|
||||||
|
|
||||||
yield temporary_repositories
|
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
# Restore _path and sys.meta_path
|
|
||||||
if remove_from_meta:
|
|
||||||
sys.meta_path.remove(temporary_repositories)
|
|
||||||
path = saved
|
path = saved
|
||||||
|
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
import spack.package
|
||||||
import spack.paths
|
import spack.paths
|
||||||
import spack.repo
|
import spack.repo
|
||||||
|
|
||||||
@ -98,3 +99,25 @@ def test_use_repositories_doesnt_change_class():
|
|||||||
with spack.repo.use_repositories(*current_paths):
|
with spack.repo.use_repositories(*current_paths):
|
||||||
zlib_cls_inner = spack.repo.path.get_pkg_class('zlib')
|
zlib_cls_inner = spack.repo.path.get_pkg_class('zlib')
|
||||||
assert id(zlib_cls_inner) == id(zlib_cls_outer)
|
assert id(zlib_cls_inner) == id(zlib_cls_outer)
|
||||||
|
|
||||||
|
|
||||||
|
def test_import_repo_prefixes_as_python_modules(mock_packages):
|
||||||
|
import spack.pkg.builtin.mock
|
||||||
|
assert isinstance(spack.pkg, spack.repo.SpackNamespace)
|
||||||
|
assert isinstance(spack.pkg.builtin, spack.repo.SpackNamespace)
|
||||||
|
assert isinstance(spack.pkg.builtin.mock, spack.repo.SpackNamespace)
|
||||||
|
|
||||||
|
|
||||||
|
def test_absolute_import_spack_packages_as_python_modules(mock_packages):
|
||||||
|
import spack.pkg.builtin.mock.mpileaks
|
||||||
|
assert hasattr(spack.pkg.builtin.mock, 'mpileaks')
|
||||||
|
assert hasattr(spack.pkg.builtin.mock.mpileaks, 'Mpileaks')
|
||||||
|
assert isinstance(spack.pkg.builtin.mock.mpileaks.Mpileaks,
|
||||||
|
spack.package.PackageMeta)
|
||||||
|
assert issubclass(spack.pkg.builtin.mock.mpileaks.Mpileaks, spack.package.Package)
|
||||||
|
|
||||||
|
|
||||||
|
def test_relative_import_spack_packages_as_python_modules(mock_packages):
|
||||||
|
from spack.pkg.builtin.mock.mpileaks import Mpileaks
|
||||||
|
assert isinstance(Mpileaks, spack.package.PackageMeta)
|
||||||
|
assert issubclass(Mpileaks, spack.package.Package)
|
||||||
|
@ -1,22 +0,0 @@
|
|||||||
# Copyright 2013-2022 Lawrence Livermore National Security, LLC and other
|
|
||||||
# Spack Project Developers. See the top-level COPYRIGHT file for details.
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
|
|
||||||
|
|
||||||
"""Consolidated module for all imports done by Spack.
|
|
||||||
|
|
||||||
Many parts of Spack have to import Python code. This utility package
|
|
||||||
wraps Spack's interface with Python's import system.
|
|
||||||
|
|
||||||
We do this because Python's import system is confusing and changes from
|
|
||||||
Python version to Python version, and we should be able to adapt our
|
|
||||||
approach to the underlying implementation.
|
|
||||||
|
|
||||||
Currently, this uses ``importlib.machinery`` where available and ``imp``
|
|
||||||
when ``importlib`` is not completely usable.
|
|
||||||
"""
|
|
||||||
|
|
||||||
try:
|
|
||||||
from .importlib_importer import load_source # noqa
|
|
||||||
except ImportError:
|
|
||||||
from .imp_importer import load_source # noqa
|
|
@ -1,67 +0,0 @@
|
|||||||
# Copyright 2013-2022 Lawrence Livermore National Security, LLC and other
|
|
||||||
# Spack Project Developers. See the top-level COPYRIGHT file for details.
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
|
|
||||||
|
|
||||||
"""Implementation of Spack imports that uses imp underneath.
|
|
||||||
|
|
||||||
``imp`` is deprecated in newer versions of Python, but is the only option
|
|
||||||
in Python 2.6.
|
|
||||||
"""
|
|
||||||
import imp
|
|
||||||
import tempfile
|
|
||||||
from contextlib import contextmanager
|
|
||||||
|
|
||||||
|
|
||||||
@contextmanager
|
|
||||||
def import_lock():
|
|
||||||
imp.acquire_lock()
|
|
||||||
yield
|
|
||||||
imp.release_lock()
|
|
||||||
|
|
||||||
|
|
||||||
def load_source(full_name, path, prepend=None):
|
|
||||||
"""Import a Python module from source.
|
|
||||||
|
|
||||||
Load the source file and add it to ``sys.modules``.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
full_name (str): full name of the module to be loaded
|
|
||||||
path (str): path to the file that should be loaded
|
|
||||||
prepend (str or None): some optional code to prepend to the
|
|
||||||
loaded module; e.g., can be used to inject import statements
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
the loaded module
|
|
||||||
"""
|
|
||||||
with import_lock():
|
|
||||||
if prepend is None:
|
|
||||||
return imp.load_source(full_name, path)
|
|
||||||
else:
|
|
||||||
with prepend_open(path, text=prepend) as f:
|
|
||||||
return imp.load_source(full_name, path, f)
|
|
||||||
|
|
||||||
|
|
||||||
@contextmanager
|
|
||||||
def prepend_open(f, *args, **kwargs):
|
|
||||||
"""Open a file for reading, but prepend with some text prepended
|
|
||||||
|
|
||||||
Arguments are same as for ``open()``, with one keyword argument,
|
|
||||||
``text``, specifying the text to prepend.
|
|
||||||
|
|
||||||
We have to write and read a tempfile for the ``imp``-based importer,
|
|
||||||
as the ``file`` argument to ``imp.load_source()`` requires a
|
|
||||||
low-level file handle.
|
|
||||||
|
|
||||||
See the ``importlib``-based importer for a faster way to do this in
|
|
||||||
later versions of python.
|
|
||||||
"""
|
|
||||||
text = kwargs.get('text', None)
|
|
||||||
|
|
||||||
with open(f, *args) as f:
|
|
||||||
with tempfile.NamedTemporaryFile(mode='w+') as tf:
|
|
||||||
if text:
|
|
||||||
tf.write(text + '\n')
|
|
||||||
tf.write(f.read())
|
|
||||||
tf.seek(0)
|
|
||||||
yield tf.file
|
|
@ -1,48 +0,0 @@
|
|||||||
# Copyright 2013-2022 Lawrence Livermore National Security, LLC and other
|
|
||||||
# Spack Project Developers. See the top-level COPYRIGHT file for details.
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
|
|
||||||
|
|
||||||
"""Implementation of Spack imports that uses importlib underneath.
|
|
||||||
|
|
||||||
``importlib`` is only fully implemented in Python 3.
|
|
||||||
"""
|
|
||||||
from importlib.machinery import SourceFileLoader # novm
|
|
||||||
|
|
||||||
|
|
||||||
class PrependFileLoader(SourceFileLoader):
|
|
||||||
def __init__(self, full_name, path, prepend=None):
|
|
||||||
super(PrependFileLoader, self).__init__(full_name, path)
|
|
||||||
self.prepend = prepend
|
|
||||||
|
|
||||||
def path_stats(self, path):
|
|
||||||
stats = super(PrependFileLoader, self).path_stats(path)
|
|
||||||
if self.prepend:
|
|
||||||
stats["size"] += len(self.prepend) + 1
|
|
||||||
return stats
|
|
||||||
|
|
||||||
def get_data(self, path):
|
|
||||||
data = super(PrependFileLoader, self).get_data(path)
|
|
||||||
if path != self.path or self.prepend is None:
|
|
||||||
return data
|
|
||||||
else:
|
|
||||||
return self.prepend.encode() + b"\n" + data
|
|
||||||
|
|
||||||
|
|
||||||
def load_source(full_name, path, prepend=None):
|
|
||||||
"""Import a Python module from source.
|
|
||||||
|
|
||||||
Load the source file and add it to ``sys.modules``.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
full_name (str): full name of the module to be loaded
|
|
||||||
path (str): path to the file that should be loaded
|
|
||||||
prepend (str or None): some optional code to prepend to the
|
|
||||||
loaded module; e.g., can be used to inject import statements
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
the loaded module
|
|
||||||
"""
|
|
||||||
# use our custom loader
|
|
||||||
loader = PrependFileLoader(full_name, path, prepend)
|
|
||||||
return loader.load_module()
|
|
@ -40,6 +40,9 @@ bin/spack help -a
|
|||||||
spack -p --lines 20 spec mpileaks%gcc ^dyninst@10.0.0 ^elfutils@0.170
|
spack -p --lines 20 spec mpileaks%gcc ^dyninst@10.0.0 ^elfutils@0.170
|
||||||
$coverage_run $(which spack) bootstrap status --dev --optional
|
$coverage_run $(which spack) bootstrap status --dev --optional
|
||||||
|
|
||||||
|
# Check that we can import Spack packages directly as a first import
|
||||||
|
$coverage_run $(which spack) python -c "import spack.pkg.builtin.mpileaks; repr(spack.pkg.builtin.mpileaks.Mpileaks)"
|
||||||
|
|
||||||
#-----------------------------------------------------------
|
#-----------------------------------------------------------
|
||||||
# Run unit tests with code coverage
|
# Run unit tests with code coverage
|
||||||
#-----------------------------------------------------------
|
#-----------------------------------------------------------
|
||||||
|
Loading…
Reference in New Issue
Block a user