Skip to content

Commit d841aa4

Browse files
committed
Make run.py fully reusable across modularcrypto packages
1 parent 8f938aa commit d841aa4

File tree

6 files changed

+213
-98
lines changed

6 files changed

+213
-98
lines changed

dev/__init__.py

+18
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,24 @@
1414
"ocspbuilder"
1515
]
1616

17+
task_keyword_args = [
18+
{
19+
'name': 'use_openssl',
20+
'placeholder': '/path/to/libcrypto,/path/to/libssl',
21+
'env_var': 'OSCRYPTO_USE_OPENSSL',
22+
},
23+
{
24+
'name': 'use_winlegacy',
25+
'placeholder': 'true',
26+
'env_var': 'OSCRYPTO_USE_WINLEGACY',
27+
},
28+
{
29+
'name': 'use_ctypes',
30+
'placeholder': 'true',
31+
'env_var': 'OSCRYPTO_USE_CTYPES',
32+
},
33+
]
34+
1735
requires_oscrypto = True
1836
has_tests_package = True
1937

dev/_import.py

+9-3
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
getcwd = os.getcwd
1414

1515

16-
def _import_from(mod, path, mod_dir=None):
16+
def _import_from(mod, path, mod_dir=None, allow_error=False):
1717
"""
1818
Imports a module from a specific path
1919
@@ -27,23 +27,29 @@ def _import_from(mod, path, mod_dir=None):
2727
If the sub directory of "path" is different than the "mod" name,
2828
pass the sub directory as a unicode string
2929
30+
:param allow_error:
31+
If an ImportError should be raised when the module can't be imported
32+
3033
:return:
3134
None if not loaded, otherwise the module
3235
"""
3336

3437
if mod_dir is None:
35-
mod_dir = mod
38+
mod_dir = mod.replace('.', os.sep)
3639

3740
if not os.path.exists(path):
3841
return None
3942

40-
if not os.path.exists(os.path.join(path, mod_dir)):
43+
if not os.path.exists(os.path.join(path, mod_dir)) \
44+
and not os.path.exists(os.path.join(path, mod_dir + '.py')):
4145
return None
4246

4347
try:
4448
mod_info = imp.find_module(mod_dir, [path])
4549
return imp.load_module(mod, *mod_info)
4650
except ImportError:
51+
if allow_error:
52+
raise
4753
return None
4854

4955

dev/_task.py

+163
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
# coding: utf-8
2+
from __future__ import unicode_literals, division, absolute_import, print_function
3+
4+
import ast
5+
import _ast
6+
import os
7+
import sys
8+
9+
from . import package_root, task_keyword_args
10+
from ._import import _import_from
11+
12+
13+
if sys.version_info < (3,):
14+
byte_cls = str
15+
else:
16+
byte_cls = bytes
17+
18+
19+
def _list_tasks():
20+
"""
21+
Fetches a list of all valid tasks that may be run, and the args they
22+
accept. Does not actually import the task module to prevent errors if a
23+
user does not have the dependencies installed for every task.
24+
25+
:return:
26+
A list of 2-element tuples:
27+
0: a unicode string of the task name
28+
1: a list of dicts containing the parameter definitions
29+
"""
30+
31+
out = []
32+
dev_path = os.path.join(package_root, 'dev')
33+
for fname in sorted(os.listdir(dev_path)):
34+
if fname.startswith('.') or fname.startswith('_'):
35+
continue
36+
if not fname.endswith('.py'):
37+
continue
38+
name = fname[:-3]
39+
args = ()
40+
41+
full_path = os.path.join(package_root, 'dev', fname)
42+
with open(full_path, 'rb') as f:
43+
full_code = f.read()
44+
if sys.version_info >= (3,):
45+
full_code = full_code.decode('utf-8')
46+
47+
task_node = ast.parse(full_code, filename=full_path)
48+
for node in ast.iter_child_nodes(task_node):
49+
if isinstance(node, _ast.Assign):
50+
if len(node.targets) == 1 \
51+
and isinstance(node.targets[0], _ast.Name) \
52+
and node.targets[0].id == 'run_args':
53+
args = ast.literal_eval(node.value)
54+
break
55+
56+
out.append((name, args))
57+
return out
58+
59+
60+
def show_usage():
61+
"""
62+
Prints to stderr the valid options for invoking tasks
63+
"""
64+
65+
valid_tasks = []
66+
for task in _list_tasks():
67+
usage = task[0]
68+
for run_arg in task[1]:
69+
usage += ' '
70+
name = run_arg.get('name', '')
71+
if run_arg.get('required', False):
72+
usage += '{%s}' % name
73+
else:
74+
usage += '[%s]' % name
75+
valid_tasks.append(usage)
76+
77+
out = 'Usage: run.py'
78+
for karg in task_keyword_args:
79+
out += ' [%s=%s]' % (karg['name'], karg['placeholder'])
80+
out += ' (%s)' % ' | '.join(valid_tasks)
81+
82+
print(out, file=sys.stderr)
83+
sys.exit(1)
84+
85+
86+
def _get_arg(num):
87+
"""
88+
:return:
89+
A unicode string of the requested command line arg
90+
"""
91+
92+
if len(sys.argv) < num + 1:
93+
return None
94+
arg = sys.argv[num]
95+
if isinstance(arg, byte_cls):
96+
arg = arg.decode('utf-8')
97+
return arg
98+
99+
100+
def run_task():
101+
"""
102+
Parses the command line args, invoking the requested task
103+
"""
104+
105+
arg_num = 1
106+
task = None
107+
args = []
108+
kwargs = {}
109+
110+
# We look for the task name, processing any global task keyword args
111+
# by setting the appropriate env var
112+
while True:
113+
val = _get_arg(arg_num)
114+
if val is None:
115+
break
116+
117+
next_arg = False
118+
for karg in task_keyword_args:
119+
if val.startswith(karg['name'] + '='):
120+
os.environ[karg['env_var']] = val[len(karg['name']) + 1:]
121+
next_arg = True
122+
break
123+
124+
if next_arg:
125+
arg_num += 1
126+
continue
127+
128+
task = val
129+
break
130+
131+
if task is None:
132+
show_usage()
133+
134+
task_mod = _import_from('dev.%s' % task, package_root, allow_error=True)
135+
if task_mod is None:
136+
show_usage()
137+
138+
run_args = task_mod.__dict__.get('run_args', [])
139+
max_args = arg_num + 1 + len(run_args)
140+
141+
if len(sys.argv) > max_args:
142+
show_usage()
143+
144+
for i, run_arg in enumerate(run_args):
145+
val = _get_arg(arg_num + 1 + i)
146+
if val is None:
147+
if run_arg.get('required', False):
148+
show_usage()
149+
break
150+
151+
if run_arg.get('cast') == 'int' and val.isdigit():
152+
val = int(val)
153+
154+
kwarg = run_arg.get('kwarg')
155+
if kwarg:
156+
kwargs[kwarg] = val
157+
else:
158+
args.append(val)
159+
160+
run = task_mod.__dict__.get('run')
161+
162+
result = run(*args, **kwargs)
163+
sys.exit(int(not result))

