Rationalize optional dependencies vs dependency groups

Establish clear separation between user-facing optional dependencies
and developer-only dependency groups:

**Optional Dependencies (user features):**
- watcher: File watching service for batch processing
- webservice: Streamlit-based web UI
- Installable via: uv sync --extra <name> or pip install ocrmypdf[name]

**Dependency Groups (developer tools):**
- test: Testing infrastructure (merged from test + extended_test)
- docs: Documentation building tools
- streamlit-dev: Enhanced Streamlit development tools
- dev: General development tools (mypy, ipykernel)
- Installable via: uv sync --group <name> (uv only, NOT pip)

Breaking changes for developers:
- pip install -e .[test] no longer works → use uv sync --group test
- pip install -e .[docs] no longer works → use uv sync --group docs
- pip install -e .[extended_test] removed → merged into test group

No breaking changes for end users:
- pip install ocrmypdf[watcher] still works
- pip install ocrmypdf[webservice] still works

Updated:
- CI/CD workflows to use uv sync --group test
- Docker images to exclude test dependencies
- Documentation to recommend uv with pip as fallback
- pyproject.toml with clear comments explaining both systems
This commit is contained in:
James R. Barlow
2026-01-13 00:34:55 -08:00
parent 740f67091c
commit bf76c8270c
8 changed files with 140 additions and 72 deletions
+1 -1
View File
@@ -55,7 +55,7 @@ RUN --mount=type=cache,target=/root/.cache/uv \
COPY . /app
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --frozen \
--extra test --extra webservice --extra watcher --no-dev \
--extra webservice --extra watcher --no-dev \
--no-install-package pyarrow
FROM base
+1 -1
View File
@@ -39,7 +39,7 @@ RUN --mount=type=cache,target=/root/.cache/uv \
COPY . /app
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --frozen \
--extra test --extra webservice --extra watcher --no-dev \
--extra webservice --extra watcher --no-dev \
--no-install-package pyarrow
FROM base
+3 -3
View File
@@ -74,7 +74,7 @@ jobs:
- name: Install Python packages
run: |
uv sync --extra test --no-dev
uv sync --group test
- name: Report versions
run: |
@@ -137,7 +137,7 @@ jobs:
- name: Install Python packages
run: |
uv sync --extra test --no-dev
uv sync --group test
- name: Report versions
run: |
@@ -192,7 +192,7 @@ jobs:
- name: Install Python packages
run: |
uv sync --extra test --no-dev
uv sync --group test
- name: Test
run: |
+5 -10
View File
@@ -10,17 +10,12 @@ repos:
- id: check-toml
- id: check-yaml
- id: debug-statements
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: "v0.0.261"
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: "v0.14.11"
hooks:
- id: ruff
files: "src/.*\\.pyi?$"
args: [--fix, --exit-non-zero-on-fix]
- repo: https://github.com/psf/black
rev: 23.3.0
hooks:
- id: black
language_version: python
- id: ruff-check
args: [--fix]
- id: ruff-format
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.2.0
hooks:
+4
View File
@@ -117,6 +117,10 @@ tend to give better performance. watcher.py works on all platforms.
Users may need to customize the script to meet their requirements.
:::{code} bash
# Using uv (recommended)
uv sync --extra watcher
# Or using pip
pip3 install ocrmypdf[watcher]
env OCR_INPUT_DIRECTORY=/mnt/input-pdfs \
+56 -3
View File
@@ -686,18 +686,71 @@ need to be installed. The script requires specific versions of the
dependencies. Older version than the ones mentioned in the release notes
are likely not to be compatible to OCRmyPDF.
## Optional Features
OCRmyPDF provides optional features and development tools. We recommend using `uv` as your package manager.
### Installing User Features
User features are available as optional dependencies. Install them with `uv` (recommended) or `pip`:
```bash
# Using uv (recommended)
uv sync --extra watcher # File watching service
uv sync --extra webservice # Streamlit web UI
uv sync --extra watcher --extra webservice # Multiple features
# Using pip (also works)
pip install ocrmypdf[watcher]
pip install ocrmypdf[webservice]
pip install ocrmypdf[watcher,webservice]
```
### Development Tools (uv only)
Development tools use dependency groups and require `uv`:
```bash
# Testing infrastructure
uv sync --group test
# Documentation building
uv sync --group docs
# Enhanced Streamlit development
uv sync --group streamlit-dev
# All development groups
uv sync
```
:::{note}
**User features** (`watcher`, `webservice`) work with both `uv` and `pip`.
**Developer tools** (`test`, `docs`, `streamlit-dev`) require `uv` and use dependency groups (PEP 735).
:::
**Why use uv?**
- Modern, fast Python package manager
- Required for development (testing, docs)
- Better dependency resolution
- Consistent across all platforms
Install uv: `pip install uv` or visit https://docs.astral.sh/uv/
### For development
To install all of the development and test requirements:
```bash
git clone -b main https://github.com/ocrmypdf/OCRmyPDF.git
python -m venv .venv
source .venv/bin/activate
cd OCRmyPDF
pip install -e .[test]
pip install uv # Install uv if not already installed
uv sync --group test
```
Note: Development requires `uv`. The old `pip install -e .[test]` method is no longer supported.
To add JBIG2 encoding, see {ref}`jbig2`.
## Shell completions
+28 -16
View File
@@ -52,19 +52,7 @@ Tracker = "https://github.com/ocrmypdf/OCRmyPDF/issues"
Changelog = "https://github.com/ocrmypdf/OCRmyPDF/docs/release_notes.md"
[project.optional-dependencies]
docs = ["myst-parser>=4.0.1", "sphinx", "sphinx-issues", "sphinx-rtd-theme"]
extended_test = ["PyMuPDF>=1.19.1"]
test = [
"coverage[toml]>=6.2",
"hypothesis>=6.36.0",
"pytest>=6.2.5",
"pytest-cov>=3.0.0",
"pytest-xdist>=2.5.0",
"python-xmp-toolkit==2.0.1", # also requires apt-get install libexempi3
"reportlab>=3.6.8",
"types-Pillow",
"types-humanfriendly",
]
# User-installable features - use `uv sync --extra <name>` or `pip install ocrmypdf[name]`
watcher = ["watchdog>=1.0.2", "typer-slim[standard]", "python-dotenv"]
webservice = ["streamlit>=1.41.0"]
@@ -157,10 +145,34 @@ convention = "google"
quote-style = "preserve"
[dependency-groups]
# Developer-only tools - use `uv sync --group <name>` (NOT pip-installable)
dev = [
"mypy>=1.13.0",
"pymupdf>=1.24.14",
"streamlit-pdf-viewer>=0.0.19",
"streamlit>=1.40.2",
"ipykernel>=6.29.5",
]
test = [
# Core testing framework
"coverage[toml]>=6.2",
"hypothesis>=6.36.0",
"pytest>=6.2.5",
"pytest-cov>=3.0.0",
"pytest-xdist>=2.5.0",
# Test dependencies
"python-xmp-toolkit==2.0.1", # also requires apt-get install libexempi3
"reportlab>=3.6.8",
# Type stubs for testing
"types-Pillow",
"types-humanfriendly",
# Extended test capabilities (merged from extended_test)
"pymupdf>=1.24.14",
]
docs = [
"myst-parser>=4.0.1",
"sphinx",
"sphinx-issues",
"sphinx-rtd-theme",
]
streamlit-dev = [
"streamlit>=1.40.2",
"streamlit-pdf-viewer>=0.0.19",
]
Generated
+42 -38
View File
@@ -1414,27 +1414,6 @@ dependencies = [
]
[package.optional-dependencies]
docs = [
{ name = "myst-parser" },
{ name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
{ name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
{ name = "sphinx-issues" },
{ name = "sphinx-rtd-theme" },
]
extended-test = [
{ name = "pymupdf" },
]
test = [
{ name = "coverage", extra = ["toml"] },
{ name = "hypothesis" },
{ name = "pytest" },
{ name = "pytest-cov" },
{ name = "pytest-xdist" },
{ name = "python-xmp-toolkit" },
{ name = "reportlab" },
{ name = "types-humanfriendly" },
{ name = "types-pillow" },
]
watcher = [
{ name = "python-dotenv" },
{ name = "typer-slim", extra = ["standard"] },
@@ -1448,19 +1427,36 @@ webservice = [
dev = [
{ name = "ipykernel" },
{ name = "mypy" },
{ name = "pymupdf" },
]
docs = [
{ name = "myst-parser" },
{ name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
{ name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
{ name = "sphinx-issues" },
{ name = "sphinx-rtd-theme" },
]
streamlit-dev = [
{ name = "streamlit" },
{ name = "streamlit-pdf-viewer" },
]
test = [
{ name = "coverage", extra = ["toml"] },
{ name = "hypothesis" },
{ name = "pymupdf" },
{ name = "pytest" },
{ name = "pytest-cov" },
{ name = "pytest-xdist" },
{ name = "python-xmp-toolkit" },
{ name = "reportlab" },
{ name = "types-humanfriendly" },
{ name = "types-pillow" },
]
[package.metadata]
requires-dist = [
{ name = "coverage", extras = ["toml"], marker = "extra == 'test'", specifier = ">=6.2" },
{ name = "deprecation", specifier = ">=2.1.0" },
{ name = "fpdf2", specifier = ">=2.8.0" },
{ name = "hypothesis", marker = "extra == 'test'", specifier = ">=6.36.0" },
{ name = "img2pdf", specifier = ">=0.5" },
{ name = "myst-parser", marker = "extra == 'docs'", specifier = ">=4.0.1" },
{ name = "packaging", specifier = ">=20" },
{ name = "pdfminer-six", specifier = ">=20220319" },
{ name = "pi-heif" },
@@ -1468,35 +1464,43 @@ requires-dist = [
{ name = "pillow", specifier = ">=10.0.1" },
{ name = "pluggy", specifier = ">=1" },
{ name = "pydantic", specifier = ">=2.12.5" },
{ name = "pymupdf", marker = "extra == 'extended-test'", specifier = ">=1.19.1" },
{ name = "pypdfium2", specifier = ">=5.0.0" },
{ name = "pytest", marker = "extra == 'test'", specifier = ">=6.2.5" },
{ name = "pytest-cov", marker = "extra == 'test'", specifier = ">=3.0.0" },
{ name = "pytest-xdist", marker = "extra == 'test'", specifier = ">=2.5.0" },
{ name = "python-dotenv", marker = "extra == 'watcher'" },
{ name = "python-xmp-toolkit", marker = "extra == 'test'", specifier = "==2.0.1" },
{ name = "reportlab", marker = "extra == 'test'", specifier = ">=3.6.8" },
{ name = "rich", specifier = ">=13" },
{ name = "sphinx", marker = "extra == 'docs'" },
{ name = "sphinx-issues", marker = "extra == 'docs'" },
{ name = "sphinx-rtd-theme", marker = "extra == 'docs'" },
{ name = "streamlit", marker = "extra == 'webservice'", specifier = ">=1.41.0" },
{ name = "typer-slim", extras = ["standard"], marker = "extra == 'watcher'" },
{ name = "types-humanfriendly", marker = "extra == 'test'" },
{ name = "types-pillow", marker = "extra == 'test'" },
{ name = "uharfbuzz", specifier = ">=0.53.2" },
{ name = "watchdog", marker = "extra == 'watcher'", specifier = ">=1.0.2" },
]
provides-extras = ["docs", "extended-test", "test", "watcher", "webservice"]
provides-extras = ["watcher", "webservice"]
[package.metadata.requires-dev]
dev = [
{ name = "ipykernel", specifier = ">=6.29.5" },
{ name = "mypy", specifier = ">=1.13.0" },
{ name = "pymupdf", specifier = ">=1.24.14" },
]
docs = [
{ name = "myst-parser", specifier = ">=4.0.1" },
{ name = "sphinx" },
{ name = "sphinx-issues" },
{ name = "sphinx-rtd-theme" },
]
streamlit-dev = [
{ name = "streamlit", specifier = ">=1.40.2" },
{ name = "streamlit-pdf-viewer", specifier = ">=0.0.19" },
]
test = [
{ name = "coverage", extras = ["toml"], specifier = ">=6.2" },
{ name = "hypothesis", specifier = ">=6.36.0" },
{ name = "pymupdf", specifier = ">=1.24.14" },
{ name = "pytest", specifier = ">=6.2.5" },
{ name = "pytest-cov", specifier = ">=3.0.0" },
{ name = "pytest-xdist", specifier = ">=2.5.0" },
{ name = "python-xmp-toolkit", specifier = "==2.0.1" },
{ name = "reportlab", specifier = ">=3.6.8" },
{ name = "types-humanfriendly" },
{ name = "types-pillow" },
]
[[package]]
name = "packaging"