Import hooks using Python's built-in machinery (#23288)

The function we coded in Spack to load Python modules with arbitrary
names from a file seem to have issues with local imports. For
loading hooks though it is unnecessary to use such functions, since
we don't care to bind a custom name to a module nor we have to load
it from an unknown location.

This PR thus modifies spack.hook in the following ways:

- Use __import__ instead of spack.util.imp.load_source (this
  addresses #20005)
- Sync module docstring with all the hooks we have
- Avoid using memoization in a module function
- Marked with a leading underscore all the names that are supposed
  to stay local
This commit is contained in:
Massimiliano Culpo 2021-04-28 01:55:07 +02:00 committed by Todd Gamblin
parent fb27c7ad0c
commit 13fed376f2

View File

@ -2,8 +2,8 @@
# Spack Project Developers. See the top-level COPYRIGHT file for details. # Spack Project Developers. See the top-level COPYRIGHT file for details.
# #
# SPDX-License-Identifier: (Apache-2.0 OR MIT) # SPDX-License-Identifier: (Apache-2.0 OR MIT)
"""This package contains modules with hooks for various stages in the """This package contains modules with hooks for various stages in the
Spack install process. You can add modules here and they'll be Spack install process. You can add modules here and they'll be
executed by package at various times during the package lifecycle. executed by package at various times during the package lifecycle.
@ -21,46 +21,55 @@
systems (e.g. modules, lmod, etc.) or to add other custom systems (e.g. modules, lmod, etc.) or to add other custom
features. features.
""" """
import os.path import llnl.util.lang
import spack.paths import spack.paths
import spack.util.imp as simp
from llnl.util.lang import memoized, list_modules
@memoized class _HookRunner(object):
def all_hook_modules(): #: Stores all hooks on first call, shared among
modules = [] #: all HookRunner objects
for name in list_modules(spack.paths.hooks_path): _hooks = None
mod_name = __name__ + '.' + name
path = os.path.join(spack.paths.hooks_path, name) + ".py"
mod = simp.load_source(mod_name, path)
if name == 'write_install_manifest':
last_mod = mod
else:
modules.append(mod)
# put `write_install_manifest` as the last hook to run
modules.append(last_mod)
return modules
class HookRunner(object):
def __init__(self, hook_name): def __init__(self, hook_name):
self.hook_name = hook_name self.hook_name = hook_name
@classmethod
def _populate_hooks(cls):
# Lazily populate the list of hooks
cls._hooks = []
relative_names = list(llnl.util.lang.list_modules(
spack.paths.hooks_path
))
# We want this hook to be the last registered
relative_names.sort(key=lambda x: x == 'write_install_manifest')
assert relative_names[-1] == 'write_install_manifest'
for name in relative_names:
module_name = __name__ + '.' + name
# When importing a module from a package, __import__('A.B', ...)
# returns package A when 'fromlist' is empty. If fromlist is not
# empty it returns the submodule B instead
# See: https://stackoverflow.com/a/2725668/771663
module_obj = __import__(module_name, fromlist=[None])
cls._hooks.append((module_name, module_obj))
@property
def hooks(self):
if not self._hooks:
self._populate_hooks()
return self._hooks
def __call__(self, *args, **kwargs): def __call__(self, *args, **kwargs):
for module in all_hook_modules(): for _, module in self.hooks:
if hasattr(module, self.hook_name): if hasattr(module, self.hook_name):
hook = getattr(module, self.hook_name) hook = getattr(module, self.hook_name)
if hasattr(hook, '__call__'): if hasattr(hook, '__call__'):
hook(*args, **kwargs) hook(*args, **kwargs)
pre_install = HookRunner('pre_install') pre_install = _HookRunner('pre_install')
post_install = HookRunner('post_install') post_install = _HookRunner('post_install')
pre_uninstall = HookRunner('pre_uninstall') pre_uninstall = _HookRunner('pre_uninstall')
post_uninstall = HookRunner('post_uninstall') post_uninstall = _HookRunner('post_uninstall')