Skip to content

Commit afc2ce4

Browse files
authored
Merge pull request #15 from George-Ogden/fixtures
Fixture Support
2 parents 71adb5f + 7ec8ffd commit afc2ce4

Some content is hidden

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

51 files changed

+2689
-464
lines changed

.github/workflows/test.yaml

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,16 @@ on:
99
- master
1010

1111
jobs:
12-
test:
12+
unit_tests:
1313
uses: George-Ogden/actions/.github/workflows/python-test.yaml@v3.1.3
1414
with:
1515
python-versions: "['3.13']"
1616
timeout-minutes: 15
17-
pytest-flags: -vv -n auto
17+
pytest-flags: -vv -n auto mypy_pytest_plugin -m "not local_only"
18+
19+
integration_tests:
20+
uses: George-Ogden/actions/.github/workflows/python-test.yaml@v3.1.3
21+
with:
22+
python-versions: "['3.13']"
23+
timeout-minutes: 5
24+
pytest-flags: -vv tests

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,6 @@ repos:
3838
- id: dbg-check
3939
- id: todo-check
4040
- id: mypy
41-
args: [-r, requirements-dev.txt]
41+
args: [-r, requirements-dev.txt, --show-traceback]
4242
exclude: ^test_samples/
4343
- id: spell-check-commit-msgs
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
from collections import Counter
2+
from collections.abc import Iterable
3+
from dataclasses import dataclass
4+
from typing import cast
5+
6+
from mypy.checker import TypeChecker
7+
from mypy.nodes import (
8+
Context,
9+
Expression,
10+
ListExpr,
11+
StrExpr,
12+
TupleExpr,
13+
)
14+
15+
from .error_codes import (
16+
DUPLICATE_ARGNAME,
17+
INVALID_ARGNAME,
18+
UNREADABLE_ARGNAME,
19+
UNREADABLE_ARGNAMES,
20+
)
21+
22+
23+
@dataclass(frozen=True)
24+
class ArgnamesParser:
25+
checker: TypeChecker
26+
27+
def parse_names(self, expression: Expression) -> str | list[str] | None:
28+
match expression:
29+
case StrExpr():
30+
argnames = self.parse_names_string(expression)
31+
case ListExpr() | TupleExpr():
32+
argnames = self.parse_names_sequence(expression)
33+
case _:
34+
self.checker.fail(
35+
"Unable to identify argnames. (Use a comma-separated string, list of strings or tuple of strings).",
36+
context=expression,
37+
code=UNREADABLE_ARGNAMES,
38+
)
39+
return None
40+
argnames = self._check_duplicate_argnames(argnames, expression)
41+
return argnames
42+
43+
def _check_valid_identifier(self, name: str, context: StrExpr) -> bool:
44+
if not (valid_identifier := name.isidentifier()):
45+
self.checker.fail(
46+
f"Invalid identifier {name!r}.",
47+
context=context,
48+
code=INVALID_ARGNAME,
49+
)
50+
return valid_identifier
51+
52+
def parse_names_string(self, expression: StrExpr) -> str | list[str] | None:
53+
individual_names = [name.strip() for name in expression.value.split(",")]
54+
filtered_names = [name for name in individual_names if name]
55+
if any([not self._check_valid_identifier(name, expression) for name in filtered_names]):
56+
return None
57+
if len(filtered_names) == 1:
58+
[name] = filtered_names
59+
return name
60+
return filtered_names
61+
62+
def _parse_name(self, expression: Expression) -> str | None:
63+
if isinstance(expression, StrExpr):
64+
name = expression.value
65+
if self._check_valid_identifier(name, expression):
66+
return name
67+
else:
68+
self.checker.fail(
69+
"Unable to read identifier. (Use a sequence of strings instead.)",
70+
context=expression,
71+
code=UNREADABLE_ARGNAME,
72+
)
73+
return None
74+
75+
def parse_names_sequence(self, node: TupleExpr | ListExpr) -> list[str] | None:
76+
names = [self._parse_name(item) for item in node.items]
77+
if all([isinstance(name, str) for name in names]):
78+
return cast(list[str], names)
79+
return None
80+
81+
def _check_duplicate_argnames(
82+
self, argnames: str | list[str] | None, context: Context
83+
) -> str | list[str] | None:
84+
if isinstance(argnames, list):
85+
return self._check_duplicate_argnames_sequence(argnames, context)
86+
return argnames
87+
88+
def _check_duplicate_argnames_sequence(
89+
self, argnames: list[str], context: Context
90+
) -> None | list[str]:
91+
argname_counts = Counter(argnames)
92+
duplicates = [argname for argname, count in argname_counts.items() if count > 1]
93+
if duplicates:
94+
self._warn_duplicate_argnames(duplicates, context)
95+
return None
96+
return argnames
97+
98+
def _warn_duplicate_argnames(self, duplicates: Iterable[str], context: Context) -> None:
99+
for argname in duplicates:
100+
self.checker.fail(
101+
f"Duplicated argname {argname!r}.",
102+
context=context,
103+
code=DUPLICATE_ARGNAME,
104+
)
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
from collections.abc import Callable
2+
from typing import cast
3+
4+
from mypy.nodes import (
5+
Expression,
6+
)
7+
8+
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+
)
15+
16+
17+
def _argnames_parser_parse_names_custom_test_body[T: Expression](
18+
source: str,
19+
names: str | list[str] | None,
20+
errors: list[str] | None,
21+
parse_names: Callable[[ArgnamesParser, T], str | list[str] | None],
22+
) -> None:
23+
source = f"names = {source}"
24+
parse_result = parse(source)
25+
checker = parse_result.checker
26+
27+
names_node = cast(T, parse_result.defs["names"])
28+
29+
argnames_parser = default_argnames_parser(checker)
30+
31+
assert not checker.errors.is_errors()
32+
assert parse_names(argnames_parser, names_node) == names
33+
34+
messages = get_error_messages(checker)
35+
check_error_messages(messages, errors=errors)
36+
37+
38+
def _argnames_parser_parse_names_string_test_body(
39+
source: str, names: str | list[str] | None, *, errors: list[str] | None = None
40+
) -> None:
41+
_argnames_parser_parse_names_custom_test_body(
42+
source, names, errors, ArgnamesParser.parse_names_string
43+
)
44+
45+
46+
def test_argnames_parser_parse_names_string_empty() -> None:
47+
_argnames_parser_parse_names_string_test_body("''", [])
48+
49+
50+
def test_argnames_parser_parse_names_string_noise_only() -> None:
51+
_argnames_parser_parse_names_string_test_body("',, , , , '", [])
52+
53+
54+
def test_argnames_parser_parse_names_string_one_item() -> None:
55+
_argnames_parser_parse_names_string_test_body("'bar'", "bar")
56+
57+
58+
def test_argnames_parser_parse_names_string_one_item_extra_noise() -> None:
59+
_argnames_parser_parse_names_string_test_body("', foo_8,,, , '", "foo_8")
60+
61+
62+
def test_argnames_parser_parse_names_string_three_items() -> None:
63+
_argnames_parser_parse_names_string_test_body("'a, b_, __c'", ["a", "b_", "__c"])
64+
65+
66+
def test_argnames_parser_parse_names_string_two_items_extra_noise() -> None:
67+
_argnames_parser_parse_names_string_test_body(
68+
"', aa ,b,b, ,,,,,,,,d '", ["aa", "b", "b", "d"]
69+
)
70+
71+
72+
def argnames_parser_parse_names_string_starting_with_number() -> None:
73+
_argnames_parser_parse_names_string_test_body("'8ac'", None, errors=["invalid-argname"])
74+
75+
76+
def test_argnames_parser_parse_names_string_with_space() -> None:
77+
_argnames_parser_parse_names_string_test_body("'aa b'", None, errors=["invalid-argname"])
78+
79+
80+
def test_argnames_parser_parse_names_string_with_invalid_name() -> None:
81+
_argnames_parser_parse_names_string_test_body("'aaa, b b, c'", None, errors=["invalid-argname"])
82+
83+
84+
def test_argnames_parser_parse_names_string_with_multiple_invalid_names() -> None:
85+
_argnames_parser_parse_names_string_test_body(
86+
"'aaa, b b, c-d'", None, errors=["invalid-argname", "invalid-argname"]
87+
)
88+
89+
90+
def _argnames_parser_parse_names_sequence_test_body(
91+
source: str,
92+
names: list[str] | None,
93+
*,
94+
errors: list[str] | None = None,
95+
) -> None:
96+
_argnames_parser_parse_names_custom_test_body(
97+
source, names, errors, ArgnamesParser.parse_names_sequence
98+
)
99+
100+
101+
def test_argnames_parser_parse_names_sequence_empty() -> None:
102+
_argnames_parser_parse_names_sequence_test_body("()", [])
103+
104+
105+
def test_argnames_parser_parse_names_sequence_integer_name() -> None:
106+
_argnames_parser_parse_names_sequence_test_body("[5]", None, errors=["unreadable-argname"])
107+
108+
109+
def test_argnames_parser_parse_names_sequence_one_item() -> None:
110+
_argnames_parser_parse_names_sequence_test_body("['bar']", ["bar"])
111+
112+
113+
def test_argnames_parser_parse_names_sequence_one_item_extra_space() -> None:
114+
_argnames_parser_parse_names_sequence_test_body("['foo ']", None, errors=["invalid-argname"])
115+
116+
117+
def test_argnames_parser_parse_names_sequence_three_items() -> None:
118+
_argnames_parser_parse_names_sequence_test_body("('a', 'b_', '__c_')", ["a", "b_", "__c_"])
119+
120+
121+
def test_argnames_parser_parse_names_sequence_one_starting_with_number() -> None:
122+
_argnames_parser_parse_names_sequence_test_body("['8ac']", None, errors=["invalid-argname"])
123+
124+
125+
def test_argnames_parser_parse_names_sequence_multiple_errors() -> None:
126+
_argnames_parser_parse_names_sequence_test_body(
127+
"('a', 10, '28', f'{5}')",
128+
None,
129+
# unreadable argname message not repeated
130+
errors=["unreadable-argname", "invalid-argname"],
131+
)
132+
133+
134+
def test_argnames_parser_parse_names_sequence_one_int() -> None:
135+
_argnames_parser_parse_names_sequence_test_body(
136+
"('a', 10, 'c')", None, errors=["unreadable-argname"]
137+
)
138+
139+
140+
def test_argnames_parser_parse_names_sequence_one_invalid() -> None:
141+
_argnames_parser_parse_names_sequence_test_body(
142+
"('a', '8ab', 'c')", None, errors=["invalid-argname"]
143+
)
144+
145+
146+
def test_argnames_parser_parse_names_sequence_one_undeterminable() -> None:
147+
_argnames_parser_parse_names_sequence_test_body(
148+
"('a', 'ab'.upper(), 'c')", None, errors=["unreadable-argname"]
149+
)
150+
151+
152+
def _argnames_parser_parse_names_test_body(
153+
source: str,
154+
names: list[str] | str | None,
155+
*,
156+
errors: list[str] | None = None,
157+
) -> None:
158+
_argnames_parser_parse_names_custom_test_body(source, names, errors, ArgnamesParser.parse_names)
159+
160+
161+
def test_argnames_parser_parse_names_one_as_string() -> None:
162+
_argnames_parser_parse_names_test_body("'abc'", "abc")
163+
164+
165+
def test_argnames_parser_parse_names_multiple_as_string() -> None:
166+
_argnames_parser_parse_names_test_body("'a,b,c'", ["a", "b", "c"])
167+
168+
169+
def test_argnames_parser_parse_names_one_as_sequence() -> None:
170+
_argnames_parser_parse_names_test_body("['foo']", ["foo"])
171+
172+
173+
def test_argnames_parser_parse_names_many_as_sequence() -> None:
174+
_argnames_parser_parse_names_test_body("('foo', 'bar')", ["foo", "bar"])
175+
176+
177+
def test_argnames_parser_parse_names_duplicate_name() -> None:
178+
_argnames_parser_parse_names_test_body("'a, b, a'", None, errors=["duplicate-argname"])
179+
180+
181+
def test_argnames_parser_parse_names_invalid_type() -> None:
182+
_argnames_parser_parse_names_test_body("{'a', 'b'}", None, errors=["unreadable-argnames"])

