Voice et bot modif
This commit is contained in:
parent
189d56026b
commit
7333a22bcd
10774 changed files with 634644 additions and 933308 deletions
|
|
@ -8,6 +8,7 @@ from __future__ import annotations
|
|||
|
||||
import collections.abc as cabc
|
||||
import contextlib
|
||||
import io
|
||||
import math
|
||||
import os
|
||||
import shlex
|
||||
|
|
@ -23,7 +24,6 @@ from ._compat import _default_text_stdout
|
|||
from ._compat import CYGWIN
|
||||
from ._compat import get_best_encoding
|
||||
from ._compat import isatty
|
||||
from ._compat import open_stream
|
||||
from ._compat import strip_ansi
|
||||
from ._compat import term_len
|
||||
from ._compat import WIN
|
||||
|
|
@ -32,6 +32,19 @@ from .utils import echo
|
|||
|
||||
V = t.TypeVar("V")
|
||||
|
||||
|
||||
class _BufferedTextPagerStream(t.Protocol):
|
||||
buffer: t.BinaryIO
|
||||
|
||||
|
||||
def _has_binary_buffer(
|
||||
stream: t.BinaryIO | t.TextIO,
|
||||
) -> t.TypeGuard[_BufferedTextPagerStream]:
|
||||
# TextIO is wider than TextIOWrapper; text-only streams such as StringIO
|
||||
# are valid TextIO values but do not expose a binary buffer to wrap.
|
||||
return getattr(stream, "buffer", None) is not None
|
||||
|
||||
|
||||
if os.name == "nt":
|
||||
BEFORE_BAR = "\r"
|
||||
AFTER_BAR = "\n"
|
||||
|
|
@ -173,7 +186,13 @@ class ProgressBar(t.Generic[V]):
|
|||
hours = t % 24
|
||||
t //= 24
|
||||
if t > 0:
|
||||
return f"{t}d {hours:02}:{minutes:02}:{seconds:02}"
|
||||
return "{d}{day_label} {h:02}:{m:02}:{s:02}".format(
|
||||
d=t,
|
||||
day_label=_("d"),
|
||||
h=hours,
|
||||
m=minutes,
|
||||
s=seconds,
|
||||
)
|
||||
else:
|
||||
return f"{hours:02}:{minutes:02}:{seconds:02}"
|
||||
return ""
|
||||
|
|
@ -366,7 +385,20 @@ class ProgressBar(t.Generic[V]):
|
|||
self.render_progress()
|
||||
|
||||
|
||||
def pager(generator: cabc.Iterable[str], color: bool | None = None) -> None:
|
||||
class MaybeStripAnsi(io.TextIOWrapper):
|
||||
def __init__(self, stream: t.IO[bytes], *, color: bool, **kwargs: t.Any):
|
||||
super().__init__(stream, **kwargs)
|
||||
self.color = color
|
||||
|
||||
def write(self, text: str) -> int:
|
||||
if not self.color:
|
||||
text = strip_ansi(text)
|
||||
return super().write(text)
|
||||
|
||||
|
||||
def _pager_contextmanager(
|
||||
color: bool | None = None,
|
||||
) -> t.ContextManager[tuple[t.BinaryIO | t.TextIO, str, bool]]:
|
||||
"""Decide what method to use for paging through text."""
|
||||
stdout = _default_text_stdout()
|
||||
|
||||
|
|
@ -376,50 +408,69 @@ def pager(generator: cabc.Iterable[str], color: bool | None = None) -> None:
|
|||
stdout = StringIO()
|
||||
|
||||
if not isatty(sys.stdin) or not isatty(stdout):
|
||||
return _nullpager(stdout, generator, color)
|
||||
return _nullpager(stdout, color)
|
||||
|
||||
# Split and normalize the pager command into parts.
|
||||
pager_cmd_parts = shlex.split(os.environ.get("PAGER", ""), posix=False)
|
||||
# Split using POSIX mode (the default) so that quote characters are
|
||||
# stripped from tokens and quoted Windows paths are preserved.
|
||||
# Non-POSIX mode retains quotes in tokens, and wrapping tokens
|
||||
# with shlex.quote re-introduces quoting issues on Windows.
|
||||
pager_cmd_parts = shlex.split(os.environ.get("PAGER", ""))
|
||||
if pager_cmd_parts:
|
||||
if WIN:
|
||||
if _tempfilepager(generator, pager_cmd_parts, color):
|
||||
return
|
||||
elif _pipepager(generator, pager_cmd_parts, color):
|
||||
return
|
||||
return _tempfilepager(pager_cmd_parts, color)
|
||||
return _pipepager(pager_cmd_parts, color)
|
||||
|
||||
if os.environ.get("TERM") in ("dumb", "emacs"):
|
||||
return _nullpager(stdout, generator, color)
|
||||
if (WIN or sys.platform.startswith("os2")) and _tempfilepager(
|
||||
generator, ["more"], color
|
||||
):
|
||||
return
|
||||
if _pipepager(generator, ["less"], color):
|
||||
return
|
||||
|
||||
import tempfile
|
||||
|
||||
fd, filename = tempfile.mkstemp()
|
||||
os.close(fd)
|
||||
try:
|
||||
if _pipepager(generator, ["more"], color):
|
||||
return
|
||||
return _nullpager(stdout, generator, color)
|
||||
finally:
|
||||
os.unlink(filename)
|
||||
return _nullpager(stdout, color)
|
||||
if WIN or sys.platform.startswith("os2"):
|
||||
return _tempfilepager(["more"], color)
|
||||
return _pipepager(["less"], color)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def get_pager_file(color: bool | None = None) -> t.Generator[t.TextIO, None, None]:
|
||||
"""Context manager.
|
||||
Yields a writable file-like object which can be used as an output pager.
|
||||
.. versionadded:: 8.4
|
||||
:param color: controls if the pager supports ANSI colors or not. The
|
||||
default is autodetection.
|
||||
"""
|
||||
with _pager_contextmanager(color=color) as (stream, encoding, color):
|
||||
# Split streams by capabilities rather than the abstract TextIO /
|
||||
# BinaryIO annotations: buffered text streams can be unwrapped to bytes,
|
||||
# while text-only streams are yielded as-is.
|
||||
if _has_binary_buffer(stream):
|
||||
# Text stream backed by a binary buffer.
|
||||
stream = MaybeStripAnsi(stream.buffer, color=color, encoding=encoding)
|
||||
elif isinstance(stream, t.BinaryIO):
|
||||
# Binary stream
|
||||
stream = MaybeStripAnsi(stream, color=color, encoding=encoding)
|
||||
try:
|
||||
yield stream
|
||||
finally:
|
||||
stream.flush()
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _pipepager(
|
||||
generator: cabc.Iterable[str], cmd_parts: list[str], color: bool | None
|
||||
) -> bool:
|
||||
"""Page through text by feeding it to another program. Invoking a
|
||||
pager through this might support colors.
|
||||
cmd_parts: list[str], color: bool | None = None
|
||||
) -> t.Iterator[tuple[t.BinaryIO | t.TextIO, str, bool]]:
|
||||
"""Page through text by feeding it to another program.
|
||||
|
||||
Returns `True` if the command was found, `False` otherwise and thus another
|
||||
pager should be attempted.
|
||||
Invokes the pager via :class:`subprocess.Popen` with an ``argv`` list
|
||||
produced by :func:`shlex.split`. The command is resolved to an absolute
|
||||
path with :func:`shutil.which` as recommended by the
|
||||
:mod:`subprocess` docs for Windows compatibility.
|
||||
|
||||
Invoking a pager through this might support colors: if piping to
|
||||
``less`` and the user hasn't decided on colors, ``LESS=-R`` is set
|
||||
automatically.
|
||||
"""
|
||||
# Split the command into the invoked CLI and its parameters.
|
||||
if not cmd_parts:
|
||||
return False
|
||||
stdout = _default_text_stdout() or StringIO()
|
||||
yield stdout, "utf-8", False
|
||||
return
|
||||
|
||||
import shutil
|
||||
|
||||
|
|
@ -428,7 +479,9 @@ def _pipepager(
|
|||
|
||||
cmd_filepath = shutil.which(cmd)
|
||||
if not cmd_filepath:
|
||||
return False
|
||||
stdout = _default_text_stdout() or StringIO()
|
||||
yield stdout, "utf-8", False
|
||||
return
|
||||
|
||||
# Produces a normalized absolute path string.
|
||||
# multi-call binaries such as busybox derive their identity from the symlink
|
||||
|
|
@ -451,6 +504,9 @@ def _pipepager(
|
|||
elif "r" in less_flags or "R" in less_flags:
|
||||
color = True
|
||||
|
||||
if color is None:
|
||||
color = False
|
||||
|
||||
c = subprocess.Popen(
|
||||
[str(cmd_path)] + cmd_params,
|
||||
shell=False,
|
||||
|
|
@ -459,13 +515,10 @@ def _pipepager(
|
|||
errors="replace",
|
||||
text=True,
|
||||
)
|
||||
assert c.stdin is not None
|
||||
stdin = t.cast(t.BinaryIO, c.stdin)
|
||||
encoding = get_best_encoding(stdin)
|
||||
try:
|
||||
for text in generator:
|
||||
if not color:
|
||||
text = strip_ansi(text)
|
||||
|
||||
c.stdin.write(text)
|
||||
yield stdin, encoding, color
|
||||
except BrokenPipeError:
|
||||
# In case the pager exited unexpectedly, ignore the broken pipe error.
|
||||
pass
|
||||
|
|
@ -479,7 +532,7 @@ def _pipepager(
|
|||
finally:
|
||||
# We must close stdin and wait for the pager to exit before we continue
|
||||
try:
|
||||
c.stdin.close()
|
||||
stdin.close()
|
||||
# Close implies flush, so it might throw a BrokenPipeError if the pager
|
||||
# process exited already.
|
||||
except BrokenPipeError:
|
||||
|
|
@ -501,64 +554,87 @@ def _pipepager(
|
|||
else:
|
||||
break
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _tempfilepager(
|
||||
generator: cabc.Iterable[str], cmd_parts: list[str], color: bool | None
|
||||
) -> bool:
|
||||
cmd_parts: list[str], color: bool | None = None
|
||||
) -> t.Iterator[tuple[t.BinaryIO | t.TextIO, str, bool]]:
|
||||
"""Page through text by invoking a program on a temporary file.
|
||||
|
||||
Returns `True` if the command was found, `False` otherwise and thus another
|
||||
pager should be attempted.
|
||||
Used as the primary pager strategy on Windows (where piping to
|
||||
``more`` adds spurious ``\\r\\n``), and as a fallback on other
|
||||
platforms. The command is resolved to an absolute path with
|
||||
:func:`shutil.which`.
|
||||
"""
|
||||
# Split the command into the invoked CLI and its parameters.
|
||||
if not cmd_parts:
|
||||
return False
|
||||
stdout = _default_text_stdout() or StringIO()
|
||||
yield stdout, "utf-8", False
|
||||
return
|
||||
|
||||
import shutil
|
||||
import subprocess
|
||||
|
||||
cmd = cmd_parts[0]
|
||||
|
||||
cmd_filepath = shutil.which(cmd)
|
||||
if not cmd_filepath:
|
||||
return False
|
||||
stdout = _default_text_stdout() or StringIO()
|
||||
yield stdout, "utf-8", False
|
||||
return
|
||||
|
||||
# Produces a normalized absolute path string.
|
||||
# multi-call binaries such as busybox derive their identity from the symlink
|
||||
# less -> busybox. resolve() causes them to misbehave. (eg. less becomes busybox)
|
||||
cmd_path = Path(cmd_filepath).absolute()
|
||||
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
fd, filename = tempfile.mkstemp()
|
||||
# TODO: This never terminates if the passed generator never terminates.
|
||||
text = "".join(generator)
|
||||
if not color:
|
||||
text = strip_ansi(text)
|
||||
encoding = get_best_encoding(sys.stdout)
|
||||
with open_stream(filename, "wb")[0] as f:
|
||||
f.write(text.encode(encoding))
|
||||
if color is None:
|
||||
color = False
|
||||
# On Windows, NamedTemporaryFile cannot be opened by another process
|
||||
# while Python still has it open, so we use delete=False and clean up manually
|
||||
# rather than using a contextmanager here.
|
||||
f = tempfile.NamedTemporaryFile(mode="wb", delete=False)
|
||||
try:
|
||||
subprocess.call([str(cmd_path), filename])
|
||||
except OSError:
|
||||
# Command not found
|
||||
pass
|
||||
yield t.cast(t.BinaryIO, f), encoding, color
|
||||
f.flush()
|
||||
f.close()
|
||||
subprocess.call([str(cmd_path), f.name])
|
||||
finally:
|
||||
os.close(fd)
|
||||
os.unlink(filename)
|
||||
|
||||
return True
|
||||
os.unlink(f.name)
|
||||
|
||||
|
||||
class _SkipClose:
|
||||
def __init__(self, stream: t.IO[t.Any]) -> None:
|
||||
self.stream = stream
|
||||
|
||||
def __getattr__(self, name: str) -> t.Any:
|
||||
return getattr(self.stream, name)
|
||||
|
||||
@property
|
||||
def buffer(self) -> t.BinaryIO:
|
||||
return _SkipClose(self.stream.buffer) # type: ignore[attr-defined, return-value]
|
||||
|
||||
def close(self) -> None:
|
||||
pass
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _nullpager(
|
||||
stream: t.TextIO, generator: cabc.Iterable[str], color: bool | None
|
||||
) -> None:
|
||||
"""Simply print unformatted text. This is the ultimate fallback."""
|
||||
for text in generator:
|
||||
if not color:
|
||||
text = strip_ansi(text)
|
||||
stream.write(text)
|
||||
stream: t.TextIO, color: bool | None = None
|
||||
) -> t.Iterator[tuple[t.TextIO, str, bool]]:
|
||||
"""Simply print unformatted text. This is the ultimate fallback. Don't close the
|
||||
output stream in this case, since it's coming from elsewhere rather than our
|
||||
internal helpers.
|
||||
"""
|
||||
encoding = get_best_encoding(stream)
|
||||
|
||||
if color is None:
|
||||
color = False
|
||||
|
||||
yield _SkipClose(stream), encoding, color # type: ignore[misc]
|
||||
|
||||
|
||||
class Editor:
|
||||
|
|
@ -592,6 +668,8 @@ class Editor:
|
|||
return "vi"
|
||||
|
||||
def edit_files(self, filenames: cabc.Iterable[str]) -> None:
|
||||
"""Open files in the user's editor."""
|
||||
import shlex
|
||||
import subprocess
|
||||
|
||||
editor = self.get_editor()
|
||||
|
|
@ -601,11 +679,13 @@ class Editor:
|
|||
environ = os.environ.copy()
|
||||
environ.update(self.env)
|
||||
|
||||
exc_filename = " ".join(f'"{filename}"' for filename in filenames)
|
||||
|
||||
try:
|
||||
# Split in POSIX mode (the default) for the same reasons as
|
||||
# in pager(): strips quotes from tokens and preserves quoted
|
||||
# Windows paths.
|
||||
c = subprocess.Popen(
|
||||
args=f"{editor} {exc_filename}", env=environ, shell=True
|
||||
args=shlex.split(editor) + list(filenames),
|
||||
env=environ,
|
||||
)
|
||||
exit_code = c.wait()
|
||||
if exit_code != 0:
|
||||
|
|
@ -699,18 +779,17 @@ def open_url(url: str, wait: bool = False, locate: bool = False) -> int:
|
|||
elif WIN:
|
||||
if locate:
|
||||
url = _unquote_file(url)
|
||||
args = ["explorer", f"/select,{url}"]
|
||||
args = ["explorer", "/select,", url]
|
||||
try:
|
||||
return subprocess.call(args)
|
||||
except OSError:
|
||||
return 127
|
||||
else:
|
||||
args = ["start"]
|
||||
if wait:
|
||||
args.append("/WAIT")
|
||||
args.append("")
|
||||
args.append(url)
|
||||
try:
|
||||
return subprocess.call(args)
|
||||
except OSError:
|
||||
# Command not found
|
||||
return 127
|
||||
try:
|
||||
os.startfile(url) # type: ignore[attr-defined]
|
||||
except OSError:
|
||||
return 127
|
||||
return 0
|
||||
elif CYGWIN:
|
||||
if locate:
|
||||
url = _unquote_file(url)
|
||||
|
|
@ -754,8 +833,6 @@ def _translate_ch_to_exc(ch: str) -> None:
|
|||
if ch == "\x1a" and WIN: # Windows, Ctrl+Z
|
||||
raise EOFError()
|
||||
|
||||
return None
|
||||
|
||||
|
||||
if sys.platform == "win32":
|
||||
import msvcrt
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue