From a48a79991cfecccaaa28437a188fd86204aec31d Mon Sep 17 00:00:00 2001 From: Andrey Khokhlin Date: Wed, 19 Dec 2018 21:00:33 +0300 Subject: [PATCH 01/10] Add pylint-ignore-patterns --- pytest_pylint.py | 18 ++++++++++++++++-- test_pytest_pylint.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/pytest_pylint.py b/pytest_pylint.py index d663593..dacdc30 100644 --- a/pytest_pylint.py +++ b/pytest_pylint.py @@ -1,5 +1,6 @@ """Pylint plugin for py.test""" from __future__ import absolute_import, print_function, unicode_literals +import re from os import sep from os.path import exists, join, dirname import sys @@ -104,6 +105,7 @@ def pytest_sessionstart(session): session.pylint_config = None session.pylintrc_file = None session.pylint_ignore = [] + session.pylint_ignore_patterns = [] session.pylint_msg_template = None config = session.config @@ -118,12 +120,20 @@ def pytest_sessionstart(session): session.pylintrc_file = pylintrc_file session.pylint_config = ConfigParser() session.pylint_config.read(pylintrc_file) + try: ignore_string = session.pylint_config.get('MASTER', 'ignore') if ignore_string: session.pylint_ignore = ignore_string.split(',') except (NoSectionError, NoOptionError): pass + + try: + session.pylint_ignore_patterns = session.pylint_config.get( + 'MASTER', 'ignore-patterns') + except (NoSectionError, NoOptionError): + pass + try: session.pylint_msg_template = session.pylint_config.get( 'REPORTS', 'msg-template' @@ -132,8 +142,12 @@ def pytest_sessionstart(session): pass -def include_file(path, ignore_list): +def include_file(path, ignore_list, ignore_patterns=None): """Checks if a file should be included in the collection.""" + if ignore_patterns: + for pattern in ignore_patterns: + if re.match(pattern, path): + return False parts = path.split(sep) return not set(parts) & set(ignore_list) @@ -152,7 +166,7 @@ def pytest_collect_file(path, parent): # No pylintrc, therefore no ignores, so return the item. return PyLintItem(path, parent) - if include_file(rel_path, session.pylint_ignore): + if include_file(rel_path, session.pylint_ignore, session.pylint_ignore_patterns): session.pylint_files.add(rel_path) return PyLintItem( path, parent, session.pylint_msg_template, session.pylintrc_file diff --git a/test_pytest_pylint.py b/test_pytest_pylint.py index 068b67d..dfbe1bd 100644 --- a/test_pytest_pylint.py +++ b/test_pytest_pylint.py @@ -187,3 +187,32 @@ def test_include_path(): assert include_file("part_it/other/filename.py", ignore_list) is True assert include_file("random/part_it/filename.py", ignore_list) is True assert include_file("random/other/part_it.py", ignore_list) is True + + +def test_include_path_with_ignore_patterns(): + """Test if the ignore-patterns is working""" + from pytest_pylint import include_file + ignore_patterns = [ + "first.*", + ".*second", + "^third.*fourth$", + "part", + "base.py" + ] + + # Default includes + assert include_file("random", [], ignore_patterns) is True + assert include_file("random/filename", [], ignore_patterns) is True + assert include_file("random/other/filename", [], ignore_patterns) is True + + # Pattern matches + assert include_file("first1", [], ignore_patterns) is False + assert include_file("first", [], ignore_patterns) is False + assert include_file("_second", [], ignore_patterns) is False + assert include_file("second_", [], ignore_patterns) is False + assert include_file("second_", [], ignore_patterns) is False + assert include_file("third fourth", [], ignore_patterns) is False + assert include_file("_third fourth_", [], ignore_patterns) is True + assert include_file("part", [], ignore_patterns) is False + assert include_file("1part2", [], ignore_patterns) is True + assert include_file("base.py", [], ignore_patterns) is False From 10eb449ad19db6a12e482ed35bef77bf0f82767e Mon Sep 17 00:00:00 2001 From: Andrey Khokhlin Date: Wed, 19 Dec 2018 21:08:08 +0300 Subject: [PATCH 02/10] Fix line length --- pytest_pylint.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pytest_pylint.py b/pytest_pylint.py index dacdc30..2b89053 100644 --- a/pytest_pylint.py +++ b/pytest_pylint.py @@ -166,7 +166,8 @@ def pytest_collect_file(path, parent): # No pylintrc, therefore no ignores, so return the item. return PyLintItem(path, parent) - if include_file(rel_path, session.pylint_ignore, session.pylint_ignore_patterns): + if include_file(rel_path, session.pylint_ignore, + session.pylint_ignore_patterns): session.pylint_files.add(rel_path) return PyLintItem( path, parent, session.pylint_msg_template, session.pylintrc_file From f2e120f6458f11c3810302452b6fe80e41c278bc Mon Sep 17 00:00:00 2001 From: Andrey Khokhlin Date: Wed, 19 Dec 2018 21:13:55 +0300 Subject: [PATCH 03/10] Fix test name for 2.7 --- test_pytest_pylint.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test_pytest_pylint.py b/test_pytest_pylint.py index dfbe1bd..0b0d4db 100644 --- a/test_pytest_pylint.py +++ b/test_pytest_pylint.py @@ -189,7 +189,7 @@ def test_include_path(): assert include_file("random/other/part_it.py", ignore_list) is True -def test_include_path_with_ignore_patterns(): +def test_pylint_ignore_patterns(): """Test if the ignore-patterns is working""" from pytest_pylint import include_file ignore_patterns = [ From 7e275bf75504e166d0524f8a41d007dc598774df Mon Sep 17 00:00:00 2001 From: Yan QiDong Date: Sat, 5 Jan 2019 16:31:38 +0800 Subject: [PATCH 04/10] Add a cache to skip checked unchanged files --- pytest_pylint.py | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/pytest_pylint.py b/pytest_pylint.py index 2b89053..5b10c50 100644 --- a/pytest_pylint.py +++ b/pytest_pylint.py @@ -16,6 +16,8 @@ from pylint.reporters import BaseReporter import pytest +HISTKEY = 'pylint/mtimes' + class PyLintException(Exception): """Exception to raise if a file has a specified pylint error""" @@ -152,6 +154,35 @@ def include_file(path, ignore_list, ignore_patterns=None): return not set(parts) & set(ignore_list) +def pytest_configure(config): + """ + Add a plugin to cache file mtimes. + + :param _pytest.config.Config config: pytest config object + """ + if config.option.pylint: + config.pylint = PylintPlugin(config) + config.pluginmanager.register(config.pylint) + config.addinivalue_line('markers', "pylint: Tests which run pylint.") + + +# pylint: disable=too-few-public-methods +class PylintPlugin: + """ + A Plugin object for pylint, which loads and records file mtimes. + """ + def __init__(self, config): + self.mtimes = config.cache.get(HISTKEY, {}) + + def pytest_sessionfinish(self, session): + """ + Save file mtimes to pytest cache. + + :param _pytest.main.Session session: the pytest session object + """ + session.config.cache.set(HISTKEY, self.mtimes) + + def pytest_collect_file(path, parent): """Collect files on which pylint should run""" config = parent.session.config @@ -231,6 +262,13 @@ def __init__(self, fspath, parent, msg_format=None, pylintrc_file=None): self._msg_format = msg_format self.pylintrc_file = pylintrc_file + self.__mtime = self.fspath.mtime() + + def setup(self): + """Mark unchanged files as SKIPPED.""" + previous = self.config.pylint.mtimes.get(self.nodeid, 0) + if previous == self.__mtime: + pytest.skip("file(s) previously passed pylint checks") def runtest(self): """Check the pylint messages to see if any errors were reported.""" @@ -243,6 +281,9 @@ def runtest(self): if reported_errors: raise PyLintException('\n'.join(reported_errors)) + # Update the cache if the item passed pylint. + self.config.pylint.mtimes[self.nodeid] = self.__mtime + def repr_failure(self, excinfo): """Handle any test failures by checkint that they were ours.""" if excinfo.errisinstance(PyLintException): From 4ebe17141cab73e855f3e14a3e3f20ebbf5d28d5 Mon Sep 17 00:00:00 2001 From: Yan QiDong Date: Sat, 5 Jan 2019 17:36:03 +0800 Subject: [PATCH 05/10] Add a test case to ensure cache and skipped --- test_pytest_pylint.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/test_pytest_pylint.py b/test_pytest_pylint.py index 0b0d4db..932db01 100644 --- a/test_pytest_pylint.py +++ b/test_pytest_pylint.py @@ -216,3 +216,27 @@ def test_pylint_ignore_patterns(): assert include_file("part", [], ignore_patterns) is False assert include_file("1part2", [], ignore_patterns) is True assert include_file("base.py", [], ignore_patterns) is False + + +def test_skip_checked_files(testdir): + """ + Test a file twice which can pass pylint. + The 2nd time should be skipped. + """ + testdir.makepyfile('''#!/usr/bin/env python +"""A hello world script.""" + +from __future__ import print_function + +print('Hello world!') +# pylint: disable=missing-final-newline +''') + # The 1st time should be passed + result = testdir.runpytest('--pylint') + print(result.stdout.str()) + assert '1 passed' in result.stdout.str() + + # The 2nd time should be skipped + result = testdir.runpytest('--pylint') + print(result.stdout.str()) + assert '1 skipped' in result.stdout.str() From f1f964bf060941cd4bc3f67b866ec128840e1f3e Mon Sep 17 00:00:00 2001 From: Yan QiDong Date: Sat, 5 Jan 2019 20:52:12 +0800 Subject: [PATCH 06/10] Fix 3 errors in python2.7 --- pytest_pylint.py | 4 ++-- test_pytest_pylint.py | 18 ++++++++---------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/pytest_pylint.py b/pytest_pylint.py index 5b10c50..09cdcb8 100644 --- a/pytest_pylint.py +++ b/pytest_pylint.py @@ -166,8 +166,8 @@ def pytest_configure(config): config.addinivalue_line('markers', "pylint: Tests which run pylint.") -# pylint: disable=too-few-public-methods -class PylintPlugin: +# pylint: disable=too-few-public-methods, useless-object-inheritance +class PylintPlugin(object): """ A Plugin object for pylint, which loads and records file mtimes. """ diff --git a/test_pytest_pylint.py b/test_pytest_pylint.py index 932db01..5da9871 100644 --- a/test_pytest_pylint.py +++ b/test_pytest_pylint.py @@ -223,20 +223,18 @@ def test_skip_checked_files(testdir): Test a file twice which can pass pylint. The 2nd time should be skipped. """ - testdir.makepyfile('''#!/usr/bin/env python -"""A hello world script.""" - -from __future__ import print_function - -print('Hello world!') -# pylint: disable=missing-final-newline -''') + testdir.makepyfile( + '#!/usr/bin/env python', + '"""A hello world script."""', + '', + 'from __future__ import print_function', + '', + 'print("Hello world!") # pylint: disable=missing-final-newline', + ) # The 1st time should be passed result = testdir.runpytest('--pylint') - print(result.stdout.str()) assert '1 passed' in result.stdout.str() # The 2nd time should be skipped result = testdir.runpytest('--pylint') - print(result.stdout.str()) assert '1 skipped' in result.stdout.str() From 2a01abde2b4dd97143a5a2f39f6922bc0470a994 Mon Sep 17 00:00:00 2001 From: Yan QiDong Date: Sat, 5 Jan 2019 21:50:42 +0800 Subject: [PATCH 07/10] Disable a creepy pylint error --- pylintrc | 3 ++- pytest_pylint.py | 5 +++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/pylintrc b/pylintrc index 9c4b7d0..0d936cc 100644 --- a/pylintrc +++ b/pylintrc @@ -1,2 +1,3 @@ [TYPECHECK] -ignored-classes=pytest \ No newline at end of file +ignored-classes=pytest +disable=bad-option-value diff --git a/pytest_pylint.py b/pytest_pylint.py index 09cdcb8..491ae39 100644 --- a/pytest_pylint.py +++ b/pytest_pylint.py @@ -166,6 +166,11 @@ def pytest_configure(config): config.addinivalue_line('markers', "pylint: Tests which run pylint.") +# There will be an old-style-class error in Python 2.7, +# or a useless-object-inheritance warning in Python 3. +# If disable any, pylint will have a bad-option-value error in 2.7 or 3. +# Finally I have to disable useless-object-inheritance locally +# and bad-option-value globally. # pylint: disable=too-few-public-methods, useless-object-inheritance class PylintPlugin(object): """ From e48f4d972bf1ae83ede16898e0b091ecf04dea97 Mon Sep 17 00:00:00 2001 From: Yan QiDong Date: Fri, 11 Jan 2019 22:29:09 +0800 Subject: [PATCH 08/10] Skip really in collection time --- pytest_pylint.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/pytest_pylint.py b/pytest_pylint.py index 491ae39..32767ab 100644 --- a/pytest_pylint.py +++ b/pytest_pylint.py @@ -198,17 +198,18 @@ def pytest_collect_file(path, parent): rel_path = get_rel_path(path.strpath, parent.session.fspath.strpath) session = parent.session if session.pylint_config is None: - session.pylint_files.add(rel_path) # No pylintrc, therefore no ignores, so return the item. - return PyLintItem(path, parent) - - if include_file(rel_path, session.pylint_ignore, - session.pylint_ignore_patterns): - session.pylint_files.add(rel_path) - return PyLintItem( + item = PyLintItem(path, parent) + elif include_file(rel_path, session.pylint_ignore, + session.pylint_ignore_patterns): + item = PyLintItem( path, parent, session.pylint_msg_template, session.pylintrc_file ) - return None + else: + return None + if not item.should_skip: + session.pylint_files.add(rel_path) + return item def pytest_collection_finish(session): @@ -268,11 +269,12 @@ def __init__(self, fspath, parent, msg_format=None, pylintrc_file=None): self.pylintrc_file = pylintrc_file self.__mtime = self.fspath.mtime() + prev_mtime = self.config.pylint.mtimes.get(self.nodeid, 0) + self.should_skip = (prev_mtime == self.__mtime) def setup(self): """Mark unchanged files as SKIPPED.""" - previous = self.config.pylint.mtimes.get(self.nodeid, 0) - if previous == self.__mtime: + if self.should_skip: pytest.skip("file(s) previously passed pylint checks") def runtest(self): From 2163a8f809aa4a7ba6dcdfea8ec0c2f740dbd757 Mon Sep 17 00:00:00 2001 From: Carson Gee Date: Tue, 15 Jan 2019 07:26:01 -0500 Subject: [PATCH 09/10] Pylint cleanup --- pylintrc | 9 +++++++-- pytest_pylint.py | 8 ++------ 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/pylintrc b/pylintrc index 0d936cc..411c737 100644 --- a/pylintrc +++ b/pylintrc @@ -1,3 +1,8 @@ [TYPECHECK] -ignored-classes=pytest -disable=bad-option-value + +ignored-classes = pytest + + +[MESSAGES CONTROL] + +disable = useless-object-inheritance diff --git a/pytest_pylint.py b/pytest_pylint.py index 32767ab..7565adc 100644 --- a/pytest_pylint.py +++ b/pytest_pylint.py @@ -166,16 +166,12 @@ def pytest_configure(config): config.addinivalue_line('markers', "pylint: Tests which run pylint.") -# There will be an old-style-class error in Python 2.7, -# or a useless-object-inheritance warning in Python 3. -# If disable any, pylint will have a bad-option-value error in 2.7 or 3. -# Finally I have to disable useless-object-inheritance locally -# and bad-option-value globally. -# pylint: disable=too-few-public-methods, useless-object-inheritance class PylintPlugin(object): """ A Plugin object for pylint, which loads and records file mtimes. """ + # pylint: disable=too-few-public-methods + def __init__(self, config): self.mtimes = config.cache.get(HISTKEY, {}) From 78ea69cacb65b78f2951e64616b6bcfee32066bf Mon Sep 17 00:00:00 2001 From: Carson Gee Date: Tue, 15 Jan 2019 07:58:43 -0500 Subject: [PATCH 10/10] Released 0.14.0 --- README.rst | 9 +++++++++ setup.py | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 1ff1f4a..1b9a76a 100644 --- a/README.rst +++ b/README.rst @@ -49,6 +49,15 @@ This code is heavily based on Releases ======== +0.14.0 +~~~~~~ + +- Added support for Pylint's ignore-patterns for regex based ignores + thanks to `khokhlin `_ +- pytest-pylint now caches successful pylint checks to speedup test + reruns when files haven't changed thanks to `yanqd0 + `_ + 0.13.0 ~~~~~~ diff --git a/setup.py b/setup.py index ec9f871..0e56f83 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ description='pytest plugin to check source code with pylint', long_description=open("README.rst").read(), license='MIT', - version='0.13.0', + version='0.14.0', author='Carson Gee', author_email='x@carsongee.com', url='https://github.com/carsongee/pytest-pylint',