Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1import os, json, sys, time lp|features/lp/python/call_flow_logging/index.mdpytest

2from lcdoc.tools import exists, project, write_file, read_file, to_list lp|features/lp/python/call_flow_logging/index.mdpytest

3import inspect, shutil lp|features/lp/python/call_flow_logging/index.mdpytest

4from functools import partial, wraps lp|features/lp/python/call_flow_logging/index.mdpytest

5from lcdoc.call_flows.call_flow_charting import make_call_flow_chart lp|features/lp/python/call_flow_logging/index.mdpytest

6 

7from lcdoc.call_flows.markdown import Mkdocs, deindent, to_min_header_level lp|features/lp/python/call_flow_logging/index.mdpytest

8 

9from pprint import pformat lp|features/lp/python/call_flow_logging/index.mdpytest

10 

11from inflection import humanize lp|features/lp/python/call_flow_logging/index.mdpytest

12 

13from importlib import import_module lp|features/lp/python/call_flow_logging/index.mdpytest

14from .auto_docs import mod_doc lp|features/lp/python/call_flow_logging/index.mdpytest

15 

16 

17# ------------------------------------------------------------------------------ tools 

18now = time.time lp|features/lp/python/call_flow_logging/index.mdpytest

19 

20 

21class ILS: lp|features/lp/python/call_flow_logging/index.mdpytest

22 """ 

23 Call Flow Logger State 

24 

25 Normally populated/cleared at start and end of a pytest. 

26 """ 

27 

28 traced = set() # all traced code objects lp|features/lp/python/call_flow_logging/index.mdpytest

29 max_trace = 100 lp|features/lp/python/call_flow_logging/index.mdpytest

30 call_chain = _ = [] lp|features/lp/python/call_flow_logging/index.mdpytest

31 counter = 0 lp|features/lp/python/call_flow_logging/index.mdpytest

32 calls_by_frame = {} lp|features/lp/python/call_flow_logging/index.mdpytest

33 parents = {} lp|features/lp/python/call_flow_logging/index.mdpytest

34 parents_by_code = {} lp|features/lp/python/call_flow_logging/index.mdpytest

35 doc_msgs = [] lp|features/lp/python/call_flow_logging/index.mdpytest

36 # sources = {} 

37 

38 # wrapped = {} 

39 

40 def clear(): lp|features/lp/python/call_flow_logging/index.mdpytest

41 ILS.max_trace = 100 

42 ILS.call_chain.clear() 

43 ILS.calls_by_frame.clear() 

44 ILS.counter = 0 

45 ILS.traced.clear() 

46 ILS.parents.clear() 

47 ILS.parents_by_code.clear() 

48 ILS.doc_msgs.clear() 

49 

50 

51def call_flow_note(msg, **kw): lp|features/lp/python/call_flow_logging/index.mdpytest

52 msg = json.dumps([msg, kw], indent=2) if kw else msg 

53 s = { 

54 'input': None, 

55 'fn_mod': 'note', 

56 'output': None, 

57 'thread_req': thread(), 

58 'name': msg, 

59 't0': now(), 

60 } 

61 ILS.call_chain.append(s) 

62 

63 

64def unwrap_partial(f): lp|features/lp/python/call_flow_logging/index.mdpytest

65 while hasattr(f, 'func'): 65 ↛ 66line 65 didn't jump to line 66, because the condition on line 65 was never truelp|features/lp/python/call_flow_logging/index.mdpytest|tests.test_cfl.test_one

66 f = f.func 

67 return f lp|features/lp/python/call_flow_logging/index.mdpytest|tests.test_cfl.test_one

68 

69 

70from threading import current_thread lp|features/lp/python/call_flow_logging/index.mdpytest

71 

72thread = lambda: current_thread().name.replace('ummy-', '').replace('hread-', '') 72 ↛ exitline 72 didn't run the lambda on line 72lp|features/lp/python/call_flow_logging/index.mdpytest

73 

74 

75class SetTrace: lp|features/lp/python/call_flow_logging/index.mdpytest

76 """ 

77 sys.settrace induced callbacks are passed into this 

78 (when the called function is watched, i.e. added to ILS.traced) 

79 """ 

