Repository: pallets/click

Issue:
"IOError: [Errno 12] Not enough space" on Windows 7 and Python 2
This is caused by [click/_winconsole.py:268-272](https://github.com/pallets/click/blob/5701c4000c39b5a90c45f1083c0cbf4e00731e81/click/_winconsole.py#L268-L272). It seems that switching the standard streams to binary mode reveals the underlying limit of the Windows console of limiting a single write to 64K (I think this limit was lifted in Windows 8). This affects `print`, `sys.stdout.write` and `click.echo`.

It seems that many libraries and Python itself limit a single write to 32767 bytes to workaround this. Python 3 for example: [Python/fileutils.c:1244-1250](https://github.com/python/cpython/blob/f7eae0adfcd4c50034281b2c69f461b43b68db84/Python/fileutils.c#L1244-L1250). It doesn't seem like Python 2 has this hack though.

We will have to either:
1. Don't set the streams to binary and read/write to dupped stream fds (and file objects) that are set to binary.
2. Wrap the standard streams and workaround this.
3. Submit a patch to Python 2 which I'm not sure will ever be accepted.
4. Other?

To reproduce:
```py
import sys
import click

click.echo("spam")

print('A' * 100000) # or
sys.stdout.write('A' * 100000) # or
click.echo('A' * 100000)
```

Windows 7 SP1 x64
Python: 2.7.10
Click: 6.7


Discussion:

Resolving patch:
diff --git a/click/_compat.py b/click/_compat.py
--- a/click/_compat.py
+++ b/click/_compat.py
@@ -224,9 +224,11 @@ def get_binary_stdin():
         return set_binary_mode(sys.stdin)
 
     def get_binary_stdout():
+        _wrap_std_stream('stdout')
         return set_binary_mode(sys.stdout)
 
     def get_binary_stderr():
+        _wrap_std_stream('stderr')
         return set_binary_mode(sys.stderr)
 
     def get_text_stdin(encoding=None, errors=None):
@@ -237,6 +239,7 @@ def get_text_stdin(encoding=None, errors=None):
                                  force_readable=True)
 
     def get_text_stdout(encoding=None, errors=None):
+        _wrap_std_stream('stdout')
         rv = _get_windows_console_stream(sys.stdout, encoding, errors)
         if rv is not None:
             return rv
@@ -244,6 +247,7 @@ def get_text_stdout(encoding=None, errors=None):
                                  force_writable=True)
 
     def get_text_stderr(encoding=None, errors=None):
+        _wrap_std_stream('stderr')
         rv = _get_windows_console_stream(sys.stderr, encoding, errors)
         if rv is not None:
             return rv
@@ -582,7 +586,7 @@ def should_strip_ansi(stream=None, color=None):
     # Windows has a smaller terminal
     DEFAULT_COLUMNS = 79
 
-    from ._winconsole import _get_windows_console_stream
+    from ._winconsole import _get_windows_console_stream, _wrap_std_stream
 
     def _get_argv_encoding():
         import locale
@@ -644,6 +648,7 @@ def _get_argv_encoding():
         return getattr(sys.stdin, 'encoding', None) or get_filesystem_encoding()
 
     _get_windows_console_stream = lambda *x: None
+    _wrap_std_stream = lambda *x: None
 
 
 def term_len(x):
@@ -669,6 +674,7 @@ def func():
             return rv
         rv = wrapper_func()
         try:
+            stream = src_func()  # In case wrapper_func() modified the stream
             cache[stream] = rv
         except Exception:
             pass
diff --git a/click/_winconsole.py b/click/_winconsole.py
--- a/click/_winconsole.py
+++ b/click/_winconsole.py
@@ -201,6 +201,40 @@ def __repr__(self):
         )
 
 
+class WindowsChunkedWriter(object):
+    """
+    Wraps a stream (such as stdout), acting as a transparent proxy for all
+    attribute access apart from method 'write()' which we wrap to write in
+    limited chunks due to a Windows limitation on binary console streams.
+    """
+    def __init__(self, wrapped):
+        # double-underscore everything to prevent clashes with names of
+        # attributes on the wrapped stream object.
+        self.__wrapped = wrapped
+
+    def __getattr__(self, name):
+        return getattr(self.__wrapped, name)
+
+    def write(self, text):
+        total_to_write = len(text)
+        written = 0
+
+        while written < total_to_write:
+            to_write = min(total_to_write - written, MAX_BYTES_WRITTEN)
+            self.__wrapped.write(text[written:written+to_write])
+            written += to_write
+
+
+_wrapped_std_streams = set()
+
+
+def _wrap_std_stream(name):
+    # Python 2 & Windows 7 and below
+    if PY2 and sys.getwindowsversion()[:2] <= (6, 1) and name not in _wrapped_std_streams:
+        setattr(sys, name, WindowsChunkedWriter(getattr(sys, name)))
+        _wrapped_std_streams.add(name)
+
+
 def _get_text_stdin(buffer_stream):
     text_stream = _NonClosingTextIOWrapper(
         io.BufferedReader(_WindowsConsoleReader(STDIN_HANDLE)),


Test patch: