_io.py 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  1. # -*- test-case-name: twisted.logger.test.test_io -*-
  2. # Copyright (c) Twisted Matrix Laboratories.
  3. # See LICENSE for details.
  4. """
  5. File-like object that logs.
  6. """
  7. import sys
  8. from typing import AnyStr, Iterable, Optional
  9. from constantly import NamedConstant
  10. from incremental import Version
  11. from twisted.python.deprecate import deprecatedProperty
  12. from ._levels import LogLevel
  13. from ._logger import Logger
  14. class LoggingFile:
  15. """
  16. File-like object that turns C{write()} calls into logging events.
  17. Note that because event formats are L{str}, C{bytes} received via C{write()}
  18. are converted to C{str}, which is the opposite of what C{file} does.
  19. @ivar softspace: Attribute to make this class more file-like under Python 2;
  20. value is zero or one. Do not use.
  21. """
  22. _softspace = 0
  23. @deprecatedProperty(Version("Twisted", 21, 2, 0))
  24. def softspace(self):
  25. return self._softspace
  26. @softspace.setter # type: ignore[no-redef]
  27. def softspace(self, value):
  28. self._softspace = value
  29. def __init__(
  30. self,
  31. logger: Logger,
  32. level: NamedConstant = LogLevel.info,
  33. encoding: Optional[str] = None,
  34. ) -> None:
  35. """
  36. @param logger: the logger to log through.
  37. @param level: the log level to emit events with.
  38. @param encoding: The encoding to expect when receiving bytes via
  39. C{write()}. If L{None}, use C{sys.getdefaultencoding()}.
  40. """
  41. self.level = level
  42. self.log = logger
  43. if encoding is None:
  44. self._encoding = sys.getdefaultencoding()
  45. else:
  46. self._encoding = encoding
  47. self._buffer = ""
  48. self._closed = False
  49. @property
  50. def closed(self) -> bool:
  51. """
  52. Read-only property. Is the file closed?
  53. @return: true if closed, otherwise false.
  54. """
  55. return self._closed
  56. @property
  57. def encoding(self) -> str:
  58. """
  59. Read-only property. File encoding.
  60. @return: an encoding.
  61. """
  62. return self._encoding
  63. @property
  64. def mode(self) -> str:
  65. """
  66. Read-only property. File mode.
  67. @return: "w"
  68. """
  69. return "w"
  70. @property
  71. def newlines(self) -> None:
  72. """
  73. Read-only property. Types of newlines encountered.
  74. @return: L{None}
  75. """
  76. return None
  77. @property
  78. def name(self) -> str:
  79. """
  80. The name of this file; a repr-style string giving information about its
  81. namespace.
  82. @return: A file name.
  83. """
  84. return "<{} {}#{}>".format(
  85. self.__class__.__name__,
  86. self.log.namespace,
  87. self.level.name,
  88. )
  89. def close(self) -> None:
  90. """
  91. Close this file so it can no longer be written to.
  92. """
  93. self._closed = True
  94. def flush(self) -> None:
  95. """
  96. No-op; this file does not buffer.
  97. """
  98. pass
  99. def fileno(self) -> int:
  100. """
  101. Returns an invalid file descriptor, since this is not backed by an FD.
  102. @return: C{-1}
  103. """
  104. return -1
  105. def isatty(self) -> bool:
  106. """
  107. A L{LoggingFile} is not a TTY.
  108. @return: C{False}
  109. """
  110. return False
  111. def write(self, message: AnyStr) -> None:
  112. """
  113. Log the given message.
  114. @param message: The message to write.
  115. """
  116. if self._closed:
  117. raise ValueError("I/O operation on closed file")
  118. if isinstance(message, bytes):
  119. text = message.decode(self._encoding)
  120. else:
  121. text = message
  122. lines = (self._buffer + text).split("\n")
  123. self._buffer = lines[-1]
  124. lines = lines[0:-1]
  125. for line in lines:
  126. self.log.emit(self.level, format="{log_io}", log_io=line)
  127. def writelines(self, lines: Iterable[AnyStr]) -> None:
  128. """
  129. Log each of the given lines as a separate message.
  130. @param lines: Data to write.
  131. """
  132. for line in lines:
  133. self.write(line)
  134. def _unsupported(self, *args: object) -> None:
  135. """
  136. Template for unsupported operations.
  137. @param args: Arguments.
  138. """
  139. raise OSError("unsupported operation")
  140. read = _unsupported
  141. next = _unsupported
  142. readline = _unsupported
  143. readlines = _unsupported
  144. xreadlines = _unsupported
  145. seek = _unsupported
  146. tell = _unsupported
  147. truncate = _unsupported