80 

81 def request(frame): lp|features/lp/python/call_flow_logging/index.mdpytest

82 if ILS.counter > ILS.max_trace: 

83 call_flow_note('Reached max traced calls count', max_trace=ILS.max_trace) 

84 sys.settrace(None) 

85 return 

86 ILS.counter += 1 

87 

88 # need to do (and screw time) this while tracing 

89 co = frame.f_code 

90 # frm = SetTrace.get_traced_sender(frame) 

91 

92 # if ILS.counter == 5: 

93 # f = frame.f_back 

94 # while f: 

95 # print('sender', f, inp) 

96 # f = f.f_back 

97 

98 t0 = now() 

99 p = ILS.parents_by_code.get(co) 

100 if p: 

101 if len(p) == 1: 

102 n = p[0] 

103 else: 

104 n = '%s (%s)' % (p[-1], '.'.join(p[:-1])) 

105 else: 

106 n = co.co_name 

107 inp = dumps(frame.f_locals) 

108 # inp = '' 

109 s = { 

110 'thread_req': thread(), 

111 'counter': ILS.counter, 

112 'input': inp, 

113 'frame': frame, 

114 'parents': ILS.parents_by_code.get(co), 

115 'name': n, 

116 'line': frame.f_lineno, 

117 't0': now(), 

118 'dt': -1, 

119 'output': 'n.a.', 

120 'fn_mod': co.co_filename, 

121 } 

122 ILS.call_chain.append(s) 

123 ILS.calls_by_frame[frame] = s 

124 

125 def response(frame, arg): lp|features/lp/python/call_flow_logging/index.mdpytest

126 try: 

127 s = ILS.calls_by_frame[frame] 

128 except Exception as ex: 

129 return 

130 s['dt'] = now() - s['t0'] 

131 s['output'] = dumps(arg) 

132 s['thread_resp'] = thread() 

133 ILS.call_chain.append(s) 

134 

135 def tracer(frame, event, arg, counter=0): lp|features/lp/python/call_flow_logging/index.mdpytest

136 """The settrace function. You can't pdb here!""" 

137 if not frame.f_code in ILS.traced: 

138 return 

139 if event == 'call': 

140 SetTrace.request(frame) 

141 # getting a callback on traced function exit: 

142 return SetTrace.tracer 

143 elif event == 'return': 

144 SetTrace.response(frame, arg) 

145 

146 

147is_parent = lambda o: inspect.isclass(o) or inspect.ismodule(o) lp|features/lp/python/call_flow_logging/index.mdpytestpytest|tests.test_cfl.test_one

148is_func = inspect.isfunction or inspect.ismethod lp|features/lp/python/call_flow_logging/index.mdpytest

149 

150 

151def trace_object( 

152 obj, 

153 pth=None, 

154 containing_mod=None, 

155 filters=( 

156 lambda k: not k.startswith('_'), 

157 lambda k, v: inspect.isclass(v) or is_func(v), 

158 ), 

159 blacklist=(), 

160): 

161 """recursive 

162 

163 partials: 

164 For now we ignore them, treat them like attributes. 

165 The functions they wrap, when contained by a traced object will be documented, 

166 with all args. 

167 In order to document partials we would have to wrap them into new functions, set into the parent. 

168 

169 filters: for keys and keys + values 

170 """ 

171 if is_parent(obj): lp|features/lp/python/call_flow_logging/index.mdpytest|tests.test_cfl.test_one

172 if not pth: lp|features/lp/python/call_flow_logging/index.md

173 pth = (obj.__name__,) lp|features/lp/python/call_flow_logging/index.md

174 ILS.parents[pth] = obj lp|features/lp/python/call_flow_logging/index.md

175 containing_mod = pth[0] if inspect.ismodule(obj) else obj.__module__ lp|features/lp/python/call_flow_logging/index.md

176 for k in filter(filters[0], dir(obj)): lp|features/lp/python/call_flow_logging/index.md

177 v = getattr(obj, k) lp|features/lp/python/call_flow_logging/index.md

