diff --git a/README.md b/README.md index 775b7da..8ff5cfe 100644 --- a/README.md +++ b/README.md @@ -34,20 +34,25 @@ option `-p no:mypy-testing`. # Writing Mypy Output Test Cases A mypy test case is a top-level functions decorated with -`@pytest.mark.mypy_testing` in a file name `test_*.py` or -`test_*.mypy-testing`. Note that we use the Python +`@pytest.mark.mypy_testing` in a file named `*.mypy-testing` or in a +pytest test module. `pytest-mypy-testing` follows the pytest logic in +identifying test modules and respects the +[`python_files`](https://docs.pytest.org/en/latest/reference.html#confval-python_files) +config value. + +Note that ``pytest-mypy-testing`` uses the Python [ast](https://docs.python.org/3/library/ast.html) module to parse -candidate files and do not import any file, i.e., the decorator must +candidate files and does not import any file, i.e., the decorator must be exactly named `@pytest.mark.mypy_testing`. -In a `test_*.py` file you may combine both regular pytest test -functions and mypy test functions. A single function can even be both. +In a pytest test module file you may combine both regular pytest test +functions and mypy test functions. A single function can be both. Example: A simple mypy test case could look like this: ``` python @pytest.mark.mypy_testing -def mypy_test_invalid_assginment(): +def mypy_test_invalid_assginment() -> None: foo = "abc" foo = 123 # E: Incompatible types in assignment (expression has type "int", variable has type "str") ``` @@ -81,8 +86,33 @@ decorators are extracted from the ast. # Development * Create and activate a Python virtual environment. -* Install development dependencies by calling `pip install -U -r requirements.txt`. -* Start developing +* Install development dependencies by calling `python -m pip install + -U -r requirements.txt`. +* Start developing. * To run all tests with [tox](https://tox.readthedocs.io/en/latest/), Python 3.6, 3.7 and 3.8 must be available. You might want to look into using [pyenv](https://github.com/pyenv/pyenv). + + +# Changelog + +## v0.0.7 + +* Fix `PYTEST_VERSION_INFO` - by [@blueyed](https://github.com/blueyed) (#8) +* Always pass `--check-untyped-defs` to mypy (#11) +* Respect pytest config `python_files` when identifying pytest test modules (#12) + +## v0.0.6 - add pytest 5.4 support + +* Update the plugin to work with pytest 5.4 (#7) + +## v0.0.5 - CI improvements + +* Make invoke tasks work (partially) on Windows (#6) +* Add an invoke task to run tox environments by selecting globs (e.g., + `inv tox -e py-*`) (#6) +* Use coverage directly for code coverage to get more consistent + parallel run results (#6) +* Use flit fork dflit to make packaging work with `LICENSES` directory + (#6) +* Bump dependencies (#6) diff --git a/src/pytest_mypy_testing/parser.py b/src/pytest_mypy_testing/parser.py index dd83c39..5dc083f 100644 --- a/src/pytest_mypy_testing/parser.py +++ b/src/pytest_mypy_testing/parser.py @@ -103,7 +103,7 @@ def generate_per_line_token_lists(source: str,) -> Iterator[List[tokenize.TokenI i += 1 -def parse_file(filename: str) -> MypyTestFile: +def parse_file(filename: str, config) -> MypyTestFile: """Parse *filename* and return information about mypy test cases.""" filename = os.path.abspath(filename) with open(filename, "r", encoding="utf-8") as f: diff --git a/src/pytest_mypy_testing/plugin.py b/src/pytest_mypy_testing/plugin.py index 3331167..8092070 100644 --- a/src/pytest_mypy_testing/plugin.py +++ b/src/pytest_mypy_testing/plugin.py @@ -109,7 +109,7 @@ def __init__( config = getattr(parent, "config", None) super().__init__(fspath, parent, config, session, nodeid) self.add_marker("mypy") - self.mypy_file = parse_file(self.fspath) + self.mypy_file = parse_file(self.fspath, config=config) self._mypy_result: Optional[MypyResult] = None @classmethod @@ -186,23 +186,20 @@ def _run_mypy(self, filename: str) -> MypyResult: def pytest_collect_file(path: LocalPath, parent): - import builtins - - # Add a reveal_type function to the builtins module - if not hasattr(builtins, "reveal_type"): - setattr(builtins, "reveal_type", lambda x: x) - - if path.ext not in (".mypy-testing", ".py"): - return None # pragma: no cover - if not path.basename.startswith("test_"): - return None # pragma: no cover - - file = PytestMypyFile.from_parent(parent=parent, fspath=path) - - if file.mypy_file.items: - return file - else: - return None + if path.ext == ".mypy-testing" or _is_pytest_test_file(path, parent): + file = PytestMypyFile.from_parent(parent=parent, fspath=path) + if file.mypy_file.items: + return file + return None + + +def _is_pytest_test_file(path: LocalPath, parent): + """Return `True` if *path* is considered to be a pytest test file.""" + # Based on _pytest/python.py::pytest_collect_file + fn_patterns = parent.config.getini("python_files") + ["__init__.py"] + return path.ext == ".py" and ( + parent.session.isinitpath(path) or any(path.fnmatch(pat) for pat in fn_patterns) + ) def pytest_configure(config): @@ -210,9 +207,19 @@ def pytest_configure(config): Register a custom marker for MypyItems, and configure the plugin based on the CLI. """ + _add_reveal_type_to_builtins() + config.addinivalue_line( "markers", "mypy_testing: mark functions to be used for mypy testing." ) config.addinivalue_line( "markers", "mypy: mark mypy tests. Do not add this marker manually!" ) + + +def _add_reveal_type_to_builtins(): + # Add a reveal_type function to the builtins module + import builtins + + if not hasattr(builtins, "reveal_type"): + setattr(builtins, "reveal_type", lambda x: x) diff --git a/tests/test_parser.py b/tests/test_parser.py index 03e447d..b198626 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -4,8 +4,10 @@ import ast import sys from tokenize import COMMENT, ENDMARKER, NAME, NEWLINE, NL, TokenInfo +from unittest.mock import Mock import pytest +from _pytest.config import Config from pytest_mypy_testing.parser import ( MypyTestItem, @@ -87,4 +89,5 @@ def test_mypy_bar(): ) monkeypatch.setattr(sys, "version_info", (3, 7, 5)) - parse_file(str(path)) + config = Mock(spec=Config) + parse_file(str(path), config) diff --git a/tests/test_plugin.py b/tests/test_plugin.py index 779ba0b..900f268 100644 --- a/tests/test_plugin.py +++ b/tests/test_plugin.py @@ -34,7 +34,10 @@ def mk_dummy_parent(tmp_path, filename, content=""): config = Mock(spec=Config) config.rootdir = str(tmp_path) - session = SimpleNamespace(config=config, _initialpaths=[]) + config.getini.return_value = ["test_*.py", "*_test.py"] + session = SimpleNamespace( + config=config, isinitpath=lambda p: True, _initialpaths=[] + ) parent = SimpleNamespace( config=config, session=session,