Coverage for C:\Repos\leo-editor\leo\commands\checkerCommands.py: 17%

327 statements  

« prev     ^ index     » next       coverage.py v6.4, created at 2022-05-24 10:21 -0500

1# -*- coding: utf-8 -*- 

2#@+leo-ver=5-thin 

3#@+node:ekr.20161021090740.1: * @file ../commands/checkerCommands.py 

4#@@first 

5"""Commands that invoke external checkers""" 

6#@+<< imports >> 

7#@+node:ekr.20161021092038.1: ** << imports >> checkerCommands.py 

8import os 

9import shlex 

10import sys 

11import time 

12# 

13# Third-party imports. 

14# pylint: disable=import-error 

15try: 

16 from mypy import api as mypy_api 

17except Exception: 

18 mypy_api = None 

19try: 

20 import flake8 

21 # #2248: Import only flake8. 

22except ImportError: 

23 flake8 = None # type:ignore 

24try: 

25 import mypy 

26except Exception: 

27 mypy = None # type:ignore 

28try: 

29 import pyflakes 

30 from pyflakes import api, reporter 

31except Exception: 

32 pyflakes = None # type:ignore 

33try: 

34 # pylint: disable=import-error 

35 from pylint import lint 

36except Exception: 

37 lint = None # type:ignore 

38# 

39# Leo imports. 

40from leo.core import leoGlobals as g 

41#@-<< imports >> 

42#@+others 

43#@+node:ekr.20161021091557.1: ** Commands 

44#@+node:ekr.20190608084751.1: *3* find-long-lines 

45@g.command('find-long-lines') 

46def find_long_lines(event): 

47 """Report long lines in the log, with clickable links.""" 

48 c = event and event.get('c') 

49 if not c: 

50 return 

51 #@+others # helper functions 

52 #@+node:ekr.20190609135639.1: *4* function: get_root 

53 def get_root(p): 

54 """Return True if p is any @<file> node.""" 

55 for parent in p.self_and_parents(): 

56 if parent.anyAtFileNodeName(): 

57 return parent 

58 return None 

59 #@+node:ekr.20190608084751.2: *4* function: in_no_pylint 

60 def in_nopylint(p): 

61 """Return p if p is controlled by @nopylint.""" 

62 for parent in p.self_and_parents(): 

63 if '@nopylint' in parent.h: 

64 return True 

65 return False 

66 #@-others 

67 log = c.frame.log 

68 max_line = c.config.getInt('max-find-long-lines-length') or 110 

69 count, files, ignore = 0, [], [] 

70 for p in c.all_unique_positions(): 

71 if in_nopylint(p): 

72 continue 

73 root = get_root(p) 

74 if not root: 

75 continue 

76 if root.v not in files: 

77 files.append(root.v) 

78 for i, s in enumerate(g.splitLines(p.b)): 

79 if len(s) > max_line: 

80 if not root: 

81 if p.v not in ignore: 

82 ignore.append(p.v) 

83 g.es_print('no root', p.h) 

84 else: 

85 count += 1 

86 short_s = g.truncate(s, 30) 

87 g.es('') 

88 g.es_print(root.h) 

89 g.es_print(p.h) 

90 print(short_s) 

91 unl = p.get_UNL() 

92 log.put(short_s.strip() + '\n', nodeLink=f"{unl}::{i + 1}") # Local line. 

93 break 

94 g.es_print( 

95 f"found {count} long line{g.plural(count)} " 

96 f"longer than {max_line} characters in " 

97 f"{len(files)} file{g.plural(len(files))}") 

98#@+node:ekr.20190615180048.1: *3* find-missing-docstrings 

99@g.command('find-missing-docstrings') 

100def find_missing_docstrings(event): 

101 """Report missing docstrings in the log, with clickable links.""" 

102 c = event and event.get('c') 

103 if not c: 

104 return 

105 #@+others # Define functions 

106 #@+node:ekr.20190615181104.1: *4* function: has_docstring 

107 def has_docstring(lines, n): 

108 """ 

109 Returns True if function/method/class whose definition 

110 starts on n-th line in lines has a docstring 

111 """ 

