Skip to content

Commit

Permalink
Strict typing and py.typed
Browse files Browse the repository at this point in the history
  • Loading branch information
Avasam committed Aug 28, 2024
1 parent 623f79a commit 0e1896e
Show file tree
Hide file tree
Showing 5 changed files with 66 additions and 29 deletions.
6 changes: 5 additions & 1 deletion mypy.ini
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[mypy]
# Is the project well-typed?
strict = False
strict = True

# Early opt-in even when strict = False
warn_unused_ignores = True
Expand All @@ -12,3 +12,7 @@ explicit_package_bases = True

# Disable overload-overlap due to many false-positives
disable_error_code = overload-overlap

# TODO: Raise issue upstream
[mypy-pytest_cov.*]
ignore_missing_imports = True
4 changes: 0 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,3 @@ pytest11 = {enabler = "pytest_enabler"}


[tool.setuptools_scm]


[tool.pytest-enabler.mypy]
# Disabled due to jaraco/skeleton#143
64 changes: 49 additions & 15 deletions pytest_enabler/__init__.py
Original file line number Diff line number Diff line change
@@ -1,27 +1,46 @@
import contextlib
from __future__ import annotations

import os
import pathlib
import re
import shlex
import sys
from collections.abc import Container, MutableSequence, Sequence
from typing import TYPE_CHECKING, TypeVar, overload

import toml
from jaraco.context import suppress
from jaraco.functools import apply
from pytest import Config, Parser

if sys.version_info > (3, 12):
if sys.version_info >= (3, 12):
from importlib import resources
else:
import importlib_resources as resources

if sys.version_info >= (3, 9):
from importlib.abc import Traversable
else:
from pathlib import Path as Traversable

if TYPE_CHECKING:
from _typeshed import SupportsRead
from typing_extensions import Never

import toml
from jaraco.context import suppress
from jaraco.functools import apply

_T = TypeVar("_T")

consume = tuple
consume = tuple # type: ignore[type-arg] # Generic doesn't matter here and we need to keep it callable
"""
Consume an iterable
"""


def none_as_empty(ob):
@overload
def none_as_empty(ob: None) -> dict[Never, Never]: ...
@overload
def none_as_empty(ob: _T) -> _T: ...
def none_as_empty(ob: _T | None) -> _T | dict[Never, Never]:
"""
>>> none_as_empty({})
{}
Expand All @@ -33,25 +52,31 @@ def none_as_empty(ob):
return ob or {}


def read_plugins_stream(stream):
def read_plugins_stream(
stream: str | bytes | pathlib.PurePath | SupportsRead[str],
) -> dict[str, dict[str, str]]:
defn = toml.load(stream)
return defn["tool"]["pytest-enabler"]
return defn["tool"]["pytest-enabler"] # type: ignore[no-any-return]


@apply(none_as_empty)
@suppress(Exception)
def read_plugins(path):
def read_plugins(path: pathlib.Path | Traversable) -> dict[str, dict[str, str]]:
with path.open(encoding='utf-8') as stream:
return read_plugins_stream(stream)


def pytest_load_initial_conftests(early_config, parser, args):
def pytest_load_initial_conftests(
early_config: Config,
parser: Parser | None,
args: MutableSequence[str],
) -> None:
plugins = {
**read_plugins(resources.files().joinpath('default.toml')),
**read_plugins(pathlib.Path('pyproject.toml')),
}

def _has_plugin(name):
def _has_plugin(name: str) -> bool:
pm = early_config.pluginmanager
return pm.has_plugin(name) or pm.has_plugin('pytest_' + name)

Expand All @@ -61,7 +86,7 @@ def _has_plugin(name):
_pytest_cov_check(enabled, early_config, parser, args)


def _remove_deps():
def _remove_deps() -> None:
"""
Coverage will not detect function definitions as being covered
if the functions are defined before coverage is invoked. As
Expand All @@ -82,7 +107,12 @@ def _remove_deps():
consume(map(sys.modules.__delitem__, to_delete))


def _pytest_cov_check(plugins, early_config, parser, args): # pragma: nocover
def _pytest_cov_check(
plugins: Container[str],
early_config: Config,
parser: Parser | None,
args: Sequence[str | os.PathLike[str]],
) -> None: # pragma: nocover
"""
pytest_cov runs its command-line checks so early that no hooks
can intervene. By now, the hook that installs the plugin has
Expand All @@ -99,9 +129,13 @@ def _pytest_cov_check(plugins, early_config, parser, args): # pragma: nocover
_remove_deps()
# important: parse all known args to ensure pytest-cov can configure
# itself based on other plugins like pytest-xdist (see #1).
if parser is None:
raise ValueError("parser cannot be None if cov in plugins")
parser.parse_known_and_unknown_args(args, early_config.known_args_namespace)

with contextlib.suppress(ImportError):
try:
import pytest_cov.plugin
except ImportError:
pass

pytest_cov.plugin.pytest_load_initial_conftests(early_config, parser, args)
Empty file added pytest_enabler/py.typed
Empty file.
21 changes: 12 additions & 9 deletions tests/test_enabler.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
from __future__ import annotations

import subprocess
import sys
from pathlib import Path
from unittest import mock

import pytest
Expand All @@ -8,52 +11,52 @@


@pytest.fixture
def pyproject(monkeypatch, tmp_path):
def pyproject(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> Path:
monkeypatch.chdir(tmp_path)
return tmp_path / 'pyproject.toml'


def test_pytest_addoption_default():
def test_pytest_addoption_default() -> None:
config = mock.MagicMock()
config.pluginmanager.has_plugin = lambda name: name == 'black'
args = []
args: list[str] = []
enabler.pytest_load_initial_conftests(config, None, args)
assert args == ['--black']


def test_pytest_addoption_override(pyproject):
def test_pytest_addoption_override(pyproject: Path) -> None:
pyproject.write_text(
'[tool.pytest-enabler.black]\naddopts="--black2"\n',
encoding='utf-8',
)
config = mock.MagicMock()
config.pluginmanager.has_plugin = lambda name: name == 'black'
args = []
args: list[str] = []
enabler.pytest_load_initial_conftests(config, None, args)
assert args == ['--black2']


def test_pytest_addoption_disable(pyproject):
def test_pytest_addoption_disable(pyproject: Path) -> None:
pyproject.write_text(
'[tool.pytest-enabler.black]\n#addopts="--black"\n',
encoding='utf-8',
)
config = mock.MagicMock()
config.pluginmanager.has_plugin = lambda name: name == 'black'
args = []
args: list[str] = []
enabler.pytest_load_initial_conftests(config, None, args)
assert args == []


def test_remove_deps(monkeypatch):
def test_remove_deps(monkeypatch: pytest.MonkeyPatch) -> None:
"""
Invoke _remove_deps to push coverage.
"""
monkeypatch.setattr(sys, 'modules', dict(sys.modules))
enabler._remove_deps()


def test_coverage_explicit(tmp_path, monkeypatch):
def test_coverage_explicit(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.chdir(tmp_path)
test = tmp_path.joinpath('test_x.py')
test.write_text('def test_x():\n pass\n', encoding='utf-8')
Expand Down

0 comments on commit 0e1896e

Please sign in to comment.