-
-
Notifications
You must be signed in to change notification settings - Fork 2.8k
monkeypatch doesn't patch a module if it has already been imported by production code. #2229
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
Comments
Hi @JohanLorenzo, That's a common gotcha with mocking in Python, and is not particular to Closing this for now, but feel free to ask further questions! |
Okay, I'll stick to |
No problem! 👍 |
@nicoddemus I'd like to also add that the standard 'patching in the wrong place' advice doesn't help if you need to patch a module-level variable specifically before the module is imported, because the setting of that variable will have a side-effect during the moment when the module itself is imported. For example consider something like this:
Suppose in my tests I want to monkeypatch the OS environment variable Such as some
But this does not appear to actually apply the patching, as though pytest collects and imports the module first, presenting no opportunity to get different on-import side effects. |
@spearsem even if you want that, its completely against the basics of the python module system, |
@RonnyPfannschmidt how is it "against" the module system? This pattern is actually quite common in Python. For example, in scientific computing, your production model might be something very large loaded in memory, but there might be a toy fixture model very fast to load just for tests. You can't load both and can't load on-demand per each function, since the time to re-load per function call might still be too much even for the small model. So you need to "load once" but the exact logic of that loading might need to depend on external factors. Often this way of managing loaded data sets is just dictated to the programmer, e.g. from third parties or something, and so the fact that you need to do this while testing is not optional. You often cannot control whether or not this type of side-effectful import is being done, you just have to live with third-party code that foists it onto you. |
@spearsem loading of specific datasets in those cases is done by invoking functions, not by reloading modules after changing env vars |
other valid ways is configuring and skipping tests on different testruns |
@RonnyPfannschmidt what I am asking is more generic. Suppose you're given a third party module that has important side-effects that occur based on settings it automatically detects during import time. You can't change this, but you need your tests to run with different values for those autodetected, on-import settings. What do you do currently? |
short term: run the testrunner multiple times |
Hi @spearsem, @RonnyPfannschmidt is correct in the sense that unfortunately there's not much If it is something you can change, I agree that the best long term solution is to stop doing module-level stuff that depends on global variables, as this is very brittle in the sense that it is hard to make sure nobody imports the module at the wrong moment. The short term solution is to run the test runner multiple times, as @RonnyPfannschmidt suggests: you can use the |
my suggestion si to actually run pytest twice on the outside, pytester is more for testing pytest plugins, less for controlling pytest executions |
@RonnyPfannschmidt unfortunately, I don't think your advice would work for most mainstream, recommended, common workflows. For example it's even one of the core principles of 12 Factor apps to store all config in the environment. It's extremely common to need to read This is especially common with third party dependencies that can't be omitted or replaced, so the developer may have no control over relying on checks to Instead, what is needed is a way for pytest to sandbox the import mechanism in tests, so that for the duration of the test, any uses of |
12 factor does not preclude /change those details, the key question is, do you want easy looking code or testable code Personally I can't find any acceptable reason for not having a central testable entrypoint for configuration over littering it all over the place just to trade controlled dependencies for random globals. My general personal experience with magical globals for configuration is, that it breeds horrible bugs that are hard to test and hard to debug in production. If the mainstream opinion was that such a situation is fine, then it'd be another good example for the polemic phrase "the majority is Always wrong". |
Reading from environ at import time is not in conflict with a central config. In the particular use case I am trying to workaround in pytest, everything is contained in a single file called |
This is an illustrative example: tensorflow/tensorflow#1258 (comment) |
That seems to me like a intentionally horrendous globals dependency of tensor flow, that should be solved via a supporting pytest plugin that deals with that problem |
That's certainly one perspective you can take. For a huge group of other people using this type of pattern in scientific computing for decades, it's just "good software that works and uses environment variables smoothly" and should be something that any test runner can generically handle out of the box. It does seem like a huge gulf between the two perspectives, but as I don't develop on tensorflow or many other scientific tools that use this same pattern, I can't really say what their response would be, I can only imagine it would be equal in magnitude as yours but in the opposite direction and based on at least as much professional software design experience. |
That's why a plugin should contain that difference in design that stems from from different design goals driven by different target audiences |
Aside from preferences, this bears mentioning:
This is far more difficult to accomplish than it appears at first glance, and it might bring its own host of problems given the heterogeneity of code bases out there. This utility doesn't need to be implemented by pytest, mind you, the implementation should be pytest agnostic and can be integrated with pytest via a plugin: def pytest_configure(config):
config._store["import_sandbor"] = ImportSandbox()
@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_setup(item):
sandbox = config._store["import_sandbox"]
sandbox.snapshot()
yield
@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_teardown(item):
yield
sandbox = config._store["import_sandbox"]
sandbox.restore() Or something along those lines. |
I think pytest-forked will do this for you, I actually use |
edit: disclaimer, do not use this in production. This is highly experimental / POC and not intended to be used in any real world code. Although this might go against the "patching in the wrong place" advice from the start of the issue, it is possible to patch the #test/conftest.py
patches = ['module_a.some_func']
def _import(name, *args, **kwargs):
if name == 'module_a' and len(args) > 3 and args[2][0] == 'some_func':
patches.append(f"{args[0]['__name__']}.some_func")
return original_import(name, *args, **kwargs)
import builtins
original_import = builtins.__import__
builtins.__import__ = _import
@pytest.fixture(scope="function", autouse=True)
def some_func_mock(monkeypatch):
def raise_unconfigured(*args, **kwargs):
raise RuntimeError(f"unmocked module_a.some_func call for {args}, {kwargs")
_some_func_mock = MagicMock()
_some_func_mock.side_effect = raise_unconfigured
for patch in patches:
monkeypatch.setattr(patch, _some_func_mock)
return _some_func_mock This is very rough and won't take into account monkeypatch.setattr(patch, _some_func_mock, global=True) Where |
It's a community disservice to post foot guns of that level without appropriate warnings |
I've added a more explicit disclaimer. Do you also have feedback on the proposed design? |
It's a partial solution that allows bad factorings to be less painful and will seriously splat if at any point some extra indirection is added, I strongly recommend to ensure such a hack is never part of a main branch, the cleanup cost is absymal |
@spearsem, @espears1, for cases that depend on os.environ I find pytest hooks useful to control the environment prior to any loading (which rids the need to patch the module pre-loading). For example: pytest.hookimpl()
def pytest_sessionstart(session: pytest.Session) -> None:
test_env: Dict[str, str] = {
'ENV_VAR_1': 'value 1',
'ENV_VAR_2': 'value 2,
...
}
for key, val in test_env.items():
os.environ[key] = val As a side note, you may want to sanitize the environment from other variables to ensure that tests have the exact same enviroment everywhere (as well as in docker setup, if applicable). |
Thank you for this great testing library 😃
If I understand #762 correctly, modules should be reimported if pytest monkeypatch a module, like this:
even if the module has been imported this way:
I have a reduced test case in this repo: https://github.com/JohanLorenzo/pytest-bug-2229. The README contains instructions about how to run it.
I checked the contexts via PDB:
foo
is correctly patched.foo
hasn't been changed.Environment
The text was updated successfully, but these errors were encountered: