From 9fc5b4816a743f133387949e7a5392b7215393dd Mon Sep 17 00:00:00 2001 From: Alexander Hess Date: Tue, 4 Aug 2020 00:09:29 +0200 Subject: [PATCH] Add a testing tool chain - use pytest as the base, measure coverage with pytest-cov + configure coverage to include branches and specify source locations + configure pytest to enforce explicit markers - add a package for the test suite under tests/ - add a `__version__` identifier at the package's root + it is dynamically assigned the version of the installed package + the version is PEP440 compliant and follows a strict subset of semantic versioning: x.y.z[.devN] where x, y, z, and N are all non-negative integers + add module with tests for the __version__ - add a nox session "test" that runs the test suite - use flake8 to lint pytest for consistent style --- noxfile.py | 26 +++- poetry.lock | 202 +++++++++++++++++++++++++++- pyproject.toml | 6 + setup.cfg | 50 ++++++- src/urban_meal_delivery/__init__.py | 8 ++ tests/__init__.py | 1 + tests/test_version.py | 116 ++++++++++++++++ 7 files changed, 406 insertions(+), 3 deletions(-) create mode 100644 tests/__init__.py create mode 100644 tests/test_version.py diff --git a/noxfile.py b/noxfile.py index 683ab79..9b24133 100644 --- a/noxfile.py +++ b/noxfile.py @@ -14,8 +14,11 @@ MAIN_PYTHON = '3.8' # Keep the project is forward compatible. NEXT_PYTHON = '3.9' +# Path to the test suite. +PYTEST_LOCATION = 'tests/' + # Paths with *.py files. -SRC_LOCATIONS = 'noxfile.py', 'src/' +SRC_LOCATIONS = 'noxfile.py', 'src/', PYTEST_LOCATION # Use a unified .cache/ folder for all tools. @@ -70,6 +73,7 @@ def lint(session: Session) -> None: 'flake8-annotations', 'flake8-black', 'flake8-expression-complexity', + 'flake8-pytest-style', 'mypy', 'pylint', 'wemake-python-styleguide', @@ -96,7 +100,27 @@ def lint(session: Session) -> None: @nox.session(python=[MAIN_PYTHON, NEXT_PYTHON]) def test(session: Session) -> None: """Test the code base.""" + # 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 with the "-r" option') + _begin(session) + # Install only the non-develop dependencies + # and the testing tool chain. + session.run('poetry', 'install', '--no-dev', external=True) + _install_packages(session, 'pytest', 'pytest-cov') + # Interpret extra arguments as options for pytest. + args = session.posargs or ( + '--cov', + '--no-cov-on-fail', + '--cov-branch', + '--cov-fail-under=100', + '--cov-report=term-missing:skip-covered', + PYTEST_LOCATION, + ) + session.run('pytest', *args) @nox.session(name='pre-commit', python=MAIN_PYTHON, venv_backend='none') diff --git a/poetry.lock b/poetry.lock index 2c6ed11..f4a669c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -49,6 +49,15 @@ lazy-object-proxy = ">=1.4.0,<1.5.0" six = ">=1.12,<2.0" wrapt = ">=1.11,<2.0" +[[package]] +category = "dev" +description = "Atomic file writes." +marker = "sys_platform == \"win32\"" +name = "atomicwrites" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "1.4.0" + [[package]] category = "dev" description = "Classes Without Boilerplate" @@ -137,6 +146,17 @@ version = "4.2.1" [package.dependencies] colorama = "*" +[[package]] +category = "dev" +description = "Code coverage measurement for Python" +name = "coverage" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" +version = "5.2.1" + +[package.extras] +toml = ["toml"] + [[package]] category = "dev" description = "A utility for ensuring Google-style docstrings stay up to date with the source code." @@ -341,6 +361,14 @@ version = ">=4.3.5,<5" [package.extras] test = ["pytest (>=4.0.2,<6)"] +[[package]] +category = "dev" +description = "The package provides base classes and utils for flake8 plugin writing" +name = "flake8-plugin-utils" +optional = false +python-versions = ">=3.6,<4.0" +version = "1.3.0" + [[package]] category = "dev" description = "Polyfill package for Flake8 plugins" @@ -352,6 +380,17 @@ version = "1.0.2" [package.dependencies] flake8 = "*" +[[package]] +category = "dev" +description = "A flake8 plugin checking common style issues or inconsistencies with pytest-based tests." +name = "flake8-pytest-style" +optional = false +python-versions = ">=3.6,<4.0" +version = "1.2.2" + +[package.dependencies] +flake8-plugin-utils = ">=1.3.0,<2.0.0" + [[package]] category = "dev" description = "Flake8 lint for quotes." @@ -408,6 +447,14 @@ version = "3.1.7" [package.dependencies] gitdb = ">=4.0.1,<5" +[[package]] +category = "dev" +description = "iniconfig: brain-dead simple config-ini parsing" +name = "iniconfig" +optional = false +python-versions = "*" +version = "1.0.1" + [[package]] category = "dev" description = "A Python utility / library to sort Python imports." @@ -438,6 +485,14 @@ optional = false python-versions = "*" version = "0.6.1" +[[package]] +category = "dev" +description = "More routines for operating on iterables, beyond itertools" +name = "more-itertools" +optional = false +python-versions = ">=3.5" +version = "8.4.0" + [[package]] category = "dev" description = "Optional static typing for Python" @@ -479,6 +534,18 @@ virtualenv = ">=14.0.0" [package.extras] tox_to_nox = ["jinja2", "tox"] +[[package]] +category = "dev" +description = "Core utilities for Python packages" +name = "packaging" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "20.4" + +[package.dependencies] +pyparsing = ">=2.0.2" +six = "*" + [[package]] category = "dev" description = "Utility library for gitignore style pattern matching of file paths." @@ -506,6 +573,17 @@ version = "0.9.1" [package.dependencies] flake8-polyfill = ">=1.0.2,<2" +[[package]] +category = "dev" +description = "plugin and hook calling mechanisms for python" +name = "pluggy" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "0.13.1" + +[package.extras] +dev = ["pre-commit", "tox"] + [[package]] category = "dev" description = "library with cross-python path, ini-parsing, io, code, log facilities" @@ -564,6 +642,52 @@ isort = ">=4.2.5,<5" mccabe = ">=0.6,<0.7" toml = ">=0.7.1" +[[package]] +category = "dev" +description = "Python parsing module" +name = "pyparsing" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +version = "2.4.7" + +[[package]] +category = "dev" +description = "pytest: simple powerful testing with Python" +name = "pytest" +optional = false +python-versions = ">=3.5" +version = "6.0.1" + +[package.dependencies] +atomicwrites = ">=1.0" +attrs = ">=17.4.0" +colorama = "*" +iniconfig = "*" +more-itertools = ">=4.0.0" +packaging = "*" +pluggy = ">=0.12,<1.0" +py = ">=1.8.2" +toml = "*" + +[package.extras] +checkqa_mypy = ["mypy (0.780)"] +testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] + +[[package]] +category = "dev" +description = "Pytest plugin for measuring coverage." +name = "pytest-cov" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "2.10.0" + +[package.dependencies] +coverage = ">=4.4" +pytest = ">=4.6" + +[package.extras] +testing = ["fields", "hunter", "process-tests (2.0.2)", "six", "pytest-xdist", "virtualenv"] + [[package]] category = "dev" description = "YAML parser and emitter for Python" @@ -719,7 +843,7 @@ python-versions = "*" version = "1.12.1" [metadata] -content-hash = "e69534c9133ef3fb975f33937c97711f69281a8ffb931097a7b9e4ae1132fa09" +content-hash = "1a6ddbc4c05cb41329cbac4792210897f11dda8e8ba3aa7e814a4a0d0d9c4fa8" lock-version = "1.0" python-versions = "^3.8" @@ -744,6 +868,10 @@ astroid = [ {file = "astroid-2.4.2-py3-none-any.whl", hash = "sha256:bc58d83eb610252fd8de6363e39d4f1d0619c894b0ed24603b881c02e64c7386"}, {file = "astroid-2.4.2.tar.gz", hash = "sha256:2f4078c2a41bf377eea06d71c9d2ba4eb8f6b1af2135bec27bbbb7d8f12bb703"}, ] +atomicwrites = [ + {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, + {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, +] attrs = [ {file = "attrs-19.3.0-py2.py3-none-any.whl", hash = "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c"}, {file = "attrs-19.3.0.tar.gz", hash = "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"}, @@ -771,6 +899,42 @@ colorlog = [ {file = "colorlog-4.2.1-py2.py3-none-any.whl", hash = "sha256:43597fd822ce705190fc997519342fdaaf44b9b47f896ece7aa153ed4b909c74"}, {file = "colorlog-4.2.1.tar.gz", hash = "sha256:75e55822c3a3387d721579241e776de2cf089c9ef9528b1f09e8b04d403ad118"}, ] +coverage = [ + {file = "coverage-5.2.1-cp27-cp27m-macosx_10_13_intel.whl", hash = "sha256:40f70f81be4d34f8d491e55936904db5c527b0711b2a46513641a5729783c2e4"}, + {file = "coverage-5.2.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:675192fca634f0df69af3493a48224f211f8db4e84452b08d5fcebb9167adb01"}, + {file = "coverage-5.2.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:2fcc8b58953d74d199a1a4d633df8146f0ac36c4e720b4a1997e9b6327af43a8"}, + {file = "coverage-5.2.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:64c4f340338c68c463f1b56e3f2f0423f7b17ba6c3febae80b81f0e093077f59"}, + {file = "coverage-5.2.1-cp27-cp27m-win32.whl", hash = "sha256:52f185ffd3291196dc1aae506b42e178a592b0b60a8610b108e6ad892cfc1bb3"}, + {file = "coverage-5.2.1-cp27-cp27m-win_amd64.whl", hash = "sha256:30bc103587e0d3df9e52cd9da1dd915265a22fad0b72afe54daf840c984b564f"}, + {file = "coverage-5.2.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:9ea749fd447ce7fb1ac71f7616371f04054d969d412d37611716721931e36efd"}, + {file = "coverage-5.2.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ce7866f29d3025b5b34c2e944e66ebef0d92e4a4f2463f7266daa03a1332a651"}, + {file = "coverage-5.2.1-cp35-cp35m-macosx_10_13_x86_64.whl", hash = "sha256:4869ab1c1ed33953bb2433ce7b894a28d724b7aa76c19b11e2878034a4e4680b"}, + {file = "coverage-5.2.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:a3ee9c793ffefe2944d3a2bd928a0e436cd0ac2d9e3723152d6fd5398838ce7d"}, + {file = "coverage-5.2.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:28f42dc5172ebdc32622a2c3f7ead1b836cdbf253569ae5673f499e35db0bac3"}, + {file = "coverage-5.2.1-cp35-cp35m-win32.whl", hash = "sha256:e26c993bd4b220429d4ec8c1468eca445a4064a61c74ca08da7429af9bc53bb0"}, + {file = "coverage-5.2.1-cp35-cp35m-win_amd64.whl", hash = "sha256:4186fc95c9febeab5681bc3248553d5ec8c2999b8424d4fc3a39c9cba5796962"}, + {file = "coverage-5.2.1-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:b360d8fd88d2bad01cb953d81fd2edd4be539df7bfec41e8753fe9f4456a5082"}, + {file = "coverage-5.2.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:1adb6be0dcef0cf9434619d3b892772fdb48e793300f9d762e480e043bd8e716"}, + {file = "coverage-5.2.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:098a703d913be6fbd146a8c50cc76513d726b022d170e5e98dc56d958fd592fb"}, + {file = "coverage-5.2.1-cp36-cp36m-win32.whl", hash = "sha256:962c44070c281d86398aeb8f64e1bf37816a4dfc6f4c0f114756b14fc575621d"}, + {file = "coverage-5.2.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b1ed2bdb27b4c9fc87058a1cb751c4df8752002143ed393899edb82b131e0546"}, + {file = "coverage-5.2.1-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:c890728a93fffd0407d7d37c1e6083ff3f9f211c83b4316fae3778417eab9811"}, + {file = "coverage-5.2.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:538f2fd5eb64366f37c97fdb3077d665fa946d2b6d95447622292f38407f9258"}, + {file = "coverage-5.2.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:27ca5a2bc04d68f0776f2cdcb8bbd508bbe430a7bf9c02315cd05fb1d86d0034"}, + {file = "coverage-5.2.1-cp37-cp37m-win32.whl", hash = "sha256:aab75d99f3f2874733946a7648ce87a50019eb90baef931698f96b76b6769a46"}, + {file = "coverage-5.2.1-cp37-cp37m-win_amd64.whl", hash = "sha256:c2ff24df02a125b7b346c4c9078c8936da06964cc2d276292c357d64378158f8"}, + {file = "coverage-5.2.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:304fbe451698373dc6653772c72c5d5e883a4aadaf20343592a7abb2e643dae0"}, + {file = "coverage-5.2.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:c96472b8ca5dc135fb0aa62f79b033f02aa434fb03a8b190600a5ae4102df1fd"}, + {file = "coverage-5.2.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:8505e614c983834239f865da2dd336dcf9d72776b951d5dfa5ac36b987726e1b"}, + {file = "coverage-5.2.1-cp38-cp38-win32.whl", hash = "sha256:700997b77cfab016533b3e7dbc03b71d33ee4df1d79f2463a318ca0263fc29dd"}, + {file = "coverage-5.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:46794c815e56f1431c66d81943fa90721bb858375fb36e5903697d5eef88627d"}, + {file = "coverage-5.2.1-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:16042dc7f8e632e0dcd5206a5095ebd18cb1d005f4c89694f7f8aafd96dd43a3"}, + {file = "coverage-5.2.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:c1bbb628ed5192124889b51204de27c575b3ffc05a5a91307e7640eff1d48da4"}, + {file = "coverage-5.2.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:4f6428b55d2916a69f8d6453e48a505c07b2245653b0aa9f0dee38785939f5e4"}, + {file = "coverage-5.2.1-cp39-cp39-win32.whl", hash = "sha256:9e536783a5acee79a9b308be97d3952b662748c4037b6a24cbb339dc7ed8eb89"}, + {file = "coverage-5.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:b8f58c7db64d8f27078cbf2a4391af6aa4e4767cc08b37555c4ae064b8558d9b"}, + {file = "coverage-5.2.1.tar.gz", hash = "sha256:a34cb28e0747ea15e82d13e14de606747e9e484fb28d63c999483f5d5188e89b"}, +] darglint = [ {file = "darglint-1.5.2-py3-none-any.whl", hash = "sha256:049a98cf3aec8cf6ea344a863c68112d80b7f8de214459b5fa6853371f89c3e7"}, {file = "darglint-1.5.2.tar.gz", hash = "sha256:6b9461f96694c2cf1d8edb1597a783fe6840953b0eb18cc6cc1e72a26f196d79"}, @@ -838,10 +1002,18 @@ flake8-isort = [ {file = "flake8-isort-3.0.1.tar.gz", hash = "sha256:5d976da513cc390232ad5a9bb54aee8a092466a15f442d91dfc525834bee727a"}, {file = "flake8_isort-3.0.1-py2.py3-none-any.whl", hash = "sha256:df1dd6dd73f6a8b128c9c783356627231783cccc82c13c6dc343d1a5a491699b"}, ] +flake8-plugin-utils = [ + {file = "flake8-plugin-utils-1.3.0.tar.gz", hash = "sha256:965931e7c17a760915e38bb10dc60516b414ef8210e987252a8d73dcb196a5f5"}, + {file = "flake8_plugin_utils-1.3.0-py3-none-any.whl", hash = "sha256:305461c4fbf94877bcc9ccf435771b135d72a40eefd92e70a4b5f761ca43b1c8"}, +] flake8-polyfill = [ {file = "flake8-polyfill-1.0.2.tar.gz", hash = "sha256:e44b087597f6da52ec6393a709e7108b2905317d0c0b744cdca6208e670d8eda"}, {file = "flake8_polyfill-1.0.2-py2.py3-none-any.whl", hash = "sha256:12be6a34ee3ab795b19ca73505e7b55826d5f6ad7230d31b18e106400169b9e9"}, ] +flake8-pytest-style = [ + {file = "flake8-pytest-style-1.2.2.tar.gz", hash = "sha256:93830b18d961613a012061867b15b1434e433662197ec3a7594db8389637d5f9"}, + {file = "flake8_pytest_style-1.2.2-py3-none-any.whl", hash = "sha256:63e93d10b9cca6c73309319e89e7b1a8d316c7d1f12407faca975c498192fde3"}, +] flake8-quotes = [ {file = "flake8-quotes-2.1.2.tar.gz", hash = "sha256:c844c9592940c8926c60f00bc620808912ff2acd34923ab5338f3a5ca618a331"}, ] @@ -860,6 +1032,10 @@ gitpython = [ {file = "GitPython-3.1.7-py3-none-any.whl", hash = "sha256:fa3b92da728a457dd75d62bb5f3eb2816d99a7fe6c67398e260637a40e3fafb5"}, {file = "GitPython-3.1.7.tar.gz", hash = "sha256:2db287d71a284e22e5c2846042d0602465c7434d910406990d5b74df4afb0858"}, ] +iniconfig = [ + {file = "iniconfig-1.0.1-py3-none-any.whl", hash = "sha256:80cf40c597eb564e86346103f609d74efce0f6b4d4f30ec8ce9e2c26411ba437"}, + {file = "iniconfig-1.0.1.tar.gz", hash = "sha256:e5f92f89355a67de0595932a6c6c02ab4afddc6fcdc0bfc5becd0d60884d3f69"}, +] isort = [ {file = "isort-4.3.21-py2.py3-none-any.whl", hash = "sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd"}, {file = "isort-4.3.21.tar.gz", hash = "sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1"}, @@ -891,6 +1067,10 @@ mccabe = [ {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, ] +more-itertools = [ + {file = "more-itertools-8.4.0.tar.gz", hash = "sha256:68c70cc7167bdf5c7c9d8f6954a7837089c6a36bf565383919bb595efb8a17e5"}, + {file = "more_itertools-8.4.0-py3-none-any.whl", hash = "sha256:b78134b2063dd214000685165d81c154522c3ee0a1c0d4d113c80361c234c5a2"}, +] mypy = [ {file = "mypy-0.782-cp35-cp35m-macosx_10_6_x86_64.whl", hash = "sha256:2c6cde8aa3426c1682d35190b59b71f661237d74b053822ea3d748e2c9578a7c"}, {file = "mypy-0.782-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9c7a9a7ceb2871ba4bac1cf7217a7dd9ccd44c27c2950edbc6dc08530f32ad4e"}, @@ -915,6 +1095,10 @@ nox = [ {file = "nox-2020.5.24-py3-none-any.whl", hash = "sha256:c4509621fead99473a1401870e680b0aadadce5c88440f0532863595176d64c1"}, {file = "nox-2020.5.24.tar.gz", hash = "sha256:61a55705736a1a73efbd18d5b262a43d55a1176546e0eb28b29064cfcffe26c0"}, ] +packaging = [ + {file = "packaging-20.4-py2.py3-none-any.whl", hash = "sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181"}, + {file = "packaging-20.4.tar.gz", hash = "sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8"}, +] pathspec = [ {file = "pathspec-0.8.0-py2.py3-none-any.whl", hash = "sha256:7d91249d21749788d07a2d0f94147accd8f845507400749ea19c1ec9054a12b0"}, {file = "pathspec-0.8.0.tar.gz", hash = "sha256:da45173eb3a6f2a5a487efba21f050af2b41948be6ab52b6a1e3ff22bb8b7061"}, @@ -927,6 +1111,10 @@ pep8-naming = [ {file = "pep8-naming-0.9.1.tar.gz", hash = "sha256:a33d38177056321a167decd6ba70b890856ba5025f0a8eca6a3eda607da93caf"}, {file = "pep8_naming-0.9.1-py2.py3-none-any.whl", hash = "sha256:45f330db8fcfb0fba57458c77385e288e7a3be1d01e8ea4268263ef677ceea5f"}, ] +pluggy = [ + {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, + {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, +] py = [ {file = "py-1.9.0-py2.py3-none-any.whl", hash = "sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2"}, {file = "py-1.9.0.tar.gz", hash = "sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342"}, @@ -951,6 +1139,18 @@ pylint = [ {file = "pylint-2.5.3-py3-none-any.whl", hash = "sha256:d0ece7d223fe422088b0e8f13fa0a1e8eb745ebffcb8ed53d3e95394b6101a1c"}, {file = "pylint-2.5.3.tar.gz", hash = "sha256:7dd78437f2d8d019717dbf287772d0b2dbdfd13fc016aa7faa08d67bccc46adc"}, ] +pyparsing = [ + {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, + {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, +] +pytest = [ + {file = "pytest-6.0.1-py3-none-any.whl", hash = "sha256:8b6007800c53fdacd5a5c192203f4e531eb2a1540ad9c752e052ec0f7143dbad"}, + {file = "pytest-6.0.1.tar.gz", hash = "sha256:85228d75db9f45e06e57ef9bf4429267f81ac7c0d742cc9ed63d09886a9fe6f4"}, +] +pytest-cov = [ + {file = "pytest-cov-2.10.0.tar.gz", hash = "sha256:1a629dc9f48e53512fcbfda6b07de490c374b0c83c55ff7a1720b3fccff0ac87"}, + {file = "pytest_cov-2.10.0-py2.py3-none-any.whl", hash = "sha256:6e6d18092dce6fad667cd7020deed816f858ad3b49d5b5e2b1cc1c97a4dba65c"}, +] pyyaml = [ {file = "PyYAML-5.3.1-cp27-cp27m-win32.whl", hash = "sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f"}, {file = "PyYAML-5.3.1-cp27-cp27m-win_amd64.whl", hash = "sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76"}, diff --git a/pyproject.toml b/pyproject.toml index b63b74f..c4934e0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,6 +41,12 @@ 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" diff --git a/setup.cfg b/setup.cfg index 93815ad..59af783 100644 --- a/setup.cfg +++ b/setup.cfg @@ -2,6 +2,23 @@ # 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. @@ -57,6 +74,8 @@ select = 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. @@ -70,6 +89,10 @@ extend-ignore = 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, per-file-ignores = noxfile.py: @@ -79,6 +102,11 @@ per-file-ignores = WPS213, # No overuse of string constants (e.g., '--version'). WPS226, + tests/*.py: + # `assert` statements are ok in the test suite. + S101, + # Shadowing outer scopes occurs naturally with mocks. + WPS442, # Explicitly set mccabe's maximum complexity to 10 as recommended by # Thomas McCabe, the inventor of the McCabe complexity, and the NIST. @@ -106,6 +134,17 @@ 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 @@ -134,7 +173,7 @@ single_line_exclusions = typing [mypy] cache_dir = .cache/mypy -[mypy-nox.*] +[mypy-nox.*,packaging.*,pytest] ignore_missing_imports = true @@ -165,6 +204,15 @@ disable = 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 diff --git a/src/urban_meal_delivery/__init__.py b/src/urban_meal_delivery/__init__.py index 27023c3..d17278b 100644 --- a/src/urban_meal_delivery/__init__.py +++ b/src/urban_meal_delivery/__init__.py @@ -1 +1,9 @@ """Source code for the urban-meal-delivery research project.""" + +from importlib import metadata + + +try: + __version__ = metadata.version(__name__) +except metadata.PackageNotFoundError: # pragma: no cover + __version__ = 'unknown' diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..606a650 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +"""Test the urban-meal-delivery package.""" diff --git a/tests/test_version.py b/tests/test_version.py new file mode 100644 index 0000000..e3bf23c --- /dev/null +++ b/tests/test_version.py @@ -0,0 +1,116 @@ +"""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 +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: + """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: + """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: + """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: + """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) -> None: + """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: str) -> None: + """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: str) -> None: + """Versions follow the x.y.z or x.y.z.devN format.""" + result = self.version_pattern.fullmatch(version) + + assert result is None