Skip to content

Commit d69d13a

Browse files
authored
Allow different test output for different Python versions (#10382)
1 parent ab790ea commit d69d13a

File tree

9 files changed

+109
-27
lines changed

9 files changed

+109
-27
lines changed

doc/development_guide/contributor_guide/tests/writing_test.rst

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,32 @@ test runner. The following options are currently supported:
6666
- "except_implementations": List of python implementations on which the test should not run
6767
- "exclude_platforms": List of operating systems on which the test should not run
6868

69+
**Different output for different Python versions**
70+
71+
Sometimes the linting result can change between Python releases. In these cases errors can be marked as conditional.
72+
Supported operators are ``<``, ``<=``, ``>`` and ``>=``.
73+
74+
.. code-block:: python
75+
76+
def some_func() -> X: # <3.14:[undefined-variable]
77+
...
78+
79+
# It can also be combined with offsets
80+
# +1:<3.14:[undefined-variable]
81+
def some_other_func() -> X:
82+
...
83+
84+
class X: ...
85+
86+
Since the output messages are different, it is necessary to add two separate files for it.
87+
First ``<test-file-name>.314.txt``, this will include the output messages for ``>=3.14``, i.e. should be empty here.
88+
Second ``<test-file-name>.txt``, this will be the default for all other Python versions.
89+
90+
.. note::
91+
92+
This does only work if the code itself is parsable in all tested Python versions.
93+
For new syntax, use ``min_pyver`` / ``max_pyver`` instead.
94+
6995
**Functional test file locations**
7096

7197
For existing checkers, new test cases should preferably be appended to the existing test file.

doc/whatsnew/fragments/10382.internal

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Modified test framework to allow for different test output for different Python versions.
2+
3+
Refs #10382

pylint/testutils/functional/test_file.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,13 @@
55
from __future__ import annotations
66

77
import configparser
8+
import sys
89
from collections.abc import Callable
9-
from os.path import basename, exists, join
10-
from typing import TypedDict
10+
from os.path import basename, exists, join, split
11+
from pathlib import Path
12+
from typing import Final, TypedDict
13+
14+
_CURRENT_VERSION: Final = sys.version_info[:2]
1115

1216

1317
def parse_python_version(ver_str: str) -> tuple[int, ...]:
@@ -99,7 +103,20 @@ def module(self) -> str:
99103

100104
@property
101105
def expected_output(self) -> str:
102-
return self._file_type(".txt", check_exists=False)
106+
files = [
107+
p.stem
108+
for p in Path(self._directory).glob(f"{split(self.base)[-1]}.[0-9]*.txt")
109+
]
110+
output_options = [
111+
(int(version[0]), int(version[1:]))
112+
for s in files
113+
if (version := s.rpartition(".")[2]).isalnum()
114+
]
115+
for opt in sorted(output_options, reverse=True):
116+
if _CURRENT_VERSION >= opt:
117+
str_opt = "".join([str(s) for s in opt])
118+
return join(self._directory, f"{self.base}.{str_opt}.txt")
119+
return join(self._directory, self.base + ".txt")
103120

104121
@property
105122
def source(self) -> str:

tests/functional/ext/typing/unnecessary_default_type_args.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@
33
import typing as t
44

55
a1: t.Generator[int, str, str]
6-
a2: t.Generator[int, None, None]
6+
a2: t.Generator[int, None, None] # >=3.13:[unnecessary-default-type-args]
77
a3: t.Generator[int]
88
b1: t.AsyncGenerator[int, str]
9-
b2: t.AsyncGenerator[int, None]
9+
b2: t.AsyncGenerator[int, None] # >=3.13:[unnecessary-default-type-args]
1010
b3: t.AsyncGenerator[int]
1111

1212
c1: ca.Generator[int, str, str]
Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,2 @@
11
[main]
2-
py-version=3.10
32
load-plugins=pylint.extensions.typing

tests/functional/ext/typing/unnecessary_default_type_args_py313.py

Lines changed: 0 additions & 17 deletions
This file was deleted.

tests/functional/ext/typing/unnecessary_default_type_args_py313.rc

Lines changed: 0 additions & 3 deletions
This file was deleted.

tests/testutils/test_functional_testutils.py

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,14 @@
44

55
"""Tests for the functional test framework."""
66

7+
import contextlib
8+
import os
9+
import os.path
10+
import shutil
11+
import tempfile
12+
from collections.abc import Iterator
713
from pathlib import Path
8-
from unittest.mock import MagicMock
14+
from unittest.mock import MagicMock, patch
915

1016
import pytest
1117
from _pytest.outcomes import Skipped
@@ -20,6 +26,26 @@
2026
DATA_DIRECTORY = HERE / "data"
2127

2228

29+
@contextlib.contextmanager
30+
def tempdir() -> Iterator[str]:
31+
"""Create a temp directory and change the current location to it.
32+
33+
This is supposed to be used with a *with* statement.
34+
"""
35+
tmp = tempfile.mkdtemp()
36+
37+
# Get real path of tempfile, otherwise test fail on mac os x
38+
current_dir = os.getcwd()
39+
os.chdir(tmp)
40+
abs_tmp = os.path.abspath(".")
41+
42+
try:
43+
yield abs_tmp
44+
finally:
45+
os.chdir(current_dir)
46+
shutil.rmtree(abs_tmp)
47+
48+
2349
@pytest.fixture(name="pytest_config")
2450
def pytest_config_fixture() -> MagicMock:
2551
def _mock_getoption(option: str) -> bool:
@@ -69,6 +95,37 @@ def test_get_functional_test_files_from_crowded_directory() -> None:
6995
assert "max_overflow" not in str(exc_info.value)
7096

7197

98+
@pytest.mark.parametrize(
99+
["files", "output_file_name"],
100+
[
101+
([], "file.txt"),
102+
(["file.txt"], "file.txt"),
103+
(["file.314.txt"], "file.txt"), # don't match 3.14
104+
(["file.42.txt"], "file.txt"), # don't match 4.2
105+
(["file.32.txt", "file.txt"], "file.32.txt"),
106+
(["file.312.txt", "file.txt"], "file.312.txt"),
107+
(["file.313.txt", "file.txt"], "file.313.txt"),
108+
(["file.310.txt", "file.313.txt", "file.312.txt", "file.txt"], "file.313.txt"),
109+
# don't match other test file names accidentally
110+
([".file.313.txt"], "file.txt"),
111+
(["file_other.313.txt"], "file.txt"),
112+
(["other_file.313.txt"], "file.txt"),
113+
],
114+
)
115+
def test_expected_output_file_matching(files: list[str], output_file_name: str) -> None:
116+
"""Test output file matching. Pin current Python version to 3.13."""
117+
with tempdir():
118+
for file in files:
119+
with open(file, "w", encoding="utf-8"):
120+
...
121+
test_file = FunctionalTestFile(".", "file.py")
122+
with patch(
123+
"pylint.testutils.functional.test_file._CURRENT_VERSION",
124+
new=(3, 13),
125+
):
126+
assert test_file.expected_output == f".{os.path.sep}{output_file_name}"
127+
128+
72129
def test_minimal_messages_config_enabled(pytest_config: MagicMock) -> None:
73130
"""Test that all messages not targeted in the functional test are disabled
74131
when running with --minimal-messages-config.

0 commit comments

Comments
 (0)