From 16d0f46ffe27a1d1752c7c77eae35f3ec2dce0f3 Mon Sep 17 00:00:00 2001 From: "Storm B. Heg" Date: Sat, 26 Oct 2024 14:53:53 +0200 Subject: [PATCH 1/8] Drop support for Python 3.8 Support has ended. --- .github/workflows/test.yml | 2 +- README.md | 2 +- docs/installation.rst | 2 +- pyproject.toml | 3 +-- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c00d88f..4148052 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -31,7 +31,7 @@ jobs: needs: lint strategy: matrix: - python: ["3.8", "3.9", "3.10", "3.11", "3.12"] + python: ["3.9", "3.10", "3.11", "3.12"] steps: - uses: actions/checkout@v4 - name: Install optimizers diff --git a/README.md b/README.md index abafe6e..d93d1f4 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ It converts the image between the libraries when necessary. Willow currently has basic resize and crop operations, face and feature detection and animated GIF support. New operations and library integrations can also be [easily implemented](https://willow.wagtail.org/latest/guide/extend.html). -The library is written in pure Python and supports versions 3.8 3.9, 3.10, 3.11 and 3.12. +The library is written in pure Python and supports versions 3.9, 3.10, 3.11 and 3.12. ## Examples diff --git a/docs/installation.rst b/docs/installation.rst index 3759b48..261e491 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -1,7 +1,7 @@ Installation ============ -Willow supports Python 3.8+. It is a pure Python library with no hard +Willow supports Python 3.9+. It is a pure Python library with no hard dependencies so it doesn't require a C compiler for a basic installation. Installation using ``pip`` diff --git a/pyproject.toml b/pyproject.toml index 8cb5f1d..e2d24de 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,6 @@ classifiers = [ "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", @@ -24,7 +23,7 @@ classifiers = [ ] dynamic = ["version"] # will read __version__ from willow/__init__.py -requires-python = ">=3.8" +requires-python = ">=3.9" dependencies = [ "filetype>=1.0.10,!=1.1.0", "defusedxml>=0.7,<1.0", From 4e83de8ac0d19cec95df1a0b1e4ffb696d31e150 Mon Sep 17 00:00:00 2001 From: "Storm B. Heg" Date: Sat, 26 Oct 2024 14:56:31 +0200 Subject: [PATCH 2/8] Add official support for Python 3.13 --- .github/workflows/publish.yml | 2 +- .github/workflows/test.yml | 4 ++-- README.md | 2 +- pyproject.toml | 1 + 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 5814888..817ce91 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -17,7 +17,7 @@ jobs: - uses: actions/setup-python@v5 with: - python-version: '3.12' + python-version: '3.13' cache: "pip" cache-dependency-path: "**/pyproject.toml" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4148052..16ed35c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,7 +13,7 @@ env: PIP_DISABLE_PIP_VERSION_CHECK: "1" PIP_NO_PYTHON_VERSION_WARNING: "1" COVERAGE_CORE: sysmon # Only supported on Python 3.12+, ignore on older versions - PYTHON_LATEST: "3.12" + PYTHON_LATEST: "3.13" jobs: lint: @@ -31,7 +31,7 @@ jobs: needs: lint strategy: matrix: - python: ["3.9", "3.10", "3.11", "3.12"] + python: ["3.9", "3.10", "3.11", "3.12", "3.13"] steps: - uses: actions/checkout@v4 - name: Install optimizers diff --git a/README.md b/README.md index d93d1f4..315774a 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ It converts the image between the libraries when necessary. Willow currently has basic resize and crop operations, face and feature detection and animated GIF support. New operations and library integrations can also be [easily implemented](https://willow.wagtail.org/latest/guide/extend.html). -The library is written in pure Python and supports versions 3.9, 3.10, 3.11 and 3.12. +The library is written in pure Python and supports versions 3.9, 3.10, 3.11, 3.12, and 3.13. ## Examples diff --git a/pyproject.toml b/pyproject.toml index e2d24de..26009aa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,6 +20,7 @@ classifiers = [ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", ] dynamic = ["version"] # will read __version__ from willow/__init__.py From 4ea0975bbaed436b8116e631f94a0e4c5243cf09 Mon Sep 17 00:00:00 2001 From: "Storm B. Heg" Date: Sat, 26 Oct 2024 15:10:09 +0200 Subject: [PATCH 3/8] Update ruff and use ruff-format, drop Black --- .pre-commit-config.yaml | 12 ++++-------- pyproject.toml | 16 +++++++++++++++- ruff.toml | 14 -------------- 3 files changed, 19 insertions(+), 23 deletions(-) delete mode 100644 ruff.toml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b3c926a..0bd691c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,13 +1,9 @@ -default_language_version: - python: python3 +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks repos: - - repo: https://github.com/psf/black-pre-commit-mirror - rev: 23.10.1 - hooks: - - id: black - - repo: https://github.com/astral-sh/ruff-pre-commit - rev: 'v0.1.2' + rev: "v0.7.1" hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] + - id: ruff-format diff --git a/pyproject.toml b/pyproject.toml index 26009aa..8dc71a3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -75,7 +75,6 @@ exclude = [ "tests/", "CHANGELOG.txt", "Dockerfile.py3", - "ruff.toml", "runtests.py", ] @@ -115,3 +114,18 @@ exclude_lines = [ # Nor complain about type checking "if TYPE_CHECKING:", ] + +[tool.ruff] +target-version = "py39" # minimum target version + +# E501: Line too long +lint.ignore = ["E501"] +lint.select = [ + "E", # pycodestyle errors + "F", # pyflakes + "I", # isort + "T20", # flake8-print + "BLE", # flake8-blind-except + "C4", # flake8-comprehensions + "UP", # pyupgrade +] diff --git a/ruff.toml b/ruff.toml deleted file mode 100644 index 2bd2a45..0000000 --- a/ruff.toml +++ /dev/null @@ -1,14 +0,0 @@ -# E501: Line too long -ignore = ["E501"] - -target-version = "py38" # minimum target version - -select = [ - "E", # pycodestyle errors - "F", # pyflakes - "I", # isort - "T20", # flake8-print - "BLE", # flake8-blind-except - "C4", # flake8-comprehensions - "UP", # pyupgrade -] From 0aa233c838bb4a348913768854ec64220497d4f7 Mon Sep 17 00:00:00 2001 From: "Storm B. Heg" Date: Sat, 26 Oct 2024 15:13:12 +0200 Subject: [PATCH 4/8] Reformat with new Ruff 0.7.1 --- tests/test_image.py | 5 +++-- tests/test_registry.py | 18 +++++++++--------- willow/image.py | 2 +- willow/optimizers/base.py | 6 +++--- willow/optimizers/cwebp.py | 6 +++--- willow/optimizers/gifsicle.py | 4 ++-- willow/optimizers/jpegoptim.py | 4 ++-- willow/optimizers/optipng.py | 4 ++-- willow/optimizers/pngquant.py | 4 ++-- willow/registry.py | 10 ++++------ 10 files changed, 31 insertions(+), 32 deletions(-) diff --git a/tests/test_image.py b/tests/test_image.py index 194c9b4..4e967fc 100644 --- a/tests/test_image.py +++ b/tests/test_image.py @@ -326,8 +326,9 @@ def test_optimize_with_an_actual_file( # let's preserve the original file by mocking the open call with it so we don't end up changing it. with open("tests/images/people.jpg", "rb") as f: original_value = f.read() - with open("tests/images/people.jpg", "wb") as f, mock.patch( - "builtins.open", mock.mock_open(read_data=original_value) + with ( + open("tests/images/people.jpg", "wb") as f, + mock.patch("builtins.open", mock.mock_open(read_data=original_value)), ): self.image.optimize(f, "jpeg") mock_process.assert_called_with("tests/images/people.jpg") diff --git a/tests/test_registry.py b/tests/test_registry.py index b8e8bdb..dbc543b 100644 --- a/tests/test_registry.py +++ b/tests/test_registry.py @@ -227,9 +227,9 @@ def test_get_converter(self): def test_converter(image): pass - self.registry._registered_converters[ - self.TestImage, self.AnotherTestImage - ] = test_converter + self.registry._registered_converters[self.TestImage, self.AnotherTestImage] = ( + test_converter + ) self.assertEqual( test_converter, @@ -252,15 +252,15 @@ def test_converter_2(image): def test_converter_3(image): return image - self.registry._registered_converters[ - self.TestImage, self.AnotherTestImage - ] = test_converter + self.registry._registered_converters[self.TestImage, self.AnotherTestImage] = ( + test_converter + ) self.registry._registered_converters[ self.TestImage, self.UnregisteredTestImage ] = test_converter_2 - self.registry._registered_converters[ - self.AnotherTestImage, self.TestImage - ] = test_converter_3 + self.registry._registered_converters[self.AnotherTestImage, self.TestImage] = ( + test_converter_3 + ) result = list(self.registry.get_converters_from(self.TestImage)) self.assertIn((test_converter, self.AnotherTestImage), result) diff --git a/willow/image.py b/willow/image.py index c3f3f2c..67d42fd 100644 --- a/willow/image.py +++ b/willow/image.py @@ -128,7 +128,7 @@ def save( "avif", "ico", ]: - raise ValueError("Unknown image format: %s" % image_format) + raise ValueError(f"Unknown image format: {image_format}") operation_name = "save_as_" + image_format return getattr(self, operation_name)(output, apply_optimizers=apply_optimizers) diff --git a/willow/optimizers/base.py b/willow/optimizers/base.py index 676292d..e1cae40 100644 --- a/willow/optimizers/base.py +++ b/willow/optimizers/base.py @@ -1,6 +1,6 @@ import logging import subprocess -from typing import ClassVar, List +from typing import ClassVar logger = logging.getLogger("willow") @@ -17,7 +17,7 @@ def applies_to(cls, image_format: str) -> bool: return image_format.lower() == cls.image_format.lower() @classmethod - def get_check_library_arguments(cls) -> List[str]: + def get_check_library_arguments(cls) -> list[str]: """ Return a list of arguments to check if the library exists. @@ -35,7 +35,7 @@ def check_library(cls) -> bool: return False @classmethod - def get_command_arguments(cls, file_path: str) -> List[str]: + def get_command_arguments(cls, file_path: str) -> list[str]: """Return a list of arguments for the given optimizer library.""" return [] diff --git a/willow/optimizers/cwebp.py b/willow/optimizers/cwebp.py index 67079a1..10c5a6c 100644 --- a/willow/optimizers/cwebp.py +++ b/willow/optimizers/cwebp.py @@ -1,4 +1,4 @@ -from typing import ClassVar, List +from typing import ClassVar from .base import OptimizerBase @@ -12,14 +12,14 @@ class Cwebp(OptimizerBase): image_format: ClassVar[str] = "webp" @classmethod - def get_check_library_arguments(cls) -> List[str]: + def get_check_library_arguments(cls) -> list[str]: # running just cwebp gives basic infor and returns a zero exit code return [] @classmethod def get_command_arguments( cls, file_path: str, progressive: bool = False - ) -> List[str]: + ) -> list[str]: return [ "-m", "6", # inspect all encoding possibilities for best file size diff --git a/willow/optimizers/gifsicle.py b/willow/optimizers/gifsicle.py index f1217ca..c0a6d50 100644 --- a/willow/optimizers/gifsicle.py +++ b/willow/optimizers/gifsicle.py @@ -1,4 +1,4 @@ -from typing import ClassVar, List +from typing import ClassVar from .base import OptimizerBase @@ -12,7 +12,7 @@ class Gifsicle(OptimizerBase): image_format: ClassVar[str] = "gif" @classmethod - def get_command_arguments(cls, file_path: str) -> List[str]: + def get_command_arguments(cls, file_path: str) -> list[str]: return [ "-b", # required parameter for the package "-O3", # slowest, but produces best results diff --git a/willow/optimizers/jpegoptim.py b/willow/optimizers/jpegoptim.py index 6531a53..f4dfbe1 100644 --- a/willow/optimizers/jpegoptim.py +++ b/willow/optimizers/jpegoptim.py @@ -1,4 +1,4 @@ -from typing import ClassVar, List +from typing import ClassVar from .base import OptimizerBase @@ -12,7 +12,7 @@ class Jpegoptim(OptimizerBase): image_format: ClassVar[str] = "jpeg" @classmethod - def get_command_arguments(cls, file_path: str) -> List[str]: + def get_command_arguments(cls, file_path: str) -> list[str]: return [ "--strip-all", # strip out all text information like comments and EXIF data "--max=85", # set maximum quality diff --git a/willow/optimizers/optipng.py b/willow/optimizers/optipng.py index c9ee497..26e9735 100644 --- a/willow/optimizers/optipng.py +++ b/willow/optimizers/optipng.py @@ -1,4 +1,4 @@ -from typing import ClassVar, List +from typing import ClassVar from .base import OptimizerBase @@ -12,7 +12,7 @@ class Optipng(OptimizerBase): image_format: ClassVar[str] = "png" @classmethod - def get_command_arguments(cls, file_path: str) -> List[str]: + def get_command_arguments(cls, file_path: str) -> list[str]: return [ "-quiet", "-o2", # optimization level 2 (out of 7) diff --git a/willow/optimizers/pngquant.py b/willow/optimizers/pngquant.py index e6db64f..64fc450 100644 --- a/willow/optimizers/pngquant.py +++ b/willow/optimizers/pngquant.py @@ -1,4 +1,4 @@ -from typing import ClassVar, List +from typing import ClassVar from .base import OptimizerBase @@ -14,7 +14,7 @@ class Pngquant(OptimizerBase): @classmethod def get_command_arguments( cls, file_path: str, progressive: bool = False - ) -> List[str]: + ) -> list[str]: return [ "--force", # allow overwriting existing files "--strip", # remove optional metadata diff --git a/willow/registry.py b/willow/registry.py index 24573fb..756846d 100644 --- a/willow/registry.py +++ b/willow/registry.py @@ -1,5 +1,5 @@ from collections import defaultdict -from typing import TYPE_CHECKING, List +from typing import TYPE_CHECKING if TYPE_CHECKING: from .optimizers import OptimizerBase @@ -38,7 +38,7 @@ def __init__(self): self._registered_operations = defaultdict(dict) self._registered_converters = {} self._registered_converter_costs = {} - self._registered_optimizers: List["OptimizerBase"] = [] + self._registered_optimizers: list[OptimizerBase] = [] def register_operation(self, image_class, operation_name, func): self._registered_operations[image_class][operation_name] = func @@ -171,9 +171,7 @@ def get_image_classes(self, with_operation=None, available=None): raise UnavailableOperationError( "\n".join( [ - "The operation '{}' is available in the following image classes but they all raised errors:".format( - with_operation - ) + f"The operation '{with_operation}' is available in the following image classes but they all raised errors:" ] + [ "{image_class_name}: {error_message}".format( @@ -193,7 +191,7 @@ def get_image_classes(self, with_operation=None, available=None): else: return image_classes - def get_optimizers_for_format(self, image_format: str) -> List["OptimizerBase"]: + def get_optimizers_for_format(self, image_format: str) -> list["OptimizerBase"]: optimizers = [] for optimizer in self._registered_optimizers: if optimizer.applies_to(image_format): From 820f4aa7847d2a6ec097107bdfeea43cd041744b Mon Sep 17 00:00:00 2001 From: "Storm B. Heg" Date: Sat, 26 Oct 2024 15:13:43 +0200 Subject: [PATCH 5/8] Add prettier and other useful pre-commit hooks --- .pre-commit-config.yaml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0bd691c..bee04aa 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,9 +1,27 @@ # See https://pre-commit.com for more information # See https://pre-commit.com/hooks.html for more hooks + +# Don't touch our sample test images +exclude: "^tests/images/" repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-added-large-files + - id: check-merge-conflict + - id: check-toml + - id: check-yaml + - id: debug-statements - repo: https://github.com/astral-sh/ruff-pre-commit rev: "v0.7.1" hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] - id: ruff-format + - repo: https://github.com/pycontribs/mirrors-prettier + rev: v3.3.3 + hooks: + - id: prettier + types_or: [json, yaml, markdown, bash, editorconfig, toml] From ee083fc9e794e5f06c501d9c34b7b3f91dcde3a8 Mon Sep 17 00:00:00 2001 From: "Storm B. Heg" Date: Sat, 26 Oct 2024 15:25:24 +0200 Subject: [PATCH 6/8] Reformat yaml files with prettier --- .github/workflows/publish.yml | 4 ++-- .github/workflows/test.yml | 2 +- .readthedocs.yaml | 2 +- README.md | 3 +-- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 817ce91..96c1a8a 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -17,7 +17,7 @@ jobs: - uses: actions/setup-python@v5 with: - python-version: '3.13' + python-version: "3.13" cache: "pip" cache-dependency-path: "**/pyproject.toml" @@ -35,7 +35,7 @@ jobs: # https://docs.pypi.org/trusted-publishers/using-a-publisher/ pypi-publish: needs: build - environment: 'publish' + environment: "publish" name: ⬆️ Upload release to PyPI runs-on: ubuntu-latest diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 16ed35c..13334b7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -35,7 +35,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Install optimizers - run: | + run: | sudo apt-get install -y jpegoptim pngquant gifsicle optipng libjpeg-progs webp - name: Set up Python ${{ matrix.python }} uses: actions/setup-python@v5 diff --git a/.readthedocs.yaml b/.readthedocs.yaml index c592bb7..5ed2b4d 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -8,7 +8,7 @@ build: python: "3.12" sphinx: - configuration: docs/conf.py + configuration: docs/conf.py python: install: diff --git a/README.md b/README.md index 315774a..eb685a0 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,6 @@ This will open the image file with Pillow or Wand (if Pillow is unavailable). It will then resize it to 100x100 pixels and save it back out as a PNG file. - ### Detecting faces ```python @@ -87,6 +86,6 @@ As neither Pillow nor Wand support detecting faces, Willow would automatically c \* Always returns `False` -\** Always returns `1` +\*\* Always returns `1` ⁺ Requires the [pillow-heif](https://pypi.org/project/pillow-heif/) library From addb84d3db576070e2612784406d30fc59796e9d Mon Sep 17 00:00:00 2001 From: "Storm B. Heg" Date: Sat, 26 Oct 2024 15:32:10 +0200 Subject: [PATCH 7/8] Allow use of Pillow 11 This allows running under Python 3.13. As of writing, the 10.x series does not officially support Python 3.13. --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 8dc71a3..f5e6b98 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,7 +31,7 @@ dependencies = [ ] [project.optional-dependencies] -pillow = ["Pillow>=9.1.0,<11.0.0"] +pillow = ["Pillow>=9.1.0,<12.0.0"] wand = ["Wand>=0.6,<1.0"] heif = [ "pillow-heif>=0.10.0,<1.0.0; python_version < '3.12'", From 204efd1ca15674aa58ce99d575ada86de1af188d Mon Sep 17 00:00:00 2001 From: "Storm B. Heg" Date: Sat, 26 Oct 2024 15:37:05 +0200 Subject: [PATCH 8/8] Add changelog entries --- docs/changelog.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 5bccc5e..536e04a 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -5,6 +5,9 @@ Unreleased ---------- - Improve type handling when running optimisers (Jake Howard) +- Add support for Pillow 11 (Storm Heg) +- Add support for Python 3.13 (Storm Heg) +- Drop support for Python 3.8 (Storm Heg) 1.8.0 (2024-01-17) ------------------