Run type checks only against packaged *.py files
- for tests/ and the noxfile.py, type annotations are not strictly enforced any more + this simplifies the way test cases and nox sessions are written + for many pytest fixtures, no types are available via a public API - put fixtures inside the classes the corresponding test cases are grouped in
This commit is contained in:
parent
97d714d9ee
commit
8586db58c7
4 changed files with 57 additions and 64 deletions
37
noxfile.py
37
noxfile.py
|
@ -3,7 +3,6 @@
|
|||
import contextlib
|
||||
import os
|
||||
import tempfile
|
||||
from typing import Any, Generator
|
||||
|
||||
import nox
|
||||
from nox.sessions import Session
|
||||
|
@ -14,11 +13,14 @@ MAIN_PYTHON = '3.8'
|
|||
# Keep the project is forward compatible.
|
||||
NEXT_PYTHON = '3.9'
|
||||
|
||||
# Path to the *.py files to be packaged.
|
||||
PACKAGE_SOURCE_LOCATION = 'src/'
|
||||
|
||||
# Path to the test suite.
|
||||
PYTEST_LOCATION = 'tests/'
|
||||
|
||||
# Paths with *.py files.
|
||||
SRC_LOCATIONS = 'noxfile.py', 'src/', PYTEST_LOCATION
|
||||
# Paths with all *.py files.
|
||||
SRC_LOCATIONS = 'noxfile.py', PACKAGE_SOURCE_LOCATION, PYTEST_LOCATION
|
||||
|
||||
|
||||
# Use a unified .cache/ folder for all tools.
|
||||
|
@ -38,7 +40,7 @@ nox.options.sessions = (
|
|||
|
||||
|
||||
@nox.session(name='format', python=MAIN_PYTHON)
|
||||
def format_(session: Session) -> None:
|
||||
def format_(session):
|
||||
"""Format source files with autoflake, black, and isort.
|
||||
|
||||
If no extra arguments are provided, all source files are formatted.
|
||||
|
@ -68,7 +70,7 @@ def format_(session: Session) -> None:
|
|||
|
||||
|
||||
@nox.session(python=MAIN_PYTHON)
|
||||
def lint(session: Session) -> None:
|
||||
def lint(session):
|
||||
"""Lint source files with flake8, mypy, and pylint.
|
||||
|
||||
If no extra arguments are provided, all source files are linted.
|
||||
|
@ -93,8 +95,15 @@ def lint(session: Session) -> None:
|
|||
with _isort_fix(session): # TODO (isort): Remove after upgrading
|
||||
session.run('isort', '--version')
|
||||
session.run('isort', '--check-only', *locations)
|
||||
session.run('mypy', '--version')
|
||||
session.run('mypy', *locations)
|
||||
# For mypy, only lint *.py files to be packaged.
|
||||
mypy_locations = [
|
||||
path for path in locations if path.startswith(PACKAGE_SOURCE_LOCATION)
|
||||
]
|
||||
if mypy_locations:
|
||||
session.run('mypy', '--version')
|
||||
session.run('mypy', *mypy_locations)
|
||||
else:
|
||||
session.log('No paths to be checked with mypy')
|
||||
# Ignore errors where pylint cannot import a third-party package due its
|
||||
# being run in an isolated environment. For the same reason, pylint is
|
||||
# also not able to determine the correct order of imports.
|
||||
|
@ -109,7 +118,7 @@ def lint(session: Session) -> None:
|
|||
|
||||
|
||||
@nox.session(python=[MAIN_PYTHON, NEXT_PYTHON])
|
||||
def test(session: Session) -> None:
|
||||
def test(session):
|
||||
"""Test the code base.
|
||||
|
||||
Runs the unit and integration tests (written with pytest).
|
||||
|
@ -148,7 +157,7 @@ def test(session: Session) -> None:
|
|||
|
||||
|
||||
@nox.session(name='pre-commit', python=MAIN_PYTHON, venv_backend='none')
|
||||
def pre_commit(session: Session) -> None:
|
||||
def pre_commit(session):
|
||||
"""Source files must be well-formed before they enter git.
|
||||
|
||||
Intended to be run as a pre-commit hook.
|
||||
|
@ -165,7 +174,7 @@ def pre_commit(session: Session) -> None:
|
|||
|
||||
|
||||
@nox.session(name='pre-merge', python=MAIN_PYTHON)
|
||||
def pre_merge(session: Session) -> None:
|
||||
def pre_merge(session):
|
||||
"""The test suite must pass before merges are made.
|
||||
|
||||
Intended to be run either as a pre-merge or pre-push hook.
|
||||
|
@ -188,7 +197,7 @@ def pre_merge(session: Session) -> None:
|
|||
test(session)
|
||||
|
||||
|
||||
def _begin(session: Session) -> None:
|
||||
def _begin(session):
|
||||
"""Show generic info about a session."""
|
||||
if session.posargs:
|
||||
# Part of the hack in pre_merge() to "drop" the extra arguments.
|
||||
|
@ -205,9 +214,7 @@ def _begin(session: Session) -> None:
|
|||
print(os.getcwd()) # noqa:WPS421
|
||||
|
||||
|
||||
def _install_packages(
|
||||
session: Session, *packages_or_pip_args: str, **kwargs: Any,
|
||||
) -> None:
|
||||
def _install_packages(session: Session, *packages_or_pip_args: str, **kwargs) -> None:
|
||||
"""Install packages respecting the poetry.lock file.
|
||||
|
||||
This function wraps nox.sessions.Session.install() such that it installs
|
||||
|
@ -251,7 +258,7 @@ def _install_packages(
|
|||
# TODO (isort): Remove this fix after
|
||||
# upgrading to isort ^5.2.2 in pyproject.toml.
|
||||
@contextlib.contextmanager
|
||||
def _isort_fix(session: Session) -> Generator:
|
||||
def _isort_fix(session):
|
||||
"""Temporarily upgrade to isort 5.2.2."""
|
||||
session.install('isort==5.2.2')
|
||||
try:
|
||||
|
|
|
@ -96,6 +96,8 @@ extend-ignore =
|
|||
|
||||
per-file-ignores =
|
||||
noxfile.py:
|
||||
# Type annotations are not strictly enforced.
|
||||
ANN0, ANN2,
|
||||
# TODO (isort): Check if still too many module members.
|
||||
WPS202,
|
||||
# TODO (isort): Remove after simplifying the nox session "lint".
|
||||
|
@ -103,6 +105,8 @@ per-file-ignores =
|
|||
# No overuse of string constants (e.g., '--version').
|
||||
WPS226,
|
||||
tests/*.py:
|
||||
# Type annotations are not strictly enforced.
|
||||
ANN0, ANN2,
|
||||
# `assert` statements are ok in the test suite.
|
||||
S101,
|
||||
# Shadowing outer scopes occurs naturally with mocks.
|
||||
|
@ -177,7 +181,7 @@ single_line_exclusions = typing
|
|||
[mypy]
|
||||
cache_dir = .cache/mypy
|
||||
|
||||
[mypy-nox.*,packaging.*,pytest,_pytest.*]
|
||||
[mypy-nox.*,packaging,pytest]
|
||||
ignore_missing_imports = true
|
||||
|
||||
|
||||
|
|
|
@ -2,19 +2,11 @@
|
|||
|
||||
import click
|
||||
import pytest
|
||||
from _pytest.capture import CaptureFixture # noqa:WPS436
|
||||
from _pytest.monkeypatch import MonkeyPatch # noqa:WPS436
|
||||
from click import testing as click_testing
|
||||
|
||||
from urban_meal_delivery import console
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def ctx() -> click.Context:
|
||||
"""Context around the console.main Command."""
|
||||
return click.Context(console.main)
|
||||
|
||||
|
||||
class TestShowVersion:
|
||||
"""Test console.show_version().
|
||||
|
||||
|
@ -29,7 +21,12 @@ class TestShowVersion:
|
|||
|
||||
# pylint:disable=no-self-use
|
||||
|
||||
def test_no_version(self, capsys: CaptureFixture, ctx: click.Context) -> None:
|
||||
@pytest.fixture
|
||||
def ctx(self) -> click.Context:
|
||||
"""Context around the console.main Command."""
|
||||
return click.Context(console.main)
|
||||
|
||||
def test_no_version(self, capsys, ctx):
|
||||
"""The the early exit branch without any output."""
|
||||
console.show_version(ctx, _param='discarded', value=False)
|
||||
|
||||
|
@ -37,9 +34,7 @@ class TestShowVersion:
|
|||
|
||||
assert captured.out == ''
|
||||
|
||||
def test_final_version(
|
||||
self, capsys: CaptureFixture, ctx: click.Context, monkeypatch: MonkeyPatch,
|
||||
) -> None:
|
||||
def test_final_version(self, capsys, ctx, monkeypatch):
|
||||
"""For final versions, NO "development" warning is emitted."""
|
||||
version = '1.2.3'
|
||||
monkeypatch.setattr(console.urban_meal_delivery, '__version__', version)
|
||||
|
@ -51,9 +46,7 @@ class TestShowVersion:
|
|||
|
||||
assert captured.out.endswith(f', version {version}\n')
|
||||
|
||||
def test_develop_version(
|
||||
self, capsys: CaptureFixture, ctx: click.Context, monkeypatch: MonkeyPatch,
|
||||
) -> None:
|
||||
def test_develop_version(self, capsys, ctx, monkeypatch):
|
||||
"""For develop versions, a warning thereof is emitted."""
|
||||
version = '1.2.3.dev0'
|
||||
monkeypatch.setattr(console.urban_meal_delivery, '__version__', version)
|
||||
|
@ -66,12 +59,6 @@ class TestShowVersion:
|
|||
assert captured.out.strip().endswith(f', version {version} (development)')
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def cli() -> click_testing.CliRunner:
|
||||
"""Initialize Click's CLI Test Runner."""
|
||||
return click_testing.CliRunner()
|
||||
|
||||
|
||||
class TestCLI:
|
||||
"""Test the `umd` CLI utility.
|
||||
|
||||
|
@ -81,8 +68,13 @@ class TestCLI:
|
|||
|
||||
# pylint:disable=no-self-use
|
||||
|
||||
@pytest.fixture
|
||||
def cli(self) -> click_testing.CliRunner:
|
||||
"""Initialize Click's CLI Test Runner."""
|
||||
return click_testing.CliRunner()
|
||||
|
||||
@pytest.mark.no_cover
|
||||
def test_no_options(self, cli: click_testing.CliRunner) -> None:
|
||||
def test_no_options(self, cli):
|
||||
"""Exit with 0 status code and no output if run without options."""
|
||||
result = cli.invoke(console.main)
|
||||
|
||||
|
@ -94,12 +86,8 @@ class TestCLI:
|
|||
version_options = ('--version', '-V')
|
||||
|
||||
@pytest.mark.no_cover
|
||||
@pytest.mark.parametrize(
|
||||
'option', version_options,
|
||||
)
|
||||
def test_final_version(
|
||||
self, cli: click_testing.CliRunner, monkeypatch: MonkeyPatch, option: str,
|
||||
) -> None:
|
||||
@pytest.mark.parametrize('option', version_options)
|
||||
def test_final_version(self, cli, monkeypatch, option):
|
||||
"""For final versions, NO "development" warning is emitted."""
|
||||
version = '1.2.3'
|
||||
monkeypatch.setattr(console.urban_meal_delivery, '__version__', version)
|
||||
|
@ -110,12 +98,8 @@ class TestCLI:
|
|||
assert result.output.strip().endswith(f', version {version}')
|
||||
|
||||
@pytest.mark.no_cover
|
||||
@pytest.mark.parametrize(
|
||||
'option', version_options,
|
||||
)
|
||||
def test_develop_version(
|
||||
self, cli: click_testing.CliRunner, monkeypatch: MonkeyPatch, option: str,
|
||||
) -> None:
|
||||
@pytest.mark.parametrize('option', version_options)
|
||||
def test_develop_version(self, cli, monkeypatch, option):
|
||||
"""For develop versions, a warning thereof is emitted."""
|
||||
version = '1.2.3.dev0'
|
||||
monkeypatch.setattr(console.urban_meal_delivery, '__version__', version)
|
||||
|
|
|
@ -13,35 +13,33 @@ import re
|
|||
|
||||
import pytest
|
||||
from packaging import version as pkg_version
|
||||
from packaging.version import Version
|
||||
|
||||
import urban_meal_delivery
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def parsed_version() -> str:
|
||||
"""The packaged version."""
|
||||
return pkg_version.Version(urban_meal_delivery.__version__) # noqa:WPS609
|
||||
|
||||
|
||||
class TestPEP404Compliance:
|
||||
"""Packaged version identifier is PEP440 compliant."""
|
||||
|
||||
# pylint:disable=no-self-use
|
||||
|
||||
def test_parsed_version_has_no_epoch(self, parsed_version: Version) -> None:
|
||||
@pytest.fixture
|
||||
def parsed_version(self) -> str:
|
||||
"""The packaged version."""
|
||||
return pkg_version.Version(urban_meal_delivery.__version__) # noqa:WPS609
|
||||
|
||||
def test_parsed_version_has_no_epoch(self, parsed_version):
|
||||
"""PEP440 compliant subset of semantic versioning: no epoch."""
|
||||
assert parsed_version.epoch == 0
|
||||
|
||||
def test_parsed_version_is_non_local(self, parsed_version: Version) -> None:
|
||||
def test_parsed_version_is_non_local(self, parsed_version):
|
||||
"""PEP440 compliant subset of semantic versioning: no local version."""
|
||||
assert parsed_version.local is None
|
||||
|
||||
def test_parsed_version_is_no_post_release(self, parsed_version: Version) -> None:
|
||||
def test_parsed_version_is_no_post_release(self, parsed_version):
|
||||
"""PEP440 compliant subset of semantic versioning: no post releases."""
|
||||
assert parsed_version.is_postrelease is False
|
||||
|
||||
def test_parsed_version_is_all_public(self, parsed_version: Version) -> None:
|
||||
def test_parsed_version_is_all_public(self, parsed_version):
|
||||
"""PEP440 compliant subset of semantic versioning: all public parts."""
|
||||
assert parsed_version.public == urban_meal_delivery.__version__ # noqa:WPS609
|
||||
|
||||
|
@ -55,7 +53,7 @@ class TestSemanticVersioning:
|
|||
r'^(0|([1-9]\d*))\.(0|([1-9]\d*))\.(0|([1-9]\d*))(\.dev(0|([1-9]\d*)))?$',
|
||||
)
|
||||
|
||||
def test_version_is_semantic(self) -> None:
|
||||
def test_version_is_semantic(self):
|
||||
"""Packaged version follows semantic versioning."""
|
||||
result = self.version_pattern.fullmatch(
|
||||
urban_meal_delivery.__version__, # noqa:WPS609
|
||||
|
@ -79,7 +77,7 @@ class TestSemanticVersioning:
|
|||
'1.2.3.dev10',
|
||||
],
|
||||
)
|
||||
def test_valid_semantic_versioning(self, version: str) -> None:
|
||||
def test_valid_semantic_versioning(self, version):
|
||||
"""Versions follow the x.y.z or x.y.z.devN format."""
|
||||
result = self.version_pattern.fullmatch(version)
|
||||
|
||||
|
@ -109,7 +107,7 @@ class TestSemanticVersioning:
|
|||
'1.2..3',
|
||||
],
|
||||
)
|
||||
def test_invalid_semantic_versioning(self, version: str) -> None:
|
||||
def test_invalid_semantic_versioning(self, version):
|
||||
"""Versions follow the x.y.z or x.y.z.devN format."""
|
||||
result = self.version_pattern.fullmatch(version)
|
||||
|
||||
|
|
Loading…
Reference in a new issue