remove kebab
[youtube-dl] / youtube_dl / compat.py
1 from __future__ import unicode_literals
2
3 import collections
4 import getpass
5 import optparse
6 import os
7 import re
8 import shutil
9 import socket
10 import subprocess
11 import sys
12 import itertools
13
14
15 try:
16     import urllib.request as compat_urllib_request
17 except ImportError:  # Python 2
18     import urllib2 as compat_urllib_request
19
20 try:
21     import urllib.error as compat_urllib_error
22 except ImportError:  # Python 2
23     import urllib2 as compat_urllib_error
24
25 try:
26     import urllib.parse as compat_urllib_parse
27 except ImportError:  # Python 2
28     import urllib as compat_urllib_parse
29
30 try:
31     from urllib.parse import urlparse as compat_urllib_parse_urlparse
32 except ImportError:  # Python 2
33     from urlparse import urlparse as compat_urllib_parse_urlparse
34
35 try:
36     import urllib.parse as compat_urlparse
37 except ImportError:  # Python 2
38     import urlparse as compat_urlparse
39
40 try:
41     import http.cookiejar as compat_cookiejar
42 except ImportError:  # Python 2
43     import cookielib as compat_cookiejar
44
45 try:
46     import html.entities as compat_html_entities
47 except ImportError:  # Python 2
48     import htmlentitydefs as compat_html_entities
49
50 try:
51     import http.client as compat_http_client
52 except ImportError:  # Python 2
53     import httplib as compat_http_client
54
55 try:
56     from urllib.error import HTTPError as compat_HTTPError
57 except ImportError:  # Python 2
58     from urllib2 import HTTPError as compat_HTTPError
59
60 try:
61     from urllib.request import urlretrieve as compat_urlretrieve
62 except ImportError:  # Python 2
63     from urllib import urlretrieve as compat_urlretrieve
64
65
66 try:
67     from subprocess import DEVNULL
68     compat_subprocess_get_DEVNULL = lambda: DEVNULL
69 except ImportError:
70     compat_subprocess_get_DEVNULL = lambda: open(os.path.devnull, 'w')
71
72 try:
73     import http.server as compat_http_server
74 except ImportError:
75     import BaseHTTPServer as compat_http_server
76
77 try:
78     from urllib.parse import unquote as compat_urllib_parse_unquote
79 except ImportError:
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
111     def compat_urllib_parse_unquote(string, encoding='utf-8', errors='replace'):
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
145 try:
146     compat_str = unicode  # Python 2
147 except NameError:
148     compat_str = str
149
150 try:
151     compat_basestring = basestring  # Python 2
152 except NameError:
153     compat_basestring = str
154
155 try:
156     compat_chr = unichr  # Python 2
157 except NameError:
158     compat_chr = chr
159
160 try:
161     from xml.etree.ElementTree import ParseError as compat_xml_parse_error
162 except ImportError:  # Python 2.6
163     from xml.parsers.expat import ExpatError as compat_xml_parse_error
164
165
166 try:
167     from urllib.parse import parse_qs as compat_parse_qs
168 except ImportError:  # Python 2
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,
173                    encoding='utf-8', errors='replace'):
174         qs, _coerce_result = qs, compat_str
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,
202                         encoding='utf-8', errors='replace'):
203         parsed_result = {}
204         pairs = _parse_qsl(qs, keep_blank_values, strict_parsing,
205                            encoding=encoding, errors=errors)
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
213 try:
214     from shlex import quote as shlex_quote
215 except ImportError:  # Python < 3.3
216     def shlex_quote(s):
217         if re.match(r'^[-_\w./]+$', s):
218             return s
219         else:
220             return "'" + s.replace("'", "'\"'\"'") + "'"
221
222
223 def compat_ord(c):
224     if type(c) is int:
225         return c
226     else:
227         return ord(c)
228
229
230 if sys.version_info >= (3, 0):
231     compat_getenv = os.getenv
232     compat_expanduser = os.path.expanduser
233 else:
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')
289             elif 'HOMEPATH' not in os.environ:
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
298             if i != 1:  # ~user
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
306 if sys.version_info < (3, 0):
307     def compat_print(s):
308         from .utils import preferredencoding
309         print(s.encode(preferredencoding(), 'xmlcharrefreplace'))
310 else:
311     def compat_print(s):
312         assert isinstance(s, compat_str)
313         print(s)
314
315
316 try:
317     subprocess_check_output = subprocess.check_output
318 except 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
328 if sys.version_info < (3, 0) and sys.platform == 'win32':
329     def compat_getpass(prompt, *args, **kwargs):
330         if isinstance(prompt, compat_str):
331             from .utils import preferredencoding
332             prompt = prompt.encode(preferredencoding())
333         return getpass.getpass(prompt, *args, **kwargs)
334 else:
335     compat_getpass = getpass.getpass
336
337 # Old 2.6 and 2.7 releases require kwargs to be bytes
338 try:
339     def _testfunc(x):
340         pass
341     _testfunc(**{'x': 0})
342 except TypeError:
343     def compat_kwargs(kwargs):
344         return dict((bytes(k), v) for k, v in kwargs.items())
345 else:
346     compat_kwargs = lambda kwargs: kwargs
347
348
349 if 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:
370             raise socket.error("getaddrinfo returns an empty list")
371 else:
372     compat_socket_create_connection = socket.create_connection
373
374
375 # Fix https://github.com/rg3/youtube-dl/issues/4223
376 # See http://bugs.python.org/issue9161 for what is broken
377 def workaround_optparse_bug9161():
378     op = optparse.OptionParser()
379     og = optparse.OptionGroup(op, 'foo')
380     try:
381         og.add_option('-t')
382     except TypeError:
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
395 if hasattr(shutil, 'get_terminal_size'):  # Python >= 3.3
396     compat_get_terminal_size = shutil.get_terminal_size
397 else:
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())
418         except Exception:
419             pass
420         return _terminal_size(columns, lines)
421
422 try:
423     itertools.count(start=0, step=1)
424     compat_itertools_count = itertools.count
425 except 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
431
432 __all__ = [
433     'compat_HTTPError',
434     'compat_basestring',
435     'compat_chr',
436     'compat_cookiejar',
437     'compat_expanduser',
438     'compat_get_terminal_size',
439     'compat_getenv',
440     'compat_getpass',
441     'compat_html_entities',
442     'compat_http_client',
443     'compat_http_server',
444     'compat_itertools_count',
445     'compat_kwargs',
446     'compat_ord',
447     'compat_parse_qs',
448     'compat_print',
449     'compat_socket_create_connection',
450     'compat_str',
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',
462     'workaround_optparse_bug9161',
463 ]