Skip to content

Commit fc972d4

Browse files
authored
Merge pull request #59 from George-Ogden/usefixtures
Support `pytest.mark.usefixtures`
2 parents 942dacf + bb2c8ca commit fc972d4

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+949
-581
lines changed

.mirror.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# DANGER: EDIT AT YOUR OWN RISK. Track this file in version control so that others can sync files correctly.
2-
- commit: 26f0c34dacfaf7ad3d790b9b8a005daf242960b2
2+
- commit: 6b9433e75ab272c0e3cefb8ed56a4144c10986cf
33
files:
44
- .github/python-release.yaml
55
- .github/python-test.yaml

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ ci:
66

77
repos:
88
- repo: https://github.com/George-Ogden/mirror-rorrim/
9-
rev: v0.4.4
9+
rev: v0.4.5
1010
hooks:
1111
- id: mirror-check
1212
fail_fast: true

mypy_pytest_plugin/argmapper_test.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,7 @@ def _named_arg_mapping_test_body(defs: str, expected_keys: list[str]) -> None:
1515

1616
raw_arg_map = ArgMapper.named_arg_mapping(call, parse_result.checker)
1717

18-
def dump_arg_map(
19-
arg_map: dict[str, Expression],
20-
) -> dict[str, tuple[type, dict[str, Any]]]:
18+
def dump_arg_map(arg_map: dict[str, Expression]) -> dict[str, tuple[type, dict[str, Any]]]:
2119
return {key: (dump_expr(expr)) for key, expr in arg_map.items()}
2220

