Repository: pipxproject/pipx (python)

Issue:
clear_line() isn't working on Windows Powershell
<!--
Thank you for filing a bug! Please feel free to answer as much or as little of this template as you can.

Please check pipx's Troubleshooting page to see if any of those solutions help solve your issue:
https://pipxproject.github.io/pipx/troubleshooting/
-->

**Describe the bug**
<!-- Please be as detailed as possible! -->
I think this may be an "upstream" bug, but I just wanted to note it.

If `colorama.init()` has been executed in pipx (normal operation), executing `pipx.animate.clear_line()` does not actually erase the current line on Powershell in Windows.

I commented out the `colorama.init()` statement and on Windows powershell, the `clear_line()` functions properly and erases the final animation frame.

**How to reproduce**
<!-- If possible, include output of `pipx --verbose ...` -->
On Windows Powershell:
```shell
pipx install nox
pipx reinstall-all
```

What is output is that "nox" is short enough that one can tell that "upgrading shared libraries" is not erased before "uninstalled nox!" (see the "ies" still visible past the stars):

```shell
> pipx reinstall-all
uninstalled nox! ✨ 🌟 ✨ies
  installed package nox 2020.12.31, Python 3.9.4
  These apps are now globally available
    - nox.exe
    - tox-to-nox.exe
done! ✨ 🌟 ✨
>
```

Pull request:
Make clear_line() in animate.py work reliably
<!-- add an 'x' in the brackets below -->
* [ ] I have added an entry to `docs/changelog.md`

## Summary of changes
Closes #673 

I noticed that `clear_line()` in `animate.py` wasn't working on Windows.  

It appears that the code wasn't structured properly.  The escape code sequence used is supposed delete everything from the current cursor position to the end of the line, and at the time it was invoked the cursor was at the end of the line, so it only deleted from there to the end of the line, i.e. deleted not much of anything.

I modified the code so that `clear_line()` first prints a linefeed character, positioning the cursor at the start of the line before executing the escape code to clear the line from cursor to end of line.  We always wanted to print a linefeed anyway when we executed `clear_line()`, so it worked out well.  The result is just putting linefeed before `CLEAR_LINE` instead of after.

In addition, I had to add a `sys.stderr.flush()` to the `animate` context manager code.  With the new `clear_line()`, the buffering tended to just show an empty line instead of the non-empty characters.  (This was possibly because `clear_line()` is actually working now.)  Flushing after printing the animation frame and before waiting ensures that the frame will be visible during the wait.


Patch:
diff --git a/src/pipx/animate.py b/src/pipx/animate.py
index 962c09c..5814ec7 100644
--- a/src/pipx/animate.py
+++ b/src/pipx/animate.py
@@ -68,8 +68,6 @@ def animate(
     finally:
         event.set()
         clear_line()
-        sys.stderr.write("\r")
-        sys.stdout.write("\r")
 
 
 def print_animation(
@@ -95,8 +93,8 @@ def print_animation(
                 cur_line = f"{message:.{max_message_len}}{s}"
 
             clear_line()
-            sys.stderr.write("\r")
             sys.stderr.write(cur_line)
+            sys.stderr.flush()
             if event.wait(period):
                 break
 
@@ -130,5 +128,6 @@ def show_cursor() -> None:
 
 
 def clear_line() -> None:
-    sys.stderr.write(f"{CLEAR_LINE}")
-    sys.stdout.write(f"{CLEAR_LINE}")
+    """Clears current line and positions cursor at start of line"""
+    sys.stderr.write(f"\r{CLEAR_LINE}")
+    sys.stdout.write(f"\r{CLEAR_LINE}")


Test patch:
diff --git a/tests/test_animate.py b/tests/test_animate.py
index 8999f9f..4c612a2 100644
--- a/tests/test_animate.py
+++ b/tests/test_animate.py
@@ -82,7 +82,7 @@ def test_line_lengths_emoji(capsys, monkeypatch, env_columns, expected_frame_mes
 
     frames_to_test = 4
     frame_strings = [
-        f"{CLEAR_LINE}\r{x} {expected_frame_message}" for x in EMOJI_ANIMATION_FRAMES
+        f"\r{CLEAR_LINE}{x} {expected_frame_message}" for x in EMOJI_ANIMATION_FRAMES
     ]
     check_animate_output(
         capsys, TEST_STRING_40_CHAR, frame_strings, EMOJI_FRAME_PERIOD, frames_to_test
@@ -109,7 +109,7 @@ def test_line_lengths_no_emoji(
 
     frames_to_test = 2
     frame_strings = [
-        f"{CLEAR_LINE}\r{expected_frame_message}{x}" for x in NONEMOJI_ANIMATION_FRAMES
+        f"\r{CLEAR_LINE}{expected_frame_message}{x}" for x in NONEMOJI_ANIMATION_FRAMES
     ]
 
     check_animate_output(