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

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

237 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 types import ModuleType 

10from types import TracebackType 

11 

12from ._compat import _default_text_stderr 

13from ._compat import _default_text_stdout 

14from ._compat import _find_binary_writer 

15from ._compat import auto_wrap_for_ansi 

16from ._compat import binary_streams 

17from ._compat import open_stream 

18from ._compat import should_strip_ansi 

19from ._compat import strip_ansi 

20from ._compat import text_streams 

21from ._compat import WIN 

22from .globals import resolve_color_default 

23 

24if t.TYPE_CHECKING: 

25 import typing_extensions as te 

26 

27 P = te.ParamSpec("P") 

28 

29R = t.TypeVar("R") 

30 

31 

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

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

34 

35 

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

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

38 

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

40 try: 

41 return func(*args, **kwargs) 

42 except Exception: 

43 pass 

44 return None 

45 

46 return update_wrapper(wrapper, func) 

47 

48 

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

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

51 if isinstance(value, bytes): 

52 try: 

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

54 except UnicodeError: 

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

56 return str(value) 

57 

58 

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

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

61 

62 :meta private: 

63 """ 

64 # Consider only the first paragraph. 

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

66 

67 if paragraph_end != -1: 

68 help = help[:paragraph_end] 

69 

70 # Collapse newlines, tabs, and spaces. 

71 words = help.split() 

72 

73 if not words: 

74 return "" 

75 

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

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

78 words = words[1:] 

79 

80 total_length = 0 

81 last_index = len(words) - 1 

82 

83 for i, word in enumerate(words): 

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

85 

86 if total_length > max_length: # too long, truncate 

87 break 

88 

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

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

91 

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

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

94 else: 

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

96 

97 # Account for the length of the suffix. 

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

99 

100 # remove words until the length is short enough 

101 while i > 0: 

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

103 

104 if total_length <= max_length: 

105 break 

106 

107 i -= 1 

108 

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

110 

111 

112class LazyFile: 

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

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

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

116 files for writing. 

117 """ 

118 

119 def __init__( 

120 self, 

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

122 mode: str = "r", 

123 encoding: str | None = None, 

124 errors: str | None = "strict", 

125 atomic: bool = False, 

126 ): 

127 self.name: str = os.fspath(filename) 

128 self.mode = mode 

129 self.encoding = encoding 

130 self.errors = errors 

131 self.atomic = atomic 

132 self._f: t.IO[t.Any] | None 

133 self.should_close: bool 

134 

135 if self.name == "-": 

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

137 else: 

138 if "r" in mode: 

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

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

141 # some cases early. 

142 open(filename, mode).close() 

143 self._f = None 

144 self.should_close = True 

145 

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

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

148 

149 def __repr__(self) -> str: 

150 if self._f is not None: 

151 return repr(self._f) 

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

153 

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

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

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

157 that Click shows. 

158 """ 

159 if self._f is not None: 

160 return self._f 

161 try: 

162 rv, self.should_close = open_stream( 

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

164 ) 

165 except OSError as e: 

166 from .exceptions import FileError 

167 

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

169 self._f = rv 

170 return rv 

171 

172 def close(self) -> None: 

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

174 if self._f is not None: 

175 self._f.close() 

176 

177 def close_intelligently(self) -> None: 

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

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

180 """ 

181 if self.should_close: 

182 self.close() 

183 

184 def __enter__(self) -> LazyFile: 

185 return self 

186 

187 def __exit__( 

188 self, 

189 exc_type: type[BaseException] | None, 

190 exc_value: BaseException | None, 

191 tb: TracebackType | None, 

192 ) -> None: 

193 self.close_intelligently() 

194 

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

196 self.open() 

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

198 

199 

200class KeepOpenFile: 

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

202 self._file: t.IO[t.Any] = file 

203 

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

205 return getattr(self._file, name) 

206 

207 def __enter__(self) -> KeepOpenFile: 

208 return self 

209 

