Coverage for src/lcdoc/call_flows/call_flow_logging.py: 30.14%

Shortcuts 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

366 statements  

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 

16try: lp|features/lp/python/call_flow_logging/index.mdpytest

17 # *IF* we are in a project based on devapps, we can build call flow logs also 

18 # from pytest, not just from docs/lp. Requirements are these imports: 

19 from devapp.tools import FLG, exists, project lp|features/lp/python/call_flow_logging/index.mdpytest

20 from devapp.tools import define_flags, project, to_list 

21 from absl.flags._exceptions import UnparsedFlagAccessError 

22except: lp|features/lp/python/call_flow_logging/index.mdpytest

23 pass lp|features/lp/python/call_flow_logging/index.mdpytest

24# ------------------------------------------------------------------------------ tools 

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

26 

27 

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

29 """ 

30 Call Flow Logger State 

31 

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

33 """ 

34 

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

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

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

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

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

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

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

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

43 # sources = {} 

44 

45 # wrapped = {} 

46 

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

48 ILS.max_trace = 100 

49 ILS.call_chain.clear() 

50 ILS.calls_by_frame.clear() 

51 ILS.counter = 0 

52 ILS.traced.clear() 

53 ILS.parents.clear() 

54 ILS.parents_by_code.clear() 

55 ILS.doc_msgs.clear() 

56 

57 

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

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

60 s = { 

61 'input': None, 

62 'fn_mod': 'note', 

63 'output': None, 

64 'thread_req': thread(), 

65 'name': msg, 

66 't0': now(), 

67 } 

68 ILS.call_chain.append(s) 

69 

70 

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

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

73 f = f.func 

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

75 

76 

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

78 

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

80 

81 

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

83 """ 

84 sys.settrace induced callbacks are passed into this 

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

86 """ 

87 

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

89 if ILS.counter > ILS.max_trace: 

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

91 sys.settrace(None) 

92 return 

93 ILS.counter += 1 

94 

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

96 co = frame.f_code 

97 # frm = SetTrace.get_traced_sender(frame) 

98 

99 # if ILS.counter == 5: 

100 # f = frame.f_back 

101 # while f: 

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

103 # f = f.f_back 

104 

105 t0 = now() 

106 p = ILS.parents_by_code.get(co) 

107 if p: 

108 if len(p) == 1: 

109 n = p[0] 

110 else: 

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

112 else: 

113 n = co.co_name 

114 inp = dumps(frame.f_locals) 

115 # inp = '' 

116 s = { 

117 'thread_req': thread(), 

118 'counter': ILS.counter, 

119 'input': inp, 

120 'frame': frame, 

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

122 'name': n, 

123 'line': frame.f_lineno, 

124 't0': now(), 

125 'dt': -1, 

126 'output': 'n.a.', 

127 'fn_mod': co.co_filename, 

128 } 

129 ILS.call_chain.append(s) 

130 ILS.calls_by_frame[frame] = s 

131 

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

133 try: 

134 s = ILS.calls_by_frame[frame] 

135 except Exception as ex: 

136 return 

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

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

139 s['thread_resp'] = thread() 

140 ILS.call_chain.append(s) 

141 

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

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

144 if not frame.f_code in ILS.traced: 

145 return 

146 if event == 'call': 

147 SetTrace.request(frame) 

148 # getting a callback on traced function exit: 

149 return SetTrace.tracer 

150 elif event == 'return': 

151 SetTrace.response(frame, arg) 

152 

153 

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

155is_func = lambda o: inspect.isfunction(o) or inspect.ismethod(o) lp|features/lp/python/call_flow_logging/index.mdpytestpytest|tests.test_cfl.test_one

156 

157 

158def trace_object( 

159 obj, 

160 pth=None, 

161 containing_mod=None, 

162 filters=( 

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

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

165 ), 

166 blacklist=(), 

167): 

168 """recursive 

169 

170 partials: 

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

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

173 with all args. 

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

175 

176 filters: for keys and keys + values 

177 """ 

