diff options
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 @@ -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 @@ -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") @@ -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 |