2
\$\begingroup\$

This is a small python API to be able to display a busy spinner using emojis for a textual bot that run lengthy async jobs without access to a percentage of completion and need to provide feedback so the user knows the bot isn't stuck. I'm looking for any remarks (general, code quality, ease of use, ease of use of the API, code smell, ... ?)

How the code is used:

for progress_bar_emoji in SlackBusySpinner(timeout=3600):
    # Do something and react with progress_bar_emoji 
    # until job.is_finished or it timeout after 3600 s
    if job.is_finished:
        break

Code:

from __future__ import annotations


class SlackBusySpinner:
    def __init__(self, timeout: int | None = None):
        self.__progress_bar_index = 0
        self.timeout = timeout

    def __iter__(self):
        return self

    def __next__(self) -> str:
        if self.timeout is not None and self.__progress_bar_index > self.timeout:
            raise StopIteration
        minutes = "30" if self.__progress_bar_index % 2 else ""
        clock_number = (self.__progress_bar_index % 24 // 2) + 1
        emoji = f"clock{clock_number}{minutes}"
        self.__progress_bar_index += 1
        return emoji

Tests:

from emoji_progress_bar import SlackBusySpinner


def test_clock_emoji() -> None:
    expected = [
        "clock1",  # 🕐
        "clock130", # 🕜
        "clock2",  # 🕑
        "clock230", # etc.
        "clock3",
        "clock330",
        "clock4",
        "clock430",
        "clock5",
        "clock530",
        "clock6",
        "clock630",
        "clock7",
        "clock730",
        "clock8",
        "clock830",
        "clock9",
        "clock930",
        "clock10",
        "clock1030",
        "clock11",
        "clock1130",
        "clock12",
        "clock1230",
    ]
    emojis = []
    slack_answer = SlackBusySpinner()
    for _ in range(48):
        emojis.append(next(slack_answer))
    assert emojis == expected * 2

Result:

🕐 then 🕜, then 🕑, etc. ...back to 🕐 then 🕜, then 🕑, etc.

\$\endgroup\$

1 Answer 1

2
\$\begingroup\$

I don't think it's a good idea to conflate your display spinner with a timeout. These should be separated.

Don't double-underscore your index; that's reserved for a different purpose.

clock_number is just "hour".

Your test can be simplified by using islice instead of successive concatenation:

import itertools


class SlackBusySpinner:
    def __init__(self) -> None:
        self._index = 0

    def __iter__(self) -> 'SlackBusySpinner':
        return self

    def __next__(self) -> str:
        minute = '30' if self._index % 2 else ''
        hour = (self._index % 24 // 2) + 1
        emoji = f'clock{hour}{minute}'
        self._index += 1
        return emoji


def test_clock_emoji() -> None:
    expected = (
        "clock1",  "clock130",
        "clock2",  "clock230",
        "clock3",  "clock330",
        "clock4",  "clock430",
        "clock5",  "clock530",
        "clock6",  "clock630",
        "clock7",  "clock730",
        "clock8",  "clock830",
        "clock9",  "clock930",
        "clock10", "clock1030",
        "clock11", "clock1130",
        "clock12", "clock1230",
    )
    emojis = tuple(itertools.islice(SlackBusySpinner(), 48))
    assert emojis == expected * 2


if __name__ == '__main__':
    test_clock_emoji()

Unicode

There is an alternative with trade-offs: write out the clock face unicode code points, and put them in a cycle. This might be able to render better for text-only clients (not that those are common for Slack), and it might also place a smaller performance burden than Slack emoji lookup and render; but it also carries a risk that the client doesn't have a good font.

import itertools


CLOCK_POINTS = (
    '\U0001F55B' '\U0001F567'  # 1200
    '\U0001F550' '\U0001F55C'  # 0100
    '\U0001F551' '\U0001F55D'  # 0200
    '\U0001F552' '\U0001F55E'  # 0300
    '\U0001F553' '\U0001F55F'  # 0400
    '\U0001F554' '\U0001F560'  # 0500
    '\U0001F555' '\U0001F561'  # 0600
    '\U0001F556' '\U0001F562'  # 0700
    '\U0001F557' '\U0001F563'  # 0800
    '\U0001F558' '\U0001F564'  # 0900
    '\U0001F559' '\U0001F565'  # 1000
    '\U0001F55A' '\U0001F566'  # 1100
)

SlackBusySpinner = itertools.cycle(CLOCK_POINTS)
print(' '.join(itertools.islice(SlackBusySpinner, 48)))

outputs

🕛 🕧 🕐 🕜 🕑 🕝 🕒 🕞 🕓 🕟 🕔 🕠 🕕 🕡 🕖 🕢 🕗 🕣 🕘 🕤 🕙 🕥 🕚 🕦 🕛 🕧 🕐 🕜 🕑 🕝 🕒 🕞 🕓 🕟 🕔 🕠 🕕 🕡 🕖 🕢 🕗 🕣 🕘 🕤 🕙 🕥 🕚 🕦

\$\endgroup\$
6
  • \$\begingroup\$ Thank you for the insightful answer. I wanted to make the index private to hide the "implementation details" of the class. What would be the purpose of the double underscore if not that ? But I guess it can be useful to access it to know the number of tick if used with next. \$\endgroup\$ Commented Sep 29, 2023 at 20:58
  • 1
    \$\begingroup\$ (a) no amount of underscores will completely prevent someone from accessing the member; "private" is only a suggestion in Python; but \$\endgroup\$
    – Reinderien
    Commented Sep 29, 2023 at 21:15
  • \$\begingroup\$ (b) read docs.python.org/3.11/reference/… . \$\endgroup\$
    – Reinderien
    Commented Sep 29, 2023 at 21:16
  • \$\begingroup\$ By convention, one underscore means "private". Two underscores means "name-mangle". But the whole concept of private variables in Python is rather flimsy. \$\endgroup\$
    – Reinderien
    Commented Sep 29, 2023 at 21:28
  • \$\begingroup\$ I always considered one underscore as protected because the attribute keep being accessible in child classes, whereas two leading underscores is private. According to the doc you linked "two or more underscore characters (...) is considered a private name of that class". \$\endgroup\$ Commented Sep 30, 2023 at 19:05

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.