Use tox for running tests and building docs

* Set up tox for testing supported Django/Python combinations
* Also include tox env for checking and building docs
* Use tox-travis for Travis CI integration
* Add tests against Django master
* Document building docs and running tests with tox
This commit is contained in:
medmunds
2018-03-12 11:56:08 -07:00
committed by Mike Edmunds
parent b32c3ccb38
commit b06d684dd5
8 changed files with 489 additions and 61 deletions

2
.gitignore vendored
View File

@@ -3,6 +3,8 @@
*.pyc *.pyc
*.egg *.egg
*.egg-info *.egg-info
.eggs/
.tox/
build/ build/
dist/ dist/
docs/_build/ docs/_build/

View File

@@ -11,6 +11,8 @@ branches:
matrix: matrix:
include: include:
- { env: LINT_AND_DOCS=true, python: 3.6 }
# Anymail supports the same python versions as Django, excluding Python 3.2, but adding pypy. # Anymail supports the same python versions as Django, excluding Python 3.2, but adding pypy.
# https://docs.djangoproject.com/en/dev/faq/install/#what-python-version-can-i-use-with-django # https://docs.djangoproject.com/en/dev/faq/install/#what-python-version-can-i-use-with-django
@@ -18,54 +20,52 @@ matrix:
# combinations, to avoid rapidly consuming the testing accounts' entire send allotments. # combinations, to avoid rapidly consuming the testing accounts' entire send allotments.
# Django 1.8: Python 2.7, 3.3, 3.4, 3.5 # Django 1.8: Python 2.7, 3.3, 3.4, 3.5
- { env: DJANGO=django==1.8 RUN_LIVE_TESTS=true, python: 2.7 } - { env: DJANGO=1.8 RUN_LIVE_TESTS=true, python: 2.7 }
- { env: DJANGO=django==1.8, python: 3.3 } - { env: DJANGO=1.8, python: 3.3 }
- { env: DJANGO=django==1.8, python: 3.4 } - { env: DJANGO=1.8, python: 3.4 }
- { env: DJANGO=django==1.8, python: 3.5 } - { env: DJANGO=1.8, python: 3.5 }
- { env: DJANGO=django==1.8, python: pypy } - { env: DJANGO=1.8, python: pypy }
# Django 1.9: Python 2.7, 3.4, 3.5 # Django 1.9: Python 2.7, 3.4, 3.5
- { env: DJANGO=django==1.9, python: 2.7 } - { env: DJANGO=1.9, python: 2.7 }
- { env: DJANGO=django==1.9, python: 3.4 } - { env: DJANGO=1.9, python: 3.4 }
- { env: DJANGO=django==1.9, python: 3.5 } - { env: DJANGO=1.9, python: 3.5 }
- { env: DJANGO=django==1.9, python: pypy } - { env: DJANGO=1.9, python: pypy }
# Django 1.10: Python 2.7, 3.4, 3.5 # Django 1.10: Python 2.7, 3.4, 3.5
- { env: DJANGO=django==1.10, python: 2.7 } - { env: DJANGO=1.10, python: 2.7 }
- { env: DJANGO=django==1.10, python: 3.4 } - { env: DJANGO=1.10, python: 3.4 }
- { env: DJANGO=django==1.10, python: 3.5 } - { env: DJANGO=1.10, python: 3.5 }
- { env: DJANGO=django==1.10, python: pypy } - { env: DJANGO=1.10, python: pypy }
# Django 1.11: Python 2.7, 3.4, 3.5, or 3.6 # Django 1.11: Python 2.7, 3.4, 3.5, or 3.6
- { env: DJANGO=django==1.11, python: 2.7 } - { env: DJANGO=1.11, python: 2.7 }
- { env: DJANGO=django==1.11, python: 3.4 } - { env: DJANGO=1.11, python: 3.4 }
- { env: DJANGO=django==1.11, python: 3.5 } - { env: DJANGO=1.11, python: 3.5 }
- { env: DJANGO=django==1.11, python: 3.6 } - { env: DJANGO=1.11, python: 3.6 }
- { env: DJANGO=django==1.11, python: pypy } - { env: DJANGO=1.11, python: pypy }
# Django 2.0: Python 3.5+ # Django 2.0: Python 3.5+
- { env: DJANGO=django==2.0, python: 3.5 } - { env: DJANGO=2.0, python: 3.5 }
- { env: DJANGO=django==2.0 RUN_LIVE_TESTS=true, python: 3.6 } - { env: DJANGO=2.0 RUN_LIVE_TESTS=true, python: 3.6 }
- { env: DJANGO=2.0, python: pypy3 }
# Django 2.1 (prerelease): Python 3.5+ # Django 2.1 (prerelease): Python 3.5+
#- { env: DJANGO="--pre django", python: 3.5 } #- { env: DJANGO=2.1, python: 3.5 }
#- { env: DJANGO="--pre django", python: 3.6 } #- { env: DJANGO=2.1, python: 3.6 }
# Django development master (direct from GitHub source):
- { env: DJANGO=master, python: 3.6 }
- { env: DJANGO=master, python: 3.7-dev }
- { env: FLAKE8=true, python: 2.7 } allow_failures:
- { env: FLAKE8=true, python: 3.6 } - env: DJANGO=2.1
python: 3.5
# allow_failures: - env: DJANGO=2.1
# - env: DJANGO="--pre django" python: 3.6
# - python: 3.6 - env: DJANGO=master
python: 3.6
- env: DJANGO=master
python: 3.7-dev
cache: pip cache: pip
# If env DJANGO is set, install Anymail and run tests
# If env FLAKE8 is set, run flake8
install: install:
- pip install --upgrade setuptools pip - pip install tox-travis
- if [[ -n $DJANGO ]]; then pip install $DJANGO; fi
# For now, install Anymail including all optional ESPs, and test at once
# (in future, might want to matrix ESPs to test cross-dependencies)
- if [[ -n $DJANGO ]]; then pip install .[mailgun,mailjet,mandrill,postmark,sendinblue,sendgrid,sparkpost]; fi
- if [[ -n $FLAKE8 ]]; then pip install flake8; fi
- pip list
script: script:
- if [[ -n $DJANGO ]]; then python setup.py test; fi - tox
- if [[ -n $FLAKE8 ]]; then flake8; fi

