Coverage for yuio / parse.py: 90%

1819 statements  

« prev     ^ index     » next       coverage.py v7.13.3, created at 2026-02-03 15:42 +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 return value.name 

2551 else: 

2552 return value.value 

2553 

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

2555 if self.__docs is not None: 

2556 return self.__docs 

2557 docs = _find_docs(self._inner) 

2558 res = {} 

2559 for e in self._inner: 

2560 text = docs.get(e.name) 

2561 if not text: 

2562 continue 

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

2564 res[e] = text[:index] 

2565 else: 

2566 res[e] = text 

2567 return res 

2568 

2569 def _get_desc(self) -> str: 

2570 return self._inner.__name__ 

2571 

2572 def to_json_schema( 

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

2574 ) -> yuio.json_schema.JsonSchemaType: 

2575 schema = super().to_json_schema(ctx) 

2576 

2577 if self._doc_inline: 

2578 return schema 

2579 else: 

2580 return ctx.add_type( 

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

2582 _tx.type_repr(self._inner), 

2583 lambda: yuio.json_schema.Meta( 

2584 schema, 

2585 title=self._inner.__name__, 

2586 description=self._inner.__doc__, 

2587 ), 

2588 ) 

2589 

2590 def __repr__(self): 

2591 if self._inner_raw is not None: 

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

2593 else: 

2594 return self.__class__.__name__ 

2595 

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

2597 class _TyWrapper: 

2598 inner: type 

2599 by_name: bool 

2600 to_dash_case: bool 

2601 

2602 

2603class _LiteralType: 

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

2605 self._allowed_values = allowed_values 

2606 

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

2608 return instance in self._allowed_values 

2609 

2610 

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

2612 """ 

2613 Parser for literal values. 

2614 

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

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

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

2618 

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

2620 

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

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

2623 to parse these values. 

2624 

2625 """ 

2626 

2627 if TYPE_CHECKING: 

2628 

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

2630 

2631 def __init__( 

2632 self, 

2633 *literal_values: L, 

2634 ): 

2635 self._converted_values = {} 

2636 

2637 for value in literal_values: 

2638 orig_value = value 

2639 

2640 if isinstance(value, enum.Enum): 

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

2642 value = value.name 

2643 else: 

2644 value = value.value 

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

2646 value, str 

2647 ): 

2648 value = _to_dash_case(value) 

2649 self._converted_values[orig_value] = value 

2650 

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

2652 raise TypeError( 

2653 f"literal parser doesn't support literals " 

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

2655 ) 

2656 super().__init__( 

2657 literal_values, 

2658 _LiteralType(literal_values), # type: ignore 

2659 ) 

2660 

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

2662 with self._patch_stack_summary(): 

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

2664 

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

2666 return self._inner 

2667 

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

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

2670 

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

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

2673 if isinstance(value, str): 

2674 return value == given 

2675 elif isinstance(value, bool): 

2676 try: 

2677 given_parsed = Bool().parse(given) 

2678 except ParsingError: 

2679 return False 

2680 else: 

2681 return value == given_parsed 

2682 elif isinstance(value, int): 

2683 try: 

2684 given_parsed = Int().parse(given) 

2685 except ParsingError: 

2686 return False 

2687 else: 

2688 return value == given_parsed 

2689 else: 

2690 return False 

2691 

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

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

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

2695 

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

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

2698 return value == given 

2699 

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

2701 return value # type: ignore 

2702 

2703 def __repr__(self): 

2704 if self._inner_raw is not None: 

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

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

2707 else: 

2708 return self.__class__.__name__ 

2709 

2710 

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

2712 """ 

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

2714 

2715 """ 

2716 

2717 def __init__(self): 

2718 super().__init__(decimal.Decimal) 

2719 

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

2721 ctx = ctx.strip_if_non_space() 

2722 try: 

2723 return decimal.Decimal(ctx.value) 

2724 except (ArithmeticError, ValueError, TypeError): 

2725 raise ParsingError( 

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

2727 ctx.value, 

2728 ctx=ctx, 

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

2730 ) from None 

2731 

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

2733 value = ctx.value 

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

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

2736 try: 

2737 return decimal.Decimal(value) 

2738 except (ArithmeticError, ValueError, TypeError): 

2739 raise ParsingError( 

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

2741 value, 

2742 ctx=ctx, 

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

2744 ) from None 

2745 

2746 def to_json_schema( 

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

2748 ) -> yuio.json_schema.JsonSchemaType: 

2749 return ctx.add_type( 

2750 decimal.Decimal, 

2751 "Decimal", 

2752 lambda: yuio.json_schema.Meta( 

2753 yuio.json_schema.OneOf( 

2754 [ 

2755 yuio.json_schema.Number(), 

2756 yuio.json_schema.String( 

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

2758 ), 

2759 ] 

2760 ), 

2761 title="Decimal", 

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

2763 ), 

2764 ) 

2765 

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

2767 assert self.assert_type(value) 

2768 return str(value) 

2769 

2770 

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

2772 """ 

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

2774 

2775 """ 

2776 

2777 def __init__(self): 

2778 super().__init__(fractions.Fraction) 

2779 

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

2781 ctx = ctx.strip_if_non_space() 

2782 try: 

2783 return fractions.Fraction(ctx.value) 

2784 except ValueError: 

2785 raise ParsingError( 

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

2787 ctx.value, 

2788 ctx=ctx, 

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

2790 ) from None 

2791 except ZeroDivisionError: 

2792 raise ParsingError( 

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

2794 ctx.value, 

2795 ctx=ctx, 

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

2797 ) from None 

2798 

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

2800 value = ctx.value 

2801 if ( 

2802 isinstance(value, (list, tuple)) 

2803 and len(value) == 2 

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

2805 ): 

2806 try: 

2807 return fractions.Fraction(*value) 

2808 except (ValueError, TypeError): 

2809 raise ParsingError( 

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

2811 value[0], 

2812 value[1], 

2813 ctx=ctx, 

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

2815 ) from None 

2816 except ZeroDivisionError: 

2817 raise ParsingError( 

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

2819 value[0], 

2820 value[1], 

2821 ctx=ctx, 

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

2823 ) from None 

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

2825 try: 

2826 return fractions.Fraction(value) 

2827 except (ValueError, TypeError): 

2828 raise ParsingError( 

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

2830 value, 

2831 ctx=ctx, 

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

2833 ) from None 

2834 except ZeroDivisionError: 

2835 raise ParsingError( 

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

2837 value, 

2838 ctx=ctx, 

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

2840 ) from None 

2841 raise ParsingError.type_mismatch( 

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

2843 ) 

2844 

2845 def to_json_schema( 

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

2847 ) -> yuio.json_schema.JsonSchemaType: 

2848 return ctx.add_type( 

2849 fractions.Fraction, 

2850 "Fraction", 

2851 lambda: yuio.json_schema.Meta( 

2852 yuio.json_schema.OneOf( 

2853 [ 

2854 yuio.json_schema.Number(), 

2855 yuio.json_schema.String( 

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

2857 ), 

2858 yuio.json_schema.Tuple( 

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

2860 ), 

2861 ] 

2862 ), 

2863 title="Fraction", 

2864 description="A rational number.", 

2865 ), 

2866 ) 

2867 

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

2869 assert self.assert_type(value) 

2870 return str(value) 

2871 

2872 

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

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

2875 

2876 A parser that tries to parse value as JSON. 

2877 

2878 This parser will load JSON strings into python objects. 

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

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

2881 

2882 :param inner: 

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

2884 

2885 """ 

2886 

2887 if TYPE_CHECKING: 

2888 

2889 @_t.overload 

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

2891 

2892 @_t.overload 

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

2894 

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

2896 

2897 def __init__( 

2898 self, 

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

2900 /, 

2901 ): 

2902 super().__init__(inner, object) 

2903 

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

2905 result = _copy(self) 

2906 result._inner = parser 

2907 return result 

2908 

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

2910 ctx = ctx.strip_if_non_space() 

2911 try: 

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

2913 except json.JSONDecodeError as e: 

2914 raise ParsingError( 

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

2916 ctx.value, 

2917 yuio.string.Indent(e), 

2918 ctx=ctx, 

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

2920 ) from None 

2921 try: 

2922 return self.parse_config_with_ctx(ConfigParsingContext(config_value)) 

2923 except ParsingError as e: 

2924 raise ParsingError( 

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

2926 yuio.string.Indent(e), 

2927 ctx=ctx, 

2928 fallback_msg="Error in parsed json value", 

2929 ) from None 

2930 

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

2932 if self._inner_raw is not None: 

2933 return self._inner_raw.parse_config_with_ctx(ctx) 

2934 else: 

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

2936 

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

2938 return True 

2939 

2940 def to_json_schema( 

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

2942 ) -> yuio.json_schema.JsonSchemaType: 

2943 if self._inner_raw is not None: 

2944 return self._inner_raw.to_json_schema(ctx) 

2945 else: 

2946 return yuio.json_schema.Any() 

2947 

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

2949 assert self.assert_type(value) 

2950 if self._inner_raw is not None: 

2951 return self._inner_raw.to_json_value(value) 

2952 return value # type: ignore 

2953 

2954 def __repr__(self): 

2955 if self._inner_raw is not None: 

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

2957 else: 

2958 return super().__repr__() 

2959 

2960 

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

2962 """ 

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

2964 

2965 """ 

2966 

2967 def __init__(self): 

2968 super().__init__(datetime.datetime) 

2969 

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

2971 ctx = ctx.strip_if_non_space() 

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

2973 

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

2975 value = ctx.value 

2976 if isinstance(value, datetime.datetime): 

2977 return value 

2978 elif isinstance(value, str): 

2979 return self._parse(value, ctx) 

2980 else: 

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

2982 

2983 @staticmethod 

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

2985 try: 

2986 return datetime.datetime.fromisoformat(value) 

2987 except ValueError: 

2988 raise ParsingError( 

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

2990 value, 

2991 ctx=ctx, 

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

2993 ) from None 

2994 

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

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

2997 

2998 def to_json_schema( 

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

3000 ) -> yuio.json_schema.JsonSchemaType: 

3001 return ctx.add_type( 

3002 datetime.datetime, 

3003 "DateTime", 

3004 lambda: yuio.json_schema.Meta( 

3005 yuio.json_schema.String( 

3006 pattern=( 

3007 r"^" 

3008 r"(" 

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

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

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

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

3013 r")" 

3014 r"(" 

3015 r"[T ]" 

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

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

3018 r")?" 

3019 r"$" 

3020 ) 

3021 ), 

3022 title="DateTime", 

3023 description="ISO 8601 datetime.", 

3024 ), 

3025 ) 

3026 

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

3028 assert self.assert_type(value) 

3029 return str(value) 

3030 

3031 

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

3033 """ 

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

3035 

3036 """ 

3037 

3038 def __init__(self): 

3039 super().__init__(datetime.date) 

3040 

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

3042 ctx = ctx.strip_if_non_space() 

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

3044 

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

3046 value = ctx.value 

3047 if isinstance(value, datetime.datetime): 

3048 return value.date() 

3049 elif isinstance(value, datetime.date): 

3050 return value 

3051 elif isinstance(value, str): 

3052 return self._parse(value, ctx) 

3053 else: 

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

3055 

3056 @staticmethod 

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

3058 try: 

3059 return datetime.date.fromisoformat(value) 

3060 except ValueError: 

3061 raise ParsingError( 

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

3063 value, 

3064 ctx=ctx, 

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

3066 ) from None 

3067 

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

3069 return "YYYY-MM-DD" 

3070 

3071 def to_json_schema( 

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

3073 ) -> yuio.json_schema.JsonSchemaType: 

3074 return ctx.add_type( 

3075 datetime.date, 

3076 "Date", 

3077 lambda: yuio.json_schema.Meta( 

3078 yuio.json_schema.String( 

3079 pattern=( 

3080 r"^" 

3081 r"(" 

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

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

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

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

3086 r")" 

3087 r"$" 

3088 ) 

3089 ), 

3090 title="Date", 

3091 description="ISO 8601 date.", 

3092 ), 

3093 ) 

3094 

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

3096 assert self.assert_type(value) 

3097 return str(value) 

3098 

3099 

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

3101 """ 

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

3103 

3104 """ 

3105 

3106 def __init__(self): 

3107 super().__init__(datetime.time) 

3108 

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

3110 ctx = ctx.strip_if_non_space() 

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

3112 

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

3114 value = ctx.value 

3115 if isinstance(value, datetime.datetime): 

3116 return value.time() 

3117 elif isinstance(value, datetime.time): 

3118 return value 

3119 elif isinstance(value, str): 

3120 return self._parse(value, ctx) 

3121 else: 

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

3123 

3124 @staticmethod 

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

3126 try: 

3127 return datetime.time.fromisoformat(value) 

3128 except ValueError: 

3129 raise ParsingError( 

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

3131 value, 

3132 ctx=ctx, 

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

3134 ) from None 

3135 

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

3137 return "HH:MM:SS" 

3138 

3139 def to_json_schema( 

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

3141 ) -> yuio.json_schema.JsonSchemaType: 

3142 return ctx.add_type( 

3143 datetime.time, 

3144 "Time", 

3145 lambda: yuio.json_schema.Meta( 

3146 yuio.json_schema.String( 

3147 pattern=( 

3148 r"^" 

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

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

3151 r"$" 

3152 ) 

3153 ), 

3154 title="Time", 

3155 description="ISO 8601 time.", 

3156 ), 

3157 ) 

3158 

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

3160 assert self.assert_type(value) 

3161 return str(value) 

3162 

3163 

3164_UNITS_MAP = ( 

3165 ("days", ("d", "day", "days")), 

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

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

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

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

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

3171 ("weeks", ("w", "week", "weeks")), 

3172) 

3173 

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

3175 

3176_TIMEDELTA_RE = re.compile( 

3177 r""" 

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

3179 ^ 

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

3181 (?:,\s*)? 

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

3183 $ 

3184 """, 

3185 re.VERBOSE | re.IGNORECASE, 

3186) 

3187 

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

3189 

3190 

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

3192 """ 

3193 Parse a time delta. 

3194 

