Skip to content

FileBinaryRead: file is already closed #209

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

Open
danjac opened this issue Apr 3, 2025 · 6 comments
Open

FileBinaryRead: file is already closed #209

danjac opened this issue Apr 3, 2025 · 6 comments
Assignees
Labels
bug Something isn't working
Milestone

Comments

@danjac
Copy link

danjac commented Apr 3, 2025

Trying to run this command using FileBinaryRead e.g.

./manage.py my_command path_to_file.txt

app = Typer()

@app.command()
def handle(self, file: typer.FileBinaryRead) -> None:
    """Print out file"""
    for chunk in file:
        print(chunk)

ValueError: I/O operation on closed file

In the Typer docs this should work: https://typer.tiangolo.com/tutorial/parameter-types/file/#filebinaryread

@bckohan bckohan self-assigned this Apr 3, 2025
@bckohan bckohan added the bug Something isn't working label Apr 3, 2025
bckohan added a commit that referenced this issue Apr 3, 2025
@bckohan
Copy link
Member

bckohan commented Apr 3, 2025

@danjac Thanks for the bug report!

I suspect this bug exists because django's BaseCommand has a two-phase parse/execute execution pathway and we've stuffed click's single phase pathway into it which results in some redundant work - for example two contexts are constructed. Click's file type closes the file on context destruction.

I really consider this a click design flaw because parse/execute should be independent exposed but chainable processes - but I doubt click is going to change at this point so we probably need to come up with a workaround. I do not want to inject special case code for these types because working equivalent behavior is easy (see below) and the most correct solution would be to avoid the redundant contexts. This will be more difficult to solve behind Django's interface, so I might not be able to get to it for a bit.

In the mean time it's probably best to avoid these types anyway. This implementation is a similar level of verbosity and also fully encapsulates file/open close. The above code puts the responsibility on the caller to close the file when invoked directly as a function (not through the CLI or call_command):

An equivalent example:

app = Typer()

@app.command()
def handle(self, file: Path) -> None:
    """Print out file"""
    for chunk in file.open("rb"):
        print(chunk)

An example that encapsulates handle ownership more fully:

app = Typer()

@app.command()
def handle(self, file: Path) -> None:
    """Print out file"""
    with open(file, "rb") as f:
        for chunk in f:
            print(chunk)

@bckohan
Copy link
Member

bckohan commented Apr 3, 2025

Blocked by #210

@bckohan bckohan added this to the 3.2 milestone Apr 3, 2025
@danjac
Copy link
Author

danjac commented Apr 4, 2025

Unfortunately I also need to be able to pipe streams to the command as well as use file paths, so for example cat some_file.txt | ./manage.py my_command - does not work with Path as argument.

@bckohan
Copy link
Member

bckohan commented Apr 5, 2025

@danjac Weirdly the pipe use case actually works. I'll see what I can do.

@bckohan
Copy link
Member

bckohan commented Apr 5, 2025

@danjac I don't want to rush this fix because it's delicate. In the meantime here's an easy workaround that works for the pipe and argument case:

app = Typer()

@app.command()
def handle(self, file: typer.FileBinaryRead) -> None:
    """Print out file"""
    if file.closed:
        file = open(file.name, "rb")
    for chunk in file:
        print(chunk)

@danjac
Copy link
Author

danjac commented Apr 5, 2025

Makes sense, thanks!

bckohan added a commit that referenced this issue Apr 18, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

When branches are created from issues, their pull requests are automatically linked.

2 participants