Skip to content

Commit 3a4ea4e

Browse files
committed
20 - Replaced closure+function to keep state with classes implementing __call__
- Added initial version for setup.cfg - Added initial documentation for testing
1 parent db90306 commit 3a4ea4e

12 files changed

Lines changed: 187 additions & 60 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ pip-log.txt
2525
.coverage
2626
.tox
2727
nosetests.xml
28+
.pytest_cache
2829

2930
# Translations
3031
*.mo
@@ -33,3 +34,5 @@ nosetests.xml
3334
.mr.developer.cfg
3435
.project
3536
.pydevproject
37+
.pylint-plugin-utils
38+
.idea

.travis.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ python:
44
- "3.5"
55
- "3.6"
66
- "3.7"
7+
- "3.8"
78
env:
89
- PYLINT=2.0.0
910
- PYLINT=2.1.1

README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,34 @@
1010

1111
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).
1212

13+
# Testing
14+
We use [tox](https://tox.readthedocs.io/en/latest/) and [pytest-benchmark](https://pytest-benchmark.readthedocs.io/en/latest/index.html) for running the test suite. You should be able to install it with:
15+
```bash
16+
pip install tox pytest pytest-benchmark
17+
```
18+
19+
To run the test suite for a particular Python version, you can do:
20+
```bash
21+
tox -e py37
22+
```
23+
24+
To run individual tests with ``tox``, you can do::
25+
```bash
26+
tox -e py37 -- -k name_of_the_test
27+
```
28+
29+
We use pytest_ for testing ``pylint``, which you can use without using ``tox`` for a faster development cycle.
30+
31+
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::
32+
```bash
33+
pip install pytest-cov
34+
# Everything:
35+
python3 -m pytest tests/ --cov=pylint_plugin_utils
36+
coverage html
37+
```
38+
39+
40+
1341
# License
1442

1543
`pylint-plugin-utils` is available under the GPLv2 License.

pylint_plugin_utils/__init__.py

Lines changed: 69 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import sys
1+
from typing import List
22

33
from pylint.exceptions import UnknownMessageError
4+
from pylint.lint import PyLinter
45

56

67
def get_class(module_name, kls):
@@ -21,14 +22,14 @@ def __repr__(self):
2122
return self.message
2223

2324

24-
def get_checker(linter, checker_class):
25+
def get_checker(linter: PyLinter, checker_class):
2526
for checker in linter.get_checkers():
2627
if isinstance(checker, checker_class):
2728
return checker
2829
raise NoSuchChecker(checker_class)
2930

3031

31-
def augment_visit(linter, checker_method, augmentation):
32+
def augment_visit(linter: PyLinter, checker_method, augmentation):
3233
"""
3334
Augmenting a visit enables additional errors to be raised (although that case is
3435
better served using a new checker) or to suppress all warnings in certain circumstances.
@@ -39,25 +40,34 @@ def augment_visit(linter, checker_method, augmentation):
3940
prevent any further checking.
4041
"""
4142

42-
if sys.version_info[0] <= 2:
43-
checker = get_checker(linter, checker_method.im_class)
44-
else:
45-
try:
46-
checker = get_checker(linter, checker_method.__self__.__class__)
47-
except AttributeError:
48-
checker = get_checker(linter, get_class(checker_method.__module__, checker_method.__qualname__))
43+
try:
44+
checker = get_checker(linter, checker_method.__self__.__class__)
45+
except AttributeError:
46+
checker = get_checker(linter, get_class(checker_method.__module__, checker_method.__qualname__))
4947

5048
old_method = getattr(checker, checker_method.__name__)
49+
setattr(checker, checker_method.__name__, AugmentFunc(old_method, augmentation))
50+
51+
52+
class AugmentFunc:
53+
def __init__(self, old_method, augmentation_func):
54+
self.old_method = old_method
55+
self.augmentation_func = augmentation_func
56+
57+
def __call__(self, node):
58+
self.augmentation_func(Chain(self.old_method, node), node)
59+
5160

52-
def augment_func(node):
53-
def chain():
54-
old_method(node)
55-
augmentation(chain, node)
61+
class Chain:
62+
def __init__(self, old_method, node):
63+
self.old_method = old_method
64+
self.node = node
5665

57-
setattr(checker, checker_method.__name__, augment_func)
66+
def __call__(self):
67+
self.old_method(self.node)
5868

5969

60-
class Suppress(object):
70+
class Suppress:
6171

6272
def __init__(self, linter):
6373
self._linter = linter
@@ -91,31 +101,54 @@ def __exit__(self, exc_type, exc_val, exc_tb):
91101
self._linter.add_message(*to_append_args, **to_append_kwargs)
92102

93103

94-
def supress_message(linter, checker_method, message_id, test_func):
95-
import warnings
96-
warnings.warn("'supress_message' has been deprecated in favour of the correctly-spelled 'suppress_message'",
97-
DeprecationWarning)
98-
return suppress_message(linter, checker_method, message_id, test_func)
99-
100-
101-
def suppress_message(linter, checker_method, message_id_or_symbol, test_func):
104+
def suppress_message(linter: PyLinter, checker_method, message_id_or_symbol, test_func):
102105
"""
103106
This wrapper allows the suppression of a message if the supplied test function
104107
returns True. It is useful to prevent one particular message from being raised
105108
in one particular case, while leaving the rest of the messages intact.
106109
"""
107-
# At some point, pylint started preferring message symbols to message IDs. However this is not done
108-
# consistently or uniformly - occasionally there are some message IDs with no matching symbols.
109-
# We try to work around this here by suppressing both the ID and the symbol, if we can find it.
110-
# This also gives us compatability with a broader range of pylint versions.
111-
112-
# Similarly, a commit between version 1.2 and 1.3 changed where the messages are stored - see:
113-
# https://bitbucket.org/logilab/pylint/commits/0b67f42799bed08aebb47babdc9fb0e761efc4ff#chg-reporters/__init__.py
114-
# Therefore here, we try the new attribute name, and fall back to the old version for
115-
# compatability with <=1.2 and >=1.3
116-
msgs_store = getattr(linter, 'msgs_store', linter)
117-
118-
def get_message_definitions(message_id_or_symbol):
110+
augment_visit(linter, checker_method, DoSuppress(linter, message_id_or_symbol, test_func))
111+
112+
113+
class DoSuppress:
114+
def __init__(self, linter: PyLinter, message_id_or_symbol, test_func):
115+
self.linter = linter
116+
self.message_id_or_symbol = message_id_or_symbol
117+
self.test_func = test_func
118+
119+
def __call__(self, chain, node):
120+
with Suppress(self.linter) as s:
121+
if self.test_func(node):
122+
s.suppress(*self.symbols)
123+
chain()
124+
125+
@property
126+
def symbols(self) -> List:
127+
# At some point, pylint started preferring message symbols to message IDs. However this is not done
128+
# consistently or uniformly - occasionally there are some message IDs with no matching symbols.
129+
# We try to work around this here by suppressing both the ID and the symbol, if we can find it.
130+
# This also gives us compatability with a broader range of pylint versions.
131+
132+
# Similarly, a commit between version 1.2 and 1.3 changed where the messages are stored - see:
133+
# https://bitbucket.org/logilab/pylint/commits/0b67f42799bed08aebb47babdc9fb0e761efc4ff#chg-reporters/__init__.py
134+
# Therefore here, we try the new attribute name, and fall back to the old version for
135+
# compatability with <=1.2 and >=1.3
136+
137+
try:
138+
pylint_messages = self.get_message_definitions(self.message_id_or_symbol)
139+
the_symbols = [symbol
140+
for pylint_message in pylint_messages
141+
for symbol in (pylint_message.msgid, pylint_message.symbol)
142+
if symbol is not None]
143+
except UnknownMessageError:
144+
# This can happen due to mismatches of pylint versions and plugin expectations of available messages
145+
the_symbols = [self.message_id_or_symbol]
146+
147+
return the_symbols
148+
149+
def get_message_definitions(self, message_id_or_symbol):
150+
msgs_store = getattr(self.linter, 'msgs_store', self.linter)
151+
119152
if hasattr(msgs_store, 'check_message_id'):
120153
return [msgs_store.check_message_id(message_id_or_symbol)]
121154
# pylint 2.0 renamed check_message_id to get_message_definition in:
@@ -128,20 +161,3 @@ def get_message_definitions(message_id_or_symbol):
128161
return msgs_store.get_message_definitions(message_id_or_symbol)
129162
else:
130163
raise ValueError('pylint.utils.MessagesStore does not have a get_message_definition(s) method')
131-
132-
try:
133-
pylint_messages = get_message_definitions(message_id_or_symbol)
134-
symbols = [symbol
135-
for pylint_message in pylint_messages
136-
for symbol in (pylint_message.msgid, pylint_message.symbol)
137-
if symbol is not None]
138-
except UnknownMessageError:
139-
# This can happen due to mismatches of pylint versions and plugin expectations of available messages
140-
symbols = [message_id_or_symbol]
141-
142-
def do_suppress(chain, node):
143-
with Suppress(linter) as s:
144-
if test_func(node):
145-
s.suppress(*symbols)
146-
chain()
147-
augment_visit(linter, checker_method, do_suppress)

setup.cfg

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
[aliases]
2+
test = pytest
3+
4+
[tool:pytest]
5+
testpaths = tests
6+
python_files = *test_*.py
7+
8+
[isort]
9+
profile = black

setup.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,21 +12,27 @@
1212
'Intended Audience :: Developers',
1313
'Operating System :: Unix',
1414
'Topic :: Software Development :: Quality Assurance',
15-
'Programming Language :: Python :: 3.5',
15+
'Programming Language :: Python',
16+
'Programming Language :: Python :: 3'
17+
'Programming Language :: Python :: 3 :: Only',
1618
'Programming Language :: Python :: 3.6',
1719
'Programming Language :: Python :: 3.7',
20+
'Programming Language :: Python :: 3.8',
21+
'Programming Language :: Python :: 3.9',
22+
'Programming Language :: Python :: 3.10',
1823
]
1924

