Coverage for yuio / parse.py: 90%

1830 statements  

« prev     ^ index     » next       coverage.py v7.13.4, created at 2026-03-29 19:55 +0000

1# Yuio project, MIT license. 

2# 

3# https://github.com/taminomara/yuio/ 

4# 

5# You're free to copy this file to your project and edit it for your needs, 

6# just keep this copyright line please :3 

7 

8""" 

9Everything to do with parsing user input. 

10 

11Use provided classes to construct parsers and add validation:: 

12 

13 >>> # Parses a string that matches the given regex. 

14 >>> ident = Regex(Str(), r'^[a-zA-Z_][a-zA-Z0-9_]*$') 

15 

16 >>> # Parses a non-empty list of strings. 

17 >>> idents = LenGe(List(ident), 1) 

18 

19Pass a parser to other yuio functions:: 

20 

21 >>> yuio.io.ask('List of modules to reformat', parser=idents) # doctest: +SKIP 

22 

23Or parse strings yourself:: 

24 

25 >>> idents.parse('sys os enum dataclasses') 

26 ['sys', 'os', 'enum', 'dataclasses'] 

27 

28Build a parser from type hints:: 

29 

30 >>> from_type_hint(list[int] | None) 

31 Optional(List(Int)) 

32 

33 

34Parser basics 

35------------- 

36 

37All parsers are derived from the same base class :class:`Parser`, 

38which describes parsing API. 

39 

40.. autoclass:: Parser 

41 

42 .. automethod:: parse 

43 

44 .. automethod:: parse_many 

45 

46 .. automethod:: supports_parse_many 

47 

48 .. automethod:: parse_config 

49 

50.. autoclass:: ParsingError 

51 :members: 

52 

53 

54Value parsers 

55------------- 

56 

57.. autoclass:: Str 

58 

59.. autoclass:: Int 

60 

61.. autoclass:: Float 

62 

63.. autoclass:: Bool 

64 

65.. autoclass:: Enum 

66 

67.. autoclass:: Literal 

68 

69.. autoclass:: Decimal 

70 

71.. autoclass:: Fraction 

72 

73.. autoclass:: DateTime 

74 

75.. autoclass:: Date 

76 

77.. autoclass:: Time 

78 

79.. autoclass:: TimeDelta 

80 

81.. autoclass:: Seconds 

82 

83.. autoclass:: Json 

84 

85.. autoclass:: List 

86 

87.. autoclass:: Set 

88 

89.. autoclass:: FrozenSet 

90 

91.. autoclass:: Dict 

92 

93.. autoclass:: Tuple 

94 

95.. autoclass:: Optional 

96 

97.. autoclass:: Union 

98 

99.. autoclass:: Path 

100 

101.. autoclass:: NonExistentPath 

102 

103.. autoclass:: ExistingPath 

104 

105.. autoclass:: File 

106 

107.. autoclass:: Dir 

108 

109.. autoclass:: GitRepo 

110 

111.. autoclass:: Secret 

112 

113 

114.. _validating-parsers: 

115 

116Validators 

117---------- 

118 

119.. autoclass:: Regex 

120 

121.. autoclass:: Bound 

122 

123.. autoclass:: Gt 

124 

125.. autoclass:: Ge 

126 

127.. autoclass:: Lt 

128 

129.. autoclass:: Le 

130 

131.. autoclass:: LenBound 

132 

133.. autoclass:: LenGt 

134 

135.. autoclass:: LenGe 

136 

137.. autoclass:: LenLt 

138 

139.. autoclass:: LenLe 

140 

141.. autoclass:: OneOf 

142 

143 

144Auxiliary parsers 

145----------------- 

146 

147.. autoclass:: Map 

148 

149.. autoclass:: Apply 

150 

151.. autoclass:: Lower 

152 

153.. autoclass:: Upper 

154 

155.. autoclass:: CaseFold 

156 

157.. autoclass:: Strip 

158 

159.. autoclass:: WithMeta 

160 

161 

162Deriving parsers from type hints 

163-------------------------------- 

164 

165There is a way to automatically derive basic parsers from type hints 

166(used by :mod:`yuio.config`): 

167 

168.. autofunction:: from_type_hint 

169 

170 

171.. _partial parsers: 

172 

173Partial parsers 

174--------------- 

175 

176Sometimes it's not convenient to provide a parser for a complex type when 

177all we need is to make a small adjustment to a part of the type. For example: 

178 

179.. invisible-code-block: python 

180 

181 from yuio.config import Config, field 

182 

183.. code-block:: python 

184 

185 class AppConfig(Config): 

186 max_line_width: int | str = field( 

187 default="default", 

188 parser=Union( 

189 Gt(Int(), 0), 

190 OneOf(Str(), ["default", "unlimited", "keep"]), 

191 ), 

192 ) 

193 

194.. invisible-code-block: python 

195 

196 AppConfig() 

197 

198Instead, we can use :obj:`typing.Annotated` to attach validating parsers directly 

199to type hints: 

200 

201.. code-block:: python 

202 

203 from typing import Annotated 

204 

205 

206 class AppConfig(Config): 

207 max_line_width: ( 

208 Annotated[int, Gt(0)] 

209 | Annotated[str, OneOf(["default", "unlimited", "keep"])] 

210 ) = "default" 

211 

212.. invisible-code-block: python 

213 

214 AppConfig() 

215 

216Notice that we didn't specify inner parsers for :class:`Gt` and :class:`OneOf`. 

217This is because their internal parsers are derived from type hint, so we only care 

218about their settings. 

219 

220Parsers created in such a way are called "partial". You can't use a partial parser 

221on its own because it doesn't have full information about the object's type. 

222You can only use partial parsers in type hints:: 

223 

224 >>> partial_parser = List(delimiter=",") 

225 >>> partial_parser.parse_with_ctx("1,2,3") # doctest: +ELLIPSIS 

226 Traceback (most recent call last): 

227 ... 

228 TypeError: List requires an inner parser 

229 ... 

230 

231 

232Other parser methods 

233-------------------- 

234 

235:class:`Parser` defines some more methods and attributes. 

236They're rarely used because Yuio handles everything they do itself. 

237However, you can still use them in case you need to. 

238 

239.. autoclass:: Parser 

240 :noindex: 

241 

242 .. autoattribute:: __wrapped_parser__ 

243 

244 .. automethod:: parse_with_ctx 

245 

246 .. automethod:: parse_many_with_ctx 

247 

248 .. automethod:: parse_config_with_ctx 

249 

250 .. automethod:: get_nargs 

251 

252 .. automethod:: check_type 

253 

254 .. automethod:: assert_type 

255 

256 .. automethod:: describe 

257 

258 .. automethod:: describe_or_def 

259 

260 .. automethod:: describe_many 

261 

262 .. automethod:: describe_value 

263 

264 .. automethod:: options 

265 

266 .. automethod:: completer 

267 

268 .. automethod:: widget 

269 

270 .. automethod:: to_json_schema 

271 

272 .. automethod:: to_json_value 

273 

274 .. automethod:: is_secret 

275 

276 

277Building your own parser 

278------------------------ 

279 

280.. _parser hierarchy: 

281 

282Understanding parser hierarchy 

283~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 

284 

285The topmost class in the parser hierarchy is :class:`PartialParser`. It provides 

286abstract methods to deal with `partial parsers`_. The primary parser interface, 

287:class:`Parser`, is derived from it. Below :class:`Parser`, there are several 

288abstract classes that provide boilerplate implementations for common use cases. 

289 

290.. raw:: html 

291 

292 <p> 

293 <pre class="mermaid"> 

294 --- 

295 config: 

296 class: 

297 hideEmptyMembersBox: true 

298 --- 

299 classDiagram 

300 

301 class PartialParser 

302 click PartialParser href "#yuio.parse.PartialParser" "yuio.parse.PartialParser" 

303 

304 class Parser 

305 click Parser href "#yuio.parse.Parser" "yuio.parse.Parser" 

306 PartialParser <|-- Parser 

307 

308 class ValueParser 

309 click ValueParser href "#yuio.parse.ValueParser" "yuio.parse.ValueParser" 

310 Parser <|-- ValueParser 

311 

312 class WrappingParser 

313 click WrappingParser href "#yuio.parse.WrappingParser" "yuio.parse.WrappingParser" 

314 Parser <|-- WrappingParser 

315 

316 class MappingParser 

317 click MappingParser href "#yuio.parse.MappingParser" "yuio.parse.MappingParser" 

318 WrappingParser <|-- MappingParser 

319 

320 class Map 

321 click Map href "#yuio.parse.Map" "yuio.parse.Map" 

322 MappingParser <|-- Map 

323 

324 class Apply 

325 click Apply href "#yuio.parse.Apply" "yuio.parse.Apply" 

326 MappingParser <|-- Apply 

327 

328 class ValidatingParser 

329 click ValidatingParser href "#yuio.parse.ValidatingParser" "yuio.parse.ValidatingParser" 

330 Apply <|-- ValidatingParser 

331 

332 class CollectionParser 

333 click CollectionParser href "#yuio.parse.CollectionParser" "yuio.parse.CollectionParser" 

334 ValueParser <|-- CollectionParser 

335 WrappingParser <|-- CollectionParser 

336 </pre> 

337 </p> 

338 

339The reason for separation of :class:`PartialParser` and :class:`Parser` 

340is better type checking. We want to prevent users from making a mistake of providing 

341a partial parser to a function that expect a fully initialized parser. For example, 

342consider this code: 

343 

344.. skip: next 

345 

346.. code-block:: python 

347 

348 yuio.io.ask("Enter some names", parser=List()) 

349 

350This will fail because :class:`~List` needs an inner parser to function. 

351 

352To annotate this behavior, we provide type hints for ``__new__`` methods 

353on each parser. When an inner parser is given, ``__new__`` is annotated as 

354returning an instance of :class:`Parser`. When inner parser is omitted, 

355``__new__`` is annotated as returning an instance of :class:`PartialParser`: 

356 

357.. skip: next 

358 

359.. code-block:: python 

360 

361 from typing import TYPE_CHECKING, Any, Generic, overload 

362 

363 class List(..., Generic[T]): 

364 if TYPE_CHECKING: 

365 @overload 

366 def __new__(cls, delimiter: str | None = None) -> PartialParser: 

367 ... 

368 @overload 

369 def __new__(cls, inner: Parser[T], delimiter: str | None = None) -> PartialParser: 

370 ... 

371 def __new__(cls, *args, **kwargs) -> Any: 

372 ... 

373 

374With these type hints, our example will fail to type check: :func:`yuio.io.ask` 

375expects a :class:`Parser`, but ``List.__new__`` returns a :class:`PartialParser`. 

376 

377Unfortunately, this means that all parsers derived from :class:`WrappingParser` 

378must provide appropriate type hints for their ``__new__`` method. 

379 

380.. autoclass:: PartialParser 

381 :members: 

382 

383 

384Parsing contexts 

385~~~~~~~~~~~~~~~~ 

386 

387To track location of errors, parsers work with parsing context: 

388:class:`StrParsingContext` for parsing raw strings, and :class:`ConfigParsingContext` 

389for parsing configs. 

390 

391When raising a :class:`ParsingError`, pass context to it so that we can show error 

392location to the user. 

393 

394.. autoclass:: StrParsingContext 

395 :members: 

396 

397.. autoclass:: ConfigParsingContext 

398 :members: 

399 

400 

401Base classes 

402~~~~~~~~~~~~ 

403 

404.. autoclass:: ValueParser 

405 

406.. autoclass:: WrappingParser 

407 

408 .. autoattribute:: _inner 

409 

410 .. autoattribute:: _inner_raw 

411 

412.. autoclass:: MappingParser 

413 

414.. autoclass:: ValidatingParser 

415 

416 .. autoattribute:: __wrapped_parser__ 

417 :noindex: 

418 

419 .. automethod:: _validate 

420 

421.. autoclass:: CollectionParser 

422 

423 .. autoattribute:: _allow_completing_duplicates 

424 

425 

426Adding type hint conversions 

427~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 

428 

429You can register a converter so that :func:`from_type_hint` can derive custom 

430parsers from type hints: 

431 

432.. autofunction:: register_type_hint_conversion(cb: Cb) -> Cb 

433 

434When implementing a callback, you might need to specify a delimiter 

435for a collection parser. Use :func:`suggest_delim_for_type_hint_conversion`: 

436 

437.. autofunction:: suggest_delim_for_type_hint_conversion 

438 

439 

440Re-imports 

441---------- 

442 

443.. type:: JsonValue 

444 :no-index: 

445 

446 Alias of :obj:`yuio.json_schema.JsonValue`. 

447 

448.. type:: SecretString 

449 :no-index: 

450 

451 Alias of :obj:`yuio.secret.SecretString`. 

452 

453.. type:: SecretValue 

454 :no-index: 

455 

456 Alias of :obj:`yuio.secret.SecretValue`. 

457 

458""" 

459 

460from __future__ import annotations 

461 

462import abc 

463import argparse 

464import contextlib 

465import dataclasses 

466import datetime 

467import decimal 

468import enum 

469import fractions 

470import functools 

471import json 

472import pathlib 

473import re 

474import threading 

475import traceback 

476from copy import copy as _copy 

477 

478import yuio 

479import yuio.color 

480import yuio.complete 

481import yuio.json_schema 

482import yuio.string 

483import yuio.widget 

484from yuio.json_schema import JsonValue 

485from yuio.secret import SecretString, SecretValue 

486from yuio.util import find_docs as _find_docs 

487from yuio.util import to_dash_case as _to_dash_case 

488 

489import typing 

490import yuio._typing_ext as _tx 

491from typing import TYPE_CHECKING 

492 

493if TYPE_CHECKING: 

494 import typing_extensions as _t 

495else: 

496 from yuio import _typing as _t 

497 

498__all__ = [ 

499 "Apply", 

500 "Bool", 

501 "Bound", 

502 "CaseFold", 

503 "CollectionParser", 

504 "ConfigParsingContext", 

505 "Date", 

506 "DateTime", 

507 "Decimal", 

508 "Dict", 

509 "Dir", 

510 "Enum", 

511 "ExistingPath", 

512 "File", 

513 "Float", 

514 "Fraction", 

515 "FrozenSet", 

516 "Ge", 

517 "GitRepo", 

518 "Gt", 

519 "Int", 

520 "Json", 

521 "JsonValue", 

522 "Le", 

523 "LenBound", 

524 "LenGe", 

525 "LenGt", 

526 "LenLe", 

527 "LenLt", 

528 "List", 

529 "Literal", 

530 "Lower", 

531 "Lt", 

532 "Map", 

533 "MappingParser", 

534 "NonExistentPath", 

535 "OneOf", 

536 "Optional", 

537 "Parser", 

538 "ParsingError", 

539 "PartialParser", 

540 "Path", 

541 "Regex", 

542 "Seconds", 

543 "Secret", 

544 "SecretString", 

545 "SecretValue", 

546 "Set", 

547 "Str", 

548 "StrParsingContext", 

549 "Strip", 

550 "Time", 

551 "TimeDelta", 

552 "Tuple", 

553 "Union", 

554 "Upper", 

555 "ValidatingParser", 

556 "ValueParser", 

557 "WithMeta", 

558 "WrappingParser", 

559 "from_type_hint", 

560 "register_type_hint_conversion", 

561 "suggest_delim_for_type_hint_conversion", 

562] 

563 

564T_co = _t.TypeVar("T_co", covariant=True) 

565T = _t.TypeVar("T") 

566U = _t.TypeVar("U") 

567K = _t.TypeVar("K") 

568V = _t.TypeVar("V") 

569C = _t.TypeVar("C", bound=_t.Collection[object]) 

570C2 = _t.TypeVar("C2", bound=_t.Collection[object]) 

571Sz = _t.TypeVar("Sz", bound=_t.Sized) 

572Cmp = _t.TypeVar("Cmp", bound=_tx.SupportsLt[_t.Any]) 

573E = _t.TypeVar("E", bound=enum.Enum) 

574L = _t.TypeVar("L", bound=enum.Enum | int | str | bool | None) 

575TU = _t.TypeVar("TU", bound=tuple[object, ...]) 

576P = _t.TypeVar("P", bound="Parser[_t.Any]") 

577Params = _t.ParamSpec("Params") 

578 

579 

580class ParsingError(yuio.PrettyException, ValueError, argparse.ArgumentTypeError): 

581 """PrettyException(msg: typing.LiteralString, /, *args: typing.Any, ctx: ConfigParsingContext | StrParsingContext | None = None, fallback_msg: typing.LiteralString | None = None, **kwargs) 

582 PrettyException(msg: str, /, *, ctx: ConfigParsingContext | StrParsingContext | None = None, fallback_msg: typing.LiteralString | None = None, **kwargs) 

583 

584 Raised when parsing or validation fails. 

585 

586 :param msg: 

587 message to format. Can be a literal string or any other colorable object. 

588 

589 If it's given as a literal string, additional arguments for ``%``-formatting 

590 may be given. Otherwise, giving additional arguments will cause 

591 a :class:`TypeError`. 

592 :param args: 

593 arguments for ``%``-formatting the message. 

594 :param fallback_msg: 

595 fallback message that's guaranteed not to include representation of the faulty 

596 value, will replace `msg` when parsing secret values. 

597 

598 .. warning:: 

599 

600 This parameter must not include contents of the faulty value. It is typed 

601 as :class:`~typing.LiteralString` as a deterrent; if you need string 

602 interpolation, create an instance of :class:`ParsingError` and set 

603 :attr:`~ParsingError.fallback_msg` directly. 

604 :param ctx: 

605 current error context that will be used to set :attr:`~ParsingError.raw`, 

606 :attr:`~ParsingError.pos`, and other attributes. 

607 :param kwargs: 

608 other keyword arguments set :attr:`~ParsingError.raw`, 

609 :attr:`~ParsingError.pos`, :attr:`~ParsingError.n_arg`, 

610 :attr:`~ParsingError.path`. 

611 

612 """ 

613 

614 @_t.overload 

615 def __init__( 

616 self, 

617 msg: _t.LiteralString, 

618 /, 

619 *args, 

620 fallback_msg: _t.LiteralString | None = None, 

621 ctx: ConfigParsingContext | StrParsingContext | None = None, 

622 raw: str | None = None, 

623 pos: tuple[int, int] | None = None, 

624 n_arg: int | None = None, 

625 path: list[tuple[_t.Any, str | None]] | None = None, 

626 ): ... 

627 @_t.overload 

628 def __init__( 

629 self, 

630 msg: yuio.string.ToColorable | None | yuio.Missing = yuio.MISSING, 

631 /, 

632 *, 

633 fallback_msg: _t.LiteralString | None = None, 

634 ctx: ConfigParsingContext | StrParsingContext | None = None, 

635 raw: str | None = None, 

636 pos: tuple[int, int] | None = None, 

637 n_arg: int | None = None, 

638 path: list[tuple[_t.Any, str | None]] | None = None, 

639 ): ... 

640 def __init__( 

641 self, 

642 *args, 

643 fallback_msg: _t.LiteralString | None = None, 

644 ctx: ConfigParsingContext | StrParsingContext | None = None, 

645 raw: str | None = None, 

646 pos: tuple[int, int] | None = None, 

647 n_arg: int | None = None, 

648 path: list[tuple[_t.Any, str | None]] | None = None, 

649 ): 

650 super().__init__(*args) 

651 

652 if ctx: 

653 if isinstance(ctx, ConfigParsingContext): 

654 path = path if path is not None else ctx.make_path() 

655 else: 

656 raw = raw if raw is not None else ctx.content 

657 pos = pos if pos is not None else (ctx.start, ctx.end) 

658 n_arg = n_arg if n_arg is not None else ctx.n_arg 

659 

660 self.fallback_msg: yuio.string.Colorable | None = fallback_msg 

661 """ 

662 This message will be used if error occurred while parsing a secret value. 

663 

664 .. warning:: 

665 

666 This colorable must not include contents of the faulty value. 

667 

668 """ 

669 

670 self.raw: str | None = raw 

671 """ 

672 For errors that happened when parsing a string, this attribute contains the 

673 original string. 

674 

675 """ 

676 

677 self.pos: tuple[int, int] | None = pos 

678 """ 

679 For errors that happened when parsing a string, this attribute contains 

680 position in the original string in which this error has occurred (start 

681 and end indices). 

682 

683 """ 

684 

685 self.n_arg: int | None = n_arg 

686 """ 

687 For errors that happened in :meth:`~Parser.parse_many`, this attribute contains 

688 index of the string in which this error has occurred. 

689 

690 """ 

691 

692 self.path: list[tuple[_t.Any, str | None]] | None = path 

693 """ 

694 For errors that happened in :meth:`~Parser.parse_config_with_ctx`, this attribute 

695 contains path to the value in which this error has occurred. 

696 

697 """ 

698 

699 @classmethod 

700 def type_mismatch( 

701 cls, 

702 value: _t.Any, 

703 /, 

704 *expected: type | str, 

705 ctx: ConfigParsingContext | StrParsingContext | None = None, 

706 raw: str | None = None, 

707 pos: tuple[int, int] | None = None, 

708 n_arg: int | None = None, 

709 path: list[tuple[_t.Any, str | None]] | None = None, 

710 ): 

711 """type_mismatch(value: _t.Any, /, *expected: type | str, **kwargs) 

712 

713 Make an error with a standard message "expected type X, got type Y". 

714 

715 :param value: 

716 value of an unexpected type. 

717 :param expected: 

718 expected types. Each argument can be a type or a string that describes 

719 a type. 

720 :param kwargs: 

721 keyword arguments will be passed to constructor. 

722 :example: 

723 :: 

724 

725 >>> raise ParsingError.type_mismatch(10, str) 

726 Traceback (most recent call last): 

727 ... 

728 yuio.parse.ParsingError: Expected str, got int: 10 

729 

730 """ 

731 

732 err = cls( 

733 "Expected %s, got `%s`: `%r`", 

734 yuio.string.Or(map(yuio.string.TypeRepr, expected)), 

735 yuio.string.TypeRepr(type(value)), 

736 value, 

737 ctx=ctx, 

738 raw=raw, 

739 pos=pos, 

740 n_arg=n_arg, 

741 path=path, 

742 ) 

743 err.fallback_msg = yuio.string.Format( 

744 "Expected %s, got `%s`", 

745 yuio.string.Or(map(yuio.string.TypeRepr, expected)), 

746 yuio.string.TypeRepr(type(value)), 

747 ) 

748 

749 return err 

750 

751 def set_ctx(self, ctx: ConfigParsingContext | StrParsingContext): 

752 if isinstance(ctx, ConfigParsingContext): 

753 self.path = ctx.make_path() 

754 else: 

755 self.raw = ctx.content 

756 self.pos = (ctx.start, ctx.end) 

757 self.n_arg = ctx.n_arg 

758 

759 def to_colorable(self) -> yuio.string.Colorable: 

760 colorable = super().to_colorable() 

761 if self.path: 

762 colorable = yuio.string.Format( 

763 "In `%s`:\n%s", 

764 _PathRenderer(self.path), 

765 yuio.string.Indent(colorable), 

766 ) 

767 if self.pos and self.raw and self.pos != (0, len(self.raw)): 

768 raw, pos = _repr_and_adjust_pos(self.raw, self.pos) 

769 colorable = yuio.string.Stack( 

770 _CodeRenderer(raw, pos), 

771 colorable, 

772 ) 

773 return colorable 

774 

775 

776class PartialParser(abc.ABC): 

777 """ 

778 An interface of a partial parser. 

779 

780 """ 

781 

782 def __init__(self): 

783 self.__orig_traceback = traceback.extract_stack() 

784 while self.__orig_traceback and self.__orig_traceback[-1].filename.endswith( 

785 "yuio/parse.py" 

786 ): 

787 self.__orig_traceback.pop() 

788 super().__init__() 

789 

790 def _get_orig_traceback(self) -> traceback.StackSummary: 

791 """ 

792 Get stack summary for the place where this partial parser was created. 

793 

794 """ 

795 

796 return self.__orig_traceback # pragma: no cover 

797 

798 @contextlib.contextmanager 

799 def _patch_stack_summary(self): 

800 """ 

801 Attach original traceback to any exception that's raised 

802 within this context manager. 

803 

804 """ 

805 

806 try: 

807 yield 

808 except Exception as e: 

809 stack_summary_text = "Traceback (most recent call last):\n" + "".join( 

810 self.__orig_traceback.format() 

811 ) 

812 e.args = ( 

813 f"{e}\n\nThe above error happened because of " 

814 f"this type hint:\n\n{stack_summary_text}", 

815 ) 

816 setattr(e, "__yuio_stack_summary_text__", stack_summary_text) 

817 raise e 

818 

819 @abc.abstractmethod 

820 def wrap(self, parser: Parser[_t.Any]) -> Parser[_t.Any]: 

821 """ 

822 Apply this partial parser. 

823 

824 When Yuio checks type annotations, it derives a parser for the given type hint, 

825 and the applies all partial parsers to it. 

826 

827 For example, given this type hint: 

828 

829 .. invisible-code-block: python 

830 

831 from typing import Annotated 

832 

833 .. code-block:: python 

834 

835 field: Annotated[str, Map(str.lower)] 

836 

837 Yuio will first infer parser for string (:class:`Str`), then it will pass 

838 this parser to ``Map.wrap``. 

839 

840 :param parser: 

841 a parser instance that was created by inspecting type hints 

842 and previous annotations. 

843 :returns: 

844 a result of upgrading this parser from partial to full. This method 

845 usually returns copy of `self`. 

846 :raises: 

847 :class:`TypeError` if this parser can't be wrapped. Specifically, this 

848 method should raise a :class:`TypeError` for any non-partial parser. 

849 

850 """ 

851 

852 return _copy(self) # pyright: ignore[reportReturnType] 

853 

854 

855class Parser(PartialParser, _t.Generic[T_co]): 

856 """ 

857 Base class for parsers. 

858 

859 """ 

860 

861 # Original type hint from which this parser was derived. 

862 __typehint: _t.Any = None 

863 

864 __wrapped_parser__: Parser[object] | None = None 

865 """ 

866 An attribute for unwrapping parsers that validate or map results 

867 of other parsers. 

868 

869 """ 

870 

871 @_t.final 

872 def parse(self, value: str, /) -> T_co: 

873 """ 

874 Parse user input, raise :class:`ParsingError` on failure. 

875 

876 :param value: 

877 value to parse. 

878 :returns: 

879 a parsed and processed value. 

880 :raises: 

881 :class:`ParsingError`. 

882 

883 """ 

884 

885 return self.parse_with_ctx(StrParsingContext(value)) 

886 

887 @abc.abstractmethod 

888 def parse_with_ctx(self, ctx: StrParsingContext, /) -> T_co: 

889 """ 

890 Actual implementation of :meth:`~Parser.parse`, receives parsing context instead 

891 of a raw string. 

892 

893 :param ctx: 

894 value to parse, wrapped into a parsing context. 

895 :returns: 

896 a parsed and processed value. 

897 :raises: 

898 :class:`ParsingError`. 

899 

900 """ 

901 

902 raise NotImplementedError() 

903 

904 def parse_many(self, value: _t.Sequence[str], /) -> T_co: 

905 """ 

906 For collection parsers, parse and validate collection 

907 by parsing its items one-by-one. 

908 

909 :param value: 

910 collection of values to parse. 

911 :returns: 

912 each value parsed and assembled into the target collection. 

913 :raises: 

914 :class:`ParsingError`. Also raises :class:`RuntimeError` if trying to call 

915 this method on a parser that doesn't supports parsing collections 

916 of objects. 

917 :example: 

918 :: 

919 

920 >>> # Let's say we're parsing a set of ints. 

921 >>> parser = Set(Int()) 

922 

923 >>> # And the user enters collection items one-by-one. 

924 >>> user_input = ['1', '2', '3'] 

925 

926 >>> # We can parse collection from its items: 

927 >>> parser.parse_many(user_input) 

928 {1, 2, 3} 

929 

930 """ 

931 

932 return self.parse_many_with_ctx( 

933 [StrParsingContext(item, n_arg=i) for i, item in enumerate(value)] 

934 ) 

935 

936 @abc.abstractmethod 

937 def parse_many_with_ctx(self, ctxs: _t.Sequence[StrParsingContext], /) -> T_co: 

938 """ 

939 Actual implementation of :meth:`~Parser.parse_many`, receives parsing contexts 

940 instead of a raw strings. 

941 

942 :param ctxs: 

943 values to parse, wrapped into a parsing contexts. 

944 :returns: 

945 a parsed and processed value. 

946 :raises: 

947 :class:`ParsingError`. 

948 

949 """ 

