diff options
| author | Barry Warsaw | 2017-06-17 03:33:39 +0000 |
|---|---|---|
| committer | Barry Warsaw | 2017-06-17 03:33:39 +0000 |
| commit | 66559bacf1dbfa166c2d4c2596ea96c7909bb43c (patch) | |
| tree | 9bc9cef4a696639654185709c1143924b1d41f43 /src/mailman/utilities/modules.py | |
| parent | 4fdad6f9c8a9487a6f2c8462be734cd97aaf4d94 (diff) | |
| parent | 88f40ac0add14cc9e7c106c5e2e9ec3d6f73df6e (diff) | |
| download | mailman-66559bacf1dbfa166c2d4c2596ea96c7909bb43c.tar.gz mailman-66559bacf1dbfa166c2d4c2596ea96c7909bb43c.tar.zst mailman-66559bacf1dbfa166c2d4c2596ea96c7909bb43c.zip | |
Diffstat (limited to 'src/mailman/utilities/modules.py')
| -rw-r--r-- | src/mailman/utilities/modules.py | 75 |
1 files changed, 65 insertions, 10 deletions
diff --git a/src/mailman/utilities/modules.py b/src/mailman/utilities/modules.py index 7aa2ac009..83a4384b7 100644 --- a/src/mailman/utilities/modules.py +++ b/src/mailman/utilities/modules.py @@ -25,6 +25,19 @@ from public import public @public +def abstract_component(cls): + """Decorator preventing `find_components()` from instantiating the class. + + Normally, `find_components()` instantiates any component class that + it finds matching the given interface. Some component classes must not be + instantiated though, because they act as base classes. Put this decorator + on the class definition to prevent instantiation. + """ + cls.__abstract_component__ = True + return cls + + +@public def find_name(dotted_name): """Import and return the named object in package space. @@ -55,24 +68,36 @@ def call_name(dotted_name, *args, **kws): return named_callable(*args, **kws) -@public def scan_module(module, interface): - """Return all the items in a module that conform to an interface. + """Return all the object in a module that conform to an interface. + + Scan every item named in the module's `__all__`. If that item conforms to + the given interface, *and* the item is not declared as an + `@abstract_component`, then instantiate the item and return the resulting + instance. - :param module: A module object. The module's `__all__` will be scanned. + :param module: A module object. :type module: module :param interface: The interface that returned objects must conform to. :type interface: `Interface` - :return: The sequence of matching components. - :rtype: objects implementing `interface` + :return: The sequence of instantiated matching components. + :rtype: instantiated objects implementing `interface` """ missing = object() for name in module.__all__: component = getattr(module, name, missing) assert component is not missing, ( '%s has bad __all__: %s' % (module, name)) # pragma: no cover - if interface.implementedBy(component): - yield component + if (interface.implementedBy(component) + # We cannot use getattr() here because that will return True + # for all subclasses. __abstract_component__ should *not* be + # inherited, meaning subclasses must declare themselves to be + # abstract if they also don't want to be instantiated. Only + # by looking at the component's __dict__ can we know for sure + # where the marker has been placed. The value of + # __abstract_component__ doesn't matter, only its presence. + and '__abstract_component__' not in component.__dict__): + yield component() @public @@ -80,14 +105,15 @@ def find_components(package, interface): """Find components which conform to a given interface. Search all the modules in a given package, returning an iterator over all - objects found that conform to the given interface. + objects found that conform to the given interface, unless that object is + decorated with `@abstract_component`. :param package: The package path to search. :type package: string :param interface: The interface that returned objects must conform to. :type interface: `Interface` - :return: The sequence of matching components. - :rtype: objects implementing `interface` + :return: The sequence of instantiated matching components. + :rtype: instantiated objects implementing `interface` """ for filename in resource_listdir(package, ''): basename, extension = os.path.splitext(filename) @@ -102,6 +128,35 @@ def find_components(package, interface): @public +def add_components(package, 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. + + :param package: The package path to search. + :type package: string + :param interface: The interface that returned objects must conform to. + Objects found must have a `.name` attribute containing a unique + string. + :type interface: `Interface` + :param mapping: The mapping to add the found components to. + :type mapping: A dict-like mapping. This only needs to support + 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): + if component.name in mapping: + raise RuntimeError( + '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? |
