Module mfutil.cli
Utility functions to build CLI (Python >= 3.6 only).
Expand source code
# -*- coding: utf-8 -*-
"""Utility functions to build CLI (Python >= 3.6 only)."""
from __future__ import print_function
import six
import sys
try:
from rich.progress import Progress, BarColumn, ProgressColumn, \
TimeRemainingColumn
from rich.text import Text
from rich.bar import Bar
from rich.table import Table
from rich.console import Console
except ImportError:
class Dummy():
pass
TimeRemainingColumn = Dummy
BarColumn = Dummy
ProgressColumn = Dummy
Progress = Dummy
Bar = Dummy
from mfutil.exc import MFUtilException
__pdoc__ = {
"MFTimeRemainingColumn": False,
"StateColumn": False
}
_STDOUT_CONSOLE = None
_STDERR_CONSOLE = None
def _get_console(**kwargs):
global _STDOUT_CONSOLE, _STDERR_CONSOLE
if len(kwargs) > 1 or \
(len(kwargs) == 1 and
(kwargs.get('file', None) not in (sys.stdout, sys.stderr))):
return Console(**kwargs)
file = kwargs.get('file', None)
if file == sys.stdout:
if _STDOUT_CONSOLE is None:
_STDOUT_CONSOLE = Console(**kwargs)
return _STDOUT_CONSOLE
elif file == sys.stderr:
if _STDERR_CONSOLE is None:
_STDERR_CONSOLE = Console(**kwargs)
return _STDERR_CONSOLE
return Console(**kwargs)
def _is_interactive(f):
c = _get_console(file=f)
return c.is_terminal
def is_interactive(target=None):
"""Return True if we are in an interactive terminal.
Args:
target (string): can be None (for stdout AND stderr checking),
"stdout" (for stdout checking only or "stderr" (for stderr checking
only).
Returns:
boolean (True (interactive) or False (non-interactive).
Raises:
MFUtilException: if target is invalid
"""
if target is None:
return _is_interactive(sys.stdout) and _is_interactive(sys.stderr)
elif target == "stdout":
return _is_interactive(sys.stdout)
elif target == "stderr":
return _is_interactive(sys.stderr)
else:
raise MFUtilException("invalid target parameter: %s" % target)
def echo_ok(message=""):
"""Write [OK] with colors if supported a little optional message.
Args:
message (string): little optional message.
"""
if is_interactive("stdout"):
echo_clean()
print("\033[60G[ \033[32mOK\033[0;0m ] %s%s" % (
message, " " * (13 - len(message)))
)
else:
print(" [ OK ] %s" % message)
def echo_nok(message=""):
"""Write [ERROR] with colors if supported a little optional message.
Args:
message (string): little optional message.
"""
if is_interactive("stdout"):
echo_clean()
print("\033[60G[ \033[31mERROR\033[0;0m ] %s%s" % (
message, " " * (10 - len(message)))
)
else:
print(" [ ERROR ] %s" % message)
def echo_warning(message=""):
"""Write [WARNING] with colors if supported a little optional message.
Args:
message (string): little optional message.
"""
if is_interactive("stdout"):
echo_clean()
print("\033[60G[ \033[33mWARNING\033[0;0m ] %s" % message)
else:
print("[ WARNING ] %s" % message)
def echo_bold(message):
"""Write a message in bold (if supported).
Args:
message (string): message to write in bold.
"""
if is_interactive("stdout"):
print("\033[1m%s\033[0m" % message)
else:
print(message)
def echo_running(message=None):
"""Write [RUNNING] with colors if supported.
You can pass an optional message which will be rendered before [RUNNING]
on the same line.
Args:
message (string): little optional message.
"""
if message is not None:
if is_interactive("stdout"):
if six.PY2:
print(message, end="")
sys.stdout.flush()
else:
print(message, end="", flush=True)
else:
print(message, end="")
if is_interactive("stdout"):
echo_clean()
print("\033[60G[ \033[33mRUNNING\033[0;0m ]", end="")
def echo_clean():
"""Clean waiting status."""
if is_interactive("stdout"):
print("\033[60G[ \033 ", end="")
class StateColumn(ProgressColumn):
def render(self, task):
if task.finished:
extra = task.fields.get('status_extra', '')
if len(extra) > 0:
extra = " (%s)" % extra
if task.fields.get('status', 'OK') == 'ERROR':
return Text.assemble(Text("[ "),
Text("ERROR", style="red"),
Text(" ]%s" % extra))
elif task.fields.get('status', 'OK') == 'WARNING':
return Text.assemble(Text("[ "),
Text("WARNING", style="yellow"),
Text(" ]%s" % extra))
else:
return Text.assemble(Text("[ "),
Text("OK", style="green"),
Text(" ]%s" % extra))
else:
return Text.assemble(Text("[ "),
Text("RUNNING", style="yellow"),
Text(" ]"))
class MFTimeRemainingColumn(TimeRemainingColumn):
def render(self, task):
if task.finished:
return Text("")
else:
return TimeRemainingColumn.render(self, task)
class MFProgress(Progress):
"""[Rich Progress](https://rich.readthedocs.io/en/latest/progress.html) child class.
This class add three features to the original one:
- support (basic) rendering in non-terminal
- task status management
- different default columns setup (but you can override this)
- rendering on stdout by default (but you can change this by providing
a custom Console object)
You can use it exactly like the original [Progress class](https://rich.readthedocs.io/en/latest/reference/progress.html):
```python
import time
from mfutil.cli import MFProgress
with MFProgress() as progress:
t1 = p.add_task("Foo task")
t2 = p.add_task("Foo task")
while not progress.finished:
progress.update(t1, advance=10)
progress.update(t2, advance=10)
time.sleep(1)
```
For status management:
- if you leave MFProgress context manager, not finished tasks are
automatically set to `ERROR` state, finished tasks are automatically
set to `OK` state
- you have 3 new methods to manually override this behaviour:
(complete_task(), complete_task_nok(), complete_task_warning())
Example:
```python
import time
from mfutil.cli import MFProgress
with MFProgress() as progress:
t1 = p.add_task("Foo task")
t2 = p.add_task("Foo task")
i = 0
while not progress.finished:
progress.update(t1, advance=10)
if i < 5:
progress.update(t2, advance=10)
elif i == 5:
progress.complete_task_failed(t2, "unknown error")
time.sleep(1)
i = i + 1
```
"""
def __init__(self, *args, **kwargs):
console = kwargs.get("console", None)
if not console:
self._interactive = _is_interactive(sys.stdout)
kwargs["console"] = Console()
else:
self._interactive = _is_interactive(console.file)
if len(args) == 0:
columns = ["[progress.description]{task.description}",
BarColumn(12),
StateColumn(),
MFTimeRemainingColumn()]
self.mfprogress_columns = True
else:
columns = args
self.mfprogress_columns = False
Progress.__init__(self, *columns, **kwargs)
def make_tasks_table(self, tasks):
if self.mfprogress_columns:
return self._mfprogress_make_tasks_table(tasks)
else:
return MFProgress.make_tasks_table(self, tasks)
def _mfprogress_make_tasks_table(self, tasks):
"""Get a table to render the Progress display."""
table = Table.grid()
table.pad_edge = True
table.padding = (0, 1, 0, 0)
try:
finished = self.finished
except Exception:
finished = False
if finished:
table.add_column(width=57, no_wrap=True)
table.add_column(width=0)
table.add_column()
table.add_column()
else:
table.add_column(width=45, no_wrap=True)
table.add_column()
table.add_column()
table.add_column()
for task in tasks:
if task.visible:
row = []
append = row.append
for index, column in enumerate(self.columns):
if isinstance(column, str):
txt = column.format(task=task)
if finished:
if len(txt) > 79:
txt = txt[0:74] + "[[...]]"
else:
if len(txt) > 67:
txt = txt[0:62] + "[[...]]"
append(txt)
table.columns[index].no_wrap = True
else:
widget = column(task)
append(widget)
if isinstance(widget, (str, Text)):
table.columns[index].no_wrap = True
table.add_row(*row)
return table
def __exit__(self, *args, **kwargs):
with self._lock:
for tid, task in self._tasks.items():
if not task.finished:
self.complete_task_nok(tid)
Progress.__exit__(self, *args, **kwargs)
def start(self, *args, **kwargs):
if self._interactive:
return Progress.start(self, *args, **kwargs)
def __complete_task_nok_nolock(self, task_id, status_extra=""):
task = self._tasks[task_id]
self.update(task_id, completed=task.total, status="ERROR",
refresh=False, status_extra=status_extra)
def complete_task_nok(self, task_id, status_extra=""):
"""Complete a task with ERROR status.
Args:
task_id (TaskID): a task ID.
status_extra (str): a little extra message to add.
"""
with self._lock:
self.__complete_task_nok_nolock(task_id, status_extra=status_extra)
def complete_task_warning(self, task_id, status_extra=""):
"""Complete a task with WARNING status.
Args:
task_id (TaskID): a task ID.
status_extra (str): a little extra message to add.
"""
with self._lock:
task = self._tasks[task_id]
self.update(task_id, completed=task.total, status="WARNING",
refresh=False, status_extra=status_extra)
def complete_task(self, task_id, status_extra=""):
"""Complete a task with OK status.
Args:
task_id (TaskID): a task ID.
status_extra (str): a little extra message to add.
"""
with self._lock:
task = self._tasks[task_id]
self.update(task_id, completed=task.total, status="OK",
refresh=False, status_extra=status_extra)
def stop(self):
if self._interactive:
return Progress.stop(self)
else:
with self._lock:
for tid, task in self._tasks.items():
status = ""
extra = task.fields.get('status_extra', '')
if len(extra) > 0:
extra = " (%s)" % extra
if task.finished:
if task.fields.get('status', 'OK') == 'OK':
status = "OK"
elif task.fields.get('status', 'OK') == 'WARNING':
status = "WARNING"
else:
status = "ERROR"
print("%s [ %s ]%s" % (task.description, status, extra),
file=self.console.file)
def refresh(self, *args, **kwargs):
if self._interactive:
return Progress.refresh(self, *args, **kwargs)
Functions
def echo_bold(message)
-
Write a message in bold (if supported).
Args
message
:string
- message to write in bold.
Expand source code
def echo_bold(message): """Write a message in bold (if supported). Args: message (string): message to write in bold. """ if is_interactive("stdout"): print("\033[1m%s\033[0m" % message) else: print(message)
def echo_clean()
-
Clean waiting status.
Expand source code
def echo_clean(): """Clean waiting status.""" if is_interactive("stdout"): print("\033[60G[ \033 ", end="")
def echo_nok(message='')
-
Write [ERROR] with colors if supported a little optional message.
Args
message
:string
- little optional message.
Expand source code
def echo_nok(message=""): """Write [ERROR] with colors if supported a little optional message. Args: message (string): little optional message. """ if is_interactive("stdout"): echo_clean() print("\033[60G[ \033[31mERROR\033[0;0m ] %s%s" % ( message, " " * (10 - len(message))) ) else: print(" [ ERROR ] %s" % message)
def echo_ok(message='')
-
Write [OK] with colors if supported a little optional message.
Args
message
:string
- little optional message.
Expand source code
def echo_ok(message=""): """Write [OK] with colors if supported a little optional message. Args: message (string): little optional message. """ if is_interactive("stdout"): echo_clean() print("\033[60G[ \033[32mOK\033[0;0m ] %s%s" % ( message, " " * (13 - len(message))) ) else: print(" [ OK ] %s" % message)
def echo_running(message=None)
-
Write [RUNNING] with colors if supported.
You can pass an optional message which will be rendered before [RUNNING] on the same line.
Args
message
:string
- little optional message.
Expand source code
def echo_running(message=None): """Write [RUNNING] with colors if supported. You can pass an optional message which will be rendered before [RUNNING] on the same line. Args: message (string): little optional message. """ if message is not None: if is_interactive("stdout"): if six.PY2: print(message, end="") sys.stdout.flush() else: print(message, end="", flush=True) else: print(message, end="") if is_interactive("stdout"): echo_clean() print("\033[60G[ \033[33mRUNNING\033[0;0m ]", end="")
def echo_warning(message='')
-
Write [WARNING] with colors if supported a little optional message.
Args
message
:string
- little optional message.
Expand source code
def echo_warning(message=""): """Write [WARNING] with colors if supported a little optional message. Args: message (string): little optional message. """ if is_interactive("stdout"): echo_clean() print("\033[60G[ \033[33mWARNING\033[0;0m ] %s" % message) else: print("[ WARNING ] %s" % message)
def is_interactive(target=None)
-
Return True if we are in an interactive terminal.
Args
target
:string
- can be None (for stdout AND stderr checking), "stdout" (for stdout checking only or "stderr" (for stderr checking only).
Returns
boolean (True (interactive) or False (non-interactive).
Raises
MFUtilException
- if target is invalid
Expand source code
def is_interactive(target=None): """Return True if we are in an interactive terminal. Args: target (string): can be None (for stdout AND stderr checking), "stdout" (for stdout checking only or "stderr" (for stderr checking only). Returns: boolean (True (interactive) or False (non-interactive). Raises: MFUtilException: if target is invalid """ if target is None: return _is_interactive(sys.stdout) and _is_interactive(sys.stderr) elif target == "stdout": return _is_interactive(sys.stdout) elif target == "stderr": return _is_interactive(sys.stderr) else: raise MFUtilException("invalid target parameter: %s" % target)
Classes
class MFProgress (*args, **kwargs)
-
Rich Progress child class.
This class add three features to the original one:
- support (basic) rendering in non-terminal
- task status management
- different default columns setup (but you can override this)
- rendering on stdout by default (but you can change this by providing a custom Console object)
You can use it exactly like the original Progress class:
import time from mfutil.cli import MFProgress with MFProgress() as progress: t1 = p.add_task("Foo task") t2 = p.add_task("Foo task") while not progress.finished: progress.update(t1, advance=10) progress.update(t2, advance=10) time.sleep(1)
For status management:
- if you leave MFProgress context manager, not finished tasks are
automatically set to
ERROR
state, finished tasks are automatically set toOK
state - you have 3 new methods to manually override this behaviour: (complete_task(), complete_task_nok(), complete_task_warning())
Example:
import time from mfutil.cli import MFProgress with MFProgress() as progress: t1 = p.add_task("Foo task") t2 = p.add_task("Foo task") i = 0 while not progress.finished: progress.update(t1, advance=10) if i < 5: progress.update(t2, advance=10) elif i == 5: progress.complete_task_failed(t2, "unknown error") time.sleep(1) i = i + 1
Expand source code
class MFProgress(Progress): """[Rich Progress](https://rich.readthedocs.io/en/latest/progress.html) child class. This class add three features to the original one: - support (basic) rendering in non-terminal - task status management - different default columns setup (but you can override this) - rendering on stdout by default (but you can change this by providing a custom Console object) You can use it exactly like the original [Progress class](https://rich.readthedocs.io/en/latest/reference/progress.html): ```python import time from mfutil.cli import MFProgress with MFProgress() as progress: t1 = p.add_task("Foo task") t2 = p.add_task("Foo task") while not progress.finished: progress.update(t1, advance=10) progress.update(t2, advance=10) time.sleep(1) ``` For status management: - if you leave MFProgress context manager, not finished tasks are automatically set to `ERROR` state, finished tasks are automatically set to `OK` state - you have 3 new methods to manually override this behaviour: (complete_task(), complete_task_nok(), complete_task_warning()) Example: ```python import time from mfutil.cli import MFProgress with MFProgress() as progress: t1 = p.add_task("Foo task") t2 = p.add_task("Foo task") i = 0 while not progress.finished: progress.update(t1, advance=10) if i < 5: progress.update(t2, advance=10) elif i == 5: progress.complete_task_failed(t2, "unknown error") time.sleep(1) i = i + 1 ``` """ def __init__(self, *args, **kwargs): console = kwargs.get("console", None) if not console: self._interactive = _is_interactive(sys.stdout) kwargs["console"] = Console() else: self._interactive = _is_interactive(console.file) if len(args) == 0: columns = ["[progress.description]{task.description}", BarColumn(12), StateColumn(), MFTimeRemainingColumn()] self.mfprogress_columns = True else: columns = args self.mfprogress_columns = False Progress.__init__(self, *columns, **kwargs) def make_tasks_table(self, tasks): if self.mfprogress_columns: return self._mfprogress_make_tasks_table(tasks) else: return MFProgress.make_tasks_table(self, tasks) def _mfprogress_make_tasks_table(self, tasks): """Get a table to render the Progress display.""" table = Table.grid() table.pad_edge = True table.padding = (0, 1, 0, 0) try: finished = self.finished except Exception: finished = False if finished: table.add_column(width=57, no_wrap=True) table.add_column(width=0) table.add_column() table.add_column() else: table.add_column(width=45, no_wrap=True) table.add_column() table.add_column() table.add_column() for task in tasks: if task.visible: row = [] append = row.append for index, column in enumerate(self.columns): if isinstance(column, str): txt = column.format(task=task) if finished: if len(txt) > 79: txt = txt[0:74] + "[[...]]" else: if len(txt) > 67: txt = txt[0:62] + "[[...]]" append(txt) table.columns[index].no_wrap = True else: widget = column(task) append(widget) if isinstance(widget, (str, Text)): table.columns[index].no_wrap = True table.add_row(*row) return table def __exit__(self, *args, **kwargs): with self._lock: for tid, task in self._tasks.items(): if not task.finished: self.complete_task_nok(tid) Progress.__exit__(self, *args, **kwargs) def start(self, *args, **kwargs): if self._interactive: return Progress.start(self, *args, **kwargs) def __complete_task_nok_nolock(self, task_id, status_extra=""): task = self._tasks[task_id] self.update(task_id, completed=task.total, status="ERROR", refresh=False, status_extra=status_extra) def complete_task_nok(self, task_id, status_extra=""): """Complete a task with ERROR status. Args: task_id (TaskID): a task ID. status_extra (str): a little extra message to add. """ with self._lock: self.__complete_task_nok_nolock(task_id, status_extra=status_extra) def complete_task_warning(self, task_id, status_extra=""): """Complete a task with WARNING status. Args: task_id (TaskID): a task ID. status_extra (str): a little extra message to add. """ with self._lock: task = self._tasks[task_id] self.update(task_id, completed=task.total, status="WARNING", refresh=False, status_extra=status_extra) def complete_task(self, task_id, status_extra=""): """Complete a task with OK status. Args: task_id (TaskID): a task ID. status_extra (str): a little extra message to add. """ with self._lock: task = self._tasks[task_id] self.update(task_id, completed=task.total, status="OK", refresh=False, status_extra=status_extra) def stop(self): if self._interactive: return Progress.stop(self) else: with self._lock: for tid, task in self._tasks.items(): status = "" extra = task.fields.get('status_extra', '') if len(extra) > 0: extra = " (%s)" % extra if task.finished: if task.fields.get('status', 'OK') == 'OK': status = "OK" elif task.fields.get('status', 'OK') == 'WARNING': status = "WARNING" else: status = "ERROR" print("%s [ %s ]%s" % (task.description, status, extra), file=self.console.file) def refresh(self, *args, **kwargs): if self._interactive: return Progress.refresh(self, *args, **kwargs)
Ancestors
- rich.progress.Progress
- rich.jupyter.JupyterMixin
Methods
def complete_task(self, task_id, status_extra='')
-
Complete a task with OK status.
Args
task_id
:TaskID
- a task ID.
status_extra
:str
- a little extra message to add.
Expand source code
def complete_task(self, task_id, status_extra=""): """Complete a task with OK status. Args: task_id (TaskID): a task ID. status_extra (str): a little extra message to add. """ with self._lock: task = self._tasks[task_id] self.update(task_id, completed=task.total, status="OK", refresh=False, status_extra=status_extra)
def complete_task_nok(self, task_id, status_extra='')
-
Complete a task with ERROR status.
Args
task_id
:TaskID
- a task ID.
status_extra
:str
- a little extra message to add.
Expand source code
def complete_task_nok(self, task_id, status_extra=""): """Complete a task with ERROR status. Args: task_id (TaskID): a task ID. status_extra (str): a little extra message to add. """ with self._lock: self.__complete_task_nok_nolock(task_id, status_extra=status_extra)
def complete_task_warning(self, task_id, status_extra='')
-
Complete a task with WARNING status.
Args
task_id
:TaskID
- a task ID.
status_extra
:str
- a little extra message to add.
Expand source code
def complete_task_warning(self, task_id, status_extra=""): """Complete a task with WARNING status. Args: task_id (TaskID): a task ID. status_extra (str): a little extra message to add. """ with self._lock: task = self._tasks[task_id] self.update(task_id, completed=task.total, status="WARNING", refresh=False, status_extra=status_extra)
def make_tasks_table(self, tasks)
-
Get a table to render the Progress display.
Args
tasks
:Iterable[Task]
- An iterable of Task instances, one per row of the table.
Returns
Table
- A table instance.
Expand source code
def make_tasks_table(self, tasks): if self.mfprogress_columns: return self._mfprogress_make_tasks_table(tasks) else: return MFProgress.make_tasks_table(self, tasks)
def refresh(self, *args, **kwargs)
-
Refresh (render) the progress information.
Expand source code
def refresh(self, *args, **kwargs): if self._interactive: return Progress.refresh(self, *args, **kwargs)
def start(self, *args, **kwargs)
-
Start the progress display.
Expand source code
def start(self, *args, **kwargs): if self._interactive: return Progress.start(self, *args, **kwargs)
def stop(self)
-
Stop the progress display.
Expand source code
def stop(self): if self._interactive: return Progress.stop(self) else: with self._lock: for tid, task in self._tasks.items(): status = "" extra = task.fields.get('status_extra', '') if len(extra) > 0: extra = " (%s)" % extra if task.finished: if task.fields.get('status', 'OK') == 'OK': status = "OK" elif task.fields.get('status', 'OK') == 'WARNING': status = "WARNING" else: status = "ERROR" print("%s [ %s ]%s" % (task.description, status, extra), file=self.console.file)