Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Failed: [undefined]OSError: Could not find a backend to open <fsspec.implementations.local.LocalFileOpener object at * > with iomode 'r' since version 2022.8.0 #1057

Closed
ptrba opened this issue Oct 2, 2022 · 5 comments

Comments

@ptrba
Copy link
Contributor

ptrba commented Oct 2, 2022

Description

Starting with version 2022.8.0 the 'file' protocol does not interoperate with imageio.v3. Reading a file from fs.open(filename,mode='rb') leads to the exeption

Failed: [undefined]OSError: Could not find a backend to open <fsspec.implementations.local.LocalFileOpener object at * > with iomode 'r'

Despite the fact that it occurs in the imageio read call, I guess this is an fsspec issue since the issue does not occur on fsspec==2022.7.1. Also, other fsspec protocols, such as abfs work just fine.

Releated issue may be #579 .

Minimal reproducible sample

import fsspec
import numpy as np
import uuid
import tempfile
import imageio.v3 as iio
import pathlib
with tempfile.TemporaryDirectory() as dir:
    filename = f"{dir}/{str(uuid.uuid4())}.bmp"
    fs = fsspec.filesystem('file',auto_mkdir=True)
    img =(np.random.rand(100,100,3) * 255).astype('uint8')
    with fs.open(filename, mode='wb') as file:
        iio.imwrite(file, img, extension=pathlib.Path(filename).suffix)
    with fs.open(filename, mode='rb') as file:
        img_ = iio.imread(file, extension=pathlib.Path(filename).suffix)
        np.testing.assert_array_equal(img,img_)
@ptrba ptrba changed the title Failed: [undefined]OSError: Could not find a backend to open <fsspec.implementations.local.LocalFileOpener object at * > with iomode 'r' since version 2022.8.2 on Windows Failed: [undefined]OSError: Could not find a backend to open <fsspec.implementations.local.LocalFileOpener object at * > with iomode 'r' since version 2022.8.1 Oct 3, 2022
@ptrba ptrba changed the title Failed: [undefined]OSError: Could not find a backend to open <fsspec.implementations.local.LocalFileOpener object at * > with iomode 'r' since version 2022.8.1 Failed: [undefined]OSError: Could not find a backend to open <fsspec.implementations.local.LocalFileOpener object at * > with iomode 'r' since version 2022.8.0 Oct 3, 2022
@martindurant
Copy link
Member

I haven't yet found the problem root, but it is certainly on the write call not the read - the BMP extension is not making it to pillow, and so whatever is default is getting written, so then BMP fails to read it. A file does get created, but nothing can read it as a BMP. Yet, you can pass the fsspec file-like to pillow for writing or reading just fine. Perhaps we can ping someone at imagio to ask why the extension information isn't reaching the required place?

@ptrba
Copy link
Contributor Author

ptrba commented Oct 8, 2022

@martindurant: I agree that the problem is in the write and not in the read. I nailed down the problem to #1010. Due to the new method fileno, imageio will directly use the integer file descriptor rather than the file object to write to. This happens in https://github.com/python-pillow/Pillow/blob/main/src/PIL/ImageFile.py:520

try:
    fh = fp.fileno()
    fp.flush()
    _encode_tile(im, fp, tile, bufsize, fh)
except (AttributeError, io.UnsupportedOperation) as exc:
    _encode_tile(im, fp, tile, bufsize, None, exc)
if hasattr(fp, "flush"):
    fp.flush()

Before #1010 (without fileno) the AttributeError was raised and the _encode_tile happened without using fileno. After #1010 the _encode_tile is invoked using fh. Under the hood, _encode_tile uses a function encoder.encode_to_file which does not work when used with fsspec. I used the following example to investigate this:

version = "head"
dir = pathlib.Path().resolve()
# just an arbitrary bitmap with constant value 23, to easily analyze the binaries
img = (23 * np.ones((3, 3, 3))).astype("uint8")
# via native python file open, works just fine
filename = f"{dir}/via-native-{version}.bmp"
with open(filename, mode="wb") as file:
    iio.imwrite(file, img, extension=pathlib.Path(filename).suffix)