210 def __exit__( 

211 self, 

212 exc_type: type[BaseException] | None, 

213 exc_value: BaseException | None, 

214 tb: TracebackType | None, 

215 ) -> None: 

216 pass 

217 

218 def __repr__(self) -> str: 

219 return repr(self._file) 

220 

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

222 return iter(self._file) 

223 

224 

225def echo( 

226 message: t.Any | None = None, 

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

228 nl: bool = True, 

229 err: bool = False, 

230 color: bool | None = None, 

231) -> None: 

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

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

234 for different data, files, and environments. 

235 

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

237 

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

239 - Supports Unicode in the Windows console. 

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

241 to text outputs. 

242 - Supports colors and styles on Windows. 

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

244 like an interactive terminal. 

245 - Always flushes the output. 

246 

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

248 converted to strings. 

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

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

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

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

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

254 an interactive terminal. 

255 

256 .. versionchanged:: 6.0 

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

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

259 will still not support Unicode. 

260 

261 .. versionchanged:: 4.0 

262 Added the ``color`` parameter. 

263 

264 .. versionadded:: 3.0 

265 Added the ``err`` parameter. 

266 

267 .. versionchanged:: 2.0 

268 Support colors on Windows if colorama is installed. 

269 """ 

270 if file is None: 

271 if err: 

272 file = _default_text_stderr() 

273 else: 

274 file = _default_text_stdout() 

275 

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

277 # pythonw on Windows. 

278 if file is None: 

279 return 

280 

281 # Convert non bytes/text into the native string type. 

282 if message is not None and not isinstance(message, (str, bytes, bytearray)): 

283 out: str | bytes | bytearray | None = str(message) 

284 else: 

285 out = message 

286 

287 if nl: 

288 out = out or "" 

289 if isinstance(out, str): 

290 out += "\n" 

291 else: 

292 out += b"\n" 

293 

294 if not out: 

295 file.flush() 

296 return 

297 

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

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

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

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

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

303 binary_file = _find_binary_writer(file) 

304 

305 if binary_file is not None: 

306 file.flush() 

307 binary_file.write(out) 

308 binary_file.flush() 

309 return 

310 

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

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

313 else: 

314 color = resolve_color_default(color) 

315 

316 if should_strip_ansi(file, color): 

317 out = strip_ansi(out) 

318 elif WIN: 

319 if auto_wrap_for_ansi is not None: 

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

321 elif not color: 

322 out = strip_ansi(out) 

323 

324 file.write(out) # type: ignore 

325 file.flush() 

326 

327 

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

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

330 

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

332 ``'stdout'`` and ``'stderr'`` 

333 """ 

334 opener = binary_streams.get(name) 

335 if opener is None: 

336 raise TypeError(f"Unknown standard stream '{name}'") 

337 return opener() 

338 

339 

340def get_text_stream( 

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

342 encoding: str | None = None, 

343 errors: str | None = "strict", 

344) -> t.TextIO: 

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

346 a wrapped stream around a binary stream returned from 

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

348 correctly configured streams. 

349 

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

351 ``'stdout'`` and ``'stderr'`` 

352 :param encoding: overrides the detected default encoding. 

353 :param errors: overrides the default error mode. 

354 """ 

355 opener = text_streams.get(name) 

356 if opener is None: 

357 raise TypeError(f"Unknown standard stream '{name}'") 

358 return opener(encoding, errors) 

359 

360 

361def open_file( 

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

363 mode: str = "r", 

364 encoding: str | None = None, 

365 errors: str | None = "strict", 

366 lazy: bool = False, 

367 atomic: bool = False, 

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

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

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

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

372 

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

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

375 This makes it possible to use the function without accidentally 

376 closing a standard stream: 

377 

378 .. code-block:: python 

379 

380 with open_file(filename) as f: 

381 ... 

382 

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

384 ``stdin``/``stdout``. 

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

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

387 text mode. 

388 :param errors: The error handling mode. 

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

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

391 early, then closed until it is read again. 

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

393 on close. 

394 

395 .. versionadded:: 3.0 

396 """ 

