From db406ea8efac2c3aa224881f56825aa990bb6310 Mon Sep 17 00:00:00 2001 From: Farzin Date: Sun, 9 May 2021 01:18:42 +0430 Subject: [PATCH 1/4] simplified and add some deleter --- halo/__init__.py | 2 ++ halo/_utils.py | 21 +++++---------------- halo/cursor.py | 3 ++- halo/halo.py | 44 ++++++++++++++++++++++++++++++++++++++------ tests/__init__.py | 1 + 5 files changed, 48 insertions(+), 23 deletions(-) diff --git a/halo/__init__.py b/halo/__init__.py index e9d67d9..bc259a7 100644 --- a/halo/__init__.py +++ b/halo/__init__.py @@ -2,6 +2,8 @@ __author__ = 'Manraj Singh' __email__ = 'manrajsinghgrover@gmail.com' +all = ["cursor.py", "halo_notebook.py", "halo.py"] + import logging from .halo import Halo diff --git a/halo/_utils.py b/halo/_utils.py index 999a95b..f8286a1 100644 --- a/halo/_utils.py +++ b/halo/_utils.py @@ -26,10 +26,8 @@ def is_supported(): os_arch = platform.system() - if os_arch != 'Windows': - return True + return True if os_arch != 'Windows' else False - return False def get_environment(): @@ -42,10 +40,7 @@ def get_environment(): """ try: from IPython import get_ipython - except ImportError: - return 'terminal' - - try: + shell = get_ipython().__class__.__name__ if shell == 'ZMQInteractiveShell': # Jupyter notebook or qtconsole @@ -55,7 +50,7 @@ def get_environment(): else: return 'terminal' # Other type (?) - except NameError: + except (ImportError, NameError): return 'terminal' @@ -90,10 +85,7 @@ def is_text_type(text): bool Whether parameter is a string or not """ - if isinstance(text, six.text_type) or isinstance(text, six.string_types): - return True - - return False + return True if isinstance(text, (six.text_type, six.string_types)) else False def decode_utf_8_text(text): @@ -146,7 +138,4 @@ def get_terminal_columns(): # If column size is 0 either we are not connected # to a terminal or something else went wrong. Fallback to 80. - if terminal_size.columns == 0: - return 80 - else: - return terminal_size.columns + return 80 if terminal_size.columns == 0 else terminal_size.columns diff --git a/halo/cursor.py b/halo/cursor.py index b0e54c9..6554757 100644 --- a/halo/cursor.py +++ b/halo/cursor.py @@ -9,7 +9,8 @@ import ctypes class _CursorInfo(ctypes.Structure): - _fields_ = [("size", ctypes.c_int), ("visible", ctypes.c_byte)] + _fields_ = [("size", ctypes.c_int), + ("visible", ctypes.c_byte)] def hide(stream=sys.stdout): diff --git a/halo/halo.py b/halo/halo.py index 9e10b66..2b772be 100644 --- a/halo/halo.py +++ b/halo/halo.py @@ -77,6 +77,15 @@ def __init__( stream : io, optional Output. """ + + # To reset Values in deleter + self.reset_values = {"text": text, + "color": color, + "text_color": text_color, + "spinner": spinner, + "animation": animation, + "placement": placement,} + self._color = color self._animation = animation @@ -156,6 +165,12 @@ def spinner(self, spinner=None): self._frame_index = 0 self._text_index = 0 + @spinner.deleter + def spinner(self): + """set spinner to None when delete spinner is + """ + self._spinner = self.reset_values["spinner"] + @property def text(self): """Getter for text property. @@ -176,6 +191,10 @@ def text(self, text): """ self._text = self._get_text(text) + @text.deleter + def text(self): + self.text = self.reset_values["text"] + @property def text_color(self): """Getter for text color property. @@ -196,6 +215,10 @@ def text_color(self, text_color): """ self._text_color = text_color + @text_color.deleter + def text_color(self): + self._text_color = self.reset_values["text_color"] + @property def color(self): """Getter for color property. @@ -216,6 +239,10 @@ def color(self, color): """ self._color = color + @color.deleter + def color(self): + self._color = self.reset_values["color"] + @property def placement(self): """Getter for placement property. @@ -242,6 +269,10 @@ def placement(self, placement): ) self._placement = placement + @placement.deleter + def placement(self): + self.placement = self.reset_values["placement"] + @property def spinner_id(self): """Getter for spinner id @@ -273,6 +304,10 @@ def animation(self, animation): self._animation = animation self._text = self._get_text(self._text["original"]) + @animation.deleter + def animation(self): + self._animation = self.reset_values["animation"] + def _check_stream(self): """Returns whether the stream is open, and if applicable, writable Returns @@ -334,10 +369,7 @@ def _get_spinner(self, spinner): return spinner if is_supported(): - if all([is_text_type(spinner), spinner in Spinners.__members__]): - return Spinners[spinner].value - else: - return default_spinner + return Spinners[spinner].valueif if all([is_text_type(spinner), spinner in Spinners.__members__]) else default_spinner else: return Spinners["line"].value @@ -354,8 +386,8 @@ def _get_text(self, text): max_spinner_length = max([len(i) for i in self._spinner["frames"]]) # Subtract to the current terminal size the max spinner length - # (-1 to leave room for the extra space between spinner and text) - terminal_width = get_terminal_columns() - max_spinner_length - 1 + # (+1 to leave room for the extra space between spinner and text) + terminal_width = get_terminal_columns() - (max_spinner_length + 1) text_length = len(stripped_text) frames = [] diff --git a/tests/__init__.py b/tests/__init__.py index e69de29..1695b10 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -0,0 +1 @@ +all = ["test_halo_notebook.py", "test_halo.py"] \ No newline at end of file From 6ae75b80e3f45132306854606b5ea5dc350637d3 Mon Sep 17 00:00:00 2001 From: Farzin Date: Sun, 9 May 2021 07:39:15 +0430 Subject: [PATCH 2/4] fix issue #157 fix issue #157 -+- change decorator implementation -+- add an example of new decorator approach -+- it needs to write new tests but example work well --- examples/colored_text_spin.py | 2 +- examples/dict_text_loop.py | 15 +++++++ halo/halo.py | 73 +++++++++++++++++++++++++++++------ 3 files changed, 77 insertions(+), 13 deletions(-) create mode 100644 examples/dict_text_loop.py diff --git a/examples/colored_text_spin.py b/examples/colored_text_spin.py index f1e36a6..6721025 100644 --- a/examples/colored_text_spin.py +++ b/examples/colored_text_spin.py @@ -23,6 +23,6 @@ spinner.spinner = 'hearts' spinner.text_color = 'magenta' time.sleep(2) - spinner.stop_and_persist(symbol='🦄 '.encode('utf-8'), text='Wow!') + spinner.stop_and_persist(symbol='🦄'.encode('utf-8'), text='Wow!') except (KeyboardInterrupt, SystemExit): spinner.stop() diff --git a/examples/dict_text_loop.py b/examples/dict_text_loop.py new file mode 100644 index 0000000..f7462eb --- /dev/null +++ b/examples/dict_text_loop.py @@ -0,0 +1,15 @@ +from halo import Halo +from time import sleep + + +@Halo(text='Loading {task}', spinner='line') +def run_task(halo_iter=[], stop_text='', stop_symbol=' '): + sleep(.5) + + +tasks1 = ['breakfest', 'launch', 'dinner'] +tasks2 = ['morning', 'noon', 'night'] + +run_task(halo_iter=tasks1, stop_symbol='🦄'.encode( + 'utf-8'), stop_text='Task1 Finished') +run_task(halo_iter=tasks2, stop_text='Finished Time') diff --git a/halo/halo.py b/halo/halo.py index 2b772be..823279f 100644 --- a/halo/halo.py +++ b/halo/halo.py @@ -10,6 +10,9 @@ import threading import time +import re + + import halo.cursor as cursor from log_symbols.symbols import LogSymbols @@ -77,14 +80,18 @@ def __init__( stream : io, optional Output. """ - + # To reset Values in deleter self.reset_values = {"text": text, - "color": color, - "text_color": text_color, - "spinner": spinner, - "animation": animation, - "placement": placement,} + "color": color, + "text_color": text_color, + "spinner": spinner, + "animation": animation, + "placement": placement, } + + self._symbol = " " + self._stop_persist = False + self._color = color self._animation = animation @@ -129,8 +136,11 @@ def __enter__(self): return self.start() def __exit__(self, type, value, traceback): - """Stops the spinner. For use in context managers.""" - self.stop() + """Stops the spinner with show text at the end or not. For use in context managers.""" + if self._stop_persist: + self.stop_and_persist(symbol=self._symbol, text=self.text) + else: + self.stop() def __call__(self, f): """Allow the Halo object to be used as a regular function decorator.""" @@ -138,10 +148,48 @@ def __call__(self, f): @functools.wraps(f) def wrapped(*args, **kwargs): with self: + self._change_text(f, *args, **kwargs) return f(*args, **kwargs) return wrapped + def _change_text(self, f, *args, **kwargs): + """if you want to change text in decorator as your function is in a loop + * you have to use halo_iter as the argument of function + * if you want to show finished text use stop_persist:bool, stop_text:str and stop_symbol:str + + Args: + f (callable): the function which supposed to be in a loop + """ + if "halo_iter" in kwargs: + if type(kwargs['halo_iter']) in [list, tuple, dict]: + main_text = self.text + curl_brackets = re.findall( + r'\{([a-zA-Z_]*)\}', main_text) + results = [] + for text in kwargs['halo_iter']: + for k, curl_value in enumerate(curl_brackets): + if len(curl_brackets) > 1: + text = list(text)[k] + self.text = main_text.format( + **{curl_value: text}) + results.append(f(*args, **kwargs)) + + if 'stop_text' in kwargs: + self._stop_persist = True + self.text = kwargs['stop_text'] + + if 'stop_symbol' in kwargs: + self._stop_persist = True + self._symbol = kwargs['stop_symbol'] + else: + self._symbol = ' ' + + return results + + else: + self._stop_persist = False + @property def spinner(self): """Getter for spinner property. @@ -193,7 +241,7 @@ def text(self, text): @text.deleter def text(self): - self.text = self.reset_values["text"] + self._text = self.reset_values["text"] @property def text_color(self): @@ -398,15 +446,16 @@ def _get_text(self, text): Make the text bounce back and forth """ for x in range(0, text_length - terminal_width + 1): - frames.append(stripped_text[x : terminal_width + x]) + frames.append(stripped_text[x: terminal_width + x]) frames.extend(list(reversed(frames))) elif "marquee": """ Make the text scroll like a marquee """ - stripped_text = stripped_text + " " + stripped_text[:terminal_width] + stripped_text = stripped_text + " " + \ + stripped_text[:terminal_width] for x in range(0, text_length + 1): - frames.append(stripped_text[x : terminal_width + x]) + frames.append(stripped_text[x: terminal_width + x]) elif terminal_width < text_length and not animation: # Add ellipsis if text is larger than terminal width and no animation was specified frames = [stripped_text[: terminal_width - 6] + " (...)"] From 5d5f412c9ec45f55288d3fe75397ff1b3a26a972 Mon Sep 17 00:00:00 2001 From: Farzin Date: Sun, 9 May 2021 07:55:51 +0430 Subject: [PATCH 3/4] fix typing fault --- halo/halo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/halo/halo.py b/halo/halo.py index 823279f..ad769a8 100644 --- a/halo/halo.py +++ b/halo/halo.py @@ -417,7 +417,7 @@ def _get_spinner(self, spinner): return spinner if is_supported(): - return Spinners[spinner].valueif if all([is_text_type(spinner), spinner in Spinners.__members__]) else default_spinner + return Spinners[spinner].value if all([is_text_type(spinner), spinner in Spinners.__members__]) else default_spinner else: return Spinners["line"].value From 9c5e282a6f3c842f5e67b0494cc48808596ba832 Mon Sep 17 00:00:00 2001 From: Farzin Date: Sun, 9 May 2021 15:57:25 +0430 Subject: [PATCH 4/4] fix some bugs --- .travis.yml | 20 ++++++++++---------- examples/dict_text_loop.py | 11 +++++++++-- halo/halo.py | 25 +++++++++++++------------ tests/test_halo.py | 30 ++++++++++++++++++++---------- 4 files changed, 52 insertions(+), 34 deletions(-) diff --git a/.travis.yml b/.travis.yml index ff4699a..b4ba857 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,16 +6,16 @@ git: depth: 5 matrix: include: - - python: '3.5' - env: TOXENV=lint - - python: '3.5' - env: TOXENV=py35 - - python: '3.6' - env: TOXENV=py36 - - python: '3.7' - env: TOXENV=py37 - - python: '3.8' - env: TOXENV=py38 + - python: "3.5" + env: TOXENV=lint + - python: "3.5" + env: TOXENV=py35 + - python: "3.6" + env: TOXENV=py36 + - python: "3.7" + env: TOXENV=py37 + - python: "3.8" + env: TOXENV=py38 fast_finish: true install: - pip install tox coveralls diff --git a/examples/dict_text_loop.py b/examples/dict_text_loop.py index f7462eb..459cf57 100644 --- a/examples/dict_text_loop.py +++ b/examples/dict_text_loop.py @@ -4,12 +4,19 @@ @Halo(text='Loading {task}', spinner='line') def run_task(halo_iter=[], stop_text='', stop_symbol=' '): - sleep(.5) + sleep(1) +@Halo(text='Loading {task} at {task2}', spinner='line') +def run_task2(halo_iter=[], stop_text='', stop_symbol=' '): + sleep(1) tasks1 = ['breakfest', 'launch', 'dinner'] tasks2 = ['morning', 'noon', 'night'] +#with symbol run_task(halo_iter=tasks1, stop_symbol='🦄'.encode( 'utf-8'), stop_text='Task1 Finished') -run_task(halo_iter=tasks2, stop_text='Finished Time') + +#without symbol +run_task2(halo_iter=list(zip(tasks1, tasks2)), stop_text='Finished Time') +run_task2.spinner.symbol = '' \ No newline at end of file diff --git a/halo/halo.py b/halo/halo.py index ad769a8..19f0b3a 100644 --- a/halo/halo.py +++ b/halo/halo.py @@ -92,7 +92,6 @@ def __init__( self._symbol = " " self._stop_persist = False - self._color = color self._animation = animation @@ -163,18 +162,20 @@ def _change_text(self, f, *args, **kwargs): """ if "halo_iter" in kwargs: if type(kwargs['halo_iter']) in [list, tuple, dict]: - main_text = self.text + main_text = self.text # text have curl-brackets like + # 'This is task {number}' curl_brackets = re.findall( - r'\{([a-zA-Z_]*)\}', main_text) - results = [] + r'\{([^\s\{\}]+)\}', main_text) + results = [] # store all return f(*args, **kwargs) in loop for text in kwargs['halo_iter']: - for k, curl_value in enumerate(curl_brackets): - if len(curl_brackets) > 1: - text = list(text)[k] - self.text = main_text.format( - **{curl_value: text}) - results.append(f(*args, **kwargs)) - + #* type(text) is str in single curl-bracket + #* or list[str] in multiple curl-brackets + text_dict = dict(list(zip(curl_brackets, text))) if len( + curl_brackets) > 1 else dict([(curl_brackets[0], text)]) + + self.text = main_text.format(**text_dict) + results.append(f(*args, **kwargs)) + if 'stop_text' in kwargs: self._stop_persist = True self.text = kwargs['stop_text'] @@ -186,7 +187,7 @@ def _change_text(self, f, *args, **kwargs): self._symbol = ' ' return results - + else: self._stop_persist = False diff --git a/tests/test_halo.py b/tests/test_halo.py index 29918aa..aa49883 100644 --- a/tests/test_halo.py +++ b/tests/test_halo.py @@ -27,10 +27,12 @@ get_coded_text = decode_utf_8_text if is_supported(): - frames = [get_coded_text(frame) for frame in Spinners['dots'].value['frames']] + frames = [get_coded_text(frame) + for frame in Spinners['dots'].value['frames']] default_spinner = Spinners['dots'].value else: - frames = [get_coded_text(frame) for frame in Spinners['line'].value['frames']] + frames = [get_coded_text(frame) + for frame in Spinners['line'].value['frames']] default_spinner = Spinners['line'].value @@ -65,7 +67,8 @@ def _get_test_output(self, no_ansi=True): output_colors = [] for line in data: - clean_line = strip_ansi(line.strip('\n')) if no_ansi else line.strip('\n') + clean_line = strip_ansi(line.strip( + '\n')) if no_ansi else line.strip('\n') if clean_line != '': output_text.append(get_coded_text(clean_line)) @@ -175,9 +178,12 @@ def test_text_ellipsing(self): terminal_width = get_terminal_columns() # -6 of the ' (...)' ellipsis, -2 of the spinner and space - self.assertEqual(output[0], '{} {} (...)'.format(frames[0], text[:terminal_width - 6 - 2])) - self.assertEqual(output[1], '{} {} (...)'.format(frames[1], text[:terminal_width - 6 - 2])) - self.assertEqual(output[2], '{} {} (...)'.format(frames[2], text[:terminal_width - 6 - 2])) + self.assertEqual(output[0], '{} {} (...)'.format( + frames[0], text[:terminal_width - 6 - 2])) + self.assertEqual(output[1], '{} {} (...)'.format( + frames[1], text[:terminal_width - 6 - 2])) + self.assertEqual(output[2], '{} {} (...)'.format( + frames[2], text[:terminal_width - 6 - 2])) pattern = re.compile(r'(✔|v) End!', re.UNICODE) @@ -189,7 +195,8 @@ def test_text_animation(self): text = 'This is a text that it is too long. In fact, it exceeds the eighty column standard ' \ 'terminal width, which forces the text frame renderer to add an ellipse at the end of the ' \ 'text. ' * 6 - spinner = Halo(text=text, spinner='dots', stream=self._stream, animation='marquee') + spinner = Halo(text=text, spinner='dots', + stream=self._stream, animation='marquee') spinner.start() time.sleep(1) @@ -198,9 +205,12 @@ def test_text_animation(self): terminal_width = get_terminal_columns() - self.assertEqual(output[0], '{} {}'.format(frames[0], text[:terminal_width - 2])) - self.assertEqual(output[1], '{} {}'.format(frames[1], text[1:terminal_width - 1])) - self.assertEqual(output[2], '{} {}'.format(frames[2], text[2:terminal_width])) + self.assertEqual(output[0], '{} {}'.format( + frames[0], text[:terminal_width - 2])) + self.assertEqual(output[1], '{} {}'.format( + frames[1], text[1:terminal_width - 1])) + self.assertEqual(output[2], '{} {}'.format( + frames[2], text[2:terminal_width])) pattern = re.compile(r'(✔|v) End!', re.UNICODE)