Repository: getmoto/moto
Pull request #5943 (2023-02-17 23:18:10)

Techdebt: MyPy ECR


Patch:
diff --git a/moto/ecr/exceptions.py b/moto/ecr/exceptions.py
index a28fd8f4e..07b9c3742 100644
--- a/moto/ecr/exceptions.py
+++ b/moto/ecr/exceptions.py
@@ -4,9 +4,9 @@ from moto.core.exceptions import JsonRESTError
 class LifecyclePolicyNotFoundException(JsonRESTError):
     code = 400
 
-    def __init__(self, repository_name, registry_id):
+    def __init__(self, repository_name: str, registry_id: str):
         super().__init__(
-            error_type=__class__.__name__,
+            error_type="LifecyclePolicyNotFoundException",
             message=(
                 "Lifecycle policy does not exist "
                 f"for the repository with name '{repository_name}' "
@@ -18,9 +18,9 @@ class LifecyclePolicyNotFoundException(JsonRESTError):
 class LimitExceededException(JsonRESTError):
     code = 400
 
-    def __init__(self):
+    def __init__(self) -> None:
         super().__init__(
-            error_type=__class__.__name__,
+            error_type="LimitExceededException",
             message=("The scan quota per image has been exceeded. Wait and try again."),
         )
 
@@ -28,9 +28,9 @@ class LimitExceededException(JsonRESTError):
 class RegistryPolicyNotFoundException(JsonRESTError):
     code = 400
 
-    def __init__(self, registry_id):
+    def __init__(self, registry_id: str):
         super().__init__(
-            error_type=__class__.__name__,
+            error_type="RegistryPolicyNotFoundException",
             message=(
                 f"Registry policy does not exist in the registry with id '{registry_id}'"
             ),
@@ -40,9 +40,9 @@ class RegistryPolicyNotFoundException(JsonRESTError):
 class RepositoryAlreadyExistsException(JsonRESTError):
     code = 400
 
-    def __init__(self, repository_name, registry_id):
+    def __init__(self, repository_name: str, registry_id: str):
         super().__init__(
-            error_type=__class__.__name__,
+            error_type="RepositoryAlreadyExistsException",
             message=(
                 f"The repository with name '{repository_name}' already exists "
                 f"in the registry with id '{registry_id}'"
@@ -53,9 +53,9 @@ class RepositoryAlreadyExistsException(JsonRESTError):
 class RepositoryNotEmptyException(JsonRESTError):
     code = 400
 
-    def __init__(self, repository_name, registry_id):
+    def __init__(self, repository_name: str, registry_id: str):
         super().__init__(
-            error_type=__class__.__name__,
+            error_type="RepositoryNotEmptyException",
             message=(
                 f"The repository with name '{repository_name}' "
                 f"in registry with id '{registry_id}' "
@@ -67,9 +67,9 @@ class RepositoryNotEmptyException(JsonRESTError):
 class RepositoryNotFoundException(JsonRESTError):
     code = 400
 
-    def __init__(self, repository_name, registry_id):
+    def __init__(self, repository_name: str, registry_id: str):
         super().__init__(
-            error_type=__class__.__name__,
+            error_type="RepositoryNotFoundException",
             message=(
                 f"The repository with name '{repository_name}' does not exist "
                 f"in the registry with id '{registry_id}'"
@@ -80,9 +80,9 @@ class RepositoryNotFoundException(JsonRESTError):
 class RepositoryPolicyNotFoundException(JsonRESTError):
     code = 400
 
-    def __init__(self, repository_name, registry_id):
+    def __init__(self, repository_name: str, registry_id: str):
         super().__init__(
-            error_type=__class__.__name__,
+            error_type="RepositoryPolicyNotFoundException",
             message=(
                 "Repository policy does not exist "
                 f"for the repository with name '{repository_name}' "
@@ -94,9 +94,9 @@ class RepositoryPolicyNotFoundException(JsonRESTError):
 class ImageNotFoundException(JsonRESTError):
     code = 400
 
-    def __init__(self, image_id, repository_name, registry_id):
+    def __init__(self, image_id: str, repository_name: str, registry_id: str):
         super().__init__(
-            error_type=__class__.__name__,
+            error_type="ImageNotFoundException",
             message=(
                 f"The image with imageId {image_id} does not exist "
                 f"within the repository with name '{repository_name}' "
@@ -108,16 +108,16 @@ class ImageNotFoundException(JsonRESTError):
 class InvalidParameterException(JsonRESTError):
     code = 400
 
-    def __init__(self, message):
-        super().__init__(error_type=__class__.__name__, message=message)
+    def __init__(self, message: str):
+        super().__init__(error_type="InvalidParameterException", message=message)
 
 
 class ScanNotFoundException(JsonRESTError):
     code = 400
 
-    def __init__(self, image_id, repository_name, registry_id):
+    def __init__(self, image_id: str, repository_name: str, registry_id: str):
         super().__init__(
-            error_type=__class__.__name__,
+            error_type="ScanNotFoundException",
             message=(
                 f"Image scan does not exist for the image with '{image_id}' "
                 f"in the repository with name '{repository_name}' "
@@ -129,5 +129,5 @@ class ScanNotFoundException(JsonRESTError):
 class ValidationException(JsonRESTError):
     code = 400
 
-    def __init__(self, message):
-        super().__init__(error_type=__class__.__name__, message=message)
+    def __init__(self, message: str):
+        super().__init__(error_type="ValidationException", message=message)
diff --git a/moto/ecr/models.py b/moto/ecr/models.py
index c9ddcdf05..7457e0c43 100644
--- a/moto/ecr/models.py
+++ b/moto/ecr/models.py
@@ -3,7 +3,7 @@ import json
 import re
 from collections import namedtuple
 from datetime import datetime, timezone
-from typing import Dict, List
+from typing import Any, Dict, List, Iterable, Optional
 
 from botocore.exceptions import ParamValidationError
 
@@ -36,7 +36,7 @@ EcrRepositoryArn = namedtuple(
 
 
 class BaseObject(BaseModel):
-    def camelCase(self, key):
+    def camelCase(self, key: str) -> str:
         words = []
         for i, word in enumerate(key.split("_")):
             if i > 0:
@@ -45,7 +45,7 @@ class BaseObject(BaseModel):
                 words.append(word)
         return "".join(words)
 
-    def gen_response_object(self):
+    def gen_response_object(self) -> Dict[str, Any]:
         response_object = dict()
         for key, value in self.__dict__.items():
             if "_" in key:
@@ -55,20 +55,20 @@ class BaseObject(BaseModel):
         return response_object
 
     @property
-    def response_object(self):
+    def response_object(self) -> Dict[str, Any]:  # type: ignore[misc]
         return self.gen_response_object()
 
 
 class Repository(BaseObject, CloudFormationModel):
     def __init__(
         self,
-        account_id,
-        region_name,
-        repository_name,
-        registry_id,
-        encryption_config,
-        image_scan_config,
-        image_tag_mutablility,
+        account_id: str,
+        region_name: str,
+        repository_name: str,
+        registry_id: str,
+        encryption_config: Optional[Dict[str, str]],
+        image_scan_config: str,
+        image_tag_mutablility: str,
     ):
         self.account_id = account_id
         self.region_name = region_name
@@ -86,11 +86,13 @@ class Repository(BaseObject, CloudFormationModel):
         self.encryption_configuration = self._determine_encryption_config(
             encryption_config
         )
-        self.policy = None
-        self.lifecycle_policy = None
+        self.policy: Optional[str] = None
+        self.lifecycle_policy: Optional[str] = None
         self.images: List[Image] = []
 
-    def _determine_encryption_config(self, encryption_config):
+    def _determine_encryption_config(
+        self, encryption_config: Optional[Dict[str, str]]
+    ) -> Dict[str, str]:
         if not encryption_config:
             return {"encryptionType": "AES256"}
         if encryption_config == {"encryptionType": "KMS"}:
@@ -99,7 +101,9 @@ class Repository(BaseObject, CloudFormationModel):
             ] = f"arn:aws:kms:{self.region_name}:{self.account_id}:key/{random.uuid4()}"
         return encryption_config
 
-    def _get_image(self, image_tag, image_digest):
+    def _get_image(
+        self, image_tag: Optional[str], image_digest: Optional[str]
+    ) -> "Image":
         # you can either search for one or both
         image = next(
             (
@@ -125,11 +129,11 @@ class Repository(BaseObject, CloudFormationModel):
         return image
 
     @property
-    def physical_resource_id(self):
+    def physical_resource_id(self) -> str:
         return self.name
 
     @property
-    def response_object(self):
+    def response_object(self) -> Dict[str, Any]:  # type: ignore[misc]
         response_object = self.gen_response_object()
 
         response_object["registryId"] = self.registry_id
@@ -142,21 +146,25 @@ class Repository(BaseObject, CloudFormationModel):
         del response_object["arn"], response_object["name"], response_object["images"]
         return response_object
 
-    def update(self, image_scan_config=None, image_tag_mutability=None):
+    def update(
+        self,
+        image_scan_config: Optional[Dict[str, Any]] = None,
+        image_tag_mutability: Optional[str] = None,
+    ) -> None:
         if image_scan_config:
             self.image_scanning_configuration = image_scan_config
         if image_tag_mutability:
             self.image_tag_mutability = image_tag_mutability
 
-    def delete(self, account_id, region_name):
+    def delete(self, account_id: str, region_name: str) -> None:
         ecr_backend = ecr_backends[account_id][region_name]
         ecr_backend.delete_repository(self.name)
 
     @classmethod
-    def has_cfn_attr(cls, attr):
+    def has_cfn_attr(cls, attr: str) -> bool:
         return attr in ["Arn", "RepositoryUri"]
 
-    def get_cfn_attribute(self, attribute_name):
+    def get_cfn_attribute(self, attribute_name: str) -> str:
         from moto.cloudformation.exceptions import UnformattedGetAttTemplateException
 
         if attribute_name == "Arn":
@@ -167,18 +175,23 @@ class Repository(BaseObject, CloudFormationModel):
         raise UnformattedGetAttTemplateException()
 
     @staticmethod
-    def cloudformation_name_type():
+    def cloudformation_name_type() -> str:
         return "RepositoryName"
 
     @staticmethod
-    def cloudformation_type():
+    def cloudformation_type() -> str:
         # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ecr-repository.html
         return "AWS::ECR::Repository"
 
     @classmethod
-    def create_from_cloudformation_json(
-        cls, resource_name, cloudformation_json, account_id, region_name, **kwargs
-    ):
+    def create_from_cloudformation_json(  # type: ignore[misc]
+        cls,
+        resource_name: str,
+        cloudformation_json: Any,
+        account_id: str,
+        region_name: str,
+        **kwargs: Any,
+    ) -> "Repository":
         ecr_backend = ecr_backends[account_id][region_name]
         properties = cloudformation_json["Properties"]
 
@@ -199,14 +212,14 @@ class Repository(BaseObject, CloudFormationModel):
         )
 
     @classmethod
-    def update_from_cloudformation_json(
+    def update_from_cloudformation_json(  # type: ignore[misc]
         cls,
-        original_resource,
-        new_resource_name,
-        cloudformation_json,
-        account_id,
-        region_name,
-    ):
+        original_resource: Any,
+        new_resource_name: str,
+        cloudformation_json: Any,
+        account_id: str,
+        region_name: str,
+    ) -> "Repository":
         ecr_backend = ecr_backends[account_id][region_name]
         properties = cloudformation_json["Properties"]
         encryption_configuration = properties.get(
@@ -237,13 +250,13 @@ class Repository(BaseObject, CloudFormationModel):
 class Image(BaseObject):
     def __init__(
         self,
-        account_id,
-        tag,
-        manifest,
-        repository,
-        image_manifest_mediatype=None,
-        digest=None,
-        registry_id=None,
+        account_id: str,
+        tag: str,
+        manifest: str,
+        repository: str,
+        image_manifest_mediatype: Optional[str] = None,
+        digest: Optional[str] = None,
+        registry_id: Optional[str] = None,
     ):
         self.image_tag = tag
         self.image_tags = [tag] if tag is not None else []
@@ -253,9 +266,9 @@ class Image(BaseObject):
         self.registry_id = registry_id or account_id
         self.image_digest = digest
         self.image_pushed_at = str(datetime.now(timezone.utc).isoformat())
-        self.last_scan = None
+        self.last_scan: Optional[datetime] = None
 
-    def _create_digest(self):
+    def _create_digest(self) -> None:
         image_manifest = json.loads(self.image_manifest)
         if "layers" in image_manifest:
             layer_digests = [layer["digest"] for layer in image_manifest["layers"]]
@@ -269,12 +282,12 @@ class Image(BaseObject):
             ).hexdigest()
             self.image_digest = f"sha256:{random_sha}"
 
-    def get_image_digest(self):
+    def get_image_digest(self) -> str:
         if not self.image_digest:
             self._create_digest()
-        return self.image_digest
+        return self.image_digest  # type: ignore[return-value]
 
-    def get_image_size_in_bytes(self):
+    def get_image_size_in_bytes(self) -> Optional[int]:
         image_manifest = json.loads(self.image_manifest)
         if "layers" in image_manifest:
             try:
@@ -284,22 +297,22 @@ class Image(BaseObject):
         else:
             return None
 
-    def get_image_manifest(self):
+    def get_image_manifest(self) -> str:
         return self.image_manifest
 
-    def remove_tag(self, tag):
+    def remove_tag(self, tag: str) -> None:
         if tag is not None and tag in self.image_tags:
             self.image_tags.remove(tag)
             if self.image_tags:
                 self.image_tag = self.image_tags[-1]
 
-    def update_tag(self, tag):
+    def update_tag(self, tag: str) -> None:
         self.image_tag = tag
         if tag not in self.image_tags and tag is not None:
             self.image_tags.append(tag)
 
     @property
-    def response_object(self):
+    def response_object(self) -> Dict[str, Any]:  # type: ignore[misc]
         response_object = self.gen_response_object()
         response_object["imageId"] = {}
         response_object["imageId"]["imageTag"] = self.image_tag
@@ -312,7 +325,7 @@ class Image(BaseObject):
         }
 
     @property
-    def response_list_object(self):
+    def response_list_object(self) -> Dict[str, Any]:  # type: ignore[misc]
         response_object = self.gen_response_object()
         response_object["imageTag"] = self.image_tag
         response_object["imageDigest"] = self.get_image_digest()
@@ -321,7 +334,7 @@ class Image(BaseObject):
         }
 
     @property
-    def response_describe_object(self):
+    def response_describe_object(self) -> Dict[str, Any]:  # type: ignore[misc]
         response_object = self.gen_response_object()
         response_object["imageTags"] = self.image_tags
         response_object["imageDigest"] = self.get_image_digest()
@@ -334,38 +347,40 @@ class Image(BaseObject):
         return {k: v for k, v in response_object.items() if v is not None and v != []}
 
     @property
-    def response_batch_get_image(self):
-        response_object = {}
-        response_object["imageId"] = {}
-        response_object["imageId"]["imageTag"] = self.image_tag
-        response_object["imageId"]["imageDigest"] = self.get_image_digest()
-        response_object["imageManifest"] = self.image_manifest
-        response_object["repositoryName"] = self.repository
-        response_object["registryId"] = self.registry_id
+    def response_batch_get_image(self) -> Dict[str, Any]:  # type: ignore[misc]
+        response_object = {
+            "imageId": {
+                "imageTag": self.image_tag,
+                "imageDigest": self.get_image_digest(),
+            },
+            "imageManifest": self.image_manifest,
+            "repositoryName": self.repository,
+            "registryId": self.registry_id,
+        }
         return {
-            k: v for k, v in response_object.items() if v is not None and v != [None]
+            k: v for k, v in response_object.items() if v is not None and v != [None]  # type: ignore
         }
 
     @property
-    def response_batch_delete_image(self):
+    def response_batch_delete_image(self) -> Dict[str, Any]:  # type: ignore[misc]
         response_object = {}
         response_object["imageDigest"] = self.get_image_digest()
         response_object["imageTag"] = self.image_tag
         return {
-            k: v for k, v in response_object.items() if v is not None and v != [None]
+            k: v for k, v in response_object.items() if v is not None and v != [None]  # type: ignore
         }
 
 
 class ECRBackend(BaseBackend):
-    def __init__(self, region_name, account_id):
+    def __init__(self, region_name: str, account_id: str):
         super().__init__(region_name, account_id)
-        self.registry_policy = None
-        self.replication_config = {"rules": []}
+        self.registry_policy: Optional[str] = None
+        self.replication_config: Dict[str, Any] = {"rules": []}
         self.repositories: Dict[str, Repository] = {}
         self.tagger = TaggingService(tag_name="tags")
 
     @staticmethod
-    def default_vpc_endpoint_service(service_region, zones):
+    def default_vpc_endpoint_service(service_region: str, zones: List[str]) -> List[Dict[str, Any]]:  # type: ignore[misc]
         """Default VPC endpoint service."""
         docker_endpoint = {
             "AcceptanceRequired": False,
@@ -388,7 +403,9 @@ class ECRBackend(BaseBackend):
             service_region, zones, "api.ecr", special_service_name="ecr.api"
         ) + [docker_endpoint]
 
-    def _get_repository(self, name, registry_id=None) -> Repository:
+    def _get_repository(
+        self, name: str, registry_id: Optional[str] = None
+    ) -> Repository:
         repo = self.repositories.get(name)
         reg_id = registry_id or self.account_id
 
@@ -397,7 +414,7 @@ class ECRBackend(BaseBackend):
         return repo
 
     @staticmethod
-    def _parse_resource_arn(resource_arn) -> EcrRepositoryArn:
+    def _parse_resource_arn(resource_arn: str) -> EcrRepositoryArn:  # type: ignore[misc]
         match = re.match(ECR_REPOSITORY_ARN_PATTERN, resource_arn)
         if not match:
             raise InvalidParameterException(
@@ -406,7 +423,11 @@ class ECRBackend(BaseBackend):
             )
         return EcrRepositoryArn(**match.groupdict())
 
-    def describe_repositories(self, registry_id=None, repository_names=None):
+    def describe_repositories(
+        self,
+        registry_id: Optional[str] = None,
+        repository_names: Optional[List[str]] = None,
+    ) -> List[Dict[str, Any]]:
         """
         maxResults and nextToken not implemented
         """
@@ -433,13 +454,13 @@ class ECRBackend(BaseBackend):
 
     def create_repository(
         self,
-        repository_name,
-        registry_id,
-        encryption_config,
-        image_scan_config,
-        image_tag_mutablility,
-        tags,
-    ):
+        repository_name: str,
+        registry_id: str,
+        encryption_config: Dict[str, str],
+        image_scan_config: Any,
+        image_tag_mutablility: str,
+        tags: List[Dict[str, str]],
+    ) -> Repository:
         if self.repositories.get(repository_name):
             raise RepositoryAlreadyExistsException(repository_name, self.account_id)
 
@@ -457,7 +478,12 @@ class ECRBackend(BaseBackend):
 
         return repository
 
-    def delete_repository(self, repository_name, registry_id=None, force=False):
+    def delete_repository(
+        self,
+        repository_name: str,
+        registry_id: Optional[str] = None,
+        force: bool = False,
+    ) -> Repository:
         repo = self._get_repository(repository_name, registry_id)
 
         if repo.images and not force:
@@ -468,7 +494,9 @@ class ECRBackend(BaseBackend):
         self.tagger.delete_all_tags_for_resource(repo.arn)
         return self.repositories.pop(repository_name)
 
-    def list_images(self, repository_name, registry_id=None):
+    def list_images(
+        self, repository_name: str, registry_id: Optional[str] = None
+    ) -> List[Image]:
         """
         maxResults and filtering not implemented
         """
@@ -487,16 +515,18 @@ class ECRBackend(BaseBackend):
                 repository_name, registry_id or self.account_id
             )
 
-        images = []
-        for image in repository.images:
-            images.append(image)
-        return images
+        return list(repository.images)  # type: ignore[union-attr]
 
-    def describe_images(self, repository_name, registry_id=None, image_ids=None):
+    def describe_images(
+        self,
+        repository_name: str,
+        registry_id: Optional[str] = None,
+        image_ids: Optional[List[Dict[str, str]]] = None,
+    ) -> Iterable[Image]:
         repository = self._get_repository(repository_name, registry_id)
 
         if image_ids:
-            response = set(
+            return set(
                 repository._get_image(
                     image_id.get("imageTag"), image_id.get("imageDigest")
                 )
@@ -504,15 +534,15 @@ class ECRBackend(BaseBackend):
             )
 
         else:
-            response = []
-            for image in repository.images:
-                response.append(image)
-
-        return response
+            return list(repository.images)
 
     def put_image(
-        self, repository_name, image_manifest, image_tag, image_manifest_mediatype=None
-    ):
+        self,
+        repository_name: str,
+        image_manifest: str,
+        image_tag: str,
+        image_manifest_mediatype: Optional[str] = None,
+    ) -> Image:
         if repository_name in self.repositories:
             repository = self.repositories[repository_name]
         else:
@@ -563,7 +593,12 @@ class ECRBackend(BaseBackend):
             existing_images[0].update_tag(image_tag)
             return existing_images[0]
 
-    def batch_get_image(self, repository_name, registry_id=None, image_ids=None):
+    def batch_get_image(
+        self,
+        repository_name: str,
+        registry_id: Optional[str] = None,
+        image_ids: Optional[List[Dict[str, Any]]] = None,
+    ) -> Dict[str, Any]:
         """
         The parameter AcceptedMediaTypes has not yet been implemented
         """
@@ -579,7 +614,7 @@ class ECRBackend(BaseBackend):
                 msg='Missing required parameter in input: "imageIds"'
             )
 
-        response = {"images": [], "failures": []}
+        response: Dict[str, Any] = {"images": [], "failures": []}
 
         for image_id in image_ids:
             found = False
@@ -604,7 +639,12 @@ class ECRBackend(BaseBackend):
 
         return response
 
-    def batch_delete_image(self, repository_name, registry_id=None, image_ids=None):
+    def batch_delete_image(
+        self,
+        repository_name: str,
+        registry_id: Optional[str] = None,
+        image_ids: Optional[List[Dict[str, str]]] = None,
+    ) -> Dict[str, Any]:
         if repository_name in self.repositories:
             repository = self.repositories[repository_name]
         else:
@@ -617,7 +657,7 @@ class ECRBackend(BaseBackend):
                 msg='Missing required parameter in input: "imageIds"'
             )
 
-        response = {"imageIds": [], "failures": []}
+        response: Dict[str, Any] = {"imageIds": [], "failures": []}
 
         for image_id in image_ids:
             image_found = False
@@ -636,12 +676,10 @@ class ECRBackend(BaseBackend):
             # If we have a digest, is it valid?
             if "imageDigest" in image_id:
                 pattern = re.compile(r"^[0-9a-zA-Z_+\.-]+:[0-9a-fA-F]{64}")
-                if not pattern.match(image_id.get("imageDigest")):
+                if not pattern.match(image_id["imageDigest"]):
                     response["failures"].append(
                         {
-                            "imageId": {
-                                "imageDigest": image_id.get("imageDigest", "null")
-                            },
+                            "imageId": {"imageDigest": image_id["imageDigest"]},
                             "failureCode": "InvalidImageDigest",
                             "failureReason": "Invalid request parameters: image digest should satisfy the regex '[a-zA-Z0-9-_+.]+:[a-fA-F0-9]+'",
                         }
@@ -690,7 +728,7 @@ class ECRBackend(BaseBackend):
                         repository.images.remove(image)
 
             if not image_found:
-                failure_response = {
+                failure_response: Dict[str, Any] = {
                     "imageId": {},
                     "failureCode": "ImageNotFound",
                     "failureReason": "Requested image not found",
@@ -710,29 +748,25 @@ class ECRBackend(BaseBackend):
 
         return response
 
-    def list_tags_for_resource(self, arn):
+    def list_tags_for_resource(self, arn: str) -> Dict[str, List[Dict[str, str]]]:
         resource = self._parse_resource_arn(arn)
         repo = self._get_repository(resource.repo_name, resource.account_id)
 
         return self.tagger.list_tags_for_resource(repo.arn)
 
-    def tag_resource(self, arn, tags):
+    def tag_resource(self, arn: str, tags: List[Dict[str, str]]) -> None:
         resource = self._parse_resource_arn(arn)
         repo = self._get_repository(resource.repo_name, resource.account_id)
         self.tagger.tag_resource(repo.arn, tags)
 
-        return {}
-
-    def untag_resource(self, arn, tag_keys):
+    def untag_resource(self, arn: str, tag_keys: List[str]) -> None:
         resource = self._parse_resource_arn(arn)
         repo = self._get_repository(resource.repo_name, resource.account_id)
         self.tagger.untag_resource_using_names(repo.arn, tag_keys)
 
-        return {}
-
     def put_image_tag_mutability(
-        self, registry_id, repository_name, image_tag_mutability
-    ):
+        self, registry_id: str, repository_name: str, image_tag_mutability: str
+    ) -> Dict[str, str]:
         if image_tag_mutability not in ["IMMUTABLE", "MUTABLE"]:
             raise InvalidParameterException(
                 "Invalid parameter at 'imageTagMutability' failed to satisfy constraint: "
@@ -749,8 +783,8 @@ class ECRBackend(BaseBackend):
         }
 
     def put_image_scanning_configuration(
-        self, registry_id, repository_name, image_scan_config
-    ):
+        self, registry_id: str, repository_name: str, image_scan_config: Dict[str, Any]
+    ) -> Dict[str, Any]:
         repo = self._get_repository(repository_name, registry_id)
         repo.update(image_scan_config=image_scan_config)
 
@@ -760,15 +794,17 @@ class ECRBackend(BaseBackend):
             "imageScanningConfiguration": repo.image_scanning_configuration,
         }
 
-    def set_repository_policy(self, registry_id, repository_name, policy_text):
+    def set_repository_policy(
+        self, registry_id: str, repository_name: str, policy_text: str
+    ) -> Dict[str, Any]:
         repo = self._get_repository(repository_name, registry_id)
 
         try:
             iam_policy_document_validator = IAMPolicyDocumentValidator(policy_text)
             # the repository policy can be defined without a resource field
-            iam_policy_document_validator._validate_resource_exist = lambda: None
+            iam_policy_document_validator._validate_resource_exist = lambda: None  # type: ignore
             # the repository policy can have the old version 2008-10-17
-            iam_policy_document_validator._validate_version = lambda: None
+            iam_policy_document_validator._validate_version = lambda: None  # type: ignore
             iam_policy_document_validator.validate()
         except MalformedPolicyDocument:
             raise InvalidParameterException(
@@ -784,7 +820,9 @@ class ECRBackend(BaseBackend):
             "policyText": repo.policy,
         }
 
-    def get_repository_policy(self, registry_id, repository_name):
+    def get_repository_policy(
+        self, registry_id: str, repository_name: str
+    ) -> Dict[str, Any]:
         repo = self._get_repository(repository_name, registry_id)
 
         if not repo.policy:
@@ -796,7 +834,9 @@ class ECRBackend(BaseBackend):
             "policyText": repo.policy,
         }
 
-    def delete_repository_policy(self, registry_id, repository_name):
+    def delete_repository_policy(
+        self, registry_id: str, repository_name: str
+    ) -> Dict[str, Any]:
         repo = self._get_repository(repository_name, registry_id)
         policy = repo.policy
 
@@ -811,7 +851,9 @@ class ECRBackend(BaseBackend):
             "policyText": policy,
         }
 
-    def put_lifecycle_policy(self, registry_id, repository_name, lifecycle_policy_text):
+    def put_lifecycle_policy(
+        self, registry_id: str, repository_name: str, lifecycle_policy_text: str
+    ) -> Dict[str, Any]:
         repo = self._get_repository(repository_name, registry_id)
 
         validator = EcrLifecyclePolicyValidator(lifecycle_policy_text)
@@ -825,7 +867,9 @@ class ECRBackend(BaseBackend):
             "lifecyclePolicyText": repo.lifecycle_policy,
         }
 
-    def get_lifecycle_policy(self, registry_id, repository_name):
+    def get_lifecycle_policy(
+        self, registry_id: str, repository_name: str
+    ) -> Dict[str, Any]:
         repo = self._get_repository(repository_name, registry_id)
 
         if not repo.lifecycle_policy:
@@ -840,7 +884,9 @@ class ECRBackend(BaseBackend):
             ),
         }
 
-    def delete_lifecycle_policy(self, registry_id, repository_name):
+    def delete_lifecycle_policy(
+        self, registry_id: str, repository_name: str
+    ) -> Dict[str, Any]:
         repo = self._get_repository(repository_name, registry_id)
         policy = repo.lifecycle_policy
 
@@ -858,7 +904,7 @@ class ECRBackend(BaseBackend):
             ),
         }
 
-    def _validate_registry_policy_action(self, policy_text):
+    def _validate_registry_policy_action(self, policy_text: str) -> None:
         # only CreateRepository & ReplicateImage actions are allowed
         VALID_ACTIONS = {"ecr:CreateRepository", "ecr:ReplicateImage"}
 
@@ -870,7 +916,7 @@ class ECRBackend(BaseBackend):
             if set(action) - VALID_ACTIONS:
                 raise MalformedPolicyDocument()
 
-    def put_registry_policy(self, policy_text):
+    def put_registry_policy(self, policy_text: str) -> Dict[str, Any]:
         try:
             iam_policy_document_validator = IAMPolicyDocumentValidator(policy_text)
             iam_policy_document_validator.validate()
@@ -889,7 +935,7 @@ class ECRBackend(BaseBackend):
             "policyText": policy_text,
         }
 
-    def get_registry_policy(self):
+    def get_registry_policy(self) -> Dict[str, Any]:
         if not self.registry_policy:
             raise RegistryPolicyNotFoundException(self.account_id)
 
@@ -898,7 +944,7 @@ class ECRBackend(BaseBackend):
             "policyText": self.registry_policy,
         }
 
-    def delete_registry_policy(self):
+    def delete_registry_policy(self) -> Dict[str, Any]:
         policy = self.registry_policy
         if not policy:
             raise RegistryPolicyNotFoundException(self.account_id)
@@ -910,7 +956,9 @@ class ECRBackend(BaseBackend):
             "policyText": policy,
         }
 
-    def start_image_scan(self, registry_id, repository_name, image_id):
+    def start_image_scan(
+        self, registry_id: str, repository_name: str, image_id: Dict[str, str]
+    ) -> Dict[str, Any]:
         repo = self._get_repository(repository_name, registry_id)
 
         image = repo._get_image(image_id.get("imageTag"), image_id.get("imageDigest"))
@@ -931,7 +979,9 @@ class ECRBackend(BaseBackend):
             "imageScanStatus": {"status": "IN_PROGRESS"},
         }
 
-    def describe_image_scan_findings(self, registry_id, repository_name, image_id):
+    def describe_image_scan_findings(
+        self, registry_id: str, repository_name: str, image_id: Dict[str, Any]
+    ) -> Dict[str, Any]:
         repo = self._get_repository(repository_name, registry_id)
 
         image = repo._get_image(image_id.get("imageTag"), image_id.get("imageDigest"))
@@ -984,7 +1034,9 @@ class ECRBackend(BaseBackend):
             },
         }
 
-    def put_replication_configuration(self, replication_config):
+    def put_replication_configuration(
+        self, replication_config: Dict[str, Any]
+    ) -> Dict[str, Any]:
         rules = replication_config["rules"]
         if len(rules) > 1:
             raise ValidationException("This feature is disabled")
