]> jfr.im git - yt-dlp.git/blame - youtube_dl/compat.py
Merge remote-tracking branch 'yan12125/download-dash-segments' (#5886)
[yt-dlp.git] / youtube_dl / compat.py
CommitLineData
451948b2
PH
1from __future__ import unicode_literals
2
003c69a8 3import collections
8c25f81b 4import getpass
e07e9313 5import optparse
8c25f81b 6import os
7d4111ed 7import re
003c69a8 8import shutil
be4a824d 9import socket
8c25f81b
PH
10import subprocess
11import sys
a0e060ac 12import itertools
8c25f81b
PH
13
14
15try:
16 import urllib.request as compat_urllib_request
5f6a1245 17except ImportError: # Python 2
8c25f81b
PH
18 import urllib2 as compat_urllib_request
19
20try:
21 import urllib.error as compat_urllib_error
5f6a1245 22except ImportError: # Python 2
8c25f81b
PH
23 import urllib2 as compat_urllib_error
24
25try:
26 import urllib.parse as compat_urllib_parse
5f6a1245 27except ImportError: # Python 2
8c25f81b
PH
28 import urllib as compat_urllib_parse
29
30try:
31 from urllib.parse import urlparse as compat_urllib_parse_urlparse
5f6a1245 32except ImportError: # Python 2
8c25f81b
PH
33 from urlparse import urlparse as compat_urllib_parse_urlparse
34
35try:
36 import urllib.parse as compat_urlparse
5f6a1245 37except ImportError: # Python 2
8c25f81b
PH
38 import urlparse as compat_urlparse
39
40try:
41 import http.cookiejar as compat_cookiejar
5f6a1245 42except ImportError: # Python 2
8c25f81b
PH
43 import cookielib as compat_cookiejar
44
45try:
46 import html.entities as compat_html_entities
5f6a1245 47except ImportError: # Python 2
8c25f81b
PH
48 import htmlentitydefs as compat_html_entities
49
8c25f81b
PH
50try:
51 import http.client as compat_http_client
5f6a1245 52except ImportError: # Python 2
8c25f81b
PH
53 import httplib as compat_http_client
54
55try:
56 from urllib.error import HTTPError as compat_HTTPError
57except ImportError: # Python 2
58 from urllib2 import HTTPError as compat_HTTPError
59
60try:
61 from urllib.request import urlretrieve as compat_urlretrieve
62except ImportError: # Python 2
63 from urllib import urlretrieve as compat_urlretrieve
64
65
66try:
67 from subprocess import DEVNULL
68 compat_subprocess_get_DEVNULL = lambda: DEVNULL
69except ImportError:
70 compat_subprocess_get_DEVNULL = lambda: open(os.path.devnull, 'w')
71
83fda3c0
PH
72try:
73 import http.server as compat_http_server
74except ImportError:
75 import BaseHTTPServer as compat_http_server
76
8c25f81b 77try:
55139679 78 from urllib.parse import unquote_to_bytes as compat_urllib_parse_unquote_to_bytes
8c25f81b 79 from urllib.parse import unquote as compat_urllib_parse_unquote
aa99aa4e 80 from urllib.parse import unquote_plus as compat_urllib_parse_unquote_plus
55139679 81except ImportError: # Python 2
22603348
S
82 _asciire = (compat_urllib_parse._asciire if hasattr(compat_urllib_parse, '_asciire')
83 else re.compile('([\x00-\x7f]+)'))
3cc8b4c3 84
4d08161a 85 # HACK: The following are the correct unquote_to_bytes, unquote and unquote_plus
55139679
S
86 # implementations from cpython 3.4.3's stdlib. Python 2's version
87 # is apparently broken (see https://github.com/rg3/youtube-dl/pull/6244)
88
c9c854ce 89 def compat_urllib_parse_unquote_to_bytes(string):
90 """unquote_to_bytes('abc%20def') -> b'abc def'."""
91 # Note: strings are encoded as UTF-8. This is only an issue if it contains
92 # unescaped non-ASCII characters, which URIs should not.
93 if not string:
94 # Is it a string-like object?
95 string.split
96 return b''
55139679 97 if isinstance(string, unicode):
c9c854ce 98 string = string.encode('utf-8')
55139679 99 bits = string.split(b'%')
c9c854ce 100 if len(bits) == 1:
101 return string
102 res = [bits[0]]
103 append = res.append
c9c854ce 104 for item in bits[1:]:
105 try:
55139679 106 append(compat_urllib_parse._hextochr[item[:2]])
c9c854ce 107 append(item[2:])
55139679 108 except KeyError:
c9c854ce 109 append(b'%')
110 append(item)
111 return b''.join(res)
112
a0f28f90 113 def compat_urllib_parse_unquote(string, encoding='utf-8', errors='replace'):
c9c854ce 114 """Replace %xx escapes by their single-character equivalent. The optional
115 encoding and errors parameters specify how to decode percent-encoded
116 sequences into Unicode characters, as accepted by the bytes.decode()
117 method.
118 By default, percent-encoded sequences are decoded with UTF-8, and invalid
119 sequences are replaced by a placeholder character.
120
121 unquote('abc%20def') -> 'abc def'.
122 """
c9c854ce 123 if '%' not in string:
124 string.split
125 return string
126 if encoding is None:
127 encoding = 'utf-8'
128 if errors is None:
129 errors = 'replace'
3cc8b4c3 130 bits = _asciire.split(string)
c9c854ce 131 res = [bits[0]]
132 append = res.append
133 for i in range(1, len(bits), 2):
55139679
S
134 append(compat_urllib_parse_unquote_to_bytes(bits[i]).decode(encoding, errors))
135 append(bits[i + 1])
c9c854ce 136 return ''.join(res)
137
aa99aa4e
S
138 def compat_urllib_parse_unquote_plus(string, encoding='utf-8', errors='replace'):
139 """Like unquote(), but also replace plus signs by spaces, as required for
140 unquoting HTML form values.
141
142 unquote_plus('%7e/abc+def') -> '~/abc def'
143 """
144 string = string.replace('+', ' ')
145 return compat_urllib_parse_unquote(string, encoding, errors)
146
8f9312c3
PH
147try:
148 compat_str = unicode # Python 2
149except NameError:
150 compat_str = str
151
152try:
0196149c 153 compat_basestring = basestring # Python 2
8f9312c3 154except NameError:
0196149c 155 compat_basestring = str
8f9312c3
PH
156
157try:
158 compat_chr = unichr # Python 2
159except NameError:
160 compat_chr = chr
161
162try:
163 from xml.etree.ElementTree import ParseError as compat_xml_parse_error
164except ImportError: # Python 2.6
165 from xml.parsers.expat import ExpatError as compat_xml_parse_error
166
8c25f81b
PH
167
168try:
169 from urllib.parse import parse_qs as compat_parse_qs
5f6a1245 170except ImportError: # Python 2
8c25f81b
PH
171 # HACK: The following is the correct parse_qs implementation from cpython 3's stdlib.
172 # Python 2's version is apparently totally broken
173
174 def _parse_qsl(qs, keep_blank_values=False, strict_parsing=False,
9e1a5b84 175 encoding='utf-8', errors='replace'):
8f9312c3 176 qs, _coerce_result = qs, compat_str
8c25f81b
PH
177 pairs = [s2 for s1 in qs.split('&') for s2 in s1.split(';')]
178 r = []
179 for name_value in pairs:
180 if not name_value and not strict_parsing:
181 continue
182 nv = name_value.split('=', 1)
183 if len(nv) != 2:
184 if strict_parsing:
185 raise ValueError("bad query field: %r" % (name_value,))
186 # Handle case of a control-name with no equal sign
187 if keep_blank_values:
188 nv.append('')
189 else:
190 continue
191 if len(nv[1]) or keep_blank_values:
192 name = nv[0].replace('+', ' ')
193 name = compat_urllib_parse_unquote(
194 name, encoding=encoding, errors=errors)
195 name = _coerce_result(name)
196 value = nv[1].replace('+', ' ')
197 value = compat_urllib_parse_unquote(
198 value, encoding=encoding, errors=errors)
199 value = _coerce_result(value)
200 r.append((name, value))
201 return r
202
203 def compat_parse_qs(qs, keep_blank_values=False, strict_parsing=False,
9e1a5b84 204 encoding='utf-8', errors='replace'):
8c25f81b
PH
205 parsed_result = {}
206 pairs = _parse_qsl(qs, keep_blank_values, strict_parsing,
9e1a5b84 207 encoding=encoding, errors=errors)
8c25f81b
PH
208 for name, value in pairs:
209 if name in parsed_result:
210 parsed_result[name].append(value)
211 else:
212 parsed_result[name] = [value]
213 return parsed_result
214
8c25f81b
PH
215try:
216 from shlex import quote as shlex_quote
217except ImportError: # Python < 3.3
218 def shlex_quote(s):
7d4111ed
PH
219 if re.match(r'^[-_\w./]+$', s):
220 return s
221 else:
222 return "'" + s.replace("'", "'\"'\"'") + "'"
8c25f81b
PH
223
224
225def compat_ord(c):
5f6a1245
JW
226 if type(c) is int:
227 return c
228 else:
229 return ord(c)
8c25f81b
PH
230
231
232if sys.version_info >= (3, 0):
233 compat_getenv = os.getenv
234 compat_expanduser = os.path.expanduser
235else:
236 # Environment variables should be decoded with filesystem encoding.
237 # Otherwise it will fail if any non-ASCII characters present (see #3854 #3217 #2918)
238
239 def compat_getenv(key, default=None):
240 from .utils import get_filesystem_encoding
241 env = os.getenv(key, default)
242 if env:
243 env = env.decode(get_filesystem_encoding())
244 return env
245
246 # HACK: The default implementations of os.path.expanduser from cpython do not decode
247 # environment variables with filesystem encoding. We will work around this by
248 # providing adjusted implementations.
249 # The following are os.path.expanduser implementations from cpython 2.7.8 stdlib
250 # for different platforms with correct environment variables decoding.
251
252 if os.name == 'posix':
253 def compat_expanduser(path):
254 """Expand ~ and ~user constructions. If user or $HOME is unknown,
255 do nothing."""
256 if not path.startswith('~'):
257 return path
258 i = path.find('/', 1)
259 if i < 0:
260 i = len(path)
261 if i == 1:
262 if 'HOME' not in os.environ:
263 import pwd
264 userhome = pwd.getpwuid(os.getuid()).pw_dir
265 else:
266 userhome = compat_getenv('HOME')
267 else:
268 import pwd
269 try:
270 pwent = pwd.getpwnam(path[1:i])
271 except KeyError:
272 return path
273 userhome = pwent.pw_dir
274 userhome = userhome.rstrip('/')
275 return (userhome + path[i:]) or '/'
276 elif os.name == 'nt' or os.name == 'ce':
277 def compat_expanduser(path):
278 """Expand ~ and ~user constructs.
279
280 If user or $HOME is unknown, do nothing."""
281 if path[:1] != '~':
282 return path
283 i, n = 1, len(path)
284 while i < n and path[i] not in '/\\':
285 i = i + 1
286
287 if 'HOME' in os.environ:
288 userhome = compat_getenv('HOME')
289 elif 'USERPROFILE' in os.environ:
290 userhome = compat_getenv('USERPROFILE')
83e865a3 291 elif 'HOMEPATH' not in os.environ:
8c25f81b
PH
292 return path
293 else:
294 try:
295 drive = compat_getenv('HOMEDRIVE')
296 except KeyError:
297 drive = ''
298 userhome = os.path.join(drive, compat_getenv('HOMEPATH'))
299
5f6a1245 300 if i != 1: # ~user
8c25f81b
PH
301 userhome = os.path.join(os.path.dirname(userhome), path[1:i])
302
303 return userhome + path[i:]
304 else:
305 compat_expanduser = os.path.expanduser
306
307
308if sys.version_info < (3, 0):
309 def compat_print(s):
310 from .utils import preferredencoding
311 print(s.encode(preferredencoding(), 'xmlcharrefreplace'))
312else:
313 def compat_print(s):
b061ea6e 314 assert isinstance(s, compat_str)
8c25f81b
PH
315 print(s)
316
317
318try:
319 subprocess_check_output = subprocess.check_output
320except AttributeError:
321 def subprocess_check_output(*args, **kwargs):
322 assert 'input' not in kwargs
323 p = subprocess.Popen(*args, stdout=subprocess.PIPE, **kwargs)
324 output, _ = p.communicate()
325 ret = p.poll()
326 if ret:
327 raise subprocess.CalledProcessError(ret, p.args, output=output)
328 return output
329
330if sys.version_info < (3, 0) and sys.platform == 'win32':
331 def compat_getpass(prompt, *args, **kwargs):
332 if isinstance(prompt, compat_str):
baa70803 333 from .utils import preferredencoding
8c25f81b
PH
334 prompt = prompt.encode(preferredencoding())
335 return getpass.getpass(prompt, *args, **kwargs)
336else:
337 compat_getpass = getpass.getpass
338
c7b0add8
PH
339# Old 2.6 and 2.7 releases require kwargs to be bytes
340try:
c6973bd4
PH
341 def _testfunc(x):
342 pass
343 _testfunc(**{'x': 0})
c7b0add8
PH
344except TypeError:
345 def compat_kwargs(kwargs):
346 return dict((bytes(k), v) for k, v in kwargs.items())
347else:
348 compat_kwargs = lambda kwargs: kwargs
8c25f81b 349
e07e9313 350
be4a824d
PH
351if sys.version_info < (2, 7):
352 def compat_socket_create_connection(address, timeout, source_address=None):
353 host, port = address
354 err = None
355 for res in socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM):
356 af, socktype, proto, canonname, sa = res
357 sock = None
358 try:
359 sock = socket.socket(af, socktype, proto)
360 sock.settimeout(timeout)
361 if source_address:
362 sock.bind(source_address)
363 sock.connect(sa)
364 return sock
365 except socket.error as _:
366 err = _
367 if sock is not None:
368 sock.close()
369 if err is not None:
370 raise err
371 else:
8ad6b5ed 372 raise socket.error("getaddrinfo returns an empty list")
be4a824d
PH
373else:
374 compat_socket_create_connection = socket.create_connection
375
376
e07e9313
PH
377# Fix https://github.com/rg3/youtube-dl/issues/4223
378# See http://bugs.python.org/issue9161 for what is broken
379def workaround_optparse_bug9161():
07e378fa
PH
380 op = optparse.OptionParser()
381 og = optparse.OptionGroup(op, 'foo')
e07e9313 382 try:
07e378fa 383 og.add_option('-t')
b244b5c3 384 except TypeError:
e07e9313
PH
385 real_add_option = optparse.OptionGroup.add_option
386
387 def _compat_add_option(self, *args, **kwargs):
388 enc = lambda v: (
389 v.encode('ascii', 'replace') if isinstance(v, compat_str)
390 else v)
391 bargs = [enc(a) for a in args]
392 bkwargs = dict(
393 (k, enc(v)) for k, v in kwargs.items())
394 return real_add_option(self, *bargs, **bkwargs)
395 optparse.OptionGroup.add_option = _compat_add_option
396
003c69a8
JMF
397if hasattr(shutil, 'get_terminal_size'): # Python >= 3.3
398 compat_get_terminal_size = shutil.get_terminal_size
399else:
400 _terminal_size = collections.namedtuple('terminal_size', ['columns', 'lines'])
401
402 def compat_get_terminal_size():
403 columns = compat_getenv('COLUMNS', None)
404 if columns:
405 columns = int(columns)
406 else:
407 columns = None
408 lines = compat_getenv('LINES', None)
409 if lines:
410 lines = int(lines)
411 else:
412 lines = None
413
414 try:
415 sp = subprocess.Popen(
416 ['stty', 'size'],
417 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
418 out, err = sp.communicate()
419 lines, columns = map(int, out.split())
70a1165b 420 except Exception:
003c69a8
JMF
421 pass
422 return _terminal_size(columns, lines)
423
a0e060ac
YCH
424try:
425 itertools.count(start=0, step=1)
426 compat_itertools_count = itertools.count
427except TypeError: # Python 2.6
428 def compat_itertools_count(start=0, step=1):
429 n = start
430 while True:
431 yield n
432 n += step
e07e9313 433
8c25f81b
PH
434__all__ = [
435 'compat_HTTPError',
0196149c 436 'compat_basestring',
8c25f81b
PH
437 'compat_chr',
438 'compat_cookiejar',
439 'compat_expanduser',
003c69a8 440 'compat_get_terminal_size',
8c25f81b
PH
441 'compat_getenv',
442 'compat_getpass',
443 'compat_html_entities',
8c25f81b 444 'compat_http_client',
83fda3c0 445 'compat_http_server',
a0e060ac 446 'compat_itertools_count',
c7b0add8 447 'compat_kwargs',
8c25f81b
PH
448 'compat_ord',
449 'compat_parse_qs',
450 'compat_print',
be4a824d 451 'compat_socket_create_connection',
987493ae 452 'compat_str',
8c25f81b
PH
453 'compat_subprocess_get_DEVNULL',
454 'compat_urllib_error',
455 'compat_urllib_parse',
456 'compat_urllib_parse_unquote',
aa99aa4e 457 'compat_urllib_parse_unquote_plus',
9fefc886 458 'compat_urllib_parse_unquote_to_bytes',
8c25f81b
PH
459 'compat_urllib_parse_urlparse',
460 'compat_urllib_request',
461 'compat_urlparse',
462 'compat_urlretrieve',
463 'compat_xml_parse_error',
464 'shlex_quote',
465 'subprocess_check_output',
e07e9313 466 'workaround_optparse_bug9161',
8c25f81b 467]