[compat] Allow overriding by only COLUMNS or LINES in compat_get_terminal_size
[youtube-dl] / youtube_dl / compat.py
index 2fd2278aa8dc37921d17e8f702c41f0e7ef3cdb4..c36c9c23ff633b82e837cc739e725e8e734f35fa 100644 (file)
@@ -5,6 +5,7 @@ import getpass
 import optparse
 import os
 import re
+import shlex
 import shutil
 import socket
 import subprocess
@@ -42,6 +43,11 @@ try:
 except ImportError:  # Python 2
     import cookielib as compat_cookiejar
 
+try:
+    import http.cookies as compat_cookies
+except ImportError:  # Python 2
+    import Cookie as compat_cookies
+
 try:
     import html.entities as compat_html_entities
 except ImportError:  # Python 2
@@ -74,15 +80,23 @@ try:
 except ImportError:
     import BaseHTTPServer as compat_http_server
 
-from pprint import (pprint, pformat)
-
-
-def dprint(fmt):
-    sys.stderr.write(pformat(fmt) + "\n")
+try:
+    compat_str = unicode  # Python 2
+except NameError:
+    compat_str = str
 
 try:
+    from urllib.parse import unquote_to_bytes as compat_urllib_parse_unquote_to_bytes
     from urllib.parse import unquote as compat_urllib_parse_unquote
-except ImportError:
+    from urllib.parse import unquote_plus as compat_urllib_parse_unquote_plus
+except ImportError:  # Python 2
+    _asciire = (compat_urllib_parse._asciire if hasattr(compat_urllib_parse, '_asciire')
+                else re.compile('([\x00-\x7f]+)'))
+
+    # HACK: The following are the correct unquote_to_bytes, unquote and unquote_plus
+    # implementations from cpython 3.4.3's stdlib. Python 2's version
+    # is apparently broken (see https://github.com/rg3/youtube-dl/pull/6244)
+
     def compat_urllib_parse_unquote_to_bytes(string):
         """unquote_to_bytes('abc%20def') -> b'abc def'."""
         # Note: strings are encoded as UTF-8. This is only an issue if it contains
@@ -91,30 +105,23 @@ except ImportError:
             # Is it a string-like object?
             string.split
             return b''
-        if isinstance(string, str):
+        if isinstance(string, compat_str):
             string = string.encode('utf-8')
-            # string = encode('utf-8')
-
-        # python3 -> 2: must implicitly convert to bits
-        bits = bytes(string).split(b'%')
-
+        bits = string.split(b'%')
         if len(bits) == 1:
             return string
         res = [bits[0]]
         append = res.append
-
         for item in bits[1:]:
             try:
-                append(item[:2].decode('hex'))
+                append(compat_urllib_parse._hextochr[item[:2]])
                 append(item[2:])
-            except:
+            except KeyError:
                 append(b'%')
                 append(item)
         return b''.join(res)
 
-    compat_urllib_parse_asciire = re.compile('([\x00-\x7f]+)')
-
-    def new_compat_urllib_parse_unquote(string, encoding='utf-8', errors='replace'):
+    def compat_urllib_parse_unquote(string, encoding='utf-8', errors='replace'):
         """Replace %xx escapes by their single-character equivalent. The optional
         encoding and errors parameters specify how to decode percent-encoded
         sequences into Unicode characters, as accepted by the bytes.decode()
@@ -124,7 +131,6 @@ except ImportError:
 
         unquote('abc%20def') -> 'abc def'.
         """
-
         if '%' not in string:
             string.split
             return string
@@ -132,65 +138,22 @@ except ImportError:
             encoding = 'utf-8'
         if errors is None:
             errors = 'replace'
-
-        bits = compat_urllib_parse_asciire.split(string)
+        bits = _asciire.split(string)
         res = [bits[0]]
         append = res.append
         for i in range(1, len(bits), 2):
-            foo = compat_urllib_parse_unquote_to_bytes(bits[i])
-            foo = foo.decode(encoding, errors)
-            append(foo)
-
-            if bits[i + 1]:
-                bar = bits[i + 1]
-                if not isinstance(bar, unicode):
-                    bar = bar.decode('utf-8')
-                append(bar)
+            append(compat_urllib_parse_unquote_to_bytes(bits[i]).decode(encoding, errors))
+            append(bits[i + 1])
         return ''.join(res)
 
