merge

authorMichal Nowikowski <godfryd@gmail.com>
changeset889d8237ff34
branchline-ending-checks
phasepublic
hiddenno
parent revision#d28befce35c8 Merged logilab/pylint into default, #74e4eb9fd1f9 Moved changelog entry to the top.
child revision#a3d0b6d984c8 Closing branch line-ending-checks, #4c793bc5e93b Merged in godfryd/pylint/line-ending-checks (pull request #133)
files modified by this revision
ChangeLog
__pkginfo__.py
checkers/strings.py
checkers/variables.py
setup.py
test/functional/bad_open_mode.py
test/functional/bad_open_mode.txt
test/functional/class_scope.py
test/functional/class_scope.txt
test/functional/invalid_encoded_data.py
test/functional/invalid_encoded_data.txt
test/functional/invalid_exceptions_caught.py
test/functional/invalid_exceptions_caught.txt
test/functional/missing_self_argument.py
test/functional/missing_self_argument.txt
test/functional/superfluous_parens.py
test/functional/superfluous_parens.txt
test/input/func_bad_open_mode.py
test/input/func_invalid_encoded_data.py
test/input/func_method_missing_self.py
test/input/func_method_without_self_but_self_assignment.py
test/input/func_scope_regrtest.py
test/input/func_string_format_py27.py
test/input/func_superfluous_parens.py
test/messages/func_bad_open_mode.txt
test/messages/func_invalid_encoded_data.txt
test/messages/func_method_missing_self.txt
test/messages/func_method_without_self_but_self_assignment.txt
test/messages/func_scope_regrtest.txt
test/messages/func_superfluous_parens.txt
test/test_func.py
# HG changeset patch
# User Michal Nowikowski <godfryd@gmail.com>
# Date 1406689534 -7200
# Wed Jul 30 05:05:34 2014 +0200
# Branch line-ending-checks
# Node ID 889d8237ff34a2ece999d67a886576012e95457e
# Parent 74e4eb9fd1f9ea1072706941b6efb4f8e3ec7422
# Parent d28befce35c8f99024bfcd7a354ad8b42fc4e069
merge

