Coverage for typer / _click / types.py: 100%

271 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-11-09 00:15 +0000

1import os 1aebfcdg

2import sys 1aebfcdg

3from collections.abc import Callable, Sequence 1aebfcdg

4from datetime import datetime 1aebfcdg

5from typing import ( 1aebfcdg

6 IO, 

7 TYPE_CHECKING, 

8 Any, 

9 ClassVar, 

10 Literal, 

11 NoReturn, 

12 TypedDict, 

13 TypeGuard, 

14 TypeVar, 

15 Union, 

16 cast, 

17) 

18 

19from ._compat import _get_argv_encoding, open_stream 1aebfcdg

20from .exceptions import BadParameter 1aebfcdg

21from .utils import LazyFile, _escape_control_characters, format_filename, safecall 1aebfcdg

22 

23if TYPE_CHECKING: 1aebfcdg

24 from .core import Context, Parameter 

25 from .shell_completion import CompletionItem 

26 

27ParamTypeValue = TypeVar("ParamTypeValue") 1aebfcdg

28 

29 

30class ParamType: 1aebfcdg

31 """Represents the type of a parameter. Validates and converts values 

32 from the command line or Python into the correct type. 

33 

34 To implement a custom type, subclass and implement at least the 

35 following: 

36 

37 - The `name` class attribute must be set. 

38 - Calling an instance of the type with ``None`` must return 

39 ``None``. This is already implemented by default. 

40 - `convert` must convert string values to the correct type. 

41 - `convert` must accept values that are already the correct 

42 type. 

43 - It must be able to convert a value if the ``ctx`` and ``param`` 

44 arguments are ``None``. This can occur when converting prompt 

45 input. 

46 """ 

47 

48 is_composite: ClassVar[bool] = False 1aebfcdg

49 arity: ClassVar[int] = 1 1aebfcdg

50 name: str 1abcd

51 

52 # if a list of this type is expected and the value is pulled from a 

53 # string environment variable, this is what splits it up. `None` 

54 # means any whitespace. For all parameters the general rule is that 

55 # whitespace splits them up. The exception are paths and files which 

56 # are split by ``os.path.pathsep`` by default (":" on Unix and ";" on 

57 # Windows). 

58 envvar_list_splitter: ClassVar[str | None] = None 1aebfcdg

59 

60 def __call__( 1aebfcdg

61 self, 

62 value: Any, 

63 param: Union["Parameter", None] = None, 

64 ctx: Union["Context", None] = None, 

65 ) -> Any: 

66 if value is not None: 1aebfcdg

67 return self.convert(value, param, ctx) 1aebfcdg

68 

69 def get_metavar(self, param: "Parameter", ctx: "Context") -> str | None: 1aebfcdg

70 """Returns the metavar default for this param if it provides one.""" 

71 pass # pragma: no cover 1aebfcdg

72 

73 def get_missing_message( 1aebfcdg

74 self, param: "Parameter", ctx: Union["Context", None] 

75 ) -> str | None: 

76 """Optionally might return extra information about a missing 

77 parameter. 

78 """ 

79 pass # pragma: no cover 1aebfcdg

80 

81 def convert( 1aebfcdg

82 self, value: Any, param: Union["Parameter", None], ctx: Union["Context", None] 

83 ) -> Any: 

84 pass # pragma: no cover 

85 

86 def split_envvar_value(self, rv: str) -> Sequence[str]: 1aebfcdg

87 """Given a value from an environment variable this splits it up 

88 into small chunks depending on the defined envvar list splitter. 

89 

90 If the splitter is set to `None`, which means that whitespace splits, 

91 then leading and trailing whitespace is ignored. Otherwise, leading 

92 and trailing splitters usually lead to empty items being included. 

93 """ 

94 return (rv or "").split(self.envvar_list_splitter) 1aebfcdg

95 

96 def fail( 1aebfcdg

97 self, 

98 message: str, 

99 param: Union["Parameter", None] = None, 

100 ctx: Union["Context", None] = None, 

101 ) -> NoReturn: 

