Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/click/utils.py: 26%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

248 statements  

1from __future__ import annotations 

2 

3import collections.abc as cabc 

4import os 

5import re 

6import sys 

7import typing as t 

8from functools import update_wrapper 

9from gettext import gettext as _ 

10from types import ModuleType 

11from types import TracebackType 

12 

13from ._compat import _default_text_stderr 

14from ._compat import _default_text_stdout 

15from ._compat import _find_binary_writer 

16from ._compat import auto_wrap_for_ansi 

17from ._compat import binary_streams 

18from ._compat import open_stream 

19from ._compat import should_strip_ansi 

20from ._compat import strip_ansi 

21from ._compat import text_streams 

22from ._compat import WIN 

23from .globals import resolve_color_default 

24 

25if t.TYPE_CHECKING: 

26 import typing_extensions as te 

27 

28 P = te.ParamSpec("P") 

29 

30R = t.TypeVar("R") 

31 

32 

33def _posixify(name: str) -> str: 

34 return "-".join(name.split()).lower() 

35 

36 

37def safecall(func: t.Callable[P, R]) -> t.Callable[P, R | None]: 

38 """Wraps a function so that it swallows exceptions.""" 

39 

40 def wrapper(*args: P.args, **kwargs: P.kwargs) -> R | None: 

41 try: 

42 return func(*args, **kwargs) 

43 except Exception: 

44 pass 

45 return None 

46 

47 return update_wrapper(wrapper, func) 

48 

49 

50def make_str(value: t.Any) -> str: 

51 """Converts a value into a valid string.""" 

52 if isinstance(value, bytes): 

53 try: 

54 return value.decode(sys.getfilesystemencoding()) 

55 except UnicodeError: 

56 return value.decode("utf-8", "replace") 

57 return str(value) 

58 

59 

60def make_default_short_help(help: str, max_length: int = 45) -> str: 

61 """Returns a condensed version of help string. 

62 

63 :meta private: 

64 """ 

65 # Consider only the first paragraph. 

66 paragraph_end = help.find("\n\n") 

67 

68 if paragraph_end != -1: 

69 help = help[:paragraph_end] 

70 

71 # Collapse newlines, tabs, and spaces. 

72 words = help.split() 

73 

74 if not words: 

75 return "" 

76 

77 # The first paragraph started with a "no rewrap" marker, ignore it. 

78 if words[0] == "\b": 

79 words = words[1:] 

80 

81 total_length = 0 

82 last_index = len(words) - 1 

83 

84 for i, word in enumerate(words): 

85 total_length += len(word) + (i > 0) 

86 

87 if total_length > max_length: # too long, truncate 

88 break 

89 

90 if word[-1] == ".": # sentence end, truncate without "..." 

91 return " ".join(words[: i + 1]) 

92 

93 if total_length == max_length and i != last_index: 

94 break # not at sentence end, truncate with "..." 

95 else: 

96 return " ".join(words) # no truncation needed 

97 

98 # Account for the length of the suffix. 

99 total_length += len("...") 

100 

101 # remove words until the length is short enough 

102 while i > 0: 

103 total_length -= len(words[i]) + (i > 0) 

104 

105 if total_length <= max_length: 

106 break 

107 

108 i -= 1 

109 

110 return " ".join(words[:i]) + "..." 

111 

112 

113class LazyFile: 

114 """A lazy file works like a regular file but it does not fully open 

115 the file but it does perform some basic checks early to see if the 

116 filename parameter does make sense. This is useful for safely opening 

117 files for writing. 

118 """ 

119 

120 name: str 

121 mode: str 

122 encoding: str | None 

123 errors: str | None 

124 atomic: bool 

125 _f: t.IO[t.Any] | None 

126 should_close: bool 

127 

128 def __init__( 

129 self, 

130 filename: str | os.PathLike[str], 

131 mode: str = "r", 

132 encoding: str | None = None, 

133 errors: str | None = "strict", 

134 atomic: bool = False, 

135 ) -> None: 

136 self.name = os.fspath(filename) 

137 self.mode = mode 

138 self.encoding = encoding 

139 self.errors = errors 

140 self.atomic = atomic 

141 

142 if self.name == "-": 

143 self._f, self.should_close = open_stream(filename, mode, encoding, errors) 

144 else: 

145 if "r" in mode: 

146 # Open and close the file in case we're opening it for 

147 # reading so that we can catch at least some errors in 

148 # some cases early. 

