Add CLI entry point umd
- add the following file: + src/urban_meal_delivery/console.py => click-based CLI tools + tests/test_console.py => tests for the module above - add a CLI entry point `umd`: + implement the --version / -V option to show the installed package's version + rework to package's top-level: * add a __pkg_name__ variable to parameterize the package name + add unit and integration tests - fix that pylint cannot know the proper order of imports in the isolated nox session
This commit is contained in:
parent
da233e2e35
commit
97d714d9ee
7 changed files with 196 additions and 15 deletions
15
noxfile.py
15
noxfile.py
|
@ -96,13 +96,16 @@ def lint(session: Session) -> None:
|
|||
session.run('mypy', '--version')
|
||||
session.run('mypy', *locations)
|
||||
# Ignore errors where pylint cannot import a third-party package due its
|
||||
# being run in an isolated environment. One way to fix this is to install
|
||||
# all develop dependencies in this nox session, which we do not do. The
|
||||
# whole point of static linting tools is to not rely on any package be
|
||||
# importable at runtime. Instead, these imports are validated implicitly
|
||||
# when the test suite is run.
|
||||
# being run in an isolated environment. For the same reason, pylint is
|
||||
# also not able to determine the correct order of imports.
|
||||
# One way to fix this is to install all develop dependencies in this nox
|
||||
# session, which we do not do. The whole point of static linting tools is
|
||||
# to not rely on any package be importable at runtime. Instead, these
|
||||
# imports are validated implicitly when the test suite is run.
|
||||
session.run('pylint', '--version')
|
||||
session.run('pylint', '--disable=import-error', *locations)
|
||||
session.run(
|
||||
'pylint', '--disable=import-error', '--disable=wrong-import-order', *locations,
|
||||
)
|
||||
|
||||
|
||||
@nox.session(python=[MAIN_PYTHON, NEXT_PYTHON])
|
||||
|
|
10
poetry.lock
generated
10
poetry.lock
generated
|
@ -127,7 +127,7 @@ python-versions = ">=3.6.1"
|
|||
version = "3.2.0"
|
||||
|
||||
[[package]]
|
||||
category = "dev"
|
||||
category = "main"
|
||||
description = "Composable command line interface toolkit"
|
||||
name = "click"
|
||||
optional = false
|
||||
|
@ -836,7 +836,7 @@ description = "Virtual Python Environment builder"
|
|||
name = "virtualenv"
|
||||
optional = false
|
||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7"
|
||||
version = "20.0.29"
|
||||
version = "20.0.30"
|
||||
|
||||
[package.dependencies]
|
||||
appdirs = ">=1.4.3,<2"
|
||||
|
@ -886,7 +886,7 @@ python-versions = "*"
|
|||
version = "1.12.1"
|
||||
|
||||
[metadata]
|
||||
content-hash = "7a843263817a908ca01d198402d3e9310c33307ac85085f028c8fbdd7587f48f"
|
||||
content-hash = "899f376a54c187e41392fb83831a59926668b662bfb32004f4a0964c1202698c"
|
||||
lock-version = "1.0"
|
||||
python-versions = "^3.8"
|
||||
|
||||
|
@ -1301,8 +1301,8 @@ typing-extensions = [
|
|||
{file = "typing_extensions-3.7.4.2.tar.gz", hash = "sha256:79ee589a3caca649a9bfd2a8de4709837400dfa00b6cc81962a1e6a1815969ae"},
|
||||
]
|
||||
virtualenv = [
|
||||
{file = "virtualenv-20.0.29-py2.py3-none-any.whl", hash = "sha256:8aa9c37b082664dbce2236fa420759c02d64109d8e6013593ad13914718a30fd"},
|
||||
{file = "virtualenv-20.0.29.tar.gz", hash = "sha256:f14a0a98ea4397f0d926cff950361766b6a73cd5975ae7eb259d12919f819a25"},
|
||||
{file = "virtualenv-20.0.30-py2.py3-none-any.whl", hash = "sha256:8cd7b2a4850b003a11be2fc213e206419efab41115cc14bca20e69654f2ac08e"},
|
||||
{file = "virtualenv-20.0.30.tar.gz", hash = "sha256:7b54fd606a1b85f83de49ad8d80dbec08e983a2d2f96685045b262ebc7481ee5"},
|
||||
]
|
||||
wemake-python-styleguide = [
|
||||
{file = "wemake-python-styleguide-0.14.1.tar.gz", hash = "sha256:e13dc580fa56b7b548de8da170bccb8ddff2d4ab026ca987db8a9893bf8a7b5b"},
|
||||
|
|
|
@ -27,6 +27,8 @@ repository = "https://github.com/webartifex/urban-meal-delivery"
|
|||
[tool.poetry.dependencies]
|
||||
python = "^3.8"
|
||||
|
||||
click = "^7.1.2"
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
# Task Runners
|
||||
nox = "^2020.5.24"
|
||||
|
@ -51,3 +53,6 @@ wemake-python-styleguide = "^0.14.1" # flake8 plug-in
|
|||
packaging = "^20.4" # used to test the packaged version
|
||||
pytest = "^6.0.1"
|
||||
pytest-cov = "^2.10.0"
|
||||
|
||||
[tool.poetry.scripts]
|
||||
umd = "urban_meal_delivery.console:main"
|
||||
|
|
|
@ -107,6 +107,8 @@ per-file-ignores =
|
|||
S101,
|
||||
# Shadowing outer scopes occurs naturally with mocks.
|
||||
WPS442,
|
||||
# No overuse of string constants (e.g., '__version__').
|
||||
WPS226,
|
||||
|
||||
# Explicitly set mccabe's maximum complexity to 10 as recommended by
|
||||
# Thomas McCabe, the inventor of the McCabe complexity, and the NIST.
|
||||
|
@ -124,7 +126,9 @@ show-source = true
|
|||
# wemake-python-styleguide's settings
|
||||
# ===================================
|
||||
allowed-domain-names =
|
||||
param,
|
||||
result,
|
||||
value,
|
||||
min-name-length = 3
|
||||
max-name-length = 40
|
||||
# darglint
|
||||
|
@ -173,7 +177,7 @@ single_line_exclusions = typing
|
|||
[mypy]
|
||||
cache_dir = .cache/mypy
|
||||
|
||||
[mypy-nox.*,packaging.*,pytest]
|
||||
[mypy-nox.*,packaging.*,pytest,_pytest.*]
|
||||
ignore_missing_imports = true
|
||||
|
||||
|
||||
|
|
|
@ -1,9 +1,15 @@
|
|||
"""Source code for the urban-meal-delivery research project."""
|
||||
|
||||
from importlib import metadata
|
||||
from importlib import metadata as _metadata
|
||||
|
||||
|
||||
try:
|
||||
__version__ = metadata.version(__name__)
|
||||
except metadata.PackageNotFoundError: # pragma: no cover
|
||||
_pkg_info = _metadata.metadata(__name__)
|
||||
|
||||
except _metadata.PackageNotFoundError: # pragma: no cover
|
||||
__pkg_name__ = 'unknown'
|
||||
__version__ = 'unknown'
|
||||
|
||||
else:
|
||||
__pkg_name__ = _pkg_info['name']
|
||||
__version__ = _pkg_info['version']
|
||||
|
|
37
src/urban_meal_delivery/console.py
Normal file
37
src/urban_meal_delivery/console.py
Normal file
|
@ -0,0 +1,37 @@
|
|||
"""Provide CLI scripts for the project."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
import click
|
||||
from click.core import Context
|
||||
|
||||
import urban_meal_delivery
|
||||
|
||||
|
||||
def show_version(ctx: Context, _param: Any, value: bool) -> None:
|
||||
"""Show the package's version."""
|
||||
# If --version / -V is NOT passed in,
|
||||
# continue with the command.
|
||||
if not value or ctx.resilient_parsing:
|
||||
return
|
||||
|
||||
# Mimic the colors of `poetry version`.
|
||||
pkg_name = click.style(urban_meal_delivery.__pkg_name__, fg='green') # noqa:WPS609
|
||||
version = click.style(urban_meal_delivery.__version__, fg='blue') # noqa:WPS609
|
||||
# Include a warning for development versions.
|
||||
warning = click.style(' (development)', fg='red') if '.dev' in version else ''
|
||||
click.echo(f'{pkg_name}, version {version}{warning}')
|
||||
ctx.exit()
|
||||
|
||||
|
||||
@click.command()
|
||||
@click.option(
|
||||
'--version',
|
||||
'-V',
|
||||
is_flag=True,
|
||||
callback=show_version,
|
||||
is_eager=True,
|
||||
expose_value=False,
|
||||
)
|
||||
def main() -> None:
|
||||
"""The urban-meal-delivery research project."""
|
126
tests/test_console.py
Normal file
126
tests/test_console.py
Normal file
|
@ -0,0 +1,126 @@
|
|||
"""Test the package's `umd` command-line client."""
|
||||
|
||||
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().
|
||||
|
||||
The function is used as a callback to a click command option.
|
||||
|
||||
show_version() prints the name and version of the installed package to
|
||||
stdout. The output looks like this: "{pkg_name}, version {version}".
|
||||
|
||||
Development (= non-final) versions are indicated by appending a
|
||||
" (development)" to the output.
|
||||
"""
|
||||
|
||||
# pylint:disable=no-self-use
|
||||
|
||||
def test_no_version(self, capsys: CaptureFixture, ctx: click.Context) -> None:
|
||||
"""The the early exit branch without any output."""
|
||||
console.show_version(ctx, _param='discarded', value=False)
|
||||
|
||||
captured = capsys.readouterr()
|
||||
|
||||
assert captured.out == ''
|
||||
|
||||
def test_final_version(
|
||||
self, capsys: CaptureFixture, ctx: click.Context, monkeypatch: MonkeyPatch,
|
||||
) -> None:
|
||||
"""For final versions, NO "development" warning is emitted."""
|
||||
version = '1.2.3'
|
||||
monkeypatch.setattr(console.urban_meal_delivery, '__version__', version)
|
||||
|
||||
with pytest.raises(click.exceptions.Exit):
|
||||
console.show_version(ctx, _param='discarded', value=True)
|
||||
|
||||
captured = capsys.readouterr()
|
||||
|
||||
assert captured.out.endswith(f', version {version}\n')
|
||||
|
||||
def test_develop_version(
|
||||
self, capsys: CaptureFixture, ctx: click.Context, monkeypatch: MonkeyPatch,
|
||||
) -> None:
|
||||
"""For develop versions, a warning thereof is emitted."""
|
||||
version = '1.2.3.dev0'
|
||||
monkeypatch.setattr(console.urban_meal_delivery, '__version__', version)
|
||||
|
||||
with pytest.raises(click.exceptions.Exit):
|
||||
console.show_version(ctx, _param='discarded', value=True)
|
||||
|
||||
captured = capsys.readouterr()
|
||||
|
||||
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.
|
||||
|
||||
The test cases are integration tests.
|
||||
Therefore, they are not considered for coverage reporting.
|
||||
"""
|
||||
|
||||
# pylint:disable=no-self-use
|
||||
|
||||
@pytest.mark.no_cover
|
||||
def test_no_options(self, cli: click_testing.CliRunner) -> None:
|
||||
"""Exit with 0 status code and no output if run without options."""
|
||||
result = cli.invoke(console.main)
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert result.output == ''
|
||||
|
||||
# The following test cases validate the --version / -V option.
|
||||
|
||||
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:
|
||||
"""For final versions, NO "development" warning is emitted."""
|
||||
version = '1.2.3'
|
||||
monkeypatch.setattr(console.urban_meal_delivery, '__version__', version)
|
||||
|
||||
result = cli.invoke(console.main, option)
|
||||
|
||||
assert result.exit_code == 0
|
||||
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:
|
||||
"""For develop versions, a warning thereof is emitted."""
|
||||
version = '1.2.3.dev0'
|
||||
monkeypatch.setattr(console.urban_meal_delivery, '__version__', version)
|
||||
|
||||
result = cli.invoke(console.main, option)
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert result.output.strip().endswith(f', version {version} (development)')
|
Loading…
Reference in a new issue