diff options
author | Sylvain Th?nault <thenault@gmail.com> | 2013-10-03 16:27:26 +0200 |
---|---|---|
committer | Sylvain Th?nault <thenault@gmail.com> | 2013-10-03 16:27:26 +0200 |
commit | 0a53cc2d8e60ade303893d19ff1b0d2e8be70ad4 (patch) | |
tree | 5e8d2cda54a8649b04e1d231d537566adc988906 | |
parent | 8c1788f1cec10a874aa5f398832f71d98e44c366 (diff) | |
parent | 8cfc7f6be64bc107beea841f61680c7111d3d9bf (diff) | |
download | pylint-0a53cc2d8e60ade303893d19ff1b0d2e8be70ad4.tar.gz |
Merged in TobiasRzepka/pylint (pull request #58)
#51: Building pylint Windows installer for Python3 fails
-rw-r--r-- | ChangeLog | 2 | ||||
-rw-r--r-- | __main__.py | 3 | ||||
-rw-r--r-- | doc/Makefile | 4 | ||||
-rw-r--r-- | doc/contribute.rst | 2 | ||||
-rw-r--r-- | doc/output.rst | 24 | ||||
-rw-r--r-- | doc/run.rst | 22 | ||||
-rw-r--r-- | doc/tutorial.rst | 123 | ||||
-rw-r--r-- | test/input/func_noerror_defined_and_used_on_same_line_py27.py | 2 | ||||
-rw-r--r-- | test/test_misc.py | 48 | ||||
-rw-r--r-- | test/unittest_lint.py | 73 | ||||
-rw-r--r-- | test/unittest_reporting.py | 29 | ||||
-rw-r--r-- | utils.py | 160 |
12 files changed, 258 insertions, 234 deletions
@@ -2,6 +2,8 @@ ChangeLog for Pylint ==================== -- + * Run pylint as a python module 'python -m pylint' (anatoly techtonik) + * Check for non-exception classes inside an except clause * epylint support options to give to pylint after the file to analyze and diff --git a/__main__.py b/__main__.py new file mode 100644 index 0000000..7716361 --- /dev/null +++ b/__main__.py @@ -0,0 +1,3 @@ +#!/usr/bin/env python +import pylint +pylint.run_pylint() diff --git a/doc/Makefile b/doc/Makefile index 8b8b3ec..98076a6 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -37,7 +37,7 @@ clean: -rm -rf $(BUILDDIR)/* -rm -f features.rst -html: features.rst +html: features $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." @@ -131,7 +131,7 @@ doctest: "results in $(BUILDDIR)/doctest/output.txt." -features.rst: +features: rm -f features.rst echo "Pylint features" > features.rst echo "===============" >> features.rst diff --git a/doc/contribute.rst b/doc/contribute.rst index 2468933..6a45dea 100644 --- a/doc/contribute.rst +++ b/doc/contribute.rst @@ -135,7 +135,7 @@ sorted. E.g on Unix system, you may generate it using:: Also, here are a few naming convention which are used: -* Python files starting with 'func_noerror_' don't have any message file +* Python files starting with 'func_noerror' don't have any message file associated as they are expected to provide no output at all * You may provide different input files (and associated output) depending on the diff --git a/doc/output.rst b/doc/output.rst index 6617ac4..3a87547 100644 --- a/doc/output.rst +++ b/doc/output.rst @@ -24,20 +24,34 @@ obj object within the module (if any) msg text of the message +msg_id + the message code (eg. I0011) symbol - symbolic name of the message + symbolic name of the message (eg. locally-disabled) C one letter indication of the message category category fullname of the message category -For exemple the default format can be obtained with:: +For exemple the former (pre 1.0) default format can be obtained with:: - pylint --msg-template='{sigle}:{line:3d},{column}: {obj}: {msg}' + pylint --msg-template='{msg_id}:{line:3d},{column}: {obj}: {msg}' + +A few other examples: + +* the new default format:: + + {C}:{line:3d},{column:2d}: {msg} ({symbol}) + +* Visual Studio compatible format (former 'msvs' output format):: + + {path}({line}): [{msg_id}{obj}] {msg} + +* Parseable (Emacs and all, former 'parseable' output format) format:: + + {path}:{line}: [{msg_id}({symbol}), {obj}] {msg} -and a Visual Studio compatible format can be obtained with:: - pylint --msg-template='{path}({line}): [{sigle}{obj}] {msg}' .. _Python new format syntax: http://docs.python.org/2/library/string.html#formatstrings diff --git a/doc/run.rst b/doc/run.rst index 7360c3e..4b0ccfa 100644 --- a/doc/run.rst +++ b/doc/run.rst @@ -97,18 +97,14 @@ hand tune the configuration. Other useful global options include: ---zope Initialize Zope products before starting ---ignore=file Add <file> (may be a directory) to the black - list. It should be a base name, not a path. - You may set this option multiple times. ---statistics=y_or_n Compute statistics on collected data. ---persistent=y_or_n Pickle collected data for later comparisons. ---comment=y_or_n Add a comment according to your evaluation note. ---parseable=y_or_n Use a parseable output format. ---html=y_or_n Use HTML as output format instead of text. ---list-msgs Generate pylint's messages. ---full-documentation Generate pylint's full documentation, in reST format. ---include_ids=y_or_n Show numeric ids of messages (like 'C0301') ---symbols=y_or_n Show symbolic ids of messsages (like 'line-too-long') +--ignore=file Add <file> (may be a directory) to the black + list. It should be a base name, not a path. + You may set this option multiple times. +--persistent=y_or_n Pickle collected data for later comparisons. +--output-format=<format> Select output format (text, html, custom). +--msg-template=<template> Modifiy text output message template. +--list-msgs Generate pylint's messages. +--full-documentation Generate pylint's full documentation, in reST + format. diff --git a/doc/tutorial.rst b/doc/tutorial.rst index 5561c50..cbe4cb5 100644 --- a/doc/tutorial.rst +++ b/doc/tutorial.rst @@ -69,7 +69,6 @@ A couple of the options that we'll focus on here are: :: Reports: --files-output=<y_or_n> --reports=<y_or_n> - --include-ids=<y_or_n> --output-format=<format> Also pay attention to the last bit of help output. This gives you a hint of what @@ -138,16 +137,16 @@ If we run this: robertk01 Desktop$ pylint simplecaeser.py No config file found, using default configuration ************* Module simplecaeser - C: 1: Missing docstring - W: 3: Uses of a deprecated module 'string' - C: 5: Invalid name "shift" (should match (([A-Z_][A-Z1-9_]*)|(__.*__))$) - C: 6: Invalid name "choice" (should match (([A-Z_][A-Z1-9_]*)|(__.*__))$) - C: 7: Invalid name "word" (should match (([A-Z_][A-Z1-9_]*)|(__.*__))$) - C: 8: Invalid name "letters" (should match (([A-Z_][A-Z1-9_]*)|(__.*__))$) - C: 9: Invalid name "encoded" (should match (([A-Z_][A-Z1-9_]*)|(__.*__))$) - C: 16: Operator not preceded by a space - encoded=encoded + letters[x] - ^ + C: 1, 0: Missing module docstring (missing-docstring) + W: 3, 0: Uses of a deprecated module 'string' (deprecated-module) + C: 5, 0: Invalid constant name "shift" (invalid-name) + C: 6, 0: Invalid constant name "choice" (invalid-name) + C: 7, 0: Invalid constant name "word" (invalid-name) + C: 8, 0: Invalid constant name "letters" (invalid-name) + C: 9, 0: Invalid constant name "encoded" (invalid-name) + C: 16,12: Operator not preceded by a space + encoded=encoded + letters[x] + ^ (no-space-before-operator) Report @@ -221,17 +220,17 @@ If we run this: Messages -------- - +-----------+-----------+ - |message id |occurrences | - +===========+===========+ - |C0103 |5 | - +-----------+-----------+ - |W0402 |1 | - +-----------+-----------+ - |C0322 |1 | - +-----------+-----------+ - |C0111 |1 | - +-----------+-----------+ + +-------------------------+------------+ + |message id |occurrences | + +=========================+============+ + |invalid-name |5 | + +-------------------------+------------+ + |no-space-before-operator |1 | + +-------------------------+------------+ + |missing-docstring |1 | + +-------------------------+------------+ + |deprecated-module |1 | + +-------------------------+------------+ @@ -244,59 +243,42 @@ Wow. That's a lot of stuff. The first part is the 'messages' section while the second part is the 'report' section. There are two points I want to tackle here. First point is that all the tables of statistics (i.e. the report) are a bit -overwhelming so I want to silence them. To do that, I will use the "--reports=n" option. +overwhelming so I want to silence them. To do that, I will use the +"--reports=n" option. + +.. tip:: Many of Pylint's commonly used command line options have shortcuts. + for example, "--reports=n" can be abbreviated to "-rn". Pylint's man page lists + all these shortcuts. Second, previous experience taught me that the default output for the messages needed a bit more info. We can see the first line is: :: - "C: 1: Missing docstring" + "C: 1: Missing docstring (missing-docstring)" This basically means that line 1 violates a convention 'C'. It's telling me I really should have a docstring. I agree, but what if I didn't fully understand what rule I violated. Knowing only that I violated a convention isn't much help -if I'm a newbie. So let's turn on a bit more info by using the option -"--include-ids=y". - -.. tip:: Many of Pylint's commonly used command line options have shortcuts. - for example, "--reports=n" can be abbreviated to "-r n", and "--include-ids=y" - can be abbreviated to "-i y". Pylint's man page lists all these shortcuts. - -Let's do it again! +if I'm a newbie. Another information there is the message symbol between parens, +`missing-docstring` here. -.. sourcecode:: bash - - robertk01 Desktop$ pylint --reports=n --include-ids=y simplecaeser.py - No config file found, using default configuration - ************* Module simplecaeser - C0111: 1: Missing docstring - W0402: 3: Uses of a deprecated module 'string' - C0103: 5: Invalid name "shift" (should match (([A-Z_][A-Z1-9_]*)|(__.*__))$) - C0103: 6: Invalid name "choice" (should match (([A-Z_][A-Z1-9_]*)|(__.*__))$) - C0103: 7: Invalid name "word" (should match (([A-Z_][A-Z1-9_]*)|(__.*__))$) - C0103: 8: Invalid name "letters" (should match (([A-Z_][A-Z1-9_]*)|(__.*__))$) - C0103: 9: Invalid name "encoded" (should match (([A-Z_][A-Z1-9_]*)|(__.*__))$) - C0322: 16: Operator not preceded by a space - encoded=encoded + letters[x] - -Oooh. I like that better. Now I know that I violated the convention number -C0111 and now I can read up a bit more about that. Let's go back to the +If I want to read up a bit more about that, I can go back to the command line and try this: .. sourcecode:: bash - robertk01 Desktop$ pylint --help-msg=C0111 + robertk01 Desktop$ pylint --help-msg=missing-docstring No config file found, using default configuration - :C0111: *Missing docstring* + :missing-docstring (C0111): *Missing docstring* Used when a module, function, class or method has no docstring. Some special methods like __init__ doesn't necessary require a docstring. This message belongs to the basic checker. -Yeah, ok. That one was a bit of a no-brainer but I have run into error messages +Yeah, ok. That one was a bit of a no-brainer but I have run into error messages that left me with no clue about what went wrong, simply because I was unfamiliar with the underlying mechanism of code theory. One error that puzzled my newbie mind was: :: - :R0902: *Too many instance attributes (%s/%s)* + :too-many-instance-attributes (R0902): *Too many instance attributes (%s/%s)* I get it now thanks to Pylint pointing it out to me. If you don't get that one, pour a fresh cup of coffee and look into it - let your programmer mind grow! @@ -309,13 +291,13 @@ Now that we got some configuration stuff out of the way, let's see what we can do with the remaining warnings. If we add a docstring to describe what the code is meant to do that will help. -I'm also going to be a bit cowboy and ignore the W0402 message because I like to -take risks in life. A deprecation warning means that future versions of Python -may not support that code so my code may break in the future. There are 5 C0103 -messages that we will get to later. Lastly, I violated the convention of using -spaces around an operator such as "=" so I'll fix that too. To sum up, I'll add -a docstring to line 2, put spaces around the = sign on line 16 and use the -"--disable=W0402" to ignore the deprecation warning. +I'm also going to be a bit cowboy and ignore the `deprecated-module` message +because I like to take risks in life. A deprecation warning means that future +versions of Python may not support that code so my code may break in the future. +There are 5 `invalid-name` messages that we will get to later. Lastly, I violated the +convention of using spaces around an operator such as "=" so I'll fix that too. +To sum up, I'll add a docstring to line 2, put spaces around the = sign on line +16 and use the `--disable=deprecated-module` to ignore the deprecation warning. Here is the updated code: @@ -349,20 +331,21 @@ Here is the updated code: 26 27 print encoded -And here is what happens when we run it with our --disable=W0402 option: +And here is what happens when we run it with our `--disable=deprecated-module` +option: .. sourcecode:: bash - robertk01 Desktop$ pylint --reports=n --include-ids=y --disable=W0402 simplecaeser.py + robertk01 Desktop$ pylint --reports=n --disable=deprecated-module simplecaeser.py No config file found, using default configuration ************* Module simplecaeser - C0103: 7: Invalid name "shift" (should match (([A-Z_][A-Z1-9_]*)|(__.*__))$) - C0103: 8: Invalid name "choice" (should match (([A-Z_][A-Z1-9_]*)|(__.*__))$) - C0103: 9: Invalid name "word" (should match (([A-Z_][A-Z1-9_]*)|(__.*__))$) - C0103: 10: Invalid name "letters" (should match (([A-Z_][A-Z1-9_]*)|(__.*__))$) - C0103: 11: Invalid name "encoded" (should match (([A-Z_][A-Z1-9_]*)|(__.*__))$) + C: 7, 0: Invalid constant name "shift" (invalid-name) + C: 8, 0: Invalid constant name "choice" (invalid-name) + C: 9, 0: Invalid constant name "word" (invalid-name) + C: 10, 0: Invalid constant name "letters" (invalid-name) + C: 11, 0: Invalid constant name "encoded" (invalid-name) -Nice! We're down to just the C0103 messages. +Nice! We're down to just the `invalid-name` messages. There are fairly well defined conventions around naming things like instance variables, functions, classes, etc. The conventions focus on the use of @@ -380,12 +363,12 @@ of all lowercase. The appropriate rule would be something like: "should match [a-z\_][a-z0-9\_]{2,30}$". Notice the lowercase letters in the regular expression (a-z versus A-Z). -If we run that rule using a --const-rgx='[a-z\_][a-z0-9\_]{2,30}$' option, it +If we run that rule using a `--const-rgx='[a-z\_][a-z0-9\_]{2,30}$'` option, it will now be quite quiet: .. sourcecode:: bash - robertk01 Desktop$ pylint --reports=n --include-ids=y --disable=W0402 --const-rgx='[a-z_][a-z0-9_]{2,30}$' simplecaeser.py + robertk01 Desktop$ pylint --reports=n --disable=deprecated-module --const-rgx='[a-z_][a-z0-9_]{2,30}$' simplecaeser.py No config file found, using default configuration Regular expressions can be quite a beast so take my word on this particular diff --git a/test/input/func_noerror_defined_and_used_on_same_line_py27.py b/test/input/func_noerror_defined_and_used_on_same_line_py27.py index 5a75722..11b1a2d 100644 --- a/test/input/func_noerror_defined_and_used_on_same_line_py27.py +++ b/test/input/func_noerror_defined_and_used_on_same_line_py27.py @@ -1,5 +1,7 @@ #pylint: disable=C0111,C0321 """pylint complains about 'index' being used before definition""" +__revision__ = 1 + with open('f') as f, open(f.read()) as g: print g.read() diff --git a/test/test_misc.py b/test/test_misc.py index 68f3e4a..d2c03c0 100644 --- a/test/test_misc.py +++ b/test/test_misc.py @@ -17,44 +17,50 @@ Tests for the misc checker. """ import tempfile +import os +import contextlib from logilab.common.testlib import unittest_main from astroid import test_utils from pylint.checkers import misc from pylint.testutils import CheckerTestCase, Message +@contextlib.contextmanager +def create_file_backed_module(code): + # Can't use tempfile.NamedTemporaryFile here + # because on Windows the file must be closed before writing to it, + # see http://bugs.python.org/issue14243 + fd, tmp = tempfile.mkstemp() + os.write(fd, code) + + try: + module = test_utils.build_module(code) + module.file = tmp + yield module + finally: + os.close(fd) + os.remove(tmp) + class FixmeTest(CheckerTestCase): CHECKER_CLASS = misc.EncodingChecker - def create_file_backed_module(self, code): - tmp = tempfile.NamedTemporaryFile() - tmp.write(code) - tmp.flush() - module = test_utils.build_module(code) - module.file = tmp.name - # Just make sure to keep a reference to the file - # so it isn't deleted. - module._tmpfile = tmp - return module - def test_fixme(self): - module = self.create_file_backed_module( + with create_file_backed_module( """a = 1 - # FIXME - """) - with self.assertAddsMessages( - Message(msg_id='W0511', line=2, args=u'FIXME')): - self.checker.process_module(module) + # FIXME """) as module: + with self.assertAddsMessages( + Message(msg_id='W0511', line=2, args=u'FIXME')): + self.checker.process_module(module) def test_emtpy_fixme_regex(self): self.checker.config.notes = [] - module = self.create_file_backed_module( + with create_file_backed_module( """a = 1 # fixme - """) - with self.assertNoMessages(): - self.checker.process_module(module) + """) as module: + with self.assertNoMessages(): + self.checker.process_module(module) if __name__ == '__main__': diff --git a/test/unittest_lint.py b/test/unittest_lint.py index a65eb01..7612ef4 100644 --- a/test/unittest_lint.py +++ b/test/unittest_lint.py @@ -26,22 +26,16 @@ from logilab.common.compat import reload from pylint import config from pylint.lint import PyLinter, Run, UnknownMessage, preprocess_options, \ ArgumentPreprocessingError -from pylint.utils import sort_msgs, PyLintASTWalker, MSG_STATE_SCOPE_CONFIG, \ - MSG_STATE_SCOPE_MODULE, tokenize_module +from pylint.utils import MSG_STATE_SCOPE_CONFIG, MSG_STATE_SCOPE_MODULE, \ + PyLintASTWalker, MessageDefinition, build_message_def, tokenize_module from pylint.testutils import TestReporter from pylint.reporters import text from pylint import checkers -class SortMessagesTC(TestCase): - - def test(self): - l = ['E0501', 'E0503', 'F0002', 'I0201', 'W0540', - 'R0202', 'F0203', 'R0220', 'W0321', 'I0001'] - self.assertEqual(sort_msgs(l), ['E0501', 'E0503', - 'W0321', 'W0540', - 'R0202', 'R0220', - 'I0001', 'I0201', - 'F0002', 'F0203',]) +if sys.platform == 'win32': + HOME = 'USERPROFILE' +else: + HOME = 'HOME' class GetNoteMessageTC(TestCase): def test(self): @@ -83,11 +77,40 @@ class PyLinterTC(TestCase): checkers.initialize(self.linter) self.linter.set_reporter(TestReporter()) + def test_check_message_id(self): + self.assertIsInstance(self.linter.check_message_id('F0001'), + MessageDefinition) + self.assertRaises(UnknownMessage, + self.linter.check_message_id, 'YB12') + def test_message_help(self): - msg = self.linter.get_message_help('F0001', checkerref=True) - expected = ':F0001 (fatal):\n Used when an error occurred preventing the analysis of a module (unable to\n find it for instance). This message belongs to the master checker.' - self.assertMultiLineEqual(msg, expected) - self.assertRaises(UnknownMessage, self.linter.get_message_help, 'YB12') + msg = self.linter.check_message_id('F0001') + self.assertMultiLineEqual( + ''':F0001 (fatal): + Used when an error occurred preventing the analysis of a module (unable to + find it for instance). This message belongs to the master checker.''', + msg.format_help(checkerref=True)) + self.assertMultiLineEqual( + ''':F0001 (fatal): + Used when an error occurred preventing the analysis of a module (unable to + find it for instance).''', + msg.format_help(checkerref=False)) + + def test_message_help_minmax(self): + # build the message manually to be python version independant + msg = build_message_def(self.linter._checkers['typecheck'][0], + 'E1122', checkers.typecheck.MSGS['E1122']) + self.assertMultiLineEqual( + ''':E1122 (duplicate-keyword-arg): *Duplicate keyword argument %r in function call* + Used when a function call passes the same keyword argument multiple times. + This message belongs to the typecheck checker. It can't be emitted when using + Python >= 2.6.''', + msg.format_help(checkerref=True)) + self.assertMultiLineEqual( + ''':E1122 (duplicate-keyword-arg): *Duplicate keyword argument %r in function call* + Used when a function call passes the same keyword argument multiple times. + This message can't be emitted when using Python >= 2.6.''', + msg.format_help(checkerref=False)) def test_enable_message(self): linter = self.linter @@ -369,9 +392,9 @@ class ConfigTC(TestCase): def test_pylintrc(self): fake_home = tempfile.mkdtemp('fake-home') - home = os.environ['HOME'] + home = os.environ[HOME] try: - os.environ['HOME'] = fake_home + os.environ[HOME] = fake_home self.assertEqual(config.find_pylintrc(), None) os.environ['PYLINTRC'] = join(tempfile.gettempdir(), '.pylintrc') self.assertEqual(config.find_pylintrc(), None) @@ -379,7 +402,7 @@ class ConfigTC(TestCase): self.assertEqual(config.find_pylintrc(), None) finally: os.environ.pop('PYLINTRC', '') - os.environ['HOME'] = home + os.environ[HOME] = home rmtree(fake_home, ignore_errors=True) reload(config) @@ -397,12 +420,12 @@ class ConfigTC(TestCase): 'a/b/c/__init__.py', 'a/b/c/d/__init__.py'], chroot) os.chdir(chroot) fake_home = tempfile.mkdtemp('fake-home') - home = os.environ['HOME'] + home = os.environ[HOME] try: - os.environ['HOME'] = fake_home + os.environ[HOME] = fake_home self.assertEqual(config.find_pylintrc(), None) finally: - os.environ['HOME'] = home + os.environ[HOME] = home os.rmdir(fake_home) results = {'a' : join(chroot, 'a', 'pylintrc'), 'a/b' : join(chroot, 'a', 'b', 'pylintrc'), @@ -427,8 +450,8 @@ class ConfigTC(TestCase): chdir(cdir) fake_home = tempfile.mkdtemp('fake-home') - home = os.environ['HOME'] - os.environ['HOME'] = fake_home + home = os.environ[HOME] + os.environ[HOME] = fake_home try: create_files(['a/pylintrc', 'a/b/pylintrc', 'a/b/c/d/__init__.py'], chroot) os.chdir(chroot) @@ -442,7 +465,7 @@ class ConfigTC(TestCase): os.chdir(join(chroot, basedir)) self.assertEqual(config.find_pylintrc(), expected) finally: - os.environ['HOME'] = home + os.environ[HOME] = home rmtree(fake_home, ignore_errors=True) os.chdir(HERE) rmtree(chroot) diff --git a/test/unittest_reporting.py b/test/unittest_reporting.py index d338670..8fda31d 100644 --- a/test/unittest_reporting.py +++ b/test/unittest_reporting.py @@ -1,4 +1,4 @@ -# Copyright (c) 2003-2012 LOGILAB S.A. (Paris, FRANCE). +# Copyright (c) 2003-2013 LOGILAB S.A. (Paris, FRANCE). # This program is free software; you can redistribute it and/or modify it under # the terms of the GNU General Public License as published by the Free Software # Foundation; either version 2 of the License, or (at your option) any later @@ -12,23 +12,13 @@ # this program; if not, write to the Free Software Foundation, Inc., # 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. -import sys import os -import tempfile -from shutil import rmtree -from os import getcwd, chdir -from os.path import join, basename, dirname, isdir, abspath +from os.path import join, dirname, abspath from cStringIO import StringIO -from logilab.common.testlib import TestCase, unittest_main, create_files -from logilab.common.compat import reload +from logilab.common.testlib import TestCase, unittest_main -from pylint import config -from pylint.lint import PyLinter, Run, UnknownMessage, preprocess_options, \ - ArgumentPreprocessingError -from pylint.utils import sort_msgs, PyLintASTWalker, MSG_STATE_SCOPE_CONFIG, \ - MSG_STATE_SCOPE_MODULE, tokenize_module -from pylint.testutils import TestReporter +from pylint.lint import PyLinter from pylint import checkers from pylint.reporters.text import TextReporter @@ -46,12 +36,6 @@ class PyLinterTC(TestCase): os.environ.pop('PYLINTRC', None) def test_template_option(self): - # self.linter.set_reporter(TextReporter()) - expected = ( '************* Module 0123\n' - 'C0301:001\n' - 'C0301:002\n' - ) - output = StringIO() self.linter.reporter.set_output(output) self.linter.set_option('msg-template', '{msg_id}:{line:03d}') @@ -59,7 +43,10 @@ class PyLinterTC(TestCase): self.linter.set_current_module('0123') self.linter.add_message('C0301', line=1, args=(1, 2)) self.linter.add_message('line-too-long', line=2, args=(3, 4)) - self.assertMultiLineEqual(output.getvalue(), expected) + self.assertMultiLineEqual(output.getvalue(), + '************* Module 0123\n' + 'C0301:001\n' + 'C0301:002\n') if __name__ == '__main__': @@ -20,8 +20,8 @@ main pylint class import re import sys import tokenize -from warnings import warn import os +from warnings import warn from os.path import dirname, basename, splitext, exists, isdir, join, normpath from logilab.common.interface import implements @@ -43,7 +43,6 @@ class EmptyReport(Exception): """raised when a report is empty and so should not be displayed""" - MSG_TYPES = { 'I' : 'info', 'C' : 'convention', @@ -77,17 +76,6 @@ class WarningScope(object): NODE = 'node-based-msg' -def sort_msgs(msgids): - """sort message identifiers according to their category first""" - msgs = {} - for msg in msgids: - msgs.setdefault(msg[0], []).append(msg) - result = [] - for m_id in _MSG_ORDER: - if m_id in msgs: - result.extend( sorted(msgs[m_id]) ) - return result - def get_module_and_frameid(node): """return the module name and the frame id in the module""" frame = node.frame() @@ -122,18 +110,78 @@ def tokenize_module(module): return list(tokenize.generate_tokens(readline)) return list(tokenize.tokenize(readline)) +def build_message_def(checker, msgid, msg_tuple): + if implements(checker, (IRawChecker, ITokenChecker)): + default_scope = WarningScope.LINE + else: + default_scope = WarningScope.NODE + options = {} + if len(msg_tuple) > 3: + (msg, symbol, descr, options) = msg_tuple + elif len(msg_tuple) > 2: + (msg, symbol, descr) = msg_tuple[:3] + else: + # messages should have a symbol, but for backward compatibility + # they may not. + (msg, descr) = msg_tuple + warn("[pylint 0.26] description of message %s doesn't include " + "a symbolic name" % msgid, DeprecationWarning) + symbol = None + options.setdefault('scope', default_scope) + return MessageDefinition(checker, msgid, msg, descr, symbol, **options) + class MessageDefinition(object): - def __init__(self, checker, msgid, msg, descr, symbol, scope): + def __init__(self, checker, msgid, msg, descr, symbol, scope, + minversion=None, maxversion=None): + self.checker = checker assert len(msgid) == 5, 'Invalid message id %s' % msgid assert msgid[0] in MSG_TYPES, \ 'Bad message type %s in %r' % (msgid[0], msgid) self.msgid = msgid self.msg = msg self.descr = descr - self.checker = checker self.symbol = symbol self.scope = scope + self.minversion = minversion + self.maxversion = maxversion + + def may_be_emitted(self): + """return True if message may be emitted using the current interpreter""" + if self.minversion is not None and self.minversion > sys.version_info: + return False + if self.maxversion is not None and self.maxversion <= sys.version_info: + return False + return True + + def format_help(self, checkerref=False): + """return the help string for the given message id""" + desc = self.descr + if checkerref: + desc += ' This message belongs to the %s checker.' % \ + self.checker.name + title = self.msg + if self.symbol: + msgid = '%s (%s)' % (self.symbol, self.msgid) + else: + msgid = self.msgid + if self.minversion or self.maxversion: + restr = [] + if self.minversion: + restr.append('< %s' % '.'.join([str(n) for n in self.minversion])) + if self.maxversion: + restr.append('>= %s' % '.'.join([str(n) for n in self.maxversion])) + restr = ' or '.join(restr) + if checkerref: + desc += " It can't be emitted when using Python %s." % restr + else: + desc += " This message can't be emitted when using Python %s." % restr + desc = normalize_text(' '.join(desc.split()), indent=' ') + if title != '%s': + title = title.splitlines()[0] + return ':%s: *%s*\n%s' % (msgid, title, desc) + return ':%s:\n%s' % (msgid, desc) + class MessagesHandlerMixIn(object): """a mix-in class containing all the messages related methods for the main @@ -162,65 +210,23 @@ class MessagesHandlerMixIn(object): message ids should be a string of len 4, where the two first characters are the checker id and the two last the message id in this checker """ - msgs_dict = checker.msgs chkid = None - - for msgid, msg_tuple in msgs_dict.iteritems(): - if implements(checker, (IRawChecker, ITokenChecker)): - scope = WarningScope.LINE - else: - scope = WarningScope.NODE - if len(msg_tuple) > 2: - (msg, msgsymbol, msgdescr) = msg_tuple[:3] - assert msgsymbol not in self._messages_by_symbol, \ - 'Message symbol %r is already defined' % msgsymbol - if len(msg_tuple) > 3: - if 'scope' in msg_tuple[3]: - scope = msg_tuple[3]['scope'] - if 'minversion' in msg_tuple[3]: - minversion = msg_tuple[3]['minversion'] - if minversion > sys.version_info: - self._msgs_state[msgid] = False - continue - if 'maxversion' in msg_tuple[3]: - maxversion = msg_tuple[3]['maxversion'] - if maxversion <= sys.version_info: - self._msgs_state[msgid] = False - continue - else: - # messages should have a symbol, but for backward compatibility - # they may not. - (msg, msgdescr) = msg_tuple - warn("[pylint 0.26] description of message %s doesn't include " - "a symbolic name" % msgid, DeprecationWarning) - msgsymbol = None + for msgid, msg_tuple in checker.msgs.iteritems(): + msg = build_message_def(checker, msgid, msg_tuple) + assert msg.symbol not in self._messages_by_symbol, \ + 'Message symbol %r is already defined' % msg.symbol # avoid duplicate / malformed ids - assert msgid not in self._messages, \ + assert msg.msgid not in self._messages, \ 'Message id %r is already defined' % msgid - assert chkid is None or chkid == msgid[1:3], \ + assert chkid is None or chkid == msg.msgid[1:3], \ 'Inconsistent checker part in message id %r' % msgid - chkid = msgid[1:3] - msg = MessageDefinition(checker, msgid, msg, msgdescr, msgsymbol, scope) - self._messages[msgid] = msg - self._messages_by_symbol[msgsymbol] = msg - self._msgs_by_category.setdefault(msgid[0], []).append(msgid) - - def get_message_help(self, msgid, checkerref=False): - """return the help string for the given message id""" - msg = self.check_message_id(msgid) - desc = normalize_text(' '.join(msg.descr.split()), indent=' ') - if checkerref: - desc += ' This message belongs to the %s checker.' % \ - msg.checker.name - title = msg.msg - if msg.symbol: - symbol_part = ' (%s)' % msg.symbol - else: - symbol_part = '' - if title != '%s': - title = title.splitlines()[0] - return ':%s%s: *%s*\n%s' % (msg.msgid, symbol_part, title, desc) - return ':%s%s:\n%s' % (msg.msgid, symbol_part, desc) + chkid = msg.msgid[1:3] + if not msg.may_be_emitted(): + self._msgs_state[msg.msgid] = False + continue + self._messages[msg.msgid] = msg + self._messages_by_symbol[msg.symbol] = msg + self._msgs_by_category.setdefault(msg.msgid[0], []).append(msg.msgid) def disable(self, msgid, scope='package', line=None): """don't output message of the given id""" @@ -396,9 +402,9 @@ class MessagesHandlerMixIn(object): self.stats[msg_cat] += 1 self.stats['by_module'][self.current_name][msg_cat] += 1 try: - self.stats['by_msg'][msgid] += 1 + self.stats['by_msg'][msg_info.symbol] += 1 except KeyError: - self.stats['by_msg'][msgid] = 1 + self.stats['by_msg'][msg_info.symbol] = 1 # expand message ? msg = msg_info.msg if args: @@ -417,7 +423,7 @@ class MessagesHandlerMixIn(object): """display help messages for the given message identifiers""" for msgid in msgids: try: - print self.get_message_help(msgid, True) + print self.check_message_id(msgid).format_help(checkerref=True) print except UnknownMessage, ex: print ex @@ -467,8 +473,10 @@ class MessagesHandlerMixIn(object): title = ('%smessages' % prefix).capitalize() print title print '~' * len(title) - for msgid in sort_msgs(msgs.iterkeys()): - print self.get_message_help(msgid, False) + for msgid, msg in sorted(msgs.iteritems(), + key=lambda (k,v): (_MSG_ORDER.index(k[0]), k)): + msg = build_message_def(checker, msgid, msg) + print msg.format_help(checkerref=False) print if reports: title = ('%sreports' % prefix).capitalize() @@ -486,7 +494,7 @@ class MessagesHandlerMixIn(object): msgids.append(msgid) msgids.sort() for msgid in msgids: - print self.get_message_help(msgid, False) + print self.check_message_id(msgid).format_help(checkerref=False) print |