Repository: python-pillow/Pillow

Issue:
Pillow should require NumPy >= 1.23
<!--
Thank you for reporting an issue.

Follow these guidelines to ensure your issue is handled properly.

If you have a ...

1.  General question: consider asking the question on Stack Overflow
    with the python-imaging-library tag:

    * https://stackoverflow.com/questions/tagged/python-imaging-library

    Do not ask a question in both places.

    If you think you have found a bug or have an unexplained exception
    then file a bug report here.

2.  Bug report: include a self-contained, copy-pastable example that
    generates the issue if possible. Be concise with code posted.
    Guidelines on how to provide a good bug report:

    * https://stackoverflow.com/help/mcve

    Bug reports which follow these guidelines are easier to diagnose,
    and are often handled much more quickly.

3.  Feature request: do a quick search of existing issues
    to make sure this has not been asked before.

We know asking good questions takes effort, and we appreciate your time.
Thank you.
-->

### What did you do?

I opened a truncated image file and converted it to an array to check the validity of the file. (The equivalent of [this test](https://github.com/radarhere/Pillow/blob/e5046b1057f24794ea83f694ecd23a404873853b/Tests/test_image_array.py#L39-L41), though my file was a tiff.)

### What did you expect to happen?

OSError

### What actually happened?

It returned an actual array.  I didn't imshow it to see what was in the place of the missing bytes.

### What are your OS, Python and Pillow versions?

* OS: ubuntu:20.04 from dockerhub
* Python: 3.8.10
* Pillow: 9.2.0
* Numpy: 1.19.5

<!--
Please include **code** that reproduces the issue and whenever possible, an **image** that demonstrates the issue. Please upload images to GitHub, not to third-party file hosting sites. If necessary, add the image to a zip or tar archive.

The best reproductions are self-contained scripts with minimal dependencies. If you are using a framework such as Plone, Django, or Buildout, try to replicate the issue just using Pillow.
-->

I think it would be better to either (a) keep the workaround for old numpy versions or (b) require a numpy version that works in setup.py.  The current setup is not self consistent.



Discussion:
Just for context, the reason you are specifying Pillow 9.2.0 is because of #6394.

Can I ask, what is it that lead you to update Pillow to the latest version, but not NumPy?
I'm not entirely sure which dependency caused it, but this was the result when I installed my package with pip.  I assume one of my other dependencies defaulted to an older NumPy version, but I'm not sure which one.  I'm still digging through to figure out a self consistent setup.

Regardless, in principle there doesn't have to be a particular reason - somebody could have an older NumPy installed already but no Pillow, and then run `pip install pillow`, and as it currently stands they would get Pillow 9.2.0 while keeping their old NumPy.  They would then get bitten by this issue like I was.
NumPy is an optional dependency of Pillow. Performing a quick Google search, I don't think that an optional dependency can have a minimum version. Correct me if I missed something.

I then considered the possibility of raising an error if NumPy is too old in `__array_interface__`... but the problem in the first place (#5084) is that `__array_interface__` can't raise anything except for a `RecursionError` or `MemoryError`.

So I don't think that b) is possible.

As for a), keeping the workaround, I'm not keen on introducing a change (#5379), reverting that change (#6394), and then re-introducing it... I think that's off-putting to users, who would prefer a consistent experience - whether or not an error is raised is not the only difference. See #6581.

What if we instead raised a warning when an error is triggered with older NumPy versions? I've created PR #6594 for this.

Resolving patch:
diff --git a/src/PIL/Image.py b/src/PIL/Image.py
--- a/src/PIL/Image.py
+++ b/src/PIL/Image.py
@@ -679,12 +679,24 @@ def __array_interface__(self):
         new["shape"] = shape
         new["typestr"] = typestr
         new["version"] = 3
-        if self.mode == "1":
-            # Binary images need to be extended from bits to bytes
-            # See: https://github.com/python-pillow/Pillow/issues/350
-            new["data"] = self.tobytes("raw", "L")
-        else:
-            new["data"] = self.tobytes()
+        try:
+            if self.mode == "1":
+                # Binary images need to be extended from bits to bytes
+                # See: https://github.com/python-pillow/Pillow/issues/350
+                new["data"] = self.tobytes("raw", "L")
+            else:
+                new["data"] = self.tobytes()
+        except Exception as e:
+            if not isinstance(e, (MemoryError, RecursionError)):
+                try:
+                    import numpy
+                    from packaging.version import parse as parse_version
+                except ImportError:
+                    pass
+                else:
+                    if parse_version(numpy.__version__) < parse_version("1.23"):
+                        warnings.warn(e)
+            raise
         return new
 
     def __getstate__(self):


Test patch:
diff --git a/Tests/test_image_array.py b/Tests/test_image_array.py
--- a/Tests/test_image_array.py
+++ b/Tests/test_image_array.py
@@ -35,10 +35,13 @@ def test_with_dtype(dtype):
     test_with_dtype(numpy.float64)
     test_with_dtype(numpy.uint8)
 
-    if parse_version(numpy.__version__) >= parse_version("1.23"):
-        with Image.open("Tests/images/truncated_jpeg.jpg") as im_truncated:
+    with Image.open("Tests/images/truncated_jpeg.jpg") as im_truncated:
+        if parse_version(numpy.__version__) >= parse_version("1.23"):
             with pytest.raises(OSError):
                 numpy.array(im_truncated)
+        else:
+            with pytest.warns(UserWarning):
+                numpy.array(im_truncated)
 
 
 def test_fromarray():