178 if filters[1](k, v): lp|features/lp/python/call_flow_logging/index.md

179 pth1 = pth + (k,) lp|features/lp/python/call_flow_logging/index.md

180 ILS.parents[pth1] = v lp|features/lp/python/call_flow_logging/index.md

181 trace_object(v, pth1, containing_mod, filters, blacklist) lp|features/lp/python/call_flow_logging/index.md

182 elif is_func(obj): 182 ↛ exitline 182 didn't return from function 'trace_object', because the condition on line 182 was never falselp|features/lp/python/call_flow_logging/index.mdpytest|tests.test_cfl.test_one

183 spth = str(pth) + str(obj) lp|features/lp/python/call_flow_logging/index.mdpytest|tests.test_cfl.test_one

184 if any([b for b in blacklist if b in spth]): 184 ↛ 186line 184 didn't jump to line 186, because the condition on line 184 was never truelp|features/lp/python/call_flow_logging/index.mdpytest|tests.test_cfl.test_one

185 # TODO: Just return in settrace and write the info into the spec of the call: 

186 print('blacklisted for tracing', str(obj)) 

187 return 

188 

189 if containing_mod and not obj.__module__.startswith(containing_mod): 189 ↛ 190line 189 didn't jump to line 190, because the condition on line 189 was never truelp|features/lp/python/call_flow_logging/index.mdpytest|tests.test_cfl.test_one

190 return 

191 co = obj.__code__ lp|features/lp/python/call_flow_logging/index.mdpytest|tests.test_cfl.test_one

192 ILS.parents_by_code[co] = pth lp|features/lp/python/call_flow_logging/index.mdpytest|tests.test_cfl.test_one

193 ILS.traced.add(co) lp|features/lp/python/call_flow_logging/index.mdpytest|tests.test_cfl.test_one

194 # print(obj) 

195 

196 

197def trace_func(traced_func, settrace=True): lp|features/lp/python/call_flow_logging/index.mdpytest

198 """trace a function w/o context""" 

199 func = unwrap_partial(traced_func) lp|features/lp/python/call_flow_logging/index.mdpytest|tests.test_cfl.test_one

200 ILS.traced.add(func.__code__) lp|features/lp/python/call_flow_logging/index.mdpytest|tests.test_cfl.test_one

201 # collector = collector or ILS.call_chain.append 

202 if settrace: 202 ↛ exitline 202 didn't return from function 'trace_func', because the condition on line 202 was never falselp|features/lp/python/call_flow_logging/index.mdpytest|tests.test_cfl.test_one

203 sys.settrace(SetTrace.tracer) # partial(tracer, collector=collector)) lp|features/lp/python/call_flow_logging/index.mdpytest|tests.test_cfl.test_one

204 

205 

206# we already dumps formatted, so that the js does not need to parse/stringify: 

207def dumps(s): lp|features/lp/python/call_flow_logging/index.mdpytest

208 try: 

209 return json.dumps(s, default=str, indent=4, sort_keys=True) 

210 except Exception as ex: 

211 # sorting sometimes not works (e.g. 2 classes as keys) 

212 return pformat(s) 

213 

214 

215fmts = {'mkdocs': Mkdocs} lp|features/lp/python/call_flow_logging/index.mdpytest

216 

217 

218def autodoc_dir(mod, _d_dest): lp|features/lp/python/call_flow_logging/index.mdpytest

219 if _d_dest != 'auto': 

220 return _d_dest 

221 

222 if not inspect.ismodule(mod): 

223 breakpoint() # FIXME BREAKPOINT 

224 raise 

225 modn, fn = mod.__name__, mod.__file__ 

226 

227 r = project.root() 

228 if fn.startswith(r): 

229 # fn '/home/gk/repos/lc-python/tests/operators/test_build.py' -> tests/operators: 

230 d_mod = fn.rsplit(r, 1)[1][1:].rsplit('/', 1)[0] 

231 else: 

232 d_mod = modn.replace('.', '/') 

233 _d_dest = project.root() + '/build/autodocs/' + d_mod 

234 return _d_dest 

235 

236 