102 """Helper method to fail with an invalid value message.""" 

103 raise BadParameter(message, ctx=ctx, param=param) 1aebfcdg

104 

105 def shell_complete( 1aebfcdg

106 self, ctx: "Context", param: "Parameter", incomplete: str 

107 ) -> list["CompletionItem"]: 

108 """Return a list of `CompletionItem` objects for the 

109 incomplete value. Most types do not provide completions, but 

110 some do, and this allows custom types to provide custom 

111 completions as well. 

112 """ 

113 return [] 1aebfcdg

114 

115 

116class CompositeParamType(ParamType): 1aebfcdg

117 is_composite = True 1aebfcdg

118 

119 @property 1aebfcdg

120 def arity(self) -> int: # type: ignore 1aebfcdg

121 raise NotImplementedError() # pragma: no cover 

122 

123 

124class FuncParamType(ParamType): 1aebfcdg

125 def __init__(self, func: Callable[[Any], Any]) -> None: 1aebfcdg

126 self.name: str = getattr(func, "__name__", "function") 1aebfcdg

127 self.func = func 1aebfcdg

128 

129 def convert( 1aebfcdg

130 self, value: Any, param: Union["Parameter", None], ctx: Union["Context", None] 

131 ) -> Any: 

132 try: 1aebfcdg

133 return self.func(value) 1aebfcdg

134 except ValueError: 1aebfcdg

135 try: 1aebfcdg

136 value = str(value) 1aebfcdg

137 except UnicodeError: # pragma: no cover 

138 assert isinstance(value, bytes) 

139 value = value.decode("utf-8", "replace") 

140 

141 self.fail(_escape_control_characters(value), param, ctx) 1aebfcdg

142 

143 

144class StringParamType(ParamType): 1aebfcdg

145 name = "str" 1aebfcdg

146 

147 def convert( 1aebfcdg

148 self, value: Any, param: Union["Parameter", None], ctx: Union["Context", None] 

149 ) -> Any: 

150 if isinstance(value, bytes): 1aebfcdg

151 enc = _get_argv_encoding() 1aebfcdg

152 try: 1aebfcdg

153 value = value.decode(enc) 1aebfcdg

154 except UnicodeError: 1aebfcdg

155 fs_enc = sys.getfilesystemencoding() 1aebfcdg

156 if fs_enc != enc: 1aebfcdg

157 try: 1aebfcdg

158 value = value.decode(fs_enc) 1aebfcdg

159 except UnicodeError: 1aebfcdg

160 value = value.decode("utf-8", "replace") 1aebfcdg

161 else: 

162 value = value.decode("utf-8", "replace") 1aebfcdg

163 return value 1aebfcdg

164 return str(value) 1aebfcdg

165 

166 def __repr__(self) -> str: 1aebfcdg

167 return "STRING" 1aebfcdg

168 

169 

170class DateTime(ParamType): 1aebfcdg

171 """The DateTime type converts date strings into `datetime` objects. 

172 

173 The format strings which are checked are configurable, but default to some 

174 common (non-timezone aware) ISO 8601 formats. 

175 

176 When specifying *DateTime* formats, you should only pass a list or a tuple. 

177 Other iterables, like generators, may lead to surprising results. 

178 

179 The format strings are processed using ``datetime.strptime``, and this 

180 consequently defines the format strings which are allowed. 

181 

182 Parsing is tried using each format, in order, and the first format which 

183 parses successfully is used. 

184 """ 

185 

186 name = "datetime" 1aebfcdg

187 

188 def __init__(self, formats: Sequence[str] | None = None): 1aebfcdg

189 self.formats: Sequence[str] = formats or [ 1aebfcdg

190 "%Y-%m-%d", 

191 "%Y-%m-%dT%H:%M:%S", 

192 "%Y-%m-%d %H:%M:%S", 

193 ] 

194 

195 def get_metavar(self, param: "Parameter", ctx: "Context") -> str | None: 1aebfcdg

