Skip to content

Commit 5542939

Browse files
authored
Version 3.0.0 release (#123)
* Version 3.0.0 release
1 parent 71ff2d7 commit 5542939

22 files changed

+340
-86
lines changed

.github/workflows/test.yml

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,8 @@ jobs:
1313

1414
strategy:
1515
matrix:
16-
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"]
17-
# TODO: remove `7.1` in the next release
18-
pytest-version: ["~=6.2", "~=7.1", "~=7.2"]
16+
python-version: ["3.8", "3.9", "3.10", "3.11"]
17+
pytest-version: ["~=6.2", "~=7.2"]
1918

2019
steps:
2120
- uses: actions/checkout@v3
@@ -40,7 +39,7 @@ jobs:
4039
- name: Set up Python 3.9
4140
uses: actions/setup-python@v4
4241
with:
43-
python-version: 3.9
42+
python-version: 3.11
4443
- name: Install dependencies
4544
run: |
4645
pip install -U pip setuptools wheel

CHANGELOG.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
11
# Version history
22

33

4-
## WIP
4+
## 3.0.0
55

66
### Features
77

8-
- Add `tox.ini` file
8+
- *Breaking*: Drop python3.7 support
9+
- Add `pyproject.toml` config file support with `--mypy-pyproject-toml-file` option
910

1011
### Bugfixes
1112

12-
- Add `requirements.txt` to `sdist` package
13+
- Add `tox.ini` file to `sdist` package
14+
- Add `requirements.txt` file to `sdist` package
15+
- Add `pyproject.toml` file to `sdist` package
1316

1417

1518
## 2.0.0

MANIFEST.in

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
include mypy.ini
2-
include pytest.ini
31
include requirements.txt
42
include tox.ini
3+
include pyproject.toml
54
graft pytest_mypy_plugins/tests

README.md

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ On top of that, each case must comply to following types:
6868
| `main` | `str` | Portion of the code as if written in `.py` file |
6969
| `files` | `Optional[List[File]]=[]`\* | List of extra files to simulate imports if needed |
7070
| `disable_cache` | `Optional[bool]=False` | Set to `true` disables `mypy` caching |
71-
| `mypy_config` | `Optional[Dict[str, Union[str, int, bool, float]]]={}` | Inline `mypy` configuration, passed directly to `mypy` as `--config-file` option |
71+
| `mypy_config` | `Optional[Dict[str, Union[str, int, bool, float]]]={}` | Inline `mypy` configuration, passed directly to `mypy` as `--config-file` option, possibly joined with `--mypy-pyproject-toml-file` or `--mypy-ini-file` contents if they are passed. By default is treated as `ini`, treated as `toml` only if `--mypy-pyproject-toml-file` is passed |
7272
| `env` | `Optional[Dict[str, str]]={}` | Environmental variables to be provided inside of test run |
7373
| `parametrized` | `Optional[List[Parameter]]=[]`\* | List of parameters, similar to [`@pytest.mark.parametrize`](https://docs.pytest.org/en/stable/parametrize.html) |
7474
| `skip` | `str` | Expression evaluated with following globals set: `sys`, `os`, `pytest` and `platform` |
@@ -170,11 +170,18 @@ Properties that you can parametrize:
170170
mypy-tests:
171171
--mypy-testing-base=MYPY_TESTING_BASE
172172
Base directory for tests to use
173+
--mypy-pyproject-toml-file=MYPY_PYPROJECT_TOML_FILE
174+
Which `pyproject.toml` file to use
175+
as a default config for tests.
176+
Incompatible with `--mypy-ini-file`
173177
--mypy-ini-file=MYPY_INI_FILE
174-
Which .ini file to use as a default config for tests
175-
--mypy-same-process Run in the same process. Useful for debugging, will create problems with import cache
178+
Which `.ini` file to use as a default config for tests.
179+
Incompatible with `--mypy-pyproject-toml-file`
180+
--mypy-same-process Run in the same process. Useful for debugging,
181+
will create problems with import cache
176182
--mypy-extension-hook=MYPY_EXTENSION_HOOK
177-
Fully qualified path to the extension hook function, in case you need custom yaml keys. Has to be top-level.
183+
Fully qualified path to the extension hook function,
184+
in case you need custom yaml keys. Has to be top-level
178185
--mypy-only-local-stub
179186
mypy will ignore errors from site-packages
180187

mypy.ini

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

pyproject.toml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,23 @@
1+
[tool.mypy]
2+
ignore_missing_imports = true
3+
strict_optional = true
4+
no_implicit_optional = true
5+
disallow_any_generics = true
6+
disallow_untyped_defs = true
7+
strict_equality = true
8+
warn_unreachable = true
9+
warn_no_return = true
10+
warn_unused_ignores = true
11+
warn_redundant_casts = true
12+
warn_unused_configs = true
13+
14+
[tool.pytest.ini_options]
15+
python_files = "test_*.py"
16+
addopts = "-s --mypy-extension-hook pytest_mypy_plugins.tests.reveal_type_hook.hook"
17+
118
[tool.black]
219
line-length = 120
20+
target-version = ["py38", "py39", "py310", "py311"]
321

422
[tool.isort]
523
include_trailing_comma = true

pytest.ini

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

pytest_mypy_plugins/collect.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import platform
44
import sys
55
import tempfile
6+
from dataclasses import dataclass
67
from typing import (
78
TYPE_CHECKING,
89
Any,
@@ -28,10 +29,10 @@
2829
from pytest_mypy_plugins.item import YamlTestItem
2930

3031

32+
@dataclass
3133
class File:
32-
def __init__(self, path: str, content: str) -> None:
33-
self.path = path
34-
self.content = content
34+
path: str
35+
content: str
3536

3637

3738
def parse_test_files(test_files: List[Dict[str, Any]]) -> List[File]:
@@ -77,7 +78,7 @@ def construct_mapping(self, node: yaml.MappingNode, deep: bool = False) -> Dict[
7778
mapping = super().construct_mapping(node, deep=deep)
7879
# Add 1 so line numbering starts at 1
7980
starting_line = node.start_mark.line + 1
80-
for title_node, contents_node in node.value:
81+
for title_node, _contents_node in node.value:
8182
if title_node.value == "main":
8283
starting_line = title_node.start_mark.line + 1
8384
mapping["__line__"] = starting_line
@@ -172,7 +173,16 @@ def pytest_addoption(parser: Parser) -> None:
172173
group.addoption(
173174
"--mypy-testing-base", type=str, default=tempfile.gettempdir(), help="Base directory for tests to use"
174175
)
175-
group.addoption("--mypy-ini-file", type=str, help="Which .ini file to use as a default config for tests")
176+
group.addoption(
177+
"--mypy-pyproject-toml-file",
178+
type=str,
179+
help="Which `pyproject.toml` file to use as a default config for tests. Incompatible with `--mypy-ini-file`",
180+
)
181+
group.addoption(
182+
"--mypy-ini-file",
183+
type=str,
184+
help="Which `.ini` file to use as a default config for tests. Incompatible with `--mypy-pyproject-toml-file`",
185+
)
176186
group.addoption(
177187
"--mypy-same-process",
178188
action="store_true",

pytest_mypy_plugins/configs.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
from configparser import ConfigParser
2+
from pathlib import Path
3+
from textwrap import dedent
4+
from typing import Final, Optional
5+
6+
import tomlkit
7+
8+
_TOML_TABLE_NAME: Final = "[tool.mypy]"
9+
10+
11+
def join_ini_configs(base_ini_fpath: Optional[str], additional_mypy_config: str, execution_path: Path) -> Optional[str]:
12+
mypy_ini_config = ConfigParser()
13+
if base_ini_fpath:
14+
mypy_ini_config.read(base_ini_fpath)
15+
if additional_mypy_config:
16+
if "[mypy]" not in additional_mypy_config:
17+
additional_mypy_config = f"[mypy]\n{additional_mypy_config}"
18+
mypy_ini_config.read_string(additional_mypy_config)
19+
20+
if mypy_ini_config.sections():
21+
mypy_config_file_path = execution_path / "mypy.ini"
22+
with mypy_config_file_path.open("w") as f:
23+
mypy_ini_config.write(f)
24+
return str(mypy_config_file_path)
25+
return None
26+
27+
28+
def join_toml_configs(
29+
base_pyproject_toml_fpath: str, additional_mypy_config: str, execution_path: Path
30+
) -> Optional[str]:
31+
if base_pyproject_toml_fpath:
32+
with open(base_pyproject_toml_fpath) as f:
33+
toml_config = tomlkit.parse(f.read())
34+
else:
35+
# Emtpy document with `[tool.mypy` empty table,
36+
# useful for overrides further.
37+
toml_config = tomlkit.document()
38+
39+
if "tool" not in toml_config or "mypy" not in toml_config["tool"]: # type: ignore[operator]
40+
tool = tomlkit.table(is_super_table=True)
41+
tool.append("mypy", tomlkit.table())
42+
toml_config.append("tool", tool)
43+
44+
if additional_mypy_config:
45+
if _TOML_TABLE_NAME not in additional_mypy_config:
46+
additional_mypy_config = f"{_TOML_TABLE_NAME}\n{dedent(additional_mypy_config)}"
47+
48+
additional_data = tomlkit.parse(additional_mypy_config)
49+
toml_config["tool"]["mypy"].update( # type: ignore[index, union-attr]
50+
additional_data["tool"]["mypy"].value.items(), # type: ignore[index]
51+
)
52+
53+
mypy_config_file_path = execution_path / "pyproject.toml"
54+
with mypy_config_file_path.open("w") as f:
55+
# We don't want the whole config file, because it can contain
56+
# other sections like `[tool.isort]`, we only need `[tool.mypy]` part.
57+
f.write(f"{_TOML_TABLE_NAME}\n")
58+
f.write(dedent(toml_config["tool"]["mypy"].as_string())) # type: ignore[index]
59+
return str(mypy_config_file_path)

pytest_mypy_plugins/item.py

Lines changed: 31 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,8 @@
44
import subprocess
55
import sys
66
import tempfile
7-
from configparser import ConfigParser
87
from pathlib import Path
9-
from typing import (
10-
TYPE_CHECKING,
11-
Any,
12-
Dict,
13-
List,
14-
Optional,
15-
TextIO,
16-
Tuple,
17-
Union,
18-
no_type_check,
19-
)
8+
from typing import TYPE_CHECKING, Any, Dict, List, Optional, TextIO, Tuple, Union
209

2110
import py
2211
import pytest
@@ -31,7 +20,7 @@
3120
if TYPE_CHECKING:
3221
from _pytest._code.code import _TracebackStyle
3322

34-
from pytest_mypy_plugins import utils
23+
from pytest_mypy_plugins import configs, utils
3524
from pytest_mypy_plugins.collect import File, YamlTestFile
3625
from pytest_mypy_plugins.utils import (
3726
OutputMatcher,
@@ -147,10 +136,19 @@ def __init__(
147136

148137
# config parameters
149138
self.root_directory = self.config.option.mypy_testing_base
139+
140+
# You cannot use both `.ini` and `pyproject.toml` files at the same time:
141+
if self.config.option.mypy_ini_file and self.config.option.mypy_pyproject_toml_file:
142+
raise ValueError("Cannot specify both `--mypy-ini-file` and `--mypy-pyproject-toml-file`")
143+
150144
if self.config.option.mypy_ini_file:
151145
self.base_ini_fpath = os.path.abspath(self.config.option.mypy_ini_file)
152146
else:
153147
self.base_ini_fpath = None
148+
if self.config.option.mypy_pyproject_toml_file:
149+
self.base_pyproject_toml_fpath = os.path.abspath(self.config.option.mypy_pyproject_toml_file)
150+
else:
151+
self.base_pyproject_toml_fpath = None
154152
self.incremental_cache_dir = os.path.join(self.root_directory, ".mypy_cache")
155153

156154
def make_test_file(self, file: File) -> None:
@@ -204,8 +202,7 @@ def typecheck_in_new_subprocess(
204202

205203
completed = subprocess.run(
206204
[mypy_executable, *mypy_cmd_options],
207-
stdout=subprocess.PIPE,
208-
stderr=subprocess.PIPE,
205+
capture_output=True,
209206
cwd=os.getcwd(),
210207
env=self.environment_variables,
211208
)
@@ -314,25 +311,27 @@ def prepare_mypy_cmd_options(self, execution_path: Path) -> List[str]:
314311
if not self.disable_cache:
315312
mypy_cmd_options.extend(["--cache-dir", self.incremental_cache_dir])
316313

317-
# Merge `self.base_ini_fpath` and `self.additional_mypy_config`
318-
# into one file and copy to the typechecking folder:
319-
mypy_ini_config = ConfigParser()
320-
if self.base_ini_fpath:
321-
mypy_ini_config.read(self.base_ini_fpath)
322-
if self.additional_mypy_config:
323-
additional_config = self.additional_mypy_config
324-
if "[mypy]" not in additional_config:
325-
additional_config = "[mypy]\n" + additional_config
326-
mypy_ini_config.read_string(additional_config)
327-
328-
if mypy_ini_config.sections():
329-
mypy_config_file_path = execution_path / "mypy.ini"
330-
with mypy_config_file_path.open("w") as f:
331-
mypy_ini_config.write(f)
332-
mypy_cmd_options.append(f"--config-file={str(mypy_config_file_path)}")
314+
config_file = self.prepare_config_file(execution_path)
315+
if config_file:
316+
mypy_cmd_options.append(f"--config-file={config_file}")
333317

334318
return mypy_cmd_options
335319

320+
def prepare_config_file(self, execution_path: Path) -> Optional[str]:
321+
# Merge (`self.base_ini_fpath` or `base_pyproject_toml_fpath`)
322+
# and `self.additional_mypy_config`
323+
# into one file and copy to the typechecking folder:
324+
if self.base_pyproject_toml_fpath:
325+
return configs.join_toml_configs(
326+
self.base_pyproject_toml_fpath, self.additional_mypy_config, execution_path
327+
)
328+
elif self.base_ini_fpath or self.additional_mypy_config:
329+
# We might have `self.base_ini_fpath` set as well.
330+
# Or this might be a legacy case: only `mypy_config:` is set in the `yaml` test case.
331+
# This means that no real file is provided.
332+
return configs.join_ini_configs(self.base_ini_fpath, self.additional_mypy_config, execution_path)
333+
return None
334+
336335
def repr_failure(
337336
self, excinfo: ExceptionInfo[BaseException], style: Optional["_TracebackStyle"] = None
338337
) -> Union[str, TerminalRepr]:
@@ -357,10 +356,10 @@ def repr_failure(
357356
else:
358357
return super().repr_failure(excinfo, style="native")
359358

360-
@no_type_check
361359
def reportinfo(self) -> Tuple[Union[py.path.local, Path, str], Optional[int], str]:
362360
# To support both Pytest 6.x and 7.x
363361
path = getattr(self, "path", None) or getattr(self, "fspath")
362+
assert path
364363
return path, None, self.name
365364

366365
def _collect_python_path(
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Also used in `test_explicit_configs.py`
2+
3+
- case: custom_mypy_config_strict_optional_true_set
4+
main: |
5+
from typing import Optional
6+
a: Optional[int] = None
7+
a + 1 # should not raise an error
8+
mypy_config: |
9+
strict_optional = false

pytest_mypy_plugins/tests/test-simple-cases.yml

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -60,15 +60,6 @@
6060
a.lower() # E: "int" has no attribute "lower" [attr-defined]
6161
6262
63-
- case: custom_mypy_config_strict_optional_true_set
64-
main: |
65-
from typing import Optional
66-
a: Optional[int] = None
67-
a + 1
68-
mypy_config: |
69-
strict_optional = False
70-
71-
7263
- case: skip_incorrect_test_case
7364
skip: yes
7465
main: |
@@ -101,4 +92,4 @@
10192
a = 'abc'
10293
reveal_type(a)
10394
out: |
104-
main:2: note: Some other message
95+
main:2: note: Some other message
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[mypy]
2+
show_traceback = true
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Empty
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# This file has `[tool.mypy]` existing config
2+
3+
[tool.mypy]
4+
warn_unused_ignores = true
5+
pretty = true
6+
show_error_codes = true
7+
8+
[tool.other]
9+
# This section should not be copied:
10+
key = 'value'

0 commit comments

Comments
 (0)