Skip to content

Commit

Permalink
Merge pull request #111 from rsalmei/jupyter-support
Browse files Browse the repository at this point in the history
Include Jupyter and Disable support
  • Loading branch information
rsalmei authored Oct 19, 2021
2 parents be82b98 + d7b4532 commit 1e1aa78
Show file tree
Hide file tree
Showing 24 changed files with 336 additions and 185 deletions.
1 change: 1 addition & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ branch = True
omit =
alive_progress/styles/*
alive_progress/tools/*
alive_progress/utils/terminal/*

[report]
ignore_errors = True
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Changelog

## 2.1.0 - Oct 18, 2021
- Jupyter notebook support (experimental), Jupyter auto-detection, disable feature and configuration
- four internal terminal abstractions, to support TTY, JUPYTER, NON_TTY and VOID


## 2.0.0 - Aug 25, 2021
This is a major breakthrough in `alive-progress`!
- now there's complete support for Emojis 🤩 and exotic Unicode chars in general, which required MAJOR refactoring deep within the project, giving rise to what I called **Cells Architecture** => now all internal components use and generate streams of cells instead of chars, and correctly interprets grapheme clusters — it has enabled to render complex multi-chars symbols as if they were one, thus making them work on any spinners, bars, texts, borders, backgrounds, everything!!! there's even support for wide chars, which are represented with any number of chars, including one, but take two spaces on screen!! pretty advanced stuff 🤓
Expand Down
47 changes: 35 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,19 @@ I like to think of it as a new kind of progress bar for Python, since it has amo
- it is **customizable**, with a growing smorgasbord of different bar and spinner styles, as well as several factories to easily generate yours! Now (📌 new in 2.0) we even have super powerful and cool `.check()` tools in both bars and spinners, to help you design your animations! You can see all the frames and cycles exploded on screen, with several verbosity levels, even including an **alive** rendition! 😜


## 📌 NEW 2.1 series!

YES! Now `alive-progres` has support for Jupyter Notebooks, and also includes a Disabled state! Both were highly sought after, and have finally landed!
<br>And better, I've implemented an auto-detection mechanism for jupyter notebooks, so it just works, out of the box, without any changes in your code!!

See for yourself:

![alive-progress demo](img/alive-jupyter.gif)

It seems to work very well, but at this moment it should be considered **Experimental**.
<br>There were instances in which some visual glitches did appear to me, but it's something I think I can't possibly workaround. It seems Jupyter sometimes refresh the screen at odd times, which makes the screen loses some updates...


## 📌 NEW 2.0 series!

This is a major breakthrough in `alive-progress`!
Expand Down Expand Up @@ -130,7 +143,8 @@ So, in short: retrieve the items as usual, enter the `alive_bar` context manager
While inside an `alive_bar` context, you can effortlessly display messages with:
- the usual Python `print()` statement, where `alive_bar` nicely cleans up the line, prints your message alongside the current bar position at the time, and continues the bar right below it;
- the standard Python `logging` framework, including file outputs, are also enriched exactly like the previous one;
- the cool `bar.text('message')`, which sets a situational message right within the bar, where you can display something about the current item, or the phase the processing is in!
- the cool `bar.text('message')`, which sets a situational message right within the bar, where you can display something about the current item, or the phase the processing is in;
- and all of this works just the same in an actual terminal or in a Jupyter notebook!

![alive-progress printing messages](img/print-hook.gif)

Expand Down Expand Up @@ -297,7 +311,8 @@ These are the options - default values in brackets:
<br> ↳ accepts a predefined spinner name, or a custom spinner factory (cannot be None)
- `theme`: [`'smooth'`] a set of matching spinner, bar and unknown
<br> ↳ accepts a predefined theme name
- `force_tty`: [`None`] forces animations to be on, off, or according to the tty (more details [here](#advanced))
- `force_tty`: [`None`] forces animations to be on, off, or according to the tty (more details [here](#forcing-animations-on-non-interactive-consoles))
- `disable`: [`False`] if True, completely disables all output, do not install hooks
- `manual`: [`False`] set to manually control the bar position
- `enrich_print`: [`True`] enriches print() and logging messages with the bar position
- `receipt_text`: [`False`] set to repeat the last text message in the final receipt
Expand All @@ -309,7 +324,7 @@ These are the options - default values in brackets:
- `spinner_length`: [`0`] forces the spinner length, or `0` for its natural one

And there's also one that can only be set locally in an `alive_bar` context:
- `calibrate`: maximum theoretical throughput to calibrate animation speed (more details [here](#advanced))
- `calibrate`: maximum theoretical throughput to calibrate animation speed (more details [here](#fps-calibration))

To set them locally, just send them as keyword arguments to `alive_bar`:

Expand Down Expand Up @@ -432,10 +447,10 @@ If you've appreciated my work and would like me to continue improving it, please

## Advanced

### Static loop-less use
### Loop-less use

So, you need to monitor a fixed operation, without any loops?
<br>It'll work for sure! Here is an example (although a naive approach, we'll do better):
<br>It'll work for sure! Here is a naive example (we'll do better in a moment):

```python
with alive_bar(4) as bar:
Expand All @@ -449,9 +464,9 @@ with alive_bar(4) as bar:
bar() # we're done! four bar calls with `total=4`
```

It's naive because it considers all steps are equal, but actually each one may take a very different time to complete. Think a `read_file` and a `tokenize` steps being extremely fast, making the percentage skyrocket to 50%, then stopping for a long time in the `process` step. You get the point, it can ruin the user experience and create a very misleading ETA.
It's naive because it assumes all steps take the same amount of time, but actually each one may take very different times to complete. Think `read_file` and `tokenize` may be extremely fast, which makes the percentage skyrocket to 50%, then stopping for a long time in the `process` step... You get the point, it can ruin the user experience and create a very misleading ETA.

What you need to do is distribute the steps accordingly! Since you told `alive_bar` there were four steps, when the first one completed it understood 1/4 or 25% of the whole processing was complete, which as we've seen may not be the case. Thus, you need to measure how long your steps do take, and use the **manual mode** to increase the bar percentage by different amounts at each step!
To improve upon that you need to distribute the steps' percentages accordingly! Since you told `alive_bar` there were four steps, when the first one completed it understood 1/4 or 25% of the whole processing was complete. Thus, you need to measure how long your steps actually take, and use the **manual mode** to increase the bar percentage by different amounts at each step!

You can use my other open source project [about-time](https://github.com/rsalmei/about-time) to easily measure these durations! Just try to simulate with some representative inputs, to get better results. Something like:

Expand Down Expand Up @@ -560,9 +575,11 @@ In [21]: next(gen, None)

Those astonishing animations refuse to display?

There are ttys that do not report themselves as "interactive", which are valid for example in shell pipelines "|" or headless consoles. But there are some that do that for no good reason, like Pycharm's python console for instance. And if a console is not interactive, `alive_bar` disables all animations and refreshes, only printing the final receipt. This is made to avoid spamming a log file or messing up a pipe output with hundreds of refreshes.
There are terminals that occasionally do not report themselves as "interactive", for example in shell pipeline commands "|" or background processes. And there are some that never reports themselves as interactive, like Pycharm's python console and Jupyter Notebooks for instance.

So if you are in an interactive environment, like the aforementioned Pycharm console, you can see `alive_bar` in all its glory! Just use the `force_tty` argument!
When a console is not interactive, `alive-progress` disables all kinds of animations, only printing the final receipt. This is made to avoid spamming a log file or messing up a pipe output with thousands of progress bar updates.

So, when you know it's safe, you can in fact see `alive-progress` in all its glory! Just use the `force_tty` argument!

```python
with alive_bar(1000, force_tty=True) as bar:
Expand All @@ -571,11 +588,16 @@ with alive_bar(1000, force_tty=True) as bar:
bar()
```

You can also set it system-wide using the `config_handler`, then you won't need to pass it manually anymore.
The values accepted are:
- `force_tty=True` -> enables animations, and auto-detects Jupyter Notebooks!
- `force_tty=False` -> disables animations, keeping only the final receipt
- `force_tty=None` (default) -> auto select, according to the terminal's tty state

You can also set it system-wide using the `config_handler`, then you won't need to pass that manually in all `alive_bar` calls.

Do note that Pycharm's console is heavily instrumented and thus has more overhead, so the outcome may not be as fluid as one would expect. To see `alive_bar` animations perfectly, always prefer a full-fledged terminal.
Do note that Pycharm's console and Jupyter notebooks are heavily instrumented and thus have more overhead, so the outcome may not be as fluid as you would expect; and on top of that Jupyter notebooks do not support ANSI Escape Codes, so I had to develop some workarounds, to emulate functions like "clear the line" and "clear from cursor".

> (📌 new) Now `force_tty` also supports `False`, which will disable animations even on interactive displays.
> To see `alive_bar` animations as I intended, always prefer a full-fledged terminal.

## Interesting facts
Expand Down Expand Up @@ -631,6 +653,7 @@ The `alive_progress` framework starting from version 2.0 does not support Python

## Changelog highlights (complete [here](CHANGELOG.md)):
- 2.1.0: Jupyter notebook support (experimental), Jupyter auto-detection, disable feature and configuration
- 2.0.0: new system-wide Cell Architecture with grapheme clusters support; super cool spinner compiler and runner; `.check()` tools in both spinners and bars; bars and spinners engines revamp; new animation modes in alongside and sequential spinners; new builtin spinners, bars and themes; dynamic showtime with themes, scroll protection and filter patterns; improved logging for files; several new configuration options for customizing appearance; new iterator adapter `alive_it`; uses `time.perf_counter()` high resolution clock; requires python 3.6+ (and officially supports python 3.9 and 3.10)
- 1.6.2: new `bar.current()` method; newlines get printed on vanilla Python REPL; bar is truncated to 80 chars on Windows.
- 1.6.1: fix logging support for python 3.6 and lower; support logging for file; support for wide unicode chars, which use 2 columns but have length 1
Expand Down
2 changes: 1 addition & 1 deletion alive_progress/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from .core.configuration import config_handler
from .core.progress import alive_bar, alive_it

VERSION = (2, 0, 0)
VERSION = (2, 1, 0)

__author__ = 'Rogério Sampaio de Almeida'
__email__ = '[email protected]'
Expand Down
10 changes: 5 additions & 5 deletions alive_progress/animations/bars.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from ..utils.cells import VS_15, combine_cells, fix_cells, has_wide, is_wide, join_cells, \
mark_graphemes, split_graphemes, strip_marks, to_cells
from ..utils.colors import BLUE, BLUE_BOLD, CYAN, DIM, GREEN, ORANGE, ORANGE_BOLD, RED, YELLOW_BOLD
from ..utils.terminal import clear_end, cursor_up_1, hide_cursor, show_cursor
from ..utils.terminal import FULL


def bar_factory(chars=None, *, tip=None, background=None, borders=None, errors=None):
Expand Down Expand Up @@ -222,16 +222,16 @@ def animate(bar): # pragma: no cover
print(f'\n{SECTION("Animation")}')
from ..styles.exhibit import exhibit_bar
bar_gen = exhibit_bar(bar, 15)
hide_cursor()
FULL.hide_cursor()
try:
while True:
rendition, percent = next(bar_gen)
print(f'\r{join_cells(rendition)}', CYAN(percent, "6.1%"))
print(DIM('(press CTRL+C to stop)'), end='')
clear_end()
FULL.clear_end()
time.sleep(1 / 15)
cursor_up_1()
FULL.cursor_up_1()
except KeyboardInterrupt:
pass
finally:
show_cursor()
FULL.show_cursor()
10 changes: 5 additions & 5 deletions alive_progress/animations/spinner_compiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from .utils import fix_signature
from ..utils.cells import fix_cells, is_wide, join_cells, strip_marks, to_cells
from ..utils.colors import BLUE, BLUE_BOLD, CYAN, DIM, GREEN, ORANGE, ORANGE_BOLD, RED, YELLOW_BOLD
from ..utils.terminal import clear_end, cursor_up_1, hide_cursor, show_cursor
from ..utils.terminal import FULL


def spinner_controller(*, natural, skip_compiler=False):
Expand Down Expand Up @@ -335,18 +335,18 @@ def animate(spec): # pragma: no cover
cf, lf, tf = (f'>{len(str(x))}' for x in (spec.cycles, max(spec.frames), spec.total_frames))
from itertools import cycle
cycles, frames = cycle(range(1, spec.cycles + 1)), cycle(range(1, spec.total_frames + 1))
hide_cursor()
FULL.hide_cursor()
try:
while True:
c = next(cycles)
for i, f in enumerate(spec.runner(), 1):
n = next(frames)
print(f'\r{CYAN(c, cf)}:{CYAN(i, lf)} -->{join_cells(f)}<-- {CYAN(n, tf)} ')
print(DIM('(press CTRL+C to stop)'), end='')
clear_end()
FULL.clear_end()
time.sleep(1 / 15)
cursor_up_1()
FULL.cursor_up_1()
except KeyboardInterrupt:
pass
finally:
show_cursor()
FULL.show_cursor()
24 changes: 17 additions & 7 deletions alive_progress/core/configuration.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import os
import sys
from collections import namedtuple
from types import FunctionType

from ..utils.terminal import NON_TTY, FULL

ERROR = object() # represents a config value not accepted.


Expand Down Expand Up @@ -63,10 +66,15 @@ def _input(x):
return _input


def _tristate_input_factory():
def _force_tty_input_factory():
def _input(x):
return None if x is None else bool(x)
return table.get(x, ERROR)

table = {
None: FULL if sys.stdout.isatty() else NON_TTY,
False: NON_TTY,
True: FULL,
}
return _input


Expand All @@ -77,8 +85,9 @@ def _input(x):
return _input


Config = namedtuple('Config', 'title length spinner bar unknown force_tty manual enrich_print '
' receipt_text monitor stats elapsed title_length spinner_length')
Config = namedtuple('Config', 'title length spinner bar unknown force_tty disable manual '
'enrich_print receipt_text monitor stats elapsed title_length '
'spinner_length')


def create_config():
Expand All @@ -89,6 +98,7 @@ def reset():
length=40,
theme='smooth', # includes spinner, bar and unknown.
force_tty=None,
disable=False,
manual=False,
enrich_print=True,
receipt_text=False,
Expand All @@ -113,8 +123,7 @@ def create_context(theme=None, **options):
"""Create an immutable copy of the current configuration, with optional customization."""
lazy_init()
local_config = {**global_config, **_parse(theme, options)}
# noinspection PyArgumentList
return Config(**{k: local_config[k] for k in Config._fields})
return Config(**local_config)

def _parse(theme, options):
"""Validate and convert some configuration options."""
Expand Down Expand Up @@ -149,7 +158,8 @@ def lazy_init():
spinner=_spinner_input_factory(None), # accept empty.
bar=_bar_input_factory(),
unknown=_spinner_input_factory(ERROR), # do not accept empty.
force_tty=_tristate_input_factory(),
force_tty=_force_tty_input_factory(),
disable=_bool_input_factory(),
manual=_bool_input_factory(),
enrich_print=_bool_input_factory(),
receipt_text=_bool_input_factory(),
Expand Down
42 changes: 32 additions & 10 deletions alive_progress/core/hook_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,16 @@
from logging import StreamHandler
from types import SimpleNamespace

from ..utils.terminal import clear_line


def buffered_hook_manager(header_template, get_pos, cond_refresh):
def buffered_hook_manager(header_template, get_pos, cond_refresh, term):
"""Create and maintain a buffered hook manager, used for instrumenting print
statements and logging.
Args:
header_template (): the template for enriching output
get_pos (Callable[..., Any]): the container to retrieve the current position
cond_refresh: Condition object to force a refresh when printing
term: the current terminal
Returns:
a closure with several functions
Expand All @@ -43,11 +42,12 @@ def write(stream, part):
header = get_header()
with cond_refresh:
nested = ''.join(line or ' ' * len(header) for line in buffer)
if stream in base:
# this avoids potential flickering, since now the stream can also be
# files from logging, and thus not needing to clear the screen...
clear_line()
stream.write(f'{header}{nested.strip()}\n')
text = f'{header}{nested.strip()}\n'
if stream in base: # pragma: no cover
# use the current terminal abstraction for preparing the screen.
term.clear_line()
# handle all streams, both screen and logging.
stream.write(text)
stream.flush()
cond_refresh.notify()
buffer[:] = []
Expand All @@ -57,7 +57,7 @@ def get_hook_for(handler):
handler.stream.flush()
return SimpleNamespace(write=partial(write, handler.stream),
flush=partial(flush, handler.stream),
isatty=sys.__stdout__.isatty)
isatty=sys.stdout.isatty)

def install():
root = logging.root
Expand All @@ -77,7 +77,7 @@ def uninstall():

# internal data.
buffers = defaultdict(list)
get_header = (lambda: header_template.format(get_pos())) if header_template else lambda: ''
get_header = gen_header(header_template, get_pos) if header_template else null_header
base = sys.stdout, sys.stderr # needed for tests.
before_handlers = {}

Expand All @@ -91,6 +91,28 @@ def uninstall():
return hook_manager


def passthrough_hook_manager(): # pragma: no cover
passthrough_hook_manager.flush_buffers = __noop
passthrough_hook_manager.install = __noop
passthrough_hook_manager.uninstall = __noop
return passthrough_hook_manager


def __noop(): # pragma: no cover
pass


def gen_header(header_template, get_pos): # pragma: no cover
def inner():
return header_template.format(get_pos())

return inner


def null_header(): # pragma: no cover
return ''


if sys.version_info >= (3, 7): # pragma: no cover
def _set_stream(handler, stream):
return handler.setStream(stream)
Expand Down
Loading

0 comments on commit 1e1aa78

Please sign in to comment.