Skip to content

Commit 2aadb58

Browse files
committed
WIP test click CI wrapper
1 parent f4e855b commit 2aadb58

File tree

4 files changed

+196
-0
lines changed

4 files changed

+196
-0
lines changed

easybuild/cli/__init__.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
try:
2+
import rich_click as click
3+
except ImportError:
4+
import click
5+
6+
try:
7+
from rich.traceback import install
8+
except ImportError:
9+
pass
10+
else:
11+
install(suppress=[click])
12+
13+
from .options import EasyBuildCliOption
14+
15+
from easybuild.main import main_with_hooks
16+
17+
@click.command()
18+
@EasyBuildCliOption.apply_options
19+
@click.pass_context
20+
@click.argument('other_args', nargs=-1, type=click.UNPROCESSED, required=False)
21+
def eb(ctx, other_args):
22+
"""EasyBuild command line interface."""
23+
args = []
24+
for key, value in ctx.hidden_params.items():
25+
key = key.replace('_', '-')
26+
if isinstance(value, bool):
27+
if value:
28+
args.append(f"--{key}")
29+
else:
30+
if value and value != EasyBuildCliOption.OPTIONS_MAP[key].default:
31+
if isinstance(value, (list, tuple)):
32+
value = ','.join(value)
33+
args.append(f"--{key}={value}")
34+
35+
args.extend(other_args)
36+
37+
main_with_hooks(args=args)

easybuild/cli/options/__init__.py

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
import os
2+
3+
from typing import Callable, Any
4+
from dataclasses import dataclass
5+
6+
opt_group = {}
7+
try:
8+
import rich_click as click
9+
except ImportError:
10+
import click
11+
else:
12+
opt_group = click.rich_click.OPTION_GROUPS
13+
14+
from easybuild.tools.options import EasyBuildOptions
15+
16+
DEBUG_EASYBUILD_OPTIONS = os.environ.get('DEBUG_EASYBUILD_OPTIONS', '').lower() in ('1', 'true', 'yes', 'y')
17+
18+
class OptionExtracter(EasyBuildOptions):
19+
def __init__(self, *args, **kwargs):
20+
self._option_dicts = {}
21+
super().__init__(*args, **kwargs)
22+
23+
def add_group_parser(self, opt_dict, descr, *args, prefix='', **kwargs):
24+
super().add_group_parser(opt_dict, descr, *args, prefix=prefix, **kwargs)
25+
self._option_dicts[descr[0]] = (prefix, opt_dict)
26+
27+
extracter = OptionExtracter(go_args=[])
28+
29+
def register_hidden_param(ctx, param, value):
30+
"""Register a hidden parameter in the context."""
31+
if not hasattr(ctx, 'hidden_params'):
32+
ctx.hidden_params = {}
33+
ctx.hidden_params[param.name] = value
34+
35+
@dataclass
36+
class OptionData:
37+
name: str
38+
description: str
39+
type: str
40+
action: str
41+
default: Any
42+
group: str = None
43+
short: str = None
44+
meta: dict = None
45+
lst: list = None
46+
47+
def __post_init__(self):
48+
if self.short is not None and not isinstance(self.short, str):
49+
raise TypeError(f"Short option must be a string, got {type(self.short)}")
50+
if self.meta is not None and not isinstance(self.meta, dict):
51+
raise TypeError(f"Meta must be a dictionary, got {type(self.meta)}")
52+
if self.lst is not None and not isinstance(self.lst, (list, tuple)):
53+
raise TypeError(f"List must be a list or tuple, got {type(self.lst)}")
54+
55+
def to_click_option_dec(self):
56+
"""Convert OptionData to a click.Option."""
57+
decls = [f"--{self.name}"]
58+
if self.short:
59+
decls.insert(0, f"-{self.short}")
60+
61+
kwargs = {
62+
'help': self.description,
63+
# 'help': '123',
64+
'default': self.default,
65+
'show_default': True,
66+
}
67+
68+
if self.default is False or self.default is True:
69+
kwargs['is_flag'] = True
70+
71+
if isinstance(self.default, (list, tuple)):
72+
kwargs['multiple'] = True
73+
kwargs['type'] = click.STRING
74+
75+
return click.option(
76+
*decls,
77+
expose_value=False,
78+
callback=register_hidden_param,
79+
**kwargs
80+
)
81+
82+
class EasyBuildCliOption():
83+
OPTIONS: list[OptionData] = []
84+
OPTIONS_MAP: dict[str, OptionData] = {}
85+
86+
@classmethod
87+
def apply_options(cls, function: Callable) -> Callable:
88+
"""Decorator to apply EasyBuild options to a function."""
89+
group_data = {}
90+
for opt_obj in cls.OPTIONS:
91+
group_data.setdefault(opt_obj.group, []).append(f'--{opt_obj.name}')
92+
function = opt_obj.to_click_option_dec()(function)
93+
lst = []
94+
for key, value in group_data.items():
95+
lst.append({
96+
'name': key,
97+
# 'description': f'Options for {key}',
98+
'options': value
99+
})
100+
opt_group[function.__name__] = lst
101+
return function
102+
103+
@classmethod
104+
def register_option(cls, group: str, name: str, data: tuple, prefix: str = '') -> None:
105+
"""Register an EasyBuild option."""
106+
if prefix:
107+
name = f"{prefix}-{name}"
108+
if name == 'help':
109+
return
110+
short = None
111+
meta = None
112+
lst = None
113+
descr, typ, action, default, *others = data
114+
while others:
115+
opt = others.pop(0)
116+
if isinstance(opt, str):
117+
if short is not None:
118+
raise ValueError(f"Short option already set: {short} for {name}")
119+
short = opt
120+
elif isinstance(opt, dict):
121+
if meta is not None:
122+
raise ValueError(f"Meta already set: {meta} for {name}")
123+
meta = opt
124+
elif isinstance(opt, (list, tuple)):
125+
if lst is not None:
126+
raise ValueError(f"List already set: {lst} for {name}")
127+
lst = opt
128+
else:
129+
raise ValueError(f"Unexpected type for others: {type(others[0])} in {others}")
130+
131+
opt = OptionData(
132+
group=group,
133+
name=name,
134+
description=descr,
135+
type=typ,
136+
action=action,
137+
default=default,
138+
short=short,
139+
meta=meta,
140+
lst=lst
141+
)
142+
cls.OPTIONS_MAP[name] = opt
143+
cls.OPTIONS.append(opt)
144+
145+
for grp, dct in extracter._option_dicts.items():
146+
prefix, dct = dct
147+
if dct is None:
148+
continue
149+
for key, value in dct.items():
150+
EasyBuildCliOption.register_option(grp, key, value, prefix=prefix)

eb2

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#!/usr/bin/env python
2+
# -*- coding: utf-8 -*-
3+
import re
4+
import sys
5+
from easybuild.cli import eb
6+
if __name__ == '__main__':
7+
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
8+
sys.exit(eb())

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ def find_rel_test():
9393
package_data={'test.framework': find_rel_test()},
9494
scripts=[
9595
'eb',
96+
'eb2',
9697
# bash completion
9798
'optcomplete.bash',
9899
'minimal_bash_completion.bash',

0 commit comments

Comments
 (0)