@@ -74,7 +74,7 @@ def maybe_open(f, mode='r', **kwargs):
7474 with open (f , mode , ** kwargs ) as f : yield f
7575 else : yield f
7676
77- # %% ../nbs/03_xtras.ipynb 26
77+ # %% ../nbs/03_xtras.ipynb 28
7878def image_size (fn ):
7979 "Tuple of (w,h) for png, gif, or jpg; `None` otherwise"
8080 import imghdr ,struct
@@ -98,7 +98,7 @@ def _png_size(f):
9898 d = dict (png = _png_size , gif = _gif_size , jpeg = _jpg_size )
9999 with maybe_open (fn , 'rb' ) as f : return d [imghdr .what (f )](f )
100100
101- # %% ../nbs/03_xtras.ipynb 28
101+ # %% ../nbs/03_xtras.ipynb 30
102102def bunzip (fn ):
103103 "bunzip `fn`, raising exception if output already exists"
104104 fn = Path (fn )
@@ -109,15 +109,15 @@ def bunzip(fn):
109109 with bz2 .BZ2File (fn , 'rb' ) as src , out_fn .open ('wb' ) as dst :
110110 for d in iter (lambda : src .read (1024 * 1024 ), b'' ): dst .write (d )
111111
112- # %% ../nbs/03_xtras.ipynb 30
112+ # %% ../nbs/03_xtras.ipynb 32
113113def loads (s , ** kw ):
114114 "Same as `json.loads`, but handles `None`"
115115 if not s : return {}
116116 try : import ujson as json
117117 except ModuleNotFoundError : import json
118118 return json .loads (s , ** kw )
119119
120- # %% ../nbs/03_xtras.ipynb 31
120+ # %% ../nbs/03_xtras.ipynb 33
121121def loads_multi (s :str ):
122122 "Generator of >=0 decoded json dicts, possibly with non-json ignored text at start and end"
123123 import json
@@ -129,22 +129,22 @@ def loads_multi(s:str):
129129 yield obj
130130 s = s [pos :]
131131
132- # %% ../nbs/03_xtras.ipynb 33
132+ # %% ../nbs/03_xtras.ipynb 35
133133def dumps (obj , ** kw ):
134134 "Same as `json.dumps`, but uses `ujson` if available"
135135 try : import ujson as json
136136 except ModuleNotFoundError : import json
137137 else : kw ['escape_forward_slashes' ]= False
138138 return json .dumps (obj , ** kw )
139139
140- # %% ../nbs/03_xtras.ipynb 34
140+ # %% ../nbs/03_xtras.ipynb 36
141141def _unpack (fname , out ):
142142 import shutil
143143 shutil .unpack_archive (str (fname ), str (out ))
144144 ls = out .ls ()
145145 return ls [0 ] if len (ls ) == 1 else out
146146
147- # %% ../nbs/03_xtras.ipynb 35
147+ # %% ../nbs/03_xtras.ipynb 37
148148def untar_dir (fname , dest , rename = False , overwrite = False ):
149149 "untar `file` into `dest`, creating a directory if the root contains more than one item"
150150 import tempfile ,shutil
@@ -162,14 +162,14 @@ def untar_dir(fname, dest, rename=False, overwrite=False):
162162 shutil .move (str (src ), dest )
163163 return dest
164164
165- # %% ../nbs/03_xtras.ipynb 43
165+ # %% ../nbs/03_xtras.ipynb 45
166166def repo_details (url ):
167167 "Tuple of `owner,name` from ssh or https git repo `url`"
168168 res = remove_suffix (url .strip (), '.git' )
169169 res = res .split (':' )[- 1 ]
170170 return res .split ('/' )[- 2 :]
171171
172- # %% ../nbs/03_xtras.ipynb 45
172+ # %% ../nbs/03_xtras.ipynb 47
173173def run (cmd , * rest , same_in_win = False , ignore_ex = False , as_bytes = False , stderr = False ):
174174 "Pass `cmd` (splitting with `shlex` if string) to `subprocess.run`; return `stdout`; raise `IOError` if fails"
175175 # Even the command is same on Windows, we have to add `cmd /c `"
@@ -193,7 +193,7 @@ def run(cmd, *rest, same_in_win=False, ignore_ex=False, as_bytes=False, stderr=F
193193 if res .returncode : raise IOError (stdout )
194194 return stdout
195195
196- # %% ../nbs/03_xtras.ipynb 53
196+ # %% ../nbs/03_xtras.ipynb 55
197197def open_file (fn , mode = 'r' , ** kwargs ):
198198 "Open a file, with optional compression if gz or bz2 suffix"
199199 if isinstance (fn , io .IOBase ): return fn
@@ -204,81 +204,81 @@ def open_file(fn, mode='r', **kwargs):
204204 elif fn .suffix == '.zip' : return zipfile .ZipFile (fn , mode , ** kwargs )
205205 else : return open (fn ,mode , ** kwargs )
206206
207- # %% ../nbs/03_xtras.ipynb 54
207+ # %% ../nbs/03_xtras.ipynb 56
208208def save_pickle (fn , o ):
209209 "Save a pickle file, to a file name or opened file"
210210 import pickle
211211 with open_file (fn , 'wb' ) as f : pickle .dump (o , f )
212212
213- # %% ../nbs/03_xtras.ipynb 55
213+ # %% ../nbs/03_xtras.ipynb 57
214214def load_pickle (fn ):
215215 "Load a pickle file from a file name or opened file"
216216 import pickle
217217 with open_file (fn , 'rb' ) as f : return pickle .load (f )
218218
219- # %% ../nbs/03_xtras.ipynb 58
219+ # %% ../nbs/03_xtras.ipynb 60
220220def dict2obj (d , list_func = L , dict_func = AttrDict ):
221221 "Convert (possibly nested) dicts (or lists of dicts) to `AttrDict`"
222222 if isinstance (d , (L ,list )): return list_func (d ).map (dict2obj )
223223 if not isinstance (d , dict ): return d
224224 return dict_func (** {k :dict2obj (v ) for k ,v in d .items ()})
225225
226- # %% ../nbs/03_xtras.ipynb 63
226+ # %% ../nbs/03_xtras.ipynb 65
227227def obj2dict (d ):
228228 "Convert (possibly nested) AttrDicts (or lists of AttrDicts) to `dict`"
229229 if isinstance (d , (L ,list )): return list (L (d ).map (obj2dict ))
230230 if not isinstance (d , dict ): return d
231231 return dict (** {k :obj2dict (v ) for k ,v in d .items ()})
232232
233- # %% ../nbs/03_xtras.ipynb 66
233+ # %% ../nbs/03_xtras.ipynb 68
234234def _repr_dict (d , lvl ):
235235 if isinstance (d ,dict ):
236236 its = [f"{ k } : { _repr_dict (v ,lvl + 1 )} " for k ,v in d .items ()]
237237 elif isinstance (d ,(list ,L )): its = [_repr_dict (o ,lvl + 1 ) for o in d ]
238238 else : return str (d )
239239 return '\n ' + '\n ' .join ([" " * (lvl * 2 ) + "- " + o for o in its ])
240240
241- # %% ../nbs/03_xtras.ipynb 67
241+ # %% ../nbs/03_xtras.ipynb 69
242242def repr_dict (d ):
243243 "Print nested dicts and lists, such as returned by `dict2obj`"
244244 return _repr_dict (d ,0 ).strip ()
245245
246- # %% ../nbs/03_xtras.ipynb 69
246+ # %% ../nbs/03_xtras.ipynb 71
247247def is_listy (x ):
248248 "`isinstance(x, (tuple,list,L,slice,Generator))`"
249249 return isinstance (x , (tuple ,list ,L ,slice ,Generator ))
250250
251- # %% ../nbs/03_xtras.ipynb 71
251+ # %% ../nbs/03_xtras.ipynb 73
252252def mapped (f , it ):
253253 "map `f` over `it`, unless it's not listy, in which case return `f(it)`"
254254 return L (it ).map (f ) if is_listy (it ) else f (it )
255255
256- # %% ../nbs/03_xtras.ipynb 75
256+ # %% ../nbs/03_xtras.ipynb 77
257257@patch
258258def readlines (self :Path , hint = - 1 , encoding = 'utf8' ):
259259 "Read the content of `self`"
260260 with self .open (encoding = encoding ) as f : return f .readlines (hint )
261261
262- # %% ../nbs/03_xtras.ipynb 76
262+ # %% ../nbs/03_xtras.ipynb 78
263263@patch
264264def read_json (self :Path , encoding = None , errors = None ):
265265 "Same as `read_text` followed by `loads`"
266266 return loads (self .read_text (encoding = encoding , errors = errors ))
267267
268- # %% ../nbs/03_xtras.ipynb 77
268+ # %% ../nbs/03_xtras.ipynb 79
269269@patch
270270def mk_write (self :Path , data , encoding = None , errors = None , mode = 511 ):
271271 "Make all parent dirs of `self`, and write `data`"
272272 self .parent .mkdir (exist_ok = True , parents = True , mode = mode )
273273 self .write_text (data , encoding = encoding , errors = errors )
274274
275- # %% ../nbs/03_xtras.ipynb 78
275+ # %% ../nbs/03_xtras.ipynb 80
276276@patch
277277def relpath (self :Path , start = None ):
278278 "Same as `os.path.relpath`, but returns a `Path`, and resolves symlinks"
279279 return Path (os .path .relpath (self .resolve (), Path (start ).resolve ()))
280280
281- # %% ../nbs/03_xtras.ipynb 81
281+ # %% ../nbs/03_xtras.ipynb 83
282282@patch
283283def ls (self :Path , n_max = None , file_type = None , file_exts = None ):
284284 "Contents of path as a list"
@@ -290,7 +290,7 @@ def ls(self:Path, n_max=None, file_type=None, file_exts=None):
290290 if n_max is not None : res = itertools .islice (res , n_max )
291291 return L (res )
292292
293- # %% ../nbs/03_xtras.ipynb 87
293+ # %% ../nbs/03_xtras.ipynb 89
294294@patch
295295def __repr__ (self :Path ):
296296 b = getattr (Path , 'BASE_PATH' , None )
@@ -299,7 +299,7 @@ def __repr__(self:Path):
299299 except : pass
300300 return f"Path({ self .as_posix ()!r} )"
301301
302- # %% ../nbs/03_xtras.ipynb 90
302+ # %% ../nbs/03_xtras.ipynb 92
303303@patch
304304def delete (self :Path ):
305305 "Delete a file, symlink, or directory tree"
@@ -309,12 +309,12 @@ def delete(self:Path):
309309 shutil .rmtree (self )
310310 else : self .unlink ()
311311
312- # %% ../nbs/03_xtras.ipynb 92
312+ # %% ../nbs/03_xtras.ipynb 94
313313class IterLen :
314314 "Base class to add iteration to anything supporting `__len__` and `__getitem__`"
315315 def __iter__ (self ): return (self [i ] for i in range_of (self ))
316316
317- # %% ../nbs/03_xtras.ipynb 93
317+ # %% ../nbs/03_xtras.ipynb 95
318318@docs
319319class ReindexCollection (GetAttr , IterLen ):
320320 "Reindexes collection `coll` with indices `idxs` and optional LRU cache of size `cache`"
@@ -339,7 +339,7 @@ def __setstate__(self, s): self.coll,self.idxs,self.cache,self.tfm = s['coll'],s
339339 shuffle = "Randomly shuffle indices" ,
340340 cache_clear = "Clear LRU cache" )
341341
342- # %% ../nbs/03_xtras.ipynb 112
342+ # %% ../nbs/03_xtras.ipynb 114
343343def _is_type_dispatch (x ): return type (x ).__name__ == "TypeDispatch"
344344def _unwrapped_type_dispatch_func (x ): return x .first () if _is_type_dispatch (x ) else x
345345
@@ -366,15 +366,15 @@ def get_source_link(func):
366366 return f"{ nbdev_mod .git_url } { module } #L{ line } "
367367 except : return f"{ module } #L{ line } "
368368
369- # %% ../nbs/03_xtras.ipynb 115
369+ # %% ../nbs/03_xtras.ipynb 117
370370def truncstr (s :str , maxlen :int , suf :str = '…' , space = '' )-> str :
371371 "Truncate `s` to length `maxlen`, adding suffix `suf` if truncated"
372372 return s [:maxlen - len (suf )]+ suf if len (s )+ len (space )> maxlen else s + space
373373
374- # %% ../nbs/03_xtras.ipynb 117
374+ # %% ../nbs/03_xtras.ipynb 119
375375spark_chars = '▁▂▃▅▆▇'
376376
377- # %% ../nbs/03_xtras.ipynb 118
377+ # %% ../nbs/03_xtras.ipynb 120
378378def _ceil (x , lim = None ): return x if (not lim or x <= lim ) else lim
379379
380380def _sparkchar (x , mn , mx , incr , empty_zero ):
@@ -383,7 +383,7 @@ def _sparkchar(x, mn, mx, incr, empty_zero):
383383 res = int ((_ceil (x ,mx )- mn )/ incr - 0.5 )
384384 return spark_chars [res ]
385385
386- # %% ../nbs/03_xtras.ipynb 119
386+ # %% ../nbs/03_xtras.ipynb 121
387387def sparkline (data , mn = None , mx = None , empty_zero = False ):
388388 "Sparkline for `data`, with `None`s (and zero, if `empty_zero`) shown as empty column"
389389 valid = [o for o in data if o is not None ]
@@ -392,7 +392,7 @@ def sparkline(data, mn=None, mx=None, empty_zero=False):
392392 res = [_sparkchar (x = o , mn = mn , mx = mx , incr = (mx - mn )/ n , empty_zero = empty_zero ) for o in data ]
393393 return '' .join (res )
394394
395- # %% ../nbs/03_xtras.ipynb 123
395+ # %% ../nbs/03_xtras.ipynb 125
396396def modify_exception (
397397 e :Exception , # An exception
398398 msg :str = None , # A custom message
@@ -402,14 +402,14 @@ def modify_exception(
402402 e .args = [f'{ e .args [0 ]} { msg } ' ] if not replace and len (e .args ) > 0 else [msg ]
403403 return e
404404
405- # %% ../nbs/03_xtras.ipynb 125
405+ # %% ../nbs/03_xtras.ipynb 127
406406def round_multiple (x , mult , round_down = False ):
407407 "Round `x` to nearest multiple of `mult`"
408408 def _f (x_ ): return (int if round_down else round )(x_ / mult )* mult
409409 res = L (x ).map (_f )
410410 return res if is_listy (x ) else res [0 ]
411411
412- # %% ../nbs/03_xtras.ipynb 127
412+ # %% ../nbs/03_xtras.ipynb 129
413413def set_num_threads (nt ):
414414 "Get numpy (and others) to use `nt` threads"
415415 try : import mkl ; mkl .set_num_threads (nt )
@@ -420,14 +420,14 @@ def set_num_threads(nt):
420420 for o in ['OPENBLAS_NUM_THREADS' ,'NUMEXPR_NUM_THREADS' ,'OMP_NUM_THREADS' ,'MKL_NUM_THREADS' ]:
421421 os .environ [o ] = str (nt )
422422
423- # %% ../nbs/03_xtras.ipynb 129
423+ # %% ../nbs/03_xtras.ipynb 131
424424def join_path_file (file , path , ext = '' ):
425425 "Return `path/file` if file is a string or a `Path`, file otherwise"
426426 if not isinstance (file , (str , Path )): return file
427427 path .mkdir (parents = True , exist_ok = True )
428428 return path / f'{ file } { ext } '
429429
430- # %% ../nbs/03_xtras.ipynb 132
430+ # %% ../nbs/03_xtras.ipynb 134
431431def autostart (g ):
432432 "Decorator that automatically starts a generator"
433433 @functools .wraps (g )
@@ -437,7 +437,7 @@ def f():
437437 return r
438438 return f
439439
440- # %% ../nbs/03_xtras.ipynb 133
440+ # %% ../nbs/03_xtras.ipynb 135
441441class EventTimer :
442442 "An event timer with history of `store` items of time `span`"
443443
@@ -461,15 +461,15 @@ def duration(self): return time.perf_counter()-self.start
461461 @property
462462 def freq (self ): return self .events / self .duration
463463
464- # %% ../nbs/03_xtras.ipynb 137
464+ # %% ../nbs/03_xtras.ipynb 139
465465_fmt = string .Formatter ()
466466
467- # %% ../nbs/03_xtras.ipynb 138
467+ # %% ../nbs/03_xtras.ipynb 140
468468def stringfmt_names (s :str )-> list :
469469 "Unique brace-delimited names in `s`"
470470 return uniqueify (o [1 ] for o in _fmt .parse (s ) if o [1 ])
471471
472- # %% ../nbs/03_xtras.ipynb 140
472+ # %% ../nbs/03_xtras.ipynb 142
473473class PartialFormatter (string .Formatter ):
474474 "A `string.Formatter` that doesn't error on missing fields, and tracks missing fields and unused args"
475475 def __init__ (self ):
@@ -485,24 +485,24 @@ def get_field(self, nm, args, kwargs):
485485 def check_unused_args (self , used , args , kwargs ):
486486 self .xtra = filter_keys (kwargs , lambda o : o not in used )
487487
488- # %% ../nbs/03_xtras.ipynb 142
488+ # %% ../nbs/03_xtras.ipynb 144
489489def partial_format (s :str , ** kwargs ):
490490 "string format `s`, ignoring missing field errors, returning missing and extra fields"
491491 fmt = PartialFormatter ()
492492 res = fmt .format (s , ** kwargs )
493493 return res ,list (fmt .missing ),fmt .xtra
494494
495- # %% ../nbs/03_xtras.ipynb 145
495+ # %% ../nbs/03_xtras.ipynb 147
496496def utc2local (dt :datetime )-> datetime :
497497 "Convert `dt` from UTC to local time"
498498 return dt .replace (tzinfo = timezone .utc ).astimezone (tz = None )
499499
500- # %% ../nbs/03_xtras.ipynb 147
500+ # %% ../nbs/03_xtras.ipynb 149
501501def local2utc (dt :datetime )-> datetime :
502502 "Convert `dt` from local to UTC time"
503503 return dt .replace (tzinfo = None ).astimezone (tz = timezone .utc )
504504
505- # %% ../nbs/03_xtras.ipynb 149
505+ # %% ../nbs/03_xtras.ipynb 151
506506def trace (f ):
507507 "Add `set_trace` to an existing function `f`"
508508 from pdb import set_trace
@@ -513,7 +513,7 @@ def _inner(*args,**kwargs):
513513 _inner ._traced = True
514514 return _inner
515515
516- # %% ../nbs/03_xtras.ipynb 151
516+ # %% ../nbs/03_xtras.ipynb 153
517517@contextmanager
518518def modified_env (* delete , ** replace ):
519519 "Context manager temporarily modifying `os.environ` by deleting `delete` and replacing `replace`"
@@ -526,21 +526,21 @@ def modified_env(*delete, **replace):
526526 os .environ .clear ()
527527 os .environ .update (prev )
528528
529- # %% ../nbs/03_xtras.ipynb 153
529+ # %% ../nbs/03_xtras.ipynb 155
530530class ContextManagers (GetAttr ):
531531 "Wrapper for `contextlib.ExitStack` which enters a collection of context managers"
532532 def __init__ (self , mgrs ): self .default ,self .stack = L (mgrs ),ExitStack ()
533533 def __enter__ (self ): self .default .map (self .stack .enter_context )
534534 def __exit__ (self , * args , ** kwargs ): self .stack .__exit__ (* args , ** kwargs )
535535
536- # %% ../nbs/03_xtras.ipynb 155
536+ # %% ../nbs/03_xtras.ipynb 157
537537def shufflish (x , pct = 0.04 ):
538538 "Randomly relocate items of `x` up to `pct` of `len(x)` from their starting location"
539539 n = len (x )
540540 import random
541541 return L (x [i ] for i in sorted (range_of (x ), key = lambda o : o + n * (1 + random .random ()* pct )))
542542
543- # %% ../nbs/03_xtras.ipynb 156
543+ # %% ../nbs/03_xtras.ipynb 158
544544def console_help (
545545 libname :str ): # name of library for console script listing
546546 "Show help for all console scripts from `libname`"
0 commit comments