|
| 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) |
0 commit comments