MyPy improvements (#7045)

This commit is contained in:
tungol 2023-11-21 15:51:03 -08:00 committed by GitHub
parent 4127cb7b9f
commit b3c3883a78
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 142 additions and 26 deletions

View File

@ -5,6 +5,8 @@ exclude_lines =
raise NotImplemented. raise NotImplemented.
return NotImplemented return NotImplemented
def __repr__ def __repr__
if TYPE_CHECKING:
^\s*\.\.\.$
[run] [run]
include = moto/* include = moto/*

View File

@ -1,11 +1,19 @@
import importlib import importlib
import sys import sys
from contextlib import ContextDecorator from contextlib import ContextDecorator
from typing import Any, Callable, List, Optional, TypeVar
from moto.core.models import BaseMockAWS from moto.core.models import BaseMockAWS, base_decorator, BaseDecorator
from typing import Any, Callable, List, Optional, TypeVar, Union, overload
from typing import TYPE_CHECKING
TEST_METHOD = TypeVar("TEST_METHOD", bound=Callable[..., Any]) if TYPE_CHECKING:
from moto.xray import XRaySegment as xray_segment_type
from typing_extensions import ParamSpec
P = ParamSpec("P")
T = TypeVar("T")
def lazy_load( def lazy_load(
@ -13,10 +21,21 @@ def lazy_load(
element: str, element: str,
boto3_name: Optional[str] = None, boto3_name: Optional[str] = None,
backend: Optional[str] = None, backend: Optional[str] = None,
) -> Callable[..., BaseMockAWS]: ) -> BaseDecorator:
def f(*args: Any, **kwargs: Any) -> Any: @overload
def f(func: None = None) -> BaseMockAWS:
...
@overload
def f(func: "Callable[P, T]") -> "Callable[P, T]":
...
def f(
func: "Optional[Callable[P, T]]" = None,
) -> "Union[BaseMockAWS, Callable[P, T]]":
module = importlib.import_module(module_name, "moto") module = importlib.import_module(module_name, "moto")
return getattr(module, element)(*args, **kwargs) decorator: base_decorator = getattr(module, element)
return decorator(func)
setattr(f, "name", module_name.replace(".", "")) setattr(f, "name", module_name.replace(".", ""))
setattr(f, "element", element) setattr(f, "element", element)
@ -25,6 +44,18 @@ def lazy_load(
return f return f
def load_xray_segment() -> Callable[[], "xray_segment_type"]:
def f() -> "xray_segment_type":
# We can't use `lazy_load` here
# XRaySegment will always be run as a context manager
# I.e.: no function is passed directly: `with XRaySegment()`
from moto.xray import XRaySegment as xray_segment
return xray_segment()
return f
mock_acm = lazy_load(".acm", "mock_acm") mock_acm = lazy_load(".acm", "mock_acm")
mock_acmpca = lazy_load(".acmpca", "mock_acmpca", boto3_name="acm-pca") mock_acmpca = lazy_load(".acmpca", "mock_acmpca", boto3_name="acm-pca")
mock_amp = lazy_load(".amp", "mock_amp") mock_amp = lazy_load(".amp", "mock_amp")
@ -190,7 +221,7 @@ mock_timestreamwrite = lazy_load(
".timestreamwrite", "mock_timestreamwrite", boto3_name="timestream-write" ".timestreamwrite", "mock_timestreamwrite", boto3_name="timestream-write"
) )
mock_transcribe = lazy_load(".transcribe", "mock_transcribe") mock_transcribe = lazy_load(".transcribe", "mock_transcribe")
XRaySegment = lazy_load(".xray", "XRaySegment") XRaySegment = load_xray_segment()
mock_xray = lazy_load(".xray", "mock_xray") mock_xray = lazy_load(".xray", "mock_xray")
mock_xray_client = lazy_load(".xray", "mock_xray_client") mock_xray_client = lazy_load(".xray", "mock_xray_client")
mock_wafv2 = lazy_load(".wafv2", "mock_wafv2") mock_wafv2 = lazy_load(".wafv2", "mock_wafv2")

View File

@ -5,8 +5,8 @@ import os
import re import re
import unittest import unittest
from types import FunctionType from types import FunctionType
from typing import Any, Callable, Dict, Optional, Set, TypeVar, Union from typing import Any, Callable, Dict, Optional, Set, TypeVar, Union, overload
from typing import ContextManager from typing import ContextManager, TYPE_CHECKING
from unittest.mock import patch from unittest.mock import patch
import boto3 import boto3
@ -26,8 +26,16 @@ from .custom_responses_mock import (
) )
from .model_instances import reset_model_data from .model_instances import reset_model_data
if TYPE_CHECKING:
from typing_extensions import ParamSpec, Protocol
P = ParamSpec("P")
else:
Protocol = object
DEFAULT_ACCOUNT_ID = "123456789012" DEFAULT_ACCOUNT_ID = "123456789012"
CALLABLE_RETURN = TypeVar("CALLABLE_RETURN") T = TypeVar("T")
class BaseMockAWS(ContextManager["BaseMockAWS"]): class BaseMockAWS(ContextManager["BaseMockAWS"]):
@ -67,10 +75,10 @@ class BaseMockAWS(ContextManager["BaseMockAWS"]):
def __call__( def __call__(
self, self,
func: Callable[..., "BaseMockAWS"], func: "Callable[P, T]",
reset: bool = True, reset: bool = True,
remove_data: bool = True, remove_data: bool = True,
) -> Callable[..., "BaseMockAWS"]: ) -> "Callable[P, T]":
if inspect.isclass(func): if inspect.isclass(func):
return self.decorate_class(func) # type: ignore return self.decorate_class(func) # type: ignore
return self.decorate_callable(func, reset, remove_data) return self.decorate_callable(func, reset, remove_data)
@ -120,9 +128,12 @@ class BaseMockAWS(ContextManager["BaseMockAWS"]):
self.disable_patching() # type: ignore[attr-defined] self.disable_patching() # type: ignore[attr-defined]
def decorate_callable( def decorate_callable(
self, func: Callable[..., "BaseMockAWS"], reset: bool, remove_data: bool self,
) -> Callable[..., "BaseMockAWS"]: func: "Callable[P, T]",
def wrapper(*args: Any, **kwargs: Any) -> "BaseMockAWS": reset: bool,
remove_data: bool,
) -> "Callable[P, T]":
def wrapper(*args: "P.args", **kwargs: "P.kwargs") -> T:
self.start(reset=reset) self.start(reset=reset)
try: try:
result = func(*args, **kwargs) result = func(*args, **kwargs)
@ -433,7 +444,7 @@ class ServerModeMockAWS(BaseMockAWS):
def _get_region(self, *args: Any, **kwargs: Any) -> Optional[str]: def _get_region(self, *args: Any, **kwargs: Any) -> Optional[str]:
if "region_name" in kwargs: if "region_name" in kwargs:
return kwargs["region_name"] return kwargs["region_name"]
if type(args) == tuple and len(args) == 2: if type(args) is tuple and len(args) == 2:
_, region = args _, region = args
return region return region
return None return None
@ -513,13 +524,21 @@ class base_decorator:
def __init__(self, backends: BackendDict): def __init__(self, backends: BackendDict):
self.backends = backends self.backends = backends
@overload
def __call__(self, func: None = None) -> BaseMockAWS:
...
@overload
def __call__(self, func: "Callable[P, T]") -> "Callable[P, T]":
...
def __call__( def __call__(
self, func: Optional[Callable[..., Any]] = None self, func: "Optional[Callable[P, T]]" = None
) -> Union[BaseMockAWS, Callable[..., BaseMockAWS]]: ) -> "Union[BaseMockAWS, Callable[P, T]]":
if settings.test_proxy_mode(): if settings.test_proxy_mode():
mocked_backend: BaseMockAWS = ProxyModeMockAWS(self.backends) mocked_backend: BaseMockAWS = ProxyModeMockAWS(self.backends)
elif settings.TEST_SERVER_MODE: elif settings.TEST_SERVER_MODE:
mocked_backend: BaseMockAWS = ServerModeMockAWS(self.backends) # type: ignore mocked_backend = ServerModeMockAWS(self.backends)
else: else:
mocked_backend = self.mock_backend(self.backends) mocked_backend = self.mock_backend(self.backends)
@ -527,3 +546,18 @@ class base_decorator:
return mocked_backend(func) return mocked_backend(func)
else: else:
return mocked_backend return mocked_backend
class BaseDecorator(Protocol):
"""A protocol for base_decorator's signature.
This enables typing of callables with the same behavior as base_decorator.
"""
@overload
def __call__(self, func: None = None) -> BaseMockAWS:
...
@overload
def __call__(self, func: "Callable[P, T]") -> "Callable[P, T]":
...

View File

@ -1,5 +1,4 @@
from typing import Dict, Tuple, List, Any, NamedTuple, Optional from typing import Dict, Tuple, List, Any, NamedTuple, Optional, TYPE_CHECKING
from typing_extensions import Self
from moto.utilities.paginator import paginate from moto.utilities.paginator import paginate
from botocore.exceptions import ParamValidationError from botocore.exceptions import ParamValidationError
@ -13,6 +12,9 @@ from .exceptions import (
) )
import warnings import warnings
if TYPE_CHECKING:
from typing_extensions import Self
class Group(NamedTuple): class Group(NamedTuple):
GroupId: str GroupId: str
@ -31,7 +33,7 @@ class Name(NamedTuple):
HonorificSuffix: Optional[str] HonorificSuffix: Optional[str]
@classmethod @classmethod
def from_dict(cls, name_dict: Dict[str, str]) -> Optional[Self]: def from_dict(cls, name_dict: Dict[str, str]) -> "Optional[Self]":
if not name_dict: if not name_dict:
return None return None
return cls( return cls(

View File

@ -7,6 +7,12 @@ inflection
lxml lxml
mypy mypy
typing-extensions<=4.5.0; python_version < '3.8' typing-extensions<=4.5.0; python_version < '3.8'
typing-extensions; python_version >= '3.8'
packaging packaging
build build
prompt_toolkit prompt_toolkit
# typing_extensions is currently used for:
# Protocol (3.8+)
# ParamSpec (3.10+)
# Self (3.11+)

View File

@ -274,7 +274,7 @@ disable = W,C,R,E
enable = anomalous-backslash-in-string, arguments-renamed, dangerous-default-value, deprecated-module, function-redefined, import-self, redefined-builtin, redefined-outer-name, reimported, pointless-statement, super-with-arguments, unused-argument, unused-import, unused-variable, useless-else-on-loop, wildcard-import enable = anomalous-backslash-in-string, arguments-renamed, dangerous-default-value, deprecated-module, function-redefined, import-self, redefined-builtin, redefined-outer-name, reimported, pointless-statement, super-with-arguments, unused-argument, unused-import, unused-variable, useless-else-on-loop, wildcard-import
[mypy] [mypy]
files= moto, tests/test_core/test_mock_all.py, tests/test_core/test_decorator_calls.py, tests/test_core/test_responses_module.py files= moto, tests/test_core/test_mock_all.py, tests/test_core/test_decorator_calls.py, tests/test_core/test_responses_module.py, tests/test_core/test_mypy.py
show_column_numbers=True show_column_numbers=True
show_error_codes = True show_error_codes = True
disable_error_code=abstract disable_error_code=abstract

View File

@ -5,7 +5,6 @@ import unittest
from botocore.exceptions import ClientError from botocore.exceptions import ClientError
from typing import Any from typing import Any
from moto import mock_ec2, mock_kinesis, mock_s3, settings from moto import mock_ec2, mock_kinesis, mock_s3, settings
from moto.core.models import BaseMockAWS
from unittest import SkipTest from unittest import SkipTest
""" """
@ -48,7 +47,7 @@ def test_context_manager(aws_credentials: Any) -> None: # type: ignore[misc] #
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: BaseMockAWS = mock_ec2() mock = mock_ec2()
mock.start() 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"] == []

View File

@ -0,0 +1,42 @@
import boto3
from moto import mock_s3
from moto.core.models import BaseMockAWS
@mock_s3
def test_without_parentheses() -> int:
assert boto3.client("s3").list_buckets()["Buckets"] == []
return 123
@mock_s3()
def test_with_parentheses() -> int:
assert boto3.client("s3").list_buckets()["Buckets"] == []
return 456
@mock_s3
def test_no_return() -> None:
assert boto3.client("s3").list_buckets()["Buckets"] == []
def test_with_context_manager() -> None:
with mock_s3():
assert boto3.client("s3").list_buckets()["Buckets"] == []
def test_manual() -> None:
# this has the explicit type not because it's necessary but so that mypy will
# complain if it's wrong
m: BaseMockAWS = mock_s3()
m.start()
assert boto3.client("s3").list_buckets()["Buckets"] == []
m.stop()
x: int = test_with_parentheses()
assert x == 456
y: int = test_without_parentheses()
assert y == 123

View File

@ -19,7 +19,7 @@ class TestResponsesModule(TestCase):
@mock_s3 @mock_s3
@responses.activate @responses.activate
def test_moto_first(self) -> None: def test_moto_first(self) -> None: # type: ignore
""" """
Verify we can activate a user-defined `responses` on top of our Moto mocks Verify we can activate a user-defined `responses` on top of our Moto mocks
""" """