Merge lp:~sinzui/ubuntu/oneiric/pocket-lint/0.5.21 into lp:ubuntu/oneiric/pocket-lint

Proposed by Curtis Hovey
Status: Superseded
Proposed branch: lp:~sinzui/ubuntu/oneiric/pocket-lint/0.5.21
Merge into: lp:ubuntu/oneiric/pocket-lint
Diff against target: 2449 lines (+1499/-241)
10 files modified
ChangeLog (+121/-0)
PKG-INFO (+1/-1)
debian/changelog (+6/-0)
debian/control (+1/-2)
debian/copyright (+6/-1)
pocketlint/contrib/cssccc.py (+495/-0)
pocketlint/contrib/pep8.py (+683/-188)
pocketlint/formatcheck.py (+94/-34)
pocketlint/jsreporter.js (+71/-14)
setup.py (+21/-1)
To merge this branch: bzr merge lp:~sinzui/ubuntu/oneiric/pocket-lint/0.5.21
Reviewer Review Type Date Requested Status
Scott Moser Needs Fixing
Ubuntu branches Pending
Review via email: mp+74924@code.launchpad.net

This proposal has been superseded by a proposal from 2011-10-18.

Description of the change

Merge upstream release that improves CSS and JS support
debian/control was updated to require gjs

To post a comment you must log in.
Revision history for this message
Scott Moser (smoser) wrote :

It looks like debian/copyright needs updating at least for './pocketlint/contrib/cssccc.py', which is listed as "public domain", unlike the other files that are MIT.

The rest of it looks good to me.

review: Needs Fixing
Revision history for this message
Scott Moser (smoser) wrote :

oh, and just to be clear, you're not expecting this for SRU to oneiric, right?

Revision history for this message
Curtis Hovey (sinzui) wrote :

Thank you for spotting the license change. I updated the license file. No I do not think this deserves an SRU given that the branch is more than a month old.

6. By Curtis Hovey

Updated license information.

Unmerged revisions

6. By Curtis Hovey

Updated license information.

5. By Curtis Hovey

