Repository: getmoto/moto

Issue:
moto not compatible with mock 4.0.3
**Versions**
Using `moto-1.3.16` (boto-2.49.0 boto3-1.16.20 botocore-1.19.33)

**Problem**
Mock has released 5h ago a new version: 4.0.3
Since then our tests are failing with the exception below.

**Fix**
Forcing the version of mock to `4.0.2` is solving our issue

```
Traceback (most recent call last):
  File "/usr/local/Cellar/python@3.9/3.9.0_1/Frameworks/Python.framework/Versions/3.9/lib/python3.9/runpy.py", line 197, in _run_module_as_main
    return _run_code(code, main_globals, None,
  File "/usr/local/Cellar/python@3.9/3.9.0_1/Frameworks/Python.framework/Versions/3.9/lib/python3.9/runpy.py", line 87, in _run_code
    exec(code, run_globals)
  File "/usr/local/Cellar/python@3.9/3.9.0_1/Frameworks/Python.framework/Versions/3.9/lib/python3.9/unittest/__main__.py", line 18, in <module>
    main(module=None)
  File "/usr/local/Cellar/python@3.9/3.9.0_1/Frameworks/Python.framework/Versions/3.9/lib/python3.9/unittest/main.py", line 101, in __init__
    self.runTests()
  File "/usr/local/Cellar/python@3.9/3.9.0_1/Frameworks/Python.framework/Versions/3.9/lib/python3.9/unittest/main.py", line 271, in runTests
    self.result = testRunner.run(self.test)
  File "/usr/local/Cellar/python@3.9/3.9.0_1/Frameworks/Python.framework/Versions/3.9/lib/python3.9/unittest/runner.py", line 176, in run
    test(result)
  File "/usr/local/Cellar/python@3.9/3.9.0_1/Frameworks/Python.framework/Versions/3.9/lib/python3.9/unittest/suite.py", line 84, in __call__
    return self.run(*args, **kwds)
  File "/usr/local/Cellar/python@3.9/3.9.0_1/Frameworks/Python.framework/Versions/3.9/lib/python3.9/unittest/suite.py", line 122, in run
    test(result)
  File "/usr/local/Cellar/python@3.9/3.9.0_1/Frameworks/Python.framework/Versions/3.9/lib/python3.9/unittest/suite.py", line 84, in __call__
    return self.run(*args, **kwds)
  File "/usr/local/Cellar/python@3.9/3.9.0_1/Frameworks/Python.framework/Versions/3.9/lib/python3.9/unittest/suite.py", line 122, in run
    test(result)
  File "/usr/local/Cellar/python@3.9/3.9.0_1/Frameworks/Python.framework/Versions/3.9/lib/python3.9/unittest/suite.py", line 84, in __call__
    return self.run(*args, **kwds)
  File "/usr/local/Cellar/python@3.9/3.9.0_1/Frameworks/Python.framework/Versions/3.9/lib/python3.9/unittest/suite.py", line 122, in run
    test(result)
  File "/usr/local/Cellar/python@3.9/3.9.0_1/Frameworks/Python.framework/Versions/3.9/lib/python3.9/unittest/case.py", line 653, in __call__
    return self.run(*args, **kwds)
  File "../.venv/lib/python3.9/site-packages/moto/core/models.py", line 102, in wrapper
    self.stop()
  File "../.venv/lib/python3.9/site-packages/moto/core/models.py", line 86, in stop
    self.default_session_mock.stop()
  File "../.venv/lib/python3.9/site-packages/mock/mock.py", line 1563, in stop
    return self.__exit__(None, None, None)
  File "../.venv/lib/python3.9/site-packages/mock/mock.py", line 1529, in __exit__
    if self.is_local and self.temp_original is not DEFAULT:
```


Discussion:
Thanks for raising it @etiennechabert - will investigate
Do you have an example test case? I can't reproduce this with a bare-bones scenario:

```
pip install moto
pip show mock
Version: 4.0.3
```
```
@mock_s3
def test_x():
  print("working")
```
Can you please add to yours:
- `@mock_cloudwatch`
- `@mock_sts `

With `@mock_s3` these are all the mock we are using in the test failing with mock 4.0.3.

Additionally, we are also using `@set_initial_no_auth_action_count(1)` (moto.core) inside of a `@mock_sts`

Let me know if this helps, otherwise I come back with more tomorrow.
Still no luck with this test case, @etiennechabert:
```
import boto3
from moto import mock_cloudwatch, mock_s3, mock_sts
from moto.core import set_initial_no_auth_action_count


@mock_cloudwatch
@mock_s3
@mock_sts
def test_x():
	s3 = boto3.client("s3", region_name="us-east-1")
	
	@set_initial_no_auth_action_count(1)
	def inside():
		print("inside")
	print(s3)
	inside()
```

I'm testing this all on Linux (Ubuntu 20.04.1), although I'm assuming that doesn't make a difference..
can you try this?
```
import unittest

import boto3
from moto import mock_s3


@mock_s3
class MyTestCase(unittest.TestCase):

    def test_x(self):
        s3 = boto3.client("s3", region_name="us-east-1")

        def inside():
            print("inside")

        print(s3)
        inside()


if __name__ == "__main__":
    unittest.main()
```

Resolving patch:
diff --git a/moto/core/models.py b/moto/core/models.py
--- a/moto/core/models.py
+++ b/moto/core/models.py
@@ -35,6 +35,7 @@
 
 class BaseMockAWS(object):
     nested_count = 0
