Skip to content

Commit

Permalink
Version 3.0.0 release (#123)
Browse files Browse the repository at this point in the history
* Version 3.0.0 release
  • Loading branch information
sobolevn authored Jul 25, 2023
1 parent 71ff2d7 commit 5542939
Show file tree
Hide file tree
Showing 22 changed files with 340 additions and 86 deletions.
7 changes: 3 additions & 4 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,8 @@ jobs:

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

steps:
- uses: actions/checkout@v3
Expand All @@ -40,7 +39,7 @@ jobs:
- name: Set up Python 3.9
uses: actions/setup-python@v4
with:
python-version: 3.9
python-version: 3.11
- name: Install dependencies
run: |
pip install -U pip setuptools wheel
Expand Down
9 changes: 6 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
# Version history


## WIP
## 3.0.0

### Features

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

### Bugfixes

- Add `requirements.txt` to `sdist` package
- Add `tox.ini` file to `sdist` package
- Add `requirements.txt` file to `sdist` package
- Add `pyproject.toml` file to `sdist` package


## 2.0.0
Expand Down
3 changes: 1 addition & 2 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
include mypy.ini
include pytest.ini
include requirements.txt
include tox.ini
include pyproject.toml
graft pytest_mypy_plugins/tests
15 changes: 11 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ On top of that, each case must comply to following types:
| `main` | `str` | Portion of the code as if written in `.py` file |
| `files` | `Optional[List[File]]=[]`\* | List of extra files to simulate imports if needed |
| `disable_cache` | `Optional[bool]=False` | Set to `true` disables `mypy` caching |
| `mypy_config` | `Optional[Dict[str, Union[str, int, bool, float]]]={}` | Inline `mypy` configuration, passed directly to `mypy` as `--config-file` option |
| `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 |
| `env` | `Optional[Dict[str, str]]={}` | Environmental variables to be provided inside of test run |
| `parametrized` | `Optional[List[Parameter]]=[]`\* | List of parameters, similar to [`@pytest.mark.parametrize`](https://docs.pytest.org/en/stable/parametrize.html) |
| `skip` | `str` | Expression evaluated with following globals set: `sys`, `os`, `pytest` and `platform` |
Expand Down Expand Up @@ -170,11 +170,18 @@ Properties that you can parametrize:
mypy-tests:
--mypy-testing-base=MYPY_TESTING_BASE
Base directory for tests to use
--mypy-pyproject-toml-file=MYPY_PYPROJECT_TOML_FILE
Which `pyproject.toml` file to use
as a default config for tests.
Incompatible with `--mypy-ini-file`
--mypy-ini-file=MYPY_INI_FILE
Which .ini file to use as a default config for tests
--mypy-same-process Run in the same process. Useful for debugging, will create problems with import cache
Which `.ini` file to use as a default config for tests.
Incompatible with `--mypy-pyproject-toml-file`
--mypy-same-process Run in the same process. Useful for debugging,
will create problems with import cache
--mypy-extension-hook=MYPY_EXTENSION_HOOK
Fully qualified path to the extension hook function, in case you need custom yaml keys. Has to be top-level.
Fully qualified path to the extension hook function,
in case you need custom yaml keys. Has to be top-level
--mypy-only-local-stub
mypy will ignore errors from site-packages

Expand Down
13 changes: 0 additions & 13 deletions mypy.ini

This file was deleted.

18 changes: 18 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,23 @@
[tool.mypy]
ignore_missing_imports = true
strict_optional = true
no_implicit_optional = true
disallow_any_generics = true
disallow_untyped_defs = true
strict_equality = true
warn_unreachable = true
warn_no_return = true
warn_unused_ignores = true
warn_redundant_casts = true
warn_unused_configs = true

[tool.pytest.ini_options]
python_files = "test_*.py"
addopts = "-s --mypy-extension-hook pytest_mypy_plugins.tests.reveal_type_hook.hook"

[tool.black]
line-length = 120
target-version = ["py38", "py39", "py310", "py311"]

[tool.isort]
include_trailing_comma = true
Expand Down
5 changes: 0 additions & 5 deletions pytest.ini

This file was deleted.

20 changes: 15 additions & 5 deletions pytest_mypy_plugins/collect.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import platform
import sys
import tempfile
from dataclasses import dataclass
from typing import (
TYPE_CHECKING,
Any,
Expand All @@ -28,10 +29,10 @@
from pytest_mypy_plugins.item import YamlTestItem


@dataclass
class File:
def __init__(self, path: str, content: str) -> None:
self.path = path
self.content = content
path: str
content: str


def parse_test_files(test_files: List[Dict[str, Any]]) -> List[File]:
Expand Down Expand Up @@ -77,7 +78,7 @@ def construct_mapping(self, node: yaml.MappingNode, deep: bool = False) -> Dict[
mapping = super().construct_mapping(node, deep=deep)
# Add 1 so line numbering starts at 1
starting_line = node.start_mark.line + 1
for title_node, contents_node in node.value:
for title_node, _contents_node in node.value:
if title_node.value == "main":
starting_line = title_node.start_mark.line + 1
mapping["__line__"] = starting_line
Expand Down Expand Up @@ -172,7 +173,16 @@ def pytest_addoption(parser: Parser) -> None:
group.addoption(
"--mypy-testing-base", type=str, default=tempfile.gettempdir(), help="Base directory for tests to use"
)
group.addoption("--mypy-ini-file", type=str, help="Which .ini file to use as a default config for tests")
group.addoption(
"--mypy-pyproject-toml-file",
type=str,
help="Which `pyproject.toml` file to use as a default config for tests. Incompatible with `--mypy-ini-file`",
)
group.addoption(
"--mypy-ini-file",
type=str,
help="Which `.ini` file to use as a default config for tests. Incompatible with `--mypy-pyproject-toml-file`",
)
group.addoption(
"--mypy-same-process",
action="store_true",
Expand Down
59 changes: 59 additions & 0 deletions pytest_mypy_plugins/configs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
from configparser import ConfigParser
from pathlib import Path
from textwrap import dedent
from typing import Final, Optional

import tomlkit

_TOML_TABLE_NAME: Final = "[tool.mypy]"


def join_ini_configs(base_ini_fpath: Optional[str], additional_mypy_config: str, execution_path: Path) -> Optional[str]:
mypy_ini_config = ConfigParser()
if base_ini_fpath:
mypy_ini_config.read(base_ini_fpath)
if additional_mypy_config:
if "[mypy]" not in additional_mypy_config:
additional_mypy_config = f"[mypy]\n{additional_mypy_config}"
mypy_ini_config.read_string(additional_mypy_config)

if mypy_ini_config.sections():
mypy_config_file_path = execution_path / "mypy.ini"
with mypy_config_file_path.open("w") as f:
mypy_ini_config.write(f)
return str(mypy_config_file_path)
return None


def join_toml_configs(
base_pyproject_toml_fpath: str, additional_mypy_config: str, execution_path: Path
) -> Optional[str]:
if base_pyproject_toml_fpath:
with open(base_pyproject_toml_fpath) as f:
toml_config = tomlkit.parse(f.read())
else:
# Emtpy document with `[tool.mypy` empty table,
# useful for overrides further.
toml_config = tomlkit.document()

if "tool" not in toml_config or "mypy" not in toml_config["tool"]: # type: ignore[operator]
tool = tomlkit.table(is_super_table=True)
tool.append("mypy", tomlkit.table())
toml_config.append("tool", tool)

if additional_mypy_config:
if _TOML_TABLE_NAME not in additional_mypy_config:
additional_mypy_config = f"{_TOML_TABLE_NAME}\n{dedent(additional_mypy_config)}"

additional_data = tomlkit.parse(additional_mypy_config)
toml_config["tool"]["mypy"].update( # type: ignore[index, union-attr]
additional_data["tool"]["mypy"].value.items(), # type: ignore[index]
)

mypy_config_file_path = execution_path / "pyproject.toml"
with mypy_config_file_path.open("w") as f:
# We don't want the whole config file, because it can contain
# other sections like `[tool.isort]`, we only need `[tool.mypy]` part.
f.write(f"{_TOML_TABLE_NAME}\n")
f.write(dedent(toml_config["tool"]["mypy"].as_string())) # type: ignore[index]
return str(mypy_config_file_path)
63 changes: 31 additions & 32 deletions pytest_mypy_plugins/item.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,8 @@
import subprocess
import sys
import tempfile
from configparser import ConfigParser
from pathlib import Path
from typing import (
TYPE_CHECKING,
Any,
Dict,
List,
Optional,
TextIO,
Tuple,
Union,
no_type_check,
)
from typing import TYPE_CHECKING, Any, Dict, List, Optional, TextIO, Tuple, Union

import py
import pytest
Expand All @@ -31,7 +20,7 @@
if TYPE_CHECKING:
from _pytest._code.code import _TracebackStyle

from pytest_mypy_plugins import utils
from pytest_mypy_plugins import configs, utils
from pytest_mypy_plugins.collect import File, YamlTestFile
from pytest_mypy_plugins.utils import (
OutputMatcher,
Expand Down Expand Up @@ -147,10 +136,19 @@ def __init__(

# config parameters
self.root_directory = self.config.option.mypy_testing_base

# You cannot use both `.ini` and `pyproject.toml` files at the same time:
if self.config.option.mypy_ini_file and self.config.option.mypy_pyproject_toml_file:
raise ValueError("Cannot specify both `--mypy-ini-file` and `--mypy-pyproject-toml-file`")

if self.config.option.mypy_ini_file:
self.base_ini_fpath = os.path.abspath(self.config.option.mypy_ini_file)
else:
self.base_ini_fpath = None
if self.config.option.mypy_pyproject_toml_file:
self.base_pyproject_toml_fpath = os.path.abspath(self.config.option.mypy_pyproject_toml_file)
else:
self.base_pyproject_toml_fpath = None
self.incremental_cache_dir = os.path.join(self.root_directory, ".mypy_cache")

def make_test_file(self, file: File) -> None:
Expand Down Expand Up @@ -204,8 +202,7 @@ def typecheck_in_new_subprocess(

completed = subprocess.run(
[mypy_executable, *mypy_cmd_options],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
capture_output=True,
cwd=os.getcwd(),
env=self.environment_variables,
)
Expand Down Expand Up @@ -314,25 +311,27 @@ def prepare_mypy_cmd_options(self, execution_path: Path) -> List[str]:
if not self.disable_cache:
mypy_cmd_options.extend(["--cache-dir", self.incremental_cache_dir])

# Merge `self.base_ini_fpath` and `self.additional_mypy_config`
# into one file and copy to the typechecking folder:
mypy_ini_config = ConfigParser()
if self.base_ini_fpath:
mypy_ini_config.read(self.base_ini_fpath)
if self.additional_mypy_config:
additional_config = self.additional_mypy_config
if "[mypy]" not in additional_config:
additional_config = "[mypy]\n" + additional_config
mypy_ini_config.read_string(additional_config)

if mypy_ini_config.sections():
mypy_config_file_path = execution_path / "mypy.ini"
with mypy_config_file_path.open("w") as f:
mypy_ini_config.write(f)
mypy_cmd_options.append(f"--config-file={str(mypy_config_file_path)}")
config_file = self.prepare_config_file(execution_path)
if config_file:
mypy_cmd_options.append(f"--config-file={config_file}")

return mypy_cmd_options

def prepare_config_file(self, execution_path: Path) -> Optional[str]:
# Merge (`self.base_ini_fpath` or `base_pyproject_toml_fpath`)
# and `self.additional_mypy_config`
# into one file and copy to the typechecking folder:
if self.base_pyproject_toml_fpath:
return configs.join_toml_configs(
self.base_pyproject_toml_fpath, self.additional_mypy_config, execution_path
)
elif self.base_ini_fpath or self.additional_mypy_config:
# We might have `self.base_ini_fpath` set as well.
# Or this might be a legacy case: only `mypy_config:` is set in the `yaml` test case.
# This means that no real file is provided.
return configs.join_ini_configs(self.base_ini_fpath, self.additional_mypy_config, execution_path)
return None

def repr_failure(
self, excinfo: ExceptionInfo[BaseException], style: Optional["_TracebackStyle"] = None
) -> Union[str, TerminalRepr]:
Expand All @@ -357,10 +356,10 @@ def repr_failure(
else:
return super().repr_failure(excinfo, style="native")

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

def _collect_python_path(
Expand Down
9 changes: 9 additions & 0 deletions pytest_mypy_plugins/tests/test-mypy-config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Also used in `test_explicit_configs.py`

- case: custom_mypy_config_strict_optional_true_set
main: |
from typing import Optional
a: Optional[int] = None
a + 1 # should not raise an error
mypy_config: |
strict_optional = false
11 changes: 1 addition & 10 deletions pytest_mypy_plugins/tests/test-simple-cases.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,15 +60,6 @@
a.lower() # E: "int" has no attribute "lower" [attr-defined]
- case: custom_mypy_config_strict_optional_true_set
main: |
from typing import Optional
a: Optional[int] = None
a + 1
mypy_config: |
strict_optional = False
- case: skip_incorrect_test_case
skip: yes
main: |
Expand Down Expand Up @@ -101,4 +92,4 @@
a = 'abc'
reveal_type(a)
out: |
main:2: note: Some other message
main:2: note: Some other message
2 changes: 2 additions & 0 deletions pytest_mypy_plugins/tests/test_configs/mypy1.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[mypy]
show_traceback = true
1 change: 1 addition & 0 deletions pytest_mypy_plugins/tests/test_configs/mypy2.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Empty
10 changes: 10 additions & 0 deletions pytest_mypy_plugins/tests/test_configs/pyproject1.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# This file has `[tool.mypy]` existing config

[tool.mypy]
warn_unused_ignores = true
pretty = true
show_error_codes = true

[tool.other]
# This section should not be copied:
key = 'value'
Loading

0 comments on commit 5542939

Please sign in to comment.