950 

951 raise NotImplementedError() 

952 

953 @abc.abstractmethod 

954 def supports_parse_many(self) -> bool: 

955 """ 

956 Return :data:`True` if this parser returns a collection 

957 and so supports :meth:`~Parser.parse_many`. 

958 

959 :returns: 

960 :data:`True` if :meth:`~Parser.parse_many` is safe to call. 

961 

962 """ 

963 

964 raise NotImplementedError() 

965 

966 @_t.final 

967 def parse_config(self, value: object, /) -> T_co: 

968 """ 

969 Parse value from a config, raise :class:`ParsingError` on failure. 

970 

971 This method accepts python values that would result from 

972 parsing json, yaml, and similar formats. 

973 

974 :param value: 

975 config value to parse. 

976 :returns: 

977 verified and processed config value. 

978 :raises: 

979 :class:`ParsingError`. 

980 :example: 

981 :: 

982 

983 >>> # Let's say we're parsing a set of ints. 

984 >>> parser = Set(Int()) 

985 

986 >>> # And we're loading it from json. 

987 >>> import json 

988 >>> user_config = json.loads('[1, 2, 3]') 

989 

990 >>> # We can process parsed json: 

991 >>> parser.parse_config(user_config) 

992 {1, 2, 3} 

993 

994 """ 

995 

996 return self.parse_config_with_ctx(ConfigParsingContext(value)) 

997 

998 @abc.abstractmethod 

999 def parse_config_with_ctx(self, ctx: ConfigParsingContext, /) -> T_co: 

1000 """ 

1001 Actual implementation of :meth:`~Parser.parse_config`, receives parsing context 

1002 instead of a raw value. 

1003 

1004 :param ctx: 

1005 config value to parse, wrapped into a parsing contexts. 

1006 :returns: 

1007 verified and processed config value. 

1008 :raises: 

1009 :class:`ParsingError`. 

1010 

1011 """ 

1012 

1013 raise NotImplementedError() 

1014 

1015 @abc.abstractmethod 

1016 def get_nargs(self) -> _t.Literal["+", "*"] | int: 

1017 """ 

1018 Generate ``nargs`` for argparse. 

1019 

1020 :returns: 

1021 `nargs` as defined by argparse. If :meth:`~Parser.supports_parse_many` 

1022 returns :data:`True`, value should be ``"+"`` or an integer. Otherwise, 

1023 value should be ``1``. 

1024 

1025 """ 

1026 

1027 raise NotImplementedError() 

1028 

1029 @abc.abstractmethod 

1030 def check_type(self, value: object, /) -> _t.TypeGuard[T_co]: 

1031 """ 

1032 Check whether the parser can handle a particular value in its 

1033 :meth:`~Parser.describe_value` and other methods. 

1034 

1035 This function is used to raise :class:`TypeError`\\ s in function that accept 

1036 unknown values. Parsers like :class:`Union` rely on :class:`TypeError`\\ s 

1037 to dispatch values to correct sub-parsers. 

1038 

1039 .. note:: 

1040 

1041 For performance reasons, this method should not inspect contents 

1042 of containers, only their type (otherwise some methods turn from linear 

1043 to quadratic). 

1044 

1045 This also means that validating and mapping parsers 

1046 can always return :data:`True`. 

1047 

1048 :param value: 

1049 value that needs a type check. 

1050 :returns: 

1051 :data:`True` if the value matches the type of this parser. 

1052 

1053 """ 

1054 

1055 raise NotImplementedError() 

1056 

1057 def assert_type(self, value: object, /) -> _t.TypeGuard[T_co]: 

1058 """ 

1059 Call :meth:`~Parser.check_type` and raise a :class:`TypeError` 

1060 if it returns :data:`False`. 

1061 

1062 This method always returns :data:`True` or throws an error, but type checkers 

1063 don't know this. Use ``assert parser.assert_type(value)`` so that they 

1064 understand that type of the `value` has narrowed. 

1065 

1066 :param value: 

1067 value that needs a type check. 

1068 :returns: 

1069 always returns :data:`True`. 

1070 :raises: 

1071 :class:`TypeError`. 

1072 

1073 """ 

1074 

1075 if not self.check_type(value): 

1076 raise TypeError( 

1077 f"parser {self} can't handle value of type {_tx.type_repr(type(value))}" 

1078 ) 

1079 return True 

1080 

1081 @abc.abstractmethod 

1082 def describe(self) -> str | None: 

1083 """ 

1084 Return a human-readable description of an expected input. 

1085 

1086 Used to describe expected input in widgets. 

1087 

1088 :returns: 

1089 human-readable description of an expected input. Can return :data:`None` 

1090 for simple values that don't need a special description. 

1091 

1092 """ 

1093 

1094 raise NotImplementedError() 

1095 

1096 @abc.abstractmethod 

1097 def describe_or_def(self) -> str: 

1098 """ 

1099 Like :py:meth:`~Parser.describe`, but guaranteed to return something. 

1100 

1101 Used to describe expected input in CLI help. 

1102 

1103 :returns: 

1104 human-readable description of an expected input. 

1105 

1106 """ 

1107 

1108 raise NotImplementedError() 

1109 

1110 @abc.abstractmethod 

1111 def describe_many(self) -> str | tuple[str, ...]: 

1112 """ 

1113 Return a human-readable description of a container element. 

1114 

1115 Used to describe expected input in CLI help. 

1116 

1117 :returns: 

1118 human-readable description of expected inputs. If the value is a string, 

1119 then it describes an individual member of a collection. The the value 

1120 is a tuple, then each of the tuple's element describes an expected value 

1121 at the corresponding position. 

1122 :raises: 

1123 :class:`RuntimeError` if trying to call this method on a parser 

1124 that doesn't supports parsing collections of objects. 

1125 

1126 """ 

1127 

1128 raise NotImplementedError() 

1129 

1130 @abc.abstractmethod 

1131 def describe_value(self, value: object, /) -> str: 

1132 """ 

1133 Return a human-readable description of the given value. 

1134 

1135 Used in error messages, and to describe returned input in widgets. 

1136 

1137 Note that, since parser's type parameter is covariant, this function is not 

1138 guaranteed to receive a value of the same type that this parser produces. 

1139 Call :meth:`~Parser.assert_type` to check for this case. 

1140 

1141 :param value: 

1142 value that needs a description. 

1143 :returns: 

1144 description of a value in the format that this parser would expect to see 

1145 in a CLI argument or an environment variable. 

1146 :raises: 

1147 :class:`TypeError` if the given value is not of type 

1148 that this parser produces. 

1149 

1150 """ 

1151 

1152 raise NotImplementedError() 

1153 

1154 @abc.abstractmethod 

1155 def options(self) -> _t.Collection[yuio.widget.Option[T_co]] | None: 

1156 """ 

1157 Return options for a :class:`~yuio.widget.Choice` or 

1158 a :class:`~yuio.widget.Multiselect` widget. 

1159 

1160 This function can be implemented for parsers that return a fixed set 

1161 of pre-defined values, like :class:`Enum` or :class:`Literal`. 

1162 Collection and union parsers may use this data to improve their widgets. 

1163 For example, the :class:`Set` parser will use 

1164 a :class:`~yuio.widget.Multiselect` widget. 

1165 

1166 :returns: 

1167 a full list of options that will be passed to a choice widget, 

1168 or :data:`None` if the set of possible values is not known. 

1169 

1170 Note that returning :data:`None` is not equivalent to returning an empty 

1171 array: :data:`None` signals other parsers that they can't use choice 

1172 widgets, while an empty array signals that there are simply no choices 

1173 to add. 

1174 

1175 """ 

1176 

1177 raise NotImplementedError() 

1178 

1179 @abc.abstractmethod 

1180 def completer(self) -> yuio.complete.Completer | None: 

1181 """ 

1182 Return a completer for values of this parser. 

1183 

1184 This function is used when assembling autocompletion functions for shells, 

1185 and when reading values from user via :func:`yuio.io.ask`. 

1186 

1187 :returns: 

1188 a completer that will be used with CLI arguments or widgets. 

1189 

1190 """ 

1191 

1192 raise NotImplementedError() 

1193 

1194 @abc.abstractmethod 

1195 def widget( 

1196 self, 

1197 default: object | yuio.Missing, 

1198 input_description: str | None, 

1199 default_description: str | None, 

1200 /, 

1201 ) -> yuio.widget.Widget[T_co | yuio.Missing]: 

1202 """ 

1203 Return a widget for reading values of this parser. 

1204 

1205 This function is used when reading values from user via :func:`yuio.io.ask`. 

1206 

1207 The returned widget must produce values of type ``T``. If `default` is given, 

1208 and the user input is empty, the widget must produce 

1209 the :data:`~yuio.MISSING` constant (*not* the default constant). 

1210 This is because the default value might be of any type 

1211 (for example :data:`None`), and validating parsers should not check it. 

1212 

1213 Validating parsers must wrap the widget they got from 

1214 :attr:`__wrapped_parser__` into :class:`~yuio.widget.Map` 

1215 or :class:`~yuio.widget.Apply` in order to validate widget's results. 

1216 

1217 :param default: 

1218 default value that will be used if widget returns :data:`~yuio.MISSING`. 

1219 :param input_description: 

1220 a string describing what input is expected. 

1221 :param default_description: 

1222 a string describing default value. 

1223 :returns: 

1224 a widget that will be used to ask user for values. The widget can choose 

1225 to use :func:`~Parser.completer` or :func:`~Parser.options`, or implement 

1226 some custom logic. 

1227 

1228 """ 

1229 

1230 raise NotImplementedError() 

1231 

1232 @abc.abstractmethod 

1233 def to_json_schema( 

1234 self, ctx: yuio.json_schema.JsonSchemaContext, / 

1235 ) -> yuio.json_schema.JsonSchemaType: 

1236 """ 

1237 Create a JSON schema object based on this parser. 

1238 

1239 The purpose of this method is to make schemas for use in IDEs, i.e. to provide 

1240 autocompletion or simple error checking. The returned schema is not guaranteed 

1241 to reflect all constraints added to the parser. For example, :class:`OneOf` 

1242 and :class:`Regex` parsers will not affect the generated schema. 

1243 

1244 :param ctx: 

1245 context for building a schema. 

1246 :returns: 

1247 a JSON schema that describes structure of values expected by this parser. 

1248 

1249 """ 

1250 

1251 raise NotImplementedError() 

1252 

1253 @abc.abstractmethod 

1254 def to_json_value(self, value: object, /) -> yuio.json_schema.JsonValue: 

1255 """ 

1256 Convert given value to a representation suitable for JSON serialization. 

1257 

1258 Note that, since parser's type parameter is covariant, this function is not 

1259 guaranteed to receive a value of the same type that this parser produces. 

1260 Call :meth:`~Parser.assert_type` to check for this case. 

1261 

1262 :returns: 

1263 a value converted to JSON-serializable representation. 

1264 :raises: 

1265 :class:`TypeError` if the given value is not of type 

1266 that this parser produces. 

1267 

1268 """ 

1269 

1270 raise NotImplementedError() 

1271 

1272 @abc.abstractmethod 

1273 def is_secret(self) -> bool: 

1274 """ 

1275 Indicates that input functions should use secret input, 

1276 i.e. :func:`~getpass.getpass` or :class:`yuio.widget.SecretInput`. 

1277 

1278 """ 

1279 

1280 raise NotImplementedError() 

1281 

1282 def __repr__(self): 

1283 return self.__class__.__name__ 

1284 

1285 

1286class ValueParser(Parser[T], PartialParser, _t.Generic[T]): 

1287 """ 

1288 Base implementation for a parser that returns a single value. 

1289 

1290 Implements all method, except for :meth:`~Parser.parse_with_ctx`, 

1291 :meth:`~Parser.parse_config_with_ctx`, :meth:`~Parser.to_json_schema`, 

1292 and :meth:`~Parser.to_json_value`. 

1293 

1294 :param ty: 

1295 type of the produced value, used in :meth:`~Parser.check_type`. 

1296 :example: 

1297 .. invisible-code-block: python 

1298 

1299 from dataclasses import dataclass 

1300 @dataclass 

1301 class MyType: 

1302 data: str 

1303 

1304 .. code-block:: python 

1305 

1306 class MyTypeParser(ValueParser[MyType]): 

1307 def __init__(self): 

1308 super().__init__(MyType) 

1309 

1310 def parse_with_ctx(self, ctx: StrParsingContext, /) -> MyType: 

1311 return MyType(ctx.value) 

1312 

1313 def parse_config_with_ctx(self, ctx: ConfigParsingContext, /) -> MyType: 

1314 if not isinstance(ctx.value, str): 

1315 raise ParsingError.type_mismatch(value, str, ctx=ctx) 

1316 return MyType(ctx.value) 

1317 

1318 def to_json_schema( 

1319 self, ctx: yuio.json_schema.JsonSchemaContext, / 

1320 ) -> yuio.json_schema.JsonSchemaType: 

1321 return yuio.json_schema.String() 

1322 

1323 def to_json_value(self, value: object, /) -> yuio.json_schema.JsonValue: 

1324 assert self.assert_type(value) 

1325 return value.data 

1326 

1327 :: 

1328 

1329 >>> MyTypeParser().parse('pancake') 

1330 MyType(data='pancake') 

1331 

1332 """ 

1333 

1334 def __init__(self, ty: type[T], /, *args, **kwargs): 

1335 super().__init__(*args, **kwargs) 

1336 

1337 self._value_type = ty 

1338 """ 

1339 Type of the produced value, used in :meth:`~Parser.check_type`. 

1340 

1341 """ 

1342 

1343 def wrap(self: P, parser: Parser[_t.Any]) -> P: 

1344 typehint = getattr(parser, "_Parser__typehint", None) 

1345 if typehint is None: 

1346 with self._patch_stack_summary(): 

1347 raise TypeError( 

1348 f"annotating a type with {self} will override" 

1349 " all previous annotations. Make sure that" 

1350 f" {self} is the first annotation in" 

1351 " your type hint.\n\n" 

1352 "Example:\n" 

1353 " Incorrect: Str() overrides effects of Map()\n" 

1354 " field: typing.Annotated[str, Map(fn=str.lower), Str()]\n" 

1355 " ^^^^^\n" 

1356 " Correct: Str() is applied first, then Map()\n" 

1357 " field: typing.Annotated[str, Str(), Map(fn=str.lower)]\n" 

1358 " ^^^^^" 

1359 ) 

1360 if not isinstance(self, parser.__class__): 

1361 with self._patch_stack_summary(): 

1362 raise TypeError( 

1363 f"annotating {_tx.type_repr(typehint)} with {self.__class__.__name__}" 

1364 " conflicts with default parser for this type, which is" 

1365 f" {parser.__class__.__name__}.\n\n" 

1366 "Example:\n" 

1367 " Incorrect: Path() can't be used to annotate `str`\n" 

1368 " field: typing.Annotated[str, Path(extensions=[...])]\n" 

1369 " ^^^^^^^^^^^^^^^^^^^^^^\n" 

1370 " Correct: using Path() to annotate `pathlib.Path`\n" 

1371 " field: typing.Annotated[pathlib.Path, Path(extensions=[...])]\n" 

1372 " ^^^^^^^^^^^^^^^^^^^^^^" 

1373 ) 

1374 return super().wrap(parser) # pyright: ignore[reportReturnType] 

1375 

1376 def parse_many_with_ctx(self, ctxs: _t.Sequence[StrParsingContext], /) -> T: 

1377 raise RuntimeError("unable to parse multiple values") 

1378 

1379 def supports_parse_many(self) -> bool: 

1380 return False 

1381 

1382 def get_nargs(self) -> _t.Literal["+", "*"] | int: 

1383 return 1 

1384 

1385 def check_type(self, value: object, /) -> _t.TypeGuard[T]: 

1386 return isinstance(value, self._value_type) 

1387 

1388 def describe(self) -> str | None: 

1389 return None 

1390 

1391 def describe_or_def(self) -> str: 

1392 return self.describe() or f"<{_to_dash_case(self.__class__.__name__)}>" 

1393 

1394 def describe_many(self) -> str | tuple[str, ...]: 

1395 return self.describe_or_def() 

1396 

1397 def describe_value(self, value: object, /) -> str: 

1398 assert self.assert_type(value) 

1399 return str(value) or "<empty>" 

1400 

1401 def options(self) -> _t.Collection[yuio.widget.Option[T]] | None: 

1402 return None 

1403 

1404 def completer(self) -> yuio.complete.Completer | None: 

1405 return None 

1406 

1407 def widget( 

1408 self, 

1409 default: object | yuio.Missing, 

1410 input_description: str | None, 

1411 default_description: str | None, 

1412 /, 

1413 ) -> yuio.widget.Widget[T | yuio.Missing]: 

1414 completer = self.completer() 

1415 return _WidgetResultMapper( 

1416 self, 

1417 input_description, 

1418 default, 

1419 ( 

1420 yuio.widget.InputWithCompletion( 

1421 completer, 

1422 placeholder=default_description or "", 

1423 ) 

1424 if completer is not None 

1425 else yuio.widget.Input( 

1426 placeholder=default_description or "", 

1427 ) 

1428 ), 

1429 ) 

1430 

1431 def is_secret(self) -> bool: 

1432 return False 

1433 

1434 

1435class WrappingParser(Parser[T], _t.Generic[T, U]): 

1436 """ 

1437 A base for a parser that wraps another parser and alters its output. 

1438 

1439 This base simplifies dealing with partial parsers. 

1440 

1441 The :attr:`~WrappingParser._inner` attribute is whatever internal state you need 

1442 to store. When it is :data:`None`, the parser is considered partial. That is, 

1443 you can't use such a parser to actually parse anything, but you can 

1444 use it in a type annotation. When it is not :data:`None`, the parser is considered 

1445 non partial. You can use it to parse things, but you can't use it 

1446 in a type annotation. 

1447 

1448 .. warning:: 

1449 

1450 All descendants of this class must include appropriate type hints 

1451 for their ``__new__`` method, otherwise type annotations from this base 

1452 will shadow implementation's ``__init__`` signature. 

1453 

1454 See section on `parser hierarchy`_ for details. 

1455 

1456 :param inner: 

1457 inner data or :data:`None`. 

1458 

1459 """ 

1460 

1461 if TYPE_CHECKING: 

1462 

1463 @_t.overload 

1464 def __new__(cls, inner: U, /) -> WrappingParser[T, U]: ... 

1465 

1466 @_t.overload 

1467 def __new__(cls, /) -> PartialParser: ... 

1468 

1469 def __new__(cls, *args, **kwargs) -> _t.Any: ... 

1470 

1471 def __init__(self, inner: U | None, /, *args, **kwargs): 

1472 self.__inner = inner 

1473 super().__init__(*args, **kwargs) 

1474 

1475 @property 

1476 def _inner(self) -> U: 

1477 """ 

1478 Internal resource wrapped by this parser. 

1479 

1480 :raises: 

1481 Accessing it when the parser is in a partial state triggers an error 

1482 and warns user that they didn't provide an inner parser. 

1483 

1484 Setting a new value when the parser is not in a partial state triggers 

1485 an error and warns user that they shouldn't provide an inner parser 

1486 in type annotations. 

1487 

1488 """ 

1489 

1490 if self.__inner is None: 

1491 with self._patch_stack_summary(): 

1492 raise TypeError(f"{self.__class__.__name__} requires an inner parser") 

1493 return self.__inner 

1494 

1495 @_inner.setter 

1496 def _inner(self, inner: U): 

1497 if self.__inner is not None: 

1498 with self._patch_stack_summary(): 

1499 raise TypeError( 

1500 f"don't provide inner parser when using {self.__class__.__name__}" 

1501 " with type annotations. The inner parser will be derived automatically" 

1502 "from type hint.\n\n" 

1503 "Example:\n" 

1504 " Incorrect: List() has an inner parser\n" 

1505 " field: typing.Annotated[list[str], List(Str(), delimiter=';')]\n" 

1506 " ^^^^^^^^^^^^^^^^^^^^^^^^^^\n" 

1507 " Correct: inner parser for List() derived from type hint\n" 

1508 " field: typing.Annotated[list[str], List(delimiter=';')]\n" 

1509 " ^^^^^^^^^^^^^^^^^^^" 

1510 ) 

1511 self.__inner = inner 

1512 

1513 @property 

1514 def _inner_raw(self) -> U | None: 

1515 """ 

1516 Unchecked access to the wrapped resource. 

1517 

1518 """ 

1519 

1520 return self.__inner 

1521 

1522 

1523class MappingParser(WrappingParser[T, Parser[U]], _t.Generic[T, U]): 

1524 """ 

1525 This is base abstraction for :class:`Map` and :class:`Optional`. 

1526 Forwards all calls to the inner parser, except for :meth:`~Parser.parse_with_ctx`, 

1527 :meth:`~Parser.parse_many_with_ctx`, :meth:`~Parser.parse_config_with_ctx`, 

1528 :meth:`~Parser.options`, :meth:`~Parser.check_type`, 

1529 :meth:`~Parser.describe_value`, :meth:`~Parser.widget`, 

1530 and :meth:`~Parser.to_json_value`. 

1531 

1532 :param inner: 

1533 mapped parser or :data:`None`. 

1534 

1535 """ 

1536 

1537 if TYPE_CHECKING: 

1538 

1539 @_t.overload 

1540 def __new__(cls, inner: Parser[U], /) -> MappingParser[T, U]: ... 

1541 

1542 @_t.overload 

1543 def __new__(cls, /) -> PartialParser: ... 

1544 

1545 def __new__(cls, *args, **kwargs) -> _t.Any: ... 

1546 

1547 def __init__(self, inner: Parser[U] | None, /): 

1548 super().__init__(inner) 

1549 

1550 def wrap(self, parser: Parser[_t.Any]) -> Parser[_t.Any]: 

1551 result = super().wrap(parser) 

1552 result._inner = parser # pyright: ignore[reportAttributeAccessIssue] 

1553 return result 

1554 

1555 def supports_parse_many(self) -> bool: 

1556 return self._inner.supports_parse_many() 

1557 

1558 def get_nargs(self) -> _t.Literal["+", "*"] | int: 

1559 return self._inner.get_nargs() 

1560 

1561 def describe(self) -> str | None: 

1562 return self._inner.describe() 

1563 

1564 def describe_or_def(self) -> str: 

1565 return self._inner.describe_or_def() 

1566 

1567 def describe_many(self) -> str | tuple[str, ...]: 

1568 return self._inner.describe_many() 

1569 

1570 def completer(self) -> yuio.complete.Completer | None: 

1571 return self._inner.completer() 

1572 

1573 def to_json_schema( 

1574 self, ctx: yuio.json_schema.JsonSchemaContext, / 

1575 ) -> yuio.json_schema.JsonSchemaType: 

1576 return self._inner.to_json_schema(ctx) 

1577 

1578 def is_secret(self) -> bool: 

1579 return self._inner.is_secret() 

1580 

1581 def __repr__(self): 

1582 return f"{self.__class__.__name__}({self._inner_raw!r})" 

1583 

1584 @property 

1585 def __wrapped_parser__(self): # pyright: ignore[reportIncompatibleVariableOverride] 

1586 return self._inner_raw 

1587 

1588 

1589class Map(MappingParser[T, U], _t.Generic[T, U]): 

1590 """Map(inner: Parser[U], fn: typing.Callable[[U], T], rev: typing.Callable[[T | object], U] | None = None, /) 

1591 

1592 A wrapper that maps result of the given parser using the given function. 

1593 

1594 :param inner: 

1595 a parser whose result will be mapped. 

1596 :param fn: 

1597 a function to convert a result. 

1598 :param rev: 

1599 a function used to un-map a value. 

1600 

1601 This function is used in :meth:`Parser.describe_value` 

1602 and :meth:`Parser.to_json_value` to convert parsed value back 

1603 to its original state. 

1604 

1605 Note that, since parser's type parameter is covariant, this function is not 

1606 guaranteed to receive a value of the same type that this parser produces. 

1607 In this case, you should raise a :class:`TypeError`. 

1608 :example: 

1609 .. 

1610 >>> import math 

1611 

1612 :: 

1613 

1614 >>> parser = yuio.parse.Map( 

1615 ... yuio.parse.Int(), 

1616 ... lambda x: 2 ** x, 

1617 ... lambda x: int(math.log2(x)), 

1618 ... ) 

1619 >>> parser.parse("10") 

1620 1024 

1621 >>> parser.describe_value(1024) 

1622 '10' 

1623 

1624 """ 

1625 

1626 if TYPE_CHECKING: 

1627 

1628 @_t.overload 

1629 def __new__(cls, inner: Parser[T], fn: _t.Callable[[T], T], /) -> Map[T, T]: ... 

1630 

1631 @_t.overload 

1632 def __new__(cls, fn: _t.Callable[[T], T], /) -> PartialParser: ... 

1633 

1634 @_t.overload 

1635 def __new__( 

1636 cls, 

1637 inner: Parser[U], 

1638 fn: _t.Callable[[U], T], 

1639 rev: _t.Callable[[T | object], U], 

1640 /, 

1641 ) -> Map[T, T]: ... 

1642 

1643 @_t.overload 

1644 def __new__( 

1645 cls, fn: _t.Callable[[U], T], rev: _t.Callable[[T | object], U], / 

1646 ) -> PartialParser: ... 

1647 

1648 def __new__(cls, *args, **kwargs) -> _t.Any: ... 

1649 

1650 def __init__(self, *args): 

1651 inner: Parser[U] | None = None 

1652 fn: _t.Callable[[U], T] 

1653 rev: _t.Callable[[T | object], U] | None = None 

1654 if len(args) == 1: 

1655 (fn,) = args 

1656 elif len(args) == 2 and isinstance(args[0], Parser): 

1657 inner, fn = args 

1658 elif len(args) == 2: 

1659 fn, rev = args 

1660 elif len(args) == 3: 

1661 inner, fn, rev = args 

1662 else: 

1663 raise TypeError( 

1664 f"expected between 1 and 2 positional arguments, got {len(args)}" 

1665 ) 

1666 

1667 self._fn = fn 

1668 self._rev = rev 

1669 super().__init__(inner) 

1670 

1671 def parse_with_ctx(self, ctx: StrParsingContext, /) -> T: 

1672 res = self._inner.parse_with_ctx(ctx) 

1673 try: 

1674 return self._fn(res) 

1675 except ParsingError as e: 

1676 e.set_ctx(ctx) 

1677 raise 

1678 

1679 def parse_many_with_ctx(self, ctxs: _t.Sequence[StrParsingContext], /) -> T: 

1680 return self._fn(self._inner.parse_many_with_ctx(ctxs)) 

1681 

1682 def parse_config_with_ctx(self, ctx: ConfigParsingContext, /) -> T: 

1683 res = self._inner.parse_config_with_ctx(ctx) 

1684 try: 

1685 return self._fn(res) 

1686 except ParsingError as e: 

1687 e.set_ctx(ctx) 

1688 raise 

1689 

1690 def check_type(self, value: object, /) -> _t.TypeGuard[T]: 

1691 return True 

1692 

1693 def describe_value(self, value: object, /) -> str: 

1694 if self._rev: 

1695 value = self._rev(value) 

1696 return self._inner.describe_value(value) 

1697 

1698 def options(self) -> _t.Collection[yuio.widget.Option[T]] | None: 

1699 options = self._inner.options() 

1700 if options is not None: 

1701 return [ 

1702 _t.cast( 

1703 yuio.widget.Option[T], 

1704 dataclasses.replace(option, value=self._fn(option.value)), 

1705 ) 

1706 for option in options 

1707 ] 

1708 else: 

1709 return None 

1710 

1711 def widget( 

1712 self, 

1713 default: object | yuio.Missing, 

1714 input_description: str | None, 

1715 default_description: str | None, 

1716 /, 

1717 ) -> yuio.widget.Widget[T | yuio.Missing]: 

1718 return yuio.widget.Map( 

1719 self._inner.widget(default, input_description, default_description), 

1720 lambda v: self._fn(v) if v is not yuio.MISSING else yuio.MISSING, 

1721 ) 

1722 

1723 def to_json_value(self, value: object, /) -> yuio.json_schema.JsonValue: 

1724 if self._rev: 

1725 value = self._rev(value) 

1726 return self._inner.to_json_value(value) 

1727 

1728 

1729@_t.overload 

