diff --git a/.gitignore b/.gitignore index 868f63c..e9789e1 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,8 @@ *.pyc *.egg *.egg-info +.eggs/ +.tox/ build/ dist/ docs/_build/ diff --git a/.travis.yml b/.travis.yml index f3dffe3..d9ddadc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,6 +11,8 @@ branches: matrix: include: + - { env: LINT_AND_DOCS=true, python: 3.6 } + # 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 @@ -18,54 +20,52 @@ matrix: # combinations, to avoid rapidly consuming the testing accounts' entire send allotments. # 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=django==1.8, python: 3.3 } - - { env: DJANGO=django==1.8, python: 3.4 } - - { env: DJANGO=django==1.8, python: 3.5 } - - { env: DJANGO=django==1.8, python: pypy } + - { env: DJANGO=1.8 RUN_LIVE_TESTS=true, python: 2.7 } + - { env: DJANGO=1.8, python: 3.3 } + - { env: DJANGO=1.8, python: 3.4 } + - { env: DJANGO=1.8, python: 3.5 } + - { env: DJANGO=1.8, python: pypy } # Django 1.9: Python 2.7, 3.4, 3.5 - - { env: DJANGO=django==1.9, python: 2.7 } - - { env: DJANGO=django==1.9, python: 3.4 } - - { env: DJANGO=django==1.9, python: 3.5 } - - { env: DJANGO=django==1.9, python: pypy } + - { env: DJANGO=1.9, python: 2.7 } + - { env: DJANGO=1.9, python: 3.4 } + - { env: DJANGO=1.9, python: 3.5 } + - { env: DJANGO=1.9, python: pypy } # Django 1.10: Python 2.7, 3.4, 3.5 - - { env: DJANGO=django==1.10, python: 2.7 } - - { env: DJANGO=django==1.10, python: 3.4 } - - { env: DJANGO=django==1.10, python: 3.5 } - - { env: DJANGO=django==1.10, python: pypy } + - { env: DJANGO=1.10, python: 2.7 } + - { env: DJANGO=1.10, python: 3.4 } + - { env: DJANGO=1.10, python: 3.5 } + - { env: DJANGO=1.10, python: pypy } # Django 1.11: Python 2.7, 3.4, 3.5, or 3.6 - - { env: DJANGO=django==1.11, python: 2.7 } - - { env: DJANGO=django==1.11, python: 3.4 } - - { env: DJANGO=django==1.11, python: 3.5 } - - { env: DJANGO=django==1.11, python: 3.6 } - - { env: DJANGO=django==1.11, python: pypy } + - { env: DJANGO=1.11, python: 2.7 } + - { env: DJANGO=1.11, python: 3.4 } + - { env: DJANGO=1.11, python: 3.5 } + - { env: DJANGO=1.11, python: 3.6 } + - { env: DJANGO=1.11, python: pypy } # Django 2.0: Python 3.5+ - - { env: DJANGO=django==2.0, python: 3.5 } - - { env: DJANGO=django==2.0 RUN_LIVE_TESTS=true, python: 3.6 } + - { env: DJANGO=2.0, python: 3.5 } + - { env: DJANGO=2.0 RUN_LIVE_TESTS=true, python: 3.6 } + - { env: DJANGO=2.0, python: pypy3 } # Django 2.1 (prerelease): Python 3.5+ - #- { env: DJANGO="--pre django", python: 3.5 } - #- { env: DJANGO="--pre django", python: 3.6 } + #- { env: DJANGO=2.1, python: 3.5 } + #- { 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 } - - { env: FLAKE8=true, python: 3.6 } - - # allow_failures: - # - env: DJANGO="--pre django" - # - python: 3.6 + allow_failures: + - env: DJANGO=2.1 + python: 3.5 + - env: DJANGO=2.1 + python: 3.6 + - env: DJANGO=master + python: 3.6 + - env: DJANGO=master + python: 3.7-dev cache: pip -# If env DJANGO is set, install Anymail and run tests -# If env FLAKE8 is set, run flake8 install: - - pip install --upgrade setuptools pip - - 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 + - pip install tox-travis script: - - if [[ -n $DJANGO ]]; then python setup.py test; fi - - if [[ -n $FLAKE8 ]]; then flake8; fi + - tox diff --git a/docs/_readme/docutils.cfg b/docs/_readme/docutils.cfg new file mode 100644 index 0000000..00d52a2 --- /dev/null +++ b/docs/_readme/docutils.cfg @@ -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 diff --git a/docs/_readme/readme.css b/docs/_readme/readme.css new file mode 100644 index 0000000..75c0df5 --- /dev/null +++ b/docs/_readme/readme.css @@ -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; +} diff --git a/docs/contributing.rst b/docs/contributing.rst index 3140859..60f8b75 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -1,3 +1,10 @@ +.. role:: shell(code) + :language: shell + +.. role:: rst(code) + :language: rst + + .. _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). * By submitting a pull request, you're agreeing to release your changes under under 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): .. _Django coding style: @@ -57,32 +66,121 @@ Pull requests are always welcome to fix bugs and improve support for ESP and Dja Testing ------- -Anymail is `tested on Travis`_ against several combinations of Django -and Python versions. (Full list in `.travis.yml`_.) +Anymail is `tested on Travis CI`_ against several combinations of Django +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 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 - $ python setup.py test + $ python setup.py test # (also installs test dependencies if needed) -or: +Or: .. code-block:: console + $ pip install mock sparkpost # install test dependencies $ python runtests.py -Anymail also includes some integration tests, which do call the live ESP APIs. -These integration tests require API keys (and sometimes other settings) they -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. + ## this command can also run just a few test cases, e.g.: + $ python runtests.py tests.test_mailgun_backend tests.test_mailgun_webhooks -.. _.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 -.. _mock: http://www.voidspace.org.uk/python/mock/index.html -.. _tested on Travis: https://travis-ci.org/anymail/django-anymail +.. _.travis.yml: https://github.com/anymail/django-anymail/blob/master/.travis.yml + + +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/ diff --git a/tests/test_settings/settings_2_0.py b/tests/test_settings/settings_2_0.py index 837233f..b9bad06 100644 --- a/tests/test_settings/settings_2_0.py +++ b/tests/test_settings/settings_2_0.py @@ -1,13 +1,13 @@ """ 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 -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 -https://docs.djangoproject.com/en/dev/ref/settings/ +https://docs.djangoproject.com/en/2.0/ref/settings/ """ 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 -# 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! SECRET_KEY = 'NOT_FOR_PRODUCTION_USE' @@ -72,7 +72,7 @@ WSGI_APPLICATION = 'tests.wsgi.application' # Database -# https://docs.djangoproject.com/en/dev/ref/settings/#databases +# https://docs.djangoproject.com/en/2.0/ref/settings/#databases DATABASES = { 'default': { @@ -83,7 +83,7 @@ DATABASES = { # 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 = [ { @@ -102,7 +102,7 @@ AUTH_PASSWORD_VALIDATORS = [ # Internationalization -# https://docs.djangoproject.com/en/dev/topics/i18n/ +# https://docs.djangoproject.com/en/2.0/topics/i18n/ LANGUAGE_CODE = 'en-us' @@ -116,6 +116,6 @@ USE_TZ = True # 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/' diff --git a/tests/test_settings/settings_2_1.py b/tests/test_settings/settings_2_1.py new file mode 100644 index 0000000..62b2cfb --- /dev/null +++ b/tests/test_settings/settings_2_1.py @@ -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/' diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..93ae11a --- /dev/null +++ b/tox.ini @@ -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