@@ -1004,7 +1056,7 @@ class ECRBackend(BaseBackend):
 
         return {"replicationConfiguration": replication_config}
 
-    def describe_registry(self):
+    def describe_registry(self) -> Dict[str, Any]:
         return {
             "registryId": self.account_id,
             "replicationConfiguration": self.replication_config,
diff --git a/moto/ecr/policy_validation.py b/moto/ecr/policy_validation.py
index 18277a615..286599de6 100644
--- a/moto/ecr/policy_validation.py
+++ b/moto/ecr/policy_validation.py
@@ -1,4 +1,5 @@
 import json
+from typing import Any, Dict, List
 
 from moto.ecr.exceptions import InvalidParameterException
 
@@ -28,12 +29,12 @@ class EcrLifecyclePolicyValidator:
         "'Lifecycle policy validation failure: "
     )
 
-    def __init__(self, policy_text):
+    def __init__(self, policy_text: str):
         self._policy_text = policy_text
-        self._policy_json = {}
-        self._rules = []
+        self._policy_json: Dict[str, Any] = {}
+        self._rules: List[Any] = []
 
-    def validate(self):
+    def validate(self) -> None:
         try:
             self._parse_policy()
         except Exception:
@@ -61,17 +62,17 @@ class EcrLifecyclePolicyValidator:
         self._validate_rule_type()
         self._validate_rule_top_properties()
 
