cdxcore 0.1.6__py3-none-any.whl → 0.1.9__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of cdxcore might be problematic. Click here for more details.

cdxcore/verbose.py CHANGED
@@ -1,34 +1,369 @@
1
- """
2
- verbose
3
- Utility for verbose printing with indentation
4
- Hans Buehler 2022
1
+ r"""
2
+ Overview
3
+ --------
4
+
5
+ This module contains the :class:`cdxcore.verbose.Context` manager class
6
+ which supports printing hierarchical verbose progress reports.
7
+ The key point of this class is to implement an easy-to-use method to print indented progress which
8
+ can also be turned off easily without
9
+ untidy code constructs such as excessive ``if`` blocks. In this case, we also avoid formatting
10
+ any strings.
11
+
12
+ Here is an example::
13
+
14
+ from cdxcore.verbose import Context
15
+
16
+ def f_sub( num=3, context = Context.quiet ):
17
+ context.write("Entering loop")
18
+ for i in range(num):
19
+ context.report(1, "Number %ld", i)
20
+
21
+ def f_main( context = Context.quiet ):
22
+ context.write( "First step" )
23
+ # ... do something
24
+ context.report( 1, "Intermediate step 1" )
25
+ context.report( 1, "Intermediate step 2\\n with newlines" )
26
+ # ... do something
27
+ f_sub( context=context(2) ) # call function f_sub with a sub-context
28
+ # ... do something
29
+ context.write( "Final step" )
30
+
31
+ print("Verbose=1")
32
+ context = Context(1)
33
+ f_main(context)
34
+
35
+ print("\\nVerbose=2")
36
+ context = Context(2)
37
+ f_main(context)
38
+
39
+ print("\\nVerbose='all'")
40
+ context = Context('all')
41
+ f_main(context)
42
+
43
+ print("\\nVerbose='quiet'")
44
+ context = Context('quiet')
45
+ f_main(context)
46
+
47
+ print("\\ndone")
48
+
49
+ Returns::
50
+
51
+ Verbose=1
52
+ 00: First step
53
+ 01: Intermediate step 1
54
+ 01: Intermediate step 2
55
+ 01: with newlines
56
+ 00: Final step
57
+
58
+ Verbose=2
59
+ 00: First step
60
+ 01: Intermediate step 1
61
+ 01: Intermediate step 2
62
+ 01: with newlines
63
+ 02: Entering loop
64
+ 00: Final step
65
+
66
+ Verbose='all'
67
+ 00: First step
68
+ 01: Intermediate step 1
69
+ 01: Intermediate step 2
70
+ 01: with newlines
71
+ 02: Entering loop
72
+ 03: Number 0
73
+ 03: Number 1
74
+ 03: Number 2
75
+ 00: Final step
76
+
77
+ Verbose='quiet'
78
+
79
+ done
80
+
81
+ Workflow
82
+ ^^^^^^^^
83
+
84
+ The basic idea is that the root context has level 0, with increasing levels for sub-contexts.
85
+ When printing information, we can limit printing up to a given level and
86
+ automatically indent the output to reflect the current level of detail.
87
+
88
+ Workflow:
89
+
90
+ * Create a :class:`cdxcore.verbose.Context` model, and define its verbosity in its constructor, e.g.
91
+ by specifying ``"all"``, ``"quiet"``, or a number.
92
+ * To write a text at current level to ``stdout`` use :meth:`cdxcore.verbose.Context.write`.
93
+ * To write a text at an indented sub-level use :meth:`cdxcore.verbose.Context.report`.
94
+ * To create a sub-context (indentation), use :meth:`cdxcore.verbose.Context.__call__`.
95
+
96
+ Lazy Formatting
97
+ ^^^^^^^^^^^^^^^
98
+
99
+ :class:`cdxcore.verbose.Context` message formattting is meant to be lazy and only executed
100
+ if a message is actually written. This means that if the ``Contex`` is ``"quiet"`` no string
101
+ formatting takes place.
102
+
103
+ Consider a naive example::
104
+
105
+ from cdxcore.verbose import Context
106
+ import numpy as np
107
+
108
+ def f( data : np.ndarray, verbose : Context = Context.quiet ):
109
+ verbose.write(f"'f' called; data has mean {np.mean(data)} and variance {np.var(data)}")
110
+ # ...
111
+ f( verbose = Context.quiet )
112
+
113
+ In this case ``f`` will compute ``np.mean(data)`` and ``np.var(data)`` even though the use of the ``quiet``
114
+ ``Context`` means that the formatted
115
+ string will not be printed.
116
+
117
+ To alleviate this, :meth:`cdxcore.verbose.Context.write` supports a number of alternatives
118
+ which are leveraging :func:`cdxcore.err.fmt`. In above example, the most efficient use case
119
+ is the use of a ``lambda`` function::
120
+
121
+ def f( data : np.ndarray, verbose : Context ):
122
+ verbose.write(lambda : f"'f' called; data has mean {np.mean(data)} and variance {np.var(data)}")
123
+
124
+ The ``lambda`` function is only called when the message is about to be printed.
125
+
126
+ Providing Updates
127
+ ^^^^^^^^^^^^^^^^^
128
+
129
+ In many applications we wish to provide progress updates in a single line, and not clutter the output.
130
+ In the example from the beginng, the long lists of output are not informative.
131
+
132
+ :class:`cdxcore.verbose.Context` supports the use of "\\r" and "\\n for simple output formatting.
133
+ Under the hood it uses :class:`cdxcore.crman.CRMan`.
134
+
135
+ Consider the following change to ``f_sub`` in above code example:
136
+
137
+ .. code-block:: python
138
+ :emphasize-lines: 4,5
139
+
140
+ def f_sub( num=3, context = Context.quiet ):
141
+ context.write("Entering loop")
142
+ for i in range(num):
143
+ context.report(1, ":emphasis:`\\r`Number %ld", i, end='') # Notice use of \\r and end=''
144
+ context.write("\\rLoop done") # Notice use of \\r `
145
+
146
+ context = Context('all')
147
+ f_main(context)
148
+
149
+ During execution this prints, for example at step ``i==1``::
150
+
151
+ 00: First step
152
+ 01: Intermediate step 1
153
+ 01: Intermediate step 2
154
+ 01: with newlines
155
+ 02: Entering loop
156
+ 03: Number 1
157
+
158
+ But once the loop finished the update per ``i`` is overwitten::
159
+
160
+ 00: First step
161
+ 01: Intermediate step 1
162
+ 01: with newlines
163
+ 02: Entering loop
164
+ 02: Loop done
165
+ 00: Final step
166
+
167
+ Composing Line Output and Timing
168
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
169
+
170
+ For lengthy operations it is often considerate to provide the user with an update on
171
+ how long an operation takes. :class:`cdxcore.verbose.Context` provides some simple tooling::
172
+
173
+ from cdxcore.verbose import Context
174
+ import time as time
175
+
176
+ def takes_long( n : int, verbose : context = Context.quiet ):
177
+ with verbose.write_t("About to start... ", end='') as tme:
178
+ for t in range(n):
179
+ verbose.write(lambda : f"\\rTakes long {int(100.*(t+1)/n)}%... ", end='')
180
+ time.sleep(0.22)
181
+ verbose.write(lambda : f"done; this took {tme}.", head=False)
182
+
183
+ takes_long(5, Context.all)
184
+
185
+ During execution prints
186
+
187
+ .. code-block:: python
188
+
189
+ 00: Takes long 80%...
190
+
191
+ The example finishes with
192
+
193
+ .. code-block:: python
194
+
195
+ 00: Takes long 100%... done; this took 1.1s.
196
+
197
+ Import
198
+ ------
199
+ .. code-block:: python
200
+
201
+ from cdxcore.verbose import Context
202
+
203
+ Documentation
204
+ -------------
5
205
  """
6
206
 
7
207
  from .util import fmt, Timer
8
- from .util import _verify
208
+ from .err import verify
9
209
  from .crman import CRMan, Callable
10
210
 
11
211
  class Context(object):
12
- """
13
- Class for printing indented messages, filtered by overall level of verbosity.
212
+ r"""
213
+ Class for printing indented messages, filtered by overall level of visibility.
14
214
 
15
- context = Context( verbose = 4 )
215
+ * Construction with keywords::
216
+
217
+ Context( "all" )` or
218
+ Context( "quiet" )
16
219
 
17
- def f_2( context ):
220
+ * Display everything::
221
+
222
+ Context( None )
18
223
 
19
- context.report( 1, "Running 'f_2'")
224
+ * Display only up to level 2 (top level is 0) e.g.::
20
225
 
21
- def f_1( context ):
226
+ Context( 2 )
227
+
228
+ * Copy constructor::
229
+
230
+ Context( context )
231
+
232
+ **Example:**
22
233
 
23
- context.report( 1, "Running 'f_1'")
24
- f_2( context.sub(1, "Entering 'f_2'") )
234
+ .. code-block:: python
235
+
236
+ from cdxcore.verbose import Context
237
+
238
+ def f_2( verbose : Context = Context.quiet ):
239
+ verbose.write( "Running 'f_2'")
240
+ for i in range(5):
241
+ verbose.report(1, "Sub-task {i}", i=i)
242
+ # do something
243
+
244
+ def f_1( verbose : Context = Context.quiet ):
245
+ verbose.write( "Running 'f_1'")
246
+ f_2( verbose(1) )
247
+ # do something
248
+
249
+ verbose = Context("all")
250
+ verbose.write("Starting:")
251
+ f_1(verbose(1))
252
+ verbose.write("Done.")
25
253
 
254
+ prints
255
+
256
+ .. code-block:: python
257
+
258
+ 00: Starting:
259
+ 01: Running 'f_1'
260
+ 02: Running 'f_2'
261
+ 03: Sub-task 0
262
+ 03: Sub-task 1
263
+ 03: Sub-task 2
264
+ 03: Sub-task 3
265
+ 03: Sub-task 4
266
+ 00: Done.
267
+
268
+ If we set visibility to 2
269
+
270
+ .. code-block:: python
271
+
272
+ verbose = Context(2)
273
+ verbose.write("Starting:")
274
+ f_1(verbose(1)) # <-- make it a level higher
275
+ verbose.write("Done.")
276
+
277
+ we get the reduced
278
+
279
+ .. code-block:: python
280
+
281
+ 00: Starting:
282
+ 01: Running 'f_1'
283
+ 02: Running 'f_2'
284
+ 00: Done.
285
+
286
+ **Lazy Formatting**
287
+
288
+ The :meth:`cdxcore.verbose.Context.write` and :meth:`cdxcore.verbose.Context.report` functions provide
289
+ string formatting capabilities. If used, then a message will only be formatted if the current level grants
290
+ it visibility. This avoids unnecessary string operations when no output is required.
291
+
292
+ In the second example above, the format string ``verbose.report(1, "Sub-task {i}", i=i)`` in ``f_2``
293
+ will not be evaluated as that reporting level is turned off.
294
+
295
+ Parameters
296
+ ----------
297
+ init : str | int | :class:`cdxcore.verbose.Context`
298
+
299
+ * If a string is provided: must be ``"all"`` or ``"quiet"``.
300
+
301
+ * If an integer is privided it represents the visibility level up to which to print.
302
+ Set to 0 to print only top level messages.
303
+ Any negative number will turn off any messages and is equivalent to ``"quiet"``.
304
+
305
+ * If set to ``None`` display everything.
306
+
307
+ * A ``Context`` is copied.
308
+
309
+ indent : int, optional
310
+ How much to indent strings per level. Default 2.
311
+
312
+ fmt_level : str, optional
313
+ A format string containing ``%d`` for the current indentation.
314
+ Default is ``"%02ld: "``.
315
+
316
+ level : int, optional
317
+ Current level. If ``init`` is another context, and ``level`` is specified,
318
+ it overwrites the ``level`` from the other context.
319
+
320
+ If ``level`` is ``None``:
321
+
322
+ * If ``init`` is another ``Context`` object, use that object's level.
323
+
324
+ * If ``init`` is an integer or one of the keywords above, use the default, 0.
325
+
326
+ channel : Callable, optional
327
+ *Advanced parameter.*
328
+
329
+ A callable which is called to print text. The call signature is::
330
+
331
+ channel( msg : str, flush : bool )`
332
+
333
+ which is meant to mirror
334
+ ``print( msg, end='', flush )`` for the provided ``channel``.
335
+ In particular do not terminate ``msg`` automatically with a new line.
336
+
337
+ Illustration::
338
+
339
+ class Collector:
340
+ def __init__(self):
341
+ self.messages = []
342
+ def __call__(self, msg, flush ):
343
+ self.messages.append( msg )
344
+
345
+ collect = Collector()
346
+ verbose = Context( channel = collect )
347
+
348
+ verbose.write("Write at 0")
349
+ verbose.report(1,"Report at 1")
350
+
351
+ print(collect.messages)
352
+
353
+ prints
354
+
355
+ .. code-block:: python
356
+
357
+ ['00: Write at 0\\n', '01: Report at 1\\n']
26
358
  """