-    def old_compat_urllib_parse_unquote(string, encoding='utf-8', errors='replace'):
-        if string == '':
-            return string
-        res = string.split('%')
-        if len(res) == 1:
-            return string
-        if encoding is None:
-            encoding = 'utf-8'
-        if errors is None:
-            errors = 'replace'
-        # pct_sequence: contiguous sequence of percent-encoded bytes, decoded
-        pct_sequence = b''
-        string = res[0]
-        for item in res[1:]:
-            try:
-                if not item:
-                    raise ValueError
-                if not re.match('[0-9a-fA-F][0-9a-fA-F]',item[:2]):
-                    raise ValueError
-                pct_sequence += item[:2].decode('hex')
-                rest = item[2:]
-                if not rest:
-                    # This segment was just a single percent-encoded character.
-                    # May be part of a sequence of code units, so delay decoding.
-                    # (Stored in pct_sequence).
-                    continue
-            except ValueError:
-                rest = '%' + item
-            # Encountered non-percent-encoded characters. Flush the current
-            # pct_sequence.
-            string += pct_sequence.decode(encoding, errors) + rest
-            pct_sequence = b''
-        if pct_sequence:
-            # Flush the final pct_sequence
-            string += pct_sequence.decode(encoding, errors)
-        return string
-
-    compat_urllib_parse_unquote = new_compat_urllib_parse_unquote
+    def compat_urllib_parse_unquote_plus(string, encoding='utf-8', errors='replace'):
+        """Like unquote(), but also replace plus signs by spaces, as required for
+        unquoting HTML form values.
 
-try:
-    compat_str = unicode  # Python 2
-except NameError:
-    compat_str = str
+        unquote_plus('%7e/abc+def') -> '~/abc def'
+        """
+        string = string.replace('+', ' ')
+        return compat_urllib_parse_unquote(string, encoding, errors)
 
 try:
     compat_basestring = basestring  # Python 2
@@ -265,6 +228,17 @@ except ImportError:  # Python < 3.3
             return "'" + s.replace("'", "'\"'\"'") + "'"
 
 
+if sys.version_info >= (2, 7, 3):
+    compat_shlex_split = shlex.split
+else:
+    # Working around shlex issue with unicode strings on some python 2
+    # versions (see http://bugs.python.org/issue1548891)
+    def compat_shlex_split(s, comments=False, posix=True):
+        if isinstance(s, compat_str):
+            s = s.encode('utf-8')
+        return shlex.split(s, comments, posix)
+
+
 def compat_ord(c):
     if type(c) is int:
         return c
@@ -442,7 +416,7 @@ if hasattr(shutil, 'get_terminal_size'):  # Python >= 3.3
 else:
     _terminal_size = collections.namedtuple('terminal_size', ['columns', 'lines'])
 
-    def compat_get_terminal_size():
+    def compat_get_terminal_size(fallback=(80, 24)):
         columns = compat_getenv('COLUMNS', None)
         if columns:
             columns = int(columns)
@@ -454,14 +428,20 @@ else:
         else:
             lines = None
 
-        try:
-            sp = subprocess.Popen(
-                ['stty', 'size'],
-                stdout=subprocess.PIPE, stderr=subprocess.PIPE)
-            out, err = sp.communicate()
-            lines, columns = map(int, out.split())
-        except Exception:
-            pass
+        if columns <= 0 or lines <= 0:
+            try:
+                sp = subprocess.Popen(
+                    ['stty', 'size'],
+                    stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+                out, err = sp.communicate()
+                _columns, _lines = map(int, out.split())
+            except Exception:
+                _columns, _lines = _terminal_size(*fallback)
+
+            if columns <= 0:
+                columns = _columns
+            if lines <= 0:
+                lines = _lines
         return _terminal_size(columns, lines)
 
 try:
@@ -474,11 +454,17 @@ except TypeError:  # Python 2.6
             yield n
             n += step
 
+if sys.version_info >= (3, 0):
+    from tokenize import tokenize as compat_tokenize_tokenize
+else:
+    from tokenize import generate_tokens as compat_tokenize_tokenize
+
 __all__ = [
     'compat_HTTPError',
     'compat_basestring',
     'compat_chr',
     'compat_cookiejar',
+    'compat_cookies',
     'compat_expanduser',
     'compat_get_terminal_size',
     'compat_getenv',
@@ -491,12 +477,16 @@ __all__ = [
     'compat_ord',
     'compat_parse_qs',
     'compat_print',
+    'compat_shlex_split',
     'compat_socket_create_connection',
     'compat_str',
     'compat_subprocess_get_DEVNULL',
+    'compat_tokenize_tokenize',
     'compat_urllib_error',
     'compat_urllib_parse',
     'compat_urllib_parse_unquote',
+    'compat_urllib_parse_unquote_plus',
+    'compat_urllib_parse_unquote_to_bytes',
     'compat_urllib_parse_urlparse',
     'compat_urllib_request',
     'compat_urlparse',