Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 16 additions & 34 deletions lib/ramble/ramble/repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import contextlib
import errno
import functools
import importlib
import importlib.machinery
import importlib.util
import inspect
Expand Down Expand Up @@ -425,21 +426,24 @@ def converter(self, spec_like, *args, **kwargs):
return converter


class ObjectNamespace(types.ModuleType):
class RambleNamespace(types.ModuleType):
"""Allow lazy loading of modules."""

def __init__(self, namespace):
super().__init__(namespace)
self.__file__ = "(ramble namespace)"
self.__path__ = []
self.__name__ = namespace
self.__application__ = namespace
self.__modules = {}

def __getattr__(self, name):
"""Getattr lazily loads modules if they're not already loaded."""
submodule = self.__application__ + "." + name
setattr(self, name, __import__(submodule))
submodule = f"{self.__name__}.{name}"
try:
setattr(self, name, importlib.import_module(submodule))
except ImportError:
msg = "'{0}' object has no attribute {1}"
raise AttributeError(msg.format(type(self), name)) from None
Comment thread
linsword13 marked this conversation as resolved.
return getattr(self, name)


Expand Down Expand Up @@ -881,7 +885,7 @@ def load_module(self, fullname):
if not self.by_namespace.is_prefix(fullname):
raise ImportError(f"No such ramble repo: {fullname}")

module = ObjectNamespace(fullname)
module = RambleNamespace(fullname)
module.__loader__ = self
sys.modules[fullname] = module
return module
Expand Down Expand Up @@ -1075,7 +1079,7 @@ def _create_namespace(self):
ns = ".".join(self._names[:i])

if ns not in sys.modules:
module = ObjectNamespace(ns)
module = RambleNamespace(ns)
module.__loader__ = self
sys.modules[ns] = module

Expand Down Expand Up @@ -1151,7 +1155,7 @@ def load_module(self, fullname):
namespace, _, module_name = fullname.rpartition(".")

if self.is_prefix(fullname):
module = ObjectNamespace(fullname)
module = RambleNamespace(fullname)

elif namespace == self.full_namespace:
real_name = self.real_name(module_name)
Expand Down Expand Up @@ -1498,28 +1502,6 @@ def create(configuration, object_type=default_type):
return RepoPath(*repo_dirs, object_type=object_type)


class RepositoryNamespace(types.ModuleType):
"""Allow lazy loading of modules."""

def __init__(self, namespace):
super().__init__(namespace)
self.__file__ = "(repository namespace)"
self.__path__ = []
self.__name__ = namespace
self.__package__ = namespace
self.__modules = {}

def __getattr__(self, name):
"""Getattr lazily loads modules if they're not already loaded."""
submodule = self.__package__ + "." + name
try:
setattr(self, name, __import__(submodule))
except ImportError:
msg = "'{0}' object has no attribute {1}"
raise AttributeError(msg.format(type(self), name)) from None
return getattr(self, name)


class RepoLoader(importlib.machinery.SourceFileLoader):
"""Loads a Python module associated with a object in specific repository"""

Expand All @@ -1536,9 +1518,9 @@ def is_package(self, fullname):
return True


class RepositoryNamespaceLoader:
class RambleNamespaceLoader:
def create_module(self, spec):
return RepositoryNamespace(spec.name)
return RambleNamespace(spec.name)

def exec_module(self, module):
module.__loader__ = self
Expand Down Expand Up @@ -1581,14 +1563,14 @@ def compute_loader(self, fullname):
if object_name:
return RepoLoader(fullname, repo, object_name)

# We are importing a full namespace like 'spack.pkg.builtin'
# We are importing a full namespace like 'ramble.app.builtin'
if fullname == repo.full_namespace:
return RepositoryNamespaceLoader()
return RambleNamespaceLoader()

# No repo provides the namespace, but it is a valid prefix of
# something in the RepoPath.
if paths[self.object_type].by_namespace.is_prefix(fullname):
return RepositoryNamespaceLoader()
return RambleNamespaceLoader()

return None

Expand Down
32 changes: 32 additions & 0 deletions lib/ramble/ramble/test/repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,3 +218,35 @@ def test_use_repositories_exception_cleanup(extra_repo):
raise RuntimeError("test error inside context")
assert ramble.repository.paths[ramble.repository.ObjectTypes.applications] is orig_path
assert sys.meta_path == orig_meta_path


def test_namespace_import_and_attribute_access(mutable_mock_apps_repo):
import ramble.app.builtin.mock.basic as mock_basic

assert hasattr(mock_basic, "Basic")

import ramble.app.builtin.mock as mock_ns

assert hasattr(mock_ns, "basic")
assert mock_ns.basic.Basic.name == "basic"

import ramble.app as app_ns

assert hasattr(app_ns.builtin.mock, "basic")
assert app_ns.builtin.mock.basic.Basic.name == "basic"


def test_namespace_nonexistent_attribute(mutable_mock_apps_repo):
import ramble.app.builtin.mock as mock_ns

assert not hasattr(mock_ns, "nonexistent_object")
assert getattr(mock_ns, "nonexistent_object", "default") == "default"
with pytest.raises(AttributeError):
_ = mock_ns.nonexistent_object

import ramble.app as app_ns

assert not hasattr(app_ns, "nonexistent_subnamespace")
assert getattr(app_ns, "nonexistent_subnamespace", None) is None
with pytest.raises(AttributeError):
_ = app_ns.nonexistent_subnamespace
Loading