jpayne@68
|
1 """
|
jpayne@68
|
2 Module version for monitoring CLI pipes (`... | python -m tqdm | ...`).
|
jpayne@68
|
3 """
|
jpayne@68
|
4 import logging
|
jpayne@68
|
5 import re
|
jpayne@68
|
6 import sys
|
jpayne@68
|
7 from ast import literal_eval as numeric
|
jpayne@68
|
8 from textwrap import indent
|
jpayne@68
|
9
|
jpayne@68
|
10 from .std import TqdmKeyError, TqdmTypeError, tqdm
|
jpayne@68
|
11 from .version import __version__
|
jpayne@68
|
12
|
jpayne@68
|
13 __all__ = ["main"]
|
jpayne@68
|
14 log = logging.getLogger(__name__)
|
jpayne@68
|
15
|
jpayne@68
|
16
|
jpayne@68
|
17 def cast(val, typ):
|
jpayne@68
|
18 log.debug((val, typ))
|
jpayne@68
|
19 if " or " in typ:
|
jpayne@68
|
20 for t in typ.split(" or "):
|
jpayne@68
|
21 try:
|
jpayne@68
|
22 return cast(val, t)
|
jpayne@68
|
23 except TqdmTypeError:
|
jpayne@68
|
24 pass
|
jpayne@68
|
25 raise TqdmTypeError(f"{val} : {typ}")
|
jpayne@68
|
26
|
jpayne@68
|
27 # sys.stderr.write('\ndebug | `val:type`: `' + val + ':' + typ + '`.\n')
|
jpayne@68
|
28 if typ == 'bool':
|
jpayne@68
|
29 if (val == 'True') or (val == ''):
|
jpayne@68
|
30 return True
|
jpayne@68
|
31 if val == 'False':
|
jpayne@68
|
32 return False
|
jpayne@68
|
33 raise TqdmTypeError(val + ' : ' + typ)
|
jpayne@68
|
34 if typ == 'chr':
|
jpayne@68
|
35 if len(val) == 1:
|
jpayne@68
|
36 return val.encode()
|
jpayne@68
|
37 if re.match(r"^\\\w+$", val):
|
jpayne@68
|
38 return eval(f'"{val}"').encode()
|
jpayne@68
|
39 raise TqdmTypeError(f"{val} : {typ}")
|
jpayne@68
|
40 if typ == 'str':
|
jpayne@68
|
41 return val
|
jpayne@68
|
42 if typ == 'int':
|
jpayne@68
|
43 try:
|
jpayne@68
|
44 return int(val)
|
jpayne@68
|
45 except ValueError as exc:
|
jpayne@68
|
46 raise TqdmTypeError(f"{val} : {typ}") from exc
|
jpayne@68
|
47 if typ == 'float':
|
jpayne@68
|
48 try:
|
jpayne@68
|
49 return float(val)
|
jpayne@68
|
50 except ValueError as exc:
|
jpayne@68
|
51 raise TqdmTypeError(f"{val} : {typ}") from exc
|
jpayne@68
|
52 raise TqdmTypeError(f"{val} : {typ}")
|
jpayne@68
|
53
|
jpayne@68
|
54
|
jpayne@68
|
55 def posix_pipe(fin, fout, delim=b'\\n', buf_size=256,
|
jpayne@68
|
56 callback=lambda float: None, callback_len=True):
|
jpayne@68
|
57 """
|
jpayne@68
|
58 Params
|
jpayne@68
|
59 ------
|
jpayne@68
|
60 fin : binary file with `read(buf_size : int)` method
|
jpayne@68
|
61 fout : binary file with `write` (and optionally `flush`) methods.
|
jpayne@68
|
62 callback : function(float), e.g.: `tqdm.update`
|
jpayne@68
|
63 callback_len : If (default: True) do `callback(len(buffer))`.
|
jpayne@68
|
64 Otherwise, do `callback(data) for data in buffer.split(delim)`.
|
jpayne@68
|
65 """
|
jpayne@68
|
66 fp_write = fout.write
|
jpayne@68
|
67
|
jpayne@68
|
68 if not delim:
|
jpayne@68
|
69 while True:
|
jpayne@68
|
70 tmp = fin.read(buf_size)
|
jpayne@68
|
71
|
jpayne@68
|
72 # flush at EOF
|
jpayne@68
|
73 if not tmp:
|
jpayne@68
|
74 getattr(fout, 'flush', lambda: None)()
|
jpayne@68
|
75 return
|
jpayne@68
|
76
|
jpayne@68
|
77 fp_write(tmp)
|
jpayne@68
|
78 callback(len(tmp))
|
jpayne@68
|
79 # return
|
jpayne@68
|
80
|
jpayne@68
|
81 buf = b''
|
jpayne@68
|
82 len_delim = len(delim)
|
jpayne@68
|
83 # n = 0
|
jpayne@68
|
84 while True:
|
jpayne@68
|
85 tmp = fin.read(buf_size)
|
jpayne@68
|
86
|
jpayne@68
|
87 # flush at EOF
|
jpayne@68
|
88 if not tmp:
|
jpayne@68
|
89 if buf:
|
jpayne@68
|
90 fp_write(buf)
|
jpayne@68
|
91 if callback_len:
|
jpayne@68
|
92 # n += 1 + buf.count(delim)
|
jpayne@68
|
93 callback(1 + buf.count(delim))
|
jpayne@68
|
94 else:
|
jpayne@68
|
95 for i in buf.split(delim):
|
jpayne@68
|
96 callback(i)
|
jpayne@68
|
97 getattr(fout, 'flush', lambda: None)()
|
jpayne@68
|
98 return # n
|
jpayne@68
|
99
|
jpayne@68
|
100 while True:
|
jpayne@68
|
101 i = tmp.find(delim)
|
jpayne@68
|
102 if i < 0:
|
jpayne@68
|
103 buf += tmp
|
jpayne@68
|
104 break
|
jpayne@68
|
105 fp_write(buf + tmp[:i + len(delim)])
|
jpayne@68
|
106 # n += 1
|
jpayne@68
|
107 callback(1 if callback_len else (buf + tmp[:i]))
|
jpayne@68
|
108 buf = b''
|
jpayne@68
|
109 tmp = tmp[i + len_delim:]
|
jpayne@68
|
110
|
jpayne@68
|
111
|
jpayne@68
|
112 # ((opt, type), ... )
|
jpayne@68
|
113 RE_OPTS = re.compile(r'\n {4}(\S+)\s{2,}:\s*([^,]+)')
|
jpayne@68
|
114 # better split method assuming no positional args
|
jpayne@68
|
115 RE_SHLEX = re.compile(r'\s*(?<!\S)--?([^\s=]+)(\s+|=|$)')
|
jpayne@68
|
116
|
jpayne@68
|
117 # TODO: add custom support for some of the following?
|
jpayne@68
|
118 UNSUPPORTED_OPTS = ('iterable', 'gui', 'out', 'file')
|
jpayne@68
|
119
|
jpayne@68
|
120 # The 8 leading spaces are required for consistency
|
jpayne@68
|
121 CLI_EXTRA_DOC = r"""
|
jpayne@68
|
122 Extra CLI Options
|
jpayne@68
|
123 -----------------
|
jpayne@68
|
124 name : type, optional
|
jpayne@68
|
125 TODO: find out why this is needed.
|
jpayne@68
|
126 delim : chr, optional
|
jpayne@68
|
127 Delimiting character [default: '\n']. Use '\0' for null.
|
jpayne@68
|
128 N.B.: on Windows systems, Python converts '\n' to '\r\n'.
|
jpayne@68
|
129 buf_size : int, optional
|
jpayne@68
|
130 String buffer size in bytes [default: 256]
|
jpayne@68
|
131 used when `delim` is specified.
|
jpayne@68
|
132 bytes : bool, optional
|
jpayne@68
|
133 If true, will count bytes, ignore `delim`, and default
|
jpayne@68
|
134 `unit_scale` to True, `unit_divisor` to 1024, and `unit` to 'B'.
|
jpayne@68
|
135 tee : bool, optional
|
jpayne@68
|
136 If true, passes `stdin` to both `stderr` and `stdout`.
|
jpayne@68
|
137 update : bool, optional
|
jpayne@68
|
138 If true, will treat input as newly elapsed iterations,
|
jpayne@68
|
139 i.e. numbers to pass to `update()`. Note that this is slow
|
jpayne@68
|
140 (~2e5 it/s) since every input must be decoded as a number.
|
jpayne@68
|
141 update_to : bool, optional
|
jpayne@68
|
142 If true, will treat input as total elapsed iterations,
|
jpayne@68
|
143 i.e. numbers to assign to `self.n`. Note that this is slow
|
jpayne@68
|
144 (~2e5 it/s) since every input must be decoded as a number.
|
jpayne@68
|
145 null : bool, optional
|
jpayne@68
|
146 If true, will discard input (no stdout).
|
jpayne@68
|
147 manpath : str, optional
|
jpayne@68
|
148 Directory in which to install tqdm man pages.
|
jpayne@68
|
149 comppath : str, optional
|
jpayne@68
|
150 Directory in which to place tqdm completion.
|
jpayne@68
|
151 log : str, optional
|
jpayne@68
|
152 CRITICAL|FATAL|ERROR|WARN(ING)|[default: 'INFO']|DEBUG|NOTSET.
|
jpayne@68
|
153 """
|
jpayne@68
|
154
|
jpayne@68
|
155
|
jpayne@68
|
156 def main(fp=sys.stderr, argv=None):
|
jpayne@68
|
157 """
|
jpayne@68
|
158 Parameters (internal use only)
|
jpayne@68
|
159 ---------
|
jpayne@68
|
160 fp : file-like object for tqdm
|
jpayne@68
|
161 argv : list (default: sys.argv[1:])
|
jpayne@68
|
162 """
|
jpayne@68
|
163 if argv is None:
|
jpayne@68
|
164 argv = sys.argv[1:]
|
jpayne@68
|
165 try:
|
jpayne@68
|
166 log_idx = argv.index('--log')
|
jpayne@68
|
167 except ValueError:
|
jpayne@68
|
168 for i in argv:
|
jpayne@68
|
169 if i.startswith('--log='):
|
jpayne@68
|
170 logLevel = i[len('--log='):]
|
jpayne@68
|
171 break
|
jpayne@68
|
172 else:
|
jpayne@68
|
173 logLevel = 'INFO'
|
jpayne@68
|
174 else:
|
jpayne@68
|
175 # argv.pop(log_idx)
|
jpayne@68
|
176 # logLevel = argv.pop(log_idx)
|
jpayne@68
|
177 logLevel = argv[log_idx + 1]
|
jpayne@68
|
178 logging.basicConfig(level=getattr(logging, logLevel),
|
jpayne@68
|
179 format="%(levelname)s:%(module)s:%(lineno)d:%(message)s")
|
jpayne@68
|
180
|
jpayne@68
|
181 # py<3.13 doesn't dedent docstrings
|
jpayne@68
|
182 d = (tqdm.__doc__ if sys.version_info < (3, 13)
|
jpayne@68
|
183 else indent(tqdm.__doc__, " ")) + CLI_EXTRA_DOC
|
jpayne@68
|
184
|
jpayne@68
|
185 opt_types = dict(RE_OPTS.findall(d))
|
jpayne@68
|
186 # opt_types['delim'] = 'chr'
|
jpayne@68
|
187
|
jpayne@68
|
188 for o in UNSUPPORTED_OPTS:
|
jpayne@68
|
189 opt_types.pop(o)
|
jpayne@68
|
190
|
jpayne@68
|
191 log.debug(sorted(opt_types.items()))
|
jpayne@68
|
192
|
jpayne@68
|
193 # d = RE_OPTS.sub(r' --\1=<\1> : \2', d)
|
jpayne@68
|
194 split = RE_OPTS.split(d)
|
jpayne@68
|
195 opt_types_desc = zip(split[1::3], split[2::3], split[3::3])
|
jpayne@68
|
196 d = ''.join(('\n --{0} : {2}{3}' if otd[1] == 'bool' else
|
jpayne@68
|
197 '\n --{0}=<{1}> : {2}{3}').format(
|
jpayne@68
|
198 otd[0].replace('_', '-'), otd[0], *otd[1:])
|
jpayne@68
|
199 for otd in opt_types_desc if otd[0] not in UNSUPPORTED_OPTS)
|
jpayne@68
|
200
|
jpayne@68
|
201 help_short = "Usage:\n tqdm [--help | options]\n"
|
jpayne@68
|
202 d = help_short + """
|
jpayne@68
|
203 Options:
|
jpayne@68
|
204 -h, --help Print this help and exit.
|
jpayne@68
|
205 -v, --version Print version and exit.
|
jpayne@68
|
206 """ + d.strip('\n') + '\n'
|
jpayne@68
|
207
|
jpayne@68
|
208 # opts = docopt(d, version=__version__)
|
jpayne@68
|
209 if any(v in argv for v in ('-v', '--version')):
|
jpayne@68
|
210 sys.stdout.write(__version__ + '\n')
|
jpayne@68
|
211 sys.exit(0)
|
jpayne@68
|
212 elif any(v in argv for v in ('-h', '--help')):
|
jpayne@68
|
213 sys.stdout.write(d + '\n')
|
jpayne@68
|
214 sys.exit(0)
|
jpayne@68
|
215 elif argv and argv[0][:2] != '--':
|
jpayne@68
|
216 sys.stderr.write(f"Error:Unknown argument:{argv[0]}\n{help_short}")
|
jpayne@68
|
217
|
jpayne@68
|
218 argv = RE_SHLEX.split(' '.join(["tqdm"] + argv))
|
jpayne@68
|
219 opts = dict(zip(argv[1::3], argv[3::3]))
|
jpayne@68
|
220
|
jpayne@68
|
221 log.debug(opts)
|
jpayne@68
|
222 opts.pop('log', True)
|
jpayne@68
|
223
|
jpayne@68
|
224 tqdm_args = {'file': fp}
|
jpayne@68
|
225 try:
|
jpayne@68
|
226 for (o, v) in opts.items():
|
jpayne@68
|
227 o = o.replace('-', '_')
|
jpayne@68
|
228 try:
|
jpayne@68
|
229 tqdm_args[o] = cast(v, opt_types[o])
|
jpayne@68
|
230 except KeyError as e:
|
jpayne@68
|
231 raise TqdmKeyError(str(e))
|
jpayne@68
|
232 log.debug('args:' + str(tqdm_args))
|
jpayne@68
|
233
|
jpayne@68
|
234 delim_per_char = tqdm_args.pop('bytes', False)
|
jpayne@68
|
235 update = tqdm_args.pop('update', False)
|
jpayne@68
|
236 update_to = tqdm_args.pop('update_to', False)
|
jpayne@68
|
237 if sum((delim_per_char, update, update_to)) > 1:
|
jpayne@68
|
238 raise TqdmKeyError("Can only have one of --bytes --update --update_to")
|
jpayne@68
|
239 except Exception:
|
jpayne@68
|
240 fp.write("\nError:\n" + help_short)
|
jpayne@68
|
241 stdin, stdout_write = sys.stdin, sys.stdout.write
|
jpayne@68
|
242 for i in stdin:
|
jpayne@68
|
243 stdout_write(i)
|
jpayne@68
|
244 raise
|
jpayne@68
|
245 else:
|
jpayne@68
|
246 buf_size = tqdm_args.pop('buf_size', 256)
|
jpayne@68
|
247 delim = tqdm_args.pop('delim', b'\\n')
|
jpayne@68
|
248 tee = tqdm_args.pop('tee', False)
|
jpayne@68
|
249 manpath = tqdm_args.pop('manpath', None)
|
jpayne@68
|
250 comppath = tqdm_args.pop('comppath', None)
|
jpayne@68
|
251 if tqdm_args.pop('null', False):
|
jpayne@68
|
252 class stdout(object):
|
jpayne@68
|
253 @staticmethod
|
jpayne@68
|
254 def write(_):
|
jpayne@68
|
255 pass
|
jpayne@68
|
256 else:
|
jpayne@68
|
257 stdout = sys.stdout
|
jpayne@68
|
258 stdout = getattr(stdout, 'buffer', stdout)
|
jpayne@68
|
259 stdin = getattr(sys.stdin, 'buffer', sys.stdin)
|
jpayne@68
|
260 if manpath or comppath:
|
jpayne@68
|
261 try: # py<3.9
|
jpayne@68
|
262 import importlib_resources as resources
|
jpayne@68
|
263 except ImportError:
|
jpayne@68
|
264 from importlib import resources
|
jpayne@68
|
265 from pathlib import Path
|
jpayne@68
|
266
|
jpayne@68
|
267 def cp(name, dst):
|
jpayne@68
|
268 """copy resource `name` to `dst`"""
|
jpayne@68
|
269 fi = resources.files('tqdm') / name
|
jpayne@68
|
270 dst.write_bytes(fi.read_bytes())
|
jpayne@68
|
271 log.info("written:%s", dst)
|
jpayne@68
|
272 if manpath is not None:
|
jpayne@68
|
273 cp('tqdm.1', Path(manpath) / 'tqdm.1')
|
jpayne@68
|
274 if comppath is not None:
|
jpayne@68
|
275 cp('completion.sh', Path(comppath) / 'tqdm_completion.sh')
|
jpayne@68
|
276 sys.exit(0)
|
jpayne@68
|
277 if tee:
|
jpayne@68
|
278 stdout_write = stdout.write
|
jpayne@68
|
279 fp_write = getattr(fp, 'buffer', fp).write
|
jpayne@68
|
280
|
jpayne@68
|
281 class stdout(object): # pylint: disable=function-redefined
|
jpayne@68
|
282 @staticmethod
|
jpayne@68
|
283 def write(x):
|
jpayne@68
|
284 with tqdm.external_write_mode(file=fp):
|
jpayne@68
|
285 fp_write(x)
|
jpayne@68
|
286 stdout_write(x)
|
jpayne@68
|
287 if delim_per_char:
|
jpayne@68
|
288 tqdm_args.setdefault('unit', 'B')
|
jpayne@68
|
289 tqdm_args.setdefault('unit_scale', True)
|
jpayne@68
|
290 tqdm_args.setdefault('unit_divisor', 1024)
|
jpayne@68
|
291 log.debug(tqdm_args)
|
jpayne@68
|
292 with tqdm(**tqdm_args) as t:
|
jpayne@68
|
293 posix_pipe(stdin, stdout, '', buf_size, t.update)
|
jpayne@68
|
294 elif delim == b'\\n':
|
jpayne@68
|
295 log.debug(tqdm_args)
|
jpayne@68
|
296 write = stdout.write
|
jpayne@68
|
297 if update or update_to:
|
jpayne@68
|
298 with tqdm(**tqdm_args) as t:
|
jpayne@68
|
299 if update:
|
jpayne@68
|
300 def callback(i):
|
jpayne@68
|
301 t.update(numeric(i.decode()))
|
jpayne@68
|
302 else: # update_to
|
jpayne@68
|
303 def callback(i):
|
jpayne@68
|
304 t.update(numeric(i.decode()) - t.n)
|
jpayne@68
|
305 for i in stdin:
|
jpayne@68
|
306 write(i)
|
jpayne@68
|
307 callback(i)
|
jpayne@68
|
308 else:
|
jpayne@68
|
309 for i in tqdm(stdin, **tqdm_args):
|
jpayne@68
|
310 write(i)
|
jpayne@68
|
311 else:
|
jpayne@68
|
312 log.debug(tqdm_args)
|
jpayne@68
|
313 with tqdm(**tqdm_args) as t:
|
jpayne@68
|
314 callback_len = False
|
jpayne@68
|
315 if update:
|
jpayne@68
|
316 def callback(i):
|
jpayne@68
|
317 t.update(numeric(i.decode()))
|
jpayne@68
|
318 elif update_to:
|
jpayne@68
|
319 def callback(i):
|
jpayne@68
|
320 t.update(numeric(i.decode()) - t.n)
|
jpayne@68
|
321 else:
|
jpayne@68
|
322 callback = t.update
|
jpayne@68
|
323 callback_len = True
|
jpayne@68
|
324 posix_pipe(stdin, stdout, delim, buf_size, callback, callback_len)
|