]> jfr.im git - yt-dlp.git/blame - youtube_dl/compat.py
[vidio] Improve (Closes #9562)
[yt-dlp.git] / youtube_dl / compat.py
CommitLineData
451948b2
PH
1from __future__ import unicode_literals
2
0a67a363 3import binascii
003c69a8 4import collections
0a67a363 5import email
8c25f81b 6import getpass
0a67a363 7import io
e07e9313 8import optparse
8c25f81b 9import os
7d4111ed 10import re
51f579b6 11import shlex
003c69a8 12import shutil
be4a824d 13import socket
dab0daee 14import struct
8c25f81b
PH
15import subprocess
16import sys
a0e060ac 17import itertools
36e6f62c 18import xml.etree.ElementTree
8c25f81b
PH
19
20
21try:
22 import urllib.request as compat_urllib_request
5f6a1245 23except ImportError: # Python 2
8c25f81b
PH
24 import urllib2 as compat_urllib_request
25
26try:
27 import urllib.error as compat_urllib_error
5f6a1245 28except ImportError: # Python 2
8c25f81b
PH
29 import urllib2 as compat_urllib_error
30
31try:
32 import urllib.parse as compat_urllib_parse
5f6a1245 33except ImportError: # Python 2
8c25f81b
PH
34 import urllib as compat_urllib_parse
35
36try:
37 from urllib.parse import urlparse as compat_urllib_parse_urlparse
5f6a1245 38except ImportError: # Python 2
8c25f81b
PH
39 from urlparse import urlparse as compat_urllib_parse_urlparse
40
41try:
42 import urllib.parse as compat_urlparse
5f6a1245 43except ImportError: # Python 2
8c25f81b
PH
44 import urlparse as compat_urlparse
45
0a67a363
YCH
46try:
47 import urllib.response as compat_urllib_response
48except ImportError: # Python 2
49 import urllib as compat_urllib_response
50
8c25f81b
PH
51try:
52 import http.cookiejar as compat_cookiejar
5f6a1245 53except ImportError: # Python 2
8c25f81b
PH
54 import cookielib as compat_cookiejar
55
799207e8 56try:
57 import http.cookies as compat_cookies
58except ImportError: # Python 2
59 import Cookie as compat_cookies
60
8c25f81b
PH
61try:
62 import html.entities as compat_html_entities
5f6a1245 63except ImportError: # Python 2
8c25f81b
PH
64 import htmlentitydefs as compat_html_entities
65
8c25f81b
PH
66try:
67 import http.client as compat_http_client
5f6a1245 68except ImportError: # Python 2
8c25f81b
PH
69 import httplib as compat_http_client
70
71try:
72 from urllib.error import HTTPError as compat_HTTPError
73except ImportError: # Python 2
74 from urllib2 import HTTPError as compat_HTTPError
75
76try:
77 from urllib.request import urlretrieve as compat_urlretrieve
78except ImportError: # Python 2
79 from urllib import urlretrieve as compat_urlretrieve
80
8bb56eee
BF
81try:
82 from html.parser import HTMLParser as compat_HTMLParser
83except ImportError: # Python 2
84 from HTMLParser import HTMLParser as compat_HTMLParser
85
8c25f81b
PH
86
87try:
88 from subprocess import DEVNULL
89 compat_subprocess_get_DEVNULL = lambda: DEVNULL
90except ImportError:
91 compat_subprocess_get_DEVNULL = lambda: open(os.path.devnull, 'w')
92
83fda3c0
PH
93try:
94 import http.server as compat_http_server
95except ImportError:
96 import BaseHTTPServer as compat_http_server
97
953fed28
PH
98try:
99 compat_str = unicode # Python 2
100except NameError:
101 compat_str = str
102
8c25f81b 103try:
55139679 104 from urllib.parse import unquote_to_bytes as compat_urllib_parse_unquote_to_bytes
8c25f81b 105 from urllib.parse import unquote as compat_urllib_parse_unquote
aa99aa4e 106 from urllib.parse import unquote_plus as compat_urllib_parse_unquote_plus
55139679 107except ImportError: # Python 2
22603348
S
108 _asciire = (compat_urllib_parse._asciire if hasattr(compat_urllib_parse, '_asciire')
109 else re.compile('([\x00-\x7f]+)'))
3cc8b4c3 110
4d08161a 111 # HACK: The following are the correct unquote_to_bytes, unquote and unquote_plus
55139679
S
112 # implementations from cpython 3.4.3's stdlib. Python 2's version
113 # is apparently broken (see https://github.com/rg3/youtube-dl/pull/6244)
114
c9c854ce 115 def compat_urllib_parse_unquote_to_bytes(string):
116 """unquote_to_bytes('abc%20def') -> b'abc def'."""
117 # Note: strings are encoded as UTF-8. This is only an issue if it contains
118 # unescaped non-ASCII characters, which URIs should not.
119 if not string:
120 # Is it a string-like object?
121 string.split
122 return b''
953fed28 123 if isinstance(string, compat_str):
c9c854ce 124 string = string.encode('utf-8')
55139679 125 bits = string.split(b'%')
c9c854ce 126 if len(bits) == 1:
127 return string
128 res = [bits[0]]
129 append = res.append
c9c854ce 130 for item in bits[1:]:
131 try:
55139679 132 append(compat_urllib_parse._hextochr[item[:2]])
c9c854ce 133 append(item[2:])
55139679 134 except KeyError:
c9c854ce 135 append(b'%')
136 append(item)
137 return b''.join(res)
138
a0f28f90 139 def compat_urllib_parse_unquote(string, encoding='utf-8', errors='replace'):
c9c854ce 140 """Replace %xx escapes by their single-character equivalent. The optional
141 encoding and errors parameters specify how to decode percent-encoded
142 sequences into Unicode characters, as accepted by the bytes.decode()
143 method.
144 By default, percent-encoded sequences are decoded with UTF-8, and invalid
145 sequences are replaced by a placeholder character.
146
147 unquote('abc%20def') -> 'abc def'.
148 """
c9c854ce 149 if '%' not in string:
150 string.split
151 return string
152 if encoding is None:
153 encoding = 'utf-8'
154 if errors is None:
155 errors = 'replace'
3cc8b4c3 156 bits = _asciire.split(string)
c9c854ce 157 res = [bits[0]]
158 append = res.append
159 for i in range(1, len(bits), 2):
55139679
S
160 append(compat_urllib_parse_unquote_to_bytes(bits[i]).decode(encoding, errors))
161 append(bits[i + 1])
c9c854ce 162 return ''.join(res)
163
aa99aa4e
S
164 def compat_urllib_parse_unquote_plus(string, encoding='utf-8', errors='replace'):
165 """Like unquote(), but also replace plus signs by spaces, as required for
166 unquoting HTML form values.
167
168 unquote_plus('%7e/abc+def') -> '~/abc def'
169 """
170 string = string.replace('+', ' ')
171 return compat_urllib_parse_unquote(string, encoding, errors)
172
15707c7e
S
173try:
174 from urllib.parse import urlencode as compat_urllib_parse_urlencode
175except ImportError: # Python 2
176 # Python 2 will choke in urlencode on mixture of byte and unicode strings.
177 # Possible solutions are to either port it from python 3 with all
178 # the friends or manually ensure input query contains only byte strings.
179 # We will stick with latter thus recursively encoding the whole query.
180 def compat_urllib_parse_urlencode(query, doseq=0, encoding='utf-8'):
181 def encode_elem(e):
182 if isinstance(e, dict):
183 e = encode_dict(e)
184 elif isinstance(e, (list, tuple,)):
92d5477d
YCH
185 list_e = encode_list(e)
186 e = tuple(list_e) if isinstance(e, tuple) else list_e
15707c7e
S
187 elif isinstance(e, compat_str):
188 e = e.encode(encoding)
189 return e
190
191 def encode_dict(d):
192 return dict((encode_elem(k), encode_elem(v)) for k, v in d.items())
193
194 def encode_list(l):
195 return [encode_elem(e) for e in l]
196
197 return compat_urllib_parse.urlencode(encode_elem(query), doseq=doseq)
198
0a67a363
YCH
199try:
200 from urllib.request import DataHandler as compat_urllib_request_DataHandler
201except ImportError: # Python < 3.4
202 # Ported from CPython 98774:1733b3bd46db, Lib/urllib/request.py
203 class compat_urllib_request_DataHandler(compat_urllib_request.BaseHandler):
204 def data_open(self, req):
205 # data URLs as specified in RFC 2397.
206 #
207 # ignores POSTed data
208 #
209 # syntax:
210 # dataurl := "data:" [ mediatype ] [ ";base64" ] "," data
211 # mediatype := [ type "/" subtype ] *( ";" parameter )
212 # data := *urlchar
213 # parameter := attribute "=" value
214 url = req.get_full_url()
215
611c1dd9
S
216 scheme, data = url.split(':', 1)
217 mediatype, data = data.split(',', 1)
0a67a363
YCH
218
219 # even base64 encoded data URLs might be quoted so unquote in any case:
220 data = compat_urllib_parse_unquote_to_bytes(data)
611c1dd9 221 if mediatype.endswith(';base64'):
0a67a363
YCH
222 data = binascii.a2b_base64(data)
223 mediatype = mediatype[:-7]
224
225 if not mediatype:
611c1dd9 226 mediatype = 'text/plain;charset=US-ASCII'
0a67a363
YCH
227
228 headers = email.message_from_string(
611c1dd9 229 'Content-type: %s\nContent-length: %d\n' % (mediatype, len(data)))
0a67a363
YCH
230
231 return compat_urllib_response.addinfourl(io.BytesIO(data), headers, url)
232
8f9312c3 233try:
0196149c 234 compat_basestring = basestring # Python 2
8f9312c3 235except NameError:
0196149c 236 compat_basestring = str
8f9312c3
PH
237
238try:
239 compat_chr = unichr # Python 2
240except NameError:
241 compat_chr = chr
242
243try:
244 from xml.etree.ElementTree import ParseError as compat_xml_parse_error
245except ImportError: # Python 2.6
246 from xml.parsers.expat import ExpatError as compat_xml_parse_error
247
eb7941e3
YCH
248
249etree = xml.etree.ElementTree
250
251
252class _TreeBuilder(etree.TreeBuilder):
253 def doctype(self, name, pubid, system):
254 pass
255
36e6f62c 256if sys.version_info[0] >= 3:
eb7941e3
YCH
257 def compat_etree_fromstring(text):
258 return etree.XML(text, parser=etree.XMLParser(target=_TreeBuilder()))
36e6f62c 259else:
ae37338e
JMF
260 # python 2.x tries to encode unicode strings with ascii (see the
261 # XMLParser._fixtext method)
f7854627
JMF
262 try:
263 _etree_iter = etree.Element.iter
264 except AttributeError: # Python <=2.6
265 def _etree_iter(root):
266 for el in root.findall('*'):
267 yield el
268 for sub in _etree_iter(el):
269 yield sub
270
36e6f62c
JMF
271 # on 2.6 XML doesn't have a parser argument, function copied from CPython
272 # 2.7 source
273 def _XML(text, parser=None):
274 if not parser:
eb7941e3 275 parser = etree.XMLParser(target=_TreeBuilder())
36e6f62c
JMF
276 parser.feed(text)
277 return parser.close()
278
279 def _element_factory(*args, **kwargs):
280 el = etree.Element(*args, **kwargs)
281 for k, v in el.items():
387db16a
JMF
282 if isinstance(v, bytes):
283 el.set(k, v.decode('utf-8'))
36e6f62c
JMF
284 return el
285
286 def compat_etree_fromstring(text):
eb7941e3 287 doc = _XML(text, parser=etree.XMLParser(target=_TreeBuilder(element_factory=_element_factory)))
f7854627
JMF
288 for el in _etree_iter(doc):
289 if el.text is not None and isinstance(el.text, bytes):
290 el.text = el.text.decode('utf-8')
291 return doc
8c25f81b 292
57f7e3c6
S
293if sys.version_info < (2, 7):
294 # Here comes the crazy part: In 2.6, if the xpath is a unicode,
295 # .//node does not match if a node is a direct child of . !
296 def compat_xpath(xpath):
297 if isinstance(xpath, compat_str):
298 xpath = xpath.encode('ascii')
299 return xpath
300else:
301 compat_xpath = lambda xpath: xpath
302
8c25f81b
PH
303try:
304 from urllib.parse import parse_qs as compat_parse_qs
5f6a1245 305except ImportError: # Python 2
8c25f81b
PH
306 # HACK: The following is the correct parse_qs implementation from cpython 3's stdlib.
307 # Python 2's version is apparently totally broken
308
309 def _parse_qsl(qs, keep_blank_values=False, strict_parsing=False,
9e1a5b84 310 encoding='utf-8', errors='replace'):
8f9312c3 311 qs, _coerce_result = qs, compat_str
8c25f81b
PH
312 pairs = [s2 for s1 in qs.split('&') for s2 in s1.split(';')]
313 r = []
314 for name_value in pairs:
315 if not name_value and not strict_parsing:
316 continue
317 nv = name_value.split('=', 1)
318 if len(nv) != 2:
319 if strict_parsing:
611c1dd9 320 raise ValueError('bad query field: %r' % (name_value,))
8c25f81b
PH
321 # Handle case of a control-name with no equal sign
322 if keep_blank_values:
323 nv.append('')
324 else:
325 continue
326 if len(nv[1]) or keep_blank_values:
327 name = nv[0].replace('+', ' ')
328 name = compat_urllib_parse_unquote(
329 name, encoding=encoding, errors=errors)
330 name = _coerce_result(name)
331 value = nv[1].replace('+', ' ')
332 value = compat_urllib_parse_unquote(
333 value, encoding=encoding, errors=errors)
334 value = _coerce_result(value)
335 r.append((name, value))
336 return r
337
338 def compat_parse_qs(qs, keep_blank_values=False, strict_parsing=False,
9e1a5b84 339 encoding='utf-8', errors='replace'):
8c25f81b
PH
340 parsed_result = {}
341 pairs = _parse_qsl(qs, keep_blank_values, strict_parsing,
9e1a5b84 342 encoding=encoding, errors=errors)
8c25f81b
PH
343 for name, value in pairs:
344 if name in parsed_result:
345 parsed_result[name].append(value)
346 else:
347 parsed_result[name] = [value]
348 return parsed_result
349
8c25f81b 350try:
702ccf2d 351 from shlex import quote as compat_shlex_quote
8c25f81b 352except ImportError: # Python < 3.3
702ccf2d 353 def compat_shlex_quote(s):
7d4111ed
PH
354 if re.match(r'^[-_\w./]+$', s):
355 return s
356 else:
357 return "'" + s.replace("'", "'\"'\"'") + "'"
8c25f81b
PH
358
359
8df5ae15 360if sys.version_info >= (2, 7, 3):
51f579b6
S
361 compat_shlex_split = shlex.split
362else:
363 # Working around shlex issue with unicode strings on some python 2
364 # versions (see http://bugs.python.org/issue1548891)
365 def compat_shlex_split(s, comments=False, posix=True):
953fed28 366 if isinstance(s, compat_str):
51f579b6
S
367 s = s.encode('utf-8')
368 return shlex.split(s, comments, posix)
369
370
8c25f81b 371def compat_ord(c):
5f6a1245
JW
372 if type(c) is int:
373 return c
374 else:
375 return ord(c)
8c25f81b
PH
376
377
e9c0cdd3
YCH
378compat_os_name = os._name if os.name == 'java' else os.name
379
380
8c25f81b
PH
381if sys.version_info >= (3, 0):
382 compat_getenv = os.getenv
383 compat_expanduser = os.path.expanduser
fe40f9ee
S
384
385 def compat_setenv(key, value, env=os.environ):
386 env[key] = value
8c25f81b
PH
387else:
388 # Environment variables should be decoded with filesystem encoding.
389 # Otherwise it will fail if any non-ASCII characters present (see #3854 #3217 #2918)
390
391 def compat_getenv(key, default=None):
392 from .utils import get_filesystem_encoding
393 env = os.getenv(key, default)
394 if env:
395 env = env.decode(get_filesystem_encoding())
396 return env
397
fe40f9ee
S
398 def compat_setenv(key, value, env=os.environ):
399 def encode(v):
400 from .utils import get_filesystem_encoding
401 return v.encode(get_filesystem_encoding()) if isinstance(v, compat_str) else v
402 env[encode(key)] = encode(value)
403
8c25f81b
PH
404 # HACK: The default implementations of os.path.expanduser from cpython do not decode
405 # environment variables with filesystem encoding. We will work around this by
406 # providing adjusted implementations.
407 # The following are os.path.expanduser implementations from cpython 2.7.8 stdlib
408 # for different platforms with correct environment variables decoding.
409
e9c0cdd3 410 if compat_os_name == 'posix':
8c25f81b
PH
411 def compat_expanduser(path):
412 """Expand ~ and ~user constructions. If user or $HOME is unknown,
413 do nothing."""
414 if not path.startswith('~'):
415 return path
416 i = path.find('/', 1)
417 if i < 0:
418 i = len(path)
419 if i == 1:
420 if 'HOME' not in os.environ:
421 import pwd
422 userhome = pwd.getpwuid(os.getuid()).pw_dir
423 else:
424 userhome = compat_getenv('HOME')
425 else:
426 import pwd
427 try:
428 pwent = pwd.getpwnam(path[1:i])
429 except KeyError:
430 return path
431 userhome = pwent.pw_dir
432 userhome = userhome.rstrip('/')
433 return (userhome + path[i:]) or '/'
e9c0cdd3 434 elif compat_os_name == 'nt' or compat_os_name == 'ce':
8c25f81b
PH
435 def compat_expanduser(path):
436 """Expand ~ and ~user constructs.
437
438 If user or $HOME is unknown, do nothing."""
439 if path[:1] != '~':
440 return path
441 i, n = 1, len(path)
442 while i < n and path[i] not in '/\\':
443 i = i + 1
444
445 if 'HOME' in os.environ:
446 userhome = compat_getenv('HOME')
447 elif 'USERPROFILE' in os.environ:
448 userhome = compat_getenv('USERPROFILE')
83e865a3 449 elif 'HOMEPATH' not in os.environ:
8c25f81b
PH
450 return path
451 else:
452 try:
453 drive = compat_getenv('HOMEDRIVE')
454 except KeyError:
455 drive = ''
456 userhome = os.path.join(drive, compat_getenv('HOMEPATH'))
457
5f6a1245 458 if i != 1: # ~user
8c25f81b
PH
459 userhome = os.path.join(os.path.dirname(userhome), path[1:i])
460
461 return userhome + path[i:]
462 else:
463 compat_expanduser = os.path.expanduser
464
465
466if sys.version_info < (3, 0):
467 def compat_print(s):
468 from .utils import preferredencoding
469 print(s.encode(preferredencoding(), 'xmlcharrefreplace'))
470else:
471 def compat_print(s):
b061ea6e 472 assert isinstance(s, compat_str)
8c25f81b
PH
473 print(s)
474
475
8c25f81b
PH
476if sys.version_info < (3, 0) and sys.platform == 'win32':
477 def compat_getpass(prompt, *args, **kwargs):
478 if isinstance(prompt, compat_str):
baa70803 479 from .utils import preferredencoding
8c25f81b
PH
480 prompt = prompt.encode(preferredencoding())
481 return getpass.getpass(prompt, *args, **kwargs)
482else:
483 compat_getpass = getpass.getpass
484
614db89a 485# Python < 2.6.5 require kwargs to be bytes
c7b0add8 486try:
c6973bd4
PH
487 def _testfunc(x):
488 pass
489 _testfunc(**{'x': 0})
c7b0add8
PH
490except TypeError:
491 def compat_kwargs(kwargs):
492 return dict((bytes(k), v) for k, v in kwargs.items())
493else:
494 compat_kwargs = lambda kwargs: kwargs
8c25f81b 495
e07e9313 496
be4a824d
PH
497if sys.version_info < (2, 7):
498 def compat_socket_create_connection(address, timeout, source_address=None):
499 host, port = address
500 err = None
501 for res in socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM):
502 af, socktype, proto, canonname, sa = res
503 sock = None
504 try:
505 sock = socket.socket(af, socktype, proto)
506 sock.settimeout(timeout)
507 if source_address:
508 sock.bind(source_address)
509 sock.connect(sa)
510 return sock
511 except socket.error as _:
512 err = _
513 if sock is not None:
514 sock.close()
515 if err is not None:
516 raise err
517 else:
611c1dd9 518 raise socket.error('getaddrinfo returns an empty list')
be4a824d
PH
519else:
520 compat_socket_create_connection = socket.create_connection
521
522
e07e9313
PH
523# Fix https://github.com/rg3/youtube-dl/issues/4223
524# See http://bugs.python.org/issue9161 for what is broken
525def workaround_optparse_bug9161():
07e378fa
PH
526 op = optparse.OptionParser()
527 og = optparse.OptionGroup(op, 'foo')
e07e9313 528 try:
07e378fa 529 og.add_option('-t')
b244b5c3 530 except TypeError:
e07e9313
PH
531 real_add_option = optparse.OptionGroup.add_option
532
533 def _compat_add_option(self, *args, **kwargs):
534 enc = lambda v: (
535 v.encode('ascii', 'replace') if isinstance(v, compat_str)
536 else v)
537 bargs = [enc(a) for a in args]
538 bkwargs = dict(
539 (k, enc(v)) for k, v in kwargs.items())
540 return real_add_option(self, *bargs, **bkwargs)
541 optparse.OptionGroup.add_option = _compat_add_option
542
003c69a8
JMF
543if hasattr(shutil, 'get_terminal_size'): # Python >= 3.3
544 compat_get_terminal_size = shutil.get_terminal_size
545else:
546 _terminal_size = collections.namedtuple('terminal_size', ['columns', 'lines'])
547
13118a50 548 def compat_get_terminal_size(fallback=(80, 24)):
4810c48d 549 columns = compat_getenv('COLUMNS')
003c69a8
JMF
550 if columns:
551 columns = int(columns)
552 else:
553 columns = None
4810c48d 554 lines = compat_getenv('LINES')
003c69a8
JMF
555 if lines:
556 lines = int(lines)
557 else:
558 lines = None
559
4810c48d 560 if columns is None or lines is None or columns <= 0 or lines <= 0:
13118a50
YCH
561 try:
562 sp = subprocess.Popen(
563 ['stty', 'size'],
564 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
565 out, err = sp.communicate()
f2dbc540 566 _lines, _columns = map(int, out.split())
13118a50
YCH
567 except Exception:
568 _columns, _lines = _terminal_size(*fallback)
569
4810c48d 570 if columns is None or columns <= 0:
13118a50 571 columns = _columns
4810c48d 572 if lines is None or lines <= 0:
13118a50 573 lines = _lines
003c69a8
JMF
574 return _terminal_size(columns, lines)
575
a0e060ac
YCH
576try:
577 itertools.count(start=0, step=1)
578 compat_itertools_count = itertools.count
579except TypeError: # Python 2.6
580 def compat_itertools_count(start=0, step=1):
581 n = start
582 while True:
583 yield n
584 n += step
e07e9313 585
67134eab
JMF
586if sys.version_info >= (3, 0):
587 from tokenize import tokenize as compat_tokenize_tokenize
588else:
589 from tokenize import generate_tokens as compat_tokenize_tokenize
e07e9313 590
dab0daee
YCH
591
592try:
593 struct.pack('!I', 0)
594except TypeError:
595 # In Python 2.6 and 2.7.x < 2.7.7, struct requires a bytes argument
596 # See https://bugs.python.org/issue19099
edaa23f8 597 def compat_struct_pack(spec, *args):
dab0daee
YCH
598 if isinstance(spec, compat_str):
599 spec = spec.encode('ascii')
600 return struct.pack(spec, *args)
601
edaa23f8 602 def compat_struct_unpack(spec, *args):
dab0daee
YCH
603 if isinstance(spec, compat_str):
604 spec = spec.encode('ascii')
605 return struct.unpack(spec, *args)
606else:
edaa23f8
YCH
607 compat_struct_pack = struct.pack
608 compat_struct_unpack = struct.unpack
dab0daee
YCH
609
610
8c25f81b 611__all__ = [
8bb56eee 612 'compat_HTMLParser',
8c25f81b 613 'compat_HTTPError',
0196149c 614 'compat_basestring',
8c25f81b
PH
615 'compat_chr',
616 'compat_cookiejar',
799207e8 617 'compat_cookies',
36e6f62c 618 'compat_etree_fromstring',
8c25f81b 619 'compat_expanduser',
003c69a8 620 'compat_get_terminal_size',
8c25f81b
PH
621 'compat_getenv',
622 'compat_getpass',
623 'compat_html_entities',
8c25f81b 624 'compat_http_client',
83fda3c0 625 'compat_http_server',
a0e060ac 626 'compat_itertools_count',
c7b0add8 627 'compat_kwargs',
8c25f81b 628 'compat_ord',
e9c0cdd3 629 'compat_os_name',
8c25f81b
PH
630 'compat_parse_qs',
631 'compat_print',
fe40f9ee 632 'compat_setenv',
702ccf2d 633 'compat_shlex_quote',
51f579b6 634 'compat_shlex_split',
be4a824d 635 'compat_socket_create_connection',
987493ae 636 'compat_str',
edaa23f8
YCH
637 'compat_struct_pack',
638 'compat_struct_unpack',
8c25f81b 639 'compat_subprocess_get_DEVNULL',
67134eab 640 'compat_tokenize_tokenize',
8c25f81b
PH
641 'compat_urllib_error',
642 'compat_urllib_parse',
643 'compat_urllib_parse_unquote',
aa99aa4e 644 'compat_urllib_parse_unquote_plus',
9fefc886 645 'compat_urllib_parse_unquote_to_bytes',
15707c7e 646 'compat_urllib_parse_urlencode',
8c25f81b
PH
647 'compat_urllib_parse_urlparse',
648 'compat_urllib_request',
0a67a363
YCH
649 'compat_urllib_request_DataHandler',
650 'compat_urllib_response',
8c25f81b
PH
651 'compat_urlparse',
652 'compat_urlretrieve',
653 'compat_xml_parse_error',
57f7e3c6 654 'compat_xpath',
e07e9313 655 'workaround_optparse_bug9161',
8c25f81b 656]