Skip to content

Commit

Permalink
Redirect all spinner output to stderr
Browse files Browse the repository at this point in the history
- Fixes #3239

Signed-off-by: Dan Ryan <dan@danryan.co>
  • Loading branch information
techalchemy committed Nov 18, 2018
1 parent ce0ed98 commit 589b1a4
Show file tree
Hide file tree
Showing 5 changed files with 126 additions and 33 deletions.
1 change: 1 addition & 0 deletions news/3239.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fixed a bug which caused spinner frames to be written to stdout during locking operations which could cause redirection pipes to fail.
2 changes: 1 addition & 1 deletion pipenv/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -482,7 +482,7 @@ def create_spinner(text, nospin=None, spinner_name=None):
with vistir.spin.create_spinner(
spinner_name=spinner_name,
start_text=vistir.compat.fs_str(text),
nospin=nospin
nospin=nospin, write_to_stdout=False
) as sp:
yield sp

Expand Down
10 changes: 7 additions & 3 deletions pipenv/vendor/vistir/contextmanagers.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,13 +103,14 @@ def write(self, text):


@contextmanager
def spinner(spinner_name=None, start_text=None, handler_map=None, nospin=False):
def spinner(spinner_name=None, start_text=None, handler_map=None, nospin=False, write_to_stdout=True):
"""Get a spinner object or a dummy spinner to wrap a context.
:param str spinner_name: A spinner type e.g. "dots" or "bouncingBar" (default: {"bouncingBar"})
:param str start_text: Text to start off the spinner with (default: {None})
:param dict handler_map: Handler map for signals to be handled gracefully (default: {None})
:param bool nospin: If true, use the dummy spinner (default: {False})
:param bool write_to_stdout: Writes to stdout if true, otherwise writes to stderr (default: True)
:return: A spinner object which can be manipulated while alive
:rtype: :class:`~vistir.spin.VistirSpinner`
Expand All @@ -136,14 +137,17 @@ def spinner(spinner_name=None, start_text=None, handler_map=None, nospin=False):
use_yaspin = (has_yaspin is False) or (nospin is True)
if has_yaspin is None or has_yaspin is True and not nospin:
use_yaspin = True
if not start_text and nospin is False:
if not start_text and use_yaspin is True:
start_text = "Running..."
else:
start_text = ""
with create_spinner(
spinner_name=spinner_name,
text=start_text,
handler_map=handler_map,
nospin=nospin,
use_yaspin=use_yaspin
use_yaspin=use_yaspin,
write_to_stdout=write_to_stdout
) as _spinner:
yield _spinner

Expand Down
7 changes: 5 additions & 2 deletions pipenv/vendor/vistir/misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,8 @@ def run(
nospin=False,
spinner_name=None,
combine_stderr=True,
display_limit=200
display_limit=200,
write_to_stdout=True
):
"""Use `subprocess.Popen` to get the output of a command and decode it.
Expand All @@ -266,6 +267,7 @@ def run(
:param str spinner_name: The name of the spinner to use if enabled, defaults to bouncingBar
:param bool combine_stderr: Optionally merge stdout and stderr in the subprocess, false if nonblocking.
:param int dispay_limit: The max width of output lines to display when using a spinner.
:param bool write_to_stdout: Whether to write to stdout when using a spinner, default True.
:returns: A 2-tuple of (output, error) or a :class:`subprocess.Popen` object.
.. Warning:: Merging standard out and standarad error in a nonblocking subprocess
Expand Down Expand Up @@ -296,7 +298,8 @@ def run(
if block or not return_object:
combine_stderr = False
start_text = ""
with spinner(spinner_name=spinner_name, start_text=start_text, nospin=nospin) as sp:
with spinner(spinner_name=spinner_name, start_text=start_text, nospin=nospin,
write_to_stdout=write_to_stdout) as sp:
return _create_subprocess(
cmd,
env=_env,
Expand Down
139 changes: 112 additions & 27 deletions pipenv/vendor/vistir/spin.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import os
import signal
import sys
import threading
import time

import colorama
import cursor
Expand Down Expand Up @@ -34,14 +36,18 @@ class DummySpinner(object):
def __init__(self, text="", **kwargs):
colorama.init()
from .misc import decode_for_output
self.text = to_native_string(decode_for_output(text))
self.text = to_native_string(decode_for_output(text)) if text else ""
self.stdout = kwargs.get("stdout", sys.stdout)
self.stderr = kwargs.get("stderr", sys.stderr)
self.out_buff = StringIO()
self.write_to_stdout = kwargs.get("write_to_stdout", False)

def __enter__(self):
if self.text and self.text != "None":
self.write_err(self.text)
if self.write_to_stdout:
self.write(self.text)
else:
self.write_err(self.text)
return self

def __exit__(self, exc_type, exc_val, traceback):
Expand Down Expand Up @@ -72,16 +78,24 @@ def _close_output_buffer(self):
def fail(self, exitcode=1, text="FAIL"):
from .misc import decode_for_output
if text and text != "None":
self.write_err(decode_for_output(text))
if self.write_to_stdout:
self.write(decode_for_output(text))
else:
self.write_err(decode_for_output(text))
self._close_output_buffer()

def ok(self, text="OK"):
if text and text != "None":
self.stderr.write(self.text)
if self.write_to_stdout:
self.stdout.write(self.text)
else:
self.stderr.write(self.text)
self._close_output_buffer()
return 0

def write(self, text=None):
if not self.write_to_stdout:
return self.write_err(text)
from .misc import decode_for_output
if text is None or isinstance(text, six.string_types) and text == "None":
pass
Expand All @@ -102,26 +116,30 @@ def write_err(self, text=None):
self.stderr.write(CLEAR_LINE)

@staticmethod
def _hide_cursor():
def _hide_cursor(target=None):
pass

@staticmethod
def _show_cursor():
def _show_cursor(target=None):
pass


base_obj = yaspin.core.Yaspin if yaspin is not None else DummySpinner


class VistirSpinner(base_obj):
"A spinner class for handling spinners on windows and posix."

def __init__(self, *args, **kwargs):
"""Get a spinner object or a dummy spinner to wrap a context.
"""
Get a spinner object or a dummy spinner to wrap a context.
Keyword Arguments:
:param str spinner_name: A spinner type e.g. "dots" or "bouncingBar" (default: {"bouncingBar"})
:param str start_text: Text to start off the spinner with (default: {None})
:param dict handler_map: Handler map for signals to be handled gracefully (default: {None})
:param bool nospin: If true, use the dummy spinner (default: {False})
:param bool write_to_stdout: Writes to stdout if true, otherwise writes to stderr (default: True)
"""

self.handler = handler
Expand All @@ -145,36 +163,42 @@ def __init__(self, *args, **kwargs):
kwargs["text"] = start_text if start_text is not None else _text
kwargs["sigmap"] = sigmap
kwargs["spinner"] = getattr(Spinners, spinner_name, "")
write_to_stdout = kwargs.pop("write_to_stdout", True)
self.stdout = kwargs.pop("stdout", sys.stdout)
self.stderr = kwargs.pop("stderr", sys.stderr)
self.out_buff = StringIO()
super(VistirSpinner, self).__init__(*args, **kwargs)
self.write_to_stdout = write_to_stdout
self.is_dummy = bool(yaspin is None)
super(VistirSpinner, self).__init__(*args, **kwargs)

def ok(self, text="OK"):
def ok(self, text="OK", err=False):
"""Set Ok (success) finalizer to a spinner."""
# Do not display spin text for ok state
self._text = None

_text = text if text else "OK"
self._freeze(_text)
err = err or not self.write_to_stdout
self._freeze(_text, err=err)

def fail(self, text="FAIL"):
def fail(self, text="FAIL", err=False):
"""Set fail finalizer to a spinner."""
# Do not display spin text for fail state
self._text = None

_text = text if text else "FAIL"
self._freeze(_text)
err = err or not self.write_to_stdout
self._freeze(_text, err=err)

def write(self, text):
if not self.write_to_stdout:
return self.write_err(text)
from .misc import to_text
sys.stdout.write("\r")
self.stdout.write(CLEAR_LINE)
if text is None:
text = ""
text = to_native_string("{0}\n".format(text))
sys.stdout.write(text)
self.stdout.write(text)
self.out_buff.write(to_text(text))

def write_err(self, text):
Expand All @@ -189,7 +213,46 @@ def write_err(self, text):
self.stderr.write(text)
self.out_buff.write(to_text(text))

def _freeze(self, final_text):
def start(self):
if self._sigmap:
self._register_signal_handlers()

target = self.stdout if self.write_to_stdout else self.stderr
if target.isatty():
self._hide_cursor(target=target)

self._stop_spin = threading.Event()
self._hide_spin = threading.Event()
self._spin_thread = threading.Thread(target=self._spin)
self._spin_thread.start()

def stop(self):
if self._dfl_sigmap:
# Reset registered signal handlers to default ones
self._reset_signal_handlers()

if self._spin_thread:
self._stop_spin.set()
self._spin_thread.join()

target = self.stdout if self.write_to_stdout else self.stderr
if target.isatty():
target.write("\r")

if self.write_to_stdout:
self._clear_line()
else:
self._clear_err()

if target.isatty():
self._show_cursor(target=target)
if self.stderr and self.stderr != sys.stderr:
self.stderr.close()
if self.stdout and self.stdout != sys.stdout:
self.stdout.close()
self.out_buff.close()

def _freeze(self, final_text, err=False):
"""Stop spinner, compose last frame and 'freeze' it."""
if not final_text:
final_text = ""
Expand All @@ -199,15 +262,10 @@ def _freeze(self, final_text):
# Should be stopped here, otherwise prints after
# self._freeze call will mess up the spinner
self.stop()
self.stdout.write(self._last_frame)

def stop(self, *args, **kwargs):
if self.stderr and self.stderr != sys.stderr:
self.stderr.close()
if self.stdout and self.stdout != sys.stdout:
self.stdout.close()
self.out_buff.close()
super(VistirSpinner, self).stop(*args, **kwargs)
if err or not self.write_to_stdout:
self.stderr.write(self._last_frame)
else:
self.stdout.write(self._last_frame)

def _compose_color_func(self):
fn = functools.partial(
Expand Down Expand Up @@ -236,6 +294,29 @@ def _compose_out(self, frame, mode=None):
out = to_native_string("{0} {1}\n".format(frame, text))
return out

def _spin(self):
target = self.stdout if self.write_to_stdout else self.stderr
clear_fn = self._clear_line if self.write_to_stdout else self._clear_err
while not self._stop_spin.is_set():

if self._hide_spin.is_set():
# Wait a bit to avoid wasting cycles
time.sleep(self._interval)
continue

# Compose output
spin_phase = next(self._cycle)
out = self._compose_out(spin_phase)

# Write
target.write(out)
clear_fn()
target.flush()

# Wait
time.sleep(self._interval)
target.write("\b")

def _register_signal_handlers(self):
# SIGKILL cannot be caught or ignored, and the receiving
# process cannot perform any clean-up upon receiving this
Expand Down Expand Up @@ -273,12 +354,16 @@ def _reset_signal_handlers(self):
signal.signal(sig, sig_handler)

@staticmethod
def _hide_cursor():
cursor.hide()
def _hide_cursor(target=None):
if not target:
target = sys.stdout
cursor.hide(stream=target)

@staticmethod
def _show_cursor():
cursor.show()
def _show_cursor(target=None):
if not target:
target = sys.stdout
cursor.show(stream=target)

@staticmethod
def _clear_err():
Expand Down

0 comments on commit 589b1a4

Please sign in to comment.