• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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