Skip to content

Commit

Permalink
Maintenance: Python 3.13 / Pillow 11 / new Ruff / drop Black / drop P…
Browse files Browse the repository at this point in the history
…ython 3.8 (#158)
  • Loading branch information
Stormheg authored Oct 26, 2024
1 parent dc0999d commit e81d28e
Show file tree
Hide file tree
Showing 19 changed files with 82 additions and 67 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand All @@ -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
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -31,11 +31,11 @@ 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", "3.13"]
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
Expand Down
28 changes: 21 additions & 7 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,13 +1,27 @@
default_language_version:
python: python3
# 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/psf/black-pre-commit-mirror
rev: 23.10.1
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: black

- 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.1.2'
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]
2 changes: 1 addition & 1 deletion .readthedocs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ build:
python: "3.12"

sphinx:
configuration: docs/conf.py
configuration: docs/conf.py

python:
install:
Expand Down
5 changes: 2 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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, 3.12, and 3.13.

## Examples

Expand All @@ -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
Expand Down Expand Up @@ -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
3 changes: 3 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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)
------------------
Expand Down
2 changes: 1 addition & 1 deletion docs/installation.rst
Original file line number Diff line number Diff line change
@@ -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``
Expand Down
22 changes: 18 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,22 +16,22 @@ 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",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
]

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",
]

[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'",
Expand Down Expand Up @@ -75,7 +75,6 @@ exclude = [
"tests/",
"CHANGELOG.txt",
"Dockerfile.py3",
"ruff.toml",
"runtests.py",
]

Expand Down Expand Up @@ -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
]
14 changes: 0 additions & 14 deletions ruff.toml

This file was deleted.

5 changes: 3 additions & 2 deletions tests/test_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
18 changes: 9 additions & 9 deletions tests/test_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion willow/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
6 changes: 3 additions & 3 deletions willow/optimizers/base.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import logging
import subprocess
from typing import ClassVar, List
from typing import ClassVar

logger = logging.getLogger("willow")

Expand All @@ -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.
Expand All @@ -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 []

Expand Down
6 changes: 3 additions & 3 deletions willow/optimizers/cwebp.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import ClassVar, List
from typing import ClassVar

from .base import OptimizerBase

Expand All @@ -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
Expand Down
4 changes: 2 additions & 2 deletions willow/optimizers/gifsicle.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import ClassVar, List
from typing import ClassVar

from .base import OptimizerBase

Expand All @@ -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
Expand Down
4 changes: 2 additions & 2 deletions willow/optimizers/jpegoptim.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import ClassVar, List
from typing import ClassVar

from .base import OptimizerBase

Expand All @@ -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
Expand Down
4 changes: 2 additions & 2 deletions willow/optimizers/optipng.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import ClassVar, List
from typing import ClassVar

from .base import OptimizerBase

Expand All @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions willow/optimizers/pngquant.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import ClassVar, List
from typing import ClassVar

from .base import OptimizerBase

Expand All @@ -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
Expand Down
10 changes: 4 additions & 6 deletions willow/registry.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand All @@ -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):
Expand Down

0 comments on commit e81d28e

Please sign in to comment.