31
docs/_readme/docutils.cfg Normal file
View File

@@ -0,0 +1,31 @@
# docutils (rst2html) config for generating static HTML that approximates
# PyPI package description rendering (as of 3/2018).
#
# Usage (in package root dir):
# python setup.py --long-description | rst2html.py --config=docs/_readme/docutils.cfg > ${OUTDIR}/readme.html
#
# Requires docutils and pygments (both are installed with Sphinx)
[general]
# Duplicate docutils config used by PyPA readme_renderer.
# https://github.com/pypa/readme_renderer/blob/master/readme_renderer/rst.py
# (This isn't exactly what's used on legacy PyPI, but it's close enough.)
cloak_email_addresses = True
doctitle_xform = True
sectsubtitle_xform = True
initial_header_level = 2
file_insertion_enabled = False
math_output = MathJax
raw_enabled = False
smart_quotes = True
strip_comments = True
syntax_highlight = short
# Halt rendering and throw an exception if there was any errors or warnings from docutils.
halt_level = 2
# DON'T Disable all system messages from being reported.
# (We're not running inside readme_renderer, so *do* want to see warnings and errors.)
# report_level = 5
# Approximate PyPI's styles:
stylesheet = docs/_readme/readme.css

74
docs/_readme/readme.css Normal file
View File

@@ -0,0 +1,74 @@
/*
readme.css
Approximates PyPI package description rendering as of 3/2018,
using docutils rst2html output.
*/
/* Borrow base docutils and pygments styles directly from PyPI: */
@import url("https://pypi.python.org/static/css/docutils.css");
@import url("https://pypi.python.org/static/css/pygments.css"); /* requires rst2html 'short' classnames */
/* Subset of PyPI site styles applicable to package description: */
HTML, BODY {
font-family: Arial, Verdana, Geneva, "Bitstream Vera Sans", Helvetica, sans-serif;
font-size: 103%;
color: #000;
background-color: #FFF;
}
H1, H2, H3, H4, H5 {
font-family: Georgia, "Bitstream Vera Serif", "New York", Palatino, serif;
font-weight: normal;
line-height: 1em;
}
H1 {
font-size: 160%;
color: #234764;
margin: 0.7em 0;
text-decoration: none;
}
H2 {
font-size: 140%;
color: #366D9C;
margin: 0.7em 0 0.7em 0;
}
IMG {
border: 0;
}
A:link {
color: #00A;
}
A:visited {
color: #551A8B;
}
P A:link, P A:visited,
UL A:link, UL A:visited,
OL A:link, OL A:visited {
text-decoration: none;
border-bottom: 1px dashed #ccc;
}
/* Additional styles, to account for not having all of PyPI's wrapper and navigation divs: */
body {
line-height: 1.5;
font-size: 14.6px; /* ~computed font-size in PyPI's div#content */
}
h1.title {
text-align: left;
}
ul, li {
margin-left: 1em;
padding-left: 0;
}
pre {
padding: 10px;
font-size: 11.9px; /* ~computed font-size in PyPI's div#content pre */
}
/* Give the page a little breathing room: */
.document {
max-width: 960px;
margin: 0 auto;
}
body {
padding: 1em;
}

