b2bf149ef63ffb1c31bfb02f976f5cce2dbadad3
[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
13
14 try:
15     import urllib.request as compat_urllib_request
16 except ImportError:  # Python 2
17     import urllib2 as compat_urllib_request
18
19 try:
20     import urllib.error as compat_urllib_error
21 except ImportError:  # Python 2
22     import urllib2 as compat_urllib_error
23
24 try:
25     import urllib.parse as compat_urllib_parse
26 except ImportError:  # Python 2
27     import urllib as compat_urllib_parse
28
29 try:
30     from urllib.parse import urlparse as compat_urllib_parse_urlparse
31 except ImportError:  # Python 2
32     from urlparse import urlparse as compat_urllib_parse_urlparse
33
34 try:
35     import urllib.parse as compat_urlparse
36 except ImportError:  # Python 2
37     import urlparse as compat_urlparse
38
39 try:
40     import http.cookiejar as compat_cookiejar
41 except ImportError:  # Python 2
42     import cookielib as compat_cookiejar
43
44 try:
45     import html.entities as compat_html_entities
46 except ImportError:  # Python 2
47     import htmlentitydefs as compat_html_entities
48
49 try:
50     import html.parser as compat_html_parser
51 except ImportError:  # Python 2
52     import HTMLParser as compat_html_parser
53
54 try:
55     import http.client as compat_http_client
56 except ImportError:  # Python 2
57     import httplib as compat_http_client
58
59 try:
60     from urllib.error import HTTPError as compat_HTTPError
61 except ImportError:  # Python 2
62     from urllib2 import HTTPError as compat_HTTPError
63
64 try:
65     from urllib.request import urlretrieve as compat_urlretrieve
66 except ImportError:  # Python 2
67     from urllib import urlretrieve as compat_urlretrieve
68
69
70 try:
71     from subprocess import DEVNULL
72     compat_subprocess_get_DEVNULL = lambda: DEVNULL
73 except ImportError:
74     compat_subprocess_get_DEVNULL = lambda: open(os.path.devnull, 'w')
75
76 try:
77     import http.server as compat_http_server
78 except ImportError:
79     import BaseHTTPServer as compat_http_server
80
81 try:
82     from urllib.parse import unquote as compat_urllib_parse_unquote
83 except ImportError:
84     def compat_urllib_parse_unquote(string, encoding='utf-8', errors='replace'):
85         if string == '':
86             return string
87         res = string.split('%')
88         if len(res) == 1:
89             return string
90         if encoding is None:
91             encoding = 'utf-8'
92         if errors is None:
93             errors = 'replace'
94         # pct_sequence: contiguous sequence of percent-encoded bytes, decoded
95         pct_sequence = b''
96         string = res[0]
97         for item in res[1:]:
98             try:
99                 if not item:
100                     raise ValueError
101                 pct_sequence += item[:2].decode('hex')
102                 rest = item[2:]
103                 if not rest:
104                     # This segment was just a single percent-encoded character.
105                     # May be part of a sequence of code units, so delay decoding.
106                     # (Stored in pct_sequence).
107                     continue
108             except ValueError:
109                 rest = '%' + item
110             # Encountered non-percent-encoded characters. Flush the current
111             # pct_sequence.
112             string += pct_sequence.decode(encoding, errors) + rest
113             pct_sequence = b''
114         if pct_sequence:
115             # Flush the final pct_sequence
116             string += pct_sequence.decode(encoding, errors)
117         return string
118
119 try:
120     compat_str = unicode  # Python 2
121 except NameError:
122     compat_str = str
123
124 try:
125     compat_basestring = basestring  # Python 2
126 except NameError:
127     compat_basestring = str
128
129 try:
130     compat_chr = unichr  # Python 2
131 except NameError:
132     compat_chr = chr
133
134 try:
135     from xml.etree.ElementTree import ParseError as compat_xml_parse_error
136 except ImportError:  # Python 2.6
137     from xml.parsers.expat import ExpatError as compat_xml_parse_error
138
139
140 try:
141     from urllib.parse import parse_qs as compat_parse_qs
142 except ImportError:  # Python 2
143     # HACK: The following is the correct parse_qs implementation from cpython 3's stdlib.
144     # Python 2's version is apparently totally broken
145
146     def _parse_qsl(qs, keep_blank_values=False, strict_parsing=False,
147                    encoding='utf-8', errors='replace'):
148         qs, _coerce_result = qs, compat_str
149         pairs = [s2 for s1 in qs.split('&') for s2 in s1.split(';')]
150         r = []
151         for name_value in pairs:
152             if not name_value and not strict_parsing:
153                 continue
154             nv = name_value.split('=', 1)
155             if len(nv) != 2:
156                 if strict_parsing:
157                     raise ValueError("bad query field: %r" % (name_value,))
158                 # Handle case of a control-name with no equal sign
159                 if keep_blank_values:
160                     nv.append('')
161                 else:
162                     continue
163             if len(nv[1]) or keep_blank_values:
164                 name = nv[0].replace('+', ' ')
165                 name = compat_urllib_parse_unquote(
166                     name, encoding=encoding, errors=errors)
167                 name = _coerce_result(name)
168                 value = nv[1].replace('+', ' ')
169                 value = compat_urllib_parse_unquote(
170                     value, encoding=encoding, errors=errors)
171                 value = _coerce_result(value)
172                 r.append((name, value))
173         return r
174
175     def compat_parse_qs(qs, keep_blank_values=False, strict_parsing=False,
176                         encoding='utf-8', errors='replace'):
177         parsed_result = {}
178         pairs = _parse_qsl(qs, keep_blank_values, strict_parsing,
179                            encoding=encoding, errors=errors)
180         for name, value in pairs:
181             if name in parsed_result:
182                 parsed_result[name].append(value)
183             else:
184                 parsed_result[name] = [value]
185         return parsed_result
186
187 try:
188     from shlex import quote as shlex_quote
189 except ImportError:  # Python < 3.3
190     def shlex_quote(s):
191         if re.match(r'^[-_\w./]+$', s):
192             return s
193         else:
194             return "'" + s.replace("'", "'\"'\"'") + "'"
195
196
197 def compat_ord(c):
198     if type(c) is int:
199         return c
200     else:
201         return ord(c)
202
203
204 if sys.version_info >= (3, 0):
205     compat_getenv = os.getenv
206     compat_expanduser = os.path.expanduser
207 else:
208     # Environment variables should be decoded with filesystem encoding.
209     # Otherwise it will fail if any non-ASCII characters present (see #3854 #3217 #2918)
210
211     def compat_getenv(key, default=None):
212         from .utils import get_filesystem_encoding
213         env = os.getenv(key, default)
214         if env:
215             env = env.decode(get_filesystem_encoding())
216         return env
217
218     # HACK: The default implementations of os.path.expanduser from cpython do not decode
219     # environment variables with filesystem encoding. We will work around this by
220     # providing adjusted implementations.
221     # The following are os.path.expanduser implementations from cpython 2.7.8 stdlib
222     # for different platforms with correct environment variables decoding.
223
224     if os.name == 'posix':
225         def compat_expanduser(path):
226             """Expand ~ and ~user constructions.  If user or $HOME is unknown,
227             do nothing."""
228             if not path.startswith('~'):
229                 return path
230             i = path.find('/', 1)
231             if i < 0:
232                 i = len(path)
233             if i == 1:
234                 if 'HOME' not in os.environ:
235                     import pwd
236                     userhome = pwd.getpwuid(os.getuid()).pw_dir
237                 else:
238                     userhome = compat_getenv('HOME')
239             else:
240                 import pwd
241                 try:
242                     pwent = pwd.getpwnam(path[1:i])
243                 except KeyError:
244                     return path
245                 userhome = pwent.pw_dir
246             userhome = userhome.rstrip('/')
247             return (userhome + path[i:]) or '/'
248     elif os.name == 'nt' or os.name == 'ce':
249         def compat_expanduser(path):
250             """Expand ~ and ~user constructs.
251
252             If user or $HOME is unknown, do nothing."""
253             if path[:1] != '~':
254                 return path
255             i, n = 1, len(path)
256             while i < n and path[i] not in '/\\':
257                 i = i + 1
258
259             if 'HOME' in os.environ:
260                 userhome = compat_getenv('HOME')
261             elif 'USERPROFILE' in os.environ:
262                 userhome = compat_getenv('USERPROFILE')
263             elif 'HOMEPATH' not in os.environ:
264                 return path
265             else:
266                 try:
267                     drive = compat_getenv('HOMEDRIVE')
268                 except KeyError:
269                     drive = ''
270                 userhome = os.path.join(drive, compat_getenv('HOMEPATH'))
271
272             if i != 1:  # ~user
273                 userhome = os.path.join(os.path.dirname(userhome), path[1:i])
274
275             return userhome + path[i:]
276     else:
277         compat_expanduser = os.path.expanduser
278
279
280 if sys.version_info < (3, 0):
281     def compat_print(s):
282         from .utils import preferredencoding
283         print(s.encode(preferredencoding(), 'xmlcharrefreplace'))
284 else:
285     def compat_print(s):
286         assert isinstance(s, compat_str)
287         print(s)
288
289
290 try:
291     subprocess_check_output = subprocess.check_output
292 except AttributeError:
293     def subprocess_check_output(*args, **kwargs):
294         assert 'input' not in kwargs
295         p = subprocess.Popen(*args, stdout=subprocess.PIPE, **kwargs)
296         output, _ = p.communicate()
297         ret = p.poll()
298         if ret:
299             raise subprocess.CalledProcessError(ret, p.args, output=output)
300         return output
301
302 if sys.version_info < (3, 0) and sys.platform == 'win32':
303     def compat_getpass(prompt, *args, **kwargs):
304         if isinstance(prompt, compat_str):
305             from .utils import preferredencoding
306             prompt = prompt.encode(preferredencoding())
307         return getpass.getpass(prompt, *args, **kwargs)
308 else:
309     compat_getpass = getpass.getpass
310
311 # Old 2.6 and 2.7 releases require kwargs to be bytes
312 try:
313     def _testfunc(x):
314         pass
315     _testfunc(**{'x': 0})
316 except TypeError:
317     def compat_kwargs(kwargs):
318         return dict((bytes(k), v) for k, v in kwargs.items())
319 else:
320     compat_kwargs = lambda kwargs: kwargs
321
322
323 if sys.version_info < (2, 7):
324     def compat_socket_create_connection(address, timeout, source_address=None):
325         host, port = address
326         err = None
327         for res in socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM):
328             af, socktype, proto, canonname, sa = res
329             sock = None
330             try:
331                 sock = socket.socket(af, socktype, proto)
332                 sock.settimeout(timeout)
333                 if source_address:
334                     sock.bind(source_address)
335                 sock.connect(sa)
336                 return sock
337             except socket.error as _:
338                 err = _
339                 if sock is not None:
340                     sock.close()
341         if err is not None:
342             raise err
343         else:
344             raise socket.error("getaddrinfo returns an empty list")
345 else:
346     compat_socket_create_connection = socket.create_connection
347
348
349 # Fix https://github.com/rg3/youtube-dl/issues/4223
350 # See http://bugs.python.org/issue9161 for what is broken
351 def workaround_optparse_bug9161():
352     op = optparse.OptionParser()
353     og = optparse.OptionGroup(op, 'foo')
354     try:
355         og.add_option('-t')
356     except TypeError:
357         real_add_option = optparse.OptionGroup.add_option
358
359         def _compat_add_option(self, *args, **kwargs):
360             enc = lambda v: (
361                 v.encode('ascii', 'replace') if isinstance(v, compat_str)
362                 else v)
363             bargs = [enc(a) for a in args]
364             bkwargs = dict(
365                 (k, enc(v)) for k, v in kwargs.items())
366             return real_add_option(self, *bargs, **bkwargs)
367         optparse.OptionGroup.add_option = _compat_add_option
368
369 if hasattr(shutil, 'get_terminal_size'):  # Python >= 3.3
370     compat_get_terminal_size = shutil.get_terminal_size
371 else:
372     _terminal_size = collections.namedtuple('terminal_size', ['columns', 'lines'])
373
374     def compat_get_terminal_size():
375         columns = compat_getenv('COLUMNS', None)
376         if columns:
377             columns = int(columns)
378         else:
379             columns = None
380         lines = compat_getenv('LINES', None)
381         if lines:
382             lines = int(lines)
383         else:
384             lines = None
385
386         try:
387             sp = subprocess.Popen(
388                 ['stty', 'size'],
389                 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
390             out, err = sp.communicate()
391             lines, columns = map(int, out.split())
392         except:
393             pass
394         return _terminal_size(columns, lines)
395
396
397 __all__ = [
398     'compat_HTTPError',
399     'compat_basestring',
400     'compat_chr',
401     'compat_cookiejar',
402     'compat_expanduser',
403     'compat_get_terminal_size',
404     'compat_getenv',
405     'compat_getpass',
406     'compat_html_entities',
407     'compat_html_parser',
408     'compat_http_client',
409     'compat_http_server',
410     'compat_kwargs',
411     'compat_ord',
412     'compat_parse_qs',
413     'compat_print',
414     'compat_socket_create_connection',
415     'compat_str',
416     'compat_subprocess_get_DEVNULL',
417     'compat_urllib_error',
418     'compat_urllib_parse',
419     'compat_urllib_parse_unquote',
420     'compat_urllib_parse_urlparse',
421     'compat_urllib_request',
422     'compat_urlparse',
423     'compat_urlretrieve',
424     'compat_xml_parse_error',
425     'shlex_quote',
426     'subprocess_check_output',
427     'workaround_optparse_bug9161',
428 ]