with open(filename, mode="rb") as file:
    img_ = iio.imread(file, extension=pathlib.Path(filename).suffix)
    np.testing.assert_array_equal(img, img_)

# via fsspec open, writes the bitmap header AFTER the content of the file
# hence throws could not find a backend to open
filename = f"{dir}/via-fsspec-{version}.bmp"
fs = fsspec.filesystem("file", auto_mkdir=True)
with fs.open(filename, mode="wb") as file:
    iio.imwrite(file, img, extension=pathlib.Path(filename).suffix)
with open(filename, mode="rb") as file:
    img_ = iio.imread(file, extension=pathlib.Path(filename).suffix)
    np.testing.assert_array_equal(img, img_)

The output of the files in int8 decoding is:
via-native.bmp (valid bitmap)

0    0    3    0    0    0    3    0    0    0    1    0   24    0    0    0
0    0   36    0    0    0  -60   14    0    0  -60   14    0    0    0    0
0    0    0    0    0    0   23   23   23   23   23   23   23   23   23    0
0    0   23   23   23   23   23   23   23   23   23    0    0    0   23   23
23   23   23   23   23   23   23    0    0    0

via-fsspec.bmp (invalid bitmap)

 23   23   23   23   23   23   23   23   23    0    0    0   23   23   23   23
23   23   23   23   23    0    0    0   23   23   23   23   23   23   23   23
23    0    0    0   66   77   90    0    0    0    0    0    0    0   54    0
 0    0   40    0    0    0    3    0    0    0    3    0    0    0    1    0
24    0    0    0    0    0   36    0    0    0  -60   14    0    0  -60   14
 0    0    0    0    0    0    0    0    0    0

It turns out, that the bmp header is located at the tail (!) instead of the start (starting with 66 77 90 ...) of the file.

I conclude, that the LocalFileOpener does not work properly when used with 'integer file descriptors'. If LocalFileOpener is supposed to support fileno (such as suggested in #1010) then this issue should be further investigated.

Next, I would drill into the encode_to_file of imageio to fix this issue. Before doing this, I would like to ask fsspec experts to have a look at the LocalFileOpenener and guess why the encoder does write header and content in the wrong order.

@ptrba
Copy link
Contributor Author

ptrba commented Oct 10, 2022

Found the issue. The fp.flush() call in https://github.com/python-pillow/Pillow/blob/main/src/PIL/ImageFile.py:504 translates into a LocalFileOpener.__getattr__(self,'__IOBase_closed'). I guess this comes from the python IOBase of the spec.AbstractBufferedFile class, which checks for 'open' before 'flush' is called.

I cannot make this more transparent, the actual call chain is hidden in cpython. One explanation would be, that the exception raised by LocalFileOpener.__getattr__(self,'__IOBase_closed') is silently interpreted as closed = True and flush never gets called.

In any case, making the flush override explicit in the LocalFileOpener fixes the issue. I filed a PR draft #1070.

It is probably a good idea to completely remove the LocalFileOpener.__getattr__ call and replace it by explicit overrides. So far I found that name and raw are the only ones being translated into LocalFileOpener.__getattr__. I suggest to include that into #1070, but I need an expert opinion on the actual purpose of LocalFileOpener.__getattr__.

@ptrba
Copy link
Contributor Author

ptrba commented Oct 10, 2022

I see the point of having the LocalFileOpener.__getattr__ is to cover all methods of the IOBase hierarchy https://docs.python.org/3/library/io.html#io.IOBase (including Raw, Buffered and File). I made PR #1070 for ready review containing only the flush fix for this issue. Please open a new issue if LocalFileOpener.__getattr__ should be replaced by all possible methods of the IOBase hierarchy to avoid similar issues in future. Don't now about possible unintended side effects of such refactoring.

@ptrba
Copy link
Contributor Author

ptrba commented Oct 11, 2022

Solution merged in #1070.

@ptrba ptrba closed this as completed Oct 11, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants