Skip to content

Commit

Permalink
Merge pull request #118 from dmtucker/dev
Browse files Browse the repository at this point in the history
Update dev deps and extend static checks
  • Loading branch information
dmtucker committed Mar 15, 2021
2 parents 4066a83 + 98429f7 commit 4802444
Show file tree
Hide file tree
Showing 6 changed files with 270 additions and 234 deletions.
4 changes: 3 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
[build-system]
requires = ['setuptools ~= 45.2.0', 'setuptools-scm ~= 3.5.0', 'wheel ~= 0.34.0']
requires = ['setuptools ~= 50.3.0', 'setuptools-scm[toml] ~= 5.0.0', 'wheel ~= 0.36.0']
build-backend = 'setuptools.build_meta'

[tool.setuptools_scm]
77 changes: 35 additions & 42 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,63 +4,56 @@
import glob
import os
import codecs
from setuptools import setup, find_packages
from setuptools import setup, find_packages # type: ignore


def read(fname):
file_path = os.path.join(os.path.dirname(__file__), fname)
return codecs.open(file_path, encoding='utf-8').read()
return codecs.open(file_path, encoding="utf-8").read()


setup(
name='pytest-mypy',
name="pytest-mypy",
use_scm_version=True,
author='Daniel Bader',
author_email='mail@dbader.org',
maintainer='David Tucker',
maintainer_email='david@tucker.name',
license='MIT',
url='https://github.com/dbader/pytest-mypy',
description='Mypy static type checker plugin for Pytest',
long_description=read('README.rst'),
long_description_content_type='text/x-rst',
packages=find_packages('src'),
package_dir={'': 'src'},
author="Daniel Bader",
author_email="mail@dbader.org",
maintainer="David Tucker",
maintainer_email="david@tucker.name",
license="MIT",
url="https://github.com/dbader/pytest-mypy",
description="Mypy static type checker plugin for Pytest",
long_description=read("README.rst"),
long_description_content_type="text/x-rst",
packages=find_packages("src"),
package_dir={"": "src"},
py_modules=[
os.path.splitext(os.path.basename(path))[0]
for path in glob.glob('src/*.py')
],
python_requires='>=3.5',
setup_requires=[
'setuptools-scm>=3.5',
os.path.splitext(os.path.basename(path))[0] for path in glob.glob("src/*.py")
],
python_requires=">=3.5",
setup_requires=["setuptools-scm>=3.5"],
install_requires=[
'attrs>=19.0',
'filelock>=3.0',
'pytest>=3.5',
"attrs>=19.0",
"filelock>=3.0",
"pytest>=3.5",
'mypy>=0.500; python_version<"3.8"',
'mypy>=0.700; python_version>="3.8" and python_version<"3.9"',
'mypy>=0.780; python_version>="3.9"',
],
classifiers=[
'Development Status :: 4 - Beta',
'Framework :: Pytest',
'Intended Audience :: Developers',
'Topic :: Software Development :: Testing',
'Programming Language :: Python',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: Implementation :: CPython',
'Operating System :: OS Independent',
'License :: OSI Approved :: MIT License',
"Development Status :: 4 - Beta",
"Framework :: Pytest",
"Intended Audience :: Developers",
"Topic :: Software Development :: Testing",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.5",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: Implementation :: CPython",
"Operating System :: OS Independent",
"License :: OSI Approved :: MIT License",
],
entry_points={
'pytest11': [
'mypy = pytest_mypy',
],
},
entry_points={"pytest11": ["mypy = pytest_mypy"]},
)
106 changes: 51 additions & 55 deletions src/pytest_mypy.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,32 +12,32 @@


mypy_argv = []
nodeid_name = 'mypy'
nodeid_name = "mypy"


def default_file_error_formatter(item, results, errors):
"""Create a string to be displayed when mypy finds errors in a file."""
return '\n'.join(errors)
return "\n".join(errors)


file_error_formatter = default_file_error_formatter


