diff --git a/.appveyor.yml b/.appveyor.yml index 4c5a7f9ee..b0740b1ac 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -6,6 +6,7 @@ init: # Uncomment previous line to get RDP access during the build. environment: + COVERAGE_CORE: sysmon EXECUTABLE: python.exe TEST_OPTIONS: DEPLOY: YES diff --git a/.ci/requirements-cibw.txt b/.ci/requirements-cibw.txt index dd61634cd..ccd6d87ed 100644 --- a/.ci/requirements-cibw.txt +++ b/.ci/requirements-cibw.txt @@ -1 +1 @@ -cibuildwheel==2.16.2 +cibuildwheel==2.16.5 diff --git a/.ci/requirements-mypy.txt b/.ci/requirements-mypy.txt new file mode 100644 index 000000000..3673e1d81 --- /dev/null +++ b/.ci/requirements-mypy.txt @@ -0,0 +1 @@ +mypy==1.8.0 diff --git a/.coveragerc b/.coveragerc index 46df3f90d..018cc1cbf 100644 --- a/.coveragerc +++ b/.coveragerc @@ -10,6 +10,11 @@ exclude_also = if DEBUG: # Don't complain about compatibility code for missing optional dependencies except ImportError + if TYPE_CHECKING: + @abc.abstractmethod + # Empty bodies in protocols or abstract methods + ^\s*def [a-zA-Z0-9_]+\(.*\)(\s*->.*)?:\s*\.\.\.(\s*#.*)?$ + ^\s*\.\.\.(\s*#.*)?$ [run] omit = diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index e0e6804bf..8fc6bd0ad 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1 @@ -tidelift: "pypi/Pillow" +tidelift: "pypi/pillow" diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 9fe345c8a..92e860cb5 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -7,10 +7,12 @@ on: paths: - ".github/workflows/docs.yml" - "docs/**" + - "src/PIL/**" pull_request: paths: - ".github/workflows/docs.yml" - "docs/**" + - "src/PIL/**" workflow_dispatch: permissions: @@ -37,16 +39,26 @@ jobs: with: python-version: "3.x" cache: pip - cache-dependency-path: ".ci/*.sh" + cache-dependency-path: | + ".ci/*.sh" + "pyproject.toml" - name: Build system information run: python3 .github/workflows/system-info.py + - name: Cache libimagequant + uses: actions/cache@v4 + id: cache-libimagequant + with: + path: ~/cache-libimagequant + key: ${{ runner.os }}-libimagequant-${{ hashFiles('depends/install_imagequant.sh') }} + - name: Install Linux dependencies run: | .ci/install.sh env: GHA_PYTHON_VERSION: "3.x" + GHA_LIBIMAGEQUANT_CACHE_HIT: ${{ steps.cache-libimagequant.outputs.cache-hit }} - name: Build run: | diff --git a/.github/workflows/macos-install.sh b/.github/workflows/macos-install.sh index f41324c4b..28124d7f7 100755 --- a/.github/workflows/macos-install.sh +++ b/.github/workflows/macos-install.sh @@ -2,7 +2,16 @@ set -e -brew install libtiff libjpeg openjpeg libimagequant webp little-cms2 freetype libraqm +brew install \ + freetype \ + ghostscript \ + libimagequant \ + libjpeg \ + libraqm \ + libtiff \ + little-cms2 \ + openjpeg \ + webp export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig" # TODO Update condition when cffi supports 3.13 diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml index 8fc7bd379..a8ddef22c 100644 --- a/.github/workflows/release-drafter.yml +++ b/.github/workflows/release-drafter.yml @@ -23,6 +23,6 @@ jobs: runs-on: ubuntu-latest steps: # Drafts your next release notes as pull requests are merged into "main" - - uses: release-drafter/release-drafter@v5 + - uses: release-drafter/release-drafter@v6 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/system-info.py b/.github/workflows/system-info.py index 57f28c620..9e97b8971 100644 --- a/.github/workflows/system-info.py +++ b/.github/workflows/system-info.py @@ -6,6 +6,7 @@ This sort of info is missing from GitHub Actions. Requested here: https://github.com/actions/virtual-environments/issues/79 """ + from __future__ import annotations import os diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml index 9c3eb0924..4526b9454 100644 --- a/.github/workflows/test-cygwin.yml +++ b/.github/workflows/test-cygwin.yml @@ -26,6 +26,9 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +env: + COVERAGE_CORE: sysmon + jobs: build: runs-on: windows-latest @@ -47,9 +50,8 @@ jobs: uses: actions/checkout@v4 - name: Install Cygwin - uses: cygwin/cygwin-install-action@v4 + uses: egor-tensin/setup-cygwin@v4 with: - platform: x86_64 packages: > gcc-g++ ghostscript @@ -69,6 +71,7 @@ jobs: make netpbm perl + python39=3.9.16-1 python3${{ matrix.python-minor-version }}-cffi python3${{ matrix.python-minor-version }}-cython python3${{ matrix.python-minor-version }}-devel @@ -80,13 +83,13 @@ jobs: zlib-devel - name: Add Lapack to PATH - uses: egor-tensin/cleanup-path@v3 + uses: egor-tensin/cleanup-path@v4 with: dirs: 'C:\cygwin\bin;C:\cygwin\lib\lapack' - name: Select Python version run: | - ln -sf c:/cygwin/bin/python3.${{ matrix.python-minor-version }} c:/cygwin/bin/python3 + ln -sf c:/tools/cygwin/bin/python3.${{ matrix.python-minor-version }} c:/tools/cygwin/bin/python3 - name: Get latest NumPy version id: latest-numpy @@ -141,7 +144,7 @@ jobs: bash.exe .ci/after_success.sh - name: Upload coverage - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v3.1.5 with: file: ./coverage.xml flags: GHA_Cygwin diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index 3bb6856f6..f40286fe4 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -101,7 +101,7 @@ jobs: MATRIX_DOCKER: ${{ matrix.docker }} - name: Upload coverage - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v3.1.5 with: flags: GHA_Docker name: ${{ matrix.docker }} diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml index cdd51e2bb..a07a27c46 100644 --- a/.github/workflows/test-mingw.yml +++ b/.github/workflows/test-mingw.yml @@ -26,6 +26,9 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +env: + COVERAGE_CORE: sysmon + jobs: build: runs-on: windows-latest @@ -64,10 +67,10 @@ jobs: mingw-w64-x86_64-python3-cffi \ mingw-w64-x86_64-python3-numpy \ mingw-w64-x86_64-python3-olefile \ - mingw-w64-x86_64-python3-pip \ mingw-w64-x86_64-python3-setuptools \ mingw-w64-x86_64-python-pyqt6 + python3 -m ensurepip python3 -m pip install pyroma pytest pytest-cov pytest-timeout pushd depends && ./install_extra_test_images.sh && popd @@ -82,7 +85,7 @@ jobs: python3 -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests - name: Upload coverage - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v3.1.5 with: file: ./coverage.xml flags: GHA_Windows diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 8cad7a8b2..c936be559 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -26,13 +26,16 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +env: + COVERAGE_CORE: sysmon + jobs: build: runs-on: windows-latest strategy: fail-fast: false matrix: - python-version: ["pypy3.10", "pypy3.9", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] + python-version: ["pypy3.10", "pypy3.9", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13.0-alpha.3"] timeout-minutes: 30 @@ -66,8 +69,16 @@ jobs: - name: Print build system information run: python3 .github/workflows/system-info.py - - name: python3 -m pip install pytest pytest-cov pytest-timeout defusedxml olefile pyroma - run: python3 -m pip install pytest pytest-cov pytest-timeout defusedxml olefile pyroma + - name: Install Python dependencies + run: > + python3 -m pip install + coverage>=7.4.2 + defusedxml + olefile + pyroma + pytest + pytest-cov + pytest-timeout - name: Install dependencies id: install @@ -202,7 +213,7 @@ jobs: shell: pwsh - name: Upload coverage - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v3.1.5 with: file: ./coverage.xml flags: GHA_Windows diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 05f78704b..643273e58 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -26,6 +26,10 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +env: + COVERAGE_CORE: sysmon + FORCE_COLOR: 1 + jobs: build: @@ -33,7 +37,7 @@ jobs: fail-fast: false matrix: os: [ - "macos-latest", + "macos-14", "ubuntu-latest", ] python-version: [ @@ -47,11 +51,21 @@ jobs: "3.8", ] include: - - python-version: "3.9" + - python-version: "3.11" PYTHONOPTIMIZE: 1 REVERSE: "--reverse" - - python-version: "3.8" + - python-version: "3.10" PYTHONOPTIMIZE: 2 + # M1 only available for 3.10+ + - os: "macos-latest" + python-version: "3.9" + - os: "macos-latest" + python-version: "3.8" + exclude: + - os: "macos-14" + python-version: "3.9" + - os: "macos-14" + python-version: "3.8" runs-on: ${{ matrix.os }} name: ${{ matrix.os }} Python ${{ matrix.python-version }} @@ -65,17 +79,28 @@ jobs: python-version: ${{ matrix.python-version }} allow-prereleases: true cache: pip - cache-dependency-path: ".ci/*.sh" + cache-dependency-path: | + ".ci/*.sh" + "pyproject.toml" - name: Build system information run: python3 .github/workflows/system-info.py + - name: Cache libimagequant + if: startsWith(matrix.os, 'ubuntu') + uses: actions/cache@v4 + id: cache-libimagequant + with: + path: ~/cache-libimagequant + key: ${{ runner.os }}-libimagequant-${{ hashFiles('depends/install_imagequant.sh') }} + - name: Install Linux dependencies if: startsWith(matrix.os, 'ubuntu') run: | .ci/install.sh env: GHA_PYTHON_VERSION: ${{ matrix.python-version }} + GHA_LIBIMAGEQUANT_CACHE_HIT: ${{ steps.cache-libimagequant.outputs.cache-hit }} - name: Install macOS dependencies if: startsWith(matrix.os, 'macOS') @@ -125,9 +150,9 @@ jobs: .ci/after_success.sh - name: Upload coverage - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v3.1.5 with: - flags: ${{ matrix.os == 'macos-latest' && 'GHA_macOS' || 'GHA_Ubuntu' }} + flags: ${{ matrix.os == 'ubuntu-latest' && 'GHA_Ubuntu' || 'GHA_macOS' }} name: ${{ matrix.os }} Python ${{ matrix.python-version }} gcov: true diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index a30d0468c..0bb586b6d 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -19,7 +19,7 @@ FREETYPE_VERSION=2.13.2 HARFBUZZ_VERSION=8.3.0 LIBPNG_VERSION=1.6.40 JPEGTURBO_VERSION=3.0.1 -OPENJPEG_VERSION=2.5.0 +OPENJPEG_VERSION=2.5.2 XZ_VERSION=5.4.5 TIFF_VERSION=4.6.0 LCMS2_VERSION=2.16 @@ -40,7 +40,7 @@ BROTLI_VERSION=1.1.0 if [[ -n "$IS_MACOS" ]] && [[ "$CIBW_ARCHS" == "x86_64" ]]; then function build_openjpeg { - local out_dir=$(fetch_unpack https://github.com/uclouvain/openjpeg/archive/v${OPENJPEG_VERSION}.tar.gz openjpeg-2.5.0.tar.gz) + local out_dir=$(fetch_unpack https://github.com/uclouvain/openjpeg/archive/v${OPENJPEG_VERSION}.tar.gz openjpeg-${OPENJPEG_VERSION}.tar.gz) (cd $out_dir \ && cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib . \ && make install) @@ -62,7 +62,7 @@ function build_brotli { function build { if [[ -n "$IS_MACOS" ]] && [[ "$CIBW_ARCHS" == "arm64" ]]; then - export BUILD_PREFIX="/usr/local" + sudo chown -R runner /usr/local fi build_xz if [ -z "$IS_ALPINE" ] && [ -z "$IS_MACOS" ]; then @@ -72,13 +72,11 @@ function build { build_simple xcb-proto 1.16.0 https://xorg.freedesktop.org/archive/individual/proto if [ -n "$IS_MACOS" ]; then + build_simple xorgproto 2023.2 https://www.x.org/pub/individual/proto + build_simple libXau 1.0.11 https://www.x.org/pub/individual/lib + build_simple libpthread-stubs 0.5 https://xcb.freedesktop.org/dist if [[ "$CIBW_ARCHS" == "arm64" ]]; then - build_simple xorgproto 2023.2 https://www.x.org/pub/individual/proto - build_simple libXau 1.0.11 https://www.x.org/pub/individual/lib - build_simple libpthread-stubs 0.5 https://xcb.freedesktop.org/dist - if [ -f /Library/Frameworks/Python.framework/Versions/Current/share/pkgconfig/xcb-proto.pc ]; then - cp /Library/Frameworks/Python.framework/Versions/Current/share/pkgconfig/xcb-proto.pc /Library/Frameworks/Python.framework/Versions/Current/lib/pkgconfig/xcb-proto.pc - fi + cp /usr/local/share/pkgconfig/xcb-proto.pc /usr/local/lib/pkgconfig fi else sed s/\${pc_sysrootdir\}// /usr/local/share/pkgconfig/xcb-proto.pc > /usr/local/lib/pkgconfig/xcb-proto.pc @@ -89,12 +87,10 @@ function build { build_tiff build_libpng build_lcms2 - if [[ -n "$IS_MACOS" ]] && [[ "$CIBW_ARCHS" == "arm64" ]]; then - for dylib in libjpeg.dylib libtiff.dylib liblcms2.dylib; do - cp $BUILD_PREFIX/lib/$dylib /opt/arm64-builds/lib - done - fi build_openjpeg + if [ -f /usr/local/lib64/libopenjp2.so ]; then + cp /usr/local/lib64/libopenjp2.so /usr/local/lib + fi ORIGINAL_CFLAGS=$CFLAGS CFLAGS="$CFLAGS -O3 -DNDEBUG" @@ -130,14 +126,19 @@ curl -fsSL -o pillow-depends-main.zip https://github.com/python-pillow/pillow-de untar pillow-depends-main.zip if [[ -n "$IS_MACOS" ]]; then - # webp, libtiff, libxcb cause a conflict with building webp, libtiff, libxcb - # libxdmcp causes an issue on macOS < 11 - # if php is installed, brew tries to reinstall these after installing openblas + # libtiff and libxcb cause a conflict with building libtiff and libxcb + # libxau and libxdmcp cause an issue on macOS < 11 # remove cairo to fix building harfbuzz on arm64 # remove lcms2 and libpng to fix building openjpeg on arm64 - # remove zstd to avoid inclusion on x86_64 + # remove jpeg-turbo to avoid inclusion on arm64 + # remove webp and zstd to avoid inclusion on x86_64 # curl from brew requires zstd, use system curl - brew remove --ignore-dependencies webp libpng libtiff libxcb libxdmcp curl php cairo lcms2 ghostscript zstd + brew remove --ignore-dependencies libpng libtiff libxcb libxau libxdmcp curl cairo lcms2 zstd + if [[ "$CIBW_ARCHS" == "arm64" ]]; then + brew remove --ignore-dependencies jpeg-turbo + else + brew remove --ignore-dependencies webp + fi brew install pkg-config fi diff --git a/.github/workflows/wheels-test.sh b/.github/workflows/wheels-test.sh index 207ec1567..3fbf3be69 100755 --- a/.github/workflows/wheels-test.sh +++ b/.github/workflows/wheels-test.sh @@ -4,6 +4,9 @@ set -e if [[ "$OSTYPE" == "darwin"* ]]; then brew install fribidi export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig" + if [ -f /opt/homebrew/lib/libfribidi.dylib ]; then + sudo cp /opt/homebrew/lib/libfribidi.dylib /usr/local/lib + fi elif [ "${AUDITWHEEL_POLICY::9}" == "musllinux" ]; then apk add curl fribidi else diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 1140aaaad..e4008489b 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -99,7 +99,7 @@ jobs: cibw_arch: x86_64 macosx_deployment_target: "10.10" - name: "macOS arm64" - os: macos-latest + os: macos-14 cibw_arch: arm64 macosx_deployment_target: "11.0" - name: "manylinux2014 and musllinux x86_64" @@ -132,7 +132,7 @@ jobs: CIBW_MANYLINUX_PYPY_X86_64_IMAGE: ${{ matrix.manylinux }} CIBW_MANYLINUX_X86_64_IMAGE: ${{ matrix.manylinux }} CIBW_SKIP: pp38-* - CIBW_TEST_SKIP: "*-macosx_arm64" + CIBW_TEST_SKIP: cp38-macosx_arm64 MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macosx_deployment_target }} - uses: actions/upload-artifact@v4 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6adc75b49..c52fdcb55 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,17 +1,17 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.9 + rev: v0.2.0 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] - repo: https://github.com/psf/black-pre-commit-mirror - rev: 23.12.1 + rev: 24.1.1 hooks: - id: black - repo: https://github.com/PyCQA/bandit - rev: 1.7.6 + rev: 1.7.7 hooks: - id: bandit args: [--severity-level=high] @@ -32,6 +32,7 @@ repos: rev: v4.5.0 hooks: - id: check-executables-have-shebangs + - id: check-shebang-scripts-are-executable - id: check-merge-conflict - id: check-json - id: check-toml @@ -47,12 +48,12 @@ repos: - id: sphinx-lint - repo: https://github.com/tox-dev/pyproject-fmt - rev: 1.5.3 + rev: 1.7.0 hooks: - id: pyproject-fmt - repo: https://github.com/abravalheri/validate-pyproject - rev: v0.15 + rev: v0.16 hooks: - id: validate-pyproject diff --git a/CHANGES.rst b/CHANGES.rst index 62ae2a68b..44dfa65f7 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,33 @@ Changelog (Pillow) 10.3.0 (unreleased) ------------------- +- Open 16-bit grayscale PNGs as I;16 #7849 + [radarhere] + +- Handle truncated chunks at the end of PNG images #7709 + [lajiyuan, radarhere] + +- Match mask size to pasted image size in GifImagePlugin #7779 + [radarhere] + +- Release GIL while calling ``WebPAnimDecoderGetNext`` #7782 + [evanmiller, radarhere] + +- Fixed reading FLI/FLC images with a prefix chunk #7804 + [twolife] + +- Update wl-paste handling and return None for some errors in grabclipboard() on Linux #7745 + [nik012003, radarhere] + +- Remove execute bit from ``setup.py`` #7760 + [hugovk] + +- Do not support using test-image-results to upload images after test failures #7739 + [radarhere] + +- Changed ImageMath.ops to be static #7721 + [radarhere] + - Fix APNG info after seeking backwards more than twice #7701 [esoma, radarhere] diff --git a/README.md b/README.md index 6ca870166..f142ef563 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ As of 2019, Pillow development is src="https://zenodo.org/badge/17549/python-pillow/Pillow.svg"> Tidelift + src="https://tidelift.com/badges/package/pypi/pillow?style=flat"> Newest PyPI version @@ -82,9 +82,6 @@ As of 2019, Pillow development is Join the chat at https://gitter.im/python-pillow/Pillow - Follow on https://twitter.com/PythonPillow Follow on https://fosstodon.org/@pillow None: (w, h) = size for x in range(w): for y in range(h): access[(x, y)] -def iterate_set(size, access): +def iterate_set(size, access) -> None: (w, h) = size for x in range(w): for y in range(h): access[(x, y)] = (x % 256, y % 256, 0) -def timer(func, label, *args): +def timer(func, label, *args) -> None: iterations = 5000 starttime = time.time() for x in range(iterations): @@ -38,7 +38,7 @@ def timer(func, label, *args): ) -def test_direct(): +def test_direct() -> None: im = hopper() im.load() # im = Image.new("RGB", (2000, 2000), (1, 3, 2)) diff --git a/Tests/check_fli_oob.py b/Tests/check_fli_oob.py index ac46ff1eb..e0057a2c2 100644 --- a/Tests/check_fli_oob.py +++ b/Tests/check_fli_oob.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 from __future__ import annotations from PIL import Image diff --git a/Tests/check_imaging_leaks.py b/Tests/check_imaging_leaks.py index 890167039..231789ca0 100755 --- a/Tests/check_imaging_leaks.py +++ b/Tests/check_imaging_leaks.py @@ -23,7 +23,10 @@ def _get_mem_usage() -> float: def _test_leak( - min_iterations: int, max_iterations: int, fn: Callable[..., None], *args: Any + min_iterations: int, + max_iterations: int, + fn: Callable[..., Image.Image | None], + *args: Any, ) -> None: mem_limit = None for i in range(max_iterations): diff --git a/Tests/check_j2k_overflow.py b/Tests/check_j2k_overflow.py index 8a85783fc..dbdd5a4f5 100644 --- a/Tests/check_j2k_overflow.py +++ b/Tests/check_j2k_overflow.py @@ -1,13 +1,13 @@ from __future__ import annotations -from pathlib import PosixPath +from pathlib import Path import pytest from PIL import Image -def test_j2k_overflow(tmp_path: PosixPath) -> None: +def test_j2k_overflow(tmp_path: Path) -> None: im = Image.new("RGBA", (1024, 131584)) target = str(tmp_path / "temp.jpc") with pytest.raises(OSError): diff --git a/Tests/check_jp2_overflow.py b/Tests/check_jp2_overflow.py old mode 100755 new mode 100644 index 5adbb84b6..954d68bf7 --- a/Tests/check_jp2_overflow.py +++ b/Tests/check_jp2_overflow.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # Reproductions/tests for OOB read errors in FliDecode.c # When run in python, all of these images should fail for diff --git a/Tests/check_large_memory.py b/Tests/check_large_memory.py index 2c8c77800..a9ce79e57 100644 --- a/Tests/check_large_memory.py +++ b/Tests/check_large_memory.py @@ -1,7 +1,7 @@ from __future__ import annotations import sys -from pathlib import PosixPath +from pathlib import Path from types import ModuleType import pytest @@ -31,18 +31,18 @@ XDIM = 48000 pytestmark = pytest.mark.skipif(sys.maxsize <= 2**32, reason="requires 64-bit system") -def _write_png(tmp_path: PosixPath, xdim: int, ydim: int) -> None: +def _write_png(tmp_path: Path, xdim: int, ydim: int) -> None: f = str(tmp_path / "temp.png") im = Image.new("L", (xdim, ydim), 0) im.save(f) -def test_large(tmp_path: PosixPath) -> None: +def test_large(tmp_path: Path) -> None: """succeeded prepatch""" _write_png(tmp_path, XDIM, YDIM) -def test_2gpx(tmp_path: PosixPath) -> None: +def test_2gpx(tmp_path: Path) -> None: """failed prepatch""" _write_png(tmp_path, XDIM, XDIM) diff --git a/Tests/check_large_memory_numpy.py b/Tests/check_large_memory_numpy.py index 8609fe6d0..f4ca8d0aa 100644 --- a/Tests/check_large_memory_numpy.py +++ b/Tests/check_large_memory_numpy.py @@ -1,7 +1,7 @@ from __future__ import annotations import sys -from pathlib import PosixPath +from pathlib import Path import pytest @@ -25,7 +25,7 @@ XDIM = 48000 pytestmark = pytest.mark.skipif(sys.maxsize <= 2**32, reason="requires 64-bit system") -def _write_png(tmp_path: PosixPath, xdim: int, ydim: int) -> None: +def _write_png(tmp_path: Path, xdim: int, ydim: int) -> None: dtype = np.uint8 a = np.zeros((xdim, ydim), dtype=dtype) f = str(tmp_path / "temp.png") @@ -33,11 +33,11 @@ def _write_png(tmp_path: PosixPath, xdim: int, ydim: int) -> None: im.save(f) -def test_large(tmp_path: PosixPath) -> None: +def test_large(tmp_path: Path) -> None: """succeeded prepatch""" _write_png(tmp_path, XDIM, YDIM) -def test_2gpx(tmp_path: PosixPath) -> None: +def test_2gpx(tmp_path: Path) -> None: """failed prepatch""" _write_png(tmp_path, XDIM, XDIM) diff --git a/Tests/check_png_dos.py b/Tests/check_png_dos.py index d65ba6abc..63d6657bc 100644 --- a/Tests/check_png_dos.py +++ b/Tests/check_png_dos.py @@ -17,6 +17,7 @@ def test_ignore_dos_text() -> None: finally: ImageFile.LOAD_TRUNCATED_IMAGES = False + assert isinstance(im, PngImagePlugin.PngImageFile) for s in im.text.values(): assert len(s) < 1024 * 1024, "Text chunk larger than 1M" @@ -32,6 +33,7 @@ def test_dos_text() -> None: assert msg, "Decompressed Data Too Large" return + assert isinstance(im, PngImagePlugin.PngImageFile) for s in im.text.values(): assert len(s) < 1024 * 1024, "Text chunk larger than 1M" @@ -57,6 +59,7 @@ def test_dos_total_memory() -> None: return total_len = 0 + assert isinstance(im2, PngImagePlugin.PngImageFile) for txt in im2.text.values(): total_len += len(txt) assert total_len < 64 * 1024 * 1024, "Total text chunks greater than 64M" diff --git a/Tests/helper.py b/Tests/helper.py index 99170c765..9849bf655 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -1,6 +1,7 @@ """ Helper functions. """ + from __future__ import annotations import logging @@ -25,13 +26,6 @@ if os.environ.get("SHOW_ERRORS"): uploader = "show" elif "GITHUB_ACTIONS" in os.environ: uploader = "github_actions" -else: - try: - import test_image_results - - uploader = "aws" - except ImportError: - pass def upload(a: Image.Image, b: Image.Image) -> str | None: @@ -46,8 +40,6 @@ def upload(a: Image.Image, b: Image.Image) -> str | None: a.save(os.path.join(tmpdir, "a.png")) b.save(os.path.join(tmpdir, "b.png")) return tmpdir - elif uploader == "aws": - return test_image_results.upload(a, b) return None @@ -252,7 +244,7 @@ def fromstring(data: bytes) -> Image.Image: return Image.open(BytesIO(data)) -def tostring(im: Image.Image, string_format: str, **options: dict[str, Any]) -> bytes: +def tostring(im: Image.Image, string_format: str, **options: Any) -> bytes: out = BytesIO() im.save(out, string_format, **options) return out.getvalue() @@ -359,7 +351,7 @@ def is_mingw() -> bool: class CachedProperty: - def __init__(self, func: Callable[[Any], None]) -> None: + def __init__(self, func: Callable[[Any], Any]) -> None: self.func = func def __get__(self, instance: Any, cls: type[Any] | None = None) -> Any: diff --git a/Tests/images/16_bit_binary_pgm.png b/Tests/images/16_bit_binary_pgm.png deleted file mode 100644 index 918be1ad4..000000000 Binary files a/Tests/images/16_bit_binary_pgm.png and /dev/null differ diff --git a/Tests/images/16_bit_binary_pgm.tiff b/Tests/images/16_bit_binary_pgm.tiff new file mode 100644 index 000000000..1ce808bcf Binary files /dev/null and b/Tests/images/16_bit_binary_pgm.tiff differ diff --git a/Tests/images/2422.flc b/Tests/images/2422.flc new file mode 100644 index 000000000..eed5fb59e Binary files /dev/null and b/Tests/images/2422.flc differ diff --git a/Tests/images/cmx3g8_wv_1998.260_0745_mcidas.png b/Tests/images/cmx3g8_wv_1998.260_0745_mcidas.png deleted file mode 100644 index 2b84283b7..000000000 Binary files a/Tests/images/cmx3g8_wv_1998.260_0745_mcidas.png and /dev/null differ diff --git a/Tests/images/cmx3g8_wv_1998.260_0745_mcidas.tiff b/Tests/images/cmx3g8_wv_1998.260_0745_mcidas.tiff new file mode 100644 index 000000000..b72509cc4 Binary files /dev/null and b/Tests/images/cmx3g8_wv_1998.260_0745_mcidas.tiff differ diff --git a/Tests/images/create_eps.gnuplot b/Tests/images/create_eps.gnuplot index 4d7e29877..57a3c8c97 100644 --- a/Tests/images/create_eps.gnuplot +++ b/Tests/images/create_eps.gnuplot @@ -1,5 +1,3 @@ -#!/usr/bin/gnuplot - #This is the script that was used to create our sample EPS files #We used the following version of the gnuplot program #G N U P L O T diff --git a/Tests/images/hopper_emboss.bmp b/Tests/images/hopper_emboss.bmp index d8e001e2b..01d48fa3f 100644 Binary files a/Tests/images/hopper_emboss.bmp and b/Tests/images/hopper_emboss.bmp differ diff --git a/Tests/images/hopper_emboss_I.png b/Tests/images/hopper_emboss_I.png deleted file mode 100644 index f4dab388f..000000000 Binary files a/Tests/images/hopper_emboss_I.png and /dev/null differ diff --git a/Tests/images/hopper_emboss_more.bmp b/Tests/images/hopper_emboss_more.bmp index 37a5db830..01247f97e 100644 Binary files a/Tests/images/hopper_emboss_more.bmp and b/Tests/images/hopper_emboss_more.bmp differ diff --git a/Tests/images/hopper_emboss_more_I.png b/Tests/images/hopper_emboss_more_I.png deleted file mode 100644 index c417c915f..000000000 Binary files a/Tests/images/hopper_emboss_more_I.png and /dev/null differ diff --git a/Tests/images/imagedraw_rectangle_I.png b/Tests/images/imagedraw_rectangle_I.png deleted file mode 100644 index a75f12c2e..000000000 Binary files a/Tests/images/imagedraw_rectangle_I.png and /dev/null differ diff --git a/Tests/images/imagedraw_rectangle_I.tiff b/Tests/images/imagedraw_rectangle_I.tiff new file mode 100644 index 000000000..9b9eda883 Binary files /dev/null and b/Tests/images/imagedraw_rectangle_I.tiff differ diff --git a/Tests/images/truncated_end_chunk.png b/Tests/images/truncated_end_chunk.png new file mode 100644 index 000000000..5e88c5e4f Binary files /dev/null and b/Tests/images/truncated_end_chunk.png differ diff --git a/Tests/oss-fuzz/fuzz_pillow.py b/Tests/oss-fuzz/fuzz_pillow.py index e6e99d415..9137391b6 100644 --- a/Tests/oss-fuzz/fuzz_pillow.py +++ b/Tests/oss-fuzz/fuzz_pillow.py @@ -1,5 +1,3 @@ -#!/usr/bin/python3 - # Copyright 2020 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/Tests/test_000_sanity.py b/Tests/test_000_sanity.py index f64216bca..c3926250f 100644 --- a/Tests/test_000_sanity.py +++ b/Tests/test_000_sanity.py @@ -3,7 +3,7 @@ from __future__ import annotations from PIL import Image -def test_sanity(): +def test_sanity() -> None: # Make sure we have the binary extension Image.core.new("L", (100, 100)) diff --git a/Tests/test_binary.py b/Tests/test_binary.py index 41fb93fcf..d19799a09 100644 --- a/Tests/test_binary.py +++ b/Tests/test_binary.py @@ -3,12 +3,12 @@ from __future__ import annotations from PIL import _binary -def test_standard(): +def test_standard() -> None: assert _binary.i8(b"*") == 42 assert _binary.o8(42) == b"*" -def test_little_endian(): +def test_little_endian() -> None: assert _binary.i16le(b"\xff\xff\x00\x00") == 65535 assert _binary.i32le(b"\xff\xff\x00\x00") == 65535 @@ -16,7 +16,7 @@ def test_little_endian(): assert _binary.o32le(65535) == b"\xff\xff\x00\x00" -def test_big_endian(): +def test_big_endian() -> None: assert _binary.i16be(b"\x00\x00\xff\xff") == 0 assert _binary.i32be(b"\x00\x00\xff\xff") == 65535 diff --git a/Tests/test_bmp_reference.py b/Tests/test_bmp_reference.py index 0da41e858..0ad496135 100644 --- a/Tests/test_bmp_reference.py +++ b/Tests/test_bmp_reference.py @@ -10,13 +10,13 @@ from .helper import assert_image_similar base = os.path.join("Tests", "images", "bmp") -def get_files(d, ext=".bmp"): +def get_files(d: str, ext: str = ".bmp") -> list[str]: return [ os.path.join(base, d, f) for f in os.listdir(os.path.join(base, d)) if ext in f ] -def test_bad(): +def test_bad() -> None: """These shouldn't crash/dos, but they shouldn't return anything either""" for f in get_files("b"): @@ -29,7 +29,7 @@ def test_bad(): pass -def test_questionable(): +def test_questionable() -> None: """These shouldn't crash/dos, but it's not well defined that these are in spec""" supported = [ @@ -56,7 +56,7 @@ def test_questionable(): raise -def test_good(): +def test_good() -> None: """These should all work. There's a set of target files in the html directory that we can compare against.""" @@ -80,7 +80,7 @@ def test_good(): "rgb32bf.bmp": "rgb24.png", } - def get_compare(f): + def get_compare(f: str) -> str: name = os.path.split(f)[1] if name in file_map: return os.path.join(base, "html", file_map[name]) diff --git a/Tests/test_box_blur.py b/Tests/test_box_blur.py index 461e6aaac..1f6ed6127 100644 --- a/Tests/test_box_blur.py +++ b/Tests/test_box_blur.py @@ -16,18 +16,18 @@ sample.putdata(sum([ # fmt: on -def test_imageops_box_blur(): +def test_imageops_box_blur() -> None: i = sample.filter(ImageFilter.BoxBlur(1)) assert i.mode == sample.mode assert i.size == sample.size assert isinstance(i, Image.Image) -def box_blur(image, radius=1, n=1): +def box_blur(image: Image.Image, radius: float = 1, n: int = 1) -> Image.Image: return image._new(image.im.box_blur((radius, radius), n)) -def assert_image(im, data, delta=0): +def assert_image(im: Image.Image, data: list[list[int]], delta: int = 0) -> None: it = iter(im.getdata()) for data_row in data: im_row = [next(it) for _ in range(im.size[0])] @@ -37,7 +37,13 @@ def assert_image(im, data, delta=0): next(it) -def assert_blur(im, radius, data, passes=1, delta=0): +def assert_blur( + im: Image.Image, + radius: float, + data: list[list[int]], + passes: int = 1, + delta: int = 0, +) -> None: # check grayscale image assert_image(box_blur(im, radius, passes), data, delta) rgba = Image.merge("RGBA", (im, im, im, im)) @@ -45,7 +51,7 @@ def assert_blur(im, radius, data, passes=1, delta=0): assert_image(band, data, delta) -def test_color_modes(): +def test_color_modes() -> None: with pytest.raises(ValueError): box_blur(sample.convert("1")) with pytest.raises(ValueError): @@ -65,7 +71,7 @@ def test_color_modes(): box_blur(sample.convert("YCbCr")) -def test_radius_0(): +def test_radius_0() -> None: assert_blur( sample, 0, @@ -81,7 +87,7 @@ def test_radius_0(): ) -def test_radius_0_02(): +def test_radius_0_02() -> None: assert_blur( sample, 0.02, @@ -98,7 +104,7 @@ def test_radius_0_02(): ) -def test_radius_0_05(): +def test_radius_0_05() -> None: assert_blur( sample, 0.05, @@ -115,7 +121,7 @@ def test_radius_0_05(): ) -def test_radius_0_1(): +def test_radius_0_1() -> None: assert_blur( sample, 0.1, @@ -132,7 +138,7 @@ def test_radius_0_1(): ) -def test_radius_0_5(): +def test_radius_0_5() -> None: assert_blur( sample, 0.5, @@ -149,7 +155,7 @@ def test_radius_0_5(): ) -def test_radius_1(): +def test_radius_1() -> None: assert_blur( sample, 1, @@ -166,7 +172,7 @@ def test_radius_1(): ) -def test_radius_1_5(): +def test_radius_1_5() -> None: assert_blur( sample, 1.5, @@ -183,7 +189,7 @@ def test_radius_1_5(): ) -def test_radius_bigger_then_half(): +def test_radius_bigger_then_half() -> None: assert_blur( sample, 3, @@ -200,7 +206,7 @@ def test_radius_bigger_then_half(): ) -def test_radius_bigger_then_width(): +def test_radius_bigger_then_width() -> None: assert_blur( sample, 10, @@ -215,7 +221,7 @@ def test_radius_bigger_then_width(): ) -def test_extreme_large_radius(): +def test_extreme_large_radius() -> None: assert_blur( sample, 600, @@ -230,7 +236,7 @@ def test_extreme_large_radius(): ) -def test_two_passes(): +def test_two_passes() -> None: assert_blur( sample, 1, @@ -248,7 +254,7 @@ def test_two_passes(): ) -def test_three_passes(): +def test_three_passes() -> None: assert_blur( sample, 1, diff --git a/Tests/test_color_lut.py b/Tests/test_color_lut.py index fcd1169ef..c8886a779 100644 --- a/Tests/test_color_lut.py +++ b/Tests/test_color_lut.py @@ -1,6 +1,7 @@ from __future__ import annotations from array import array +from types import ModuleType import pytest @@ -8,6 +9,7 @@ from PIL import Image, ImageFilter from .helper import assert_image_equal +numpy: ModuleType | None try: import numpy except ImportError: @@ -15,7 +17,9 @@ except ImportError: class TestColorLut3DCoreAPI: - def generate_identity_table(self, channels, size): + def generate_identity_table( + self, channels: int, size: int | tuple[int, int, int] + ) -> tuple[int, int, int, int, list[float]]: if isinstance(size, tuple): size_1d, size_2d, size_3d = size else: @@ -41,7 +45,7 @@ class TestColorLut3DCoreAPI: [item for sublist in table for item in sublist], ) - def test_wrong_args(self): + def test_wrong_args(self) -> None: im = Image.new("RGB", (10, 10), 0) with pytest.raises(ValueError, match="filter"): @@ -101,7 +105,7 @@ class TestColorLut3DCoreAPI: with pytest.raises(TypeError): im.im.color_lut_3d("RGB", Image.Resampling.BILINEAR, 3, 2, 2, 2, 16) - def test_correct_args(self): + def test_correct_args(self) -> None: im = Image.new("RGB", (10, 10), 0) im.im.color_lut_3d( @@ -136,7 +140,7 @@ class TestColorLut3DCoreAPI: *self.generate_identity_table(3, (3, 3, 65)), ) - def test_wrong_mode(self): + def test_wrong_mode(self) -> None: with pytest.raises(ValueError, match="wrong mode"): im = Image.new("L", (10, 10), 0) im.im.color_lut_3d( @@ -167,7 +171,7 @@ class TestColorLut3DCoreAPI: "RGB", Image.Resampling.BILINEAR, *self.generate_identity_table(4, 3) ) - def test_correct_mode(self): + def test_correct_mode(self) -> None: im = Image.new("RGBA", (10, 10), 0) im.im.color_lut_3d( "RGBA", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3) @@ -188,7 +192,7 @@ class TestColorLut3DCoreAPI: "RGBA", Image.Resampling.BILINEAR, *self.generate_identity_table(4, 3) ) - def test_identities(self): + def test_identities(self) -> None: g = Image.linear_gradient("L") im = Image.merge( "RGB", @@ -224,7 +228,7 @@ class TestColorLut3DCoreAPI: ), ) - def test_identities_4_channels(self): + def test_identities_4_channels(self) -> None: g = Image.linear_gradient("L") im = Image.merge( "RGB", @@ -247,7 +251,7 @@ class TestColorLut3DCoreAPI: ), ) - def test_copy_alpha_channel(self): + def test_copy_alpha_channel(self) -> None: g = Image.linear_gradient("L") im = Image.merge( "RGBA", @@ -270,7 +274,7 @@ class TestColorLut3DCoreAPI: ), ) - def test_channels_order(self): + def test_channels_order(self) -> None: g = Image.linear_gradient("L") im = Image.merge( "RGB", @@ -295,7 +299,7 @@ class TestColorLut3DCoreAPI: ]))) # fmt: on - def test_overflow(self): + def test_overflow(self) -> None: g = Image.linear_gradient("L") im = Image.merge( "RGB", @@ -348,7 +352,7 @@ class TestColorLut3DCoreAPI: class TestColorLut3DFilter: - def test_wrong_args(self): + def test_wrong_args(self) -> None: with pytest.raises(ValueError, match="should be either an integer"): ImageFilter.Color3DLUT("small", [1]) @@ -376,7 +380,7 @@ class TestColorLut3DFilter: with pytest.raises(ValueError, match="Only 3 or 4 output"): ImageFilter.Color3DLUT((2, 2, 2), [[1, 1]] * 8, channels=2) - def test_convert_table(self): + def test_convert_table(self) -> None: lut = ImageFilter.Color3DLUT(2, [0, 1, 2] * 8) assert tuple(lut.size) == (2, 2, 2) assert lut.name == "Color 3D LUT" @@ -394,7 +398,8 @@ class TestColorLut3DFilter: assert lut.table == list(range(4)) * 8 @pytest.mark.skipif(numpy is None, reason="NumPy not installed") - def test_numpy_sources(self): + def test_numpy_sources(self) -> None: + assert numpy is not None table = numpy.ones((5, 6, 7, 3), dtype=numpy.float16) with pytest.raises(ValueError, match="should have either channels"): lut = ImageFilter.Color3DLUT((5, 6, 7), table) @@ -427,7 +432,8 @@ class TestColorLut3DFilter: assert lut.table[0] == 33 @pytest.mark.skipif(numpy is None, reason="NumPy not installed") - def test_numpy_formats(self): + def test_numpy_formats(self) -> None: + assert numpy is not None g = Image.linear_gradient("L") im = Image.merge( "RGB", @@ -466,7 +472,7 @@ class TestColorLut3DFilter: lut.table = numpy.array(lut.table, dtype=numpy.int8) im.filter(lut) - def test_repr(self): + def test_repr(self) -> None: lut = ImageFilter.Color3DLUT(2, [0, 1, 2] * 8) assert repr(lut) == "" @@ -484,7 +490,7 @@ class TestColorLut3DFilter: class TestGenerateColorLut3D: - def test_wrong_channels_count(self): + def test_wrong_channels_count(self) -> None: with pytest.raises(ValueError, match="3 or 4 output channels"): ImageFilter.Color3DLUT.generate( 5, channels=2, callback=lambda r, g, b: (r, g, b) @@ -498,7 +504,7 @@ class TestGenerateColorLut3D: 5, channels=4, callback=lambda r, g, b: (r, g, b) ) - def test_3_channels(self): + def test_3_channels(self) -> None: lut = ImageFilter.Color3DLUT.generate(5, lambda r, g, b: (r, g, b)) assert tuple(lut.size) == (5, 5, 5) assert lut.name == "Color 3D LUT" @@ -508,7 +514,7 @@ class TestGenerateColorLut3D: 1.0, 0.0, 0.0, 0.0, 0.25, 0.0, 0.25, 0.25, 0.0, 0.5, 0.25, 0.0] # fmt: on - def test_4_channels(self): + def test_4_channels(self) -> None: lut = ImageFilter.Color3DLUT.generate( 5, channels=4, callback=lambda r, g, b: (b, r, g, (r + g + b) / 2) ) @@ -521,7 +527,7 @@ class TestGenerateColorLut3D: ] # fmt: on - def test_apply(self): + def test_apply(self) -> None: lut = ImageFilter.Color3DLUT.generate(5, lambda r, g, b: (r, g, b)) g = Image.linear_gradient("L") @@ -537,7 +543,7 @@ class TestGenerateColorLut3D: class TestTransformColorLut3D: - def test_wrong_args(self): + def test_wrong_args(self) -> None: source = ImageFilter.Color3DLUT.generate(5, lambda r, g, b: (r, g, b)) with pytest.raises(ValueError, match="Only 3 or 4 output"): @@ -552,7 +558,7 @@ class TestTransformColorLut3D: with pytest.raises(TypeError): source.transform(lambda r, g, b, a: (r, g, b)) - def test_target_mode(self): + def test_target_mode(self) -> None: source = ImageFilter.Color3DLUT.generate( 2, lambda r, g, b: (r, g, b), target_mode="HSV" ) @@ -563,7 +569,7 @@ class TestTransformColorLut3D: lut = source.transform(lambda r, g, b: (r, g, b), target_mode="RGB") assert lut.mode == "RGB" - def test_3_to_3_channels(self): + def test_3_to_3_channels(self) -> None: source = ImageFilter.Color3DLUT.generate((3, 4, 5), lambda r, g, b: (r, g, b)) lut = source.transform(lambda r, g, b: (r * r, g * g, b * b)) assert tuple(lut.size) == tuple(source.size) @@ -571,7 +577,7 @@ class TestTransformColorLut3D: assert lut.table != source.table assert lut.table[:10] == [0.0, 0.0, 0.0, 0.25, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0] - def test_3_to_4_channels(self): + def test_3_to_4_channels(self) -> None: source = ImageFilter.Color3DLUT.generate((6, 5, 4), lambda r, g, b: (r, g, b)) lut = source.transform(lambda r, g, b: (r * r, g * g, b * b, 1), channels=4) assert tuple(lut.size) == tuple(source.size) @@ -583,7 +589,7 @@ class TestTransformColorLut3D: 0.4**2, 0.0, 0.0, 1, 0.6**2, 0.0, 0.0, 1] # fmt: on - def test_4_to_3_channels(self): + def test_4_to_3_channels(self) -> None: source = ImageFilter.Color3DLUT.generate( (3, 6, 5), lambda r, g, b: (r, g, b, 1), channels=4 ) @@ -599,7 +605,7 @@ class TestTransformColorLut3D: 1.0, 0.96, 1.0, 0.75, 0.96, 1.0, 0.0, 0.96, 1.0] # fmt: on - def test_4_to_4_channels(self): + def test_4_to_4_channels(self) -> None: source = ImageFilter.Color3DLUT.generate( (6, 5, 4), lambda r, g, b: (r, g, b, 1), channels=4 ) @@ -613,7 +619,7 @@ class TestTransformColorLut3D: 0.4**2, 0.0, 0.0, 0.5, 0.6**2, 0.0, 0.0, 0.5] # fmt: on - def test_with_normals_3_channels(self): + def test_with_normals_3_channels(self) -> None: source = ImageFilter.Color3DLUT.generate( (6, 5, 4), lambda r, g, b: (r * r, g * g, b * b) ) @@ -629,7 +635,7 @@ class TestTransformColorLut3D: 0.24, 0.0, 0.0, 0.8 - (0.8**2), 0, 0, 0, 0, 0] # fmt: on - def test_with_normals_4_channels(self): + def test_with_normals_4_channels(self) -> None: source = ImageFilter.Color3DLUT.generate( (3, 6, 5), lambda r, g, b: (r * r, g * g, b * b, 1), channels=4 ) diff --git a/Tests/test_core_resources.py b/Tests/test_core_resources.py index d3f76fdb1..2c1de8bc3 100644 --- a/Tests/test_core_resources.py +++ b/Tests/test_core_resources.py @@ -9,7 +9,7 @@ from PIL import Image from .helper import is_pypy -def test_get_stats(): +def test_get_stats() -> None: # Create at least one image Image.new("RGB", (10, 10)) @@ -22,7 +22,7 @@ def test_get_stats(): assert "blocks_cached" in stats -def test_reset_stats(): +def test_reset_stats() -> None: Image.core.reset_stats() stats = Image.core.get_stats() @@ -35,19 +35,19 @@ def test_reset_stats(): class TestCoreMemory: - def teardown_method(self): + def teardown_method(self) -> None: # Restore default values Image.core.set_alignment(1) Image.core.set_block_size(1024 * 1024) Image.core.set_blocks_max(0) Image.core.clear_cache() - def test_get_alignment(self): + def test_get_alignment(self) -> None: alignment = Image.core.get_alignment() assert alignment > 0 - def test_set_alignment(self): + def test_set_alignment(self) -> None: for i in [1, 2, 4, 8, 16, 32]: Image.core.set_alignment(i) alignment = Image.core.get_alignment() @@ -63,12 +63,12 @@ class TestCoreMemory: with pytest.raises(ValueError): Image.core.set_alignment(3) - def test_get_block_size(self): + def test_get_block_size(self) -> None: block_size = Image.core.get_block_size() assert block_size >= 4096 - def test_set_block_size(self): + def test_set_block_size(self) -> None: for i in [4096, 2 * 4096, 3 * 4096]: Image.core.set_block_size(i) block_size = Image.core.get_block_size() @@ -84,7 +84,7 @@ class TestCoreMemory: with pytest.raises(ValueError): Image.core.set_block_size(4000) - def test_set_block_size_stats(self): + def test_set_block_size_stats(self) -> None: Image.core.reset_stats() Image.core.set_blocks_max(0) Image.core.set_block_size(4096) @@ -96,12 +96,12 @@ class TestCoreMemory: if not is_pypy(): assert stats["freed_blocks"] >= 64 - def test_get_blocks_max(self): + def test_get_blocks_max(self) -> None: blocks_max = Image.core.get_blocks_max() assert blocks_max >= 0 - def test_set_blocks_max(self): + def test_set_blocks_max(self) -> None: for i in [0, 1, 10]: Image.core.set_blocks_max(i) blocks_max = Image.core.get_blocks_max() @@ -117,7 +117,7 @@ class TestCoreMemory: Image.core.set_blocks_max(2**29) @pytest.mark.skipif(is_pypy(), reason="Images not collected") - def test_set_blocks_max_stats(self): + def test_set_blocks_max_stats(self) -> None: Image.core.reset_stats() Image.core.set_blocks_max(128) Image.core.set_block_size(4096) @@ -132,7 +132,7 @@ class TestCoreMemory: assert stats["blocks_cached"] == 64 @pytest.mark.skipif(is_pypy(), reason="Images not collected") - def test_clear_cache_stats(self): + def test_clear_cache_stats(self) -> None: Image.core.reset_stats() Image.core.clear_cache() Image.core.set_blocks_max(128) @@ -149,7 +149,7 @@ class TestCoreMemory: assert stats["freed_blocks"] >= 48 assert stats["blocks_cached"] == 16 - def test_large_images(self): + def test_large_images(self) -> None: Image.core.reset_stats() Image.core.set_blocks_max(0) Image.core.set_block_size(4096) @@ -166,14 +166,14 @@ class TestCoreMemory: class TestEnvVars: - def teardown_method(self): + def teardown_method(self) -> None: # Restore default values Image.core.set_alignment(1) Image.core.set_block_size(1024 * 1024) Image.core.set_blocks_max(0) Image.core.clear_cache() - def test_units(self): + def test_units(self) -> None: Image._apply_env_variables({"PILLOW_BLOCKS_MAX": "2K"}) assert Image.core.get_blocks_max() == 2 * 1024 Image._apply_env_variables({"PILLOW_BLOCK_SIZE": "2m"}) @@ -187,6 +187,6 @@ class TestEnvVars: {"PILLOW_BLOCKS_MAX": "wat"}, ), ) - def test_warnings(self, var): + def test_warnings(self, var: dict[str, str]) -> None: with pytest.warns(UserWarning): Image._apply_env_variables(var) diff --git a/Tests/test_decompression_bomb.py b/Tests/test_decompression_bomb.py index d3049eff1..9c21efa45 100644 --- a/Tests/test_decompression_bomb.py +++ b/Tests/test_decompression_bomb.py @@ -12,16 +12,16 @@ ORIGINAL_LIMIT = Image.MAX_IMAGE_PIXELS class TestDecompressionBomb: - def teardown_method(self, method): + def teardown_method(self, method) -> None: Image.MAX_IMAGE_PIXELS = ORIGINAL_LIMIT - def test_no_warning_small_file(self): + def test_no_warning_small_file(self) -> None: # Implicit assert: no warning. # A warning would cause a failure. with Image.open(TEST_FILE): pass - def test_no_warning_no_limit(self): + def test_no_warning_no_limit(self) -> None: # Arrange # Turn limit off Image.MAX_IMAGE_PIXELS = None @@ -33,7 +33,7 @@ class TestDecompressionBomb: with Image.open(TEST_FILE): pass - def test_warning(self): + def test_warning(self) -> None: # Set limit to trigger warning on the test file Image.MAX_IMAGE_PIXELS = 128 * 128 - 1 assert Image.MAX_IMAGE_PIXELS == 128 * 128 - 1 @@ -42,7 +42,7 @@ class TestDecompressionBomb: with Image.open(TEST_FILE): pass - def test_exception(self): + def test_exception(self) -> None: # Set limit to trigger exception on the test file Image.MAX_IMAGE_PIXELS = 64 * 128 - 1 assert Image.MAX_IMAGE_PIXELS == 64 * 128 - 1 @@ -51,22 +51,22 @@ class TestDecompressionBomb: with Image.open(TEST_FILE): pass - def test_exception_ico(self): + def test_exception_ico(self) -> None: with pytest.raises(Image.DecompressionBombError): with Image.open("Tests/images/decompression_bomb.ico"): pass - def test_exception_gif(self): + def test_exception_gif(self) -> None: with pytest.raises(Image.DecompressionBombError): with Image.open("Tests/images/decompression_bomb.gif"): pass - def test_exception_gif_extents(self): + def test_exception_gif_extents(self) -> None: with Image.open("Tests/images/decompression_bomb_extents.gif") as im: with pytest.raises(Image.DecompressionBombError): im.seek(1) - def test_exception_gif_zero_width(self): + def test_exception_gif_zero_width(self) -> None: # Set limit to trigger exception on the test file Image.MAX_IMAGE_PIXELS = 4 * 64 * 128 assert Image.MAX_IMAGE_PIXELS == 4 * 64 * 128 @@ -75,7 +75,7 @@ class TestDecompressionBomb: with Image.open("Tests/images/zero_width.gif"): pass - def test_exception_bmp(self): + def test_exception_bmp(self) -> None: with pytest.raises(Image.DecompressionBombError): with Image.open("Tests/images/bmp/b/reallybig.bmp"): pass @@ -83,15 +83,15 @@ class TestDecompressionBomb: class TestDecompressionCrop: @classmethod - def setup_class(cls): + def setup_class(cls) -> None: width, height = 128, 128 Image.MAX_IMAGE_PIXELS = height * width * 4 - 1 @classmethod - def teardown_class(cls): + def teardown_class(cls) -> None: Image.MAX_IMAGE_PIXELS = ORIGINAL_LIMIT - def test_enlarge_crop(self): + def test_enlarge_crop(self) -> None: # Crops can extend the extents, therefore we should have the # same decompression bomb warnings on them. with hopper() as src: @@ -99,7 +99,7 @@ class TestDecompressionCrop: with pytest.warns(Image.DecompressionBombWarning): src.crop(box) - def test_crop_decompression_checks(self): + def test_crop_decompression_checks(self) -> None: im = Image.new("RGB", (100, 100)) for value in ((-9999, -9999, -9990, -9990), (-999, -999, -990, -990)): diff --git a/Tests/test_deprecate.py b/Tests/test_deprecate.py index 6c7f509a7..584d8f91d 100644 --- a/Tests/test_deprecate.py +++ b/Tests/test_deprecate.py @@ -20,12 +20,12 @@ from PIL import _deprecate ), ], ) -def test_version(version, expected): +def test_version(version: int | None, expected: str) -> None: with pytest.warns(DeprecationWarning, match=expected): _deprecate.deprecate("Old thing", version, "new thing") -def test_unknown_version(): +def test_unknown_version() -> None: expected = r"Unknown removal version: 12345. Update PIL\._deprecate\?" with pytest.raises(ValueError, match=expected): _deprecate.deprecate("Old thing", 12345, "new thing") @@ -46,13 +46,13 @@ def test_unknown_version(): ), ], ) -def test_old_version(deprecated, plural, expected): +def test_old_version(deprecated: str, plural: bool, expected: str) -> None: expected = r"" with pytest.raises(RuntimeError, match=expected): _deprecate.deprecate(deprecated, 1, plural=plural) -def test_plural(): +def test_plural() -> None: expected = ( r"Old things are deprecated and will be removed in Pillow 11 \(2024-10-15\)\. " r"Use new thing instead\." @@ -61,7 +61,7 @@ def test_plural(): _deprecate.deprecate("Old things", 11, "new thing", plural=True) -def test_replacement_and_action(): +def test_replacement_and_action() -> None: expected = "Use only one of 'replacement' and 'action'" with pytest.raises(ValueError, match=expected): _deprecate.deprecate( @@ -76,7 +76,7 @@ def test_replacement_and_action(): "Upgrade to new thing.", ], ) -def test_action(action): +def test_action(action: str) -> None: expected = ( r"Old thing is deprecated and will be removed in Pillow 11 \(2024-10-15\)\. " r"Upgrade to new thing\." @@ -85,7 +85,7 @@ def test_action(action): _deprecate.deprecate("Old thing", 11, action=action) -def test_no_replacement_or_action(): +def test_no_replacement_or_action() -> None: expected = ( r"Old thing is deprecated and will be removed in Pillow 11 \(2024-10-15\)" ) diff --git a/Tests/test_features.py b/Tests/test_features.py index b90c1d25f..8d2d198ff 100644 --- a/Tests/test_features.py +++ b/Tests/test_features.py @@ -2,6 +2,7 @@ from __future__ import annotations import io import re +from typing import Callable import pytest @@ -15,7 +16,7 @@ except ImportError: pass -def test_check(): +def test_check() -> None: # Check the correctness of the convenience function for module in features.modules: assert features.check_module(module) == features.check(module) @@ -25,11 +26,11 @@ def test_check(): assert features.check_feature(feature) == features.check(feature) -def test_version(): +def test_version() -> None: # Check the correctness of the convenience function # and the format of version numbers - def test(name, function): + def test(name: str, function: Callable[[str], bool]) -> None: version = features.version(name) if not features.check(name): assert version is None @@ -47,56 +48,56 @@ def test_version(): @skip_unless_feature("webp") -def test_webp_transparency(): +def test_webp_transparency() -> None: assert features.check("transp_webp") != _webp.WebPDecoderBuggyAlpha() assert features.check("transp_webp") == _webp.HAVE_TRANSPARENCY @skip_unless_feature("webp") -def test_webp_mux(): +def test_webp_mux() -> None: assert features.check("webp_mux") == _webp.HAVE_WEBPMUX @skip_unless_feature("webp") -def test_webp_anim(): +def test_webp_anim() -> None: assert features.check("webp_anim") == _webp.HAVE_WEBPANIM @skip_unless_feature("libjpeg_turbo") -def test_libjpeg_turbo_version(): +def test_libjpeg_turbo_version() -> None: assert re.search(r"\d+\.\d+\.\d+$", features.version("libjpeg_turbo")) @skip_unless_feature("libimagequant") -def test_libimagequant_version(): +def test_libimagequant_version() -> None: assert re.search(r"\d+\.\d+\.\d+$", features.version("libimagequant")) @pytest.mark.parametrize("feature", features.modules) -def test_check_modules(feature): +def test_check_modules(feature: str) -> None: assert features.check_module(feature) in [True, False] @pytest.mark.parametrize("feature", features.codecs) -def test_check_codecs(feature): +def test_check_codecs(feature: str) -> None: assert features.check_codec(feature) in [True, False] -def test_check_warns_on_nonexistent(): +def test_check_warns_on_nonexistent() -> None: with pytest.warns(UserWarning) as cm: has_feature = features.check("typo") assert has_feature is False assert str(cm[-1].message) == "Unknown feature 'typo'." -def test_supported_modules(): +def test_supported_modules() -> None: assert isinstance(features.get_supported_modules(), list) assert isinstance(features.get_supported_codecs(), list) assert isinstance(features.get_supported_features(), list) assert isinstance(features.get_supported(), list) -def test_unsupported_codec(): +def test_unsupported_codec() -> None: # Arrange codec = "unsupported_codec" # Act / Assert @@ -106,7 +107,7 @@ def test_unsupported_codec(): features.version_codec(codec) -def test_unsupported_module(): +def test_unsupported_module() -> None: # Arrange module = "unsupported_module" # Act / Assert @@ -116,7 +117,7 @@ def test_unsupported_module(): features.version_module(module) -def test_pilinfo(): +def test_pilinfo() -> None: buf = io.StringIO() features.pilinfo(buf) out = buf.getvalue() diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py index 23263b5d4..395165b36 100644 --- a/Tests/test_file_apng.py +++ b/Tests/test_file_apng.py @@ -1,5 +1,7 @@ from __future__ import annotations +from pathlib import Path + import pytest from PIL import Image, ImageSequence, PngImagePlugin @@ -8,7 +10,7 @@ from PIL import Image, ImageSequence, PngImagePlugin # APNG browser support tests and fixtures via: # https://philip.html5.org/tests/apng/tests.html # (referenced from https://wiki.mozilla.org/APNG_Specification) -def test_apng_basic(): +def test_apng_basic() -> None: with Image.open("Tests/images/apng/single_frame.png") as im: assert not im.is_animated assert im.n_frames == 1 @@ -45,14 +47,14 @@ def test_apng_basic(): "filename", ("Tests/images/apng/split_fdat.png", "Tests/images/apng/split_fdat_zero_chunk.png"), ) -def test_apng_fdat(filename): +def test_apng_fdat(filename: str) -> None: with Image.open(filename) as im: im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (0, 255, 0, 255) assert im.getpixel((64, 32)) == (0, 255, 0, 255) -def test_apng_dispose(): +def test_apng_dispose() -> None: with Image.open("Tests/images/apng/dispose_op_none.png") as im: im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (0, 255, 0, 255) @@ -84,7 +86,7 @@ def test_apng_dispose(): assert im.getpixel((64, 32)) == (0, 0, 0, 0) -def test_apng_dispose_region(): +def test_apng_dispose_region() -> None: with Image.open("Tests/images/apng/dispose_op_none_region.png") as im: im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (0, 255, 0, 255) @@ -106,7 +108,7 @@ def test_apng_dispose_region(): assert im.getpixel((64, 32)) == (0, 255, 0, 255) -def test_apng_dispose_op_previous_frame(): +def test_apng_dispose_op_previous_frame() -> None: # Test that the dispose settings being used are from the previous frame # # Image created with: @@ -131,14 +133,14 @@ def test_apng_dispose_op_previous_frame(): assert im.getpixel((0, 0)) == (255, 0, 0, 255) -def test_apng_dispose_op_background_p_mode(): +def test_apng_dispose_op_background_p_mode() -> None: with Image.open("Tests/images/apng/dispose_op_background_p_mode.png") as im: im.seek(1) im.load() assert im.size == (128, 64) -def test_apng_blend(): +def test_apng_blend() -> None: with Image.open("Tests/images/apng/blend_op_source_solid.png") as im: im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (0, 255, 0, 255) @@ -165,20 +167,20 @@ def test_apng_blend(): assert im.getpixel((64, 32)) == (0, 255, 0, 255) -def test_apng_blend_transparency(): +def test_apng_blend_transparency() -> None: with Image.open("Tests/images/blend_transparency.png") as im: im.seek(1) assert im.getpixel((0, 0)) == (255, 0, 0) -def test_apng_chunk_order(): +def test_apng_chunk_order() -> None: with Image.open("Tests/images/apng/fctl_actl.png") as im: im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (0, 255, 0, 255) assert im.getpixel((64, 32)) == (0, 255, 0, 255) -def test_apng_delay(): +def test_apng_delay() -> None: with Image.open("Tests/images/apng/delay.png") as im: im.seek(1) assert im.info.get("duration") == 500.0 @@ -218,7 +220,7 @@ def test_apng_delay(): assert im.info.get("duration") == 1000.0 -def test_apng_num_plays(): +def test_apng_num_plays() -> None: with Image.open("Tests/images/apng/num_plays.png") as im: assert im.info.get("loop") == 0 @@ -226,7 +228,7 @@ def test_apng_num_plays(): assert im.info.get("loop") == 1 -def test_apng_mode(): +def test_apng_mode() -> None: with Image.open("Tests/images/apng/mode_16bit.png") as im: assert im.mode == "RGBA" im.seek(im.n_frames - 1) @@ -267,7 +269,7 @@ def test_apng_mode(): assert im.getpixel((64, 32)) == (0, 0, 255, 128) -def test_apng_chunk_errors(): +def test_apng_chunk_errors() -> None: with Image.open("Tests/images/apng/chunk_no_actl.png") as im: assert not im.is_animated @@ -292,7 +294,7 @@ def test_apng_chunk_errors(): im.seek(im.n_frames - 1) -def test_apng_syntax_errors(): +def test_apng_syntax_errors() -> None: with pytest.warns(UserWarning): with Image.open("Tests/images/apng/syntax_num_frames_zero.png") as im: assert not im.is_animated @@ -336,14 +338,14 @@ def test_apng_syntax_errors(): "sequence_fdat_fctl.png", ), ) -def test_apng_sequence_errors(test_file): +def test_apng_sequence_errors(test_file: str) -> None: with pytest.raises(SyntaxError): with Image.open(f"Tests/images/apng/{test_file}") as im: im.seek(im.n_frames - 1) im.load() -def test_apng_save(tmp_path): +def test_apng_save(tmp_path: Path) -> None: with Image.open("Tests/images/apng/single_frame.png") as im: test_file = str(tmp_path / "temp.png") im.save(test_file, save_all=True) @@ -374,7 +376,7 @@ def test_apng_save(tmp_path): assert im.getpixel((64, 32)) == (0, 255, 0, 255) -def test_apng_save_alpha(tmp_path): +def test_apng_save_alpha(tmp_path: Path) -> None: test_file = str(tmp_path / "temp.png") im = Image.new("RGBA", (1, 1), (255, 0, 0, 255)) @@ -388,7 +390,7 @@ def test_apng_save_alpha(tmp_path): assert reloaded.getpixel((0, 0)) == (255, 0, 0, 127) -def test_apng_save_split_fdat(tmp_path): +def test_apng_save_split_fdat(tmp_path: Path) -> None: # test to make sure we do not generate sequence errors when writing # frames with image data spanning multiple fdAT chunks (in this case # both the default image and first animation frame will span multiple @@ -412,7 +414,7 @@ def test_apng_save_split_fdat(tmp_path): assert exception is None -def test_apng_save_duration_loop(tmp_path): +def test_apng_save_duration_loop(tmp_path: Path) -> None: test_file = str(tmp_path / "temp.png") with Image.open("Tests/images/apng/delay.png") as im: frames = [] @@ -475,7 +477,7 @@ def test_apng_save_duration_loop(tmp_path): assert im.info["duration"] == 600 -def test_apng_save_disposal(tmp_path): +def test_apng_save_disposal(tmp_path: Path) -> None: test_file = str(tmp_path / "temp.png") size = (128, 64) red = Image.new("RGBA", size, (255, 0, 0, 255)) @@ -576,7 +578,7 @@ def test_apng_save_disposal(tmp_path): assert im.getpixel((64, 32)) == (0, 0, 0, 0) -def test_apng_save_disposal_previous(tmp_path): +def test_apng_save_disposal_previous(tmp_path: Path) -> None: test_file = str(tmp_path / "temp.png") size = (128, 64) blue = Image.new("RGBA", size, (0, 0, 255, 255)) @@ -598,7 +600,7 @@ def test_apng_save_disposal_previous(tmp_path): assert im.getpixel((64, 32)) == (0, 255, 0, 255) -def test_apng_save_blend(tmp_path): +def test_apng_save_blend(tmp_path: Path) -> None: test_file = str(tmp_path / "temp.png") size = (128, 64) red = Image.new("RGBA", size, (255, 0, 0, 255)) @@ -666,7 +668,7 @@ def test_apng_save_blend(tmp_path): assert im.getpixel((0, 0)) == (0, 255, 0, 255) -def test_seek_after_close(): +def test_seek_after_close() -> None: im = Image.open("Tests/images/apng/delay.png") im.seek(1) im.close() @@ -678,7 +680,9 @@ def test_seek_after_close(): @pytest.mark.parametrize("mode", ("RGBA", "RGB", "P")) @pytest.mark.parametrize("default_image", (True, False)) @pytest.mark.parametrize("duplicate", (True, False)) -def test_different_modes_in_later_frames(mode, default_image, duplicate, tmp_path): +def test_different_modes_in_later_frames( + mode: str, default_image: bool, duplicate: bool, tmp_path: Path +) -> None: test_file = str(tmp_path / "temp.png") im = Image.new("L", (1, 1)) diff --git a/Tests/test_file_blp.py b/Tests/test_file_blp.py index 27ff7ab66..1e2f20c40 100644 --- a/Tests/test_file_blp.py +++ b/Tests/test_file_blp.py @@ -1,5 +1,7 @@ from __future__ import annotations +from pathlib import Path + import pytest from PIL import Image @@ -12,7 +14,7 @@ from .helper import ( ) -def test_load_blp1(): +def test_load_blp1() -> None: with Image.open("Tests/images/blp/blp1_jpeg.blp") as im: assert_image_equal_tofile(im, "Tests/images/blp/blp1_jpeg.png") @@ -20,22 +22,22 @@ def test_load_blp1(): im.load() -def test_load_blp2_raw(): +def test_load_blp2_raw() -> None: with Image.open("Tests/images/blp/blp2_raw.blp") as im: assert_image_equal_tofile(im, "Tests/images/blp/blp2_raw.png") -def test_load_blp2_dxt1(): +def test_load_blp2_dxt1() -> None: with Image.open("Tests/images/blp/blp2_dxt1.blp") as im: assert_image_equal_tofile(im, "Tests/images/blp/blp2_dxt1.png") -def test_load_blp2_dxt1a(): +def test_load_blp2_dxt1a() -> None: with Image.open("Tests/images/blp/blp2_dxt1a.blp") as im: assert_image_equal_tofile(im, "Tests/images/blp/blp2_dxt1a.png") -def test_save(tmp_path): +def test_save(tmp_path: Path) -> None: f = str(tmp_path / "temp.blp") for version in ("BLP1", "BLP2"): @@ -69,7 +71,7 @@ def test_save(tmp_path): "Tests/images/timeout-ef9112a065e7183fa7faa2e18929b03e44ee16bf.blp", ], ) -def test_crashes(test_file): +def test_crashes(test_file: str) -> None: with open(test_file, "rb") as f: with Image.open(f) as im: with pytest.raises(OSError): diff --git a/Tests/test_file_bmp.py b/Tests/test_file_bmp.py index 225fb28ba..1eaff0c7d 100644 --- a/Tests/test_file_bmp.py +++ b/Tests/test_file_bmp.py @@ -1,6 +1,7 @@ from __future__ import annotations import io +from pathlib import Path import pytest @@ -14,8 +15,8 @@ from .helper import ( ) -def test_sanity(tmp_path): - def roundtrip(im): +def test_sanity(tmp_path: Path) -> None: + def roundtrip(im: Image.Image) -> None: outfile = str(tmp_path / "temp.bmp") im.save(outfile, "BMP") @@ -35,20 +36,20 @@ def test_sanity(tmp_path): roundtrip(hopper("RGB")) -def test_invalid_file(): +def test_invalid_file() -> None: with open("Tests/images/flower.jpg", "rb") as fp: with pytest.raises(SyntaxError): BmpImagePlugin.BmpImageFile(fp) -def test_fallback_if_mmap_errors(): +def test_fallback_if_mmap_errors() -> None: # This image has been truncated, # so that the buffer is not large enough when using mmap with Image.open("Tests/images/mmap_error.bmp") as im: assert_image_equal_tofile(im, "Tests/images/pal8_offset.bmp") -def test_save_to_bytes(): +def test_save_to_bytes() -> None: output = io.BytesIO() im = hopper() im.save(output, "BMP") @@ -60,7 +61,7 @@ def test_save_to_bytes(): assert reloaded.format == "BMP" -def test_small_palette(tmp_path): +def test_small_palette(tmp_path: Path) -> None: im = Image.new("P", (1, 1)) colors = [0, 0, 0, 125, 125, 125, 255, 255, 255] im.putpalette(colors) @@ -72,7 +73,7 @@ def test_small_palette(tmp_path): assert reloaded.getpalette() == colors -def test_save_too_large(tmp_path): +def test_save_too_large(tmp_path: Path) -> None: outfile = str(tmp_path / "temp.bmp") with Image.new("RGB", (1, 1)) as im: im._size = (37838, 37838) @@ -80,7 +81,7 @@ def test_save_too_large(tmp_path): im.save(outfile) -def test_dpi(): +def test_dpi() -> None: dpi = (72, 72) output = io.BytesIO() @@ -92,7 +93,7 @@ def test_dpi(): assert reloaded.info["dpi"] == (72.008961115161, 72.008961115161) -def test_save_bmp_with_dpi(tmp_path): +def test_save_bmp_with_dpi(tmp_path: Path) -> None: # Test for #1301 # Arrange outfile = str(tmp_path / "temp.jpg") @@ -110,7 +111,7 @@ def test_save_bmp_with_dpi(tmp_path): assert reloaded.format == "JPEG" -def test_save_float_dpi(tmp_path): +def test_save_float_dpi(tmp_path: Path) -> None: outfile = str(tmp_path / "temp.bmp") with Image.open("Tests/images/hopper.bmp") as im: im.save(outfile, dpi=(72.21216100543306, 72.21216100543306)) @@ -118,7 +119,7 @@ def test_save_float_dpi(tmp_path): assert reloaded.info["dpi"] == (72.21216100543306, 72.21216100543306) -def test_load_dib(): +def test_load_dib() -> None: # test for #1293, Imagegrab returning Unsupported Bitfields Format with Image.open("Tests/images/clipboard.dib") as im: assert im.format == "DIB" @@ -127,7 +128,7 @@ def test_load_dib(): assert_image_equal_tofile(im, "Tests/images/clipboard_target.png") -def test_save_dib(tmp_path): +def test_save_dib(tmp_path: Path) -> None: outfile = str(tmp_path / "temp.dib") with Image.open("Tests/images/clipboard.dib") as im: @@ -139,7 +140,7 @@ def test_save_dib(tmp_path): assert_image_equal(im, reloaded) -def test_rgba_bitfields(): +def test_rgba_bitfields() -> None: # This test image has been manually hexedited # to change the bitfield compression in the header from XBGR to RGBA with Image.open("Tests/images/rgb32bf-rgba.bmp") as im: @@ -157,7 +158,7 @@ def test_rgba_bitfields(): ) -def test_rle8(): +def test_rle8() -> None: with Image.open("Tests/images/hopper_rle8.bmp") as im: assert_image_similar_tofile(im.convert("RGB"), "Tests/images/hopper.bmp", 12) @@ -177,7 +178,7 @@ def test_rle8(): im.load() -def test_rle4(): +def test_rle4() -> None: with Image.open("Tests/images/bmp/g/pal4rle.bmp") as im: assert_image_similar_tofile(im, "Tests/images/bmp/g/pal4.bmp", 12) @@ -193,7 +194,7 @@ def test_rle4(): ("Tests/images/bmp/g/pal8rle.bmp", 1064), ), ) -def test_rle8_eof(file_name, length): +def test_rle8_eof(file_name: str, length: int) -> None: with open(file_name, "rb") as fp: data = fp.read(length) with Image.open(io.BytesIO(data)) as im: @@ -201,7 +202,7 @@ def test_rle8_eof(file_name, length): im.load() -def test_offset(): +def test_offset() -> None: # This image has been hexedited # to exclude the palette size from the pixel data offset with Image.open("Tests/images/pal8_offset.bmp") as im: diff --git a/Tests/test_file_bufrstub.py b/Tests/test_file_bufrstub.py index 45081832e..3dd24533a 100644 --- a/Tests/test_file_bufrstub.py +++ b/Tests/test_file_bufrstub.py @@ -1,5 +1,7 @@ from __future__ import annotations +from pathlib import Path + import pytest from PIL import BufrStubImagePlugin, Image @@ -9,7 +11,7 @@ from .helper import hopper TEST_FILE = "Tests/images/gfs.t06z.rassda.tm00.bufr_d" -def test_open(): +def test_open() -> None: # Act with Image.open(TEST_FILE) as im: # Assert @@ -20,7 +22,7 @@ def test_open(): assert im.size == (1, 1) -def test_invalid_file(): +def test_invalid_file() -> None: # Arrange invalid_file = "Tests/images/flower.jpg" @@ -29,7 +31,7 @@ def test_invalid_file(): BufrStubImagePlugin.BufrStubImageFile(invalid_file) -def test_load(): +def test_load() -> None: # Arrange with Image.open(TEST_FILE) as im: # Act / Assert: stub cannot load without an implemented handler @@ -37,7 +39,7 @@ def test_load(): im.load() -def test_save(tmp_path): +def test_save(tmp_path: Path) -> None: # Arrange im = hopper() tmpfile = str(tmp_path / "temp.bufr") @@ -47,13 +49,13 @@ def test_save(tmp_path): im.save(tmpfile) -def test_handler(tmp_path): +def test_handler(tmp_path: Path) -> None: class TestHandler: opened = False loaded = False saved = False - def open(self, im): + def open(self, im) -> None: self.opened = True def load(self, im): @@ -61,7 +63,7 @@ def test_handler(tmp_path): im.fp.close() return Image.new("RGB", (1, 1)) - def save(self, im, fp, filename): + def save(self, im, fp, filename) -> None: self.saved = True handler = TestHandler() diff --git a/Tests/test_file_container.py b/Tests/test_file_container.py index 95a5b2337..7f76fb47a 100644 --- a/Tests/test_file_container.py +++ b/Tests/test_file_container.py @@ -1,5 +1,7 @@ from __future__ import annotations +from typing import Literal + import pytest from PIL import ContainerIO, Image @@ -9,21 +11,28 @@ from .helper import hopper TEST_FILE = "Tests/images/dummy.container" -def test_sanity(): +def test_sanity() -> None: dir(Image) dir(ContainerIO) -def test_isatty(): +def test_isatty() -> None: with hopper() as im: container = ContainerIO.ContainerIO(im, 0, 0) assert container.isatty() is False -def test_seek_mode_0(): +@pytest.mark.parametrize( + "mode, expected_position", + ( + (0, 33), + (1, 66), + (2, 100), + ), +) +def test_seek_mode(mode: Literal[0, 1, 2], expected_position: int) -> None: # Arrange - mode = 0 with open(TEST_FILE, "rb") as fh: container = ContainerIO.ContainerIO(fh, 22, 100) @@ -32,39 +41,11 @@ def test_seek_mode_0(): container.seek(33, mode) # Assert - assert container.tell() == 33 - - -def test_seek_mode_1(): - # Arrange - mode = 1 - with open(TEST_FILE, "rb") as fh: - container = ContainerIO.ContainerIO(fh, 22, 100) - - # Act - container.seek(33, mode) - container.seek(33, mode) - - # Assert - assert container.tell() == 66 - - -def test_seek_mode_2(): - # Arrange - mode = 2 - with open(TEST_FILE, "rb") as fh: - container = ContainerIO.ContainerIO(fh, 22, 100) - - # Act - container.seek(33, mode) - container.seek(33, mode) - - # Assert - assert container.tell() == 100 + assert container.tell() == expected_position @pytest.mark.parametrize("bytesmode", (True, False)) -def test_read_n0(bytesmode): +def test_read_n0(bytesmode: bool) -> None: # Arrange with open(TEST_FILE, "rb" if bytesmode else "r") as fh: container = ContainerIO.ContainerIO(fh, 22, 100) @@ -80,7 +61,7 @@ def test_read_n0(bytesmode): @pytest.mark.parametrize("bytesmode", (True, False)) -def test_read_n(bytesmode): +def test_read_n(bytesmode: bool) -> None: # Arrange with open(TEST_FILE, "rb" if bytesmode else "r") as fh: container = ContainerIO.ContainerIO(fh, 22, 100) @@ -96,7 +77,7 @@ def test_read_n(bytesmode): @pytest.mark.parametrize("bytesmode", (True, False)) -def test_read_eof(bytesmode): +def test_read_eof(bytesmode: bool) -> None: # Arrange with open(TEST_FILE, "rb" if bytesmode else "r") as fh: container = ContainerIO.ContainerIO(fh, 22, 100) @@ -112,7 +93,7 @@ def test_read_eof(bytesmode): @pytest.mark.parametrize("bytesmode", (True, False)) -def test_readline(bytesmode): +def test_readline(bytesmode: bool) -> None: # Arrange with open(TEST_FILE, "rb" if bytesmode else "r") as fh: container = ContainerIO.ContainerIO(fh, 0, 120) @@ -127,7 +108,7 @@ def test_readline(bytesmode): @pytest.mark.parametrize("bytesmode", (True, False)) -def test_readlines(bytesmode): +def test_readlines(bytesmode: bool) -> None: # Arrange expected = [ "This is line 1\n", diff --git a/Tests/test_file_cur.py b/Tests/test_file_cur.py index 27b2bc914..dbf1b866d 100644 --- a/Tests/test_file_cur.py +++ b/Tests/test_file_cur.py @@ -7,7 +7,7 @@ from PIL import CurImagePlugin, Image TEST_FILE = "Tests/images/deerstalker.cur" -def test_sanity(): +def test_sanity() -> None: with Image.open(TEST_FILE) as im: assert im.size == (32, 32) assert isinstance(im, CurImagePlugin.CurImageFile) @@ -17,7 +17,7 @@ def test_sanity(): assert im.getpixel((16, 16)) == (84, 87, 86, 255) -def test_invalid_file(): +def test_invalid_file() -> None: invalid_file = "Tests/images/flower.jpg" with pytest.raises(SyntaxError): diff --git a/Tests/test_file_dcx.py b/Tests/test_file_dcx.py index cba7c10bf..65337cad9 100644 --- a/Tests/test_file_dcx.py +++ b/Tests/test_file_dcx.py @@ -12,7 +12,7 @@ from .helper import assert_image_equal, hopper, is_pypy TEST_FILE = "Tests/images/hopper.dcx" -def test_sanity(): +def test_sanity() -> None: # Arrange # Act @@ -25,8 +25,8 @@ def test_sanity(): @pytest.mark.skipif(is_pypy(), reason="Requires CPython") -def test_unclosed_file(): - def open(): +def test_unclosed_file() -> None: + def open() -> None: im = Image.open(TEST_FILE) im.load() @@ -34,26 +34,26 @@ def test_unclosed_file(): open() -def test_closed_file(): +def test_closed_file() -> None: with warnings.catch_warnings(): im = Image.open(TEST_FILE) im.load() im.close() -def test_context_manager(): +def test_context_manager() -> None: with warnings.catch_warnings(): with Image.open(TEST_FILE) as im: im.load() -def test_invalid_file(): +def test_invalid_file() -> None: with open("Tests/images/flower.jpg", "rb") as fp: with pytest.raises(SyntaxError): DcxImagePlugin.DcxImageFile(fp) -def test_tell(): +def test_tell() -> None: # Arrange with Image.open(TEST_FILE) as im: # Act @@ -63,13 +63,13 @@ def test_tell(): assert frame == 0 -def test_n_frames(): +def test_n_frames() -> None: with Image.open(TEST_FILE) as im: assert im.n_frames == 1 assert not im.is_animated -def test_eoferror(): +def test_eoferror() -> None: with Image.open(TEST_FILE) as im: n_frames = im.n_frames @@ -82,7 +82,7 @@ def test_eoferror(): im.seek(n_frames - 1) -def test_seek_too_far(): +def test_seek_too_far() -> None: # Arrange with Image.open(TEST_FILE) as im: frame = 999 # too big on purpose diff --git a/Tests/test_file_dds.py b/Tests/test_file_dds.py index 7064b74c0..ebc0e89a1 100644 --- a/Tests/test_file_dds.py +++ b/Tests/test_file_dds.py @@ -1,7 +1,9 @@ """Test DdsImagePlugin""" + from __future__ import annotations from io import BytesIO +from pathlib import Path import pytest @@ -46,7 +48,7 @@ TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA = "Tests/images/uncompressed_rgb.dds" TEST_FILE_DX10_BC1_TYPELESS, ), ) -def test_sanity_dxt1_bc1(image_path): +def test_sanity_dxt1_bc1(image_path: str) -> None: """Check DXT1 and BC1 images can be opened""" with Image.open(TEST_FILE_DXT1.replace(".dds", ".png")) as target: target = target.convert("RGBA") @@ -60,7 +62,7 @@ def test_sanity_dxt1_bc1(image_path): assert_image_equal(im, target) -def test_sanity_dxt3(): +def test_sanity_dxt3() -> None: """Check DXT3 images can be opened""" with Image.open(TEST_FILE_DXT3) as im: @@ -73,7 +75,7 @@ def test_sanity_dxt3(): assert_image_equal_tofile(im, TEST_FILE_DXT3.replace(".dds", ".png")) -def test_sanity_dxt5(): +def test_sanity_dxt5() -> None: """Check DXT5 images can be opened""" with Image.open(TEST_FILE_DXT5) as im: @@ -94,7 +96,7 @@ def test_sanity_dxt5(): TEST_FILE_BC4U, ), ) -def test_sanity_ati1_bc4u(image_path): +def test_sanity_ati1_bc4u(image_path: str) -> None: """Check ATI1 and BC4U images can be opened""" with Image.open(image_path) as im: @@ -115,7 +117,7 @@ def test_sanity_ati1_bc4u(image_path): TEST_FILE_DX10_BC4_TYPELESS, ), ) -def test_dx10_bc4(image_path): +def test_dx10_bc4(image_path: str) -> None: """Check DX10 BC4 images can be opened""" with Image.open(image_path) as im: @@ -136,7 +138,7 @@ def test_dx10_bc4(image_path): TEST_FILE_BC5U, ), ) -def test_sanity_ati2_bc5u(image_path): +def test_sanity_ati2_bc5u(image_path: str) -> None: """Check ATI2 and BC5U images can be opened""" with Image.open(image_path) as im: @@ -160,7 +162,7 @@ def test_sanity_ati2_bc5u(image_path): (TEST_FILE_BC5S, TEST_FILE_BC5S), ), ) -def test_dx10_bc5(image_path, expected_path): +def test_dx10_bc5(image_path: str, expected_path: str) -> None: """Check DX10 BC5 images can be opened""" with Image.open(image_path) as im: @@ -174,7 +176,7 @@ def test_dx10_bc5(image_path, expected_path): @pytest.mark.parametrize("image_path", (TEST_FILE_BC6H, TEST_FILE_BC6HS)) -def test_dx10_bc6h(image_path): +def test_dx10_bc6h(image_path: str) -> None: """Check DX10 BC6H/BC6HS images can be opened""" with Image.open(image_path) as im: @@ -187,7 +189,7 @@ def test_dx10_bc6h(image_path): assert_image_equal_tofile(im, image_path.replace(".dds", ".png")) -def test_dx10_bc7(): +def test_dx10_bc7() -> None: """Check DX10 images can be opened""" with Image.open(TEST_FILE_DX10_BC7) as im: @@ -200,7 +202,7 @@ def test_dx10_bc7(): assert_image_equal_tofile(im, TEST_FILE_DX10_BC7.replace(".dds", ".png")) -def test_dx10_bc7_unorm_srgb(): +def test_dx10_bc7_unorm_srgb() -> None: """Check DX10 unsigned normalized integer images can be opened""" with Image.open(TEST_FILE_DX10_BC7_UNORM_SRGB) as im: @@ -216,7 +218,7 @@ def test_dx10_bc7_unorm_srgb(): ) -def test_dx10_r8g8b8a8(): +def test_dx10_r8g8b8a8() -> None: """Check DX10 images can be opened""" with Image.open(TEST_FILE_DX10_R8G8B8A8) as im: @@ -229,7 +231,7 @@ def test_dx10_r8g8b8a8(): assert_image_equal_tofile(im, TEST_FILE_DX10_R8G8B8A8.replace(".dds", ".png")) -def test_dx10_r8g8b8a8_unorm_srgb(): +def test_dx10_r8g8b8a8_unorm_srgb() -> None: """Check DX10 unsigned normalized integer images can be opened""" with Image.open(TEST_FILE_DX10_R8G8B8A8_UNORM_SRGB) as im: @@ -255,7 +257,7 @@ def test_dx10_r8g8b8a8_unorm_srgb(): ("RGBA", (800, 600), TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA), ], ) -def test_uncompressed(mode, size, test_file): +def test_uncompressed(mode: str, size: tuple[int, int], test_file: str) -> None: """Check uncompressed images can be opened""" with Image.open(test_file) as im: @@ -266,7 +268,7 @@ def test_uncompressed(mode, size, test_file): assert_image_equal_tofile(im, test_file.replace(".dds", ".png")) -def test__accept_true(): +def test__accept_true() -> None: """Check valid prefix""" # Arrange prefix = b"DDS etc" @@ -278,7 +280,7 @@ def test__accept_true(): assert output -def test__accept_false(): +def test__accept_false() -> None: """Check invalid prefix""" # Arrange prefix = b"something invalid" @@ -290,19 +292,19 @@ def test__accept_false(): assert not output -def test_invalid_file(): +def test_invalid_file() -> None: invalid_file = "Tests/images/flower.jpg" with pytest.raises(SyntaxError): DdsImagePlugin.DdsImageFile(invalid_file) -def test_short_header(): +def test_short_header() -> None: """Check a short header""" with open(TEST_FILE_DXT5, "rb") as f: img_file = f.read() - def short_header(): + def short_header() -> None: with Image.open(BytesIO(img_file[:119])): pass # pragma: no cover @@ -310,13 +312,13 @@ def test_short_header(): short_header() -def test_short_file(): +def test_short_file() -> None: """Check that the appropriate error is thrown for a short file""" with open(TEST_FILE_DXT5, "rb") as f: img_file = f.read() - def short_file(): + def short_file() -> None: with Image.open(BytesIO(img_file[:-100])) as im: im.load() @@ -324,7 +326,7 @@ def test_short_file(): short_file() -def test_dxt5_colorblock_alpha_issue_4142(): +def test_dxt5_colorblock_alpha_issue_4142() -> None: """Check that colorblocks are decoded correctly in DXT5""" with Image.open("Tests/images/dxt5-colorblock-alpha-issue-4142.dds") as im: @@ -339,12 +341,12 @@ def test_dxt5_colorblock_alpha_issue_4142(): assert px[2] != 0 -def test_palette(): +def test_palette() -> None: with Image.open("Tests/images/palette.dds") as im: assert_image_equal_tofile(im, "Tests/images/transparent.gif") -def test_unsupported_bitcount(): +def test_unsupported_bitcount() -> None: with pytest.raises(OSError): with Image.open("Tests/images/unsupported_bitcount.dds"): pass @@ -357,13 +359,13 @@ def test_unsupported_bitcount(): "Tests/images/unimplemented_pfflags.dds", ), ) -def test_not_implemented(test_file): +def test_not_implemented(test_file: str) -> None: with pytest.raises(NotImplementedError): with Image.open(test_file): pass -def test_save_unsupported_mode(tmp_path): +def test_save_unsupported_mode(tmp_path: Path) -> None: out = str(tmp_path / "temp.dds") im = hopper("HSV") with pytest.raises(OSError): @@ -379,7 +381,7 @@ def test_save_unsupported_mode(tmp_path): ("RGBA", "Tests/images/pil123rgba.png"), ], ) -def test_save(mode, test_file, tmp_path): +def test_save(mode: str, test_file: str, tmp_path: Path) -> None: out = str(tmp_path / "temp.dds") with Image.open(test_file) as im: assert im.mode == mode diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py index 8def9a435..00f5f39e8 100644 --- a/Tests/test_file_eps.py +++ b/Tests/test_file_eps.py @@ -1,6 +1,7 @@ from __future__ import annotations import io +from pathlib import Path import pytest @@ -83,7 +84,7 @@ simple_eps_file_with_long_binary_data = ( ("filename", "size"), ((FILE1, (460, 352)), (FILE2, (360, 252))) ) @pytest.mark.parametrize("scale", (1, 2)) -def test_sanity(filename, size, scale): +def test_sanity(filename: str, size: tuple[int, int], scale: int) -> None: expected_size = tuple(s * scale for s in size) with Image.open(filename) as image: image.load(scale=scale) @@ -93,7 +94,7 @@ def test_sanity(filename, size, scale): @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") -def test_load(): +def test_load() -> None: with Image.open(FILE1) as im: assert im.load()[0, 0] == (255, 255, 255) @@ -101,7 +102,7 @@ def test_load(): assert im.load()[0, 0] == (255, 255, 255) -def test_binary(): +def test_binary() -> None: if HAS_GHOSTSCRIPT: assert EpsImagePlugin.gs_binary is not None else: @@ -115,41 +116,41 @@ def test_binary(): assert EpsImagePlugin.gs_windows_binary is not None -def test_invalid_file(): +def test_invalid_file() -> None: invalid_file = "Tests/images/flower.jpg" with pytest.raises(SyntaxError): EpsImagePlugin.EpsImageFile(invalid_file) -def test_binary_header_only(): +def test_binary_header_only() -> None: data = io.BytesIO(simple_binary_header) with pytest.raises(SyntaxError, match='EPS header missing "%!PS-Adobe" comment'): EpsImagePlugin.EpsImageFile(data) @pytest.mark.parametrize("prefix", (b"", simple_binary_header)) -def test_missing_version_comment(prefix): +def test_missing_version_comment(prefix: bytes) -> None: data = io.BytesIO(prefix + b"\n".join(simple_eps_file_without_version)) with pytest.raises(SyntaxError): EpsImagePlugin.EpsImageFile(data) @pytest.mark.parametrize("prefix", (b"", simple_binary_header)) -def test_missing_boundingbox_comment(prefix): +def test_missing_boundingbox_comment(prefix: bytes) -> None: data = io.BytesIO(prefix + b"\n".join(simple_eps_file_without_boundingbox)) with pytest.raises(SyntaxError, match='EPS header missing "%%BoundingBox" comment'): EpsImagePlugin.EpsImageFile(data) @pytest.mark.parametrize("prefix", (b"", simple_binary_header)) -def test_invalid_boundingbox_comment(prefix): +def test_invalid_boundingbox_comment(prefix: bytes) -> None: data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_invalid_boundingbox)) with pytest.raises(OSError, match="cannot determine EPS bounding box"): EpsImagePlugin.EpsImageFile(data) @pytest.mark.parametrize("prefix", (b"", simple_binary_header)) -def test_invalid_boundingbox_comment_valid_imagedata_comment(prefix): +def test_invalid_boundingbox_comment_valid_imagedata_comment(prefix: bytes) -> None: data = io.BytesIO( prefix + b"\n".join(simple_eps_file_with_invalid_boundingbox_valid_imagedata) ) @@ -160,21 +161,21 @@ def test_invalid_boundingbox_comment_valid_imagedata_comment(prefix): @pytest.mark.parametrize("prefix", (b"", simple_binary_header)) -def test_ascii_comment_too_long(prefix): +def test_ascii_comment_too_long(prefix: bytes) -> None: data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_long_ascii_comment)) with pytest.raises(SyntaxError, match="not an EPS file"): EpsImagePlugin.EpsImageFile(data) @pytest.mark.parametrize("prefix", (b"", simple_binary_header)) -def test_long_binary_data(prefix): +def test_long_binary_data(prefix: bytes) -> None: data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_long_binary_data)) EpsImagePlugin.EpsImageFile(data) @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") @pytest.mark.parametrize("prefix", (b"", simple_binary_header)) -def test_load_long_binary_data(prefix): +def test_load_long_binary_data(prefix: bytes) -> None: data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_long_binary_data)) with Image.open(data) as img: img.load() @@ -187,7 +188,7 @@ def test_load_long_binary_data(prefix): pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") -def test_cmyk(): +def test_cmyk() -> None: with Image.open("Tests/images/pil_sample_cmyk.eps") as cmyk_image: assert cmyk_image.mode == "CMYK" assert cmyk_image.size == (100, 100) @@ -203,7 +204,7 @@ def test_cmyk(): @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") -def test_showpage(): +def test_showpage() -> None: # See https://github.com/python-pillow/Pillow/issues/2615 with Image.open("Tests/images/reqd_showpage.eps") as plot_image: with Image.open("Tests/images/reqd_showpage.png") as target: @@ -214,7 +215,7 @@ def test_showpage(): @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") -def test_transparency(): +def test_transparency() -> None: with Image.open("Tests/images/reqd_showpage.eps") as plot_image: plot_image.load(transparency=True) assert plot_image.mode == "RGBA" @@ -225,7 +226,7 @@ def test_transparency(): @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") -def test_file_object(tmp_path): +def test_file_object(tmp_path: Path) -> None: # issue 479 with Image.open(FILE1) as image1: with open(str(tmp_path / "temp.eps"), "wb") as fh: @@ -233,7 +234,7 @@ def test_file_object(tmp_path): @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") -def test_bytesio_object(): +def test_bytesio_object() -> None: with open(FILE1, "rb") as f: img_bytes = io.BytesIO(f.read()) @@ -246,12 +247,12 @@ def test_bytesio_object(): assert_image_similar(img, image1_scale1_compare, 5) -def test_1_mode(): +def test_1_mode() -> None: with Image.open("Tests/images/1.eps") as im: assert im.mode == "1" -def test_image_mode_not_supported(tmp_path): +def test_image_mode_not_supported(tmp_path: Path) -> None: im = hopper("RGBA") tmpfile = str(tmp_path / "temp.eps") with pytest.raises(ValueError): @@ -260,7 +261,7 @@ def test_image_mode_not_supported(tmp_path): @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") @skip_unless_feature("zlib") -def test_render_scale1(): +def test_render_scale1() -> None: # We need png support for these render test # Zero bounding box @@ -282,7 +283,7 @@ def test_render_scale1(): @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") @skip_unless_feature("zlib") -def test_render_scale2(): +def test_render_scale2() -> None: # We need png support for these render test # Zero bounding box @@ -304,7 +305,7 @@ def test_render_scale2(): @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") @pytest.mark.parametrize("filename", (FILE1, FILE2, "Tests/images/illu10_preview.eps")) -def test_resize(filename): +def test_resize(filename: str) -> None: with Image.open(filename) as im: new_size = (100, 100) im = im.resize(new_size) @@ -313,7 +314,7 @@ def test_resize(filename): @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") @pytest.mark.parametrize("filename", (FILE1, FILE2)) -def test_thumbnail(filename): +def test_thumbnail(filename: str) -> None: # Issue #619 with Image.open(filename) as im: new_size = (100, 100) @@ -321,20 +322,20 @@ def test_thumbnail(filename): assert max(im.size) == max(new_size) -def test_read_binary_preview(): +def test_read_binary_preview() -> None: # Issue 302 # open image with binary preview with Image.open(FILE3): pass -def test_readline_psfile(tmp_path): +def test_readline_psfile(tmp_path: Path) -> None: # check all the freaking line endings possible from the spec # test_string = u'something\r\nelse\n\rbaz\rbif\n' line_endings = ["\r\n", "\n", "\n\r", "\r"] strings = ["something", "else", "baz", "bif"] - def _test_readline(t, ending): + def _test_readline(t: EpsImagePlugin.PSFile, ending: str) -> None: ending = "Failure with line ending: %s" % ( "".join("%s" % ord(s) for s in ending) ) @@ -343,13 +344,13 @@ def test_readline_psfile(tmp_path): assert t.readline().strip("\r\n") == "baz", ending assert t.readline().strip("\r\n") == "bif", ending - def _test_readline_io_psfile(test_string, ending): + def _test_readline_io_psfile(test_string: str, ending: str) -> None: f = io.BytesIO(test_string.encode("latin-1")) with pytest.warns(DeprecationWarning): t = EpsImagePlugin.PSFile(f) _test_readline(t, ending) - def _test_readline_file_psfile(test_string, ending): + def _test_readline_file_psfile(test_string: str, ending: str) -> None: f = str(tmp_path / "temp.txt") with open(f, "wb") as w: w.write(test_string.encode("latin-1")) @@ -365,7 +366,7 @@ def test_readline_psfile(tmp_path): _test_readline_file_psfile(s, ending) -def test_psfile_deprecation(): +def test_psfile_deprecation() -> None: with pytest.warns(DeprecationWarning): EpsImagePlugin.PSFile(None) @@ -375,7 +376,7 @@ def test_psfile_deprecation(): "line_ending", (b"\r\n", b"\n", b"\n\r", b"\r"), ) -def test_readline(prefix, line_ending): +def test_readline(prefix: bytes, line_ending: bytes) -> None: simple_file = prefix + line_ending.join(simple_eps_file_with_comments) data = io.BytesIO(simple_file) test_file = EpsImagePlugin.EpsImageFile(data) @@ -393,14 +394,14 @@ def test_readline(prefix, line_ending): "Tests/images/illuCS6_preview.eps", ), ) -def test_open_eps(filename): +def test_open_eps(filename: str) -> None: # https://github.com/python-pillow/Pillow/issues/1104 with Image.open(filename) as img: assert img.mode == "RGB" @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") -def test_emptyline(): +def test_emptyline() -> None: # Test file includes an empty line in the header data emptyline_file = "Tests/images/zero_bb_emptyline.eps" @@ -416,14 +417,14 @@ def test_emptyline(): "test_file", ["Tests/images/timeout-d675703545fee17acab56e5fec644c19979175de.eps"], ) -def test_timeout(test_file): +def test_timeout(test_file: str) -> None: with open(test_file, "rb") as f: with pytest.raises(Image.UnidentifiedImageError): with Image.open(f): pass -def test_bounding_box_in_trailer(): +def test_bounding_box_in_trailer() -> None: # Check bounding boxes are parsed in the same way # when specified in the header and the trailer with Image.open("Tests/images/zero_bb_trailer.eps") as trailer_image, Image.open( @@ -432,7 +433,7 @@ def test_bounding_box_in_trailer(): assert trailer_image.size == header_image.size -def test_eof_before_bounding_box(): +def test_eof_before_bounding_box() -> None: with pytest.raises(OSError): with Image.open("Tests/images/zero_bb_eof_before_boundingbox.eps"): pass diff --git a/Tests/test_file_fits.py b/Tests/test_file_fits.py index 7444eb673..cce0b05cd 100644 --- a/Tests/test_file_fits.py +++ b/Tests/test_file_fits.py @@ -11,7 +11,7 @@ from .helper import assert_image_equal, hopper TEST_FILE = "Tests/images/hopper.fits" -def test_open(): +def test_open() -> None: # Act with Image.open(TEST_FILE) as im: # Assert @@ -22,7 +22,7 @@ def test_open(): assert_image_equal(im, hopper("L")) -def test_invalid_file(): +def test_invalid_file() -> None: # Arrange invalid_file = "Tests/images/flower.jpg" @@ -31,14 +31,14 @@ def test_invalid_file(): FitsImagePlugin.FitsImageFile(invalid_file) -def test_truncated_fits(): +def test_truncated_fits() -> None: # No END to headers image_data = b"SIMPLE = T" + b" " * 50 + b"TRUNCATE" with pytest.raises(OSError): FitsImagePlugin.FitsImageFile(BytesIO(image_data)) -def test_naxis_zero(): +def test_naxis_zero() -> None: # This test image has been manually hexedited # to set the number of data axes to zero with pytest.raises(ValueError): @@ -46,7 +46,7 @@ def test_naxis_zero(): pass -def test_comment(): +def test_comment() -> None: image_data = b"SIMPLE = T / comment string" with pytest.raises(OSError): FitsImagePlugin.FitsImageFile(BytesIO(image_data)) diff --git a/Tests/test_file_fli.py b/Tests/test_file_fli.py index 00377e0c9..f86fb8d09 100644 --- a/Tests/test_file_fli.py +++ b/Tests/test_file_fli.py @@ -4,7 +4,7 @@ import warnings import pytest -from PIL import FliImagePlugin, Image +from PIL import FliImagePlugin, Image, ImageFile from .helper import assert_image_equal, assert_image_equal_tofile, is_pypy @@ -12,11 +12,14 @@ from .helper import assert_image_equal, assert_image_equal_tofile, is_pypy # save as...-> hopper.fli, default options. static_test_file = "Tests/images/hopper.fli" -# From https://samples.libav.org/fli-flc/ +# From https://samples.ffmpeg.org/fli-flc/ animated_test_file = "Tests/images/a.fli" +# From https://samples.ffmpeg.org/fli-flc/ +animated_test_file_with_prefix_chunk = "Tests/images/2422.flc" -def test_sanity(): + +def test_sanity() -> None: with Image.open(static_test_file) as im: im.load() assert im.mode == "P" @@ -32,9 +35,27 @@ def test_sanity(): assert im.is_animated +def test_prefix_chunk() -> None: + ImageFile.LOAD_TRUNCATED_IMAGES = True + try: + with Image.open(animated_test_file_with_prefix_chunk) as im: + assert im.mode == "P" + assert im.size == (320, 200) + assert im.format == "FLI" + assert im.info["duration"] == 171 + assert im.is_animated + + palette = im.getpalette() + assert palette[3:6] == [255, 255, 255] + assert palette[381:384] == [204, 204, 12] + assert palette[765:] == [252, 0, 0] + finally: + ImageFile.LOAD_TRUNCATED_IMAGES = False + + @pytest.mark.skipif(is_pypy(), reason="Requires CPython") -def test_unclosed_file(): - def open(): +def test_unclosed_file() -> None: + def open() -> None: im = Image.open(static_test_file) im.load() @@ -42,14 +63,14 @@ def test_unclosed_file(): open() -def test_closed_file(): +def test_closed_file() -> None: with warnings.catch_warnings(): im = Image.open(static_test_file) im.load() im.close() -def test_seek_after_close(): +def test_seek_after_close() -> None: im = Image.open(animated_test_file) im.seek(1) im.close() @@ -58,13 +79,13 @@ def test_seek_after_close(): im.seek(0) -def test_context_manager(): +def test_context_manager() -> None: with warnings.catch_warnings(): with Image.open(static_test_file) as im: im.load() -def test_tell(): +def test_tell() -> None: # Arrange with Image.open(static_test_file) as im: # Act @@ -74,20 +95,20 @@ def test_tell(): assert frame == 0 -def test_invalid_file(): +def test_invalid_file() -> None: invalid_file = "Tests/images/flower.jpg" with pytest.raises(SyntaxError): FliImagePlugin.FliImageFile(invalid_file) -def test_palette_chunk_second(): +def test_palette_chunk_second() -> None: with Image.open("Tests/images/hopper_palette_chunk_second.fli") as im: with Image.open(static_test_file) as expected: assert_image_equal(im.convert("RGB"), expected.convert("RGB")) -def test_n_frames(): +def test_n_frames() -> None: with Image.open(static_test_file) as im: assert im.n_frames == 1 assert not im.is_animated @@ -97,7 +118,7 @@ def test_n_frames(): assert im.is_animated -def test_eoferror(): +def test_eoferror() -> None: with Image.open(animated_test_file) as im: n_frames = im.n_frames @@ -110,7 +131,7 @@ def test_eoferror(): im.seek(n_frames - 1) -def test_seek_tell(): +def test_seek_tell() -> None: with Image.open(animated_test_file) as im: layer_number = im.tell() assert layer_number == 0 @@ -132,7 +153,7 @@ def test_seek_tell(): assert layer_number == 1 -def test_seek(): +def test_seek() -> None: with Image.open(animated_test_file) as im: im.seek(50) @@ -147,7 +168,7 @@ def test_seek(): ], ) @pytest.mark.timeout(timeout=3) -def test_timeouts(test_file): +def test_timeouts(test_file: str) -> None: with open(test_file, "rb") as f: with Image.open(f) as im: with pytest.raises(OSError): @@ -160,7 +181,7 @@ def test_timeouts(test_file): "Tests/images/crash-5762152299364352.fli", ], ) -def test_crash(test_file): +def test_crash(test_file: str) -> None: with open(test_file, "rb") as f: with Image.open(f) as im: with pytest.raises(OSError): diff --git a/Tests/test_file_fpx.py b/Tests/test_file_fpx.py index d710070c0..e32f30a01 100644 --- a/Tests/test_file_fpx.py +++ b/Tests/test_file_fpx.py @@ -11,7 +11,7 @@ FpxImagePlugin = pytest.importorskip( ) -def test_sanity(): +def test_sanity() -> None: with Image.open("Tests/images/input_bw_one_band.fpx") as im: assert im.mode == "L" assert im.size == (70, 46) @@ -20,7 +20,7 @@ def test_sanity(): assert_image_equal_tofile(im, "Tests/images/input_bw_one_band.png") -def test_close(): +def test_close() -> None: with Image.open("Tests/images/input_bw_one_band.fpx") as im: pass assert im.ole.fp.closed @@ -30,7 +30,7 @@ def test_close(): assert im.ole.fp.closed -def test_invalid_file(): +def test_invalid_file() -> None: # Test an invalid OLE file invalid_file = "Tests/images/flower.jpg" with pytest.raises(SyntaxError): @@ -42,7 +42,7 @@ def test_invalid_file(): FpxImagePlugin.FpxImageFile(ole_file) -def test_fpx_invalid_number_of_bands(): +def test_fpx_invalid_number_of_bands() -> None: with pytest.raises(OSError, match="Invalid number of bands"): with Image.open("Tests/images/input_bw_five_bands.fpx"): pass diff --git a/Tests/test_file_ftex.py b/Tests/test_file_ftex.py index 0f9154e3d..0c544245a 100644 --- a/Tests/test_file_ftex.py +++ b/Tests/test_file_ftex.py @@ -7,18 +7,18 @@ from PIL import FtexImagePlugin, Image from .helper import assert_image_equal_tofile, assert_image_similar -def test_load_raw(): +def test_load_raw() -> None: with Image.open("Tests/images/ftex_uncompressed.ftu") as im: assert_image_equal_tofile(im, "Tests/images/ftex_uncompressed.png") -def test_load_dxt1(): +def test_load_dxt1() -> None: with Image.open("Tests/images/ftex_dxt1.ftc") as im: with Image.open("Tests/images/ftex_dxt1.png") as target: assert_image_similar(im, target.convert("RGBA"), 15) -def test_invalid_file(): +def test_invalid_file() -> None: invalid_file = "Tests/images/flower.jpg" with pytest.raises(SyntaxError): diff --git a/Tests/test_file_gbr.py b/Tests/test_file_gbr.py index d84004e14..be98b08f2 100644 --- a/Tests/test_file_gbr.py +++ b/Tests/test_file_gbr.py @@ -7,12 +7,12 @@ from PIL import GbrImagePlugin, Image from .helper import assert_image_equal_tofile -def test_gbr_file(): +def test_gbr_file() -> None: with Image.open("Tests/images/gbr.gbr") as im: assert_image_equal_tofile(im, "Tests/images/gbr.png") -def test_load(): +def test_load() -> None: with Image.open("Tests/images/gbr.gbr") as im: assert im.load()[0, 0] == (0, 0, 0, 0) @@ -20,14 +20,14 @@ def test_load(): assert im.load()[0, 0] == (0, 0, 0, 0) -def test_multiple_load_operations(): +def test_multiple_load_operations() -> None: with Image.open("Tests/images/gbr.gbr") as im: im.load() im.load() assert_image_equal_tofile(im, "Tests/images/gbr.png") -def test_invalid_file(): +def test_invalid_file() -> None: invalid_file = "Tests/images/flower.jpg" with pytest.raises(SyntaxError): diff --git a/Tests/test_file_gd.py b/Tests/test_file_gd.py index e7db54fb4..d512df284 100644 --- a/Tests/test_file_gd.py +++ b/Tests/test_file_gd.py @@ -7,18 +7,18 @@ from PIL import GdImageFile, UnidentifiedImageError TEST_GD_FILE = "Tests/images/hopper.gd" -def test_sanity(): +def test_sanity() -> None: with GdImageFile.open(TEST_GD_FILE) as im: assert im.size == (128, 128) assert im.format == "GD" -def test_bad_mode(): +def test_bad_mode() -> None: with pytest.raises(ValueError): GdImageFile.open(TEST_GD_FILE, "bad mode") -def test_invalid_file(): +def test_invalid_file() -> None: invalid_file = "Tests/images/flower.jpg" with pytest.raises(UnidentifiedImageError): diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 3e19940aa..9585e576f 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -2,6 +2,8 @@ from __future__ import annotations import warnings from io import BytesIO +from pathlib import Path +from typing import Generator import pytest @@ -23,7 +25,7 @@ with open(TEST_GIF, "rb") as f: data = f.read() -def test_sanity(): +def test_sanity() -> None: with Image.open(TEST_GIF) as im: im.load() assert im.mode == "P" @@ -33,8 +35,8 @@ def test_sanity(): @pytest.mark.skipif(is_pypy(), reason="Requires CPython") -def test_unclosed_file(): - def open(): +def test_unclosed_file() -> None: + def open() -> None: im = Image.open(TEST_GIF) im.load() @@ -42,14 +44,14 @@ def test_unclosed_file(): open() -def test_closed_file(): +def test_closed_file() -> None: with warnings.catch_warnings(): im = Image.open(TEST_GIF) im.load() im.close() -def test_seek_after_close(): +def test_seek_after_close() -> None: im = Image.open("Tests/images/iss634.gif") im.load() im.close() @@ -62,20 +64,20 @@ def test_seek_after_close(): im.seek(1) -def test_context_manager(): +def test_context_manager() -> None: with warnings.catch_warnings(): with Image.open(TEST_GIF) as im: im.load() -def test_invalid_file(): +def test_invalid_file() -> None: invalid_file = "Tests/images/flower.jpg" with pytest.raises(SyntaxError): GifImagePlugin.GifImageFile(invalid_file) -def test_l_mode_transparency(): +def test_l_mode_transparency() -> None: with Image.open("Tests/images/no_palette_with_transparency.gif") as im: assert im.mode == "L" assert im.load()[0, 0] == 128 @@ -86,7 +88,7 @@ def test_l_mode_transparency(): assert im.load()[0, 0] == 128 -def test_l_mode_after_rgb(): +def test_l_mode_after_rgb() -> None: with Image.open("Tests/images/no_palette_after_rgb.gif") as im: im.seek(1) assert im.mode == "RGB" @@ -95,13 +97,13 @@ def test_l_mode_after_rgb(): assert im.mode == "RGB" -def test_palette_not_needed_for_second_frame(): +def test_palette_not_needed_for_second_frame() -> None: with Image.open("Tests/images/palette_not_needed_for_second_frame.gif") as im: im.seek(1) assert_image_similar(im, hopper("L").convert("RGB"), 8) -def test_strategy(): +def test_strategy() -> None: with Image.open("Tests/images/iss634.gif") as im: expected_rgb_always = im.convert("RGB") @@ -142,14 +144,14 @@ def test_strategy(): GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST -def test_optimize(): - def test_grayscale(optimize): +def test_optimize() -> None: + def test_grayscale(optimize: int) -> int: im = Image.new("L", (1, 1), 0) filename = BytesIO() im.save(filename, "GIF", optimize=optimize) return len(filename.getvalue()) - def test_bilevel(optimize): + def test_bilevel(optimize: int) -> int: im = Image.new("1", (1, 1), 0) test_file = BytesIO() im.save(test_file, "GIF", optimize=optimize) @@ -177,7 +179,9 @@ def test_optimize(): (4, 513, 256), ), ) -def test_optimize_correctness(colors, size, expected_palette_length): +def test_optimize_correctness( + colors: int, size: int, expected_palette_length: int +) -> None: # 256 color Palette image, posterize to > 128 and < 128 levels. # Size bigger and smaller than 512x512. # Check the palette for number of colors allocated. @@ -199,14 +203,14 @@ def test_optimize_correctness(colors, size, expected_palette_length): assert_image_equal(im.convert("RGB"), reloaded.convert("RGB")) -def test_optimize_full_l(): +def test_optimize_full_l() -> None: im = Image.frombytes("L", (16, 16), bytes(range(256))) test_file = BytesIO() im.save(test_file, "GIF", optimize=True) assert im.mode == "L" -def test_optimize_if_palette_can_be_reduced_by_half(): +def test_optimize_if_palette_can_be_reduced_by_half() -> None: im = Image.new("P", (8, 1)) im.palette = ImagePalette.raw("RGB", bytes((0, 0, 0) * 150)) for i in range(8): @@ -219,7 +223,7 @@ def test_optimize_if_palette_can_be_reduced_by_half(): assert len(reloaded.palette.palette) // 3 == colors -def test_full_palette_second_frame(tmp_path): +def test_full_palette_second_frame(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") im = Image.new("P", (1, 256)) @@ -240,7 +244,7 @@ def test_full_palette_second_frame(tmp_path): reloaded.getpixel((0, i)) == i -def test_roundtrip(tmp_path): +def test_roundtrip(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") im = hopper() im.save(out) @@ -248,7 +252,7 @@ def test_roundtrip(tmp_path): assert_image_similar(reread.convert("RGB"), im, 50) -def test_roundtrip2(tmp_path): +def test_roundtrip2(tmp_path: Path) -> None: # see https://github.com/python-pillow/Pillow/issues/403 out = str(tmp_path / "temp.gif") with Image.open(TEST_GIF) as im: @@ -258,7 +262,7 @@ def test_roundtrip2(tmp_path): assert_image_similar(reread.convert("RGB"), hopper(), 50) -def test_roundtrip_save_all(tmp_path): +def test_roundtrip_save_all(tmp_path: Path) -> None: # Single frame image out = str(tmp_path / "temp.gif") im = hopper() @@ -275,7 +279,7 @@ def test_roundtrip_save_all(tmp_path): assert reread.n_frames == 5 -def test_roundtrip_save_all_1(tmp_path): +def test_roundtrip_save_all_1(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") im = Image.new("1", (1, 1)) im2 = Image.new("1", (1, 1), 1) @@ -296,7 +300,7 @@ def test_roundtrip_save_all_1(tmp_path): ("Tests/images/dispose_bgnd_rgba.gif", "RGBA"), ), ) -def test_loading_multiple_palettes(path, mode): +def test_loading_multiple_palettes(path: str, mode: str) -> None: with Image.open(path) as im: assert im.mode == "P" first_frame_colors = im.palette.colors.keys() @@ -314,7 +318,7 @@ def test_loading_multiple_palettes(path, mode): assert im.load()[24, 24] not in first_frame_colors -def test_headers_saving_for_animated_gifs(tmp_path): +def test_headers_saving_for_animated_gifs(tmp_path: Path) -> None: important_headers = ["background", "version", "duration", "loop"] # Multiframe image with Image.open("Tests/images/dispose_bgnd.gif") as im: @@ -327,7 +331,7 @@ def test_headers_saving_for_animated_gifs(tmp_path): assert info[header] == reread.info[header] -def test_palette_handling(tmp_path): +def test_palette_handling(tmp_path: Path) -> None: # see https://github.com/python-pillow/Pillow/issues/513 with Image.open(TEST_GIF) as im: @@ -343,12 +347,12 @@ def test_palette_handling(tmp_path): assert_image_similar(im, reloaded.convert("RGB"), 10) -def test_palette_434(tmp_path): +def test_palette_434(tmp_path: Path) -> None: # see https://github.com/python-pillow/Pillow/issues/434 - def roundtrip(im, *args, **kwargs): + def roundtrip(im: Image.Image, **kwargs: bool) -> Image.Image: out = str(tmp_path / "temp.gif") - im.copy().save(out, *args, **kwargs) + im.copy().save(out, **kwargs) reloaded = Image.open(out) return reloaded @@ -368,7 +372,7 @@ def test_palette_434(tmp_path): @pytest.mark.skipif(not netpbm_available(), reason="Netpbm not available") -def test_save_netpbm_bmp_mode(tmp_path): +def test_save_netpbm_bmp_mode(tmp_path: Path) -> None: with Image.open(TEST_GIF) as img: img = img.convert("RGB") @@ -379,7 +383,7 @@ def test_save_netpbm_bmp_mode(tmp_path): @pytest.mark.skipif(not netpbm_available(), reason="Netpbm not available") -def test_save_netpbm_l_mode(tmp_path): +def test_save_netpbm_l_mode(tmp_path: Path) -> None: with Image.open(TEST_GIF) as img: img = img.convert("L") @@ -389,7 +393,7 @@ def test_save_netpbm_l_mode(tmp_path): assert_image_similar(img, reloaded.convert("L"), 0) -def test_seek(): +def test_seek() -> None: with Image.open("Tests/images/dispose_none.gif") as img: frame_count = 0 try: @@ -400,7 +404,7 @@ def test_seek(): assert frame_count == 5 -def test_seek_info(): +def test_seek_info() -> None: with Image.open("Tests/images/iss634.gif") as im: info = im.info.copy() @@ -410,7 +414,7 @@ def test_seek_info(): assert im.info == info -def test_seek_rewind(): +def test_seek_rewind() -> None: with Image.open("Tests/images/iss634.gif") as im: im.seek(2) im.seek(1) @@ -428,7 +432,7 @@ def test_seek_rewind(): ("Tests/images/iss634.gif", 42), ), ) -def test_n_frames(path, n_frames): +def test_n_frames(path: str, n_frames: int) -> None: # Test is_animated before n_frames with Image.open(path) as im: assert im.is_animated == (n_frames != 1) @@ -439,7 +443,7 @@ def test_n_frames(path, n_frames): assert im.is_animated == (n_frames != 1) -def test_no_change(): +def test_no_change() -> None: # Test n_frames does not change the image with Image.open("Tests/images/dispose_bgnd.gif") as im: im.seek(1) @@ -460,7 +464,7 @@ def test_no_change(): assert_image_equal(im, expected) -def test_eoferror(): +def test_eoferror() -> None: with Image.open(TEST_GIF) as im: n_frames = im.n_frames @@ -473,13 +477,13 @@ def test_eoferror(): im.seek(n_frames - 1) -def test_first_frame_transparency(): +def test_first_frame_transparency() -> None: with Image.open("Tests/images/first_frame_transparency.gif") as im: px = im.load() assert px[0, 0] == im.info["transparency"] -def test_dispose_none(): +def test_dispose_none() -> None: with Image.open("Tests/images/dispose_none.gif") as img: try: while True: @@ -489,7 +493,7 @@ def test_dispose_none(): pass -def test_dispose_none_load_end(): +def test_dispose_none_load_end() -> None: # Test image created with: # # im = Image.open("transparent.gif") @@ -502,7 +506,7 @@ def test_dispose_none_load_end(): assert_image_equal_tofile(img, "Tests/images/dispose_none_load_end_second.png") -def test_dispose_background(): +def test_dispose_background() -> None: with Image.open("Tests/images/dispose_bgnd.gif") as img: try: while True: @@ -512,7 +516,7 @@ def test_dispose_background(): pass -def test_dispose_background_transparency(): +def test_dispose_background_transparency() -> None: with Image.open("Tests/images/dispose_bgnd_transparency.gif") as img: img.seek(2) px = img.load() @@ -540,7 +544,10 @@ def test_dispose_background_transparency(): ), ), ) -def test_transparent_dispose(loading_strategy, expected_colors): +def test_transparent_dispose( + loading_strategy: GifImagePlugin.LoadingStrategy, + expected_colors: tuple[tuple[int | tuple[int, int, int, int], ...]], +) -> None: GifImagePlugin.LOADING_STRATEGY = loading_strategy try: with Image.open("Tests/images/transparent_dispose.gif") as img: @@ -553,7 +560,7 @@ def test_transparent_dispose(loading_strategy, expected_colors): GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST -def test_dispose_previous(): +def test_dispose_previous() -> None: with Image.open("Tests/images/dispose_prev.gif") as img: try: while True: @@ -563,7 +570,7 @@ def test_dispose_previous(): pass -def test_dispose_previous_first_frame(): +def test_dispose_previous_first_frame() -> None: with Image.open("Tests/images/dispose_prev_first_frame.gif") as im: im.seek(1) assert_image_equal_tofile( @@ -571,7 +578,7 @@ def test_dispose_previous_first_frame(): ) -def test_previous_frame_loaded(): +def test_previous_frame_loaded() -> None: with Image.open("Tests/images/dispose_none.gif") as img: img.load() img.seek(1) @@ -582,7 +589,7 @@ def test_previous_frame_loaded(): assert_image_equal(img_skipped, img) -def test_save_dispose(tmp_path): +def test_save_dispose(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") im_list = [ Image.new("L", (100, 100), "#000"), @@ -610,7 +617,7 @@ def test_save_dispose(tmp_path): assert img.disposal_method == i + 1 -def test_dispose2_palette(tmp_path): +def test_dispose2_palette(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") # Four colors: white, gray, black, red @@ -641,7 +648,7 @@ def test_dispose2_palette(tmp_path): assert rgb_img.getpixel((50, 50)) == circle -def test_dispose2_diff(tmp_path): +def test_dispose2_diff(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") # 4 frames: red/blue, red/red, blue/blue, red/blue @@ -683,7 +690,7 @@ def test_dispose2_diff(tmp_path): assert rgb_img.getpixel((1, 1)) == (255, 255, 255, 0) -def test_dispose2_background(tmp_path): +def test_dispose2_background(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") im_list = [] @@ -709,7 +716,7 @@ def test_dispose2_background(tmp_path): assert im.getpixel((0, 0)) == (255, 0, 0) -def test_dispose2_background_frame(tmp_path): +def test_dispose2_background_frame(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") im_list = [Image.new("RGBA", (1, 20))] @@ -727,7 +734,7 @@ def test_dispose2_background_frame(tmp_path): assert im.n_frames == 3 -def test_transparency_in_second_frame(tmp_path): +def test_transparency_in_second_frame(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") with Image.open("Tests/images/different_transparency.gif") as im: assert im.info["transparency"] == 0 @@ -747,7 +754,7 @@ def test_transparency_in_second_frame(tmp_path): ) -def test_no_transparency_in_second_frame(): +def test_no_transparency_in_second_frame() -> None: with Image.open("Tests/images/iss634.gif") as img: # Seek to the second frame img.seek(img.tell() + 1) @@ -757,7 +764,7 @@ def test_no_transparency_in_second_frame(): assert img.histogram()[255] == 0 -def test_remapped_transparency(tmp_path): +def test_remapped_transparency(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") im = Image.new("P", (1, 2)) @@ -773,7 +780,7 @@ def test_remapped_transparency(tmp_path): assert reloaded.info["transparency"] == reloaded.getpixel((0, 1)) -def test_duration(tmp_path): +def test_duration(tmp_path: Path) -> None: duration = 1000 out = str(tmp_path / "temp.gif") @@ -787,7 +794,7 @@ def test_duration(tmp_path): assert reread.info["duration"] == duration -def test_multiple_duration(tmp_path): +def test_multiple_duration(tmp_path: Path) -> None: duration_list = [1000, 2000, 3000] out = str(tmp_path / "temp.gif") @@ -822,7 +829,7 @@ def test_multiple_duration(tmp_path): pass -def test_roundtrip_info_duration(tmp_path): +def test_roundtrip_info_duration(tmp_path: Path) -> None: duration_list = [100, 500, 500] out = str(tmp_path / "temp.gif") @@ -839,7 +846,7 @@ def test_roundtrip_info_duration(tmp_path): ] == duration_list -def test_roundtrip_info_duration_combined(tmp_path): +def test_roundtrip_info_duration_combined(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") with Image.open("Tests/images/duplicate_frame.gif") as im: assert [frame.info["duration"] for frame in ImageSequence.Iterator(im)] == [ @@ -855,7 +862,7 @@ def test_roundtrip_info_duration_combined(tmp_path): ] == [1000, 2000] -def test_identical_frames(tmp_path): +def test_identical_frames(tmp_path: Path) -> None: duration_list = [1000, 1500, 2000, 4000] out = str(tmp_path / "temp.gif") @@ -888,7 +895,9 @@ def test_identical_frames(tmp_path): 1500, ), ) -def test_identical_frames_to_single_frame(duration, tmp_path): +def test_identical_frames_to_single_frame( + duration: int | list[int], tmp_path: Path +) -> None: out = str(tmp_path / "temp.gif") im_list = [ Image.new("L", (100, 100), "#000"), @@ -905,7 +914,7 @@ def test_identical_frames_to_single_frame(duration, tmp_path): assert reread.info["duration"] == 4500 -def test_loop_none(tmp_path): +def test_loop_none(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") im = Image.new("L", (100, 100), "#000") im.save(out, loop=None) @@ -913,7 +922,7 @@ def test_loop_none(tmp_path): assert "loop" not in reread.info -def test_number_of_loops(tmp_path): +def test_number_of_loops(tmp_path: Path) -> None: number_of_loops = 2 out = str(tmp_path / "temp.gif") @@ -931,7 +940,7 @@ def test_number_of_loops(tmp_path): assert im.info["loop"] == 2 -def test_background(tmp_path): +def test_background(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") im = Image.new("L", (100, 100), "#000") im.info["background"] = 1 @@ -940,7 +949,7 @@ def test_background(tmp_path): assert reread.info["background"] == im.info["background"] -def test_webp_background(tmp_path): +def test_webp_background(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") # Test opaque WebP background @@ -955,7 +964,7 @@ def test_webp_background(tmp_path): im.save(out) -def test_comment(tmp_path): +def test_comment(tmp_path: Path) -> None: with Image.open(TEST_GIF) as im: assert im.info["comment"] == b"File written by Adobe Photoshop\xa8 4.0" @@ -975,7 +984,7 @@ def test_comment(tmp_path): assert reread.info["version"] == b"GIF89a" -def test_comment_over_255(tmp_path): +def test_comment_over_255(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") im = Image.new("L", (100, 100), "#000") comment = b"Test comment text" @@ -990,18 +999,18 @@ def test_comment_over_255(tmp_path): assert reread.info["version"] == b"GIF89a" -def test_zero_comment_subblocks(): +def test_zero_comment_subblocks() -> None: with Image.open("Tests/images/hopper_zero_comment_subblocks.gif") as im: assert_image_equal_tofile(im, TEST_GIF) -def test_read_multiple_comment_blocks(): +def test_read_multiple_comment_blocks() -> None: with Image.open("Tests/images/multiple_comments.gif") as im: # Multiple comment blocks in a frame are separated not concatenated assert im.info["comment"] == b"Test comment 1\nTest comment 2" -def test_empty_string_comment(tmp_path): +def test_empty_string_comment(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") with Image.open("Tests/images/chi.gif") as im: assert "comment" in im.info @@ -1014,7 +1023,7 @@ def test_empty_string_comment(tmp_path): assert "comment" not in frame.info -def test_retain_comment_in_subsequent_frames(tmp_path): +def test_retain_comment_in_subsequent_frames(tmp_path: Path) -> None: # Test that a comment block at the beginning is kept with Image.open("Tests/images/chi.gif") as im: for frame in ImageSequence.Iterator(im): @@ -1045,10 +1054,10 @@ def test_retain_comment_in_subsequent_frames(tmp_path): assert frame.info["comment"] == b"Test" -def test_version(tmp_path): +def test_version(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") - def assert_version_after_save(im, version): + def assert_version_after_save(im: Image.Image, version: bytes) -> None: im.save(out) with Image.open(out) as reread: assert reread.info["version"] == version @@ -1075,7 +1084,7 @@ def test_version(tmp_path): assert_version_after_save(im, b"GIF87a") -def test_append_images(tmp_path): +def test_append_images(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") # Test appending single frame images @@ -1087,7 +1096,7 @@ def test_append_images(tmp_path): assert reread.n_frames == 3 # Tests appending using a generator - def im_generator(ims): + def im_generator(ims: list[Image.Image]) -> Generator[Image.Image, None, None]: yield from ims im.save(out, save_all=True, append_images=im_generator(ims)) @@ -1104,7 +1113,22 @@ def test_append_images(tmp_path): assert reread.n_frames == 10 -def test_transparent_optimize(tmp_path): +def test_append_different_size_image(tmp_path: Path) -> None: + out = str(tmp_path / "temp.gif") + + im = Image.new("RGB", (100, 100)) + bigger_im = Image.new("RGB", (200, 200), "#f00") + + im.save(out, save_all=True, append_images=[bigger_im]) + + with Image.open(out) as reread: + assert reread.size == (100, 100) + + reread.seek(1) + assert reread.size == (100, 100) + + +def test_transparent_optimize(tmp_path: Path) -> None: # From issue #2195, if the transparent color is incorrectly optimized out, GIF loses # transparency. # Need a palette that isn't using the 0 color, @@ -1124,7 +1148,7 @@ def test_transparent_optimize(tmp_path): assert reloaded.info["transparency"] == reloaded.getpixel((252, 0)) -def test_removed_transparency(tmp_path): +def test_removed_transparency(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") im = Image.new("RGB", (256, 1)) @@ -1139,7 +1163,7 @@ def test_removed_transparency(tmp_path): assert "transparency" not in reloaded.info -def test_rgb_transparency(tmp_path): +def test_rgb_transparency(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") # Single frame @@ -1161,7 +1185,7 @@ def test_rgb_transparency(tmp_path): assert "transparency" not in reloaded.info -def test_rgba_transparency(tmp_path): +def test_rgba_transparency(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") im = hopper("P") @@ -1172,13 +1196,13 @@ def test_rgba_transparency(tmp_path): assert_image_equal(hopper("P").convert("RGB"), reloaded) -def test_background_outside_palettte(tmp_path): +def test_background_outside_palettte(tmp_path: Path) -> None: with Image.open("Tests/images/background_outside_palette.gif") as im: im.seek(1) assert im.info["background"] == 255 -def test_bbox(tmp_path): +def test_bbox(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") im = Image.new("RGB", (100, 100), "#fff") @@ -1189,7 +1213,7 @@ def test_bbox(tmp_path): assert reread.n_frames == 2 -def test_bbox_alpha(tmp_path): +def test_bbox_alpha(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") im = Image.new("RGBA", (1, 2), (255, 0, 0, 255)) @@ -1201,7 +1225,7 @@ def test_bbox_alpha(tmp_path): assert reread.n_frames == 2 -def test_palette_save_L(tmp_path): +def test_palette_save_L(tmp_path: Path) -> None: # Generate an L mode image with a separate palette im = hopper("P") @@ -1215,7 +1239,7 @@ def test_palette_save_L(tmp_path): assert_image_equal(reloaded.convert("RGB"), im.convert("RGB")) -def test_palette_save_P(tmp_path): +def test_palette_save_P(tmp_path: Path) -> None: im = Image.new("P", (1, 2)) im.putpixel((0, 1), 1) @@ -1229,7 +1253,7 @@ def test_palette_save_P(tmp_path): assert reloaded_rgb.getpixel((0, 1)) == (4, 5, 6) -def test_palette_save_duplicate_entries(tmp_path): +def test_palette_save_duplicate_entries(tmp_path: Path) -> None: im = Image.new("P", (1, 2)) im.putpixel((0, 1), 1) @@ -1242,7 +1266,7 @@ def test_palette_save_duplicate_entries(tmp_path): assert reloaded.convert("RGB").getpixel((0, 1)) == (0, 0, 0) -def test_palette_save_all_P(tmp_path): +def test_palette_save_all_P(tmp_path: Path) -> None: frames = [] colors = ((255, 0, 0), (0, 255, 0)) for color in colors: @@ -1265,7 +1289,7 @@ def test_palette_save_all_P(tmp_path): assert im.palette.palette == im.global_palette.palette -def test_palette_save_ImagePalette(tmp_path): +def test_palette_save_ImagePalette(tmp_path: Path) -> None: # Pass in a different palette, as an ImagePalette.ImagePalette # effectively the same as test_palette_save_P @@ -1280,7 +1304,7 @@ def test_palette_save_ImagePalette(tmp_path): assert_image_equal(reloaded.convert("RGB"), im.convert("RGB")) -def test_save_I(tmp_path): +def test_save_I(tmp_path: Path) -> None: # Test saving something that would trigger the auto-convert to 'L' im = hopper("I") @@ -1292,7 +1316,7 @@ def test_save_I(tmp_path): assert_image_equal(reloaded.convert("L"), im.convert("L")) -def test_getdata(): +def test_getdata() -> None: # Test getheader/getdata against legacy values. # Create a 'P' image with holes in the palette. im = Image._wedge().resize((16, 16), Image.Resampling.NEAREST) @@ -1320,7 +1344,7 @@ def test_getdata(): GifImagePlugin._FORCE_OPTIMIZE = False -def test_lzw_bits(): +def test_lzw_bits() -> None: # see https://github.com/python-pillow/Pillow/issues/2811 with Image.open("Tests/images/issue_2811.gif") as im: assert im.tile[0][3][0] == 11 # LZW bits @@ -1328,7 +1352,7 @@ def test_lzw_bits(): im.load() -def test_extents(): +def test_extents() -> None: with Image.open("Tests/images/test_extents.gif") as im: assert im.size == (100, 100) @@ -1340,7 +1364,7 @@ def test_extents(): assert im.size == (150, 150) -def test_missing_background(): +def test_missing_background() -> None: # The Global Color Table Flag isn't set, so there is no background color index, # but the disposal method is "Restore to background color" with Image.open("Tests/images/missing_background.gif") as im: @@ -1348,7 +1372,7 @@ def test_missing_background(): assert_image_equal_tofile(im, "Tests/images/missing_background_first_frame.png") -def test_saving_rgba(tmp_path): +def test_saving_rgba(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") with Image.open("Tests/images/transparent.png") as im: im.save(out) diff --git a/Tests/test_file_gimpgradient.py b/Tests/test_file_gimpgradient.py index ceea1edd3..006ee952d 100644 --- a/Tests/test_file_gimpgradient.py +++ b/Tests/test_file_gimpgradient.py @@ -3,7 +3,7 @@ from __future__ import annotations from PIL import GimpGradientFile, ImagePalette -def test_linear_pos_le_middle(): +def test_linear_pos_le_middle() -> None: # Arrange middle = 0.5 pos = 0.25 @@ -15,7 +15,7 @@ def test_linear_pos_le_middle(): assert ret == 0.25 -def test_linear_pos_le_small_middle(): +def test_linear_pos_le_small_middle() -> None: # Arrange middle = 1e-11 pos = 1e-12 @@ -27,7 +27,7 @@ def test_linear_pos_le_small_middle(): assert ret == 0.0 -def test_linear_pos_gt_middle(): +def test_linear_pos_gt_middle() -> None: # Arrange middle = 0.5 pos = 0.75 @@ -39,7 +39,7 @@ def test_linear_pos_gt_middle(): assert ret == 0.75 -def test_linear_pos_gt_small_middle(): +def test_linear_pos_gt_small_middle() -> None: # Arrange middle = 1 - 1e-11 pos = 1 - 1e-12 @@ -51,7 +51,7 @@ def test_linear_pos_gt_small_middle(): assert ret == 1.0 -def test_curved(): +def test_curved() -> None: # Arrange middle = 0.5 pos = 0.75 @@ -63,7 +63,7 @@ def test_curved(): assert ret == 0.75 -def test_sine(): +def test_sine() -> None: # Arrange middle = 0.5 pos = 0.75 @@ -75,7 +75,7 @@ def test_sine(): assert ret == 0.8535533905932737 -def test_sphere_increasing(): +def test_sphere_increasing() -> None: # Arrange middle = 0.5 pos = 0.75 @@ -87,7 +87,7 @@ def test_sphere_increasing(): assert round(abs(ret - 0.9682458365518543), 7) == 0 -def test_sphere_decreasing(): +def test_sphere_decreasing() -> None: # Arrange middle = 0.5 pos = 0.75 @@ -99,7 +99,7 @@ def test_sphere_decreasing(): assert ret == 0.3385621722338523 -def test_load_via_imagepalette(): +def test_load_via_imagepalette() -> None: # Arrange test_file = "Tests/images/gimp_gradient.ggr" @@ -112,7 +112,7 @@ def test_load_via_imagepalette(): assert palette[1] == "RGBA" -def test_load_1_3_via_imagepalette(): +def test_load_1_3_via_imagepalette() -> None: # Arrange # GIMP 1.3 gradient files contain a name field test_file = "Tests/images/gimp_gradient_with_name.ggr" diff --git a/Tests/test_file_gimppalette.py b/Tests/test_file_gimppalette.py index 28855c28a..e8d5f1705 100644 --- a/Tests/test_file_gimppalette.py +++ b/Tests/test_file_gimppalette.py @@ -5,7 +5,7 @@ import pytest from PIL.GimpPaletteFile import GimpPaletteFile -def test_sanity(): +def test_sanity() -> None: with open("Tests/images/test.gpl", "rb") as fp: GimpPaletteFile(fp) @@ -22,7 +22,7 @@ def test_sanity(): GimpPaletteFile(fp) -def test_get_palette(): +def test_get_palette() -> None: # Arrange with open("Tests/images/custom_gimp_palette.gpl", "rb") as fp: palette_file = GimpPaletteFile(fp) diff --git a/Tests/test_file_gribstub.py b/Tests/test_file_gribstub.py index a4ce6dde6..096a5b88b 100644 --- a/Tests/test_file_gribstub.py +++ b/Tests/test_file_gribstub.py @@ -1,5 +1,8 @@ from __future__ import annotations +from pathlib import Path +from typing import IO + import pytest from PIL import GribStubImagePlugin, Image @@ -9,7 +12,7 @@ from .helper import hopper TEST_FILE = "Tests/images/WAlaska.wind.7days.grb" -def test_open(): +def test_open() -> None: # Act with Image.open(TEST_FILE) as im: # Assert @@ -20,7 +23,7 @@ def test_open(): assert im.size == (1, 1) -def test_invalid_file(): +def test_invalid_file() -> None: # Arrange invalid_file = "Tests/images/flower.jpg" @@ -29,7 +32,7 @@ def test_invalid_file(): GribStubImagePlugin.GribStubImageFile(invalid_file) -def test_load(): +def test_load() -> None: # Arrange with Image.open(TEST_FILE) as im: # Act / Assert: stub cannot load without an implemented handler @@ -37,7 +40,7 @@ def test_load(): im.load() -def test_save(tmp_path): +def test_save(tmp_path: Path) -> None: # Arrange im = hopper() tmpfile = str(tmp_path / "temp.grib") @@ -47,21 +50,21 @@ def test_save(tmp_path): im.save(tmpfile) -def test_handler(tmp_path): +def test_handler(tmp_path: Path) -> None: class TestHandler: opened = False loaded = False saved = False - def open(self, im): + def open(self, im: Image.Image) -> None: self.opened = True - def load(self, im): + def load(self, im: Image.Image) -> Image.Image: self.loaded = True im.fp.close() return Image.new("RGB", (1, 1)) - def save(self, im, fp, filename): + def save(self, im: Image.Image, fp: IO[bytes], filename: str) -> None: self.saved = True handler = TestHandler() diff --git a/Tests/test_file_hdf5stub.py b/Tests/test_file_hdf5stub.py index 727644617..f871e2eff 100644 --- a/Tests/test_file_hdf5stub.py +++ b/Tests/test_file_hdf5stub.py @@ -1,5 +1,8 @@ from __future__ import annotations +from pathlib import Path +from typing import IO + import pytest from PIL import Hdf5StubImagePlugin, Image @@ -7,7 +10,7 @@ from PIL import Hdf5StubImagePlugin, Image TEST_FILE = "Tests/images/hdf5.h5" -def test_open(): +def test_open() -> None: # Act with Image.open(TEST_FILE) as im: # Assert @@ -18,7 +21,7 @@ def test_open(): assert im.size == (1, 1) -def test_invalid_file(): +def test_invalid_file() -> None: # Arrange invalid_file = "Tests/images/flower.jpg" @@ -27,7 +30,7 @@ def test_invalid_file(): Hdf5StubImagePlugin.HDF5StubImageFile(invalid_file) -def test_load(): +def test_load() -> None: # Arrange with Image.open(TEST_FILE) as im: # Act / Assert: stub cannot load without an implemented handler @@ -35,7 +38,7 @@ def test_load(): im.load() -def test_save(): +def test_save() -> None: # Arrange with Image.open(TEST_FILE) as im: dummy_fp = None @@ -48,21 +51,21 @@ def test_save(): Hdf5StubImagePlugin._save(im, dummy_fp, dummy_filename) -def test_handler(tmp_path): +def test_handler(tmp_path: Path) -> None: class TestHandler: opened = False loaded = False saved = False - def open(self, im): + def open(self, im: Image.Image) -> None: self.opened = True - def load(self, im): + def load(self, im: Image.Image) -> Image.Image: self.loaded = True im.fp.close() return Image.new("RGB", (1, 1)) - def save(self, im, fp, filename): + def save(self, im: Image.Image, fp: IO[bytes], filename: str) -> None: self.saved = True handler = TestHandler() diff --git a/Tests/test_file_icns.py b/Tests/test_file_icns.py index 314fa8008..488984aef 100644 --- a/Tests/test_file_icns.py +++ b/Tests/test_file_icns.py @@ -3,6 +3,7 @@ from __future__ import annotations import io import os import warnings +from pathlib import Path import pytest @@ -14,7 +15,7 @@ from .helper import assert_image_equal, assert_image_similar_tofile, skip_unless TEST_FILE = "Tests/images/pillow.icns" -def test_sanity(): +def test_sanity() -> None: # Loading this icon by default should result in the largest size # (512x512@2x) being loaded with Image.open(TEST_FILE) as im: @@ -27,7 +28,7 @@ def test_sanity(): assert im.format == "ICNS" -def test_load(): +def test_load() -> None: with Image.open(TEST_FILE) as im: assert im.load()[0, 0] == (0, 0, 0, 0) @@ -35,7 +36,7 @@ def test_load(): assert im.load()[0, 0] == (0, 0, 0, 0) -def test_save(tmp_path): +def test_save(tmp_path: Path) -> None: temp_file = str(tmp_path / "temp.icns") with Image.open(TEST_FILE) as im: @@ -52,7 +53,7 @@ def test_save(tmp_path): assert _binary.i32be(fp.read(4)) == file_length -def test_save_append_images(tmp_path): +def test_save_append_images(tmp_path: Path) -> None: temp_file = str(tmp_path / "temp.icns") provided_im = Image.new("RGBA", (32, 32), (255, 0, 0, 128)) @@ -67,7 +68,7 @@ def test_save_append_images(tmp_path): assert_image_equal(reread, provided_im) -def test_save_fp(): +def test_save_fp() -> None: fp = io.BytesIO() with Image.open(TEST_FILE) as im: @@ -79,7 +80,7 @@ def test_save_fp(): assert reread.format == "ICNS" -def test_sizes(): +def test_sizes() -> None: # Check that we can load all of the sizes, and that the final pixel # dimensions are as expected with Image.open(TEST_FILE) as im: @@ -96,7 +97,7 @@ def test_sizes(): im.size = (1, 1) -def test_older_icon(): +def test_older_icon() -> None: # This icon was made with Icon Composer rather than iconutil; it still # uses PNG rather than JP2, however (since it was made on 10.9). with Image.open("Tests/images/pillow2.icns") as im: @@ -111,7 +112,7 @@ def test_older_icon(): @skip_unless_feature("jpg_2000") -def test_jp2_icon(): +def test_jp2_icon() -> None: # This icon uses JPEG 2000 images instead of the PNG images. # The advantage of doing this is that OS X 10.5 supports JPEG 2000 # but not PNG; some commercial software therefore does just this. @@ -127,7 +128,7 @@ def test_jp2_icon(): assert im2.size == (wr, hr) -def test_getimage(): +def test_getimage() -> None: with open(TEST_FILE, "rb") as fp: icns_file = IcnsImagePlugin.IcnsFile(fp) @@ -140,14 +141,14 @@ def test_getimage(): assert im.size == (512, 512) -def test_not_an_icns_file(): +def test_not_an_icns_file() -> None: with io.BytesIO(b"invalid\n") as fp: with pytest.raises(SyntaxError): IcnsImagePlugin.IcnsFile(fp) @skip_unless_feature("jpg_2000") -def test_icns_decompression_bomb(): +def test_icns_decompression_bomb() -> None: with Image.open( "Tests/images/oom-8ed3316a4109213ca96fb8a256a0bfefdece1461.icns" ) as im: diff --git a/Tests/test_file_ico.py b/Tests/test_file_ico.py index 99b3048d1..f69a290fa 100644 --- a/Tests/test_file_ico.py +++ b/Tests/test_file_ico.py @@ -2,6 +2,7 @@ from __future__ import annotations import io import os +from pathlib import Path import pytest @@ -12,7 +13,7 @@ from .helper import assert_image_equal, assert_image_equal_tofile, hopper TEST_ICO_FILE = "Tests/images/hopper.ico" -def test_sanity(): +def test_sanity() -> None: with Image.open(TEST_ICO_FILE) as im: im.load() assert im.mode == "RGBA" @@ -21,29 +22,29 @@ def test_sanity(): assert im.get_format_mimetype() == "image/x-icon" -def test_load(): +def test_load() -> None: with Image.open(TEST_ICO_FILE) as im: assert im.load()[0, 0] == (1, 1, 9, 255) -def test_mask(): +def test_mask() -> None: with Image.open("Tests/images/hopper_mask.ico") as im: assert_image_equal_tofile(im, "Tests/images/hopper_mask.png") -def test_black_and_white(): +def test_black_and_white() -> None: with Image.open("Tests/images/black_and_white.ico") as im: assert im.mode == "RGBA" assert im.size == (16, 16) -def test_invalid_file(): +def test_invalid_file() -> None: with open("Tests/images/flower.jpg", "rb") as fp: with pytest.raises(SyntaxError): IcoImagePlugin.IcoImageFile(fp) -def test_save_to_bytes(): +def test_save_to_bytes() -> None: output = io.BytesIO() im = hopper() im.save(output, "ico", sizes=[(32, 32), (64, 64)]) @@ -73,7 +74,7 @@ def test_save_to_bytes(): ) -def test_getpixel(tmp_path): +def test_getpixel(tmp_path: Path) -> None: temp_file = str(tmp_path / "temp.ico") im = hopper() @@ -86,7 +87,7 @@ def test_getpixel(tmp_path): assert reloaded.getpixel((0, 0)) == (18, 20, 62) -def test_no_duplicates(tmp_path): +def test_no_duplicates(tmp_path: Path) -> None: temp_file = str(tmp_path / "temp.ico") temp_file2 = str(tmp_path / "temp2.ico") @@ -100,7 +101,7 @@ def test_no_duplicates(tmp_path): assert os.path.getsize(temp_file) == os.path.getsize(temp_file2) -def test_different_bit_depths(tmp_path): +def test_different_bit_depths(tmp_path: Path) -> None: temp_file = str(tmp_path / "temp.ico") temp_file2 = str(tmp_path / "temp2.ico") @@ -134,7 +135,7 @@ def test_different_bit_depths(tmp_path): @pytest.mark.parametrize("mode", ("1", "L", "P", "RGB", "RGBA")) -def test_save_to_bytes_bmp(mode): +def test_save_to_bytes_bmp(mode: str) -> None: output = io.BytesIO() im = hopper(mode) im.save(output, "ico", bitmap_format="bmp", sizes=[(32, 32), (64, 64)]) @@ -162,13 +163,13 @@ def test_save_to_bytes_bmp(mode): assert_image_equal(reloaded, im) -def test_incorrect_size(): +def test_incorrect_size() -> None: with Image.open(TEST_ICO_FILE) as im: with pytest.raises(ValueError): im.size = (1, 1) -def test_save_256x256(tmp_path): +def test_save_256x256(tmp_path: Path) -> None: """Issue #2264 https://github.com/python-pillow/Pillow/issues/2264""" # Arrange with Image.open("Tests/images/hopper_256x256.ico") as im: @@ -181,7 +182,7 @@ def test_save_256x256(tmp_path): assert im_saved.size == (256, 256) -def test_only_save_relevant_sizes(tmp_path): +def test_only_save_relevant_sizes(tmp_path: Path) -> None: """Issue #2266 https://github.com/python-pillow/Pillow/issues/2266 Should save in 16x16, 24x24, 32x32, 48x48 sizes and not in 16x16, 24x24, 32x32, 48x48, 48x48, 48x48, 48x48 sizes @@ -197,7 +198,7 @@ def test_only_save_relevant_sizes(tmp_path): assert im_saved.info["sizes"] == {(16, 16), (24, 24), (32, 32), (48, 48)} -def test_save_append_images(tmp_path): +def test_save_append_images(tmp_path: Path) -> None: # append_images should be used for scaled down versions of the image im = hopper("RGBA") provided_im = Image.new("RGBA", (32, 32), (255, 0, 0)) @@ -211,7 +212,7 @@ def test_save_append_images(tmp_path): assert_image_equal(reread, provided_im) -def test_unexpected_size(): +def test_unexpected_size() -> None: # This image has been manually hexedited to state that it is 16x32 # while the image within is still 16x16 with pytest.warns(UserWarning): @@ -219,7 +220,7 @@ def test_unexpected_size(): assert im.size == (16, 16) -def test_draw_reloaded(tmp_path): +def test_draw_reloaded(tmp_path: Path) -> None: with Image.open(TEST_ICO_FILE) as im: outfile = str(tmp_path / "temp_saved_hopper_draw.ico") diff --git a/Tests/test_file_im.py b/Tests/test_file_im.py index a031b3e88..036965bf5 100644 --- a/Tests/test_file_im.py +++ b/Tests/test_file_im.py @@ -2,6 +2,7 @@ from __future__ import annotations import filecmp import warnings +from pathlib import Path import pytest @@ -13,7 +14,7 @@ from .helper import assert_image_equal_tofile, hopper, is_pypy TEST_IM = "Tests/images/hopper.im" -def test_sanity(): +def test_sanity() -> None: with Image.open(TEST_IM) as im: im.load() assert im.mode == "RGB" @@ -21,7 +22,7 @@ def test_sanity(): assert im.format == "IM" -def test_name_limit(tmp_path): +def test_name_limit(tmp_path: Path) -> None: out = str(tmp_path / ("name_limit_test" * 7 + ".im")) with Image.open(TEST_IM) as im: im.save(out) @@ -29,8 +30,8 @@ def test_name_limit(tmp_path): @pytest.mark.skipif(is_pypy(), reason="Requires CPython") -def test_unclosed_file(): - def open(): +def test_unclosed_file() -> None: + def open() -> None: im = Image.open(TEST_IM) im.load() @@ -38,20 +39,20 @@ def test_unclosed_file(): open() -def test_closed_file(): +def test_closed_file() -> None: with warnings.catch_warnings(): im = Image.open(TEST_IM) im.load() im.close() -def test_context_manager(): +def test_context_manager() -> None: with warnings.catch_warnings(): with Image.open(TEST_IM) as im: im.load() -def test_tell(): +def test_tell() -> None: # Arrange with Image.open(TEST_IM) as im: # Act @@ -61,13 +62,13 @@ def test_tell(): assert frame == 0 -def test_n_frames(): +def test_n_frames() -> None: with Image.open(TEST_IM) as im: assert im.n_frames == 1 assert not im.is_animated -def test_eoferror(): +def test_eoferror() -> None: with Image.open(TEST_IM) as im: n_frames = im.n_frames @@ -81,14 +82,14 @@ def test_eoferror(): @pytest.mark.parametrize("mode", ("RGB", "P", "PA")) -def test_roundtrip(mode, tmp_path): +def test_roundtrip(mode: str, tmp_path: Path) -> None: out = str(tmp_path / "temp.im") im = hopper(mode) im.save(out) assert_image_equal_tofile(im, out) -def test_small_palette(tmp_path): +def test_small_palette(tmp_path: Path) -> None: im = Image.new("P", (1, 1)) colors = [0, 1, 2] im.putpalette(colors) @@ -100,19 +101,19 @@ def test_small_palette(tmp_path): assert reloaded.getpalette() == colors + [0] * 765 -def test_save_unsupported_mode(tmp_path): +def test_save_unsupported_mode(tmp_path: Path) -> None: out = str(tmp_path / "temp.im") im = hopper("HSV") with pytest.raises(ValueError): im.save(out) -def test_invalid_file(): +def test_invalid_file() -> None: invalid_file = "Tests/images/flower.jpg" with pytest.raises(SyntaxError): ImImagePlugin.ImImageFile(invalid_file) -def test_number(): +def test_number() -> None: assert ImImagePlugin.number("1.2") == 1.2 diff --git a/Tests/test_file_imt.py b/Tests/test_file_imt.py index aa13d4407..6957dfa0a 100644 --- a/Tests/test_file_imt.py +++ b/Tests/test_file_imt.py @@ -9,13 +9,13 @@ from PIL import Image, ImtImagePlugin from .helper import assert_image_equal_tofile -def test_sanity(): +def test_sanity() -> None: with Image.open("Tests/images/bw_gradient.imt") as im: assert_image_equal_tofile(im, "Tests/images/bw_gradient.png") @pytest.mark.parametrize("data", (b"\n", b"\n-", b"width 1\n")) -def test_invalid_file(data): +def test_invalid_file(data: bytes) -> None: with io.BytesIO(data) as fp: with pytest.raises(SyntaxError): ImtImagePlugin.ImtImageFile(fp) diff --git a/Tests/test_file_iptc.py b/Tests/test_file_iptc.py index a2c50ecef..88c30d468 100644 --- a/Tests/test_file_iptc.py +++ b/Tests/test_file_iptc.py @@ -12,7 +12,7 @@ from .helper import assert_image_equal, hopper TEST_FILE = "Tests/images/iptc.jpg" -def test_open(): +def test_open() -> None: expected = Image.new("L", (1, 1)) f = BytesIO( @@ -24,7 +24,7 @@ def test_open(): assert_image_equal(im, expected) -def test_getiptcinfo_jpg_none(): +def test_getiptcinfo_jpg_none() -> None: # Arrange with hopper() as im: # Act @@ -34,7 +34,7 @@ def test_getiptcinfo_jpg_none(): assert iptc is None -def test_getiptcinfo_jpg_found(): +def test_getiptcinfo_jpg_found() -> None: # Arrange with Image.open(TEST_FILE) as im: # Act @@ -46,7 +46,7 @@ def test_getiptcinfo_jpg_found(): assert iptc[(2, 101)] == b"Hungary" -def test_getiptcinfo_fotostation(): +def test_getiptcinfo_fotostation() -> None: # Arrange with open(TEST_FILE, "rb") as fp: data = bytearray(fp.read()) @@ -63,7 +63,7 @@ def test_getiptcinfo_fotostation(): pytest.fail("FotoStation tag not found") -def test_getiptcinfo_zero_padding(): +def test_getiptcinfo_zero_padding() -> None: # Arrange with Image.open(TEST_FILE) as im: im.info["photoshop"][0x0404] += b"\x00\x00\x00" @@ -76,7 +76,7 @@ def test_getiptcinfo_zero_padding(): assert len(iptc) == 3 -def test_getiptcinfo_tiff_none(): +def test_getiptcinfo_tiff_none() -> None: # Arrange with Image.open("Tests/images/hopper.tif") as im: # Act @@ -86,7 +86,7 @@ def test_getiptcinfo_tiff_none(): assert iptc is None -def test_i(): +def test_i() -> None: # Arrange c = b"a" @@ -98,7 +98,7 @@ def test_i(): assert ret == 97 -def test_dump(monkeypatch): +def test_dump(monkeypatch: pytest.MonkeyPatch) -> None: # Arrange c = b"abc" # Temporarily redirect stdout @@ -113,6 +113,6 @@ def test_dump(monkeypatch): assert mystdout.getvalue() == "61 62 63 \n" -def test_pad_deprecation(): +def test_pad_deprecation() -> None: with pytest.warns(DeprecationWarning): assert IptcImagePlugin.PAD == b"\0\0\0\0" diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 232e51f91..33f845402 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -4,6 +4,9 @@ import os import re import warnings from io import BytesIO +from pathlib import Path +from types import ModuleType +from typing import Any, cast import pytest @@ -31,6 +34,7 @@ from .helper import ( skip_unless_feature, ) +ElementTree: ModuleType | None try: from defusedxml import ElementTree except ImportError: @@ -41,16 +45,22 @@ TEST_FILE = "Tests/images/hopper.jpg" @skip_unless_feature("jpg") class TestFileJpeg: - def roundtrip(self, im, **options): + def roundtrip_with_bytes( + self, im: Image.Image, **options: Any + ) -> tuple[JpegImagePlugin.JpegImageFile, int]: out = BytesIO() im.save(out, "JPEG", **options) test_bytes = out.tell() out.seek(0) - im = Image.open(out) - im.bytes = test_bytes # for testing only - return im + reloaded = cast(JpegImagePlugin.JpegImageFile, Image.open(out)) + return reloaded, test_bytes - def gen_random_image(self, size, mode="RGB"): + def roundtrip( + self, im: Image.Image, **options: Any + ) -> JpegImagePlugin.JpegImageFile: + return self.roundtrip_with_bytes(im, **options)[0] + + def gen_random_image(self, size: tuple[int, int], mode: str = "RGB") -> Image.Image: """Generates a very hard to compress file :param size: tuple :param mode: optional image mode @@ -58,7 +68,7 @@ class TestFileJpeg: """ return Image.frombytes(mode, size, os.urandom(size[0] * size[1] * len(mode))) - def test_sanity(self): + def test_sanity(self) -> None: # internal version number assert re.search(r"\d+\.\d+$", features.version_codec("jpg")) @@ -70,13 +80,13 @@ class TestFileJpeg: assert im.get_format_mimetype() == "image/jpeg" @pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0))) - def test_zero(self, size, tmp_path): + def test_zero(self, size: tuple[int, int], tmp_path: Path) -> None: f = str(tmp_path / "temp.jpg") im = Image.new("RGB", size) with pytest.raises(ValueError): im.save(f) - def test_app(self): + def test_app(self) -> None: # Test APP/COM reader (@PIL135) with Image.open(TEST_FILE) as im: assert im.applist[0] == ("APP0", b"JFIF\x00\x01\x01\x01\x00`\x00`\x00\x00") @@ -89,7 +99,7 @@ class TestFileJpeg: assert im.info["comment"] == b"File written by Adobe Photoshop\xa8 4.0\x00" assert im.app["COM"] == im.info["comment"] - def test_comment_write(self): + def test_comment_write(self) -> None: with Image.open(TEST_FILE) as im: assert im.info["comment"] == b"File written by Adobe Photoshop\xa8 4.0\x00" @@ -107,15 +117,13 @@ class TestFileJpeg: assert "comment" not in reloaded.info # Test that a comment argument overrides the default comment - for comment in ("Test comment text", b"Text comment text"): + for comment in ("Test comment text", b"Test comment text"): out = BytesIO() im.save(out, format="JPEG", comment=comment) with Image.open(out) as reloaded: - if not isinstance(comment, bytes): - comment = comment.encode() - assert reloaded.info["comment"] == comment + assert reloaded.info["comment"] == b"Test comment text" - def test_cmyk(self): + def test_cmyk(self) -> None: # Test CMYK handling. Thanks to Tim and Charlie for test data, # Michael for getting me to look one more time. f = "Tests/images/pil_sample_cmyk.jpg" @@ -143,8 +151,8 @@ class TestFileJpeg: ) assert k > 0.9 - def test_rgb(self): - def getchannels(im): + def test_rgb(self) -> None: + def getchannels(im: Image.Image) -> tuple[int, int, int]: return tuple(v[0] for v in im.layer) im = hopper() @@ -160,8 +168,8 @@ class TestFileJpeg: "test_image_path", [TEST_FILE, "Tests/images/pil_sample_cmyk.jpg"], ) - def test_dpi(self, test_image_path): - def test(xdpi, ydpi=None): + def test_dpi(self, test_image_path: str) -> None: + def test(xdpi: int, ydpi: int | None = None): with Image.open(test_image_path) as im: im = self.roundtrip(im, dpi=(xdpi, ydpi or xdpi)) return im.info.get("dpi") @@ -174,7 +182,7 @@ class TestFileJpeg: @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) - def test_icc(self, tmp_path): + def test_icc(self, tmp_path: Path) -> None: # Test ICC support with Image.open("Tests/images/rgb.jpg") as im1: icc_profile = im1.info["icc_profile"] @@ -206,7 +214,7 @@ class TestFileJpeg: ImageFile.MAXBLOCK * 4 + 3, # large block ), ) - def test_icc_big(self, n): + def test_icc_big(self, n: int) -> None: # Make sure that the "extra" support handles large blocks # The ICC APP marker can store 65519 bytes per marker, so # using a 4-byte test code should allow us to detect out of @@ -219,7 +227,7 @@ class TestFileJpeg: @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) - def test_large_icc_meta(self, tmp_path): + def test_large_icc_meta(self, tmp_path: Path) -> None: # https://github.com/python-pillow/Pillow/issues/148 # Sometimes the meta data on the icc_profile block is bigger than # Image.MAXBLOCK or the image size. @@ -243,52 +251,52 @@ class TestFileJpeg: f = str(tmp_path / "temp3.jpg") im.save(f, progressive=True, quality=94, exif=b" " * 43668) - def test_optimize(self): - im1 = self.roundtrip(hopper()) - im2 = self.roundtrip(hopper(), optimize=0) - im3 = self.roundtrip(hopper(), optimize=1) + def test_optimize(self) -> None: + im1, im1_bytes = self.roundtrip_with_bytes(hopper()) + im2, im2_bytes = self.roundtrip_with_bytes(hopper(), optimize=0) + im3, im3_bytes = self.roundtrip_with_bytes(hopper(), optimize=1) assert_image_equal(im1, im2) assert_image_equal(im1, im3) - assert im1.bytes >= im2.bytes - assert im1.bytes >= im3.bytes + assert im1_bytes >= im2_bytes + assert im1_bytes >= im3_bytes - def test_optimize_large_buffer(self, tmp_path): + def test_optimize_large_buffer(self, tmp_path: Path) -> None: # https://github.com/python-pillow/Pillow/issues/148 f = str(tmp_path / "temp.jpg") # this requires ~ 1.5x Image.MAXBLOCK im = Image.new("RGB", (4096, 4096), 0xFF3333) im.save(f, format="JPEG", optimize=True) - def test_progressive(self): - im1 = self.roundtrip(hopper()) + def test_progressive(self) -> None: + im1, im1_bytes = self.roundtrip_with_bytes(hopper()) im2 = self.roundtrip(hopper(), progressive=False) - im3 = self.roundtrip(hopper(), progressive=True) + im3, im3_bytes = self.roundtrip_with_bytes(hopper(), progressive=True) assert not im1.info.get("progressive") assert not im2.info.get("progressive") assert im3.info.get("progressive") assert_image_equal(im1, im3) - assert im1.bytes >= im3.bytes + assert im1_bytes >= im3_bytes - def test_progressive_large_buffer(self, tmp_path): + def test_progressive_large_buffer(self, tmp_path: Path) -> None: f = str(tmp_path / "temp.jpg") # this requires ~ 1.5x Image.MAXBLOCK im = Image.new("RGB", (4096, 4096), 0xFF3333) im.save(f, format="JPEG", progressive=True) - def test_progressive_large_buffer_highest_quality(self, tmp_path): + def test_progressive_large_buffer_highest_quality(self, tmp_path: Path) -> None: f = str(tmp_path / "temp.jpg") im = self.gen_random_image((255, 255)) # this requires more bytes than pixels in the image im.save(f, format="JPEG", progressive=True, quality=100) - def test_progressive_cmyk_buffer(self): + def test_progressive_cmyk_buffer(self) -> None: # Issue 2272, quality 90 cmyk image is tripping the large buffer bug. f = BytesIO() im = self.gen_random_image((256, 256), "CMYK") im.save(f, format="JPEG", progressive=True, quality=94) - def test_large_exif(self, tmp_path): + def test_large_exif(self, tmp_path: Path) -> None: # https://github.com/python-pillow/Pillow/issues/148 f = str(tmp_path / "temp.jpg") im = hopper() @@ -297,12 +305,12 @@ class TestFileJpeg: with pytest.raises(ValueError): im.save(f, "JPEG", quality=90, exif=b"1" * 65534) - def test_exif_typeerror(self): + def test_exif_typeerror(self) -> None: with Image.open("Tests/images/exif_typeerror.jpg") as im: # Should not raise a TypeError im._getexif() - def test_exif_gps(self, tmp_path): + def test_exif_gps(self, tmp_path: Path) -> None: expected_exif_gps = { 0: b"\x00\x00\x00\x01", 2: 4294967295, @@ -327,7 +335,7 @@ class TestFileJpeg: exif = reloaded._getexif() assert exif[gps_index] == expected_exif_gps - def test_empty_exif_gps(self): + def test_empty_exif_gps(self) -> None: with Image.open("Tests/images/empty_gps_ifd.jpg") as im: exif = im.getexif() del exif[0x8769] @@ -339,13 +347,14 @@ class TestFileJpeg: assert exif.get_ifd(0x8825) == {} transposed = ImageOps.exif_transpose(im) + assert transposed is not None exif = transposed.getexif() assert exif.get_ifd(0x8825) == {} # Assert that it was transposed assert 0x0112 not in exif - def test_exif_equality(self): + def test_exif_equality(self) -> None: # In 7.2.0, Exif rationals were changed to be read as # TiffImagePlugin.IFDRational. This class had a bug in __eq__, # breaking the self-equality of Exif data @@ -355,7 +364,7 @@ class TestFileJpeg: exifs.append(im._getexif()) assert exifs[0] == exifs[1] - def test_exif_rollback(self): + def test_exif_rollback(self) -> None: # rolling back exif support in 3.1 to pre-3.0 formatting. # expected from 2.9, with b/u qualifiers switched for 3.2 compatibility # this test passes on 2.9 and 3.1, but not 3.0 @@ -390,12 +399,12 @@ class TestFileJpeg: for tag, value in expected_exif.items(): assert value == exif[tag] - def test_exif_gps_typeerror(self): + def test_exif_gps_typeerror(self) -> None: with Image.open("Tests/images/exif_gps_typeerror.jpg") as im: # Should not raise a TypeError im._getexif() - def test_progressive_compat(self): + def test_progressive_compat(self) -> None: im1 = self.roundtrip(hopper()) assert not im1.info.get("progressive") assert not im1.info.get("progression") @@ -416,23 +425,23 @@ class TestFileJpeg: assert im3.info.get("progressive") assert im3.info.get("progression") - def test_quality(self): - im1 = self.roundtrip(hopper()) - im2 = self.roundtrip(hopper(), quality=50) + def test_quality(self) -> None: + im1, im1_bytes = self.roundtrip_with_bytes(hopper()) + im2, im2_bytes = self.roundtrip_with_bytes(hopper(), quality=50) assert_image(im1, im2.mode, im2.size) - assert im1.bytes >= im2.bytes + assert im1_bytes >= im2_bytes - im3 = self.roundtrip(hopper(), quality=0) + im3, im3_bytes = self.roundtrip_with_bytes(hopper(), quality=0) assert_image(im1, im3.mode, im3.size) - assert im2.bytes > im3.bytes + assert im2_bytes > im3_bytes - def test_smooth(self): + def test_smooth(self) -> None: im1 = self.roundtrip(hopper()) im2 = self.roundtrip(hopper(), smooth=100) assert_image(im1, im2.mode, im2.size) - def test_subsampling(self): - def getsampling(im): + def test_subsampling(self) -> None: + def getsampling(im: Image.Image): layer = im.layer return layer[0][1:3] + layer[1][1:3] + layer[2][1:3] @@ -440,46 +449,46 @@ class TestFileJpeg: for subsampling in (-1, 3): # (default, invalid) im = self.roundtrip(hopper(), subsampling=subsampling) assert getsampling(im) == (2, 2, 1, 1, 1, 1) - for subsampling in (0, "4:4:4"): - im = self.roundtrip(hopper(), subsampling=subsampling) + for subsampling1 in (0, "4:4:4"): + im = self.roundtrip(hopper(), subsampling=subsampling1) assert getsampling(im) == (1, 1, 1, 1, 1, 1) - for subsampling in (1, "4:2:2"): - im = self.roundtrip(hopper(), subsampling=subsampling) + for subsampling1 in (1, "4:2:2"): + im = self.roundtrip(hopper(), subsampling=subsampling1) assert getsampling(im) == (2, 1, 1, 1, 1, 1) - for subsampling in (2, "4:2:0", "4:1:1"): - im = self.roundtrip(hopper(), subsampling=subsampling) + for subsampling1 in (2, "4:2:0", "4:1:1"): + im = self.roundtrip(hopper(), subsampling=subsampling1) assert getsampling(im) == (2, 2, 1, 1, 1, 1) # RGB colorspace - for subsampling in (-1, 0, "4:4:4"): + for subsampling1 in (-1, 0, "4:4:4"): # "4:4:4" doesn't really make sense for RGB, but the conversion # to an integer happens at a higher level - im = self.roundtrip(hopper(), keep_rgb=True, subsampling=subsampling) + im = self.roundtrip(hopper(), keep_rgb=True, subsampling=subsampling1) assert getsampling(im) == (1, 1, 1, 1, 1, 1) - for subsampling in (1, "4:2:2", 2, "4:2:0", 3): + for subsampling1 in (1, "4:2:2", 2, "4:2:0", 3): with pytest.raises(OSError): - self.roundtrip(hopper(), keep_rgb=True, subsampling=subsampling) + self.roundtrip(hopper(), keep_rgb=True, subsampling=subsampling1) with pytest.raises(TypeError): self.roundtrip(hopper(), subsampling="1:1:1") - def test_exif(self): + def test_exif(self) -> None: with Image.open("Tests/images/pil_sample_rgb.jpg") as im: info = im._getexif() assert info[305] == "Adobe Photoshop CS Macintosh" - def test_get_child_images(self): + def test_get_child_images(self) -> None: with Image.open("Tests/images/flower.jpg") as im: ims = im.get_child_images() assert len(ims) == 1 assert_image_similar_tofile(ims[0], "Tests/images/flower_thumbnail.png", 2.1) - def test_mp(self): + def test_mp(self) -> None: with Image.open("Tests/images/pil_sample_rgb.jpg") as im: assert im._getmp() is None - def test_quality_keep(self, tmp_path): + def test_quality_keep(self, tmp_path: Path) -> None: # RGB with Image.open("Tests/images/hopper.jpg") as im: f = str(tmp_path / "temp.jpg") @@ -493,13 +502,13 @@ class TestFileJpeg: f = str(tmp_path / "temp.jpg") im.save(f, quality="keep") - def test_junk_jpeg_header(self): + def test_junk_jpeg_header(self) -> None: # https://github.com/python-pillow/Pillow/issues/630 filename = "Tests/images/junk_jpeg_header.jpg" with Image.open(filename): pass - def test_ff00_jpeg_header(self): + def test_ff00_jpeg_header(self) -> None: filename = "Tests/images/jpeg_ff00_header.jpg" with Image.open(filename): pass @@ -507,7 +516,7 @@ class TestFileJpeg: @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) - def test_truncated_jpeg_should_read_all_the_data(self): + def test_truncated_jpeg_should_read_all_the_data(self) -> None: filename = "Tests/images/truncated_jpeg.jpg" ImageFile.LOAD_TRUNCATED_IMAGES = True with Image.open(filename) as im: @@ -515,7 +524,7 @@ class TestFileJpeg: ImageFile.LOAD_TRUNCATED_IMAGES = False assert im.getbbox() is not None - def test_truncated_jpeg_throws_oserror(self): + def test_truncated_jpeg_throws_oserror(self) -> None: filename = "Tests/images/truncated_jpeg.jpg" with Image.open(filename) as im: with pytest.raises(OSError): @@ -528,8 +537,8 @@ class TestFileJpeg: @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) - def test_qtables(self, tmp_path): - def _n_qtables_helper(n, test_file): + def test_qtables(self, tmp_path: Path) -> None: + def _n_qtables_helper(n: int, test_file: str) -> None: with Image.open(test_file) as im: f = str(tmp_path / "temp.jpg") im.save(f, qtables=[[n] * 64] * n) @@ -637,24 +646,24 @@ class TestFileJpeg: with pytest.raises(ValueError): self.roundtrip(im, qtables=[[1, 2, 3, 4]]) - def test_load_16bit_qtables(self): + def test_load_16bit_qtables(self) -> None: with Image.open("Tests/images/hopper_16bit_qtables.jpg") as im: assert len(im.quantization) == 2 assert len(im.quantization[0]) == 64 assert max(im.quantization[0]) > 255 - def test_save_multiple_16bit_qtables(self): + def test_save_multiple_16bit_qtables(self) -> None: with Image.open("Tests/images/hopper_16bit_qtables.jpg") as im: im2 = self.roundtrip(im, qtables="keep") assert im.quantization == im2.quantization - def test_save_single_16bit_qtable(self): + def test_save_single_16bit_qtable(self) -> None: with Image.open("Tests/images/hopper_16bit_qtables.jpg") as im: im2 = self.roundtrip(im, qtables={0: im.quantization[0]}) assert len(im2.quantization) == 1 assert im2.quantization[0] == im.quantization[0] - def test_save_low_quality_baseline_qtables(self): + def test_save_low_quality_baseline_qtables(self) -> None: with Image.open(TEST_FILE) as im: im2 = self.roundtrip(im, quality=10) assert len(im2.quantization) == 2 @@ -665,7 +674,7 @@ class TestFileJpeg: "blocks, rows, markers", ((0, 0, 0), (1, 0, 15), (3, 0, 5), (8, 0, 1), (0, 1, 3), (0, 2, 1)), ) - def test_restart_markers(self, blocks, rows, markers): + def test_restart_markers(self, blocks: int, rows: int, markers: int) -> None: im = Image.new("RGB", (32, 32)) # 16 MCUs out = BytesIO() im.save( @@ -679,20 +688,20 @@ class TestFileJpeg: assert len(re.findall(b"\xff[\xd0-\xd7]", out.getvalue())) == markers @pytest.mark.skipif(not djpeg_available(), reason="djpeg not available") - def test_load_djpeg(self): + def test_load_djpeg(self) -> None: with Image.open(TEST_FILE) as img: img.load_djpeg() assert_image_similar_tofile(img, TEST_FILE, 5) @pytest.mark.skipif(not cjpeg_available(), reason="cjpeg not available") - def test_save_cjpeg(self, tmp_path): + def test_save_cjpeg(self, tmp_path: Path) -> None: with Image.open(TEST_FILE) as img: tempfile = str(tmp_path / "temp.jpg") JpegImagePlugin._save_cjpeg(img, 0, tempfile) # Default save quality is 75%, so a tiny bit of difference is alright assert_image_similar_tofile(img, tempfile, 17) - def test_no_duplicate_0x1001_tag(self): + def test_no_duplicate_0x1001_tag(self) -> None: # Arrange tag_ids = {v: k for k, v in ExifTags.TAGS.items()} @@ -700,7 +709,7 @@ class TestFileJpeg: assert tag_ids["RelatedImageWidth"] == 0x1001 assert tag_ids["RelatedImageLength"] == 0x1002 - def test_MAXBLOCK_scaling(self, tmp_path): + def test_MAXBLOCK_scaling(self, tmp_path: Path) -> None: im = self.gen_random_image((512, 512)) f = str(tmp_path / "temp.jpeg") im.save(f, quality=100, optimize=True) @@ -711,7 +720,7 @@ class TestFileJpeg: reloaded.save(f, quality="keep", progressive=True) reloaded.save(f, quality="keep", optimize=True) - def test_bad_mpo_header(self): + def test_bad_mpo_header(self) -> None: """Treat unknown MPO as JPEG""" # Arrange @@ -723,20 +732,20 @@ class TestFileJpeg: assert im.format == "JPEG" @pytest.mark.parametrize("mode", ("1", "L", "RGB", "RGBX", "CMYK", "YCbCr")) - def test_save_correct_modes(self, mode): + def test_save_correct_modes(self, mode: str) -> None: out = BytesIO() img = Image.new(mode, (20, 20)) img.save(out, "JPEG") @pytest.mark.parametrize("mode", ("LA", "La", "RGBA", "RGBa", "P")) - def test_save_wrong_modes(self, mode): + def test_save_wrong_modes(self, mode: str) -> None: # ref https://github.com/python-pillow/Pillow/issues/2005 out = BytesIO() img = Image.new(mode, (20, 20)) with pytest.raises(OSError): img.save(out, "JPEG") - def test_save_tiff_with_dpi(self, tmp_path): + def test_save_tiff_with_dpi(self, tmp_path: Path) -> None: # Arrange outfile = str(tmp_path / "temp.tif") with Image.open("Tests/images/hopper.tif") as im: @@ -748,7 +757,7 @@ class TestFileJpeg: reloaded.load() assert im.info["dpi"] == reloaded.info["dpi"] - def test_save_dpi_rounding(self, tmp_path): + def test_save_dpi_rounding(self, tmp_path: Path) -> None: outfile = str(tmp_path / "temp.jpg") with Image.open("Tests/images/hopper.jpg") as im: im.save(outfile, dpi=(72.2, 72.2)) @@ -761,7 +770,7 @@ class TestFileJpeg: with Image.open(outfile) as reloaded: assert reloaded.info["dpi"] == (73, 73) - def test_dpi_tuple_from_exif(self): + def test_dpi_tuple_from_exif(self) -> None: # Arrange # This Photoshop CC 2017 image has DPI in EXIF not metadata # EXIF XResolution is (2000000, 10000) @@ -769,7 +778,7 @@ class TestFileJpeg: # Act / Assert assert im.info.get("dpi") == (200, 200) - def test_dpi_int_from_exif(self): + def test_dpi_int_from_exif(self) -> None: # Arrange # This image has DPI in EXIF not metadata # EXIF XResolution is 72 @@ -777,7 +786,7 @@ class TestFileJpeg: # Act / Assert assert im.info.get("dpi") == (72, 72) - def test_dpi_from_dpcm_exif(self): + def test_dpi_from_dpcm_exif(self) -> None: # Arrange # This is photoshop-200dpi.jpg with EXIF resolution unit set to cm: # exiftool -exif:ResolutionUnit=cm photoshop-200dpi.jpg @@ -785,7 +794,7 @@ class TestFileJpeg: # Act / Assert assert im.info.get("dpi") == (508, 508) - def test_dpi_exif_zero_division(self): + def test_dpi_exif_zero_division(self) -> None: # Arrange # This is photoshop-200dpi.jpg with EXIF resolution set to 0/0: # exiftool -XResolution=0/0 -YResolution=0/0 photoshop-200dpi.jpg @@ -794,7 +803,7 @@ class TestFileJpeg: # This should return the default, and not raise a ZeroDivisionError assert im.info.get("dpi") == (72, 72) - def test_dpi_exif_string(self): + def test_dpi_exif_string(self) -> None: # Arrange # 0x011A tag in this exif contains string '300300\x02' with Image.open("Tests/images/broken_exif_dpi.jpg") as im: @@ -802,14 +811,14 @@ class TestFileJpeg: # This should return the default assert im.info.get("dpi") == (72, 72) - def test_dpi_exif_truncated(self): + def test_dpi_exif_truncated(self) -> None: # Arrange with Image.open("Tests/images/truncated_exif_dpi.jpg") as im: # Act / Assert # This should return the default assert im.info.get("dpi") == (72, 72) - def test_no_dpi_in_exif(self): + def test_no_dpi_in_exif(self) -> None: # Arrange # This is photoshop-200dpi.jpg with resolution removed from EXIF: # exiftool "-*resolution*"= photoshop-200dpi.jpg @@ -819,7 +828,7 @@ class TestFileJpeg: # https://exiv2.org/tags.html assert im.info.get("dpi") == (72, 72) - def test_invalid_exif(self): + def test_invalid_exif(self) -> None: # This is no-dpi-in-exif with the tiff header of the exif block # hexedited from MM * to FF FF FF FF with Image.open("Tests/images/invalid-exif.jpg") as im: @@ -830,7 +839,7 @@ class TestFileJpeg: @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) - def test_exif_x_resolution(self, tmp_path): + def test_exif_x_resolution(self, tmp_path: Path) -> None: with Image.open("Tests/images/flower.jpg") as im: exif = im.getexif() assert exif[282] == 180 @@ -842,14 +851,14 @@ class TestFileJpeg: with Image.open(out) as reloaded: assert reloaded.getexif()[282] == 180 - def test_invalid_exif_x_resolution(self): + def test_invalid_exif_x_resolution(self) -> None: # When no x or y resolution is defined in EXIF with Image.open("Tests/images/invalid-exif-without-x-resolution.jpg") as im: # This should return the default, and not a ValueError or # OSError for an unidentified image. assert im.info.get("dpi") == (72, 72) - def test_ifd_offset_exif(self): + def test_ifd_offset_exif(self) -> None: # Arrange # This image has been manually hexedited to have an IFD offset of 10, # in contrast to normal 8 @@ -857,14 +866,14 @@ class TestFileJpeg: # Act / Assert assert im._getexif()[306] == "2017:03:13 23:03:09" - def test_multiple_exif(self): + def test_multiple_exif(self) -> None: with Image.open("Tests/images/multiple_exif.jpg") as im: assert im.info["exif"] == b"Exif\x00\x00firstsecond" @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) - def test_photoshop(self): + def test_photoshop(self) -> None: with Image.open("Tests/images/photoshop-200dpi.jpg") as im: assert im.info["photoshop"][0x03ED] == { "XResolution": 200.0, @@ -881,14 +890,14 @@ class TestFileJpeg: with Image.open("Tests/images/app13.jpg") as im: assert "photoshop" not in im.info - def test_photoshop_malformed_and_multiple(self): + def test_photoshop_malformed_and_multiple(self) -> None: with Image.open("Tests/images/app13-multiple.jpg") as im: assert "photoshop" in im.info assert 24 == len(im.info["photoshop"]) apps_13_lengths = [len(v) for k, v in im.applist if k == "APP13"] assert [65504, 24] == apps_13_lengths - def test_adobe_transform(self): + def test_adobe_transform(self) -> None: with Image.open("Tests/images/pil_sample_rgb.jpg") as im: assert im.info["adobe_transform"] == 1 @@ -902,11 +911,11 @@ class TestFileJpeg: assert "adobe" in im.info assert "adobe_transform" not in im.info - def test_icc_after_SOF(self): + def test_icc_after_SOF(self) -> None: with Image.open("Tests/images/icc-after-SOF.jpg") as im: assert im.info["icc_profile"] == b"profile" - def test_jpeg_magic_number(self): + def test_jpeg_magic_number(self) -> None: size = 4097 buffer = BytesIO(b"\xFF" * size) # Many xFF bytes buffer.max_pos = 0 @@ -925,7 +934,7 @@ class TestFileJpeg: # Assert the entire file has not been read assert 0 < buffer.max_pos < size - def test_getxmp(self): + def test_getxmp(self) -> None: with Image.open("Tests/images/xmp_test.jpg") as im: if ElementTree is None: with pytest.warns( @@ -954,7 +963,7 @@ class TestFileJpeg: with Image.open("Tests/images/hopper.jpg") as im: assert im.getxmp() == {} - def test_getxmp_no_prefix(self): + def test_getxmp_no_prefix(self) -> None: with Image.open("Tests/images/xmp_no_prefix.jpg") as im: if ElementTree is None: with pytest.warns( @@ -965,7 +974,7 @@ class TestFileJpeg: else: assert im.getxmp() == {"xmpmeta": {"key": "value"}} - def test_getxmp_padded(self): + def test_getxmp_padded(self) -> None: with Image.open("Tests/images/xmp_padded.jpg") as im: if ElementTree is None: with pytest.warns( @@ -977,16 +986,16 @@ class TestFileJpeg: assert im.getxmp() == {"xmpmeta": None} @pytest.mark.timeout(timeout=1) - def test_eof(self): + def test_eof(self) -> None: # Even though this decoder never says that it is finished # the image should still end when there is no new data class InfiniteMockPyDecoder(ImageFile.PyDecoder): - def decode(self, buffer): + def decode(self, buffer: bytes) -> tuple[int, int]: return 0, 0 decoder = InfiniteMockPyDecoder(None) - def closure(mode, *args): + def closure(mode: str, *args) -> InfiniteMockPyDecoder: decoder.__init__(mode, *args) return decoder @@ -1000,7 +1009,7 @@ class TestFileJpeg: im.load() ImageFile.LOAD_TRUNCATED_IMAGES = False - def test_separate_tables(self): + def test_separate_tables(self) -> None: im = hopper() data = [] # [interchange, tables-only, image-only] for streamtype in range(3): @@ -1022,14 +1031,14 @@ class TestFileJpeg: with Image.open(BytesIO(data[1] + data[2])) as combined_im: assert_image_equal(interchange_im, combined_im) - def test_repr_jpeg(self): + def test_repr_jpeg(self) -> None: im = hopper() with Image.open(BytesIO(im._repr_jpeg_())) as repr_jpeg: assert repr_jpeg.format == "JPEG" assert_image_similar(im, repr_jpeg, 17) - def test_repr_jpeg_error_returns_none(self): + def test_repr_jpeg_error_returns_none(self) -> None: im = hopper("F") assert im._repr_jpeg_() is None @@ -1038,7 +1047,7 @@ class TestFileJpeg: @pytest.mark.skipif(not is_win32(), reason="Windows only") @skip_unless_feature("jpg") class TestFileCloseW32: - def test_fd_leak(self, tmp_path): + def test_fd_leak(self, tmp_path: Path) -> None: tmpfile = str(tmp_path / "temp.jpg") with Image.open("Tests/images/hopper.jpg") as im: diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index 94b02c9ff..b7f8350c7 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -3,6 +3,8 @@ from __future__ import annotations import os import re from io import BytesIO +from pathlib import Path +from typing import Any import pytest @@ -35,18 +37,16 @@ test_card.load() # 'Not enough memory to handle tile data' -def roundtrip(im, **options): +def roundtrip(im: Image.Image, **options: Any) -> Image.Image: out = BytesIO() im.save(out, "JPEG2000", **options) - test_bytes = out.tell() out.seek(0) with Image.open(out) as im: - im.bytes = test_bytes # for testing only im.load() return im -def test_sanity(): +def test_sanity() -> None: # Internal version number assert re.search(r"\d+\.\d+\.\d+$", features.version_codec("jpg_2000")) @@ -59,30 +59,32 @@ def test_sanity(): assert im.get_format_mimetype() == "image/jp2" -def test_jpf(): +def test_jpf() -> None: with Image.open("Tests/images/balloon.jpf") as im: assert im.format == "JPEG2000" assert im.get_format_mimetype() == "image/jpx" -def test_invalid_file(): +def test_invalid_file() -> None: invalid_file = "Tests/images/flower.jpg" with pytest.raises(SyntaxError): Jpeg2KImagePlugin.Jpeg2KImageFile(invalid_file) -def test_bytesio(): +def test_bytesio() -> None: with open("Tests/images/test-card-lossless.jp2", "rb") as f: data = BytesIO(f.read()) - assert_image_similar_tofile(test_card, data, 1.0e-3) + with Image.open(data) as im: + im.load() + assert_image_similar(im, test_card, 1.0e-3) # These two test pre-written JPEG 2000 files that were not written with # PIL (they were made using Adobe Photoshop) -def test_lossless(tmp_path): +def test_lossless(tmp_path: Path) -> None: with Image.open("Tests/images/test-card-lossless.jp2") as im: im.load() outfile = str(tmp_path / "temp_test-card.png") @@ -90,54 +92,54 @@ def test_lossless(tmp_path): assert_image_similar(im, test_card, 1.0e-3) -def test_lossy_tiled(): +def test_lossy_tiled() -> None: assert_image_similar_tofile( test_card, "Tests/images/test-card-lossy-tiled.jp2", 2.0 ) -def test_lossless_rt(): +def test_lossless_rt() -> None: im = roundtrip(test_card) assert_image_equal(im, test_card) -def test_lossy_rt(): +def test_lossy_rt() -> None: im = roundtrip(test_card, quality_layers=[20]) assert_image_similar(im, test_card, 2.0) -def test_tiled_rt(): +def test_tiled_rt() -> None: im = roundtrip(test_card, tile_size=(128, 128)) assert_image_equal(im, test_card) -def test_tiled_offset_rt(): +def test_tiled_offset_rt() -> None: im = roundtrip(test_card, tile_size=(128, 128), tile_offset=(0, 0), offset=(32, 32)) assert_image_equal(im, test_card) -def test_tiled_offset_too_small(): +def test_tiled_offset_too_small() -> None: with pytest.raises(ValueError): roundtrip(test_card, tile_size=(128, 128), tile_offset=(0, 0), offset=(128, 32)) -def test_irreversible_rt(): +def test_irreversible_rt() -> None: im = roundtrip(test_card, irreversible=True, quality_layers=[20]) assert_image_similar(im, test_card, 2.0) -def test_prog_qual_rt(): +def test_prog_qual_rt() -> None: im = roundtrip(test_card, quality_layers=[60, 40, 20], progression="LRCP") assert_image_similar(im, test_card, 2.0) -def test_prog_res_rt(): +def test_prog_res_rt() -> None: im = roundtrip(test_card, num_resolutions=8, progression="RLCP") assert_image_equal(im, test_card) @pytest.mark.parametrize("num_resolutions", range(2, 6)) -def test_default_num_resolutions(num_resolutions): +def test_default_num_resolutions(num_resolutions: int) -> None: d = 1 << (num_resolutions - 1) im = test_card.resize((d - 1, d - 1)) with pytest.raises(OSError): @@ -146,7 +148,7 @@ def test_default_num_resolutions(num_resolutions): assert_image_equal(im, reloaded) -def test_reduce(): +def test_reduce() -> None: with Image.open("Tests/images/test-card-lossless.jp2") as im: assert callable(im.reduce) @@ -160,7 +162,7 @@ def test_reduce(): assert im.size == (40, 30) -def test_load_dpi(): +def test_load_dpi() -> None: with Image.open("Tests/images/test-card-lossless.jp2") as im: assert im.info["dpi"] == (71.9836, 71.9836) @@ -168,7 +170,7 @@ def test_load_dpi(): assert "dpi" not in im.info -def test_restricted_icc_profile(): +def test_restricted_icc_profile() -> None: ImageFile.LOAD_TRUNCATED_IMAGES = True try: # JPEG2000 image with a restricted ICC profile and a known colorspace @@ -178,7 +180,7 @@ def test_restricted_icc_profile(): ImageFile.LOAD_TRUNCATED_IMAGES = False -def test_header_errors(): +def test_header_errors() -> None: for path in ( "Tests/images/invalid_header_length.jp2", "Tests/images/not_enough_data.jp2", @@ -192,17 +194,17 @@ def test_header_errors(): pass -def test_layers_type(tmp_path): +def test_layers_type(tmp_path: Path) -> None: outfile = str(tmp_path / "temp_layers.jp2") for quality_layers in [[100, 50, 10], (100, 50, 10), None]: test_card.save(outfile, quality_layers=quality_layers) - for quality_layers in ["quality_layers", ("100", "50", "10")]: + for quality_layers_str in ["quality_layers", ("100", "50", "10")]: with pytest.raises(ValueError): - test_card.save(outfile, quality_layers=quality_layers) + test_card.save(outfile, quality_layers=quality_layers_str) -def test_layers(): +def test_layers() -> None: out = BytesIO() test_card.save(out, "JPEG2000", quality_layers=[100, 50, 10], progression="LRCP") out.seek(0) @@ -232,7 +234,7 @@ def test_layers(): ("foo.jp2", {"no_jp2": False}, 4, b"jP"), ), ) -def test_no_jp2(name, args, offset, data): +def test_no_jp2(name: str, args: dict[str, bool], offset: int, data: bytes) -> None: out = BytesIO() if name: out.name = name @@ -241,7 +243,7 @@ def test_no_jp2(name, args, offset, data): assert out.read(2) == data -def test_mct(): +def test_mct() -> None: # Three component for val in (0, 1): out = BytesIO() @@ -262,7 +264,7 @@ def test_mct(): assert_image_similar(im, jp2, 1.0e-3) -def test_sgnd(tmp_path): +def test_sgnd(tmp_path: Path) -> None: outfile = str(tmp_path / "temp.jp2") im = Image.new("L", (1, 1)) @@ -277,7 +279,7 @@ def test_sgnd(tmp_path): @pytest.mark.parametrize("ext", (".j2k", ".jp2")) -def test_rgba(ext): +def test_rgba(ext: str) -> None: # Arrange with Image.open("Tests/images/rgb_trns_ycbc" + ext) as im: # Act @@ -288,47 +290,47 @@ def test_rgba(ext): @pytest.mark.parametrize("ext", (".j2k", ".jp2")) -def test_16bit_monochrome_has_correct_mode(ext): +def test_16bit_monochrome_has_correct_mode(ext: str) -> None: with Image.open("Tests/images/16bit.cropped" + ext) as im: im.load() assert im.mode == "I;16" -def test_16bit_monochrome_jp2_like_tiff(): +def test_16bit_monochrome_jp2_like_tiff() -> None: with Image.open("Tests/images/16bit.cropped.tif") as tiff_16bit: assert_image_similar_tofile(tiff_16bit, "Tests/images/16bit.cropped.jp2", 1e-3) -def test_16bit_monochrome_j2k_like_tiff(): +def test_16bit_monochrome_j2k_like_tiff() -> None: with Image.open("Tests/images/16bit.cropped.tif") as tiff_16bit: assert_image_similar_tofile(tiff_16bit, "Tests/images/16bit.cropped.j2k", 1e-3) -def test_16bit_j2k_roundtrips(): +def test_16bit_j2k_roundtrips() -> None: with Image.open("Tests/images/16bit.cropped.j2k") as j2k: im = roundtrip(j2k) assert_image_equal(im, j2k) -def test_16bit_jp2_roundtrips(): +def test_16bit_jp2_roundtrips() -> None: with Image.open("Tests/images/16bit.cropped.jp2") as jp2: im = roundtrip(jp2) assert_image_equal(im, jp2) -def test_issue_6194(): +def test_issue_6194() -> None: with Image.open("Tests/images/issue_6194.j2k") as im: assert im.getpixel((5, 5)) == 31 -def test_unbound_local(): +def test_unbound_local() -> None: # prepatch, a malformed jp2 file could cause an UnboundLocalError exception. with pytest.raises(OSError): with Image.open("Tests/images/unbound_variable.jp2"): pass -def test_parser_feed(): +def test_parser_feed() -> None: # Arrange with open("Tests/images/test-card-lossless.jp2", "rb") as f: data = f.read() @@ -338,6 +340,7 @@ def test_parser_feed(): p.feed(data) # Assert + assert p.image is not None assert p.image.size == (640, 480) @@ -345,12 +348,12 @@ def test_parser_feed(): not os.path.exists(EXTRA_DIR), reason="Extra image files not installed" ) @pytest.mark.parametrize("name", ("subsampling_1", "subsampling_2", "zoo1", "zoo2")) -def test_subsampling_decode(name): +def test_subsampling_decode(name: str) -> None: test = f"{EXTRA_DIR}/{name}.jp2" reference = f"{EXTRA_DIR}/{name}.ppm" with Image.open(test) as im: - epsilon = 3 # for YCbCr images + epsilon = 3.0 # for YCbCr images with Image.open(reference) as im2: width, height = im2.size if name[-1] == "2": @@ -361,7 +364,7 @@ def test_subsampling_decode(name): assert_image_similar(im, expected, epsilon) -def test_comment(): +def test_comment() -> None: with Image.open("Tests/images/comment.jp2") as im: assert im.info["comment"] == b"Created by OpenJPEG version 2.5.0" @@ -372,7 +375,7 @@ def test_comment(): pass -def test_save_comment(): +def test_save_comment() -> None: for comment in ("Created by Pillow", b"Created by Pillow"): out = BytesIO() test_card.save(out, "JPEG2000", comment=comment) @@ -399,7 +402,7 @@ def test_save_comment(): "Tests/images/crash-d2c93af851d3ab9a19e34503626368b2ecde9c03.j2k", ], ) -def test_crashes(test_file): +def test_crashes(test_file: str) -> None: with open(test_file, "rb") as f: with Image.open(f) as im: # Valgrind should not complain here @@ -410,7 +413,7 @@ def test_crashes(test_file): @skip_unless_feature_version("jpg_2000", "2.4.0") -def test_plt_marker(): +def test_plt_marker() -> None: # Search the start of the codesteam for PLT out = BytesIO() test_card.save(out, "JPEG2000", no_jp2=True, plt=True) diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index 494253c87..908464a11 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -7,6 +7,7 @@ import os import re import sys from collections import namedtuple +from pathlib import Path import pytest @@ -26,7 +27,7 @@ from .helper import ( @skip_unless_feature("libtiff") class LibTiffTestCase: - def _assert_noerr(self, tmp_path, im): + def _assert_noerr(self, tmp_path: Path, im: TiffImagePlugin.TiffImageFile) -> None: """Helper tests that assert basic sanity about the g4 tiff reading""" # 1 bit assert im.mode == "1" @@ -50,10 +51,10 @@ class LibTiffTestCase: class TestFileLibTiff(LibTiffTestCase): - def test_version(self): + def test_version(self) -> None: assert re.search(r"\d+\.\d+\.\d+$", features.version_codec("libtiff")) - def test_g4_tiff(self, tmp_path): + def test_g4_tiff(self, tmp_path: Path) -> None: """Test the ordinary file path load path""" test_file = "Tests/images/hopper_g4_500.tif" @@ -61,12 +62,12 @@ class TestFileLibTiff(LibTiffTestCase): assert im.size == (500, 500) self._assert_noerr(tmp_path, im) - def test_g4_large(self, tmp_path): + def test_g4_large(self, tmp_path: Path) -> None: test_file = "Tests/images/pport_g4.tif" with Image.open(test_file) as im: self._assert_noerr(tmp_path, im) - def test_g4_tiff_file(self, tmp_path): + def test_g4_tiff_file(self, tmp_path: Path) -> None: """Testing the string load path""" test_file = "Tests/images/hopper_g4_500.tif" @@ -75,7 +76,7 @@ class TestFileLibTiff(LibTiffTestCase): assert im.size == (500, 500) self._assert_noerr(tmp_path, im) - def test_g4_tiff_bytesio(self, tmp_path): + def test_g4_tiff_bytesio(self, tmp_path: Path) -> None: """Testing the stringio loading code path""" test_file = "Tests/images/hopper_g4_500.tif" s = io.BytesIO() @@ -86,7 +87,7 @@ class TestFileLibTiff(LibTiffTestCase): assert im.size == (500, 500) self._assert_noerr(tmp_path, im) - def test_g4_non_disk_file_object(self, tmp_path): + def test_g4_non_disk_file_object(self, tmp_path: Path) -> None: """Testing loading from non-disk non-BytesIO file object""" test_file = "Tests/images/hopper_g4_500.tif" s = io.BytesIO() @@ -98,18 +99,18 @@ class TestFileLibTiff(LibTiffTestCase): assert im.size == (500, 500) self._assert_noerr(tmp_path, im) - def test_g4_eq_png(self): + def test_g4_eq_png(self) -> None: """Checking that we're actually getting the data that we expect""" with Image.open("Tests/images/hopper_bw_500.png") as png: assert_image_equal_tofile(png, "Tests/images/hopper_g4_500.tif") # see https://github.com/python-pillow/Pillow/issues/279 - def test_g4_fillorder_eq_png(self): + def test_g4_fillorder_eq_png(self) -> None: """Checking that we're actually getting the data that we expect""" with Image.open("Tests/images/g4-fillorder-test.tif") as g4: assert_image_equal_tofile(g4, "Tests/images/g4-fillorder-test.png") - def test_g4_write(self, tmp_path): + def test_g4_write(self, tmp_path: Path) -> None: """Checking to see that the saved image is the same as what we wrote""" test_file = "Tests/images/hopper_g4_500.tif" with Image.open(test_file) as orig: @@ -128,7 +129,7 @@ class TestFileLibTiff(LibTiffTestCase): assert orig.tobytes() != reread.tobytes() - def test_adobe_deflate_tiff(self): + def test_adobe_deflate_tiff(self) -> None: test_file = "Tests/images/tiff_adobe_deflate.tif" with Image.open(test_file) as im: assert im.mode == "RGB" @@ -139,7 +140,7 @@ class TestFileLibTiff(LibTiffTestCase): assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png") @pytest.mark.parametrize("legacy_api", (False, True)) - def test_write_metadata(self, legacy_api, tmp_path): + def test_write_metadata(self, legacy_api: bool, tmp_path: Path) -> None: """Test metadata writing through libtiff""" f = str(tmp_path / "temp.tiff") with Image.open("Tests/images/hopper_g4.tif") as img: @@ -184,7 +185,7 @@ class TestFileLibTiff(LibTiffTestCase): assert field in reloaded, f"{field} not in metadata" @pytest.mark.valgrind_known_error(reason="Known invalid metadata") - def test_additional_metadata(self, tmp_path): + def test_additional_metadata(self, tmp_path: Path) -> None: # these should not crash. Seriously dummy data, most of it doesn't make # any sense, so we're running up against limits where we're asking # libtiff to do stupid things. @@ -241,8 +242,8 @@ class TestFileLibTiff(LibTiffTestCase): TiffImagePlugin.WRITE_LIBTIFF = False - def test_custom_metadata(self, tmp_path): - tc = namedtuple("test_case", "value,type,supported_by_default") + def test_custom_metadata(self, tmp_path: Path) -> None: + tc = namedtuple("tc", "value,type,supported_by_default") custom = { 37000 + k: v for k, v in enumerate( @@ -283,7 +284,9 @@ class TestFileLibTiff(LibTiffTestCase): for libtiff in libtiffs: TiffImagePlugin.WRITE_LIBTIFF = libtiff - def check_tags(tiffinfo): + def check_tags( + tiffinfo: TiffImagePlugin.ImageFileDirectory_v2 | dict[int, str] + ) -> None: im = hopper() out = str(tmp_path / "temp.tif") @@ -322,7 +325,7 @@ class TestFileLibTiff(LibTiffTestCase): ) TiffImagePlugin.WRITE_LIBTIFF = False - def test_subifd(self, tmp_path): + def test_subifd(self, tmp_path: Path) -> None: outfile = str(tmp_path / "temp.tif") with Image.open("Tests/images/g4_orientation_6.tif") as im: im.tag_v2[SUBIFD] = 10000 @@ -330,7 +333,7 @@ class TestFileLibTiff(LibTiffTestCase): # Should not segfault im.save(outfile) - def test_xmlpacket_tag(self, tmp_path): + def test_xmlpacket_tag(self, tmp_path: Path) -> None: TiffImagePlugin.WRITE_LIBTIFF = True out = str(tmp_path / "temp.tif") @@ -341,7 +344,7 @@ class TestFileLibTiff(LibTiffTestCase): if 700 in reloaded.tag_v2: assert reloaded.tag_v2[700] == b"xmlpacket tag" - def test_int_dpi(self, tmp_path): + def test_int_dpi(self, tmp_path: Path) -> None: # issue #1765 im = hopper("RGB") out = str(tmp_path / "temp.tif") @@ -351,7 +354,7 @@ class TestFileLibTiff(LibTiffTestCase): with Image.open(out) as reloaded: assert reloaded.info["dpi"] == (72.0, 72.0) - def test_g3_compression(self, tmp_path): + def test_g3_compression(self, tmp_path: Path) -> None: with Image.open("Tests/images/hopper_g4_500.tif") as i: out = str(tmp_path / "temp.tif") i.save(out, compression="group3") @@ -360,7 +363,7 @@ class TestFileLibTiff(LibTiffTestCase): assert reread.info["compression"] == "group3" assert_image_equal(reread, i) - def test_little_endian(self, tmp_path): + def test_little_endian(self, tmp_path: Path) -> None: with Image.open("Tests/images/16bit.deflate.tif") as im: assert im.getpixel((0, 0)) == 480 assert im.mode == "I;16" @@ -379,7 +382,7 @@ class TestFileLibTiff(LibTiffTestCase): # UNDONE - libtiff defaults to writing in native endian, so # on big endian, we'll get back mode = 'I;16B' here. - def test_big_endian(self, tmp_path): + def test_big_endian(self, tmp_path: Path) -> None: with Image.open("Tests/images/16bit.MM.deflate.tif") as im: assert im.getpixel((0, 0)) == 480 assert im.mode == "I;16B" @@ -396,7 +399,7 @@ class TestFileLibTiff(LibTiffTestCase): assert reread.info["compression"] == im.info["compression"] assert reread.getpixel((0, 0)) == 480 - def test_g4_string_info(self, tmp_path): + def test_g4_string_info(self, tmp_path: Path) -> None: """Tests String data in info directory""" test_file = "Tests/images/hopper_g4_500.tif" with Image.open(test_file) as orig: @@ -409,7 +412,7 @@ class TestFileLibTiff(LibTiffTestCase): assert "temp.tif" == reread.tag_v2[269] assert "temp.tif" == reread.tag[269][0] - def test_12bit_rawmode(self): + def test_12bit_rawmode(self) -> None: """Are we generating the same interpretation of the image as Imagemagick is?""" TiffImagePlugin.READ_LIBTIFF = True @@ -424,7 +427,7 @@ class TestFileLibTiff(LibTiffTestCase): assert_image_equal_tofile(im, "Tests/images/12in16bit.tif") - def test_blur(self, tmp_path): + def test_blur(self, tmp_path: Path) -> None: # test case from irc, how to do blur on b/w image # and save to compressed tif. out = str(tmp_path / "temp.tif") @@ -436,7 +439,7 @@ class TestFileLibTiff(LibTiffTestCase): assert_image_equal_tofile(im, out) - def test_compressions(self, tmp_path): + def test_compressions(self, tmp_path: Path) -> None: # Test various tiff compressions and assert similar image content but reduced # file sizes. im = hopper("RGB") @@ -462,7 +465,7 @@ class TestFileLibTiff(LibTiffTestCase): assert size_compressed > size_jpeg assert size_jpeg > size_jpeg_30 - def test_tiff_jpeg_compression(self, tmp_path): + def test_tiff_jpeg_compression(self, tmp_path: Path) -> None: im = hopper("RGB") out = str(tmp_path / "temp.tif") im.save(out, compression="tiff_jpeg") @@ -470,7 +473,7 @@ class TestFileLibTiff(LibTiffTestCase): with Image.open(out) as reloaded: assert reloaded.info["compression"] == "jpeg" - def test_tiff_deflate_compression(self, tmp_path): + def test_tiff_deflate_compression(self, tmp_path: Path) -> None: im = hopper("RGB") out = str(tmp_path / "temp.tif") im.save(out, compression="tiff_deflate") @@ -478,7 +481,7 @@ class TestFileLibTiff(LibTiffTestCase): with Image.open(out) as reloaded: assert reloaded.info["compression"] == "tiff_adobe_deflate" - def test_quality(self, tmp_path): + def test_quality(self, tmp_path: Path) -> None: im = hopper("RGB") out = str(tmp_path / "temp.tif") @@ -493,7 +496,7 @@ class TestFileLibTiff(LibTiffTestCase): im.save(out, compression="jpeg", quality=0) im.save(out, compression="jpeg", quality=100) - def test_cmyk_save(self, tmp_path): + def test_cmyk_save(self, tmp_path: Path) -> None: im = hopper("CMYK") out = str(tmp_path / "temp.tif") @@ -501,7 +504,7 @@ class TestFileLibTiff(LibTiffTestCase): assert_image_equal_tofile(im, out) @pytest.mark.parametrize("im", (hopper("P"), Image.new("P", (1, 1), "#000"))) - def test_palette_save(self, im, tmp_path): + def test_palette_save(self, im: Image.Image, tmp_path: Path) -> None: out = str(tmp_path / "temp.tif") TiffImagePlugin.WRITE_LIBTIFF = True @@ -513,15 +516,16 @@ class TestFileLibTiff(LibTiffTestCase): assert len(reloaded.tag_v2[320]) == 768 @pytest.mark.parametrize("compression", ("tiff_ccitt", "group3", "group4")) - def test_bw_compression_w_rgb(self, compression, tmp_path): + def test_bw_compression_w_rgb(self, compression: str, tmp_path: Path) -> None: im = hopper("RGB") out = str(tmp_path / "temp.tif") with pytest.raises(OSError): im.save(out, compression=compression) - def test_fp_leak(self): - im = Image.open("Tests/images/hopper_g4_500.tif") + def test_fp_leak(self) -> None: + im: Image.Image | None = Image.open("Tests/images/hopper_g4_500.tif") + assert im is not None fn = im.fp.fileno() os.fstat(fn) @@ -534,7 +538,7 @@ class TestFileLibTiff(LibTiffTestCase): with pytest.raises(OSError): os.close(fn) - def test_multipage(self): + def test_multipage(self) -> None: # issue #862 TiffImagePlugin.READ_LIBTIFF = True with Image.open("Tests/images/multipage.tiff") as im: @@ -557,7 +561,7 @@ class TestFileLibTiff(LibTiffTestCase): TiffImagePlugin.READ_LIBTIFF = False - def test_multipage_nframes(self): + def test_multipage_nframes(self) -> None: # issue #862 TiffImagePlugin.READ_LIBTIFF = True with Image.open("Tests/images/multipage.tiff") as im: @@ -570,7 +574,7 @@ class TestFileLibTiff(LibTiffTestCase): TiffImagePlugin.READ_LIBTIFF = False - def test_multipage_seek_backwards(self): + def test_multipage_seek_backwards(self) -> None: TiffImagePlugin.READ_LIBTIFF = True with Image.open("Tests/images/multipage.tiff") as im: im.seek(1) @@ -581,14 +585,14 @@ class TestFileLibTiff(LibTiffTestCase): TiffImagePlugin.READ_LIBTIFF = False - def test__next(self): + def test__next(self) -> None: TiffImagePlugin.READ_LIBTIFF = True with Image.open("Tests/images/hopper.tif") as im: assert not im.tag.next im.load() assert not im.tag.next - def test_4bit(self): + def test_4bit(self) -> None: # Arrange test_file = "Tests/images/hopper_gray_4bpp.tif" original = hopper("L") @@ -603,7 +607,7 @@ class TestFileLibTiff(LibTiffTestCase): assert im.mode == "L" assert_image_similar(im, original, 7.3) - def test_gray_semibyte_per_pixel(self): + def test_gray_semibyte_per_pixel(self) -> None: test_files = ( ( 24.8, # epsilon @@ -636,7 +640,7 @@ class TestFileLibTiff(LibTiffTestCase): assert im2.mode == "L" assert_image_equal(im, im2) - def test_save_bytesio(self): + def test_save_bytesio(self) -> None: # PR 1011 # Test TIFF saving to io.BytesIO() object. @@ -646,7 +650,7 @@ class TestFileLibTiff(LibTiffTestCase): # Generate test image pilim = hopper() - def save_bytesio(compression=None): + def save_bytesio(compression: str | None = None) -> None: buffer_io = io.BytesIO() pilim.save(buffer_io, format="tiff", compression=compression) buffer_io.seek(0) @@ -661,7 +665,7 @@ class TestFileLibTiff(LibTiffTestCase): TiffImagePlugin.WRITE_LIBTIFF = False TiffImagePlugin.READ_LIBTIFF = False - def test_save_ycbcr(self, tmp_path): + def test_save_ycbcr(self, tmp_path: Path) -> None: im = hopper("YCbCr") outfile = str(tmp_path / "temp.tif") im.save(outfile, compression="jpeg") @@ -670,7 +674,7 @@ class TestFileLibTiff(LibTiffTestCase): assert reloaded.tag_v2[530] == (1, 1) assert reloaded.tag_v2[532] == (0, 255, 128, 255, 128, 255) - def test_exif_ifd(self, tmp_path): + def test_exif_ifd(self, tmp_path: Path) -> None: outfile = str(tmp_path / "temp.tif") with Image.open("Tests/images/tiff_adobe_deflate.tif") as im: assert im.tag_v2[34665] == 125456 @@ -680,7 +684,7 @@ class TestFileLibTiff(LibTiffTestCase): if Image.core.libtiff_support_custom_tags: assert reloaded.tag_v2[34665] == 125456 - def test_crashing_metadata(self, tmp_path): + def test_crashing_metadata(self, tmp_path: Path) -> None: # issue 1597 with Image.open("Tests/images/rdf.tif") as im: out = str(tmp_path / "temp.tif") @@ -690,7 +694,7 @@ class TestFileLibTiff(LibTiffTestCase): im.save(out, format="TIFF") TiffImagePlugin.WRITE_LIBTIFF = False - def test_page_number_x_0(self, tmp_path): + def test_page_number_x_0(self, tmp_path: Path) -> None: # Issue 973 # Test TIFF with tag 297 (Page Number) having value of 0 0. # The first number is the current page number. @@ -704,7 +708,7 @@ class TestFileLibTiff(LibTiffTestCase): # Should not divide by zero im.save(outfile) - def test_fd_duplication(self, tmp_path): + def test_fd_duplication(self, tmp_path: Path) -> None: # https://github.com/python-pillow/Pillow/issues/1651 tmpfile = str(tmp_path / "temp.tif") @@ -713,12 +717,13 @@ class TestFileLibTiff(LibTiffTestCase): f.write(src.read()) im = Image.open(tmpfile) + assert isinstance(im, TiffImagePlugin.TiffImageFile) im.n_frames im.close() # Should not raise PermissionError. os.remove(tmpfile) - def test_read_icc(self): + def test_read_icc(self) -> None: with Image.open("Tests/images/hopper.iccprofile.tif") as img: icc = img.info.get("icc_profile") assert icc is not None @@ -729,8 +734,8 @@ class TestFileLibTiff(LibTiffTestCase): TiffImagePlugin.READ_LIBTIFF = False assert icc == icc_libtiff - def test_write_icc(self, tmp_path): - def check_write(libtiff): + def test_write_icc(self, tmp_path: Path) -> None: + def check_write(libtiff: bool) -> None: TiffImagePlugin.WRITE_LIBTIFF = libtiff with Image.open("Tests/images/hopper.iccprofile.tif") as img: @@ -749,7 +754,7 @@ class TestFileLibTiff(LibTiffTestCase): for libtiff in libtiffs: check_write(libtiff) - def test_multipage_compression(self): + def test_multipage_compression(self) -> None: with Image.open("Tests/images/compression.tif") as im: im.seek(0) assert im._compression == "tiff_ccitt" @@ -765,7 +770,7 @@ class TestFileLibTiff(LibTiffTestCase): assert im.size == (10, 10) im.load() - def test_save_tiff_with_jpegtables(self, tmp_path): + def test_save_tiff_with_jpegtables(self, tmp_path: Path) -> None: # Arrange outfile = str(tmp_path / "temp.tif") @@ -777,7 +782,7 @@ class TestFileLibTiff(LibTiffTestCase): # Should not raise UnicodeDecodeError or anything else im.save(outfile) - def test_16bit_RGB_tiff(self): + def test_16bit_RGB_tiff(self) -> None: with Image.open("Tests/images/tiff_16bit_RGB.tiff") as im: assert im.mode == "RGB" assert im.size == (100, 40) @@ -793,7 +798,7 @@ class TestFileLibTiff(LibTiffTestCase): assert_image_equal_tofile(im, "Tests/images/tiff_16bit_RGB_target.png") - def test_16bit_RGBa_tiff(self): + def test_16bit_RGBa_tiff(self) -> None: with Image.open("Tests/images/tiff_16bit_RGBa.tiff") as im: assert im.mode == "RGBA" assert im.size == (100, 40) @@ -805,7 +810,7 @@ class TestFileLibTiff(LibTiffTestCase): assert_image_equal_tofile(im, "Tests/images/tiff_16bit_RGBa_target.png") @skip_unless_feature("jpg") - def test_gimp_tiff(self): + def test_gimp_tiff(self) -> None: # Read TIFF JPEG images from GIMP [@PIL168] filename = "Tests/images/pil168.tif" with Image.open(filename) as im: @@ -818,14 +823,14 @@ class TestFileLibTiff(LibTiffTestCase): assert_image_equal_tofile(im, "Tests/images/pil168.png") - def test_sampleformat(self): + def test_sampleformat(self) -> None: # https://github.com/python-pillow/Pillow/issues/1466 with Image.open("Tests/images/copyleft.tiff") as im: assert im.mode == "RGB" assert_image_equal_tofile(im, "Tests/images/copyleft.png", mode="RGB") - def test_sampleformat_write(self, tmp_path): + def test_sampleformat_write(self, tmp_path: Path) -> None: im = Image.new("F", (1, 1)) out = str(tmp_path / "temp.tif") TiffImagePlugin.WRITE_LIBTIFF = True @@ -836,7 +841,7 @@ class TestFileLibTiff(LibTiffTestCase): assert reloaded.mode == "F" assert reloaded.getexif()[SAMPLEFORMAT] == 3 - def test_lzma(self, capfd): + def test_lzma(self, capfd: pytest.CaptureFixture[str]) -> None: try: with Image.open("Tests/images/hopper_lzma.tif") as im: assert im.mode == "RGB" @@ -852,7 +857,7 @@ class TestFileLibTiff(LibTiffTestCase): sys.stderr.write(captured.err) raise - def test_webp(self, capfd): + def test_webp(self, capfd: pytest.CaptureFixture[str]) -> None: try: with Image.open("Tests/images/hopper_webp.tif") as im: assert im.mode == "RGB" @@ -874,7 +879,7 @@ class TestFileLibTiff(LibTiffTestCase): sys.stderr.write(captured.err) raise - def test_lzw(self): + def test_lzw(self) -> None: with Image.open("Tests/images/hopper_lzw.tif") as im: assert im.mode == "RGB" assert im.size == (128, 128) @@ -882,12 +887,12 @@ class TestFileLibTiff(LibTiffTestCase): im2 = hopper() assert_image_similar(im, im2, 5) - def test_strip_cmyk_jpeg(self): + def test_strip_cmyk_jpeg(self) -> None: infile = "Tests/images/tiff_strip_cmyk_jpeg.tif" with Image.open(infile) as im: assert_image_similar_tofile(im, "Tests/images/pil_sample_cmyk.jpg", 0.5) - def test_strip_cmyk_16l_jpeg(self): + def test_strip_cmyk_16l_jpeg(self) -> None: infile = "Tests/images/tiff_strip_cmyk_16l_jpeg.tif" with Image.open(infile) as im: assert_image_similar_tofile(im, "Tests/images/pil_sample_cmyk.jpg", 0.5) @@ -895,7 +900,7 @@ class TestFileLibTiff(LibTiffTestCase): @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) - def test_strip_ycbcr_jpeg_2x2_sampling(self): + def test_strip_ycbcr_jpeg_2x2_sampling(self) -> None: infile = "Tests/images/tiff_strip_ycbcr_jpeg_2x2_sampling.tif" with Image.open(infile) as im: assert_image_similar_tofile(im, "Tests/images/flower.jpg", 1.2) @@ -903,12 +908,12 @@ class TestFileLibTiff(LibTiffTestCase): @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) - def test_strip_ycbcr_jpeg_1x1_sampling(self): + def test_strip_ycbcr_jpeg_1x1_sampling(self) -> None: infile = "Tests/images/tiff_strip_ycbcr_jpeg_1x1_sampling.tif" with Image.open(infile) as im: assert_image_similar_tofile(im, "Tests/images/flower2.jpg", 0.01) - def test_tiled_cmyk_jpeg(self): + def test_tiled_cmyk_jpeg(self) -> None: infile = "Tests/images/tiff_tiled_cmyk_jpeg.tif" with Image.open(infile) as im: assert_image_similar_tofile(im, "Tests/images/pil_sample_cmyk.jpg", 0.5) @@ -916,7 +921,7 @@ class TestFileLibTiff(LibTiffTestCase): @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) - def test_tiled_ycbcr_jpeg_1x1_sampling(self): + def test_tiled_ycbcr_jpeg_1x1_sampling(self) -> None: infile = "Tests/images/tiff_tiled_ycbcr_jpeg_1x1_sampling.tif" with Image.open(infile) as im: assert_image_similar_tofile(im, "Tests/images/flower2.jpg", 0.01) @@ -924,45 +929,45 @@ class TestFileLibTiff(LibTiffTestCase): @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) - def test_tiled_ycbcr_jpeg_2x2_sampling(self): + def test_tiled_ycbcr_jpeg_2x2_sampling(self) -> None: infile = "Tests/images/tiff_tiled_ycbcr_jpeg_2x2_sampling.tif" with Image.open(infile) as im: assert_image_similar_tofile(im, "Tests/images/flower.jpg", 1.5) - def test_strip_planar_rgb(self): + def test_strip_planar_rgb(self) -> None: # gdal_translate -co TILED=no -co INTERLEAVE=BAND -co COMPRESS=LZW \ # tiff_strip_raw.tif tiff_strip_planar_lzw.tiff infile = "Tests/images/tiff_strip_planar_lzw.tiff" with Image.open(infile) as im: assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png") - def test_tiled_planar_rgb(self): + def test_tiled_planar_rgb(self) -> None: # gdal_translate -co TILED=yes -co INTERLEAVE=BAND -co COMPRESS=LZW \ # tiff_tiled_raw.tif tiff_tiled_planar_lzw.tiff infile = "Tests/images/tiff_tiled_planar_lzw.tiff" with Image.open(infile) as im: assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png") - def test_tiled_planar_16bit_RGB(self): + def test_tiled_planar_16bit_RGB(self) -> None: # gdal_translate -co TILED=yes -co INTERLEAVE=BAND -co COMPRESS=LZW \ # tiff_16bit_RGB.tiff tiff_tiled_planar_16bit_RGB.tiff with Image.open("Tests/images/tiff_tiled_planar_16bit_RGB.tiff") as im: assert_image_equal_tofile(im, "Tests/images/tiff_16bit_RGB_target.png") - def test_strip_planar_16bit_RGB(self): + def test_strip_planar_16bit_RGB(self) -> None: # gdal_translate -co TILED=no -co INTERLEAVE=BAND -co COMPRESS=LZW \ # tiff_16bit_RGB.tiff tiff_strip_planar_16bit_RGB.tiff with Image.open("Tests/images/tiff_strip_planar_16bit_RGB.tiff") as im: assert_image_equal_tofile(im, "Tests/images/tiff_16bit_RGB_target.png") - def test_tiled_planar_16bit_RGBa(self): + def test_tiled_planar_16bit_RGBa(self) -> None: # gdal_translate -co TILED=yes \ # -co INTERLEAVE=BAND -co COMPRESS=LZW -co ALPHA=PREMULTIPLIED \ # tiff_16bit_RGBa.tiff tiff_tiled_planar_16bit_RGBa.tiff with Image.open("Tests/images/tiff_tiled_planar_16bit_RGBa.tiff") as im: assert_image_equal_tofile(im, "Tests/images/tiff_16bit_RGBa_target.png") - def test_strip_planar_16bit_RGBa(self): + def test_strip_planar_16bit_RGBa(self) -> None: # gdal_translate -co TILED=no \ # -co INTERLEAVE=BAND -co COMPRESS=LZW -co ALPHA=PREMULTIPLIED \ # tiff_16bit_RGBa.tiff tiff_strip_planar_16bit_RGBa.tiff @@ -970,7 +975,7 @@ class TestFileLibTiff(LibTiffTestCase): assert_image_equal_tofile(im, "Tests/images/tiff_16bit_RGBa_target.png") @pytest.mark.parametrize("compression", (None, "jpeg")) - def test_block_tile_tags(self, compression, tmp_path): + def test_block_tile_tags(self, compression: str | None, tmp_path: Path) -> None: im = hopper() out = str(tmp_path / "temp.tif") @@ -986,11 +991,11 @@ class TestFileLibTiff(LibTiffTestCase): for tag in tags: assert tag not in reloaded.getexif() - def test_old_style_jpeg(self): + def test_old_style_jpeg(self) -> None: with Image.open("Tests/images/old-style-jpeg-compression.tif") as im: assert_image_equal_tofile(im, "Tests/images/old-style-jpeg-compression.png") - def test_open_missing_samplesperpixel(self): + def test_open_missing_samplesperpixel(self) -> None: with Image.open( "Tests/images/old-style-jpeg-compression-no-samplesperpixel.tif" ) as im: @@ -1019,21 +1024,23 @@ class TestFileLibTiff(LibTiffTestCase): ), ], ) - def test_wrong_bits_per_sample(self, file_name, mode, size, tile): + def test_wrong_bits_per_sample( + self, file_name: str, mode: str, size: tuple[int, int], tile + ) -> None: with Image.open("Tests/images/" + file_name) as im: assert im.mode == mode assert im.size == size assert im.tile == tile im.load() - def test_no_rows_per_strip(self): + def test_no_rows_per_strip(self) -> None: # This image does not have a RowsPerStrip TIFF tag infile = "Tests/images/no_rows_per_strip.tif" with Image.open(infile) as im: im.load() assert im.size == (950, 975) - def test_orientation(self): + def test_orientation(self) -> None: with Image.open("Tests/images/g4_orientation_1.tif") as base_im: for i in range(2, 9): with Image.open("Tests/images/g4_orientation_" + str(i) + ".tif") as im: @@ -1044,7 +1051,7 @@ class TestFileLibTiff(LibTiffTestCase): assert_image_similar(base_im, im, 0.7) - def test_exif_transpose(self): + def test_exif_transpose(self) -> None: with Image.open("Tests/images/g4_orientation_1.tif") as base_im: for i in range(2, 9): with Image.open("Tests/images/g4_orientation_" + str(i) + ".tif") as im: @@ -1053,7 +1060,7 @@ class TestFileLibTiff(LibTiffTestCase): assert_image_similar(base_im, im, 0.7) @pytest.mark.valgrind_known_error(reason="Backtrace in Python Core") - def test_sampleformat_not_corrupted(self): + def test_sampleformat_not_corrupted(self) -> None: # Assert that a TIFF image with SampleFormat=UINT tag is not corrupted # when saving to a new file. # Pillow 6.0 fails with "OSError: cannot identify image file". @@ -1074,7 +1081,7 @@ class TestFileLibTiff(LibTiffTestCase): with Image.open(out) as im: im.load() - def test_realloc_overflow(self): + def test_realloc_overflow(self) -> None: TiffImagePlugin.READ_LIBTIFF = True with Image.open("Tests/images/tiff_overflow_rows_per_strip.tif") as im: with pytest.raises(OSError) as e: @@ -1085,41 +1092,43 @@ class TestFileLibTiff(LibTiffTestCase): TiffImagePlugin.READ_LIBTIFF = False @pytest.mark.parametrize("compression", ("tiff_adobe_deflate", "jpeg")) - def test_save_multistrip(self, compression, tmp_path): + def test_save_multistrip(self, compression: str, tmp_path: Path) -> None: im = hopper("RGB").resize((256, 256)) out = str(tmp_path / "temp.tif") im.save(out, compression=compression) with Image.open(out) as im: # Assert that there are multiple strips + assert isinstance(im, TiffImagePlugin.TiffImageFile) assert len(im.tag_v2[STRIPOFFSETS]) > 1 @pytest.mark.parametrize("argument", (True, False)) - def test_save_single_strip(self, argument, tmp_path): + def test_save_single_strip(self, argument: bool, tmp_path: Path) -> None: im = hopper("RGB").resize((256, 256)) out = str(tmp_path / "temp.tif") if not argument: TiffImagePlugin.STRIP_SIZE = 2**18 try: - arguments = {"compression": "tiff_adobe_deflate"} + arguments: dict[str, str | int] = {"compression": "tiff_adobe_deflate"} if argument: arguments["strip_size"] = 2**18 im.save(out, **arguments) with Image.open(out) as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) assert len(im.tag_v2[STRIPOFFSETS]) == 1 finally: TiffImagePlugin.STRIP_SIZE = 65536 @pytest.mark.parametrize("compression", ("tiff_adobe_deflate", None)) - def test_save_zero(self, compression, tmp_path): + def test_save_zero(self, compression: str | None, tmp_path: Path) -> None: im = Image.new("RGB", (0, 0)) out = str(tmp_path / "temp.tif") with pytest.raises(SystemError): im.save(out, compression=compression) - def test_save_many_compressed(self, tmp_path): + def test_save_many_compressed(self, tmp_path: Path) -> None: im = hopper() out = str(tmp_path / "temp.tif") for _ in range(10000): @@ -1133,7 +1142,7 @@ class TestFileLibTiff(LibTiffTestCase): ("Tests/images/child_ifd_jpeg.tiff", (20,)), ), ) - def test_get_child_images(self, path, sizes): + def test_get_child_images(self, path: str, sizes: tuple[int, ...]) -> None: with Image.open(path) as im: ims = im.get_child_images() diff --git a/Tests/test_file_libtiff_small.py b/Tests/test_file_libtiff_small.py index 171e4a3f8..617e1e89c 100644 --- a/Tests/test_file_libtiff_small.py +++ b/Tests/test_file_libtiff_small.py @@ -1,6 +1,7 @@ from __future__ import annotations from io import BytesIO +from pathlib import Path from PIL import Image @@ -8,7 +9,6 @@ from .test_file_libtiff import LibTiffTestCase class TestFileLibTiffSmall(LibTiffTestCase): - """The small lena image was failing on open in the libtiff decoder because the file pointer was set to the wrong place by a spurious seek. It wasn't failing with the byteio method. @@ -17,7 +17,7 @@ class TestFileLibTiffSmall(LibTiffTestCase): file just before reading in libtiff. These tests remain to ensure that it stays fixed.""" - def test_g4_hopper_file(self, tmp_path): + def test_g4_hopper_file(self, tmp_path: Path) -> None: """Testing the open file load path""" test_file = "Tests/images/hopper_g4.tif" @@ -26,7 +26,7 @@ class TestFileLibTiffSmall(LibTiffTestCase): assert im.size == (128, 128) self._assert_noerr(tmp_path, im) - def test_g4_hopper_bytesio(self, tmp_path): + def test_g4_hopper_bytesio(self, tmp_path: Path) -> None: """Testing the bytesio loading code path""" test_file = "Tests/images/hopper_g4.tif" s = BytesIO() @@ -37,7 +37,7 @@ class TestFileLibTiffSmall(LibTiffTestCase): assert im.size == (128, 128) self._assert_noerr(tmp_path, im) - def test_g4_hopper(self, tmp_path): + def test_g4_hopper(self, tmp_path: Path) -> None: """The 128x128 lena image failed for some reason.""" test_file = "Tests/images/hopper_g4.tif" diff --git a/Tests/test_file_mcidas.py b/Tests/test_file_mcidas.py index 73eba5cc8..e11e6bb52 100644 --- a/Tests/test_file_mcidas.py +++ b/Tests/test_file_mcidas.py @@ -7,19 +7,19 @@ from PIL import Image, McIdasImagePlugin from .helper import assert_image_equal_tofile -def test_invalid_file(): +def test_invalid_file() -> None: invalid_file = "Tests/images/flower.jpg" with pytest.raises(SyntaxError): McIdasImagePlugin.McIdasImageFile(invalid_file) -def test_valid_file(): +def test_valid_file() -> None: # Arrange # https://ghrc.nsstc.nasa.gov/hydro/details/cmx3g8 # https://ghrc.nsstc.nasa.gov/pub/fieldCampaigns/camex3/cmx3g8/browse/ test_file = "Tests/images/cmx3g8_wv_1998.260_0745_mcidas.ara" - saved_file = "Tests/images/cmx3g8_wv_1998.260_0745_mcidas.png" + saved_file = "Tests/images/cmx3g8_wv_1998.260_0745_mcidas.tiff" # Act with Image.open(test_file) as im: diff --git a/Tests/test_file_mic.py b/Tests/test_file_mic.py index 8c43f7d7a..9a6f13ea3 100644 --- a/Tests/test_file_mic.py +++ b/Tests/test_file_mic.py @@ -13,7 +13,7 @@ pytestmark = skip_unless_feature("libtiff") TEST_FILE = "Tests/images/hopper.mic" -def test_sanity(): +def test_sanity() -> None: with Image.open(TEST_FILE) as im: im.load() assert im.mode == "RGBA" @@ -28,22 +28,22 @@ def test_sanity(): assert_image_similar(im, im2, 10) -def test_n_frames(): +def test_n_frames() -> None: with Image.open(TEST_FILE) as im: assert im.n_frames == 1 -def test_is_animated(): +def test_is_animated() -> None: with Image.open(TEST_FILE) as im: assert not im.is_animated -def test_tell(): +def test_tell() -> None: with Image.open(TEST_FILE) as im: assert im.tell() == 0 -def test_seek(): +def test_seek() -> None: with Image.open(TEST_FILE) as im: im.seek(0) assert im.tell() == 0 @@ -53,7 +53,7 @@ def test_seek(): assert im.tell() == 0 -def test_close(): +def test_close() -> None: with Image.open(TEST_FILE) as im: pass assert im.ole.fp.closed @@ -63,7 +63,7 @@ def test_close(): assert im.ole.fp.closed -def test_invalid_file(): +def test_invalid_file() -> None: # Test an invalid OLE file invalid_file = "Tests/images/flower.jpg" with pytest.raises(SyntaxError): diff --git a/Tests/test_file_mpo.py b/Tests/test_file_mpo.py index c7121ea28..f105428ca 100644 --- a/Tests/test_file_mpo.py +++ b/Tests/test_file_mpo.py @@ -2,10 +2,11 @@ from __future__ import annotations import warnings from io import BytesIO +from typing import Any, cast import pytest -from PIL import Image +from PIL import Image, MpoImagePlugin from .helper import ( assert_image_equal, @@ -19,18 +20,15 @@ test_files = ["Tests/images/sugarshack.mpo", "Tests/images/frozenpond.mpo"] pytestmark = skip_unless_feature("jpg") -def roundtrip(im, **options): +def roundtrip(im: Image.Image, **options: Any) -> MpoImagePlugin.MpoImageFile: out = BytesIO() im.save(out, "MPO", **options) - test_bytes = out.tell() out.seek(0) - im = Image.open(out) - im.bytes = test_bytes # for testing only - return im + return cast(MpoImagePlugin.MpoImageFile, Image.open(out)) @pytest.mark.parametrize("test_file", test_files) -def test_sanity(test_file): +def test_sanity(test_file: str) -> None: with Image.open(test_file) as im: im.load() assert im.mode == "RGB" @@ -39,8 +37,8 @@ def test_sanity(test_file): @pytest.mark.skipif(is_pypy(), reason="Requires CPython") -def test_unclosed_file(): - def open(): +def test_unclosed_file() -> None: + def open() -> None: im = Image.open(test_files[0]) im.load() @@ -48,14 +46,14 @@ def test_unclosed_file(): open() -def test_closed_file(): +def test_closed_file() -> None: with warnings.catch_warnings(): im = Image.open(test_files[0]) im.load() im.close() -def test_seek_after_close(): +def test_seek_after_close() -> None: im = Image.open(test_files[0]) im.close() @@ -63,14 +61,14 @@ def test_seek_after_close(): im.seek(1) -def test_context_manager(): +def test_context_manager() -> None: with warnings.catch_warnings(): with Image.open(test_files[0]) as im: im.load() @pytest.mark.parametrize("test_file", test_files) -def test_app(test_file): +def test_app(test_file: str) -> None: # Test APP/COM reader (@PIL135) with Image.open(test_file) as im: assert im.applist[0][0] == "APP1" @@ -82,7 +80,7 @@ def test_app(test_file): @pytest.mark.parametrize("test_file", test_files) -def test_exif(test_file): +def test_exif(test_file: str) -> None: with Image.open(test_file) as im_original: im_reloaded = roundtrip(im_original, save_all=True, exif=im_original.getexif()) @@ -93,7 +91,7 @@ def test_exif(test_file): assert info[34665] == 188 -def test_frame_size(): +def test_frame_size() -> None: # This image has been hexedited to contain a different size # in the EXIF data of the second frame with Image.open("Tests/images/sugarshack_frame_size.mpo") as im: @@ -106,7 +104,7 @@ def test_frame_size(): assert im.size == (640, 480) -def test_ignore_frame_size(): +def test_ignore_frame_size() -> None: # Ignore the different size of the second frame # since this is not a "Large Thumbnail" image with Image.open("Tests/images/ignore_frame_size.mpo") as im: @@ -120,7 +118,7 @@ def test_ignore_frame_size(): assert im.size == (64, 64) -def test_parallax(): +def test_parallax() -> None: # Nintendo with Image.open("Tests/images/sugarshack.mpo") as im: exif = im.getexif() @@ -133,7 +131,7 @@ def test_parallax(): assert exif.get_ifd(0x927C)[0xB211] == -3.125 -def test_reload_exif_after_seek(): +def test_reload_exif_after_seek() -> None: with Image.open("Tests/images/sugarshack.mpo") as im: exif = im.getexif() del exif[296] @@ -143,14 +141,14 @@ def test_reload_exif_after_seek(): @pytest.mark.parametrize("test_file", test_files) -def test_mp(test_file): +def test_mp(test_file: str) -> None: with Image.open(test_file) as im: mpinfo = im._getmp() assert mpinfo[45056] == b"0100" assert mpinfo[45057] == 2 -def test_mp_offset(): +def test_mp_offset() -> None: # This image has been manually hexedited to have an IFD offset of 10 # in APP2 data, in contrast to normal 8 with Image.open("Tests/images/sugarshack_ifd_offset.mpo") as im: @@ -159,7 +157,7 @@ def test_mp_offset(): assert mpinfo[45057] == 2 -def test_mp_no_data(): +def test_mp_no_data() -> None: # This image has been manually hexedited to have the second frame # beyond the end of the file with Image.open("Tests/images/sugarshack_no_data.mpo") as im: @@ -168,7 +166,7 @@ def test_mp_no_data(): @pytest.mark.parametrize("test_file", test_files) -def test_mp_attribute(test_file): +def test_mp_attribute(test_file: str) -> None: with Image.open(test_file) as im: mpinfo = im._getmp() for frame_number, mpentry in enumerate(mpinfo[0xB002]): @@ -185,7 +183,7 @@ def test_mp_attribute(test_file): @pytest.mark.parametrize("test_file", test_files) -def test_seek(test_file): +def test_seek(test_file: str) -> None: with Image.open(test_file) as im: assert im.tell() == 0 # prior to first image raises an error, both blatant and borderline @@ -209,13 +207,13 @@ def test_seek(test_file): assert im.tell() == 0 -def test_n_frames(): +def test_n_frames() -> None: with Image.open("Tests/images/sugarshack.mpo") as im: assert im.n_frames == 2 assert im.is_animated -def test_eoferror(): +def test_eoferror() -> None: with Image.open("Tests/images/sugarshack.mpo") as im: n_frames = im.n_frames @@ -229,7 +227,7 @@ def test_eoferror(): @pytest.mark.parametrize("test_file", test_files) -def test_image_grab(test_file): +def test_image_grab(test_file: str) -> None: with Image.open(test_file) as im: assert im.tell() == 0 im0 = im.tobytes() @@ -244,7 +242,7 @@ def test_image_grab(test_file): @pytest.mark.parametrize("test_file", test_files) -def test_save(test_file): +def test_save(test_file: str) -> None: with Image.open(test_file) as im: assert im.tell() == 0 jpg0 = roundtrip(im) @@ -255,7 +253,7 @@ def test_save(test_file): assert_image_similar(im, jpg1, 30) -def test_save_all(): +def test_save_all() -> None: for test_file in test_files: with Image.open(test_file) as im: im_reloaded = roundtrip(im, save_all=True) diff --git a/Tests/test_file_msp.py b/Tests/test_file_msp.py index 9037ea33b..b0964aabe 100644 --- a/Tests/test_file_msp.py +++ b/Tests/test_file_msp.py @@ -1,6 +1,7 @@ from __future__ import annotations import os +from pathlib import Path import pytest @@ -13,7 +14,7 @@ EXTRA_DIR = "Tests/images/picins" YA_EXTRA_DIR = "Tests/images/msp" -def test_sanity(tmp_path): +def test_sanity(tmp_path: Path) -> None: test_file = str(tmp_path / "temp.msp") hopper("1").save(test_file) @@ -25,14 +26,14 @@ def test_sanity(tmp_path): assert im.format == "MSP" -def test_invalid_file(): +def test_invalid_file() -> None: invalid_file = "Tests/images/flower.jpg" with pytest.raises(SyntaxError): MspImagePlugin.MspImageFile(invalid_file) -def test_bad_checksum(): +def test_bad_checksum() -> None: # Arrange # This was created by forcing Pillow to save with checksum=0 bad_checksum = "Tests/images/hopper_bad_checksum.msp" @@ -42,7 +43,7 @@ def test_bad_checksum(): MspImagePlugin.MspImageFile(bad_checksum) -def test_open_windows_v1(): +def test_open_windows_v1() -> None: # Arrange # Act with Image.open(TEST_FILE) as im: @@ -51,7 +52,7 @@ def test_open_windows_v1(): assert isinstance(im, MspImagePlugin.MspImageFile) -def _assert_file_image_equal(source_path, target_path): +def _assert_file_image_equal(source_path: str, target_path: str) -> None: with Image.open(source_path) as im: assert_image_equal_tofile(im, target_path) @@ -59,7 +60,7 @@ def _assert_file_image_equal(source_path, target_path): @pytest.mark.skipif( not os.path.exists(EXTRA_DIR), reason="Extra image files not installed" ) -def test_open_windows_v2(): +def test_open_windows_v2() -> None: files = ( os.path.join(EXTRA_DIR, f) for f in os.listdir(EXTRA_DIR) @@ -72,7 +73,7 @@ def test_open_windows_v2(): @pytest.mark.skipif( not os.path.exists(YA_EXTRA_DIR), reason="Even More Extra image files not installed" ) -def test_msp_v2(): +def test_msp_v2() -> None: for f in os.listdir(YA_EXTRA_DIR): if ".MSP" not in f: continue @@ -80,7 +81,7 @@ def test_msp_v2(): _assert_file_image_equal(path, path.replace(".MSP", ".png")) -def test_cannot_save_wrong_mode(tmp_path): +def test_cannot_save_wrong_mode(tmp_path: Path) -> None: # Arrange im = hopper() filename = str(tmp_path / "temp.msp") diff --git a/Tests/test_file_palm.py b/Tests/test_file_palm.py index eba694153..194f39b30 100644 --- a/Tests/test_file_palm.py +++ b/Tests/test_file_palm.py @@ -2,6 +2,7 @@ from __future__ import annotations import os.path import subprocess +from pathlib import Path import pytest @@ -10,7 +11,7 @@ from PIL import Image from .helper import assert_image_equal, hopper, magick_command -def helper_save_as_palm(tmp_path, mode): +def helper_save_as_palm(tmp_path: Path, mode: str) -> None: # Arrange im = hopper(mode) outfile = str(tmp_path / ("temp_" + mode + ".palm")) @@ -23,7 +24,7 @@ def helper_save_as_palm(tmp_path, mode): assert os.path.getsize(outfile) > 0 -def open_with_magick(magick, tmp_path, f): +def open_with_magick(magick: list[str], tmp_path: Path, f: str) -> Image.Image: outfile = str(tmp_path / "temp.png") rc = subprocess.call( magick + [f, outfile], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT @@ -32,7 +33,7 @@ def open_with_magick(magick, tmp_path, f): return Image.open(outfile) -def roundtrip(tmp_path, mode): +def roundtrip(tmp_path: Path, mode: str) -> None: magick = magick_command() if not magick: return @@ -45,7 +46,7 @@ def roundtrip(tmp_path, mode): assert_image_equal(converted, im) -def test_monochrome(tmp_path): +def test_monochrome(tmp_path: Path) -> None: # Arrange mode = "1" @@ -55,7 +56,7 @@ def test_monochrome(tmp_path): @pytest.mark.xfail(reason="Palm P image is wrong") -def test_p_mode(tmp_path): +def test_p_mode(tmp_path: Path) -> None: # Arrange mode = "P" @@ -65,6 +66,6 @@ def test_p_mode(tmp_path): @pytest.mark.parametrize("mode", ("L", "RGB")) -def test_oserror(tmp_path, mode): +def test_oserror(tmp_path: Path, mode: str) -> None: with pytest.raises(OSError): helper_save_as_palm(tmp_path, mode) diff --git a/Tests/test_file_pcd.py b/Tests/test_file_pcd.py index 1a37c6ab3..81a316fc1 100644 --- a/Tests/test_file_pcd.py +++ b/Tests/test_file_pcd.py @@ -3,7 +3,7 @@ from __future__ import annotations from PIL import Image -def test_load_raw(): +def test_load_raw() -> None: with Image.open("Tests/images/hopper.pcd") as im: im.load() # should not segfault. diff --git a/Tests/test_file_pcx.py b/Tests/test_file_pcx.py index 2565e0b6d..ab9f9663e 100644 --- a/Tests/test_file_pcx.py +++ b/Tests/test_file_pcx.py @@ -1,5 +1,7 @@ from __future__ import annotations +from pathlib import Path + import pytest from PIL import Image, ImageFile, PcxImagePlugin @@ -7,7 +9,7 @@ from PIL import Image, ImageFile, PcxImagePlugin from .helper import assert_image_equal, hopper -def _roundtrip(tmp_path, im): +def _roundtrip(tmp_path: Path, im: Image.Image) -> None: f = str(tmp_path / "temp.pcx") im.save(f) with Image.open(f) as im2: @@ -18,7 +20,7 @@ def _roundtrip(tmp_path, im): assert_image_equal(im2, im) -def test_sanity(tmp_path): +def test_sanity(tmp_path: Path) -> None: for mode in ("1", "L", "P", "RGB"): _roundtrip(tmp_path, hopper(mode)) @@ -34,7 +36,7 @@ def test_sanity(tmp_path): im.save(f) -def test_invalid_file(): +def test_invalid_file() -> None: invalid_file = "Tests/images/flower.jpg" with pytest.raises(SyntaxError): @@ -42,7 +44,7 @@ def test_invalid_file(): @pytest.mark.parametrize("mode", ("1", "L", "P", "RGB")) -def test_odd(tmp_path, mode): +def test_odd(tmp_path: Path, mode: str) -> None: # See issue #523, odd sized images should have a stride that's even. # Not that ImageMagick or GIMP write PCX that way. # We were not handling properly. @@ -51,7 +53,7 @@ def test_odd(tmp_path, mode): _roundtrip(tmp_path, hopper(mode).resize((511, 511))) -def test_odd_read(): +def test_odd_read() -> None: # Reading an image with an odd stride, making it malformed with Image.open("Tests/images/odd_stride.pcx") as im: im.load() @@ -59,7 +61,7 @@ def test_odd_read(): assert im.size == (371, 150) -def test_pil184(): +def test_pil184() -> None: # Check reading of files where xmin/xmax is not zero. test_file = "Tests/images/pil184.pcx" @@ -71,7 +73,7 @@ def test_pil184(): assert im.histogram()[0] + im.histogram()[255] == 447 * 144 -def test_1px_width(tmp_path): +def test_1px_width(tmp_path: Path) -> None: im = Image.new("L", (1, 256)) px = im.load() for y in range(256): @@ -79,7 +81,7 @@ def test_1px_width(tmp_path): _roundtrip(tmp_path, im) -def test_large_count(tmp_path): +def test_large_count(tmp_path: Path) -> None: im = Image.new("L", (256, 1)) px = im.load() for x in range(256): @@ -87,7 +89,7 @@ def test_large_count(tmp_path): _roundtrip(tmp_path, im) -def _test_buffer_overflow(tmp_path, im, size=1024): +def _test_buffer_overflow(tmp_path: Path, im: Image.Image, size: int = 1024) -> None: _last = ImageFile.MAXBLOCK ImageFile.MAXBLOCK = size try: @@ -96,7 +98,7 @@ def _test_buffer_overflow(tmp_path, im, size=1024): ImageFile.MAXBLOCK = _last -def test_break_in_count_overflow(tmp_path): +def test_break_in_count_overflow(tmp_path: Path) -> None: im = Image.new("L", (256, 5)) px = im.load() for y in range(4): @@ -105,7 +107,7 @@ def test_break_in_count_overflow(tmp_path): _test_buffer_overflow(tmp_path, im) -def test_break_one_in_loop(tmp_path): +def test_break_one_in_loop(tmp_path: Path) -> None: im = Image.new("L", (256, 5)) px = im.load() for y in range(5): @@ -114,7 +116,7 @@ def test_break_one_in_loop(tmp_path): _test_buffer_overflow(tmp_path, im) -def test_break_many_in_loop(tmp_path): +def test_break_many_in_loop(tmp_path: Path) -> None: im = Image.new("L", (256, 5)) px = im.load() for y in range(4): @@ -125,7 +127,7 @@ def test_break_many_in_loop(tmp_path): _test_buffer_overflow(tmp_path, im) -def test_break_one_at_end(tmp_path): +def test_break_one_at_end(tmp_path: Path) -> None: im = Image.new("L", (256, 5)) px = im.load() for y in range(5): @@ -135,7 +137,7 @@ def test_break_one_at_end(tmp_path): _test_buffer_overflow(tmp_path, im) -def test_break_many_at_end(tmp_path): +def test_break_many_at_end(tmp_path: Path) -> None: im = Image.new("L", (256, 5)) px = im.load() for y in range(5): @@ -147,7 +149,7 @@ def test_break_many_at_end(tmp_path): _test_buffer_overflow(tmp_path, im) -def test_break_padding(tmp_path): +def test_break_padding(tmp_path: Path) -> None: im = Image.new("L", (257, 5)) px = im.load() for y in range(5): diff --git a/Tests/test_file_pdf.py b/Tests/test_file_pdf.py index 30c54c963..d39a86565 100644 --- a/Tests/test_file_pdf.py +++ b/Tests/test_file_pdf.py @@ -5,6 +5,8 @@ import os import os.path import tempfile import time +from pathlib import Path +from typing import Any, Generator import pytest @@ -13,7 +15,7 @@ from PIL import Image, PdfParser, features from .helper import hopper, mark_if_feature_version, skip_unless_feature -def helper_save_as_pdf(tmp_path, mode, **kwargs): +def helper_save_as_pdf(tmp_path: Path, mode: str, **kwargs: Any) -> str: # Arrange im = hopper(mode) outfile = str(tmp_path / ("temp_" + mode + ".pdf")) @@ -40,17 +42,17 @@ def helper_save_as_pdf(tmp_path, mode, **kwargs): @pytest.mark.parametrize("mode", ("L", "P", "RGB", "CMYK")) -def test_save(tmp_path, mode): +def test_save(tmp_path: Path, mode: str) -> None: helper_save_as_pdf(tmp_path, mode) @skip_unless_feature("jpg_2000") @pytest.mark.parametrize("mode", ("LA", "RGBA")) -def test_save_alpha(tmp_path, mode): +def test_save_alpha(tmp_path: Path, mode: str) -> None: helper_save_as_pdf(tmp_path, mode) -def test_p_alpha(tmp_path): +def test_p_alpha(tmp_path: Path) -> None: # Arrange outfile = str(tmp_path / "temp.pdf") with Image.open("Tests/images/pil123p.png") as im: @@ -66,7 +68,7 @@ def test_p_alpha(tmp_path): assert b"\n/SMask " in contents -def test_monochrome(tmp_path): +def test_monochrome(tmp_path: Path) -> None: # Arrange mode = "1" @@ -75,7 +77,7 @@ def test_monochrome(tmp_path): assert os.path.getsize(outfile) < (5000 if features.check("libtiff") else 15000) -def test_unsupported_mode(tmp_path): +def test_unsupported_mode(tmp_path: Path) -> None: im = hopper("PA") outfile = str(tmp_path / "temp_PA.pdf") @@ -83,7 +85,7 @@ def test_unsupported_mode(tmp_path): im.save(outfile) -def test_resolution(tmp_path): +def test_resolution(tmp_path: Path) -> None: im = hopper() outfile = str(tmp_path / "temp.pdf") @@ -111,7 +113,7 @@ def test_resolution(tmp_path): {"dpi": (75, 150), "resolution": 200}, ), ) -def test_dpi(params, tmp_path): +def test_dpi(params: dict[str, int | tuple[int, int]], tmp_path: Path) -> None: im = hopper() outfile = str(tmp_path / "temp.pdf") @@ -135,7 +137,7 @@ def test_dpi(params, tmp_path): @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) -def test_save_all(tmp_path): +def test_save_all(tmp_path: Path) -> None: # Single frame image helper_save_as_pdf(tmp_path, "RGB", save_all=True) @@ -155,7 +157,7 @@ def test_save_all(tmp_path): assert os.path.getsize(outfile) > 0 # Test appending using a generator - def im_generator(ims): + def im_generator(ims: list[Image.Image]) -> Generator[Image.Image, None, None]: yield from ims im.save(outfile, save_all=True, append_images=im_generator(ims)) @@ -171,7 +173,7 @@ def test_save_all(tmp_path): assert os.path.getsize(outfile) > 0 -def test_multiframe_normal_save(tmp_path): +def test_multiframe_normal_save(tmp_path: Path) -> None: # Test saving a multiframe image without save_all with Image.open("Tests/images/dispose_bgnd.gif") as im: outfile = str(tmp_path / "temp.pdf") @@ -181,7 +183,7 @@ def test_multiframe_normal_save(tmp_path): assert os.path.getsize(outfile) > 0 -def test_pdf_open(tmp_path): +def test_pdf_open(tmp_path: Path) -> None: # fail on a buffer full of null bytes with pytest.raises(PdfParser.PdfFormatError): PdfParser.PdfParser(buf=bytearray(65536)) @@ -218,14 +220,14 @@ def test_pdf_open(tmp_path): assert not hopper_pdf.should_close_file -def test_pdf_append_fails_on_nonexistent_file(): +def test_pdf_append_fails_on_nonexistent_file() -> None: im = hopper("RGB") with tempfile.TemporaryDirectory() as temp_dir: with pytest.raises(OSError): im.save(os.path.join(temp_dir, "nonexistent.pdf"), append=True) -def check_pdf_pages_consistency(pdf): +def check_pdf_pages_consistency(pdf: PdfParser.PdfParser) -> None: pages_info = pdf.read_indirect(pdf.pages_ref) assert b"Parent" not in pages_info assert b"Kids" in pages_info @@ -243,7 +245,7 @@ def check_pdf_pages_consistency(pdf): assert kids_not_used == [] -def test_pdf_append(tmp_path): +def test_pdf_append(tmp_path: Path) -> None: # make a PDF file pdf_filename = helper_save_as_pdf(tmp_path, "RGB", producer="PdfParser") @@ -294,7 +296,7 @@ def test_pdf_append(tmp_path): check_pdf_pages_consistency(pdf) -def test_pdf_info(tmp_path): +def test_pdf_info(tmp_path: Path) -> None: # make a PDF file pdf_filename = helper_save_as_pdf( tmp_path, @@ -323,7 +325,7 @@ def test_pdf_info(tmp_path): check_pdf_pages_consistency(pdf) -def test_pdf_append_to_bytesio(): +def test_pdf_append_to_bytesio() -> None: im = hopper("RGB") f = io.BytesIO() im.save(f, format="PDF") @@ -338,7 +340,7 @@ def test_pdf_append_to_bytesio(): @pytest.mark.timeout(1) @pytest.mark.skipif("PILLOW_VALGRIND_TEST" in os.environ, reason="Valgrind is slower") @pytest.mark.parametrize("newline", (b"\r", b"\n")) -def test_redos(newline): +def test_redos(newline: bytes) -> None: malicious = b" trailer<<>>" + newline * 3456 # This particular exception isn't relevant here. diff --git a/Tests/test_file_pixar.py b/Tests/test_file_pixar.py index c6ddc54e7..8f208cfbf 100644 --- a/Tests/test_file_pixar.py +++ b/Tests/test_file_pixar.py @@ -9,7 +9,7 @@ from .helper import assert_image_similar, hopper TEST_FILE = "Tests/images/hopper.pxr" -def test_sanity(): +def test_sanity() -> None: with Image.open(TEST_FILE) as im: im.load() assert im.mode == "RGB" @@ -21,7 +21,7 @@ def test_sanity(): assert_image_similar(im, im2, 4.8) -def test_invalid_file(): +def test_invalid_file() -> None: invalid_file = "Tests/images/flower.jpg" with pytest.raises(SyntaxError): diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index ae2a4772b..1a8d58bd6 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -5,6 +5,9 @@ import sys import warnings import zlib from io import BytesIO +from pathlib import Path +from types import ModuleType +from typing import Any, cast import pytest @@ -21,6 +24,7 @@ from .helper import ( skip_unless_feature, ) +ElementTree: ModuleType | None try: from defusedxml import ElementTree except ImportError: @@ -35,7 +39,7 @@ TEST_PNG_FILE = "Tests/images/hopper.png" MAGIC = PngImagePlugin._MAGIC -def chunk(cid, *data): +def chunk(cid: bytes, *data: bytes) -> bytes: test_file = BytesIO() PngImagePlugin.putchunk(*(test_file, cid) + data) return test_file.getvalue() @@ -51,20 +55,20 @@ HEAD = MAGIC + IHDR TAIL = IDAT + IEND -def load(data): +def load(data: bytes) -> Image.Image: return Image.open(BytesIO(data)) -def roundtrip(im, **options): +def roundtrip(im: Image.Image, **options: Any) -> PngImagePlugin.PngImageFile: out = BytesIO() im.save(out, "PNG", **options) out.seek(0) - return Image.open(out) + return cast(PngImagePlugin.PngImageFile, Image.open(out)) @skip_unless_feature("zlib") class TestFilePng: - def get_chunks(self, filename): + def get_chunks(self, filename: str) -> list[bytes]: chunks = [] with open(filename, "rb") as fp: fp.read(8) @@ -79,7 +83,7 @@ class TestFilePng: png.crc(cid, s) return chunks - def test_sanity(self, tmp_path): + def test_sanity(self, tmp_path: Path) -> None: # internal version number assert re.search(r"\d+(\.\d+){1,3}$", features.version_codec("zlib")) @@ -98,17 +102,17 @@ class TestFilePng: im = hopper(mode) im.save(test_file) with Image.open(test_file) as reloaded: - if mode in ("I;16", "I;16B"): + if mode in ("I", "I;16B"): reloaded = reloaded.convert(mode) assert_image_equal(reloaded, im) - def test_invalid_file(self): + def test_invalid_file(self) -> None: invalid_file = "Tests/images/flower.jpg" with pytest.raises(SyntaxError): PngImagePlugin.PngImageFile(invalid_file) - def test_broken(self): + def test_broken(self) -> None: # Check reading of totally broken files. In this case, the test # file was checked into Subversion as a text file. @@ -117,7 +121,7 @@ class TestFilePng: with Image.open(test_file): pass - def test_bad_text(self): + def test_bad_text(self) -> None: # Make sure PIL can read malformed tEXt chunks (@PIL152) im = load(HEAD + chunk(b"tEXt") + TAIL) @@ -135,7 +139,7 @@ class TestFilePng: im = load(HEAD + chunk(b"tEXt", b"spam\0egg\0") + TAIL) assert im.info == {"spam": "egg\x00"} - def test_bad_ztxt(self): + def test_bad_ztxt(self) -> None: # Test reading malformed zTXt chunks (python-pillow/Pillow#318) im = load(HEAD + chunk(b"zTXt") + TAIL) @@ -156,7 +160,7 @@ class TestFilePng: im = load(HEAD + chunk(b"zTXt", b"spam\0\0" + zlib.compress(b"egg")) + TAIL) assert im.info == {"spam": "egg"} - def test_bad_itxt(self): + def test_bad_itxt(self) -> None: im = load(HEAD + chunk(b"iTXt") + TAIL) assert im.info == {} @@ -200,7 +204,7 @@ class TestFilePng: assert im.info["spam"].lang == "en" assert im.info["spam"].tkey == "Spam" - def test_interlace(self): + def test_interlace(self) -> None: test_file = "Tests/images/pil123p.png" with Image.open(test_file) as im: assert_image(im, "P", (162, 150)) @@ -215,7 +219,7 @@ class TestFilePng: im.load() - def test_load_transparent_p(self): + def test_load_transparent_p(self) -> None: test_file = "Tests/images/pil123p.png" with Image.open(test_file) as im: assert_image(im, "P", (162, 150)) @@ -225,7 +229,7 @@ class TestFilePng: # image has 124 unique alpha values assert len(im.getchannel("A").getcolors()) == 124 - def test_load_transparent_rgb(self): + def test_load_transparent_rgb(self) -> None: test_file = "Tests/images/rgb_trns.png" with Image.open(test_file) as im: assert im.info["transparency"] == (0, 255, 52) @@ -237,7 +241,7 @@ class TestFilePng: # image has 876 transparent pixels assert im.getchannel("A").getcolors()[0][0] == 876 - def test_save_p_transparent_palette(self, tmp_path): + def test_save_p_transparent_palette(self, tmp_path: Path) -> None: in_file = "Tests/images/pil123p.png" with Image.open(in_file) as im: # 'transparency' contains a byte string with the opacity for @@ -258,7 +262,7 @@ class TestFilePng: # image has 124 unique alpha values assert len(im.getchannel("A").getcolors()) == 124 - def test_save_p_single_transparency(self, tmp_path): + def test_save_p_single_transparency(self, tmp_path: Path) -> None: in_file = "Tests/images/p_trns_single.png" with Image.open(in_file) as im: # pixel value 164 is full transparent @@ -281,7 +285,7 @@ class TestFilePng: # image has 876 transparent pixels assert im.getchannel("A").getcolors()[0][0] == 876 - def test_save_p_transparent_black(self, tmp_path): + def test_save_p_transparent_black(self, tmp_path: Path) -> None: # check if solid black image with full transparency # is supported (check for #1838) im = Image.new("RGBA", (10, 10), (0, 0, 0, 0)) @@ -299,9 +303,9 @@ class TestFilePng: assert_image(im, "RGBA", (10, 10)) assert im.getcolors() == [(100, (0, 0, 0, 0))] - def test_save_grayscale_transparency(self, tmp_path): - for mode, num_transparent in {"1": 1994, "L": 559, "I": 559}.items(): - in_file = "Tests/images/" + mode.lower() + "_trns.png" + def test_save_grayscale_transparency(self, tmp_path: Path) -> None: + for mode, num_transparent in {"1": 1994, "L": 559, "I;16": 559}.items(): + in_file = "Tests/images/" + mode.split(";")[0].lower() + "_trns.png" with Image.open(in_file) as im: assert im.mode == mode assert im.info["transparency"] == 255 @@ -320,13 +324,13 @@ class TestFilePng: test_im_rgba = test_im.convert("RGBA") assert test_im_rgba.getchannel("A").getcolors()[0][0] == num_transparent - def test_save_rgb_single_transparency(self, tmp_path): + def test_save_rgb_single_transparency(self, tmp_path: Path) -> None: in_file = "Tests/images/caption_6_33_22.png" with Image.open(in_file) as im: test_file = str(tmp_path / "temp.png") im.save(test_file) - def test_load_verify(self): + def test_load_verify(self) -> None: # Check open/load/verify exception (@PIL150) with Image.open(TEST_PNG_FILE) as im: @@ -339,7 +343,7 @@ class TestFilePng: with pytest.raises(RuntimeError): im.verify() - def test_verify_struct_error(self): + def test_verify_struct_error(self) -> None: # Check open/load/verify exception (#1755) # offsets to test, -10: breaks in i32() in read. (OSError) @@ -355,7 +359,7 @@ class TestFilePng: with pytest.raises((OSError, SyntaxError)): im.verify() - def test_verify_ignores_crc_error(self): + def test_verify_ignores_crc_error(self) -> None: # check ignores crc errors in ancillary chunks chunk_data = chunk(b"tEXt", b"spam") @@ -372,7 +376,7 @@ class TestFilePng: finally: ImageFile.LOAD_TRUNCATED_IMAGES = False - def test_verify_not_ignores_crc_error_in_required_chunk(self): + def test_verify_not_ignores_crc_error_in_required_chunk(self) -> None: # check does not ignore crc errors in required chunks image_data = MAGIC + IHDR[:-1] + b"q" + TAIL @@ -384,18 +388,18 @@ class TestFilePng: finally: ImageFile.LOAD_TRUNCATED_IMAGES = False - def test_roundtrip_dpi(self): + def test_roundtrip_dpi(self) -> None: # Check dpi roundtripping with Image.open(TEST_PNG_FILE) as im: im = roundtrip(im, dpi=(100.33, 100.33)) assert im.info["dpi"] == (100.33, 100.33) - def test_load_float_dpi(self): + def test_load_float_dpi(self) -> None: with Image.open(TEST_PNG_FILE) as im: assert im.info["dpi"] == (95.9866, 95.9866) - def test_roundtrip_text(self): + def test_roundtrip_text(self) -> None: # Check text roundtripping with Image.open(TEST_PNG_FILE) as im: @@ -407,7 +411,7 @@ class TestFilePng: assert im.info == {"TXT": "VALUE", "ZIP": "VALUE"} assert im.text == {"TXT": "VALUE", "ZIP": "VALUE"} - def test_roundtrip_itxt(self): + def test_roundtrip_itxt(self) -> None: # Check iTXt roundtripping im = Image.new("RGB", (32, 32)) @@ -423,7 +427,7 @@ class TestFilePng: assert im.text["eggs"].lang == "en" assert im.text["eggs"].tkey == "Eggs" - def test_nonunicode_text(self): + def test_nonunicode_text(self) -> None: # Check so that non-Unicode text is saved as a tEXt rather than iTXt im = Image.new("RGB", (32, 32)) @@ -432,10 +436,10 @@ class TestFilePng: im = roundtrip(im, pnginfo=info) assert isinstance(im.info["Text"], str) - def test_unicode_text(self): + def test_unicode_text(self) -> None: # Check preservation of non-ASCII characters - def rt_text(value): + def rt_text(value: str) -> None: im = Image.new("RGB", (32, 32)) info = PngImagePlugin.PngInfo() info.add_text("Text", value) @@ -448,7 +452,7 @@ class TestFilePng: rt_text(chr(0x4E00) + chr(0x66F0) + chr(0x9FBA) + chr(0x3042) + chr(0xAC00)) rt_text("A" + chr(0xC4) + chr(0x472) + chr(0x3042)) # Combined - def test_scary(self): + def test_scary(self) -> None: # Check reading of evil PNG file. For information, see: # http://scary.beasts.org/security/CESA-2004-001.txt # The first byte is removed from pngtest_bad.png @@ -462,7 +466,7 @@ class TestFilePng: with Image.open(pngfile): pass - def test_trns_rgb(self): + def test_trns_rgb(self) -> None: # Check writing and reading of tRNS chunks for RGB images. # Independent file sample provided by Sebastian Spaeth. @@ -477,7 +481,7 @@ class TestFilePng: im = roundtrip(im, transparency=(0, 1, 2)) assert im.info["transparency"] == (0, 1, 2) - def test_trns_p(self, tmp_path): + def test_trns_p(self, tmp_path: Path) -> None: # Check writing a transparency of 0, issue #528 im = hopper("P") im.info["transparency"] = 0 @@ -490,13 +494,13 @@ class TestFilePng: assert_image_equal(im2.convert("RGBA"), im.convert("RGBA")) - def test_trns_null(self): + def test_trns_null(self) -> None: # Check reading images with null tRNS value, issue #1239 test_file = "Tests/images/tRNS_null_1x1.png" with Image.open(test_file) as im: assert im.info["transparency"] == 0 - def test_save_icc_profile(self): + def test_save_icc_profile(self) -> None: with Image.open("Tests/images/icc_profile_none.png") as im: assert im.info["icc_profile"] is None @@ -506,40 +510,40 @@ class TestFilePng: im = roundtrip(im, icc_profile=expected_icc) assert im.info["icc_profile"] == expected_icc - def test_discard_icc_profile(self): + def test_discard_icc_profile(self) -> None: with Image.open("Tests/images/icc_profile.png") as im: assert "icc_profile" in im.info im = roundtrip(im, icc_profile=None) assert "icc_profile" not in im.info - def test_roundtrip_icc_profile(self): + def test_roundtrip_icc_profile(self) -> None: with Image.open("Tests/images/icc_profile.png") as im: expected_icc = im.info["icc_profile"] im = roundtrip(im) assert im.info["icc_profile"] == expected_icc - def test_roundtrip_no_icc_profile(self): + def test_roundtrip_no_icc_profile(self) -> None: with Image.open("Tests/images/icc_profile_none.png") as im: assert im.info["icc_profile"] is None im = roundtrip(im) assert "icc_profile" not in im.info - def test_repr_png(self): + def test_repr_png(self) -> None: im = hopper() with Image.open(BytesIO(im._repr_png_())) as repr_png: assert repr_png.format == "PNG" assert_image_equal(im, repr_png) - def test_repr_png_error_returns_none(self): + def test_repr_png_error_returns_none(self) -> None: im = hopper("F") assert im._repr_png_() is None - def test_chunk_order(self, tmp_path): + def test_chunk_order(self, tmp_path: Path) -> None: with Image.open("Tests/images/icc_profile.png") as im: test_file = str(tmp_path / "temp.png") im.convert("P").save(test_file, dpi=(100, 100)) @@ -560,17 +564,17 @@ class TestFilePng: # pHYs - before IDAT assert chunks.index(b"pHYs") < chunks.index(b"IDAT") - def test_getchunks(self): + def test_getchunks(self) -> None: im = hopper() chunks = PngImagePlugin.getchunks(im) assert len(chunks) == 3 - def test_read_private_chunks(self): + def test_read_private_chunks(self) -> None: with Image.open("Tests/images/exif.png") as im: assert im.private_chunks == [(b"orNT", b"\x01")] - def test_roundtrip_private_chunk(self): + def test_roundtrip_private_chunk(self) -> None: # Check private chunk roundtripping with Image.open(TEST_PNG_FILE) as im: @@ -588,7 +592,7 @@ class TestFilePng: (b"prIV", b"VALUE3", True), ] - def test_textual_chunks_after_idat(self): + def test_textual_chunks_after_idat(self) -> None: with Image.open("Tests/images/hopper.png") as im: assert "comment" in im.text for k, v in { @@ -615,7 +619,7 @@ class TestFilePng: with Image.open("Tests/images/hopper_idat_after_image_end.png") as im: assert im.text == {"TXT": "VALUE", "ZIP": "VALUE"} - def test_padded_idat(self): + def test_padded_idat(self) -> None: # This image has been manually hexedited # so that the IDAT chunk has padding at the end # Set MAXBLOCK to the length of the actual data @@ -635,7 +639,7 @@ class TestFilePng: @pytest.mark.parametrize( "cid", (b"IHDR", b"sRGB", b"pHYs", b"acTL", b"fcTL", b"fdAT") ) - def test_truncated_chunks(self, cid): + def test_truncated_chunks(self, cid: bytes) -> None: fp = BytesIO() with PngImagePlugin.PngStream(fp) as png: with pytest.raises(ValueError): @@ -645,7 +649,7 @@ class TestFilePng: png.call(cid, 0, 0) ImageFile.LOAD_TRUNCATED_IMAGES = False - def test_specify_bits(self, tmp_path): + def test_specify_bits(self, tmp_path: Path) -> None: im = hopper("P") out = str(tmp_path / "temp.png") @@ -654,7 +658,7 @@ class TestFilePng: with Image.open(out) as reloaded: assert len(reloaded.png.im_palette[1]) == 48 - def test_plte_length(self, tmp_path): + def test_plte_length(self, tmp_path: Path) -> None: im = Image.new("P", (1, 1)) im.putpalette((1, 1, 1)) @@ -664,7 +668,7 @@ class TestFilePng: with Image.open(out) as reloaded: assert len(reloaded.png.im_palette[1]) == 3 - def test_getxmp(self): + def test_getxmp(self) -> None: with Image.open("Tests/images/color_snakes.png") as im: if ElementTree is None: with pytest.warns( @@ -679,7 +683,7 @@ class TestFilePng: assert description["PixelXDimension"] == "10" assert description["subject"]["Seq"] is None - def test_exif(self): + def test_exif(self) -> None: # With an EXIF chunk with Image.open("Tests/images/exif.png") as im: exif = im._getexif() @@ -705,7 +709,7 @@ class TestFilePng: exif = im.getexif() assert exif[274] == 3 - def test_exif_save(self, tmp_path): + def test_exif_save(self, tmp_path: Path) -> None: # Test exif is not saved from info test_file = str(tmp_path / "temp.png") with Image.open("Tests/images/exif.png") as im: @@ -725,7 +729,7 @@ class TestFilePng: @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) - def test_exif_from_jpg(self, tmp_path): + def test_exif_from_jpg(self, tmp_path: Path) -> None: with Image.open("Tests/images/pil_sample_rgb.jpg") as im: test_file = str(tmp_path / "temp.png") im.save(test_file, exif=im.getexif()) @@ -734,7 +738,7 @@ class TestFilePng: exif = reloaded._getexif() assert exif[305] == "Adobe Photoshop CS Macintosh" - def test_exif_argument(self, tmp_path): + def test_exif_argument(self, tmp_path: Path) -> None: with Image.open(TEST_PNG_FILE) as im: test_file = str(tmp_path / "temp.png") im.save(test_file, exif=b"exifstring") @@ -742,11 +746,11 @@ class TestFilePng: with Image.open(test_file) as reloaded: assert reloaded.info["exif"] == b"Exif\x00\x00exifstring" - def test_tell(self): + def test_tell(self) -> None: with Image.open(TEST_PNG_FILE) as im: assert im.tell() == 0 - def test_seek(self): + def test_seek(self) -> None: with Image.open(TEST_PNG_FILE) as im: im.seek(0) @@ -754,7 +758,7 @@ class TestFilePng: im.seek(1) @pytest.mark.parametrize("buffer", (True, False)) - def test_save_stdout(self, buffer): + def test_save_stdout(self, buffer: bool) -> None: old_stdout = sys.stdout if buffer: @@ -779,6 +783,18 @@ class TestFilePng: with Image.open(mystdout) as reloaded: assert_image_equal_tofile(reloaded, TEST_PNG_FILE) + def test_truncated_end_chunk(self) -> None: + with Image.open("Tests/images/truncated_end_chunk.png") as im: + with pytest.raises(OSError): + im.load() + + ImageFile.LOAD_TRUNCATED_IMAGES = True + try: + with Image.open("Tests/images/truncated_end_chunk.png") as im: + assert_image_equal_tofile(im, "Tests/images/hopper.png") + finally: + ImageFile.LOAD_TRUNCATED_IMAGES = False + @pytest.mark.skipif(is_win32(), reason="Requires Unix or macOS") @skip_unless_feature("zlib") @@ -786,7 +802,7 @@ class TestTruncatedPngPLeaks(PillowLeakTestCase): mem_limit = 2 * 1024 # max increase in K iterations = 100 # Leak is 56k/iteration, this will leak 5.6megs - def test_leak_load(self): + def test_leak_load(self) -> None: with open("Tests/images/hopper.png", "rb") as f: DATA = BytesIO(f.read(16 * 1024)) @@ -794,7 +810,7 @@ class TestTruncatedPngPLeaks(PillowLeakTestCase): with Image.open(DATA) as im: im.load() - def core(): + def core() -> None: with Image.open(DATA) as im: im.load() diff --git a/Tests/test_file_ppm.py b/Tests/test_file_ppm.py index 32de42ed4..6a0a5a445 100644 --- a/Tests/test_file_ppm.py +++ b/Tests/test_file_ppm.py @@ -2,6 +2,7 @@ from __future__ import annotations import sys from io import BytesIO +from pathlib import Path import pytest @@ -18,7 +19,7 @@ from .helper import ( TEST_FILE = "Tests/images/hopper.ppm" -def test_sanity(): +def test_sanity() -> None: with Image.open(TEST_FILE) as im: assert im.mode == "RGB" assert im.size == (128, 128) @@ -69,7 +70,9 @@ def test_sanity(): ), ), ) -def test_arbitrary_maxval(data, mode, pixels): +def test_arbitrary_maxval( + data: bytes, mode: str, pixels: tuple[int | tuple[int, int, int], ...] +) -> None: fp = BytesIO(data) with Image.open(fp) as im: assert im.size == (3, 1) @@ -79,16 +82,16 @@ def test_arbitrary_maxval(data, mode, pixels): assert tuple(px[x, 0] for x in range(3)) == pixels -def test_16bit_pgm(): +def test_16bit_pgm() -> None: with Image.open("Tests/images/16_bit_binary.pgm") as im: assert im.mode == "I" assert im.size == (20, 100) assert im.get_format_mimetype() == "image/x-portable-graymap" - assert_image_equal_tofile(im, "Tests/images/16_bit_binary_pgm.png") + assert_image_equal_tofile(im, "Tests/images/16_bit_binary_pgm.tiff") -def test_16bit_pgm_write(tmp_path): +def test_16bit_pgm_write(tmp_path: Path) -> None: with Image.open("Tests/images/16_bit_binary.pgm") as im: filename = str(tmp_path / "temp.pgm") im.save(filename, "PPM") @@ -96,7 +99,7 @@ def test_16bit_pgm_write(tmp_path): assert_image_equal_tofile(im, filename) -def test_pnm(tmp_path): +def test_pnm(tmp_path: Path) -> None: with Image.open("Tests/images/hopper.pnm") as im: assert_image_similar(im, hopper(), 0.0001) @@ -106,7 +109,7 @@ def test_pnm(tmp_path): assert_image_equal_tofile(im, filename) -def test_pfm(tmp_path): +def test_pfm(tmp_path: Path) -> None: with Image.open("Tests/images/hopper.pfm") as im: assert im.info["scale"] == 1.0 assert_image_equal(im, hopper("F")) @@ -117,7 +120,7 @@ def test_pfm(tmp_path): assert_image_equal_tofile(im, filename) -def test_pfm_big_endian(tmp_path): +def test_pfm_big_endian(tmp_path: Path) -> None: with Image.open("Tests/images/hopper_be.pfm") as im: assert im.info["scale"] == 2.5 assert_image_equal(im, hopper("F")) @@ -138,7 +141,7 @@ def test_pfm_big_endian(tmp_path): b"Pf 1 1 -0.0 \0\0\0\0", ], ) -def test_pfm_invalid(data): +def test_pfm_invalid(data: bytes) -> None: with pytest.raises(ValueError): with Image.open(BytesIO(data)): pass @@ -161,12 +164,12 @@ def test_pfm_invalid(data): ), ), ) -def test_plain(plain_path, raw_path): +def test_plain(plain_path: str, raw_path: str) -> None: with Image.open(plain_path) as im: assert_image_equal_tofile(im, raw_path) -def test_16bit_plain_pgm(): +def test_16bit_plain_pgm() -> None: # P2 with maxval 2 ** 16 - 1 with Image.open("Tests/images/hopper_16bit_plain.pgm") as im: assert im.mode == "I" @@ -185,7 +188,9 @@ def test_16bit_plain_pgm(): (b"P3\n2 2\n255", b"0 0 0 001 1 1 2 2 2 255 255 255", 10**6), ), ) -def test_plain_data_with_comment(tmp_path, header, data, comment_count): +def test_plain_data_with_comment( + tmp_path: Path, header: bytes, data: bytes, comment_count: int +) -> None: path1 = str(tmp_path / "temp1.ppm") path2 = str(tmp_path / "temp2.ppm") comment = b"# comment" * comment_count @@ -198,7 +203,7 @@ def test_plain_data_with_comment(tmp_path, header, data, comment_count): @pytest.mark.parametrize("data", (b"P1\n128 128\n", b"P3\n128 128\n255\n")) -def test_plain_truncated_data(tmp_path, data): +def test_plain_truncated_data(tmp_path: Path, data: bytes) -> None: path = str(tmp_path / "temp.ppm") with open(path, "wb") as f: f.write(data) @@ -209,7 +214,7 @@ def test_plain_truncated_data(tmp_path, data): @pytest.mark.parametrize("data", (b"P1\n128 128\n1009", b"P3\n128 128\n255\n100A")) -def test_plain_invalid_data(tmp_path, data): +def test_plain_invalid_data(tmp_path: Path, data: bytes) -> None: path = str(tmp_path / "temp.ppm") with open(path, "wb") as f: f.write(data) @@ -226,7 +231,7 @@ def test_plain_invalid_data(tmp_path, data): b"P3\n128 128\n255\n012345678910 0", # token too long ), ) -def test_plain_ppm_token_too_long(tmp_path, data): +def test_plain_ppm_token_too_long(tmp_path: Path, data: bytes) -> None: path = str(tmp_path / "temp.ppm") with open(path, "wb") as f: f.write(data) @@ -236,7 +241,7 @@ def test_plain_ppm_token_too_long(tmp_path, data): im.load() -def test_plain_ppm_value_too_large(tmp_path): +def test_plain_ppm_value_too_large(tmp_path: Path) -> None: path = str(tmp_path / "temp.ppm") with open(path, "wb") as f: f.write(b"P3\n128 128\n255\n256") @@ -246,12 +251,12 @@ def test_plain_ppm_value_too_large(tmp_path): im.load() -def test_magic(): +def test_magic() -> None: with pytest.raises(SyntaxError): PpmImagePlugin.PpmImageFile(fp=BytesIO(b"PyInvalid")) -def test_header_with_comments(tmp_path): +def test_header_with_comments(tmp_path: Path) -> None: path = str(tmp_path / "temp.ppm") with open(path, "wb") as f: f.write(b"P6 #comment\n#comment\r12#comment\r8\n128 #comment\n255\n") @@ -260,7 +265,7 @@ def test_header_with_comments(tmp_path): assert im.size == (128, 128) -def test_non_integer_token(tmp_path): +def test_non_integer_token(tmp_path: Path) -> None: path = str(tmp_path / "temp.ppm") with open(path, "wb") as f: f.write(b"P6\nTEST") @@ -270,7 +275,7 @@ def test_non_integer_token(tmp_path): pass -def test_header_token_too_long(tmp_path): +def test_header_token_too_long(tmp_path: Path) -> None: path = str(tmp_path / "temp.ppm") with open(path, "wb") as f: f.write(b"P6\n 01234567890") @@ -282,7 +287,7 @@ def test_header_token_too_long(tmp_path): assert str(e.value) == "Token too long in file header: 01234567890" -def test_truncated_file(tmp_path): +def test_truncated_file(tmp_path: Path) -> None: # Test EOF in header path = str(tmp_path / "temp.pgm") with open(path, "wb") as f: @@ -301,7 +306,7 @@ def test_truncated_file(tmp_path): im.load() -def test_not_enough_image_data(tmp_path): +def test_not_enough_image_data(tmp_path: Path) -> None: path = str(tmp_path / "temp.ppm") with open(path, "wb") as f: f.write(b"P2 1 2 255 255") @@ -312,7 +317,7 @@ def test_not_enough_image_data(tmp_path): @pytest.mark.parametrize("maxval", (b"0", b"65536")) -def test_invalid_maxval(maxval, tmp_path): +def test_invalid_maxval(maxval: bytes, tmp_path: Path) -> None: path = str(tmp_path / "temp.ppm") with open(path, "wb") as f: f.write(b"P6\n3 1 " + maxval) @@ -324,7 +329,7 @@ def test_invalid_maxval(maxval, tmp_path): assert str(e.value) == "maxval must be greater than 0 and less than 65536" -def test_neg_ppm(): +def test_neg_ppm() -> None: # Storage.c accepted negative values for xsize, ysize. the # internal open_ppm function didn't check for sanity but it # has been removed. The default opener doesn't accept negative @@ -335,7 +340,7 @@ def test_neg_ppm(): pass -def test_mimetypes(tmp_path): +def test_mimetypes(tmp_path: Path) -> None: path = str(tmp_path / "temp.pgm") with open(path, "wb") as f: @@ -350,7 +355,7 @@ def test_mimetypes(tmp_path): @pytest.mark.parametrize("buffer", (True, False)) -def test_save_stdout(buffer): +def test_save_stdout(buffer: bool) -> None: old_stdout = sys.stdout if buffer: diff --git a/Tests/test_file_psd.py b/Tests/test_file_psd.py index 16f049602..e60638b22 100644 --- a/Tests/test_file_psd.py +++ b/Tests/test_file_psd.py @@ -11,7 +11,7 @@ from .helper import assert_image_equal_tofile, assert_image_similar, hopper, is_ test_file = "Tests/images/hopper.psd" -def test_sanity(): +def test_sanity() -> None: with Image.open(test_file) as im: im.load() assert im.mode == "RGB" @@ -24,8 +24,8 @@ def test_sanity(): @pytest.mark.skipif(is_pypy(), reason="Requires CPython") -def test_unclosed_file(): - def open(): +def test_unclosed_file() -> None: + def open() -> None: im = Image.open(test_file) im.load() @@ -33,27 +33,27 @@ def test_unclosed_file(): open() -def test_closed_file(): +def test_closed_file() -> None: with warnings.catch_warnings(): im = Image.open(test_file) im.load() im.close() -def test_context_manager(): +def test_context_manager() -> None: with warnings.catch_warnings(): with Image.open(test_file) as im: im.load() -def test_invalid_file(): +def test_invalid_file() -> None: invalid_file = "Tests/images/flower.jpg" with pytest.raises(SyntaxError): PsdImagePlugin.PsdImageFile(invalid_file) -def test_n_frames(): +def test_n_frames() -> None: with Image.open("Tests/images/hopper_merged.psd") as im: assert im.n_frames == 1 assert not im.is_animated @@ -64,7 +64,7 @@ def test_n_frames(): assert im.is_animated -def test_eoferror(): +def test_eoferror() -> None: with Image.open(test_file) as im: # PSD seek index starts at 1 rather than 0 n_frames = im.n_frames + 1 @@ -78,7 +78,7 @@ def test_eoferror(): im.seek(n_frames - 1) -def test_seek_tell(): +def test_seek_tell() -> None: with Image.open(test_file) as im: layer_number = im.tell() assert layer_number == 1 @@ -95,30 +95,30 @@ def test_seek_tell(): assert layer_number == 2 -def test_seek_eoferror(): +def test_seek_eoferror() -> None: with Image.open(test_file) as im: with pytest.raises(EOFError): im.seek(-1) -def test_open_after_exclusive_load(): +def test_open_after_exclusive_load() -> None: with Image.open(test_file) as im: im.load() im.seek(im.tell() + 1) im.load() -def test_rgba(): +def test_rgba() -> None: with Image.open("Tests/images/rgba.psd") as im: assert_image_equal_tofile(im, "Tests/images/imagedraw_square.png") -def test_layer_skip(): +def test_layer_skip() -> None: with Image.open("Tests/images/five_channels.psd") as im: assert im.n_frames == 1 -def test_icc_profile(): +def test_icc_profile() -> None: with Image.open(test_file) as im: assert "icc_profile" in im.info @@ -126,12 +126,12 @@ def test_icc_profile(): assert len(icc_profile) == 3144 -def test_no_icc_profile(): +def test_no_icc_profile() -> None: with Image.open("Tests/images/hopper_merged.psd") as im: assert "icc_profile" not in im.info -def test_combined_larger_than_size(): +def test_combined_larger_than_size() -> None: # The combined size of the individual parts is larger than the # declared 'size' of the extra data field, resulting in a backwards seek. @@ -157,7 +157,7 @@ def test_combined_larger_than_size(): ("Tests/images/timeout-dedc7a4ebd856d79b4359bbcc79e8ef231ce38f6.psd", OSError), ], ) -def test_crashes(test_file, raises): +def test_crashes(test_file: str, raises) -> None: with open(test_file, "rb") as f: with pytest.raises(raises): with Image.open(f): diff --git a/Tests/test_file_qoi.py b/Tests/test_file_qoi.py index 6dc468754..fd4b981ce 100644 --- a/Tests/test_file_qoi.py +++ b/Tests/test_file_qoi.py @@ -7,7 +7,7 @@ from PIL import Image, QoiImagePlugin from .helper import assert_image_equal_tofile -def test_sanity(): +def test_sanity() -> None: with Image.open("Tests/images/hopper.qoi") as im: assert im.mode == "RGB" assert im.size == (128, 128) @@ -23,7 +23,7 @@ def test_sanity(): assert_image_equal_tofile(im, "Tests/images/pil123rgba.png") -def test_invalid_file(): +def test_invalid_file() -> None: invalid_file = "Tests/images/flower.jpg" with pytest.raises(SyntaxError): diff --git a/Tests/test_file_sgi.py b/Tests/test_file_sgi.py index bc45bbfd3..e13a8019e 100644 --- a/Tests/test_file_sgi.py +++ b/Tests/test_file_sgi.py @@ -1,5 +1,7 @@ from __future__ import annotations +from pathlib import Path + import pytest from PIL import Image, SgiImagePlugin @@ -12,7 +14,7 @@ from .helper import ( ) -def test_rgb(): +def test_rgb() -> None: # Created with ImageMagick then renamed: # convert hopper.ppm -compress None sgi:hopper.rgb test_file = "Tests/images/hopper.rgb" @@ -22,11 +24,11 @@ def test_rgb(): assert im.get_format_mimetype() == "image/rgb" -def test_rgb16(): +def test_rgb16() -> None: assert_image_equal_tofile(hopper(), "Tests/images/hopper16.rgb") -def test_l(): +def test_l() -> None: # Created with ImageMagick # convert hopper.ppm -monochrome -compress None sgi:hopper.bw test_file = "Tests/images/hopper.bw" @@ -36,7 +38,7 @@ def test_l(): assert im.get_format_mimetype() == "image/sgi" -def test_rgba(): +def test_rgba() -> None: # Created with ImageMagick: # convert transparent.png -compress None transparent.sgi test_file = "Tests/images/transparent.sgi" @@ -46,7 +48,7 @@ def test_rgba(): assert im.get_format_mimetype() == "image/sgi" -def test_rle(): +def test_rle() -> None: # Created with ImageMagick: # convert hopper.ppm hopper.sgi test_file = "Tests/images/hopper.sgi" @@ -55,22 +57,22 @@ def test_rle(): assert_image_equal_tofile(im, "Tests/images/hopper.rgb") -def test_rle16(): +def test_rle16() -> None: test_file = "Tests/images/tv16.sgi" with Image.open(test_file) as im: assert_image_equal_tofile(im, "Tests/images/tv.rgb") -def test_invalid_file(): +def test_invalid_file() -> None: invalid_file = "Tests/images/flower.jpg" with pytest.raises(ValueError): SgiImagePlugin.SgiImageFile(invalid_file) -def test_write(tmp_path): - def roundtrip(img): +def test_write(tmp_path: Path) -> None: + def roundtrip(img: Image.Image) -> None: out = str(tmp_path / "temp.sgi") img.save(out, format="sgi") assert_image_equal_tofile(img, out) @@ -89,7 +91,7 @@ def test_write(tmp_path): roundtrip(Image.new("L", (10, 1))) -def test_write16(tmp_path): +def test_write16(tmp_path: Path) -> None: test_file = "Tests/images/hopper16.rgb" with Image.open(test_file) as im: @@ -99,7 +101,7 @@ def test_write16(tmp_path): assert_image_equal_tofile(im, out) -def test_unsupported_mode(tmp_path): +def test_unsupported_mode(tmp_path: Path) -> None: im = hopper("LA") out = str(tmp_path / "temp.sgi") diff --git a/Tests/test_file_spider.py b/Tests/test_file_spider.py index 42d833fb2..9b82a962a 100644 --- a/Tests/test_file_spider.py +++ b/Tests/test_file_spider.py @@ -3,17 +3,18 @@ from __future__ import annotations import tempfile import warnings from io import BytesIO +from pathlib import Path import pytest from PIL import Image, ImageSequence, SpiderImagePlugin -from .helper import assert_image_equal_tofile, hopper, is_pypy +from .helper import assert_image_equal, hopper, is_pypy TEST_FILE = "Tests/images/hopper.spider" -def test_sanity(): +def test_sanity() -> None: with Image.open(TEST_FILE) as im: im.load() assert im.mode == "F" @@ -22,8 +23,8 @@ def test_sanity(): @pytest.mark.skipif(is_pypy(), reason="Requires CPython") -def test_unclosed_file(): - def open(): +def test_unclosed_file() -> None: + def open() -> None: im = Image.open(TEST_FILE) im.load() @@ -31,20 +32,20 @@ def test_unclosed_file(): open() -def test_closed_file(): +def test_closed_file() -> None: with warnings.catch_warnings(): im = Image.open(TEST_FILE) im.load() im.close() -def test_context_manager(): +def test_context_manager() -> None: with warnings.catch_warnings(): with Image.open(TEST_FILE) as im: im.load() -def test_save(tmp_path): +def test_save(tmp_path: Path) -> None: # Arrange temp = str(tmp_path / "temp.spider") im = hopper() @@ -59,7 +60,7 @@ def test_save(tmp_path): assert im2.format == "SPIDER" -def test_tempfile(): +def test_tempfile() -> None: # Arrange im = hopper() @@ -75,11 +76,11 @@ def test_tempfile(): assert reloaded.format == "SPIDER" -def test_is_spider_image(): +def test_is_spider_image() -> None: assert SpiderImagePlugin.isSpiderImage(TEST_FILE) -def test_tell(): +def test_tell() -> None: # Arrange with Image.open(TEST_FILE) as im: # Act @@ -89,13 +90,13 @@ def test_tell(): assert index == 0 -def test_n_frames(): +def test_n_frames() -> None: with Image.open(TEST_FILE) as im: assert im.n_frames == 1 assert not im.is_animated -def test_load_image_series(): +def test_load_image_series() -> None: # Arrange not_spider_file = "Tests/images/hopper.ppm" file_list = [TEST_FILE, not_spider_file, "path/not_found.ext"] @@ -109,7 +110,7 @@ def test_load_image_series(): assert img_list[0].size == (128, 128) -def test_load_image_series_no_input(): +def test_load_image_series_no_input() -> None: # Arrange file_list = None @@ -120,7 +121,7 @@ def test_load_image_series_no_input(): assert img_list is None -def test_is_int_not_a_number(): +def test_is_int_not_a_number() -> None: # Arrange not_a_number = "a" @@ -131,7 +132,7 @@ def test_is_int_not_a_number(): assert ret == 0 -def test_invalid_file(): +def test_invalid_file() -> None: invalid_file = "Tests/images/invalid.spider" with pytest.raises(OSError): @@ -139,24 +140,25 @@ def test_invalid_file(): pass -def test_nonstack_file(): +def test_nonstack_file() -> None: with Image.open(TEST_FILE) as im: with pytest.raises(EOFError): im.seek(0) -def test_nonstack_dos(): +def test_nonstack_dos() -> None: with Image.open(TEST_FILE) as im: for i, frame in enumerate(ImageSequence.Iterator(im)): assert i <= 1, "Non-stack DOS file test failed" # for issue #4093 -def test_odd_size(): +def test_odd_size() -> None: data = BytesIO() width = 100 im = Image.new("F", (width, 64)) im.save(data, format="SPIDER") data.seek(0) - assert_image_equal_tofile(im, data) + with Image.open(data) as im2: + assert_image_equal(im, im2) diff --git a/Tests/test_file_sun.py b/Tests/test_file_sun.py index 41f3b7d98..6cfff8730 100644 --- a/Tests/test_file_sun.py +++ b/Tests/test_file_sun.py @@ -11,7 +11,7 @@ from .helper import assert_image_equal_tofile, assert_image_similar, hopper EXTRA_DIR = "Tests/images/sunraster" -def test_sanity(): +def test_sanity() -> None: # Arrange # Created with ImageMagick: convert hopper.jpg hopper.ras test_file = "Tests/images/hopper.ras" @@ -28,7 +28,7 @@ def test_sanity(): SunImagePlugin.SunImageFile(invalid_file) -def test_im1(): +def test_im1() -> None: with Image.open("Tests/images/sunraster.im1") as im: assert_image_equal_tofile(im, "Tests/images/sunraster.im1.png") @@ -36,7 +36,7 @@ def test_im1(): @pytest.mark.skipif( not os.path.exists(EXTRA_DIR), reason="Extra image files not installed" ) -def test_others(): +def test_others() -> None: files = ( os.path.join(EXTRA_DIR, f) for f in os.listdir(EXTRA_DIR) diff --git a/Tests/test_file_tar.py b/Tests/test_file_tar.py index 58226c330..6217ebedd 100644 --- a/Tests/test_file_tar.py +++ b/Tests/test_file_tar.py @@ -19,7 +19,7 @@ TEST_TAR_FILE = "Tests/images/hopper.tar" ("jpg", "hopper.jpg", "JPEG"), ), ) -def test_sanity(codec, test_path, format): +def test_sanity(codec: str, test_path: str, format: str) -> None: if features.check(codec): with TarIO.TarIO(TEST_TAR_FILE, test_path) as tar: with Image.open(tar) as im: @@ -30,18 +30,18 @@ def test_sanity(codec, test_path, format): @pytest.mark.skipif(is_pypy(), reason="Requires CPython") -def test_unclosed_file(): +def test_unclosed_file() -> None: with pytest.warns(ResourceWarning): TarIO.TarIO(TEST_TAR_FILE, "hopper.jpg") -def test_close(): +def test_close() -> None: with warnings.catch_warnings(): tar = TarIO.TarIO(TEST_TAR_FILE, "hopper.jpg") tar.close() -def test_contextmanager(): +def test_contextmanager() -> None: with warnings.catch_warnings(): with TarIO.TarIO(TEST_TAR_FILE, "hopper.jpg"): pass diff --git a/Tests/test_file_tga.py b/Tests/test_file_tga.py index eafb61d30..3c6da50c5 100644 --- a/Tests/test_file_tga.py +++ b/Tests/test_file_tga.py @@ -3,6 +3,7 @@ from __future__ import annotations import os from glob import glob from itertools import product +from pathlib import Path import pytest @@ -21,8 +22,8 @@ _ORIGIN_TO_ORIENTATION = {"tl": 1, "bl": -1} @pytest.mark.parametrize("mode", _MODES) -def test_sanity(mode, tmp_path): - def roundtrip(original_im): +def test_sanity(mode: str, tmp_path: Path) -> None: + def roundtrip(original_im: Image.Image) -> None: out = str(tmp_path / "temp.tga") original_im.save(out, rle=rle) @@ -64,7 +65,7 @@ def test_sanity(mode, tmp_path): roundtrip(original_im) -def test_palette_depth_16(tmp_path): +def test_palette_depth_16(tmp_path: Path) -> None: with Image.open("Tests/images/p_16.tga") as im: assert_image_equal_tofile(im.convert("RGB"), "Tests/images/p_16.png") @@ -74,7 +75,7 @@ def test_palette_depth_16(tmp_path): assert_image_equal_tofile(reloaded.convert("RGB"), "Tests/images/p_16.png") -def test_id_field(): +def test_id_field() -> None: # tga file with id field test_file = "Tests/images/tga_id_field.tga" @@ -84,7 +85,7 @@ def test_id_field(): assert im.size == (100, 100) -def test_id_field_rle(): +def test_id_field_rle() -> None: # tga file with id field test_file = "Tests/images/rgb32rle.tga" @@ -94,7 +95,7 @@ def test_id_field_rle(): assert im.size == (199, 199) -def test_cross_scan_line(): +def test_cross_scan_line() -> None: with Image.open("Tests/images/cross_scan_line.tga") as im: assert_image_equal_tofile(im, "Tests/images/cross_scan_line.png") @@ -103,7 +104,7 @@ def test_cross_scan_line(): im.load() -def test_save(tmp_path): +def test_save(tmp_path: Path) -> None: test_file = "Tests/images/tga_id_field.tga" with Image.open(test_file) as im: out = str(tmp_path / "temp.tga") @@ -120,7 +121,7 @@ def test_save(tmp_path): assert test_im.size == (100, 100) -def test_small_palette(tmp_path): +def test_small_palette(tmp_path: Path) -> None: im = Image.new("P", (1, 1)) colors = [0, 0, 0] im.putpalette(colors) @@ -132,7 +133,7 @@ def test_small_palette(tmp_path): assert reloaded.getpalette() == colors -def test_save_wrong_mode(tmp_path): +def test_save_wrong_mode(tmp_path: Path) -> None: im = hopper("PA") out = str(tmp_path / "temp.tga") @@ -140,7 +141,7 @@ def test_save_wrong_mode(tmp_path): im.save(out) -def test_save_mapdepth(): +def test_save_mapdepth() -> None: # This image has been manually hexedited from 200x32_p_bl_raw.tga # to include an origin test_file = "Tests/images/200x32_p_bl_raw_origin.tga" @@ -148,7 +149,7 @@ def test_save_mapdepth(): assert_image_equal_tofile(im, "Tests/images/tga/common/200x32_p.png") -def test_save_id_section(tmp_path): +def test_save_id_section(tmp_path: Path) -> None: test_file = "Tests/images/rgb32rle.tga" with Image.open(test_file) as im: out = str(tmp_path / "temp.tga") @@ -179,7 +180,7 @@ def test_save_id_section(tmp_path): assert "id_section" not in test_im.info -def test_save_orientation(tmp_path): +def test_save_orientation(tmp_path: Path) -> None: test_file = "Tests/images/rgb32rle.tga" out = str(tmp_path / "temp.tga") with Image.open(test_file) as im: @@ -190,7 +191,7 @@ def test_save_orientation(tmp_path): assert test_im.info["orientation"] == 1 -def test_horizontal_orientations(): +def test_horizontal_orientations() -> None: # These images have been manually hexedited to have the relevant orientations with Image.open("Tests/images/rgb32rle_top_right.tga") as im: assert im.load()[90, 90][:3] == (0, 0, 0) @@ -199,7 +200,7 @@ def test_horizontal_orientations(): assert im.load()[90, 90][:3] == (0, 255, 0) -def test_save_rle(tmp_path): +def test_save_rle(tmp_path: Path) -> None: test_file = "Tests/images/rgb32rle.tga" with Image.open(test_file) as im: assert im.info["compression"] == "tga_rle" @@ -232,7 +233,7 @@ def test_save_rle(tmp_path): assert test_im.info["compression"] == "tga_rle" -def test_save_l_transparency(tmp_path): +def test_save_l_transparency(tmp_path: Path) -> None: # There are 559 transparent pixels in la.tga. num_transparent = 559 diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index f0995679b..21d52462e 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -3,6 +3,9 @@ from __future__ import annotations import os import warnings from io import BytesIO +from pathlib import Path +from types import ModuleType +from typing import Generator import pytest @@ -19,6 +22,7 @@ from .helper import ( is_win32, ) +ElementTree: ModuleType | None try: from defusedxml import ElementTree except ImportError: @@ -26,7 +30,7 @@ except ImportError: class TestFileTiff: - def test_sanity(self, tmp_path): + def test_sanity(self, tmp_path: Path) -> None: filename = str(tmp_path / "temp.tif") hopper("RGB").save(filename) @@ -58,21 +62,21 @@ class TestFileTiff: pass @pytest.mark.skipif(is_pypy(), reason="Requires CPython") - def test_unclosed_file(self): - def open(): + def test_unclosed_file(self) -> None: + def open() -> None: im = Image.open("Tests/images/multipage.tiff") im.load() with pytest.warns(ResourceWarning): open() - def test_closed_file(self): + def test_closed_file(self) -> None: with warnings.catch_warnings(): im = Image.open("Tests/images/multipage.tiff") im.load() im.close() - def test_seek_after_close(self): + def test_seek_after_close(self) -> None: im = Image.open("Tests/images/multipage.tiff") im.close() @@ -81,12 +85,12 @@ class TestFileTiff: with pytest.raises(ValueError): im.seek(1) - def test_context_manager(self): + def test_context_manager(self) -> None: with warnings.catch_warnings(): with Image.open("Tests/images/multipage.tiff") as im: im.load() - def test_mac_tiff(self): + def test_mac_tiff(self) -> None: # Read RGBa images from macOS [@PIL136] filename = "Tests/images/pil136.tiff" @@ -98,7 +102,7 @@ class TestFileTiff: assert_image_similar_tofile(im, "Tests/images/pil136.png", 1) - def test_bigtiff(self, tmp_path): + def test_bigtiff(self, tmp_path: Path) -> None: with Image.open("Tests/images/hopper_bigtiff.tif") as im: assert_image_equal_tofile(im, "Tests/images/hopper.tif") @@ -109,13 +113,13 @@ class TestFileTiff: outfile = str(tmp_path / "temp.tif") im.save(outfile, save_all=True, append_images=[im], tiffinfo=im.tag_v2) - def test_set_legacy_api(self): + def test_set_legacy_api(self) -> None: ifd = TiffImagePlugin.ImageFileDirectory_v2() with pytest.raises(Exception) as e: ifd.legacy_api = None assert str(e.value) == "Not allowing setting of legacy api" - def test_xyres_tiff(self): + def test_xyres_tiff(self) -> None: filename = "Tests/images/pil168.tif" with Image.open(filename) as im: # legacy api @@ -128,7 +132,7 @@ class TestFileTiff: assert im.info["dpi"] == (72.0, 72.0) - def test_xyres_fallback_tiff(self): + def test_xyres_fallback_tiff(self) -> None: filename = "Tests/images/compression.tif" with Image.open(filename) as im: # v2 api @@ -142,7 +146,7 @@ class TestFileTiff: # Fallback "inch". assert im.info["dpi"] == (100.0, 100.0) - def test_int_resolution(self): + def test_int_resolution(self) -> None: filename = "Tests/images/pil168.tif" with Image.open(filename) as im: # Try to read a file where X,Y_RESOLUTION are ints @@ -155,14 +159,14 @@ class TestFileTiff: "resolution_unit, dpi", [(None, 72.8), (2, 72.8), (3, 184.912)], ) - def test_load_float_dpi(self, resolution_unit, dpi): + def test_load_float_dpi(self, resolution_unit: int | None, dpi: float) -> None: with Image.open( "Tests/images/hopper_float_dpi_" + str(resolution_unit) + ".tif" ) as im: assert im.tag_v2.get(RESOLUTION_UNIT) == resolution_unit assert im.info["dpi"] == (dpi, dpi) - def test_save_float_dpi(self, tmp_path): + def test_save_float_dpi(self, tmp_path: Path) -> None: outfile = str(tmp_path / "temp.tif") with Image.open("Tests/images/hopper.tif") as im: dpi = (72.2, 72.2) @@ -171,7 +175,7 @@ class TestFileTiff: with Image.open(outfile) as reloaded: assert reloaded.info["dpi"] == dpi - def test_save_setting_missing_resolution(self): + def test_save_setting_missing_resolution(self) -> None: b = BytesIO() with Image.open("Tests/images/10ct_32bit_128.tiff") as im: im.save(b, format="tiff", resolution=123.45) @@ -179,7 +183,7 @@ class TestFileTiff: assert im.tag_v2[X_RESOLUTION] == 123.45 assert im.tag_v2[Y_RESOLUTION] == 123.45 - def test_invalid_file(self): + def test_invalid_file(self) -> None: invalid_file = "Tests/images/flower.jpg" with pytest.raises(SyntaxError): @@ -190,30 +194,30 @@ class TestFileTiff: TiffImagePlugin.TiffImageFile(invalid_file) TiffImagePlugin.PREFIXES.pop() - def test_bad_exif(self): + def test_bad_exif(self) -> None: with Image.open("Tests/images/hopper_bad_exif.jpg") as i: # Should not raise struct.error. with pytest.warns(UserWarning): i._getexif() - def test_save_rgba(self, tmp_path): + def test_save_rgba(self, tmp_path: Path) -> None: im = hopper("RGBA") outfile = str(tmp_path / "temp.tif") im.save(outfile) - def test_save_unsupported_mode(self, tmp_path): + def test_save_unsupported_mode(self, tmp_path: Path) -> None: im = hopper("HSV") outfile = str(tmp_path / "temp.tif") with pytest.raises(OSError): im.save(outfile) - def test_8bit_s(self): + def test_8bit_s(self) -> None: with Image.open("Tests/images/8bit.s.tif") as im: im.load() assert im.mode == "L" assert im.getpixel((50, 50)) == 184 - def test_little_endian(self): + def test_little_endian(self) -> None: with Image.open("Tests/images/16bit.cropped.tif") as im: assert im.getpixel((0, 0)) == 480 assert im.mode == "I;16" @@ -223,7 +227,7 @@ class TestFileTiff: assert b[0] == ord(b"\xe0") assert b[1] == ord(b"\x01") - def test_big_endian(self): + def test_big_endian(self) -> None: with Image.open("Tests/images/16bit.MM.cropped.tif") as im: assert im.getpixel((0, 0)) == 480 assert im.mode == "I;16B" @@ -233,7 +237,7 @@ class TestFileTiff: assert b[0] == ord(b"\x01") assert b[1] == ord(b"\xe0") - def test_16bit_r(self): + def test_16bit_r(self) -> None: with Image.open("Tests/images/16bit.r.tif") as im: assert im.getpixel((0, 0)) == 480 assert im.mode == "I;16" @@ -242,14 +246,14 @@ class TestFileTiff: assert b[0] == ord(b"\xe0") assert b[1] == ord(b"\x01") - def test_16bit_s(self): + def test_16bit_s(self) -> None: with Image.open("Tests/images/16bit.s.tif") as im: im.load() assert im.mode == "I" assert im.getpixel((0, 0)) == 32767 assert im.getpixel((0, 1)) == 0 - def test_12bit_rawmode(self): + def test_12bit_rawmode(self) -> None: """Are we generating the same interpretation of the image as Imagemagick is?""" @@ -262,7 +266,7 @@ class TestFileTiff: assert_image_equal_tofile(im, "Tests/images/12in16bit.tif") - def test_32bit_float(self): + def test_32bit_float(self) -> None: # Issue 614, specific 32-bit float format path = "Tests/images/10ct_32bit_128.tiff" with Image.open(path) as im: @@ -271,7 +275,7 @@ class TestFileTiff: assert im.getpixel((0, 0)) == -0.4526388943195343 assert im.getextrema() == (-3.140936851501465, 3.140684127807617) - def test_unknown_pixel_mode(self): + def test_unknown_pixel_mode(self) -> None: with pytest.raises(OSError): with Image.open("Tests/images/hopper_unknown_pixel_mode.tif"): pass @@ -283,12 +287,12 @@ class TestFileTiff: ("Tests/images/multipage.tiff", 3), ), ) - def test_n_frames(self, path, n_frames): + def test_n_frames(self, path: str, n_frames: int) -> None: with Image.open(path) as im: assert im.n_frames == n_frames assert im.is_animated == (n_frames != 1) - def test_eoferror(self): + def test_eoferror(self) -> None: with Image.open("Tests/images/multipage-lastframe.tif") as im: n_frames = im.n_frames @@ -300,7 +304,7 @@ class TestFileTiff: # Test that seeking to the last frame does not raise an error im.seek(n_frames - 1) - def test_multipage(self): + def test_multipage(self) -> None: # issue #862 with Image.open("Tests/images/multipage.tiff") as im: # file is a multipage tiff: 10x10 green, 10x10 red, 20x20 blue @@ -324,13 +328,13 @@ class TestFileTiff: assert im.size == (20, 20) assert im.convert("RGB").getpixel((0, 0)) == (0, 0, 255) - def test_multipage_last_frame(self): + def test_multipage_last_frame(self) -> None: with Image.open("Tests/images/multipage-lastframe.tif") as im: im.load() assert im.size == (20, 20) assert im.convert("RGB").getpixel((0, 0)) == (0, 0, 255) - def test_frame_order(self): + def test_frame_order(self) -> None: # A frame can't progress to itself after reading with Image.open("Tests/images/multipage_single_frame_loop.tiff") as im: assert im.n_frames == 1 @@ -343,7 +347,7 @@ class TestFileTiff: with Image.open("Tests/images/multipage_out_of_order.tiff") as im: assert im.n_frames == 3 - def test___str__(self): + def test___str__(self) -> None: filename = "Tests/images/pil136.tiff" with Image.open(filename) as im: # Act @@ -352,7 +356,7 @@ class TestFileTiff: # Assert assert isinstance(ret, str) - def test_dict(self): + def test_dict(self) -> None: # Arrange filename = "Tests/images/pil136.tiff" with Image.open(filename) as im: @@ -392,7 +396,7 @@ class TestFileTiff: } assert dict(im.tag) == legacy_tags - def test__delitem__(self): + def test__delitem__(self) -> None: filename = "Tests/images/pil136.tiff" with Image.open(filename) as im: len_before = len(dict(im.ifd)) @@ -401,36 +405,36 @@ class TestFileTiff: assert len_before == len_after + 1 @pytest.mark.parametrize("legacy_api", (False, True)) - def test_load_byte(self, legacy_api): + def test_load_byte(self, legacy_api: bool) -> None: ifd = TiffImagePlugin.ImageFileDirectory_v2() data = b"abc" ret = ifd.load_byte(data, legacy_api) assert ret == b"abc" - def test_load_string(self): + def test_load_string(self) -> None: ifd = TiffImagePlugin.ImageFileDirectory_v2() data = b"abc\0" ret = ifd.load_string(data, False) assert ret == "abc" - def test_load_float(self): + def test_load_float(self) -> None: ifd = TiffImagePlugin.ImageFileDirectory_v2() data = b"abcdabcd" ret = ifd.load_float(data, False) assert ret == (1.6777999408082104e22, 1.6777999408082104e22) - def test_load_double(self): + def test_load_double(self) -> None: ifd = TiffImagePlugin.ImageFileDirectory_v2() data = b"abcdefghabcdefgh" ret = ifd.load_double(data, False) assert ret == (8.540883223036124e194, 8.540883223036124e194) - def test_ifd_tag_type(self): + def test_ifd_tag_type(self) -> None: with Image.open("Tests/images/ifd_tag_type.tiff") as im: assert 0x8825 in im.tag_v2 - def test_exif(self, tmp_path): - def check_exif(exif): + def test_exif(self, tmp_path: Path) -> None: + def check_exif(exif: Image.Exif) -> None: assert sorted(exif.keys()) == [ 256, 257, @@ -481,7 +485,7 @@ class TestFileTiff: exif = im.getexif() check_exif(exif) - def test_modify_exif(self, tmp_path): + def test_modify_exif(self, tmp_path: Path) -> None: outfile = str(tmp_path / "temp.tif") with Image.open("Tests/images/ifd_tag_type.tiff") as im: exif = im.getexif() @@ -493,7 +497,7 @@ class TestFileTiff: exif = im.getexif() assert exif[264] == 100 - def test_reload_exif_after_seek(self): + def test_reload_exif_after_seek(self) -> None: with Image.open("Tests/images/multipage.tiff") as im: exif = im.getexif() del exif[256] @@ -501,7 +505,7 @@ class TestFileTiff: assert 256 in exif - def test_exif_frames(self): + def test_exif_frames(self) -> None: # Test that EXIF data can change across frames with Image.open("Tests/images/g4-multi.tiff") as im: assert im.getexif()[273] == (328, 815) @@ -510,7 +514,7 @@ class TestFileTiff: assert im.getexif()[273] == (1408, 1907) @pytest.mark.parametrize("mode", ("1", "L")) - def test_photometric(self, mode, tmp_path): + def test_photometric(self, mode: str, tmp_path: Path) -> None: filename = str(tmp_path / "temp.tif") im = hopper(mode) im.save(filename, tiffinfo={262: 0}) @@ -518,13 +522,13 @@ class TestFileTiff: assert reloaded.tag_v2[262] == 0 assert_image_equal(im, reloaded) - def test_seek(self): + def test_seek(self) -> None: filename = "Tests/images/pil136.tiff" with Image.open(filename) as im: im.seek(0) assert im.tell() == 0 - def test_seek_eof(self): + def test_seek_eof(self) -> None: filename = "Tests/images/pil136.tiff" with Image.open(filename) as im: assert im.tell() == 0 @@ -533,21 +537,21 @@ class TestFileTiff: with pytest.raises(EOFError): im.seek(1) - def test__limit_rational_int(self): + def test__limit_rational_int(self) -> None: from PIL.TiffImagePlugin import _limit_rational value = 34 ret = _limit_rational(value, 65536) assert ret == (34, 1) - def test__limit_rational_float(self): + def test__limit_rational_float(self) -> None: from PIL.TiffImagePlugin import _limit_rational value = 22.3 ret = _limit_rational(value, 65536) assert ret == (223, 10) - def test_4bit(self): + def test_4bit(self) -> None: test_file = "Tests/images/hopper_gray_4bpp.tif" original = hopper("L") with Image.open(test_file) as im: @@ -555,7 +559,7 @@ class TestFileTiff: assert im.mode == "L" assert_image_similar(im, original, 7.3) - def test_gray_semibyte_per_pixel(self): + def test_gray_semibyte_per_pixel(self) -> None: test_files = ( ( 24.8, # epsilon @@ -588,7 +592,7 @@ class TestFileTiff: assert im2.mode == "L" assert_image_equal(im, im2) - def test_with_underscores(self, tmp_path): + def test_with_underscores(self, tmp_path: Path) -> None: kwargs = {"resolution_unit": "inch", "x_resolution": 72, "y_resolution": 36} filename = str(tmp_path / "temp.tif") hopper("RGB").save(filename, **kwargs) @@ -601,7 +605,7 @@ class TestFileTiff: assert im.tag_v2[X_RESOLUTION] == 72 assert im.tag_v2[Y_RESOLUTION] == 36 - def test_roundtrip_tiff_uint16(self, tmp_path): + def test_roundtrip_tiff_uint16(self, tmp_path: Path) -> None: # Test an image of all '0' values pixel_value = 0x1234 infile = "Tests/images/uint16_1_4660.tif" @@ -613,33 +617,34 @@ class TestFileTiff: assert_image_equal_tofile(im, tmpfile) - def test_rowsperstrip(self, tmp_path): + def test_rowsperstrip(self, tmp_path: Path) -> None: outfile = str(tmp_path / "temp.tif") im = hopper() im.save(outfile, tiffinfo={278: 256}) with Image.open(outfile) as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) assert im.tag_v2[278] == 256 - def test_strip_raw(self): + def test_strip_raw(self) -> None: infile = "Tests/images/tiff_strip_raw.tif" with Image.open(infile) as im: assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png") - def test_strip_planar_raw(self): + def test_strip_planar_raw(self) -> None: # gdal_translate -of GTiff -co INTERLEAVE=BAND \ # tiff_strip_raw.tif tiff_strip_planar_raw.tiff infile = "Tests/images/tiff_strip_planar_raw.tif" with Image.open(infile) as im: assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png") - def test_strip_planar_raw_with_overviews(self): + def test_strip_planar_raw_with_overviews(self) -> None: # gdaladdo tiff_strip_planar_raw2.tif 2 4 8 16 infile = "Tests/images/tiff_strip_planar_raw_with_overviews.tif" with Image.open(infile) as im: assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png") - def test_tiled_planar_raw(self): + def test_tiled_planar_raw(self) -> None: # gdal_translate -of GTiff -co TILED=YES -co BLOCKXSIZE=32 \ # -co BLOCKYSIZE=32 -co INTERLEAVE=BAND \ # tiff_tiled_raw.tif tiff_tiled_planar_raw.tiff @@ -647,7 +652,7 @@ class TestFileTiff: with Image.open(infile) as im: assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png") - def test_planar_configuration_save(self, tmp_path): + def test_planar_configuration_save(self, tmp_path: Path) -> None: infile = "Tests/images/tiff_tiled_planar_raw.tif" with Image.open(infile) as im: assert im._planar_configuration == 2 @@ -659,7 +664,7 @@ class TestFileTiff: assert_image_equal_tofile(reloaded, infile) @pytest.mark.parametrize("mode", ("P", "PA")) - def test_palette(self, mode, tmp_path): + def test_palette(self, mode: str, tmp_path: Path) -> None: outfile = str(tmp_path / "temp.tif") im = hopper(mode) @@ -668,7 +673,7 @@ class TestFileTiff: with Image.open(outfile) as reloaded: assert_image_equal(im.convert("RGB"), reloaded.convert("RGB")) - def test_tiff_save_all(self): + def test_tiff_save_all(self) -> None: mp = BytesIO() with Image.open("Tests/images/multipage.tiff") as im: im.save(mp, format="tiff", save_all=True) @@ -688,7 +693,7 @@ class TestFileTiff: assert reread.n_frames == 3 # Test appending using a generator - def im_generator(ims): + def im_generator(ims: list[Image.Image]) -> Generator[Image.Image, None, None]: yield from ims mp = BytesIO() @@ -698,7 +703,7 @@ class TestFileTiff: with Image.open(mp) as reread: assert reread.n_frames == 3 - def test_saving_icc_profile(self, tmp_path): + def test_saving_icc_profile(self, tmp_path: Path) -> None: # Tests saving TIFF with icc_profile set. # At the time of writing this will only work for non-compressed tiffs # as libtiff does not support embedded ICC profiles, @@ -712,7 +717,7 @@ class TestFileTiff: with Image.open(tmpfile) as reloaded: assert b"Dummy value" == reloaded.info["icc_profile"] - def test_save_icc_profile(self, tmp_path): + def test_save_icc_profile(self, tmp_path: Path) -> None: im = hopper() assert "icc_profile" not in im.info @@ -723,14 +728,14 @@ class TestFileTiff: with Image.open(outfile) as reloaded: assert reloaded.info["icc_profile"] == icc_profile - def test_save_bmp_compression(self, tmp_path): + def test_save_bmp_compression(self, tmp_path: Path) -> None: with Image.open("Tests/images/hopper.bmp") as im: assert im.info["compression"] == 0 outfile = str(tmp_path / "temp.tif") im.save(outfile) - def test_discard_icc_profile(self, tmp_path): + def test_discard_icc_profile(self, tmp_path: Path) -> None: outfile = str(tmp_path / "temp.tif") with Image.open("Tests/images/icc_profile.png") as im: @@ -741,7 +746,7 @@ class TestFileTiff: with Image.open(outfile) as reloaded: assert "icc_profile" not in reloaded.info - def test_getxmp(self): + def test_getxmp(self) -> None: with Image.open("Tests/images/lab.tif") as im: if ElementTree is None: with pytest.warns( @@ -756,7 +761,7 @@ class TestFileTiff: assert description[0]["format"] == "image/tiff" assert description[3]["BitsPerSample"]["Seq"]["li"] == ["8", "8", "8"] - def test_get_photoshop_blocks(self): + def test_get_photoshop_blocks(self) -> None: with Image.open("Tests/images/lab.tif") as im: assert list(im.get_photoshop_blocks().keys()) == [ 1061, @@ -782,7 +787,7 @@ class TestFileTiff: 4001, ] - def test_tiff_chunks(self, tmp_path): + def test_tiff_chunks(self, tmp_path: Path) -> None: tmpfile = str(tmp_path / "temp.tif") im = hopper() @@ -803,7 +808,7 @@ class TestFileTiff: assert_image_equal_tofile(im, tmpfile) - def test_close_on_load_exclusive(self, tmp_path): + def test_close_on_load_exclusive(self, tmp_path: Path) -> None: # similar to test_fd_leak, but runs on unixlike os tmpfile = str(tmp_path / "temp.tif") @@ -816,7 +821,7 @@ class TestFileTiff: im.load() assert fp.closed - def test_close_on_load_nonexclusive(self, tmp_path): + def test_close_on_load_nonexclusive(self, tmp_path: Path) -> None: tmpfile = str(tmp_path / "temp.tif") with Image.open("Tests/images/uint16_1_4660.tif") as im: @@ -838,7 +843,7 @@ class TestFileTiff: not os.path.exists("Tests/images/string_dimension.tiff"), reason="Extra image files not installed", ) - def test_string_dimension(self): + def test_string_dimension(self) -> None: # Assert that an error is raised if one of the dimensions is a string with Image.open("Tests/images/string_dimension.tiff") as im: with pytest.raises(OSError): @@ -846,7 +851,7 @@ class TestFileTiff: @pytest.mark.timeout(6) @pytest.mark.filterwarnings("ignore:Truncated File Read") - def test_timeout(self): + def test_timeout(self) -> None: with Image.open("Tests/images/timeout-6646305047838720") as im: ImageFile.LOAD_TRUNCATED_IMAGES = True im.load() @@ -859,7 +864,7 @@ class TestFileTiff: ], ) @pytest.mark.timeout(2) - def test_oom(self, test_file): + def test_oom(self, test_file: str) -> None: with pytest.raises(UnidentifiedImageError): with pytest.warns(UserWarning): with Image.open(test_file): @@ -868,7 +873,7 @@ class TestFileTiff: @pytest.mark.skipif(not is_win32(), reason="Windows only") class TestFileTiffW32: - def test_fd_leak(self, tmp_path): + def test_fd_leak(self, tmp_path: Path) -> None: tmpfile = str(tmp_path / "temp.tif") # this is an mmaped file. diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py index 06689bc90..d7a18c725 100644 --- a/Tests/test_file_tiff_metadata.py +++ b/Tests/test_file_tiff_metadata.py @@ -2,6 +2,7 @@ from __future__ import annotations import io import struct +from pathlib import Path import pytest @@ -13,7 +14,7 @@ from .helper import assert_deep_equal, hopper TAG_IDS = {info.name: info.value for info in TiffTags.TAGS_V2.values()} -def test_rt_metadata(tmp_path): +def test_rt_metadata(tmp_path: Path) -> None: """Test writing arbitrary metadata into the tiff image directory Use case is ImageJ private tags, one numeric, one arbitrary data. https://github.com/python-pillow/Pillow/issues/291 @@ -79,7 +80,7 @@ def test_rt_metadata(tmp_path): assert loaded.tag_v2[ImageJMetaDataByteCounts] == (8, len(bin_data) - 8) -def test_read_metadata(): +def test_read_metadata() -> None: with Image.open("Tests/images/hopper_g4.tif") as img: assert { "YResolution": IFDRational(4294967295, 113653537), @@ -120,7 +121,7 @@ def test_read_metadata(): } == img.tag.named() -def test_write_metadata(tmp_path): +def test_write_metadata(tmp_path: Path) -> None: """Test metadata writing through the python code""" with Image.open("Tests/images/hopper.tif") as img: f = str(tmp_path / "temp.tiff") @@ -157,7 +158,7 @@ def test_write_metadata(tmp_path): assert value == reloaded[tag], f"{tag} didn't roundtrip" -def test_change_stripbytecounts_tag_type(tmp_path): +def test_change_stripbytecounts_tag_type(tmp_path: Path) -> None: out = str(tmp_path / "temp.tiff") with Image.open("Tests/images/hopper.tif") as im: info = im.tag_v2 @@ -176,19 +177,21 @@ def test_change_stripbytecounts_tag_type(tmp_path): assert reloaded.tag_v2.tagtype[TiffImagePlugin.STRIPBYTECOUNTS] == TiffTags.LONG -def test_no_duplicate_50741_tag(): +def test_no_duplicate_50741_tag() -> None: assert TAG_IDS["MakerNoteSafety"] == 50741 assert TAG_IDS["BestQualityScale"] == 50780 -def test_iptc(tmp_path): +def test_iptc(tmp_path: Path) -> None: out = str(tmp_path / "temp.tiff") with Image.open("Tests/images/hopper.Lab.tif") as im: im.save(out) @pytest.mark.parametrize("value, expected", ((b"test", "test"), (1, "1"))) -def test_writing_other_types_to_ascii(value, expected, tmp_path): +def test_writing_other_types_to_ascii( + value: bytes | int, expected: str, tmp_path: Path +) -> None: info = TiffImagePlugin.ImageFileDirectory_v2() tag = TiffTags.TAGS_V2[271] @@ -205,7 +208,7 @@ def test_writing_other_types_to_ascii(value, expected, tmp_path): @pytest.mark.parametrize("value", (1, IFDRational(1))) -def test_writing_other_types_to_bytes(value, tmp_path): +def test_writing_other_types_to_bytes(value: int | IFDRational, tmp_path: Path) -> None: im = hopper() info = TiffImagePlugin.ImageFileDirectory_v2() @@ -221,7 +224,7 @@ def test_writing_other_types_to_bytes(value, tmp_path): assert reloaded.tag_v2[700] == b"\x01" -def test_writing_other_types_to_undefined(tmp_path): +def test_writing_other_types_to_undefined(tmp_path: Path) -> None: im = hopper() info = TiffImagePlugin.ImageFileDirectory_v2() @@ -237,7 +240,7 @@ def test_writing_other_types_to_undefined(tmp_path): assert reloaded.tag_v2[33723] == b"1" -def test_undefined_zero(tmp_path): +def test_undefined_zero(tmp_path: Path) -> None: # Check that the tag has not been changed since this test was created tag = TiffTags.TAGS_V2[45059] assert tag.type == TiffTags.UNDEFINED @@ -252,7 +255,7 @@ def test_undefined_zero(tmp_path): assert info[45059] == original -def test_empty_metadata(): +def test_empty_metadata() -> None: f = io.BytesIO(b"II*\x00\x08\x00\x00\x00") head = f.read(8) info = TiffImagePlugin.ImageFileDirectory(head) @@ -261,7 +264,7 @@ def test_empty_metadata(): info.load(f) -def test_iccprofile(tmp_path): +def test_iccprofile(tmp_path: Path) -> None: # https://github.com/python-pillow/Pillow/issues/1462 out = str(tmp_path / "temp.tiff") with Image.open("Tests/images/hopper.iccprofile.tif") as im: @@ -272,7 +275,7 @@ def test_iccprofile(tmp_path): assert im.info["icc_profile"] == reloaded.info["icc_profile"] -def test_iccprofile_binary(): +def test_iccprofile_binary() -> None: # https://github.com/python-pillow/Pillow/issues/1526 # We should be able to load this, # but probably won't be able to save it. @@ -282,19 +285,19 @@ def test_iccprofile_binary(): assert im.info["icc_profile"] -def test_iccprofile_save_png(tmp_path): +def test_iccprofile_save_png(tmp_path: Path) -> None: with Image.open("Tests/images/hopper.iccprofile.tif") as im: outfile = str(tmp_path / "temp.png") im.save(outfile) -def test_iccprofile_binary_save_png(tmp_path): +def test_iccprofile_binary_save_png(tmp_path: Path) -> None: with Image.open("Tests/images/hopper.iccprofile_binary.tif") as im: outfile = str(tmp_path / "temp.png") im.save(outfile) -def test_exif_div_zero(tmp_path): +def test_exif_div_zero(tmp_path: Path) -> None: im = hopper() info = TiffImagePlugin.ImageFileDirectory_v2() info[41988] = TiffImagePlugin.IFDRational(0, 0) @@ -307,7 +310,7 @@ def test_exif_div_zero(tmp_path): assert 0 == reloaded.tag_v2[41988].denominator -def test_ifd_unsigned_rational(tmp_path): +def test_ifd_unsigned_rational(tmp_path: Path) -> None: im = hopper() info = TiffImagePlugin.ImageFileDirectory_v2() @@ -338,7 +341,7 @@ def test_ifd_unsigned_rational(tmp_path): assert 1 == reloaded.tag_v2[41493].denominator -def test_ifd_signed_rational(tmp_path): +def test_ifd_signed_rational(tmp_path: Path) -> None: im = hopper() info = TiffImagePlugin.ImageFileDirectory_v2() @@ -381,7 +384,7 @@ def test_ifd_signed_rational(tmp_path): assert -1 == reloaded.tag_v2[37380].denominator -def test_ifd_signed_long(tmp_path): +def test_ifd_signed_long(tmp_path: Path) -> None: im = hopper() info = TiffImagePlugin.ImageFileDirectory_v2() @@ -394,7 +397,7 @@ def test_ifd_signed_long(tmp_path): assert reloaded.tag_v2[37000] == -60000 -def test_empty_values(): +def test_empty_values() -> None: data = io.BytesIO( b"II*\x00\x08\x00\x00\x00\x03\x00\x1a\x01\x05\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x1b\x01\x05\x00\x00\x00\x00\x00\x00\x00\x00\x00" @@ -409,7 +412,7 @@ def test_empty_values(): assert 33432 in info -def test_photoshop_info(tmp_path): +def test_photoshop_info(tmp_path: Path) -> None: with Image.open("Tests/images/issue_2278.tif") as im: assert len(im.tag_v2[34377]) == 70 assert isinstance(im.tag_v2[34377], bytes) @@ -420,7 +423,7 @@ def test_photoshop_info(tmp_path): assert isinstance(reloaded.tag_v2[34377], bytes) -def test_too_many_entries(): +def test_too_many_entries() -> None: ifd = TiffImagePlugin.ImageFileDirectory_v2() # 277: ("SamplesPerPixel", SHORT, 1), @@ -432,7 +435,7 @@ def test_too_many_entries(): assert ifd[277] == 4 -def test_tag_group_data(): +def test_tag_group_data() -> None: base_ifd = TiffImagePlugin.ImageFileDirectory_v2() interop_ifd = TiffImagePlugin.ImageFileDirectory_v2(group=40965) for ifd in (base_ifd, interop_ifd): @@ -446,7 +449,7 @@ def test_tag_group_data(): assert base_ifd.tagtype[2] != interop_ifd.tagtype[256] -def test_empty_subifd(tmp_path): +def test_empty_subifd(tmp_path: Path) -> None: out = str(tmp_path / "temp.jpg") im = hopper() diff --git a/Tests/test_file_wal.py b/Tests/test_file_wal.py index 7acec9759..b34975e83 100644 --- a/Tests/test_file_wal.py +++ b/Tests/test_file_wal.py @@ -7,7 +7,7 @@ from .helper import assert_image_equal_tofile TEST_FILE = "Tests/images/hopper.wal" -def test_open(): +def test_open() -> None: with WalImageFile.open(TEST_FILE) as im: assert im.format == "WAL" assert im.format_description == "Quake2 Texture" @@ -19,7 +19,7 @@ def test_open(): assert_image_equal_tofile(im, "Tests/images/hopper_wal.png") -def test_load(): +def test_load() -> None: with WalImageFile.open(TEST_FILE) as im: assert im.load()[0, 0] == 122 diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py index c49418ce3..249846da4 100644 --- a/Tests/test_file_webp.py +++ b/Tests/test_file_webp.py @@ -4,6 +4,7 @@ import io import re import sys import warnings +from pathlib import Path import pytest @@ -26,7 +27,7 @@ except ImportError: class TestUnsupportedWebp: - def test_unsupported(self): + def test_unsupported(self) -> None: if HAVE_WEBP: WebPImagePlugin.SUPPORTED = False @@ -42,15 +43,15 @@ class TestUnsupportedWebp: @skip_unless_feature("webp") class TestFileWebp: - def setup_method(self): + def setup_method(self) -> None: self.rgb_mode = "RGB" - def test_version(self): + def test_version(self) -> None: _webp.WebPDecoderVersion() _webp.WebPDecoderBuggyAlpha() assert re.search(r"\d+\.\d+\.\d+$", features.version_module("webp")) - def test_read_rgb(self): + def test_read_rgb(self) -> None: """ Can we read a RGB mode WebP file without error? Does it have the bits we expect? @@ -67,7 +68,7 @@ class TestFileWebp: # dwebp -ppm ../../Tests/images/hopper.webp -o hopper_webp_bits.ppm assert_image_similar_tofile(image, "Tests/images/hopper_webp_bits.ppm", 1.0) - def _roundtrip(self, tmp_path, mode, epsilon, args={}): + def _roundtrip(self, tmp_path: Path, mode, epsilon, args={}) -> None: temp_file = str(tmp_path / "temp.webp") hopper(mode).save(temp_file, **args) @@ -93,7 +94,7 @@ class TestFileWebp: target = target.convert(self.rgb_mode) assert_image_similar(image, target, epsilon) - def test_write_rgb(self, tmp_path): + def test_write_rgb(self, tmp_path: Path) -> None: """ Can we write a RGB mode file to webp without error? Does it have the bits we expect? @@ -101,7 +102,7 @@ class TestFileWebp: self._roundtrip(tmp_path, self.rgb_mode, 12.5) - def test_write_method(self, tmp_path): + def test_write_method(self, tmp_path: Path) -> None: self._roundtrip(tmp_path, self.rgb_mode, 12.0, {"method": 6}) buffer_no_args = io.BytesIO() @@ -112,7 +113,7 @@ class TestFileWebp: assert buffer_no_args.getbuffer() != buffer_method.getbuffer() @skip_unless_feature("webp_anim") - def test_save_all(self, tmp_path): + def test_save_all(self, tmp_path: Path) -> None: temp_file = str(tmp_path / "temp.webp") im = Image.new("RGB", (1, 1)) im2 = Image.new("RGB", (1, 1), "#f00") @@ -124,14 +125,14 @@ class TestFileWebp: reloaded.seek(1) assert_image_similar(im2, reloaded, 1) - def test_icc_profile(self, tmp_path): + def test_icc_profile(self, tmp_path: Path) -> None: self._roundtrip(tmp_path, self.rgb_mode, 12.5, {"icc_profile": None}) if _webp.HAVE_WEBPANIM: self._roundtrip( tmp_path, self.rgb_mode, 12.5, {"icc_profile": None, "save_all": True} ) - def test_write_unsupported_mode_L(self, tmp_path): + def test_write_unsupported_mode_L(self, tmp_path: Path) -> None: """ Saving a black-and-white file to WebP format should work, and be similar to the original file. @@ -139,7 +140,7 @@ class TestFileWebp: self._roundtrip(tmp_path, "L", 10.0) - def test_write_unsupported_mode_P(self, tmp_path): + def test_write_unsupported_mode_P(self, tmp_path: Path) -> None: """ Saving a palette-based file to WebP format should work, and be similar to the original file. @@ -148,14 +149,14 @@ class TestFileWebp: self._roundtrip(tmp_path, "P", 50.0) @pytest.mark.skipif(sys.maxsize <= 2**32, reason="Requires 64-bit system") - def test_write_encoding_error_message(self, tmp_path): + def test_write_encoding_error_message(self, tmp_path: Path) -> None: temp_file = str(tmp_path / "temp.webp") im = Image.new("RGB", (15000, 15000)) with pytest.raises(ValueError) as e: im.save(temp_file, method=0) assert str(e.value) == "encoding error 6" - def test_WebPEncode_with_invalid_args(self): + def test_WebPEncode_with_invalid_args(self) -> None: """ Calling encoder functions with no arguments should result in an error. """ @@ -166,7 +167,7 @@ class TestFileWebp: with pytest.raises(TypeError): _webp.WebPEncode() - def test_WebPDecode_with_invalid_args(self): + def test_WebPDecode_with_invalid_args(self) -> None: """ Calling decoder functions with no arguments should result in an error. """ @@ -177,14 +178,14 @@ class TestFileWebp: with pytest.raises(TypeError): _webp.WebPDecode() - def test_no_resource_warning(self, tmp_path): + def test_no_resource_warning(self, tmp_path: Path) -> None: file_path = "Tests/images/hopper.webp" with Image.open(file_path) as image: temp_file = str(tmp_path / "temp.webp") with warnings.catch_warnings(): image.save(temp_file) - def test_file_pointer_could_be_reused(self): + def test_file_pointer_could_be_reused(self) -> None: file_path = "Tests/images/hopper.webp" with open(file_path, "rb") as blob: Image.open(blob).load() @@ -195,14 +196,14 @@ class TestFileWebp: (0, (0,), (-1, 0, 1, 2), (253, 254, 255, 256)), ) @skip_unless_feature("webp_anim") - def test_invalid_background(self, background, tmp_path): + def test_invalid_background(self, background, tmp_path: Path) -> None: temp_file = str(tmp_path / "temp.webp") im = hopper() with pytest.raises(OSError): im.save(temp_file, save_all=True, append_images=[im], background=background) @skip_unless_feature("webp_anim") - def test_background_from_gif(self, tmp_path): + def test_background_from_gif(self, tmp_path: Path) -> None: # Save L mode GIF with background with Image.open("Tests/images/no_palette_with_background.gif") as im: out_webp = str(tmp_path / "temp.webp") @@ -227,7 +228,7 @@ class TestFileWebp: assert difference < 5 @skip_unless_feature("webp_anim") - def test_duration(self, tmp_path): + def test_duration(self, tmp_path: Path) -> None: with Image.open("Tests/images/dispose_bgnd.gif") as im: assert im.info["duration"] == 1000 @@ -238,7 +239,7 @@ class TestFileWebp: reloaded.load() assert reloaded.info["duration"] == 1000 - def test_roundtrip_rgba_palette(self, tmp_path): + def test_roundtrip_rgba_palette(self, tmp_path: Path) -> None: temp_file = str(tmp_path / "temp.webp") im = Image.new("RGBA", (1, 1)).convert("P") assert im.mode == "P" diff --git a/Tests/test_file_webp_alpha.py b/Tests/test_file_webp_alpha.py index cfda35a09..a95434624 100644 --- a/Tests/test_file_webp_alpha.py +++ b/Tests/test_file_webp_alpha.py @@ -1,5 +1,7 @@ from __future__ import annotations +from pathlib import Path + import pytest from PIL import Image @@ -14,12 +16,12 @@ from .helper import ( _webp = pytest.importorskip("PIL._webp", reason="WebP support not installed") -def setup_module(): +def setup_module() -> None: if _webp.WebPDecoderBuggyAlpha(): pytest.skip("Buggy early version of WebP installed, not testing transparency") -def test_read_rgba(): +def test_read_rgba() -> None: """ Can we read an RGBA mode file without error? Does it have the bits we expect? @@ -39,7 +41,7 @@ def test_read_rgba(): assert_image_similar_tofile(image, "Tests/images/transparent.png", 20.0) -def test_write_lossless_rgb(tmp_path): +def test_write_lossless_rgb(tmp_path: Path) -> None: """ Can we write an RGBA mode file with lossless compression without error? Does it have the bits we expect? @@ -68,7 +70,7 @@ def test_write_lossless_rgb(tmp_path): assert_image_equal(image, pil_image) -def test_write_rgba(tmp_path): +def test_write_rgba(tmp_path: Path) -> None: """ Can we write a RGBA mode file to WebP without error. Does it have the bits we expect? @@ -99,7 +101,7 @@ def test_write_rgba(tmp_path): assert_image_similar(image, pil_image, 1.0) -def test_keep_rgb_values_when_transparent(tmp_path): +def test_keep_rgb_values_when_transparent(tmp_path: Path) -> None: """ Saving transparent pixels should retain their original RGB values when using the "exact" parameter. @@ -128,7 +130,7 @@ def test_keep_rgb_values_when_transparent(tmp_path): assert_image_equal(reloaded.convert("RGB"), image) -def test_write_unsupported_mode_PA(tmp_path): +def test_write_unsupported_mode_PA(tmp_path: Path) -> None: """ Saving a palette-based file with transparency to WebP format should work, and be similar to the original file. diff --git a/Tests/test_file_webp_animated.py b/Tests/test_file_webp_animated.py index 426fe7a02..9a730f1f9 100644 --- a/Tests/test_file_webp_animated.py +++ b/Tests/test_file_webp_animated.py @@ -1,5 +1,7 @@ from __future__ import annotations +from pathlib import Path + import pytest from packaging.version import parse as parse_version @@ -18,7 +20,7 @@ pytestmark = [ ] -def test_n_frames(): +def test_n_frames() -> None: """Ensure that WebP format sets n_frames and is_animated attributes correctly.""" with Image.open("Tests/images/hopper.webp") as im: @@ -30,7 +32,7 @@ def test_n_frames(): assert im.is_animated -def test_write_animation_L(tmp_path): +def test_write_animation_L(tmp_path: Path) -> None: """ Convert an animated GIF to animated WebP, then compare the frame count, and first and last frames to ensure they're visually similar. @@ -60,13 +62,13 @@ def test_write_animation_L(tmp_path): assert_image_similar(im, orig.convert("RGBA"), 32.9) -def test_write_animation_RGB(tmp_path): +def test_write_animation_RGB(tmp_path: Path) -> None: """ Write an animated WebP from RGB frames, and ensure the frames are visually similar to the originals. """ - def check(temp_file): + def check(temp_file) -> None: with Image.open(temp_file) as im: assert im.n_frames == 2 @@ -105,7 +107,7 @@ def test_write_animation_RGB(tmp_path): check(temp_file2) -def test_timestamp_and_duration(tmp_path): +def test_timestamp_and_duration(tmp_path: Path) -> None: """ Try passing a list of durations, and make sure the encoded timestamps and durations are correct. @@ -136,7 +138,7 @@ def test_timestamp_and_duration(tmp_path): ts += durations[frame] -def test_float_duration(tmp_path): +def test_float_duration(tmp_path: Path) -> None: temp_file = str(tmp_path / "temp.webp") with Image.open("Tests/images/iss634.apng") as im: assert im.info["duration"] == 70.0 @@ -148,7 +150,7 @@ def test_float_duration(tmp_path): assert reloaded.info["duration"] == 70 -def test_seeking(tmp_path): +def test_seeking(tmp_path: Path) -> None: """ Create an animated WebP file, and then try seeking through frames in reverse-order, verifying the timestamps and durations are correct. @@ -179,7 +181,7 @@ def test_seeking(tmp_path): ts -= dur -def test_seek_errors(): +def test_seek_errors() -> None: with Image.open("Tests/images/iss634.webp") as im: with pytest.raises(EOFError): im.seek(-1) diff --git a/Tests/test_file_webp_lossless.py b/Tests/test_file_webp_lossless.py index 08c80973a..32e29de56 100644 --- a/Tests/test_file_webp_lossless.py +++ b/Tests/test_file_webp_lossless.py @@ -1,5 +1,7 @@ from __future__ import annotations +from pathlib import Path + import pytest from PIL import Image @@ -10,7 +12,7 @@ _webp = pytest.importorskip("PIL._webp", reason="WebP support not installed") RGB_MODE = "RGB" -def test_write_lossless_rgb(tmp_path): +def test_write_lossless_rgb(tmp_path: Path) -> None: if _webp.WebPDecoderVersion() < 0x0200: pytest.skip("lossless not included") diff --git a/Tests/test_file_webp_metadata.py b/Tests/test_file_webp_metadata.py index deaf5e380..875941240 100644 --- a/Tests/test_file_webp_metadata.py +++ b/Tests/test_file_webp_metadata.py @@ -1,6 +1,8 @@ from __future__ import annotations from io import BytesIO +from pathlib import Path +from types import ModuleType import pytest @@ -13,13 +15,14 @@ pytestmark = [ skip_unless_feature("webp_mux"), ] +ElementTree: ModuleType | None try: from defusedxml import ElementTree except ImportError: ElementTree = None -def test_read_exif_metadata(): +def test_read_exif_metadata() -> None: file_path = "Tests/images/flower.webp" with Image.open(file_path) as image: assert image.format == "WEBP" @@ -37,7 +40,7 @@ def test_read_exif_metadata(): assert exif_data == expected_exif -def test_read_exif_metadata_without_prefix(): +def test_read_exif_metadata_without_prefix() -> None: with Image.open("Tests/images/flower2.webp") as im: # Assert prefix is not present assert im.info["exif"][:6] != b"Exif\x00\x00" @@ -49,7 +52,7 @@ def test_read_exif_metadata_without_prefix(): @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) -def test_write_exif_metadata(): +def test_write_exif_metadata() -> None: file_path = "Tests/images/flower.jpg" test_buffer = BytesIO() with Image.open(file_path) as image: @@ -63,7 +66,7 @@ def test_write_exif_metadata(): assert webp_exif == expected_exif[6:], "WebP EXIF didn't match" -def test_read_icc_profile(): +def test_read_icc_profile() -> None: file_path = "Tests/images/flower2.webp" with Image.open(file_path) as image: assert image.format == "WEBP" @@ -80,7 +83,7 @@ def test_read_icc_profile(): @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) -def test_write_icc_metadata(): +def test_write_icc_metadata() -> None: file_path = "Tests/images/flower2.jpg" test_buffer = BytesIO() with Image.open(file_path) as image: @@ -100,7 +103,7 @@ def test_write_icc_metadata(): @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) -def test_read_no_exif(): +def test_read_no_exif() -> None: file_path = "Tests/images/flower.jpg" test_buffer = BytesIO() with Image.open(file_path) as image: @@ -113,7 +116,7 @@ def test_read_no_exif(): assert not webp_image._getexif() -def test_getxmp(): +def test_getxmp() -> None: with Image.open("Tests/images/flower.webp") as im: assert "xmp" not in im.info assert im.getxmp() == {} @@ -133,7 +136,7 @@ def test_getxmp(): @skip_unless_feature("webp_anim") -def test_write_animated_metadata(tmp_path): +def test_write_animated_metadata(tmp_path: Path) -> None: iccp_data = b"" exif_data = b"" xmp_data = b"" diff --git a/Tests/test_file_wmf.py b/Tests/test_file_wmf.py index 6e1d4c136..b43e3f296 100644 --- a/Tests/test_file_wmf.py +++ b/Tests/test_file_wmf.py @@ -1,5 +1,7 @@ from __future__ import annotations +from pathlib import Path + import pytest from PIL import Image, WmfImagePlugin @@ -7,7 +9,7 @@ from PIL import Image, WmfImagePlugin from .helper import assert_image_similar_tofile, hopper -def test_load_raw(): +def test_load_raw() -> None: # Test basic EMF open and rendering with Image.open("Tests/images/drawing.emf") as im: if hasattr(Image.core, "drawwmf"): @@ -25,17 +27,17 @@ def test_load_raw(): assert_image_similar_tofile(im, "Tests/images/drawing_wmf_ref.png", 2.0) -def test_load(): +def test_load() -> None: with Image.open("Tests/images/drawing.emf") as im: if hasattr(Image.core, "drawwmf"): assert im.load()[0, 0] == (255, 255, 255) -def test_register_handler(tmp_path): +def test_register_handler(tmp_path: Path) -> None: class TestHandler: methodCalled = False - def save(self, im, fp, filename): + def save(self, im, fp, filename) -> None: self.methodCalled = True handler = TestHandler() @@ -51,12 +53,12 @@ def test_register_handler(tmp_path): WmfImagePlugin.register_handler(original_handler) -def test_load_float_dpi(): +def test_load_float_dpi() -> None: with Image.open("Tests/images/drawing.emf") as im: assert im.info["dpi"] == 1423.7668161434979 -def test_load_set_dpi(): +def test_load_set_dpi() -> None: with Image.open("Tests/images/drawing.wmf") as im: assert im.size == (82, 82) @@ -68,7 +70,7 @@ def test_load_set_dpi(): @pytest.mark.parametrize("ext", (".wmf", ".emf")) -def test_save(ext, tmp_path): +def test_save(ext, tmp_path: Path) -> None: im = hopper() tmpfile = str(tmp_path / ("temp" + ext)) diff --git a/Tests/test_file_xbm.py b/Tests/test_file_xbm.py index 69a0a1b38..44dd2541f 100644 --- a/Tests/test_file_xbm.py +++ b/Tests/test_file_xbm.py @@ -1,6 +1,7 @@ from __future__ import annotations from io import BytesIO +from pathlib import Path import pytest @@ -32,14 +33,14 @@ static char basic_bits[] = { """ -def test_pil151(): +def test_pil151() -> None: with Image.open(BytesIO(PIL151)) as im: im.load() assert im.mode == "1" assert im.size == (32, 32) -def test_open(): +def test_open() -> None: # Arrange # Created with `convert hopper.png hopper.xbm` filename = "Tests/images/hopper.xbm" @@ -51,7 +52,7 @@ def test_open(): assert im.size == (128, 128) -def test_open_filename_with_underscore(): +def test_open_filename_with_underscore() -> None: # Arrange # Created with `convert hopper.png hopper_underscore.xbm` filename = "Tests/images/hopper_underscore.xbm" @@ -63,14 +64,14 @@ def test_open_filename_with_underscore(): assert im.size == (128, 128) -def test_invalid_file(): +def test_invalid_file() -> None: invalid_file = "Tests/images/flower.jpg" with pytest.raises(SyntaxError): XbmImagePlugin.XbmImageFile(invalid_file) -def test_save_wrong_mode(tmp_path): +def test_save_wrong_mode(tmp_path: Path) -> None: im = hopper() out = str(tmp_path / "temp.xbm") @@ -78,7 +79,7 @@ def test_save_wrong_mode(tmp_path): im.save(out) -def test_hotspot(tmp_path): +def test_hotspot(tmp_path: Path) -> None: im = hopper("1") out = str(tmp_path / "temp.xbm") diff --git a/Tests/test_file_xpm.py b/Tests/test_file_xpm.py index 529a45580..26afe93f4 100644 --- a/Tests/test_file_xpm.py +++ b/Tests/test_file_xpm.py @@ -9,7 +9,7 @@ from .helper import assert_image_similar, hopper TEST_FILE = "Tests/images/hopper.xpm" -def test_sanity(): +def test_sanity() -> None: with Image.open(TEST_FILE) as im: im.load() assert im.mode == "P" @@ -20,14 +20,14 @@ def test_sanity(): assert_image_similar(im.convert("RGB"), hopper("RGB"), 60) -def test_invalid_file(): +def test_invalid_file() -> None: invalid_file = "Tests/images/flower.jpg" with pytest.raises(SyntaxError): XpmImagePlugin.XpmImageFile(invalid_file) -def test_load_read(): +def test_load_read() -> None: # Arrange with Image.open(TEST_FILE) as im: dummy_bytes = 1 diff --git a/Tests/test_file_xvthumb.py b/Tests/test_file_xvthumb.py index b87494eba..6b8115930 100644 --- a/Tests/test_file_xvthumb.py +++ b/Tests/test_file_xvthumb.py @@ -9,7 +9,7 @@ from .helper import assert_image_similar, hopper TEST_FILE = "Tests/images/hopper.p7" -def test_open(): +def test_open() -> None: # Act with Image.open(TEST_FILE) as im: # Assert @@ -20,7 +20,7 @@ def test_open(): assert_image_similar(im, im_hopper, 9) -def test_unexpected_eof(): +def test_unexpected_eof() -> None: # Test unexpected EOF reading XV thumbnail file # Arrange bad_file = "Tests/images/hopper_bad.p7" @@ -30,7 +30,7 @@ def test_unexpected_eof(): XVThumbImagePlugin.XVThumbImageFile(bad_file) -def test_invalid_file(): +def test_invalid_file() -> None: # Arrange invalid_file = "Tests/images/flower.jpg" diff --git a/Tests/test_font_bdf.py b/Tests/test_font_bdf.py index e5e856186..136070f9e 100644 --- a/Tests/test_font_bdf.py +++ b/Tests/test_font_bdf.py @@ -7,7 +7,7 @@ from PIL import BdfFontFile, FontFile filename = "Tests/images/courB08.bdf" -def test_sanity(): +def test_sanity() -> None: with open(filename, "rb") as test_file: font = BdfFontFile.BdfFontFile(test_file) @@ -15,7 +15,7 @@ def test_sanity(): assert len([_f for _f in font.glyph if _f]) == 190 -def test_invalid_file(): +def test_invalid_file() -> None: with open("Tests/images/flower.jpg", "rb") as fp: with pytest.raises(SyntaxError): BdfFontFile.BdfFontFile(fp) diff --git a/Tests/test_font_crash.py b/Tests/test_font_crash.py index e3c72c1ae..b82340ef7 100644 --- a/Tests/test_font_crash.py +++ b/Tests/test_font_crash.py @@ -8,7 +8,7 @@ from .helper import skip_unless_feature class TestFontCrash: - def _fuzz_font(self, font): + def _fuzz_font(self, font: ImageFont.FreeTypeFont) -> None: # from fuzzers.fuzz_font font.getbbox("ABC") font.getmask("test text") @@ -18,7 +18,7 @@ class TestFontCrash: draw.text((10, 10), "Test Text", font=font, fill="#000") @skip_unless_feature("freetype2") - def test_segfault(self): + def test_segfault(self) -> None: with pytest.raises(OSError): font = ImageFont.truetype("Tests/fonts/fuzz_font-5203009437302784") self._fuzz_font(font) diff --git a/Tests/test_font_leaks.py b/Tests/test_font_leaks.py index 4e29a856b..241f455b8 100644 --- a/Tests/test_font_leaks.py +++ b/Tests/test_font_leaks.py @@ -10,7 +10,7 @@ class TestTTypeFontLeak(PillowLeakTestCase): iterations = 10 mem_limit = 4096 # k - def _test_font(self, font): + def _test_font(self, font: ImageFont.FreeTypeFont) -> None: im = Image.new("RGB", (255, 255), "white") draw = ImageDraw.ImageDraw(im) self._test_leak( @@ -20,7 +20,7 @@ class TestTTypeFontLeak(PillowLeakTestCase): ) @skip_unless_feature("freetype2") - def test_leak(self): + def test_leak(self) -> None: ttype = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 20) self._test_font(ttype) @@ -30,6 +30,6 @@ class TestDefaultFontLeak(TestTTypeFontLeak): iterations = 100 mem_limit = 1024 # k - def test_leak(self): + def test_leak(self) -> None: default_font = ImageFont.load_default() self._test_font(default_font) diff --git a/Tests/test_font_pcf.py b/Tests/test_font_pcf.py index e6abede07..997809e46 100644 --- a/Tests/test_font_pcf.py +++ b/Tests/test_font_pcf.py @@ -1,6 +1,7 @@ from __future__ import annotations import os +from pathlib import Path import pytest @@ -20,7 +21,7 @@ message = "hello, world" pytestmark = skip_unless_feature("zlib") -def save_font(request, tmp_path): +def save_font(request: pytest.FixtureRequest, tmp_path: Path) -> str: with open(fontname, "rb") as test_file: font = PcfFontFile.PcfFontFile(test_file) assert isinstance(font, FontFile.FontFile) @@ -29,7 +30,7 @@ def save_font(request, tmp_path): tempname = str(tmp_path / "temp.pil") - def delete_tempfile(): + def delete_tempfile() -> None: try: os.remove(tempname[:-4] + ".pbm") except OSError: @@ -47,11 +48,11 @@ def save_font(request, tmp_path): return tempname -def test_sanity(request, tmp_path): +def test_sanity(request: pytest.FixtureRequest, tmp_path: Path) -> None: save_font(request, tmp_path) -def test_less_than_256_characters(): +def test_less_than_256_characters() -> None: with open("Tests/fonts/10x20-ISO8859-1-fewer-characters.pcf", "rb") as test_file: font = PcfFontFile.PcfFontFile(test_file) assert isinstance(font, FontFile.FontFile) @@ -59,13 +60,13 @@ def test_less_than_256_characters(): assert len([_f for _f in font.glyph if _f]) == 127 -def test_invalid_file(): +def test_invalid_file() -> None: with open("Tests/images/flower.jpg", "rb") as fp: with pytest.raises(SyntaxError): PcfFontFile.PcfFontFile(fp) -def test_draw(request, tmp_path): +def test_draw(request: pytest.FixtureRequest, tmp_path: Path) -> None: tempname = save_font(request, tmp_path) font = ImageFont.load(tempname) im = Image.new("L", (130, 30), "white") @@ -74,7 +75,7 @@ def test_draw(request, tmp_path): assert_image_similar_tofile(im, "Tests/images/test_draw_pbm_target.png", 0) -def test_textsize(request, tmp_path): +def test_textsize(request: pytest.FixtureRequest, tmp_path: Path) -> None: tempname = save_font(request, tmp_path) font = ImageFont.load(tempname) for i in range(255): @@ -90,7 +91,9 @@ def test_textsize(request, tmp_path): assert font.getbbox(msg) == (0, 0, len(msg) * 10, 20) -def _test_high_characters(request, tmp_path, message): +def _test_high_characters( + request: pytest.FixtureRequest, tmp_path: Path, message: str | bytes +) -> None: tempname = save_font(request, tmp_path) font = ImageFont.load(tempname) im = Image.new("L", (750, 30), "white") @@ -99,7 +102,7 @@ def _test_high_characters(request, tmp_path, message): assert_image_similar_tofile(im, "Tests/images/high_ascii_chars.png", 0) -def test_high_characters(request, tmp_path): +def test_high_characters(request: pytest.FixtureRequest, tmp_path: Path) -> None: message = "".join(chr(i + 1) for i in range(140, 232)) _test_high_characters(request, tmp_path, message) # accept bytes instances. diff --git a/Tests/test_font_pcf_charsets.py b/Tests/test_font_pcf_charsets.py index 4c2d7185e..895458d9d 100644 --- a/Tests/test_font_pcf_charsets.py +++ b/Tests/test_font_pcf_charsets.py @@ -1,6 +1,8 @@ from __future__ import annotations import os +from pathlib import Path +from typing import TypedDict import pytest @@ -14,7 +16,14 @@ from .helper import ( fontname = "Tests/fonts/ter-x20b.pcf" -charsets = { + +class Charset(TypedDict): + glyph_count: int + message: str + image1: str + + +charsets: dict[str, Charset] = { "iso8859-1": { "glyph_count": 223, "message": "hello, world", @@ -36,7 +45,7 @@ charsets = { pytestmark = skip_unless_feature("zlib") -def save_font(request, tmp_path, encoding): +def save_font(request: pytest.FixtureRequest, tmp_path: Path, encoding: str) -> str: with open(fontname, "rb") as test_file: font = PcfFontFile.PcfFontFile(test_file, encoding) assert isinstance(font, FontFile.FontFile) @@ -45,7 +54,7 @@ def save_font(request, tmp_path, encoding): tempname = str(tmp_path / "temp.pil") - def delete_tempfile(): + def delete_tempfile() -> None: try: os.remove(tempname[:-4] + ".pbm") except OSError: @@ -64,12 +73,12 @@ def save_font(request, tmp_path, encoding): @pytest.mark.parametrize("encoding", ("iso8859-1", "iso8859-2", "cp1250")) -def test_sanity(request, tmp_path, encoding): +def test_sanity(request: pytest.FixtureRequest, tmp_path: Path, encoding: str) -> None: save_font(request, tmp_path, encoding) @pytest.mark.parametrize("encoding", ("iso8859-1", "iso8859-2", "cp1250")) -def test_draw(request, tmp_path, encoding): +def test_draw(request: pytest.FixtureRequest, tmp_path: Path, encoding: str) -> None: tempname = save_font(request, tmp_path, encoding) font = ImageFont.load(tempname) im = Image.new("L", (150, 30), "white") @@ -80,7 +89,9 @@ def test_draw(request, tmp_path, encoding): @pytest.mark.parametrize("encoding", ("iso8859-1", "iso8859-2", "cp1250")) -def test_textsize(request, tmp_path, encoding): +def test_textsize( + request: pytest.FixtureRequest, tmp_path: Path, encoding: str +) -> None: tempname = save_font(request, tmp_path, encoding) font = ImageFont.load(tempname) for i in range(255): diff --git a/Tests/test_fontfile.py b/Tests/test_fontfile.py index eda8fb812..206499a04 100644 --- a/Tests/test_fontfile.py +++ b/Tests/test_fontfile.py @@ -1,11 +1,13 @@ from __future__ import annotations +from pathlib import Path + import pytest from PIL import FontFile -def test_save(tmp_path): +def test_save(tmp_path: Path) -> None: tempname = str(tmp_path / "temp.pil") font = FontFile.FontFile() diff --git a/Tests/test_format_hsv.py b/Tests/test_format_hsv.py index 6395ae4aa..c07024a2c 100644 --- a/Tests/test_format_hsv.py +++ b/Tests/test_format_hsv.py @@ -2,30 +2,27 @@ from __future__ import annotations import colorsys import itertools +from typing import Callable from PIL import Image from .helper import assert_image_similar, hopper -def int_to_float(i): +def int_to_float(i: int) -> float: return i / 255 -def str_to_float(i): - return ord(i) / 255 - - -def tuple_to_ints(tp): +def tuple_to_ints(tp: tuple[float, float, float]) -> tuple[int, int, int]: x, y, z = tp return int(x * 255.0), int(y * 255.0), int(z * 255.0) -def test_sanity(): +def test_sanity() -> None: Image.new("HSV", (100, 100)) -def wedge(): +def wedge() -> Image.Image: w = Image._wedge() w90 = w.rotate(90) @@ -49,7 +46,11 @@ def wedge(): return img -def to_xxx_colorsys(im, func, mode): +def to_xxx_colorsys( + im: Image.Image, + func: Callable[[float, float, float], tuple[float, float, float]], + mode: str, +) -> Image.Image: # convert the hard way using the library colorsys routines. (r, g, b) = im.split() @@ -70,15 +71,15 @@ def to_xxx_colorsys(im, func, mode): return hsv -def to_hsv_colorsys(im): +def to_hsv_colorsys(im: Image.Image) -> Image.Image: return to_xxx_colorsys(im, colorsys.rgb_to_hsv, "HSV") -def to_rgb_colorsys(im): +def to_rgb_colorsys(im: Image.Image) -> Image.Image: return to_xxx_colorsys(im, colorsys.hsv_to_rgb, "RGB") -def test_wedge(): +def test_wedge() -> None: src = wedge().resize((3 * 32, 32), Image.Resampling.BILINEAR) im = src.convert("HSV") comparable = to_hsv_colorsys(src) @@ -110,7 +111,7 @@ def test_wedge(): ) -def test_convert(): +def test_convert() -> None: im = hopper("RGB").convert("HSV") comparable = to_hsv_colorsys(hopper("RGB")) @@ -128,7 +129,7 @@ def test_convert(): ) -def test_hsv_to_rgb(): +def test_hsv_to_rgb() -> None: comparable = to_hsv_colorsys(hopper("RGB")) converted = comparable.convert("RGB") comparable = to_rgb_colorsys(comparable) diff --git a/Tests/test_format_lab.py b/Tests/test_format_lab.py index a55620e09..4fcc37e88 100644 --- a/Tests/test_format_lab.py +++ b/Tests/test_format_lab.py @@ -3,7 +3,7 @@ from __future__ import annotations from PIL import Image -def test_white(): +def test_white() -> None: with Image.open("Tests/images/lab.tif") as i: i.load() @@ -24,7 +24,7 @@ def test_white(): assert list(b) == [128] * 100 -def test_green(): +def test_green() -> None: # l= 50 (/100), a = -100 (-128 .. 128) b=0 in PS # == RGB: 0, 152, 117 with Image.open("Tests/images/lab-green.tif") as i: @@ -32,7 +32,7 @@ def test_green(): assert k == (128, 28, 128) -def test_red(): +def test_red() -> None: # l= 50 (/100), a = 100 (-128 .. 128) b=0 in PS # == RGB: 255, 0, 124 with Image.open("Tests/images/lab-red.tif") as i: diff --git a/Tests/test_image.py b/Tests/test_image.py index dd989ad99..b4e8e660c 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -7,6 +7,8 @@ import shutil import sys import tempfile import warnings +from pathlib import Path +from typing import IO import pytest @@ -60,19 +62,19 @@ class TestImage: "HSV", ), ) - def test_image_modes_success(self, mode): + def test_image_modes_success(self, mode: str) -> None: Image.new(mode, (1, 1)) @pytest.mark.parametrize("mode", ("", "bad", "very very long")) - def test_image_modes_fail(self, mode): + def test_image_modes_fail(self, mode: str) -> None: with pytest.raises(ValueError) as e: Image.new(mode, (1, 1)) assert str(e.value) == "unrecognized image mode" - def test_exception_inheritance(self): + def test_exception_inheritance(self) -> None: assert issubclass(UnidentifiedImageError, OSError) - def test_sanity(self): + def test_sanity(self) -> None: im = Image.new("L", (100, 100)) assert repr(im)[:45] == " None: class Pretty: - def text(self, text): + def text(self, text: str) -> None: self.pretty_output = text im = Image.new("L", (100, 100)) @@ -108,7 +110,7 @@ class TestImage: im._repr_pretty_(p, None) assert p.pretty_output == "" - def test_open_formats(self): + def test_open_formats(self) -> None: PNGFILE = "Tests/images/hopper.png" JPGFILE = "Tests/images/hopper.jpg" @@ -130,39 +132,37 @@ class TestImage: assert im.mode == "RGB" assert im.size == (128, 128) - def test_width_height(self): + def test_width_height(self) -> None: im = Image.new("RGB", (1, 2)) assert im.width == 1 assert im.height == 2 with pytest.raises(AttributeError): - im.size = (3, 4) + im.size = (3, 4) # type: ignore[misc] - def test_set_mode(self): + def test_set_mode(self) -> None: im = Image.new("RGB", (1, 1)) with pytest.raises(AttributeError): - im.mode = "P" + im.mode = "P" # type: ignore[misc] - def test_invalid_image(self): + def test_invalid_image(self) -> None: im = io.BytesIO(b"") with pytest.raises(UnidentifiedImageError): with Image.open(im): pass - def test_bad_mode(self): + def test_bad_mode(self) -> None: with pytest.raises(ValueError): with Image.open("filename", "bad mode"): pass - def test_stringio(self): + def test_stringio(self) -> None: with pytest.raises(ValueError): with Image.open(io.StringIO()): pass - def test_pathlib(self, tmp_path): - from PIL.Image import Path - + def test_pathlib(self, tmp_path: Path) -> None: with Image.open(Path("Tests/images/multipage-mmap.tiff")) as im: assert im.mode == "P" assert im.size == (10, 10) @@ -179,11 +179,13 @@ class TestImage: os.remove(temp_file) im.save(Path(temp_file)) - def test_fp_name(self, tmp_path): + def test_fp_name(self, tmp_path: Path) -> None: temp_file = str(tmp_path / "temp.jpg") class FP: - def write(self, b): + name: str + + def write(self, b: bytes) -> None: pass fp = FP() @@ -192,7 +194,7 @@ class TestImage: im = hopper() im.save(fp) - def test_tempfile(self): + def test_tempfile(self) -> None: # see #1460, pathlib support breaks tempfile.TemporaryFile on py27 # Will error out on save on 3.0.0 im = hopper() @@ -201,13 +203,13 @@ class TestImage: fp.seek(0) assert_image_similar_tofile(im, fp, 20) - def test_unknown_extension(self, tmp_path): + def test_unknown_extension(self, tmp_path: Path) -> None: im = hopper() temp_file = str(tmp_path / "temp.unknown") with pytest.raises(ValueError): im.save(temp_file) - def test_internals(self): + def test_internals(self) -> None: im = Image.new("L", (100, 100)) im.readonly = 1 im._copy() @@ -222,7 +224,7 @@ class TestImage: sys.platform == "cygwin", reason="Test requires opening an mmaped file for writing", ) - def test_readonly_save(self, tmp_path): + def test_readonly_save(self, tmp_path: Path) -> None: temp_file = str(tmp_path / "temp.bmp") shutil.copy("Tests/images/rgb32bf-rgba.bmp", temp_file) @@ -230,7 +232,7 @@ class TestImage: assert im.readonly im.save(temp_file) - def test_dump(self, tmp_path): + def test_dump(self, tmp_path: Path) -> None: im = Image.new("L", (10, 10)) im._dump(str(tmp_path / "temp_L.ppm")) @@ -241,7 +243,7 @@ class TestImage: with pytest.raises(ValueError): im._dump(str(tmp_path / "temp_HSV.ppm")) - def test_comparison_with_other_type(self): + def test_comparison_with_other_type(self) -> None: # Arrange item = Image.new("RGB", (25, 25), "#000") num = 12 @@ -251,7 +253,7 @@ class TestImage: assert item is not None assert item != num - def test_expand_x(self): + def test_expand_x(self) -> None: # Arrange im = hopper() orig_size = im.size @@ -264,7 +266,7 @@ class TestImage: assert im.size[0] == orig_size[0] + 2 * xmargin assert im.size[1] == orig_size[1] + 2 * xmargin - def test_expand_xy(self): + def test_expand_xy(self) -> None: # Arrange im = hopper() orig_size = im.size @@ -278,12 +280,12 @@ class TestImage: assert im.size[0] == orig_size[0] + 2 * xmargin assert im.size[1] == orig_size[1] + 2 * ymargin - def test_getbands(self): + def test_getbands(self) -> None: # Assert assert hopper("RGB").getbands() == ("R", "G", "B") assert hopper("YCbCr").getbands() == ("Y", "Cb", "Cr") - def test_getchannel_wrong_params(self): + def test_getchannel_wrong_params(self) -> None: im = hopper() with pytest.raises(ValueError): @@ -295,7 +297,7 @@ class TestImage: with pytest.raises(ValueError): im.getchannel("1") - def test_getchannel(self): + def test_getchannel(self) -> None: im = hopper("YCbCr") Y, Cb, Cr = im.split() @@ -306,7 +308,7 @@ class TestImage: assert_image_equal(Cr, im.getchannel(2)) assert_image_equal(Cr, im.getchannel("Cr")) - def test_getbbox(self): + def test_getbbox(self) -> None: # Arrange im = hopper() @@ -316,7 +318,7 @@ class TestImage: # Assert assert bbox == (0, 0, 128, 128) - def test_ne(self): + def test_ne(self) -> None: # Arrange im1 = Image.new("RGB", (25, 25), "black") im2 = Image.new("RGB", (25, 25), "white") @@ -324,7 +326,7 @@ class TestImage: # Act / Assert assert im1 != im2 - def test_alpha_composite(self): + def test_alpha_composite(self) -> None: # https://stackoverflow.com/questions/3374878 # Arrange expected_colors = sorted( @@ -355,7 +357,7 @@ class TestImage: img_colors = sorted(img.getcolors()) assert img_colors == expected_colors - def test_alpha_inplace(self): + def test_alpha_inplace(self) -> None: src = Image.new("RGBA", (128, 128), "blue") over = Image.new("RGBA", (128, 128), "red") @@ -407,7 +409,7 @@ class TestImage: with pytest.raises(ValueError): source.alpha_composite(over, (0, 0), (0, -1)) - def test_register_open_duplicates(self): + def test_register_open_duplicates(self) -> None: # Arrange factory, accept = Image.OPEN["JPEG"] id_length = len(Image.ID) @@ -418,7 +420,7 @@ class TestImage: # Assert assert len(Image.ID) == id_length - def test_registered_extensions_uninitialized(self): + def test_registered_extensions_uninitialized(self) -> None: # Arrange Image._initialized = 0 @@ -428,7 +430,7 @@ class TestImage: # Assert assert Image._initialized == 2 - def test_registered_extensions(self): + def test_registered_extensions(self) -> None: # Arrange # Open an image to trigger plugin registration with Image.open("Tests/images/rgb.jpg"): @@ -442,7 +444,7 @@ class TestImage: for ext in [".cur", ".icns", ".tif", ".tiff"]: assert ext in extensions - def test_effect_mandelbrot(self): + def test_effect_mandelbrot(self) -> None: # Arrange size = (512, 512) extent = (-3, -2.5, 2, 2.5) @@ -455,7 +457,7 @@ class TestImage: assert im.size == (512, 512) assert_image_equal_tofile(im, "Tests/images/effect_mandelbrot.png") - def test_effect_mandelbrot_bad_arguments(self): + def test_effect_mandelbrot_bad_arguments(self) -> None: # Arrange size = (512, 512) # Get coordinates the wrong way round: @@ -467,7 +469,7 @@ class TestImage: with pytest.raises(ValueError): Image.effect_mandelbrot(size, extent, quality) - def test_effect_noise(self): + def test_effect_noise(self) -> None: # Arrange size = (100, 100) sigma = 128 @@ -485,7 +487,7 @@ class TestImage: p4 = im.getpixel((0, 4)) assert_not_all_same([p0, p1, p2, p3, p4]) - def test_effect_spread(self): + def test_effect_spread(self) -> None: # Arrange im = hopper() distance = 10 @@ -497,7 +499,7 @@ class TestImage: assert im.size == (128, 128) assert_image_similar_tofile(im2, "Tests/images/effect_spread.png", 110) - def test_effect_spread_zero(self): + def test_effect_spread_zero(self) -> None: # Arrange im = hopper() distance = 0 @@ -508,7 +510,7 @@ class TestImage: # Assert assert_image_equal(im, im2) - def test_check_size(self): + def test_check_size(self) -> None: # Checking that the _check_size function throws value errors when we want it to with pytest.raises(ValueError): Image.new("RGB", 0) # not a tuple @@ -537,10 +539,10 @@ class TestImage: "PILLOW_VALGRIND_TEST" in os.environ, reason="Valgrind is slower" ) @pytest.mark.parametrize("size", ((0, 100000000), (100000000, 0))) - def test_empty_image(self, size): + def test_empty_image(self, size: tuple[int, int]) -> None: Image.new("RGB", size) - def test_storage_neg(self): + def test_storage_neg(self) -> None: # Storage.c accepted negative values for xsize, ysize. Was # test_neg_ppm, but the core function for that has been # removed Calling directly into core to test the error in @@ -549,13 +551,13 @@ class TestImage: with pytest.raises(ValueError): Image.core.fill("RGB", (2, -2), (0, 0, 0)) - def test_one_item_tuple(self): + def test_one_item_tuple(self) -> None: for mode in ("I", "F", "L"): im = Image.new(mode, (100, 100), (5,)) px = im.load() assert px[0, 0] == 5 - def test_linear_gradient_wrong_mode(self): + def test_linear_gradient_wrong_mode(self) -> None: # Arrange wrong_mode = "RGB" @@ -564,7 +566,7 @@ class TestImage: Image.linear_gradient(wrong_mode) @pytest.mark.parametrize("mode", ("L", "P", "I", "F")) - def test_linear_gradient(self, mode): + def test_linear_gradient(self, mode: str) -> None: # Arrange target_file = "Tests/images/linear_gradient.png" @@ -580,7 +582,7 @@ class TestImage: target = target.convert(mode) assert_image_equal(im, target) - def test_radial_gradient_wrong_mode(self): + def test_radial_gradient_wrong_mode(self) -> None: # Arrange wrong_mode = "RGB" @@ -589,7 +591,7 @@ class TestImage: Image.radial_gradient(wrong_mode) @pytest.mark.parametrize("mode", ("L", "P", "I", "F")) - def test_radial_gradient(self, mode): + def test_radial_gradient(self, mode: str) -> None: # Arrange target_file = "Tests/images/radial_gradient.png" @@ -605,7 +607,7 @@ class TestImage: target = target.convert(mode) assert_image_equal(im, target) - def test_register_extensions(self): + def test_register_extensions(self) -> None: test_format = "a" exts = ["b", "c"] for ext in exts: @@ -621,7 +623,7 @@ class TestImage: assert ext_individual == ext_multiple - def test_remap_palette(self): + def test_remap_palette(self) -> None: # Test identity transform with Image.open("Tests/images/hopper.gif") as im: assert_image_equal(im, im.remap_palette(list(range(256)))) @@ -640,7 +642,7 @@ class TestImage: with pytest.raises(ValueError): im.remap_palette(None) - def test_remap_palette_transparency(self): + def test_remap_palette_transparency(self) -> None: im = Image.new("P", (1, 2), (0, 0, 0)) im.putpixel((0, 1), (255, 0, 0)) im.info["transparency"] = 0 @@ -655,7 +657,7 @@ class TestImage: im_remapped = im.remap_palette([1, 0]) assert "transparency" not in im_remapped.info - def test__new(self): + def test__new(self) -> None: im = hopper("RGB") im_p = hopper("P") @@ -664,7 +666,11 @@ class TestImage: blank_p.palette = None blank_pa.palette = None - def _make_new(base_image, image, palette_result=None): + def _make_new( + base_image: Image.Image, + image: Image.Image, + palette_result: ImagePalette.ImagePalette | None = None, + ) -> None: new_image = base_image._new(image.im) assert new_image.mode == image.mode assert new_image.size == image.size @@ -679,17 +685,20 @@ class TestImage: _make_new(im, blank_p, ImagePalette.ImagePalette()) _make_new(im, blank_pa, ImagePalette.ImagePalette()) - def test_p_from_rgb_rgba(self): - for mode, color in [ + @pytest.mark.parametrize( + "mode, color", + ( ("RGB", "#DDEEFF"), ("RGB", (221, 238, 255)), ("RGBA", (221, 238, 255, 255)), - ]: - im = Image.new("P", (100, 100), color) - expected = Image.new(mode, (100, 100), color) - assert_image_equal(im.convert(mode), expected) + ), + ) + def test_p_from_rgb_rgba(self, mode: str, color: str | tuple[int, ...]) -> None: + im = Image.new("P", (100, 100), color) + expected = Image.new(mode, (100, 100), color) + assert_image_equal(im.convert(mode), expected) - def test_no_resource_warning_on_save(self, tmp_path): + def test_no_resource_warning_on_save(self, tmp_path: Path) -> None: # https://github.com/python-pillow/Pillow/issues/835 # Arrange test_file = "Tests/images/hopper.png" @@ -700,7 +709,7 @@ class TestImage: with warnings.catch_warnings(): im.save(temp_file) - def test_no_new_file_on_error(self, tmp_path): + def test_no_new_file_on_error(self, tmp_path: Path) -> None: temp_file = str(tmp_path / "temp.jpg") im = Image.new("RGB", (0, 0)) @@ -709,10 +718,10 @@ class TestImage: assert not os.path.exists(temp_file) - def test_load_on_nonexclusive_multiframe(self): + def test_load_on_nonexclusive_multiframe(self) -> None: with open("Tests/images/frozenpond.mpo", "rb") as fp: - def act(fp): + def act(fp: IO[bytes]) -> None: im = Image.open(fp) im.load() @@ -723,7 +732,7 @@ class TestImage: assert not fp.closed - def test_empty_exif(self): + def test_empty_exif(self) -> None: with Image.open("Tests/images/exif.png") as im: exif = im.getexif() assert dict(exif) @@ -739,7 +748,7 @@ class TestImage: @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) - def test_exif_jpeg(self, tmp_path): + def test_exif_jpeg(self, tmp_path: Path) -> None: with Image.open("Tests/images/exif-72dpi-int.jpg") as im: # Little endian exif = im.getexif() assert 258 not in exif @@ -785,7 +794,7 @@ class TestImage: @skip_unless_feature("webp") @skip_unless_feature("webp_anim") - def test_exif_webp(self, tmp_path): + def test_exif_webp(self, tmp_path: Path) -> None: with Image.open("Tests/images/hopper.webp") as im: exif = im.getexif() assert exif == {} @@ -795,7 +804,7 @@ class TestImage: exif[40963] = 455 exif[305] = "Pillow test" - def check_exif(): + def check_exif() -> None: with Image.open(out) as reloaded: reloaded_exif = reloaded.getexif() assert reloaded_exif[258] == 8 @@ -807,7 +816,7 @@ class TestImage: im.save(out, exif=exif, save_all=True) check_exif() - def test_exif_png(self, tmp_path): + def test_exif_png(self, tmp_path: Path) -> None: with Image.open("Tests/images/exif.png") as im: exif = im.getexif() assert exif == {274: 1} @@ -823,7 +832,7 @@ class TestImage: reloaded_exif = reloaded.getexif() assert reloaded_exif == {258: 8, 40963: 455, 305: "Pillow test"} - def test_exif_interop(self): + def test_exif_interop(self) -> None: with Image.open("Tests/images/flower.jpg") as im: exif = im.getexif() assert exif.get_ifd(0xA005) == { @@ -837,7 +846,7 @@ class TestImage: reloaded_exif.load(exif.tobytes()) assert reloaded_exif.get_ifd(0xA005) == exif.get_ifd(0xA005) - def test_exif_ifd1(self): + def test_exif_ifd1(self) -> None: with Image.open("Tests/images/flower.jpg") as im: exif = im.getexif() assert exif.get_ifd(ExifTags.IFD.IFD1) == { @@ -849,7 +858,7 @@ class TestImage: 283: 180.0, } - def test_exif_ifd(self): + def test_exif_ifd(self) -> None: with Image.open("Tests/images/flower.jpg") as im: exif = im.getexif() del exif.get_ifd(0x8769)[0xA005] @@ -858,7 +867,7 @@ class TestImage: reloaded_exif.load(exif.tobytes()) assert reloaded_exif.get_ifd(0x8769) == exif.get_ifd(0x8769) - def test_exif_load_from_fp(self): + def test_exif_load_from_fp(self) -> None: with Image.open("Tests/images/flower.jpg") as im: data = im.info["exif"] if data.startswith(b"Exif\x00\x00"): @@ -879,7 +888,7 @@ class TestImage: 34665: 196, } - def test_exif_hide_offsets(self): + def test_exif_hide_offsets(self) -> None: with Image.open("Tests/images/flower.jpg") as im: exif = im.getexif() @@ -905,18 +914,18 @@ class TestImage: assert exif.get_ifd(0xA005) @pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0))) - def test_zero_tobytes(self, size): + def test_zero_tobytes(self, size: tuple[int, int]) -> None: im = Image.new("RGB", size) assert im.tobytes() == b"" @pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0))) - def test_zero_frombytes(self, size): + def test_zero_frombytes(self, size: tuple[int, int]) -> None: Image.frombytes("RGB", size, b"") im = Image.new("RGB", size) im.frombytes(b"") - def test_has_transparency_data(self): + def test_has_transparency_data(self) -> None: for mode in ("1", "L", "P", "RGB"): im = Image.new(mode, (1, 1)) assert not im.has_transparency_data @@ -941,7 +950,7 @@ class TestImage: assert im.palette.mode == "RGBA" assert im.has_transparency_data - def test_apply_transparency(self): + def test_apply_transparency(self) -> None: im = Image.new("P", (1, 1)) im.putpalette((0, 0, 0, 1, 1, 1)) assert im.palette.colors == {(0, 0, 0): 0, (1, 1, 1): 1} @@ -970,7 +979,7 @@ class TestImage: im.apply_transparency() assert im.palette.colors[(27, 35, 6, 214)] == 24 - def test_constants(self): + def test_constants(self) -> None: for enum in ( Image.Transpose, Image.Transform, @@ -995,7 +1004,7 @@ class TestImage: "01r_00.pcx", ], ) - def test_overrun(self, path): + def test_overrun(self, path: str) -> None: """For overrun completeness, test as: valgrind pytest -qq Tests/test_image.py::TestImage::test_overrun | grep decode.c """ @@ -1009,7 +1018,7 @@ class TestImage: assert buffer_overrun or truncated - def test_fli_overrun2(self): + def test_fli_overrun2(self) -> None: with Image.open("Tests/images/fli_overrun2.bin") as im: try: im.seek(1) @@ -1017,12 +1026,12 @@ class TestImage: except OSError as e: assert str(e) == "buffer overrun when reading image file" - def test_exit_fp(self): + def test_exit_fp(self) -> None: with Image.new("L", (1, 1)) as im: pass assert not hasattr(im, "fp") - def test_close_graceful(self, caplog): + def test_close_graceful(self, caplog: pytest.LogCaptureFixture) -> None: with Image.open("Tests/images/hopper.jpg") as im: copy = im.copy() with caplog.at_level(logging.DEBUG): @@ -1033,17 +1042,17 @@ class TestImage: class MockEncoder: - pass + args: tuple[str, ...] -def mock_encode(*args): +def mock_encode(*args: str) -> MockEncoder: encoder = MockEncoder() encoder.args = args return encoder class TestRegistry: - def test_encode_registry(self): + def test_encode_registry(self) -> None: Image.register_encoder("MOCK", mock_encode) assert "MOCK" in Image.ENCODERS @@ -1052,6 +1061,6 @@ class TestRegistry: assert isinstance(enc, MockEncoder) assert enc.args == ("RGB", "args", "extra") - def test_encode_registry_fail(self): + def test_encode_registry_fail(self) -> None: with pytest.raises(OSError): Image._getencoder("RGB", "DoesNotExist", ("args",), extra=("extra",)) diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py index 4ae56fae0..8c42da57a 100644 --- a/Tests/test_image_access.py +++ b/Tests/test_image_access.py @@ -4,6 +4,7 @@ import os import subprocess import sys import sysconfig +from types import ModuleType import pytest @@ -13,6 +14,7 @@ from .helper import assert_image_equal, hopper, is_win32 # CFFI imports pycparser which doesn't support PYTHONOPTIMIZE=2 # https://github.com/eliben/pycparser/pull/198#issuecomment-317001670 +cffi: ModuleType | None if os.environ.get("PYTHONOPTIMIZE") == "2": cffi = None else: @@ -23,6 +25,7 @@ else: except ImportError: cffi = None +numpy: ModuleType | None try: import numpy except ImportError: @@ -35,16 +38,16 @@ class AccessTest: _need_cffi_access = False @classmethod - def setup_class(cls): + def setup_class(cls) -> None: Image.USE_CFFI_ACCESS = cls._need_cffi_access @classmethod - def teardown_class(cls): + def teardown_class(cls) -> None: Image.USE_CFFI_ACCESS = cls._init_cffi_access class TestImagePutPixel(AccessTest): - def test_sanity(self): + def test_sanity(self) -> None: im1 = hopper() im2 = Image.new(im1.mode, im1.size, 0) @@ -71,9 +74,10 @@ class TestImagePutPixel(AccessTest): pix1 = im1.load() pix2 = im2.load() - for x, y in ((0, "0"), ("0", 0)): - with pytest.raises(TypeError): - pix1[x, y] + with pytest.raises(TypeError): + pix1[0, "0"] + with pytest.raises(TypeError): + pix1["0", 0] for y in range(im1.size[1]): for x in range(im1.size[0]): @@ -81,7 +85,7 @@ class TestImagePutPixel(AccessTest): assert_image_equal(im1, im2) - def test_sanity_negative_index(self): + def test_sanity_negative_index(self) -> None: im1 = hopper() im2 = Image.new(im1.mode, im1.size, 0) @@ -119,16 +123,17 @@ class TestImagePutPixel(AccessTest): assert_image_equal(im1, im2) @pytest.mark.skipif(numpy is None, reason="NumPy not installed") - def test_numpy(self): + def test_numpy(self) -> None: im = hopper() pix = im.load() + assert numpy is not None assert pix[numpy.int32(1), numpy.int32(2)] == (18, 20, 59) class TestImageGetPixel(AccessTest): @staticmethod - def color(mode): + def color(mode: str) -> int | tuple[int, ...]: bands = Image.getmodebands(mode) if bands == 1: return 1 @@ -138,12 +143,13 @@ class TestImageGetPixel(AccessTest): return (16, 32, 49) return tuple(range(1, bands + 1)) - def check(self, mode, expected_color=None): + def check(self, mode: str, expected_color_int: int | None = None) -> None: if self._need_cffi_access and mode.startswith("BGR;"): pytest.skip("Support not added to deprecated module for BGR;* modes") - if not expected_color: - expected_color = self.color(mode) + expected_color = ( + self.color(mode) if expected_color_int is None else expected_color_int + ) # check putpixel im = Image.new(mode, (1, 1), None) @@ -222,25 +228,23 @@ class TestImageGetPixel(AccessTest): "YCbCr", ), ) - def test_basic(self, mode): + def test_basic(self, mode: str) -> None: self.check(mode) - def test_list(self): + def test_list(self) -> None: im = hopper() assert im.getpixel([0, 0]) == (20, 20, 70) @pytest.mark.parametrize("mode", ("I;16", "I;16B")) - @pytest.mark.parametrize( - "expected_color", (2**15 - 1, 2**15, 2**15 + 1, 2**16 - 1) - ) - def test_signedness(self, mode, expected_color): + @pytest.mark.parametrize("expected_color", (2**15 - 1, 2**15, 2**15 + 1, 2**16 - 1)) + def test_signedness(self, mode: str, expected_color: int) -> None: # see https://github.com/python-pillow/Pillow/issues/452 # pixelaccess is using signed int* instead of uint* self.check(mode, expected_color) @pytest.mark.parametrize("mode", ("P", "PA")) @pytest.mark.parametrize("color", ((255, 0, 0), (255, 0, 0, 255))) - def test_p_putpixel_rgb_rgba(self, mode, color): + def test_p_putpixel_rgb_rgba(self, mode: str, color: tuple[int, ...]) -> None: im = Image.new(mode, (1, 1)) im.putpixel((0, 0), color) @@ -264,7 +268,7 @@ class TestCffiGetPixel(TestImageGetPixel): class TestCffi(AccessTest): _need_cffi_access = True - def _test_get_access(self, im): + def _test_get_access(self, im: Image.Image) -> None: """Do we get the same thing as the old pixel access Using private interfaces, forcing a capi access and @@ -282,7 +286,7 @@ class TestCffi(AccessTest): with pytest.raises(ValueError): access[(access.xsize + 1, access.ysize + 1)] - def test_get_vs_c(self): + def test_get_vs_c(self) -> None: with pytest.warns(DeprecationWarning): rgb = hopper("RGB") rgb.load() @@ -301,7 +305,7 @@ class TestCffi(AccessTest): # im = Image.new('I;32B', (10, 10), 2**10) # self._test_get_access(im) - def _test_set_access(self, im, color): + def _test_set_access(self, im: Image.Image, color: tuple[int, ...] | float) -> None: """Are we writing the correct bits into the image? Using private interfaces, forcing a capi access and @@ -322,7 +326,7 @@ class TestCffi(AccessTest): with pytest.raises(ValueError): access[(0, 0)] = color - def test_set_vs_c(self): + def test_set_vs_c(self) -> None: rgb = hopper("RGB") with pytest.warns(DeprecationWarning): rgb.load() @@ -345,11 +349,11 @@ class TestCffi(AccessTest): # self._test_set_access(im, 2**13-1) @pytest.mark.filterwarnings("ignore::DeprecationWarning") - def test_not_implemented(self): + def test_not_implemented(self) -> None: assert PyAccess.new(hopper("BGR;15")) is None # ref https://github.com/python-pillow/Pillow/pull/2009 - def test_reference_counting(self): + def test_reference_counting(self) -> None: size = 10 for _ in range(10): @@ -361,7 +365,7 @@ class TestCffi(AccessTest): assert px[i, 0] == 0 @pytest.mark.parametrize("mode", ("P", "PA")) - def test_p_putpixel_rgb_rgba(self, mode): + def test_p_putpixel_rgb_rgba(self, mode: str) -> None: for color in ((255, 0, 0), (255, 0, 0, 127 if mode == "PA" else 255)): im = Image.new(mode, (1, 1)) with pytest.warns(DeprecationWarning): @@ -379,7 +383,7 @@ class TestImagePutPixelError(AccessTest): INVALID_TYPES = ["foo", 1.0, None] @pytest.mark.parametrize("mode", IMAGE_MODES1) - def test_putpixel_type_error1(self, mode): + def test_putpixel_type_error1(self, mode: str) -> None: im = hopper(mode) for v in self.INVALID_TYPES: with pytest.raises(TypeError, match="color must be int or tuple"): @@ -402,14 +406,16 @@ class TestImagePutPixelError(AccessTest): ), ), ) - def test_putpixel_invalid_number_of_bands(self, mode, band_numbers, match): + def test_putpixel_invalid_number_of_bands( + self, mode: str, band_numbers: tuple[int, ...], match: str + ) -> None: im = hopper(mode) for band_number in band_numbers: with pytest.raises(TypeError, match=match): im.putpixel((0, 0), (0,) * band_number) @pytest.mark.parametrize("mode", IMAGE_MODES2) - def test_putpixel_type_error2(self, mode): + def test_putpixel_type_error2(self, mode: str) -> None: im = hopper(mode) for v in self.INVALID_TYPES: with pytest.raises( @@ -418,7 +424,7 @@ class TestImagePutPixelError(AccessTest): im.putpixel((0, 0), v) @pytest.mark.parametrize("mode", IMAGE_MODES1 + IMAGE_MODES2) - def test_putpixel_overflow_error(self, mode): + def test_putpixel_overflow_error(self, mode: str) -> None: im = hopper(mode) with pytest.raises(OverflowError): im.putpixel((0, 0), 2**80) @@ -427,10 +433,10 @@ class TestImagePutPixelError(AccessTest): class TestEmbeddable: @pytest.mark.xfail(reason="failing test") @pytest.mark.skipif(not is_win32(), reason="requires Windows") - def test_embeddable(self): + def test_embeddable(self) -> None: import ctypes - from setuptools.command.build_ext import new_compiler + from setuptools.command import build_ext with open("embed_pil.c", "w", encoding="utf-8") as fh: fh.write( @@ -459,7 +465,7 @@ int main(int argc, char* argv[]) % sys.prefix.replace("\\", "\\\\") ) - compiler = new_compiler() + compiler = getattr(build_ext, "new_compiler")() compiler.add_include_dir(sysconfig.get_config_var("INCLUDEPY")) libdir = sysconfig.get_config_var("LIBDIR") or sysconfig.get_config_var( @@ -473,7 +479,7 @@ int main(int argc, char* argv[]) env["PATH"] = sys.prefix + ";" + env["PATH"] # do not display the Windows Error Reporting dialog - ctypes.windll.kernel32.SetErrorMode(0x0002) + getattr(ctypes, "windll").kernel32.SetErrorMode(0x0002) process = subprocess.Popen(["embed_pil.exe"], env=env) process.communicate() diff --git a/Tests/test_image_array.py b/Tests/test_image_array.py index 0dacb3157..cf85ee4fa 100644 --- a/Tests/test_image_array.py +++ b/Tests/test_image_array.py @@ -1,5 +1,7 @@ from __future__ import annotations +from typing import Any + import pytest from packaging.version import parse as parse_version @@ -12,12 +14,12 @@ numpy = pytest.importorskip("numpy", reason="NumPy not installed") im = hopper().resize((128, 100)) -def test_toarray(): - def test(mode): +def test_toarray() -> None: + def test(mode: str) -> tuple[tuple[int, ...], str, int]: ai = numpy.array(im.convert(mode)) return ai.shape, ai.dtype.str, ai.nbytes - def test_with_dtype(dtype): + def test_with_dtype(dtype) -> None: ai = numpy.array(im, dtype=dtype) assert ai.dtype == dtype @@ -46,18 +48,18 @@ def test_toarray(): numpy.array(im_truncated) -def test_fromarray(): +def test_fromarray() -> None: class Wrapper: """Class with API matching Image.fromarray""" - def __init__(self, img, arr_params): + def __init__(self, img: Image.Image, arr_params: dict[str, Any]) -> None: self.img = img self.__array_interface__ = arr_params - def tobytes(self): + def tobytes(self) -> bytes: return self.img.tobytes() - def test(mode): + def test(mode: str) -> tuple[str, tuple[int, int], bool]: i = im.convert(mode) a = numpy.array(i) # Make wrapper instance for image, new array interface @@ -89,7 +91,7 @@ def test_fromarray(): Image.fromarray(wrapped) -def test_fromarray_palette(): +def test_fromarray_palette() -> None: # Arrange i = im.convert("L") a = numpy.array(i) diff --git a/Tests/test_image_convert.py b/Tests/test_image_convert.py index d4ddc2a31..f154de123 100644 --- a/Tests/test_image_convert.py +++ b/Tests/test_image_convert.py @@ -1,5 +1,7 @@ from __future__ import annotations +from pathlib import Path + import pytest from PIL import Image @@ -7,8 +9,8 @@ from PIL import Image from .helper import assert_image, assert_image_equal, assert_image_similar, hopper -def test_sanity(): - def convert(im, mode): +def test_sanity() -> None: + def convert(im: Image.Image, mode: str) -> None: out = im.convert(mode) assert out.mode == mode assert out.size == im.size @@ -40,13 +42,13 @@ def test_sanity(): convert(im, output_mode) -def test_unsupported_conversion(): +def test_unsupported_conversion() -> None: im = hopper() with pytest.raises(ValueError): im.convert("INVALID") -def test_default(): +def test_default() -> None: im = hopper("P") assert im.mode == "P" converted_im = im.convert() @@ -62,18 +64,18 @@ def test_default(): # ref https://github.com/python-pillow/Pillow/issues/274 -def _test_float_conversion(im): +def _test_float_conversion(im: Image.Image) -> None: orig = im.getpixel((5, 5)) converted = im.convert("F").getpixel((5, 5)) assert orig == converted -def test_8bit(): +def test_8bit() -> None: with Image.open("Tests/images/hopper.jpg") as im: _test_float_conversion(im.convert("L")) -def test_16bit(): +def test_16bit() -> None: with Image.open("Tests/images/16bit.cropped.tif") as im: _test_float_conversion(im) @@ -83,19 +85,19 @@ def test_16bit(): assert im_i16.getpixel((0, 0)) == 65535 -def test_16bit_workaround(): +def test_16bit_workaround() -> None: with Image.open("Tests/images/16bit.cropped.tif") as im: _test_float_conversion(im.convert("I")) -def test_opaque(): +def test_opaque() -> None: alpha = hopper("P").convert("PA").getchannel("A") solid = Image.new("L", (128, 128), 255) assert_image_equal(alpha, solid) -def test_rgba_p(): +def test_rgba_p() -> None: im = hopper("RGBA") im.putalpha(hopper("L")) @@ -105,14 +107,14 @@ def test_rgba_p(): assert_image_similar(im, comparable, 20) -def test_rgba(): +def test_rgba() -> None: with Image.open("Tests/images/transparent.png") as im: assert im.mode == "RGBA" assert_image_similar(im.convert("RGBa").convert("RGB"), im.convert("RGB"), 1.5) -def test_trns_p(tmp_path): +def test_trns_p(tmp_path: Path) -> None: im = hopper("P") im.info["transparency"] = 0 @@ -131,7 +133,7 @@ def test_trns_p(tmp_path): @pytest.mark.parametrize("mode", ("LA", "PA", "RGBA")) -def test_trns_p_transparency(mode): +def test_trns_p_transparency(mode: str) -> None: # Arrange im = hopper("P") im.info["transparency"] = 128 @@ -148,7 +150,7 @@ def test_trns_p_transparency(mode): assert converted_im.palette is None -def test_trns_l(tmp_path): +def test_trns_l(tmp_path: Path) -> None: im = hopper("L") im.info["transparency"] = 128 @@ -171,7 +173,7 @@ def test_trns_l(tmp_path): im_p.save(f) -def test_trns_RGB(tmp_path): +def test_trns_RGB(tmp_path: Path) -> None: im = hopper("RGB") im.info["transparency"] = im.getpixel((0, 0)) @@ -201,7 +203,7 @@ def test_trns_RGB(tmp_path): @pytest.mark.parametrize("convert_mode", ("L", "LA", "I")) -def test_l_macro_rounding(convert_mode): +def test_l_macro_rounding(convert_mode: str) -> None: for mode in ("P", "PA"): im = Image.new(mode, (1, 1)) im.palette.getcolor((0, 1, 2)) @@ -214,7 +216,7 @@ def test_l_macro_rounding(convert_mode): assert converted_color == 1 -def test_gif_with_rgba_palette_to_p(): +def test_gif_with_rgba_palette_to_p() -> None: # See https://github.com/python-pillow/Pillow/issues/2433 with Image.open("Tests/images/hopper.gif") as im: im.info["transparency"] = 255 @@ -226,7 +228,7 @@ def test_gif_with_rgba_palette_to_p(): im_p.load() -def test_p_la(): +def test_p_la() -> None: im = hopper("RGBA") alpha = hopper("L") im.putalpha(alpha) @@ -236,7 +238,7 @@ def test_p_la(): assert_image_similar(alpha, comparable, 5) -def test_p2pa_alpha(): +def test_p2pa_alpha() -> None: with Image.open("Tests/images/tiny.png") as im: assert im.mode == "P" @@ -250,13 +252,13 @@ def test_p2pa_alpha(): assert im_a.getpixel((x, y)) == alpha -def test_p2pa_palette(): +def test_p2pa_palette() -> None: with Image.open("Tests/images/tiny.png") as im: im_pa = im.convert("PA") assert im_pa.getpalette() == im.getpalette() -def test_matrix_illegal_conversion(): +def test_matrix_illegal_conversion() -> None: # Arrange im = hopper("CMYK") # fmt: off @@ -272,7 +274,7 @@ def test_matrix_illegal_conversion(): im.convert(mode="CMYK", matrix=matrix) -def test_matrix_wrong_mode(): +def test_matrix_wrong_mode() -> None: # Arrange im = hopper("L") # fmt: off @@ -289,7 +291,7 @@ def test_matrix_wrong_mode(): @pytest.mark.parametrize("mode", ("RGB", "L")) -def test_matrix_xyz(mode): +def test_matrix_xyz(mode: str) -> None: # Arrange im = hopper("RGB") im.info["transparency"] = (255, 0, 0) @@ -317,7 +319,7 @@ def test_matrix_xyz(mode): assert converted_im.info["transparency"] == 105 -def test_matrix_identity(): +def test_matrix_identity() -> None: # Arrange im = hopper("RGB") # fmt: off diff --git a/Tests/test_image_copy.py b/Tests/test_image_copy.py index 3a26ef96e..027e5338b 100644 --- a/Tests/test_image_copy.py +++ b/Tests/test_image_copy.py @@ -10,7 +10,7 @@ from .helper import hopper, skip_unless_feature @pytest.mark.parametrize("mode", ("1", "P", "L", "RGB", "I", "F")) -def test_copy(mode): +def test_copy(mode: str) -> None: cropped_coordinates = (10, 10, 20, 20) cropped_size = (10, 10) @@ -39,7 +39,7 @@ def test_copy(mode): assert out.size == cropped_size -def test_copy_zero(): +def test_copy_zero() -> None: im = Image.new("RGB", (0, 0)) out = im.copy() assert out.mode == im.mode @@ -47,7 +47,7 @@ def test_copy_zero(): @skip_unless_feature("libtiff") -def test_deepcopy(): +def test_deepcopy() -> None: with Image.open("Tests/images/g4_orientation_5.tif") as im: out = copy.deepcopy(im) assert out.size == (590, 88) diff --git a/Tests/test_image_crop.py b/Tests/test_image_crop.py index 5e02a3b0d..d095364ba 100644 --- a/Tests/test_image_crop.py +++ b/Tests/test_image_crop.py @@ -8,7 +8,7 @@ from .helper import assert_image_equal, hopper @pytest.mark.parametrize("mode", ("1", "P", "L", "RGB", "I", "F")) -def test_crop(mode): +def test_crop(mode: str) -> None: im = hopper(mode) assert_image_equal(im.crop(), im) @@ -17,8 +17,8 @@ def test_crop(mode): assert cropped.size == (50, 50) -def test_wide_crop(): - def crop(*bbox): +def test_wide_crop() -> None: + def crop(*bbox: int) -> tuple[int, ...]: i = im.crop(bbox) h = i.histogram() while h and not h[-1]: @@ -47,14 +47,14 @@ def test_wide_crop(): @pytest.mark.parametrize("box", ((8, 2, 2, 8), (2, 8, 8, 2), (8, 8, 2, 2))) -def test_negative_crop(box): +def test_negative_crop(box: tuple[int, int, int, int]) -> None: im = Image.new("RGB", (10, 10)) with pytest.raises(ValueError): im.crop(box) -def test_crop_float(): +def test_crop_float() -> None: # Check cropping floats are rounded to nearest integer # https://github.com/python-pillow/Pillow/issues/1744 @@ -69,7 +69,7 @@ def test_crop_float(): assert cropped.size == (3, 5) -def test_crop_crash(): +def test_crop_crash() -> None: # Image.crop crashes prepatch with an access violation # apparently a use after free on Windows, see # https://github.com/python-pillow/Pillow/issues/1077 @@ -87,7 +87,7 @@ def test_crop_crash(): img.load() -def test_crop_zero(): +def test_crop_zero() -> None: im = Image.new("RGB", (0, 0), "white") cropped = im.crop((0, 0, 0, 0)) diff --git a/Tests/test_image_draft.py b/Tests/test_image_draft.py index 08c40af1f..1ce1a7cd8 100644 --- a/Tests/test_image_draft.py +++ b/Tests/test_image_draft.py @@ -7,7 +7,12 @@ from .helper import fromstring, skip_unless_feature, tostring pytestmark = skip_unless_feature("jpg") -def draft_roundtrip(in_mode, in_size, req_mode, req_size): +def draft_roundtrip( + in_mode: str, + in_size: tuple[int, int], + req_mode: str | None, + req_size: tuple[int, int] | None, +) -> Image.Image: im = Image.new(in_mode, in_size) data = tostring(im, "JPEG") im = fromstring(data) @@ -19,7 +24,7 @@ def draft_roundtrip(in_mode, in_size, req_mode, req_size): return im -def test_size(): +def test_size() -> None: for in_size, req_size, out_size in [ ((435, 361), (2048, 2048), (435, 361)), # bigger ((435, 361), (435, 361), (435, 361)), # same @@ -48,7 +53,7 @@ def test_size(): assert im.size == out_size -def test_mode(): +def test_mode() -> None: for in_mode, req_mode, out_mode in [ ("RGB", "1", "RGB"), ("RGB", "L", "L"), @@ -68,7 +73,7 @@ def test_mode(): assert im.mode == out_mode -def test_several_drafts(): +def test_several_drafts() -> None: im = draft_roundtrip("L", (128, 128), None, (64, 64)) im.draft(None, (64, 64)) im.load() diff --git a/Tests/test_image_entropy.py b/Tests/test_image_entropy.py index fce161224..c1dbb879b 100644 --- a/Tests/test_image_entropy.py +++ b/Tests/test_image_entropy.py @@ -3,8 +3,8 @@ from __future__ import annotations from .helper import hopper -def test_entropy(): - def entropy(mode): +def test_entropy() -> None: + def entropy(mode: str) -> float: return hopper(mode).entropy() assert round(abs(entropy("1") - 0.9138803254693582), 7) == 0 diff --git a/Tests/test_image_filter.py b/Tests/test_image_filter.py index 3fa5dd242..47f9ffa3d 100644 --- a/Tests/test_image_filter.py +++ b/Tests/test_image_filter.py @@ -36,7 +36,7 @@ from .helper import assert_image_equal, hopper ), ) @pytest.mark.parametrize("mode", ("L", "I", "RGB", "CMYK")) -def test_sanity(filter_to_apply, mode): +def test_sanity(filter_to_apply: ImageFilter.Filter, mode: str) -> None: im = hopper(mode) if mode != "I" or isinstance(filter_to_apply, ImageFilter.BuiltinFilter): out = im.filter(filter_to_apply) @@ -45,7 +45,7 @@ def test_sanity(filter_to_apply, mode): @pytest.mark.parametrize("mode", ("L", "I", "RGB", "CMYK")) -def test_sanity_error(mode): +def test_sanity_error(mode: str) -> None: with pytest.raises(TypeError): im = hopper(mode) im.filter("hello") @@ -53,7 +53,7 @@ def test_sanity_error(mode): # crashes on small images @pytest.mark.parametrize("size", ((1, 1), (2, 2), (3, 3))) -def test_crash(size): +def test_crash(size: tuple[int, int]) -> None: im = Image.new("RGB", size) im.filter(ImageFilter.SMOOTH) @@ -67,7 +67,10 @@ def test_crash(size): ("RGB", ((4, 0, 0), (0, 0, 0))), ), ) -def test_modefilter(mode, expected): +def test_modefilter( + mode: str, + expected: tuple[int, int] | tuple[tuple[int, int, int], tuple[int, int, int]], +) -> None: im = Image.new(mode, (3, 3), None) im.putdata(list(range(9))) # image is: @@ -90,7 +93,13 @@ def test_modefilter(mode, expected): ("F", (0.0, 4.0, 8.0)), ), ) -def test_rankfilter(mode, expected): +def test_rankfilter( + mode: str, + expected: ( + tuple[float, float, float] + | tuple[tuple[int, int, int], tuple[int, int, int], tuple[int, int, int]] + ), +) -> None: im = Image.new(mode, (3, 3), None) im.putdata(list(range(9))) # image is: @@ -106,7 +115,7 @@ def test_rankfilter(mode, expected): @pytest.mark.parametrize( "filter", (ImageFilter.MinFilter, ImageFilter.MedianFilter, ImageFilter.MaxFilter) ) -def test_rankfilter_error(filter): +def test_rankfilter_error(filter: ImageFilter.RankFilter) -> None: with pytest.raises(ValueError): im = Image.new("P", (3, 3), None) im.putdata(list(range(9))) @@ -117,31 +126,29 @@ def test_rankfilter_error(filter): im.filter(filter).getpixel((1, 1)) -def test_rankfilter_properties(): +def test_rankfilter_properties() -> None: rankfilter = ImageFilter.RankFilter(1, 2) assert rankfilter.size == 1 assert rankfilter.rank == 2 -def test_builtinfilter_p(): +def test_builtinfilter_p() -> None: builtin_filter = ImageFilter.BuiltinFilter() with pytest.raises(ValueError): builtin_filter.filter(hopper("P")) -def test_kernel_not_enough_coefficients(): +def test_kernel_not_enough_coefficients() -> None: with pytest.raises(ValueError): ImageFilter.Kernel((3, 3), (0, 0)) @pytest.mark.parametrize("mode", ("L", "LA", "I", "RGB", "CMYK")) -def test_consistency_3x3(mode): +def test_consistency_3x3(mode: str) -> None: with Image.open("Tests/images/hopper.bmp") as source: - reference_name = "hopper_emboss" - reference_name += "_I.png" if mode == "I" else ".bmp" - with Image.open("Tests/images/" + reference_name) as reference: + with Image.open("Tests/images/hopper_emboss.bmp") as reference: kernel = ImageFilter.Kernel( (3, 3), # fmt: off @@ -151,23 +158,13 @@ def test_consistency_3x3(mode): # fmt: on 0.3, ) - source = source.split() * 2 - reference = reference.split() * 2 - - if mode == "I": - source = source[0].convert(mode) - else: - source = Image.merge(mode, source[: len(mode)]) - reference = Image.merge(mode, reference[: len(mode)]) assert_image_equal(source.filter(kernel), reference) @pytest.mark.parametrize("mode", ("L", "LA", "I", "RGB", "CMYK")) -def test_consistency_5x5(mode): +def test_consistency_5x5(mode: str) -> None: with Image.open("Tests/images/hopper.bmp") as source: - reference_name = "hopper_emboss_more" - reference_name += "_I.png" if mode == "I" else ".bmp" - with Image.open("Tests/images/" + reference_name) as reference: + with Image.open("Tests/images/hopper_emboss_more.bmp") as reference: kernel = ImageFilter.Kernel( (5, 5), # fmt: off @@ -179,14 +176,6 @@ def test_consistency_5x5(mode): # fmt: on 0.3, ) - source = source.split() * 2 - reference = reference.split() * 2 - - if mode == "I": - source = source[0].convert(mode) - else: - source = Image.merge(mode, source[: len(mode)]) - reference = Image.merge(mode, reference[: len(mode)]) assert_image_equal(source.filter(kernel), reference) @@ -199,7 +188,7 @@ def test_consistency_5x5(mode): (2, -2), ), ) -def test_invalid_box_blur_filter(radius): +def test_invalid_box_blur_filter(radius: int | tuple[int, int]) -> None: with pytest.raises(ValueError): ImageFilter.BoxBlur(radius) diff --git a/Tests/test_image_frombytes.py b/Tests/test_image_frombytes.py index 5d5e9f2df..98c0ea0b4 100644 --- a/Tests/test_image_frombytes.py +++ b/Tests/test_image_frombytes.py @@ -8,7 +8,7 @@ from .helper import assert_image_equal, hopper @pytest.mark.parametrize("data_type", ("bytes", "memoryview")) -def test_sanity(data_type): +def test_sanity(data_type: str) -> None: im1 = hopper() data = im1.tobytes() diff --git a/Tests/test_image_fromqimage.py b/Tests/test_image_fromqimage.py index 76b576da5..c20123a1b 100644 --- a/Tests/test_image_fromqimage.py +++ b/Tests/test_image_fromqimage.py @@ -16,22 +16,19 @@ pytestmark = pytest.mark.skipif( not ImageQt.qt_is_installed, reason="Qt bindings are not installed" ) - -@pytest.fixture -def test_images(): - ims = [ - hopper(), - Image.open("Tests/images/transparent.png"), - Image.open("Tests/images/7x13.png"), - ] - try: - yield ims - finally: - for im in ims: - im.close() +ims = [ + hopper(), + Image.open("Tests/images/transparent.png"), + Image.open("Tests/images/7x13.png"), +] -def roundtrip(expected): +def teardown_module() -> None: + for im in ims: + im.close() + + +def roundtrip(expected: Image.Image) -> None: # PIL -> Qt intermediate = expected.toqimage() # Qt -> PIL @@ -43,26 +40,26 @@ def roundtrip(expected): assert_image_equal(result, expected.convert("RGB")) -def test_sanity_1(test_images): - for im in test_images: +def test_sanity_1() -> None: + for im in ims: roundtrip(im.convert("1")) -def test_sanity_rgb(test_images): - for im in test_images: +def test_sanity_rgb() -> None: + for im in ims: roundtrip(im.convert("RGB")) -def test_sanity_rgba(test_images): - for im in test_images: +def test_sanity_rgba() -> None: + for im in ims: roundtrip(im.convert("RGBA")) -def test_sanity_l(test_images): - for im in test_images: +def test_sanity_l() -> None: + for im in ims: roundtrip(im.convert("L")) -def test_sanity_p(test_images): - for im in test_images: +def test_sanity_p() -> None: + for im in ims: roundtrip(im.convert("P")) diff --git a/Tests/test_image_getbands.py b/Tests/test_image_getbands.py index 64339e2cd..887553fc0 100644 --- a/Tests/test_image_getbands.py +++ b/Tests/test_image_getbands.py @@ -3,7 +3,7 @@ from __future__ import annotations from PIL import Image -def test_getbands(): +def test_getbands() -> None: assert Image.new("1", (1, 1)).getbands() == ("1",) assert Image.new("L", (1, 1)).getbands() == ("L",) assert Image.new("I", (1, 1)).getbands() == ("I",) diff --git a/Tests/test_image_getbbox.py b/Tests/test_image_getbbox.py index b18a7202e..18c6f6579 100644 --- a/Tests/test_image_getbbox.py +++ b/Tests/test_image_getbbox.py @@ -7,13 +7,13 @@ from PIL import Image from .helper import hopper -def test_sanity(): +def test_sanity() -> None: bbox = hopper().getbbox() assert isinstance(bbox, tuple) -def test_bbox(): - def check(im, fill_color): +def test_bbox() -> None: + def check(im: Image.Image, fill_color: int | tuple[int, ...]) -> None: assert im.getbbox() is None im.paste(fill_color, (10, 25, 90, 75)) @@ -34,8 +34,8 @@ def test_bbox(): check(im, 255) for mode in ("RGBA", "RGBa"): - for color in ((0, 0, 0, 0), (127, 127, 127, 0), (255, 255, 255, 0)): - im = Image.new(mode, (100, 100), color) + for rgba_color in ((0, 0, 0, 0), (127, 127, 127, 0), (255, 255, 255, 0)): + im = Image.new(mode, (100, 100), rgba_color) check(im, (255, 255, 255, 255)) for mode in ("La", "LA", "PA"): @@ -45,7 +45,7 @@ def test_bbox(): @pytest.mark.parametrize("mode", ("RGBA", "RGBa", "La", "LA", "PA")) -def test_bbox_alpha_only_false(mode): +def test_bbox_alpha_only_false(mode: str) -> None: im = Image.new(mode, (100, 100)) assert im.getbbox(alpha_only=False) is None diff --git a/Tests/test_image_getcolors.py b/Tests/test_image_getcolors.py index 17460fa93..8f8870f4f 100644 --- a/Tests/test_image_getcolors.py +++ b/Tests/test_image_getcolors.py @@ -3,8 +3,8 @@ from __future__ import annotations from .helper import hopper -def test_getcolors(): - def getcolors(mode, limit=None): +def test_getcolors() -> None: + def getcolors(mode: str, limit: int | None = None) -> int | None: im = hopper(mode) if limit: colors = im.getcolors(limit) @@ -39,7 +39,7 @@ def test_getcolors(): # -------------------------------------------------------------------- -def test_pack(): +def test_pack() -> None: # Pack problems for small tables (@PIL209) im = hopper().quantize(3).convert("RGB") diff --git a/Tests/test_image_getdata.py b/Tests/test_image_getdata.py index ace64279b..ac27400be 100644 --- a/Tests/test_image_getdata.py +++ b/Tests/test_image_getdata.py @@ -5,7 +5,7 @@ from PIL import Image from .helper import hopper -def test_sanity(): +def test_sanity() -> None: data = hopper().getdata() len(data) @@ -14,8 +14,8 @@ def test_sanity(): assert data[0] == (20, 20, 70) -def test_roundtrip(): - def getdata(mode): +def test_roundtrip() -> None: + def getdata(mode: str) -> tuple[float | tuple[int, ...], int, int]: im = hopper(mode).resize((32, 30), Image.Resampling.NEAREST) data = im.getdata() return data[0], len(data), len(list(data)) diff --git a/Tests/test_image_getextrema.py b/Tests/test_image_getextrema.py index 6bbc4da9a..a5b974459 100644 --- a/Tests/test_image_getextrema.py +++ b/Tests/test_image_getextrema.py @@ -5,8 +5,8 @@ from PIL import Image from .helper import hopper -def test_extrema(): - def extrema(mode): +def test_extrema() -> None: + def extrema(mode: str) -> tuple[int, int] | tuple[tuple[int, int], ...]: return hopper(mode).getextrema() assert extrema("1") == (0, 255) @@ -20,7 +20,7 @@ def test_extrema(): assert extrema("I;16") == (1, 255) -def test_true_16(): +def test_true_16() -> None: with Image.open("Tests/images/16_bit_noise.tif") as im: assert im.mode == "I;16" extrema = im.getextrema() diff --git a/Tests/test_image_getim.py b/Tests/test_image_getim.py index bc8a7485e..9afa02b0a 100644 --- a/Tests/test_image_getim.py +++ b/Tests/test_image_getim.py @@ -3,7 +3,7 @@ from __future__ import annotations from .helper import hopper -def test_sanity(): +def test_sanity() -> None: im = hopper() type_repr = repr(type(im.getim())) diff --git a/Tests/test_image_getpalette.py b/Tests/test_image_getpalette.py index 4340f46f6..6a8f157fc 100644 --- a/Tests/test_image_getpalette.py +++ b/Tests/test_image_getpalette.py @@ -5,8 +5,8 @@ from PIL import Image from .helper import hopper -def test_palette(): - def palette(mode): +def test_palette() -> None: + def palette(mode: str) -> list[int] | None: p = hopper(mode).getpalette() if p: return p[:10] @@ -23,7 +23,7 @@ def test_palette(): assert palette("YCbCr") is None -def test_palette_rawmode(): +def test_palette_rawmode() -> None: im = Image.new("P", (1, 1)) im.putpalette((1, 2, 3)) diff --git a/Tests/test_image_getprojection.py b/Tests/test_image_getprojection.py index e90f5f505..2b5a758ed 100644 --- a/Tests/test_image_getprojection.py +++ b/Tests/test_image_getprojection.py @@ -5,7 +5,7 @@ from PIL import Image from .helper import hopper -def test_sanity(): +def test_sanity() -> None: im = hopper() projection = im.getprojection() diff --git a/Tests/test_image_histogram.py b/Tests/test_image_histogram.py index 3ac6649e0..dbd55d4c2 100644 --- a/Tests/test_image_histogram.py +++ b/Tests/test_image_histogram.py @@ -3,8 +3,8 @@ from __future__ import annotations from .helper import hopper -def test_histogram(): - def histogram(mode): +def test_histogram() -> None: + def histogram(mode: str) -> tuple[int, int, int]: h = hopper(mode).histogram() return len(h), min(h), max(h) diff --git a/Tests/test_image_load.py b/Tests/test_image_load.py index 36f8ba575..0605821e0 100644 --- a/Tests/test_image_load.py +++ b/Tests/test_image_load.py @@ -10,14 +10,14 @@ from PIL import Image from .helper import hopper -def test_sanity(): +def test_sanity() -> None: im = hopper() pix = im.load() assert pix[0, 0] == (20, 20, 70) -def test_close(): +def test_close() -> None: im = Image.open("Tests/images/hopper.gif") im.close() with pytest.raises(ValueError): @@ -26,7 +26,7 @@ def test_close(): im.getpixel((0, 0)) -def test_close_after_load(caplog): +def test_close_after_load(caplog: pytest.LogCaptureFixture) -> None: im = Image.open("Tests/images/hopper.gif") im.load() with caplog.at_level(logging.DEBUG): @@ -34,7 +34,7 @@ def test_close_after_load(caplog): assert len(caplog.records) == 0 -def test_contextmanager(): +def test_contextmanager() -> None: fn = None with Image.open("Tests/images/hopper.gif") as im: fn = im.fp.fileno() @@ -44,7 +44,7 @@ def test_contextmanager(): os.fstat(fn) -def test_contextmanager_non_exclusive_fp(): +def test_contextmanager_non_exclusive_fp() -> None: with open("Tests/images/hopper.gif", "rb") as fp: with Image.open(fp): pass diff --git a/Tests/test_image_mode.py b/Tests/test_image_mode.py index 3c1d494fa..8e94aafc5 100644 --- a/Tests/test_image_mode.py +++ b/Tests/test_image_mode.py @@ -7,7 +7,7 @@ from PIL import Image, ImageMode from .helper import hopper -def test_sanity(): +def test_sanity() -> None: with hopper() as im: im.mode @@ -69,7 +69,7 @@ def test_sanity(): ) def test_properties( mode, expected_base, expected_type, expected_bands, expected_band_names -): +) -> None: assert Image.getmodebase(mode) == expected_base assert Image.getmodetype(mode) == expected_type assert Image.getmodebands(mode) == expected_bands diff --git a/Tests/test_image_paste.py b/Tests/test_image_paste.py index fd117f9db..d8f6b65e0 100644 --- a/Tests/test_image_paste.py +++ b/Tests/test_image_paste.py @@ -8,13 +8,11 @@ from .helper import CachedProperty, assert_image_equal class TestImagingPaste: - masks = {} size = 128 - def assert_9points_image(self, im, expected): - expected = [ - point[0] if im.mode == "L" else point[: len(im.mode)] for point in expected - ] + def assert_9points_image( + self, im: Image.Image, expected: list[tuple[int, int, int, int]] + ) -> None: px = im.load() actual = [ px[0, 0], @@ -27,9 +25,17 @@ class TestImagingPaste: px[self.size // 2, self.size - 1], px[self.size - 1, self.size - 1], ] - assert actual == expected + assert actual == [ + point[0] if im.mode == "L" else point[: len(im.mode)] for point in expected + ] - def assert_9points_paste(self, im, im2, mask, expected): + def assert_9points_paste( + self, + im: Image.Image, + im2: Image.Image | str | tuple[int, ...], + mask: Image.Image, + expected: list[tuple[int, int, int, int]], + ) -> None: im3 = im.copy() im3.paste(im2, (0, 0), mask) self.assert_9points_image(im3, expected) @@ -39,7 +45,7 @@ class TestImagingPaste: self.assert_9points_image(im, expected) @CachedProperty - def mask_1(self): + def mask_1(self) -> Image.Image: mask = Image.new("1", (self.size, self.size)) px = mask.load() for y in range(mask.height): @@ -48,11 +54,11 @@ class TestImagingPaste: return mask @CachedProperty - def mask_L(self): + def mask_L(self) -> Image.Image: return self.gradient_L.transpose(Image.Transpose.ROTATE_270) @CachedProperty - def gradient_L(self): + def gradient_L(self) -> Image.Image: gradient = Image.new("L", (self.size, self.size)) px = gradient.load() for y in range(gradient.height): @@ -61,7 +67,7 @@ class TestImagingPaste: return gradient @CachedProperty - def gradient_RGB(self): + def gradient_RGB(self) -> Image.Image: return Image.merge( "RGB", [ @@ -72,7 +78,7 @@ class TestImagingPaste: ) @CachedProperty - def gradient_LA(self): + def gradient_LA(self) -> Image.Image: return Image.merge( "LA", [ @@ -82,7 +88,7 @@ class TestImagingPaste: ) @CachedProperty - def gradient_RGBA(self): + def gradient_RGBA(self) -> Image.Image: return Image.merge( "RGBA", [ @@ -94,7 +100,7 @@ class TestImagingPaste: ) @CachedProperty - def gradient_RGBa(self): + def gradient_RGBa(self) -> Image.Image: return Image.merge( "RGBa", [ @@ -106,7 +112,7 @@ class TestImagingPaste: ) @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) - def test_image_solid(self, mode): + def test_image_solid(self, mode: str) -> None: im = Image.new(mode, (200, 200), "red") im2 = getattr(self, "gradient_" + mode) @@ -116,7 +122,7 @@ class TestImagingPaste: assert_image_equal(im, im2) @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) - def test_image_mask_1(self, mode): + def test_image_mask_1(self, mode: str) -> None: im = Image.new(mode, (200, 200), "white") im2 = getattr(self, "gradient_" + mode) @@ -138,7 +144,7 @@ class TestImagingPaste: ) @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) - def test_image_mask_L(self, mode): + def test_image_mask_L(self, mode: str) -> None: im = Image.new(mode, (200, 200), "white") im2 = getattr(self, "gradient_" + mode) @@ -160,7 +166,7 @@ class TestImagingPaste: ) @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) - def test_image_mask_LA(self, mode): + def test_image_mask_LA(self, mode: str) -> None: im = Image.new(mode, (200, 200), "white") im2 = getattr(self, "gradient_" + mode) @@ -182,7 +188,7 @@ class TestImagingPaste: ) @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) - def test_image_mask_RGBA(self, mode): + def test_image_mask_RGBA(self, mode: str) -> None: im = Image.new(mode, (200, 200), "white") im2 = getattr(self, "gradient_" + mode) @@ -204,7 +210,7 @@ class TestImagingPaste: ) @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) - def test_image_mask_RGBa(self, mode): + def test_image_mask_RGBa(self, mode: str) -> None: im = Image.new(mode, (200, 200), "white") im2 = getattr(self, "gradient_" + mode) @@ -226,7 +232,7 @@ class TestImagingPaste: ) @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) - def test_color_solid(self, mode): + def test_color_solid(self, mode: str) -> None: im = Image.new(mode, (200, 200), "black") rect = (12, 23, 128 + 12, 128 + 23) @@ -239,7 +245,7 @@ class TestImagingPaste: assert sum(head[:255]) == 0 @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) - def test_color_mask_1(self, mode): + def test_color_mask_1(self, mode: str) -> None: im = Image.new(mode, (200, 200), (50, 60, 70, 80)[: len(mode)]) color = (10, 20, 30, 40)[: len(mode)] @@ -261,7 +267,7 @@ class TestImagingPaste: ) @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) - def test_color_mask_L(self, mode): + def test_color_mask_L(self, mode: str) -> None: im = getattr(self, "gradient_" + mode).copy() color = "white" @@ -283,7 +289,7 @@ class TestImagingPaste: ) @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) - def test_color_mask_RGBA(self, mode): + def test_color_mask_RGBA(self, mode: str) -> None: im = getattr(self, "gradient_" + mode).copy() color = "white" @@ -305,7 +311,7 @@ class TestImagingPaste: ) @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) - def test_color_mask_RGBa(self, mode): + def test_color_mask_RGBa(self, mode: str) -> None: im = getattr(self, "gradient_" + mode).copy() color = "white" @@ -326,7 +332,7 @@ class TestImagingPaste: ], ) - def test_different_sizes(self): + def test_different_sizes(self) -> None: im = Image.new("RGB", (100, 100)) im2 = Image.new("RGB", (50, 50)) diff --git a/Tests/test_image_point.py b/Tests/test_image_point.py index 2232b9442..05f209351 100644 --- a/Tests/test_image_point.py +++ b/Tests/test_image_point.py @@ -5,7 +5,7 @@ import pytest from .helper import assert_image_equal, hopper -def test_sanity(): +def test_sanity() -> None: im = hopper() with pytest.raises(ValueError): @@ -39,7 +39,7 @@ def test_sanity(): im.point(lambda x: x // 2) -def test_16bit_lut(): +def test_16bit_lut() -> None: """Tests for 16 bit -> 8 bit lut for converting I->L images see https://github.com/python-pillow/Pillow/issues/440 """ @@ -47,7 +47,7 @@ def test_16bit_lut(): im.point(list(range(256)) * 256, "L") -def test_f_lut(): +def test_f_lut() -> None: """Tests for floating point lut of 8bit gray image""" im = hopper("L") lut = [0.5 * float(x) for x in range(256)] @@ -58,7 +58,7 @@ def test_f_lut(): assert_image_equal(out.convert("L"), im.point(int_lut, "L")) -def test_f_mode(): +def test_f_mode() -> None: im = hopper("F") with pytest.raises(ValueError): im.point(None) diff --git a/Tests/test_image_putalpha.py b/Tests/test_image_putalpha.py index c44b048d5..2c92911d1 100644 --- a/Tests/test_image_putalpha.py +++ b/Tests/test_image_putalpha.py @@ -3,7 +3,7 @@ from __future__ import annotations from PIL import Image -def test_interface(): +def test_interface() -> None: im = Image.new("RGBA", (1, 1), (1, 2, 3, 0)) assert im.getpixel((0, 0)) == (1, 2, 3, 0) @@ -17,7 +17,7 @@ def test_interface(): assert im.getpixel((0, 0)) == (1, 2, 3, 5) -def test_promote(): +def test_promote() -> None: im = Image.new("L", (1, 1), 1) assert im.getpixel((0, 0)) == 1 @@ -40,7 +40,7 @@ def test_promote(): assert im.getpixel((0, 0)) == (1, 2, 3, 4) -def test_readonly(): +def test_readonly() -> None: im = Image.new("RGB", (1, 1), (1, 2, 3)) im.readonly = 1 diff --git a/Tests/test_image_putdata.py b/Tests/test_image_putdata.py index 2648af8fa..73145faac 100644 --- a/Tests/test_image_putdata.py +++ b/Tests/test_image_putdata.py @@ -10,7 +10,7 @@ from PIL import Image from .helper import assert_image_equal, hopper -def test_sanity(): +def test_sanity() -> None: im1 = hopper() data = list(im1.getdata()) @@ -29,9 +29,9 @@ def test_sanity(): assert_image_equal(im1, im2) -def test_long_integers(): +def test_long_integers() -> None: # see bug-200802-systemerror - def put(value): + def put(value: int) -> tuple[int, int, int, int]: im = Image.new("RGBA", (1, 1)) im.putdata([value]) return im.getpixel((0, 0)) @@ -46,19 +46,19 @@ def test_long_integers(): assert put(sys.maxsize) == (255, 255, 255, 127) -def test_pypy_performance(): +def test_pypy_performance() -> None: im = Image.new("L", (256, 256)) im.putdata(list(range(256)) * 256) -def test_mode_with_L_with_float(): +def test_mode_with_L_with_float() -> None: im = Image.new("L", (1, 1), 0) im.putdata([2.0]) assert im.getpixel((0, 0)) == 2 @pytest.mark.parametrize("mode", ("I", "I;16", "I;16L", "I;16B")) -def test_mode_i(mode): +def test_mode_i(mode: str) -> None: src = hopper("L") data = list(src.getdata()) im = Image.new(mode, src.size, 0) @@ -68,7 +68,7 @@ def test_mode_i(mode): assert list(im.getdata()) == target -def test_mode_F(): +def test_mode_F() -> None: src = hopper("L") data = list(src.getdata()) im = Image.new("F", src.size, 0) @@ -79,7 +79,7 @@ def test_mode_F(): @pytest.mark.parametrize("mode", ("BGR;15", "BGR;16", "BGR;24")) -def test_mode_BGR(mode): +def test_mode_BGR(mode: str) -> None: data = [(16, 32, 49), (32, 32, 98)] im = Image.new(mode, (1, 2)) im.putdata(data) @@ -87,7 +87,7 @@ def test_mode_BGR(mode): assert list(im.getdata()) == data -def test_array_B(): +def test_array_B() -> None: # shouldn't segfault # see https://github.com/python-pillow/Pillow/issues/1008 @@ -98,7 +98,7 @@ def test_array_B(): assert len(im.getdata()) == len(arr) -def test_array_F(): +def test_array_F() -> None: # shouldn't segfault # see https://github.com/python-pillow/Pillow/issues/1008 @@ -109,7 +109,7 @@ def test_array_F(): assert len(im.getdata()) == len(arr) -def test_not_flattened(): +def test_not_flattened() -> None: im = Image.new("L", (1, 1)) with pytest.raises(TypeError): im.putdata([[0]]) diff --git a/Tests/test_image_putpalette.py b/Tests/test_image_putpalette.py index 43b65be2b..cc7cf58f0 100644 --- a/Tests/test_image_putpalette.py +++ b/Tests/test_image_putpalette.py @@ -7,8 +7,8 @@ from PIL import Image, ImagePalette from .helper import assert_image_equal, assert_image_equal_tofile, hopper -def test_putpalette(): - def palette(mode): +def test_putpalette() -> None: + def palette(mode: str) -> str | tuple[str, list[int]]: im = hopper(mode).copy() im.putpalette(list(range(256)) * 3) p = im.getpalette() @@ -43,7 +43,7 @@ def test_putpalette(): im.putpalette(list(range(256)) * 3) -def test_imagepalette(): +def test_imagepalette() -> None: im = hopper("P") im.putpalette(ImagePalette.negative()) assert_image_equal_tofile(im.convert("RGB"), "Tests/images/palette_negative.png") @@ -57,7 +57,7 @@ def test_imagepalette(): assert_image_equal_tofile(im.convert("RGB"), "Tests/images/palette_wedge.png") -def test_putpalette_with_alpha_values(): +def test_putpalette_with_alpha_values() -> None: with Image.open("Tests/images/transparent.gif") as im: expected = im.convert("RGBA") @@ -81,19 +81,19 @@ def test_putpalette_with_alpha_values(): ("RGBAX", (1, 2, 3, 4, 0)), ), ) -def test_rgba_palette(mode, palette): +def test_rgba_palette(mode: str, palette: tuple[int, ...]) -> None: im = Image.new("P", (1, 1)) im.putpalette(palette, mode) assert im.getpalette() == [1, 2, 3] assert im.palette.colors == {(1, 2, 3, 4): 0} -def test_empty_palette(): +def test_empty_palette() -> None: im = Image.new("P", (1, 1)) assert im.getpalette() == [] -def test_undefined_palette_index(): +def test_undefined_palette_index() -> None: im = Image.new("P", (1, 1), 3) im.putpalette((1, 2, 3)) assert im.convert("RGB").getpixel((0, 0)) == (0, 0, 0) diff --git a/Tests/test_image_quantize.py b/Tests/test_image_quantize.py index 1475b027b..873a9bb5d 100644 --- a/Tests/test_image_quantize.py +++ b/Tests/test_image_quantize.py @@ -8,7 +8,7 @@ from PIL import Image, features from .helper import assert_image_similar, hopper, is_ppc64le, skip_unless_feature -def test_sanity(): +def test_sanity() -> None: image = hopper() converted = image.quantize() assert converted.mode == "P" @@ -21,7 +21,7 @@ def test_sanity(): @skip_unless_feature("libimagequant") -def test_libimagequant_quantize(): +def test_libimagequant_quantize() -> None: image = hopper() if is_ppc64le(): libimagequant = parse_version(features.version_feature("libimagequant")) @@ -33,7 +33,7 @@ def test_libimagequant_quantize(): assert len(converted.getcolors()) == 100 -def test_octree_quantize(): +def test_octree_quantize() -> None: image = hopper() converted = image.quantize(100, Image.Quantize.FASTOCTREE) assert converted.mode == "P" @@ -41,7 +41,7 @@ def test_octree_quantize(): assert len(converted.getcolors()) == 100 -def test_rgba_quantize(): +def test_rgba_quantize() -> None: image = hopper("RGBA") with pytest.raises(ValueError): image.quantize(method=0) @@ -49,7 +49,7 @@ def test_rgba_quantize(): assert image.quantize().convert().mode == "RGBA" -def test_quantize(): +def test_quantize() -> None: with Image.open("Tests/images/caption_6_33_22.png") as image: image = image.convert("RGB") converted = image.quantize() @@ -57,7 +57,7 @@ def test_quantize(): assert_image_similar(converted.convert("RGB"), image, 1) -def test_quantize_no_dither(): +def test_quantize_no_dither() -> None: image = hopper() with Image.open("Tests/images/caption_6_33_22.png") as palette: palette = palette.convert("P") @@ -67,7 +67,7 @@ def test_quantize_no_dither(): assert converted.palette.palette == palette.palette.palette -def test_quantize_no_dither2(): +def test_quantize_no_dither2() -> None: im = Image.new("RGB", (9, 1)) im.putdata([(p,) * 3 for p in range(0, 36, 4)]) @@ -83,7 +83,7 @@ def test_quantize_no_dither2(): assert px[x, 0] == (0 if x < 5 else 1) -def test_quantize_dither_diff(): +def test_quantize_dither_diff() -> None: image = hopper() with Image.open("Tests/images/caption_6_33_22.png") as palette: palette = palette.convert("P") @@ -94,14 +94,14 @@ def test_quantize_dither_diff(): assert dither.tobytes() != nodither.tobytes() -def test_colors(): +def test_colors() -> None: im = hopper() colors = 2 converted = im.quantize(colors) assert len(converted.palette.palette) == colors * len("RGB") -def test_transparent_colors_equal(): +def test_transparent_colors_equal() -> None: im = Image.new("RGBA", (1, 2), (0, 0, 0, 0)) px = im.load() px[0, 1] = (255, 255, 255, 0) @@ -120,7 +120,7 @@ def test_transparent_colors_equal(): (Image.Quantize.FASTOCTREE, (0, 0, 0, 0)), ), ) -def test_palette(method, color): +def test_palette(method: Image.Quantize, color: tuple[int, ...]) -> None: im = Image.new("RGBA" if len(color) == 4 else "RGB", (1, 1), color) converted = im.quantize(method=method) @@ -128,7 +128,7 @@ def test_palette(method, color): assert converted_px[0, 0] == converted.palette.colors[color] -def test_small_palette(): +def test_small_palette() -> None: # Arrange im = hopper() diff --git a/Tests/test_image_reduce.py b/Tests/test_image_reduce.py index ba9100415..33b33d6b7 100644 --- a/Tests/test_image_reduce.py +++ b/Tests/test_image_reduce.py @@ -48,7 +48,7 @@ gradients_image.load() ((1, 3), (10, 4)), ), ) -def test_args_factor(size, expected): +def test_args_factor(size: int | tuple[int, int], expected: tuple[int, int]) -> None: im = Image.new("L", (10, 10)) assert expected == im.reduce(size).size @@ -56,7 +56,7 @@ def test_args_factor(size, expected): @pytest.mark.parametrize( "size, expected_error", ((0, ValueError), (2.0, TypeError), ((0, 10), ValueError)) ) -def test_args_factor_error(size, expected_error): +def test_args_factor_error(size: float | tuple[int, int], expected_error) -> None: im = Image.new("L", (10, 10)) with pytest.raises(expected_error): im.reduce(size) @@ -69,7 +69,7 @@ def test_args_factor_error(size, expected_error): ((5, 5, 6, 6), (1, 1)), ), ) -def test_args_box(size, expected): +def test_args_box(size: tuple[int, int, int, int], expected: tuple[int, int]) -> None: im = Image.new("L", (10, 10)) assert expected == im.reduce(2, size).size @@ -86,20 +86,20 @@ def test_args_box(size, expected): ((5, 0, 5, 10), ValueError), ), ) -def test_args_box_error(size, expected_error): +def test_args_box_error(size: str | tuple[int, int, int, int], expected_error) -> None: im = Image.new("L", (10, 10)) with pytest.raises(expected_error): im.reduce(2, size).size @pytest.mark.parametrize("mode", ("P", "1", "I;16")) -def test_unsupported_modes(mode): +def test_unsupported_modes(mode: str) -> None: im = Image.new("P", (10, 10)) with pytest.raises(ValueError): im.reduce(3) -def get_image(mode): +def get_image(mode: str) -> Image.Image: mode_info = ImageMode.getmode(mode) if mode_info.basetype == "L": bands = [gradients_image] @@ -119,14 +119,19 @@ def get_image(mode): return im.crop((0, 0, im.width, im.height - 5)) -def compare_reduce_with_box(im, factor): +def compare_reduce_with_box(im: Image.Image, factor: int | tuple[int, int]) -> None: box = (11, 13, 146, 164) reduced = im.reduce(factor, box=box) reference = im.crop(box).reduce(factor) assert reduced == reference -def compare_reduce_with_reference(im, factor, average_diff=0.4, max_diff=1): +def compare_reduce_with_reference( + im: Image.Image, + factor: int | tuple[int, int], + average_diff: float = 0.4, + max_diff: int = 1, +) -> None: """Image.reduce() should look very similar to Image.resize(BOX). A reference image is compiled from a large source area @@ -171,7 +176,9 @@ def compare_reduce_with_reference(im, factor, average_diff=0.4, max_diff=1): assert_compare_images(reduced, reference, average_diff, max_diff) -def assert_compare_images(a, b, max_average_diff, max_diff=255): +def assert_compare_images( + a: Image.Image, b: Image.Image, max_average_diff: float, max_diff: int = 255 +) -> None: assert a.mode == b.mode, f"got mode {repr(a.mode)}, expected {repr(b.mode)}" assert a.size == b.size, f"got size {repr(a.size)}, expected {repr(b.size)}" @@ -199,20 +206,20 @@ def assert_compare_images(a, b, max_average_diff, max_diff=255): @pytest.mark.parametrize("factor", remarkable_factors) -def test_mode_L(factor): +def test_mode_L(factor: int | tuple[int, int]) -> None: im = get_image("L") compare_reduce_with_reference(im, factor) compare_reduce_with_box(im, factor) @pytest.mark.parametrize("factor", remarkable_factors) -def test_mode_LA(factor): +def test_mode_LA(factor: int | tuple[int, int]) -> None: im = get_image("LA") compare_reduce_with_reference(im, factor, 0.8, 5) @pytest.mark.parametrize("factor", remarkable_factors) -def test_mode_LA_opaque(factor): +def test_mode_LA_opaque(factor: int | tuple[int, int]) -> None: im = get_image("LA") # With opaque alpha, an error should be way smaller. im.putalpha(Image.new("L", im.size, 255)) @@ -221,27 +228,27 @@ def test_mode_LA_opaque(factor): @pytest.mark.parametrize("factor", remarkable_factors) -def test_mode_La(factor): +def test_mode_La(factor: int | tuple[int, int]) -> None: im = get_image("La") compare_reduce_with_reference(im, factor) compare_reduce_with_box(im, factor) @pytest.mark.parametrize("factor", remarkable_factors) -def test_mode_RGB(factor): +def test_mode_RGB(factor: int | tuple[int, int]) -> None: im = get_image("RGB") compare_reduce_with_reference(im, factor) compare_reduce_with_box(im, factor) @pytest.mark.parametrize("factor", remarkable_factors) -def test_mode_RGBA(factor): +def test_mode_RGBA(factor: int | tuple[int, int]) -> None: im = get_image("RGBA") compare_reduce_with_reference(im, factor, 0.8, 5) @pytest.mark.parametrize("factor", remarkable_factors) -def test_mode_RGBA_opaque(factor): +def test_mode_RGBA_opaque(factor: int | tuple[int, int]) -> None: im = get_image("RGBA") # With opaque alpha, an error should be way smaller. im.putalpha(Image.new("L", im.size, 255)) @@ -250,27 +257,27 @@ def test_mode_RGBA_opaque(factor): @pytest.mark.parametrize("factor", remarkable_factors) -def test_mode_RGBa(factor): +def test_mode_RGBa(factor: int | tuple[int, int]) -> None: im = get_image("RGBa") compare_reduce_with_reference(im, factor) compare_reduce_with_box(im, factor) @pytest.mark.parametrize("factor", remarkable_factors) -def test_mode_I(factor): +def test_mode_I(factor: int | tuple[int, int]) -> None: im = get_image("I") compare_reduce_with_reference(im, factor) compare_reduce_with_box(im, factor) @pytest.mark.parametrize("factor", remarkable_factors) -def test_mode_F(factor): +def test_mode_F(factor: int | tuple[int, int]) -> None: im = get_image("F") compare_reduce_with_reference(im, factor, 0, 0) compare_reduce_with_box(im, factor) @skip_unless_feature("jpg_2000") -def test_jpeg2k(): +def test_jpeg2k() -> None: with Image.open("Tests/images/test-card-lossless.jp2") as im: assert im.reduce(2).size == (320, 240) diff --git a/Tests/test_image_resample.py b/Tests/test_image_resample.py index af730dce1..dbe193808 100644 --- a/Tests/test_image_resample.py +++ b/Tests/test_image_resample.py @@ -1,6 +1,7 @@ from __future__ import annotations from contextlib import contextmanager +from typing import Generator import pytest @@ -16,7 +17,7 @@ from .helper import ( class TestImagingResampleVulnerability: # see https://github.com/python-pillow/Pillow/issues/1710 - def test_overflow(self): + def test_overflow(self) -> None: im = hopper("L") size_too_large = 0x100000008 // 4 size_normal = 1000 # unimportant @@ -28,7 +29,7 @@ class TestImagingResampleVulnerability: # any resampling filter will do here im.im.resize((xsize, ysize), Image.Resampling.BILINEAR) - def test_invalid_size(self): + def test_invalid_size(self) -> None: im = hopper() # Should not crash @@ -40,7 +41,7 @@ class TestImagingResampleVulnerability: with pytest.raises(ValueError): im.resize((100, -100)) - def test_modify_after_resizing(self): + def test_modify_after_resizing(self) -> None: im = hopper("RGB") # get copy with same size copy = im.resize(im.size) @@ -51,7 +52,7 @@ class TestImagingResampleVulnerability: class TestImagingCoreResampleAccuracy: - def make_case(self, mode, size, color): + def make_case(self, mode: str, size: tuple[int, int], color: int) -> Image.Image: """Makes a sample image with two dark and two bright squares. For example: e0 e0 1f 1f @@ -66,7 +67,7 @@ class TestImagingCoreResampleAccuracy: return Image.merge(mode, [case] * len(mode)) - def make_sample(self, data, size): + def make_sample(self, data: str, size: tuple[int, int]) -> Image.Image: """Restores a sample image from given data string which contains hex-encoded pixels from the top left fourth of a sample. """ @@ -83,7 +84,7 @@ class TestImagingCoreResampleAccuracy: s_px[size[0] - x - 1, y] = 255 - val return sample - def check_case(self, case, sample): + def check_case(self, case: Image.Image, sample: Image.Image) -> None: s_px = sample.load() c_px = case.load() for y in range(case.size[1]): @@ -95,7 +96,7 @@ class TestImagingCoreResampleAccuracy: ) assert s_px[x, y] == c_px[x, y], message - def serialize_image(self, image): + def serialize_image(self, image: Image.Image) -> str: s_px = image.load() return "\n".join( " ".join(f"{s_px[x, y]:02x}" for x in range(image.size[0])) @@ -103,7 +104,7 @@ class TestImagingCoreResampleAccuracy: ) @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) - def test_reduce_box(self, mode): + def test_reduce_box(self, mode: str) -> None: case = self.make_case(mode, (8, 8), 0xE1) case = case.resize((4, 4), Image.Resampling.BOX) # fmt: off @@ -114,7 +115,7 @@ class TestImagingCoreResampleAccuracy: self.check_case(channel, self.make_sample(data, (4, 4))) @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) - def test_reduce_bilinear(self, mode): + def test_reduce_bilinear(self, mode: str) -> None: case = self.make_case(mode, (8, 8), 0xE1) case = case.resize((4, 4), Image.Resampling.BILINEAR) # fmt: off @@ -125,7 +126,7 @@ class TestImagingCoreResampleAccuracy: self.check_case(channel, self.make_sample(data, (4, 4))) @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) - def test_reduce_hamming(self, mode): + def test_reduce_hamming(self, mode: str) -> None: case = self.make_case(mode, (8, 8), 0xE1) case = case.resize((4, 4), Image.Resampling.HAMMING) # fmt: off @@ -136,7 +137,7 @@ class TestImagingCoreResampleAccuracy: self.check_case(channel, self.make_sample(data, (4, 4))) @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) - def test_reduce_bicubic(self, mode): + def test_reduce_bicubic(self, mode: str) -> None: case = self.make_case(mode, (12, 12), 0xE1) case = case.resize((6, 6), Image.Resampling.BICUBIC) # fmt: off @@ -148,7 +149,7 @@ class TestImagingCoreResampleAccuracy: self.check_case(channel, self.make_sample(data, (6, 6))) @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) - def test_reduce_lanczos(self, mode): + def test_reduce_lanczos(self, mode: str) -> None: case = self.make_case(mode, (16, 16), 0xE1) case = case.resize((8, 8), Image.Resampling.LANCZOS) # fmt: off @@ -161,7 +162,7 @@ class TestImagingCoreResampleAccuracy: self.check_case(channel, self.make_sample(data, (8, 8))) @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) - def test_enlarge_box(self, mode): + def test_enlarge_box(self, mode: str) -> None: case = self.make_case(mode, (2, 2), 0xE1) case = case.resize((4, 4), Image.Resampling.BOX) # fmt: off @@ -172,7 +173,7 @@ class TestImagingCoreResampleAccuracy: self.check_case(channel, self.make_sample(data, (4, 4))) @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) - def test_enlarge_bilinear(self, mode): + def test_enlarge_bilinear(self, mode: str) -> None: case = self.make_case(mode, (2, 2), 0xE1) case = case.resize((4, 4), Image.Resampling.BILINEAR) # fmt: off @@ -183,7 +184,7 @@ class TestImagingCoreResampleAccuracy: self.check_case(channel, self.make_sample(data, (4, 4))) @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) - def test_enlarge_hamming(self, mode): + def test_enlarge_hamming(self, mode: str) -> None: case = self.make_case(mode, (2, 2), 0xE1) case = case.resize((4, 4), Image.Resampling.HAMMING) # fmt: off @@ -194,7 +195,7 @@ class TestImagingCoreResampleAccuracy: self.check_case(channel, self.make_sample(data, (4, 4))) @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) - def test_enlarge_bicubic(self, mode): + def test_enlarge_bicubic(self, mode: str) -> None: case = self.make_case(mode, (4, 4), 0xE1) case = case.resize((8, 8), Image.Resampling.BICUBIC) # fmt: off @@ -207,7 +208,7 @@ class TestImagingCoreResampleAccuracy: self.check_case(channel, self.make_sample(data, (8, 8))) @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) - def test_enlarge_lanczos(self, mode): + def test_enlarge_lanczos(self, mode: str) -> None: case = self.make_case(mode, (6, 6), 0xE1) case = case.resize((12, 12), Image.Resampling.LANCZOS) data = ( @@ -221,7 +222,7 @@ class TestImagingCoreResampleAccuracy: for channel in case.split(): self.check_case(channel, self.make_sample(data, (12, 12))) - def test_box_filter_correct_range(self): + def test_box_filter_correct_range(self) -> None: im = Image.new("RGB", (8, 8), "#1688ff").resize( (100, 100), Image.Resampling.BOX ) @@ -230,11 +231,13 @@ class TestImagingCoreResampleAccuracy: class TestCoreResampleConsistency: - def make_case(self, mode, fill): + def make_case( + self, mode: str, fill: tuple[int, int, int] | float + ) -> tuple[Image.Image, tuple[int, ...]]: im = Image.new(mode, (512, 9), fill) return im.resize((9, 512), Image.Resampling.LANCZOS), im.load()[0, 0] - def run_case(self, case): + def run_case(self, case: tuple[Image.Image, int | tuple[int, ...]]) -> None: channel, color = case px = channel.load() for x in range(channel.size[0]): @@ -243,7 +246,7 @@ class TestCoreResampleConsistency: message = f"{px[x, y]} != {color} for pixel {(x, y)}" assert px[x, y] == color, message - def test_8u(self): + def test_8u(self) -> None: im, color = self.make_case("RGB", (0, 64, 255)) r, g, b = im.split() self.run_case((r, color[0])) @@ -251,13 +254,13 @@ class TestCoreResampleConsistency: self.run_case((b, color[2])) self.run_case(self.make_case("L", 12)) - def test_32i(self): + def test_32i(self) -> None: self.run_case(self.make_case("I", 12)) self.run_case(self.make_case("I", 0x7FFFFFFF)) self.run_case(self.make_case("I", -12)) self.run_case(self.make_case("I", -1 << 31)) - def test_32f(self): + def test_32f(self) -> None: self.run_case(self.make_case("F", 1)) self.run_case(self.make_case("F", 3.40282306074e38)) self.run_case(self.make_case("F", 1.175494e-38)) @@ -265,7 +268,7 @@ class TestCoreResampleConsistency: class TestCoreResampleAlphaCorrect: - def make_levels_case(self, mode): + def make_levels_case(self, mode: str) -> Image.Image: i = Image.new(mode, (256, 16)) px = i.load() for y in range(i.size[1]): @@ -275,7 +278,7 @@ class TestCoreResampleAlphaCorrect: px[x, y] = tuple(pix) return i - def run_levels_case(self, i): + def run_levels_case(self, i: Image.Image) -> None: px = i.load() for y in range(i.size[1]): used_colors = {px[x, y][0] for x in range(i.size[0])} @@ -285,7 +288,7 @@ class TestCoreResampleAlphaCorrect: ) @pytest.mark.xfail(reason="Current implementation isn't precise enough") - def test_levels_rgba(self): + def test_levels_rgba(self) -> None: case = self.make_levels_case("RGBA") self.run_levels_case(case.resize((512, 32), Image.Resampling.BOX)) self.run_levels_case(case.resize((512, 32), Image.Resampling.BILINEAR)) @@ -294,7 +297,7 @@ class TestCoreResampleAlphaCorrect: self.run_levels_case(case.resize((512, 32), Image.Resampling.LANCZOS)) @pytest.mark.xfail(reason="Current implementation isn't precise enough") - def test_levels_la(self): + def test_levels_la(self) -> None: case = self.make_levels_case("LA") self.run_levels_case(case.resize((512, 32), Image.Resampling.BOX)) self.run_levels_case(case.resize((512, 32), Image.Resampling.BILINEAR)) @@ -302,7 +305,9 @@ class TestCoreResampleAlphaCorrect: self.run_levels_case(case.resize((512, 32), Image.Resampling.BICUBIC)) self.run_levels_case(case.resize((512, 32), Image.Resampling.LANCZOS)) - def make_dirty_case(self, mode, clean_pixel, dirty_pixel): + def make_dirty_case( + self, mode: str, clean_pixel: tuple[int, ...], dirty_pixel: tuple[int, ...] + ) -> Image.Image: i = Image.new(mode, (64, 64), dirty_pixel) px = i.load() xdiv4 = i.size[0] // 4 @@ -312,7 +317,7 @@ class TestCoreResampleAlphaCorrect: px[x + xdiv4, y + ydiv4] = clean_pixel return i - def run_dirty_case(self, i, clean_pixel): + def run_dirty_case(self, i: Image.Image, clean_pixel: tuple[int, ...]) -> None: px = i.load() for y in range(i.size[1]): for x in range(i.size[0]): @@ -323,7 +328,7 @@ class TestCoreResampleAlphaCorrect: ) assert px[x, y][:3] == clean_pixel, message - def test_dirty_pixels_rgba(self): + def test_dirty_pixels_rgba(self) -> None: case = self.make_dirty_case("RGBA", (255, 255, 0, 128), (0, 0, 255, 0)) self.run_dirty_case(case.resize((20, 20), Image.Resampling.BOX), (255, 255, 0)) self.run_dirty_case( @@ -339,7 +344,7 @@ class TestCoreResampleAlphaCorrect: case.resize((20, 20), Image.Resampling.LANCZOS), (255, 255, 0) ) - def test_dirty_pixels_la(self): + def test_dirty_pixels_la(self) -> None: case = self.make_dirty_case("LA", (255, 128), (0, 0)) self.run_dirty_case(case.resize((20, 20), Image.Resampling.BOX), (255,)) self.run_dirty_case(case.resize((20, 20), Image.Resampling.BILINEAR), (255,)) @@ -350,27 +355,27 @@ class TestCoreResampleAlphaCorrect: class TestCoreResamplePasses: @contextmanager - def count(self, diff): + def count(self, diff: int) -> Generator[None, None, None]: count = Image.core.get_stats()["new_count"] yield assert Image.core.get_stats()["new_count"] - count == diff - def test_horizontal(self): + def test_horizontal(self) -> None: im = hopper("L") with self.count(1): im.resize((im.size[0] - 10, im.size[1]), Image.Resampling.BILINEAR) - def test_vertical(self): + def test_vertical(self) -> None: im = hopper("L") with self.count(1): im.resize((im.size[0], im.size[1] - 10), Image.Resampling.BILINEAR) - def test_both(self): + def test_both(self) -> None: im = hopper("L") with self.count(2): im.resize((im.size[0] - 10, im.size[1] - 10), Image.Resampling.BILINEAR) - def test_box_horizontal(self): + def test_box_horizontal(self) -> None: im = hopper("L") box = (20, 0, im.size[0] - 20, im.size[1]) with self.count(1): @@ -380,7 +385,7 @@ class TestCoreResamplePasses: cropped = im.crop(box).resize(im.size, Image.Resampling.BILINEAR) assert_image_similar(with_box, cropped, 0.1) - def test_box_vertical(self): + def test_box_vertical(self) -> None: im = hopper("L") box = (0, 20, im.size[0], im.size[1] - 20) with self.count(1): @@ -392,7 +397,7 @@ class TestCoreResamplePasses: class TestCoreResampleCoefficients: - def test_reduce(self): + def test_reduce(self) -> None: test_color = 254 for size in range(400000, 400010, 2): @@ -404,7 +409,7 @@ class TestCoreResampleCoefficients: if px[2, 0] != test_color // 2: assert test_color // 2 == px[2, 0] - def test_non_zero_coefficients(self): + def test_non_zero_coefficients(self) -> None: # regression test for the wrong coefficients calculation # due to bug https://github.com/python-pillow/Pillow/issues/2161 im = Image.new("RGBA", (1280, 1280), (0x20, 0x40, 0x60, 0xFF)) @@ -432,7 +437,7 @@ class TestCoreResampleBox: Image.Resampling.LANCZOS, ), ) - def test_wrong_arguments(self, resample): + def test_wrong_arguments(self, resample: Image.Resampling) -> None: im = hopper() im.resize((32, 32), resample, (0, 0, im.width, im.height)) im.resize((32, 32), resample, (20, 20, im.width, im.height)) @@ -459,8 +464,12 @@ class TestCoreResampleBox: with pytest.raises(ValueError, match="can't exceed"): im.resize((32, 32), resample, (0, 0, im.width, im.height + 1)) - def resize_tiled(self, im, dst_size, xtiles, ytiles): - def split_range(size, tiles): + def resize_tiled( + self, im: Image.Image, dst_size: tuple[int, int], xtiles: int, ytiles: int + ) -> Image.Image: + def split_range( + size: int, tiles: int + ) -> Generator[tuple[int, int], None, None]: scale = size / tiles for i in range(tiles): yield int(round(scale * i)), int(round(scale * (i + 1))) @@ -478,7 +487,7 @@ class TestCoreResampleBox: @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) - def test_tiles(self): + def test_tiles(self) -> None: with Image.open("Tests/images/flower.jpg") as im: assert im.size == (480, 360) dst_size = (251, 188) @@ -491,7 +500,7 @@ class TestCoreResampleBox: @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) - def test_subsample(self): + def test_subsample(self) -> None: # This test shows advantages of the subpixel resizing # after supersampling (e.g. during JPEG decoding). with Image.open("Tests/images/flower.jpg") as im: @@ -518,14 +527,14 @@ class TestCoreResampleBox: @pytest.mark.parametrize( "resample", (Image.Resampling.NEAREST, Image.Resampling.BILINEAR) ) - def test_formats(self, mode, resample): + def test_formats(self, mode: str, resample: Image.Resampling) -> None: im = hopper(mode) box = (20, 20, im.size[0] - 20, im.size[1] - 20) with_box = im.resize((32, 32), resample, box) cropped = im.crop(box).resize((32, 32), resample) assert_image_similar(cropped, with_box, 0.4) - def test_passthrough(self): + def test_passthrough(self) -> None: # When no resize is required im = hopper() @@ -539,7 +548,7 @@ class TestCoreResampleBox: assert res.size == size assert_image_equal(res, im.crop(box), f">>> {size} {box}") - def test_no_passthrough(self): + def test_no_passthrough(self) -> None: # When resize is required im = hopper() @@ -558,7 +567,7 @@ class TestCoreResampleBox: @pytest.mark.parametrize( "flt", (Image.Resampling.NEAREST, Image.Resampling.BICUBIC) ) - def test_skip_horizontal(self, flt): + def test_skip_horizontal(self, flt: Image.Resampling) -> None: # Can skip resize for one dimension im = hopper() @@ -581,7 +590,7 @@ class TestCoreResampleBox: @pytest.mark.parametrize( "flt", (Image.Resampling.NEAREST, Image.Resampling.BICUBIC) ) - def test_skip_vertical(self, flt): + def test_skip_vertical(self, flt: Image.Resampling) -> None: # Can skip resize for one dimension im = hopper() diff --git a/Tests/test_image_resize.py b/Tests/test_image_resize.py index aedcf4a09..64098f80f 100644 --- a/Tests/test_image_resize.py +++ b/Tests/test_image_resize.py @@ -1,9 +1,12 @@ """ Tests for resize functionality. """ + from __future__ import annotations from itertools import permutations +from pathlib import Path +from typing import Generator import pytest @@ -19,7 +22,9 @@ from .helper import ( class TestImagingCoreResize: - def resize(self, im, size, f): + def resize( + self, im: Image.Image, size: tuple[int, int], f: Image.Resampling + ) -> Image.Image: # Image class independent version of resize. im.load() return im._new(im.im.resize(size, f)) @@ -27,14 +32,14 @@ class TestImagingCoreResize: @pytest.mark.parametrize( "mode", ("1", "P", "L", "I", "F", "RGB", "RGBA", "CMYK", "YCbCr", "I;16") ) - def test_nearest_mode(self, mode): + def test_nearest_mode(self, mode: str) -> None: im = hopper(mode) r = self.resize(im, (15, 12), Image.Resampling.NEAREST) assert r.mode == mode assert r.size == (15, 12) assert r.im.bands == im.im.bands - def test_convolution_modes(self): + def test_convolution_modes(self) -> None: with pytest.raises(ValueError): self.resize(hopper("1"), (15, 12), Image.Resampling.BILINEAR) with pytest.raises(ValueError): @@ -59,7 +64,7 @@ class TestImagingCoreResize: Image.Resampling.LANCZOS, ), ) - def test_reduce_filters(self, resample): + def test_reduce_filters(self, resample: Image.Resampling) -> None: r = self.resize(hopper("RGB"), (15, 12), resample) assert r.mode == "RGB" assert r.size == (15, 12) @@ -75,7 +80,7 @@ class TestImagingCoreResize: Image.Resampling.LANCZOS, ), ) - def test_enlarge_filters(self, resample): + def test_enlarge_filters(self, resample: Image.Resampling) -> None: r = self.resize(hopper("RGB"), (212, 195), resample) assert r.mode == "RGB" assert r.size == (212, 195) @@ -99,7 +104,9 @@ class TestImagingCoreResize: ("LA", ("filled", "dirty")), ), ) - def test_endianness(self, resample, mode, channels_set): + def test_endianness( + self, resample: Image.Resampling, mode: str, channels_set: tuple[str, ...] + ) -> None: # Make an image with one colored pixel, in one channel. # When resized, that channel should be the same as a GS image. # Other channels should be unaffected. @@ -139,17 +146,17 @@ class TestImagingCoreResize: Image.Resampling.LANCZOS, ), ) - def test_enlarge_zero(self, resample): + def test_enlarge_zero(self, resample: Image.Resampling) -> None: r = self.resize(Image.new("RGB", (0, 0), "white"), (212, 195), resample) assert r.mode == "RGB" assert r.size == (212, 195) assert r.getdata()[0] == (0, 0, 0) - def test_unknown_filter(self): + def test_unknown_filter(self) -> None: with pytest.raises(ValueError): - self.resize(hopper(), (10, 10), 9) + self.resize(hopper(), (10, 10), 9) # type: ignore[arg-type] - def test_cross_platform(self, tmp_path): + def test_cross_platform(self, tmp_path: Path) -> None: # This test is intended for only check for consistent behaviour across # platforms. So if a future Pillow change requires that the test file # be updated, that is okay. @@ -162,7 +169,7 @@ class TestImagingCoreResize: @pytest.fixture -def gradients_image(): +def gradients_image() -> Generator[Image.Image, None, None]: with Image.open("Tests/images/radial_gradients.png") as im: im.load() try: @@ -172,7 +179,7 @@ def gradients_image(): class TestReducingGapResize: - def test_reducing_gap_values(self, gradients_image): + def test_reducing_gap_values(self, gradients_image: Image.Image) -> None: ref = gradients_image.resize( (52, 34), Image.Resampling.BICUBIC, reducing_gap=None ) @@ -191,7 +198,12 @@ class TestReducingGapResize: "box, epsilon", ((None, 4), ((1.1, 2.2, 510.8, 510.9), 4), ((3, 10, 410, 256), 10)), ) - def test_reducing_gap_1(self, gradients_image, box, epsilon): + def test_reducing_gap_1( + self, + gradients_image: Image.Image, + box: tuple[float, float, float, float], + epsilon: float, + ) -> None: ref = gradients_image.resize((52, 34), Image.Resampling.BICUBIC, box=box) im = gradients_image.resize( (52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=1.0 @@ -206,7 +218,12 @@ class TestReducingGapResize: "box, epsilon", ((None, 1.5), ((1.1, 2.2, 510.8, 510.9), 1.5), ((3, 10, 410, 256), 1)), ) - def test_reducing_gap_2(self, gradients_image, box, epsilon): + def test_reducing_gap_2( + self, + gradients_image: Image.Image, + box: tuple[float, float, float, float], + epsilon: float, + ) -> None: ref = gradients_image.resize((52, 34), Image.Resampling.BICUBIC, box=box) im = gradients_image.resize( (52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=2.0 @@ -221,7 +238,12 @@ class TestReducingGapResize: "box, epsilon", ((None, 1), ((1.1, 2.2, 510.8, 510.9), 1), ((3, 10, 410, 256), 0.5)), ) - def test_reducing_gap_3(self, gradients_image, box, epsilon): + def test_reducing_gap_3( + self, + gradients_image: Image.Image, + box: tuple[float, float, float, float], + epsilon: float, + ) -> None: ref = gradients_image.resize((52, 34), Image.Resampling.BICUBIC, box=box) im = gradients_image.resize( (52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=3.0 @@ -233,7 +255,9 @@ class TestReducingGapResize: assert_image_similar(ref, im, epsilon) @pytest.mark.parametrize("box", (None, (1.1, 2.2, 510.8, 510.9), (3, 10, 410, 256))) - def test_reducing_gap_8(self, gradients_image, box): + def test_reducing_gap_8( + self, gradients_image: Image.Image, box: tuple[float, float, float, float] + ) -> None: ref = gradients_image.resize((52, 34), Image.Resampling.BICUBIC, box=box) im = gradients_image.resize( (52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=8.0 @@ -245,7 +269,12 @@ class TestReducingGapResize: "box, epsilon", (((0, 0, 512, 512), 5.5), ((0.9, 1.7, 128, 128), 9.5)), ) - def test_box_filter(self, gradients_image, box, epsilon): + def test_box_filter( + self, + gradients_image: Image.Image, + box: tuple[float, float, float, float], + epsilon: float, + ) -> None: ref = gradients_image.resize((52, 34), Image.Resampling.BOX, box=box) im = gradients_image.resize( (52, 34), Image.Resampling.BOX, box=box, reducing_gap=1.0 @@ -255,8 +284,8 @@ class TestReducingGapResize: class TestImageResize: - def test_resize(self): - def resize(mode, size): + def test_resize(self) -> None: + def resize(mode: str, size: tuple[int, int]) -> None: out = hopper(mode).resize(size) assert out.mode == mode assert out.size == size @@ -271,7 +300,7 @@ class TestImageResize: im.resize((10, 10), "unknown") @skip_unless_feature("libtiff") - def test_load_first(self): + def test_load_first(self) -> None: # load() may change the size of the image # Test that resize() is calling it before getting the size with Image.open("Tests/images/g4_orientation_5.tif") as im: @@ -279,13 +308,13 @@ class TestImageResize: assert im.size == (64, 64) @pytest.mark.parametrize("mode", ("L", "RGB", "I", "F")) - def test_default_filter_bicubic(self, mode): + def test_default_filter_bicubic(self, mode: str) -> None: im = hopper(mode) assert im.resize((20, 20), Image.Resampling.BICUBIC) == im.resize((20, 20)) @pytest.mark.parametrize( "mode", ("1", "P", "I;16", "I;16L", "I;16B", "BGR;15", "BGR;16") ) - def test_default_filter_nearest(self, mode): + def test_default_filter_nearest(self, mode: str) -> None: im = hopper(mode) assert im.resize((20, 20), Image.Resampling.NEAREST) == im.resize((20, 20)) diff --git a/Tests/test_image_rotate.py b/Tests/test_image_rotate.py index e63fef2c1..c10c96da6 100644 --- a/Tests/test_image_rotate.py +++ b/Tests/test_image_rotate.py @@ -12,7 +12,13 @@ from .helper import ( ) -def rotate(im, mode, angle, center=None, translate=None): +def rotate( + im: Image.Image, + mode: str, + angle: int, + center: tuple[int, int] | None = None, + translate: tuple[int, int] | None = None, +) -> None: out = im.rotate(angle, center=center, translate=translate) assert out.mode == mode assert out.size == im.size # default rotate clips output @@ -27,13 +33,13 @@ def rotate(im, mode, angle, center=None, translate=None): @pytest.mark.parametrize("mode", ("1", "P", "L", "RGB", "I", "F")) -def test_mode(mode): +def test_mode(mode: str) -> None: im = hopper(mode) rotate(im, mode, 45) @pytest.mark.parametrize("angle", (0, 90, 180, 270)) -def test_angle(angle): +def test_angle(angle: int) -> None: with Image.open("Tests/images/test-card.png") as im: rotate(im, im.mode, angle) @@ -42,12 +48,12 @@ def test_angle(angle): @pytest.mark.parametrize("angle", (0, 45, 90, 180, 270)) -def test_zero(angle): +def test_zero(angle: int) -> None: im = Image.new("RGB", (0, 0)) rotate(im, im.mode, angle) -def test_resample(): +def test_resample() -> None: # Target image creation, inspected by eye. # >>> im = Image.open('Tests/images/hopper.ppm') # >>> im = im.rotate(45, resample=Image.Resampling.BICUBIC, expand=True) @@ -64,7 +70,7 @@ def test_resample(): assert_image_similar(im, target, epsilon) -def test_center_0(): +def test_center_0() -> None: im = hopper() im = im.rotate(45, center=(0, 0), resample=Image.Resampling.BICUBIC) @@ -75,7 +81,7 @@ def test_center_0(): assert_image_similar(im, target, 15) -def test_center_14(): +def test_center_14() -> None: im = hopper() im = im.rotate(45, center=(14, 14), resample=Image.Resampling.BICUBIC) @@ -86,7 +92,7 @@ def test_center_14(): assert_image_similar(im, target, 10) -def test_translate(): +def test_translate() -> None: im = hopper() with Image.open("Tests/images/hopper_45.png") as target: target_origin = (target.size[1] / 2 - 64) - 5 @@ -99,7 +105,7 @@ def test_translate(): assert_image_similar(im, target, 1) -def test_fastpath_center(): +def test_fastpath_center() -> None: # if the center is -1,-1 and we rotate by 90<=x<=270 the # resulting image should be black for angle in (90, 180, 270): @@ -107,7 +113,7 @@ def test_fastpath_center(): assert_image_equal(im, Image.new("RGB", im.size, "black")) -def test_fastpath_translate(): +def test_fastpath_translate() -> None: # if we post-translate by -128 # resulting image should be black for angle in (0, 90, 180, 270): @@ -115,26 +121,26 @@ def test_fastpath_translate(): assert_image_equal(im, Image.new("RGB", im.size, "black")) -def test_center(): +def test_center() -> None: im = hopper() rotate(im, im.mode, 45, center=(0, 0)) rotate(im, im.mode, 45, translate=(im.size[0] / 2, 0)) rotate(im, im.mode, 45, center=(0, 0), translate=(im.size[0] / 2, 0)) -def test_rotate_no_fill(): +def test_rotate_no_fill() -> None: im = Image.new("RGB", (100, 100), "green") im = im.rotate(45) assert_image_equal_tofile(im, "Tests/images/rotate_45_no_fill.png") -def test_rotate_with_fill(): +def test_rotate_with_fill() -> None: im = Image.new("RGB", (100, 100), "green") im = im.rotate(45, fillcolor="white") assert_image_equal_tofile(im, "Tests/images/rotate_45_with_fill.png") -def test_alpha_rotate_no_fill(): +def test_alpha_rotate_no_fill() -> None: # Alpha images are handled differently internally im = Image.new("RGBA", (10, 10), "green") im = im.rotate(45, expand=1) @@ -142,7 +148,7 @@ def test_alpha_rotate_no_fill(): assert corner == (0, 0, 0, 0) -def test_alpha_rotate_with_fill(): +def test_alpha_rotate_with_fill() -> None: # Alpha images are handled differently internally im = Image.new("RGBA", (10, 10), "green") im = im.rotate(45, expand=1, fillcolor=(255, 0, 0, 255)) diff --git a/Tests/test_image_split.py b/Tests/test_image_split.py index c39a100e7..3385f81f5 100644 --- a/Tests/test_image_split.py +++ b/Tests/test_image_split.py @@ -1,5 +1,7 @@ from __future__ import annotations +from pathlib import Path + import pytest from PIL import Image, features @@ -7,8 +9,8 @@ from PIL import Image, features from .helper import assert_image_equal, hopper -def test_split(): - def split(mode): +def test_split() -> None: + def split(mode: str) -> list[tuple[str, int, int]]: layers = hopper(mode).split() return [(i.mode, i.size[0], i.size[1]) for i in layers] @@ -36,18 +38,18 @@ def test_split(): @pytest.mark.parametrize( "mode", ("1", "L", "I", "F", "P", "RGB", "RGBA", "CMYK", "YCbCr") ) -def test_split_merge(mode): +def test_split_merge(mode: str) -> None: expected = Image.merge(mode, hopper(mode).split()) assert_image_equal(hopper(mode), expected) -def test_split_open(tmp_path): +def test_split_open(tmp_path: Path) -> None: if features.check("zlib"): test_file = str(tmp_path / "temp.png") else: test_file = str(tmp_path / "temp.pcx") - def split_open(mode): + def split_open(mode: str) -> int: hopper(mode).save(test_file) with Image.open(test_file) as im: return len(im.split()) diff --git a/Tests/test_image_thumbnail.py b/Tests/test_image_thumbnail.py index 7fa5692aa..2ca1d2cfc 100644 --- a/Tests/test_image_thumbnail.py +++ b/Tests/test_image_thumbnail.py @@ -14,14 +14,14 @@ from .helper import ( ) -def test_sanity(): +def test_sanity() -> None: im = hopper() assert im.thumbnail((100, 100)) is None assert im.size == (100, 100) -def test_aspect(): +def test_aspect() -> None: im = Image.new("L", (128, 128)) im.thumbnail((100, 100)) assert im.size == (100, 100) @@ -67,19 +67,19 @@ def test_aspect(): assert im.size == (75, 23) # ratio is 3.260869565217 -def test_division_by_zero(): +def test_division_by_zero() -> None: im = Image.new("L", (200, 2)) im.thumbnail((75, 75)) assert im.size == (75, 1) -def test_float(): +def test_float() -> None: im = Image.new("L", (128, 128)) im.thumbnail((99.9, 99.9)) assert im.size == (99, 99) -def test_no_resize(): +def test_no_resize() -> None: # Check that draft() can resize the image to the destination size with Image.open("Tests/images/hopper.jpg") as im: im.draft(None, (64, 64)) @@ -92,7 +92,7 @@ def test_no_resize(): @skip_unless_feature("libtiff") -def test_load_first(): +def test_load_first() -> None: # load() may change the size of the image # Test that thumbnail() is calling it before performing size calculations with Image.open("Tests/images/g4_orientation_5.tif") as im: @@ -106,12 +106,12 @@ def test_load_first(): assert im.size == (590, 88) -def test_load_first_unless_jpeg(): +def test_load_first_unless_jpeg() -> None: # Test that thumbnail() still uses draft() for JPEG with Image.open("Tests/images/hopper.jpg") as im: draft = im.draft - def im_draft(mode, size): + def im_draft(mode: str, size: tuple[int, int]): result = draft(mode, size) assert result is not None @@ -124,7 +124,7 @@ def test_load_first_unless_jpeg(): # valgrind test is failing with memory allocated in libjpeg @pytest.mark.valgrind_known_error(reason="Known Failing") -def test_DCT_scaling_edges(): +def test_DCT_scaling_edges() -> None: # Make an image with red borders and size (N * 8) + 1 to cross DCT grid im = Image.new("RGB", (257, 257), "red") im.paste(Image.new("RGB", (235, 235)), (11, 11)) @@ -138,7 +138,7 @@ def test_DCT_scaling_edges(): assert_image_similar(thumb, ref, 1.5) -def test_reducing_gap_values(): +def test_reducing_gap_values() -> None: im = hopper() im.thumbnail((18, 18), Image.Resampling.BICUBIC) @@ -155,7 +155,7 @@ def test_reducing_gap_values(): assert_image_similar(ref, im, 3.5) -def test_reducing_gap_for_DCT_scaling(): +def test_reducing_gap_for_DCT_scaling() -> None: with Image.open("Tests/images/hopper.jpg") as ref: # thumbnail should call draft with reducing_gap scale ref.draft(None, (18 * 3, 18 * 3)) diff --git a/Tests/test_image_tobitmap.py b/Tests/test_image_tobitmap.py index 89a41cf8e..f7a3cc41d 100644 --- a/Tests/test_image_tobitmap.py +++ b/Tests/test_image_tobitmap.py @@ -5,7 +5,7 @@ import pytest from .helper import assert_image_equal, fromstring, hopper -def test_sanity(): +def test_sanity() -> None: with pytest.raises(ValueError): hopper().tobitmap() diff --git a/Tests/test_image_tobytes.py b/Tests/test_image_tobytes.py index 8f15adac0..d32b6c09b 100644 --- a/Tests/test_image_tobytes.py +++ b/Tests/test_image_tobytes.py @@ -3,6 +3,6 @@ from __future__ import annotations from .helper import hopper -def test_sanity(): +def test_sanity() -> None: data = hopper().tobytes() assert isinstance(data, bytes) diff --git a/Tests/test_image_transform.py b/Tests/test_image_transform.py index 0fe9fd1d5..638d12247 100644 --- a/Tests/test_image_transform.py +++ b/Tests/test_image_transform.py @@ -1,6 +1,7 @@ from __future__ import annotations import math +from typing import Callable import pytest @@ -10,7 +11,7 @@ from .helper import assert_image_equal, assert_image_similar, hopper class TestImageTransform: - def test_sanity(self): + def test_sanity(self) -> None: im = hopper() for transform in ( @@ -31,7 +32,7 @@ class TestImageTransform: ): assert_image_equal(im, im.transform(im.size, transform)) - def test_info(self): + def test_info(self) -> None: comment = b"File written by Adobe Photoshop\xa8 4.0" with Image.open("Tests/images/hopper.gif") as im: @@ -41,14 +42,14 @@ class TestImageTransform: new_im = im.transform((100, 100), transform) assert new_im.info["comment"] == comment - def test_palette(self): + def test_palette(self) -> None: with Image.open("Tests/images/hopper.gif") as im: transformed = im.transform( im.size, Image.Transform.AFFINE, [1, 0, 0, 0, 1, 0] ) assert im.palette.palette == transformed.palette.palette - def test_extent(self): + def test_extent(self) -> None: im = hopper("RGB") (w, h) = im.size transformed = im.transform( @@ -63,7 +64,7 @@ class TestImageTransform: # undone -- precision? assert_image_similar(transformed, scaled, 23) - def test_quad(self): + def test_quad(self) -> None: # one simple quad transform, equivalent to scale & crop upper left quad im = hopper("RGB") (w, h) = im.size @@ -91,7 +92,7 @@ class TestImageTransform: ("LA", (76, 0)), ), ) - def test_fill(self, mode, expected_pixel): + def test_fill(self, mode: str, expected_pixel: tuple[int, ...]) -> None: im = hopper(mode) (w, h) = im.size transformed = im.transform( @@ -103,7 +104,7 @@ class TestImageTransform: ) assert transformed.getpixel((w - 1, h - 1)) == expected_pixel - def test_mesh(self): + def test_mesh(self) -> None: # this should be a checkerboard of halfsized hoppers in ul, lr im = hopper("RGBA") (w, h) = im.size @@ -142,7 +143,9 @@ class TestImageTransform: assert_image_equal(blank, transformed.crop((w // 2, 0, w, h // 2))) assert_image_equal(blank, transformed.crop((0, h // 2, w // 2, h))) - def _test_alpha_premult(self, op): + def _test_alpha_premult( + self, op: Callable[[Image.Image, tuple[int, int]], Image.Image] + ) -> None: # create image with half white, half black, # with the black half transparent. # do op, @@ -158,14 +161,14 @@ class TestImageTransform: hist = im_background.histogram() assert 40 * 10 == hist[-1] - def test_alpha_premult_resize(self): - def op(im, sz): + def test_alpha_premult_resize(self) -> None: + def op(im: Image.Image, sz: tuple[int, int]) -> Image.Image: return im.resize(sz, Image.Resampling.BILINEAR) self._test_alpha_premult(op) - def test_alpha_premult_transform(self): - def op(im, sz): + def test_alpha_premult_transform(self) -> None: + def op(im: Image.Image, sz: tuple[int, int]) -> Image.Image: (w, h) = im.size return im.transform( sz, Image.Transform.EXTENT, (0, 0, w, h), Image.Resampling.BILINEAR @@ -173,7 +176,9 @@ class TestImageTransform: self._test_alpha_premult(op) - def _test_nearest(self, op, mode): + def _test_nearest( + self, op: Callable[[Image.Image, tuple[int, int]], Image.Image], mode: str + ) -> None: # create white image with half transparent, # do op, # the image should remain white with half transparent @@ -196,15 +201,15 @@ class TestImageTransform: ) @pytest.mark.parametrize("mode", ("RGBA", "LA")) - def test_nearest_resize(self, mode): - def op(im, sz): + def test_nearest_resize(self, mode: str) -> None: + def op(im: Image.Image, sz: tuple[int, int]) -> Image.Image: return im.resize(sz, Image.Resampling.NEAREST) self._test_nearest(op, mode) @pytest.mark.parametrize("mode", ("RGBA", "LA")) - def test_nearest_transform(self, mode): - def op(im, sz): + def test_nearest_transform(self, mode: str) -> None: + def op(im: Image.Image, sz: tuple[int, int]) -> Image.Image: (w, h) = im.size return im.transform( sz, Image.Transform.EXTENT, (0, 0, w, h), Image.Resampling.NEAREST @@ -212,7 +217,7 @@ class TestImageTransform: self._test_nearest(op, mode) - def test_blank_fill(self): + def test_blank_fill(self) -> None: # attempting to hit # https://github.com/python-pillow/Pillow/issues/254 reported # @@ -227,20 +232,22 @@ class TestImageTransform: # Running by default, but I'd totally understand not doing it in # the future - pattern = [Image.new("RGBA", (1024, 1024), (a, a, a, a)) for a in range(1, 65)] + pattern: list[Image.Image] | None = [ + Image.new("RGBA", (1024, 1024), (a, a, a, a)) for a in range(1, 65) + ] # Yeah. Watch some JIT optimize this out. pattern = None # noqa: F841 self.test_mesh() - def test_missing_method_data(self): + def test_missing_method_data(self) -> None: with hopper() as im: with pytest.raises(ValueError): im.transform((100, 100), None) @pytest.mark.parametrize("resample", (Image.Resampling.BOX, "unknown")) - def test_unknown_resampling_filter(self, resample): + def test_unknown_resampling_filter(self, resample: Image.Resampling | str) -> None: with hopper() as im: (w, h) = im.size with pytest.raises(ValueError): @@ -250,7 +257,7 @@ class TestImageTransform: class TestImageTransformAffine: transform = Image.Transform.AFFINE - def _test_image(self): + def _test_image(self) -> Image.Image: im = hopper("RGB") return im.crop((10, 20, im.width - 10, im.height - 20)) @@ -263,7 +270,7 @@ class TestImageTransformAffine: (270, Image.Transpose.ROTATE_270), ), ) - def test_rotate(self, deg, transpose): + def test_rotate(self, deg: int, transpose: Image.Transpose | None) -> None: im = self._test_image() angle = -math.radians(deg) @@ -313,7 +320,13 @@ class TestImageTransformAffine: (Image.Resampling.BICUBIC, 1), ), ) - def test_resize(self, scale, epsilon_scale, resample, epsilon): + def test_resize( + self, + scale: float, + epsilon_scale: float, + resample: Image.Resampling, + epsilon: int, + ) -> None: im = self._test_image() size_up = int(round(im.width * scale)), int(round(im.height * scale)) @@ -342,7 +355,14 @@ class TestImageTransformAffine: (Image.Resampling.BICUBIC, 1), ), ) - def test_translate(self, x, y, epsilon_scale, resample, epsilon): + def test_translate( + self, + x: float, + y: float, + epsilon_scale: float, + resample: Image.Resampling, + epsilon: float, + ) -> None: im = self._test_image() size_up = int(round(im.width + x)), int(round(im.height + y)) diff --git a/Tests/test_image_transpose.py b/Tests/test_image_transpose.py index 01bf5a839..d384d8141 100644 --- a/Tests/test_image_transpose.py +++ b/Tests/test_image_transpose.py @@ -2,6 +2,7 @@ from __future__ import annotations import pytest +from PIL import Image from PIL.Image import Transpose from . import helper @@ -14,7 +15,7 @@ HOPPER = { @pytest.mark.parametrize("mode", HOPPER) -def test_flip_left_right(mode): +def test_flip_left_right(mode: str) -> None: im = HOPPER[mode] out = im.transpose(Transpose.FLIP_LEFT_RIGHT) assert out.mode == mode @@ -28,7 +29,7 @@ def test_flip_left_right(mode): @pytest.mark.parametrize("mode", HOPPER) -def test_flip_top_bottom(mode): +def test_flip_top_bottom(mode: str) -> None: im = HOPPER[mode] out = im.transpose(Transpose.FLIP_TOP_BOTTOM) assert out.mode == mode @@ -42,7 +43,7 @@ def test_flip_top_bottom(mode): @pytest.mark.parametrize("mode", HOPPER) -def test_rotate_90(mode): +def test_rotate_90(mode: str) -> None: im = HOPPER[mode] out = im.transpose(Transpose.ROTATE_90) assert out.mode == mode @@ -56,7 +57,7 @@ def test_rotate_90(mode): @pytest.mark.parametrize("mode", HOPPER) -def test_rotate_180(mode): +def test_rotate_180(mode: str) -> None: im = HOPPER[mode] out = im.transpose(Transpose.ROTATE_180) assert out.mode == mode @@ -70,7 +71,7 @@ def test_rotate_180(mode): @pytest.mark.parametrize("mode", HOPPER) -def test_rotate_270(mode): +def test_rotate_270(mode: str) -> None: im = HOPPER[mode] out = im.transpose(Transpose.ROTATE_270) assert out.mode == mode @@ -84,7 +85,7 @@ def test_rotate_270(mode): @pytest.mark.parametrize("mode", HOPPER) -def test_transpose(mode): +def test_transpose(mode: str) -> None: im = HOPPER[mode] out = im.transpose(Transpose.TRANSPOSE) assert out.mode == mode @@ -98,7 +99,7 @@ def test_transpose(mode): @pytest.mark.parametrize("mode", HOPPER) -def test_tranverse(mode): +def test_tranverse(mode: str) -> None: im = HOPPER[mode] out = im.transpose(Transpose.TRANSVERSE) assert out.mode == mode @@ -112,10 +113,10 @@ def test_tranverse(mode): @pytest.mark.parametrize("mode", HOPPER) -def test_roundtrip(mode): +def test_roundtrip(mode: str) -> None: im = HOPPER[mode] - def transpose(first, second): + def transpose(first: Transpose, second: Transpose) -> Image.Image: return im.transpose(first).transpose(second) assert_image_equal( diff --git a/Tests/test_imagechops.py b/Tests/test_imagechops.py index 2f0614385..7e2290c15 100644 --- a/Tests/test_imagechops.py +++ b/Tests/test_imagechops.py @@ -1,5 +1,7 @@ from __future__ import annotations +from typing import Callable + from PIL import Image, ImageChops from .helper import assert_image_equal, hopper @@ -15,7 +17,7 @@ WHITE = (255, 255, 255) GRAY = 128 -def test_sanity(): +def test_sanity() -> None: im = hopper("L") ImageChops.constant(im, 128) @@ -48,7 +50,7 @@ def test_sanity(): ImageChops.offset(im, 10, 20) -def test_add(): +def test_add() -> None: # Arrange with Image.open("Tests/images/imagedraw_ellipse_RGB.png") as im1: with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im2: @@ -60,7 +62,7 @@ def test_add(): assert new.getpixel((50, 50)) == ORANGE -def test_add_scale_offset(): +def test_add_scale_offset() -> None: # Arrange with Image.open("Tests/images/imagedraw_ellipse_RGB.png") as im1: with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im2: @@ -72,7 +74,7 @@ def test_add_scale_offset(): assert new.getpixel((50, 50)) == (202, 151, 100) -def test_add_clip(): +def test_add_clip() -> None: # Arrange im = hopper() @@ -83,7 +85,7 @@ def test_add_clip(): assert new.getpixel((50, 50)) == (255, 255, 254) -def test_add_modulo(): +def test_add_modulo() -> None: # Arrange with Image.open("Tests/images/imagedraw_ellipse_RGB.png") as im1: with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im2: @@ -95,7 +97,7 @@ def test_add_modulo(): assert new.getpixel((50, 50)) == ORANGE -def test_add_modulo_no_clip(): +def test_add_modulo_no_clip() -> None: # Arrange im = hopper() @@ -106,7 +108,7 @@ def test_add_modulo_no_clip(): assert new.getpixel((50, 50)) == (224, 76, 254) -def test_blend(): +def test_blend() -> None: # Arrange with Image.open("Tests/images/imagedraw_ellipse_RGB.png") as im1: with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im2: @@ -118,7 +120,7 @@ def test_blend(): assert new.getpixel((50, 50)) == BROWN -def test_constant(): +def test_constant() -> None: # Arrange im = Image.new("RGB", (20, 10)) @@ -131,7 +133,7 @@ def test_constant(): assert new.getpixel((19, 9)) == GRAY -def test_darker_image(): +def test_darker_image() -> None: # Arrange with Image.open("Tests/images/imagedraw_chord_RGB.png") as im1: with Image.open("Tests/images/imagedraw_outline_chord_RGB.png") as im2: @@ -142,7 +144,7 @@ def test_darker_image(): assert_image_equal(new, im2) -def test_darker_pixel(): +def test_darker_pixel() -> None: # Arrange im1 = hopper() with Image.open("Tests/images/imagedraw_chord_RGB.png") as im2: @@ -153,7 +155,7 @@ def test_darker_pixel(): assert new.getpixel((50, 50)) == (240, 166, 0) -def test_difference(): +def test_difference() -> None: # Arrange with Image.open("Tests/images/imagedraw_arc_end_le_start.png") as im1: with Image.open("Tests/images/imagedraw_arc_no_loops.png") as im2: @@ -164,7 +166,7 @@ def test_difference(): assert new.getbbox() == (25, 25, 76, 76) -def test_difference_pixel(): +def test_difference_pixel() -> None: # Arrange im1 = hopper() with Image.open("Tests/images/imagedraw_polygon_kite_RGB.png") as im2: @@ -175,7 +177,7 @@ def test_difference_pixel(): assert new.getpixel((50, 50)) == (240, 166, 128) -def test_duplicate(): +def test_duplicate() -> None: # Arrange im = hopper() @@ -186,7 +188,7 @@ def test_duplicate(): assert_image_equal(new, im) -def test_invert(): +def test_invert() -> None: # Arrange with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im: # Act @@ -198,7 +200,7 @@ def test_invert(): assert new.getpixel((50, 50)) == CYAN -def test_lighter_image(): +def test_lighter_image() -> None: # Arrange with Image.open("Tests/images/imagedraw_chord_RGB.png") as im1: with Image.open("Tests/images/imagedraw_outline_chord_RGB.png") as im2: @@ -209,7 +211,7 @@ def test_lighter_image(): assert_image_equal(new, im1) -def test_lighter_pixel(): +def test_lighter_pixel() -> None: # Arrange im1 = hopper() with Image.open("Tests/images/imagedraw_chord_RGB.png") as im2: @@ -220,7 +222,7 @@ def test_lighter_pixel(): assert new.getpixel((50, 50)) == (255, 255, 127) -def test_multiply_black(): +def test_multiply_black() -> None: """If you multiply an image with a solid black image, the result is black.""" # Arrange @@ -234,7 +236,7 @@ def test_multiply_black(): assert_image_equal(new, black) -def test_multiply_green(): +def test_multiply_green() -> None: # Arrange with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im: green = Image.new("RGB", im.size, "green") @@ -248,7 +250,7 @@ def test_multiply_green(): assert new.getpixel((50, 50)) == BLACK -def test_multiply_white(): +def test_multiply_white() -> None: """If you multiply with a solid white image, the image is unaffected.""" # Arrange im1 = hopper() @@ -261,7 +263,7 @@ def test_multiply_white(): assert_image_equal(new, im1) -def test_offset(): +def test_offset() -> None: # Arrange xoffset = 45 yoffset = 20 @@ -278,7 +280,7 @@ def test_offset(): assert ImageChops.offset(im, xoffset) == ImageChops.offset(im, xoffset, xoffset) -def test_screen(): +def test_screen() -> None: # Arrange with Image.open("Tests/images/imagedraw_ellipse_RGB.png") as im1: with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im2: @@ -290,7 +292,7 @@ def test_screen(): assert new.getpixel((50, 50)) == ORANGE -def test_subtract(): +def test_subtract() -> None: # Arrange with Image.open("Tests/images/imagedraw_chord_RGB.png") as im1: with Image.open("Tests/images/imagedraw_outline_chord_RGB.png") as im2: @@ -303,7 +305,7 @@ def test_subtract(): assert new.getpixel((50, 52)) == BLACK -def test_subtract_scale_offset(): +def test_subtract_scale_offset() -> None: # Arrange with Image.open("Tests/images/imagedraw_chord_RGB.png") as im1: with Image.open("Tests/images/imagedraw_outline_chord_RGB.png") as im2: @@ -315,7 +317,7 @@ def test_subtract_scale_offset(): assert new.getpixel((50, 50)) == (100, 202, 100) -def test_subtract_clip(): +def test_subtract_clip() -> None: # Arrange im1 = hopper() with Image.open("Tests/images/imagedraw_chord_RGB.png") as im2: @@ -326,7 +328,7 @@ def test_subtract_clip(): assert new.getpixel((50, 50)) == (0, 0, 127) -def test_subtract_modulo(): +def test_subtract_modulo() -> None: # Arrange with Image.open("Tests/images/imagedraw_chord_RGB.png") as im1: with Image.open("Tests/images/imagedraw_outline_chord_RGB.png") as im2: @@ -339,7 +341,7 @@ def test_subtract_modulo(): assert new.getpixel((50, 52)) == BLACK -def test_subtract_modulo_no_clip(): +def test_subtract_modulo_no_clip() -> None: # Arrange im1 = hopper() with Image.open("Tests/images/imagedraw_chord_RGB.png") as im2: @@ -350,7 +352,7 @@ def test_subtract_modulo_no_clip(): assert new.getpixel((50, 50)) == (241, 167, 127) -def test_soft_light(): +def test_soft_light() -> None: # Arrange with Image.open("Tests/images/hopper.png") as im1: with Image.open("Tests/images/hopper-XYZ.png") as im2: @@ -362,7 +364,7 @@ def test_soft_light(): assert new.getpixel((15, 100)) == (1, 1, 3) -def test_hard_light(): +def test_hard_light() -> None: # Arrange with Image.open("Tests/images/hopper.png") as im1: with Image.open("Tests/images/hopper-XYZ.png") as im2: @@ -374,7 +376,7 @@ def test_hard_light(): assert new.getpixel((15, 100)) == (1, 1, 2) -def test_overlay(): +def test_overlay() -> None: # Arrange with Image.open("Tests/images/hopper.png") as im1: with Image.open("Tests/images/hopper-XYZ.png") as im2: @@ -386,8 +388,10 @@ def test_overlay(): assert new.getpixel((15, 100)) == (1, 1, 2) -def test_logical(): - def table(op, a, b): +def test_logical() -> None: + def table( + op: Callable[[Image.Image, Image.Image], Image.Image], a: int, b: int + ) -> tuple[int, int, int, int]: out = [] for x in (a, b): imx = Image.new("1", (1, 1), x) diff --git a/Tests/test_imagecms.py b/Tests/test_imagecms.py index 03332699a..6be29a70f 100644 --- a/Tests/test_imagecms.py +++ b/Tests/test_imagecms.py @@ -5,6 +5,8 @@ import os import re import shutil from io import BytesIO +from pathlib import Path +from typing import Any import pytest @@ -32,7 +34,7 @@ SRGB = "Tests/icc/sRGB_IEC61966-2-1_black_scaled.icc" HAVE_PROFILE = os.path.exists(SRGB) -def setup_module(): +def setup_module() -> None: try: from PIL import ImageCms @@ -42,12 +44,12 @@ def setup_module(): pytest.skip(str(v)) -def skip_missing(): +def skip_missing() -> None: if not HAVE_PROFILE: pytest.skip("SRGB profile not available") -def test_sanity(): +def test_sanity() -> None: # basic smoke test. # this mostly follows the cms_test outline. with pytest.warns(DeprecationWarning): @@ -91,7 +93,7 @@ def test_sanity(): hopper().point(t) -def test_flags(): +def test_flags() -> None: assert ImageCms.Flags.NONE == 0 assert ImageCms.Flags.GRIDPOINTS(0) == ImageCms.Flags.NONE assert ImageCms.Flags.GRIDPOINTS(256) == ImageCms.Flags.NONE @@ -101,7 +103,7 @@ def test_flags(): assert ImageCms.Flags.GRIDPOINTS(511) == ImageCms.Flags.GRIDPOINTS(255) -def test_name(): +def test_name() -> None: skip_missing() # get profile information for file assert ( @@ -110,7 +112,7 @@ def test_name(): ) -def test_info(): +def test_info() -> None: skip_missing() assert ImageCms.getProfileInfo(SRGB).splitlines() == [ "sRGB IEC61966-2-1 black scaled", @@ -120,7 +122,7 @@ def test_info(): ] -def test_copyright(): +def test_copyright() -> None: skip_missing() assert ( ImageCms.getProfileCopyright(SRGB).strip() @@ -128,12 +130,12 @@ def test_copyright(): ) -def test_manufacturer(): +def test_manufacturer() -> None: skip_missing() assert ImageCms.getProfileManufacturer(SRGB).strip() == "" -def test_model(): +def test_model() -> None: skip_missing() assert ( ImageCms.getProfileModel(SRGB).strip() @@ -141,14 +143,14 @@ def test_model(): ) -def test_description(): +def test_description() -> None: skip_missing() assert ( ImageCms.getProfileDescription(SRGB).strip() == "sRGB IEC61966-2-1 black scaled" ) -def test_intent(): +def test_intent() -> None: skip_missing() assert ImageCms.getDefaultIntent(SRGB) == 0 support = ImageCms.isIntentSupported( @@ -157,7 +159,7 @@ def test_intent(): assert support == 1 -def test_profile_object(): +def test_profile_object() -> None: # same, using profile object p = ImageCms.createProfile("sRGB") # assert ImageCms.getProfileName(p).strip() == "sRGB built-in - (lcms internal)" @@ -170,7 +172,7 @@ def test_profile_object(): assert support == 1 -def test_extensions(): +def test_extensions() -> None: # extensions with Image.open("Tests/images/rgb.jpg") as i: @@ -181,7 +183,7 @@ def test_extensions(): ) -def test_exceptions(): +def test_exceptions() -> None: # Test mode mismatch psRGB = ImageCms.createProfile("sRGB") pLab = ImageCms.createProfile("LAB") @@ -207,17 +209,17 @@ def test_exceptions(): ImageCms.isIntentSupported(SRGB, None, None) -def test_display_profile(): +def test_display_profile() -> None: # try fetching the profile for the current display device ImageCms.get_display_profile() -def test_lab_color_profile(): +def test_lab_color_profile() -> None: ImageCms.createProfile("LAB", 5000) ImageCms.createProfile("LAB", 6500) -def test_unsupported_color_space(): +def test_unsupported_color_space() -> None: with pytest.raises( ImageCms.PyCMSError, match=re.escape( @@ -227,7 +229,7 @@ def test_unsupported_color_space(): ImageCms.createProfile("unsupported") -def test_invalid_color_temperature(): +def test_invalid_color_temperature() -> None: with pytest.raises( ImageCms.PyCMSError, match='Color temperature must be numeric, "invalid" not valid', @@ -236,7 +238,7 @@ def test_invalid_color_temperature(): @pytest.mark.parametrize("flag", ("my string", -1)) -def test_invalid_flag(flag): +def test_invalid_flag(flag: str | int) -> None: with hopper() as im: with pytest.raises( ImageCms.PyCMSError, match="flags must be an integer between 0 and " @@ -244,7 +246,7 @@ def test_invalid_flag(flag): ImageCms.profileToProfile(im, "foo", "bar", flags=flag) -def test_simple_lab(): +def test_simple_lab() -> None: i = Image.new("RGB", (10, 10), (128, 128, 128)) psRGB = ImageCms.createProfile("sRGB") @@ -268,7 +270,7 @@ def test_simple_lab(): assert list(b_data) == [128] * 100 -def test_lab_color(): +def test_lab_color() -> None: psRGB = ImageCms.createProfile("sRGB") pLab = ImageCms.createProfile("LAB") t = ImageCms.buildTransform(psRGB, pLab, "RGB", "LAB") @@ -283,7 +285,7 @@ def test_lab_color(): assert_image_similar_tofile(i, "Tests/images/hopper.Lab.tif", 3.5) -def test_lab_srgb(): +def test_lab_srgb() -> None: psRGB = ImageCms.createProfile("sRGB") pLab = ImageCms.createProfile("LAB") t = ImageCms.buildTransform(pLab, psRGB, "LAB", "RGB") @@ -300,7 +302,7 @@ def test_lab_srgb(): assert "sRGB" in ImageCms.getProfileDescription(profile) -def test_lab_roundtrip(): +def test_lab_roundtrip() -> None: # check to see if we're at least internally consistent. psRGB = ImageCms.createProfile("sRGB") pLab = ImageCms.createProfile("LAB") @@ -317,7 +319,7 @@ def test_lab_roundtrip(): assert_image_similar(hopper(), out, 2) -def test_profile_tobytes(): +def test_profile_tobytes() -> None: with Image.open("Tests/images/rgb.jpg") as i: p = ImageCms.getOpenProfile(BytesIO(i.info["icc_profile"])) @@ -329,22 +331,26 @@ def test_profile_tobytes(): assert ImageCms.getProfileDescription(p) == ImageCms.getProfileDescription(p2) -def test_extended_information(): +def test_extended_information() -> None: skip_missing() o = ImageCms.getOpenProfile(SRGB) p = o.profile - def assert_truncated_tuple_equal(tup1, tup2, digits=10): + def assert_truncated_tuple_equal( + tup1: tuple[Any, ...], tup2: tuple[Any, ...], digits: int = 10 + ) -> None: # Helper function to reduce precision of tuples of floats # recursively and then check equality. power = 10**digits - def truncate_tuple(tuple_or_float): + def truncate_tuple(tuple_value: tuple[Any, ...]) -> tuple[Any, ...]: return tuple( - truncate_tuple(val) - if isinstance(val, tuple) - else int(val * power) / power - for val in tuple_or_float + ( + truncate_tuple(val) + if isinstance(val, tuple) + else int(val * power) / power + ) + for val in tuple_value ) assert truncate_tuple(tup1) == truncate_tuple(tup2) @@ -476,7 +482,7 @@ def test_extended_information(): assert p.xcolor_space == "RGB " -def test_non_ascii_path(tmp_path): +def test_non_ascii_path(tmp_path: Path) -> None: skip_missing() tempfile = str(tmp_path / ("temp_" + chr(128) + ".icc")) try: @@ -489,7 +495,7 @@ def test_non_ascii_path(tmp_path): assert p.model == "IEC 61966-2-1 Default RGB Colour Space - sRGB" -def test_profile_typesafety(): +def test_profile_typesafety() -> None: """Profile init type safety prepatch, these would segfault, postpatch they should emit a typeerror @@ -501,8 +507,10 @@ def test_profile_typesafety(): ImageCms.ImageCmsProfile(1).tobytes() -def assert_aux_channel_preserved(mode, transform_in_place, preserved_channel): - def create_test_image(): +def assert_aux_channel_preserved( + mode: str, transform_in_place: bool, preserved_channel: str +) -> None: + def create_test_image() -> Image.Image: # set up test image with something interesting in the tested aux channel. # fmt: off nine_grid_deltas = [ @@ -556,31 +564,31 @@ def assert_aux_channel_preserved(mode, transform_in_place, preserved_channel): assert_image_equal(source_image_aux, result_image_aux) -def test_preserve_auxiliary_channels_rgba(): +def test_preserve_auxiliary_channels_rgba() -> None: assert_aux_channel_preserved( mode="RGBA", transform_in_place=False, preserved_channel="A" ) -def test_preserve_auxiliary_channels_rgba_in_place(): +def test_preserve_auxiliary_channels_rgba_in_place() -> None: assert_aux_channel_preserved( mode="RGBA", transform_in_place=True, preserved_channel="A" ) -def test_preserve_auxiliary_channels_rgbx(): +def test_preserve_auxiliary_channels_rgbx() -> None: assert_aux_channel_preserved( mode="RGBX", transform_in_place=False, preserved_channel="X" ) -def test_preserve_auxiliary_channels_rgbx_in_place(): +def test_preserve_auxiliary_channels_rgbx_in_place() -> None: assert_aux_channel_preserved( mode="RGBX", transform_in_place=True, preserved_channel="X" ) -def test_auxiliary_channels_isolated(): +def test_auxiliary_channels_isolated() -> None: # test data in aux channels does not affect non-aux channels aux_channel_formats = [ # format, profile, color-only format, source test image @@ -630,7 +638,7 @@ def test_auxiliary_channels_isolated(): @pytest.mark.parametrize("mode", ("RGB", "RGBA", "RGBX")) -def test_rgb_lab(mode): +def test_rgb_lab(mode: str) -> None: im = Image.new(mode, (1, 1)) converted_im = im.convert("LAB") assert converted_im.getpixel((0, 0)) == (0, 128, 128) diff --git a/Tests/test_imagecolor.py b/Tests/test_imagecolor.py index b602172b6..6eea7886d 100644 --- a/Tests/test_imagecolor.py +++ b/Tests/test_imagecolor.py @@ -5,7 +5,7 @@ import pytest from PIL import Image, ImageColor -def test_hash(): +def test_hash() -> None: # short 3 components assert (255, 0, 0) == ImageColor.getrgb("#f00") assert (0, 255, 0) == ImageColor.getrgb("#0f0") @@ -57,7 +57,7 @@ def test_hash(): ImageColor.getrgb("#f00000 ") -def test_colormap(): +def test_colormap() -> None: assert (0, 0, 0) == ImageColor.getrgb("black") assert (255, 255, 255) == ImageColor.getrgb("white") assert (255, 255, 255) == ImageColor.getrgb("WHITE") @@ -66,7 +66,7 @@ def test_colormap(): ImageColor.getrgb("black ") -def test_functions(): +def test_functions() -> None: # rgb numbers assert (255, 0, 0) == ImageColor.getrgb("rgb(255,0,0)") assert (0, 255, 0) == ImageColor.getrgb("rgb(0,255,0)") @@ -160,7 +160,7 @@ def test_functions(): # look for rounding errors (based on code by Tim Hatch) -def test_rounding_errors(): +def test_rounding_errors() -> None: for color in ImageColor.colormap: expected = Image.new("RGB", (1, 1), color).convert("L").getpixel((0, 0)) actual = ImageColor.getcolor(color, "L") @@ -195,11 +195,11 @@ def test_rounding_errors(): Image.new("LA", (1, 1), "white") -def test_color_hsv(): +def test_color_hsv() -> None: assert (170, 255, 255) == ImageColor.getcolor("hsv(240, 100%, 100%)", "HSV") -def test_color_too_long(): +def test_color_too_long() -> None: # Arrange color_too_long = "hsl(" + "1" * 40 + "," + "1" * 40 + "%," + "1" * 40 + "%)" diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index 69aab4891..274753c6c 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -6,6 +6,7 @@ import os.path import pytest from PIL import Image, ImageColor, ImageDraw, ImageFont, features +from PIL._typing import Coords from .helper import ( assert_image_equal, @@ -47,7 +48,7 @@ KITE_POINTS = ( ) -def test_sanity(): +def test_sanity() -> None: im = hopper("RGB").copy() draw = ImageDraw.ImageDraw(im) @@ -59,13 +60,13 @@ def test_sanity(): draw.rectangle(list(range(4))) -def test_valueerror(): +def test_valueerror() -> None: with Image.open("Tests/images/chi.gif") as im: draw = ImageDraw.Draw(im) draw.line((0, 0), fill=(0, 0, 0)) -def test_mode_mismatch(): +def test_mode_mismatch() -> None: im = hopper("RGB").copy() with pytest.raises(ValueError): @@ -74,7 +75,7 @@ def test_mode_mismatch(): @pytest.mark.parametrize("bbox", BBOX) @pytest.mark.parametrize("start, end", ((0, 180), (0.5, 180.4))) -def test_arc(bbox, start, end): +def test_arc(bbox: Coords, start: float, end: float) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -87,7 +88,7 @@ def test_arc(bbox, start, end): @pytest.mark.parametrize("bbox", BBOX) -def test_arc_end_le_start(bbox): +def test_arc_end_le_start(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -102,7 +103,7 @@ def test_arc_end_le_start(bbox): @pytest.mark.parametrize("bbox", BBOX) -def test_arc_no_loops(bbox): +def test_arc_no_loops(bbox: Coords) -> None: # No need to go in loops # Arrange im = Image.new("RGB", (W, H)) @@ -118,7 +119,7 @@ def test_arc_no_loops(bbox): @pytest.mark.parametrize("bbox", BBOX) -def test_arc_width(bbox): +def test_arc_width(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -131,7 +132,7 @@ def test_arc_width(bbox): @pytest.mark.parametrize("bbox", BBOX) -def test_arc_width_pieslice_large(bbox): +def test_arc_width_pieslice_large(bbox: Coords) -> None: # Tests an arc with a large enough width that it is a pieslice # Arrange im = Image.new("RGB", (W, H)) @@ -145,7 +146,7 @@ def test_arc_width_pieslice_large(bbox): @pytest.mark.parametrize("bbox", BBOX) -def test_arc_width_fill(bbox): +def test_arc_width_fill(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -158,7 +159,7 @@ def test_arc_width_fill(bbox): @pytest.mark.parametrize("bbox", BBOX) -def test_arc_width_non_whole_angle(bbox): +def test_arc_width_non_whole_angle(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -171,7 +172,7 @@ def test_arc_width_non_whole_angle(bbox): assert_image_similar_tofile(im, expected, 1) -def test_arc_high(): +def test_arc_high() -> None: # Arrange im = Image.new("RGB", (200, 200)) draw = ImageDraw.Draw(im) @@ -184,7 +185,7 @@ def test_arc_high(): assert_image_equal_tofile(im, "Tests/images/imagedraw_arc_high.png") -def test_bitmap(): +def test_bitmap() -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -200,7 +201,7 @@ def test_bitmap(): @pytest.mark.parametrize("mode", ("RGB", "L")) @pytest.mark.parametrize("bbox", BBOX) -def test_chord(mode, bbox): +def test_chord(mode: str, bbox: Coords) -> None: # Arrange im = Image.new(mode, (W, H)) draw = ImageDraw.Draw(im) @@ -214,7 +215,7 @@ def test_chord(mode, bbox): @pytest.mark.parametrize("bbox", BBOX) -def test_chord_width(bbox): +def test_chord_width(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -227,7 +228,7 @@ def test_chord_width(bbox): @pytest.mark.parametrize("bbox", BBOX) -def test_chord_width_fill(bbox): +def test_chord_width_fill(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -240,7 +241,7 @@ def test_chord_width_fill(bbox): @pytest.mark.parametrize("bbox", BBOX) -def test_chord_zero_width(bbox): +def test_chord_zero_width(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -252,7 +253,7 @@ def test_chord_zero_width(bbox): assert_image_equal_tofile(im, "Tests/images/imagedraw_chord_zero_width.png") -def test_chord_too_fat(): +def test_chord_too_fat() -> None: # Arrange im = Image.new("RGB", (100, 100)) draw = ImageDraw.Draw(im) @@ -266,7 +267,7 @@ def test_chord_too_fat(): @pytest.mark.parametrize("mode", ("RGB", "L")) @pytest.mark.parametrize("bbox", BBOX) -def test_ellipse(mode, bbox): +def test_ellipse(mode: str, bbox: Coords) -> None: # Arrange im = Image.new(mode, (W, H)) draw = ImageDraw.Draw(im) @@ -280,7 +281,7 @@ def test_ellipse(mode, bbox): @pytest.mark.parametrize("bbox", BBOX) -def test_ellipse_translucent(bbox): +def test_ellipse_translucent(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im, "RGBA") @@ -293,7 +294,7 @@ def test_ellipse_translucent(bbox): assert_image_similar_tofile(im, expected, 1) -def test_ellipse_edge(): +def test_ellipse_edge() -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -305,7 +306,7 @@ def test_ellipse_edge(): assert_image_similar_tofile(im, "Tests/images/imagedraw_ellipse_edge.png", 1) -def test_ellipse_symmetric(): +def test_ellipse_symmetric() -> None: for width, bbox in ( (100, (24, 24, 75, 75)), (101, (25, 25, 75, 75)), @@ -317,7 +318,7 @@ def test_ellipse_symmetric(): @pytest.mark.parametrize("bbox", BBOX) -def test_ellipse_width(bbox): +def test_ellipse_width(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -329,7 +330,7 @@ def test_ellipse_width(bbox): assert_image_similar_tofile(im, "Tests/images/imagedraw_ellipse_width.png", 1) -def test_ellipse_width_large(): +def test_ellipse_width_large() -> None: # Arrange im = Image.new("RGB", (500, 500)) draw = ImageDraw.Draw(im) @@ -342,7 +343,7 @@ def test_ellipse_width_large(): @pytest.mark.parametrize("bbox", BBOX) -def test_ellipse_width_fill(bbox): +def test_ellipse_width_fill(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -355,7 +356,7 @@ def test_ellipse_width_fill(bbox): @pytest.mark.parametrize("bbox", BBOX) -def test_ellipse_zero_width(bbox): +def test_ellipse_zero_width(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -367,7 +368,7 @@ def test_ellipse_zero_width(bbox): assert_image_equal_tofile(im, "Tests/images/imagedraw_ellipse_zero_width.png") -def ellipse_various_sizes_helper(filled): +def ellipse_various_sizes_helper(filled: bool) -> Image.Image: ellipse_sizes = range(32) image_size = sum(ellipse_sizes) + len(ellipse_sizes) + 1 im = Image.new("RGB", (image_size, image_size)) @@ -394,13 +395,13 @@ def ellipse_various_sizes_helper(filled): return im -def test_ellipse_various_sizes(): +def test_ellipse_various_sizes() -> None: im = ellipse_various_sizes_helper(False) assert_image_equal_tofile(im, "Tests/images/imagedraw_ellipse_various_sizes.png") -def test_ellipse_various_sizes_filled(): +def test_ellipse_various_sizes_filled() -> None: im = ellipse_various_sizes_helper(True) assert_image_equal_tofile( @@ -409,7 +410,7 @@ def test_ellipse_various_sizes_filled(): @pytest.mark.parametrize("points", POINTS) -def test_line(points): +def test_line(points: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -421,7 +422,7 @@ def test_line(points): assert_image_equal_tofile(im, "Tests/images/imagedraw_line.png") -def test_shape1(): +def test_shape1() -> None: # Arrange im = Image.new("RGB", (100, 100), "white") draw = ImageDraw.Draw(im) @@ -442,7 +443,7 @@ def test_shape1(): assert_image_equal_tofile(im, "Tests/images/imagedraw_shape1.png") -def test_shape2(): +def test_shape2() -> None: # Arrange im = Image.new("RGB", (100, 100), "white") draw = ImageDraw.Draw(im) @@ -463,7 +464,7 @@ def test_shape2(): assert_image_equal_tofile(im, "Tests/images/imagedraw_shape2.png") -def test_transform(): +def test_transform() -> None: # Arrange im = Image.new("RGB", (100, 100), "white") expected = im.copy() @@ -482,7 +483,7 @@ def test_transform(): @pytest.mark.parametrize("bbox", BBOX) @pytest.mark.parametrize("start, end", ((-92, 46), (-92.2, 46.2))) -def test_pieslice(bbox, start, end): +def test_pieslice(bbox: Coords, start: float, end: float) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -495,7 +496,7 @@ def test_pieslice(bbox, start, end): @pytest.mark.parametrize("bbox", BBOX) -def test_pieslice_width(bbox): +def test_pieslice_width(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -508,7 +509,7 @@ def test_pieslice_width(bbox): @pytest.mark.parametrize("bbox", BBOX) -def test_pieslice_width_fill(bbox): +def test_pieslice_width_fill(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -522,7 +523,7 @@ def test_pieslice_width_fill(bbox): @pytest.mark.parametrize("bbox", BBOX) -def test_pieslice_zero_width(bbox): +def test_pieslice_zero_width(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -534,7 +535,7 @@ def test_pieslice_zero_width(bbox): assert_image_equal_tofile(im, "Tests/images/imagedraw_pieslice_zero_width.png") -def test_pieslice_wide(): +def test_pieslice_wide() -> None: # Arrange im = Image.new("RGB", (200, 100)) draw = ImageDraw.Draw(im) @@ -546,7 +547,7 @@ def test_pieslice_wide(): assert_image_equal_tofile(im, "Tests/images/imagedraw_pieslice_wide.png") -def test_pieslice_no_spikes(): +def test_pieslice_no_spikes() -> None: im = Image.new("RGB", (161, 161), "white") draw = ImageDraw.Draw(im) cxs = ( @@ -577,7 +578,7 @@ def test_pieslice_no_spikes(): @pytest.mark.parametrize("points", POINTS) -def test_point(points): +def test_point(points: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -589,7 +590,7 @@ def test_point(points): assert_image_equal_tofile(im, "Tests/images/imagedraw_point.png") -def test_point_I16(): +def test_point_I16() -> None: # Arrange im = Image.new("I;16", (1, 1)) draw = ImageDraw.Draw(im) @@ -602,7 +603,7 @@ def test_point_I16(): @pytest.mark.parametrize("points", POINTS) -def test_polygon(points): +def test_polygon(points: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -616,7 +617,9 @@ def test_polygon(points): @pytest.mark.parametrize("mode", ("RGB", "L")) @pytest.mark.parametrize("kite_points", KITE_POINTS) -def test_polygon_kite(mode, kite_points): +def test_polygon_kite( + mode: str, kite_points: tuple[tuple[int, int], ...] | list[tuple[int, int]] +) -> None: # Test drawing lines of different gradients (dx>dy, dy>dx) and # vertical (dx==0) and horizontal (dy==0) lines # Arrange @@ -631,7 +634,7 @@ def test_polygon_kite(mode, kite_points): assert_image_equal_tofile(im, expected) -def test_polygon_1px_high(): +def test_polygon_1px_high() -> None: # Test drawing a 1px high polygon # Arrange im = Image.new("RGB", (3, 3)) @@ -645,7 +648,7 @@ def test_polygon_1px_high(): assert_image_equal_tofile(im, expected) -def test_polygon_1px_high_translucent(): +def test_polygon_1px_high_translucent() -> None: # Test drawing a translucent 1px high polygon # Arrange im = Image.new("RGB", (4, 3)) @@ -659,7 +662,7 @@ def test_polygon_1px_high_translucent(): assert_image_equal_tofile(im, expected) -def test_polygon_translucent(): +def test_polygon_translucent() -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im, "RGBA") @@ -673,7 +676,7 @@ def test_polygon_translucent(): @pytest.mark.parametrize("bbox", BBOX) -def test_rectangle(bbox): +def test_rectangle(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -685,7 +688,7 @@ def test_rectangle(bbox): assert_image_equal_tofile(im, "Tests/images/imagedraw_rectangle.png") -def test_big_rectangle(): +def test_big_rectangle() -> None: # Test drawing a rectangle bigger than the image # Arrange im = Image.new("RGB", (W, H)) @@ -700,7 +703,7 @@ def test_big_rectangle(): @pytest.mark.parametrize("bbox", BBOX) -def test_rectangle_width(bbox): +def test_rectangle_width(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -714,7 +717,7 @@ def test_rectangle_width(bbox): @pytest.mark.parametrize("bbox", BBOX) -def test_rectangle_width_fill(bbox): +def test_rectangle_width_fill(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -728,7 +731,7 @@ def test_rectangle_width_fill(bbox): @pytest.mark.parametrize("bbox", BBOX) -def test_rectangle_zero_width(bbox): +def test_rectangle_zero_width(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -741,7 +744,7 @@ def test_rectangle_zero_width(bbox): @pytest.mark.parametrize("bbox", BBOX) -def test_rectangle_I16(bbox): +def test_rectangle_I16(bbox: Coords) -> None: # Arrange im = Image.new("I;16", (W, H)) draw = ImageDraw.Draw(im) @@ -750,11 +753,11 @@ def test_rectangle_I16(bbox): draw.rectangle(bbox, outline=0xFFFF) # Assert - assert_image_equal_tofile(im.convert("I"), "Tests/images/imagedraw_rectangle_I.png") + assert_image_equal_tofile(im, "Tests/images/imagedraw_rectangle_I.tiff") @pytest.mark.parametrize("bbox", BBOX) -def test_rectangle_translucent_outline(bbox): +def test_rectangle_translucent_outline(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im, "RGBA") @@ -772,7 +775,13 @@ def test_rectangle_translucent_outline(bbox): "xy", [(10, 20, 190, 180), ([10, 20], [190, 180]), ((10, 20), (190, 180))], ) -def test_rounded_rectangle(xy): +def test_rounded_rectangle( + xy: ( + tuple[int, int, int, int] + | tuple[list[int]] + | tuple[tuple[int, int], tuple[int, int]] + ) +) -> None: # Arrange im = Image.new("RGB", (200, 200)) draw = ImageDraw.Draw(im) @@ -788,7 +797,9 @@ def test_rounded_rectangle(xy): @pytest.mark.parametrize("top_right", (True, False)) @pytest.mark.parametrize("bottom_right", (True, False)) @pytest.mark.parametrize("bottom_left", (True, False)) -def test_rounded_rectangle_corners(top_left, top_right, bottom_right, bottom_left): +def test_rounded_rectangle_corners( + top_left: bool, top_right: bool, bottom_right: bool, bottom_left: bool +) -> None: corners = (top_left, top_right, bottom_right, bottom_left) # Arrange @@ -822,7 +833,9 @@ def test_rounded_rectangle_corners(top_left, top_right, bottom_right, bottom_lef ((10, 20, 190, 181), 85, "height"), ], ) -def test_rounded_rectangle_non_integer_radius(xy, radius, type): +def test_rounded_rectangle_non_integer_radius( + xy: tuple[int, int, int, int], radius: float, type: str +) -> None: # Arrange im = Image.new("RGB", (200, 200)) draw = ImageDraw.Draw(im) @@ -838,7 +851,7 @@ def test_rounded_rectangle_non_integer_radius(xy, radius, type): @pytest.mark.parametrize("bbox", BBOX) -def test_rounded_rectangle_zero_radius(bbox): +def test_rounded_rectangle_zero_radius(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -860,7 +873,9 @@ def test_rounded_rectangle_zero_radius(bbox): ((20, 20, 80, 80), "both"), ], ) -def test_rounded_rectangle_translucent(xy, suffix): +def test_rounded_rectangle_translucent( + xy: tuple[int, int, int, int], suffix: str +) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im, "RGBA") @@ -877,7 +892,7 @@ def test_rounded_rectangle_translucent(xy, suffix): @pytest.mark.parametrize("bbox", BBOX) -def test_floodfill(bbox): +def test_floodfill(bbox: Coords) -> None: red = ImageColor.getrgb("red") for mode, value in [("L", 1), ("RGBA", (255, 0, 0, 0)), ("RGB", red)]: @@ -910,7 +925,7 @@ def test_floodfill(bbox): @pytest.mark.parametrize("bbox", BBOX) -def test_floodfill_border(bbox): +def test_floodfill_border(bbox: Coords) -> None: # floodfill() is experimental # Arrange @@ -932,7 +947,7 @@ def test_floodfill_border(bbox): @pytest.mark.parametrize("bbox", BBOX) -def test_floodfill_thresh(bbox): +def test_floodfill_thresh(bbox: Coords) -> None: # floodfill() is experimental # Arrange @@ -948,7 +963,7 @@ def test_floodfill_thresh(bbox): assert_image_equal_tofile(im, "Tests/images/imagedraw_floodfill2.png") -def test_floodfill_not_negative(): +def test_floodfill_not_negative() -> None: # floodfill() is experimental # Test that floodfill does not extend into negative coordinates @@ -966,8 +981,11 @@ def test_floodfill_not_negative(): def create_base_image_draw( - size, mode=DEFAULT_MODE, background1=WHITE, background2=GRAY -): + size: tuple[int, int], + mode: str = DEFAULT_MODE, + background1: tuple[int, int, int] = WHITE, + background2: tuple[int, int, int] = GRAY, +) -> tuple[Image.Image, ImageDraw.ImageDraw]: img = Image.new(mode, size, background1) for x in range(0, size[0]): for y in range(0, size[1]): @@ -976,7 +994,7 @@ def create_base_image_draw( return img, ImageDraw.Draw(img) -def test_square(): +def test_square() -> None: expected = os.path.join(IMAGES_PATH, "square.png") img, draw = create_base_image_draw((10, 10)) draw.polygon([(2, 2), (2, 7), (7, 7), (7, 2)], BLACK) @@ -989,7 +1007,7 @@ def test_square(): assert_image_equal_tofile(img, expected, "square as normal rectangle failed") -def test_triangle_right(): +def test_triangle_right() -> None: img, draw = create_base_image_draw((20, 20)) draw.polygon([(3, 5), (17, 5), (10, 12)], BLACK) assert_image_equal_tofile( @@ -1001,7 +1019,7 @@ def test_triangle_right(): "fill, suffix", ((BLACK, "width"), (None, "width_no_fill")), ) -def test_triangle_right_width(fill, suffix): +def test_triangle_right_width(fill: tuple[int, int, int] | None, suffix: str) -> None: img, draw = create_base_image_draw((100, 100)) draw.polygon([(15, 25), (85, 25), (50, 60)], fill, WHITE, width=5) assert_image_equal_tofile( @@ -1009,7 +1027,7 @@ def test_triangle_right_width(fill, suffix): ) -def test_line_horizontal(): +def test_line_horizontal() -> None: img, draw = create_base_image_draw((20, 20)) draw.line((5, 5, 14, 5), BLACK, 2) assert_image_equal_tofile( @@ -1047,7 +1065,7 @@ def test_line_horizontal(): ) -def test_line_h_s1_w2(): +def test_line_h_s1_w2() -> None: pytest.skip("failing") img, draw = create_base_image_draw((20, 20)) draw.line((5, 5, 14, 6), BLACK, 2) @@ -1058,7 +1076,7 @@ def test_line_h_s1_w2(): ) -def test_line_vertical(): +def test_line_vertical() -> None: img, draw = create_base_image_draw((20, 20)) draw.line((5, 5, 5, 14), BLACK, 2) assert_image_equal_tofile( @@ -1104,7 +1122,7 @@ def test_line_vertical(): ) -def test_line_oblique_45(): +def test_line_oblique_45() -> None: expected = os.path.join(IMAGES_PATH, "line_oblique_45_w3px_a.png") img, draw = create_base_image_draw((20, 20)) draw.line((5, 5, 14, 14), BLACK, 3) @@ -1126,7 +1144,7 @@ def test_line_oblique_45(): ) -def test_wide_line_dot(): +def test_wide_line_dot() -> None: # Test drawing a wide "line" from one point to another just draws a single point # Arrange im = Image.new("RGB", (W, H)) @@ -1139,7 +1157,7 @@ def test_wide_line_dot(): assert_image_similar_tofile(im, "Tests/images/imagedraw_wide_line_dot.png", 1) -def test_wide_line_larger_than_int(): +def test_wide_line_larger_than_int() -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -1233,7 +1251,7 @@ def test_wide_line_larger_than_int(): ], ], ) -def test_line_joint(xy): +def test_line_joint(xy: list[tuple[int, int]] | tuple[int, ...] | list[int]) -> None: im = Image.new("RGB", (500, 325)) draw = ImageDraw.Draw(im) @@ -1244,7 +1262,7 @@ def test_line_joint(xy): assert_image_similar_tofile(im, "Tests/images/imagedraw_line_joint_curve.png", 3) -def test_textsize_empty_string(): +def test_textsize_empty_string() -> None: # https://github.com/python-pillow/Pillow/issues/2783 # Arrange im = Image.new("RGB", (W, H)) @@ -1260,7 +1278,7 @@ def test_textsize_empty_string(): @skip_unless_feature("freetype2") -def test_textbbox_stroke(): +def test_textbbox_stroke() -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -1274,7 +1292,7 @@ def test_textbbox_stroke(): @skip_unless_feature("freetype2") -def test_stroke(): +def test_stroke() -> None: for suffix, stroke_fill in {"same": None, "different": "#0f0"}.items(): # Arrange im = Image.new("RGB", (120, 130)) @@ -1291,7 +1309,7 @@ def test_stroke(): @skip_unless_feature("freetype2") -def test_stroke_descender(): +def test_stroke_descender() -> None: # Arrange im = Image.new("RGB", (120, 130)) draw = ImageDraw.Draw(im) @@ -1305,7 +1323,7 @@ def test_stroke_descender(): @skip_unless_feature("freetype2") -def test_split_word(): +def test_split_word() -> None: # Arrange im = Image.new("RGB", (230, 55)) expected = im.copy() @@ -1326,7 +1344,7 @@ def test_split_word(): @skip_unless_feature("freetype2") -def test_stroke_multiline(): +def test_stroke_multiline() -> None: # Arrange im = Image.new("RGB", (100, 250)) draw = ImageDraw.Draw(im) @@ -1342,7 +1360,7 @@ def test_stroke_multiline(): @skip_unless_feature("freetype2") -def test_setting_default_font(): +def test_setting_default_font() -> None: # Arrange im = Image.new("RGB", (100, 250)) draw = ImageDraw.Draw(im) @@ -1359,7 +1377,7 @@ def test_setting_default_font(): assert isinstance(draw.getfont(), ImageFont.load_default().__class__) -def test_default_font_size(): +def test_default_font_size() -> None: freetype_support = features.check_module("freetype2") text = "Default font at a specific size." @@ -1386,7 +1404,7 @@ def test_default_font_size(): @pytest.mark.parametrize("bbox", BBOX) -def test_same_color_outline(bbox): +def test_same_color_outline(bbox: Coords) -> None: # Prepare shape x0, y0 = 5, 5 x1, y1 = 5, 50 @@ -1400,7 +1418,8 @@ def test_same_color_outline(bbox): # Begin for mode in ["RGB", "L"]: - for fill, outline in [["red", None], ["red", "red"], ["red", "#f00"]]: + fill = "red" + for outline in [None, "red", "#f00"]: for operation, args in { "chord": [bbox, 0, 180], "ellipse": [bbox], @@ -1415,6 +1434,7 @@ def test_same_color_outline(bbox): # Act draw_method = getattr(draw, operation) + assert isinstance(args, list) args += [fill, outline] draw_method(*args) @@ -1432,7 +1452,9 @@ def test_same_color_outline(bbox): (3, "triangle_width", {"width": 5, "outline": "yellow"}), ], ) -def test_draw_regular_polygon(n_sides, polygon_name, args): +def test_draw_regular_polygon( + n_sides: int, polygon_name: str, args: dict[str, int | str] +) -> None: im = Image.new("RGBA", size=(W, H), color=(255, 0, 0, 0)) filename = f"Tests/images/imagedraw_{polygon_name}.png" draw = ImageDraw.Draw(im) @@ -1469,7 +1491,9 @@ def test_draw_regular_polygon(n_sides, polygon_name, args): ), ], ) -def test_compute_regular_polygon_vertices(n_sides, expected_vertices): +def test_compute_regular_polygon_vertices( + n_sides: int, expected_vertices: list[tuple[float, float]] +) -> None: bounding_circle = (W // 2, H // 2, 25) vertices = ImageDraw._compute_regular_polygon_vertices(bounding_circle, n_sides, 0) assert vertices == expected_vertices @@ -1480,7 +1504,7 @@ def test_compute_regular_polygon_vertices(n_sides, expected_vertices): [ (None, (50, 50, 25), 0, TypeError, "n_sides should be an int"), (1, (50, 50, 25), 0, ValueError, "n_sides should be an int > 2"), - (3, 50, 0, TypeError, "bounding_circle should be a tuple"), + (3, 50, 0, TypeError, "bounding_circle should be a sequence"), ( 3, (50, 50, 100, 100), @@ -1521,13 +1545,13 @@ def test_compute_regular_polygon_vertices(n_sides, expected_vertices): ) def test_compute_regular_polygon_vertices_input_error_handling( n_sides, bounding_circle, rotation, expected_error, error_message -): +) -> None: with pytest.raises(expected_error) as e: ImageDraw._compute_regular_polygon_vertices(bounding_circle, n_sides, rotation) assert str(e.value) == error_message -def test_continuous_horizontal_edges_polygon(): +def test_continuous_horizontal_edges_polygon() -> None: xy = [ (2, 6), (6, 6), @@ -1546,7 +1570,7 @@ def test_continuous_horizontal_edges_polygon(): ) -def test_discontiguous_corners_polygon(): +def test_discontiguous_corners_polygon() -> None: img, draw = create_base_image_draw((84, 68)) draw.polygon(((1, 21), (34, 4), (71, 1), (38, 18)), BLACK) draw.polygon(((71, 44), (38, 27), (1, 24)), BLACK) @@ -1558,7 +1582,7 @@ def test_discontiguous_corners_polygon(): assert_image_similar_tofile(img, expected, 1) -def test_polygon2(): +def test_polygon2() -> None: im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) draw.polygon([(18, 30), (19, 31), (18, 30), (85, 30), (60, 72)], "red") @@ -1567,7 +1591,7 @@ def test_polygon2(): @pytest.mark.parametrize("xy", ((1, 1, 0, 1), (1, 1, 1, 0))) -def test_incorrectly_ordered_coordinates(xy): +def test_incorrectly_ordered_coordinates(xy: tuple[int, int, int, int]) -> None: im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) with pytest.raises(ValueError): diff --git a/Tests/test_imagedraw2.py b/Tests/test_imagedraw2.py index 004c2d768..3171eb9ae 100644 --- a/Tests/test_imagedraw2.py +++ b/Tests/test_imagedraw2.py @@ -5,6 +5,7 @@ import os.path import pytest from PIL import Image, ImageDraw, ImageDraw2, features +from PIL._typing import Coords from .helper import ( assert_image_equal, @@ -43,7 +44,7 @@ POINTS = ( FONT_PATH = "Tests/fonts/FreeMono.ttf" -def test_sanity(): +def test_sanity() -> None: im = hopper("RGB").copy() draw = ImageDraw2.Draw(im) @@ -56,7 +57,7 @@ def test_sanity(): @pytest.mark.parametrize("bbox", BBOX) -def test_ellipse(bbox): +def test_ellipse(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw2.Draw(im) @@ -70,7 +71,7 @@ def test_ellipse(bbox): assert_image_similar_tofile(im, "Tests/images/imagedraw_ellipse_RGB.png", 1) -def test_ellipse_edge(): +def test_ellipse_edge() -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw2.Draw(im) @@ -84,7 +85,7 @@ def test_ellipse_edge(): @pytest.mark.parametrize("points", POINTS) -def test_line(points): +def test_line(points: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw2.Draw(im) @@ -98,7 +99,7 @@ def test_line(points): @pytest.mark.parametrize("points", POINTS) -def test_line_pen_as_brush(points): +def test_line_pen_as_brush(points: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw2.Draw(im) @@ -114,7 +115,7 @@ def test_line_pen_as_brush(points): @pytest.mark.parametrize("points", POINTS) -def test_polygon(points): +def test_polygon(points: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw2.Draw(im) @@ -129,7 +130,7 @@ def test_polygon(points): @pytest.mark.parametrize("bbox", BBOX) -def test_rectangle(bbox): +def test_rectangle(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw2.Draw(im) @@ -143,7 +144,7 @@ def test_rectangle(bbox): assert_image_equal_tofile(im, "Tests/images/imagedraw_rectangle.png") -def test_big_rectangle(): +def test_big_rectangle() -> None: # Test drawing a rectangle bigger than the image # Arrange im = Image.new("RGB", (W, H)) @@ -160,7 +161,7 @@ def test_big_rectangle(): @skip_unless_feature("freetype2") -def test_text(): +def test_text() -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw2.Draw(im) @@ -175,7 +176,7 @@ def test_text(): @skip_unless_feature("freetype2") -def test_textbbox(): +def test_textbbox() -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw2.Draw(im) @@ -190,7 +191,7 @@ def test_textbbox(): @skip_unless_feature("freetype2") -def test_textsize_empty_string(): +def test_textsize_empty_string() -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw2.Draw(im) @@ -206,7 +207,7 @@ def test_textsize_empty_string(): @skip_unless_feature("freetype2") -def test_flush(): +def test_flush() -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw2.Draw(im) diff --git a/Tests/test_imageenhance.py b/Tests/test_imageenhance.py index e3d8a7ab2..6ebc61e1b 100644 --- a/Tests/test_imageenhance.py +++ b/Tests/test_imageenhance.py @@ -7,7 +7,7 @@ from PIL import Image, ImageEnhance from .helper import assert_image_equal, hopper -def test_sanity(): +def test_sanity() -> None: # FIXME: assert_image # Implicit asserts no exception: ImageEnhance.Color(hopper()).enhance(0.5) @@ -16,13 +16,13 @@ def test_sanity(): ImageEnhance.Sharpness(hopper()).enhance(0.5) -def test_crash(): +def test_crash() -> None: # crashes on small images im = Image.new("RGB", (1, 1)) ImageEnhance.Sharpness(im).enhance(0.5) -def _half_transparent_image(): +def _half_transparent_image() -> Image.Image: # returns an image, half transparent, half solid im = hopper("RGB") @@ -34,7 +34,9 @@ def _half_transparent_image(): return im -def _check_alpha(im, original, op, amount): +def _check_alpha( + im: Image.Image, original: Image.Image, op: str, amount: float +) -> None: assert im.getbands() == original.getbands() assert_image_equal( im.getchannel("A"), @@ -44,7 +46,7 @@ def _check_alpha(im, original, op, amount): @pytest.mark.parametrize("op", ("Color", "Brightness", "Contrast", "Sharpness")) -def test_alpha(op): +def test_alpha(op: str) -> None: # Issue https://github.com/python-pillow/Pillow/issues/899 # Is alpha preserved through image enhancement? diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py index 99731f352..44521a8b3 100644 --- a/Tests/test_imagefile.py +++ b/Tests/test_imagefile.py @@ -30,8 +30,8 @@ SAFEBLOCK = ImageFile.SAFEBLOCK class TestImageFile: - def test_parser(self): - def roundtrip(format): + def test_parser(self) -> None: + def roundtrip(format: str) -> tuple[Image.Image, Image.Image]: im = hopper("L").resize((1000, 1000), Image.Resampling.NEAREST) if format in ("MSP", "XBM"): im = im.convert("1") @@ -84,7 +84,7 @@ class TestImageFile: with pytest.raises(OSError): roundtrip("PDF") - def test_ico(self): + def test_ico(self) -> None: with open("Tests/images/python.ico", "rb") as f: data = f.read() with ImageFile.Parser() as p: @@ -93,7 +93,7 @@ class TestImageFile: @skip_unless_feature("webp") @skip_unless_feature("webp_anim") - def test_incremental_webp(self): + def test_incremental_webp(self) -> None: with ImageFile.Parser() as p: with open("Tests/images/hopper.webp", "rb") as f: p.feed(f.read(1024)) @@ -105,7 +105,7 @@ class TestImageFile: assert (128, 128) == p.image.size @skip_unless_feature("zlib") - def test_safeblock(self): + def test_safeblock(self) -> None: im1 = hopper() try: @@ -116,17 +116,17 @@ class TestImageFile: assert_image_equal(im1, im2) - def test_raise_oserror(self): + def test_raise_oserror(self) -> None: with pytest.warns(DeprecationWarning): with pytest.raises(OSError): ImageFile.raise_oserror(1) - def test_raise_typeerror(self): + def test_raise_typeerror(self) -> None: with pytest.raises(TypeError): parser = ImageFile.Parser() parser.feed(1) - def test_negative_stride(self): + def test_negative_stride(self) -> None: with open("Tests/images/raw_negative_stride.bin", "rb") as f: input = f.read() p = ImageFile.Parser() @@ -134,11 +134,11 @@ class TestImageFile: with pytest.raises(OSError): p.close() - def test_no_format(self): + def test_no_format(self) -> None: buf = BytesIO(b"\x00" * 255) class DummyImageFile(ImageFile.ImageFile): - def _open(self): + def _open(self) -> None: self._mode = "RGB" self._size = (1, 1) @@ -146,12 +146,12 @@ class TestImageFile: assert im.format is None assert im.get_format_mimetype() is None - def test_oserror(self): + def test_oserror(self) -> None: im = Image.new("RGB", (1, 1)) with pytest.raises(OSError): im.save(BytesIO(), "JPEG2000", num_resolutions=2) - def test_truncated(self): + def test_truncated(self) -> None: b = BytesIO( b"BM000000000000" # head_data + _binary.o32le( @@ -166,7 +166,7 @@ class TestImageFile: assert str(e.value) == "Truncated File Read" @skip_unless_feature("zlib") - def test_truncated_with_errors(self): + def test_truncated_with_errors(self) -> None: with Image.open("Tests/images/truncated_image.png") as im: with pytest.raises(OSError): im.load() @@ -176,7 +176,7 @@ class TestImageFile: im.load() @skip_unless_feature("zlib") - def test_truncated_without_errors(self): + def test_truncated_without_errors(self) -> None: with Image.open("Tests/images/truncated_image.png") as im: ImageFile.LOAD_TRUNCATED_IMAGES = True try: @@ -185,13 +185,13 @@ class TestImageFile: ImageFile.LOAD_TRUNCATED_IMAGES = False @skip_unless_feature("zlib") - def test_broken_datastream_with_errors(self): + def test_broken_datastream_with_errors(self) -> None: with Image.open("Tests/images/broken_data_stream.png") as im: with pytest.raises(OSError): im.load() @skip_unless_feature("zlib") - def test_broken_datastream_without_errors(self): + def test_broken_datastream_without_errors(self) -> None: with Image.open("Tests/images/broken_data_stream.png") as im: ImageFile.LOAD_TRUNCATED_IMAGES = True try: @@ -210,7 +210,7 @@ class MockPyEncoder(ImageFile.PyEncoder): def encode(self, buffer): return 1, 1, b"" - def cleanup(self): + def cleanup(self) -> None: self.cleanup_called = True @@ -218,7 +218,7 @@ xoff, yoff, xsize, ysize = 10, 20, 100, 100 class MockImageFile(ImageFile.ImageFile): - def _open(self): + def _open(self) -> None: self.rawmode = "RGBA" self._mode = "RGBA" self._size = (200, 200) @@ -227,7 +227,7 @@ class MockImageFile(ImageFile.ImageFile): class CodecsTest: @classmethod - def setup_class(cls): + def setup_class(cls) -> None: cls.decoder = MockPyDecoder(None) cls.encoder = MockPyEncoder(None) @@ -244,7 +244,7 @@ class CodecsTest: class TestPyDecoder(CodecsTest): - def test_setimage(self): + def test_setimage(self) -> None: buf = BytesIO(b"\x00" * 255) im = MockImageFile(buf) @@ -259,7 +259,7 @@ class TestPyDecoder(CodecsTest): with pytest.raises(ValueError): self.decoder.set_as_raw(b"\x00") - def test_extents_none(self): + def test_extents_none(self) -> None: buf = BytesIO(b"\x00" * 255) im = MockImageFile(buf) @@ -272,7 +272,7 @@ class TestPyDecoder(CodecsTest): assert self.decoder.state.xsize == 200 assert self.decoder.state.ysize == 200 - def test_negsize(self): + def test_negsize(self) -> None: buf = BytesIO(b"\x00" * 255) im = MockImageFile(buf) @@ -285,7 +285,7 @@ class TestPyDecoder(CodecsTest): with pytest.raises(ValueError): im.load() - def test_oversize(self): + def test_oversize(self) -> None: buf = BytesIO(b"\x00" * 255) im = MockImageFile(buf) @@ -298,14 +298,14 @@ class TestPyDecoder(CodecsTest): with pytest.raises(ValueError): im.load() - def test_decode(self): + def test_decode(self) -> None: decoder = ImageFile.PyDecoder(None) with pytest.raises(NotImplementedError): decoder.decode(None) class TestPyEncoder(CodecsTest): - def test_setimage(self): + def test_setimage(self) -> None: buf = BytesIO(b"\x00" * 255) im = MockImageFile(buf) @@ -320,7 +320,7 @@ class TestPyEncoder(CodecsTest): assert self.encoder.state.xsize == xsize assert self.encoder.state.ysize == ysize - def test_extents_none(self): + def test_extents_none(self) -> None: buf = BytesIO(b"\x00" * 255) im = MockImageFile(buf) @@ -334,7 +334,7 @@ class TestPyEncoder(CodecsTest): assert self.encoder.state.xsize == 200 assert self.encoder.state.ysize == 200 - def test_negsize(self): + def test_negsize(self) -> None: buf = BytesIO(b"\x00" * 255) im = MockImageFile(buf) @@ -352,7 +352,7 @@ class TestPyEncoder(CodecsTest): im, fp, [("MOCK", (xoff, yoff, xoff + xsize, -10), 0, "RGB")] ) - def test_oversize(self): + def test_oversize(self) -> None: buf = BytesIO(b"\x00" * 255) im = MockImageFile(buf) @@ -372,7 +372,7 @@ class TestPyEncoder(CodecsTest): [("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize + 100), 0, "RGB")], ) - def test_encode(self): + def test_encode(self) -> None: encoder = ImageFile.PyEncoder(None) with pytest.raises(NotImplementedError): encoder.encode(None) @@ -388,6 +388,6 @@ class TestPyEncoder(CodecsTest): with pytest.raises(NotImplementedError): encoder.encode_to_file(None, None) - def test_zero_height(self): + def test_zero_height(self) -> None: with pytest.raises(UnidentifiedImageError): Image.open("Tests/images/zero_height.j2k") diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index d2c87d42a..05b5d4716 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -7,11 +7,13 @@ import shutil import sys from io import BytesIO from pathlib import Path +from typing import Any, BinaryIO import pytest from packaging.version import parse as parse_version from PIL import Image, ImageDraw, ImageFont, features +from PIL._typing import StrOrBytesPath from .helper import ( assert_image_equal, @@ -31,7 +33,7 @@ TEST_TEXT = "hey you\nyou are awesome\nthis looks awkward" pytestmark = skip_unless_feature("freetype2") -def test_sanity(): +def test_sanity() -> None: assert re.search(r"\d+\.\d+\.\d+$", features.version_module("freetype2")) @@ -42,16 +44,16 @@ def test_sanity(): pytest.param(ImageFont.Layout.RAQM, marks=skip_unless_feature("raqm")), ], ) -def layout_engine(request): +def layout_engine(request: pytest.FixtureRequest) -> ImageFont.Layout: return request.param @pytest.fixture(scope="module") -def font(layout_engine): +def font(layout_engine: ImageFont.Layout) -> ImageFont.FreeTypeFont: return ImageFont.truetype(FONT_PATH, FONT_SIZE, layout_engine=layout_engine) -def test_font_properties(font): +def test_font_properties(font: ImageFont.FreeTypeFont) -> None: assert font.path == FONT_PATH assert font.size == FONT_SIZE @@ -67,7 +69,9 @@ def test_font_properties(font): assert font_copy.path == second_font_path -def _render(font, layout_engine): +def _render( + font: StrOrBytesPath | BinaryIO, layout_engine: ImageFont.Layout +) -> Image.Image: txt = "Hello World!" ttf = ImageFont.truetype(font, FONT_SIZE, layout_engine=layout_engine) ttf.getbbox(txt) @@ -80,12 +84,12 @@ def _render(font, layout_engine): @pytest.mark.parametrize("font", (FONT_PATH, Path(FONT_PATH))) -def test_font_with_name(layout_engine, font): +def test_font_with_name(layout_engine: ImageFont.Layout, font: str | Path) -> None: _render(font, layout_engine) -def test_font_with_filelike(layout_engine): - def _font_as_bytes(): +def test_font_with_filelike(layout_engine: ImageFont.Layout) -> None: + def _font_as_bytes() -> BytesIO: with open(FONT_PATH, "rb") as f: font_bytes = BytesIO(f.read()) return font_bytes @@ -102,12 +106,12 @@ def test_font_with_filelike(layout_engine): # _render(shared_bytes) -def test_font_with_open_file(layout_engine): +def test_font_with_open_file(layout_engine: ImageFont.Layout) -> None: with open(FONT_PATH, "rb") as f: _render(f, layout_engine) -def test_render_equal(layout_engine): +def test_render_equal(layout_engine: ImageFont.Layout) -> None: img_path = _render(FONT_PATH, layout_engine) with open(FONT_PATH, "rb") as f: font_filelike = BytesIO(f.read()) @@ -116,7 +120,7 @@ def test_render_equal(layout_engine): assert_image_equal(img_path, img_filelike) -def test_non_ascii_path(tmp_path, layout_engine): +def test_non_ascii_path(tmp_path: Path, layout_engine: ImageFont.Layout) -> None: tempfile = str(tmp_path / ("temp_" + chr(128) + ".ttf")) try: shutil.copy(FONT_PATH, tempfile) @@ -126,7 +130,7 @@ def test_non_ascii_path(tmp_path, layout_engine): ImageFont.truetype(tempfile, FONT_SIZE, layout_engine=layout_engine) -def test_transparent_background(font): +def test_transparent_background(font: ImageFont.FreeTypeFont) -> None: im = Image.new(mode="RGBA", size=(300, 100)) draw = ImageDraw.Draw(im) @@ -140,7 +144,7 @@ def test_transparent_background(font): assert_image_similar_tofile(im.convert("L"), target, 0.01) -def test_I16(font): +def test_I16(font: ImageFont.FreeTypeFont) -> None: im = Image.new(mode="I;16", size=(300, 100)) draw = ImageDraw.Draw(im) @@ -153,7 +157,7 @@ def test_I16(font): assert_image_similar_tofile(im.convert("L"), target, 0.01) -def test_textbbox_equal(font): +def test_textbbox_equal(font: ImageFont.FreeTypeFont) -> None: im = Image.new(mode="RGB", size=(300, 100)) draw = ImageDraw.Draw(im) @@ -181,8 +185,14 @@ def test_textbbox_equal(font): ), ) def test_getlength( - text, mode, fontname, size, layout_engine, length_basic, length_raqm -): + text: str, + mode: str, + fontname: str, + size: int, + layout_engine: ImageFont.Layout, + length_basic: int, + length_raqm: float, +) -> None: f = ImageFont.truetype("Tests/fonts/" + fontname, size, layout_engine=layout_engine) im = Image.new(mode, (1, 1), 0) @@ -197,7 +207,7 @@ def test_getlength( assert length == length_raqm -def test_float_size(): +def test_float_size() -> None: lengths = [] for size in (48, 48.5, 49): f = ImageFont.truetype( @@ -207,7 +217,7 @@ def test_float_size(): assert lengths[0] != lengths[1] != lengths[2] -def test_render_multiline(font): +def test_render_multiline(font: ImageFont.FreeTypeFont) -> None: im = Image.new(mode="RGB", size=(300, 100)) draw = ImageDraw.Draw(im) line_spacing = font.getbbox("A")[3] + 4 @@ -223,7 +233,7 @@ def test_render_multiline(font): assert_image_similar_tofile(im, "Tests/images/multiline_text.png", 6.2) -def test_render_multiline_text(font): +def test_render_multiline_text(font: ImageFont.FreeTypeFont) -> None: # Test that text() correctly connects to multiline_text() # and that align defaults to left im = Image.new(mode="RGB", size=(300, 100)) @@ -243,7 +253,9 @@ def test_render_multiline_text(font): @pytest.mark.parametrize( "align, ext", (("left", ""), ("center", "_center"), ("right", "_right")) ) -def test_render_multiline_text_align(font, align, ext): +def test_render_multiline_text_align( + font: ImageFont.FreeTypeFont, align: str, ext: str +) -> None: im = Image.new(mode="RGB", size=(300, 100)) draw = ImageDraw.Draw(im) draw.multiline_text((0, 0), TEST_TEXT, font=font, align=align) @@ -251,7 +263,7 @@ def test_render_multiline_text_align(font, align, ext): assert_image_similar_tofile(im, f"Tests/images/multiline_text{ext}.png", 0.01) -def test_unknown_align(font): +def test_unknown_align(font: ImageFont.FreeTypeFont) -> None: im = Image.new(mode="RGB", size=(300, 100)) draw = ImageDraw.Draw(im) @@ -260,14 +272,14 @@ def test_unknown_align(font): draw.multiline_text((0, 0), TEST_TEXT, font=font, align="unknown") -def test_draw_align(font): +def test_draw_align(font: ImageFont.FreeTypeFont) -> None: im = Image.new("RGB", (300, 100), "white") draw = ImageDraw.Draw(im) line = "some text" draw.text((100, 40), line, (0, 0, 0), font=font, align="left") -def test_multiline_bbox(font): +def test_multiline_bbox(font: ImageFont.FreeTypeFont) -> None: im = Image.new(mode="RGB", size=(300, 100)) draw = ImageDraw.Draw(im) @@ -285,7 +297,7 @@ def test_multiline_bbox(font): draw.textbbox((0, 0), TEST_TEXT, font=font, spacing=4) -def test_multiline_width(font): +def test_multiline_width(font: ImageFont.FreeTypeFont) -> None: im = Image.new(mode="RGB", size=(300, 100)) draw = ImageDraw.Draw(im) @@ -295,7 +307,7 @@ def test_multiline_width(font): ) -def test_multiline_spacing(font): +def test_multiline_spacing(font: ImageFont.FreeTypeFont) -> None: im = Image.new(mode="RGB", size=(300, 100)) draw = ImageDraw.Draw(im) draw.multiline_text((0, 0), TEST_TEXT, font=font, spacing=10) @@ -306,7 +318,9 @@ def test_multiline_spacing(font): @pytest.mark.parametrize( "orientation", (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270) ) -def test_rotated_transposed_font(font, orientation): +def test_rotated_transposed_font( + font: ImageFont.FreeTypeFont, orientation: Image.Transpose +) -> None: img_gray = Image.new("L", (100, 100)) draw = ImageDraw.Draw(img_gray) word = "testing" @@ -347,7 +361,9 @@ def test_rotated_transposed_font(font, orientation): Image.Transpose.FLIP_TOP_BOTTOM, ), ) -def test_unrotated_transposed_font(font, orientation): +def test_unrotated_transposed_font( + font: ImageFont.FreeTypeFont, orientation: Image.Transpose +) -> None: img_gray = Image.new("L", (100, 100)) draw = ImageDraw.Draw(img_gray) word = "testing" @@ -382,7 +398,9 @@ def test_unrotated_transposed_font(font, orientation): @pytest.mark.parametrize( "orientation", (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270) ) -def test_rotated_transposed_font_get_mask(font, orientation): +def test_rotated_transposed_font_get_mask( + font: ImageFont.FreeTypeFont, orientation: Image.Transpose +) -> None: # Arrange text = "mask this" transposed_font = ImageFont.TransposedFont(font, orientation=orientation) @@ -403,7 +421,9 @@ def test_rotated_transposed_font_get_mask(font, orientation): Image.Transpose.FLIP_TOP_BOTTOM, ), ) -def test_unrotated_transposed_font_get_mask(font, orientation): +def test_unrotated_transposed_font_get_mask( + font: ImageFont.FreeTypeFont, orientation: Image.Transpose +) -> None: # Arrange text = "mask this" transposed_font = ImageFont.TransposedFont(font, orientation=orientation) @@ -415,11 +435,11 @@ def test_unrotated_transposed_font_get_mask(font, orientation): assert mask.size == (108, 13) -def test_free_type_font_get_name(font): +def test_free_type_font_get_name(font: ImageFont.FreeTypeFont) -> None: assert ("FreeMono", "Regular") == font.getname() -def test_free_type_font_get_metrics(font): +def test_free_type_font_get_metrics(font: ImageFont.FreeTypeFont) -> None: ascent, descent = font.getmetrics() assert isinstance(ascent, int) @@ -427,7 +447,7 @@ def test_free_type_font_get_metrics(font): assert (ascent, descent) == (16, 4) -def test_free_type_font_get_mask(font): +def test_free_type_font_get_mask(font: ImageFont.FreeTypeFont) -> None: # Arrange text = "mask this" @@ -438,7 +458,7 @@ def test_free_type_font_get_mask(font): assert mask.size == (108, 13) -def test_load_path_not_found(): +def test_load_path_not_found() -> None: # Arrange filename = "somefilenamethatdoesntexist.ttf" @@ -449,13 +469,13 @@ def test_load_path_not_found(): ImageFont.truetype(filename) -def test_load_non_font_bytes(): +def test_load_non_font_bytes() -> None: with open("Tests/images/hopper.jpg", "rb") as f: with pytest.raises(OSError): ImageFont.truetype(f) -def test_default_font(): +def test_default_font() -> None: # Arrange txt = "This is a default font using FreeType support." im = Image.new(mode="RGB", size=(300, 100)) @@ -473,16 +493,16 @@ def test_default_font(): @pytest.mark.parametrize("mode", (None, "1", "RGBA")) -def test_getbbox(font, mode): +def test_getbbox(font: ImageFont.FreeTypeFont, mode: str | None) -> None: assert (0, 4, 12, 16) == font.getbbox("A", mode) -def test_getbbox_empty(font): +def test_getbbox_empty(font: ImageFont.FreeTypeFont) -> None: # issue #2614, should not crash. assert (0, 0, 0, 0) == font.getbbox("") -def test_render_empty(font): +def test_render_empty(font: ImageFont.FreeTypeFont) -> None: # issue 2666 im = Image.new(mode="RGB", size=(300, 100)) target = im.copy() @@ -492,7 +512,7 @@ def test_render_empty(font): assert_image_equal(im, target) -def test_unicode_extended(layout_engine): +def test_unicode_extended(layout_engine: ImageFont.Layout) -> None: # issue #3777 text = "A\u278A\U0001F12B" target = "Tests/images/unicode_extended.png" @@ -515,21 +535,23 @@ def test_unicode_extended(layout_engine): (("linux", "/usr/local/share/fonts"), ("darwin", "/System/Library/Fonts")), ) @pytest.mark.skipif(is_win32(), reason="requires Unix or macOS") -def test_find_font(monkeypatch, platform, font_directory): - def _test_fake_loading_font(path_to_fake, fontname): +def test_find_font( + monkeypatch: pytest.MonkeyPatch, platform: str, font_directory: str +) -> None: + def _test_fake_loading_font(path_to_fake: str, fontname: str) -> None: # Make a copy of FreeTypeFont so we can patch the original free_type_font = copy.deepcopy(ImageFont.FreeTypeFont) with monkeypatch.context() as m: m.setattr(ImageFont, "_FreeTypeFont", free_type_font, raising=False) - def loadable_font(filepath, size, index, encoding, *args, **kwargs): + def loadable_font( + filepath: str, size: int, index: int, encoding: str, *args: Any + ): if filepath == path_to_fake: return ImageFont._FreeTypeFont( - FONT_PATH, size, index, encoding, *args, **kwargs + FONT_PATH, size, index, encoding, *args ) - return ImageFont._FreeTypeFont( - filepath, size, index, encoding, *args, **kwargs - ) + return ImageFont._FreeTypeFont(filepath, size, index, encoding, *args) m.setattr(ImageFont, "FreeTypeFont", loadable_font) font = ImageFont.truetype(fontname) @@ -543,7 +565,7 @@ def test_find_font(monkeypatch, platform, font_directory): if platform == "linux": monkeypatch.setenv("XDG_DATA_DIRS", "/usr/share/:/usr/local/share/") - def fake_walker(path): + def fake_walker(path: str) -> list[tuple[str, list[str], list[str]]]: if path == font_directory: return [ ( @@ -567,7 +589,7 @@ def test_find_font(monkeypatch, platform, font_directory): _test_fake_loading_font(font_directory + "/Duplicate.ttf", "Duplicate") -def test_imagefont_getters(font): +def test_imagefont_getters(font: ImageFont.FreeTypeFont) -> None: assert font.getmetrics() == (16, 4) assert font.font.ascent == 16 assert font.font.descent == 4 @@ -588,7 +610,7 @@ def test_imagefont_getters(font): @pytest.mark.parametrize("stroke_width", (0, 2)) -def test_getsize_stroke(font, stroke_width): +def test_getsize_stroke(font: ImageFont.FreeTypeFont, stroke_width: int) -> None: assert font.getbbox("A", stroke_width=stroke_width) == ( 0 - stroke_width, 4 - stroke_width, @@ -597,7 +619,7 @@ def test_getsize_stroke(font, stroke_width): ) -def test_complex_font_settings(): +def test_complex_font_settings() -> None: t = ImageFont.truetype(FONT_PATH, FONT_SIZE, layout_engine=ImageFont.Layout.BASIC) with pytest.raises(KeyError): t.getmask("абвг", direction="rtl") @@ -607,7 +629,7 @@ def test_complex_font_settings(): t.getmask("абвг", language="sr") -def test_variation_get(font): +def test_variation_get(font: ImageFont.FreeTypeFont) -> None: freetype = parse_version(features.version_module("freetype2")) if freetype < parse_version("2.9.1"): with pytest.raises(NotImplementedError): @@ -662,7 +684,7 @@ def test_variation_get(font): ] -def _check_text(font, path, epsilon): +def _check_text(font: ImageFont.FreeTypeFont, path: str, epsilon: float) -> None: im = Image.new("RGB", (100, 75), "white") d = ImageDraw.Draw(im) d.text((10, 10), "Text", font=font, fill="black") @@ -677,7 +699,7 @@ def _check_text(font, path, epsilon): raise -def test_variation_set_by_name(font): +def test_variation_set_by_name(font: ImageFont.FreeTypeFont) -> None: freetype = parse_version(features.version_module("freetype2")) if freetype < parse_version("2.9.1"): with pytest.raises(NotImplementedError): @@ -702,7 +724,7 @@ def test_variation_set_by_name(font): _check_text(font, "Tests/images/variation_tiny_name.png", 40) -def test_variation_set_by_axes(font): +def test_variation_set_by_axes(font: ImageFont.FreeTypeFont) -> None: freetype = parse_version(features.version_module("freetype2")) if freetype < parse_version("2.9.1"): with pytest.raises(NotImplementedError): @@ -737,7 +759,9 @@ def test_variation_set_by_axes(font): ), ids=("ls", "ms", "rs", "ma", "mt", "mm", "mb", "md"), ) -def test_anchor(layout_engine, anchor, left, top): +def test_anchor( + layout_engine: ImageFont.Layout, anchor: str, left: int, top: int +) -> None: name, text = "quick", "Quick" path = f"Tests/images/test_anchor_{name}_{anchor}.png" @@ -782,7 +806,9 @@ def test_anchor(layout_engine, anchor, left, top): ("md", "center"), ), ) -def test_anchor_multiline(layout_engine, anchor, align): +def test_anchor_multiline( + layout_engine: ImageFont.Layout, anchor: str, align: str +) -> None: target = f"Tests/images/test_anchor_multiline_{anchor}_{align}.png" text = "a\nlong\ntext sample" @@ -800,7 +826,7 @@ def test_anchor_multiline(layout_engine, anchor, align): assert_image_similar_tofile(im, target, 4) -def test_anchor_invalid(font): +def test_anchor_invalid(font: ImageFont.FreeTypeFont) -> None: im = Image.new("RGB", (100, 100), "white") d = ImageDraw.Draw(im) d.font = font @@ -826,7 +852,7 @@ def test_anchor_invalid(font): @pytest.mark.parametrize("bpp", (1, 2, 4, 8)) -def test_bitmap_font(layout_engine, bpp): +def test_bitmap_font(layout_engine: ImageFont.Layout, bpp: int) -> None: text = "Bitmap Font" layout_name = ["basic", "raqm"][layout_engine] target = f"Tests/images/bitmap_font_{bpp}_{layout_name}.png" @@ -843,7 +869,7 @@ def test_bitmap_font(layout_engine, bpp): assert_image_equal_tofile(im, target) -def test_bitmap_font_stroke(layout_engine): +def test_bitmap_font_stroke(layout_engine: ImageFont.Layout) -> None: text = "Bitmap Font" layout_name = ["basic", "raqm"][layout_engine] target = f"Tests/images/bitmap_font_stroke_{layout_name}.png" @@ -861,7 +887,7 @@ def test_bitmap_font_stroke(layout_engine): @pytest.mark.parametrize("embedded_color", (False, True)) -def test_bitmap_blend(layout_engine, embedded_color): +def test_bitmap_blend(layout_engine: ImageFont.Layout, embedded_color: bool) -> None: font = ImageFont.truetype( "Tests/fonts/EBDTTestFont.ttf", size=64, layout_engine=layout_engine ) @@ -873,7 +899,7 @@ def test_bitmap_blend(layout_engine, embedded_color): assert_image_equal_tofile(im, "Tests/images/bitmap_font_blend.png") -def test_standard_embedded_color(layout_engine): +def test_standard_embedded_color(layout_engine: ImageFont.Layout) -> None: txt = "Hello World!" ttf = ImageFont.truetype(FONT_PATH, 40, layout_engine=layout_engine) ttf.getbbox(txt) @@ -886,7 +912,7 @@ def test_standard_embedded_color(layout_engine): @pytest.mark.parametrize("fontmode", ("1", "L", "RGBA")) -def test_float_coord(layout_engine, fontmode): +def test_float_coord(layout_engine: ImageFont.Layout, fontmode: str) -> None: txt = "Hello World!" ttf = ImageFont.truetype(FONT_PATH, 40, layout_engine=layout_engine) @@ -908,7 +934,7 @@ def test_float_coord(layout_engine, fontmode): raise -def test_cbdt(layout_engine): +def test_cbdt(layout_engine: ImageFont.Layout) -> None: try: font = ImageFont.truetype( "Tests/fonts/CBDTTestFont.ttf", size=64, layout_engine=layout_engine @@ -925,7 +951,7 @@ def test_cbdt(layout_engine): pytest.skip("freetype compiled without libpng or CBDT support") -def test_cbdt_mask(layout_engine): +def test_cbdt_mask(layout_engine: ImageFont.Layout) -> None: try: font = ImageFont.truetype( "Tests/fonts/CBDTTestFont.ttf", size=64, layout_engine=layout_engine @@ -942,7 +968,7 @@ def test_cbdt_mask(layout_engine): pytest.skip("freetype compiled without libpng or CBDT support") -def test_sbix(layout_engine): +def test_sbix(layout_engine: ImageFont.Layout) -> None: try: font = ImageFont.truetype( "Tests/fonts/chromacheck-sbix.woff", size=300, layout_engine=layout_engine @@ -959,7 +985,7 @@ def test_sbix(layout_engine): pytest.skip("freetype compiled without libpng or SBIX support") -def test_sbix_mask(layout_engine): +def test_sbix_mask(layout_engine: ImageFont.Layout) -> None: try: font = ImageFont.truetype( "Tests/fonts/chromacheck-sbix.woff", size=300, layout_engine=layout_engine @@ -977,7 +1003,7 @@ def test_sbix_mask(layout_engine): @skip_unless_feature_version("freetype2", "2.10.0") -def test_colr(layout_engine): +def test_colr(layout_engine: ImageFont.Layout) -> None: font = ImageFont.truetype( "Tests/fonts/BungeeColor-Regular_colr_Windows.ttf", size=64, @@ -993,7 +1019,7 @@ def test_colr(layout_engine): @skip_unless_feature_version("freetype2", "2.10.0") -def test_colr_mask(layout_engine): +def test_colr_mask(layout_engine: ImageFont.Layout) -> None: font = ImageFont.truetype( "Tests/fonts/BungeeColor-Regular_colr_Windows.ttf", size=64, @@ -1008,7 +1034,7 @@ def test_colr_mask(layout_engine): assert_image_similar_tofile(im, "Tests/images/colr_bungee_mask.png", 22) -def test_woff2(layout_engine): +def test_woff2(layout_engine: ImageFont.Layout) -> None: try: font = ImageFont.truetype( "Tests/fonts/OpenSans.woff2", @@ -1027,7 +1053,7 @@ def test_woff2(layout_engine): assert_image_similar_tofile(im, "Tests/images/test_woff2.png", 5) -def test_render_mono_size(): +def test_render_mono_size() -> None: # issue 4177 im = Image.new("P", (100, 30), "white") @@ -1042,7 +1068,7 @@ def test_render_mono_size(): assert_image_equal_tofile(im, "Tests/images/text_mono.gif") -def test_too_many_characters(font): +def test_too_many_characters(font: ImageFont.FreeTypeFont) -> None: with pytest.raises(ValueError): font.getlength("A" * 1_000_001) with pytest.raises(ValueError): @@ -1070,14 +1096,14 @@ def test_too_many_characters(font): "Tests/fonts/oom-4da0210eb7081b0bf15bf16cc4c52ce02c1e1bbc.ttf", ], ) -def test_oom(test_file): +def test_oom(test_file: str) -> None: with open(test_file, "rb") as f: font = ImageFont.truetype(BytesIO(f.read())) with pytest.raises(Image.DecompressionBombError): font.getmask("Test Text") -def test_raqm_missing_warning(monkeypatch): +def test_raqm_missing_warning(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(ImageFont.core, "HAVE_RAQM", False) with pytest.warns(UserWarning) as record: font = ImageFont.truetype( @@ -1091,6 +1117,8 @@ def test_raqm_missing_warning(monkeypatch): @pytest.mark.parametrize("size", [-1, 0]) -def test_invalid_truetype_sizes_raise_valueerror(layout_engine, size): +def test_invalid_truetype_sizes_raise_valueerror( + layout_engine: ImageFont.Layout, size: int +) -> None: with pytest.raises(ValueError): ImageFont.truetype(FONT_PATH, size, layout_engine=layout_engine) diff --git a/Tests/test_imagefontctl.py b/Tests/test_imagefontctl.py index 09e68ea48..24c7b871a 100644 --- a/Tests/test_imagefontctl.py +++ b/Tests/test_imagefontctl.py @@ -12,7 +12,7 @@ FONT_PATH = "Tests/fonts/DejaVuSans/DejaVuSans.ttf" pytestmark = skip_unless_feature("raqm") -def test_english(): +def test_english() -> None: # smoke test, this should not fail ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) im = Image.new(mode="RGB", size=(300, 100)) @@ -20,7 +20,7 @@ def test_english(): draw.text((0, 0), "TEST", font=ttf, fill=500, direction="ltr") -def test_complex_text(): +def test_complex_text() -> None: ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) im = Image.new(mode="RGB", size=(300, 100)) @@ -31,7 +31,7 @@ def test_complex_text(): assert_image_similar_tofile(im, target, 0.5) -def test_y_offset(): +def test_y_offset() -> None: ttf = ImageFont.truetype("Tests/fonts/NotoNastaliqUrdu-Regular.ttf", FONT_SIZE) im = Image.new(mode="RGB", size=(300, 100)) @@ -42,7 +42,7 @@ def test_y_offset(): assert_image_similar_tofile(im, target, 1.7) -def test_complex_unicode_text(): +def test_complex_unicode_text() -> None: ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) im = Image.new(mode="RGB", size=(300, 100)) @@ -62,7 +62,7 @@ def test_complex_unicode_text(): assert_image_similar_tofile(im, target, 2.33) -def test_text_direction_rtl(): +def test_text_direction_rtl() -> None: ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) im = Image.new(mode="RGB", size=(300, 100)) @@ -73,7 +73,7 @@ def test_text_direction_rtl(): assert_image_similar_tofile(im, target, 0.5) -def test_text_direction_ltr(): +def test_text_direction_ltr() -> None: ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) im = Image.new(mode="RGB", size=(300, 100)) @@ -84,7 +84,7 @@ def test_text_direction_ltr(): assert_image_similar_tofile(im, target, 0.5) -def test_text_direction_rtl2(): +def test_text_direction_rtl2() -> None: ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) im = Image.new(mode="RGB", size=(300, 100)) @@ -95,7 +95,7 @@ def test_text_direction_rtl2(): assert_image_similar_tofile(im, target, 0.5) -def test_text_direction_ttb(): +def test_text_direction_ttb() -> None: ttf = ImageFont.truetype("Tests/fonts/NotoSansJP-Regular.otf", FONT_SIZE) im = Image.new(mode="RGB", size=(100, 300)) @@ -110,7 +110,7 @@ def test_text_direction_ttb(): assert_image_similar_tofile(im, target, 2.8) -def test_text_direction_ttb_stroke(): +def test_text_direction_ttb_stroke() -> None: ttf = ImageFont.truetype("Tests/fonts/NotoSansJP-Regular.otf", 50) im = Image.new(mode="RGB", size=(100, 300)) @@ -133,7 +133,7 @@ def test_text_direction_ttb_stroke(): assert_image_similar_tofile(im, target, 19.4) -def test_ligature_features(): +def test_ligature_features() -> None: ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) im = Image.new(mode="RGB", size=(300, 100)) @@ -146,7 +146,7 @@ def test_ligature_features(): assert liga_bbox == (0, 4, 13, 19) -def test_kerning_features(): +def test_kerning_features() -> None: ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) im = Image.new(mode="RGB", size=(300, 100)) @@ -157,7 +157,7 @@ def test_kerning_features(): assert_image_similar_tofile(im, target, 0.5) -def test_arabictext_features(): +def test_arabictext_features() -> None: ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) im = Image.new(mode="RGB", size=(300, 100)) @@ -174,7 +174,7 @@ def test_arabictext_features(): assert_image_similar_tofile(im, target, 0.5) -def test_x_max_and_y_offset(): +def test_x_max_and_y_offset() -> None: ttf = ImageFont.truetype("Tests/fonts/ArefRuqaa-Regular.ttf", 40) im = Image.new(mode="RGB", size=(50, 100)) @@ -185,7 +185,7 @@ def test_x_max_and_y_offset(): assert_image_similar_tofile(im, target, 0.5) -def test_language(): +def test_language() -> None: ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) im = Image.new(mode="RGB", size=(300, 100)) @@ -208,7 +208,9 @@ def test_language(): ), ids=("None", "ltr", "rtl2", "rtl", "ttb"), ) -def test_getlength(mode, text, direction, expected): +def test_getlength( + mode: str, text: str, direction: str | None, expected: float +) -> None: ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) im = Image.new(mode, (1, 1), 0) d = ImageDraw.Draw(im) @@ -230,7 +232,7 @@ def test_getlength(mode, text, direction, expected): ("i" + ("\u030C" * 15) + "i", "i" + "\u032C" * 15 + "i", "\u035Cii", "i\u0305i"), ids=("caron-above", "caron-below", "double-breve", "overline"), ) -def test_getlength_combine(mode, direction, text): +def test_getlength_combine(mode: str, direction: str, text: str) -> None: if text == "i\u0305i" and direction == "ttb": pytest.skip("fails with this font") @@ -250,7 +252,7 @@ def test_getlength_combine(mode, direction, text): @pytest.mark.parametrize("anchor", ("lt", "mm", "rb", "sm")) -def test_anchor_ttb(anchor): +def test_anchor_ttb(anchor: str) -> None: text = "f" path = f"Tests/images/test_anchor_ttb_{text}_{anchor}.png" f = ImageFont.truetype("Tests/fonts/NotoSans-Regular.ttf", 120) @@ -306,7 +308,9 @@ combine_tests = ( @pytest.mark.parametrize( "name, text, anchor, dir, epsilon", combine_tests, ids=[r[0] for r in combine_tests] ) -def test_combine(name, text, dir, anchor, epsilon): +def test_combine( + name: str, text: str, dir: str | None, anchor: str | None, epsilon: float +) -> None: path = f"Tests/images/test_combine_{name}.png" f = ImageFont.truetype("Tests/fonts/NotoSans-Regular.ttf", 48) @@ -337,7 +341,7 @@ def test_combine(name, text, dir, anchor, epsilon): ("rm", "right"), # pass with getsize ), ) -def test_combine_multiline(anchor, align): +def test_combine_multiline(anchor: str, align: str) -> None: # test that multiline text uses getlength, not getsize or getbbox path = f"Tests/images/test_combine_multiline_{anchor}_{align}.png" @@ -355,7 +359,7 @@ def test_combine_multiline(anchor, align): assert_image_similar_tofile(im, path, 0.015) -def test_anchor_invalid_ttb(): +def test_anchor_invalid_ttb() -> None: font = ImageFont.truetype(FONT_PATH, FONT_SIZE) im = Image.new("RGB", (100, 100), "white") d = ImageDraw.Draw(im) diff --git a/Tests/test_imagefontpil.py b/Tests/test_imagefontpil.py index be4be1c54..3b1c14b4e 100644 --- a/Tests/test_imagefontpil.py +++ b/Tests/test_imagefontpil.py @@ -12,16 +12,16 @@ from .helper import assert_image_equal_tofile original_core = ImageFont.core -def setup_module(): +def setup_module() -> None: if features.check_module("freetype2"): ImageFont.core = _util.DeferredError(ImportError) -def teardown_module(): +def teardown_module() -> None: ImageFont.core = original_core -def test_default_font(): +def test_default_font() -> None: # Arrange txt = 'This is a "better than nothing" default font.' im = Image.new(mode="RGB", size=(300, 100)) @@ -35,12 +35,12 @@ def test_default_font(): assert_image_equal_tofile(im, "Tests/images/default_font.png") -def test_size_without_freetype(): +def test_size_without_freetype() -> None: with pytest.raises(ImportError): ImageFont.load_default(size=14) -def test_unicode(): +def test_unicode() -> None: # should not segfault, should return UnicodeDecodeError # issue #2826 font = ImageFont.load_default() @@ -48,7 +48,7 @@ def test_unicode(): font.getbbox("’") -def test_textbbox(): +def test_textbbox() -> None: im = Image.new("RGB", (200, 200)) d = ImageDraw.Draw(im) default_font = ImageFont.load_default() @@ -56,7 +56,7 @@ def test_textbbox(): assert d.textbbox((0, 0), "test", font=default_font) == (0, 0, 24, 11) -def test_decompression_bomb(): +def test_decompression_bomb() -> None: glyph = struct.pack(">hhhhhhhhhh", 1, 0, 0, 0, 256, 256, 0, 0, 256, 256) fp = BytesIO(b"PILfont\n\nDATA\n" + glyph * 256) @@ -67,7 +67,7 @@ def test_decompression_bomb(): @pytest.mark.timeout(4) -def test_oom(): +def test_oom() -> None: glyph = struct.pack( ">hhhhhhhhhh", 1, 0, -32767, -32767, 32767, 32767, -32767, -32767, 32767, 32767 ) diff --git a/Tests/test_imagegrab.py b/Tests/test_imagegrab.py index 9d3d40398..e23adeb70 100644 --- a/Tests/test_imagegrab.py +++ b/Tests/test_imagegrab.py @@ -20,7 +20,7 @@ class TestImageGrab: @pytest.mark.skipif( sys.platform not in ("win32", "darwin"), reason="requires Windows or macOS" ) - def test_grab(self): + def test_grab(self) -> None: ImageGrab.grab() ImageGrab.grab(include_layered_windows=True) ImageGrab.grab(all_screens=True) @@ -29,7 +29,7 @@ class TestImageGrab: assert im.size == (40, 60) @skip_unless_feature("xcb") - def test_grab_x11(self): + def test_grab_x11(self) -> None: try: if sys.platform not in ("win32", "darwin"): ImageGrab.grab() @@ -39,7 +39,7 @@ class TestImageGrab: pytest.skip(str(e)) @pytest.mark.skipif(Image.core.HAVE_XCB, reason="tests missing XCB") - def test_grab_no_xcb(self): + def test_grab_no_xcb(self) -> None: if sys.platform not in ("win32", "darwin") and not shutil.which( "gnome-screenshot" ): @@ -52,12 +52,12 @@ class TestImageGrab: assert str(e.value).startswith("Pillow was built without XCB support") @skip_unless_feature("xcb") - def test_grab_invalid_xdisplay(self): + def test_grab_invalid_xdisplay(self) -> None: with pytest.raises(OSError) as e: ImageGrab.grab(xdisplay="error.test:0.0") assert str(e.value).startswith("X connection failed") - def test_grabclipboard(self): + def test_grabclipboard(self) -> None: if sys.platform == "darwin": subprocess.call(["screencapture", "-cx"]) elif sys.platform == "win32": @@ -82,8 +82,9 @@ $bmp = New-Object Drawing.Bitmap 200, 200 ImageGrab.grabclipboard() @pytest.mark.skipif(sys.platform != "win32", reason="Windows only") - def test_grabclipboard_file(self): + def test_grabclipboard_file(self) -> None: p = subprocess.Popen(["powershell", "-command", "-"], stdin=subprocess.PIPE) + assert p.stdin is not None p.stdin.write(rb'Set-Clipboard -Path "Tests\images\hopper.gif"') p.communicate() @@ -92,8 +93,9 @@ $bmp = New-Object Drawing.Bitmap 200, 200 assert os.path.samefile(im[0], "Tests/images/hopper.gif") @pytest.mark.skipif(sys.platform != "win32", reason="Windows only") - def test_grabclipboard_png(self): + def test_grabclipboard_png(self) -> None: p = subprocess.Popen(["powershell", "-command", "-"], stdin=subprocess.PIPE) + assert p.stdin is not None p.stdin.write( rb"""$bytes = [System.IO.File]::ReadAllBytes("Tests\images\hopper.png") $ms = new-object System.IO.MemoryStream(, $bytes) @@ -113,9 +115,21 @@ $ms = new-object System.IO.MemoryStream(, $bytes) reason="Linux with wl-clipboard only", ) @pytest.mark.parametrize("ext", ("gif", "png", "ico")) - def test_grabclipboard_wl_clipboard(self, ext): + def test_grabclipboard_wl_clipboard(self, ext: str) -> None: image_path = "Tests/images/hopper." + ext with open(image_path, "rb") as fp: subprocess.call(["wl-copy"], stdin=fp) im = ImageGrab.grabclipboard() assert_image_equal_tofile(im, image_path) + + @pytest.mark.skipif( + ( + sys.platform != "linux" + or not all(shutil.which(cmd) for cmd in ("wl-paste", "wl-copy")) + ), + reason="Linux with wl-clipboard only", + ) + @pytest.mark.parametrize("arg", ("text", "--clear")) + def test_grabclipboard_wl_clipboard_errors(self, arg: str) -> None: + subprocess.call(["wl-copy", arg]) + assert ImageGrab.grabclipboard() is None diff --git a/Tests/test_imagemath.py b/Tests/test_imagemath.py index 622ad27ea..a21e2307d 100644 --- a/Tests/test_imagemath.py +++ b/Tests/test_imagemath.py @@ -5,12 +5,12 @@ import pytest from PIL import Image, ImageMath -def pixel(im): - if hasattr(im, "im"): - return f"{im.mode} {repr(im.getpixel((0, 0)))}" +def pixel(im: Image.Image | int) -> str | int: if isinstance(im, int): return int(im) # hack to deal with booleans + return f"{im.mode} {repr(im.getpixel((0, 0)))}" + A = Image.new("L", (1, 1), 1) B = Image.new("L", (1, 1), 2) @@ -24,7 +24,7 @@ B2 = B.resize((2, 2)) images = {"A": A, "B": B, "F": F, "I": I} -def test_sanity(): +def test_sanity() -> None: assert ImageMath.eval("1") == 1 assert ImageMath.eval("1+A", A=2) == 3 assert pixel(ImageMath.eval("A+B", A=A, B=B)) == "I 3" @@ -33,7 +33,7 @@ def test_sanity(): assert pixel(ImageMath.eval("int(float(A)+B)", images)) == "I 3" -def test_ops(): +def test_ops() -> None: assert pixel(ImageMath.eval("-A", images)) == "I -1" assert pixel(ImageMath.eval("+B", images)) == "L 2" @@ -60,51 +60,51 @@ def test_ops(): "(lambda: (lambda: exec('pass'))())()", ), ) -def test_prevent_exec(expression): +def test_prevent_exec(expression: str) -> None: with pytest.raises(ValueError): ImageMath.eval(expression) -def test_prevent_double_underscores(): +def test_prevent_double_underscores() -> None: with pytest.raises(ValueError): ImageMath.eval("1", {"__": None}) -def test_prevent_builtins(): +def test_prevent_builtins() -> None: with pytest.raises(ValueError): ImageMath.eval("(lambda: exec('exit()'))()", {"exec": None}) -def test_logical(): +def test_logical() -> None: assert pixel(ImageMath.eval("not A", images)) == 0 assert pixel(ImageMath.eval("A and B", images)) == "L 2" assert pixel(ImageMath.eval("A or B", images)) == "L 1" -def test_convert(): +def test_convert() -> None: assert pixel(ImageMath.eval("convert(A+B, 'L')", images)) == "L 3" assert pixel(ImageMath.eval("convert(A+B, '1')", images)) == "1 0" assert pixel(ImageMath.eval("convert(A+B, 'RGB')", images)) == "RGB (3, 3, 3)" -def test_compare(): +def test_compare() -> None: assert pixel(ImageMath.eval("min(A, B)", images)) == "I 1" assert pixel(ImageMath.eval("max(A, B)", images)) == "I 2" assert pixel(ImageMath.eval("A == 1", images)) == "I 1" assert pixel(ImageMath.eval("A == 2", images)) == "I 0" -def test_one_image_larger(): +def test_one_image_larger() -> None: assert pixel(ImageMath.eval("A+B", A=A2, B=B)) == "I 3" assert pixel(ImageMath.eval("A+B", A=A, B=B2)) == "I 3" -def test_abs(): +def test_abs() -> None: assert pixel(ImageMath.eval("abs(A)", A=A)) == "I 1" assert pixel(ImageMath.eval("abs(B)", B=B)) == "I 2" -def test_binary_mod(): +def test_binary_mod() -> None: assert pixel(ImageMath.eval("A%A", A=A)) == "I 0" assert pixel(ImageMath.eval("B%B", B=B)) == "I 0" assert pixel(ImageMath.eval("A%B", A=A, B=B)) == "I 1" @@ -113,90 +113,90 @@ def test_binary_mod(): assert pixel(ImageMath.eval("Z%B", B=B, Z=Z)) == "I 0" -def test_bitwise_invert(): +def test_bitwise_invert() -> None: assert pixel(ImageMath.eval("~Z", Z=Z)) == "I -1" assert pixel(ImageMath.eval("~A", A=A)) == "I -2" assert pixel(ImageMath.eval("~B", B=B)) == "I -3" -def test_bitwise_and(): +def test_bitwise_and() -> None: assert pixel(ImageMath.eval("Z&Z", A=A, Z=Z)) == "I 0" assert pixel(ImageMath.eval("Z&A", A=A, Z=Z)) == "I 0" assert pixel(ImageMath.eval("A&Z", A=A, Z=Z)) == "I 0" assert pixel(ImageMath.eval("A&A", A=A, Z=Z)) == "I 1" -def test_bitwise_or(): +def test_bitwise_or() -> None: assert pixel(ImageMath.eval("Z|Z", A=A, Z=Z)) == "I 0" assert pixel(ImageMath.eval("Z|A", A=A, Z=Z)) == "I 1" assert pixel(ImageMath.eval("A|Z", A=A, Z=Z)) == "I 1" assert pixel(ImageMath.eval("A|A", A=A, Z=Z)) == "I 1" -def test_bitwise_xor(): +def test_bitwise_xor() -> None: assert pixel(ImageMath.eval("Z^Z", A=A, Z=Z)) == "I 0" assert pixel(ImageMath.eval("Z^A", A=A, Z=Z)) == "I 1" assert pixel(ImageMath.eval("A^Z", A=A, Z=Z)) == "I 1" assert pixel(ImageMath.eval("A^A", A=A, Z=Z)) == "I 0" -def test_bitwise_leftshift(): +def test_bitwise_leftshift() -> None: assert pixel(ImageMath.eval("Z<<0", Z=Z)) == "I 0" assert pixel(ImageMath.eval("Z<<1", Z=Z)) == "I 0" assert pixel(ImageMath.eval("A<<0", A=A)) == "I 1" assert pixel(ImageMath.eval("A<<1", A=A)) == "I 2" -def test_bitwise_rightshift(): +def test_bitwise_rightshift() -> None: assert pixel(ImageMath.eval("Z>>0", Z=Z)) == "I 0" assert pixel(ImageMath.eval("Z>>1", Z=Z)) == "I 0" assert pixel(ImageMath.eval("A>>0", A=A)) == "I 1" assert pixel(ImageMath.eval("A>>1", A=A)) == "I 0" -def test_logical_eq(): +def test_logical_eq() -> None: assert pixel(ImageMath.eval("A==A", A=A)) == "I 1" assert pixel(ImageMath.eval("B==B", B=B)) == "I 1" assert pixel(ImageMath.eval("A==B", A=A, B=B)) == "I 0" assert pixel(ImageMath.eval("B==A", A=A, B=B)) == "I 0" -def test_logical_ne(): +def test_logical_ne() -> None: assert pixel(ImageMath.eval("A!=A", A=A)) == "I 0" assert pixel(ImageMath.eval("B!=B", B=B)) == "I 0" assert pixel(ImageMath.eval("A!=B", A=A, B=B)) == "I 1" assert pixel(ImageMath.eval("B!=A", A=A, B=B)) == "I 1" -def test_logical_lt(): +def test_logical_lt() -> None: assert pixel(ImageMath.eval("A None: assert pixel(ImageMath.eval("A<=A", A=A)) == "I 1" assert pixel(ImageMath.eval("B<=B", B=B)) == "I 1" assert pixel(ImageMath.eval("A<=B", A=A, B=B)) == "I 1" assert pixel(ImageMath.eval("B<=A", A=A, B=B)) == "I 0" -def test_logical_gt(): +def test_logical_gt() -> None: assert pixel(ImageMath.eval("A>A", A=A)) == "I 0" assert pixel(ImageMath.eval("B>B", B=B)) == "I 0" assert pixel(ImageMath.eval("A>B", A=A, B=B)) == "I 0" assert pixel(ImageMath.eval("B>A", A=A, B=B)) == "I 1" -def test_logical_ge(): +def test_logical_ge() -> None: assert pixel(ImageMath.eval("A>=A", A=A)) == "I 1" assert pixel(ImageMath.eval("B>=B", B=B)) == "I 1" assert pixel(ImageMath.eval("A>=B", A=A, B=B)) == "I 0" assert pixel(ImageMath.eval("B>=A", A=A, B=B)) == "I 1" -def test_logical_equal(): +def test_logical_equal() -> None: assert pixel(ImageMath.eval("equal(A, A)", A=A)) == "I 1" assert pixel(ImageMath.eval("equal(B, B)", B=B)) == "I 1" assert pixel(ImageMath.eval("equal(Z, Z)", Z=Z)) == "I 1" @@ -205,7 +205,7 @@ def test_logical_equal(): assert pixel(ImageMath.eval("equal(A, Z)", A=A, Z=Z)) == "I 0" -def test_logical_not_equal(): +def test_logical_not_equal() -> None: assert pixel(ImageMath.eval("notequal(A, A)", A=A)) == "I 0" assert pixel(ImageMath.eval("notequal(B, B)", B=B)) == "I 0" assert pixel(ImageMath.eval("notequal(Z, Z)", Z=Z)) == "I 0" diff --git a/Tests/test_imagemorph.py b/Tests/test_imagemorph.py index 0708ee639..32615cf0e 100644 --- a/Tests/test_imagemorph.py +++ b/Tests/test_imagemorph.py @@ -1,6 +1,8 @@ # Test the ImageMorphology functionality from __future__ import annotations +from pathlib import Path + import pytest from PIL import Image, ImageMorph, _imagingmorph @@ -8,7 +10,7 @@ from PIL import Image, ImageMorph, _imagingmorph from .helper import assert_image_equal_tofile, hopper -def string_to_img(image_string): +def string_to_img(image_string: str) -> Image.Image: """Turn a string image representation into a binary image""" rows = [s for s in image_string.replace(" ", "").split("\n") if len(s)] height = len(rows) @@ -36,7 +38,7 @@ A = string_to_img( ) -def img_to_string(im): +def img_to_string(im: Image.Image) -> str: """Turn a (small) binary image into a string representation""" chars = ".1" width, height = im.size @@ -46,22 +48,22 @@ def img_to_string(im): ) -def img_string_normalize(im): +def img_string_normalize(im: str) -> str: return img_to_string(string_to_img(im)) -def assert_img_equal_img_string(a, b_string): +def assert_img_equal_img_string(a: Image.Image, b_string: str) -> None: assert img_to_string(a) == img_string_normalize(b_string) -def test_str_to_img(): +def test_str_to_img() -> None: assert_image_equal_tofile(A, "Tests/images/morph_a.png") @pytest.mark.parametrize( "op", ("corner", "dilation4", "dilation8", "erosion4", "erosion8", "edge") ) -def test_lut(op): +def test_lut(op: str) -> None: lb = ImageMorph.LutBuilder(op_name=op) assert lb.get_lut() is None @@ -70,21 +72,22 @@ def test_lut(op): assert lut == bytearray(f.read()) -def test_no_operator_loaded(): +def test_no_operator_loaded() -> None: + im = Image.new("L", (1, 1)) mop = ImageMorph.MorphOp() with pytest.raises(Exception) as e: - mop.apply(None) + mop.apply(im) assert str(e.value) == "No operator loaded" with pytest.raises(Exception) as e: - mop.match(None) + mop.match(im) assert str(e.value) == "No operator loaded" with pytest.raises(Exception) as e: - mop.save_lut(None) + mop.save_lut("") assert str(e.value) == "No operator loaded" # Test the named patterns -def test_erosion8(): +def test_erosion8() -> None: # erosion8 mop = ImageMorph.MorphOp(op_name="erosion8") count, Aout = mop.apply(A) @@ -103,7 +106,7 @@ def test_erosion8(): ) -def test_dialation8(): +def test_dialation8() -> None: # dialation8 mop = ImageMorph.MorphOp(op_name="dilation8") count, Aout = mop.apply(A) @@ -122,7 +125,7 @@ def test_dialation8(): ) -def test_erosion4(): +def test_erosion4() -> None: # erosion4 mop = ImageMorph.MorphOp(op_name="dilation4") count, Aout = mop.apply(A) @@ -141,7 +144,7 @@ def test_erosion4(): ) -def test_edge(): +def test_edge() -> None: # edge mop = ImageMorph.MorphOp(op_name="edge") count, Aout = mop.apply(A) @@ -160,7 +163,7 @@ def test_edge(): ) -def test_corner(): +def test_corner() -> None: # Create a corner detector pattern mop = ImageMorph.MorphOp(patterns=["1:(... ... ...)->0", "4:(00. 01. ...)->1"]) count, Aout = mop.apply(A) @@ -188,7 +191,7 @@ def test_corner(): assert tuple(coords) == ((2, 2), (4, 2), (2, 4), (4, 4)) -def test_mirroring(): +def test_mirroring() -> None: # Test 'M' for mirroring mop = ImageMorph.MorphOp(patterns=["1:(... ... ...)->0", "M:(00. 01. ...)->1"]) count, Aout = mop.apply(A) @@ -207,7 +210,7 @@ def test_mirroring(): ) -def test_negate(): +def test_negate() -> None: # Test 'N' for negate mop = ImageMorph.MorphOp(patterns=["1:(... ... ...)->0", "N:(00. 01. ...)->1"]) count, Aout = mop.apply(A) @@ -226,7 +229,7 @@ def test_negate(): ) -def test_incorrect_mode(): +def test_incorrect_mode() -> None: im = hopper("RGB") mop = ImageMorph.MorphOp(op_name="erosion8") @@ -241,7 +244,7 @@ def test_incorrect_mode(): assert str(e.value) == "Image mode must be L" -def test_add_patterns(): +def test_add_patterns() -> None: # Arrange lb = ImageMorph.LutBuilder(op_name="corner") assert lb.patterns == ["1:(... ... ...)->0", "4:(00. 01. ...)->1"] @@ -259,12 +262,12 @@ def test_add_patterns(): ] -def test_unknown_pattern(): +def test_unknown_pattern() -> None: with pytest.raises(Exception): ImageMorph.LutBuilder(op_name="unknown") -def test_pattern_syntax_error(): +def test_pattern_syntax_error() -> None: # Arrange lb = ImageMorph.LutBuilder(op_name="corner") new_patterns = ["a pattern with a syntax error"] @@ -276,7 +279,7 @@ def test_pattern_syntax_error(): assert str(e.value) == 'Syntax error in pattern "a pattern with a syntax error"' -def test_load_invalid_mrl(): +def test_load_invalid_mrl() -> None: # Arrange invalid_mrl = "Tests/images/hopper.png" mop = ImageMorph.MorphOp() @@ -287,7 +290,7 @@ def test_load_invalid_mrl(): assert str(e.value) == "Wrong size operator file!" -def test_roundtrip_mrl(tmp_path): +def test_roundtrip_mrl(tmp_path: Path) -> None: # Arrange tempfile = str(tmp_path / "temp.mrl") mop = ImageMorph.MorphOp(op_name="corner") @@ -301,7 +304,7 @@ def test_roundtrip_mrl(tmp_path): assert mop.lut == initial_lut -def test_set_lut(): +def test_set_lut() -> None: # Arrange lb = ImageMorph.LutBuilder(op_name="corner") lut = lb.build_lut() @@ -314,7 +317,7 @@ def test_set_lut(): assert mop.lut == lut -def test_wrong_mode(): +def test_wrong_mode() -> None: lut = ImageMorph.LutBuilder(op_name="corner").build_lut() imrgb = Image.new("RGB", (10, 10)) iml = Image.new("L", (10, 10)) diff --git a/Tests/test_imageops.py b/Tests/test_imageops.py index 636b99dbe..d6bdaf450 100644 --- a/Tests/test_imageops.py +++ b/Tests/test_imageops.py @@ -13,8 +13,12 @@ from .helper import ( ) -class Deformer: - def getmesh(self, im): +class Deformer(ImageOps.SupportsGetMesh): + def getmesh( + self, im: Image.Image + ) -> list[ + tuple[tuple[int, int, int, int], tuple[int, int, int, int, int, int, int, int]] + ]: x, y = im.size return [((0, 0, x, y), (0, 0, x, 0, x, y, y, 0))] @@ -22,7 +26,7 @@ class Deformer: deformer = Deformer() -def test_sanity(): +def test_sanity() -> None: ImageOps.autocontrast(hopper("L")) ImageOps.autocontrast(hopper("RGB")) @@ -84,7 +88,7 @@ def test_sanity(): ImageOps.exif_transpose(hopper("RGB")) -def test_1pxfit(): +def test_1pxfit() -> None: # Division by zero in equalize if image is 1 pixel high newimg = ImageOps.fit(hopper("RGB").resize((1, 1)), (35, 35)) assert newimg.size == (35, 35) @@ -96,7 +100,7 @@ def test_1pxfit(): assert newimg.size == (35, 35) -def test_fit_same_ratio(): +def test_fit_same_ratio() -> None: # The ratio for this image is 1000.0 / 755 = 1.3245033112582782 # If the ratios are not acknowledged to be the same, # and Pillow attempts to adjust the width to @@ -108,13 +112,13 @@ def test_fit_same_ratio(): @pytest.mark.parametrize("new_size", ((256, 256), (512, 256), (256, 512))) -def test_contain(new_size): +def test_contain(new_size: tuple[int, int]) -> None: im = hopper() new_im = ImageOps.contain(im, new_size) assert new_im.size == (256, 256) -def test_contain_round(): +def test_contain_round() -> None: im = Image.new("1", (43, 63), 1) new_im = ImageOps.contain(im, (5, 7)) assert new_im.width == 5 @@ -132,13 +136,13 @@ def test_contain_round(): ("hopper.png", (256, 256)), # square ), ) -def test_cover(image_name, expected_size): +def test_cover(image_name: str, expected_size: tuple[int, int]) -> None: with Image.open("Tests/images/" + image_name) as im: new_im = ImageOps.cover(im, (256, 256)) assert new_im.size == expected_size -def test_pad(): +def test_pad() -> None: # Same ratio im = hopper() new_size = (im.width * 2, im.height * 2) @@ -158,7 +162,7 @@ def test_pad(): ) -def test_pad_round(): +def test_pad_round() -> None: im = Image.new("1", (1, 1), 1) new_im = ImageOps.pad(im, (4, 1)) assert new_im.load()[2, 0] == 1 @@ -168,7 +172,7 @@ def test_pad_round(): @pytest.mark.parametrize("mode", ("P", "PA")) -def test_palette(mode): +def test_palette(mode: str) -> None: im = hopper(mode) # Expand @@ -182,7 +186,7 @@ def test_palette(mode): ) -def test_pil163(): +def test_pil163() -> None: # Division by zero in equalize if < 255 pixels in image (@PIL163) i = hopper("RGB").resize((15, 16)) @@ -192,7 +196,7 @@ def test_pil163(): ImageOps.equalize(i.convert("RGB")) -def test_scale(): +def test_scale() -> None: # Test the scaling function i = hopper("L").resize((50, 50)) @@ -210,7 +214,7 @@ def test_scale(): @pytest.mark.parametrize("border", (10, (1, 2, 3, 4))) -def test_expand_palette(border): +def test_expand_palette(border: int | tuple[int, int, int, int]) -> None: with Image.open("Tests/images/p_16.tga") as im: im_expanded = ImageOps.expand(im, border, (255, 0, 0)) @@ -236,7 +240,7 @@ def test_expand_palette(border): assert_image_equal(im_cropped, im) -def test_colorize_2color(): +def test_colorize_2color() -> None: # Test the colorizing function with 2-color functionality # Open test image (256px by 10px, black to white) @@ -270,7 +274,7 @@ def test_colorize_2color(): ) -def test_colorize_2color_offset(): +def test_colorize_2color_offset() -> None: # Test the colorizing function with 2-color functionality and offset # Open test image (256px by 10px, black to white) @@ -306,7 +310,7 @@ def test_colorize_2color_offset(): ) -def test_colorize_3color_offset(): +def test_colorize_3color_offset() -> None: # Test the colorizing function with 3-color functionality and offset # Open test image (256px by 10px, black to white) @@ -359,14 +363,14 @@ def test_colorize_3color_offset(): ) -def test_exif_transpose(): +def test_exif_transpose() -> None: exts = [".jpg"] if features.check("webp") and features.check("webp_anim"): exts.append(".webp") for ext in exts: with Image.open("Tests/images/hopper" + ext) as base_im: - def check(orientation_im): + def check(orientation_im: Image.Image) -> None: for im in [ orientation_im, orientation_im.copy(), @@ -376,6 +380,7 @@ def test_exif_transpose(): else: original_exif = im.info["exif"] transposed_im = ImageOps.exif_transpose(im) + assert transposed_im is not None assert_image_similar(base_im, transposed_im, 17) if orientation_im is base_im: assert "exif" not in im.info @@ -387,6 +392,7 @@ def test_exif_transpose(): # Repeat the operation to test that it does not keep transposing transposed_im2 = ImageOps.exif_transpose(transposed_im) + assert transposed_im2 is not None assert_image_equal(transposed_im2, transposed_im) check(base_im) @@ -402,6 +408,7 @@ def test_exif_transpose(): assert im.getexif()[0x0112] == 3 transposed_im = ImageOps.exif_transpose(im) + assert transposed_im is not None assert 0x0112 not in transposed_im.getexif() transposed_im._reload_exif() @@ -414,16 +421,18 @@ def test_exif_transpose(): assert im.getexif()[0x0112] == 3 transposed_im = ImageOps.exif_transpose(im) + assert transposed_im is not None assert 0x0112 not in transposed_im.getexif() # Orientation set directly on Image.Exif im = hopper() im.getexif()[0x0112] = 3 transposed_im = ImageOps.exif_transpose(im) + assert transposed_im is not None assert 0x0112 not in transposed_im.getexif() -def test_exif_transpose_in_place(): +def test_exif_transpose_in_place() -> None: with Image.open("Tests/images/orientation_rectangle.jpg") as im: assert im.size == (2, 1) assert im.getexif()[0x0112] == 8 @@ -435,24 +444,24 @@ def test_exif_transpose_in_place(): assert_image_equal(im, expected) -def test_autocontrast_unsupported_mode(): +def test_autocontrast_unsupported_mode() -> None: im = Image.new("RGBA", (1, 1)) with pytest.raises(OSError): ImageOps.autocontrast(im) -def test_autocontrast_cutoff(): +def test_autocontrast_cutoff() -> None: # Test the cutoff argument of autocontrast with Image.open("Tests/images/bw_gradient.png") as img: - def autocontrast(cutoff): + def autocontrast(cutoff: int | tuple[int, int]): return ImageOps.autocontrast(img, cutoff).histogram() assert autocontrast(10) == autocontrast((10, 10)) assert autocontrast(10) != autocontrast((1, 10)) -def test_autocontrast_mask_toy_input(): +def test_autocontrast_mask_toy_input() -> None: # Test the mask argument of autocontrast with Image.open("Tests/images/bw_gradient.png") as img: rect_mask = Image.new("L", img.size, 0) @@ -471,7 +480,7 @@ def test_autocontrast_mask_toy_input(): assert ImageStat.Stat(result_nomask).median == [128] -def test_autocontrast_mask_real_input(): +def test_autocontrast_mask_real_input() -> None: # Test the autocontrast with a rectangular mask with Image.open("Tests/images/iptc.jpg") as img: rect_mask = Image.new("L", img.size, 0) @@ -486,20 +495,20 @@ def test_autocontrast_mask_real_input(): assert result_nomask != result assert_tuple_approx_equal( ImageStat.Stat(result, mask=rect_mask).median, - [195, 202, 184], + (195, 202, 184), threshold=2, msg="autocontrast with mask pixel incorrect", ) assert_tuple_approx_equal( ImageStat.Stat(result_nomask).median, - [119, 106, 79], + (119, 106, 79), threshold=2, msg="autocontrast without mask pixel incorrect", ) -def test_autocontrast_preserve_tone(): - def autocontrast(mode, preserve_tone): +def test_autocontrast_preserve_tone() -> None: + def autocontrast(mode: str, preserve_tone: bool) -> list[int]: im = hopper(mode) return ImageOps.autocontrast(im, preserve_tone=preserve_tone).histogram() @@ -507,7 +516,7 @@ def test_autocontrast_preserve_tone(): assert autocontrast("L", True) == autocontrast("L", False) -def test_autocontrast_preserve_gradient(): +def test_autocontrast_preserve_gradient() -> None: gradient = Image.linear_gradient("L") # test with a grayscale gradient that extends to 0,255. @@ -533,7 +542,7 @@ def test_autocontrast_preserve_gradient(): @pytest.mark.parametrize( "color", ((255, 255, 255), (127, 255, 0), (127, 127, 127), (0, 0, 0)) ) -def test_autocontrast_preserve_one_color(color): +def test_autocontrast_preserve_one_color(color: tuple[int, int, int]) -> None: img = Image.new("RGB", (10, 10), color) # single color images shouldn't change diff --git a/Tests/test_imageops_usm.py b/Tests/test_imageops_usm.py index 8ffb9bff7..c15907a55 100644 --- a/Tests/test_imageops_usm.py +++ b/Tests/test_imageops_usm.py @@ -1,12 +1,14 @@ from __future__ import annotations +from typing import Generator + import pytest from PIL import Image, ImageFilter @pytest.fixture -def test_images(): +def test_images() -> Generator[dict[str, Image.Image], None, None]: ims = { "im": Image.open("Tests/images/hopper.ppm"), "snakes": Image.open("Tests/images/color_snakes.png"), @@ -18,7 +20,7 @@ def test_images(): im.close() -def test_filter_api(test_images): +def test_filter_api(test_images: dict[str, Image.Image]) -> None: im = test_images["im"] test_filter = ImageFilter.GaussianBlur(2.0) @@ -26,13 +28,13 @@ def test_filter_api(test_images): assert i.mode == "RGB" assert i.size == (128, 128) - test_filter = ImageFilter.UnsharpMask(2.0, 125, 8) - i = im.filter(test_filter) + test_filter2 = ImageFilter.UnsharpMask(2.0, 125, 8) + i = im.filter(test_filter2) assert i.mode == "RGB" assert i.size == (128, 128) -def test_usm_formats(test_images): +def test_usm_formats(test_images: dict[str, Image.Image]) -> None: im = test_images["im"] usm = ImageFilter.UnsharpMask @@ -50,7 +52,7 @@ def test_usm_formats(test_images): im.convert("YCbCr").filter(usm) -def test_blur_formats(test_images): +def test_blur_formats(test_images: dict[str, Image.Image]) -> None: im = test_images["im"] blur = ImageFilter.GaussianBlur @@ -68,7 +70,7 @@ def test_blur_formats(test_images): im.convert("YCbCr").filter(blur) -def test_usm_accuracy(test_images): +def test_usm_accuracy(test_images: dict[str, Image.Image]) -> None: snakes = test_images["snakes"] src = snakes.convert("RGB") @@ -77,7 +79,7 @@ def test_usm_accuracy(test_images): assert i.tobytes() == src.tobytes() -def test_blur_accuracy(test_images): +def test_blur_accuracy(test_images: dict[str, Image.Image]) -> None: snakes = test_images["snakes"] i = snakes.filter(ImageFilter.GaussianBlur(0.4)) diff --git a/Tests/test_imagepalette.py b/Tests/test_imagepalette.py index be21464b4..8e2db15aa 100644 --- a/Tests/test_imagepalette.py +++ b/Tests/test_imagepalette.py @@ -1,5 +1,7 @@ from __future__ import annotations +from pathlib import Path + import pytest from PIL import Image, ImagePalette @@ -7,19 +9,19 @@ from PIL import Image, ImagePalette from .helper import assert_image_equal, assert_image_equal_tofile -def test_sanity(): +def test_sanity() -> None: palette = ImagePalette.ImagePalette("RGB", list(range(256)) * 3) assert len(palette.colors) == 256 -def test_reload(): +def test_reload() -> None: with Image.open("Tests/images/hopper.gif") as im: original = im.copy() im.palette.dirty = 1 assert_image_equal(im.convert("RGB"), original.convert("RGB")) -def test_getcolor(): +def test_getcolor() -> None: palette = ImagePalette.ImagePalette() assert len(palette.palette) == 0 assert len(palette.colors) == 0 @@ -46,7 +48,7 @@ def test_getcolor(): palette.getcolor("unknown") -def test_getcolor_rgba_color_rgb_palette(): +def test_getcolor_rgba_color_rgb_palette() -> None: palette = ImagePalette.ImagePalette("RGB") # Opaque RGBA colors are converted @@ -65,7 +67,7 @@ def test_getcolor_rgba_color_rgb_palette(): (255, ImagePalette.ImagePalette("RGB", list(range(256)) * 3)), ], ) -def test_getcolor_not_special(index, palette): +def test_getcolor_not_special(index: int, palette: ImagePalette.ImagePalette) -> None: im = Image.new("P", (1, 1)) # Do not use transparency index as a new color @@ -79,7 +81,7 @@ def test_getcolor_not_special(index, palette): assert index2 not in (index, index1) -def test_file(tmp_path): +def test_file(tmp_path: Path) -> None: palette = ImagePalette.ImagePalette("RGB", list(range(256)) * 3) f = str(tmp_path / "temp.lut") @@ -97,7 +99,7 @@ def test_file(tmp_path): assert p.palette == palette.tobytes() -def test_make_linear_lut(): +def test_make_linear_lut() -> None: # Arrange black = 0 white = 255 @@ -113,7 +115,7 @@ def test_make_linear_lut(): assert lut[i] == i -def test_make_linear_lut_not_yet_implemented(): +def test_make_linear_lut_not_yet_implemented() -> None: # Update after FIXME # Arrange black = 1 @@ -124,7 +126,7 @@ def test_make_linear_lut_not_yet_implemented(): ImagePalette.make_linear_lut(black, white) -def test_make_gamma_lut(): +def test_make_gamma_lut() -> None: # Arrange exp = 5 @@ -142,7 +144,7 @@ def test_make_gamma_lut(): assert lut[255] == 255 -def test_rawmode_valueerrors(tmp_path): +def test_rawmode_valueerrors(tmp_path: Path) -> None: # Arrange palette = ImagePalette.raw("RGB", list(range(256)) * 3) @@ -156,7 +158,7 @@ def test_rawmode_valueerrors(tmp_path): palette.save(f) -def test_getdata(): +def test_getdata() -> None: # Arrange data_in = list(range(256)) * 3 palette = ImagePalette.ImagePalette("RGB", data_in) @@ -168,7 +170,7 @@ def test_getdata(): assert mode == "RGB" -def test_rawmode_getdata(): +def test_rawmode_getdata() -> None: # Arrange data_in = list(range(256)) * 3 palette = ImagePalette.raw("RGB", data_in) @@ -181,7 +183,7 @@ def test_rawmode_getdata(): assert data_in == data_out -def test_2bit_palette(tmp_path): +def test_2bit_palette(tmp_path: Path) -> None: # issue #2258, 2 bit palettes are corrupted. outfile = str(tmp_path / "temp.png") @@ -193,6 +195,6 @@ def test_2bit_palette(tmp_path): assert_image_equal_tofile(img, outfile) -def test_invalid_palette(): +def test_invalid_palette() -> None: with pytest.raises(OSError): ImagePalette.load("Tests/images/hopper.jpg") diff --git a/Tests/test_imagepath.py b/Tests/test_imagepath.py index 5c6393e23..9487560af 100644 --- a/Tests/test_imagepath.py +++ b/Tests/test_imagepath.py @@ -3,13 +3,14 @@ from __future__ import annotations import array import math import struct +from typing import Sequence import pytest from PIL import Image, ImagePath -def test_path(): +def test_path() -> None: p = ImagePath.Path(list(range(10))) # sequence interface @@ -57,7 +58,9 @@ def test_path(): ImagePath.Path((0, 1)), ), ) -def test_path_constructors(coords): +def test_path_constructors( + coords: Sequence[float] | array.array[float] | ImagePath.Path, +) -> None: # Arrange / Act p = ImagePath.Path(coords) @@ -75,7 +78,9 @@ def test_path_constructors(coords): [[0.0, 1.0]], ), ) -def test_invalid_path_constructors(coords): +def test_invalid_path_constructors( + coords: tuple[str, str] | Sequence[Sequence[int]] +) -> None: # Act with pytest.raises(ValueError) as e: ImagePath.Path(coords) @@ -93,7 +98,7 @@ def test_invalid_path_constructors(coords): [0, 1, 2], ), ) -def test_path_odd_number_of_coordinates(coords): +def test_path_odd_number_of_coordinates(coords: Sequence[int]) -> None: # Act with pytest.raises(ValueError) as e: ImagePath.Path(coords) @@ -111,7 +116,9 @@ def test_path_odd_number_of_coordinates(coords): (1, (0.0, 0.0, 0.0, 0.0)), ], ) -def test_getbbox(coords, expected): +def test_getbbox( + coords: int | list[int], expected: tuple[float, float, float, float] +) -> None: # Arrange p = ImagePath.Path(coords) @@ -119,7 +126,7 @@ def test_getbbox(coords, expected): assert p.getbbox() == expected -def test_getbbox_no_args(): +def test_getbbox_no_args() -> None: # Arrange p = ImagePath.Path([0, 1, 2, 3]) @@ -135,7 +142,7 @@ def test_getbbox_no_args(): (list(range(6)), [(0.0, 3.0), (4.0, 9.0), (8.0, 15.0)]), ], ) -def test_map(coords, expected): +def test_map(coords: int | list[int], expected: list[tuple[float, float]]) -> None: # Arrange p = ImagePath.Path(coords) @@ -147,7 +154,7 @@ def test_map(coords, expected): assert list(p) == expected -def test_transform(): +def test_transform() -> None: # Arrange p = ImagePath.Path([0, 1, 2, 3]) theta = math.pi / 15 @@ -165,7 +172,7 @@ def test_transform(): ] -def test_transform_with_wrap(): +def test_transform_with_wrap() -> None: # Arrange p = ImagePath.Path([0, 1, 2, 3]) theta = math.pi / 15 @@ -184,7 +191,7 @@ def test_transform_with_wrap(): ] -def test_overflow_segfault(): +def test_overflow_segfault() -> None: # Some Pythons fail getting the argument as an integer, and it falls # through to the sequence. Seeing this on 32-bit Windows. with pytest.raises((TypeError, MemoryError)): @@ -198,12 +205,12 @@ def test_overflow_segfault(): class Evil: - def __init__(self): + def __init__(self) -> None: self.corrupt = Image.core.path(0x4000000000000000) - def __getitem__(self, i): + def __getitem__(self, i: int) -> bytes: x = self.corrupt[i] return struct.pack("dd", x[0], x[1]) - def __setitem__(self, i, x): + def __setitem__(self, i: int, x: bytes) -> None: self.corrupt[i] = struct.unpack("dd", x) diff --git a/Tests/test_imageqt.py b/Tests/test_imageqt.py index d55d980d9..88ad1f9ee 100644 --- a/Tests/test_imageqt.py +++ b/Tests/test_imageqt.py @@ -16,7 +16,7 @@ if ImageQt.qt_is_installed: from PIL.ImageQt import qRgba -def test_rgb(): +def test_rgb() -> None: # from https://doc.qt.io/archives/qt-4.8/qcolor.html # typedef QRgb # An ARGB quadruplet on the format #AARRGGBB, @@ -28,7 +28,7 @@ def test_rgb(): assert qRgb(0, 0, 0) == qRgba(0, 0, 0, 255) - def checkrgb(r, g, b): + def checkrgb(r: int, g: int, b: int) -> None: val = ImageQt.rgb(r, g, b) val = val % 2**24 # drop the alpha assert val >> 16 == r @@ -41,7 +41,7 @@ def test_rgb(): checkrgb(0, 0, 255) -def test_image(): +def test_image() -> None: modes = ["1", "RGB", "RGBA", "L", "P"] qt_format = ImageQt.QImage.Format if ImageQt.qt_version == "6" else ImageQt.QImage if hasattr(qt_format, "Format_Grayscale16"): # Qt 5.13+ @@ -55,6 +55,6 @@ def test_image(): assert_image_similar(roundtripped_im, im, 1) -def test_closed_file(): +def test_closed_file() -> None: with warnings.catch_warnings(): ImageQt.ImageQt("Tests/images/hopper.gif") diff --git a/Tests/test_imagesequence.py b/Tests/test_imagesequence.py index 66d553bcb..7f3a3d141 100644 --- a/Tests/test_imagesequence.py +++ b/Tests/test_imagesequence.py @@ -1,5 +1,7 @@ from __future__ import annotations +from pathlib import Path + import pytest from PIL import Image, ImageSequence, TiffImagePlugin @@ -7,7 +9,7 @@ from PIL import Image, ImageSequence, TiffImagePlugin from .helper import assert_image_equal, hopper, skip_unless_feature -def test_sanity(tmp_path): +def test_sanity(tmp_path: Path) -> None: test_file = str(tmp_path / "temp.im") im = hopper("RGB") @@ -24,10 +26,10 @@ def test_sanity(tmp_path): assert index == 1 with pytest.raises(AttributeError): - ImageSequence.Iterator(0) + ImageSequence.Iterator(0) # type: ignore[arg-type] -def test_iterator(): +def test_iterator() -> None: with Image.open("Tests/images/multipage.tiff") as im: i = ImageSequence.Iterator(im) for index in range(0, im.n_frames): @@ -38,14 +40,14 @@ def test_iterator(): next(i) -def test_iterator_min_frame(): +def test_iterator_min_frame() -> None: with Image.open("Tests/images/hopper.psd") as im: i = ImageSequence.Iterator(im) for index in range(1, im.n_frames): assert i[index] == next(i) -def _test_multipage_tiff(): +def _test_multipage_tiff() -> None: with Image.open("Tests/images/multipage.tiff") as im: for index, frame in enumerate(ImageSequence.Iterator(im)): frame.load() @@ -53,29 +55,30 @@ def _test_multipage_tiff(): frame.convert("RGB") -def test_tiff(): +def test_tiff() -> None: _test_multipage_tiff() @skip_unless_feature("libtiff") -def test_libtiff(): +def test_libtiff() -> None: TiffImagePlugin.READ_LIBTIFF = True _test_multipage_tiff() TiffImagePlugin.READ_LIBTIFF = False -def test_consecutive(): +def test_consecutive() -> None: with Image.open("Tests/images/multipage.tiff") as im: first_frame = None for frame in ImageSequence.Iterator(im): if first_frame is None: first_frame = frame.copy() + assert first_frame is not None for frame in ImageSequence.Iterator(im): assert_image_equal(frame, first_frame) break -def test_palette_mmap(): +def test_palette_mmap() -> None: # Using mmap in ImageFile can require to reload the palette. with Image.open("Tests/images/multipage-mmap.tiff") as im: color1 = im.getpalette()[:3] @@ -84,7 +87,7 @@ def test_palette_mmap(): assert color1 == color2 -def test_all_frames(): +def test_all_frames() -> None: # Test a single image with Image.open("Tests/images/iss634.gif") as im: ims = ImageSequence.all_frames(im) diff --git a/Tests/test_imageshow.py b/Tests/test_imageshow.py index 0996ad41d..4e9291fbb 100644 --- a/Tests/test_imageshow.py +++ b/Tests/test_imageshow.py @@ -1,5 +1,7 @@ from __future__ import annotations +from typing import Any + import pytest from PIL import Image, ImageShow @@ -7,12 +9,12 @@ from PIL import Image, ImageShow from .helper import hopper, is_win32, on_ci -def test_sanity(): +def test_sanity() -> None: dir(Image) dir(ImageShow) -def test_register(): +def test_register() -> None: # Test registering a viewer that is not a class ImageShow.register("not a class") @@ -24,9 +26,9 @@ def test_register(): "order", [-1, 0], ) -def test_viewer_show(order): +def test_viewer_show(order: int) -> None: class TestViewer(ImageShow.Viewer): - def show_image(self, image, **options): + def show_image(self, image: Image.Image, **options: Any) -> bool: self.methodCalled = True return True @@ -48,12 +50,12 @@ def test_viewer_show(order): reason="Only run on CIs; hangs on Windows CIs", ) @pytest.mark.parametrize("mode", ("1", "I;16", "LA", "RGB", "RGBA")) -def test_show(mode): +def test_show(mode: str) -> None: im = hopper(mode) assert ImageShow.show(im) -def test_show_without_viewers(): +def test_show_without_viewers() -> None: viewers = ImageShow._viewers ImageShow._viewers = [] @@ -63,24 +65,25 @@ def test_show_without_viewers(): ImageShow._viewers = viewers -def test_viewer(): +def test_viewer() -> None: viewer = ImageShow.Viewer() - assert viewer.get_format(None) is None + im = Image.new("L", (1, 1)) + assert viewer.get_format(im) is None with pytest.raises(NotImplementedError): - viewer.get_command(None) + viewer.get_command("") @pytest.mark.parametrize("viewer", ImageShow._viewers) -def test_viewers(viewer): +def test_viewers(viewer: ImageShow.Viewer) -> None: try: viewer.get_command("test.jpg") except NotImplementedError: pass -def test_ipythonviewer(): +def test_ipythonviewer() -> None: pytest.importorskip("IPython", reason="IPython not installed") for viewer in ImageShow._viewers: if isinstance(viewer, ImageShow.IPythonViewer): diff --git a/Tests/test_imagestat.py b/Tests/test_imagestat.py index 01687db35..b1c1306c1 100644 --- a/Tests/test_imagestat.py +++ b/Tests/test_imagestat.py @@ -7,7 +7,7 @@ from PIL import Image, ImageStat from .helper import hopper -def test_sanity(): +def test_sanity() -> None: im = hopper() st = ImageStat.Stat(im) @@ -31,7 +31,7 @@ def test_sanity(): ImageStat.Stat(1) -def test_hopper(): +def test_hopper() -> None: im = hopper() st = ImageStat.Stat(im) @@ -44,7 +44,7 @@ def test_hopper(): assert st.sum[2] == 1563008 -def test_constant(): +def test_constant() -> None: im = Image.new("L", (128, 128), 128) st = ImageStat.Stat(im) diff --git a/Tests/test_imagetk.py b/Tests/test_imagetk.py index c06fc5823..b607b8c43 100644 --- a/Tests/test_imagetk.py +++ b/Tests/test_imagetk.py @@ -23,7 +23,7 @@ TK_MODES = ("1", "L", "P", "RGB", "RGBA") pytestmark = pytest.mark.skipif(not HAS_TK, reason="Tk not installed") -def setup_module(): +def setup_module() -> None: try: # setup tk tk.Frame() @@ -34,7 +34,7 @@ def setup_module(): pytest.skip(f"TCL Error: {v}") -def test_kw(): +def test_kw() -> None: TEST_JPG = "Tests/images/hopper.jpg" TEST_PNG = "Tests/images/hopper.png" with Image.open(TEST_JPG) as im1: @@ -57,7 +57,7 @@ def test_kw(): @pytest.mark.parametrize("mode", TK_MODES) -def test_photoimage(mode): +def test_photoimage(mode: str) -> None: # test as image: im = hopper(mode) @@ -71,7 +71,7 @@ def test_photoimage(mode): assert_image_equal(reloaded, im.convert("RGBA")) -def test_photoimage_apply_transparency(): +def test_photoimage_apply_transparency() -> None: with Image.open("Tests/images/pil123p.png") as im: im_tk = ImageTk.PhotoImage(im) reloaded = ImageTk.getimage(im_tk) @@ -79,7 +79,7 @@ def test_photoimage_apply_transparency(): @pytest.mark.parametrize("mode", TK_MODES) -def test_photoimage_blank(mode): +def test_photoimage_blank(mode: str) -> None: # test a image using mode/size: im_tk = ImageTk.PhotoImage(mode, (100, 100)) @@ -91,7 +91,7 @@ def test_photoimage_blank(mode): assert_image_equal(reloaded.convert(mode), im) -def test_bitmapimage(): +def test_bitmapimage() -> None: im = hopper("1") # this should not crash diff --git a/Tests/test_imagewin.py b/Tests/test_imagewin.py index f93eabcb4..b43c31b52 100644 --- a/Tests/test_imagewin.py +++ b/Tests/test_imagewin.py @@ -8,10 +8,10 @@ from .helper import hopper, is_win32 class TestImageWin: - def test_sanity(self): + def test_sanity(self) -> None: dir(ImageWin) - def test_hdc(self): + def test_hdc(self) -> None: # Arrange dc = 50 @@ -22,7 +22,7 @@ class TestImageWin: # Assert assert dc2 == 50 - def test_hwnd(self): + def test_hwnd(self) -> None: # Arrange wnd = 50 @@ -36,7 +36,7 @@ class TestImageWin: @pytest.mark.skipif(not is_win32(), reason="Windows only") class TestImageWinDib: - def test_dib_image(self): + def test_dib_image(self) -> None: # Arrange im = hopper() @@ -46,7 +46,7 @@ class TestImageWinDib: # Assert assert dib.size == im.size - def test_dib_mode_string(self): + def test_dib_mode_string(self) -> None: # Arrange mode = "RGBA" size = (128, 128) @@ -57,7 +57,7 @@ class TestImageWinDib: # Assert assert dib.size == (128, 128) - def test_dib_paste(self): + def test_dib_paste(self) -> None: # Arrange im = hopper() @@ -71,7 +71,7 @@ class TestImageWinDib: # Assert assert dib.size == (128, 128) - def test_dib_paste_bbox(self): + def test_dib_paste_bbox(self) -> None: # Arrange im = hopper() bbox = (0, 0, 10, 10) @@ -86,7 +86,7 @@ class TestImageWinDib: # Assert assert dib.size == (128, 128) - def test_dib_frombytes_tobytes_roundtrip(self): + def test_dib_frombytes_tobytes_roundtrip(self) -> None: # Arrange # Make two different DIB images im = hopper() diff --git a/Tests/test_imagewin_pointers.py b/Tests/test_imagewin_pointers.py index 63d6b903c..f59ee7284 100644 --- a/Tests/test_imagewin_pointers.py +++ b/Tests/test_imagewin_pointers.py @@ -1,6 +1,7 @@ from __future__ import annotations from io import BytesIO +from pathlib import Path from PIL import Image, ImageWin @@ -69,7 +70,7 @@ if is_win32(): ] CreateDIBSection.restype = ctypes.wintypes.HBITMAP - def serialize_dib(bi, pixels): + def serialize_dib(bi, pixels) -> bytearray: bf = BITMAPFILEHEADER() bf.bfType = 0x4D42 bf.bfOffBits = ctypes.sizeof(bf) + bi.biSize @@ -83,7 +84,7 @@ if is_win32(): memcpy(bp + bf.bfOffBits, pixels, bi.biSizeImage) return bytearray(buf) - def test_pointer(tmp_path): + def test_pointer(tmp_path: Path) -> None: im = hopper() (width, height) = im.size opath = str(tmp_path / "temp.png") diff --git a/Tests/test_lib_image.py b/Tests/test_lib_image.py index 1c642e4c9..31548bbc9 100644 --- a/Tests/test_lib_image.py +++ b/Tests/test_lib_image.py @@ -5,7 +5,7 @@ import pytest from PIL import Image -def test_setmode(): +def test_setmode() -> None: im = Image.new("L", (1, 1), 255) im.im.setmode("1") assert im.im.getpixel((0, 0)) == 255 diff --git a/Tests/test_lib_pack.py b/Tests/test_lib_pack.py index e2024abbf..629a6dc7a 100644 --- a/Tests/test_lib_pack.py +++ b/Tests/test_lib_pack.py @@ -10,7 +10,13 @@ X = 255 class TestLibPack: - def assert_pack(self, mode, rawmode, data, *pixels): + def assert_pack( + self, + mode: str, + rawmode: str, + data: int | bytes, + *pixels: int | float | tuple[int, ...], + ) -> None: """ data - either raw bytes with data or just number of bytes in rawmode. """ @@ -24,7 +30,7 @@ class TestLibPack: assert data == im.tobytes("raw", rawmode) - def test_1(self): + def test_1(self) -> None: self.assert_pack("1", "1", b"\x01", 0, 0, 0, 0, 0, 0, 0, X) self.assert_pack("1", "1;I", b"\x01", X, X, X, X, X, X, X, 0) self.assert_pack("1", "1;R", b"\x01", X, 0, 0, 0, 0, 0, 0, 0) @@ -37,29 +43,29 @@ class TestLibPack: self.assert_pack("1", "L", b"\xff\x00\x00\xff\x00\x00", X, 0, 0, X, 0, 0) - def test_L(self): + def test_L(self) -> None: self.assert_pack("L", "L", 1, 1, 2, 3, 4) self.assert_pack("L", "L;16", b"\x00\xc6\x00\xaf", 198, 175) self.assert_pack("L", "L;16B", b"\xc6\x00\xaf\x00", 198, 175) - def test_LA(self): + def test_LA(self) -> None: self.assert_pack("LA", "LA", 2, (1, 2), (3, 4), (5, 6)) self.assert_pack("LA", "LA;L", 2, (1, 4), (2, 5), (3, 6)) - def test_La(self): + def test_La(self) -> None: self.assert_pack("La", "La", 2, (1, 2), (3, 4), (5, 6)) - def test_P(self): + def test_P(self) -> None: self.assert_pack("P", "P;1", b"\xe4", 1, 1, 1, 0, 0, 255, 0, 0) self.assert_pack("P", "P;2", b"\xe4", 3, 2, 1, 0) self.assert_pack("P", "P;4", b"\x02\xef", 0, 2, 14, 15) self.assert_pack("P", "P", 1, 1, 2, 3, 4) - def test_PA(self): + def test_PA(self) -> None: self.assert_pack("PA", "PA", 2, (1, 2), (3, 4), (5, 6)) self.assert_pack("PA", "PA;L", 2, (1, 4), (2, 5), (3, 6)) - def test_RGB(self): + def test_RGB(self) -> None: self.assert_pack("RGB", "RGB", 3, (1, 2, 3), (4, 5, 6), (7, 8, 9)) self.assert_pack( "RGB", "RGBX", b"\x01\x02\x03\xff\x05\x06\x07\xff", (1, 2, 3), (5, 6, 7) @@ -79,7 +85,7 @@ class TestLibPack: self.assert_pack("RGB", "G", 1, (9, 1, 9), (9, 2, 9), (9, 3, 9)) self.assert_pack("RGB", "B", 1, (9, 9, 1), (9, 9, 2), (9, 9, 3)) - def test_RGBA(self): + def test_RGBA(self) -> None: self.assert_pack("RGBA", "RGBA", 4, (1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12)) self.assert_pack( "RGBA", "RGBA;L", 4, (1, 4, 7, 10), (2, 5, 8, 11), (3, 6, 9, 12) @@ -101,12 +107,12 @@ class TestLibPack: self.assert_pack("RGBA", "B", 1, (6, 7, 1, 9), (6, 7, 2, 0), (6, 7, 3, 9)) self.assert_pack("RGBA", "A", 1, (6, 7, 0, 1), (6, 7, 0, 2), (0, 7, 0, 3)) - def test_RGBa(self): + def test_RGBa(self) -> None: self.assert_pack("RGBa", "RGBa", 4, (1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12)) self.assert_pack("RGBa", "BGRa", 4, (3, 2, 1, 4), (7, 6, 5, 8), (11, 10, 9, 12)) self.assert_pack("RGBa", "aBGR", 4, (4, 3, 2, 1), (8, 7, 6, 5), (12, 11, 10, 9)) - def test_RGBX(self): + def test_RGBX(self) -> None: self.assert_pack("RGBX", "RGBX", 4, (1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12)) self.assert_pack( "RGBX", "RGBX;L", 4, (1, 4, 7, 10), (2, 5, 8, 11), (3, 6, 9, 12) @@ -134,7 +140,7 @@ class TestLibPack: self.assert_pack("RGBX", "B", 1, (6, 7, 1, 9), (6, 7, 2, 0), (6, 7, 3, 9)) self.assert_pack("RGBX", "X", 1, (6, 7, 0, 1), (6, 7, 0, 2), (0, 7, 0, 3)) - def test_CMYK(self): + def test_CMYK(self) -> None: self.assert_pack("CMYK", "CMYK", 4, (1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12)) self.assert_pack( "CMYK", @@ -149,7 +155,7 @@ class TestLibPack: ) self.assert_pack("CMYK", "K", 1, (6, 7, 0, 1), (6, 7, 0, 2), (0, 7, 0, 3)) - def test_YCbCr(self): + def test_YCbCr(self) -> None: self.assert_pack("YCbCr", "YCbCr", 3, (1, 2, 3), (4, 5, 6), (7, 8, 9)) self.assert_pack("YCbCr", "YCbCr;L", 3, (1, 4, 7), (2, 5, 8), (3, 6, 9)) self.assert_pack( @@ -172,19 +178,19 @@ class TestLibPack: self.assert_pack("YCbCr", "Cb", 1, (6, 1, 8, 9), (6, 2, 8, 9), (6, 3, 8, 9)) self.assert_pack("YCbCr", "Cr", 1, (6, 7, 1, 9), (6, 7, 2, 0), (6, 7, 3, 9)) - def test_LAB(self): + def test_LAB(self) -> None: self.assert_pack("LAB", "LAB", 3, (1, 130, 131), (4, 133, 134), (7, 136, 137)) self.assert_pack("LAB", "L", 1, (1, 9, 9), (2, 9, 9), (3, 9, 9)) self.assert_pack("LAB", "A", 1, (9, 1, 9), (9, 2, 9), (9, 3, 9)) self.assert_pack("LAB", "B", 1, (9, 9, 1), (9, 9, 2), (9, 9, 3)) - def test_HSV(self): + def test_HSV(self) -> None: self.assert_pack("HSV", "HSV", 3, (1, 2, 3), (4, 5, 6), (7, 8, 9)) self.assert_pack("HSV", "H", 1, (1, 9, 9), (2, 9, 9), (3, 9, 9)) self.assert_pack("HSV", "S", 1, (9, 1, 9), (9, 2, 9), (9, 3, 9)) self.assert_pack("HSV", "V", 1, (9, 9, 1), (9, 9, 2), (9, 9, 3)) - def test_I(self): + def test_I(self) -> None: self.assert_pack("I", "I;16B", 2, 0x0102, 0x0304) self.assert_pack( "I", "I;32S", b"\x83\x00\x00\x01\x01\x00\x00\x83", 0x01000083, -2097151999 @@ -209,10 +215,10 @@ class TestLibPack: 0x01000083, ) - def test_I16(self): + def test_I16(self) -> None: self.assert_pack("I;16N", "I;16N", 2, 0x0201, 0x0403, 0x0605) - def test_F_float(self): + def test_F_float(self) -> None: self.assert_pack("F", "F;32F", 4, 1.539989614439558e-36, 4.063216068939723e-34) if sys.byteorder == "little": @@ -228,7 +234,13 @@ class TestLibPack: class TestLibUnpack: - def assert_unpack(self, mode, rawmode, data, *pixels): + def assert_unpack( + self, + mode: str, + rawmode: str, + data: int | bytes, + *pixels: int | float | tuple[int, ...], + ) -> None: """ data - either raw bytes with data or just number of bytes in rawmode. """ @@ -241,7 +253,7 @@ class TestLibUnpack: for x, pixel in enumerate(pixels): assert pixel == im.getpixel((x, 0)) - def test_1(self): + def test_1(self) -> None: self.assert_unpack("1", "1", b"\x01", 0, 0, 0, 0, 0, 0, 0, X) self.assert_unpack("1", "1;I", b"\x01", X, X, X, X, X, X, X, 0) self.assert_unpack("1", "1;R", b"\x01", X, 0, 0, 0, 0, 0, 0, 0) @@ -254,7 +266,7 @@ class TestLibUnpack: self.assert_unpack("1", "1;8", b"\x00\x01\x02\xff", 0, X, X, X) - def test_L(self): + def test_L(self) -> None: self.assert_unpack("L", "L;2", b"\xe4", 255, 170, 85, 0) self.assert_unpack("L", "L;2I", b"\xe4", 0, 85, 170, 255) self.assert_unpack("L", "L;2R", b"\xe4", 0, 170, 85, 255) @@ -273,14 +285,14 @@ class TestLibUnpack: self.assert_unpack("L", "L;16", b"\x00\xc6\x00\xaf", 198, 175) self.assert_unpack("L", "L;16B", b"\xc6\x00\xaf\x00", 198, 175) - def test_LA(self): + def test_LA(self) -> None: self.assert_unpack("LA", "LA", 2, (1, 2), (3, 4), (5, 6)) self.assert_unpack("LA", "LA;L", 2, (1, 4), (2, 5), (3, 6)) - def test_La(self): + def test_La(self) -> None: self.assert_unpack("La", "La", 2, (1, 2), (3, 4), (5, 6)) - def test_P(self): + def test_P(self) -> None: self.assert_unpack("P", "P;1", b"\xe4", 1, 1, 1, 0, 0, 1, 0, 0) self.assert_unpack("P", "P;2", b"\xe4", 3, 2, 1, 0) # erroneous? @@ -291,11 +303,11 @@ class TestLibUnpack: self.assert_unpack("P", "P", 1, 1, 2, 3, 4) self.assert_unpack("P", "P;R", 1, 128, 64, 192, 32) - def test_PA(self): + def test_PA(self) -> None: self.assert_unpack("PA", "PA", 2, (1, 2), (3, 4), (5, 6)) self.assert_unpack("PA", "PA;L", 2, (1, 4), (2, 5), (3, 6)) - def test_RGB(self): + def test_RGB(self) -> None: self.assert_unpack("RGB", "RGB", 3, (1, 2, 3), (4, 5, 6), (7, 8, 9)) self.assert_unpack("RGB", "RGB;L", 3, (1, 4, 7), (2, 5, 8), (3, 6, 9)) self.assert_unpack("RGB", "RGB;R", 3, (128, 64, 192), (32, 160, 96)) @@ -346,14 +358,14 @@ class TestLibUnpack: "RGB", "CMYK", 4, (250, 249, 248), (242, 241, 240), (234, 233, 233) ) - def test_BGR(self): + def test_BGR(self) -> None: self.assert_unpack("BGR;15", "BGR;15", 3, (8, 131, 0), (24, 0, 8), (41, 131, 8)) self.assert_unpack( "BGR;16", "BGR;16", 3, (8, 64, 0), (24, 129, 0), (41, 194, 0) ) self.assert_unpack("BGR;24", "BGR;24", 3, (1, 2, 3), (4, 5, 6), (7, 8, 9)) - def test_RGBA(self): + def test_RGBA(self) -> None: self.assert_unpack("RGBA", "LA", 2, (1, 1, 1, 2), (3, 3, 3, 4), (5, 5, 5, 6)) self.assert_unpack( "RGBA", "LA;16B", 4, (1, 1, 1, 3), (5, 5, 5, 7), (9, 9, 9, 11) @@ -522,7 +534,7 @@ class TestLibUnpack: "RGBA", "A;16N", 2, (0, 0, 0, 1), (0, 0, 0, 3), (0, 0, 0, 5) ) - def test_RGBa(self): + def test_RGBa(self) -> None: self.assert_unpack( "RGBa", "RGBa", 4, (1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12) ) @@ -536,7 +548,7 @@ class TestLibUnpack: "RGBa", "aBGR", 4, (4, 3, 2, 1), (8, 7, 6, 5), (12, 11, 10, 9) ) - def test_RGBX(self): + def test_RGBX(self) -> None: self.assert_unpack("RGBX", "RGB", 3, (1, 2, 3, X), (4, 5, 6, X), (7, 8, 9, X)) self.assert_unpack("RGBX", "RGB;L", 3, (1, 4, 7, X), (2, 5, 8, X), (3, 6, 9, X)) self.assert_unpack("RGBX", "RGB;16B", 6, (1, 3, 5, X), (7, 9, 11, X)) @@ -581,7 +593,7 @@ class TestLibUnpack: self.assert_unpack("RGBX", "B", 1, (0, 0, 1, 0), (0, 0, 2, 0), (0, 0, 3, 0)) self.assert_unpack("RGBX", "X", 1, (0, 0, 0, 1), (0, 0, 0, 2), (0, 0, 0, 3)) - def test_CMYK(self): + def test_CMYK(self) -> None: self.assert_unpack( "CMYK", "CMYK", 4, (1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12) ) @@ -619,25 +631,25 @@ class TestLibUnpack: "CMYK", "K;I", 1, (0, 0, 0, 254), (0, 0, 0, 253), (0, 0, 0, 252) ) - def test_YCbCr(self): + def test_YCbCr(self) -> None: self.assert_unpack("YCbCr", "YCbCr", 3, (1, 2, 3), (4, 5, 6), (7, 8, 9)) self.assert_unpack("YCbCr", "YCbCr;L", 3, (1, 4, 7), (2, 5, 8), (3, 6, 9)) self.assert_unpack("YCbCr", "YCbCrK", 4, (1, 2, 3), (5, 6, 7), (9, 10, 11)) self.assert_unpack("YCbCr", "YCbCrX", 4, (1, 2, 3), (5, 6, 7), (9, 10, 11)) - def test_LAB(self): + def test_LAB(self) -> None: self.assert_unpack("LAB", "LAB", 3, (1, 130, 131), (4, 133, 134), (7, 136, 137)) self.assert_unpack("LAB", "L", 1, (1, 0, 0), (2, 0, 0), (3, 0, 0)) self.assert_unpack("LAB", "A", 1, (0, 1, 0), (0, 2, 0), (0, 3, 0)) self.assert_unpack("LAB", "B", 1, (0, 0, 1), (0, 0, 2), (0, 0, 3)) - def test_HSV(self): + def test_HSV(self) -> None: self.assert_unpack("HSV", "HSV", 3, (1, 2, 3), (4, 5, 6), (7, 8, 9)) self.assert_unpack("HSV", "H", 1, (1, 0, 0), (2, 0, 0), (3, 0, 0)) self.assert_unpack("HSV", "S", 1, (0, 1, 0), (0, 2, 0), (0, 3, 0)) self.assert_unpack("HSV", "V", 1, (0, 0, 1), (0, 0, 2), (0, 0, 3)) - def test_I(self): + def test_I(self) -> None: self.assert_unpack("I", "I;8", 1, 0x01, 0x02, 0x03, 0x04) self.assert_unpack("I", "I;8S", b"\x01\x83", 1, -125) self.assert_unpack("I", "I;16", 2, 0x0201, 0x0403) @@ -678,7 +690,7 @@ class TestLibUnpack: 0x01000083, ) - def test_F_int(self): + def test_F_int(self) -> None: self.assert_unpack("F", "F;8", 1, 0x01, 0x02, 0x03, 0x04) self.assert_unpack("F", "F;8S", b"\x01\x83", 1, -125) self.assert_unpack("F", "F;16", 2, 0x0201, 0x0403) @@ -717,7 +729,7 @@ class TestLibUnpack: 16777348, ) - def test_F_float(self): + def test_F_float(self) -> None: self.assert_unpack( "F", "F;32F", 4, 1.539989614439558e-36, 4.063216068939723e-34 ) @@ -768,7 +780,7 @@ class TestLibUnpack: -1234.5, ) - def test_I16(self): + def test_I16(self) -> None: self.assert_unpack("I;16", "I;16", 2, 0x0201, 0x0403, 0x0605) self.assert_unpack("I;16", "I;16B", 2, 0x0102, 0x0304, 0x0506) self.assert_unpack("I;16B", "I;16B", 2, 0x0102, 0x0304, 0x0506) @@ -785,7 +797,7 @@ class TestLibUnpack: self.assert_unpack("I;16L", "I;16N", 2, 0x0102, 0x0304, 0x0506) self.assert_unpack("I;16N", "I;16N", 2, 0x0102, 0x0304, 0x0506) - def test_CMYK16(self): + def test_CMYK16(self) -> None: self.assert_unpack("CMYK", "CMYK;16L", 8, (2, 4, 6, 8), (10, 12, 14, 16)) self.assert_unpack("CMYK", "CMYK;16B", 8, (1, 3, 5, 7), (9, 11, 13, 15)) if sys.byteorder == "little": @@ -793,7 +805,7 @@ class TestLibUnpack: else: self.assert_unpack("CMYK", "CMYK;16N", 8, (1, 3, 5, 7), (9, 11, 13, 15)) - def test_value_error(self): + def test_value_error(self) -> None: with pytest.raises(ValueError): self.assert_unpack("L", "L", 0, 0) with pytest.raises(ValueError): diff --git a/Tests/test_locale.py b/Tests/test_locale.py index db9557d7b..1c8b84a2b 100644 --- a/Tests/test_locale.py +++ b/Tests/test_locale.py @@ -24,7 +24,7 @@ from PIL import Image path = "Tests/images/hopper.jpg" -def test_sanity(): +def test_sanity() -> None: with Image.open(path): pass try: diff --git a/Tests/test_main.py b/Tests/test_main.py index 9f61a0c81..46259f1dc 100644 --- a/Tests/test_main.py +++ b/Tests/test_main.py @@ -5,7 +5,7 @@ import subprocess import sys -def test_main(): +def test_main() -> None: out = subprocess.check_output([sys.executable, "-m", "PIL"]).decode("utf-8") lines = out.splitlines() assert lines[0] == "-" * 68 diff --git a/Tests/test_map.py b/Tests/test_map.py index 9c79fe359..93140f6e5 100644 --- a/Tests/test_map.py +++ b/Tests/test_map.py @@ -7,7 +7,7 @@ import pytest from PIL import Image -def test_overflow(): +def test_overflow() -> None: # There is the potential to overflow comparisons in map.c # if there are > SIZE_MAX bytes in the image or if # the file encodes an offset that makes @@ -25,7 +25,7 @@ def test_overflow(): Image.MAX_IMAGE_PIXELS = max_pixels -def test_tobytes(): +def test_tobytes() -> None: # Note that this image triggers the decompression bomb warning: max_pixels = Image.MAX_IMAGE_PIXELS Image.MAX_IMAGE_PIXELS = None @@ -39,7 +39,7 @@ def test_tobytes(): @pytest.mark.skipif(sys.maxsize <= 2**32, reason="Requires 64-bit system") -def test_ysize(): +def test_ysize() -> None: numpy = pytest.importorskip("numpy", reason="NumPy not installed") # Should not raise 'Integer overflow in ysize' diff --git a/Tests/test_mode_i16.py b/Tests/test_mode_i16.py index d3ee511b7..1b01f95ce 100644 --- a/Tests/test_mode_i16.py +++ b/Tests/test_mode_i16.py @@ -1,5 +1,7 @@ from __future__ import annotations +from pathlib import Path + import pytest from PIL import Image @@ -9,7 +11,7 @@ from .helper import hopper original = hopper().resize((32, 32)).convert("I") -def verify(im1): +def verify(im1: Image.Image) -> None: im2 = original.copy() assert im1.size == im2.size pix1 = im1.load() @@ -25,7 +27,7 @@ def verify(im1): @pytest.mark.parametrize("mode", ("L", "I;16", "I;16B", "I;16L", "I")) -def test_basic(tmp_path, mode): +def test_basic(tmp_path: Path, mode: str) -> None: # PIL 1.1 has limited support for 16-bit image data. Check that # create/copy/transform and save works as expected. @@ -75,8 +77,8 @@ def test_basic(tmp_path, mode): assert im_in.getpixel((0, 0)) == min(512, maximum) -def test_tobytes(): - def tobytes(mode): +def test_tobytes() -> None: + def tobytes(mode: str) -> bytes: return Image.new(mode, (1, 1), 1).tobytes() order = 1 if Image._ENDIAN == "<" else -1 @@ -87,7 +89,7 @@ def test_tobytes(): assert tobytes("I") == b"\x01\x00\x00\x00"[::order] -def test_convert(): +def test_convert() -> None: im = original.copy() for mode in ("I;16", "I;16B", "I;16N"): diff --git a/Tests/test_numpy.py b/Tests/test_numpy.py index 24dff36a6..9f4e6534e 100644 --- a/Tests/test_numpy.py +++ b/Tests/test_numpy.py @@ -13,8 +13,8 @@ numpy = pytest.importorskip("numpy", reason="NumPy not installed") TEST_IMAGE_SIZE = (10, 10) -def test_numpy_to_image(): - def to_image(dtype, bands=1, boolean=0): +def test_numpy_to_image() -> None: + def to_image(dtype, bands: int = 1, boolean: int = 0) -> Image.Image: if bands == 1: if boolean: data = [0, 255] * 50 @@ -82,7 +82,7 @@ def test_numpy_to_image(): # Based on an erring example at # https://stackoverflow.com/questions/10854903/what-is-causing-dimension-dependent-attributeerror-in-pil-fromarray-function -def test_3d_array(): +def test_3d_array() -> None: size = (5, TEST_IMAGE_SIZE[0], TEST_IMAGE_SIZE[1]) a = numpy.ones(size, dtype=numpy.uint8) assert_image(Image.fromarray(a[1, :, :]), "L", TEST_IMAGE_SIZE) @@ -94,12 +94,12 @@ def test_3d_array(): assert_image(Image.fromarray(a[:, :, 1]), "L", TEST_IMAGE_SIZE) -def test_1d_array(): +def test_1d_array() -> None: a = numpy.ones(5, dtype=numpy.uint8) assert_image(Image.fromarray(a), "L", (1, 5)) -def _test_img_equals_nparray(img, np): +def _test_img_equals_nparray(img: Image.Image, np) -> None: assert len(np.shape) >= 2 np_size = np.shape[1], np.shape[0] assert img.size == np_size @@ -109,14 +109,14 @@ def _test_img_equals_nparray(img, np): assert_deep_equal(px[x, y], np[y, x]) -def test_16bit(): +def test_16bit() -> None: with Image.open("Tests/images/16bit.cropped.tif") as img: np_img = numpy.array(img) _test_img_equals_nparray(img, np_img) assert np_img.dtype == numpy.dtype(" None: # Test that 1-bit arrays convert to numpy and back # See: https://github.com/python-pillow/Pillow/issues/350 arr = numpy.array([[1, 0, 0, 1, 0], [0, 1, 0, 0, 0]], "u1") @@ -126,7 +126,7 @@ def test_1bit(): numpy.testing.assert_array_equal(arr, arr_back) -def test_save_tiff_uint16(): +def test_save_tiff_uint16() -> None: # Tests that we're getting the pixel value in the right byte order. pixel_value = 0x1234 a = numpy.array( @@ -157,7 +157,7 @@ def test_save_tiff_uint16(): ("HSV", numpy.uint8), ), ) -def test_to_array(mode, dtype): +def test_to_array(mode: str, dtype) -> None: img = hopper(mode) # Resize to non-square @@ -169,7 +169,7 @@ def test_to_array(mode, dtype): assert np_img.dtype == dtype -def test_point_lut(): +def test_point_lut() -> None: # See https://github.com/python-pillow/Pillow/issues/439 data = list(range(256)) * 3 @@ -180,7 +180,7 @@ def test_point_lut(): im.point(lut) -def test_putdata(): +def test_putdata() -> None: # Shouldn't segfault # See https://github.com/python-pillow/Pillow/issues/1008 @@ -207,12 +207,12 @@ def test_putdata(): numpy.float64, ), ) -def test_roundtrip_eye(dtype): +def test_roundtrip_eye(dtype) -> None: arr = numpy.eye(10, dtype=dtype) numpy.testing.assert_array_equal(arr, numpy.array(Image.fromarray(arr))) -def test_zero_size(): +def test_zero_size() -> None: # Shouldn't cause floating point exception # See https://github.com/python-pillow/Pillow/issues/2259 @@ -222,13 +222,13 @@ def test_zero_size(): @skip_unless_feature("libtiff") -def test_load_first(): +def test_load_first() -> None: with Image.open("Tests/images/g4_orientation_5.tif") as im: a = numpy.array(im) assert a.shape == (88, 590) -def test_bool(): +def test_bool() -> None: # https://github.com/python-pillow/Pillow/issues/2044 a = numpy.zeros((10, 2), dtype=bool) a[0][0] = True @@ -237,7 +237,7 @@ def test_bool(): assert im2.getdata()[0] == 255 -def test_no_resource_warning_for_numpy_array(): +def test_no_resource_warning_for_numpy_array() -> None: # https://github.com/python-pillow/Pillow/issues/835 # Arrange from numpy import array diff --git a/Tests/test_pdfparser.py b/Tests/test_pdfparser.py index a89d75b59..f6b12cb20 100644 --- a/Tests/test_pdfparser.py +++ b/Tests/test_pdfparser.py @@ -19,14 +19,14 @@ from PIL.PdfParser import ( ) -def test_text_encode_decode(): +def test_text_encode_decode() -> None: assert encode_text("abc") == b"\xFE\xFF\x00a\x00b\x00c" assert decode_text(b"\xFE\xFF\x00a\x00b\x00c") == "abc" assert decode_text(b"abc") == "abc" assert decode_text(b"\x1B a \x1C") == "\u02D9 a \u02DD" -def test_indirect_refs(): +def test_indirect_refs() -> None: assert IndirectReference(1, 2) == IndirectReference(1, 2) assert IndirectReference(1, 2) != IndirectReference(1, 3) assert IndirectReference(1, 2) != IndirectObjectDef(1, 2) @@ -37,7 +37,7 @@ def test_indirect_refs(): assert IndirectObjectDef(1, 2) != (1, 2) -def test_parsing(): +def test_parsing() -> None: assert PdfParser.interpret_name(b"Name#23Hash") == b"Name#Hash" assert PdfParser.interpret_name(b"Name#23Hash", as_text=True) == "Name#Hash" assert PdfParser.get_value(b"1 2 R ", 0) == (IndirectReference(1, 2), 5) @@ -95,7 +95,7 @@ def test_parsing(): assert time.strftime("%Y%m%d%H%M%S", getattr(d, name)) == value -def test_pdf_repr(): +def test_pdf_repr() -> None: assert bytes(IndirectReference(1, 2)) == b"1 2 R" assert bytes(IndirectObjectDef(*IndirectReference(1, 2))) == b"1 2 obj" assert bytes(PdfName(b"Name#Hash")) == b"/Name#23Hash" @@ -121,7 +121,7 @@ def test_pdf_repr(): assert pdf_repr(PdfBinary(b"\x90\x1F\xA0")) == b"<901FA0>" -def test_duplicate_xref_entry(): +def test_duplicate_xref_entry() -> None: pdf = PdfParser("Tests/images/duplicate_xref_entry.pdf") assert pdf.xref_table.existing_entries[6][0] == 1197 pdf.close() diff --git a/Tests/test_pickle.py b/Tests/test_pickle.py index c445e3494..ed415953f 100644 --- a/Tests/test_pickle.py +++ b/Tests/test_pickle.py @@ -1,6 +1,7 @@ from __future__ import annotations import pickle +from pathlib import Path import pytest @@ -12,7 +13,9 @@ FONT_SIZE = 20 FONT_PATH = "Tests/fonts/DejaVuSans/DejaVuSans.ttf" -def helper_pickle_file(tmp_path, pickle, protocol, test_file, mode): +def helper_pickle_file( + tmp_path: Path, protocol: int, test_file: str, mode: str | None +) -> None: # Arrange with Image.open(test_file) as im: filename = str(tmp_path / "temp.pkl") @@ -29,7 +32,7 @@ def helper_pickle_file(tmp_path, pickle, protocol, test_file, mode): assert im == loaded_im -def helper_pickle_string(pickle, protocol, test_file, mode): +def helper_pickle_string(protocol: int, test_file: str, mode: str | None) -> None: with Image.open(test_file) as im: if mode: im = im.convert(mode) @@ -63,13 +66,15 @@ def helper_pickle_string(pickle, protocol, test_file, mode): ], ) @pytest.mark.parametrize("protocol", range(0, pickle.HIGHEST_PROTOCOL + 1)) -def test_pickle_image(tmp_path, test_file, test_mode, protocol): +def test_pickle_image( + tmp_path: Path, test_file: str, test_mode: str | None, protocol: int +) -> None: # Act / Assert - helper_pickle_string(pickle, protocol, test_file, test_mode) - helper_pickle_file(tmp_path, pickle, protocol, test_file, test_mode) + helper_pickle_string(protocol, test_file, test_mode) + helper_pickle_file(tmp_path, protocol, test_file, test_mode) -def test_pickle_la_mode_with_palette(tmp_path): +def test_pickle_la_mode_with_palette(tmp_path: Path) -> None: # Arrange filename = str(tmp_path / "temp.pkl") with Image.open("Tests/images/hopper.jpg") as im: @@ -88,7 +93,7 @@ def test_pickle_la_mode_with_palette(tmp_path): @skip_unless_feature("webp") -def test_pickle_tell(): +def test_pickle_tell() -> None: # Arrange with Image.open("Tests/images/hopper.webp") as image: # Act: roundtrip @@ -98,7 +103,9 @@ def test_pickle_tell(): assert unpickled_image.tell() == 0 -def helper_assert_pickled_font_images(font1, font2): +def helper_assert_pickled_font_images( + font1: ImageFont.FreeTypeFont, font2: ImageFont.FreeTypeFont +) -> None: # Arrange im1 = Image.new(mode="RGBA", size=(300, 100)) im2 = Image.new(mode="RGBA", size=(300, 100)) @@ -116,7 +123,7 @@ def helper_assert_pickled_font_images(font1, font2): @skip_unless_feature("freetype2") @pytest.mark.parametrize("protocol", list(range(0, pickle.HIGHEST_PROTOCOL + 1))) -def test_pickle_font_string(protocol): +def test_pickle_font_string(protocol: int) -> None: # Arrange font = ImageFont.truetype(FONT_PATH, FONT_SIZE) @@ -130,7 +137,7 @@ def test_pickle_font_string(protocol): @skip_unless_feature("freetype2") @pytest.mark.parametrize("protocol", list(range(0, pickle.HIGHEST_PROTOCOL + 1))) -def test_pickle_font_file(tmp_path, protocol): +def test_pickle_font_file(tmp_path: Path, protocol: int) -> None: # Arrange font = ImageFont.truetype(FONT_PATH, FONT_SIZE) filename = str(tmp_path / "temp.pkl") diff --git a/Tests/test_psdraw.py b/Tests/test_psdraw.py index 7f618d0f5..64dfb2c95 100644 --- a/Tests/test_psdraw.py +++ b/Tests/test_psdraw.py @@ -3,13 +3,14 @@ from __future__ import annotations import os import sys from io import BytesIO +from pathlib import Path import pytest from PIL import Image, PSDraw -def _create_document(ps): +def _create_document(ps: PSDraw.PSDraw) -> None: title = "hopper" box = (1 * 72, 2 * 72, 7 * 72, 10 * 72) # in points @@ -31,7 +32,7 @@ def _create_document(ps): ps.end_document() -def test_draw_postscript(tmp_path): +def test_draw_postscript(tmp_path: Path) -> None: # Based on Pillow tutorial, but there is no textsize: # https://pillow.readthedocs.io/en/latest/handbook/tutorial.html#drawing-postscript @@ -49,7 +50,7 @@ def test_draw_postscript(tmp_path): @pytest.mark.parametrize("buffer", (True, False)) -def test_stdout(buffer): +def test_stdout(buffer: bool) -> None: # Temporarily redirect stdout old_stdout = sys.stdout diff --git a/Tests/test_pyroma.py b/Tests/test_pyroma.py index c2cea08ca..c2f7fe22e 100644 --- a/Tests/test_pyroma.py +++ b/Tests/test_pyroma.py @@ -7,7 +7,7 @@ from PIL import __version__ pyroma = pytest.importorskip("pyroma", reason="Pyroma not installed") -def test_pyroma(): +def test_pyroma() -> None: # Arrange data = pyroma.projectdata.get_data(".") diff --git a/Tests/test_qt_image_qapplication.py b/Tests/test_qt_image_qapplication.py index ad2b5ad9b..3cd323553 100644 --- a/Tests/test_qt_image_qapplication.py +++ b/Tests/test_qt_image_qapplication.py @@ -1,8 +1,10 @@ from __future__ import annotations +from pathlib import Path + import pytest -from PIL import ImageQt +from PIL import Image, ImageQt from .helper import assert_image_equal_tofile, assert_image_similar, hopper @@ -19,7 +21,7 @@ if ImageQt.qt_is_installed: from PySide6.QtWidgets import QApplication, QHBoxLayout, QLabel, QWidget class Example(QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() img = hopper().resize((1000, 1000)) @@ -35,14 +37,14 @@ if ImageQt.qt_is_installed: lbl.setPixmap(pixmap1.copy()) -def roundtrip(expected): +def roundtrip(expected: Image.Image) -> None: result = ImageQt.fromqpixmap(ImageQt.toqpixmap(expected)) # Qt saves all pixmaps as rgb assert_image_similar(result, expected.convert("RGB"), 1) @pytest.mark.skipif(not ImageQt.qt_is_installed, reason="Qt bindings are not installed") -def test_sanity(tmp_path): +def test_sanity(tmp_path: Path) -> None: # Segfault test app = QApplication([]) ex = Example() diff --git a/Tests/test_qt_image_toqimage.py b/Tests/test_qt_image_toqimage.py index b26787ce6..6110be707 100644 --- a/Tests/test_qt_image_toqimage.py +++ b/Tests/test_qt_image_toqimage.py @@ -1,5 +1,7 @@ from __future__ import annotations +from pathlib import Path + import pytest from PIL import ImageQt @@ -15,7 +17,7 @@ if ImageQt.qt_is_installed: @pytest.mark.parametrize("mode", ("RGB", "RGBA", "L", "P", "1")) -def test_sanity(mode, tmp_path): +def test_sanity(mode: str, tmp_path: Path) -> None: src = hopper(mode) data = ImageQt.toqimage(src) diff --git a/Tests/test_sgi_crash.py b/Tests/test_sgi_crash.py index dee6258ec..3ce31cd2d 100644 --- a/Tests/test_sgi_crash.py +++ b/Tests/test_sgi_crash.py @@ -21,7 +21,7 @@ from PIL import Image "Tests/images/crash-db8bfa78b19721225425530c5946217720d7df4e.sgi", ], ) -def test_crashes(test_file): +def test_crashes(test_file: str) -> None: with open(test_file, "rb") as f: with Image.open(f) as im: with pytest.raises(OSError): diff --git a/Tests/test_shell_injection.py b/Tests/test_shell_injection.py index 9f3e86a32..2a072fd44 100644 --- a/Tests/test_shell_injection.py +++ b/Tests/test_shell_injection.py @@ -1,6 +1,8 @@ from __future__ import annotations import shutil +from pathlib import Path +from typing import Callable import pytest @@ -16,7 +18,12 @@ test_filenames = ("temp_';", 'temp_";', "temp_'\"|", "temp_'\"||", "temp_'\"&&") @pytest.mark.skipif(is_win32(), reason="Requires Unix or macOS") class TestShellInjection: - def assert_save_filename_check(self, tmp_path, src_img, save_func): + def assert_save_filename_check( + self, + tmp_path: Path, + src_img: Image.Image, + save_func: Callable[[Image.Image, int, str], None], + ) -> None: for filename in test_filenames: dest_file = str(tmp_path / filename) save_func(src_img, 0, dest_file) @@ -25,7 +32,7 @@ class TestShellInjection: im.load() @pytest.mark.skipif(not djpeg_available(), reason="djpeg not available") - def test_load_djpeg_filename(self, tmp_path): + def test_load_djpeg_filename(self, tmp_path: Path) -> None: for filename in test_filenames: src_file = str(tmp_path / filename) shutil.copy(TEST_JPG, src_file) @@ -34,18 +41,18 @@ class TestShellInjection: im.load_djpeg() @pytest.mark.skipif(not cjpeg_available(), reason="cjpeg not available") - def test_save_cjpeg_filename(self, tmp_path): + def test_save_cjpeg_filename(self, tmp_path: Path) -> None: with Image.open(TEST_JPG) as im: self.assert_save_filename_check(tmp_path, im, JpegImagePlugin._save_cjpeg) @pytest.mark.skipif(not netpbm_available(), reason="Netpbm not available") - def test_save_netpbm_filename_bmp_mode(self, tmp_path): + def test_save_netpbm_filename_bmp_mode(self, tmp_path: Path) -> None: with Image.open(TEST_GIF) as im: im = im.convert("RGB") self.assert_save_filename_check(tmp_path, im, GifImagePlugin._save_netpbm) @pytest.mark.skipif(not netpbm_available(), reason="Netpbm not available") - def test_save_netpbm_filename_l_mode(self, tmp_path): + def test_save_netpbm_filename_l_mode(self, tmp_path: Path) -> None: with Image.open(TEST_GIF) as im: im = im.convert("L") self.assert_save_filename_check(tmp_path, im, GifImagePlugin._save_netpbm) diff --git a/Tests/test_tiff_crashes.py b/Tests/test_tiff_crashes.py index 64e781cba..073e5415c 100644 --- a/Tests/test_tiff_crashes.py +++ b/Tests/test_tiff_crashes.py @@ -42,14 +42,13 @@ from .helper import on_ci @pytest.mark.filterwarnings("ignore:Possibly corrupt EXIF data") @pytest.mark.filterwarnings("ignore:Metadata warning") @pytest.mark.filterwarnings("ignore:Truncated File Read") -def test_tiff_crashes(test_file): +def test_tiff_crashes(test_file: str) -> None: try: with Image.open(test_file) as im: im.load() except FileNotFoundError: - if not on_ci(): - pytest.skip("test image not found") - return - raise + if on_ci(): + raise + pytest.skip("test image not found") except OSError: pass diff --git a/Tests/test_tiff_ifdrational.py b/Tests/test_tiff_ifdrational.py index c07e7f7d3..f6adae3e6 100644 --- a/Tests/test_tiff_ifdrational.py +++ b/Tests/test_tiff_ifdrational.py @@ -1,6 +1,7 @@ from __future__ import annotations from fractions import Fraction +from pathlib import Path from PIL import Image, TiffImagePlugin, features from PIL.TiffImagePlugin import IFDRational @@ -8,14 +9,14 @@ from PIL.TiffImagePlugin import IFDRational from .helper import hopper -def _test_equal(num, denom, target): +def _test_equal(num, denom, target) -> None: t = IFDRational(num, denom) assert target == t assert t == target -def test_sanity(): +def test_sanity() -> None: _test_equal(1, 1, 1) _test_equal(1, 1, Fraction(1, 1)) @@ -31,13 +32,13 @@ def test_sanity(): _test_equal(7, 5, 1.4) -def test_ranges(): +def test_ranges() -> None: for num in range(1, 10): for denom in range(1, 10): assert IFDRational(num, denom) == IFDRational(num, denom) -def test_nonetype(): +def test_nonetype() -> None: # Fails if the _delegate function doesn't return a valid function xres = IFDRational(72) @@ -51,10 +52,10 @@ def test_nonetype(): assert xres and yres -def test_ifd_rational_save(tmp_path): - methods = (True, False) - if not features.check("libtiff"): - methods = (False,) +def test_ifd_rational_save(tmp_path: Path) -> None: + methods = [True] + if features.check("libtiff"): + methods.append(False) for libtiff in methods: TiffImagePlugin.WRITE_LIBTIFF = libtiff diff --git a/Tests/test_uploader.py b/Tests/test_uploader.py index 75326288f..d55ceb4be 100644 --- a/Tests/test_uploader.py +++ b/Tests/test_uploader.py @@ -3,13 +3,13 @@ from __future__ import annotations from .helper import assert_image_equal, assert_image_similar, hopper -def check_upload_equal(): +def check_upload_equal() -> None: result = hopper("P").convert("RGB") target = hopper("RGB") assert_image_equal(result, target) -def check_upload_similar(): +def check_upload_similar() -> None: result = hopper("P").convert("RGB") target = hopper("RGB") assert_image_similar(result, target, 0) diff --git a/Tests/test_util.py b/Tests/test_util.py index 3395ef753..197ef79ee 100644 --- a/Tests/test_util.py +++ b/Tests/test_util.py @@ -1,27 +1,16 @@ from __future__ import annotations +from pathlib import Path, PurePath + import pytest from PIL import _util -def test_is_path(): - # Arrange - fp = "filename.ext" - - # Act - it_is = _util.is_path(fp) - - # Assert - assert it_is - - -def test_path_obj_is_path(): - # Arrange - from pathlib import Path - - test_path = Path("filename.ext") - +@pytest.mark.parametrize( + "test_path", ["filename.ext", Path("filename.ext"), PurePath("filename.ext")] +) +def test_is_path(test_path: str | Path | PurePath) -> None: # Act it_is = _util.is_path(test_path) @@ -29,7 +18,7 @@ def test_path_obj_is_path(): assert it_is -def test_is_not_path(tmp_path): +def test_is_not_path(tmp_path: Path) -> None: # Arrange with (tmp_path / "temp.ext").open("w") as fp: pass @@ -41,7 +30,7 @@ def test_is_not_path(tmp_path): assert not it_is_not -def test_is_directory(): +def test_is_directory() -> None: # Arrange directory = "Tests" @@ -52,7 +41,7 @@ def test_is_directory(): assert it_is -def test_is_not_directory(): +def test_is_not_directory() -> None: # Arrange text = "abc" @@ -63,7 +52,7 @@ def test_is_not_directory(): assert not it_is_not -def test_deferred_error(): +def test_deferred_error() -> None: # Arrange # Act diff --git a/Tests/test_webp_leaks.py b/Tests/test_webp_leaks.py index 0f51abc95..626fe427c 100644 --- a/Tests/test_webp_leaks.py +++ b/Tests/test_webp_leaks.py @@ -14,11 +14,11 @@ class TestWebPLeaks(PillowLeakTestCase): mem_limit = 3 * 1024 # kb iterations = 100 - def test_leak_load(self): + def test_leak_load(self) -> None: with open(test_file, "rb") as f: im_data = f.read() - def core(): + def core() -> None: with Image.open(BytesIO(im_data)) as im: im.load() diff --git a/depends/download-and-extract.sh b/depends/download-and-extract.sh index a318bfafd..04bfbc755 100755 --- a/depends/download-and-extract.sh +++ b/depends/download-and-extract.sh @@ -5,7 +5,7 @@ archive=$1 url=$2 if [ ! -f $archive.tar.gz ]; then - wget -O $archive.tar.gz $url + wget --no-verbose -O $archive.tar.gz $url fi rmdir $archive diff --git a/depends/install_imagequant.sh b/depends/install_imagequant.sh index b7cebbdbf..3adae91a5 100755 --- a/depends/install_imagequant.sh +++ b/depends/install_imagequant.sh @@ -1,15 +1,39 @@ #!/bin/bash # install libimagequant -archive=libimagequant-4.2.2 +archive_name=libimagequant +archive_version=4.2.2 -./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz +archive=$archive_name-$archive_version -pushd $archive/imagequant-sys +if [[ "$GHA_LIBIMAGEQUANT_CACHE_HIT" == "true" ]]; then -cargo install cargo-c -cargo cinstall --prefix=/usr --destdir=. -sudo cp usr/lib/libimagequant.so* /usr/lib/ -sudo cp usr/include/libimagequant.h /usr/include/ + # Copy cached files into place + sudo cp ~/cache-$archive_name/libimagequant.so* /usr/lib/ + sudo cp ~/cache-$archive_name/libimagequant.h /usr/include/ -popd +else + + # Build from source + ./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz + + pushd $archive/imagequant-sys + + cargo install cargo-c + cargo cinstall --prefix=/usr --destdir=. + + # Copy into place + sudo cp usr/lib/libimagequant.so* /usr/lib/ + sudo cp usr/include/libimagequant.h /usr/include/ + + if [ -n "$GITHUB_ACTIONS" ]; then + # Copy to cache + rm -rf ~/cache-$archive_name + mkdir ~/cache-$archive_name + cp usr/lib/libimagequant.so* ~/cache-$archive_name/ + cp usr/include/libimagequant.h ~/cache-$archive_name/ + fi + + popd + +fi diff --git a/depends/install_openjpeg.sh b/depends/install_openjpeg.sh index 4f4b81a62..8c2967bc2 100755 --- a/depends/install_openjpeg.sh +++ b/depends/install_openjpeg.sh @@ -1,7 +1,7 @@ #!/bin/bash # install openjpeg -archive=openjpeg-2.5.0 +archive=openjpeg-2.5.2 ./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz diff --git a/docs/about.rst b/docs/about.rst index cdb32ca5d..98cdd8e5a 100644 --- a/docs/about.rst +++ b/docs/about.rst @@ -23,7 +23,7 @@ Like PIL, Pillow is `licensed under the open source HPND License `_. In January 2020, `the PyPI moderators exhausted the PEP 541 process for contacting the PIL project owner `_ and the `PIL project on PyPI `_ was transferred to the `Pillow team `_. The Pillow team has no plans to update the PIL project on PyPI. diff --git a/docs/conf.py b/docs/conf.py index 9ae7ae605..97289c91d 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -326,7 +326,7 @@ linkcheck_allowed_redirects = { r"https://gitter.im/python-pillow/Pillow?.*": r"https://app.gitter.im/#/room/#python-pillow_Pillow:gitter.im?.*", r"https://pillow.readthedocs.io/?badge=latest": r"https://pillow.readthedocs.io/en/stable/?badge=latest", r"https://pillow.readthedocs.io": r"https://pillow.readthedocs.io/en/stable/", - r"https://tidelift.com/badges/package/pypi/Pillow?.*": r"https://img.shields.io/badge/.*", + r"https://tidelift.com/badges/package/pypi/pillow?.*": r"https://img.shields.io/badge/.*", r"https://zenodo.org/badge/17549/python-pillow/Pillow.svg": r"https://zenodo.org/badge/doi/[\.0-9]+/zenodo.[0-9]+.svg", r"https://zenodo.org/badge/latestdoi/17549/python-pillow/Pillow": r"https://zenodo.org/record/[0-9]+", } diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 205fcb9ab..8877ccdeb 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -504,3 +504,27 @@ PIL.OleFileIO the upstream :pypi:`olefile` Python package, and replaced with an :py:exc:`ImportError` in 5.0.0 (2018-01). The deprecated file has now been removed from Pillow. If needed, install from PyPI (eg. ``python3 -m pip install olefile``). + +import _imaging +~~~~~~~~~~~~~~~ + +.. versionremoved:: 2.1.0 + +Pillow >= 2.1.0 no longer supports ``import _imaging``. +Please use ``from PIL.Image import core as _imaging`` instead. + +Pillow and PIL +~~~~~~~~~~~~~~ + +.. versionremoved:: 1.0.0 + +Pillow and PIL cannot co-exist in the same environment. +Before installing Pillow, please uninstall PIL. + +import Image +~~~~~~~~~~~~ + +.. versionremoved:: 1.0.0 + +Pillow >= 1.0 no longer supports ``import Image``. +Please use ``from PIL import Image`` instead. diff --git a/docs/example/DdsImagePlugin.py b/docs/example/DdsImagePlugin.py index e98bb8680..2a2a0ba29 100644 --- a/docs/example/DdsImagePlugin.py +++ b/docs/example/DdsImagePlugin.py @@ -9,6 +9,7 @@ The contents of this file are hereby released in the public domain (CC0) Full text of the CC0 license: https://creativecommons.org/publicdomain/zero/1.0/ """ + from __future__ import annotations import struct diff --git a/docs/index.rst b/docs/index.rst index 558369919..1ed9266eb 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -49,7 +49,7 @@ Pillow for enterprise is available via the Tidelift Subscription. `Learn more - document.addEventListener('DOMContentLoaded', function() { - activateTab(getOS()); - }); - - -Warnings --------- - -.. warning:: Pillow and PIL cannot co-exist in the same environment. Before installing Pillow, please uninstall PIL. - -.. warning:: Pillow >= 1.0 no longer supports ``import Image``. Please use ``from PIL import Image`` instead. - -.. warning:: Pillow >= 2.1.0 no longer supports ``import _imaging``. Please use ``from PIL.Image import core as _imaging`` instead. - -Python Support --------------- - -Pillow supports these Python versions. - -.. csv-table:: Newer versions - :file: newer-versions.csv - :header-rows: 1 - -.. csv-table:: Older versions - :file: older-versions.csv - :header-rows: 1 - -.. _Linux Installation: -.. _macOS Installation: -.. _Windows Installation: -.. _FreeBSD Installation: - Basic Installation ------------------ -.. note:: +.. Note:: This section has moved to :ref:`basic-installation`. Please update references accordingly. - The following instructions will install Pillow with support for - most common image formats. See :ref:`external-libraries` for a - full list of external libraries supported. +Python Support +-------------- -Install Pillow with :command:`pip`:: - - python3 -m pip install --upgrade pip - python3 -m pip install --upgrade Pillow - -Optionally, install :pypi:`defusedxml` for Pillow to read XMP data, -and :pypi:`olefile` for Pillow to read FPX and MIC images:: - - python3 -m pip install --upgrade defusedxml olefile - - -.. tab:: Linux - - We provide binaries for Linux for each of the supported Python - versions in the manylinux wheel format. These include support for all - optional libraries except libimagequant. Raqm support requires - FriBiDi to be installed separately:: - - python3 -m pip install --upgrade pip - python3 -m pip install --upgrade Pillow - - Most major Linux distributions, including Fedora, Ubuntu and ArchLinux - also include Pillow in packages that previously contained PIL e.g. - ``python-imaging``. Debian splits it into two packages, ``python3-pil`` - and ``python3-pil.imagetk``. - -.. tab:: macOS - - We provide binaries for macOS for each of the supported Python - versions in the wheel format. These include support for all optional - libraries except libimagequant. Raqm support requires - FriBiDi to be installed separately:: - - python3 -m pip install --upgrade pip - python3 -m pip install --upgrade Pillow - - While we provide binaries for both x86-64 and arm64, we do not provide universal2 - binaries. However, it is simple to combine our current binaries to create one:: - - python3 -m pip download --only-binary=:all: --platform macosx_10_10_x86_64 Pillow - python3 -m pip download --only-binary=:all: --platform macosx_11_0_arm64 Pillow - python3 -m pip install delocate - - Then, with the names of the downloaded wheels, use Python to combine them:: - - from delocate.fuse import fuse_wheels - fuse_wheels('Pillow-9.4.0-2-cp39-cp39-macosx_10_10_x86_64.whl', 'Pillow-9.4.0-cp39-cp39-macosx_11_0_arm64.whl', 'Pillow-9.4.0-cp39-cp39-macosx_11_0_universal2.whl') - -.. tab:: Windows - - We provide Pillow binaries for Windows compiled for the matrix of supported - Pythons in the wheel format. These include x86, x86-64 and arm64 versions - (with the exception of Python 3.8 on arm64). These binaries include support - for all optional libraries except libimagequant and libxcb. Raqm support - requires FriBiDi to be installed separately:: - - python3 -m pip install --upgrade pip - python3 -m pip install --upgrade Pillow - - To install Pillow in MSYS2, see `Building on Windows using MSYS2/MinGW`_. - -.. tab:: FreeBSD - - Pillow can be installed on FreeBSD via the official Ports or Packages systems: - - **Ports**:: - - cd /usr/ports/graphics/py-pillow && make install clean - - **Packages**:: - - pkg install py38-pillow - - .. note:: - - The `Pillow FreeBSD port - `_ and packages - are tested by the ports team with all supported FreeBSD versions. - - -.. _Building on Linux: -.. _Building on macOS: -.. _Building on Windows: -.. _Building on Windows using MSYS2/MinGW: -.. _Building on FreeBSD: -.. _Building on Android: - -Building From Source --------------------- - -.. _external-libraries: - -External Libraries -^^^^^^^^^^^^^^^^^^ - -.. note:: - - You **do not need to install all supported external libraries** to - use Pillow's basic features. **Zlib** and **libjpeg** are required - by default. - -.. note:: - - There are Dockerfiles in our `Docker images repo - `_ to install the - dependencies for some operating systems. - -Many of Pillow's features require external libraries: - -* **libjpeg** provides JPEG functionality. - - * Pillow has been tested with libjpeg versions **6b**, **8**, **9-9d** and - libjpeg-turbo version **8**. - * Starting with Pillow 3.0.0, libjpeg is required by default. It can be - disabled with the ``-C jpeg=disable`` flag. - -* **zlib** provides access to compressed PNGs - - * Starting with Pillow 3.0.0, zlib is required by default. It can be - disabled with the ``-C zlib=disable`` flag. - -* **libtiff** provides compressed TIFF functionality - - * Pillow has been tested with libtiff versions **3.x** and **4.0-4.6.0** - -* **libfreetype** provides type related services - -* **littlecms** provides color management - - * Pillow version 2.2.1 and below uses liblcms1, Pillow 2.3.0 and - above uses liblcms2. Tested with **1.19** and **2.7-2.16**. - -* **libwebp** provides the WebP format. - - * Pillow has been tested with version **0.1.3**, which does not read - transparent WebP files. Versions **0.3.0** and above support - transparency. - -* **openjpeg** provides JPEG 2000 functionality. - - * Pillow has been tested with openjpeg **2.0.0**, **2.1.0**, **2.3.1**, - **2.4.0** and **2.5.0**. - * Pillow does **not** support the earlier **1.5** series which ships - with Debian Jessie. - -* **libimagequant** provides improved color quantization - - * Pillow has been tested with libimagequant **2.6-4.2.2** - * Libimagequant is licensed GPLv3, which is more restrictive than - the Pillow license, therefore we will not be distributing binaries - with libimagequant support enabled. - -* **libraqm** provides complex text layout support. - - * libraqm provides bidirectional text support (using FriBiDi), - shaping (using HarfBuzz), and proper script itemization. As a - result, Raqm can support most writing systems covered by Unicode. - * libraqm depends on the following libraries: FreeType, HarfBuzz, - FriBiDi, make sure that you install them before installing libraqm - if not available as package in your system. - * Setting text direction or font features is not supported without libraqm. - * Pillow wheels since version 8.2.0 include a modified version of libraqm that - loads libfribidi at runtime if it is installed. - On Windows this requires compiling FriBiDi and installing ``fribidi.dll`` - into a directory listed in the `Dynamic-link library search order (Microsoft Learn) - `_ - (``fribidi-0.dll`` or ``libfribidi-0.dll`` are also detected). - See `Build Options`_ to see how to build this version. - * Previous versions of Pillow (5.0.0 to 8.1.2) linked libraqm dynamically at runtime. - -* **libxcb** provides X11 screengrab support. - -.. tab:: Linux - - If you didn't build Python from source, make sure you have Python's - development libraries installed. - - In Debian or Ubuntu:: - - sudo apt-get install python3-dev python3-setuptools - - In Fedora, the command is:: - - sudo dnf install python3-devel redhat-rpm-config - - In Alpine, the command is:: - - sudo apk add python3-dev py3-setuptools - - .. Note:: ``redhat-rpm-config`` is required on Fedora 23, but not earlier versions. - - Prerequisites for **Ubuntu 16.04 LTS - 22.04 LTS** are installed with:: - - sudo apt-get install libtiff5-dev libjpeg8-dev libopenjp2-7-dev zlib1g-dev \ - libfreetype6-dev liblcms2-dev libwebp-dev tcl8.6-dev tk8.6-dev python3-tk \ - libharfbuzz-dev libfribidi-dev libxcb1-dev - - To install libraqm, ``sudo apt-get install meson`` and then see - ``depends/install_raqm.sh``. - - Prerequisites are installed on recent **Red Hat**, **CentOS** or **Fedora** with:: - - sudo dnf install libtiff-devel libjpeg-devel openjpeg2-devel zlib-devel \ - freetype-devel lcms2-devel libwebp-devel tcl-devel tk-devel \ - harfbuzz-devel fribidi-devel libraqm-devel libimagequant-devel libxcb-devel - - Note that the package manager may be yum or DNF, depending on the - exact distribution. - - Prerequisites are installed for **Alpine** with:: - - sudo apk add tiff-dev jpeg-dev openjpeg-dev zlib-dev freetype-dev lcms2-dev \ - libwebp-dev tcl-dev tk-dev harfbuzz-dev fribidi-dev libimagequant-dev \ - libxcb-dev libpng-dev - - See also the ``Dockerfile``\s in the Test Infrastructure repo - (https://github.com/python-pillow/docker-images) for a known working - install process for other tested distros. - -.. tab:: macOS - - The Xcode command line tools are required to compile portions of - Pillow. The tools are installed by running ``xcode-select --install`` - from the command line. The command line tools are required even if you - have the full Xcode package installed. It may be necessary to run - ``sudo xcodebuild -license`` to accept the license prior to using the - tools. - - The easiest way to install external libraries is via `Homebrew - `_. After you install Homebrew, run:: - - brew install libjpeg libtiff little-cms2 openjpeg webp - - To install libraqm on macOS use Homebrew to install its dependencies:: - - brew install freetype harfbuzz fribidi - - Then see ``depends/install_raqm_cmake.sh`` to install libraqm. - -.. tab:: Windows - - We recommend you use prebuilt wheels from PyPI. - If you wish to compile Pillow manually, you can use the build scripts - in the ``winbuild`` directory used for CI testing and development. - These scripts require Visual Studio 2017 or newer and NASM. - - The scripts also install Pillow from the local copy of the source code, so the - `Installing`_ instructions will not be necessary afterwards. - -.. tab:: Windows using MSYS2/MinGW - - To build Pillow using MSYS2, make sure you run the **MSYS2 MinGW 32-bit** or - **MSYS2 MinGW 64-bit** console, *not* **MSYS2** directly. - - The following instructions target the 64-bit build, for 32-bit - replace all occurrences of ``mingw-w64-x86_64-`` with ``mingw-w64-i686-``. - - Make sure you have Python and GCC installed:: - - pacman -S \ - mingw-w64-x86_64-gcc \ - mingw-w64-x86_64-python3 \ - mingw-w64-x86_64-python3-pip \ - mingw-w64-x86_64-python3-setuptools - - Prerequisites are installed on **MSYS2 MinGW 64-bit** with:: - - pacman -S \ - mingw-w64-x86_64-libjpeg-turbo \ - mingw-w64-x86_64-zlib \ - mingw-w64-x86_64-libtiff \ - mingw-w64-x86_64-freetype \ - mingw-w64-x86_64-lcms2 \ - mingw-w64-x86_64-libwebp \ - mingw-w64-x86_64-openjpeg2 \ - mingw-w64-x86_64-libimagequant \ - mingw-w64-x86_64-libraqm - - https://www.msys2.org/docs/python/ states that setuptools >= 60 does not work with - MSYS2. To workaround this, before installing Pillow you must run:: - - export SETUPTOOLS_USE_DISTUTILS=stdlib - -.. tab:: FreeBSD - - .. Note:: Only FreeBSD 10 and 11 tested - - Make sure you have Python's development libraries installed:: - - sudo pkg install python3 - - Prerequisites are installed on **FreeBSD 10 or 11** with:: - - sudo pkg install jpeg-turbo tiff webp lcms2 freetype2 openjpeg harfbuzz fribidi libxcb - - Then see ``depends/install_raqm_cmake.sh`` to install libraqm. - -.. tab:: Android - - Basic Android support has been added for compilation within the Termux - environment. The dependencies can be installed by:: - - pkg install -y python ndk-sysroot clang make \ - libjpeg-turbo - - This has been tested within the Termux app on ChromeOS, on x86. - -Installing -^^^^^^^^^^ - -Once you have installed the prerequisites, to install Pillow from the source -code on PyPI, run:: - - python3 -m pip install --upgrade pip - python3 -m pip install --upgrade Pillow --no-binary :all: - -If the prerequisites are installed in the standard library locations -for your machine (e.g. :file:`/usr` or :file:`/usr/local`), no -additional configuration should be required. If they are installed in -a non-standard location, you may need to configure setuptools to use -those locations by editing :file:`setup.py` or -:file:`pyproject.toml`, or by adding environment variables on the command -line:: - - CFLAGS="-I/usr/pkg/include" python3 -m pip install --upgrade Pillow --no-binary :all: - -If Pillow has been previously built without the required -prerequisites, it may be necessary to manually clear the pip cache or -build without cache using the ``--no-cache-dir`` option to force a -build with newly installed external libraries. - -If you would like to install from a local copy of the source code instead, you -can clone from GitHub with ``git clone https://github.com/python-pillow/Pillow`` -or download and extract the `compressed archive from PyPI`_. - -After navigating to the Pillow directory, run:: - - python3 -m pip install --upgrade pip - python3 -m pip install . - -.. _compressed archive from PyPI: https://pypi.org/project/pillow/#files - -Build Options -""""""""""""" - -* Environment variable: ``MAX_CONCURRENCY=n``. Pillow can use - multiprocessing to build the extension. Setting ``MAX_CONCURRENCY`` - sets the number of CPUs to use, or can disable parallel building by - using a setting of 1. By default, it uses 4 CPUs, or if 4 are not - available, as many as are present. - -* Config settings: ``-C zlib=disable``, ``-C jpeg=disable``, - ``-C tiff=disable``, ``-C freetype=disable``, ``-C raqm=disable``, - ``-C lcms=disable``, ``-C webp=disable``, ``-C webpmux=disable``, - ``-C jpeg2000=disable``, ``-C imagequant=disable``, ``-C xcb=disable``. - Disable building the corresponding feature even if the development - libraries are present on the building machine. - -* Config settings: ``-C zlib=enable``, ``-C jpeg=enable``, - ``-C tiff=enable``, ``-C freetype=enable``, ``-C raqm=enable``, - ``-C lcms=enable``, ``-C webp=enable``, ``-C webpmux=enable``, - ``-C jpeg2000=enable``, ``-C imagequant=enable``, ``-C xcb=enable``. - Require that the corresponding feature is built. The build will raise - an exception if the libraries are not found. Webpmux (WebP metadata) - relies on WebP support. Tcl and Tk also must be used together. - -* Config settings: ``-C raqm=vendor``, ``-C fribidi=vendor``. - These flags are used to compile a modified version of libraqm and - a shim that dynamically loads libfribidi at runtime. These are - used to compile the standard Pillow wheels. Compiling libraqm requires - a C99-compliant compiler. - -* Build flag: ``-C platform-guessing=disable``. Skips all of the - platform dependent guessing of include and library directories for - automated build systems that configure the proper paths in the - environment variables (e.g. Buildroot). - -* Build flag: ``-C debug=true``. Adds a debugging flag to the include and - library search process to dump all paths searched for and found to - stdout. - - -Sample usage:: - - python3 -m pip install --upgrade Pillow -C [feature]=enable +.. Note:: This section has moved to :ref:`python-support`. Please update references accordingly. Platform Support ---------------- -Current platform support for Pillow. Binary distributions are -contributed for each release on a volunteer basis, but the source -should compile and run everywhere platform support is listed. In -general, we aim to support all current versions of Linux, macOS, and -Windows. +.. Note:: This section has moved to :ref:`platform-support`. Please update references accordingly. -Continuous Integration Targets -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Building From Source +-------------------- -These platforms are built and tested for every change. - -+----------------------------------+----------------------------+---------------------+ -| Operating system | Tested Python versions | Tested architecture | -+==================================+============================+=====================+ -| Alpine | 3.9 | x86-64 | -+----------------------------------+----------------------------+---------------------+ -| Amazon Linux 2 | 3.9 | x86-64 | -+----------------------------------+----------------------------+---------------------+ -| Amazon Linux 2023 | 3.9 | x86-64 | -+----------------------------------+----------------------------+---------------------+ -| Arch | 3.9 | x86-64 | -+----------------------------------+----------------------------+---------------------+ -| CentOS 7 | 3.9 | x86-64 | -+----------------------------------+----------------------------+---------------------+ -| CentOS Stream 8 | 3.9 | x86-64 | -+----------------------------------+----------------------------+---------------------+ -| CentOS Stream 9 | 3.9 | x86-64 | -+----------------------------------+----------------------------+---------------------+ -| Debian 11 Bullseye | 3.9 | x86-64 | -+----------------------------------+----------------------------+---------------------+ -| Debian 12 Bookworm | 3.11 | x86, x86-64 | -+----------------------------------+----------------------------+---------------------+ -| Fedora 38 | 3.11 | x86-64 | -+----------------------------------+----------------------------+---------------------+ -| Fedora 39 | 3.12 | x86-64 | -+----------------------------------+----------------------------+---------------------+ -| Gentoo | 3.9 | x86-64 | -+----------------------------------+----------------------------+---------------------+ -| macOS 12 Monterey | 3.8, 3.9, 3.10, 3.11, | x86-64 | -| | 3.12, PyPy3 | | -+----------------------------------+----------------------------+---------------------+ -| Ubuntu Linux 20.04 LTS (Focal) | 3.8 | x86-64 | -+----------------------------------+----------------------------+---------------------+ -| Ubuntu Linux 22.04 LTS (Jammy) | 3.8, 3.9, 3.10, 3.11, | x86-64 | -| | 3.12, PyPy3 | | -| +----------------------------+---------------------+ -| | 3.10 | arm64v8, ppc64le, | -| | | s390x | -+----------------------------------+----------------------------+---------------------+ -| Windows Server 2016 | 3.8 | x86-64 | -+----------------------------------+----------------------------+---------------------+ -| Windows Server 2022 | 3.8, 3.9, 3.10, 3.11, | x86-64 | -| | 3.12, PyPy3 | | -| +----------------------------+---------------------+ -| | 3.12 | x86 | -| +----------------------------+---------------------+ -| | 3.9 (MinGW) | x86-64 | -| +----------------------------+---------------------+ -| | 3.8, 3.9 (Cygwin) | x86-64 | -+----------------------------------+----------------------------+---------------------+ - - -Other Platforms -^^^^^^^^^^^^^^^ - -These platforms have been reported to work at the versions mentioned. - -.. note:: - - Contributors please test Pillow on your platform then update this - document and send a pull request. - -+----------------------------------+----------------------------+------------------+--------------+ -| Operating system | | Tested Python | | Latest tested | | Tested | -| | | versions | | Pillow version | | processors | -+==================================+============================+==================+==============+ -| macOS 14 Sonoma | 3.8, 3.9, 3.10, 3.11, 3.12 | 10.2.0 |arm | -+----------------------------------+----------------------------+------------------+--------------+ -| macOS 13 Ventura | 3.8, 3.9, 3.10, 3.11 | 10.0.1 |arm | -| +----------------------------+------------------+ | -| | 3.7 | 9.5.0 | | -+----------------------------------+----------------------------+------------------+--------------+ -| macOS 12 Monterey | 3.7, 3.8, 3.9, 3.10, 3.11 | 9.3.0 |arm | -+----------------------------------+----------------------------+------------------+--------------+ -| macOS 11 Big Sur | 3.7, 3.8, 3.9, 3.10 | 8.4.0 |arm | -| +----------------------------+------------------+--------------+ -| | 3.7, 3.8, 3.9, 3.10, 3.11 | 9.4.0 |x86-64 | -| +----------------------------+------------------+ | -| | 3.6 | 8.4.0 | | -+----------------------------------+----------------------------+------------------+--------------+ -| macOS 10.15 Catalina | 3.6, 3.7, 3.8, 3.9 | 8.3.2 |x86-64 | -| +----------------------------+------------------+ | -| | 3.5 | 7.2.0 | | -+----------------------------------+----------------------------+------------------+--------------+ -| macOS 10.14 Mojave | 3.5, 3.6, 3.7, 3.8 | 7.2.0 |x86-64 | -| +----------------------------+------------------+ | -| | 2.7 | 6.0.0 | | -| +----------------------------+------------------+ | -| | 3.4 | 5.4.1 | | -+----------------------------------+----------------------------+------------------+--------------+ -| macOS 10.13 High Sierra | 2.7, 3.4, 3.5, 3.6 | 4.2.1 |x86-64 | -+----------------------------------+----------------------------+------------------+--------------+ -| macOS 10.12 Sierra | 2.7, 3.4, 3.5, 3.6 | 4.1.1 |x86-64 | -+----------------------------------+----------------------------+------------------+--------------+ -| Mac OS X 10.11 El Capitan | 2.7, 3.4, 3.5, 3.6, 3.7 | 5.4.1 |x86-64 | -| +----------------------------+------------------+ | -| | 3.3 | 4.1.0 | | -+----------------------------------+----------------------------+------------------+--------------+ -| Mac OS X 10.9 Mavericks | 2.7, 3.2, 3.3, 3.4 | 3.0.0 |x86-64 | -+----------------------------------+----------------------------+------------------+--------------+ -| Mac OS X 10.8 Mountain Lion | 2.6, 2.7, 3.2, 3.3 | |x86-64 | -+----------------------------------+----------------------------+------------------+--------------+ -| Redhat Linux 6 | 2.6 | |x86 | -+----------------------------------+----------------------------+------------------+--------------+ -| CentOS 6.3 | 2.7, 3.3 | |x86 | -+----------------------------------+----------------------------+------------------+--------------+ -| CentOS 8 | 3.9 | 9.0.0 |x86-64 | -+----------------------------------+----------------------------+------------------+--------------+ -| Fedora 23 | 2.7, 3.4 | 3.1.0 |x86-64 | -+----------------------------------+----------------------------+------------------+--------------+ -| Ubuntu Linux 12.04 LTS (Precise) | | 2.6, 3.2, 3.3, 3.4, 3.5 | 3.4.1 |x86,x86-64 | -| | | PyPy5.3.1, PyPy3 v2.4.0 | | | -| +----------------------------+------------------+--------------+ -| | 2.7 | 4.3.0 |x86-64 | -| +----------------------------+------------------+--------------+ -| | 2.7, 3.2 | 3.4.1 |ppc | -+----------------------------------+----------------------------+------------------+--------------+ -| Ubuntu Linux 10.04 LTS (Lucid) | 2.6 | 2.3.0 |x86,x86-64 | -+----------------------------------+----------------------------+------------------+--------------+ -| Debian 8.2 Jessie | 2.7, 3.4 | 3.1.0 |x86-64 | -+----------------------------------+----------------------------+------------------+--------------+ -| Raspbian Jessie | 2.7, 3.4 | 3.1.0 |arm | -+----------------------------------+----------------------------+------------------+--------------+ -| Raspbian Stretch | 2.7, 3.5 | 4.0.0 |arm | -+----------------------------------+----------------------------+------------------+--------------+ -| Raspberry Pi OS | 3.6, 3.7, 3.8, 3.9 | 8.2.0 |arm | -| +----------------------------+------------------+ | -| | 2.7 | 6.2.2 | | -+----------------------------------+----------------------------+------------------+--------------+ -| Gentoo Linux | 2.7, 3.2 | 2.1.0 |x86-64 | -+----------------------------------+----------------------------+------------------+--------------+ -| FreeBSD 11.1 | 2.7, 3.4, 3.5, 3.6 | 4.3.0 |x86-64 | -+----------------------------------+----------------------------+------------------+--------------+ -| FreeBSD 10.3 | 2.7, 3.4, 3.5 | 4.2.0 |x86-64 | -+----------------------------------+----------------------------+------------------+--------------+ -| FreeBSD 10.2 | 2.7, 3.4 | 3.1.0 |x86-64 | -+----------------------------------+----------------------------+------------------+--------------+ -| Windows 11 | 3.9, 3.10, 3.11, 3.12 | 10.2.0 |arm64 | -+----------------------------------+----------------------------+------------------+--------------+ -| Windows 11 Pro | 3.11, 3.12 | 10.2.0 |x86-64 | -+----------------------------------+----------------------------+------------------+--------------+ -| Windows 10 | 3.7 | 7.1.0 |x86-64 | -+----------------------------------+----------------------------+------------------+--------------+ -| Windows 10/Cygwin 3.3 | 3.6, 3.7, 3.8, 3.9 | 8.4.0 |x86-64 | -+----------------------------------+----------------------------+------------------+--------------+ -| Windows 8.1 Pro | 2.6, 2.7, 3.2, 3.3, 3.4 | 2.4.0 |x86,x86-64 | -+----------------------------------+----------------------------+------------------+--------------+ -| Windows 8 Pro | 2.6, 2.7, 3.2, 3.3, 3.4a3 | 2.2.0 |x86,x86-64 | -+----------------------------------+----------------------------+------------------+--------------+ -| Windows 7 Professional | 3.7 | 7.0.0 |x86,x86-64 | -+----------------------------------+----------------------------+------------------+--------------+ -| Windows Server 2008 R2 Enterprise| 3.3 | |x86-64 | -+----------------------------------+----------------------------+------------------+--------------+ +.. Note:: This section has moved to :ref:`building-from-source`. Please update references accordingly. Old Versions ------------ -You can download old distributions from the `release history at PyPI -`_ and by direct URL access -eg. https://pypi.org/project/pillow/1.0/. +.. Note:: This section has moved to :ref:`old-versions`. Please update references accordingly. diff --git a/docs/installation/basic-installation.rst b/docs/installation/basic-installation.rst new file mode 100644 index 000000000..01981aa4f --- /dev/null +++ b/docs/installation/basic-installation.rst @@ -0,0 +1,97 @@ +.. raw:: html + + + +.. _basic-installation: + +Basic Installation +================== + +.. note:: + + The following instructions will install Pillow with support for + most common image formats. See :ref:`external-libraries` for a + full list of external libraries supported. + +Install Pillow with :command:`pip`:: + + python3 -m pip install --upgrade pip + python3 -m pip install --upgrade Pillow + +Optionally, install :pypi:`defusedxml` for Pillow to read XMP data, +and :pypi:`olefile` for Pillow to read FPX and MIC images:: + + python3 -m pip install --upgrade defusedxml olefile + + +.. tab:: Linux + + We provide binaries for Linux for each of the supported Python + versions in the manylinux wheel format. These include support for all + optional libraries except libimagequant. Raqm support requires + FriBiDi to be installed separately:: + + python3 -m pip install --upgrade pip + python3 -m pip install --upgrade Pillow + + Most major Linux distributions, including Fedora, Ubuntu and ArchLinux + also include Pillow in packages that previously contained PIL e.g. + ``python-imaging``. Debian splits it into two packages, ``python3-pil`` + and ``python3-pil.imagetk``. + +.. tab:: macOS + + We provide binaries for macOS for each of the supported Python + versions in the wheel format. These include support for all optional + libraries except libimagequant. Raqm support requires + FriBiDi to be installed separately:: + + python3 -m pip install --upgrade pip + python3 -m pip install --upgrade Pillow + + While we provide binaries for both x86-64 and arm64, we do not provide universal2 + binaries. However, it is simple to combine our current binaries to create one:: + + python3 -m pip download --only-binary=:all: --platform macosx_10_10_x86_64 Pillow + python3 -m pip download --only-binary=:all: --platform macosx_11_0_arm64 Pillow + python3 -m pip install delocate + + Then, with the names of the downloaded wheels, use Python to combine them:: + + from delocate.fuse import fuse_wheels + fuse_wheels('Pillow-9.4.0-2-cp39-cp39-macosx_10_10_x86_64.whl', 'Pillow-9.4.0-cp39-cp39-macosx_11_0_arm64.whl', 'Pillow-9.4.0-cp39-cp39-macosx_11_0_universal2.whl') + +.. tab:: Windows + + We provide Pillow binaries for Windows compiled for the matrix of supported + Pythons in the wheel format. These include x86, x86-64 and arm64 versions + (with the exception of Python 3.8 on arm64). These binaries include support + for all optional libraries except libimagequant and libxcb. Raqm support + requires FriBiDi to be installed separately:: + + python3 -m pip install --upgrade pip + python3 -m pip install --upgrade Pillow + + To install Pillow in MSYS2, see :ref:`building-from-source`. + +.. tab:: FreeBSD + + Pillow can be installed on FreeBSD via the official Ports or Packages systems: + + **Ports**:: + + cd /usr/ports/graphics/py-pillow && make install clean + + **Packages**:: + + pkg install py38-pillow + + .. note:: + + The `Pillow FreeBSD port + `_ and packages + are tested by the ports team with all supported FreeBSD versions. diff --git a/docs/installation/building-from-source.rst b/docs/installation/building-from-source.rst new file mode 100644 index 000000000..349d191a6 --- /dev/null +++ b/docs/installation/building-from-source.rst @@ -0,0 +1,317 @@ +.. raw:: html + + + +.. _building-from-source: + +Building From Source +==================== + +.. _external-libraries: + +External Libraries +------------------ + +.. note:: + + You **do not need to install all supported external libraries** to + use Pillow's basic features. **Zlib** and **libjpeg** are required + by default. + +.. note:: + + There are Dockerfiles in our `Docker images repo + `_ to install the + dependencies for some operating systems. + +Many of Pillow's features require external libraries: + +* **libjpeg** provides JPEG functionality. + + * Pillow has been tested with libjpeg versions **6b**, **8**, **9-9d** and + libjpeg-turbo version **8**. + * Starting with Pillow 3.0.0, libjpeg is required by default. It can be + disabled with the ``-C jpeg=disable`` flag. + +* **zlib** provides access to compressed PNGs + + * Starting with Pillow 3.0.0, zlib is required by default. It can be + disabled with the ``-C zlib=disable`` flag. + +* **libtiff** provides compressed TIFF functionality + + * Pillow has been tested with libtiff versions **3.x** and **4.0-4.6.0** + +* **libfreetype** provides type related services + +* **littlecms** provides color management + + * Pillow version 2.2.1 and below uses liblcms1, Pillow 2.3.0 and + above uses liblcms2. Tested with **1.19** and **2.7-2.16**. + +* **libwebp** provides the WebP format. + + * Pillow has been tested with version **0.1.3**, which does not read + transparent WebP files. Versions **0.3.0** and above support + transparency. + +* **openjpeg** provides JPEG 2000 functionality. + + * Pillow has been tested with openjpeg **2.0.0**, **2.1.0**, **2.3.1**, + **2.4.0**, **2.5.0** and **2.5.2**. + * Pillow does **not** support the earlier **1.5** series which ships + with Debian Jessie. + +* **libimagequant** provides improved color quantization + + * Pillow has been tested with libimagequant **2.6-4.2.2** + * Libimagequant is licensed GPLv3, which is more restrictive than + the Pillow license, therefore we will not be distributing binaries + with libimagequant support enabled. + +* **libraqm** provides complex text layout support. + + * libraqm provides bidirectional text support (using FriBiDi), + shaping (using HarfBuzz), and proper script itemization. As a + result, Raqm can support most writing systems covered by Unicode. + * libraqm depends on the following libraries: FreeType, HarfBuzz, + FriBiDi, make sure that you install them before installing libraqm + if not available as package in your system. + * Setting text direction or font features is not supported without libraqm. + * Pillow wheels since version 8.2.0 include a modified version of libraqm that + loads libfribidi at runtime if it is installed. + On Windows this requires compiling FriBiDi and installing ``fribidi.dll`` + into a directory listed in the `Dynamic-link library search order (Microsoft Learn) + `_ + (``fribidi-0.dll`` or ``libfribidi-0.dll`` are also detected). + See `Build Options`_ to see how to build this version. + * Previous versions of Pillow (5.0.0 to 8.1.2) linked libraqm dynamically at runtime. + +* **libxcb** provides X11 screengrab support. + +.. tab:: Linux + + If you didn't build Python from source, make sure you have Python's + development libraries installed. + + In Debian or Ubuntu:: + + sudo apt-get install python3-dev python3-setuptools + + In Fedora, the command is:: + + sudo dnf install python3-devel redhat-rpm-config + + In Alpine, the command is:: + + sudo apk add python3-dev py3-setuptools + + .. Note:: ``redhat-rpm-config`` is required on Fedora 23, but not earlier versions. + + Prerequisites for **Ubuntu 16.04 LTS - 22.04 LTS** are installed with:: + + sudo apt-get install libtiff5-dev libjpeg8-dev libopenjp2-7-dev zlib1g-dev \ + libfreetype6-dev liblcms2-dev libwebp-dev tcl8.6-dev tk8.6-dev python3-tk \ + libharfbuzz-dev libfribidi-dev libxcb1-dev + + To install libraqm, ``sudo apt-get install meson`` and then see + ``depends/install_raqm.sh``. + + Prerequisites are installed on recent **Red Hat**, **CentOS** or **Fedora** with:: + + sudo dnf install libtiff-devel libjpeg-devel openjpeg2-devel zlib-devel \ + freetype-devel lcms2-devel libwebp-devel tcl-devel tk-devel \ + harfbuzz-devel fribidi-devel libraqm-devel libimagequant-devel libxcb-devel + + Note that the package manager may be yum or DNF, depending on the + exact distribution. + + Prerequisites are installed for **Alpine** with:: + + sudo apk add tiff-dev jpeg-dev openjpeg-dev zlib-dev freetype-dev lcms2-dev \ + libwebp-dev tcl-dev tk-dev harfbuzz-dev fribidi-dev libimagequant-dev \ + libxcb-dev libpng-dev + + See also the ``Dockerfile``\s in the Test Infrastructure repo + (https://github.com/python-pillow/docker-images) for a known working + install process for other tested distros. + +.. tab:: macOS + + The Xcode command line tools are required to compile portions of + Pillow. The tools are installed by running ``xcode-select --install`` + from the command line. The command line tools are required even if you + have the full Xcode package installed. It may be necessary to run + ``sudo xcodebuild -license`` to accept the license prior to using the + tools. + + The easiest way to install external libraries is via `Homebrew + `_. After you install Homebrew, run:: + + brew install libjpeg libtiff little-cms2 openjpeg webp + + To install libraqm on macOS use Homebrew to install its dependencies:: + + brew install freetype harfbuzz fribidi + + Then see ``depends/install_raqm_cmake.sh`` to install libraqm. + +.. tab:: Windows + + We recommend you use prebuilt wheels from PyPI. + If you wish to compile Pillow manually, you can use the build scripts + in the ``winbuild`` directory used for CI testing and development. + These scripts require Visual Studio 2017 or newer and NASM. + + The scripts also install Pillow from the local copy of the source code, so the + `Installing`_ instructions will not be necessary afterwards. + +.. tab:: Windows using MSYS2/MinGW + + To build Pillow using MSYS2, make sure you run the **MSYS2 MinGW 32-bit** or + **MSYS2 MinGW 64-bit** console, *not* **MSYS2** directly. + + The following instructions target the 64-bit build, for 32-bit + replace all occurrences of ``mingw-w64-x86_64-`` with ``mingw-w64-i686-``. + + Make sure you have Python and GCC installed:: + + pacman -S \ + mingw-w64-x86_64-gcc \ + mingw-w64-x86_64-python3 \ + mingw-w64-x86_64-python3-pip \ + mingw-w64-x86_64-python3-setuptools + + Prerequisites are installed on **MSYS2 MinGW 64-bit** with:: + + pacman -S \ + mingw-w64-x86_64-libjpeg-turbo \ + mingw-w64-x86_64-zlib \ + mingw-w64-x86_64-libtiff \ + mingw-w64-x86_64-freetype \ + mingw-w64-x86_64-lcms2 \ + mingw-w64-x86_64-libwebp \ + mingw-w64-x86_64-openjpeg2 \ + mingw-w64-x86_64-libimagequant \ + mingw-w64-x86_64-libraqm + + https://www.msys2.org/docs/python/ states that setuptools >= 60 does not work with + MSYS2. To workaround this, before installing Pillow you must run:: + + export SETUPTOOLS_USE_DISTUTILS=stdlib + +.. tab:: FreeBSD + + .. Note:: Only FreeBSD 10 and 11 tested + + Make sure you have Python's development libraries installed:: + + sudo pkg install python3 + + Prerequisites are installed on **FreeBSD 10 or 11** with:: + + sudo pkg install jpeg-turbo tiff webp lcms2 freetype2 openjpeg harfbuzz fribidi libxcb + + Then see ``depends/install_raqm_cmake.sh`` to install libraqm. + +.. tab:: Android + + Basic Android support has been added for compilation within the Termux + environment. The dependencies can be installed by:: + + pkg install -y python ndk-sysroot clang make \ + libjpeg-turbo + + This has been tested within the Termux app on ChromeOS, on x86. + +Installing +---------- + +Once you have installed the prerequisites, to install Pillow from the source +code on PyPI, run:: + + python3 -m pip install --upgrade pip + python3 -m pip install --upgrade Pillow --no-binary :all: + +If the prerequisites are installed in the standard library locations +for your machine (e.g. :file:`/usr` or :file:`/usr/local`), no +additional configuration should be required. If they are installed in +a non-standard location, you may need to configure setuptools to use +those locations by editing :file:`setup.py` or +:file:`pyproject.toml`, or by adding environment variables on the command +line:: + + CFLAGS="-I/usr/pkg/include" python3 -m pip install --upgrade Pillow --no-binary :all: + +If Pillow has been previously built without the required +prerequisites, it may be necessary to manually clear the pip cache or +build without cache using the ``--no-cache-dir`` option to force a +build with newly installed external libraries. + +If you would like to install from a local copy of the source code instead, you +can clone from GitHub with ``git clone https://github.com/python-pillow/Pillow`` +or download and extract the `compressed archive from PyPI`_. + +After navigating to the Pillow directory, run:: + + python3 -m pip install --upgrade pip + python3 -m pip install . + +.. _compressed archive from PyPI: https://pypi.org/project/pillow/#files + +Build Options +^^^^^^^^^^^^^ + +* Environment variable: ``MAX_CONCURRENCY=n``. Pillow can use + multiprocessing to build the extension. Setting ``MAX_CONCURRENCY`` + sets the number of CPUs to use, or can disable parallel building by + using a setting of 1. By default, it uses 4 CPUs, or if 4 are not + available, as many as are present. + +* Config settings: ``-C zlib=disable``, ``-C jpeg=disable``, + ``-C tiff=disable``, ``-C freetype=disable``, ``-C raqm=disable``, + ``-C lcms=disable``, ``-C webp=disable``, ``-C webpmux=disable``, + ``-C jpeg2000=disable``, ``-C imagequant=disable``, ``-C xcb=disable``. + Disable building the corresponding feature even if the development + libraries are present on the building machine. + +* Config settings: ``-C zlib=enable``, ``-C jpeg=enable``, + ``-C tiff=enable``, ``-C freetype=enable``, ``-C raqm=enable``, + ``-C lcms=enable``, ``-C webp=enable``, ``-C webpmux=enable``, + ``-C jpeg2000=enable``, ``-C imagequant=enable``, ``-C xcb=enable``. + Require that the corresponding feature is built. The build will raise + an exception if the libraries are not found. Webpmux (WebP metadata) + relies on WebP support. Tcl and Tk also must be used together. + +* Config settings: ``-C raqm=vendor``, ``-C fribidi=vendor``. + These flags are used to compile a modified version of libraqm and + a shim that dynamically loads libfribidi at runtime. These are + used to compile the standard Pillow wheels. Compiling libraqm requires + a C99-compliant compiler. + +* Build flag: ``-C platform-guessing=disable``. Skips all of the + platform dependent guessing of include and library directories for + automated build systems that configure the proper paths in the + environment variables (e.g. Buildroot). + +* Build flag: ``-C debug=true``. Adds a debugging flag to the include and + library search process to dump all paths searched for and found to + stdout. + + +Sample usage:: + + python3 -m pip install --upgrade Pillow -C [feature]=enable + +.. _old-versions: + +Old Versions +============ + +You can download old distributions from the `release history at PyPI +`_ and by direct URL access +eg. https://pypi.org/project/pillow/1.0/. diff --git a/docs/installation/index.rst b/docs/installation/index.rst new file mode 100644 index 000000000..a94204b6b --- /dev/null +++ b/docs/installation/index.rst @@ -0,0 +1,10 @@ +Installation +============ + +.. toctree:: + :maxdepth: 2 + + basic-installation + python-support + platform-support + building-from-source diff --git a/docs/newer-versions.csv b/docs/installation/newer-versions.csv similarity index 100% rename from docs/newer-versions.csv rename to docs/installation/newer-versions.csv diff --git a/docs/older-versions.csv b/docs/installation/older-versions.csv similarity index 100% rename from docs/older-versions.csv rename to docs/installation/older-versions.csv diff --git a/docs/installation/platform-support.rst b/docs/installation/platform-support.rst new file mode 100644 index 000000000..59fc312ab --- /dev/null +++ b/docs/installation/platform-support.rst @@ -0,0 +1,170 @@ +.. _platform-support: + +Platform Support +================ + +Current platform support for Pillow. Binary distributions are +contributed for each release on a volunteer basis, but the source +should compile and run everywhere platform support is listed. In +general, we aim to support all current versions of Linux, macOS, and +Windows. + +Continuous Integration Targets +------------------------------ + +These platforms are built and tested for every change. + ++----------------------------------+----------------------------+---------------------+ +| Operating system | Tested Python versions | Tested architecture | ++==================================+============================+=====================+ +| Alpine | 3.9 | x86-64 | ++----------------------------------+----------------------------+---------------------+ +| Amazon Linux 2 | 3.9 | x86-64 | ++----------------------------------+----------------------------+---------------------+ +| Amazon Linux 2023 | 3.9 | x86-64 | ++----------------------------------+----------------------------+---------------------+ +| Arch | 3.9 | x86-64 | ++----------------------------------+----------------------------+---------------------+ +| CentOS 7 | 3.9 | x86-64 | ++----------------------------------+----------------------------+---------------------+ +| CentOS Stream 8 | 3.9 | x86-64 | ++----------------------------------+----------------------------+---------------------+ +| CentOS Stream 9 | 3.9 | x86-64 | ++----------------------------------+----------------------------+---------------------+ +| Debian 11 Bullseye | 3.9 | x86-64 | ++----------------------------------+----------------------------+---------------------+ +| Debian 12 Bookworm | 3.11 | x86, x86-64 | ++----------------------------------+----------------------------+---------------------+ +| Fedora 38 | 3.11 | x86-64 | ++----------------------------------+----------------------------+---------------------+ +| Fedora 39 | 3.12 | x86-64 | ++----------------------------------+----------------------------+---------------------+ +| Gentoo | 3.9 | x86-64 | ++----------------------------------+----------------------------+---------------------+ +| macOS 12 Monterey | 3.8, 3.9 | x86-64 | ++----------------------------------+----------------------------+---------------------+ +| macOS 14 Sonoma | 3.10, 3.11, 3.12, 3.13, | arm64 | +| | PyPy3 | | ++----------------------------------+----------------------------+---------------------+ +| Ubuntu Linux 20.04 LTS (Focal) | 3.8 | x86-64 | ++----------------------------------+----------------------------+---------------------+ +| Ubuntu Linux 22.04 LTS (Jammy) | 3.8, 3.9, 3.10, 3.11, | x86-64 | +| | 3.12, 3.13, PyPy3 | | +| +----------------------------+---------------------+ +| | 3.10 | arm64v8, ppc64le, | +| | | s390x | ++----------------------------------+----------------------------+---------------------+ +| Windows Server 2016 | 3.8 | x86-64 | ++----------------------------------+----------------------------+---------------------+ +| Windows Server 2022 | 3.8, 3.9, 3.10, 3.11, | x86-64 | +| | 3.12, 3.13, PyPy3 | | +| +----------------------------+---------------------+ +| | 3.12 | x86 | +| +----------------------------+---------------------+ +| | 3.9 (MinGW) | x86-64 | +| +----------------------------+---------------------+ +| | 3.8, 3.9 (Cygwin) | x86-64 | ++----------------------------------+----------------------------+---------------------+ + + +Other Platforms +--------------- + +These platforms have been reported to work at the versions mentioned. + +.. note:: + + Contributors please test Pillow on your platform then update this + document and send a pull request. + ++----------------------------------+----------------------------+------------------+--------------+ +| Operating system | | Tested Python | | Latest tested | | Tested | +| | | versions | | Pillow version | | processors | ++==================================+============================+==================+==============+ +| macOS 14 Sonoma | 3.8, 3.9, 3.10, 3.11, 3.12 | 10.2.0 |arm | ++----------------------------------+----------------------------+------------------+--------------+ +| macOS 13 Ventura | 3.8, 3.9, 3.10, 3.11 | 10.0.1 |arm | +| +----------------------------+------------------+ | +| | 3.7 | 9.5.0 | | ++----------------------------------+----------------------------+------------------+--------------+ +| macOS 12 Monterey | 3.7, 3.8, 3.9, 3.10, 3.11 | 9.3.0 |arm | ++----------------------------------+----------------------------+------------------+--------------+ +| macOS 11 Big Sur | 3.7, 3.8, 3.9, 3.10 | 8.4.0 |arm | +| +----------------------------+------------------+--------------+ +| | 3.7, 3.8, 3.9, 3.10, 3.11 | 9.4.0 |x86-64 | +| +----------------------------+------------------+ | +| | 3.6 | 8.4.0 | | ++----------------------------------+----------------------------+------------------+--------------+ +| macOS 10.15 Catalina | 3.6, 3.7, 3.8, 3.9 | 8.3.2 |x86-64 | +| +----------------------------+------------------+ | +| | 3.5 | 7.2.0 | | ++----------------------------------+----------------------------+------------------+--------------+ +| macOS 10.14 Mojave | 3.5, 3.6, 3.7, 3.8 | 7.2.0 |x86-64 | +| +----------------------------+------------------+ | +| | 2.7 | 6.0.0 | | +| +----------------------------+------------------+ | +| | 3.4 | 5.4.1 | | ++----------------------------------+----------------------------+------------------+--------------+ +| macOS 10.13 High Sierra | 2.7, 3.4, 3.5, 3.6 | 4.2.1 |x86-64 | ++----------------------------------+----------------------------+------------------+--------------+ +| macOS 10.12 Sierra | 2.7, 3.4, 3.5, 3.6 | 4.1.1 |x86-64 | ++----------------------------------+----------------------------+------------------+--------------+ +| Mac OS X 10.11 El Capitan | 2.7, 3.4, 3.5, 3.6, 3.7 | 5.4.1 |x86-64 | +| +----------------------------+------------------+ | +| | 3.3 | 4.1.0 | | ++----------------------------------+----------------------------+------------------+--------------+ +| Mac OS X 10.9 Mavericks | 2.7, 3.2, 3.3, 3.4 | 3.0.0 |x86-64 | ++----------------------------------+----------------------------+------------------+--------------+ +| Mac OS X 10.8 Mountain Lion | 2.6, 2.7, 3.2, 3.3 | |x86-64 | ++----------------------------------+----------------------------+------------------+--------------+ +| Redhat Linux 6 | 2.6 | |x86 | ++----------------------------------+----------------------------+------------------+--------------+ +| CentOS 6.3 | 2.7, 3.3 | |x86 | ++----------------------------------+----------------------------+------------------+--------------+ +| CentOS 8 | 3.9 | 9.0.0 |x86-64 | ++----------------------------------+----------------------------+------------------+--------------+ +| Fedora 23 | 2.7, 3.4 | 3.1.0 |x86-64 | ++----------------------------------+----------------------------+------------------+--------------+ +| Ubuntu Linux 12.04 LTS (Precise) | | 2.6, 3.2, 3.3, 3.4, 3.5 | 3.4.1 |x86,x86-64 | +| | | PyPy5.3.1, PyPy3 v2.4.0 | | | +| +----------------------------+------------------+--------------+ +| | 2.7 | 4.3.0 |x86-64 | +| +----------------------------+------------------+--------------+ +| | 2.7, 3.2 | 3.4.1 |ppc | ++----------------------------------+----------------------------+------------------+--------------+ +| Ubuntu Linux 10.04 LTS (Lucid) | 2.6 | 2.3.0 |x86,x86-64 | ++----------------------------------+----------------------------+------------------+--------------+ +| Debian 8.2 Jessie | 2.7, 3.4 | 3.1.0 |x86-64 | ++----------------------------------+----------------------------+------------------+--------------+ +| Raspbian Jessie | 2.7, 3.4 | 3.1.0 |arm | ++----------------------------------+----------------------------+------------------+--------------+ +| Raspbian Stretch | 2.7, 3.5 | 4.0.0 |arm | ++----------------------------------+----------------------------+------------------+--------------+ +| Raspberry Pi OS | 3.6, 3.7, 3.8, 3.9 | 8.2.0 |arm | +| +----------------------------+------------------+ | +| | 2.7 | 6.2.2 | | ++----------------------------------+----------------------------+------------------+--------------+ +| Gentoo Linux | 2.7, 3.2 | 2.1.0 |x86-64 | ++----------------------------------+----------------------------+------------------+--------------+ +| FreeBSD 11.1 | 2.7, 3.4, 3.5, 3.6 | 4.3.0 |x86-64 | ++----------------------------------+----------------------------+------------------+--------------+ +| FreeBSD 10.3 | 2.7, 3.4, 3.5 | 4.2.0 |x86-64 | ++----------------------------------+----------------------------+------------------+--------------+ +| FreeBSD 10.2 | 2.7, 3.4 | 3.1.0 |x86-64 | ++----------------------------------+----------------------------+------------------+--------------+ +| Windows 11 | 3.9, 3.10, 3.11, 3.12 | 10.2.0 |arm64 | ++----------------------------------+----------------------------+------------------+--------------+ +| Windows 11 Pro | 3.11, 3.12 | 10.2.0 |x86-64 | ++----------------------------------+----------------------------+------------------+--------------+ +| Windows 10 | 3.7 | 7.1.0 |x86-64 | ++----------------------------------+----------------------------+------------------+--------------+ +| Windows 10/Cygwin 3.3 | 3.6, 3.7, 3.8, 3.9 | 8.4.0 |x86-64 | ++----------------------------------+----------------------------+------------------+--------------+ +| Windows 8.1 Pro | 2.6, 2.7, 3.2, 3.3, 3.4 | 2.4.0 |x86,x86-64 | ++----------------------------------+----------------------------+------------------+--------------+ +| Windows 8 Pro | 2.6, 2.7, 3.2, 3.3, 3.4a3 | 2.2.0 |x86,x86-64 | ++----------------------------------+----------------------------+------------------+--------------+ +| Windows 7 Professional | 3.7 | 7.0.0 |x86,x86-64 | ++----------------------------------+----------------------------+------------------+--------------+ +| Windows Server 2008 R2 Enterprise| 3.3 | |x86-64 | ++----------------------------------+----------------------------+------------------+--------------+ diff --git a/docs/installation/python-support.rst b/docs/installation/python-support.rst new file mode 100644 index 000000000..dd5765b6b --- /dev/null +++ b/docs/installation/python-support.rst @@ -0,0 +1,14 @@ +.. _python-support: + +Python Support +============== + +Pillow supports these Python versions. + +.. csv-table:: Newer versions + :file: newer-versions.csv + :header-rows: 1 + +.. csv-table:: Older versions + :file: older-versions.csv + :header-rows: 1 diff --git a/docs/reference/ImageOps.rst b/docs/reference/ImageOps.rst index 475253078..051fdcfc9 100644 --- a/docs/reference/ImageOps.rst +++ b/docs/reference/ImageOps.rst @@ -14,6 +14,8 @@ only work on L and RGB images. .. autofunction:: colorize .. autofunction:: crop .. autofunction:: scale +.. autoclass:: SupportsGetMesh + :show-inheritance: .. autofunction:: deform .. autofunction:: equalize .. autofunction:: expand diff --git a/docs/reference/internal_design.rst b/docs/reference/internal_design.rst index 2e2d3322f..99a18e9ea 100644 --- a/docs/reference/internal_design.rst +++ b/docs/reference/internal_design.rst @@ -1,5 +1,5 @@ -Internal Reference Docs -======================= +Internal Reference +================== .. toctree:: :maxdepth: 2 diff --git a/docs/reference/internal_modules.rst b/docs/reference/internal_modules.rst index f2932c322..899e4966f 100644 --- a/docs/reference/internal_modules.rst +++ b/docs/reference/internal_modules.rst @@ -33,6 +33,14 @@ Internal Modules Provides a convenient way to import type hints that are not available on some Python versions. +.. py:class:: StrOrBytesPath + + Typing alias. + +.. py:class:: SupportsRead + + An object that supports the read method. + .. py:data:: TypeGuard :value: typing.TypeGuard diff --git a/docs/reference/open_files.rst b/docs/reference/open_files.rst index f31941c9a..730c8da5b 100644 --- a/docs/reference/open_files.rst +++ b/docs/reference/open_files.rst @@ -3,7 +3,7 @@ File Handling in Pillow ======================= -When opening a file as an image, Pillow requires a filename, ``pathlib.Path`` +When opening a file as an image, Pillow requires a filename, ``os.PathLike`` object, or a file-like object. Pillow uses the filename or ``Path`` to open a file, so for the rest of this article, they will all be treated as a file-like object. diff --git a/docs/releasenotes/10.3.0.rst b/docs/releasenotes/10.3.0.rst index 8772a382d..af31cdb74 100644 --- a/docs/releasenotes/10.3.0.rst +++ b/docs/releasenotes/10.3.0.rst @@ -79,3 +79,9 @@ Portable FloatMap (PFM) images Support has been added for reading and writing grayscale (Pf format) Portable FloatMap (PFM) files containing ``F`` data. + +Release GIL when fetching WebP frames +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Python's Global Interpreter Lock is now released when fetching WebP frames from +the libwebp decoder. diff --git a/pyproject.toml b/pyproject.toml index b1ce9cf1d..518facc34 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,6 +33,7 @@ classifiers = [ "Topic :: Multimedia :: Graphics :: Capture :: Screen Capture", "Topic :: Multimedia :: Graphics :: Graphics Conversion", "Topic :: Multimedia :: Graphics :: Viewers", + "Typing :: Typed", ] dynamic = [ "version", @@ -79,7 +80,6 @@ Homepage = "https://python-pillow.org" Mastodon = "https://fosstodon.org/@pillow" "Release notes" = "https://pillow.readthedocs.io/en/stable/releasenotes/index.html" Source = "https://github.com/python-pillow/Pillow" -Twitter = "https://twitter.com/PythonPillow" [tool.setuptools] packages = ["PIL"] @@ -96,7 +96,7 @@ config-settings = "raqm=enable raqm=vendor fribidi=vendor imagequant=disable" test-command = "cd {project} && .github/workflows/wheels-test.sh" test-extras = "tests" -[tool.ruff] +[tool.ruff.lint] select = [ "C4", # flake8-comprehensions "E", # pycodestyle errors @@ -104,25 +104,25 @@ select = [ "F", # pyflakes errors "I", # isort "ISC", # flake8-implicit-str-concat + "LOG", # flake8-logging "PGH", # pygrep-hooks "RUF100", # unused noqa (yesqa) "UP", # pyupgrade "W", # pycodestyle warnings "YTT", # flake8-2020 - # "LOG", # TODO: enable flake8-logging when it's not in preview anymore ] -extend-ignore = [ +ignore = [ "E203", # Whitespace before ':' "E221", # Multiple spaces before operator "E226", # Missing whitespace around arithmetic operator "E241", # Multiple spaces after ',' ] -[tool.ruff.per-file-ignores] +[tool.ruff.lint.per-file-ignores] "Tests/oss-fuzz/fuzz_font.py" = ["I002"] "Tests/oss-fuzz/fuzz_pillow.py" = ["I002"] -[tool.ruff.isort] +[tool.ruff.lint.isort] known-first-party = ["PIL"] required-imports = ["from __future__ import annotations"] @@ -140,17 +140,3 @@ follow_imports = "silent" warn_redundant_casts = true warn_unreachable = true warn_unused_ignores = true -exclude = [ - '^src/PIL/_tkinter_finder.py$', - '^src/PIL/DdsImagePlugin.py$', - '^src/PIL/FpxImagePlugin.py$', - '^src/PIL/Image.py$', - '^src/PIL/ImageQt.py$', - '^src/PIL/ImImagePlugin.py$', - '^src/PIL/MicImagePlugin.py$', - '^src/PIL/PdfParser.py$', - '^src/PIL/PyAccess.py$', - '^src/PIL/TiffImagePlugin.py$', - '^src/PIL/TiffTags.py$', - '^src/PIL/WebPImagePlugin.py$', -] diff --git a/setup.py b/setup.py old mode 100755 new mode 100644 index 1bf0bcff5..1bbd2c05c --- a/setup.py +++ b/setup.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # > pyroma . # ------------------------------ # Checking . diff --git a/src/PIL/BlpImagePlugin.py b/src/PIL/BlpImagePlugin.py index b8f38b78a..f0fbc8cc2 100644 --- a/src/PIL/BlpImagePlugin.py +++ b/src/PIL/BlpImagePlugin.py @@ -28,6 +28,7 @@ BLP files come in many different flavours: - DXT3 compression is used if alpha_encoding == 1. - DXT5 compression is used if alpha_encoding == 7. """ + from __future__ import annotations import os diff --git a/src/PIL/DdsImagePlugin.py b/src/PIL/DdsImagePlugin.py index eb4c8f557..be17f4223 100644 --- a/src/PIL/DdsImagePlugin.py +++ b/src/PIL/DdsImagePlugin.py @@ -9,6 +9,7 @@ The contents of this file are hereby released in the public domain (CC0) Full text of the CC0 license: https://creativecommons.org/publicdomain/zero/1.0/ """ + from __future__ import annotations import io @@ -269,13 +270,17 @@ class D3DFMT(IntEnum): # Backward compatibility layer module = sys.modules[__name__] for item in DDSD: + assert item.name is not None setattr(module, "DDSD_" + item.name, item.value) -for item in DDSCAPS: - setattr(module, "DDSCAPS_" + item.name, item.value) -for item in DDSCAPS2: - setattr(module, "DDSCAPS2_" + item.name, item.value) -for item in DDPF: - setattr(module, "DDPF_" + item.name, item.value) +for item1 in DDSCAPS: + assert item1.name is not None + setattr(module, "DDSCAPS_" + item1.name, item1.value) +for item2 in DDSCAPS2: + assert item2.name is not None + setattr(module, "DDSCAPS2_" + item2.name, item2.value) +for item3 in DDPF: + assert item3.name is not None + setattr(module, "DDPF_" + item3.name, item3.value) DDS_FOURCC = DDPF.FOURCC DDS_RGB = DDPF.RGB diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py index d2e60aa07..726359c67 100644 --- a/src/PIL/EpsImagePlugin.py +++ b/src/PIL/EpsImagePlugin.py @@ -38,7 +38,7 @@ from ._deprecate import deprecate split = re.compile(r"^%%([^:]*):[ \t]*(.*)[ \t]*$") field = re.compile(r"^%[%!\w]([^:]*)[ \t]*$") -gs_binary = None +gs_binary: str | bool | None = None gs_windows_binary = None diff --git a/src/PIL/FliImagePlugin.py b/src/PIL/FliImagePlugin.py index 9769761fc..f9e4c731c 100644 --- a/src/PIL/FliImagePlugin.py +++ b/src/PIL/FliImagePlugin.py @@ -77,6 +77,7 @@ class FliImageFile(ImageFile.ImageFile): if i16(s, 4) == 0xF100: # prefix chunk; ignore it self.__offset = self.__offset + i32(s) + self.fp.seek(self.__offset) s = self.fp.read(16) if i16(s, 4) == 0xF1FA: diff --git a/src/PIL/FontFile.py b/src/PIL/FontFile.py index 3ec1ae819..1e0c1c166 100644 --- a/src/PIL/FontFile.py +++ b/src/PIL/FontFile.py @@ -50,9 +50,7 @@ class FontFile: | None ] = [None] * 256 - def __getitem__( - self, ix: int - ) -> ( + def __getitem__(self, ix: int) -> ( tuple[ tuple[int, int], tuple[int, int, int, int], diff --git a/src/PIL/FtexImagePlugin.py b/src/PIL/FtexImagePlugin.py index d5513a56a..b4488e6ee 100644 --- a/src/PIL/FtexImagePlugin.py +++ b/src/PIL/FtexImagePlugin.py @@ -50,6 +50,7 @@ bytes for that mipmap level. Note: All data is stored in little-Endian (Intel) byte order. """ + from __future__ import annotations import struct diff --git a/src/PIL/GdImageFile.py b/src/PIL/GdImageFile.py index 7bb4736af..88b87a22c 100644 --- a/src/PIL/GdImageFile.py +++ b/src/PIL/GdImageFile.py @@ -27,11 +27,12 @@ """ from __future__ import annotations -from io import BytesIO +from typing import IO from . import ImageFile, ImagePalette, UnidentifiedImageError from ._binary import i16be as i16 from ._binary import i32be as i32 +from ._typing import StrOrBytesPath class GdImageFile(ImageFile.ImageFile): @@ -80,7 +81,7 @@ class GdImageFile(ImageFile.ImageFile): ] -def open(fp: BytesIO, mode: str = "r") -> GdImageFile: +def open(fp: StrOrBytesPath | IO[bytes], mode: str = "r") -> GdImageFile: """ Load texture from a GD image file. diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index 57d87078b..b38b43013 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -641,17 +641,15 @@ def _write_multiple_frames(im, fp, palette): if encoderinfo.get("optimize") and im_frame.mode != "1": if "transparency" not in encoderinfo: try: - encoderinfo[ - "transparency" - ] = im_frame.palette._new_color_index(im_frame) + encoderinfo["transparency"] = ( + im_frame.palette._new_color_index(im_frame) + ) except ValueError: pass if "transparency" in encoderinfo: # When the delta is zero, fill the image with transparency diff_frame = im_frame.copy() - fill = Image.new( - "P", diff_frame.size, encoderinfo["transparency"] - ) + fill = Image.new("P", delta.size, encoderinfo["transparency"]) if delta.mode == "RGBA": r, g, b, a = delta.split() mask = ImageMath.eval( diff --git a/src/PIL/ImImagePlugin.py b/src/PIL/ImImagePlugin.py index 97d726a8a..4613e40b6 100644 --- a/src/PIL/ImImagePlugin.py +++ b/src/PIL/ImImagePlugin.py @@ -93,8 +93,8 @@ for i in ["16", "16L", "16B"]: for i in ["32S"]: OPEN[f"L {i} image"] = ("I", f"I;{i}") OPEN[f"L*{i} image"] = ("I", f"I;{i}") -for i in range(2, 33): - OPEN[f"L*{i} image"] = ("F", f"F;{i}") +for j in range(2, 33): + OPEN[f"L*{j} image"] = ("F", f"F;{j}") # -------------------------------------------------------------------- diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 553f36703..5f2baa69b 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -26,6 +26,7 @@ from __future__ import annotations +import abc import atexit import builtins import io @@ -39,12 +40,8 @@ import tempfile import warnings from collections.abc import Callable, MutableMapping from enum import IntEnum -from pathlib import Path - -try: - from defusedxml import ElementTree -except ImportError: - ElementTree = None +from types import ModuleType +from typing import IO, TYPE_CHECKING, Any # VERSION was removed in Pillow 6.0.0. # PILLOW_VERSION was removed in Pillow 9.0.0. @@ -60,6 +57,12 @@ from . import ( from ._binary import i32le, o32be, o32le from ._util import DeferredError, is_path +ElementTree: ModuleType | None +try: + from defusedxml import ElementTree +except ImportError: + ElementTree = None + logger = logging.getLogger(__name__) @@ -72,7 +75,7 @@ class DecompressionBombError(Exception): # Limit to around a quarter gigabyte for a 24-bit (3 bpp) image -MAX_IMAGE_PIXELS = int(1024 * 1024 * 1024 // 4 // 3) +MAX_IMAGE_PIXELS: int | None = int(1024 * 1024 * 1024 // 4 // 3) try: @@ -110,6 +113,7 @@ except ImportError as v: USE_CFFI_ACCESS = False +cffi: ModuleType | None try: import cffi except ImportError: @@ -211,14 +215,22 @@ if hasattr(core, "DEFAULT_STRATEGY"): # -------------------------------------------------------------------- # Registries -ID = [] -OPEN = {} -MIME = {} -SAVE = {} -SAVE_ALL = {} -EXTENSION = {} -DECODERS = {} -ENCODERS = {} +if TYPE_CHECKING: + from . import ImageFile +ID: list[str] = [] +OPEN: dict[ + str, + tuple[ + Callable[[IO[bytes], str | bytes], ImageFile.ImageFile], + Callable[[bytes], bool] | None, + ], +] = {} +MIME: dict[str, str] = {} +SAVE: dict[str, Callable[[Image, IO[bytes], str | bytes], None]] = {} +SAVE_ALL: dict[str, Callable[[Image, IO[bytes], str | bytes], None]] = {} +EXTENSION: dict[str, str] = {} +DECODERS: dict[str, object] = {} +ENCODERS: dict[str, object] = {} # -------------------------------------------------------------------- # Modes @@ -571,7 +583,7 @@ class Image: # object is gone. self.im = DeferredError(ValueError("Operation on closed image")) - def _copy(self): + def _copy(self) -> None: self.load() self.im = self.im.copy() self.pyaccess = None @@ -966,7 +978,7 @@ class Image: delete_trns = False # transparency handling if has_transparency: - if (self.mode in ("1", "L", "I") and mode in ("LA", "RGBA")) or ( + if (self.mode in ("1", "L", "I", "I;16") and mode in ("LA", "RGBA")) or ( self.mode == "RGB" and mode == "RGBA" ): # Use transparent conversion to promote from transparent @@ -1418,7 +1430,7 @@ class Image: root = ElementTree.fromstring(xmp_tags) return {get_name(root.tag): get_value(root)} - def getexif(self): + def getexif(self) -> Exif: """ Gets EXIF data from the image. @@ -1426,7 +1438,6 @@ class Image: """ if self._exif is None: self._exif = Exif() - self._exif._loaded = False elif self._exif._loaded: return self._exif self._exif._loaded = True @@ -1513,7 +1524,7 @@ class Image: self.load() return self.im.ptr - def getpalette(self, rawmode="RGB"): + def getpalette(self, rawmode: str | None = "RGB") -> list[int] | None: """ Returns the image palette as a list. @@ -1603,7 +1614,7 @@ class Image: x, y = self.im.getprojection() return list(x), list(y) - def histogram(self, mask=None, extrema=None): + def histogram(self, mask=None, extrema=None) -> list[int]: """ Returns a histogram for the image. The histogram is returned as a list of pixel counts, one for each pixel value in the source @@ -1792,7 +1803,7 @@ class Image: result = alpha_composite(background, overlay) self.paste(result, box) - def point(self, lut, mode=None): + def point(self, lut, mode: str | None = None) -> Image: """ Maps this image through a lookup table or function. @@ -1916,7 +1927,7 @@ class Image: self.im.putdata(data, scale, offset) - def putpalette(self, data, rawmode="RGB"): + def putpalette(self, data, rawmode="RGB") -> None: """ Attaches a palette to this image. The image must be a "P", "PA", "L" or "LA" image. @@ -2096,7 +2107,7 @@ class Image: min(self.size[1], math.ceil(box[3] + support_y)), ) - def resize(self, size, resample=None, box=None, reducing_gap=None): + def resize(self, size, resample=None, box=None, reducing_gap=None) -> Image: """ Returns a resized copy of this image. @@ -2188,10 +2199,11 @@ class Image: if factor_x > 1 or factor_y > 1: reduce_box = self._get_safe_box(size, resample, box) factor = (factor_x, factor_y) - if callable(self.reduce): - self = self.reduce(factor, box=reduce_box) - else: - self = Image.reduce(self, factor, box=reduce_box) + self = ( + self.reduce(factor, box=reduce_box) + if callable(self.reduce) + else Image.reduce(self, factor, box=reduce_box) + ) box = ( (box[0] - reduce_box[0]) / factor_x, (box[1] - reduce_box[1]) / factor_y, @@ -2370,7 +2382,7 @@ class Image: implement the ``seek``, ``tell``, and ``write`` methods, and be opened in binary mode. - :param fp: A filename (string), pathlib.Path object or file object. + :param fp: A filename (string), os.PathLike object or file object. :param format: Optional format override. If omitted, the format to use is determined from the filename extension. If a file object was used instead of a filename, this @@ -2383,13 +2395,10 @@ class Image: may have been created, and may contain partial data. """ - filename = "" + filename: str | bytes = "" open_fp = False - if isinstance(fp, Path): - filename = str(fp) - open_fp = True - elif is_path(fp): - filename = fp + if is_path(fp): + filename = os.path.realpath(os.fspath(fp)) open_fp = True elif fp == sys.stdout: try: @@ -2398,7 +2407,7 @@ class Image: pass if not filename and hasattr(fp, "name") and is_path(fp.name): # only set the name for metadata purposes - filename = fp.name + filename = os.path.realpath(os.fspath(fp.name)) # may mutate self! self._ensure_mutable() @@ -2409,7 +2418,8 @@ class Image: preinit() - ext = os.path.splitext(filename)[1].lower() + filename_ext = os.path.splitext(filename)[1].lower() + ext = filename_ext.decode() if isinstance(filename_ext, bytes) else filename_ext if not format: if ext not in EXTENSION: @@ -2451,7 +2461,7 @@ class Image: if open_fp: fp.close() - def seek(self, frame) -> Image: + def seek(self, frame) -> None: """ Seeks to the given frame in this sequence file. If you seek beyond the end of the sequence, the method raises an @@ -2511,10 +2521,8 @@ class Image: self.load() if self.im.bands == 1: - ims = [self.copy()] - else: - ims = map(self._new, self.im.split()) - return tuple(ims) + return (self.copy(),) + return tuple(map(self._new, self.im.split())) def getchannel(self, channel): """ @@ -2810,7 +2818,7 @@ class Image: self.im.transform2(box, image.im, method, data, resample, fill) - def transpose(self, method): + def transpose(self, method: Transpose) -> Image: """ Transpose image (flip or rotate in 90 degree steps) @@ -2862,7 +2870,9 @@ class ImagePointHandler: (for use with :py:meth:`~PIL.Image.Image.point`) """ - pass + @abc.abstractmethod + def point(self, im: Image) -> Image: + pass class ImageTransformHandler: @@ -2871,7 +2881,14 @@ class ImageTransformHandler: (for use with :py:meth:`~PIL.Image.Image.transform`) """ - pass + @abc.abstractmethod + def transform( + self, + size: tuple[int, int], + image: Image, + **options: dict[str, str | int | tuple[int, ...] | list[int]], + ) -> Image: + pass # -------------------------------------------------------------------- @@ -3206,7 +3223,7 @@ def open(fp, mode="r", formats=None) -> Image: :py:meth:`~PIL.Image.Image.load` method). See :py:func:`~PIL.Image.new`. See :ref:`file-handling`. - :param fp: A filename (string), pathlib.Path object or a file object. + :param fp: A filename (string), os.PathLike object or a file object. The file object must implement ``file.read``, ``file.seek``, and ``file.tell`` methods, and be opened in binary mode. The file object will also seek to zero @@ -3243,11 +3260,9 @@ def open(fp, mode="r", formats=None) -> Image: raise TypeError(msg) exclusive_fp = False - filename = "" - if isinstance(fp, Path): - filename = str(fp.resolve()) - elif is_path(fp): - filename = fp + filename: str | bytes = "" + if is_path(fp): + filename = os.path.realpath(os.fspath(fp)) if filename: fp = builtins.open(filename, "rb") @@ -3421,7 +3436,11 @@ def merge(mode, bands): # Plugin registry -def register_open(id, factory, accept=None) -> None: +def register_open( + id, + factory: Callable[[IO[bytes], str | bytes], ImageFile.ImageFile], + accept: Callable[[bytes], bool] | None = None, +) -> None: """ Register an image file plugin. This function should not be used in application code. @@ -3631,7 +3650,13 @@ _apply_env_variables() atexit.register(core.clear_cache) -class Exif(MutableMapping): +if TYPE_CHECKING: + _ExifBase = MutableMapping[int, Any] +else: + _ExifBase = MutableMapping + + +class Exif(_ExifBase): """ This class provides read and write access to EXIF image data:: @@ -3667,6 +3692,7 @@ class Exif(MutableMapping): endian = None bigtiff = False + _loaded = False def __init__(self): self._data = {} @@ -3782,7 +3808,7 @@ class Exif(MutableMapping): return merged_dict - def tobytes(self, offset=8): + def tobytes(self, offset: int = 8) -> bytes: from . import TiffImagePlugin head = self._get_head() @@ -3937,7 +3963,7 @@ class Exif(MutableMapping): del self._info[tag] self._data[tag] = value - def __delitem__(self, tag): + def __delitem__(self, tag: int) -> None: if self._info is not None and tag in self._info: del self._info[tag] else: diff --git a/src/PIL/ImageCms.py b/src/PIL/ImageCms.py index 3e40105e4..2b0ed6c9d 100644 --- a/src/PIL/ImageCms.py +++ b/src/PIL/ImageCms.py @@ -281,7 +281,6 @@ class ImageCmsProfile: class ImageCmsTransform(Image.ImagePointHandler): - """ Transform. This can be used with the procedural API, or with the standard :py:func:`~PIL.Image.Image.point` method. @@ -369,7 +368,6 @@ def get_display_profile(handle=None): class PyCMSError(Exception): - """(pyCMS) Exception class. This is used for all errors in the pyCMS API.""" diff --git a/src/PIL/ImageColor.py b/src/PIL/ImageColor.py index ad59b0667..5fb80b753 100644 --- a/src/PIL/ImageColor.py +++ b/src/PIL/ImageColor.py @@ -124,7 +124,7 @@ def getrgb(color): @lru_cache -def getcolor(color, mode): +def getcolor(color, mode: str) -> tuple[int, ...]: """ Same as :py:func:`~PIL.ImageColor.getrgb` for most modes. However, if ``mode`` is HSV, converts the RGB value to a HSV value, or if ``mode`` is diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index 84665f54f..d4e000087 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -34,8 +34,10 @@ from __future__ import annotations import math import numbers import struct +from typing import Sequence, cast from . import Image, ImageColor +from ._typing import Coords """ A simple 2D drawing interface for PIL images. @@ -48,7 +50,7 @@ directly. class ImageDraw: font = None - def __init__(self, im, mode=None): + def __init__(self, im: Image.Image, mode: str | None = None) -> None: """ Create a drawing instance. @@ -115,7 +117,7 @@ class ImageDraw: self.font = ImageFont.load_default() return self.font - def _getfont(self, font_size): + def _getfont(self, font_size: float | None): if font_size is not None: from . import ImageFont @@ -124,7 +126,7 @@ class ImageDraw: font = self.getfont() return font - def _getink(self, ink, fill=None): + def _getink(self, ink, fill=None) -> tuple[int | None, int | None]: if ink is None and fill is None: if self.fill: fill = self.ink @@ -145,13 +147,13 @@ class ImageDraw: fill = self.draw.draw_ink(fill) return ink, fill - def arc(self, xy, start, end, fill=None, width=1): + def arc(self, xy: Coords, start, end, fill=None, width=1) -> None: """Draw an arc.""" ink, fill = self._getink(fill) if ink is not None: self.draw.draw_arc(xy, start, end, ink, width) - def bitmap(self, xy, bitmap, fill=None): + def bitmap(self, xy: Sequence[int], bitmap, fill=None) -> None: """Draw a bitmap.""" bitmap.load() ink, fill = self._getink(fill) @@ -160,7 +162,7 @@ class ImageDraw: if ink is not None: self.draw.draw_bitmap(xy, bitmap.im, ink) - def chord(self, xy, start, end, fill=None, outline=None, width=1): + def chord(self, xy: Coords, start, end, fill=None, outline=None, width=1) -> None: """Draw a chord.""" ink, fill = self._getink(outline, fill) if fill is not None: @@ -168,7 +170,7 @@ class ImageDraw: if ink is not None and ink != fill and width != 0: self.draw.draw_chord(xy, start, end, ink, 0, width) - def ellipse(self, xy, fill=None, outline=None, width=1): + def ellipse(self, xy: Coords, fill=None, outline=None, width=1) -> None: """Draw an ellipse.""" ink, fill = self._getink(outline, fill) if fill is not None: @@ -176,20 +178,29 @@ class ImageDraw: if ink is not None and ink != fill and width != 0: self.draw.draw_ellipse(xy, ink, 0, width) - def line(self, xy, fill=None, width=0, joint=None): + def line(self, xy: Coords, fill=None, width=0, joint=None) -> None: """Draw a line, or a connected sequence of line segments.""" ink = self._getink(fill)[0] if ink is not None: self.draw.draw_lines(xy, ink, width) if joint == "curve" and width > 4: - if not isinstance(xy[0], (list, tuple)): - xy = [tuple(xy[i : i + 2]) for i in range(0, len(xy), 2)] - for i in range(1, len(xy) - 1): - point = xy[i] + points: Sequence[Sequence[float]] + if isinstance(xy[0], (list, tuple)): + points = cast(Sequence[Sequence[float]], xy) + else: + points = [ + cast(Sequence[float], tuple(xy[i : i + 2])) + for i in range(0, len(xy), 2) + ] + for i in range(1, len(points) - 1): + point = points[i] angles = [ math.degrees(math.atan2(end[0] - start[0], start[1] - end[1])) % 360 - for start, end in ((xy[i - 1], point), (point, xy[i + 1])) + for start, end in ( + (points[i - 1], point), + (point, points[i + 1]), + ) ] if angles[0] == angles[1]: # This is a straight line, so no joint is required @@ -236,7 +247,7 @@ class ImageDraw: ] self.line(gap_coords, fill, width=3) - def shape(self, shape, fill=None, outline=None): + def shape(self, shape, fill=None, outline=None) -> None: """(Experimental) Draw a shape.""" shape.close() ink, fill = self._getink(outline, fill) @@ -245,7 +256,9 @@ class ImageDraw: if ink is not None and ink != fill: self.draw.draw_outline(shape, ink, 0) - def pieslice(self, xy, start, end, fill=None, outline=None, width=1): + def pieslice( + self, xy: Coords, start, end, fill=None, outline=None, width=1 + ) -> None: """Draw a pieslice.""" ink, fill = self._getink(outline, fill) if fill is not None: @@ -253,13 +266,13 @@ class ImageDraw: if ink is not None and ink != fill and width != 0: self.draw.draw_pieslice(xy, start, end, ink, 0, width) - def point(self, xy, fill=None): + def point(self, xy: Coords, fill=None) -> None: """Draw one or more individual pixels.""" ink, fill = self._getink(fill) if ink is not None: self.draw.draw_points(xy, ink) - def polygon(self, xy, fill=None, outline=None, width=1): + def polygon(self, xy: Coords, fill=None, outline=None, width=1) -> None: """Draw a polygon.""" ink, fill = self._getink(outline, fill) if fill is not None: @@ -267,7 +280,7 @@ class ImageDraw: if ink is not None and ink != fill and width != 0: if width == 1: self.draw.draw_polygon(xy, ink, 0, width) - else: + elif self.im is not None: # To avoid expanding the polygon outwards, # use the fill as a mask mask = Image.new("1", self.im.size) @@ -291,12 +304,12 @@ class ImageDraw: def regular_polygon( self, bounding_circle, n_sides, rotation=0, fill=None, outline=None, width=1 - ): + ) -> None: """Draw a regular polygon.""" xy = _compute_regular_polygon_vertices(bounding_circle, n_sides, rotation) self.polygon(xy, fill, outline, width) - def rectangle(self, xy, fill=None, outline=None, width=1): + def rectangle(self, xy: Coords, fill=None, outline=None, width=1) -> None: """Draw a rectangle.""" ink, fill = self._getink(outline, fill) if fill is not None: @@ -305,13 +318,13 @@ class ImageDraw: self.draw.draw_rectangle(xy, ink, 0, width) def rounded_rectangle( - self, xy, radius=0, fill=None, outline=None, width=1, *, corners=None - ): + self, xy: Coords, radius=0, fill=None, outline=None, width=1, *, corners=None + ) -> None: """Draw a rounded rectangle.""" if isinstance(xy[0], (list, tuple)): - (x0, y0), (x1, y1) = xy + (x0, y0), (x1, y1) = cast(Sequence[Sequence[float]], xy) else: - x0, y0, x1, y1 = xy + x0, y0, x1, y1 = cast(Sequence[float], xy) if x1 < x0: msg = "x1 must be greater than or equal to x0" raise ValueError(msg) @@ -346,7 +359,8 @@ class ImageDraw: r = d // 2 ink, fill = self._getink(outline, fill) - def draw_corners(pieslice): + def draw_corners(pieslice) -> None: + parts: tuple[tuple[tuple[float, float, float, float], int, int], ...] if full_x: # Draw top and bottom halves parts = ( @@ -361,17 +375,18 @@ class ImageDraw: ) else: # Draw four separate corners - parts = [] - for i, part in enumerate( - ( - ((x0, y0, x0 + d, y0 + d), 180, 270), - ((x1 - d, y0, x1, y0 + d), 270, 360), - ((x1 - d, y1 - d, x1, y1), 0, 90), - ((x0, y1 - d, x0 + d, y1), 90, 180), + parts = tuple( + part + for i, part in enumerate( + ( + ((x0, y0, x0 + d, y0 + d), 180, 270), + ((x1 - d, y0, x1, y0 + d), 270, 360), + ((x1 - d, y1 - d, x1, y1), 0, 90), + ((x0, y1 - d, x0 + d, y1), 90, 180), + ) ) - ): - if corners[i]: - parts.append(part) + if corners[i] + ) for part in parts: if pieslice: self.draw.draw_pieslice(*(part + (fill, 1))) @@ -431,12 +446,12 @@ class ImageDraw: right[3] -= r + 1 self.draw.draw_rectangle(right, ink, 1) - def _multiline_check(self, text): + def _multiline_check(self, text) -> bool: split_character = "\n" if isinstance(text, str) else b"\n" return split_character in text - def _multiline_split(self, text): + def _multiline_split(self, text) -> list[str | bytes]: split_character = "\n" if isinstance(text, str) else b"\n" return text.split(split_character) @@ -465,7 +480,7 @@ class ImageDraw: embedded_color=False, *args, **kwargs, - ): + ) -> None: """Draw text.""" if embedded_color and self.mode not in ("RGB", "RGBA"): msg = "Embedded color supported only in RGB and RGBA modes" @@ -497,7 +512,7 @@ class ImageDraw: return fill return ink - def draw_text(ink, stroke_width=0, stroke_offset=None): + def draw_text(ink, stroke_width=0, stroke_offset=None) -> None: mode = self.fontmode if stroke_width == 0 and embedded_color: mode = "RGBA" @@ -520,7 +535,7 @@ class ImageDraw: *args, **kwargs, ) - coord = coord[0] + offset[0], coord[1] + offset[1] + coord = [coord[0] + offset[0], coord[1] + offset[1]] except AttributeError: try: mask = font.getmask( @@ -539,7 +554,7 @@ class ImageDraw: except TypeError: mask = font.getmask(text) if stroke_offset: - coord = coord[0] + stroke_offset[0], coord[1] + stroke_offset[1] + coord = [coord[0] + stroke_offset[0], coord[1] + stroke_offset[1]] if mode == "RGBA": # font.getmask2(mode="RGBA") returns color in RGB bands and mask in A # extract mask and set text alpha @@ -547,7 +562,10 @@ class ImageDraw: ink_alpha = struct.pack("i", ink)[3] color.fillband(3, ink_alpha) x, y = coord - self.im.paste(color, (x, y, x + mask.size[0], y + mask.size[1]), mask) + if self.im is not None: + self.im.paste( + color, (x, y, x + mask.size[0], y + mask.size[1]), mask + ) else: self.draw.draw_bitmap(coord, mask, ink) @@ -584,7 +602,7 @@ class ImageDraw: embedded_color=False, *, font_size=None, - ): + ) -> None: if direction == "ttb": msg = "ttb direction is unsupported for multiline text" raise ValueError(msg) @@ -693,7 +711,7 @@ class ImageDraw: embedded_color=False, *, font_size=None, - ): + ) -> tuple[int, int, int, int]: """Get the bounding box of a given string, in pixels.""" if embedded_color and self.mode not in ("RGB", "RGBA"): msg = "Embedded color supported only in RGB and RGBA modes" @@ -738,7 +756,7 @@ class ImageDraw: embedded_color=False, *, font_size=None, - ): + ) -> tuple[int, int, int, int]: if direction == "ttb": msg = "ttb direction is unsupported for multiline text" raise ValueError(msg) @@ -777,7 +795,7 @@ class ImageDraw: elif anchor[1] == "d": top -= (len(lines) - 1) * line_spacing - bbox = None + bbox: tuple[int, int, int, int] | None = None for idx, line in enumerate(lines): left = xy[0] @@ -828,7 +846,7 @@ class ImageDraw: return bbox -def Draw(im, mode=None): +def Draw(im, mode: str | None = None) -> ImageDraw: """ A simple 2D drawing interface for PIL images. @@ -876,7 +894,7 @@ def getdraw(im=None, hints=None): return im, handler -def floodfill(image, xy, value, border=None, thresh=0): +def floodfill(image: Image.Image, xy, value, border=None, thresh=0) -> None: """ (experimental) Fills a bounded region with a given color. @@ -932,7 +950,9 @@ def floodfill(image, xy, value, border=None, thresh=0): edge = new_edge -def _compute_regular_polygon_vertices(bounding_circle, n_sides, rotation): +def _compute_regular_polygon_vertices( + bounding_circle, n_sides, rotation +) -> list[tuple[float, float]]: """ Generate a list of vertices for a 2D regular polygon. @@ -982,7 +1002,7 @@ def _compute_regular_polygon_vertices(bounding_circle, n_sides, rotation): # 1.2 Check `bounding_circle` has an appropriate value if not isinstance(bounding_circle, (list, tuple)): - msg = "bounding_circle should be a tuple" + msg = "bounding_circle should be a sequence" raise TypeError(msg) if len(bounding_circle) == 3: @@ -1014,7 +1034,7 @@ def _compute_regular_polygon_vertices(bounding_circle, n_sides, rotation): raise ValueError(msg) # 2. Define Helper Functions - def _apply_rotation(point, degrees, centroid): + def _apply_rotation(point: list[float], degrees: float) -> tuple[int, int]: return ( round( point[0] * math.cos(math.radians(360 - degrees)) @@ -1030,11 +1050,11 @@ def _compute_regular_polygon_vertices(bounding_circle, n_sides, rotation): ), ) - def _compute_polygon_vertex(centroid, polygon_radius, angle): + def _compute_polygon_vertex(angle: float) -> tuple[int, int]: start_point = [polygon_radius, 0] - return _apply_rotation(start_point, angle, centroid) + return _apply_rotation(start_point, angle) - def _get_angles(n_sides, rotation): + def _get_angles(n_sides: int, rotation: float) -> list[float]: angles = [] degrees = 360 / n_sides # Start with the bottom left polygon vertex @@ -1050,12 +1070,10 @@ def _compute_regular_polygon_vertices(bounding_circle, n_sides, rotation): angles = _get_angles(n_sides, rotation) # 4. Compute Vertices - return [ - _compute_polygon_vertex(centroid, polygon_radius, angle) for angle in angles - ] + return [_compute_polygon_vertex(angle) for angle in angles] -def _color_diff(color1, color2): +def _color_diff(color1, color2: float | tuple[int, ...]) -> float: """ Uses 1-norm distance to calculate difference between two values. """ diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 5ba5a6f82..a654dea27 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -32,7 +32,7 @@ import io import itertools import struct import sys -from typing import Any, NamedTuple +from typing import IO, Any, NamedTuple from . import Image from ._deprecate import deprecate @@ -91,7 +91,7 @@ def _tilesort(t): class _Tile(NamedTuple): - encoder_name: str + codec_name: str extents: tuple[int, int, int, int] offset: int args: tuple[Any, ...] | str | None @@ -384,7 +384,7 @@ class Parser: """ incremental = None - image = None + image: Image.Image | None = None data = None decoder = None offset = 0 @@ -616,7 +616,7 @@ class PyCodecState: class PyCodec: - fd: io.BytesIO | None + fd: IO[bytes] | None def __init__(self, mode, *args): self.im = None diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index a63b73b33..256c581df 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -33,10 +33,10 @@ import sys import warnings from enum import IntEnum from io import BytesIO -from pathlib import Path from typing import BinaryIO from . import Image +from ._typing import StrOrBytesPath from ._util import is_directory, is_path @@ -193,7 +193,7 @@ class FreeTypeFont: def __init__( self, - font: bytes | str | Path | BinaryIO | None = None, + font: StrOrBytesPath | BinaryIO | None = None, size: float = 10, index: int = 0, encoding: str = "", @@ -230,8 +230,7 @@ class FreeTypeFont: ) if is_path(font): - if isinstance(font, Path): - font = str(font) + font = os.path.realpath(os.fspath(font)) if sys.platform == "win32": font_bytes_path = font if isinstance(font, bytes) else font.encode() try: @@ -872,7 +871,7 @@ def load_path(filename): raise OSError(msg) -def load_default(size=None): +def load_default(size: float | None = None) -> FreeTypeFont | ImageFont: """If FreeType support is available, load a version of Aileron Regular, https://dotcolon.net/font/aileron, with a more limited character set. diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index a4993d3d4..3f3be706d 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -149,18 +149,7 @@ def grabclipboard(): session_type = None if shutil.which("wl-paste") and session_type in ("wayland", None): - output = subprocess.check_output(["wl-paste", "-l"]).decode() - mimetypes = output.splitlines() - if "image/png" in mimetypes: - mimetype = "image/png" - elif mimetypes: - mimetype = mimetypes[0] - else: - mimetype = None - - args = ["wl-paste"] - if mimetype: - args.extend(["-t", mimetype]) + args = ["wl-paste", "-t", "image"] elif shutil.which("xclip") and session_type in ("x11", None): args = ["xclip", "-selection", "clipboard", "-t", "image/png", "-o"] else: @@ -168,10 +157,29 @@ def grabclipboard(): raise NotImplementedError(msg) p = subprocess.run(args, capture_output=True) - err = p.stderr - if err: - msg = f"{args[0]} error: {err.strip().decode()}" + if p.returncode != 0: + err = p.stderr + for silent_error in [ + # wl-paste, when the clipboard is empty + b"Nothing is copied", + # Ubuntu/Debian wl-paste, when the clipboard is empty + b"No selection", + # Ubuntu/Debian wl-paste, when an image isn't available + b"No suitable type of content copied", + # wl-paste or Ubuntu/Debian xclip, when an image isn't available + b" not available", + # xclip, when an image isn't available + b"cannot convert ", + # xclip, when the clipboard isn't initialized + b"xclip: Error: There is no owner for the ", + ]: + if silent_error in err: + return None + msg = f"{args[0]} error" + if err: + msg += f": {err.strip().decode()}" raise ChildProcessError(msg) + data = io.BytesIO(p.stdout) im = Image.open(data) im.load() diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py index a9e626b2b..33db8fa50 100644 --- a/src/PIL/ImageOps.py +++ b/src/PIL/ImageOps.py @@ -21,6 +21,7 @@ from __future__ import annotations import functools import operator import re +from typing import Protocol, Sequence, cast from . import ExifTags, Image, ImagePalette @@ -28,7 +29,7 @@ from . import ExifTags, Image, ImagePalette # helpers -def _border(border): +def _border(border: int | tuple[int, ...]) -> tuple[int, int, int, int]: if isinstance(border, tuple): if len(border) == 2: left, top = right, bottom = border @@ -39,7 +40,7 @@ def _border(border): return left, top, right, bottom -def _color(color, mode): +def _color(color: str | int | tuple[int, ...], mode: str) -> int | tuple[int, ...]: if isinstance(color, str): from . import ImageColor @@ -47,7 +48,7 @@ def _color(color, mode): return color -def _lut(image, lut): +def _lut(image: Image.Image, lut: list[int]) -> Image.Image: if image.mode == "P": # FIXME: apply to lookup table, not image data msg = "mode P support coming soon" @@ -65,7 +66,13 @@ def _lut(image, lut): # actions -def autocontrast(image, cutoff=0, ignore=None, mask=None, preserve_tone=False): +def autocontrast( + image: Image.Image, + cutoff: float | tuple[float, float] = 0, + ignore: int | Sequence[int] | None = None, + mask: Image.Image | None = None, + preserve_tone: bool = False, +) -> Image.Image: """ Maximize (normalize) image contrast. This function calculates a histogram of the input image (or mask region), removes ``cutoff`` percent of the @@ -97,10 +104,9 @@ def autocontrast(image, cutoff=0, ignore=None, mask=None, preserve_tone=False): h = histogram[layer : layer + 256] if ignore is not None: # get rid of outliers - try: + if isinstance(ignore, int): h[ignore] = 0 - except TypeError: - # assume sequence + else: for ix in ignore: h[ix] = 0 if cutoff: @@ -112,7 +118,7 @@ def autocontrast(image, cutoff=0, ignore=None, mask=None, preserve_tone=False): for ix in range(256): n = n + h[ix] # remove cutoff% pixels from the low end - cut = n * cutoff[0] // 100 + cut = int(n * cutoff[0] // 100) for lo in range(256): if cut > h[lo]: cut = cut - h[lo] @@ -123,7 +129,7 @@ def autocontrast(image, cutoff=0, ignore=None, mask=None, preserve_tone=False): if cut <= 0: break # remove cutoff% samples from the high end - cut = n * cutoff[1] // 100 + cut = int(n * cutoff[1] // 100) for hi in range(255, -1, -1): if cut > h[hi]: cut = cut - h[hi] @@ -156,7 +162,15 @@ def autocontrast(image, cutoff=0, ignore=None, mask=None, preserve_tone=False): return _lut(image, lut) -def colorize(image, black, white, mid=None, blackpoint=0, whitepoint=255, midpoint=127): +def colorize( + image: Image.Image, + black: str | tuple[int, ...], + white: str | tuple[int, ...], + mid: str | int | tuple[int, ...] | None = None, + blackpoint: int = 0, + whitepoint: int = 255, + midpoint: int = 127, +) -> Image.Image: """ Colorize grayscale image. This function calculates a color wedge which maps all black pixels in @@ -188,10 +202,9 @@ def colorize(image, black, white, mid=None, blackpoint=0, whitepoint=255, midpoi assert 0 <= blackpoint <= midpoint <= whitepoint <= 255 # Define colors from arguments - black = _color(black, "RGB") - white = _color(white, "RGB") - if mid is not None: - mid = _color(mid, "RGB") + rgb_black = cast(Sequence[int], _color(black, "RGB")) + rgb_white = cast(Sequence[int], _color(white, "RGB")) + rgb_mid = cast(Sequence[int], _color(mid, "RGB")) if mid is not None else None # Empty lists for the mapping red = [] @@ -200,18 +213,24 @@ def colorize(image, black, white, mid=None, blackpoint=0, whitepoint=255, midpoi # Create the low-end values for i in range(0, blackpoint): - red.append(black[0]) - green.append(black[1]) - blue.append(black[2]) + red.append(rgb_black[0]) + green.append(rgb_black[1]) + blue.append(rgb_black[2]) # Create the mapping (2-color) - if mid is None: + if rgb_mid is None: range_map = range(0, whitepoint - blackpoint) for i in range_map: - red.append(black[0] + i * (white[0] - black[0]) // len(range_map)) - green.append(black[1] + i * (white[1] - black[1]) // len(range_map)) - blue.append(black[2] + i * (white[2] - black[2]) // len(range_map)) + red.append( + rgb_black[0] + i * (rgb_white[0] - rgb_black[0]) // len(range_map) + ) + green.append( + rgb_black[1] + i * (rgb_white[1] - rgb_black[1]) // len(range_map) + ) + blue.append( + rgb_black[2] + i * (rgb_white[2] - rgb_black[2]) // len(range_map) + ) # Create the mapping (3-color) else: @@ -219,26 +238,36 @@ def colorize(image, black, white, mid=None, blackpoint=0, whitepoint=255, midpoi range_map2 = range(0, whitepoint - midpoint) for i in range_map1: - red.append(black[0] + i * (mid[0] - black[0]) // len(range_map1)) - green.append(black[1] + i * (mid[1] - black[1]) // len(range_map1)) - blue.append(black[2] + i * (mid[2] - black[2]) // len(range_map1)) + red.append( + rgb_black[0] + i * (rgb_mid[0] - rgb_black[0]) // len(range_map1) + ) + green.append( + rgb_black[1] + i * (rgb_mid[1] - rgb_black[1]) // len(range_map1) + ) + blue.append( + rgb_black[2] + i * (rgb_mid[2] - rgb_black[2]) // len(range_map1) + ) for i in range_map2: - red.append(mid[0] + i * (white[0] - mid[0]) // len(range_map2)) - green.append(mid[1] + i * (white[1] - mid[1]) // len(range_map2)) - blue.append(mid[2] + i * (white[2] - mid[2]) // len(range_map2)) + red.append(rgb_mid[0] + i * (rgb_white[0] - rgb_mid[0]) // len(range_map2)) + green.append( + rgb_mid[1] + i * (rgb_white[1] - rgb_mid[1]) // len(range_map2) + ) + blue.append(rgb_mid[2] + i * (rgb_white[2] - rgb_mid[2]) // len(range_map2)) # Create the high-end values for i in range(0, 256 - whitepoint): - red.append(white[0]) - green.append(white[1]) - blue.append(white[2]) + red.append(rgb_white[0]) + green.append(rgb_white[1]) + blue.append(rgb_white[2]) # Return converted image image = image.convert("RGB") return _lut(image, red + green + blue) -def contain(image, size, method=Image.Resampling.BICUBIC): +def contain( + image: Image.Image, size: tuple[int, int], method: int = Image.Resampling.BICUBIC +) -> Image.Image: """ Returns a resized version of the image, set to the maximum width and height within the requested size, while maintaining the original aspect ratio. @@ -267,7 +296,9 @@ def contain(image, size, method=Image.Resampling.BICUBIC): return image.resize(size, resample=method) -def cover(image, size, method=Image.Resampling.BICUBIC): +def cover( + image: Image.Image, size: tuple[int, int], method: int = Image.Resampling.BICUBIC +) -> Image.Image: """ Returns a resized version of the image, so that the requested size is covered, while maintaining the original aspect ratio. @@ -296,7 +327,13 @@ def cover(image, size, method=Image.Resampling.BICUBIC): return image.resize(size, resample=method) -def pad(image, size, method=Image.Resampling.BICUBIC, color=None, centering=(0.5, 0.5)): +def pad( + image: Image.Image, + size: tuple[int, int], + method: int = Image.Resampling.BICUBIC, + color: str | int | tuple[int, ...] | None = None, + centering: tuple[float, float] = (0.5, 0.5), +) -> Image.Image: """ Returns a resized and padded version of the image, expanded to fill the requested aspect ratio and size. @@ -334,7 +371,7 @@ def pad(image, size, method=Image.Resampling.BICUBIC, color=None, centering=(0.5 return out -def crop(image, border=0): +def crop(image: Image.Image, border: int = 0) -> Image.Image: """ Remove border from image. The same amount of pixels are removed from all four sides. This function works on all image modes. @@ -349,7 +386,9 @@ def crop(image, border=0): return image.crop((left, top, image.size[0] - right, image.size[1] - bottom)) -def scale(image, factor, resample=Image.Resampling.BICUBIC): +def scale( + image: Image.Image, factor: float, resample: int = Image.Resampling.BICUBIC +) -> Image.Image: """ Returns a rescaled image by a specific factor given in parameter. A factor greater than 1 expands the image, between 0 and 1 contracts the @@ -372,7 +411,27 @@ def scale(image, factor, resample=Image.Resampling.BICUBIC): return image.resize(size, resample) -def deform(image, deformer, resample=Image.Resampling.BILINEAR): +class SupportsGetMesh(Protocol): + """ + An object that supports the ``getmesh`` method, taking an image as an + argument, and returning a list of tuples. Each tuple contains two tuples, + the source box as a tuple of 4 integers, and a tuple of 8 integers for the + final quadrilateral, in order of top left, bottom left, bottom right, top + right. + """ + + def getmesh( + self, image: Image.Image + ) -> list[ + tuple[tuple[int, int, int, int], tuple[int, int, int, int, int, int, int, int]] + ]: ... + + +def deform( + image: Image.Image, + deformer: SupportsGetMesh, + resample: int = Image.Resampling.BILINEAR, +) -> Image.Image: """ Deform the image. @@ -388,7 +447,7 @@ def deform(image, deformer, resample=Image.Resampling.BILINEAR): ) -def equalize(image, mask=None): +def equalize(image: Image.Image, mask: Image.Image | None = None) -> Image.Image: """ Equalize the image histogram. This function applies a non-linear mapping to the input image, in order to create a uniform @@ -419,7 +478,11 @@ def equalize(image, mask=None): return _lut(image, lut) -def expand(image, border=0, fill=0): +def expand( + image: Image.Image, + border: int | tuple[int, ...] = 0, + fill: str | int | tuple[int, ...] = 0, +) -> Image.Image: """ Add border to the image @@ -445,7 +508,13 @@ def expand(image, border=0, fill=0): return out -def fit(image, size, method=Image.Resampling.BICUBIC, bleed=0.0, centering=(0.5, 0.5)): +def fit( + image: Image.Image, + size: tuple[int, int], + method: int = Image.Resampling.BICUBIC, + bleed: float = 0.0, + centering: tuple[float, float] = (0.5, 0.5), +) -> Image.Image: """ Returns a resized and cropped version of the image, cropped to the requested aspect ratio and size. @@ -479,13 +548,12 @@ def fit(image, size, method=Image.Resampling.BICUBIC, bleed=0.0, centering=(0.5, # kevin@cazabon.com # https://www.cazabon.com - # ensure centering is mutable - centering = list(centering) + centering_x, centering_y = centering - if not 0.0 <= centering[0] <= 1.0: - centering[0] = 0.5 - if not 0.0 <= centering[1] <= 1.0: - centering[1] = 0.5 + if not 0.0 <= centering_x <= 1.0: + centering_x = 0.5 + if not 0.0 <= centering_y <= 1.0: + centering_y = 0.5 if not 0.0 <= bleed < 0.5: bleed = 0.0 @@ -522,8 +590,8 @@ def fit(image, size, method=Image.Resampling.BICUBIC, bleed=0.0, centering=(0.5, crop_height = live_size[0] / output_ratio # make the crop - crop_left = bleed_pixels[0] + (live_size[0] - crop_width) * centering[0] - crop_top = bleed_pixels[1] + (live_size[1] - crop_height) * centering[1] + crop_left = bleed_pixels[0] + (live_size[0] - crop_width) * centering_x + crop_top = bleed_pixels[1] + (live_size[1] - crop_height) * centering_y crop = (crop_left, crop_top, crop_left + crop_width, crop_top + crop_height) @@ -531,7 +599,7 @@ def fit(image, size, method=Image.Resampling.BICUBIC, bleed=0.0, centering=(0.5, return image.resize(size, method, box=crop) -def flip(image): +def flip(image: Image.Image) -> Image.Image: """ Flip the image vertically (top to bottom). @@ -541,7 +609,7 @@ def flip(image): return image.transpose(Image.Transpose.FLIP_TOP_BOTTOM) -def grayscale(image): +def grayscale(image: Image.Image) -> Image.Image: """ Convert the image to grayscale. @@ -551,7 +619,7 @@ def grayscale(image): return image.convert("L") -def invert(image): +def invert(image: Image.Image) -> Image.Image: """ Invert (negate) the image. @@ -562,7 +630,7 @@ def invert(image): return image.point(lut) if image.mode == "1" else _lut(image, lut) -def mirror(image): +def mirror(image: Image.Image) -> Image.Image: """ Flip image horizontally (left to right). @@ -572,7 +640,7 @@ def mirror(image): return image.transpose(Image.Transpose.FLIP_LEFT_RIGHT) -def posterize(image, bits): +def posterize(image: Image.Image, bits: int) -> Image.Image: """ Reduce the number of bits for each color channel. @@ -585,7 +653,7 @@ def posterize(image, bits): return _lut(image, lut) -def solarize(image, threshold=128): +def solarize(image: Image.Image, threshold: int = 128) -> Image.Image: """ Invert all pixel values above a threshold. @@ -602,7 +670,7 @@ def solarize(image, threshold=128): return _lut(image, lut) -def exif_transpose(image, *, in_place=False): +def exif_transpose(image: Image.Image, *, in_place: bool = False) -> Image.Image | None: """ If an image has an EXIF Orientation tag, other than 1, transpose the image accordingly, and remove the orientation data. @@ -616,7 +684,7 @@ def exif_transpose(image, *, in_place=False): """ image.load() image_exif = image.getexif() - orientation = image_exif.get(ExifTags.Base.Orientation) + orientation = image_exif.get(ExifTags.Base.Orientation, 1) method = { 2: Image.Transpose.FLIP_LEFT_RIGHT, 3: Image.Transpose.ROTATE_180, @@ -653,3 +721,4 @@ def exif_transpose(image, *, in_place=False): return transposed_image elif not in_place: return image.copy() + return None diff --git a/src/PIL/ImagePalette.py b/src/PIL/ImagePalette.py index 2b6cecc61..770d10025 100644 --- a/src/PIL/ImagePalette.py +++ b/src/PIL/ImagePalette.py @@ -18,6 +18,7 @@ from __future__ import annotations import array +from typing import Sequence from . import GimpGradientFile, GimpPaletteFile, ImageColor, PaletteFile @@ -34,11 +35,11 @@ class ImagePalette: Defaults to an empty palette. """ - def __init__(self, mode="RGB", palette=None): + def __init__(self, mode: str = "RGB", palette: Sequence[int] | None = None) -> None: self.mode = mode self.rawmode = None # if set, palette contains raw data self.palette = palette or bytearray() - self.dirty = None + self.dirty: int | None = None @property def palette(self): @@ -127,7 +128,7 @@ class ImagePalette: raise ValueError(msg) from e return index - def getcolor(self, color, image=None): + def getcolor(self, color, image=None) -> int: """Given an rgb tuple, allocate palette entry. .. warning:: This method is experimental. diff --git a/src/PIL/ImageQt.py b/src/PIL/ImageQt.py index 6377c7501..293ba4941 100644 --- a/src/PIL/ImageQt.py +++ b/src/PIL/ImageQt.py @@ -19,19 +19,26 @@ from __future__ import annotations import sys from io import BytesIO +from typing import Callable from . import Image from ._util import is_path +qt_version: str | None qt_versions = [ ["6", "PyQt6"], ["side6", "PySide6"], ] # If a version has already been imported, attempt it first -qt_versions.sort(key=lambda qt_version: qt_version[1] in sys.modules, reverse=True) -for qt_version, qt_module in qt_versions: +qt_versions.sort(key=lambda version: version[1] in sys.modules, reverse=True) +for version, qt_module in qt_versions: try: + QBuffer: type + QIODevice: type + QImage: type + QPixmap: type + qRgba: Callable[[int, int, int, int], int] if qt_module == "PyQt6": from PyQt6.QtCore import QBuffer, QIODevice from PyQt6.QtGui import QImage, QPixmap, qRgba @@ -41,6 +48,7 @@ for qt_version, qt_module in qt_versions: except (ImportError, RuntimeError): continue qt_is_installed = True + qt_version = version break else: qt_is_installed = False diff --git a/src/PIL/ImageShow.py b/src/PIL/ImageShow.py index c03122c11..d90545e92 100644 --- a/src/PIL/ImageShow.py +++ b/src/PIL/ImageShow.py @@ -184,7 +184,7 @@ class UnixViewer(Viewer): @abc.abstractmethod def get_command_ex(self, file: str, **options: Any) -> tuple[str, str]: - pass # pragma: no cover + pass def get_command(self, file: str, **options: Any) -> str: command = self.get_command_ex(file, **options)[0] diff --git a/src/PIL/JpegPresets.py b/src/PIL/JpegPresets.py index 9ecfdb259..d0e64a35e 100644 --- a/src/PIL/JpegPresets.py +++ b/src/PIL/JpegPresets.py @@ -62,6 +62,7 @@ Libjpeg ref.: https://web.archive.org/web/20120328125543/http://www.jpegcameras.com/libjpeg/libjpeg-3.html """ + from __future__ import annotations # fmt: off diff --git a/src/PIL/MpegImagePlugin.py b/src/PIL/MpegImagePlugin.py index b9e9243e5..1565612f8 100644 --- a/src/PIL/MpegImagePlugin.py +++ b/src/PIL/MpegImagePlugin.py @@ -14,17 +14,16 @@ # from __future__ import annotations -from io import BytesIO - from . import Image, ImageFile from ._binary import i8 +from ._typing import SupportsRead # # Bitstream parser class BitStream: - def __init__(self, fp: BytesIO) -> None: + def __init__(self, fp: SupportsRead[bytes]) -> None: self.fp = fp self.bits = 0 self.bitbuffer = 0 diff --git a/src/PIL/MspImagePlugin.py b/src/PIL/MspImagePlugin.py index bb7e466a7..65cc70624 100644 --- a/src/PIL/MspImagePlugin.py +++ b/src/PIL/MspImagePlugin.py @@ -26,6 +26,7 @@ from __future__ import annotations import io import struct +from typing import IO from . import Image, ImageFile from ._binary import i16le as i16 @@ -163,7 +164,7 @@ Image.register_decoder("MSP", MspDecoder) # write MSP files (uncompressed only) -def _save(im: Image.Image, fp: io.BytesIO, filename: str) -> None: +def _save(im: Image.Image, fp: IO[bytes], filename: str) -> None: if im.mode != "1": msg = f"cannot write mode {im.mode} as MSP" raise OSError(msg) diff --git a/src/PIL/PcxImagePlugin.py b/src/PIL/PcxImagePlugin.py index 3e0968a83..026bfd9a0 100644 --- a/src/PIL/PcxImagePlugin.py +++ b/src/PIL/PcxImagePlugin.py @@ -28,6 +28,7 @@ from __future__ import annotations import io import logging +from typing import IO from . import Image, ImageFile, ImagePalette from ._binary import i16le as i16 @@ -143,7 +144,7 @@ SAVE = { } -def _save(im: Image.Image, fp: io.BytesIO, filename: str) -> None: +def _save(im: Image.Image, fp: IO[bytes], filename: str) -> None: try: version, bits, planes, rawmode = SAVE[im.mode] except KeyError as e: diff --git a/src/PIL/PdfImagePlugin.py b/src/PIL/PdfImagePlugin.py index 3506aadce..1777f1f20 100644 --- a/src/PIL/PdfImagePlugin.py +++ b/src/PIL/PdfImagePlugin.py @@ -188,9 +188,9 @@ def _save(im, fp, filename, save_all=False): x_resolution = y_resolution = im.encoderinfo.get("resolution", 72.0) info = { - "title": None - if is_appending - else os.path.splitext(os.path.basename(filename))[0], + "title": ( + None if is_appending else os.path.splitext(os.path.basename(filename))[0] + ), "author": None, "subject": None, "keywords": None, diff --git a/src/PIL/PdfParser.py b/src/PIL/PdfParser.py index 014460006..4c5101738 100644 --- a/src/PIL/PdfParser.py +++ b/src/PIL/PdfParser.py @@ -8,6 +8,7 @@ import os import re import time import zlib +from typing import TYPE_CHECKING, Any, List, Union # see 7.9.2.2 Text String Type on page 86 and D.3 PDFDocEncoding Character Set @@ -239,12 +240,18 @@ class PdfName: return bytes(result) -class PdfArray(list): +class PdfArray(List[Any]): def __bytes__(self): return b"[ " + b" ".join(pdf_repr(x) for x in self) + b" ]" -class PdfDict(collections.UserDict): +if TYPE_CHECKING: + _DictBase = collections.UserDict[Union[str, bytes], Any] +else: + _DictBase = collections.UserDict + + +class PdfDict(_DictBase): def __setattr__(self, key, value): if key == "data": collections.UserDict.__setattr__(self, key, value) diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index 823f12492..35f38d67c 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -62,7 +62,7 @@ _MODES = { (2, 0): ("L", "L;2"), (4, 0): ("L", "L;4"), (8, 0): ("L", "L"), - (16, 0): ("I", "I;16B"), + (16, 0): ("I;16", "I;16B"), # Truecolour (8, 2): ("RGB", "RGB"), (16, 2): ("RGB", "RGB;16B"), @@ -467,7 +467,7 @@ class PngStream(ChunkStream): # otherwise, we have a byte string with one alpha value # for each palette entry self.im_info["transparency"] = s - elif self.im_mode in ("1", "L", "I"): + elif self.im_mode in ("1", "L", "I;16"): self.im_info["transparency"] = i16(s) elif self.im_mode == "RGB": self.im_info["transparency"] = i16(s), i16(s, 2), i16(s, 4) @@ -981,7 +981,13 @@ class PngImageFile(ImageFile.ImageFile): except EOFError: if cid == b"fdAT": length -= 4 - ImageFile._safe_read(self.fp, length) + try: + ImageFile._safe_read(self.fp, length) + except OSError as e: + if ImageFile.LOAD_TRUNCATED_IMAGES: + break + else: + raise e except AttributeError: logger.debug("%r %s %s (unknown)", cid, pos, length) s = ImageFile._safe_read(self.fp, length) @@ -1350,7 +1356,7 @@ def _save(im, fp, filename, chunk=putchunk, save_all=False): transparency = max(0, min(255, transparency)) alpha = b"\xFF" * transparency + b"\0" chunk(fp, b"tRNS", alpha[:alpha_bytes]) - elif im.mode in ("1", "L", "I"): + elif im.mode in ("1", "L", "I", "I;16"): transparency = max(0, min(65535, transparency)) chunk(fp, b"tRNS", o16(transparency)) elif im.mode == "RGB": diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py index 3e45ba95c..6ac7a9bbc 100644 --- a/src/PIL/PpmImagePlugin.py +++ b/src/PIL/PpmImagePlugin.py @@ -16,7 +16,7 @@ from __future__ import annotations import math -from io import BytesIO +from typing import IO from . import Image, ImageFile from ._binary import i16be as i16 @@ -324,7 +324,7 @@ class PpmDecoder(ImageFile.PyDecoder): # -------------------------------------------------------------------- -def _save(im: Image.Image, fp: BytesIO, filename: str) -> None: +def _save(im: Image.Image, fp: IO[bytes], filename: str) -> None: if im.mode == "1": rawmode, head = "1;I", b"P4" elif im.mode == "L": diff --git a/src/PIL/PyAccess.py b/src/PIL/PyAccess.py index 07bb712d8..2c831913d 100644 --- a/src/PIL/PyAccess.py +++ b/src/PIL/PyAccess.py @@ -25,6 +25,7 @@ import sys from ._deprecate import deprecate +FFI: type try: from cffi import FFI diff --git a/src/PIL/SgiImagePlugin.py b/src/PIL/SgiImagePlugin.py index ccf661ff1..7bd84ebd4 100644 --- a/src/PIL/SgiImagePlugin.py +++ b/src/PIL/SgiImagePlugin.py @@ -24,7 +24,7 @@ from __future__ import annotations import os import struct -from io import BytesIO +from typing import IO from . import Image, ImageFile from ._binary import i16be as i16 @@ -125,7 +125,7 @@ class SgiImageFile(ImageFile.ImageFile): ] -def _save(im: Image.Image, fp: BytesIO, filename: str) -> None: +def _save(im: Image.Image, fp: IO[bytes], filename: str) -> None: if im.mode not in {"RGB", "RGBA", "L"}: msg = "Unsupported SGI image mode" raise ValueError(msg) diff --git a/src/PIL/TgaImagePlugin.py b/src/PIL/TgaImagePlugin.py index 584932d2c..828701342 100644 --- a/src/PIL/TgaImagePlugin.py +++ b/src/PIL/TgaImagePlugin.py @@ -18,7 +18,7 @@ from __future__ import annotations import warnings -from io import BytesIO +from typing import IO from . import Image, ImageFile, ImagePalette from ._binary import i16le as i16 @@ -175,7 +175,7 @@ SAVE = { } -def _save(im: Image.Image, fp: BytesIO, filename: str) -> None: +def _save(im: Image.Image, fp: IO[bytes], filename: str) -> None: try: rawmode, bits, colormaptype, imagetype = SAVE[im.mode] except KeyError as e: diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index e20d4d5ea..3ba4de9d1 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -50,6 +50,7 @@ import warnings from collections.abc import MutableMapping from fractions import Fraction from numbers import Number, Rational +from typing import TYPE_CHECKING, Any, Callable from . import ExifTags, Image, ImageFile, ImageOps, ImagePalette, TiffTags from ._binary import i16be as i16 @@ -306,6 +307,13 @@ _load_dispatch = {} _write_dispatch = {} +def _delegate(op): + def delegate(self, *args): + return getattr(self._val, op)(*args) + + return delegate + + class IFDRational(Rational): """Implements a rational class where 0/0 is a legal value to match the in the wild use of exif rationals. @@ -391,12 +399,6 @@ class IFDRational(Rational): self._numerator = _numerator self._denominator = _denominator - def _delegate(op): - def delegate(self, *args): - return getattr(self._val, op)(*args) - - return delegate - """ a = ['add','radd', 'sub', 'rsub', 'mul', 'rmul', 'truediv', 'rtruediv', 'floordiv', 'rfloordiv', 'mod','rmod', 'pow','rpow', 'pos', 'neg', @@ -436,7 +438,50 @@ class IFDRational(Rational): __int__ = _delegate("__int__") -class ImageFileDirectory_v2(MutableMapping): +def _register_loader(idx, size): + def decorator(func): + from .TiffTags import TYPES + + if func.__name__.startswith("load_"): + TYPES[idx] = func.__name__[5:].replace("_", " ") + _load_dispatch[idx] = size, func # noqa: F821 + return func + + return decorator + + +def _register_writer(idx): + def decorator(func): + _write_dispatch[idx] = func # noqa: F821 + return func + + return decorator + + +def _register_basic(idx_fmt_name): + from .TiffTags import TYPES + + idx, fmt, name = idx_fmt_name + TYPES[idx] = name + size = struct.calcsize("=" + fmt) + _load_dispatch[idx] = ( # noqa: F821 + size, + lambda self, data, legacy_api=True: ( + self._unpack(f"{len(data) // size}{fmt}", data) + ), + ) + _write_dispatch[idx] = lambda self, *values: ( # noqa: F821 + b"".join(self._pack(fmt, value) for value in values) + ) + + +if TYPE_CHECKING: + _IFDv2Base = MutableMapping[int, Any] +else: + _IFDv2Base = MutableMapping + + +class ImageFileDirectory_v2(_IFDv2Base): """This class represents a TIFF tag directory. To speed things up, we don't decode tags unless they're asked for. @@ -497,6 +542,9 @@ class ImageFileDirectory_v2(MutableMapping): """ + _load_dispatch: dict[int, Callable[[ImageFileDirectory_v2, bytes, bool], Any]] = {} + _write_dispatch: dict[int, Callable[..., Any]] = {} + def __init__(self, ifh=b"II\052\0\0\0\0\0", prefix=None, group=None): """Initialize an ImageFileDirectory. @@ -531,7 +579,10 @@ class ImageFileDirectory_v2(MutableMapping): prefix = property(lambda self: self._prefix) offset = property(lambda self: self._offset) - legacy_api = property(lambda self: self._legacy_api) + + @property + def legacy_api(self): + return self._legacy_api @legacy_api.setter def legacy_api(self, value): @@ -674,40 +725,6 @@ class ImageFileDirectory_v2(MutableMapping): def _pack(self, fmt, *values): return struct.pack(self._endian + fmt, *values) - def _register_loader(idx, size): - def decorator(func): - from .TiffTags import TYPES - - if func.__name__.startswith("load_"): - TYPES[idx] = func.__name__[5:].replace("_", " ") - _load_dispatch[idx] = size, func # noqa: F821 - return func - - return decorator - - def _register_writer(idx): - def decorator(func): - _write_dispatch[idx] = func # noqa: F821 - return func - - return decorator - - def _register_basic(idx_fmt_name): - from .TiffTags import TYPES - - idx, fmt, name = idx_fmt_name - TYPES[idx] = name - size = struct.calcsize("=" + fmt) - _load_dispatch[idx] = ( # noqa: F821 - size, - lambda self, data, legacy_api=True: ( - self._unpack(f"{len(data) // size}{fmt}", data) - ), - ) - _write_dispatch[idx] = lambda self, *values: ( # noqa: F821 - b"".join(self._pack(fmt, value) for value in values) - ) - list( map( _register_basic, @@ -995,7 +1012,7 @@ class ImageFileDirectory_v1(ImageFileDirectory_v2): tagdata = property(lambda self: self._tagdata) # defined in ImageFileDirectory_v2 - tagtype: dict + tagtype: dict[int, int] """Dictionary of tag types""" @classmethod @@ -1835,11 +1852,11 @@ def _save(im, fp, filename): tags = list(atts.items()) tags.sort() a = (rawmode, compression, _fp, filename, tags, types) - e = Image._getencoder(im.mode, "libtiff", a, encoderconfig) - e.setimage(im.im, (0, 0) + im.size) + encoder = Image._getencoder(im.mode, "libtiff", a, encoderconfig) + encoder.setimage(im.im, (0, 0) + im.size) while True: # undone, change to self.decodermaxblock: - errcode, data = e.encode(16 * 1024)[1:] + errcode, data = encoder.encode(16 * 1024)[1:] if not _fp: fp.write(data) if errcode: diff --git a/src/PIL/TiffTags.py b/src/PIL/TiffTags.py index 88ff2f4fc..b94193931 100644 --- a/src/PIL/TiffTags.py +++ b/src/PIL/TiffTags.py @@ -22,7 +22,7 @@ from collections import namedtuple class TagInfo(namedtuple("_TagInfo", "value name type length enum")): - __slots__ = [] + __slots__: list[str] = [] def __new__(cls, value=None, name="unknown", type=None, length=None, enum=None): return super().__new__(cls, value, name, type, length, enum or {}) @@ -437,7 +437,7 @@ _populate() ## # Map type numbers to type names -- defined in ImageFileDirectory. -TYPES = {} +TYPES: dict[int, str] = {} # # These tags are handled by default in libtiff, without diff --git a/src/PIL/XbmImagePlugin.py b/src/PIL/XbmImagePlugin.py index 0291e2858..eee727436 100644 --- a/src/PIL/XbmImagePlugin.py +++ b/src/PIL/XbmImagePlugin.py @@ -21,7 +21,7 @@ from __future__ import annotations import re -from io import BytesIO +from typing import IO from . import Image, ImageFile @@ -70,7 +70,7 @@ class XbmImageFile(ImageFile.ImageFile): self.tile = [("xbm", (0, 0) + self.size, m.end(), None)] -def _save(im: Image.Image, fp: BytesIO, filename: str) -> None: +def _save(im: Image.Image, fp: IO[bytes], filename: str) -> None: if im.mode != "1": msg = f"cannot write mode {im.mode} as XBM" raise OSError(msg) diff --git a/src/PIL/__init__.py b/src/PIL/__init__.py index 3fcac8643..63a45769b 100644 --- a/src/PIL/__init__.py +++ b/src/PIL/__init__.py @@ -12,6 +12,7 @@ Use PIL.__version__ for this Pillow version. ;-) """ + from __future__ import annotations from . import _version diff --git a/src/PIL/_imaging.pyi b/src/PIL/_imaging.pyi new file mode 100644 index 000000000..b0235555d --- /dev/null +++ b/src/PIL/_imaging.pyi @@ -0,0 +1,5 @@ +from __future__ import annotations + +from typing import Any + +def __getattr__(name: str) -> Any: ... diff --git a/src/PIL/_tkinter_finder.py b/src/PIL/_tkinter_finder.py index 03a6eba44..beddfb062 100644 --- a/src/PIL/_tkinter_finder.py +++ b/src/PIL/_tkinter_finder.py @@ -1,10 +1,12 @@ """ Find compiled module linking to Tcl / Tk libraries """ + from __future__ import annotations import sys import tkinter -from tkinter import _tkinter as tk + +tk = getattr(tkinter, "_tkinter") try: if hasattr(sys, "pypy_find_executable"): diff --git a/src/PIL/_typing.py b/src/PIL/_typing.py index 608b2b41f..7075e8672 100644 --- a/src/PIL/_typing.py +++ b/src/PIL/_typing.py @@ -1,6 +1,8 @@ from __future__ import annotations +import os import sys +from typing import Protocol, Sequence, TypeVar, Union if sys.version_info >= (3, 10): from typing import TypeGuard @@ -15,4 +17,17 @@ else: return bool -__all__ = ["TypeGuard"] +Coords = Union[Sequence[float], Sequence[Sequence[float]]] + + +_T_co = TypeVar("_T_co", covariant=True) + + +class SupportsRead(Protocol[_T_co]): + def read(self, __length: int = ...) -> _T_co: ... + + +StrOrBytesPath = Union[str, bytes, "os.PathLike[str]", "os.PathLike[bytes]"] + + +__all__ = ["TypeGuard", "StrOrBytesPath", "SupportsRead"] diff --git a/src/PIL/_util.py b/src/PIL/_util.py index 13f369cca..6bc762816 100644 --- a/src/PIL/_util.py +++ b/src/PIL/_util.py @@ -1,17 +1,16 @@ from __future__ import annotations import os -from pathlib import Path from typing import Any, NoReturn -from ._typing import TypeGuard +from ._typing import StrOrBytesPath, TypeGuard -def is_path(f: Any) -> TypeGuard[bytes | str | Path]: - return isinstance(f, (bytes, str, Path)) +def is_path(f: Any) -> TypeGuard[StrOrBytesPath]: + return isinstance(f, (bytes, str, os.PathLike)) -def is_directory(f: Any) -> TypeGuard[bytes | str | Path]: +def is_directory(f: Any) -> TypeGuard[StrOrBytesPath]: """Checks if an object is a string, and that it points to a directory.""" return is_path(f) and os.path.isdir(f) diff --git a/src/PIL/_webp.pyi b/src/PIL/_webp.pyi new file mode 100644 index 000000000..b0235555d --- /dev/null +++ b/src/PIL/_webp.pyi @@ -0,0 +1,5 @@ +from __future__ import annotations + +from typing import Any + +def __getattr__(name: str) -> Any: ... diff --git a/src/PIL/py.typed b/src/PIL/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/src/_webp.c b/src/_webp.c index a1b4dbc1a..47592547c 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -448,11 +448,16 @@ PyObject * _anim_decoder_get_next(PyObject *self) { uint8_t *buf; int timestamp; + int ok; PyObject *bytes; PyObject *ret; + ImagingSectionCookie cookie; WebPAnimDecoderObject *decp = (WebPAnimDecoderObject *)self; - if (!WebPAnimDecoderGetNext(decp->dec, &buf, ×tamp)) { + ImagingSectionEnter(&cookie); + ok = WebPAnimDecoderGetNext(decp->dec, &buf, ×tamp); + ImagingSectionLeave(&cookie); + if (!ok) { PyErr_SetString(PyExc_OSError, "failed to read next frame"); return NULL; } diff --git a/src/libImaging/Convert.c b/src/libImaging/Convert.c index 99d2a4ada..2654fd40d 100644 --- a/src/libImaging/Convert.c +++ b/src/libImaging/Convert.c @@ -878,6 +878,18 @@ I16B_L(UINT8 *out, const UINT8 *in, int xsize) { } } +static void +I16_RGB(UINT8 *out, const UINT8 *in, int xsize) { + int x; + for (x = 0; x < xsize; x++, in += 2) { + UINT8 v = in[1] == 0 ? in[0] : 255; + *out++ = v; + *out++ = v; + *out++ = v; + *out++ = 255; + } +} + static struct { const char *from; const char *to; @@ -978,6 +990,7 @@ static struct { {"I", "I;16", I_I16L}, {"I;16", "I", I16L_I}, + {"I;16", "RGB", I16_RGB}, {"L", "I;16", L_I16L}, {"I;16", "L", I16L_L}, @@ -1678,6 +1691,7 @@ ImagingConvertTransparent(Imaging imIn, const char *mode, int r, int g, int b) { convert = rgb2rgba; } else if ((strcmp(imIn->mode, "1") == 0 || strcmp(imIn->mode, "I") == 0 || + strcmp(imIn->mode, "I;16") == 0 || strcmp(imIn->mode, "L") == 0 ) && ( strcmp(mode, "RGBA") == 0 || @@ -1687,6 +1701,8 @@ ImagingConvertTransparent(Imaging imIn, const char *mode, int r, int g, int b) { convert = bit2rgb; } else if (strcmp(imIn->mode, "I") == 0) { convert = i2rgb; + } else if (strcmp(imIn->mode, "I;16") == 0) { + convert = I16_RGB; } else { convert = l2rgb; } diff --git a/tox.ini b/tox.ini index fb6746ce7..85a2020d6 100644 --- a/tox.ini +++ b/tox.ini @@ -33,9 +33,15 @@ commands = [testenv:mypy] skip_install = true deps = + -r .ci/requirements-mypy.txt + IceSpringPySideStubs-PyQt6 + IceSpringPySideStubs-PySide6 ipython - mypy==1.7.1 numpy + packaging + types-cffi + types-defusedxml + types-olefile extras = typing commands = diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 3117065e8..2cfc13dbc 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -87,7 +87,7 @@ def cmd_msbuild( file: str, configuration: str = "Release", target: str = "Build", - platform: str = "{msbuild_arch}", + plat: str = "{msbuild_arch}", ) -> str: return " ".join( [ @@ -95,7 +95,7 @@ def cmd_msbuild( f"{file}", f'/t:"{target}"', f'/p:Configuration="{configuration}"', - f"/p:Platform={platform}", + f"/p:Platform={plat}", "/m", ] ) @@ -109,13 +109,32 @@ ARCHITECTURES = { "ARM64": {"vcvars_arch": "x86_arm64", "msbuild_arch": "ARM64"}, } +V = { + "BROTLI": "1.1.0", + "FREETYPE": "2.13.2", + "FRIBIDI": "1.0.13", + "HARFBUZZ": "8.3.0", + "JPEGTURBO": "3.0.1", + "LCMS2": "2.16", + "LIBPNG": "1.6.39", + "LIBWEBP": "1.3.2", + "OPENJPEG": "2.5.2", + "TIFF": "4.6.0", + "XZ": "5.4.5", + "ZLIB": "1.3.1", +} +V["LIBPNG_DOTLESS"] = V["LIBPNG"].replace(".", "") +V["LIBPNG_XY"] = "".join(V["LIBPNG"].split(".")[:2]) +V["ZLIB_DOTLESS"] = V["ZLIB"].replace(".", "") + + # dependencies, listed in order of compilation DEPS = { "libjpeg": { - "url": SF_PROJECTS - + "/libjpeg-turbo/files/3.0.1/libjpeg-turbo-3.0.1.tar.gz/download", - "filename": "libjpeg-turbo-3.0.1.tar.gz", - "dir": "libjpeg-turbo-3.0.1", + "url": f"{SF_PROJECTS}/libjpeg-turbo/files/{V['JPEGTURBO']}/" + f"libjpeg-turbo-{V['JPEGTURBO']}.tar.gz/download", + "filename": f"libjpeg-turbo-{V['JPEGTURBO']}.tar.gz", + "dir": f"libjpeg-turbo-{V['JPEGTURBO']}", "license": ["README.ijg", "LICENSE.md"], "license_pattern": ( "(LEGAL ISSUES\n============\n\n.+?)\n\nREFERENCES\n==========" @@ -143,9 +162,9 @@ DEPS = { "bins": ["cjpeg.exe", "djpeg.exe"], }, "zlib": { - "url": "https://zlib.net/zlib131.zip", - "filename": "zlib131.zip", - "dir": "zlib-1.3.1", + "url": f"https://zlib.net/zlib{V['ZLIB_DOTLESS']}.zip", + "filename": f"zlib{V['ZLIB_DOTLESS']}.zip", + "dir": f"zlib-{V['ZLIB']}", "license": "README", "license_pattern": "Copyright notice:\n\n(.+)$", "build": [ @@ -157,9 +176,9 @@ DEPS = { "libs": [r"*.lib"], }, "xz": { - "url": SF_PROJECTS + "/lzmautils/files/xz-5.4.5.tar.gz/download", - "filename": "xz-5.4.5.tar.gz", - "dir": "xz-5.4.5", + "url": f"{SF_PROJECTS}/lzmautils/files/xz-{V['XZ']}.tar.gz/download", + "filename": f"xz-{V['XZ']}.tar.gz", + "dir": f"xz-{V['XZ']}", "license": "COPYING", "build": [ *cmds_cmake("liblzma", "-DBUILD_SHARED_LIBS:BOOL=OFF"), @@ -170,9 +189,9 @@ DEPS = { "libs": [r"liblzma.lib"], }, "libwebp": { - "url": "http://downloads.webmproject.org/releases/webp/libwebp-1.3.2.tar.gz", - "filename": "libwebp-1.3.2.tar.gz", - "dir": "libwebp-1.3.2", + "url": f"http://downloads.webmproject.org/releases/webp/libwebp-{V['LIBWEBP']}.tar.gz", + "filename": f"libwebp-{V['LIBWEBP']}.tar.gz", + "dir": f"libwebp-{V['LIBWEBP']}", "license": "COPYING", "patch": { r"src\enc\picture_csp_enc.c": { @@ -192,9 +211,9 @@ DEPS = { "libs": [r"libsharpyuv.lib", r"libwebp*.lib"], }, "libtiff": { - "url": "https://download.osgeo.org/libtiff/tiff-4.6.0.tar.gz", - "filename": "tiff-4.6.0.tar.gz", - "dir": "tiff-4.6.0", + "url": f"https://download.osgeo.org/libtiff/tiff-{V['TIFF']}.tar.gz", + "filename": f"tiff-{V['TIFF']}.tar.gz", + "dir": f"tiff-{V['TIFF']}", "license": "LICENSE.md", "patch": { r"libtiff\tif_lzma.c": { @@ -224,21 +243,24 @@ DEPS = { "libs": [r"libtiff\*.lib"], }, "libpng": { - "url": SF_PROJECTS + "/libpng/files/libpng16/1.6.39/lpng1639.zip/download", - "filename": "lpng1639.zip", - "dir": "lpng1639", + "url": f"{SF_PROJECTS}/libpng/files/libpng{V['LIBPNG_XY']}/{V['LIBPNG']}/" + f"lpng{V['LIBPNG_DOTLESS']}.zip/download", + "filename": f"lpng{V['LIBPNG_DOTLESS']}.zip", + "dir": f"lpng{V['LIBPNG_DOTLESS']}", "license": "LICENSE", "build": [ *cmds_cmake("png_static", "-DPNG_SHARED:BOOL=OFF", "-DPNG_TESTS:BOOL=OFF"), - cmd_copy("libpng16_static.lib", "libpng16.lib"), + cmd_copy( + f"libpng{V['LIBPNG_XY']}_static.lib", f"libpng{V['LIBPNG_XY']}.lib" + ), ], "headers": [r"png*.h"], - "libs": [r"libpng16.lib"], + "libs": [f"libpng{V['LIBPNG_XY']}.lib"], }, "brotli": { - "url": "https://github.com/google/brotli/archive/refs/tags/v1.1.0.tar.gz", - "filename": "brotli-1.1.0.tar.gz", - "dir": "brotli-1.1.0", + "url": f"https://github.com/google/brotli/archive/refs/tags/v{V['BROTLI']}.tar.gz", + "filename": f"brotli-{V['BROTLI']}.tar.gz", + "dir": f"brotli-{V['BROTLI']}", "license": "LICENSE", "build": [ *cmds_cmake(("brotlicommon", "brotlidec"), "-DBUILD_SHARED_LIBS:BOOL=OFF"), @@ -247,9 +269,9 @@ DEPS = { "libs": ["*.lib"], }, "freetype": { - "url": "https://download.savannah.gnu.org/releases/freetype/freetype-2.13.2.tar.gz", - "filename": "freetype-2.13.2.tar.gz", - "dir": "freetype-2.13.2", + "url": f"https://download.savannah.gnu.org/releases/freetype/freetype-{V['FREETYPE']}.tar.gz", + "filename": f"freetype-{V['FREETYPE']}.tar.gz", + "dir": f"freetype-{V['FREETYPE']}", "license": ["LICENSE.TXT", r"docs\FTL.TXT", r"docs\GPLv2.TXT"], "patch": { r"builds\windows\vc2010\freetype.vcxproj": { @@ -262,7 +284,7 @@ DEPS = { "": "FT_CONFIG_OPTION_SYSTEM_ZLIB;FT_CONFIG_OPTION_USE_PNG;FT_CONFIG_OPTION_USE_HARFBUZZ;FT_CONFIG_OPTION_USE_BROTLI", # noqa: E501 "": r"{dir_harfbuzz}\src;{inc_dir}", # noqa: E501 "": "{lib_dir}", # noqa: E501 - "": "zlib.lib;libpng16.lib;brotlicommon.lib;brotlidec.lib", # noqa: E501 + "": f"zlib.lib;libpng{V['LIBPNG_XY']}.lib;brotlicommon.lib;brotlidec.lib", # noqa: E501 }, r"src/autofit/afshaper.c": { # link against harfbuzz.lib @@ -282,9 +304,9 @@ DEPS = { "libs": [r"objs\{msbuild_arch}\Release Static\freetype.lib"], }, "lcms2": { - "url": SF_PROJECTS + "/lcms/files/lcms/2.16/lcms2-2.16.tar.gz/download", - "filename": "lcms2-2.16.tar.gz", - "dir": "lcms2-2.16", + "url": f"{SF_PROJECTS}/lcms/files/lcms/{V['LCMS2']}/lcms2-{V['LCMS2']}.tar.gz/download", # noqa: E501 + "filename": f"lcms2-{V['LCMS2']}.tar.gz", + "dir": f"lcms2-{V['LCMS2']}", "license": "LICENSE", "patch": { r"Projects\VC2022\lcms2_static\lcms2_static.vcxproj": { @@ -308,21 +330,16 @@ DEPS = { "libs": [r"Lib\MS\*.lib"], }, "openjpeg": { - "url": "https://github.com/uclouvain/openjpeg/archive/v2.5.0.tar.gz", - "filename": "openjpeg-2.5.0.tar.gz", - "dir": "openjpeg-2.5.0", + "url": f"https://github.com/uclouvain/openjpeg/archive/v{V['OPENJPEG']}.tar.gz", + "filename": f"openjpeg-{V['OPENJPEG']}.tar.gz", + "dir": f"openjpeg-{V['OPENJPEG']}", "license": "LICENSE", - "patch": { - r"src\lib\openjp2\ht_dec.c": { - "#ifdef OPJ_COMPILER_MSVC\n return (OPJ_UINT32)__popcnt(val);": "#if defined(OPJ_COMPILER_MSVC) && (defined(_M_IX86) || defined(_M_AMD64))\n return (OPJ_UINT32)__popcnt(val);", # noqa: E501 - } - }, "build": [ *cmds_cmake( "openjp2", "-DBUILD_CODEC:BOOL=OFF", "-DBUILD_SHARED_LIBS:BOOL=OFF" ), - cmd_mkdir(r"{inc_dir}\openjpeg-2.5.0"), - cmd_copy(r"src\lib\openjp2\*.h", r"{inc_dir}\openjpeg-2.5.0"), + cmd_mkdir(rf"{{inc_dir}}\openjpeg-{V['OPENJPEG']}"), + cmd_copy(r"src\lib\openjp2\*.h", rf"{{inc_dir}}\openjpeg-{V['OPENJPEG']}"), ], "libs": [r"bin\*.lib"], }, @@ -348,9 +365,9 @@ DEPS = { "libs": [r"imagequant.lib"], }, "harfbuzz": { - "url": "https://github.com/harfbuzz/harfbuzz/archive/8.3.0.zip", - "filename": "harfbuzz-8.3.0.zip", - "dir": "harfbuzz-8.3.0", + "url": f"https://github.com/harfbuzz/harfbuzz/archive/{V['HARFBUZZ']}.zip", + "filename": f"harfbuzz-{V['HARFBUZZ']}.zip", + "dir": f"harfbuzz-{V['HARFBUZZ']}", "license": "COPYING", "build": [ *cmds_cmake( @@ -363,12 +380,12 @@ DEPS = { "libs": [r"*.lib"], }, "fribidi": { - "url": "https://github.com/fribidi/fribidi/archive/v1.0.13.zip", - "filename": "fribidi-1.0.13.zip", - "dir": "fribidi-1.0.13", + "url": f"https://github.com/fribidi/fribidi/archive/v{V['FRIBIDI']}.zip", + "filename": f"fribidi-{V['FRIBIDI']}.zip", + "dir": f"fribidi-{V['FRIBIDI']}", "license": "COPYING", "build": [ - cmd_copy(r"COPYING", r"{bin_dir}\fribidi-1.0.13-COPYING"), + cmd_copy(r"COPYING", rf"{{bin_dir}}\fribidi-{V['FRIBIDI']}-COPYING"), cmd_copy(r"{winbuild_dir}\fribidi.cmake", r"CMakeLists.txt"), # generated tab.i files cannot be cross-compiled " ^&^& ".join( @@ -445,6 +462,7 @@ def find_msvs(architecture: str) -> dict[str, str] | None: def download_dep(url: str, file: str) -> None: + import urllib.error import urllib.request ex = None @@ -461,11 +479,14 @@ def download_dep(url: str, file: str) -> None: raise RuntimeError(ex) -def extract_dep(url: str, filename: str) -> None: +def extract_dep(url: str, filename: str, prefs: dict[str, str]) -> None: import tarfile import zipfile - file = os.path.join(args.depends_dir, filename) + depends_dir = prefs["depends_dir"] + sources_dir = prefs["src_dir"] + + file = os.path.join(depends_dir, filename) if not os.path.exists(file): # First try our mirror mirror_url = ( @@ -504,13 +525,15 @@ def extract_dep(url: str, filename: str) -> None: raise RuntimeError(msg) -def write_script(name: str, lines: list[str]) -> None: - name = os.path.join(args.build_dir, name) +def write_script( + name: str, lines: list[str], prefs: dict[str, str], verbose: bool +) -> None: + name = os.path.join(prefs["build_dir"], name) lines = [line.format(**prefs) for line in lines] print("Writing " + name) with open(name, "w", newline="") as f: f.write(os.linesep.join(lines)) - if args.verbose: + if verbose: for line in lines: print(" " + line) @@ -526,7 +549,7 @@ def get_footer(dep: dict) -> list[str]: return lines -def build_env() -> None: +def build_env(prefs: dict[str, str], verbose: bool) -> None: lines = [ "if defined DISTUTILS_USE_SDK goto end", cmd_set("INCLUDE", "{inc_dir}"), @@ -539,33 +562,35 @@ def build_env() -> None: ":end", "@echo on", ] - write_script("build_env.cmd", lines) + write_script("build_env.cmd", lines, prefs, verbose) -def build_dep(name: str) -> str: +def build_dep(name: str, prefs: dict[str, str], verbose: bool) -> str: dep = DEPS[name] - dir = dep["dir"] + directory = dep["dir"] file = f"build_dep_{name}.cmd" + license_dir = prefs["license_dir"] + sources_dir = prefs["src_dir"] - extract_dep(dep["url"], dep["filename"]) + extract_dep(dep["url"], dep["filename"], prefs) licenses = dep["license"] if isinstance(licenses, str): licenses = [licenses] license_text = "" for license_file in licenses: - with open(os.path.join(sources_dir, dir, license_file)) as f: + with open(os.path.join(sources_dir, directory, license_file)) as f: license_text += f.read() if "license_pattern" in dep: match = re.search(dep["license_pattern"], license_text, re.DOTALL) license_text = "\n".join(match.groups()) assert len(license_text) > 50 - with open(os.path.join(license_dir, f"{dir}.txt"), "w") as f: - print(f"Writing license {dir}.txt") + with open(os.path.join(license_dir, f"{directory}.txt"), "w") as f: + print(f"Writing license {directory}.txt") f.write(license_text) for patch_file, patch_list in dep.get("patch", {}).items(): - patch_file = os.path.join(sources_dir, dir, patch_file.format(**prefs)) + patch_file = os.path.join(sources_dir, directory, patch_file.format(**prefs)) with open(patch_file) as f: text = f.read() for patch_from, patch_to in patch_list.items(): @@ -577,22 +602,22 @@ def build_dep(name: str) -> str: print(f"Patching {patch_file}") f.write(text) - banner = f"Building {name} ({dir})" + banner = f"Building {name} ({directory})" lines = [ r'call "{build_dir}\build_env.cmd"', "@echo " + ("=" * 70), f"@echo ==== {banner:<60} ====", "@echo " + ("=" * 70), - cmd_cd(os.path.join(sources_dir, dir)), + cmd_cd(os.path.join(sources_dir, directory)), *dep.get("build", []), *get_footer(dep), ] - write_script(file, lines) + write_script(file, lines, prefs, verbose) return file -def build_dep_all() -> None: +def build_dep_all(disabled: list[str], prefs: dict[str, str], verbose: bool) -> None: lines = [r'call "{build_dir}\build_env.cmd"'] gha_groups = "GITHUB_ACTIONS" in os.environ for dep_name in DEPS: @@ -600,7 +625,7 @@ def build_dep_all() -> None: if dep_name in disabled: print(f"Skipping disabled dependency {dep_name}") continue - script = build_dep(dep_name) + script = build_dep(dep_name, prefs, verbose) if gha_groups: lines.append(f"@echo ::group::Running {script}") lines.append(rf'cmd.exe /c "{{build_dir}}\{script}"') @@ -609,12 +634,11 @@ def build_dep_all() -> None: lines.append("@echo ::endgroup::") print() lines.append("@echo All Pillow dependencies built successfully!") - write_script("build_dep_all.cmd", lines) + write_script("build_dep_all.cmd", lines, prefs, verbose) -if __name__ == "__main__": +def main() -> None: winbuild_dir = os.path.dirname(os.path.realpath(__file__)) - pillow_dir = os.path.realpath(os.path.join(winbuild_dir, "..")) parser = argparse.ArgumentParser( prog="winbuild\\build_prepare.py", @@ -720,15 +744,15 @@ if __name__ == "__main__": "architecture": args.architecture, **arch_prefs, # Pillow paths - "pillow_dir": pillow_dir, "winbuild_dir": winbuild_dir, # Build paths + "bin_dir": bin_dir, "build_dir": args.build_dir, + "depends_dir": args.depends_dir, "inc_dir": inc_dir, "lib_dir": lib_dir, - "bin_dir": bin_dir, - "src_dir": sources_dir, "license_dir": license_dir, + "src_dir": sources_dir, # Compilers / Tools **msvs, "cmake": "cmake.exe", # TODO find CMAKE automatically @@ -741,6 +765,10 @@ if __name__ == "__main__": print() - write_script(".gitignore", ["*"]) - build_env() - build_dep_all() + write_script(".gitignore", ["*"], prefs, args.verbose) + build_env(prefs, args.verbose) + build_dep_all(disabled, prefs, args.verbose) + + +if __name__ == "__main__": + main()