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