cdxcore 0.1.5__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 ADDED
@@ -0,0 +1,403 @@
1
+ """
2
+ verbose
3
+ Utility for verbose printing with indentation
4
+ Hans Buehler 2022
5
+ """
6
+
7
+ from .util import fmt, Timer
8
+ from .util import _verify
9
+ from .crman import CRMan, Callable
10
+
11
+ class Context(object):
12
+ """
13
+ Class for printing indented messages, filtered by overall level of verbosity.
14
+
15
+ context = Context( verbose = 4 )
16
+
17
+ def f_2( context ):
18
+
19
+ context.report( 1, "Running 'f_2'")
20
+
21
+ def f_1( context ):
22
+
23
+ context.report( 1, "Running 'f_1'")
24
+ f_2( context.sub(1, "Entering 'f_2'") )
25
+
26
+ """
27
+
28
+ QUIET = "quiet"
29
+ ALL = "all"
30
+
31
+ def __init__(self, verbose_or_init = None, *,
32
+ indent : int = 2,
33
+ fmt_level : str = "%02ld: ",
34
+ level : int = None,
35
+ channel : Callable = None
36
+ ):
37
+ """
38
+ 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
+ """
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":
88
+ # 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
93
+ self.crman = CRMan()
94
+ self.channel = verbose_or_init.channel if channel is None else channel
95
+ return
96
+
97
+ if isinstance( verbose_or_init, str ):
98
+ # construct with key word
99
+ if verbose_or_init == self.QUIET:
100
+ verbose_or_init = -1
101
+ 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)
106
+
107
+ indent = int(indent)
108
+ _verify( indent >=0, "'indent' cannot be negative. Found {indent}", exception=ValueError)
109
+
110
+ self.verbose = verbose_or_init # print up to this level
111
+ self.level = 0 if level is None else level
112
+ self.indent = indent # indentation level
113
+ self.fmt_level = str(fmt_level) # output format
114
+ self.crman = CRMan()
115
+ self.channel = channel
116
+
117
+ 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'.
122
+
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.
126
+
127
+ This means the following is a valid pattern
128
+
129
+ verbose = Context()
130
+ verbose.write("Doing something... ", end='')
131
+ # do something
132
+ verbose.write("done.", head=False)
133
+
134
+ which now prints
135
+
136
+ 00: Doing something... done.
137
+
138
+ """
139
+ self.report( 0, message, *args, end=end, head=head, **kwargs )
140
+
141
+ def write_t( self, message : str, *args, end : str = "\n", head : bool = True, **kwargs ) -> Timer:
142
+ """
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:
155
+
156
+ verbose = Context()
157
+ with verbose.write_t("Doing something... ", end='') as t:
158
+ # do something
159
+ verbose.write("done; this took {t}.", head=False)
160
+ """
161
+ self.report( 0, message, *args, end=end, head=head, **kwargs )
162
+ return Timer()
163
+
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.
169
+
170
+ The 'end' and 'head' parameters can be used as follows
171
+
172
+ verbose = Context()
173
+
174
+ verbose.report(1, "Testing... ", end='')
175
+ # do some stuff
176
+ verbose.report(1, "done.\nOverall result is good", head=False)
177
+
178
+ prints
179
+
180
+ 01: Testing... done.
181
+ 01: Overall result is good
182
+
183
+ Parameters
184
+ ----------
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'
193
+
194
+ """
195
+ message = self.fmt( level, message, *args, head=head, **kwargs )
196
+ if not message is None:
197
+ self.crman.write(message,end=end,flush=True, channel=self.channel )
198
+
199
+ def fmt( self, level : int, message : str, *args, head : bool = True, **kwargs ) -> str:
200
+ """
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.
203
+
204
+ Parameters
205
+ ----------
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
213
+
214
+ Returns
215
+ -------
216
+ Formatted string, or None if not to be reported at set level.
217
+ """
218
+ if not self.shall_report(level):
219
+ return None
220
+ message = str(message)
221
+ if message == "":
222
+ return ""
223
+ str_level = self.str_indent( level )
224
+ text = fmt( message, *args, **kwargs ) if (len(args) + len(kwargs) > 0) else message
225
+ text = text[:-1].replace("\r", "\r" + str_level ) + text[-1]
226
+ text = text[:-1].replace("\n", "\n" + str_level ) + text[-1]
227
+ text = str_level + text if head and text[:1] != "\r" else text
228
+ return text
229
+
230
+ def sub( self, add_level : int = 1, message : str = None, *args, **kwargs ):
231
+ """
232
+ Create a sub context at level 'sub_level'. The latter defaults to self.default_sub
233
+
234
+ Parameters
235
+ ----------
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
241
+
242
+ Returns
243
+ -------
244
+ Context
245
+ Sub context with level = self.level + sub_level
246
+ """
247
+ add_level = int(add_level)
248
+ _verify( add_level >= 0, "'add_level' cannot be negative. Found {add_level}", exception=ValueError)
249
+
250
+ if not message is None:
251
+ self.write( message=message, *args, **kwargs )
252
+
253
+ sub = Context(self.verbose)
254
+ assert sub.verbose == self.verbose, "Internal error"
255
+ sub.level = self.level + add_level
256
+ sub.indent = self.indent
257
+ sub.fmt_level = self.fmt_level
258
+ return sub
259
+
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
+ @property
323
+ def as_verbose(self):
324
+ """ Return a Context at the same level as 'self' with full verbosity """
325
+ copy = Context(self)
326
+ copy.verbose = None
327
+ return copy
328
+
329
+ @property
330
+ def as_quiet(self):
331
+ """ Return a Context at the same level as 'self' with zero verbosity """
332
+ copy = Context(self)
333
+ copy.verbose = 0
334
+ return copy
335
+
336
+ @property
337
+ 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)
353
+ return s2+s1
354
+
355
+ # Misc
356
+ # ----
357
+
358
+ def timer(self) -> Timer:
359
+ """
360
+ Returns a new util.Timer object to measure time spent in a block of code
361
+
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()
369
+ """
370
+ return Timer()
371
+
372
+ # uniqueHash
373
+ # ----------
374
+
375
+ def __unique_hash__( self, uniqueHash, debug_trace ) -> str:
376
+ """
377
+ Compute non-hash for use with cdxbasics.util.uniqueHash()
378
+ This function always returns an empty string, which means that the object is never hashed.
379
+ """
380
+ return ""
381
+
382
+ # Channels
383
+ # --------
384
+
385
+ def apply_channel( self, channel : Callable ):
386
+ """
387
+ Returns a new Context object with the same currrent state as 'self', but pointing to 'channel'
388
+ """
389
+ return Context( self, channel=channel ) if channel != self.channel else self
390
+
391
+
392
+ # Recommended default parameter 'quiet' for functions accepting a context parameter
393
+ quiet = Context(Context.QUIET)
394
+ Context.quiet = quiet
395
+
396
+ all_ = Context(Context.ALL)
397
+ Context.all = all_
398
+
399
+ Context.Timer = Timer
400
+
401
+
402
+
403
+