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

feat: add parameter to for output stream #235

Open
wants to merge 5 commits into
base: master
Choose a base branch
from

Conversation

bpshaver
Copy link

The addition of the stream parameter allows the user to choose to send the yaspin spinner output to a stream or TextIO object other than sys.stdout. The most obvious use for this feature is to set the stream to stderr to support piping of output like python main.py > some.log and seeing the yaspin spinner in the terminal while stdout goes to some.log

The addition of the `stream` parameter allows the user to choose to send the yaspin spinner output to a stream or TextIO object other than sys.stdout. The most obvious use for this feature is to set the stream to stderr to support piping of output like `python main.py > some.log` and seeing the yaspin spinner in the terminal while stdout goes to `some.log`
@@ -591,24 +612,3 @@ def _set_interval(spinner: Spinner) -> float:
def _set_cycle(frames: Union[str, Sequence[str]]) -> Iterator[str]:
return itertools.cycle(frames)

@staticmethod
Copy link
Author

Choose a reason for hiding this comment

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

Because of calls so isatty these can no longer be static methods

@pavdmyt
Copy link
Owner

pavdmyt commented Oct 18, 2023

@bpshaver please run make fmt on your changes so that check-fmt check can succeed in CI.

@bpshaver
Copy link
Author

@bpshaver please run make fmt on your changes so that check-fmt check can succeed in CI.

Done.

Does this seem like a sensible change? If so, I'll look through the failing test cases now.

Leaving the default parameter of `stream` as `None` and using `sys.stdout` helps in cases when `sys.stdout` is patched between class definition and instance creation. This helps a few previously failing test cases pass.
…unction

`test_in_out.py::test_hide_show` was failing after my changes due to the teardown function calling `.stop()` on a spinner that somehow already had its output stream closed. This may indicate a problem, but I put `.stop()` in a try/except block since it seems inappropriate for a test to fail because a teardown function raises an exception.
@bpshaver bpshaver marked this pull request as ready for review October 18, 2023 17:59
@@ -120,7 +120,13 @@ def test_hide_show(monkeypatch, capsys, text, request, isatty_fixture):

# Ensure that sp.stop() will be executed
def teardown():
sp.stop()
try:
sp.stop()
Copy link
Author

@bpshaver bpshaver Oct 18, 2023

Choose a reason for hiding this comment

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

I don't know why sp.stop() was raising an exception due to an already closed file.

@pavdmyt
Copy link
Owner

pavdmyt commented Oct 22, 2023

@bpshaver thanks for your contribution! I'll try to review this thoroughly next week.

@pavdmyt
Copy link
Owner

pavdmyt commented Oct 26, 2023

Apparently it is possible to detect situations when different streams are not tty:

import os

if not os.isatty(sys.stdout.fileno()):
    print("The output stream is not a TTY.")
else:
    print("The output stream is a TTY.")

So, I would rather prefer this approach, aligning behavior with e.g. #31 (comment) to move further here. It must be much more convenient than requiring developer to define custom streams upfront. Plus, if yaspin is capable to find out which streams it is attached to, there is no need to rewrite older code.

@bpshaver
Copy link
Author

bpshaver commented Oct 26, 2023

@pavdmyt I do not understand how this is more convenient. The idea of detecting whether the target stream is a TTY and changing behavior or issuing a warning is a good one. But whether that stream is always stdout or whether it should be configurable by the user is a separate question.

If anything, #31 (comment) supports making sys.stderr the default stream, not sys.stdout. That seems like a good idea to me, but I am suggesting a less drastic change, which is simply that yaspin open up the possibility for a user to send the spinner to stderr. It is also more explicit and it is backwards compatible. I disagree with your comment:

It must be much more convenient than requiring developer to define custom streams upfront.

In fact, my proposed change keeps sys.stdout as the default value (when stream is None) so it does not require anything from a developer who wants to continue using yaspin as before. Again, the change is completely backwards compatible. And there is no need to define custom streams. stdout or stderr should both be compatible streams and neither are "custom."

Plus, if yaspin is capable to find out which streams it is attached to

Just because yaspin can determine that doesn't mean it knows what it should do with the corresponding stream. If it detects that stdout is not a TTY, the user may want the spinner to appear in stderr, as in the case I show below. In general, explicit is better than implicit (Zen of Python).

My proposed changes support a file like this:

import sys
from time import sleep

from yaspin import yaspin


