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

ValueError: I/O operation on closed file in testing #824

Open
ResidentMario opened this issue Jul 9, 2017 · 13 comments
Open

ValueError: I/O operation on closed file in testing #824

ResidentMario opened this issue Jul 9, 2017 · 13 comments
Labels

Comments

@ResidentMario
Copy link

I am getting a ValueError when using CliRunner to test my CLI tool. Unfortunately I can't isolate, but I can give a reproduction:

pip install git+git://github.com/ResidentMario/datablocks.git@d6601c8321e7fb050bb62760067dc63ca4600003
pip install pytest
cd datablocks/tests
pytest cli_tests.py

Which raises the following traceback:

=================================== FAILURES ===================================
_________________________ TestLink.test_depositor_link _________________________

self = <cli_tests.TestLink testMethod=test_depositor_link>

    def test_depositor_link(self):
>       result = runner.invoke(cli.link, ["test_depositor.py", "--outputs", "['bar2.txt']"])

cli_tests.py:41: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <click.testing.CliRunner object at 0x7f2c833a6cc0>
cli = <click.core.Command object at 0x7f2c82e7a5f8>
args = ['test_depositor.py', '--outputs', "['bar2.txt']"], input = None
env = None, catch_exceptions = True, color = False, extra = {}
exc_info = (<class 'SystemExit'>, SystemExit(0,), <traceback object at 0x7f2c82e1eb08>)
out = <_io.BytesIO object at 0x7f2c82e0a5c8>, exception = None, exit_code = 0

    def invoke(self, cli, args=None, input=None, env=None,
               catch_exceptions=True, color=False, **extra):
        """Invokes a command in an isolated environment.  The arguments are
            forwarded directly to the command line script, the `extra` keyword
            arguments are passed to the :meth:`~clickpkg.Command.main` function of
            the command.
    
            This returns a :class:`Result` object.
    
            .. versionadded:: 3.0
               The ``catch_exceptions`` parameter was added.
    
            .. versionchanged:: 3.0
               The result object now has an `exc_info` attribute with the
               traceback if available.
    
            .. versionadded:: 4.0
               The ``color`` parameter was added.
    
            :param cli: the command to invoke
            :param args: the arguments to invoke
            :param input: the input data for `sys.stdin`.
            :param env: the environment overrides.
            :param catch_exceptions: Whether to catch any other exceptions than
                                     ``SystemExit``.
            :param extra: the keyword arguments to pass to :meth:`main`.
            :param color: whether the output should contain color codes. The
                          application can still override this explicitly.
            """
        exc_info = None
        with self.isolation(input=input, env=env, color=color) as out:
            exception = None
            exit_code = 0
    
            try:
                cli.main(args=args or (),
                         prog_name=self.get_default_prog_name(cli), **extra)
            except SystemExit as e:
                if e.code != 0:
                    exception = e
    
                exc_info = sys.exc_info()
    
                exit_code = e.code
                if not isinstance(exit_code, int):
                    sys.stdout.write(str(exit_code))
                    sys.stdout.write('\n')
                    exit_code = 1
            except Exception as e:
                if not catch_exceptions:
                    raise
                exception = e
                exit_code = -1
                exc_info = sys.exc_info()
            finally:
                sys.stdout.flush()
>               output = out.getvalue()
E               ValueError: I/O operation on closed file.

../../../miniconda3/envs/datablocks-dev/lib/python3.6/site-packages/click/testing.py:299: ValueError
========================== 1 failed in 37.10 seconds ===========================

This is on Ubuntu 16.04.

@blueyed
Copy link
Contributor

blueyed commented Mar 26, 2018

I am seeing this also when using __import__('pdb').set_trace() somewhere inside of the runner.
This is likely related to pytest turning IO-capturing off / changing it.

blueyed added a commit to blueyed/click that referenced this issue Mar 26, 2018
This also helps with failures during testing, e.g. when using
`pdb.set_tracce()` somewhere inside the runner's isolation - likely
since it handles `write` already.

Ref: pallets#824
@blueyed
Copy link
Contributor

blueyed commented Mar 26, 2018

Reminded me of an old stash I had: #951.

@blueyed
Copy link
Contributor

blueyed commented Mar 26, 2018

As for pytest itself I've created pytest-dev/pytest#3344.

@blueyed
Copy link
Contributor

