Repository: python-pillow/Pillow

Issue:
Is CFFI still needed?
The CFFI is only used in PyAccess as a replacement for the C implementation in Access.c/PixelAccess. This was done in #476 to improve performance when using PyPy (see performance profiling in #816). However, that was nine years ago, and the PyPy cpyext has been [greatly improved](https://www.pypy.org/posts/2018/09/inside-cpyext-why-emulating-cpython-c-8083064623681286567.html) since then. Is PyAccess still necessary for performance on PyPy, or can we go back to just using the C implementation?


Discussion:
How does performance profiling look now with PyPy, with and without CFFI?
Running Tests/bench_cffi_access.py,

PyPy3.9 on Windows - https://github.com/radarhere/Pillow/actions/runs/5343847790/jobs/9687477077#step:27:46
> PyAccess - get: breaking at 30 iterations, 0.343930 per iteration
> PyAccess - set: breaking at 32 iterations, 0.317073 per iteration
> C-api - get: breaking at 245 iterations, 0.040906 per iteration
> C-api - set: breaking at 259 iterations, 0.038629 per iteration

PyPy3.10 on Windows - https://github.com/radarhere/Pillow/actions/runs/5343847790/jobs/9687477221#step:27:46
> PyAccess - get: breaking at 36 iterations, 0.278341 per iteration
> PyAccess - set: breaking at 42 iterations, 0.243066 per iteration
> C-api - get: breaking at 208 iterations, 0.048108 per iteration
> C-api - set: breaking at 198 iterations, 0.050532 per iteration

PyPy3.9 on Ubuntu - https://github.com/radarhere/Pillow/actions/runs/5343847795/jobs/9687476737#step:8:63
> PyAccess - get: breaking at 58 iterations, 0.173087 per iteration
> PyAccess - set: breaking at 76 iterations, 0.132276 per iteration
> C-api - get: breaking at 187 iterations, 0.053598 per iteration
> C-api - set: breaking at 193 iterations, 0.052231 per iteration

PyPy3.10 on Ubuntu - https://github.com/radarhere/Pillow/actions/runs/5343847795/jobs/9687476573
> PyAccess - get: breaking at 63 iterations, 0.160498 per iteration
> PyAccess - set: breaking at 83 iterations, 0.121258 per iteration
> C-api - get: breaking at 197 iterations, 0.050895 per iteration
> C-api - set: breaking at 204 iterations, 0.050110 per iteration

So PyAccess is running 2-8 times faster than the C API.
> So PyAccess is running 2-8 times faster than the C API.

Isn't that backwards? In the last test PyAccess took 0.12-0.16 seconds per iteration, while the C API took 0.05 seconds per iteration. So it looks like the C API is actually faster than PyAccess.
I'm fine with taking it out -- There were some somewhat janky hacks to make that work (the unsafe members in the c struct) so it would be nice to clean that all up. Assuming that the benchmarks are pointing the right way. 

Resolving patch:
diff --git a/src/PIL/Image.py b/src/PIL/Image.py
--- a/src/PIL/Image.py
+++ b/src/PIL/Image.py
@@ -107,8 +107,7 @@ class DecompressionBombError(Exception):
     raise
 
 
-# works everywhere, win for pypy, not cpython
-USE_CFFI_ACCESS = hasattr(sys, "pypy_version_info")
+USE_CFFI_ACCESS = False
 try:
     import cffi
 except ImportError:
diff --git a/src/PIL/PyAccess.py b/src/PIL/PyAccess.py
--- a/src/PIL/PyAccess.py
+++ b/src/PIL/PyAccess.py
@@ -22,6 +22,8 @@
 import logging
 import sys
 
+from ._deprecate import deprecate
+
 try:
     from cffi import FFI
 
@@ -47,6 +49,7 @@
 
 class PyAccess:
     def __init__(self, img, readonly=False):
+        deprecate("PyAccess", 11)
         vals = dict(img.im.unsafe_ptrs)
         self.readonly = readonly
         self.image8 = ffi.cast("unsigned char **", vals["image8"])


Test patch:
diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py
--- a/Tests/test_image_access.py
+++ b/Tests/test_image_access.py
@@ -232,11 +232,13 @@ def test_p_putpixel_rgb_rgba(self, mode, color):
         assert im.convert("RGBA").getpixel((0, 0)) == (255, 0, 0, alpha)
 
 
+@pytest.mark.filterwarnings("ignore::DeprecationWarning")
 @pytest.mark.skipif(cffi is None, reason="No CFFI")
 class TestCffiPutPixel(TestImagePutPixel):
     _need_cffi_access = True
 
 
