diff options
author | cpopa <devnull@localhost> | 2014-04-25 11:20:38 +0300 |
---|---|---|
committer | cpopa <devnull@localhost> | 2014-04-25 11:20:38 +0300 |
commit | 5500f23bf628980cf260923cc600cd3ac47ef63e (patch) | |
tree | 36166ec48629e0f47119273159c44e19b3bb4726 | |
parent | 5d0874d07d58716158efae4bead9cbe18bd15c9c (diff) | |
parent | 5a130703e1a1f23bc5f80294527299675de1293e (diff) | |
download | pylint-5500f23bf628980cf260923cc600cd3ac47ef63e.tar.gz |
Merge with default.
23 files changed, 858 insertions, 61 deletions
@@ -2,10 +2,21 @@ ChangeLog for Pylint ==================== -- + * Add a new warning [bad-continuation] for badly indentend continued + lines. + + * Emit [assignment-from-none] when the function contains bare returns. + Fixes BitBucket issue #191. + + * Added a new warning for closing over variables that are + defined in loops. Fixes Bitbucket issue #176. * Extend the checking for unbalanced-tuple-unpacking and unpacking-non-sequence to instance attribute unpacking as well. + * Restore --init-hook, renamed accidentally into --init-hooks in 1.2.0 + (#211) + * Add 'indexing-exception' warning, which detects that indexing an exception occurs in Python 2 (behaviour removed in Python 3). diff --git a/__pkginfo__.py b/__pkginfo__.py index ea67fe6..d0a9aac 100644 --- a/__pkginfo__.py +++ b/__pkginfo__.py @@ -45,8 +45,8 @@ classifiers = ['Development Status :: 4 - Beta', 'Programming Language :: Python :: 3', 'Topic :: Software Development :: Debuggers', 'Topic :: Software Development :: Quality Assurance', - 'Topic :: Software Development :: Testing', - ] + 'Topic :: Software Development :: Testing' + ] long_desc = """\ diff --git a/checkers/format.py b/checkers/format.py index 7ab6c7e..c02986e 100644 --- a/checkers/format.py +++ b/checkers/format.py @@ -35,6 +35,7 @@ from pylint.checkers import BaseTokenChecker from pylint.checkers.utils import check_messages from pylint.utils import WarningScope, OPTION_RGX +_CONTINUATION_BLOCK_OPENERS = ['elif', 'except', 'for', 'if', 'while', 'def', 'class'] _KEYWORD_TOKENS = ['assert', 'del', 'elif', 'except', 'for', 'if', 'in', 'not', 'raise', 'return', 'while', 'yield'] if sys.version_info < (3, 0): @@ -45,8 +46,10 @@ _SPACED_OPERATORS = ['==', '<', '>', '!=', '<>', '<=', '>=', '%=', '>>=', '<<='] _OPENING_BRACKETS = ['(', '[', '{'] _CLOSING_BRACKETS = [')', ']', '}'] +_TAB_LENGTH = 8 _EOL = frozenset([tokenize.NEWLINE, tokenize.NL, tokenize.COMMENT]) +_JUNK_TOKENS = (tokenize.COMMENT, tokenize.NL) # Whitespace checking policy constants _MUST = 0 @@ -77,6 +80,9 @@ MSGS = { 'bad-indentation', 'Used when an unexpected number of indentation\'s tabulations or ' 'spaces has been found.'), + 'C0330': ('Wrong %s indentation%s.\n%s%s', + 'bad-continuation', + 'TODO'), 'W0312': ('Found indentation with %ss instead of %ss', 'mixed-indentation', 'Used when there are some mixed tabs and spaces in a module.'), @@ -138,6 +144,261 @@ def _column_distance(token1, token2): return token2[2][1] - token1[3][1] +def _last_token_on_line_is(tokens, line_end, token): + return ( + line_end > 0 and tokens.token(line_end-1) == token or + line_end > 1 and tokens.token(line_end-2) == token + and tokens.type(line_end-1) == tokenize.COMMENT) + + +def _token_followed_by_eol(tokens, position): + return (tokens.type(position+1) == tokenize.NL or + tokens.type(position+1) == tokenize.COMMENT and + tokens.type(position+2) == tokenize.NL) + + +def _get_indent_length(line): + """Return the length of the indentation on the given token's line.""" + result = 0 + for char in line: + if char == ' ': + result += 1 + elif char == '\t': + result += _TAB_LENGTH + else: + break + return result + + +def _get_indent_hint_line(bar_positions, bad_position): + """Return a line with |s for each of the positions in the given lists.""" + if not bar_positions: + return '' + markers = [(pos, '|') for pos in bar_positions] + markers.append((bad_position, '^')) + markers.sort() + line = [' '] * (markers[-1][0] + 1) + for position, marker in markers: + line[position] = marker + return ''.join(line) + + +class _ContinuedIndent(object): + __slots__ = ('valid_outdent_offsets', + 'valid_continuation_offsets', + 'context_type', + 'token', + 'position') + + def __init__(self, + context_type, + token, + position, + valid_outdent_offsets, + valid_continuation_offsets): + self.valid_outdent_offsets = valid_outdent_offsets + self.valid_continuation_offsets = valid_continuation_offsets + self.context_type = context_type + self.position = position + self.token = token + + +# The contexts for hanging indents. +# A hanging indented dictionary value after : +HANGING_DICT_VALUE = 'dict-value' +# Hanging indentation in an expression. +HANGING = 'hanging' +# Hanging indentation in a block header. +HANGING_BLOCK = 'hanging-block' +# Continued indentation inside an expression. +CONTINUED = 'continued' +# Continued indentation in a block header. +CONTINUED_BLOCK = 'continued-block' + +SINGLE_LINE = 'single' +WITH_BODY = 'multi' + +_CONTINUATION_MSG_PARTS = { + HANGING_DICT_VALUE: ('hanging', ' in dict value'), + HANGING: ('hanging', ''), + HANGING_BLOCK: ('hanging', ' before block'), + CONTINUED: ('continued', ''), + CONTINUED_BLOCK: ('continued', ' before block'), +} + + +def _Offsets(*args): + """Valid indentation offsets for a continued line.""" + return {a: None for a in args} + + +def _BeforeBlockOffsets(single, with_body): + """Valid alternative indent offsets for continued lines before blocks. + + :param single: Valid offset for statements on a single logical line. + :param with_body: Valid offset for statements on several lines. + """ + return {single: SINGLE_LINE, with_body: WITH_BODY} + + +class TokenWrapper(object): + """A wrapper for readable access to token information.""" + + def __init__(self, tokens): + self._tokens = tokens + + def token(self, idx): + return self._tokens[idx][1] + + def type(self, idx): + return self._tokens[idx][0] + + def start_line(self, idx): + return self._tokens[idx][2][0] + + def start_col(self, idx): + return self._tokens[idx][2][1] + + def line(self, idx): + return self._tokens[idx][4] + + +class ContinuedLineState(object): + """Tracker for continued indentation inside a logical line.""" + + def __init__(self, tokens, config): + self._line_start = -1 + self._cont_stack = [] + self._is_block_opener = False + self.retained_warnings = [] + self._config = config + self._tokens = TokenWrapper(tokens) + + @property + def has_content(self): + return bool(self._cont_stack) + + @property + def _block_indent_size(self): + return len(self._config.indent_string.replace('\t', ' ' * _TAB_LENGTH)) + + @property + def _continuation_size(self): + return self._config.indent_after_paren + + def handle_line_start(self, pos): + """Record the first non-junk token at the start of a line.""" + if self._line_start > -1: + return + self._is_block_opener = self._tokens.token(pos) in _CONTINUATION_BLOCK_OPENERS + self._line_start = pos + + def next_physical_line(self): + """Prepares the tracker for a new physical line (NL).""" + self._line_start = -1 + self._is_block_opener = False + + def next_logical_line(self): + """Prepares the tracker for a new logical line (NEWLINE). + + A new logical line only starts with block indentation. + """ + self.next_physical_line() + self.retained_warnings = [] + self._cont_stack = [] + + def add_block_warning(self, token_position, state, valid_offsets): + self.retained_warnings.append((token_position, state, valid_offsets)) + + def get_valid_offsets(self, idx): + """"Returns the valid offsets for the token at the given position.""" + # The closing brace on a dict or the 'for' in a dict comprehension may + # reset two indent levels because the dict value is ended implicitly + stack_top = -1 + if self._tokens.token(idx) in ('}', 'for') and self._cont_stack[-1].token == ':': + stack_top = -2 + indent = self._cont_stack[stack_top] + if self._tokens.token(idx) in _CLOSING_BRACKETS: + valid_offsets = indent.valid_outdent_offsets + else: + valid_offsets = indent.valid_continuation_offsets + return indent, valid_offsets.copy() + + def _hanging_indent_after_bracket(self, bracket, position): + """Extracts indentation information for a hanging indent.""" + indentation = _get_indent_length(self._tokens.line(position)) + if self._is_block_opener and self._continuation_size == self._block_indent_size: + return _ContinuedIndent( + HANGING_BLOCK, + bracket, + position, + _Offsets(indentation + self._continuation_size, indentation), + _BeforeBlockOffsets(indentation + self._continuation_size, + indentation + self._continuation_size * 2)) + elif bracket == ':': + if self._cont_stack[-1].context_type == CONTINUED: + # If the dict key was on the same line as the open brace, the new + # correct indent should be relative to the key instead of the + # current indent level + paren_align = self._cont_stack[-1].valid_outdent_offsets + next_align = self._cont_stack[-1].valid_continuation_offsets.copy() + next_align[next_align.keys()[0] + self._continuation_size] = True + else: + next_align = _Offsets(indentation + self._continuation_size, indentation) + paren_align = _Offsets(indentation + self._continuation_size, indentation) + return _ContinuedIndent(HANGING_DICT_VALUE, bracket, position, paren_align, next_align) + else: + return _ContinuedIndent( + HANGING, + bracket, + position, + _Offsets(indentation, indentation + self._continuation_size), + _Offsets(indentation + self._continuation_size)) + + def _continuation_inside_bracket(self, bracket, pos): + """Extracts indentation information for a continued indent.""" + indentation = _get_indent_length(self._tokens.line(pos)) + if self._is_block_opener and self._tokens.start_col(pos+1) - indentation == self._block_indent_size: + return _ContinuedIndent( + CONTINUED_BLOCK, + bracket, + pos, + _Offsets(self._tokens.start_col(pos)), + _BeforeBlockOffsets(self._tokens.start_col(pos+1), + self._tokens.start_col(pos+1) + self._continuation_size)) + else: + return _ContinuedIndent( + CONTINUED, + bracket, + pos, + _Offsets(self._tokens.start_col(pos)), + _Offsets(self._tokens.start_col(pos+1))) + + def pop_token(self): + self._cont_stack.pop() + + def push_token(self, token, position): + """Pushes a new token for continued indentation on the stack. + + Tokens that can modify continued indentation offsets are: + * opening brackets + * 'lambda' + * : inside dictionaries + + push_token relies on the caller to filter out those + interesting tokens. + + :param token: The concrete token + :param position: The position of the token in the stream. + """ + if _token_followed_by_eol(self._tokens, position): + self._cont_stack.append( + self._hanging_indent_after_bracket(token, position)) + else: + self._cont_stack.append( + self._continuation_inside_bracket(token, position)) + + class FormatChecker(BaseTokenChecker): """checks for : * unauthorized constructions @@ -180,15 +441,34 @@ class FormatChecker(BaseTokenChecker): {'default' : ' ', 'type' : "string", 'metavar' : '<string>', 'help' : 'String used as indentation unit. This is usually \ " " (4 spaces) or "\\t" (1 tab).'}), + ('indent-after-paren', + {'type': 'int', 'metavar': '<int>', 'default': 4, + 'help': 'Number of spaces of indent required inside a hanging ' + ' or continued line.'}), ) + def __init__(self, linter=None): BaseTokenChecker.__init__(self, linter) self._lines = None self._visited_lines = None + self._bracket_stack = [None] + + def _pop_token(self): + self._bracket_stack.pop() + self._current_line.pop_token() + + def _push_token(self, token, idx): + self._bracket_stack.append(token) + self._current_line.push_token(token, idx) - def new_line(self, tok_type, line, line_num, junk): + def new_line(self, tokens, line_end, line_start): """a new line has been encountered, process it if necessary""" - if not tok_type in junk: + if _last_token_on_line_is(tokens, line_end, ';'): + self.add_message('unnecessary-semicolon', line=tokens.start_line(line_end)) + + line_num = tokens.start_line(line_start) + line = tokens.line(line_start) + if tokens.type(line_start) not in _JUNK_TOKENS: self._lines[line_num] = line.split('\n')[0] self.check_lines(line, line_num) @@ -211,6 +491,8 @@ class FormatChecker(BaseTokenChecker): start: int; the position of the keyword in the token list. """ # If the next token is not a paren, we're fine. + if self._inside_brackets(':') and tokens[start][1] == 'for': + self._pop_token() if tokens[start+1][1] != '(': return @@ -270,7 +552,7 @@ class FormatChecker(BaseTokenChecker): return def _opening_bracket(self, tokens, i): - self._bracket_stack.append(tokens[i][1]) + self._push_token(tokens[i][1], i) # Special case: ignore slices if tokens[i][1] == '[' and tokens[i+1][1] == ':': return @@ -283,7 +565,9 @@ class FormatChecker(BaseTokenChecker): self._check_space(tokens, i, (_IGNORE, _MUST_NOT)) def _closing_bracket(self, tokens, i): - self._bracket_stack.pop() + if self._inside_brackets(':'): + self._pop_token() + self._pop_token() # Special case: ignore slices if tokens[i-1][1] == ':' and tokens[i][1] == ']': return @@ -302,7 +586,7 @@ class FormatChecker(BaseTokenChecker): self._check_space(tokens, i, (_MUST, _MUST)) def _open_lambda(self, tokens, i): # pylint:disable=unused-argument - self._bracket_stack.append('lambda') + self._push_token('lambda', i) def _handle_colon(self, tokens, i): # Special case: ignore slices @@ -316,7 +600,9 @@ class FormatChecker(BaseTokenChecker): self._check_space(tokens, i, policy) if self._inside_brackets('lambda'): - self._bracket_stack.pop() + self._pop_token() + elif self._inside_brackets('{'): + self._push_token(':', i) def _handle_comma(self, tokens, i): # Only require a following whitespace if this is @@ -325,6 +611,8 @@ class FormatChecker(BaseTokenChecker): self._check_space(tokens, i, (_MUST_NOT, _IGNORE)) else: self._check_space(tokens, i, (_MUST_NOT, _MUST)) + if self._inside_brackets(':'): + self._pop_token() def _check_surrounded_by_space(self, tokens, i): """Check that a binary operator is surrounded by exactly one space.""" @@ -383,6 +671,10 @@ class FormatChecker(BaseTokenChecker): def _inside_brackets(self, left): return self._bracket_stack[-1] == left + def _handle_old_ne_operator(self, tokens, i): + if tokens[i][1] == '<>': + self.add_message('old-ne-operator', line=tokens[i][2][0]) + def _prepare_token_dispatcher(self): raw = [ (_KEYWORD_TOKENS, @@ -401,6 +693,8 @@ class FormatChecker(BaseTokenChecker): ([':'], self._handle_colon), (['lambda'], self._open_lambda), + + (['<>'], self._handle_old_ne_operator), ] dispatch = {} @@ -419,76 +713,63 @@ class FormatChecker(BaseTokenChecker): regular expression). """ self._bracket_stack = [None] - indent = tokenize.INDENT - dedent = tokenize.DEDENT - newline = tokenize.NEWLINE - junk = (tokenize.COMMENT, tokenize.NL) indents = [0] - check_equal = 0 + check_equal = False line_num = 0 - previous = None self._lines = {} self._visited_lines = {} - new_line_delay = False token_handlers = self._prepare_token_dispatcher() + + self._current_line = ContinuedLineState(tokens, self.config) for idx, (tok_type, token, start, _, line) in enumerate(tokens): - if new_line_delay: - new_line_delay = False - self.new_line(tok_type, line, line_num, junk) if start[0] != line_num: - if previous is not None and previous[0] == tokenize.OP and previous[1] == ';': - self.add_message('unnecessary-semicolon', line=previous[2]) - previous = None line_num = start[0] # A tokenizer oddity: if an indented line contains a multi-line # docstring, the line member of the INDENT token does not contain - # the full line; therefore we delay checking the new line until - # the next token. + # the full line; therefore we check the next token on the line. if tok_type == tokenize.INDENT: - new_line_delay = True + self.new_line(TokenWrapper(tokens), idx-1, idx+1) else: - self.new_line(tok_type, line, line_num, junk) - if tok_type not in (indent, dedent, newline) + junk: - previous = tok_type, token, start[0] - - if tok_type == tokenize.OP: - if token == '<>': - self.add_message('old-ne-operator', line=line_num) - elif tok_type == tokenize.NUMBER: - if token.endswith('l'): - self.add_message('lowercase-l-suffix', line=line_num) - - elif tok_type == newline: + self.new_line(TokenWrapper(tokens), idx-1, idx) + + if tok_type == tokenize.NEWLINE: # a program statement, or ENDMARKER, will eventually follow, # after some (possibly empty) run of tokens of the form # (NL | COMMENT)* (INDENT | DEDENT+)? # If an INDENT appears, setting check_equal is wrong, and will # be undone when we see the INDENT. - check_equal = 1 - - elif tok_type == indent: - check_equal = 0 + check_equal = True + self._process_retained_warnings(TokenWrapper(tokens), idx) + self._current_line.next_logical_line() + elif tok_type == tokenize.INDENT: + check_equal = False self.check_indent_level(token, indents[-1]+1, line_num) indents.append(indents[-1]+1) - - elif tok_type == dedent: + elif tok_type == tokenize.DEDENT: # there's nothing we need to check here! what's important is # that when the run of DEDENTs ends, the indentation of the # program statement (or ENDMARKER) that triggered the run is # equal to what's left at the top of the indents stack - check_equal = 1 + check_equal = True if len(indents) > 1: del indents[-1] - - elif check_equal and tok_type not in junk: - # this is the first "real token" following a NEWLINE, so it + elif tok_type == tokenize.NL: + self._check_continued_indentation(TokenWrapper(tokens), idx+1) + self._current_line.next_physical_line() + elif tok_type != tokenize.COMMENT: + self._current_line.handle_line_start(idx) + # This is the first concrete token following a NEWLINE, so it # must be the first token of the next program statement, or an # ENDMARKER; the "line" argument exposes the leading whitespace # for this statement; in the case of ENDMARKER, line is an empty # string, so will properly match the empty string with which the # "indents" stack was seeded - check_equal = 0 - self.check_indent_level(line, indents[-1], line_num) + if check_equal: + check_equal = False + self.check_indent_level(line, indents[-1], line_num) + + if tok_type == tokenize.NUMBER and token.endswith('l'): + self.add_message('lowercase-l-suffix', line=line_num) try: handler = token_handlers[token] @@ -501,6 +782,49 @@ class FormatChecker(BaseTokenChecker): if line_num > self.config.max_module_lines: self.add_message('too-many-lines', args=line_num, line=1) + def _process_retained_warnings(self, tokens, current_pos): + single_line_block_stmt = not _last_token_on_line_is(tokens, current_pos, ':') + + for indent_pos, state, offsets in self._current_line.retained_warnings: + block_type = offsets[tokens.start_col(indent_pos)] + hints = dict((k, v) for k, v in offsets.iteritems() + if v != block_type) + if single_line_block_stmt and block_type == WITH_BODY: + self._add_continuation_message(state, hints, tokens, indent_pos) + elif not single_line_block_stmt and block_type == SINGLE_LINE: + self._add_continuation_message(state, hints, tokens, indent_pos) + + def _check_continued_indentation(self, tokens, next_idx): + # Do not issue any warnings if the next line is empty. + if not self._current_line.has_content or tokens.type(next_idx) == tokenize.NL: + return + + state, valid_offsets = self._current_line.get_valid_offsets(next_idx) + # Special handling for hanging comments. If the last line ended with a + # comment and the new line contains only a comment, the line may also be + # indented to the start of the previous comment. + if (tokens.type(next_idx) == tokenize.COMMENT and + tokens.type(next_idx-2) == tokenize.COMMENT): + valid_offsets[tokens.start_col(next_idx-2)] = True + + # We can only decide if the indentation of a continued line before opening + # a new block is valid once we know of the body of the block is on the + # same line as the block opener. Since the token processing is single-pass, + # emitting those warnings is delayed until the block opener is processed. + if (state.context_type in (HANGING_BLOCK, CONTINUED_BLOCK) + and tokens.start_col(next_idx) in valid_offsets): + self._current_line.add_block_warning(next_idx, state, valid_offsets) + elif tokens.start_col(next_idx) not in valid_offsets: + self._add_continuation_message(state, valid_offsets, tokens, next_idx) + + def _add_continuation_message(self, state, offsets, tokens, position): + readable_type, readable_position = _CONTINUATION_MSG_PARTS[state.context_type] + hint_line = _get_indent_hint_line(offsets, tokens.start_col(position)) + self.add_message( + 'bad-continuation', + line=tokens.start_line(position), + args=(readable_type, readable_position, tokens.line(position), hint_line)) + @check_messages('multiple-statements') def visit_default(self, node): """check the node line number and check it if not yet done""" diff --git a/checkers/typecheck.py b/checkers/typecheck.py index 5fe293d..4cb406f 100644 --- a/checkers/typecheck.py +++ b/checkers/typecheck.py @@ -287,7 +287,8 @@ accessed. Python regular expressions are accepted.'} else: for rnode in returns: if not (isinstance(rnode.value, astroid.Const) - and rnode.value.value is None): + and rnode.value.value is None + or rnode.value is None): break else: self.add_message('assignment-from-none', node=node) diff --git a/checkers/variables.py b/checkers/variables.py index 2ce7fcf..a6123de 100644 --- a/checkers/variables.py +++ b/checkers/variables.py @@ -146,6 +146,12 @@ MSGS = { 'Used when something which is not ' 'a sequence is used in an unpack assignment'), + 'W0640': ('Cell variable %s defined in loop', + 'cell-var-from-loop', + 'A variable used in a closure is defined in a loop. ' + 'This will result in all closures using the same value for ' + 'the closed-over variable.'), + } class VariablesChecker(BaseChecker): @@ -423,6 +429,25 @@ builtins. Remember that you should avoid to define new builtins when possible.' if default_message: self.add_message('global-statement', node=node) + def _check_late_binding_closure(self, node, assignment_node, scope_type): + node_scope = node.scope() + if not isinstance(node_scope, (astroid.Lambda, astroid.Function)): + return + + if isinstance(assignment_node, astroid.Comprehension): + if assignment_node.parent.parent_of(node.scope()): + self.add_message('cell-var-from-loop', node=node, args=node.name) + else: + assign_scope = assignment_node.scope() + maybe_for = assignment_node + while not isinstance(maybe_for, astroid.For): + if maybe_for is assign_scope: + break + maybe_for = maybe_for.parent + else: + if maybe_for.parent_of(node_scope) and not isinstance(node_scope.statement(), astroid.Return): + self.add_message('cell-var-from-loop', node=node, args=node.name) + def _loopvar_name(self, node, name): # filter variables according to node's scope # XXX used to filter parents but don't remember why, and removing this @@ -509,6 +534,8 @@ builtins. Remember that you should avoid to define new builtins when possible.' # the name has already been consumed, only check it's not a loop # variable used outside the loop if name in consumed: + defnode = assign_parent(consumed[name][0]) + self._check_late_binding_closure(node, defnode, scope_type) self._loopvar_name(node, name) break # mark the name as consumed if it's defined in this scope @@ -520,6 +547,7 @@ builtins. Remember that you should avoid to define new builtins when possible.' # checks for use before assignment defnode = assign_parent(to_consume[name][0]) if defnode is not None: + self._check_late_binding_closure(node, defnode, scope_type) defstmt = defnode.statement() defframe = defstmt.frame() maybee0601 = True @@ -907,7 +907,7 @@ group are mutually exclusive.'), try: preprocess_options(args, { # option: (callback, takearg) - 'init-hooks': (cb_init_hook, True), + 'init-hook': (cb_init_hook, True), 'rcfile': (self.cb_set_rcfile, True), 'load-plugins': (self.cb_add_plugins, True), }) diff --git a/test/input/func_bad_continuation.py b/test/input/func_bad_continuation.py new file mode 100644 index 0000000..f0787de --- /dev/null +++ b/test/input/func_bad_continuation.py @@ -0,0 +1,222 @@ +"""Regression test case for bad-continuation.""" + +__revision__ = 1 + +# Various alignment for brackets +print [ + 1, 2, 3 +] +print [ + 1, 2, 3 + ] +print [ + 1, 2, 3 + ] # [bad-continuation] + +# Alignment inside literals +W0 = [1, 2, 3, + 4, 5, 6, + 7, # [bad-continuation] + 8, 9, 10, + 11, 12, 13, + # and a comment + 14, 15, 16] + +W1 = { + 'a': 1, + 'b': 2, # [bad-continuation] + 'c': 3, + } + +W2 = { + 'a': 1, + 'b': 2, # [bad-continuation] + 'c': 3, + } + +W2 = ['some', 'contents' # with a continued comment that may be aligned + # under the previous comment (optionally) + 'and', + 'more', # but this + # [bad-continuation] is not accepted + 'contents', # nor this. [bad-continuation] + ] + +# Values in dictionaries should be indented 4 spaces further if they are on a +# different line than their key +W4 = { + 'key1': + 'value1', # Grandfather in the old style + 'key2': + 'value2', # [bad-continuation] + 'key3': + 'value3', # Comma here + } + +# And should follow the same rules as continuations within parens +W5 = { + 'key1': 'long value' + 'long continuation', + 'key2': 'breaking' + 'wrong', # [bad-continuation] + 'key3': 2*( + 2+2), + 'key4': ('parenthesis', + 'continuation') # No comma here + } + +# Allow values to line up with their keys when the key is next to the brace +W6 = {'key1': + 'value1', + 'key2': + 'value2', + } + +# Or allow them to be indented +W7 = {'key1': + 'value1', + 'key2': + 'value2' + } + +# Bug that caused a warning on the previous two cases permitted these odd +# incorrect indentations +W8 = {'key1': +'value1', # [bad-continuation] + } + +W9 = {'key1': + 'value1', # [bad-continuation] + } + +# Dictionary comprehensions should not require extra indentation when breaking +# before the 'for', which is not part of the value +C1 = {'key{}'.format(x): 'value{}'.format(x) + for x in range(3)} + +C2 = {'key{}'.format(x): 'value{}'.format(x) for x in + range(3)} + +# Dictionary comprehensions with multiple loops broken in different places +C3 = {x*y: (x, y) for x in range(3) for y in range(3)} + +C4 = {x*y: (x, y) + for x in range(3) for y in range(3)} + +C5 = {x*y: (x, y) for x + in range(3) for y in range(3)} + +C6 = {x*y: (x, y) for x in range(3) + for y in range(3)} + +C7 = {key: + key ** 2 + for key in range(10)} + +C8 = { + key: key ** 2 + for key in range(10)} + +# Misaligned cases for dict comprehensions +C9 = {'key{}'.format(x): 'value{}'.format(x) + for x in range(3)} # [bad-continuation] + +C9 = {'key{}'.format(x): 'value{}'.format(x) + for x in range(3)} # [bad-continuation] + +# Alignment of arguments in function definitions +def continue1(some_arg, + some_other_arg): + """A function with well-aligned arguments.""" + print some_arg, some_other_arg + + +def continue2( + some_arg, + some_other_arg): + """A function with well-aligned arguments.""" + print some_arg, some_other_arg + +def continue3( + some_arg, # [bad-continuation] + some_other_arg): # [bad-continuation] + """A function with misaligned arguments""" + print some_arg, some_other_arg + +def continue4( + arg1, + arg2): print arg1, arg2 + + +def callee(*args): + """noop""" + print args + + +callee( + "a", + "b" + ) + +callee("a", + "b") # [bad-continuation] + +callee(5, {'a': 'b', + 'c': 'd'}) + +if ( + 1 + ): pass + +if ( + 1 +): pass +if ( + 1 + ): pass # [bad-continuation] + +if (1 and + 2): # [bad-continuation] + pass + +while (1 and + 2): + pass + +while (1 and + 2 and # [bad-continuation] + 3): + pass + +if ( + 2): pass # [bad-continuation] + +if (1 or + 2 or + 3): pass + +if (1 or + 2 or # [bad-continuation] + 3): print 1, 2 + +if (1 and + 2): pass # [bad-continuation] + +if ( + 2): pass + +if ( + 2): # [bad-continuation] + pass + +L1 = (lambda a, + b: a + b) + +if not (1 and + 2): + print 3 + +if not (1 and + 2): # [bad-continuation] + print 3 + diff --git a/test/input/func_format_py27.py b/test/input/func_format_py27.py index 828a180..46d5cfe 100644 --- a/test/input/func_format_py27.py +++ b/test/input/func_format_py27.py @@ -79,8 +79,7 @@ def hop(context): return ['''<a id="sendbutton" href="javascript: $('%(domid)s').submit()"> <img src="%(sendimgpath)s" alt="%(send)s"/>%(send)s</a>''' % context, '''<a id="cancelbutton" href="javascript: history.back()"> -<img src="%(cancelimgpath)s" alt="%(cancel)s"/>%(cancel)s</a>''' % context, - ] +<img src="%(cancelimgpath)s" alt="%(cancel)s"/>%(cancel)s</a>''' % context] titreprojet = '<tr><td colspan="10">\ <img src="images/drapeau_vert.png" alt="Drapeau vert" />\ <strong>%s</strong></td></tr>' % aaaa diff --git a/test/input/func_format_py_27.py b/test/input/func_format_py_27.py index f062f27..95f7fde 100644 --- a/test/input/func_format_py_27.py +++ b/test/input/func_format_py_27.py @@ -79,8 +79,7 @@ def hop(context): return ['''<a id="sendbutton" href="javascript: $('%(domid)s').submit()"> <img src="%(sendimgpath)s" alt="%(send)s"/>%(send)s</a>''' % context, '''<a id="cancelbutton" href="javascript: history.back()"> -<img src="%(cancelimgpath)s" alt="%(cancel)s"/>%(cancel)s</a>''' % context, - ] +<img src="%(cancelimgpath)s" alt="%(cancel)s"/>%(cancel)s</a>''' % context] titreprojet = '<tr><td colspan="10">\ <img src="images/drapeau_vert.png" alt="Drapeau vert" />\ <strong>%s</strong></td></tr>' % aaaa diff --git a/test/input/func_indent.py b/test/input/func_indent.py index 18159e9..3c6ee41 100644 --- a/test/input/func_indent.py +++ b/test/input/func_indent.py @@ -11,6 +11,7 @@ def tutuu(): def titii(): """also malindented""" + 1 # and this. def tataa(kdict): """blank line unindented""" diff --git a/test/input/func_loopvar_in_closure.py b/test/input/func_loopvar_in_closure.py new file mode 100644 index 0000000..32b7a6c --- /dev/null +++ b/test/input/func_loopvar_in_closure.py @@ -0,0 +1,114 @@ +"""Tests for loopvar-in-closure.""" + +__revision__ = 0 + + +def good_case(): + """No problems here.""" + lst = [] + for i in range(10): + lst.append(i) + + +def good_case2(): + """No problems here.""" + return [i for i in range(10)] + + +def good_case3(): + """No problems here.""" + lst = [] + for i in range(10): + lst.append(lambda i=i: i) + + +def good_case4(): + """No problems here.""" + lst = [] + for i in range(10): + print i + lst.append(lambda i: i) + + +def good_case5(): + """No problems here.""" + return (i for i in range(10)) + + +def good_case6(): + """Accept use of the variable after the loop. + + There's already a warning about possibly undefined loop variables, and + the value will not change any more.""" + for i in range(10): + print i + return lambda: i + + +def good_case7(): + """Accept use of the variable inside return.""" + for i in range(10): + if i == 8: + return lambda: i + return lambda: -1 + + +def bad_case(): + """Closing over a loop variable.""" + lst = [] + for i in range(10): + print i + lst.append(lambda: i) + + +def bad_case2(): + """Closing over a loop variable.""" + return [lambda: i for i in range(10)] + + +def bad_case3(): + """Closing over variable defined in loop.""" + lst = [] + for i in range(10): + j = i * i + lst.append(lambda: j) + return lst + + +def bad_case4(): + """Closing over variable defined in loop.""" + lst = [] + for i in range(10): + def nested(): + """Nested function.""" + return i**2 + lst.append(nested) + return lst + + +def bad_case5(): + """Problematic case. + + If this function is used as + + >>> [x() for x in bad_case5()] + + it behaves 'as expected', i.e. the result is range(10). + + If it's used with + + >>> lst = list(bad_case5()) + >>> [x() for x in lst] + + the result is [9] * 10 again. + """ + return (lambda: i for i in range(10)) + + +def bad_case6(): + """Closing over variable defined in loop.""" + lst = [] + for i, j in zip(range(10), range(10, 20)): + print j + lst.append(lambda: i) + return lst diff --git a/test/input/func_loopvar_in_dict_comp_py27.py b/test/input/func_loopvar_in_dict_comp_py27.py new file mode 100644 index 0000000..312eee7 --- /dev/null +++ b/test/input/func_loopvar_in_dict_comp_py27.py @@ -0,0 +1,8 @@ +"""Tests for loopvar-in-closure.""" + +__revision__ = 0 + + +def bad_case(): + """Loop variable from dict comprehension.""" + return {x: lambda: x for x in range(10)} diff --git a/test/input/func_noerror_new_style_class_py_30.py b/test/input/func_noerror_new_style_class_py_30.py index 87bfb9e..9e55277 100644 --- a/test/input/func_noerror_new_style_class_py_30.py +++ b/test/input/func_noerror_new_style_class_py_30.py @@ -18,7 +18,7 @@ class File(file): super(File, self).__init__(name, mode, buffering) if self.verbose: print "File %s is opened. The mode is: %s" % (self.name, -self.mode) + self.mode) # def write(self, a_string): diff --git a/test/input/func_typecheck_callfunc_assigment.py b/test/input/func_typecheck_callfunc_assigment.py index a909f17..838b27c 100644 --- a/test/input/func_typecheck_callfunc_assigment.py +++ b/test/input/func_typecheck_callfunc_assigment.py @@ -28,6 +28,13 @@ def func_return_none(): A = func_return_none() +def func_implicit_return_none(): + """Function returning None from bare return statement.""" + return + +A = func_implicit_return_none() + + def func_return_none_and_smth(): """function returning none and something else""" print 'dougloup' diff --git a/test/messages/func_bad_continuation.txt b/test/messages/func_bad_continuation.txt new file mode 100644 index 0000000..ba6516d --- /dev/null +++ b/test/messages/func_bad_continuation.txt @@ -0,0 +1,70 @@ +C: 14: Wrong hanging indentation. + ] # [bad-continuation] +| ^| +C: 19: Wrong continued indentation. + 7, # [bad-continuation] + | ^ +C: 27: Wrong hanging indentation. + 'b': 2, # [bad-continuation] + ^| +C: 33: Wrong hanging indentation. + 'b': 2, # [bad-continuation] + ^| +C: 41: Wrong continued indentation. + # [bad-continuation] is not accepted + | | ^ +C: 42: Wrong continued indentation. + 'contents', # nor this. [bad-continuation] + | ^ +C: 51: Wrong hanging indentation in dict value. + 'value2', # [bad-continuation] + | ^ | +C: 61: Wrong continued indentation. + 'wrong', # [bad-continuation] + ^ | +C: 85: Wrong hanging indentation in dict value. +'value1', # [bad-continuation] +^ | | +C: 89: Wrong hanging indentation in dict value. + 'value1', # [bad-continuation] + ^ | | +C:122: Wrong continued indentation. + for x in range(3)} # [bad-continuation] + ^ | +C:125: Wrong continued indentation. + for x in range(3)} # [bad-continuation] + | ^ +C:141: Wrong hanging indentation before block. + some_arg, # [bad-continuation] + ^ | +C:142: Wrong hanging indentation before block. + some_other_arg): # [bad-continuation] + ^ | +C:146:continue4: Missing function docstring +C:162: Wrong continued indentation. + "b") # [bad-continuation] + ^ | +C:176: Wrong hanging indentation before block. + ): pass # [bad-continuation] +| ^| +C:179: Wrong continued indentation before block. + 2): # [bad-continuation] + ^ | +C:187: Wrong continued indentation. + 2 and # [bad-continuation] + | ^ +C:192: Wrong hanging indentation before block. + 2): pass # [bad-continuation] + ^ | | +C:199: Wrong continued indentation before block. + 2 or # [bad-continuation] + |^ | +C:203: Wrong continued indentation before block. + 2): pass # [bad-continuation] + ^ | | +C:209: Wrong hanging indentation before block. + 2): # [bad-continuation] + ^ | | +C:220: Wrong continued indentation. + 2): # [bad-continuation] + ^ | diff --git a/test/messages/func_break_or_return_in_try_finally.txt b/test/messages/func_break_or_return_in_try_finally.txt index 4b674ae..04f27fe 100644 --- a/test/messages/func_break_or_return_in_try_finally.txt +++ b/test/messages/func_break_or_return_in_try_finally.txt @@ -1,2 +1,3 @@ W: 18:insidious_break_and_return: break statement in finally block may swallow exception W: 20:insidious_break_and_return: return statement in finally block may swallow exception +W: 39:break_and_return.strange: Cell variable my_var defined in loop diff --git a/test/messages/func_format_py27.txt b/test/messages/func_format_py27.txt index ce5df81..f71cee8 100644 --- a/test/messages/func_format_py27.txt +++ b/test/messages/func_format_py27.txt @@ -41,6 +41,6 @@ C: 71: Exactly one space required before assignment C: 73: Exactly one space required around assignment ocount[obj.__class__]=1 ^ -C: 91: More than one statement on a single line -C:105: More than one statement on a single line -C:108: More than one statement on a single line +C: 90: More than one statement on a single line +C:104: More than one statement on a single line +C:107: More than one statement on a single line diff --git a/test/messages/func_indent.txt b/test/messages/func_indent.txt index aa3645f..e24af18 100644 --- a/test/messages/func_indent.txt +++ b/test/messages/func_indent.txt @@ -1,3 +1,5 @@ W: 5: Bad indentation. Found 1 spaces, expected 4 W: 6: Bad indentation. Found 1 spaces, expected 4 W: 13: Bad indentation. Found 5 spaces, expected 4 +W: 14: Bad indentation. Found 5 spaces, expected 4 +W: 14:titii: Statement seems to have no effect diff --git a/test/messages/func_loopvar_in_closure.txt b/test/messages/func_loopvar_in_closure.txt new file mode 100644 index 0000000..6ca613a --- /dev/null +++ b/test/messages/func_loopvar_in_closure.txt @@ -0,0 +1,8 @@ +W: 21:good_case3: Unused variable 'i' +W: 45:good_case6.<lambda>: Using possibly undefined loop variable 'i' +W: 61:bad_case.<lambda>: Cell variable i defined in loop +W: 66:bad_case2.<lambda>: Cell variable i defined in loop +W: 74:bad_case3.<lambda>: Cell variable j defined in loop +W: 84:bad_case4.nested: Cell variable i defined in loop +W:105:bad_case5.<lambda>: Cell variable i defined in loop +W:113:bad_case6.<lambda>: Cell variable i defined in loop diff --git a/test/messages/func_loopvar_in_dict_comp_py27.txt b/test/messages/func_loopvar_in_dict_comp_py27.txt new file mode 100644 index 0000000..bc11121 --- /dev/null +++ b/test/messages/func_loopvar_in_dict_comp_py27.txt @@ -0,0 +1 @@ +W: 8:bad_case.<lambda>: Cell variable x defined in loop diff --git a/test/messages/func_typecheck_callfunc_assigment.txt b/test/messages/func_typecheck_callfunc_assigment.txt index 96ad43e..1d84510 100644 --- a/test/messages/func_typecheck_callfunc_assigment.txt +++ b/test/messages/func_typecheck_callfunc_assigment.txt @@ -1,2 +1,3 @@ E: 20: Assigning to function call which doesn't return W: 28: Assigning to function call which only returns None +W: 35: Assigning to function call which only returns None diff --git a/test/unittest_checker_format.py b/test/unittest_checker_format.py index 8f1be78..b9fba5c 100644 --- a/test/unittest_checker_format.py +++ b/test/unittest_checker_format.py @@ -135,7 +135,7 @@ class CheckSpaceTest(CheckerTestCase): good_cases = [ '(a)\n', '(a * (b + c))\n', - '( #\na)\n', + '(#\n a)\n', ] with self.assertNoMessages(): for code in good_cases: diff --git a/test/unittest_lint.py b/test/unittest_lint.py index a7213bd..2b13039 100644 --- a/test/unittest_lint.py +++ b/test/unittest_lint.py @@ -381,9 +381,9 @@ class PyLinterTC(TestCase): def test_init_hooks_called_before_load_plugins(self): self.assertRaises(RuntimeError, - Run, ['--load-plugins', 'unexistant', '--init-hooks', 'raise RuntimeError']) + Run, ['--load-plugins', 'unexistant', '--init-hook', 'raise RuntimeError']) self.assertRaises(RuntimeError, - Run, ['--init-hooks', 'raise RuntimeError', '--load-plugins', 'unexistant']) + Run, ['--init-hook', 'raise RuntimeError', '--load-plugins', 'unexistant']) class ConfigTC(TestCase): |