112 # By Виталије Милошевић. 

113 for line in lines[n:]: 

114 s = line.strip() 

115 if not s or s.startswith('#'): 

116 continue 

117 if s.startswith(('"""', "'''")): 

118 return True 

119 return False 

120 #@+node:ekr.20190615181104.2: *4* function: is_a_definition 

121 def is_a_definition(line): 

122 """Return True if line is a definition line.""" 

123 # By Виталије Милошевић. 

124 # It may be useful to skip __init__ methods because their docstring 

125 # is usually docstring of the class 

126 return ( 

127 line.startswith(('def ', 'class ')) and 

128 not line.partition(' ')[2].startswith('__init__') 

129 ) 

130 #@+node:ekr.20190615182754.1: *4* function: is_root 

131 def is_root(p): 

132 """ 

133 A predicate returning True if p is an @<file> node that is not under @nopylint. 

134 """ 

135 for parent in p.self_and_parents(): 

136 if g.match_word(parent.h, 0, '@nopylint'): 

137 return False 

138 return p.isAnyAtFileNode() and p.h.strip().endswith('.py') 

139 #@-others 

140 log = c.frame.log 

141 count, files, found, t1 = 0, 0, [], time.process_time() 

142 for root in g.findRootsWithPredicate(c, c.p, predicate=is_root): 

143 files += 1 

144 for p in root.self_and_subtree(): 

145 lines = p.b.split('\n') 

146 for i, line in enumerate(lines): 

147 if is_a_definition(line) and not has_docstring(lines, i): 

148 count += 1 

149 if root.v not in found: 

150 found.append(root.v) 

151 g.es_print('') 

152 g.es_print(root.h) 

153 print(line) 

154 unl = p.get_UNL() 

155 log.put(line.strip() + '\n', nodeLink=f"{unl}::{i + 1}") # Local line. 

156 break 

157 g.es_print('') 

158 g.es_print( 

159 f"found {count} missing docstring{g.plural(count)} " 

160 f"in {files} file{g.plural(files)} " 

161 f"in {time.process_time() - t1:5.2f} sec.") 

162#@+node:ekr.20160517133001.1: *3* flake8-files command 

163@g.command('flake8-files') 

164def flake8_command(event): 

165 """ 

166 Run flake8 on all nodes of the selected tree, 

167 or the first @<file> node in an ancestor. 

168 """ 

169 tag = 'flake8-files' 

170 if not flake8: 

171 g.es_print(f"{tag} can not import flake8") 

172 return 

173 c = event and event.get('c') 

174 if not c or not c.p: 

175 return 

176 python = sys.executable 

177 for root in g.findRootsWithPredicate(c, c.p): 

178 path = g.fullPath(c, root) 

179 if path and os.path.exists(path): 

180 g.es_print(f"{tag}: {path}") 

181 g.execute_shell_commands(f'&"{python}" -m flake8 "{path}"') 

182 else: 

183 g.es_print(f"{tag}: file not found:{path}") 

184#@+node:ekr.20161026092059.1: *3* kill-pylint 

185@g.command('kill-pylint') 

186@g.command('pylint-kill') 

187def kill_pylint(event): 

188 """Kill any running pylint processes and clear the queue.""" 

189 g.app.backgroundProcessManager.kill('pylint') 

190#@+node:ekr.20210302111730.1: *3* mypy command 

191@g.command('mypy') 

192def mypy_command(event): 

193 """ 

194 Run mypy on all @<file> nodes of the selected tree, or the first 

195 @<file> node in an ancestor. Running mypy on a single file usually 

196 suffices. 

197 

198 For example, in LeoPyRef.leo, you can run mypy on most of Leo's files 

199 by running this command with the following node selected: 

200 

201 `@edit ../../launchLeo.py` 

202 

203 Unlike running mypy outside of Leo, Leo's mypy command creates 

204 clickable links in Leo's log pane for each error. 

205 

206 Settings 

207 -------- 

208 

209 @data mypy-arguments 

210 @int mypy-link-limit = 0 

211 @string mypy-config-file='' 

212 

213 See leoSettings.leo for details. 

214 """ 

