openpy.py 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106
  1. """
  2. Tools to open .py files as Unicode, using the encoding specified within the file,
  3. as per PEP 263.
  4. Much of the code is taken from the tokenize module in Python 3.2.
  5. """
  6. from __future__ import annotations
  7. import io
  8. from collections.abc import Generator, Iterable
  9. from io import TextIOWrapper, BytesIO
  10. from pathlib import Path
  11. import re
  12. from tokenize import open, detect_encoding
  13. cookie_re = re.compile(r"coding[:=]\s*([-\w.]+)", re.UNICODE)
  14. cookie_comment_re = re.compile(r"^\s*#.*coding[:=]\s*([-\w.]+)", re.UNICODE)
  15. def source_to_unicode(txt: str | bytes | BytesIO, errors: str = 'replace', skip_encoding_cookie: bool = True) -> str:
  16. """Converts a bytes string with python source code to unicode.
  17. Unicode strings are passed through unchanged. Byte strings are checked
  18. for the python source file encoding cookie to determine encoding.
  19. txt can be either a bytes buffer or a string containing the source
  20. code.
  21. """
  22. if isinstance(txt, str):
  23. return txt
  24. if isinstance(txt, bytes):
  25. buffer = BytesIO(txt)
  26. else:
  27. buffer = txt
  28. try:
  29. encoding, _ = detect_encoding(buffer.readline)
  30. except SyntaxError:
  31. encoding = "ascii"
  32. buffer.seek(0)
  33. with TextIOWrapper(buffer, encoding, errors=errors, line_buffering=True) as text:
  34. text.mode = 'r'
  35. if skip_encoding_cookie:
  36. return u"".join(strip_encoding_cookie(text))
  37. else:
  38. return text.read()
  39. def strip_encoding_cookie(filelike: Iterable[str]) -> Generator[str, None, None]:
  40. """Generator to pull lines from a text-mode file, skipping the encoding
  41. cookie if it is found in the first two lines.
  42. """
  43. it = iter(filelike)
  44. try:
  45. first = next(it)
  46. if not cookie_comment_re.match(first):
  47. yield first
  48. second = next(it)
  49. if not cookie_comment_re.match(second):
  50. yield second
  51. except StopIteration:
  52. return
  53. yield from it
  54. def read_py_file(filename: str | Path, skip_encoding_cookie: bool = True) -> str:
  55. """Read a Python file, using the encoding declared inside the file.
  56. Parameters
  57. ----------
  58. filename : str
  59. The path to the file to read.
  60. skip_encoding_cookie : bool
  61. If True (the default), and the encoding declaration is found in the first
  62. two lines, that line will be excluded from the output.
  63. Returns
  64. -------
  65. A unicode string containing the contents of the file.
  66. """
  67. filepath = Path(filename)
  68. with open(filepath) as f: # the open function defined in this module.
  69. if skip_encoding_cookie:
  70. return "".join(strip_encoding_cookie(f))
  71. else:
  72. return f.read()
  73. def read_py_url(url: str, errors: str = 'replace', skip_encoding_cookie: bool = True) -> str:
  74. """Read a Python file from a URL, using the encoding declared inside the file.
  75. Parameters
  76. ----------
  77. url : str
  78. The URL from which to fetch the file.
  79. errors : str
  80. How to handle decoding errors in the file. Options are the same as for
  81. bytes.decode(), but here 'replace' is the default.
  82. skip_encoding_cookie : bool
  83. If True (the default), and the encoding declaration is found in the first
  84. two lines, that line will be excluded from the output.
  85. Returns
  86. -------
  87. A unicode string containing the contents of the file.
  88. """
  89. # Deferred import for faster start
  90. from urllib.request import urlopen
  91. response = urlopen(url)
  92. buffer = io.BytesIO(response.read())
  93. return source_to_unicode(buffer, errors, skip_encoding_cookie)