Coverage for .tox/cov/lib/python3.11/site-packages/confattr/formatters.py: 100%

335 statements  

« prev     ^ index     » next       coverage.py v7.4.0, created at 2024-01-27 13:51 +0100

1#!/usr/bin/env python3 

2 

3import re 

4import copy 

5import abc 

6import enum 

7import typing 

8import builtins 

9from collections.abc import Iterable, Iterator, Sequence, Mapping, Callable 

10 

11if typing.TYPE_CHECKING: 

12 from .configfile import ConfigFile 

13 from typing_extensions import Self 

14 

15try: 

16 Collection = typing.Collection 

17except: # pragma: no cover 

18 from collections.abc import Collection 

19 

20 

21TYPES_REQUIRING_UNIT = {int, float} 

22 

23VALUE_TRUE = 'true' 

24VALUE_FALSE = 'false' 

25 

26def format_primitive_value(value: object) -> str: 

27 if isinstance(value, enum.Enum): 

28 return value.name.lower().replace('_', '-') 

29 if isinstance(value, bool): 

30 return VALUE_TRUE if value else VALUE_FALSE 

31 return str(value) 

32 

33 

34# mypy rightfully does not allow AbstractFormatter to be declared as covariant with respect to T because 

35# def format_value(self, t: AbstractFormatter[object], val: object): 

36# return t.format_value(self, val) 

37# ... 

38# config_file.format_value(Hex(), "boom") 

39# would typecheck ok but crash 

40T = typing.TypeVar('T') 

41 

42class AbstractFormatter(typing.Generic[T]): 

43 

44 ''' 

45 An abstract base class for classes which define how to parse, format and complete a value. 

46 Instances of (subclasses of this class) can be passed to the :paramref:`~confattr.config.Config.type` attribute of settings. 

47 ''' 

48 

49 config_key: 'str|None' = None 

50 

51 @abc.abstractmethod 

52 def format_value(self, config_file: 'ConfigFile', value: 'T') -> str: 

53 raise NotImplementedError() 

54 

55 @abc.abstractmethod 

56 def expand_value(self, config_file: 'ConfigFile', value: 'T', format_spec: str) -> str: 

57 ''' 

58 :param config_file: has e.g. the :attr:`~confattr.configfile.ConfigFile.ITEM_SEP` attribute 

59 :param value: The value to be formatted 

60 :param format_spec: A format specifier 

61 :return: :paramref:`~confattr.formatters.AbstractFormatter.expand_value.value` formatted according to :paramref:`~confattr.formatters.AbstractFormatter.expand_value.format_spec` 

62 :raises ValueError, LookupError: If :paramref:`~confattr.formatters.AbstractFormatter.expand_value.format_spec` is invalid 

63 ''' 

64 raise NotImplementedError() 

65 

66 @abc.abstractmethod 

67 def parse_value(self, config_file: 'ConfigFile', value: str) -> 'T': 

68 ''' 

69 :param config_file: Is needed e.g. to call :meth:`~confattr.formatters.AbstractFormatter.get_description` in error messages 

70 :param value: The value to be parsed 

71 :return: The parsed value 

72 :raises ValueError: If value cannot be parsed 

73 ''' 

74 raise NotImplementedError() 

75 

76 @abc.abstractmethod 

77 def get_description(self, config_file: 'ConfigFile') -> str: 

78 raise NotImplementedError() 

79 

80 @abc.abstractmethod 

81 def get_completions(self, config_file: 'ConfigFile', start_of_line: str, start: str, end_of_line: str) -> 'tuple[str, list[str], str]': 

82 raise NotImplementedError() 

83 

84 @abc.abstractmethod 

85 def get_primitives(self) -> 'Sequence[Primitive[typing.Any]]': 

86 ''' 

87 If self is a Primitive data type, return self. 

88 If self is a Collection, return self.item_type. 

89 ''' 

90 raise NotImplementedError() 

91 

92 def set_config_key(self, config_key: str) -> None: 

93 ''' 

94 In order to generate a useful error message if parsing a value fails the key of the setting is required. 

95 This method is called by the constructor of :class:`~confattr.config.Config`. 

96 This method must not be called more than once. 

97 

98 :raises TypeError: If :attr:`~confattr.formatters.AbstractFormatter.config_key` has already been set. 

99 ''' 

100 if self.config_key: 

101 raise TypeError(f"config_key has already been set to {self.config_key!r}, not setting to {config_key!r}") 

102 self.config_key = config_key 

103 

104 

105class CopyableAbstractFormatter(AbstractFormatter[T]): 

106 

107 @abc.abstractmethod 

108 def copy(self) -> 'Self': 

109 raise NotImplementedError() 

110 

111 

112class Primitive(CopyableAbstractFormatter[T]): 

113 

114 PATTERN_ONE_OF = "one of {}" 

115 PATTERN_ALLOWED_VALUES_UNIT = "{allowed_values} (unit: {unit})" 

116 PATTERN_TYPE_UNIT = "{type} in {unit}" 

117 

118 #: Help for data types. This is used by :meth:`~confattr.formatters.Primitive.get_help`. 

119 help_dict: 'dict[type[typing.Any]|Callable[..., typing.Any], str]' = { 

120 str : 'A text. If it contains spaces it must be wrapped in single or double quotes.', 

121 int : '''\ 

122 An integer number in python 3 syntax, as decimal (e.g. 42), hexadecimal (e.g. 0x2a), octal (e.g. 0o52) or binary (e.g. 0b101010). 

123 Leading zeroes are not permitted to avoid confusion with python 2's syntax for octal numbers. 

124 It is permissible to group digits with underscores for better readability, e.g. 1_000_000.''', 

125 #bool, 

126 float : 'A floating point number in python syntax, e.g. 23, 1.414, -1e3, 3.14_15_93.', 

127 } 

128 

129 

130 #: If this is set it is used in :meth:`~confattr.formatters.Primitive.get_description` and the list of possible values is moved to the output of :meth:`~confattr.formatters.Primitive.get_help`. 

131 type_name: 'str|None' 

132 

133 #: The unit of a number 

134 unit: 'str|None' 

135 

136 #: :class:`str`, :class:`int`, :class:`float`, :class:`bool`, a subclass of :class:`enum.Enum` or any class that follows the pattern of :class:`confattr.types.AbstractType` 

137 type: 'type[T]|Callable[..., T]' 

138 

139 #: If this is set and a value read from a config file is not contained it is considered invalid. If this is a mapping the keys are the string representations used in the config file. 

140 allowed_values: 'Collection[T]|dict[str, T]|None' 

141 

142 def __init__(self, type: 'builtins.type[T]|Callable[..., T]', *, allowed_values: 'Collection[T]|dict[str, T]|None' = None, unit: 'str|None' = None, type_name: 'str|None' = None) -> None: 

143 ''' 

144 :param type: :class:`str`, :class:`int`, :class:`float`, :class:`bool`, a subclass of :class:`enum.Enum` or any class which looks like :class:`~confattr.types.AbstractType` 

145 :param unit: The unit of an int or float value 

146 :param allowed_values: The possible values this setting can have. Values read from a config file or an environment variable are checked against this. 

147 :param type_name: A name for this type which is used in the config file. 

148 ''' 

149 if type in TYPES_REQUIRING_UNIT and unit is None and not isinstance(allowed_values, dict): 

150 raise TypeError(f"missing argument unit for {self.config_key}, pass an empty string if the number really has no unit") 

151 

152 self.type = type 

153 self.type_name = type_name 

154 self.allowed_values = allowed_values 

155 self.unit = unit 

156 

157 def copy(self) -> 'Self': 

158 out = copy.copy(self) 

159 out.config_key = None 

160 return out 

161 

162 def format_value(self, config_file: 'ConfigFile', value: 'T') -> str: 

163 if isinstance(self.allowed_values, dict): 

164 for key, val in self.allowed_values.items(): 

165 if val == value: 

166 return key 

167 raise ValueError('%r is not an allowed value, should be one of %s' % (value, ', '.join(repr(v) for v in self.allowed_values.values()))) 

168 

169 if isinstance(value, str): 

170 return value.replace('\n', r'\n') 

171 

172 return format_primitive_value(value) 

173 

174 def expand_value(self, config_file: 'ConfigFile', value: 'T', format_spec: str) -> str: 

175 ''' 

176 This method simply calls the builtin :func:`format`. 

177 ''' 

178 return format(value, format_spec) 

179 

180 def parse_value(self, config_file: 'ConfigFile', value: str) -> 'T': 

181 if isinstance(self.allowed_values, dict): 

182 try: 

183 return self.allowed_values[value] 

184 except KeyError: 

185 raise ValueError(f'invalid value for {self.config_key}: {value!r} (should be {self.get_description(config_file)})') 

186 elif self.type is str: 