215 c = event and event.get('c') 

216 if not c: 

217 return 

218 if c.isChanged(): 

219 c.save() 

220 if mypy_api: 

221 MypyCommand(c).run(c.p) 

222 else: 

223 g.es_print('can not import mypy') 

224#@+node:ekr.20160516072613.1: *3* pyflakes command 

225@g.command('pyflakes') 

226def pyflakes_command(event): 

227 """ 

228 Run pyflakes on all nodes of the selected tree, 

229 or the first @<file> node in an ancestor. 

230 """ 

231 c = event and event.get('c') 

232 if not c: 

233 return 

234 if c.isChanged(): 

235 c.save() 

236 if not pyflakes: 

237 g.es_print('can not import pyflakes') 

238 return 

239 ok = PyflakesCommand(c).run(c.p) 

240 if ok: 

241 g.es('OK: pyflakes') 

242#@+node:ekr.20150514125218.7: *3* pylint command 

243last_pylint_path = None 

244 

245@g.command('pylint') 

246def pylint_command(event): 

247 """ 

248 Run pylint on all nodes of the selected tree, 

249 or the first @<file> node in an ancestor, 

250 or the last checked @<file> node. 

251 """ 

252 global last_pylint_path 

253 c = event and event.get('c') 

254 if c: 

255 if c.isChanged(): 

256 c.save() 

257 data = PylintCommand(c).run(last_path=last_pylint_path) 

258 if data: 

259 path, p = data 

260 last_pylint_path = path 

261#@+node:ekr.20210302111917.1: ** class MypyCommand 

262class MypyCommand: 

263 """A class to run mypy on all Python @<file> nodes in c.p's tree.""" 

264 

265 def __init__(self, c): 

266 """ctor for MypyCommand class.""" 

267 self.c = c 

268 self.link_limit = None # Set in check_file. 

269 self.unknown_path_names = [] 

270 

271 #@+others 

272 #@+node:ekr.20210302111935.3: *3* mypy.check_all 

273 def check_all(self, roots): 

274 """Run mypy on all files in paths.""" 

275 c = self.c 

276 if not mypy: 

277 print('install mypy with `pip install mypy`') 

278 return 

279 self.unknown_path_names = [] 

280 for root in roots: 

281 fn = os.path.normpath(g.fullPath(c, root)) 

282 self.check_file(fn, root) 

283 

284 

285 #@+node:ekr.20210727212625.1: *3* mypy.check_file 

286 def check_file(self, fn, root): 

287 """Run mypy on one file.""" 

288 c = self.c 

289 if not mypy: 

290 print('install mypy with `pip install mypy`') 

291 return 

292 command = f"{sys.executable} -m mypy {fn}" 

293 bpm = g.app.backgroundProcessManager 

294 bpm.start_process(c, command, 

295 fn=fn, 

296 kind='mypy', 

297 link_pattern=g.mypy_pat, 

298 link_root=root, 

299 ) 

300 

301 #@+node:ekr.20210302111935.7: *3* mypy.run (entry) 

302 def run(self, p): 

303 """Run mypy on all Python @<file> nodes in c.p's tree.""" 

304 c = self.c 

305 if not mypy: 

306 print('install mypy with `pip install mypy`') 

307 return 

308 root = p.copy() 

309 # Make sure Leo is on sys.path. 

310 leo_path = g.os_path_finalize_join(g.app.loadDir, '..') 

311 if leo_path not in sys.path: 

312 sys.path.append(leo_path) 

313 roots = g.findRootsWithPredicate(c, root, predicate=None) 

314 g.printObj([z.h for z in roots], tag='mypy.run') 

315 self.check_all(roots) 

316 #@-others 

317#@+node:ekr.20160516072613.2: ** class PyflakesCommand 

318class PyflakesCommand: 

319 """A class to run pyflakes on all Python @<file> nodes in c.p's tree.""" 

320 

321 def __init__(self, c): 

322 """ctor for PyflakesCommand class.""" 

323 self.c = c 

324 self.seen = [] # List of checked paths. 

325 

326 #@+others 

