Repository: getmoto/moto

Issue:
Deleting S3 object version leaks key
Continued from https://github.com/getmoto/moto/issues/5930

```python
python <<END
import warnings

from boto3 import client
from moto import __version__, mock_s3

warnings.simplefilter('error')

print(__version__)

with mock_s3():
    s3 = client('s3')
    s3.create_bucket(Bucket='foo')
    s3.put_bucket_versioning(Bucket='foo',
                             VersioningConfiguration={
                                 'Status': 'Enabled',
                                 'MFADelete': 'Disabled'
                             })
    version = s3.put_object(Bucket='foo', Key='bar', Body='stuff')['VersionId']
    s3.delete_object(Bucket='foo', Key='bar', VersionId=version)
END
```

produces

```
4.1.3.dev29
Exception ignored in: <function FakeKey.__del__ at 0x104297040>
Traceback (most recent call last):
  File "/Users/hannes/workspace/hca/azul/.venv/lib/python3.9/site-packages/moto/s3/models.py", line 359, in __del__
    self.dispose(garbage=True)
  File "/Users/hannes/workspace/hca/azul/.venv/lib/python3.9/site-packages/moto/s3/models.py", line 349, in dispose
    warnings.warn("S3 key was not disposed of in time", ResourceWarning)
ResourceWarning: S3 key was not disposed of in time
```

Removing `, VersionId=version` from the `delete_object` call makes it pass.




Discussion:

Resolving patch:
diff --git a/moto/s3/models.py b/moto/s3/models.py
--- a/moto/s3/models.py
+++ b/moto/s3/models.py
@@ -1827,8 +1827,6 @@ def put_object(
         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
--- a/moto/s3/utils.py
+++ b/moto/s3/utils.py
@@ -148,6 +148,12 @@ def setlist(self, key, list_):
         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):


Test patch:
diff --git a/tests/test_s3/test_s3_file_handles.py b/tests/test_s3/test_s3_file_handles.py
--- a/tests/test_s3/test_s3_file_handles.py
+++ b/tests/test_s3/test_s3_file_handles.py
@@ -251,6 +251,17 @@ def test_overwrite_file(self):
         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