3195 """ 

3196 

3197 def __init__(self): 

3198 super().__init__(datetime.timedelta) 

3199 

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

3201 ctx = ctx.strip_if_non_space() 

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

3203 

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

3205 value = ctx.value 

3206 if isinstance(value, datetime.timedelta): 

3207 return value 

3208 elif isinstance(value, str): 

3209 return self._parse(value, ctx) 

3210 else: 

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

3212 

3213 @staticmethod 

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

3215 value = value.strip() 

3216 

3217 if not value: 

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

3219 if value.endswith(","): 

3220 raise ParsingError( 

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

3222 value, 

3223 ctx=ctx, 

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

3225 ) 

3226 if value.startswith(","): 

3227 raise ParsingError( 

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

3229 value, 

3230 ctx=ctx, 

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

3232 ) 

3233 

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

3235 ( 

3236 c_sign_s, 

3237 components_s, 

3238 t_sign_s, 

3239 hour, 

3240 minute, 

3241 second, 

3242 millisecond, 

3243 microsecond, 

3244 ) = match.groups() 

3245 else: 

3246 raise ParsingError( 

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

3248 value, 

3249 ctx=ctx, 

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

3251 ) 

3252 

3253 c_sign_s = -1 if c_sign_s == "-" else 1 

3254 t_sign_s = -1 if t_sign_s == "-" else 1 

3255 

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

3257 

3258 if components_s: 

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

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

3261 kwargs[unit_key] += int(num) 

3262 else: 

3263 raise ParsingError( 

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

3265 value, 

3266 unit, 

3267 ctx=ctx, 

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

3269 ) 

3270 

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

3272 

3273 timedelta += t_sign_s * datetime.timedelta( 

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

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

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

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

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

3279 ) 

3280 

3281 return timedelta 

3282 

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

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

3285 

3286 def to_json_schema( 

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

3288 ) -> yuio.json_schema.JsonSchemaType: 

3289 return ctx.add_type( 

3290 datetime.timedelta, 

3291 "TimeDelta", 

3292 lambda: yuio.json_schema.Meta( 

3293 yuio.json_schema.String( 

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

3295 pattern=( 

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

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

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

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

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

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

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

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

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

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

3306 ) 

3307 ), 

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

3309 description=".", 

3310 ), 

3311 ) 

3312 

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

3314 assert self.assert_type(value) 

3315 return str(value) 

3316 

3317 

3318class Seconds(TimeDelta): 

3319 """ 

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

3321 

3322 """ 

3323 

3324 @staticmethod 

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

3326 try: 

3327 seconds = float(value) 

3328 except ValueError: 

3329 raise ParsingError( 

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

3331 ctx.value, 

3332 ctx=ctx, 

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

3334 ) from None 

3335 return datetime.timedelta(seconds=seconds) 

3336 

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

3338 return "<seconds>" 

3339 

3340 def describe_or_def(self) -> str: 

3341 return "<seconds>" 

3342 

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

3344 return "<seconds>" 

3345 

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

3347 assert self.assert_type(value) 

3348 return str(value.total_seconds()) 

3349 

3350 def to_json_schema( 

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

3352 ) -> yuio.json_schema.JsonSchemaType: 

3353 return yuio.json_schema.Meta(yuio.json_schema.Number(), description="seconds") 

3354 

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

3356 assert self.assert_type(value) 

3357 return value.total_seconds() 

3358 

3359 

3360class Path(ValueParser[pathlib.Path]): 

3361 """ 

3362 Parse a file system path, return a :class:`pathlib.Path`. 

3363 

3364 :param extensions: 

3365 list of allowed file extensions, including preceding dots. 

3366 

3367 """ 

3368 

3369 def __init__( 

3370 self, 

3371 /, 

3372 *, 

3373 extensions: str | _t.Collection[str] | None = None, 

3374 ): 

3375 self._extensions = [extensions] if isinstance(extensions, str) else extensions 

3376 super().__init__(pathlib.Path) 

3377 

3378 def parse_with_ctx(self, ctx: StrParsingContext, /) -> pathlib.Path: 

3379 ctx = ctx.strip_if_non_space() 

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

3381 

3382 def parse_config_with_ctx(self, ctx: ConfigParsingContext, /) -> pathlib.Path: 

3383 value = ctx.value 

3384 if not isinstance(value, str): 

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

3386 return self._parse(value, ctx) 

3387 

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

3389 res = pathlib.Path(value).expanduser().resolve().absolute() 

3390 try: 

3391 self._validate(res) 

3392 except ParsingError as e: 

3393 e.set_ctx(ctx) 

3394 raise 

3395 return res 

3396 

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

3398 if self._extensions is not None: 

3399 desc = "|".join(f"<*{e}>" for e in self._extensions) 

3400 if len(self._extensions) > 1: 

3401 desc = f"{{{desc}}}" 

3402 return desc 

3403 else: 

3404 return super().describe() 

3405 

3406 def _validate(self, value: pathlib.Path, /): 

3407 if self._extensions is not None and not any( 

3408 value.name.endswith(ext) for ext in self._extensions 

3409 ): 

3410 raise ParsingError( 

3411 "<c path>%s</c> should have extension %s", 

3412 value, 

3413 yuio.string.Or(self._extensions), 

3414 ) 

3415 

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

3417 return yuio.complete.File(extensions=self._extensions) 

3418 

3419 def to_json_schema( 

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

3421 ) -> yuio.json_schema.JsonSchemaType: 

3422 return yuio.json_schema.String() 

3423 

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

3425 assert self.assert_type(value) 

3426 return str(value) 

3427 

3428 

3429class NonExistentPath(Path): 

3430 """ 

3431 Parse a file system path and verify that it doesn't exist. 

3432 

3433 :param extensions: 

3434 list of allowed file extensions, including preceding dots. 

3435 

3436 """ 

3437 

3438 def _validate(self, value: pathlib.Path, /): 

3439 super()._validate(value) 

3440 

3441 if value.exists(): 

3442 raise ParsingError("<c path>%s</c> already exists", value) 

3443 

3444 

3445class ExistingPath(Path): 

3446 """ 

3447 Parse a file system path and verify that it exists. 

3448 

3449 :param extensions: 

3450 list of allowed file extensions, including preceding dots. 

3451 

3452 """ 

3453 

3454 def _validate(self, value: pathlib.Path, /): 

3455 super()._validate(value) 

3456 

3457 if not value.exists(): 

3458 raise ParsingError("<c path>%s</c> doesn't exist", value) 

3459 

3460 

3461class File(ExistingPath): 

3462 """ 

3463 Parse a file system path and verify that it points to a regular file. 

3464 

3465 :param extensions: 

3466 list of allowed file extensions, including preceding dots. 

3467 

3468 """ 

3469 

3470 def _validate(self, value: pathlib.Path, /): 

3471 super()._validate(value) 

3472 

3473 if not value.is_file(): 

3474 raise ParsingError("<c path>%s</c> is not a file", value) 

3475 

3476 

3477class Dir(ExistingPath): 

3478 """ 

3479 Parse a file system path and verify that it points to a directory. 

3480 

3481 """ 

3482 

3483 def __init__(self): 

3484 # Disallow passing `extensions`. 

3485 super().__init__() 

3486 

3487 def _validate(self, value: pathlib.Path, /): 

3488 super()._validate(value) 

3489 

3490 if not value.is_dir(): 

3491 raise ParsingError("<c path>%s</c> is not a directory", value) 

3492 

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

3494 return yuio.complete.Dir() 

3495 

3496 

3497class GitRepo(Dir): 

3498 """ 

3499 Parse a file system path and verify that it points to a git repository. 

3500 

3501 This parser just checks that the given directory has 

3502 a subdirectory named ``.git``. 

3503 

3504 """ 

3505 

3506 def _validate(self, value: pathlib.Path, /): 

3507 super()._validate(value) 

3508 

3509 if not value.joinpath(".git").is_dir(): 

3510 raise ParsingError("<c path>%s</c> is not a git repository root", value) 

3511 

3512 

3513class Secret(Map[SecretValue[T], T], _t.Generic[T]): 

3514 """Secret(inner: Parser[U], /) 

3515 

3516 Wraps result of the inner parser into :class:`~yuio.secret.SecretValue` 

3517 and ensures that :func:`yuio.io.ask` doesn't show value as user enters it. 

3518 

3519 """ 

3520 

3521 if TYPE_CHECKING: 

3522 

3523 @_t.overload 

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

3525 

3526 @_t.overload 

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

3528 

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

3530 

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

3532 super().__init__(inner, SecretValue, lambda x: x.data) 

3533 

3534 def parse_with_ctx(self, ctx: StrParsingContext, /) -> SecretValue[T]: 

3535 with self._replace_error(): 

3536 return super().parse_with_ctx(ctx) 

3537 

3538 def parse_many_with_ctx( 

3539 self, ctxs: _t.Sequence[StrParsingContext], / 

3540 ) -> SecretValue[T]: 

3541 with self._replace_error(): 

3542 return super().parse_many_with_ctx(ctxs) 

3543 

3544 def parse_config_with_ctx(self, ctx: ConfigParsingContext, /) -> SecretValue[T]: 

3545 with self._replace_error(): 

3546 return super().parse_config_with_ctx(ctx) 

3547 

3548 @staticmethod 

3549 @contextlib.contextmanager 

3550 def _replace_error(): 

3551 try: 

3552 yield 

3553 except ParsingError as e: 

3554 # Error messages can contain secret value, hide them. 

3555 raise ParsingError( 

3556 yuio.string.Printable( 

3557 e.fallback_msg or "Error when parsing secret data" 

3558 ), 

3559 pos=e.pos, 

3560 path=e.path, 

3561 n_arg=e.n_arg, 

3562 # Omit raw value. 

3563 ) from None 

3564 

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

3566 return "***" 

3567 

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

3569 return None 

3570 

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

3572 return None 

3573 

3574 def widget( 

3575 self, 

3576 default: object | yuio.Missing, 

3577 input_description: str | None, 

3578 default_description: str | None, 

3579 /, 

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

3581 return _secret_widget(self, default, input_description, default_description) 

3582 

3583 def is_secret(self) -> bool: 

3584 return True 

3585 

3586 

3587class CollectionParser( 

3588 WrappingParser[C, Parser[T]], ValueParser[C], PartialParser, _t.Generic[C, T] 

3589): 

3590 """CollectionParser(inner: Parser[T] | None, /, **kwargs) 

3591 

3592 A base class for implementing collection parsing. It will split a string 

3593 by the given delimiter, parse each item using a subparser, and then pass 

3594 the result to the given constructor. 

3595 

3596 :param inner: 

3597 parser that will be used to parse collection items. 

3598 :param ty: 

3599 type of the collection that this parser returns. 

3600 :param ctor: 

3601 factory of instances of the collection that this parser returns. 

3602 It should take an iterable of parsed items, and return a collection. 

3603 :param iter: 

3604 a function that is used to get an iterator from a collection. 

3605 This defaults to :func:`iter`, but sometimes it may be different. 

3606 For example, :class:`Dict` is implemented as a collection of pairs, 

3607 and its `iter` is :meth:`dict.items`. 

3608 :param config_type: 

3609 type of a collection that we expect to find when parsing a config. 

3610 This will usually be a list. 

3611 :param config_type_iter: 

3612 a function that is used to get an iterator from a config value. 

3613 :param delimiter: 

3614 delimiter that will be passed to :py:meth:`str.split`. 

3615 

3616 The above parameters are exposed via protected attributes: 

3617 ``self._inner``, ``self._ty``, etc. 

3618 

3619 For example, let's implement a :class:`list` parser 

3620 that repeats each element twice: 

3621 

3622 .. code-block:: python 

3623 

3624 from typing import Iterable, Generic 

3625 

3626 

3627 class DoubleList(CollectionParser[list[T], T], Generic[T]): 

3628 def __init__(self, inner: Parser[T], /, *, delimiter: str | None = None): 

3629 super().__init__(inner, ty=list, ctor=self._ctor, delimiter=delimiter) 

3630 

3631 @staticmethod 

3632 def _ctor(values: Iterable[T]) -> list[T]: 

3633 return [x for value in values for x in [value, value]] 

3634 

3635 def to_json_schema( 

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

3637 ) -> yuio.json_schema.JsonSchemaType: 

3638 return {"type": "array", "items": self._inner.to_json_schema(ctx)} 

3639 

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

3641 assert self.assert_type(value) 

3642 return [self._inner.to_json_value(item) for item in value[::2]] 

3643 

3644 :: 

3645 

3646 >>> parser = DoubleList(Int()) 

3647 >>> parser.parse("1 2 3") 

3648 [1, 1, 2, 2, 3, 3] 

3649 >>> parser.to_json_value([1, 1, 2, 2, 3, 3]) 

3650 [1, 2, 3] 

3651 

3652 """ 

3653 

3654 _allow_completing_duplicates: typing.ClassVar[bool] = True 

3655 """ 

3656 If set to :data:`False`, autocompletion will not suggest item duplicates. 

3657 

3658 """ 

3659 

3660 def __init__( 

3661 self, 

3662 inner: Parser[T] | None, 

3663 /, 

3664 *, 

3665 ty: type[C], 

3666 ctor: _t.Callable[[_t.Iterable[T]], C], 

3667 iter: _t.Callable[[C], _t.Iterable[T]] = iter, 

3668 config_type: type[C2] | tuple[type[C2], ...] = list, 

3669 config_type_iter: _t.Callable[[C2], _t.Iterable[T]] = iter, 

3670 delimiter: str | None = None, 

3671 ): 

3672 if delimiter == "": 

3673 raise ValueError("empty delimiter") 

3674 

3675 #: See class parameters for more details. 

3676 self._ty = ty 

3677 self._ctor = ctor 

3678 self._iter = iter 

3679 self._config_type = config_type 

3680 self._config_type_iter = config_type_iter 

3681 self._delimiter = delimiter 

3682 

3683 super().__init__(inner, ty) 

3684 

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

3686 result = super().wrap(parser) 

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

3688 return result 

3689 

3690 def parse_with_ctx(self, ctx: StrParsingContext, /) -> C: 

3691 return self._ctor( 

3692 self._inner.parse_with_ctx(item) for item in ctx.split(self._delimiter) 

3693 ) 

3694 

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

3696 return self._ctor(self._inner.parse_with_ctx(item) for item in ctxs) 

3697 

3698 def supports_parse_many(self) -> bool: 

3699 return True 

3700 

3701 def parse_config_with_ctx(self, ctx: ConfigParsingContext, /) -> C: 

3702 value = ctx.value 

3703 if not isinstance(value, self._config_type): 

3704 expected = self._config_type 

3705 if not isinstance(expected, tuple): 

3706 expected = (expected,) 

3707 raise ParsingError.type_mismatch(value, *expected, ctx=ctx) 

3708 

3709 return self._ctor( 

3710 self._inner.parse_config_with_ctx(ctx.descend(item, i)) 

3711 for i, item in enumerate(self._config_type_iter(value)) 

3712 ) 

3713 

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

3715 return "*" 

3716 

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

3718 delimiter = self._delimiter or " " 

3719 value = self._inner.describe_or_def() 

3720 

3721 return f"{value}[{delimiter}{value}[{delimiter}...]]" 

3722 

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

3724 return self._inner.describe_or_def() 

3725 

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

3727 assert self.assert_type(value) 

3728 

3729 return (self._delimiter or " ").join( 

3730 self._inner.describe_value(item) for item in self._iter(value) 

3731 ) 

3732 

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

3734 return None 

3735 

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

3737 completer = self._inner.completer() 

3738 return ( 

3739 yuio.complete.List( 

3740 completer, 

3741 delimiter=self._delimiter, 

3742 allow_duplicates=self._allow_completing_duplicates, 

3743 ) 

3744 if completer is not None 

3745 else None 

3746 ) 

3747 

3748 def widget( 

3749 self, 

3750 default: object | yuio.Missing, 

3751 input_description: str | None, 

3752 default_description: str | None, 

3753 /, 

3754 ) -> yuio.widget.Widget[C | yuio.Missing]: 

3755 completer = self.completer() 

3756 return _WidgetResultMapper( 

3757 self, 

3758 input_description, 

3759 default, 

3760 ( 

3761 yuio.widget.InputWithCompletion( 

3762 completer, 

3763 placeholder=default_description or "", 

3764 ) 

3765 if completer is not None 

3766 else yuio.widget.Input( 

3767 placeholder=default_description or "", 

3768 ) 

3769 ), 

3770 ) 

3771 

3772 def is_secret(self) -> bool: 

3773 return self._inner.is_secret() 

3774 

3775 def __repr__(self): 

3776 if self._inner_raw is not None: 

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

3778 else: 

3779 return self.__class__.__name__ 

3780 

3781 

3782class List(CollectionParser[list[T], T], _t.Generic[T]): 

3783 """List(inner: Parser[T], /, *, delimiter: str | None = None) 

