1
+ # import logging
1
2
import os
2
3
3
4
from typing import Callable , Any
4
5
from dataclasses import dataclass
5
6
7
+ from click .shell_completion import CompletionItem
8
+ from easybuild .tools .options import EasyBuildOptions
9
+
6
10
opt_group = {}
7
11
try :
8
12
import rich_click as click
9
13
except ImportError :
10
14
import click
11
15
else :
12
16
opt_group = click .rich_click .OPTION_GROUPS
17
+ opt_group .clear () # Clear existing groups to avoid conflicts
13
18
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
19
18
20
class OptionExtracter (EasyBuildOptions ):
19
21
def __init__ (self , * args , ** kwargs ):
@@ -24,9 +26,69 @@ def add_group_parser(self, opt_dict, descr, *args, prefix='', **kwargs):
24
26
super ().add_group_parser (opt_dict , descr , * args , prefix = prefix , ** kwargs )
25
27
self ._option_dicts [descr [0 ]] = (prefix , opt_dict )
26
28
29
+
27
30
extracter = OptionExtracter (go_args = [])
28
31
29
32
33
+ class DelimitedPathList (click .Path ):
34
+ """Custom Click parameter type for delimited lists."""
35
+ name = 'pathlist'
36
+
37
+ def __init__ (self , * args , delimiter = ',' , resolve_full : bool = True , ** kwargs ):
38
+ super ().__init__ (* args , ** kwargs )
39
+ self .delimiter = delimiter
40
+ self .resolve_full = resolve_full
41
+
42
+ def convert (self , value , param , ctx ):
43
+ if not isinstance (value , str ):
44
+ raise click .BadParameter (f"Expected a comma-separated string, got { value } " )
45
+ res = value .split (self .delimiter )
46
+ if self .resolve_full :
47
+ res = [os .path .abspath (v ) for v in res ]
48
+ return res
49
+
50
+ def shell_complete (self , ctx , param , incomplete ):
51
+ others , last = (['' ] + incomplete .rsplit (self .delimiter , 1 ))[- 2 :]
52
+ # logging.warning(f"Shell completion for delimited path list: others={others}, last={last}")
53
+ dir_path , prefix = os .path .split (last )
54
+ dir_path = dir_path or '.'
55
+ # logging.warning(f"Shell completion for delimited path list: dir_path={dir_path}, prefix={prefix}")
56
+ possibles = []
57
+ for path in os .listdir (dir_path ):
58
+ if not path .startswith (prefix ):
59
+ continue
60
+ full_path = os .path .join (dir_path , path )
61
+ if os .path .isdir (full_path ):
62
+ if self .dir_okay :
63
+ possibles .append (full_path )
64
+ possibles .append (full_path + os .sep )
65
+ elif os .path .isfile (full_path ):
66
+ if self .file_okay :
67
+ possibles .append (full_path )
68
+ start = f'{ others } { self .delimiter } ' if others else ''
69
+ res = [CompletionItem (f"{ start } { path } " ) for path in possibles ]
70
+ # logging.warning(f"Shell completion for delimited path list: res={possibles}")
71
+ return res
72
+
73
+
74
+ class DelimitedString (click .ParamType ):
75
+ """Custom Click parameter type for delimited strings."""
76
+ name = 'strlist'
77
+
78
+ def __init__ (self , * args , delimiter = ',' , ** kwargs ):
79
+ super ().__init__ (* args , ** kwargs )
80
+ self .delimiter = delimiter
81
+
82
+ def convert (self , value , param , ctx ):
83
+ if isinstance (value , str ):
84
+ return value .split (self .delimiter )
85
+ raise click .BadParameter (f"Expected a string or a comma-separated string, got { value } " )
86
+
87
+ def shell_complete (self , ctx , param , incomplete ):
88
+ last = incomplete .rsplit (self .delimiter , 1 )[- 1 ]
89
+ return super ().shell_complete (ctx , param , last )
90
+
91
+
30
92
@dataclass
31
93
class OptionData :
32
94
name : str
@@ -55,17 +117,112 @@ def to_click_option_dec(self):
55
117
56
118
kwargs = {
57
119
'help' : self .description ,
58
- # 'help': '123',
59
120
'default' : self .default ,
60
121
'show_default' : True ,
122
+ 'type' : None
61
123
}
62
124
63
- if self .default is False or self .default is True :
64
- kwargs ['is_flag' ] = True
65
-
66
- if isinstance (self .default , (list , tuple )):
125
+ if self .type in ['strlist' , 'strtuple' ]:
126
+ kwargs ['type' ] = DelimitedString (delimiter = ',' )
67
127
kwargs ['multiple' ] = True
128
+ elif self .type in ['pathlist' , 'pathtuple' ]:
129
+ # kwargs['type'] = DelimitedPathList(delimiter=os.pathsep)
130
+ kwargs ['type' ] = DelimitedPathList (delimiter = ',' )
131
+ kwargs ['multiple' ] = True
132
+ elif self .type in ['urllist' , 'urltuple' ]:
133
+ kwargs ['type' ] = DelimitedString (delimiter = '|' )
134
+ kwargs ['multiple' ] = True
135
+ elif self .type == 'choice' :
136
+ if self .lst is None :
137
+ raise ValueError (f"Choice type requires a list of choices for option { self .name } " )
138
+ kwargs ['type' ] = click .Choice (self .lst , case_sensitive = False )
139
+ elif self .type in ['int' , int ]:
140
+ kwargs ['type' ] = click .INT
141
+ elif self .type in ['float' , float ]:
142
+ kwargs ['type' ] = click .FLOAT
143
+ elif self .type in ['str' , str ]:
68
144
kwargs ['type' ] = click .STRING
145
+ elif self .type is None :
146
+ if self .default is False or self .default is True :
147
+ kwargs ['is_flag' ] = True
148
+ kwargs ['type' ] = click .BOOL
149
+ elif isinstance (self .default , (list , tuple )):
150
+ kwargs ['multiple' ] = True
151
+ kwargs ['type' ] = click .STRING
152
+
153
+ # if kwargs['type'] is None:
154
+ # print(f"Warning: No type specified for option {self.name}, defaulting to STRING")
155
+
156
+ # actions = set()
157
+ # for opt in EasyBuildCliOption.OPTIONS:
158
+ # actions.add(opt.action)
159
+ # print(f"Registered {len(EasyBuildCliOption.OPTIONS)} options with actions: {actions}")
160
+ # # Registered 296 options with actions: {
161
+ # # 'store_infolog', 'add_flex', 'append', 'add', 'store_true', 'store_debuglog', 'store_or_None',
162
+ # # 'store_warninglog', 'store', 'extend', 'regex'
163
+ # # }
164
+
165
+ # Actions:
166
+ # - shorthelp : hook for shortend help messages
167
+ # - confighelp : hook for configfile-style help messages
168
+ # - store_debuglog : turns on fancylogger debugloglevel
169
+ # - also: 'store_infolog', 'store_warninglog'
170
+ # - add : add value to default (result is default + value)
171
+ # - add_first : add default to value (result is value + default)
172
+ # - extend : alias for add with strlist type
173
+ # - type must support + (__add__) and one of negate (__neg__) or slicing (__getslice__)
174
+ # - add_flex : similar to add / add_first, but replaces the first "empty" element with the default
175
+ # - the empty element is dependent of the type
176
+ # - for {str,path}{list,tuple} this is the empty string
177
+ # - types must support the index method to determine the location of the "empty" element
178
+ # - the replacement uses +
179
+ # - e.g. a strlist type with value "0,,1"` and default [3,4] and action add_flex will
180
+ # use the empty string '' as "empty" element, and will result in [0,3,4,1] (not [0,[3,4],1])
181
+ # (but also a strlist with value "" and default [3,4] will result in [3,4];
182
+ # so you can't set an empty list with add_flex)
183
+ # - date : convert into datetime.date
184
+ # - datetime : convert into datetime.datetime
185
+ # - regex: compile str in regexp
186
+ # - store_or_None
187
+ # - set default to None if no option passed,
188
+ # - set to default if option without value passed,
189
+ # - set to value if option with value passed
190
+
191
+ # Types:
192
+ # - strlist, strtuple : convert comma-separated string in a list resp. tuple of strings
193
+ # - pathlist, pathtuple : using os.pathsep, convert pathsep-separated string in a list resp. tuple of strings
194
+ # - the path separator is OS-dependent
195
+ # - urllist, urltuple: convert string seperated by '|' to a list resp. tuple of strings
196
+
197
+ # def take_action(self, action, dest, opt, value, values, parser):
198
+ # if action == "store":
199
+ # setattr(values, dest, value)
200
+ # elif action == "store_const":
201
+ # setattr(values, dest, self.const)
202
+ # elif action == "store_true":
203
+ # setattr(values, dest, True)
204
+ # elif action == "store_false":
205
+ # setattr(values, dest, False)
206
+ # elif action == "append":
207
+ # values.ensure_value(dest, []).append(value)
208
+ # elif action == "append_const":
209
+ # values.ensure_value(dest, []).append(self.const)
210
+ # elif action == "count":
211
+ # setattr(values, dest, values.ensure_value(dest, 0) + 1)
212
+ # elif action == "callback":
213
+ # args = self.callback_args or ()
214
+ # kwargs = self.callback_kwargs or {}
215
+ # self.callback(self, opt, value, parser, *args, **kwargs)
216
+ # elif action == "help":
217
+ # parser.print_help()
218
+ # parser.exit()
219
+ # elif action == "version":
220
+ # parser.print_version()
221
+ # parser.exit()
222
+ # else:
223
+ # raise ValueError("unknown action %r" % self.action)
224
+
225
+ # return 1
69
226
70
227
return click .option (
71
228
* decls ,
@@ -81,6 +238,7 @@ def register_hidden_param(ctx, param, value):
81
238
ctx .hidden_params = {}
82
239
ctx .hidden_params [param .name ] = value
83
240
241
+
84
242
class EasyBuildCliOption ():
85
243
OPTIONS : list [OptionData ] = []
86
244
OPTIONS_MAP : dict [str , OptionData ] = {}
@@ -144,6 +302,7 @@ def register_option(cls, group: str, name: str, data: tuple, prefix: str = '') -
144
302
cls .OPTIONS_MAP [name ] = opt
145
303
cls .OPTIONS .append (opt )
146
304
305
+
147
306
for grp , dct in extracter ._option_dicts .items ():
148
307
prefix , dct = dct
149
308
if dct is None :
0 commit comments