This commit is contained in:
commit
52514ca411
16 changed files with 2839 additions and 4 deletions
14
.github/workflows/tests.yml
vendored
Normal file
14
.github/workflows/tests.yml
vendored
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
name: CI
|
||||||
|
on: push
|
||||||
|
jobs:
|
||||||
|
tests:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: actions/setup-python@v1
|
||||||
|
with:
|
||||||
|
python-version: 3.8
|
||||||
|
architecture: x64
|
||||||
|
- run: pip install nox==2020.5.24
|
||||||
|
- run: pip install poetry==1.0.10
|
||||||
|
- run: nox
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,3 +1,4 @@
|
||||||
|
.cache/
|
||||||
*.egg-info/
|
*.egg-info/
|
||||||
.python-version
|
.python-version
|
||||||
.venv/
|
.venv/
|
||||||
|
|
38
.pre-commit-config.yaml
Normal file
38
.pre-commit-config.yaml
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
default_stages: [commit] # only used if a hook does not specify stages
|
||||||
|
fail_fast: true
|
||||||
|
repos:
|
||||||
|
# Run the local formatting, linting, and testing tool chains.
|
||||||
|
- repo: local
|
||||||
|
hooks:
|
||||||
|
- id: local-pre-commit-checks
|
||||||
|
name: Run code formatters and linters
|
||||||
|
entry: poetry run nox -s pre-commit --
|
||||||
|
language: system
|
||||||
|
stages: [commit]
|
||||||
|
types: [python]
|
||||||
|
- id: local-pre-merge-checks
|
||||||
|
name: Run the entire test suite
|
||||||
|
entry: poetry run nox -s pre-merge --
|
||||||
|
language: system
|
||||||
|
stages: [merge-commit, push]
|
||||||
|
types: [python]
|
||||||
|
# Enable hooks provided by the pre-commit project to
|
||||||
|
# enforce rules that local tools could not that easily.
|
||||||
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
|
rev: v3.2.0
|
||||||
|
hooks:
|
||||||
|
- id: check-added-large-files
|
||||||
|
args: [--maxkb=100]
|
||||||
|
- id: check-case-conflict
|
||||||
|
- id: check-builtin-literals
|
||||||
|
- id: check-merge-conflict
|
||||||
|
- id: check-toml
|
||||||
|
- id: check-yaml
|
||||||
|
- id: end-of-file-fixer
|
||||||
|
stages: [commit] # overwrite the default
|
||||||
|
- id: mixed-line-ending
|
||||||
|
args: [--fix=no]
|
||||||
|
- id: no-commit-to-branch
|
||||||
|
args: [--branch, main]
|
||||||
|
- id: trailing-whitespace
|
||||||
|
stages: [commit] # overwrite the default
|
15
docs/conf.py
Normal file
15
docs/conf.py
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
"""Configure sphinx."""
|
||||||
|
|
||||||
|
import urban_meal_delivery as umd
|
||||||
|
|
||||||
|
|
||||||
|
project = umd.__pkg_name__
|
||||||
|
author = umd.__author__
|
||||||
|
copyright = f'2020, {author}' # pylint:disable=redefined-builtin
|
||||||
|
version = release = umd.__version__
|
||||||
|
|
||||||
|
extensions = [
|
||||||
|
'sphinx.ext.autodoc',
|
||||||
|
'sphinx.ext.napoleon',
|
||||||
|
'sphinx_autodoc_typehints',
|
||||||
|
]
|
95
docs/index.rst
Normal file
95
docs/index.rst
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
Urban Meal Delivery
|
||||||
|
===================
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:hidden:
|
||||||
|
:maxdepth: 1
|
||||||
|
|
||||||
|
license
|
||||||
|
reference
|
||||||
|
|
||||||
|
|
||||||
|
This is the documentation for the `urban-meal-delivery` package
|
||||||
|
that contains all the source code
|
||||||
|
used in a research project
|
||||||
|
residing in this `repository`_.
|
||||||
|
|
||||||
|
|
||||||
|
Prerequisites
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Only `git`_, `Python`_ 3.8 and `poetry`_ must be available.
|
||||||
|
All other software is automatically installed (cf., next section).
|
||||||
|
|
||||||
|
|
||||||
|
Installation
|
||||||
|
------------
|
||||||
|
|
||||||
|
The `urban-meal-delivery` package is installed
|
||||||
|
only after cloning the `repository`_.
|
||||||
|
It is *not* available on `PyPI`_ via `pip`_.
|
||||||
|
|
||||||
|
.. code-block:: console
|
||||||
|
|
||||||
|
$ git clone https://github.com/webartifex/urban-meal-delivery.git
|
||||||
|
|
||||||
|
Use `poetry`_ to install the package
|
||||||
|
with its dependencies into a `virtual environment`_,
|
||||||
|
which we also refer to as the **local** or the **develop** environment here.
|
||||||
|
|
||||||
|
.. code-block:: console
|
||||||
|
|
||||||
|
$ poetry install
|
||||||
|
|
||||||
|
|
||||||
|
First Steps
|
||||||
|
-----------
|
||||||
|
|
||||||
|
Verify that the installation is in a good state by
|
||||||
|
running the test suite and the code linters
|
||||||
|
via the automated task runner `nox`_.
|
||||||
|
|
||||||
|
``poetry run`` executes whatever comes after it in the develop environment.
|
||||||
|
If you have a project-independent `nox`_ installation available,
|
||||||
|
you may drop the ``poetry run`` prefix.
|
||||||
|
Otherwise, `nox`_ was installed as a develop dependency in the `virtual environment`_
|
||||||
|
in the installation step above.
|
||||||
|
|
||||||
|
Run the default sessions in `nox`_:
|
||||||
|
|
||||||
|
.. code-block:: console
|
||||||
|
|
||||||
|
$ [poetry run] nox
|
||||||
|
|
||||||
|
`nox`_ provides many options.
|
||||||
|
You can use them to choose among the different tasks you want to achieve.
|
||||||
|
|
||||||
|
.. option:: -l, --list
|
||||||
|
|
||||||
|
List all available `nox`_ sessions.
|
||||||
|
This includes sessions that are *not* run by default
|
||||||
|
and that may be used as develop tools
|
||||||
|
to work on the source files.
|
||||||
|
|
||||||
|
.. option:: -s [SESSION [SESSION ...]], --session[s] [SESSION [SESSION ...]]
|
||||||
|
|
||||||
|
Run only the specified session[s]
|
||||||
|
|
||||||
|
.. option:: -r, --reuse-existing-virtualenvs
|
||||||
|
|
||||||
|
By default,
|
||||||
|
`nox`_ creates a new `virtual environment`_ for every session.
|
||||||
|
To speed things up,
|
||||||
|
one can re-use existing environments with the -r flag.
|
||||||
|
This should only be done when developing
|
||||||
|
and not before committing or on the CI server.
|
||||||
|
|
||||||
|
|
||||||
|
.. _git: https://git-scm.com/
|
||||||
|
.. _nox: https://nox.thea.codes/en/stable/
|
||||||
|
.. _pip: https://pip.pypa.io/en/stable/
|
||||||
|
.. _poetry: https://python-poetry.org/docs/
|
||||||
|
.. _pypi: https://pypi.org/
|
||||||
|
.. _python: https://docs.python.org/3/
|
||||||
|
.. _repository: https://github.com/webartifex/urban-meal-delivery
|
||||||
|
.. _virtual environment: https://docs.python.org/3/tutorial/venv.html
|
4
docs/license.rst
Normal file
4
docs/license.rst
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
License
|
||||||
|
=======
|
||||||
|
|
||||||
|
.. include:: ../LICENSE.txt
|
6
docs/reference.rst
Normal file
6
docs/reference.rst
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
Reference
|
||||||
|
=========
|
||||||
|
|
||||||
|
.. contents::
|
||||||
|
:local:
|
||||||
|
:backlinks: none
|
439
noxfile.py
Normal file
439
noxfile.py
Normal file
|
@ -0,0 +1,439 @@
|
||||||
|
"""Configure nox as the task runner, including CI and pre-commit hooks.
|
||||||
|
|
||||||
|
Generic maintainance tasks:
|
||||||
|
|
||||||
|
- "init-project": set up the pre-commit hooks
|
||||||
|
|
||||||
|
- "clean-pwd": ~ `git clean -X` with minor exceptions
|
||||||
|
|
||||||
|
|
||||||
|
For local development, use the "format", "lint", and "test" sessions
|
||||||
|
as unified tasks to assure the quality of the source code:
|
||||||
|
|
||||||
|
- "format" (autoflake, black, isort):
|
||||||
|
|
||||||
|
+ check all source files [default]
|
||||||
|
+ accept extra arguments, e.g., `poetry run nox -s format -- noxfile.py`,
|
||||||
|
that are then interpreted as the paths the formatters and linters work
|
||||||
|
on recursively
|
||||||
|
|
||||||
|
- "lint" (flake8, mypy, pylint): same as "format"
|
||||||
|
|
||||||
|
- "test" (pytest, xdoctest):
|
||||||
|
|
||||||
|
+ run the entire test suite [default]
|
||||||
|
+ accepts extra arguments, e.g., `poetry run nox -s test -- --no-cov`,
|
||||||
|
that are passed on to `pytest` and `xdoctest` with no changes
|
||||||
|
=> may be paths or options
|
||||||
|
|
||||||
|
|
||||||
|
GitHub Actions implements a CI workflow:
|
||||||
|
|
||||||
|
- "format", "lint", and "test" as above
|
||||||
|
|
||||||
|
- "safety": check if dependencies contain known security vulnerabilites
|
||||||
|
|
||||||
|
- "docs": build the documentation with sphinx
|
||||||
|
|
||||||
|
|
||||||
|
The pre-commit framework invokes the "pre-commit" and "pre-merge" sessions:
|
||||||
|
|
||||||
|
- "pre-commit" before all commits:
|
||||||
|
|
||||||
|
+ triggers "format" and "lint" on staged source files
|
||||||
|
+ => test coverage may be < 100%
|
||||||
|
|
||||||
|
- "pre-merge" before all merges and pushes:
|
||||||
|
|
||||||
|
+ same as "pre-commit"
|
||||||
|
+ plus: triggers "test", "safety", and "docs" (that ignore extra arguments)
|
||||||
|
+ => test coverage is enforced to be 100%
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import contextlib
|
||||||
|
import glob
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
import nox
|
||||||
|
from nox.sessions import Session
|
||||||
|
|
||||||
|
|
||||||
|
PACKAGE_IMPORT_NAME = 'urban_meal_delivery'
|
||||||
|
|
||||||
|
# Docs/sphinx locations.
|
||||||
|
DOCS_SRC = 'docs/'
|
||||||
|
DOCS_BUILD = '.cache/docs/'
|
||||||
|
|
||||||
|
# Path to the *.py files to be packaged.
|
||||||
|
PACKAGE_SOURCE_LOCATION = 'src/'
|
||||||
|
|
||||||
|
# Path to the test suite.
|
||||||
|
PYTEST_LOCATION = 'tests/'
|
||||||
|
|
||||||
|
# Paths with all *.py files.
|
||||||
|
SRC_LOCATIONS = (
|
||||||
|
f'{DOCS_SRC}/conf.py',
|
||||||
|
'noxfile.py',
|
||||||
|
PACKAGE_SOURCE_LOCATION,
|
||||||
|
PYTEST_LOCATION,
|
||||||
|
)
|
||||||
|
|
||||||
|
PYTHON = '3.8'
|
||||||
|
|
||||||
|
# Use a unified .cache/ folder for all tools.
|
||||||
|
nox.options.envdir = '.cache/nox'
|
||||||
|
|
||||||
|
# All tools except git and poetry are project dependencies.
|
||||||
|
# Avoid accidental successes if the environment is not set up properly.
|
||||||
|
nox.options.error_on_external_run = True
|
||||||
|
|
||||||
|
# Run only CI related checks by default.
|
||||||
|
nox.options.sessions = (
|
||||||
|
'format',
|
||||||
|
'lint',
|
||||||
|
'test',
|
||||||
|
'safety',
|
||||||
|
'docs',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@nox.session(name='format', python=PYTHON)
|
||||||
|
def format_(session):
|
||||||
|
"""Format source files with autoflake, black, and isort.
|
||||||
|
|
||||||
|
If no extra arguments are provided, all source files are formatted.
|
||||||
|
Otherwise, they are interpreted as paths the formatters work on recursively.
|
||||||
|
"""
|
||||||
|
_begin(session)
|
||||||
|
|
||||||
|
# The formatting tools do not require the developed
|
||||||
|
# package be installed in the virtual environment.
|
||||||
|
_install_packages(session, 'autoflake', 'black', 'isort')
|
||||||
|
|
||||||
|
# Interpret extra arguments as locations of source files.
|
||||||
|
locations = session.posargs or SRC_LOCATIONS
|
||||||
|
|
||||||
|
session.run('autoflake', '--version')
|
||||||
|
session.run(
|
||||||
|
'autoflake',
|
||||||
|
'--in-place',
|
||||||
|
'--recursive',
|
||||||
|
'--expand-star-imports',
|
||||||
|
'--remove-all-unused-imports',
|
||||||
|
'--ignore-init-module-imports', # modifies --remove-all-unused-imports
|
||||||
|
'--remove-duplicate-keys',
|
||||||
|
'--remove-unused-variables',
|
||||||
|
*locations,
|
||||||
|
)
|
||||||
|
|
||||||
|
session.run('black', '--version')
|
||||||
|
session.run('black', *locations)
|
||||||
|
|
||||||
|
with _isort_fix(session): # TODO (isort): Remove after upgrading
|
||||||
|
session.run('isort', '--version')
|
||||||
|
session.run('isort', *locations)
|
||||||
|
|
||||||
|
|
||||||
|
@nox.session(python=PYTHON)
|
||||||
|
def lint(session):
|
||||||
|
"""Lint source files with flake8, mypy, and pylint.
|
||||||
|
|
||||||
|
If no extra arguments are provided, all source files are linted.
|
||||||
|
Otherwise, they are interpreted as paths the linters work on recursively.
|
||||||
|
"""
|
||||||
|
_begin(session)
|
||||||
|
|
||||||
|
# The linting tools do not require the developed
|
||||||
|
# package be installed in the virtual environment.
|
||||||
|
_install_packages(
|
||||||
|
session,
|
||||||
|
'flake8',
|
||||||
|
'flake8-annotations',
|
||||||
|
'flake8-black',
|
||||||
|
'flake8-expression-complexity',
|
||||||
|
'flake8-pytest-style',
|
||||||
|
'mypy',
|
||||||
|
'pylint',
|
||||||
|
'wemake-python-styleguide',
|
||||||
|
)
|
||||||
|
|
||||||
|
# Interpret extra arguments as locations of source files.
|
||||||
|
locations = session.posargs or SRC_LOCATIONS
|
||||||
|
|
||||||
|
session.run('flake8', '--version')
|
||||||
|
session.run('flake8', '--ignore=I0', *locations) # TODO (isort): Remove flag
|
||||||
|
|
||||||
|
with _isort_fix(session): # TODO (isort): Remove after upgrading
|
||||||
|
session.run('isort', '--version')
|
||||||
|
session.run('isort', '--check-only', *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.
|
||||||
|
# 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', '--disable=wrong-import-order', *locations,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@nox.session(name='pre-commit', python=PYTHON, venv_backend='none')
|
||||||
|
def pre_commit(session):
|
||||||
|
"""Run the format and lint sessions.
|
||||||
|
|
||||||
|
Source files must be well-formed before they enter git.
|
||||||
|
|
||||||
|
Intended to be run as a pre-commit hook.
|
||||||
|
|
||||||
|
Passed in extra arguments are forwarded. So, if it is run as a pre-commit
|
||||||
|
hook, only the currently staged source files are formatted and linted.
|
||||||
|
"""
|
||||||
|
# "format" and "lint" are run in sessions on their own as
|
||||||
|
# session.notify() creates new Session objects.
|
||||||
|
session.notify('format')
|
||||||
|
session.notify('lint')
|
||||||
|
|
||||||
|
|
||||||
|
@nox.session(python=PYTHON)
|
||||||
|
def test(session):
|
||||||
|
"""Test the code base.
|
||||||
|
|
||||||
|
Runs the unit and integration tests with pytest and
|
||||||
|
validate that all code snippets in docstrings work with xdoctest.
|
||||||
|
|
||||||
|
If no extra arguments are provided, the entire test suite
|
||||||
|
is exexcuted and succeeds only with 100% coverage.
|
||||||
|
|
||||||
|
If extra arguments are provided, they are
|
||||||
|
forwarded to pytest and xdoctest without any changes.
|
||||||
|
xdoctest ignores arguments it does not understand.
|
||||||
|
"""
|
||||||
|
# Re-using an old environment is not so easy here as
|
||||||
|
# `poetry install --no-dev` removes previously installed packages.
|
||||||
|
# We keep things simple and forbid such usage.
|
||||||
|
if session.virtualenv.reuse_existing:
|
||||||
|
raise RuntimeError('The "test" session must be run without the "-r" option')
|
||||||
|
|
||||||
|
_begin(session)
|
||||||
|
|
||||||
|
# The testing tools require the developed package and its
|
||||||
|
# non-develop dependencies be installed in the virtual environment.
|
||||||
|
session.run('poetry', 'install', '--no-dev', external=True)
|
||||||
|
_install_packages(
|
||||||
|
session, 'packaging', 'pytest', 'pytest-cov', 'xdoctest[optional]',
|
||||||
|
)
|
||||||
|
|
||||||
|
# Interpret extra arguments as options for pytest.
|
||||||
|
# They are "dropped" by the hack in the pre_merge() function
|
||||||
|
# if this function is run within the "pre-merge" session.
|
||||||
|
posargs = () if session.env.get('_drop_posargs') else session.posargs
|
||||||
|
|
||||||
|
args = posargs or (
|
||||||
|
'--cov',
|
||||||
|
'--no-cov-on-fail',
|
||||||
|
'--cov-branch',
|
||||||
|
'--cov-fail-under=100',
|
||||||
|
'--cov-report=term-missing:skip-covered',
|
||||||
|
PYTEST_LOCATION,
|
||||||
|
)
|
||||||
|
session.run('pytest', '--version')
|
||||||
|
session.run('pytest', *args)
|
||||||
|
|
||||||
|
# For xdoctest, the default arguments are different from pytest.
|
||||||
|
args = posargs or [PACKAGE_IMPORT_NAME]
|
||||||
|
session.run('xdoctest', '--version')
|
||||||
|
session.run('xdoctest', '--quiet', *args) # --quiet => less verbose output
|
||||||
|
|
||||||
|
|
||||||
|
@nox.session(python=PYTHON)
|
||||||
|
def safety(session):
|
||||||
|
"""Check the dependencies for known security vulnerabilities."""
|
||||||
|
_begin(session)
|
||||||
|
|
||||||
|
# We do not pin the version of `safety` to always check with
|
||||||
|
# the latest version. The risk this breaks the CI is rather low.
|
||||||
|
session.install('safety')
|
||||||
|
|
||||||
|
with tempfile.NamedTemporaryFile() as requirements_txt:
|
||||||
|
session.run(
|
||||||
|
'poetry',
|
||||||
|
'export',
|
||||||
|
'--dev',
|
||||||
|
'--format=requirements.txt',
|
||||||
|
f'--output={requirements_txt.name}',
|
||||||
|
external=True,
|
||||||
|
)
|
||||||
|
session.run(
|
||||||
|
'safety', 'check', f'--file={requirements_txt.name}', '--full-report',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@nox.session(python=PYTHON)
|
||||||
|
def docs(session):
|
||||||
|
"""Build the documentation with sphinx."""
|
||||||
|
# The latest version of the package needs to be installed
|
||||||
|
# so that sphinx's autodoc can include the latest docstrings.
|
||||||
|
if session.virtualenv.reuse_existing:
|
||||||
|
raise RuntimeError('The "docs" session must be run without the "-r" option')
|
||||||
|
|
||||||
|
_begin(session)
|
||||||
|
|
||||||
|
# The documentation tools require the developed package and its
|
||||||
|
# non-develop dependencies be installed in the virtual environment.
|
||||||
|
# Otherwise, sphinx's autodoc could not include the docstrings.
|
||||||
|
session.run('poetry', 'install', '--no-dev', external=True)
|
||||||
|
_install_packages(session, 'sphinx', 'sphinx-autodoc-typehints')
|
||||||
|
|
||||||
|
session.run('sphinx-build', DOCS_SRC, DOCS_BUILD)
|
||||||
|
# Verify all external links return 200 OK.
|
||||||
|
session.run('sphinx-build', '-b', 'linkcheck', DOCS_SRC, DOCS_BUILD)
|
||||||
|
|
||||||
|
print(f'Docs are available at {os.getcwd()}/{DOCS_BUILD}index.html') # noqa:WPS421
|
||||||
|
|
||||||
|
|
||||||
|
@nox.session(name='pre-merge', python=PYTHON)
|
||||||
|
def pre_merge(session):
|
||||||
|
"""Run the format, lint, test, safety, and docs sessions.
|
||||||
|
|
||||||
|
Intended to be run either as a pre-merge or pre-push hook.
|
||||||
|
|
||||||
|
Ignores the paths passed in by the pre-commit framework
|
||||||
|
for the test, safety, and docs sessions so that the
|
||||||
|
entire test suite is executed.
|
||||||
|
"""
|
||||||
|
# Re-using an old environment is not so easy here as the "test" session
|
||||||
|
# runs `poetry install --no-dev`, which removes previously installed packages.
|
||||||
|
if session.virtualenv.reuse_existing:
|
||||||
|
raise RuntimeError(
|
||||||
|
'The "pre-merge" session must be run without the "-r" option',
|
||||||
|
)
|
||||||
|
|
||||||
|
session.notify('format')
|
||||||
|
session.notify('lint')
|
||||||
|
session.notify('safety')
|
||||||
|
session.notify('docs')
|
||||||
|
|
||||||
|
# Little hack to not work with the extra arguments provided
|
||||||
|
# by the pre-commit framework. Create a flag in the
|
||||||
|
# env(ironment) that must contain only `str`-like objects.
|
||||||
|
session.env['_drop_posargs'] = 'true'
|
||||||
|
|
||||||
|
# Cannot use session.notify() to trigger the "test" session
|
||||||
|
# as that would create a new Session object without the flag
|
||||||
|
# in the env(ironment). Instead, run the test() function within
|
||||||
|
# the "pre-merge" session.
|
||||||
|
test(session)
|
||||||
|
|
||||||
|
|
||||||
|
@nox.session(name='init-project', python=PYTHON, venv_backend='none')
|
||||||
|
def init_project(session):
|
||||||
|
"""Install the pre-commit hooks."""
|
||||||
|
for type_ in ('pre-commit', 'pre-merge-commit', 'pre-push'):
|
||||||
|
session.run('poetry', 'run', 'pre-commit', 'install', f'--hook-type={type_}')
|
||||||
|
|
||||||
|
|
||||||
|
@nox.session(name='clean-pwd', python=PYTHON, venv_backend='none')
|
||||||
|
def clean_pwd(session):
|
||||||
|
"""Remove (almost) all glob patterns listed in .gitignore.
|
||||||
|
|
||||||
|
The difference compared to `git clean -X` is that this task
|
||||||
|
does not remove pyenv's .python-version file and poetry's
|
||||||
|
virtual environment.
|
||||||
|
"""
|
||||||
|
exclude = frozenset(('.python-version', '.venv', 'venv'))
|
||||||
|
|
||||||
|
with open('.gitignore') as file_handle:
|
||||||
|
paths = file_handle.readlines()
|
||||||
|
|
||||||
|
for path in paths:
|
||||||
|
path = path.strip()
|
||||||
|
if path.startswith('#') or path in exclude:
|
||||||
|
continue
|
||||||
|
|
||||||
|
for expanded in glob.glob(path):
|
||||||
|
session.run(f'rm -rf {expanded}')
|
||||||
|
|
||||||
|
|
||||||
|
def _begin(session):
|
||||||
|
"""Show generic info about a session."""
|
||||||
|
if session.posargs:
|
||||||
|
# Part of the hack in pre_merge() to "drop" the extra arguments.
|
||||||
|
# Indicate to stdout that the passed in extra arguments are ignored.
|
||||||
|
if session.env.get('_drop_posargs') is None:
|
||||||
|
print('Provided extra argument(s):', *session.posargs) # noqa:WPS421
|
||||||
|
else:
|
||||||
|
print('The provided extra arguments are ignored') # noqa:WPS421
|
||||||
|
|
||||||
|
session.run('python', '--version')
|
||||||
|
|
||||||
|
# Fake GNU's pwd.
|
||||||
|
session.log('pwd')
|
||||||
|
print(os.getcwd()) # noqa:WPS421
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
packages respecting the pinnned versions specified in poetry's lock file.
|
||||||
|
This makes nox sessions even more deterministic.
|
||||||
|
|
||||||
|
IMPORTANT: This function skips installation if the current nox session
|
||||||
|
is run with the "-r" flag to re-use an existing virtual environment.
|
||||||
|
That turns nox into a fast task runner provided that a virtual
|
||||||
|
environment actually existed and does not need to be changed (e.g.,
|
||||||
|
new dependencies added in the meantime). Do not use the "-r" flag on CI
|
||||||
|
or as part of pre-commit hooks!
|
||||||
|
|
||||||
|
Args:
|
||||||
|
session: the Session object
|
||||||
|
*packages_or_pip_args: the packages to be installed or pip options
|
||||||
|
**kwargs: passed on to nox.sessions.Session.install()
|
||||||
|
""" # noqa:RST210,RST213
|
||||||
|
if session.virtualenv.reuse_existing:
|
||||||
|
session.log(
|
||||||
|
'No dependencies are installed as an existing environment is re-used',
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
session.log('Dependencies are installed respecting the poetry.lock file')
|
||||||
|
|
||||||
|
with tempfile.NamedTemporaryFile() as requirements_txt:
|
||||||
|
session.run(
|
||||||
|
'poetry',
|
||||||
|
'export',
|
||||||
|
'--dev',
|
||||||
|
'--format=requirements.txt',
|
||||||
|
f'--output={requirements_txt.name}',
|
||||||
|
external=True,
|
||||||
|
)
|
||||||
|
session.install(
|
||||||
|
f'--constraint={requirements_txt.name}', *packages_or_pip_args, **kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# TODO (isort): Remove this fix after
|
||||||
|
# upgrading to isort ^5.3.0 in pyproject.toml.
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def _isort_fix(session):
|
||||||
|
"""Temporarily upgrade to isort 5.3.0."""
|
||||||
|
session.install('isort==5.3.0')
|
||||||
|
try:
|
||||||
|
yield
|
||||||
|
finally:
|
||||||
|
session.install('isort==4.3.21')
|
1674
poetry.lock
generated
1674
poetry.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -2,9 +2,14 @@
|
||||||
build-backend = "poetry.masonry.api"
|
build-backend = "poetry.masonry.api"
|
||||||
requires = ["poetry>=0.12"]
|
requires = ["poetry>=0.12"]
|
||||||
|
|
||||||
|
[tool.black]
|
||||||
|
line-length = 88
|
||||||
|
skip-string-normalization = true # wemake-python-styleguide enforces single quotes
|
||||||
|
target-version = ["py38"]
|
||||||
|
|
||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "urban-meal-delivery"
|
name = "urban-meal-delivery"
|
||||||
version = "0.1.0.dev0"
|
version = "0.1.0"
|
||||||
|
|
||||||
authors = ["Alexander Hess <alexander@webartifex.biz>"]
|
authors = ["Alexander Hess <alexander@webartifex.biz>"]
|
||||||
description = "Optimizing an urban meal delivery platform"
|
description = "Optimizing an urban meal delivery platform"
|
||||||
|
@ -22,4 +27,37 @@ repository = "https://github.com/webartifex/urban-meal-delivery"
|
||||||
[tool.poetry.dependencies]
|
[tool.poetry.dependencies]
|
||||||
python = "^3.8"
|
python = "^3.8"
|
||||||
|
|
||||||
|
click = "^7.1.2"
|
||||||
|
|
||||||
[tool.poetry.dev-dependencies]
|
[tool.poetry.dev-dependencies]
|
||||||
|
# Task Runners
|
||||||
|
nox = "^2020.5.24"
|
||||||
|
pre-commit = "^2.6.0"
|
||||||
|
|
||||||
|
# Code Formatters
|
||||||
|
autoflake = "^1.3.1"
|
||||||
|
black = "^19.10b0"
|
||||||
|
isort = "^4.3.21" # TODO (isort): not ^5.2.2 due to pylint and wemake-python-styleguide
|
||||||
|
|
||||||
|
# (Static) Code Analyzers
|
||||||
|
flake8 = "^3.8.3"
|
||||||
|
flake8-annotations = "^2.3.0"
|
||||||
|
flake8-black = "^0.2.1"
|
||||||
|
flake8-expression-complexity = "^0.0.8"
|
||||||
|
flake8-pytest-style = "^1.2.2"
|
||||||
|
mypy = "^0.782"
|
||||||
|
pylint = "^2.5.3"
|
||||||
|
wemake-python-styleguide = "^0.14.1" # flake8 plug-in
|
||||||
|
|
||||||
|
# Test Suite
|
||||||
|
packaging = "^20.4" # used to test the packaged version
|
||||||
|
pytest = "^6.0.1"
|
||||||
|
pytest-cov = "^2.10.0"
|
||||||
|
xdoctest = { version="^0.13.0", extras=["optional"] }
|
||||||
|
|
||||||
|
# Documentation
|
||||||
|
sphinx = "^3.1.2"
|
||||||
|
sphinx-autodoc-typehints = "^1.11.0"
|
||||||
|
|
||||||
|
[tool.poetry.scripts]
|
||||||
|
umd = "urban_meal_delivery.console:main"
|
||||||
|
|
231
setup.cfg
Normal file
231
setup.cfg
Normal file
|
@ -0,0 +1,231 @@
|
||||||
|
[black]
|
||||||
|
# black's settings are in pyproject.toml => [tool.black]
|
||||||
|
|
||||||
|
|
||||||
|
[coverage:paths]
|
||||||
|
source =
|
||||||
|
src/
|
||||||
|
*/site-packages/
|
||||||
|
|
||||||
|
[coverage:report]
|
||||||
|
show_missing = true
|
||||||
|
skip_covered = true
|
||||||
|
skip_empty = true
|
||||||
|
|
||||||
|
[coverage:run]
|
||||||
|
branch = true
|
||||||
|
data_file = .cache/coverage/data
|
||||||
|
source =
|
||||||
|
urban_meal_delivery
|
||||||
|
|
||||||
|
|
||||||
|
[flake8]
|
||||||
|
# Include error classes only explicitly
|
||||||
|
# to avoid forward compatibility issues.
|
||||||
|
select =
|
||||||
|
# =============
|
||||||
|
# flake8's base
|
||||||
|
# =============
|
||||||
|
# mccabe => cyclomatic complexity
|
||||||
|
C901,
|
||||||
|
# pycodestyle => PEP8 compliance
|
||||||
|
E, W,
|
||||||
|
# pyflakes => basic errors
|
||||||
|
F4, F5, F6, F7, F8, F9
|
||||||
|
# ========================
|
||||||
|
# wemake-python-styleguide
|
||||||
|
# Source: https://wemake-python-stylegui.de/en/latest/pages/usage/violations/index.html
|
||||||
|
# ========================
|
||||||
|
WPS1, WPS2, WPS3, WPS4, WPS5, WPS6,
|
||||||
|
# darglint => docstring matches implementation
|
||||||
|
DAR0, DAR1, DAR2, DAR3, DAR4, DAR5,
|
||||||
|
# flake8-bandit => common security issues
|
||||||
|
S1, S2, S3, S4, S5, S6, S7,
|
||||||
|
# flake8-broken-line => no \ to end a line
|
||||||
|
N400,
|
||||||
|
# flake8-bugbear => opinionated bugs and design flaws
|
||||||
|
B0, B3, B9,
|
||||||
|
# flake8-commas => better comma placements
|
||||||
|
C8,
|
||||||
|
# flake8-comprehensions => better comprehensions
|
||||||
|
C4,
|
||||||
|
# flake8-debugger => no debugger usage
|
||||||
|
T100,
|
||||||
|
# flake8-docstrings => PEP257 compliance
|
||||||
|
D1, D2, D3, D4,
|
||||||
|
# flake8-eradicate => no commented out code
|
||||||
|
E800,
|
||||||
|
# flake8-isort => isort would make changes
|
||||||
|
I0,
|
||||||
|
# flake8-rst-docstrings => valid rst in docstrings
|
||||||
|
RST2, RST3, RST4,
|
||||||
|
# flake8-string-format => unify usage of str.format()
|
||||||
|
P1, P2, P3,
|
||||||
|
# flake8-quotes => use double quotes everywhere (complying with black)
|
||||||
|
Q0,
|
||||||
|
# pep8-naming
|
||||||
|
N8,
|
||||||
|
# =====
|
||||||
|
# other
|
||||||
|
# =====
|
||||||
|
# flake8-annotations => enforce type checking for functions
|
||||||
|
ANN0, ANN2, ANN3,
|
||||||
|
# flake8-black => complain if black would make changes
|
||||||
|
BLK1, BLK9,
|
||||||
|
# flake8-expression-complexity => not too many expressions at once
|
||||||
|
ECE001,
|
||||||
|
# flake8-pytest-style => enforce a consistent style with pytest
|
||||||
|
PT0,
|
||||||
|
|
||||||
|
# By default, flake8 ignores some errors.
|
||||||
|
# Instead, do not ignore anything.
|
||||||
|
ignore =
|
||||||
|
|
||||||
|
# If --ignore is passed on the command
|
||||||
|
# line, still ignore the following:
|
||||||
|
extend-ignore =
|
||||||
|
# Comply with black's style.
|
||||||
|
# Source: https://github.com/psf/black/blob/master/docs/compatible_configs.md#flake8
|
||||||
|
E203, W503,
|
||||||
|
# f-strings are ok.
|
||||||
|
WPS305,
|
||||||
|
# Classes should not have to specify a base class.
|
||||||
|
WPS306,
|
||||||
|
# Putting logic into __init__.py files may be justified.
|
||||||
|
WPS412,
|
||||||
|
# Allow multiple assignment, e.g., x = y = 123
|
||||||
|
WPS429,
|
||||||
|
|
||||||
|
per-file-ignores =
|
||||||
|
docs/conf.py:
|
||||||
|
# Allow shadowing built-ins and reading __*__ variables.
|
||||||
|
WPS125,WPS609,
|
||||||
|
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".
|
||||||
|
WPS213,
|
||||||
|
# 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.
|
||||||
|
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.
|
||||||
|
# Source: https://en.wikipedia.org/wiki/Cyclomatic_complexity#Limiting_complexity_during_development
|
||||||
|
max-complexity = 10
|
||||||
|
|
||||||
|
# Comply with black's style.
|
||||||
|
# Source: https://github.com/psf/black/blob/master/docs/the_black_code_style.md#line-length
|
||||||
|
max-line-length = 88
|
||||||
|
|
||||||
|
# Preview the code lines that cause errors.
|
||||||
|
show-source = true
|
||||||
|
|
||||||
|
# ===================================
|
||||||
|
# wemake-python-styleguide's settings
|
||||||
|
# ===================================
|
||||||
|
allowed-domain-names =
|
||||||
|
param,
|
||||||
|
result,
|
||||||
|
value,
|
||||||
|
min-name-length = 3
|
||||||
|
max-name-length = 40
|
||||||
|
# darglint
|
||||||
|
strictness = long
|
||||||
|
# flake8-docstrings
|
||||||
|
docstring-convention = google
|
||||||
|
# flake8-eradicate
|
||||||
|
eradicate-aggressive = true
|
||||||
|
|
||||||
|
# ==============================
|
||||||
|
# flake8-pytest-style's settings
|
||||||
|
# ==============================
|
||||||
|
# Prefer @pytest.fixture over @pytest.fixture().
|
||||||
|
pytest-fixture-no-parentheses = true
|
||||||
|
# Prefer @pytest.mark.parametrize(['param1', 'param2'], [(1, 2), (3, 4)])
|
||||||
|
# over @pytest.mark.parametrize(('param1', 'param2'), ([1, 2], [3, 4]))
|
||||||
|
pytest-parametrize-names-type = list
|
||||||
|
pytest-parametrize-values-row-type = tuple
|
||||||
|
pytest-parametrize-values-type = list
|
||||||
|
|
||||||
|
|
||||||
|
[isort]
|
||||||
|
atomic = true
|
||||||
|
case_sensitive = true
|
||||||
|
combine_star = true
|
||||||
|
force_alphabetical_sort_within_sections = true
|
||||||
|
lines_after_imports = 2
|
||||||
|
remove_redundant_aliases = true
|
||||||
|
|
||||||
|
# Comply with black's style.
|
||||||
|
# Source: https://github.com/psf/black/blob/master/docs/compatible_configs.md#isort
|
||||||
|
ensure_newline_before_comments = true
|
||||||
|
force_grid_wrap = 0
|
||||||
|
include_trailing_comma = true
|
||||||
|
line_length = 88
|
||||||
|
multi_line_output = 3
|
||||||
|
use_parentheses = true
|
||||||
|
|
||||||
|
# Comply with Google's Python Style Guide.
|
||||||
|
# All imports go on a single line except the ones from the typing module.
|
||||||
|
# Source: https://google.github.io/styleguide/pyguide.html#313-imports-formatting
|
||||||
|
force_single_line = true
|
||||||
|
single_line_exclusions = typing
|
||||||
|
|
||||||
|
|
||||||
|
[mypy]
|
||||||
|
cache_dir = .cache/mypy
|
||||||
|
|
||||||
|
[mypy-nox.*,packaging,pytest]
|
||||||
|
ignore_missing_imports = true
|
||||||
|
|
||||||
|
|
||||||
|
[pylint.FORMAT]
|
||||||
|
# Comply with black's style.
|
||||||
|
max-line-length = 88
|
||||||
|
|
||||||
|
[pylint.MESSAGES CONTROL]
|
||||||
|
disable =
|
||||||
|
# We use TODO's to indicate locations in the source base
|
||||||
|
# that must be worked on in the near future.
|
||||||
|
fixme,
|
||||||
|
# Comply with black's style.
|
||||||
|
bad-continuation, bad-whitespace,
|
||||||
|
# =====================
|
||||||
|
# flake8 de-duplication
|
||||||
|
# Source: https://pylint.pycqa.org/en/latest/faq.html#i-am-using-another-popular-linter-alongside-pylint-which-messages-should-i-disable-to-avoid-duplicates
|
||||||
|
# =====================
|
||||||
|
# mccabe
|
||||||
|
too-many-branches,
|
||||||
|
# pep8-naming
|
||||||
|
bad-classmethod-argument, bad-mcs-classmethod-argument,
|
||||||
|
invalid-name, no-self-argument,
|
||||||
|
# pycodestyle
|
||||||
|
bad-indentation, bare-except, line-too-long, missing-final-newline,
|
||||||
|
multiple-statements, trailing-whitespace, unnecessary-semicolon, unneeded-not,
|
||||||
|
# pydocstyle
|
||||||
|
missing-class-docstring, missing-function-docstring, missing-module-docstring,
|
||||||
|
# pyflakes
|
||||||
|
undefined-variable, unused-import, unused-variable,
|
||||||
|
# wemake-python-styleguide
|
||||||
|
redefined-outer-name,
|
||||||
|
|
||||||
|
[pylint.REPORTS]
|
||||||
|
score = no
|
||||||
|
|
||||||
|
|
||||||
|
[tool:pytest]
|
||||||
|
addopts =
|
||||||
|
--strict-markers
|
||||||
|
cache_dir = .cache/pytest
|
||||||
|
console_output_style = count
|
|
@ -1 +1,23 @@
|
||||||
"""Source code for the urban-meal-delivery research project."""
|
"""Source code for the urban-meal-delivery research project.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> import urban_meal_delivery as umd
|
||||||
|
>>> umd.__version__ != '0.0.0'
|
||||||
|
True
|
||||||
|
"""
|
||||||
|
|
||||||
|
from importlib import metadata as _metadata
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
_pkg_info = _metadata.metadata(__name__)
|
||||||
|
|
||||||
|
except _metadata.PackageNotFoundError: # pragma: no cover
|
||||||
|
__author__ = 'unknown'
|
||||||
|
__pkg_name__ = 'unknown'
|
||||||
|
__version__ = 'unknown'
|
||||||
|
|
||||||
|
else:
|
||||||
|
__author__ = _pkg_info['author']
|
||||||
|
__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."""
|
1
tests/__init__.py
Normal file
1
tests/__init__.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
"""Test the urban-meal-delivery package."""
|
110
tests/test_console.py
Normal file
110
tests/test_console.py
Normal file
|
@ -0,0 +1,110 @@
|
||||||
|
"""Test the package's `umd` command-line client."""
|
||||||
|
|
||||||
|
import click
|
||||||
|
import pytest
|
||||||
|
from click import testing as click_testing
|
||||||
|
|
||||||
|
from urban_meal_delivery import console
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
@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)
|
||||||
|
|
||||||
|
captured = capsys.readouterr()
|
||||||
|
|
||||||
|
assert captured.out == ''
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
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, ctx, monkeypatch):
|
||||||
|
"""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)')
|
||||||
|
|
||||||
|
|
||||||
|
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.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):
|
||||||
|
"""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, monkeypatch, option):
|
||||||
|
"""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, monkeypatch, option):
|
||||||
|
"""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)')
|
114
tests/test_version.py
Normal file
114
tests/test_version.py
Normal file
|
@ -0,0 +1,114 @@
|
||||||
|
"""Test the package's version identifier.
|
||||||
|
|
||||||
|
This packaged version identifier must adhere to PEP440
|
||||||
|
and a strict subset of semantic versioning:
|
||||||
|
|
||||||
|
- version identifiers must follow the x.y.z format
|
||||||
|
where x.y.z are non-negative integers
|
||||||
|
- non-final versions are only allowed with a .devN suffix
|
||||||
|
where N is also a non-negative integer
|
||||||
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from packaging import version as pkg_version
|
||||||
|
|
||||||
|
import urban_meal_delivery
|
||||||
|
|
||||||
|
|
||||||
|
class TestPEP404Compliance:
|
||||||
|
"""Packaged version identifier is PEP440 compliant."""
|
||||||
|
|
||||||
|
# pylint:disable=no-self-use
|
||||||
|
|
||||||
|
@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):
|
||||||
|
"""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):
|
||||||
|
"""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):
|
||||||
|
"""PEP440 compliant subset of semantic versioning: all public parts."""
|
||||||
|
assert parsed_version.public == urban_meal_delivery.__version__ # noqa:WPS609
|
||||||
|
|
||||||
|
|
||||||
|
class TestSemanticVersioning:
|
||||||
|
"""Packaged version follows a strict subset of semantic versioning."""
|
||||||
|
|
||||||
|
# pylint:disable=no-self-use
|
||||||
|
|
||||||
|
version_pattern = re.compile(
|
||||||
|
r'^(0|([1-9]\d*))\.(0|([1-9]\d*))\.(0|([1-9]\d*))(\.dev(0|([1-9]\d*)))?$',
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_version_is_semantic(self):
|
||||||
|
"""Packaged version follows semantic versioning."""
|
||||||
|
result = self.version_pattern.fullmatch(
|
||||||
|
urban_meal_delivery.__version__, # noqa:WPS609
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result is not None
|
||||||
|
|
||||||
|
# The next two test cases are sanity checks to validate the version_pattern.
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
'version',
|
||||||
|
[
|
||||||
|
'0.1.0',
|
||||||
|
'1.0.0',
|
||||||
|
'1.2.3',
|
||||||
|
'1.23.456',
|
||||||
|
'123.4.56',
|
||||||
|
'10.11.12',
|
||||||
|
'1.2.3.dev0',
|
||||||
|
'1.2.3.dev1',
|
||||||
|
'1.2.3.dev10',
|
||||||
|
],
|
||||||
|
)
|
||||||
|
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)
|
||||||
|
|
||||||
|
assert result is not None
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
'version',
|
||||||
|
[
|
||||||
|
'1',
|
||||||
|
'1.2',
|
||||||
|
'-1.2.3',
|
||||||
|
'01.2.3',
|
||||||
|
'1.02.3',
|
||||||
|
'1.2.03',
|
||||||
|
'1.2.3.4',
|
||||||
|
'1.2.3.abc',
|
||||||
|
'1.2.3-dev0',
|
||||||
|
'1.2.3+dev0',
|
||||||
|
'1.2.3.d0',
|
||||||
|
'1.2.3.develop0',
|
||||||
|
'1.2.3.dev-1',
|
||||||
|
'1.2.3.dev01',
|
||||||
|
'1.2.3..dev0',
|
||||||
|
'1-2-3',
|
||||||
|
'1,2,3',
|
||||||
|
'1..2.3',
|
||||||
|
'1.2..3',
|
||||||
|
],
|
||||||
|
)
|
||||||
|
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)
|
||||||
|
|
||||||
|
assert result is None
|
Loading…
Reference in a new issue