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
« 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.
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:
201 `@edit ../../launchLeo.py`
203 Unlike running mypy outside of Leo, Leo's mypy command creates
204 clickable links in Leo's log pane for each error.
206 Settings
207 --------
209 @data mypy-arguments
210 @int mypy-link-limit = 0
211 @string mypy-config-file=''
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
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."""
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 = []
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)
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 )
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."""
321 def __init__(self, c):
322 """ctor for PyflakesCommand class."""
323 self.c = c
324 self.seen = [] # List of checked paths.
326 #@+others
327 #@+node:ekr.20171228013818.1: *3* class PyflakesCommand.LogStream
328 class LogStream:
330 """A log stream for pyflakes."""
332 def __init__(self, fn_n=0, roots=None):
333 self.fn_n = fn_n
334 self.roots = roots
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."""
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)
445 # Ignore @nopylint trees.
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.
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
528#@-leo