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