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

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

238 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 def __init__( 

121 self, 

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

123 mode: str = "r", 

124 encoding: str | None = None, 

125 errors: str | None = "strict", 

126 atomic: bool = False, 

127 ): 

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

129 self.mode = mode 

130 self.encoding = encoding 

131 self.errors = errors 

132 self.atomic = atomic 

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

134 self.should_close: bool 

135 

136 if self.name == "-": 

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

138 else: 

139 if "r" in mode: 

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

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

142 # some cases early. 

143 open(filename, mode).close() 

144 self._f = None 

145 self.should_close = True 

146 

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

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

149 

150 def __repr__(self) -> str: 

151 if self._f is not None: 

152 return repr(self._f) 

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

154 

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

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

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

158 that Click shows. 

159 """ 

160 if self._f is not None: 

161 return self._f 

162 try: 

163 rv, self.should_close = open_stream( 

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

165 ) 

166 except OSError as e: 

167 from .exceptions import FileError 

168 

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

170 self._f = rv 

171 return rv 

172 

173 def close(self) -> None: 

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

175 if self._f is not None: 

176 self._f.close() 

177 

178 def close_intelligently(self) -> None: 

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

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

181 """ 

182 if self.should_close: 

183 self.close() 

184 

185 def __enter__(self) -> LazyFile: 

186 return self 

187 

188 def __exit__( 

189 self, 

190 exc_type: type[BaseException] | None, 

191 exc_value: BaseException | None, 

192 tb: TracebackType | None, 

193 ) -> None: 

194 self.close_intelligently() 

195 

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

197 self.open() 

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

199 

200 

201class KeepOpenFile: 

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

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

204 

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

206 return getattr(self._file, name) 

207 

208 def __enter__(self) -> KeepOpenFile: 

209 return self 

210 

211 def __exit__( 

212 self, 

213 exc_type: type[BaseException] | None, 

214 exc_value: BaseException | None, 

215 tb: TracebackType | None, 

216 ) -> None: 

217 pass 

218 

219 def __repr__(self) -> str: 

220 return repr(self._file) 

221 

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

223 return iter(self._file) 

224 

225 

226def echo( 

227 message: t.Any | None = None, 

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

229 nl: bool = True, 

230 err: bool = False, 

231 color: bool | None = None, 

232) -> None: 

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

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

235 for different data, files, and environments. 

236 

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

238 

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

240 - Supports Unicode in the Windows console. 

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

242 to text outputs. 

243 - Supports colors and styles on Windows. 

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

245 like an interactive terminal. 

246 - Always flushes the output. 

247 

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

249 converted to strings. 

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

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

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

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

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

255 an interactive terminal. 

256 

257 .. versionchanged:: 6.0 

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

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

260 will still not support Unicode. 

261 

262 .. versionchanged:: 4.0 

263 Added the ``color`` parameter. 

264 

265 .. versionadded:: 3.0 

266 Added the ``err`` parameter. 

267 

268 .. versionchanged:: 2.0 

269 Support colors on Windows if colorama is installed. 

270 """ 

271 if file is None: 

272 if err: 

273 file = _default_text_stderr() 

274 else: 

275 file = _default_text_stdout() 

276 

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

278 # pythonw on Windows. 

279 if file is None: 

280 return 

281 

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

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

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

285 else: 

286 out = message 

287 

288 if nl: 

289 out = out or "" 

290 if isinstance(out, str): 

291 out += "\n" 

292 else: 

293 out += b"\n" 

294 

295 if not out: 

296 file.flush() 

297 return 

298 

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

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

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

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

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

304 binary_file = _find_binary_writer(file) 

305 

306 if binary_file is not None: 

307 file.flush() 

308 binary_file.write(out) 

309 binary_file.flush() 

310 return 

311 

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

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

314 else: 

315 color = resolve_color_default(color) 

316 

317 if should_strip_ansi(file, color): 

318 out = strip_ansi(out) 

319 elif WIN: 

320 if auto_wrap_for_ansi is not None: 

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

322 elif not color: 

323 out = strip_ansi(out) 

324 

325 file.write(out) # type: ignore 

326 file.flush() 

327 

328 

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

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

331 

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

333 ``'stdout'`` and ``'stderr'`` 

334 """ 

335 opener = binary_streams.get(name) 

336 if opener is None: 

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

338 return opener() 

339 

340 

341def get_text_stream( 

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

343 encoding: str | None = None, 

344 errors: str | None = "strict", 

345) -> t.TextIO: 

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

347 a wrapped stream around a binary stream returned from 

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

349 correctly configured streams. 

350 

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

352 ``'stdout'`` and ``'stderr'`` 

353 :param encoding: overrides the detected default encoding. 

354 :param errors: overrides the default error mode. 

