1# Copyright 2020 The ChromiumOS Authors 2# Use of this source code is governed by a BSD-style license that can be 3# found in the LICENSE file. 4"""Module to dynamically discover and load ConstraintSuites""" 5 6import contextlib 7import glob 8import importlib 9import inspect 10import os 11import sys 12 13from typing import List 14 15from checker import constraint_suite 16 17 18@contextlib.contextmanager 19def _prepend_to_pythonpath(directory: str): 20 """Yields a context with directory prepended to sys.path. 21 22 Directory is removed from sys.path when the context is exited. This can be 23 used when a directory temporarily needs to be prepended to sys.path, without 24 polluting sys.path permanently. For example: 25 26 with _prepend_to_pythonpath(some_dir): 27 importlib.import_module(some_mod) 28 """ 29 sys.path.insert(0, directory) 30 yield 31 sys.path.remove(directory) 32 33 34def discover_suites( 35 directory: str, 36 pattern: str = 'check*.py', 37 exclude_pattern: str = '*test.py' 38) -> List[constraint_suite.ConstraintSuite]: 39 """Returns instances of all ConstraintSuites defined in a directory. 40 41 All files matching pattern in directory are dynamically loaded (pattern must 42 specify Python files). If those modules define a subclass of ConstraintSuite 43 an instance of this of this subclass is returned in the list of 44 ConstraintSuites. 45 46 Args: 47 directory: Directory to search. Note that the search is not recursive. 48 pattern: Filename pattern to match. Must be compatible with the glob module. 49 Must specify Python files. 50 exclude_pattern: Filename pattern to exclude. Must be compatible with the 51 glob module. If a file matches both pattern and exclude_pattern, it will 52 be excluded. 53 """ 54 if not pattern.endswith('.py'): 55 raise ValueError('pattern must end with ".py"') 56 57 # Sort discovered modules, so suites can be run in this order. 58 module_files = sorted( 59 set(glob.glob(os.path.join(directory, pattern))) - 60 set(glob.glob(os.path.join(directory, exclude_pattern)))) 61 62 # Convert file names -> module names. This means strip the '.py' and get only 63 # the filename (directory will be added to path so the modules can be loaded). 64 module_names = [] 65 for f in module_files: 66 assert f.endswith('.py') 67 module_names.append(os.path.basename(f)[:-3]) 68 69 # Create a context with directory in path, so modules can be loaded. 70 with _prepend_to_pythonpath(directory): 71 suites = [] 72 for name in module_names: 73 module = importlib.import_module(name) 74 75 # Inspect all classes defined in the module. If a class is a subclass of 76 # ConstraintSuite, add an instance of the class to suites. 77 for _, cls in inspect.getmembers(module, predicate=inspect.isclass): 78 if issubclass(cls, constraint_suite.ConstraintSuite): 79 suites.append(cls()) 80 81 # pytype has trouble inferring the type when the return statement is nested 82 # within the with block. Thus, pull the return statement outside the with 83 # block, which should be equivalent behavior. 84 return suites 85