-    def _parse_policy(self):
+    def _parse_policy(self) -> None:
         self._policy_json = json.loads(self._policy_text)
         assert isinstance(self._policy_json, dict)
 
-    def _extract_rules(self):
+    def _extract_rules(self) -> None:
         assert "rules" in self._policy_json
         assert isinstance(self._policy_json["rules"], list)
 
         self._rules = self._policy_json["rules"]
 
-    def _validate_rule_type(self):
+    def _validate_rule_type(self) -> None:
         for rule in self._rules:
             if not isinstance(rule, dict):
                 raise InvalidParameterException(
@@ -83,7 +84,7 @@ class EcrLifecyclePolicyValidator:
                     )
                 )
 
-    def _validate_rule_top_properties(self):
+    def _validate_rule_top_properties(self) -> None:
         for rule in self._rules:
             rule_properties = set(rule.keys())
             missing_properties = REQUIRED_RULE_PROPERTIES - rule_properties
@@ -111,7 +112,7 @@ class EcrLifecyclePolicyValidator:
             self._validate_action(rule["action"])
             self._validate_selection(rule["selection"])
 
-    def _validate_action(self, action):
+    def _validate_action(self, action: Any) -> None:
         given_properties = set(action.keys())
         missing_properties = REQUIRED_ACTION_PROPERTIES - given_properties
 
