]>
Commit | Line | Data |
---|---|---|
1 | """ | |
2 | API for the command-line I{pyflakes} tool. | |
3 | """ | |
4 | import ast | |
5 | import os | |
6 | import platform | |
7 | import re | |
8 | import sys | |
9 | ||
10 | from pyflakes import checker, __version__ | |
11 | from pyflakes import reporter as modReporter | |
12 | ||
13 | __all__ = ['check', 'checkPath', 'checkRecursive', 'iterSourceCode', 'main'] | |
14 | ||
15 | PYTHON_SHEBANG_REGEX = re.compile(br'^#!.*\bpython(3(\.\d+)?|w)?[dmu]?\s') | |
16 | ||
17 | ||
18 | def check(codeString, filename, reporter=None): | |
19 | """ | |
20 | Check the Python source given by C{codeString} for flakes. | |
21 | ||
22 | @param codeString: The Python source to check. | |
23 | @type codeString: C{str} | |
24 | ||
25 | @param filename: The name of the file the source came from, used to report | |
26 | errors. | |
27 | @type filename: C{str} | |
28 | ||
29 | @param reporter: A L{Reporter} instance, where errors and warnings will be | |
30 | reported. | |
31 | ||
32 | @return: The number of warnings emitted. | |
33 | @rtype: C{int} | |
34 | """ | |
35 | if reporter is None: | |
36 | reporter = modReporter._makeDefaultReporter() | |
37 | # First, compile into an AST and handle syntax errors. | |
38 | try: | |
39 | tree = ast.parse(codeString, filename=filename) | |
40 | except SyntaxError as e: | |
41 | reporter.syntaxError(filename, e.args[0], e.lineno, e.offset, e.text) | |
42 | return 1 | |
43 | except Exception: | |
44 | reporter.unexpectedError(filename, 'problem decoding source') | |
45 | return 1 | |
46 | # Okay, it's syntactically valid. Now check it. | |
47 | w = checker.Checker(tree, filename=filename) | |
48 | w.messages.sort(key=lambda m: m.lineno) | |
49 | for warning in w.messages: | |
50 | reporter.flake(warning) | |
51 | return len(w.messages) | |
52 | ||
53 | ||
54 | def checkPath(filename, reporter=None): | |
55 | """ | |
56 | Check the given path, printing out any warnings detected. | |
57 | ||
58 | @param reporter: A L{Reporter} instance, where errors and warnings will be | |
59 | reported. | |
60 | ||
61 | @return: the number of warnings printed | |
62 | """ | |
63 | if reporter is None: | |
64 | reporter = modReporter._makeDefaultReporter() | |
65 | try: | |
66 | with open(filename, 'rb') as f: | |
67 | codestr = f.read() | |
68 | except OSError as e: | |
69 | reporter.unexpectedError(filename, e.args[1]) | |
70 | return 1 | |
71 | return check(codestr, filename, reporter) | |
72 | ||
73 | ||
74 | def isPythonFile(filename): | |
75 | """Return True if filename points to a Python file.""" | |
76 | if filename.endswith('.py'): | |
77 | return True | |
78 | ||
79 | # Avoid obvious Emacs backup files | |
80 | if filename.endswith("~"): | |
81 | return False | |
82 | ||
83 | max_bytes = 128 | |
84 | ||
85 | try: | |
86 | with open(filename, 'rb') as f: | |
87 | text = f.read(max_bytes) | |
88 | if not text: | |
89 | return False | |
90 | except OSError: | |
91 | return False | |
92 | ||
93 | return PYTHON_SHEBANG_REGEX.match(text) | |
94 | ||
95 | ||
96 | def iterSourceCode(paths): | |
97 | """ | |
98 | Iterate over all Python source files in C{paths}. | |
99 | ||
100 | @param paths: A list of paths. Directories will be recursed into and | |
101 | any .py files found will be yielded. Any non-directories will be | |
102 | yielded as-is. | |
103 | """ | |
104 | for path in paths: | |
105 | if os.path.isdir(path): | |
106 | for dirpath, dirnames, filenames in os.walk(path): | |
107 | for filename in filenames: | |
108 | full_path = os.path.join(dirpath, filename) | |
109 | if isPythonFile(full_path): | |
110 | yield full_path | |
111 | else: | |
112 | yield path | |
113 | ||
114 | ||
115 | def checkRecursive(paths, reporter): | |
116 | """ | |
117 | Recursively check all source files in C{paths}. | |
118 | ||
119 | @param paths: A list of paths to Python source files and directories | |
120 | containing Python source files. | |
121 | @param reporter: A L{Reporter} where all of the warnings and errors | |
122 | will be reported to. | |
123 | @return: The number of warnings found. | |
124 | """ | |
125 | warnings = 0 | |
126 | for sourcePath in iterSourceCode(paths): | |
127 | warnings += checkPath(sourcePath, reporter) | |
128 | return warnings | |
129 | ||
130 | ||
131 | def _exitOnSignal(sigName, message): | |
132 | """Handles a signal with sys.exit. | |
133 | ||
134 | Some of these signals (SIGPIPE, for example) don't exist or are invalid on | |
135 | Windows. So, ignore errors that might arise. | |
136 | """ | |
137 | import signal | |
138 | ||
139 | try: | |
140 | sigNumber = getattr(signal, sigName) | |
141 | except AttributeError: | |
142 | # the signal constants defined in the signal module are defined by | |
143 | # whether the C library supports them or not. So, SIGPIPE might not | |
144 | # even be defined. | |
145 | return | |
146 | ||
147 | def handler(sig, f): | |
148 | sys.exit(message) | |
149 | ||
150 | try: | |
151 | signal.signal(sigNumber, handler) | |
152 | except ValueError: | |
153 | # It's also possible the signal is defined, but then it's invalid. In | |
154 | # this case, signal.signal raises ValueError. | |
155 | pass | |
156 | ||
157 | ||
158 | def _get_version(): | |
159 | """ | |
160 | Retrieve and format package version along with python version & OS used | |
161 | """ | |
162 | return ('%s Python %s on %s' % | |
163 | (__version__, platform.python_version(), platform.system())) | |
164 | ||
165 | ||
166 | def main(prog=None, args=None): | |
167 | """Entry point for the script "pyflakes".""" | |
168 | import argparse | |
169 | ||
170 | # Handle "Keyboard Interrupt" and "Broken pipe" gracefully | |
171 | _exitOnSignal('SIGINT', '... stopped') | |
172 | _exitOnSignal('SIGPIPE', 1) | |
173 | ||
174 | parser = argparse.ArgumentParser(prog=prog, | |
175 | description='Check Python source files for errors') | |
176 | parser.add_argument('-V', '--version', action='version', version=_get_version()) | |
177 | parser.add_argument('path', nargs='*', | |
178 | help='Path(s) of Python file(s) to check. STDIN if not given.') | |
179 | args = parser.parse_args(args=args).path | |
180 | reporter = modReporter._makeDefaultReporter() | |
181 | if args: | |
182 | warnings = checkRecursive(args, reporter) | |
183 | else: | |
184 | warnings = check(sys.stdin.read(), '<stdin>', reporter) | |
185 | raise SystemExit(warnings > 0) |