+@pytest.mark.filterwarnings("ignore::DeprecationWarning")
 @pytest.mark.skipif(cffi is None, reason="No CFFI")
 class TestCffiGetPixel(TestImageGetPixel):
     _need_cffi_access = True
@@ -252,7 +254,8 @@ def _test_get_access(self, im):
         Using private interfaces, forcing a capi access and
         a pyaccess for the same image"""
         caccess = im.im.pixel_access(False)
-        access = PyAccess.new(im, False)
+        with pytest.warns(DeprecationWarning):
+            access = PyAccess.new(im, False)
 
         w, h = im.size
         for x in range(0, w, 10):
@@ -264,20 +267,16 @@ def _test_get_access(self, im):
             access[(access.xsize + 1, access.ysize + 1)]
 
     def test_get_vs_c(self):
-        rgb = hopper("RGB")
-        rgb.load()
-        self._test_get_access(rgb)
-        self._test_get_access(hopper("RGBA"))
-        self._test_get_access(hopper("L"))
-        self._test_get_access(hopper("LA"))
-        self._test_get_access(hopper("1"))
-        self._test_get_access(hopper("P"))
-        # self._test_get_access(hopper('PA')) # PA -- how do I make a PA image?
-        self._test_get_access(hopper("F"))
+        with pytest.warns(DeprecationWarning):
+            rgb = hopper("RGB")
+            rgb.load()
+            self._test_get_access(rgb)
+            for mode in ("RGBA", "L", "LA", "1", "P", "F"):
+                self._test_get_access(hopper(mode))
 
-        for mode in ("I;16", "I;16L", "I;16B", "I;16N", "I"):
-            im = Image.new(mode, (10, 10), 40000)
-            self._test_get_access(im)
+            for mode in ("I;16", "I;16L", "I;16B", "I;16N", "I"):
+                im = Image.new(mode, (10, 10), 40000)
+                self._test_get_access(im)
 
         # These don't actually appear to be modes that I can actually make,
         # as unpack sets them directly into the I mode.
@@ -292,7 +291,8 @@ def _test_set_access(self, im, color):
         Using private interfaces, forcing a capi access and
         a pyaccess for the same image"""
         caccess = im.im.pixel_access(False)
-        access = PyAccess.new(im, False)
+        with pytest.warns(DeprecationWarning):
+            access = PyAccess.new(im, False)
 
         w, h = im.size
         for x in range(0, w, 10):
@@ -301,13 +301,15 @@ def _test_set_access(self, im, color):
                 assert color == caccess[(x, y)]
 
         # Attempt to set the value on a read-only image
-        access = PyAccess.new(im, True)
+        with pytest.warns(DeprecationWarning):
+            access = PyAccess.new(im, True)
         with pytest.raises(ValueError):
             access[(0, 0)] = color
 
     def test_set_vs_c(self):
         rgb = hopper("RGB")
-        rgb.load()
+        with pytest.warns(DeprecationWarning):
+            rgb.load()
         self._test_set_access(rgb, (255, 128, 0))
         self._test_set_access(hopper("RGBA"), (255, 192, 128, 0))
         self._test_set_access(hopper("L"), 128)
@@ -326,6 +328,7 @@ def test_set_vs_c(self):
         # im = Image.new('I;32B', (10, 10), 2**10)
         # self._test_set_access(im, 2**13-1)
 
+    @pytest.mark.filterwarnings("ignore::DeprecationWarning")
     def test_not_implemented(self):
         assert PyAccess.new(hopper("BGR;15")) is None
 
@@ -335,7 +338,8 @@ def test_reference_counting(self):
 
         for _ in range(10):
             # Do not save references to the image, only to the access object
-            px = Image.new("L", (size, 1), 0).load()
+            with pytest.warns(DeprecationWarning):
+                px = Image.new("L", (size, 1), 0).load()
             for i in range(size):
                 # pixels can contain garbage if image is released
                 assert px[i, 0] == 0
@@ -344,12 +348,13 @@ def test_reference_counting(self):
     def test_p_putpixel_rgb_rgba(self, mode):
         for color in ((255, 0, 0), (255, 0, 0, 127 if mode == "PA" else 255)):
             im = Image.new(mode, (1, 1))
-            access = PyAccess.new(im, False)
-            access.putpixel((0, 0), color)
+            with pytest.warns(DeprecationWarning):
+                access = PyAccess.new(im, False)
+                access.putpixel((0, 0), color)
 
-            if len(color) == 3:
-                color += (255,)
-            assert im.convert("RGBA").getpixel((0, 0)) == color
+                if len(color) == 3:
+                    color += (255,)
+                assert im.convert("RGBA").getpixel((0, 0)) == color
 
 
 class TestImagePutPixelError(AccessTest):