3784 

3785 Parser for lists. 

3786 

3787 Will split a string by the given delimiter, and parse each item 

3788 using a subparser. 

3789 

3790 :param inner: 

3791 inner parser that will be used to parse list items. 

3792 :param delimiter: 

3793 delimiter that will be passed to :py:meth:`str.split`. 

3794 

3795 """ 

3796 

3797 if TYPE_CHECKING: 

3798 

3799 @_t.overload 

3800 def __new__( 

3801 cls, inner: Parser[T], /, *, delimiter: str | None = None 

3802 ) -> List[T]: ... 

3803 

3804 @_t.overload 

3805 def __new__(cls, /, *, delimiter: str | None = None) -> PartialParser: ... 

3806 

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

3808 

3809 def __init__( 

3810 self, 

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

3812 /, 

3813 *, 

3814 delimiter: str | None = None, 

3815 ): 

3816 super().__init__(inner, ty=list, ctor=list, delimiter=delimiter) 

3817 

3818 def to_json_schema( 

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

3820 ) -> yuio.json_schema.JsonSchemaType: 

3821 return yuio.json_schema.Array(self._inner.to_json_schema(ctx)) 

3822 

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

3824 assert self.assert_type(value) 

3825 return [self._inner.to_json_value(item) for item in value] 

3826 

3827 

3828class Set(CollectionParser[set[T], T], _t.Generic[T]): 

3829 """Set(inner: Parser[T], /, *, delimiter: str | None = None) 

3830 

3831 Parser for sets. 

3832 

3833 Will split a string by the given delimiter, and parse each item 

3834 using a subparser. 

3835 

3836 :param inner: 

3837 inner parser that will be used to parse set items. 

3838 :param delimiter: 

3839 delimiter that will be passed to :py:meth:`str.split`. 

3840 

3841 """ 

3842 

3843 if TYPE_CHECKING: 

3844 

3845 @_t.overload 

3846 def __new__( 

3847 cls, inner: Parser[T], /, *, delimiter: str | None = None 

3848 ) -> Set[T]: ... 

3849 

3850 @_t.overload 

3851 def __new__(cls, /, *, delimiter: str | None = None) -> PartialParser: ... 

3852 

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

3854 

3855 _allow_completing_duplicates = False 

3856 

3857 def __init__( 

3858 self, 

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

3860 /, 

3861 *, 

3862 delimiter: str | None = None, 

3863 ): 

3864 super().__init__(inner, ty=set, ctor=set, delimiter=delimiter) 

3865 

3866 def widget( 

3867 self, 

3868 default: object | yuio.Missing, 

3869 input_description: str | None, 

3870 default_description: str | None, 

3871 /, 

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

3873 options = self._inner.options() 

3874 if options and len(options) <= 25: 

3875 return yuio.widget.Map(yuio.widget.Multiselect(list(options)), set) 

3876 else: 

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

3878 

3879 def to_json_schema( 

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

3881 ) -> yuio.json_schema.JsonSchemaType: 

3882 return yuio.json_schema.Array( 

3883 self._inner.to_json_schema(ctx), unique_items=True 

3884 ) 

3885 

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

3887 assert self.assert_type(value) 

3888 return [self._inner.to_json_value(item) for item in value] 

3889 

3890 

3891class FrozenSet(CollectionParser[frozenset[T], T], _t.Generic[T]): 

3892 """FrozenSet(inner: Parser[T], /, *, delimiter: str | None = None) 

3893 

3894 Parser for frozen sets. 

3895 

3896 Will split a string by the given delimiter, and parse each item 

3897 using a subparser. 

3898 

3899 :param inner: 

3900 inner parser that will be used to parse set items. 

3901 :param delimiter: 

3902 delimiter that will be passed to :py:meth:`str.split`. 

3903 

3904 """ 

3905 

3906 if TYPE_CHECKING: 

3907 

3908 @_t.overload 

3909 def __new__( 

3910 cls, inner: Parser[T], /, *, delimiter: str | None = None 

3911 ) -> FrozenSet[T]: ... 

3912 

3913 @_t.overload 

3914 def __new__(cls, /, *, delimiter: str | None = None) -> PartialParser: ... 

3915 

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

3917 

3918 _allow_completing_duplicates = False 

3919 

3920 def __init__( 

3921 self, 

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

3923 /, 

3924 *, 

3925 delimiter: str | None = None, 

3926 ): 

3927 super().__init__(inner, ty=frozenset, ctor=frozenset, delimiter=delimiter) 

3928 

3929 def to_json_schema( 

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

3931 ) -> yuio.json_schema.JsonSchemaType: 

3932 return yuio.json_schema.Array( 

3933 self._inner.to_json_schema(ctx), unique_items=True 

3934 ) 

3935 

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

3937 assert self.assert_type(value) 

3938 return [self._inner.to_json_value(item) for item in value] 

3939 

3940 

3941class Dict(CollectionParser[dict[K, V], tuple[K, V]], _t.Generic[K, V]): 

3942 """Dict(key: Parser[K], value: Parser[V], /, *, delimiter: str | None = None, pair_delimiter: str = ":") 

3943 

3944 Parser for dicts. 

3945 

3946 Will split a string by the given delimiter, and parse each item 

3947 using a :py:class:`Tuple` parser. 

3948 

3949 :param key: 

3950 inner parser that will be used to parse dict keys. 

3951 :param value: 

3952 inner parser that will be used to parse dict values. 

3953 :param delimiter: 

3954 delimiter that will be passed to :py:meth:`str.split`. 

3955 :param pair_delimiter: 

3956 delimiter that will be used to split key-value elements. 

3957 

3958 """ 

3959 

3960 if TYPE_CHECKING: 

3961 

3962 @_t.overload 

3963 def __new__( 

3964 cls, 

3965 key: Parser[K], 

3966 value: Parser[V], 

3967 /, 

3968 *, 

3969 delimiter: str | None = None, 

3970 pair_delimiter: str = ":", 

3971 ) -> Dict[K, V]: ... 

3972 

3973 @_t.overload 

3974 def __new__( 

3975 cls, 

3976 /, 

3977 *, 

3978 delimiter: str | None = None, 

3979 pair_delimiter: str = ":", 

3980 ) -> PartialParser: ... 

3981 

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

3983 

3984 _allow_completing_duplicates = False 

3985 

3986 def __init__( 

3987 self, 

3988 key: Parser[K] | None = None, 

3989 value: Parser[V] | None = None, 

3990 /, 

3991 *, 

3992 delimiter: str | None = None, 

3993 pair_delimiter: str = ":", 

3994 ): 

3995 self._pair_delimiter = pair_delimiter 

3996 super().__init__( 

3997 ( 

3998 _DictElementParser(key, value, delimiter=pair_delimiter) 

3999 if key and value 

4000 else None 

4001 ), 

4002 ty=dict, 

4003 ctor=dict, 

4004 iter=dict.items, 

4005 config_type=(dict, list), 

4006 config_type_iter=self.__config_type_iter, 

4007 delimiter=delimiter, 

4008 ) 

4009 

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

4011 result = super().wrap(parser) 

4012 result._inner._delimiter = self._pair_delimiter # pyright: ignore[reportAttributeAccessIssue] 

4013 return result 

4014 

4015 @staticmethod 

4016 def __config_type_iter(x) -> _t.Iterator[tuple[K, V]]: 

4017 if isinstance(x, dict): 

4018 return iter(x.items()) 

4019 else: 

4020 return iter(x) 

4021 

4022 def to_json_schema( 

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

4024 ) -> yuio.json_schema.JsonSchemaType: 

4025 key_schema = self._inner._inner[0].to_json_schema(ctx) # type: ignore 

4026 value_schema = self._inner._inner[1].to_json_schema(ctx) # type: ignore 

4027 return yuio.json_schema.Dict(key_schema, value_schema) 

4028 

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

4030 assert self.assert_type(value) 

4031 items = _t.cast( 

4032 list[tuple[yuio.json_schema.JsonValue, yuio.json_schema.JsonValue]], 

4033 [self._inner.to_json_value(item) for item in value.items()], 

4034 ) 

4035 

4036 if all(isinstance(k, str) for k, _ in items): 

4037 return dict(_t.cast(list[tuple[str, yuio.json_schema.JsonValue]], items)) 

4038 else: 

4039 return items 

4040 

4041 

4042class Tuple( 

4043 WrappingParser[TU, tuple[Parser[object], ...]], 

4044 ValueParser[TU], 

4045 PartialParser, 

4046 _t.Generic[TU], 

4047): 

4048 """Tuple(*parsers: Parser[...], delimiter: str | None = None) 

4049 

4050 Parser for tuples of fixed lengths. 

4051 

4052 :param parsers: 

4053 parsers for each tuple element. 

4054 :param delimiter: 

4055 delimiter that will be passed to :py:meth:`str.split`. 

4056 

