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

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

372 statements  

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 re 

10import shlex 

11import sys 

12import time 

13# 

14# Third-party imports. 

15# pylint: disable=import-error 

16try: 

17 from mypy import api as mypy_api 

18except Exception: 

19 mypy_api = None 

20try: 

21 import flake8 

22 # #2248: Import only flake8. 

23except ImportError: 

24 flake8 = None # type:ignore 

25try: 

26 import mypy 

27except Exception: 

28 mypy = None # type:ignore 

29try: 

30 import pyflakes 

31 from pyflakes import api, reporter 

32except Exception: 

33 pyflakes = None # type:ignore 

34try: 

35 # pylint: disable=import-error 

36 from pylint import lint 

37except Exception: 

38 lint = None # type:ignore 

39# 

40# Leo imports. 

41from leo.core import leoGlobals as g 

42#@-<< imports >> 

43#@+others 

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

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

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

47def find_long_lines(event): 

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

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

50 if not c: 

51 return 

52 #@+others # helper functions 

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

54 def get_root(p): 

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

56 for parent in p.self_and_parents(): 

57 if parent.anyAtFileNodeName(): 

58 return parent 

59 return None 

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

61 def in_nopylint(p): 

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

63 for parent in p.self_and_parents(): 

64 if '@nopylint' in parent.h: 

65 return True 

66 return False 

67 #@-others 

68 log = c.frame.log 

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

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

71 for p in c.all_unique_positions(): 

72 if in_nopylint(p): 

73 continue 

74 root = get_root(p) 

75 if not root: 

76 continue 

77 if root.v not in files: 

78 files.append(root.v) 

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

80 if len(s) > max_line: 

81 if not root: 

82 if p.v not in ignore: 

83 ignore.append(p.v) 

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

85 else: 

86 count += 1 

87 short_s = g.truncate(s, 30) 

88 g.es('') 

89 g.es_print(root.h) 

90 g.es_print(p.h) 

91 print(short_s) 

92 unl = p.get_UNL() 

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

94 break 