@@ -139,7 +140,7 @@ class EcrLifecyclePolicyValidator:
 
             self._validate_action_type(action["type"])
 
-    def _validate_action_type(self, action_type):
+    def _validate_action_type(self, action_type: str) -> None:
         if action_type not in VALID_ACTION_TYPE_VALUES:
             raise InvalidParameterException(
                 "".join(
@@ -151,7 +152,7 @@ class EcrLifecyclePolicyValidator:
                 )
             )
 
-    def _validate_selection(self, selection):
+    def _validate_selection(self, selection: Any) -> None:
         given_properties = set(selection.keys())
         missing_properties = REQUIRED_SELECTION_PROPERTIES - given_properties
 
@@ -182,7 +183,7 @@ class EcrLifecyclePolicyValidator:
             self._validate_selection_count_unit(selection.get("countUnit"))
             self._validate_selection_count_number(selection["countNumber"])
 
-    def _validate_selection_tag_status(self, tag_status):
+    def _validate_selection_tag_status(self, tag_status: Any) -> None:
         if tag_status not in VALID_SELECTION_TAG_STATUS_VALUES:
             raise InvalidParameterException(
                 "".join(
@@ -194,7 +195,7 @@ class EcrLifecyclePolicyValidator:
                 )
             )
 
-    def _validate_selection_count_type(self, count_type):
+    def _validate_selection_count_type(self, count_type: Any) -> None:
         if count_type not in VALID_SELECTION_COUNT_TYPE_VALUES:
             raise InvalidParameterException(
                 "".join(
@@ -205,7 +206,7 @@ class EcrLifecyclePolicyValidator:
                 )
             )
 