1730def Lower(inner: Parser[str], /) -> Parser[str]: ... 

1731@_t.overload 

1732def Lower() -> PartialParser: ... 

1733def Lower(*args) -> _t.Any: 

1734 """Lower(inner: Parser[str], /) 

1735 

1736 Applies :meth:`str.lower` to the result of a string parser. 

1737 

1738 :param inner: 

1739 a parser whose result will be mapped. 

1740 

1741 """ 

1742 

1743 return Map(*args, str.lower) # pyright: ignore[reportCallIssue] 

1744 

1745 

1746@_t.overload 

1747def Upper(inner: Parser[str], /) -> Parser[str]: ... 

1748@_t.overload 

1749def Upper() -> PartialParser: ... 

1750def Upper(*args) -> _t.Any: 

1751 """Upper(inner: Parser[str], /) 

1752 

1753 Applies :meth:`str.upper` to the result of a string parser. 

1754 

1755 :param inner: 

1756 a parser whose result will be mapped. 

1757 

1758 """ 

1759 

1760 return Map(*args, str.upper) # pyright: ignore[reportCallIssue] 

1761 

1762 

1763@_t.overload 

1764def CaseFold(inner: Parser[str], /) -> Parser[str]: ... 

1765@_t.overload 

1766def CaseFold() -> PartialParser: ... 

1767def CaseFold(*args) -> _t.Any: 

1768 """CaseFold(inner: Parser[str], /) 

1769 

1770 Applies :meth:`str.casefold` to the result of a string parser. 

1771 

1772 :param inner: 

1773 a parser whose result will be mapped. 

1774 

1775 """ 

1776 

1777 return Map(*args, str.casefold) # pyright: ignore[reportCallIssue] 

1778 

1779 

1780@_t.overload 

1781def Strip(inner: Parser[str], /) -> Parser[str]: ... 

1782@_t.overload 

1783def Strip() -> PartialParser: ... 

1784def Strip(*args) -> _t.Any: 

1785 """Strip(inner: Parser[str], /) 

1786 

1787 Applies :meth:`str.strip` to the result of a string parser. 

1788 

1789 :param inner: 

1790 a parser whose result will be mapped. 

1791 

1792 """ 

1793 

1794 return Map(*args, str.strip) # pyright: ignore[reportCallIssue] 

1795 

1796 

1797@_t.overload 

1798def Regex( 

1799 inner: Parser[str], 

1800 regex: str | _tx.StrRePattern, 

1801 /, 

1802 *, 

1803 group: int | str = 0, 

1804) -> Parser[str]: ... 

1805@_t.overload 

1806def Regex( 

1807 regex: str | _tx.StrRePattern, /, *, group: int | str = 0 

1808) -> PartialParser: ... 

1809def Regex(*args, group: int | str = 0) -> _t.Any: 

1810 """Regex(inner: Parser[str], regex: str | re.Pattern[str], /, *, group: int | str = 0) 

1811 

1812 Matches the parsed string with the given regular expression. 

1813 

1814 If regex has capturing groups, parser can return contents of a group. 

1815 

1816 :param regex: 

1817 regular expression for matching. 

1818 :param group: 

1819 name or index of a capturing group that should be used to get the final 

1820 parsed value. 

1821 

1822 """ 

1823 

1824 inner: Parser[str] | None 

1825 regex: str | _tx.StrRePattern 

1826 if len(args) == 1: 

1827 inner, regex = None, args[0] 

1828 elif len(args) == 2: 

1829 inner, regex = args 

1830 else: 

1831 raise TypeError(f"expected 1 or 2 positional arguments, got {len(args)}") 

1832 

1833 if isinstance(regex, re.Pattern): 

1834 compiled = regex 

1835 else: 

1836 compiled = re.compile(regex) 

1837 

1838 def mapper(value: str) -> str: 

1839 if (match := compiled.match(value)) is None: 

1840 raise ParsingError( 

1841 "Value doesn't match regex `%s`: `%r`", 

1842 compiled.pattern, 

1843 value, 

1844 fallback_msg="Incorrect value format", 

1845 ) 

1846 return match.group(group) 

1847 

1848 return Map(inner, mapper) # type: ignore 

1849 

1850 

1851class Apply(MappingParser[T, T], _t.Generic[T]): 

1852 """Apply(inner: Parser[T], fn: typing.Callable[[T], None], /) 

1853 

1854 A wrapper that applies the given function to the result of a wrapped parser. 

1855 

1856 :param inner: 

1857 a parser used to extract and validate a value. 

1858 :param fn: 

1859 a function that will be called after parsing a value. 

1860 :example: 

1861 :: 

1862 

1863 >>> # Run `Int` parser, then print its output before returning. 

1864 >>> print_output = Apply(Int(), lambda x: print(f"Value is {x}")) 

1865 >>> result = print_output.parse("10") 

1866 Value is 10 

1867 >>> result 

1868 10 

1869 

1870 """ 

1871 

1872 if TYPE_CHECKING: 

1873 

1874 @_t.overload 

1875 def __new__( 

1876 cls, inner: Parser[T], fn: _t.Callable[[T], None], / 

1877 ) -> Apply[T]: ... 

1878 

1879 @_t.overload 

1880 def __new__(cls, fn: _t.Callable[[T], None], /) -> PartialParser: ... 

1881 

1882 def __new__(cls, *args, **kwargs) -> _t.Any: ... 

1883 

1884 def __init__(self, *args): 

1885 inner: Parser[T] | None 

1886 fn: _t.Callable[[T], None] 

1887 if len(args) == 1: 

1888 inner, fn = None, args[0] 

1889 elif len(args) == 2: 

1890 inner, fn = args 

1891 else: 

1892 raise TypeError(f"expected 1 or 2 positional arguments, got {len(args)}") 

1893 

1894 self._fn = fn 

1895 super().__init__(inner) 

1896 

1897 def parse_with_ctx(self, ctx: StrParsingContext, /) -> T: 

1898 result = self._inner.parse_with_ctx(ctx) 

1899 try: 

1900 self._fn(result) 

1901 except ParsingError as e: 

1902 e.set_ctx(ctx) 

1903 raise 

1904 return result 

1905 

1906 def parse_many_with_ctx(self, ctxs: _t.Sequence[StrParsingContext], /) -> T: 

1907 result = self._inner.parse_many_with_ctx(ctxs) 

1908 self._fn(result) 

1909 return result 

1910 

1911 def parse_config_with_ctx(self, ctx: ConfigParsingContext, /) -> T: 

1912 result = self._inner.parse_config_with_ctx(ctx) 

1913 try: 

1914 self._fn(result) 

1915 except ParsingError as e: 

1916 e.set_ctx(ctx) 

1917 raise 

1918 return result 

1919 

1920 def check_type(self, value: object, /) -> _t.TypeGuard[T]: 

1921 return True 

1922 

1923 def describe_value(self, value: object, /) -> str: 

1924 return self._inner.describe_value(value) 

1925 

1926 def options(self) -> _t.Collection[yuio.widget.Option[T]] | None: 

1927 return self._inner.options() 

1928 

1929 def completer(self) -> yuio.complete.Completer | None: 

1930 return self._inner.completer() 

1931 

1932 def widget( 

1933 self, 

1934 default: object | yuio.Missing, 

1935 input_description: str | None, 

1936 default_description: str | None, 

1937 /, 

1938 ) -> yuio.widget.Widget[T | yuio.Missing]: 

1939 return yuio.widget.Apply( 

1940 self._inner.widget(default, input_description, default_description), 

1941 lambda v: self._fn(v) if v is not yuio.MISSING else None, 

1942 ) 

1943 

1944 def to_json_schema( 

1945 self, ctx: yuio.json_schema.JsonSchemaContext, / 

1946 ) -> yuio.json_schema.JsonSchemaType: 

1947 return self._inner.to_json_schema(ctx) 

1948 

1949 def to_json_value(self, value: object, /) -> yuio.json_schema.JsonValue: 

1950 return self._inner.to_json_value(value) 

1951 

1952 

1953class ValidatingParser(Apply[T], _t.Generic[T]): 

1954 """ 

1955 Base implementation for a parser that validates result of another parser. 

1956 

1957 This class wraps another parser and passes all method calls to it. 

1958 All parsed values are additionally passed to :meth:`~ValidatingParser._validate`. 

1959 

1960 :param inner: 

1961 a parser which output will be validated. 

1962 :example: 

1963 .. code-block:: python 

1964 

1965 class IsLower(ValidatingParser[str]): 

1966 def _validate(self, value: str, /): 

1967 if not value.islower(): 

1968 raise ParsingError( 

1969 "Value should be lowercase: `%r`", 

1970 value, 

1971 fallback_msg="Value should be lowercase", 

1972 ) 

1973 

1974 :: 

1975 

1976 >>> IsLower(Str()).parse("Not lowercase!") 

1977 Traceback (most recent call last): 

1978 ... 

1979 yuio.parse.ParsingError: Value should be lowercase: 'Not lowercase!' 

1980 

1981 """ 

1982 

1983 if TYPE_CHECKING: 

1984 

1985 @_t.overload 

1986 def __new__(cls, inner: Parser[T], /) -> ValidatingParser[T]: ... 

1987 

1988 @_t.overload 

1989 def __new__(cls, /) -> PartialParser: ... 

1990 

1991 def __new__(cls, *args, **kwargs) -> _t.Any: ... 

1992 

1993 def __init__(self, inner: Parser[T] | None = None, /): 

1994 super().__init__(inner, self._validate) 

1995 

1996 @abc.abstractmethod 

1997 def _validate(self, value: T, /): 

1998 """ 

1999 Implementation of value validation. 

2000 

2001 :param value: 

2002 value which needs validating. 

2003 :raises: 

2004 should raise :class:`ParsingError` if validation fails. 

2005 

2006 """ 

2007 

2008 raise NotImplementedError() 

2009 

2010 

2011class Str(ValueParser[str]): 

2012 """ 

2013 Parser for str values. 

2014 

2015 """ 

2016 

2017 def __init__(self): 

2018 super().__init__(str) 

2019 

2020 def parse_with_ctx(self, ctx: StrParsingContext, /) -> str: 

2021 return str(ctx.value) 

2022 

2023 def parse_config_with_ctx(self, ctx: ConfigParsingContext, /) -> str: 

2024 if not isinstance(ctx.value, str): 

2025 raise ParsingError.type_mismatch(ctx.value, str, ctx=ctx) 

2026 return str(ctx.value) 

2027 

2028 def to_json_schema( 

2029 self, ctx: yuio.json_schema.JsonSchemaContext, / 

2030 ) -> yuio.json_schema.JsonSchemaType: 

2031 return yuio.json_schema.String() 

2032 

2033 def to_json_value(self, value: object, /) -> yuio.json_schema.JsonValue: 

2034 assert self.assert_type(value) 

2035 return value 

2036 

2037 

2038class Int(ValueParser[int]): 

2039 """ 

2040 Parser for int values. 

2041 

2042 """ 

2043 

2044 def __init__(self): 

2045 super().__init__(int) 

2046 

2047 def parse_with_ctx(self, ctx: StrParsingContext, /) -> int: 

2048 ctx = ctx.strip_if_non_space() 

2049 try: 

2050 value = ctx.value.casefold() 

2051 if value.startswith("-"): 

2052 neg = True 

2053 value = value[1:].lstrip() 

2054 else: 

2055 neg = False 

2056 if value.startswith("0x"): 

2057 base = 16 

2058 value = value[2:] 

2059 elif value.startswith("0o"): 

2060 base = 8 

2061 value = value[2:] 

2062 elif value.startswith("0b"): 

2063 base = 2 

2064 value = value[2:] 

2065 else: 

2066 base = 10 

2067 if value[:1] in "-\n\t\r\v\b ": 

2068 raise ValueError() 

2069 res = int(value, base=base) 

2070 if neg: 

2071 res = -res 

2072 return res 

2073 except ValueError: 

2074 raise ParsingError( 

2075 "Can't parse `%r` as `int`", 

2076 ctx.value, 

2077 ctx=ctx, 

2078 fallback_msg="Can't parse value as `int`", 

2079 ) from None 

2080 

2081 def parse_config_with_ctx(self, ctx: ConfigParsingContext, /) -> int: 

2082 value = ctx.value 

2083 if isinstance(value, float): 

2084 if value != int(value): # pyright: ignore[reportUnnecessaryComparison] 

2085 raise ParsingError.type_mismatch(value, int, ctx=ctx) 

2086 value = int(value) 

2087 if not isinstance(value, int): 

2088 raise ParsingError.type_mismatch(value, int, ctx=ctx) 

2089 return value 

2090 

2091 def to_json_schema( 

2092 self, ctx: yuio.json_schema.JsonSchemaContext, / 

2093 ) -> yuio.json_schema.JsonSchemaType: 

2094 return yuio.json_schema.Integer() 

2095 

2096 def to_json_value(self, value: object, /) -> yuio.json_schema.JsonValue: 

2097 assert self.assert_type(value) 

2098 return value 

2099 

2100 

2101class Float(ValueParser[float]): 

2102 """ 

2103 Parser for float values. 

2104 

2105 """ 

2106 

2107 def __init__(self): 

2108 super().__init__(float) 

2109 

2110 def parse_with_ctx(self, ctx: StrParsingContext, /) -> float: 

2111 ctx = ctx.strip_if_non_space() 

2112 try: 

2113 return float(ctx.value) 

2114 except ValueError: 

2115 raise ParsingError( 

2116 "Can't parse `%r` as `float`", 

2117 ctx.value, 

2118 ctx=ctx, 

2119 fallback_msg="Can't parse value as `float`", 

2120 ) from None 

2121 

2122 def parse_config_with_ctx(self, ctx: ConfigParsingContext, /) -> float: 

2123 value = ctx.value 

2124 if not isinstance(value, (float, int)): 

2125 raise ParsingError.type_mismatch(value, float, ctx=ctx) 

2126 return value 

2127 

2128 def to_json_schema( 

2129 self, ctx: yuio.json_schema.JsonSchemaContext, / 

2130 ) -> yuio.json_schema.JsonSchemaType: 

2131 return yuio.json_schema.Number() 

2132 

2133 def to_json_value(self, value: object, /) -> yuio.json_schema.JsonValue: 

2134 assert self.assert_type(value) 

2135 return value 

2136 

2137 

2138class Bool(ValueParser[bool]): 

2139 """ 

2140 Parser for bool values, such as ``"yes"`` or ``"no"``. 

2141 

2142 """ 

2143 

2144 def __init__(self): 

2145 super().__init__(bool) 

2146 

2147 def parse_with_ctx(self, ctx: StrParsingContext, /) -> bool: 

2148 ctx = ctx.strip_if_non_space() 

2149 value = ctx.value.casefold() 

2150 if value in ("y", "yes", "true", "1"): 

2151 return True 

2152 elif value in ("n", "no", "false", "0"): 

2153 return False 

2154 else: 

2155 raise ParsingError( 

2156 "Can't parse `%r` as `bool`, should be `yes`, `no`, `true`, or `false`", 

2157 value, 

2158 ctx=ctx, 

2159 fallback_msg="Can't parse value as `bool`", 

2160 ) 

2161 

2162 def parse_config_with_ctx(self, ctx: ConfigParsingContext, /) -> bool: 

2163 value = ctx.value 

2164 if not isinstance(value, bool): 

2165 raise ParsingError.type_mismatch(value, bool, ctx=ctx) 

2166 return value 

2167 

2168 def describe(self) -> str | None: 

2169 return "{yes|no}" 

2170 

2171 def describe_value(self, value: object, /) -> str: 

2172 assert self.assert_type(value) 

2173 return "yes" if value else "no" 

2174 

2175 def options(self) -> _t.Collection[yuio.widget.Option[bool]] | None: 

2176 return [ 

2177 yuio.widget.Option(True, display_text="yes"), 

2178 yuio.widget.Option(False, display_text="no"), 

2179 ] 

2180 

2181 def completer(self) -> yuio.complete.Completer | None: 

2182 return yuio.complete.Choice( 

2183 [ 

2184 yuio.complete.Option("true"), 

2185 yuio.complete.Option("false"), 

2186 ] 

2187 ) 

2188 

2189 def widget( 

2190 self, 

2191 default: object | yuio.Missing, 

2192 input_description: str | None, 

2193 default_description: str | None, 

2194 /, 

2195 ) -> yuio.widget.Widget[bool | yuio.Missing]: 

2196 options: list[yuio.widget.Option[bool | yuio.Missing]] = [ 

2197 yuio.widget.Option(False, "no"), 

2198 yuio.widget.Option(True, "yes"), 

2199 ] 

2200 

2201 if default is yuio.MISSING: 

2202 default_index = 0 

2203 elif isinstance(default, bool): 

2204 default_index = int(default) 

2205 else: 

2206 options.append( 

2207 yuio.widget.Option(yuio.MISSING, default_description or str(default)) 

2208 ) 

2209 default_index = 2 

2210 

2211 return yuio.widget.Choice(options, default_index=default_index) 

2212 

2213 def to_json_schema( 

2214 self, ctx: yuio.json_schema.JsonSchemaContext, / 

2215 ) -> yuio.json_schema.JsonSchemaType: 

2216 return yuio.json_schema.Boolean() 

2217 

2218 def to_json_value(self, value: object, /) -> yuio.json_schema.JsonValue: 

2219 assert self.assert_type(value) 

2220 return value 

2221 

2222 

2223class _EnumBase(WrappingParser[T, U], ValueParser[T], _t.Generic[T, U]): 

2224 def __init__(self, inner: U | None = None, ty: type[T] | None = None, /): 

2225 super().__init__(inner, ty) 

2226 

2227 def wrap(self, parser: Parser[_t.Any]) -> Parser[_t.Any]: 

2228 result = super().wrap(parser) 

2229 result._inner = parser._inner # type: ignore 

2230 result._value_type = parser._value_type # type: ignore 

2231 return result 

2232 

2233 @abc.abstractmethod 

2234 def _get_items(self) -> _t.Iterable[T]: 

2235 raise NotImplementedError() 

2236 

2237 @abc.abstractmethod 

2238 def _value_to_str(self, value: T) -> str: 

2239 raise NotImplementedError() 

2240 

2241 @abc.abstractmethod 

2242 def _str_value_matches(self, value: T, given: str) -> bool: 

2243 raise NotImplementedError() 

2244 

2245 @abc.abstractmethod 

2246 def _str_value_matches_prefix(self, value: T, given: str) -> bool: 

2247 raise NotImplementedError() 

2248 

2249 @abc.abstractmethod 

2250 def _config_value_matches(self, value: T, given: object) -> bool: 

2251 raise NotImplementedError() 

2252 

2253 @abc.abstractmethod 

2254 def _value_to_json(self, value: T) -> JsonValue: 

2255 raise NotImplementedError() 

2256 

2257 def _get_docs(self) -> dict[T, str | None]: 

2258 return {} 

2259 

2260 def _get_desc(self) -> str: 

2261 return repr(self) 

2262 

2263 def parse_with_ctx(self, ctx: StrParsingContext, /) -> T: 

2264 ctx = ctx.strip_if_non_space() 

2265 

2266 candidates: list[T] = [] 

2267 for item in self._get_items(): 

2268 if self._str_value_matches(item, ctx.value): 

2269 return item 

2270 elif self._str_value_matches_prefix(item, ctx.value): 

2271 candidates.append(item) 

2272 

2273 if len(candidates) == 1: 

2274 return candidates[0] 

2275 elif len(candidates) > 1: 

2276 enum_values = tuple(self._value_to_str(e) for e in candidates) 

2277 raise ParsingError( 

2278 "Can't parse `%r` as `%s`, possible candidates are %s", 

2279 ctx.value, 

2280 self._get_desc(), 

2281 yuio.string.Or(enum_values), 

2282 ctx=ctx, 

2283 ) 

2284 else: 

2285 enum_values = tuple(self._value_to_str(e) for e in self._get_items()) 

2286 raise ParsingError( 

2287 "Can't parse `%r` as `%s`, should be %s", 

2288 ctx.value, 

2289 self._get_desc(), 

2290 yuio.string.Or(enum_values), 

2291 ctx=ctx, 

2292 ) 

2293 

2294 def parse_config_with_ctx(self, ctx: ConfigParsingContext, /) -> T: 

2295 value = ctx.value 

2296 

2297 for item in self._get_items(): 

2298 if self._config_value_matches(item, value): 

2299 return item 

2300 

2301 enum_values = tuple(self._value_to_str(e) for e in self._get_items()) 

2302 raise ParsingError( 

2303 "Can't parse `%r` as `%s`, should be %s", 

2304 value, 

2305 self._get_desc(), 

2306 yuio.string.Or(enum_values), 

2307 ctx=ctx, 

2308 ) 

2309 

2310 def describe(self) -> str | None: 

2311 enum_values = tuple(self._value_to_str(e) for e in self._get_items()) 

2312 desc = "|".join(enum_values) 

2313 if len(enum_values) > 1: 

2314 desc = f"{{{desc}}}" 

2315 return desc 

2316 

2317 def describe_many(self) -> str | tuple[str, ...]: 

2318 return self.describe_or_def() 

2319 

2320 def describe_value(self, value: object, /) -> str: 

2321 assert self.assert_type(value) 

2322 return self._value_to_str(value) 

2323 

2324 def options(self) -> _t.Collection[yuio.widget.Option[T]]: 

2325 docs = self._get_docs() 

2326 options = [] 

2327 for value in self._get_items(): 

2328 comment = docs.get(value) 

2329 if comment: 

2330 lines = comment.splitlines() 

2331 if not lines: 

2332 comment = None 

2333 elif len(lines) == 1: 

2334 comment = str(lines[0]) 

2335 else: 

2336 comment = str(lines[0]) + ("..." if lines[1] else "") 

2337 options.append( 

2338 yuio.widget.Option( 

2339 value, display_text=self._value_to_str(value), comment=comment 

2340 ) 

2341 ) 

2342 return options 

2343 

2344 def completer(self) -> yuio.complete.Completer | None: 

2345 return yuio.complete.Choice( 

2346 [ 

2347 yuio.complete.Option(option.display_text, comment=option.comment) 

2348 for option in self.options() 

2349 ] 

2350 ) 

2351 

2352 def widget( 

2353 self, 

2354 default: object | yuio.Missing, 

2355 input_description: str | None, 

2356 default_description: str | None, 

2357 /, 

2358 ) -> yuio.widget.Widget[T | yuio.Missing]: 

2359 options: list[yuio.widget.Option[T | yuio.Missing]] = list(self.options()) 

2360 

2361 if not options: 

2362 return super().widget(default, input_description, default_description) 

2363 

2364 items = list(self._get_items()) 

2365 

2366 if default is yuio.MISSING: 

2367 default_index = 0 

2368 elif default in items: 

2369 default_index = items.index(default) # type: ignore 

2370 else: 

2371 options.insert( 

2372 0, yuio.widget.Option(yuio.MISSING, default_description or str(default)) 

2373 ) 

2374 default_index = 0 

2375 

2376 return yuio.widget.Choice(options, default_index=default_index) 

2377 

2378 def to_json_schema( 

2379 self, ctx: yuio.json_schema.JsonSchemaContext, / 

2380 ) -> yuio.json_schema.JsonSchemaType: 

2381 items = [self._value_to_json((e)) for e in self._get_items()] 

2382 docs = self._get_docs() 

2383 

2384 descriptions = [docs.get(e) for e in self._get_items()] 

2385 if not any(descriptions): 

2386 descriptions = None 

2387 

2388 return yuio.json_schema.Enum(items, descriptions) # type: ignore 

2389 

2390 def to_json_value(self, value: object, /) -> yuio.json_schema.JsonValue: 

2391 assert self.assert_type(value) 

2392 return self._value_to_json(value) 

2393 

2394 def __repr__(self): 

2395 if self._inner_raw is not None: 

2396 return f"{self.__class__.__name__}({self._inner_raw!r})" 

2397 else: 

2398 return self.__class__.__name__ 

2399 

2400 

2401class Enum(_EnumBase[E, type[E]], _t.Generic[E]): 

2402 """Enum(enum_type: typing.Type[E], /, *, by_name: bool | None = None, to_dash_case: bool | None = None, doc_inline: bool = False) 

2403 

2404 Parser for enums, as defined in the standard :mod:`enum` module. 

2405 

2406 :param enum_type: 

2407 enum class that will be used to parse and extract values. 

2408 :param by_name: 

2409 if :data:`True`, the parser will use enumerator names, instead of 

2410 their values, to match the input. 

2411 

2412 If not given, Yuio will search for :data:`__yuio_by_name__` attribute on the 

2413 given enum class to infer value for this option. 

2414 :param to_dash_case: 

2415 convert enum names/values to dash case. 

2416 

2417 If not given, Yuio will search for :data:`__yuio_to_dash_case__` attribute on the 

2418 given enum class to infer value for this option. 

2419 :param doc_inline: 

2420 inline this enum in json schema and in documentation. 

2421 

2422 Useful for small enums that don't warrant a separate section in documentation. 

2423 

2424 If not given, Yuio will search for :data:`__yuio_doc_inline__` attribute on the 

2425 given enum class to infer value for this option. 

2426 

2427 """ 

2428 

2429 if TYPE_CHECKING: 

2430 

2431 @_t.overload 

2432 def __new__( 

2433 cls, 

2434 inner: type[E], 

2435 /, 

2436 *, 

2437 by_name: bool | None = None, 

2438 to_dash_case: bool | None = None, 

2439 doc_inline: bool | None = None, 

2440 ) -> Enum[E]: ... 

2441 

2442 @_t.overload 

2443 def __new__( 

2444 cls, 

2445 /, 

2446 *, 

2447 by_name: bool | None = None, 

2448 to_dash_case: bool | None = None, 

2449 doc_inline: bool | None = None, 

2450 ) -> PartialParser: ... 

2451 

2452 def __new__(cls, *args, **kwargs) -> _t.Any: ... 

2453 

2454 def __init__( 

2455 self, 

2456 enum_type: type[E] | None = None, 

2457 /, 

2458 *, 

2459 by_name: bool | None = None, 

2460 to_dash_case: bool | None = None, 

2461 doc_inline: bool | None = None, 

2462 ): 

2463 self.__by_name = by_name 

2464 self.__to_dash_case = to_dash_case 

2465 self.__doc_inline = doc_inline 

2466 self.__docs = None 

2467 super().__init__(enum_type, enum_type) 

2468 

2469 @functools.cached_property 

2470 def _by_name(self) -> bool: 

2471 by_name = self.__by_name 

2472 if by_name is None: 

2473 by_name = getattr(self._inner, "__yuio_by_name__", False) 

2474 return by_name 

2475 

2476 @functools.cached_property 

2477 def _to_dash_case(self) -> bool: 

2478 to_dash_case = self.__to_dash_case 

2479 if to_dash_case is None: 

2480 to_dash_case = getattr(self._inner, "__yuio_to_dash_case__", False) 

2481 return to_dash_case 

2482 

2483 @functools.cached_property 

2484 def _doc_inline(self) -> bool: 

2485 doc_inline = self.__doc_inline 

2486 if doc_inline is None: 

2487 doc_inline = getattr(self._inner, "__yuio_doc_inline__", False) 

2488 return doc_inline 

2489 

2490 @functools.cached_property 

2491 def _map_cache(self): 

2492 items: dict[E, str] = {} 

2493 for e in self._inner: 

2494 if self._by_name: 

2495 name = e.name 

2496 else: 

2497 name = str(e.value) 

2498 if self._to_dash_case and isinstance(name, str): 

2499 name = _to_dash_case(name) 

2500 items[e] = name 

2501 return items 

2502 

2503 def _get_items(self) -> _t.Iterable[E]: 

2504 return self._inner 

2505 

2506 def _value_to_str(self, value: E) -> str: 

2507 return self._map_cache[value] 

2508 

2509 def _str_value_matches(self, value: E, given: str) -> bool: 

2510 expected = self._map_cache[value] 

2511 

2512 if isinstance(expected, str): 

2513 return expected == given 

2514 elif isinstance(expected, bool): 

2515 try: 

2516 given_parsed = Bool().parse(given) 

2517 except ParsingError: 

2518 return False 

2519 else: 

2520 return expected == given_parsed 

2521 elif isinstance(expected, int): 

2522 try: 

2523 given_parsed = Int().parse(given) 

2524 except ParsingError: 

2525 return False 

2526 else: 

2527 return expected == given_parsed 

2528 else: 

2529 return False 

2530 