dev/tests.py

+13
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,19 @@
1818
from io import StringIO
1919

2020

21+
run_args = [
22+
{
23+
'name': 'regex',
24+
'kwarg': 'matcher',
25+
},
26+
{
27+
'name': 'repeat_count',
28+
'kwarg': 'repeat',
29+
'cast': 'int',
30+
},
31+
]
32+
33+
2134
def run(matcher=None, repeat=1, ci=False):
2235
"""
2336
Runs the tests

dev/version.py

+8
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,14 @@
88
from . import package_root, package_name, has_tests_package
99

1010

11+
run_args = [
12+
{
13+
'name': 'pep440_version',
14+
'required': True
15+
},
16+
]
17+
18+
1119
def run(new_version):
1220
"""
1321
Updates the package version in the various locations

run.py

+2-95
Original file line numberDiff line numberDiff line change
@@ -2,100 +2,7 @@
22
# coding: utf-8
33
from __future__ import unicode_literals, division, absolute_import, print_function
44

5-
import os
6-
import sys
5+
from dev._task import run_task
76

8-
if sys.version_info < (3,):
9-
byte_cls = str
10-
else:
11-
byte_cls = bytes
127

13-
14-
def show_usage():
15-
print(
16-
'Usage: run.py [use_openssl=/path/to/libcrypto,/path/to/libssl] [use_winlegacy=true] '
17-
'(api_docs | lint | tests [regex] [repeat_count] | coverage | deps | ci | '
18-
'version {pep440_version} | build | release)',
19-
file=sys.stderr
20-
)
21-
sys.exit(1)
22-
23-
24-
def get_arg(num):
25-
if len(sys.argv) < num + 1:
26-
return None, num
27-
arg = sys.argv[num]
28-
if isinstance(arg, byte_cls):
29-
arg = arg.decode('utf-8')
30-
return arg, num + 1
31-
32-
33-
if len(sys.argv) < 2 or len(sys.argv) > 4:
34-
show_usage()
35-
36-
task, next_arg = get_arg(1)
37-
38-
39-
# We don't actually configure here since we want any coverage
40-
# testing to record that we tested overriding the backend
41-
if task.startswith('use_openssl='):
42-
os.environ['OSCRYPTO_USE_OPENSSL'] = task[12:]
43-
task, next_arg = get_arg(next_arg)
44-
elif task == 'use_winlegacy=true':
45-
os.environ['OSCRYPTO_USE_WINLEGACY'] = 'true'
46-
task, next_arg = get_arg(next_arg)
47-
elif task == 'use_ctypes=true':
48-
os.environ['OSCRYPTO_USE_CTYPES'] = 'true'
49-
task, next_arg = get_arg(next_arg)
50-
51-
52-
if task not in set(['api_docs', 'lint', 'tests', 'coverage', 'deps', 'ci', 'version', 'build', 'release']):
53-
show_usage()
54-
55-
if task != 'tests' and task != 'version' and len(sys.argv) - next_arg > 0:
56-
show_usage()
57-
58-
params = []
59-
kwargs = {}
60-
if task == 'api_docs':
61-
from dev.api_docs import run
62-
63-
elif task == 'lint':
64-
from dev.lint import run
65-
66-
elif task == 'tests':
67-
from dev.tests import run
68-
matcher, next_arg = get_arg(next_arg)
69-
if matcher:
70-
if matcher.isdigit():
71-
kwargs['repeat'] = int(matcher)
72-
else:
73-
kwargs['matcher'] = matcher
74-
repeat, next_arg = get_arg(next_arg)
75-
if repeat:
76-
kwargs['repeat'] = int(repeat)
77-
78-
elif task == 'coverage':
79-
from dev.coverage import run
80-
81-
elif task == 'deps':
82-
from dev.deps import run
83-
84-
elif task == 'ci':
85-
from dev.ci import run
86-
87-
elif task == 'version':
88-
from dev.version import run
89-
if len(sys.argv) != 3:
90-
show_usage()
91-
pep440_version, next_arg = get_arg(next_arg)
92-
params.append(pep440_version)
93-
94-
elif task == 'build':
95-
from dev.build import run
96-
97-
elif task == 'release':
98-
from dev.release import run
99-
100-
result = run(*params, **kwargs)
101-
sys.exit(int(not result))
8+
run_task()

0 commit comments

Comments
 (0)