237flag_defaults = {} lp|features/lp/python/call_flow_logging/index.mdpytest

238 

239 

240def set_flags(flags, unset=False): lp|features/lp/python/call_flow_logging/index.mdpytest

241 if not unset: 

242 try: 

243 FLG.log_level # is always imported 

244 except UnparsedFlagAccessError: 

245 from devapp.app import init_app_parse_flags 

246 

247 init_app_parse_flags('pytest') 

248 for k in dir(FLG): 

249 flag_defaults[k] = getattr(FLG, k) 

250 M = flags 

251 else: 

252 M = flag_defaults 

253 

254 return [setattr(FLG, k, v) for k, v in M.items()] 

255 

256 

257import os lp|features/lp/python/call_flow_logging/index.mdpytest

258from functools import partial lp|features/lp/python/call_flow_logging/index.mdpytest

259 

260 

261def pytest_plot_dest(dest): lp|features/lp/python/call_flow_logging/index.mdpytest

262 cur_test = lambda: os.environ['PYTEST_CURRENT_TEST'] 

263 # if os.path.isfile(dest): 

264 fn_t = cur_test().split('.py::', 1)[1].replace('::', '.').replace(' (call)', '') 

265 dest = dest.rsplit('.', 1)[0] + '/' + fn_t 

266 

267 # fn_t = cur_test().rsplit('/', 1)[-1].replace(' (call)', '') 

268 # '%s/%s._plot_tag_.graph_easy.src' % (dest, fn_t) 

269 return dest + '/_plot_tag_.graph_easy.src' 

270 

271 

272def plot_build_flow(dest): lp|features/lp/python/call_flow_logging/index.mdpytest

273 f = { 

274 'plot_mode': 'src', 

275 'plot_before_build': True, 

276 'plot_write_flow_json': 'prebuild', 

277 'plot_after_build': True, 

278 'plot_destination': partial(pytest_plot_dest, dest=dest), 

279 } 

280 return f 

281 

282 

283def add_doc_msg(msg, code=None, **kw): lp|features/lp/python/call_flow_logging/index.mdpytest

284 if code: 

285 T = fmts[kw.pop('fmt', 'mkdocs')] 

286 c = T.code(kw.pop('lang', 'js'), code) 

287 msg = (T.closed_admon if kw.pop('closed', 0) else T.admon)(msg, c, 'info') 

288 kw = None 

289 ILS.doc_msgs.append([msg, kw]) 

290 

291 

292def document( 

293 trace, 

294 max_trace=100, 

295 fmt='mkdocs', 

296 blacklist=(), 

297 call_seq_closed=True, 

298 flags=None, 

299 dest=None, 

300): 

301 """ 

302 Decorating call flow triggering functions (usually pytest functions) with this 

303 

304 dest: 

305 - if not a file equal to d_dest below 

306 - if file: d_dest is dir named file w/o ext. e.g. /foo/bar.md -> /foo/bar/ 

307 

308 

309 """ 

310 

311 def check_tracable(t): lp|features/lp/python/call_flow_logging/index.mdpytest|tests.test_cfl.test_one

312 if ( 312 ↛ 317line 312 didn't jump to line 317

313 not isinstance(t, type) 

314 and not callable(t) 

315 and not getattr(type(t), '__name__') == 'module' 

316 ): 

317 raise Exception('Can only trace modules, classes and callables, got: %s' % t) 

318 

319 [check_tracable(t) for t in to_list(trace)] lp|features/lp/python/call_flow_logging/index.mdpytest|tests.test_cfl.test_one

320 if not dest: 320 ↛ 325line 320 didn't jump to line 325, because the condition on line 320 was never truelp|features/lp/python/call_flow_logging/index.mdpytest|tests.test_cfl.test_one

321 # dest is empty if env var make_autodocs is not set. 

322 # Then we do nothing. 

323 # noop if env var not set, we don't want to trace at every pytest run, that 

324 # would distract the developer when writing tests: 

325 def decorator(func): 

326 @wraps(func) 

327 def noop_wrapper(*args, **kwargs): 

328 return func(*args, **kwargs) 

329 

330 return noop_wrapper 

