PEP8: applied even more rules
[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:
186         return c
187     else:
188         return ord(c)
189
190
191 if sys.version_info >= (3, 0):
192     compat_getenv = os.getenv
193     compat_expanduser = os.path.expanduser
194 else:
195     # Environment variables should be decoded with filesystem encoding.
196     # Otherwise it will fail if any non-ASCII characters present (see #3854 #3217 #2918)
197
198     def compat_getenv(key, default=None):
199         from .utils import get_filesystem_encoding
200         env = os.getenv(key, default)
201         if env:
202             env = env.decode(get_filesystem_encoding())
203         return env
204
205     # HACK: The default implementations of os.path.expanduser from cpython do not decode
206     # environment variables with filesystem encoding. We will work around this by
207     # providing adjusted implementations.
208     # The following are os.path.expanduser implementations from cpython 2.7.8 stdlib
209     # for different platforms with correct environment variables decoding.
210
211     if os.name == 'posix':
212         def compat_expanduser(path):
213             """Expand ~ and ~user constructions.  If user or $HOME is unknown,
214             do nothing."""
215             if not path.startswith('~'):
216                 return path
217             i = path.find('/', 1)
218             if i < 0:
219                 i = len(path)
220             if i == 1:
221                 if 'HOME' not in os.environ:
222                     import pwd
223                     userhome = pwd.getpwuid(os.getuid()).pw_dir
224                 else:
225                     userhome = compat_getenv('HOME')
226             else:
227                 import pwd
228                 try:
229                     pwent = pwd.getpwnam(path[1:i])
230                 except KeyError:
231                     return path
232                 userhome = pwent.pw_dir
233             userhome = userhome.rstrip('/')
234             return (userhome + path[i:]) or '/'
235     elif os.name == 'nt' or os.name == 'ce':
236         def compat_expanduser(path):
237             """Expand ~ and ~user constructs.
238
239             If user or $HOME is unknown, do nothing."""
240             if path[:1] != '~':
241                 return path
242             i, n = 1, len(path)
243             while i < n and path[i] not in '/\\':
244                 i = i + 1
245
246             if 'HOME' in os.environ:
247                 userhome = compat_getenv('HOME')
248             elif 'USERPROFILE' in os.environ:
249                 userhome = compat_getenv('USERPROFILE')
250             elif not 'HOMEPATH' in os.environ:
251                 return path
252             else:
253                 try:
254                     drive = compat_getenv('HOMEDRIVE')
255                 except KeyError:
256                     drive = ''
257                 userhome = os.path.join(drive, compat_getenv('HOMEPATH'))
258
259             if i != 1:  # ~user
260                 userhome = os.path.join(os.path.dirname(userhome), path[1:i])
261
262             return userhome + path[i:]
263     else:
264         compat_expanduser = os.path.expanduser
265
266
267 if sys.version_info < (3, 0):
268     def compat_print(s):
269         from .utils import preferredencoding
270         print(s.encode(preferredencoding(), 'xmlcharrefreplace'))
271 else:
272     def compat_print(s):
273         assert type(s) == type(u'')
274         print(s)
275
276
277 try:
278     subprocess_check_output = subprocess.check_output
279 except AttributeError:
280     def subprocess_check_output(*args, **kwargs):
281         assert 'input' not in kwargs
282         p = subprocess.Popen(*args, stdout=subprocess.PIPE, **kwargs)
283         output, _ = p.communicate()
284         ret = p.poll()
285         if ret:
286             raise subprocess.CalledProcessError(ret, p.args, output=output)
287         return output
288
289 if sys.version_info < (3, 0) and sys.platform == 'win32':
290     def compat_getpass(prompt, *args, **kwargs):
291         if isinstance(prompt, compat_str):
292             from .utils import preferredencoding
293             prompt = prompt.encode(preferredencoding())
294         return getpass.getpass(prompt, *args, **kwargs)
295 else:
296     compat_getpass = getpass.getpass
297
298 # Old 2.6 and 2.7 releases require kwargs to be bytes
299 try:
300     (lambda x: x)(**{'x': 0})
301 except TypeError:
302     def compat_kwargs(kwargs):
303         return dict((bytes(k), v) for k, v in kwargs.items())
304 else:
305     compat_kwargs = lambda kwargs: kwargs
306
307
308 # Fix https://github.com/rg3/youtube-dl/issues/4223
309 # See http://bugs.python.org/issue9161 for what is broken
310 def workaround_optparse_bug9161():
311     op = optparse.OptionParser()
312     og = optparse.OptionGroup(op, 'foo')
313     try:
314         og.add_option('-t')
315     except TypeError:
316         real_add_option = optparse.OptionGroup.add_option
317
318         def _compat_add_option(self, *args, **kwargs):
319             enc = lambda v: (
320                 v.encode('ascii', 'replace') if isinstance(v, compat_str)
321                 else v)
322             bargs = [enc(a) for a in args]
323             bkwargs = dict(
324                 (k, enc(v)) for k, v in kwargs.items())
325             return real_add_option(self, *bargs, **bkwargs)
326         optparse.OptionGroup.add_option = _compat_add_option
327
328
329 __all__ = [
330     'compat_HTTPError',
331     'compat_chr',
332     'compat_cookiejar',
333     'compat_expanduser',
334     'compat_getenv',
335     'compat_getpass',
336     'compat_html_entities',
337     'compat_html_parser',
338     'compat_http_client',
339     'compat_kwargs',
340     'compat_ord',
341     'compat_parse_qs',
342     'compat_print',
343     'compat_str',
344     'compat_subprocess_get_DEVNULL',
345     'compat_urllib_error',
346     'compat_urllib_parse',
347     'compat_urllib_parse_unquote',
348     'compat_urllib_parse_urlparse',
349     'compat_urllib_request',
350     'compat_urlparse',
351     'compat_urlretrieve',
352     'compat_xml_parse_error',
353     'shlex_quote',
354     'subprocess_check_output',
355     'workaround_optparse_bug9161',
356 ]