build: consolidate packaging on pyproject.toml and correct the dependency set

The project carried three sets of packaging metadata — pyproject.toml
[project], setup.cfg [options] and setup.py kwargs — declaring different
dependencies. pyproject.toml wins under any modern backend, so the wheel
was correct, but the other two said "Pillow, numpy" and reading either
gave the wrong answer about what the library needs.

Reduce setup.cfg to its [flake8] section and setup.py to a setup() shim,
each pointing at pyproject.toml.

Correct the authoritative list while consolidating:

- flask was a runtime dependency but is imported only by the fixture HTTP
  server in tests. Every user was installing Flask, Jinja2, Werkzeug,
  click, itsdangerous and blinker for nothing.
- ebooklib was a runtime dependency and is never imported by the library;
  epub_reader.py uses zipfile + xml.etree directly. Only the tests use it,
  to build EPUB fixtures.
- requests was declared required but concrete/image.py imports it lazily
  and degrades gracefully when absent, so it belongs in an extra.
- requires-python said >=3.6, which cannot be true: the package uses
  dataclasses and `from __future__ import annotations`, both 3.7+, and CI
  tests 3.10/3.12/3.13.

Runtime install drops from 7 direct dependencies to 4. Adds test,
remote-images and dev extras, and a CI step that installs into an empty
venv with only the runtime deps and imports every subpackage — this class
of defect is only caught by installing what you ship.

