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

333 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-07-14 08:57 +0200

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 raise NotImplementedError() 

69 

70 @abc.abstractmethod 

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

72 raise NotImplementedError() 

73 

74 @abc.abstractmethod 

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

76 raise NotImplementedError() 

77 

78 @abc.abstractmethod 

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

80 ''' 

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

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

83 ''' 

84 raise NotImplementedError() 

85 

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

87 ''' 

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

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

90 This method must not be called more than once. 

91 

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

93 ''' 

94 if self.config_key: 

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

96 self.config_key = config_key 

97 

98 

99class CopyableAbstractFormatter(AbstractFormatter[T]): 

100 

101 @abc.abstractmethod 

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

103 raise NotImplementedError() 

104 

105 

106class Primitive(CopyableAbstractFormatter[T]): 

107 

108 PATTERN_ONE_OF = "one of {}" 

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

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

111 

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

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

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

115 int : '''\ 

116 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). 

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

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

119 #bool, 

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

121 } 

122 

123 

124 #: 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`. 

125 type_name: 'str|None' 

126 

127 #: The unit of a number 

128 unit: 'str|None' 

129 

130 #: :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` 

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

132 

133 #: 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. 

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

135 

136 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: 

137 ''' 

138 :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` 

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

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

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

142 ''' 

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

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

145 

146 self.type = type 

147 self.type_name = type_name 

148 self.allowed_values = allowed_values 

149 self.unit = unit 

150 

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

152 out = copy.copy(self) 

153 out.config_key = None 

154 return out 

155 

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

157 if isinstance(self.allowed_values, dict): 

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

159 if val == value: 

160 return key 

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

162 

163 if isinstance(value, str): 

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

165 

166 return format_primitive_value(value) 

167 

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

169 ''' 

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

171 ''' 

172 return format(value, format_spec) 

173 

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

175 if isinstance(self.allowed_values, dict): 

176 try: 

177 return self.allowed_values[value] 

178 except KeyError: 

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

180 elif self.type is str: 

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

182 out = typing.cast(T, value) 

183 elif self.type is int: 

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

185 elif self.type is float: 

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

187 elif self.type is bool: 

188 if value == VALUE_TRUE: 

189 out = typing.cast(T, True) 

190 elif value == VALUE_FALSE: 

191 out = typing.cast(T, False) 

192 else: 

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

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

195 for i in self.type: 

196 enum_item = typing.cast(T, i) 

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

198 out = enum_item 

199 break 

200 else: 

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

202 else: 

203 try: 

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

205 except Exception as e: 

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

207 

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

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

210 return out 

211 

212 

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

214 ''' 

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

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

217 :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. 

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

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

220 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`. 

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

222 

223 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` 

224 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`. 

225 ''' 

226 if plural: 

227 article = False 

228 

229 if not self.type_name: 

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

231 if out: 

232 return out 

233 

234 out = self.get_type_name() 

235 if self.unit: 

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

237 if article: 

238 out = self.format_indefinite_singular_article(out) 

239 return out 

240 

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

242 allowed_values = self.get_allowed_values() 

243 if not allowed_values: 

244 return None 

245 

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

247 if article: 

248 out = self.PATTERN_ONE_OF.format(out) 

249 if self.unit: 

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

251 return out 

252 

253 def get_type_name(self) -> str: 

254 ''' 

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

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

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

258 

259 :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 

260 ''' 

261 if self.type_name: 

262 return self.type_name 

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

264 

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

266 ''' 

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

268 ''' 

269 return ', '.join(names) 

270 

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

272 ''' 

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

274 Alternatively this method can be overridden. 

275 This also gives the possibility to omit the article. 

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

277 

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

279 ''' 

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

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

282 if not article: 

283 return type_name 

284 assert isinstance(article, str) 

285 return article + ' ' + type_name 

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

287 return 'an ' + type_name 

288 return 'a ' + type_name 

289 

290 

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

292 ''' 

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

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

295 

296 For example the help for an int might be: 

297 

298 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). 

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

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

301 

302 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`. 

303 

304 :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. 

305 :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`. 