327 #@+node:ekr.20171228013818.1: *3* class PyflakesCommand.LogStream 

328 class LogStream: 

329 

330 """A log stream for pyflakes.""" 

331 

332 def __init__(self, fn_n=0, roots=None): 

333 self.fn_n = fn_n 

334 self.roots = roots 

335 

336 def write(self, s): 

337 fn_n, roots = self.fn_n, self.roots 

338 if not s.strip(): 

339 return 

340 g.pr(s) 

341 # It *is* useful to send pyflakes errors to the console. 

342 if roots: 

343 try: 

344 root = roots[fn_n] 

345 line = int(s.split(':')[1]) 

346 unl = root.get_UNL() 

347 g.es(s, nodeLink=f"{unl}::{(-line)}") # Global line 

348 except(IndexError, TypeError, ValueError): 

349 # in case any assumptions fail 

350 g.es(s) 

351 else: 

352 g.es(s) 

353 #@+node:ekr.20160516072613.6: *3* pyflakes.check_all 

354 def check_all(self, roots): 

355 """Run pyflakes on all files in paths.""" 

356 total_errors = 0 

357 for i, root in enumerate(roots): 

358 fn = self.finalize(root) 

359 sfn = g.shortFileName(fn) 

360 # #1306: nopyflakes 

361 if any(z.strip().startswith('@nopyflakes') for z in g.splitLines(root.b)): 

362 continue 

363 # Report the file name. 

364 s = g.readFileIntoEncodedString(fn) 

365 if s and s.strip(): 

366 # Send all output to the log pane. 

367 r = reporter.Reporter( 

368 errorStream=self.LogStream(i, roots), 

369 warningStream=self.LogStream(i, roots), 

370 ) 

371 errors = api.check(s, sfn, r) 

372 total_errors += errors 

373 return total_errors 

374 #@+node:ekr.20171228013625.1: *3* pyflakes.check_script 

375 def check_script(self, p, script): 

376 """Call pyflakes to check the given script.""" 

377 try: 

378 from pyflakes import api, reporter 

379 except Exception: # ModuleNotFoundError 

380 return True # Pretend all is fine. 

381 # #1306: nopyflakes 

382 lines = g.splitLines(p.b) 

383 for line in lines: 

384 if line.strip().startswith('@nopyflakes'): 

385 return True 

386 r = reporter.Reporter( 

387 errorStream=self.LogStream(), 

388 warningStream=self.LogStream(), 

389 ) 

390 errors = api.check(script, '', r) 

391 return errors == 0 

392 #@+node:ekr.20170220114553.1: *3* pyflakes.finalize 

393 def finalize(self, p): 

394 """Finalize p's path.""" 

395 c = self.c 

396 # Use os.path.normpath to give system separators. 

397 return os.path.normpath(g.fullPath(c, p)) # #1914. 

398 #@+node:ekr.20160516072613.5: *3* pyflakes.run 

399 def run(self, p): 

400 """Run Pyflakes on all Python @<file> nodes in p's tree.""" 

401 ok = True 

402 if not pyflakes: 

403 return ok 

404 c = self.c 

405 root = p 

406 # Make sure Leo is on sys.path. 

407 leo_path = g.os_path_finalize_join(g.app.loadDir, '..') 

408 if leo_path not in sys.path: 

409 sys.path.append(leo_path) 

410 roots = g.findRootsWithPredicate(c, root, predicate=None) 

411 if roots: 

412 # These messages are important for clarity. 

413 total_errors = self.check_all(roots) 

414 if total_errors > 0: 

415 g.es(f"ERROR: pyflakes: {total_errors} error{g.plural(total_errors)}") 

416 ok = total_errors == 0 

417 else: 

418 ok = True 

419 return ok 

420 #@-others 

421#@+node:ekr.20150514125218.8: ** class PylintCommand 

422class PylintCommand: 

423 """A class to run pylint on all Python @<file> nodes in c.p's tree.""" 

424 

425 def __init__(self, c): 

426 self.c = c 

427 self.data = None # Data for the *running* process. 

428 self.rc_fn = None # Name of the rc file. 