149 open(filename, mode).close() 

150 self._f = None 

151 self.should_close = True 

152 

153 def __getattr__(self, name: str) -> t.Any: 

154 return getattr(self.open(), name) 

155 

156 def __repr__(self) -> str: 

157 if self._f is not None: 

158 return repr(self._f) 

159 return f"<unopened file '{format_filename(self.name)}' {self.mode}>" 

160 

161 def open(self) -> t.IO[t.Any]: 

162 """Opens the file if it's not yet open. This call might fail with 

163 a :exc:`FileError`. Not handling this error will produce an error 

164 that Click shows. 

165 """ 

166 if self._f is not None: 

167 return self._f 

168 try: 

169 rv, self.should_close = open_stream( 

170 self.name, self.mode, self.encoding, self.errors, atomic=self.atomic 

171 ) 

172 except OSError as e: 

173 from .exceptions import FileError 

174 

175 raise FileError(self.name, hint=e.strerror) from e 

176 self._f = rv 

177 return rv 

178 

179 def close(self) -> None: 

180 """Closes the underlying file, no matter what.""" 

181 if self._f is not None: 

182 self._f.close() 

183 

184 def close_intelligently(self) -> None: 

185 """This function only closes the file if it was opened by the lazy 

186 file wrapper. For instance this will never close stdin. 

187 """ 

188 if self.should_close: 

189 self.close() 

190 

191 def __enter__(self) -> LazyFile: 

192 return self 

193 

194 def __exit__( 

195 self, 

196 exc_type: type[BaseException] | None, 

197 exc_value: BaseException | None, 

198 tb: TracebackType | None, 

199 ) -> None: 

200 self.close_intelligently() 

201 

202 def __iter__(self) -> cabc.Iterator[t.AnyStr]: 

203 self.open() 

204 return iter(self._f) # type: ignore 

205 

206 

207class KeepOpenFile: 

208 _file: t.IO[t.Any] 

209 

210 def __init__(self, file: t.IO[t.Any]) -> None: 

211 self._file = file 

212 

213 def __getattr__(self, name: str) -> t.Any: 

214 return getattr(self._file, name) 

215 

216 def __enter__(self) -> KeepOpenFile: 

217 return self 

218 

219 def __exit__( 

220 self, 

221 exc_type: type[BaseException] | None, 

222 exc_value: BaseException | None, 

223 tb: TracebackType | None, 

224 ) -> None: 

225 pass 

226 

227 def __repr__(self) -> str: 

228 return repr(self._file) 

229 

230 def __iter__(self) -> cabc.Iterator[t.AnyStr]: 

231 return iter(self._file) 

232 

233 

234def echo( 

235 message: object = None, 

236 file: t.IO[t.Any] | None = None, 

237 nl: bool = True, 

238 err: bool = False, 

239 color: bool | None = None, 

240) -> None: 

241 """Print a message and newline to stdout or a file. This should be 

242 used instead of :func:`print` because it provides better support 

243 for different data, files, and environments. 

244 

245 Compared to :func:`print`, this does the following: 

246 

247 - Ensures that the output encoding is not misconfigured on Linux. 

248 - Supports Unicode in the Windows console. 

249 - Supports writing to binary outputs, and supports writing bytes 

250 to text outputs. 

251 - Supports colors and styles on Windows. 

252 - Removes ANSI color and style codes if the output does not look 

253 like an interactive terminal. 

254 - Always flushes the output. 

255 

256 :param message: The string or bytes to output. Other objects are 

257 converted to strings. 

258 :param file: The file to write to. Defaults to ``stdout``. 

259 :param err: Write to ``stderr`` instead of ``stdout``. 

260 :param nl: Print a newline after the message. Enabled by default. 

261 :param color: Force showing or hiding colors and other styles. By 

262 default Click will remove color if the output does not look like 

263 an interactive terminal. 

264 

265 .. versionchanged:: 6.0 

266 Support Unicode output on the Windows console. Click does not 

267 modify ``sys.stdout``, so ``sys.stdout.write()`` and ``print()`` 

268 will still not support Unicode. 

269 

270 .. versionchanged:: 4.0 

271 Added the ``color`` parameter. 

272 

273 .. versionadded:: 3.0 

274 Added the ``err`` parameter. 

275 

276 .. versionchanged:: 2.0 

277 Support colors on Windows if colorama is installed. 

278 """ 

279 if file is None: 

280 if err: 