diff --git a/ChangeLog b/ChangeLog
@@ -8,10 +8,15 @@
1 
2      * Fix a false positive with string formatting checker, when
3        encountering a string which uses only position-based arguments.
4        Closes issue #285.
5 
6 +    * Fix a false positive with string formatting checker, when using
7 +      keyword argument packing. Closes issue #288.
8 +
9 +    * Proper handle class level scope for lambdas.
10 +
11  2014-07-26  --  1.3.0
12 
13      * Allow hanging continued indentation for implicitly concatenated
14        strings. Closes issue #232.
15 
diff --git a/__pkginfo__.py b/__pkginfo__.py
@@ -21,14 +21,14 @@
16 
17  numversion = (1, 3, 0)
18  version = '.'.join([str(num) for num in numversion])
19 
20  if sys.version_info < (2, 6):
21 -    install_requires = ['logilab-common >= 0.53.0', 'astroid >= 1.1',
22 +    install_requires = ['logilab-common >= 0.53.0', 'astroid >= 1.2',
23                          'StringFormat']
24  else:
25 -    install_requires = ['logilab-common >= 0.53.0', 'astroid >= 1.1']
26 +    install_requires = ['logilab-common >= 0.53.0', 'astroid >= 1.2']
27 
28  license = 'GPL'
29  description = "python code static checker"
30  web = 'http://www.pylint.org'
31  mailinglist = "mailto://code-quality@python.org"
diff --git a/checkers/strings.py b/checkers/strings.py
@@ -384,28 +384,34 @@
32                  # to 0. It will not be present in `named`, so use the value
33                  # 0 for it.
34                  key = 0
35              if isinstance(key, int):
36                  try:
37 -                    argument = utils.get_argument_from_call(node, key)
38 +                    argname = utils.get_argument_from_call(node, key)
39                  except utils.NoSuchArgumentError:
40                      continue
41              else:
42                  if key not in named:
43                      continue
44 -                argument = named[key]
45 -            if argument in (astroid.YES, None):
46 +                argname = named[key]
47 +            if argname in (astroid.YES, None):
48                  continue
49              try:
50 -                argument = argument.infer().next()
51 +                argument = argname.infer().next()
52              except astroid.InferenceError:
53                  continue
54              if not specifiers or argument is astroid.YES:
55                  # No need to check this key if it doesn't
56                  # use attribute / item access
57                  continue
58 -
59 +            if argument.parent and isinstance(argument.parent, astroid.Arguments):
60 +                # Check to see if our argument is kwarg or vararg,
61 +                # and skip the check for this argument if so, because when inferring,
62 +                # astroid will return empty objects (dicts and tuples) and
63 +                # that can lead to false positives.
64 +                if argname.name in (argument.parent.kwarg, argument.parent.vararg):
65 +                    continue
66              previous = argument
67              parsed = []
68              for is_attribute, specifier in specifiers:
69                  if previous is astroid.YES:
70                      break
diff --git a/checkers/variables.py b/checkers/variables.py
@@ -647,12 +647,19 @@
71              # the globals one in function members when there are some common
72              # names. The only exception is when the starting scope is a
73              # comprehension and its direct outer scope is a class
74              if scope_type == 'class' and i != start_index and not (
75                      base_scope_type == 'comprehension' and i == start_index-1):
76 -                # XXX find a way to handle class scope in a smoother way
77 -                continue
78 +                # Detect if we are in a local class scope, as an assignment.
79 +                # For example, the following is fair game.
80 +                # class A:
81 +                #    b = 1
82 +                #    c = lambda b=b: b * b
83 +                class_assignment = (isinstance(frame, astroid.Class) and
84 +                                    name in frame.locals)
85 +                if not class_assignment:
86 +                    continue
87              # the name has already been consumed, only check it's not a loop
88              # variable used outside the loop
89              if name in consumed:
90                  defnode = assign_parent(consumed[name][0])
91                  self._check_late_binding_closure(node, defnode, scope_type)
@@ -687,22 +694,47 @@
92                          # check if we have a nonlocal
93                          if name in defframe.locals:
94                              maybee0601 = not any(isinstance(child, astroid.Nonlocal)
95                                                   and name in child.names
96                                                   for child in defframe.get_children())
97 +                if (self._to_consume[-1][-1] == 'lambda' and
98 +                        isinstance(frame, astroid.Class)
99 +                        and name in frame.locals):
100 +                    maybee0601 = True
101 +                else:
102 +                    maybee0601 = maybee0601 and stmt.fromlineno <= defstmt.fromlineno
103 +
104                  if (maybee0601
105 -                        and stmt.fromlineno <= defstmt.fromlineno
106                          and not is_defined_before(node)
107                          and not are_exclusive(stmt, defstmt, ('NameError',
108                                                                'Exception',
109                                                                'BaseException'))):
110                      if defstmt is stmt and isinstance(node, (astroid.DelName,
111                                                               astroid.AssName)):
112                          self.add_message('undefined-variable', args=name, node=node)
113                      elif self._to_consume[-1][-1] != 'lambda':
114 -                        # E0601 may *not* occurs in lambda scope
115 +                        # E0601 may *not* occurs in lambda scope.
116                          self.add_message('used-before-assignment', args=name, node=node)
117 +                    elif self._to_consume[-1][-1] == 'lambda':
118 +                        # E0601 can occur in class-level scope in lambdas, as in
119 +                        # the following example:
120 +                        #   class A:
121 +                        #      x = lambda attr: f + attr
122 +                        #      f = 42
123 +                        if isinstance(frame, astroid.Class) and name in frame.locals:
124 +                            if isinstance(node.parent, astroid.Arguments):
125 +                                # Doing the following is fine:
126 +                                #   class A:
127 +                                #      x = 42
128 +                                #      y = lambda attr=x: attr
129 +                                if stmt.fromlineno <= defstmt.fromlineno:
130 +                                    self.add_message('used-before-assignment',
131 +                                                     args=name, node=node)
132 +                            else:
133 +                                self.add_message('undefined-variable',
134 +                                                 args=name, node=node)
135 +
136              if isinstance(node, astroid.AssName): # Aug AssName
137                  del consumed[name]
138              else:
139                  del to_consume[name]
140              # check it's not a loop variable used outside the loop
diff --git a/setup.py b/setup.py
@@ -125,11 +125,12 @@
141                  base = modname
142              for directory in include_dirs:
143                  dest = join(self.install_dir, base, directory)
144                  if sys.version_info >= (3, 0):
145                      exclude = set(('func_unknown_encoding.py',
146 -                                   'func_invalid_encoded_data.py'))
147 +                                   'func_invalid_encoded_data.py',
148 +                                   'invalid_encoded_data.py'))
149                  else:
150                      exclude = set()
151                  shutil.rmtree(dest, ignore_errors=True)
152                  shutil.copytree(directory, dest)
153                  # since python2.5's copytree doesn't support the ignore
diff --git a/test/functional/bad_open_mode.py b/test/functional/bad_open_mode.py
@@ -0,0 +1,13 @@
154 +"""Warnings for using open() with an invalid mode string."""
155 +
156 +open('foo.bar', 'w', 2)
157 +open('foo.bar', 'rw')  # [bad-open-mode]
158 +open(name='foo.bar', buffering=10, mode='rw')  # [bad-open-mode]
159 +open(mode='rw', name='foo.bar')  # [bad-open-mode]
160 +open('foo.bar', 'U+')  # [bad-open-mode]
161 +open('foo.bar', 'rb+')  # [bad-open-mode]
162 +open('foo.bar', 'Uw')  # [bad-open-mode]
163 +open('foo.bar', 2)
164 +open('foo.bar', buffering=2)
165 +WRITE_MODE = 'w'
166 +open('foo.bar', 'U' + WRITE_MODE + 'z')  # [bad-open-mode]
diff --git a/test/functional/bad_open_mode.txt b/test/functional/bad_open_mode.txt
@@ -0,0 +1,7 @@
167 +bad-open-mode:4::"rw" is not a valid mode for open.
168 +bad-open-mode:5::"rw" is not a valid mode for open.
169 +bad-open-mode:6::"rw" is not a valid mode for open.
170 +bad-open-mode:7::"U+" is not a valid mode for open.
171 +bad-open-mode:8::"rb+" is not a valid mode for open.
172 +bad-open-mode:9::"Uw" is not a valid mode for open.
173 +bad-open-mode:13::"Uwz" is not a valid mode for open.
diff --git a/test/functional/class_scope.py b/test/functional/class_scope.py
@@ -0,0 +1,22 @@
174 +# pylint: disable=R0903,W0232
175 +"""check for scope problems"""
176 +
177 +__revision__ = None
178 +
179 +class Well(object):
180 +    """well"""
181 +    attr = 42
182 +    get_attr = lambda arg=attr: arg * 24
183 +    # +1: [used-before-assignment]
184 +    get_attr_bad = lambda arg=revattr: revattr * 42
185 +    revattr = 24
186 +    bad_lambda = lambda: get_attr_bad # [undefined-variable]
187 +
188 +    class Data(object):
189 +        """base hidden class"""
190 +    class Sub(Data): # [undefined-variable
191 +        """whaou, is Data found???"""
192 +        attr = Data() # [undefined-variable]
193 +    def func(self):
194 +        """check Sub is not defined here"""
195 +        return Sub(), self # [undefined-variable]
diff --git a/test/functional/class_scope.txt b/test/functional/class_scope.txt
@@ -0,0 +1,4 @@
196 +used-before-assignment:11:Well.<lambda>:Using variable 'revattr' before assignment
197 +undefined-variable:13:Well.<lambda>:Undefined variable 'get_attr_bad'
198 +undefined-variable:19:Well.Sub:Undefined variable 'Data'
199 +undefined-variable:22:Well.func:Undefined variable 'Sub'
diff --git a/test/functional/invalid_encoded_data.py b/test/functional/invalid_encoded_data.py
@@ -0,0 +1,5 @@
200 +# coding: utf-8
201 +"""Test data file with encoding errors."""
202 +
203 +
204 +STR = 'Су�ествительное'  # [invalid-encoded-data]
diff --git a/test/functional/invalid_encoded_data.txt b/test/functional/invalid_encoded_data.txt
@@ -0,0 +1,1 @@
205 +invalid-encoded-data:5::Cannot decode using encoding "utf-8", unexpected byte at position 11
diff --git a/test/functional/invalid_exceptions_caught.py b/test/functional/invalid_exceptions_caught.py
@@ -0,0 +1,47 @@
206 +"""Test for catching non-exceptions."""
207 +import socket
208 +
209 +
210 +class MyException(object):
211 +    """Custom 'exception'."""
212 +
213 +class MySecondException(object):
214 +    """Custom 'exception'."""
215 +
216 +class MyGoodException(Exception):
217 +    """Custom exception."""
218 +
219 +class MySecondGoodException(MyGoodException):
220 +    """Custom exception."""
221 +
222 +class SkipException(socket.error):
223 +    """Not an exception for Python 2, but one in 3."""
224 +
225 +class SecondSkipException(SkipException):
226 +    """Also a good exception."""
227 +
228 +try:
229 +    1 + 1
230 +except MyException:  # [catching-non-exception]
231 +    print "caught"
232 +
233 +try:
234 +    1 + 2
235 +# +1:[catching-non-exception,catching-non-exception]
236 +except (MyException, MySecondException):
237 +    print "caught"
238 +
239 +try:
240 +    1 + 3
241 +except MyGoodException:
242 +    print "caught"
243 +
244 +try:
245 +    1 + 3
246 +except (MyGoodException, MySecondGoodException):
247 +    print "caught"
248 +
249 +try:
250 +    1 + 3
251 +except (SkipException, SecondSkipException):
252 +    print "caught"
diff --git a/test/functional/invalid_exceptions_caught.txt b/test/functional/invalid_exceptions_caught.txt
@@ -0,0 +1,3 @@
253 +catching-non-exception:25::Catching an exception which doesn't inherit from BaseException: MyException
254 +catching-non-exception:31::Catching an exception which doesn't inherit from BaseException: MyException
255 +catching-non-exception:31::Catching an exception which doesn't inherit from BaseException: MySecondException
diff --git a/test/functional/missing_self_argument.py b/test/functional/missing_self_argument.py
@@ -0,0 +1,20 @@
256 +"""Checks that missing self in method defs don't crash Pylint."""
257 +
258 +
259 +
260 +class MyClass(object):
261 +    """A class with some methods missing self args."""
262 +
263 +    def __init__(self):
264 +        self.var = "var"
265 +
266 +    def method():  # [no-method-argument]
267 +        """A method without a self argument."""
268 +
269 +    def setup():  # [no-method-argument]
270 +        """A method without a self argument, but usage."""
271 +        self.var = 1  # [undefined-variable]
272 +
273 +    def correct(self):
274 +        """Correct."""
275 +        self.var = "correct"
diff --git a/test/functional/missing_self_argument.txt b/test/functional/missing_self_argument.txt
@@ -0,0 +1,4 @@
276 +no-method-argument:11:MyClass.method:Method has no argument
277 +no-method-argument:13:MyClass.met:Method has no argument
278 +no-method-argument:14:MyClass.setup:Method has no argument
279 +undefined-variable:16:MyClass.setup:Undefined variable 'self'
diff --git a/test/functional/superfluous_parens.py b/test/functional/superfluous_parens.py
@@ -0,0 +1,18 @@
280 +"""Test the superfluous-parens warning."""
281 +
282 +
283 +if (3 == 5):  # [superfluous-parens]
284 +    pass
285 +if not (3 == 5):  # [superfluous-parens]
286 +    pass
287 +if not (3 or 5):
288 +    pass
289 +for (x) in (1, 2, 3):  # [superfluous-parens]
290 +    print x
291 +if (1) in (1, 2, 3):  # [superfluous-parens]
292 +    pass
293 +if (1, 2) in (1, 2, 3):
294 +    pass
295 +DICT = {'a': 1, 'b': 2}
296 +del(DICT['b'])  # [superfluous-parens]
297 +del DICT['a']
diff --git a/test/functional/superfluous_parens.txt b/test/functional/superfluous_parens.txt
@@ -0,0 +1,5 @@
298 +superfluous-parens:4::Unnecessary parens after 'if' keyword
299 +superfluous-parens:6::Unnecessary parens after 'not' keyword
300 +superfluous-parens:10::Unnecessary parens after 'for' keyword
301 +superfluous-parens:12::Unnecessary parens after 'if' keyword
302 +superfluous-parens:17::Unnecessary parens after 'del' keyword
diff --git a/test/input/func_bad_open_mode.py b/test/input/func_bad_open_mode.py
@@ -1,15 +0,0 @@
303 -"""Warnings for using open() with an invalid mode string."""
304 -
305 -__revision__ = 0
306 -
307 -open('foo.bar', 'w', 2)
308 -open('foo.bar', 'rw')
309 -open(name='foo.bar', buffering=10, mode='rw')
310 -open(mode='rw', name='foo.bar')
311 -open('foo.bar', 'U+')
312 -open('foo.bar', 'rb+')
313 -open('foo.bar', 'Uw')
314 -open('foo.bar', 2)
315 -open('foo.bar', buffering=2)
316 -WRITE_MODE = 'w'
317 -open('foo.bar', 'U' + WRITE_MODE + 'z')
diff --git a/test/input/func_invalid_encoded_data.py b/test/input/func_invalid_encoded_data.py
@@ -1,6 +0,0 @@
318 -# coding: utf-8
319 -"""Test data file with encoding errors."""
320 -
321 -__revision__ = 0
322 -
323 -STR = 'Су�ествительное'
diff --git a/test/input/func_method_missing_self.py b/test/input/func_method_missing_self.py
@@ -1,24 +0,0 @@
324 -"""Checks that missing self in method defs don't crash Pylint !
325 -"""
326 -
327 -__revision__ = ''
328 -
329 -
330 -class MyClass(object):
331 -    """SimpleClass
332 -    """
333 -
334 -    def __init__(self):
335 -        self.var = "var"
336 -
337 -    def met():
338 -        """Checks that missing self dont crash Pylint !
339 -        """
340 -
341 -    def correct(self):
342 -        """yo"""
343 -        self.var = "correct"
344 -
345 -if __name__ == '__main__':
346 -    OBJ = MyClass()
347 -
diff --git a/test/input/func_method_without_self_but_self_assignment.py b/test/input/func_method_without_self_but_self_assignment.py
@@ -1,15 +0,0 @@
348 -# pylint: disable=R0903
349 -"""regression test: setup() leads to "unable to load module..."
350 -"""
351 -
352 -__revision__ = 1
353 -
354 -class Example(object):
355 -    """bla"""
356 -
357 -    def __init__(self):
358 -        pass
359 -
360 -    def setup():
361 -        "setup without self"
362 -        self.foo = 1
diff --git a/test/input/func_scope_regrtest.py b/test/input/func_scope_regrtest.py
@@ -1,15 +0,0 @@
363 -# pylint: disable=R0903,W0232
364 -"""check for scope problems"""
365 -
366 -__revision__ = None
367 -
368 -class Well(object):
369 -    """well"""
370 -    class Data(object):
371 -        """base hidden class"""
372 -    class Sub(Data):
373 -        """whaou, is Data found???"""
374 -        attr = Data()
375 -    def func(self):
376 -        """check Sub is not defined here"""
377 -        return Sub(), self
diff --git a/test/input/func_string_format_py27.py b/test/input/func_string_format_py27.py
@@ -76,5 +76,13 @@
378      print "{a[0]}".format(a=object)

