Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion .github/workflows/pythontest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,13 @@ jobs:
fail-fast: false
matrix:
os: [windows-latest, ubuntu-latest, macos-latest]
python-version: ['3.9', '3.10', '3.11', '3.12', '3.13']
python-version: ['3.10', '3.11', '3.12', '3.13', '3.14']
exclude:
# CPython 3.14 on Windows links zlib-ng, which emits different DEFLATE
# bytes than the reference zlib; predictable-zip hashes are pinned in
# Studio, so ricecooker.utils.zip refuses to build archives there.
- os: windows-latest
python-version: '3.14'
steps:
- uses: actions/checkout@v7.0.0
if: ${{ needs.pre_job.outputs.should_skip != 'true' }}
Expand Down
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ Becoming a ricecooker developer
-------------------------------

Ready to contribute? In order to work on the `ricecooker` code you'll first need
to have [Python 3.9+](https://www.python.org/downloads/) on your computer.
to have [Python 3.10+](https://www.python.org/downloads/) on your computer.

Here are the steps for setting up `ricecooker` for local development:

Expand Down Expand Up @@ -131,7 +131,7 @@ Before you submit a pull request, check that it meets these guidelines:
2. If the pull request adds functionality, the docs should be updated. Put
your new functionality into a function with a docstring, and add the
feature to the list in `README.md`.
3. The pull request should work for Python 3.9+. Check the GitHub Actions CI
3. The pull request should work for Python 3.10+. Check the GitHub Actions CI
and make sure that the tests pass for all supported Python versions.


Expand Down
17 changes: 10 additions & 7 deletions docs/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ Installation
Ricecooker chef scripts are typically developed inside their own project, managed
with [`uv`](https://docs.astral.sh/uv/). Add `ricecooker` as a dependency in your
chef project's `pyproject.toml` and run `uv sync` to install it and its Python
dependencies. You'll need Python 3.9-3.13 (matching `ricecooker`'s supported
range), as well as some software for file conversions: `ffmpeg` and `poppler` for
media, and `pandoc` for converting documents (`.docx`, `.odt`, `.rtf`, `.md`) to KPUB.
dependencies. You'll need Python 3.10-3.14 (3.10-3.13 on Windows — see below), as
well as some software for file conversions: `ffmpeg` and `poppler` for media, and
`pandoc` for converting documents (`.docx`, `.odt`, `.rtf`, `.md`) to KPUB.

In the next fifteen minutes or so, we'll setup your computer with all these things
so you can get started writing your first content integration scripts.
Expand All @@ -30,7 +30,7 @@ On a Debian or Ubuntu GNU/Linux, you can install the necessary packages using:

You may need to adjust the package names for other Linux distributions (ContOS/Fedora/OpenSuSE).

*Checklist*: verify your python version is between 3.9 and 3.13 by running `python3 --version`.
*Checklist*: verify your python version is between 3.10 and 3.14 by running `python3 --version`.
If no `python3` command exists, then try `python --version`.
Run the commands `ffmpeg -h`, `pdftoppm -h`, and `pandoc -v` to make sure they are available.

Expand All @@ -40,7 +40,7 @@ Mac OS X users can install the necessary software using [Homebrew](https://brew.

brew install git python3 ffmpeg poppler pandoc

*Checklist*: verify your python version is between 3.9 and 3.13 by running `python3 --version`.
*Checklist*: verify your python version is between 3.10 and 3.14 by running `python3 --version`.
Also run the commands `ffmpeg -h`, `pdftoppm -h`, and `pandoc -v` to make sure they are available.


Expand All @@ -50,9 +50,12 @@ On windows the process is a little longer since we'll have to download and insta
several programs and make sure their `bin`-directories are added to the `Path` variable:

1. Download Python from [https://www.python.org/downloads/windows/](https://www.python.org/downloads/windows/).
Look under the **Python 3.9.x** heading and choose the "Windows x86-64 executable installer"
Look under a supported **Python 3.10.x** or newer heading and choose the "Windows x86-64 executable installer"
option to download the latest installer and follow usual installation steps.
During the installation, make sure to check the box **"Add Python 3.9 to path"**.
During the installation, make sure to check the box **"Add Python to PATH"**.
Python 3.14 is not supported on Windows: its build links `zlib-ng`, which
compresses to different bytes than the reference `zlib`, so the archives it
produces would not match the file hashes Kolibri Studio expects.
- *Checklist*: after installation, open a new command prompt (`cmd.exe`) and
type in `python --version` and `pip --version` to make sure the commands are available.
2. Download `ffmpeg` from [https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-win64-gpl.zip](https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-win64-gpl.zip).
Expand Down
6 changes: 3 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,16 @@ authors = [
license = "MIT"
readme = "README.md"
keywords = ["ricecooker"]
requires-python = ">=3.9, <3.14"
requires-python = ">=3.10, <3.15"
dynamic = ["version"]
classifiers = [
"Intended Audience :: Developers",
"Development Status :: 5 - Production/Stable",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Natural Language :: English",
"Topic :: Education",
]
Expand Down Expand Up @@ -110,7 +110,7 @@ env = [

[tool.ruff]
line-length = 88
target-version = "py39"
target-version = "py310"
exclude = ["docs", "examples"]

[tool.ruff.lint]
Expand Down
4 changes: 2 additions & 2 deletions ricecooker/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,5 @@
except PackageNotFoundError:
__version__ = "0.0.0.dev0"

if sys.version_info < (3, 9, 0):
raise RuntimeError("Ricecooker only supports Python 3.9+")
if sys.version_info < (3, 10, 0):
raise RuntimeError("Ricecooker only supports Python 3.10+")
23 changes: 2 additions & 21 deletions ricecooker/utils/thumbscropping.py
Original file line number Diff line number Diff line change
@@ -1,27 +1,8 @@
import math
import re
import sys
import types

from PIL import Image

# Useful for very coarse version differentiation.
PY2 = sys.version_info[0] == 2
PY3 = sys.version_info[0] == 3

if PY3:
string_types = (str,)
integer_types = (int,)
class_types = (type,)
text_type = str
binary_type = bytes
else:
string_types = (basestring,) # noqa: F821
integer_types = (int, long) # noqa: F821
class_types = (type, types.ClassType)
text_type = unicode # noqa: F821
binary_type = str

# SmileyChris/easy-thumbnails is licensed under the BSD 3-Clause "New" or "Revised" License
# Copyright (c) 2009, Chris Beaven
# All rights reserved.
Expand Down Expand Up @@ -161,7 +142,7 @@ def scale_and_crop( # noqa: C901
diff_x = int(source_x - min(source_x, target_x))
diff_y = int(source_y - min(source_y, target_y))
if crop != "scale" and (diff_x or diff_y):
if isinstance(target, string_types):
if isinstance(target, str):
target = re.match(r"(\d+)?,(\d+)?$", target)
if target:
target = target.groups()
Expand All @@ -180,7 +161,7 @@ def scale_and_crop( # noqa: C901
box.append(int(min(source_x, box[0] + target_x)))
box.append(int(min(source_y, box[1] + target_y)))
# See if an edge cropping argument was provided.
edge_crop = isinstance(crop, string_types) and re.match(
edge_crop = isinstance(crop, str) and re.match(
r"(?:(-?)(\d+))?,(?:(-?)(\d+))?$", crop
)
if edge_crop and filter(None, edge_crop.groups()):
Expand Down
24 changes: 24 additions & 0 deletions ricecooker/utils/zip.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import os
import tempfile
import zipfile
import zlib


def _read_file(path):
Expand Down Expand Up @@ -57,6 +58,28 @@ def find_html_entrypoint(names):
return normalized[0][0]


def _assert_reference_zlib():
"""
Refuse to build predictable zips on interpreters linked against zlib-ng.

zlib-ng emits different DEFLATE bytes than zlib at every level and strategy, and
predictable-zip MD5s are pinned in Studio's JavaScript and already in use for
deduplication. CPython 3.14 on Windows exposes ``zlib.ZLIBNG_VERSION``; older
builds only advertise it as ``1.3.1.zlib-ng`` in ``ZLIB_RUNTIME_VERSION``.
"""
zlibng_version = getattr(zlib, "ZLIBNG_VERSION", None)
if zlibng_version is None and "zlib-ng" in zlib.ZLIB_RUNTIME_VERSION:
zlibng_version = zlib.ZLIB_RUNTIME_VERSION
if zlibng_version is not None:
raise RuntimeError(
"This interpreter's zlib module is backed by zlib-ng {}, which produces "
"different compressed bytes from the reference zlib implementation. "
"Archives built here would not match the file hashes Kolibri Studio "
"expects. Use a Python built against regular zlib (on Windows, Python "
"3.13 or earlier).".format(zlibng_version)
)


def create_predictable_zip(path, entrypoint=None, file_converter=None):
"""
Create a zip file with predictable sort order and metadata so that MD5 will
Expand All @@ -66,6 +89,7 @@ def create_predictable_zip(path, entrypoint=None, file_converter=None):
entrypoint (str or None): if specified, a relative file path in the zip to serve as the first page to load
Returns: path (str) to the output zip file
"""
_assert_reference_zlib()
extension = "zip"
# if path is a directory, recursively enumerate all the files under the directory
if os.path.isdir(path):
Expand Down
16 changes: 16 additions & 0 deletions tests/test_zip.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import pytest

from ricecooker.utils import zip as zip_module
from ricecooker.utils.zip import create_predictable_zip
from ricecooker.utils.zip import find_common_root
from ricecooker.utils.zip import find_html_entrypoint
Expand Down Expand Up @@ -80,6 +81,21 @@ def test_predictable_zip(case_name, case):
cleanup(temp_dir)


@pytest.mark.parametrize(
"attribute,value",
[
# CPython 3.14+ advertises zlib-ng with a dedicated constant...
("ZLIBNG_VERSION", "2.2.5"),
# ...older interpreters only report it in the zlib-compat version string.
("ZLIB_RUNTIME_VERSION", "1.3.1.zlib-ng"),
],
)
def test_refuses_to_build_archives_on_zlib_ng(monkeypatch, tmp_path, attribute, value):
monkeypatch.setattr(zip_module.zlib, attribute, value, raising=False)
with pytest.raises(RuntimeError, match="zlib-ng"):
create_predictable_zip(str(tmp_path))


def test_order_independence():
reversed_files = dict(reversed(list(TEST_CASES["reversed"]["files"].items())))
temp_dir1 = create_test_files(TEST_CASES["reversed"]["files"])
Expand Down
Loading
Loading