196 return f"<{'|'.join(self.formats)}>" 1aebfcdg

197 

198 def _try_to_convert_date(self, value: Any, format: str) -> datetime | None: 1aebfcdg

199 try: 1aebfcdg

200 return datetime.strptime(value, format) 1aebfcdg

201 except ValueError: 1aebfcdg

202 return None 1aebfcdg

203 

204 def convert( 1aebfcdg

205 self, value: Any, param: Union["Parameter", None], ctx: Union["Context", None] 

206 ) -> Any: 

207 if isinstance(value, datetime): 1aebfcdg

208 return value 1aebfcdg

209 

210 for format in self.formats: 1aebfcdg

211 converted = self._try_to_convert_date(value, format) 1aebfcdg

212 

213 if converted is not None: 1aebfcdg

214 return converted 1aebfcdg

215 

216 formats_str = ", ".join(map(repr, self.formats)) 1aebfcdg

217 self.fail( 1aebfcdg

218 f"{value!r} does not match the formats {formats_str}.", 

219 param, 

220 ctx, 

221 ) 

222 

223 def __repr__(self) -> str: 1aebfcdg

224 return "DateTime" 1aebfcdg

225 

226 

227class _NumberParamTypeBase(ParamType): 1aebfcdg

228 _number_class: ClassVar[type[Any]] 1abcd

229 

230 def convert( 1aebfcdg

231 self, value: Any, param: Union["Parameter", None], ctx: Union["Context", None] 

232 ) -> Any: 

233 try: 1aebfcdg

234 return self._number_class(value) 1aebfcdg

235 except ValueError: 1aebfcdg

236 self.fail( 1aebfcdg

237 f"{value!r} is not a valid {self.name}.", 

238 param, 

239 ctx, 

240 ) 

241 

242 

243class _NumberRangeBase(_NumberParamTypeBase): 1aebfcdg

244 def __init__( 1aebfcdg

245 self, 

246 min: float | None = None, 

247 max: float | None = None, 

248 min_open: bool = False, 

249 max_open: bool = False, 

250 clamp: bool = False, 

251 ) -> None: 

252 self.min = min 1aebfcdg

253 self.max = max 1aebfcdg

254 self.min_open = min_open 1aebfcdg

255 self.max_open = max_open 1aebfcdg

256 self.clamp = clamp 1aebfcdg

257 

258 def convert( 1aebfcdg

259 self, value: Any, param: Union["Parameter", None], ctx: Union["Context", None] 

260 ) -> Any: 

261 import operator 1aebfcdg

262 

263 rv = super().convert(value, param, ctx) 1aebfcdg

264 lt_min: bool = self.min is not None and ( 1aebfcdg

265 operator.le if self.min_open else operator.lt 

266 )(rv, self.min) 

267 gt_max: bool = self.max is not None and ( 1aebfcdg

268 operator.ge if self.max_open else operator.gt 

269 )(rv, self.max) 

270 

271 if self.clamp: 1aebfcdg

272 if lt_min: 1aebfcdg

273 return self._clamp(self.min, 1, self.min_open) # type: ignore[arg-type] 1aebfcdg

274 

275 if gt_max: 1aebfcdg

276 return self._clamp(self.max, -1, self.max_open) # type: ignore[arg-type] 1aebfcdg

277 

278 if lt_min or gt_max: 1aebfcdg

279 self.fail( 1aebfcdg

280 f"{rv} is not in the range {self._describe_range()}.", 

281 param, 

282 ctx, 

283 ) 

284 

285 return rv 1aebfcdg

286 

287 def _clamp(self, bound: float, dir: Literal[1, -1], open: bool) -> float: 1aebfcdg

288 """Find the valid value to clamp to bound in the given 

289 direction. 

290 """ 

291 raise NotImplementedError # pragma: no cover 

292 

293 def _describe_range(self) -> str: 1aebfcdg

294 """Describe the range for use in help text.""" 

