diff --git a/.coveragerc b/.coveragerc index 5c730da..aa9d3be 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,3 +1,19 @@ +[paths] +source = + pylint_plugin_utils -[run] -source=pylint_plugin_utils +[report] +include = + pylint_plugin_utils/* +omit = + */test/* +exclude_lines = + # Re-enable default pragma + pragma: no cover + + # Debug-only code + def __repr__ + + # Type checking code not executed during pytest runs + if TYPE_CHECKING: + @overload diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..8b98ed4 --- /dev/null +++ b/.flake8 @@ -0,0 +1,6 @@ +[flake8] +ignore = + E203, W503, # Incompatible with black see https://github.com/ambv/black/issues/315 + +max-line-length=88 +max-complexity=39 diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..06042df --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,349 @@ +name: CI + +on: [push] + +env: + CACHE_VERSION: 3 + DEFAULT_PYTHON: 3.8 + PRE_COMMIT_CACHE: ~/.cache/pre-commit + +jobs: + prepare-base: + name: Prepare base dependencies + runs-on: ubuntu-latest + outputs: + python-key: ${{ steps.generate-python-key.outputs.key }} + pre-commit-key: ${{ steps.generate-pre-commit-key.outputs.key }} + steps: + - name: Check out code from GitHub + uses: actions/checkout@v2.4.0 + with: + fetch-depth: 0 + - name: Set up Python ${{ env.DEFAULT_PYTHON }} + id: python + uses: actions/setup-python@v2.3.1 + with: + python-version: ${{ env.DEFAULT_PYTHON }} + - name: Generate partial Python venv restore key + id: generate-python-key + run: >- + echo "::set-output name=key::base-venv-${{ env.CACHE_VERSION }}-${{ + hashFiles('setup.cfg', 'requirements_test.txt', 'requirements_test_min.txt') + }}" + - name: Restore Python virtual environment + id: cache-venv + uses: actions/cache@v2.1.7 + with: + path: venv + key: >- + ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ + steps.generate-python-key.outputs.key }} + restore-keys: | + ${{ runner.os }}-${{ steps.python.outputs.python-version }}-base-venv-${{ env.CACHE_VERSION }}- + - name: Create Python virtual environment + if: steps.cache-venv.outputs.cache-hit != 'true' + run: | + python -m venv venv + . venv/bin/activate + python -m pip install -U pip setuptools wheel + pip install -U -r requirements_test.txt + - name: Generate pre-commit restore key + id: generate-pre-commit-key + run: >- + echo "::set-output name=key::pre-commit-${{ env.CACHE_VERSION }}-${{ + hashFiles('.pre-commit-config.yaml') }}" + - name: Restore pre-commit environment + id: cache-precommit + uses: actions/cache@v2.1.7 + with: + path: ${{ env.PRE_COMMIT_CACHE }} + key: >- + ${{ runner.os }}-${{ steps.generate-pre-commit-key.outputs.key }} + restore-keys: | + ${{ runner.os }}-pre-commit-${{ env.CACHE_VERSION }}- + - name: Install pre-commit dependencies + if: steps.cache-precommit.outputs.cache-hit != 'true' + run: | + . venv/bin/activate + pre-commit install --install-hooks + + prepare-tests-linux: + name: Prepare tests for Python ${{ matrix.python-version }} (Linux) + runs-on: ubuntu-latest + strategy: + matrix: + python-version: [3.6, 3.7, 3.8, 3.9, "3.10"] + outputs: + python-key: ${{ steps.generate-python-key.outputs.key }} + steps: + - name: Check out code from GitHub + uses: actions/checkout@v2.4.0 + with: + fetch-depth: 0 + - name: Set up Python ${{ matrix.python-version }} + id: python + uses: actions/setup-python@v2.3.1 + with: + python-version: ${{ matrix.python-version }} + - name: Generate partial Python venv restore key + id: generate-python-key + run: >- + echo "::set-output name=key::venv-${{ env.CACHE_VERSION }}-${{ + hashFiles('setup.cfg', 'requirements_test.txt', 'requirements_test_min.txt') + }}" + - name: Restore Python virtual environment + id: cache-venv + uses: actions/cache@v2.1.7 + with: + path: venv + key: >- + ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ + steps.generate-python-key.outputs.key }} + restore-keys: | + ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ env.CACHE_VERSION }}- + - name: Create Python virtual environment + if: steps.cache-venv.outputs.cache-hit != 'true' + run: | + python -m venv venv + . venv/bin/activate + python -m pip install -U pip setuptools wheel + pip install -U -r requirements_test.txt + + pytest-linux: + name: Run tests Python ${{ matrix.python-version }} (Linux) + runs-on: ubuntu-latest + needs: prepare-tests-linux + strategy: + fail-fast: false + matrix: + python-version: [3.6, 3.7, 3.8, 3.9, "3.10"] + steps: + - name: Check out code from GitHub + uses: actions/checkout@v2.4.0 + - name: Set up Python ${{ matrix.python-version }} + id: python + uses: actions/setup-python@v2.3.1 + with: + python-version: ${{ matrix.python-version }} + - name: Restore Python virtual environment + id: cache-venv + uses: actions/cache@v2.1.7 + with: + path: venv + key: + ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ + needs.prepare-tests-linux.outputs.python-key }} + - name: Fail job if Python cache restore failed + if: steps.cache-venv.outputs.cache-hit != 'true' + run: | + echo "Failed to restore Python venv from cache" + exit 1 + - name: Run pytest + run: | + . venv/bin/activate + pytest --cov --cov-report= tests/ + - name: Upload coverage artifact + uses: actions/upload-artifact@v2.3.1 + with: + name: coverage-${{ matrix.python-version }} + path: .coverage + + coverage: + name: Process test coverage + runs-on: ubuntu-latest + needs: ["prepare-tests-linux", "pytest-linux"] + strategy: + matrix: + python-version: [3.8] + env: + COVERAGERC_FILE: .coveragerc + steps: + - name: Check out code from GitHub + uses: actions/checkout@v2.4.0 + - name: Set up Python ${{ matrix.python-version }} + id: python + uses: actions/setup-python@v2.3.1 + with: + python-version: ${{ matrix.python-version }} + - name: Restore Python virtual environment + id: cache-venv + uses: actions/cache@v2.1.7 + with: + path: venv + key: + ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ + needs.prepare-tests-linux.outputs.python-key }} + - name: Fail job if Python cache restore failed + if: steps.cache-venv.outputs.cache-hit != 'true' + run: | + echo "Failed to restore Python venv from cache" + exit 1 + - name: Download all coverage artifacts + uses: actions/download-artifact@v2.1.0 + - name: Combine coverage results + run: | + . venv/bin/activate + coverage combine coverage*/.coverage + coverage report --rcfile=${{ env.COVERAGERC_FILE }} + - name: Upload coverage to Coveralls + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + . venv/bin/activate + coveralls --rcfile=${{ env.COVERAGERC_FILE }} --service=github + + prepare-tests-windows: + name: Prepare tests for Python ${{ matrix.python-version }} (Windows) + runs-on: windows-latest + strategy: + matrix: + python-version: [3.6, 3.7, 3.8, 3.9, "3.10"] + outputs: + python-key: ${{ steps.generate-python-key.outputs.key }} + steps: + - name: Check out code from GitHub + uses: actions/checkout@v2.4.0 + with: + fetch-depth: 0 + - name: Set up Python ${{ matrix.python-version }} + id: python + uses: actions/setup-python@v2.3.1 + with: + python-version: ${{ matrix.python-version }} + - name: Generate partial Python venv restore key + id: generate-python-key + run: >- + echo "::set-output name=key::venv-${{ env.CACHE_VERSION }}-${{ + hashFiles('setup.cfg', 'requirements_test_min.txt') + }}" + - name: Restore Python virtual environment + id: cache-venv + uses: actions/cache@v2.1.7 + with: + path: venv + key: >- + ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ + steps.generate-python-key.outputs.key }} + restore-keys: | + ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ env.CACHE_VERSION }}- + - name: Create Python virtual environment + if: steps.cache-venv.outputs.cache-hit != 'true' + run: | + python -m venv venv + . venv\\Scripts\\activate + python -m pip install -U pip setuptools wheel + pip install -U -r requirements_test_min.txt + + pytest-windows: + name: Run tests Python ${{ matrix.python-version }} (Windows) + runs-on: windows-latest + needs: prepare-tests-windows + strategy: + fail-fast: false + matrix: + python-version: [3.6, 3.7, 3.8, 3.9, "3.10"] + steps: + - name: Set temp directory + run: echo "TEMP=$env:USERPROFILE\AppData\Local\Temp" >> $env:GITHUB_ENV + # Workaround to set correct temp directory on Windows + # https://github.com/actions/virtual-environments/issues/712 + - name: Check out code from GitHub + uses: actions/checkout@v2.4.0 + - name: Set up Python ${{ matrix.python-version }} + id: python + uses: actions/setup-python@v2.3.1 + with: + python-version: ${{ matrix.python-version }} + - name: Restore Python virtual environment + id: cache-venv + uses: actions/cache@v2.1.7 + with: + path: venv + key: + ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ + needs.prepare-tests-windows.outputs.python-key }} + - name: Fail job if Python cache restore failed + if: steps.cache-venv.outputs.cache-hit != 'true' + run: | + echo "Failed to restore Python venv from cache" + exit 1 + - name: Run pytest + run: | + . venv\\Scripts\\activate + pytest tests/ + + prepare-tests-pypy: + name: Prepare tests for Python ${{ matrix.python-version }} + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["pypy3"] + outputs: + python-key: ${{ steps.generate-python-key.outputs.key }} + steps: + - name: Check out code from GitHub + uses: actions/checkout@v2.4.0 + with: + fetch-depth: 0 + - name: Set up Python ${{ matrix.python-version }} + id: python + uses: actions/setup-python@v2.3.1 + with: + python-version: ${{ matrix.python-version }} + - name: Generate partial Python venv restore key + id: generate-python-key + run: >- + echo "::set-output name=key::venv-${{ env.CACHE_VERSION }}-${{ + hashFiles('setup.cfg', 'requirements_test_min.txt') + }}" + - name: Restore Python virtual environment + id: cache-venv + uses: actions/cache@v2.1.7 + with: + path: venv + key: >- + ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ + steps.generate-python-key.outputs.key }} + restore-keys: | + ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ env.CACHE_VERSION }}- + - name: Create Python virtual environment + if: steps.cache-venv.outputs.cache-hit != 'true' + run: | + python -m venv venv + . venv/bin/activate + python -m pip install -U pip setuptools wheel + pip install -U -r requirements_test_min.txt + + pytest-pypy: + name: Run tests Python ${{ matrix.python-version }} + runs-on: ubuntu-latest + needs: prepare-tests-pypy + strategy: + fail-fast: false + matrix: + python-version: ["pypy3"] + steps: + - name: Check out code from GitHub + uses: actions/checkout@v2.4.0 + - name: Set up Python ${{ matrix.python-version }} + id: python + uses: actions/setup-python@v2.3.1 + with: + python-version: ${{ matrix.python-version }} + - name: Restore Python virtual environment + id: cache-venv + uses: actions/cache@v2.1.7 + with: + path: venv + key: + ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ + needs.prepare-tests-pypy.outputs.python-key }} + - name: Fail job if Python cache restore failed + if: steps.cache-venv.outputs.cache-hit != 'true' + run: | + echo "Failed to restore Python venv from cache" + exit 1 + - name: Run pytest + run: | + . venv/bin/activate + pytest tests/ diff --git a/.gitignore b/.gitignore index d2d6f36..f84c18d 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,9 @@ pip-log.txt .coverage .tox nosetests.xml +.pytest_cache +.benchmarks +htmlcov # Translations *.mo @@ -33,3 +36,5 @@ nosetests.xml .mr.developer.cfg .project .pydevproject +.pylint-plugin-utils +.idea diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..0ff6c8b --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,24 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.1.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - repo: https://github.com/PyCQA/isort + rev: 5.10.1 + hooks: + - id: isort + - repo: https://github.com/psf/black + rev: 21.12b0 + hooks: + - id: black + args: [--safe, --quiet] + - repo: https://github.com/Pierre-Sassoulas/black-disable-checker/ + rev: 1.0.1 + hooks: + - id: black-disable-checker + - repo: https://github.com/PyCQA/flake8 + rev: 4.0.1 + hooks: + - id: flake8 + additional_dependencies: [flake8-typing-imports==1.10.1] diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index b2d15a8..0000000 --- a/.travis.yml +++ /dev/null @@ -1,26 +0,0 @@ -dist: xenial -language: python -python: - - "3.5" - - "3.6" - - "3.7" -env: - - PYLINT=2.0.0 - - PYLINT=2.1.1 - - PYLINT=2.2.2 - - PYLINT=2.3.0 - - PYLINT=master -install: - - | - pip install coverage coveralls - if [ "$PYLINT" == "master" ]; then - pip install https://github.com/PyCQA/astroid/zipball/master - pip install https://github.com/PyCQA/pylint/zipball/master - else - pip install pylint==$PYLINT - fi - -script: - - coverage run tests.py -after_success: - - coveralls diff --git a/README.md b/README.md index 460be3b..280f55d 100644 --- a/README.md +++ b/README.md @@ -2,14 +2,47 @@ ## Status -[![Build Status](https://travis-ci.org/PyCQA/pylint-plugin-utils.svg?branch=master)](https://travis-ci.org/PyCQA/pylint-plugin-utils) -[![Code Health](https://landscape.io/github/PyCQA/pylint-plugin-utils/master/landscape.svg?style=flat)](https://landscape.io/github/PyCQA/pylint-plugin-utils/master) +[![Build Status](https://github.com/PyCQA/pylint/actions/workflows/ci.yaml/badge.svg?branch=master)](https://github.com/PyCQA/pylint/actions) [![Coverage Status](https://coveralls.io/repos/github/PyCQA/pylint-plugin-utils/badge.svg?branch=master)](https://coveralls.io/github/PyCQA/pylint-plugin-utils?branch=master) +[![Code Style](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/ambv/black) # About Utilities and helpers for writing Pylint plugins. This is not a direct Pylint plugin, but rather a set of tools and functions used by other plugins such as [pylint-django](https://github.com/PyCQA/pylint-django) and [pylint-celery](https://github.com/PyCQA/pylint-celery). +# Testing +Create virtualenv: +```bash +python3.8 -m venv .pylint-plugin-utils +source .pylint-plugin-utils/bin/activate +pip install --upgrade pip setuptools +``` + +We use [tox](https://tox.readthedocs.io/en/latest/) for running the test suite. You should be able to install it with: +```bash +pip install tox pytest +``` + +To run the test suite for a particular Python version, you can do: +```bash +tox -e py38 +``` + +To run individual tests with ``tox``, you can do:: +```bash +tox -e py38 -- -k test_linter_should_be_pickleable +``` + +We use pytest_ for testing ``pylint``, which you can use without using ``tox`` for a faster development cycle. + +If you want to run tests on a specific portion of the code with [pytest](https://docs.pytest.org/en/latest/), [pytest-cov](https://pypi.org/project/pytest-cov/) and your local python version:: +```bash +pip install pytest-cov +# Everything: +python3 -m pytest tests/ --cov=pylint_plugin_utils +coverage html +``` + # License `pylint-plugin-utils` is available under the GPLv2 License. diff --git a/pylint_plugin_utils/__init__.py b/pylint_plugin_utils/__init__.py index e871dbc..0340683 100644 --- a/pylint_plugin_utils/__init__.py +++ b/pylint_plugin_utils/__init__.py @@ -1,19 +1,19 @@ -import sys +from typing import List from pylint.exceptions import UnknownMessageError +from pylint.lint import PyLinter def get_class(module_name, kls): - parts = kls.split('.') + parts = kls.split(".") m = __import__(module_name) - for mp in module_name.split('.')[1:]: + for mp in module_name.split(".")[1:]: m = getattr(m, mp) klass = getattr(m, parts[0]) return klass class NoSuchChecker(Exception): - def __init__(self, checker_class): self.message = "Checker class %s was not found" % checker_class @@ -21,44 +21,55 @@ def __repr__(self): return self.message -def get_checker(linter, checker_class): +def get_checker(linter: PyLinter, checker_class): for checker in linter.get_checkers(): if isinstance(checker, checker_class): return checker raise NoSuchChecker(checker_class) -def augment_visit(linter, checker_method, augmentation): +def augment_visit(linter: PyLinter, checker_method, augmentation): """ Augmenting a visit enables additional errors to be raised (although that case is - better served using a new checker) or to suppress all warnings in certain circumstances. + better served using a new checker) or to suppress all warnings in certain + circumstances. - Augmenting functions should accept a 'chain' function, which runs the checker method - and possibly any other augmentations, and secondly an Astroid node. "chain()" can be - called at any point to trigger the continuation of other checks, or not at all to - prevent any further checking. + Augmenting functions should accept a 'chain' function, which runs the checker + method and possibly any other augmentations, and secondly an Astroid node. + "chain()" can be called at any point to trigger the continuation of other + checks, or not at all to prevent any further checking. """ - if sys.version_info[0] <= 2: - checker = get_checker(linter, checker_method.im_class) - else: - try: - checker = get_checker(linter, checker_method.__self__.__class__) - except AttributeError: - checker = get_checker(linter, get_class(checker_method.__module__, checker_method.__qualname__)) + try: + checker = get_checker(linter, checker_method.__self__.__class__) + except AttributeError: + checker = get_checker( + linter, get_class(checker_method.__module__, checker_method.__qualname__) + ) old_method = getattr(checker, checker_method.__name__) + setattr(checker, checker_method.__name__, AugmentFunc(old_method, augmentation)) + + +class AugmentFunc: + def __init__(self, old_method, augmentation_func): + self.old_method = old_method + self.augmentation_func = augmentation_func - def augment_func(node): - def chain(): - old_method(node) - augmentation(chain, node) + def __call__(self, node): + self.augmentation_func(Chain(self.old_method, node), node) - setattr(checker, checker_method.__name__, augment_func) +class Chain: + def __init__(self, old_method, node): + self.old_method = old_method + self.node = node -class Suppress(object): + def __call__(self): + self.old_method(self.node) + +class Suppress: def __init__(self, linter): self._linter = linter self._suppress = [] @@ -79,69 +90,79 @@ def suppress(self, *symbols): def __exit__(self, exc_type, exc_val, exc_tb): self._linter.add_message = self._orig_add_message for to_append_args, to_append_kwargs in self._messages_to_append: - # Depending on the Pylint version, the add_message API is different. - # Either a single object called 'message' is passed, or the first argument - # is a message symbol. - if hasattr('symbol', to_append_args[0]): - code = to_append_args[0].symbol - else: - code = to_append_args[0] if to_append_args[0] in self._suppress: continue self._linter.add_message(*to_append_args, **to_append_kwargs) -def supress_message(linter, checker_method, message_id, test_func): - import warnings - warnings.warn("'supress_message' has been deprecated in favour of the correctly-spelled 'suppress_message'", - DeprecationWarning) - return suppress_message(linter, checker_method, message_id, test_func) - - -def suppress_message(linter, checker_method, message_id_or_symbol, test_func): +def suppress_message(linter: PyLinter, checker_method, message_id_or_symbol, test_func): """ This wrapper allows the suppression of a message if the supplied test function returns True. It is useful to prevent one particular message from being raised in one particular case, while leaving the rest of the messages intact. """ - # At some point, pylint started preferring message symbols to message IDs. However this is not done - # consistently or uniformly - occasionally there are some message IDs with no matching symbols. - # We try to work around this here by suppressing both the ID and the symbol, if we can find it. - # This also gives us compatability with a broader range of pylint versions. - - # Similarly, a commit between version 1.2 and 1.3 changed where the messages are stored - see: - # https://bitbucket.org/logilab/pylint/commits/0b67f42799bed08aebb47babdc9fb0e761efc4ff#chg-reporters/__init__.py - # Therefore here, we try the new attribute name, and fall back to the old version for - # compatability with <=1.2 and >=1.3 - msgs_store = getattr(linter, 'msgs_store', linter) - - def get_message_definitions(message_id_or_symbol): - if hasattr(msgs_store, 'check_message_id'): + augment_visit( + linter, checker_method, DoSuppress(linter, message_id_or_symbol, test_func) + ) + + +class DoSuppress: + def __init__(self, linter: PyLinter, message_id_or_symbol, test_func): + self.linter = linter + self.message_id_or_symbol = message_id_or_symbol + self.test_func = test_func + + def __call__(self, chain, node): + with Suppress(self.linter) as s: + if self.test_func(node): + s.suppress(*self.symbols) + chain() + + @property + def symbols(self) -> List: + # At some point, pylint started preferring message symbols to message IDs. + # However, this is not done consistently or uniformly + # - occasionally there are some message IDs with no matching symbols. + # We try to work around this here by suppressing both the ID and the symbol. + # This also gives us compatability with a broader range of pylint versions. + + # Similarly, between version 1.2 and 1.3 changed where the messages are stored + # - see: + # https://bitbucket.org/logilab/pylint/commits/0b67f42799bed08aebb47babdc9fb0e761efc4ff#chg-reporters/__init__.py + # Therefore here, we try the new attribute name, and fall back to the old + # version for compatability with <=1.2 and >=1.3 + + try: + pylint_messages = self.get_message_definitions(self.message_id_or_symbol) + the_symbols = [ + symbol + for pylint_message in pylint_messages + for symbol in (pylint_message.msgid, pylint_message.symbol) + if symbol is not None + ] + except UnknownMessageError: + # This can happen due to mismatches of pylint versions and plugin + # expectations of available messages + the_symbols = [self.message_id_or_symbol] + + return the_symbols + + def get_message_definitions(self, message_id_or_symbol): + msgs_store = getattr(self.linter, "msgs_store", self.linter) + + if hasattr(msgs_store, "check_message_id"): return [msgs_store.check_message_id(message_id_or_symbol)] # pylint 2.0 renamed check_message_id to get_message_definition in: # https://github.com/PyCQA/pylint/commit/5ccbf9eaa54c0c302c9180bdfb745566c16e416d - elif hasattr(msgs_store, 'get_message_definition'): + elif hasattr(msgs_store, "get_message_definition"): return [msgs_store.get_message_definition(message_id_or_symbol)] # pylint 2.3.0 renamed get_message_definition to get_message_definitions in: # https://github.com/PyCQA/pylint/commit/da67a9da682e51844fbc674229ff6619eb9c816a - elif hasattr(msgs_store, 'get_message_definitions'): + elif hasattr(msgs_store, "get_message_definitions"): return msgs_store.get_message_definitions(message_id_or_symbol) else: - raise ValueError('pylint.utils.MessagesStore does not have a get_message_definition(s) method') - - try: - pylint_messages = get_message_definitions(message_id_or_symbol) - symbols = [symbol - for pylint_message in pylint_messages - for symbol in (pylint_message.msgid, pylint_message.symbol) - if symbol is not None] - except UnknownMessageError: - # This can happen due to mismatches of pylint versions and plugin expectations of available messages - symbols = [message_id_or_symbol] - - def do_suppress(chain, node): - with Suppress(linter) as s: - if test_func(node): - s.suppress(*symbols) - chain() - augment_visit(linter, checker_method, do_suppress) + msg = ( + "pylint.utils.MessagesStore does not have a " + "get_message_definition(s) method" + ) + raise ValueError(msg) diff --git a/requirements_test.txt b/requirements_test.txt new file mode 100644 index 0000000..6dae003 --- /dev/null +++ b/requirements_test.txt @@ -0,0 +1,5 @@ +-r requirements_test_pre_commit.txt +-r requirements_test_min.txt +pre-commit~=2.16 +pytest-cov~=3.0 +coveralls~=3.3 \ No newline at end of file diff --git a/requirements_test_min.txt b/requirements_test_min.txt new file mode 100644 index 0000000..4d0e9fa --- /dev/null +++ b/requirements_test_min.txt @@ -0,0 +1,2 @@ +-e . +pytest~=6.2 diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt new file mode 100644 index 0000000..93c1adf --- /dev/null +++ b/requirements_test_pre_commit.txt @@ -0,0 +1,6 @@ +# Everything in this file should reflect the pre-commit configuration +# in .pre-commit-config.yaml +black==21.12b0 +flake8==4.0.1 +flake8-typing-imports==1.12.0 +isort==5.10.1 diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..2adfc0a --- /dev/null +++ b/setup.cfg @@ -0,0 +1,9 @@ +[aliases] +test = pytest + +[tool:pytest] +testpaths = tests +python_files = *test_*.py + +[isort] +profile = black diff --git a/setup.py b/setup.py index c6d4fe3..65f3b21 100644 --- a/setup.py +++ b/setup.py @@ -7,26 +7,32 @@ _short_description = "Utilities and helpers for writing Pylint plugins" _classifiers = [ - 'Development Status :: 5 - Production/Stable', - 'Environment :: Console', - 'Intended Audience :: Developers', - 'Operating System :: Unix', - 'Topic :: Software Development :: Quality Assurance', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', + "Development Status :: 5 - Production/Stable", + "Environment :: Console", + "Intended Audience :: Developers", + "Operating System :: Unix", + "Topic :: Software Development :: Quality Assurance", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", ] setup( - name='pylint-plugin-utils', - url='https://github.com/landscapeio/pylint-plugin-utils', - author='landscape.io', - author_email='code@landscape.io', + name="pylint-plugin-utils", + url="https://github.com/PyCQA/pylint-plugin-utils", + author="Python Code Quality Authority", + author_email="code-quality@python.org", description=_short_description, version=_version, - install_requires=['pylint>=1.7'], + install_requires=["pylint>=1.7"], packages=_packages, - license='GPLv2', + license="GPLv2", classifiers=_classifiers, - keywords='pylint plugin helpers' + keywords="pylint plugin helpers", + python_requires=">=3.6.2", ) diff --git a/tests.py b/tests.py deleted file mode 100755 index 75485b5..0000000 --- a/tests.py +++ /dev/null @@ -1 +0,0 @@ -import pylint_plugin_utils diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..768e42d --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,8 @@ +import pytest +from pylint.lint import PyLinter + + +@pytest.fixture +def linter(): + _linter = PyLinter() + return _linter diff --git a/tests/test_linter_pickle.py b/tests/test_linter_pickle.py new file mode 100644 index 0000000..cce6916 --- /dev/null +++ b/tests/test_linter_pickle.py @@ -0,0 +1,25 @@ +import pickle + +from pylint.checkers.typecheck import TypeChecker + +from pylint_plugin_utils import augment_visit, suppress_message + + +def fake_augmentation_func(*args, **kwargs): + ... + + +def fake_suppress_func(*args, **kwargs): + ... + + +def test_linter_should_be_pickleable(linter): + # Setup + linter.register_checker(TypeChecker()) + augment_visit(linter, TypeChecker.visit_attribute, fake_augmentation_func) + suppress_message( + linter, TypeChecker.visit_attribute, "no-member", fake_suppress_func + ) + + # Act and Assert + pickle.dumps(linter) diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..2ef35cd --- /dev/null +++ b/tox.ini @@ -0,0 +1,32 @@ +[tox] +minversion = 2.4 +envlist = formatting, py36, py37, py38, py39, py310, pypy +skip_missing_interpreters = true +requires = pip >=21.3.1 + +[testenv:formatting] +basepython = python3 +deps = + -r {toxinidir}/requirements_test_min.txt + pre-commit~=2.16 +commands = + pre-commit run --all-files + +[testenv] +setenv = + COVERAGE_FILE = {toxinidir}/.coverage.{envname} +deps = + !pypy: -r {toxinidir}/requirements_test.txt + pypy: -r {toxinidir}/requirements_test_min.txt +commands = + pytest {toxinidir}/tests/ {posargs:} + +[testenv:coverage-html] +setenv = + COVERAGE_FILE = {toxinidir}/.coverage +deps = + -r {toxinidir}/requirements_test.txt +skip_install = true +commands = + coverage combine + coverage html --ignore-errors --rcfile={toxinidir}/.coveragerc