posix_pipe.py 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118
  1. from __future__ import annotations
  2. import sys
  3. assert sys.platform != "win32"
  4. import os
  5. from contextlib import contextmanager
  6. from typing import ContextManager, Iterator, TextIO, cast
  7. from ..utils import DummyContext
  8. from .base import PipeInput
  9. from .vt100 import Vt100Input
  10. __all__ = [
  11. "PosixPipeInput",
  12. ]
  13. class _Pipe:
  14. "Wrapper around os.pipe, that ensures we don't double close any end."
  15. def __init__(self) -> None:
  16. self.read_fd, self.write_fd = os.pipe()
  17. self._read_closed = False
  18. self._write_closed = False
  19. def close_read(self) -> None:
  20. "Close read-end if not yet closed."
  21. if self._read_closed:
  22. return
  23. os.close(self.read_fd)
  24. self._read_closed = True
  25. def close_write(self) -> None:
  26. "Close write-end if not yet closed."
  27. if self._write_closed:
  28. return
  29. os.close(self.write_fd)
  30. self._write_closed = True
  31. def close(self) -> None:
  32. "Close both read and write ends."
  33. self.close_read()
  34. self.close_write()
  35. class PosixPipeInput(Vt100Input, PipeInput):
  36. """
  37. Input that is send through a pipe.
  38. This is useful if we want to send the input programmatically into the
  39. application. Mostly useful for unit testing.
  40. Usage::
  41. with PosixPipeInput.create() as input:
  42. input.send_text('inputdata')
  43. """
  44. _id = 0
  45. def __init__(self, _pipe: _Pipe, _text: str = "") -> None:
  46. # Private constructor. Users should use the public `.create()` method.
  47. self.pipe = _pipe
  48. class Stdin:
  49. encoding = "utf-8"
  50. def isatty(stdin) -> bool:
  51. return True
  52. def fileno(stdin) -> int:
  53. return self.pipe.read_fd
  54. super().__init__(cast(TextIO, Stdin()))
  55. self.send_text(_text)
  56. # Identifier for every PipeInput for the hash.
  57. self.__class__._id += 1
  58. self._id = self.__class__._id
  59. @classmethod
  60. @contextmanager
  61. def create(cls, text: str = "") -> Iterator[PosixPipeInput]:
  62. pipe = _Pipe()
  63. try:
  64. yield PosixPipeInput(_pipe=pipe, _text=text)
  65. finally:
  66. pipe.close()
  67. def send_bytes(self, data: bytes) -> None:
  68. os.write(self.pipe.write_fd, data)
  69. def send_text(self, data: str) -> None:
  70. "Send text to the input."
  71. os.write(self.pipe.write_fd, data.encode("utf-8"))
  72. def raw_mode(self) -> ContextManager[None]:
  73. return DummyContext()
  74. def cooked_mode(self) -> ContextManager[None]:
  75. return DummyContext()
  76. def close(self) -> None:
  77. "Close pipe fds."
  78. # Only close the write-end of the pipe. This will unblock the reader
  79. # callback (in vt100.py > _attached_input), which eventually will raise
  80. # `EOFError`. If we'd also close the read-end, then the event loop
  81. # won't wake up the corresponding callback because of this.
  82. self.pipe.close_write()
  83. def typeahead_hash(self) -> str:
  84. """
  85. This needs to be unique for every `PipeInput`.
  86. """
  87. return f"pipe-input-{self._id}"