blueyed commented Mar 31, 2018

See also #654 (duplicate?!).

@fdavis fdavis added the bug label May 17, 2018
@jesserobertson
Copy link

jesserobertson commented Jan 16, 2019

I hit a similar error when writing a CLI of my own & thought I'd share my fix in the hope it helps isolate where there might be a fix (and this is the top hit in Google for me).

In one of my commands I was opening stdout using click.get_text_stream - something like

with click.open_file(input) as src, click.get_text_stream('stdout') as sink:
    data = parse_input()
    sink.write(json.dumps(data) + '\n')

...and I think the closing of the sink stream at the end of the context was killing the mocked version of stdout in the test?

Anyway, the workaround was to shift to click.echo which is much better behaved (and doesn't require the context manager). Hopefully this helps someone else (I certainly wasted a fair bit of time working it out!).

@glemaitre
Copy link

I got the issue recently as well. It was due to the use of a logger within the function.
Using click.echo can as well solve the issue if the logging was to only display some info to the user.

@thomasleveil
Copy link

The workaround I found is to write my pytest function with the caplog fixture and making sure no logger will produce any message.

def test_xxxx(caplog):
    caplog.set_level(100000)  
    ...

@cdleonard
Copy link

I recently ran into this and created a repo which reproduces the issue standalone: https://github.com/cdleonard/click-pytest-issue

The problem seems to be that the stdio capturing of pytest and click does not mix well. My workaround is to just avoid CliRunner and call main() directly because pytest caplog/capsys already covers everything.

Maybe there could be an option to disable the stdio isolation feature of CliRunner?

@clamdad
Copy link

clamdad commented Dec 22, 2021

See #2156 for a simple example that reproduces the error on Windows. The example only fails if pytest live logs is enabled.

@trothwell
Copy link

trothwell commented Jun 8, 2023

Doing the following seems to work well for me:

def test_no_args(capsys: CaptureFixture):
    with capsys.disabled() as disabled:
        result = CliRunner.invoke(...)

mtache added a commit to gmuloc/network-test-automation that referenced this issue Nov 30, 2023
mtache added a commit to aristanetworks/anta that referenced this issue Dec 7, 2023
… get commands (#447)

* Test: Add tests for get from-ansible
* CI: Update pre-commit
* fix `tags` for nrfu foo commands
* add custom type for loglevel
* add error handling for None arguments
* add logs to anta.device
* Cleanup anta.cli
* add logs to anta.inventory
* update unit tests for anta.cli
* fix pallets/click#824
* update unit tests for anta.cli.nrfu
* refactor cli modules
* update anta.cli.check and anta.cli.debug
* update cli unit tests
* update unit tests for anta.cli
* small fix for anta get from-ansible
* refactor wrappers
* consume ignore_status and ignore_error
* remove shebangs
* test revision and version in unit tests
* fix anta nrfu --help
* Test: Fix tox coloring issues
* Test: Adjust pytest default logging level
* Refactor(anta): Make sure test name is not truncated in table
* Test: Set width of CliRunner
* Update documentation
* Add warning about CLI changes in documentation
* change -log option to -l

---------

Co-authored-by: Matthieu Tâche <mtache@arista.com>
@gmuloc
Copy link

gmuloc commented Dec 14, 2023

FYI, building on previous answers in this thread, we ended up creating the following fixture to disable capsys on all invoke calls (truncated to simplify)

@pytest.fixture
def click_runner(capsys: CaptureFixture[str]) -> Iterator[CliRunner]:
    """
    Convenience fixture to return a click.CliRunner for cli testing
    """

    class MyCliRunner(CliRunner):
        """Override CliRunner to disable capsys"""
        def invoke(self, *args, **kwargs) -> Result: 
            # Way to fix https://github.com/pallets/click/issues/824
            with capsys.disabled():
                result = super().invoke(*args, **kwargs)
            return result
    
    yield MyCliRunner()

@bodograumann
Copy link

See #2156 for a simple example that reproduces the error on Windows. The example only fails if pytest live logs is enabled.

I had the issue as well for pytest live logs. I actually wanted to see logs for passed tests though (not necessarily "live"). So running pytest -rP instead worked for me.

@jlumpe
Copy link

jlumpe commented Mar 27, 2024

I was running into this with the --cli-log-level option, solved by adding -s.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests