diff --git a/.coveragerc b/.coveragerc index 6f9e48e61..acdb32091 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,3 +1,7 @@ [run] branch = True source = klein + +[report] +precision = 2 +show_missing = True diff --git a/.gitignore b/.gitignore index 91d68db94..d2b3f8098 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,13 @@ *.pyc /_trial_temp/ /build/ -htmlcov -.coverage /src/*.egg-info /.tox/ /docs/_build/ .DS_Store /dist/ *~ +.coverage.* +/.hypothesis/ +/.mypy_cache/ +__pycache__ diff --git a/.travis.yml b/.travis.yml index 1ef9fe806..9f84fa9db 100644 --- a/.travis.yml +++ b/.travis.yml @@ -26,6 +26,17 @@ branches: # matrix: include: + - python: 3.5 + env: TOXENV=flake8 + - python: 3.5 + env: TOXENV=mypy + - python: 2.7 + env: TOXENV=twistedchecker-diff + - python: 2.7 + env: TOXENV=docs + - python: 2.7 + env: TOXENV=docs-linkcheck + # PyPy (Python 2.7) - env: TOXENV=coverage-pypy-tw155,codecov PYPY_VERSION=5.7.1 - env: TOXENV=coverage-pypy-tw160,codecov PYPY_VERSION=5.7.1 @@ -59,21 +70,21 @@ matrix: - python: 3.5 env: TOXENV=coverage-py35-twtrunk,codecov - - python: 3.5 - env: TOXENV=flake8 - - python: 2.7 - env: TOXENV=twistedchecker-diff - - python: 2.7 - env: TOXENV=docs - - python: 2.7 - env: TOXENV=docs-linkcheck + - python: 3.6 + env: TOXENV=coverage-py36-tw155,codecov + - python: 3.6 + env: TOXENV=coverage-py36-tw160,codecov + - python: 3.6 + env: TOXENV=coverage-py36-twcurrent,codecov + - python: 3.6 + env: TOXENV=coverage-py36-twtrunk,codecov allow_failures: # Tests against Twisted trunk are allow to fail, as they are not supported. + - env: TOXENV=coverage-pypy-twtrunk - env: TOXENV=coverage-py27-twtrunk - env: TOXENV=coverage-py34-twtrunk - env: TOXENV=coverage-py35-twtrunk - - env: TOXENV=coverage-pypy-twtrunk # This is not yet required. - env: TOXENV=twistedchecker-diff diff --git a/.travis/mypy b/.travis/mypy new file mode 100755 index 000000000..c1520692b --- /dev/null +++ b/.travis/mypy @@ -0,0 +1,47 @@ +#!/bin/sh + +# +# Work around bugs in mypy by filtering out false positive error messages. +# + +set -e +set -u + +tmp="$(mktemp -t mypy.XXXX)"; + +mypy "$@" \ + | grep -v \ + -e '^src/klein/_decorators.py:.[0-9:]*: error: Function is missing a type annotation' \ + -e '^src/klein/_plating.py:.[0-9:]*: error: Function is missing a type annotation' \ + -e '^src/klein/app.py:.[0-9:]*: error: Function is missing a type annotation' \ + -e '^src/klein/app.py:.[0-9:]*: error: All conditional function variants must have identical signatures' \ + -e '^src/klein/app.py:.[0-9:]*: error: Need type annotation for variable' \ + -e '^src/klein/interfaces.py:.[0-9:]*: error: Function is missing a type annotation' \ + -e '^src/klein/resource.py:.[0-9:]*: error: Function is missing a type annotation' \ + -e '^src/klein/test/py3_test_resource.py:.[0-9:]*: error: Function is missing a type annotation' \ + -e '^src/klein/test/_trial.py:.[0-9:]*: error: Function is missing a type annotation' \ + -e '^src/klein/test/test_app.py:.[0-9:]*: error: Function is missing a type annotation' \ + -e '^src/klein/test/test_plating.py:.[0-9:]*: error: Function is missing a type annotation' \ + -e '^src/klein/test/test_resource.py:.[0-9:]*: error: Function is missing a type annotation' \ + -e '^src/klein/test/test_resource.py:.[0-9:]*: error: .* has no attribute' \ + -e '^src/klein/test/util.py:.[0-9:]*: error: Function is missing a type annotation' \ + -e '^src/klein/test/util.py:.[0-9:]*: error: Name .* already defined' \ + -e '^src/klein/_headers.py:.[0-9:]*: error: Incompatible types in assignment (expression has type "str", variable has type "bytes")' \ + -e '^src/klein/test/test_headers.py:.[0-9:]*: error: '\''builtins.object'\'' object is not iterable' \ + -e ': error: Unexpected keyword argument "[^"]*" for "FrozenHTTPHeaders"' \ + -e ': error: Unexpected keyword argument "[^"]*" for "FrozenHTTPRequest"' \ + -e ': error: Unexpected keyword argument "[^"]*" for "HTTPHeadersFromHeaders"' \ + -e ': error: Unexpected keyword argument "[^"]*" for "HTTPRequestFromIRequest"' \ + -e ': error: Unexpected keyword argument "[^"]*" for "IOFount"' \ + -e ': error: Unexpected keyword argument "[^"]*" for "MutableHTTPHeaders"' \ + -e ': error: Incompatible return value type (got "[^"]*", expected "I[^"]*")' \ + -e ': error: Method must have at least one argument' \ + -e ': error: Callable\[\[[^]]*\], [^]]*\] has no attribute "todo"' \ + -e ': error: Generator has incompatible item type' \ + > "${tmp}" || true; + +sort < "${tmp}"; + +if grep -e ": error: " "${tmp}" > /dev/null; then + exit 1; +fi; diff --git a/src/klein/test/py3_test_resource.py b/src/klein/test/py3_test_resource.py index 58f3ffa34..b6adce1be 100644 --- a/src/klein/test/py3_test_resource.py +++ b/src/klein/test/py3_test_resource.py @@ -7,6 +7,7 @@ from .test_resource import LeafResource, _render, requestMock + class PY3KleinResourceTests(TestCase): def assertFired(self, deferred, result=None): diff --git a/tox.ini b/tox.ini index 6e1bee40e..1c914c408 100644 --- a/tox.ini +++ b/tox.ini @@ -1,15 +1,13 @@ [tox] envlist = - flake8 + flake8, mypy # Twisted 15.5 is the first version to support Python 3 (3.3+) coverage-py{27,py,34,35,36}-tw{155,166,current,trunk} docs, docs-linkcheck -skip_missing_interpreters = True - ## # Default environment: unit tests @@ -18,18 +16,15 @@ skip_missing_interpreters = True [testenv] basepython = - pypy: pypy py27: python2.7 + py34: python3.4 py35: python3.5 py36: python3.6 + pypy: pypy + deps = - tw150: Twisted==15.0 - tw151: Twisted==15.1 - tw152: Twisted==15.2 - tw153: Twisted==15.3 - tw154: Twisted==15.4 tw155: Twisted==15.5 tw160: Twisted==16.0 tw161: Twisted==16.1 @@ -56,14 +51,30 @@ setenv = PIP_DISABLE_PIP_VERSION_CHECK=1 VIRTUALENV_NO_DOWNLOAD=1 + coverage: COVERAGE_FILE={toxworkdir}/coverage/coverage.{envname} + {coverage_combine,codecov}: COVERAGE_FILE={toxworkdir}/coverage/coverage + + {coverage,coverage_combine}: COVERAGE_HTML={envlogdir}/coverage_report_html + {coverage,coverage_combine,codecov}: COVERAGE_XML={envlogdir}/coverage_report.xml + + coverage: COVERAGE_PROCESS_START={toxinidir}/.coveragerc + commands = "{toxinidir}/.travis/environment" # Run trial without coverage trial: trial --random=0 --logfile="{envlogdir}/trial.log" --temp-directory="{envlogdir}/trial.d" {posargs:klein} - # Run trial with coverage - coverage: coverage run -p "{envbindir}/trial" --random=0 --logfile="{envlogdir}/trial.log" --temp-directory="{envlogdir}/trial.d" {posargs:klein} + coverage: mkdir -p "{toxworkdir}/coverage" + coverage: coverage run --rcfile="{toxinidir}/.coveragerc" "{envdir}/bin/trial" --random=0 --logfile="{envlogdir}/trial.log" --temp-directory="{envlogdir}/trial.d" {posargs:klein} + + # Copy aside coverage data for each test environment in case we want to look at it later + coverage: cp "{env:COVERAGE_FILE}" "{envlogdir}/coverage" + + # Run coverage reports, ignore exit status + coverage: - coverage html --rcfile="{toxinidir}/.coveragerc" -d "{env:COVERAGE_HTML}" + coverage: - coverage xml --rcfile="{toxinidir}/.coveragerc" -o "{env:COVERAGE_XML}" + coverage: - coverage report --rcfile="{toxinidir}/.coveragerc" --skip-covered --omit "*/test/*" ## @@ -72,6 +83,8 @@ commands = [testenv:flake8] +basepython = python3.5 + skip_install = True deps = @@ -83,8 +96,6 @@ deps = pep8-naming mccabe -basepython = python3.5 - commands = "{toxinidir}/.travis/environment" @@ -101,10 +112,14 @@ doctests = True # Codes: http://flake8.pycqa.org/en/latest/user/error-codes.html ignore = + # multiple spaces before operator + E221, # too many blank lines E302, # too many blank lines E303, + # expected 2 blank lines after class or function definition + E305, # function name should be lowercase N802, # argument name should be lowercase @@ -119,16 +134,53 @@ application-import-names = klein max-complexity = 21 +## +# Mypy linting +## + +[testenv:mypy] + +basepython = python3.5 + +skip_install = True + +deps = + mypy + +commands = + "{toxinidir}/.travis/environment" + + "{toxinidir}/.travis/mypy" --config-file="{toxinidir}/tox.ini" {posargs:src} + + +[mypy] + +# Global settings + +warn_redundant_casts = True +warn_unused_ignores = True +strict_optional = True +show_column_numbers = True + +# Module default settings +# disallow_untyped_calls = True +disallow_untyped_defs = True +# warn_return_any = True + +# Need some stub files to get rid of this +ignore_missing_imports = True + + ## # Run twistedchecker ## [testenv:twistedchecker] -deps = twistedchecker - basepython = python2.7 +deps = twistedchecker + commands = "{toxinidir}/.travis/environment" @@ -141,18 +193,46 @@ commands = [testenv:twistedchecker-diff] +basepython = python2.7 + deps = {[testenv:twistedchecker]deps} diff_cover -basepython = python2.7 - commands = "{toxinidir}/.travis/environment" "{toxinidir}/.travis/twistedchecker-diff" {posargs:klein} +## +# Combine coverage reports +## + +[testenv:coverage_combine] + +basepython = python3.5 + +skip_install = True + +deps = coverage + +commands = + "{toxinidir}/.travis/environment" + + coverage combine --append + + # Copy aside coverage data for each test environment in case we want to look at it later + cp "{env:COVERAGE_FILE}" "{envlogdir}/coverage" + + # Run coverage reports, ignore exit status + - coverage html --rcfile="{toxinidir}/.coveragerc" -d "{env:COVERAGE_HTML}" + - coverage xml --rcfile="{toxinidir}/.coveragerc" -o "{env:COVERAGE_XML}" + + # Don't ignore exit status here; this is our failure status if coverage is insufficient. + coverage report --rcfile="{toxinidir}/.coveragerc" + + ## # Publish to Codecov ## @@ -179,12 +259,12 @@ commands = [testenv:docs] +basepython = python3.5 + deps = sphinx sphinx_rtd_theme -basepython = python2.7 - commands = "{toxinidir}/.travis/environment" @@ -197,9 +277,9 @@ commands = [testenv:docs-linkcheck] -deps = {[testenv:docs]deps} +basepython = python3.5 -basepython = python2.7 +deps = {[testenv:docs]deps} commands = "{toxinidir}/.travis/environment"