2531 def _str_value_matches_prefix(self, value: E, given: str) -> bool: 

2532 expected = self._map_cache[value] 

2533 return isinstance(expected, str) and expected.casefold().startswith( 

2534 given.casefold() 

2535 ) 

2536 

2537 def _config_value_matches(self, value: E, given: object) -> bool: 

2538 if given is value: 

2539 return True 

2540 

2541 if self._by_name: 

2542 expected = self._map_cache[value] 

2543 else: 

2544 expected = value.value 

2545 

2546 return expected == given 

2547 

2548 def _value_to_json(self, value: E) -> JsonValue: 

2549 if self._by_name: 

2550 res = value.name 

2551 else: 

2552 res = value.value 

2553 if self._to_dash_case and isinstance(res, str): 

2554 res = _to_dash_case(res) 

2555 return res 

2556 

2557 def _get_docs(self) -> dict[E, str | None]: 

2558 if self.__docs is not None: 

2559 return self.__docs 

2560 docs = _find_docs(self._inner) 

2561 res = {} 

2562 for e in self._inner: 

2563 text = docs.get(e.name) 

2564 if not text: 

2565 continue 

2566 if (index := text.find("\n\n")) != -1: 

2567 res[e] = text[:index] 

2568 else: 

2569 res[e] = text 

2570 return res 

2571 

2572 def _get_desc(self) -> str: 

2573 return self._inner.__name__ 

2574 

2575 def to_json_schema( 

2576 self, ctx: yuio.json_schema.JsonSchemaContext, / 

2577 ) -> yuio.json_schema.JsonSchemaType: 

2578 schema = super().to_json_schema(ctx) 

2579 

2580 if self._doc_inline: 

2581 return schema 

2582 else: 

2583 return ctx.add_type( 

2584 Enum._TyWrapper(self._inner, self._by_name, self._to_dash_case), 

2585 _tx.type_repr(self._inner), 

2586 lambda: yuio.json_schema.Meta( 

2587 schema, 

2588 title=self._inner.__name__, 

2589 description=self._inner.__doc__, 

2590 ), 

2591 ) 

2592 

2593 def __repr__(self): 

2594 if self._inner_raw is not None: 

2595 return f"{self.__class__.__name__}({self._inner_raw!r})" 

2596 else: 

2597 return self.__class__.__name__ 

2598 

2599 @dataclasses.dataclass(unsafe_hash=True, match_args=False, slots=True) 

2600 class _TyWrapper: 

2601 inner: type 

2602 by_name: bool 

2603 to_dash_case: bool 

2604 

2605 

2606class _LiteralType: 

2607 def __init__(self, allowed_values: tuple[L, ...]) -> None: 

2608 self._allowed_values = allowed_values 

2609 

2610 def __instancecheck__(self, instance: _t.Any) -> bool: 

2611 return instance in self._allowed_values 

2612 

2613 

2614class Literal(_EnumBase[L, tuple[L, ...]], _t.Generic[L]): 

2615 """ 

2616 Parser for literal values. 

2617 

2618 This parser accepts a set of allowed values, and parses them using semantics of 

2619 :class:`Enum` parser. It can be used with creating an enum for some value isn't 

2620 practical, and semantics of :class:`OneOf` is limiting. 

2621 

2622 Allowed values should be strings, ints, bools, or instances of :class:`enum.Enum`. 

2623 

2624 If instances of :class:`enum.Enum` are passed, :class:`Literal` will rely on 

2625 enum's :data:`__yuio_by_name__` and :data:`__yuio_to_dash_case__` attributes 

2626 to parse these values. 

2627 

2628 """ 

2629 

2630 if TYPE_CHECKING: 

2631 

2632 def __new__(cls, *args: L) -> Literal[L]: ... 

2633 

2634 def __init__( 

2635 self, 

2636 *literal_values: L, 

2637 ): 

2638 self._converted_values = {} 

2639 

2640 for value in literal_values: 

2641 orig_value = value 

2642 

2643 if isinstance(value, enum.Enum): 

2644 if getattr(type(value), "__yuio_by_name__", False): 

2645 value = value.name 

2646 else: 

2647 value = value.value 

2648 if getattr(type(value), "__yuio_to_dash_case__", False) and isinstance( 

2649 value, str 

2650 ): 

2651 value = _to_dash_case(value) 

2652 self._converted_values[orig_value] = value 

2653 

2654 if not isinstance(value, (int, str, bool)): 

2655 raise TypeError( 

2656 f"literal parser doesn't support literals " 

2657 f"of type {_t.type_repr(type(value))}: {orig_value!r}" 

2658 ) 

2659 super().__init__( 

2660 literal_values, 

2661 _LiteralType(literal_values), # type: ignore 

2662 ) 

2663 

2664 def wrap(self, parser: Parser[_t.Any]) -> Parser[_t.Any]: 

2665 with self._patch_stack_summary(): 

2666 raise TypeError(f"annotating a type with {self} is not supported") 

2667 

2668 def _get_items(self) -> _t.Iterable[L]: 

2669 return self._inner 

2670 

2671 def _value_to_str(self, value: L) -> str: 

2672 return str(self._converted_values.get(value, value)) 

2673 

2674 def _str_value_matches(self, value: L, given: str) -> bool: 

2675 value = self._converted_values.get(value, value) 

2676 if isinstance(value, str): 

2677 return value == given 

2678 elif isinstance(value, bool): 

2679 try: 

2680 given_parsed = Bool().parse(given) 

2681 except ParsingError: 

2682 return False 

2683 else: 

2684 return value == given_parsed 

2685 elif isinstance(value, int): 

2686 try: 

2687 given_parsed = Int().parse(given) 

2688 except ParsingError: 

2689 return False 

2690 else: 

2691 return value == given_parsed 

2692 else: 

2693 return False 

2694 

2695 def _str_value_matches_prefix(self, value: L, given: str) -> bool: 

2696 value = self._converted_values.get(value, value) 

2697 return isinstance(value, str) and value.casefold().startswith(given.casefold()) 

2698 

2699 def _config_value_matches(self, value: L, given: object) -> bool: 

2700 value = self._converted_values.get(value, value) 

2701 return value == given 

2702 

2703 def _value_to_json(self, value: L) -> JsonValue: 

2704 return value # type: ignore 

2705 

2706 def __repr__(self): 

2707 if self._inner_raw is not None: 

2708 values = map(self._value_to_str, self._inner_raw) 

2709 return f"{self.__class__.__name__}({yuio.string.JoinRepr(values)})" 

2710 else: 

2711 return self.__class__.__name__ 

2712 

2713 

2714class Decimal(ValueParser[decimal.Decimal]): 

2715 """ 

2716 Parser for :class:`decimal.Decimal`. 

2717 

2718 """ 

2719 

2720 def __init__(self): 

2721 super().__init__(decimal.Decimal) 

2722 

2723 def parse_with_ctx(self, ctx: StrParsingContext, /) -> decimal.Decimal: 

2724 ctx = ctx.strip_if_non_space() 

2725 try: 

2726 return decimal.Decimal(ctx.value) 

2727 except (ArithmeticError, ValueError, TypeError): 

2728 raise ParsingError( 

2729 "Can't parse `%r` as `decimal`", 

2730 ctx.value, 

2731 ctx=ctx, 

2732 fallback_msg="Can't parse value as `decimal`", 

2733 ) from None 

2734 

2735 def parse_config_with_ctx(self, ctx: ConfigParsingContext, /) -> decimal.Decimal: 

2736 value = ctx.value 

2737 if not isinstance(value, (int, float, str, decimal.Decimal)): 

2738 raise ParsingError.type_mismatch(value, int, float, str, ctx=ctx) 

2739 try: 

2740 return decimal.Decimal(value) 

2741 except (ArithmeticError, ValueError, TypeError): 

2742 raise ParsingError( 

2743 "Can't parse `%r` as `decimal`", 

2744 value, 

2745 ctx=ctx, 

2746 fallback_msg="Can't parse value as `decimal`", 

2747 ) from None 

2748 

2749 def to_json_schema( 

2750 self, ctx: yuio.json_schema.JsonSchemaContext, / 

2751 ) -> yuio.json_schema.JsonSchemaType: 

2752 return ctx.add_type( 

2753 decimal.Decimal, 

2754 "Decimal", 

2755 lambda: yuio.json_schema.Meta( 

2756 yuio.json_schema.OneOf( 

2757 [ 

2758 yuio.json_schema.Number(), 

2759 yuio.json_schema.String( 

2760 pattern=r"(?i)^[+-]?((\d+\.\d*|\.?\d+)(e[+-]?\d+)?|inf(inity)?|(nan|snan)\d*)$" 

2761 ), 

2762 ] 

2763 ), 

2764 title="Decimal", 

2765 description="Decimal fixed-point and floating-point number.", 

2766 ), 

2767 ) 

2768 

2769 def to_json_value(self, value: object, /) -> yuio.json_schema.JsonValue: 

2770 assert self.assert_type(value) 

2771 return str(value) 

2772 

2773 

2774class Fraction(ValueParser[fractions.Fraction]): 

2775 """ 

2776 Parser for :class:`fractions.Fraction`. 

2777 

2778 """ 

2779 

2780 def __init__(self): 

2781 super().__init__(fractions.Fraction) 

2782 

2783 def parse_with_ctx(self, ctx: StrParsingContext, /) -> fractions.Fraction: 

2784 ctx = ctx.strip_if_non_space() 

2785 try: 

2786 return fractions.Fraction(ctx.value) 

2787 except ValueError: 

2788 raise ParsingError( 

2789 "Can't parse `%r` as `fraction`", 

2790 ctx.value, 

2791 ctx=ctx, 

2792 fallback_msg="Can't parse value as `fraction`", 

2793 ) from None 

2794 except ZeroDivisionError: 

2795 raise ParsingError( 

2796 "Can't parse `%r` as `fraction`, division by zero", 

2797 ctx.value, 

2798 ctx=ctx, 

2799 fallback_msg="Can't parse value as `fraction`", 

2800 ) from None 

2801 

2802 def parse_config_with_ctx(self, ctx: ConfigParsingContext, /) -> fractions.Fraction: 

2803 value = ctx.value 

2804 if ( 

2805 isinstance(value, (list, tuple)) 

2806 and len(value) == 2 

2807 and all(isinstance(v, (float, int)) for v in value) 

2808 ): 

2809 try: 

2810 return fractions.Fraction(*value) 

2811 except (ValueError, TypeError): 

2812 raise ParsingError( 

2813 "Can't parse `%s/%s` as `fraction`", 

2814 value[0], 

2815 value[1], 

2816 ctx=ctx, 

2817 fallback_msg="Can't parse value as `fraction`", 

2818 ) from None 

2819 except ZeroDivisionError: 

2820 raise ParsingError( 

2821 "Can't parse `%s/%s` as `fraction`, division by zero", 

2822 value[0], 

2823 value[1], 

2824 ctx=ctx, 

2825 fallback_msg="Can't parse value as `fraction`", 

2826 ) from None 

2827 if isinstance(value, (int, float, str, decimal.Decimal, fractions.Fraction)): 

2828 try: 

2829 return fractions.Fraction(value) 

2830 except (ValueError, TypeError): 

2831 raise ParsingError( 

2832 "Can't parse `%r` as `fraction`", 

2833 value, 

2834 ctx=ctx, 

2835 fallback_msg="Can't parse value as `fraction`", 

2836 ) from None 

2837 except ZeroDivisionError: 

2838 raise ParsingError( 

2839 "Can't parse `%r` as `fraction`, division by zero", 

2840 value, 

2841 ctx=ctx, 

2842 fallback_msg="Can't parse value as `fraction`", 

2843 ) from None 

2844 raise ParsingError.type_mismatch( 

2845 value, int, float, str, "a tuple of two ints", ctx=ctx 

2846 ) 

2847 

2848 def to_json_schema( 

2849 self, ctx: yuio.json_schema.JsonSchemaContext, / 

2850 ) -> yuio.json_schema.JsonSchemaType: 

2851 return ctx.add_type( 

2852 fractions.Fraction, 

2853 "Fraction", 

2854 lambda: yuio.json_schema.Meta( 

2855 yuio.json_schema.OneOf( 

2856 [ 

2857 yuio.json_schema.Number(), 

2858 yuio.json_schema.String( 

2859 pattern=r"(?i)^[+-]?(\d+(\/\d+)?|(\d+\.\d*|\.?\d+)(e[+-]?\d+)?|inf(inity)?|nan)$" 

2860 ), 

2861 yuio.json_schema.Tuple( 

2862 [yuio.json_schema.Number(), yuio.json_schema.Number()] 

2863 ), 

2864 ] 

2865 ), 

2866 title="Fraction", 

2867 description="A rational number.", 

2868 ), 

2869 ) 

2870 

2871 def to_json_value(self, value: object, /) -> yuio.json_schema.JsonValue: 

2872 assert self.assert_type(value) 

2873 return str(value) 

2874 

2875 

2876class Json(WrappingParser[T, Parser[T]], ValueParser[T], _t.Generic[T]): 

2877 """Json(inner: Parser[T] | None = None, /) 

2878 

2879 A parser that tries to parse value as JSON. 

2880 

2881 This parser will load JSON strings into python objects. 

2882 If `inner` parser is given, :class:`Json` will validate parsing results 

2883 by calling :meth:`~Parser.parse_config_with_ctx` on the inner parser. 

2884 

2885 :param inner: 

2886 a parser used to convert and validate contents of json. 

2887 

2888 """ 

2889 

2890 if TYPE_CHECKING: 

2891 

2892 @_t.overload 

2893 def __new__(cls, inner: Parser[T], /) -> Json[T]: ... 

2894 

2895 @_t.overload 

2896 def __new__(cls, /) -> Json[yuio.json_schema.JsonValue]: ... 

2897 

2898 def __new__(cls, inner: Parser[T] | None = None, /) -> Json[_t.Any]: ... 

2899 

2900 def __init__( 

2901 self, 

2902 inner: Parser[T] | None = None, 

2903 /, 

2904 ): 

2905 super().__init__(inner, object) 

2906 

2907 def wrap(self, parser: Parser[_t.Any]) -> Parser[_t.Any]: 

2908 result = _copy(self) 

2909 result._inner = parser 

2910 return result 

2911 

2912 def parse_with_ctx(self, ctx: StrParsingContext, /) -> T: 

2913 ctx = ctx.strip_if_non_space() 

2914 try: 

2915 config_value: JsonValue = json.loads(ctx.value) 

2916 except json.JSONDecodeError as e: 

2917 raise ParsingError( 

2918 "Can't parse `%r` as `JsonValue`:\n%s", 

2919 ctx.value, 

2920 yuio.string.Indent(e), 

2921 ctx=ctx, 

2922 fallback_msg="Can't parse value as `JsonValue`", 

2923 ) from None 

2924 try: 

2925 return self.parse_config_with_ctx(ConfigParsingContext(config_value)) 

2926 except ParsingError as e: 

2927 raise ParsingError( 

2928 "Error in parsed json value:\n%s", 

2929 yuio.string.Indent(e), 

2930 ctx=ctx, 

2931 fallback_msg="Error in parsed json value", 

2932 ) from None 

2933 

2934 def parse_config_with_ctx(self, ctx: ConfigParsingContext, /) -> T: 

2935 if self._inner_raw is not None: 

2936 return self._inner_raw.parse_config_with_ctx(ctx) 

2937 else: 

2938 return _t.cast(T, ctx.value) 

2939 

2940 def check_type(self, value: object, /) -> _t.TypeGuard[T]: 

2941 return True 

2942 

2943 def to_json_schema( 

2944 self, ctx: yuio.json_schema.JsonSchemaContext, / 

2945 ) -> yuio.json_schema.JsonSchemaType: 

2946 if self._inner_raw is not None: 

2947 return self._inner_raw.to_json_schema(ctx) 

2948 else: 

2949 return yuio.json_schema.Any() 

2950 

2951 def to_json_value(self, value: object, /) -> yuio.json_schema.JsonValue: 

2952 assert self.assert_type(value) 

2953 if self._inner_raw is not None: 

2954 return self._inner_raw.to_json_value(value) 

2955 return value # type: ignore 

2956 

2957 def __repr__(self): 

2958 if self._inner_raw is not None: 

2959 return f"{self.__class__.__name__}({self._inner_raw!r})" 

2960 else: 

2961 return super().__repr__() 

2962 

2963 

2964class DateTime(ValueParser[datetime.datetime]): 

2965 """ 

2966 Parse a datetime in ISO ('YYYY-MM-DD HH:MM:SS') format. 

2967 

2968 """ 

2969 

2970 def __init__(self): 

2971 super().__init__(datetime.datetime) 

2972 

2973 def parse_with_ctx(self, ctx: StrParsingContext, /) -> datetime.datetime: 

2974 ctx = ctx.strip_if_non_space() 

2975 return self._parse(ctx.value, ctx) 

2976 

2977 def parse_config_with_ctx(self, ctx: ConfigParsingContext, /) -> datetime.datetime: 

2978 value = ctx.value 

2979 if isinstance(value, datetime.datetime): 

2980 return value 

2981 elif isinstance(value, str): 

2982 return self._parse(value, ctx) 

2983 else: 

2984 raise ParsingError.type_mismatch(value, str, ctx=ctx) 

2985 

2986 @staticmethod 

2987 def _parse(value: str, ctx: ConfigParsingContext | StrParsingContext): 

2988 try: 

2989 return datetime.datetime.fromisoformat(value) 

2990 except ValueError: 

2991 raise ParsingError( 

2992 "Can't parse `%r` as `datetime`", 

2993 value, 

2994 ctx=ctx, 

2995 fallback_msg="Can't parse value as `datetime`", 

2996 ) from None 

2997 

2998 def describe(self) -> str | None: 

2999 return "YYYY-MM-DD[ HH:MM:SS]" 

3000 

3001 def to_json_schema( 

3002 self, ctx: yuio.json_schema.JsonSchemaContext, / 

3003 ) -> yuio.json_schema.JsonSchemaType: 

3004 return ctx.add_type( 

3005 datetime.datetime, 

3006 "DateTime", 

3007 lambda: yuio.json_schema.Meta( 

3008 yuio.json_schema.String( 

3009 pattern=( 

3010 r"^" 

3011 r"(" 

3012 r"\d{4}-W\d{2}(-\d)?" 

3013 r"|\d{4}-\d{2}-\d{2}" 

3014 r"|\d{4}W\d{2}\d?" 

3015 r"|\d{4}\d{2}\d{2}" 

3016 r")" 

3017 r"(" 

3018 r"[T ]" 

3019 r"\d{2}(:\d{2}(:\d{2}(.\d{3}(\d{3})?)?)?)?" 

3020 r"([+-]\d{2}(:\d{2}(:\d{2}(.\d{3}(\d{3})?)?)?)?|Z)?" 

3021 r")?" 

3022 r"$" 

3023 ) 

3024 ), 

3025 title="DateTime", 

3026 description="ISO 8601 datetime.", 

3027 ), 

3028 ) 

3029 

3030 def to_json_value(self, value: object, /) -> yuio.json_schema.JsonValue: 

3031 assert self.assert_type(value) 

3032 return str(value) 

3033 

3034 

3035class Date(ValueParser[datetime.date]): 

3036 """ 

3037 Parse a date in ISO ('YYYY-MM-DD') format. 

3038 

3039 """ 

3040 

3041 def __init__(self): 

3042 super().__init__(datetime.date) 

3043 

3044 def parse_with_ctx(self, ctx: StrParsingContext, /) -> datetime.date: 

3045 ctx = ctx.strip_if_non_space() 

3046 return self._parse(ctx.value, ctx) 

3047 

3048 def parse_config_with_ctx(self, ctx: ConfigParsingContext, /) -> datetime.date: 

3049 value = ctx.value 

3050 if isinstance(value, datetime.datetime): 

3051 return value.date() 

3052 elif isinstance(value, datetime.date): 

3053 return value 

3054 elif isinstance(value, str): 

3055 return self._parse(value, ctx) 

3056 else: 

3057 raise ParsingError.type_mismatch(value, str, ctx=ctx) 

3058 

3059 @staticmethod 

3060 def _parse(value: str, ctx: ConfigParsingContext | StrParsingContext): 

3061 try: 

3062 return datetime.date.fromisoformat(value) 

3063 except ValueError: 

3064 raise ParsingError( 

3065 "Can't parse `%r` as `date`", 

3066 value, 

3067 ctx=ctx, 

3068 fallback_msg="Can't parse value as `date`", 

3069 ) from None 

3070 

3071 def describe(self) -> str | None: 

3072 return "YYYY-MM-DD" 

3073 

3074 def to_json_schema( 

3075 self, ctx: yuio.json_schema.JsonSchemaContext, / 

3076 ) -> yuio.json_schema.JsonSchemaType: 

3077 return ctx.add_type( 

3078 datetime.date, 

3079 "Date", 

3080 lambda: yuio.json_schema.Meta( 

3081 yuio.json_schema.String( 

3082 pattern=( 

3083 r"^" 

3084 r"(" 

3085 r"\d{4}-W\d{2}(-\d)?" 

3086 r"|\d{4}-\d{2}-\d{2}" 

3087 r"|\d{4}W\d{2}\d?" 

3088 r"|\d{4}\d{2}\d{2}" 

3089 r")" 

3090 r"$" 

3091 ) 

3092 ), 

3093 title="Date", 

3094 description="ISO 8601 date.", 

3095 ), 

3096 ) 

3097 

3098 def to_json_value(self, value: object, /) -> yuio.json_schema.JsonValue: 

3099 assert self.assert_type(value) 

3100 return str(value) 

3101 

3102 

3103class Time(ValueParser[datetime.time]): 

3104 """ 

3105 Parse a time in ISO ('HH:MM:SS') format. 

3106 

3107 """ 

3108 

3109 def __init__(self): 

3110 super().__init__(datetime.time) 

3111 

3112 def parse_with_ctx(self, ctx: StrParsingContext, /) -> datetime.time: 

3113 ctx = ctx.strip_if_non_space() 

3114 return self._parse(ctx.value, ctx) 

3115 

3116 def parse_config_with_ctx(self, ctx: ConfigParsingContext, /) -> datetime.time: 

3117 value = ctx.value 

3118 if isinstance(value, datetime.datetime): 

3119 return value.time() 

3120 elif isinstance(value, datetime.time): 

3121 return value 

3122 elif isinstance(value, str): 

3123 return self._parse(value, ctx) 

3124 else: 

3125 raise ParsingError.type_mismatch(value, str, ctx=ctx) 

3126 

3127 @staticmethod 

3128 def _parse(value: str, ctx: ConfigParsingContext | StrParsingContext): 

3129 try: 

3130 return datetime.time.fromisoformat(value) 

3131 except ValueError: 

3132 raise ParsingError( 

3133 "Can't parse `%r` as `time`", 

3134 value, 

3135 ctx=ctx, 

3136 fallback_msg="Can't parse value as `time`", 

3137 ) from None 

3138 

3139 def describe(self) -> str | None: 

3140 return "HH:MM:SS" 

3141 

3142 def to_json_schema( 

3143 self, ctx: yuio.json_schema.JsonSchemaContext, / 

3144 ) -> yuio.json_schema.JsonSchemaType: 

3145 return ctx.add_type( 

3146 datetime.time, 

3147 "Time", 

3148 lambda: yuio.json_schema.Meta( 

3149 yuio.json_schema.String( 

3150 pattern=( 

3151 r"^" 

3152 r"\d{2}(:\d{2}(:\d{2}(.\d{3}(\d{3})?)?)?)?" 

3153 r"([+-]\d{2}(:\d{2}(:\d{2}(.\d{3}(\d{3})?)?)?)?|Z)?" 

3154 r"$" 

3155 ) 

3156 ), 

3157 title="Time", 

3158 description="ISO 8601 time.", 

3159 ), 

3160 ) 

3161 

3162 def to_json_value(self, value: object, /) -> yuio.json_schema.JsonValue: 

3163 assert self.assert_type(value) 

3164 return str(value) 

3165 

3166 

3167_UNITS_MAP = ( 

3168 ("days", ("d", "day", "days")), 

3169 ("seconds", ("s", "sec", "secs", "second", "seconds")), 

3170 ("microseconds", ("us", "u", "micro", "micros", "microsecond", "microseconds")), 

3171 ("milliseconds", ("ms", "l", "milli", "millis", "millisecond", "milliseconds")), 

3172 ("minutes", ("m", "min", "mins", "minute", "minutes")), 

3173 ("hours", ("h", "hr", "hrs", "hour", "hours")), 

3174 ("weeks", ("w", "week", "weeks")), 

3175) 

3176 

3177_UNITS = {unit: name for name, units in _UNITS_MAP for unit in units} 

3178 

3179_TIMEDELTA_RE = re.compile( 

3180 r""" 

3181 # General format: -1 day, -01:00:00.000000 

3182 ^ 

3183 (?:([+-]?)\s*((?:\d+\s*[a-z]+\s*)+))? 

3184 (?:,\s*)? 

3185 (?:([+-]?)\s*(\d+):(\d?\d)(?::(\d?\d)(?:\.(?:(\d\d\d)(\d\d\d)?))?)?)? 

3186 $ 

3187 """, 

3188 re.VERBOSE | re.IGNORECASE, 

3189) 

3190 

3191_COMPONENT_RE = re.compile(r"(\d+)\s*([a-z]+)\s*") 

3192 

3193 

3194class TimeDelta(ValueParser[datetime.timedelta]): 

3195 """ 

3196 Parse a time delta. 

3197 

3198 """ 

3199 

3200 def __init__(self): 

3201 super().__init__(datetime.timedelta) 

3202 

3203 def parse_with_ctx(self, ctx: StrParsingContext, /) -> datetime.timedelta: 

3204 ctx = ctx.strip_if_non_space() 

3205 return self._parse(ctx.value, ctx) 

3206 

3207 def parse_config_with_ctx(self, ctx: ConfigParsingContext, /) -> datetime.timedelta: 

3208 value = ctx.value 

3209 if isinstance(value, datetime.timedelta): 

3210 return value 

3211 elif isinstance(value, str): 

3212 return self._parse(value, ctx) 

3213 else: 

3214 raise ParsingError.type_mismatch(value, str, ctx=ctx) 

3215 

3216 @staticmethod 

3217 def _parse(value: str, ctx: ConfigParsingContext | StrParsingContext): 

3218 value = value.strip() 

3219 

3220 if not value: 

3221 raise ParsingError("Got an empty `timedelta`", ctx=ctx) 

3222 if value.endswith(","): 

3223 raise ParsingError( 

3224 "Can't parse `%r` as `timedelta`, trailing comma is not allowed", 

3225 value, 

3226 ctx=ctx, 

3227 fallback_msg="Can't parse value as `timedelta`", 

3228 ) 

3229 if value.startswith(","): 

3230 raise ParsingError( 

3231 "Can't parse `%r` as `timedelta`, leading comma is not allowed", 

3232 value, 

3233 ctx=ctx, 

3234 fallback_msg="Can't parse value as `timedelta`", 

3235 ) 

3236 

3237 if match := _TIMEDELTA_RE.match(value): 

3238 ( 

3239 c_sign_s, 

3240 components_s, 

3241 t_sign_s, 

3242 hour, 

3243 minute, 

3244 second, 

3245 millisecond, 

3246 microsecond, 

3247 ) = match.groups() 

3248 else: 

3249 raise ParsingError( 

3250 "Can't parse `%r` as `timedelta`", 

3251 value, 

3252 ctx=ctx, 

3253 fallback_msg="Can't parse value as `timedelta`", 

3254 ) 

3255 

3256 c_sign_s = -1 if c_sign_s == "-" else 1 

3257 t_sign_s = -1 if t_sign_s == "-" else 1 

3258 

3259 kwargs = {u: 0 for u, _ in _UNITS_MAP} 

3260 

3261 if components_s: 

3262 for num, unit in _COMPONENT_RE.findall(components_s): 

3263 if unit_key := _UNITS.get(unit.lower()): 

3264 kwargs[unit_key] += int(num) 

3265 else: 

3266 raise ParsingError( 

3267 "Can't parse `%r` as `timedelta`, unknown unit `%r`", 

3268 value, 

3269 unit, 

3270 ctx=ctx, 

3271 fallback_msg="Can't parse value as `timedelta`", 

3272 ) 

3273 

3274 timedelta = c_sign_s * datetime.timedelta(**kwargs) 

3275 