Verified: runtime-only install imports all subpackages; `.[test]` runs the
full suite, 853 passed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-08 12:30:08 +02:00
co-authored by Claude Opus 5
parent 7384a32cdd
commit 767e4c135c
5 changed files with 114 additions and 92 deletions
+20 -8
View File
@@ -24,17 +24,29 @@ jobs:
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
- name: Verify declared dependencies are sufficient
run: |
# A clean venv with ONLY the declared runtime deps. If an import here
# fails, install_requires is incomplete and a real `pip install
# pyWebLayout` would fail the same way for a user.
python -m venv /tmp/clean-install
/tmp/clean-install/bin/pip install --upgrade pip
/tmp/clean-install/bin/pip install .
/tmp/clean-install/bin/python -c "
import pyWebLayout.concrete, pyWebLayout.abstract
import pyWebLayout.io.readers.epub_reader
import pyWebLayout.io.readers.html_extraction
import pyWebLayout.layout.ereader_manager
print('clean install imports OK')
"
- name: Install dependencies - name: Install dependencies
run: | run: |
python -m pip install --upgrade pip python -m pip install --upgrade pip
# Install package in development mode # Install package in development mode, with the declared dev extra.
pip install -e . # Test dependencies belong in setup.cfg, not in an ad-hoc pip line.
# Install test dependencies if they exist pip install -e '.[dev]'
if [ -f requirements-dev.txt ]; then pip install -r requirements-dev.txt; fi
if [ -f requirements/test.txt ]; then pip install -r requirements/test.txt; fi
# Install common test packages
pip install pytest pytest-cov flake8 coverage-badge interrogate
- name: Download initial failed badges - name: Download initial failed badges
run: | run: |
echo "Downloading initial failed badges..." echo "Downloading initial failed badges..."
+45 -25
View File
@@ -18,7 +18,7 @@ finding is already specced, it is cross-referenced rather than restated.
| [R1](#r1--the-process-pool-crashes-on-python-314) | The process pool crashes on Python 3.14 | Critical | New; raises priority of S12 | | [R1](#r1--the-process-pool-crashes-on-python-314) | The process pool crashes on Python 3.14 | Critical | New; raises priority of S12 |
| [R2](#r2--the-test-suite-hangs-at-interpreter-exit) | Test suite hangs at interpreter exit | High | New; same root cause as R1 | | [R2](#r2--the-test-suite-hangs-at-interpreter-exit) | Test suite hangs at interpreter exit | High | New; same root cause as R1 |
| [R3](#r3--font-scaling-destroys-hyperlinks) | Font scaling destroys hyperlinks | High | New | | [R3](#r3--font-scaling-destroys-hyperlinks) | Font scaling destroys hyperlinks | High | New |
| [R4](#r4--declared-dependencies-are-incomplete) | Declared dependencies are incomplete | High | New | | [R4](#r4--three-packaging-configs-that-disagree) | Three packaging configs that disagree | Medium | New (corrected) |
| [R5](#r5--monkey-patched-page-methods-with-a-conflicting-signature) | Monkey-patched `Page` methods with a conflicting signature | Medium | New | | [R5](#r5--monkey-patched-page-methods-with-a-conflicting-signature) | Monkey-patched `Page` methods with a conflicting signature | Medium | New |
| [R6](#r6--dead-duck-typing-cluster-in-pagepy) | Dead duck-typing cluster in `page.py` | Low | New; extends S10.3 | | [R6](#r6--dead-duck-typing-cluster-in-pagepy) | Dead duck-typing cluster in `page.py` | Low | New; extends S10.3 |
| [R7](#r7--two-orphaned-subsystems) | Two orphaned subsystems | Low | New | | [R7](#r7--two-orphaned-subsystems) | Two orphaned subsystems | Low | New |
@@ -79,8 +79,9 @@ At `c5c61a3`, in a clean venv on Python 3.14.6:
833 passed, 2 skipped, 24 subtests passed in 11.51s 833 passed, 2 skipped, 24 subtests passed in 11.51s
``` ```
(Two further failures in `test_concrete_image.py` are environmental — the review (The 2 skips were environmental — the review venv lacked `requests`, so the URL
venv lacked `requests`. See R4, which is the same underlying problem.) image tests skipped. With the `test` extra from R4 installed the suite reports
`853 passed, 24 subtests passed in 13.53s`.)
The suite then **hangs indefinitely** rather than exiting. See R2. The suite then **hangs indefinitely** rather than exiting. See R2.
@@ -267,40 +268,59 @@ This is strictly a stopgap — it keeps the per-page allocation cost.
--- ---
## R4 — Declared dependencies are incomplete ## R4 — Three packaging configs that disagree
**Severity: high. A clean `pip install pyWebLayout` fails on first import.** **Severity: medium.** *Corrected: the original review claimed a clean install
fails on first import. It does not — see below.*
### Problem ### Problem
[setup.cfg](../setup.cfg) declares: The project carries **three** sets of packaging metadata:
``` | File | Declares |
install_requires = |------|----------|
Pillow | `pyproject.toml` `[project]` | Pillow, numpy, pyphen, beautifulsoup4, flask, ebooklib, requests |
numpy | `setup.cfg` `[options]` | Pillow, numpy |
``` | `setup.py` `setup(...)` kwargs | Pillow, numpy |
The library also imports `pyphen` ([abstract/inline.py](../pyWebLayout/abstract/inline.py)), `pyproject.toml`'s `[project]` table wins under any modern build backend, so the
`bs4` ([io/readers/html_extraction.py](../pyWebLayout/io/readers/html_extraction.py)) shipped wheel is correct and `pip install pyWebLayout` works. The `setup.cfg` and
and `ebooklib` ([io/readers/epub_reader.py](../pyWebLayout/io/readers/epub_reader.py)). `setup.py` copies are dead, contradictory, and actively misleading — reading
`import pyWebLayout.concrete` fails without `pyphen`. either one gives the wrong answer about what the library needs.
There is no declared test extra either; `tests/concrete/test_concrete_image.py` The authoritative list is itself wrong in the other direction:
needs `requests`, which nothing declares.
- **`flask` is a runtime dependency.** It is imported only by
`tests/abstract/test_abstract_blocks.py`, as a fixture HTTP server. Every user
installs Flask, Jinja2, Werkzeug, click, itsdangerous and blinker for nothing.
- **`ebooklib` is a runtime dependency and is never imported by the library.**
`epub_reader.py` uses `zipfile` + `xml.etree` directly. Only the *tests* use
ebooklib, to build EPUB fixtures.
- **`requests` is declared required but is optional.** `concrete/image.py:100-111`
imports it lazily and degrades to an error message on the image when absent.
- **`requires-python = ">=3.6"` is false.** The package uses dataclasses (3.7+)
and `from __future__ import annotations` (3.7+); CI tests 3.10, 3.12 and 3.13.
Net effect: a runtime install pulls 7 direct dependencies where 4 are needed.
### Action ### Action
- Add `pyphen`, `beautifulsoup4` and `ebooklib` to `install_requires`, with lower - Consolidate on `pyproject.toml`. Reduce `setup.cfg` to its `[flake8]` section
bounds. and `setup.py` to a `setup()` shim, each with a comment saying where metadata
- Add an `[options.extras_require] test =` entry covering `pytest` and `requests`. lives.
- Add a CI job that installs the built wheel into an empty environment and runs - Runtime deps: Pillow, numpy, pyphen, beautifulsoup4. Move flask, werkzeug,
`python -c "import pyWebLayout.concrete, pyWebLayout.io.readers.epub_reader"`. ebooklib and requests into a `test` extra; add a `remote-images` extra for
This class of defect is only ever caught by installing what you ship. requests; add a `dev` extra composing them.
- Set `requires-python = ">=3.10"` to match the CI matrix, and add version
classifiers.
- Add a CI step that installs the package into an empty venv with **only**
declared runtime deps and imports every top-level subpackage. This class of
defect is only caught by installing what you ship — and it is what would have
caught the original misreading.
### Files ### Files
`setup.cfg`, `setup.py`, CI configuration `pyproject.toml`, `setup.cfg`, `setup.py`, `.gitea/workflows/ci.yml`
--- ---
@@ -477,7 +497,7 @@ positions round-trip through tables correctly (S8 already notes this dependency)
## Recommended order ## Recommended order
``` ```
R4 ── packaging; independent, minutes, unblocks clean CI R4 ── packaging; independent, minutes, unblocks clean CI [done]
S12 ── delete the process pool; resolves R1 and R2 with it S12 ── delete the process pool; resolves R1 and R2 with it
R3 ── font scaling loses links; independent, user-visible R3 ── font scaling loses links; independent, user-visible
R5 ── delete the monkey patch; minutes R5 ── delete the monkey patch; minutes
+37 -5
View File
@@ -4,24 +4,56 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "pyWebLayout" name = "pyWebLayout"
version = "0.1.1"
description = "A Python library for HTML-like layout and rendering" description = "A Python library for HTML-like layout and rendering"
readme = "README.md" readme = "README.md"
requires-python = ">=3.6" requires-python = ">=3.10"
license = {file = "LICENSE"} license = {file = "LICENSE"}
authors = [ authors = [
{name = "Duncan Tourolle", email = "duncan@tourolle.paris"} {name = "Duncan Tourolle", email = "duncan@tourolle.paris"}
] ]
dynamic = ["version"] classifiers = [
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
]
dependencies = [ dependencies = [
"Pillow", "Pillow",
"numpy", "numpy",
"pyphen", "pyphen",
"beautifulsoup4", "beautifulsoup4",
"flask",
"ebooklib",
"requests"
] ]
[project.urls]
Homepage = "https://gitea.tourolle.paris/pyWebLayout"
[project.optional-dependencies]
# Loading images from http(s) URLs. concrete.image imports requests lazily and
# degrades to an error message on the image when it is absent, so it is not a
# hard requirement.
remote-images = ["requests"]
test = [
"pytest>=6.0",
"pytest-cov",
"flask", # fixture HTTP server in tests/abstract/test_abstract_blocks.py
"werkzeug", # make_server, same fixture
"ebooklib", # builds EPUB fixtures; the reader itself uses zipfile + ElementTree
"requests", # exercises the remote-images path
]
dev = [
"pyWebLayout[test,remote-images]",
"flake8",
"coverage-badge",
"interrogate",
]
[tool.setuptools.packages.find]
include = ["pyWebLayout*"]
[tool.coverage.run] [tool.coverage.run]
source = ["pyWebLayout"] source = ["pyWebLayout"]
branch = true branch = true
+3 -23
View File
@@ -1,26 +1,6 @@
[metadata] # Packaging metadata lives in pyproject.toml ([project]), which takes
name = pyWebLayout # precedence over anything declared here. This file keeps only tool config
version = 0.1.1 # that has nowhere better to live.
author = Duncan Tourolle
author_email = duncan@tourolle.paris
description = A Python library for HTML-like layout and rendering
long_description = file: README.md
long_description_content_type = text/markdown
url = https://gitea.tourolle.paris/pyWebLayout
classifiers =
Programming Language :: Python :: 3
License :: OSI Approved :: MIT License
Operating System :: OS Independent
[options]
packages = find:
python_requires = >=3.6
install_requires =
Pillow
numpy
[options.packages.find]
include = pyWebLayout*
[flake8] [flake8]
exclude = exclude =
+9 -31
View File
@@ -1,32 +1,10 @@
from setuptools import setup, find_packages """Shim for legacy `python setup.py` invocations.
setup( All packaging metadata lives in setup.cfg. Keeping a second copy here was an
name="pyWebLayout", active hazard: keyword arguments passed to setup() override setup.cfg, so the
version="0.1.1", two could disagree silently and the setup.py copy would win.
packages=find_packages(), """
install_requires=[
"Pillow", from setuptools import setup
"numpy",
], setup()
extras_require={
"test": [
"coverage>=5.0",
],
"dev": [
"coverage>=5.0",
"pytest>=6.0",
],
},
author="Duncan Tourolle",
author_email="duncan@tourolle.paris",
description="A Python library for HTML-like layout and rendering",
long_description=open("README.md").read(),
long_description_content_type="text/markdown",
url="https://gitea.tourolle.paris/pyWebLayout",
classifiers=[
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
],
python_requires=">=3.6",
)