397 if lazy: 

398 return t.cast( 

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

400 ) 

401 

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

403 

404 if not should_close: 

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

406 

407 return f 

408 

409 

410def format_filename( 

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

412 shorten: bool = False, 

413) -> str: 

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

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

416 with the replacement character ``�``. 

417 

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

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

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

421 

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

423 PEP 540, including: 

424 

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

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

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

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

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

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

431 

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

433 the filename into unicode without failing. 

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

435 path that leads up to it. 

436 """ 

437 if shorten: 

438 filename = os.path.basename(filename) 

439 else: 

440 filename = os.fspath(filename) 

441 

442 if isinstance(filename, bytes): 

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

444 else: 

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

446 "utf-8", "replace" 

447 ) 

448 

449 return filename 

450 

451 

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

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

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

455 

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

457 the following folders could be returned: 

458 

459 Mac OS X: 

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

461 Mac OS X (POSIX): 

462 ``~/.foo-bar`` 

463 Unix: 

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

465 Unix (POSIX): 

466 ``~/.foo-bar`` 

467 Windows (roaming): 

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

469 Windows (not roaming): 

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

471 

472 .. versionadded:: 2.0 

473 

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

475 and can contain whitespace. 

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

477 Has no effect otherwise. 

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

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

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

481 application support folder. 

482 """ 

483 if WIN: 

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

485 folder = os.environ.get(key) 

486 if folder is None: 

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

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

489 if force_posix: 

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

491 if sys.platform == "darwin": 

492 return os.path.join( 

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

494 ) 

495 return os.path.join( 

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

497 _posixify(app_name), 

498 ) 

499 

500 

501class PacifyFlushWrapper: 

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

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

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

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

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

507 pipe, all calls and attributes are proxied. 

508 """ 

509 

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

511 self.wrapped = wrapped 

512 

513 def flush(self) -> None: 

514 try: 

515 self.wrapped.flush() 

516 except OSError as e: 

517 import errno 

518 

519 if e.errno != errno.EPIPE: 

520 raise 

521 

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

523 return getattr(self.wrapped, attr) 

524 

525 

526def _detect_program_name( 

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

528) -> str: 

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

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

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

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

533 

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

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

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

537 ``sys.executable`` is not shown. 

538 

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

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

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

542 during internal testing. 

543 

544 .. versionadded:: 8.0 

545 Based on command args detection in the Werkzeug reloader. 

546 

547 :meta private: 

548 """ 

549 if _main is None: 

550 _main = sys.modules["__main__"] 

551 

552 if not path: 

553 path = sys.argv[0] 

554 

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

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

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

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

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

560 os.name == "nt" 

561 and _main.__package__ == "" 

562 and not os.path.exists(path) 

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

564 ): 

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

566 return os.path.basename(path) 

567 

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

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

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

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

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

573 

574 # A submodule like "example.cli". 

575 if name != "__main__": 

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

577 

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

579 

580 

581def _expand_args( 

582 args: cabc.Iterable[str], 

583 *, 

584 user: bool = True, 

585 env: bool = True, 

586 glob_recursive: bool = True, 

587) -> list[str]: 

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

589 

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

591 :func:`os.path.expandvars`. 

592 

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

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

595 

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

597 :param user: Expand user home directory. 

598 :param env: Expand environment variables. 

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

600 

601 .. versionchanged:: 8.1 

602 Invalid glob patterns are treated as empty expansions rather 

603 than raising an error. 

604 

605 .. versionadded:: 8.0 

606 

607 :meta private: 

608 """ 

609 from glob import glob 

610 

611 out = [] 

612 

613 for arg in args: 

614 if user: 

615 arg = os.path.expanduser(arg) 

616 

617 if env: 

618 arg = os.path.expandvars(arg) 

619 

620 try: 

621 matches = glob(arg, recursive=glob_recursive) 

622 except re.error: 

623 matches = [] 

624 

625 if not matches: 

626 out.append(arg) 

627 else: 

628 out.extend(matches) 

629 

630 return out