3276 timedelta += t_sign_s * datetime.timedelta( 

3277 hours=int(hour or "0"), 

3278 minutes=int(minute or "0"), 

3279 seconds=int(second or "0"), 

3280 milliseconds=int(millisecond or "0"), 

3281 microseconds=int(microsecond or "0"), 

3282 ) 

3283 

3284 return timedelta 

3285 

3286 def describe(self) -> str | None: 

3287 return "[+|-]HH:MM:SS" 

3288 

3289 def to_json_schema( 

3290 self, ctx: yuio.json_schema.JsonSchemaContext, / 

3291 ) -> yuio.json_schema.JsonSchemaType: 

3292 return ctx.add_type( 

3293 datetime.timedelta, 

3294 "TimeDelta", 

3295 lambda: yuio.json_schema.Meta( 

3296 yuio.json_schema.String( 

3297 # save yourself some trouble, paste this into https://regexper.com/ 

3298 pattern=( 

3299 r"^(([+-]?\s*(\d+\s*(d|day|days|s|sec|secs|second|seconds" 

3300 r"|us|u|micro|micros|microsecond|microseconds|ms|l|milli|" 

3301 r"millis|millisecond|milliseconds|m|min|mins|minute|minutes" 

3302 r"|h|hr|hrs|hour|hours|w|week|weeks)\s*)+)(,\s*)?" 

3303 r"([+-]?\s*\d+:\d?\d(:\d?\d(\.\d\d\d(\d\d\d)?)?)?)" 

3304 r"|([+-]?\s*\d+:\d?\d(:\d?\d(\.\d\d\d(\d\d\d)?)?)?)" 

3305 r"|([+-]?\s*(\d+\s*(d|day|days|s|sec|secs|second|seconds" 

3306 r"|us|u|micro|micros|microsecond|microseconds|ms|l|milli" 

3307 r"|millis|millisecond|milliseconds|m|min|mins|minute|minutes" 

3308 r"|h|hr|hrs|hour|hours|w|week|weeks)\s*)+))$" 

3309 ) 

3310 ), 

3311 title="Time delta. General format: '[+-] [M weeks] [N days] [+-]HH:MM:SS'", 

3312 description=".", 

3313 ), 

3314 ) 

3315 

3316 def to_json_value(self, value: object, /) -> yuio.json_schema.JsonValue: 

3317 assert self.assert_type(value) 

3318 return str(value) 

3319 

3320 

3321class Seconds(TimeDelta): 

3322 """ 

3323 Parse a float and convert it to a time delta as a number of seconds. 

3324 

3325 """ 

3326 

3327 @staticmethod 

3328 def _parse(value: str, ctx: ConfigParsingContext | StrParsingContext): 

3329 try: 

3330 seconds = float(value) 

3331 except ValueError: 

3332 raise ParsingError( 

3333 "Can't parse `%r` as `<seconds>`", 

3334 ctx.value, 

3335 ctx=ctx, 

3336 fallback_msg="Can't parse value as `<seconds>`", 

3337 ) from None 

3338 return datetime.timedelta(seconds=seconds) 

3339 

3340 def describe(self) -> str | None: 

3341 return "<seconds>" 

3342 

3343 def describe_or_def(self) -> str: 

3344 return "<seconds>" 

3345 

3346 def describe_many(self) -> str | tuple[str, ...]: 

3347 return "<seconds>" 

3348 

3349 def describe_value(self, value: object) -> str: 

3350 assert self.assert_type(value) 

3351 return str(value.total_seconds()) 

3352 

3353 def to_json_schema( 

3354 self, ctx: yuio.json_schema.JsonSchemaContext, / 

3355 ) -> yuio.json_schema.JsonSchemaType: 

3356 return yuio.json_schema.Meta(yuio.json_schema.Number(), description="seconds") 

3357 

3358 def to_json_value(self, value: object, /) -> yuio.json_schema.JsonValue: 

3359 assert self.assert_type(value) 

3360 return value.total_seconds() 

3361 

3362 

3363class Path(ValueParser[pathlib.Path]): 

3364 """ 

3365 Parse a file system path, return a :class:`pathlib.Path`. 

3366 

3367 :param extensions: 

3368 list of allowed file extensions, including preceding dots. 

3369 

3370 """ 

3371 

3372 def __init__( 

3373 self, 

3374 /, 

3375 *, 

3376 extensions: str | _t.Collection[str] | None = None, 

3377 ): 

3378 self._extensions = [extensions] if isinstance(extensions, str) else extensions 

3379 super().__init__(pathlib.Path) 

3380 

3381 def parse_with_ctx(self, ctx: StrParsingContext, /) -> pathlib.Path: 

3382 ctx = ctx.strip_if_non_space() 

3383 return self._parse(ctx.value, ctx) 

3384 

3385 def parse_config_with_ctx(self, ctx: ConfigParsingContext, /) -> pathlib.Path: 

3386 value = ctx.value 

3387 if not isinstance(value, str): 

3388 raise ParsingError.type_mismatch(value, str, ctx=ctx) 

3389 return self._parse(value, ctx) 

3390 

3391 def _parse(self, value: str, ctx: ConfigParsingContext | StrParsingContext): 

3392 res = pathlib.Path(value).expanduser().resolve().absolute() 

3393 try: 

3394 self._validate(res) 

3395 except ParsingError as e: 

3396 e.set_ctx(ctx) 

3397 raise 

3398 return res 

3399 

3400 def describe(self) -> str | None: 

3401 if self._extensions is not None: 

3402 desc = "|".join(f"<*{e}>" for e in self._extensions) 

3403 if len(self._extensions) > 1: 

3404 desc = f"{{{desc}}}" 

3405 return desc 

3406 else: 

3407 return super().describe() 

3408 

3409 def _validate(self, value: pathlib.Path, /): 

3410 if self._extensions is not None and not any( 

3411 value.name.endswith(ext) for ext in self._extensions 

3412 ): 

3413 raise ParsingError( 

3414 "<c path>%s</c> should have extension %s", 

3415 value, 

3416 yuio.string.Or(self._extensions), 

3417 ) 

3418 

3419 def completer(self) -> yuio.complete.Completer | None: 

3420 return yuio.complete.File(extensions=self._extensions) 

3421 

3422 def to_json_schema( 

3423 self, ctx: yuio.json_schema.JsonSchemaContext, / 

3424 ) -> yuio.json_schema.JsonSchemaType: 

3425 return yuio.json_schema.String() 

3426 

3427 def to_json_value(self, value: object, /) -> yuio.json_schema.JsonValue: 

3428 assert self.assert_type(value) 

3429 return str(value) 

3430 

3431 

3432class NonExistentPath(Path): 

3433 """ 

3434 Parse a file system path and verify that it doesn't exist. 

3435 

3436 :param extensions: 

3437 list of allowed file extensions, including preceding dots. 

3438 

3439 """ 

3440 

3441 def _validate(self, value: pathlib.Path, /): 

3442 super()._validate(value) 

3443 

3444 if value.exists(): 

3445 raise ParsingError("<c path>%s</c> already exists", value) 

3446 

3447 

3448class ExistingPath(Path): 

3449 """ 

3450 Parse a file system path and verify that it exists. 

3451 

3452 :param extensions: 

3453 list of allowed file extensions, including preceding dots. 

3454 

3455 """ 

3456 

3457 def _validate(self, value: pathlib.Path, /): 

3458 super()._validate(value) 

3459 

3460 if not value.exists(): 

3461 raise ParsingError("<c path>%s</c> doesn't exist", value) 

3462 

3463 

3464class File(ExistingPath): 

3465 """ 

3466 Parse a file system path and verify that it points to a regular file. 

3467 

3468 :param extensions: 

3469 list of allowed file extensions, including preceding dots. 

3470 

3471 """ 

3472 

3473 def _validate(self, value: pathlib.Path, /): 

3474 super()._validate(value) 

3475 

3476 if not value.is_file(): 

3477 raise ParsingError("<c path>%s</c> is not a file", value) 

3478 

3479 

3480class Dir(ExistingPath): 

3481 """ 

3482 Parse a file system path and verify that it points to a directory. 

3483 

3484 """ 

3485 

3486 def __init__(self): 

3487 # Disallow passing `extensions`. 

3488 super().__init__() 

3489 

3490 def _validate(self, value: pathlib.Path, /): 

3491 super()._validate(value) 

3492 

3493 if not value.is_dir(): 

3494 raise ParsingError("<c path>%s</c> is not a directory", value) 

3495 

3496 def completer(self) -> yuio.complete.Completer | None: 

3497 return yuio.complete.Dir() 

3498 

3499 

3500class GitRepo(Dir): 

3501 """ 

3502 Parse a file system path and verify that it points to a git repository. 

3503 

3504 This parser just checks that the given directory has 

3505 a subdirectory named ``.git``. 

3506 

3507 """ 

3508 

3509 def _validate(self, value: pathlib.Path, /): 

3510 super()._validate(value) 

3511 

3512 if not value.joinpath(".git").is_dir(): 

3513 raise ParsingError("<c path>%s</c> is not a git repository root", value) 

3514 

3515 

3516class Secret(Map[SecretValue[T], T], _t.Generic[T]): 

3517 """Secret(inner: Parser[U], /) 

3518 

3519 Wraps result of the inner parser into :class:`~yuio.secret.SecretValue` 

3520 and ensures that :func:`yuio.io.ask` doesn't show value as user enters it. 

3521 

3522 """ 

3523 

3524 if TYPE_CHECKING: 

3525 

3526 @_t.overload 

3527 def __new__(cls, inner: Parser[T], /) -> Secret[T]: ... 

3528 

3529 @_t.overload 

3530 def __new__(cls, /) -> PartialParser: ... 

3531 

3532 def __new__(cls, *args, **kwargs) -> _t.Any: ... 

3533 

3534 def __init__(self, inner: Parser[U] | None = None, /): 

3535 super().__init__(inner, SecretValue, lambda x: x.data) 

3536 

3537 def parse_with_ctx(self, ctx: StrParsingContext, /) -> SecretValue[T]: 

3538 with self._replace_error(): 

3539 return super().parse_with_ctx(ctx) 

3540 

3541 def parse_many_with_ctx( 

3542 self, ctxs: _t.Sequence[StrParsingContext], / 

3543 ) -> SecretValue[T]: 

3544 with self._replace_error(): 

3545 return super().parse_many_with_ctx(ctxs) 

3546 

3547 def parse_config_with_ctx(self, ctx: ConfigParsingContext, /) -> SecretValue[T]: 

3548 with self._replace_error(): 

3549 return super().parse_config_with_ctx(ctx) 

3550 

3551 @staticmethod 

3552 @contextlib.contextmanager 

3553 def _replace_error(): 

3554 try: 

3555 yield 

3556 except ParsingError as e: 

3557 # Error messages can contain secret value, hide them. 

3558 raise ParsingError( 

3559 yuio.string.Printable( 

3560 e.fallback_msg or "Error when parsing secret data" 

3561 ), 

3562 pos=e.pos, 

3563 path=e.path, 

3564 n_arg=e.n_arg, 

3565 # Omit raw value. 

3566 ) from None 

3567 

3568 def describe_value(self, value: object, /) -> str: 

3569 return "***" 

3570 

3571 def completer(self) -> yuio.complete.Completer | None: 

3572 return None 

3573 

3574 def options(self) -> _t.Collection[yuio.widget.Option[SecretValue[T]]] | None: 

3575 return None 

3576 

3577 def widget( 

3578 self, 

3579 default: object | yuio.Missing, 

3580 input_description: str | None, 

3581 default_description: str | None, 

3582 /, 

3583 ) -> yuio.widget.Widget[SecretValue[T] | yuio.Missing]: 

3584 return _secret_widget(self, default, input_description, default_description) 

3585 

3586 def is_secret(self) -> bool: 

3587 return True 

3588 

3589 

3590class CollectionParser( 

3591 WrappingParser[C, Parser[T]], ValueParser[C], PartialParser, _t.Generic[C, T] 

3592): 

3593 """CollectionParser(inner: Parser[T] | None, /, **kwargs) 

3594 

3595 A base class for implementing collection parsing. It will split a string 

3596 by the given delimiter, parse each item using a subparser, and then pass 

3597 the result to the given constructor. 

3598 

3599 :param inner: 

3600 parser that will be used to parse collection items. 

3601 :param ty: 

3602 type of the collection that this parser returns. 

3603 :param ctor: 

3604 factory of instances of the collection that this parser returns. 

3605 It should take an iterable of parsed items, and return a collection. 

3606 :param iter: 

3607 a function that is used to get an iterator from a collection. 

3608 This defaults to :func:`iter`, but sometimes it may be different. 

3609 For example, :class:`Dict` is implemented as a collection of pairs, 

3610 and its `iter` is :meth:`dict.items`. 

3611 :param config_type: 

3612 type of a collection that we expect to find when parsing a config. 

3613 This will usually be a list. 

3614 :param config_type_iter: 

3615 a function that is used to get an iterator from a config value. 

3616 :param delimiter: 

3617 delimiter that will be passed to :py:meth:`str.split`. 

3618 

3619 The above parameters are exposed via protected attributes: 

3620 ``self._inner``, ``self._ty``, etc. 

3621 

3622 For example, let's implement a :class:`list` parser 

3623 that repeats each element twice: 

3624 

3625 .. code-block:: python 

3626 

3627 from typing import Iterable, Generic 

3628 

3629 

3630 class DoubleList(CollectionParser[list[T], T], Generic[T]): 

3631 def __init__(self, inner: Parser[T], /, *, delimiter: str | None = None): 

3632 super().__init__(inner, ty=list, ctor=self._ctor, delimiter=delimiter) 

3633 

3634 @staticmethod 

3635 def _ctor(values: Iterable[T]) -> list[T]: 

3636 return [x for value in values for x in [value, value]] 

3637 

3638 def to_json_schema( 

3639 self, ctx: yuio.json_schema.JsonSchemaContext, / 

3640 ) -> yuio.json_schema.JsonSchemaType: 

3641 return {"type": "array", "items": self._inner.to_json_schema(ctx)} 

3642 

3643 def to_json_value(self, value: object, /) -> yuio.json_schema.JsonValue: 

3644 assert self.assert_type(value) 

3645 return [self._inner.to_json_value(item) for item in value[::2]] 

3646 

3647 :: 

3648 

3649 >>> parser = DoubleList(Int()) 

3650 >>> parser.parse("1 2 3") 

3651 [1, 1, 2, 2, 3, 3] 

3652 >>> parser.to_json_value([1, 1, 2, 2, 3, 3]) 

3653 [1, 2, 3] 

3654 

3655 """ 

3656 

3657 _allow_completing_duplicates: typing.ClassVar[bool] = True 

3658 """ 

3659 If set to :data:`False`, autocompletion will not suggest item duplicates. 

3660 

3661 """ 

3662 

3663 def __init__( 

3664 self, 

3665 inner: Parser[T] | None, 

3666 /, 

3667 *, 

3668 ty: type[C], 

3669 ctor: _t.Callable[[_t.Iterable[T]], C], 

3670 iter: _t.Callable[[C], _t.Iterable[T]] = iter, 

3671 config_type: type[C2] | tuple[type[C2], ...] = list, 

3672 config_type_iter: _t.Callable[[C2], _t.Iterable[T]] = iter, 

3673 delimiter: str | None = None, 

3674 ): 

3675 if delimiter == "": 

3676 raise ValueError("empty delimiter") 

3677 

3678 #: See class parameters for more details. 

3679 self._ty = ty 

3680 self._ctor = ctor 

3681 self._iter = iter 

3682 self._config_type = config_type 

3683 self._config_type_iter = config_type_iter 

3684 self._delimiter = delimiter 

3685 

3686 super().__init__(inner, ty) 

3687 

3688 def wrap(self: P, parser: Parser[_t.Any]) -> P: 

3689 result = super().wrap(parser) 

3690 result._inner = parser._inner # type: ignore 

3691 return result 

3692 

3693 def parse_with_ctx(self, ctx: StrParsingContext, /) -> C: 

3694 return self._ctor( 

3695 self._inner.parse_with_ctx(item) for item in ctx.split(self._delimiter) 

3696 ) 

3697 

3698 def parse_many_with_ctx(self, ctxs: _t.Sequence[StrParsingContext], /) -> C: 

3699 return self._ctor(self._inner.parse_with_ctx(item) for item in ctxs) 

3700 

3701 def supports_parse_many(self) -> bool: 

3702 return True 

3703 

3704 def parse_config_with_ctx(self, ctx: ConfigParsingContext, /) -> C: 

3705 value = ctx.value 

3706 if not isinstance(value, self._config_type): 

3707 expected = self._config_type 

3708 if not isinstance(expected, tuple): 

3709 expected = (expected,) 

3710 raise ParsingError.type_mismatch(value, *expected, ctx=ctx) 

3711 

3712 return self._ctor( 

3713 self._inner.parse_config_with_ctx(ctx.descend(item, i)) 

3714 for i, item in enumerate(self._config_type_iter(value)) 

3715 ) 

3716 

3717 def get_nargs(self) -> _t.Literal["+", "*"] | int: 

3718 return "*" 

3719 

3720 def describe(self) -> str | None: 

3721 delimiter = self._delimiter or " " 

3722 value = self._inner.describe_or_def() 

3723 

3724 return f"{value}[{delimiter}{value}[{delimiter}...]]" 

3725 

3726 def describe_many(self) -> str | tuple[str, ...]: 

3727 return self._inner.describe_or_def() 

3728 

3729 def describe_value(self, value: object, /) -> str: 

3730 assert self.assert_type(value) 

3731 

3732 return (self._delimiter or " ").join( 

3733 self._inner.describe_value(item) for item in self._iter(value) 

3734 ) 

3735 

3736 def options(self) -> _t.Collection[yuio.widget.Option[C]] | None: 

3737 return None 

3738 

3739 def completer(self) -> yuio.complete.Completer | None: 

3740 completer = self._inner.completer() 

3741 return ( 

3742 yuio.complete.List( 

3743 completer, 

3744 delimiter=self._delimiter, 

3745 allow_duplicates=self._allow_completing_duplicates, 

3746 ) 

3747 if completer is not None 

3748 else None 

3749 ) 

3750 

3751 def widget( 

3752 self, 

3753 default: object | yuio.Missing, 

3754 input_description: str | None, 

3755 default_description: str | None, 

3756 /, 

3757 ) -> yuio.widget.Widget[C | yuio.Missing]: 

3758 completer = self.completer() 

3759 return _WidgetResultMapper( 

3760 self, 

3761 input_description, 

3762 default, 

3763 ( 

3764 yuio.widget.InputWithCompletion( 

3765 completer, 

3766 placeholder=default_description or "", 

3767 ) 

3768 if completer is not None 

3769 else yuio.widget.Input( 

3770 placeholder=default_description or "", 

3771 ) 

3772 ), 

3773 ) 

3774 

3775 def is_secret(self) -> bool: 

3776 return self._inner.is_secret() 

3777 

3778 def __repr__(self): 

3779 if self._inner_raw is not None: 

3780 return f"{self.__class__.__name__}({self._inner_raw!r})" 

3781 else: 

3782 return self.__class__.__name__ 

3783 

3784 

3785class List(CollectionParser[list[T], T], _t.Generic[T]): 

3786 """List(inner: Parser[T], /, *, delimiter: str | None = None) 

3787 

3788 Parser for lists. 

3789 

3790 Will split a string by the given delimiter, and parse each item 

3791 using a subparser. 

3792 

3793 :param inner: 

3794 inner parser that will be used to parse list items. 

3795 :param delimiter: 

3796 delimiter that will be passed to :py:meth:`str.split`. 

3797 

3798 """ 

3799 

3800 if TYPE_CHECKING: 

3801 

3802 @_t.overload 

3803 def __new__( 

3804 cls, inner: Parser[T], /, *, delimiter: str | None = None 

3805 ) -> List[T]: ... 

3806 

3807 @_t.overload 

3808 def __new__(cls, /, *, delimiter: str | None = None) -> PartialParser: ... 

3809 

3810 def __new__(cls, *args, **kwargs) -> _t.Any: ... 

3811 

3812 def __init__( 

3813 self, 

3814 inner: Parser[T] | None = None, 

3815 /, 

3816 *, 

3817 delimiter: str | None = None, 

3818 ): 

3819 super().__init__(inner, ty=list, ctor=list, delimiter=delimiter) 

3820 

3821 def to_json_schema( 

3822 self, ctx: yuio.json_schema.JsonSchemaContext, / 

3823 ) -> yuio.json_schema.JsonSchemaType: 

3824 return yuio.json_schema.Array(self._inner.to_json_schema(ctx)) 

3825 

3826 def to_json_value(self, value: object, /) -> yuio.json_schema.JsonValue: 

3827 assert self.assert_type(value) 

3828 return [self._inner.to_json_value(item) for item in value] 

3829 

3830 

3831class Set(CollectionParser[set[T], T], _t.Generic[T]): 

3832 """Set(inner: Parser[T], /, *, delimiter: str | None = None) 

3833 

3834 Parser for sets. 

3835 

3836 Will split a string by the given delimiter, and parse each item 

3837 using a subparser. 

3838 

3839 :param inner: 

3840 inner parser that will be used to parse set items. 

3841 :param delimiter: 

3842 delimiter that will be passed to :py:meth:`str.split`. 

3843 

3844 """ 

3845 

3846 if TYPE_CHECKING: 

3847 

3848 @_t.overload 

3849 def __new__( 

3850 cls, inner: Parser[T], /, *, delimiter: str | None = None 

3851 ) -> Set[T]: ... 

3852 

3853 @_t.overload 

3854 def __new__(cls, /, *, delimiter: str | None = None) -> PartialParser: ... 

3855 

3856 def __new__(cls, *args, **kwargs) -> _t.Any: ... 

3857 

3858 _allow_completing_duplicates = False 

3859 

3860 def __init__( 

3861 self, 

3862 inner: Parser[T] | None = None, 

3863 /, 

3864 *, 

3865 delimiter: str | None = None, 

3866 ): 

3867 super().__init__(inner, ty=set, ctor=set, delimiter=delimiter) 

3868 

3869 def widget( 

3870 self, 

3871 default: object | yuio.Missing, 

3872 input_description: str | None, 

3873 default_description: str | None, 

3874 /, 

3875 ) -> yuio.widget.Widget[set[T] | yuio.Missing]: 

3876 options = self._inner.options() 

3877 if options and len(options) <= 25: 

3878 return yuio.widget.Map(yuio.widget.Multiselect(list(options)), set) 

3879 else: 

3880 return super().widget(default, input_description, default_description) 

3881 

3882 def to_json_schema( 

3883 self, ctx: yuio.json_schema.JsonSchemaContext, / 

3884 ) -> yuio.json_schema.JsonSchemaType: 

3885 return yuio.json_schema.Array( 

3886 self._inner.to_json_schema(ctx), unique_items=True 

3887 ) 

3888 

3889 def to_json_value(self, value: object, /) -> yuio.json_schema.JsonValue: 

3890 assert self.assert_type(value) 

3891 return [self._inner.to_json_value(item) for item in value] 

3892 

3893 

3894class FrozenSet(CollectionParser[frozenset[T], T], _t.Generic[T]): 

3895 """FrozenSet(inner: Parser[T], /, *, delimiter: str | None = None) 

3896 

3897 Parser for frozen sets. 

3898 

3899 Will split a string by the given delimiter, and parse each item 

3900 using a subparser. 

3901 

3902 :param inner: 

3903 inner parser that will be used to parse set items. 

3904 :param delimiter: 

3905 delimiter that will be passed to :py:meth:`str.split`. 

3906 

3907 """ 

3908 

3909 if TYPE_CHECKING: 

3910 

3911 @_t.overload 

3912 def __new__( 

3913 cls, inner: Parser[T], /, *, delimiter: str | None = None 

3914 ) -> FrozenSet[T]: ... 

3915 

3916 @_t.overload 

3917 def __new__(cls, /, *, delimiter: str | None = None) -> PartialParser: ... 

3918 

3919 def __new__(cls, *args, **kwargs) -> _t.Any: ... 

3920 

3921 _allow_completing_duplicates = False 

3922 

3923 def __init__( 

3924 self, 

3925 inner: Parser[T] | None = None, 

3926 /, 

3927 *, 

3928 delimiter: str | None = None, 

3929 ): 

3930 super().__init__(inner, ty=frozenset, ctor=frozenset, delimiter=delimiter) 

3931 

3932 def to_json_schema( 

3933 self, ctx: yuio.json_schema.JsonSchemaContext, / 

3934 ) -> yuio.json_schema.JsonSchemaType: 

3935 return yuio.json_schema.Array( 

3936 self._inner.to_json_schema(ctx), unique_items=True 

3937 ) 

3938 

3939 def to_json_value(self, value: object, /) -> yuio.json_schema.JsonValue: 

3940 assert self.assert_type(value) 

3941 return [self._inner.to_json_value(item) for item in value] 

3942 

3943 

3944class Dict(CollectionParser[dict[K, V], tuple[K, V]], _t.Generic[K, V]): 

3945 """Dict(key: Parser[K], value: Parser[V], /, *, delimiter: str | None = None, pair_delimiter: str = ":") 

3946 

3947 Parser for dicts. 

3948 

3949 Will split a string by the given delimiter, and parse each item 

3950 using a :py:class:`Tuple` parser. 

3951 

3952 :param key: 

3953 inner parser that will be used to parse dict keys. 

3954 :param value: 

3955 inner parser that will be used to parse dict values. 

3956 :param delimiter: 

3957 delimiter that will be passed to :py:meth:`str.split`. 

3958 :param pair_delimiter: 

3959 delimiter that will be used to split key-value elements. 

3960 

3961 """ 

3962 

3963 if TYPE_CHECKING: 

3964 

3965 @_t.overload 

3966 def __new__( 

3967 cls, 

3968 key: Parser[K], 

3969 value: Parser[V], 

3970 /, 

3971 *, 

3972 delimiter: str | None = None, 

3973 pair_delimiter: str = ":", 

3974 ) -> Dict[K, V]: ... 

3975 

3976 @_t.overload 

3977 def __new__( 

3978 cls, 

3979 /, 

3980 *, 

3981 delimiter: str | None = None, 

3982 pair_delimiter: str = ":", 

3983 ) -> PartialParser: ... 

3984 

3985 def __new__(cls, *args, **kwargs) -> _t.Any: ... 

3986 

3987 _allow_completing_duplicates = False 

3988 

3989 def __init__( 

3990 self, 

3991 key: Parser[K] | None = None, 

3992 value: Parser[V] | None = None, 

3993 /, 

3994 *, 

3995 delimiter: str | None = None, 

3996 pair_delimiter: str = ":", 

3997 ): 

3998 self._pair_delimiter = pair_delimiter 

3999 super().__init__( 

4000 ( 

4001 _DictElementParser(key, value, delimiter=pair_delimiter) 

4002 if key and value 

4003 else None 

4004 ), 

4005 ty=dict, 

4006 ctor=dict, 

4007 iter=dict.items, 

4008 config_type=(dict, list), 

4009 config_type_iter=self.__config_type_iter, 

4010 delimiter=delimiter, 

4011 ) 

4012 

4013 def wrap(self, parser: Parser[_t.Any]) -> Parser[_t.Any]: 

4014 result = super().wrap(parser) 

4015 result._inner._delimiter = self._pair_delimiter # pyright: ignore[reportAttributeAccessIssue] 

4016 return result 

4017 

4018 @staticmethod 

4019 def __config_type_iter(x) -> _t.Iterator[tuple[K, V]]: 

4020 if isinstance(x, dict): 

4021 return iter(x.items()) 

4022 else: 

4023 return iter(x) 

4024 

4025 def to_json_schema( 

4026 self, ctx: yuio.json_schema.JsonSchemaContext, / 

4027 ) -> yuio.json_schema.JsonSchemaType: 

4028 key_schema = self._inner._inner[0].to_json_schema(ctx) # type: ignore 

4029 value_schema = self._inner._inner[1].to_json_schema(ctx) # type: ignore 

4030 return yuio.json_schema.Dict(key_schema, value_schema) 

4031 

4032 def to_json_value(self, value: object, /) -> yuio.json_schema.JsonValue: 

4033 assert self.assert_type(value) 

4034 items = _t.cast( 

4035 list[tuple[yuio.json_schema.JsonValue, yuio.json_schema.JsonValue]], 

4036 [self._inner.to_json_value(item) for item in value.items()], 

4037 ) 

4038 

4039 if all(isinstance(k, str) for k, _ in items): 