281 file = _default_text_stderr() 

282 else: 

283 file = _default_text_stdout() 

284 

285 # There are no standard streams attached to write to. For example, 

286 # pythonw on Windows. 

287 if file is None: 

288 return 

289 

290 match message: 

291 case str() | bytes() | bytearray(): 

292 out = message 

293 case None: 

294 out = "" 

295 case _: 

296 out = str(message) 

297 

298 if nl: 

299 if isinstance(out, str): 

300 out += "\n" 

301 else: 

302 out += b"\n" 

303 

304 if not out: 

305 file.flush() 

306 return 

307 

308 # If there is a message and the value looks like bytes, we manually 

309 # need to find the binary stream and write the message in there. 

310 # This is done separately so that most stream types will work as you 

311 # would expect. Eg: you can write to StringIO for other cases. 

312 if isinstance(out, (bytes, bytearray)): 

313 binary_file = _find_binary_writer(file) 

314 if binary_file is not None: 

315 file.flush() 

316 binary_file.write(out) 

317 binary_file.flush() 

318 return 

319 

320 # ANSI style code support. For no message or bytes, nothing happens. 

321 # When outputting to a file instead of a terminal, strip codes. 

322 else: 

323 color = resolve_color_default(color) 

324 

325 if should_strip_ansi(file, color): 

326 out = strip_ansi(out) 

327 elif WIN: 

328 if auto_wrap_for_ansi is not None: 

329 file = auto_wrap_for_ansi(file, color) # type: ignore 

330 elif not color: 

331 out = strip_ansi(out) 

332 

333 file.write(out) # type: ignore 

334 file.flush() 

335 

336 

337def get_binary_stream(name: t.Literal["stdin", "stdout", "stderr"]) -> t.BinaryIO: 

338 """Returns a system stream for byte processing. 

339 

340 :param name: the name of the stream to open. Valid names are ``'stdin'``, 

341 ``'stdout'`` and ``'stderr'`` 

342 """ 

343 opener = binary_streams.get(name) 

344 if opener is None: 

345 raise TypeError(_("Unknown standard stream '{name}'").format(name=name)) 

346 return opener() 

347 

348 

349def get_text_stream( 

350 name: t.Literal["stdin", "stdout", "stderr"], 

351 encoding: str | None = None, 

352 errors: str | None = "strict", 

353) -> t.TextIO: 

354 """Returns a system stream for text processing. This usually returns 

355 a wrapped stream around a binary stream returned from 

356 :func:`get_binary_stream` but it also can take shortcuts for already 

357 correctly configured streams. 

358 

359 :param name: the name of the stream to open. Valid names are ``'stdin'``, 

360 ``'stdout'`` and ``'stderr'`` 

361 :param encoding: overrides the detected default encoding. 

362 :param errors: overrides the default error mode. 

363 """ 

364 opener = text_streams.get(name) 

365 if opener is None: 

366 raise TypeError(_("Unknown standard stream '{name}'").format(name=name)) 

367 return opener(encoding, errors) 

368 

369 

370def open_file( 

371 filename: str | os.PathLike[str], 

372 mode: str = "r", 

373 encoding: str | None = None, 

374 errors: str | None = "strict", 

375 lazy: bool = False, 

376 atomic: bool = False, 

377) -> t.IO[t.Any]: 

378 """Open a file, with extra behavior to handle ``'-'`` to indicate 

379 a standard stream, lazy open on write, and atomic write. Similar to 

380 the behavior of the :class:`~click.File` param type. 

381 

382 If ``'-'`` is given to open ``stdout`` or ``stdin``, the stream is 

383 wrapped so that using it in a context manager will not close it. 

384 This makes it possible to use the function without accidentally 

385 closing a standard stream: 

386 

387 .. code-block:: python 

388 

389 with open_file(filename) as f: 

390 ... 

391 

392 :param filename: The name or Path of the file to open, or ``'-'`` for 

393 ``stdin``/``stdout``. 

394 :param mode: The mode in which to open the file. 

395 :param encoding: The encoding to decode or encode a file opened in 

396 text mode. 

397 :param errors: The error handling mode. 

398 :param lazy: Wait to open the file until it is accessed. For read 

399 mode, the file is temporarily opened to raise access errors 

400 early, then closed until it is read again. 

401 :param atomic: Write to a temporary file and replace the given file 

402 on close. 

403 

404 .. versionadded:: 3.0 

405 """ 