def pytest_addoption(parser):
"""Add options for enabling and running mypy."""
group = parser.getgroup('mypy')
group = parser.getgroup("mypy")
group.addoption("--mypy", action="store_true", help="run mypy on .py files")
group.addoption(
'--mypy', action='store_true',
help='run mypy on .py files')
group.addoption(
'--mypy-ignore-missing-imports', action='store_true',
help="suppresses error messages about imports that cannot be resolved")
"--mypy-ignore-missing-imports",
action="store_true",
help="suppresses error messages about imports that cannot be resolved",
)


XDIST_WORKERINPUT_ATTRIBUTE_NAMES = (
'workerinput',
"workerinput",
# xdist < 2.0.0:
'slaveinput',
"slaveinput",
)


Expand Down Expand Up @@ -76,38 +76,38 @@ def pytest_configure(config):

# If xdist is enabled, then the results path should be exposed to
# the workers so that they know where to read parsed results from.
if config.pluginmanager.getplugin('xdist'):
if config.pluginmanager.getplugin("xdist"):

class _MypyXdistPlugin:
def pytest_configure_node(self, node): # xdist hook
"""Pass config._mypy_results_path to workers."""
_get_xdist_workerinput(node)['_mypy_results_path'] = \
node.config._mypy_results_path
_get_xdist_workerinput(node)[
"_mypy_results_path"
] = node.config._mypy_results_path

config.pluginmanager.register(_MypyXdistPlugin())

# pytest_terminal_summary cannot accept config before pytest 4.2.
global _pytest_terminal_summary_config
_pytest_terminal_summary_config = config

config.addinivalue_line(
'markers',
'{marker}: mark tests to be checked by mypy.'.format(
marker=MypyItem.MARKER,
),
"markers",
"{marker}: mark tests to be checked by mypy.".format(marker=MypyItem.MARKER),
)
if config.getoption('--mypy-ignore-missing-imports'):
mypy_argv.append('--ignore-missing-imports')
if config.getoption("--mypy-ignore-missing-imports"):
mypy_argv.append("--ignore-missing-imports")


def pytest_collect_file(path, parent):
"""Create a MypyFileItem for every file mypy should run on."""
if path.ext in {'.py', '.pyi'} and any([
parent.config.option.mypy,
parent.config.option.mypy_ignore_missing_imports,
]):
if path.ext in {".py", ".pyi"} and any(
[parent.config.option.mypy, parent.config.option.mypy_ignore_missing_imports],
):
# Do not create MypyFile instance for a .py file if a
# .pyi file with the same name already exists;
# pytest will complain about duplicate modules otherwise
if path.ext == '.pyi' or not path.new(ext='.pyi').isfile():
if path.ext == ".pyi" or not path.new(ext=".pyi").isfile():
return MypyFile.from_parent(parent=parent, fspath=path)
return None

Expand All @@ -120,17 +120,15 @@ class MypyFile(pytest.File):
def from_parent(cls, *args, **kwargs):
"""Override from_parent for compatibility."""
# pytest.File.from_parent did not exist before pytest 5.4.
return getattr(super(), 'from_parent', cls)(*args, **kwargs)
return getattr(super(), "from_parent", cls)(*args, **kwargs)

