import os import sys import inspect import logging import traceback from .coverage_file import CoverageFile from lighthouse.util.python import iteritems from lighthouse.exceptions import CoverageParsingError logger = logging.getLogger("Lighthouse.Reader") MODULES_DIRECTORY = os.path.join(os.path.dirname(os.path.realpath(__file__)), "parsers") class CoverageReader(object): """ Middleware to automatically parse and load different coverage file formats. This class will dynamically load and make use of coverage file parsers that subclass from the CoverageFile abstraction and live within the reader's 'parsers' folder. This should allow end-users to write parsers for custom coverage file format without having to modify any of Lighthouse's existing code (ideally) """ def __init__(self): self._installed_parsers = {} self._import_parsers() def open(self, filepath): """ Open and parse a coverage file from disk. Returns a CoverageFile on success, or raises CoverageParsingError on failure. """ coverage_file = None parse_failures = {} # attempt to parse the given coverage file with each available parser for name, parser in iteritems(self._installed_parsers): logger.debug("Attempting parse with '%s'" % name) # attempt to open/parse the coverage file with the given parser try: coverage_file = parser(filepath) break # log the exceptions for each parse failure except Exception as e: parse_failures[name] = traceback.format_exc() logger.debug("| Parse FAILED - " + str(e)) #logger.exception("| Parse FAILED") # # if *all* the coverage file parsers failed, raise an exception with # information for each failure (for debugging) # if not coverage_file: raise CoverageParsingError(filepath, parse_failures) # successful parse logger.debug("| Parse OKAY") return coverage_file def _import_parsers(self): """ Scan and import coverage file parsers from the 'parsers' directory. """ target_subclass = CoverageFile ignored_files = ["__init__.py"] # loop through all the files in the parsers folder for filename in os.listdir(MODULES_DIRECTORY): # ignore specified files, and anything not *.py if filename in ignored_files or filename.endswith(".py") == False: continue # attempt to load a CoverageFile format from the current *.py file logger.debug("| Searching file %s" % filename) parser_file = filename[:-3] parser_class = self._locate_subclass(parser_file, target_subclass) if not parser_class: logger.warning("| - No object subclassing from %s found in %s..." \ % (target_subclass.__name__, parser_file)) continue # instantiate and add the parser to our dict of imported parsers logger.debug("| | Found %s" % parser_class.__name__) self._installed_parsers[parser_class.__name__] = parser_class logger.debug("+- Done dynamically importing parsers") # return the number of modules successfully imported return self._installed_parsers def _locate_subclass(self, module_file, target_subclass): """ Return the first matching target_subclass in module_file. This function is used to scan a specific file (module_file) in the Lighthouse parsers directory for class definitions that subclass from target_subclass. We use this to dynmically import, locate, and return objects that are utilizing our CoverageFile abstraction. """ module = None module_class = None # attempt to import the given filepath as a python module try: module = __import__("lighthouse.reader.parsers." + module_file, globals(), locals(), ['object']) except Exception as e: logger.exception("| - Parser import failed") return None # # inspect the module for any classes that subclass from target_subclass # eg: target_subclass == CoverageFile # class_members = inspect.getmembers(module, inspect.isclass) for a_class in class_members: # does the current class definition we're inspecting subclass # from target_subclass? if so, it is a match try: if a_class[1].__bases__[0] == target_subclass: module_class = a_class[1] break # this class does not subclass / etc / not interesting / ignore it except IndexError as e: pass # return discovered parser or None return module_class