Skip to content

Accept falsy file-like objects in the C Unpacker - #773

Open
0z5a wants to merge 1 commit into
msgpack:mainfrom
0z5a:fix/falsy-file-like
Open

0z5a wants to merge 1 commit into
msgpack:mainfrom
0z5a:fix/falsy-file-like

Conversation

@0z5a

@0z5a 0z5a commented Nov 15, 2026

Copy link
Copy Markdown

Unpacker(file_like) in the C extension checks if file_like: before it looks up file_like.read. A stream that defines __bool__ or __len__ and is falsy is therefore stored as the input source, but its read method is never saved. The first unpack(), next() or read_bytes() that needs data then fails with TypeError: 'NoneType' object is not callable.

Such objects are common file wrappers. For example, werkzeug's FileStorage is falsy when it has no filename, so Unpacker(FileStorage(io.BytesIO(data), filename="")) fails with the C extension. The pure Python Unpacker checks file_like is None and unpacks the same stream correctly, and the documentation only asks for an object with a .read(n) method.

import io
import msgpack

class FalsyBytesIO(io.BytesIO):
    def __bool__(self):
        return False

list(msgpack.Unpacker(FalsyBytesIO(msgpack.packb([1, 2]))))
# C extension:  TypeError: 'NoneType' object is not callable
# fallback:     [[1, 2]]

This PR changes the check to if file_like is not None:, which matches the fallback and the rest of the class (feed(), read_bytes() and _unpack() already test self.file_like is not None).

Nothing that worked before changes behaviour. A falsy non-None file_like could not be used at all: feed() raised AssertionError and reading raised the TypeError above. A falsy object without a read attribute, such as b"", now raises AttributeError in the constructor, as the fallback already does.

A regression test in test/test_sequnpack.py unpacks two objects and calls read_bytes() on a falsy BytesIO subclass. It fails with the C extension before this change and passes after it; it passes with MSGPACK_PUREPYTHON=1 both before and after. The full test suite passes with the C extension (179 passed, 1 skipped) and with the pure Python implementation (176 passed, 4 skipped), and ruff check / ruff format --check are clean.

This change was drafted with Claude Code (claude-opus-5-5); the test results above are from the runs described.

Unpacker.__init__ in the C extension used `if file_like:` before saving
`file_like.read`. A stream that defines __bool__ or __len__ and is falsy
(for example werkzeug's FileStorage without a filename) was stored as the
input but its read method was not, so the first read failed with
"TypeError: 'NoneType' object is not callable". The pure Python Unpacker
checks `file_like is None` and works.

Check `file_like is not None` instead and add a regression test.

This branch has not been deployed

No deployments
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

Successfully merging this pull request may close these issues.

1 participant