Skip to content

Commit 170b21d

Browse files
authored
Fix for #54 - allow regexes when matching expected message text (#55)
* Some refactoring before making the change - use an object for expected output rather than a string. * Simple regex case now working * A spot of clean-up. * Document regex option. * Specify default value for regex flag. * Add test for regexes inb out section. * Add test to ensure that regexes ony run if the flag is switched on. * Allow regexes on specific messages. * Fix regex - single line flag froup needed to be optional. * Add simple failing simple cases. * Add case for mismatching regex.
1 parent 0c887b0 commit 170b21d

File tree

9 files changed

+223
-55
lines changed

9 files changed

+223
-55
lines changed

.github/workflows/test.yml

+3-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@ jobs:
2626
pip install -U pip setuptools wheel
2727
pip install -r dev-requirements.txt
2828
- name: Run tests
29-
run: pytest
29+
run: pytest --ignore-glob="*.shouldfail.yml"
30+
- name: Run test with expected failures
31+
run: pytest pytest_mypy_plugins/tests/*.shouldfail.yml 2>&1 | grep "5 failed"
3032

3133
lint:
3234
runs-on: ubuntu-latest

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,5 @@
44
__pycache__
55
dist/
66
build/
7+
.pytest_cache/
8+
venv/

README.md

+32-10
Original file line numberDiff line numberDiff line change
@@ -52,16 +52,17 @@ You can also specify `PYTHONPATH`, `MYPYPATH`, or any other environment variable
5252
In general each test case is just an element in an array written in a properly formatted `YAML` file.
5353
On top of that, each case must comply to following types:
5454

55-
| Property | Type | Description |
56-
| --------------- | ------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------- |
57-
| `case` | `str` | Name of the test case, complies to `[a-zA-Z0-9]` pattern |
58-
| `main` | `str` | Portion of the code as if written in `.py` file |
59-
| `files` | `Optional[List[File]]=[]`\* | List of extra files to simulate imports if needed |
60-
| `disable_cache` | `Optional[bool]=False` | Set to `true` disables `mypy` caching |
61-
| `mypy_config` | `Optional[Dict[str, Union[str, int, bool, float]]]={}` | Inline `mypy` configuration, passed directly to `mypy` as `--config-file` option |
62-
| `env` | `Optional[Dict[str, str]]={}` | Environmental variables to be provided inside of test run |
63-
| `parametrized` | `Optional[List[Parameter]]=[]`\* | List of parameters, similar to [`@pytest.mark.parametrize`](https://docs.pytest.org/en/stable/parametrize.html) |
64-
| `skip` | `str` | Expression evaluated with following globals set: `sys`, `os`, `pytest` and `platform` |
55+
| Property | Type | Description |
56+
| --------------- | ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------- |
57+
| `case` | `str` | Name of the test case, complies to `[a-zA-Z0-9]` pattern |
58+
| `main` | `str` | Portion of the code as if written in `.py` file |
59+
| `files` | `Optional[List[File]]=[]`\* | List of extra files to simulate imports if needed |
60+
| `disable_cache` | `Optional[bool]=False` | Set to `true` disables `mypy` caching |
61+
| `mypy_config` | `Optional[Dict[str, Union[str, int, bool, float]]]={}` | Inline `mypy` configuration, passed directly to `mypy` as `--config-file` option |
62+
| `env` | `Optional[Dict[str, str]]={}` | Environmental variables to be provided inside of test run |
63+
| `parametrized` | `Optional[List[Parameter]]=[]`\* | List of parameters, similar to [`@pytest.mark.parametrize`](https://docs.pytest.org/en/stable/parametrize.html) |
64+
| `skip` | `str` | Expression evaluated with following globals set: `sys`, `os`, `pytest` and `platform` |
65+
| `regex` | `str` | Allow regular expressions in comments to be matched against actual output. Defaults to "no", i.e. matches full text.|
6566

6667
(*) Appendix to **pseudo** types used above:
6768

@@ -126,6 +127,27 @@ Implementation notes:
126127
main:1: note: Revealed type is 'builtins.str'
127128
```
128129

130+
#### 4. Regular expressions in expectations
131+
132+
```yaml
133+
- case: expected_message_regex_with_out
134+
regex: yes
135+
main: |
136+
a = 'abc'
137+
reveal_type(a)
138+
out: |
139+
main:2: note: .*str.*
140+
```
141+
142+
#### 5. Regular expressions specific lines of output.
143+
144+
```yaml
145+
- case: expected_single_message_regex
146+
main: |
147+
a = 'hello'
148+
reveal_type(a) # NR: .*str.*
149+
```
150+
129151
## Options
130152

131153
```

pytest_mypy_plugins/collect.py

+10-5
Original file line numberDiff line numberDiff line change
@@ -101,16 +101,21 @@ def collect(self) -> Iterator["YamlTestItem"]:
101101
test_name = f"{test_name_prefix}{test_name_suffix}"
102102
main_file = File(path="main.py", content=pystache.render(raw_test["main"], params))
103103
test_files = [main_file] + parse_test_files(raw_test.get("files", []))
104+
regex = raw_test.get("regex", False)
104105

105-
output_from_comments = []
106+
expected_output = []
106107
for test_file in test_files:
107-
output_lines = utils.extract_errors_from_comments(test_file.path, test_file.content.split("\n"))
108-
output_from_comments.extend(output_lines)
108+
output_lines = utils.extract_output_matchers_from_comments(
109+
test_file.path, test_file.content.split("\n"), regex=regex
110+
)
111+
expected_output.extend(output_lines)
109112

110113
starting_lineno = raw_test["__line__"]
111114
extra_environment_variables = parse_environment_variables(raw_test.get("env", []))
112115
disable_cache = raw_test.get("disable_cache", False)
113-
expected_output_lines = pystache.render(raw_test.get("out", ""), params).split("\n")
116+
expected_output.extend(
117+
utils.extract_output_matchers_from_out(raw_test.get("out", ""), params, regex=regex)
118+
)
114119
additional_mypy_config = raw_test.get("mypy_config", "")
115120

116121
skip = self._eval_skip(str(raw_test.get("skip", "False")))
@@ -122,7 +127,7 @@ def collect(self) -> Iterator["YamlTestItem"]:
122127
starting_lineno=starting_lineno,
123128
environment_variables=extra_environment_variables,
124129
disable_cache=disable_cache,
125-
expected_output_lines=output_from_comments + expected_output_lines,
130+
expected_output=expected_output,
126131
parsed_test_data=raw_test,
127132
mypy_config=additional_mypy_config,
128133
)

pytest_mypy_plugins/item.py

+5-4
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,9 @@
3232
from pytest_mypy_plugins import utils
3333
from pytest_mypy_plugins.collect import File, YamlTestFile
3434
from pytest_mypy_plugins.utils import (
35+
OutputMatcher,
3536
TypecheckAssertionError,
36-
assert_string_arrays_equal,
37+
assert_expected_matched_actual,
3738
capture_std_streams,
3839
fname_to_module,
3940
)
@@ -124,7 +125,7 @@ def __init__(
124125
*,
125126
files: List[File],
126127
starting_lineno: int,
127-
expected_output_lines: List[str],
128+
expected_output: List[OutputMatcher],
128129
environment_variables: Dict[str, Any],
129130
disable_cache: bool,
130131
mypy_config: str,
@@ -134,7 +135,7 @@ def __init__(
134135
self.files = files
135136
self.environment_variables = environment_variables
136137
self.disable_cache = disable_cache
137-
self.expected_output_lines = expected_output_lines
138+
self.expected_output = expected_output
138139
self.starting_lineno = starting_lineno
139140
self.additional_mypy_config = mypy_config
140141
self.parsed_test_data = parsed_test_data
@@ -279,7 +280,7 @@ def runtest(self) -> None:
279280
for line in mypy_output.splitlines():
280281
output_line = replace_fpath_with_module_name(line, rootdir=execution_path)
281282
output_lines.append(output_line)
282-
assert_string_arrays_equal(expected=self.expected_output_lines, actual=output_lines)
283+
assert_expected_matched_actual(expected=self.expected_output, actual=output_lines)
283284
finally:
284285
temp_dir.cleanup()
285286
# remove created modules and all their dependants from cache
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
- case: rexex_but_not_turned_on
2+
main: |
3+
a = 'hello'
4+
reveal_type(a) # N: .*str.*
5+
6+
- case: rexex_but_turned_off
7+
regex: no
8+
main: |
9+
a = 'hello'
10+
reveal_type(a) # N: .*str.*
11+
12+
- case: regext_does_not_match
13+
regex: no
14+
main: |
15+
a = 'hello'
16+
reveal_type(a) # NR: .*banana.*
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
- case: expected_message_regex
2+
regex: yes
3+
main: |
4+
a = 1
5+
b = 'hello'
6+
7+
reveal_type(a) # N: Revealed type is "builtins.int"
8+
reveal_type(b) # N: .*str.*
9+
10+
- case: expected_message_regex_with_out
11+
regex: yes
12+
main: |
13+
a = 'abc'
14+
reveal_type(a)
15+
out: |
16+
main:2: note: .*str.*
17+
18+
- case: expected_single_message_regex
19+
regex: no
20+
main: |
21+
a = 'hello'
22+
reveal_type(a) # NR: .*str.*
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
- case: fail_if_message_does_not_match
2+
main: |
3+
a = 'hello'
4+
reveal_type(a) # N: Some other message
5+
6+
- case: fail_if_message_from_outdoes_not_match
7+
regex: yes
8+
main: |
9+
a = 'abc'
10+
reveal_type(a)
11+
out: |
12+
main:2: note: Some other message

0 commit comments

Comments
 (0)