diff --git a/ChangeLog b/ChangeLog index ce32d1628e..7337fae928 100644 --- a/ChangeLog +++ b/ChangeLog @@ -53,6 +53,9 @@ Release date: TBA trigger a ``DeprecationWarning``. Expected output files can be easily updated with the ``python tests/test_functional.py --update-functional-output`` command. +* The functional test runner now supports the option ``min_pyver_end_position`` to control on which python + versions the ``end_lineno`` and ``end_column`` attributes should be checked. The default value is 3.8. + * Fix ``accept-no-yields-doc`` and ``accept-no-return-doc`` not allowing missing ``yield`` or ``return`` documentation when a docstring is partially correct diff --git a/doc/development_guide/testing.rst b/doc/development_guide/testing.rst index 278294a087..d2fe7e7851 100644 --- a/doc/development_guide/testing.rst +++ b/doc/development_guide/testing.rst @@ -72,6 +72,15 @@ You can also use ``# +n: [`` with n an integer if the above syntax would make th If you need special control over Pylint's configuration, you can also create a .rc file, which can have sections of Pylint's configuration. +The .rc file can also contain a section ``[testoptions]`` to pass options for the functional +test runner. The following options are currently supported: + + "min_pyver": Minimal python version required to run the test + "max_pyver": Maximum python version required to run the test + "min_pyver_end_position": Minimal python version required to check the end_line and end_column attributes of the message + "requires": Packages required to be installed locally to run the test + "except_implementations": List of python implementations on which the test should not run + "exclude_platforms": List of operating systems on which the test should not run During development, it's sometimes helpful to run all functional tests in your current environment in order to have faster feedback. Run from Pylint root directory with:: diff --git a/doc/whatsnew/2.12.rst b/doc/whatsnew/2.12.rst index 1dee53bb88..40374fce00 100644 --- a/doc/whatsnew/2.12.rst +++ b/doc/whatsnew/2.12.rst @@ -132,6 +132,9 @@ Other Changes trigger a ``DeprecationWarning``. Expected output files can be easily updated with the ``python tests/test_functional.py --update-functional-output`` command. +* The functional test runner now supports the option ``min_pyver_end_position`` to control on which python + versions the ``end_lineno`` and ``end_column`` attributes should be checked. The default value is 3.8. + * ``undefined-variable`` now correctly flags variables which only receive a type annotations and never get assigned a value diff --git a/pylint/testutils/functional_test_file.py b/pylint/testutils/functional_test_file.py index bea65394bb..4bf0777f80 100644 --- a/pylint/testutils/functional_test_file.py +++ b/pylint/testutils/functional_test_file.py @@ -19,6 +19,7 @@ class FunctionalTestFile: _CONVERTERS = { "min_pyver": parse_python_version, "max_pyver": parse_python_version, + "min_pyver_end_position": parse_python_version, "requires": lambda s: s.split(","), } @@ -28,6 +29,7 @@ def __init__(self, directory, filename): self.options = { "min_pyver": (2, 5), "max_pyver": (4, 0), + "min_pyver_end_position": (3, 8), "requires": [], "except_implementations": [], "exclude_platforms": [], diff --git a/pylint/testutils/lint_module_test.py b/pylint/testutils/lint_module_test.py index 4ac2397689..859f1c6c94 100644 --- a/pylint/testutils/lint_module_test.py +++ b/pylint/testutils/lint_module_test.py @@ -57,6 +57,9 @@ def __init__( pass self._test_file = test_file self._config = config + self._check_end_position = ( + sys.version_info >= self._test_file.options["min_pyver_end_position"] + ) def setUp(self) -> None: if self._should_be_skipped_due_to_version(): @@ -166,7 +169,8 @@ def _get_expected(self) -> Tuple["MessageCounter", List[OutputLine]]: expected_msgs = Counter() with self._open_expected_file() as f: expected_output_lines = [ - OutputLine.from_csv(row) for row in csv.reader(f, "test") + OutputLine.from_csv(row, self._check_end_position) + for row in csv.reader(f, "test") ] return expected_msgs, expected_output_lines @@ -180,7 +184,9 @@ def _get_actual(self) -> Tuple["MessageCounter", List[OutputLine]]: msg.symbol != "fatal" ), f"Pylint analysis failed because of '{msg.msg}'" received_msgs[msg.line, msg.symbol] += 1 - received_output_lines.append(OutputLine.from_msg(msg)) + received_output_lines.append( + OutputLine.from_msg(msg, self._check_end_position) + ) return received_msgs, received_output_lines def _runTest(self) -> None: diff --git a/pylint/testutils/output_line.py b/pylint/testutils/output_line.py index 61c6f2557b..420b58c882 100644 --- a/pylint/testutils/output_line.py +++ b/pylint/testutils/output_line.py @@ -73,11 +73,11 @@ class OutputLine(NamedTuple): confidence: str @classmethod - def from_msg(cls, msg: Message) -> "OutputLine": + def from_msg(cls, msg: Message, check_endline: bool = True) -> "OutputLine": """Create an OutputLine from a Pylint Message""" column = cls._get_column(msg.column) - end_line = cls._get_py38_none_value(msg.end_line) - end_column = cls._get_py38_none_value(msg.end_column) + end_line = cls._get_py38_none_value(msg.end_line, check_endline) + end_column = cls._get_py38_none_value(msg.end_column, check_endline) return cls( msg.symbol, msg.line, @@ -100,15 +100,17 @@ def _get_column(column: str) -> int: return int(column) @staticmethod - def _get_py38_none_value(value: T) -> Optional[T]: - """Handle attributes that are always None on pylint < 3.8 similar to _get_column.""" - if not PY38_PLUS: - # We check the value only for the new better ast parser introduced in python 3.8 + def _get_py38_none_value(value: T, check_endline: bool) -> Optional[T]: + """Used to make end_line and end_column None as indicated by our version compared to + `min_pyver_end_position`.""" + if not check_endline: return None # pragma: no cover return value @classmethod - def from_csv(cls, row: Union[Sequence[str], str]) -> "OutputLine": + def from_csv( + cls, row: Union[Sequence[str], str], check_endline: bool = True + ) -> "OutputLine": """Create an OutputLine from a comma separated list (the functional tests expected output .txt files). """ @@ -143,8 +145,8 @@ def from_csv(cls, row: Union[Sequence[str], str]) -> "OutputLine": row[0], int(row[1]), column, None, None, row[3], row[4], row[5] ) if len(row) == 8: - end_line = cls._get_py38_none_value(row[3]) - end_column = cls._get_py38_none_value(row[4]) + end_line = cls._get_py38_none_value(row[3], check_endline) + end_column = cls._get_py38_none_value(row[4], check_endline) return cls( row[0], int(row[1]), diff --git a/tests/testutils/test_output_line.py b/tests/testutils/test_output_line.py index 25f784a543..eafb3eb533 100644 --- a/tests/testutils/test_output_line.py +++ b/tests/testutils/test_output_line.py @@ -55,35 +55,66 @@ def test_output_line() -> None: def test_output_line_from_message(message: Callable) -> None: """Test that the OutputLine NamedTuple is instantiated correctly with from_msg.""" expected_column = 2 if PY38_PLUS else 0 - expected_end_lineno = 1 if PY38_PLUS else None - expected_end_column = 3 if PY38_PLUS else None + output_line = OutputLine.from_msg(message()) assert output_line.symbol == "missing-docstring" assert output_line.lineno == 1 assert output_line.column == expected_column - assert output_line.end_lineno == expected_end_lineno - assert output_line.end_column == expected_end_column + assert output_line.end_lineno == 1 + assert output_line.end_column == 3 assert output_line.object == "obj" assert output_line.msg == "msg" assert output_line.confidence == "HIGH" + output_line_with_end = OutputLine.from_msg(message(), True) + assert output_line_with_end.symbol == "missing-docstring" + assert output_line_with_end.lineno == 1 + assert output_line_with_end.column == expected_column + assert output_line_with_end.end_lineno == 1 + assert output_line_with_end.end_column == 3 + assert output_line_with_end.object == "obj" + assert output_line_with_end.msg == "msg" + assert output_line_with_end.confidence == "HIGH" + + output_line_without_end = OutputLine.from_msg(message(), False) + assert output_line_without_end.symbol == "missing-docstring" + assert output_line_without_end.lineno == 1 + assert output_line_without_end.column == expected_column + assert output_line_without_end.end_lineno is None + assert output_line_without_end.end_column is None + assert output_line_without_end.object == "obj" + assert output_line_without_end.msg == "msg" + assert output_line_without_end.confidence == "HIGH" + @pytest.mark.parametrize("confidence", [HIGH, INFERENCE]) def test_output_line_to_csv(confidence: Confidence, message: Callable) -> None: """Test that the OutputLine NamedTuple is instantiated correctly with from_msg and then converted to csv. """ - output_line = OutputLine.from_msg(message(confidence)) + output_line = OutputLine.from_msg(message(confidence), True) csv = output_line.to_csv() expected_column = "2" if PY38_PLUS else "0" - expected_end_lineno = "1" if PY38_PLUS else "None" - expected_end_column = "3" if PY38_PLUS else "None" assert csv == ( "missing-docstring", "1", expected_column, - expected_end_lineno, - expected_end_column, + "1", + "3", + "obj", + "msg", + confidence.name, + ) + + output_line_without_end = OutputLine.from_msg(message(confidence), False) + csv = output_line_without_end.to_csv() + expected_column = "2" if PY38_PLUS else "0" + assert csv == ( + "missing-docstring", + "1", + expected_column, + "None", + "None", "obj", "msg", confidence.name, @@ -96,12 +127,12 @@ def test_output_line_from_csv_error() -> None: MalformedOutputLineException, match="msg-symbolic-name:42:27:MyClass.my_function:The message", ): - OutputLine.from_csv("'missing-docstring', 'line', 'column', 'obj', 'msg'") + OutputLine.from_csv("'missing-docstring', 'line', 'column', 'obj', 'msg'", True) with pytest.raises( MalformedOutputLineException, match="symbol='missing-docstring' ?" ): csv = ("missing-docstring", "line", "column", "obj", "msg") - OutputLine.from_csv(csv) + OutputLine.from_csv(csv, True) @pytest.mark.parametrize( @@ -125,7 +156,7 @@ def test_output_line_from_csv_deprecated( else: proper_csv = ["missing-docstring", "1", "2", "obj", "msg"] with pytest.warns(DeprecationWarning) as records: - output_line = OutputLine.from_csv(proper_csv) + output_line = OutputLine.from_csv(proper_csv, True) assert len(records) == 1 expected_column = 2 if PY38_PLUS else 0 @@ -155,14 +186,36 @@ def test_output_line_from_csv() -> None: "msg", "HIGH", ] - output_line = OutputLine.from_csv(proper_csv) expected_column = 2 if PY38_PLUS else 0 - expected_end_lineno = 1 if PY38_PLUS else None + + output_line = OutputLine.from_csv(proper_csv) assert output_line == OutputLine( symbol="missing-docstring", lineno=1, column=expected_column, - end_lineno=expected_end_lineno, + end_lineno=1, + end_column=None, + object="obj", + msg="msg", + confidence="HIGH", + ) + output_line_with_end = OutputLine.from_csv(proper_csv, True) + assert output_line_with_end == OutputLine( + symbol="missing-docstring", + lineno=1, + column=expected_column, + end_lineno=1, + end_column=None, + object="obj", + msg="msg", + confidence="HIGH", + ) + output_line_without_end = OutputLine.from_csv(proper_csv, False) + assert output_line_without_end == OutputLine( + symbol="missing-docstring", + lineno=1, + column=expected_column, + end_lineno=None, end_column=None, object="obj", msg="msg",