View File

@@ -1,3 +1,10 @@
.. role:: shell(code)
:language: shell
.. role:: rst(code)
:language: rst
.. _contributing: .. _contributing:
Contributing Contributing
@@ -48,6 +55,8 @@ Pull requests are always welcome to fix bugs and improve support for ESP and Dja
(basically, :pep:`8` with longer lines OK). (basically, :pep:`8` with longer lines OK).
* By submitting a pull request, you're agreeing to release your changes under under * By submitting a pull request, you're agreeing to release your changes under under
the same BSD license as the rest of this project. the same BSD license as the rest of this project.
* Documentation is appreciated, but not required.
(Please don't let missing or incomplete documentation keep you from contributing code.)
.. Intentionally point to Django dev branch for coding docs (rather than Django stable): .. Intentionally point to Django dev branch for coding docs (rather than Django stable):
.. _Django coding style: .. _Django coding style:
@@ -57,32 +66,121 @@ Pull requests are always welcome to fix bugs and improve support for ESP and Dja
Testing Testing
------- -------
Anymail is `tested on Travis`_ against several combinations of Django Anymail is `tested on Travis CI`_ against several combinations of Django
and Python versions. (Full list in `.travis.yml`_.) and Python versions. Tests are run at least once a week, to check whether ESP APIs
and other dependencies have changed out from under Anymail.
For local development, the recommended test command is
:shell:`tox -e django20-py36,django18-py27,lint`, which tests a representative
combination of Python and Django versions. It also runs :pypi:`flake8` and other
code-style checkers. Some other test options are covered below, but using this
tox command catches most problems, and is a good pre-pull-request check.
Most of the included tests verify that Anymail constructs the expected ESP API Most of the included tests verify that Anymail constructs the expected ESP API
calls, without actually calling the ESP's API or sending any email. So these tests calls, without actually calling the ESP's API or sending any email. So these tests
don't require API keys, but they *do* require `mock`_ (``pip install mock``). don't require API keys, but they *do* require :pypi:`mock` and all ESP-specific
package requirements.
To run the tests, either: To run the tests, you can:
.. code-block:: console .. code-block:: console
$ python setup.py test $ python setup.py test # (also installs test dependencies if needed)
or: Or:
.. code-block:: console .. code-block:: console
$ pip install mock sparkpost # install test dependencies
$ python runtests.py $ python runtests.py
Anymail also includes some integration tests, which do call the live ESP APIs. ## this command can also run just a few test cases, e.g.:
These integration tests require API keys (and sometimes other settings) they $ python runtests.py tests.test_mailgun_backend tests.test_mailgun_webhooks
get from from environment variables. They're skipped if these keys aren't present.
If you want to run them, look in the ``*_integration_tests.py``
files in the `tests source`_ for specific requirements.
.. _.travis.yml: https://github.com/anymail/django-anymail/blob/master/.travis.yml Or to test against multiple versions of Python and Django all at once, use :pypi:`tox`.
You'll need at least Python 2.7 and Python 3.6 available. (If your system doesn't come
with those, `pyenv`_ is a helpful way to install and manage multiple Python versions.)
.. code-block:: console
$ pip install tox # (if you haven't already)
$ tox -e django20-py36,django18-py27,lint # test recommended environments
## you can also run just some test cases, e.g.:
$ tox -e django20-py36,django18-py27 tests.test_mailgun_backend tests.test_utils
## to test more Python/Django versions:
$ tox # ALL 20+ envs! (grab a coffee, or use `detox` to run tests in parallel)
$ tox --skip-missing-interpreters # if some Python versions aren't installed
In addition to the mocked tests, Anymail has integration tests which *do* call live ESP APIs.
These tests are normally skipped; to run them, set environment variables with the necessary
API keys or other settings. For example:
.. code-block:: console
$ export MAILGUN_TEST_API_KEY='your-Mailgun-API-key'
$ export MAILGUN_TEST_DOMAIN='mail.example.com' # sending domain for that API key
$ tox -e django20-py36 tests.test_mailgun_integration
Check the ``*_integration_tests.py`` files in the `tests source`_ to see which variables
are required for each ESP. Depending on the supported features, the integration tests for
a particular ESP send around 5-15 individual messages. For ESPs that don't offer a sandbox,
these will be real sends charged to your account (again, see the notes in each test case).
Be sure to specify a particular testenv with tox's `-e` option, or tox may repeat the tests
for all 20+ supported combinations of Python and Django, sending hundreds of messages.
.. _pyenv: https://github.com/pyenv/pyenv
.. _tested on Travis CI: https://travis-ci.org/anymail/django-anymail
.. _tests source: https://github.com/anymail/django-anymail/blob/master/tests .. _tests source: https://github.com/anymail/django-anymail/blob/master/tests
.. _mock: http://www.voidspace.org.uk/python/mock/index.html .. _.travis.yml: https://github.com/anymail/django-anymail/blob/master/.travis.yml
.. _tested on Travis: https://travis-ci.org/anymail/django-anymail
Documentation
-------------
As noted above, Anymail welcomes pull requests with missing or incomplete
documentation. (Code without docs is better than no contribution at all.)
But documentation---even needing edits---is always appreciated, as are pull
requests simply to improve the docs themselves.
Like many Python packages, Anymail's docs use :pypi:`Sphinx`. If you've never
worked with Sphinx or reStructuredText, Django's `Writing Documentation`_ can
get you started.
It's easiest to build Anymail's docs using tox:
.. code-block:: console
$ pip install tox # (if you haven't already)
$ tox -e docs # build the docs using Sphinx
You can run Python's simple HTTP server to view them:
.. code-block:: console
$ (cd .tox/docs/_html; python3 -m http.server 8123 --bind 127.0.0.1)
... and then open http://localhost:8123/ in a browser. Leave the server running,
and just re-run the tox command and refresh your browser as you make changes.
If you've edited the main README.rst, you can preview an approximation of what
will end up on PyPI at http://localhost:8123/readme.html.
Anymail's Sphinx conf sets up a few enhancements you can use in the docs:
* Loads `intersphinx`_ mappings for Python 3, Django (stable), and Requests.
Docs can refer to things like :rst:`:ref:`django:topics-testing-email``
or :rst:`:class:`django.core.mail.EmailMessage``.
* Supports much of `Django's added markup`_, notably :rst:`:setting:`
for documenting or referencing Django and Anymail settings.
* Allows linking to Python packages with :rst:`:pypi:`package-name``
(via `extlinks`_).
.. _Django's added markup:
https://docs.djangoproject.com/en/stable/internals/contributing/writing-documentation/#django-specific-markup
.. _extlinks: http://www.sphinx-doc.org/en/stable/ext/extlinks.html
.. _intersphinx: http://www.sphinx-doc.org/en/master/ext/intersphinx.html
.. _Writing Documentation:
https://docs.djangoproject.com/en/stable/internals/contributing/writing-documentation/