331 

332 return decorator 

333 

334 ILS.max_trace = max_trace # limit traced calls lp|features/lp/python/call_flow_logging/index.mdpytest|tests.test_cfl.test_one

335 

336 # this is the documentation tracing decorator: 

337 def use_case_deco(use_case, trace=trace): lp|features/lp/python/call_flow_logging/index.mdpytest|tests.test_cfl.test_one

338 def use_case_(*args, _dest=dest, _fmt=fmt, **kwargs): lp|features/lp/python/call_flow_logging/index.mdpytest|tests.test_cfl.test_one

339 n_mod = use_case.__module__ lp|features/lp/python/call_flow_logging/index.mdpytest|tests.test_cfl.test_one

340 if os.path.isfile(_dest): 340 ↛ 345line 340 didn't jump to line 345, because the condition on line 340 was never falselp|features/lp/python/call_flow_logging/index.mdpytest|tests.test_cfl.test_one

341 fn_md_into = _dest lp|features/lp/python/call_flow_logging/index.mdpytest|tests.test_cfl.test_one

342 _d_dest = _dest.rsplit('.', 1)[0] lp|features/lp/python/call_flow_logging/index.mdpytest|tests.test_cfl.test_one

343 os.makedirs(_d_dest, exist_ok=True) lp|features/lp/python/call_flow_logging/index.mdpytest|tests.test_cfl.test_one

344 else: 

345 raise NotImplementedError( 

346 'derive autodocs dir when no mod was documented' 

347 ) 

348 # _d_dest = autodoc_dir(n_mod, dest) 

349 # fn_md_into = (d_usecase(_d_dest, use_case) + '.md',) 

350 

351 n = n_func(use_case) lp|features/lp/python/call_flow_logging/index.mdpytest|tests.test_cfl.test_one

352 fn_chart = n lp|features/lp/python/call_flow_logging/index.mdpytest|tests.test_cfl.test_one

353 blackl = to_list(blacklist) lp|features/lp/python/call_flow_logging/index.mdpytest|tests.test_cfl.test_one

354 if max_trace and trace: 354 ↛ 357line 354 didn't jump to line 357, because the condition on line 354 was never falselp|features/lp/python/call_flow_logging/index.mdpytest|tests.test_cfl.test_one

355 [trace_object(t, blacklist=blackl) for t in to_list(trace)] lp|features/lp/python/call_flow_logging/index.mdpytest|tests.test_cfl.test_one

356 trace_func(use_case) lp|features/lp/python/call_flow_logging/index.mdpytest|tests.test_cfl.test_one

357 flg = {} lp|features/lp/python/call_flow_logging/index.mdpytest|tests.test_cfl.test_one

358 if flags: 358 ↛ 359line 358 didn't jump to line 359, because the condition on line 358 was never truelp|features/lp/python/call_flow_logging/index.mdpytest|tests.test_cfl.test_one

359 for k, v in flags.items(): 

360 flg[k] = v() if callable(v) else v 

361 set_flags(flg) 

362 try: lp|features/lp/python/call_flow_logging/index.mdpytest|tests.test_cfl.test_one

363 throw = None lp|features/lp/python/call_flow_logging/index.mdpytest|tests.test_cfl.test_one

364 # set_flags(flags) 

365 value = use_case(*args, **kwargs) # <-------- the actual original call lp|features/lp/python/call_flow_logging/index.mdpytest|tests.test_cfl.test_one

366 except SystemExit as ex: 

367 throw = ex 

368 

369 set_flags(flags, unset=True) lp|features/lp/python/call_flow_logging/index.mdpytest|tests.test_cfl.test_one

370 doc_msgs = list(ILS.doc_msgs) # will be cleared in next call: lp|features/lp/python/call_flow_logging/index.mdpytest|tests.test_cfl.test_one

371 

372 write.flow_chart(_d_dest, use_case, clear=True, with_chart=fn_chart) lp|features/lp/python/call_flow_logging/index.mdpytest|tests.test_cfl.test_one

373 T = fmts[fmt] 

374 src = deindent(inspect.getsource(use_case)).splitlines() 