295 if self.min is None: 1aebfcdg

296 op = "<" if self.max_open else "<=" 1aebfcdg

297 return f"x{op}{self.max}" 1aebfcdg

298 

299 if self.max is None: 1aebfcdg

300 op = ">" if self.min_open else ">=" 1aebfcdg

301 return f"x{op}{self.min}" 1aebfcdg

302 

303 lop = "<" if self.min_open else "<=" 1aebfcdg

304 rop = "<" if self.max_open else "<=" 1aebfcdg

305 return f"{self.min}{lop}x{rop}{self.max}" 1aebfcdg

306 

307 def __repr__(self) -> str: 1aebfcdg

308 clamp = " clamped" if self.clamp else "" 1aebfcdg

309 return f"<{type(self).__name__} {self._describe_range()}{clamp}>" 1aebfcdg

310 

311 

312class IntParamType(_NumberParamTypeBase): 1aebfcdg

313 name = "int" 1aebfcdg

314 _number_class = int 1aebfcdg

315 

316 def __repr__(self) -> str: 1aebfcdg

317 return "INT" 1aebfcdg

318 

319 

320class IntRange(_NumberRangeBase, IntParamType): 1aebfcdg

321 """Restrict an `INT` value to a range of accepted values. See 

322 

323 If ``min`` or ``max`` are not passed, any value is accepted in that 

324 direction. If ``min_open`` or ``max_open`` are enabled, the 

325 corresponding boundary is not included in the range. 

326 

327 If ``clamp`` is enabled, a value outside the range is clamped to the 

328 boundary instead of failing. 

329 """ 

330 

331 name = "int range" 1aebfcdg

332 

333 def _clamp( # type: ignore 1aebfcdg

334 self, bound: int, dir: Literal[1, -1], open: bool 

335 ) -> int: 

336 if not open: 1aebfcdg

337 return bound 1aebfcdg

338 

339 return bound + dir 1aebfcdg

340 

341 

342class FloatParamType(_NumberParamTypeBase): 1aebfcdg

343 name = "float" 1aebfcdg

344 _number_class = float 1aebfcdg

345 

346 def __repr__(self) -> str: 1aebfcdg

347 return "FLOAT" 1aebfcdg

348 

349 

350class FloatRange(_NumberRangeBase, FloatParamType): 1aebfcdg

351 """Restrict a `FLOAT` value to a range of accepted 

352 values. See `ranges`. 

353 

354 If ``min`` or ``max`` are not passed, any value is accepted in that 

355 direction. If ``min_open`` or ``max_open`` are enabled, the 

356 corresponding boundary is not included in the range. 

357 

358 If ``clamp`` is enabled, a value outside the range is clamped to the 

359 boundary instead of failing. This is not supported if either 

360 boundary is marked ``open``. 

361 """ 

362 

363 name = "float range" 1aebfcdg

364 

365 def __init__( 1aebfcdg

366 self, 

367 min: float | None = None, 

368 max: float | None = None, 

369 min_open: bool = False, 

370 max_open: bool = False, 

371 clamp: bool = False, 

372 ) -> None: 

373 super().__init__( 1aebfcdg

374 min=min, max=max, min_open=min_open, max_open=max_open, clamp=clamp 

375 ) 

376 

377 if (min_open or max_open) and clamp: 1aebfcdg

378 raise TypeError("Clamping is not supported for open bounds.") 1aebfcdg

379 

380 def _clamp(self, bound: float, dir: Literal[1, -1], open: bool) -> float: 1aebfcdg

381 if not open: 1aebfcdg

382 return bound 1aebfcdg

383 

384 # Could use math.nextafter here, but clamping an 

385 # open float range doesn't seem to be particularly useful. It's 

386 # left up to the user to write a callback to do it if needed. 

387 raise RuntimeError( 

388 "Clamping is not supported for open bounds." 

389 ) # pragma: no cover 

390 

391 

392class BoolParamType(ParamType): 1aebfcdg

393 name = "boolean" 1aebfcdg