4057 """ 

4058 

4059 # See the links below for an explanation of shy this is so ugly: 

4060 # https://github.com/python/typing/discussions/1450 

4061 # https://github.com/python/typing/issues/1216 

4062 if TYPE_CHECKING: 

4063 T1 = _t.TypeVar("T1") 

4064 T2 = _t.TypeVar("T2") 

4065 T3 = _t.TypeVar("T3") 

4066 T4 = _t.TypeVar("T4") 

4067 T5 = _t.TypeVar("T5") 

4068 T6 = _t.TypeVar("T6") 

4069 T7 = _t.TypeVar("T7") 

4070 T8 = _t.TypeVar("T8") 

4071 T9 = _t.TypeVar("T9") 

4072 T10 = _t.TypeVar("T10") 

4073 

4074 @_t.overload 

4075 def __new__( 

4076 cls, 

4077 /, 

4078 *, 

4079 delimiter: str | None = None, 

4080 ) -> PartialParser: ... 

4081 

4082 @_t.overload 

4083 def __new__( 

4084 cls, 

4085 p1: Parser[T1], 

4086 /, 

4087 *, 

4088 delimiter: str | None = None, 

4089 ) -> Tuple[tuple[T1]]: ... 

4090 

4091 @_t.overload 

4092 def __new__( 

4093 cls, 

4094 p1: Parser[T1], 

4095 p2: Parser[T2], 

4096 /, 

4097 *, 

4098 delimiter: str | None = None, 

4099 ) -> Tuple[tuple[T1, T2]]: ... 

4100 

4101 @_t.overload 

4102 def __new__( 

4103 cls, 

4104 p1: Parser[T1], 

4105 p2: Parser[T2], 

4106 p3: Parser[T3], 

4107 /, 

4108 *, 

4109 delimiter: str | None = None, 

4110 ) -> Tuple[tuple[T1, T2, T3]]: ... 

4111 

4112 @_t.overload 

4113 def __new__( 

4114 cls, 

4115 p1: Parser[T1], 

4116 p2: Parser[T2], 

4117 p3: Parser[T3], 

4118 p4: Parser[T4], 

4119 /, 

4120 *, 

4121 delimiter: str | None = None, 

4122 ) -> Tuple[tuple[T1, T2, T3, T4]]: ... 

4123 

4124 @_t.overload 

4125 def __new__( 

4126 cls, 

4127 p1: Parser[T1], 

4128 p2: Parser[T2], 

4129 p3: Parser[T3], 

4130 p4: Parser[T4], 

4131 p5: Parser[T5], 

4132 /, 

4133 *, 

4134 delimiter: str | None = None, 

4135 ) -> Tuple[tuple[T1, T2, T3, T4, T5]]: ... 

4136 

4137 @_t.overload 

4138 def __new__( 

4139 cls, 

4140 p1: Parser[T1], 

4141 p2: Parser[T2], 

4142 p3: Parser[T3], 

4143 p4: Parser[T4], 

4144 p5: Parser[T5], 

4145 p6: Parser[T6], 

4146 /, 

4147 *, 

4148 delimiter: str | None = None, 

4149 ) -> Tuple[tuple[T1, T2, T3, T4, T5, T6]]: ... 

4150 

4151 @_t.overload 

4152 def __new__( 

4153 cls, 

4154 p1: Parser[T1], 

4155 p2: Parser[T2], 

4156 p3: Parser[T3], 

4157 p4: Parser[T4], 

4158 p5: Parser[T5], 

4159 p6: Parser[T6], 

4160 p7: Parser[T7], 

4161 /, 

4162 *, 

4163 delimiter: str | None = None, 

4164 ) -> Tuple[tuple[T1, T2, T3, T4, T5, T6, T7]]: ... 

4165 

4166 @_t.overload 

4167 def __new__( 

4168 cls, 

4169 p1: Parser[T1], 

4170 p2: Parser[T2], 

4171 p3: Parser[T3], 

4172 p4: Parser[T4], 

4173 p5: Parser[T5], 

4174 p6: Parser[T6], 

4175 p7: Parser[T7], 

4176 p8: Parser[T8], 

4177 /, 

4178 *, 

4179 delimiter: str | None = None, 

4180 ) -> Tuple[tuple[T1, T2, T3, T4, T5, T6, T7, T8]]: ... 

4181 

4182 @_t.overload 

4183 def __new__( 

4184 cls, 

4185 p1: Parser[T1], 

4186 p2: Parser[T2], 

4187 p3: Parser[T3], 

4188 p4: Parser[T4], 

4189 p5: Parser[T5], 

4190 p6: Parser[T6], 

4191 p7: Parser[T7], 

4192 p8: Parser[T8], 

4193 p9: Parser[T9], 

4194 /, 

4195 *, 

4196 delimiter: str | None = None, 

4197 ) -> Tuple[tuple[T1, T2, T3, T4, T5, T6, T7, T8, T9]]: ... 

4198 

4199 @_t.overload 

4200 def __new__( 

4201 cls, 

4202 p1: Parser[T1], 

4203 p2: Parser[T2], 

4204 p3: Parser[T3], 

4205 p4: Parser[T4], 

4206 p5: Parser[T5], 

4207 p6: Parser[T6], 

4208 p7: Parser[T7], 

4209 p8: Parser[T8], 

4210 p9: Parser[T9], 

4211 p10: Parser[T10], 

4212 /, 

4213 *, 

4214 delimiter: str | None = None, 

4215 ) -> Tuple[tuple[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10]]: ... 

4216 

4217 @_t.overload 

4218 def __new__( 

4219 cls, 

4220 p1: Parser[T1], 

4221 p2: Parser[T2], 

4222 p3: Parser[T3], 

4223 p4: Parser[T4], 

4224 p5: Parser[T5], 

4225 p6: Parser[T6], 

4226 p7: Parser[T7], 

4227 p8: Parser[T8], 

4228 p9: Parser[T9], 

4229 p10: Parser[T10], 

4230 p11: Parser[object], 

4231 *tail: Parser[object], 

4232 delimiter: str | None = None, 

4233 ) -> Tuple[tuple[_t.Any, ...]]: ... 

4234 

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

4236 

4237 def __init__( 

4238 self, 

4239 *parsers: Parser[_t.Any], 

4240 delimiter: str | None = None, 

4241 ): 

4242 if delimiter == "": 

4243 raise ValueError("empty delimiter") 

4244 self._delimiter = delimiter 

4245 super().__init__(parsers or None, tuple) 

4246 

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

4248 result = super().wrap(parser) 

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

4250 return result 

4251 

4252 def parse_with_ctx(self, ctx: StrParsingContext, /) -> TU: 

4253 items = list(ctx.split(self._delimiter, maxsplit=len(self._inner) - 1)) 

4254 

4255 if len(items) != len(self._inner): 

4256 raise ParsingError( 

4257 "Expected %s element%s, got %s: `%r`", 

4258 len(self._inner), 

4259 "" if len(self._inner) == 1 else "s", 

4260 len(items), 

4261 ctx.value, 

4262 ctx=ctx, 

4263 ) 

4264 

4265 return _t.cast( 

4266 TU, 

4267 tuple( 

4268 parser.parse_with_ctx(item) for parser, item in zip(self._inner, items) 

4269 ), 

4270 ) 

4271 

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

4273 if len(ctxs) != len(self._inner): 

4274 raise ParsingError( 

4275 "Expected %s element%s, got %s: `%r`", 

4276 len(self._inner), 

4277 "" if len(self._inner) == 1 else "s", 

4278 len(ctxs), 

4279 ctxs, 

4280 ) 

4281 

4282 return _t.cast( 

4283 TU, 

4284 tuple( 

4285 parser.parse_with_ctx(item) for parser, item in zip(self._inner, ctxs) 

4286 ), 

4287 ) 

4288 

4289 def parse_config_with_ctx(self, ctx: ConfigParsingContext, /) -> TU: 

4290 value = ctx.value 

4291 if not isinstance(value, (list, tuple)): 

4292 raise ParsingError.type_mismatch(value, list, tuple, ctx=ctx) 

4293 elif len(value) != len(self._inner): 

4294 raise ParsingError( 

4295 "Expected %s element%s, got %s: `%r`", 

4296 len(self._inner), 

4297 "" if len(self._inner) == 1 else "s", 

4298 len(value), 

4299 value, 

4300 ) 

4301 

4302 return _t.cast( 

4303 TU, 

4304 tuple( 

4305 parser.parse_config_with_ctx(ctx.descend(item, i)) 

4306 for i, (parser, item) in enumerate(zip(self._inner, value)) 

4307 ), 

4308 ) 

4309 

4310 def supports_parse_many(self) -> bool: 

4311 return True 

4312 

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

4314 return len(self._inner) 

4315 

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

4317 delimiter = self._delimiter or " " 

4318 desc = [parser.describe_or_def() for parser in self._inner] 

4319 return delimiter.join(desc) 

4320 

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

4322 return tuple(parser.describe_or_def() for parser in self._inner) 

4323 

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

4325 assert self.assert_type(value) 

4326 

4327 delimiter = self._delimiter or " " 

4328 desc = [parser.describe_value(item) for parser, item in zip(self._inner, value)] 

4329 

4330 return delimiter.join(desc) 

4331 

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

4333 return None 

4334 

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

4336 return yuio.complete.Tuple( 

4337 *[parser.completer() or yuio.complete.Empty() for parser in self._inner], 

4338 delimiter=self._delimiter, 

4339 ) 

4340 

4341 def widget( 

4342 self, 

4343 default: object | yuio.Missing, 

4344 input_description: str | None, 

4345 default_description: str | None, 

4346 /, 

4347 ) -> yuio.widget.Widget[TU | yuio.Missing]: 

4348 completer = self.completer() 

4349 

4350 return _WidgetResultMapper( 

4351 self, 

4352 input_description, 

4353 default, 

4354 ( 

4355 yuio.widget.InputWithCompletion( 

4356 completer, 

4357 placeholder=default_description or "", 

4358 ) 

4359 if completer is not None 

4360 else yuio.widget.Input( 

4361 placeholder=default_description or "", 

4362 ) 

4363 ), 

4364 ) 

4365 

4366 def to_json_schema( 

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

4368 ) -> yuio.json_schema.JsonSchemaType: 

4369 return yuio.json_schema.Tuple( 

4370 [parser.to_json_schema(ctx) for parser in self._inner] 

4371 ) 

4372 

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

4374 assert self.assert_type(value) 

4375 return [parser.to_json_value(item) for parser, item in zip(self._inner, value)] 

4376 

4377 def is_secret(self) -> bool: 

4378 return any(parser.is_secret() for parser in self._inner) 

4379 

4380 def __repr__(self): 

4381 if self._inner_raw is not None: 

4382 return f"{self.__class__.__name__}{self._inner_raw!r}" 

4383 else: 

4384 return self.__class__.__name__ 

4385 

4386 

4387class _DictElementParser(Tuple[tuple[K, V]], _t.Generic[K, V]): 

4388 def __init__(self, k: Parser[K], v: Parser[V], delimiter: str | None = None): 

4389 super().__init__(k, v, delimiter=delimiter) 

4390 

4391 # def parse_with_ctx(self, ctx: StrParsingContext, /) -> tuple[K, V]: 

4392 # items = list(ctx.split(self._delimiter, maxsplit=len(self._inner) - 1)) 

4393 

4394 # if len(items) != len(self._inner): 

4395 # raise ParsingError("Expected key-value pair, got `%r`", ctx.value) 

4396 

4397 # return _t.cast( 

4398 # tuple[K, V], 

4399 # tuple(parser.parse_with_ctx(item) for parser, item in zip(self._inner, items)), 

4400 # ) 

4401 

4402 # def parse_many_with_ctx(self, ctxs: _t.Sequence[StrParsingContext], /) -> tuple[K, V]: 

4403 # if len(value) != len(self._inner): 

4404 # with describe_context("element #%(key)r"): 

4405 # raise ParsingError( 

4406 # "Expected key-value pair, got `%r`", 

4407 # value, 

4408 # ) 

4409 

4410 # k = describe_context("key of element #%(key)r", self._inner[0].parse, value[0]) 

4411 # v = replace_context(k, self._inner[1].parse, value[1]) 

4412 

4413 # return _t.cast(tuple[K, V], (k, v)) 

4414 

4415 # def parse_config_with_ctx(self, ctx: ConfigParsingContext, /) -> tuple[K, V]: 

4416 # if not isinstance(value, (list, tuple)): 

4417 # with describe_context("element #%(key)r"): 

4418 # raise ParsingError.type_mismatch(value, list, tuple) 

4419 # elif len(value) != len(self._inner): 

4420 # with describe_context("element #%(key)r"): 

4421 # raise ParsingError( 

4422 # "Expected key-value pair, got `%r`", 

4423 # value, 

4424 # ) 

4425 

4426 # k = describe_context( 

4427 # "key of element #%(key)r", self._inner[0].parse_config_with_ctx, value[0] 

4428 # ) 

4429 # v = replace_context(k, self._inner[1].parse_config_with_ctx, value[1]) 

4430 

4431 # return _t.cast(tuple[K, V], (k, v)) 

4432 

4433 

4434class Optional(MappingParser[T | None, T], _t.Generic[T]): 

4435 """Optional(inner: Parser[T], /) 

4436 

4437 Parser for optional values. 

4438 

4439 Allows handling :data:`None`\\ s when parsing config. Does not change how strings 

4440 are parsed, though. 

4441 

4442 :param inner: 

4443 a parser used to extract and validate contents of an optional. 

4444 

4445 """ 

4446 

4447 if TYPE_CHECKING: 

4448 

4449 @_t.overload 

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

4451 

4452 @_t.overload 

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

4454 

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

4456 

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

4458 super().__init__(inner) 

4459 

4460 def parse_with_ctx(self, ctx: StrParsingContext, /) -> T | None: 

4461 return self._inner.parse_with_ctx(ctx) 

4462 

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

4464 return self._inner.parse_many_with_ctx(ctxs) 

4465 

4466 def parse_config_with_ctx(self, ctx: ConfigParsingContext, /) -> T | None: 

4467 if ctx.value is None: 

4468 return None 

4469 return self._inner.parse_config_with_ctx(ctx) 

4470 

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

4472 return True 

4473 

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

4475 if value is None: 

4476 return "<none>" 

4477 return self._inner.describe_value(value) 

4478 

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

4480 return self._inner.options() 

4481 

4482 def widget( 

4483 self, 

4484 default: object | yuio.Missing, 

4485 input_description: str | None, 

4486 default_description: str | None, 

4487 /, 

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

4489 return self._inner.widget(default, input_description, default_description) 

4490 

4491 def to_json_schema( 

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

4493 ) -> yuio.json_schema.JsonSchemaType: 

4494 return yuio.json_schema.OneOf( 

4495 [self._inner.to_json_schema(ctx), yuio.json_schema.Null()] 

4496 ) 

4497 

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

4499 if value is None: 

4500 return None 

4501 else: 

4502 return self._inner.to_json_value(value) 

4503 

4504 

4505class Union(WrappingParser[T, tuple[Parser[T], ...]], ValueParser[T], _t.Generic[T]): 

4506 """Union(*parsers: Parser[T]) 

4507 

4508 Tries several parsers and returns the first successful result. 

4509 

4510 .. warning:: 

4511 

4512 Order of parsers matters. Since parsers are tried in the same order as they're 

4513 given, make sure to put parsers that are likely to succeed at the end. 

4514 

4515 For example, this parser will always return a string because :class:`Str` 

4516 can't fail:: 

4517 

4518 >>> parser = Union(Str(), Int()) # Always returns a string! 

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

4520 '10' 

4521 

4522 To fix this, put :class:`Str` at the end so that :class:`Int` is tried first:: 

4523 

4524 >>> parser = Union(Int(), Str()) 

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

4526 10 

4527 >>> parser.parse("not an int") 

4528 'not an int' 

4529 

