Coverage for yuio / parse.py: 88%

1278 statements  

« prev     ^ index     » next       coverage.py v7.12.0, created at 2025-12-04 10:05 +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 

52 

53Value parsers 

54------------- 

55 

56.. autoclass:: Str 

57 

58.. autoclass:: Int 

59 

60.. autoclass:: Float 

61 

62.. autoclass:: Bool 

63 

64.. autoclass:: Enum 

65 

66.. autoclass:: Decimal 

67 

68.. autoclass:: Fraction 

69 

70.. autoclass:: Json 

71 

72.. autoclass:: List 

73 

74.. autoclass:: Set 

75 

76.. autoclass:: FrozenSet 

77 

78.. autoclass:: Dict 

79 

80.. autoclass:: Tuple 

81 

82.. autoclass:: Optional 

83 

84.. autoclass:: Union 

85 

86.. autoclass:: Path 

87 

88.. autoclass:: NonExistentPath 

89 

90.. autoclass:: ExistingPath 

91 

92.. autoclass:: File 

93 

94.. autoclass:: Dir 

95 

96.. autoclass:: GitRepo 

97 

98 

99.. _validating-parsers: 

100 

101Validators 

102---------- 

103 

104.. autoclass:: Regex 

105 

106.. autoclass:: Bound 

107 

108.. autoclass:: Gt 

109 

110.. autoclass:: Ge 

111 

112.. autoclass:: Lt 

113 

114.. autoclass:: Le 

115 

116.. autoclass:: LenBound 

117 

118.. autoclass:: LenGt 

119 

120.. autoclass:: LenGe 

121 

122.. autoclass:: LenLt 

123 

124.. autoclass:: LenLe 

125 

126.. autoclass:: OneOf 

127 

128 

129Auxiliary parsers 

130----------------- 

131 

132.. autoclass:: Map 

133 

134.. autoclass:: Apply 

135 

136.. autoclass:: Lower 

137 

138.. autoclass:: Upper 

139 

140.. autoclass:: CaseFold 

141 

142.. autoclass:: Strip 

143 

144.. autoclass:: WithMeta 

145 

146.. autoclass:: Secret 

147 

148 

149Deriving parsers from type hints 

150-------------------------------- 

151 

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

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

154 

155.. autofunction:: from_type_hint 

156 

157 

158.. _partial parsers: 

159 

160Partial parsers 

161--------------- 

162 

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

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

165 

166.. invisible-code-block: python 

167 

168 from yuio.config import Config, field 

169 

170.. code-block:: python 

171 

172 class AppConfig(Config): 

173 max_line_width: int | str = field( 

174 default="default", 

175 parser=Union( 

176 Gt(Int(), 0), 

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

178 ), 

179 ) 

180 

181.. invisible-code-block: python 

182 

183 AppConfig() 

184 

185Instead, we can use :class:`typing.Annotated` to attach validating parsers directly 

186to type hints: 

187 

188.. code-block:: python 

189 

190 from typing import Annotated 

191 

192 

193 class AppConfig(Config): 

194 max_line_width: ( 

195 Annotated[int, Gt(0)] 

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

197 ) = "default" 

198 

199.. invisible-code-block: python 

200 

201 AppConfig() 

202 

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

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

205about their settings. 

206 

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

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

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

210 

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

212 >>> partial_parser.parse("1,2,3") # doctest: +ELLIPSIS 

213 Traceback (most recent call last): 

214 ... 

215 TypeError: List requires an inner parser 

216 ... 

217 

218 

219Other parser methods 

220-------------------- 

221 

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

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

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

225 

226.. autoclass:: Parser 

227 :noindex: 

228 

229 .. autoattribute:: __wrapped_parser__ 

230 

231 .. automethod:: get_nargs 

232 

233 .. automethod:: check_type 

234 

235 .. automethod:: assert_type 

236 

237 .. automethod:: describe 

238 

239 .. automethod:: describe_or_def 

240 

241 .. automethod:: describe_many 

242 

243 .. automethod:: describe_value 

244 

245 .. automethod:: options 

246 

247 .. automethod:: completer 

248 

249 .. automethod:: widget 

250 

251 .. automethod:: to_json_schema 

252 

253 .. automethod:: to_json_value 

254 

255 .. automethod:: is_secret 

256 

257 

258Building your own parser 

259------------------------ 

260 

261.. _parser hierarchy: 

262 

263Understanding parser hierarchy 

264~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 

265 

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

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

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

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

270 

271.. raw:: html 

272 

273 <p> 

274 <pre class="mermaid"> 

275 --- 

276 config: 

277 class: 

278 hideEmptyMembersBox: true 

279 --- 

280 classDiagram 

281 

282 class PartialParser 

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

284 

285 class Parser 

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

287 PartialParser <|-- Parser 

288 

289 class ValueParser 

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

291 Parser <|-- ValueParser 

292 

293 class WrappingParser 

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

295 Parser <|-- WrappingParser 

296 

297 class MappingParser 

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

299 WrappingParser <|-- MappingParser 

300 

301 class Map 

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

303 MappingParser <|-- Map 

304 

305 class Apply 

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

307 MappingParser <|-- Apply 

308 

309 class ValidatingParser 

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

311 Apply <|-- ValidatingParser 

312 

313 class CollectionParser 

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

315 ValueParser <|-- CollectionParser 

316 WrappingParser <|-- CollectionParser 

317 </pre> 

318 </p> 

319 

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

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

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

323consider this code: 

324 

325.. skip: next 

326 

327.. code-block:: python 

328 

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

330 

331This will fail because ``List`` needs an inner parser to function. 

332 

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

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

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

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

337 

338.. skip: next 

339 

340.. code-block:: python 

341 

342 from typing import TYPE_CHECKING, Any, Generic, overload 

343 

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

345 if TYPE_CHECKING: 

346 @overload 

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

348 ... 

349 @overload 

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

351 ... 

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

353 ... 

354 

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

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

357 

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

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

360 

361.. autoclass:: PartialParser 

362 :members: 

363 

364 

365Base classes 

366~~~~~~~~~~~~ 

367 

368.. autoclass:: ValueParser 

369 

370.. autoclass:: WrappingParser 

371 

372 .. autoattribute:: _inner 

373 

374 .. autoattribute:: _inner_raw 

375 

376.. autoclass:: MappingParser 

377 

378.. autoclass:: ValidatingParser 

379 

380 .. autoattribute:: __wrapped_parser__ 

381 :noindex: 

382 

383 .. automethod:: _validate 

384 

385.. autoclass:: CollectionParser 

386 

387 .. autoattribute:: _allow_completing_duplicates 

388 

389 

390Adding type hint conversions 

391~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 

392 

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

394parsers from type hints: 

395 

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

397 

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

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

400 

401.. autofunction:: suggest_delim_for_type_hint_conversion 

402 

403""" 

404 

405from __future__ import annotations 

406 

407import abc 

408import argparse 

409import contextlib 

410import dataclasses 

411import datetime 

412import decimal 

413import enum 

414import fractions 

415import functools 

416import json 

417import pathlib 

418import re 

419import threading 

420import traceback 

421import types 

422import typing 

423 

424import yuio 

425import yuio.complete 

426import yuio.json_schema 

427import yuio.string 

428import yuio.widget 

429from yuio import _typing as _t 

430from yuio.json_schema import JsonValue 

431from yuio.secret import SecretString, SecretValue 

432from yuio.util import _find_docs 

433from yuio.util import to_dash_case as _to_dash_case 

434 

435__all__ = [ 

436 "Apply", 

437 "Bool", 

438 "Bound", 

439 "CaseFold", 

440 "CollectionParser", 

441 "Date", 

442 "DateTime", 

443 "Decimal", 

444 "Dict", 

445 "Dir", 

446 "Enum", 

447 "ExistingPath", 

448 "File", 

449 "Float", 

450 "Fraction", 

451 "FrozenSet", 

452 "Ge", 

453 "GitRepo", 

454 "Gt", 

455 "Int", 

456 "Json", 

457 "JsonValue", 

458 "Le", 

459 "LenBound", 

460 "LenGe", 

461 "LenGt", 

462 "LenLe", 

463 "LenLt", 

464 "List", 

465 "Lower", 

466 "Lt", 

467 "Map", 

468 "MappingParser", 

469 "NonExistentPath", 

470 "OneOf", 

471 "Optional", 

472 "Parser", 

473 "ParsingError", 

474 "PartialParser", 

475 "Path", 

476 "Regex", 

477 "Secret", 

478 "SecretString", 

479 "SecretValue", 

480 "Set", 

481 "Str", 

482 "Strip", 

483 "Time", 

484 "TimeDelta", 

485 "Tuple", 

486 "Union", 

487 "Upper", 

488 "ValidatingParser", 

489 "ValueParser", 

490 "WithMeta", 

491 "WrappingParser", 

492 "from_type_hint", 

493 "register_type_hint_conversion", 

494 "suggest_delim_for_type_hint_conversion", 

495] 

496 

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

498T = _t.TypeVar("T") 

499U = _t.TypeVar("U") 

500K = _t.TypeVar("K") 

501V = _t.TypeVar("V") 

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

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

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

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

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

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

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

509 

510 

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

512 """ 

513 Raised when parsing or validation fails. 

514 

515 """ 

516 

517 @classmethod 

518 def type_mismatch(cls, value: _t.Any, /, *expected: type | str): 

519 """ 

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

521 

522 :param value: 

523 value of an unexpected type. 

524 :param expected: 

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

526 a type. 

527 :example: 

528 :: 

529 

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

531 Traceback (most recent call last): 

532 ... 

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

534 

535 """ 

536 

537 return cls( 

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

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

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

541 value, 

542 ) 

543 

544 

545class PartialParser(abc.ABC): 

546 """ 

547 An interface of a partial parser. 

548 

549 """ 

550 

551 def __init__(self): 

552 self.__orig_traceback = traceback.extract_stack() 

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

554 "yuio/parse.py" 

555 ): 

556 self.__orig_traceback.pop() 

557 super().__init__() 

558 

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

560 """ 

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

562 

563 """ 

564 

565 return self.__orig_traceback 

566 

567 @contextlib.contextmanager 

568 def _patch_stack_summary(self): 

569 """ 

570 Attach original traceback to any exception that's raised 

571 within this context manager. 

572 

573 """ 

574 

575 try: 

576 yield 

577 except Exception as e: 

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

579 self.__orig_traceback.format() 

580 ) 

581 e.args = ( 

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

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

584 ) 

585 setattr(e, "__yuio_stack_summary_text__", stack_summary_text) 

586 raise e 

587 

588 @abc.abstractmethod 

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

590 """ 

591 Apply this partial parser. 

592 

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

594 and the applies all partial parsers to it. 

595 

596 For example, given this type hint: 

597 

598 .. invisible-code-block: python 

599 

600 from typing import Annotated 

601 

602 .. code-block:: python 

603 

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

605 

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

607 this parser to ``Map.wrap``. 

608 

609 :param parser: 

610 a parser instance that was created by inspecting type hints 

611 and previous annotations. 

612 :returns: 

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

614 usually returns ``self``. 

615 :raises: 

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

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

618 

619 """ 

620 

621 raise NotImplementedError() 

622 

623 

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

625 """ 

626 Base class for parsers. 

627 

628 """ 

629 

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

631 __typehint: _t.Any = None 

632 

633 __wrapped_parser__: Parser[object] | None = None 

634 """ 

635 An attribute for unwrapping parsers that validate or map results 

636 of other parsers. 

637 

638 """ 

639 

640 @abc.abstractmethod 

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

642 """ 

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

644 

645 :param value: 

646 value to parse. 

647 :returns: 

648 a parsed and processed value. 

649 :raises: 

650 :class:`ParsingError`. 

651 

652 """ 

653 

654 raise NotImplementedError() 

655 

656 @abc.abstractmethod 

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

658 """ 

659 For collection parsers, parse and validate collection 

660 by parsing its items one-by-one. 

661 

662 :param value: 

663 collection of values to parse. 

664 :returns: 

665 each value parsed and assembled into the target collection. 

666 :raises: 

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

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

669 of objects. 

670 :example: 

671 :: 

672 

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

674 >>> parser = Set(Int()) 

675 

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

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

678 

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

680 >>> parser.parse_many(user_input) 

681 {1, 2, 3} 

682 

683 """ 

684 

685 raise NotImplementedError() 

686 

687 @abc.abstractmethod 

688 def supports_parse_many(self) -> bool: 

689 """ 

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

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

692 

693 :returns: 

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

695 

696 """ 

697 

698 raise NotImplementedError() 

699 

700 @abc.abstractmethod 

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

702 """ 

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

704 

705 This method accepts python values that would result from 

706 parsing json, yaml, and similar formats. 

707 

708 :param value: 

709 config value to parse. 

710 :returns: 

711 verified and processed config value. 

712 :raises: 

713 :class:`ParsingError`. 

714 :example: 

715 :: 

716 

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

718 >>> parser = Set(Int()) 

719 

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

721 >>> import json 

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

723 

724 >>> # We can process parsed json: 

725 >>> parser.parse_config(user_config) 

726 {1, 2, 3} 

727 

728 """ 

729 

730 raise NotImplementedError() 

731 

732 @abc.abstractmethod 

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

734 """ 

735 Generate ``nargs`` for argparse. 

736 

737 :returns: 

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

739 returns :data:`True`, value should be ``"+"``, ``"*"``, ``"?"``, 

740 or an integer. Otherwise, value should be :data:`None`. 

741 

742 """ 

743 

744 raise NotImplementedError() 

745 

746 @abc.abstractmethod 

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

748 """ 

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

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

751 

752 This function is used in :class:`Union` to dispatch values to correct parsers. 

753 

754 :param value: 

755 value that needs a type check. 

756 :returns: 

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

758 

759 """ 

760 

761 raise NotImplementedError() 

762 

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

764 """ 

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

766 if it returns :data:`False`. 

767 

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

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

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

771 

772 :param value: 

773 value that needs a type check. 

774 :returns: 

775 always returns :data:`True`. 

776 :raises: 

777 :class:`TypeError`. 

778 

779 """ 

780 

781 if not self.check_type(value): 

782 raise TypeError( 

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

784 ) 

785 return True 

786 

787 @abc.abstractmethod 

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

789 """ 

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

791 

792 Used to describe expected input in widgets. 

793 

794 :returns: 

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

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

797 

798 """ 

799 

800 raise NotImplementedError() 

801 

802 @abc.abstractmethod 

803 def describe_or_def(self) -> str: 

804 """ 

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

806 

807 Used to describe expected input in CLI help. 

808 

809 :returns: 

810 human-readable description of an expected input. 

811 

812 """ 

813 

814 raise NotImplementedError() 

815 

816 @abc.abstractmethod 

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

818 """ 

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

820 

821 Used to describe expected input in CLI help. 

822 

823 :returns: 

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

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

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

827 at the corresponding position. 

828 :raises: 

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

830 that doesn't supports parsing collections of objects. 

831 

832 """ 

833 

834 raise NotImplementedError() 

835 

836 @abc.abstractmethod 

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

838 """ 

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

840 

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

842 

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

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

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

846 

847 :param value: 

848 value that needs a description. 

849 :returns: 

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

851 in a CLI argument or an environment variable. 

852 :raises: 

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

854 that this parser produces. 

855 

856 """ 

857 

858 raise NotImplementedError() 

859 

860 @abc.abstractmethod 

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

862 """ 

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

864 

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

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

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

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

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

870 

871 :returns: 

872 a full list of options that will be passed to 

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