def collect(self):
"""Create a MypyFileItem for the File."""
yield MypyFileItem.from_parent(parent=self, name=nodeid_name)
# Since mypy might check files that were not collected,
# pytest could pass even though mypy failed!
# To prevent that, add an explicit check for the mypy exit status.
if not any(
isinstance(item, MypyStatusItem) for item in self.session.items
):
if not any(isinstance(item, MypyStatusItem) for item in self.session.items):
yield MypyStatusItem.from_parent(
parent=self,
name=nodeid_name + "-status",
Expand All @@ -141,7 +139,7 @@ class MypyItem(pytest.Item):

"""A Mypy-related test Item."""

MARKER = 'mypy'
MARKER = "mypy"

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
Expand All @@ -151,7 +149,7 @@ def __init__(self, *args, **kwargs):
def from_parent(cls, *args, **kwargs):
"""Override from_parent for compatibility."""
# pytest.Item.from_parent did not exist before pytest 5.4.
return getattr(super(), 'from_parent', cls)(*args, **kwargs)
return getattr(super(), "from_parent", cls)(*args, **kwargs)

def repr_failure(self, excinfo):
"""
Expand Down Expand Up @@ -193,7 +191,7 @@ def runtest(self):
results = MypyResults.from_session(self.session)
if results.status:
raise MypyError(
'mypy exited with status {status}.'.format(
"mypy exited with status {status}.".format(
status=results.status,
),
)
Expand All @@ -218,33 +216,32 @@ def dump(self, results_f: TextIO) -> None:
return json.dump(vars(self), results_f)

@classmethod
def load(cls, results_f: TextIO) -> 'MypyResults':
def load(cls, results_f: TextIO) -> "MypyResults":
"""Get results cached by dump()."""
return cls(**json.load(results_f))

@classmethod
def from_mypy(
cls,
items: List[MypyFileItem],
*,
opts: Optional[List[str]] = None
) -> 'MypyResults':
cls,
items: List[MypyFileItem],
*,
opts: Optional[List[str]] = None # noqa: C816
) -> "MypyResults":
"""Generate results from mypy."""

if opts is None:
opts = mypy_argv[:]
abspath_errors = {
os.path.abspath(str(item.fspath)): []
for item in items
os.path.abspath(str(item.fspath)): [] for item in items
} # type: MypyResults._abspath_errors_type

stdout, stderr, status = mypy.api.run(opts + list(abspath_errors))

unmatched_lines = []
for line in stdout.split('\n'):
for line in stdout.split("\n"):
if not line:
continue
path, _, error = line.partition(':')
path, _, error = line.partition(":")
abspath = os.path.abspath(path)
try:
abspath_errors[abspath].append(error)
Expand All @@ -257,27 +254,26 @@ def from_mypy(
stderr=stderr,
status=status,
abspath_errors=abspath_errors,
unmatched_stdout='\n'.join(unmatched_lines),
unmatched_stdout="\n".join(unmatched_lines),
)

@classmethod
def from_session(cls, session) -> 'MypyResults':
def from_session(cls, session) -> "MypyResults":
"""Load (or generate) cached mypy results for a pytest session."""
results_path = (
session.config._mypy_results_path
if _is_master(session.config) else
_get_xdist_workerinput(session.config)['_mypy_results_path']
if _is_master(session.config)
else _get_xdist_workerinput(session.config)["_mypy_results_path"]
)
with FileLock(results_path + '.lock'):
with FileLock(results_path + ".lock"):
try:
with open(results_path, mode='r') as results_f:
with open(results_path, mode="r") as results_f:
results = cls.load(results_f)
except FileNotFoundError:
results = cls.from_mypy([
item for item in session.items
if isinstance(item, MypyFileItem)
])
with open(results_path, mode='w') as results_f:
results = cls.from_mypy(
[item for item in session.items if isinstance(item, MypyFileItem)],
)
with open(results_path, mode="w") as results_f:
results.dump(results_f)
return results

Expand All @@ -293,15 +289,15 @@ def pytest_terminal_summary(terminalreporter):
"""Report stderr and unrecognized lines from stdout."""
config = _pytest_terminal_summary_config
try:
with open(config._mypy_results_path, mode='r') as results_f:
with open(config._mypy_results_path, mode="r") as results_f:
results = MypyResults.load(results_f)
except FileNotFoundError:
# No MypyItems executed.
return
if results.unmatched_stdout or results.stderr:
terminalreporter.section('mypy')
terminalreporter.section("mypy")
if results.unmatched_stdout:
color = {'red': True} if results.status else {'green': True}
color = {"red": True} if results.status else {"green": True}
terminalreporter.write_line(results.unmatched_stdout, **color)
if results.stderr:
terminalreporter.write_line(results.stderr, yellow=True)
Expand Down
2 changes: 1 addition & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
pytest_plugins = 'pytester'
pytest_plugins = "pytester"
Loading

0 comments on commit 4802444

Please sign in to comment.