187 value = value.replace(r'\n', '\n') 

188 out = typing.cast(T, value) 

189 elif self.type is int: 

190 out = typing.cast(T, int(value, base=0)) 

191 elif self.type is float: 

192 out = typing.cast(T, float(value)) 

193 elif self.type is bool: 

194 if value == VALUE_TRUE: 

195 out = typing.cast(T, True) 

196 elif value == VALUE_FALSE: 

197 out = typing.cast(T, False) 

198 else: 

199 raise ValueError(f'invalid value for {self.config_key}: {value!r} (should be {self.get_description(config_file)})') 

200 elif isinstance(self.type, type) and issubclass(self.type, enum.Enum): 

201 for i in self.type: 

202 enum_item = typing.cast(T, i) 

203 if self.format_value(config_file, enum_item) == value: 

204 out = enum_item 

205 break 

206 else: 

207 raise ValueError(f'invalid value for {self.config_key}: {value!r} (should be {self.get_description(config_file)})') 

208 else: 

209 try: 

210 out = self.type(value) # type: ignore [call-arg] 

211 except Exception as e: 

212 raise ValueError(f'invalid value for {self.config_key}: {value!r} ({e})') 

213 

214 if self.allowed_values is not None and out not in self.allowed_values: 

215 raise ValueError(f'invalid value for {self.config_key}: {value!r} (should be {self.get_description(config_file)})') 

216 return out 

217 

218 

219 def get_description(self, config_file: 'ConfigFile', *, plural: bool = False, article: bool = True) -> str: 

220 ''' 

221 :param config_file: May contain some additional information how to format the allowed values. 

222 :param plural: Whether the return value should be a plural form. 

223 :param article: Whether the return value is supposed to be formatted with :meth:`~confattr.formatters.Primitive.format_indefinite_singular_article` (if :meth:`~confattr.formatters.Primitive.get_type_name` is used) or :attr:`~confattr.formatters.Primitive.PATTERN_ONE_OF` (if :meth:`~confattr.formatters.Primitive.get_allowed_values` returns an empty sequence). This is assumed to be false if :paramref:`~confattr.formatters.Primitive.get_description.plural` is true. 

224 :return: A short description which is displayed in the help/comment for each setting explaining what kind of value is expected. 

225 In the easiest case this is just a list of allowed value, e.g. "one of true, false". 

226 If :attr:`~confattr.formatters.Primitive.type_name` has been passed to the constructor this is used instead and the list of possible values is moved to the output of :meth:`~confattr.formatters.Primitive.get_help`. 

227 If a unit is specified it is included, e.g. "an int in km/h". 

228 

229 You can customize the return value of this method by overriding :meth:`~confattr.formatters.Primitive.get_type_name`, :meth:`~confattr.formatters.Primitive.join` or :meth:`~confattr.formatters.Primitive.format_indefinite_singular_article` 

230 or by changing the value of :attr:`~confattr.formatters.Primitive.PATTERN_ONE_OF`, :attr:`~confattr.formatters.Primitive.PATTERN_ALLOWED_VALUES_UNIT` or :attr:`~confattr.formatters.Primitive.PATTERN_TYPE_UNIT`. 

231 ''' 

232 if plural: 

233 article = False 

234 

235 if not self.type_name: 

236 out = self.format_allowed_values(config_file, article=article) 

237 if out: 

238 return out 

239 

240 out = self.get_type_name() 

241 if self.unit: 

242 out = self.PATTERN_TYPE_UNIT.format(type=out, unit=self.unit) 

243 if article: 

244 out = self.format_indefinite_singular_article(out) 

245 return out 

246 

247 def format_allowed_values(self, config_file: 'ConfigFile', *, article: bool = True) -> 'str|None': 

248 allowed_values = self.get_allowed_values() 

249 if not allowed_values: 

250 return None 

251 

252 out = self.join(self.format_value(config_file, v) for v in allowed_values) 

253 if article: 

254 out = self.PATTERN_ONE_OF.format(out) 

255 if self.unit: 

256 out = self.PATTERN_ALLOWED_VALUES_UNIT.format(allowed_values=out, unit=self.unit) 

257 return out 

258 

259 def get_type_name(self) -> str: 

