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