compat.py 13 KB

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