355 """ 

356 opener = text_streams.get(name) 

357 if opener is None: 

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

359 return opener(encoding, errors) 

360 

361 

362def open_file( 

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

364 mode: str = "r", 

365 encoding: str | None = None, 

366 errors: str | None = "strict", 

367 lazy: bool = False, 

368 atomic: bool = False, 

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

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

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

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

373 

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

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

376 This makes it possible to use the function without accidentally 

377 closing a standard stream: 

378 

379 .. code-block:: python 

380 

381 with open_file(filename) as f: 

382 ... 

383 

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

385 ``stdin``/``stdout``. 

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

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

388 text mode. 

389 :param errors: The error handling mode. 

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

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

392 early, then closed until it is read again. 

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

394 on close. 

395 

396 .. versionadded:: 3.0 

397 """ 

398 if lazy: 

399 return t.cast( 

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

401 ) 

402 

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

404 

405 if not should_close: 

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

407 

408 return f 

409 

410 

411def format_filename( 

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

413 shorten: bool = False, 

414) -> str: 

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

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

417 with the replacement character ``�``. 

418 

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

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

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

422 

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

424 PEP 540, including: 

425 

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

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

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

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

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

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

432 

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

434 the filename into unicode without failing. 

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

436 path that leads up to it. 

437 """ 

438 if shorten: 

439 filename = os.path.basename(filename) 

440 else: 

441 filename = os.fspath(filename) 

442 

443 if isinstance(filename, bytes): 

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

445 else: 

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

447 "utf-8", "replace" 

448 ) 

449 

450 return filename 

451 

452 

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

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

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

456 

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

458 the following folders could be returned: 

459 

460 Mac OS X: 

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

462 Mac OS X (POSIX): 

463 ``~/.foo-bar`` 

464 Unix: 

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

466 Unix (POSIX): 

467 ``~/.foo-bar`` 

468 Windows (roaming): 

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

470 Windows (not roaming): 

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

472 

473 .. versionadded:: 2.0 

474 

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

476 and can contain whitespace. 

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

478 Has no effect otherwise. 

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

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

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

482 application support folder. 

483 """ 

484 if WIN: 

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

486 folder = os.environ.get(key) 

487 if folder is None: 

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

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

490 if force_posix: 

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

492 if sys.platform == "darwin": 

493 return os.path.join( 

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

495 ) 

496 return os.path.join( 

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

498 _posixify(app_name), 

499 ) 

500 

501 

502class PacifyFlushWrapper: 

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

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

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

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

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

508 pipe, all calls and attributes are proxied. 

509 """ 

510 

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

512 self.wrapped = wrapped 

513 

514 def flush(self) -> None: 

515 try: 

516 self.wrapped.flush() 

517 except OSError as e: 

518 import errno 

519 

520 if e.errno != errno.EPIPE: 

521 raise 

522 

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

524 return getattr(self.wrapped, attr) 

525 

526 

527def _detect_program_name( 

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

529) -> str: 

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

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

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

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

534 

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

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

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

538 ``sys.executable`` is not shown. 

539 

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

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

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

543 during internal testing. 

544 

545 .. versionadded:: 8.0 

546 Based on command args detection in the Werkzeug reloader. 

547 

548 :meta private: 

549 """ 

550 if _main is None: 

551 _main = sys.modules["__main__"] 

552 

553 if not path: 

554 path = sys.argv[0] 

555 

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

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

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

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

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

561 os.name == "nt" 

562 and _main.__package__ == "" 

563 and not os.path.exists(path) 

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

565 ): 

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

567 return os.path.basename(path) 

568 

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

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

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

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

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

574 

575 # A submodule like "example.cli". 

576 if name != "__main__": 

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

578 

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

580 

581 

582def _expand_args( 

583 args: cabc.Iterable[str], 

584 *, 

585 user: bool = True, 

586 env: bool = True, 

587 glob_recursive: bool = True, 

588) -> list[str]: 

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

590 

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

592 :func:`os.path.expandvars`. 

593 

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

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

596 

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

598 :param user: Expand user home directory. 

599 :param env: Expand environment variables. 

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

601 

602 .. versionchanged:: 8.1 

603 Invalid glob patterns are treated as empty expansions rather 

604 than raising an error. 

605 

606 .. versionadded:: 8.0 

607 

608 :meta private: 

609 """ 

610 from glob import glob 

611 

612 out = [] 

613 

614 for arg in args: 

615 if user: 

616 arg = os.path.expanduser(arg) 

617 

618 if env: 

619 arg = os.path.expandvars(arg) 

620 

621 try: 

622 matches = glob(arg, recursive=glob_recursive) 

623 except re.error: 

624 matches = [] 

625 

626 if not matches: 

627 out.append(arg) 

628 else: 

629 out.extend(matches) 

630 

631 return out