260 ''' 

261 Return the name of this type (without :attr:`~confattr.formatters.Primitive.unit` or :attr:`~confattr.formatters.Primitive.allowed_values`). 

262 This can be used in :meth:`~confattr.formatters.Primitive.get_description` if the type can have more than just a couple of values. 

263 If that is the case a help should be provided by :meth:`~confattr.formatters.Primitive.get_help`. 

264 

265 :return: :paramref:`~confattr.formatters.Primitive.type_name` if it has been passed to the constructor, the value of an attribute of :attr:`~confattr.formatters.Primitive.type` called ``type_name`` if existing or the lower case name of the class stored in :attr:`~confattr.formatters.Primitive.type` otherwise 

266 ''' 

267 if self.type_name: 

268 return self.type_name 

269 return getattr(self.type, 'type_name', self.type.__name__.lower()) 

270 

271 def join(self, names: 'Iterable[str]') -> str: 

272 ''' 

273 Join several values which have already been formatted with :meth:`~confattr.formatters.Primitive.format_value`. 

274 ''' 

275 return ', '.join(names) 

276 

277 def format_indefinite_singular_article(self, type_name: str) -> str: 

278 ''' 

279 Getting the article right is not so easy, so a user can specify the correct article with a str attribute called ``type_article``. 

280 Alternatively this method can be overridden. 

281 This also gives the possibility to omit the article. 

282 https://en.wiktionary.org/wiki/Appendix:English_articles#Indefinite_singular_articles 

283 

284 This is used in :meth:`~confattr.formatters.Primitive.get_description`. 

285 ''' 

286 if hasattr(self.type, 'type_article'): 

287 article = getattr(self.type, 'type_article') 

288 if not article: 

289 return type_name 

290 assert isinstance(article, str) 

291 return article + ' ' + type_name 

292 if type_name[0].lower() in 'aeio': 

293 return 'an ' + type_name 

294 return 'a ' + type_name 

295 

296 

297 def get_help(self, config_file: 'ConfigFile') -> 'str|None': 

298 ''' 

299 The help for the generic data type, independent of the unit. 

300 This is displayed once at the top of the help or the config file (if one or more settings use this type). 

301 

302 For example the help for an int might be: 

303 

304 An integer number in python 3 syntax, as decimal (e.g. 42), hexadecimal (e.g. 0x2a), octal (e.g. 0o52) or binary (e.g. 0b101010). 

305 Leading zeroes are not permitted to avoid confusion with python 2's syntax for octal numbers. 

306 It is permissible to group digits with underscores for better readability, e.g. 1_000_000. 

307 

308 Return None if (and only if) :meth:`~confattr.formatters.Primitive.get_description` returns a simple list of all possible values and not :meth:`~confattr.formatters.Primitive.get_type_name`. 

309 

310 :return: The corresponding value in :attr:`~confattr.formatters.Primitive.help_dict`, the value of an attribute called ``help`` on the :attr:`~confattr.formatters.Primitive.type` or None if the return value of :meth:`~confattr.formatters.Primitive.get_allowed_values` is empty. 

311 :raises TypeError: If the ``help`` attribute is not a str. If you have no influence over this attribute you can avoid checking it by adding a corresponding value to :attr:`~confattr.formatters.Primitive.help_dict`. 

312 :raises NotImplementedError: If there is no help or list of allowed values. If this is raised add a ``help`` attribute to the class or a value for it in :attr:`~confattr.formatters.Primitive.help_dict`. 

313 ''' 

314 

315 if self.type_name: 

316 allowed_values = self.format_allowed_values(config_file) 

317 if not allowed_values: 

318 raise NotImplementedError("used 'type_name' without 'allowed_values', please override 'get_help'") 

319 return allowed_values[:1].upper() + allowed_values[1:] 

320 

321 if self.type in self.help_dict: 

322 return self.help_dict[self.type] 

323 elif hasattr(self.type, 'help'): 

324 out = getattr(self.type, 'help') 

325 if not isinstance(out, str): 

326 raise TypeError(f"help attribute of {self.type.__name__!r} has invalid type {type(out).__name__!r}, if you cannot change that attribute please add an entry in Primitive.help_dict") 

327 return out 

328 elif self.get_allowed_values(): 

329 return None 

330 else: 

331 raise NotImplementedError('No help for type %s' % self.get_type_name()) 

332 

333 

334 def get_completions(self, config_file: 'ConfigFile', start_of_line: str, start: str, end_of_line: str) -> 'tuple[str, list[str], str]': 

335 completions = [config_file.quote(config_file.format_any_value(self, val)) for val in self.get_allowed_values()] 

336 completions = [v for v in completions if v.startswith(start)] 