394 

395 bool_states: dict[str, bool] = { 1aebfcdg

396 "1": True, 

397 "0": False, 

398 "yes": True, 

399 "no": False, 

400 "true": True, 

401 "false": False, 

402 "on": True, 

403 "off": False, 

404 "t": True, 

405 "f": False, 

406 "y": True, 

407 "n": False, 

408 # Absence of value is considered False. 

409 "": False, 

410 } 

411 """A mapping of string values to boolean states. 1aebfcdg

412 

413 Mapping is inspired by `configparser.ConfigParser.BOOLEAN_STATES` 

414 and extends it. 

415 """ 

416 

417 @staticmethod 1aebfcdg

418 def str_to_bool(value: str | bool) -> bool | None: 1aebfcdg

419 """Convert a string to a boolean value. 

420 

421 If the value is already a boolean, it is returned as-is. If the value is a 

422 string, it is stripped of whitespaces and lower-cased, then checked against 

423 the known boolean states pre-defined in the `BoolParamType.bool_states` mapping 

424 above. 

425 

426 Returns `None` if the value does not match any known boolean state. 

427 """ 

428 if isinstance(value, bool): 1aebfcdg

429 return value 1aebfcdg

430 return BoolParamType.bool_states.get(value.strip().lower()) 1aebfcdg

431 

432 def convert( 1aebfcdg

433 self, value: Any, param: Union["Parameter", None], ctx: Union["Context", None] 

434 ) -> bool: 

435 normalized = self.str_to_bool(value) 1aebfcdg

436 if normalized is None: 1aebfcdg

437 states = ", ".join(sorted(self.bool_states)) 1aebfcdg

438 self.fail( 1aebfcdg

439 f"{value!r} is not a valid boolean. Recognized values: {states}", 

440 param, 

441 ctx, 

442 ) 

443 return normalized 1aebfcdg

444 

445 def __repr__(self) -> str: 1aebfcdg

446 return "BOOL" 1aebfcdg

447 

448 

449class UUIDParameterType(ParamType): 1aebfcdg

450 name = "uuid" 1aebfcdg

451 

452 def convert( 1aebfcdg

453 self, value: Any, param: Union["Parameter", None], ctx: Union["Context", None] 

454 ) -> Any: 

455 import uuid 1aebfcdg

456 

457 if isinstance(value, uuid.UUID): 1aebfcdg

458 return value 1aebfcdg

459 

460 value = value.strip() 1aebfcdg

461 

462 try: 1aebfcdg

463 return uuid.UUID(value) 1aebfcdg

464 except ValueError: 1aebfcdg

465 self.fail(f"{value!r} is not a valid UUID.", param, ctx) 1aebfcdg

466 

467 def __repr__(self) -> str: 1aebfcdg

468 return "UUID" 1aebfcdg

469 

470 

471class File(ParamType): 1aebfcdg

472 """Declares a parameter to be a file for reading or writing. The file 

473 is automatically closed once the context tears down (after the command 

474 finished working). 

475 

476 Files can be opened for reading or writing. The special value ``-`` 

477 indicates stdin or stdout depending on the mode. 

478 

479 By default, the file is opened for reading text data, but it can also be 

480 opened in binary mode or for writing. The encoding parameter can be used 

481 to force a specific encoding. 

482 

483 The `lazy` flag controls if the file should be opened immediately or upon 

484 first IO. The default is to be non-lazy for standard input and output 

485 streams as well as files opened for reading, `lazy` otherwise. When opening a 

486 file lazily for reading, it is still opened temporarily for validation, but 

487 will not be held open until first IO. lazy is mainly useful when opening 

488 for writing to avoid creating the file until it is needed. 

489 

490 Files can also be opened atomically in which case all writes go into a 

491 separate file in the same folder and upon completion the file will 

492 be moved over to the original location. This is useful if a file 

493 regularly read by other users is modified. 

494 """ 

495 

496 name = "filename" 1aebfcdg