4040 return dict(_t.cast(list[tuple[str, yuio.json_schema.JsonValue]], items)) 

4041 else: 

4042 return items 

4043 

4044 

4045class Tuple( 

4046 WrappingParser[TU, tuple[Parser[object], ...]], 

4047 ValueParser[TU], 

4048 PartialParser, 

4049 _t.Generic[TU], 

4050): 

4051 """Tuple(*parsers: Parser[...], delimiter: str | None = None) 

4052 

4053 Parser for tuples of fixed lengths. 

4054 

4055 :param parsers: 

4056 parsers for each tuple element. 

4057 :param delimiter: 

4058 delimiter that will be passed to :py:meth:`str.split`. 

4059 

4060 """ 

4061 

4062 # See the links below for an explanation of shy this is so ugly: 

4063 # https://github.com/python/typing/discussions/1450 

4064 # https://github.com/python/typing/issues/1216 

4065 if TYPE_CHECKING: 

4066 T1 = _t.TypeVar("T1") 

4067 T2 = _t.TypeVar("T2") 

4068 T3 = _t.TypeVar("T3") 

4069 T4 = _t.TypeVar("T4") 

4070 T5 = _t.TypeVar("T5") 

4071 T6 = _t.TypeVar("T6") 

4072 T7 = _t.TypeVar("T7") 

4073 T8 = _t.TypeVar("T8") 

4074 T9 = _t.TypeVar("T9") 

4075 T10 = _t.TypeVar("T10") 

4076 

4077 @_t.overload 

4078 def __new__( 

4079 cls, 

4080 /, 

4081 *, 

4082 delimiter: str | None = None, 

4083 ) -> PartialParser: ... 

4084 

4085 @_t.overload 

4086 def __new__( 

4087 cls, 

4088 p1: Parser[T1], 

4089 /, 

4090 *, 

4091 delimiter: str | None = None, 

4092 ) -> Tuple[tuple[T1]]: ... 

4093 

4094 @_t.overload 

4095 def __new__( 

4096 cls, 

4097 p1: Parser[T1], 

4098 p2: Parser[T2], 

4099 /, 

4100 *, 

4101 delimiter: str | None = None, 

4102 ) -> Tuple[tuple[T1, T2]]: ... 

4103 

4104 @_t.overload 

4105 def __new__( 

4106 cls, 

4107 p1: Parser[T1], 

4108 p2: Parser[T2], 

4109 p3: Parser[T3], 

4110 /, 

4111 *, 

4112 delimiter: str | None = None, 

4113 ) -> Tuple[tuple[T1, T2, T3]]: ... 

4114 

4115 @_t.overload 

4116 def __new__( 

4117 cls, 

4118 p1: Parser[T1], 

4119 p2: Parser[T2], 

4120 p3: Parser[T3], 

4121 p4: Parser[T4], 

4122 /, 

4123 *, 

4124 delimiter: str | None = None, 

4125 ) -> Tuple[tuple[T1, T2, T3, T4]]: ... 

4126 

4127 @_t.overload 

4128 def __new__( 

4129 cls, 

4130 p1: Parser[T1], 

4131 p2: Parser[T2], 

4132 p3: Parser[T3], 

4133 p4: Parser[T4], 

4134 p5: Parser[T5], 

4135 /, 

4136 *, 

4137 delimiter: str | None = None, 

4138 ) -> Tuple[tuple[T1, T2, T3, T4, T5]]: ... 

4139 

4140 @_t.overload 

4141 def __new__( 

4142 cls, 

4143 p1: Parser[T1], 

4144 p2: Parser[T2], 

4145 p3: Parser[T3], 

4146 p4: Parser[T4], 

4147 p5: Parser[T5], 

4148 p6: Parser[T6], 

4149 /, 

4150 *, 

4151 delimiter: str | None = None, 

4152 ) -> Tuple[tuple[T1, T2, T3, T4, T5, T6]]: ... 

4153 

4154 @_t.overload 

4155 def __new__( 

4156 cls, 

4157 p1: Parser[T1], 

4158 p2: Parser[T2], 

4159 p3: Parser[T3], 

4160 p4: Parser[T4], 

4161 p5: Parser[T5], 

4162 p6: Parser[T6], 

4163 p7: Parser[T7], 

4164 /, 

4165 *, 

4166 delimiter: str | None = None, 

4167 ) -> Tuple[tuple[T1, T2, T3, T4, T5, T6, T7]]: ... 

4168 

4169 @_t.overload 

4170 def __new__( 

4171 cls, 

4172 p1: Parser[T1], 

4173 p2: Parser[T2], 

4174 p3: Parser[T3], 

4175 p4: Parser[T4], 

4176 p5: Parser[T5], 

4177 p6: Parser[T6], 

4178 p7: Parser[T7], 

4179 p8: Parser[T8], 

4180 /, 

4181 *, 

4182 delimiter: str | None = None, 

4183 ) -> Tuple[tuple[T1, T2, T3, T4, T5, T6, T7, T8]]: ... 

4184 

4185 @_t.overload 

4186 def __new__( 

4187 cls, 

4188 p1: Parser[T1], 

4189 p2: Parser[T2], 

4190 p3: Parser[T3], 

4191 p4: Parser[T4], 

4192 p5: Parser[T5], 

4193 p6: Parser[T6], 

4194 p7: Parser[T7], 

4195 p8: Parser[T8], 

4196 p9: Parser[T9], 

4197 /, 

4198 *, 

4199 delimiter: str | None = None, 

4200 ) -> Tuple[tuple[T1, T2, T3, T4, T5, T6, T7, T8, T9]]: ... 

4201 

4202 @_t.overload 

4203 def __new__( 

4204 cls, 

4205 p1: Parser[T1], 

4206 p2: Parser[T2], 

4207 p3: Parser[T3], 

4208 p4: Parser[T4], 

4209 p5: Parser[T5], 

4210 p6: Parser[T6], 

4211 p7: Parser[T7], 

4212 p8: Parser[T8], 

4213 p9: Parser[T9], 

4214 p10: Parser[T10], 

4215 /, 

4216 *, 

4217 delimiter: str | None = None, 

4218 ) -> Tuple[tuple[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10]]: ... 

4219 

4220 @_t.overload 

4221 def __new__( 

4222 cls, 

4223 p1: Parser[T1], 

4224 p2: Parser[T2], 

4225 p3: Parser[T3], 

4226 p4: Parser[T4], 

4227 p5: Parser[T5], 

4228 p6: Parser[T6], 

4229 p7: Parser[T7], 

4230 p8: Parser[T8], 

4231 p9: Parser[T9], 

4232 p10: Parser[T10], 

4233 p11: Parser[object], 

4234 *tail: Parser[object], 

4235 delimiter: str | None = None, 

4236 ) -> Tuple[tuple[_t.Any, ...]]: ... 

4237 

4238 def __new__(cls, *args, **kwargs) -> _t.Any: ... 

4239 

4240 def __init__( 

4241 self, 

4242 *parsers: Parser[_t.Any], 

4243 delimiter: str | None = None, 

4244 ): 

4245 if delimiter == "": 

4246 raise ValueError("empty delimiter") 

4247 self._delimiter = delimiter 

4248 super().__init__(parsers or None, tuple) 

4249 

4250 def wrap(self, parser: Parser[_t.Any]) -> Parser[_t.Any]: 

4251 result = super().wrap(parser) 

4252 result._inner = parser._inner # type: ignore 

4253 return result 

4254 

4255 def parse_with_ctx(self, ctx: StrParsingContext, /) -> TU: 

4256 items = list(ctx.split(self._delimiter, maxsplit=len(self._inner) - 1)) 

4257 

4258 if len(items) != len(self._inner): 

4259 raise ParsingError( 

4260 "Expected %s element%s, got %s: `%r`", 

4261 len(self._inner), 

4262 "" if len(self._inner) == 1 else "s", 

4263 len(items), 

4264 ctx.value, 

4265 ctx=ctx, 

4266 ) 

4267 

4268 return _t.cast( 

4269 TU, 

4270 tuple( 

4271 parser.parse_with_ctx(item) for parser, item in zip(self._inner, items) 

4272 ), 

4273 ) 

4274 

4275 def parse_many_with_ctx(self, ctxs: _t.Sequence[StrParsingContext], /) -> TU: 

4276 if len(ctxs) != len(self._inner): 

4277 raise ParsingError( 

4278 "Expected %s element%s, got %s: `%r`", 

4279 len(self._inner), 

4280 "" if len(self._inner) == 1 else "s", 

4281 len(ctxs), 

4282 ctxs, 

4283 ) 

4284 

4285 return _t.cast( 

4286 TU, 

4287 tuple( 

4288 parser.parse_with_ctx(item) for parser, item in zip(self._inner, ctxs) 

4289 ), 

4290 ) 

4291 

4292 def parse_config_with_ctx(self, ctx: ConfigParsingContext, /) -> TU: 

4293 value = ctx.value 

4294 if not isinstance(value, (list, tuple)): 

4295 raise ParsingError.type_mismatch(value, list, tuple, ctx=ctx) 

4296 elif len(value) != len(self._inner): 

4297 raise ParsingError( 

4298 "Expected %s element%s, got %s: `%r`", 

4299 len(self._inner), 

4300 "" if len(self._inner) == 1 else "s", 

4301 len(value), 

4302 value, 

4303 ctx=ctx, 

4304 ) 

4305 

4306 return _t.cast( 

4307 TU, 

4308 tuple( 

4309 parser.parse_config_with_ctx(ctx.descend(item, i)) 

4310 for i, (parser, item) in enumerate(zip(self._inner, value)) 

4311 ), 

4312 ) 

4313 

4314 def supports_parse_many(self) -> bool: 

4315 return True 

4316 

4317 def get_nargs(self) -> _t.Literal["+", "*"] | int: 

4318 return len(self._inner) 

4319 

4320 def describe(self) -> str | None: 

4321 delimiter = self._delimiter or " " 

4322 desc = [parser.describe_or_def() for parser in self._inner] 

4323 return delimiter.join(desc) 

4324 

4325 def describe_many(self) -> str | tuple[str, ...]: 

4326 return tuple(parser.describe_or_def() for parser in self._inner) 

4327 

4328 def describe_value(self, value: object, /) -> str: 

4329 assert self.assert_type(value) 

4330 

4331 delimiter = self._delimiter or " " 

4332 desc = [parser.describe_value(item) for parser, item in zip(self._inner, value)] 

4333 

4334 return delimiter.join(desc) 

4335 

4336 def options(self) -> _t.Collection[yuio.widget.Option[TU]] | None: 

4337 return None 

4338 

4339 def completer(self) -> yuio.complete.Completer | None: 

4340 return yuio.complete.Tuple( 

4341 *[parser.completer() or yuio.complete.Empty() for parser in self._inner], 

4342 delimiter=self._delimiter, 

4343 ) 

4344 

4345 def widget( 

4346 self, 

4347 default: object | yuio.Missing, 

4348 input_description: str | None, 

4349 default_description: str | None, 

4350 /, 

4351 ) -> yuio.widget.Widget[TU | yuio.Missing]: 

4352 completer = self.completer() 

4353 

4354 return _WidgetResultMapper( 

4355 self, 

4356 input_description, 

4357 default, 

4358 ( 

4359 yuio.widget.InputWithCompletion( 

4360 completer, 

4361 placeholder=default_description or "", 

4362 ) 

4363 if completer is not None 

4364 else yuio.widget.Input( 

4365 placeholder=default_description or "", 

4366 ) 

4367 ), 

4368 ) 

4369 

4370 def to_json_schema( 

4371 self, ctx: yuio.json_schema.JsonSchemaContext, / 

4372 ) -> yuio.json_schema.JsonSchemaType: 

4373 return yuio.json_schema.Tuple( 

4374 [parser.to_json_schema(ctx) for parser in self._inner] 

4375 ) 

4376 

4377 def to_json_value(self, value: object, /) -> yuio.json_schema.JsonValue: 

4378 assert self.assert_type(value) 

4379 return [parser.to_json_value(item) for parser, item in zip(self._inner, value)] 

4380 

4381 def is_secret(self) -> bool: 

4382 return any(parser.is_secret() for parser in self._inner) 

4383 

4384 def __repr__(self): 

4385 if self._inner_raw is not None: 

4386 return f"{self.__class__.__name__}{self._inner_raw!r}" 

4387 else: 

4388 return self.__class__.__name__ 

4389 

4390 

4391class _DictElementParser(Tuple[tuple[K, V]], _t.Generic[K, V]): 

4392 def __init__(self, k: Parser[K], v: Parser[V], delimiter: str | None = None): 

4393 super().__init__(k, v, delimiter=delimiter) 

4394 

4395 def parse_config_with_ctx(self, ctx: ConfigParsingContext, /) -> tuple[K, V]: 

4396 if not isinstance(ctx.value, (list, tuple)): 

4397 raise ParsingError.type_mismatch(ctx.value, list, tuple, ctx=ctx) 

4398 elif len(ctx.value) != 2: 

4399 raise ParsingError( 

4400 "Expected 2 element, got %s: `%r`", 

4401 len(ctx.value), 

4402 ctx.value, 

4403 ctx=ctx, 

4404 ) 

4405 

4406 key = self._inner[0].parse_config_with_ctx( 

4407 ConfigParsingContext( 

4408 ctx.value[0], 

4409 parent=ctx.parent, 

4410 key=ctx.key, 

4411 desc="key of element #%(key)r", 

4412 ) 

4413 ) 

4414 value = self._inner[1].parse_config_with_ctx( 

4415 ConfigParsingContext( 

4416 ctx.value[1], 

4417 parent=ctx.parent, 

4418 key=key, 

4419 ) 

4420 ) 

4421 

4422 return _t.cast(tuple[K, V], (key, value)) 

4423 

4424 

4425class Optional(MappingParser[T | None, T], _t.Generic[T]): 

4426 """Optional(inner: Parser[T], /) 

4427 

4428 Parser for optional values. 

4429 

4430 Allows handling :data:`None`\\ s when parsing config. Does not change how strings 

4431 are parsed, though. 

4432 

4433 :param inner: 

4434 a parser used to extract and validate contents of an optional. 

4435 

4436 """ 

4437 

4438 if TYPE_CHECKING: 

4439 

4440 @_t.overload 

4441 def __new__(cls, inner: Parser[T], /) -> Optional[T]: ... 

4442 

4443 @_t.overload 

4444 def __new__(cls, /) -> PartialParser: ... 

4445 

4446 def __new__(cls, *args, **kwargs) -> _t.Any: ... 

4447 

4448 def __init__(self, inner: Parser[T] | None = None, /): 

4449 super().__init__(inner) 

4450 

4451 def parse_with_ctx(self, ctx: StrParsingContext, /) -> T | None: 

4452 return self._inner.parse_with_ctx(ctx) 

4453 

4454 def parse_many_with_ctx(self, ctxs: _t.Sequence[StrParsingContext], /) -> T | None: 

4455 return self._inner.parse_many_with_ctx(ctxs) 

4456 

4457 def parse_config_with_ctx(self, ctx: ConfigParsingContext, /) -> T | None: 

4458 if ctx.value is None: 

4459 return None 

4460 return self._inner.parse_config_with_ctx(ctx) 

4461 

4462 def check_type(self, value: object, /) -> _t.TypeGuard[T | None]: 

4463 return True 

4464 

4465 def describe_value(self, value: object, /) -> str: 

4466 if value is None: 

4467 return "<none>" 

4468 return self._inner.describe_value(value) 

4469 

4470 def options(self) -> _t.Collection[yuio.widget.Option[T | None]] | None: 

4471 return self._inner.options() 

4472 

4473 def widget( 

4474 self, 

4475 default: object | yuio.Missing, 

4476 input_description: str | None, 

4477 default_description: str | None, 

4478 /, 

4479 ) -> yuio.widget.Widget[T | yuio.Missing]: 

4480 return self._inner.widget(default, input_description, default_description) 

4481 

4482 def to_json_schema( 

4483 self, ctx: yuio.json_schema.JsonSchemaContext, / 

4484 ) -> yuio.json_schema.JsonSchemaType: 

4485 return yuio.json_schema.OneOf( 

4486 [self._inner.to_json_schema(ctx), yuio.json_schema.Null()] 

4487 ) 

4488 

4489 def to_json_value(self, value: object, /) -> yuio.json_schema.JsonValue: 

4490 if value is None: 

4491 return None 

4492 else: 

4493 return self._inner.to_json_value(value) 

4494 

4495 

4496class Union(WrappingParser[T, tuple[Parser[T], ...]], ValueParser[T], _t.Generic[T]): 

4497 """Union(*parsers: Parser[T]) 

4498 

4499 Tries several parsers and returns the first successful result. 

4500 

4501 .. warning:: 

4502 

4503 Order of parsers matters. Since parsers are tried in the same order as they're 

4504 given, make sure to put parsers that are likely to succeed at the end. 

4505 

4506 For example, this parser will always return a string because :class:`Str` 

4507 can't fail:: 

4508 

4509 >>> parser = Union(Str(), Int()) # Always returns a string! 

4510 >>> parser.parse("10") 

4511 '10' 

4512 

4513 To fix this, put :class:`Str` at the end so that :class:`Int` is tried first:: 

4514 

4515 >>> parser = Union(Int(), Str()) 

4516 >>> parser.parse("10") 

4517 10 

4518 >>> parser.parse("not an int") 

4519 'not an int' 

4520 

4521 """ 

4522 

4523 # See the links below for an explanation of shy this is so ugly: 

4524 # https://github.com/python/typing/discussions/1450 

4525 # https://github.com/python/typing/issues/1216 

4526 if TYPE_CHECKING: 

4527 T1 = _t.TypeVar("T1") 

4528 T2 = _t.TypeVar("T2") 

4529 T3 = _t.TypeVar("T3") 

4530 T4 = _t.TypeVar("T4") 

4531 T5 = _t.TypeVar("T5") 

4532 T6 = _t.TypeVar("T6") 

4533 T7 = _t.TypeVar("T7") 

4534 T8 = _t.TypeVar("T8") 

4535 T9 = _t.TypeVar("T9") 

4536 T10 = _t.TypeVar("T10") 

4537 

4538 @_t.overload 

4539 def __new__( 

4540 cls, 

4541 /, 

4542 ) -> PartialParser: ... 

4543 

4544 @_t.overload 

4545 def __new__( 

4546 cls, 

4547 p1: Parser[T1], 

4548 /, 

4549 ) -> Union[T1]: ... 

4550 

4551 @_t.overload 

4552 def __new__( 

4553 cls, 

4554 p1: Parser[T1], 

4555 p2: Parser[T2], 

4556 /, 

4557 ) -> Union[T1 | T2]: ... 

4558 

4559 @_t.overload 

4560 def __new__( 

4561 cls, 

4562 p1: Parser[T1], 

4563 p2: Parser[T2], 

4564 p3: Parser[T3], 

4565 /, 

4566 ) -> Union[T1 | T2 | T3]: ... 

4567 

4568 @_t.overload 

4569 def __new__( 

4570 cls, 

4571 p1: Parser[T1], 

4572 p2: Parser[T2], 

4573 p3: Parser[T3], 

4574 p4: Parser[T4], 

4575 /, 

4576 ) -> Union[T1 | T2 | T3 | T4]: ... 

4577 

4578 @_t.overload 

4579 def __new__( 

4580 cls, 

4581 p1: Parser[T1], 

4582 p2: Parser[T2], 

4583 p3: Parser[T3], 

4584 p4: Parser[T4], 

4585 p5: Parser[T5], 

4586 /, 

4587 ) -> Union[T1 | T2 | T3 | T4 | T5]: ... 

4588 

4589 @_t.overload 

4590 def __new__( 

4591 cls, 

4592 p1: Parser[T1], 

4593 p2: Parser[T2], 

4594 p3: Parser[T3], 

4595 p4: Parser[T4], 

4596 p5: Parser[T5], 

4597 p6: Parser[T6], 

4598 /, 

4599 ) -> Union[T1 | T2 | T3 | T4 | T5 | T6]: ... 

4600 

4601 @_t.overload 

4602 def __new__( 

4603 cls, 

4604 p1: Parser[T1], 

4605 p2: Parser[T2], 

4606 p3: Parser[T3], 

4607 p4: Parser[T4], 

4608 p5: Parser[T5], 

4609 p6: Parser[T6], 

4610 p7: Parser[T7], 

4611 /, 

4612 ) -> Union[T1 | T2 | T3 | T4 | T5 | T6 | T7]: ... 

4613 

4614 @_t.overload 

4615 def __new__( 

4616 cls, 

4617 p1: Parser[T1], 

4618 p2: Parser[T2], 

4619 p3: Parser[T3], 

4620 p4: Parser[T4], 

4621 p5: Parser[T5], 

4622 p6: Parser[T6], 

4623 p7: Parser[T7], 

4624 p8: Parser[T8], 

4625 /, 

4626 ) -> Union[T1 | T2 | T3 | T4 | T5 | T6 | T7 | T8]: ... 

4627 

4628 @_t.overload 

4629 def __new__( 

4630 cls, 

4631 p1: Parser[T1], 

4632 p2: Parser[T2], 

4633 p3: Parser[T3], 

4634 p4: Parser[T4], 

4635 p5: Parser[T5], 

4636 p6: Parser[T6], 

4637 p7: Parser[T7], 

4638 p8: Parser[T8], 

4639 p9: Parser[T9], 

4640 /, 

4641 ) -> Union[T1 | T2 | T3 | T4 | T5 | T6 | T7 | T8 | T9]: ... 

4642 

4643 @_t.overload 

4644 def __new__( 

4645 cls, 

4646 p1: Parser[T1], 

4647 p2: Parser[T2], 

4648 p3: Parser[T3], 

4649 p4: Parser[T4], 

4650 p5: Parser[T5], 

4651 p6: Parser[T6], 

4652 p7: Parser[T7], 

4653 p8: Parser[T8], 

4654 p9: Parser[T9], 

4655 p10: Parser[T10], 

4656 /, 

4657 ) -> Union[T1 | T2 | T3 | T4 | T5 | T6 | T7 | T8 | T9 | T10]: ... 

4658 

4659 @_t.overload 

4660 def __new__( 

4661 cls, 

4662 p1: Parser[T1], 

4663 p2: Parser[T2], 

4664 p3: Parser[T3], 

4665 p4: Parser[T4], 

4666 p5: Parser[T5], 

4667 p6: Parser[T6], 

4668 p7: Parser[T7], 

4669 p8: Parser[T8], 

4670 p9: Parser[T9], 

4671 p10: Parser[T10], 

4672 p11: Parser[object], 

4673 *parsers: Parser[object], 

4674 ) -> Union[object]: ... 

4675 

4676 def __new__(cls, *args, **kwargs) -> _t.Any: ... 

4677 

4678 def __init__(self, *parsers: Parser[_t.Any]): 

4679 super().__init__(parsers or None, object) 

4680 

4681 def wrap(self, parser: Parser[_t.Any]) -> Parser[_t.Any]: 

4682 result = super().wrap(parser) 

4683 result._inner = parser._inner # type: ignore 

4684 return result 

4685 

4686 def parse_with_ctx(self, ctx: StrParsingContext, /) -> T: 

4687 errors: list[tuple[Parser[object], ParsingError]] = [] 

4688 for parser in self._inner: 

4689 try: 

4690 return parser.parse_with_ctx(ctx) 

4691 except ParsingError as e: 

4692 errors.append((parser, e)) 

4693 raise self._make_error(errors, ctx) 

4694 

4695 def parse_config_with_ctx(self, ctx: ConfigParsingContext, /) -> T: 

4696 errors: list[tuple[Parser[object], ParsingError]] = [] 

4697 for parser in self._inner: 

4698 try: 

4699 return parser.parse_config_with_ctx(ctx) 

4700 except ParsingError as e: 

4701 errors.append((parser, e)) 

4702 raise self._make_error(errors, ctx) 

4703 

4704 def _make_error( 

4705 self, 

4706 errors: list[tuple[Parser[object], ParsingError]], 

4707 ctx: StrParsingContext | ConfigParsingContext, 

4708 ): 

4709 msgs = [] 

4710 for parser, error in errors: 

4711 error.raw = None 

4712 error.pos = None 

4713 msgs.append( 

4714 yuio.string.Format( 

4715 " Trying as `%s`:\n%s", 

4716 parser.describe_or_def(), 

4717 yuio.string.Indent(error, indent=4), 

4718 ) 

4719 ) 

4720 return ParsingError( 

4721 "Can't parse `%r`:\n%s", ctx.value, yuio.string.Stack(*msgs), ctx=ctx 

4722 ) 

4723 

4724 def check_type(self, value: object, /) -> _t.TypeGuard[T]: 

4725 return True 

4726 

4727 def describe(self) -> str | None: 

4728 if len(self._inner) > 1: 

4729 

4730 def strip_curly_brackets(desc: str): 

4731 if desc.startswith("{") and desc.endswith("}") and "|" in desc: 

4732 s = desc[1:-1] 

4733 if "{" not in s and "}" not in s: 

4734 return s 

4735 return desc 

4736 

4737 desc = "|".join( 

4738 strip_curly_brackets(parser.describe_or_def()) for parser in self._inner 

4739 ) 

4740 desc = f"{{{desc}}}" 

4741 else: 

4742 desc = "|".join(parser.describe_or_def() for parser in self._inner) 

4743 return desc 

4744 

4745 def describe_value(self, value: object, /) -> str: 

4746 for parser in self._inner: 

4747 try: 

4748 return parser.describe_value(value) 

4749 except TypeError: 

4750 pass 

4751 

4752 raise TypeError( 

4753 f"parser {self} can't handle value of type {_tx.type_repr(type(value))}" 

4754 ) 

4755 

4756 def options(self) -> _t.Collection[yuio.widget.Option[T]] | None: 

4757 result = [] 

4758 got_options = False 

4759 for parser in self._inner: 

4760 if options := parser.options(): 

4761 result.extend(options) 

4762 got_options = True 

4763 if got_options: 

4764 return result 

4765 else: 

4766 return None 

4767 

4768 def completer(self) -> yuio.complete.Completer | None: 

4769 completers = [] 

4770 for parser in self._inner: 

4771 if completer := parser.completer(): 

4772 completers.append((parser.describe(), completer)) 

4773 if not completers: 

4774 return None 

4775 elif len(completers) == 1: 

4776 return completers[0][1] 

4777 else: 

4778 return yuio.complete.Alternative(completers) 

4779 

4780 def widget( 

4781 self, 

4782 default: object | yuio.Missing, 

4783 input_description: str | None, 

4784 default_description: str | None, 

4785 ) -> yuio.widget.Widget[T | yuio.Missing]: 

4786 options = [] 

4787 for parser in self._inner: 

4788 parser_options = parser.options() 

4789 if parser_options is None: 

4790 options = None 

4791 break 

4792 options.extend(parser_options) 

4793 

4794 if not options: 

4795 return super().widget(default, input_description, default_description) 

4796 

4797 if default is yuio.MISSING: 

4798 default_index = 0 

4799 else: 

4800 for i, option in enumerate(options): 

4801 if option.value == default: 

4802 default_index = i 

4803 break 

4804 else: 

4805 options.insert( 

4806 0, 

4807 yuio.widget.Option( 

4808 yuio.MISSING, default_description or str(default) 

4809 ), 

4810 ) 

4811 default_index = 0 

4812 

4813 return yuio.widget.Choice(options, default_index=default_index) 

4814 

4815 def to_json_schema( 

4816 self, ctx: yuio.json_schema.JsonSchemaContext, / 

4817 ) -> yuio.json_schema.JsonSchemaType: 

4818 return yuio.json_schema.OneOf( 

4819 [parser.to_json_schema(ctx) for parser in self._inner] 

4820 ) 

4821 

4822 def to_json_value(self, value: object, /) -> yuio.json_schema.JsonValue: 

4823 for parser in self._inner: 

4824 try: 

4825 return parser.to_json_value(value) 

4826 except TypeError: 

4827 pass 

4828 

4829 raise TypeError( 

4830 f"parser {self} can't handle value of type {_tx.type_repr(type(value))}" 

4831 ) 

4832 

4833 def is_secret(self) -> bool: 

4834 return any(parser.is_secret() for parser in self._inner) 

4835 

4836 def __repr__(self): 

4837 return f"{self.__class__.__name__}{self._inner_raw!r}" 

4838 

4839 

4840class _BoundImpl(ValidatingParser[T], _t.Generic[T, Cmp]): 