27
359
 
28
360
  QUIET = "quiet"
361
+ """ Constant for the keyword ``"quiet"`` """
362
+
29
363
  ALL = "all"
364
+ """ Constant for the keyword ``"all"`` """
30
365
 
31
- def __init__(self, verbose_or_init = None, *,
366
+ def __init__(self, init : str|int|type = None, *,
32
367
  indent : int = 2,
33
368
  fmt_level : str = "%02ld: ",
34
369
  level : int = None,
@@ -36,78 +371,34 @@ class Context(object):
36
371
  ):
37
372
  """
38
373
  Create a Context object.
39
-
40
- The following three calling styles are supported
41
-
42
- Construct with keywords
43
- Context( "all" )
44
- Context( "quiet" )
45
-
46
- Display everything
47
- Context( None )
48
-
49
- Display only up to level 2 (the root context is level 0)
50
- Context( 2 )
51
-
52
- Copy constructor
53
- Context( context )
54
- In this case all other parameters are ignored.
55
-
56
- Parameters
57
- ----------
58
- verbose_or_init : str, int, or Context
59
- if a string: one of 'all' or 'quiet'
60
- if an integer: the level at which to print. Any negative number will not print anything because the default level is 0.
61
- if None: equivalent to displaying everything ("all")
62
- if a Context: copy constructor.
63
- indent : int
64
- How much to indent prints per level
65
- fmt_level :
66
- How to format output given level*indentm using %ld for the current level.
67
-
68
- Advanced parameters
69
- -------------------
70
- level :
71
- Initial level. This can also be set if verbose_or_init is another context.
72
- If 'level' is None:
73
- If 'verbose_or_init' is another Context object, use that object's level
74
- If 'verbose_or_init' is an integer or one of the keywords above, use 0
75
- channel :
76
- A callable which is called to print text.
77
- It will be called channel( msg : str, flush : bool ) which should mirror print( msg, end='', flush ).
78
- In particular do not terminate with a new line.
79
- This can also be set if verbose_or_init is another context, i.e.
80
- verbose = Context()
81
- ...
82
- cverbose = Context( verbose, channel=lambda msg, flush : pass )
83
- will return a silenced verbose.
84
-
85
374
  """
86
- if not level is None: _verify( level>=0, "'level' must not be negative; found {level}", exception=ValueError)
87
- if isinstance( verbose_or_init, Context ) or type(verbose_or_init).__name__ == "Context":
375
+ if not level is None: verify( level>=0, "'level' must not be negative; found {level}", level=level, exception=ValueError)
376
+ if isinstance( init, Context ) or type(init).__name__ == "Context":
88
377
  # copy constructor
89
- self.verbose = verbose_or_init.verbose
90
- self.level = verbose_or_init.level if level is None else level
91
- self.indent = verbose_or_init.indent
92
- self.fmt_level = verbose_or_init.fmt_level
378
+ self.visibility = init.visibility
379
+ self.level = init.level if level is None else level
380
+ self.indent = init.indent
381
+ self.fmt_level = init.fmt_level
93
382
  self.crman = CRMan()
94
- self.channel = verbose_or_init.channel if channel is None else channel
383
+ self.channel = init.channel if channel is None else channel
95
384
  return
96
385
 
97
- if isinstance( verbose_or_init, str ):
386
+ if isinstance( init, str ):
98
387
  # construct with key word
99
- if verbose_or_init == self.QUIET:
100
- verbose_or_init = -1
388
+ if init == self.QUIET:
389
+ init = -1
101
390
  else:
102
- _verify( verbose_or_init == self.ALL, lambda : f"'verbose_or_init': if provided as a string, has to be '{self.QUIET}' or '{self.ALL}'. Found '{verbose_or_init}'", exception=ValueError)
103
- verbose_or_init = None
104
- elif not verbose_or_init is None:
105
- verbose_or_init = int(verbose_or_init)
391
+ verify( init == self.ALL,
392
+ lambda : f"'init': if provided as a string, has to be '{self.QUIET}' or"+\
393
+ f"'{self.ALL}'. Found '{init}'", exception=ValueError)
394
+ init = None
395
+ elif not init is None:
396
+ init = int(init)
106
397
 
107
398
  indent = int(indent)
108
- _verify( indent >=0, "'indent' cannot be negative. Found {indent}", exception=ValueError)
399
+ verify( indent >=0, "'indent' cannot be negative. Found {indent}", indent=indent, exception=ValueError)
109
400
 
110
- self.verbose = verbose_or_init # print up to this level
401
+ self.visibility = init # print up to this level
111
402
  self.level = 0 if level is None else level
112
403
  self.indent = indent # indentation level
113
404
  self.fmt_level = str(fmt_level) # output format
@@ -115,241 +406,357 @@ class Context(object):
115
406
  self.channel = channel
116
407
 
117
408
  def write( self, message : str, *args, end : str = "\n", head : bool = True, **kwargs ):
118
- """
119
- Report message at level 0 with the formattting arguments at curent context level.
120
- The message will be formatted as util.fmt( message, *args, **kwargs )
121
- It will be displayed in all cases except if the context is 'quiet'.
409
+ r"""
410
+ Report message at current level.
411
+
412
+ The message will be formatted using :func:`cdxcore.err.fmt`
413
+ if the current level is visible. If the current level is not visible no message formatting
414
+ will take place.
122
415
 
123
- The parameter 'end' matches 'end' in print, e.g. end='' avoids a newline at the end of the message.
124
- If 'head' is True, then the first line of the text will be preceeded by proper indentation.
125
- If 'head' is False, the first line will be printed without preamble.
416
+ The parameter ``end`` matches ``end`` in :func:`print`
417
+ e.g. ``end=''``
418
+ avoids a newline at the end of the message.
419
+
420
+ * If ``head`` is ``True``, then the first line of the text will be preceeded by proper indentation.
421
+
422
+ * If ``head`` is ``False``, the first line will be printed without preamble.
126
423
 
127
- This means the following is a valid pattern
424
+ This means the following is a valid pattern::
128
425
 
426
+ from cdxcore.verbose import Context
129
427
  verbose = Context()
130
428
  verbose.write("Doing something... ", end='')
131
- # do something
429
+ # ... do something
132
430
  verbose.write("done.", head=False)
133
431
 
134
- which now prints
432
+ which prints
433
+
434
+ .. code-block:: python
135
435
 
136
436
  00: Doing something... done.
437
+
438
+ Another use case is updates per line, for example::
439
+
440
+ from cdxcore.verbose import Context
441
+ verbose = Context()
442
+ N = 1000
443
+ for i in range(N):
444
+ verbose.write(f"\\rDoing something {int(float(i+1)/float(N)*100)}%... ", end='')
445
+ # do something
446
+ verbose.write("done.", head=False)
447
+
448
+ which will provide progress information in a given line.
449
+
450
+ *Implementation notice*: the use of ``\r`` is managed using :class:`cdxcore.crman.CRMan`.
451
+
452
+ Parameters
453
+ ----------
454
+ message : str|Callable
455
+
456
+ Text containing format characters.
457
+
458
+ The following alternatives are suppoted:
459
+
460
+ * Python 3 ```{parameter:d}```, in which case ``message.fmt(kwargs)`` for :meth:`str.format` is used
461
+ to obtain the output message.
462
+
463
+ * Python 2 ```%(parameter)d``` in which case ``message % kwargs`` is used to obtain the output message.
464
+
465
+ * Classic C-stype ```%d, %s, %f``` in which case ``message % args`` is used to obtain the output message.
466
+
467
+ * If ``message`` is a ``Callable`` such as a ``lambda`` function, then ``message( *args, **kwargs )``
468
+ is called to obtain the output message.
469
+
470
+ Note that a common use case is using an f-string wrapped in a ``lambda`` function. In this case
471
+ you do not need ``args`` or ``kwargs``::
472
+
473
+ x = 1
474
+ verbose.write(lambda : f"Delayed f-string formatting {x}")
475
+
476
+ end : str, optional
477
+ Terminating string akin to ``end`` in :func:`print`.
478
+ Use ``''`` to not print a newline. See example above for a use case.
479
+
480
+ head : bool, optional;
481
+ Whether this message needs a header (i.e. the ``01`` and spacing).
482
+ Typically ``False`` if the previous call to ``write()`` used `end=''`. See examples above.
137
483
 
484
+ *args, **kwargs:
485
+ See above
138
486
  """
139
487
  self.report( 0, message, *args, end=end, head=head, **kwargs )
140
488
 
141
- def write_t( self, message : str, *args, end : str = "\n", head : bool = True, **kwargs ) -> Timer:
489
+ def write_t( self, message : str|Callable, *args, end : str = "\n", head : bool = True, **kwargs ) -> Timer:
142
490
  """
143
- Same as using write() first and then timer()
144
-
145
- Report message at level 0 with the formattting arguments at curent context level.
146
- The message will be formatted as util.fmt( message, *args, **kwargs )
147
- It will be displayed in all cases except if the context is 'quiet'.
148
-
149
- The parameter 'end' matches 'end' in print, e.g. end='' avoids a newline at the end of the message.
150
- If 'head' is True, then the first line of the text will be preceeded by proper indentation.
151
- If 'head' is False, the first line will be printed without preamble.
152
-
153
- This function returns a util.Timer() object which can be used to measure
154
- time taken for a given task:
491
+ Reports ``message`` subject to string formatting at current level if visible and returns a
492
+ :class:`cdxcore.util.Timer` object
493
+ which can be used to measure time elapsed since ``write_t()`` was called::
155
494
 
495
+ from cdxcore.verbose import Context
156
496
  verbose = Context()
157
- with verbose.write_t("Doing something... ", end='') as t:
497
+ with verbose.write_t("Doing something... ", end='') as tme:
158
498
  # do something
159
- verbose.write("done; this took {t}.", head=False)
499
+ verbose.write("done; this took {tme}.", head=False)
500
+
501
+ produces
502
+
503
+ .. code-block:: python
504
+
505
+ 00: Doing something... done; this took 1s.
506
+
507
+ Equivalent to using :meth:`cdxcore.verbose.Context.write` first followed by
508
+ :meth:`cdxcore.verbose.Context.timer`.
160
509
  """
161
510
  self.report( 0, message, *args, end=end, head=head, **kwargs )
162
- return Timer()
511
+ return self.timer()
163
512
 
164
- def report( self, level : int, message : str, *args, end : str = "\n", head : bool = True, **kwargs ):
165
- """
166
- Print message with the formattting arguments at curent context level plus 'level'
167
- The message will be formatted as util.fmt( message, *args, **kwargs )
168
- Will print empty lines.
513
+ def report( self, level : int, message : str|Callable, *args, end : str = "\n", head : bool = True, **kwargs ):
514
+ r"""
515
+ Report message at current level plus ``level``.
516
+
517
+ The message will be formatted using :func:`cdxcore.err.fmt` is the current level
518
+ plus `level` is visible.
169
519
 
170
- The 'end' and 'head' parameters can be used as follows
520
+ The parameter ``end`` matches ``end`` in :func:`print`
521
+ e.g. ``end=''``
522
+ avoids a newline at the end of the message.
523
+
524
+ * If ``head`` is ``True``, then the first line of the text will be preceeded by proper indentation.
525
+
526
+ * If ``head`` is ``False``, the first line will be printed without preamble.
171
527
 
528
+ This means the following is a valid pattern::
529
+
530
+ from cdxcore.verbose import Context
172
531
  verbose = Context()
532
+ verbose.report(1, "Doing something... ", end='')
533
+ # ... do something
534
+ verbose.report(1, "done.", head=False)
173
535
 
174
- verbose.report(1, "Testing... ", end='')
175
- # do some stuff
176
- verbose.report(1, "done.\nOverall result is good", head=False)
536
+ which prints
177
537
 
178
- prints
538
+ .. code-block:: python
539
+
540
+ 01: Doing something... done.
541
+
542
+ Another use case is updates per line, for example:::
179
543
 
180
- 01: Testing... done.
181
- 01: Overall result is good
544
+ from cdxcore.verbose import Context
545
+ verbose = Context()
546
+ N = 1000
547
+ for i in range(N):
548
+ verbose.report(1,f"\\rStatus {int(float(i+1)/float(N)*100)}%... ", end='')
549
+ # do something
550
+ verbose.report(1,"done.", head=False)
551
+
552
+ will provide progress information in the current line as the loop is processed.
553
+
554
+ *Implementation notice:* The use of ``\\r`` is managed using :class:`cdxcore.crman.CRMan`.
182
555
 
183
556
  Parameters
184
557
  ----------
185
- level : int
186
- Additional context level, added to the level of 'self'.
187
- message, args, kwargs:
188
- Parameters for util.fmt().
189
- end : string
190
- Same function as in print(). In particular, end='' avoids a newline at the end of the messahe
191
- head : bool
192
- If False, do not print out preamble for first line of 'message'
558
+ level : int
559
+ Level to add to current level.
560
+
561
+ message : str|Callable
562
+
563
+ Text containing format characters.
564
+
565
+ The following alternatives are suppoted:
566
+
567
+ * Python 3 ```{parameter:d}```, in which case ``message.fmt(kwargs)`` for :meth:`str.format` is used
568
+ to obtain the output message.
569
+
570
+ * Python 2 ```%(parameter)d``` in which case ``message % kwargs`` is used to obtain the output message.
571
+
572
+ * Classic C-stype ```%d, %s, %f``` in which case ``message % args`` is used to obtain the output message.
573
+
574
+ * If ``message`` is a ``Callable`` such as a ``lambda`` function, then ``message( *args, **kwargs )``
575
+ is called to obtain the output message.
576
+
577
+ Note that a common use case is using an f-string wrapped in a ``lambda`` function. In this case
578
+ you do not need ``args`` or ``kwargs``::
579
+
580
+ x = 1
581
+ verbose.write(lambda : f"Delayed f-string formatting {x}")
582
+
583
+ end : str, optional
584
+ Terminating string akin to ``end`` in :func:`print`.
585
+ Use ``''`` to not print a newline. See example above for a use case.
586
+
587
+ head : bool, optional;
588
+ Whether this message needs a header (i.e. the ``01`` and spacing).
589
+ Typically ``False`` if the previous call to ``write()`` used `end=''`. See examples above.
193
590
 
591
+ *args, **kwargs:
592
+ See above
194
593
  """
195
594
  message = self.fmt( level, message, *args, head=head, **kwargs )
196
595
  if not message is None:
197
596
  self.crman.write(message,end=end,flush=True, channel=self.channel )
198
597
 
199
- def fmt( self, level : int, message : str, *args, head : bool = True, **kwargs ) -> str:
598
+ def fmt( self, level : int, message : str|Callable, *args, head : bool = True, **kwargs ) -> str:
200
599
  """
201
- Formats message with the formattting arguments at curent context level plus 'level'
202
- The message will be formatted with util.fmt( message, *args, **kwargs ) and then indented appropriately.
600
+ Formats message with the formattting arguments at curent context level plus ``level``.
601
+
602
+ This function returns ```` if current level plus ``level`` is not visible.
603
+ In that case no string formatting takes place.
203
604
 
204
605
  Parameters
205
606
  ----------
206
- level : int
207
- Additional context level, added to the level of 'self'.
208
- message, args, kwargs:
209
- Parameters for the util.fmt().
210
- head : bool
211
- Set to False to turn off indentation for the first line of the resulting
212
- message. See write() for an example use case
607
+ level : int
608
+ Level to add to current level.
609
+
610
+ message : str|Callable
611
+
612
+ Text containing format characters.
613
+
614
+ The following alternatives are suppoted:
615
+
616
+ * Python 3 ```{parameter:d}```, in which case ``message.fmt(kwargs)`` for :meth:`str.format` is used
617
+ to obtain the output message.
618
+
619
+ * Python 2 ```%(parameter)d``` in which case ``message % kwargs`` is used to obtain the output message.
620
+
621
+ * Classic C-stype ```%d, %s, %f``` in which case ``message % args`` is used to obtain the output message.
622
+
623
+ * If ``message`` is a ``Callable`` such as a ``lambda`` function, then ``message( *args, **kwargs )``
624
+ is called to obtain the output message.
625
+
626
+ Note that a common use case is using an f-string wrapped in a ``lambda`` function. In this case
627
+ you do not need ``args`` or ``kwargs``::
628
+
629
+ x = 1
630
+ verbose.write(lambda : f"Delayed f-string formatting {x}")
631
+
632
+ head : bool, optional;
633
+ Whether this message needs a header (i.e. the ``01`` and spacing).
634
+ Typically ``False`` if the previous call to ``write()`` used `end=''`. See examples above.
635
+
636
+ *args, **kwargs:
637
+ See above
213
638
 
214
639
  Returns
215
640
  -------
216
- Formatted string, or None if not to be reported at set level.
641
+ String : str
642
+ Formatted string, or ``None` `if the current level plus ``level`` is not visible.
217
643
  """
218
644
  if not self.shall_report(level):
219
645
  return None
220
- message = str(message)
221
- if message == "":
646
+ if isinstance(message, str) and message == "":
222
647
  return ""
223
648
  str_level = self.str_indent( level )
224
- text = fmt( message, *args, **kwargs ) if (len(args) + len(kwargs) > 0) else message
649
+ text = fmt( message, *args, **kwargs )
225
650
  text = text[:-1].replace("\r", "\r" + str_level ) + text[-1]
226
651
  text = text[:-1].replace("\n", "\n" + str_level ) + text[-1]
227
652
  text = str_level + text if head and text[:1] != "\r" else text
228
653
  return text
229
654
 
230
- def sub( self, add_level : int = 1, message : str = None, *args, **kwargs ):
655
+ def __call__(self, add_level : int = 1, message : str|Callable = None, end : str = "\n", head : bool = True, *args, **kwargs ):
231
656
  """
232
- Create a sub context at level 'sub_level'. The latter defaults to self.default_sub
657
+ Create and return a sub ``Context`` at current level plus ``add_level``.
658
+
659
+ If a ``message`` is provided, :meth:`cdxcore.verbose.Context.write()` is called
660
+ before the new ``Context`` is created.
661
+
662
+ Example::
663
+
664
+ from cdxcore.verbose import Context
665
+ def f( verbose : Context = Context.quiet ):
666
+ # ...
667
+ verbose.write("'f'' usuing a sub-context.")
668
+ verbose = Context.all
669
+ verbose.write("Main")
670
+ f( verbose=verbose(1) ) # create sub-context
671
+
672
+ prints
673
+
674
+ .. code-block:: python
675
+
676
+ 00: Main
677
+ 01: 'f'' usuing a sub-context.
233
678
 
234
679
  Parameters
235
680
  ----------
236
- add_level : int
237
- Level of the sub context with respect to self. Set to 0 for the same level.
238
- message, fmt, args:
239
- If message is not None, call report() at _current_ level, not the newly
240
- created sub level
681
+ add_level : int
682
+ Level to add to the current level. Set to 0 for the same level.
683
+
684
+ message : str|Callable, optional.
685
+
686
+ Text containing format characters, or ``None`` to not print a message.
687
+
688
+ The following alternatives are suppoted:
689
+
690
+ * Python 3 ```{parameter:d}```, in which case ``message.fmt(kwargs)`` for :meth:`str.format` is used
691
+ to obtain the output message.
692
+
693
+ * Python 2 ```%(parameter)d``` in which case ``message % kwargs`` is used to obtain the output message.
694
+
695
+ * Classic C-stype ```%d, %s, %f``` in which case ``message % args`` is used to obtain the output message.
696
+
697
+ * If ``message`` is a ``Callable`` such as a ``lambda`` function, then ``message( *args, **kwargs )``
698
+ is called to obtain the output message.
699
+
700
+ Note that a common use case is using an f-string wrapped in a ``lambda`` function. In this case
701
+ you do not need ``args`` or ``kwargs``::
702
+
703
+ x = 1
704
+ verbose.write(lambda : f"Delayed f-string formatting {x}")
705
+
706
+ head : bool, optional;
707
+ Whether this message needs a header (i.e. the ``01`` and spacing).
708
+ Typically ``False`` if the previous call to ``write()`` used `end=''`. See examples above.
709
+
710
+ *args, **kwargs:
711
+ See above
241
712
 
242
713
  Returns
243
714
  -------
244
- Context
245
- Sub context with level = self.level + sub_level
715
+ verbose : ``Context``
716
+ Sub context with new level equal to current level plus ``add_level``.
246
717
  """
247
718
  add_level = int(add_level)
248
- _verify( add_level >= 0, "'add_level' cannot be negative. Found {add_level}", exception=ValueError)
719
+ verify( add_level >= 0, "'add_level' cannot be negative. Found {add_level}", add_level=add_level, exception=ValueError)
249
720
 
250
721
  if not message is None:
251
- self.write( message=message, *args, **kwargs )
722
+ self.write( message=message, end=end, head=head, *args, **kwargs )
252
723
 
253
- sub = Context(self.verbose)
254
- assert sub.verbose == self.verbose, "Internal error"
724
+ sub = Context(self)
725
+ assert sub.visibility == self.visibility, "Internal error"
255
726
  sub.level = self.level + add_level
256
- sub.indent = self.indent
257
- sub.fmt_level = self.fmt_level
258
727
  return sub
259
728
 
260
- def __call__(self, add_level : int, message : str = None, *args, **kwargs ):
261
- """
262
- Create a sub context at level 'sub_level'. The latter defaults to self.default_sub.
263
- Optionally write message at the new level.
264
-
265
- That means writing
266
- verbose(1,"Hallo")
267
- is equivalent to
268
- verbose.report(1,"Hallo")
269
-
270
- Parameters
271
- ----------
272
- add_level : int
273
- Level of the sub context with respect to self. Set to 0 for the same level.
274
- For convenience, the user may also let 'add_level' be a string, replacing 'message'.
275
-
276
- The following two are equivalent
277
- verbose(0,"Message %(test)s", test="test")
278
- and
279
- verbose("Message %(test)s", test="test")
280
-
281
- message, fmt, args:
282
- If message is not None, call report() at _current_ level, not the newly
283
- created sub level.
284
-
285
- Returns
286
- -------
287
- Context
288
- Sub context with level = self.level + sub_level
289
- """
290
- if message is None:
291
- assert len(args) == 0 and len(kwargs) == 0, "Internal error: no 'message' is provided."
292
- return self.sub(add_level)
293
- if isinstance(add_level, str):
294
- _verify( message is None, "Cannot specify 'add_level' as string and also specify 'message'", exception=ValueError)
295
- self.write( add_level, *args, **kwargs )
296
- return self
297
- else:
298
- assert isinstance(add_level, int), "'add_level' should be an int or a string"
299
- self.report( add_level, message, *args, **kwargs )
300
- return self.sub(add_level)
301
-
302
- def limit(self, verbose):
303
- """ Assigns the minimim verbosity of self and verbose, i.e. self.verbose = min(self.verbose,verbose) """
304
- if verbose is None:
305
- return self # None means accepting everything
306
- if isinstance(verbose, Context) or type(verbose).__name__ == "Context":
307
- verbose = verbose.verbose
308
- if verbose is None:
309
- return self
310
- elif verbose == self.QUIET:
311
- verbose = -1
312
- elif verbose == self.ALL:
313
- return self
314
- else:
315
- verbose = int(verbose)
316
- if self.verbose is None:
317
- self.verbose = verbose
318
- elif self.verbose > verbose:
319
- self.verbose = verbose
320
- return self
321
-
322
729
  @property
323
730
  def as_verbose(self):
324
- """ Return a Context at the same level as 'self' with full verbosity """
731
+ """ Return a Context at the same current reporting level as ``self`` with full visibility """
325
732
  copy = Context(self)
326
- copy.verbose = None
733
+ copy.visibility = None
327
734
  return copy
328
735
 
329
736
  @property
330
737
  def as_quiet(self):
331
- """ Return a Context at the same level as 'self' with zero verbosity """
738
+ """ Return a Context at the same current reporting level as ``self`` with zero visibility """
332
739
  copy = Context(self)
333
- copy.verbose = 0
740
+ copy.visibility = 0
334
741
  return copy
335
742
 
336
743
  @property
337
744
  def is_quiet(self) -> bool:
338
- """ Whether the current context is quiet """
339
- return not self.verbose is None and self.verbose < 0
340
-
341
- def shall_report(self, sub_level : int = 0 ) -> bool:
342
- """ Returns whether to print something at 'sub_level' relative to the current level """
343
- sub_level = int(sub_level)
344
- _verify( sub_level >= 0, "'sub_level' cannot be negative. Found {sub_level}", exception=ValueError)
345
- return self.verbose is None or self.verbose >= self.level + sub_level
346
-
347
- def str_indent(self, sub_level : int = 0) -> str:
348
- """ Returns the string identation for a given 'sub_level', or the context """
349
- sub_level = int(sub_level)
350
- _verify( sub_level >= 0, "'sub_level' cannot be negative. Found {sub_level}", exception=ValueError)
351
- s1 = ' ' * (self.indent * (self.level + sub_level))
352
- s2 = self.fmt_level if self.fmt_level.find("%") == -1 else self.fmt_level % (self.level + sub_level)
745
+ """ Whether the current context is ``"quiet"`` """
746
+ return not self.visibility is None and self.visibility < 0
747
+
748
+ def shall_report(self, add_level : int = 0 ) -> bool:
749
+ """ Returns whether to print something at current level plus ``add_level``. """
750
+ add_level = int(add_level)
751
+ verify( add_level >= 0, "'add_level' cannot be negative. Found {add_level}", add_level=add_level, exception=ValueError)
752
+ return self.visibility is None or self.visibility >= self.level + add_level
753
+
754
+ def str_indent(self, add_level : int = 0) -> str:
755
+ """ Returns the string identation for the current level plus ``add_level`` """
756
+ add_level = int(add_level)
757
+ verify( add_level >= 0, "'add_level' cannot be negative. Found {add_level}", add_level=add_level, exception=ValueError)
758
+ s1 = ' ' * (self.indent * (self.level + add_level))
759
+ s2 = self.fmt_level if self.fmt_level.find("%") == -1 else self.fmt_level % (self.level + add_level)
353
760
  return s2+s1
354
761
 
355
762
  # Misc
@@ -357,25 +764,36 @@ class Context(object):
357
764
 
358
765
  def timer(self) -> Timer:
359
766
  """
360
- Returns a new util.Timer object to measure time spent in a block of code
767
+ Returns a new :class:`cdxcore.util.Timer` object to measure time spent in a block of code.
361
768
 
362
- verbose = Context("all")
363
- with verbose.timer() as t:
364
- verbose.write("Starting... ", end='')
365
- ...
366
- verbose.write(f"this took {t}.", head=False)
367
-
368
- Equivalent to Context.Timer()
769
+ Example::
770
+
771
+ import time as time
772
+ from cdxcore.verbose import Context
773
+
774
+ verbose = Context("all")
775
+ with verbose.Timer() as tme:
776
+ verbose.write("Starting job... ", end='')
777
+ time.sleep(1)
778
+ verbose.write(f"done; this took {tme}.", head=False)
779
+
780
+ produces
781
+
782
+ .. code-block:: python
783
+
784
+ 00: Starting job... done; this took 1s.
785
+
369
786
  """
370
787
  return Timer()
371
788
 
372
789
  # uniqueHash
373
790
  # ----------
374
791
 
375
- def __unique_hash__( self, uniqueHash, debug_trace ) -> str:
792
+ def __unique_hash__( self, unique_hash, debug_trace ) -> str:
376
793
  """
377
- Compute non-hash for use with cdxbasics.util.uniqueHash()
794
+ Hash function for :class:`cdxcore.uniquehash.UniqueHash`.
378
795
  This function always returns an empty string, which means that the object is never hashed.
796
+ :meta private:
379
797
  """
380
798
  return ""
381
799
 
@@ -384,19 +802,36 @@ class Context(object):
384
802
 
385
803
  def apply_channel( self, channel : Callable ):
386
804
  """
387
- Returns a new Context object with the same currrent state as 'self', but pointing to 'channel'
805
+ *Advanced Use*
806
+
807
+ Returns a new ```Context`` object with the same currrent state as ``self``,
808
+ but pointing to ``channel``.
388
809
  """
389
810
  return Context( self, channel=channel ) if channel != self.channel else self
390
811
 
391
812
 
392
- # Recommended default parameter 'quiet' for functions accepting a context parameter
393
- quiet = Context(Context.QUIET)
813
+ quiet = Context(Context.QUIET)
814
+ all_ = Context(Context.ALL)
394
815
  Context.quiet = quiet
816
+ """
817
+ A default ``Context`` with zero visibility.
818
+ """
395
819
 
396
- all_ = Context(Context.ALL)
397
- Context.all = all_
820
+ Context.all = all_
821
+ """
822
+ A default ``Context`` with full visibility.
823
+ """
824
+
825
+ Context.quiet.__doc__ = \
826
+ """
827
+ A default ``Context`` with zero visibility.
828
+ """
829
+
830
+ Context.quiet.__doc__ = \
831
+ """
832
+ A default ``Context`` with full visibility.
833
+ """
398
834
 
399
- Context.Timer = Timer
400
835
 
401
836
 
402
837