testing-cabal / mock

The Python mock library
https://docs.python.org/dev/library/unittest.mock.html
BSD 2-Clause "Simplified" License
536 stars 107 forks source link

Can no longer patch flask.g #490

Closed rtaft closed 4 years ago

rtaft commented 4 years ago

After upgrading from mock 3.0.5 to 4.0.0, I am no longer able to patch flask.g

from mock import patch

import flask

def some_function():
    flask.g.somevariable = True
    return flask.g.somevariable

@patch('flask.g')
def test_some_function(mock_flask_global):
    assert some_function()

This raises the error:

name = 'g'

    def _lookup_app_object(name):
        top = _app_ctx_stack.top
        if top is None:
>           raise RuntimeError(_app_ctx_err_msg)
E           RuntimeError: Working outside of application context.
E           
E           This typically means that you attempted to use functionality that needed
E           to interface with the current application object in some way. To solve
E           this, set up an application context with app.app_context().  See the
E           documentation for more information.

venv/lib/python3.6/site-packages/flask/globals.py:45: RuntimeError
============================== short test summary info =========================
FAILED temp_test.py::test_some_function - RuntimeError: Working outside of application context.
cjw296 commented 4 years ago

Why 4.0.0 rather than 4.0.2? What happens if you try and latest Python 3 release rather than 3.6?

(Trying to find out where the change was introduced, rather than telling you to use either of these things for ever...)

rtaft commented 4 years ago

I started at the latest and moved back to 4.0.0 trying to track down when it was introduced. It also happens on python 3.8.0. For this setup I used pyenv to install python 3.8

$ ~/pyenv/shims/python3.8 -m venv p3venv
$ . ./p3venv/bin/activate
$ python --version
Python 3.8.0
$ pip install mock pytest flask
... install output ...
$ pytest temp_test.py 
============================================================================================================ test session starts =============================================================================================================
platform linux -- Python 3.8.0, pytest-5.4.2, py-1.8.1, pluggy-0.13.1
rootdir: /home/rtaft/testpath, inifile: pytest.ini
collected 1 item                                                                                                                                                                                                                             

temp_test.py F                                                                                                                                                                                                                         [100%]

================================================================================================================== FAILURES ==================================================================================================================
_____________________________________________________________________________________________________________ test_some_function _____________________________________________________________________________________________________________

args = (), keywargs = {}

    @wraps(func)
    def patched(*args, **keywargs):
>       with self.decoration_helper(patched,
                                    args,
                                    keywargs) as (newargs, newkeywargs):

p3venv/lib/python3.8/site-packages/mock/mock.py:1366: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
../../../pyenv/versions/3.8.0/lib/python3.8/contextlib.py:113: in __enter__
    return next(self.gen)
p3venv/lib/python3.8/site-packages/mock/mock.py:1334: in decoration_helper
    arg = patching.__enter__()
p3venv/lib/python3.8/site-packages/mock/mock.py:1460: in __enter__
    if spec is None and _is_async_obj(original):
p3venv/lib/python3.8/site-packages/mock/mock.py:52: in _is_async_obj
    if hasattr(obj, '__func__'):
p3venv/lib/python3.8/site-packages/werkzeug/local.py:347: in __getattr__
    return getattr(self._get_current_object(), name)
p3venv/lib/python3.8/site-packages/werkzeug/local.py:306: in _get_current_object
    return self.__local()
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

name = 'g'

    def _lookup_app_object(name):
        top = _app_ctx_stack.top
        if top is None:
>           raise RuntimeError(_app_ctx_err_msg)
E           RuntimeError: Working outside of application context.
E           
E           This typically means that you attempted to use functionality that needed
E           to interface with the current application object in some way. To solve
E           this, set up an application context with app.app_context().  See the
E           documentation for more information.

p3venv/lib/python3.8/site-packages/flask/globals.py:45: RuntimeError
========================================================================================================== short test summary info ===========================================================================================================
FAILED temp_test.py::test_some_function - RuntimeError: Working outside of application context.
============================================================================================================= 1 failed in 0.28s ==============================================================================================================
cjw296 commented 4 years ago

Okay, this looks like an upstream bug, please can you file on https://bugs.python.org/? (A link to this issue may well suffice!) I'll close this issue, but if you can ping a link to the bugs.python.org issue on here once submitted, it may help others.

Once it's fixed upstream, I can roll another backport release to get you the fix.

rtaft commented 4 years ago

Posted https://bugs.python.org/issue40805

rtaft commented 4 years ago

@cjw296 Why do you believe this is an upstream bug? They pushed back and said it is not. Replacing mock with the upstream unittest.mock works without issues.

cjw296 commented 4 years ago

I'm afraid @terryjreedy's comment wasn't very helpful, please see my followup.

But, also, you need to replace mock with unittest.mock in your imports if you're asked to try and reproduce against upstream.

Why do I believe this is an upstream bug? Because this repo us a rolling backport of cpython's unittest.mock, so bugs, unless specifically in backporting changes, are going to be in the upstream cpython repo and need to be fixed there, not here.

rtaft commented 4 years ago

I was able to reproduce this with python 3.8.3 and 3.9-dev with just unittest.mock. I only tested 3.6 before.

hmvp commented 4 years ago

The issue is that in 3.8 support was added for mocking async code. This is done by checking if the thing to mock has a __func__ attribute:

/usr/lib/python3.8/unittest/mock.py:51: in _is_async_obj
    if hasattr(obj, '__func__'):

This triggers flask code since flask overrides __getattr__. The fix is probably that flask need to ignore __getattr__ calls for __func__ and return an Error instead of trying to start flask magic..

See https://github.com/pallets/flask/issues/3637

hmvp commented 4 years ago

As a workaround set the spec attribute while patching (anything will probably work but using the correct value is best)

@patch('flask.g', spec={})
def test_some_function(mock_flask_global):
    assert some_function()