diff --git a/noxfile.py b/noxfile.py index 7204f1a..b743fb8 100644 --- a/noxfile.py +++ b/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: diff --git a/setup.cfg b/setup.cfg index 9099cbc..f275247 100644 --- a/setup.cfg +++ b/setup.cfg @@ -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 diff --git a/tests/test_console.py b/tests/test_console.py index 0b3cba8..00c721f 100644 --- a/tests/test_console.py +++ b/tests/test_console.py @@ -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) diff --git a/tests/test_version.py b/tests/test_version.py index e3bf23c..474b7b1 100644 --- a/tests/test_version.py +++ b/tests/test_version.py @@ -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)