compat.py 12 KB

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