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.
#
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
"""This package contains modules with hooks for various stages in the
Spack install process. You can add modules here and they'll be
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
features.
"""
import os.path
import llnl.util.lang
import spack.paths
import spack.util.imp as simp
from llnl.util.lang import memoized, list_modules
@memoized
def all_hook_modules():
modules = []
for name in list_modules(spack.paths.hooks_path):
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):
class _HookRunner(object):
#: Stores all hooks on first call, shared among
#: all HookRunner objects
_hooks = None
def __init__(self, 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):
for module in all_hook_modules():
for _, module in self.hooks:
if hasattr(module, self.hook_name):
hook = getattr(module, self.hook_name)
if hasattr(hook, '__call__'):
hook(*args, **kwargs)
pre_install = HookRunner('pre_install')
post_install = HookRunner('post_install')
pre_install = _HookRunner('pre_install')
post_install = _HookRunner('post_install')
pre_uninstall = HookRunner('pre_uninstall')
post_uninstall = HookRunner('post_uninstall')
pre_uninstall = _HookRunner('pre_uninstall')
post_uninstall = _HookRunner('post_uninstall')