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