337 return start_of_line, completions, end_of_line 

338 

339 def get_allowed_values(self) -> 'Collection[T]': 

340 if isinstance(self.allowed_values, dict): 

341 return self.allowed_values.values() 

342 if self.allowed_values: 

343 return self.allowed_values 

344 if self.type is bool: 

345 return (typing.cast(T, True), typing.cast(T, False)) 

346 if isinstance(self.type, type) and issubclass(self.type, enum.Enum): 

347 return self.type 

348 if hasattr(self.type, 'get_instances'): 

349 return self.type.get_instances() # type: ignore [union-attr,no-any-return] # mypy does not understand that I have just checked the existence of get_instances 

350 return () 

351 

352 def get_primitives(self) -> 'tuple[Self]': 

353 return (self,) 

354 

355class Hex(Primitive[int]): 

356 

357 def __init__(self, *, allowed_values: 'Collection[int]|None' = None) -> None: 

358 super().__init__(int, allowed_values=allowed_values, unit='') 

359 

360 def format_value(self, config_file: 'ConfigFile', value: int) -> str: 

361 return '%X' % value 

362 

363 def parse_value(self, config_file: 'ConfigFile', value: str) -> int: 

364 return int(value, base=16) 

365 

366 def get_description(self, config_file: 'ConfigFile', *, plural: bool = False, article: bool = True) -> str: 

367 out = 'hexadecimal number' 

368 if plural: 

369 out += 's' 

370 elif article: 

371 out = 'a ' + out 

372 return out 

373 

374 def get_help(self, config_file: 'ConfigFile') -> None: 

375 return None 

376 

377 

378class AbstractCollection(AbstractFormatter[Collection[T]]): 

379 

380 def __init__(self, item_type: 'Primitive[T]') -> None: 

381 self.item_type = item_type 

382 

383 def split_values(self, config_file: 'ConfigFile', values: str) -> 'Iterable[str]': 

384 return values.split(config_file.ITEM_SEP) 

385 

386 def get_completions(self, config_file: 'ConfigFile', start_of_line: str, start: str, end_of_line: str) -> 'tuple[str, list[str], str]': 

387 if config_file.ITEM_SEP in start: 

388 first, start = start.rsplit(config_file.ITEM_SEP, 1) 

389 start_of_line += first + config_file.ITEM_SEP 

390 return self.item_type.get_completions(config_file, start_of_line, start, end_of_line) 

391 

392 def get_primitives(self) -> 'tuple[Primitive[T]]': 

393 return (self.item_type,) 

394 

395 def set_config_key(self, config_key: str) -> None: 

396 super().set_config_key(config_key) 

397 self.item_type.set_config_key(config_key) 

398 

399 

400 # ------- expand ------ 

401 

402 def expand_value(self, config_file: 'ConfigFile', values: 'Collection[T]', format_spec: str) -> str: 

403 ''' 

404 :paramref:`~confattr.formatters.AbstractCollection.expand_value.format_spec` supports the following features: 

405 

406 - Filter out some values, e.g. ``-foo,bar`` expands to all items except for ``foo`` and ``bar``, it is no error if ``foo`` or ``bar`` are not contained 

407 - Get the length, ``len`` expands to the number of items 

408 - Get extreme values, ``min`` expands to the smallest item and ``max`` expands to the biggest item, raises :class:`TypeError` if the items are not comparable 

409  

410 To any of the above you can append another format_spec after a colon to specify how to format the items/the length. 

411 ''' 

412 m = re.match(r'(-(?P<exclude>[^[:]*)|(?P<func>[^[:]*))(:(?P<format_spec>.*))?$', format_spec) 

413 if m is None: 

414 raise ValueError('Invalid format_spec for collection: %r' % format_spec) 

415 

416 format_spec = m.group('format_spec') or '' 

417 func = m.group('func') 

418 if func == 'len': 

419 return self.expand_length(config_file, values, format_spec) 

420 elif func: 

421 return self.expand_min_max(config_file, values, func, format_spec) 

422 

423 exclude = m.group('exclude') 

424 if exclude: 

425 return self.expand_exclude_items(config_file, values, exclude, format_spec) 

426 

427 return self.expand_parsed_items(config_file, values, format_spec) 

428 

429 def expand_length(self, config_file: 'ConfigFile', values: 'Collection[T]', int_format_spec: str) -> str: 

430 return format(len(values), int_format_spec) 

431 

432 def expand_min_max(self, config_file: 'ConfigFile', values: 'Collection[T]', func: str, item_format_spec: str) -> str: 

433 if func == 'min': 

434 v = min(values) # type: ignore [type-var] # The TypeError is caught in ConfigFile.expand_config_match 

435 elif func == 'max': 

436 v = max(values) # type: ignore [type-var] # The TypeError is caught in ConfigFile.expand_config_match 

437 else: 

438 raise ValueError(f'Invalid format_spec for collection: {func!r}') 

439 

440 return self.expand_parsed_items(config_file, [v], item_format_spec) 

441 

442 def expand_exclude_items(self, config_file: 'ConfigFile', values: 'Collection[T]', items_to_be_excluded: str, item_format_spec: str) -> str: 

443 exclude = {self.item_type.parse_value(config_file, item) for item in items_to_be_excluded.split(',')} 

444 out = [v for v in values if v not in exclude] 

445 return self.expand_parsed_items(config_file, out, item_format_spec) 

446 

447 def expand_parsed_items(self, config_file: 'ConfigFile', values: 'Collection[T]', item_format_spec: str) -> str: 

448 if not item_format_spec: 

449 return self.format_value(config_file, values) 

450 return config_file.ITEM_SEP.join(format(v, item_format_spec) for v in values) 

451 

452class List(AbstractCollection[T]): 

453 

454 def get_description(self, config_file: 'ConfigFile') -> str: 

455 return 'a comma separated list of ' + self.item_type.get_description(config_file, plural=True) 

456 

457 def format_value(self, config_file: 'ConfigFile', values: 'Collection[T]') -> str: 

458 return config_file.ITEM_SEP.join(config_file.format_any_value(self.item_type, i) for i in values) 

459 

460 def expand_value(self, config_file: 'ConfigFile', values: 'Sequence[T]', format_spec: str) -> str: # type: ignore [override] # supertype defines the argument type as "Collection[T]", yes because type vars depending on other type vars is not supported yet https://github.com/python/typing/issues/548 

461 ''' 

462 :paramref:`~confattr.formatters.List.expand_value.format_spec` supports all features inherited from :meth:`AbstractCollection.expand_value() <confattr.formatters.AbstractCollection.expand_value>` as well as the following: 

463 

464 - Access a single item, e.g. ``[0]`` expands to the first item, ``[-1]`` expands to the last item [1] 

465 - Access several items, e.g. ``[0,2,5]`` expands to the items at index 0, 2 and 5, if the list is not that long an :class:`IndexError` is raised 

466 - Access a slice of items, e.g. ``[:3]`` expands to the first three items or to as many items as the list is long if the list is not that long [1] 

467 - Access a slice of items with a step, e.g. ``[::-1]`` expands to all items in reverse order [1] 

468 

469 To any of the above you can append another format_spec after a colon to specify how to format the items. 

470 

471 [1] For more information see the `common slicing operations of sequences <https://docs.python.org/3/library/stdtypes.html#common-sequence-operations>`__. 

472 ''' 

473 m = re.match(r'(\[(?P<indices>[^]]+)\])(:(?P<format_spec>.*))?$', format_spec) 

474 if m is None: 

475 return super().expand_value(config_file, values, format_spec) 

476 

477 format_spec = m.group('format_spec') or '' 

478 indices = m.group('indices') 

479 assert isinstance(indices, str) 

480 return self.expand_items(config_file, values, indices, format_spec) 

481 

482 def expand_items(self, config_file: 'ConfigFile', values: 'Sequence[T]', indices: str, item_format_spec: str) -> str: 

483 out = [v for sl in self.parse_slices(indices) for v in values[sl]] 

484 return self.expand_parsed_items(config_file, out, item_format_spec) 

485 

486 def parse_slices(self, indices: str) -> 'Iterator[slice]': 

487 for s in indices.split(','): 

488 yield self.parse_slice(s) 

489 

490 def parse_slice(self, s: str) -> 'slice': 

491 sl = [int(i) if i else None for i in s.split(':')] 

492 if len(sl) == 1 and isinstance(sl[0], int): 

493 i = sl[0] 

494 return slice(i, i+1) 

495 return slice(*sl) 

496 

497 def parse_value(self, config_file: 'ConfigFile', values: str) -> 'list[T]': 

498 return [self.item_type.parse_value(config_file, i) for i in self.split_values(config_file, values)] 

499 

500class Set(AbstractCollection[T]): 

501 