4530 """ 

4531 

4532 # See the links below for an explanation of shy this is so ugly: 

4533 # https://github.com/python/typing/discussions/1450 

4534 # https://github.com/python/typing/issues/1216 

4535 if TYPE_CHECKING: 

4536 T1 = _t.TypeVar("T1") 

4537 T2 = _t.TypeVar("T2") 

4538 T3 = _t.TypeVar("T3") 

4539 T4 = _t.TypeVar("T4") 

4540 T5 = _t.TypeVar("T5") 

4541 T6 = _t.TypeVar("T6") 

4542 T7 = _t.TypeVar("T7") 

4543 T8 = _t.TypeVar("T8") 

4544 T9 = _t.TypeVar("T9") 

4545 T10 = _t.TypeVar("T10") 

4546 

4547 @_t.overload 

4548 def __new__( 

4549 cls, 

4550 /, 

4551 ) -> PartialParser: ... 

4552 

4553 @_t.overload 

4554 def __new__( 

4555 cls, 

4556 p1: Parser[T1], 

4557 /, 

4558 ) -> Union[T1]: ... 

4559 

4560 @_t.overload 

4561 def __new__( 

4562 cls, 

4563 p1: Parser[T1], 

4564 p2: Parser[T2], 

4565 /, 

4566 ) -> Union[T1 | T2]: ... 

4567 

4568 @_t.overload 

4569 def __new__( 

4570 cls, 

4571 p1: Parser[T1], 

4572 p2: Parser[T2], 

4573 p3: Parser[T3], 

4574 /, 

4575 ) -> Union[T1 | T2 | T3]: ... 

4576 

4577 @_t.overload 

4578 def __new__( 

4579 cls, 

4580 p1: Parser[T1], 

4581 p2: Parser[T2], 

4582 p3: Parser[T3], 

4583 p4: Parser[T4], 

4584 /, 

4585 ) -> Union[T1 | T2 | T3 | T4]: ... 

4586 

4587 @_t.overload 

4588 def __new__( 

4589 cls, 

4590 p1: Parser[T1], 

4591 p2: Parser[T2], 

4592 p3: Parser[T3], 

4593 p4: Parser[T4], 

4594 p5: Parser[T5], 

4595 /, 

4596 ) -> Union[T1 | T2 | T3 | T4 | T5]: ... 

4597 

4598 @_t.overload 

4599 def __new__( 

4600 cls, 

4601 p1: Parser[T1], 

4602 p2: Parser[T2], 

4603 p3: Parser[T3], 

4604 p4: Parser[T4], 

4605 p5: Parser[T5], 

4606 p6: Parser[T6], 

4607 /, 

4608 ) -> Union[T1 | T2 | T3 | T4 | T5 | T6]: ... 

4609 

4610 @_t.overload 

4611 def __new__( 

4612 cls, 

4613 p1: Parser[T1], 

4614 p2: Parser[T2], 

4615 p3: Parser[T3], 

4616 p4: Parser[T4], 

4617 p5: Parser[T5], 

4618 p6: Parser[T6], 

4619 p7: Parser[T7], 

4620 /, 

4621 ) -> Union[T1 | T2 | T3 | T4 | T5 | T6 | T7]: ... 

4622 

4623 @_t.overload 

4624 def __new__( 

4625 cls, 

4626 p1: Parser[T1], 

4627 p2: Parser[T2], 

4628 p3: Parser[T3], 

4629 p4: Parser[T4], 

4630 p5: Parser[T5], 

4631 p6: Parser[T6], 

4632 p7: Parser[T7], 

4633 p8: Parser[T8], 

4634 /, 

4635 ) -> Union[T1 | T2 | T3 | T4 | T5 | T6 | T7 | T8]: ... 

4636 

4637 @_t.overload 

4638 def __new__( 

4639 cls, 

4640 p1: Parser[T1], 

4641 p2: Parser[T2], 

4642 p3: Parser[T3], 

4643 p4: Parser[T4], 

4644 p5: Parser[T5], 

4645 p6: Parser[T6], 

4646 p7: Parser[T7], 

4647 p8: Parser[T8], 

4648 p9: Parser[T9], 

4649 /, 

4650 ) -> Union[T1 | T2 | T3 | T4 | T5 | T6 | T7 | T8 | T9]: ... 

4651 

4652 @_t.overload 

4653 def __new__( 

4654 cls, 

4655 p1: Parser[T1], 

4656 p2: Parser[T2], 

4657 p3: Parser[T3], 

4658 p4: Parser[T4], 

4659 p5: Parser[T5], 

4660 p6: Parser[T6], 

4661 p7: Parser[T7], 

4662 p8: Parser[T8], 

4663 p9: Parser[T9], 

4664 p10: Parser[T10], 

4665 /, 

4666 ) -> Union[T1 | T2 | T3 | T4 | T5 | T6 | T7 | T8 | T9 | T10]: ... 

4667 

4668 @_t.overload 

4669 def __new__( 

4670 cls, 

4671 p1: Parser[T1], 

4672 p2: Parser[T2], 

4673 p3: Parser[T3], 

4674 p4: Parser[T4], 

4675 p5: Parser[T5], 

4676 p6: Parser[T6], 

4677 p7: Parser[T7], 

4678 p8: Parser[T8], 

4679 p9: Parser[T9], 

4680 p10: Parser[T10], 

4681 p11: Parser[object], 

4682 *parsers: Parser[object], 

4683 ) -> Union[object]: ... 

4684 

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

4686 

4687 def __init__(self, *parsers: Parser[_t.Any]): 

4688 super().__init__(parsers or None, object) 

4689 

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

4691 result = super().wrap(parser) 

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

4693 return result 

4694 

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

4696 errors: list[tuple[Parser[object], ParsingError]] = [] 

4697 for parser in self._inner: 

4698 try: 

4699 return parser.parse_with_ctx(ctx) 

4700 except ParsingError as e: 

4701 errors.append((parser, e)) 

4702 raise self._make_error(errors, ctx) 

4703 

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

4705 errors: list[tuple[Parser[object], ParsingError]] = [] 

4706 for parser in self._inner: 

4707 try: 

4708 return parser.parse_config_with_ctx(ctx) 

4709 except ParsingError as e: 

4710 errors.append((parser, e)) 

4711 raise self._make_error(errors, ctx) 

4712 

4713 def _make_error( 

4714 self, 

4715 errors: list[tuple[Parser[object], ParsingError]], 

4716 ctx: StrParsingContext | ConfigParsingContext, 

4717 ): 

4718 msgs = [] 

4719 for parser, error in errors: 

4720 error.raw = None 

4721 error.pos = None 

4722 msgs.append( 

4723 yuio.string.Format( 

4724 " Trying as `%s`:\n%s", 

4725 parser.describe_or_def(), 

4726 yuio.string.Indent(error, indent=4), 

4727 ) 

4728 ) 

4729 return ParsingError( 

4730 "Can't parse `%r`:\n%s", ctx.value, yuio.string.Stack(*msgs), ctx=ctx 

4731 ) 

4732 

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

4734 return True 

4735 

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

4737 if len(self._inner) > 1: 

4738 

4739 def strip_curly_brackets(desc: str): 

4740 if desc.startswith("{") and desc.endswith("}") and "|" in desc: 

4741 s = desc[1:-1] 

4742 if "{" not in s and "}" not in s: 

4743 return s 

4744 return desc 

4745 

4746 desc = "|".join( 

4747 strip_curly_brackets(parser.describe_or_def()) for parser in self._inner 

4748 ) 

4749 desc = f"{{{desc}}}" 

4750 else: 

4751 desc = "|".join(parser.describe_or_def() for parser in self._inner) 

4752 return desc 

4753 

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

4755 for parser in self._inner: 

4756 try: 

4757 return parser.describe_value(value) 

4758 except TypeError: 

4759 pass 

4760 

4761 raise TypeError( 

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

4763 ) 

4764 

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

4766 result = [] 

4767 got_options = False 

4768 for parser in self._inner: 

4769 if options := parser.options(): 

4770 result.extend(options) 

4771 got_options = True 

4772 if got_options: 

4773 return result 

4774 else: 

4775 return None 

4776 

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

4778 completers = [] 

4779 for parser in self._inner: 

4780 if completer := parser.completer(): 

4781 completers.append((parser.describe(), completer)) 

4782 if not completers: 

4783 return None 

4784 elif len(completers) == 1: 

4785 return completers[0][1] 

4786 else: 

4787 return yuio.complete.Alternative(completers) 

4788 

4789 def widget( 

4790 self, 

4791 default: object | yuio.Missing, 

4792 input_description: str | None, 

4793 default_description: str | None, 

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

4795 options = [] 

4796 for parser in self._inner: 

4797 parser_options = parser.options() 

4798 if parser_options is None: 

4799 options = None 

4800 break 

4801 options.extend(parser_options) 

4802 

4803 if not options: 

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

4805 

4806 if default is yuio.MISSING: 

4807 default_index = 0 

4808 else: 

4809 for i, option in enumerate(options): 

4810 if option.value == default: 

4811 default_index = i 

4812 break 

4813 else: 

4814 options.insert( 

4815 0, 

4816 yuio.widget.Option( 

4817 yuio.MISSING, default_description or str(default) 

4818 ), 

4819 ) 

4820 default_index = 0 

4821 

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

4823 

4824 def to_json_schema( 

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

4826 ) -> yuio.json_schema.JsonSchemaType: 

4827 return yuio.json_schema.OneOf( 

4828 [parser.to_json_schema(ctx) for parser in self._inner] 

4829 ) 

4830 

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

4832 for parser in self._inner: 

4833 try: 

4834 return parser.to_json_value(value) 

4835 except TypeError: 

4836 pass 

4837 

4838 raise TypeError( 

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

4840 ) 

4841 

4842 def is_secret(self) -> bool: 

4843 return any(parser.is_secret() for parser in self._inner) 

4844 

4845 def __repr__(self): 

4846 return f"{self.__class__.__name__}{self._inner_raw!r}" 

4847 

4848 

4849class _BoundImpl(ValidatingParser[T], _t.Generic[T, Cmp]): 

4850 def __init__( 

4851 self, 

4852 inner: Parser[T] | None, 

4853 /, 

4854 *, 

4855 lower: Cmp | None = None, 

4856 lower_inclusive: Cmp | None = None, 

4857 upper: Cmp | None = None, 

4858 upper_inclusive: Cmp | None = None, 

4859 mapper: _t.Callable[[T], Cmp], 

4860 desc: str, 

4861 ): 

4862 super().__init__(inner) 

4863 

4864 self._lower_bound: Cmp | None = None 

4865 self._lower_bound_is_inclusive: bool = True 

4866 self._upper_bound: Cmp | None = None 

4867 self._upper_bound_is_inclusive: bool = True 

4868 

4869 if lower is not None and lower_inclusive is not None: 

4870 raise TypeError( 

4871 "lower and lower_inclusive cannot be given at the same time" 

4872 ) 

4873 elif lower is not None: 

4874 self._lower_bound = lower 

4875 self._lower_bound_is_inclusive = False 

4876 elif lower_inclusive is not None: 

4877 self._lower_bound = lower_inclusive 

4878 self._lower_bound_is_inclusive = True 

4879 

4880 if upper is not None and upper_inclusive is not None: 

4881 raise TypeError( 

4882 "upper and upper_inclusive cannot be given at the same time" 

4883 ) 

4884 elif upper is not None: 

4885 self._upper_bound = upper 

4886 self._upper_bound_is_inclusive = False 

4887 elif upper_inclusive is not None: 

4888 self._upper_bound = upper_inclusive 

4889 self._upper_bound_is_inclusive = True 

4890 

4891 self.__mapper = mapper 

4892 self.__desc = desc 

4893 

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

4895 mapped = self.__mapper(value) 

4896 

4897 if self._lower_bound is not None: 

4898 if self._lower_bound_is_inclusive and mapped < self._lower_bound: 

4899 raise ParsingError( 

4900 "%s should be greater than or equal to `%s`: `%r`", 

4901 self.__desc, 

4902 self._lower_bound, 

4903 value, 

4904 ) 

4905 elif not self._lower_bound_is_inclusive and not self._lower_bound < mapped: 

4906 raise ParsingError( 

4907 "%s should be greater than `%s`: `%r`", 

4908 self.__desc, 

4909 self._lower_bound, 

4910 value, 

4911 ) 

4912 

4913 if self._upper_bound is not None: 

4914 if self._upper_bound_is_inclusive and self._upper_bound < mapped: 

4915 raise ParsingError( 

4916 "%s should be lesser than or equal to `%s`: `%r`", 

4917 self.__desc, 

4918 self._upper_bound, 

4919 value, 

4920 ) 

4921 elif not self._upper_bound_is_inclusive and not mapped < self._upper_bound: 

4922 raise ParsingError( 

4923 "%s should be lesser than `%s`: `%r`", 

4924 self.__desc, 

4925 self._upper_bound, 

4926 value, 

4927 ) 

4928 

4929 def __repr__(self): 

4930 desc = "" 

4931 if self._lower_bound is not None: 

4932 desc += repr(self._lower_bound) 

4933 desc += " <= " if self._lower_bound_is_inclusive else " < " 

4934 mapper_name = getattr(self.__mapper, "__name__") 

4935 if mapper_name and mapper_name != "<lambda>": 

4936 desc += mapper_name 

4937 else: 

4938 desc += "x" 

4939 if self._upper_bound is not None: 

4940 desc += " <= " if self._upper_bound_is_inclusive else " < " 

4941 desc += repr(self._upper_bound) 

4942 return f"{self.__class__.__name__}({self.__wrapped_parser__!r}, {desc})" 

4943 

4944 

4945class Bound(_BoundImpl[Cmp, Cmp], _t.Generic[Cmp]): 

4946 """Bound(inner: Parser[Cmp], /, *, lower: Cmp | None = None, lower_inclusive: Cmp | None = None, upper: Cmp | None = None, upper_inclusive: Cmp | None = None) 

4947 

4948 Check that value is upper- or lower-bound by some constraints. 

4949 

4950 :param inner: 

4951 parser whose result will be validated. 

4952 :param lower: 

4953 set lower bound for value, so we require that ``value > lower``. 

4954 Can't be given if `lower_inclusive` is also given. 

4955 :param lower_inclusive: 

4956 set lower bound for value, so we require that ``value >= lower``. 

4957 Can't be given if `lower` is also given. 

4958 :param upper: 

4959 set upper bound for value, so we require that ``value < upper``. 

4960 Can't be given if `upper_inclusive` is also given. 

4961 :param upper_inclusive: 

4962 set upper bound for value, so we require that ``value <= upper``. 

4963 Can't be given if `upper` is also given. 

4964 :example: 

4965 :: 

4966 

4967 >>> # Int in range `0 < x <= 1`: 

4968 >>> Bound(Int(), lower=0, upper_inclusive=1) 

4969 Bound(Int, 0 < x <= 1) 

4970 

4971 """ 

4972 

4973 if TYPE_CHECKING: 

4974 

4975 @_t.overload 

4976 def __new__( 

4977 cls, 

4978 inner: Parser[Cmp], 

4979 /, 

4980 *, 

4981 lower: Cmp | None = None, 

4982 lower_inclusive: Cmp | None = None, 

4983 upper: Cmp | None = None, 

4984 upper_inclusive: Cmp | None = None, 

4985 ) -> Bound[Cmp]: ... 

4986 

4987 @_t.overload 

4988 def __new__( 

4989 cls, 

4990 *, 

4991 lower: Cmp | None = None, 

4992 lower_inclusive: Cmp | None = None, 

4993 upper: Cmp | None = None, 

4994 upper_inclusive: Cmp | None = None, 

4995 ) -> PartialParser: ... 

4996 

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

4998 

4999 def __init__( 

5000 self, 

5001 inner: Parser[Cmp] | None = None, 

5002 /, 

5003 *, 

5004 lower: Cmp | None = None, 

5005 lower_inclusive: Cmp | None = None, 

5006 upper: Cmp | None = None, 

5007 upper_inclusive: Cmp | None = None, 

5008 ): 

5009 super().__init__( 

5010 inner, 

5011 lower=lower, 

5012 lower_inclusive=lower_inclusive, 

5013 upper=upper, 

5014 upper_inclusive=upper_inclusive, 

5015 mapper=lambda x: x, 

5016 desc="Value", 

5017 ) 

5018 

5019 def to_json_schema( 

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

5021 ) -> yuio.json_schema.JsonSchemaType: 

5022 bound = {} 

5023 if isinstance(self._lower_bound, (int, float)): 

5024 bound[ 

5025 "minimum" if self._lower_bound_is_inclusive else "exclusiveMinimum" 

5026 ] = self._lower_bound 

5027 if isinstance(self._upper_bound, (int, float)): 

5028 bound[ 

5029 "maximum" if self._upper_bound_is_inclusive else "exclusiveMaximum" 

5030 ] = self._upper_bound 

5031 if bound: 

5032 return yuio.json_schema.AllOf( 

5033 [super().to_json_schema(ctx), yuio.json_schema.Opaque(bound)] 

5034 ) 

5035 else: 

5036 return super().to_json_schema(ctx) 

5037 

5038 

5039@_t.overload 

5040def Gt(inner: Parser[Cmp], bound: Cmp, /) -> Bound[Cmp]: ... 

5041@_t.overload 

5042def Gt(bound: _tx.SupportsLt[_t.Any], /) -> PartialParser: ... 

5043def Gt(*args) -> _t.Any: 

5044 """Gt(inner: Parser[Cmp], bound: Cmp, /) 

5045 

5046 Alias for :class:`Bound`. 

5047 

5048 :param inner: 

5049 parser whose result will be validated. 

5050 :param bound: 

5051 lower bound for parsed values. 

5052 

5053 """ 

5054 

5055 if len(args) == 1: 

5056 return Bound(lower=args[0]) 

5057 elif len(args) == 2: 

5058 return Bound(args[0], lower=args[1]) 

5059 else: 

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

5061 

5062 

5063@_t.overload 

5064def Ge(inner: Parser[Cmp], bound: Cmp, /) -> Bound[Cmp]: ... 

5065@_t.overload 

5066def Ge(bound: _tx.SupportsLt[_t.Any], /) -> PartialParser: ... 

5067def Ge(*args) -> _t.Any: 

5068 """Ge(inner: Parser[Cmp], bound: Cmp, /) 

5069 

5070 Alias for :class:`Bound`. 

5071 

5072 :param inner: 

5073 parser whose result will be validated. 

5074 :param bound: 

5075 lower inclusive bound for parsed values. 

5076 

5077 """ 

5078 

5079 if len(args) == 1: 

5080 return Bound(lower_inclusive=args[0]) 

5081 elif len(args) == 2: 

5082 return Bound(args[0], lower_inclusive=args[1]) 

5083 else: 

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

5085 

5086 

5087@_t.overload 

5088def Lt(inner: Parser[Cmp], bound: Cmp, /) -> Bound[Cmp]: ... 

5089@_t.overload 

5090def Lt(bound: _tx.SupportsLt[_t.Any], /) -> PartialParser: ... 

5091def Lt(*args) -> _t.Any: 

5092 """Lt(inner: Parser[Cmp], bound: Cmp, /) 

