OswExpress.upload_file / OswExpressFile document IO as an accepted source, but no in-memory stream can actually be uploaded. Two independent bugs:
1. The IO branch is unreachable.
https://github.com/OpenSemanticLab/osw-python/blob/main/src/osw/express.py#L638
elif isinstance(source, IO):
data["source_file_controller"] = InMemoryController(stream=source)
IO (from typing_extensions, imported at express.py:25) is not runtime-checkable as a protocol:
>>> import io, typing_extensions as te
>>> isinstance(io.BytesIO(b"x"), te.IO)
False
So a BytesIO falls through to the ValueError at express.py:641, whose message advertises IO as valid.
2. InMemoryController cannot be constructed anyway.
https://github.com/OpenSemanticLab/osw-python/blob/main/src/osw/controller/file/memory.py#L21-L23
def __init__(self, **kwargs):
self.stream = StringIO()
super().__init__(**kwargs)
The assignment happens before the model is initialised:
>>> InMemoryController(stream=io.BytesIO(b"hi"))
AttributeError: 'osw.controller.file.memory.InMemoryController' object has no attribute '__iris__'
It also unconditionally overwrites a caller-supplied stream with an empty StringIO, and defaults to StringIO where the put/get counterparts are byte-oriented.
Suggested fix
- Replace the
isinstance(source, IO) check with a duck-typed one (hasattr(source, "read")), or use typing.BinaryIO/io.IOBase.
- Drop the custom
__init__ so pydantic assigns stream from kwargs, or set the default via a field default rather than in __init__.
- Add a test that round-trips a
BytesIO through upload_file.
Context: found while planning the path-free MCP file tools (#133). That work routes around both by calling WikiFileController.put(file: IO) directly, so it is not blocked by this, but the public express API stays broken.
OswExpress.upload_file/OswExpressFiledocumentIOas an acceptedsource, but no in-memory stream can actually be uploaded. Two independent bugs:1. The
IObranch is unreachable.https://github.com/OpenSemanticLab/osw-python/blob/main/src/osw/express.py#L638
IO(fromtyping_extensions, imported atexpress.py:25) is not runtime-checkable as a protocol:So a
BytesIOfalls through to theValueErroratexpress.py:641, whose message advertisesIOas valid.2.
InMemoryControllercannot be constructed anyway.https://github.com/OpenSemanticLab/osw-python/blob/main/src/osw/controller/file/memory.py#L21-L23
The assignment happens before the model is initialised:
It also unconditionally overwrites a caller-supplied
streamwith an emptyStringIO, and defaults toStringIOwhere theput/getcounterparts are byte-oriented.Suggested fix
isinstance(source, IO)check with a duck-typed one (hasattr(source, "read")), or usetyping.BinaryIO/io.IOBase.__init__so pydantic assignsstreamfromkwargs, or set the default via a field default rather than in__init__.BytesIOthroughupload_file.Context: found while planning the path-free MCP file tools (#133). That work routes around both by calling
WikiFileController.put(file: IO)directly, so it is not blocked by this, but the publicexpressAPI stays broken.