306 :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`. 

307 ''' 

308 

309 if self.type_name: 

310 allowed_values = self.format_allowed_values(config_file) 

311 if not allowed_values: 

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

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

314 

315 if self.type in self.help_dict: 

316 return self.help_dict[self.type] 

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

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

319 if not isinstance(out, str): 

320 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") 

321 return out 

322 elif self.get_allowed_values(): 

323 return None 

324 else: 

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

326 

327 

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

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

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

331 return start_of_line, completions, end_of_line 

332 

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

334 if isinstance(self.allowed_values, dict): 

335 return self.allowed_values.values() 

336 if self.allowed_values: 

337 return self.allowed_values 

338 if self.type is bool: 

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

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

341 return self.type 

342 return () 

343 

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

345 return (self,) 

346 

347class Hex(Primitive[int]): 

348 

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

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

351 

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

353 return '%X' % value 

354 

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

356 return int(value, base=16) 

357 

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

359 out = 'hexadecimal number' 

360 if plural: 

361 out += 's' 

362 elif article: 

363 out = 'a ' + out 

364 return out 

365 

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

367 return None 

368 

369 

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

371 

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

373 self.item_type = item_type 

374 

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

376 return values.split(config_file.ITEM_SEP) 

377 

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

379 if config_file.ITEM_SEP in start: 

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

381 start_of_line += first + config_file.ITEM_SEP 

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

383 

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

385 return (self.item_type,) 

386 

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

388 super().set_config_key(config_key) 

389 self.item_type.set_config_key(config_key) 

390 

391 

392 # ------- expand ------ 

393 

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

395 ''' 

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

397 

398 - 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 

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

400 - 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 

401  

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

403 ''' 

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

405 if m is None: 

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

407 

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

409 func = m.group('func') 

410 if func == 'len': 

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

412 elif func: 

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

414 

415 exclude = m.group('exclude') 

416 if exclude: 

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

418 

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

420 

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

422 return format(len(values), int_format_spec) 

423 

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

425 if func == 'min': 

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

427 elif func == 'max': 

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

429 else: 

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

431 

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

433 

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

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

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

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

438 

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

440 if not item_format_spec: 

441 return self.format_value(config_file, values) 

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

443 

444class List(AbstractCollection[T]): 

445 

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

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

448 

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

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

451 

452 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 

453 ''' 

454 :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: 

455 

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

457 - 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 

458 - 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] 

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

460 

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

462 

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

464 ''' 

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

466 if m is None: 

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

468 

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

470 indices = m.group('indices') 

471 assert isinstance(indices, str) 

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

473 

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

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

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

477 

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

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

480 yield self.parse_slice(s) 

481 

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

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

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

485 i = sl[0] 

486 return slice(i, i+1) 

487 return slice(*sl) 

488 

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

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

491 

492class Set(AbstractCollection[T]): 

493 

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

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

496 

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

498 try: 

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

500 except TypeError: 

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

502 

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

504 

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

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

507 

508 

509T_key = typing.TypeVar('T_key') 

510T_val = typing.TypeVar('T_val') 

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

512 

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

514 self.key_type = key_type 

515 self.value_type = value_type 

516 

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

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

519 

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

521 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()) 

522 

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

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

525 

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

527 return values.split(config_file.ITEM_SEP) 

528 

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

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

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

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

533 return key, val 

534 

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

536 return (self.key_type, self.value_type) 

537 

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

539 if config_file.ITEM_SEP in start: 

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

541 start_of_line += first + config_file.ITEM_SEP 

542 if config_file.KEY_SEP in start: 

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

544 start_of_line += first + config_file.KEY_SEP 

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

546 

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

548 

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

550 ''' 

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

552 

553 - 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 

554 - 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 

555 - 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 

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

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

558 

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

560 ''' 

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

562 if m is None: 

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

564 

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

566 

567 key = m.group('key') 

568 if key: 

569 default = m.group('default') 

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

571 

572 keys_filter = m.group('filter') 

573 if keys_filter: 

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

575 

576 keys_select = m.group('select') 

577 if keys_select: 

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

579 

580 func = m.group('func') 

581 if func == 'len': 

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

583 

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

585 

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

587 ''' 

588 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]``. 

589 ''' 

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

591 try: 

592 v = values[parsed_key] 

593 except KeyError: 

594 if default is not None: 

595 return default 

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

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

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

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

600 

601 if not item_format_spec: 

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

603 return format(v, item_format_spec) 

604 

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

606 ''' 

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

608 ''' 

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

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

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

612 

613 def expand_select(self, config_file: 'ConfigFile', values: 'Mapping[T_key, T_val]', keys_select: 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_select_keys = {self.key_type.parse_value(config_file, key) for key in keys_select.split(',')} 

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

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

620 

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

622 ''' 

623 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 

624 ''' 

625 if not item_format_spec: 

626 return self.format_value(config_file, values) 

627 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()) 

628 

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

630 ''' 

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

632 ''' 

633 return format(len(values), int_format_spec)