497 envvar_list_splitter: ClassVar[str] = os.path.pathsep 1aebfcdg

498 

499 def __init__( 1aebfcdg

500 self, 

501 mode: str = "r", 

502 encoding: str | None = None, 

503 errors: str | None = "strict", 

504 lazy: bool | None = None, 

505 atomic: bool = False, 

506 ) -> None: 

507 self.mode = mode 1aebfcdg

508 self.encoding = encoding 1aebfcdg

509 self.errors = errors 1aebfcdg

510 self.lazy = lazy 1aebfcdg

511 self.atomic = atomic 1aebfcdg

512 

513 def resolve_lazy_flag(self, value: str | os.PathLike[str]) -> bool: 1aebfcdg

514 if self.lazy is not None: 1aebfcdg

515 return self.lazy 1aebfcdg

516 if os.fspath(value) == "-": 1aebfcdg

517 return False 1aebfcdg

518 elif "w" in self.mode: 1aebfcdg

519 return True 1aebfcdg

520 return False 1aebfcdg

521 

522 def convert( 1aebfcdg

523 self, 

524 value: str | os.PathLike[str] | IO[Any], 

525 param: Union["Parameter", None], 

526 ctx: Union["Context", None], 

527 ) -> IO[Any]: 

528 if _is_file_like(value): 1aebfcdg

529 return value 1aebfcdg

530 

531 value = cast("str | os.PathLike[str]", value) 1aebfcdg

532 

533 try: 1aebfcdg

534 lazy = self.resolve_lazy_flag(value) 1aebfcdg

535 

536 if lazy: 1aebfcdg

537 lf = LazyFile( 1aebfcdg

538 value, self.mode, self.encoding, self.errors, atomic=self.atomic 

539 ) 

540 

541 if ctx is not None: 1aebfcdg

542 ctx.call_on_close(lf.close_intelligently) 1aebfcdg

543 

544 return cast("IO[Any]", lf) 1aebfcdg

545 

546 f, should_close = open_stream( 1aebfcdg

547 value, self.mode, self.encoding, self.errors, atomic=self.atomic 

548 ) 

549 

550 # If a context is provided, we automatically close the file 

551 # at the end of the context execution (or flush out). If a 

552 # context does not exist, it's the caller's responsibility to 

553 # properly close the file. This for instance happens when the 

554 # type is used with prompts. 

555 if ctx is not None: 1aebfcdg

556 if should_close: 1aebfcdg

557 ctx.call_on_close(safecall(f.close)) 1aebfcdg

558 else: 

559 ctx.call_on_close(safecall(f.flush)) 1aebfcdg

560 

561 return f 1aebfcdg

562 except OSError as e: 1aebfcdg

563 filename = _escape_control_characters(format_filename(value)) 1aebfcdg

564 self.fail(f"'{filename}': {e.strerror}", param, ctx) 1aebfcdg

565 

566 def shell_complete( 1aebfcdg

567 self, ctx: "Context", param: "Parameter", incomplete: str 

568 ) -> list["CompletionItem"]: 

569 """Return a special completion marker that tells the completion 

570 system to use the shell to provide file path completions. 

571 """ 

572 from .shell_completion import CompletionItem 1aebfcdg

573 

574 return [CompletionItem(incomplete, type="file")] 1aebfcdg

575 

576 

577def _is_file_like(value: Any) -> TypeGuard[IO[Any]]: 1aebfcdg

578 return hasattr(value, "read") or hasattr(value, "write") 1aebfcdg

579 

580 

581class Tuple(CompositeParamType): 1aebfcdg

582 """The default behavior of Click is to apply a type on a value directly. 

583 This works well in most cases, except for when `nargs` is set to a fixed 

584 count and different types should be used for different items. In this 

585 case the `Tuple` type can be used. This type can only be used 

586 if `nargs` is set to a fixed number. 

587 

588 For more information see `tuple-type`. 

589 

590 This can be selected by using a Python tuple literal as a type. 

591 """ 

592 

