4

If my library has a contrib extra that has dependencies in it (say requests) that I want users to have to install to have access to a CLI API, but I install the contrib extra during my tests in CI how do I use pytest's MonkeyPatch to remove the dependencies during tests to ensure my detection is correct?

For example, if the contrib extra will additionally install requests and so I want users to have to do

$ python -m pip install mylib[contrib]

to then be able to at the command line have a CLI API that would look like

$ mylib contrib myfunction

where myfunction uses the requests dependency

# mylib/src/mylib/cli/contrib.py
import click
try:
    import requests
except ModuleNotFoundError:
    pass # should probably warn though, but this is just an example

# ...

@click.group(name="contrib")
def cli():
    """
    Contrib experimental operations.
    """

@cli.command()
@click.argument("example", default="-")
def myfunction(example):
   requests.get(example)
   # ...

How do I mock or monkeypatch out requests in my pytest tests so that I can make sure that a user would properly get a warning along with the ModuleNotFoundError if they just do

$ python -m pip install mylib
$ mylib contrib myfunction

? After reading some other questions on the pytest tag I still don't think I understand how to do this, so I'm asking here.

Matthew Feickert
  • 786
  • 6
  • 26
  • Does [this](https://stackoverflow.com/questions/62455023/mock-import-failure) answer your question? – MrBean Bremen Sep 13 '20 at 07:56
  • 1
    https://stackoverflow.com/q/51044068/2650249 – hoefling Sep 15 '20 at 22:34
  • Thanks both for your replies, but perhaps I should have specified that in the pytest tests I'm using the script_runner fixture from pytest-console-scripts to make sure I'm actually testing the CLI functionality. I agree that in most situations something like the [Mock import example](https://stackoverflow.com/questions/62455023/mock-import-failure) would work, but there seems to be an issue with state of the runner I need to resolve. – Matthew Feickert Sep 30 '20 at 22:39
  • Does this answer your question? [How to write unittests for an optional dependency in a python package?](https://stackoverflow.com/questions/37916040/how-to-write-unittests-for-an-optional-dependency-in-a-python-package) – ricoms Mar 11 '21 at 05:18

1 Answers1

3

What I ended up doing that worked, and which I had confirmed was a reasonable method thanks to Anthony Sottile, was to mock that the extra dependency (here requests) does not exist by setting it to None in sys.modules and then reloading the module(s) that would require use of requests. I test that there is an actual complaint that requests doesn't exist to be imported by using caplog.

Here is the test I'm currently using (with the names changed to match my toy example problem in the question above)

import mylib
import sys
import logging
import pytest
from unittest import mock
from importlib import reload
from importlib import import_module

# ...

def test_missing_contrib_extra(caplog):
    with mock.patch.dict(sys.modules):
        sys.modules["requests"] = None
        if "mylib.contrib.utils" in sys.modules:
            reload(sys.modules["mylib.contrib.utils"])
        else:
            import_module("mylib.cli")

    with caplog.at_level(logging.ERROR):
        # The 2nd and 3rd lines check for an error message that mylib throws
        for line in [
            "import of requests halted; None in sys.modules",
            "Installation of the contrib extra is required to use mylib.contrib.utils.download",
            "Please install with: python -m pip install mylib[contrib]",
        ]:
            assert line in caplog.text
        caplog.clear()

I should note that this is actually advocated in @Abhyudai's answer to "Test for import of optional dependencies in init.py with pytest: Python 3.5 /3.6 differs in behaviour" which @hoefling linked to above (posted after I had solved this problem but before I got around to posting this).

If people are interested in seeing this in an actual library, c.f. the following two PRs:

A note: Anthony Sottile has warned that

reload() can be kinda iffy -- I'd be careful with it (things which have old references to the old module will live on, sometimes it can introduce new copies of singletons (doubletons? tripletons?)) -- I've tracked down many-a-test-pollution problems to reload()

so I'll revise this answer if I implement a safer alternative.

Matthew Feickert
  • 786
  • 6
  • 26
  • Maybe for clarification - but `monkeypatch.setitem` is probably better to rely on? – kratsg Dec 14 '20 at 18:07
  • 2
    From this Twitter thread, Anthony would say no: https://twitter.com/codewithanthony/status/1338547566607101952 "my personal opinion is to never use the monkeypatch fixture because mocking scope is not super well defined / controlled" – Matthew Feickert Dec 14 '20 at 18:31