Techdebt: Hide private decorator-methods/attributes (#7216)

This commit is contained in:
Bert Blommers 2024-01-14 20:51:55 +00:00
parent 5aa3cc9d73
commit 0c8ccbb406
7 changed files with 129 additions and 101 deletions

View File

@ -40,6 +40,20 @@ Docker Digest for 4.2.14: _sha256:2fa10aa48e32f85c63c62a7d437b8a4b320a56a8494bc2
* SNS: set_subscription_attributes() can now unset the FilterPolicy
5.0.0alpha2:
------------
General:
* It is now possible to configure methods/services which should reach out to AWS.
@mock_aws(
config={"core": {"mock_credentials": False, "passthrough": {"urls": [], "services": []}}}
)
* All requests now return a RequestId
Miscellaneous:
* S3: list_objects() now returns a hashed ContinuationToken
5.0.0alpha1:
------------

View File

@ -1,41 +1,4 @@
from typing import TYPE_CHECKING, Callable, Optional, TypeVar, Union, overload
from moto import settings
from moto.core.config import DefaultConfig
from moto.core.models import MockAWS, ProxyModeMockAWS, ServerModeMockAWS
if TYPE_CHECKING:
from typing_extensions import ParamSpec
P = ParamSpec("P")
T = TypeVar("T")
@overload
def mock_aws(func: "Callable[P, T]") -> "Callable[P, T]":
...
@overload
def mock_aws(func: None = None, config: Optional[DefaultConfig] = None) -> "MockAWS":
...
def mock_aws(
func: "Optional[Callable[P, T]]" = None,
config: Optional[DefaultConfig] = None,
) -> Union["MockAWS", "Callable[P, T]"]:
clss = (
ServerModeMockAWS
if settings.TEST_SERVER_MODE
else (ProxyModeMockAWS if settings.test_proxy_mode() else MockAWS)
)
if func is not None:
return clss().__call__(func=func)
else:
return clss(config)
from moto.core.decorator import mock_aws # noqa # pylint: disable=unused-import
__title__ = "moto"
__version__ = "4.2.15.dev"

View File

@ -29,9 +29,6 @@ class BotocoreStubber:
def __init__(self) -> None:
self.enabled = False
def reset(self) -> None:
BackendDict.reset()
def __call__(
self, event_name: str, request: Any, **kwargs: Any
) -> Optional[AWSResponse]:

37
moto/core/decorator.py Normal file
View File

@ -0,0 +1,37 @@
from typing import TYPE_CHECKING, Callable, Optional, TypeVar, Union, overload
from moto import settings
from moto.core.config import DefaultConfig
from moto.core.models import MockAWS, ProxyModeMockAWS, ServerModeMockAWS
if TYPE_CHECKING:
from typing_extensions import ParamSpec
P = ParamSpec("P")
T = TypeVar("T")
@overload
def mock_aws(func: "Callable[P, T]") -> "Callable[P, T]":
...
@overload
def mock_aws(func: None = None, config: Optional[DefaultConfig] = None) -> "MockAWS":
...
def mock_aws(
func: "Optional[Callable[P, T]]" = None,
config: Optional[DefaultConfig] = None,
) -> Union["MockAWS", "Callable[P, T]"]:
clss = (
ServerModeMockAWS
if settings.TEST_SERVER_MODE
else (ProxyModeMockAWS if settings.test_proxy_mode() else MockAWS)
)
if func is not None:
return clss().__call__(func=func)
else:
return clss(config)

View File

@ -27,6 +27,7 @@ from botocore.handlers import BUILTIN_HANDLERS
import moto.backend_index as backend_index
from moto import settings
from .base_backend import BackendDict
from .botocore_stubber import BotocoreStubber
from .config import DefaultConfig, default_user_config, mock_credentials
from .custom_responses_mock import (
@ -48,9 +49,9 @@ T = TypeVar("T")
class MockAWS(ContextManager["MockAWS"]):
nested_count = 0
mocks_active = False
mock_init_lock = Lock()
_nested_count = 0
_mocks_active = False
_mock_init_lock = Lock()
def __init__(self, config: Optional[DefaultConfig] = None) -> None:
self._fake_creds = {
@ -58,17 +59,17 @@ class MockAWS(ContextManager["MockAWS"]):
"AWS_SECRET_ACCESS_KEY": "FOOBARSECRET",
}
self._orig_creds: Dict[str, Optional[str]] = {}
self.default_session_mock = patch("boto3.DEFAULT_SESSION", None)
self._default_session_mock = patch("boto3.DEFAULT_SESSION", None)
current_user_config = default_user_config.copy()
current_user_config.update(config or {})
self.user_config_mock = patch.dict(default_user_config, current_user_config)
self._user_config_mock = patch.dict(default_user_config, current_user_config)
def __call__(
self, func: "Callable[P, T]", reset: bool = True, remove_data: bool = True
) -> "Callable[P, T]":
if inspect.isclass(func):
return self.decorate_class(func)
return self.decorate_callable(func, reset, remove_data)
return self._decorate_class(func)
return self._decorate_callable(func, reset, remove_data)
def __enter__(self) -> "MockAWS":
self.start()
@ -78,37 +79,37 @@ class MockAWS(ContextManager["MockAWS"]):
self.stop()
def start(self, reset: bool = True) -> None:
with MockAWS.mock_init_lock:
self.user_config_mock.start()
with MockAWS._mock_init_lock:
self._user_config_mock.start()
if mock_credentials():
self.mock_env_variables()
if not self.__class__.mocks_active:
self.default_session_mock.start()
self.__class__.mocks_active = True
self._mock_env_variables()
if not self.__class__._mocks_active:
self._default_session_mock.start()
self.__class__._mocks_active = True
self.__class__.nested_count += 1
self.__class__._nested_count += 1
if self.__class__.nested_count == 1:
self.enable_patching(reset=reset)
if self.__class__._nested_count == 1:
self._enable_patching(reset=reset)
def stop(self, remove_data: bool = True) -> None:
with MockAWS.mock_init_lock:
self.__class__.nested_count -= 1
with MockAWS._mock_init_lock:
self.__class__._nested_count -= 1
if self.__class__.nested_count < 0:
if self.__class__._nested_count < 0:
raise RuntimeError("Called stop() before start().")
if mock_credentials():
self.unmock_env_variables()
self._unmock_env_variables()
if self.__class__.nested_count == 0:
if self.__class__.mocks_active:
self.default_session_mock.stop()
self.user_config_mock.stop()
self.__class__.mocks_active = False
self.disable_patching(remove_data)
if self.__class__._nested_count == 0:
if self.__class__._mocks_active:
self._default_session_mock.stop()
self._user_config_mock.stop()
self.__class__._mocks_active = False
self._disable_patching(remove_data)
def decorate_callable(
def _decorate_callable(
self, func: "Callable[P, T]", reset: bool, remove_data: bool
) -> "Callable[P, T]":
def wrapper(*args: Any, **kwargs: Any) -> T:
@ -123,7 +124,7 @@ class MockAWS(ContextManager["MockAWS"]):
wrapper.__wrapped__ = func # type: ignore[attr-defined]
return wrapper
def decorate_class(self, klass: "Callable[P, T]") -> "Callable[P, T]":
def _decorate_class(self, klass: "Callable[P, T]") -> "Callable[P, T]":
assert inspect.isclass(klass) # Keep mypy happy
direct_methods = get_direct_methods_of(klass)
defined_classes = set(
@ -191,13 +192,13 @@ class MockAWS(ContextManager["MockAWS"]):
continue
return klass
def mock_env_variables(self) -> None:
def _mock_env_variables(self) -> None:
# "Mock" the AWS credentials as they can't be mocked in Botocore currently
for k, v in self._fake_creds.items():
self._orig_creds[k] = os.environ.get(k, None)
os.environ[k] = v
def unmock_env_variables(self) -> None:
def _unmock_env_variables(self) -> None:
for k, v in self._orig_creds.items():
if v:
os.environ[k] = v
@ -205,12 +206,10 @@ class MockAWS(ContextManager["MockAWS"]):
del os.environ[k]
def reset(self) -> None:
botocore_stubber.reset()
BackendDict.reset()
reset_responses_mock(responses_mock)
def enable_patching(
self, reset: bool = True # pylint: disable=unused-argument
) -> None:
def _enable_patching(self, reset: bool = True) -> None:
botocore_stubber.enabled = True
if reset:
self.reset()
@ -233,7 +232,7 @@ class MockAWS(ContextManager["MockAWS"]):
)
)
def disable_patching(self, remove_data: bool) -> None:
def _disable_patching(self, remove_data: bool) -> None:
botocore_stubber.enabled = False
if remove_data:
self.reset()
@ -337,24 +336,24 @@ def override_responses_real_send(user_mock: Optional[responses.RequestsMock]) ->
class ServerModeMockAWS(MockAWS):
RESET_IN_PROGRESS = False
_RESET_IN_PROGRESS = False
def __init__(self, *args: Any, **kwargs: Any):
self.test_server_mode_endpoint = settings.test_server_mode_endpoint()
self._test_server_mode_endpoint = settings.test_server_mode_endpoint()
super().__init__(*args, **kwargs)
def reset(self) -> None:
call_reset_api = os.environ.get("MOTO_CALL_RESET_API")
if not call_reset_api or call_reset_api.lower() != "false":
if not ServerModeMockAWS.RESET_IN_PROGRESS:
ServerModeMockAWS.RESET_IN_PROGRESS = True
if not ServerModeMockAWS._RESET_IN_PROGRESS:
ServerModeMockAWS._RESET_IN_PROGRESS = True
import requests
requests.post(f"{self.test_server_mode_endpoint}/moto-api/reset")
ServerModeMockAWS.RESET_IN_PROGRESS = False
requests.post(f"{self._test_server_mode_endpoint}/moto-api/reset")
ServerModeMockAWS._RESET_IN_PROGRESS = False
def enable_patching(self, reset: bool = True) -> None:
if self.__class__.nested_count == 1 and reset:
def _enable_patching(self, reset: bool = True) -> None:
if self.__class__._nested_count == 1 and reset:
# Just started
self.reset()
@ -373,12 +372,12 @@ class ServerModeMockAWS(MockAWS):
config = Config(user_agent_extra="region/" + region)
kwargs["config"] = config
if "endpoint_url" not in kwargs:
kwargs["endpoint_url"] = self.test_server_mode_endpoint
kwargs["endpoint_url"] = self._test_server_mode_endpoint
return real_boto3_client(*args, **kwargs)
def fake_boto3_resource(*args: Any, **kwargs: Any) -> Any:
if "endpoint_url" not in kwargs:
kwargs["endpoint_url"] = self.test_server_mode_endpoint
kwargs["endpoint_url"] = self._test_server_mode_endpoint
return real_boto3_resource(*args, **kwargs)
self._client_patcher = patch("boto3.client", fake_boto3_client)
@ -394,7 +393,7 @@ class ServerModeMockAWS(MockAWS):
return region
return None
def disable_patching(self, remove_data: bool) -> None:
def _disable_patching(self, remove_data: bool) -> None:
if self._client_patcher:
self._client_patcher.stop()
self._resource_patcher.stop()
@ -404,24 +403,24 @@ class ServerModeMockAWS(MockAWS):
class ProxyModeMockAWS(MockAWS):
RESET_IN_PROGRESS = False
_RESET_IN_PROGRESS = False
def __init__(self, *args: Any, **kwargs: Any):
self.test_proxy_mode_endpoint = settings.test_proxy_mode_endpoint()
self._test_proxy_mode_endpoint = settings.test_proxy_mode_endpoint()
super().__init__(*args, **kwargs)
def reset(self) -> None:
call_reset_api = os.environ.get("MOTO_CALL_RESET_API")
if not call_reset_api or call_reset_api.lower() != "false":
if not ProxyModeMockAWS.RESET_IN_PROGRESS:
ProxyModeMockAWS.RESET_IN_PROGRESS = True
if not ProxyModeMockAWS._RESET_IN_PROGRESS:
ProxyModeMockAWS._RESET_IN_PROGRESS = True
import requests
requests.post(f"{self.test_proxy_mode_endpoint}/moto-api/reset")
ProxyModeMockAWS.RESET_IN_PROGRESS = False
requests.post(f"{self._test_proxy_mode_endpoint}/moto-api/reset")
ProxyModeMockAWS._RESET_IN_PROGRESS = False
def enable_patching(self, reset: bool = True) -> None:
if self.__class__.nested_count == 1 and reset:
def _enable_patching(self, reset: bool = True) -> None:
if self.__class__._nested_count == 1 and reset:
# Just started
self.reset()
@ -460,7 +459,7 @@ class ProxyModeMockAWS(MockAWS):
self._client_patcher.start()
self._resource_patcher.start()
def disable_patching(self, remove_data: bool) -> None:
def _disable_patching(self, remove_data: bool) -> None:
if self._client_patcher:
self._client_patcher.stop()
self._resource_patcher.stop()

View File

@ -1,12 +1,15 @@
import inspect
import os
import unittest
from typing import Any
from unittest import SkipTest
from unittest import SkipTest, mock
import boto3
import pytest
from botocore.exceptions import ClientError
from moto import mock_aws, settings
from moto.core.decorator import ProxyModeMockAWS, ServerModeMockAWS
"""
Test the different ways that the decorator can be used
@ -44,15 +47,29 @@ def test_context_manager(aws_credentials: Any) -> None: # type: ignore[misc] #
assert client.describe_addresses()["Addresses"] == []
@mock.patch.dict(os.environ, {"MOTO_CALL_RESET_API": "false"})
@pytest.mark.parametrize("mock_class", [mock_aws, ServerModeMockAWS, ProxyModeMockAWS])
def test_context_decorator_exposes_bare_essentials(mock_class: Any) -> None: # type: ignore
# Verify we're only exposing the necessary methods
with mock_class() as m:
exposed_attributes = [a for a in m.__dict__.keys() if not a.startswith("_")]
assert exposed_attributes == []
# Methods + Static attributes
exposed_methods = [n for n, _ in inspect.getmembers(m) if not n.startswith("_")]
assert sorted(exposed_methods) == ["reset", "start", "stop"]
@pytest.mark.network
def test_decorator_start_and_stop() -> None:
if settings.TEST_SERVER_MODE:
raise SkipTest("Authentication always works in ServerMode")
mock = mock_aws()
mock.start()
my_mock = mock_aws()
my_mock.start()
client = boto3.client("ec2", region_name="us-west-1")
assert client.describe_addresses()["Addresses"] == []
mock.stop()
my_mock.stop()
with pytest.raises(ClientError) as exc:
client.describe_addresses()

View File

@ -1,6 +1,7 @@
import boto3
from moto import MockAWS, mock_aws
from moto import mock_aws
from moto.core.decorator import MockAWS
@mock_aws