5093 

5094 Alias for :class:`Bound`. 

5095 

5096 :param inner: 

5097 parser whose result will be validated. 

5098 :param bound: 

5099 upper bound for parsed values. 

5100 

5101 """ 

5102 

5103 if len(args) == 1: 

5104 return Bound(upper=args[0]) 

5105 elif len(args) == 2: 

5106 return Bound(args[0], upper=args[1]) 

5107 else: 

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

5109 

5110 

5111@_t.overload 

5112def Le(inner: Parser[Cmp], bound: Cmp, /) -> Bound[Cmp]: ... 

5113@_t.overload 

5114def Le(bound: _tx.SupportsLt[_t.Any], /) -> PartialParser: ... 

5115def Le(*args) -> _t.Any: 

5116 """Le(inner: Parser[Cmp], bound: Cmp, /) 

5117 

5118 Alias for :class:`Bound`. 

5119 

5120 :param inner: 

5121 parser whose result will be validated. 

5122 :param bound: 

5123 upper inclusive bound for parsed values. 

5124 

5125 """ 

5126 

5127 if len(args) == 1: 

5128 return Bound(upper_inclusive=args[0]) 

5129 elif len(args) == 2: 

5130 return Bound(args[0], upper_inclusive=args[1]) 

5131 else: 

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

5133 

5134 

5135class LenBound(_BoundImpl[Sz, int], _t.Generic[Sz]): 

5136 """LenBound(inner: Parser[Sz], /, *, lower: int | None = None, lower_inclusive: int | None = None, upper: int | None = None, upper_inclusive: int | None = None) 

5137 

5138 Check that length of a value is upper- or lower-bound by some constraints. 

5139 

5140 The signature is the same as of the :class:`Bound` class. 

5141 

5142 :param inner: 

5143 parser whose result will be validated. 

5144 :param lower: 

5145 set lower bound for value's length, so we require that ``len(value) > lower``. 

5146 Can't be given if `lower_inclusive` is also given. 

5147 :param lower_inclusive: 

5148 set lower bound for value's length, so we require that ``len(value) >= lower``. 

5149 Can't be given if `lower` is also given. 

5150 :param upper: 

5151 set upper bound for value's length, so we require that ``len(value) < upper``. 

5152 Can't be given if `upper_inclusive` is also given. 

5153 :param upper_inclusive: 

5154 set upper bound for value's length, so we require that ``len(value) <= upper``. 

5155 Can't be given if `upper` is also given. 

5156 :example: 

5157 :: 

5158 

5159 >>> # List of up to five ints: 

5160 >>> LenBound(List(Int()), upper_inclusive=5) 

5161 LenBound(List(Int), len <= 5) 

5162 

5163 """ 

5164 

5165 if TYPE_CHECKING: 

5166 

5167 @_t.overload 

5168 def __new__( 

5169 cls, 

5170 inner: Parser[Sz], 

5171 /, 

5172 *, 

5173 lower: int | None = None, 

5174 lower_inclusive: int | None = None, 

5175 upper: int | None = None, 

5176 upper_inclusive: int | None = None, 

5177 ) -> LenBound[Sz]: ... 

5178 

5179 @_t.overload 

5180 def __new__( 

5181 cls, 

5182 /, 

5183 *, 

5184 lower: int | None = None, 

5185 lower_inclusive: int | None = None, 

5186 upper: int | None = None, 

5187 upper_inclusive: int | None = None, 

5188 ) -> PartialParser: ... 

5189 

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

5191 

5192 def __init__( 

5193 self, 

5194 inner: Parser[Sz] | None = None, 

5195 /, 

5196 *, 

5197 lower: int | None = None, 

5198 lower_inclusive: int | None = None, 

5199 upper: int | None = None, 

5200 upper_inclusive: int | None = None, 

5201 ): 

5202 super().__init__( 

5203 inner, 

5204 lower=lower, 

5205 lower_inclusive=lower_inclusive, 

5206 upper=upper, 

5207 upper_inclusive=upper_inclusive, 

5208 mapper=len, 

5209 desc="Length of value", 

5210 ) 

5211 

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

5213 if not self._inner.supports_parse_many(): 

5214 # somebody bound len of a string? 

5215 return self._inner.get_nargs() 

5216 

5217 lower = self._lower_bound 

5218 if lower is not None and not self._lower_bound_is_inclusive: 

5219 lower += 1 

5220 upper = self._upper_bound 

5221 if upper is not None and not self._upper_bound_is_inclusive: 

5222 upper -= 1 

5223 

5224 if lower == upper and lower is not None: 

5225 return lower 

5226 elif lower is not None and lower > 0: 

5227 return "+" 

5228 else: 

5229 return "*" 

5230 

5231 def to_json_schema( 

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

5233 ) -> yuio.json_schema.JsonSchemaType: 

5234 bound = {} 

5235 min_bound = self._lower_bound 

5236 if not self._lower_bound_is_inclusive and min_bound is not None: 

5237 min_bound -= 1 

5238 if min_bound is not None: 

5239 bound["minLength"] = bound["minItems"] = bound["minProperties"] = min_bound 

5240 max_bound = self._upper_bound 

5241 if not self._upper_bound_is_inclusive and max_bound is not None: 

5242 max_bound += 1 

5243 if max_bound is not None: 

5244 bound["maxLength"] = bound["maxItems"] = bound["maxProperties"] = max_bound 

5245 if bound: 

5246 return yuio.json_schema.AllOf( 

5247 [super().to_json_schema(ctx), yuio.json_schema.Opaque(bound)] 

5248 ) 

5249 else: 

5250 return super().to_json_schema(ctx) 

5251 

5252 

5253@_t.overload 

5254def LenGt(inner: Parser[Sz], bound: int, /) -> LenBound[Sz]: ... 

5255@_t.overload 

5256def LenGt(bound: int, /) -> PartialParser: ... 

5257def LenGt(*args) -> _t.Any: 

5258 """LenGt(inner: Parser[Sz], bound: int, /) 

5259 

5260 Alias for :class:`LenBound`. 

5261 

5262 :param inner: 

5263 parser whose result will be validated. 

5264 :param bound: 

5265 lower bound for parsed values's length. 

5266 

5267 """ 

5268 

5269 if len(args) == 1: 

5270 return LenBound(lower=args[0]) 

5271 elif len(args) == 2: 

5272 return LenBound(args[0], lower=args[1]) 

5273 else: 

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

5275 

5276 

5277@_t.overload 

5278def LenGe(inner: Parser[Sz], bound: int, /) -> LenBound[Sz]: ... 

5279@_t.overload 

5280def LenGe(bound: int, /) -> PartialParser: ... 

5281def LenGe(*args) -> _t.Any: 

5282 """LenGe(inner: Parser[Sz], bound: int, /) 

5283 

5284 Alias for :class:`LenBound`. 

5285 

5286 :param inner: 

5287 parser whose result will be validated. 

5288 :param bound: 

5289 lower inclusive bound for parsed values's length. 

5290 

5291 """ 

5292 

5293 if len(args) == 1: 

5294 return LenBound(lower_inclusive=args[0]) 

5295 elif len(args) == 2: 

5296 return LenBound(args[0], lower_inclusive=args[1]) 

5297 else: 

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

5299 

5300 

5301@_t.overload 

5302def LenLt(inner: Parser[Sz], bound: int, /) -> LenBound[Sz]: ... 

5303@_t.overload 

5304def LenLt(bound: int, /) -> PartialParser: ... 

5305def LenLt(*args) -> _t.Any: 

5306 """LenLt(inner: Parser[Sz], bound: int, /) 

5307 

5308 Alias for :class:`LenBound`. 

5309 

5310 :param inner: 

5311 parser whose result will be validated. 

5312 :param bound: 

5313 upper bound for parsed values's length. 

5314 

5315 """ 

5316 

5317 if len(args) == 1: 

5318 return LenBound(upper=args[0]) 

5319 elif len(args) == 2: 

5320 return LenBound(args[0], upper=args[1]) 

5321 else: 

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

5323 

5324 

5325@_t.overload 

5326def LenLe(inner: Parser[Sz], bound: int, /) -> LenBound[Sz]: ... 

5327@_t.overload 

5328def LenLe(bound: int, /) -> PartialParser: ... 

5329def LenLe(*args) -> _t.Any: 

5330 """LenLe(inner: Parser[Sz], bound: int, /) 

5331 

5332 Alias for :class:`LenBound`. 

5333 

5334 :param inner: 

5335 parser whose result will be validated. 

5336 :param bound: 

5337 upper inclusive bound for parsed values's length. 

5338 

5339 """ 

5340 

5341 if len(args) == 1: 

5342 return LenBound(upper_inclusive=args[0]) 

5343 elif len(args) == 2: 

5344 return LenBound(args[0], upper_inclusive=args[1]) 

5345 else: 

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

5347 

5348 

5349class OneOf(ValidatingParser[T], _t.Generic[T]): 

5350 """OneOf(inner: Parser[T], values: typing.Collection[T], /) 

5351 

5352 Check that the parsed value is one of the given set of values. 

5353 

5354 .. note:: 

5355 

5356 This parser is meant to validate results of other parsers; if you're looking 

5357 to parse enums or literal values, check out :class:`Enum` or :class:`Literal`. 

5358 

5359 :param inner: 

5360 parser whose result will be validated. 

5361 :param values: 

5362 collection of allowed values. 

5363 :example: 

5364 :: 

5365 

5366 >>> # Accepts only strings 'A', 'B', or 'C': 

5367 >>> OneOf(Str(), ['A', 'B', 'C']) 

5368 OneOf(Str) 

5369 

5370 """ 

5371 

5372 if TYPE_CHECKING: 

5373 

5374 @_t.overload 

5375 def __new__(cls, inner: Parser[T], values: _t.Collection[T], /) -> OneOf[T]: ... 

5376 

5377 @_t.overload 

5378 def __new__(cls, values: _t.Collection[T], /) -> PartialParser: ... 

5379 

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

5381 

5382 def __init__(self, *args): 

5383 inner: Parser[T] | None 

5384 values: _t.Collection[T] 

5385 if len(args) == 1: 

5386 inner, values = None, args[0] 

5387 elif len(args) == 2: 

5388 inner, values = args 

5389 else: 

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

5391 

5392 super().__init__(inner) 

5393 

5394 self._allowed_values = values 

5395 

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

5397 if value not in self._allowed_values: 

5398 raise ParsingError( 

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

5400 value, 

5401 yuio.string.JoinRepr.or_(self._allowed_values), 

5402 ) 

5403 

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

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

5412 

5413 def describe_or_def(self) -> str: 

5414 desc = "|".join(self.describe_value(e) for e in self._allowed_values) 

5415 if len(desc) < 80: 

5416 if len(self._allowed_values) > 1: 

5417 desc = f"{{{desc}}}" 

5418 return desc 

5419 else: 

5420 return super().describe_or_def() 

5421 

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

5423 return [ 

5424 yuio.widget.Option(e, self.describe_value(e)) for e in self._allowed_values 

5425 ] 

5426 

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

5428 return yuio.complete.Choice( 

5429 [yuio.complete.Option(self.describe_value(e)) for e in self._allowed_values] 

5430 ) 

5431 

5432 def widget( 

5433 self, 

5434 default: object | yuio.Missing, 

5435 input_description: str | None, 

5436 default_description: str | None, 

5437 /, 

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

5439 allowed_values = list(self._allowed_values) 

5440 

5441 options = _t.cast(list[yuio.widget.Option[T | yuio.Missing]], self.options()) 

5442 

5443 if not options: 

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

5445 

5446 if default is yuio.MISSING: 

5447 default_index = 0 

5448 elif default in allowed_values: 

5449 default_index = list(allowed_values).index(default) # type: ignore 

5450 else: 

5451 options.insert( 

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

5453 ) 

5454 default_index = 0 

5455 

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

5457 

5458 

5459class WithMeta(MappingParser[T, T], _t.Generic[T]): 

5460 """WithMeta(inner: Parser[T], /, *, desc: str, completer: yuio.complete.Completer | None | ~yuio.MISSING = MISSING) 

5461 

5462 Overrides inline help messages and other meta information of a wrapped parser. 

5463 

5464 Inline help messages will show up as hints in autocompletion and widgets. 

5465 

5466 :param inner: 

5467 inner parser. 

5468 :param desc: 

5469 description override. This short string will be used in CLI, widgets, and 

5470 completers to describe expected value. 

5471 :param completer: 

5472 completer override. Pass :data:`None` to disable completion. 

5473 

5474 """ 

5475 

5476 if TYPE_CHECKING: 

5477 

5478 @_t.overload 

5479 def __new__( 

5480 cls, 

5481 inner: Parser[T], 

5482 /, 

5483 *, 

5484 desc: str | None = None, 

5485 completer: yuio.complete.Completer | yuio.Missing | None = yuio.MISSING, 

5486 ) -> MappingParser[T, T]: ... 

5487 

5488 @_t.overload 

5489 def __new__( 

5490 cls, 

5491 /, 

5492 *, 

5493 desc: str | None = None, 

5494 completer: yuio.complete.Completer | yuio.Missing | None = yuio.MISSING, 

5495 ) -> PartialParser: ... 

5496 

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

5498 

5499 def __init__( 

5500 self, 

5501 *args, 

5502 desc: str | None = None, 

5503 completer: yuio.complete.Completer | yuio.Missing | None = yuio.MISSING, 

5504 ): 

5505 inner: Parser[T] | None 

5506 if not args: 

5507 inner = None 

5508 elif len(args) == 1: 

5509 inner = args[0] 

5510 else: 

5511 raise TypeError(f"expected at most 1 positional argument, got {len(args)}") 

5512 

5513 self._desc = desc 

5514 self._completer = completer 

5515 super().__init__(inner) 

5516 

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

5518 return True 

5519 

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

5521 return self._desc or self._inner.describe() 

5522 

5523 def describe_or_def(self) -> str: 

5524 return self._desc or self._inner.describe_or_def() 

5525 

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

5527 return self._desc or self._inner.describe_many() 

5528 

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

5530 return self._inner.describe_value(value) 

5531 

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

5533 return self._inner.parse_with_ctx(ctx) 

5534 

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

5536 return self._inner.parse_many_with_ctx(ctxs) 

5537 

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

5539 return self._inner.parse_config_with_ctx(ctx) 

5540 

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

5542 return self._inner.options() 

5543 

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

5545 if self._completer is not yuio.MISSING: 

5546 return self._completer # type: ignore 

5547 else: 

5548 return self._inner.completer() 

5549 

5550 def widget( 

5551 self, 

5552 default: object | yuio.Missing, 

5553 input_description: str | None, 

5554 default_description: str | None, 

5555 /, 

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

5557 return self._inner.widget(default, input_description, default_description) 

5558 

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

5560 return self._inner.to_json_value(value) 

5561 

5562 

5563class _WidgetResultMapper(yuio.widget.Map[T | yuio.Missing, str]): 

5564 def __init__( 

5565 self, 

5566 parser: Parser[T], 

5567 input_description: str | None, 

5568 default: object | yuio.Missing, 

5569 widget: yuio.widget.Widget[str], 

5570 ): 

5571 self._parser = parser 

5572 self._input_description = input_description 

5573 self._default = default 

5574 super().__init__(widget, self.mapper) 

5575 

5576 def mapper(self, s: str) -> T | yuio.Missing: 

5577 if not s and self._default is not yuio.MISSING: 

5578 return yuio.MISSING 

5579 elif not s: 

5580 raise ParsingError("Input is required") 

5581 try: 

5582 return self._parser.parse_with_ctx(StrParsingContext(s)) 

5583 except ParsingError as e: 

5584 if ( 

5585 isinstance( 

5586 self._inner, (yuio.widget.Input, yuio.widget.InputWithCompletion) 

5587 ) 

5588 and e.pos 

5589 and e.raw == self._inner.text 

5590 ): 

5591 if e.pos == (0, len(self._inner.text)): 

5592 # Don't highlight the entire text, it's not useful and creates 

5593 # visual noise. 

5594 self._inner.err_region = None 

5595 else: 

5596 self._inner.err_region = e.pos 

5597 e.raw = None 

5598 e.pos = None 

5599 raise 

5600 

5601 @property 

5602 def help_data(self): 

5603 return super().help_data.with_action( 

5604 group="Input Format", 

5605 msg=self._input_description, 

5606 prepend=True, 

5607 prepend_group=True, 

5608 ) 

5609 

5610 

5611def _secret_widget( 

5612 parser: Parser[T], 

5613 default: object | yuio.Missing, 

5614 input_description: str | None, 

5615 default_description: str | None, 

5616 /, 

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

5618 return _WidgetResultMapper( 

5619 parser, 

5620 input_description, 

5621 default, 

5622 ( 

5623 yuio.widget.SecretInput( 

5624 placeholder=default_description or "", 

5625 ) 

5626 ), 

5627 ) 

5628 

5629 

5630class StrParsingContext: 

5631 """StrParsingContext(content: str, /, *, n_arg: int | None = None) 