874 if the set of possible values is not known. 

875 

876 """ 

877 

878 raise NotImplementedError() 

879 

880 @abc.abstractmethod 

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

882 """ 

883 Return a completer for values of this parser. 

884 

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

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

887 

888 :returns: 

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

890 

891 """ 

892 

893 raise NotImplementedError() 

894 

895 @abc.abstractmethod 

896 def widget( 

897 self, 

898 default: object | yuio.Missing, 

899 input_description: str | None, 

900 default_description: str | None, 

901 /, 

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

903 """ 

904 Return a widget for reading values of this parser. 

905 

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

907 

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

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

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

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

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

913 

914 Validating parsers must wrap the widget they got from 

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

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

917 

918 :param default: 

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

920 :param input_description: 

921 a string describing what input is expected. 

922 :param default_description: 

923 a string describing default value. 

924 :returns: 

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

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

927 some custom logic. 

928 

929 """ 

930 

931 raise NotImplementedError() 

932 

933 @abc.abstractmethod 

934 def to_json_schema( 

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

936 ) -> yuio.json_schema.JsonSchemaType: 

937 """ 

938 Create a JSON schema object based on this parser. 

939 

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

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

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

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

944 

945 :param ctx: 

946 context for building a schema. 

947 :returns: 

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

949 

950 """ 

951 

952 raise NotImplementedError() 

953 

954 @abc.abstractmethod 

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

956 """ 

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

958 

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

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

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

962 

963 :returns: 

964 a value converted to JSON-serializable representation. 

965 :raises: 

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

967 that this parser produces. 

968 

969 """ 

970 

971 raise NotImplementedError() 

972 

973 @abc.abstractmethod 

974 def is_secret(self) -> bool: 

975 """ 

976 Indicates that input functions should use secret input, 

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

978 

979 """ 

980 

981 raise NotImplementedError() 

982 

983 def __repr__(self): 

984 return self.__class__.__name__ 

985 

986 

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

988 """ 

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

990 

991 Implements all method, except for :meth:`~Parser.parse`, 

992 :meth:`~Parser.parse_config`, :meth:`~Parser.to_json_schema`, 

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

994 

995 :param ty: 

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

997 :example: 

998 .. invisible-code-block: python 

999 

1000 from dataclasses import dataclass 

1001 @dataclass 

1002 class MyType: 

1003 data: str 

1004 

1005 .. code-block:: python 

1006 

1007 class MyTypeParser(ValueParser[MyType]): 

1008 def __init__(self): 

1009 super().__init__(MyType) 

1010 

1011 def parse(self, value: str, /) -> MyType: 

1012 return self.parse_config(value) 

1013 

1014 def parse_config(self, value: object, /) -> MyType: 

1015 if not isinstance(value, str): 

1016 raise ParsingError.type_mismatch(value, str) 

1017 return MyType(value) 

1018 

1019 def to_json_schema( 

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

1021 ) -> yuio.json_schema.JsonSchemaType: 

1022 return yuio.json_schema.String() 

1023 

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

1025 assert self.assert_type(value) 

1026 return value.data 

1027 

1028 :: 

1029 

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

1031 MyType(data='pancake') 

1032 

1033 """ 

1034 

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

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

1037 

1038 self._value_type = ty 

1039 """ 

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

1041 

1042 """ 

1043 

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

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

1046 if typehint is None: 

1047 with self._patch_stack_summary(): 

1048 raise TypeError( 

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

1050 " all previous annotations. Make sure that" 

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

1052 " your type hint.\n\n" 

1053 "Example:\n" 

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

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

1056 " ^^^^^\n" 

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

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

1059 " ^^^^^" 

1060 ) 

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

1062 with self._patch_stack_summary(): 

1063 raise TypeError( 

1064 f"annotating {_t.type_repr(typehint)} with {self.__class__.__name__}" 

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

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

1067 "Example:\n" 

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

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

1070 " ^^^^^^^^^^^^^^^^^^^^^^\n" 

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

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

1073 " ^^^^^^^^^^^^^^^^^^^^^^" 

1074 ) 

1075 return self 

1076 

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

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

1079 

1080 def supports_parse_many(self) -> bool: 

1081 return False 

1082 

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

1084 return None 

1085 

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

1087 return isinstance(value, self._value_type) 

1088 

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

1090 return None 

1091 

1092 def describe_or_def(self) -> str: 

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

1094 

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

1096 return self.describe_or_def() 

1097 

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

1099 assert self.assert_type(value) 

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

1101 

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

1103 return None 

1104 

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

1106 return None 

1107 

1108 def widget( 

1109 self, 

1110 default: object | yuio.Missing, 

1111 input_description: str | None, 

1112 default_description: str | None, 

1113 /, 

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

1115 completer = self.completer() 

1116 return _WidgetResultMapper( 

1117 self, 

1118 input_description, 

1119 default, 

1120 ( 

1121 yuio.widget.InputWithCompletion( 

1122 completer, 

1123 placeholder=default_description or "", 

1124 ) 

1125 if completer is not None 

1126 else yuio.widget.Input( 

1127 placeholder=default_description or "", 

1128 ) 

1129 ), 

1130 ) 

1131 

1132 def is_secret(self) -> bool: 

1133 return False 

1134 

1135 

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

1137 """ 

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

1139 

1140 This base simplifies dealing with partial parsers. 

1141 

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

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

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

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

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

1147 in a type annotation. 

1148 

1149 .. warning:: 

1150 

1151 All descendants of this class must include appropriate type hints 

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

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

1154 

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

1156 

1157 :param inner: 

1158 inner data or :data:`None`. 

1159 

1160 """ 

1161 

1162 if _t.TYPE_CHECKING: 

1163 

1164 @_t.overload 

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

1166 

1167 @_t.overload 

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

1169 

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

1171 

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

1173 self.__inner = inner 

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

1175 

1176 @property 

1177 def _inner(self) -> U: 

1178 """ 

1179 Internal resource wrapped by this parser. 

1180 

1181 :raises: 

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

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

1184 

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

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

1187 in type annotations. 

1188 

1189 """ 

1190 

1191 if self.__inner is None: 

1192 with self._patch_stack_summary(): 

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

1194 return self.__inner 

1195 

1196 @_inner.setter 

1197 def _inner(self, inner: U): 

1198 if self.__inner is not None: 

1199 with self._patch_stack_summary(): 

1200 raise TypeError( 

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

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

1203 "from type hint.\n\n" 

1204 "Example:\n" 

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

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

1207 " ^^^^^^^^^^^^^^^^^^^^^^^^^^\n" 

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

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

1210 " ^^^^^^^^^^^^^^^^^^^" 

1211 ) 

1212 self.__inner = inner 

1213 

1214 @property 

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

1216 """ 

1217 Unchecked access to the wrapped resource. 

1218 

1219 """ 

1220 

1221 return self.__inner 

1222 

1223 

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

1225 """ 

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

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

1228 :meth:`~Parser.parse_many`, :meth:`~Parser.parse_config`, 

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

1230 :meth:`~Parser.describe_value`, :meth:`~Parser.describe_value`, 

1231 :meth:`~Parser.widget`, and :meth:`~Parser.to_json_value`. 

1232 

1233 :param inner: 

1234 mapped parser or :data:`None`. 

1235 

1236 """ 

1237 

1238 if _t.TYPE_CHECKING: 

1239 

1240 @_t.overload 

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

1242 

1243 @_t.overload 

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

1245 

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

1247 

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

1249 super().__init__(inner) 

1250 

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

1252 self._inner = parser 

1253 return self 

1254 

1255 def supports_parse_many(self) -> bool: 

1256 return self._inner.supports_parse_many() 

1257 

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

1259 return self._inner.get_nargs() 

1260 

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

1262 return self._inner.describe() 

1263 

1264 def describe_or_def(self) -> str: 

1265 return self._inner.describe_or_def() 

1266 

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

1268 return self._inner.describe_many() 

1269 

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

1271 return self._inner.completer() 

1272 

1273 def to_json_schema( 

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

1275 ) -> yuio.json_schema.JsonSchemaType: 

1276 return self._inner.to_json_schema(ctx) 

1277 

1278 def is_secret(self) -> bool: 

1279 return self._inner.is_secret() 

1280 

1281 def __repr__(self): 

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

1283 

1284 @property 

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

1286 return self._inner_raw 

1287 

1288 

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

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

1291 

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

1293 

1294 :param inner: 

1295 a parser whose result will be mapped. 

1296 :param fn: 

1297 a function to convert a result. 

1298 :param rev: 

1299 a function used to un-map a value. 

1300 

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

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

1303 to its original state. 

1304 

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

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

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

1308 :example: 

1309 .. 

1310 >>> import math 

1311 

1312 :: 

1313 

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

1315 ... yuio.parse.Int(), 

1316 ... lambda x: 2 ** x, 

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

1318 ... ) 

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

1320 1024 

1321 >>> parser.describe_value(1024) 

1322 '10' 

1323 

1324 """ 

1325 

1326 if _t.TYPE_CHECKING: 

1327 

1328 @_t.overload 

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

1330 

1331 @_t.overload 

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

1333 

1334 @_t.overload 