502 def get_description(self, config_file: 'ConfigFile') -> str: 

503 return 'a comma separated set of ' + self.item_type.get_description(config_file, plural=True) 

504 

505 def format_value(self, config_file: 'ConfigFile', values: 'Collection[T]') -> str: 

506 try: 

507 sorted_values = sorted(values) # type: ignore [type-var] # values may be not comparable but that's what the try/except is there for 

508 except TypeError: 

509 return config_file.ITEM_SEP.join(sorted(config_file.format_any_value(self.item_type, i) for i in values)) 

510 

511 return config_file.ITEM_SEP.join(config_file.format_any_value(self.item_type, i) for i in sorted_values) 

512 

513 def parse_value(self, config_file: 'ConfigFile', values: str) -> 'set[T]': 

514 return {self.item_type.parse_value(config_file, i) for i in self.split_values(config_file, values)} 

515 

516 

517T_key = typing.TypeVar('T_key') 

518T_val = typing.TypeVar('T_val') 

519class Dict(AbstractFormatter['dict[T_key, T_val]']): 

520 

521 def __init__(self, key_type: 'Primitive[T_key]', value_type: 'Primitive[T_val]') -> None: 

522 self.key_type = key_type 

523 self.value_type = value_type 

524 

525 def get_description(self, config_file: 'ConfigFile') -> str: 

526 return 'a dict of %s:%s' % (self.key_type.get_description(config_file, article=False), self.value_type.get_description(config_file, article=False)) 

527 

528 def format_value(self, config_file: 'ConfigFile', values: 'Mapping[T_key, T_val]') -> str: 

529 return config_file.ITEM_SEP.join(config_file.format_any_value(self.key_type, key) + config_file.KEY_SEP + config_file.format_any_value(self.value_type, val) for key, val in values.items()) 

530 

531 def parse_value(self, config_file: 'ConfigFile', values: str) -> 'dict[T_key, T_val]': 

532 return dict(self.parse_item(config_file, i) for i in self.split_values(config_file, values)) 

533 

534 def split_values(self, config_file: 'ConfigFile', values: str) -> 'Iterable[str]': 

535 return values.split(config_file.ITEM_SEP) 

536 

537 def parse_item(self, config_file: 'ConfigFile', item: str) -> 'tuple[T_key, T_val]': 

538 key_name, val_name = item.split(config_file.KEY_SEP, 1) 

539 key = self.key_type.parse_value(config_file, key_name) 

540 val = self.value_type.parse_value(config_file, val_name) 

541 return key, val 

542 

543 def get_primitives(self) -> 'tuple[Primitive[T_key], Primitive[T_val]]': 

544 return (self.key_type, self.value_type) 

545 

546 def get_completions(self, config_file: 'ConfigFile', start_of_line: str, start: str, end_of_line: str) -> 'tuple[str, list[str], str]': 

547 if config_file.ITEM_SEP in start: 

548 first, start = start.rsplit(config_file.ITEM_SEP, 1) 

549 start_of_line += first + config_file.ITEM_SEP 

550 if config_file.KEY_SEP in start: 

551 first, start = start.rsplit(config_file.KEY_SEP, 1) 

552 start_of_line += first + config_file.KEY_SEP 

553 return self.value_type.get_completions(config_file, start_of_line, start, end_of_line) 

554 

555 return self.key_type.get_completions(config_file, start_of_line, start, end_of_line) 

556 

557 def expand_value(self, config_file: 'ConfigFile', values: 'Mapping[T_key, T_val]', format_spec: str) -> str: 

558 ''' 

559 :paramref:`~confattr.formatters.Dict.expand_value.format_spec` supports the following features: 

560 

561 - Get a single value, e.g. ``[key1]`` expands to the value corresponding to ``key1``, a :class:`KeyError` is raised if ``key1`` is not contained in the dict 

562 - Get a single value or a default value, e.g. ``[key1|default]`` expands to the value corresponding to ``key1`` or to ``default`` if ``key1`` is not contained 

563 - Get values with their corresponding keys, e.g. ``{key1,key2}`` expands to ``key1:val1,key2:val2``, if a key is not contained it is skipped 

564 - Filter out elements, e.g. ``{^key1}`` expands to all ``key:val`` pairs except for ``key1`` 

565 - Get the length, ``len`` expands to the number of items 

566 

567 To any of the above you can append another format_spec after a colon to specify how to format the items/the length. 

568 ''' 