5632 

5633 String parsing context tracks current position in the string. 

5634 

5635 :param content: 

5636 content to parse. 

5637 :param n_arg: 

5638 content index when using :meth:`~Parser.parse_many`. 

5639 

5640 """ 

5641 

5642 def __init__( 

5643 self, 

5644 content: str, 

5645 /, 

5646 *, 

5647 n_arg: int | None = None, 

5648 _value: str | None = None, 

5649 _start: int | None = None, 

5650 _end: int | None = None, 

5651 ): 

5652 self.start: int = _start if _start is not None else 0 

5653 """ 

5654 Start position of the value. 

5655 

5656 """ 

5657 

5658 self.end: int = _end if _end is not None else self.start + len(content) 

5659 """ 

5660 End position of the value. 

5661 

5662 """ 

5663 

5664 self.content: str = content 

5665 """ 

5666 Full content of the value that was passed to :meth:`Parser.parse`. 

5667 

5668 """ 

5669 

5670 self.value: str = _value if _value is not None else content 

5671 """ 

5672 Part of the :attr:`~StrParsingContext.content` that's currently being parsed. 

5673 

5674 """ 

5675 

5676 self.n_arg: int | None = n_arg 

5677 """ 

5678 For :meth:`~Parser.parse_many`, this attribute contains index of the value 

5679 that is being parsed. For :meth:`~Parser.parse`, this is :data:`None`. 

5680 

5681 """ 

5682 

5683 def split( 

5684 self, delimiter: str | None = None, /, maxsplit: int = -1 

5685 ) -> _t.Generator[StrParsingContext]: 

5686 """ 

5687 Split current value by the given delimiter while keeping track of the current position. 

5688 

5689 """ 

5690 

5691 if delimiter is None: 

5692 yield from self._split_space(maxsplit=maxsplit) 

5693 return 

5694 

5695 dlen = len(delimiter) 

5696 start = self.start 

5697 for part in self.value.split(delimiter, maxsplit=maxsplit): 

5698 yield StrParsingContext( 

5699 self.content, 

5700 _value=part, 

5701 _start=start, 

5702 _end=start + len(part), 

5703 n_arg=self.n_arg, 

5704 ) 

5705 start += len(part) + dlen 

5706 

5707 def _split_space(self, maxsplit: int = -1) -> _t.Generator[StrParsingContext]: 

5708 i = 0 

5709 n_splits = 0 

5710 is_space = True 

5711 for part in re.split(r"(\s+)", self.value): 

5712 is_space = not is_space 

5713 if is_space: 

5714 i += len(part) 

5715 continue 

5716 

5717 if not part: 

5718 continue 

5719 

5720 if maxsplit >= 0 and n_splits >= maxsplit: 

5721 part = self.value[i:] 

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 return 

5730 else: 

5731 yield StrParsingContext( 

5732 self.content, 

5733 _value=part, 

5734 _start=i, 

5735 _end=i + len(part), 

5736 n_arg=self.n_arg, 

5737 ) 

5738 i += len(part) 

5739 n_splits += 1 

5740 

5741 def strip(self, chars: str | None = None, /) -> StrParsingContext: 

5742 """ 

5743 Strip current value while keeping track of the current position. 

5744 

5745 """ 

5746 

5747 l_stripped = self.value.lstrip(chars) 

5748 start = self.start + (len(self.value) - len(l_stripped)) 

5749 stripped = l_stripped.rstrip(chars) 

5750 return StrParsingContext( 

5751 self.content, 

5752 _value=stripped, 

5753 _start=start, 

5754 _end=start + len(stripped), 

5755 n_arg=self.n_arg, 

5756 ) 

5757 

5758 def strip_if_non_space(self) -> StrParsingContext: 

5759 """ 

5760 Strip current value unless it entirely consists of spaces. 

5761 

5762 """ 

5763 

5764 if not self.value or self.value.isspace(): 

5765 return self 

5766 else: 

5767 return self.strip() 

5768 

5769 # If you need more methods, feel free to open an issue or send a PR! 

5770 # For now, `split` and `split` is enough. 

5771 

5772 

5773class ConfigParsingContext: 

5774 """ 

5775 Config parsing context tracks path in the config, similar to JSON path. 

5776 

5777 """ 

5778 

5779 def __init__( 

5780 self, 

5781 value: object, 

5782 /, 

5783 *, 

5784 parent: ConfigParsingContext | None = None, 

5785 key: _t.Any = None, 

5786 desc: str | None = None, 

5787 ): 

5788 self.value: object = value 

5789 """ 

5790 Config value to be validated and parsed. 

5791 

5792 """ 

5793 

5794 self.parent: ConfigParsingContext | None = parent 

5795 """ 

5796 Parent context. 

5797 

5798 """ 

5799 

5800 self.key: _t.Any = key 

5801 """ 

5802 Key that was accessed when we've descended from parent context to this one. 

5803 

5804 Root context has key :data:`None`. 

5805 

5806 """ 

5807 

5808 self.desc: str | None = desc 

5809 """ 

5810 Additional description of the key. 

5811 

5812 """ 

5813 

5814 def descend( 

5815 self, 

5816 value: _t.Any, 

5817 key: _t.Any, 

5818 desc: str | None = None, 

5819 ) -> ConfigParsingContext: 

5820 """ 

5821 Create a new context that adds a new key to the path. 

5822 

5823 :param value: 

5824 inner value that was derived from the current value by accessing it with 

5825 the given `key`. 

5826 :param key: 

5827 key that we use to descend into the current value. 

5828 

5829 For example, let's say we're parsing a list. We iterate over it and pass 

5830 its elements to a sub-parser. Before calling a sub-parser, we need to 

5831 make a new context for it. In this situation, we'll pass current element 

5832 as `value`, and is index as `key`. 

5833 :param desc: 

5834 human-readable description for the new context. Will be colorized 

5835 and ``%``-formatted with a single named argument `key`. 

5836 

5837 This is useful when parsing structures that need something more complex than 

5838 JSON path. For example, when parsing a key in a dictionary, it is helpful 

5839 to set description to something like ``"key of element #%(key)r"``. 

5840 This way, parsing errors will have a more clear message: 

5841 

5842 .. code-block:: text 

5843 

5844 Parsing error: 

5845 In key of element #2: 

5846 Expected str, got int: 10 

5847 

5848 """ 

5849 

5850 return ConfigParsingContext(value, parent=self, key=key, desc=desc) 

5851 

5852 def make_path(self) -> list[tuple[_t.Any, str | None]]: 

5853 """ 

5854 Capture current path. 

5855 

5856 :returns: 

5857 a list of tuples. First element of each tuple is a key, second is 

5858 an additional description. 

5859 

5860 """ 

5861 

5862 path = [] 

5863 

5864 root = self 

5865 while True: 

5866 if root.parent is None: 

5867 break 

5868 else: 

5869 path.append((root.key, root.desc)) 

5870 root = root.parent 

5871 

5872 path.reverse() 

5873 

5874 return path 

5875 

5876 

5877class _PathRenderer: 

5878 def __init__(self, path: list[tuple[_t.Any, str | None]]): 

5879 self._path = path 

5880 

5881 def __colorized_str__( 

5882 self, ctx: yuio.string.ReprContext 

5883 ) -> yuio.string.ColorizedString: 

5884 code_color = ctx.theme.get_color("msg/text:code/repr hl:repr") 

5885 punct_color = ctx.theme.get_color("msg/text:code/repr hl/punct:repr") 

5886 

5887 msg = yuio.string.ColorizedString(code_color) 

5888 msg.start_no_wrap() 

5889 

5890 for i, (key, desc) in enumerate(self._path): 

5891 if desc: 

5892 desc = ( 

5893 (yuio.string) 

5894 .colorize(desc, ctx=ctx) 

5895 .percent_format({"key": key}, ctx=ctx) 

5896 ) 

5897 

5898 if i == len(self._path) - 1: 

5899 # Last key. 

5900 if msg: 

5901 msg.append_color(punct_color) 

5902 msg.append_str(", ") 

5903 msg.append_colorized_str(desc) 

5904 else: 

5905 # Element in the middle. 

5906 if not msg: 

5907 msg.append_str("$") 

5908 msg.append_color(punct_color) 

5909 msg.append_str(".<") 

5910 msg.append_colorized_str(desc) 

5911 msg.append_str(">") 

5912 elif isinstance(key, str) and re.match(r"^[a-zA-Z_][\w-]*$", key): 

5913 # Key is identifier-like, use `x.key` notation. 

5914 if not msg: 

5915 msg.append_str("$") 

5916 msg.append_color(punct_color) 

5917 msg.append_str(".") 

5918 msg.append_color(code_color) 

5919 msg.append_str(key) 

5920 else: 

5921 # Key is not identifier-like, use `x[key]` notation. 

5922 if not msg: 

5923 msg.append_str("$") 

5924 msg.append_color(punct_color) 

5925 msg.append_str("[") 

5926 msg.append_color(code_color) 

5927 msg.append_str(repr(key)) 

5928 msg.append_color(punct_color) 

5929 msg.append_str("]") 

5930 

5931 msg.end_no_wrap() 

5932 return msg 

5933 

5934 

5935class _CodeRenderer: 

5936 def __init__(self, code: str, pos: tuple[int, int], as_cli: bool = False): 

5937 self._code = code 

5938 self._pos = pos 

5939 self._as_cli = as_cli 

5940 

5941 def __colorized_str__( 

5942 self, ctx: yuio.string.ReprContext 

5943 ) -> yuio.string.ColorizedString: 

5944 width = ctx.width - 2 # Account for indentation. 

5945 

5946 if width < 10: # 6 symbols for ellipsis and at least 2 wide chars. 

5947 return yuio.string.ColorizedString() 

5948 

5949 start, end = self._pos 

5950 if end == start: 

5951 end += 1 

5952 

5953 left = self._code[:start] 

5954 center = self._code[start:end] 

5955 right = self._code[end:] 

5956 

5957 l_width = yuio.string.line_width(left) 

5958 c_width = yuio.string.line_width(center) 

5959 r_width = yuio.string.line_width(right) 

5960 

5961 available_width = width - (3 if left else 0) - 3 

5962 if c_width > available_width: 

5963 # Center can't fit: remove left and right side, 

5964 # and trim as much center as needed. 

5965 

5966 left = "..." if l_width > 3 else left 

5967 l_width = len(left) 

5968 

5969 right = "" 

5970 r_width = 0 

5971 

5972 new_c = "" 

5973 c_width = 0 

5974 

5975 for c in center: 

5976 cw = yuio.string.line_width(c) 

5977 if c_width + cw <= available_width: 

5978 new_c += c 

5979 c_width += cw 

5980 else: 

5981 new_c += "..." 

5982 c_width += 3 

5983 break 

5984 center = new_c 

5985 

5986 if r_width > 3 and l_width + c_width + r_width > width: 

5987 # Trim right side. 

5988 new_r = "" 

5989 r_width = 3 

5990 for c in right: 

5991 cw = yuio.string.line_width(c) 

5992 if l_width + c_width + r_width + cw <= width: 

5993 new_r += c 

5994 r_width += cw 

5995 else: 

5996 new_r += "..." 

5997 break 

5998 right = new_r 

5999 

6000 if l_width > 3 and l_width + c_width + r_width > width: 

6001 # Trim left side. 

6002 new_l = "" 

6003 l_width = 3 

6004 for c in left[::-1]: 

6005 cw = yuio.string.line_width(c) 

6006 if l_width + c_width + r_width + cw <= width: 

6007 new_l += c 

6008 l_width += cw 

6009 else: 

6010 new_l += "..." 

6011 break 

6012 left = new_l[::-1] 

6013 

6014 if self._as_cli: 

6015 punct_color = ctx.theme.get_color( 

6016 "msg/text:code/sh-usage hl/punct:sh-usage" 

6017 ) 

6018 else: 

6019 punct_color = ctx.theme.get_color("msg/text:code/text hl/punct:text") 

6020 

6021 res = yuio.string.ColorizedString() 

6022 res.start_no_wrap() 

6023 

6024 if self._as_cli: 

6025 res.append_color(punct_color) 

6026 res.append_str("$ ") 

6027 res.append_colorized_str( 

6028 ctx.str( 

6029 yuio.string.Hl( 

6030 left.replace("%", "%%") + "%s" + right.replace("%", "%%"), # pyright: ignore[reportArgumentType] 

6031 yuio.string.WithBaseColor( 

6032 center, base_color="hl/error:sh-usage" 

6033 ), 

6034 syntax="sh-usage", 

6035 ) 

6036 ) 

6037 ) 

6038 else: 

6039 text_color = ctx.theme.get_color("msg/text:code/text") 

6040 res.append_color(punct_color) 

6041 res.append_str("> ") 

6042 res.append_color(text_color) 

6043 res.append_str(left) 

6044 res.append_color(text_color | ctx.theme.get_color("hl/error:text")) 

6045 res.append_str(center) 

6046 res.append_color(text_color) 

6047 res.append_str(right) 

6048 res.append_color(yuio.color.Color.NONE) 

6049 res.append_str("\n") 

6050 if self._as_cli: 

6051 text_color = ctx.theme.get_color("msg/text:code/sh-usage") 

6052 res.append_color(text_color | ctx.theme.get_color("hl/error:sh-usage")) 

6053 else: 

6054 text_color = ctx.theme.get_color("msg/text:code/text") 

6055 res.append_color(text_color | ctx.theme.get_color("hl/error:text")) 

6056 res.append_str(" ") 

6057 res.append_str(" " * yuio.string.line_width(left)) 

6058 res.append_str("~" * yuio.string.line_width(center)) 

6059 

6060 res.end_no_wrap() 

6061 

6062 return res 

6063 

6064 

6065def _repr_and_adjust_pos(s: str, pos: tuple[int, int]): 

6066 start, end = pos 

6067 

6068 left = json.dumps(s[:start])[:-1] 

6069 center = json.dumps(s[start:end])[1:-1] 

6070 right = json.dumps(s[end:])[1:] 

6071 

6072 return left + center + right, (len(left), len(left) + len(center)) 

6073 

6074 

6075_FromTypeHintCallback: _t.TypeAlias = _t.Callable[ 

6076 [type, type | None, tuple[object, ...]], Parser[object] | None 

6077] 

6078 

6079 

6080_FROM_TYPE_HINT_CALLBACKS: list[tuple[_FromTypeHintCallback, bool]] = [] 

6081_FROM_TYPE_HINT_DELIM_SUGGESTIONS: list[str | None] = [ 

6082 None, 

6083 ",", 

6084 "@", 

6085 "/", 

6086 "=", 

6087] 

6088 

6089 

6090class _FromTypeHintDepth(threading.local): 

6091 def __init__(self): 

6092 self.depth: int = 0 

6093 self.uses_delim = False 

6094 

6095 

6096_FROM_TYPE_HINT_DEPTH: _FromTypeHintDepth = _FromTypeHintDepth() 

6097 

6098 

6099@_t.overload 

6100def from_type_hint(ty: type[T], /) -> Parser[T]: ... 

6101@_t.overload 

6102def from_type_hint(ty: object, /) -> Parser[object]: ... 

6103def from_type_hint(ty: _t.Any, /) -> Parser[object]: 

6104 """from_type_hint(ty: type[T], /) -> Parser[T] 

