Coverage for yuio / parse.py: 92%

1634 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-01-05 11:41 +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:: Decimal 

68 

69.. autoclass:: Fraction 

70 

71.. autoclass:: Json 

72 

73.. autoclass:: List 

74 

75.. autoclass:: Set 

76 

77.. autoclass:: FrozenSet 

78 

79.. autoclass:: Dict 

80 

81.. autoclass:: Tuple 

82 

83.. autoclass:: Optional 

84 

85.. autoclass:: Union 

86 

87.. autoclass:: Path 

88 

89.. autoclass:: NonExistentPath 

90 

91.. autoclass:: ExistingPath 

92 

93.. autoclass:: File 

94 

95.. autoclass:: Dir 

96 

97.. autoclass:: GitRepo 

98 

99.. autoclass:: Secret 

100 

101 

102.. _validating-parsers: 

103 

104Validators 

105---------- 

106 

107.. autoclass:: Regex 

108 

109.. autoclass:: Bound 

110 

111.. autoclass:: Gt 

112 

113.. autoclass:: Ge 

114 

115.. autoclass:: Lt 

116 

117.. autoclass:: Le 

118 

119.. autoclass:: LenBound 

120 

121.. autoclass:: LenGt 

122 

123.. autoclass:: LenGe 

124 

125.. autoclass:: LenLt 

126 

127.. autoclass:: LenLe 

128 

129.. autoclass:: OneOf 

130 

131 

132Auxiliary parsers 

133----------------- 

134 

135.. autoclass:: Map 

136 

137.. autoclass:: Apply 

138 

139.. autoclass:: Lower 

140 

141.. autoclass:: Upper 

142 

143.. autoclass:: CaseFold 

144 

145.. autoclass:: Strip 

146 

147.. autoclass:: WithMeta 

148 

149 

150Deriving parsers from type hints 

151-------------------------------- 

152 

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

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

155 

156.. autofunction:: from_type_hint 

157 

158 

159.. _partial parsers: 

160 

161Partial parsers 

162--------------- 

163 

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

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

166 

167.. invisible-code-block: python 

168 

169 from yuio.config import Config, field 

170 

171.. code-block:: python 

172 

173 class AppConfig(Config): 

174 max_line_width: int | str = field( 

175 default="default", 

176 parser=Union( 

177 Gt(Int(), 0), 

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

179 ), 

180 ) 

181 

182.. invisible-code-block: python 

183 

184 AppConfig() 

185 

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

187to type hints: 

188 

189.. code-block:: python 

190 

191 from typing import Annotated 

192 

193 

194 class AppConfig(Config): 

195 max_line_width: ( 

196 Annotated[int, Gt(0)] 

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

198 ) = "default" 

199 

200.. invisible-code-block: python 

201 

202 AppConfig() 

203 

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

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

206about their settings. 

207 

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

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

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

211 

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

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

214 Traceback (most recent call last): 

215 ... 

216 TypeError: List requires an inner parser 

217 ... 

218 

219 

220Other parser methods 

221-------------------- 

222 

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

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

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

226 

227.. autoclass:: Parser 

228 :noindex: 

229 

230 .. autoattribute:: __wrapped_parser__ 

231 

232 .. automethod:: parse_with_ctx 

233 

234 .. automethod:: parse_many_with_ctx 

235 

236 .. automethod:: parse_config_with_ctx 

237 

238 .. automethod:: get_nargs 

239 

240 .. automethod:: check_type 

241 

242 .. automethod:: assert_type 

243 

244 .. automethod:: describe 

245 

246 .. automethod:: describe_or_def 

247 

248 .. automethod:: describe_many 

249 

250 .. automethod:: describe_value 

251 

252 .. automethod:: options 

253 

254 .. automethod:: completer 

255 

256 .. automethod:: widget 

257 

258 .. automethod:: to_json_schema 

259 

260 .. automethod:: to_json_value 

261 

262 .. automethod:: is_secret 

263 

264 

265Building your own parser 

266------------------------ 

267 

268.. _parser hierarchy: 

269 

270Understanding parser hierarchy 

271~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 

272 

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

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

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

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

277 

278.. raw:: html 

279 

280 <p> 

281 <pre class="mermaid"> 

282 --- 

283 config: 

284 class: 

285 hideEmptyMembersBox: true 

286 --- 

287 classDiagram 

288 

289 class PartialParser 

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

291 

292 class Parser 

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

294 PartialParser <|-- Parser 

295 

296 class ValueParser 

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

298 Parser <|-- ValueParser 

299 

300 class WrappingParser 

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

302 Parser <|-- WrappingParser 

303 

304 class MappingParser 

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

306 WrappingParser <|-- MappingParser 

307 

308 class Map 

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

310 MappingParser <|-- Map 

311 

312 class Apply 

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

314 MappingParser <|-- Apply 

315 

316 class ValidatingParser 

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

318 Apply <|-- ValidatingParser 

319 

320 class CollectionParser 

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

322 ValueParser <|-- CollectionParser 

323 WrappingParser <|-- CollectionParser 

324 </pre> 

325 </p> 

326 

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

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

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

330consider this code: 

331 

332.. skip: next 

333 

334.. code-block:: python 

335 

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

337 

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

339 

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

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

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

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

344 

345.. skip: next 

346 

347.. code-block:: python 

348 

349 from typing import TYPE_CHECKING, Any, Generic, overload 

350 

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

352 if TYPE_CHECKING: 

353 @overload 

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

355 ... 

356 @overload 

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

358 ... 

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

360 ... 

361 

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

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

364 

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

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

367 

368.. autoclass:: PartialParser 

369 :members: 

370 

371 

372Parsing contexts 

373~~~~~~~~~~~~~~~~ 

374 

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

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

377for parsing configs. 

378 

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

380location to the user. 

381 

382.. autoclass:: StrParsingContext 

383 :members: 

384 

385.. autoclass:: ConfigParsingContext 

386 :members: 

387 

388 

389Base classes 

390~~~~~~~~~~~~ 

391 

392.. autoclass:: ValueParser 

393 

394.. autoclass:: WrappingParser 

395 

396 .. autoattribute:: _inner 

397 

398 .. autoattribute:: _inner_raw 

399 

400.. autoclass:: MappingParser 

401 

402.. autoclass:: ValidatingParser 

403 

404 .. autoattribute:: __wrapped_parser__ 

405 :noindex: 

406 

407 .. automethod:: _validate 

408 

409.. autoclass:: CollectionParser 

410 

411 .. autoattribute:: _allow_completing_duplicates 

412 

413 

414Adding type hint conversions 

415~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 

416 

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

418parsers from type hints: 

419 

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

421 

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

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

424 

425.. autofunction:: suggest_delim_for_type_hint_conversion 

426 

427 

428Re-imports 

429---------- 

430 

431.. type:: JsonValue 

432 :no-index: 

433 

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

435 

436.. type:: SecretString 

437 :no-index: 

438 

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

440 

441.. type:: SecretValue 

442 :no-index: 

443 

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

445 

446""" 

447 

448from __future__ import annotations 

449 

450import abc 

451import argparse 

452import contextlib 

453import dataclasses 

454import datetime 

455import decimal 

456import enum 

457import fractions 

458import functools 

459import json 

460import pathlib 

461import re 

462import threading 

463import traceback 

464import types 

465 

466import yuio 

467import yuio.color 

468import yuio.complete 

469import yuio.json_schema 

470import yuio.string 

471import yuio.widget 

472from yuio.json_schema import JsonValue 

473from yuio.secret import SecretString, SecretValue 

474from yuio.util import _find_docs 

475from yuio.util import to_dash_case as _to_dash_case 

476 

477import typing 

478import yuio._typing_ext as _tx 

479from typing import TYPE_CHECKING 

480 

481if TYPE_CHECKING: 

482 import typing_extensions as _t 

483else: 

484 from yuio import _typing as _t 

485 

486__all__ = [ 

487 "Apply", 

488 "Bool", 

489 "Bound", 

490 "CaseFold", 

491 "CollectionParser", 

492 "ConfigParsingContext", 

493 "Date", 

494 "DateTime", 

495 "Decimal", 

496 "Dict", 

497 "Dir", 

498 "Enum", 

499 "ExistingPath", 

500 "File", 

501 "Float", 

502 "Fraction", 

503 "FrozenSet", 

504 "Ge", 

505 "GitRepo", 

506 "Gt", 

507 "Int", 

508 "Json", 

509 "JsonValue", 

510 "Le", 

511 "LenBound", 

512 "LenGe", 

513 "LenGt", 

514 "LenLe", 

515 "LenLt", 

516 "List", 

517 "Lower", 

518 "Lt", 

519 "Map", 

520 "MappingParser", 

521 "NonExistentPath", 

522 "OneOf", 

523 "Optional", 

524 "Parser", 

525 "ParsingError", 

526 "PartialParser", 

527 "Path", 

528 "Regex", 

529 "Secret", 

530 "SecretString", 

531 "SecretValue", 

532 "Set", 

533 "Str", 

534 "StrParsingContext", 

535 "Strip", 

536 "Time", 

537 "TimeDelta", 

538 "Tuple", 

539 "Union", 

540 "Upper", 

541 "ValidatingParser", 

542 "ValueParser", 

543 "WithMeta", 

544 "WrappingParser", 

545 "from_type_hint", 

546 "register_type_hint_conversion", 

547 "suggest_delim_for_type_hint_conversion", 

548] 

549 

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

551T = _t.TypeVar("T") 

552U = _t.TypeVar("U") 

553K = _t.TypeVar("K") 

554V = _t.TypeVar("V") 

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

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

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

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

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

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

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

562Params = _t.ParamSpec("Params") 

563 

564 

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

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

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

568 

569 Raised when parsing or validation fails. 

570 

571 :param msg: 

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

573 

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

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

576 a :class:`TypeError`. 

577 :param args: 

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

579 :param fallback_msg: 

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

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

582 

583 .. warning:: 

584 

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

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

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

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

589 :param ctx: 

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

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

592 :param kwargs: 

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

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

595 :attr:`~ParsingError.path`. 

596 

597 """ 

598 

599 @_t.overload 

600 def __init__( 

601 self, 

602 msg: _t.LiteralString, 

603 /, 

604 *args, 

605 fallback_msg: _t.LiteralString | None = None, 

606 ctx: ConfigParsingContext | StrParsingContext | None = None, 

607 raw: str | None = None, 

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

609 n_arg: int | None = None, 

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

611 ): ... 

612 @_t.overload 

613 def __init__( 

614 self, 

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

616 /, 

617 *, 

618 fallback_msg: _t.LiteralString | None = None, 

619 ctx: ConfigParsingContext | StrParsingContext | None = None, 

620 raw: str | None = None, 

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

622 n_arg: int | None = None, 

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

624 ): ... 

625 def __init__( 

626 self, 

627 *args, 

628 fallback_msg: _t.LiteralString | None = None, 

629 ctx: ConfigParsingContext | StrParsingContext | None = None, 

630 raw: str | None = None, 

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

632 n_arg: int | None = None, 

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

634 ): 

635 super().__init__(*args) 

636 

637 if ctx: 

638 if isinstance(ctx, ConfigParsingContext): 

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

640 else: 

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

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

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

644 

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

646 """ 

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

648 

649 .. warning:: 

650 

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

652 

653 """ 

654 

655 self.raw: str | None = raw 

656 """ 

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

658 original string. 

659 

660 """ 

661 

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

663 """ 

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

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

666 and end indices). 

667 

668 """ 

669 

670 self.n_arg: int | None = n_arg 

671 """ 

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

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

674 

675 """ 

676 

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

678 """ 

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

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

681 

682 """ 

683 

684 @classmethod 

685 def type_mismatch( 

686 cls, 

687 value: _t.Any, 

688 /, 

689 *expected: type | str, 

690 ctx: ConfigParsingContext | StrParsingContext | None = None, 

691 raw: str | None = None, 

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

693 n_arg: int | None = None, 

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

695 ): 

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

697 

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

699 

700 :param value: 

701 value of an unexpected type. 

702 :param expected: 

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

704 a type. 

705 :param kwargs: 

706 keyword arguments will be passed to constructor. 

707 :example: 

708 :: 

709 

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

711 Traceback (most recent call last): 

712 ... 

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

714 

715 """ 

716 

717 err = cls( 

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

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

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

721 value, 

722 ctx=ctx, 

723 raw=raw, 

724 pos=pos, 

725 n_arg=n_arg, 

726 path=path, 

727 ) 

728 err.fallback_msg = yuio.string.Format( 

729 "Expected %s, got `%s`", 

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

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

732 ) 

733 

734 return err 

735 

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

737 if isinstance(ctx, ConfigParsingContext): 

738 self.path = ctx.make_path() 

739 else: 

740 self.raw = ctx.content 

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

742 self.n_arg = ctx.n_arg 

743 

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

745 colorable = super().to_colorable() 

746 if self.path: 

747 colorable = yuio.string.Format( 

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

749 _PathRenderer(self.path), 

750 yuio.string.Indent(colorable), 

751 ) 

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

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

754 colorable = yuio.string.Stack( 

755 _CodeRenderer(raw, pos), 

756 colorable, 

757 ) 

758 return colorable 

759 

760 

761class PartialParser(abc.ABC): 

762 """ 

763 An interface of a partial parser. 

764 

765 """ 

766 

767 def __init__(self): 

768 self.__orig_traceback = traceback.extract_stack() 

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

770 "yuio/parse.py" 

771 ): 

772 self.__orig_traceback.pop() 

773 super().__init__() 

774 

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

776 """ 

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

778 

779 """ 

780 

781 return self.__orig_traceback # pragma: no cover 

782 

783 @contextlib.contextmanager 

784 def _patch_stack_summary(self): 

785 """ 

786 Attach original traceback to any exception that's raised 

787 within this context manager. 

788 

789 """ 

790 

791 try: 

792 yield 

793 except Exception as e: 

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

795 self.__orig_traceback.format() 

796 ) 

797 e.args = ( 

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

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

800 ) 

801 setattr(e, "__yuio_stack_summary_text__", stack_summary_text) 

802 raise e 

803 

804 @abc.abstractmethod 

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

806 """ 

807 Apply this partial parser. 

808 

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

810 and the applies all partial parsers to it. 

811 

812 For example, given this type hint: 

813 

814 .. invisible-code-block: python 

815 

816 from typing import Annotated 

817 

818 .. code-block:: python 

819 

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

821 

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

823 this parser to ``Map.wrap``. 

824 

825 :param parser: 

826 a parser instance that was created by inspecting type hints 

827 and previous annotations. 

828 :returns: 

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

830 usually returns `self`. 

831 :raises: 

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

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

834 

835 """ 

836 

837 raise NotImplementedError() 

838 

839 

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

841 """ 

842 Base class for parsers. 

843 

844 """ 

845 

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

847 __typehint: _t.Any = None 

848 

849 __wrapped_parser__: Parser[object] | None = None 

850 """ 

851 An attribute for unwrapping parsers that validate or map results 

852 of other parsers. 

853 

854 """ 

855 

856 @_t.final 

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

858 """ 

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

860 

861 :param value: 

862 value to parse. 

863 :returns: 

864 a parsed and processed value. 

865 :raises: 

866 :class:`ParsingError`. 

867 

868 """ 

869 

870 return self.parse_with_ctx(StrParsingContext(value)) 

871 

872 @abc.abstractmethod 

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

874 """ 

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

876 of a raw string. 

877 

878 :param ctx: 

879 value to parse, wrapped into a parsing context. 

880 :returns: 

881 a parsed and processed value. 

882 :raises: 

883 :class:`ParsingError`. 

884 

885 """ 

886 

887 raise NotImplementedError() 

888 

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

890 """ 

891 For collection parsers, parse and validate collection 

892 by parsing its items one-by-one. 

893 

894 :param value: 

895 collection of values to parse. 

896 :returns: 

897 each value parsed and assembled into the target collection. 

898 :raises: 

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

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

901 of objects. 

902 :example: 

903 :: 

904 

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

906 >>> parser = Set(Int()) 

907 

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

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

910 

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

912 >>> parser.parse_many(user_input) 

913 {1, 2, 3} 

914 

915 """ 

916 

917 return self.parse_many_with_ctx( 

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

919 ) 

920 

921 @abc.abstractmethod 

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

923 """ 

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

925 instead of a raw strings. 

926 

927 :param ctxs: 

928 values to parse, wrapped into a parsing contexts. 

929 :returns: 

930 a parsed and processed value. 

931 :raises: 

932 :class:`ParsingError`. 

933 

934 """ 

935 

936 raise NotImplementedError() 

937 

938 @abc.abstractmethod 

939 def supports_parse_many(self) -> bool: 

940 """ 

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

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

943 

944 :returns: 

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

946 

947 """ 

948 

949 raise NotImplementedError() 

950 

951 @_t.final 

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

953 """ 

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

955 

956 This method accepts python values that would result from 

957 parsing json, yaml, and similar formats. 

958 

959 :param value: 

960 config value to parse. 

961 :returns: 

962 verified and processed config value. 

963 :raises: 

964 :class:`ParsingError`. 

965 :example: 

966 :: 

967 

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

969 >>> parser = Set(Int()) 

970 

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

972 >>> import json 

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

974 

975 >>> # We can process parsed json: 

976 >>> parser.parse_config(user_config) 

977 {1, 2, 3} 

978 

979 """ 

980 

981 return self.parse_config_with_ctx(ConfigParsingContext(value)) 

982 

983 @abc.abstractmethod 

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

985 """ 

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

987 instead of a raw value. 

988 

989 :param ctx: 

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

991 :returns: 

992 verified and processed config value. 

993 :raises: 

994 :class:`ParsingError`. 

995 

996 """ 

997 

998 raise NotImplementedError() 

999 

1000 @abc.abstractmethod 

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

1002 """ 

1003 Generate ``nargs`` for argparse. 

1004 

1005 :returns: 

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

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

1008 value should be ``1``. 

1009 

1010 """ 

1011 

1012 raise NotImplementedError() 

1013 

1014 @abc.abstractmethod 

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

1016 """ 

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

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

1019 

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

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

1022 to dispatch values to correct sub-parsers. 

1023 

1024 .. note:: 

1025 

1026 For performance reasons, this method should not inspect contents 

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

1028 to quadratic). 

1029 

1030 This also means that validating and mapping parsers 

1031 can always return :data:`True`. 

1032 

1033 :param value: 

1034 value that needs a type check. 

1035 :returns: 

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

1037 

1038 """ 

1039 

1040 raise NotImplementedError() 

1041 

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

1043 """ 

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

1045 if it returns :data:`False`. 

1046 

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

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

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

1050 

1051 :param value: 

1052 value that needs a type check. 

1053 :returns: 

1054 always returns :data:`True`. 

1055 :raises: 

1056 :class:`TypeError`. 

1057 

1058 """ 

1059 

1060 if not self.check_type(value): 

1061 raise TypeError( 

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

1063 ) 

1064 return True 

1065 

1066 @abc.abstractmethod 

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

1068 """ 

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

1070 

1071 Used to describe expected input in widgets. 

1072 

1073 :returns: 

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

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

1076 

1077 """ 

1078 

1079 raise NotImplementedError() 

1080 

1081 @abc.abstractmethod 

1082 def describe_or_def(self) -> str: 

1083 """ 

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

1085 

1086 Used to describe expected input in CLI help. 

1087 

1088 :returns: 

1089 human-readable description of an expected input. 

1090 

1091 """ 

1092 

1093 raise NotImplementedError() 

1094 

1095 @abc.abstractmethod 

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

1097 """ 

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

1099 

1100 Used to describe expected input in CLI help. 

1101 

1102 :returns: 

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

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

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

1106 at the corresponding position. 

1107 :raises: 

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

1109 that doesn't supports parsing collections of objects. 

1110 

1111 """ 

1112 

1113 raise NotImplementedError() 

1114 

1115 @abc.abstractmethod 

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

1117 """ 

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

1119 

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

1121 

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

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

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

1125 

1126 :param value: 

1127 value that needs a description. 

1128 :returns: 

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