569 m = re.match(r'(\[(?P<key>[^]|]+)(\|(?P<default>[^]]+))?\]|\{\^(?P<filter>[^}]+)\}|\{(?P<select>[^}]*)\}|(?P<func>[^[{:]+))(:(?P<format_spec>.*))?$', format_spec) 

570 if m is None: 

571 raise ValueError('Invalid format_spec for dict: %r' % format_spec) 

572 

573 item_format_spec = m.group('format_spec') or '' 

574 

575 key = m.group('key') 

576 if key: 

577 default = m.group('default') 

578 return self.expand_single_value(config_file, values, key, default, item_format_spec) 

579 

580 keys_filter = m.group('filter') 

581 if keys_filter: 

582 return self.expand_filter(config_file, values, keys_filter, item_format_spec) 

583 

584 keys_select = m.group('select') 

585 if keys_select: 

586 return self.expand_select(config_file, values, keys_select, item_format_spec) 

587 

588 func = m.group('func') 

589 if func == 'len': 

590 return self.expand_length(config_file, values, item_format_spec) 

591 

592 raise ValueError('Invalid format_spec for dict: %r' % format_spec) 

593 

594 def expand_single_value(self, config_file: 'ConfigFile', values: 'Mapping[T_key, T_val]', key: str, default: 'str|None', item_format_spec: str) -> str: 

595 ''' 

596 Is called by :meth:`~confattr.formatters.Dict.expand_value` if :paramref:`~confattr.formatters.Dict.expand_value.format_spec` has the pattern ``[key]`` or ``[key|default]``. 

597 ''' 

598 parsed_key = self.key_type.parse_value(config_file, key) 

599 try: 

600 v = values[parsed_key] 

601 except KeyError: 

602 if default is not None: 

603 return default 

604 # The message of a KeyError is the repr of the missing key, nothing more. 

605 # Therefore I am raising a new exception with a more descriptive message. 

606 # I am not using KeyError because that takes the repr of the argument. 

607 raise LookupError(f"key {key!r} is not contained in {self.config_key!r}") 

608 

609 if not item_format_spec: 

610 return self.value_type.format_value(config_file, v) 

611 return format(v, item_format_spec) 

612 

613 def expand_filter(self, config_file: 'ConfigFile', values: 'Mapping[T_key, T_val]', keys_filter: str, item_format_spec: str) -> str: 

614 ''' 

615 Is called by :meth:`~confattr.formatters.Dict.expand_value` if :paramref:`~confattr.formatters.Dict.expand_value.format_spec` has the pattern ``{^key1,key2}``. 

616 ''' 

617 parsed_filter_keys = {self.key_type.parse_value(config_file, key) for key in keys_filter.split(',')} 

618 values = {k:v for k,v in values.items() if k not in parsed_filter_keys} 

619 return self.expand_selected(config_file, values, item_format_spec) 

620 

621 def expand_select(self, config_file: 'ConfigFile', values: 'Mapping[T_key, T_val]', keys_select: str, item_format_spec: str) -> str: 

622 ''' 

623 Is called by :meth:`~confattr.formatters.Dict.expand_value` if :paramref:`~confattr.formatters.Dict.expand_value.format_spec` has the pattern ``{key1,key2}``. 

624 ''' 

625 parsed_select_keys = {self.key_type.parse_value(config_file, key) for key in keys_select.split(',')} 

626 values = {k:v for k,v in values.items() if k in parsed_select_keys} 

627 return self.expand_selected(config_file, values, item_format_spec) 

628 

629 def expand_selected(self, config_file: 'ConfigFile', values: 'Mapping[T_key, T_val]', item_format_spec: str) -> str: 

630 ''' 

631 Is called by :meth:`~confattr.formatters.Dict.expand_filter` and :meth:`~confattr.formatters.Dict.expand_select` to do the formatting of the filtered/selected values 

632 ''' 

633 if not item_format_spec: 

634 return self.format_value(config_file, values) 

635 return config_file.ITEM_SEP.join(self.key_type.format_value(config_file, k) + config_file.KEY_SEP + format(v, item_format_spec) for k, v in values.items()) 

636 

637 def expand_length(self, config_file: 'ConfigFile', values: 'Collection[T]', int_format_spec: str) -> str: 

638 ''' 

639 Is called by :meth:`~confattr.formatters.Dict.expand_value` if :paramref:`~confattr.formatters.Dict.expand_value.format_spec` is ``len``. 

640 ''' 

641 return format(len(values), int_format_spec)