compat.py 12 KB

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