# Migration to Python 3.12, Pydantic v2, and uv tooling

This document describes the migration steps performed on the `pyconverters_pyword` project.

## 1. Build system: flit → hatchling/uv

The build system was migrated from **flit** to **hatchling** with **uv** as the package manager.

- `pyproject.toml`: rewritten from flit legacy format (`[tool.flit.metadata]`) to PEP 621
  with `hatchling` backend and `[project]` table
- Version read dynamically from `src/pyconverters_pyword/__init__.py` via `[tool.hatch.version]`
- Entry point `pyconverters.plugins` moved from `[tool.flit.entrypoints]` to `[project.entry-points]`
- Replaced `flit` commands with `uv build` and `uv publish`
- Jenkinsfile updated to use `uv sync` and `uv build`/`uv publish`
- Dockerfile updated to install `uv` via `COPY --from=ghcr.io/astral-sh/uv:latest`

## 2. Python version: 3.8+ → 3.12

- `requires-python` set to `>=3.12` in `pyproject.toml`
- `Dockerfile` base image changed from `python:3.8-slim-bookworm` to `python:3.12-slim-bookworm`
- `FLIT_ROOT_INSTALL=1` env var removed from Dockerfile (no longer needed)
- Classifiers updated from `Python :: 3.8` to `Python :: 3.12`

## 3. Pydantic v1 → v2

Replaced deprecated Pydantic v1 APIs in `pyword.py` and `tests/test_pyword.py`:

- `.json(exclude_none=True, exclude_unset=True, indent=2)` → `.model_dump_json(...)` in tests
- `DocumentList(__root__=docs)` → `DocumentList(root=docs)` (Pydantic v2 `RootModel` API)
- `Field(extra="internal"/"advanced")` → `Field(json_schema_extra={"extra": "..."})` in `pyword.py`
- `params.dict()` → `params.model_dump()` in `pyword.py`

## 4. Python 3.12 type modernization

Leveraged Python 3.12 builtins and modern syntax (enforced by ruff `UP` rules):

- `typing.List[Document]` → `list[Document]` (method signatures in `pyword.py` and tests)
- `typing.Type[BaseModel]` → `type[BaseModel]` (`get_model` return type in `pyword.py`)
- Removed unused `typing` imports: `List`, `Type`

## 5. Linter: black + flake8 → ruff

- Removed `black`, `flake8`, `pytest-flake8`, `pytest-black`, `flakehell` dependencies
- Added `ruff` to `[project.optional-dependencies].test`
- Removed `[tool.flakehell]` configuration block from `pyproject.toml`
- Configured `[tool.ruff]` in `pyproject.toml`:
  - `line-length = 120`
  - `target-version = "py312"`
  - Lint rules: `E`, `W`, `F`, `I`, `B`, `C4`, `UP`, `ARG`, `SIM`
  - Format: double quotes, space indent
- Updated `.pre-commit-config.yaml`: replaced `black` and `flake8` hooks with `ruff` and `ruff-format`

### Running the linter

```
uv run ruff check .
uv run ruff format --check .
```

To auto-fix formatting:

```
uv run ruff format .
```

## 6. Test runner: tox → uv

**tox** was removed entirely. The project now uses `uv run` to execute tests and linting directly.

### What changed

- Deleted `tox.ini`
- Removed `tox`, `flake8`, `pytest-flake8`, `pytest-black` from test dependencies
- Moved pytest configuration from `tox.ini` to `pyproject.toml`:

```toml
[tool.pytest.ini_options]
addopts = "--durations=5"
norecursedirs = ["docs"]
```

### Running tests

```
uv run pytest
```

### Why tox was unnecessary

- The project targets a single Python version (3.12)
- `uv run` provides virtual environment isolation without the extra layer

## 7. Documentation generation

Sphinx is used for documentation with the `docs` optional dependency group.

### Fix: added `lxml_html_clean` dependency

The `jupyter_sphinx` extension requires `lxml_html_clean` (split from `lxml` in recent versions).
This was added to `[project.optional-dependencies].docs`.

### Generating docs

```
uv run --extra docs sphinx-build docs docs/_build
```

## 8. Jenkinsfile updates

- `__init__.py` generation now includes a blank line between the docstring and `__version__`
  to comply with `ruff format`
- Set `__version__ = "0.6.0"`
- In Jenkinsfile, set `MINOR_VERSION = "6"`
- Install step changed from `flit install` to `uv sync --extra test`
- Lint step added: `uv run ruff check .` + `uv run ruff format --check .`
- Test step changed from `tox` to `uv run pytest --junit-xml=...`
- Build/publish pipeline uses `uv build && uv publish` instead of `flit publish`
- Credentials: Jenkinsfile helper functions still read `FLIT_USERNAME` and `FLIT_PASSWORD`
  from `.passwd-pypi`, but expose them as `UV_PUBLISH_USERNAME` and `UV_PUBLISH_PASSWORD`
  for `uv publish`
- Removed `.tox` cleanup steps

## 9. .gitignore updates

Added the following entry:
- `uv.lock` — generated lock file, not committed

(`docs/_build/` was already present.)

## 10. Dependency cleanup and upgrades

### Removed unused dependencies

- None

### Upgraded pinned dependencies

- `pymultirole-plugins>=0.5.0,<0.6.0` → `pymultirole-plugins>=0.6.0,<0.7.0`

## 11. Starlette `UploadFile` API change

The `UploadFile` constructor signature changed in recent Starlette versions:

**Before (old API):**
```python
UploadFile(filename, file, content_type)  # positional args
```

**After (new API):**
```python
UploadFile(file=..., filename=..., headers=Headers({"content-type": "..."}))
```

Updated in `tests/test_pyword.py` and added `Headers` import from `starlette.datastructures`.

## 12. Additional unit tests

Extended `tests/test_pyword.py` with tests covering previously untested behaviour:

| Test | What it covers |
|------|----------------|
| `test_pyword_identifier_is_filename` | `doc.identifier` equals the uploaded filename (not a full path) |
| `test_pyword_image_as_altTexts` | `include_image_base64_as_altTexts=True` → `doc.altTexts` populated with valid mime-types |
| `test_pyword_image_as_links` | `include_image_base64_as_links=True` → `data:image` embedded in document text |