429 #@+others 

430 #@+node:ekr.20150514125218.11: *3* 1. pylint.run 

431 def run(self, last_path=None): 

432 """Run Pylint on all Python @<file> nodes in c.p's tree.""" 

433 c, root = self.c, self.c.p 

434 if not lint: 

435 g.es_print('pylint is not installed') 

436 return False 

437 self.rc_fn = self.get_rc_file() 

438 if not self.rc_fn: 

439 return False 

440 # Make sure Leo is on sys.path. 

441 leo_path = g.os_path_finalize_join(g.app.loadDir, '..') 

442 if leo_path not in sys.path: 

443 sys.path.append(leo_path) 

444 

445 # Ignore @nopylint trees. 

446 

447 def predicate(p): 

448 for parent in p.self_and_parents(): 

449 if g.match_word(parent.h, 0, '@nopylint'): 

450 return False 

451 return p.isAnyAtFileNode() and p.h.strip().endswith(('.py', '.pyw')) # #2354. 

452 

453 roots = g.findRootsWithPredicate(c, root, predicate=predicate) 

454 data = [(self.get_fn(p), p.copy()) for p in roots] 

455 data = [z for z in data if z[0] is not None] 

456 if not data and last_path: 

457 # Default to the last path. 

458 fn = last_path 

459 for p in c.all_positions(): 

460 if p.isAnyAtFileNode() and g.fullPath(c, p) == fn: 

461 data = [(fn, p.copy())] 

462 break 

463 if not data: 

464 g.es('pylint: no files found', color='red') 

465 return None 

466 for fn, p in data: 

467 self.run_pylint(fn, p) 

468 # #1808: return the last data file. 

469 return data[-1] if data else False 

470 #@+node:ekr.20150514125218.10: *3* 3. pylint.get_rc_file 

471 def get_rc_file(self): 

472 """Return the path to the pylint configuration file.""" 

473 base = 'pylint-leo-rc.txt' 

474 table = ( 

475 # In ~/.leo 

476 g.os_path_finalize_join(g.app.homeDir, '.leo', base), 

477 # In leo/test 

478 g.os_path_finalize_join(g.app.loadDir, '..', '..', 'leo', 'test', base), 

479 ) 

480 for fn in table: 

481 fn = g.os_path_abspath(fn) 

482 if g.os_path_exists(fn): 

483 return fn 

484 table_s = '\n'.join(table) 

485 g.es_print(f"no pylint configuration file found in\n{table_s}") 

486 return None 

487 #@+node:ekr.20150514125218.9: *3* 4. pylint.get_fn 

488 def get_fn(self, p): 

489 """ 

490 Finalize p's file name. 

491 Return if p is not an @file node for a python file. 

492 """ 

493 c = self.c 

494 fn = p.isAnyAtFileNode() 

495 if not fn: 

496 g.trace(f"not an @<file> node: {p.h!r}") 

497 return None 

498 return g.fullPath(c, p) # #1914 

499 #@+node:ekr.20150514125218.12: *3* 5. pylint.run_pylint 

500 def run_pylint(self, fn, p): 

501 """Run pylint on fn with the given pylint configuration file.""" 

502 c, rc_fn = self.c, self.rc_fn 

503 # 

504 # Invoke pylint directly. 

505 is_win = sys.platform.startswith('win') 

506 args = ','.join([f"'--rcfile={rc_fn}'", f"'{fn}'"]) 

507 if is_win: 

508 args = args.replace('\\', '\\\\') 

509 command = ( 

510 f'{sys.executable} -c "from pylint import lint; args=[{args}]; lint.Run(args)"') 

511 if not is_win: 

512 command = shlex.split(command) # type:ignore 

513 # 

514 # Run the command using the BPM. 

515 bpm = g.app.backgroundProcessManager 

516 bpm.start_process(c, command, 

517 fn=fn, 

518 kind='pylint', 

519 link_pattern=g.pylint_pat, 

520 link_root=p, 

521 ) 

522 #@-others 

523#@-others 

524#@@language python 

525#@@tabwidth -4 

526#@@pagewidth 70 

527 

528#@-leo