406 if lazy: 

407 return t.cast( 

408 "t.IO[t.Any]", LazyFile(filename, mode, encoding, errors, atomic=atomic) 

409 ) 

410 

411 f, should_close = open_stream(filename, mode, encoding, errors, atomic=atomic) 

412 

413 if not should_close: 

414 f = t.cast("t.IO[t.Any]", KeepOpenFile(f)) 

415 

416 return f 

417 

418 

419def format_filename( 

420 filename: str | bytes | os.PathLike[str] | os.PathLike[bytes], 

421 shorten: bool = False, 

422) -> str: 

423 """Format a filename as a string for display. Ensures the filename can be 

424 displayed by replacing any invalid bytes or surrogate escapes in the name 

425 with the replacement character ``�``. 

426 

427 Invalid bytes or surrogate escapes will raise an error when written to a 

428 stream with ``errors="strict"``. This will typically happen with ``stdout`` 

429 when the locale is something like ``en_GB.UTF-8``. 

430 

431 Many scenarios *are* safe to write surrogates though, due to PEP 538 and 

432 PEP 540, including: 

433 

434 - Writing to ``stderr``, which uses ``errors="backslashreplace"``. 

435 - The system has ``LANG=C.UTF-8``, ``C``, or ``POSIX``. Python opens 

436 stdout and stderr with ``errors="surrogateescape"``. 

437 - None of ``LANG/LC_*`` are set. Python assumes ``LANG=C.UTF-8``. 

438 - Python is started in UTF-8 mode with ``PYTHONUTF8=1`` or ``-X utf8``. 

439 Python opens stdout and stderr with ``errors="surrogateescape"``. 

440 

441 :param filename: formats a filename for UI display. This will also convert 

442 the filename into unicode without failing. 

443 :param shorten: this optionally shortens the filename to strip of the 

444 path that leads up to it. 

445 """ 

446 if shorten: 

447 filename = os.path.basename(filename) 

448 else: 

449 filename = os.fspath(filename) 

450 

451 if isinstance(filename, bytes): 

452 filename = filename.decode(sys.getfilesystemencoding(), "replace") 

453 else: 

454 filename = filename.encode("utf-8", "surrogateescape").decode( 

455 "utf-8", "replace" 

456 ) 

457 

458 return filename 

459 

460 

461def get_app_dir(app_name: str, roaming: bool = True, force_posix: bool = False) -> str: 

462 r"""Returns the config folder for the application. The default behavior 

463 is to return whatever is most appropriate for the operating system. 

464 

465 To give you an idea, for an app called ``"Foo Bar"``, something like 

466 the following folders could be returned: 

467 

468 Mac OS X: 

469 ``~/Library/Application Support/Foo Bar`` 

470 Mac OS X (POSIX): 

471 ``~/.foo-bar`` 

472 Unix: 

473 ``~/.config/foo-bar`` 

474 Unix (POSIX): 

475 ``~/.foo-bar`` 

476 Windows (roaming): 

477 ``C:\Users\<user>\AppData\Roaming\Foo Bar`` 

478 Windows (not roaming): 

479 ``C:\Users\<user>\AppData\Local\Foo Bar`` 

480 

481 .. versionadded:: 2.0 

482 

483 :param app_name: the application name. This should be properly capitalized 

484 and can contain whitespace. 

485 :param roaming: controls if the folder should be roaming or not on Windows. 

486 Has no effect otherwise. 

487 :param force_posix: if this is set to `True` then on any POSIX system the 

488 folder will be stored in the home folder with a leading 

489 dot instead of the XDG config home or darwin's 

490 application support folder. 

491 """ 

492 if WIN: 

493 key = "APPDATA" if roaming else "LOCALAPPDATA" 

494 folder = os.environ.get(key) 

495 if folder is None: 

496 folder = os.path.expanduser("~") 

497 return os.path.join(folder, app_name) 

498 if force_posix: 

499 return os.path.join(os.path.expanduser(f"~/.{_posixify(app_name)}")) 

500 if sys.platform == "darwin": 

501 return os.path.join( 

502 os.path.expanduser("~/Library/Application Support"), app_name 

503 ) 

504 return os.path.join( 

505 os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config")), 

506 _posixify(app_name), 

507 ) 

508 

509 

510class PacifyFlushWrapper: 