View File

@@ -1,13 +1,13 @@
""" """
Django settings for Anymail tests. Django settings for Anymail tests.
Generated by 'django-admin startproject' using Django 2.0a1. Generated by 'django-admin startproject' using Django 2.0.3.
For more information on this file, see For more information on this file, see
https://docs.djangoproject.com/en/dev/topics/settings/ https://docs.djangoproject.com/en/2.0/topics/settings/
For the full list of settings and their values, see For the full list of settings and their values, see
https://docs.djangoproject.com/en/dev/ref/settings/ https://docs.djangoproject.com/en/2.0/ref/settings/
""" """
import os import os
@@ -17,7 +17,7 @@ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Quick-start development settings - unsuitable for production # Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/dev/howto/deployment/checklist/ # See https://docs.djangoproject.com/en/2.0/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret! # SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'NOT_FOR_PRODUCTION_USE' SECRET_KEY = 'NOT_FOR_PRODUCTION_USE'
@@ -72,7 +72,7 @@ WSGI_APPLICATION = 'tests.wsgi.application'
# Database # Database
# https://docs.djangoproject.com/en/dev/ref/settings/#databases # https://docs.djangoproject.com/en/2.0/ref/settings/#databases
DATABASES = { DATABASES = {
'default': { 'default': {
@@ -83,7 +83,7 @@ DATABASES = {
# Password validation # Password validation
# https://docs.djangoproject.com/en/dev/ref/settings/#auth-password-validators # https://docs.djangoproject.com/en/2.0/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [ AUTH_PASSWORD_VALIDATORS = [
{ {
@@ -102,7 +102,7 @@ AUTH_PASSWORD_VALIDATORS = [
# Internationalization # Internationalization
# https://docs.djangoproject.com/en/dev/topics/i18n/ # https://docs.djangoproject.com/en/2.0/topics/i18n/
LANGUAGE_CODE = 'en-us' LANGUAGE_CODE = 'en-us'
@@ -116,6 +116,6 @@ USE_TZ = True
# Static files (CSS, JavaScript, Images) # Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/dev/howto/static-files/ # https://docs.djangoproject.com/en/2.0/howto/static-files/
STATIC_URL = '/static/' STATIC_URL = '/static/'

View File

@@ -0,0 +1,121 @@
"""
Django settings for Anymail tests.
Generated by 'django-admin startproject' using Django 2.1.dev20180313162727.
For more information on this file, see
https://docs.djangoproject.com/en/dev/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/dev/ref/settings/
"""
import os
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/dev/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'NOT_FOR_PRODUCTION_USE'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = []
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'anymail',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'tests.test_settings.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'tests.wsgi.application'
# Database
# https://docs.djangoproject.com/en/dev/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
}
}
# Password validation
# https://docs.djangoproject.com/en/dev/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/dev/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_L10N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/dev/howto/static-files/
STATIC_URL = '/static/'

102
tox.ini Normal file
View File

@@ -0,0 +1,102 @@
[tox]
envlist =
# Test these environments first, to catch most errors early...
lint
django20-py36
django18-py27
docs
# ... then test all the other supported combinations:
django20-py{35,py3}
django111-py{27,34,35,36,py2}
django110-py{27,34,35,py2}
django19-py{27,34,35,py2}
django18-py{33,34,35,py2}
# ... then prereleases (if available):
#django21-py{35,36}
djangoMaster-py{36,37}
[testenv]
deps =
django18: django>=1.8,<1.9
django19: django>=1.9,<1.10
django110: django>=1.10,<1.11
django111: django>=1.11,<1.12
django20: django>=2.0,<2.1
django21: django>=2.1a1
djangoMaster: https://github.com/django/django/tarball/master
# testing dependencies (duplicates setup.py tests_require):
mock
sparkpost
ignore_outcome =
django21: True
djangoMaster: True
usedevelop = True
args_are_paths = False
commands =
python --version
# pip install .[mailgun,...,sparkpost] ## usedevelop=True + manual deps is much faster on repeat runs
python runtests.py {posargs}
passenv =
RUN_LIVE_TESTS
CONTINUOUS_INTEGRATION
MAILGUN_TEST_*
MAILJET_TEST_*
MANDRILL_TEST_*
POSTMARK_TEST_*
SENDINBLUE_TEST_*
SENDGRID_TEST_*
SPARKPOST_TEST_*
[testenv:lint]
basepython = python3
skip_install = True
passenv =
CONTINUOUS_INTEGRATION
# (but not any of the live test API keys)
deps =
flake8
commands =
python --version
flake8 --version
flake8
[testenv:docs]
basepython = python3
skip_install = True
passenv =
CONTINUOUS_INTEGRATION
# (but not any of the live test API keys)
setenv =
DOCS_BUILD_DIR={envdir}/_html
whitelist_externals = /bin/bash
deps =
sphinx
sphinx-rtd-theme
commands =
# Verify README.rst as used in setup.py long_description:
python setup.py check --restructuredtext --strict
# Build and verify docs:
sphinx-build -W -b dirhtml docs {env:DOCS_BUILD_DIR}
# Build README.rst into html:
/bin/bash -c 'python setup.py --long-description \
| rst2html.py --config=docs/_readme/docutils.cfg \
> {env:DOCS_BUILD_DIR}/readme.html'
[travis]
unignore_outcomes = True
python =
3.6: py36, lint, docs
[travis:env]
DJANGO =
1.8: django18
1.9: django19
1.10: django110
1.11: django111
2.0: django20
2.1: django21
master: djangoMaster
LINT_AND_DOCS =
true: lint, docs
docs: docs
lint: lint