diff options
-rw-r--r-- | .appveyor.yml | 10 | ||||
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | .travis.yml | 18 | ||||
-rw-r--r-- | MANIFEST.in | 2 | ||||
-rw-r--r-- | azure-pipelines.yml | 12 | ||||
-rw-r--r-- | noxfile.py | 18 | ||||
-rw-r--r-- | setup.cfg | 4 | ||||
-rwxr-xr-x | setup.py | 22 | ||||
-rw-r--r-- | tasks.py | 265 |
9 files changed, 246 insertions, 106 deletions
diff --git a/.appveyor.yml b/.appveyor.yml index 3e147335..33f48d91 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -7,12 +7,12 @@ environment: - PYTHON: "C:\\Miniconda36-x64" PYTHON_VERSION: "3.6.x" PYTHON_ARCH: "64" - TOXENV: "py36" + NOXSESSION: "tests-3.6" - PYTHON: "C:\\Miniconda37-x64" PYTHON_VERSION: "3.7.x" PYTHON_ARCH: "64" - TOXENV: "py37" + NOXSESSION: "tests-3.7" init: - "%PYTHON%/python -V" @@ -30,10 +30,10 @@ install: # Update conda stuff to make sure pip, setuptools, wheel etc are up to date - "conda update --all -y" - # Install tox - - "python -m pip install --upgrade tox" + # Install nox + - "python -m pip install --upgrade nox" test_script: -- "tox -e %TOXENV%" +- "nox --non-interactive --session %NOXSESSION%" @@ -7,6 +7,7 @@ cmd2.egg-info .cache *.pyc .tox +.nox .pytest_cache # Code Coverage diff --git a/.travis.yml b/.travis.yml index 5f83a058..521c383c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,25 +4,25 @@ matrix: include: - os: linux python: 3.5 - env: TOXENV=py35 + env: NOXSESSION=tests-3.5 - os: linux python: 3.6 - env: TOXENV=py36 + env: NOXSESSION=tests-3.6 - os: linux python: 3.7 dist: xenial - env: TOXENV=py37 + env: NOXSESSION=tests-3.7 - os: linux python: 3.8 dist: xenial - env: TOXENV=py38 + env: NOXSESSION=tests-3.8 - os: linux python: 3.9-dev dist: xenial - env: TOXENV=py39 + env: NOXSESSION=tests-3.9 - os: linux python: 3.7 - env: TOXENV=docs + env: NOXSESSION=docs # # Warning: Don't try to use code coverage analysis with pypy as it is insanely slow # - os: linux # python: pypy3 @@ -35,7 +35,7 @@ matrix: # - BREW_INSTALL=python3 install: - - pip install flake8 tox + - pip install flake8 nox # - | # if [[ $TRAVIS_OS_NAME == 'osx' ]]; then # if [[ -n "$BREW_INSTALL" ]]; then @@ -47,9 +47,9 @@ install: before_script: # stop the build if there are Python syntax errors or undefined names # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - if [[ $TOXENV == py38 ]]; then + if [[ $NOXSESSION == tests-3.8 ]]; then flake8 . --count --ignore=E252,W503 --max-complexity=26 --max-line-length=127 --show-source --statistics ; fi script: - - tox + - echo "$NOXSESSION"; nox --non-interactive --session "$NOXSESSION" diff --git a/MANIFEST.in b/MANIFEST.in index b0606391..734f73ad 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,4 @@ -include LICENSE README.md CHANGELOG.md CONTRIBUTING.md tox.ini Pipfile +include LICENSE README.md CHANGELOG.md CONTRIBUTING.md tox.ini noxfile.py Pipfile recursive-include examples * recursive-include tests * recursive-include docs * diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 49f7daf3..5f58bc0d 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -16,16 +16,16 @@ jobs: matrix: Python35: python.version: '3.5' - TOXENV: 'py35' + NOXSESSION: 'tests-3.5' Python36: python.version: '3.6' - TOXENV: 'py36' + NOXSESSION: 'tests-3.6' Python37: python.version: '3.7' - TOXENV: 'py37' + NOXSESSION: 'tests-3.7' Python38: python.version: '3.8' - TOXENV: 'py38' + NOXSESSION: 'tests-3.8' # Increase the maxParallel value to simultaneously run the job for all versions in the matrix (max 10 for free open-source) maxParallel: 4 @@ -38,7 +38,7 @@ jobs: # Install dependencies - install specific PyPI packages with pip, including cmd2 dependencies - script: | - python -m pip install --upgrade pip && pip3 install --upgrade setuptools tox + python -m pip install --upgrade pip && pip3 install --upgrade setuptools nox displayName: 'Upgrade pip and setuptools' continueOnError: false @@ -46,7 +46,7 @@ jobs: # Test - test with pytest, collect coverage metrics with pytest-cov, and publish these metrics to codecov.io - script: | - tox -e $(TOXENV) + nox --non-interactive --session $(NOXSESSION) displayName: 'Run tests and code coverage' continueOnError: false diff --git a/noxfile.py b/noxfile.py new file mode 100644 index 00000000..483369f9 --- /dev/null +++ b/noxfile.py @@ -0,0 +1,18 @@ +import nox + + +@nox.session(python=['3.7']) +def docs(session): + session.install('sphinx', 'sphinx-rtd-theme', '.') + session.chdir('docs') + tmpdir = session.create_tmp() + + session.run('sphinx-build', '-a', '-W', '-T', '-b', 'html', + '-d', '{}/doctrees'.format(tmpdir), '.', '{}/html'.format(tmpdir)) + + +@nox.session(python=['3.5', '3.6', '3.7', '3.8', '3.9']) +def tests(session): + session.install('invoke', './[test]') + session.run('invoke', 'pytest', '--junit', '--no-pty') + session.run('codecov') @@ -1,5 +1,5 @@ [flake8] -exclude = .git,.idea,.pytest_cache,.tox,.venv,.vscode,build,cmd2.egg-info,dist,htmlcov,__pycache__,*.egg +exclude = .git,.idea,.pytest_cache,.tox,.nox,.venv,.vscode,build,cmd2.egg-info,dist,htmlcov,__pycache__,*.egg max-line-length = 127 max-complexity = 26 @@ -12,6 +12,6 @@ force_grid_wrap = 0 use_parentheses = true [doc8] -ignore-path=docs/_build,.git,.idea,.pytest_cache,.tox,.venv,.vscode,build,cmd2,examples,tests,cmd2.egg-info,dist,htmlcov,__pycache__,*.egg +ignore-path=docs/_build,.git,.idea,.pytest_cache,.tox,.nox,.venv,.vscode,build,cmd2,examples,tests,cmd2.egg-info,dist,htmlcov,__pycache__,*.egg max-line-length=117 verbose=0 @@ -33,18 +33,30 @@ Topic :: Software Development :: Libraries :: Python Modules SETUP_REQUIRES = ['setuptools_scm >= 3.0'] -INSTALL_REQUIRES = ['attrs >= 16.3.0', 'colorama >= 0.3.7', 'pyperclip >= 1.6', 'setuptools >= 34.4', 'wcwidth >= 0.1.7'] +INSTALL_REQUIRES = [ + 'attrs >= 16.3.0', + 'colorama >= 0.3.7', + 'pyperclip >= 1.6', + 'setuptools >= 34.4', + 'wcwidth >= 0.1.7', +] EXTRAS_REQUIRE = { # Windows also requires pyreadline to ensure tab completion works ":sys_platform=='win32'": ['pyreadline'], # Extra dependencies for running unit tests - 'test': ["gnureadline; sys_platform=='darwin'", # include gnureadline on macOS to ensure it is available in tox env - "mock ; python_version<'3.6'", # for python 3.5 we need the third party mock module - 'codecov', 'coverage', 'pytest', 'pytest-cov', 'pytest-mock'], + 'test': [ + "gnureadline; sys_platform=='darwin'", # include gnureadline on macOS to ensure it is available in tox env + "mock ; python_version<'3.6'", # for python 3.5 we need the third party mock module + 'codecov', + 'coverage', + 'pytest', + 'pytest-cov', + 'pytest-mock', + ], # development only dependencies: install with 'pip install -e .[dev]' 'dev': ["mock ; python_version<'3.6'", # for python 3.5 we need the third party mock module - 'pytest', 'codecov', 'pytest-cov', 'pytest-mock', 'tox', 'flake8', + 'pytest', 'codecov', 'pytest-cov', 'pytest-mock', 'tox', 'nox', 'flake8', 'sphinx', 'sphinx-rtd-theme', 'sphinx-autobuild', 'doc8', 'invoke', 'twine>=1.11', ] @@ -9,16 +9,20 @@ Make sure you satisfy the following Python module requirements if you are trying - setuptools >= 39.1.0 """ import os +import invoke +import pathlib import re import shutil import sys -import invoke + +TASK_ROOT = pathlib.Path(__file__).resolve().parent +TASK_ROOT_STR = str(TASK_ROOT) # shared function def rmrf(items, verbose=True): - "Silently remove a list of directories or files" + """Silently remove a list of directories or files""" if isinstance(items, str): items = [items] @@ -40,52 +44,93 @@ namespace.add_collection(namespace_clean, 'clean') ##### # -# pytest, tox, pylint, and codecov +# pytest, tox, nox, pylint, and codecov # ##### + + @invoke.task -def pytest(context): - "Run tests and code coverage using pytest" - context.run("pytest --cov=cmd2 --cov-report=term --cov-report=html", pty=True) +def pytest(context, junit=False, pty=True): + """Run tests and code coverage using pytest""" + with context.cd(TASK_ROOT_STR): + command_str = 'pytest --cov=cmd2 --cov-report=term --cov-report=html' + if junit: + # command_str += ' --junitxml={}/junit/test-results.xml'.format(TASK_ROOT_STR) + command_str += ' --junitxml=junit/test-results.xml' + command_str += ' tests' + context.run(command_str, pty=pty) + + namespace.add_task(pytest) + @invoke.task def pytest_clean(context): - "Remove pytest cache and code coverage files and directories" - #pylint: disable=unused-argument - dirs = ['.pytest_cache', '.cache', 'htmlcov', '.coverage'] + """Remove pytest cache and code coverage files and directories""" + # pylint: disable=unused-argument + with context.cd(str(TASK_ROOT/'tests')): + dirs = ['.pytest_cache', '.cache', 'htmlcov', '.coverage'] + rmrf(dirs) rmrf(dirs) + + namespace_clean.add_task(pytest_clean, 'pytest') + @invoke.task def mypy(context): - "Run mypy optional static type checker" - context.run("mypy main.py") - namespace.add_task(mypy) + """Run mypy optional static type checker""" + with context.cd(TASK_ROOT_STR): + context.run("mypy main.py") + + namespace.add_task(mypy) + @invoke.task def mypy_clean(context): - "Remove mypy cache directory" - #pylint: disable=unused-argument - dirs = ['.mypy_cache', 'dmypy.json', 'dmypy.sock'] - rmrf(dirs) + """Remove mypy cache directory""" + # pylint: disable=unused-argument + with context.cd(TASK_ROOT_STR): + dirs = ['.mypy_cache', 'dmypy.json', 'dmypy.sock'] + rmrf(dirs) + + namespace_clean.add_task(mypy_clean, 'mypy') + @invoke.task def tox(context): - "Run unit and integration tests on multiple python versions using tox" - context.run("tox") + """Run unit and integration tests on multiple python versions using tox""" + with context.cd(TASK_ROOT_STR): + context.run("tox") + + namespace.add_task(tox) + @invoke.task def tox_clean(context): - "Remove tox virtualenvs and logs" - #pylint: disable=unused-argument - rmrf('.tox') + """Remove tox virtualenvs and logs""" + # pylint: disable=unused-argument + with context.cd(TASK_ROOT_STR): + rmrf('.tox') + + namespace_clean.add_task(tox_clean, 'tox') +@invoke.task +def nox_clean(context): + """Remove nox virtualenvs and logs""" + # pylint: disable=unused-argument + with context.cd(TASK_ROOT_STR): + rmrf('.nox') + + +namespace_clean.add_task(nox_clean, 'nox') + + ##### # # documentation @@ -95,39 +140,59 @@ DOCS_SRCDIR = 'docs' DOCS_BUILDDIR = os.path.join('docs', '_build') SPHINX_OPTS = '-nvWT' # Be nitpicky, verbose, and treat warnings as errors + @invoke.task() def docs(context, builder='html'): - "Build documentation using sphinx" - cmdline = 'python -msphinx -M {} {} {} {}'.format(builder, DOCS_SRCDIR, DOCS_BUILDDIR, SPHINX_OPTS) - context.run(cmdline, pty=True) + """Build documentation using sphinx""" + with context.cd(TASK_ROOT_STR): + cmdline = 'python -msphinx -M {} {} {} {}'.format(builder, DOCS_SRCDIR, DOCS_BUILDDIR, SPHINX_OPTS) + context.run(cmdline, pty=True) + + namespace.add_task(docs) + @invoke.task() def doc8(context): - "Check documentation with doc8" - context.run('doc8 docs --ignore-path docs/_build') + """Check documentation with doc8""" + with context.cd(TASK_ROOT_STR): + context.run('doc8 docs --ignore-path docs/_build') + + namespace.add_task(doc8) + @invoke.task def docs_clean(context): - "Remove rendered documentation" - #pylint: disable=unused-argument - rmrf(DOCS_BUILDDIR) + """Remove rendered documentation""" + # pylint: disable=unused-argument + with context.cd(TASK_ROOT_STR): + rmrf(DOCS_BUILDDIR) + + namespace_clean.add_task(docs_clean, name='docs') + @invoke.task() def linkcheck(context): """Check external links in Sphinx documentation for integrity.""" - context.run('cd docs && make linkcheck', pty=True) + with context.cd(str(TASK_ROOT/'docs')): + context.run('make linkcheck', pty=True) + + namespace.add_task(linkcheck) + @invoke.task def livehtml(context): - "Launch webserver on http://localhost:8000 with rendered documentation" - builder = 'html' - outputdir = os.path.join(DOCS_BUILDDIR, builder) - cmdline = 'sphinx-autobuild -b {} {} {}'.format(builder, DOCS_SRCDIR, outputdir) - context.run(cmdline, pty=True) + """Launch webserver on http://localhost:8000 with rendered documentation""" + with context.cd(TASK_ROOT_STR): + builder = 'html' + outputdir = os.path.join(DOCS_BUILDDIR, builder) + cmdline = 'sphinx-autobuild -b {} {} {}'.format(builder, DOCS_SRCDIR, outputdir) + context.run(cmdline, pty=True) + + namespace.add_task(livehtml) @@ -139,112 +204,156 @@ namespace.add_task(livehtml) BUILDDIR = 'build' DISTDIR = 'dist' + @invoke.task def build_clean(context): - "Remove the build directory" - #pylint: disable=unused-argument - rmrf(BUILDDIR) + """Remove the build directory""" + # pylint: disable=unused-argument + with context.cd(TASK_ROOT_STR): + rmrf(BUILDDIR) + + namespace_clean.add_task(build_clean, 'build') + @invoke.task def dist_clean(context): - "Remove the dist directory" - #pylint: disable=unused-argument - rmrf(DISTDIR) + """Remove the dist directory""" + # pylint: disable=unused-argument + with context.cd(TASK_ROOT_STR): + rmrf(DISTDIR) + + namespace_clean.add_task(dist_clean, 'dist') + @invoke.task def eggs_clean(context): - "Remove egg directories" - #pylint: disable=unused-argument - dirs = set() - dirs.add('.eggs') - for name in os.listdir(os.curdir): - if name.endswith('.egg-info'): - dirs.add(name) - if name.endswith('.egg'): - dirs.add(name) - rmrf(dirs) + """Remove egg directories""" + # pylint: disable=unused-argument + with context.cd(TASK_ROOT_STR): + dirs = set() + dirs.add('.eggs') + for name in os.listdir(os.curdir): + if name.endswith('.egg-info'): + dirs.add(name) + if name.endswith('.egg'): + dirs.add(name) + rmrf(dirs) + + namespace_clean.add_task(eggs_clean, 'eggs') + @invoke.task def pycache_clean(context): - "Remove __pycache__ directories" - #pylint: disable=unused-argument - dirs = set() - for root, dirnames, _ in os.walk(os.curdir): - if '__pycache__' in dirnames: - dirs.add(os.path.join(root, '__pycache__')) - print("Removing __pycache__ directories") - rmrf(dirs, verbose=False) + """Remove __pycache__ directories""" + # pylint: disable=unused-argument + with context.cd(TASK_ROOT_STR): + dirs = set() + for root, dirnames, _ in os.walk(os.curdir): + if '__pycache__' in dirnames: + dirs.add(os.path.join(root, '__pycache__')) + print("Removing __pycache__ directories") + rmrf(dirs, verbose=False) + + namespace_clean.add_task(pycache_clean, 'pycache') # # make a dummy clean task which runs all the tasks in the clean namespace clean_tasks = list(namespace_clean.tasks.values()) + + @invoke.task(pre=list(namespace_clean.tasks.values()), default=True) -def clean_all(context): - "Run all clean tasks" - #pylint: disable=unused-argument +def clean_all(_): + """Run all clean tasks""" + # pylint: disable=unused-argument pass + + namespace_clean.add_task(clean_all, 'all') + @invoke.task def tag(context, name, message=''): - "Add a Git tag and push it to origin" + """Add a Git tag and push it to origin""" # If a tag was provided on the command-line, then add a Git tag and push it to origin if name: context.run('git tag -a {} -m {!r}'.format(name, message)) context.run('git push origin {}'.format(name)) + + namespace.add_task(tag) + @invoke.task() def validatetag(context): - "Check to make sure that a tag exists for the current HEAD and it looks like a valid version number" + """Check to make sure that a tag exists for the current HEAD and it looks like a valid version number""" # Validate that a Git tag exists for the current commit HEAD result = context.run("git describe --exact-match --tags $(git log -n1 --pretty='%h')") - tag = result.stdout.rstrip() + git_tag = result.stdout.rstrip() # Validate that the Git tag appears to be a valid version number ver_regex = re.compile(r'(\d+)\.(\d+)\.(\d+)') - match = ver_regex.fullmatch(tag) + match = ver_regex.fullmatch(git_tag) if match is None: - print('Tag {!r} does not appear to be a valid version number'.format(tag)) + print('Tag {!r} does not appear to be a valid version number'.format(git_tag)) sys.exit(-1) else: - print('Tag {!r} appears to be a valid version number'.format(tag)) + print('Tag {!r} appears to be a valid version number'.format(git_tag)) namespace.add_task(validatetag) + @invoke.task(pre=[clean_all]) def sdist(context): - "Create a source distribution" - context.run('python setup.py sdist') + """Create a source distribution""" + with context.cd(TASK_ROOT_STR): + context.run('python setup.py sdist') + + namespace.add_task(sdist) + @invoke.task(pre=[clean_all]) def wheel(context): - "Build a wheel distribution" - context.run('python setup.py bdist_wheel') + """Build a wheel distribution""" + with context.cd(TASK_ROOT_STR): + context.run('python setup.py bdist_wheel') + + namespace.add_task(wheel) + @invoke.task(pre=[validatetag, sdist, wheel]) def pypi(context): - "Build and upload a distribution to pypi" - context.run('twine upload dist/*') + """Build and upload a distribution to pypi""" + with context.cd(TASK_ROOT_STR): + context.run('twine upload dist/*') + + namespace.add_task(pypi) + @invoke.task(pre=[validatetag, sdist, wheel]) def pypi_test(context): - "Build and upload a distribution to https://test.pypi.org" - context.run('twine upload --repository-url https://test.pypi.org/legacy/ dist/*') + """Build and upload a distribution to https://test.pypi.org""" + with context.cd(TASK_ROOT_STR): + context.run('twine upload --repository-url https://test.pypi.org/legacy/ dist/*') + + namespace.add_task(pypi_test) # Flake8 - linter and tool for style guide enforcement and linting @invoke.task def flake8(context): - "Run flake8 linter and tool for style guide enforcement" - context.run("flake8 --ignore=E252,W503 --max-complexity=26 --max-line-length=127 --show-source --statistics --exclude=.git,__pycache__,.tox,.eggs,*.egg,.venv,.idea,.pytest_cache,.vscode,build,dist,htmlcov") + """Run flake8 linter and tool for style guide enforcement""" + with context.cd(TASK_ROOT_STR): + context.run("flake8 --ignore=E252,W503 --max-complexity=26 --max-line-length=127 --show-source --statistics " + "--exclude=.git,__pycache__,.tox,.nox,.eggs,*.egg,.venv,.idea,.pytest_cache,.vscode,build,dist,htmlcov") + + namespace.add_task(flake8) |