511 """This wrapper is used to catch and suppress BrokenPipeErrors resulting 

512 from ``.flush()`` being called on broken pipe during the shutdown/final-GC 

513 of the Python interpreter. Notably ``.flush()`` is always called on 

514 ``sys.stdout`` and ``sys.stderr``. So as to have minimal impact on any 

515 other cleanup code, and the case where the underlying file is not a broken 

516 pipe, all calls and attributes are proxied. 

517 """ 

518 

519 wrapped: t.IO[t.Any] 

520 

521 def __init__(self, wrapped: t.IO[t.Any]) -> None: 

522 self.wrapped = wrapped 

523 

524 def flush(self) -> None: 

525 try: 

526 self.wrapped.flush() 

527 except OSError as e: 

528 import errno 

529 

530 if e.errno != errno.EPIPE: 

531 raise 

532 

533 def __getattr__(self, attr: str) -> t.Any: 

534 return getattr(self.wrapped, attr) 

535 

536 

537def _detect_program_name( 

538 path: str | None = None, _main: ModuleType | None = None 

539) -> str: 

540 """Determine the command used to run the program, for use in help 

541 text. If a file or entry point was executed, the file name is 

542 returned. If ``python -m`` was used to execute a module or package, 

543 ``python -m name`` is returned. 

544 

545 This doesn't try to be too precise, the goal is to give a concise 

546 name for help text. Files are only shown as their name without the 

547 path. ``python`` is only shown for modules, and the full path to 

548 ``sys.executable`` is not shown. 

549 

550 :param path: The Python file being executed. Python puts this in 

551 ``sys.argv[0]``, which is used by default. 

552 :param _main: The ``__main__`` module. This should only be passed 

553 during internal testing. 

554 

555 .. versionadded:: 8.0 

556 Based on command args detection in the Werkzeug reloader. 

557 

558 :meta private: 

559 """ 

560 if _main is None: 

561 _main = sys.modules["__main__"] 

562 

563 if not path: 

564 path = sys.argv[0] 

565 

566 # The value of __package__ indicates how Python was called. It may 

567 # not exist if a setuptools script is installed as an egg. It may be 

568 # set incorrectly for entry points created with pip on Windows. 

569 # It is set to "" inside a Shiv or PEX zipapp. 

570 if getattr(_main, "__package__", None) in {None, ""} or ( 

571 os.name == "nt" 

572 and _main.__package__ == "" 

573 and not os.path.exists(path) 

574 and os.path.exists(f"{path}.exe") 

575 ): 

576 # Executed a file, like "python app.py". 

577 return os.path.basename(path) 

578 

579 # Executed a module, like "python -m example". 

580 # Rewritten by Python from "-m script" to "/path/to/script.py". 

581 # Need to look at main module to determine how it was executed. 

582 py_module = t.cast(str, _main.__package__) 

583 name = os.path.splitext(os.path.basename(path))[0] 

584 

585 # A submodule like "example.cli". 

586 if name != "__main__": 

587 py_module = f"{py_module}.{name}" 

588 

589 return f"python -m {py_module.lstrip('.')}" 

590 

591 

592def _expand_args( 

593 args: cabc.Iterable[str], 

594 *, 

595 user: bool = True, 

596 env: bool = True, 

597 glob_recursive: bool = True, 

598) -> list[str]: 

599 """Simulate Unix shell expansion with Python functions. 

600 

601 See :func:`glob.glob`, :func:`os.path.expanduser`, and 

602 :func:`os.path.expandvars`. 

603 

604 This is intended for use on Windows, where the shell does not do any 

605 expansion. It may not exactly match what a Unix shell would do. 

606 

607 :param args: List of command line arguments to expand. 

608 :param user: Expand user home directory. 

609 :param env: Expand environment variables. 

610 :param glob_recursive: ``**`` matches directories recursively. 

611 

612 .. versionchanged:: 8.1 

613 Invalid glob patterns are treated as empty expansions rather 

614 than raising an error. 

615 

616 .. versionadded:: 8.0 

617 

618 :meta private: 

619 """ 

620 from glob import glob 

621 

622 out = [] 

623 

624 for arg in args: 

625 if user: 

626 arg = os.path.expanduser(arg) 

627 

628 if env: 

629 arg = os.path.expandvars(arg) 

630 

631 try: 

632 matches = glob(arg, recursive=glob_recursive) 

633 except re.error: 

634 matches = [] 

635 

636 if not matches: 

637 out.append(arg) 

638 else: 

639 out.extend(matches) 

640 

641 return out