1335 def __new__( 

1336 cls, 

1337 inner: Parser[U], 

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

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

1340 /, 

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

1342 

1343 @_t.overload 

1344 def __new__( 

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

1346 ) -> PartialParser: ... 

1347 

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

1349 

1350 def __init__(self, *args): 

1351 inner: Parser[U] | None = None 

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

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

1354 if len(args) == 1: 

1355 (fn,) = args 

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

1357 inner, fn = args 

1358 elif len(args) == 2: 

1359 fn, rev = args 

1360 elif len(args) == 3: 

1361 inner, fn, rev = args 

1362 else: 

1363 raise TypeError( 

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

1365 ) 

1366 

1367 self.__fn = fn 

1368 self.__rev = rev 

1369 super().__init__(inner) 

1370 

1371 def parse(self, value: str, /) -> T: 

1372 return self.__fn(self._inner.parse(value)) 

1373 

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

1375 return self.__fn(self._inner.parse_many(value)) 

1376 

1377 def parse_config(self, value: object, /) -> T: 

1378 return self.__fn(self._inner.parse_config(value)) 

1379 

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

1381 if self.__rev: 

1382 value = self.__rev(value) 

1383 return self._inner.check_type(value) 

1384 

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

1386 if self.__rev: 

1387 value = self.__rev(value) 

1388 return self._inner.describe_value(value) 

1389 

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

1391 options = self._inner.options() 

1392 if options is not None: 

1393 return [ 

1394 _t.cast( 

1395 yuio.widget.Option[T], 

1396 dataclasses.replace(option, value=self.__fn(option.value)), 

1397 ) 

1398 for option in options 

1399 ] 

1400 else: 

1401 return None 

1402 

1403 def widget( 

1404 self, 

1405 default: object | yuio.Missing, 

1406 input_description: str | None, 

1407 default_description: str | None, 

1408 /, 

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

1410 return yuio.widget.Map( 

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

1412 lambda v: self.__fn(v) if v is not yuio.MISSING else yuio.MISSING, 

1413 ) 

1414 

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

1416 if self.__rev: 

1417 value = self.__rev(value) 

1418 return self._inner.to_json_value(value) 

1419 

1420 

1421@_t.overload 

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

1423@_t.overload 

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

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

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

1427 

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

1429 

1430 :param inner: 

1431 a parser whose result will be mapped. 

1432 

1433 """ 

1434 

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

1436 

1437 

1438@_t.overload 

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

1440@_t.overload 

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

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

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

1444 

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

1446 

1447 :param inner: 

1448 a parser whose result will be mapped. 

1449 

1450 """ 

1451 

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

1453 

1454 

1455@_t.overload 

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

1457@_t.overload 

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

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

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

1461 

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

1463 

1464 :param inner: 

1465 a parser whose result will be mapped. 

1466 

1467 """ 

1468 

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

1470 

1471 

1472@_t.overload 

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

1474@_t.overload 

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

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

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

1478 

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

1480 

1481 :param inner: 

1482 a parser whose result will be mapped. 

1483 

1484 """ 

1485 

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

1487 

1488 

1489@_t.overload 

1490def Regex( 

1491 inner: Parser[str], 

1492 regex: str | _t.StrRePattern, 

1493 /, 

1494 *, 

1495 group: int | str = 0, 

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

1497@_t.overload 

1498def Regex( 

1499 regex: str | _t.StrRePattern, /, *, group: int | str = 0 

1500) -> PartialParser: ... 

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

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

1503 

1504 Matches the parsed string with the given regular expression. 

1505 

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

1507 

1508 :param regex: 

1509 regular expression for matching. 

1510 :param group: 

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

1512 parsed value. 

1513 

1514 """ 

1515 

1516 inner: Parser[str] | None 

1517 regex: str | _t.StrRePattern 

1518 if len(args) == 1: 

1519 inner, regex = None, args[0] 

1520 elif len(args) == 2: 

1521 inner, regex = args 

1522 else: 

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

1524 

1525 if isinstance(regex, re.Pattern): 

1526 compiled = regex 

1527 else: 

1528 compiled = re.compile(regex) 

1529 

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

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

1532 raise ParsingError( 

1533 "value doesn't match regex `%s`: `%r`", compiled.pattern, value 

1534 ) 

1535 return match.group(group) 

1536 

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

1538 

1539 

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

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

1542 

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

1544 

1545 :param inner: 

1546 a parser used to extract and validate a value. 

1547 :param fn: 

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

1549 :example: 

1550 :: 

1551 

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

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

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

1555 Value is 10 

1556 >>> result 

1557 10 

1558 

1559 """ 

1560 

1561 if _t.TYPE_CHECKING: 

1562 

1563 @_t.overload 

1564 def __new__( 

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

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

1567 

1568 @_t.overload 

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

1570 

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

1572 

1573 def __init__(self, *args): 

1574 inner: Parser[T] | None 

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

1576 if len(args) == 1: 

1577 inner, fn = None, args[0] 

1578 elif len(args) == 2: 

1579 inner, fn = args 

1580 else: 

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

1582 

1583 self.__fn = fn 

1584 super().__init__(inner) 

1585 

1586 def parse(self, value: str, /) -> T: 

1587 result = self._inner.parse(value) 

1588 self.__fn(result) 

1589 return result 

1590 

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

1592 result = self._inner.parse_many(value) 

1593 self.__fn(result) 

1594 return result 

1595 

1596 def parse_config(self, value: object, /) -> T: 

1597 result = self._inner.parse_config(value) 

1598 self.__fn(result) 

1599 return result 

1600 

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

1602 return self._inner.check_type(value) 

1603 

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

1605 return self._inner.describe_value(value) 

1606 

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

1608 return self._inner.options() 

1609 

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

1611 return self._inner.completer() 

1612 

1613 def widget( 

1614 self, 

1615 default: object | yuio.Missing, 

1616 input_description: str | None, 

1617 default_description: str | None, 

1618 /, 

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

1620 return yuio.widget.Apply( 

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

1622 lambda v: self.__fn(v) if v is not yuio.MISSING else None, 

1623 ) 

1624 

1625 def to_json_schema( 

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

1627 ) -> yuio.json_schema.JsonSchemaType: 

1628 return self._inner.to_json_schema(ctx) 

1629 

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

1631 return self._inner.to_json_value(value) 

1632 

1633 

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

1635 """ 

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

1637 

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

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

1640 

1641 :param inner: 

1642 a parser which output will be validated. 

1643 :example: 

1644 .. code-block:: python 

1645 

1646 class IsLower(ValidatingParser[str]): 

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

1648 if not value.islower(): 

1649 raise ParsingError("value should be lowercase: `%r`", value) 

1650 

1651 :: 

1652 

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

1654 Traceback (most recent call last): 

1655 ... 

1656 yuio.parse.ParsingError: value should be lowercase: 'Not lowercase!' 

1657 

1658 """ 

1659 

1660 if _t.TYPE_CHECKING: 

1661 

1662 @_t.overload 

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

1664 

1665 @_t.overload 

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

1667 

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

1669 

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

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

1672 

1673 @abc.abstractmethod 

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

1675 """ 

1676 Implementation of value validation. 

1677 

1678 :param value: 

1679 value which needs validating. 

1680 :raises: 

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

1682 

1683 """ 

1684 

1685 raise NotImplementedError() 

1686 

1687 

1688class Str(ValueParser[str]): 

1689 """ 

1690 Parser for str values. 

1691 

1692 """ 

1693 

1694 def __init__(self): 

1695 super().__init__(str) 

1696 

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

1698 return value 

1699 

1700 def parse_config(self, value: object, /) -> str: 

1701 if not isinstance(value, str): 

1702 raise ParsingError.type_mismatch(value, str) 

1703 return value 

1704 

1705 def to_json_schema( 

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

1707 ) -> yuio.json_schema.JsonSchemaType: 

1708 return yuio.json_schema.String() 

1709 

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

1711 assert self.assert_type(value) 

1712 return value 

1713 

1714 

1715class Int(ValueParser[int]): 

1716 """ 

1717 Parser for int values. 

1718 

1719 """ 

1720 

1721 def __init__(self): 

1722 super().__init__(int) 

1723 

1724 def parse(self, value: str, /) -> int: 

1725 try: 

1726 return int(value.strip()) 

1727 except ValueError: 

1728 raise ParsingError("Can't parse `%r` as `int`", value) from None 

1729 

1730 def parse_config(self, value: object, /) -> int: 

1731 if isinstance(value, float): 

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

1733 raise ParsingError.type_mismatch(value, int) 

1734 value = int(value) 

1735 if not isinstance(value, int): 

1736 raise ParsingError.type_mismatch(value, int) 

1737 return value 

1738 

1739 def to_json_schema( 

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

1741 ) -> yuio.json_schema.JsonSchemaType: 

1742 return yuio.json_schema.Integer() 

1743 

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

1745 assert self.assert_type(value) 

1746 return value 

1747 

1748 

1749class Float(ValueParser[float]): 

1750 """ 

1751 Parser for float values. 

1752 

1753 """ 

1754 

1755 def __init__(self): 

1756 super().__init__(float) 

1757 

1758 def parse(self, value: str, /) -> float: 

1759 try: 

1760 return float(value.strip()) 

1761 except ValueError: 

1762 raise ParsingError("Can't parse `%r` as `float`", value) from None 

1763 

1764 def parse_config(self, value: object, /) -> float: 

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

1766 raise ParsingError.type_mismatch(value, float) 

1767 return value 

1768 

1769 def to_json_schema( 

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

1771 ) -> yuio.json_schema.JsonSchemaType: 

1772 return yuio.json_schema.Number() 

1773 

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

1775 assert self.assert_type(value) 

1776 return value 

1777 

1778 

1779class Bool(ValueParser[bool]): 

1780 """ 

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

1782 

1783 """ 

1784 

1785 def __init__(self): 

1786 super().__init__(bool) 

1787 

1788 def parse(self, value: str, /) -> bool: 

1789 value = value.strip().lower() 

1790 

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

1792 return True 

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

1794 return False 

1795 else: 

1796 raise ParsingError( 

1797 "Can't parse `%r` as `bool`, should be one of `'yes'`, `'no'`", value 

1798 ) 

1799 

1800 def parse_config(self, value: object, /) -> bool: 

1801 if not isinstance(value, bool): 

1802 raise ParsingError.type_mismatch(value, bool) 

1803 return value 

1804 

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

1806 return "{yes|no}" 

1807 

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

1809 assert self.assert_type(value) 

1810 return "yes" if value else "no" 

1811 

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

1813 return yuio.complete.Choice( 

1814 [ 

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

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

1817 ] 

1818 ) 

1819 

1820 def widget( 

1821 self, 

1822 default: object | yuio.Missing, 

1823 input_description: str | None, 

1824 default_description: str | None, 

1825 /, 

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

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

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

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

1830 ] 

1831 

1832 if default is yuio.MISSING: 

1833 default_index = 0 

1834 elif isinstance(default, bool): 

1835 default_index = int(default) 

1836 else: 

1837 options.append( 

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

1839 ) 

1840 default_index = 2 

1841 

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

1843 

1844 def to_json_schema( 

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

1846 ) -> yuio.json_schema.JsonSchemaType: 

1847 return yuio.json_schema.Boolean() 

1848 

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

1850 assert self.assert_type(value) 

1851 return value 

1852 

1853 

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

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

1856 

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

1858 

1859 :param enum_type: 

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

1861 :param by_name: 

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

1863 their values, to match the input. 

1864 :param to_dash_case: 

1865 convert enum names/values to dash case. 

1866 :param doc_inline: 

1867 inline this enum in json schema and in documentation. 

1868 

1869 """ 

1870 

1871 if _t.TYPE_CHECKING: 

1872 

1873 @_t.overload 

1874 def __new__( 

1875 cls, 

1876 inner: type[E], 

1877 /, 

1878 *, 

1879 by_name: bool = False, 

1880 to_dash_case: bool = False, 

1881 doc_inline: bool = False, 

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

1883 

1884 @_t.overload 

1885 def __new__( 

1886 cls, 

1887 /, 

1888 *, 

1889 by_name: bool = False, 

1890 to_dash_case: bool = False, 

1891 doc_inline: bool = False, 

1892 ) -> PartialParser: ... 

1893 

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

1895 

1896 def __init__( 

1897 self, 

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

1899 /, 

1900 *, 

1901 by_name: bool = False, 

1902 to_dash_case: bool = False, 

1903 doc_inline: bool = False, 

1904 ): 

1905 self.__by_name = by_name 

1906 self.__to_dash_case = to_dash_case 

1907 self.__doc_inline = doc_inline 

1908 super().__init__(enum_type, enum_type) 

1909 

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

1911 result = super().wrap(parser) 

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

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

1914 return result 

1915 

1916 @functools.cached_property 

1917 def __getter(self) -> _t.Callable[[E], str]: 

1918 items = {} 

1919 for e in self._inner: 

1920 if self.__by_name: 

1921 name = e.name 

1922 else: 

1923 name = str(e.value) 

1924 if self.__to_dash_case: 

1925 name = _to_dash_case(name) 

1926 items[e] = name 

1927 return lambda e: items[e] 

1928 

1929 @functools.cached_property 

1930 def __docs(self) -> dict[str, str]: 

1931 return _find_docs(self._inner) 

1932 

1933 def parse(self, value: str, /) -> E: 

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

1935 

1936 candidates: list[E] = [] 

1937 for item in self._inner: 

1938 if self.__getter(item) == value: 

1939 return item 

1940 elif (self.__getter(item)).casefold().startswith(cf_value): 

1941 candidates.append(item) 

1942 

1943 if len(candidates) == 1: 

1944 return candidates[0] 

1945 elif len(candidates) > 1: 

1946 enum_values = tuple(self.__getter(e) for e in candidates) 

1947 raise ParsingError( 

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

1949 value, 

1950 self._inner.__name__, 

1951 yuio.string.Or(enum_values), 

1952 ) 

1953 else: 

1954 enum_values = tuple(self.__getter(e) for e in self._inner) 

1955 raise ParsingError( 

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

1957 value, 

1958 self._inner.__name__, 

1959 yuio.string.Or(enum_values), 

1960 ) 

1961 

1962 def parse_config(self, value: object, /) -> E: 

1963 if not isinstance(value, str): 

1964 raise ParsingError.type_mismatch(value, str) 

1965 

1966 result = self.parse(value) 

1967 

1968 if self.__getter(result) != value: 

1969 raise ParsingError( 

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

1971 value, 

1972 self._inner.__name__, 

1973 self.__getter(result), 

1974 ) 

1975 

1976 return result 

1977 

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

1979 desc = "|".join(self.__getter(e) for e in self._inner) 

1980 if len(self._inner) > 1: 

1981 desc = f"{{{desc}}}" 

1982 return desc 

1983 

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

1985 return self.describe_or_def() 

1986 

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

1988 assert self.assert_type(value) 

1989 return str(self.__getter(value)) 

1990 

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

1992 docs = self.__docs 

1993 return [ 

1994 yuio.widget.Option( 

1995 e, display_text=self.__getter(e), comment=docs.get(e.name) 

1996 ) 

1997 for e in self._inner 

1998 ] 

1999 

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

2001 docs = self.__docs 

2002 return yuio.complete.Choice( 

2003 [ 

2004 yuio.complete.Option(self.__getter(e), comment=docs.get(e.name)) 

2005 for e in self._inner 

2006 ] 

2007 ) 

2008 

2009 def widget( 

2010 self, 

2011 default: object | yuio.Missing, 

2012 input_description: str | None, 

2013 default_description: str | None, 

2014 /, 

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

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

2017 

2018 if default is yuio.MISSING: 

2019 default_index = 0 

2020 elif isinstance(default, self._inner): 

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

2022 else: 

2023 options.insert( 

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

2025 ) 

2026 default_index = 0 

2027 

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

2029 

2030 def to_json_schema( 

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

2032 ) -> yuio.json_schema.JsonSchemaType: 

2033 items = [self.__getter(e) for e in self._inner] 

2034 docs = self.__docs 

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

2036 if not any(descriptions): 

2037 descriptions = None 

2038 if self.__doc_inline: 

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

2040 else: 

2041 return ctx.add_type( 

2042 Enum._TyWrapper(self._inner, self.__by_name, self.__to_dash_case), 

2043 _t.type_repr(self._inner), 

2044 lambda: yuio.json_schema.Meta( 

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

2046 title=self._inner.__name__, 

2047 description=self._inner.__doc__, 

2048 ), 

2049 ) 

2050 

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

2052 assert self.assert_type(value) 

2053 return self.__getter(value) 

2054 

2055 def __repr__(self): 

2056 if self._inner_raw is not None: 

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

2058 else: 

2059 return self.__class__.__name__ 

2060 

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

2062 class _TyWrapper: 

2063 inner: type 

2064 by_name: bool 

2065 to_dash_case: bool 

2066 

2067 

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

2069 """ 

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

2071 

2072 """ 

2073 

2074 def __init__(self): 

2075 super().__init__(decimal.Decimal) 

2076 

2077 def parse(self, value: str, /) -> decimal.Decimal: 

2078 return self.parse_config(value) 

2079 

2080 def parse_config(self, value: object, /) -> decimal.Decimal: 

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

2082 raise ParsingError.type_mismatch(value, int, float, str) 

2083 try: 

2084 return decimal.Decimal(value) 

2085 except (ArithmeticError, ValueError, TypeError): 

2086 raise ParsingError("Can't parse `%r` as `decimal`", value) from None 

2087 

2088 def to_json_schema( 

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

2090 ) -> yuio.json_schema.JsonSchemaType: 

2091 return ctx.add_type( 

2092 decimal.Decimal, 

2093 "Decimal", 

2094 lambda: yuio.json_schema.Meta( 

2095 yuio.json_schema.OneOf( 

2096 [ 

2097 yuio.json_schema.Number(), 

2098 yuio.json_schema.String( 

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

2100 ), 

2101 ] 

2102 ), 

2103 title="Decimal", 

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

2105 ), 

2106 ) 

2107 

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

2109 assert self.assert_type(value) 

2110 return str(value) 

2111 

2112 

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

2114 """ 

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

2116 

2117 """ 

2118 

2119 def __init__(self): 

2120 super().__init__(fractions.Fraction) 

2121 

2122 def parse(self, value: str, /) -> fractions.Fraction: 

2123 return self.parse_config(value) 

2124 

2125 def parse_config(self, value: object, /) -> fractions.Fraction: 

2126 if ( 

2127 isinstance(value, (list, tuple)) 

2128 and len(value) == 2 

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

2130 ): 

2131 try: 

2132 return fractions.Fraction(*value) 

2133 except ValueError: 

2134 raise ParsingError( 

2135 "Can't parse `%s/%s` as `fraction`", value[0], value[1] 

2136 ) from None 

2137 except ZeroDivisionError: 

2138 raise ParsingError( 

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

2140 value[0], 

2141 value[1], 

2142 ) from None 

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

2144 try: 

2145 return fractions.Fraction(value) 

2146 except ValueError: 

2147 raise ParsingError( 

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

2149 value, 

2150 ) from None 

2151 except ZeroDivisionError: 

2152 raise ParsingError( 

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

2154 value, 

2155 ) from None 

2156 raise ParsingError.type_mismatch(value, int, float, str, "a tuple of two ints") 

2157 

2158 def to_json_schema( 

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

2160 ) -> yuio.json_schema.JsonSchemaType: 

2161 return ctx.add_type( 

2162 fractions.Fraction, 

2163 "Fraction", 

2164 lambda: yuio.json_schema.Meta( 

2165 yuio.json_schema.OneOf( 

2166 [ 

2167 yuio.json_schema.Number(), 

2168 yuio.json_schema.String( 

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

2170 ), 

2171 yuio.json_schema.Tuple( 

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

2173 ), 

2174 ] 

2175 ), 

2176 title="Fraction", 

2177 description="A rational number.", 

2178 ), 

2179 ) 

2180 

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

2182 assert self.assert_type(value) 

2183 return str(value) 

2184 

2185 

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

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

2188 

2189 A parser that tries to parse value as JSON. 

2190 

2191 This parser will load JSON strings into python objects. 

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

2193 by calling :meth:`~Parser.parse_config` on the inner parser. 

2194 

2195 :param inner: 

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

2197 

2198 """ 

2199 

2200 if _t.TYPE_CHECKING: 

2201 

2202 @_t.overload 

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

2204 

2205 @_t.overload 

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

2207 

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

2209 

2210 def __init__( 

2211 self, 

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

2213 /, 

2214 ): 

2215 super().__init__(inner, object) 

2216 

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

2218 self._inner = parser 

2219 return self 

2220 

2221 def parse(self, value: str) -> T: 

2222 try: 

2223 config_value = json.loads(value) 

2224 except json.JSONDecodeError as e: 

2225 raise ParsingError( 

2226 "Can't parse `%r` as `JsonValue`: %s", value, e 

2227 ) from None 

2228 return self.parse_config(config_value) 

2229 

2230 def parse_config(self, value: object) -> T: 

2231 if self._inner_raw is not None: 

2232 return self._inner_raw.parse_config(value) 

2233 else: 

2234 return _t.cast(T, value) 

2235 

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

2237 if self._inner_raw is not None: 

2238 return self._inner_raw.check_type(value) 

2239 else: 

2240 return True # xxx: make a better check 

2241 

2242 def to_json_schema( 

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

2244 ) -> yuio.json_schema.JsonSchemaType: 

2245 if self._inner_raw is not None: 

2246 return self._inner_raw.to_json_schema(ctx) 

2247 else: 

2248 return yuio.json_schema.Any() 

2249 

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

2251 assert self.assert_type(value) 

2252 if self._inner_raw is not None: 

2253 return self._inner_raw.to_json_value(value) 

2254 return value 

2255 

2256 def __repr__(self): 

2257 if self._inner_raw is not None: 

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

2259 else: 

2260 return super().__repr__() 

2261 

2262 

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

2264 """ 

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

2266 

2267 """ 

2268 

2269 def __init__(self): 

2270 super().__init__(datetime.datetime) 

2271 

2272 def parse(self, value: str, /) -> datetime.datetime: 

2273 try: 

2274 return datetime.datetime.fromisoformat(value) 

2275 except ValueError: 

2276 raise ParsingError("Can't parse `%r` as `datetime`", value) from None 

2277 

2278 def parse_config(self, value: object, /) -> datetime.datetime: 

2279 if isinstance(value, datetime.datetime): 

2280 return value 

2281 elif isinstance(value, str): 

2282 return self.parse(value) 

2283 else: 

2284 raise ParsingError.type_mismatch(value, str) 

2285 

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

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

2288 

2289 def to_json_schema( 

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

2291 ) -> yuio.json_schema.JsonSchemaType: 

2292 return ctx.add_type( 

2293 datetime.datetime, 

2294 "DateTime", 

2295 lambda: yuio.json_schema.Meta( 

2296 yuio.json_schema.String( 

2297 pattern=( 

2298 r"^" 

2299 r"(" 

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

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

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

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

2304 r")" 

2305 r"(" 

2306 r"[T ]" 

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

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

2309 r")?" 

2310 r"$" 

2311 ) 

2312 ), 

2313 title="DateTime", 

2314 description="ISO 8601 datetime.", 

2315 ), 

2316 ) 

2317 

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

2319 assert self.assert_type(value) 

2320 return str(value) 

2321 

2322 

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

2324 """ 

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

2326 

2327 """ 

2328 

2329 def __init__(self): 

2330 super().__init__(datetime.date) 

2331 

2332 def parse(self, value: str, /) -> datetime.date: 

2333 try: 

2334 return datetime.date.fromisoformat(value) 

2335 except ValueError: 

2336 raise ParsingError("Can't parse `%r` as `date`", value) from None 

2337 

2338 def parse_config(self, value: object, /) -> datetime.date: 

2339 if isinstance(value, datetime.datetime): 

2340 return value.date() 

2341 elif isinstance(value, datetime.date): 

2342 return value 

2343 elif isinstance(value, str): 

2344 return self.parse(value) 

2345 else: 

2346 raise ParsingError.type_mismatch(value, str) 

2347 

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

2349 return "YYYY-MM-DD" 

2350 

2351 def to_json_schema( 

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

2353 ) -> yuio.json_schema.JsonSchemaType: 

2354 return ctx.add_type( 

2355 datetime.date, 

2356 "Date", 

2357 lambda: yuio.json_schema.Meta( 

2358 yuio.json_schema.String( 

2359 pattern=( 

2360 r"^" 

2361 r"(" 

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

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

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

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

2366 r")" 

2367 r"$" 

2368 ) 

2369 ), 

2370 title="Date", 

2371 description="ISO 8601 date.", 

2372 ), 

2373 ) 

2374 

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

2376 assert self.assert_type(value) 

2377 return str(value) 

2378 

2379 

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

2381 """ 

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

2383 

2384 """ 

2385 

2386 def __init__(self): 

2387 super().__init__(datetime.time) 

2388 

2389 def parse(self, value: str, /) -> datetime.time: 

2390 try: 

2391 return datetime.time.fromisoformat(value) 

2392 except ValueError: 

2393 raise ParsingError("Can't parse `%r` as `time`", value) from None 

2394 

2395 def parse_config(self, value: object, /) -> datetime.time: 

2396 if isinstance(value, datetime.datetime): 

2397 return value.time() 

2398 elif isinstance(value, datetime.time): 

2399 return value 

2400 elif isinstance(value, str): 

2401 return self.parse(value) 

2402 else: 

2403 raise ParsingError.type_mismatch(value, str) 

2404 

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

2406 return "HH:MM:SS" 

2407 

2408 def to_json_schema( 

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

2410 ) -> yuio.json_schema.JsonSchemaType: 

2411 return ctx.add_type( 

2412 datetime.time, 

2413 "Time", 

2414 lambda: yuio.json_schema.Meta( 

2415 yuio.json_schema.String( 

2416 pattern=( 

2417 r"^" 

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

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

2420 r"$" 

2421 ) 

2422 ), 

2423 title="Time", 

2424 description="ISO 8601 time.", 

2425 ), 

2426 ) 

2427 

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

2429 assert self.assert_type(value) 

2430 return str(value) 

2431 

2432 

2433_UNITS_MAP = ( 

2434 ("days", ("d", "day", "days")), 

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

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

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

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

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

2440 ("weeks", ("w", "week", "weeks")), 

2441) 

2442 

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

2444 

2445_TIMEDELTA_RE = re.compile( 

2446 r""" 

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

2448 ^ 

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

2450 (?:,\s*)? 

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

2452 $ 

2453 """, 

2454 re.VERBOSE | re.IGNORECASE, 

2455) 

2456 

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

2458 

2459 

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

2461 """ 

2462 Parse a time delta. 

2463 

2464 """ 

2465 

2466 def __init__(self): 

2467 super().__init__(datetime.timedelta) 

2468 

2469 def parse(self, value: str, /) -> datetime.timedelta: 

2470 value = value.strip() 

2471 

2472 if not value: 

2473 raise ParsingError("Got an empty `timedelta`") 

2474 if value.endswith(","): 

2475 raise ParsingError( 

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

2477 ) 

2478 if value.startswith(","): 

2479 raise ParsingError( 

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

2481 ) 

2482 

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

2484 ( 

2485 c_sign_s, 

2486 components_s, 

2487 t_sign_s, 

2488 hour, 

2489 minute, 

2490 second, 

2491 millisecond, 

2492 microsecond, 

2493 ) = match.groups() 

2494 else: 

2495 raise ParsingError("Can't parse `%r` as `timedelta`", value) 

2496 

2497 c_sign_s = -1 if c_sign_s == "-" else 1 

2498 t_sign_s = -1 if t_sign_s == "-" else 1 

2499 

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

2501 

2502 if components_s: 

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

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

2505 kwargs[unit_key] += int(num) 

2506 else: 

2507 raise ParsingError( 

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

2509 value, 

2510 unit, 

2511 ) 

2512 

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

2514 

2515 timedelta += t_sign_s * datetime.timedelta( 

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

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

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

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

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

2521 ) 

2522 

2523 return timedelta 

2524 

2525 def parse_config(self, value: object, /) -> datetime.timedelta: 

2526 if isinstance(value, datetime.timedelta): 

2527 return value 

2528 elif isinstance(value, str): 

2529 return self.parse(value) 

2530 else: 

2531 raise ParsingError.type_mismatch(value, str) 

2532 

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

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

2535 

2536 def to_json_schema( 

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

2538 ) -> yuio.json_schema.JsonSchemaType: 

2539 return ctx.add_type( 

2540 datetime.date, 

2541 "TimeDelta", 

2542 lambda: yuio.json_schema.Meta( 

2543 yuio.json_schema.String( 

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

2545 pattern=( 

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

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

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

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

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

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

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

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

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

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

2556 ) 

2557 ), 

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

2559 description=".", 

2560 ), 

2561 ) 

2562 

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

2564 assert self.assert_type(value) 

2565 return str(value) 

2566 

2567 

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

2569 """ 

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

2571 

2572 :param extensions: 

2573 list of allowed file extensions, including preceding dots. 

2574 

2575 """ 

2576 

2577 def __init__( 

2578 self, 

2579 /, 

2580 *, 

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

2582 ): 

2583 self.__extensions = [extensions] if isinstance(extensions, str) else extensions 

2584 super().__init__(pathlib.Path) 

2585 

2586 def parse(self, value: str, /) -> pathlib.Path: 

2587 path = pathlib.Path(value).expanduser().resolve().absolute() 

2588 self._validate(path) 

2589 return path 

2590 

2591 def parse_config(self, value: object, /) -> pathlib.Path: 

2592 if not isinstance(value, str): 

2593 raise ParsingError.type_mismatch(value, str) 

2594 return self.parse(value) 

2595 

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

2597 if self.__extensions is not None: 

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

2599 if len(self.__extensions) > 1: 

2600 desc = f"{{{desc}}}" 

2601 return desc 

2602 else: 

2603 return super().describe() 

2604 

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

2606 if self.__extensions is not None and not any( 

2607 value.name.endswith(ext) for ext in self.__extensions 

2608 ): 

2609 raise ParsingError( 

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

2611 value, 

2612 yuio.string.Or(self.__extensions), 

2613 ) 

2614 

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

2616 return yuio.complete.File(extensions=self.__extensions) 

2617 

2618 def to_json_schema( 

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

2620 ) -> yuio.json_schema.JsonSchemaType: 

2621 return yuio.json_schema.String() 

2622 

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

2624 assert self.assert_type(value) 

2625 return str(value) 

2626 

2627 

2628class NonExistentPath(Path): 

2629 """ 

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

2631 

2632 :param extensions: 

2633 list of allowed file extensions, including preceding dots. 

2634 

2635 """ 

2636 

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

2638 super()._validate(value) 

2639 

2640 if value.exists(): 

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

2642 

2643 

2644class ExistingPath(Path): 

2645 """ 

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

2647 

2648 :param extensions: 

2649 list of allowed file extensions, including preceding dots. 

2650 

2651 """ 

2652 

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

2654 super()._validate(value) 

2655 

2656 if not value.exists(): 

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

2658 

2659 

2660class File(ExistingPath): 

2661 """ 

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

2663 

2664 :param extensions: 

2665 list of allowed file extensions, including preceding dots. 

2666 

2667 """ 

2668 

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

2670 super()._validate(value) 

2671 

2672 if not value.is_file(): 

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

2674 

2675 

2676class Dir(ExistingPath): 

2677 """ 

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

2679 

2680 """ 

2681 

2682 def __init__(self): 

2683 # Disallow passing `extensions`. 

2684 super().__init__() 

2685 

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

2687 super()._validate(value) 

2688 

2689 if not value.is_dir(): 

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

2691 

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

2693 return yuio.complete.Dir() 

2694 

2695 

2696class GitRepo(Dir): 

2697 """ 

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

2699 

2700 This parser just checks that the given directory has 

2701 a subdirectory named ``.git``. 

2702 

2703 """ 

2704 

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

2706 super()._validate(value) 

2707 

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

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

2710 

2711 

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

2713 """ 

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

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

2716 

2717 """ 

2718 

2719 if _t.TYPE_CHECKING: 

2720 

2721 @_t.overload 

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

2723 

2724 @_t.overload 

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

2726 

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

2728 

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

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

2731 

2732 def parse(self, value: str) -> SecretValue[T]: 

2733 try: 

2734 return super().parse(value) 

2735 except ParsingError: 

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

2737 raise ParsingError("Error when parsing secret data") 

2738 

2739 def parse_many(self, value: _t.Sequence[str]) -> SecretValue[T]: 

2740 try: 

2741 return super().parse_many(value) 

2742 except ParsingError: 

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

2744 raise ParsingError("Error when parsing secret data") 

2745 

2746 def parse_config(self, value: object) -> SecretValue[T]: 

2747 try: 

2748 return super().parse_config(value) 

2749 except ParsingError: 

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

2751 raise ParsingError("Error when parsing secret data") 

2752 

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

2754 return "***" 

2755 

2756 def widget( 

2757 self, 

2758 default: object | yuio.Missing, 

2759 input_description: str | None, 

2760 default_description: str | None, 

2761 /, 

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

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

2764 

2765 def is_secret(self) -> bool: 

2766 return True 

2767 

2768 

2769class CollectionParser( 

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

2771): 

2772 """CollectionParser(inner: Parser[T] | None, /, *, ty: type[C], ctor: typing.Callable[[typing.Iterable[T]], C], iter: typing.Callable[[C], typing.Iterable[T]] = iter, config_type: type[C2] | tuple[type[C2], ...] = list, config_type_iter: typing.Callable[[C2], typing.Iterable[T]] = iter, delimiter: str | None = None) 

2773 

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

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

2776 the result to the given constructor. 

2777 

2778 :param inner: 

2779 parser that will be used to parse collection items. 

2780 :param ty: 

2781 type of the collection that this parser returns. 

2782 :param ctor: 

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

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

2785 :param iter: 

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

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

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

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

2790 :param config_type: 

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

2792 This will usually be a list. 

2793 :param config_type_iter: 

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

2795 :param delimiter: 

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

2797 

2798 The above parameters are exposed via protected attributes: 

2799 ``self._inner``, ``self._ty``, etc. 

2800 

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

2802 that repeats each element twice: 

2803 

2804 .. code-block:: 

2805 

2806 from typing import Iterable, Generic 

2807 

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

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

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

2811 

2812 @staticmethod 

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

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

2815 

2816 def to_json_schema(self, ctx: yuio.json_schema.JsonSchemaContext, /) -> yuio.json_schema.JsonSchemaType: 

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

2818 

2819 """ 

2820 

2821 _allow_completing_duplicates: typing.ClassVar[bool] = True 

2822 """ 

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

2824 

2825 """ 

2826 

2827 def __init__( 

2828 self, 

2829 inner: Parser[T] | None, 

2830 /, 

2831 *, 

2832 ty: type[C], 

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

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

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

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

2837 delimiter: str | None = None, 

2838 ): 

2839 if delimiter == "": 

2840 raise ValueError("empty delimiter") 

2841 

2842 #: See class parameters for more details. 

2843 self._ty = ty 

2844 #: See class parameters for more details. 

2845 self._ctor = ctor 

2846 #: See class parameters for more details. 

2847 self._iter = iter 

2848 #: See class parameters for more details. 

2849 self._config_type = config_type 

2850 #: See class parameters for more details. 

2851 self._config_type_iter = config_type_iter 

2852 #: See class parameters for more details. 

2853 self._delimiter = delimiter 

2854 

2855 super().__init__(inner, ty) 

2856 

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

2858 result = super().wrap(parser) 

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

2860 return result 

2861 

2862 def parse(self, value: str, /) -> C: 

2863 return self.parse_many(value.split(self._delimiter)) 

2864 

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

2866 return self._ctor(self._inner.parse(item) for item in value) 

2867 

2868 def supports_parse_many(self) -> bool: 

2869 return True 

2870 

2871 def parse_config(self, value: object, /) -> C: 

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

2873 expected = self._config_type 

2874 if not isinstance(expected, tuple): 

2875 expected = (expected,) 

2876 raise ParsingError.type_mismatch(value, *expected) 

2877 

2878 return self._ctor( 

2879 self._inner.parse_config(item) for item in self._config_type_iter(value) 

2880 ) 

2881 

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

2883 return "*" 

2884 

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

2886 delimiter = self._delimiter or " " 

2887 value = self._inner.describe_or_def() 

2888 

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

2890 

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

2892 return self._inner.describe_or_def() 

2893 

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

2895 assert self.assert_type(value) 

2896 

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

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

2899 ) 

2900 

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

2902 return None 

2903 

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

2905 completer = self._inner.completer() 

2906 return ( 

2907 yuio.complete.List( 

2908 completer, 

2909 delimiter=self._delimiter, 

2910 allow_duplicates=self._allow_completing_duplicates, 

2911 ) 

2912 if completer is not None 

2913 else None 

2914 ) 

2915 

2916 def widget( 

2917 self, 

2918 default: object | yuio.Missing, 

2919 input_description: str | None, 

2920 default_description: str | None, 

2921 /, 

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

2923 completer = self.completer() 

2924 return _WidgetResultMapper( 

2925 self, 

2926 input_description, 

2927 default, 

2928 ( 

2929 yuio.widget.InputWithCompletion( 

2930 completer, 

2931 placeholder=default_description or "", 

2932 ) 

2933 if completer is not None 

2934 else yuio.widget.Input( 

2935 placeholder=default_description or "", 

2936 ) 

2937 ), 

2938 ) 

2939 

2940 def is_secret(self) -> bool: 

2941 return self._inner.is_secret() 

2942 

2943 def __repr__(self): 

2944 if self._inner_raw is not None: 

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

2946 else: 

2947 return self.__class__.__name__ 

2948 

2949 

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

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

2952 

2953 Parser for lists. 

2954 

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

2956 using a subparser. 

2957 

2958 :param inner: 

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

2960 :param delimiter: 

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

2962 

2963 """ 

2964 

2965 if _t.TYPE_CHECKING: 

2966 

2967 @_t.overload 

2968 def __new__( 

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

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

2971 

2972 @_t.overload 

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

2974 

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

2976 

2977 def __init__( 

2978 self, 

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

2980 /, 

2981 *, 

2982 delimiter: str | None = None, 

2983 ): 

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

2985 

2986 def to_json_schema( 

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

2988 ) -> yuio.json_schema.JsonSchemaType: 

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

2990 

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

2992 assert self.assert_type(value) 

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

2994 

2995 

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

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

2998 

2999 Parser for sets. 

3000 

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

3002 using a subparser. 

3003 

3004 :param inner: 

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

3006 :param delimiter: 

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

3008 

3009 """ 

3010 

3011 if _t.TYPE_CHECKING: 

3012 

3013 @_t.overload 

3014 def __new__( 

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

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

3017 

3018 @_t.overload 

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

3020 

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

3022 

3023 _allow_completing_duplicates = False 

3024 

3025 def __init__( 

3026 self, 

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

3028 /, 

3029 *, 

3030 delimiter: str | None = None, 

3031 ): 

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

3033 

3034 def widget( 

3035 self, 

3036 default: object | yuio.Missing, 

3037 input_description: str | None, 

3038 default_description: str | None, 

3039 /, 

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

3041 options = self._inner.options() 

3042 if options is not None and len(options) <= 25: 

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

3044 else: 

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

3046 

3047 def to_json_schema( 

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

3049 ) -> yuio.json_schema.JsonSchemaType: 

3050 return yuio.json_schema.Array( 

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

3052 ) 

3053 

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

3055 assert self.assert_type(value) 

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

3057 

3058 

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

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

3061 

3062 Parser for frozen sets. 

3063 

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

3065 using a subparser. 

3066 

3067 :param inner: 

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

3069 :param delimiter: 

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

3071 

3072 """ 

3073 

3074 if _t.TYPE_CHECKING: 

3075 

3076 @_t.overload 

3077 def __new__( 

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

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

3080 

3081 @_t.overload 

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

3083 

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

3085 

3086 _allow_completing_duplicates = False 

3087 

3088 def __init__( 

3089 self, 

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

3091 /, 

3092 *, 

3093 delimiter: str | None = None, 

3094 ): 

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

3096 

3097 def to_json_schema( 

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

3099 ) -> yuio.json_schema.JsonSchemaType: 

3100 return yuio.json_schema.Array( 

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

3102 ) 

3103 

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

3105 assert self.assert_type(value) 

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

3107 

3108 

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

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

3111 

3112 Parser for dicts. 

3113 

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

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

3116 

3117 :param key: 

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

3119 :param value: 

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

3121 :param delimiter: 

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

3123 :param pair_delimiter: 

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

3125 

3126 """ 

3127 

3128 if _t.TYPE_CHECKING: 

3129 

3130 @_t.overload 

3131 def __new__( 

3132 cls, 

3133 key: Parser[K], 

3134 value: Parser[V], 

3135 /, 

3136 *, 

3137 delimiter: str | None = None, 

3138 pair_delimiter: str = ":", 

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

3140 

3141 @_t.overload 

3142 def __new__( 

3143 cls, 

3144 /, 

3145 *, 

3146 delimiter: str | None = None, 

3147 pair_delimiter: str = ":", 

3148 ) -> PartialParser: ... 

3149 

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

3151 

3152 _allow_completing_duplicates = False 

3153 

3154 def __init__( 

3155 self, 

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

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

3158 /, 

3159 *, 

3160 delimiter: str | None = None, 

3161 pair_delimiter: str = ":", 

3162 ): 

3163 self.__pair_delimiter = pair_delimiter 

3164 super().__init__( 

3165 Tuple(key, value, delimiter=pair_delimiter) if key and value else None, 

3166 ty=dict, 

3167 ctor=dict, 

3168 iter=dict.items, 

3169 config_type=(dict, list), 

3170 config_type_iter=self.__config_type_iter, 

3171 delimiter=delimiter, 

3172 ) 

3173 

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

3175 result = super().wrap(parser) 

3176 setattr(result._inner, "_Tuple__delimiter", self.__pair_delimiter) 

3177 return result 

3178 

3179 @staticmethod 

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

3181 if isinstance(x, dict): 

3182 return iter(x.items()) 

3183 else: 

3184 return iter(x) 

3185 

3186 def to_json_schema( 

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

3188 ) -> yuio.json_schema.JsonSchemaType: 

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

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

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

3192 

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

3194 assert self.assert_type(value) 

3195 items = _t.cast( 

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

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

3198 ) 

3199 

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

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

3202 else: 

3203 return items 

3204 

3205 

3206class Tuple( 

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

3208 ValueParser[TU], 

3209 PartialParser, 

3210 _t.Generic[TU], 

3211): 

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

3213 

3214 Parser for tuples of fixed lengths. 

3215 

3216 :param parsers: 

3217 parsers for each tuple element. 

3218 :param delimiter: 

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

3220 

3221 """ 

3222 

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

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

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

3226 if _t.TYPE_CHECKING: 

3227 T1 = _t.TypeVar("T1") 

3228 T2 = _t.TypeVar("T2") 

3229 T3 = _t.TypeVar("T3") 

3230 T4 = _t.TypeVar("T4") 

3231 T5 = _t.TypeVar("T5") 

3232 T6 = _t.TypeVar("T6") 

3233 T7 = _t.TypeVar("T7") 

3234 T8 = _t.TypeVar("T8") 

3235 T9 = _t.TypeVar("T9") 

3236 T10 = _t.TypeVar("T10") 

3237 

3238 @_t.overload 

3239 def __new__( 

3240 cls, 

3241 /, 

3242 *, 

3243 delimiter: str | None = None, 

3244 ) -> PartialParser: ... 

3245 

3246 @_t.overload 

3247 def __new__( 

3248 cls, 

3249 p1: Parser[T1], 

3250 /, 

3251 *, 

3252 delimiter: str | None = None, 

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

3254 

3255 @_t.overload 

3256 def __new__( 

3257 cls, 

3258 p1: Parser[T1], 

3259 p2: Parser[T2], 

3260 /, 

3261 *, 

3262 delimiter: str | None = None, 

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

3264 

3265 @_t.overload 

3266 def __new__( 

3267 cls, 

3268 p1: Parser[T1], 

3269 p2: Parser[T2], 

3270 p3: Parser[T3], 

3271 /, 

3272 *, 

3273 delimiter: str | None = None, 

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

3275 

3276 @_t.overload 

3277 def __new__( 

3278 cls, 

3279 p1: Parser[T1], 

3280 p2: Parser[T2], 

3281 p3: Parser[T3], 

3282 p4: Parser[T4], 

3283 /, 

3284 *, 

3285 delimiter: str | None = None, 

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

3287 

3288 @_t.overload 

3289 def __new__( 

3290 cls, 

3291 p1: Parser[T1], 

3292 p2: Parser[T2], 

3293 p3: Parser[T3], 

3294 p4: Parser[T4], 

3295 p5: Parser[T5], 

3296 /, 

3297 *, 

3298 delimiter: str | None = None, 

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

3300 

3301 @_t.overload 

3302 def __new__( 

3303 cls, 

3304 p1: Parser[T1], 

3305 p2: Parser[T2], 

3306 p3: Parser[T3], 

3307 p4: Parser[T4], 

3308 p5: Parser[T5], 

3309 p6: Parser[T6], 

3310 /, 

3311 *, 

3312 delimiter: str | None = None, 

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

3314 

3315 @_t.overload 

3316 def __new__( 

3317 cls, 

3318 p1: Parser[T1], 

3319 p2: Parser[T2], 

3320 p3: Parser[T3], 

3321 p4: Parser[T4], 

3322 p5: Parser[T5], 

3323 p6: Parser[T6], 

3324 p7: Parser[T7], 

3325 /, 

3326 *, 

3327 delimiter: str | None = None, 

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

3329 

3330 @_t.overload 

3331 def __new__( 

3332 cls, 

3333 p1: Parser[T1], 

3334 p2: Parser[T2], 

3335 p3: Parser[T3], 

3336 p4: Parser[T4], 

3337 p5: Parser[T5], 

3338 p6: Parser[T6], 

3339 p7: Parser[T7], 

3340 p8: Parser[T8], 

3341 /, 

3342 *, 

3343 delimiter: str | None = None, 

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

3345 

3346 @_t.overload 

3347 def __new__( 

3348 cls, 

3349 p1: Parser[T1], 

3350 p2: Parser[T2], 

3351 p3: Parser[T3], 

3352 p4: Parser[T4], 

3353 p5: Parser[T5], 

3354 p6: Parser[T6], 

3355 p7: Parser[T7], 

3356 p8: Parser[T8], 

3357 p9: Parser[T9], 

3358 /, 

3359 *, 

3360 delimiter: str | None = None, 

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

3362 

3363 @_t.overload 

3364 def __new__( 

3365 cls, 

3366 p1: Parser[T1], 

3367 p2: Parser[T2], 

3368 p3: Parser[T3], 

3369 p4: Parser[T4], 

3370 p5: Parser[T5], 

3371 p6: Parser[T6], 

3372 p7: Parser[T7], 

3373 p8: Parser[T8], 

3374 p9: Parser[T9], 

3375 p10: Parser[T10], 

3376 /, 

3377 *, 

3378 delimiter: str | None = None, 

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

3380 

3381 @_t.overload 

3382 def __new__( 

3383 cls, 

3384 p1: Parser[T1], 

3385 p2: Parser[T2], 

3386 p3: Parser[T3], 

3387 p4: Parser[T4], 

3388 p5: Parser[T5], 

3389 p6: Parser[T6], 

3390 p7: Parser[T7], 

3391 p8: Parser[T8], 

3392 p9: Parser[T9], 

3393 p10: Parser[T10], 

3394 p11: Parser[object], 

3395 *tail: Parser[object], 

3396 delimiter: str | None = None, 

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

3398 

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

3400 

3401 def __init__( 

3402 self, 

3403 *parsers: Parser[_t.Any], 

3404 delimiter: str | None = None, 

3405 ): 

3406 if delimiter == "": 

3407 raise ValueError("empty delimiter") 

3408 self.__delimiter = delimiter 

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

3410 

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

3412 result = super().wrap(parser) 

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

3414 return result 

3415 

3416 def parse(self, value: str, /) -> TU: 

3417 items = value.split(self.__delimiter, maxsplit=len(self._inner) - 1) 

3418 return self.parse_many(items) 

3419 

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

3421 if len(value) != len(self._inner): 

3422 raise ParsingError( 

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

3424 len(self._inner), 

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

3426 len(value), 

3427 value, 

3428 ) 

3429 

3430 return _t.cast( 

3431 TU, 

3432 tuple(parser.parse(item) for parser, item in zip(self._inner, value)), 

3433 ) 

3434 

3435 def parse_config(self, value: object, /) -> TU: 

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

3437 raise ParsingError.type_mismatch(value, list, tuple) 

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

3439 raise ParsingError( 

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

3441 len(self._inner), 

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

3443 len(value), 

3444 value, 

3445 ) 

3446 

3447 return _t.cast( 

3448 TU, 

3449 tuple( 

3450 parser.parse_config(item) for parser, item in zip(self._inner, value) 

3451 ), 

3452 ) 

3453 

3454 def supports_parse_many(self) -> bool: 

3455 return True 

3456 

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

3458 return len(self._inner) 

3459 

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

3461 delimiter = self.__delimiter or " " 

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

3463 return delimiter.join(desc) 

3464 

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

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

3467 

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

3469 assert self.assert_type(value) 

3470 

3471 delimiter = self.__delimiter or " " 

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

3473 

3474 return delimiter.join(desc) 

3475 

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

3477 return None 

3478 

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

3480 return yuio.complete.Tuple( 

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

3482 delimiter=self.__delimiter, 

3483 ) 

3484 

3485 def widget( 

3486 self, 

3487 default: object | yuio.Missing, 

3488 input_description: str | None, 

3489 default_description: str | None, 

3490 /, 

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

3492 completer = self.completer() 

3493 

3494 return _WidgetResultMapper( 

3495 self, 

3496 input_description, 

3497 default, 

3498 ( 

3499 yuio.widget.InputWithCompletion( 

3500 completer, 

3501 placeholder=default_description or "", 

3502 ) 

3503 if completer is not None 

3504 else yuio.widget.Input( 

3505 placeholder=default_description or "", 

3506 ) 

3507 ), 

3508 ) 

3509 

3510 def to_json_schema( 

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

3512 ) -> yuio.json_schema.JsonSchemaType: 

3513 return yuio.json_schema.Tuple( 

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

3515 ) 

3516 

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

3518 assert self.assert_type(value) 

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

3520 

3521 def is_secret(self) -> bool: 

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

3523 

3524 def __repr__(self): 

3525 if self._inner_raw is not None: 

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

3527 else: 

3528 return self.__class__.__name__ 

3529 

3530 

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

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

3533 

3534 Parser for optional values. 

3535 

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

3537 are parsed, though. 

3538 

3539 :param inner: 

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

3541 

3542 """ 

3543 

3544 if _t.TYPE_CHECKING: 

3545 

3546 @_t.overload 

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

3548 

3549 @_t.overload 

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

3551 

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

3553 

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

3555 super().__init__(inner) 

3556 

3557 def parse(self, value: str, /) -> T | None: 

3558 return self._inner.parse(value) 

3559 

3560 def parse_many(self, value: _t.Sequence[str], /) -> T | None: 

3561 return self._inner.parse_many(value) 

3562 

3563 def parse_config(self, value: object, /) -> T | None: 

3564 if value is None: 

3565 return None 

3566 return self._inner.parse_config(value) 

3567 

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

3569 if value is None: 

3570 return True 

3571 return self._inner.check_type(value) 

3572 

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

3574 if value is None: 

3575 return "<none>" 

3576 return self._inner.describe_value(value) 

3577 

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

3579 return self._inner.options() 

3580 

3581 def widget( 

3582 self, 

3583 default: object | yuio.Missing, 

3584 input_description: str | None, 

3585 default_description: str | None, 

3586 /, 

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

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

3589 

3590 def to_json_schema( 

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

3592 ) -> yuio.json_schema.JsonSchemaType: 

3593 return yuio.json_schema.OneOf( 

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

3595 ) 

3596 

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

3598 if value is None: 

3599 return None 

3600 else: 

3601 return self._inner.to_json_value(value) 

3602 

3603 

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

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

3606 

3607 Tries several parsers and returns the first successful result. 

3608 

3609 .. warning:: 

3610 

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

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

3613 

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

3615 can't fail:: 

3616 

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

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

3619 '10' 

3620 

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

3622 

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

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

3625 10 

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

3627 'not an int' 

3628 

3629 """ 

3630 

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

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

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

3634 if _t.TYPE_CHECKING: 

3635 T1 = _t.TypeVar("T1") 

3636 T2 = _t.TypeVar("T2") 

3637 T3 = _t.TypeVar("T3") 

3638 T4 = _t.TypeVar("T4") 

3639 T5 = _t.TypeVar("T5") 

3640 T6 = _t.TypeVar("T6") 

3641 T7 = _t.TypeVar("T7") 

3642 T8 = _t.TypeVar("T8") 

3643 T9 = _t.TypeVar("T9") 

3644 T10 = _t.TypeVar("T10") 

3645 

3646 @_t.overload 

3647 def __new__( 

3648 cls, 

3649 /, 

3650 ) -> PartialParser: ... 

3651 

3652 @_t.overload 

3653 def __new__( 

3654 cls, 

3655 p1: Parser[T1], 

3656 /, 

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

3658 

3659 @_t.overload 

3660 def __new__( 

3661 cls, 

3662 p1: Parser[T1], 

3663 p2: Parser[T2], 

3664 /, 

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

3666 

3667 @_t.overload 

3668 def __new__( 

3669 cls, 

3670 p1: Parser[T1], 

3671 p2: Parser[T2], 

3672 p3: Parser[T3], 

3673 /, 

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

3675 

3676 @_t.overload 

3677 def __new__( 

3678 cls, 

3679 p1: Parser[T1], 

3680 p2: Parser[T2], 

3681 p3: Parser[T3], 

3682 p4: Parser[T4], 

3683 /, 

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

3685 

3686 @_t.overload 

3687 def __new__( 

3688 cls, 

3689 p1: Parser[T1], 

3690 p2: Parser[T2], 

3691 p3: Parser[T3], 

3692 p4: Parser[T4], 

3693 p5: Parser[T5], 

3694 /, 

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

3696 

3697 @_t.overload 

3698 def __new__( 

3699 cls, 

3700 p1: Parser[T1], 

3701 p2: Parser[T2], 

3702 p3: Parser[T3], 

3703 p4: Parser[T4], 

3704 p5: Parser[T5], 

3705 p6: Parser[T6], 

3706 /, 

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

3708 

3709 @_t.overload 

3710 def __new__( 

3711 cls, 

3712 p1: Parser[T1], 

3713 p2: Parser[T2], 

3714 p3: Parser[T3], 

3715 p4: Parser[T4], 

3716 p5: Parser[T5], 

3717 p6: Parser[T6], 

3718 p7: Parser[T7], 

3719 /, 

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

3721 

3722 @_t.overload 

3723 def __new__( 

3724 cls, 

3725 p1: Parser[T1], 

3726 p2: Parser[T2], 

3727 p3: Parser[T3], 

3728 p4: Parser[T4], 

3729 p5: Parser[T5], 

3730 p6: Parser[T6], 

3731 p7: Parser[T7], 

3732 p8: Parser[T8], 

3733 /, 

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

3735 

3736 @_t.overload 

3737 def __new__( 

3738 cls, 

3739 p1: Parser[T1], 

3740 p2: Parser[T2], 

3741 p3: Parser[T3], 

3742 p4: Parser[T4], 

3743 p5: Parser[T5], 

3744 p6: Parser[T6], 

3745 p7: Parser[T7], 

3746 p8: Parser[T8], 

3747 p9: Parser[T9], 

3748 /, 

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

3750 

3751 @_t.overload 

3752 def __new__( 

3753 cls, 

3754 p1: Parser[T1], 

3755 p2: Parser[T2], 

3756 p3: Parser[T3], 

3757 p4: Parser[T4], 

3758 p5: Parser[T5], 

3759 p6: Parser[T6], 

3760 p7: Parser[T7], 

3761 p8: Parser[T8], 

3762 p9: Parser[T9], 

3763 p10: Parser[T10], 

3764 /, 

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

3766 

3767 @_t.overload 

3768 def __new__( 

3769 cls, 

3770 p1: Parser[T1], 

3771 p2: Parser[T2], 

3772 p3: Parser[T3], 

3773 p4: Parser[T4], 

3774 p5: Parser[T5], 

3775 p6: Parser[T6], 

3776 p7: Parser[T7], 

3777 p8: Parser[T8], 

3778 p9: Parser[T9], 

3779 p10: Parser[T10], 

3780 p11: Parser[object], 

3781 *parsers: Parser[object], 

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

3783 

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

3785 

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

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

3788 

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

3790 result = super().wrap(parser) 

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

3792 return result 

3793 

3794 def parse(self, value: str, /) -> T: 

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

3796 for parser in self._inner: 

3797 try: 

3798 return parser.parse(value) 

3799 except ParsingError as e: 

3800 errors.append((parser, e)) 

3801 

3802 msgs = [] 

3803 for parser, error in errors: 

3804 msgs.append( 

3805 yuio.string.Format( 

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

3807 parser.describe_or_def(), 

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

3809 ) 

3810 ) 

3811 raise ParsingError( 

3812 "Can't parse `%r`:\n%s", value, yuio.string.JoinStr(msgs, sep="\n") 

3813 ) 

3814 

3815 def parse_config(self, value: object, /) -> T: 

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

3817 for parser in self._inner: 

3818 try: 

3819 return parser.parse_config(value) 

3820 except ParsingError as e: 

3821 errors.append((parser, e)) 

3822 

3823 msgs = [] 

3824 for parser, error in errors: 

3825 msgs.append( 

3826 yuio.string.Format( 

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

3828 parser.describe(), 

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

3830 ) 

3831 ) 

3832 raise ParsingError( 

3833 "Can't parse `%r`:\n%s", value, yuio.string.JoinStr(msgs, sep="\n") 

3834 ) 

3835 

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

3837 return any(parser.check_type(value) for parser in self._inner) 

3838 

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

3840 if len(self._inner) > 1: 

3841 

3842 def strip_curly_brackets(desc: str): 

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

3844 s = desc[1:-1] 

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

3846 return s 

3847 return desc 

3848 

3849 desc = "|".join( 

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

3851 ) 

3852 desc = f"{{{desc}}}" 

3853 else: 

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

3855 return desc 

3856 

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

3858 for parser in self._inner: 

3859 try: 

3860 return parser.describe_value(value) 

3861 except TypeError: 

3862 pass 

3863 

3864 raise TypeError( 

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

3866 ) 

3867 

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

3869 result = [] 

3870 got_options = False 

3871 for parser in self._inner: 

3872 if options := parser.options(): 

3873 result.extend(options) 

3874 got_options = True 

3875 if got_options: 

3876 return result 

3877 else: 

3878 return None 

3879 

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

3881 completers = [] 

3882 for parser in self._inner: 

3883 if completer := parser.completer(): 

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

3885 if not completers: 

3886 return None 

3887 elif len(completers) == 1: 

3888 return completers[0][1] 

3889 else: 

3890 return yuio.complete.Alternative(completers) 

3891 

3892 def to_json_schema( 

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

3894 ) -> yuio.json_schema.JsonSchemaType: 

3895 return yuio.json_schema.OneOf( 

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

3897 ) 

3898 

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

3900 for parser in self._inner: 

3901 try: 

3902 return parser.to_json_value(value) 

3903 except TypeError: 

3904 pass 

3905 

3906 raise TypeError( 

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

3908 ) 

3909 

3910 def is_secret(self) -> bool: 

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

3912 

3913 def __repr__(self): 

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

3915 

3916 

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

3918 def __init__( 

3919 self, 

3920 inner: Parser[T] | None, 

3921 /, 

3922 *, 

3923 lower: Cmp | None = None, 

3924 lower_inclusive: Cmp | None = None, 

3925 upper: Cmp | None = None, 

3926 upper_inclusive: Cmp | None = None, 

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

3928 desc: str, 

3929 ): 

3930 super().__init__(inner) 

3931 

3932 self._lower_bound: Cmp | None = None 

3933 self._lower_bound_is_inclusive: bool = True 

3934 self._upper_bound: Cmp | None = None 

3935 self._upper_bound_is_inclusive: bool = True 

3936 

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

3938 raise TypeError( 

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

3940 ) 

3941 elif lower is not None: 

3942 self._lower_bound = lower 

3943 self._lower_bound_is_inclusive = False 

3944 elif lower_inclusive is not None: 

3945 self._lower_bound = lower_inclusive 

3946 self._lower_bound_is_inclusive = True 

3947 

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

3949 raise TypeError( 

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

3951 ) 

3952 elif upper is not None: 

3953 self._upper_bound = upper 

3954 self._upper_bound_is_inclusive = False 

3955 elif upper_inclusive is not None: 

3956 self._upper_bound = upper_inclusive 

3957 self._upper_bound_is_inclusive = True 

3958 

3959 self.__mapper = mapper 

3960 self.__desc = desc 

3961 

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

3963 mapped = self.__mapper(value) 

3964 

3965 if self._lower_bound is not None: 

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

3967 raise ParsingError( 

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

3969 self.__desc, 

3970 self._lower_bound, 

3971 value, 

3972 ) 

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

3974 raise ParsingError( 

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

3976 self.__desc, 

3977 self._lower_bound, 

3978 value, 

3979 ) 

3980 

3981 if self._upper_bound is not None: 

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

3983 raise ParsingError( 

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

3985 self.__desc, 

3986 self._upper_bound, 

3987 value, 

3988 ) 

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

3990 raise ParsingError( 

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

3992 self.__desc, 

3993 self._upper_bound, 

3994 value, 

3995 ) 

3996 

3997 def __repr__(self): 

3998 desc = "" 

3999 if self._lower_bound is not None: 

4000 desc += repr(self._lower_bound) 

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

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

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

4004 desc += mapper_name 

4005 else: 

4006 desc += "x" 

4007 if self._upper_bound is not None: 

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

4009 desc += repr(self._upper_bound) 

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

4011 

4012 

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

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

4015 

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

4017 

4018 :param inner: 

4019 parser whose result will be validated. 

4020 :param lower: 

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

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

4023 :param lower_inclusive: 

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

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

4026 :param upper: 

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

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

4029 :param upper_inclusive: 

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

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

4032 :example: 

4033 :: 

4034 

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

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

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

4038 

4039 """ 

4040 

4041 if _t.TYPE_CHECKING: 

4042 

4043 @_t.overload 

4044 def __new__( 

4045 cls, 

4046 inner: Parser[Cmp], 

4047 /, 

4048 *, 

4049 lower: Cmp | None = None, 

4050 lower_inclusive: Cmp | None = None, 

4051 upper: Cmp | None = None, 

4052 upper_inclusive: Cmp | None = None, 

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

4054 

4055 @_t.overload 

4056 def __new__( 

4057 cls, 

4058 *, 

4059 lower: Cmp | None = None, 

4060 lower_inclusive: Cmp | None = None, 

4061 upper: Cmp | None = None, 

4062 upper_inclusive: Cmp | None = None, 

4063 ) -> PartialParser: ... 

4064 

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

4066 

4067 def __init__( 

4068 self, 

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

4070 /, 

4071 *, 

4072 lower: Cmp | None = None, 

4073 lower_inclusive: Cmp | None = None, 

4074 upper: Cmp | None = None, 

4075 upper_inclusive: Cmp | None = None, 

4076 ): 

4077 super().__init__( 

4078 inner, 

4079 lower=lower, 

4080 lower_inclusive=lower_inclusive, 

4081 upper=upper, 

4082 upper_inclusive=upper_inclusive, 

4083 mapper=lambda x: x, 

4084 desc="Value", 

4085 ) 

4086 

4087 def to_json_schema( 

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

4089 ) -> yuio.json_schema.JsonSchemaType: 

4090 bound = {} 

4091 if isinstance(self._lower_bound, (int, float)): 

4092 bound[ 

4093 "minimum" if self._lower_bound_is_inclusive else "exclusiveMinimum" 

4094 ] = self._lower_bound 

4095 if isinstance(self._upper_bound, (int, float)): 

4096 bound[ 

4097 "maximum" if self._upper_bound_is_inclusive else "exclusiveMaximum" 

4098 ] = self._upper_bound 

4099 if bound: 

4100 return yuio.json_schema.AllOf( 

4101 [super().to_json_schema(ctx), yuio.json_schema.Opaque(bound)] 

4102 ) 

4103 else: 

4104 return super().to_json_schema(ctx) 

4105 

4106 

4107@_t.overload 

4108def Gt(inner: Parser[Cmp], bound: Cmp, /) -> Bound[Cmp]: ... 

4109@_t.overload 

4110def Gt(bound: _t.SupportsLt[_t.Any], /) -> PartialParser: ... 

4111def Gt(*args) -> _t.Any: 

4112 """Gt(inner: Parser[Cmp], bound: Cmp, /) 

4113 

4114 Alias for :class:`Bound`. 

4115 

4116 :param inner: 

4117 parser whose result will be validated. 

4118 :param bound: 

4119 lower bound for parsed values. 

4120 

4121 """ 

4122 

4123 if len(args) == 1: 

4124 return Bound(lower=args[0]) 

4125 elif len(args) == 2: 

4126 return Bound(args[0], lower=args[1]) 

4127 else: 

4128 raise TypeError(f"expected 1 or 2 positional arguments, got {len(args)}") 

4129 

4130 

4131@_t.overload 

4132def Ge(inner: Parser[Cmp], bound: Cmp, /) -> Bound[Cmp]: ... 

4133@_t.overload 

4134def Ge(bound: _t.SupportsLt[_t.Any], /) -> PartialParser: ... 

4135def Ge(*args) -> _t.Any: 

4136 """Ge(inner: Parser[Cmp], bound: Cmp, /) 

4137 

4138 Alias for :class:`Bound`. 

4139 

4140 :param inner: 

4141 parser whose result will be validated. 

4142 :param bound: 

4143 lower inclusive bound for parsed values. 

4144 

4145 """ 

4146 

4147 if len(args) == 1: 

4148 return Bound(lower_inclusive=args[0]) 

4149 elif len(args) == 2: 

4150 return Bound(args[0], lower_inclusive=args[1]) 

4151 else: 

4152 raise TypeError(f"expected 1 or 2 positional arguments, got {len(args)}") 

4153 

4154 

4155@_t.overload 

4156def Lt(inner: Parser[Cmp], bound: Cmp, /) -> Bound[Cmp]: ... 

4157@_t.overload 

4158def Lt(bound: _t.SupportsLt[_t.Any], /) -> PartialParser: ... 

4159def Lt(*args) -> _t.Any: 

4160 """Lt(inner: Parser[Cmp], bound: Cmp, /) 

4161 

4162 Alias for :class:`Bound`. 

4163 

4164 :param inner: 

4165 parser whose result will be validated. 

4166 :param bound: 

4167 upper bound for parsed values. 

4168 

4169 """ 

4170 

4171 if len(args) == 1: 

4172 return Bound(upper=args[0]) 

4173 elif len(args) == 2: 

4174 return Bound(args[0], upper=args[1]) 

4175 else: 

4176 raise TypeError(f"expected 1 or 2 positional arguments, got {len(args)}") 

4177 

4178 

4179@_t.overload 

4180def Le(inner: Parser[Cmp], bound: Cmp, /) -> Bound[Cmp]: ... 

4181@_t.overload 

4182def Le(bound: _t.SupportsLt[_t.Any], /) -> PartialParser: ... 

4183def Le(*args) -> _t.Any: 

4184 """Le(inner: Parser[Cmp], bound: Cmp, /) 

4185 

4186 Alias for :class:`Bound`. 

4187 

4188 :param inner: 

4189 parser whose result will be validated. 

4190 :param bound: 

4191 upper inclusive bound for parsed values. 

4192 

4193 """ 

4194 

4195 if len(args) == 1: 

4196 return Bound(upper_inclusive=args[0]) 

4197 elif len(args) == 2: 

4198 return Bound(args[0], upper_inclusive=args[1]) 

4199 else: 

4200 raise TypeError(f"expected 1 or 2 positional arguments, got {len(args)}") 

4201 

4202 

4203class LenBound(_BoundImpl[Sz, int], _t.Generic[Sz]): 

4204 """LenBound(inner: Parser[Sz], /, *, lower: int | None = None, lower_inclusive: int | None = None, upper: int | None = None, upper_inclusive: int | None = None) 

4205 

4206 Check that length of a value is upper- or lower-bound by some constraints. 

4207 

4208 The signature is the same as of the :class:`Bound` class. 

4209 

4210 :param inner: 

4211 parser whose result will be validated. 

4212 :param lower: 

4213 set lower bound for value's length, so we require that ``len(value) > lower``. 

4214 Can't be given if ``lower_inclusive`` is also given. 

4215 :param lower_inclusive: 

4216 set lower bound for value's length, so we require that ``len(value) >= lower``. 

4217 Can't be given if ``lower`` is also given. 

4218 :param upper: 

4219 set upper bound for value's length, so we require that ``len(value) < upper``. 

4220 Can't be given if ``upper_inclusive`` is also given. 

4221 :param upper_inclusive: 

4222 set upper bound for value's length, so we require that ``len(value) <= upper``. 

4223 Can't be given if ``upper`` is also given. 

4224 :example: 

4225 :: 

4226 

4227 >>> # List of up to five ints: 

4228 >>> LenBound(List(Int()), upper_inclusive=5) 

4229 LenBound(List(Int), len <= 5) 

4230 

4231 """ 

4232 

4233 if _t.TYPE_CHECKING: 

4234 

4235 @_t.overload 

4236 def __new__( 

4237 cls, 

4238 inner: Parser[Sz], 

4239 /, 

4240 *, 

4241 lower: int | None = None, 

4242 lower_inclusive: int | None = None, 

4243 upper: int | None = None, 

4244 upper_inclusive: int | None = None, 

4245 ) -> LenBound[Sz]: ... 

4246 

4247 @_t.overload 

4248 def __new__( 

4249 cls, 

4250 /, 

4251 *, 

4252 lower: int | None = None, 

4253 lower_inclusive: int | None = None, 

4254 upper: int | None = None, 

4255 upper_inclusive: int | None = None, 

4256 ) -> PartialParser: ... 

4257 

4258 def __new__(cls, *args, **kwargs) -> _t.Any: ... 

4259 

4260 def __init__( 

4261 self, 

4262 inner: Parser[Sz] | None = None, 

4263 /, 

4264 *, 

4265 lower: int | None = None, 

4266 lower_inclusive: int | None = None, 

4267 upper: int | None = None, 

4268 upper_inclusive: int | None = None, 

4269 ): 

4270 super().__init__( 

4271 inner, 

4272 lower=lower, 

4273 lower_inclusive=lower_inclusive, 

4274 upper=upper, 

4275 upper_inclusive=upper_inclusive, 

4276 mapper=len, 

4277 desc="Length of value", 

4278 ) 

4279 

4280 def get_nargs(self) -> _t.Literal["+", "*", "?"] | int | None: 

4281 if not self._inner.supports_parse_many(): 

4282 # somebody bound len of a string? 

4283 return self._inner.get_nargs() 

4284 

4285 lower = self._lower_bound 

4286 if lower is not None and not self._lower_bound_is_inclusive: 

4287 lower += 1 

4288 upper = self._upper_bound 

4289 if upper is not None and not self._upper_bound_is_inclusive: 

4290 upper -= 1 

4291 

4292 if lower == upper and lower is not None: 

4293 return lower 

4294 elif lower is not None and lower > 0: 

4295 return "+" 

4296 else: 

4297 return "*" 

4298 

4299 def to_json_schema( 

4300 self, ctx: yuio.json_schema.JsonSchemaContext, / 

4301 ) -> yuio.json_schema.JsonSchemaType: 

4302 bound = {} 

4303 min_bound = self._lower_bound 

4304 if not self._lower_bound_is_inclusive and min_bound is not None: 

4305 min_bound -= 1 

4306 if min_bound is not None: 

4307 bound["minLength"] = bound["minItems"] = bound["minProperties"] = min_bound 

4308 max_bound = self._upper_bound 

4309 if not self._upper_bound_is_inclusive and max_bound is not None: 

4310 max_bound += 1 

4311 if max_bound is not None: 

4312 bound["maxLength"] = bound["maxItems"] = bound["maxProperties"] = max_bound 

4313 if bound: 

4314 return yuio.json_schema.AllOf( 

4315 [super().to_json_schema(ctx), yuio.json_schema.Opaque(bound)] 

4316 ) 

4317 else: 

4318 return super().to_json_schema(ctx) 

4319 

4320 

4321@_t.overload 

4322def LenGt(inner: Parser[Sz], bound: int, /) -> LenBound[Sz]: ... 

4323@_t.overload 

4324def LenGt(bound: int, /) -> PartialParser: ... 

4325def LenGt(*args) -> _t.Any: 

4326 """LenGt(inner: Parser[Sz], bound: int, /) 

4327 

4328 Alias for :class:`LenBound`. 

4329 

4330 :param inner: 

4331 parser whose result will be validated. 

4332 :param bound: 

4333 lower bound for parsed values's length. 

4334 

4335 """ 

4336 

4337 if len(args) == 1: 

4338 return LenBound(lower=args[0]) 

4339 elif len(args) == 2: 

4340 return LenBound(args[0], lower=args[1]) 

4341 else: 

4342 raise TypeError(f"expected 1 or 2 positional arguments, got {len(args)}") 

4343 

4344 

4345@_t.overload 

4346def LenGe(inner: Parser[Sz], bound: int, /) -> LenBound[Sz]: ... 

4347@_t.overload 

4348def LenGe(bound: int, /) -> PartialParser: ... 

4349def LenGe(*args) -> _t.Any: 

4350 """LenGe(inner: Parser[Sz], bound: int, /) 

4351 

4352 Alias for :class:`LenBound`. 

4353 

4354 :param inner: 

4355 parser whose result will be validated. 

4356 :param bound: 

4357 lower inclusive bound for parsed values's length. 

4358 

4359 """ 

4360 

4361 if len(args) == 1: 

4362 return LenBound(lower_inclusive=args[0]) 

4363 elif len(args) == 2: 

4364 return LenBound(args[0], lower_inclusive=args[1]) 

4365 else: 

4366 raise TypeError(f"expected 1 or 2 positional arguments, got {len(args)}") 

4367 

4368 

4369@_t.overload 

4370def LenLt(inner: Parser[Sz], bound: int, /) -> LenBound[Sz]: ... 

4371@_t.overload 

4372def LenLt(bound: int, /) -> PartialParser: ... 

4373def LenLt(*args) -> _t.Any: 

4374 """LenLt(inner: Parser[Sz], bound: int, /) 

4375 

4376 Alias for :class:`LenBound`. 

4377 

4378 :param inner: 

4379 parser whose result will be validated. 

4380 :param bound: 

4381 upper bound for parsed values's length. 

4382 

4383 """ 

4384 

4385 if len(args) == 1: 

4386 return LenBound(upper=args[0]) 

4387 elif len(args) == 2: 

4388 return LenBound(args[0], upper=args[1]) 

4389 else: 

4390 raise TypeError(f"expected 1 or 2 positional arguments, got {len(args)}") 

4391 

4392 

4393@_t.overload 

4394def LenLe(inner: Parser[Sz], bound: int, /) -> LenBound[Sz]: ... 

4395@_t.overload 

4396def LenLe(bound: int, /) -> PartialParser: ... 

4397def LenLe(*args) -> _t.Any: 

4398 """LenLe(inner: Parser[Sz], bound: int, /) 

4399 

4400 Alias for :class:`LenBound`. 

4401 

4402 :param inner: 

4403 parser whose result will be validated. 

4404 :param bound: 

4405 upper inclusive bound for parsed values's length. 

4406 

4407 """ 

4408 

4409 if len(args) == 1: 

4410 return LenBound(upper_inclusive=args[0]) 

4411 elif len(args) == 2: 

4412 return LenBound(args[0], upper_inclusive=args[1]) 

4413 else: 

4414 raise TypeError(f"expected 1 or 2 positional arguments, got {len(args)}") 

4415 

4416 

4417class OneOf(ValidatingParser[T], _t.Generic[T]): 

4418 """OneOf(inner: Parser[T], values: typing.Collection[T], /) 

4419 

4420 Check that the parsed value is one of the given set of values. 

4421 

4422 :param inner: 

4423 parser whose result will be validated. 

4424 :param values: 

4425 collection of allowed values. 

4426 :example: 

4427 :: 

4428 

4429 >>> # Accepts only strings 'A', 'B', or 'C': 

4430 >>> OneOf(Str(), ['A', 'B', 'C']) 

4431 OneOf(Str) 

4432 

4433 """ 

4434 

4435 if _t.TYPE_CHECKING: 

4436 

4437 @_t.overload 

4438 def __new__(cls, inner: Parser[T], values: _t.Collection[T], /) -> OneOf[T]: ... 

4439 

4440 @_t.overload 

4441 def __new__(cls, values: _t.Collection[T], /) -> PartialParser: ... 

4442 

4443 def __new__(cls, *args) -> _t.Any: ... 

4444 

4445 def __init__(self, *args): 

4446 inner: Parser[T] | None 

4447 values: _t.Collection[T] 

4448 if len(args) == 1: 

4449 inner, values = None, args[0] 

4450 elif len(args) == 2: 

4451 inner, values = args 

4452 else: 

4453 raise TypeError(f"expected 1 or 2 positional arguments, got {len(args)}") 

4454 

4455 super().__init__(inner) 

4456 

4457 self.__allowed_values = values 

4458 

4459 def _validate(self, value: T, /): 

4460 if value not in self.__allowed_values: 

4461 raise ParsingError( 

4462 "Can't parse `%r`, should be %s", 

4463 value, 

4464 yuio.string.JoinRepr.or_(self.__allowed_values), 

4465 ) 

4466 

4467 def describe(self) -> str | None: 

4468 desc = "|".join(self.describe_value(e) for e in self.__allowed_values) 

4469 if len(desc) < 80: 

4470 if len(self.__allowed_values) > 1: 

4471 desc = f"{{{desc}}}" 

4472 return desc 

4473 else: 

4474 return super().describe() 

4475 

4476 def describe_or_def(self) -> str: 

4477 desc = "|".join(self.describe_value(e) for e in self.__allowed_values) 

4478 if len(desc) < 80: 

4479 if len(self.__allowed_values) > 1: 

4480 desc = f"{{{desc}}}" 

4481 return desc 

4482 else: 

4483 return super().describe_or_def() 

4484 

4485 def options(self) -> _t.Collection[yuio.widget.Option[T]] | None: 

4486 return [ 

4487 yuio.widget.Option(e, self.describe_value(e)) for e in self.__allowed_values 

4488 ] 

4489 

4490 def completer(self) -> yuio.complete.Completer | None: 

4491 return yuio.complete.Choice( 

4492 [ 

4493 yuio.complete.Option(self.describe_value(e)) 

4494 for e in self.__allowed_values 

4495 ] 

4496 ) 

4497 

4498 def widget( 

4499 self, 

4500 default: object | yuio.Missing, 

4501 input_description: str | None, 

4502 default_description: str | None, 

4503 /, 

4504 ) -> yuio.widget.Widget[T | yuio.Missing]: 

4505 allowed_values = list(self.__allowed_values) 

4506 

4507 options = _t.cast(list[yuio.widget.Option[T | yuio.Missing]], self.options()) 

4508 

4509 if default is yuio.MISSING: 

4510 default_index = 0 

4511 elif default in allowed_values: 

4512 default_index = list(allowed_values).index(default) # type: ignore 

4513 else: 

4514 options.insert( 

4515 0, yuio.widget.Option(yuio.MISSING, default_description or str(default)) 

4516 ) 

4517 default_index = 0 

4518 

4519 return yuio.widget.Choice(options, default_index=default_index) 

4520 

4521 

4522class WithMeta(MappingParser[T, T], _t.Generic[T]): 

4523 """WithMeta(inner: Parser[T], /, *, desc: str, completer: yuio.complete.Completer | None | ~yuio.MISSING = MISSING) 

4524 

4525 Overrides inline help messages and other meta information of a wrapped parser. 

4526 

4527 Inline help messages will show up as hints in autocompletion and widgets. 

4528 

4529 :param inner: 

4530 inner parser. 

4531 :param desc: 

4532 description override. This short string will be used in CLI, widgets, and 

4533 completers to describe expected value. 

4534 :param completer: 

4535 completer override. Pass :data:`None` to disable completion. 

4536 

4537 """ 

4538 

4539 if _t.TYPE_CHECKING: 

4540 

4541 @_t.overload 

4542 def __new__( 

4543 cls, 

4544 inner: Parser[T], 

4545 /, 

4546 *, 

4547 desc: str | None = None, 

4548 completer: yuio.complete.Completer | yuio.Missing | None = yuio.MISSING, 

4549 ) -> MappingParser[T, T]: ... 

4550 

4551 @_t.overload 

4552 def __new__( 

4553 cls, 

4554 /, 

4555 *, 

4556 desc: str | None = None, 

4557 completer: yuio.complete.Completer | yuio.Missing | None = yuio.MISSING, 

4558 ) -> PartialParser: ... 

4559 

4560 def __new__(cls, *args, **kwargs) -> _t.Any: ... 

4561 

4562 def __init__( 

4563 self, 

4564 *args, 

4565 desc: str | None = None, 

4566 completer: yuio.complete.Completer | yuio.Missing | None = yuio.MISSING, 

4567 ): 

4568 inner: Parser[T] | None 

4569 if not args: 

4570 inner = None 

4571 elif len(args) == 1: 

4572 inner = args[0] 

4573 else: 

4574 raise TypeError(f"expected at most 1 positional argument, got {len(args)}") 

4575 

4576 self.__desc = desc 

4577 self.__completer = completer 

4578 super().__init__(inner) 

4579 

4580 def check_type(self, value: object, /) -> _t.TypeGuard[T]: 

4581 return self._inner.check_type(value) 

4582 

4583 def describe(self) -> str | None: 

4584 return self.__desc or self._inner.describe() 

4585 

4586 def describe_or_def(self) -> str: 

4587 return self.__desc or self._inner.describe_or_def() 

4588 

4589 def describe_many(self) -> str | tuple[str, ...]: 

4590 return self.__desc or self._inner.describe_many() 

4591 

4592 def describe_value(self, value: object, /) -> str: 

4593 return self._inner.describe_value(value) 

4594 

4595 def parse(self, value: str, /) -> T: 

4596 return self._inner.parse(value) 

4597 

4598 def parse_many(self, value: _t.Sequence[str], /) -> T: 

4599 return self._inner.parse_many(value) 

4600 

4601 def parse_config(self, value: object, /) -> T: 

4602 return self._inner.parse_config(value) 

4603 

4604 def options(self) -> _t.Collection[yuio.widget.Option[T]] | None: 

4605 return self._inner.options() 

4606 

4607 def completer(self) -> yuio.complete.Completer | None: 

4608 if self.__completer is not yuio.MISSING: 

4609 return self.__completer # type: ignore 

4610 else: 

4611 return self._inner.completer() 

4612 

4613 def widget( 

4614 self, 

4615 default: object | yuio.Missing, 

4616 input_description: str | None, 

4617 default_description: str | None, 

4618 /, 

4619 ) -> yuio.widget.Widget[T | yuio.Missing]: 

4620 return self._inner.widget(default, input_description, default_description) 

4621 

4622 def to_json_value(self, value: object) -> yuio.json_schema.JsonValue: 

4623 return self._inner.to_json_value(value) 

4624 

4625 

4626class _WidgetResultMapper(yuio.widget.Map[T | yuio.Missing, str]): 

4627 def __init__( 

4628 self, 

4629 parser: Parser[T], 

4630 input_description: str | None, 

4631 default: object | yuio.Missing, 

4632 widget: yuio.widget.Widget[str], 

4633 ): 

4634 self._parser = parser 

4635 self._input_description = input_description 

4636 self._default = default 

4637 super().__init__(widget, self.mapper) 

4638 

4639 def mapper(self, s: str) -> T | yuio.Missing: 

4640 if not s and self._default is not yuio.MISSING: 

4641 return yuio.MISSING 

4642 elif not s: 

4643 raise ParsingError("Input is required") 

4644 else: 

4645 return self._parser.parse(s) 

4646 

4647 @property 

4648 def help_data(self): 

4649 return super().help_data.with_action( 

4650 group="Input Format", 

4651 msg=self._input_description, 

4652 prepend=True, 

4653 prepend_group=True, 

4654 ) 

4655 

4656 

4657def _secret_widget( 

4658 parser: Parser[T], 

4659 default: object | yuio.Missing, 

4660 input_description: str | None, 

4661 default_description: str | None, 

4662 /, 

4663) -> yuio.widget.Widget[T | yuio.Missing]: 

4664 return _WidgetResultMapper( 

4665 parser, 

4666 input_description, 

4667 default, 

4668 ( 

4669 yuio.widget.SecretInput( 

4670 placeholder=default_description or "", 

4671 ) 

4672 ), 

4673 ) 

4674 

4675 

4676_FromTypeHintCallback: _t.TypeAlias = _t.Callable[ 

4677 [type, type | None, tuple[object, ...]], Parser[object] | None 

4678] 

4679 

4680 

4681_FROM_TYPE_HINT_CALLBACKS: list[tuple[_FromTypeHintCallback, bool]] = [] 

4682_FROM_TYPE_HINT_DELIM_SUGGESTIONS: list[str | None] = [ 

4683 None, 

4684 ",", 

4685 "@", 

4686 "/", 

4687 "=", 

4688] 

4689 

4690 

4691class _FromTypeHintDepth(threading.local): 

4692 def __init__(self): 

4693 self.depth: int = 0 

4694 self.uses_delim = False 

4695 

4696 

4697_FROM_TYPE_HINT_DEPTH: _FromTypeHintDepth = _FromTypeHintDepth() 

4698 

4699 

4700@_t.overload 

4701def from_type_hint(ty: type[T], /) -> Parser[T]: ... 

4702@_t.overload 

4703def from_type_hint(ty: object, /) -> Parser[object]: ... 

4704def from_type_hint(ty: _t.Any, /) -> Parser[object]: 

4705 """from_type_hint(ty: type[T], /) -> Parser[T] 

4706 

4707 Create parser from a type hint. 

4708 

4709 :param ty: 

4710 a type hint. 

4711 

4712 This type hint should not contain strings or forward references. Make sure 

4713 they're resolved before passing it to this function. 

4714 :returns: 

4715 a parser instance created from type hint. 

4716 :raises: 

4717 :class:`TypeError` if type hint contains forward references or types 

4718 that don't have associated parsers. 

4719 :example: 

4720 :: 

4721 

4722 >>> from_type_hint(list[int] | None) 

4723 Optional(List(Int)) 

4724 

4725 """ 

4726 

4727 result = _from_type_hint(ty) 

4728 setattr(result, "_Parser__typehint", ty) 

4729 return result 

4730 

4731 

4732def _from_type_hint(ty: _t.Any, /) -> Parser[object]: 

4733 if isinstance(ty, (str, _t.ForwardRef)): 

4734 raise TypeError(f"forward references are not supported here: {ty}") 

4735 

4736 origin = _t.get_origin(ty) 

4737 args = _t.get_args(ty) 

4738 

4739 if origin is _t.Annotated: 

4740 p = from_type_hint(args[0]) 

4741 for arg in args[1:]: 

4742 if isinstance(arg, PartialParser): 

4743 p = arg.wrap(p) 

4744 return p 

4745 

4746 for cb, uses_delim in _FROM_TYPE_HINT_CALLBACKS: 

4747 prev_uses_delim = _FROM_TYPE_HINT_DEPTH.uses_delim 

4748 _FROM_TYPE_HINT_DEPTH.uses_delim = uses_delim 

4749 _FROM_TYPE_HINT_DEPTH.depth += uses_delim 

4750 try: 

4751 p = cb(ty, origin, args) 

4752 if p is not None: 

4753 return p 

4754 finally: 

4755 _FROM_TYPE_HINT_DEPTH.uses_delim = prev_uses_delim 

4756 _FROM_TYPE_HINT_DEPTH.depth -= uses_delim 

4757 

4758 if _t.is_union(origin): 

4759 if is_optional := (type(None) in args): 

4760 args = list(args) 

4761 args.remove(type(None)) 

4762 if len(args) == 1: 

4763 p = from_type_hint(args[0]) 

4764 else: 

4765 p = Union(*[from_type_hint(arg) for arg in args]) 

4766 if is_optional: 

4767 p = Optional(p) 

4768 return p 

4769 else: 

4770 raise TypeError(f"unsupported type {_t.type_repr(ty)}") 

4771 

4772 

4773@_t.overload 

4774def register_type_hint_conversion( 

4775 cb: _FromTypeHintCallback, 

4776 /, 

4777 *, 

4778 uses_delim: bool = False, 

4779) -> _FromTypeHintCallback: ... 

4780@_t.overload 

4781def register_type_hint_conversion( 

4782 *, 

4783 uses_delim: bool = False, 

4784) -> _t.Callable[[_FromTypeHintCallback], _FromTypeHintCallback]: ... 

4785def register_type_hint_conversion( 

4786 cb: _FromTypeHintCallback | None = None, 

4787 /, 

4788 *, 

4789 uses_delim: bool = False, 

4790) -> ( 

4791 _FromTypeHintCallback | _t.Callable[[_FromTypeHintCallback], _FromTypeHintCallback] 

4792): 

4793 """ 

4794 Register a new converter from a type hint to a parser. 

4795 

4796 This function takes a callback that accepts three positional arguments: 

4797 

4798 - a type hint, 

4799 - a type hint's origin (as defined by :func:`typing.get_origin`), 

4800 - a type hint's args (as defined by :func:`typing.get_args`). 

4801 

4802 The callback should return a parser if it can, or :data:`None` otherwise. 

4803 

4804 All registered callbacks are tried in the same order 

4805 as they were registered. 

4806 

4807 If ``uses_delim`` is :data:`True`, callback can use 

4808 :func:`suggest_delim_for_type_hint_conversion`. 

4809 

4810 This function can be used as a decorator. 

4811 

4812 :param cb: 

4813 a function that should inspect a type hint and possibly return a parser. 

4814 :param uses_delim: 

4815 indicates that callback will use 

4816 :func:`suggest_delim_for_type_hint_conversion`. 

4817 :example: 

4818 .. invisible-code-block: python 

4819 

4820 class MyType: ... 

4821 class MyTypeParser(ValueParser[MyType]): 

4822 def __init__(self): super().__init__(MyType) 

4823 def parse(self, value: str, /): ... 

4824 def parse_config(self, value, /): ... 

4825 def to_json_schema(self, ctx, /): ... 

4826 def to_json_value(self, value, /): ... 

4827 

4828 .. code-block:: python 

4829 

4830 @register_type_hint_conversion 

4831 def my_type_conversion(ty, origin, args): 

4832 if ty is MyType: 

4833 return MyTypeParser() 

4834 else: 

4835 return None 

4836 

4837 :: 

4838 

4839 >>> from_type_hint(MyType) 

4840 MyTypeParser 

4841 

4842 .. invisible-code-block: python 

4843 

4844 del _FROM_TYPE_HINT_CALLBACKS[-1] 

4845 

4846 """ 

4847 

4848 def registrar(cb: _FromTypeHintCallback): 

4849 _FROM_TYPE_HINT_CALLBACKS.append((cb, uses_delim)) 

4850 return cb 

4851 

4852 return registrar(cb) if cb is not None else registrar 

4853 

4854 

4855def suggest_delim_for_type_hint_conversion() -> str | None: 

4856 """ 

4857 Suggests a delimiter for use in type hint converters. 

4858 

4859 When creating a parser for a collection of items based on a type hint, 

4860 it is important to use different delimiters for nested collections. 

4861 This function can suggest such a delimiter based on the current type hint's depth. 

4862 

4863 .. invisible-code-block: python 

4864 

4865 class MyCollection(list, _t.Generic[T]): ... 

4866 class MyCollectionParser(CollectionParser[MyCollection[T], T], _t.Generic[T]): 

4867 def __init__(self, inner: Parser[T], /, *, delimiter: _t.Optional[str] = None): 

4868 super().__init__(inner, ty=MyCollection, ctor=MyCollection, delimiter=delimiter) 

4869 def to_json_schema(self, ctx, /): ... 

4870 def to_json_value(self, value, /): ... 

4871 

4872 :raises: 

4873 :class:`RuntimeError` if called from a type converter that 

4874 didn't set ``uses_delim`` to :data:`True`. 

4875 :example: 

4876 .. code-block:: python 

4877 

4878 @register_type_hint_conversion(uses_delim=True) 

4879 def my_collection_conversion(ty, origin, args): 

4880 if origin is MyCollection: 

4881 return MyCollectionParser( 

4882 from_type_hint(args[0]), 

4883 delimiter=suggest_delim_for_type_hint_conversion(), 

4884 ) 

4885 else: 

4886 return None 

4887 

4888 :: 

4889 

4890 >>> parser = from_type_hint(MyCollection[MyCollection[str]]) 

4891 >>> parser 

4892 MyCollectionParser(MyCollectionParser(Str)) 

4893 >>> parser._delimiter is None 

4894 True 

4895 >>> parser._inner._delimiter == "," 

4896 True 

4897 

4898 .. 

4899 >>> del _FROM_TYPE_HINT_CALLBACKS[-1] 

4900 

4901 """ 

4902 

4903 if not _FROM_TYPE_HINT_DEPTH.uses_delim: 

4904 raise RuntimeError( 

4905 "looking up delimiters is not available in this callback; did you forget" 

4906 " to pass `uses_delim=True` when registering this callback?" 

4907 ) 

4908 

4909 depth = _FROM_TYPE_HINT_DEPTH.depth - 1 

4910 if depth < len(_FROM_TYPE_HINT_DELIM_SUGGESTIONS): 

4911 return _FROM_TYPE_HINT_DELIM_SUGGESTIONS[depth] 

4912 else: 

4913 return None 

4914 

4915 

4916register_type_hint_conversion(lambda ty, origin, args: Str() if ty is str else None) 

4917register_type_hint_conversion(lambda ty, origin, args: Int() if ty is int else None) 

4918register_type_hint_conversion(lambda ty, origin, args: Float() if ty is float else None) 

4919register_type_hint_conversion(lambda ty, origin, args: Bool() if ty is bool else None) 

4920register_type_hint_conversion( 

4921 lambda ty, origin, args: ( 

4922 Enum(ty) if isinstance(ty, type) and issubclass(ty, enum.Enum) else None 

4923 ) 

4924) 

4925register_type_hint_conversion( 

4926 lambda ty, origin, args: Decimal() if ty is decimal.Decimal else None 

4927) 

4928register_type_hint_conversion( 

4929 lambda ty, origin, args: Fraction() if ty is fractions.Fraction else None 

4930) 

4931register_type_hint_conversion( 

4932 lambda ty, origin, args: ( 

4933 List( 

4934 from_type_hint(args[0]), delimiter=suggest_delim_for_type_hint_conversion() 

4935 ) 

4936 if origin is list 

4937 else None 

4938 ), 

4939 uses_delim=True, 

4940) 

4941register_type_hint_conversion( 

4942 lambda ty, origin, args: ( 

4943 Set(from_type_hint(args[0]), delimiter=suggest_delim_for_type_hint_conversion()) 

4944 if origin is set 

4945 else None 

4946 ), 

4947 uses_delim=True, 

4948) 

4949register_type_hint_conversion( 

4950 lambda ty, origin, args: ( 

4951 FrozenSet( 

4952 from_type_hint(args[0]), delimiter=suggest_delim_for_type_hint_conversion() 

4953 ) 

4954 if origin is frozenset 

4955 else None 

4956 ), 

4957 uses_delim=True, 

4958) 

4959register_type_hint_conversion( 

4960 lambda ty, origin, args: ( 

4961 Dict( 

4962 from_type_hint(args[0]), 

4963 from_type_hint(args[1]), 

4964 delimiter=suggest_delim_for_type_hint_conversion(), 

4965 ) 

4966 if origin is dict 

4967 else None 

4968 ), 

4969 uses_delim=True, 

4970) 

4971register_type_hint_conversion( 

4972 lambda ty, origin, args: ( 

4973 Tuple( 

4974 *[from_type_hint(arg) for arg in args], 

4975 delimiter=suggest_delim_for_type_hint_conversion(), 

4976 ) 

4977 if origin is tuple and ... not in args 

4978 else None 

4979 ), 

4980 uses_delim=True, 

4981) 

4982register_type_hint_conversion( 

4983 lambda ty, origin, args: Path() if ty is pathlib.Path else None 

4984) 

4985register_type_hint_conversion( 

4986 lambda ty, origin, args: Json() if ty is yuio.json_schema.JsonValue else None 

4987) 

4988register_type_hint_conversion( 

4989 lambda ty, origin, args: DateTime() if ty is datetime.datetime else None 

4990) 

4991register_type_hint_conversion( 

4992 lambda ty, origin, args: Date() if ty is datetime.date else None 

4993) 

4994register_type_hint_conversion( 

4995 lambda ty, origin, args: Time() if ty is datetime.time else None 

4996) 

4997register_type_hint_conversion( 

4998 lambda ty, origin, args: TimeDelta() if ty is datetime.timedelta else None 

4999) 

5000 

5001 

5002@register_type_hint_conversion 

5003def __secret(ty, origin, args): 

5004 if ty is SecretValue: 

5005 raise TypeError("yuio.secret.SecretValue requires type arguments") 

5006 if origin is SecretValue: 

5007 if len(args) == 1: 

5008 return Secret(from_type_hint(args[0])) 

5009 else: 

5010 raise TypeError( 

5011 f"yuio.secret.SecretValue requires 1 type argument, got {len(args)}" 

5012 ) 

5013 return None 

5014 

5015 

5016def _is_optional_parser(parser: Parser[_t.Any] | None, /) -> bool: 

5017 while parser is not None: 

5018 if isinstance(parser, Optional): 

5019 return True 

5020 parser = parser.__wrapped_parser__ 

5021 return False 

5022 

5023 

5024def _is_bool_parser(parser: Parser[_t.Any] | None, /) -> bool: 

5025 while parser is not None: 

5026 if isinstance(parser, Bool): 

5027 return True 

5028 parser = parser.__wrapped_parser__ 

5029 return False