New upstream release improves CSS and JS support.
Fixes Gtk3 bugs.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'ChangeLog'
--- ChangeLog 2011-04-16 15:32:48 +0000
+++ ChangeLog 2011-10-14 21:24:26 +0000
@@ -1,5 +1,126 @@
1<Generated by bzr log --log-format=gnu>1<Generated by bzr log --log-format=gnu>
22
32011-06-06 Curtis Hovey <sinzui.is@verizon.net>
4
5 [396] Use seed as the js interpreter.
6
72011-06-06 Curtis Hovey <sinzui.is@verizon.net>
8
9 [395] Clear refernece.
10
112011-06-06 Curtis Hovey <sinzui.is@verizon.net>
12
13 [394] Documented how to use jsreporter.js
14
152011-06-06 Curtis Hovey <sinzui.is@verizon.net>
16
17 [393] Removed unused code.
18
192011-06-06 Curtis Hovey <sinzui.is@verizon.net>
20
21 [392] Use seed as the default js command.
22
232011-06-06 Curtis Hovey <sinzui.is@verizon.net>
24
25 [391] Updated attr name.
26
272011-06-06 Curtis Hovey <sinzui.is@verizon.net>
28
29 [390] Removed js subprocess, but HTML5Browser is slow.
30
312011-06-05 Curtis Hovey <sinzui.is@verizon.net>
32
33 [389] Updated documentation.
34
352011-06-05 Curtis Hovey <sinzui.is@verizon.net>
36
37 [388] Minor speed changes.
38
392011-06-05 Curtis Hovey <sinzui.is@verizon.net>
40
41 [387] Added rudimentary JS checker based on Webkit.
42
432011-05-10 Curtis Hovey <sinzui.is@verizon.net>
44
45 [386] Incremented version.
46
472011-05-10 Curtis Hovey <sinzui.is@verizon.net>
48
49 [385] merged Adi's fix.
50
512011-05-04 Curtis Hovey <sinzui.is@verizon.net>
52
53 [384] Inremented version.
54
552011-05-04 Curtis Hovey <sinzui.is@verizon.net>
56
57 [383] Corrected the js line number.
58
592011-05-04 Curtis Hovey <sinzui.is@verizon.net>
60
61 [382] Catch and report IndentationExceptions.
62
632011-05-04 Curtis Hovey <sinzui.is@verizon.net>
64
65 [381] Removed unneeded checks.
66
672011-05-04 Curtis Hovey <sinzui.is@verizon.net>
68
69 [380] Remove trailing whitespace.
70
712011-05-04 Curtis Hovey <sinzui.is@verizon.net>
72
73 [379] Merged python encoding fix.
74
752011-04-19 Curtis Hovey <sinzui.is@verizon.net>
76
77 [378] Increment version.
78
792011-04-19 Curtis Hovey <sinzui.is@verizon.net>
80
81 [377] Hush WindowError.
82
832011-04-19 Curtis Hovey <sinzui.is@verizon.net>
84
85 [376] Move signed archives rules into setup.py.
86
872011-04-17 Curtis Hovey <sinzui.is@verizon.net>
88
89 [375] Incremented version for cssccc
90
912011-04-17 Curtis Hovey <sinzui.is@verizon.net>
92
93 [374] Merged cssccc lib.
94
952011-04-17 Curtis Hovey <sinzui.is@verizon.net>
96
97 [373] Added a rule to sign the release after it is made.
98
992011-04-17 Curtis Hovey <sinzui.is@verizon.net>
100
101 [372] Incremenet version for release.
102
1032011-04-17 Curtis Hovey <sinzui.is@verizon.net>
104
105 [371] Merged Adi's feature that returns the count od reported issues.
106
1072011-04-17 Curtis Hovey <sinzui.is@verizon.net>
108
109 [370] Added simple SQL checker.
110
1112011-04-17 Curtis Hovey <sinzui.is@verizon.net>
112
113 [369] Elaborated comment test.
114
1152011-04-17 Curtis Hovey <sinzui.is@verizon.net>
116
117 [368] Added a basic test to verify comments do not mess up blank line
118 counts.
119
1202011-04-17 Curtis Hovey <sinzui.is@verizon.net>
121
122 [367] Update pep8 to version 0.6.1
123
32011-04-15 Curtis Hovey <sinzui.is@verizon.net>1242011-04-15 Curtis Hovey <sinzui.is@verizon.net>
4125
5 [366] Increment version.126 [366] Increment version.
6127
=== modified file 'PKG-INFO'
--- PKG-INFO 2011-04-16 15:32:48 +0000
+++ PKG-INFO 2011-10-14 21:24:26 +0000
@@ -1,6 +1,6 @@
1Metadata-Version: 1.01Metadata-Version: 1.0
2Name: pocketlint2Name: pocketlint
3Version: 0.5.133Version: 0.5.21
4Summary: Pocket-lint a composite linter and style checker.4Summary: Pocket-lint a composite linter and style checker.
5Home-page: https://launchpad.net/pocket-lint5Home-page: https://launchpad.net/pocket-lint
6Author: Curtis C. Hovey6Author: Curtis C. Hovey
77
=== modified file 'debian/changelog'
--- debian/changelog 2011-04-16 15:32:48 +0000
+++ debian/changelog 2011-10-14 21:24:26 +0000
@@ -1,3 +1,9 @@
1pocket-lint (0.5.21-0ubuntu1) UNRELEASED; urgency=low
2
3 * New upstream release.
4
5 -- Curtis C. Hovey <sinzui.is@verizon.net> Sun, 11 Sep 2011 17:22:02 -0400
6
1pocket-lint (0.5.13-0ubuntu1) natty; urgency=low7pocket-lint (0.5.13-0ubuntu1) natty; urgency=low
28
3 * New upstream bug fix release9 * New upstream bug fix release
410
=== modified file 'debian/control'
--- debian/control 2011-04-15 13:37:44 +0000
+++ debian/control 2011-10-14 21:24:26 +0000
@@ -11,8 +11,7 @@
11Package: python-pocket-lint11Package: python-pocket-lint
12Architecture: all12Architecture: all
13Depends: ${misc:Depends}, ${shlibs:Depends}, ${python:Depends}13Depends: ${misc:Depends}, ${shlibs:Depends}, ${python:Depends}
14Recommends: python-cssutils14Recommends: python-cssutils, gjs
15Suggests: spidermonkey-bin
16Description: a composite linter and style checker15Description: a composite linter and style checker
17 Pocket-lint has several notable features:16 Pocket-lint has several notable features:
18 .17 .
1918
=== modified file 'debian/copyright'
--- debian/copyright 2010-11-17 15:46:54 +0000
+++ debian/copyright 2011-10-14 21:24:26 +0000
@@ -4,7 +4,7 @@
4Copyright:4Copyright:
5 Upstream Author: Curtis C. Hovey5 Upstream Author: Curtis C. Hovey
66
7License:7Primary license:
8 The MIT License:8 The MIT License:
99
10Permission is hereby granted, free of charge, to any person obtaining a copy10Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -25,6 +25,11 @@
25OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN25OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
26THE SOFTWARE.26THE SOFTWARE.
2727
28
29contrib/cssccc.py license:
30 Public Domain
31
32
28The Debian packaging is:33The Debian packaging is:
29 Copyright 2010 Curtis C. Hovey34 Copyright 2010 Curtis C. Hovey
30 and is under the MIT licence.35 and is under the MIT licence.
3136
=== added file 'pocketlint/contrib/cssccc.py'
--- pocketlint/contrib/cssccc.py 1970-01-01 00:00:00 +0000
+++ pocketlint/contrib/cssccc.py 2011-10-14 21:24:26 +0000
@@ -0,0 +1,495 @@
1'''
2This code is in the public domain.
3
4Check CSS code for some common coding conventions.
5The code must be in a valid CSS format.
6It is recommend to first parse it using cssutils.
7It is also recommend to check it with pocket-lint for things like trailing
8spaces or tab characters.
9
10If a comment is on the whole line, it will consume the whole line like it
11was not there.
12If a comment is inside a line it will only consume its own content.
13
14Bases on Stoyan Stefanov's http://www.phpied.com/css-coding-conventions/
15
16'@media' rule is not supported.
17 @media print {
18 html {
19 background: #fff;
20 color: #000;
21 }
22 body {
23 padding: 1in;
24 border: 0.5pt solid #666;
25 }
26 }
27
28The following at-rules are supported:
29 * keyword / text at-rules
30 * @charset "ISO-8859-15";
31 * @import url(/css/screen.css) screen, projection;
32 * @namespace foo "http://example.com/ns/foo";
33 * keybord / block rules
34 * @page { block; }
35 * @font-face { block; }
36
37
38TODO:
39 * add warning for using px for fonts.
40 * add Unicode support.
41 * add AtRule checks
42 * add support for TAB as a separator / identation.
43 * add support for @media
44'''
45from __future__ import with_statement
46
47__version__ = '0.1.1'
48
49import sys
50
51SELECTOR_SEPARATOR = ','
52DECLARATION_SEPARATOR = ';'
53PROPERTY_SEPARATOR = ':'
54COMMENT_START = r'/*'
55COMMENT_END = r'*/'
56AT_TEXT_RULES = ['import', 'charset', 'namespace']
57AT_BLOCK_RULES = ['page', 'font-face']
58# If you want
59# selector,
60# selector2
61# {
62# property:
63# }
64#IGNORED_MESSAGES = ['I013', 'I014']
65
66# If you want
67# selector,
68# selector {
69# property:
70# }
71#IGNORED_MESSAGES = ['I005', 'I014']
72
73# If you want
74# selector,
75# selector2 {
76# property:
77# }
78IGNORED_MESSAGES = ['I005', 'I006']
79
80
81class CSSRule(object):
82 '''A CSS rule.'''
83
84 def check(self):
85 '''Check the rule.'''
86 raise AssertionError('Method not implemtned.')
87
88
89class CSSAtRule(object):
90 '''A CSS @rule.'''
91
92 type = object()
93
94 def __init__(self, identifier, keyword, log, text=None, block=None):
95 self.identifier = identifier
96 self.keyword = keyword
97 self.text = text
98 self.block = block
99 self.log = log
100
101 def check(self):
102 '''Check the rule.'''
103
104
105class CSSRuleSet(object):
106 '''A CSS rule_set.'''
107
108 type = object()
109
110 def __init__(self, selector, declarations, log):
111 self.selector = selector
112 self.declarations = declarations
113 self.log = log
114
115 def __str__(self):
116 return '%s{%s}' % (str(self.selector), str(self.declarations))
117
118 def __repr__(self):
119 return '%d:%s{%s}' % (
120 self.selector.start_line,
121 str(self.selector),
122 str(self.declarations),
123 )
124
125 def check(self):
126 '''Check the rule set.'''
127 self.checkSelector()
128 self.checkDeclarations()
129
130 def checkSelector(self):
131 '''Check rule-set selector.'''
132 start_line = self.selector.getStartLine()
133 selectors = self.selector.text.split(SELECTOR_SEPARATOR)
134 offset = 0
135 last_selector = selectors[-1]
136 first_selector = selectors[0]
137 rest_selectors = selectors[1:]
138
139 if first_selector.startswith('\n\n\n'):
140 self.log(start_line, 'I002', 'To many newlines before selectors.')
141 elif first_selector.startswith('\n\n'):
142 pass
143 elif start_line > 2:
144 self.log(start_line, 'I003', 'To few newlines before selectors.')
145 else:
146 pass
147
148 for selector in rest_selectors:
149 if not selector.startswith('\n'):
150 self.log(
151 start_line + offset,
152 'I004',
153 'Selector must be on a new line.')
154 offset += selector.count('\n')
155
156 if not last_selector.endswith('\n'):
157 self.log(
158 start_line + offset,
159 'I005',
160 'No newline after last selector.')
161
162 if not (last_selector[-2] != ' ' and last_selector[-1] == (' ')):
163 self.log(
164 start_line + offset,
165 'I013',
166 'Last selector must be followed by " {".')
167
168 def checkDeclarations(self):
169 '''Check rule-set declarations.'''
170 start_line = self.declarations.getStartLine()
171 declarations = self.declarations.text.split(DECLARATION_SEPARATOR)
172 offset = 0
173
174 # Check all declarations except last as this is the new line.
175 first_declaration = True
176 for declaration in declarations[:-1]:
177 if not declaration.startswith('\n'):
178 self.log(
179 start_line + offset,
180 'I007',
181 'Each declarations should start on a new line.',
182 )
183 elif (not declaration.startswith('\n ') or
184 declaration[5] == ' '):
185 self.log(
186 start_line + offset,
187 'I008',
188 'Each declaration must be indented with 4 spaces.',
189 )
190
191 parts = declaration.split(PROPERTY_SEPARATOR)
192 if len(parts) != 2:
193 self.log(
194 start_line + offset,
195 'I009',
196 'Wrong separator on property: value pair.',
197 )
198 else:
199 prop, value = parts
200 if prop.endswith(' '):
201 self.log(
202 start_line + offset,
203 'I010',
204 'Whitespace before ":".',
205 )
206 if not (value.startswith(' ') or value.startswith('\n')):
207 self.log(
208 start_line + offset,
209 'I011',
210 'Missing whitespace after ":".',
211 )
212 elif value.startswith(' '):
213 self.log(
214 start_line + offset,
215 'I012',
216 'Multiple whitespaces after ":".',
217 )
218 if first_declaration:
219 first_declaration = False
220 else:
221 offset += declaration.count('\n')
222
223 last_declaration = declarations[-1]
224 offset += last_declaration.count('\n')
225 if last_declaration != '\n':
226 self.log(
227 start_line + offset,
228 'I006',
229 'Rule declarations should end with a single new line.',
230 )
231 if last_declaration != '\n ':
232 self.log(
233 start_line + offset,
234 'I014',
235 'Rule declarations should end indented on a single new line.',
236 )
237
238
239class CSSStatementMember(object):
240 '''A member of CSS statement.'''
241
242 def __init__(self, start_line, start_character, text):
243 self.start_line = start_line
244 self.start_character = start_character
245 self.text = text
246
247 def getStartLine(self):
248 '''Return the line number for first character in the statement and
249 the number of new lines untilg the first character.'''
250 index = 0
251 text = self.text
252 character = text[index]
253 while character == '\n':
254 index += 1
255 character = text[index]
256
257 return self.start_line + index + 1
258
259 def __str__(self):
260 return self.text
261
262 def __repr__(self):
263 return '%d:%d:{%s}' % (
264 self.start_line, self.start_character, self.text)
265
266
267class CSSCodingConventionChecker(object):
268 '''CSS coding convention checker.'''
269
270 icons = {
271 'E': 'error',
272 'I': 'info',
273 }
274
275 def __init__(self, text, logger=None):
276 self._text = text.splitlines(True)
277 self.line_number = 0
278 self.character_number = 0
279 if logger:
280 self._logger = logger
281 else:
282 self._logger = self._defaultLog
283
284 def log(self, line_number, code, message):
285 '''Log the message with `code`.'''
286 if code in IGNORED_MESSAGES:
287 return
288 icon = self.icons[code[0]]
289 self._logger(line_number, code + ': ' + message, icon=icon)
290
291 def check(self):
292 '''Check all rules.'''
293 for rule in self.getRules():
294 rule.check()
295
296 def getRules(self):
297 '''Generates the next CSS rule ignoring comments.'''
298 while True:
299 yield self.getNextRule()
300
301 def getNextRule(self):
302 '''Return the next parsed rule.
303
304 Raise `StopIteration` if we are at the last rule.
305 '''
306 if self._nextStatementIsAtRule():
307 text = None
308 block = None
309 keyword = self._parse('@')
310 # TODO: user regex [ \t {]
311 keyword_text = self._parse(' ')
312 keyword_name = keyword_text.text
313 keyword.text += '@' + keyword_name + ' '
314
315 if keyword_name.lower() in AT_TEXT_RULES:
316 text = self._parse(';')
317 elif keyword_name.lower() in AT_BLOCK_RULES:
318 start = self._parse('{')
319 keyword.text += start.text
320 block = self._parse('}')
321 else:
322 self._parse(';')
323 raise StopIteration
324
325 return CSSAtRule(
326 identifier=keyword_name,
327 keyword=keyword,
328 text=text,
329 block=block,
330 log=self.log)
331 else:
332 selector = self._parse('{')
333 declarations = self._parse('}')
334 return CSSRuleSet(
335 selector=selector,
336 declarations=declarations,
337 log=self.log)
338
339 def _defaultLog(self, line_number, message, icon='info'):
340 '''Log the message to STDOUT.'''
341 print ' %4s:%s' % (line_number, message)
342
343 def _nextStatementIsAtRule(self):
344 '''Return True if next statement in the buffer is an at-rule.
345
346 Just look for open brackets and see if there is an @ before that
347 braket.
348 '''
349 search_buffer = []
350 line_counter = self.line_number
351 current_line = self._text[line_counter][self.character_number:]
352 while current_line.find('@') == -1:
353 search_buffer.append(current_line)
354 line_counter += 1
355 try:
356 current_line = self._text[line_counter]
357 except IndexError:
358 return False
359
360 text_buffer = ''.join(search_buffer)
361 if text_buffer.find('{') == -1:
362 return True
363 else:
364 return False
365
366 def _parse(self, stop_character):
367 '''Return the parsed text until stop_character.'''
368 try:
369 self._text[self.line_number][self.character_number]
370 except IndexError:
371 raise StopIteration
372 result = []
373 start_line = self.line_number
374 start_character = self.character_number
375 comment_started = False
376 while True:
377 try:
378 data = self._text[self.line_number][self.character_number:]
379 except IndexError:
380 break
381
382 # Look for comment start/end.
383 (comment_update,
384 before_comment,
385 after_comment,
386 newline_consumed) = _check_comment(data)
387 if comment_update is not None:
388 comment_started = comment_update
389
390 if comment_started:
391 # We are inside a comment.
392 # Add the data before the comment and go to next line.
393 if before_comment is not None:
394 result.append(before_comment)
395 self.character_number = 0
396 self.line_number += 1
397 continue
398
399 # If we have a comment, strip it from the data.
400 # Remember the initial cursor position to know where to
401 # continue.
402 initial_position = data.find(stop_character)
403 if before_comment is not None or after_comment is not None:
404 if before_comment is None:
405 before_comment = ''
406 if after_comment is None:
407 after_comment = ''
408 data = before_comment + after_comment
409
410 if initial_position == -1 or newline_consumed:
411 # We are not at the end.
412 # Go to next line and append the data.
413 result.append(data)
414 self.character_number = 0
415 self.line_number += 1
416 continue
417 else:
418 # Delimiter found.
419 # Find it again in the text that now has no comments.
420 # Append data until the delimiter.
421 # Move cursor to next character and stop searching for it.
422 new_position = data.find(stop_character)
423 result.append(data[:new_position])
424 self.character_number += initial_position + 1
425 break
426
427 return CSSStatementMember(
428 start_line=start_line,
429 start_character=start_character,
430 text=''.join(result))
431
432
433def _check_comment(data):
434 '''Check the data for comment markers.'''
435
436 comment_started = None
437 before_comment = None
438 after_comment = None
439 newline_consumed = False
440
441 comment_start = data.find(COMMENT_START)
442 if comment_start != -1:
443 comment_started = True
444 before_comment = data[:comment_start]
445 # Only use `None` to signal that there is no text before the comment.
446 if before_comment == '':
447 before_comment = None
448
449 comment_end = data.find(COMMENT_END)
450 if comment_end != -1:
451 comment_started = False
452 # Set comment end after the lenght of the actual comment end
453 # marker.
454 comment_end += len(COMMENT_END)
455 if before_comment is None and data[comment_end] == '\n':
456 # Consume the new line if it next to the comment end and
457 # the comment in on the whole line.
458 comment_end += 1
459 newline_consumed = True
460 after_comment = data[comment_end:]
461 return (comment_started, before_comment, after_comment, newline_consumed)
462
463
464def show_usage():
465 '''Print the command usage.'''
466 print 'Usage: cssccc OPTIONS'
467 print ' -h, --help\t\tShow this help.'
468 print ' -v, --version\t\tShow version.'
469 print ' -f FILE, --file=FILE\tCheck FILE'
470
471
472def read_file(filename):
473 '''Return the content of filename.'''
474 text = ''
475 with open(filename, 'r') as f:
476 text = f.read()
477 return text
478
479
480if __name__ == '__main__':
481 if len(sys.argv) < 2:
482 show_usage()
483 elif sys.argv[1] in ['-v', '--version']:
484 print 'CSS Code Convention Checker %s' % (__version__)
485 sys.exit(0)
486 elif sys.argv[1] == '-f':
487 text = read_file(sys.argv[2])
488 checker = CSSCodingConventionChecker(text)
489 sys.exit(checker.check())
490 elif sys.argv[1] == '--file=':
491 text = read_file(sys.argv[1][len('--file='):])
492 checker = CSSCodingConventionChecker(text)
493 sys.exit(checker.check())
494 else:
495 show_usage()
0496
=== modified file 'pocketlint/contrib/pep8.py'
--- pocketlint/contrib/pep8.py 2010-11-17 15:46:54 +0000
+++ pocketlint/contrib/pep8.py 2011-10-14 21:24:26 +0000
@@ -1,6 +1,6 @@
1#!/usr/bin/python1#!/usr/bin/python
2# pep8.py - Check Python source code formatting, according to PEP 82# pep8.py - Check Python source code formatting, according to PEP 8
3# Copyright (C) 2006 Johann C. Rocholl <johann@browsershots.org>3# Copyright (C) 2006 Johann C. Rocholl <johann@rocholl.net>
4#4#
5# Permission is hereby granted, free of charge, to any person5# Permission is hereby granted, free of charge, to any person
6# obtaining a copy of this software and associated documentation files6# obtaining a copy of this software and associated documentation files
@@ -30,8 +30,7 @@
30$ python pep8.py -h30$ python pep8.py -h
3131
32This program and its regression test suite live here:32This program and its regression test suite live here:
33http://svn.browsershots.org/trunk/devtools/pep8/33http://github.com/jcrocholl/pep8
34http://trac.browsershots.org/browser/trunk/devtools/pep8/
3534
36Groups of errors and warnings:35Groups of errors and warnings:
37E errors36E errors
@@ -79,33 +78,65 @@
7978
80The docstring of each check function shall be the relevant part of79The docstring of each check function shall be the relevant part of
81text from PEP 8. It is printed if the user enables --show-pep8.80text from PEP 8. It is printed if the user enables --show-pep8.
81Several docstrings contain examples directly from the PEP 8 document.
82
83Okay: spam(ham[1], {eggs: 2})
84E201: spam( ham[1], {eggs: 2})
85
86These examples are verified automatically when pep8.py is run with the
87--doctest option. You can add examples for your own check functions.
88The format is simple: "Okay" or error/warning code followed by colon
89and space, the rest of the line is example source code. If you put 'r'
90before the docstring, you can use \n for newline, \t for tab and \s
91for space.
8292
83"""93"""
8494
95__version__ = '0.5.1dev'
96
85import os97import os
86import sys98import sys
87import re99import re
88import time100import time
89import inspect101import inspect
102import keyword
90import tokenize103import tokenize
91from optparse import OptionParser104from optparse import OptionParser
92from keyword import iskeyword
93from fnmatch import fnmatch105from fnmatch import fnmatch
94106try:
95__version__ = '0.2.0'107 frozenset
96__revision__ = '$Rev: 2208 $'108except NameError:
97109 from sets import ImmutableSet as frozenset
98default_exclude = '.svn,CVS,*.pyc,*.pyo'110
99111
100indent_match = re.compile(r'([ \t]*)').match112DEFAULT_EXCLUDE = '.svn,CVS,.bzr,.hg,.git'
101raise_comma_match = re.compile(r'raise\s+\w+\s*(,)').match113DEFAULT_IGNORE = 'E24'
102114MAX_LINE_LENGTH = 79
103operators = """115
104+ - * / % ^ & | = < > >> <<116INDENT_REGEX = re.compile(r'([ \t]*)')
105+= -= *= /= %= ^= &= |= == <= >= >>= <<=117RAISE_COMMA_REGEX = re.compile(r'raise\s+\w+\s*(,)')
106!= <> :118SELFTEST_REGEX = re.compile(r'(Okay|[EW]\d{3}):\s(.*)')
107in is or not and119ERRORCODE_REGEX = re.compile(r'[EW]\d{3}')
108""".split()120DOCSTRING_REGEX = re.compile(r'u?r?["\']')
121WHITESPACE_AROUND_OPERATOR_REGEX = \
122 re.compile('([^\w\s]*)\s*(\t| )\s*([^\w\s]*)')
123EXTRANEOUS_WHITESPACE_REGEX = re.compile(r'[[({] | []}),;:]')
124WHITESPACE_AROUND_NAMED_PARAMETER_REGEX = \
125 re.compile(r'[()]|\s=[^=]|[^=!<>]=\s')
126
127
128WHITESPACE = ' \t'
129
130BINARY_OPERATORS = frozenset(['**=', '*=', '+=', '-=', '!=', '<>',
131 '%=', '^=', '&=', '|=', '==', '/=', '//=', '<=', '>=', '<<=', '>>=',
132 '%', '^', '&', '|', '=', '/', '//', '<', '>', '<<'])
133UNARY_OPERATORS = frozenset(['>>', '**', '*', '+', '-'])
134OPERATORS = BINARY_OPERATORS | UNARY_OPERATORS
135SKIP_TOKENS = frozenset([tokenize.COMMENT, tokenize.NL, tokenize.INDENT,
136 tokenize.DEDENT, tokenize.NEWLINE])
137E225NOT_KEYWORDS = (frozenset(keyword.kwlist + ['print']) -
138 frozenset(['False', 'None', 'True']))
139BENCHMARK_KEYS = ('directories', 'files', 'logical lines', 'physical lines')
109140
110options = None141options = None
111args = None142args = None
@@ -117,7 +148,7 @@
117148
118149
119def tabs_or_spaces(physical_line, indent_char):150def tabs_or_spaces(physical_line, indent_char):
120 """151 r"""
121 Never mix tabs and spaces.152 Never mix tabs and spaces.
122153
123 The most popular way of indenting Python is with spaces only. The154 The most popular way of indenting Python is with spaces only. The
@@ -126,38 +157,65 @@
126 invoking the Python command line interpreter with the -t option, it issues157 invoking the Python command line interpreter with the -t option, it issues
127 warnings about code that illegally mixes tabs and spaces. When using -tt158 warnings about code that illegally mixes tabs and spaces. When using -tt
128 these warnings become errors. These options are highly recommended!159 these warnings become errors. These options are highly recommended!
160
161 Okay: if a == 0:\n a = 1\n b = 1
162 E101: if a == 0:\n a = 1\n\tb = 1
129 """163 """
130 indent = indent_match(physical_line).group(1)164 indent = INDENT_REGEX.match(physical_line).group(1)
131 for offset, char in enumerate(indent):165 for offset, char in enumerate(indent):
132 if char != indent_char:166 if char != indent_char:
133 return offset, "E101 indentation contains mixed spaces and tabs"167 return offset, "E101 indentation contains mixed spaces and tabs"
134168
135169
136def tabs_obsolete(physical_line):170def tabs_obsolete(physical_line):
137 """171 r"""
138 For new projects, spaces-only are strongly recommended over tabs. Most172 For new projects, spaces-only are strongly recommended over tabs. Most
139 editors have features that make this easy to do.173 editors have features that make this easy to do.
174
175 Okay: if True:\n return
176 W191: if True:\n\treturn
140 """177 """
141 indent = indent_match(physical_line).group(1)178 indent = INDENT_REGEX.match(physical_line).group(1)
142 if indent.count('\t'):179 if indent.count('\t'):
143 return indent.index('\t'), "W191 indentation contains tabs"180 return indent.index('\t'), "W191 indentation contains tabs"
144181
145182
146def trailing_whitespace(physical_line):183def trailing_whitespace(physical_line):
147 """184 r"""
148 JCR: Trailing whitespace is superfluous.185 JCR: Trailing whitespace is superfluous.
186 FBM: Except when it occurs as part of a blank line (i.e. the line is
187 nothing but whitespace). According to Python docs[1] a line with only
188 whitespace is considered a blank line, and is to be ignored. However,
189 matching a blank line to its indentation level avoids mistakenly
190 terminating a multi-line statement (e.g. class declaration) when
191 pasting code into the standard Python interpreter.
192
193 [1] http://docs.python.org/reference/lexical_analysis.html#blank-lines
194
195 The warning returned varies on whether the line itself is blank, for easier
196 filtering for those who want to indent their blank lines.
197
198 Okay: spam(1)
199 W291: spam(1)\s
200 W293: class Foo(object):\n \n bang = 12
149 """201 """
150 physical_line = physical_line.rstrip('\n') # chr(10), newline202 physical_line = physical_line.rstrip('\n') # chr(10), newline
151 physical_line = physical_line.rstrip('\r') # chr(13), carriage return203 physical_line = physical_line.rstrip('\r') # chr(13), carriage return
152 physical_line = physical_line.rstrip('\x0c') # chr(12), form feed, ^L204 physical_line = physical_line.rstrip('\x0c') # chr(12), form feed, ^L
153 stripped = physical_line.rstrip()205 stripped = physical_line.rstrip()
154 if physical_line != stripped:206 if physical_line != stripped:
155 return len(stripped), "W291 trailing whitespace"207 if stripped:
208 return len(stripped), "W291 trailing whitespace"
209 else:
210 return 0, "W293 blank line contains whitespace"
156211
157212
158def trailing_blank_lines(physical_line, lines, line_number):213def trailing_blank_lines(physical_line, lines, line_number):
159 """214 r"""
160 JCR: Trailing blank lines are superfluous.215 JCR: Trailing blank lines are superfluous.
216
217 Okay: spam(1)
218 W391: spam(1)\n
161 """219 """
162 if physical_line.strip() == '' and line_number == len(lines):220 if physical_line.strip() == '' and line_number == len(lines):
163 return 0, "W391 blank line at end of file"221 return 0, "W391 blank line at end of file"
@@ -182,9 +240,18 @@
182 For flowing long blocks of text (docstrings or comments), limiting the240 For flowing long blocks of text (docstrings or comments), limiting the
183 length to 72 characters is recommended.241 length to 72 characters is recommended.
184 """242 """
185 length = len(physical_line.rstrip())243 line = physical_line.rstrip()
186 if length > 79:244 length = len(line)
187 return 79, "E501 line too long (%d characters)" % length245 if length > MAX_LINE_LENGTH:
246 try:
247 # The line could contain multi-byte characters
248 if not hasattr(line, 'decode'): # Python 3
249 line = line.encode('latin-1')
250 length = len(line.decode('utf-8'))
251 except UnicodeDecodeError:
252 pass
253 if length > MAX_LINE_LENGTH:
254 return MAX_LINE_LENGTH, "E501 line too long (%d characters)" % length
188255
189256
190##############################################################################257##############################################################################
@@ -193,8 +260,9 @@
193260
194261
195def blank_lines(logical_line, blank_lines, indent_level, line_number,262def blank_lines(logical_line, blank_lines, indent_level, line_number,
196 previous_logical):263 previous_logical, previous_indent_level,
197 """264 blank_lines_before_comment):
265 r"""
198 Separate top-level function and class definitions with two blank lines.266 Separate top-level function and class definitions with two blank lines.
199267
200 Method definitions inside a class are separated by a single blank line.268 Method definitions inside a class are separated by a single blank line.
@@ -204,20 +272,33 @@
204 one-liners (e.g. a set of dummy implementations).272 one-liners (e.g. a set of dummy implementations).
205273
206 Use blank lines in functions, sparingly, to indicate logical sections.274 Use blank lines in functions, sparingly, to indicate logical sections.
275
276 Okay: def a():\n pass\n\n\ndef b():\n pass
277 Okay: def a():\n pass\n\n\n# Foo\n# Bar\n\ndef b():\n pass
278
279 E301: class Foo:\n b = 0\n def bar():\n pass
280 E302: def a():\n pass\n\ndef b(n):\n pass
281 E303: def a():\n pass\n\n\n\ndef b(n):\n pass
282 E303: def a():\n\n\n\n pass
283 E304: @decorator\n\ndef a():\n pass
207 """284 """
208 if line_number == 1:285 if line_number == 1:
209 return # Don't expect blank lines before the first line286 return # Don't expect blank lines before the first line
287 max_blank_lines = max(blank_lines, blank_lines_before_comment)
210 if previous_logical.startswith('@'):288 if previous_logical.startswith('@'):
211 return # Don't expect blank lines after function decorator289 if max_blank_lines:
212 if (logical_line.startswith('def ') or290 return 0, "E304 blank lines found after function decorator"
213 logical_line.startswith('class ') or291 elif max_blank_lines > 2 or (indent_level and max_blank_lines == 2):
214 logical_line.startswith('@')):292 return 0, "E303 too many blank lines (%d)" % max_blank_lines
215 if indent_level > 0 and blank_lines != 1:293 elif (logical_line.startswith('def ') or
216 return 0, "E301 expected 1 blank line, found %d" % blank_lines294 logical_line.startswith('class ') or
217 if indent_level == 0 and blank_lines != 2:295 logical_line.startswith('@')):
218 return 0, "E302 expected 2 blank lines, found %d" % blank_lines296 if indent_level:
219 if blank_lines > 2:297 if not (max_blank_lines or previous_indent_level < indent_level or
220 return 0, "E303 too many blank lines (%d)" % blank_lines298 DOCSTRING_REGEX.match(previous_logical)):
299 return 0, "E301 expected 1 blank line, found 0"
300 elif max_blank_lines != 2:
301 return 0, "E302 expected 2 blank lines, found %d" % max_blank_lines
221302
222303
223def extraneous_whitespace(logical_line):304def extraneous_whitespace(logical_line):
@@ -227,45 +308,75 @@
227 - Immediately inside parentheses, brackets or braces.308 - Immediately inside parentheses, brackets or braces.
228309
229 - Immediately before a comma, semicolon, or colon.310 - Immediately before a comma, semicolon, or colon.
311
312 Okay: spam(ham[1], {eggs: 2})
313 E201: spam( ham[1], {eggs: 2})
314 E201: spam(ham[ 1], {eggs: 2})
315 E201: spam(ham[1], { eggs: 2})
316 E202: spam(ham[1], {eggs: 2} )
317 E202: spam(ham[1 ], {eggs: 2})
318 E202: spam(ham[1], {eggs: 2 })
319
320 E203: if x == 4: print x, y; x, y = y , x
321 E203: if x == 4: print x, y ; x, y = y, x
322 E203: if x == 4 : print x, y; x, y = y, x
230 """323 """
231 line = logical_line324 line = logical_line
232 for char in '([{':325 for match in EXTRANEOUS_WHITESPACE_REGEX.finditer(line):
233 found = line.find(char + ' ')326 text = match.group()
234 if found > -1:327 char = text.strip()
328 found = match.start()
329 if text == char + ' ' and char in '([{':
235 return found + 1, "E201 whitespace after '%s'" % char330 return found + 1, "E201 whitespace after '%s'" % char
236 for char in '}])':331 if text == ' ' + char and line[found - 1] != ',':
237 found = line.find(' ' + char)332 if char in '}])':
238 if found > -1 and line[found - 1] != ',':333 return found, "E202 whitespace before '%s'" % char
239 return found, "E202 whitespace before '%s'" % char334 if char in ',;:':
240 for char in ',;:':335 return found, "E203 whitespace before '%s'" % char
241 found = line.find(' ' + char)
242 if found > -1:
243 return found, "E203 whitespace before '%s'" % char
244336
245337
246def missing_whitespace(logical_line):338def missing_whitespace(logical_line):
247 """339 """
248 JCR: Each comma, semicolon or colon should be followed by whitespace.340 JCR: Each comma, semicolon or colon should be followed by whitespace.
341
342 Okay: [a, b]
343 Okay: (3,)
344 Okay: a[1:4]
345 Okay: a[:4]
346 Okay: a[1:]
347 Okay: a[1:4:2]
348 E231: ['a','b']
349 E231: foo(bar,baz)
249 """350 """
250 line = logical_line351 line = logical_line
251 for index in range(len(line) - 1):352 for index in range(len(line) - 1):
252 char = line[index]353 char = line[index]
253 if char in ',;:' and line[index + 1] != ' ':354 if char in ',;:' and line[index + 1] not in WHITESPACE:
254 before = line[:index]355 before = line[:index]
255 if char == ':' and before.count('[') > before.count(']'):356 if char == ':' and before.count('[') > before.count(']'):
256 continue # Slice syntax, no space required357 continue # Slice syntax, no space required
257 if char == ',' and line[index + 1] == ')':358 if char == ',' and line[index + 1] == ')':
258 continue # Singleton tuple syntax, no space required359 continue # Allow tuple with only one element: (3,)
259 return index, "E231 missing whitespace after '%s'" % char360 return index, "E231 missing whitespace after '%s'" % char
260361
261362
262def indentation(logical_line, previous_logical, indent_char,363def indentation(logical_line, previous_logical, indent_char,
263 indent_level, previous_indent_level):364 indent_level, previous_indent_level):
264 """365 r"""
265 Use 4 spaces per indentation level.366 Use 4 spaces per indentation level.
266367
267 For really old code that you don't want to mess up, you can continue to368 For really old code that you don't want to mess up, you can continue to
268 use 8-space tabs.369 use 8-space tabs.
370
371 Okay: a = 1
372 Okay: if a == 0:\n a = 1
373 E111: a = 1
374
375 Okay: for item in items:\n pass
376 E112: for item in items:\npass
377
378 Okay: a = 1\nb = 2
379 E113: a = 1\n b = 2
269 """380 """
270 if indent_char == ' ' and indent_level % 4:381 if indent_char == ' ' and indent_level % 4:
271 return 0, "E111 indentation is not a multiple of four"382 return 0, "E111 indentation is not a multiple of four"
@@ -285,6 +396,13 @@
285396
286 - Immediately before the open parenthesis that starts an indexing or397 - Immediately before the open parenthesis that starts an indexing or
287 slicing.398 slicing.
399
400 Okay: spam(1)
401 E211: spam (1)
402
403 Okay: dict['key'] = list[index]
404 E211: dict ['key'] = list[index]
405 E211: dict['key'] = list [index]
288 """406 """
289 prev_type = tokens[0][0]407 prev_type = tokens[0][0]
290 prev_text = tokens[0][1]408 prev_text = tokens[0][1]
@@ -294,9 +412,11 @@
294 if (token_type == tokenize.OP and412 if (token_type == tokenize.OP and
295 text in '([' and413 text in '([' and
296 start != prev_end and414 start != prev_end and
297 prev_type == tokenize.NAME and415 (prev_type == tokenize.NAME or prev_text in '}])') and
416 # Syntax "class A (B):" is allowed, but avoid it
298 (index < 2 or tokens[index - 2][1] != 'class') and417 (index < 2 or tokens[index - 2][1] != 'class') and
299 (not iskeyword(prev_text))):418 # Allow "return (a.foo for a in range(5))"
419 (not keyword.iskeyword(prev_text))):
300 return prev_end, "E211 whitespace before '%s'" % text420 return prev_end, "E211 whitespace before '%s'" % text
301 prev_type = token_type421 prev_type = token_type
302 prev_text = text422 prev_text = text
@@ -309,21 +429,97 @@
309429
310 - More than one space around an assignment (or other) operator to430 - More than one space around an assignment (or other) operator to
311 align it with another.431 align it with another.
312 """432
313 line = logical_line433 Okay: a = 12 + 3
314 for operator in operators:434 E221: a = 4 + 5
315 found = line.find(' ' + operator)435 E222: a = 4 + 5
316 if found > -1:436 E223: a = 4\t+ 5
317 return found, "E221 multiple spaces before operator"437 E224: a = 4 +\t5
318 found = line.find(operator + ' ')438 """
319 if found > -1:439 for match in WHITESPACE_AROUND_OPERATOR_REGEX.finditer(logical_line):
320 return found, "E222 multiple spaces after operator"440 before, whitespace, after = match.groups()
321 found = line.find('\t' + operator)441 tab = whitespace == '\t'
322 if found > -1:442 offset = match.start(2)
323 return found, "E223 tab before operator"443 if before in OPERATORS:
324 found = line.find(operator + '\t')444 return offset, (tab and "E224 tab after operator" or
325 if found > -1:445 "E222 multiple spaces after operator")
326 return found, "E224 tab after operator"446 elif after in OPERATORS:
447 return offset, (tab and "E223 tab before operator" or
448 "E221 multiple spaces before operator")
449
450
451def missing_whitespace_around_operator(logical_line, tokens):
452 r"""
453 - Always surround these binary operators with a single space on
454 either side: assignment (=), augmented assignment (+=, -= etc.),
455 comparisons (==, <, >, !=, <>, <=, >=, in, not in, is, is not),
456 Booleans (and, or, not).
457
458 - Use spaces around arithmetic operators.
459
460 Okay: i = i + 1
461 Okay: submitted += 1
462 Okay: x = x * 2 - 1
463 Okay: hypot2 = x * x + y * y
464 Okay: c = (a + b) * (a - b)
465 Okay: foo(bar, key='word', *args, **kwargs)
466 Okay: baz(**kwargs)
467 Okay: negative = -1
468 Okay: spam(-1)
469 Okay: alpha[:-i]
470 Okay: if not -5 < x < +5:\n pass
471 Okay: lambda *args, **kw: (args, kw)
472
473 E225: i=i+1
474 E225: submitted +=1
475 E225: x = x*2 - 1
476 E225: hypot2 = x*x + y*y
477 E225: c = (a+b) * (a-b)
478 E225: c = alpha -4
479 E225: z = x **y
480 """
481 parens = 0
482 need_space = False
483 prev_type = tokenize.OP
484 prev_text = prev_end = None
485 for token_type, text, start, end, line in tokens:
486 if token_type in (tokenize.NL, tokenize.NEWLINE, tokenize.ERRORTOKEN):
487 # ERRORTOKEN is triggered by backticks in Python 3000
488 continue
489 if text in ('(', 'lambda'):
490 parens += 1
491 elif text == ')':
492 parens -= 1
493 if need_space:
494 if start != prev_end:
495 need_space = False
496 elif text == '>' and prev_text == '<':
497 # Tolerate the "<>" operator, even if running Python 3
498 pass
499 else:
500 return prev_end, "E225 missing whitespace around operator"
501 elif token_type == tokenize.OP and prev_end is not None:
502 if text == '=' and parens:
503 # Allow keyword args or defaults: foo(bar=None).
504 pass
505 elif text in BINARY_OPERATORS:
506 need_space = True
507 elif text in UNARY_OPERATORS:
508 # Allow unary operators: -123, -x, +1.
509 # Allow argument unpacking: foo(*args, **kwargs).
510 if prev_type == tokenize.OP:
511 if prev_text in '}])':
512 need_space = True
513 elif prev_type == tokenize.NAME:
514 if prev_text not in E225NOT_KEYWORDS:
515 need_space = True
516 else:
517 need_space = True
518 if need_space and start == prev_end:
519 return prev_end, "E225 missing whitespace around operator"
520 prev_type = token_type
521 prev_text = text
522 prev_end = end
327523
328524
329def whitespace_around_comma(logical_line):525def whitespace_around_comma(logical_line):
@@ -334,6 +530,11 @@
334 align it with another.530 align it with another.
335531
336 JCR: This should also be applied around comma etc.532 JCR: This should also be applied around comma etc.
533 Note: these checks are disabled by default
534
535 Okay: a = (1, 2)
536 E241: a = (1, 2)
537 E242: a = (1,\t2)
337 """538 """
338 line = logical_line539 line = logical_line
339 for separator in ',;:':540 for separator in ',;:':
@@ -345,9 +546,77 @@
345 return found + 1, "E242 tab after '%s'" % separator546 return found + 1, "E242 tab after '%s'" % separator
346547
347548
549def whitespace_around_named_parameter_equals(logical_line):
550 """
551 Don't use spaces around the '=' sign when used to indicate a
552 keyword argument or a default parameter value.
553
554 Okay: def complex(real, imag=0.0):
555 Okay: return magic(r=real, i=imag)
556 Okay: boolean(a == b)
557 Okay: boolean(a != b)
558 Okay: boolean(a <= b)
559 Okay: boolean(a >= b)
560
561 E251: def complex(real, imag = 0.0):
562 E251: return magic(r = real, i = imag)
563 """
564 parens = 0
565 for match in WHITESPACE_AROUND_NAMED_PARAMETER_REGEX.finditer(
566 logical_line):
567 text = match.group()
568 if parens and len(text) == 3:
569 issue = "E251 no spaces around keyword / parameter equals"
570 return match.start(), issue
571 if text == '(':
572 parens += 1
573 elif text == ')':
574 parens -= 1
575
576
577def whitespace_before_inline_comment(logical_line, tokens):
578 """
579 Separate inline comments by at least two spaces.
580
581 An inline comment is a comment on the same line as a statement. Inline
582 comments should be separated by at least two spaces from the statement.
583 They should start with a # and a single space.
584
585 Okay: x = x + 1 # Increment x
586 Okay: x = x + 1 # Increment x
587 E261: x = x + 1 # Increment x
588 E262: x = x + 1 #Increment x
589 E262: x = x + 1 # Increment x
590 """
591 prev_end = (0, 0)
592 for token_type, text, start, end, line in tokens:
593 if token_type == tokenize.NL:
594 continue
595 if token_type == tokenize.COMMENT:
596 if not line[:start[1]].strip():
597 continue
598 if prev_end[0] == start[0] and start[1] < prev_end[1] + 2:
599 return (prev_end,
600 "E261 at least two spaces before inline comment")
601 if (len(text) > 1 and text.startswith('# ')
602 or not text.startswith('# ')):
603 return start, "E262 inline comment should start with '# '"
604 else:
605 prev_end = end
606
607
348def imports_on_separate_lines(logical_line):608def imports_on_separate_lines(logical_line):
349 """609 r"""
350 Imports should usually be on separate lines.610 Imports should usually be on separate lines.
611
612 Okay: import os\nimport sys
613 E401: import sys, os
614
615 Okay: from subprocess import Popen, PIPE
616 Okay: from myclas import MyClass
617 Okay: from foo.bar.yourclass import YourClass
618 Okay: import myclass
619 Okay: import foo.bar.yourclass
351 """620 """
352 line = logical_line621 line = logical_line
353 if line.startswith('import '):622 if line.startswith('import '):
@@ -357,17 +626,37 @@
357626
358627
359def compound_statements(logical_line):628def compound_statements(logical_line):
360 """629 r"""
361 Compound statements (multiple statements on the same line) are630 Compound statements (multiple statements on the same line) are
362 generally discouraged.631 generally discouraged.
632
633 While sometimes it's okay to put an if/for/while with a small body
634 on the same line, never do this for multi-clause statements. Also
635 avoid folding such long lines!
636
637 Okay: if foo == 'blah':\n do_blah_thing()
638 Okay: do_one()
639 Okay: do_two()
640 Okay: do_three()
641
642 E701: if foo == 'blah': do_blah_thing()
643 E701: for x in lst: total += x
644 E701: while t < 10: t = delay()
645 E701: if foo == 'blah': do_blah_thing()
646 E701: else: do_non_blah_thing()
647 E701: try: something()
648 E701: finally: cleanup()
649 E701: if foo == 'blah': one(); two(); three()
650
651 E702: do_one(); do_two(); do_three()
363 """652 """
364 line = logical_line653 line = logical_line
365 found = line.find(':')654 found = line.find(':')
366 if -1 < found < len(line) - 1:655 if -1 < found < len(line) - 1:
367 before = line[:found]656 before = line[:found]
368 if (before.count('{') <= before.count('}') and # {'a': 1} (dict)657 if (before.count('{') <= before.count('}') and # {'a': 1} (dict)
369 before.count('[') <= before.count(']') and # [1:2] (slice)658 before.count('[') <= before.count(']') and # [1:2] (slice)
370 not re.search(r'\blambda\b', before)): # lambda x: x659 not re.search(r'\blambda\b', before)): # lambda x: x
371 return found, "E701 multiple statements on one line (colon)"660 return found, "E701 multiple statements on one line (colon)"
372 found = line.find(';')661 found = line.find(';')
373 if -1 < found:662 if -1 < found:
@@ -397,16 +686,49 @@
397 continuation characters thanks to the containing parentheses. The older686 continuation characters thanks to the containing parentheses. The older
398 form will be removed in Python 3000.687 form will be removed in Python 3000.
399 """688 """
400 match = raise_comma_match(logical_line)689 match = RAISE_COMMA_REGEX.match(logical_line)
401 if match:690 if match:
402 return match.start(1), "W602 deprecated form of raising exception"691 return match.start(1), "W602 deprecated form of raising exception"
403692
404693
694def python_3000_not_equal(logical_line):
695 """
696 != can also be written <>, but this is an obsolete usage kept for
697 backwards compatibility only. New code should always use !=.
698 The older syntax is removed in Python 3000.
699 """
700 pos = logical_line.find('<>')
701 if pos > -1:
702 return pos, "W603 '<>' is deprecated, use '!='"
703
704
705def python_3000_backticks(logical_line):
706 """
707 Backticks are removed in Python 3000.
708 Use repr() instead.
709 """
710 pos = logical_line.find('`')
711 if pos > -1:
712 return pos, "W604 backticks are deprecated, use 'repr()'"
713
714
405##############################################################################715##############################################################################
406# Helper functions716# Helper functions
407##############################################################################717##############################################################################
408718
409719
720if '' == ''.encode():
721 # Python 2: implicit encoding.
722 def readlines(filename):
723 return open(filename).readlines()
724else:
725 # Python 3: decode to latin-1.
726 # This function is lazy, it does not read the encoding declaration.
727 # XXX: use tokenize.detect_encoding()
728 def readlines(filename):
729 return open(filename, encoding='latin-1').readlines()
730
731
410def expand_indent(line):732def expand_indent(line):
411 """733 """
412 Return the amount of indentation.734 Return the amount of indentation.
@@ -426,7 +748,7 @@
426 result = 0748 result = 0
427 for char in line:749 for char in line:
428 if char == '\t':750 if char == '\t':
429 result = result / 8 * 8 + 8751 result = result // 8 * 8 + 8
430 elif char == ' ':752 elif char == ' ':
431 result += 1753 result += 1
432 else:754 else:
@@ -434,34 +756,6 @@
434 return result756 return result
435757
436758
437##############################################################################
438# Framework to run all checks
439##############################################################################
440
441
442def message(text):
443 """Print a message."""
444 # print >> sys.stderr, options.prog + ': ' + text
445 # print >> sys.stderr, text
446 print text
447
448
449def find_checks(argument_name):
450 """
451 Find all globally visible functions where the first argument name
452 starts with argument_name.
453 """
454 checks = []
455 function_type = type(find_checks)
456 for name, function in globals().iteritems():
457 if type(function) is function_type:
458 args = inspect.getargspec(function)[0]
459 if len(args) >= 1 and args[0].startswith(argument_name):
460 checks.append((name, function, args))
461 checks.sort()
462 return checks
463
464
465def mute_string(text):759def mute_string(text):
466 """760 """
467 Replace contents with 'xxx' to prevent syntax matching.761 Replace contents with 'xxx' to prevent syntax matching.
@@ -487,18 +781,53 @@
487 return text[:start] + 'x' * (end - start) + text[end:]781 return text[:start] + 'x' * (end - start) + text[end:]
488782
489783
490class Checker:784def message(text):
785 """Print a message."""
786 # print >> sys.stderr, options.prog + ': ' + text
787 # print >> sys.stderr, text
788 print(text)
789
790
791##############################################################################
792# Framework to run all checks
793##############################################################################
794
795
796def find_checks(argument_name):
797 """
798 Find all globally visible functions where the first argument name
799 starts with argument_name.
800 """
801 checks = []
802 for name, function in globals().items():
803 if not inspect.isfunction(function):
804 continue
805 args = inspect.getargspec(function)[0]
806 if args and args[0].startswith(argument_name):
807 codes = ERRORCODE_REGEX.findall(inspect.getdoc(function) or '')
808 for code in codes or ['']:
809 if not code or not ignore_code(code):
810 checks.append((name, function, args))
811 break
812 checks.sort()
813 return checks
814
815
816class Checker(object):
491 """817 """
492 Load a Python source file, tokenize it, check coding style.818 Load a Python source file, tokenize it, check coding style.
493 """819 """
494820
495 def __init__(self, filename):821 def __init__(self, filename, lines=None):
496 self.filename = filename822 self.filename = filename
497 self.lines = file(filename).readlines()823 if filename is None:
498 self.physical_checks = find_checks('physical_line')824 self.filename = 'stdin'
499 self.logical_checks = find_checks('logical_line')825 self.lines = lines or []
500 options.counters['physical lines'] = \826 elif lines is None:
501 options.counters.get('physical lines', 0) + len(self.lines)827 self.lines = readlines(filename)
828 else:
829 self.lines = lines
830 options.counters['physical lines'] += len(self.lines)
502831
503 def readline(self):832 def readline(self):
504 """833 """
@@ -535,7 +864,7 @@
535 self.physical_line = line864 self.physical_line = line
536 if self.indent_char is None and len(line) and line[0] in ' \t':865 if self.indent_char is None and len(line) and line[0] in ' \t':
537 self.indent_char = line[0]866 self.indent_char = line[0]
538 for name, check, argument_names in self.physical_checks:867 for name, check, argument_names in options.physical_checks:
539 result = self.run_check(check, argument_names)868 result = self.run_check(check, argument_names)
540 if result is not None:869 if result is not None:
541 offset, text = result870 offset, text = result
@@ -551,20 +880,20 @@
551 previous = None880 previous = None
552 for token in self.tokens:881 for token in self.tokens:
553 token_type, text = token[0:2]882 token_type, text = token[0:2]
554 if token_type in (tokenize.COMMENT, tokenize.NL,883 if token_type in SKIP_TOKENS:
555 tokenize.INDENT, tokenize.DEDENT,
556 tokenize.NEWLINE):
557 continue884 continue
558 if token_type == tokenize.STRING:885 if token_type == tokenize.STRING:
559 text = mute_string(text)886 text = mute_string(text)
560 if previous:887 if previous:
561 end_line, end = previous[3]888 end_line, end = previous[3]
562 start_line, start = token[2]889 start_line, start = token[2]
563 if end_line != start_line: # different row890 if end_line != start_line: # different row
564 if self.lines[end_line - 1][end - 1] not in '{[(':891 prev_text = self.lines[end_line - 1][end - 1]
892 if prev_text == ',' or (prev_text not in '{[('
893 and text not in '}])'):
565 logical.append(' ')894 logical.append(' ')
566 length += 1895 length += 1
567 elif end != start: # different column896 elif end != start: # different column
568 fill = self.lines[end_line - 1][end:start]897 fill = self.lines[end_line - 1][end:start]
569 logical.append(fill)898 logical.append(fill)
570 length += len(fill)899 length += len(fill)
@@ -580,22 +909,21 @@
580 """909 """
581 Build a line from tokens and run all logical checks on it.910 Build a line from tokens and run all logical checks on it.
582 """911 """
583 options.counters['logical lines'] = \912 options.counters['logical lines'] += 1
584 options.counters.get('logical lines', 0) + 1
585 self.build_tokens_line()913 self.build_tokens_line()
586 first_line = self.lines[self.mapping[0][1][2][0] - 1]914 first_line = self.lines[self.mapping[0][1][2][0] - 1]
587 indent = first_line[:self.mapping[0][1][2][1]]915 indent = first_line[:self.mapping[0][1][2][1]]
588 self.previous_indent_level = self.indent_level916 self.previous_indent_level = self.indent_level
589 self.indent_level = expand_indent(indent)917 self.indent_level = expand_indent(indent)
590 if options.verbose >= 2:918 if options.verbose >= 2:
591 print self.logical_line[:80].rstrip()919 print(self.logical_line[:80].rstrip())
592 for name, check, argument_names in self.logical_checks:920 for name, check, argument_names in options.logical_checks:
593 if options.verbose >= 3:921 if options.verbose >= 4:
594 print ' ', name922 print(' ' + name)
595 result = self.run_check(check, argument_names)923 result = self.run_check(check, argument_names)
596 if result is not None:924 if result is not None:
597 offset, text = result925 offset, text = result
598 if type(offset) is tuple:926 if isinstance(offset, tuple):
599 original_number, original_offset = offset927 original_number, original_offset = offset
600 else:928 else:
601 for token_offset, token in self.mapping:929 for token_offset, token in self.mapping:
@@ -607,20 +935,29 @@
607 text, check)935 text, check)
608 self.previous_logical = self.logical_line936 self.previous_logical = self.logical_line
609937
610 def check_all(self):938 def check_all(self, expected=None, line_offset=0):
611 """939 """
612 Run all checks on the input file.940 Run all checks on the input file.
613 """941 """
942 self.expected = expected or ()
943 self.line_offset = line_offset
944 self.line_number = 0
614 self.file_errors = 0945 self.file_errors = 0
615 self.line_number = 0
616 self.indent_char = None946 self.indent_char = None
617 self.indent_level = 0947 self.indent_level = 0
618 self.previous_logical = ''948 self.previous_logical = ''
619 self.blank_lines = 0949 self.blank_lines = 0
950 self.blank_lines_before_comment = 0
620 self.tokens = []951 self.tokens = []
621 parens = 0952 parens = 0
622 for token in tokenize.generate_tokens(self.readline_check_physical):953 for token in tokenize.generate_tokens(self.readline_check_physical):
623 # print tokenize.tok_name[token[0]], repr(token)954 if options.verbose >= 3:
955 if token[2][0] == token[3][0]:
956 pos = '[%s:%s]' % (token[2][1] or '', token[3][1])
957 else:
958 pos = 'l.%s' % token[3][0]
959 print('l.%s\t%s\t%s\t%r' %
960 (token[2][0], pos, tokenize.tok_name[token[0]], token[1]))
624 self.tokens.append(token)961 self.tokens.append(token)
625 token_type, text = token[0:2]962 token_type, text = token[0:2]
626 if token_type == tokenize.OP and text in '([{':963 if token_type == tokenize.OP and text in '([{':
@@ -630,40 +967,49 @@
630 if token_type == tokenize.NEWLINE and not parens:967 if token_type == tokenize.NEWLINE and not parens:
631 self.check_logical()968 self.check_logical()
632 self.blank_lines = 0969 self.blank_lines = 0
970 self.blank_lines_before_comment = 0
633 self.tokens = []971 self.tokens = []
634 if token_type == tokenize.NL and not parens:972 if token_type == tokenize.NL and not parens:
635 self.blank_lines += 1973 if len(self.tokens) <= 1:
974 # The physical line contains only this token.
975 self.blank_lines += 1
636 self.tokens = []976 self.tokens = []
637 if token_type == tokenize.COMMENT:977 if token_type == tokenize.COMMENT:
638 source_line = token[4]978 source_line = token[4]
639 token_start = token[2][1]979 token_start = token[2][1]
640 if source_line[:token_start].strip() == '':980 if source_line[:token_start].strip() == '':
981 self.blank_lines_before_comment = max(self.blank_lines,
982 self.blank_lines_before_comment)
641 self.blank_lines = 0983 self.blank_lines = 0
984 if text.endswith('\n') and not parens:
985 # The comment also ends a physical line. This works around
986 # Python < 2.6 behaviour, which does not generate NL after
987 # a comment which is on a line by itself.
988 self.tokens = []
642 return self.file_errors989 return self.file_errors
643990
644 def report_error(self, line_number, offset, text, check):991 def report_error(self, line_number, offset, text, check):
645 """992 """
646 Report an error, according to options.993 Report an error, according to options.
647 """994 """
995 code = text[:4]
996 if ignore_code(code):
997 return
648 if options.quiet == 1 and not self.file_errors:998 if options.quiet == 1 and not self.file_errors:
649 message(self.filename)999 message(self.filename)
1000 if code in options.counters:
1001 options.counters[code] += 1
1002 else:
1003 options.counters[code] = 1
1004 options.messages[code] = text[5:]
1005 if options.quiet or code in self.expected:
1006 # Don't care about expected errors or warnings
1007 return
650 self.file_errors += 11008 self.file_errors += 1
651 code = text[:4]
652 options.counters[code] = options.counters.get(code, 0) + 1
653 options.messages[code] = text[5:]
654 if options.quiet:
655 return
656 if options.testsuite:
657 base = os.path.basename(self.filename)[:4]
658 if base == code:
659 return
660 if base[0] == 'E' and code[0] == 'W':
661 return
662 if ignore_code(code):
663 return
664 if options.counters[code] == 1 or options.repeat:1009 if options.counters[code] == 1 or options.repeat:
665 message("%s:%s:%d: %s" %1010 message("%s:%s:%d: %s" %
666 (self.filename, line_number, offset + 1, text))1011 (self.filename, self.line_offset + line_number,
1012 offset + 1, text))
667 if options.show_source:1013 if options.show_source:
668 line = self.lines[line_number - 1]1014 line = self.lines[line_number - 1]
669 message(line.rstrip())1015 message(line.rstrip())
@@ -676,35 +1022,33 @@
676 """1022 """
677 Run all checks on a Python source file.1023 Run all checks on a Python source file.
678 """1024 """
679 if excluded(filename) or not filename_match(filename):
680 return {}
681 if options.verbose:1025 if options.verbose:
682 message('checking ' + filename)1026 message('checking ' + filename)
683 options.counters['files'] = options.counters.get('files', 0) + 1
684 errors = Checker(filename).check_all()1027 errors = Checker(filename).check_all()
685 if options.testsuite and not errors:1028
686 message("%s: %s" % (filename, "no errors found"))1029
6871030def input_dir(dirname, runner=None):
688
689def input_dir(dirname):
690 """1031 """
691 Check all Python source files in this directory and all subdirectories.1032 Check all Python source files in this directory and all subdirectories.
692 """1033 """
693 dirname = dirname.rstrip('/')1034 dirname = dirname.rstrip('/')
694 if excluded(dirname):1035 if excluded(dirname):
695 return1036 return
1037 if runner is None:
1038 runner = input_file
696 for root, dirs, files in os.walk(dirname):1039 for root, dirs, files in os.walk(dirname):
697 if options.verbose:1040 if options.verbose:
698 message('directory ' + root)1041 message('directory ' + root)
699 options.counters['directories'] = \1042 options.counters['directories'] += 1
700 options.counters.get('directories', 0) + 1
701 dirs.sort()1043 dirs.sort()
702 for subdir in dirs:1044 for subdir in dirs:
703 if excluded(subdir):1045 if excluded(subdir):
704 dirs.remove(subdir)1046 dirs.remove(subdir)
705 files.sort()1047 files.sort()
706 for filename in files:1048 for filename in files:
707 input_file(os.path.join(root, filename))1049 if filename_match(filename) and not excluded(filename):
1050 options.counters['files'] += 1
1051 runner(os.path.join(root, filename))
7081052
7091053
710def excluded(filename):1054def excluded(filename):
@@ -733,12 +1077,23 @@
733def ignore_code(code):1077def ignore_code(code):
734 """1078 """
735 Check if options.ignore contains a prefix of the error code.1079 Check if options.ignore contains a prefix of the error code.
1080 If options.select contains a prefix of the error code, do not ignore it.
736 """1081 """
1082 for select in options.select:
1083 if code.startswith(select):
1084 return False
737 for ignore in options.ignore:1085 for ignore in options.ignore:
738 if code.startswith(ignore):1086 if code.startswith(ignore):
739 return True1087 return True
7401088
7411089
1090def reset_counters():
1091 for key in list(options.counters.keys()):
1092 if key not in BENCHMARK_KEYS:
1093 del options.counters[key]
1094 options.messages = {}
1095
1096
742def get_error_statistics():1097def get_error_statistics():
743 """Get error statistics."""1098 """Get error statistics."""
744 return get_statistics("E")1099 return get_statistics("E")
@@ -759,7 +1114,7 @@
759 prefix='E4' matches all errors that have to do with imports1114 prefix='E4' matches all errors that have to do with imports
760 """1115 """
761 stats = []1116 stats = []
762 keys = options.messages.keys()1117 keys = list(options.messages.keys())
763 keys.sort()1118 keys.sort()
764 for key in keys:1119 for key in keys:
765 if key.startswith(prefix):1120 if key.startswith(prefix):
@@ -768,24 +1123,131 @@
768 return stats1123 return stats
7691124
7701125
1126def get_count(prefix=''):
1127 """Return the total count of errors and warnings."""
1128 keys = list(options.messages.keys())
1129 count = 0
1130 for key in keys:
1131 if key.startswith(prefix):
1132 count += options.counters[key]
1133 return count
1134
1135
771def print_statistics(prefix=''):1136def print_statistics(prefix=''):
772 """Print overall statistics (number of errors and warnings)."""1137 """Print overall statistics (number of errors and warnings)."""
773 for line in get_statistics(prefix):1138 for line in get_statistics(prefix):
774 print line1139 print(line)
7751140
7761141
777def print_benchmark(elapsed):1142def print_benchmark(elapsed):
778 """1143 """
779 Print benchmark numbers.1144 Print benchmark numbers.
780 """1145 """
781 print '%-7.2f %s' % (elapsed, 'seconds elapsed')1146 print('%-7.2f %s' % (elapsed, 'seconds elapsed'))
782 keys = ['directories', 'files',1147 for key in BENCHMARK_KEYS:
783 'logical lines', 'physical lines']1148 print('%-7d %s per second (%d total)' % (
784 for key in keys:1149 options.counters[key] / elapsed, key,
785 if key in options.counters:1150 options.counters[key]))
786 print '%-7d %s per second (%d total)' % (1151
787 options.counters[key] / elapsed, key,1152
788 options.counters[key])1153def run_tests(filename):
1154 """
1155 Run all the tests from a file.
1156
1157 A test file can provide many tests. Each test starts with a declaration.
1158 This declaration is a single line starting with '#:'.
1159 It declares codes of expected failures, separated by spaces or 'Okay'
1160 if no failure is expected.
1161 If the file does not contain such declaration, it should pass all tests.
1162 If the declaration is empty, following lines are not checked, until next
1163 declaration.
1164
1165 Examples:
1166
1167 * Only E224 and W701 are expected: #: E224 W701
1168 * Following example is conform: #: Okay
1169 * Don't check these lines: #:
1170 """
1171 lines = readlines(filename) + ['#:\n']
1172 line_offset = 0
1173 codes = ['Okay']
1174 testcase = []
1175 for index, line in enumerate(lines):
1176 if not line.startswith('#:'):
1177 if codes:
1178 # Collect the lines of the test case
1179 testcase.append(line)
1180 continue
1181 if codes and index > 0:
1182 label = '%s:%s:1' % (filename, line_offset + 1)
1183 codes = [c for c in codes if c != 'Okay']
1184 # Run the checker
1185 errors = Checker(filename, testcase).check_all(codes, line_offset)
1186 # Check if the expected errors were found
1187 for code in codes:
1188 if not options.counters.get(code):
1189 errors += 1
1190 message('%s: error %s not found' % (label, code))
1191 if options.verbose and not errors:
1192 message('%s: passed (%s)' % (label, ' '.join(codes)))
1193 # Keep showing errors for multiple tests
1194 reset_counters()
1195 # output the real line numbers
1196 line_offset = index
1197 # configure the expected errors
1198 codes = line.split()[1:]
1199 # empty the test case buffer
1200 del testcase[:]
1201
1202
1203def selftest():
1204 """
1205 Test all check functions with test cases in docstrings.
1206 """
1207 count_passed = 0
1208 count_failed = 0
1209 checks = options.physical_checks + options.logical_checks
1210 for name, check, argument_names in checks:
1211 for line in check.__doc__.splitlines():
1212 line = line.lstrip()
1213 match = SELFTEST_REGEX.match(line)
1214 if match is None:
1215 continue
1216 code, source = match.groups()
1217 checker = Checker(None)
1218 for part in source.split(r'\n'):
1219 part = part.replace(r'\t', '\t')
1220 part = part.replace(r'\s', ' ')
1221 checker.lines.append(part + '\n')
1222 options.quiet = 2
1223 checker.check_all()
1224 error = None
1225 if code == 'Okay':
1226 if len(options.counters) > len(BENCHMARK_KEYS):
1227 codes = [key for key in options.counters.keys()
1228 if key not in BENCHMARK_KEYS]
1229 error = "incorrectly found %s" % ', '.join(codes)
1230 elif not options.counters.get(code):
1231 error = "failed to find %s" % code
1232 # Reset the counters
1233 reset_counters()
1234 if not error:
1235 count_passed += 1
1236 else:
1237 count_failed += 1
1238 if len(checker.lines) == 1:
1239 print("pep8.py: %s: %s" %
1240 (error, checker.lines[0].rstrip()))
1241 else:
1242 print("pep8.py: %s:" % error)
1243 for line in checker.lines:
1244 print(line.rstrip())
1245 if options.verbose:
1246 print("%d passed and %d failed." % (count_passed, count_failed))
1247 if count_failed:
1248 print("Test failed.")
1249 else:
1250 print("Test passed.")
7891251
7901252
791def process_options(arglist=None):1253def process_options(arglist=None):
@@ -793,26 +1255,36 @@
793 Process options passed either via arglist or via command line args.1255 Process options passed either via arglist or via command line args.
794 """1256 """
795 global options, args1257 global options, args
796 usage = "%prog [options] input ..."1258 parser = OptionParser(version=__version__,
797 parser = OptionParser(usage)1259 usage="%prog [options] input ...")
798 parser.add_option('-v', '--verbose', default=0, action='count',1260 parser.add_option('-v', '--verbose', default=0, action='count',
799 help="print status messages, or debug with -vv")1261 help="print status messages, or debug with -vv")
800 parser.add_option('-q', '--quiet', default=0, action='count',1262 parser.add_option('-q', '--quiet', default=0, action='count',
801 help="report only file names, or nothing with -qq")1263 help="report only file names, or nothing with -qq")
802 parser.add_option('--exclude', metavar='patterns', default=default_exclude,1264 parser.add_option('-r', '--repeat', action='store_true',
803 help="skip matches (default %s)" % default_exclude)1265 help="show all occurrences of the same error")
804 parser.add_option('--filename', metavar='patterns',1266 parser.add_option('--exclude', metavar='patterns', default=DEFAULT_EXCLUDE,
805 help="only check matching files (e.g. *.py)")1267 help="exclude files or directories which match these "
1268 "comma separated patterns (default: %s)" %
1269 DEFAULT_EXCLUDE)
1270 parser.add_option('--filename', metavar='patterns', default='*.py',
1271 help="when parsing directories, only check filenames "
1272 "matching these comma separated patterns (default: "
1273 "*.py)")
1274 parser.add_option('--select', metavar='errors', default='',
1275 help="select errors and warnings (e.g. E,W6)")
806 parser.add_option('--ignore', metavar='errors', default='',1276 parser.add_option('--ignore', metavar='errors', default='',
807 help="skip errors and warnings (e.g. E4,W)")1277 help="skip errors and warnings (e.g. E4,W)")
808 parser.add_option('--repeat', action='store_true',
809 help="show all occurrences of the same error")
810 parser.add_option('--show-source', action='store_true',1278 parser.add_option('--show-source', action='store_true',
811 help="show source code for each error")1279 help="show source code for each error")
812 parser.add_option('--show-pep8', action='store_true',1280 parser.add_option('--show-pep8', action='store_true',
813 help="show text of PEP 8 for each error")1281 help="show text of PEP 8 for each error")
814 parser.add_option('--statistics', action='store_true',1282 parser.add_option('--statistics', action='store_true',
815 help="count errors and warnings")1283 help="count errors and warnings")
1284 parser.add_option('--count', action='store_true',
1285 help="print total number of errors and warnings "
1286 "to standard error and set exit code to 1 if "
1287 "total is not null")
816 parser.add_option('--benchmark', action='store_true',1288 parser.add_option('--benchmark', action='store_true',
817 help="measure processing speed")1289 help="measure processing speed")
818 parser.add_option('--testsuite', metavar='dir',1290 parser.add_option('--testsuite', metavar='dir',
@@ -822,7 +1294,7 @@
822 options, args = parser.parse_args(arglist)1294 options, args = parser.parse_args(arglist)
823 if options.testsuite:1295 if options.testsuite:
824 args.append(options.testsuite)1296 args.append(options.testsuite)
825 if len(args) == 0:1297 if not args and not options.doctest:
826 parser.error('input not specified')1298 parser.error('input not specified')
827 options.prog = os.path.basename(sys.argv[0])1299 options.prog = os.path.basename(sys.argv[0])
828 options.exclude = options.exclude.split(',')1300 options.exclude = options.exclude.split(',')
@@ -830,13 +1302,25 @@
830 options.exclude[index] = options.exclude[index].rstrip('/')1302 options.exclude[index] = options.exclude[index].rstrip('/')
831 if options.filename:1303 if options.filename:
832 options.filename = options.filename.split(',')1304 options.filename = options.filename.split(',')
1305 if options.select:
1306 options.select = options.select.split(',')
1307 else:
1308 options.select = []
833 if options.ignore:1309 if options.ignore:
834 options.ignore = options.ignore.split(',')1310 options.ignore = options.ignore.split(',')
835 else:1311 elif options.select:
1312 # Ignore all checks which are not explicitly selected
1313 options.ignore = ['']
1314 elif options.testsuite or options.doctest:
1315 # For doctest and testsuite, all checks are required
836 options.ignore = []1316 options.ignore = []
837 options.counters = {}1317 else:
1318 # The default choice: ignore controversial checks
1319 options.ignore = DEFAULT_IGNORE.split(',')
1320 options.physical_checks = find_checks('physical_line')
1321 options.logical_checks = find_checks('logical_line')
1322 options.counters = dict.fromkeys(BENCHMARK_KEYS, 0)
838 options.messages = {}1323 options.messages = {}
839
840 return options, args1324 return options, args
8411325
8421326
@@ -847,18 +1331,29 @@
847 options, args = process_options()1331 options, args = process_options()
848 if options.doctest:1332 if options.doctest:
849 import doctest1333 import doctest
850 return doctest.testmod()1334 doctest.testmod(verbose=options.verbose)
1335 selftest()
1336 if options.testsuite:
1337 runner = run_tests
1338 else:
1339 runner = input_file
851 start_time = time.time()1340 start_time = time.time()
852 for path in args:1341 for path in args:
853 if os.path.isdir(path):1342 if os.path.isdir(path):
854 input_dir(path)1343 input_dir(path, runner=runner)
855 else:1344 elif not excluded(path):
856 input_file(path)1345 options.counters['files'] += 1
1346 runner(path)
857 elapsed = time.time() - start_time1347 elapsed = time.time() - start_time
858 if options.statistics:1348 if options.statistics:
859 print_statistics()1349 print_statistics()
860 if options.benchmark:1350 if options.benchmark:
861 print_benchmark(elapsed)1351 print_benchmark(elapsed)
1352 count = get_count()
1353 if count:
1354 if options.count:
1355 sys.stderr.write(str(count) + '\n')
1356 sys.exit(1)
8621357
8631358
864if __name__ == '__main__':1359if __name__ == '__main__':
8651360
=== modified file 'pocketlint/formatcheck.py'
--- pocketlint/formatcheck.py 2011-04-15 13:37:44 +0000
+++ pocketlint/formatcheck.py 2011-10-14 21:24:26 +0000
@@ -3,10 +3,15 @@
3# This software is licensed under the MIT license (see the file COPYING).3# This software is licensed under the MIT license (see the file COPYING).
4"""Check for syntax and style problems."""4"""Check for syntax and style problems."""
55
6from __future__ import with_statement
7
8__metaclass__ = type6__metaclass__ = type
97
8
9__all__ = [
10 'Reporter',
11 'UniversalChecker',
12 ]
13
14
10import compiler15import compiler
11import htmlentitydefs16import htmlentitydefs
12import logging17import logging
@@ -31,26 +36,39 @@
31from formatdoctest import DoctestReviewer36from formatdoctest import DoctestReviewer
3237
33import contrib.pep8 as pep838import contrib.pep8 as pep8
34from contrib.pyflakes.checker import Checker39from contrib.cssccc import CSSCodingConventionChecker
40from contrib.pyflakes.checker import Checker as PyFlakesChecker
35try:41try:
36 import cssutils42 import cssutils
37 HAS_CSSUTILS = True43 HAS_CSSUTILS = True
38except ImportError:44except ImportError:
39 HAS_CSSUTILS = False45 HAS_CSSUTILS = False
4046
41# Javascript checking is available if spider money's js is available.47
42js = subprocess.Popen(48def find_exec(names):
43 ['which', 'js'], stdout=subprocess.PIPE, stderr=subprocess.PIPE)49 """Return the name of a GI enabled JS interpreter."""
44js_exec, ignore = js.communicate()50 for name in names:
45if js.returncode != 0:51 js = subprocess.Popen(
46 JS = None52 ['which', name], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
47else:53 js_exec, ignore = js.communicate()
48 JS = js_exec.strip()54 if js.returncode == 0:
4955 return js_exec.strip()
50__all__ = [56
51 'Reporter',57
52 'UniversalChecker',58JS = find_exec(['gjs', 'seed'])
53 ]59
60
61class PocketLintPyFlakesChecker(PyFlakesChecker):
62 '''PocketLint checker for pyflakes.
63
64 This is here to work around some of the pyflakes problems.
65 '''
66
67 def NAME(self, node):
68 '''Locate name. Ignore WindowsErrors.'''
69 if node.name == 'WindowsError':
70 return
71 return super(PocketLintPyFlakesChecker, self).NAME(node)
5472
5573
56class Reporter:74class Reporter:
@@ -65,10 +83,12 @@
65 self.treestore = self.file_lines_view.get_model()83 self.treestore = self.file_lines_view.get_model()
66 self.piter = None84 self.piter = None
67 self._last_file_name = None85 self._last_file_name = None
86 self.call_count = 0
6887
69 def __call__(self, line_no, message, icon=None,88 def __call__(self, line_no, message, icon=None,
70 base_dir=None, file_name=None):89 base_dir=None, file_name=None):
71 """Report a message."""90 """Report a message."""
91 self.call_count += 1
72 if self.report_type == self.FILE_LINES:92 if self.report_type == self.FILE_LINES:
73 self._message_file_lines(93 self._message_file_lines(
74 line_no, message, icon=icon,94 line_no, message, icon=icon,
@@ -117,6 +137,7 @@
117 ZCML = object()137 ZCML = object()
118 DOCBOOK = object()138 DOCBOOK = object()
119 LOG = object()139 LOG = object()
140 SQL = object()
120141
121 XML_LIKE = (XML, XSLT, HTML, ZPT, ZCML, DOCBOOK)142 XML_LIKE = (XML, XSLT, HTML, ZPT, ZCML, DOCBOOK)
122143
@@ -130,6 +151,7 @@
130 'text/css': CSS,151 'text/css': CSS,
131 'text/html': HTML,152 'text/html': HTML,
132 'text/plain': TEXT,153 'text/plain': TEXT,
154 'text/x-sql': SQL,
133 'text/x-log': LOG,155 'text/x-log': LOG,
134 'application/javascript': JAVASCRIPT,156 'application/javascript': JAVASCRIPT,
135 'application/xml': XML,157 'application/xml': XML,
@@ -285,6 +307,20 @@
285 self.check_conflicts(line_no, line)307 self.check_conflicts(line_no, line)
286308
287309
310class SQLChecker(BaseChecker, AnyTextMixin):
311 """Verify SQL style."""
312
313 def check(self):
314 """Call each line_method for each line in text."""
315 # Consider http://code.google.com/p/python-sqlparse/ to verify
316 # keywords and reformatting.
317 for line_no, line in enumerate(self.text.splitlines()):
318 line_no += 1
319 self.check_trailing_whitespace(line_no, line)
320 self.check_tab(line_no, line)
321 self.check_conflicts(line_no, line)
322
323
288class XMLChecker(BaseChecker, AnyTextMixin):324class XMLChecker(BaseChecker, AnyTextMixin):
289 """Check XML documents."""325 """Check XML documents."""
290326
@@ -312,7 +348,7 @@
312 start, end = match.span(0)348 start, end = match.span(0)
313 text = text[:start] + self.xhtml_doctype + text[end:]349 text = text[:start] + self.xhtml_doctype + text[end:]
314 try:350 try:
315 root = ElementTree.parse(StringIO(text), parser)351 ElementTree.parse(StringIO(text), parser)
316 except (ExpatError, ParseError), error:352 except (ExpatError, ParseError), error:
317 if hasattr(error, 'code'):353 if hasattr(error, 'code'):
318 error_message = ErrorString(error.code)354 error_message = ErrorString(error.code)
@@ -326,7 +362,7 @@
326 error_lineno = 0362 error_lineno = 0
327 else:363 else:
328 error_message, location = str(error).rsplit(':')364 error_message, location = str(error).rsplit(':')
329 error_lineno = int(location.split(',')[0].split()[1])- offset365 error_lineno = int(location.split(',')[0].split()[1]) - offset
330 self.message(error_lineno, error_message, icon='error')366 self.message(error_lineno, error_message, icon='error')
331 self.check_text()367 self.check_text()
332368
@@ -378,7 +414,18 @@
378414
379 def check(self):415 def check(self):
380 """Check the syntax of the CSS code."""416 """Check the syntax of the CSS code."""
381 if self.text == '' or not HAS_CSSUTILS:417 if self.text == '':
418 return
419
420 self.check_cssutils()
421 self.check_text()
422 # CSS coding conventoins checks should go last since they rely
423 # on previous checks.
424 self.check_css_coding_conventions()
425
426 def check_cssutils(self):
427 """Check the CSS code by parsing it using CSSUtils module."""
428 if not HAS_CSSUTILS:
382 return429 return
383 handler = CSSReporterHandler(self)430 handler = CSSReporterHandler(self)
384 log = logging.getLogger('pocket-lint')431 log = logging.getLogger('pocket-lint')
@@ -387,7 +434,6 @@
387 log=log, loglevel=logging.INFO, raiseExceptions=False)434 log=log, loglevel=logging.INFO, raiseExceptions=False)
388 parser.parseString(self.text)435 parser.parseString(self.text)
389 log.removeHandler(handler)436 log.removeHandler(handler)
390 self.check_text()
391437
392 def check_text(self):438 def check_text(self):
393 """Call each line_method for each line in text."""439 """Call each line_method for each line in text."""
@@ -398,22 +444,29 @@
398 self.check_conflicts(line_no, line)444 self.check_conflicts(line_no, line)
399 self.check_tab(line_no, line)445 self.check_tab(line_no, line)
400446
447 def check_css_coding_conventions(self):
448 """Check the input using CSS Coding Convention checker."""
449 CSSCodingConventionChecker(self.text, logger=self.message).check()
450
401451
402class PythonChecker(BaseChecker, AnyTextMixin):452class PythonChecker(BaseChecker, AnyTextMixin):
403 """Check python source code."""453 """Check python source code."""
404454
455 # This regex is taken from PEP 0263.
456 encoding_pattern = re.compile("coding[:=]\s*([-\w.]+)")
457
405 def __init__(self, file_path, text, reporter=None):458 def __init__(self, file_path, text, reporter=None):
406 super(PythonChecker, self).__init__(459 super(PythonChecker, self).__init__(
407 file_path, text, reporter=reporter)460 file_path, text, reporter=reporter)
408 self.is_utf8 = False461 self.encoding = 'ascii'
409462
410 def check(self):463 def check(self):
411 """Check the syntax of the python code."""464 """Check the syntax of the python code."""
412 if self.text == '':465 if self.text == '':
413 return466 return
467 self.check_text()
414 self.check_flakes()468 self.check_flakes()
415 self.check_pep8()469 self.check_pep8()
416 self.check_text()
417470
418 def check_flakes(self):471 def check_flakes(self):
419 """Check compilation and syntax."""472 """Check compilation and syntax."""
@@ -421,13 +474,12 @@
421 tree = compiler.parse(self.text)474 tree = compiler.parse(self.text)
422 except (SyntaxError, IndentationError), exc:475 except (SyntaxError, IndentationError), exc:
423 line_no = exc.lineno or 0476 line_no = exc.lineno or 0
424 offset = exc.offset or 0
425 line = exc.text or ''477 line = exc.text or ''
426 explanation = 'Could not compile; %s' % exc.msg478 explanation = 'Could not compile; %s' % exc.msg
427 message = '%s: %s' % (explanation, line.strip())479 message = '%s: %s' % (explanation, line.strip())
428 self.message(line_no, message, icon='error')480 self.message(line_no, message, icon='error')
429 else:481 else:
430 warnings = Checker(tree)482 warnings = PocketLintPyFlakesChecker(tree)
431 for warning in warnings.messages:483 for warning in warnings.messages:
432 dummy, line_no, message = str(warning).split(':')484 dummy, line_no, message = str(warning).split(':')
433 self.message(int(line_no), message.strip(), icon='error')485 self.message(int(line_no), message.strip(), icon='error')
@@ -448,20 +500,24 @@
448 except TokenError, er:500 except TokenError, er:
449 message, location = er.args501 message, location = er.args
450 self.message(location[0], message, icon='error')502 self.message(location[0], message, icon='error')
503 except IndentationError, er:
504 message, location = er.args
505 message = "%s: %s" % (message, location[3].strip())
506 self.message(location[1], message, icon='error')
451 finally:507 finally:
452 Checker.report_error = original_report_error508 pep8.Checker.report_error = original_report_error
453509
454 def check_text(self):510 def check_text(self):
455 """Call each line_method for each line in text."""511 """Call each line_method for each line in text."""
456 for line_no, line in enumerate(self.text.splitlines()):512 for line_no, line in enumerate(self.text.splitlines()):
457 line_no += 1513 line_no += 1
458 if line_no in (1, 2) and '# -*- coding: utf-8 -*-' in line:514 if line_no in (1, 2):
459 self.is_utf8 = True515 match = self.encoding_pattern.search(line)
516 if match:
517 self.encoding = match.group(1).lower()
460 self.check_pdb(line_no, line)518 self.check_pdb(line_no, line)
461 self.check_length(line_no, line)519 self.check_length(line_no, line)
462 self.check_trailing_whitespace(line_no, line)
463 self.check_conflicts(line_no, line)520 self.check_conflicts(line_no, line)
464 self.check_tab(line_no, line)
465 self.check_ascii(line_no, line)521 self.check_ascii(line_no, line)
466522
467 def check_pdb(self, line_no, line):523 def check_pdb(self, line_no, line):
@@ -473,10 +529,10 @@
473529
474 def check_ascii(self, line_no, line):530 def check_ascii(self, line_no, line):
475 """Check that the line is ascii."""531 """Check that the line is ascii."""
476 if self.is_utf8:532 if self.encoding != 'ascii':
477 return533 return
478 try:534 try:
479 ascii_line = line.encode('ascii')535 line.encode('ascii')
480 except UnicodeEncodeError, error:536 except UnicodeEncodeError, error:
481 self.message(537 self.message(
482 line_no, 'Non-ascii characer at position %s.' % error.end,538 line_no, 'Non-ascii characer at position %s.' % error.end,
@@ -494,7 +550,7 @@
494 """Check the syntax of the javascript code."""550 """Check the syntax of the javascript code."""
495 if JS is None or self.text == '':551 if JS is None or self.text == '':
496 return552 return
497 args = [JS, '-f', self.FULLJSLINT, self.JSREPORTER, self.text]553 args = [JS, self.JSREPORTER, self.FULLJSLINT, self.file_path]
498 jslint = subprocess.Popen(554 jslint = subprocess.Popen(
499 args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)555 args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
500 issues, errors = jslint.communicate()556 issues, errors = jslint.communicate()
@@ -502,7 +558,9 @@
502 if issues:558 if issues:
503 for issue in issues.splitlines():559 for issue in issues.splitlines():
504 line_no, char_no_, message = issue.split('::')560 line_no, char_no_, message = issue.split('::')
505 self.message(int(line_no), message, icon='error')561 line_no = int(line_no)
562 line_no -= 1
563 self.message(line_no, message, icon='error')
506 self.check_text()564 self.check_text()
507565
508 def check_text(self):566 def check_text(self):
@@ -530,6 +588,7 @@
530def check_sources(sources, reporter=None):588def check_sources(sources, reporter=None):
531 if reporter is None:589 if reporter is None:
532 reporter = Reporter(Reporter.CONSOLE)590 reporter = Reporter(Reporter.CONSOLE)
591 reporter.call_count = 0
533 for source in sources:592 for source in sources:
534 file_path = os.path.normpath(source)593 file_path = os.path.normpath(source)
535 if os.path.isdir(source) or not Language.is_editable(source):594 if os.path.isdir(source) or not Language.is_editable(source):
@@ -540,6 +599,7 @@
540 checker = UniversalChecker(599 checker = UniversalChecker(
541 file_path, text=text, language=language, reporter=reporter)600 file_path, text=text, language=language, reporter=reporter)
542 checker.check()601 checker.check()
602 return reporter.call_count
543603
544604
545def main(argv=None):605def main(argv=None):
@@ -554,7 +614,7 @@
554 if options.verbose:614 if options.verbose:
555 pass615 pass
556 reporter = Reporter(Reporter.CONSOLE)616 reporter = Reporter(Reporter.CONSOLE)
557 check_sources(sources, reporter)617 return check_sources(sources, reporter)
558618
559619
560if __name__ == '__main__':620if __name__ == '__main__':
561621
=== modified file 'pocketlint/jsreporter.js'
--- pocketlint/jsreporter.js 2011-04-15 13:37:44 +0000
+++ pocketlint/jsreporter.js 2011-10-14 21:24:26 +0000
@@ -1,44 +1,101 @@
1// Copyright (C) 2009-2010 - Curtis Hovey <sinzui.is at verizon.net>1// Copyright (C) 2009-2011 - Curtis Hovey <sinzui.is at verizon.net>
2// This software is licensed under the MIT license (see the file COPYING).2// This software is licensed under the MIT license (see the file COPYING).
33
4// Run like:
5// <seed|gjs> jsreporter.js <path/to/fulljslint.js> <path/file/to/lint.js>
6
7
8function get_seed() {
9 // Define a common global object like seed.
10 var argv = ['gjs', 'jsreporter.js'];
11 var i;
12 for (i = 0; i < ARGV.length; i++) {
13 argv.push(ARGV[i]);
14 }
15 return {
16 'print': print,
17 'argv': argv
18 };
19 }
20
21
22var Seed = Seed || get_seed();
23
24
25jslint_path = Seed.argv[2].substring(0, Seed.argv[2].lastIndexOf('/'));
26imports.searchPath.push(jslint_path);
27var JSLINT = imports.fulljslint.JSLINT;
28
29
30function get_file_content(file_path) {
31 // Return the content of the file.
32 var Gio = imports.gi.Gio;
33 var file = Gio.file_new_for_path(file_path);
34 var istream = file.read(null);
35 var dstream = new Gio.DataInputStream({base_stream: istream});
36 var content_and_count = dstream.read_upto("", -1, null);
37 istream.close(null);
38 dstream = null;
39 return content_and_count[0];
40 }
41
42
4function report_implied_names() {43function report_implied_names() {
5 // Report about implied global names.44 // Report about implied global names.
6 var implied_names = [];45 var implied_names = [];
7 for (var name in JSLINT.implied) {46 var prop;
8 if (JSLINT.implied.hasOwnPropery(name)) {47 for (prop in JSLINT.implied) {
9 implied_names.push(name);48 if (JSLINT.implied.hasOwnProperty(prop)) {
49 implied_names.push(prop);
10 }50 }
11 }51 }
12 if (implied_names.length > 0) {52 if (implied_names.length > 0) {
13 implied_names.sort();53 implied_names.sort();
14 print('0::0::Implied globals:' + implied_names.join(', '));54 return '0::0::Implied globals:' + implied_names.join(', ');
15 }55 }
56 return '';
16 }57 }
1758
1859
19function report_lint_errors() {60function report_lint_errors() {
20 // Report about lint errors.61 // Report about lint errors.
21 for (var i = 0; i < JSLINT.errors.length; i++) {62 var errors = [];
63 var i;
64 for (i = 0; i < JSLINT.errors.length; i++) {
22 var error = JSLINT.errors[i];65 var error = JSLINT.errors[i];
23 if (error === null) {66 if (error === null) {
24 print('0::0::JSLINT had a fatal error.');67 error = {
68 'line': -1,
69 'character': -1,
70 'reason': 'JSLINT had a fatal error.'
71 };
25 }72 }
26 // Fix the line and character offset for editors.73 // Fix the line and character offset for editors.
27 error.line += 1;74 error.line += 1;
28 error.character += 1;75 error.character += 1;
29 print(error.line + '::' + error.character + '::' + error.reason);76 errors.push(
77 [error.line, error.character, error.reason].join('::'));
30 }78 }
79 return errors.join('\n');
31 }80 }
3281
3382
34function main(source_script) {83function lint_script() {
35 // Lint the source and report errors.84 // Lint the source and report errors.
36 var result = JSLINT(source_script);85 var script = get_file_content(Seed.argv[3]);
86 var result = JSLINT(script);
37 if (! result) {87 if (! result) {
38 report_lint_errors();88 var issues = [];
39 report_implied_names();89 errors = report_lint_errors();
90 if (errors) {
91 issues.push(errors);
92 }
93 implied = report_implied_names();
94 if (implied) {
95 issues.push(implied);
96 }
97 Seed.print(issues.join('\n'));
40 }98 }
41 }99 }
42100
43101lint_script();
44main(arguments[0]);
45102
=== modified file 'setup.py' (properties changed: -x to +x)
--- setup.py 2011-04-16 15:32:48 +0000
+++ setup.py 2011-10-14 21:24:26 +0000
@@ -1,9 +1,26 @@
1import subprocess
2
1from distutils.core import setup3from distutils.core import setup
4from distutils.command.sdist import sdist
5
6
7class SignedSDistCommand(sdist):
8 """Sign the source archive with a detached signature."""
9
10 description = "Sign the source archive after it is generated."
11
12 def run(self):
13 sdist.run(self)
14 gpg_args = [
15 'gpg', '--armor', '--sign', '--detach-sig', self.archive_files[0]]
16 gpg = subprocess.Popen(
17 gpg_args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
18 gpg.communicate()
219
3setup(20setup(
4 name="pocketlint",21 name="pocketlint",
5 description="Pocket-lint a composite linter and style checker.",22 description="Pocket-lint a composite linter and style checker.",
6 version="0.5.13",23 version="0.5.21",
7 maintainer="Curtis C. Hovey",24 maintainer="Curtis C. Hovey",
8 maintainer_email="sinzui.is@verizon.net",25 maintainer_email="sinzui.is@verizon.net",
9 url="https://launchpad.net/pocket-lint",26 url="https://launchpad.net/pocket-lint",
@@ -17,4 +34,7 @@
17 'pocketlint/contrib': ['fulljslint.js'],34 'pocketlint/contrib': ['fulljslint.js'],
18 },35 },
19 scripts=['scripts/pocketlint'],36 scripts=['scripts/pocketlint'],
37 cmdclass={
38 'signed_sdist': SignedSDistCommand,
39 },
20 )40 )

Subscribers

People subscribed via source and target branches

to all changes: