pax_global_header00006660000000000000000000000064145077045410014521gustar00rootroot0000000000000052 comment=229b64fcef27c5ac436f3b3a3d0ca07df0bf8b52 pytest-pylint-0.21.0/000077500000000000000000000000001450770454100144465ustar00rootroot00000000000000pytest-pylint-0.21.0/.coveragerc000066400000000000000000000001001450770454100165560ustar00rootroot00000000000000[run] source = . parallel = True omit = .tox/* setup.py pytest-pylint-0.21.0/.github/000077500000000000000000000000001450770454100160065ustar00rootroot00000000000000pytest-pylint-0.21.0/.github/workflows/000077500000000000000000000000001450770454100200435ustar00rootroot00000000000000pytest-pylint-0.21.0/.github/workflows/tests.yml000066400000000000000000000015011450770454100217250ustar00rootroot00000000000000--- name: Tests on: [push, pull_request] jobs: tests: runs-on: ubuntu-latest strategy: fail-fast: false matrix: python-version: - "3.8" - "3.9" - "3.10" - "3.11" - "3.12.0-rc - 3.12" 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 pytest-pylint-0.21.0/.gitignore000066400000000000000000000014341450770454100164400ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] # C extensions *.so # Distribution / packaging .Python env/ build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ *.egg-info/ .installed.cfg *.egg # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache .pytest_cache/ nosetests.xml coverage.xml *,cover # Translations *.mo *.pot # Django stuff: *.log # Sphinx documentation docs/_build/ # PyBuilder target/ # PyEnv .python-version # Virtualenv .venv # Pycharm .idea # Vscode .vscodepytest-pylint-0.21.0/DEVELOPMENT.rst000066400000000000000000000057371450770454100167760ustar00rootroot00000000000000pytest pylint ------------- How it works ============ Helpers for running pylint with py.test and have configurable rule types (i.e. Convention, Warn, and Error) fail the build. You can also specify a pylintrc file. How it works We have a thin plugin wrapper that is installed through setup.py hooks as `pylint`. This wrapper uses pytest_addoption and pytest_configure to decide to configure and register the real plugin PylintPlygin Once it is registered in `pytest_configure`, the hooks already executed 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 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". `pytest_collect_file` hook returns a collection of Node, or None. In py.test context, Node being a base class that defines py.test Collection Tree. A Node can be a subclass of Collector, which has children, or an Item, which is a leaf node. A practical example would be, a Python test file (Collector), can have multiple test functions (multiple Items) For this plugin, the relatioship of File to Item is one to one, one file represents one pylint result. From that, there are two important classes: PyLintFile, and PyLintItem. PyLintFile represents a python file, extension ".py", that was collected based on target directory as mentioned previously. PyLintItem represents one file which pylint was ran or will run. Back to PylintPlugin, `pytest_collection_finish` hook will run after the collection phase where pylint will be ran on the collected files. Based on the ProgrammaticReporter, the result is stored in a dictionary with the file relative path of the file being the key, and a list of errors related to the file. All PylintFile returned during `pytest_collect_file`, returns an one element list of PyLintItem. The Item implements runtest method which will get the pylint messages per file and expose to the user. Development Environment ======================= Suggestion 1 ~~~~~~~~~~~~ Use `pyenv `_, and install all the versions supported by the plugin. Double-check on `tox.ini `_. .. code-block:: shell pyenv install 3.7.7 pyenv install 3.8.2 pyenv install 3.9.13 pyenv install 3.10.6 Set the installed versions as global, that will allow tox to find all of them. .. code-block:: shell pyenv global 3.10.6 3.9.13 3.8.2 3.7.7 Create virtualenv, install dependencies, run tests, and tox: .. code-block:: shell python3.10 -m venv .pytest_pylint source .pytest_pylint/bin/activate pip install --upgrade setuptools pip tox python setup.py install python setup.py test tox The development environment is complete. pytest-pylint-0.21.0/LICENSE000066400000000000000000000020661450770454100154570ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2015 Carson Gee Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. pytest-pylint-0.21.0/MANIFEST.in000066400000000000000000000002071450770454100162030ustar00rootroot00000000000000include README.rst include LICENSE include pytest_pylint graft pytest_pylint/tests global-exclude __pycache__ global-exclude *.py[cod] pytest-pylint-0.21.0/README.rst000066400000000000000000000130161450770454100161360ustar00rootroot00000000000000pytest pylint ------------- .. image:: https://github.com/carsongee/pytest-pylint/actions/workflows/tests.yml/badge.svg :target: https://github.com/carsongee/pytest-pylint/actions/workflows/tests.yml .. image:: https://img.shields.io/coveralls/carsongee/pytest-pylint.svg :target: https://coveralls.io/r/carsongee/pytest-pylint .. image:: https://img.shields.io/pypi/v/pytest-pylint.svg :target: https://pypi.python.org/pypi/pytest-pylint .. image:: https://anaconda.org/conda-forge/pytest-pylint/badges/version.svg :target: https://anaconda.org/conda-forge/pytest-pylint .. image:: https://anaconda.org/conda-forge/pytest-pylint/badges/downloads.svg :target: https://anaconda.org/conda-forge/pytest-pylint .. image:: https://img.shields.io/pypi/l/pytest-pylint.svg :target: https://pypi.python.org/pypi/pytest-pylint Run pylint with pytest and have configurable rule types (i.e. Convention, Warn, and Error) fail the build. You can also specify a pylintrc file. Sample Usage ============ .. code-block:: shell py.test --pylint would be the most simple usage and would run pylint for all error messages. .. code-block:: shell py.test --pylint --pylint-rcfile=/my/pyrc --pylint-error-types=EF --pylint-jobs=4 This would use the pylintrc file at /my/pyrc, only error on pylint Errors and Failures, and use 4 cores for running pylint. You can restrict your test run to only perform pylint checks and not any other tests by typing: .. code-block:: shell py.test --pylint -m pylint Acknowledgements ================ This code is heavily based on `pytest-flakes `__ Development =========== If you want to help development, there is `overview documentation `_ Releases ======== 0.21.0 ~~~~~~ - Dropped support for pytest < 7.0 in preparation for pytest 8.0 (should work with it when it comes out) - Dropped support for pylint < 2.15 to work better with Python 3.11 and drop backwards compatibility code - Use baked in TOML support with fallback to newer tomli library thanks to `mgorny `__ 0.20.0 ~~~~~~ - Corrected issues introduced by deprecations in pylint - Added support for Python 3.12 and dropped support for Python 3.7 - Last version that will support pytest < 7 and pylint < 2.6 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 ~~~~~~ - Added support for creating missing folders when using ``--pylint-output-file`` - Now when pylint's ``ignore_patterns`` is blank, we don't ignore all files - Added cache invalidation when your pylintrc changes - Verified support for latest pytest and Python 3.9 - Corrected badly named nodes (duplicated path) thanks to `yanqd0 `__ - Added tests to source distribution thanks to `sbraz `__ 0.17.0 ~~~~~~ - Added support for latest pylint API >=2.5.1 0.16.1 ~~~~~~ - Corrected documentation and correctly pinned dependencies properly 0.16.0 ~~~~~~ - Switched to new ``from_parent`` API and added development documentation `dineshtrivedi `_ - Added support for toml based configuration of pylint thanks to `michael-k `_ 0.15.1 ~~~~~~ - Made `--no-pylint` functional again 0.15.0 ~~~~~~ - Added support for Python 3.8 thanks to `michael-k `_ - Implemented option to output Pylint results to a reports file thanks to `jose-lpa `_ - Refactored into simpler plugin structure 0.14.1 ~~~~~~ - Corrected pytest-pylint to properly support ``-p no:cacheprovider`` thanks to `yanqd0 `__ 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 ~~~~~~ - Python 3.7 compatibility verified - Ignore paths no longer match partial names thanks to `heoga `__ 0.12.3 ~~~~~~ - `jamur2 `__ corrected issue where file paths where not being output properly on lint failures. 0.12.2 ~~~~~~ - Resolved issue where failing files weren't reported thanks to reports from `skirpichev `__ and `jamur2 `__ 0.12.1 ~~~~~~ - Corrected a bug preventing this plugin from working with py.test >= 3.7.0. 0.12.0 ~~~~~~ - `jwkvam `__ added progress output during linting. 0.11.0 ~~~~~~ - Added option ``--no-pylint`` to override ``--pylint`` for cases when it's turned on by default. 0.10.0 ~~~~~~ - `jwkvam `__ provided support for pylint 2.0 0.9.0 ~~~~~ - `noisecapella `__ added an option to run pylint with multiple processes 0.8.0 ~~~~~ - `bdrung `__ corrected inconsistent returns in a function - Dropped Python 3.3 support 0.7.1 ~~~~~ - Corrected path issue reported by `Kargathia `_ 0.7.0 ~~~~~ - Linting is performed before tests which enables code duplication checks to work along with a performance boost, thanks to @heoga pytest-pylint-0.21.0/pylintrc000066400000000000000000000001571450770454100162400ustar00rootroot00000000000000[MESSAGES CONTROL] disable = C0330, C0326 [FORMAT] max-line-length = 88 [TYPECHECK] ignored-classes = pytest pytest-pylint-0.21.0/pytest_pylint/000077500000000000000000000000001450770454100173755ustar00rootroot00000000000000pytest-pylint-0.21.0/pytest_pylint/__init__.py000066400000000000000000000000001450770454100214740ustar00rootroot00000000000000pytest-pylint-0.21.0/pytest_pylint/plugin.py000066400000000000000000000327261450770454100212570ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ pytest plugins. Both pylint wrapper and PylintPlugin """ import sys from collections import defaultdict from configparser import ConfigParser, NoOptionError, NoSectionError from os import getcwd, makedirs, sep from os.path import dirname, exists, getmtime, join from pathlib import Path import pytest from pylint import config as pylint_config from pylint import lint from .pylint_util import ProgrammaticReporter from .util import PyLintException, get_rel_path, should_include_file if sys.version_info >= (3, 11): import tomllib else: # pylint: disable=import-error import tomli as tomllib HISTKEY = "pylint/mtimes" PYLINT_CONFIG_CACHE_KEY = "pylintrc" FILL_CHARS = 80 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" ) group.addoption( "--no-pylint", action="store_true", default=False, help="disable running pylint ", ) group.addoption( "--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).", ) group.addoption( "--pylint-jobs", default=None, help="Specify number of processes to use for pylint", ) group.addoption( "--pylint-output-file", default=None, 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" ) group.addoption( "--pylint-ignore-patterns", default=None, help="Files/directories patterns that will be ignored", ) def pytest_configure(config): """ Add plugin class. :param _pytest.config.Config config: pytest config object """ 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) class PylintPlugin: """ The core plugin for pylint """ # pylint: disable=too-many-instance-attributes def __init__(self, config): if hasattr(config, "cache"): self.mtimes = config.cache.get(HISTKEY, {}) else: self.mtimes = {} self.pylint_files = set() self.pylint_messages = defaultdict(list) self.pylint_config = None self.pylintrc_file = None self.pylint_ignore = [] self.pylint_ignore_patterns = [] self.pylint_msg_template = None def pytest_configure(self, config): """Configure pytest after it is already enabled""" # Find pylintrc to check ignore list if config.option.pylint_rcfile: pylintrc_file = config.option.pylint_rcfile else: pylintrc_file = next(pylint_config.find_default_config_files(), None) if pylintrc_file and not exists(pylintrc_file): # The directory of pytest.ini got a chance pylintrc_file = join(dirname(str(config.inifile)), pylintrc_file) # Try getting ignores from pylintrc since we use pytest # collection methods and not pylint's internal mechanism if pylintrc_file and exists(pylintrc_file): self.pylintrc_file = pylintrc_file # 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.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.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(",") if config.option.pylint_ignore_patterns is not None: self.pylint_ignore_patterns = config.option.pylint_ignore_patterns.split( "," ) def _load_rc_file(self, pylintrc_file): self.pylint_config = ConfigParser() self.pylint_config.read(pylintrc_file) try: ignore_string = self.pylint_config.get("MAIN", "ignore") if ignore_string: self.pylint_ignore = ignore_string.split(",") except (NoSectionError, NoOptionError): 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("MAIN", "ignore-patterns") if ignore_patterns: self.pylint_ignore_patterns = ignore_patterns.split(",") except (NoSectionError, NoOptionError): 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") except (NoSectionError, NoOptionError): pass def _load_pyproject_toml(self, pylintrc_file): with open(pylintrc_file, "rb") as f_p: try: content = tomllib.load(f_p) except (TypeError, tomllib.TOMLDecodeError): return try: self.pylint_config = content["tool"]["pylint"] except KeyError: return main_section = {} reports_section = {} for key, value in self.pylint_config.items(): 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 = main_section.get("ignore") if ignore: self.pylint_ignore = ( ignore.split(",") if isinstance(ignore, str) else ignore ) self.pylint_ignore_patterns = main_section.get("ignore-patterns") or [] self.pylint_msg_template = reports_section.get("msg-template") def pytest_sessionfinish(self, session): """ Save file mtimes to pytest cache. :param _pytest.main.Session session: the pytest session object """ if hasattr(session.config, "cache"): session.config.cache.set(HISTKEY, self.mtimes) def pytest_collect_file(self, path, parent): """Collect files on which pylint should run""" if path.ext != ".py": return None rel_path = get_rel_path(path.strpath, str(parent.session.path)) if should_include_file( rel_path, self.pylint_ignore, self.pylint_ignore_patterns ): item = PylintFile.from_parent(parent, path=Path(path), plugin=self) else: return None # Check the cache if we should run it if not item.should_skip: self.pylint_files.add(rel_path) return item def pytest_collection_finish(self, session): """Lint collected files""" if not self.pylint_files: return jobs = session.config.option.pylint_jobs reporter = ProgrammaticReporter() # 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(f"--rcfile={self.pylintrc_file}") if jobs is not None: 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(f"--ignore={','.join(self.pylint_ignore)}") if self.pylint_ignore_patterns: args_list.append( f"--ignore-patterns={','.join(self.pylint_ignore_patterns)}" ) print("-" * FILL_CHARS) print("Linting files") # Run pylint over the collected files. # Pylint has changed APIs, but we support both # pylint: disable=unexpected-keyword-arg try: # pylint >= 2.5.1 API result = lint.Run(args_list, reporter=reporter, exit=False) except TypeError: # pylint < 2.5.1 API result = lint.Run(args_list, reporter=reporter, do_exit=False) messages = result.linter.reporter.data # Stores the messages in a dictionary for lookup in tests. for message in messages: # 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 mtime = None # : float @classmethod def from_parent(cls, parent, *, path, plugin, **kw): # pylint: disable=arguments-differ # We add the ``plugin`` kwarg to get plugin level information so the # signature differs _self = getattr(super(), "from_parent", cls)(parent, path=path, **kw) _self.plugin = plugin _self.rel_path = get_rel_path(str(path), str(parent.session.path)) _self.mtime = path.stat().st_mtime prev_mtime = _self.plugin.mtimes.get(_self.rel_path, 0) _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") class PyLintItem(pytest.Item): """pylint test running class.""" parent = None # : PylintFile plugin = None # : PylintPlugin def __init__(self, *args, **kw): super().__init__(*args, **kw) self.add_marker(MARKER) self.plugin = self.parent.plugin msg_format = self.plugin.pylint_msg_template if msg_format is None: 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) def setup(self): """Mark unchanged files as SKIPPED.""" if self.parent.should_skip: pytest.skip("file(s) previously passed pylint checks") def runtest(self): """Check the pylint messages to see if any errors were reported.""" pylint_output_file = self.config.option.pylint_output_file def _loop_errors(writer): reported_errors = [] 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)) writer( f"{error.path}:{error.line}: [{error.msg_id}" f"({error.symbol}), {error.obj}] " f"{error.msg}\n" ) return reported_errors if pylint_output_file: output_dir = dirname(pylint_output_file) if output_dir: makedirs(output_dir, exist_ok=True) 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)) # Update the cache if the item passed pylint. self.plugin.mtimes[self.parent.rel_path] = self.parent.mtime def repr_failure(self, excinfo, style=None): """Handle any test failures by checking that they were ours.""" # pylint: disable=arguments-differ if excinfo.errisinstance(PyLintException): return excinfo.value.args[0] return super().repr_failure(excinfo) def reportinfo(self): """Generate our test report""" # pylint: disable=no-member return self.path, None, f"[pylint] {self.parent.rel_path}" pytest-pylint-0.21.0/pytest_pylint/pylint_util.py000066400000000000000000000020521450770454100223220ustar00rootroot00000000000000# -*- coding: utf-8 -*- """Pylint reporter classes.""" import sys from pylint.reporters import BaseReporter class ProgrammaticReporter(BaseReporter): """Reporter that replaces output with storage in list of dictionaries""" extension = "prog" def __init__(self, output=None): BaseReporter.__init__(self, output) self.current_module = None self.data = [] def add_message(self, msg_id, location, msg): """Deprecated, but required""" raise NotImplementedError def handle_message(self, msg): """Get message and append to our data structure""" self.data.append(msg) def _display(self, layout): """launch layouts display""" def on_set_current_module(self, module, filepath): """Hook called when a module starts to be analysed.""" 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("") pytest-pylint-0.21.0/pytest_pylint/tests/000077500000000000000000000000001450770454100205375ustar00rootroot00000000000000pytest-pylint-0.21.0/pytest_pylint/tests/test_pytest_pylint.py000066400000000000000000000310741450770454100251040ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ Unit testing module for pytest-pylint plugin """ import pathlib import re from textwrap import dedent from unittest import mock import pylint.config import pytest 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() def test_nodeid(testdir): """Verify our nodeid adds a suffix""" 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") assert re.search( 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") 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() 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() 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() def test_pylintrc_file(testdir): """Verify that a specified pylint rc file will work.""" rcfile = testdir.makefile( ".rc", """ [FORMAT] max-line-length=3 """, ) 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", pylint=""" [tool.pylint.FORMAT] max-line-length = "3" """, ) 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(): rcfile = testdir.makefile( ".toml", pylint=""" [tool.pylint.FORMAT] max-line-length = 3 """, ) 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() def test_pylintrc_file_beside_ini(testdir): """ Verify that a specified pylint rc file will work when placed into pytest ini dir. """ non_cwd_dir = testdir.mkdir("non_cwd_dir") 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( f""" [pytest] addopts = --pylint --pylint-rcfile={rcfile.strpath} """ ) ) # 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(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")) @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", f""" [tool.pylint.{sectionname}] ignore = ["test_pylintrc_ignore.py", "foo.py"] """, ) elif rcformat == "simple_toml": rcfile = testdir.makefile( ".toml", f""" [tool.pylint.{sectionname.upper()}] ignore = "test_pylintrc_ignore.py,foo.py" """, ) else: rcfile = testdir.makefile( ".rc", f""" [{sectionname.upper()}] ignore = test_pylintrc_ignore.py """, ) 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")) def test_pylintrc_msg_template(testdir, rcformat): """Verify that msg-template from pylintrc file is handled.""" if rcformat == "toml": rcfile = testdir.makefile( ".toml", """ [tool.pylint.REPORTS] msg-template = "start {msg_id} end" """, ) else: rcfile = testdir.makefile( ".rc", """ [REPORTS] msg-template=start {msg_id} end """, ) 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: jobs = 0 testdir.runpytest("--pylint", f"--pylint-jobs={jobs}") assert run_mock.call_count == 1 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") assert run_mock.call_count == 1 assert "-j" not in run_mock.call_args[0][0] 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") assert "1 passed" in result.stdout.str() # The 2nd time should be skipped 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() 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" ) testdir.makepyfile('"""hi."""') result = testdir.runpytest("--pylint", f"--pylint-rcfile={rcfile.strpath}") assert "1 passed" in result.stdout.str() result = testdir.runpytest("--pylint", f"--pylint-rcfile={rcfile.strpath}") assert "1 skipped" in result.stdout.str() # Change RC file entirely 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", f"--pylint-rcfile={rcfile.strpath}") assert "1 passed" in result.stdout.str() with open(rcfile, "w", encoding="utf-8"): pass 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 = pathlib.Path(testdir.tmpdir.strpath) / "pylint.report" assert output_file.is_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" ) in report assert ( "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" ) in report assert ( "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 = pathlib.Path("reports", "pylint.report") testdir.runpytest("--pylint", f"--pylint-output-file={output_path}") output_file = pathlib.Path(testdir.tmpdir.strpath) / output_path assert output_file.is_file() # Run again to make sure we don't crash trying to make a dir that exists 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"], ) 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", 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" cmd_line_ignore = arg_opt_value rcfile = testdir.makefile( ".rc", f""" [{sectionname.upper()}] {arg_opt_name} = {file_ignore},foo """, ) testdir.makepyfile(**{file_ignore: "import sys", cmd_arg_ignore: "import os"}) result = testdir.runpytest( "--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() pytest-pylint-0.21.0/pytest_pylint/tests/test_util.py000066400000000000000000000054561450770454100231370ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ Unit testing module for pytest-pylint util.py module """ from pytest_pylint.util import get_rel_path, should_include_file 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/" assert get_rel_path(path, parent_path) == correct_rel_path parent_path = "/Hi" assert get_rel_path(path, parent_path) == correct_rel_path 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"] # Default includes. assert should_include_file("random", ignore_list) is True assert should_include_file("random/filename", ignore_list) is True assert should_include_file("random/other/filename", ignore_list) is True # Basic ignore matches. assert should_include_file("first/filename", ignore_list) is False assert should_include_file("random/base.py", ignore_list) is False # Part on paths. assert should_include_file("part/second/filename.py", ignore_list) is False 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("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"] # 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 # Pattern matches assert should_include_file("first1", [], ignore_patterns) is False assert should_include_file("first", [], ignore_patterns) is False assert should_include_file("_second", [], ignore_patterns) is False assert should_include_file("second_", [], ignore_patterns) is False assert should_include_file("second_", [], ignore_patterns) is False assert should_include_file("third fourth", [], ignore_patterns) is False assert should_include_file("_third fourth_", [], ignore_patterns) is True assert should_include_file("part", [], ignore_patterns) is False assert should_include_file("1part2", [], ignore_patterns) is True assert should_include_file("base.py", [], ignore_patterns) is False pytest-pylint-0.21.0/pytest_pylint/util.py000066400000000000000000000015501450770454100207250ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ Utility functions for gathering files, etc. """ import re from os import sep class PyLintException(Exception): """Exception to raise if a file has a specified pylint error""" 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 and replaced_path != path: rel_path = replaced_path[1:] else: rel_path = replaced_path return rel_path def should_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) pytest-pylint-0.21.0/setup.cfg000066400000000000000000000001261450770454100162660ustar00rootroot00000000000000[aliases] test=pytest [flake8] max-line-length = 88 extend-ignore = E203, W503, E231 pytest-pylint-0.21.0/setup.py000066400000000000000000000024431450770454100161630ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ pytest-pylint ============= Plugin for py.test for doing pylint tests """ # pylint: disable=import-error 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=LONG_DESCRIPTION, license="MIT", version="0.21.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>=7.0", "pylint>=2.15.0", "tomli>=1.1.0; python_version < '3.11'", ], 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.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", ], ) pytest-pylint-0.21.0/tox.ini000066400000000000000000000020361450770454100157620ustar00rootroot00000000000000[tox] envlist = py3{8, 9, 10}-pylint{215, 30}-pytest{7} py3{8, 9, 10, 11}-pylint{215, latest, main}-pytest{7, latest, main} py3{12}-pylint{latest, main}-pytest{7, latest, main} coverage qa skip_missing_interpreters = true [testenv] usedevelop = true deps = pylint215: pylint~=2.15.10 pylint30: pylint~=3.0 pylintlatest: pylint pylintmain: git+https://github.com/PyCQA/pylint.git@main#egg=pylint pylintmain: git+https://github.com/PyCQA/astroid.git@main#egg=astroid pytest7: pytest~=7.0.0 pytestlatest: pytest pytestmain: git+https://github.com/pytest-dev/pytest.git@main#egg=pytest coverage commands = coverage run -m pytest {posargs} [testenv:coverage] depends = py3{7, 8, 9, 10, 11}-pylint{215, latest, main}-pytest{71, latest, main} commands = coverage combine coverage report coverage html -d htmlcov [testenv:qa] skip_install=true deps = black flake8 isort commands = flake8 . black --check . isort --check-only --diff . [pytest] addopts = --pylint