-    def _validate_selection_count_unit(self, count_unit):
+    def _validate_selection_count_unit(self, count_unit: Any) -> None:
         if not count_unit:
             return None
 
@@ -220,7 +221,7 @@ class EcrLifecyclePolicyValidator:
                 )
             )
 
-    def _validate_selection_count_number(self, count_number):
+    def _validate_selection_count_number(self, count_number: int) -> None:
         if count_number < 1:
             raise InvalidParameterException(
                 "".join(
diff --git a/moto/ecr/responses.py b/moto/ecr/responses.py
index 717483491..fb2884100 100644
--- a/moto/ecr/responses.py
+++ b/moto/ecr/responses.py
@@ -4,28 +4,18 @@ from datetime import datetime
 import time
 
 from moto.core.responses import BaseResponse
-from .models import ecr_backends
+from .models import ecr_backends, ECRBackend
 
 
 class ECRResponse(BaseResponse):
-    def __init__(self):
+    def __init__(self) -> None:
         super().__init__(service_name="ecr")
 
     @property
-    def ecr_backend(self):
+    def ecr_backend(self) -> ECRBackend:
         return ecr_backends[self.current_account][self.region]
 
-    @property
-    def request_params(self):
-        try:
-            return json.loads(self.body)
-        except ValueError:
-            return {}
-
-    def _get_param(self, param_name, if_none=None):
-        return self.request_params.get(param_name, if_none)
-
-    def create_repository(self):
+    def create_repository(self) -> str:
         repository_name = self._get_param("repositoryName")
         registry_id = self._get_param("registryId")
         encryption_config = self._get_param("encryptionConfiguration")
@@ -43,7 +33,7 @@ class ECRResponse(BaseResponse):
         )
         return json.dumps({"repository": repository.response_object})
 
-    def describe_repositories(self):
+    def describe_repositories(self) -> str:
         describe_repositories_name = self._get_param("repositoryNames")
         registry_id = self._get_param("registryId")
 
@@ -52,7 +42,7 @@ class ECRResponse(BaseResponse):
         )
         return json.dumps({"repositories": repositories, "failures": []})
 
-    def delete_repository(self):
+    def delete_repository(self) -> str:
         repository_str = self._get_param("repositoryName")
         registry_id = self._get_param("registryId")
         force = self._get_param("force")
@@ -62,7 +52,7 @@ class ECRResponse(BaseResponse):
         )
         return json.dumps({"repository": repository.response_object})
 
-    def put_image(self):
+    def put_image(self) -> str:
         repository_str = self._get_param("repositoryName")
         image_manifest = self._get_param("imageManifest")
         image_tag = self._get_param("imageTag")
@@ -70,7 +60,7 @@ class ECRResponse(BaseResponse):
 
         return json.dumps({"image": image.response_object})
 
-    def list_images(self):
+    def list_images(self) -> str:
         repository_str = self._get_param("repositoryName")
         registry_id = self._get_param("registryId")
         images = self.ecr_backend.list_images(repository_str, registry_id)
@@ -78,7 +68,7 @@ class ECRResponse(BaseResponse):
             {"imageIds": [image.response_list_object for image in images]}
         )
 
-    def describe_images(self):
+    def describe_images(self) -> str:
         repository_str = self._get_param("repositoryName")
         registry_id = self._get_param("registryId")
         image_ids = self._get_param("imageIds")
@@ -89,13 +79,13 @@ class ECRResponse(BaseResponse):
             {"imageDetails": [image.response_describe_object for image in images]}
         )
 
-    def batch_check_layer_availability(self):
+    def batch_check_layer_availability(self) -> None:
         self.error_on_dryrun()
         raise NotImplementedError(
             "ECR.batch_check_layer_availability is not yet implemented"
         )
 
-    def batch_delete_image(self):
+    def batch_delete_image(self) -> str:
         repository_str = self._get_param("repositoryName")
         registry_id = self._get_param("registryId")
         image_ids = self._get_param("imageIds")
@@ -105,7 +95,7 @@ class ECRResponse(BaseResponse):
         )
         return json.dumps(response)
 
