fix_encoding.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322
  1. # Copyright (c) 2011 The Chromium Authors. All rights reserved.
  2. # Use of this source code is governed by a BSD-style license that can be
  3. # found in the LICENSE file.
  4. """Collection of functions and classes to fix various encoding problems on
  5. multiple platforms with python.
  6. """
  7. import codecs
  8. import locale
  9. import os
  10. import sys
  11. def complain(message):
  12. """If any exception occurs in this file, we'll probably try to print it
  13. on stderr, which makes for frustrating debugging if stderr is directed
  14. to our wrapper. So be paranoid about catching errors and reporting them
  15. to sys.__stderr__, so that the user has a higher chance to see them.
  16. """
  17. print(isinstance(message, str) and message or repr(message),
  18. file=sys.__stderr__)
  19. def fix_default_encoding():
  20. """Forces utf8 solidly on all platforms.
  21. By default python execution environment is lazy and defaults to ascii
  22. encoding.
  23. http://uucode.com/blog/2007/03/23/shut-up-you-dummy-7-bit-python/
  24. """
  25. if sys.getdefaultencoding() == 'utf-8':
  26. return False
  27. # Regenerate setdefaultencoding.
  28. reload(sys)
  29. # Module 'sys' has no 'setdefaultencoding' member
  30. # pylint: disable=no-member
  31. sys.setdefaultencoding('utf-8')
  32. for attr in dir(locale):
  33. if attr[0:3] != 'LC_':
  34. continue
  35. aref = getattr(locale, attr)
  36. try:
  37. locale.setlocale(aref, '')
  38. except locale.Error:
  39. continue
  40. try:
  41. lang, _ = locale.getdefaultlocale()
  42. except (TypeError, ValueError):
  43. continue
  44. if lang:
  45. try:
  46. locale.setlocale(aref, (lang, 'UTF-8'))
  47. except locale.Error:
  48. os.environ[attr] = lang + '.UTF-8'
  49. try:
  50. locale.setlocale(locale.LC_ALL, '')
  51. except locale.Error:
  52. pass
  53. return True
  54. ###############################
  55. # Windows specific
  56. def fix_win_codec():
  57. """Works around <http://bugs.python.org/issue6058>."""
  58. # <http://msdn.microsoft.com/en-us/library/dd317756.aspx>
  59. try:
  60. codecs.lookup('cp65001')
  61. return False
  62. except LookupError:
  63. codecs.register(
  64. lambda name: name == 'cp65001' and codecs.lookup('utf-8') or None)
  65. return True
  66. class WinUnicodeOutputBase(object):
  67. """Base class to adapt sys.stdout or sys.stderr to behave correctly on
  68. Windows.
  69. Setting encoding to utf-8 is recommended.
  70. """
  71. def __init__(self, fileno, name, encoding):
  72. # Corresponding file handle.
  73. self._fileno = fileno
  74. self.encoding = encoding
  75. self.name = name
  76. self.closed = False
  77. self.softspace = False
  78. self.mode = 'w'
  79. @staticmethod
  80. def isatty():
  81. return False
  82. def close(self):
  83. # Don't really close the handle, that would only cause problems.
  84. self.closed = True
  85. def fileno(self):
  86. return self._fileno
  87. def flush(self):
  88. raise NotImplementedError()
  89. def write(self, text):
  90. raise NotImplementedError()
  91. def writelines(self, lines):
  92. try:
  93. for line in lines:
  94. self.write(line)
  95. except Exception as e:
  96. complain('%s.writelines: %r' % (self.name, e))
  97. raise
  98. class WinUnicodeConsoleOutput(WinUnicodeOutputBase):
  99. """Output adapter to a Windows Console.
  100. Understands how to use the win32 console API.
  101. """
  102. def __init__(self, console_handle, fileno, stream_name, encoding):
  103. super(WinUnicodeConsoleOutput,
  104. self).__init__(fileno, '<Unicode console %s>' % stream_name,
  105. encoding)
  106. # Handle to use for WriteConsoleW
  107. self._console_handle = console_handle
  108. # Loads the necessary function.
  109. # These types are available on linux but not Mac.
  110. # pylint: disable=no-name-in-module,F0401
  111. from ctypes import byref, GetLastError, POINTER, windll, WINFUNCTYPE
  112. from ctypes.wintypes import BOOL, DWORD, HANDLE, LPWSTR
  113. from ctypes.wintypes import LPVOID # pylint: disable=no-name-in-module
  114. self._DWORD = DWORD
  115. self._byref = byref
  116. # <http://msdn.microsoft.com/en-us/library/ms687401.aspx>
  117. self._WriteConsoleW = WINFUNCTYPE(BOOL, HANDLE, LPWSTR, DWORD,
  118. POINTER(DWORD),
  119. LPVOID)(('WriteConsoleW',
  120. windll.kernel32))
  121. self._GetLastError = GetLastError
  122. def flush(self):
  123. # No need to flush the console since it's immediate.
  124. pass
  125. def write(self, text):
  126. try:
  127. if isinstance(text, bytes):
  128. # Bytestrings need to be decoded to a string before being passed
  129. # to Windows.
  130. text = text.decode(self.encoding, 'replace')
  131. remaining = len(text)
  132. while remaining > 0:
  133. n = self._DWORD(0)
  134. # There is a shorter-than-documented limitation on the length of
  135. # the string passed to WriteConsoleW. See
  136. # <http://tahoe-lafs.org/trac/tahoe-lafs/ticket/1232>.
  137. retval = self._WriteConsoleW(self._console_handle, text,
  138. min(remaining, 10000),
  139. self._byref(n), None)
  140. if retval == 0 or n.value == 0:
  141. raise IOError('WriteConsoleW returned %r, n.value = %r, '
  142. 'last error = %r' %
  143. (retval, n.value, self._GetLastError()))
  144. remaining -= n.value
  145. if not remaining:
  146. break
  147. text = text[int(n.value):]
  148. except Exception as e:
  149. complain('%s.write: %r' % (self.name, e))
  150. raise
  151. class WinUnicodeOutput(WinUnicodeOutputBase):
  152. """Output adaptor to a file output on Windows.
  153. If the standard FileWrite function is used, it will be encoded in the current
  154. code page. WriteConsoleW() permits writing any character.
  155. """
  156. def __init__(self, stream, fileno, encoding):
  157. super(WinUnicodeOutput,
  158. self).__init__(fileno, '<Unicode redirected %s>' % stream.name,
  159. encoding)
  160. # Output stream
  161. self._stream = stream
  162. # Flush right now.
  163. self.flush()
  164. def flush(self):
  165. try:
  166. self._stream.flush()
  167. except Exception as e:
  168. complain('%s.flush: %r from %r' % (self.name, e, self._stream))
  169. raise
  170. def write(self, text):
  171. try:
  172. if isinstance(text, bytes):
  173. # Replace characters that cannot be printed instead of failing.
  174. text = text.decode(self.encoding, 'replace')
  175. # When redirecting to a file or process any \n characters will be
  176. # replaced with \r\n. If the text to be printed already has \r\n
  177. # line endings then \r\r\n line endings will be generated, leading
  178. # to double-spacing of some output. Normalizing line endings to \n
  179. # avoids this problem.
  180. text = text.replace('\r\n', '\n')
  181. self._stream.write(text)
  182. except Exception as e:
  183. complain('%s.write: %r' % (self.name, e))
  184. raise
  185. def win_handle_is_a_console(handle):
  186. """Returns True if a Windows file handle is a handle to a console."""
  187. # These types are available on linux but not Mac.
  188. # pylint: disable=no-name-in-module,F0401
  189. from ctypes import byref, POINTER, windll, WINFUNCTYPE
  190. from ctypes.wintypes import BOOL, DWORD, HANDLE
  191. FILE_TYPE_CHAR = 0x0002
  192. FILE_TYPE_REMOTE = 0x8000
  193. INVALID_HANDLE_VALUE = DWORD(-1).value
  194. # <http://msdn.microsoft.com/en-us/library/ms683167.aspx>
  195. GetConsoleMode = WINFUNCTYPE(BOOL, HANDLE, POINTER(DWORD))(
  196. ('GetConsoleMode', windll.kernel32))
  197. # <http://msdn.microsoft.com/en-us/library/aa364960.aspx>
  198. GetFileType = WINFUNCTYPE(DWORD, DWORD)(('GetFileType', windll.kernel32))
  199. # GetStdHandle returns INVALID_HANDLE_VALUE, NULL, or a valid handle.
  200. if handle == INVALID_HANDLE_VALUE or handle is None:
  201. return False
  202. return ((GetFileType(handle) & ~FILE_TYPE_REMOTE) == FILE_TYPE_CHAR
  203. and GetConsoleMode(handle, byref(DWORD())))
  204. def win_get_unicode_stream(stream, excepted_fileno, output_handle, encoding):
  205. """Returns a unicode-compatible stream.
  206. This function will return a direct-Console writing object only if:
  207. - the file number is the expected console file number
  208. - the handle the expected file handle
  209. - the 'real' handle is in fact a handle to a console.
  210. """
  211. old_fileno = getattr(stream, 'fileno', lambda: None)()
  212. if old_fileno == excepted_fileno:
  213. # These types are available on linux but not Mac.
  214. # pylint: disable=no-name-in-module,F0401
  215. from ctypes import windll, WINFUNCTYPE
  216. from ctypes.wintypes import DWORD, HANDLE
  217. # <http://msdn.microsoft.com/en-us/library/ms683231.aspx>
  218. GetStdHandle = WINFUNCTYPE(HANDLE,
  219. DWORD)(('GetStdHandle', windll.kernel32))
  220. real_output_handle = GetStdHandle(DWORD(output_handle))
  221. if win_handle_is_a_console(real_output_handle):
  222. # It's a console.
  223. return WinUnicodeConsoleOutput(real_output_handle, old_fileno,
  224. stream.name, encoding)
  225. # It's something else. Create an auto-encoding stream.
  226. return WinUnicodeOutput(stream, old_fileno, encoding)
  227. def fix_win_console(encoding):
  228. """Makes Unicode console output work independently of the current code page.
  229. This also fixes <http://bugs.python.org/issue1602>.
  230. Credit to Michael Kaplan
  231. <http://blogs.msdn.com/b/michkap/archive/2010/04/07/9989346.aspx> and
  232. TZOmegaTZIOY
  233. <http://stackoverflow.com/questions/878972/windows-cmd-encoding-change-causes-python-crash/1432462#1432462>.
  234. """
  235. if (isinstance(sys.stdout, WinUnicodeOutputBase)
  236. or isinstance(sys.stderr, WinUnicodeOutputBase)):
  237. return False
  238. try:
  239. # SetConsoleCP and SetConsoleOutputCP could be used to change the code
  240. # page but it's not really useful since the code here is using
  241. # WriteConsoleW(). Also, changing the code page is 'permanent' to the
  242. # console and needs to be reverted manually. In practice one needs to
  243. # set the console font to a TTF font to be able to see all the
  244. # characters but it failed for me in practice. In any case, it won't
  245. # throw any exception when printing, which is the important part. -11
  246. # and -12 are defined in stdio.h
  247. sys.stdout = win_get_unicode_stream(sys.stdout, 1, -11, encoding)
  248. sys.stderr = win_get_unicode_stream(sys.stderr, 2, -12, encoding)
  249. # TODO(maruel): Do sys.stdin with ReadConsoleW(). Albeit the limitation
  250. # is "It doesn't appear to be possible to read Unicode characters in
  251. # UTF-8 mode" and this appears to be a limitation of cmd.exe.
  252. except Exception as e:
  253. complain('exception %r while fixing up sys.stdout and sys.stderr' % e)
  254. return True
  255. def fix_encoding():
  256. """Fixes various encoding problems on all platforms.
  257. Should be called at the very beginning of the process.
  258. """
  259. ret = True
  260. if sys.platform == 'win32':
  261. ret &= fix_win_codec()
  262. ret &= fix_default_encoding()
  263. if sys.platform == 'win32':
  264. encoding = sys.getdefaultencoding()
  265. ret &= fix_win_console(encoding)
  266. return ret