379      print log("{}".format(2, "info"))

380      print "{0.missing}".format(2)

381      print "{0} {1} {2}".format(1, 2)

382      print "{0} {1}".format(1, 2, 3)

383 +

384 +def good_issue288(*args, **kwargs):

385 +    """ Test that using kwargs does not emit a false

386 +    positive.

387 +    """

388 +    data = 'Hello John Doe {0[0]}'.format(args)

389 +    print data

390 +    return 'Hello {0[name]}'.format(kwargs)

diff --git a/test/input/func_superfluous_parens.py b/test/input/func_superfluous_parens.py
@@ -1,19 +0,0 @@
391 -"""Test the superfluous-parens warning."""
392 -
393 -__revision__ = 1
394 -
395 -if (3 == 5):
396 -    pass
397 -if not (3 == 5):
398 -    pass
399 -if not (3 or 5):
400 -    pass
401 -for (x) in (1, 2, 3):
402 -    print x
403 -if (1) in (1, 2, 3):
404 -    pass
405 -if (1, 2) in (1, 2, 3):
406 -    pass
407 -DICT = {'a': 1, 'b': 2}
408 -del(DICT['b'])
409 -del DICT['a']
diff --git a/test/messages/func_bad_open_mode.txt b/test/messages/func_bad_open_mode.txt
@@ -1,7 +0,0 @@
410 -W:  6: "rw" is not a valid mode for open.
411 -W:  7: "rw" is not a valid mode for open.
412 -W:  8: "rw" is not a valid mode for open.
413 -W:  9: "U+" is not a valid mode for open.
414 -W: 10: "rb+" is not a valid mode for open.
415 -W: 11: "Uw" is not a valid mode for open.
416 -W: 15: "Uwz" is not a valid mode for open.
diff --git a/test/messages/func_invalid_encoded_data.txt b/test/messages/func_invalid_encoded_data.txt
@@ -1,1 +0,0 @@
417 -W:  6: Cannot decode using encoding "utf-8", unexpected byte at position 11
diff --git a/test/messages/func_method_missing_self.txt b/test/messages/func_method_missing_self.txt
@@ -1,1 +0,0 @@
418 -E: 14:MyClass.met: Method has no argument
diff --git a/test/messages/func_method_without_self_but_self_assignment.txt b/test/messages/func_method_without_self_but_self_assignment.txt
@@ -1,2 +0,0 @@
419 -E: 13:Example.setup: Method has no argument
420 -E: 15:Example.setup: Undefined variable 'self'
diff --git a/test/messages/func_scope_regrtest.txt b/test/messages/func_scope_regrtest.txt
@@ -1,2 +0,0 @@
421 -E: 12:Well.Sub: Undefined variable 'Data'
422 -E: 15:Well.func: Undefined variable 'Sub'
diff --git a/test/messages/func_superfluous_parens.txt b/test/messages/func_superfluous_parens.txt
@@ -1,5 +0,0 @@
423 -C:  5: Unnecessary parens after 'if' keyword
424 -C:  7: Unnecessary parens after 'not' keyword
425 -C: 11: Unnecessary parens after 'for' keyword
426 -C: 13: Unnecessary parens after 'if' keyword
427 -C: 18: Unnecessary parens after 'del' keyword
diff --git a/test/test_func.py b/test/test_func.py
@@ -46,11 +46,12 @@
428      """check that all testable messages have been checked"""
429      PORTED = set(['I0001', 'I0010', 'W0712', 'E1001', 'W1402', 'E1310', 'E0202',
430                    'W0711', 'W0108', 'E0603', 'W0710', 'E0710', 'E0711', 'W1001', 
431                    'E1124', 'E1120', 'E1121', 'E1123', 'E1003', 'E1002', 'W0212',
432                    'C0327', 'C0328',
433 -                  'W0109', 'E1004', 'W0604', 'W0601', 'W0602', 'C0112', 'C0330'])
434 +                  'W0109', 'E1004', 'W0604', 'W0601', 'W0602', 'C0112', 'C0330',
435 +                  'C0325', 'E0211', 'W1501'])
436 
437      @testlib.tag('coverage')
438      def test_exhaustivity(self):
439          # skip fatal messages
440          not_tested = set(msg.msgid for msg in linter.msgs_store.messages