95 g.es_print( 

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

97 f"longer than {max_line} characters in " 

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

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

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

101def find_missing_docstrings(event): 

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

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

104 if not c: 

105 return 

106 #@+others # Define functions 

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

108 def has_docstring(lines, n): 

109 """ 

110 Returns True if function/method/class whose definition 

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

112 """ 

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

114 for line in lines[n:]: 

115 s = line.strip() 

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

117 continue 

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

119 return True 

120 return False 

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

122 def is_a_definition(line): 

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

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

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

126 # is usually docstring of the class 

127 return ( 

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

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

130 ) 

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

132 def is_root(p): 

133 """ 

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

135 """ 

136 for parent in p.self_and_parents(): 

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

138 return False 

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

140 #@-others 

141 log = c.frame.log 

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

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

144 files += 1 

145 for p in root.self_and_subtree(): 

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

147 for i, line in enumerate(lines): 

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

149 count += 1 

150 if root.v not in found: 

151 found.append(root.v) 

152 g.es_print('') 

153 g.es_print(root.h) 

154 print(line) 

155 unl = p.get_UNL() 

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

157 break 

158 g.es_print('') 

159 g.es_print( 

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

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

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

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

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

165def flake8_command(event): 

166 """ 

167 Run flake8 on all nodes of the selected tree, 

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

169 """ 

170 tag = 'flake8-files' 

171 if not flake8: 

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

173 return 

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

175 if not c or not c.p: 

176 return 

177 python = sys.executable 

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

179 path = g.fullPath(c, root) 

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

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

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

183 else: 

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

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

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

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

188def kill_pylint(event): 

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

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

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

192@g.command('mypy') 

193def mypy_command(event): 

194 """ 

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

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

197 suffices. 

198 

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

200 by running this command with the following node selected: 

201 

202 `@edit ../../launchLeo.py` 

203 

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

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

206 

207 Settings 

208 -------- 

209 

210 @data mypy-arguments 

211 @int mypy-link-limit = 0 

212 @string mypy-config-file='' 

213 

214 See leoSettings.leo for details. 

215 """ 

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

217 if not c: 

218 return 

219 if c.isChanged(): 

220 c.save() 

221 if mypy_api: 

222 MypyCommand(c).run(c.p) 

223 else: 

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

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

226@g.command('pyflakes') 

227def pyflakes_command(event): 

228 """ 

229 Run pyflakes on all nodes of the selected tree, 

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

231 """ 

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

233 if not c: 

234 return 

235 if c.isChanged(): 

236 c.save() 

237 if not pyflakes: 

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

239 return 

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

241 if ok: 

242 g.es('OK: pyflakes') 

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

244last_pylint_path = None 

245 

246@g.command('pylint') 

247def pylint_command(event): 

248 """ 

249 Run pylint on all nodes of the selected tree, 

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

251 or the last checked @<file> node. 

252 """ 

253 global last_pylint_path 

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

255 if c: 

256 if c.isChanged(): 

257 c.save() 

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

259 if data: 

260 path, p = data 

261 last_pylint_path = path 

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

263class MypyCommand: 

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

265 

266 def __init__(self, c): 

267 """ctor for MypyCommand class.""" 

268 self.c = c 

269 self.link_limit = None # Set in check_file. 

270 self.unknown_path_names = [] 

271 # Settings. 

272 self.args = c.config.getData('mypy-arguments') or [] 

273 self.config_file = c.config.getString('mypy-config-file') or None 

274 self.directory = c.config.getString('mypy-directory') or None 

275 self.link_limit = c.config.getInt('mypy-link-limit') or 0 

276 

277 #@+others 

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

279 def check_all(self, roots): 

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

281 c = self.c 

282 self.unknown_path_names = [] 

283 for root in roots: 

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

285 self.check_file(fn) 

286 g.es_print('mypy: done') 

287 

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

289 def check_file(self, fn): 

290 """Run mypy on one file.""" 

291 c = self.c 

292 link_pattern = re.compile(r'^(.+):([0-9]+): (error|note): (.*)\s*$') 

293 # Set the working directory. 

294 if self.directory: 

295 directory = self.directory 

296 else: 

297 directory = os.path.abspath(os.path.join(g.app.loadDir, '..', '..')) 

298 print(' mypy cwd:', directory) 

299 os.chdir(directory) 

300 # Set the args. Set the config file only if explicitly given. 

301 if self.config_file: 

302 config_file = g.os_path_finalize_join(directory, self.config_file) 

303 args = [f"--config-file={config_file}"] + self.args 

304 if not os.path.exists(config_file): 

305 print(f"config file not found: {config_file}") 

306 return 

307 else: 

308 args = self.args 

309 if args: 

310 print('mypy args:', args) 

311 # Run mypy. 

312 final_args = args + [fn] 

313 result = mypy.api.run(final_args) 

314 # Print result, making clickable links. 

315 lines = g.splitLines(result[0] or []) # type:ignore 

316 s_head = directory.lower() + os.path.sep 

317 for i, s in enumerate(lines): 

318 # Print the shortened form of s *without* changing s. 

319 if s.lower().startswith(s_head): 

320 print(f"{i:<3}", s[len(s_head) :].rstrip()) 

321 else: 

322 print(f"{i:<3}", s.rstrip()) 

323 # Create links only up to the link limit. 

324 if 0 < self.link_limit <= i: 

325 print(lines[-1].rstrip()) 

326 break 

327 m = link_pattern.match(s) 

328 if not m: 

329 g.es(s.strip()) 

330 continue 

331 # m.group(1) should be an absolute path. 

332 path = g.os_path_finalize_join(directory, m.group(1)) 

333 # m.group(2) should be the line number. 

334 try: 

335 line_number = int(m.group(2)) 

336 except Exception: 

337 g.es(s.strip()) 

338 continue # Not an error. 

339 # Look for the @<file> node. 

340 link_root = g.findNodeByPath(c, path) 

341 if link_root: 

342 unl = link_root.get_UNL() 

343 if s.lower().startswith(s_head): 

344 s = s[len(s_head) :] # Do *not* strip the line! 

345 c.frame.log.put(s, nodeLink=f"{unl}::{-line_number}") # Global line 

346 elif path not in self.unknown_path_names: 

347 self.unknown_path_names.append(path) 

348 print(f"no @<file> node found: {path}") 

349 # Print stderr. 

350 if result[1]: 

351 print('stderr...') 

352 print(result[1]) 

353 #@+node:ekr.20210302111935.5: *3* mypy.finalize 

354 def finalize(self, p): 

355 """Finalize p's path.""" 

356 c = self.c 

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

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

359 #@+node:ekr.20210302111935.7: *3* mypy.run 

360 def run(self, p): 

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

362 c = self.c 

363 root = p.copy() 

364 # Make sure Leo is on sys.path. 

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

366 if leo_path not in sys.path: 

367 sys.path.append(leo_path) 

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

369 self.check_all(roots) 

370 #@-others 

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

372class PyflakesCommand: 

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

374 

375 def __init__(self, c): 

376 """ctor for PyflakesCommand class.""" 

377 self.c = c 

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

379 

380 #@+others 

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

382 class LogStream: 

383 

384 """A log stream for pyflakes.""" 

385 

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

387 self.fn_n = fn_n 

388 self.roots = roots 

389 

390 def write(self, s): 

391 fn_n, roots = self.fn_n, self.roots 

392 if not s.strip(): 

393 return 

394 g.pr(s) 

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

396 if roots: 

397 try: 

398 root = roots[fn_n] 

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

400 unl = root.get_UNL() 

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

402 except(IndexError, TypeError, ValueError): 

403 # in case any assumptions fail 

404 g.es(s) 

405 else: 

406 g.es(s) 

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

408 def check_all(self, roots): 

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

410 total_errors = 0 

411 for i, root in enumerate(roots): 

412 fn = self.finalize(root) 

413 sfn = g.shortFileName(fn) 

414 # #1306: nopyflakes 

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

416 continue 

417 # Report the file name. 

418 s = g.readFileIntoEncodedString(fn) 

419 if s and s.strip(): 

420 # Send all output to the log pane. 

421 r = reporter.Reporter( 

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

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

424 ) 

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

426 total_errors += errors 

427 return total_errors 

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

429 def check_script(self, p, script): 

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

431 try: 

432 from pyflakes import api, reporter 

433 except Exception: # ModuleNotFoundError 

434 return True # Pretend all is fine. 

435 # #1306: nopyflakes 

436 lines = g.splitLines(p.b) 

437 for line in lines: 

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

439 return True 

440 r = reporter.Reporter( 

441 errorStream=self.LogStream(), 

442 warningStream=self.LogStream(), 

443 ) 

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

445 return errors == 0 

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

447 def finalize(self, p): 

448 """Finalize p's path.""" 

449 c = self.c 

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

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

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

453 def run(self, p): 

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

455 ok = True 

456 if not pyflakes: 

457 return ok 

458 c = self.c 

459 root = p 

460 # Make sure Leo is on sys.path. 

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

462 if leo_path not in sys.path: 

463 sys.path.append(leo_path) 

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

465 if roots: 

466 # These messages are important for clarity. 

467 total_errors = self.check_all(roots) 

468 if total_errors > 0: 

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

470 ok = total_errors == 0 

471 else: 

472 ok = True 

473 return ok 

474 #@-others 

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

476class PylintCommand: 

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

478 

479 # m.group(1) is the line number. 

480 # m.group(2) is the (unused) test name. 

481 link_pattern = r'^.*:\s*([0-9]+)[,:]\s*[0-9]+:.*?\((.*)\)\s*$' 

482 

483 # Example message: file-name:3966:12: R1705:xxxx (no-else-return) 

484 

485 def __init__(self, c): 

486 self.c = c 

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

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

489 #@+others 

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

491 def run(self, last_path=None): 

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

493 c, root = self.c, self.c.p 

494 if not lint: 

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

496 return False 

497 self.rc_fn = self.get_rc_file() 

498 if not self.rc_fn: 

499 return False 

500 # Make sure Leo is on sys.path. 

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

502 if leo_path not in sys.path: 

503 sys.path.append(leo_path) 

504 

505 # Ignore @nopylint trees. 

506 

507 def predicate(p): 

508 for parent in p.self_and_parents(): 

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

510 return False 

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

512 

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

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

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

516 if not data and last_path: 

517 # Default to the last path. 

518 fn = last_path 

519 for p in c.all_positions(): 

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

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

522 break 

523 if not data: 

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

525 return None 

526 for fn, p in data: 

527 self.run_pylint(fn, p) 

528 # #1808: return the last data file. 

529 return data[-1] if data else False 

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

531 def get_rc_file(self): 

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

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

534 table = ( 

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

536 # In ~/.leo 

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

538 # In leo/test 

539 ) 

540 for fn in table: 

541 fn = g.os_path_abspath(fn) 

542 if g.os_path_exists(fn): 

543 return fn 

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

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

546 return None 

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

548 def get_fn(self, p): 

549 """ 

550 Finalize p's file name. 

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

552 """ 

553 c = self.c 

554 fn = p.isAnyAtFileNode() 

555 if not fn: 

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

557 return None 

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

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

560 def run_pylint(self, fn, p): 

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

562 c, rc_fn = self.c, self.rc_fn 

563 # 

564 # Invoke pylint directly. 

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

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

567 if is_win: 

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

569 command = ( 

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

571 if not is_win: 

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

573 # 

574 # Run the command using the BPM. 

575 bpm = g.app.backgroundProcessManager 

576 bpm.start_process(c, command, 

577 fn=fn, 

578 kind='pylint', 

579 link_pattern=self.link_pattern, 

580 link_root=p, 

581 ) 

582 #@-others 

583#@-others 

584#@@language python 

585#@@tabwidth -4 

586#@@pagewidth 70 

587 

588#@-leo