Skip to content

Commit

Permalink
Merge pull request #18 from Avasam/Strict-typing,-py.typed-and-link-i…
Browse files Browse the repository at this point in the history
…ssues

Strict typing and py.typed
  • Loading branch information
jaraco committed Aug 29, 2024
2 parents 623f79a + c3fa5a2 commit df518e8
Show file tree
Hide file tree
Showing 7 changed files with 87 additions and 30 deletions.
14 changes: 14 additions & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
from __future__ import annotations


extensions = [
'sphinx.ext.autodoc',
'jaraco.packaging.sphinx',
Expand Down Expand Up @@ -30,6 +33,7 @@

# Be strict about any broken references
nitpicky = True
nitpick_ignore: list[tuple[str, str]] = []

# Include Python intersphinx mapping to prevent failures
# jaraco/skeleton#51
Expand All @@ -40,3 +44,13 @@

# Preserve authored syntax for defaults
autodoc_preserve_defaults = True

# local

# jaraco/pytest-enabler#18
nitpick_ignore += [
('py:class', 'pytest_enabler._T'),
('py:class', '_pytest.config.Config'),
('py:class', '_pytest.config.argparsing.Parser'),
('py:class', 'SupportsRead'),
]
11 changes: 8 additions & 3 deletions 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 @@ -10,5 +10,10 @@ enable_error_code = ignore-without-code
# Support namespace packages per https://github.com/python/mypy/issues/14057
explicit_package_bases = True

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

# TODO: Raise issue upstream
[mypy-pytest_cov.*]
ignore_missing_imports = True
1 change: 1 addition & 0 deletions newsfragments/18.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Complete annotations and add ``py.typed`` marker -- by :user:`Avasam`
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
66 changes: 52 additions & 14 deletions pytest_enabler/__init__.py
Original file line number Diff line number Diff line change
@@ -1,27 +1,47 @@
from __future__ import annotations

import contextlib
import os
import pathlib
import re
import shlex
import sys
from collections.abc import Container, MutableSequence, Sequence
from typing import TYPE_CHECKING, TypeVar, cast, 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: # pragma: no cover
from pathlib import Path as Traversable

import toml
from jaraco.context import suppress
from jaraco.functools import apply
if TYPE_CHECKING:
from _typeshed import SupportsRead
from typing_extensions import Never


consume = tuple
_T = TypeVar("_T")

consume = tuple # type: ignore[type-arg] # Generic doesn't matter; 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,35 +53,48 @@ 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: 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)

enabled = {key: plugins[key] for key in plugins if _has_plugin(key)}
for plugin in enabled.values():
args.extend(shlex.split(plugin.get('addopts', "")))
_pytest_cov_check(enabled, early_config, parser, args)
_pytest_cov_check(
enabled,
early_config,
# parser is only used when known not to be None
# based on `enabled` and `early_config`.
cast(Parser, 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 +115,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,
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 Down
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 df518e8

Please sign in to comment.