3
3
Structured configuration options.
4
4
5
5
"""
6
+ import argparse
6
7
from collections import defaultdict
7
8
from dataclasses import dataclass
8
9
from pathlib import Path
10
+ import pathlib
9
11
from typing import (
10
12
Any ,
11
13
ClassVar ,
27
29
from .error_code import ErrorCode
28
30
from .safe import safe_in
29
31
32
+ try :
33
+ from argparse import BooleanOptionalAction
34
+ except ImportError :
35
+ # 3.8 and lower (modified from CPython)
36
+ class BooleanOptionalAction (argparse .Action ):
37
+ def __init__ (self , option_strings : Sequence [str ], ** kwargs : Any ) -> None :
38
+
39
+ _option_strings = []
40
+ for option_string in option_strings :
41
+ _option_strings .append (option_string )
42
+
43
+ if option_string .startswith ("--" ):
44
+ option_string = "--no-" + option_string [2 :]
45
+ _option_strings .append (option_string )
46
+
47
+ super ().__init__ (option_strings = _option_strings , nargs = 0 , ** kwargs )
48
+
49
+ def __call__ (
50
+ self ,
51
+ parser : argparse .ArgumentParser ,
52
+ namespace : argparse .Namespace ,
53
+ values : object ,
54
+ option_string : Optional [str ] = None ,
55
+ ) -> None :
56
+ if option_string is not None and option_string in self .option_strings :
57
+ setattr (namespace , self .dest , not option_string .startswith ("--no-" ))
58
+
59
+ def format_usage (self ) -> str :
60
+ return " | " .join (self .option_strings )
61
+
62
+
30
63
T = TypeVar ("T" )
31
64
OptionT = TypeVar ("OptionT" , bound = "ConfigOption" )
32
65
ModulePath = Tuple [str , ...]
@@ -56,6 +89,7 @@ class ConfigOption(Generic[T]):
56
89
name : ClassVar [str ]
57
90
is_global : ClassVar [bool ] = False
58
91
default_value : ClassVar [T ]
92
+ should_create_command_line_option : ClassVar [bool ] = True
59
93
value : T
60
94
applicable_to : ModulePath = ()
61
95
from_command_line : bool = False
@@ -104,6 +138,10 @@ def get_fallback_option(cls: Type[OptionT], fallback: Config) -> Optional[Option
104
138
else :
105
139
return cls (val )
106
140
141
+ @classmethod
142
+ def create_command_line_option (cls , parser : argparse .ArgumentParser ) -> None :
143
+ raise NotImplementedError (cls )
144
+
107
145
108
146
class BooleanOption (ConfigOption [bool ]):
109
147
default_value = False
@@ -114,6 +152,15 @@ def parse(cls: "Type[BooleanOption]", data: object, source_path: Path) -> bool:
114
152
return data
115
153
raise InvalidConfigOption .from_parser (cls , "bool" , data )
116
154
155
+ @classmethod
156
+ def create_command_line_option (cls , parser : argparse .ArgumentParser ) -> None :
157
+ parser .add_argument (
158
+ f"--{ cls .name .replace ('_' , '-' )} " ,
159
+ action = BooleanOptionalAction ,
160
+ help = cls .__doc__ ,
161
+ default = argparse .SUPPRESS ,
162
+ )
163
+
117
164
118
165
class IntegerOption (ConfigOption [int ]):
119
166
@classmethod
@@ -122,6 +169,15 @@ def parse(cls: "Type[IntegerOption]", data: object, source_path: Path) -> int:
122
169
return data
123
170
raise InvalidConfigOption .from_parser (cls , "int" , data )
124
171
172
+ @classmethod
173
+ def create_command_line_option (cls , parser : argparse .ArgumentParser ) -> None :
174
+ parser .add_argument (
175
+ f"--{ cls .name .replace ('_' , '-' )} " ,
176
+ type = int ,
177
+ help = cls .__doc__ ,
178
+ default = argparse .SUPPRESS ,
179
+ )
180
+
125
181
126
182
class ConcatenatedOption (ConfigOption [Sequence [T ]]):
127
183
"""Option for which the value is the concatenation of all the overrides."""
@@ -152,6 +208,15 @@ def parse(
152
208
return data
153
209
raise InvalidConfigOption .from_parser (cls , "sequence of strings" , data )
154
210
211
+ @classmethod
212
+ def create_command_line_option (cls , parser : argparse .ArgumentParser ) -> None :
213
+ parser .add_argument (
214
+ f"--{ cls .name .replace ('_' , '-' )} " ,
215
+ action = "append" ,
216
+ help = cls .__doc__ ,
217
+ default = argparse .SUPPRESS ,
218
+ )
219
+
155
220
156
221
class PathSequenceOption (ConfigOption [Sequence [Path ]]):
157
222
default_value : ClassVar [Sequence [Path ]] = ()
@@ -166,6 +231,16 @@ def parse(
166
231
return [(source_path .parent / elt ).resolve () for elt in data ]
167
232
raise InvalidConfigOption .from_parser (cls , "sequence of strings" , data )
168
233
234
+ @classmethod
235
+ def create_command_line_option (cls , parser : argparse .ArgumentParser ) -> None :
236
+ parser .add_argument (
237
+ f"--{ cls .name .replace ('_' , '-' )} " ,
238
+ action = "append" ,
239
+ type = pathlib .Path ,
240
+ help = cls .__doc__ ,
241
+ default = argparse .SUPPRESS ,
242
+ )
243
+
169
244
170
245
class PyObjectSequenceOption (ConfigOption [Sequence [T ]]):
171
246
"""Represents a sequence of objects parsed as Python objects."""
@@ -197,6 +272,16 @@ def contains(cls, obj: object, options: "Options") -> bool:
197
272
val = options .get_value_for (cls )
198
273
return safe_in (obj , val )
199
274
275
+ @classmethod
276
+ def create_command_line_option (cls , parser : argparse .ArgumentParser ) -> None :
277
+ parser .add_argument (
278
+ f"--{ cls .name .replace ('_' , '-' )} " ,
279
+ action = "append" ,
280
+ type = qcore .object_from_string ,
281
+ help = cls .__doc__ ,
282
+ default = argparse .SUPPRESS ,
283
+ )
284
+
200
285
201
286
@dataclass
202
287
class Options :
@@ -262,6 +347,34 @@ def is_error_code_enabled_anywhere(self, code: ErrorCode) -> bool:
262
347
return False
263
348
return option .default_value
264
349
350
+ def display (self ) -> None :
351
+ print ("Options:" )
352
+ prefix = " " * 8
353
+ for name , option_cls in sorted (ConfigOption .registry .items ()):
354
+ current_value = self .get_value_for (option_cls )
355
+ print (f" { name } (value: { current_value } )" )
356
+ instances = self .options .get (name , [])
357
+ for instance in instances :
358
+ pieces = []
359
+ if instance .applicable_to :
360
+ pieces .append (f"module: { '.' .join (instance .applicable_to )} " )
361
+ if instance .from_command_line :
362
+ pieces .append ("from command line" )
363
+ else :
364
+ pieces .append ("from config file" )
365
+ suffix = f" ({ ', ' .join (pieces )} )"
366
+ print (f"{ prefix } { instance .value } { suffix } " )
367
+ print (f"Fallback: { self .fallback } " )
368
+ if self .module_path :
369
+ print (f"For module: { '.' .join (self .module_path )} " )
370
+
371
+
372
+ def add_arguments (parser : argparse .ArgumentParser ) -> None :
373
+ for cls in ConfigOption .registry .values ():
374
+ if not cls .should_create_command_line_option :
375
+ continue
376
+ cls .create_command_line_option (parser )
377
+
265
378
266
379
def parse_config_file (path : Path ) -> Iterable [ConfigOption ]:
267
380
with path .open ("rb" ) as f :
0 commit comments