Source code for dffml.util.entrypoint

# SPDX-License-Identifier: MIT
# Copyright (c) 2019 Intel Corporation
"""
Loader subclasses know how to load classes under their entry point which conform
to their subclasses.
"""
import os
import sys
import pathlib
import importlib
import traceback
import pkg_resources
from typing import List, Dict, Union, Optional, Iterator, Any

from .log import LOGGER

LOGGER = LOGGER.getChild("entrypoint")


[docs]class EntrypointNotFound(Exception): pass # pragma: no cover
[docs]class MissingLabel(Exception): pass # pragma: no cover
[docs]def load( *args: str, relative: Optional[Union[str, pathlib.Path]] = None ) -> Iterator[Any]: """ Load objects given the entrypoint formatted path to the object. Roughly how the python stdlib docs say entrypoint loading works. """ # Push current directory into front of path so we can run things # relative to where we are in the shell if relative is not None: if relative == True: relative = os.getcwd() # str() in case of Path object sys.path.insert(0, str(relative)) try: for entry in args: modname, qualname_separator, qualname = entry.partition(":") obj = importlib.import_module(modname) for attr in qualname.split("."): obj = getattr(obj, attr) yield obj finally: if relative is not None: sys.path.pop(0)
[docs]def entrypoint(label): """ If a class if going to be registered with setuptools as an entrypoint it must have the label it will be registered under associated with it via this decorator. This decorator sets the ENTRY_POINT_ORIG_LABEL and ENTRY_POINT_LABEL class proprieties to the same value, label. Examples -------- >>> from dffml import entrypoint, Entrypoint >>> >>> @entrypoint('mylabel') ... class EntrypointSubclassClass(Entrypoint): pass In setup.py, EntrypointSubclassClass needs to have this decorator applied to it with label set to mylabel. .. code-block:: python entry_points={ 'dffml.entrypoint': [ 'mylabel = module.path.to:EntrypointSubclassClass', ] } """ def add_entry_point_label(cls): cls.ENTRY_POINT_ORIG_LABEL = label cls.ENTRY_POINT_LABEL = label return cls return add_entry_point_label
[docs]def base_entry_point(entrypoint, *args): """ Any class which subclasses from Entrypoint needs this decorator applied to it. The decorator sets the ENTRYPOINT and ENTRY_POINT_NAME properties on the class. This allows the load() classmethod to be called to load subclasses of the class being decorated. This is how the subclasses get loaded via the entry point system by calling BaseClass.load(). ENTRY_POINT_NAME corresponds to the command line argument and config file reference to the class. It comes from all arguments after the entrypoint argument (first argument) is a list which would turn into an command line argument if it were joined with hyphens. Examples -------- >>> from dffml import base_entry_point, Entrypoint >>> >>> @base_entry_point('dffml.entrypoint', 'entrypoint') ... class BaseEntrypointSubclassClass(Entrypoint): pass .. code-block:: python entry_points={ # dffml.entrypoint = ENTRYPOINT 'dffml.entrypoint': [ 'mylabel = module.path.to:EntrypointSubclassClass', ] } """ def add_entry_point_and_name(cls): cls.ENTRYPOINT = entrypoint cls.ENTRY_POINT_NAME = list(args) return cls return add_entry_point_and_name
[docs]class Entrypoint(object): """ Uses the pkg_resources.iter_entry_points on the ENTRYPOINT of the class """ ENTRYPOINT = "util.entrypoint" # Label is for configuration. Sometimes multiple of the same classes will be # loaded. They need to determine which config options are meant for which # class. Therefore a label is applied to each class after it is loaded. If # there is only one instance of any type of a certain entry point a label # need not be applied because that class will know any thing that applies to # configuration for its entry point belongs solely to it. ENTRY_POINT_LABEL = ""
[docs] @classmethod def load(cls, loading=None): """ Loads all installed loading and returns them as a list. Sources to be loaded should be registered to ENTRYPOINT via setuptools. """ try: # Loading from entrypoint if ":" is in name if loading is not None and ":" in loading: return next(load(loading, relative=True)) except: LOGGER.error("Failed to load %r for %r", loading, cls.ENTRYPOINT) raise # Load from registered entrypoints otherwise loaded_names = [] loading_classes = [] for i in pkg_resources.iter_entry_points(cls.ENTRYPOINT): loaded_names.append(i.name) if loading is not None and i.name != loading: continue try: loaded = i.load() except Exception as error: print( f"Error loading {cls.ENTRYPOINT}.{i.name}: {traceback.format_exc().strip()}", file=sys.stderr, flush=True, ) raise loaded.ENTRY_POINT_LABEL = i.name loading_classes.append(loaded) if loading is not None and i.name == loading: return loaded if loading is not None: raise EntrypointNotFound( f"{loading!r} was not found in: {loaded_names}" ) return loading_classes
[docs] @classmethod def load_multiple(cls, to_load: List[str]): """ Loads each class requested without instantiating it. """ return {name: cls.load(name) for name in to_load}
[docs] @classmethod def load_dict(cls, to_load: Dict[str, str]): """ Loads each class tagged with the key it should be accessed by without instantiating it. """ return {key: cls.load(name) for key, name in to_load.items()}
@classmethod def load_labeled(cls, label_and_loading): if "=" in label_and_loading: label, loading = label_and_loading.split("=", maxsplit=1) else: raise MissingLabel( "%r is missing a label. " "Correct syntax: label=%s" % (label_and_loading, label_and_loading) ) loaded = cls.load(loading) return type( loaded.__qualname__, (loaded,), {"ENTRY_POINT_LABEL": label} )