Skip to content

Feature/respect python files #12

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 24, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 38 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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")
```
Expand Down Expand Up @@ -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)
2 changes: 1 addition & 1 deletion src/pytest_mypy_testing/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
43 changes: 25 additions & 18 deletions src/pytest_mypy_testing/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -186,33 +186,40 @@ 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):
"""
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)
5 changes: 4 additions & 1 deletion tests/test_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)
5 changes: 4 additions & 1 deletion tests/test_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down