• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2017 The Chromium Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5"""Kludges to support legacy Autotest code.
6
7Autotest imports should be done by calling monkeypatch() first and then
8calling load().  monkeypatch() should only be called once from a
9script's main function.
10
11chromite imports should be done with chromite_load(), and any third
12party packages should be imported with deps_load().  The reason for this
13is to present a clear API for these unsafe imports, making it easier to
14identify which imports are currently unsafe.  Eventually, everything
15should be moved to virtualenv, but that will not be in the near future.
16
17As an alternative to calling monkeypatch and load in a small scope wherever
18an external module is needed, chromite and autotest imports may also be done at
19the top level of a module using deferred_load() and deferred_chromite_load().
20"""
21
22from __future__ import absolute_import
23from __future__ import division
24from __future__ import print_function
25
26import ast
27import contextlib
28import imp
29import importlib
30import logging
31import os
32import site
33import subprocess
34import sys
35import types
36
37import autotest_lib
38
39AUTOTEST_DIR = autotest_lib.__path__[0]
40_SITEPKG_DIR = os.path.join(AUTOTEST_DIR, 'site-packages')
41_SYSTEM_PYTHON = '/usr/bin/python2.7'
42
43_setup_done = False
44
45logger = logging.getLogger(__name__)
46
47
48def monkeypatch():
49    """Monkeypatch everything needed to import Autotest.
50
51    This should be called before any calls to load().  Only the main
52    function in scripts should call this function.
53    """
54    with _global_setup():
55        _monkeypatch_body()
56
57
58@contextlib.contextmanager
59def _global_setup():
60    """Context manager for checking and setting global _setup_done variable."""
61    global _setup_done
62    assert not _setup_done
63    try:
64        yield
65    except Exception:  # pragma: no cover
66        # We cannot recover from this since we leave the interpreter in
67        # an unknown state.
68        logger.exception('Uncaught exception escaped Autotest setup')
69        sys.exit(1)
70    else:
71        _setup_done = True
72
73
74def _monkeypatch_body():
75    """The body of monkeypatch() running within _global_setup() context."""
76    # Add Autotest's site-packages.
77    site.addsitedir(_SITEPKG_DIR)
78
79    # Dummy out common imports as they may cause problems.
80    sys.meta_path.insert(0, _CommonRemovingFinder())
81
82    # Add chromite's third-party to the import path (chromite does this
83    # on import).
84    try:
85        importlib.import_module('chromite')
86    except ImportError:
87        # Moblab does not run build_externals; dependencies like
88        # chromite are installed system-wide.
89        logger.info("""\
90Could not find chromite; adding system packages and retrying \
91(This should only happen on Moblab)""")
92        for d in _system_site_packages():
93            site.addsitedir(d)
94        importlib.import_module('chromite')
95
96    # Set up Django environment variables.
97    importlib.import_module('autotest_lib.frontend.setup_django_environment')
98
99    # Make Django app paths absolute.
100    settings = importlib.import_module('autotest_lib.frontend.settings')
101    settings.INSTALLED_APPS = (
102            'autotest_lib.frontend.afe',
103            'autotest_lib.frontend.tko',
104            'django.contrib.admin',
105            'django.contrib.auth',
106            'django.contrib.contenttypes',
107            'django.contrib.sessions',
108            'django.contrib.sites',
109    )
110
111
112def _system_site_packages():
113    """Get list of system site-package directories.
114
115    This is needed for Moblab because dependencies are installed
116    system-wide instead of using build_externals.py.
117    """
118    output = subprocess.check_output([
119        _SYSTEM_PYTHON, '-c',
120        'import site; print repr(site.getsitepackages())'])
121    return ast.literal_eval(output)
122
123
124class _CommonRemovingFinder(object):
125    """Python import finder that neuters Autotest's common.py
126
127    The common module is replaced with an empty module everywhere it is
128    imported.  common.py should have only been imported for side
129    effects, so nothing should actually use the imported module.
130
131    See also https://www.python.org/dev/peps/pep-0302/
132    """
133
134    def find_module(self, fullname, path=None):
135        """Find module."""
136        del path  # unused
137        if not self._is_autotest_common(fullname):
138            return None
139        logger.debug('Dummying out %s import', fullname)
140        return self
141
142    def _is_autotest_common(self, fullname):
143        return (fullname.partition('.')[0] == 'autotest_lib'
144                and fullname.rpartition('.')[-1] == 'common')
145
146    def load_module(self, fullname):
147        """Load module."""
148        if fullname in sys.modules:  # pragma: no cover
149            return sys.modules[fullname]
150        mod = imp.new_module(fullname)
151        mod.__file__ = '<removed>'
152        mod.__loader__ = self
153        mod.__package__ = fullname.rpartition('.')[0]
154        sys.modules[fullname] = mod
155        return mod
156
157
158def load(name):
159    """Import module from autotest.
160
161    This enforces that monkeypatch() is called first.
162
163    @param name: name of module as string, e.g., 'frontend.afe.models'
164    """
165    return _load('autotest_lib.%s' % name)
166
167
168def chromite_load(name):
169    """Import module from chromite.lib.
170
171    This enforces that monkeypatch() is called first.
172
173    @param name: name of module as string, e.g., 'metrics'
174    """
175    return _load('chromite.lib.%s' % name)
176
177
178def deps_load(name):
179    """Import module from dependencies, e.g. site-package.
180
181    This enforces that monkeypatch() is called first.
182
183    @param name: name of module as string, e.g., 'metrics'
184    """
185    assert not name.startswith('autotest_lib')
186    assert not name.startswith('chromite.lib')
187    return _load(name)
188
189
190def _load(name):
191    """Import a module.
192
193    This enforces that monkeypatch() is called first.
194
195    @param name: name of module as string
196    """
197    if not _setup_done:
198        raise ImportError('cannot load chromite modules before monkeypatching')
199    return importlib.import_module(name)
200
201
202def deferred_load(name):
203    """Eventually import module from autotest.
204
205    This function returns a dummy module that will load the given autotest
206    module upon its first use (if monkeypatch() has is called first; else
207    its use will fail).
208
209    @param name: name of module as string, e.g., 'frontend.afe.models'
210    """
211    return _DeferredModule('autotest_lib.%s' % name)
212
213
214def deferred_chromite_load(name):
215    """Eventually import module from chromite.lib.
216
217    This function returns a dummy module that will load the given chromite
218    module upon its first use (if monkeypatch() has is called first; else
219    its use will fail).
220
221    @param name: name of module as string, e.g., 'metrics'
222    """
223    return _DeferredModule('chromite.lib.%s' % name)
224
225
226_UNLOADED_MODULE = object()
227
228
229class _DeferredModule(types.ModuleType):
230    """Module that is loaded upon first usage."""
231
232    def __init__(self, name):
233        super(_DeferredModule, self).__init__(name)
234        self._name = name
235        self._module = _UNLOADED_MODULE
236
237    def __getattribute__(self, name):
238        module = object.__getattribute__(self, "_module")
239        if module is _UNLOADED_MODULE:
240            module_name = object.__getattribute__(self, "_name")
241            module = _load(module_name)
242            self._module = module
243
244        return getattr(module, name)
245