summaryrefslogtreecommitdiff
path: root/src/mailman/utilities/modules.py
diff options
context:
space:
mode:
authorBarry Warsaw2017-08-29 14:07:54 +0000
committerBarry Warsaw2017-08-29 14:07:54 +0000
commitae0042a90220119414f61aeb20c6b58bfacb8af2 (patch)
tree6fd2038427fbb36d8173fe338d277351cd19727b /src/mailman/utilities/modules.py
parentf847e15407bfbf824236547bdf728a1ae00bd405 (diff)
downloadmailman-ae0042a90220119414f61aeb20c6b58bfacb8af2.tar.gz
mailman-ae0042a90220119414f61aeb20c6b58bfacb8af2.tar.zst
mailman-ae0042a90220119414f61aeb20c6b58bfacb8af2.zip
Diffstat (limited to 'src/mailman/utilities/modules.py')
-rw-r--r--src/mailman/utilities/modules.py117
1 files changed, 77 insertions, 40 deletions
diff --git a/src/mailman/utilities/modules.py b/src/mailman/utilities/modules.py
index 155a34ed2..48cee1a0e 100644
--- a/src/mailman/utilities/modules.py
+++ b/src/mailman/utilities/modules.py
@@ -22,7 +22,7 @@ import sys
from contextlib import contextmanager
from importlib import import_module
-from pkg_resources import resource_filename, resource_listdir
+from pkg_resources import resource_filename, resource_isdir, resource_listdir
from public import public
@@ -70,6 +70,32 @@ def call_name(dotted_name, *args, **kws):
return named_callable(*args, **kws)
+@public
+def expand_path(url):
+ """Expand a python: path, returning the absolute file system path."""
+ # Is the context coming from a file system or Python path?
+ if url.startswith('python:'):
+ resource_path = url[7:]
+ package, dot, resource = resource_path.rpartition('.')
+ return resource_filename(package, resource + '.cfg')
+ else:
+ return url
+
+
+@public
+@contextmanager
+def hacked_sys_modules(name, module):
+ old_module = sys.modules.get(name)
+ sys.modules[name] = module
+ try:
+ yield
+ finally:
+ if old_module is None:
+ del sys.modules[name]
+ else:
+ sys.modules[name] = old_module
+
+
def scan_module(module, interface):
"""Return all the object in a module that conform to an interface.
@@ -102,7 +128,6 @@ def scan_module(module, interface):
yield component()
-@public
def find_components(package, interface):
"""Find components which conform to a given interface.
@@ -122,25 +147,63 @@ def find_components(package, interface):
if extension != '.py' or basename.startswith('.'):
continue
module_name = '{}.{}'.format(package, basename)
- __import__(module_name, fromlist='*')
- module = sys.modules[module_name]
+ module = import_module(module_name)
if not hasattr(module, '__all__'):
continue
yield from scan_module(module, interface)
+def find_pluggable_components(subpackage, interface):
+ """Find components which conform to a given interface.
+
+ This finds components which can be implemented in a plugin. It will
+ search for the interface in the named subpackage, where the Python import
+ path of the subpackage will be prepended by `mailman` for system
+ components, and the various plugin names for any external components.
+
+ :param subpackage: The subpackage to search. This is prepended by
+ 'mailman' to search for system components, and each enabled plugin for
+ external components.
+ :type subpackage: str
+ :param interface: The interface that returned objects must conform to.
+ :type interface: `Interface`
+ :return: The sequence of matching components.
+ :rtype: Objects implementing `interface`
+ """
+ # This can't be imported at module level because of circular imports.
+ from mailman.config import config
+ # Return the system components first.
+ yield from find_components('mailman.' + subpackage, interface)
+ # Return all the matching components in all the subpackages of all enabled
+ # plugins. Only enabled and existing plugins will appear in this
+ # dictionary.
+ for name, plugin_config in config.plugin_configs:
+ # If the plugin's configuration defines a components package, use
+ # that, falling back to the plugin's name.
+ package = plugin_config['component_package'].strip()
+ if len(package) == 0:
+ package = name
+ # It's possible that the plugin doesn't include the directory for this
+ # subpackage. That's fine.
+ if resource_isdir(package, subpackage):
+ plugin_package = '{}.{}'.format(package, subpackage)
+ yield from find_components(plugin_package, interface)
+
+
@public
-def add_components(package, interface, mapping):
+def add_components(subpackage, interface, mapping):
"""Add components to a given mapping.
- Similarly to `find_components()` this inspects all modules in a given
- package looking for objects that conform to a given interface. All such
- found objects (unless decorated with `@abstract_component`) are added to
- the given mapping, keyed by the object's `.name` attribute, which is
- required. It is a fatal error if that key already exists in the mapping.
+ Similarly to `find_pluggable_components()` this inspects all modules
+ in the given subpackage, relative to the 'mailman' parent package,
+ and all the plugin names, that match the given interface. All such
+ found objects (unless decorated with `@abstract_component`) are
+ added to the given mapping, keyed by the object's `.name` attribute,
+ which is required. It is a fatal error if that key already exists
+ in the mapping.
- :param package: The package path to search.
- :type package: string
+ :param subpackage: The subpackage path to search.
+ :type subpackage: str
:param interface: The interface that returned objects must conform to.
Objects found must have a `.name` attribute containing a unique
string.
@@ -150,35 +213,9 @@ def add_components(package, interface, mapping):
containment tests (e.g. `in` and `not in`) and `__setitem__()`.
:raises RuntimeError: when a duplicate key is found.
"""
- for component in find_components(package, interface):
+ for component in find_pluggable_components(subpackage, interface):
if component.name in mapping:
- raise RuntimeError(
+ raise RuntimeError( # pragma: nocover
'Duplicate key "{}" found in {}; previously {}'.format(
component.name, component, mapping[component.name]))
mapping[component.name] = component
-
-
-@public
-def expand_path(url):
- """Expand a python: path, returning the absolute file system path."""
- # Is the context coming from a file system or Python path?
- if url.startswith('python:'):
- resource_path = url[7:]
- package, dot, resource = resource_path.rpartition('.')
- return resource_filename(package, resource + '.cfg')
- else:
- return url
-
-
-@public
-@contextmanager
-def hacked_sys_modules(name, module):
- old_module = sys.modules.get(name)
- sys.modules[name] = module
- try:
- yield
- finally:
- if old_module is None:
- del sys.modules[name]
- else:
- sys.modules[name] = old_module