ターミナル上で長めの処理をするときに使いたい、くっそ便利なぐるぐるクラス[Python]

# デモ
こんな感じのぐるぐるを簡単に作れます。
<img width="600" alt="spinner" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/503266/555b57dd-46f8-45b2-1052-0b7a7ea95b98.gif">


# コード
```python
class Spinner:
    def __init__(
        self,
        text: str = "Please wait...",
        etext: str = "",
        overwrite: bool = True,
    ) -> None:
        self.text = text
        self.end_text = etext
        self.overwrite = overwrite

    def start(self) -> None:
        self._stop_flag = False
        self._spinner_thread = threading.Thread(target=self._spinner)
        self._spinner_thread.setDaemon(True)
        self._spinner_thread.start()

    def stop(self, etext: str = "", overwrite: bool = True) -> None:
        if self._spinner_thread and self._spinner_thread.is_alive():
            self._stop_flag = True
            self._spinner_thread.join()

        text = etext or self.end_text
        overwrite = self.overwrite if not self.overwrite else overwrite

        if overwrite:
            if etext == "":
                print(f"\r\033[2K\033[G", end="")
            else:
                print(f"\r\033[2K\033[G{etext}")
        else:
            if etext == "":
                print(f"\033[1D\033[K\n", end="")
            else:
                print(f"\033[1D\033[K\n{etext}")

    def _spinner(self) -> None:
        chars = itertools.cycle(r"/-\|")
        while not self._stop_flag:
            print(f"\r{self.text} {next(chars)}", end="")
            time.sleep(0.2)

    def __enter__(self) -> None:
        self.start()

    def __exit__(self, exception_type, exception_value, traceback) -> None:
        self.stop()

    def __call__(self, func: Callable) -> Callable:
        @functools.wraps(func)
        def wrapped(*args, **kwargs):
            with self:
                return func(*args, **kwargs)

        return wrapped
```

# 使い方
`text`には、ぐるぐるを付与したい文、`etext`には処理終了時に表示したい文を指定します。

3つの使い方があります。

**start()とstop()の間に処理を書く方法**
```python
spinner = Spinner(text="Loading...", etxet="Loading... Done.")
spinner.start()

time.sleep(3)

spinner.stop()
```

**withで囲む方法**
```python
with Spinner("Loading...", "Loading... Done."):
    time.sleep(3)
```

**デコレータとして使う方法**
```python
@Spinner("Loading...", "Loading... Done.")
def my_func():
    time.sleep(3)

my_func()
```

実行結果はどれも同じです。
<img width="600" alt="spinner" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/503266/555b57dd-46f8-45b2-1052-0b7a7ea95b98.gif">

:::note info
`overwrite=False`で処理後の出力を次の行に指定することができます。
:::
```python
spinner = Spinner("Loading...", "Done.", overwrite=False)
spinner.start()

time.sleep(3)

spinner.stop()
```

<img width="600" alt="spinner" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/503266/ecde1c46-438f-c8a3-4ed3-4f6e042bb62e.gif">

## 実用例
start()とstop()の間に処理を書く方法では、stop()に`etext`を指定できるので、中の処理結果に応じた出力ができます。
```python
spinner = Spinner("当たるかな...")
spinner.start()

time.sleep(3)
n = np.random.randint(0, 2)
result = {0: "当たり！", 1: "ハズレ..."}

spinner.stop(etext=result[n], overwrite=False)
```

<img width="600" alt="spinner" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/503266/7863e794-86ce-6c45-2f93-2acfe311138a.gif">

運が悪かったようです。

# 終わりに
最近cliの開発をしているときにあったら便利だな〜ということで作成しました。
みなさまも是非お使いください。

# 参考にした記事等
https://qiita.com/txkodo/items/4f69c7a3e8afcb9927bf

https://qiita.com/exy81/items/99e99ab8c184343948cc

https://qiita.com/tchnkmr/items/b05f321fa315bbce4f77

2022/7/22
一部コード修正