1130 in a CLI argument or an environment variable. 

1131 :raises: 

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

1133 that this parser produces. 

1134 

1135 """ 

1136 

1137 raise NotImplementedError() 

1138 

1139 @abc.abstractmethod 

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

1141 """ 

1142 Return options for a :class:`~yuio.widget.Multiselect` widget. 

1143 

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

1145 of pre-defined values, like :class:`Enum` or :class:`OneOf`. 

1146 Collection parsers may use this data to improve their widgets. 

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

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

1149 

1150 :returns: 

1151 a full list of options that will be passed to 

1152 a :class:`~yuio.widget.Multiselect` widget, or :data:`None` 

1153 if the set of possible values is not known. 

1154 

1155 """ 

1156 

1157 raise NotImplementedError() 

1158 

1159 @abc.abstractmethod 

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

1161 """ 

1162 Return a completer for values of this parser. 

1163 

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

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

1166 

1167 :returns: 

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

1169 

1170 """ 

1171 

1172 raise NotImplementedError() 

1173 

1174 @abc.abstractmethod 

1175 def widget( 

1176 self, 

1177 default: object | yuio.Missing, 

1178 input_description: str | None, 

1179 default_description: str | None, 

1180 /, 

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

1182 """ 

1183 Return a widget for reading values of this parser. 

1184 

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

1186 

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

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

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

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

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

1192 

1193 Validating parsers must wrap the widget they got from 

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

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

1196 

1197 :param default: 

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

1199 :param input_description: 

1200 a string describing what input is expected. 

1201 :param default_description: 

1202 a string describing default value. 

1203 :returns: 

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

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

1206 some custom logic. 

1207 

1208 """ 

1209 

1210 raise NotImplementedError() 

1211 

1212 @abc.abstractmethod 

1213 def to_json_schema( 

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

1215 ) -> yuio.json_schema.JsonSchemaType: 

1216 """ 

1217 Create a JSON schema object based on this parser. 

1218 

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

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

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

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

1223 

1224 :param ctx: 

1225 context for building a schema. 

1226 :returns: 

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

1228 

1229 """ 

1230 

1231 raise NotImplementedError() 

1232 

1233 @abc.abstractmethod 

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

1235 """ 

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

1237 

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

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

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

1241 

1242 :returns: 

1243 a value converted to JSON-serializable representation. 

1244 :raises: 

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

1246 that this parser produces. 

1247 

1248 """ 

1249 

1250 raise NotImplementedError() 

1251 

1252 @abc.abstractmethod 

1253 def is_secret(self) -> bool: 

1254 """ 

1255 Indicates that input functions should use secret input, 

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

1257 

1258 """ 

1259 

1260 raise NotImplementedError() 

1261 

1262 def __repr__(self): 

1263 return self.__class__.__name__ 

1264 

1265 

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

1267 """ 

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

1269 

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

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

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

1273 

1274 :param ty: 

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

1276 :example: 

1277 .. invisible-code-block: python 

1278 

1279 from dataclasses import dataclass 

1280 @dataclass 

1281 class MyType: 

1282 data: str 

1283 

1284 .. code-block:: python 

1285 

1286 class MyTypeParser(ValueParser[MyType]): 

1287 def __init__(self): 

1288 super().__init__(MyType) 

1289 

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

1291 return MyType(ctx.value) 

1292 

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

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

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

1296 return MyType(ctx.value) 

1297 

1298 def to_json_schema( 

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

1300 ) -> yuio.json_schema.JsonSchemaType: 

1301 return yuio.json_schema.String() 

1302 

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

1304 assert self.assert_type(value) 

1305 return value.data 

1306 

1307 :: 

1308 

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

1310 MyType(data='pancake') 

1311 

1312 """ 

1313 

1314 def __init__(self, ty: type[T], /, *args, **kwargs) -> types.NoneType: 

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

1316 

1317 self._value_type = ty 

1318 """ 

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

1320 

1321 """ 

1322 

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

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

1325 if typehint is None: 

1326 with self._patch_stack_summary(): 

1327 raise TypeError( 

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

1329 " all previous annotations. Make sure that" 

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

1331 " your type hint.\n\n" 

1332 "Example:\n" 

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

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

1335 " ^^^^^\n" 

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

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

1338 " ^^^^^" 

1339 ) 

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

1341 with self._patch_stack_summary(): 

1342 raise TypeError( 

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

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

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

1346 "Example:\n" 

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

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

1349 " ^^^^^^^^^^^^^^^^^^^^^^\n" 

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

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

1352 " ^^^^^^^^^^^^^^^^^^^^^^" 

1353 ) 

1354 return self 

1355 

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

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

1358 

1359 def supports_parse_many(self) -> bool: 

1360 return False 

1361 

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

1363 return 1 

1364 

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

1366 return isinstance(value, self._value_type) 

1367 

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

1369 return None 

1370 

1371 def describe_or_def(self) -> str: 

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

1373 

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

1375 return self.describe_or_def() 

1376 

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

1378 assert self.assert_type(value) 

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

1380 

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

1382 return None 

1383 

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

1385 return None 

1386 

1387 def widget( 

1388 self, 

1389 default: object | yuio.Missing, 

1390 input_description: str | None, 

1391 default_description: str | None, 

1392 /, 

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

1394 completer = self.completer() 

1395 return _WidgetResultMapper( 

1396 self, 

1397 input_description, 

1398 default, 

1399 ( 

1400 yuio.widget.InputWithCompletion( 

1401 completer, 

1402 placeholder=default_description or "", 

1403 ) 

1404 if completer is not None 

1405 else yuio.widget.Input( 

1406 placeholder=default_description or "", 

1407 ) 

1408 ), 

1409 ) 

1410 

1411 def is_secret(self) -> bool: 

1412 return False 

1413 

1414 

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

1416 """ 

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

1418 

1419 This base simplifies dealing with partial parsers. 

1420 

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

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

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

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

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

1426 in a type annotation. 

1427 

1428 .. warning:: 

1429 

1430 All descendants of this class must include appropriate type hints 

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

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

1433 

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

1435 

1436 :param inner: 

1437 inner data or :data:`None`. 

1438 

1439 """ 

1440 

1441 if TYPE_CHECKING: 

1442 

1443 @_t.overload 

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

1445 

1446 @_t.overload 

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

1448 

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

1450 

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

1452 self.__inner = inner 

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

1454 

1455 @property 

1456 def _inner(self) -> U: 

1457 """ 

1458 Internal resource wrapped by this parser. 

1459 

1460 :raises: 

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

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

1463 

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

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

1466 in type annotations. 

1467 

1468 """ 

1469 

1470 if self.__inner is None: 

1471 with self._patch_stack_summary(): 

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

1473 return self.__inner 

1474 

1475 @_inner.setter 

1476 def _inner(self, inner: U): 

1477 if self.__inner is not None: 

1478 with self._patch_stack_summary(): 

1479 raise TypeError( 

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

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

1482 "from type hint.\n\n" 

1483 "Example:\n" 

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

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

1486 " ^^^^^^^^^^^^^^^^^^^^^^^^^^\n" 

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

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

1489 " ^^^^^^^^^^^^^^^^^^^" 

1490 ) 

1491 self.__inner = inner 

1492 

1493 @property 

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

1495 """ 

1496 Unchecked access to the wrapped resource. 

1497 

1498 """ 

1499 

1500 return self.__inner 

1501 

1502 

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

1504 """ 

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

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

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

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

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

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

1511 

1512 :param inner: 

1513 mapped parser or :data:`None`. 

1514 

1515 """ 

1516 

1517 if TYPE_CHECKING: 

1518 

1519 @_t.overload 

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

1521 

1522 @_t.overload 

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

1524 

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

1526 

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

1528 super().__init__(inner) 

1529 

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

1531 self._inner = parser 

1532 return self 

1533 

1534 def supports_parse_many(self) -> bool: 

1535 return self._inner.supports_parse_many() 

1536 

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

1538 return self._inner.get_nargs() 

1539 

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

1541 return self._inner.describe() 

1542 

1543 def describe_or_def(self) -> str: 

1544 return self._inner.describe_or_def() 

1545 

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

1547 return self._inner.describe_many() 

1548 

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

1550 return self._inner.completer() 

1551 

1552 def to_json_schema( 

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

1554 ) -> yuio.json_schema.JsonSchemaType: 

1555 return self._inner.to_json_schema(ctx) 

1556 

1557 def is_secret(self) -> bool: 

1558 return self._inner.is_secret() 

1559 

1560 def __repr__(self): 

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

1562 

1563 @property 

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

1565 return self._inner_raw 

1566 

1567 

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

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

1570 

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

1572 

1573 :param inner: 

1574 a parser whose result will be mapped. 

1575 :param fn: 

1576 a function to convert a result. 

1577 :param rev: 

1578 a function used to un-map a value. 

1579 

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

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

1582 to its original state. 

1583 

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

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

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

1587 :example: 

1588 .. 

1589 >>> import math 

1590 

1591 :: 

1592 

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

1594 ... yuio.parse.Int(), 

1595 ... lambda x: 2 ** x, 

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

1597 ... ) 

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

1599 1024 

1600 >>> parser.describe_value(1024) 

1601 '10' 

1602 

1603 """ 

1604 

1605 if TYPE_CHECKING: 

1606 

1607 @_t.overload 

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

1609 

1610 @_t.overload 

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

1612 

1613 @_t.overload 