2025
setup(
2126
name='pylint-plugin-utils',
22-
url='https://github.com/landscapeio/pylint-plugin-utils',
23-
author='landscape.io',
24-
author_email='code@landscape.io',
27+
url='https://github.com/PyCQA/pylint-plugin-utils',
28+
author='Python Code Quality Authority',
29+
author_email='code[email protected]',
2530
description=_short_description,
2631
version=_version,
27-
install_requires=['pylint>=1.7'],
32+
install_requires=['pylint>=1.7', 'pytest'],
2833
packages=_packages,
2934
license='GPLv2',
3035
classifiers=_classifiers,
31-
keywords='pylint plugin helpers'
36+
keywords='pylint plugin helpers',
37+
python_requires='>=3.6.2',
3238
)

tests.py

Lines changed: 0 additions & 1 deletion
This file was deleted.

tests/__init__.py

Whitespace-only changes.

tests/conftest.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# pylint: disable=redefined-outer-name
2+
import os
3+
from pathlib import Path
4+
5+
import pytest
6+
7+
from pylint import checkers
8+
from pylint.lint import PyLinter
9+
from pylint.testutils import MinimalTestReporter
10+
11+
12+
@pytest.fixture()
13+
def tests_directory():
14+
return Path(__file__).parent
15+
16+
17+
@pytest.fixture
18+
def linter():
19+
_linter = PyLinter()
20+
return _linter
21+

tests/input/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)