593 def __init__(self, types: Sequence[type[Any] | ParamType]) -> None: 1aebfcdg

594 self.types: Sequence[ParamType] = [convert_type(ty) for ty in types] 1aebfcdg

595 

596 @property 1aebfcdg

597 def name(self) -> str: # type: ignore[override] 1aebfcdg

598 return f"<{' '.join(ty.name for ty in self.types)}>" 1aebfcdg

599 

600 @property 1aebfcdg

601 def arity(self) -> int: # type: ignore 1aebfcdg

602 return len(self.types) 1aebfcdg

603 

604 def convert( 1aebfcdg

605 self, value: Any, param: Union["Parameter", None], ctx: Union["Context", None] 

606 ) -> Any: 

607 len_type = len(self.types) 1aebfcdg

608 len_value = len(value) 1aebfcdg

609 

610 if len_value != len_type: 1aebfcdg

611 self.fail( 1aebfcdg

612 f"{len_type} values are required, but {len_value} given.", 

613 param=param, 

614 ctx=ctx, 

615 ) 

616 

617 return tuple( 1aebfcdg

618 ty(x, param, ctx) for ty, x in zip(self.types, value, strict=False) 

619 ) 

620 

621 

622def convert_type(ty: Any | None, default: Any | None = None) -> ParamType: 1aebfcdg

623 """Find the most appropriate `ParamType` for the given Python 

624 type. If the type isn't provided, it can be inferred from a default 

625 value. 

626 """ 

627 guessed_type = False 1aebfcdg

628 

629 if ty is None and default is not None: 1aebfcdg

630 if isinstance(default, (tuple, list)): 1aebfcdg

631 # If the default is empty, ty will remain None and will 

632 # return STRING. 

633 if default: 1aebfcdg

634 item = default[0] 1aebfcdg

635 

636 # A tuple of tuples needs to detect the inner types. 

637 # Can't call convert recursively because that would 

638 # incorrectly unwind the tuple to a single type. 

639 if isinstance(item, (tuple, list)): 1aebfcdg

640 ty = tuple(map(type, item)) 1aebfcdg

641 else: 

642 ty = type(item) 1aebfcdg

643 else: 

644 ty = type(default) 1aebfcdg

645 

646 guessed_type = True 1aebfcdg

647 

648 if isinstance(ty, tuple): 1aebfcdg

649 return Tuple(ty) 1aebfcdg

650 

651 if isinstance(ty, ParamType): 1aebfcdg

652 return ty 1aebfcdg

653 

654 if ty is str or ty is None: 1aebfcdg

655 return STRING 1aebfcdg

656 

657 if ty is int: 1aebfcdg

658 return INT 1aebfcdg

659 

660 if ty is float: 1aebfcdg

661 return FLOAT 1aebfcdg

662 

663 if ty is bool: 1aebfcdg

664 return BOOL 1aebfcdg

665 

666 if guessed_type: 1aebfcdg

667 return STRING 1aebfcdg

668 

669 return FuncParamType(ty) 1aebfcdg

670 

671 

672# A unicode string parameter type which is the implicit default. This 

673# can also be selected by using ``str`` as type. 

674STRING = StringParamType() 1aebfcdg

675 

676# An integer parameter. This can also be selected by using ``int`` as 

677# type. 

678INT = IntParamType() 1aebfcdg

679 

680# A floating point value parameter. This can also be selected by using 

681# ``float`` as type. 

682FLOAT = FloatParamType() 1aebfcdg

683 

684# A boolean parameter. This is the default for boolean flags. This can 

685# also be selected by using ``bool`` as a type. 

686BOOL = BoolParamType() 1aebfcdg

687 

688# A UUID parameter. 

689UUID = UUIDParameterType() 1aebfcdg

690 

691 

692class OptionHelpExtra(TypedDict, total=False): 1aebfcdg

693 envvars: tuple[str, ...] 1abcd

694 default: str 1abcd

695 range: str 1abcd

696 required: str 1abcd