diff --git a/.github/workflows/dependabot.yml b/.github/workflows/dependabot.yml index 137a1cb..c341d22 100644 --- a/.github/workflows/dependabot.yml +++ b/.github/workflows/dependabot.yml @@ -2,7 +2,7 @@ name: Dependabot on: pull_request: - branches: [ master ] + branches: [master] jobs: automerge: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 20f91f2..f3c8b1a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,25 +6,3 @@ repos: - id: end-of-file-fixer - id: trailing-whitespace - id: check-added-large-files - - repo: https://github.com/prettier/prettier - rev: 2.0.5 - hooks: - - id: prettier - - repo: local - hooks: - - id: black - name: black - entry: poetry run black - language: system - types: [python] - - id: flake8 - name: flake8 - entry: poetry run flake8 - language: system - types: [python] - - id: mypy - name: mypy - entry: poetry run mypy - language: system - types: [python] - require_serial: true diff --git a/noxfile.py b/noxfile.py index 81f9858..5d5cefa 100644 --- a/noxfile.py +++ b/noxfile.py @@ -1,106 +1,151 @@ """Nox sessions.""" +import contextlib import tempfile -from typing import Any +from pathlib import Path +from typing import cast +from typing import Iterator import nox from nox.sessions import Session + package = "toml_validator" -nox.options.sessions = "lint", "safety", "mypy", "tests" +python_versions = ["3.8"] +nox.options.sessions = "pre-commit", "safety", "mypy", "tests" locations = "src", "tests", "noxfile.py" -def install_with_constraints(session: Session, *args: str, **kwargs: Any) -> None: - """Install packages constrained by Poetry's lock file. - This function is a wrapper for nox.sessions.Session.install. It - invokes pip to install packages inside of the session's virtualenv. - Additionally, pip is passed a constraints file generated from - Poetry's lock file, to ensure that the packages are pinned to the - versions specified in poetry.lock. This allows you to manage the - packages as Poetry development dependencies. - Arguments: +class Poetry: + """Helper class for invoking Poetry inside a Nox session. + + Attributes: session: The Session object. - args: Command-line arguments for pip. - kwargs: Additional keyword arguments for Session.install. """ - with tempfile.NamedTemporaryFile() as requirements: - session.run( - "poetry", - "export", - "--dev", - "--format=requirements.txt", - f"--output={requirements.name}", - external=True, + + def __init__(self, session: Session) -> None: + """Constructor.""" + self.session = session + + @contextlib.contextmanager + def export(self, *args: str) -> Iterator[Path]: + """Export the lock file to requirements format. + + Args: + args: Command-line arguments for ``poetry export``. + + Yields: + The path to the requirements file. + """ + with tempfile.TemporaryDirectory() as directory: + requirements = Path(directory) / "requirements.txt" + self.session.run( + "poetry", + "export", + *args, + "--format=requirements.txt", + f"--output={requirements}", + external=True, + ) + yield requirements + + def version(self) -> str: + """Retrieve the package version. + + Returns: + The package version. + """ + output = self.session.run( + "poetry", "version", external=True, silent=True, stderr=None ) - session.install(f"--constraint={requirements.name}", *args, **kwargs) + return cast(str, output).split()[1] + def build(self, *args: str) -> None: + """Build the package. -@nox.session(python="3.8") -def black(session: Session) -> None: - """Run black code formatter.""" - args = session.posargs or locations - install_with_constraints(session, "black") - session.run("black", *args) + Args: + args: Command-line arguments for ``poetry build``. + """ + self.session.run("poetry", "build", *args, external=True) -@nox.session(python=["3.8", "3.7"]) -def lint(session: Session) -> None: - """Lint using flake8.""" - args = session.posargs or locations - install_with_constraints( - session, "flake8", "flake8-bandit", "flake8-black", "flake8-bugbear", +def install_package(session: Session) -> None: + """Build and install the package. + + Build a wheel from the package, and install it into the virtual environment + of the specified Nox session. + + The package requirements are installed using the versions specified in + Poetry's lock file. + + Args: + session: The Session object. + """ + poetry = Poetry(session) + + with poetry.export() as requirements: + session.install(f"--requirement={requirements}") + + poetry.build("--format=wheel") + + version = poetry.version() + session.install( + "--no-deps", "--force-reinstall", f"dist/{package}-{version}-py3-none-any.whl" ) - session.run("flake8", *args) + + +def install(session: Session, *args: str) -> None: + """Install development dependencies into the session's virtual environment. + + This function is a wrapper for nox.sessions.Session.install. + + The packages must be managed as development dependencies in Poetry. + + Args: + session: The Session object. + args: Command-line arguments for ``pip install``. + """ + poetry = Poetry(session) + with poetry.export("--dev") as requirements: + session.install(f"--constraint={requirements}", *args) + + +@nox.session(name="pre-commit", python="3.8") +def precommit(session: Session) -> None: + """Lint using pre-commit.""" + args = session.posargs or ["run", "--all-files"] # , "--show-diff-on-failure"] + install(session, "pre-commit") + session.run("pre-commit", *args) @nox.session(python="3.8") def safety(session: Session) -> None: """Scan dependencies for insecure packages.""" - with tempfile.NamedTemporaryFile() as requirements: - session.run( - "poetry", - "export", - "--dev", - "--format=requirements.txt", - "--without-hashes", - f"--output={requirements.name}", - external=True, - ) - install_with_constraints(session, "safety") - session.run("safety", "check", f"--file={requirements.name}", "--full-report") + poetry = Poetry(session) + with poetry.export("--dev", "--without-hashes") as requirements: + install(session, "safety") + session.run("safety", "check", f"--file={requirements}", "--bare") -@nox.session(python=["3.8", "3.7"]) +@nox.session(python=python_versions) def mypy(session: Session) -> None: """Type-check using mypy.""" args = session.posargs or locations - install_with_constraints(session, "mypy") + install(session, "mypy") session.run("mypy", *args) -@nox.session(python=["3.8", "3.7"]) +@nox.session(python=python_versions) def tests(session: Session) -> None: """Run the test suite.""" - args = session.posargs or ["--cov", "-m", "not e2e"] - session.run("poetry", "install", "--no-dev", external=True) - install_with_constraints( - session, "coverage[toml]", "pytest", "pytest-cov", "pytest-mock" - ) + args = session.posargs or ["--cov"] + install_package(session) + install(session, "coverage[toml]", "pytest", "pytest-cov", "pytest_mock") session.run("pytest", *args) -@nox.session(python=["3.8", "3.7"]) +@nox.session(python=python_versions) def typeguard(session: Session) -> None: """Runtime type checking using Typeguard.""" - args = session.posargs or ["-m", "not e2e"] - session.run("poetry", "install", "--no-dev", external=True) - install_with_constraints(session, "pytest", "pytest-mock", "typeguard") - session.run("pytest", f"--typeguard-packages={package}", *args) - - -@nox.session(python="3.8") -def coverage(session: Session) -> None: - """Upload coverage data.""" - install_with_constraints(session, "coverage[toml]", "codecov") - session.run("coverage", "xml", "--fail-under=0") - session.run("codecov", *session.posargs) + install_package(session) + install(session, "pytest", "typeguard") + session.run("pytest", f"--typeguard-packages={package}", *session.posargs) diff --git a/poetry.lock b/poetry.lock index 3722006..466ca97 100644 --- a/poetry.lock +++ b/poetry.lock @@ -72,6 +72,14 @@ optional = false python-versions = "*" version = "2020.4.5.1" +[[package]] +category = "dev" +description = "Validate configuration and produce human readable error messages." +name = "cfgv" +optional = false +python-versions = ">=3.6.1" +version = "3.1.0" + [[package]] category = "dev" description = "Universal encoding detector for Python 2 and 3" @@ -125,6 +133,14 @@ version = "*" [package.extras] toml = ["toml"] +[[package]] +category = "dev" +description = "Distribution utilities" +name = "distlib" +optional = false +python-versions = "*" +version = "0.3.0" + [[package]] category = "dev" description = "A parser for Python dependency files" @@ -141,6 +157,14 @@ toml = "*" [package.extras] pipenv = ["pipenv"] +[[package]] +category = "dev" +description = "A platform independent file lock." +name = "filelock" +optional = false +python-versions = "*" +version = "3.0.12" + [[package]] category = "dev" description = "the modular source code checker: pep8 pyflakes and co" @@ -229,6 +253,17 @@ version = "3.1.2" [package.dependencies] gitdb = ">=4.0.1,<5" +[[package]] +category = "dev" +description = "File identification library for Python" +name = "identify" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" +version = "1.4.15" + +[package.extras] +license = ["editdistance"] + [[package]] category = "dev" description = "Internationalized Domain Names in Applications (IDNA)" @@ -293,6 +328,14 @@ optional = false python-versions = "*" version = "0.4.3" +[[package]] +category = "dev" +description = "Node.js virtual environment builder" +name = "nodeenv" +optional = false +python-versions = "*" +version = "1.3.5" + [[package]] category = "dev" description = "Core utilities for Python packages" @@ -337,6 +380,26 @@ version = ">=0.12" [package.extras] dev = ["pre-commit", "tox"] +[[package]] +category = "dev" +description = "A framework for managing and maintaining multi-language pre-commit hooks." +name = "pre-commit" +optional = false +python-versions = ">=3.6.1" +version = "2.4.0" + +[package.dependencies] +cfgv = ">=2.0.0" +identify = ">=1.0.0" +nodeenv = ">=0.11.1" +pyyaml = ">=5.1" +toml = "*" +virtualenv = ">=20.0.8" + +[package.dependencies.importlib-metadata] +python = "<3.8" +version = "*" + [[package]] category = "dev" description = "library with cross-python path, ini-parsing, io, code, log facilities" @@ -558,6 +621,28 @@ brotli = ["brotlipy (>=0.6.0)"] secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "pyOpenSSL (>=0.14)", "ipaddress"] socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7,<2.0)"] +[[package]] +category = "dev" +description = "Virtual Python Environment builder" +name = "virtualenv" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" +version = "20.0.20" + +[package.dependencies] +appdirs = ">=1.4.3,<2" +distlib = ">=0.3.0,<1" +filelock = ">=3.0.0,<4" +six = ">=1.9.0,<2" + +[package.dependencies.importlib-metadata] +python = "<3.8" +version = ">=0.12,<2" + +[package.extras] +docs = ["sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=19.9.0rc1)", "proselint (>=0.10.2)"] +testing = ["pytest (>=4)", "coverage (>=5)", "coverage-enable-subprocess (>=1)", "pytest-xdist (>=1.31.0)", "pytest-mock (>=2)", "pytest-env (>=0.6.2)", "pytest-randomly (>=1)", "pytest-timeout", "packaging (>=20.0)", "xonsh (>=0.9.16)"] + [[package]] category = "dev" description = "Measures number of Terminal column cells of wide-character codes" @@ -580,7 +665,7 @@ docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] testing = ["jaraco.itertools", "func-timeout"] [metadata] -content-hash = "3712b63cfe9118f0bd244419296b471a8df3c0ad7494068a110ff3b143264d30" +content-hash = "6c8742088ea26ccaf6b4bd298ae284c9d50a4ac9f23999500d4f217cdb5e4077" python-versions = "^3.7" [metadata.files] @@ -608,6 +693,10 @@ certifi = [ {file = "certifi-2020.4.5.1-py2.py3-none-any.whl", hash = "sha256:1d987a998c75633c40847cc966fcf5904906c920a7f17ef374f5aa4282abd304"}, {file = "certifi-2020.4.5.1.tar.gz", hash = "sha256:51fcb31174be6e6664c5f69e3e1691a2d72a1a12e90f872cbdb1567eb47b6519"}, ] +cfgv = [ + {file = "cfgv-3.1.0-py2.py3-none-any.whl", hash = "sha256:1ccf53320421aeeb915275a196e23b3b8ae87dea8ac6698b1638001d4a486d53"}, + {file = "cfgv-3.1.0.tar.gz", hash = "sha256:c8e8f552ffcc6194f4e18dd4f68d9aef0c0d58ae7e7be8c82bee3c5e9edfa513"}, +] chardet = [ {file = "chardet-3.0.4-py2.py3-none-any.whl", hash = "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"}, {file = "chardet-3.0.4.tar.gz", hash = "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae"}, @@ -657,10 +746,17 @@ coverage = [ {file = "coverage-5.1-cp39-cp39-win_amd64.whl", hash = "sha256:bb28a7245de68bf29f6fb199545d072d1036a1917dca17a1e75bbb919e14ee8e"}, {file = "coverage-5.1.tar.gz", hash = "sha256:f90bfc4ad18450c80b024036eaf91e4a246ae287701aaa88eaebebf150868052"}, ] +distlib = [ + {file = "distlib-0.3.0.zip", hash = "sha256:2e166e231a26b36d6dfe35a48c4464346620f8645ed0ace01ee31822b288de21"}, +] dparse = [ {file = "dparse-0.5.1-py3-none-any.whl", hash = "sha256:e953a25e44ebb60a5c6efc2add4420c177f1d8404509da88da9729202f306994"}, {file = "dparse-0.5.1.tar.gz", hash = "sha256:a1b5f169102e1c894f9a7d5ccf6f9402a836a5d24be80a986c7ce9eaed78f367"}, ] +filelock = [ + {file = "filelock-3.0.12-py3-none-any.whl", hash = "sha256:929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836"}, + {file = "filelock-3.0.12.tar.gz", hash = "sha256:18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59"}, +] flake8 = [ {file = "flake8-3.8.1-py2.py3-none-any.whl", hash = "sha256:6c1193b0c3f853ef763969238f6c81e9e63ace9d024518edc020d5f1d6d93195"}, {file = "flake8-3.8.1.tar.gz", hash = "sha256:ea6623797bf9a52f4c9577d780da0bb17d65f870213f7b5bcc9fca82540c31d5"}, @@ -687,6 +783,10 @@ gitpython = [ {file = "GitPython-3.1.2-py3-none-any.whl", hash = "sha256:da3b2cf819974789da34f95ac218ef99f515a928685db141327c09b73dd69c09"}, {file = "GitPython-3.1.2.tar.gz", hash = "sha256:864a47472548f3ba716ca202e034c1900f197c0fb3a08f641c20c3cafd15ed94"}, ] +identify = [ + {file = "identify-1.4.15-py2.py3-none-any.whl", hash = "sha256:88ed90632023e52a6495749c6732e61e08ec9f4f04e95484a5c37b9caf40283c"}, + {file = "identify-1.4.15.tar.gz", hash = "sha256:23c18d97bb50e05be1a54917ee45cc61d57cb96aedc06aabb2b02331edf0dbf0"}, +] idna = [ {file = "idna-2.9-py2.py3-none-any.whl", hash = "sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa"}, {file = "idna-2.9.tar.gz", hash = "sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb"}, @@ -723,6 +823,9 @@ mypy-extensions = [ {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, ] +nodeenv = [ + {file = "nodeenv-1.3.5-py2.py3-none-any.whl", hash = "sha256:5b2438f2e42af54ca968dd1b374d14a1194848955187b0e5e4be1f73813a5212"}, +] packaging = [ {file = "packaging-20.3-py2.py3-none-any.whl", hash = "sha256:82f77b9bee21c1bafbf35a84905d604d5d1223801d639cf3ed140bd651c08752"}, {file = "packaging-20.3.tar.gz", hash = "sha256:3c292b474fda1671ec57d46d739d072bfd495a4f51ad01a055121d81e952b7a3"}, @@ -739,6 +842,10 @@ pluggy = [ {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, ] +pre-commit = [ + {file = "pre_commit-2.4.0-py2.py3-none-any.whl", hash = "sha256:5559e09afcac7808933951ffaf4ff9aac524f31efbc3f24d021540b6c579813c"}, + {file = "pre_commit-2.4.0.tar.gz", hash = "sha256:703e2e34cbe0eedb0d319eff9f7b83e2022bb5a3ab5289a6a8841441076514d0"}, +] py = [ {file = "py-1.8.1-py2.py3-none-any.whl", hash = "sha256:c20fdd83a5dbc0af9efd622bee9a5564e278f6380fffcacc43ba6f43db2813b0"}, {file = "py-1.8.1.tar.gz", hash = "sha256:5e27081401262157467ad6e7f851b7aa402c5852dbcb3dae06768434de5752aa"}, @@ -868,6 +975,10 @@ urllib3 = [ {file = "urllib3-1.25.9-py2.py3-none-any.whl", hash = "sha256:88206b0eb87e6d677d424843ac5209e3fb9d0190d0ee169599165ec25e9d9115"}, {file = "urllib3-1.25.9.tar.gz", hash = "sha256:3018294ebefce6572a474f0604c2021e33b3fd8006ecd11d62107a5d2a963527"}, ] +virtualenv = [ + {file = "virtualenv-20.0.20-py2.py3-none-any.whl", hash = "sha256:b4c14d4d73a0c23db267095383c4276ef60e161f94fde0427f2f21a0132dde74"}, + {file = "virtualenv-20.0.20.tar.gz", hash = "sha256:fd0e54dec8ac96c1c7c87daba85f0a59a7c37fe38748e154306ca21c73244637"}, +] wcwidth = [ {file = "wcwidth-0.1.9-py2.py3-none-any.whl", hash = "sha256:cafe2186b3c009a04067022ce1dcd79cb38d8d65ee4f4791b8888d6599d1bbe1"}, {file = "wcwidth-0.1.9.tar.gz", hash = "sha256:ee73862862a156bf77ff92b09034fc4825dd3af9cf81bc5b360668d425f3c5f1"}, diff --git a/pyproject.toml b/pyproject.toml index 1c99d3d..d732591 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,6 +28,7 @@ mypy = "^0.770" codecov = "^2.0.22" pytest-mock = "^3.0.0" typeguard = "^2.7.1" +pre-commit = "^2.4.0" [tool.poetry.scripts] toml-validator = "toml_validator.__main__:main"