Skip to content

Uploading a file from an in-memory stream is impossible: both code paths are broken #140

Description

@LukasGold

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions