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