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 * 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: 5.0.0alpha1:
------------ ------------

View File

@ -1,41 +1,4 @@
from typing import TYPE_CHECKING, Callable, Optional, TypeVar, Union, overload from moto.core.decorator import mock_aws # noqa # pylint: disable=unused-import
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)
__title__ = "moto" __title__ = "moto"
__version__ = "4.2.15.dev" __version__ = "4.2.15.dev"

View File

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

View File

@ -1,12 +1,15 @@
import inspect
import os
import unittest import unittest
from typing import Any from typing import Any
from unittest import SkipTest from unittest import SkipTest, mock
import boto3 import boto3
import pytest import pytest
from botocore.exceptions import ClientError from botocore.exceptions import ClientError
from moto import mock_aws, settings from moto import mock_aws, settings
from moto.core.decorator import ProxyModeMockAWS, ServerModeMockAWS
""" """
Test the different ways that the decorator can be used 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"] == [] 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 @pytest.mark.network
def test_decorator_start_and_stop() -> None: def test_decorator_start_and_stop() -> None:
if settings.TEST_SERVER_MODE: if settings.TEST_SERVER_MODE:
raise SkipTest("Authentication always works in ServerMode") raise SkipTest("Authentication always works in ServerMode")
mock = mock_aws() my_mock = mock_aws()
mock.start() my_mock.start()
client = boto3.client("ec2", region_name="us-west-1") client = boto3.client("ec2", region_name="us-west-1")
assert client.describe_addresses()["Addresses"] == [] assert client.describe_addresses()["Addresses"] == []
mock.stop() my_mock.stop()
with pytest.raises(ClientError) as exc: with pytest.raises(ClientError) as exc:
client.describe_addresses() client.describe_addresses()

View File

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