6105 

6106 Create parser from a type hint. 

6107 

6108 :param ty: 

6109 a type hint. 

6110 

6111 This type hint should not contain strings or forward references. Make sure 

6112 they're resolved before passing it to this function. 

6113 :returns: 

6114 a parser instance created from type hint. 

6115 :raises: 

6116 :class:`TypeError` if type hint contains forward references or types 

6117 that don't have associated parsers. 

6118 :example: 

6119 :: 

6120 

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

6122 Optional(List(Int)) 

6123 

6124 """ 

6125 

6126 result = _from_type_hint(ty) 

6127 setattr(result, "_Parser__typehint", ty) 

6128 return result 

6129 

6130 

6131def _from_type_hint(ty: _t.Any, /) -> Parser[object]: 

6132 if isinstance(ty, (str, _t.ForwardRef)): 

6133 raise TypeError(f"forward references are not supported here: {ty}") 

6134 

6135 origin = _t.get_origin(ty) 

6136 args = _t.get_args(ty) 

6137 

6138 if origin is _t.Annotated: 

6139 p = from_type_hint(args[0]) 

6140 for arg in args[1:]: 

6141 if isinstance(arg, PartialParser): 

6142 p = arg.wrap(p) 

6143 return p 

6144 

6145 for cb, uses_delim in _FROM_TYPE_HINT_CALLBACKS: 

6146 prev_uses_delim = _FROM_TYPE_HINT_DEPTH.uses_delim 

6147 _FROM_TYPE_HINT_DEPTH.uses_delim = uses_delim 

6148 _FROM_TYPE_HINT_DEPTH.depth += uses_delim 

6149 try: 

6150 p = cb(ty, origin, args) 

6151 if p is not None: 

6152 return p 

6153 finally: 

6154 _FROM_TYPE_HINT_DEPTH.uses_delim = prev_uses_delim 

6155 _FROM_TYPE_HINT_DEPTH.depth -= uses_delim 

6156 

6157 if _tx.is_union(origin): 

6158 if is_optional := (type(None) in args): 

6159 args = list(args) 

6160 args.remove(type(None)) 

6161 if len(args) == 1: 

6162 p = from_type_hint(args[0]) 

6163 else: 

6164 p = Union(*[from_type_hint(arg) for arg in args]) 

6165 if is_optional: 

6166 p = Optional(p) 

6167 return p 

6168 else: 

6169 raise TypeError(f"unsupported type {_tx.type_repr(ty)}") 

6170 

6171 

6172@_t.overload 

6173def register_type_hint_conversion( 

6174 cb: _FromTypeHintCallback, 

6175 /, 

6176 *, 

6177 uses_delim: bool = False, 

6178) -> _FromTypeHintCallback: ... 

6179@_t.overload 

6180def register_type_hint_conversion( 

6181 *, 

6182 uses_delim: bool = False, 

6183) -> _t.Callable[[_FromTypeHintCallback], _FromTypeHintCallback]: ... 

6184def register_type_hint_conversion( 

6185 cb: _FromTypeHintCallback | None = None, 

6186 /, 

6187 *, 

6188 uses_delim: bool = False, 

6189) -> ( 

6190 _FromTypeHintCallback | _t.Callable[[_FromTypeHintCallback], _FromTypeHintCallback] 

6191): 

6192 """ 

6193 Register a new converter from a type hint to a parser. 

6194 

6195 This function takes a callback that accepts three positional arguments: 

6196 

6197 - a type hint, 

6198 - a type hint's origin (as defined by :func:`typing.get_origin`), 

6199 - a type hint's args (as defined by :func:`typing.get_args`). 

6200 

6201 The callback should return a parser if it can, or :data:`None` otherwise. 

6202 

6203 All registered callbacks are tried in the same order 

6204 as they were registered. 

6205 

6206 If `uses_delim` is :data:`True`, callback can use 

6207 :func:`suggest_delim_for_type_hint_conversion`. 

6208 

6209 This function can be used as a decorator. 

6210 

6211 :param cb: 

6212 a function that should inspect a type hint and possibly return a parser. 

6213 :param uses_delim: 

6214 indicates that callback will use 

6215 :func:`suggest_delim_for_type_hint_conversion`. 

6216 :example: 

6217 .. invisible-code-block: python 

6218 

6219 class MyType: ... 

6220 class MyTypeParser(ValueParser[MyType]): 

6221 def __init__(self): super().__init__(MyType) 

6222 def parse_with_ctx(self, ctx: StrParsingContext, /): ... 

6223 def parse_config_with_ctx(self, value, /): ... 

6224 def to_json_schema(self, ctx, /): ... 

6225 def to_json_value(self, value, /): ... 

6226 

6227 .. code-block:: python 

6228 

6229 @register_type_hint_conversion 

6230 def my_type_conversion(ty, origin, args): 

6231 if ty is MyType: 

6232 return MyTypeParser() 

6233 else: 

6234 return None 

6235 

6236 :: 

6237 

6238 >>> from_type_hint(MyType) 

6239 MyTypeParser 

6240 

6241 .. invisible-code-block: python 

6242 

6243 del _FROM_TYPE_HINT_CALLBACKS[-1] 

6244 

6245 """ 

6246 

6247 def registrar(cb: _FromTypeHintCallback): 

6248 _FROM_TYPE_HINT_CALLBACKS.append((cb, uses_delim)) 

6249 return cb 

6250 

6251 return registrar(cb) if cb is not None else registrar 

6252 

6253 

6254def suggest_delim_for_type_hint_conversion() -> str | None: 

6255 """ 

6256 Suggests a delimiter for use in type hint converters. 

6257 

6258 When creating a parser for a collection of items based on a type hint, 

6259 it is important to use different delimiters for nested collections. 

6260 This function can suggest such a delimiter based on the current type hint's depth. 

6261 

6262 .. invisible-code-block: python 

6263 

6264 class MyCollection(list, _t.Generic[T]): ... 

6265 class MyCollectionParser(CollectionParser[MyCollection[T], T], _t.Generic[T]): 

6266 def __init__(self, inner: Parser[T], /, *, delimiter: _t.Optional[str] = None): 

6267 super().__init__(inner, ty=MyCollection, ctor=MyCollection, delimiter=delimiter) 

6268 def to_json_schema(self, ctx, /): ... 

6269 def to_json_value(self, value, /): ... 

6270 

6271 :raises: 

6272 :class:`RuntimeError` if called from a type converter that 

6273 didn't set `uses_delim` to :data:`True`. 

6274 :example: 

6275 .. code-block:: python 

6276 

6277 @register_type_hint_conversion(uses_delim=True) 

6278 def my_collection_conversion(ty, origin, args): 

6279 if origin is MyCollection: 

6280 return MyCollectionParser( 

6281 from_type_hint(args[0]), 

6282 delimiter=suggest_delim_for_type_hint_conversion(), 

6283 ) 

6284 else: 

6285 return None 

6286 

6287 :: 

6288 

6289 >>> parser = from_type_hint(MyCollection[MyCollection[str]]) 

6290 >>> parser 

6291 MyCollectionParser(MyCollectionParser(Str)) 

6292 >>> # First delimiter is `None`, meaning split by whitespace: 

6293 >>> parser._delimiter is None 

6294 True 

6295 >>> # Second delimiter is `","`: 

6296 >>> parser._inner._delimiter == "," 

6297 True 

6298 

6299 .. 

6300 >>> del _FROM_TYPE_HINT_CALLBACKS[-1] 

6301 

6302 """ 

6303 

6304 if not _FROM_TYPE_HINT_DEPTH.uses_delim: 

6305 raise RuntimeError( 

6306 "looking up delimiters is not available in this callback; did you forget" 

6307 " to pass `uses_delim=True` when registering this callback?" 

6308 ) 

6309 

6310 depth = _FROM_TYPE_HINT_DEPTH.depth - 1 

6311 if depth < len(_FROM_TYPE_HINT_DELIM_SUGGESTIONS): 

6312 return _FROM_TYPE_HINT_DELIM_SUGGESTIONS[depth] 

6313 else: 

6314 return None 

6315 

6316 

6317register_type_hint_conversion(lambda ty, origin, args: Str() if ty is str else None) 

6318register_type_hint_conversion(lambda ty, origin, args: Int() if ty is int else None) 

6319register_type_hint_conversion(lambda ty, origin, args: Float() if ty is float else None) 

6320register_type_hint_conversion(lambda ty, origin, args: Bool() if ty is bool else None) 

6321register_type_hint_conversion( 

6322 lambda ty, origin, args: ( 

6323 Enum(ty) if isinstance(ty, type) and issubclass(ty, enum.Enum) else None 

6324 ) 

6325) 

6326register_type_hint_conversion( 

6327 lambda ty, origin, args: Decimal() if ty is decimal.Decimal else None 

6328) 

6329register_type_hint_conversion( 

6330 lambda ty, origin, args: Fraction() if ty is fractions.Fraction else None 

6331) 

6332register_type_hint_conversion( 

6333 lambda ty, origin, args: ( 

6334 List( 

6335 from_type_hint(args[0]), delimiter=suggest_delim_for_type_hint_conversion() 

6336 ) 

6337 if origin is list 

6338 else None 

6339 ), 

6340 uses_delim=True, 

6341) 

6342register_type_hint_conversion( 

6343 lambda ty, origin, args: ( 

6344 Set(from_type_hint(args[0]), delimiter=suggest_delim_for_type_hint_conversion()) 

6345 if origin is set 

6346 else None 

6347 ), 

6348 uses_delim=True, 

6349) 

6350register_type_hint_conversion( 

6351 lambda ty, origin, args: ( 

6352 FrozenSet( 

6353 from_type_hint(args[0]), delimiter=suggest_delim_for_type_hint_conversion() 

6354 ) 

6355 if origin is frozenset 

6356 else None 

6357 ), 

6358 uses_delim=True, 

6359) 

6360register_type_hint_conversion( 

6361 lambda ty, origin, args: ( 

6362 Dict( 

6363 from_type_hint(args[0]), 

6364 from_type_hint(args[1]), 

6365 delimiter=suggest_delim_for_type_hint_conversion(), 

6366 ) 

6367 if origin is dict 

6368 else None 

6369 ), 

6370 uses_delim=True, 

6371) 

6372register_type_hint_conversion( 

6373 lambda ty, origin, args: ( 

6374 Tuple( 

6375 *[from_type_hint(arg) for arg in args], 

6376 delimiter=suggest_delim_for_type_hint_conversion(), 

6377 ) 

6378 if origin is tuple and ... not in args 

6379 else None 

6380 ), 

6381 uses_delim=True, 

6382) 

6383register_type_hint_conversion( 

6384 lambda ty, origin, args: Path() if ty is pathlib.Path else None 

6385) 

6386register_type_hint_conversion( 

6387 lambda ty, origin, args: Json() if ty is yuio.json_schema.JsonValue else None 

6388) 

6389register_type_hint_conversion( 

6390 lambda ty, origin, args: DateTime() if ty is datetime.datetime else None 

6391) 

6392register_type_hint_conversion( 

6393 lambda ty, origin, args: Date() if ty is datetime.date else None 

6394) 

6395register_type_hint_conversion( 

6396 lambda ty, origin, args: Time() if ty is datetime.time else None 

6397) 

6398register_type_hint_conversion( 

6399 lambda ty, origin, args: TimeDelta() if ty is datetime.timedelta else None 

6400) 

6401register_type_hint_conversion( 

6402 lambda ty, origin, args: ( 

6403 Literal(*_t.cast(tuple[_t.Any, ...], args)) if origin is _t.Literal else None 

6404 ) 

6405) 

6406 

6407 

6408@register_type_hint_conversion 

6409def __secret(ty, origin, args): 

6410 if ty is SecretValue: 

6411 raise TypeError("yuio.secret.SecretValue requires type arguments") 

6412 if origin is SecretValue: 

6413 if len(args) == 1: 

6414 return Secret(from_type_hint(args[0])) 

6415 else: # pragma: no cover 

6416 raise TypeError( 

6417 f"yuio.secret.SecretValue requires 1 type argument, got {len(args)}" 

6418 ) 

6419 return None 

6420 

6421 

6422def _is_optional_parser(parser: Parser[_t.Any] | None, /) -> bool: 

6423 while parser is not None: 

6424 if isinstance(parser, Optional): 

6425 return True 

6426 parser = parser.__wrapped_parser__ 

6427 return False 

6428 

6429 

6430def _is_bool_parser(parser: Parser[_t.Any] | None, /) -> bool: 

6431 while parser is not None: 

6432 if isinstance(parser, Bool): 

6433 return True 

6434 parser = parser.__wrapped_parser__ 

6435 return False