mirror of
https://github.com/pacnpal/django-anymail.git
synced 2025-12-20 03:41:05 -05:00
Modernize packaging
Switch to pyproject.toml packaging, using hatchling. - Replace all uses of setup.py with updated equivalent - BREAKING: Change extra name `amazon_ses` to `amazon-ses`, to comply with Python packaging name normalization - Use hatch custom build hook to freeze version number in readme (previously custom setup.py code) - Move separate requirements for dev, docs, tests into their own requirements.txt files - Fix AnymailImproperlyInstalled to correctly refer to package extra name - Update testing documentation - Update docs readme rendering to match PyPI (and avoid setup.py) - In tox tests, use isolated builds and update pip - Remove AUTHORS.txt (it just referred to GitHub)
This commit is contained in:
@@ -1,30 +0,0 @@
|
||||
# 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
|
||||
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 layout and styles:
|
||||
template = docs/_readme/template.txt
|
||||
109
docs/_readme/render.py
Normal file
109
docs/_readme/render.py
Normal file
@@ -0,0 +1,109 @@
|
||||
#!/usr/bin/env python
|
||||
# Render a README file (roughly) as it would appear on PyPI
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
from importlib.metadata import PackageNotFoundError, metadata
|
||||
from pathlib import Path
|
||||
from typing import Dict, Optional
|
||||
|
||||
import readme_renderer.rst
|
||||
from docutils.core import publish_string
|
||||
from docutils.utils import SystemMessage
|
||||
|
||||
# Docutils template.txt in our directory:
|
||||
DEFAULT_TEMPLATE_FILE = Path(__file__).with_name("template.txt").absolute()
|
||||
|
||||
|
||||
def get_package_readme(package: str) -> str:
|
||||
# Note: "description" was added to metadata in Python 3.10
|
||||
return metadata(package)["description"]
|
||||
|
||||
|
||||
class ReadMeHTMLWriter(readme_renderer.rst.Writer):
|
||||
translator_class = readme_renderer.rst.ReadMeHTMLTranslator
|
||||
|
||||
def interpolation_dict(self) -> Dict[str, str]:
|
||||
result = super().interpolation_dict()
|
||||
# clean the same parts as readme_renderer.rst.render:
|
||||
clean = readme_renderer.rst.clean
|
||||
result["docinfo"] = clean(result["docinfo"])
|
||||
result["body"] = result["fragment"] = clean(result["fragment"])
|
||||
return result
|
||||
|
||||
|
||||
def render(source_text: str, warning_stream=sys.stderr) -> Optional[str]:
|
||||
# Adapted from readme_renderer.rst.render
|
||||
settings = readme_renderer.rst.SETTINGS.copy()
|
||||
settings.update(
|
||||
{
|
||||
"warning_stream": warning_stream,
|
||||
"template": DEFAULT_TEMPLATE_FILE,
|
||||
# Input and output are text str (we handle decoding/encoding):
|
||||
"input_encoding": "unicode",
|
||||
"output_encoding": "unicode",
|
||||
# Exit with error on docutils warning or above.
|
||||
# (There's discussion of having readme_renderer ignore warnings;
|
||||
# this ensures they'll be treated as errors here.)
|
||||
"halt_level": 2, # (docutils.utils.Reporter.WARNING_LEVEL)
|
||||
# Report all docutils warnings or above.
|
||||
# (The readme_renderer default suppresses this output.)
|
||||
"report_level": 2, # (docutils.utils.Reporter.WARNING_LEVEL)
|
||||
}
|
||||
)
|
||||
|
||||
writer = ReadMeHTMLWriter()
|
||||
|
||||
try:
|
||||
return publish_string(
|
||||
source_text,
|
||||
writer=writer,
|
||||
settings_overrides=settings,
|
||||
)
|
||||
except SystemMessage:
|
||||
warning_stream.write("Error rendering readme source.\n")
|
||||
return None
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Render readme file as it would appear on PyPI"
|
||||
)
|
||||
input_group = parser.add_mutually_exclusive_group(required=True)
|
||||
input_group.add_argument(
|
||||
"-p", "--package", help="Source readme from package's metadata"
|
||||
)
|
||||
input_group.add_argument(
|
||||
"-i",
|
||||
"--input",
|
||||
help="Source readme.rst file ('-' for stdin)",
|
||||
type=argparse.FileType("r"),
|
||||
)
|
||||
parser.add_argument(
|
||||
"-o",
|
||||
"--output",
|
||||
help="Output file (default: stdout)",
|
||||
type=argparse.FileType("w"),
|
||||
default="-",
|
||||
)
|
||||
|
||||
args = parser.parse_args(argv)
|
||||
if args.package:
|
||||
try:
|
||||
source_text = get_package_readme(args.package)
|
||||
except PackageNotFoundError:
|
||||
print(f"Package not installed: {args.package!r}", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
if source_text is None:
|
||||
print(f"No metadata readme for {args.package!r}", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
else:
|
||||
source_text = args.input.read()
|
||||
rendered = render(source_text)
|
||||
if rendered is None:
|
||||
sys.exit(2)
|
||||
args.output.write(rendered)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,11 +1,13 @@
|
||||
%(head_prefix)s
|
||||
<!--
|
||||
This approximates PyPI.org project page styling as of 8/2020,
|
||||
This approximates PyPI.org project page styling as of 5/2023,
|
||||
and loads their compiled CSS that was in use at that time.
|
||||
|
||||
(Styling seems to change more often than basic page structure,
|
||||
so to update, it may be sufficient to copy in the current
|
||||
<link rel="stylesheet" ...> tags from any live package page.)
|
||||
<link rel="stylesheet" ...> tags from any live package page.
|
||||
Be sure to convert or escape any percent chars in copied urls,
|
||||
to avoid "not enough arguments for format string" errors.)
|
||||
|
||||
This extends the docutils base template found at
|
||||
${SITE_PACKAGES}/docutils/writers/html5_polyglot/template.txt
|
||||
@@ -15,13 +17,13 @@
|
||||
%(head)s
|
||||
<!-- template (stylesheet) omitted -->
|
||||
|
||||
<link rel="stylesheet" href="/static/css/warehouse-ltr.f2d4f304.css">
|
||||
<link rel="stylesheet" href="/static/css/fontawesome.6002a161.css">
|
||||
<link rel="stylesheet" href="/static/css/regular.98fbf39a.css">
|
||||
<link rel="stylesheet" href="/static/css/solid.c3b5f0b5.css">
|
||||
<link rel="stylesheet" href="/static/css/brands.2c303be1.css">
|
||||
<link rel="stylesheet" href="/static/css/warehouse-ltr.a42ccb04.css">
|
||||
<link rel="stylesheet" href="/static/css/fontawesome.d37999f3.css">
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,400italic,600,600italic,700,700italic|Source+Code+Pro:500">
|
||||
<link rel="icon" href="/static/images/favicon.6a76275d.ico" type="image/x-icon">
|
||||
<noscript>
|
||||
<link rel="stylesheet" href="/static/css/noscript.0673c9ea.css">
|
||||
</noscript>
|
||||
<link rel="icon" href="/static/images/favicon.35549fe8.ico" type="image/x-icon">
|
||||
|
||||
%(body_prefix)s
|
||||
|
||||
|
||||
17
docs/conf.py
17
docs/conf.py
@@ -13,6 +13,8 @@ import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from anymail import VERSION as PACKAGE_VERSION
|
||||
|
||||
ON_READTHEDOCS = os.environ.get("READTHEDOCS", None) == "True"
|
||||
DOCS_PATH = Path(__file__).parent
|
||||
PROJECT_ROOT_PATH = DOCS_PATH.parent
|
||||
@@ -22,15 +24,6 @@ PROJECT_ROOT_PATH = DOCS_PATH.parent
|
||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||
sys.path.insert(0, PROJECT_ROOT_PATH.resolve())
|
||||
|
||||
# define __version__ and __minor_version__ from ../anymail/_version.py,
|
||||
# but without importing from anymail (which would make docs dependent on Django, etc.)
|
||||
__version__ = "UNSET"
|
||||
__minor_version__ = "UNSET"
|
||||
version_path = PROJECT_ROOT_PATH / "anymail/_version.py"
|
||||
code = compile(version_path.read_text(), version_path, "exec")
|
||||
exec(code)
|
||||
|
||||
|
||||
# -- General configuration -----------------------------------------------------
|
||||
|
||||
# If your documentation needs a minimal Sphinx version, state it here.
|
||||
@@ -61,10 +54,10 @@ copyright = "Anymail contributors"
|
||||
# |version| and |release|, also used in various other places throughout the
|
||||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
version = __minor_version__
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = __version__
|
||||
release = ".".join(PACKAGE_VERSION)
|
||||
# The short X.Y version.
|
||||
version = ".".join(PACKAGE_VERSION[:2])
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
|
||||
@@ -20,13 +20,13 @@ The `Anymail source code`_ is on GitHub.
|
||||
Contributors
|
||||
------------
|
||||
|
||||
See `AUTHORS.txt`_ for a list of some of the people who have helped
|
||||
See the `contributor chart`_ for a list of some of the people who have helped
|
||||
improve Anymail.
|
||||
|
||||
Anymail evolved from the `Djrill`_ project. Special thanks to the
|
||||
folks from `brack3t`_ who developed the original version of Djrill.
|
||||
|
||||
.. _AUTHORS.txt: https://github.com/anymail/django-anymail/blob/main/AUTHORS.txt
|
||||
.. _contributor chart: https://github.com/anymail/django-anymail/graphs/contributors
|
||||
.. _brack3t: http://brack3t.com/
|
||||
.. _Djrill: https://github.com/brack3t/Djrill
|
||||
|
||||
@@ -72,48 +72,42 @@ Anymail is `tested via GitHub Actions`_ 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 django31-py38-all,django20-py35-all,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 :pypi:`mock` and all ESP-specific
|
||||
package requirements.
|
||||
|
||||
To run the tests, you can:
|
||||
To run the tests locally, use :pypi:`tox`:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ python setup.py test # (also installs test dependencies if needed)
|
||||
## install tox and other development requirements:
|
||||
$ python -m pip install -r requirements-dev.txt
|
||||
|
||||
Or:
|
||||
## test a representative combination of Python and Django versions:
|
||||
$ tox -e lint,django42-py311-all,django30-py37-all,docs
|
||||
|
||||
## you can also run just some test cases, e.g.:
|
||||
$ tox -e django42-py311-all tests.test_mailgun_backend tests.test_utils
|
||||
|
||||
## to test more Python/Django versions:
|
||||
$ tox --parallel auto # ALL 20+ envs! (in parallel if possible)
|
||||
|
||||
(If your system doesn't come with the necessary Python versions, `pyenv`_ is helpful
|
||||
to install and manage them. Or use the :shell:`--skip-missing-interpreters` tox option.)
|
||||
|
||||
If you don't want to use tox (or have trouble getting it working), you can run
|
||||
the tests in your current Python environment:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ pip install mock boto3 # install test dependencies
|
||||
## install the testing requirements (if any):
|
||||
$ python -m pip install -r tests/requirements.txt
|
||||
|
||||
## run the tests:
|
||||
$ python runtests.py
|
||||
|
||||
## this command can also run just a few test cases, e.g.:
|
||||
$ python runtests.py tests.test_mailgun_backend tests.test_mailgun_webhooks
|
||||
|
||||
Or to test against multiple versions of Python and Django all at once, use :pypi:`tox`.
|
||||
You'll need some version of Python 3 available. (If your system doesn't come
|
||||
with that, `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 django31-py38-all,django20-py35-all,lint # test recommended environments
|
||||
|
||||
## you can also run just some test cases, e.g.:
|
||||
$ tox -e django31-py38-all,django20-py35-all tests.test_mailgun_backend tests.test_utils
|
||||
|
||||
## to test more Python/Django versions:
|
||||
$ tox --parallel auto # ALL 20+ envs! (in parallel if possible)
|
||||
$ tox --skip-missing-interpreters # if some Python versions aren't installed
|
||||
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 any API keys.)
|
||||
|
||||
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
|
||||
@@ -123,20 +117,19 @@ API keys or other settings. For example:
|
||||
|
||||
$ export ANYMAIL_TEST_MAILGUN_API_KEY='your-Mailgun-API-key'
|
||||
$ export ANYMAIL_TEST_MAILGUN_DOMAIN='mail.example.com' # sending domain for that API key
|
||||
$ tox -e django31-py38-all tests.test_mailgun_integration
|
||||
$ tox -e django42-py311-all 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
|
||||
Be sure to specify a particular testenv with tox's :shell:`-e` option, or tox will repeat the tests
|
||||
for all 20+ supported combinations of Python and Django, sending hundreds of messages.
|
||||
|
||||
|
||||
.. _pyenv: https://github.com/pyenv/pyenv
|
||||
.. _tested via GitHub Actions: https://github.com/anymail/django-anymail/actions?query=workflow:test
|
||||
.. _tests source: https://github.com/anymail/django-anymail/blob/main/tests
|
||||
.. _.travis.yml: https://github.com/anymail/django-anymail/blob/main/.travis.yml
|
||||
|
||||
|
||||
Documentation
|
||||
@@ -155,14 +148,14 @@ It's easiest to build Anymail's docs using tox:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ pip install tox # (if you haven't already)
|
||||
$ python -m pip install -r requirements-dev.txt
|
||||
$ 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)
|
||||
$ (cd .tox/docs/_html; python -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.
|
||||
|
||||
@@ -34,14 +34,21 @@ Installation
|
||||
------------
|
||||
|
||||
You must ensure the :pypi:`boto3` package is installed to use Anymail's Amazon SES
|
||||
backend. Either include the ``amazon_ses`` option when you install Anymail:
|
||||
backend. Either include the ``amazon-ses`` option when you install Anymail:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ pip install "django-anymail[amazon_ses]"
|
||||
$ pip install "django-anymail[amazon-ses]"
|
||||
|
||||
or separately run ``pip install boto3``.
|
||||
|
||||
.. versionchanged:: 10.0
|
||||
|
||||
In earlier releases, the "extra name" could use an underscore
|
||||
(``django-anymail[amazon_ses]``). That now causes pip to warn
|
||||
that "django-anymail does not provide the extra 'amazon_ses'",
|
||||
and may result in a broken installation that is missing boto3.
|
||||
|
||||
To send mail with Anymail's Amazon SES backend, set:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
# Packages required only for building docs
|
||||
|
||||
# (Pygments defaulted "python" to Python 2 before v2.5.0; it doesn't use semver)
|
||||
Pygments~=2.9.0
|
||||
readme-renderer~=37.3
|
||||
sphinx~=4.0
|
||||
sphinx-rtd-theme~=0.5.2
|
||||
|
||||
Reference in New Issue
Block a user