1614 def __new__( 

1615 cls, 

1616 inner: Parser[U], 

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

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

1619 /, 

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

1621 

1622 @_t.overload 

1623 def __new__( 

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

1625 ) -> PartialParser: ... 

1626 

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

1628 

1629 def __init__(self, *args): 

1630 inner: Parser[U] | None = None 

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

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

1633 if len(args) == 1: 

1634 (fn,) = args 

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

1636 inner, fn = args 

1637 elif len(args) == 2: 

1638 fn, rev = args 

1639 elif len(args) == 3: 

1640 inner, fn, rev = args 

1641 else: 

1642 raise TypeError( 

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

1644 ) 

1645 

1646 self._fn = fn 

1647 self._rev = rev 

1648 super().__init__(inner) 

1649 

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

1651 res = self._inner.parse_with_ctx(ctx) 

1652 try: 

1653 return self._fn(res) 

1654 except ParsingError as e: 

1655 e.set_ctx(ctx) 

1656 raise 

1657 

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

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

1660 

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

1662 res = self._inner.parse_config_with_ctx(ctx) 

1663 try: 

1664 return self._fn(res) 

1665 except ParsingError as e: 

1666 e.set_ctx(ctx) 

1667 raise 

1668 

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

1670 return True 

1671 

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

1673 if self._rev: 

1674 value = self._rev(value) 

1675 return self._inner.describe_value(value) 

1676 

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

1678 options = self._inner.options() 

1679 if options is not None: 

1680 return [ 

1681 _t.cast( 

1682 yuio.widget.Option[T], 

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

1684 ) 

1685 for option in options 

1686 ] 

1687 else: 

1688 return None 

1689 

1690 def widget( 

1691 self, 

1692 default: object | yuio.Missing, 

1693 input_description: str | None, 

1694 default_description: str | None, 

1695 /, 

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

1697 return yuio.widget.Map( 

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

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

1700 ) 

1701 

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

1703 if self._rev: 

1704 value = self._rev(value) 

1705 return self._inner.to_json_value(value) 

1706 

1707 

1708@_t.overload 

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

1710@_t.overload 

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

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

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

1714 

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

1716 

1717 :param inner: 

1718 a parser whose result will be mapped. 

1719 

1720 """ 

1721 

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

1723 

1724 

1725@_t.overload 

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

1727@_t.overload 

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

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

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

1731 

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

1733 

1734 :param inner: 

1735 a parser whose result will be mapped. 

1736 

1737 """ 

1738 

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

1740 

1741 

1742@_t.overload 

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

1744@_t.overload 

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

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

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

1748 

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

1750 

1751 :param inner: 

1752 a parser whose result will be mapped. 

1753 

1754 """ 

1755 

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

1757 

1758 

1759@_t.overload 

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

1761@_t.overload 

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

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

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

1765 

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

1767 

1768 :param inner: 

1769 a parser whose result will be mapped. 

1770 

1771 """ 

1772 

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

1774 

1775 

1776@_t.overload 

1777def Regex( 

1778 inner: Parser[str], 

1779 regex: str | _tx.StrRePattern, 

1780 /, 

1781 *, 

1782 group: int | str = 0, 

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

1784@_t.overload 

1785def Regex( 

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

1787) -> PartialParser: ... 

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

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

1790 

1791 Matches the parsed string with the given regular expression. 

1792 

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

1794 

1795 :param regex: 

1796 regular expression for matching. 

1797 :param group: 

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

1799 parsed value. 

1800 

1801 """ 

1802 

1803 inner: Parser[str] | None 

1804 regex: str | _tx.StrRePattern 

1805 if len(args) == 1: 

1806 inner, regex = None, args[0] 

1807 elif len(args) == 2: 

1808 inner, regex = args 

1809 else: 

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

1811 

1812 if isinstance(regex, re.Pattern): 

1813 compiled = regex 

1814 else: 

1815 compiled = re.compile(regex) 

1816 

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

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

1819 raise ParsingError( 

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

1821 compiled.pattern, 

1822 value, 

1823 fallback_msg="Incorrect value format", 

1824 ) 

1825 return match.group(group) 

1826 

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

1828 

1829 

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

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

1832 

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

1834 

1835 :param inner: 

1836 a parser used to extract and validate a value. 

1837 :param fn: 

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

1839 :example: 

1840 :: 

1841 

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

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

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

1845 Value is 10 

1846 >>> result 

1847 10 

1848 

1849 """ 

1850 

1851 if TYPE_CHECKING: 

1852 

1853 @_t.overload 

1854 def __new__( 

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

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

1857 

1858 @_t.overload 

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

1860 

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

1862 

1863 def __init__(self, *args): 

1864 inner: Parser[T] | None 

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

1866 if len(args) == 1: 

1867 inner, fn = None, args[0] 

1868 elif len(args) == 2: 

1869 inner, fn = args 

1870 else: 

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

1872 

1873 self._fn = fn 

1874 super().__init__(inner) 

1875 

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

1877 result = self._inner.parse_with_ctx(ctx) 

1878 try: 

1879 self._fn(result) 

1880 except ParsingError as e: 

1881 e.set_ctx(ctx) 

1882 raise 

1883 return result 

1884 

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

1886 result = self._inner.parse_many_with_ctx(ctxs) 

1887 self._fn(result) 

1888 return result 

1889 

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

1891 result = self._inner.parse_config_with_ctx(ctx) 

1892 try: 

1893 self._fn(result) 

1894 except ParsingError as e: 

1895 e.set_ctx(ctx) 

1896 raise 

1897 return result 

1898 

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

1900 return True 

1901 

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

1903 return self._inner.describe_value(value) 

1904 

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

1906 return self._inner.options() 

1907 

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

1909 return self._inner.completer() 

1910 

1911 def widget( 

1912 self, 

1913 default: object | yuio.Missing, 

1914 input_description: str | None, 

1915 default_description: str | None, 

1916 /, 

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

1918 return yuio.widget.Apply( 

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

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

1921 ) 

1922 

1923 def to_json_schema( 

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

1925 ) -> yuio.json_schema.JsonSchemaType: 

1926 return self._inner.to_json_schema(ctx) 

1927 

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

1929 return self._inner.to_json_value(value) 

1930 

1931 

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

1933 """ 

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

1935 

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

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

1938 

1939 :param inner: 

1940 a parser which output will be validated. 

1941 :example: 

1942 .. code-block:: python 

1943 

1944 class IsLower(ValidatingParser[str]): 

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

1946 if not value.islower(): 

1947 raise ParsingError( 

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

1949 value, 

1950 fallback_msg="Value should be lowercase", 

1951 ) 

1952 

1953 :: 

1954 

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

1956 Traceback (most recent call last): 

1957 ... 

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

1959 

1960 """ 

1961 

1962 if TYPE_CHECKING: 

1963 

1964 @_t.overload 

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

1966 

1967 @_t.overload 

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

1969 

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

1971 

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

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

1974 

1975 @abc.abstractmethod 

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

1977 """ 

1978 Implementation of value validation. 

1979 

1980 :param value: 

1981 value which needs validating. 

1982 :raises: 

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

1984 

1985 """ 

1986 

1987 raise NotImplementedError() 

1988 

1989 

1990class Str(ValueParser[str]): 

1991 """ 

1992 Parser for str values. 

1993 

1994 """ 

1995 

1996 def __init__(self): 

1997 super().__init__(str) 

1998 

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

2000 return str(ctx.value) 

2001 

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

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

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

2005 return str(ctx.value) 

2006 

2007 def to_json_schema( 

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

2009 ) -> yuio.json_schema.JsonSchemaType: 

2010 return yuio.json_schema.String() 

2011 

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

2013 assert self.assert_type(value) 

2014 return value 

2015 

2016 

2017class Int(ValueParser[int]): 

2018 """ 

2019 Parser for int values. 

2020 

2021 """ 

2022 

2023 def __init__(self): 

2024 super().__init__(int) 

2025 

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

2027 ctx = ctx.strip_if_non_space() 

2028 try: 

2029 value = ctx.value.casefold() 

2030 if value.startswith("-"): 

2031 neg = True 

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

2033 else: 

2034 neg = False 

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

2036 base = 16 

2037 value = value[2:] 

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

2039 base = 8 

2040 value = value[2:] 

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

2042 base = 2 

2043 value = value[2:] 

2044 else: 

2045 base = 10 

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

2047 raise ValueError() 

2048 res = int(value, base=base) 

2049 if neg: 

2050 res = -res 

2051 return res 

2052 except ValueError: 

2053 raise ParsingError( 

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

2055 ctx.value, 

2056 ctx=ctx, 

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

2058 ) from None 

2059 

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

2061 value = ctx.value 

2062 if isinstance(value, float): 

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

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

2065 value = int(value) 

2066 if not isinstance(value, int): 

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

2068 return value 

2069 

2070 def to_json_schema( 

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

2072 ) -> yuio.json_schema.JsonSchemaType: 

2073 return yuio.json_schema.Integer() 

2074 

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

2076 assert self.assert_type(value) 

2077 return value 

2078 

2079 

2080class Float(ValueParser[float]): 

2081 """ 

2082 Parser for float values. 

2083 

2084 """ 

2085 

2086 def __init__(self): 

2087 super().__init__(float) 

2088 

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

2090 ctx = ctx.strip_if_non_space() 

2091 try: 

2092 return float(ctx.value) 

2093 except ValueError: 

2094 raise ParsingError( 

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

2096 ctx.value, 

2097 ctx=ctx, 

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

2099 ) from None 

2100 

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

2102 value = ctx.value 

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

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

2105 return value 

2106 

2107 def to_json_schema( 

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

2109 ) -> yuio.json_schema.JsonSchemaType: 

2110 return yuio.json_schema.Number() 

2111 

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

2113 assert self.assert_type(value) 

2114 return value 

2115 

2116 

2117class Bool(ValueParser[bool]): 

2118 """ 

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

2120 

2121 """ 

2122 

2123 def __init__(self): 

2124 super().__init__(bool) 

2125 

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

2127 ctx = ctx.strip_if_non_space() 

2128 value = ctx.value.casefold() 

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

2130 return True 

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

2132 return False 

2133 else: 

2134 raise ParsingError( 

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

2136 value, 

2137 ctx=ctx, 

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

2139 ) 

2140 

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

2142 value = ctx.value 

2143 if not isinstance(value, bool): 

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

2145 return value 

2146 

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

2148 return "{yes|no}" 

2149 

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

2151 assert self.assert_type(value) 

2152 return "yes" if value else "no" 

2153 

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

2155 return yuio.complete.Choice( 

2156 [ 

2157 yuio.complete.Option("no"), 

2158 yuio.complete.Option("yes"), 

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

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

2161 ] 

2162 ) 

2163 

2164 def widget( 

2165 self, 

2166 default: object | yuio.Missing, 

2167 input_description: str | None, 

2168 default_description: str | None, 

2169 /, 

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

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

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

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

2174 ] 

2175 

2176 if default is yuio.MISSING: 

2177 default_index = 0 

2178 elif isinstance(default, bool): 

2179 default_index = int(default) 

2180 else: 

2181 options.append( 

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

2183 ) 

2184 default_index = 2 

2185 

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

2187 

2188 def to_json_schema( 

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

2190 ) -> yuio.json_schema.JsonSchemaType: 

2191 return yuio.json_schema.Boolean() 

2192 

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

2194 assert self.assert_type(value) 

2195 return value 

2196 

2197 

2198class Enum(WrappingParser[E, type[E]], ValueParser[E], _t.Generic[E]): 

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

2200 

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

2202 

2203 :param enum_type: 

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

2205 :param by_name: 

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

2207 their values, to match the input. 

2208 :param to_dash_case: 

2209 convert enum names/values to dash case. 

2210 :param doc_inline: 

2211 inline this enum in json schema and in documentation. 

2212 

2213 """ 

2214 

2215 if TYPE_CHECKING: 

2216 

2217 @_t.overload 

2218 def __new__( 

2219 cls, 

2220 inner: type[E], 

2221 /, 

2222 *, 

2223 by_name: bool = False, 

2224 to_dash_case: bool = False, 

2225 doc_inline: bool = False, 

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

2227 

2228 @_t.overload 

2229 def __new__( 

2230 cls, 

2231 /, 

2232 *, 

2233 by_name: bool = False, 

2234 to_dash_case: bool = False, 

2235 doc_inline: bool = False, 

2236 ) -> PartialParser: ... 

2237 

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

2239 

2240 def __init__( 

2241 self, 

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

2243 /, 

2244 *, 

2245 by_name: bool = False, 

2246 to_dash_case: bool = False, 

2247 doc_inline: bool = False, 

2248 ): 

2249 self._by_name = by_name 

2250 self._to_dash_case = to_dash_case 

2251 self._doc_inline = doc_inline 

2252 super().__init__(enum_type, enum_type) 

2253 

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

2255 result = super().wrap(parser) 

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

2257 result._value_type = parser._inner # type: ignore 

2258 return result 

2259 

2260 @functools.cached_property 

2261 def _getter(self) -> _t.Callable[[E], str]: 

2262 items = {} 

2263 for e in self._inner: 

2264 if self._by_name: 

2265 name = e.name 

2266 else: 

2267 name = str(e.value) 

2268 if self._to_dash_case: 

2269 name = _to_dash_case(name) 

2270 items[e] = name 

2271 return lambda e: items[e] 

2272 

2273 @functools.cached_property 

2274 def _docs(self) -> dict[str, str]: 

2275 return _find_docs(self._inner) 

2276 

2277 def parse_with_ctx(self, ctx: StrParsingContext, /) -> E: 

2278 ctx = ctx.strip_if_non_space() 

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

2280 

2281 def parse_config_with_ctx(self, ctx: ConfigParsingContext, /) -> E: 

2282 value = ctx.value 

2283 

2284 if isinstance(value, self._inner): 

2285 return value 

2286 

2287 if not isinstance(value, str): 

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

2289 

2290 result = self._parse(value, ctx) 

2291 

2292 if self._getter(result) != value: 

2293 raise ParsingError( 

2294 "Can't parse `%r` as `%s`, did you mean `%s`?", 

2295 value, 

2296 self._inner.__name__, 

2297 self._getter(result), 

2298 ctx=ctx, 

2299 ) 

2300 

2301 return result 

2302 

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

2304 cf_value = value.strip().casefold() 

2305 

2306 candidates: list[E] = [] 

2307 for item in self._inner: 

2308 if self._getter(item) == value: 

2309 return item 

2310 elif (self._getter(item)).casefold().startswith(cf_value): 

2311 candidates.append(item) 

2312 

2313 if len(candidates) == 1: 

2314 return candidates[0] 

2315 elif len(candidates) > 1: 

2316 enum_values = tuple(self._getter(e) for e in candidates) 

2317 raise ParsingError( 

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

2319 value, 

2320 self._inner.__name__, 

2321 yuio.string.Or(enum_values), 

2322 ctx=ctx, 

2323 ) 

2324 else: 

2325 enum_values = tuple(self._getter(e) for e in self._inner) 

2326 raise ParsingError( 

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

2328 value, 

2329 self._inner.__name__, 

2330 yuio.string.Or(enum_values), 

2331 ctx=ctx, 

2332 ) 

2333 

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

2335 desc = "|".join(self._getter(e) for e in self._inner) 

2336 if len(self._inner) > 1: 

2337 desc = f"{{{desc}}}" 

2338 return desc 

2339 

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

2341 return self.describe_or_def() 

2342 

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

2344 assert self.assert_type(value) 

2345 return str(self._getter(value)) 

2346 

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

2348 docs = self._docs 

2349 return [ 

2350 yuio.widget.Option( 

2351 e, display_text=self._getter(e), comment=docs.get(e.name) 

2352 ) 

2353 for e in self._inner 

2354 ] 

2355 

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

2357 docs = self._docs 

2358 return yuio.complete.Choice( 

2359 [ 

2360 yuio.complete.Option(self._getter(e), comment=docs.get(e.name)) 

2361 for e in self._inner 

2362 ] 

2363 ) 

2364 

2365 def widget( 

2366 self, 

2367 default: object | yuio.Missing, 

2368 input_description: str | None, 

2369 default_description: str | None, 

2370 /, 

2371 ) -> yuio.widget.Widget[E | yuio.Missing]: 

2372 options: list[yuio.widget.Option[E | yuio.Missing]] = list(self.options()) 

2373 

2374 if default is yuio.MISSING: 

2375 default_index = 0 

2376 elif isinstance(default, self._inner): 

2377 default_index = list(self._inner).index(default) 

2378 else: 

2379 options.insert( 

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

2381 ) 

2382 default_index = 0 

2383 

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

2385 

2386 def to_json_schema( 

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

2388 ) -> yuio.json_schema.JsonSchemaType: 

2389 items = [self._getter(e) for e in self._inner] 

2390 docs = self._docs 

2391 descriptions = [docs.get(e.name) for e in self._inner] 

2392 if not any(descriptions): 

2393 descriptions = None 

2394 if self._doc_inline: 

2395 return yuio.json_schema.Enum(items, descriptions) 

2396 else: 

2397 return ctx.add_type( 

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

2399 _tx.type_repr(self._inner), 

2400 lambda: yuio.json_schema.Meta( 

2401 yuio.json_schema.Enum(items, descriptions), 

2402 title=self._inner.__name__, 

2403 description=self._inner.__doc__, 

2404 ), 

2405 ) 

2406 

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

2408 assert self.assert_type(value) 

2409 return self._getter(value) 

2410 

2411 def __repr__(self): 

2412 if self._inner_raw is not None: 

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

2414 else: 

2415 return self.__class__.__name__ 

2416 

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

2418 class _TyWrapper: 

2419 inner: type 

2420 by_name: bool 

2421 to_dash_case: bool 

2422 

2423 

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

2425 """ 

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

2427 

2428 """ 

2429 

2430 def __init__(self): 

2431 super().__init__(decimal.Decimal) 

2432 

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

2434 ctx = ctx.strip_if_non_space() 

2435 try: 

2436 return decimal.Decimal(ctx.value) 

2437 except (ArithmeticError, ValueError, TypeError): 

2438 raise ParsingError( 

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

2440 ctx.value, 

2441 ctx=ctx, 

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

2443 ) from None 

2444 

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

2446 value = ctx.value 

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

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

2449 try: 

2450 return decimal.Decimal(value) 

2451 except (ArithmeticError, ValueError, TypeError): 

2452 raise ParsingError( 

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

2454 value, 

2455 ctx=ctx, 

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

2457 ) from None 

2458 

2459 def to_json_schema( 

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

2461 ) -> yuio.json_schema.JsonSchemaType: 

2462 return ctx.add_type( 

2463 decimal.Decimal, 

2464 "Decimal", 

2465 lambda: yuio.json_schema.Meta( 

2466 yuio.json_schema.OneOf( 

2467 [ 

2468 yuio.json_schema.Number(), 

2469 yuio.json_schema.String( 

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

2471 ), 

2472 ] 

2473 ), 

2474 title="Decimal", 

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

2476 ), 

2477 ) 

2478 

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

2480 assert self.assert_type(value) 

2481 return str(value) 

2482 

2483 

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

2485 """ 

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

2487 

2488 """ 

2489 

2490 def __init__(self): 

2491 super().__init__(fractions.Fraction) 

2492 

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

2494 ctx = ctx.strip_if_non_space() 

2495 try: 

2496 return fractions.Fraction(ctx.value) 

2497 except ValueError: 

2498 raise ParsingError( 

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

2500 ctx.value, 

2501 ctx=ctx, 

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

2503 ) from None 

2504 except ZeroDivisionError: 

2505 raise ParsingError( 

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

2507 ctx.value, 

2508 ctx=ctx, 

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

2510 ) from None 

2511 

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

2513 value = ctx.value 

2514 if ( 

2515 isinstance(value, (list, tuple)) 

2516 and len(value) == 2 

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

2518 ): 

2519 try: 

2520 return fractions.Fraction(*value) 

2521 except (ValueError, TypeError): 

2522 raise ParsingError( 

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

2524 value[0], 

2525 value[1], 

2526 ctx=ctx, 

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

2528 ) from None 

2529 except ZeroDivisionError: 

2530 raise ParsingError( 

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

2532 value[0], 

2533 value[1], 

2534 ctx=ctx, 

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

2536 ) from None 

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

2538 try: 

2539 return fractions.Fraction(value) 

2540 except (ValueError, TypeError): 

2541 raise ParsingError( 

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

2543 value, 

2544 ctx=ctx, 

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

2546 ) from None 

2547 except ZeroDivisionError: 

2548 raise ParsingError( 

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

2550 value, 

2551 ctx=ctx, 

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

2553 ) from None 

2554 raise ParsingError.type_mismatch( 

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

2556 ) 

2557 

2558 def to_json_schema( 

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

2560 ) -> yuio.json_schema.JsonSchemaType: 

2561 return ctx.add_type( 

2562 fractions.Fraction, 

2563 "Fraction", 

2564 lambda: yuio.json_schema.Meta( 

2565 yuio.json_schema.OneOf( 

2566 [ 

2567 yuio.json_schema.Number(), 

2568 yuio.json_schema.String( 

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

2570 ), 

2571 yuio.json_schema.Tuple( 

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

2573 ), 

2574 ] 

2575 ), 

2576 title="Fraction", 

2577 description="A rational number.", 

2578 ), 

2579 ) 

2580 

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

2582 assert self.assert_type(value) 

2583 return str(value) 

2584 

2585 

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

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

2588 

2589 A parser that tries to parse value as JSON. 

2590 

2591 This parser will load JSON strings into python objects. 

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

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

2594 

2595 :param inner: 

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

2597 

2598 """ 

2599 

2600 if TYPE_CHECKING: 

2601 

2602 @_t.overload 

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

2604 

2605 @_t.overload 

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

2607 

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

2609 

2610 def __init__( 

2611 self, 

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

2613 /, 

2614 ): 

2615 super().__init__(inner, object) 

2616 

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

2618 self._inner = parser 

2619 return self 

2620 

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

2622 ctx = ctx.strip_if_non_space() 

2623 try: 

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

2625 except json.JSONDecodeError as e: 

2626 raise ParsingError( 

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

2628 ctx.value, 

2629 yuio.string.Indent(e), 

2630 ctx=ctx, 

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

2632 ) from None 

2633 try: 

2634 return self.parse_config_with_ctx(ConfigParsingContext(config_value)) 

2635 except ParsingError as e: 

2636 raise ParsingError( 

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

2638 yuio.string.Indent(e), 

2639 ctx=ctx, 

2640 fallback_msg="Error in parsed json value", 

2641 ) from None 

2642 

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

2644 if self._inner_raw is not None: 

2645 return self._inner_raw.parse_config_with_ctx(ctx) 

2646 else: 

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

2648 

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

2650 return True 

2651 

2652 def to_json_schema( 

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

2654 ) -> yuio.json_schema.JsonSchemaType: 

2655 if self._inner_raw is not None: 

2656 return self._inner_raw.to_json_schema(ctx) 

2657 else: 

2658 return yuio.json_schema.Any() 

2659 

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

2661 assert self.assert_type(value) 

2662 if self._inner_raw is not None: 

2663 return self._inner_raw.to_json_value(value) 

2664 return value # type: ignore 

2665 

2666 def __repr__(self): 

2667 if self._inner_raw is not None: 

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

2669 else: 

2670 return super().__repr__() 

2671 

2672 

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

2674 """ 

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

2676 

2677 """ 

2678 

2679 def __init__(self): 

2680 super().__init__(datetime.datetime) 

2681 

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

2683 ctx = ctx.strip_if_non_space() 

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

2685 

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

2687 value = ctx.value 

2688 if isinstance(value, datetime.datetime): 

2689 return value 

2690 elif isinstance(value, str): 

2691 return self._parse(value, ctx) 

2692 else: 

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

2694 

2695 @staticmethod 

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

2697 try: 

2698 return datetime.datetime.fromisoformat(value) 

2699 except ValueError: 

2700 raise ParsingError( 

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

2702 value, 

2703 ctx=ctx, 

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

2705 ) from None 

2706 

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

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

2709 

2710 def to_json_schema( 

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

2712 ) -> yuio.json_schema.JsonSchemaType: 

2713 return ctx.add_type( 

2714 datetime.datetime, 

2715 "DateTime", 

2716 lambda: yuio.json_schema.Meta( 

2717 yuio.json_schema.String( 

2718 pattern=( 

2719 r"^" 

2720 r"(" 

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

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

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

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

2725 r")" 

2726 r"(" 

2727 r"[T ]" 

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

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

2730 r")?" 

2731 r"$" 

2732 ) 

2733 ), 

2734 title="DateTime", 

2735 description="ISO 8601 datetime.", 

2736 ), 

2737 ) 

2738 

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

2740 assert self.assert_type(value) 

2741 return str(value) 

2742 

2743 

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

2745 """ 

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

2747 

2748 """ 

2749 

2750 def __init__(self): 

2751 super().__init__(datetime.date) 

2752 

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

2754 ctx = ctx.strip_if_non_space() 

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

2756 

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

2758 value = ctx.value 

2759 if isinstance(value, datetime.datetime): 

2760 return value.date() 

2761 elif isinstance(value, datetime.date): 

2762 return value 

2763 elif isinstance(value, str): 

2764 return self._parse(value, ctx) 

2765 else: 

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

2767 

2768 @staticmethod 

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

2770 try: 

2771 return datetime.date.fromisoformat(value) 

2772 except ValueError: 

2773 raise ParsingError( 

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

2775 value, 

2776 ctx=ctx, 

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

2778 ) from None 

2779 

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

2781 return "YYYY-MM-DD" 

2782 

2783 def to_json_schema( 

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

2785 ) -> yuio.json_schema.JsonSchemaType: 

2786 return ctx.add_type( 

2787 datetime.date, 

2788 "Date", 

2789 lambda: yuio.json_schema.Meta( 

2790 yuio.json_schema.String( 

2791 pattern=( 

2792 r"^" 

2793 r"(" 

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

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

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

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

2798 r")" 

2799 r"$" 

2800 ) 

2801 ), 

2802 title="Date", 

2803 description="ISO 8601 date.", 

2804 ), 

2805 ) 

2806 

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

2808 assert self.assert_type(value) 

2809 return str(value) 

2810 

2811 

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

2813 """ 

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

2815 

2816 """ 

2817 

2818 def __init__(self): 

2819 super().__init__(datetime.time) 

2820 

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

2822 ctx = ctx.strip_if_non_space() 

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

2824 

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

2826 value = ctx.value 

2827 if isinstance(value, datetime.datetime): 

2828 return value.time() 

2829 elif isinstance(value, datetime.time): 

2830 return value 

2831 elif isinstance(value, str): 

2832 return self._parse(value, ctx) 

2833 else: 

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

2835 

2836 @staticmethod 

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

2838 try: 

2839 return datetime.time.fromisoformat(value) 

2840 except ValueError: 

2841 raise ParsingError( 

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

2843 value, 

2844 ctx=ctx, 

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

2846 ) from None 

2847 

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

2849 return "HH:MM:SS" 

2850 

2851 def to_json_schema( 

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

2853 ) -> yuio.json_schema.JsonSchemaType: 

2854 return ctx.add_type( 

2855 datetime.time, 

2856 "Time", 

2857 lambda: yuio.json_schema.Meta( 

2858 yuio.json_schema.String( 

2859 pattern=( 

2860 r"^" 

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

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

2863 r"$" 

2864 ) 

2865 ), 

2866 title="Time", 

2867 description="ISO 8601 time.", 

2868 ), 

2869 ) 

2870 

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

2872 assert self.assert_type(value) 

2873 return str(value) 

2874 

2875 

2876_UNITS_MAP = ( 

2877 ("days", ("d", "day", "days")), 

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

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

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

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

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

2883 ("weeks", ("w", "week", "weeks")), 

2884) 

2885 

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

2887 

2888_TIMEDELTA_RE = re.compile( 

2889 r""" 

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

2891 ^ 

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

2893 (?:,\s*)? 

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

2895 $ 

2896 """, 

2897 re.VERBOSE | re.IGNORECASE, 

2898) 

2899 

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

2901 

2902 

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

2904 """ 

2905 Parse a time delta. 

2906 

2907 """ 

2908 

2909 def __init__(self): 

2910 super().__init__(datetime.timedelta) 

2911 

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

2913 ctx = ctx.strip_if_non_space() 

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

2915 

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

2917 value = ctx.value 

2918 if isinstance(value, datetime.timedelta): 

2919 return value 

2920 elif isinstance(value, str): 

2921 return self._parse(value, ctx) 

2922 else: 

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

2924 

2925 @staticmethod 

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

2927 value = value.strip() 

2928 

2929 if not value: 

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

2931 if value.endswith(","): 

2932 raise ParsingError( 

2933 "Can't parse `%r` as `timedelta`, trailing coma is not allowed", 

2934 value, 

2935 ctx=ctx, 

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

2937 ) 

2938 if value.startswith(","): 

2939 raise ParsingError( 

2940 "Can't parse `%r` as `timedelta`, leading coma is not allowed", 

2941 value, 

2942 ctx=ctx, 

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

2944 ) 

2945 

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

2947 ( 

2948 c_sign_s, 

2949 components_s, 

2950 t_sign_s, 

2951 hour, 

2952 minute, 

2953 second, 

2954 millisecond, 

2955 microsecond, 

2956 ) = match.groups() 

2957 else: 

2958 raise ParsingError( 

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

2960 value, 

2961 ctx=ctx, 

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

2963 ) 

2964 

2965 c_sign_s = -1 if c_sign_s == "-" else 1 

2966 t_sign_s = -1 if t_sign_s == "-" else 1 

2967 

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

2969 

2970 if components_s: 

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

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

2973 kwargs[unit_key] += int(num) 

2974 else: 

2975 raise ParsingError( 

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

2977 value, 

2978 unit, 

2979 ctx=ctx, 

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

2981 ) 

2982 

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

2984 

2985 timedelta += t_sign_s * datetime.timedelta( 

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

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

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

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

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

2991 ) 

2992 

2993 return timedelta 

2994 

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

2996 return "[+|-]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.date, 

3003 "TimeDelta", 

3004 lambda: yuio.json_schema.Meta( 

3005 yuio.json_schema.String( 

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

3007 pattern=( 

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

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

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

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

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

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

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

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

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

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

3018 ) 

3019 ), 

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

3021 description=".", 

3022 ), 

3023 ) 

3024 

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

3026 assert self.assert_type(value) 

3027 return str(value) 

3028 

3029 

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

3031 """ 

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

3033 

3034 :param extensions: 

3035 list of allowed file extensions, including preceding dots. 

3036 

3037 """ 

3038 

3039 def __init__( 

3040 self, 

3041 /, 

3042 *, 

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

3044 ): 

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

3046 super().__init__(pathlib.Path) 

3047 

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

3049 ctx = ctx.strip_if_non_space() 

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

3051 

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

3053 value = ctx.value 

3054 if not isinstance(value, str): 

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

3056 return self._parse(value, ctx) 

3057 

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

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

3060 try: 

3061 self._validate(res) 

3062 except ParsingError as e: 

3063 e.set_ctx(ctx) 

3064 raise 

3065 return res 

3066 

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

3068 if self._extensions is not None: 

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

3070 if len(self._extensions) > 1: 

3071 desc = f"{{{desc}}}" 

3072 return desc 

3073 else: 

3074 return super().describe() 

3075 

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

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

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

3079 ): 

3080 raise ParsingError( 

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

3082 value, 

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

3084 ) 

3085 

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

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

3088 

3089 def to_json_schema( 

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

3091 ) -> yuio.json_schema.JsonSchemaType: 

3092 return yuio.json_schema.String() 

3093 

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

3095 assert self.assert_type(value) 

3096 return str(value) 

3097 

3098 

3099class NonExistentPath(Path): 

3100 """ 

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

3102 

3103 :param extensions: 

3104 list of allowed file extensions, including preceding dots. 

3105 

3106 """ 

3107 

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

3109 super()._validate(value) 

3110 

3111 if value.exists(): 

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

3113 

3114 

3115class ExistingPath(Path): 

3116 """ 

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

3118 

3119 :param extensions: 

3120 list of allowed file extensions, including preceding dots. 

3121 

3122 """ 

3123 

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

3125 super()._validate(value) 

3126 

3127 if not value.exists(): 

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

3129 

3130 

3131class File(ExistingPath): 

3132 """ 

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

3134 

3135 :param extensions: 

3136 list of allowed file extensions, including preceding dots. 

3137 

3138 """ 

3139 

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

3141 super()._validate(value) 

3142 

3143 if not value.is_file(): 

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

3145 

3146 

3147class Dir(ExistingPath): 

3148 """ 

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

3150 

3151 """ 

3152 

3153 def __init__(self): 

3154 # Disallow passing `extensions`. 

3155 super().__init__() 

3156 

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

3158 super()._validate(value) 

3159 

3160 if not value.is_dir(): 

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

3162 

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

3164 return yuio.complete.Dir() 

3165 

3166 

3167class GitRepo(Dir): 

3168 """ 

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

3170 

3171 This parser just checks that the given directory has 

3172 a subdirectory named ``.git``. 

3173 

3174 """ 

3175 

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

3177 super()._validate(value) 

3178 

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

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

3181 

3182 

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

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

3185 

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

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

3188 

3189 """ 

3190 

3191 if TYPE_CHECKING: 

3192 

3193 @_t.overload 

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

3195 

3196 @_t.overload 

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

3198 

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

3200 

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

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

3203 

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

3205 with self._replace_error(): 

3206 return super().parse_with_ctx(ctx) 

3207 

3208 def parse_many_with_ctx( 

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

3210 ) -> SecretValue[T]: 

3211 with self._replace_error(): 

3212 return super().parse_many_with_ctx(ctxs) 

3213 

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

3215 with self._replace_error(): 

3216 return super().parse_config_with_ctx(ctx) 

3217 

3218 @staticmethod 

3219 @contextlib.contextmanager 

3220 def _replace_error(): 

3221 try: 

3222 yield 

3223 except ParsingError as e: 

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

3225 raise ParsingError( 

3226 yuio.string.Printable( 

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

3228 ), 

3229 pos=e.pos, 

3230 path=e.path, 

3231 n_arg=e.n_arg, 

3232 # Omit raw value. 

3233 ) from None 

3234 

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

3236 return "***" 

3237 

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

3239 return None 

3240 

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

3242 return None 

3243 

3244 def widget( 

3245 self, 

3246 default: object | yuio.Missing, 

3247 input_description: str | None, 

3248 default_description: str | None, 

3249 /, 

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

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

3252 

3253 def is_secret(self) -> bool: 

3254 return True 

3255 

3256 

3257class CollectionParser( 

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

3259): 

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

3261 

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

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

3264 the result to the given constructor. 

3265 

3266 :param inner: 

3267 parser that will be used to parse collection items. 

3268 :param ty: 

3269 type of the collection that this parser returns. 

3270 :param ctor: 

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

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

3273 :param iter: 

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

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

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

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

3278 :param config_type: 

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

3280 This will usually be a list. 

3281 :param config_type_iter: 

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

3283 :param delimiter: 

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

3285 

3286 The above parameters are exposed via protected attributes: 

3287 ``self._inner``, ``self._ty``, etc. 

3288 

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

3290 that repeats each element twice: 

3291 

3292 .. code-block:: python 

3293 

3294 from typing import Iterable, Generic 

3295 

3296 

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

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

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

3300 

3301 @staticmethod 

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

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

3304 

3305 def to_json_schema( 

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

3307 ) -> yuio.json_schema.JsonSchemaType: 

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

3309 

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

3311 assert self.assert_type(value) 

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

3313 

3314 :: 

3315 

3316 >>> parser = DoubleList(Int()) 

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

3318 [1, 1, 2, 2, 3, 3] 

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

3320 [1, 2, 3] 

3321 

3322 """ 

3323 

3324 _allow_completing_duplicates: typing.ClassVar[bool] = True 

3325 """ 

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

3327 

3328 """ 

3329 

3330 def __init__( 

3331 self, 

3332 inner: Parser[T] | None, 

3333 /, 

3334 *, 

3335 ty: type[C], 

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

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

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

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

3340 delimiter: str | None = None, 

3341 ): 

3342 if delimiter == "": 

3343 raise ValueError("empty delimiter") 

3344 

3345 #: See class parameters for more details. 

3346 self._ty = ty 

3347 self._ctor = ctor 

3348 self._iter = iter 

3349 self._config_type = config_type 

3350 self._config_type_iter = config_type_iter 

3351 self._delimiter = delimiter 

3352 

3353 super().__init__(inner, ty) 

3354 

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

3356 result = super().wrap(parser) 

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

3358 return result 

3359 

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

3361 return self._ctor( 

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

3363 ) 

3364 

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

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

3367 

3368 def supports_parse_many(self) -> bool: 

3369 return True 

3370 

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

3372 value = ctx.value 

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

3374 expected = self._config_type 

3375 if not isinstance(expected, tuple): 

3376 expected = (expected,) 

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

3378 

3379 return self._ctor( 

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

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

3382 ) 

3383 

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

3385 return "*" 

3386 

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

3388 delimiter = self._delimiter or " " 

3389 value = self._inner.describe_or_def() 

3390 

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

3392 

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

3394 return self._inner.describe_or_def() 

3395 

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

3397 assert self.assert_type(value) 

3398 

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

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

3401 ) 

3402 

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

3404 return None 

3405 

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

3407 completer = self._inner.completer() 

3408 return ( 

3409 yuio.complete.List( 

3410 completer, 

3411 delimiter=self._delimiter, 

3412 allow_duplicates=self._allow_completing_duplicates, 

3413 ) 

3414 if completer is not None 

3415 else None 

3416 ) 

3417 

3418 def widget( 

3419 self, 

3420 default: object | yuio.Missing, 

3421 input_description: str | None, 

3422 default_description: str | None, 

3423 /, 

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

3425 completer = self.completer() 

3426 return _WidgetResultMapper( 

3427 self, 

3428 input_description, 

3429 default, 

3430 ( 

3431 yuio.widget.InputWithCompletion( 

3432 completer, 

3433 placeholder=default_description or "", 

3434 ) 

3435 if completer is not None 

3436 else yuio.widget.Input( 

3437 placeholder=default_description or "", 

3438 ) 

3439 ), 

3440 ) 

3441 

3442 def is_secret(self) -> bool: 

3443 return self._inner.is_secret() 

3444 

3445 def __repr__(self): 

3446 if self._inner_raw is not None: 

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

3448 else: 

3449 return self.__class__.__name__ 

3450 

3451 

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

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

3454 

3455 Parser for lists. 

3456 

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

3458 using a subparser. 

3459 

3460 :param inner: 

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

3462 :param delimiter: 

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

3464 

3465 """ 

3466 

3467 if TYPE_CHECKING: 

3468 

3469 @_t.overload 

3470 def __new__( 

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

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

3473 

3474 @_t.overload 

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

3476 

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

3478 

3479 def __init__( 

3480 self, 

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

3482 /, 

3483 *, 

3484 delimiter: str | None = None, 

3485 ): 

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

3487 

3488 def to_json_schema( 

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

3490 ) -> yuio.json_schema.JsonSchemaType: 

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

3492 

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

3494 assert self.assert_type(value) 

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

3496 

3497 

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

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

3500 

3501 Parser for sets. 

3502 

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

3504 using a subparser. 

3505 

3506 :param inner: 

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

3508 :param delimiter: 

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

3510 

3511 """ 

3512 

3513 if TYPE_CHECKING: 

3514 

3515 @_t.overload 

3516 def __new__( 

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

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

3519 

3520 @_t.overload 

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

3522 

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

3524 

3525 _allow_completing_duplicates = False 

3526 

3527 def __init__( 

3528 self, 

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

3530 /, 

3531 *, 

3532 delimiter: str | None = None, 

3533 ): 

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

3535 

3536 def widget( 

3537 self, 

3538 default: object | yuio.Missing, 

3539 input_description: str | None, 

3540 default_description: str | None, 

3541 /, 

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

3543 options = self._inner.options() 

3544 if options is not None and len(options) <= 25: 

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

3546 else: 

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

3548 

3549 def to_json_schema( 

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

3551 ) -> yuio.json_schema.JsonSchemaType: 

3552 return yuio.json_schema.Array( 

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

3554 ) 

3555 

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

3557 assert self.assert_type(value) 

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

3559 

3560 

3561class FrozenSet(CollectionParser[frozenset[T], T], _t.Generic[T]): 

3562 """FrozenSet(inner: Parser[T], /, *, delimiter: str | None = None) 

3563 

3564 Parser for frozen sets. 

3565 

3566 Will split a string by the given delimiter, and parse each item 

3567 using a subparser. 

3568 

3569 :param inner: 

3570 inner parser that will be used to parse set items. 

3571 :param delimiter: 

3572 delimiter that will be passed to :py:meth:`str.split`. 

3573 

3574 """ 

3575 

3576 if TYPE_CHECKING: 

3577 

3578 @_t.overload 

3579 def __new__( 

3580 cls, inner: Parser[T], /, *, delimiter: str | None = None 

3581 ) -> FrozenSet[T]: ... 

3582 

3583 @_t.overload 

3584 def __new__(cls, /, *, delimiter: str | None = None) -> PartialParser: ... 

3585 

3586 def __new__(cls, *args, **kwargs) -> _t.Any: ... 

3587 

3588 _allow_completing_duplicates = False 

3589 

3590 def __init__( 

3591 self, 

3592 inner: Parser[T] | None = None, 

3593 /, 

3594 *, 

3595 delimiter: str | None = None, 

3596 ): 

3597 super().__init__(inner, ty=frozenset, ctor=frozenset, delimiter=delimiter) 

3598 

3599 def to_json_schema( 

3600 self, ctx: yuio.json_schema.JsonSchemaContext, / 

3601 ) -> yuio.json_schema.JsonSchemaType: 

3602 return yuio.json_schema.Array( 

3603 self._inner.to_json_schema(ctx), unique_items=True 

3604 ) 

3605 

3606 def to_json_value(self, value: object, /) -> yuio.json_schema.JsonValue: 

3607 assert self.assert_type(value) 

3608 return [self._inner.to_json_value(item) for item in value] 

3609 

3610 

3611class Dict(CollectionParser[dict[K, V], tuple[K, V]], _t.Generic[K, V]): 

3612 """Dict(key: Parser[K], value: Parser[V], /, *, delimiter: str | None = None, pair_delimiter: str = ":") 

3613 

3614 Parser for dicts. 

3615 

3616 Will split a string by the given delimiter, and parse each item 

3617 using a :py:class:`Tuple` parser. 

3618 

3619 :param key: 

3620 inner parser that will be used to parse dict keys. 

3621 :param value: 

3622 inner parser that will be used to parse dict values. 

3623 :param delimiter: 

3624 delimiter that will be passed to :py:meth:`str.split`. 

3625 :param pair_delimiter: 

3626 delimiter that will be used to split key-value elements. 

3627 

3628 """ 

3629 

3630 if TYPE_CHECKING: 

3631 

3632 @_t.overload 

3633 def __new__( 

3634 cls, 

3635 key: Parser[K], 

3636 value: Parser[V], 

3637 /, 

3638 *, 

3639 delimiter: str | None = None, 

3640 pair_delimiter: str = ":", 

3641 ) -> Dict[K, V]: ... 

3642 

3643 @_t.overload 

3644 def __new__( 

3645 cls, 

3646 /, 

3647 *, 

3648 delimiter: str | None = None, 

3649 pair_delimiter: str = ":", 

3650 ) -> PartialParser: ... 

3651 

3652 def __new__(cls, *args, **kwargs) -> _t.Any: ... 

3653 

3654 _allow_completing_duplicates = False 

3655 

3656 def __init__( 

3657 self, 

3658 key: Parser[K] | None = None, 

3659 value: Parser[V] | None = None, 

3660 /, 

3661 *, 

3662 delimiter: str | None = None, 

3663 pair_delimiter: str = ":", 

3664 ): 

3665 self._pair_delimiter = pair_delimiter 

3666 super().__init__( 

3667 ( 

3668 _DictElementParser(key, value, delimiter=pair_delimiter) 

3669 if key and value 

3670 else None 

3671 ), 

3672 ty=dict, 

3673 ctor=dict, 

3674 iter=dict.items, 

3675 config_type=(dict, list), 

3676 config_type_iter=self.__config_type_iter, 

3677 delimiter=delimiter, 

3678 ) 

3679 

3680 def wrap(self, parser: Parser[_t.Any]) -> Parser[_t.Any]: 

3681 result = super().wrap(parser) 

3682 setattr(result._inner, "_delimiter", self._pair_delimiter) 

3683 return result 

3684 

3685 @staticmethod 

3686 def __config_type_iter(x) -> _t.Iterator[tuple[K, V]]: 

3687 if isinstance(x, dict): 

3688 return iter(x.items()) 

3689 else: 

3690 return iter(x) 

3691 

3692 def to_json_schema( 

3693 self, ctx: yuio.json_schema.JsonSchemaContext, / 

3694 ) -> yuio.json_schema.JsonSchemaType: 

3695 key_schema = self._inner._inner[0].to_json_schema(ctx) # type: ignore 

3696 value_schema = self._inner._inner[1].to_json_schema(ctx) # type: ignore 

3697 return yuio.json_schema.Dict(key_schema, value_schema) 

3698 

3699 def to_json_value(self, value: object, /) -> yuio.json_schema.JsonValue: 

3700 assert self.assert_type(value) 

3701 items = _t.cast( 

3702 list[tuple[yuio.json_schema.JsonValue, yuio.json_schema.JsonValue]], 

3703 [self._inner.to_json_value(item) for item in value.items()], 

3704 ) 

3705 

3706 if all(isinstance(k, str) for k, _ in items): 

3707 return dict(_t.cast(list[tuple[str, yuio.json_schema.JsonValue]], items)) 

3708 else: 

3709 return items 

3710 

3711 

3712class Tuple( 

3713 WrappingParser[TU, tuple[Parser[object], ...]], 

3714 ValueParser[TU], 

3715 PartialParser, 

3716 _t.Generic[TU], 

3717): 

3718 """Tuple(*parsers: Parser[...], delimiter: str | None = None) 

3719 

3720 Parser for tuples of fixed lengths. 

3721 

3722 :param parsers: 

3723 parsers for each tuple element. 

3724 :param delimiter: 

3725 delimiter that will be passed to :py:meth:`str.split`. 

3726 

3727 """ 

3728 

3729 # See the links below for an explanation of shy this is so ugly: 

3730 # https://github.com/python/typing/discussions/1450 

3731 # https://github.com/python/typing/issues/1216 

3732 if TYPE_CHECKING: 

3733 T1 = _t.TypeVar("T1") 

3734 T2 = _t.TypeVar("T2") 

3735 T3 = _t.TypeVar("T3") 

3736 T4 = _t.TypeVar("T4") 

3737 T5 = _t.TypeVar("T5") 

3738 T6 = _t.TypeVar("T6") 

3739 T7 = _t.TypeVar("T7") 

3740 T8 = _t.TypeVar("T8") 

3741 T9 = _t.TypeVar("T9") 

3742 T10 = _t.TypeVar("T10") 

3743 

3744 @_t.overload 

3745 def __new__( 

3746 cls, 

3747 /, 

3748 *, 

3749 delimiter: str | None = None, 

3750 ) -> PartialParser: ... 

3751 

3752 @_t.overload 

3753 def __new__( 

3754 cls, 

3755 p1: Parser[T1], 

3756 /, 

3757 *, 

3758 delimiter: str | None = None, 

3759 ) -> Tuple[tuple[T1]]: ... 

3760 

3761 @_t.overload 

3762 def __new__( 

3763 cls, 

3764 p1: Parser[T1], 

3765 p2: Parser[T2], 

3766 /, 

3767 *, 

3768 delimiter: str | None = None, 

3769 ) -> Tuple[tuple[T1, T2]]: ... 

3770 

3771 @_t.overload 

3772 def __new__( 

3773 cls, 

3774 p1: Parser[T1], 

3775 p2: Parser[T2], 

3776 p3: Parser[T3], 

3777 /, 

3778 *, 

3779 delimiter: str | None = None, 

3780 ) -> Tuple[tuple[T1, T2, T3]]: ... 

3781 

3782 @_t.overload 

3783 def __new__( 

3784 cls, 

3785 p1: Parser[T1], 

3786 p2: Parser[T2], 

3787 p3: Parser[T3], 

3788 p4: Parser[T4], 

3789 /, 

3790 *, 

3791 delimiter: str | None = None, 

3792 ) -> Tuple[tuple[T1, T2, T3, T4]]: ... 

3793 

3794 @_t.overload 

3795 def __new__( 

3796 cls, 

3797 p1: Parser[T1], 

3798 p2: Parser[T2], 

3799 p3: Parser[T3], 

3800 p4: Parser[T4], 

3801 p5: Parser[T5], 

3802 /, 

3803 *, 

3804 delimiter: str | None = None, 

3805 ) -> Tuple[tuple[T1, T2, T3, T4, T5]]: ... 

3806 

3807 @_t.overload 

3808 def __new__( 

3809 cls, 

3810 p1: Parser[T1], 

3811 p2: Parser[T2], 

3812 p3: Parser[T3], 

3813 p4: Parser[T4], 

3814 p5: Parser[T5], 

3815 p6: Parser[T6], 

3816 /, 

3817 *, 

3818 delimiter: str | None = None, 

3819 ) -> Tuple[tuple[T1, T2, T3, T4, T5, T6]]: ... 

3820 

3821 @_t.overload 

3822 def __new__( 

3823 cls, 

3824 p1: Parser[T1], 

3825 p2: Parser[T2], 

3826 p3: Parser[T3], 

3827 p4: Parser[T4], 

3828 p5: Parser[T5], 

3829 p6: Parser[T6], 

3830 p7: Parser[T7], 

3831 /, 

3832 *, 

3833 delimiter: str | None = None, 

3834 ) -> Tuple[tuple[T1, T2, T3, T4, T5, T6, T7]]: ... 

3835 

3836 @_t.overload 

3837 def __new__( 

3838 cls, 

3839 p1: Parser[T1], 

3840 p2: Parser[T2], 

3841 p3: Parser[T3], 

3842 p4: Parser[T4], 

3843 p5: Parser[T5], 

3844 p6: Parser[T6], 

3845 p7: Parser[T7], 

3846 p8: Parser[T8], 

3847 /, 

3848 *, 

3849 delimiter: str | None = None, 

3850 ) -> Tuple[tuple[T1, T2, T3, T4, T5, T6, T7, T8]]: ... 

3851 

3852 @_t.overload 

3853 def __new__( 

3854 cls, 

3855 p1: Parser[T1], 

3856 p2: Parser[T2], 

3857 p3: Parser[T3], 

3858 p4: Parser[T4], 

3859 p5: Parser[T5], 

3860 p6: Parser[T6], 

3861 p7: Parser[T7], 

3862 p8: Parser[T8], 

3863 p9: Parser[T9], 

3864 /, 

3865 *, 

3866 delimiter: str | None = None, 

3867 ) -> Tuple[tuple[T1, T2, T3, T4, T5, T6, T7, T8, T9]]: ... 

3868 

3869 @_t.overload 

3870 def __new__( 

3871 cls, 

3872 p1: Parser[T1], 

3873 p2: Parser[T2], 

3874 p3: Parser[T3], 

3875 p4: Parser[T4], 

3876 p5: Parser[T5], 

3877 p6: Parser[T6], 

3878 p7: Parser[T7], 

3879 p8: Parser[T8], 

3880 p9: Parser[T9], 

3881 p10: Parser[T10], 

3882 /, 

3883 *, 

3884 delimiter: str | None = None, 

3885 ) -> Tuple[tuple[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10]]: ... 

3886 

3887 @_t.overload 

3888 def __new__( 

3889 cls, 

3890 p1: Parser[T1], 

3891 p2: Parser[T2], 

3892 p3: Parser[T3], 

3893 p4: Parser[T4], 

3894 p5: Parser[T5], 

3895 p6: Parser[T6], 

3896 p7: Parser[T7], 

3897 p8: Parser[T8], 

3898 p9: Parser[T9], 

3899 p10: Parser[T10], 

3900 p11: Parser[object], 

3901 *tail: Parser[object], 

3902 delimiter: str | None = None, 

3903 ) -> Tuple[tuple[_t.Any, ...]]: ... 

3904 

3905 def __new__(cls, *args, **kwargs) -> _t.Any: ... 

3906 

3907 def __init__( 

3908 self, 

3909 *parsers: Parser[_t.Any], 

3910 delimiter: str | None = None, 

3911 ): 

3912 if delimiter == "": 

3913 raise ValueError("empty delimiter") 

3914 self._delimiter = delimiter 

3915 super().__init__(parsers or None, tuple) 

3916 

3917 def wrap(self, parser: Parser[_t.Any]) -> Parser[_t.Any]: 

3918 result = super().wrap(parser) 

3919 result._inner = parser._inner # type: ignore 

3920 return result 

3921 

3922 def parse_with_ctx(self, ctx: StrParsingContext, /) -> TU: 

3923 items = list(ctx.split(self._delimiter, maxsplit=len(self._inner) - 1)) 

3924 

3925 if len(items) != len(self._inner): 

3926 raise ParsingError( 

3927 "Expected %s element%s, got %s: `%r`", 

3928 len(self._inner), 

3929 "" if len(self._inner) == 1 else "s", 

3930 len(items), 

3931 ctx.value, 

3932 ctx=ctx, 

3933 ) 

3934 

3935 return _t.cast( 

3936 TU, 

3937 tuple( 

3938 parser.parse_with_ctx(item) for parser, item in zip(self._inner, items) 

3939 ), 

3940 ) 

3941 

3942 def parse_many_with_ctx(self, ctxs: _t.Sequence[StrParsingContext], /) -> TU: 

3943 if len(ctxs) != len(self._inner): 

3944 raise ParsingError( 

3945 "Expected %s element%s, got %s: `%r`", 

3946 len(self._inner), 

3947 "" if len(self._inner) == 1 else "s", 

3948 len(ctxs), 

3949 ctxs, 

3950 ) 

3951 

3952 return _t.cast( 

3953 TU, 

3954 tuple( 

3955 parser.parse_with_ctx(item) for parser, item in zip(self._inner, ctxs) 

3956 ), 

3957 ) 

3958 

3959 def parse_config_with_ctx(self, ctx: ConfigParsingContext, /) -> TU: 

3960 value = ctx.value 

3961 if not isinstance(value, (list, tuple)): 

3962 raise ParsingError.type_mismatch(value, list, tuple, ctx=ctx) 

3963 elif len(value) != len(self._inner): 

3964 raise ParsingError( 

3965 "Expected %s element%s, got %s: `%r`", 

3966 len(self._inner), 

3967 "" if len(self._inner) == 1 else "s", 

3968 len(value), 

3969 value, 

3970 ) 

3971 

3972 return _t.cast( 

3973 TU, 

3974 tuple( 

3975 parser.parse_config_with_ctx(ctx.descend(item, i)) 

3976 for i, (parser, item) in enumerate(zip(self._inner, value)) 

3977 ), 

3978 ) 

3979 

3980 def supports_parse_many(self) -> bool: 

3981 return True 

3982 

3983 def get_nargs(self) -> _t.Literal["+", "*"] | int: 

3984 return len(self._inner) 

3985 

3986 def describe(self) -> str | None: 

3987 delimiter = self._delimiter or " " 

3988 desc = [parser.describe_or_def() for parser in self._inner] 

3989 return delimiter.join(desc) 

3990 

3991 def describe_many(self) -> str | tuple[str, ...]: 

3992 return tuple(parser.describe_or_def() for parser in self._inner) 

3993 

3994 def describe_value(self, value: object, /) -> str: 

3995 assert self.assert_type(value) 

3996 

3997 delimiter = self._delimiter or " " 

3998 desc = [parser.describe_value(item) for parser, item in zip(self._inner, value)] 

3999 

4000 return delimiter.join(desc) 

4001 

4002 def options(self) -> _t.Collection[yuio.widget.Option[TU]] | None: 

4003 return None 

4004 

4005 def completer(self) -> yuio.complete.Completer | None: 

4006 return yuio.complete.Tuple( 

4007 *[parser.completer() or yuio.complete.Empty() for parser in self._inner], 

4008 delimiter=self._delimiter, 

4009 ) 

4010 

4011 def widget( 

4012 self, 

4013 default: object | yuio.Missing, 

4014 input_description: str | None, 

4015 default_description: str | None, 

4016 /, 

4017 ) -> yuio.widget.Widget[TU | yuio.Missing]: 

4018 completer = self.completer() 

4019 

4020 return _WidgetResultMapper( 

4021 self, 

4022 input_description, 

4023 default, 

4024 ( 

4025 yuio.widget.InputWithCompletion( 

4026 completer, 

4027 placeholder=default_description or "", 

4028 ) 

4029 if completer is not None 

4030 else yuio.widget.Input( 

4031 placeholder=default_description or "", 

4032 ) 

4033 ), 

4034 ) 

4035 

4036 def to_json_schema( 

4037 self, ctx: yuio.json_schema.JsonSchemaContext, / 

4038 ) -> yuio.json_schema.JsonSchemaType: 

4039 return yuio.json_schema.Tuple( 

4040 [parser.to_json_schema(ctx) for parser in self._inner] 

4041 ) 

4042 

4043 def to_json_value(self, value: object, /) -> yuio.json_schema.JsonValue: 

4044 assert self.assert_type(value) 

4045 return [parser.to_json_value(item) for parser, item in zip(self._inner, value)] 

4046 

4047 def is_secret(self) -> bool: 

4048 return any(parser.is_secret() for parser in self._inner) 

4049 

4050 def __repr__(self): 

4051 if self._inner_raw is not None: 

4052 return f"{self.__class__.__name__}{self._inner_raw!r}" 

4053 else: 

4054 return self.__class__.__name__ 

4055 

4056 

4057class _DictElementParser(Tuple[tuple[K, V]], _t.Generic[K, V]): 

4058 def __init__(self, k: Parser[K], v: Parser[V], delimiter: str | None = None): 

4059 super().__init__(k, v, delimiter=delimiter) 

4060 

4061 # def parse_with_ctx(self, ctx: StrParsingContext, /) -> tuple[K, V]: 

4062 # items = list(ctx.split(self._delimiter, maxsplit=len(self._inner) - 1)) 

4063 

4064 # if len(items) != len(self._inner): 

4065 # raise ParsingError("Expected key-value pair, got `%r`", ctx.value) 

4066 

4067 # return _t.cast( 

4068 # tuple[K, V], 

4069 # tuple(parser.parse_with_ctx(item) for parser, item in zip(self._inner, items)), 

4070 # ) 

4071 

4072 # def parse_many_with_ctx(self, ctxs: _t.Sequence[StrParsingContext], /) -> tuple[K, V]: 

4073 # if len(value) != len(self._inner): 

4074 # with describe_context("element #%(key)r"): 

4075 # raise ParsingError( 

4076 # "Expected key-value pair, got `%r`", 

4077 # value, 

4078 # ) 

4079 

4080 # k = describe_context("key of element #%(key)r", self._inner[0].parse, value[0]) 

4081 # v = replace_context(k, self._inner[1].parse, value[1]) 

4082 

4083 # return _t.cast(tuple[K, V], (k, v)) 

4084 

4085 # def parse_config_with_ctx(self, ctx: ConfigParsingContext, /) -> tuple[K, V]: 

4086 # if not isinstance(value, (list, tuple)): 

4087 # with describe_context("element #%(key)r"): 

4088 # raise ParsingError.type_mismatch(value, list, tuple) 

4089 # elif len(value) != len(self._inner): 

4090 # with describe_context("element #%(key)r"): 

4091 # raise ParsingError( 

4092 # "Expected key-value pair, got `%r`", 

4093 # value, 

4094 # ) 

4095 

4096 # k = describe_context( 

4097 # "key of element #%(key)r", self._inner[0].parse_config_with_ctx, value[0] 

4098 # ) 

4099 # v = replace_context(k, self._inner[1].parse_config_with_ctx, value[1]) 

4100 

4101 # return _t.cast(tuple[K, V], (k, v)) 

4102 

4103 

4104class Optional(MappingParser[T | None, T], _t.Generic[T]): 

4105 """Optional(inner: Parser[T], /) 

4106 

4107 Parser for optional values. 

4108 

4109 Allows handling :data:`None`\\ s when parsing config. Does not change how strings 

4110 are parsed, though. 

4111 

4112 :param inner: 

4113 a parser used to extract and validate contents of an optional. 

4114 

4115 """ 

4116 

4117 if TYPE_CHECKING: 

4118 

4119 @_t.overload 

4120 def __new__(cls, inner: Parser[T], /) -> Optional[T]: ... 

4121 

4122 @_t.overload 

4123 def __new__(cls, /) -> PartialParser: ... 

4124 

4125 def __new__(cls, *args, **kwargs) -> _t.Any: ... 

4126 

4127 def __init__(self, inner: Parser[T] | None = None, /): 

4128 super().__init__(inner) 

4129 

4130 def parse_with_ctx(self, ctx: StrParsingContext, /) -> T | None: 

4131 return self._inner.parse_with_ctx(ctx) 

4132 

4133 def parse_many_with_ctx(self, ctxs: _t.Sequence[StrParsingContext], /) -> T | None: 

4134 return self._inner.parse_many_with_ctx(ctxs) 

4135 

4136 def parse_config_with_ctx(self, ctx: ConfigParsingContext, /) -> T | None: 

4137 if ctx.value is None: 

4138 return None 

4139 return self._inner.parse_config_with_ctx(ctx) 

4140 

4141 def check_type(self, value: object, /) -> _t.TypeGuard[T | None]: 

4142 return True 

4143 

4144 def describe_value(self, value: object, /) -> str: 

4145 if value is None: 

4146 return "<none>" 

4147 return self._inner.describe_value(value) 

4148 

4149 def options(self) -> _t.Collection[yuio.widget.Option[T | None]] | None: 

4150 return self._inner.options() 

4151 

4152 def widget( 

4153 self, 

4154 default: object | yuio.Missing, 

4155 input_description: str | None, 

4156 default_description: str | None, 

4157 /, 

4158 ) -> yuio.widget.Widget[T | yuio.Missing]: 

4159 return self._inner.widget(default, input_description, default_description) 

4160 

4161 def to_json_schema( 

4162 self, ctx: yuio.json_schema.JsonSchemaContext, / 

4163 ) -> yuio.json_schema.JsonSchemaType: 

4164 return yuio.json_schema.OneOf( 

4165 [self._inner.to_json_schema(ctx), yuio.json_schema.Null()] 

4166 ) 

4167 

4168 def to_json_value(self, value: object, /) -> yuio.json_schema.JsonValue: 

4169 if value is None: 

4170 return None 

4171 else: 

4172 return self._inner.to_json_value(value) 

4173 

4174 

4175class Union(WrappingParser[T, tuple[Parser[T], ...]], ValueParser[T], _t.Generic[T]): 

4176 """Union(*parsers: Parser[T]) 

4177 

4178 Tries several parsers and returns the first successful result. 

4179 

4180 .. warning:: 

4181 

4182 Order of parsers matters. Since parsers are tried in the same order as they're 

4183 given, make sure to put parsers that are likely to succeed at the end. 

4184 

4185 For example, this parser will always return a string because :class:`Str` 

4186 can't fail:: 

4187 

4188 >>> parser = Union(Str(), Int()) # Always returns a string! 

4189 >>> parser.parse("10") 

4190 '10' 

4191 

4192 To fix this, put :class:`Str` at the end so that :class:`Int` is tried first:: 

4193 

4194 >>> parser = Union(Int(), Str()) 

4195 >>> parser.parse("10") 

4196 10 

4197 >>> parser.parse("not an int") 

4198 'not an int' 

4199 

4200 """ 

4201 

4202 # See the links below for an explanation of shy this is so ugly: 

4203 # https://github.com/python/typing/discussions/1450 

4204 # https://github.com/python/typing/issues/1216 

4205 if TYPE_CHECKING: 

4206 T1 = _t.TypeVar("T1") 

4207 T2 = _t.TypeVar("T2") 

4208 T3 = _t.TypeVar("T3") 

4209 T4 = _t.TypeVar("T4") 

4210 T5 = _t.TypeVar("T5") 

4211 T6 = _t.TypeVar("T6") 

4212 T7 = _t.TypeVar("T7") 

4213 T8 = _t.TypeVar("T8") 

4214 T9 = _t.TypeVar("T9") 

4215 T10 = _t.TypeVar("T10") 

4216 

4217 @_t.overload 

4218 def __new__( 

4219 cls, 

4220 /, 

4221 ) -> PartialParser: ... 

4222 

4223 @_t.overload 

4224 def __new__( 

4225 cls, 

4226 p1: Parser[T1], 

4227 /, 

4228 ) -> Union[T1]: ... 

4229 

4230 @_t.overload 

4231 def __new__( 

4232 cls, 

4233 p1: Parser[T1], 

4234 p2: Parser[T2], 

4235 /, 

4236 ) -> Union[T1 | T2]: ... 

4237 

4238 @_t.overload 

4239 def __new__( 

4240 cls, 

4241 p1: Parser[T1], 

4242 p2: Parser[T2], 

4243 p3: Parser[T3], 

4244 /, 

4245 ) -> Union[T1 | T2 | T3]: ... 

4246 

4247 @_t.overload 

4248 def __new__( 

4249 cls, 

4250 p1: Parser[T1], 

4251 p2: Parser[T2], 

4252 p3: Parser[T3], 

4253 p4: Parser[T4], 

4254 /, 

4255 ) -> Union[T1 | T2 | T3 | T4]: ... 

4256 

4257 @_t.overload 

4258 def __new__( 

4259 cls, 

4260 p1: Parser[T1], 

4261 p2: Parser[T2], 

4262 p3: Parser[T3], 

4263 p4: Parser[T4], 

4264 p5: Parser[T5], 

4265 /, 

4266 ) -> Union[T1 | T2 | T3 | T4 | T5]: ... 

4267 

4268 @_t.overload 

4269 def __new__( 

4270 cls, 

4271 p1: Parser[T1], 

4272 p2: Parser[T2], 

4273 p3: Parser[T3], 

4274 p4: Parser[T4], 

4275 p5: Parser[T5], 

4276 p6: Parser[T6], 

4277 /, 

4278 ) -> Union[T1 | T2 | T3 | T4 | T5 | T6]: ... 

4279 

4280 @_t.overload 

4281 def __new__( 

4282 cls, 

4283 p1: Parser[T1], 

4284 p2: Parser[T2], 

4285 p3: Parser[T3], 

4286 p4: Parser[T4], 

4287 p5: Parser[T5], 

4288 p6: Parser[T6], 

4289 p7: Parser[T7], 

4290 /, 

4291 ) -> Union[T1 | T2 | T3 | T4 | T5 | T6 | T7]: ... 

4292 

4293 @_t.overload 

4294 def __new__( 

4295 cls, 

4296 p1: Parser[T1], 

4297 p2: Parser[T2], 

4298 p3: Parser[T3], 

4299 p4: Parser[T4], 

4300 p5: Parser[T5], 

4301 p6: Parser[T6], 

4302 p7: Parser[T7], 

4303 p8: Parser[T8], 

4304 /, 

4305 ) -> Union[T1 | T2 | T3 | T4 | T5 | T6 | T7 | T8]: ... 

4306 

4307 @_t.overload 

4308 def __new__( 

4309 cls, 

4310 p1: Parser[T1], 

4311 p2: Parser[T2], 

4312 p3: Parser[T3], 

4313 p4: Parser[T4], 

4314 p5: Parser[T5], 

4315 p6: Parser[T6], 

4316 p7: Parser[T7], 

4317 p8: Parser[T8], 

4318 p9: Parser[T9], 

4319 /, 

4320 ) -> Union[T1 | T2 | T3 | T4 | T5 | T6 | T7 | T8 | T9]: ... 

4321 

4322 @_t.overload 

4323 def __new__( 

4324 cls, 

4325 p1: Parser[T1], 

4326 p2: Parser[T2], 

4327 p3: Parser[T3], 

4328 p4: Parser[T4], 

4329 p5: Parser[T5], 

4330 p6: Parser[T6], 

4331 p7: Parser[T7], 

4332 p8: Parser[T8], 

4333 p9: Parser[T9], 

4334 p10: Parser[T10], 

4335 /, 

4336 ) -> Union[T1 | T2 | T3 | T4 | T5 | T6 | T7 | T8 | T9 | T10]: ... 

4337 

4338 @_t.overload 

4339 def __new__( 

4340 cls, 

4341 p1: Parser[T1], 

4342 p2: Parser[T2], 

4343 p3: Parser[T3], 

4344 p4: Parser[T4], 

4345 p5: Parser[T5], 

4346 p6: Parser[T6], 

4347 p7: Parser[T7], 

4348 p8: Parser[T8], 

4349 p9: Parser[T9], 

4350 p10: Parser[T10], 

4351 p11: Parser[object], 

4352 *parsers: Parser[object], 

4353 ) -> Union[object]: ... 

4354 

4355 def __new__(cls, *args, **kwargs) -> _t.Any: ... 

4356 

4357 def __init__(self, *parsers: Parser[_t.Any]): 

4358 super().__init__(parsers or None, object) 

4359 

4360 def wrap(self, parser: Parser[_t.Any]) -> Parser[_t.Any]: 

4361 result = super().wrap(parser) 

4362 result._inner = parser._inner # type: ignore 

4363 return result 

4364 

4365 def parse_with_ctx(self, ctx: StrParsingContext, /) -> T: 

4366 errors: list[tuple[Parser[object], ParsingError]] = [] 

4367 for parser in self._inner: 

4368 try: 

4369 return parser.parse_with_ctx(ctx) 

4370 except ParsingError as e: 

4371 errors.append((parser, e)) 

4372 raise self._make_error(errors, ctx) 

4373 

4374 def parse_config_with_ctx(self, ctx: ConfigParsingContext, /) -> T: 

4375 errors: list[tuple[Parser[object], ParsingError]] = [] 

4376 for parser in self._inner: 

4377 try: 

4378 return parser.parse_config_with_ctx(ctx) 

4379 except ParsingError as e: 

4380 errors.append((parser, e)) 

4381 raise self._make_error(errors, ctx) 

4382 

4383 def _make_error( 

4384 self, 

4385 errors: list[tuple[Parser[object], ParsingError]], 

4386 ctx: StrParsingContext | ConfigParsingContext, 

4387 ): 

4388 msgs = [] 

4389 for parser, error in errors: 

4390 error.raw = None 

4391 error.pos = None 

4392 msgs.append( 

4393 yuio.string.Format( 

4394 " Trying as `%s`:\n%s", 

4395 parser.describe_or_def(), 

4396 yuio.string.Indent(error, indent=4), 

4397 ) 

4398 ) 

4399 return ParsingError( 

4400 "Can't parse `%r`:\n%s", ctx.value, yuio.string.Stack(*msgs), ctx=ctx 

4401 ) 

4402 

4403 def check_type(self, value: object, /) -> _t.TypeGuard[T]: 

4404 return True 

4405 

4406 def describe(self) -> str | None: 

4407 if len(self._inner) > 1: 

4408 

4409 def strip_curly_brackets(desc: str): 

4410 if desc.startswith("{") and desc.endswith("}") and "|" in desc: 

4411 s = desc[1:-1] 

4412 if "{" not in s and "}" not in s: 

4413 return s 

4414 return desc 

4415 

4416 desc = "|".join( 

4417 strip_curly_brackets(parser.describe_or_def()) for parser in self._inner 

4418 ) 

4419 desc = f"{{{desc}}}" 

4420 else: 

4421 desc = "|".join(parser.describe_or_def() for parser in self._inner) 

4422 return desc 

4423 

4424 def describe_value(self, value: object, /) -> str: 

4425 for parser in self._inner: 

4426 try: 

4427 return parser.describe_value(value) 

4428 except TypeError: 

4429 pass 

4430 

4431 raise TypeError( 

4432 f"parser {self} can't handle value of type {_tx.type_repr(type(value))}" 

4433 ) 

4434 

4435 def options(self) -> _t.Collection[yuio.widget.Option[T]] | None: 

4436 result = [] 

4437 got_options = False 

4438 for parser in self._inner: 

4439 if options := parser.options(): 

4440 result.extend(options) 

4441 got_options = True 

4442 if got_options: 

4443 return result 

4444 else: 

4445 return None 

4446 

4447 def completer(self) -> yuio.complete.Completer | None: 

4448 completers = [] 

4449 for parser in self._inner: 

4450 if completer := parser.completer(): 

4451 completers.append((parser.describe(), completer)) 

4452 if not completers: 

4453 return None 

4454 elif len(completers) == 1: 

4455 return completers[0][1] 

4456 else: 

4457 return yuio.complete.Alternative(completers) 

4458 

4459 def to_json_schema( 

4460 self, ctx: yuio.json_schema.JsonSchemaContext, / 

4461 ) -> yuio.json_schema.JsonSchemaType: 

4462 return yuio.json_schema.OneOf( 

4463 [parser.to_json_schema(ctx) for parser in self._inner] 

4464 ) 

4465 

4466 def to_json_value(self, value: object, /) -> yuio.json_schema.JsonValue: 

4467 for parser in self._inner: 

4468 try: 

4469 return parser.to_json_value(value) 

4470 except TypeError: 

4471 pass 

4472 

4473 raise TypeError( 

4474 f"parser {self} can't handle value of type {_tx.type_repr(type(value))}" 

4475 ) 

4476 

4477 def is_secret(self) -> bool: 

4478 return any(parser.is_secret() for parser in self._inner) 

4479 

4480 def __repr__(self): 

4481 return f"{self.__class__.__name__}{self._inner_raw!r}" 

4482 

4483 

4484class _BoundImpl(ValidatingParser[T], _t.Generic[T, Cmp]): 

4485 def __init__( 

4486 self, 

4487 inner: Parser[T] | None, 

4488 /, 

4489 *, 

4490 lower: Cmp | None = None, 

4491 lower_inclusive: Cmp | None = None, 

4492 upper: Cmp | None = None, 

4493 upper_inclusive: Cmp | None = None, 

4494 mapper: _t.Callable[[T], Cmp], 

4495 desc: str, 

4496 ): 

4497 super().__init__(inner) 

4498 

4499 self._lower_bound: Cmp | None = None 

4500 self._lower_bound_is_inclusive: bool = True 

4501 self._upper_bound: Cmp | None = None 

4502 self._upper_bound_is_inclusive: bool = True 

4503 

4504 if lower is not None and lower_inclusive is not None: 

4505 raise TypeError( 

4506 "lower and lower_inclusive cannot be given at the same time" 

4507 ) 

4508 elif lower is not None: 

4509 self._lower_bound = lower 

4510 self._lower_bound_is_inclusive = False 

4511 elif lower_inclusive is not None: 

4512 self._lower_bound = lower_inclusive 

4513 self._lower_bound_is_inclusive = True 

4514 

4515 if upper is not None and upper_inclusive is not None: 

4516 raise TypeError( 

4517 "upper and upper_inclusive cannot be given at the same time" 

4518 ) 

4519 elif upper is not None: 

4520 self._upper_bound = upper 

4521 self._upper_bound_is_inclusive = False 

4522 elif upper_inclusive is not None: 

4523 self._upper_bound = upper_inclusive 

4524 self._upper_bound_is_inclusive = True 

4525 

4526 self.__mapper = mapper 

4527 self.__desc = desc 

4528 

4529 def _validate(self, value: T, /): 

4530 mapped = self.__mapper(value) 

4531 

4532 if self._lower_bound is not None: 

4533 if self._lower_bound_is_inclusive and mapped < self._lower_bound: 

4534 raise ParsingError( 

4535 "%s should be greater than or equal to `%s`: `%r`", 

4536 self.__desc, 

4537 self._lower_bound, 

4538 value, 

4539 ) 

4540 elif not self._lower_bound_is_inclusive and not self._lower_bound < mapped: 

4541 raise ParsingError( 

4542 "%s should be greater than `%s`: `%r`", 

4543 self.__desc, 

4544 self._lower_bound, 

4545 value, 

4546 ) 

4547 

4548 if self._upper_bound is not None: 

4549 if self._upper_bound_is_inclusive and self._upper_bound < mapped: 

4550 raise ParsingError( 

4551 "%s should be lesser than or equal to `%s`: `%r`", 

4552 self.__desc, 

4553 self._upper_bound, 

4554 value, 

4555 ) 

4556 elif not self._upper_bound_is_inclusive and not mapped < self._upper_bound: 

4557 raise ParsingError( 

4558 "%s should be lesser than `%s`: `%r`", 

4559 self.__desc, 

4560 self._upper_bound, 

4561 value, 

4562 ) 

4563 

4564 def __repr__(self): 

4565 desc = "" 

4566 if self._lower_bound is not None: 

4567 desc += repr(self._lower_bound) 

4568 desc += " <= " if self._lower_bound_is_inclusive else " < " 

4569 mapper_name = getattr(self.__mapper, "__name__") 

4570 if mapper_name and mapper_name != "<lambda>": 

4571 desc += mapper_name 

4572 else: 

4573 desc += "x" 

4574 if self._upper_bound is not None: 

4575 desc += " <= " if self._upper_bound_is_inclusive else " < " 

4576 desc += repr(self._upper_bound) 

4577 return f"{self.__class__.__name__}({self.__wrapped_parser__!r}, {desc})" 

4578 

4579 

4580class Bound(_BoundImpl[Cmp, Cmp], _t.Generic[Cmp]): 

4581 """Bound(inner: Parser[Cmp], /, *, lower: Cmp | None = None, lower_inclusive: Cmp | None = None, upper: Cmp | None = None, upper_inclusive: Cmp | None = None) 

4582 

4583 Check that value is upper- or lower-bound by some constraints. 

4584 

4585 :param inner: 

4586 parser whose result will be validated. 

4587 :param lower: 

4588 set lower bound for value, so we require that ``value > lower``. 

4589 Can't be given if `lower_inclusive` is also given. 

4590 :param lower_inclusive: 

4591 set lower bound for value, so we require that ``value >= lower``. 

4592 Can't be given if `lower` is also given. 

4593 :param upper: 

4594 set upper bound for value, so we require that ``value < upper``. 

4595 Can't be given if `upper_inclusive` is also given. 

4596 :param upper_inclusive: 

4597 set upper bound for value, so we require that ``value <= upper``. 

4598 Can't be given if `upper` is also given. 

4599 :example: 

4600 :: 

4601 

4602 >>> # Int in range `0 < x <= 1`: 

4603 >>> Bound(Int(), lower=0, upper_inclusive=1) 

4604 Bound(Int, 0 < x <= 1) 

4605 

4606 """ 

4607 

4608 if TYPE_CHECKING: 

4609 

4610 @_t.overload 

4611 def __new__( 

4612 cls, 

4613 inner: Parser[Cmp], 

4614 /, 

4615 *, 

4616 lower: Cmp | None = None, 

4617 lower_inclusive: Cmp | None = None, 

4618 upper: Cmp | None = None, 

4619 upper_inclusive: Cmp | None = None, 

4620 ) -> Bound[Cmp]: ... 

4621 

4622 @_t.overload 

4623 def __new__( 

4624 cls, 

4625 *, 

4626 lower: Cmp | None = None, 

4627 lower_inclusive: Cmp | None = None, 

4628 upper: Cmp | None = None, 

4629 upper_inclusive: Cmp | None = None, 

4630 ) -> PartialParser: ... 

4631 

4632 def __new__(cls, *args, **kwargs) -> _t.Any: ... 

4633 

4634 def __init__( 

4635 self, 

4636 inner: Parser[Cmp] | None = None, 

4637 /, 

4638 *, 

4639 lower: Cmp | None = None, 

4640 lower_inclusive: Cmp | None = None, 

4641 upper: Cmp | None = None, 

4642 upper_inclusive: Cmp | None = None, 

4643 ): 

4644 super().__init__( 

4645 inner, 

4646 lower=lower, 

4647 lower_inclusive=lower_inclusive, 

4648 upper=upper, 

4649 upper_inclusive=upper_inclusive, 

4650 mapper=lambda x: x, 

4651 desc="Value", 

4652 ) 

4653 

4654 def to_json_schema( 

4655 self, ctx: yuio.json_schema.JsonSchemaContext, / 

4656 ) -> yuio.json_schema.JsonSchemaType: 

4657 bound = {} 

4658 if isinstance(self._lower_bound, (int, float)): 

4659 bound[ 

4660 "minimum" if self._lower_bound_is_inclusive else "exclusiveMinimum" 

4661 ] = self._lower_bound 

4662 if isinstance(self._upper_bound, (int, float)): 

4663 bound[ 

4664 "maximum" if self._upper_bound_is_inclusive else "exclusiveMaximum" 

4665 ] = self._upper_bound 

4666 if bound: 

4667 return yuio.json_schema.AllOf( 

4668 [super().to_json_schema(ctx), yuio.json_schema.Opaque(bound)] 

4669 ) 

4670 else: 

4671 return super().to_json_schema(ctx) 

4672 

4673 

4674@_t.overload 

4675def Gt(inner: Parser[Cmp], bound: Cmp, /) -> Bound[Cmp]: ... 

4676@_t.overload 

4677def Gt(bound: _tx.SupportsLt[_t.Any], /) -> PartialParser: ... 

4678def Gt(*args) -> _t.Any: 

4679 """Gt(inner: Parser[Cmp], bound: Cmp, /) 

4680 

4681 Alias for :class:`Bound`. 

4682 

4683 :param inner: 

4684 parser whose result will be validated. 

4685 :param bound: 

4686 lower bound for parsed values. 

4687 

4688 """ 

4689 

4690 if len(args) == 1: 

4691 return Bound(lower=args[0]) 

4692 elif len(args) == 2: 

4693 return Bound(args[0], lower=args[1]) 

4694 else: 

4695 raise TypeError(f"expected 1 or 2 positional arguments, got {len(args)}") 

4696 

4697 

4698@_t.overload 

4699def Ge(inner: Parser[Cmp], bound: Cmp, /) -> Bound[Cmp]: ... 

4700@_t.overload 

4701def Ge(bound: _tx.SupportsLt[_t.Any], /) -> PartialParser: ... 

4702def Ge(*args) -> _t.Any: 

4703 """Ge(inner: Parser[Cmp], bound: Cmp, /) 

4704 

4705 Alias for :class:`Bound`. 

4706 

4707 :param inner: 

4708 parser whose result will be validated. 

4709 :param bound: 

4710 lower inclusive bound for parsed values. 

4711 

4712 """ 

4713 

4714 if len(args) == 1: 

4715 return Bound(lower_inclusive=args[0]) 

4716 elif len(args) == 2: 

4717 return Bound(args[0], lower_inclusive=args[1]) 

4718 else: 

4719 raise TypeError(f"expected 1 or 2 positional arguments, got {len(args)}") 

4720 

4721 

4722@_t.overload 

4723def Lt(inner: Parser[Cmp], bound: Cmp, /) -> Bound[Cmp]: ... 

4724@_t.overload 

4725def Lt(bound: _tx.SupportsLt[_t.Any], /) -> PartialParser: ... 

4726def Lt(*args) -> _t.Any: 

4727 """Lt(inner: Parser[Cmp], bound: Cmp, /) 

4728 

4729 Alias for :class:`Bound`. 

4730 

4731 :param inner: 

4732 parser whose result will be validated. 

4733 :param bound: 

4734 upper bound for parsed values. 

4735 

4736 """ 

4737 

4738 if len(args) == 1: 

4739 return Bound(upper=args[0]) 

4740 elif len(args) == 2: 

4741 return Bound(args[0], upper=args[1]) 

4742 else: 

4743 raise TypeError(f"expected 1 or 2 positional arguments, got {len(args)}") 

4744 

4745 

4746@_t.overload 

4747def Le(inner: Parser[Cmp], bound: Cmp, /) -> Bound[Cmp]: ... 

4748@_t.overload 

4749def Le(bound: _tx.SupportsLt[_t.Any], /) -> PartialParser: ... 

4750def Le(*args) -> _t.Any: 

4751 """Le(inner: Parser[Cmp], bound: Cmp, /) 

4752 

4753 Alias for :class:`Bound`. 

4754 

4755 :param inner: 

4756 parser whose result will be validated. 

4757 :param bound: 

4758 upper inclusive bound for parsed values. 

4759 

4760 """ 

4761 

4762 if len(args) == 1: 

4763 return Bound(upper_inclusive=args[0]) 

4764 elif len(args) == 2: 

4765 return Bound(args[0], upper_inclusive=args[1]) 

4766 else: 

4767 raise TypeError(f"expected 1 or 2 positional arguments, got {len(args)}") 

4768 

4769 

4770class LenBound(_BoundImpl[Sz, int], _t.Generic[Sz]): 

4771 """LenBound(inner: Parser[Sz], /, *, lower: int | None = None, lower_inclusive: int | None = None, upper: int | None = None, upper_inclusive: int | None = None) 

4772 

4773 Check that length of a value is upper- or lower-bound by some constraints. 

4774 

4775 The signature is the same as of the :class:`Bound` class. 

4776 

4777 :param inner: 

4778 parser whose result will be validated. 

4779 :param lower: 

4780 set lower bound for value's length, so we require that ``len(value) > lower``. 

4781 Can't be given if `lower_inclusive` is also given. 

4782 :param lower_inclusive: 

4783 set lower bound for value's length, so we require that ``len(value) >= lower``. 

4784 Can't be given if `lower` is also given. 

4785 :param upper: 

4786 set upper bound for value's length, so we require that ``len(value) < upper``. 

4787 Can't be given if `upper_inclusive` is also given. 

4788 :param upper_inclusive: 

4789 set upper bound for value's length, so we require that ``len(value) <= upper``. 

4790 Can't be given if `upper` is also given. 

4791 :example: 

4792 :: 

4793 

4794 >>> # List of up to five ints: 

4795 >>> LenBound(List(Int()), upper_inclusive=5) 

4796 LenBound(List(Int), len <= 5) 

4797 

4798 """ 

4799 

4800 if TYPE_CHECKING: 

4801 

4802 @_t.overload 

4803 def __new__( 

4804 cls, 

4805 inner: Parser[Sz], 

4806 /, 

4807 *, 

4808 lower: int | None = None, 

4809 lower_inclusive: int | None = None, 

4810 upper: int | None = None, 

4811 upper_inclusive: int | None = None, 

4812 ) -> LenBound[Sz]: ... 

4813 

4814 @_t.overload 

4815 def __new__( 

4816 cls, 

4817 /, 

4818 *, 

4819 lower: int | None = None, 

4820 lower_inclusive: int | None = None, 

4821 upper: int | None = None, 

4822 upper_inclusive: int | None = None, 

4823 ) -> PartialParser: ... 

4824 

4825 def __new__(cls, *args, **kwargs) -> _t.Any: ... 

4826 

4827 def __init__( 

4828 self, 

4829 inner: Parser[Sz] | None = None, 

4830 /, 

4831 *, 

4832 lower: int | None = None, 

4833 lower_inclusive: int | None = None, 

4834 upper: int | None = None, 

4835 upper_inclusive: int | None = None, 

4836 ): 

4837 super().__init__( 

4838 inner, 

4839 lower=lower, 

4840 lower_inclusive=lower_inclusive, 

4841 upper=upper, 

4842 upper_inclusive=upper_inclusive, 

4843 mapper=len, 

4844 desc="Length of value", 

4845 ) 

4846 

4847 def get_nargs(self) -> _t.Literal["+", "*"] | int: 

4848 if not self._inner.supports_parse_many(): 

4849 # somebody bound len of a string? 

4850 return self._inner.get_nargs() 

4851 

4852 lower = self._lower_bound 

4853 if lower is not None and not self._lower_bound_is_inclusive: 

4854 lower += 1 

4855 upper = self._upper_bound 

4856 if upper is not None and not self._upper_bound_is_inclusive: 

4857 upper -= 1 

4858 

4859 if lower == upper and lower is not None: 

4860 return lower 

4861 elif lower is not None and lower > 0: 

4862 return "+" 

4863 else: 

4864 return "*" 

4865 

4866 def to_json_schema( 

4867 self, ctx: yuio.json_schema.JsonSchemaContext, / 

4868 ) -> yuio.json_schema.JsonSchemaType: 

4869 bound = {} 

4870 min_bound = self._lower_bound 

4871 if not self._lower_bound_is_inclusive and min_bound is not None: 

4872 min_bound -= 1 

4873 if min_bound is not None: 

4874 bound["minLength"] = bound["minItems"] = bound["minProperties"] = min_bound 

4875 max_bound = self._upper_bound 

4876 if not self._upper_bound_is_inclusive and max_bound is not None: 

4877 max_bound += 1 

4878 if max_bound is not None: 

4879 bound["maxLength"] = bound["maxItems"] = bound["maxProperties"] = max_bound 

4880 if bound: 

4881 return yuio.json_schema.AllOf( 

4882 [super().to_json_schema(ctx), yuio.json_schema.Opaque(bound)] 

4883 ) 

4884 else: 

4885 return super().to_json_schema(ctx) 

4886 

4887 

4888@_t.overload 

4889def LenGt(inner: Parser[Sz], bound: int, /) -> LenBound[Sz]: ... 

4890@_t.overload 

4891def LenGt(bound: int, /) -> PartialParser: ... 

4892def LenGt(*args) -> _t.Any: 

4893 """LenGt(inner: Parser[Sz], bound: int, /) 

4894 

4895 Alias for :class:`LenBound`. 

4896 

4897 :param inner: 

4898 parser whose result will be validated. 

4899 :param bound: 

4900 lower bound for parsed values's length. 

4901 

4902 """ 

4903 

4904 if len(args) == 1: 

4905 return LenBound(lower=args[0]) 

4906 elif len(args) == 2: 

4907 return LenBound(args[0], lower=args[1]) 

4908 else: 

4909 raise TypeError(f"expected 1 or 2 positional arguments, got {len(args)}") 

4910 

4911 

4912@_t.overload 

4913def LenGe(inner: Parser[Sz], bound: int, /) -> LenBound[Sz]: ... 

4914@_t.overload 

4915def LenGe(bound: int, /) -> PartialParser: ... 

4916def LenGe(*args) -> _t.Any: 

4917 """LenGe(inner: Parser[Sz], bound: int, /) 

4918 

4919 Alias for :class:`LenBound`. 

4920 

4921 :param inner: 

4922 parser whose result will be validated. 

4923 :param bound: 

4924 lower inclusive bound for parsed values's length. 

4925 

4926 """ 

4927 

4928 if len(args) == 1: 

4929 return LenBound(lower_inclusive=args[0]) 

4930 elif len(args) == 2: 

4931 return LenBound(args[0], lower_inclusive=args[1]) 

4932 else: 

4933 raise TypeError(f"expected 1 or 2 positional arguments, got {len(args)}") 

4934 

4935 

4936@_t.overload 

4937def LenLt(inner: Parser[Sz], bound: int, /) -> LenBound[Sz]: ... 

4938@_t.overload 

4939def LenLt(bound: int, /) -> PartialParser: ... 

4940def LenLt(*args) -> _t.Any: 

4941 """LenLt(inner: Parser[Sz], bound: int, /) 

4942 

4943 Alias for :class:`LenBound`. 

4944 

4945 :param inner: 

4946 parser whose result will be validated. 

4947 :param bound: 

4948 upper bound for parsed values's length. 

4949 

4950 """ 

4951 

4952 if len(args) == 1: 

4953 return LenBound(upper=args[0]) 

4954 elif len(args) == 2: 

4955 return LenBound(args[0], upper=args[1]) 

4956 else: 

4957 raise TypeError(f"expected 1 or 2 positional arguments, got {len(args)}") 

4958 

4959 

4960@_t.overload 

4961def LenLe(inner: Parser[Sz], bound: int, /) -> LenBound[Sz]: ... 

4962@_t.overload 

4963def LenLe(bound: int, /) -> PartialParser: ... 

4964def LenLe(*args) -> _t.Any: 

4965 """LenLe(inner: Parser[Sz], bound: int, /) 

4966 

4967 Alias for :class:`LenBound`. 

4968 

4969 :param inner: 

4970 parser whose result will be validated. 

4971 :param bound: 

4972 upper inclusive bound for parsed values's length. 

4973 

4974 """ 

4975 

4976 if len(args) == 1: 

4977 return LenBound(upper_inclusive=args[0]) 

4978 elif len(args) == 2: 

4979 return LenBound(args[0], upper_inclusive=args[1]) 

4980 else: 

4981 raise TypeError(f"expected 1 or 2 positional arguments, got {len(args)}") 

4982 

4983 

4984class OneOf(ValidatingParser[T], _t.Generic[T]): 

4985 """OneOf(inner: Parser[T], values: typing.Collection[T], /) 

4986 

4987 Check that the parsed value is one of the given set of values. 

4988 

4989 :param inner: 

4990 parser whose result will be validated. 

4991 :param values: 

4992 collection of allowed values. 

4993 :example: 

4994 :: 

4995 

4996 >>> # Accepts only strings 'A', 'B', or 'C': 

4997 >>> OneOf(Str(), ['A', 'B', 'C']) 

4998 OneOf(Str) 

4999 

5000 """ 

5001 

5002 if TYPE_CHECKING: 

5003 

5004 @_t.overload 

5005 def __new__(cls, inner: Parser[T], values: _t.Collection[T], /) -> OneOf[T]: ... 

5006 

5007 @_t.overload 

5008 def __new__(cls, values: _t.Collection[T], /) -> PartialParser: ... 

5009 

5010 def __new__(cls, *args) -> _t.Any: ... 

5011 

5012 def __init__(self, *args): 

5013 inner: Parser[T] | None 

5014 values: _t.Collection[T] 

5015 if len(args) == 1: 

5016 inner, values = None, args[0] 

5017 elif len(args) == 2: 

5018 inner, values = args 

5019 else: 

5020 raise TypeError(f"expected 1 or 2 positional arguments, got {len(args)}") 

5021 

5022 super().__init__(inner) 

5023 

5024 self._allowed_values = values 

5025 

5026 def _validate(self, value: T, /): 

5027 if value not in self._allowed_values: 

5028 raise ParsingError( 

5029 "Can't parse `%r`, should be %s", 

5030 value, 

5031 yuio.string.JoinRepr.or_(self._allowed_values), 

5032 ) 

5033 

5034 def describe(self) -> str | None: 

5035 desc = "|".join(self.describe_value(e) for e in self._allowed_values) 

5036 if len(desc) < 80: 

5037 if len(self._allowed_values) > 1: 

5038 desc = f"{{{desc}}}" 

5039 return desc 

5040 else: 

5041 return super().describe() 

5042 

5043 def describe_or_def(self) -> str: 

5044 desc = "|".join(self.describe_value(e) for e in self._allowed_values) 

5045 if len(desc) < 80: 

5046 if len(self._allowed_values) > 1: 

5047 desc = f"{{{desc}}}" 

5048 return desc 

5049 else: 

5050 return super().describe_or_def() 

5051 

5052 def options(self) -> _t.Collection[yuio.widget.Option[T]] | None: 

5053 return [ 

5054 yuio.widget.Option(e, self.describe_value(e)) for e in self._allowed_values 

5055 ] 

5056 

5057 def completer(self) -> yuio.complete.Completer | None: 

5058 return yuio.complete.Choice( 

5059 [yuio.complete.Option(self.describe_value(e)) for e in self._allowed_values] 

5060 ) 

5061 

5062 def widget( 

5063 self, 

5064 default: object | yuio.Missing, 

5065 input_description: str | None, 

5066 default_description: str | None, 

5067 /, 

5068 ) -> yuio.widget.Widget[T | yuio.Missing]: 

5069 allowed_values = list(self._allowed_values) 

5070 

5071 options = _t.cast(list[yuio.widget.Option[T | yuio.Missing]], self.options()) 

5072 

5073 if default is yuio.MISSING: 

5074 default_index = 0 

5075 elif default in allowed_values: 

5076 default_index = list(allowed_values).index(default) # type: ignore 

5077 else: 

5078 options.insert( 

5079 0, yuio.widget.Option(yuio.MISSING, default_description or str(default)) 

5080 ) 

5081 default_index = 0 

5082 

5083 return yuio.widget.Choice(options, default_index=default_index) 

5084 

5085 

5086class WithMeta(MappingParser[T, T], _t.Generic[T]): 

5087 """WithMeta(inner: Parser[T], /, *, desc: str, completer: yuio.complete.Completer | None | ~yuio.MISSING = MISSING) 

5088 

5089 Overrides inline help messages and other meta information of a wrapped parser. 

5090 

5091 Inline help messages will show up as hints in autocompletion and widgets. 

5092 

5093 :param inner: 

5094 inner parser. 

5095 :param desc: 

5096 description override. This short string will be used in CLI, widgets, and 

5097 completers to describe expected value. 

5098 :param completer: 

5099 completer override. Pass :data:`None` to disable completion. 

5100 

5101 """ 

5102 

5103 if TYPE_CHECKING: 

5104 

5105 @_t.overload 

5106 def __new__( 

5107 cls, 

5108 inner: Parser[T], 

5109 /, 

5110 *, 

5111 desc: str | None = None, 

5112 completer: yuio.complete.Completer | yuio.Missing | None = yuio.MISSING, 

5113 ) -> MappingParser[T, T]: ... 

5114 

5115 @_t.overload 

5116 def __new__( 

5117 cls, 

5118 /, 

5119 *, 

5120 desc: str | None = None, 

5121 completer: yuio.complete.Completer | yuio.Missing | None = yuio.MISSING, 

5122 ) -> PartialParser: ... 

5123 

5124 def __new__(cls, *args, **kwargs) -> _t.Any: ... 

5125 

5126 def __init__( 

5127 self, 

5128 *args, 

5129 desc: str | None = None, 

5130 completer: yuio.complete.Completer | yuio.Missing | None = yuio.MISSING, 

5131 ): 

5132 inner: Parser[T] | None 

5133 if not args: 

5134 inner = None 

5135 elif len(args) == 1: 

5136 inner = args[0] 

5137 else: 

5138 raise TypeError(f"expected at most 1 positional argument, got {len(args)}") 

5139 

5140 self._desc = desc 

5141 self._completer = completer 

5142 super().__init__(inner) 

5143 

5144 def check_type(self, value: object, /) -> _t.TypeGuard[T]: 

5145 return True 

5146 

5147 def describe(self) -> str | None: 

5148 return self._desc or self._inner.describe() 

5149 

5150 def describe_or_def(self) -> str: 

5151 return self._desc or self._inner.describe_or_def() 

5152 

5153 def describe_many(self) -> str | tuple[str, ...]: 

5154 return self._desc or self._inner.describe_many() 

5155 

5156 def describe_value(self, value: object, /) -> str: 

5157 return self._inner.describe_value(value) 

5158 

5159 def parse_with_ctx(self, ctx: StrParsingContext, /) -> T: 

5160 return self._inner.parse_with_ctx(ctx) 

5161 

5162 def parse_many_with_ctx(self, ctxs: _t.Sequence[StrParsingContext], /) -> T: 

5163 return self._inner.parse_many_with_ctx(ctxs) 

5164 

5165 def parse_config_with_ctx(self, ctx: ConfigParsingContext, /) -> T: 

5166 return self._inner.parse_config_with_ctx(ctx) 

5167 

5168 def options(self) -> _t.Collection[yuio.widget.Option[T]] | None: 

5169 return self._inner.options() 

5170 

5171 def completer(self) -> yuio.complete.Completer | None: 

5172 if self._completer is not yuio.MISSING: 

5173 return self._completer # type: ignore 

5174 else: 

5175 return self._inner.completer() 

5176 

5177 def widget( 

5178 self, 

5179 default: object | yuio.Missing, 

5180 input_description: str | None, 

5181 default_description: str | None, 

5182 /, 

5183 ) -> yuio.widget.Widget[T | yuio.Missing]: 

5184 return self._inner.widget(default, input_description, default_description) 

5185 

5186 def to_json_value(self, value: object) -> yuio.json_schema.JsonValue: 

5187 return self._inner.to_json_value(value) 

5188 

5189 

5190class _WidgetResultMapper(yuio.widget.Map[T | yuio.Missing, str]): 

5191 def __init__( 

5192 self, 

5193 parser: Parser[T], 

5194 input_description: str | None, 

5195 default: object | yuio.Missing, 

5196 widget: yuio.widget.Widget[str], 

5197 ): 

5198 self._parser = parser 

5199 self._input_description = input_description 

5200 self._default = default 

5201 super().__init__(widget, self.mapper) 

5202 

5203 def mapper(self, s: str) -> T | yuio.Missing: 

5204 if not s and self._default is not yuio.MISSING: 

5205 return yuio.MISSING 

5206 elif not s: 

5207 raise ParsingError("Input is required") 

5208 try: 

5209 return self._parser.parse_with_ctx(StrParsingContext(s)) 

5210 except ParsingError as e: 

5211 if ( 

5212 isinstance( 

5213 self._inner, (yuio.widget.Input, yuio.widget.InputWithCompletion) 

5214 ) 

5215 and e.pos 

5216 and e.raw == self._inner.text 

5217 ): 

5218 if e.pos == (0, len(self._inner.text)): 

5219 # Don't highlight the entire text, it's not useful and creates 

5220 # visual noise. 

5221 self._inner.err_region = None 

5222 else: 

5223 self._inner.err_region = e.pos 

5224 e.raw = None 

5225 e.pos = None 

5226 raise 

5227 

5228 @property 

5229 def help_data(self): 

5230 return super().help_data.with_action( 

5231 group="Input Format", 

5232 msg=self._input_description, 

5233 prepend=True, 

5234 prepend_group=True, 

5235 ) 

5236 

5237 

5238def _secret_widget( 

5239 parser: Parser[T], 

5240 default: object | yuio.Missing, 

5241 input_description: str | None, 

5242 default_description: str | None, 

5243 /, 

5244) -> yuio.widget.Widget[T | yuio.Missing]: 

5245 return _WidgetResultMapper( 

5246 parser, 

5247 input_description, 

5248 default, 

5249 ( 

5250 yuio.widget.SecretInput( 

5251 placeholder=default_description or "", 

5252 ) 

5253 ), 

5254 ) 

5255 

5256 

5257class StrParsingContext: 

5258 """StrParsingContext(content: str, /, *, n_arg: int | None = None) 

5259 

5260 String parsing context tracks current position in the string. 

5261 

5262 :param content: 

5263 content to parse. 

5264 :param n_arg: 

5265 content index when using :meth:`~Parser.parse_many`. 

5266 

5267 """ 

5268 

5269 def __init__( 

5270 self, 

5271 content: str, 

5272 /, 

5273 *, 

5274 n_arg: int | None = None, 

5275 _value: str | None = None, 

5276 _start: int | None = None, 

5277 _end: int | None = None, 

5278 ): 

5279 self.start: int = _start if _start is not None else 0 

5280 """ 

5281 Start position of the value. 

5282 

5283 """ 

5284 

5285 self.end: int = _end if _end is not None else self.start + len(content) 

5286 """ 

5287 End position of the value. 

5288 

5289 """ 

5290 

5291 self.content: str = content 

5292 """ 

5293 Full content of the value that was passed to :meth:`Parser.parse`. 

5294 

5295 """ 

5296 

5297 self.value: str = _value if _value is not None else content 

5298 """ 

5299 Part of the :attr:`~StrParsingContext.content` that's currently being parsed. 

5300 

5301 """ 

5302 

5303 self.n_arg: int | None = n_arg 

5304 """ 

5305 For :meth:`~Parser.parse_many`, this attribute contains index of the value 

5306 that is being parsed. For :meth:`~Parser.parse`, this is :data:`None`. 

5307 

5308 """ 

5309 

5310 def split( 

5311 self, delimiter: str | None = None, /, maxsplit: int = -1 

5312 ) -> _t.Generator[StrParsingContext]: 

5313 """ 

5314 Split current value by the given delimiter while keeping track of the current position. 

5315 

5316 """ 

5317 

5318 if delimiter is None: 

5319 yield from self._split_space(maxsplit=maxsplit) 

5320 return 

5321 

5322 dlen = len(delimiter) 

5323 start = self.start 

5324 for part in self.value.split(delimiter, maxsplit=maxsplit): 

5325 yield StrParsingContext( 

5326 self.content, 

5327 _value=part, 

5328 _start=start, 

5329 _end=start + len(part), 

5330 n_arg=self.n_arg, 

5331 ) 

5332 start += len(part) + dlen 

5333 

5334 def _split_space(self, maxsplit: int = -1) -> _t.Generator[StrParsingContext]: 

5335 i = 0 

5336 n_splits = 0 

5337 is_space = True 

5338 for part in re.split(r"(\s+)", self.value): 

5339 is_space = not is_space 

5340 if is_space: 

5341 i += len(part) 

5342 continue 

5343 

5344 if not part: 

5345 continue 

5346 

5347 if maxsplit >= 0 and n_splits >= maxsplit: 

5348 part = self.value[i:] 

5349 yield StrParsingContext( 

5350 self.content, 

5351 _value=part, 

5352 _start=i, 

5353 _end=i + len(part), 

5354 n_arg=self.n_arg, 

5355 ) 

5356 return 

5357 else: 

5358 yield StrParsingContext( 

5359 self.content, 

5360 _value=part, 

5361 _start=i, 

5362 _end=i + len(part), 

5363 n_arg=self.n_arg, 

5364 ) 

5365 i += len(part) 

5366 n_splits += 1 

5367 

5368 def strip(self, chars: str | None = None, /) -> StrParsingContext: 

5369 """ 

5370 Strip current value while keeping track of the current position. 

5371 

5372 """ 

5373 

5374 l_stripped = self.value.lstrip(chars) 

5375 start = self.start + (len(self.value) - len(l_stripped)) 

5376 stripped = l_stripped.rstrip(chars) 

5377 return StrParsingContext( 

5378 self.content, 

5379 _value=stripped, 

5380 _start=start, 

5381 _end=start + len(stripped), 

5382 n_arg=self.n_arg, 

5383 ) 

5384 

5385 def strip_if_non_space(self) -> StrParsingContext: 

5386 """ 

5387 Strip current value unless it entirely consists of spaces. 

5388 

5389 """ 

5390 

5391 if not self.value or self.value.isspace(): 

5392 return self 

5393 else: 

5394 return self.strip() 

5395 

5396 # If you need more methods, feel free to open an issue or send a PR! 

5397 # For now, `split` and `split` is enough. 

5398 

5399 

5400class ConfigParsingContext: 

5401 """ 

5402 Config parsing context tracks path in the config, similar to JSON path. 

5403 

5404 """ 

5405 

5406 def __init__( 

5407 self, 

5408 value: object, 

5409 /, 

5410 *, 

5411 parent: ConfigParsingContext | None = None, 

5412 key: _t.Any = None, 

5413 desc: str | None = None, 

5414 ): 

5415 self.value: object = value 

5416 """ 

5417 Config value to be validated and parsed. 

5418 

5419 """ 

5420 

5421 self.parent: ConfigParsingContext | None = parent 

5422 """ 

5423 Parent context. 

5424 

5425 """ 

5426 

5427 self.key: _t.Any = key 

5428 """ 

5429 Key that was accessed when we've descended from parent context to this one. 

5430 

5431 Root context has key :data:`None`. 

5432 

5433 """ 

5434 

5435 self.desc: str | None = desc 

5436 """ 

5437 Additional description of the key. 

5438 

5439 """ 

5440 

5441 def descend( 

5442 self, 

5443 value: _t.Any, 

5444 key: _t.Any, 

5445 desc: str | None = None, 

5446 ) -> ConfigParsingContext: 

5447 """ 

5448 Create a new context that adds a new key to the path. 

5449 

5450 :param value: 

5451 inner value that was derived from the current value by accessing it with 

5452 the given `key`. 

5453 :param key: 

5454 key that we use to descend into the current value. 

5455 

5456 For example, let's say we're parsing a list. We iterate over it and pass 

5457 its elements to a sub-parser. Before calling a sub-parser, we need to 

5458 make a new context for it. In this situation, we'll pass current element 

5459 as `value`, and is index as `key`. 

5460 :param desc: 

5461 human-readable description for the new context. Will be colorized 

5462 and ``%``-formatted with a single named argument `key`. 

5463 

5464 This is useful when parsing structures that need something more complex than 

5465 JSON path. For example, when parsing a key in a dictionary, it is helpful 

5466 to set description to something like ``"key of element #%(key)r"``. 

5467 This way, parsing errors will have a more clear message: 

5468 

5469 .. code-block:: text 

5470 

5471 Parsing error: 

5472 In key of element #2: 

5473 Expected str, got int: 10 

5474 

5475 """ 

5476 

5477 return ConfigParsingContext(value, parent=self, key=key, desc=desc) 

5478 

5479 def make_path(self) -> list[tuple[_t.Any, str | None]]: 

5480 """ 

5481 Capture current path. 

5482 

5483 :returns: 

5484 a list of tuples. First element of each tuple is a key, second is 

5485 an additional description. 

5486 

5487 """ 

5488 

5489 path = [] 

5490 

5491 root = self 

5492 while True: 

5493 if root.parent is None: 

5494 break 

5495 else: 

5496 path.append((root.key, root.desc)) 

5497 root = root.parent 

5498 

5499 path.reverse() 

5500 

5501 return path 

5502 

5503 

5504class _PathRenderer: 

5505 def __init__(self, path: list[tuple[_t.Any, str | None]]): 

5506 self._path = path 

5507 

5508 def __colorized_str__( 

5509 self, ctx: yuio.string.ReprContext 

5510 ) -> yuio.string.ColorizedString: 

5511 code_color = ctx.theme.get_color("msg/text:code/repr hl:repr") 

5512 punct_color = ctx.theme.get_color("msg/text:code/repr hl/punct:repr") 

5513 

5514 msg = yuio.string.ColorizedString(code_color) 

5515 msg.start_no_wrap() 

5516 

5517 for i, (key, desc) in enumerate(self._path): 

5518 if desc: 

5519 desc = ( 

5520 (yuio.string) 

5521 .colorize(desc, ctx=ctx) 

5522 .percent_format({"key": key}, ctx=ctx) 

5523 ) 

5524 

5525 if i == len(self._path) - 1: 

5526 # Last key. 

5527 if msg: 

5528 msg.append_color(punct_color) 

5529 msg.append_str(", ") 

5530 msg.append_colorized_str(desc) 

5531 else: 

5532 # Element in the middle. 

5533 if not msg: 

5534 msg.append_str("$") 

5535 msg.append_color(punct_color) 

5536 msg.append_str(".<") 

5537 msg.append_colorized_str(desc) 

5538 msg.append_str(">") 

5539 elif isinstance(key, str) and re.match(r"^[a-zA-Z_][\w-]*$", key): 

5540 # Key is identifier-like, use `x.key` notation. 

5541 if not msg: 

5542 msg.append_str("$") 

5543 msg.append_color(punct_color) 

5544 msg.append_str(".") 

5545 msg.append_color(code_color) 

5546 msg.append_str(key) 

5547 else: 

5548 # Key is not identifier-like, use `x[key]` notation. 

5549 if not msg: 

5550 msg.append_str("$") 

5551 msg.append_color(punct_color) 

5552 msg.append_str("[") 

5553 msg.append_color(code_color) 

5554 msg.append_str(repr(key)) 

5555 msg.append_color(punct_color) 

5556 msg.append_str("]") 

5557 

5558 msg.end_no_wrap() 

5559 return msg 

5560 

5561 

5562class _CodeRenderer: 

5563 def __init__(self, code: str, pos: tuple[int, int], as_cli: bool = False): 

5564 self._code = code 

5565 self._pos = pos 

5566 self._as_cli = as_cli 

5567 

5568 def __colorized_str__( 

5569 self, ctx: yuio.string.ReprContext 

5570 ) -> yuio.string.ColorizedString: 

5571 width = ctx.width - 2 # Account for indentation. 

5572 

5573 if width < 10: # 6 symbols for ellipsis and at least 2 wide chars. 

5574 return yuio.string.ColorizedString() 

5575 

5576 start, end = self._pos 

5577 if end == start: 

5578 end += 1 

5579 

5580 left = self._code[:start] 

5581 center = self._code[start:end] 

5582 right = self._code[end:] 

5583 

5584 l_width = yuio.string.line_width(left) 

5585 c_width = yuio.string.line_width(center) 

5586 r_width = yuio.string.line_width(right) 

5587 

5588 available_width = width - (3 if left else 0) - 3 

5589 if c_width > available_width: 

5590 # Center can't fit: remove left and right side, 

5591 # and trim as much center as needed. 

5592 

5593 left = "..." if l_width > 3 else left 

5594 l_width = len(left) 

5595 

5596 right = "" 

5597 r_width = 0 

5598 

5599 new_c = "" 

5600 c_width = 0 

5601 

5602 for c in center: 

5603 cw = yuio.string.line_width(c) 

5604 if c_width + cw <= available_width: 

5605 new_c += c 

5606 c_width += cw 

5607 else: 

5608 new_c += "..." 

5609 c_width += 3 

5610 break 

5611 center = new_c 

5612 

5613 if r_width > 3 and l_width + c_width + r_width > width: 

5614 # Trim right side. 

5615 new_r = "" 

5616 r_width = 3 

5617 for c in right: 

5618 cw = yuio.string.line_width(c) 

5619 if l_width + c_width + r_width + cw <= width: 

5620 new_r += c 

5621 r_width += cw 

5622 else: 

5623 new_r += "..." 

5624 break 

5625 right = new_r 

5626 

5627 if l_width > 3 and l_width + c_width + r_width > width: 

5628 # Trim left side. 

5629 new_l = "" 

5630 l_width = 3 

5631 for c in left[::-1]: 

5632 cw = yuio.string.line_width(c) 

5633 if l_width + c_width + r_width + cw <= width: 

5634 new_l += c 

5635 l_width += cw 

5636 else: 

5637 new_l += "..." 

5638 break 

5639 left = new_l[::-1] 

5640 

5641 if self._as_cli: 

5642 punct_color = ctx.theme.get_color( 

5643 "msg/text:code/sh-usage hl/punct:sh-usage" 

5644 ) 

5645 else: 

5646 punct_color = ctx.theme.get_color("msg/text:code/text hl/punct:text") 

5647 

5648 res = yuio.string.ColorizedString() 

5649 res.start_no_wrap() 

5650 

5651 if self._as_cli: 

5652 res.append_color(punct_color) 

5653 res.append_str("$ ") 

5654 res.append_colorized_str( 

5655 ctx.str( 

5656 yuio.string.Hl( 

5657 left.replace("%", "%%") + "%s" + right.replace("%", "%%"), # pyright: ignore[reportArgumentType] 

5658 yuio.string.WithBaseColor( 

5659 center, base_color="hl/error:sh-usage" 

5660 ), 

5661 syntax="sh-usage", 

5662 ) 

5663 ) 

5664 ) 

5665 else: 

5666 text_color = ctx.theme.get_color("msg/text:code/text") 

5667 res.append_color(punct_color) 

5668 res.append_str("> ") 

5669 res.append_color(text_color) 

5670 res.append_str(left) 

5671 res.append_color(text_color | ctx.theme.get_color("hl/error:text")) 

5672 res.append_str(center) 

5673 res.append_color(text_color) 

5674 res.append_str(right) 

5675 res.append_color(yuio.color.Color.NONE) 

5676 res.append_str("\n") 

5677 if self._as_cli: 

5678 text_color = ctx.theme.get_color("msg/text:code/sh-usage") 

5679 res.append_color(text_color | ctx.theme.get_color("hl/error:sh-usage")) 

5680 else: 

5681 text_color = ctx.theme.get_color("msg/text:code/text") 

5682 res.append_color(text_color | ctx.theme.get_color("hl/error:text")) 

5683 res.append_str(" ") 

5684 res.append_str(" " * yuio.string.line_width(left)) 

5685 res.append_str("~" * yuio.string.line_width(center)) 

5686 

5687 res.end_no_wrap() 

5688 

5689 return res 

5690 

5691 

5692def _repr_and_adjust_pos(s: str, pos: tuple[int, int]): 

5693 start, end = pos 

5694 

5695 left = json.dumps(s[:start])[:-1] 

5696 center = json.dumps(s[start:end])[1:-1] 

5697 right = json.dumps(s[end:])[1:] 

5698 

5699 return left + center + right, (len(left), len(left) + len(center)) 

5700 

5701 

5702_FromTypeHintCallback: _t.TypeAlias = _t.Callable[ 

5703 [type, type | None, tuple[object, ...]], Parser[object] | None 

5704] 

5705 

5706 

5707_FROM_TYPE_HINT_CALLBACKS: list[tuple[_FromTypeHintCallback, bool]] = [] 

5708_FROM_TYPE_HINT_DELIM_SUGGESTIONS: list[str | None] = [ 

5709 None, 

5710 ",", 

5711 "@", 

5712 "/", 

5713 "=", 

5714] 

5715 

5716 

5717class _FromTypeHintDepth(threading.local): 

5718 def __init__(self): 

5719 self.depth: int = 0 

5720 self.uses_delim = False 

5721 

5722 

5723_FROM_TYPE_HINT_DEPTH: _FromTypeHintDepth = _FromTypeHintDepth() 

5724 

5725 

5726@_t.overload 

5727def from_type_hint(ty: type[T], /) -> Parser[T]: ... 

5728@_t.overload 

5729def from_type_hint(ty: object, /) -> Parser[object]: ... 

5730def from_type_hint(ty: _t.Any, /) -> Parser[object]: 

5731 """from_type_hint(ty: type[T], /) -> Parser[T] 

5732 

5733 Create parser from a type hint. 

5734 

5735 :param ty: 

5736 a type hint. 

5737 

5738 This type hint should not contain strings or forward references. Make sure 

5739 they're resolved before passing it to this function. 

5740 :returns: 

5741 a parser instance created from type hint. 

5742 :raises: 

5743 :class:`TypeError` if type hint contains forward references or types 

5744 that don't have associated parsers. 

5745 :example: 

5746 :: 

5747 

5748 >>> from_type_hint(list[int] | None) 

5749 Optional(List(Int)) 

5750 

5751 """ 

5752 

5753 result = _from_type_hint(ty) 

5754 setattr(result, "_Parser__typehint", ty) 

5755 return result 

5756 

5757 

5758def _from_type_hint(ty: _t.Any, /) -> Parser[object]: 

5759 if isinstance(ty, (str, _t.ForwardRef)): 

5760 raise TypeError(f"forward references are not supported here: {ty}") 

5761 

5762 origin = _t.get_origin(ty) 

5763 args = _t.get_args(ty) 

5764 

5765 if origin is _t.Annotated: 

5766 p = from_type_hint(args[0]) 

5767 for arg in args[1:]: 

5768 if isinstance(arg, PartialParser): 

5769 p = arg.wrap(p) 

5770 return p 

5771 

5772 for cb, uses_delim in _FROM_TYPE_HINT_CALLBACKS: 

5773 prev_uses_delim = _FROM_TYPE_HINT_DEPTH.uses_delim 

5774 _FROM_TYPE_HINT_DEPTH.uses_delim = uses_delim 

5775 _FROM_TYPE_HINT_DEPTH.depth += uses_delim 

5776 try: 

5777 p = cb(ty, origin, args) 

5778 if p is not None: 

5779 return p 

5780 finally: 

5781 _FROM_TYPE_HINT_DEPTH.uses_delim = prev_uses_delim 

5782 _FROM_TYPE_HINT_DEPTH.depth -= uses_delim 

5783 

5784 if _tx.is_union(origin): 

5785 if is_optional := (type(None) in args): 

5786 args = list(args) 

5787 args.remove(type(None)) 

5788 if len(args) == 1: 

5789 p = from_type_hint(args[0]) 

5790 else: 

5791 p = Union(*[from_type_hint(arg) for arg in args]) 

5792 if is_optional: 

5793 p = Optional(p) 

5794 return p 

5795 else: 

5796 raise TypeError(f"unsupported type {_tx.type_repr(ty)}") 

5797 

5798 

5799@_t.overload 

5800def register_type_hint_conversion( 

5801 cb: _FromTypeHintCallback, 

5802 /, 

5803 *, 

5804 uses_delim: bool = False, 

5805) -> _FromTypeHintCallback: ... 

5806@_t.overload 

5807def register_type_hint_conversion( 

5808 *, 

5809 uses_delim: bool = False, 

5810) -> _t.Callable[[_FromTypeHintCallback], _FromTypeHintCallback]: ... 

5811def register_type_hint_conversion( 

5812 cb: _FromTypeHintCallback | None = None, 

5813 /, 

5814 *, 

5815 uses_delim: bool = False, 

5816) -> ( 

5817 _FromTypeHintCallback | _t.Callable[[_FromTypeHintCallback], _FromTypeHintCallback] 

5818): 

5819 """ 

5820 Register a new converter from a type hint to a parser. 

5821 

5822 This function takes a callback that accepts three positional arguments: 

5823 

5824 - a type hint, 

5825 - a type hint's origin (as defined by :func:`typing.get_origin`), 

5826 - a type hint's args (as defined by :func:`typing.get_args`). 

5827 

5828 The callback should return a parser if it can, or :data:`None` otherwise. 

5829 

5830 All registered callbacks are tried in the same order 

5831 as they were registered. 

5832 

5833 If `uses_delim` is :data:`True`, callback can use 

5834 :func:`suggest_delim_for_type_hint_conversion`. 

5835 

5836 This function can be used as a decorator. 

5837 

5838 :param cb: 

5839 a function that should inspect a type hint and possibly return a parser. 

5840 :param uses_delim: 

5841 indicates that callback will use 

5842 :func:`suggest_delim_for_type_hint_conversion`. 

5843 :example: 

5844 .. invisible-code-block: python 

5845 

5846 class MyType: ... 

5847 class MyTypeParser(ValueParser[MyType]): 

5848 def __init__(self): super().__init__(MyType) 

5849 def parse_with_ctx(self, ctx: StrParsingContext, /): ... 

5850 def parse_config_with_ctx(self, value, /): ... 

5851 def to_json_schema(self, ctx, /): ... 

5852 def to_json_value(self, value, /): ... 

5853 

5854 .. code-block:: python 

5855 

5856 @register_type_hint_conversion 

5857 def my_type_conversion(ty, origin, args): 

5858 if ty is MyType: 

5859 return MyTypeParser() 

5860 else: 

5861 return None 

5862 

5863 :: 

5864 

5865 >>> from_type_hint(MyType) 

5866 MyTypeParser 

5867 

5868 .. invisible-code-block: python 

5869 

5870 del _FROM_TYPE_HINT_CALLBACKS[-1] 

5871 

5872 """ 

5873 

5874 def registrar(cb: _FromTypeHintCallback): 

5875 _FROM_TYPE_HINT_CALLBACKS.append((cb, uses_delim)) 

5876 return cb 

5877 

5878 return registrar(cb) if cb is not None else registrar 

5879 

5880 

5881def suggest_delim_for_type_hint_conversion() -> str | None: 

5882 """ 

5883 Suggests a delimiter for use in type hint converters. 

5884 

5885 When creating a parser for a collection of items based on a type hint, 

5886 it is important to use different delimiters for nested collections. 

5887 This function can suggest such a delimiter based on the current type hint's depth. 

5888 

5889 .. invisible-code-block: python 

5890 

5891 class MyCollection(list, _t.Generic[T]): ... 

5892 class MyCollectionParser(CollectionParser[MyCollection[T], T], _t.Generic[T]): 

5893 def __init__(self, inner: Parser[T], /, *, delimiter: _t.Optional[str] = None): 

5894 super().__init__(inner, ty=MyCollection, ctor=MyCollection, delimiter=delimiter) 

5895 def to_json_schema(self, ctx, /): ... 

5896 def to_json_value(self, value, /): ... 

5897 

5898 :raises: 

5899 :class:`RuntimeError` if called from a type converter that 

5900 didn't set `uses_delim` to :data:`True`. 

5901 :example: 

5902 .. code-block:: python 

5903 

5904 @register_type_hint_conversion(uses_delim=True) 

5905 def my_collection_conversion(ty, origin, args): 

5906 if origin is MyCollection: 

5907 return MyCollectionParser( 

5908 from_type_hint(args[0]), 

5909 delimiter=suggest_delim_for_type_hint_conversion(), 

5910 ) 

5911 else: 

5912 return None 

5913 

5914 :: 

5915 

5916 >>> parser = from_type_hint(MyCollection[MyCollection[str]]) 

5917 >>> parser 

5918 MyCollectionParser(MyCollectionParser(Str)) 

5919 >>> # First delimiter is `None`, meaning split by whitespace: 

5920 >>> parser._delimiter is None 

5921 True 

5922 >>> # Second delimiter is `","`: 

5923 >>> parser._inner._delimiter == "," 

5924 True 

5925 

5926 .. 

5927 >>> del _FROM_TYPE_HINT_CALLBACKS[-1] 

5928 

5929 """ 

5930 

5931 if not _FROM_TYPE_HINT_DEPTH.uses_delim: 

5932 raise RuntimeError( 

5933 "looking up delimiters is not available in this callback; did you forget" 

5934 " to pass `uses_delim=True` when registering this callback?" 

5935 ) 

5936 

5937 depth = _FROM_TYPE_HINT_DEPTH.depth - 1 

5938 if depth < len(_FROM_TYPE_HINT_DELIM_SUGGESTIONS): 

5939 return _FROM_TYPE_HINT_DELIM_SUGGESTIONS[depth] 

5940 else: 

5941 return None 

5942 

5943 

5944register_type_hint_conversion(lambda ty, origin, args: Str() if ty is str else None) 

5945register_type_hint_conversion(lambda ty, origin, args: Int() if ty is int else None) 

5946register_type_hint_conversion(lambda ty, origin, args: Float() if ty is float else None) 

5947register_type_hint_conversion(lambda ty, origin, args: Bool() if ty is bool else None) 

5948register_type_hint_conversion( 

5949 lambda ty, origin, args: ( 

5950 Enum(ty) if isinstance(ty, type) and issubclass(ty, enum.Enum) else None 

5951 ) 

5952) 

5953register_type_hint_conversion( 

5954 lambda ty, origin, args: Decimal() if ty is decimal.Decimal else None 

5955) 

5956register_type_hint_conversion( 

5957 lambda ty, origin, args: Fraction() if ty is fractions.Fraction else None 

5958) 

5959register_type_hint_conversion( 

5960 lambda ty, origin, args: ( 

5961 List( 

5962 from_type_hint(args[0]), delimiter=suggest_delim_for_type_hint_conversion() 

5963 ) 

5964 if origin is list 

5965 else None 

5966 ), 

5967 uses_delim=True, 

5968) 

5969register_type_hint_conversion( 

5970 lambda ty, origin, args: ( 

5971 Set(from_type_hint(args[0]), delimiter=suggest_delim_for_type_hint_conversion()) 

5972 if origin is set 

5973 else None 

5974 ), 

5975 uses_delim=True, 

5976) 

5977register_type_hint_conversion( 

5978 lambda ty, origin, args: ( 

5979 FrozenSet( 

5980 from_type_hint(args[0]), delimiter=suggest_delim_for_type_hint_conversion() 

5981 ) 

5982 if origin is frozenset 

5983 else None 

5984 ), 

5985 uses_delim=True, 

5986) 

5987register_type_hint_conversion( 

5988 lambda ty, origin, args: ( 

5989 Dict( 

5990 from_type_hint(args[0]), 

5991 from_type_hint(args[1]), 

5992 delimiter=suggest_delim_for_type_hint_conversion(), 

5993 ) 

5994 if origin is dict 

5995 else None 

5996 ), 

5997 uses_delim=True, 

5998) 

5999register_type_hint_conversion( 

6000 lambda ty, origin, args: ( 

6001 Tuple( 

6002 *[from_type_hint(arg) for arg in args], 

6003 delimiter=suggest_delim_for_type_hint_conversion(), 

6004 ) 

6005 if origin is tuple and ... not in args 

6006 else None 

6007 ), 

6008 uses_delim=True, 

6009) 

6010register_type_hint_conversion( 

6011 lambda ty, origin, args: Path() if ty is pathlib.Path else None 

6012) 

6013register_type_hint_conversion( 

6014 lambda ty, origin, args: Json() if ty is yuio.json_schema.JsonValue else None 

6015) 

6016register_type_hint_conversion( 

6017 lambda ty, origin, args: DateTime() if ty is datetime.datetime else None 

6018) 

6019register_type_hint_conversion( 

6020 lambda ty, origin, args: Date() if ty is datetime.date else None 

6021) 

6022register_type_hint_conversion( 

6023 lambda ty, origin, args: Time() if ty is datetime.time else None 

6024) 

6025register_type_hint_conversion( 

6026 lambda ty, origin, args: TimeDelta() if ty is datetime.timedelta else None 

6027) 

6028 

6029 

6030@register_type_hint_conversion 

6031def __secret(ty, origin, args): 

6032 if ty is SecretValue: 

6033 raise TypeError("yuio.secret.SecretValue requires type arguments") 

6034 if origin is SecretValue: 

6035 if len(args) == 1: 

6036 return Secret(from_type_hint(args[0])) 

6037 else: # pragma: no cover 

6038 raise TypeError( 

6039 f"yuio.secret.SecretValue requires 1 type argument, got {len(args)}" 

6040 ) 

6041 return None 

6042 

6043 

6044def _is_optional_parser(parser: Parser[_t.Any] | None, /) -> bool: 

6045 while parser is not None: 

6046 if isinstance(parser, Optional): 

6047 return True 

6048 parser = parser.__wrapped_parser__ 

6049 return False 

6050 

6051 

6052def _is_bool_parser(parser: Parser[_t.Any] | None, /) -> bool: 

6053 while parser is not None: 

6054 if isinstance(parser, Bool): 

6055 return True 

6056 parser = parser.__wrapped_parser__ 

6057 return False