4841 def __init__( 

4842 self, 

4843 inner: Parser[T] | None, 

4844 /, 

4845 *, 

4846 lower: Cmp | None = None, 

4847 lower_inclusive: Cmp | None = None, 

4848 upper: Cmp | None = None, 

4849 upper_inclusive: Cmp | None = None, 

4850 mapper: _t.Callable[[T], Cmp], 

4851 desc: str, 

4852 ): 

4853 super().__init__(inner) 

4854 

4855 self._lower_bound: Cmp | None = None 

4856 self._lower_bound_is_inclusive: bool = True 

4857 self._upper_bound: Cmp | None = None 

4858 self._upper_bound_is_inclusive: bool = True 

4859 

4860 if lower is not None and lower_inclusive is not None: 

4861 raise TypeError( 

4862 "lower and lower_inclusive cannot be given at the same time" 

4863 ) 

4864 elif lower is not None: 

4865 self._lower_bound = lower 

4866 self._lower_bound_is_inclusive = False 

4867 elif lower_inclusive is not None: 

4868 self._lower_bound = lower_inclusive 

4869 self._lower_bound_is_inclusive = True 

4870 

4871 if upper is not None and upper_inclusive is not None: 

4872 raise TypeError( 

4873 "upper and upper_inclusive cannot be given at the same time" 

4874 ) 

4875 elif upper is not None: 

4876 self._upper_bound = upper 

4877 self._upper_bound_is_inclusive = False 

4878 elif upper_inclusive is not None: 

4879 self._upper_bound = upper_inclusive 

4880 self._upper_bound_is_inclusive = True 

4881 

4882 self.__mapper = mapper 

4883 self.__desc = desc 

4884 

4885 def _validate(self, value: T, /): 

4886 mapped = self.__mapper(value) 

4887 

4888 if self._lower_bound is not None: 

4889 if self._lower_bound_is_inclusive and mapped < self._lower_bound: 

4890 raise ParsingError( 

4891 "%s should be greater than or equal to `%s`: `%r`", 

4892 self.__desc, 

4893 self._lower_bound, 

4894 value, 

4895 ) 

4896 elif not self._lower_bound_is_inclusive and not self._lower_bound < mapped: 

4897 raise ParsingError( 

4898 "%s should be greater than `%s`: `%r`", 

4899 self.__desc, 

4900 self._lower_bound, 

4901 value, 

4902 ) 

4903 

4904 if self._upper_bound is not None: 

4905 if self._upper_bound_is_inclusive and self._upper_bound < mapped: 

4906 raise ParsingError( 

4907 "%s should be lesser than or equal to `%s`: `%r`", 

4908 self.__desc, 

4909 self._upper_bound, 

4910 value, 

4911 ) 

4912 elif not self._upper_bound_is_inclusive and not mapped < self._upper_bound: 

4913 raise ParsingError( 

4914 "%s should be lesser than `%s`: `%r`", 

4915 self.__desc, 

4916 self._upper_bound, 

4917 value, 

4918 ) 

4919 

4920 def __repr__(self): 

4921 desc = "" 

4922 if self._lower_bound is not None: 

4923 desc += repr(self._lower_bound) 

4924 desc += " <= " if self._lower_bound_is_inclusive else " < " 

4925 mapper_name = getattr(self.__mapper, "__name__") 

4926 if mapper_name and mapper_name != "<lambda>": 

4927 desc += mapper_name 

4928 else: 

4929 desc += "x" 

4930 if self._upper_bound is not None: 

4931 desc += " <= " if self._upper_bound_is_inclusive else " < " 

4932 desc += repr(self._upper_bound) 

4933 return f"{self.__class__.__name__}({self.__wrapped_parser__!r}, {desc})" 

4934 

4935 

4936class Bound(_BoundImpl[Cmp, Cmp], _t.Generic[Cmp]): 

4937 """Bound(inner: Parser[Cmp], /, *, lower: Cmp | None = None, lower_inclusive: Cmp | None = None, upper: Cmp | None = None, upper_inclusive: Cmp | None = None) 

4938 

4939 Check that value is upper- or lower-bound by some constraints. 

4940 

4941 :param inner: 

4942 parser whose result will be validated. 

4943 :param lower: 

4944 set lower bound for value, so we require that ``value > lower``. 

4945 Can't be given if `lower_inclusive` is also given. 

4946 :param lower_inclusive: 

4947 set lower bound for value, so we require that ``value >= lower``. 

4948 Can't be given if `lower` is also given. 

4949 :param upper: 

4950 set upper bound for value, so we require that ``value < upper``. 

4951 Can't be given if `upper_inclusive` is also given. 

4952 :param upper_inclusive: 

4953 set upper bound for value, so we require that ``value <= upper``. 

4954 Can't be given if `upper` is also given. 

4955 :example: 

4956 :: 

4957 

4958 >>> # Int in range `0 < x <= 1`: 

4959 >>> Bound(Int(), lower=0, upper_inclusive=1) 

4960 Bound(Int, 0 < x <= 1) 

4961 

4962 """ 

4963 

4964 if TYPE_CHECKING: 

4965 

4966 @_t.overload 

4967 def __new__( 

4968 cls, 

4969 inner: Parser[Cmp], 

4970 /, 

4971 *, 

4972 lower: Cmp | None = None, 

4973 lower_inclusive: Cmp | None = None, 

4974 upper: Cmp | None = None, 

4975 upper_inclusive: Cmp | None = None, 

4976 ) -> Bound[Cmp]: ... 

4977 

4978 @_t.overload 

4979 def __new__( 

4980 cls, 

4981 *, 

4982 lower: Cmp | None = None, 

4983 lower_inclusive: Cmp | None = None, 

4984 upper: Cmp | None = None, 

4985 upper_inclusive: Cmp | None = None, 

4986 ) -> PartialParser: ... 

4987 

4988 def __new__(cls, *args, **kwargs) -> _t.Any: ... 

4989 

4990 def __init__( 

4991 self, 

4992 inner: Parser[Cmp] | None = None, 

4993 /, 

4994 *, 

4995 lower: Cmp | None = None, 

4996 lower_inclusive: Cmp | None = None, 

4997 upper: Cmp | None = None, 

4998 upper_inclusive: Cmp | None = None, 

4999 ): 

5000 super().__init__( 

5001 inner, 

5002 lower=lower, 

5003 lower_inclusive=lower_inclusive, 

5004 upper=upper, 

5005 upper_inclusive=upper_inclusive, 

5006 mapper=lambda x: x, 

5007 desc="Value", 

5008 ) 

5009 

5010 def to_json_schema( 

5011 self, ctx: yuio.json_schema.JsonSchemaContext, / 

5012 ) -> yuio.json_schema.JsonSchemaType: 

5013 bound = {} 

5014 if isinstance(self._lower_bound, (int, float)): 

5015 bound[ 

5016 "minimum" if self._lower_bound_is_inclusive else "exclusiveMinimum" 

5017 ] = self._lower_bound 

5018 if isinstance(self._upper_bound, (int, float)): 

5019 bound[ 

5020 "maximum" if self._upper_bound_is_inclusive else "exclusiveMaximum" 

5021 ] = self._upper_bound 

5022 if bound: 

5023 return yuio.json_schema.AllOf( 

5024 [super().to_json_schema(ctx), yuio.json_schema.Opaque(bound)] 

5025 ) 

5026 else: 

5027 return super().to_json_schema(ctx) 

5028 

5029 

5030@_t.overload 

5031def Gt(inner: Parser[Cmp], bound: Cmp, /) -> Bound[Cmp]: ... 

5032@_t.overload 

5033def Gt(bound: _tx.SupportsLt[_t.Any], /) -> PartialParser: ... 

5034def Gt(*args) -> _t.Any: 

5035 """Gt(inner: Parser[Cmp], bound: Cmp, /) 

5036 

5037 Alias for :class:`Bound`. 

5038 

5039 :param inner: 

5040 parser whose result will be validated. 

5041 :param bound: 

5042 lower bound for parsed values. 

5043 

5044 """ 

5045 

5046 if len(args) == 1: 

5047 return Bound(lower=args[0]) 

5048 elif len(args) == 2: 

5049 return Bound(args[0], lower=args[1]) 

5050 else: 

5051 raise TypeError(f"expected 1 or 2 positional arguments, got {len(args)}") 

5052 

5053 

5054@_t.overload 

5055def Ge(inner: Parser[Cmp], bound: Cmp, /) -> Bound[Cmp]: ... 

5056@_t.overload 

5057def Ge(bound: _tx.SupportsLt[_t.Any], /) -> PartialParser: ... 

5058def Ge(*args) -> _t.Any: 

5059 """Ge(inner: Parser[Cmp], bound: Cmp, /) 

5060 

5061 Alias for :class:`Bound`. 

5062 

5063 :param inner: 

5064 parser whose result will be validated. 

5065 :param bound: 

5066 lower inclusive bound for parsed values. 

5067 

5068 """ 

5069 

5070 if len(args) == 1: 

5071 return Bound(lower_inclusive=args[0]) 

5072 elif len(args) == 2: 

5073 return Bound(args[0], lower_inclusive=args[1]) 

5074 else: 

5075 raise TypeError(f"expected 1 or 2 positional arguments, got {len(args)}") 

5076 

5077 

5078@_t.overload 

5079def Lt(inner: Parser[Cmp], bound: Cmp, /) -> Bound[Cmp]: ... 

5080@_t.overload 

5081def Lt(bound: _tx.SupportsLt[_t.Any], /) -> PartialParser: ... 

5082def Lt(*args) -> _t.Any: 

5083 """Lt(inner: Parser[Cmp], bound: Cmp, /) 

5084 

5085 Alias for :class:`Bound`. 

5086 

5087 :param inner: 

5088 parser whose result will be validated. 

5089 :param bound: 

5090 upper bound for parsed values. 

5091 

5092 """ 

5093 

5094 if len(args) == 1: 

5095 return Bound(upper=args[0]) 

5096 elif len(args) == 2: 

5097 return Bound(args[0], upper=args[1]) 

5098 else: 

5099 raise TypeError(f"expected 1 or 2 positional arguments, got {len(args)}") 

5100 

5101 

5102@_t.overload 

5103def Le(inner: Parser[Cmp], bound: Cmp, /) -> Bound[Cmp]: ... 

5104@_t.overload 

5105def Le(bound: _tx.SupportsLt[_t.Any], /) -> PartialParser: ... 

5106def Le(*args) -> _t.Any: 

5107 """Le(inner: Parser[Cmp], bound: Cmp, /) 

5108 

5109 Alias for :class:`Bound`. 

5110 

5111 :param inner: 

5112 parser whose result will be validated. 

5113 :param bound: 

5114 upper inclusive bound for parsed values. 

5115 

5116 """ 

5117 

5118 if len(args) == 1: 

5119 return Bound(upper_inclusive=args[0]) 

5120 elif len(args) == 2: 

5121 return Bound(args[0], upper_inclusive=args[1]) 

5122 else: 

5123 raise TypeError(f"expected 1 or 2 positional arguments, got {len(args)}") 

5124 

5125 

5126class LenBound(_BoundImpl[Sz, int], _t.Generic[Sz]): 

5127 """LenBound(inner: Parser[Sz], /, *, lower: int | None = None, lower_inclusive: int | None = None, upper: int | None = None, upper_inclusive: int | None = None) 

5128 

5129 Check that length of a value is upper- or lower-bound by some constraints. 

5130 

5131 The signature is the same as of the :class:`Bound` class. 

5132 

5133 :param inner: 

5134 parser whose result will be validated. 

5135 :param lower: 

5136 set lower bound for value's length, so we require that ``len(value) > lower``. 

5137 Can't be given if `lower_inclusive` is also given. 

5138 :param lower_inclusive: 

5139 set lower bound for value's length, so we require that ``len(value) >= lower``. 

5140 Can't be given if `lower` is also given. 

5141 :param upper: 

5142 set upper bound for value's length, so we require that ``len(value) < upper``. 

5143 Can't be given if `upper_inclusive` is also given. 

5144 :param upper_inclusive: 

5145 set upper bound for value's length, so we require that ``len(value) <= upper``. 

5146 Can't be given if `upper` is also given. 

5147 :example: 

5148 :: 

5149 

5150 >>> # List of up to five ints: 

5151 >>> LenBound(List(Int()), upper_inclusive=5) 

5152 LenBound(List(Int), len <= 5) 

5153 

5154 """ 

5155 

5156 if TYPE_CHECKING: 

5157 

5158 @_t.overload 

5159 def __new__( 

5160 cls, 

5161 inner: Parser[Sz], 

5162 /, 

5163 *, 

5164 lower: int | None = None, 

5165 lower_inclusive: int | None = None, 

5166 upper: int | None = None, 

5167 upper_inclusive: int | None = None, 

5168 ) -> LenBound[Sz]: ... 

5169 

5170 @_t.overload 

5171 def __new__( 

5172 cls, 

5173 /, 

5174 *, 

5175 lower: int | None = None, 

5176 lower_inclusive: int | None = None, 

5177 upper: int | None = None, 

5178 upper_inclusive: int | None = None, 

5179 ) -> PartialParser: ... 

5180 

5181 def __new__(cls, *args, **kwargs) -> _t.Any: ... 

5182 

5183 def __init__( 

5184 self, 

5185 inner: Parser[Sz] | None = None, 

5186 /, 

5187 *, 

5188 lower: int | None = None, 

5189 lower_inclusive: int | None = None, 

5190 upper: int | None = None, 

5191 upper_inclusive: int | None = None, 

5192 ): 

5193 super().__init__( 

5194 inner, 

5195 lower=lower, 

5196 lower_inclusive=lower_inclusive, 

5197 upper=upper, 

5198 upper_inclusive=upper_inclusive, 

5199 mapper=len, 

5200 desc="Length of value", 

5201 ) 

5202 

5203 def get_nargs(self) -> _t.Literal["+", "*"] | int: 

5204 if not self._inner.supports_parse_many(): 

5205 # somebody bound len of a string? 

5206 return self._inner.get_nargs() 

5207 

5208 lower = self._lower_bound 

5209 if lower is not None and not self._lower_bound_is_inclusive: 

5210 lower += 1 

5211 upper = self._upper_bound 

5212 if upper is not None and not self._upper_bound_is_inclusive: 

5213 upper -= 1 

5214 

5215 if lower == upper and lower is not None: 

5216 return lower 

5217 elif lower is not None and lower > 0: 

5218 return "+" 

5219 else: 

5220 return "*" 

5221 

5222 def to_json_schema( 

5223 self, ctx: yuio.json_schema.JsonSchemaContext, / 

5224 ) -> yuio.json_schema.JsonSchemaType: 

5225 bound = {} 

5226 min_bound = self._lower_bound 

5227 if not self._lower_bound_is_inclusive and min_bound is not None: 

5228 min_bound += 1 

5229 if min_bound is not None: 

5230 bound["minLength"] = bound["minItems"] = bound["minProperties"] = min_bound 

5231 max_bound = self._upper_bound 

5232 if not self._upper_bound_is_inclusive and max_bound is not None: 

5233 max_bound -= 1 

5234 if max_bound is not None: 

5235 bound["maxLength"] = bound["maxItems"] = bound["maxProperties"] = max_bound 

5236 if bound: 

5237 return yuio.json_schema.AllOf( 

5238 [super().to_json_schema(ctx), yuio.json_schema.Opaque(bound)] 

5239 ) 

5240 else: 

5241 return super().to_json_schema(ctx) 

5242 

5243 

5244@_t.overload 

5245def LenGt(inner: Parser[Sz], bound: int, /) -> LenBound[Sz]: ... 

5246@_t.overload 

5247def LenGt(bound: int, /) -> PartialParser: ... 

5248def LenGt(*args) -> _t.Any: 

5249 """LenGt(inner: Parser[Sz], bound: int, /) 

5250 

5251 Alias for :class:`LenBound`. 

5252 

5253 :param inner: 

5254 parser whose result will be validated. 

5255 :param bound: 

5256 lower bound for parsed values's length. 

5257 

5258 """ 

5259 

5260 if len(args) == 1: 

5261 return LenBound(lower=args[0]) 

5262 elif len(args) == 2: 

5263 return LenBound(args[0], lower=args[1]) 

5264 else: 

5265 raise TypeError(f"expected 1 or 2 positional arguments, got {len(args)}") 

5266 

5267 

5268@_t.overload 

5269def LenGe(inner: Parser[Sz], bound: int, /) -> LenBound[Sz]: ... 

5270@_t.overload 

5271def LenGe(bound: int, /) -> PartialParser: ... 

5272def LenGe(*args) -> _t.Any: 

5273 """LenGe(inner: Parser[Sz], bound: int, /) 

5274 

5275 Alias for :class:`LenBound`. 

5276 

5277 :param inner: 

5278 parser whose result will be validated. 

5279 :param bound: 

5280 lower inclusive bound for parsed values's length. 

5281 

5282 """ 

5283 

5284 if len(args) == 1: 

5285 return LenBound(lower_inclusive=args[0]) 

5286 elif len(args) == 2: 

5287 return LenBound(args[0], lower_inclusive=args[1]) 

5288 else: 

5289 raise TypeError(f"expected 1 or 2 positional arguments, got {len(args)}") 

5290 

5291 

5292@_t.overload 

5293def LenLt(inner: Parser[Sz], bound: int, /) -> LenBound[Sz]: ... 

5294@_t.overload 

5295def LenLt(bound: int, /) -> PartialParser: ... 

5296def LenLt(*args) -> _t.Any: 

5297 """LenLt(inner: Parser[Sz], bound: int, /) 

5298 

5299 Alias for :class:`LenBound`. 

5300 

5301 :param inner: 

5302 parser whose result will be validated. 

5303 :param bound: 

5304 upper bound for parsed values's length. 

5305 

5306 """ 

5307 

5308 if len(args) == 1: 

5309 return LenBound(upper=args[0]) 

5310 elif len(args) == 2: 

5311 return LenBound(args[0], upper=args[1]) 

5312 else: 

5313 raise TypeError(f"expected 1 or 2 positional arguments, got {len(args)}") 

5314 

5315 

5316@_t.overload 

5317def LenLe(inner: Parser[Sz], bound: int, /) -> LenBound[Sz]: ... 

5318@_t.overload 

5319def LenLe(bound: int, /) -> PartialParser: ... 

5320def LenLe(*args) -> _t.Any: 

5321 """LenLe(inner: Parser[Sz], bound: int, /) 

5322 

5323 Alias for :class:`LenBound`. 

5324 

5325 :param inner: 

5326 parser whose result will be validated. 

5327 :param bound: 

5328 upper inclusive bound for parsed values's length. 

5329 

5330 """ 

5331 

5332 if len(args) == 1: 

5333 return LenBound(upper_inclusive=args[0]) 

5334 elif len(args) == 2: 

5335 return LenBound(args[0], upper_inclusive=args[1]) 

5336 else: 

5337 raise TypeError(f"expected 1 or 2 positional arguments, got {len(args)}") 

5338 

5339 

5340class OneOf(ValidatingParser[T], _t.Generic[T]): 

5341 """OneOf(inner: Parser[T], values: typing.Collection[T], /) 

5342 

5343 Check that the parsed value is one of the given set of values. 

5344 

5345 .. note:: 

5346 

5347 This parser is meant to validate results of other parsers; if you're looking 

5348 to parse enums or literal values, check out :class:`Enum` or :class:`Literal`. 

5349 

5350 :param inner: 

5351 parser whose result will be validated. 

5352 :param values: 

5353 collection of allowed values. 

5354 :example: 

5355 :: 

5356 

5357 >>> # Accepts only strings 'A', 'B', or 'C': 

5358 >>> OneOf(Str(), ['A', 'B', 'C']) 

5359 OneOf(Str) 

5360 

5361 """ 

5362 

5363 if TYPE_CHECKING: 

5364 

5365 @_t.overload 

5366 def __new__(cls, inner: Parser[T], values: _t.Collection[T], /) -> OneOf[T]: ... 

5367 

5368 @_t.overload 

5369 def __new__(cls, values: _t.Collection[T], /) -> PartialParser: ... 

5370 

5371 def __new__(cls, *args) -> _t.Any: ... 

5372 

5373 def __init__(self, *args): 

5374 inner: Parser[T] | None 

5375 values: _t.Collection[T] 

5376 if len(args) == 1: 

5377 inner, values = None, args[0] 

5378 elif len(args) == 2: 

5379 inner, values = args 

5380 else: 

5381 raise TypeError(f"expected 1 or 2 positional arguments, got {len(args)}") 

5382 

5383 super().__init__(inner) 

5384 

5385 self._allowed_values = values 

5386 

5387 def _validate(self, value: T, /): 

5388 if value not in self._allowed_values: 

5389 raise ParsingError( 

5390 "Can't parse `%r`, should be %s", 

5391 value, 

5392 yuio.string.JoinRepr.or_(self._allowed_values), 

5393 ) 

5394 

5395 def describe(self) -> str | None: 

5396 desc = "|".join(self.describe_value(e) for e in self._allowed_values) 

5397 if len(desc) < 80: 

5398 if len(self._allowed_values) > 1: 

5399 desc = f"{{{desc}}}" 

5400 return desc 

5401 else: 

5402 return super().describe() 

5403 

5404 def describe_or_def(self) -> str: 

5405 desc = "|".join(self.describe_value(e) for e in self._allowed_values) 

5406 if len(desc) < 80: 

5407 if len(self._allowed_values) > 1: 

5408 desc = f"{{{desc}}}" 

5409 return desc 

5410 else: 

5411 return super().describe_or_def() 

5412 

5413 def options(self) -> _t.Collection[yuio.widget.Option[T]] | None: 

5414 return [ 

5415 yuio.widget.Option(e, self.describe_value(e)) for e in self._allowed_values 

5416 ] 

5417 

5418 def completer(self) -> yuio.complete.Completer | None: 

5419 return yuio.complete.Choice( 

5420 [yuio.complete.Option(self.describe_value(e)) for e in self._allowed_values] 

5421 ) 

5422 

5423 def widget( 

5424 self, 

5425 default: object | yuio.Missing, 

5426 input_description: str | None, 

5427 default_description: str | None, 

5428 /, 

5429 ) -> yuio.widget.Widget[T | yuio.Missing]: 

5430 allowed_values = list(self._allowed_values) 

5431 

5432 options = _t.cast(list[yuio.widget.Option[T | yuio.Missing]], self.options()) 

5433 

5434 if not options: 

5435 return super().widget(default, input_description, default_description) 

5436 

5437 if default is yuio.MISSING: 

5438 default_index = 0 

5439 elif default in allowed_values: 

5440 default_index = list(allowed_values).index(default) # type: ignore 

5441 else: 

5442 options.insert( 

5443 0, yuio.widget.Option(yuio.MISSING, default_description or str(default)) 

5444 ) 

5445 default_index = 0 

5446 

5447 return yuio.widget.Choice(options, default_index=default_index) 

5448 

5449 

5450class WithMeta(MappingParser[T, T], _t.Generic[T]): 

5451 """WithMeta(inner: Parser[T], /, *, desc: str, completer: yuio.complete.Completer | None | ~yuio.MISSING = MISSING) 

5452 

5453 Overrides inline help messages and other meta information of a wrapped parser. 

5454 

5455 Inline help messages will show up as hints in autocompletion and widgets. 

5456 

5457 :param inner: 

5458 inner parser. 

5459 :param desc: 

5460 description override. This short string will be used in CLI, widgets, and 

5461 completers to describe expected value. 

5462 :param completer: 

5463 completer override. Pass :data:`None` to disable completion. 

5464 

5465 """ 

5466 

5467 if TYPE_CHECKING: 

5468 

5469 @_t.overload 

5470 def __new__( 

5471 cls, 

5472 inner: Parser[T], 

5473 /, 

5474 *, 

5475 desc: str | None = None, 

5476 completer: yuio.complete.Completer | yuio.Missing | None = yuio.MISSING, 

5477 ) -> MappingParser[T, T]: ... 

5478 

5479 @_t.overload 

5480 def __new__( 

5481 cls, 

5482 /, 

5483 *, 

5484 desc: str | None = None, 

5485 completer: yuio.complete.Completer | yuio.Missing | None = yuio.MISSING, 

5486 ) -> PartialParser: ... 

5487 

5488 def __new__(cls, *args, **kwargs) -> _t.Any: ... 

5489 

5490 def __init__( 

5491 self, 

5492 *args, 

5493 desc: str | None = None, 

5494 completer: yuio.complete.Completer | yuio.Missing | None = yuio.MISSING, 

5495 ): 

5496 inner: Parser[T] | None 

5497 if not args: 

5498 inner = None 

5499 elif len(args) == 1: 

5500 inner = args[0] 

5501 else: 

5502 raise TypeError(f"expected at most 1 positional argument, got {len(args)}") 

5503 

5504 self._desc = desc 

5505 self._completer = completer 

5506 super().__init__(inner) 

5507 

5508 def check_type(self, value: object, /) -> _t.TypeGuard[T]: 

5509 return True 

5510 

5511 def describe(self) -> str | None: 

5512 return self._desc or self._inner.describe() 

5513 

5514 def describe_or_def(self) -> str: 

5515 return self._desc or self._inner.describe_or_def() 

5516 

5517 def describe_many(self) -> str | tuple[str, ...]: 

5518 return self._desc or self._inner.describe_many() 

5519 

5520 def describe_value(self, value: object, /) -> str: 

5521 return self._inner.describe_value(value) 

5522 

5523 def parse_with_ctx(self, ctx: StrParsingContext, /) -> T: 

5524 return self._inner.parse_with_ctx(ctx) 

5525 

5526 def parse_many_with_ctx(self, ctxs: _t.Sequence[StrParsingContext], /) -> T: 

5527 return self._inner.parse_many_with_ctx(ctxs) 

5528 

5529 def parse_config_with_ctx(self, ctx: ConfigParsingContext, /) -> T: 

5530 return self._inner.parse_config_with_ctx(ctx) 

5531 

5532 def options(self) -> _t.Collection[yuio.widget.Option[T]] | None: 

5533 return self._inner.options() 

5534 

5535 def completer(self) -> yuio.complete.Completer | None: 

5536 if self._completer is not yuio.MISSING: 

5537 return self._completer # type: ignore 

5538 else: 

5539 return self._inner.completer() 

5540 

5541 def widget( 

5542 self, 

5543 default: object | yuio.Missing, 

5544 input_description: str | None, 

5545 default_description: str | None, 

5546 /, 

5547 ) -> yuio.widget.Widget[T | yuio.Missing]: 

5548 return self._inner.widget(default, input_description, default_description) 

5549 

5550 def to_json_value(self, value: object) -> yuio.json_schema.JsonValue: 

5551 return self._inner.to_json_value(value) 

5552 

5553 

5554class _WidgetResultMapper(yuio.widget.Map[T | yuio.Missing, str]): 

5555 def __init__( 

5556 self, 

5557 parser: Parser[T], 

5558 input_description: str | None, 

5559 default: object | yuio.Missing, 

5560 widget: yuio.widget.Widget[str], 

5561 ): 

5562 self._parser = parser 

5563 self._input_description = input_description 

5564 self._default = default 

5565 super().__init__(widget, self.mapper) 

5566 

5567 def mapper(self, s: str) -> T | yuio.Missing: 

5568 if not s and self._default is not yuio.MISSING: 

5569 return yuio.MISSING 

5570 elif not s: 

5571 raise ParsingError("Input is required") 

5572 try: 

5573 return self._parser.parse_with_ctx(StrParsingContext(s)) 

5574 except ParsingError as e: 

5575 if ( 

5576 isinstance( 

5577 self._inner, (yuio.widget.Input, yuio.widget.InputWithCompletion) 

5578 ) 

5579 and e.pos 

5580 and e.raw == self._inner.text 

5581 ): 

5582 if e.pos == (0, len(self._inner.text)): 

5583 # Don't highlight the entire text, it's not useful and creates 

5584 # visual noise. 

5585 self._inner.err_region = None 

5586 else: 

5587 self._inner.err_region = e.pos 

5588 e.raw = None 

5589 e.pos = None 

5590 raise 

5591 

5592 @property 

5593 def help_data(self): 

5594 return super().help_data.with_action( 

5595 group="Input Format", 

5596 msg=self._input_description, 

5597 prepend=True, 

5598 prepend_group=True, 

5599 ) 

5600 

5601 

