jpayne@68
|
1 """
|
jpayne@68
|
2 IPython/Jupyter Notebook progressbar decorator for iterators.
|
jpayne@68
|
3 Includes a default `range` iterator printing to `stderr`.
|
jpayne@68
|
4
|
jpayne@68
|
5 Usage:
|
jpayne@68
|
6 >>> from tqdm.notebook import trange, tqdm
|
jpayne@68
|
7 >>> for i in trange(10):
|
jpayne@68
|
8 ... ...
|
jpayne@68
|
9 """
|
jpayne@68
|
10 # import compatibility functions and utilities
|
jpayne@68
|
11 import re
|
jpayne@68
|
12 import sys
|
jpayne@68
|
13 from html import escape
|
jpayne@68
|
14 from weakref import proxy
|
jpayne@68
|
15
|
jpayne@68
|
16 # to inherit from the tqdm class
|
jpayne@68
|
17 from .std import tqdm as std_tqdm
|
jpayne@68
|
18
|
jpayne@68
|
19 if True: # pragma: no cover
|
jpayne@68
|
20 # import IPython/Jupyter base widget and display utilities
|
jpayne@68
|
21 IPY = 0
|
jpayne@68
|
22 try: # IPython 4.x
|
jpayne@68
|
23 import ipywidgets
|
jpayne@68
|
24 IPY = 4
|
jpayne@68
|
25 except ImportError: # IPython 3.x / 2.x
|
jpayne@68
|
26 IPY = 32
|
jpayne@68
|
27 import warnings
|
jpayne@68
|
28 with warnings.catch_warnings():
|
jpayne@68
|
29 warnings.filterwarnings(
|
jpayne@68
|
30 'ignore', message=".*The `IPython.html` package has been deprecated.*")
|
jpayne@68
|
31 try:
|
jpayne@68
|
32 import IPython.html.widgets as ipywidgets # NOQA: F401
|
jpayne@68
|
33 except ImportError:
|
jpayne@68
|
34 pass
|
jpayne@68
|
35
|
jpayne@68
|
36 try: # IPython 4.x / 3.x
|
jpayne@68
|
37 if IPY == 32:
|
jpayne@68
|
38 from IPython.html.widgets import HTML
|
jpayne@68
|
39 from IPython.html.widgets import FloatProgress as IProgress
|
jpayne@68
|
40 from IPython.html.widgets import HBox
|
jpayne@68
|
41 IPY = 3
|
jpayne@68
|
42 else:
|
jpayne@68
|
43 from ipywidgets import HTML
|
jpayne@68
|
44 from ipywidgets import FloatProgress as IProgress
|
jpayne@68
|
45 from ipywidgets import HBox
|
jpayne@68
|
46 except ImportError:
|
jpayne@68
|
47 try: # IPython 2.x
|
jpayne@68
|
48 from IPython.html.widgets import HTML
|
jpayne@68
|
49 from IPython.html.widgets import ContainerWidget as HBox
|
jpayne@68
|
50 from IPython.html.widgets import FloatProgressWidget as IProgress
|
jpayne@68
|
51 IPY = 2
|
jpayne@68
|
52 except ImportError:
|
jpayne@68
|
53 IPY = 0
|
jpayne@68
|
54 IProgress = None
|
jpayne@68
|
55 HBox = object
|
jpayne@68
|
56
|
jpayne@68
|
57 try:
|
jpayne@68
|
58 from IPython.display import display # , clear_output
|
jpayne@68
|
59 except ImportError:
|
jpayne@68
|
60 pass
|
jpayne@68
|
61
|
jpayne@68
|
62 __author__ = {"github.com/": ["lrq3000", "casperdcl", "alexanderkuk"]}
|
jpayne@68
|
63 __all__ = ['tqdm_notebook', 'tnrange', 'tqdm', 'trange']
|
jpayne@68
|
64 WARN_NOIPYW = ("IProgress not found. Please update jupyter and ipywidgets."
|
jpayne@68
|
65 " See https://ipywidgets.readthedocs.io/en/stable"
|
jpayne@68
|
66 "/user_install.html")
|
jpayne@68
|
67
|
jpayne@68
|
68
|
jpayne@68
|
69 class TqdmHBox(HBox):
|
jpayne@68
|
70 """`ipywidgets.HBox` with a pretty representation"""
|
jpayne@68
|
71 def _json_(self, pretty=None):
|
jpayne@68
|
72 pbar = getattr(self, 'pbar', None)
|
jpayne@68
|
73 if pbar is None:
|
jpayne@68
|
74 return {}
|
jpayne@68
|
75 d = pbar.format_dict
|
jpayne@68
|
76 if pretty is not None:
|
jpayne@68
|
77 d["ascii"] = not pretty
|
jpayne@68
|
78 return d
|
jpayne@68
|
79
|
jpayne@68
|
80 def __repr__(self, pretty=False):
|
jpayne@68
|
81 pbar = getattr(self, 'pbar', None)
|
jpayne@68
|
82 if pbar is None:
|
jpayne@68
|
83 return super().__repr__()
|
jpayne@68
|
84 return pbar.format_meter(**self._json_(pretty))
|
jpayne@68
|
85
|
jpayne@68
|
86 def _repr_pretty_(self, pp, *_, **__):
|
jpayne@68
|
87 pp.text(self.__repr__(True))
|
jpayne@68
|
88
|
jpayne@68
|
89
|
jpayne@68
|
90 class tqdm_notebook(std_tqdm):
|
jpayne@68
|
91 """
|
jpayne@68
|
92 Experimental IPython/Jupyter Notebook widget using tqdm!
|
jpayne@68
|
93 """
|
jpayne@68
|
94 @staticmethod
|
jpayne@68
|
95 def status_printer(_, total=None, desc=None, ncols=None):
|
jpayne@68
|
96 """
|
jpayne@68
|
97 Manage the printing of an IPython/Jupyter Notebook progress bar widget.
|
jpayne@68
|
98 """
|
jpayne@68
|
99 # Fallback to text bar if there's no total
|
jpayne@68
|
100 # DEPRECATED: replaced with an 'info' style bar
|
jpayne@68
|
101 # if not total:
|
jpayne@68
|
102 # return super(tqdm_notebook, tqdm_notebook).status_printer(file)
|
jpayne@68
|
103
|
jpayne@68
|
104 # fp = file
|
jpayne@68
|
105
|
jpayne@68
|
106 # Prepare IPython progress bar
|
jpayne@68
|
107 if IProgress is None: # #187 #451 #558 #872
|
jpayne@68
|
108 raise ImportError(WARN_NOIPYW)
|
jpayne@68
|
109 if total:
|
jpayne@68
|
110 pbar = IProgress(min=0, max=total)
|
jpayne@68
|
111 else: # No total? Show info style bar with no progress tqdm status
|
jpayne@68
|
112 pbar = IProgress(min=0, max=1)
|
jpayne@68
|
113 pbar.value = 1
|
jpayne@68
|
114 pbar.bar_style = 'info'
|
jpayne@68
|
115 if ncols is None:
|
jpayne@68
|
116 pbar.layout.width = "20px"
|
jpayne@68
|
117
|
jpayne@68
|
118 ltext = HTML()
|
jpayne@68
|
119 rtext = HTML()
|
jpayne@68
|
120 if desc:
|
jpayne@68
|
121 ltext.value = desc
|
jpayne@68
|
122 container = TqdmHBox(children=[ltext, pbar, rtext])
|
jpayne@68
|
123 # Prepare layout
|
jpayne@68
|
124 if ncols is not None: # use default style of ipywidgets
|
jpayne@68
|
125 # ncols could be 100, "100px", "100%"
|
jpayne@68
|
126 ncols = str(ncols) # ipywidgets only accepts string
|
jpayne@68
|
127 try:
|
jpayne@68
|
128 if int(ncols) > 0: # isnumeric and positive
|
jpayne@68
|
129 ncols += 'px'
|
jpayne@68
|
130 except ValueError:
|
jpayne@68
|
131 pass
|
jpayne@68
|
132 pbar.layout.flex = '2'
|
jpayne@68
|
133 container.layout.width = ncols
|
jpayne@68
|
134 container.layout.display = 'inline-flex'
|
jpayne@68
|
135 container.layout.flex_flow = 'row wrap'
|
jpayne@68
|
136
|
jpayne@68
|
137 return container
|
jpayne@68
|
138
|
jpayne@68
|
139 def display(self, msg=None, pos=None,
|
jpayne@68
|
140 # additional signals
|
jpayne@68
|
141 close=False, bar_style=None, check_delay=True):
|
jpayne@68
|
142 # Note: contrary to native tqdm, msg='' does NOT clear bar
|
jpayne@68
|
143 # goal is to keep all infos if error happens so user knows
|
jpayne@68
|
144 # at which iteration the loop failed.
|
jpayne@68
|
145
|
jpayne@68
|
146 # Clear previous output (really necessary?)
|
jpayne@68
|
147 # clear_output(wait=1)
|
jpayne@68
|
148
|
jpayne@68
|
149 if not msg and not close:
|
jpayne@68
|
150 d = self.format_dict
|
jpayne@68
|
151 # remove {bar}
|
jpayne@68
|
152 d['bar_format'] = (d['bar_format'] or "{l_bar}<bar/>{r_bar}").replace(
|
jpayne@68
|
153 "{bar}", "<bar/>")
|
jpayne@68
|
154 msg = self.format_meter(**d)
|
jpayne@68
|
155
|
jpayne@68
|
156 ltext, pbar, rtext = self.container.children
|
jpayne@68
|
157 pbar.value = self.n
|
jpayne@68
|
158
|
jpayne@68
|
159 if msg:
|
jpayne@68
|
160 msg = msg.replace(' ', u'\u2007') # fix html space padding
|
jpayne@68
|
161 # html escape special characters (like '&')
|
jpayne@68
|
162 if '<bar/>' in msg:
|
jpayne@68
|
163 left, right = map(escape, re.split(r'\|?<bar/>\|?', msg, maxsplit=1))
|
jpayne@68
|
164 else:
|
jpayne@68
|
165 left, right = '', escape(msg)
|
jpayne@68
|
166
|
jpayne@68
|
167 # Update description
|
jpayne@68
|
168 ltext.value = left
|
jpayne@68
|
169 # never clear the bar (signal: msg='')
|
jpayne@68
|
170 if right:
|
jpayne@68
|
171 rtext.value = right
|
jpayne@68
|
172
|
jpayne@68
|
173 # Change bar style
|
jpayne@68
|
174 if bar_style:
|
jpayne@68
|
175 # Hack-ish way to avoid the danger bar_style being overridden by
|
jpayne@68
|
176 # success because the bar gets closed after the error...
|
jpayne@68
|
177 if pbar.bar_style != 'danger' or bar_style != 'success':
|
jpayne@68
|
178 pbar.bar_style = bar_style
|
jpayne@68
|
179
|
jpayne@68
|
180 # Special signal to close the bar
|
jpayne@68
|
181 if close and pbar.bar_style != 'danger': # hide only if no error
|
jpayne@68
|
182 try:
|
jpayne@68
|
183 self.container.close()
|
jpayne@68
|
184 except AttributeError:
|
jpayne@68
|
185 self.container.visible = False
|
jpayne@68
|
186 self.container.layout.visibility = 'hidden' # IPYW>=8
|
jpayne@68
|
187
|
jpayne@68
|
188 if check_delay and self.delay > 0 and not self.displayed:
|
jpayne@68
|
189 display(self.container)
|
jpayne@68
|
190 self.displayed = True
|
jpayne@68
|
191
|
jpayne@68
|
192 @property
|
jpayne@68
|
193 def colour(self):
|
jpayne@68
|
194 if hasattr(self, 'container'):
|
jpayne@68
|
195 return self.container.children[-2].style.bar_color
|
jpayne@68
|
196
|
jpayne@68
|
197 @colour.setter
|
jpayne@68
|
198 def colour(self, bar_color):
|
jpayne@68
|
199 if hasattr(self, 'container'):
|
jpayne@68
|
200 self.container.children[-2].style.bar_color = bar_color
|
jpayne@68
|
201
|
jpayne@68
|
202 def __init__(self, *args, **kwargs):
|
jpayne@68
|
203 """
|
jpayne@68
|
204 Supports the usual `tqdm.tqdm` parameters as well as those listed below.
|
jpayne@68
|
205
|
jpayne@68
|
206 Parameters
|
jpayne@68
|
207 ----------
|
jpayne@68
|
208 display : Whether to call `display(self.container)` immediately
|
jpayne@68
|
209 [default: True].
|
jpayne@68
|
210 """
|
jpayne@68
|
211 kwargs = kwargs.copy()
|
jpayne@68
|
212 # Setup default output
|
jpayne@68
|
213 file_kwarg = kwargs.get('file', sys.stderr)
|
jpayne@68
|
214 if file_kwarg is sys.stderr or file_kwarg is None:
|
jpayne@68
|
215 kwargs['file'] = sys.stdout # avoid the red block in IPython
|
jpayne@68
|
216
|
jpayne@68
|
217 # Initialize parent class + avoid printing by using gui=True
|
jpayne@68
|
218 kwargs['gui'] = True
|
jpayne@68
|
219 # convert disable = None to False
|
jpayne@68
|
220 kwargs['disable'] = bool(kwargs.get('disable', False))
|
jpayne@68
|
221 colour = kwargs.pop('colour', None)
|
jpayne@68
|
222 display_here = kwargs.pop('display', True)
|
jpayne@68
|
223 super().__init__(*args, **kwargs)
|
jpayne@68
|
224 if self.disable or not kwargs['gui']:
|
jpayne@68
|
225 self.disp = lambda *_, **__: None
|
jpayne@68
|
226 return
|
jpayne@68
|
227
|
jpayne@68
|
228 # Get bar width
|
jpayne@68
|
229 self.ncols = '100%' if self.dynamic_ncols else kwargs.get("ncols", None)
|
jpayne@68
|
230
|
jpayne@68
|
231 # Replace with IPython progress bar display (with correct total)
|
jpayne@68
|
232 unit_scale = 1 if self.unit_scale is True else self.unit_scale or 1
|
jpayne@68
|
233 total = self.total * unit_scale if self.total else self.total
|
jpayne@68
|
234 self.container = self.status_printer(self.fp, total, self.desc, self.ncols)
|
jpayne@68
|
235 self.container.pbar = proxy(self)
|
jpayne@68
|
236 self.displayed = False
|
jpayne@68
|
237 if display_here and self.delay <= 0:
|
jpayne@68
|
238 display(self.container)
|
jpayne@68
|
239 self.displayed = True
|
jpayne@68
|
240 self.disp = self.display
|
jpayne@68
|
241 self.colour = colour
|
jpayne@68
|
242
|
jpayne@68
|
243 # Print initial bar state
|
jpayne@68
|
244 if not self.disable:
|
jpayne@68
|
245 self.display(check_delay=False)
|
jpayne@68
|
246
|
jpayne@68
|
247 def __iter__(self):
|
jpayne@68
|
248 try:
|
jpayne@68
|
249 it = super().__iter__()
|
jpayne@68
|
250 for obj in it:
|
jpayne@68
|
251 # return super(tqdm...) will not catch exception
|
jpayne@68
|
252 yield obj
|
jpayne@68
|
253 # NB: except ... [ as ...] breaks IPython async KeyboardInterrupt
|
jpayne@68
|
254 except: # NOQA
|
jpayne@68
|
255 self.disp(bar_style='danger')
|
jpayne@68
|
256 raise
|
jpayne@68
|
257 # NB: don't `finally: close()`
|
jpayne@68
|
258 # since this could be a shared bar which the user will `reset()`
|
jpayne@68
|
259
|
jpayne@68
|
260 def update(self, n=1):
|
jpayne@68
|
261 try:
|
jpayne@68
|
262 return super().update(n=n)
|
jpayne@68
|
263 # NB: except ... [ as ...] breaks IPython async KeyboardInterrupt
|
jpayne@68
|
264 except: # NOQA
|
jpayne@68
|
265 # cannot catch KeyboardInterrupt when using manual tqdm
|
jpayne@68
|
266 # as the interrupt will most likely happen on another statement
|
jpayne@68
|
267 self.disp(bar_style='danger')
|
jpayne@68
|
268 raise
|
jpayne@68
|
269 # NB: don't `finally: close()`
|
jpayne@68
|
270 # since this could be a shared bar which the user will `reset()`
|
jpayne@68
|
271
|
jpayne@68
|
272 def close(self):
|
jpayne@68
|
273 if self.disable:
|
jpayne@68
|
274 return
|
jpayne@68
|
275 super().close()
|
jpayne@68
|
276 # Try to detect if there was an error or KeyboardInterrupt
|
jpayne@68
|
277 # in manual mode: if n < total, things probably got wrong
|
jpayne@68
|
278 if self.total and self.n < self.total:
|
jpayne@68
|
279 self.disp(bar_style='danger', check_delay=False)
|
jpayne@68
|
280 else:
|
jpayne@68
|
281 if self.leave:
|
jpayne@68
|
282 self.disp(bar_style='success', check_delay=False)
|
jpayne@68
|
283 else:
|
jpayne@68
|
284 self.disp(close=True, check_delay=False)
|
jpayne@68
|
285
|
jpayne@68
|
286 def clear(self, *_, **__):
|
jpayne@68
|
287 pass
|
jpayne@68
|
288
|
jpayne@68
|
289 def reset(self, total=None):
|
jpayne@68
|
290 """
|
jpayne@68
|
291 Resets to 0 iterations for repeated use.
|
jpayne@68
|
292
|
jpayne@68
|
293 Consider combining with `leave=True`.
|
jpayne@68
|
294
|
jpayne@68
|
295 Parameters
|
jpayne@68
|
296 ----------
|
jpayne@68
|
297 total : int or float, optional. Total to use for the new bar.
|
jpayne@68
|
298 """
|
jpayne@68
|
299 if self.disable:
|
jpayne@68
|
300 return super().reset(total=total)
|
jpayne@68
|
301 _, pbar, _ = self.container.children
|
jpayne@68
|
302 pbar.bar_style = ''
|
jpayne@68
|
303 if total is not None:
|
jpayne@68
|
304 pbar.max = total
|
jpayne@68
|
305 if not self.total and self.ncols is None: # no longer unknown total
|
jpayne@68
|
306 pbar.layout.width = None # reset width
|
jpayne@68
|
307 return super().reset(total=total)
|
jpayne@68
|
308
|
jpayne@68
|
309
|
jpayne@68
|
310 def tnrange(*args, **kwargs):
|
jpayne@68
|
311 """Shortcut for `tqdm.notebook.tqdm(range(*args), **kwargs)`."""
|
jpayne@68
|
312 return tqdm_notebook(range(*args), **kwargs)
|
jpayne@68
|
313
|
jpayne@68
|
314
|
jpayne@68
|
315 # Aliases
|
jpayne@68
|
316 tqdm = tqdm_notebook
|
jpayne@68
|
317 trange = tnrange
|