-    def batch_get_image(self):
+    def batch_get_image(self) -> str:
         repository_str = self._get_param("repositoryName")
         registry_id = self._get_param("registryId")
         image_ids = self._get_param("imageIds")
@@ -115,11 +105,11 @@ class ECRResponse(BaseResponse):
         )
         return json.dumps(response)
 
-    def complete_layer_upload(self):
+    def complete_layer_upload(self) -> None:
         self.error_on_dryrun()
         raise NotImplementedError("ECR.complete_layer_upload is not yet implemented")
 
-    def delete_repository_policy(self):
+    def delete_repository_policy(self) -> str:
         registry_id = self._get_param("registryId")
         repository_name = self._get_param("repositoryName")
 
@@ -129,7 +119,7 @@ class ECRResponse(BaseResponse):
             )
         )
 
-    def get_authorization_token(self):
+    def get_authorization_token(self) -> str:
         registry_ids = self._get_param("registryIds")
         if not registry_ids:
             registry_ids = [self.current_account]
@@ -146,13 +136,13 @@ class ECRResponse(BaseResponse):
             )
         return json.dumps({"authorizationData": auth_data})
 
-    def get_download_url_for_layer(self):
+    def get_download_url_for_layer(self) -> None:
         self.error_on_dryrun()
         raise NotImplementedError(
             "ECR.get_download_url_for_layer is not yet implemented"
         )
 
-    def get_repository_policy(self):
+    def get_repository_policy(self) -> str:
         registry_id = self._get_param("registryId")
         repository_name = self._get_param("repositoryName")
 
@@ -162,11 +152,11 @@ class ECRResponse(BaseResponse):
             )
         )
 
-    def initiate_layer_upload(self):
+    def initiate_layer_upload(self) -> None:
         self.error_on_dryrun()
         raise NotImplementedError("ECR.initiate_layer_upload is not yet implemented")
 
-    def set_repository_policy(self):
+    def set_repository_policy(self) -> str:
         registry_id = self._get_param("registryId")
         repository_name = self._get_param("repositoryName")
         policy_text = self._get_param("policyText")
@@ -182,28 +172,30 @@ class ECRResponse(BaseResponse):
             )
         )
 
-    def upload_layer_part(self):
+    def upload_layer_part(self) -> None:
         self.error_on_dryrun()
         raise NotImplementedError("ECR.upload_layer_part is not yet implemented")
 
-    def list_tags_for_resource(self):
+    def list_tags_for_resource(self) -> str:
         arn = self._get_param("resourceArn")
 
         return json.dumps(self.ecr_backend.list_tags_for_resource(arn))
 
-    def tag_resource(self):
+    def tag_resource(self) -> str:
         arn = self._get_param("resourceArn")
         tags = self._get_param("tags", [])
 
-        return json.dumps(self.ecr_backend.tag_resource(arn, tags))
+        self.ecr_backend.tag_resource(arn, tags)
+        return "{}"
 
-    def untag_resource(self):
+    def untag_resource(self) -> str:
         arn = self._get_param("resourceArn")
         tag_keys = self._get_param("tagKeys", [])
 
-        return json.dumps(self.ecr_backend.untag_resource(arn, tag_keys))
+        self.ecr_backend.untag_resource(arn, tag_keys)
+        return "{}"
 
-    def put_image_tag_mutability(self):
+    def put_image_tag_mutability(self) -> str:
         registry_id = self._get_param("registryId")
         repository_name = self._get_param("repositoryName")
         image_tag_mutability = self._get_param("imageTagMutability")
@@ -216,7 +208,7 @@ class ECRResponse(BaseResponse):
             )
         )
 
-    def put_image_scanning_configuration(self):
+    def put_image_scanning_configuration(self) -> str:
         registry_id = self._get_param("registryId")
         repository_name = self._get_param("repositoryName")
         image_scan_config = self._get_param("imageScanningConfiguration")
@@ -229,7 +221,7 @@ class ECRResponse(BaseResponse):
             )
         )
 
-    def put_lifecycle_policy(self):
+    def put_lifecycle_policy(self) -> str:
         registry_id = self._get_param("registryId")
         repository_name = self._get_param("repositoryName")
         lifecycle_policy_text = self._get_param("lifecyclePolicyText")
@@ -242,7 +234,7 @@ class ECRResponse(BaseResponse):
             )
         )
 
-    def get_lifecycle_policy(self):
+    def get_lifecycle_policy(self) -> str:
         registry_id = self._get_param("registryId")
         repository_name = self._get_param("repositoryName")
 
@@ -252,7 +244,7 @@ class ECRResponse(BaseResponse):
             )
         )
 
-    def delete_lifecycle_policy(self):
+    def delete_lifecycle_policy(self) -> str:
         registry_id = self._get_param("registryId")
         repository_name = self._get_param("repositoryName")
 
@@ -262,18 +254,18 @@ class ECRResponse(BaseResponse):
             )
         )
 
-    def put_registry_policy(self):
+    def put_registry_policy(self) -> str:
         policy_text = self._get_param("policyText")
 
         return json.dumps(self.ecr_backend.put_registry_policy(policy_text=policy_text))
 
-    def get_registry_policy(self):
+    def get_registry_policy(self) -> str:
         return json.dumps(self.ecr_backend.get_registry_policy())
 
-    def delete_registry_policy(self):
+    def delete_registry_policy(self) -> str:
         return json.dumps(self.ecr_backend.delete_registry_policy())
 
-    def start_image_scan(self):
+    def start_image_scan(self) -> str:
         registry_id = self._get_param("registryId")
         repository_name = self._get_param("repositoryName")
         image_id = self._get_param("imageId")
@@ -286,7 +278,7 @@ class ECRResponse(BaseResponse):
             )
         )
 
-    def describe_image_scan_findings(self):
+    def describe_image_scan_findings(self) -> str:
         registry_id = self._get_param("registryId")
         repository_name = self._get_param("repositoryName")
         image_id = self._get_param("imageId")
@@ -299,7 +291,7 @@ class ECRResponse(BaseResponse):
             )
         )
 
-    def put_replication_configuration(self):
+    def put_replication_configuration(self) -> str:
         replication_config = self._get_param("replicationConfiguration")
 
         return json.dumps(
@@ -308,5 +300,5 @@ class ECRResponse(BaseResponse):
             )
         )
 
-    def describe_registry(self):
+    def describe_registry(self) -> str:
         return json.dumps(self.ecr_backend.describe_registry())
diff --git a/moto/iam/exceptions.py b/moto/iam/exceptions.py
index 742034d49..545a9a008 100644
--- a/moto/iam/exceptions.py
+++ b/moto/iam/exceptions.py
@@ -43,7 +43,7 @@ class MalformedCertificate(RESTError):
 class MalformedPolicyDocument(RESTError):
     code = 400
 
