Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix animation of long lines #247

Merged
merged 19 commits into from
Oct 29, 2019
Merged

Conversation

itsayellow
Copy link
Contributor

This fixes a problem with the text animation during install. With a long --spec, the animation during install of a pipx package can be corrupted, resulting in a page or more of spurious identical messages being printed. This is because the \r carriage return does not work if the printed text is so long that it moves the cursor to the next line. Under these circumstances, \r only moves to the beginning of this terminal line, leaving the previous part of the printed text still printed above it.

The patch truncates the animated printed text so it does not run over to the next line of the terminal. It uses shutil.get_terminal_size(). The fallback in case shutil.get_terminal_size() cannot determine the terminal size is 9999 columns, essentially "infinity", making sure that we revert to the original behavior if we cannot determine the actual terminal size (or we are in some circumstance like a pipe, etc. where it wouldn't matter anyway.)

Current behavior:

birchtree:pipx$ pipx install --spec git+https://github.com/itsayellow/pytivometa pytivometa
⣷ installing package 'git+https://github.com/itsayellow/pytivometa#egg=pytivomet⣯ installing package 'git+https://github.com/itsayellow/pytivometa#egg=pytivomet⣟ installing package 'git+https://github.com/itsayellow/pytivometa#egg=pytivomet⡿ installing package 'git+https://github.com/itsayellow/pytivometa#egg=pytivomet⢿ installing package 'git+https://github.com/itsayellow/pytivometa#egg=pytivomet⣻ installing package 'git+https://github.com/itsayellow/pytivometa#egg=pytivomet⣽ installing package 'git+https://github.com/itsayellow/pytivometa#egg=pytivomet⣾ installing package 'git+https://github.com/itsayellow/pytivometa#egg=pytivomet⣷ installing package 'git+https://github.com/itsayellow/pytivometa#egg=pytivomet⣯ installing package 'git+https://github.com/itsayellow/pytivometa#egg=pytivomet⣟ installing package 'git+https://github.com/itsayellow/pytivometa#egg=pytivomet⡿ installing package 'git+https://github.com/itsayellow/pytivometa#egg=pytivomet⢿ installing package 'git+https://github.com/itsayellow/pytivometa#egg=pytivomet⣻ installing package 'git+https://github.com/itsayellow/pytivometa#egg=pytivomet⣽ installing package 'git+https://github.com/itsayellow/pytivometa#egg=pytivomet⣾ installing package 'git+https://github.com/itsayellow/pytivometa#egg=pytivomet⣷ installing package 'git+https://github.com/itsayellow/pytivometa#egg=pytivomet⣯ installing package 'git+https://github.com/itsayellow/pytivometa#egg=pytivomet⣟ installing package 'git+https://github.com/itsayellow/pytivometa#egg=pytivomet⡿ installing package 'git+https://github.com/itsayellow/pytivometa#egg=pytivomet⢿ installing package 'git+https://github.com/itsayellow/pytivometa#egg=pytivomet⣻ installing package 'git+https://github.com/itsayellow/pytivometa#egg=pytivomet⣽ installing package 'git+https://github.com/itsayellow/pytivometa#egg=pytivomet⣾ installing package 'git+https://github.com/itsayellow/pytivometa#egg=pytivomet⣷ installing package 'git+https://github.com/itsayellow/pytivometa#egg=pytivomet⣯ installing package 'git+https://github.com/itsayellow/pytivometa#egg=pytivomet⣟ installing package 'git+https://github.com/itsayellow/pytivometa#egg=pytivomet⡿ installing package 'git+https://github.com/itsayellow/pytivometa#egg=pytivomet⢿ installing package 'git+https://github.com/itsayellow/pytivometa#egg=pytivomet⣻ installing package 'git+https://github.com/itsayellow/pytivometa#egg=pytivomet⣽ installing package 'git+https://github.com/itsayellow/pytivometa#egg=pytivomet⣾ installing package 'git+https://github.com/itsayellow/pytivometa#egg=pytivomet⣷ installing package 'git+https://github.com/itsayellow/pytivometa#egg=pytivomet⣯ installing package 'git+https://github.com/itsayellow/pytivometa#egg=pytivomet⣟ installing package 'git+https://github.com/itsayellow/pytivometa#egg=pytivomet⡿ installing package 'git+https://github.com/itsayellow/pytivometa#egg=pytivomet  installed package pytivometa 0.2.0, Python 3.7.4
  These apps are now globally available
    - pytivometa
done! ✨ 🌟 ✨
birchtree:pipx$ 

(Note, in my Terminal this wraps so it looks like many lines.)

With this pull request:

During:

birchtree:pipx$ pipx install --spec git+https://github.com/itsayellow/pytivometa pytivometa
⣽ installing package 'git+https://github.com/itsayellow/pytivometa#egg=pytiv...

Afterwards:

birchtree:pipx$ pipx install --spec git+https://github.com/itsayellow/pytivometa pytivometa
  installed package pytivometa 0.2.0, Python 3.7.4
  These apps are now globally available
    - pytivometa
done! ✨ 🌟 ✨
birchtree:pipx$ 

@itsayellow itsayellow changed the title Animate long lines Fix animation of long lines Oct 21, 2019
@cs01
Copy link
Member

cs01 commented Oct 21, 2019

Nice!

Should the size be retrieved more frequently since there is no guarantee it will remain the same during the entire session?

@itsayellow
Copy link
Contributor Author

Should the size be retrieved more frequently since there is no guarantee it will remain the same during the entire session?

I'm trying to think of the ways that might happen--would it only happen if the user resized the terminal while a pipx command was running?

I wasn't worrying about it, but get_terminal_size could easily be put inside of print_animation() or even more real-time, inside of the for s in symbols: loop. I don't know if there's any cost to this, but I'm guessing not much.

@itsayellow
Copy link
Contributor Author

I think something like reinstall-all is probably the longest-running command, and that takes long enough a user could easily resize the terminal, which would screw things up as it stands now.

@itsayellow
Copy link
Contributor Author

OK, I moved get_terminal_size() inside of animate() which seemed pretty obviously a good thing.

Do you think it needs to be in the for s in symbols: loop?

pipx/animate.py Outdated Show resolved Hide resolved
pipx/animate.py Outdated
cur_line = f"{message}{s}"
if len(message) <= term_cols - 4:
cur_line = f"{message}{s}"
else:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also I think for this to not suffer easily regressions going ahead we should add some tests that validate the change.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree, but I'm frankly not sure how to do that with animated (transient) text. Anybody have a pointer or ideas?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when the pytest capsys is attached the override does not happen, allowing you to check byte-wise; see https://github.com/tox-dev/tox/blob/master/tests/unit/util/test_spinner.py#L60-L81 for example

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice. And the test above it is testing with animation.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another tidbit I just figured out, is that by setting env variables COLUMNS and ROWS you can make shutil.get_terminal_size() return values at your command.

pipx/animate.py Outdated Show resolved Hide resolved
Comment on lines 7 to 9
HIDE_CURSOR = "\033[?25l"
SHOW_CURSOR = "\033[?25h"
CLEAR_LINE = "\033[K"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These should be defined in animate.py and imported here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.


frame_strings = [
f"{CLEAR_LINE}\r{x} {expected_message[i]}"
for x in ["⣷", "⣯", "⣟", "⡿", "⢿", "⣻", "⣽", "⣾"]
Copy link
Member

@cs01 cs01 Oct 27, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the animation list should be imported from animate.py

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Comment on lines 35 to 36
pipx.animate.stderr_is_tty = True
pipx.animate.emoji_support = True
Copy link
Member

@cs01 cs01 Oct 27, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Recommend defining a fixture to ensure pipx.animate.stderr_is_tty, pipx.animate.emoji_support values are stored at entry to the fixture, then yield to let the test run, then reset the values to their orignal value in the fixture. We should not assume what they were at the time they were computed on first pass.

https://docs.pytest.org/en/latest/fixture.html#fixture-finalization-executing-teardown-code

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, for the hint.
Done.

Copy link
Contributor Author

@itsayellow itsayellow Oct 28, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Used @gaborbernat 's tip about monkeypatch instead, but if things get more complicated in the future I am happy to know how to do teardown.

Comment on lines 38 to 40
frames_to_test = 4
# matches animate.py
frame_period = 0.1
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Recommend making these values importable from animate.py and using them directly

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@@ -0,0 +1,93 @@
#!/usr/bin/env python3
import time
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is great! Thanks for adding these tests.

@@ -0,0 +1,93 @@
#!/usr/bin/env python3
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is probably a copy+paste thing, but in general pipx has a bunch of these at the top of files that should be removed. It can be removed here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ack, yes. Force of habit!
Done.

captured = capsys.readouterr()

# print for debug help if fail
print("expected_string:")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

adding '-rvva --showlocals`` achieves the same, not?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, didn't know about that.

def test_line_lengths_no_emoji(capsys, monkeypatch):
# emoji_support and stderr_is_tty is set only at import animate.py
# since we are already after that, we must override both here
pipx.animate.stderr_is_tty = True
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use monkeypatch instead of direct set to ensure their correctly reset in case tests fails for the next test

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the tip!

pipx/animate.py Outdated
Comment on lines 15 to 16
EMOJI_ANIMATION_FRAMES = ["⣷", "⣯", "⣟", "⡿", "⢿", "⣻", "⣽", "⣾"]
NONEMOJI_ANIMATION_FRAMES = ["", ".", "..", "..."]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice

@itsayellow
Copy link
Contributor Author

Am I clear to merge this now? I think I've addressed all the comments.

@itsayellow itsayellow merged commit 33813d1 into pypa:master Oct 29, 2019
@itsayellow itsayellow deleted the animate_long_lines branch October 29, 2019 17:00
@cs01
Copy link
Member

cs01 commented Oct 29, 2019

I believe this is the first PR that I have not approved+merged! Thank you and congrats! 🎉

In the future please squash commits to avoid polluting commits on the master branch.

@itsayellow
Copy link
Contributor Author

In the future please squash commits to avoid polluting commits on the master branch.

Ah ok! Is the idea one commit per PR?

@gaborbernat
Copy link
Contributor

I think the idea is more one commit per feature/bug fix.

@cs01
Copy link
Member

cs01 commented Oct 29, 2019

The idea is as many commits on a PR as you want, but when you click the "merge" button, there are a couple options.

One is "merge and squash commits into a single commit" and the other is "merge all commits" (the exact text is not this, but this is the idea). We should always opt for the "squash" option.

If you look at the commit history, you generally see a single commit with the pull request number in parentheses, such as

Add printed summary after successful injection. (#226)

this is what we want, so you can look through commits on master and get a basic idea of all the PRs that went into the codebase.

If you don't mind, I will do some work with git and do a force push so the master branch looks like this so there is a single commit for this PR.

@itsayellow
Copy link
Contributor Author

I don't mind at all, thanks for offering to clean it up. Thanks for the tip.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants