diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..6c99f08 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,38 @@ +--- +name: Tests + +on: [push, pull_request] +jobs: + tests: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: + - "3.7" + - "3.8" + - "3.9" + - "3.10" + - "3.11.0-rc - 3.11" + + steps: + - uses: actions/checkout@v3 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + python -m pip install --upgrade pip setuptools wheel + python -m pip install --upgrade tox tox-py + + - name: Run tox targets for ${{ matrix.python-version }} + run: tox --py current + + - name: Run coverage + run: tox -e coverage + + - name: Run linters + run: tox -e qa diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index a66eac4..0000000 --- a/.travis.yml +++ /dev/null @@ -1,14 +0,0 @@ -language: python -dist: xenial -matrix: - include: - - python: 3.9 - - python: 3.8 - - python: 3.7 - - python: 3.6 -install: - - "pip install tox-travis tox==3.14.6 coveralls" - - "pip install -e ." -script: tox -after_success: - "coveralls" diff --git a/DEVELOPMENT.rst b/DEVELOPMENT.rst index c748a25..1e3dec2 100644 --- a/DEVELOPMENT.rst +++ b/DEVELOPMENT.rst @@ -18,7 +18,7 @@ by previous plugins will run. For instance, in case PylintPlugin had `pytest_addoption` implemented, which runs before `pytest_configure` in the hook cycle, it would be executed once PylintPlugin got registered. -PylintPlugin uses the `pytest_collect_file` hook which is called wih every +PylintPlugin uses the `pytest_collect_file` hook which is called with every file available in the test target dir. This hook collects all the file pylint should run on, in this case files with extension ".py". @@ -63,25 +63,26 @@ Double-check on `tox.ini `__ Development @@ -54,6 +54,15 @@ If you want to help development, there is Releases ======== +0.19.0 +~~~~~~ + +- Switched to GitHub Actions for CI thanks to `michael-k `__ +- Switched to using smart PyLint RC discovery thanks to `bennyrowland `__ +- Correcting rootdir/rootpath issues in pytest >7.x +- Deprecated support for Python <3.7 + + 0.18.0 ~~~~~~ @@ -161,7 +170,7 @@ Releases 0.8.0 ~~~~~ -- `bdrung `__ corrected inconsitent returns in a function +- `bdrung `__ corrected inconsistent returns in a function - Dropped Python 3.3 support 0.7.1 diff --git a/pylintrc b/pylintrc index 8c84e0a..2f67e20 100644 --- a/pylintrc +++ b/pylintrc @@ -1,3 +1,8 @@ -[TYPECHECK] +[MESSAGES CONTROL] +disable = C0330, C0326 + +[FORMAT] +max-line-length = 88 +[TYPECHECK] ignored-classes = pytest diff --git a/pytest_pylint/plugin.py b/pytest_pylint/plugin.py index 24ebef3..7cc751d 100644 --- a/pytest_pylint/plugin.py +++ b/pytest_pylint/plugin.py @@ -1,73 +1,68 @@ # -*- coding: utf-8 -*- """ - pytest plugins. Both pylint wrapper and PylintPlugin - +pytest plugins. Both pylint wrapper and PylintPlugin """ from collections import defaultdict -from configparser import ConfigParser, NoSectionError, NoOptionError -from os import makedirs -from os.path import getmtime, exists, join, dirname +from configparser import ConfigParser, NoOptionError, NoSectionError +from os import getcwd, makedirs, sep +from os.path import dirname, exists, getmtime, join +from pathlib import Path -from pylint import lint -from pylint.config import PYLINTRC import pytest import toml +from pylint import config as pylint_config +from pylint import lint from .pylint_util import ProgrammaticReporter -from .util import get_rel_path, PyLintException, should_include_file +from .util import PyLintException, get_rel_path, should_include_file -HISTKEY = 'pylint/mtimes' -PYLINT_CONFIG_CACHE_KEY = 'pylintrc' +HISTKEY = "pylint/mtimes" +PYLINT_CONFIG_CACHE_KEY = "pylintrc" FILL_CHARS = 80 -MARKER = 'pylint' +MARKER = "pylint" def pytest_addoption(parser): """Add all our command line options""" group = parser.getgroup("pylint") group.addoption( - "--pylint", - action="store_true", default=False, - help="run pylint on all" + "--pylint", action="store_true", default=False, help="run pylint on all" ) group.addoption( "--no-pylint", - action="store_true", default=False, - help="disable running pylint " + action="store_true", + default=False, + help="disable running pylint ", ) group.addoption( - '--pylint-rcfile', - default=None, - help='Location of RC file if not pylintrc' + "--pylint-rcfile", default=None, help="Location of RC file if not pylintrc" ) group.addoption( - '--pylint-error-types', - default='CRWEF', - help='The types of pylint errors to consider failures by letter' - ', default is all of them (CRWEF).' + "--pylint-error-types", + default="CRWEF", + help="The types of pylint errors to consider failures by letter" + ", default is all of them (CRWEF).", ) group.addoption( - '--pylint-jobs', + "--pylint-jobs", default=None, - help='Specify number of processes to use for pylint' + help="Specify number of processes to use for pylint", ) group.addoption( - '--pylint-output-file', + "--pylint-output-file", default=None, - help='Path to a file where Pylint report will be printed to.' + help="Path to a file where Pylint report will be printed to.", ) group.addoption( - '--pylint-ignore', - default=None, - help='Files/directories that will be ignored' + "--pylint-ignore", default=None, help="Files/directories that will be ignored" ) group.addoption( - '--pylint-ignore-patterns', + "--pylint-ignore-patterns", default=None, - help='Files/directories patterns that will be ignored' + help="Files/directories patterns that will be ignored", ) @@ -77,10 +72,7 @@ def pytest_configure(config): :param _pytest.config.Config config: pytest config object """ - config.addinivalue_line( - 'markers', - "{0}: Tests which run pylint.".format(MARKER) - ) + config.addinivalue_line("markers", f"{MARKER}: Tests which run pylint.") if config.option.pylint and not config.option.no_pylint: pylint_plugin = PylintPlugin(config) config.pluginmanager.register(pylint_plugin) @@ -90,9 +82,10 @@ class PylintPlugin: """ The core plugin for pylint """ + # pylint: disable=too-many-instance-attributes def __init__(self, config): - if hasattr(config, 'cache'): + if hasattr(config, "cache"): self.mtimes = config.cache.get(HISTKEY, {}) else: self.mtimes = {} @@ -109,7 +102,16 @@ def pytest_configure(self, config): """Configure pytest after it is already enabled""" # Find pylintrc to check ignore list - pylintrc_file = config.option.pylint_rcfile or PYLINTRC + if config.option.pylint_rcfile: + pylintrc_file = config.option.pylint_rcfile + else: + # handling files apart from pylintrc was only introduced in pylint + # 2.5, if we can't use find_default_config_files(), fall back on PYLINTRC + # once we drop support below 2.5 we can get rid of this + try: + pylintrc_file = next(pylint_config.find_default_config_files(), None) + except AttributeError: + pylintrc_file = pylint_config.PYLINTRC if pylintrc_file and not exists(pylintrc_file): # The directory of pytest.ini got a chance @@ -123,23 +125,29 @@ def pytest_configure(self, config): # Check if pylint config has a different filename or date # and invalidate the cache if it has changed. pylint_mtime = getmtime(pylintrc_file) - cache_key = PYLINT_CONFIG_CACHE_KEY + pylintrc_file + cache_key = PYLINT_CONFIG_CACHE_KEY + ( + pylintrc_file.name if isinstance(pylintrc_file, Path) else pylintrc_file + ) cache_value = self.mtimes.get(cache_key) if cache_value is None or cache_value < pylint_mtime: self.mtimes = {} self.mtimes[cache_key] = pylint_mtime - if pylintrc_file.endswith(".toml"): + if ( + (pylintrc_file.suffix == ".toml") + if isinstance(pylintrc_file, Path) + else pylintrc_file.endswith(".toml") + ): self._load_pyproject_toml(pylintrc_file) else: self._load_rc_file(pylintrc_file) # Command line arguments take presedence over rcfile ones if set if config.option.pylint_ignore is not None: - self.pylint_ignore = config.option.pylint_ignore.split(',') + self.pylint_ignore = config.option.pylint_ignore.split(",") if config.option.pylint_ignore_patterns is not None: - self.pylint_ignore_patterns = ( - config.option.pylint_ignore_patterns.split(',') + self.pylint_ignore_patterns = config.option.pylint_ignore_patterns.split( + "," ) def _load_rc_file(self, pylintrc_file): @@ -147,30 +155,36 @@ def _load_rc_file(self, pylintrc_file): self.pylint_config.read(pylintrc_file) try: - ignore_string = self.pylint_config.get('MASTER', 'ignore') + ignore_string = self.pylint_config.get("MAIN", "ignore") if ignore_string: - self.pylint_ignore = ignore_string.split(',') + self.pylint_ignore = ignore_string.split(",") except (NoSectionError, NoOptionError): - pass + try: + ignore_string = self.pylint_config.get("MASTER", "ignore") + if ignore_string: + self.pylint_ignore = ignore_string.split(",") + except (NoSectionError, NoOptionError): + pass try: - ignore_patterns = self.pylint_config.get( - 'MASTER', 'ignore-patterns' - ) + ignore_patterns = self.pylint_config.get("MAIN", "ignore-patterns") if ignore_patterns: - self.pylint_ignore_patterns = ignore_patterns.split(',') + self.pylint_ignore_patterns = ignore_patterns.split(",") except (NoSectionError, NoOptionError): - pass + try: + ignore_patterns = self.pylint_config.get("MASTER", "ignore-patterns") + if ignore_patterns: + self.pylint_ignore_patterns = ignore_patterns.split(",") + except (NoSectionError, NoOptionError): + pass try: - self.pylint_msg_template = self.pylint_config.get( - 'REPORTS', 'msg-template' - ) + self.pylint_msg_template = self.pylint_config.get("REPORTS", "msg-template") except (NoSectionError, NoOptionError): pass def _load_pyproject_toml(self, pylintrc_file): - with open(pylintrc_file, "r") as f_p: + with open(pylintrc_file, "r", encoding="utf-8") as f_p: try: content = toml.load(f_p) except (TypeError, toml.decoder.TomlDecodeError): @@ -181,24 +195,20 @@ def _load_pyproject_toml(self, pylintrc_file): except KeyError: return - master_section = {} + main_section = {} reports_section = {} for key, value in self.pylint_config.items(): - if not master_section and key.lower() == "master": - master_section = value + if not main_section and key.lower() in ("main", "master"): + main_section = value elif not reports_section and key.lower() == "reports": reports_section = value - ignore = master_section.get("ignore") + ignore = main_section.get("ignore") if ignore: self.pylint_ignore = ( - ignore.split(",") - if isinstance(ignore, str) - else ignore + ignore.split(",") if isinstance(ignore, str) else ignore ) - self.pylint_ignore_patterns = ( - master_section.get("ignore-patterns") or [] - ) + self.pylint_ignore_patterns = main_section.get("ignore-patterns") or [] self.pylint_msg_template = reports_section.get("msg-template") def pytest_sessionfinish(self, session): @@ -207,7 +217,7 @@ def pytest_sessionfinish(self, session): :param _pytest.main.Session session: the pytest session object """ - if hasattr(session.config, 'cache'): + if hasattr(session.config, "cache"): session.config.cache.set(HISTKEY, self.mtimes) def pytest_collect_file(self, path, parent): @@ -217,11 +227,9 @@ def pytest_collect_file(self, path, parent): rel_path = get_rel_path(path.strpath, parent.session.fspath.strpath) if should_include_file( - rel_path, self.pylint_ignore, self.pylint_ignore_patterns + rel_path, self.pylint_ignore, self.pylint_ignore_patterns ): - item = PylintFile.from_parent( - parent, fspath=path, plugin=self - ) + item = PylintFile.from_parent(parent, fspath=path, plugin=self) else: return None @@ -237,29 +245,34 @@ def pytest_collection_finish(self, session): jobs = session.config.option.pylint_jobs reporter = ProgrammaticReporter() - # Build argument list for pylint - args_list = list(self.pylint_files) + + # To try and bullet proof our paths, use our + # relative paths to the resolved path of the pytest rootpath + try: + root_path = session.config.rootpath.resolve() + except AttributeError: + root_path = Path(session.config.rootdir.realpath()) + + args_list = [ + str((root_path / file_path).relative_to(getcwd())) + for file_path in self.pylint_files + ] + # Add any additional arguments to our pylint run if self.pylintrc_file: - args_list.append('--rcfile={0}'.format( - self.pylintrc_file - )) + args_list.append(f"--rcfile={self.pylintrc_file}") if jobs is not None: - args_list.append('-j') + args_list.append("-j") args_list.append(jobs) # These allow the user to override the pylint configuration's # ignore list if self.pylint_ignore: - args_list.append( - '--ignore={0}'.format(','.join(self.pylint_ignore)) - ) + args_list.append(f"--ignore={','.join(self.pylint_ignore)}") if self.pylint_ignore_patterns: args_list.append( - '--ignore-patterns={0}'.format( - ','.join(self.pylint_ignore_patterns) - ) + f"--ignore-patterns={','.join(self.pylint_ignore_patterns)}" ) - print('-' * FILL_CHARS) - print('Linting files') + print("-" * FILL_CHARS) + print("Linting files") # Run pylint over the collected files. @@ -271,17 +284,20 @@ def pytest_collection_finish(self, session): except TypeError: # pylint < 2.5.1 API result = lint.Run(args_list, reporter=reporter, do_exit=False) - except RuntimeError: - return + messages = result.linter.reporter.data # Stores the messages in a dictionary for lookup in tests. for message in messages: - self.pylint_messages[message.path].append(message) - print('-' * FILL_CHARS) + # Undo our mapping to resolved absolute paths to map + # back to self.pylint_files + relpath = message.abspath.replace(f"{root_path}{sep}", "") + self.pylint_messages[relpath].append(message) + print("-" * FILL_CHARS) class PylintFile(pytest.File): """File that pylint will run on.""" + rel_path = None # : str plugin = None # : PylintPlugin should_skip = False # : bool @@ -292,25 +308,19 @@ def from_parent(cls, parent, *, fspath, plugin): # We add the ``plugin`` kwarg to get plugin level information so the # signature differs # pylint: disable=arguments-differ - _self = getattr(super(), 'from_parent', cls)(parent, fspath=fspath) + _self = getattr(super(), "from_parent", cls)(parent, fspath=fspath) _self.plugin = plugin - _self.rel_path = get_rel_path( - fspath.strpath, - parent.session.fspath.strpath - ) + _self.rel_path = get_rel_path(fspath.strpath, parent.session.fspath.strpath) _self.mtime = fspath.mtime() prev_mtime = _self.plugin.mtimes.get(_self.rel_path, 0) - _self.should_skip = (prev_mtime == _self.mtime) + _self.should_skip = prev_mtime == _self.mtime return _self def collect(self): """Create a PyLintItem for the File.""" - yield PyLintItem.from_parent( - parent=self, - name='PYLINT' - ) + yield PyLintItem.from_parent(parent=self, name="PYLINT") class PyLintItem(pytest.Item): @@ -326,13 +336,13 @@ def __init__(self, *args, **kwargs): msg_format = self.plugin.pylint_msg_template if msg_format is None: - self._msg_format = '{C}:{line:3d},{column:2d}: {msg} ({symbol})' + self._msg_format = "{C}:{line:3d},{column:2d}: {msg} ({symbol})" else: self._msg_format = msg_format @classmethod def from_parent(cls, parent, **kw): - return getattr(super(), 'from_parent', cls)(parent, **kw) + return getattr(super(), "from_parent", cls)(parent, **kw) def setup(self): """Mark unchanged files as SKIPPED.""" @@ -345,25 +355,14 @@ def runtest(self): def _loop_errors(writer): reported_errors = [] - for error in self.plugin.pylint_messages.get( - self.parent.rel_path, [] - ): + for error in self.plugin.pylint_messages.get(self.parent.rel_path, []): if error.C in self.config.option.pylint_error_types: - reported_errors.append( - error.format(self._msg_format) - ) + reported_errors.append(error.format(self._msg_format)) writer( - '{error_path}:{error_line}: [{error_msg_id}' - '({error_symbol}), {error_obj}] ' - '{error_msg}\n'.format( - error_path=error.path, - error_line=error.line, - error_msg_id=error.msg_id, - error_symbol=error.symbol, - error_obj=error.obj, - error_msg=error.msg, - ) + f"{error.path}:{error.line}: [{error.msg_id}" + f"({error.symbol}), {error.obj}] " + f"{error.msg}\n" ) return reported_errors @@ -372,13 +371,13 @@ def _loop_errors(writer): output_dir = dirname(pylint_output_file) if output_dir: makedirs(output_dir, exist_ok=True) - with open(pylint_output_file, 'a') as _file: + with open(pylint_output_file, "a", encoding="utf-8") as _file: reported_errors = _loop_errors(writer=_file.write) else: reported_errors = _loop_errors(writer=lambda *args, **kwargs: None) if reported_errors: - raise PyLintException('\n'.join(reported_errors)) + raise PyLintException("\n".join(reported_errors)) # Update the cache if the item passed pylint. self.plugin.mtimes[self.parent.rel_path] = self.parent.mtime @@ -392,4 +391,4 @@ def repr_failure(self, excinfo, style=None): def reportinfo(self): """Generate our test report""" - return self.fspath, None, "[pylint] {0}".format(self.parent.rel_path) + return self.fspath, None, f"[pylint] {self.parent.rel_path}" diff --git a/pytest_pylint/pylint_util.py b/pytest_pylint/pylint_util.py index 8c98493..d78f2c9 100644 --- a/pytest_pylint/pylint_util.py +++ b/pytest_pylint/pylint_util.py @@ -8,8 +8,9 @@ class ProgrammaticReporter(BaseReporter): """Reporter that replaces output with storage in list of dictionaries""" + __implements__ = IReporter - extension = 'prog' + extension = "prog" def __init__(self, output=None): BaseReporter.__init__(self, output) @@ -29,10 +30,10 @@ def _display(self, layout): def on_set_current_module(self, module, filepath): """Hook called when a module starts to be analysed.""" - print('.', end='') + print(".", end="") sys.stdout.flush() def on_close(self, stats, previous_stats): """Hook called when all modules finished analyzing.""" # print a new line when pylint is finished - print('') + print("") diff --git a/pytest_pylint/tests/test_pytest_pylint.py b/pytest_pylint/tests/test_pytest_pylint.py index 4fe749b..c26af26 100644 --- a/pytest_pylint/tests/test_pytest_pylint.py +++ b/pytest_pylint/tests/test_pytest_pylint.py @@ -7,187 +7,201 @@ from textwrap import dedent from unittest import mock +import pylint.config import pytest - -pytest_plugins = ('pytester',) # pylint: disable=invalid-name +pytest_plugins = ("pytester",) # pylint: disable=invalid-name def test_basic(testdir): """Verify basic pylint checks""" - testdir.makepyfile('import sys') - result = testdir.runpytest('--pylint') - assert 'Missing module docstring' in result.stdout.str() - assert 'Unused import sys' in result.stdout.str() - assert 'Final newline missing' in result.stdout.str() - assert 'passed, ' not in result.stdout.str() - assert '1 failed' in result.stdout.str() - assert 'Linting files' in result.stdout.str() + testdir.makepyfile("import sys") + result = testdir.runpytest("--pylint") + assert "Missing module docstring" in result.stdout.str() + assert "Unused import sys" in result.stdout.str() + assert "Final newline missing" in result.stdout.str() + assert "passed, " not in result.stdout.str() + assert "1 failed" in result.stdout.str() + assert "Linting files" in result.stdout.str() def test_nodeid(testdir): """Verify our nodeid adds a suffix""" - testdir.makepyfile(app='import sys') - result = testdir.runpytest('--pylint', '--collectonly', '--verbose') - for expected in '', '': + testdir.makepyfile(app="import sys") + result = testdir.runpytest("--pylint", "--collectonly", "--verbose") + for expected in "", "": assert expected in result.stdout.str() def test_nodeid_no_dupepath(testdir): """Verify we don't duplicate the node path in our node id.""" - testdir.makepyfile(app='import sys') - result = testdir.runpytest('--pylint', '--verbose') + testdir.makepyfile(app="import sys") + result = testdir.runpytest("--pylint", "--verbose") assert re.search( - r'^FAILED\s+app\.py::PYLINT$', - result.stdout.str(), - flags=re.MULTILINE + r"^FAILED\s+app\.py::PYLINT$", result.stdout.str(), flags=re.MULTILINE ) def test_subdirectories(testdir): """Verify pylint checks files in subdirectories""" - subdir = testdir.mkpydir('mymodule') + subdir = testdir.mkpydir("mymodule") testfile = subdir.join("test_file.py") - testfile.write('import sys') - result = testdir.runpytest('--pylint') - assert '[pylint] mymodule/test_file.py' in result.stdout.str() - assert 'Missing module docstring' in result.stdout.str() - assert 'Unused import sys' in result.stdout.str() - assert 'Final newline missing' in result.stdout.str() - assert '1 failed' in result.stdout.str() - assert 'Linting files' in result.stdout.str() + testfile.write("import sys") + result = testdir.runpytest("--pylint") + assert "[pylint] mymodule/test_file.py" in result.stdout.str() + assert "Missing module docstring" in result.stdout.str() + assert "Unused import sys" in result.stdout.str() + assert "Final newline missing" in result.stdout.str() + assert "1 failed" in result.stdout.str() + assert "Linting files" in result.stdout.str() def test_disable(testdir): """Verify basic pylint checks""" - testdir.makepyfile('import sys') - result = testdir.runpytest('--pylint --no-pylint') - assert 'Final newline missing' not in result.stdout.str() - assert 'Linting files' not in result.stdout.str() + testdir.makepyfile("import sys") + result = testdir.runpytest("--pylint --no-pylint") + assert "Final newline missing" not in result.stdout.str() + assert "Linting files" not in result.stdout.str() def test_error_control(testdir): """Verify that error types are configurable""" - testdir.makepyfile('import sys') - result = testdir.runpytest('--pylint', '--pylint-error-types=EF') - assert '1 passed' in result.stdout.str() + testdir.makepyfile("import sys") + result = testdir.runpytest("--pylint", "--pylint-error-types=EF") + assert "1 passed" in result.stdout.str() def test_pylintrc_file(testdir): """Verify that a specified pylint rc file will work.""" rcfile = testdir.makefile( - '.rc', + ".rc", """ [FORMAT] max-line-length=3 - """ - ) - testdir.makepyfile('import sys') - result = testdir.runpytest( - '--pylint', '--pylint-rcfile={0}'.format(rcfile.strpath) + """, ) - assert 'Line too long (10/3)' in result.stdout.str() + testdir.makepyfile("import sys") + result = testdir.runpytest("--pylint", f"--pylint-rcfile={rcfile.strpath}") + assert "Line too long (10/3)" in result.stdout.str() def test_pylintrc_file_toml(testdir): """Verify that pyproject.toml can be used as a pylint rc file.""" rcfile = testdir.makefile( - '.toml', + ".toml", pylint=""" [tool.pylint.FORMAT] max-line-length = "3" - """ - ) - testdir.makepyfile('import sys') - result = testdir.runpytest( - '--pylint', '--pylint-rcfile={0}'.format(rcfile.strpath) + """, ) + testdir.makepyfile("import sys") + result = testdir.runpytest("--pylint", f"--pylint-rcfile={rcfile.strpath}") # Parsing changed from integer to string in pylint >=2.5. Once # support is dropped <2.5 this is removable - if 'should be of type int' in result.stdout.str(): + if "should be of type int" in result.stdout.str(): rcfile = testdir.makefile( - '.toml', + ".toml", pylint=""" [tool.pylint.FORMAT] max-line-length = 3 - """ - ) - result = testdir.runpytest( - '--pylint', '--pylint-rcfile={0}'.format(rcfile.strpath) + """, ) + result = testdir.runpytest("--pylint", f"--pylint-rcfile={rcfile.strpath}") + + assert "Line too long (10/3)" in result.stdout.str() + + +def test_pylintrc_file_pyproject_toml(testdir): + """Verify that pyproject.toml can be auto-detected as a pylint rc file.""" + # pylint only auto-detects pyproject.toml from 2.5 onwards + if not hasattr(pylint.config, "find_default_config_files"): + return + testdir.makefile( + ".toml", + pyproject=""" + [tool.pylint.FORMAT] + max-line-length = "3" + """, + ) + testdir.makepyfile("import sys") + result = testdir.runpytest("--pylint") - assert 'Line too long (10/3)' in result.stdout.str() + assert "Line too long (10/3)" in result.stdout.str() def test_pylintrc_file_beside_ini(testdir): """ - Verify that a specified pylint rc file will work what placed into pytest + Verify that a specified pylint rc file will work when placed into pytest ini dir. """ - non_cwd_dir = testdir.mkdir('non_cwd_dir') + non_cwd_dir = testdir.mkdir("non_cwd_dir") - rcfile = non_cwd_dir.join('foo.rc') + rcfile = non_cwd_dir.join("foo.rc") rcfile.write( - """ + dedent( + """ [FORMAT] max-line-length=3 - """) - inifile = non_cwd_dir.join('foo.ini') - inifile.write(dedent( """ + ) + ) + inifile = non_cwd_dir.join("foo.ini") + inifile.write( + dedent( + f""" [pytest] - addopts = --pylint --pylint-rcfile={0} - """.format(rcfile.basename) - )) - - pyfile = testdir.makepyfile('import sys') - - result = testdir.runpytest( - pyfile.strpath + addopts = --pylint --pylint-rcfile={rcfile.strpath} + """ + ) ) - assert 'Line too long (10/3)' not in result.stdout.str() + # Per https://github.com/pytest-dev/pytest/pull/8537/ the rootdir + # is now wherever the ini file is, so we need to make sure our + # Python file is the right directory. + pyfile_base = testdir.makepyfile("import sys") + pyfile = non_cwd_dir / pyfile_base.basename + pyfile_base.rename(pyfile) - result = testdir.runpytest( - '-c', inifile.strpath, pyfile.strpath - ) - assert 'Line too long (10/3)' in result.stdout.str() + result = testdir.runpytest(pyfile.strpath) + assert "Line too long (10/3)" not in result.stdout.str() + + result = testdir.runpytest("-c", inifile.strpath, pyfile.strpath) + assert "Line too long (10/3)" in result.stdout.str() @pytest.mark.parametrize("rcformat", ("ini", "toml", "simple_toml")) -def test_pylintrc_ignore(testdir, rcformat): +@pytest.mark.parametrize("sectionname", ("main", "master")) +def test_pylintrc_ignore(testdir, rcformat, sectionname): """Verify that a pylintrc file with ignores will work.""" if rcformat == "toml": rcfile = testdir.makefile( - '.toml', - """ - [tool.pylint.master] + ".toml", + f""" + [tool.pylint.{sectionname}] ignore = ["test_pylintrc_ignore.py", "foo.py"] - """ + """, ) elif rcformat == "simple_toml": rcfile = testdir.makefile( - '.toml', - """ - [tool.pylint.MASTER] + ".toml", + f""" + [tool.pylint.{sectionname.upper()}] ignore = "test_pylintrc_ignore.py,foo.py" - """ + """, ) else: rcfile = testdir.makefile( - '.rc', - """ - [MASTER] + ".rc", + f""" + [{sectionname.upper()}] ignore = test_pylintrc_ignore.py - """ + """, ) - testdir.makepyfile('import sys') - result = testdir.runpytest( - '--pylint', '--pylint-rcfile={0}'.format(rcfile.strpath) - ) - assert 'collected 0 items' in result.stdout.str() + testdir.makepyfile("import sys") + result = testdir.runpytest("--pylint", f"--pylint-rcfile={rcfile.strpath}") + assert "collected 0 items" in result.stdout.str() @pytest.mark.parametrize("rcformat", ("ini", "toml")) @@ -195,51 +209,47 @@ def test_pylintrc_msg_template(testdir, rcformat): """Verify that msg-template from pylintrc file is handled.""" if rcformat == "toml": rcfile = testdir.makefile( - '.toml', + ".toml", """ [tool.pylint.REPORTS] msg-template = "start {msg_id} end" - """ + """, ) else: rcfile = testdir.makefile( - '.rc', + ".rc", """ [REPORTS] msg-template=start {msg_id} end - """ + """, ) - testdir.makepyfile('import sys') - result = testdir.runpytest( - '--pylint', '--pylint-rcfile={0}'.format(rcfile.strpath) - ) - assert 'start W0611 end' in result.stdout.str() + testdir.makepyfile("import sys") + result = testdir.runpytest("--pylint", f"--pylint-rcfile={rcfile.strpath}") + assert "start W0611 end" in result.stdout.str() def test_multiple_jobs(testdir): """ Assert that the jobs argument is passed through to pylint if provided """ - testdir.makepyfile('import sys') - with mock.patch('pytest_pylint.plugin.lint.Run') as run_mock: + testdir.makepyfile("import sys") + with mock.patch("pytest_pylint.plugin.lint.Run") as run_mock: jobs = 0 - testdir.runpytest( - '--pylint', '--pylint-jobs={0}'.format(jobs) - ) + testdir.runpytest("--pylint", f"--pylint-jobs={jobs}") assert run_mock.call_count == 1 - assert run_mock.call_args[0][0][-2:] == ['-j', str(jobs)] + assert run_mock.call_args[0][0][-2:] == ["-j", str(jobs)] def test_no_multiple_jobs(testdir): """ If no jobs argument is specified it should not appear in pylint arguments """ - testdir.makepyfile('import sys') - with mock.patch('pytest_pylint.plugin.lint.Run') as run_mock: - testdir.runpytest('--pylint') + testdir.makepyfile("import sys") + with mock.patch("pytest_pylint.plugin.lint.Run") as run_mock: + testdir.runpytest("--pylint") assert run_mock.call_count == 1 - assert '-j' not in run_mock.call_args[0][0] + assert "-j" not in run_mock.call_args[0][0] def test_skip_checked_files(testdir): @@ -248,152 +258,138 @@ def test_skip_checked_files(testdir): The 2nd time should be skipped. """ testdir.makepyfile( - '#!/usr/bin/env python', + "#!/usr/bin/env python", '"""A hello world script."""', - '', - 'from __future__ import print_function', - '', + "", + "from __future__ import print_function", + "", 'print("Hello world!") # pylint: disable=missing-final-newline', ) # The 1st time should be passed - result = testdir.runpytest('--pylint') - assert '1 passed' in result.stdout.str() + result = testdir.runpytest("--pylint") + assert "1 passed" in result.stdout.str() # The 2nd time should be skipped - result = testdir.runpytest('--pylint') - assert '1 skipped' in result.stdout.str() + result = testdir.runpytest("--pylint") + assert "1 skipped" in result.stdout.str() # Always be passed when cacheprovider disabled - result = testdir.runpytest('--pylint', '-p', 'no:cacheprovider') - assert '1 passed' in result.stdout.str() + result = testdir.runpytest("--pylint", "-p", "no:cacheprovider") + assert "1 passed" in result.stdout.str() def test_invalidate_cache_when_config_changes(testdir): """If pylintrc changes, no cache should apply.""" rcfile = testdir.makefile( - '.rc', - '[MESSAGES CONTROL]', - 'disable=missing-final-newline' + ".rc", "[MESSAGES CONTROL]", "disable=missing-final-newline" ) testdir.makepyfile('"""hi."""') - result = testdir.runpytest( - '--pylint', '--pylint-rcfile={0}'.format(rcfile.strpath) - ) - assert '1 passed' in result.stdout.str() + result = testdir.runpytest("--pylint", f"--pylint-rcfile={rcfile.strpath}") + assert "1 passed" in result.stdout.str() - result = testdir.runpytest( - '--pylint', '--pylint-rcfile={0}'.format(rcfile.strpath) - ) - assert '1 skipped' in result.stdout.str() + result = testdir.runpytest("--pylint", f"--pylint-rcfile={rcfile.strpath}") + assert "1 skipped" in result.stdout.str() # Change RC file entirely - result = testdir.runpytest('--pylint') - assert '1 failed' in result.stdout.str() + alt_rcfile = testdir.makefile( + ".rc", alt="[MESSAGES CONTROL]\ndisable=unbalanced-tuple-unpacking" + ) + result = testdir.runpytest("--pylint", f"--pylint-rcfile={alt_rcfile.strpath}") + assert "1 failed" in result.stdout.str() # Change contents of RC file - result = testdir.runpytest( - '--pylint', '--pylint-rcfile={0}'.format(rcfile.strpath) - ) - assert '1 passed' in result.stdout.str() + result = testdir.runpytest("--pylint", f"--pylint-rcfile={rcfile.strpath}") + assert "1 passed" in result.stdout.str() - with open(rcfile, 'w'): + with open(rcfile, "w", encoding="utf-8"): pass - result = testdir.runpytest( - '--pylint', '--pylint-rcfile={0}'.format(rcfile.strpath) - ) - assert '1 failed' in result.stdout.str() + result = testdir.runpytest("--pylint", f"--pylint-rcfile={rcfile.strpath}") + assert "1 failed" in result.stdout.str() def test_output_file(testdir): """Verify pylint report output""" - testdir.makepyfile('import sys') - testdir.runpytest('--pylint', '--pylint-output-file=pylint.report') - output_file = os.path.join(testdir.tmpdir.strpath, 'pylint.report') + testdir.makepyfile("import sys") + testdir.runpytest("--pylint", "--pylint-output-file=pylint.report") + output_file = os.path.join(testdir.tmpdir.strpath, "pylint.report") assert os.path.isfile(output_file) - with open(output_file, 'r') as _file: + with open(output_file, "r", encoding="utf-8") as _file: report = _file.read() assert ( - 'test_output_file.py:1: [C0304(missing-final-newline), ] Final ' - 'newline missing' + "test_output_file.py:1: [C0304(missing-final-newline), ] Final " + "newline missing" ) in report + assert ( - 'test_output_file.py:1: [C0111(missing-docstring), ] Missing ' - 'module docstring' + "test_output_file.py:1: [C0111(missing-docstring), ] Missing " + "module docstring" ) in report or ( - 'test_output_file.py:1: [C0114(missing-module-docstring), ] Missing ' - 'module docstring' + "test_output_file.py:1: [C0114(missing-module-docstring), ] Missing " + "module docstring" ) in report + assert ( - 'test_output_file.py:1: [W0611(unused-import), ] Unused import sys' + "test_output_file.py:1: [W0611(unused-import), ] Unused import sys" ) in report def test_output_file_makes_dirs(testdir): """Verify output works with folders properly.""" - testdir.makepyfile('import sys') - output_path = os.path.join('reports', 'pylint.report') - testdir.runpytest( - '--pylint', - '--pylint-output-file={}'.format(output_path) - ) + testdir.makepyfile("import sys") + output_path = os.path.join("reports", "pylint.report") + testdir.runpytest("--pylint", f"--pylint-output-file={output_path}") output_file = os.path.join(testdir.tmpdir.strpath, output_path) assert os.path.isfile(output_file) # Run again to make sure we don't crash trying to make a dir that exists - testdir.runpytest( - '--pylint', - '--pylint-output-file={}'.format(output_path) - ) + testdir.runpytest("--pylint", f"--pylint-output-file={output_path}") -@pytest.mark.parametrize('arg_opt_name, arg_opt_value', [ - ('ignore', 'test_cmd_line_ignore.py'), - ('ignore-patterns', '.+_ignore.py'), -], ids=['ignore', 'ignore-patterns']) +@pytest.mark.parametrize( + "arg_opt_name, arg_opt_value", + [("ignore", "test_cmd_line_ignore.py"), ("ignore-patterns", ".+_ignore.py")], + ids=["ignore", "ignore-patterns"], +) def test_cmd_line_ignore(testdir, arg_opt_name, arg_opt_value): """Verify that cmd line args ignores will work.""" - testdir.makepyfile(test_cmd_line_ignore='import sys') - result = testdir.runpytest( - '--pylint', '--pylint-{0}={1}'.format(arg_opt_name, arg_opt_value) - ) - assert 'collected 0 items' in result.stdout.str() - assert 'Unused import sys' not in result.stdout.str() - - -@pytest.mark.parametrize('arg_opt_name, arg_opt_value', [ - ('ignore', 'test_cmd_line_ignore_pri_arg.py'), - ('ignore-patterns', '.*arg.py$'), -], ids=['ignore', 'ignore-patterns']) -def test_cmd_line_ignore_pri(testdir, arg_opt_name, arg_opt_value): + testdir.makepyfile(test_cmd_line_ignore="import sys") + result = testdir.runpytest("--pylint", f"--pylint-{arg_opt_name}={arg_opt_value}") + assert "collected 0 items" in result.stdout.str() + assert "Unused import sys" not in result.stdout.str() + + +@pytest.mark.parametrize( + "arg_opt_name, arg_opt_value", + [("ignore", "test_cmd_line_ignore_pri_arg.py"), ("ignore-patterns", ".*arg.py$")], + ids=["ignore", "ignore-patterns"], +) +@pytest.mark.parametrize("sectionname", ("main", "master")) +def test_cmd_line_ignore_pri(testdir, arg_opt_name, arg_opt_value, sectionname): """ Verify that command line ignores and patterns take priority over rcfile ignores. """ - file_ignore = 'test_cmd_line_ignore_pri_file.py' - cmd_arg_ignore = 'test_cmd_line_ignore_pri_arg.py' + file_ignore = "test_cmd_line_ignore_pri_file.py" + cmd_arg_ignore = "test_cmd_line_ignore_pri_arg.py" cmd_line_ignore = arg_opt_value rcfile = testdir.makefile( - '.rc', - """ - [MASTER] + ".rc", + f""" + [{sectionname.upper()}] - {0} = {1},foo - """.format(arg_opt_name, file_ignore) + {arg_opt_name} = {file_ignore},foo + """, ) - testdir.makepyfile(**{ - file_ignore: 'import sys', - cmd_arg_ignore: 'import os', - }) + testdir.makepyfile(**{file_ignore: "import sys", cmd_arg_ignore: "import os"}) result = testdir.runpytest( - - '--pylint', - '--pylint-rcfile={0}'.format(rcfile.strpath), - '--pylint-{0}={1}'.format(arg_opt_name, cmd_line_ignore), - '-s', + "--pylint", + f"--pylint-rcfile={rcfile.strpath}", + f"--pylint-{arg_opt_name}={cmd_line_ignore}", + "-s", ) - assert 'collected 1 item' in result.stdout.str() - assert 'Unused import sys' in result.stdout.str() + assert "collected 1 item" in result.stdout.str() + assert "Unused import sys" in result.stdout.str() diff --git a/pytest_pylint/tests/test_util.py b/pytest_pylint/tests/test_util.py index 8cd4f35..0fc0662 100644 --- a/pytest_pylint/tests/test_util.py +++ b/pytest_pylint/tests/test_util.py @@ -9,12 +9,12 @@ def test_get_rel_path(): """ Verify our relative path function. """ - correct_rel_path = 'How/Are/You/blah.py' - path = '/Hi/How/Are/You/blah.py' - parent_path = '/Hi/' + correct_rel_path = "How/Are/You/blah.py" + path = "/Hi/How/Are/You/blah.py" + parent_path = "/Hi/" assert get_rel_path(path, parent_path) == correct_rel_path - parent_path = '/Hi' + parent_path = "/Hi" assert get_rel_path(path, parent_path) == correct_rel_path @@ -23,9 +23,7 @@ def test_should_include_path(): Files should only be included in the list if none of the directories on it's path, of the filename, match an entry in the ignore list. """ - ignore_list = [ - "first", "second", "third", "part", "base.py" - ] + ignore_list = ["first", "second", "third", "part", "base.py"] # Default includes. assert should_include_file("random", ignore_list) is True assert should_include_file("random/filename", ignore_list) is True @@ -38,31 +36,19 @@ def test_should_include_path(): assert should_include_file("random/part/filename.py", ignore_list) is False assert should_include_file("random/second/part.py", ignore_list) is False # Part as substring on paths. - assert should_include_file( - "part_it/other/filename.py", ignore_list - ) is True - assert should_include_file( - "random/part_it/filename.py", ignore_list - ) is True + assert should_include_file("part_it/other/filename.py", ignore_list) is True + assert should_include_file("random/part_it/filename.py", ignore_list) is True assert should_include_file("random/other/part_it.py", ignore_list) is True def test_pylint_ignore_patterns(): """Test if the ignore-patterns is working""" - ignore_patterns = [ - "first.*", - ".*second", - "^third.*fourth$", - "part", - "base.py" - ] + ignore_patterns = ["first.*", ".*second", "^third.*fourth$", "part", "base.py"] # Default includes assert should_include_file("random", [], ignore_patterns) is True assert should_include_file("random/filename", [], ignore_patterns) is True - assert should_include_file( - "random/other/filename", [], ignore_patterns - ) is True + assert should_include_file("random/other/filename", [], ignore_patterns) is True # Pattern matches assert should_include_file("first1", [], ignore_patterns) is False diff --git a/pytest_pylint/util.py b/pytest_pylint/util.py index 24f0618..119d5ff 100644 --- a/pytest_pylint/util.py +++ b/pytest_pylint/util.py @@ -14,8 +14,9 @@ def get_rel_path(path, parent_path): """ Give the path to object relative to ``parent_path``. """ - replaced_path = path.replace(parent_path, '', 1) - if replaced_path[0] == sep: + replaced_path = path.replace(parent_path, "", 1) + + if replaced_path[0] == sep and replaced_path != path: rel_path = replaced_path[1:] else: rel_path = replaced_path diff --git a/setup.cfg b/setup.cfg index b7e4789..1e1c926 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,2 +1,6 @@ [aliases] test=pytest + +[flake8] +max-line-length = 88 +extend-ignore = E203, W503, E231 diff --git a/setup.py b/setup.py index f741f75..748a511 100644 --- a/setup.py +++ b/setup.py @@ -8,28 +8,32 @@ from setuptools import setup +with open("README.rst", encoding="utf-8") as f: + LONG_DESCRIPTION = f.read() + setup( - name='pytest-pylint', - description='pytest plugin to check source code with pylint', - long_description=open("README.rst").read(), - license='MIT', - version='0.18.0', - author='Carson Gee', - author_email='x@carsongee.com', - url='https://github.com/carsongee/pytest-pylint', - packages=['pytest_pylint'], - entry_points={'pytest11': ['pylint = pytest_pylint.plugin']}, - python_requires=">=3.5", - install_requires=['pytest>=5.4', 'pylint>=2.3.0', 'toml>=0.7.1'], - setup_requires=['pytest-runner'], - tests_require=['coverage', 'pytest-flake8'], + name="pytest-pylint", + description="pytest plugin to check source code with pylint", + long_description=LONG_DESCRIPTION, + license="MIT", + version="0.19.0", + author="Carson Gee", + author_email="x@carsongee.com", + url="https://github.com/carsongee/pytest-pylint", + packages=["pytest_pylint"], + entry_points={"pytest11": ["pylint = pytest_pylint.plugin"]}, + python_requires=">=3.7", + install_requires=["pytest>=5.4", "pylint>=2.3.0", "toml>=0.7.1"], + setup_requires=["pytest-runner"], + tests_require=["coverage", "flake8", "black", "isort"], classifiers=[ - 'Development Status :: 5 - Production/Stable', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: MIT License', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", ], ) diff --git a/tox.ini b/tox.ini index e3c5324..9fbb084 100644 --- a/tox.ini +++ b/tox.ini @@ -1,38 +1,47 @@ [tox] -envlist = py3{6, 7, 8, 9}-pylint{23, 24, 25, latest, master}-pytest{54, latest, master}, coverage +envlist = + py3{7, 8, 9}-pylint{23}-pytest{54} + py3{7, 8, 9, 10}-pylint{213, 214}-pytest{71} + py3{7, 8, 9, 10, 11}-pylint{215, latest, main}-pytest{71, latest, main} + coverage + qa skip_missing_interpreters = true [testenv] usedevelop = true deps = - pylint23: pylint>=2.3,<2.4 - pylint24: pylint>=2.4,<2.5 - pylint25: pylint>=2.5,<2.6 + pylint23: pylint~=2.3.1 + pylint213: pylint~=2.13.9 + pylint214: pylint~=2.14.5 + pylint215: pylint~=2.15.0 pylintlatest: pylint - pylintmaster: git+https://github.com/PyCQA/pylint.git@master#egg=pylint - pylintmaster: git+https://github.com/PyCQA/astroid.git@master#egg=astroid - pytest54: pytest>=5.4,<5.5 + pylintmain: git+https://github.com/PyCQA/pylint.git@main#egg=pylint + pylintmain: git+https://github.com/PyCQA/astroid.git@main#egg=astroid + pytest54: pytest~=5.4.3 + pytest71: pytest~=7.1.2 pytestlatest: pytest - pytestmaster: git+https://github.com/pytest-dev/pytest.git@master#egg=pytest - pytest-flake8 + pytestmain: git+https://github.com/pytest-dev/pytest.git@main#egg=pytest coverage commands = coverage run -m py.test {posargs} [testenv:coverage] -depends = py3{5, 6, 7, 8}-pylint{23, 24, 25, latest, master}-pytest{54, latest, master} +depends = py3{7, 8, 9, 10, 11}-pylint{215, latest, main}-pytest{71, latest, main} commands = coverage combine coverage report coverage html -d htmlcov -[travis] -python = - 3.5: py35, coverage - 3.6: py36, coverage - 3.7: py37, coverage - 3.8: py38, coverage +[testenv:qa] +skip_install=true +deps = + black + flake8 + isort +commands = + flake8 . + black --check . + isort --check-only --diff . [pytest] -addopts = --pylint --flake8 -markers = flake8 +addopts = --pylint