-    def __init__(self, message=""):
+    def __init__(self, message: str = ""):
         super().__init__(
             "MalformedPolicyDocument",
             message,
diff --git a/moto/iam/models.py b/moto/iam/models.py
index 602d5ae8b..31554d683 100644
--- a/moto/iam/models.py
+++ b/moto/iam/models.py
@@ -1181,6 +1181,13 @@ class User(CloudFormationModel):
     def created_iso_8601(self):
         return iso_8601_datetime_with_milliseconds(self.create_date)
 
+    @property
+    def password_last_used_iso_8601(self):
+        if self.password_last_used is not None:
+            return iso_8601_datetime_with_milliseconds(self.password_last_used)
+        else:
+            return None
+
     def get_policy(self, policy_name):
         policy_json = None
         try:
diff --git a/moto/iam/policy_validation.py b/moto/iam/policy_validation.py
index 0b51c1011..90e09c863 100644
--- a/moto/iam/policy_validation.py
+++ b/moto/iam/policy_validation.py
@@ -513,10 +513,10 @@ class BaseIAMPolicyValidator:
 
 
 class IAMPolicyDocumentValidator(BaseIAMPolicyValidator):
-    def __init__(self, policy_document):
+    def __init__(self, policy_document: str):
         super().__init__(policy_document)
 
-    def validate(self):
+    def validate(self) -> None:
         super().validate()
         try:
             self._validate_resource_exist()
diff --git a/moto/iam/responses.py b/moto/iam/responses.py
index 06f2fa1e7..3a75e0367 100644
--- a/moto/iam/responses.py
+++ b/moto/iam/responses.py
@@ -1805,6 +1805,9 @@ USER_TEMPLATE = """<{{ action }}UserResponse>
          <UserId>{{ user.id }}</UserId>
          <CreateDate>{{ user.created_iso_8601 }}</CreateDate>
          <Arn>{{ user.arn }}</Arn>
+         {% if user.password_last_used_iso_8601 %}
+         <PasswordLastUsed>{{ user.password_last_used_iso_8601 }}</PasswordLastUsed>
+         {% endif %}
          {% if tags %}
          <Tags>
             {% for tag in tags %}
diff --git a/moto/s3/models.py b/moto/s3/models.py
index 237975724..409b24174 100644
--- a/moto/s3/models.py
+++ b/moto/s3/models.py
@@ -1827,8 +1827,6 @@ class S3Backend(BaseBackend, CloudWatchMetricProvider):
         if bucket.is_versioned:
             keys = existing_keys + [new_key]
         else:
-            for key in existing_keys:
-                key.dispose()
             keys = [new_key]
         bucket.keys.setlist(key_name, keys)
 
diff --git a/moto/s3/utils.py b/moto/s3/utils.py
index 11c53d59e..d6bd84fb5 100644
--- a/moto/s3/utils.py
+++ b/moto/s3/utils.py
@@ -148,6 +148,12 @@ class _VersionedKeyStore(dict):
         elif not isinstance(list_, list):
             list_ = [list_]
 
+        for existing_version in self.getlist(key, []):
+            # Dispose of any FakeKeys that we will not keep
+            # We should only have FakeKeys here - but we're checking hasattr to be sure
+            if existing_version not in list_ and hasattr(existing_version, "dispose"):
+                existing_version.dispose()
+
         super().__setitem__(key, list_)
 
     def _iteritems(self):
diff --git a/setup.cfg b/setup.cfg
index 4b0e2af7d..dc38dd9e4 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -229,7 +229,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
 
 [mypy]
-files= moto/a*,moto/b*,moto/c*,moto/d*,moto/ebs/,moto/ec2,moto/ec2instanceconnect,moto/es,moto/moto_api
+files= moto/a*,moto/b*,moto/c*,moto/d*,moto/ebs/,moto/ec2,moto/ec2instanceconnect,moto/ecr,moto/es,moto/moto_api
 show_column_numbers=True
 show_error_codes = True
 disable_error_code=abstract


Test patch:
diff --git a/tests/test_iam/test_iam_password_last_used.py b/tests/test_iam/test_iam_password_last_used.py
new file mode 100644
index 000000000..045cc7f27
--- /dev/null
+++ b/tests/test_iam/test_iam_password_last_used.py
@@ -0,0 +1,37 @@
+import boto3
+from datetime import datetime
+from moto import mock_iam, settings
+from moto.backends import get_backend
+from moto.core import DEFAULT_ACCOUNT_ID as ACCOUNT_ID
+from unittest import SkipTest
+
+
+@mock_iam
+def test_password_last_used():
+    if settings.TEST_SERVER_MODE:
+        raise SkipTest("Can't set password_last_used in ServerMode")
+    client = boto3.client("iam", "us-east-1")
+    username = "test.user"
+    client.create_user(Path="/staff/", UserName=username)["User"]
+    client.create_login_profile(
+        UserName=username, Password="Password1", PasswordResetRequired=False
+    )
+
+    access_key = client.create_access_key(UserName=username)["AccessKey"]
+
+    as_new_user = boto3.resource(
+        "iam",
+        region_name="us-east-1",
+        aws_access_key_id=access_key["AccessKeyId"],
+        aws_secret_access_key=access_key["SecretAccessKey"],
+    )
+
+    # Username is set, but password not yet
+    assert as_new_user.CurrentUser().user_name == username
+    assert not as_new_user.CurrentUser().password_last_used
+
+    iam_backend = get_backend("iam")[ACCOUNT_ID]["global"]
+    iam_backend.users[username].password_last_used = datetime.utcnow()
+
+    # Password is returned now
+    assert as_new_user.CurrentUser().password_last_used
diff --git a/tests/test_s3/test_s3_file_handles.py b/tests/test_s3/test_s3_file_handles.py
index b98fb2cef..cb4366500 100644
--- a/tests/test_s3/test_s3_file_handles.py
+++ b/tests/test_s3/test_s3_file_handles.py
@@ -251,6 +251,17 @@ class TestS3FileHandleClosuresUsingMocks(TestCase):
         with mock_s3():
             pass
 
+    @verify_zero_warnings
+    def test_delete_object_with_version(self):
+        with mock_s3():
+            self.s3.create_bucket(Bucket="foo")
+            self.s3.put_bucket_versioning(
+                Bucket="foo",
+                VersioningConfiguration={"Status": "Enabled", "MFADelete": "Disabled"},
+            )
+            version = self.s3.put_object(Bucket="foo", Key="b", Body="s")["VersionId"]
+            self.s3.delete_object(Bucket="foo", Key="b", VersionId=version)
+
 
 def test_verify_key_can_be_copied_after_disposing():
     # https://github.com/getmoto/moto/issues/5588