diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5a1c30c..ca2274f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -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 @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index c92bc61..64868ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/MANIFEST.in b/MANIFEST.in index ce56634..94d4346 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,5 +1,4 @@ -include mypy.ini -include pytest.ini include requirements.txt include tox.ini +include pyproject.toml graft pytest_mypy_plugins/tests diff --git a/README.md b/README.md index 4be152c..4711f28 100644 --- a/README.md +++ b/README.md @@ -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` | @@ -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 diff --git a/mypy.ini b/mypy.ini deleted file mode 100644 index 9f85b65..0000000 --- a/mypy.ini +++ /dev/null @@ -1,13 +0,0 @@ -[mypy] -ignore_missing_imports = True -# strict checks -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 diff --git a/pyproject.toml b/pyproject.toml index 12c3940..3e845a8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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 diff --git a/pytest.ini b/pytest.ini deleted file mode 100644 index 5a515dc..0000000 --- a/pytest.ini +++ /dev/null @@ -1,5 +0,0 @@ -[pytest] -python_files = test_*.py -addopts = - -s - --mypy-extension-hook pytest_mypy_plugins.tests.reveal_type_hook.hook diff --git a/pytest_mypy_plugins/collect.py b/pytest_mypy_plugins/collect.py index 249c41e..2b33538 100644 --- a/pytest_mypy_plugins/collect.py +++ b/pytest_mypy_plugins/collect.py @@ -3,6 +3,7 @@ import platform import sys import tempfile +from dataclasses import dataclass from typing import ( TYPE_CHECKING, Any, @@ -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]: @@ -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 @@ -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", diff --git a/pytest_mypy_plugins/configs.py b/pytest_mypy_plugins/configs.py new file mode 100644 index 0000000..7595500 --- /dev/null +++ b/pytest_mypy_plugins/configs.py @@ -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) diff --git a/pytest_mypy_plugins/item.py b/pytest_mypy_plugins/item.py index fe76dfe..20914a6 100644 --- a/pytest_mypy_plugins/item.py +++ b/pytest_mypy_plugins/item.py @@ -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 @@ -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, @@ -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: @@ -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, ) @@ -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]: @@ -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( diff --git a/pytest_mypy_plugins/tests/test-mypy-config.yml b/pytest_mypy_plugins/tests/test-mypy-config.yml new file mode 100644 index 0000000..847e6f7 --- /dev/null +++ b/pytest_mypy_plugins/tests/test-mypy-config.yml @@ -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 diff --git a/pytest_mypy_plugins/tests/test-simple-cases.yml b/pytest_mypy_plugins/tests/test-simple-cases.yml index deb220e..c0472dc 100644 --- a/pytest_mypy_plugins/tests/test-simple-cases.yml +++ b/pytest_mypy_plugins/tests/test-simple-cases.yml @@ -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: | @@ -101,4 +92,4 @@ a = 'abc' reveal_type(a) out: | - main:2: note: Some other message \ No newline at end of file + main:2: note: Some other message diff --git a/pytest_mypy_plugins/tests/test_configs/mypy1.ini b/pytest_mypy_plugins/tests/test_configs/mypy1.ini new file mode 100644 index 0000000..4354939 --- /dev/null +++ b/pytest_mypy_plugins/tests/test_configs/mypy1.ini @@ -0,0 +1,2 @@ +[mypy] +show_traceback = true diff --git a/pytest_mypy_plugins/tests/test_configs/mypy2.ini b/pytest_mypy_plugins/tests/test_configs/mypy2.ini new file mode 100644 index 0000000..b7db254 --- /dev/null +++ b/pytest_mypy_plugins/tests/test_configs/mypy2.ini @@ -0,0 +1 @@ +# Empty diff --git a/pytest_mypy_plugins/tests/test_configs/pyproject1.toml b/pytest_mypy_plugins/tests/test_configs/pyproject1.toml new file mode 100644 index 0000000..18da4f3 --- /dev/null +++ b/pytest_mypy_plugins/tests/test_configs/pyproject1.toml @@ -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' diff --git a/pytest_mypy_plugins/tests/test_configs/pyproject2.toml b/pytest_mypy_plugins/tests/test_configs/pyproject2.toml new file mode 100644 index 0000000..5733143 --- /dev/null +++ b/pytest_mypy_plugins/tests/test_configs/pyproject2.toml @@ -0,0 +1 @@ +# This file has no `[tool.mypy]` existing config diff --git a/pytest_mypy_plugins/tests/test_configs/setup1.cfg b/pytest_mypy_plugins/tests/test_configs/setup1.cfg new file mode 100644 index 0000000..4354939 --- /dev/null +++ b/pytest_mypy_plugins/tests/test_configs/setup1.cfg @@ -0,0 +1,2 @@ +[mypy] +show_traceback = true diff --git a/pytest_mypy_plugins/tests/test_configs/setup2.cfg b/pytest_mypy_plugins/tests/test_configs/setup2.cfg new file mode 100644 index 0000000..b7db254 --- /dev/null +++ b/pytest_mypy_plugins/tests/test_configs/setup2.cfg @@ -0,0 +1 @@ +# Empty diff --git a/pytest_mypy_plugins/tests/test_configs/test_join_toml_configs.py b/pytest_mypy_plugins/tests/test_configs/test_join_toml_configs.py new file mode 100644 index 0000000..d1f2ab4 --- /dev/null +++ b/pytest_mypy_plugins/tests/test_configs/test_join_toml_configs.py @@ -0,0 +1,114 @@ +from pathlib import Path +from textwrap import dedent +from typing import Callable, Final, Optional + +import pytest + +from pytest_mypy_plugins.configs import join_toml_configs + +_ADDITIONAL_CONFIG: Final = """ +[tool.mypy] +pretty = true +show_error_codes = false +show_traceback = true +""" + +_ADDITIONAL_CONFIG_NO_TABLE: Final = """ +pretty = true +show_error_codes = false +show_traceback = true +""" + +_PYPROJECT1: Final = str(Path(__file__).parent / "pyproject1.toml") +_PYPROJECT2: Final = str(Path(__file__).parent / "pyproject2.toml") + + +@pytest.fixture +def execution_path(tmpdir_factory: pytest.TempdirFactory) -> Path: + return Path(tmpdir_factory.mktemp("testproject", numbered=True)) + + +_AssertFileContents = Callable[[Optional[str], str], None] + + +@pytest.fixture +def assert_file_contents() -> _AssertFileContents: + def factory(filename: Optional[str], expected: str) -> None: + assert filename + + expected = dedent(expected).strip() + with open(filename) as f: + contents = f.read().strip() + assert contents == expected + + return factory + + +@pytest.mark.parametrize( + "additional_config", + [ + _ADDITIONAL_CONFIG, + _ADDITIONAL_CONFIG_NO_TABLE, + ], +) +def test_join_existing_config( + execution_path: Path, assert_file_contents: _AssertFileContents, additional_config: str +) -> None: + filepath = join_toml_configs(_PYPROJECT1, additional_config, execution_path) + + assert_file_contents( + filepath, + """ + [tool.mypy] + warn_unused_ignores = true + pretty = true + show_error_codes = false + show_traceback = true + """, + ) + + +@pytest.mark.parametrize( + "additional_config", + [ + _ADDITIONAL_CONFIG, + _ADDITIONAL_CONFIG_NO_TABLE, + ], +) +def test_join_missing_config( + execution_path: Path, assert_file_contents: _AssertFileContents, additional_config: str +) -> None: + filepath = join_toml_configs(_PYPROJECT2, additional_config, execution_path) + + assert_file_contents( + filepath, + """ + [tool.mypy] + pretty = true + show_error_codes = false + show_traceback = true + """, + ) + + +def test_join_missing_config1(execution_path: Path, assert_file_contents: _AssertFileContents) -> None: + filepath = join_toml_configs(_PYPROJECT1, "", execution_path) + + assert_file_contents( + filepath, + """ + [tool.mypy] + warn_unused_ignores = true + pretty = true + show_error_codes = true + """, + ) + + +def test_join_missing_config2(execution_path: Path, assert_file_contents: _AssertFileContents) -> None: + filepath = join_toml_configs(_PYPROJECT2, "", execution_path) + + assert_file_contents( + filepath, + "[tool.mypy]", + ) diff --git a/pytest_mypy_plugins/tests/test_explicit_configs.py b/pytest_mypy_plugins/tests/test_explicit_configs.py new file mode 100644 index 0000000..4e5cafd --- /dev/null +++ b/pytest_mypy_plugins/tests/test_explicit_configs.py @@ -0,0 +1,46 @@ +import subprocess +from pathlib import Path +from typing import Final + +import pytest + +_PYPROJECT1: Final = str(Path(__file__).parent / "test_configs" / "pyproject1.toml") +_PYPROJECT2: Final = str(Path(__file__).parent / "test_configs" / "pyproject2.toml") +_MYPYINI1: Final = str(Path(__file__).parent / "test_configs" / "mypy1.ini") +_MYPYINI2: Final = str(Path(__file__).parent / "test_configs" / "mypy2.ini") +_SETUPCFG1: Final = str(Path(__file__).parent / "test_configs" / "setup1.cfg") +_SETUPCFG2: Final = str(Path(__file__).parent / "test_configs" / "setup2.cfg") + +_TEST_FILE: Final = str(Path(__file__).parent / "test-mypy-config.yml") + + +@pytest.mark.parametrize("config_file", [_PYPROJECT1, _PYPROJECT2]) +def test_pyproject_toml(config_file: str) -> None: + subprocess.check_output( + [ + "pytest", + "--mypy-pyproject-toml-file", + config_file, + _TEST_FILE, + ] + ) + + +@pytest.mark.parametrize( + "config_file", + [ + _MYPYINI1, + _MYPYINI2, + _SETUPCFG1, + _SETUPCFG2, + ], +) +def test_ini_files(config_file: str) -> None: + subprocess.check_output( + [ + "pytest", + "--mypy-ini-file", + config_file, + _TEST_FILE, + ] + ) diff --git a/pytest_mypy_plugins/utils.py b/pytest_mypy_plugins/utils.py index 929c7ad..fd32d90 100644 --- a/pytest_mypy_plugins/utils.py +++ b/pytest_mypy_plugins/utils.py @@ -164,8 +164,8 @@ def _add_aligned_message(s1: str, s2: str, error_message: str) -> str: extra = "..." # Write a chunk of both lines, aligned. - error_message += " E: {}{}\n".format(s1[:maxw], extra) - error_message += " A: {}{}\n".format(s2[:maxw], extra) + error_message += f" E: {s1[:maxw]}{extra}\n" + error_message += f" A: {s2[:maxw]}{extra}\n" # Write an indicator character under the different columns. error_message += " " # sys.stderr.write(' ') @@ -208,10 +208,10 @@ def assert_expected_matched_actual(expected: List[OutputMatcher], actual: List[s """ def format_mismatched_line(line: str) -> str: - return " {:<45} (diff)".format(str(line)) + return f" {str(line):<45} (diff)" def format_matched_line(line: str, width: int = 100) -> str: - return " {}...".format(line[:width]) if len(line) > width else " {}".format(line) + return f" {line[:width]}..." if len(line) > width else f" {line}" def format_error_lines(lines: List[str]) -> str: return "\n".join(lines) if lines else " (empty)" diff --git a/setup.py b/setup.py index 79d2403..34cd01b 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ from setuptools import setup -with open("README.md", "r") as f: +with open("README.md") as f: readme = f.read() dependencies = [ @@ -11,11 +11,12 @@ "Jinja2", "regex", "packaging", + "tomlkit>=0.11", ] setup( name="pytest-mypy-plugins", - version="2.0.0", + version="3.0.0", description="pytest plugin for writing tests for mypy plugins", long_description=readme, long_description_content_type="text/markdown", @@ -29,14 +30,14 @@ # the following makes a plugin available to pytest entry_points={"pytest11": ["pytest-mypy-plugins = pytest_mypy_plugins.collect"]}, install_requires=dependencies, - python_requires=">=3.7", + python_requires=">=3.8", package_data={ "pytest_mypy_plugins": ["py.typed"], }, classifiers=[ "Development Status :: 4 - Beta", "License :: OSI Approved :: MIT License", - "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10",