+    mocks_active = False
 
     def __init__(self, backends):
         from moto.instance_metadata import instance_metadata_backend
@@ -50,13 +51,12 @@ def __init__(self, backends):
         self.backends_for_urls.update(self.backends)
         self.backends_for_urls.update(default_backends)
 
-        # "Mock" the AWS credentials as they can't be mocked in Botocore currently
-        FAKE_KEYS = {
+        self.FAKE_KEYS = {
             "AWS_ACCESS_KEY_ID": "foobar_key",
             "AWS_SECRET_ACCESS_KEY": "foobar_secret",
         }
+        self.ORIG_KEYS = {}
         self.default_session_mock = mock.patch("boto3.DEFAULT_SESSION", None)
-        self.env_variables_mocks = mock.patch.dict(os.environ, FAKE_KEYS)
 
         if self.__class__.nested_count == 0:
             self.reset()
@@ -74,8 +74,10 @@ def __exit__(self, *args):
         self.stop()
 
     def start(self, reset=True):
-        self.default_session_mock.start()
-        self.env_variables_mocks.start()
+        if not self.__class__.mocks_active:
+            self.default_session_mock.start()
+            self.mock_env_variables()
+            self.__class__.mocks_active = True
 
         self.__class__.nested_count += 1
         if reset:
@@ -85,14 +87,16 @@ def start(self, reset=True):
         self.enable_patching()
 
     def stop(self):
-        self.default_session_mock.stop()
-        self.env_variables_mocks.stop()
         self.__class__.nested_count -= 1
 
         if self.__class__.nested_count < 0:
             raise RuntimeError("Called stop() before start().")
 
         if self.__class__.nested_count == 0:
+            if self.__class__.mocks_active:
+                self.default_session_mock.stop()
+                self.unmock_env_variables()
+                self.__class__.mocks_active = False
             self.disable_patching()
 
     def decorate_callable(self, func, reset):
@@ -139,6 +143,24 @@ def decorate_class(self, klass):
                 continue
         return klass
 
+    def mock_env_variables(self):
+        # "Mock" the AWS credentials as they can't be mocked in Botocore currently
+        # self.env_variables_mocks = mock.patch.dict(os.environ, FAKE_KEYS)
+        # self.env_variables_mocks.start()
+        for k, v in self.FAKE_KEYS.items():
+            self.ORIG_KEYS[k] = os.environ.get(k, None)
+            os.environ[k] = v
+
+    def unmock_env_variables(self):
+        # This doesn't work in Python2 - for some reason, unmocking clears the entire os.environ dict
+        # Obviously bad user experience, and also breaks pytest - as it uses PYTEST_CURRENT_TEST as an env var
+        # self.env_variables_mocks.stop()
+        for k, v in self.ORIG_KEYS.items():
+            if v:
+                os.environ[k] = v
+            else:
+                del os.environ[k]
+
 
 class HttprettyMockAWS(BaseMockAWS):
     def reset(self):
diff --git a/setup.py b/setup.py
--- a/setup.py
+++ b/setup.py
@@ -60,7 +60,7 @@ def get_version():
     "configparser<5.0; python_version < '3'",
     "Jinja2>=2.10.1",
     "Jinja2<3.0.0; python_version < '3'",
-    "mock<=4.0.2",
+    "mock",
     "mock<=3.0.5; python_version < '3'",
     "more-itertools",
     "more-itertools==5.0.0; python_version < '3'",


Test patch:
diff --git a/tests/test_core/test_environ_patching.py b/tests/test_core/test_environ_patching.py
new file mode 100644
--- /dev/null
+++ b/tests/test_core/test_environ_patching.py
@@ -0,0 +1,34 @@
+import os
+import sure  # noqa
+from moto import mock_ec2, mock_s3
+
+KEY = "AWS_ACCESS_KEY_ID"
+
+
+def test_aws_keys_are_patched():
+    with mock_ec2():
+        patched_value = os.environ[KEY]
+        patched_value.should.equal("foobar_key")
+
+
+def test_aws_keys_can_be_none():
+    """
+    Verify that the os.environ[KEY] can be None
+    Patching the None-value shouldn't be an issue
+    """
+    original = os.environ.get(KEY, "value-set-by-user")
+    # Delete the original value by the user
+    try:
+        del os.environ[KEY]
+    except KeyError:
+        pass  # Value might not be set on this system in the first place
+    try:
+        # Verify that the os.environ[KEY] is patched
+        with mock_s3():
+            patched_value = os.environ[KEY]
+            patched_value.should.equal("foobar_key")
+        # Verify that the os.environ[KEY] is unpatched, and reverts to None
+        assert os.environ.get(KEY) is None
+    finally:
+        # Reset the value original - don't want to change the users system
+        os.environ[KEY] = original
diff --git a/tests/test_s3/test_s3_classdecorator.py b/tests/test_s3/test_s3_classdecorator.py
new file mode 100644
--- /dev/null
+++ b/tests/test_s3/test_s3_classdecorator.py
@@ -0,0 +1,18 @@
+import unittest
+
+import boto3
+from moto import mock_s3
+
+
+@mock_s3
+class ClassDecoratorTest(unittest.TestCase):
+    """
+    https://github.com/spulec/moto/issues/3535
+    An update to the mock-package introduced a failure during teardown.
+    This test is in place to catch any similar failures with our mocking approach
+    """
+
+    def test_instantiation_succeeds(self):
+        s3 = boto3.client("s3", region_name="us-east-1")
+
+        assert s3 is not None