From 5bb0095aa122fd75108dc8049193b55b987ef224 Mon Sep 17 00:00:00 2001 From: Pierre Sassoulas Date: Sun, 28 Nov 2021 19:50:59 +0100 Subject: Add typing to LintModuleOutputUpdate._check_output_text Better typing for test options and fix existing issue --- ChangeLog | 4 +- pylint/testutils/__init__.py | 2 +- .../testutils/functional/find_functional_tests.py | 1 + .../functional/lint_module_output_update.py | 19 +++++-- pylint/testutils/functional/test_file.py | 60 +++++++++++++++++----- pylint/testutils/lint_module_test.py | 2 +- tests/functional/f/fixme.rc | 4 +- .../r/regression/regression_issue_4631.rc | 2 +- .../t/too/too_few_public_methods_excluded.rc | 2 +- .../t/too/too_many_ancestors_ignored_parents.rc | 2 +- tests/functional/t/too/too_many_statements.rc | 2 +- tests/test_functional.py | 2 +- tests/testutils/test_lint_module_output_update.py | 2 +- 13 files changed, 76 insertions(+), 28 deletions(-) diff --git a/ChangeLog b/ChangeLog index 83b0b1586..9cf285dc2 100644 --- a/ChangeLog +++ b/ChangeLog @@ -11,8 +11,8 @@ Release date: TBA .. Put new features here and also in 'doc/whatsnew/2.13.rst' -* Some file in ``pylint.testutils`` were deprecated, imports should be done from the - ``pylint.testutils`` API directly +* Some files in ``pylint.testutils`` were deprecated. In the future imports should be done from the + ``pylint.testutils.functional`` namespace directly. .. Insert your changelog randomly, it will reduce merge conflicts diff --git a/pylint/testutils/__init__.py b/pylint/testutils/__init__.py index 803a1ae16..53e301da3 100644 --- a/pylint/testutils/__init__.py +++ b/pylint/testutils/__init__.py @@ -49,7 +49,7 @@ __all__ = [ from pylint.testutils.checker_test_case import CheckerTestCase from pylint.testutils.constants import UPDATE_FILE, UPDATE_OPTION from pylint.testutils.decorator import set_config -from pylint.testutils.functional.test_file import FunctionalTestFile +from pylint.testutils.functional 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 diff --git a/pylint/testutils/functional/find_functional_tests.py b/pylint/testutils/functional/find_functional_tests.py index 27e5880da..610b05db1 100644 --- a/pylint/testutils/functional/find_functional_tests.py +++ b/pylint/testutils/functional/find_functional_tests.py @@ -15,6 +15,7 @@ REASONABLY_DISPLAYABLE_VERTICALLY = 48 def get_functional_test_files_from_directory( input_dir: Union[Path, str] ) -> List[FunctionalTestFile]: + """Get all functional tests in the input_dir.""" suite = [] for dirpath, _, filenames in os.walk(input_dir): if dirpath.endswith("__pycache__"): diff --git a/pylint/testutils/functional/lint_module_output_update.py b/pylint/testutils/functional/lint_module_output_update.py index dfb891189..0bd46fc0b 100644 --- a/pylint/testutils/functional/lint_module_output_update.py +++ b/pylint/testutils/functional/lint_module_output_update.py @@ -3,19 +3,22 @@ import csv import os -from typing import Optional +from typing import List, Optional from _pytest.config import Config from pylint.constants import PY38_PLUS from pylint.testutils.functional.test_file import FunctionalTestFile -from pylint.testutils.lint_module_test import LintModuleTest +from pylint.testutils.lint_module_test import LintModuleTest, MessageCounter +from pylint.testutils.output_line import OutputLine class LintModuleOutputUpdate(LintModuleTest): - """If message files should be updated instead of checked.""" + """Class to be used if expected output files should be updated instead of checked.""" class TestDialect(csv.excel): + """Dialect used by the csv writer.""" + delimiter = ":" lineterminator = "\n" @@ -32,11 +35,19 @@ class LintModuleOutputUpdate(LintModuleTest): ) super().__init__(test_file, config) - def _check_output_text(self, _, expected_output, actual_output): + def _check_output_text( + self, + _: MessageCounter, + expected_output: List[OutputLine], + actual_output: List[OutputLine], + ) -> None: + """Overwrite or remove the expected output file based on actual output.""" + # Remove the file if no output is actually expected and a file exists if not expected_output and not actual_output: if os.path.exists(self._test_file.expected_output): os.remove(self._test_file.expected_output) return + # Write file with expected output with open(self._test_file.expected_output, "w", encoding="utf-8") as f: writer = csv.writer(f, dialect="test") for line in actual_output: diff --git a/pylint/testutils/functional/test_file.py b/pylint/testutils/functional/test_file.py index 4bf0777f8..8aa0361d4 100644 --- a/pylint/testutils/functional/test_file.py +++ b/pylint/testutils/functional/test_file.py @@ -2,10 +2,13 @@ # For details: https://github.com/PyCQA/pylint/blob/main/LICENSE import configparser +import sys from os.path import basename, exists, join +from typing import List, Tuple -def parse_python_version(ver_str): +def parse_python_version(ver_str: str) -> Tuple[int, ...]: + """Convert python version to a tuple of integers for easy comparison.""" return tuple(int(digit) for digit in ver_str.split(".")) @@ -13,6 +16,33 @@ class NoFileError(Exception): pass +if sys.version_info >= (3, 8): + from typing import TypedDict +else: + from typing_extensions import TypedDict + + +class TestFileOptions(TypedDict): + min_pyver: Tuple[int, ...] + max_pyver: Tuple[int, ...] + min_pyver_end_position: Tuple[int, ...] + requires: List[str] + except_implementations: str # Type is actually comma separated list of string + exclude_platforms: str # Type is actually comma separated list of string + + +# mypy need something literal, we can't create this dynamically from TestFileOptions +POSSIBLE_TEST_OPTIONS = { + "min_pyver", + "max_pyver", + "min_pyver_end_position", + "requires", + "except_implementations", + "exclude_platforms", + "exclude_platforms", +} + + class FunctionalTestFile: """A single functional test case file with options.""" @@ -23,23 +53,23 @@ class FunctionalTestFile: "requires": lambda s: s.split(","), } - def __init__(self, directory, filename): + def __init__(self, directory: str, filename: str) -> None: self._directory = directory self.base = filename.replace(".py", "") - self.options = { + self.options: TestFileOptions = { "min_pyver": (2, 5), "max_pyver": (4, 0), "min_pyver_end_position": (3, 8), "requires": [], - "except_implementations": [], - "exclude_platforms": [], + "except_implementations": "", + "exclude_platforms": "", } self._parse_options() - def __repr__(self): + def __repr__(self) -> str: return f"FunctionalTest:{self.base}" - def _parse_options(self): + def _parse_options(self) -> None: cp = configparser.ConfigParser() cp.add_section("testoptions") try: @@ -49,26 +79,30 @@ class FunctionalTestFile: for name, value in cp.items("testoptions"): conv = self._CONVERTERS.get(name, lambda v: v) - self.options[name] = conv(value) + + assert ( + name in POSSIBLE_TEST_OPTIONS + ), f"[testoptions]' can only contains one of {POSSIBLE_TEST_OPTIONS}" + self.options[name] = conv(value) # type: ignore[misc] @property - def option_file(self): + def option_file(self) -> str: return self._file_type(".rc") @property - def module(self): + def module(self) -> str: package = basename(self._directory) return ".".join([package, self.base]) @property - def expected_output(self): + def expected_output(self) -> str: return self._file_type(".txt", check_exists=False) @property - def source(self): + def source(self) -> str: return self._file_type(".py") - def _file_type(self, ext, check_exists=True): + def _file_type(self, ext: str, check_exists: bool = True) -> str: name = join(self._directory, self.base + ext) if not check_exists or exists(name): return name diff --git a/pylint/testutils/lint_module_test.py b/pylint/testutils/lint_module_test.py index 05d6f4a22..cbdedeecc 100644 --- a/pylint/testutils/lint_module_test.py +++ b/pylint/testutils/lint_module_test.py @@ -17,7 +17,7 @@ from pylint import checkers from pylint.lint import PyLinter from pylint.message.message import Message from pylint.testutils.constants import _EXPECTED_RE, _OPERATORS, UPDATE_OPTION -from pylint.testutils.functional.test_file import ( +from pylint.testutils.functional.test_file import ( # need to import from functional.test_file to avoid cyclic import FunctionalTestFile, NoFileError, parse_python_version, diff --git a/tests/functional/f/fixme.rc b/tests/functional/f/fixme.rc index 6b903c1bd..be1b23458 100644 --- a/tests/functional/f/fixme.rc +++ b/tests/functional/f/fixme.rc @@ -1,3 +1,5 @@ -[testoptions] +[MISCELLANEOUS] +# List of note tags to take in consideration, separated by a comma. notes=XXX,TODO,./TODO +# Regular expression of note tags to take in consideration. notes-rgx=FIXME(?!.*ISSUE-\d+)|TO.*DO diff --git a/tests/functional/r/regression/regression_issue_4631.rc b/tests/functional/r/regression/regression_issue_4631.rc index 44c9b77ee..88cdb85bf 100644 --- a/tests/functional/r/regression/regression_issue_4631.rc +++ b/tests/functional/r/regression/regression_issue_4631.rc @@ -1,2 +1,2 @@ -[testoptions] +[MASTER] limit-inference-results=0 diff --git a/tests/functional/t/too/too_few_public_methods_excluded.rc b/tests/functional/t/too/too_few_public_methods_excluded.rc index 00c025832..b6a2cc937 100644 --- a/tests/functional/t/too/too_few_public_methods_excluded.rc +++ b/tests/functional/t/too/too_few_public_methods_excluded.rc @@ -1,4 +1,4 @@ -[testoptions] +[DESIGN] min-public-methods=10 # to combat inherited methods exclude-too-few-public-methods=json.*,^.*Control$ diff --git a/tests/functional/t/too/too_many_ancestors_ignored_parents.rc b/tests/functional/t/too/too_many_ancestors_ignored_parents.rc index 1d06dad25..aa652704b 100644 --- a/tests/functional/t/too/too_many_ancestors_ignored_parents.rc +++ b/tests/functional/t/too/too_many_ancestors_ignored_parents.rc @@ -1,3 +1,3 @@ -[testoptions] +[DESIGN] max-parents=2 ignored-parents=functional.t.too.too_many_ancestors_ignored_parents.E diff --git a/tests/functional/t/too/too_many_statements.rc b/tests/functional/t/too/too_many_statements.rc index d6d22f237..19e0d5151 100644 --- a/tests/functional/t/too/too_many_statements.rc +++ b/tests/functional/t/too/too_many_statements.rc @@ -1,2 +1,2 @@ -[testoptions] +[DESIGN] max-statements=5 diff --git a/tests/test_functional.py b/tests/test_functional.py index c877db49b..eb923401d 100644 --- a/tests/test_functional.py +++ b/tests/test_functional.py @@ -34,9 +34,9 @@ from pylint import testutils from pylint.testutils import UPDATE_FILE, UPDATE_OPTION from pylint.testutils.functional import ( FunctionalTestFile, + LintModuleOutputUpdate, get_functional_test_files_from_directory, ) -from pylint.testutils.functional.lint_module_output_update import LintModuleOutputUpdate from pylint.utils import HAS_ISORT_5 # TODOs diff --git a/tests/testutils/test_lint_module_output_update.py b/tests/testutils/test_lint_module_output_update.py index 9f581ce76..ad414a9dd 100644 --- a/tests/testutils/test_lint_module_output_update.py +++ b/tests/testutils/test_lint_module_output_update.py @@ -29,7 +29,7 @@ def lint_module_fixture( @pytest.mark.skipif(PY38_PLUS, reason="Requires python 3.7 or lower") def test_not_py38(tmp_path: Path) -> None: - with pytest.raises(RuntimeError, match="new, better AST in python 3.8"): + with pytest.raises(RuntimeError, match="new AST parser"): LintModuleOutputUpdate( test_file=FunctionalTestFile(str(tmp_path), str(tmp_path / "filename.py")) ) -- cgit v1.2.1