375 while True: 

376 # remove decorators: 

377 if src[0].startswith('@'): 

378 src.pop(0) 

379 else: 

380 break 

381 src = '\n'.join(src) 

382 

383 # if the test is within a class we write the class as header and add the 

384 # funcs within: 

385 min_header_level, doc = 4, '' 

386 # n .e.g. test_01_sock_comm 

387 if '.' in n: 

388 min_header_level = 5 

389 section, n = n.rsplit('.', 1) 

390 have = written_sect_headers.setdefault(n_mod, {}) 

391 if not section in have: 

392 doc += '\n\n#### ' + section 

393 have[section] = True 

394 

395 doc += '\n\n' 

396 n_pretty = n.split('_', 1)[1] if n.startswith('test') else n 

397 n_pretty = humanize(n_pretty) 

398 

399 # set a jump mark for the ops reference page (doc pp): 

400 _ = pytest_plot_dest(_dest).split('/autodocs/', 1)[1].rsplit('/', 1)[0] 

401 doc += '\n<span id="%s"></span>\n' % _ 

402 

403 ud = deindent(strip_no_ad(use_case.__doc__ or '')) 

404 _ = to_min_header_level 

405 doc += _(min_header_level, deindent('\n\n#### %s\n%s' % (n_pretty, ud))) 

406 

407 n = use_case.__qualname__ 

408 f = flg.get('plot_destination') 

409 if f: 

410 doc += add_flow_json_and_graph_easy_links(f, T) 

411 

412 _ = T.closed_admon 

413 doc += _(n + ' source code', T.code('python', strip_no_ad(src))) 

414 

415 # call flow plots only when we did trace anythning: 

416 if max_trace and trace: 

417 # m = {'fn': fn_chart} 

418 # svg_ids[0] += 1 

419 # m['id'] = svg_ids[0] 

420 # v = '[![](./%(fn)s.svg)](?src=%(fn)s&sequence_details=true)' % m 

421 # v = '![](./%(fn)s.svg)' % m # ](?src=%(fn)s&sequence_details=true)' % m 

422 # v = ( 

423 # ''' 

424 # <span class="std_svg" id="std_svg_%(id)s"> 

425 # <img src="../%(fn)s.svg"></img> 

426 # </span>''' 

427 # % m 

428 # ) 

429 #![](./%(fn)s.svg)' % m # ](?src=%(fn)s&sequence_details=true)' % m 

430 tr = [name(t) for t in to_list(trace)] 

431 tr = [t for t in tr if t] 

432 tr = ', '.join(tr) 

433 

434 _ = call_seq_closed 

435 adm = T.closed_admon if _ else T.clsabl_admon 

436 # they are rendered and inserted at doc pre_process, i.e. later: 

437 fn = '%s/%s/call_flow.svg' % (fn_md_into.rsplit('.', 1)[0], fn_chart) 

438 fn = fn.rsplit('/autodocs/', 1)[1] 

439 svg_placeholder = '[svg_placeholder:%s]' % fn 

440 

441 # svg = ( 

442 # read_file('%s/%s.svg' % (os.path.dirname(fn_md_into), fn_chart)) 

443 # .split('>', 1)[1] 

444 # .strip() 

445 # .split('<!--MD5', 1)[0] 

446 # ) 

447 # if not svg.endswith('</svg>'): 

448 # svg += '</g></svg>' 

449 # id = '<svg id="%s" class="call_flow_chart" ' % fn_chart 

450 # svg = svg.replace('<svg ', id) 

451 doc += adm('Call Sequence `%s` (%s)' % (n, tr), svg_placeholder, 'info') 

452 

453 # had doc_msgs been produced during running the test fuction? Then append: 

454 for d in doc_msgs: 

455 if not d[1]: 

456 doc += '\n\n%s\n\n' % d[0] 

457 else: 

458 doc += '\n%s%s' % (d[0], T.code('js', json.dumps(d[1], indent=4))) 

459 

460 # append our stuffs: 

461 s = read_file(fn_md_into, dflt='') 

462 if s: 

463 doc = s + '\n\n' + doc 