178 nil = '\x01' lp|features/lp/python/call_flow_logging/index.mdpytest|tests.test_cfl.test_one

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

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

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

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

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

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

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

186 # properties show up in dir but getattr might fail at this point: 

187 if v == nil: 187 ↛ 188line 187 didn't jump to line 188, because the condition on line 187 was never truelp|features/lp/python/call_flow_logging/index.md

188 continue 

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

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

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

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

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

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

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

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

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

198 return 

199 

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

201 return 

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

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

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

205 # print(obj) 

206 

207 

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

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

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

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

212 # collector = collector or ILS.call_chain.append 

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

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

215 

216 

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

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

219 try: 

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

221 except Exception as ex: 

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

223 return pformat(s) 

224 

225 

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

227 

228 

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

230 if _d_dest != 'auto': 

231 return _d_dest 

232 

233 if not inspect.ismodule(mod): 

234 breakpoint() # FIXME BREAKPOINT 

235 raise 

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

237 

238 r = project.root() 

239 if fn.startswith(r): 

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

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

242 else: 

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

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

245 return _d_dest 

246 

247 

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

249 

250 

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

252 if not unset: 

253 try: 

254 FLG.log_level # is always imported 

255 except UnparsedFlagAccessError: 

256 from devapp.app import init_app_parse_flags 

257 

258 init_app_parse_flags('pytest') 

259 for k in dir(FLG): 

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

261 M = flags 

262 else: 

263 M = flag_defaults 

264 

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

266 

267 

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

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

270 

271 

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

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

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

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

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

277 

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

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

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

281 

282 

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

284 f = { 

285 'plot_mode': 'src', 

286 'plot_before_build': True, 

287 'plot_write_flow_json': 'prebuild', 

288 'plot_after_build': True, 

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

290 } 

291 return f 

292 

293 

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

295 if code: 

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

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

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

299 kw = None 

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

301 

302 

303def document( 

304 trace, 

305 max_trace=100, 

306 fmt='mkdocs', 

307 blacklist=(), 

308 call_seq_closed=True, 

309 flags=None, 

310 dest=None, 

311 wait_before=0, 

312): 

313 """ 

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

315 

316 dest: 

317 - if not a file equal to d_dest below 

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

319 

320 

321 """ 

322 

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

324 if ( 324 ↛ 329line 324 didn't jump to line 329

325 not isinstance(t, type) 

326 and not callable(t) 

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

328 ): 

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

330 

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

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

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

334 # Then we do nothing. 

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

336 # would distract the developer when writing tests: 

337 def decorator(func): 

338 @wraps(func) 

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

340 return func(*args, **kwargs) 

341 

342 return noop_wrapper 

343 

344 return decorator 

345 

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

347 

348 # this is the documentation tracing decorator: 

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

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

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

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

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

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

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

356 else: 

357 raise NotImplementedError( 

358 'derive autodocs dir when no mod was documented' 

359 ) 

360 # _d_dest = autodoc_dir(n_mod, dest) 

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

362 

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

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

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

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

