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