464 write_file(fn_md_into, doc) 

465 

466 # had the function been raising? Then throw it now: 

467 if throw: 

468 raise throw 

469 

470 return value 

471 

472 return use_case_ lp|features/lp/python/call_flow_logging/index.mdpytest|tests.test_cfl.test_one

473 

474 return use_case_deco lp|features/lp/python/call_flow_logging/index.mdpytest|tests.test_cfl.test_one

475 

476 

477# svg_ids = [0] 

478 

479 

480def strip_no_ad(s, sep='# /--'): lp|features/lp/python/call_flow_logging/index.mdpytest

481 if not sep in s: 

482 return s 

483 r, add, lines = [], True, s.splitlines() 

484 while lines: 

485 l = lines.pop(0) 

486 if l.strip() == sep: 

487 add = not add 

488 continue 

489 if not add: 

490 continue 

491 r.append(l) 

492 return ('\n'.join(r)).strip() 

493 

494 

495written_sect_headers = {} lp|features/lp/python/call_flow_logging/index.mdpytest

496 

497 

498def import_mod(test_mod_file): lp|features/lp/python/call_flow_logging/index.mdpytest

499 """ 

500 """ 

501 if not 'pytest' in sys.argv[0]: 

502 return 

503 

504 if not '/' in test_mod_file: 

505 test_mod_file = os.getcwd() + '/' + test_mod_file 

506 d, fn = test_mod_file.rsplit('/', 1) 

507 sys.path.insert(0, d) if not d in sys.path else 0 

508 mod = import_module(fn.rsplit('.py', 1)[0]) 

509 

510 fn_md = mod_doc(mod, dest='auto') 

511 return fn_md 

512 

513 

514def init_mod_doc(fn_mod): lp|features/lp/python/call_flow_logging/index.mdpytest

515 """convenience function for test modules""" 

516 # cannot be done via FLG, need to parse too early: 

517 if not os.environ.get('make_autodocs'): 

518 return None, lambda: None 

519 fn_md = import_mod(fn_mod) 

520 plot = lambda: plot_build_flow(fn_md) 

521 return fn_md, plot 

522 

523 

524def add_flow_json_and_graph_easy_links(fn, T): lp|features/lp/python/call_flow_logging/index.mdpytest

525 """ 

526 The flags had been causing operators.build to create .graph_easy files before and after build 

527 and also flow.json files for the before phase. 

528 

529 Now add those into the markdown. 

530 """ 

531 # fn like '/home/gk/repos/lc-python/build/autodocs/tests/operators/test_op_ax_socket/test01_sock_comm/_plot_tag_.graph_easy.src' 

532 d = os.path.dirname(fn) 

533 ext = '.graph_easy.src' 

534 # pre = fn.rsplit('/')[-1].split('_plot_tag_', 1)[0] 

535 

536 def link(f, d=d): 

537 """Process one plot""" 

538 fn, l = d + '/' + f + '.flow.json', '' 

539 s = read_file(fn, dflt='') 

540 if s: 

541 p = '\n\n> copy & paste into Node-RED\n\n' 

542 l = T.closed_admon('Flow Json', p + T.code('js', s), 'info') 

543 # os.unlink(fn) # will be analysed by ops refs doc page 

544 s = f.replace(ext, '') 

545 dl = d.rsplit('/', 1)[-1] 

546 r = '\n\n![](./%s/%s.svg)\n\n' % (dl, s) 

547 # when s = 'test_build.py::Sharing::test_share_deep_copy.tests.post_build.svg' 

548 # then n = 'tests.post_build': 

549 if l: 

550 return l + r 

551 else: 

552 return T.closed_admon(s, r, 'note') 

553 

554 ge = [link(f) for f in sorted(os.listdir(d)) if f.endswith(ext)] 

555 return '\n'.join(ge) 

556 

557 

558n_func = lambda func: func.__qualname__.replace('.<locals>', '') lp|features/lp/python/call_flow_logging/index.mdpytestpytest|tests.test_cfl.test_one

559d_usecase = lambda d_dest, use_case: d_dest + '/' + n_func(use_case) 559 ↛ exitline 559 didn't run the lambda on line 559lp|features/lp/python/call_flow_logging/index.mdpytest

