diff options
| author | Barry Warsaw | 2017-08-29 14:07:54 +0000 |
|---|---|---|
| committer | Barry Warsaw | 2017-08-29 14:07:54 +0000 |
| commit | ae0042a90220119414f61aeb20c6b58bfacb8af2 (patch) | |
| tree | 6fd2038427fbb36d8173fe338d277351cd19727b /src/mailman/utilities/modules.py | |
| parent | f847e15407bfbf824236547bdf728a1ae00bd405 (diff) | |
| download | mailman-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.py | 117 |
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 |