with yaspin(stream=sys.stderr):
    for i in range(5):
        print(f"{i} - Log some data")
        sleep(1)

being piped to a log file like so:

$ python foo.py > foo.log
⠋

where the spinner shows during script execution and the log file ends up looking like this:

0 - Log some data
1 - Log some data
2 - Log some data
3 - Log some data
4 - Log some data

Without stream = sys.stderr you get all the spinner artifacts in foo.log. Am I missing something? How does yaspin in its current state support this case?

@pavdmyt
Copy link
Owner

pavdmyt commented Oct 30, 2023

I think, I need to elaborate a bit more about my point. The idea behind capability to automatically detect streams to which yaspin is attached to allows to separate:

  • spinner's stream (escape and color codes, spinner frames) from the
  • useful text (.text attribute, write() and possibly .ok() and .fail() methods)

So that spinner's stream is always ends up in stderr (backwards incompatible change) and the useful text in stdout. This way, you can make decisions which stream ends up where by simply using redirects and the default behavior stays (visually) the same, as both stderr and stdout are attached to the TTY by default. This would allow doing python foo.py > foo.log (from your example above) having the useful text inside foo.log, while spinner's stream got rendered in the terminal window (as stderr remained attached to TTY). And this doesn't require any code changes using stream argument or attribute.

To summarize, we want to have spinner's stream and useful text clearly separated so that user can decide what to do with these streams. Having such feature, I'm not sure about the value of having an additional stream argument (please correct me if I'm wrong or in case I'm missing something).

@bpshaver
Copy link
Author

bpshaver commented Nov 1, 2023

I understand now, thanks!

I would call the .text, etc. accompanying text. Under normal circumstances, it is right next to the spinner. You're assuming that when the output is redirected to a non-TTY stream, however, the accompanying text should be interpreted as useful text and go to that non-TTY stream. But I'm not so sure that assumption makes sense for all users.

In the issue I was trying to tackle ( mandiant/capa#1812 ) the accompanying text should go to stderr (remaining next to the spinner graphic) while useful text going to the non-TTY stream is everything contained within the spinner context:

$ python foo.py > foo.log
⠋ Analyzing data...

while program output goes to foo.log.

I understand a sensible default may be to send accompanying text to the non-TTY stream, but this example I think illustrates a case where that is not desired.

@pavdmyt
Copy link
Owner

pavdmyt commented Nov 3, 2023

In the issue I was trying to tackle ( mandiant/capa#1812 ) the accompanying text should go to stderr (remaining next to the spinner graphic) while useful text going to the non-TTY stream is everything contained within the spinner context

I'm not sure I've understood this correctly, "accompanying text should go to stderr while useful text going to the non-TTY stream" - isn't accompanying and useful text are the same entities? It is also possible to redirect stderr to the non-TTY stream.

@bpshaver
Copy link
Author

bpshaver commented Nov 3, 2023

accompanying text is my name for whatever text normally goes right next to the spinner. You called it useful text, which is begging the question with respect to where it should go in the corner cases we're discussing. Call them what you like, but I'm pointing to three different types of text (not two):

  • The actual spinner - should probably never go to a non-TTY stream
  • Some text to the right of the spinner (.text, etc.) - maybe should go to a non-TTY stream. Probably depends on the user.
  • Normal text written as result of print calls and the like - should be redirected as normal, unaffected by being within a yaspin spinner context.

I just don't see the utility in sending the spinner and any text to the right of it (determined by .text attribute) to different streams. That seems overly complex and probably a surprise to most users. Users can call print or whatever they during the normal execution of their code within the spinner context to send text to whatever stream they like. There's no reason for yaspin to make that decision for them.

@@ -374,15 +378,11 @@ def fail(self, text: str = "FAIL") -> None:
_text = text if text else "FAIL"
self._freeze(_text)

def is_jupyter(self) -> bool:

Choose a reason for hiding this comment

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

I am pretty sure that Jupyter is not the only front-end capable of intercepting the output stream.

@@ -533,6 +544,15 @@ def _set_attrs(attrs: Sequence[str]) -> set[str]:
)
return set(attrs)

# Static

Choose a reason for hiding this comment

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

What is the purpose of this comment?

@odisseus
Copy link

odisseus commented Mar 28, 2024

What is the status of this pull request? How can I help getting it merged and released?

@bpshaver
Copy link
Author

Seems like @pavdmyt disagreed with my reasoning and the discussion ended.

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