Repository: aio-libs/multidict

Issue:
potential pypy mutability bug
### Describe the bug

when using `pypy` and mutating a `CIMultiDict`, I get a `RuntimeError` of `"Dictionary changed during iteration"`. This doesn't happen using regular python, and I don't see this error when using `pypy` and other kinds of dictionaries.

### To Reproduce

I've added the following tests to repro with `pypy`

```python
@pytest.mark.asyncio
async def test_headers_response_items_multidict_compare():
    h = CIMultiDict({'a': '123, 456', 'b': '789'})
    before_mutation_items = h.items()
    h['c'] = '000'
    with pytest.raises(RuntimeError):
        list(before_mutation_items)

def test_headers_response_items_requests_compare():
    from requests.structures import CaseInsensitiveDict
    h = CaseInsensitiveDict({'a': '123, 456', 'b': '789'})
    before_mutation_items = h.items()
    h['c'] = '000'
    list(before_mutation_items)

def test_headers_response_items_python_compare():
    h = {'a': '123, 456', 'b': '789'}
    before_mutation_items = h.items()
    h['c'] = '000'
    list(before_mutation_items)
```

### Expected behavior

My thinking is that if mutating the dictionary doesn't raise for regular python is that it shouldn't raise for `pypy`. Is this expected behavior?

### Logs/tracebacks

```python-traceback
self = _ItemsView('a': '123, 456', 'b': '789', 'c': '000')

    def _iter(self):
        for i, k, v in self._impl._items:
            if self._version != self._impl._version:
>               raise RuntimeError("Dictionary changed during iteration")
E               RuntimeError: Dictionary changed during iteration

envs/corepypy3venv/site-packages/multidict/_multidict_py.py:459: RuntimeError
```


### Python Version

```console
$ python --version
Python 3.7.10 (51efa818fd9b24f625078c65e8e2f6a5ac24d572, Apr 08 2021, 17:43:00)
[PyPy 7.3.4 with GCC Apple LLVM 12.0.0 (clang-1200.0.32.29)]
```


### aiohttp Version

```console
$ python -m pip show aiohttp
Version: 3.7.4.post0
```


### multidict Version

```console
$ python -m pip show multidict
Version: 5.1.0
```


### yarl Version

```console
$ python -m pip show yarl
Version: 1.6.3
```


### OS

macOS






### Related component

Client

### Additional context

_No response_

### Code of Conduct

- [X] I agree to follow the aio-libs Code of Conduct

Comments:

Patch:
diff --git a/CHANGES/620.bugfix b/CHANGES/620.bugfix
new file mode 100644
index 0000000..75ca32b
--- /dev/null
+++ b/CHANGES/620.bugfix
@@ -0,0 +1,1 @@
+Fix pure-python implementation that used to raise "Dictionary changed during iteration" error when iterated view (``.keys()``, ``.values()`` or ``.items()``) was created before the dictionary's content change.
\ No newline at end of file
diff --git a/multidict/_multidict_py.py b/multidict/_multidict_py.py
index 1ec63da..52002c0 100644
--- a/multidict/_multidict_py.py
+++ b/multidict/_multidict_py.py
@@ -435,7 +435,6 @@ class _Iter:
 class _ViewBase:
     def __init__(self, impl):
         self._impl = impl
-        self._version = impl._version
 
     def __len__(self):
         return len(self._impl._items)
@@ -451,11 +450,11 @@ class _ItemsView(_ViewBase, abc.ItemsView):
         return False
 
     def __iter__(self):
-        return _Iter(len(self), self._iter())
+        return _Iter(len(self), self._iter(self._impl._version))
 
-    def _iter(self):
+    def _iter(self, version):
         for i, k, v in self._impl._items:
-            if self._version != self._impl._version:
+            if version != self._impl._version:
                 raise RuntimeError("Dictionary changed during iteration")
             yield k, v
 
@@ -475,11 +474,11 @@ class _ValuesView(_ViewBase, abc.ValuesView):
         return False
 
     def __iter__(self):
-        return _Iter(len(self), self._iter())
+        return _Iter(len(self), self._iter(self._impl._version))
 
-    def _iter(self):
+    def _iter(self, version):
         for item in self._impl._items:
-            if self._version != self._impl._version:
+            if version != self._impl._version:
                 raise RuntimeError("Dictionary changed during iteration")
             yield item[2]
 
@@ -499,11 +498,11 @@ class _KeysView(_ViewBase, abc.KeysView):
         return False
 
     def __iter__(self):
-        return _Iter(len(self), self._iter())
+        return _Iter(len(self), self._iter(self._impl._version))
 
-    def _iter(self):
+    def _iter(self, version):
         for item in self._impl._items:
-            if self._version != self._impl._version:
+            if version != self._impl._version:
                 raise RuntimeError("Dictionary changed during iteration")
             yield item[1]
 


Test patch:
diff --git a/tests/test_mutable_multidict.py b/tests/test_mutable_multidict.py
index 55664c5..3d4d16a 100644
--- a/tests/test_mutable_multidict.py
+++ b/tests/test_mutable_multidict.py
@@ -484,3 +484,27 @@ class TestCIMutableMultiDict:
     def test_min_sizeof(self, cls):
         md = cls()
         assert sys.getsizeof(md) < 1024
+
+    def test_issue_620_items(self, cls):
+        # https://github.com/aio-libs/multidict/issues/620
+        d = cls({"a": "123, 456", "b": "789"})
+        before_mutation_items = d.items()
+        d["c"] = "000"
+        # This causes an error on pypy.
+        list(before_mutation_items)
+
+    def test_issue_620_keys(self, cls):
+        # https://github.com/aio-libs/multidict/issues/620
+        d = cls({"a": "123, 456", "b": "789"})
+        before_mutation_keys = d.keys()
+        d["c"] = "000"
+        # This causes an error on pypy.
+        list(before_mutation_keys)
+
+    def test_issue_620_values(self, cls):
+        # https://github.com/aio-libs/multidict/issues/620
+        d = cls({"a": "123, 456", "b": "789"})
+        before_mutation_values = d.values()
+        d["c"] = "000"
+        # This causes an error on pypy.
+        list(before_mutation_values)