2321
assert dump_arg_map(raw_arg_map) == dump_arg_map(
Lines changed: 39 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,23 @@
11
from collections import Counter
22
from collections.abc import Iterable
33
from dataclasses import dataclass
4-
from typing import cast
4+
from typing import cast, override
55

6-
from mypy.checker import TypeChecker
7-
from mypy.nodes import (
8-
Context,
9-
Expression,
10-
ListExpr,
11-
StrExpr,
12-
TupleExpr,
13-
)
6+
from mypy.nodes import Context, Expression, ListExpr, StrExpr, TupleExpr
147

15-
from .checker_wrapper import CheckerWrapper
168
from .error_codes import (
179
DUPLICATE_ARGNAME,
1810
INVALID_ARGNAME,
1911
REQUEST_KEYWORD,
2012
UNREADABLE_ARGNAME,
2113
UNREADABLE_ARGNAMES,
2214
)
15+
from .names_parser import NamesParser
2316
from .utils import cache_by_id
2417

2518

2619
@dataclass(frozen=True)
27-
class ArgnamesParser(CheckerWrapper):
28-
checker: TypeChecker
29-
20+
class ArgnamesParser(NamesParser):
3021
def __post_init__(self) -> None:
3122
object.__setattr__(self, "parse_names", cache_by_id(self.parse_names))
3223

@@ -37,29 +28,17 @@ def parse_names(self, expression: Expression) -> str | list[str] | None:
3728
case ListExpr() | TupleExpr():
3829
argnames = self.parse_names_sequence(expression)
3930
case _:
40-
self.fail(
41-
"Unable to identify argnames. (Use a comma-separated string, list of strings or tuple of strings).",
42-
context=expression,
43-
code=UNREADABLE_ARGNAMES,
44-
)
31+
self._fail_unreadable_argnames(expression)
4532
return None
4633
argnames = self._check_duplicate_argnames(argnames, expression)
4734
return argnames
4835

49-
def _check_valid_identifier(self, name: str, context: StrExpr) -> bool:
50-
if not (valid_identifier := name.isidentifier()):
51-
self.fail(
52-
f"Invalid identifier {name!r}.",
53-
context=context,
54-
code=INVALID_ARGNAME,
55-
)
56-
elif not (valid_identifier := (name != "request")):
57-
self.fail(
58-
f"{name!r} is not allowed as an argname; it is a reserved word in Pytest.",
59-
context=context,
60-
code=REQUEST_KEYWORD,
61-
)
62-
return valid_identifier
36+
def _fail_unreadable_argnames(self, context: Context) -> None:
37+
self.fail(
38+
"Unable to identify argnames. (Use a comma-separated string, list of strings or tuple of strings).",
39+
context=context,
40+
code=UNREADABLE_ARGNAMES,
41+
)
6342

6443
def parse_names_string(self, expression: StrExpr) -> str | list[str] | None:
6544
individual_names = [name.strip() for name in expression.value.split(",")]
@@ -71,21 +50,8 @@ def parse_names_string(self, expression: StrExpr) -> str | list[str] | None:
7150
return name
7251
return filtered_names
7352

74-
def _parse_name(self, expression: Expression) -> str | None:
75-
if isinstance(expression, StrExpr):
76-
name = expression.value
77-
if self._check_valid_identifier(name, expression):
78-
return name
79-
else:
80-
self.fail(
81-
"Unable to read identifier. (Use a sequence of strings instead.)",
82-
context=expression,
83-
code=UNREADABLE_ARGNAME,
84-
)
85-
return None
86-
8753
def parse_names_sequence(self, expr: TupleExpr | ListExpr) -> list[str] | None:
88-
names = [self._parse_name(item) for item in expr.items]
54+
names = [self.parse_name(item) for item in expr.items]
8955
if all([isinstance(name, str) for name in names]):
9056
return cast(list[str], names)
9157
return None
@@ -109,8 +75,30 @@ def _check_duplicate_argnames_sequence(
10975

11076
def _warn_duplicate_argnames(self, duplicates: Iterable[str], context: Context) -> None:
11177
for argname in duplicates:
112-
self.fail(
113-
f"Duplicated argname {argname!r}.",
114-
context=context,
115-
code=DUPLICATE_ARGNAME,
116-
)
78+
self.fail(f"Duplicated argname {argname!r}.", context=context, code=DUPLICATE_ARGNAME)
79+
80+
@override
81+
def _fail_invalid_identifier(self, name: str, context: Context) -> None:
82+
self.fail(
83+
f"Invalid identifier {name!r} for argname.", context=context, code=INVALID_ARGNAME
84+
)
85+
86+
@override
87+
def _fail_keyword_identifier(self, name: str, context: Context) -> None:
88+
self.fail(f"Keyword {name!r} used as an argname.", context=context, code=INVALID_ARGNAME)
89+
90+
@override
91+
def _fail_unreadable_identifier(self, context: Context) -> None:
92+
self.fail(
93+
"Unable to read argname. (Use string literals instead.)",
94+
context=context,
95+
code=UNREADABLE_ARGNAME,
96+
)
97+
98+
@override
99+
def _fail_request_identifier(self, name: str, context: Context) -> None:
100+
self.fail(
101+
f"{name!r} is not allowed as an argname; it is a reserved word in Pytest.",
102+
context=context,
103+
code=REQUEST_KEYWORD,
104+
)

mypy_pytest_plugin/argnames_parser_test.py

Lines changed: 11 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,10 @@
11
from collections.abc import Callable
22
from typing import cast
33

4-
from mypy.nodes import (
5-
Expression,
6-
)
4+
from mypy.nodes import Expression
75

86
from .argnames_parser import ArgnamesParser
9-
from .test_utils import (
10-
check_error_messages,
11-
default_argnames_parser,
12-
get_error_messages,
13-
parse,
14-
)
7+
from .test_utils import check_error_messages, default_argnames_parser, get_error_messages, parse
158

169

1710
def _argnames_parser_parse_names_custom_test_body[T: Expression](
@@ -22,6 +15,7 @@ def _argnames_parser_parse_names_custom_test_body[T: Expression](
2215
) -> None:
2316
source = f"names = {source}"
2417
parse_result = parse(source)
18+
parse_result.accept_all()
2519
checker = parse_result.checker
2620

2721
names_node = cast(T, parse_result.defs["names"])
@@ -94,10 +88,7 @@ def test_argnames_parser_parse_names_string_with_reserved_name() -> None:
9488

9589

9690
def _argnames_parser_parse_names_sequence_test_body(
97-
source: str,
98-
names: list[str] | None,
99-
*,
100-
errors: list[str] | None = None,
91+
source: str, names: list[str] | None, *, errors: list[str] | None = None
10192
) -> None:
10293
_argnames_parser_parse_names_custom_test_body(
10394
source, names, errors, ArgnamesParser.parse_names_sequence
@@ -149,6 +140,12 @@ def test_argnames_parser_parse_names_sequence_one_invalid() -> None:
149140
)
150141

151142

143+
def test_argnames_parser_parse_names_sequence_multiple_keywords() -> None:
144+
_argnames_parser_parse_names_sequence_test_body(
145+
"('if', 'it', 'is')", None, errors=["invalid-argname", "invalid-argname"]
146+
)
147+
148+
152149
def test_argnames_parser_parse_names_sequence_one_undeterminable() -> None:
153150
_argnames_parser_parse_names_sequence_test_body(
154151
"('a', 'ab'.upper(), 'c')", None, errors=["unreadable-argname"]
@@ -162,10 +159,7 @@ def test_argnames_parser_parse_names_sequence_with_reserved_name() -> None:
162159

163160

164161
def _argnames_parser_parse_names_test_body(
165-
source: str,
166-
names: list[str] | str | None,
167-
*,
168-
errors: list[str] | None = None,
162+
source: str, names: list[str] | str | None, *, errors: list[str] | None = None
169163
) -> None:
170164
_argnames_parser_parse_names_custom_test_body(source, names, errors, ArgnamesParser.parse_names)
171165

mypy_pytest_plugin/checker_wrapper.py

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,7 @@ def named_type(self, fullname: Fullname) -> Instance:
3939
)
4040

4141
def lookup_fullname_type(
42-
self,
43-
fullname: Fullname,
44-
*,
45-
context: Context | None = None,
42+
self, fullname: Fullname, *, context: Context | None = None
4643
) -> Type | None:
4744
result = self.lookup_fullname(
4845
fullname, context=context, predicate=lambda node: hasattr(node, "type")
@@ -77,10 +74,7 @@ def lookup_fullname(
7774
context: Context | None = None,
7875
predicate: None | Callable[[Any], bool] = None,
7976
) -> tuple[MypyFile, Any] | None:
80-
module_name, target = (
81-
Fullname(()),
82-
fullname,
83-
)
77+
module_name, target = (Fullname(()), fullname)
8478
while target:
8579
module_name = module_name.push_back(target.head)
8680
target = target.pop_front()

mypy_pytest_plugin/checker_wrapper_test.py

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -169,16 +169,4 @@ class x:
169169

170170

171171
def test_lookup_fullname_many_nested_modules_exists_as_module() -> None:
172-
_lookup_fullname_type_test_body(
173-
[
174-
(
175-
"test_module",
176-
"",
177-
),
178-
(
179-
"test_module.x",
180-
"",
181-
),
182-
],
183-
"test_module.x",
184-
)
172+
_lookup_fullname_type_test_body([("test_module", ""), ("test_module.x", "")], "test_module.x")

mypy_pytest_plugin/conftest.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,9 @@
1-
from .test_argument import TestArgumentParser
21
from .test_body_ranges import TestBodyRanges
32
from .test_case import TestCase
4-
from .test_info import TestArgument, TestInfo
3+
from .test_info import TestInfo
54
from .test_signature import TestSignature
65

76
TestSignature.__test__ = False # type: ignore
87
TestCase.__test__ = False # type: ignore
98
TestInfo.__test__ = False # type: ignore
10-
TestArgument.__test__ = False # type: ignore
11-
TestArgumentParser.__test__ = False # type: ignore
129
TestBodyRanges.__test__ = False # type: ignore

mypy_pytest_plugin/error_codes.py

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,19 @@
2525
)
2626

2727

28+
INVALID_USEFIXTURES: Final[ErrorCode] = ErrorCode(
29+
"invalid-usefixtures-name",
30+
"Invalid Python identifier used with `pytest.mark.usefixtures`.",
31+
category="Pytest",
32+
)
33+
34+
UNREADABLE_USEFIXTURES: Final[ErrorCode] = ErrorCode(
35+
"unreadable-usefixtures-name",
36+
"Unable to parse fixture name from `pytest.mark.usefixtures`.",
37+
category="Pytest",
38+
)
39+
40+
2841
REPEATED_FIXTURE_ARGNAME: Final[ErrorCode] = ErrorCode(
2942
"repeated-fixture-argname",
3043
"Pytest parametrization contains an argument that is shadowed by a fixture.",
@@ -36,9 +49,7 @@
3649
)
3750

3851
UNREADABLE_ARGNAMES_ARGVALUES: Final[ErrorCode] = ErrorCode(
39-
"unreadable-argnames-argvalues",
40-
"Unable to read argnames or argvalues.",
41-
category="Pytest",
52+
"unreadable-argnames-argvalues", "Unable to read argnames or argvalues.", category="Pytest"
4253
)
4354

4455

@@ -75,15 +86,11 @@
7586
)
7687

7788
DUPLICATE_FIXTURE: Final[ErrorCode] = ErrorCode(
78-
"duplicate-fixture",
79-
"Only one use of `pytest.fixture` is allowed per test.",
80-
category="Pytest",
89+
"duplicate-fixture", "Only one use of `pytest.fixture` is allowed per test.", category="Pytest"
8190
)
8291

8392
MARKED_FIXTURE: Final[ErrorCode] = ErrorCode(
84-
"marked-fixture",
85-
"Do not use `pytest.mark` with `pytest.fixture`.",
86-
category="Pytest",
93+
"marked-fixture", "Do not use `pytest.mark` with `pytest.fixture`.", category="Pytest"
8794
)
8895
INVALID_FIXTURE_AUTOUSE: Final[ErrorCode] = ErrorCode(
8996
"invalid-fixture-autouse",
@@ -117,9 +124,7 @@
117124
)
118125

119126
REQUEST_KEYWORD: Final[ErrorCode] = ErrorCode(
120-
"request-keyword",
121-
""""request" is a reserved word in Pytest.""",
122-
category="Pytest",
127+
"request-keyword", """"request" is a reserved word in Pytest.""", category="Pytest"
123128
)
124129

125130
UNKNOWN_MARK: Final[ErrorCode] = ErrorCode(
@@ -135,9 +140,7 @@
135140
)
136141

137142
TEST_RETURN_TYPE: Final[ErrorCode] = ErrorCode(
138-
"test-return-type",
139-
"Tests must return `None`.",
140-
category="robust-testing",
143+
"test-return-type", "Tests must return `None`.", category="robust-testing"
141144
)
142145

143146
ITERABLE_SEQUENCE: Final[ErrorCode] = ErrorCode(

mypy_pytest_plugin/excluded_test_checker.py

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,7 @@
33
import itertools
44

55
from mypy.checker import TypeChecker
6-
from mypy.nodes import (
7-
AssignmentStmt,
8-
Expression,
9-
MemberExpr,
10-
NameExpr,
11-
Statement,
12-
)
6+
from mypy.nodes import AssignmentStmt, Expression, MemberExpr, NameExpr, Statement
137
from mypy.subtypes import is_same_type
148
from mypy.types import LiteralType
159

@@ -35,7 +29,7 @@ def ignored_test_names(self, defs: Sequence[Statement]) -> set[str]:
3529

3630
def _ignored_test_names_from_statements(self, statements: Sequence[Statement]) -> set[str]:
3731
return self._ignored_test_names_from_assignments(
38-
[statement for statement in statements if isinstance(statement, AssignmentStmt)],
32+
[statement for statement in statements if isinstance(statement, AssignmentStmt)]
3933
)
4034

4135
def _ignored_test_names_from_assignments(

0 commit comments

Comments
 (0)