diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 53bfa29..921f638 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -24,17 +24,29 @@ jobs: with: 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 run: | python -m pip install --upgrade pip - # Install package in development mode - pip install -e . - # Install test dependencies if they exist - 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 - + # Install package in development mode, with the declared dev extra. + # Test dependencies belong in setup.cfg, not in an ad-hoc pip line. + pip install -e '.[dev]' + - name: Download initial failed badges run: | echo "Downloading initial failed badges..." diff --git a/docs/ARCHITECTURE_REVIEW.md b/docs/ARCHITECTURE_REVIEW.md index e52c260..a20fc99 100644 --- a/docs/ARCHITECTURE_REVIEW.md +++ b/docs/ARCHITECTURE_REVIEW.md @@ -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 | | [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 | -| [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 | | [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 | @@ -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 ``` -(Two further failures in `test_concrete_image.py` are environmental — the review -venv lacked `requests`. See R4, which is the same underlying problem.) +(The 2 skips were environmental — the review venv lacked `requests`, so the URL +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. @@ -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 -[setup.cfg](../setup.cfg) declares: +The project carries **three** sets of packaging metadata: -``` -install_requires = - Pillow - numpy -``` +| File | Declares | +|------|----------| +| `pyproject.toml` `[project]` | Pillow, numpy, pyphen, beautifulsoup4, flask, ebooklib, requests | +| `setup.cfg` `[options]` | Pillow, numpy | +| `setup.py` `setup(...)` kwargs | Pillow, numpy | -The library also imports `pyphen` ([abstract/inline.py](../pyWebLayout/abstract/inline.py)), -`bs4` ([io/readers/html_extraction.py](../pyWebLayout/io/readers/html_extraction.py)) -and `ebooklib` ([io/readers/epub_reader.py](../pyWebLayout/io/readers/epub_reader.py)). -`import pyWebLayout.concrete` fails without `pyphen`. +`pyproject.toml`'s `[project]` table wins under any modern build backend, so the +shipped wheel is correct and `pip install pyWebLayout` works. The `setup.cfg` and +`setup.py` copies are dead, contradictory, and actively misleading — reading +either one gives the wrong answer about what the library needs. -There is no declared test extra either; `tests/concrete/test_concrete_image.py` -needs `requests`, which nothing declares. +The authoritative list is itself wrong in the other direction: + +- **`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 -- Add `pyphen`, `beautifulsoup4` and `ebooklib` to `install_requires`, with lower - bounds. -- Add an `[options.extras_require] test =` entry covering `pytest` and `requests`. -- Add a CI job that installs the built wheel into an empty environment and runs - `python -c "import pyWebLayout.concrete, pyWebLayout.io.readers.epub_reader"`. - This class of defect is only ever caught by installing what you ship. +- Consolidate on `pyproject.toml`. Reduce `setup.cfg` to its `[flake8]` section + and `setup.py` to a `setup()` shim, each with a comment saying where metadata + lives. +- Runtime deps: Pillow, numpy, pyphen, beautifulsoup4. Move flask, werkzeug, + ebooklib and requests into a `test` extra; add a `remote-images` extra for + 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 -`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 ``` -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 R3 ── font scaling loses links; independent, user-visible R5 ── delete the monkey patch; minutes diff --git a/pyproject.toml b/pyproject.toml index 847baed..20497bb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,24 +4,56 @@ build-backend = "setuptools.build_meta" [project] name = "pyWebLayout" +version = "0.1.1" description = "A Python library for HTML-like layout and rendering" readme = "README.md" -requires-python = ">=3.6" +requires-python = ">=3.10" license = {file = "LICENSE"} authors = [ {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 = [ "Pillow", "numpy", "pyphen", "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] source = ["pyWebLayout"] branch = true diff --git a/setup.cfg b/setup.cfg index be59e20..68cd9af 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,26 +1,6 @@ -[metadata] -name = pyWebLayout -version = 0.1.1 -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* +# Packaging metadata lives in pyproject.toml ([project]), which takes +# precedence over anything declared here. This file keeps only tool config +# that has nowhere better to live. [flake8] exclude = diff --git a/setup.py b/setup.py index 2b341cb..225e940 100644 --- a/setup.py +++ b/setup.py @@ -1,32 +1,10 @@ -from setuptools import setup, find_packages +"""Shim for legacy `python setup.py` invocations. -setup( - name="pyWebLayout", - version="0.1.1", - packages=find_packages(), - install_requires=[ - "Pillow", - "numpy", - ], - 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", -) +All packaging metadata lives in setup.cfg. Keeping a second copy here was an +active hazard: keyword arguments passed to setup() override setup.cfg, so the +two could disagree silently and the setup.py copy would win. +""" + +from setuptools import setup + +setup()