367 [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

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

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

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

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

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

373 set_flags(flg) 

374 ##if wait_before: 

375 # time.sleep(wait_before) 

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

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

378 # set_flags(flags) 

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

380 except SystemExit as ex: 

381 throw = ex 

382 

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

384 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

385 

386 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

387 T = fmts[fmt] 

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

389 while True: 

390 # remove decorators: 

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

392 src.pop(0) 

393 else: 

394 break 

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

396 

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

398 # funcs within: 

399 min_header_level, doc = 4, '' 

400 # n .e.g. test_01_sock_comm 

401 if '.' in n: 

402 min_header_level = 5 

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

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

405 if not section in have: 

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

407 have[section] = True 

408 

409 doc += '\n\n' 

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

411 n_pretty = humanize(n_pretty) 

412 

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

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

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

416 

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

418 _ = to_min_header_level 

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

420 

421 n = use_case.__qualname__ 

422 f = flg.get('plot_destination') 

423 if f: 

424 doc += add_flow_json_and_graph_easy_links(f, T) 

425 

426 _ = T.closed_admon 

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

428 

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

430 if max_trace and trace: 

431 # m = {'fn': fn_chart} 

432 # svg_ids[0] += 1 

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

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

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

436 # v = ( 

437 # ''' 

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

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

440 # </span>''' 

441 # % m 

442 # ) 

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

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

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

446 tr = ', '.join(tr) 

447 

448 _ = call_seq_closed 

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

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

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

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

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

454 

455 # svg = ( 

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

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

458 # .strip() 

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

460 # ) 

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

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

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

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

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

466 

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

468 for d in doc_msgs: 

469 if not d[1]: 

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

471 else: 

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

473 

474 # append our stuffs: 

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

476 if s: 

477 doc = s + '\n\n' + doc 

478 write_file(fn_md_into, doc) 

479 

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

481 if throw: 

482 raise throw 

483 

484 return value 

485 

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

487 

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

489 

490 

491# svg_ids = [0] 

492 

493 

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

495 if not sep in s: 

496 return s 

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

498 while lines: 

499 l = lines.pop(0) 

500 if l.strip() == sep: 

501 add = not add 

502 continue 

503 if not add: 

504 continue 

505 r.append(l) 

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

507 

508 

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

510 

511 

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

513 """ 

514 """ 

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

516 return 

517 

518 if not '/' in test_mod_file: 

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

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

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

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

523 

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

525 return fn_md 

526 

527 

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

529 """convenience function for test modules""" 

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

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

532 return None, lambda: None 

533 fn_md = import_mod(fn_mod) 

534 plot = lambda: plot_build_flow(fn_md) 

535 return fn_md, plot 

536 

537 

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

539 """ 

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

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

542 

543 Now add those into the markdown. 

544 """ 

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

546 d = os.path.dirname(fn) 

547 ext = '.graph_easy.src' 

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

549 

550 def link(f, d=d): 

551 """Process one plot""" 

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

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

554 if s: 

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

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

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

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

559 parts = d.rsplit('/', 2) 

560 test = parts[-2] 

561 dl = parts[-1] 

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

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

564 # then n = 'tests.post_build': 

565 if l: 

566 return l + r 

567 else: 

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

569 

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

571 return '\n'.join(ge) 

572 

573 

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

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

576 

577 

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

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

580 if qn: 

581 return qn 

582 qn = str(qn) 

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

584 

585 

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

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

588 if clear: 

589 sys.settrace(None) 

590 

591 root_ = project.root() 

592 # we log all modules: 

593 mods = {} 

594 # and all func sources: 

595 sources = {} 

596 # to find input and output 

597 have = set() 

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

599 _ = write.arrange_frame_and_write_infos 

600 flow_w = [ 

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

602 for call in ILS.call_chain 

603 ] 

604 _ = make_call_flow_chart 

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

606 # post write, clear for next use_case: 

607 ILS.clear() if clear else 0 

608 return fn_chart 

609 

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

611 """Creates 

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

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

614 (for the flow chart) 

615 

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

617 """ 

618 l = [call] 

619 frame = call.get('frame') 

620 if frame in have: 

621 # the output one, all other infos added already 

622 have.add(frame) 

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

624 return l 

625 have.add(frame) 

626 fn = call['fn_mod'] 

627 if fn == 'note': 

628 return [call, 'note', None] 

629 d_mod = mods.get(fn) 

630 if not d_mod: 

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

632 os.makedirs(d_dest, exist_ok=True) 

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

634 d = d_mod[0] 

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

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

637 func = sources.get(frame) 

638 if not func: 

639 src = inspect.getsource(frame) 

640 src = deindent(src) 

641 func_name = frame.f_code.co_name 

642 if 'lambda' in func_name: 

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

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

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

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

647 fn_func, func_name = func 

648 d = d_usecase(d_dest, use_case) 

649 

650 m = { 

651 'line': call['line'], 

652 'fn_func': fn_func, 

653 'fn_mod': d_mod[0], 

654 'dt': call['dt'], 

655 'name': func_name, 

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

657 } 

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

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

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

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

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

663 return l 

664 

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

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

667 pth = d.split('__') 

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