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

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
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.
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:
202 `@edit ../../launchLeo.py`
204 Unlike running mypy outside of Leo, Leo's mypy command creates
205 clickable links in Leo's log pane for each error.
207 Settings
208 --------
210 @data mypy-arguments
211 @int mypy-link-limit = 0
212 @string mypy-config-file=''
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
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."""
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
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')
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."""
375 def __init__(self, c):
376 """ctor for PyflakesCommand class."""
377 self.c = c
378 self.seen = [] # List of checked paths.
380 #@+others
381 #@+node:ekr.20171228013818.1: *3* class PyflakesCommand.LogStream
382 class LogStream:
384 """A log stream for pyflakes."""
386 def __init__(self, fn_n=0, roots=None):
387 self.fn_n = fn_n
388 self.roots = roots
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."""
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*$'
483 # Example message: file-name:3966:12: R1705:xxxx (no-else-return)
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)
505 # Ignore @nopylint trees.
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.
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
588#@-leo