Source code for rspub.util.plugg

#! /usr/bin/env python3
# -*- coding: utf-8 -*-
"""
:samp:`Py-module and -class inspector`


"""
import importlib
import inspect
import logging
import os, sys

#: :samp:`The absolute path to the directory that is the application home or root directory.`
#:
#: During run time. So the value shown in documentation is not a constant!
APPLICATION_HOME = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__))))

LOG = logging.getLogger(__name__)


[docs]class Inspector(object): """ :samp:`Find py-modules and -classes in directories.` This class loads modules during its inspection. What the behavior will be upon encountering an ImportError can be set by the constructor parameter `stop_on_error` (boolean). It will then either log the exception (default) or raise the exception. """
[docs] def __init__(self, stop_on_error=False): """ :samp:`Initialize an {Inspector}.` :param stop_on_error: **True** for stop on error, **False** otherwise """ self.stop_on_error = stop_on_error
@staticmethod
[docs] def list_py_files(*directories) -> str: """ :samp:`Generator of py filenames.` Walks the given directories one-by-one recursively and yields each py-file it encounters. A file is considered py-file when its filename ends with `.py`. Files `__init__.py` and `setup.py` are neglected. :param str directories: directories to search :return: yields absolute filenames of py-files """ # LOG.info("Application home is '%s'", APPLICATION_HOME) for di in directories: if di: abs_dir = os.path.join(APPLICATION_HOME, di) for root, _directories, _filenames in os.walk(abs_dir): for filename in _filenames: if filename.endswith(".py") and not (filename == "__init__.py" or filename == "setup.py"): py_file = os.path.join(root, filename) yield py_file
[docs] def load_modules(self, *directories): """ :samp:`Generator of modules.` Walks the given directories one-by-one recursively and yields each module it encounters. The encountered modules will be imported. What the behavior will be upon encountering an ImportError can be set by the constructor parameter `stop_on_error` (boolean). :param str directories: directories to search :return: yields imported modules """ for di in directories: if di: abs_dir = os.path.join(APPLICATION_HOME, di) plugin_home = APPLICATION_HOME if not abs_dir.startswith(APPLICATION_HOME): plugin_home = abs_dir sys.path.append(plugin_home) for py_file in self.list_py_files(abs_dir): names = py_file.rsplit(".", 1) # everything but the extension path = os.path.relpath(names[0], plugin_home).replace(os.sep, ".") try: module = importlib.import_module(path) yield module except ImportError as ex: if self.stop_on_error: raise ex else: LOG.exception(ex)
[docs] def list_classes(self, *directories): """ :samp:`Generator of classes.` Walks the given directories one-by-one recursively and yields each class it encounters. :param str directories: directories to search :return: yields encountered classes """ for module in self.load_modules(*directories): clsmembers = inspect.getmembers(module, inspect.isclass) for cls in clsmembers: if cls[1].__module__ == module.__name__: yield cls[1]
[docs] def list_classes_filtered(self, predicates=list(), *directories): """ :samp:`Generator of filtered classes.` Walks the given directories one-by-one recursively and yields encountered classes *if* they pass all the predicates given in `predicates`. :param list predicates: a list of one-argument predicates that filter classes :param str directories: directories to search :return: yields encountered classes that pass the predicates """ for cls in self.list_classes(*directories): passes = True if predicates: for f in predicates: if not f(cls): passes = False break if passes: yield cls
# # functions and closures for class filtering
[docs]def is_subclass_of(super): """ :samp:`Predicate for subclass detection` :: f(cls) = issubclass(cls, super) :param super: the superclass in the detection :return: lambda for class subclass detection """ return lambda cls: issubclass(cls, super)
[docs]def is_qnamed(qname): """ :samp:`Predicate for qualified class-name detection.` :: f(cls) = cls.qualified_name == qname :param qname: the qualified name in the detection :return: lambda for qualified class-name detection """ return lambda cls: qname == cls.__module__ + "." + cls.__name__
[docs]def is_named(name): """ :samp:`Predicate for loose class-name detection.` :: f(cls) = cls.name == name or cls.qualified_name == name :param name: the class-name or qualified class-name in the detection :return: lambda for loose class-name detection """ return lambda cls: name == cls.__name__ or name == cls.__module__ + "." + cls.__name__
[docs]def from_module(module_name): """ :samp:`Predicate for module-name detection.` :: f(cls) = cls.module_name == module_name :param module_name: the module-name in the detection :return: lambda for module-name detection """ return lambda cls: cls.__module__.startswith(module_name)
[docs]def has_function(function_name): """ :samp:`Predicate for class function detection.` :: f(cls) = cls.has_function_name(function_name) :param function_name: the function name in the detection :return: closure for function name detection """ def _has_function(cls): if isinstance(cls, type): clazz = cls else: clazz = cls.__class__ func_descs = inspect.getmembers(clazz, inspect.isfunction) for func_desc in func_descs: if func_desc[0] == function_name: return True return False return _has_function