blob: 3218721bd59292926549ff3ce29f226ca2fb1441 [file] [log] [blame]
Allen Li464220f2017-09-12 17:14:22 -07001# 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
Allen Li75f88082017-11-15 15:24:33 -08007Autotest imports should be done by calling monkeypatch() first and then
8calling load(). monkeypatch() should only be called once from a
9script's main function.
Allen Li0459da32017-11-10 12:11:42 -080010
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.
Aviv Keshet7ee12c52019-05-21 14:53:15 -070016
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().
Allen Li464220f2017-09-12 17:14:22 -070020"""
21
22from __future__ import absolute_import
23from __future__ import division
24from __future__ import print_function
25
Allen Li37f67e62018-01-25 14:16:43 -080026import ast
Allen Li75f88082017-11-15 15:24:33 -080027import contextlib
Allen Li464220f2017-09-12 17:14:22 -070028import imp
29import importlib
30import logging
31import os
32import site
Allen Li37f67e62018-01-25 14:16:43 -080033import subprocess
Allen Li464220f2017-09-12 17:14:22 -070034import sys
Aviv Keshet7ee12c52019-05-21 14:53:15 -070035import types
Allen Li464220f2017-09-12 17:14:22 -070036
37import autotest_lib
38
Allen Li45c2fdf2018-02-14 18:47:40 -080039AUTOTEST_DIR = autotest_lib.__path__[0]
40_SITEPKG_DIR = os.path.join(AUTOTEST_DIR, 'site-packages')
Allen Li37f67e62018-01-25 14:16:43 -080041_SYSTEM_PYTHON = '/usr/bin/python2.7'
Allen Li464220f2017-09-12 17:14:22 -070042
43_setup_done = False
44
45logger = logging.getLogger(__name__)
46
47
Allen Li75f88082017-11-15 15:24:33 -080048def monkeypatch():
49 """Monkeypatch everything needed to import Autotest.
Allen Libca5d012017-10-20 13:44:36 -070050
51 This should be called before any calls to load(). Only the main
52 function in scripts should call this function.
Allen Libca5d012017-10-20 13:44:36 -070053 """
Allen Li75f88082017-11-15 15:24:33 -080054 with _global_setup():
55 _monkeypatch_body()
Allen Libca5d012017-10-20 13:44:36 -070056
Allen Lif4b62ae2017-11-09 15:48:05 -080057
Allen Li75f88082017-11-15 15:24:33 -080058@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
Allen Libca5d012017-10-20 13:44:36 -070072
Allen Li75f88082017-11-15 15:24:33 -080073
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).
Allen Li37f67e62018-01-25 14:16:43 -080084 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')
Allen Li75f88082017-11-15 15:24:33 -080095
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')
Allen Libca5d012017-10-20 13:44:36 -0700101 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
Allen Li37f67e62018-01-25 14:16:43 -0800112def _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
Allen Li464220f2017-09-12 17:14:22 -0700124class _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."""
Allen Libca5d012017-10-20 13:44:36 -0700148 if fullname in sys.modules: # pragma: no cover
Allen Li464220f2017-09-12 17:14:22 -0700149 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
Allen Li2678d362017-10-11 16:59:07 -0700161 This enforces that monkeypatch() is called first.
Allen Licc153502017-09-15 16:08:25 -0700162
163 @param name: name of module as string, e.g., 'frontend.afe.models'
Allen Li464220f2017-09-12 17:14:22 -0700164 """
Allen Li0459da32017-11-10 12:11:42 -0800165 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):
Xixuan Wudb053c82019-01-31 20:07:06 -0800179 """Import module from dependencies, e.g. site-package.
Allen Li0459da32017-11-10 12:11:42 -0800180
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 """
Allen Li464220f2017-09-12 17:14:22 -0700197 if not _setup_done:
Allen Li0459da32017-11-10 12:11:42 -0800198 raise ImportError('cannot load chromite modules before monkeypatching')
199 return importlib.import_module(name)
Aviv Keshet7ee12c52019-05-21 14:53:15 -0700200
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)