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:
Mike Edmunds
2023-05-03 16:55:08 -07:00
committed by GitHub
parent 9fba58237d
commit e8df0ec8e0
31 changed files with 418 additions and 292 deletions

View File

@@ -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
View 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()

View File

@@ -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

View File

@@ -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.

View File

@@ -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.

View File

@@ -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

View File

@@ -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