mypy_pytest_plugin/decorator_wrapper.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
@dataclass(frozen=True, slots=True)
1414
class DecoratorWrapper:
15-
node: CallExpr
15+
call: CallExpr
1616
checker: TypeChecker
1717

1818
@classmethod
@@ -40,13 +40,13 @@ def _is_parametrized_decorator_node(
4040
def _get_arg_type(self, i: int) -> Type:
4141
# subtract one for self
4242
i -= 1
43-
return self.node.args[i].accept(self.checker.expr_checker)
43+
return self.call.args[i].accept(self.checker.expr_checker)
4444

4545
@property
4646
def arg_names_and_arg_values(self) -> tuple[Expression, Expression] | None:
4747
mapping = map_actuals_to_formals(
48-
actual_kinds=[ArgKind.ARG_POS, *self.node.arg_kinds],
49-
actual_names=[None, *self.node.arg_names],
48+
actual_kinds=[ArgKind.ARG_POS, *self.call.arg_kinds],
49+
actual_names=[None, *self.call.arg_names],
5050
formal_kinds=self.fn_type.arg_kinds,
5151
formal_names=self.fn_type.arg_names,
5252
actual_arg_type=self._get_arg_type,
@@ -55,7 +55,7 @@ def arg_names_and_arg_values(self) -> tuple[Expression, Expression] | None:
5555

5656
@property
5757
def fn_type(self) -> CallableType:
58-
callee_type = self.node.callee.accept(self.checker.expr_checker)
58+
callee_type = self.call.callee.accept(self.checker.expr_checker)
5959
assert isinstance(callee_type, Instance)
6060
fn_type = callee_type.type.names["__call__"].type
6161
assert isinstance(fn_type, CallableType)
@@ -66,13 +66,13 @@ def _check_actuals_formals_mapping(
6666
) -> tuple[Expression, Expression] | None:
6767
arg_names_idx, arg_values_idx, *_ = self._clean_up_actuals_formals_mapping(mapping)
6868
if (
69-
self.node.arg_kinds[arg_values_idx] in self.accepted_arg_kinds
70-
and self.node.arg_kinds[arg_names_idx] in self.accepted_arg_kinds
69+
self.call.arg_kinds[arg_values_idx] in self.accepted_arg_kinds
70+
and self.call.arg_kinds[arg_names_idx] in self.accepted_arg_kinds
7171
):
72-
return self.node.args[arg_names_idx], self.node.args[arg_values_idx]
72+
return self.call.args[arg_names_idx], self.call.args[arg_values_idx]
7373
self.checker.fail(
7474
"Unable to read argnames and argvalues in a variadic argument.",
75-
context=self.node,
75+
context=self.call,
7676
code=VARIADIC_ARGNAMES_ARGVALUES,
7777
)
7878
return None

mypy_pytest_plugin/defer.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
class DeferralError(Exception): ...

0 commit comments

Comments
 (0)