]> jfr.im git - yt-dlp.git/blame - youtube_dl/compat.py
[util] Move compatibility functions out of util
[yt-dlp.git] / youtube_dl / compat.py
CommitLineData
8c25f81b
PH
1import getpass
2import os
3import subprocess
4import sys
5
6
7try:
8 import urllib.request as compat_urllib_request
9except ImportError: # Python 2
10 import urllib2 as compat_urllib_request
11
12try:
13 import urllib.error as compat_urllib_error
14except ImportError: # Python 2
15 import urllib2 as compat_urllib_error
16
17try:
18 import urllib.parse as compat_urllib_parse
19except ImportError: # Python 2
20 import urllib as compat_urllib_parse
21
22try:
23 from urllib.parse import urlparse as compat_urllib_parse_urlparse
24except ImportError: # Python 2
25 from urlparse import urlparse as compat_urllib_parse_urlparse
26
27try:
28 import urllib.parse as compat_urlparse
29except ImportError: # Python 2
30 import urlparse as compat_urlparse
31
32try:
33 import http.cookiejar as compat_cookiejar
34except ImportError: # Python 2
35 import cookielib as compat_cookiejar
36
37try:
38 import html.entities as compat_html_entities
39except ImportError: # Python 2
40 import htmlentitydefs as compat_html_entities
41
42try:
43 import html.parser as compat_html_parser
44except ImportError: # Python 2
45 import HTMLParser as compat_html_parser
46
47try:
48 import http.client as compat_http_client
49except ImportError: # Python 2
50 import httplib as compat_http_client
51
52try:
53 from urllib.error import HTTPError as compat_HTTPError
54except ImportError: # Python 2
55 from urllib2 import HTTPError as compat_HTTPError
56
57try:
58 from urllib.request import urlretrieve as compat_urlretrieve
59except ImportError: # Python 2
60 from urllib import urlretrieve as compat_urlretrieve
61
62
63try:
64 from subprocess import DEVNULL
65 compat_subprocess_get_DEVNULL = lambda: DEVNULL
66except ImportError:
67 compat_subprocess_get_DEVNULL = lambda: open(os.path.devnull, 'w')
68
69try:
70 from urllib.parse import unquote as compat_urllib_parse_unquote
71except ImportError:
72 def compat_urllib_parse_unquote(string, encoding='utf-8', errors='replace'):
73 if string == '':
74 return string
75 res = string.split('%')
76 if len(res) == 1:
77 return string
78 if encoding is None:
79 encoding = 'utf-8'
80 if errors is None:
81 errors = 'replace'
82 # pct_sequence: contiguous sequence of percent-encoded bytes, decoded
83 pct_sequence = b''
84 string = res[0]
85 for item in res[1:]:
86 try:
87 if not item:
88 raise ValueError
89 pct_sequence += item[:2].decode('hex')
90 rest = item[2:]
91 if not rest:
92 # This segment was just a single percent-encoded character.
93 # May be part of a sequence of code units, so delay decoding.
94 # (Stored in pct_sequence).
95 continue
96 except ValueError:
97 rest = '%' + item
98 # Encountered non-percent-encoded characters. Flush the current
99 # pct_sequence.
100 string += pct_sequence.decode(encoding, errors) + rest
101 pct_sequence = b''
102 if pct_sequence:
103 # Flush the final pct_sequence
104 string += pct_sequence.decode(encoding, errors)
105 return string
106
107
108try:
109 from urllib.parse import parse_qs as compat_parse_qs
110except ImportError: # Python 2
111 # HACK: The following is the correct parse_qs implementation from cpython 3's stdlib.
112 # Python 2's version is apparently totally broken
113
114 def _parse_qsl(qs, keep_blank_values=False, strict_parsing=False,
115 encoding='utf-8', errors='replace'):
116 qs, _coerce_result = qs, unicode
117 pairs = [s2 for s1 in qs.split('&') for s2 in s1.split(';')]
118 r = []
119 for name_value in pairs:
120 if not name_value and not strict_parsing:
121 continue
122 nv = name_value.split('=', 1)
123 if len(nv) != 2:
124 if strict_parsing:
125 raise ValueError("bad query field: %r" % (name_value,))
126 # Handle case of a control-name with no equal sign
127 if keep_blank_values:
128 nv.append('')
129 else:
130 continue
131 if len(nv[1]) or keep_blank_values:
132 name = nv[0].replace('+', ' ')
133 name = compat_urllib_parse_unquote(
134 name, encoding=encoding, errors=errors)
135 name = _coerce_result(name)
136 value = nv[1].replace('+', ' ')
137 value = compat_urllib_parse_unquote(
138 value, encoding=encoding, errors=errors)
139 value = _coerce_result(value)
140 r.append((name, value))
141 return r
142
143 def compat_parse_qs(qs, keep_blank_values=False, strict_parsing=False,
144 encoding='utf-8', errors='replace'):
145 parsed_result = {}
146 pairs = _parse_qsl(qs, keep_blank_values, strict_parsing,
147 encoding=encoding, errors=errors)
148 for name, value in pairs:
149 if name in parsed_result:
150 parsed_result[name].append(value)
151 else:
152 parsed_result[name] = [value]
153 return parsed_result
154
155try:
156 compat_str = unicode # Python 2
157except NameError:
158 compat_str = str
159
160try:
161 compat_chr = unichr # Python 2
162except NameError:
163 compat_chr = chr
164
165try:
166 from xml.etree.ElementTree import ParseError as compat_xml_parse_error
167except ImportError: # Python 2.6
168 from xml.parsers.expat import ExpatError as compat_xml_parse_error
169
170try:
171 from shlex import quote as shlex_quote
172except ImportError: # Python < 3.3
173 def shlex_quote(s):
174 return "'" + s.replace("'", "'\"'\"'") + "'"
175
176
177def compat_ord(c):
178 if type(c) is int: return c
179 else: return ord(c)
180
181
182if sys.version_info >= (3, 0):
183 compat_getenv = os.getenv
184 compat_expanduser = os.path.expanduser
185else:
186 # Environment variables should be decoded with filesystem encoding.
187 # Otherwise it will fail if any non-ASCII characters present (see #3854 #3217 #2918)
188
189 def compat_getenv(key, default=None):
190 from .utils import get_filesystem_encoding
191 env = os.getenv(key, default)
192 if env:
193 env = env.decode(get_filesystem_encoding())
194 return env
195
196 # HACK: The default implementations of os.path.expanduser from cpython do not decode
197 # environment variables with filesystem encoding. We will work around this by
198 # providing adjusted implementations.
199 # The following are os.path.expanduser implementations from cpython 2.7.8 stdlib
200 # for different platforms with correct environment variables decoding.
201
202 if os.name == 'posix':
203 def compat_expanduser(path):
204 """Expand ~ and ~user constructions. If user or $HOME is unknown,
205 do nothing."""
206 if not path.startswith('~'):
207 return path
208 i = path.find('/', 1)
209 if i < 0:
210 i = len(path)
211 if i == 1:
212 if 'HOME' not in os.environ:
213 import pwd
214 userhome = pwd.getpwuid(os.getuid()).pw_dir
215 else:
216 userhome = compat_getenv('HOME')
217 else:
218 import pwd
219 try:
220 pwent = pwd.getpwnam(path[1:i])
221 except KeyError:
222 return path
223 userhome = pwent.pw_dir
224 userhome = userhome.rstrip('/')
225 return (userhome + path[i:]) or '/'
226 elif os.name == 'nt' or os.name == 'ce':
227 def compat_expanduser(path):
228 """Expand ~ and ~user constructs.
229
230 If user or $HOME is unknown, do nothing."""
231 if path[:1] != '~':
232 return path
233 i, n = 1, len(path)
234 while i < n and path[i] not in '/\\':
235 i = i + 1
236
237 if 'HOME' in os.environ:
238 userhome = compat_getenv('HOME')
239 elif 'USERPROFILE' in os.environ:
240 userhome = compat_getenv('USERPROFILE')
241 elif not 'HOMEPATH' in os.environ:
242 return path
243 else:
244 try:
245 drive = compat_getenv('HOMEDRIVE')
246 except KeyError:
247 drive = ''
248 userhome = os.path.join(drive, compat_getenv('HOMEPATH'))
249
250 if i != 1: #~user
251 userhome = os.path.join(os.path.dirname(userhome), path[1:i])
252
253 return userhome + path[i:]
254 else:
255 compat_expanduser = os.path.expanduser
256
257
258if sys.version_info < (3, 0):
259 def compat_print(s):
260 from .utils import preferredencoding
261 print(s.encode(preferredencoding(), 'xmlcharrefreplace'))
262else:
263 def compat_print(s):
264 assert type(s) == type(u'')
265 print(s)
266
267
268try:
269 subprocess_check_output = subprocess.check_output
270except AttributeError:
271 def subprocess_check_output(*args, **kwargs):
272 assert 'input' not in kwargs
273 p = subprocess.Popen(*args, stdout=subprocess.PIPE, **kwargs)
274 output, _ = p.communicate()
275 ret = p.poll()
276 if ret:
277 raise subprocess.CalledProcessError(ret, p.args, output=output)
278 return output
279
280if sys.version_info < (3, 0) and sys.platform == 'win32':
281 def compat_getpass(prompt, *args, **kwargs):
282 if isinstance(prompt, compat_str):
283 prompt = prompt.encode(preferredencoding())
284 return getpass.getpass(prompt, *args, **kwargs)
285else:
286 compat_getpass = getpass.getpass
287
288
289__all__ = [
290 'compat_HTTPError',
291 'compat_chr',
292 'compat_cookiejar',
293 'compat_expanduser',
294 'compat_getenv',
295 'compat_getpass',
296 'compat_html_entities',
297 'compat_html_parser',
298 'compat_http_client',
299 'compat_ord',
300 'compat_parse_qs',
301 'compat_print',
302 'compat_str',
303 'compat_subprocess_get_DEVNULL',
304 'compat_urllib_error',
305 'compat_urllib_parse',
306 'compat_urllib_parse_unquote',
307 'compat_urllib_parse_urlparse',
308 'compat_urllib_request',
309 'compat_urlparse',
310 'compat_urlretrieve',
311 'compat_xml_parse_error',
312 'shlex_quote',
313 'subprocess_check_output',
314]