- Support ReportLab 5 and Python 3.10-3.14, fix 14 bugs, harden the test suite
Widen the ReportLab constraint to >=4.0.4,<6 and raise the Python floor to
3.10, after validating empirically that ReportLab 5 does not change rendering:
references built with one ReportLab major and compared against a render from
the other produce zero pixel differences across all 23 testrender fixtures, in
both directions and down to the declared 4.0.4 floor. The CI matrix now covers
Python 3.10 to 3.14 against both ReportLab majors.
The only behavioural regression in ReportLab 5 is rl_config.trustedHosts=None
changing from "every host is trusted" to "no host is trusted", which makes
open_for_read refuse every URL and data: URI. That default is deliberate SSRF
hardening and is left untouched; PmlImageReader falls back to xhtml2pdf's own
fetcher instead.
The ReportLab 5 assessment also surfaced that the rendering comparison could
never fail. CI regenerated the reference from the same commit and the same
ReportLab immediately before comparing, and tox passed --nofail. Removing
--nofail alone would not have helped: ImageMagick compare exits 1 when images
differ and exec_cmd treated that as a fatal error, aborting on the first
differing page before the HTML report was written. A cross-version job now
builds the reference with one ReportLab major and renders with the other, in
both directions.
Bug fixes:
- @page name:left / @page name:right now work. A stray sys.exit() in
PmlBaseDoc.handle_nextPageTemplate terminated the calling process, and the
vendored PTCycle predated ReportLab's next_value protocol, so the alternating
left/right page feature had never worked. ReportLab's own PTCycle is used now. - Temporary files are no longer shared between threads. TmpFiles subclasses
threading.local but declaredfilesas a class attribute, so one request's
cleanFiles() closed files another request was still reading. - The WSGI middleware produces PDFs. PisaMiddleware.filter wrote PDF bytes into
a StringIO, and the response buffer rejected the bytes chunks that PEP 3333
requires applications to yield. - pisaPDF.addFromString() works; it passed an unsupported
capacitykeyword to
getFile() and appended raw bytes where PdfReader needs a file-like object. - pisaFileObject accepts bytes and pathlib.Path, as its type hints claimed.
Both raised AttributeError, swallowed into a silent None. - data: URIs are parsed per RFC 2397. The percent-encoded form, which is the
usual way inline SVG is written, was rejected outright. - HTTP responses follow redirects (bounded to 5 hops), accept any 2xx status
rather than the literal "200 OK" reason phrase, close their connection, and
log a warning instead of a debug message on failure. - Plain HTTP requests honour the configured http_timeout; it was only applied
to HTTPS connections, so plain HTTP could hang indefinitely. - pisaTempFile(capacity=-1) keeps its buffer in memory, as documented.
len(buffer) > capacityis true for any buffer when capacity is negative, so
it spilled to disk immediately -- the default for pisaPDF and pisaContext. - Temporary files are registered with the cleanup registry even when the
resource is empty; registration sat inside anif data:, so those files were
never closed by cleanFiles(). Surfaced by the new ResourceWarning in 3.14. - pisaTempFile.getFileName() returns the file name instead of None.
- Paragraph.getPlainText() works; a misplaced bracket made it join a sequence
of one-element lists and raise TypeError on every call. - Filter.should_filter is abstract instead of printing headers to stdout and
returning None. - The memoization cache in xhtml2pdf.util is bounded and cleared after each
render; its keys come from CSS in the rendered document.
Test suite:
- The suite no longer reaches the public internet; fixtures are served from
tests/httpserver.py on localhost. A CI job runs it with no outbound network. - make test-render fails with a clear message when the reference set has not
been generated, instead of reporting every page as a difference. New
make test-render-all creates the reference and compares in one go. - A missing reference counts as a difference rather than being silently
skipped, and exec_cmd distinguishes "images differ" from a real error. - The tox envlist matches the CI matrix again; TOXENV=py3.13 previously matched
no environment, so that leg ran no tests. CI now asserts the match. - New coverage for pdf.py, wsgi.py, the pisa CLI, files.py and
reportlab_paragraph.py, plus a contract test pinning every private ReportLab
symbol the package imports. Removed the dead tests/runtests.py harness. - The asian font canary asserts a subset, so an upstream addition no longer
fails the suite.
Deprecations: Python 3.8 and 3.9 are no longer supported. The renderpm extra is
now an alias for pycairo; ReportLab 5 removed the C renderPM backend along with
its renderpm extra, so reportlab[renderpm] silently installed nothing there.
- Add a browser conformance oracle to testrender, and use it to fix CSS support
testrender compared xhtml2pdf against itself: the reference was produced by the
same library, so it caught changes in the output but never told anyone whether
that output was right. There was no external reference anywhere in the project,
and a CSS bug could therefore live for years without a test noticing.
New Contributors
- @dimbleby made their first contribution in #796
- @kianmeng made their first contribution in #801
- @hsmett made their first contribution in #800
- @dependabot[bot] made their first contribution in #798
- @alionLevkin made their first contribution in #812
- @jonbb made their first contribution in #803
Full Changelog: v0.2.17...v0.2.18