python-click/src/click/_winconsole.py

280 lines
7.7 KiB
Python
Raw Permalink Normal View History

2015-12-04 16:51:02 +01:00
# This module is based on the excellent work by Adam Bartoš who
# provided a lot of what went into the implementation here in
# the discussion to issue1602 in the Python bug tracker.
#
# There are some general differences in regards to how this works
# compared to the original patches as we do not need to patch
# the entire interpreter but just work in our little world of
2021-10-10 03:31:57 +02:00
# echo and prompt.
2015-12-04 16:51:02 +01:00
import io
import sys
import time
2021-10-10 03:31:57 +02:00
import typing as t
2020-07-21 08:23:42 +02:00
from ctypes import byref
from ctypes import c_char
from ctypes import c_char_p
from ctypes import c_int
from ctypes import c_ssize_t
from ctypes import c_ulong
from ctypes import c_void_p
from ctypes import POINTER
from ctypes import py_object
2021-10-10 03:31:57 +02:00
from ctypes import Structure
2020-07-21 08:23:42 +02:00
from ctypes.wintypes import DWORD
from ctypes.wintypes import HANDLE
from ctypes.wintypes import LPCWSTR
from ctypes.wintypes import LPWSTR
from ._compat import _NonClosingTextIOWrapper
2015-12-04 16:51:02 +01:00
2021-10-10 03:31:57 +02:00
assert sys.platform == "win32"
import msvcrt # noqa: E402
from ctypes import windll # noqa: E402
from ctypes import WINFUNCTYPE # noqa: E402
2015-12-04 16:51:02 +01:00
c_ssize_p = POINTER(c_ssize_t)
kernel32 = windll.kernel32
GetStdHandle = kernel32.GetStdHandle
ReadConsoleW = kernel32.ReadConsoleW
WriteConsoleW = kernel32.WriteConsoleW
2020-07-21 08:23:42 +02:00
GetConsoleMode = kernel32.GetConsoleMode
2015-12-04 16:51:02 +01:00
GetLastError = kernel32.GetLastError
2020-07-21 08:23:42 +02:00
GetCommandLineW = WINFUNCTYPE(LPWSTR)(("GetCommandLineW", windll.kernel32))
CommandLineToArgvW = WINFUNCTYPE(POINTER(LPWSTR), LPCWSTR, POINTER(c_int))(
("CommandLineToArgvW", windll.shell32)
)
2021-10-10 03:31:57 +02:00
LocalFree = WINFUNCTYPE(c_void_p, c_void_p)(("LocalFree", windll.kernel32))
2015-12-04 16:51:02 +01:00
STDIN_HANDLE = GetStdHandle(-10)
STDOUT_HANDLE = GetStdHandle(-11)
STDERR_HANDLE = GetStdHandle(-12)
PyBUF_SIMPLE = 0
PyBUF_WRITABLE = 1
ERROR_SUCCESS = 0
ERROR_NOT_ENOUGH_MEMORY = 8
ERROR_OPERATION_ABORTED = 995
STDIN_FILENO = 0
STDOUT_FILENO = 1
STDERR_FILENO = 2
2020-07-21 08:23:42 +02:00
EOF = b"\x1a"
2015-12-04 16:51:02 +01:00
MAX_BYTES_WRITTEN = 32767
2021-10-10 03:31:57 +02:00
try:
from ctypes import pythonapi
except ImportError:
# On PyPy we cannot get buffers so our ability to operate here is
# severely limited.
2016-04-06 18:13:57 +02:00
get_buffer = None
else:
2020-07-21 08:23:42 +02:00
2021-10-10 03:31:57 +02:00
class Py_buffer(Structure):
_fields_ = [
("buf", c_void_p),
("obj", py_object),
("len", c_ssize_t),
("itemsize", c_ssize_t),
("readonly", c_int),
("ndim", c_int),
("format", c_char_p),
("shape", c_ssize_p),
("strides", c_ssize_p),
("suboffsets", c_ssize_p),
("internal", c_void_p),
]
PyObject_GetBuffer = pythonapi.PyObject_GetBuffer
PyBuffer_Release = pythonapi.PyBuffer_Release
2016-04-06 18:13:57 +02:00
def get_buffer(obj, writable=False):
buf = Py_buffer()
flags = PyBUF_WRITABLE if writable else PyBUF_SIMPLE
PyObject_GetBuffer(py_object(obj), byref(buf), flags)
2021-10-10 03:31:57 +02:00
2016-04-06 18:13:57 +02:00
try:
buffer_type = c_char * buf.len
return buffer_type.from_address(buf.buf)
finally:
PyBuffer_Release(byref(buf))
2015-12-04 16:51:02 +01:00
class _WindowsConsoleRawIOBase(io.RawIOBase):
def __init__(self, handle):
self.handle = handle
def isatty(self):
2021-10-10 03:31:57 +02:00
super().isatty()
2015-12-04 16:51:02 +01:00
return True
class _WindowsConsoleReader(_WindowsConsoleRawIOBase):
def readable(self):
return True
def readinto(self, b):
bytes_to_be_read = len(b)
if not bytes_to_be_read:
return 0
elif bytes_to_be_read % 2:
2020-07-21 08:23:42 +02:00
raise ValueError(
"cannot read odd number of bytes from UTF-16-LE encoded console"
)
2015-12-04 16:51:02 +01:00
buffer = get_buffer(b, writable=True)
code_units_to_be_read = bytes_to_be_read // 2
code_units_read = c_ulong()
2020-07-21 08:23:42 +02:00
rv = ReadConsoleW(
HANDLE(self.handle),
buffer,
code_units_to_be_read,
byref(code_units_read),
None,
)
2015-12-04 16:51:02 +01:00
if GetLastError() == ERROR_OPERATION_ABORTED:
# wait for KeyboardInterrupt
time.sleep(0.1)
if not rv:
2021-10-10 03:31:57 +02:00
raise OSError(f"Windows error: {GetLastError()}")
2015-12-04 16:51:02 +01:00
if buffer[0] == EOF:
return 0
return 2 * code_units_read.value
class _WindowsConsoleWriter(_WindowsConsoleRawIOBase):
def writable(self):
return True
@staticmethod
def _get_error_message(errno):
if errno == ERROR_SUCCESS:
2020-07-21 08:23:42 +02:00
return "ERROR_SUCCESS"
2015-12-04 16:51:02 +01:00
elif errno == ERROR_NOT_ENOUGH_MEMORY:
2020-07-21 08:23:42 +02:00
return "ERROR_NOT_ENOUGH_MEMORY"
2021-10-10 03:31:57 +02:00
return f"Windows error {errno}"
2015-12-04 16:51:02 +01:00
def write(self, b):
bytes_to_be_written = len(b)
buf = get_buffer(b)
2020-07-21 08:23:42 +02:00
code_units_to_be_written = min(bytes_to_be_written, MAX_BYTES_WRITTEN) // 2
2015-12-04 16:51:02 +01:00
code_units_written = c_ulong()
2020-07-21 08:23:42 +02:00
WriteConsoleW(
HANDLE(self.handle),
buf,
code_units_to_be_written,
byref(code_units_written),
None,
)
2015-12-04 16:51:02 +01:00
bytes_written = 2 * code_units_written.value
if bytes_written == 0 and bytes_to_be_written > 0:
raise OSError(self._get_error_message(GetLastError()))
return bytes_written
2021-10-10 03:31:57 +02:00
class ConsoleStream:
def __init__(self, text_stream: t.TextIO, byte_stream: t.BinaryIO) -> None:
2015-12-04 16:51:02 +01:00
self._text_stream = text_stream
self.buffer = byte_stream
@property
2021-10-10 03:31:57 +02:00
def name(self) -> str:
2015-12-04 16:51:02 +01:00
return self.buffer.name
2021-10-10 03:31:57 +02:00
def write(self, x: t.AnyStr) -> int:
if isinstance(x, str):
2015-12-04 16:51:02 +01:00
return self._text_stream.write(x)
try:
self.flush()
except Exception:
pass
return self.buffer.write(x)
2021-10-10 03:31:57 +02:00
def writelines(self, lines: t.Iterable[t.AnyStr]) -> None:
2015-12-04 16:51:02 +01:00
for line in lines:
self.write(line)
2021-10-10 03:31:57 +02:00
def __getattr__(self, name: str) -> t.Any:
2015-12-04 16:51:02 +01:00
return getattr(self._text_stream, name)
2021-10-10 03:31:57 +02:00
def isatty(self) -> bool:
2015-12-04 16:51:02 +01:00
return self.buffer.isatty()
def __repr__(self):
2021-10-10 03:31:57 +02:00
return f"<ConsoleStream name={self.name!r} encoding={self.encoding!r}>"
2015-12-04 16:51:02 +01:00
2020-07-21 08:23:42 +02:00
2021-10-10 03:31:57 +02:00
def _get_text_stdin(buffer_stream: t.BinaryIO) -> t.TextIO:
2015-12-04 16:51:02 +01:00
text_stream = _NonClosingTextIOWrapper(
io.BufferedReader(_WindowsConsoleReader(STDIN_HANDLE)),
2020-07-21 08:23:42 +02:00
"utf-16-le",
"strict",
line_buffering=True,
)
2021-10-10 03:31:57 +02:00
return t.cast(t.TextIO, ConsoleStream(text_stream, buffer_stream))
2015-12-04 16:51:02 +01:00
2021-10-10 03:31:57 +02:00
def _get_text_stdout(buffer_stream: t.BinaryIO) -> t.TextIO:
2015-12-04 16:51:02 +01:00
text_stream = _NonClosingTextIOWrapper(
2019-01-07 17:51:19 +01:00
io.BufferedWriter(_WindowsConsoleWriter(STDOUT_HANDLE)),
2020-07-21 08:23:42 +02:00
"utf-16-le",
"strict",
line_buffering=True,
)
2021-10-10 03:31:57 +02:00
return t.cast(t.TextIO, ConsoleStream(text_stream, buffer_stream))
2015-12-04 16:51:02 +01:00
2021-10-10 03:31:57 +02:00
def _get_text_stderr(buffer_stream: t.BinaryIO) -> t.TextIO:
2015-12-04 16:51:02 +01:00
text_stream = _NonClosingTextIOWrapper(
2019-01-07 17:51:19 +01:00
io.BufferedWriter(_WindowsConsoleWriter(STDERR_HANDLE)),
2020-07-21 08:23:42 +02:00
"utf-16-le",
"strict",
line_buffering=True,
)
2021-10-10 03:31:57 +02:00
return t.cast(t.TextIO, ConsoleStream(text_stream, buffer_stream))
2015-12-04 16:51:02 +01:00
2021-10-10 03:31:57 +02:00
_stream_factories: t.Mapping[int, t.Callable[[t.BinaryIO], t.TextIO]] = {
2015-12-04 16:51:02 +01:00
0: _get_text_stdin,
1: _get_text_stdout,
2: _get_text_stderr,
}
2021-10-10 03:31:57 +02:00
def _is_console(f: t.TextIO) -> bool:
2020-07-21 08:23:42 +02:00
if not hasattr(f, "fileno"):
return False
try:
fileno = f.fileno()
2021-10-10 03:31:57 +02:00
except (OSError, io.UnsupportedOperation):
2020-07-21 08:23:42 +02:00
return False
handle = msvcrt.get_osfhandle(fileno)
return bool(GetConsoleMode(handle, byref(DWORD())))
2021-10-10 03:31:57 +02:00
def _get_windows_console_stream(
f: t.TextIO, encoding: t.Optional[str], errors: t.Optional[str]
) -> t.Optional[t.TextIO]:
2020-07-21 08:23:42 +02:00
if (
get_buffer is not None
2021-10-10 03:31:57 +02:00
and encoding in {"utf-16-le", None}
and errors in {"strict", None}
2020-07-21 08:23:42 +02:00
and _is_console(f)
):
2015-12-04 16:51:02 +01:00
func = _stream_factories.get(f.fileno())
if func is not None:
2021-10-10 03:31:57 +02:00
b = getattr(f, "buffer", None)
if b is None:
return None
return func(b)