summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.pre-commit-config.yaml10
-rw-r--r--CONTRIBUTORS.txt4
-rw-r--r--ChangeLog12
-rwxr-xr-xbin/pylint1
-rw-r--r--pylint/checkers/async.py8
-rw-r--r--pylint/checkers/base.py9
-rw-r--r--pylint/checkers/stdlib.py1
-rw-r--r--pylint/checkers/utils.py5
-rw-r--r--pylint/testutils.py640
-rw-r--r--pylint/testutils/__init__.py54
-rw-r--r--pylint/testutils/checker_test_case.py50
-rw-r--r--pylint/testutils/constants.py26
-rw-r--r--pylint/testutils/decorator.py24
-rw-r--r--pylint/testutils/functional_test_file.py73
-rw-r--r--pylint/testutils/get_test_info.py45
-rw-r--r--pylint/testutils/global_test_linter.py20
-rw-r--r--pylint/testutils/lint_module_test.py218
-rw-r--r--pylint/testutils/output_line.py68
-rw-r--r--pylint/testutils/reporter_for_tests.py80
-rw-r--r--pylint/testutils/tokenize_str.py9
-rw-r--r--pylint/testutils/unittest_linter.py40
-rw-r--r--pytest.ini3
-rw-r--r--setup.py13
-rw-r--r--tests/benchmark/test_baseline_benchmarks.py2
-rw-r--r--tests/functional/a/assignment_from_no_return_py3.txt0
-rw-r--r--tests/functional/b/bad_reversed_sequence.py1
-rw-r--r--tests/functional/b/bad_reversed_sequence.txt18
-rw-r--r--tests/functional/b/bad_reversed_sequence_py37.py2
-rw-r--r--tests/functional/b/bad_reversed_sequence_py37.rc2
-rw-r--r--tests/functional/b/bad_reversed_sequence_py37.txt1
-rw-r--r--tests/functional/b/bad_reversed_sequence_py38.py2
-rw-r--r--tests/functional/b/bad_reversed_sequence_py38.rc2
-rw-r--r--tests/functional/c/crash_missing_module_type.txt0
-rw-r--r--tests/functional/f/fallback_import_disabled.txt0
-rw-r--r--tests/functional/f/formatting.txt0
-rw-r--r--tests/functional/g/genexp_in_class_scope.txt0
-rw-r--r--tests/functional/i/implicit_str_concat_latin1.txt0
-rw-r--r--tests/functional/i/implicit_str_concat_utf8.txt0
-rw-r--r--tests/functional/i/invalid_metaclass.txt0
-rw-r--r--tests/functional/i/invalid_metaclass_py3.txt0
-rw-r--r--tests/functional/l/long_utf8_lines.txt0
-rw-r--r--tests/functional/m/missing_module_docstring_disabled.txt0
-rw-r--r--tests/functional/m/missing_self_argument.txt3
-rw-r--r--tests/functional/m/monkeypatch_method.txt0
-rw-r--r--tests/functional/n/no_self_use_py3.txt0
-rw-r--r--tests/functional/n/not_async_context_manager_py37.py11
-rw-r--r--tests/functional/n/not_async_context_manager_py37.txt0
-rw-r--r--tests/functional/p/postponed_evaluation_activated.txt0
-rw-r--r--tests/functional/r/raising_self.txt0
-rw-r--r--tests/functional/r/recursion_error_2667.txt0
-rw-r--r--tests/functional/r/recursion_error_crash.txt0
-rw-r--r--tests/functional/r/recursion_error_crash_2683.txt0
-rw-r--r--tests/functional/r/recursion_error_crash_astroid_623.txt0
-rw-r--r--tests/functional/r/regression_no_value_for_parameter.txt0
-rw-r--r--tests/functional/s/statement_without_effect.txt48
-rw-r--r--tests/functional/too/too_few_public_methods_37.txt0
-rw-r--r--tests/functional/too/too_many_arguments_issue_1045.txt0
-rw-r--r--tests/functional/u/ungrouped_imports_isort_compatible.txt0
-rw-r--r--tests/functional/u/unused_variable_py36.txt0
-rw-r--r--tests/functional/w/wrong_import_position_exclude_dunder_main.txt0
-rw-r--r--tests/lint/unittest_lint.py14
-rw-r--r--tests/profile/test_profile_against_externals.py4
-rw-r--r--tests/test_check_parallel.py2
-rw-r--r--tests/test_func.py43
-rw-r--r--tests/test_functional.py20
-rw-r--r--tests/test_import_graph.py2
-rw-r--r--tests/test_regr.py2
-rw-r--r--tox.ini16
68 files changed, 850 insertions, 758 deletions
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 6c1c3e291..50cbe6988 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -14,7 +14,9 @@ repos:
rev: v2.1.0
hooks:
- id: trailing-whitespace
+ exclude: "tests/functional/t/trailing_whitespaces.py"
- id: end-of-file-fixer
+ exclude: "tests/functional/m/missing_final_newline.py|tests/functional/t/trailing_newlines.py"
- repo: https://github.com/PyCQA/isort
rev: 5.5.2
hooks:
@@ -25,3 +27,11 @@ repos:
- id: black
args: [--safe, --quiet]
exclude: *fixtures
+- repo: local
+ hooks:
+ - id: pylint
+ name: pylint
+ entry: pylint
+ language: system
+ types: [python]
+ exclude: tests/functional/|tests/input|tests/extensions/data|tests/regrtest_data/|tests/data/|doc/
diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt
index 2c0e6aa99..19f4842c1 100644
--- a/CONTRIBUTORS.txt
+++ b/CONTRIBUTORS.txt
@@ -431,3 +431,7 @@ contributors:
* Julien Palard: contributor
* Raphael Gaschignard: contributor
+
+* Sorin Sbarnea: contributor
+
+* Batuhan Taskaya: contributor
diff --git a/ChangeLog b/ChangeLog
index a5735df0b..97ebf5f55 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,6 +1,9 @@
------------------
Pylint's ChangeLog
------------------
+* Only emit `bad-reversed-sequence` on dictionaries if below py3.8
+
+ Closes #3940
* Handle class decorators applied to function.
@@ -16,11 +19,19 @@ Pylint's ChangeLog
* Add missing checks for deprecated functions.
+* Postponed evaluation of annotations are now recognized by default if python version is above 3.10
+
+ Closes #3992
+
What's New in Pylint 2.6.1?
===========================
Release date: TBA
+* Fix false positive for `not-async-context-manager` when `contextlib.asynccontextmanager` is used
+
+ Close #3862
+
* Fix linter multiprocessing pool shutdown (triggered warnings when runned in parallels with other pytest plugins)
Closes #3779
@@ -78,6 +89,7 @@ Release date: TBA
* Improve the performance of the line length check.
+* Removed incorrect deprecation of ``inspect.getfullargspec``
What's New in Pylint 2.6.0?
===========================
diff --git a/bin/pylint b/bin/pylint
index 162e96d40..f2bbcc2ad 100755
--- a/bin/pylint
+++ b/bin/pylint
@@ -1,4 +1,5 @@
#!/usr/bin/env python
+# pylint: disable=import-self
from pylint import run_pylint
run_pylint()
diff --git a/pylint/checkers/async.py b/pylint/checkers/async.py
index 1b581c0f1..420c0e211 100644
--- a/pylint/checkers/async.py
+++ b/pylint/checkers/async.py
@@ -58,7 +58,12 @@ class AsyncChecker(checkers.BaseChecker):
if inferred is None or inferred is astroid.Uninferable:
continue
- if isinstance(inferred, bases.AsyncGenerator):
+ if isinstance(inferred, astroid.AsyncFunctionDef):
+ # Check if we are dealing with a function decorated
+ # with contextlib.asynccontextmanager.
+ if decorated_with(inferred, self._async_generators):
+ continue
+ elif isinstance(inferred, bases.AsyncGenerator):
# Check if we are dealing with a function decorated
# with contextlib.asynccontextmanager.
if decorated_with(inferred.parent, self._async_generators):
@@ -79,7 +84,6 @@ class AsyncChecker(checkers.BaseChecker):
continue
else:
continue
-
self.add_message(
"not-async-context-manager", node=node, args=(inferred.name,)
)
diff --git a/pylint/checkers/base.py b/pylint/checkers/base.py
index 791bca53f..56aca1e71 100644
--- a/pylint/checkers/base.py
+++ b/pylint/checkers/base.py
@@ -1524,14 +1524,11 @@ class BasicChecker(_BasicChecker):
return
if isinstance(argument, astroid.Instance):
- if argument._proxied.name == "dict" and utils.is_builtin_object(
- argument._proxied
- ):
- self.add_message("bad-reversed-sequence", node=node)
- return
if any(
ancestor.name == "dict" and utils.is_builtin_object(ancestor)
- for ancestor in argument._proxied.ancestors()
+ for ancestor in itertools.chain(
+ (argument._proxied,), argument._proxied.ancestors()
+ )
):
# Mappings aren't accepted by reversed(), unless
# they provide explicitly a __reversed__ method.
diff --git a/pylint/checkers/stdlib.py b/pylint/checkers/stdlib.py
index a0d13524c..270623f09 100644
--- a/pylint/checkers/stdlib.py
+++ b/pylint/checkers/stdlib.py
@@ -245,7 +245,6 @@ class StdlibChecker(BaseChecker):
"fractions.gcd",
"inspect.formatargspec",
"inspect.getcallargs",
- "inspect.getfullargspec",
"platform.linux_distribution",
"platform.dist",
},
diff --git a/pylint/checkers/utils.py b/pylint/checkers/utils.py
index 23836f909..6c707e10f 100644
--- a/pylint/checkers/utils.py
+++ b/pylint/checkers/utils.py
@@ -50,6 +50,7 @@ import itertools
import numbers
import re
import string
+import sys
from functools import lru_cache, partial
from typing import Callable, Dict, Iterable, List, Match, Optional, Set, Tuple, Union
@@ -212,6 +213,7 @@ SPECIAL_METHODS_PARAMS = {
for name in methods # type: ignore
}
PYMETHODS = set(SPECIAL_METHODS_PARAMS)
+PY310_PLUS = sys.version_info[:2] >= (3, 10)
class NoSuchArgumentError(Exception):
@@ -1264,6 +1266,9 @@ def get_node_last_lineno(node: astroid.node_classes.NodeNG) -> int:
def is_postponed_evaluation_enabled(node: astroid.node_classes.NodeNG) -> bool:
"""Check if the postponed evaluation of annotations is enabled"""
+ if PY310_PLUS:
+ return True
+
module = node.root()
return "annotations" in module.future_imports
diff --git a/pylint/testutils.py b/pylint/testutils.py
deleted file mode 100644
index 43bd94447..000000000
--- a/pylint/testutils.py
+++ /dev/null
@@ -1,640 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2012-2014 LOGILAB S.A. (Paris, FRANCE) <contact@logilab.fr>
-# Copyright (c) 2012 FELD Boris <lothiraldan@gmail.com>
-# Copyright (c) 2013-2018, 2020 Claudiu Popa <pcmanticore@gmail.com>
-# Copyright (c) 2013-2014 Google, Inc.
-# Copyright (c) 2013 buck@yelp.com <buck@yelp.com>
-# Copyright (c) 2014 LCD 47 <lcd047@gmail.com>
-# Copyright (c) 2014 Brett Cannon <brett@python.org>
-# Copyright (c) 2014 Ricardo Gemignani <ricardo.gemignani@gmail.com>
-# Copyright (c) 2014 Arun Persaud <arun@nubati.net>
-# Copyright (c) 2015 Pavel Roskin <proski@gnu.org>
-# Copyright (c) 2015 Ionel Cristian Maries <contact@ionelmc.ro>
-# Copyright (c) 2016 Derek Gustafson <degustaf@gmail.com>
-# Copyright (c) 2016 Roy Williams <roy.williams.iii@gmail.com>
-# Copyright (c) 2016 xmo-odoo <xmo-odoo@users.noreply.github.com>
-# Copyright (c) 2017 Bryce Guinta <bryce.paul.guinta@gmail.com>
-# Copyright (c) 2018 ssolanki <sushobhitsolanki@gmail.com>
-# Copyright (c) 2018 Sushobhit <31987769+sushobhit27@users.noreply.github.com>
-# Copyright (c) 2019 Mr. Senko <atodorov@mrsenko.com>
-# Copyright (c) 2019 Hugo van Kemenade <hugovk@users.noreply.github.com>
-# Copyright (c) 2019 Pierre Sassoulas <pierre.sassoulas@gmail.com>
-# Copyright (c) 2020 谭九鼎 <109224573@qq.com>
-# Copyright (c) 2020 Anthony Sottile <asottile@umich.edu>
-# Copyright (c) 2020 Guillaume Peillex <guillaume.peillex@gmail.com>
-
-# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
-# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
-
-"""functional/non regression tests for pylint"""
-import collections
-import configparser
-import contextlib
-import csv
-import functools
-import itertools
-import operator
-import platform
-import re
-import sys
-import tempfile
-import tokenize
-from glob import glob
-from io import StringIO
-from os import close, getcwd, linesep, remove, sep, write
-from os.path import abspath, basename, dirname, exists, join, splitext
-
-import astroid
-import pytest
-
-from pylint import checkers, interfaces
-from pylint.lint import PyLinter
-from pylint.reporters import BaseReporter
-from pylint.utils import ASTWalker
-
-# Utils
-
-SYS_VERS_STR = "%d%d%d" % sys.version_info[:3]
-TITLE_UNDERLINES = ["", "=", "-", "."]
-PREFIX = abspath(dirname(__file__))
-UPDATE_OPTION = "--update-functional-output"
-
-
-def _get_tests_info(input_dir, msg_dir, prefix, suffix):
- """get python input examples and output messages
-
- We use following conventions for input files and messages:
- for different inputs:
- test for python >= x.y -> input = <name>_pyxy.py
- test for python < x.y -> input = <name>_py_xy.py
- for one input and different messages:
- message for python >= x.y -> message = <name>_pyxy.txt
- lower versions -> message with highest num
- """
- result = []
- for fname in glob(join(input_dir, prefix + "*" + suffix)):
- infile = basename(fname)
- fbase = splitext(infile)[0]
- # filter input files :
- pyrestr = fbase.rsplit("_py", 1)[-1] # like _26 or 26
- if pyrestr.isdigit(): # '24', '25'...
- if SYS_VERS_STR < pyrestr:
- continue
- if pyrestr.startswith("_") and pyrestr[1:].isdigit():
- # skip test for higher python versions
- if SYS_VERS_STR >= pyrestr[1:]:
- continue
- messages = glob(join(msg_dir, fbase + "*.txt"))
- # the last one will be without ext, i.e. for all or upper versions:
- if messages:
- for outfile in sorted(messages, reverse=True):
- py_rest = outfile.rsplit("_py", 1)[-1][:-4]
- if py_rest.isdigit() and SYS_VERS_STR >= py_rest:
- break
- else:
- # This will provide an error message indicating the missing filename.
- outfile = join(msg_dir, fbase + ".txt")
- result.append((infile, outfile))
- return result
-
-
-class TestReporter(BaseReporter):
- """reporter storing plain text messages"""
-
- __implements__ = interfaces.IReporter
-
- def __init__(self): # pylint: disable=super-init-not-called
-
- self.message_ids = {}
- self.reset()
- self.path_strip_prefix = getcwd() + sep
-
- def reset(self):
- self.out = StringIO()
- self.messages = []
-
- def handle_message(self, msg):
- """manage message of different type and in the context of path """
- obj = msg.obj
- line = msg.line
- msg_id = msg.msg_id
- msg = msg.msg
- self.message_ids[msg_id] = 1
- if obj:
- obj = ":%s" % obj
- sigle = msg_id[0]
- if linesep != "\n":
- # 2to3 writes os.linesep instead of using
- # the previosly used line separators
- msg = msg.replace("\r\n", "\n")
- self.messages.append("%s:%3s%s: %s" % (sigle, line, obj, msg))
-
- def finalize(self):
- self.messages.sort()
- for msg in self.messages:
- print(msg, file=self.out)
- result = self.out.getvalue()
- self.reset()
- return result
-
- # pylint: disable=unused-argument
- def on_set_current_module(self, module, filepath):
- pass
-
- # pylint: enable=unused-argument
-
- def display_reports(self, layout):
- """ignore layouts"""
-
- _display = None
-
-
-class MinimalTestReporter(BaseReporter):
- def handle_message(self, msg):
- self.messages.append(msg)
-
- def on_set_current_module(self, module, filepath):
- self.messages = []
-
- _display = None
-
-
-class Message(
- collections.namedtuple("Message", ["msg_id", "line", "node", "args", "confidence"])
-):
- def __new__(cls, msg_id, line=None, node=None, args=None, confidence=None):
- return tuple.__new__(cls, (msg_id, line, node, args, confidence))
-
- def __eq__(self, other):
- if isinstance(other, Message):
- if self.confidence and other.confidence:
- return super().__eq__(other)
- return self[:-1] == other[:-1]
- return NotImplemented # pragma: no cover
-
- __hash__ = None
-
-
-class UnittestLinter:
- """A fake linter class to capture checker messages."""
-
- # pylint: disable=unused-argument, no-self-use
-
- def __init__(self):
- self._messages = []
- self.stats = {}
-
- def release_messages(self):
- try:
- return self._messages
- finally:
- self._messages = []
-
- def add_message(
- self, msg_id, line=None, node=None, args=None, confidence=None, col_offset=None
- ):
- # Do not test col_offset for now since changing Message breaks everything
- self._messages.append(Message(msg_id, line, node, args, confidence))
-
- def is_message_enabled(self, *unused_args, **unused_kwargs):
- return True
-
- def add_stats(self, **kwargs):
- for name, value in kwargs.items():
- self.stats[name] = value
- return self.stats
-
- @property
- def options_providers(self):
- return linter.options_providers
-
-
-def set_config(**kwargs):
- """Decorator for setting config values on a checker."""
-
- def _wrapper(fun):
- @functools.wraps(fun)
- def _forward(self):
- for key, value in kwargs.items():
- setattr(self.checker.config, key, value)
- if isinstance(self, CheckerTestCase):
- # reopen checker in case, it may be interested in configuration change
- self.checker.open()
- fun(self)
-
- return _forward
-
- return _wrapper
-
-
-class CheckerTestCase:
- """A base testcase class for unit testing individual checker classes."""
-
- CHECKER_CLASS = None
- CONFIG = {}
-
- def setup_method(self):
- self.linter = UnittestLinter()
- self.checker = self.CHECKER_CLASS(self.linter) # pylint: disable=not-callable
- for key, value in self.CONFIG.items():
- setattr(self.checker.config, key, value)
- self.checker.open()
-
- @contextlib.contextmanager
- def assertNoMessages(self):
- """Assert that no messages are added by the given method."""
- with self.assertAddsMessages():
- yield
-
- @contextlib.contextmanager
- def assertAddsMessages(self, *messages):
- """Assert that exactly the given method adds the given messages.
-
- The list of messages must exactly match *all* the messages added by the
- method. Additionally, we check to see whether the args in each message can
- actually be substituted into the message string.
- """
- yield
- got = self.linter.release_messages()
- msg = "Expected messages did not match actual.\n" "Expected:\n%s\nGot:\n%s" % (
- "\n".join(repr(m) for m in messages),
- "\n".join(repr(m) for m in got),
- )
- assert list(messages) == got, msg
-
- def walk(self, node):
- """recursive walk on the given node"""
- walker = ASTWalker(linter)
- walker.add_checker(self.checker)
- walker.walk(node)
-
-
-# Init
-test_reporter = TestReporter()
-linter = PyLinter()
-linter.set_reporter(test_reporter)
-linter.config.persistent = 0
-checkers.initialize(linter)
-
-
-def _tokenize_str(code):
- return list(tokenize.generate_tokens(StringIO(code).readline))
-
-
-@contextlib.contextmanager
-def _create_tempfile(content=None):
- """Create a new temporary file.
-
- If *content* parameter is given, then it will be written
- in the temporary file, before passing it back.
- This is a context manager and should be used with a *with* statement.
- """
- # Can't use tempfile.NamedTemporaryFile here
- # because on Windows the file must be closed before writing to it,
- # see https://bugs.python.org/issue14243
- file_handle, tmp = tempfile.mkstemp()
- if content:
- # erff
- write(file_handle, bytes(content, "ascii"))
- try:
- yield tmp
- finally:
- close(file_handle)
- remove(tmp)
-
-
-@contextlib.contextmanager
-def _create_file_backed_module(code):
- """Create an astroid module for the given code, backed by a real file."""
- with _create_tempfile() as temp:
- module = astroid.parse(code)
- module.file = temp
- yield module
-
-
-class NoFileError(Exception):
- pass
-
-
-class OutputLine(
- collections.namedtuple(
- "OutputLine", ["symbol", "lineno", "object", "msg", "confidence"]
- )
-):
- @classmethod
- def from_msg(cls, msg):
- return cls(
- msg.symbol,
- msg.line,
- msg.obj or "",
- msg.msg.replace("\r\n", "\n"),
- msg.confidence.name
- if msg.confidence != interfaces.UNDEFINED
- else interfaces.HIGH.name,
- )
-
- @classmethod
- def from_csv(cls, row):
- confidence = row[4] if len(row) == 5 else interfaces.HIGH.name
- return cls(row[0], int(row[1]), row[2], row[3], confidence)
-
- def to_csv(self):
- if self.confidence == interfaces.HIGH.name:
- return self[:-1]
-
- return self
-
-
-# Common sub-expressions.
-_MESSAGE = {"msg": r"[a-z][a-z\-]+"}
-# Matches a #,
-# - followed by a comparison operator and a Python version (optional),
-# - followed by a line number with a +/- (optional),
-# - followed by a list of bracketed message symbols.
-# Used to extract expected messages from testdata files.
-_EXPECTED_RE = re.compile(
- r"\s*#\s*(?:(?P<line>[+-]?[0-9]+):)?"
- r"(?:(?P<op>[><=]+) *(?P<version>[0-9.]+):)?"
- r"\s*\[(?P<msgs>%(msg)s(?:,\s*%(msg)s)*)\]" % _MESSAGE
-)
-
-
-def parse_python_version(ver_str):
- return tuple(int(digit) for digit in ver_str.split("."))
-
-
-class FunctionalTestReporter(BaseReporter): # pylint: disable=abstract-method
- def handle_message(self, msg):
- self.messages.append(msg)
-
- def on_set_current_module(self, module, filepath):
- self.messages = []
-
- def display_reports(self, layout):
- """Ignore layouts and don't call self._display()."""
-
-
-class FunctionalTestFile:
- """A single functional test case file with options."""
-
- _CONVERTERS = {
- "min_pyver": parse_python_version,
- "max_pyver": parse_python_version,
- "requires": lambda s: s.split(","),
- }
-
- def __init__(self, directory, filename):
- self._directory = directory
- self.base = filename.replace(".py", "")
- self.options = {
- "min_pyver": (2, 5),
- "max_pyver": (4, 0),
- "requires": [],
- "except_implementations": [],
- "exclude_platforms": [],
- }
- self._parse_options()
-
- def __repr__(self):
- return "FunctionalTest:{}".format(self.base)
-
- def _parse_options(self):
- cp = configparser.ConfigParser()
- cp.add_section("testoptions")
- try:
- cp.read(self.option_file)
- except NoFileError:
- pass
-
- for name, value in cp.items("testoptions"):
- conv = self._CONVERTERS.get(name, lambda v: v)
- self.options[name] = conv(value)
-
- @property
- def option_file(self):
- return self._file_type(".rc")
-
- @property
- def module(self):
- package = basename(self._directory)
- return ".".join([package, self.base])
-
- @property
- def expected_output(self):
- return self._file_type(".txt", check_exists=False)
-
- @property
- def source(self):
- return self._file_type(".py")
-
- def _file_type(self, ext, check_exists=True):
- name = join(self._directory, self.base + ext)
- if not check_exists or exists(name):
- return name
- raise NoFileError("Cannot find '{}'.".format(name))
-
-
-_OPERATORS = {">": operator.gt, "<": operator.lt, ">=": operator.ge, "<=": operator.le}
-
-
-def parse_expected_output(stream):
- return [OutputLine.from_csv(row) for row in csv.reader(stream, "test")]
-
-
-def get_expected_messages(stream):
- """Parses a file and get expected messages.
-
- :param stream: File-like input stream.
- :type stream: enumerable
- :returns: A dict mapping line,msg-symbol tuples to the count on this line.
- :rtype: dict
- """
- messages = collections.Counter()
- for i, line in enumerate(stream):
- match = _EXPECTED_RE.search(line)
- if match is None:
- continue
- line = match.group("line")
- if line is None:
- line = i + 1
- elif line.startswith("+") or line.startswith("-"):
- line = i + 1 + int(line)
- else:
- line = int(line)
-
- version = match.group("version")
- op = match.group("op")
- if version:
- required = parse_python_version(version)
- if not _OPERATORS[op](sys.version_info, required):
- continue
-
- for msg_id in match.group("msgs").split(","):
- messages[line, msg_id.strip()] += 1
- return messages
-
-
-def multiset_difference(left_op, right_op):
- """Takes two multisets and compares them.
-
- A multiset is a dict with the cardinality of the key as the value.
-
- :param left_op: The expected entries.
- :type left_op: set
- :param right_op: Actual entries.
- :type right_op: set
-
- :returns: The two multisets of missing and unexpected messages.
- :rtype: tuple
- """
- missing = left_op.copy()
- missing.subtract(right_op)
- unexpected = {}
- for key, value in list(missing.items()):
- if value <= 0:
- missing.pop(key)
- if value < 0:
- unexpected[key] = -value
- return missing, unexpected
-
-
-class LintModuleTest:
- maxDiff = None
-
- def __init__(self, test_file):
- _test_reporter = FunctionalTestReporter()
- self._linter = PyLinter()
- self._linter.set_reporter(_test_reporter)
- self._linter.config.persistent = 0
- checkers.initialize(self._linter)
- self._linter.disable("I")
- try:
- self._linter.read_config_file(test_file.option_file)
- self._linter.load_config_file()
- except NoFileError:
- pass
- self._test_file = test_file
-
- def setUp(self):
- if self._should_be_skipped_due_to_version():
- pytest.skip(
- "Test cannot run with Python %s." % (sys.version.split(" ")[0],)
- )
- missing = []
- for req in self._test_file.options["requires"]:
- try:
- __import__(req)
- except ImportError:
- missing.append(req)
- if missing:
- pytest.skip("Requires %s to be present." % (",".join(missing),))
- if self._test_file.options["except_implementations"]:
- implementations = [
- item.strip()
- for item in self._test_file.options["except_implementations"].split(",")
- ]
- implementation = platform.python_implementation()
- if implementation in implementations:
- pytest.skip(
- "Test cannot run with Python implementation %r" % (implementation,)
- )
- if self._test_file.options["exclude_platforms"]:
- platforms = [
- item.strip()
- for item in self._test_file.options["exclude_platforms"].split(",")
- ]
- if sys.platform.lower() in platforms:
- pytest.skip("Test cannot run on platform %r" % (sys.platform,))
-
- def _should_be_skipped_due_to_version(self):
- return (
- sys.version_info < self._test_file.options["min_pyver"]
- or sys.version_info > self._test_file.options["max_pyver"]
- )
-
- def __str__(self):
- return "%s (%s.%s)" % (
- self._test_file.base,
- self.__class__.__module__,
- self.__class__.__name__,
- )
-
- def _open_expected_file(self):
- return open(self._test_file.expected_output)
-
- def _open_source_file(self):
- if self._test_file.base == "invalid_encoded_data":
- return open(self._test_file.source)
- if "latin1" in self._test_file.base:
- return open(self._test_file.source, encoding="latin1")
- return open(self._test_file.source, encoding="utf8")
-
- def _get_expected(self):
- with self._open_source_file() as fobj:
- expected_msgs = get_expected_messages(fobj)
-
- if expected_msgs:
- with self._open_expected_file() as fobj:
- expected_output_lines = parse_expected_output(fobj)
- else:
- expected_output_lines = []
- return expected_msgs, expected_output_lines
-
- def _get_received(self):
- messages = self._linter.reporter.messages
- messages.sort(key=lambda m: (m.line, m.symbol, m.msg))
- received_msgs = collections.Counter()
- received_output_lines = []
- for msg in messages:
- assert (
- msg.symbol != "fatal"
- ), "Pylint analysis failed because of '{}'".format(msg.msg)
- received_msgs[msg.line, msg.symbol] += 1
- received_output_lines.append(OutputLine.from_msg(msg))
- return received_msgs, received_output_lines
-
- def _runTest(self):
- modules_to_check = [self._test_file.source]
- self._linter.check(modules_to_check)
- expected_messages, expected_text = self._get_expected()
- received_messages, received_text = self._get_received()
-
- if expected_messages != received_messages:
- msg = ['Wrong results for file "%s":' % (self._test_file.base)]
- missing, unexpected = multiset_difference(
- expected_messages, received_messages
- )
- if missing:
- msg.append("\nExpected in testdata:")
- msg.extend(" %3d: %s" % msg for msg in sorted(missing))
- if unexpected:
- msg.append("\nUnexpected in testdata:")
- msg.extend(" %3d: %s" % msg for msg in sorted(unexpected))
- pytest.fail("\n".join(msg))
- self._check_output_text(expected_messages, expected_text, received_text)
-
- @classmethod
- def _split_lines(cls, expected_messages, lines):
- emitted, omitted = [], []
- for msg in lines:
- if (msg[1], msg[0]) in expected_messages:
- emitted.append(msg)
- else:
- omitted.append(msg)
- return emitted, omitted
-
- def _check_output_text(self, expected_messages, expected_lines, received_lines):
- expected_lines = self._split_lines(expected_messages, expected_lines)[0]
- for exp, rec in itertools.zip_longest(expected_lines, received_lines):
- assert exp == rec, (
- "Wrong output for '{_file}.txt':\n"
- "You can update the expected output automatically with: '"
- 'python tests/test_functional.py {update_option} -k "test_functional[{_file}]"\'\n\n'
- "Expected : {expected}\n"
- "Received : {received}".format(
- update_option=UPDATE_OPTION,
- expected=exp,
- received=rec,
- _file=self._test_file.base,
- )
- )
diff --git a/pylint/testutils/__init__.py b/pylint/testutils/__init__.py
new file mode 100644
index 000000000..8f132ab9f
--- /dev/null
+++ b/pylint/testutils/__init__.py
@@ -0,0 +1,54 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2012-2014 LOGILAB S.A. (Paris, FRANCE) <contact@logilab.fr>
+# Copyright (c) 2012 FELD Boris <lothiraldan@gmail.com>
+# Copyright (c) 2013-2018, 2020 Claudiu Popa <pcmanticore@gmail.com>
+# Copyright (c) 2013-2014 Google, Inc.
+# Copyright (c) 2013 buck@yelp.com <buck@yelp.com>
+# Copyright (c) 2014 LCD 47 <lcd047@gmail.com>
+# Copyright (c) 2014 Brett Cannon <brett@python.org>
+# Copyright (c) 2014 Ricardo Gemignani <ricardo.gemignani@gmail.com>
+# Copyright (c) 2014 Arun Persaud <arun@nubati.net>
+# Copyright (c) 2015 Pavel Roskin <proski@gnu.org>
+# Copyright (c) 2015 Ionel Cristian Maries <contact@ionelmc.ro>
+# Copyright (c) 2016 Derek Gustafson <degustaf@gmail.com>
+# Copyright (c) 2016 Roy Williams <roy.williams.iii@gmail.com>
+# Copyright (c) 2016 xmo-odoo <xmo-odoo@users.noreply.github.com>
+# Copyright (c) 2017 Bryce Guinta <bryce.paul.guinta@gmail.com>
+# Copyright (c) 2018 ssolanki <sushobhitsolanki@gmail.com>
+# Copyright (c) 2018 Sushobhit <31987769+sushobhit27@users.noreply.github.com>
+# Copyright (c) 2019 Mr. Senko <atodorov@mrsenko.com>
+# Copyright (c) 2019 Hugo van Kemenade <hugovk@users.noreply.github.com>
+# Copyright (c) 2019 Pierre Sassoulas <pierre.sassoulas@gmail.com>
+# Copyright (c) 2020 谭九鼎 <109224573@qq.com>
+# Copyright (c) 2020 Anthony Sottile <asottile@umich.edu>
+# Copyright (c) 2020 Guillaume Peillex <guillaume.peillex@gmail.com>
+
+# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
+# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
+
+"""Functional/non regression tests for pylint"""
+
+__all__ = [
+ "_get_tests_info",
+ "_tokenize_str",
+ "CheckerTestCase",
+ "FunctionalTestFile",
+ "linter",
+ "LintModuleTest",
+ "Message",
+ "MinimalTestReporter",
+ "set_config",
+ "GenericTestReporter",
+ "UPDATE_OPTION",
+]
+
+from pylint.testutils.checker_test_case import CheckerTestCase
+from pylint.testutils.constants import UPDATE_OPTION
+from pylint.testutils.decorator import set_config
+from pylint.testutils.functional_test_file import FunctionalTestFile
+from pylint.testutils.get_test_info import _get_tests_info
+from pylint.testutils.global_test_linter import linter
+from pylint.testutils.lint_module_test import LintModuleTest
+from pylint.testutils.output_line import Message
+from pylint.testutils.reporter_for_tests import GenericTestReporter, MinimalTestReporter
+from pylint.testutils.tokenize_str import _tokenize_str
diff --git a/pylint/testutils/checker_test_case.py b/pylint/testutils/checker_test_case.py
new file mode 100644
index 000000000..9b8281513
--- /dev/null
+++ b/pylint/testutils/checker_test_case.py
@@ -0,0 +1,50 @@
+# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
+# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
+
+import contextlib
+
+from pylint.testutils.global_test_linter import linter
+from pylint.testutils.unittest_linter import UnittestLinter
+from pylint.utils import ASTWalker
+
+
+class CheckerTestCase:
+ """A base testcase class for unit testing individual checker classes."""
+
+ CHECKER_CLASS = None
+ CONFIG = {}
+
+ def setup_method(self):
+ self.linter = UnittestLinter()
+ self.checker = self.CHECKER_CLASS(self.linter) # pylint: disable=not-callable
+ for key, value in self.CONFIG.items():
+ setattr(self.checker.config, key, value)
+ self.checker.open()
+
+ @contextlib.contextmanager
+ def assertNoMessages(self):
+ """Assert that no messages are added by the given method."""
+ with self.assertAddsMessages():
+ yield
+
+ @contextlib.contextmanager
+ def assertAddsMessages(self, *messages):
+ """Assert that exactly the given method adds the given messages.
+
+ The list of messages must exactly match *all* the messages added by the
+ method. Additionally, we check to see whether the args in each message can
+ actually be substituted into the message string.
+ """
+ yield
+ got = self.linter.release_messages()
+ msg = "Expected messages did not match actual.\n" "Expected:\n%s\nGot:\n%s" % (
+ "\n".join(repr(m) for m in messages),
+ "\n".join(repr(m) for m in got),
+ )
+ assert list(messages) == got, msg
+
+ def walk(self, node):
+ """recursive walk on the given node"""
+ walker = ASTWalker(linter)
+ walker.add_checker(self.checker)
+ walker.walk(node)
diff --git a/pylint/testutils/constants.py b/pylint/testutils/constants.py
new file mode 100644
index 000000000..ebc58ec94
--- /dev/null
+++ b/pylint/testutils/constants.py
@@ -0,0 +1,26 @@
+# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
+# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
+
+import operator
+import re
+import sys
+from os.path import abspath, dirname
+
+SYS_VERS_STR = "%d%d%d" % sys.version_info[:3]
+TITLE_UNDERLINES = ["", "=", "-", "."]
+PREFIX = abspath(dirname(__file__))
+UPDATE_OPTION = "--update-functional-output"
+# Common sub-expressions.
+_MESSAGE = {"msg": r"[a-z][a-z\-]+"}
+# Matches a #,
+# - followed by a comparison operator and a Python version (optional),
+# - followed by a line number with a +/- (optional),
+# - followed by a list of bracketed message symbols.
+# Used to extract expected messages from testdata files.
+_EXPECTED_RE = re.compile(
+ r"\s*#\s*(?:(?P<line>[+-]?[0-9]+):)?"
+ r"(?:(?P<op>[><=]+) *(?P<version>[0-9.]+):)?"
+ r"\s*\[(?P<msgs>%(msg)s(?:,\s*%(msg)s)*)]" % _MESSAGE
+)
+
+_OPERATORS = {">": operator.gt, "<": operator.lt, ">=": operator.ge, "<=": operator.le}
diff --git a/pylint/testutils/decorator.py b/pylint/testutils/decorator.py
new file mode 100644
index 000000000..3b70867cb
--- /dev/null
+++ b/pylint/testutils/decorator.py
@@ -0,0 +1,24 @@
+# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
+# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
+
+import functools
+
+from pylint.testutils.checker_test_case import CheckerTestCase
+
+
+def set_config(**kwargs):
+ """Decorator for setting config values on a checker."""
+
+ def _wrapper(fun):
+ @functools.wraps(fun)
+ def _forward(self):
+ for key, value in kwargs.items():
+ setattr(self.checker.config, key, value)
+ if isinstance(self, CheckerTestCase):
+ # reopen checker in case, it may be interested in configuration change
+ self.checker.open()
+ fun(self)
+
+ return _forward
+
+ return _wrapper
diff --git a/pylint/testutils/functional_test_file.py b/pylint/testutils/functional_test_file.py
new file mode 100644
index 000000000..fab6e3aa2
--- /dev/null
+++ b/pylint/testutils/functional_test_file.py
@@ -0,0 +1,73 @@
+# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
+# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
+
+import configparser
+from os.path import basename, exists, join
+
+
+def parse_python_version(ver_str):
+ return tuple(int(digit) for digit in ver_str.split("."))
+
+
+class NoFileError(Exception):
+ pass
+
+
+class FunctionalTestFile:
+ """A single functional test case file with options."""
+
+ _CONVERTERS = {
+ "min_pyver": parse_python_version,
+ "max_pyver": parse_python_version,
+ "requires": lambda s: s.split(","),
+ }
+
+ def __init__(self, directory, filename):
+ self._directory = directory
+ self.base = filename.replace(".py", "")
+ self.options = {
+ "min_pyver": (2, 5),
+ "max_pyver": (4, 0),
+ "requires": [],
+ "except_implementations": [],
+ "exclude_platforms": [],
+ }
+ self._parse_options()
+
+ def __repr__(self):
+ return "FunctionalTest:{}".format(self.base)
+
+ def _parse_options(self):
+ cp = configparser.ConfigParser()
+ cp.add_section("testoptions")
+ try:
+ cp.read(self.option_file)
+ except NoFileError:
+ pass
+
+ for name, value in cp.items("testoptions"):
+ conv = self._CONVERTERS.get(name, lambda v: v)
+ self.options[name] = conv(value)
+
+ @property
+ def option_file(self):
+ return self._file_type(".rc")
+
+ @property
+ def module(self):
+ package = basename(self._directory)
+ return ".".join([package, self.base])
+
+ @property
+ def expected_output(self):
+ return self._file_type(".txt", check_exists=False)
+
+ @property
+ def source(self):
+ return self._file_type(".py")
+
+ def _file_type(self, ext, check_exists=True):
+ name = join(self._directory, self.base + ext)
+ if not check_exists or exists(name):
+ return name
+ raise NoFileError("Cannot find '{}'.".format(name))
diff --git a/pylint/testutils/get_test_info.py b/pylint/testutils/get_test_info.py
new file mode 100644
index 000000000..ea2caea1e
--- /dev/null
+++ b/pylint/testutils/get_test_info.py
@@ -0,0 +1,45 @@
+# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
+# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
+
+from glob import glob
+from os.path import basename, join, splitext
+
+from pylint.testutils.constants import SYS_VERS_STR
+
+
+def _get_tests_info(input_dir, msg_dir, prefix, suffix):
+ """get python input examples and output messages
+
+ We use following conventions for input files and messages:
+ for different inputs:
+ test for python >= x.y -> input = <name>_pyxy.py
+ test for python < x.y -> input = <name>_py_xy.py
+ for one input and different messages:
+ message for python >= x.y -> message = <name>_pyxy.txt
+ lower versions -> message with highest num
+ """
+ result = []
+ for fname in glob(join(input_dir, prefix + "*" + suffix)):
+ infile = basename(fname)
+ fbase = splitext(infile)[0]
+ # filter input files :
+ pyrestr = fbase.rsplit("_py", 1)[-1] # like _26 or 26
+ if pyrestr.isdigit(): # '24', '25'...
+ if SYS_VERS_STR < pyrestr:
+ continue
+ if pyrestr.startswith("_") and pyrestr[1:].isdigit():
+ # skip test for higher python versions
+ if SYS_VERS_STR >= pyrestr[1:]:
+ continue
+ messages = glob(join(msg_dir, fbase + "*.txt"))
+ # the last one will be without ext, i.e. for all or upper versions:
+ if messages:
+ for outfile in sorted(messages, reverse=True):
+ py_rest = outfile.rsplit("_py", 1)[-1][:-4]
+ if py_rest.isdigit() and SYS_VERS_STR >= py_rest:
+ break
+ else:
+ # This will provide an error message indicating the missing filename.
+ outfile = join(msg_dir, fbase + ".txt")
+ result.append((infile, outfile))
+ return result
diff --git a/pylint/testutils/global_test_linter.py b/pylint/testutils/global_test_linter.py
new file mode 100644
index 000000000..75a55e9c0
--- /dev/null
+++ b/pylint/testutils/global_test_linter.py
@@ -0,0 +1,20 @@
+# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
+# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
+
+
+from pylint import checkers
+from pylint.lint import PyLinter
+from pylint.testutils.reporter_for_tests import GenericTestReporter
+
+
+def create_test_linter():
+ test_reporter = GenericTestReporter()
+ linter_ = PyLinter()
+ linter_.set_reporter(test_reporter)
+ linter_.config.persistent = 0
+ checkers.initialize(linter_)
+ return linter_
+
+
+# Can't be renamed to a constant (easily), it breaks countless tests
+linter = create_test_linter()
diff --git a/pylint/testutils/lint_module_test.py b/pylint/testutils/lint_module_test.py
new file mode 100644
index 000000000..3b864184a
--- /dev/null
+++ b/pylint/testutils/lint_module_test.py
@@ -0,0 +1,218 @@
+# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
+# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
+
+import collections
+import csv
+import itertools
+import platform
+import sys
+from io import StringIO
+from typing import Tuple
+
+import pytest
+
+from pylint import checkers
+from pylint.lint import PyLinter
+from pylint.testutils.constants import _EXPECTED_RE, _OPERATORS, UPDATE_OPTION
+from pylint.testutils.functional_test_file import (
+ FunctionalTestFile,
+ NoFileError,
+ parse_python_version,
+)
+from pylint.testutils.output_line import OutputLine
+from pylint.testutils.reporter_for_tests import FunctionalTestReporter
+
+
+class LintModuleTest:
+ maxDiff = None
+
+ def __init__(self, test_file: FunctionalTestFile):
+ _test_reporter = FunctionalTestReporter()
+ self._linter = PyLinter()
+ self._linter.set_reporter(_test_reporter)
+ self._linter.config.persistent = 0
+ checkers.initialize(self._linter)
+ self._linter.disable("I")
+ try:
+ self._linter.read_config_file(test_file.option_file)
+ self._linter.load_config_file()
+ except NoFileError:
+ pass
+ self._test_file = test_file
+
+ def setUp(self):
+ if self._should_be_skipped_due_to_version():
+ pytest.skip(
+ "Test cannot run with Python %s." % (sys.version.split(" ")[0],)
+ )
+ missing = []
+ for req in self._test_file.options["requires"]:
+ try:
+ __import__(req)
+ except ImportError:
+ missing.append(req)
+ if missing:
+ pytest.skip("Requires %s to be present." % (",".join(missing),))
+ if self._test_file.options["except_implementations"]:
+ implementations = [
+ item.strip()
+ for item in self._test_file.options["except_implementations"].split(",")
+ ]
+ implementation = platform.python_implementation()
+ if implementation in implementations:
+ pytest.skip(
+ "Test cannot run with Python implementation %r" % (implementation,)
+ )
+ if self._test_file.options["exclude_platforms"]:
+ platforms = [
+ item.strip()
+ for item in self._test_file.options["exclude_platforms"].split(",")
+ ]
+ if sys.platform.lower() in platforms:
+ pytest.skip("Test cannot run on platform %r" % (sys.platform,))
+
+ def _should_be_skipped_due_to_version(self):
+ return (
+ sys.version_info < self._test_file.options["min_pyver"]
+ or sys.version_info > self._test_file.options["max_pyver"]
+ )
+
+ def __str__(self):
+ return "%s (%s.%s)" % (
+ self._test_file.base,
+ self.__class__.__module__,
+ self.__class__.__name__,
+ )
+
+ @staticmethod
+ def get_expected_messages(stream):
+ """Parses a file and get expected messages.
+
+ :param stream: File-like input stream.
+ :type stream: enumerable
+ :returns: A dict mapping line,msg-symbol tuples to the count on this line.
+ :rtype: dict
+ """
+ messages = collections.Counter()
+ for i, line in enumerate(stream):
+ match = _EXPECTED_RE.search(line)
+ if match is None:
+ continue
+ line = match.group("line")
+ if line is None:
+ line = i + 1
+ elif line.startswith("+") or line.startswith("-"):
+ line = i + 1 + int(line)
+ else:
+ line = int(line)
+
+ version = match.group("version")
+ op = match.group("op")
+ if version:
+ required = parse_python_version(version)
+ if not _OPERATORS[op](sys.version_info, required):
+ continue
+
+ for msg_id in match.group("msgs").split(","):
+ messages[line, msg_id.strip()] += 1
+ return messages
+
+ @staticmethod
+ def multiset_difference(expected_entries: set, actual_entries: set) -> Tuple[set]:
+ """Takes two multisets and compares them.
+
+ A multiset is a dict with the cardinality of the key as the value."""
+ missing = expected_entries.copy()
+ missing.subtract(actual_entries)
+ unexpected = {}
+ for key, value in list(missing.items()):
+ if value <= 0:
+ missing.pop(key)
+ if value < 0:
+ unexpected[key] = -value
+ return missing, unexpected
+
+ def _open_expected_file(self):
+ try:
+ return open(self._test_file.expected_output)
+ except FileNotFoundError:
+ return StringIO("")
+
+ def _open_source_file(self):
+ if self._test_file.base == "invalid_encoded_data":
+ return open(self._test_file.source)
+ if "latin1" in self._test_file.base:
+ return open(self._test_file.source, encoding="latin1")
+ return open(self._test_file.source, encoding="utf8")
+
+ def _get_expected(self):
+ with self._open_source_file() as fobj:
+ expected_msgs = self.get_expected_messages(fobj)
+
+ if expected_msgs:
+ with self._open_expected_file() as fobj:
+ expected_output_lines = [
+ OutputLine.from_csv(row) for row in csv.reader(fobj, "test")
+ ]
+ else:
+ expected_output_lines = []
+ return expected_msgs, expected_output_lines
+
+ def _get_actual(self):
+ messages = self._linter.reporter.messages
+ messages.sort(key=lambda m: (m.line, m.symbol, m.msg))
+ received_msgs = collections.Counter()
+ received_output_lines = []
+ for msg in messages:
+ assert (
+ msg.symbol != "fatal"
+ ), "Pylint analysis failed because of '{}'".format(msg.msg)
+ received_msgs[msg.line, msg.symbol] += 1
+ received_output_lines.append(OutputLine.from_msg(msg))
+ return received_msgs, received_output_lines
+
+ def _runTest(self):
+ modules_to_check = [self._test_file.source]
+ self._linter.check(modules_to_check)
+ expected_messages, expected_output = self._get_expected()
+ actual_messages, actual_output = self._get_actual()
+
+ if expected_messages != actual_messages:
+ msg = ['Wrong results for file "%s":' % (self._test_file.base)]
+ missing, unexpected = self.multiset_difference(
+ expected_messages, actual_messages
+ )
+ if missing:
+ msg.append("\nExpected in testdata:")
+ msg.extend(" %3d: %s" % msg for msg in sorted(missing))
+ if unexpected:
+ msg.append("\nUnexpected in testdata:")
+ msg.extend(" %3d: %s" % msg for msg in sorted(unexpected))
+ pytest.fail("\n".join(msg))
+ self._check_output_text(expected_messages, expected_output, actual_output)
+
+ @classmethod
+ def _split_lines(cls, expected_messages, lines):
+ emitted, omitted = [], []
+ for msg in lines:
+ if (msg[1], msg[0]) in expected_messages:
+ emitted.append(msg)
+ else:
+ omitted.append(msg)
+ return emitted, omitted
+
+ def _check_output_text(self, expected_messages, expected_lines, received_lines):
+ expected_lines = self._split_lines(expected_messages, expected_lines)[0]
+ for exp, rec in itertools.zip_longest(expected_lines, received_lines):
+ assert exp == rec, (
+ "Wrong output for '{_file}.txt':\n"
+ "You can update the expected output automatically with: '"
+ 'python tests/test_functional.py {update_option} -k "test_functional[{_file}]"\'\n\n'
+ "Expected : {expected}\n"
+ "Received : {received}".format(
+ update_option=UPDATE_OPTION,
+ expected=exp,
+ received=rec,
+ _file=self._test_file.base,
+ )
+ )
diff --git a/pylint/testutils/output_line.py b/pylint/testutils/output_line.py
new file mode 100644
index 000000000..17507e2d3
--- /dev/null
+++ b/pylint/testutils/output_line.py
@@ -0,0 +1,68 @@
+# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
+# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
+
+import collections
+
+from pylint import interfaces
+
+
+class Message(
+ collections.namedtuple("Message", ["msg_id", "line", "node", "args", "confidence"])
+):
+ def __new__(cls, msg_id, line=None, node=None, args=None, confidence=None):
+ return tuple.__new__(cls, (msg_id, line, node, args, confidence))
+
+ def __eq__(self, other):
+ if isinstance(other, Message):
+ if self.confidence and other.confidence:
+ return super().__eq__(other)
+ return self[:-1] == other[:-1]
+ return NotImplemented # pragma: no cover
+
+ __hash__ = None
+
+
+class MalformedOutputLineException(Exception):
+ def __init__(self, row, exception):
+ example = "msg-symbolic-name:42:MyClass.my_function:The message"
+ other_example = "msg-symbolic-name:7::The message"
+ reconstructed_row = ":".join(row)
+ msg = "Expected '{example}' or '{other_example}' but we got '{reconstructed_row}'".format(
+ example=example,
+ other_example=other_example,
+ reconstructed_row=reconstructed_row,
+ )
+ Exception.__init__(
+ self, "{msg}: {exception}".format(msg=msg, exception=exception)
+ )
+
+
+class OutputLine(
+ collections.namedtuple(
+ "OutputLine", ["symbol", "lineno", "object", "msg", "confidence"]
+ )
+):
+ @classmethod
+ def from_msg(cls, msg):
+ return cls(
+ msg.symbol,
+ msg.line,
+ msg.obj or "",
+ msg.msg.replace("\r\n", "\n"),
+ msg.confidence.name
+ if msg.confidence != interfaces.UNDEFINED
+ else interfaces.HIGH.name,
+ )
+
+ @classmethod
+ def from_csv(cls, row):
+ try:
+ confidence = row[4] if len(row) == 5 else interfaces.HIGH.name
+ return cls(row[0], int(row[1]), row[2], row[3], confidence)
+ except Exception as e:
+ raise MalformedOutputLineException(row, e) from e
+
+ def to_csv(self):
+ if self.confidence == interfaces.HIGH.name:
+ return self[:-1]
+ return self
diff --git a/pylint/testutils/reporter_for_tests.py b/pylint/testutils/reporter_for_tests.py
new file mode 100644
index 000000000..33d94dee0
--- /dev/null
+++ b/pylint/testutils/reporter_for_tests.py
@@ -0,0 +1,80 @@
+# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
+# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
+
+from io import StringIO
+from os import getcwd, linesep, sep
+
+from pylint import interfaces
+from pylint.reporters import BaseReporter
+
+
+class GenericTestReporter(BaseReporter):
+ """reporter storing plain text messages"""
+
+ __implements__ = interfaces.IReporter
+
+ def __init__(self): # pylint: disable=super-init-not-called
+
+ self.message_ids = {}
+ self.reset()
+ self.path_strip_prefix = getcwd() + sep
+
+ def reset(self):
+ self.out = StringIO()
+ self.messages = []
+
+ def handle_message(self, msg):
+ """manage message of different type and in the context of path """
+ obj = msg.obj
+ line = msg.line
+ msg_id = msg.msg_id
+ msg = msg.msg
+ self.message_ids[msg_id] = 1
+ if obj:
+ obj = ":%s" % obj
+ sigle = msg_id[0]
+ if linesep != "\n":
+ # 2to3 writes os.linesep instead of using
+ # the previously used line separators
+ msg = msg.replace("\r\n", "\n")
+ self.messages.append("%s:%3s%s: %s" % (sigle, line, obj, msg))
+
+ def finalize(self):
+ self.messages.sort()
+ for msg in self.messages:
+ print(msg, file=self.out)
+ result = self.out.getvalue()
+ self.reset()
+ return result
+
+ # pylint: disable=unused-argument
+ def on_set_current_module(self, module, filepath):
+ pass
+
+ # pylint: enable=unused-argument
+
+ def display_reports(self, layout):
+ """ignore layouts"""
+
+ _display = None
+
+
+class MinimalTestReporter(BaseReporter):
+ def handle_message(self, msg):
+ self.messages.append(msg)
+
+ def on_set_current_module(self, module, filepath):
+ self.messages = []
+
+ _display = None
+
+
+class FunctionalTestReporter(BaseReporter): # pylint: disable=abstract-method
+ def handle_message(self, msg):
+ self.messages.append(msg)
+
+ def on_set_current_module(self, module, filepath):
+ self.messages = []
+
+ def display_reports(self, layout):
+ """Ignore layouts and don't call self._display()."""
diff --git a/pylint/testutils/tokenize_str.py b/pylint/testutils/tokenize_str.py
new file mode 100644
index 000000000..2b3c5f2c1
--- /dev/null
+++ b/pylint/testutils/tokenize_str.py
@@ -0,0 +1,9 @@
+# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
+# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
+
+import tokenize
+from io import StringIO
+
+
+def _tokenize_str(code):
+ return list(tokenize.generate_tokens(StringIO(code).readline))
diff --git a/pylint/testutils/unittest_linter.py b/pylint/testutils/unittest_linter.py
new file mode 100644
index 000000000..540874611
--- /dev/null
+++ b/pylint/testutils/unittest_linter.py
@@ -0,0 +1,40 @@
+# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
+# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
+
+from pylint.testutils.global_test_linter import linter
+from pylint.testutils.output_line import Message
+
+
+class UnittestLinter:
+ """A fake linter class to capture checker messages."""
+
+ # pylint: disable=unused-argument, no-self-use
+
+ def __init__(self):
+ self._messages = []
+ self.stats = {}
+
+ def release_messages(self):
+ try:
+ return self._messages
+ finally:
+ self._messages = []
+
+ def add_message(
+ self, msg_id, line=None, node=None, args=None, confidence=None, col_offset=None
+ ):
+ # Do not test col_offset for now since changing Message breaks everything
+ self._messages.append(Message(msg_id, line, node, args, confidence))
+
+ @staticmethod
+ def is_message_enabled(*unused_args, **unused_kwargs):
+ return True
+
+ def add_stats(self, **kwargs):
+ for name, value in kwargs.items():
+ self.stats[name] = value
+ return self.stats
+
+ @property
+ def options_providers(self):
+ return linter.options_providers
diff --git a/pytest.ini b/pytest.ini
index 1f2d77424..24b952c04 100644
--- a/pytest.ini
+++ b/pytest.ini
@@ -2,4 +2,5 @@
python_files=*test_*.py
addopts=-m "not acceptance"
markers =
- acceptance
+ acceptance:
+ benchmark: Baseline of pylint performance, if this regress something serious happened
diff --git a/setup.py b/setup.py
index 600977fd6..6c2eaba3f 100644
--- a/setup.py
+++ b/setup.py
@@ -1,6 +1,3 @@
-# -*- coding: utf-8 -*-
-#!/usr/bin/env python
-# pylint: disable=W0404,W0622,W0613
# Copyright (c) 2006, 2009-2010, 2012-2014 LOGILAB S.A. (Paris, FRANCE) <contact@logilab.fr>
# Copyright (c) 2010 Julien Jehannet <julien.jehannet@logilab.fr>
# Copyright (c) 2012 FELD Boris <lothiraldan@gmail.com>
@@ -26,16 +23,15 @@
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
-"""Generic Setup script, takes package info from __pkginfo__.py file.
-"""
+"""Generic Setup script, takes package info from __pkginfo__.py file."""
+
+# pylint: disable=import-outside-toplevel,arguments-differ,ungrouped-imports,exec-used
+
import os
import sys
from distutils.command.build_py import build_py
from os.path import exists, isdir, join
-__docformat__ = "restructuredtext en"
-
-
try:
from setuptools import setup
from setuptools.command import easy_install as easy_install_lib
@@ -50,6 +46,7 @@ except ImportError:
easy_install_lib = None
+__docformat__ = "restructuredtext en"
base_dir = os.path.dirname(__file__)
__pkginfo__ = {}
diff --git a/tests/benchmark/test_baseline_benchmarks.py b/tests/benchmark/test_baseline_benchmarks.py
index ed9f599d3..3d642c33f 100644
--- a/tests/benchmark/test_baseline_benchmarks.py
+++ b/tests/benchmark/test_baseline_benchmarks.py
@@ -16,7 +16,7 @@ import pytest
import pylint.interfaces
from pylint.checkers.base_checker import BaseChecker
from pylint.lint import PyLinter, Run, check_parallel
-from pylint.testutils import TestReporter as Reporter
+from pylint.testutils import GenericTestReporter as Reporter
from pylint.utils import register_plugins
diff --git a/tests/functional/a/assignment_from_no_return_py3.txt b/tests/functional/a/assignment_from_no_return_py3.txt
deleted file mode 100644
index e69de29bb..000000000
--- a/tests/functional/a/assignment_from_no_return_py3.txt
+++ /dev/null
diff --git a/tests/functional/b/bad_reversed_sequence.py b/tests/functional/b/bad_reversed_sequence.py
index f423dd2d6..0d7b84da0 100644
--- a/tests/functional/b/bad_reversed_sequence.py
+++ b/tests/functional/b/bad_reversed_sequence.py
@@ -44,7 +44,6 @@ def test(path):
seq = reversed([1, 2, 3])
seq = reversed((1, 2, 3))
seq = reversed(set()) # [bad-reversed-sequence]
- seq = reversed({'a': 1, 'b': 2}) # [bad-reversed-sequence]
seq = reversed(iter([1, 2, 3])) # [bad-reversed-sequence]
seq = reversed(GoodReversed())
seq = reversed(SecondGoodReversed())
diff --git a/tests/functional/b/bad_reversed_sequence.txt b/tests/functional/b/bad_reversed_sequence.txt
index dd0c6f96d..25143d3a4 100644
--- a/tests/functional/b/bad_reversed_sequence.txt
+++ b/tests/functional/b/bad_reversed_sequence.txt
@@ -1,8 +1,10 @@
-bad-reversed-sequence:43:test:The first reversed() argument is not a sequence
-bad-reversed-sequence:46:test:The first reversed() argument is not a sequence
-bad-reversed-sequence:47:test:The first reversed() argument is not a sequence
-bad-reversed-sequence:48:test:The first reversed() argument is not a sequence
-bad-reversed-sequence:51:test:The first reversed() argument is not a sequence
-bad-reversed-sequence:52:test:The first reversed() argument is not a sequence
-bad-reversed-sequence:54:test:The first reversed() argument is not a sequence
-bad-reversed-sequence:55:test:The first reversed() argument is not a sequence
+bad-reversed-sequence:43:test:The first reversed() argument is not a sequence
+bad-reversed-sequence:46:test:The first reversed() argument is not a sequence
+bad-reversed-sequence:47:test:The first reversed() argument is not a sequence
+bad-reversed-sequence:48:test:The first reversed() argument is not a sequence
+bad-reversed-sequence:50:test:The first reversed() argument is not a sequence
+bad-reversed-sequence:51:test:The first reversed() argument is not a sequence
+bad-reversed-sequence:52:test:The first reversed() argument is not a sequence
+bad-reversed-sequence:53:test:The first reversed() argument is not a sequence
+bad-reversed-sequence:54:test:The first reversed() argument is not a sequence
+bad-reversed-sequence:55:test:The first reversed() argument is not a sequence
diff --git a/tests/functional/b/bad_reversed_sequence_py37.py b/tests/functional/b/bad_reversed_sequence_py37.py
new file mode 100644
index 000000000..a28c84cc0
--- /dev/null
+++ b/tests/functional/b/bad_reversed_sequence_py37.py
@@ -0,0 +1,2 @@
+""" Dictionaries are reversible starting on python 3.8"""
+reversed({'a': 1, 'b': 2}) # [bad-reversed-sequence]
diff --git a/tests/functional/b/bad_reversed_sequence_py37.rc b/tests/functional/b/bad_reversed_sequence_py37.rc
new file mode 100644
index 000000000..67a28a36a
--- /dev/null
+++ b/tests/functional/b/bad_reversed_sequence_py37.rc
@@ -0,0 +1,2 @@
+[testoptions]
+max_pyver=3.8
diff --git a/tests/functional/b/bad_reversed_sequence_py37.txt b/tests/functional/b/bad_reversed_sequence_py37.txt
new file mode 100644
index 000000000..47d0c6c54
--- /dev/null
+++ b/tests/functional/b/bad_reversed_sequence_py37.txt
@@ -0,0 +1 @@
+bad-reversed-sequence:2::The first reversed() argument is not a sequence
diff --git a/tests/functional/b/bad_reversed_sequence_py38.py b/tests/functional/b/bad_reversed_sequence_py38.py
new file mode 100644
index 000000000..bbfdd97c3
--- /dev/null
+++ b/tests/functional/b/bad_reversed_sequence_py38.py
@@ -0,0 +1,2 @@
+""" Dictionaries are reversible starting on python 3.8"""
+reversed({'a': 1, 'b': 2})
diff --git a/tests/functional/b/bad_reversed_sequence_py38.rc b/tests/functional/b/bad_reversed_sequence_py38.rc
new file mode 100644
index 000000000..85fc502b3
--- /dev/null
+++ b/tests/functional/b/bad_reversed_sequence_py38.rc
@@ -0,0 +1,2 @@
+[testoptions]
+min_pyver=3.8
diff --git a/tests/functional/c/crash_missing_module_type.txt b/tests/functional/c/crash_missing_module_type.txt
deleted file mode 100644
index e69de29bb..000000000
--- a/tests/functional/c/crash_missing_module_type.txt
+++ /dev/null
diff --git a/tests/functional/f/fallback_import_disabled.txt b/tests/functional/f/fallback_import_disabled.txt
deleted file mode 100644
index e69de29bb..000000000
--- a/tests/functional/f/fallback_import_disabled.txt
+++ /dev/null
diff --git a/tests/functional/f/formatting.txt b/tests/functional/f/formatting.txt
deleted file mode 100644
index e69de29bb..000000000
--- a/tests/functional/f/formatting.txt
+++ /dev/null
diff --git a/tests/functional/g/genexp_in_class_scope.txt b/tests/functional/g/genexp_in_class_scope.txt
deleted file mode 100644
index e69de29bb..000000000
--- a/tests/functional/g/genexp_in_class_scope.txt
+++ /dev/null
diff --git a/tests/functional/i/implicit_str_concat_latin1.txt b/tests/functional/i/implicit_str_concat_latin1.txt
deleted file mode 100644
index e69de29bb..000000000
--- a/tests/functional/i/implicit_str_concat_latin1.txt
+++ /dev/null
diff --git a/tests/functional/i/implicit_str_concat_utf8.txt b/tests/functional/i/implicit_str_concat_utf8.txt
deleted file mode 100644
index e69de29bb..000000000
--- a/tests/functional/i/implicit_str_concat_utf8.txt
+++ /dev/null
diff --git a/tests/functional/i/invalid_metaclass.txt b/tests/functional/i/invalid_metaclass.txt
deleted file mode 100644
index e69de29bb..000000000
--- a/tests/functional/i/invalid_metaclass.txt
+++ /dev/null
diff --git a/tests/functional/i/invalid_metaclass_py3.txt b/tests/functional/i/invalid_metaclass_py3.txt
deleted file mode 100644
index e69de29bb..000000000
--- a/tests/functional/i/invalid_metaclass_py3.txt
+++ /dev/null
diff --git a/tests/functional/l/long_utf8_lines.txt b/tests/functional/l/long_utf8_lines.txt
deleted file mode 100644
index e69de29bb..000000000
--- a/tests/functional/l/long_utf8_lines.txt
+++ /dev/null
diff --git a/tests/functional/m/missing_module_docstring_disabled.txt b/tests/functional/m/missing_module_docstring_disabled.txt
deleted file mode 100644
index e69de29bb..000000000
--- a/tests/functional/m/missing_module_docstring_disabled.txt
+++ /dev/null
diff --git a/tests/functional/m/missing_self_argument.txt b/tests/functional/m/missing_self_argument.txt
index 9a47c8914..ad0db0186 100644
--- a/tests/functional/m/missing_self_argument.txt
+++ b/tests/functional/m/missing_self_argument.txt
@@ -1,6 +1,3 @@
no-method-argument:12:MyClass.method:Method has no argument
-no-method-argument:14:MyClass.met:"""Method has no argument
-""
-"
no-method-argument:15:MyClass.setup:Method has no argument
undefined-variable:17:MyClass.setup:Undefined variable 'self'
diff --git a/tests/functional/m/monkeypatch_method.txt b/tests/functional/m/monkeypatch_method.txt
deleted file mode 100644
index e69de29bb..000000000
--- a/tests/functional/m/monkeypatch_method.txt
+++ /dev/null
diff --git a/tests/functional/n/no_self_use_py3.txt b/tests/functional/n/no_self_use_py3.txt
deleted file mode 100644
index e69de29bb..000000000
--- a/tests/functional/n/no_self_use_py3.txt
+++ /dev/null
diff --git a/tests/functional/n/not_async_context_manager_py37.py b/tests/functional/n/not_async_context_manager_py37.py
index 705e5afc9..c1ca26976 100644
--- a/tests/functional/n/not_async_context_manager_py37.py
+++ b/tests/functional/n/not_async_context_manager_py37.py
@@ -10,3 +10,14 @@ async def context_manager(value):
async with context_manager(42) as ans:
assert ans == 42
+
+
+def async_context_manager():
+ @asynccontextmanager
+ async def wrapper():
+ pass
+ return wrapper
+
+async def func():
+ async with async_context_manager():
+ pass
diff --git a/tests/functional/n/not_async_context_manager_py37.txt b/tests/functional/n/not_async_context_manager_py37.txt
deleted file mode 100644
index e69de29bb..000000000
--- a/tests/functional/n/not_async_context_manager_py37.txt
+++ /dev/null
diff --git a/tests/functional/p/postponed_evaluation_activated.txt b/tests/functional/p/postponed_evaluation_activated.txt
deleted file mode 100644
index e69de29bb..000000000
--- a/tests/functional/p/postponed_evaluation_activated.txt
+++ /dev/null
diff --git a/tests/functional/r/raising_self.txt b/tests/functional/r/raising_self.txt
deleted file mode 100644
index e69de29bb..000000000
--- a/tests/functional/r/raising_self.txt
+++ /dev/null
diff --git a/tests/functional/r/recursion_error_2667.txt b/tests/functional/r/recursion_error_2667.txt
deleted file mode 100644
index e69de29bb..000000000
--- a/tests/functional/r/recursion_error_2667.txt
+++ /dev/null
diff --git a/tests/functional/r/recursion_error_crash.txt b/tests/functional/r/recursion_error_crash.txt
deleted file mode 100644
index e69de29bb..000000000
--- a/tests/functional/r/recursion_error_crash.txt
+++ /dev/null
diff --git a/tests/functional/r/recursion_error_crash_2683.txt b/tests/functional/r/recursion_error_crash_2683.txt
deleted file mode 100644
index e69de29bb..000000000
--- a/tests/functional/r/recursion_error_crash_2683.txt
+++ /dev/null
diff --git a/tests/functional/r/recursion_error_crash_astroid_623.txt b/tests/functional/r/recursion_error_crash_astroid_623.txt
deleted file mode 100644
index e69de29bb..000000000
--- a/tests/functional/r/recursion_error_crash_astroid_623.txt
+++ /dev/null
diff --git a/tests/functional/r/regression_no_value_for_parameter.txt b/tests/functional/r/regression_no_value_for_parameter.txt
deleted file mode 100644
index e69de29bb..000000000
--- a/tests/functional/r/regression_no_value_for_parameter.txt
+++ /dev/null
diff --git a/tests/functional/s/statement_without_effect.txt b/tests/functional/s/statement_without_effect.txt
index bda6eaea9..f9c22b1bd 100644
--- a/tests/functional/s/statement_without_effect.txt
+++ b/tests/functional/s/statement_without_effect.txt
@@ -1,60 +1,12 @@
pointless-string-statement:5::String statement has no effect
-pointless-statement:6::"""Statement seems to have no effect
-""
-"
-pointless-statement:8::"""Statement seems to have no effect
-""
-"
pointless-statement:9::Statement seems to have no effect
pointless-statement:11::Statement seems to have no effect
-pointless-statement:12::"""Statement seems to have no effect
-""
-"
pointless-statement:15::Statement seems to have no effect
-pointless-string-statement:15::"""String statement has no effect
-""
-"
-unnecessary-semicolon:17::"""Unnecessary semicolon
-""
-"
pointless-string-statement:18::String statement has no effect
-unnecessary-semicolon:18::"""Unnecessary semicolon
-""
-"
-expression-not-assigned:19::"""Expression """"list() and tuple()"""" is assigned to nothing
-""
-"
-expression-not-assigned:20::"""Expression """"list() and tuple()"""" is assigned to nothing
-""
-"
unnecessary-semicolon:21::Unnecessary semicolon
expression-not-assigned:23::"Expression ""list() and tuple()"" is assigned to nothing"
-expression-not-assigned:26::"""Expression """"ANSWER == to_be()"""" is assigned to nothing
-""
-"
-expression-not-assigned:27::"""Expression """"ANSWER == to_be()"""" is assigned to nothing
-""
-"
-expression-not-assigned:28::"""Expression """"to_be() or not to_be()"""" is assigned to nothing
-""
-"
-expression-not-assigned:29::"""Expression """"to_be() or not to_be()"""" is assigned to nothing
-""
-"
expression-not-assigned:30::"Expression ""ANSWER == to_be()"" is assigned to nothing"
expression-not-assigned:32::"Expression ""to_be() or not to_be()"" is assigned to nothing"
expression-not-assigned:33::"Expression ""to_be().title"" is assigned to nothing"
-pointless-string-statement:54:ClassLevelAttributeTest.__init__:"""String statement has no effect
-""
-"
-pointless-string-statement:55:ClassLevelAttributeTest.__init__:"""String statement has no effect
-""
-"
pointless-string-statement:58:ClassLevelAttributeTest.__init__:String statement has no effect
-pointless-string-statement:61:ClassLevelAttributeTest.test:"""String statement has no effect
-""
-"
-pointless-string-statement:62:ClassLevelAttributeTest.test:"""String statement has no effect
-""
-"
pointless-string-statement:65:ClassLevelAttributeTest.test:String statement has no effect
diff --git a/tests/functional/too/too_few_public_methods_37.txt b/tests/functional/too/too_few_public_methods_37.txt
deleted file mode 100644
index e69de29bb..000000000
--- a/tests/functional/too/too_few_public_methods_37.txt
+++ /dev/null
diff --git a/tests/functional/too/too_many_arguments_issue_1045.txt b/tests/functional/too/too_many_arguments_issue_1045.txt
deleted file mode 100644
index e69de29bb..000000000
--- a/tests/functional/too/too_many_arguments_issue_1045.txt
+++ /dev/null
diff --git a/tests/functional/u/ungrouped_imports_isort_compatible.txt b/tests/functional/u/ungrouped_imports_isort_compatible.txt
deleted file mode 100644
index e69de29bb..000000000
--- a/tests/functional/u/ungrouped_imports_isort_compatible.txt
+++ /dev/null
diff --git a/tests/functional/u/unused_variable_py36.txt b/tests/functional/u/unused_variable_py36.txt
deleted file mode 100644
index e69de29bb..000000000
--- a/tests/functional/u/unused_variable_py36.txt
+++ /dev/null
diff --git a/tests/functional/w/wrong_import_position_exclude_dunder_main.txt b/tests/functional/w/wrong_import_position_exclude_dunder_main.txt
deleted file mode 100644
index e69de29bb..000000000
--- a/tests/functional/w/wrong_import_position_exclude_dunder_main.txt
+++ /dev/null
diff --git a/tests/lint/unittest_lint.py b/tests/lint/unittest_lint.py
index d60ea957a..3ce1d5e24 100644
--- a/tests/lint/unittest_lint.py
+++ b/tests/lint/unittest_lint.py
@@ -242,7 +242,7 @@ def disable():
@pytest.fixture(scope="module")
def reporter():
- return testutils.TestReporter
+ return testutils.GenericTestReporter
@pytest.fixture
@@ -480,7 +480,7 @@ def test_disable_alot(linter):
def test_addmessage(linter):
- linter.set_reporter(testutils.TestReporter())
+ linter.set_reporter(testutils.GenericTestReporter())
linter.open()
linter.set_current_module("0123")
linter.add_message("C0301", line=1, args=(1, 2))
@@ -492,7 +492,7 @@ def test_addmessage(linter):
def test_addmessage_invalid(linter):
- linter.set_reporter(testutils.TestReporter())
+ linter.set_reporter(testutils.GenericTestReporter())
linter.open()
linter.set_current_module("0123")
@@ -570,7 +570,7 @@ def test_init_hooks_called_before_load_plugins():
def test_analyze_explicit_script(linter):
- linter.set_reporter(testutils.TestReporter())
+ linter.set_reporter(testutils.GenericTestReporter())
linter.check(os.path.join(DATA_DIR, "ascript"))
assert ["C: 2: Line too long (175/100)"] == linter.reporter.messages
@@ -768,7 +768,7 @@ def test_custom_should_analyze_file():
wrong_file = os.path.join(package_dir, "wrong.py")
for jobs in [1, 2]:
- reporter = testutils.TestReporter()
+ reporter = testutils.GenericTestReporter()
linter = _CustomPyLinter()
linter.config.jobs = jobs
linter.config.persistent = 0
@@ -801,7 +801,7 @@ def test_multiprocessing(jobs):
"wrong_import_position.py",
]
- reporter = testutils.TestReporter()
+ reporter = testutils.GenericTestReporter()
linter = PyLinter()
linter.config.jobs = jobs
linter.config.persistent = 0
@@ -822,7 +822,7 @@ def test_filename_with__init__(init_linter):
# This tracks a regression where a file whose name ends in __init__.py,
# such as flycheck__init__.py, would accidentally lead to linting the
# entire containing directory.
- reporter = testutils.TestReporter()
+ reporter = testutils.GenericTestReporter()
linter = init_linter
linter.open()
linter.set_reporter(reporter)
diff --git a/tests/profile/test_profile_against_externals.py b/tests/profile/test_profile_against_externals.py
index e7159264b..4bbac7556 100644
--- a/tests/profile/test_profile_against_externals.py
+++ b/tests/profile/test_profile_against_externals.py
@@ -8,13 +8,11 @@
import os
import pprint
-import shutil
-import tempfile
import pytest
from pylint.lint import Run
-from pylint.testutils import TestReporter as Reporter
+from pylint.testutils import GenericTestReporter as Reporter
def _get_py_files(scanpath):
diff --git a/tests/test_check_parallel.py b/tests/test_check_parallel.py
index f44ce666d..e8f67f4b6 100644
--- a/tests/test_check_parallel.py
+++ b/tests/test_check_parallel.py
@@ -18,7 +18,7 @@ from pylint.lint import PyLinter
from pylint.lint.parallel import _worker_check_single_file as worker_check_single_file
from pylint.lint.parallel import _worker_initialize as worker_initialize
from pylint.lint.parallel import check_parallel
-from pylint.testutils import TestReporter as Reporter
+from pylint.testutils import GenericTestReporter as Reporter
def _gen_file_data(idx=0):
diff --git a/tests/test_func.py b/tests/test_func.py
index dd3a1bc63..3ba84deaf 100644
--- a/tests/test_func.py
+++ b/tests/test_func.py
@@ -25,12 +25,11 @@ import pytest
from pylint.testutils import _get_tests_info, linter
-SYS_VERS_STR = "%d%d%d" % sys.version_info[:3]
-
# Configure paths
INPUT_DIR = join(dirname(abspath(__file__)), "input")
MSG_DIR = join(dirname(abspath(__file__)), "messages")
+
FILTER_RGX = None
UPDATE = False
INFO_TEST_RGX = re.compile(r"^func_i\d\d\d\d$")
@@ -53,10 +52,6 @@ class LintTestUsingModule:
output = None
_TEST_TYPE = "module"
- # def runTest(self):
- # # This is a hack to make ./test/test_func.py work under pytest.
- # pass
-
def _test_functionality(self):
tocheck = [self.package + "." + self.module]
# pylint: disable=not-an-iterable; can't handle boolean checks for now
@@ -124,28 +119,42 @@ def gen_tests(filter_rgx):
base = module_file.replace(".py", "").split("_")[1]
dependencies = _get_tests_info(INPUT_DIR, MSG_DIR, base, ".py")
tests.append((module_file, messages_file, dependencies))
-
if UPDATE:
return tests
-
assert len(tests) < 196, "Please do not add new test cases here."
return tests
+TEST_WITH_EXPECTED_DEPRECATION = ["func_excess_escapes.py"]
+
+
@pytest.mark.parametrize(
"module_file,messages_file,dependencies",
gen_tests(FILTER_RGX),
ids=[o[0] for o in gen_tests(FILTER_RGX)],
)
-def test_functionality(module_file, messages_file, dependencies):
-
- LT = LintTestUpdate() if UPDATE else LintTestUsingModule()
-
- LT.module = module_file.replace(".py", "")
- LT.output = messages_file
- LT.depends = dependencies or None
- LT.INPUT_DIR = INPUT_DIR
- LT._test_functionality()
+def test_functionality(module_file, messages_file, dependencies, recwarn):
+ __test_functionality(module_file, messages_file, dependencies)
+ warning = None
+ try:
+ # Catch <unknown>:x: DeprecationWarning: invalid escape sequence
+ # so it's not shown during tests
+ warning = recwarn.pop()
+ except AssertionError:
+ pass
+ if warning is not None:
+ if module_file in TEST_WITH_EXPECTED_DEPRECATION and sys.version_info.minor > 5:
+ assert issubclass(warning.category, DeprecationWarning)
+ assert "invalid escape sequence" in str(warning.message)
+
+
+def __test_functionality(module_file, messages_file, dependencies):
+ lint_test = LintTestUpdate() if UPDATE else LintTestUsingModule()
+ lint_test.module = module_file.replace(".py", "")
+ lint_test.output = messages_file
+ lint_test.depends = dependencies or None
+ lint_test.INPUT_DIR = INPUT_DIR
+ lint_test._test_functionality()
if __name__ == "__main__":
diff --git a/tests/test_functional.py b/tests/test_functional.py
index 1958a23d0..70d553921 100644
--- a/tests/test_functional.py
+++ b/tests/test_functional.py
@@ -98,10 +98,14 @@ def get_tests():
TESTS = get_tests()
TESTS_NAMES = [t.base for t in TESTS]
+TEST_WITH_EXPECTED_DEPRECATION = [
+ "future_unicode_literals",
+ "anomalous_unicode_escape_py3",
+]
@pytest.mark.parametrize("test_file", TESTS, ids=TESTS_NAMES)
-def test_functional(test_file):
+def test_functional(test_file, recwarn):
LintTest = (
LintModuleOutputUpdate(test_file)
if UPDATE.exists()
@@ -109,6 +113,20 @@ def test_functional(test_file):
)
LintTest.setUp()
LintTest._runTest()
+ warning = None
+ try:
+ # Catch <unknown>:x: DeprecationWarning: invalid escape sequence
+ # so it's not shown during tests
+ warning = recwarn.pop()
+ except AssertionError:
+ pass
+ if warning is not None:
+ if (
+ test_file.base in TEST_WITH_EXPECTED_DEPRECATION
+ and sys.version_info.minor > 5
+ ):
+ assert issubclass(warning.category, DeprecationWarning)
+ assert "invalid escape sequence" in str(warning.message)
if __name__ == "__main__":
diff --git a/tests/test_import_graph.py b/tests/test_import_graph.py
index 0d4ea7f04..a57e2c880 100644
--- a/tests/test_import_graph.py
+++ b/tests/test_import_graph.py
@@ -58,7 +58,7 @@ URL="." node[shape="box"]
@pytest.fixture
def linter():
- l = PyLinter(reporter=testutils.TestReporter())
+ l = PyLinter(reporter=testutils.GenericTestReporter())
initialize(l)
return l
diff --git a/tests/test_regr.py b/tests/test_regr.py
index a26cc1be1..c7a8d56b7 100644
--- a/tests/test_regr.py
+++ b/tests/test_regr.py
@@ -38,7 +38,7 @@ except AttributeError:
@pytest.fixture(scope="module")
def reporter():
- return testutils.TestReporter
+ return testutils.GenericTestReporter
@pytest.fixture(scope="module")
diff --git a/tox.ini b/tox.ini
index 9b9671050..dd1d0da72 100644
--- a/tox.ini
+++ b/tox.ini
@@ -36,11 +36,11 @@ commands =
[testenv:formatting]
basepython = python3
deps =
- black==20.8b1
- isort==5.5.2
+ pre-commit
+ sphinx
+ pytest
commands =
- black --diff --check . --exclude="tests/functional/|tests/input|tests/extensions/data|tests/regrtest_data/|tests/data/|venv|astroid|.tox"
- isort . --check-only
+ pre-commit run --all-files
changedir = {toxinidir}
[testenv:mypy]
@@ -54,7 +54,7 @@ commands =
[testenv]
deps =
- https://github.com/PyCQA/astroid/tarball/master#egg=astroid-master-2.0
+ https://github.com/PyCQA/astroid/tarball/master#egg=astroid
coverage<5.0
mccabe
# isort 5 is not compatible with Python 3.5
@@ -80,7 +80,7 @@ changedir = {toxworkdir}
[testenv:spelling]
deps =
- https://github.com/PyCQA/astroid/tarball/master#egg=astroid-master-2.0
+ https://github.com/PyCQA/astroid/tarball/master#egg=astroid
pytest
pytest-xdist
pyenchant
@@ -136,7 +136,7 @@ commands =
[testenv:benchmark]
deps =
- https://github.com/PyCQA/astroid/tarball/master#egg=astroid-master-2.0
+ https://github.com/PyCQA/astroid/tarball/master#egg=astroid
coverage<5.0
mccabe
pytest
@@ -159,7 +159,7 @@ commands =
[testenv:profile_against_external]
deps =
- https://github.com/PyCQA/astroid/tarball/master#egg=astroid-master-2.0
+ https://github.com/PyCQA/astroid/tarball/master#egg=astroid
gprof2dot
mccabe
pytest