forked from quantumlib/Cirq
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathimport_test.py
executable file
·225 lines (190 loc) · 7.97 KB
/
import_test.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
# Copyright 2019 The Cirq Developers
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Locates imports that violate cirq's submodule dependencies.
Specifically, this test treats the modules as a tree structure where `cirq` is
the root, each submodule is a node and each python file is a leaf node. While
a node (module) is in the process of being imported, it is not allowed to import
nodes for the first time other than its children. If a module was imported
earlier by `cirq.__init__`, it may be imported. This is currently only enforced
for the first level of submodules under cirq, not sub-submodules.
Usage:
dev_tools/import_test.py [-h] [--time] [--others]
optional arguments:
-h, --help show this help message and exit
--time print a report of the modules that took the longest to import
--others also track packages other than cirq and print when they are
imported
"""
from typing import List
import argparse
import collections
import os.path
import subprocess
import sys
import time
parser = argparse.ArgumentParser(
description="Locates imports that violate cirq's submodule dependencies."
)
parser.add_argument(
'--time',
action='store_true',
help='print a report of the modules that took the longest to import',
)
parser.add_argument(
'--others',
action='store_true',
help='also track packages other than cirq and print when they are imported',
)
def verify_import_tree(depth: int = 1, track_others: bool = False, timeit: bool = False) -> bool:
"""Locates imports that violate cirq's submodule dependencies by
instrumenting python import machinery then importing cirq.
Logs when each submodule (up to the given depth) begins and ends executing
during import and prints an error when any import within a submodule causes
a neighboring module to be imported for the first time. The indent
pattern of the printed output will match the module tree structure if the
imports are all valid. Otherwise an error is printed indicating the
location of the invalid import.
Output for valid imports:
Start cirq
...
Start cirq.study
End cirq.study
Start cirq.circuits
End cirq.circuits
Start cirq.schedules
End cirq.schedules
...
End cirq
Output for an invalid import in `cirq/circuits/circuit.py`:
Start cirq
...
Start cirq.study
End cirq.study
Start cirq.circuits
ERROR: cirq.circuits.circuit imported cirq.vis
Start cirq.vis
End cirq.vis
... # Possibly more errors caused by the first.
End cirq.circuits
Start cirq.schedules
End cirq.schedules
...
End cirq
Invalid import: cirq.circuits.circuit imported cirq.vis
Args:
depth: How deep in the module tree to verify. If depth is 1, verifies
that submodules of cirq like cirq.ops doesn't import cirq.circuit.
If depth is 2, verifies that submodules and sub-submodules like
cirq.ops.raw_types doesn't import cirq.ops.common_gates or
cirq.circuit.
track_others: If True, logs where cirq first imports an external package
in addition to logging when cirq modules are imported.
timeit: Measure the import time of cirq and each submodule and print a
report of the worst. Includes times for external packages used by
cirq if `track_others` is True.
Returns:
True is no import issues, False otherwise.
"""
fail_list = []
start_times = {}
load_times = {}
current_path: List[str] = []
currently_running_paths: List[List[str]] = [[]]
import_depth = 0
indent = ' ' * 2
def wrap_module(module):
nonlocal import_depth
start_times[module.__name__] = time.perf_counter()
path = module.__name__.split('.')
if path[0] != 'cirq':
if len(path) == 1:
print(f'{indent * import_depth}Other {module.__name__}')
return module
currently_running_paths.append(path)
if len(path) == len(current_path) + 1 and path[:-1] == current_path:
# Move down in tree
current_path.append(path[-1])
else:
# Jump somewhere else in the tree
handle_error(currently_running_paths[-2], path)
current_path[:] = path
if len(path) <= depth + 1:
print(f'{indent * import_depth}Start {module.__name__}')
import_depth += 1
return module
def after_exec(module):
nonlocal import_depth
load_times[module.__name__] = time.perf_counter() - start_times[module.__name__]
path = module.__name__.split('.')
if path[0] != 'cirq':
return
assert path == currently_running_paths.pop(), 'Unexpected import state'
if len(path) <= depth + 1:
import_depth -= 1
print(f'{indent * import_depth}End {module.__name__}')
if path == current_path:
# No submodules were here
current_path.pop()
elif len(path) == len(current_path) - 1 and path == current_path[:-1]:
# Move up in tree
current_path.pop()
else:
# Jump somewhere else in the tree
current_path[:] = path[:-1]
def handle_error(import_from, import_to):
if import_from[: depth + 1] != import_to[: depth + 1]:
msg = f"{'.'.join(import_from)} imported {'.'.join(import_to)}"
fail_list.append(msg)
print(f'ERROR: {msg}')
# Import wrap_module_executions without importing cirq
orig_path = list(sys.path)
project_dir = os.path.dirname(os.path.dirname(__file__))
cirq_dir = os.path.join(project_dir, 'cirq')
sys.path.append(cirq_dir) # Put cirq/_import.py in the path.
from cirq._import import wrap_module_executions # type: ignore
sys.path[:] = orig_path # Restore the path.
sys.path.append(project_dir) # Ensure the cirq package is in the path.
# note that with the cirq.google injection we do change the metapath
with wrap_module_executions('' if track_others else 'cirq', wrap_module, after_exec, False):
# Import cirq with instrumentation
import cirq # pylint: disable=unused-import
sys.path[:] = orig_path # Restore the path.
if fail_list:
print()
# Only print the first because later errors are often caused by the
# first and not as helpful.
print(f'Invalid import: {fail_list[0]}')
if timeit:
worst_loads = collections.Counter(load_times).most_common(15)
print()
print('Worst load times:')
for name, dt in worst_loads:
print(f'{dt:.3f} {name}')
return not fail_list
FAIL_EXIT_CODE = 65
def test_no_circular_imports():
"""Runs the test in a subprocess because cirq has already been imported
before in an earlier test but this test needs to control the import process.
"""
status = subprocess.call([sys.executable, __file__])
if status == FAIL_EXIT_CODE:
# coverage: ignore
raise Exception('Invalid import. See captured output for details.')
elif status != 0:
# coverage: ignore
raise RuntimeError('Error in subprocess')
if __name__ == '__main__':
args = parser.parse_args()
success = verify_import_tree(track_others=args.others, timeit=args.time)
sys.exit(0 if success else FAIL_EXIT_CODE)