560 

561 

562def name(obj): lp|features/lp/python/call_flow_logging/index.mdpytest

563 qn = getattr(obj, '__name__', None) 

564 if qn: 

565 return qn 

566 qn = str(qn) 

567 return obj.replace('<', '&lt;').replace('>', '&gt;') 

568 

569 

570class write: lp|features/lp/python/call_flow_logging/index.mdpytest

571 def flow_chart(d_dest, use_case, clear=True, with_chart=False): lp|features/lp/python/call_flow_logging/index.mdpytest

572 if clear: 

573 sys.settrace(None) 

574 

575 root_ = project.root() 

576 # we log all modules: 

577 mods = {} 

578 # and all func sources: 

579 sources = {} 

580 # to find input and output 

581 have = set() 

582 os.makedirs(d_usecase(d_dest, use_case), exist_ok=True) 

583 _ = write.arrange_frame_and_write_infos 

584 flow_w = [ 

585 _(call, use_case, mods, sources, root_, d_dest, have) 

586 for call in ILS.call_chain 

587 ] 

588 _ = make_call_flow_chart 

589 fn_chart = _(flow_w, d_dest, fn=with_chart, ILS=ILS) if with_chart else 0 

590 # post write, clear for next use_case: 

591 ILS.clear() if clear else 0 

592 return fn_chart 

593 

594 def arrange_frame_and_write_infos(call, use_case, mods, sources, root, d_dest, have): lp|features/lp/python/call_flow_logging/index.mdpytest

595 """Creates 

596 [<callspec>, <input>, None] if input 

597 [<callspec>, None, <output>, None] if output 

598 (for the flow chart) 

599 

600 and writes module and func/linenr files plus the data jsons 

601 """ 

602 l = [call] 

603 frame = call.get('frame') 

604 if frame in have: 

605 # the output one, all other infos added already 

606 have.add(frame) 

607 l.extend([None, call.pop('output', '-')]) 

608 return l 

609 have.add(frame) 

610 fn = call['fn_mod'] 

611 if fn == 'note': 

612 return [call, 'note', None] 

613 d_mod = mods.get(fn) 

614 if not d_mod: 

615 d_mod = mods[fn] = write.module_filename_relative_to_root(fn, root) 

616 os.makedirs(d_dest, exist_ok=True) 

617 shutil.copyfile(fn, d_dest + '/' + d_mod[0]) 

618 d = d_mod[0] 

619 call['pth'] = d_mod[1] 

620 call['fn_mod'] = d.rsplit('/', 1)[-1] 

621 func = sources.get(frame) 

622 if not func: 

623 src = inspect.getsource(frame) 

624 src = deindent(src) 

625 func_name = frame.f_code.co_name 

626 if 'lambda' in func_name: 

627 func_name = src.split(' = ', 1)[0].strip() 

628 fn_func = '%s.%s.func.py' % (d, call['line']) 

629 write_file(d_dest + '/' + fn_func, src) 

630 sources[frame] = func = (fn_func, func_name) 

631 fn_func, func_name = func 

632 d = d_usecase(d_dest, use_case) 

633 

634 m = { 

635 'line': call['line'], 

636 'fn_func': fn_func, 

637 'fn_mod': d_mod[0], 

638 'dt': call['dt'], 

639 'name': func_name, 

640 'mod': '.'.join(d_mod[1]), 

641 } 

642 spec = [json.dumps(m), call['input'], call['output']] 

643 fn = call['fn'] = '%s/%s.json' % (d, call['counter']) 

644 write_file(fn, '\n-\n'.join(spec)) 

645 # s = json.dumps(call, default=str) 

646 l.extend([call.pop('input'), None]) 

647 return l 

648 

649 def module_filename_relative_to_root(fn, root): lp|features/lp/python/call_flow_logging/index.mdpytest

650 d = (fn.split(root, 1)[-1]).replace('/', '__').rsplit('.py', 1)[0][2:] 

651 pth = d.split('__') 

652 return (d + '.mod.py'), pth