5602def _secret_widget( 

5603 parser: Parser[T], 

5604 default: object | yuio.Missing, 

5605 input_description: str | None, 

5606 default_description: str | None, 

5607 /, 

5608) -> yuio.widget.Widget[T | yuio.Missing]: 

5609 return _WidgetResultMapper( 

5610 parser, 

5611 input_description, 

5612 default, 

5613 ( 

5614 yuio.widget.SecretInput( 

5615 placeholder=default_description or "", 

5616 ) 

5617 ), 

5618 ) 

5619 

5620 

5621class StrParsingContext: 

5622 """StrParsingContext(content: str, /, *, n_arg: int | None = None) 

5623 

5624 String parsing context tracks current position in the string. 

5625 

5626 :param content: 

5627 content to parse. 

5628 :param n_arg: 

5629 content index when using :meth:`~Parser.parse_many`. 

5630 

5631 """ 

5632 

5633 def __init__( 

5634 self, 

5635 content: str, 

5636 /, 

5637 *, 

5638 n_arg: int | None = None, 

5639 _value: str | None = None, 

5640 _start: int | None = None, 

5641 _end: int | None = None, 

5642 ): 

5643 self.start: int = _start if _start is not None else 0 

5644 """ 

5645 Start position of the value. 

5646 

5647 """ 

5648 

5649 self.end: int = _end if _end is not None else self.start + len(content) 

5650 """ 

5651 End position of the value. 

5652 

5653 """ 

5654 

5655 self.content: str = content 

5656 """ 

5657 Full content of the value that was passed to :meth:`Parser.parse`. 

5658 

5659 """ 

5660 

5661 self.value: str = _value if _value is not None else content 

5662 """ 

5663 Part of the :attr:`~StrParsingContext.content` that's currently being parsed. 

5664 

5665 """ 

5666 

5667 self.n_arg: int | None = n_arg 

5668 """ 

5669 For :meth:`~Parser.parse_many`, this attribute contains index of the value 

5670 that is being parsed. For :meth:`~Parser.parse`, this is :data:`None`. 

5671 

5672 """ 

5673 

5674 def split( 

5675 self, delimiter: str | None = None, /, maxsplit: int = -1 

5676 ) -> _t.Generator[StrParsingContext]: 

5677 """ 

5678 Split current value by the given delimiter while keeping track of the current position. 

5679 

5680 """ 

5681 

5682 if delimiter is None: 

5683 yield from self._split_space(maxsplit=maxsplit) 

5684 return 

5685 

5686 dlen = len(delimiter) 

5687 start = self.start 

5688 for part in self.value.split(delimiter, maxsplit=maxsplit): 

5689 yield StrParsingContext( 

5690 self.content, 

5691 _value=part, 

5692 _start=start, 

5693 _end=start + len(part), 

5694 n_arg=self.n_arg, 

5695 ) 

5696 start += len(part) + dlen 

5697 

5698 def _split_space(self, maxsplit: int = -1) -> _t.Generator[StrParsingContext]: 

5699 i = 0 

5700 n_splits = 0 

5701 is_space = True 

5702 for part in re.split(r"(\s+)", self.value): 

5703 is_space = not is_space 

5704 if is_space: 

5705 i += len(part) 

5706 continue 

5707 

5708 if not part: 

5709 continue 

5710 

5711 if maxsplit >= 0 and n_splits >= maxsplit: 

5712 part = self.value[i:] 

5713 yield StrParsingContext( 

5714 self.content, 

5715 _value=part, 

5716 _start=i, 

5717 _end=i + len(part), 

5718 n_arg=self.n_arg, 

5719 ) 

5720 return 

5721 else: 

5722 yield StrParsingContext( 

5723 self.content, 

5724 _value=part, 

5725 _start=i, 

5726 _end=i + len(part), 

5727 n_arg=self.n_arg, 

5728 ) 

5729 i += len(part) 

5730 n_splits += 1 

5731 

5732 def strip(self, chars: str | None = None, /) -> StrParsingContext: 

5733 """ 

5734 Strip current value while keeping track of the current position. 

5735 

5736 """ 

5737 

5738 l_stripped = self.value.lstrip(chars) 

5739 start = self.start + (len(self.value) - len(l_stripped)) 

5740 stripped = l_stripped.rstrip(chars) 

5741 return StrParsingContext( 

5742 self.content, 

5743 _value=stripped, 

5744 _start=start, 

5745 _end=start + len(stripped), 

5746 n_arg=self.n_arg, 

5747 ) 

5748 

5749 def strip_if_non_space(self) -> StrParsingContext: 

5750 """ 

5751 Strip current value unless it entirely consists of spaces. 

5752 

5753 """ 

5754 

5755 if not self.value or self.value.isspace(): 

5756 return self 

5757 else: 

5758 return self.strip() 

5759 

5760 # If you need more methods, feel free to open an issue or send a PR! 

5761 # For now, `split` and `strip` is enough. 

5762 

5763 

5764class ConfigParsingContext: 

5765 """ 

5766 Config parsing context tracks path in the config, similar to JSON path. 

5767 

5768 """ 

5769 

5770 def __init__( 

5771 self, 

5772 value: object, 

5773 /, 

5774 *, 

5775 parent: ConfigParsingContext | None = None, 

5776 key: _t.Any = None, 

5777 desc: str | None = None, 

5778 ): 

5779 self.value: object = value 

5780 """ 

5781 Config value to be validated and parsed. 

5782 

5783 """ 

5784 

5785 self.parent: ConfigParsingContext | None = parent 

5786 """ 

5787 Parent context. 

5788 

5789 """ 

5790 

5791 self.key: _t.Any = key 

5792 """ 

5793 Key that was accessed when we've descended from parent context to this one. 

5794 

5795 Root context has key :data:`None`. 

5796 

5797 """ 

5798 

5799 self.desc: str | None = desc 

5800 """ 

5801 Additional description of the key. 

5802 

5803 """ 

5804 

5805 def descend( 

5806 self, 

5807 value: _t.Any, 

5808 key: _t.Any, 

5809 desc: str | None = None, 

5810 ) -> ConfigParsingContext: 

5811 """ 

5812 Create a new context that adds a new key to the path. 

5813 

5814 :param value: 

5815 inner value that was derived from the current value by accessing it with 

5816 the given `key`. 

5817 :param key: 

5818 key that we use to descend into the current value. 

5819 

5820 For example, let's say we're parsing a list. We iterate over it and pass 

5821 its elements to a sub-parser. Before calling a sub-parser, we need to 

5822 make a new context for it. In this situation, we'll pass current element 

5823 as `value`, and is index as `key`. 

5824 :param desc: 

5825 human-readable description for the new context. Will be colorized 

5826 and ``%``-formatted with a single named argument `key`. 

5827 

5828 This is useful when parsing structures that need something more complex than 

5829 JSON path. For example, when parsing a key in a dictionary, it is helpful 

5830 to set description to something like ``"key of element #%(key)r"``. 

5831 This way, parsing errors will have a more clear message: 

5832 

5833 .. code-block:: text 

5834 

5835 Parsing error: 

5836 In key of element #2: 

5837 Expected str, got int: 10 

5838 

5839 """ 

5840 

5841 return ConfigParsingContext(value, parent=self, key=key, desc=desc) 

5842 

5843 def make_path(self) -> list[tuple[_t.Any, str | None]]: 

5844 """ 

5845 Capture current path. 

5846 

5847 :returns: 

5848 a list of tuples. First element of each tuple is a key, second is 

5849 an additional description. 

5850 

5851 """ 

5852 

5853 path = [] 

5854 

5855 root = self 

5856 while True: 

5857 if root.parent is None: 

5858 break 

5859 else: 

5860 path.append((root.key, root.desc)) 

5861 root = root.parent 

5862 

5863 path.reverse() 

5864 

5865 return path 

5866 

5867 

5868class _PathRenderer: 

5869 def __init__(self, path: list[tuple[_t.Any, str | None]]): 

5870 self._path = path 

5871 

5872 def __colorized_str__( 

5873 self, ctx: yuio.string.ReprContext 

5874 ) -> yuio.string.ColorizedString: 

5875 code_color = ctx.theme.get_color("msg/text:code/repr hl:repr") 

5876 punct_color = ctx.theme.get_color("msg/text:code/repr hl/punct:repr") 

5877 

5878 msg = yuio.string.ColorizedString(code_color) 

5879 msg.start_no_wrap() 

5880 

5881 for i, (key, desc) in enumerate(self._path): 

5882 if desc: 

5883 desc = ( 

5884 (yuio.string) 

5885 .colorize(desc, ctx=ctx) 

5886 .percent_format({"key": key}, ctx=ctx) 

5887 ) 

5888 

5889 if i == len(self._path) - 1: 

5890 # Last key. 

5891 if msg: 

5892 msg.append_color(punct_color) 

5893 msg.append_str(", ") 

5894 msg.append_colorized_str(desc) 

5895 else: 

5896 # Element in the middle. 

5897 if not msg: 

5898 msg.append_str("$") 

5899 msg.append_color(punct_color) 

5900 msg.append_str(".<") 

5901 msg.append_colorized_str(desc) 

5902 msg.append_str(">") 

5903 elif isinstance(key, str) and re.match(r"^[a-zA-Z_][\w-]*$", key): 

5904 # Key is identifier-like, use `x.key` notation. 

5905 if not msg: 

5906 msg.append_str("$") 

5907 msg.append_color(punct_color) 

5908 msg.append_str(".") 

5909 msg.append_color(code_color) 

5910 msg.append_str(key) 

5911 else: 

5912 # Key is not identifier-like, use `x[key]` notation. 

5913 if not msg: 

5914 msg.append_str("$") 

5915 msg.append_color(punct_color) 

5916 msg.append_str("[") 

5917 msg.append_color(code_color) 

5918 msg.append_str(repr(key)) 

5919 msg.append_color(punct_color) 

5920 msg.append_str("]") 

5921 

5922 msg.end_no_wrap() 

5923 return msg 

5924 

5925 

5926class _CodeRenderer: 

5927 def __init__(self, code: str, pos: tuple[int, int], as_cli: bool = False): 

5928 self._code = code 

5929 self._pos = pos 

5930 self._as_cli = as_cli 

5931 

5932 def __colorized_str__( 

5933 self, ctx: yuio.string.ReprContext 

5934 ) -> yuio.string.ColorizedString: 

5935 width = ctx.width - 2 # Account for indentation. 

5936 

5937 if width < 10: # 6 symbols for ellipsis and at least 2 wide chars. 

5938 return yuio.string.ColorizedString() 

5939 

5940 start, end = self._pos 

5941 if end == start: 

5942 end += 1 

5943 

5944 left = self._code[:start] 

5945 center = self._code[start:end] 

5946 right = self._code[end:] 

5947 

5948 l_width = yuio.string.line_width(left) 

5949 c_width = yuio.string.line_width(center) 

5950 r_width = yuio.string.line_width(right) 

5951 

5952 available_width = width - (3 if left else 0) - 3 

5953 if c_width > available_width: 

5954 # Center can't fit: remove left and right side, 

5955 # and trim as much center as needed. 

5956 

5957 left = "..." if l_width > 3 else left 

5958 l_width = len(left) 

5959 

5960 right = "" 

5961 r_width = 0 

5962 

5963 new_c = "" 

5964 c_width = 0 

5965 

5966 for c in center: 

5967 cw = yuio.string.line_width(c) 

5968 if c_width + cw <= available_width: 

5969 new_c += c 

5970 c_width += cw 

5971 else: 

5972 new_c += "..." 

5973 c_width += 3 

5974 break 

5975 center = new_c 

5976 

5977 if r_width > 3 and l_width + c_width + r_width > width: 

5978 # Trim right side. 

5979 new_r = "" 

5980 r_width = 3 

5981 for c in right: 

5982 cw = yuio.string.line_width(c) 

5983 if l_width + c_width + r_width + cw <= width: 

5984 new_r += c 

5985 r_width += cw 

5986 else: 

5987 new_r += "..." 

5988 break 

5989 right = new_r 

5990 

5991 if l_width > 3 and l_width + c_width + r_width > width: 

5992 # Trim left side. 

5993 new_l = "" 

5994 l_width = 3 

5995 for c in left[::-1]: 

5996 cw = yuio.string.line_width(c) 

5997 if l_width + c_width + r_width + cw <= width: 

5998 new_l += c 

5999 l_width += cw 

6000 else: 

6001 new_l += "..." 

6002 break 

6003 left = new_l[::-1] 

6004 

6005 if self._as_cli: 

6006 punct_color = ctx.theme.get_color( 

6007 "msg/text:code/sh-usage hl/punct:sh-usage" 

6008 ) 

6009 else: 

6010 punct_color = ctx.theme.get_color("msg/text:code/text hl/punct:text") 

6011 

6012 res = yuio.string.ColorizedString() 

6013 res.start_no_wrap() 

6014 

6015 if self._as_cli: 

6016 res.append_color(punct_color) 

6017 res.append_str("$ ") 

6018 res.append_colorized_str( 

6019 ctx.str( 

6020 yuio.string.Hl( 

6021 left.replace("%", "%%") + "%s" + right.replace("%", "%%"), # pyright: ignore[reportArgumentType] 

6022 yuio.string.WithBaseColor( 

6023 center, base_color="hl/error:sh-usage" 

6024 ), 

6025 syntax="sh-usage", 

6026 ) 

6027 ) 

6028 ) 

6029 else: 

6030 text_color = ctx.theme.get_color("msg/text:code/text") 

6031 res.append_color(punct_color) 

6032 res.append_str("> ") 

6033 res.append_color(text_color) 

6034 res.append_str(left) 

6035 res.append_color(text_color | ctx.theme.get_color("hl/error:text")) 

6036 res.append_str(center) 

6037 res.append_color(text_color) 

6038 res.append_str(right) 

6039 res.append_color(yuio.color.Color.NONE) 

6040 res.append_str("\n") 

6041 if self._as_cli: 

6042 text_color = ctx.theme.get_color("msg/text:code/sh-usage") 

6043 res.append_color(text_color | ctx.theme.get_color("hl/error:sh-usage")) 

6044 else: 

6045 text_color = ctx.theme.get_color("msg/text:code/text") 

6046 res.append_color(text_color | ctx.theme.get_color("hl/error:text")) 

6047 res.append_str(" ") 

6048 res.append_str(" " * yuio.string.line_width(left)) 

6049 res.append_str("~" * yuio.string.line_width(center)) 

6050 

6051 res.end_no_wrap() 

6052 

6053 return res 

6054 

6055 

6056def _repr_and_adjust_pos(s: str, pos: tuple[int, int]): 

6057 start, end = pos 

6058 

6059 left = json.dumps(s[:start])[:-1] 

6060 center = json.dumps(s[start:end])[1:-1] 

6061 right = json.dumps(s[end:])[1:] 

6062 

6063 return left + center + right, (len(left), len(left) + len(center)) 

6064 

6065 

6066_FromTypeHintCallback: _t.TypeAlias = _t.Callable[ 

6067 [type, type | None, tuple[object, ...]], Parser[object] | None 

6068] 

6069 

6070 

6071_FROM_TYPE_HINT_CALLBACKS: list[tuple[_FromTypeHintCallback, bool]] = [] 

6072_FROM_TYPE_HINT_DELIM_SUGGESTIONS: list[str | None] = [ 

6073 None, 

6074 ",", 

6075 "@", 

6076 "/", 

6077 "=", 

6078] 

6079 

6080 

6081class _FromTypeHintDepth(threading.local): 

6082 def __init__(self): 

6083 self.depth: int = 0 

6084 self.uses_delim = False 

6085 

6086 

6087_FROM_TYPE_HINT_DEPTH: _FromTypeHintDepth = _FromTypeHintDepth() 

6088 

6089 

6090@_t.overload 

6091def from_type_hint(ty: type[T], /) -> Parser[T]: ... 

6092@_t.overload 

6093def from_type_hint(ty: object, /) -> Parser[object]: ... 

6094def from_type_hint(ty: _t.Any, /) -> Parser[object]: 

6095 """from_type_hint(ty: type[T], /) -> Parser[T] 

6096 

6097 Create parser from a type hint. 

6098 

6099 :param ty: 

6100 a type hint. 

6101 

6102 This type hint should not contain strings or forward references. Make sure 

6103 they're resolved before passing it to this function. 

6104 :returns: 

6105 a parser instance created from type hint. 

6106 :raises: 

6107 :class:`TypeError` if type hint contains forward references or types 

6108 that don't have associated parsers. 

6109 :example: 

6110 :: 

6111 

6112 >>> from_type_hint(list[int] | None) 

6113 Optional(List(Int)) 

6114 

6115 """ 

6116 

6117 result = _from_type_hint(ty) 

6118 setattr(result, "_Parser__typehint", ty) 

6119 return result 

6120 

6121 

6122def _from_type_hint(ty: _t.Any, /) -> Parser[object]: 

6123 if isinstance(ty, (str, _t.ForwardRef)): 

6124 raise TypeError(f"forward references are not supported here: {ty}") 

6125 

6126 origin = _t.get_origin(ty) 

6127 args = _t.get_args(ty) 

6128 

6129 if origin is _t.Annotated: 

6130 p = from_type_hint(args[0]) 

6131 for arg in args[1:]: 

6132 if isinstance(arg, PartialParser): 

6133 p = arg.wrap(p) 

6134 return p 

6135 

6136 for cb, uses_delim in _FROM_TYPE_HINT_CALLBACKS: 

6137 prev_uses_delim = _FROM_TYPE_HINT_DEPTH.uses_delim 

6138 _FROM_TYPE_HINT_DEPTH.uses_delim = uses_delim 

6139 _FROM_TYPE_HINT_DEPTH.depth += uses_delim 

6140 try: 

6141 p = cb(ty, origin, args) 

6142 if p is not None: 

6143 return p 

6144 finally: 

6145 _FROM_TYPE_HINT_DEPTH.uses_delim = prev_uses_delim 

6146 _FROM_TYPE_HINT_DEPTH.depth -= uses_delim 

6147 

6148 if _tx.is_union(origin): 

6149 if is_optional := (type(None) in args): 

6150 args = list(args) 

6151 args.remove(type(None)) 

6152 if len(args) == 1: 

6153 p = from_type_hint(args[0]) 

6154 else: 

6155 p = Union(*[from_type_hint(arg) for arg in args]) 

6156 if is_optional: 

6157 p = Optional(p) 

6158 return p 

6159 else: 

6160 raise TypeError(f"unsupported type {_tx.type_repr(ty)}") 

6161 

6162 

6163@_t.overload 

6164def register_type_hint_conversion( 

6165 cb: _FromTypeHintCallback, 

6166 /, 

6167 *, 

6168 uses_delim: bool = False, 

6169) -> _FromTypeHintCallback: ... 

6170@_t.overload 

6171def register_type_hint_conversion( 

6172 *, 

6173 uses_delim: bool = False, 

6174) -> _t.Callable[[_FromTypeHintCallback], _FromTypeHintCallback]: ... 

6175def register_type_hint_conversion( 

6176 cb: _FromTypeHintCallback | None = None, 

6177 /, 

6178 *, 

6179 uses_delim: bool = False, 

6180) -> ( 

6181 _FromTypeHintCallback | _t.Callable[[_FromTypeHintCallback], _FromTypeHintCallback] 

6182): 

6183 """ 

6184 Register a new converter from a type hint to a parser. 

6185 

6186 This function takes a callback that accepts three positional arguments: 

6187 

6188 - a type hint, 

6189 - a type hint's origin (as defined by :func:`typing.get_origin`), 

6190 - a type hint's args (as defined by :func:`typing.get_args`). 

6191 

6192 The callback should return a parser if it can, or :data:`None` otherwise. 

6193 

6194 All registered callbacks are tried in the same order 

6195 as they were registered. 

6196 

6197 If `uses_delim` is :data:`True`, callback can use 

6198 :func:`suggest_delim_for_type_hint_conversion`. 

6199 

6200 This function can be used as a decorator. 

6201 

6202 :param cb: 

6203 a function that should inspect a type hint and possibly return a parser. 

6204 :param uses_delim: 

6205 indicates that callback will use 

6206 :func:`suggest_delim_for_type_hint_conversion`. 

6207 :example: 

6208 .. invisible-code-block: python 

6209 

6210 class MyType: ... 

6211 class MyTypeParser(ValueParser[MyType]): 

6212 def __init__(self): super().__init__(MyType) 

6213 def parse_with_ctx(self, ctx: StrParsingContext, /): ... 

6214 def parse_config_with_ctx(self, value, /): ... 

6215 def to_json_schema(self, ctx, /): ... 

6216 def to_json_value(self, value, /): ... 

6217 

6218 .. code-block:: python 

6219 

6220 @register_type_hint_conversion 

6221 def my_type_conversion(ty, origin, args): 

6222 if ty is MyType: 

6223 return MyTypeParser() 

6224 else: 

6225 return None 

6226 

6227 :: 

6228 

6229 >>> from_type_hint(MyType) 

6230 MyTypeParser 

6231 

6232 .. invisible-code-block: python 

6233 

6234 del _FROM_TYPE_HINT_CALLBACKS[-1] 

6235 

6236 """ 

6237 

6238 def registrar(cb: _FromTypeHintCallback): 

6239 _FROM_TYPE_HINT_CALLBACKS.append((cb, uses_delim)) 

6240 return cb 

6241 

6242 return registrar(cb) if cb is not None else registrar 

6243 

6244 

6245def suggest_delim_for_type_hint_conversion() -> str | None: 

6246 """ 

6247 Suggests a delimiter for use in type hint converters. 

6248 

6249 When creating a parser for a collection of items based on a type hint, 

6250 it is important to use different delimiters for nested collections. 

6251 This function can suggest such a delimiter based on the current type hint's depth. 

6252 

6253 .. invisible-code-block: python 

6254 

6255 class MyCollection(list, _t.Generic[T]): ... 

6256 class MyCollectionParser(CollectionParser[MyCollection[T], T], _t.Generic[T]): 

6257 def __init__(self, inner: Parser[T], /, *, delimiter: _t.Optional[str] = None): 

6258 super().__init__(inner, ty=MyCollection, ctor=MyCollection, delimiter=delimiter) 

6259 def to_json_schema(self, ctx, /): ... 

6260 def to_json_value(self, value, /): ... 

6261 

6262 :raises: 

6263 :class:`RuntimeError` if called from a type converter that 

6264 didn't set `uses_delim` to :data:`True`. 

6265 :example: 

6266 .. code-block:: python 

6267 

6268 @register_type_hint_conversion(uses_delim=True) 

6269 def my_collection_conversion(ty, origin, args): 

6270 if origin is MyCollection: 

6271 return MyCollectionParser( 

6272 from_type_hint(args[0]), 

6273 delimiter=suggest_delim_for_type_hint_conversion(), 

6274 ) 

6275 else: 

6276 return None 

6277 

6278 :: 

6279 

6280 >>> parser = from_type_hint(MyCollection[MyCollection[str]]) 

6281 >>> parser 

6282 MyCollectionParser(MyCollectionParser(Str)) 

6283 >>> # First delimiter is `None`, meaning split by whitespace: 

6284 >>> parser._delimiter is None 

6285 True 

6286 >>> # Second delimiter is `","`: 

6287 >>> parser._inner._delimiter == "," 

6288 True 

6289 

6290 .. 

6291 >>> del _FROM_TYPE_HINT_CALLBACKS[-1] 

6292 

6293 """ 

6294 

6295 if not _FROM_TYPE_HINT_DEPTH.uses_delim: 

6296 raise RuntimeError( 

6297 "looking up delimiters is not available in this callback; did you forget" 

6298 " to pass `uses_delim=True` when registering this callback?" 

6299 ) 

6300 

6301 depth = _FROM_TYPE_HINT_DEPTH.depth - 1 

6302 if depth < len(_FROM_TYPE_HINT_DELIM_SUGGESTIONS): 

6303 return _FROM_TYPE_HINT_DELIM_SUGGESTIONS[depth] 

6304 else: 

6305 return None 

6306 

6307 

6308register_type_hint_conversion(lambda ty, origin, args: Str() if ty is str else None) 

6309register_type_hint_conversion(lambda ty, origin, args: Int() if ty is int else None) 

6310register_type_hint_conversion(lambda ty, origin, args: Float() if ty is float else None) 

6311register_type_hint_conversion(lambda ty, origin, args: Bool() if ty is bool else None) 

6312register_type_hint_conversion( 

6313 lambda ty, origin, args: ( 

6314 Enum(ty) if isinstance(ty, type) and issubclass(ty, enum.Enum) else None 

6315 ) 

6316) 

6317register_type_hint_conversion( 

6318 lambda ty, origin, args: Decimal() if ty is decimal.Decimal else None 

6319) 

6320register_type_hint_conversion( 

6321 lambda ty, origin, args: Fraction() if ty is fractions.Fraction else None 

6322) 

6323register_type_hint_conversion( 

6324 lambda ty, origin, args: ( 

6325 List( 

6326 from_type_hint(args[0]), delimiter=suggest_delim_for_type_hint_conversion() 

6327 ) 

6328 if origin is list 

6329 else None 

6330 ), 

6331 uses_delim=True, 

6332) 

6333register_type_hint_conversion( 

6334 lambda ty, origin, args: ( 

6335 Set(from_type_hint(args[0]), delimiter=suggest_delim_for_type_hint_conversion()) 

6336 if origin is set 

6337 else None 

6338 ), 

6339 uses_delim=True, 

6340) 

6341register_type_hint_conversion( 

6342 lambda ty, origin, args: ( 

6343 FrozenSet( 

6344 from_type_hint(args[0]), delimiter=suggest_delim_for_type_hint_conversion() 

6345 ) 

6346 if origin is frozenset 

6347 else None 

6348 ), 

6349 uses_delim=True, 

6350) 

6351register_type_hint_conversion( 

6352 lambda ty, origin, args: ( 

6353 Dict( 

6354 from_type_hint(args[0]), 

6355 from_type_hint(args[1]), 

6356 delimiter=suggest_delim_for_type_hint_conversion(), 

6357 ) 

6358 if origin is dict 

6359 else None 

6360 ), 

6361 uses_delim=True, 

6362) 

6363register_type_hint_conversion( 

6364 lambda ty, origin, args: ( 

6365 Tuple( 

6366 *[from_type_hint(arg) for arg in args], 

6367 delimiter=suggest_delim_for_type_hint_conversion(), 

6368 ) 

6369 if origin is tuple and ... not in args 

6370 else None 

6371 ), 

6372 uses_delim=True, 

6373) 

6374register_type_hint_conversion( 

6375 lambda ty, origin, args: Path() if ty is pathlib.Path else None 

6376) 

6377register_type_hint_conversion( 

6378 lambda ty, origin, args: Json() if ty is yuio.json_schema.JsonValue else None 

6379) 

6380register_type_hint_conversion( 

6381 lambda ty, origin, args: DateTime() if ty is datetime.datetime else None 

6382) 

6383register_type_hint_conversion( 

6384 lambda ty, origin, args: Date() if ty is datetime.date else None 

6385) 

6386register_type_hint_conversion( 

6387 lambda ty, origin, args: Time() if ty is datetime.time else None 

6388) 

6389register_type_hint_conversion( 

6390 lambda ty, origin, args: TimeDelta() if ty is datetime.timedelta else None 

6391) 

6392register_type_hint_conversion( 

6393 lambda ty, origin, args: ( 

6394 Literal(*_t.cast(tuple[_t.Any, ...], args)) if origin is _t.Literal else None 

6395 ) 

6396) 

6397 

6398 

6399@register_type_hint_conversion 

6400def __secret(ty, origin, args): 

6401 if ty is SecretValue: 

6402 raise TypeError("yuio.secret.SecretValue requires type arguments") 

6403 if origin is SecretValue: 

6404 if len(args) == 1: 

6405 return Secret(from_type_hint(args[0])) 

6406 else: # pragma: no cover 

6407 raise TypeError( 

6408 f"yuio.secret.SecretValue requires 1 type argument, got {len(args)}" 

6409 ) 

6410 return None 

6411 

6412 

6413def _is_optional_parser(parser: Parser[_t.Any] | None, /) -> bool: 

6414 while parser is not None: 

6415 if isinstance(parser, Optional): 

6416 return True 

6417 parser = parser.__wrapped_parser__ 

6418 return False 

6419 

6420 

6421def _is_bool_parser(parser: Parser[_t.Any] | None, /) -> bool: 

6422 while parser is not None: 

6423 if isinstance(parser, Bool): 

6424 return True 

6425 parser = parser.__wrapped_parser__ 

6426 return False