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

Discussion:
codecov[bot]: # [Codecov](https://codecov.io/gh/aio-libs/multidict/pull/628?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs) Report
> Merging [#628](https://codecov.io/gh/aio-libs/multidict/pull/628?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs) (e3b146f) into [master](https://codecov.io/gh/aio-libs/multidict/commit/9dde0bd53dc238e585eb1c3b86bc89539782e29e?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs) (9dde0bd) will **decrease** coverage by `0.01%`.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/aio-libs/multidict/pull/628/graphs/tree.svg?width=650&height=150&src=pr&token=pdhoIeKQL4&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs)](https://codecov.io/gh/aio-libs/multidict/pull/628?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs)

```diff
@@            Coverage Diff             @@
##           master     #628      +/-   ##
==========================================
- Coverage   93.78%   93.77%   -0.02%     
==========================================
  Files           5        5              
  Lines         499      498       -1     
==========================================
- Hits          468      467       -1     
  Misses         31       31              
```

| Flag | Coverage Δ | |
|---|---|---|
| unit | `93.77% <100.00%> (-0.02%)` | :arrow_down: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://codecov.io/gh/aio-libs/multidict/pull/628?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs) | Coverage Δ | |
|---|---|---|
| [multidict/\_multidict\_py.py](https://codecov.io/gh/aio-libs/multidict/pull/628/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs#diff-bXVsdGlkaWN0L19tdWx0aWRpY3RfcHkucHk=) | `99.40% <100.00%> (-0.01%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/aio-libs/multidict/pull/628?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/aio-libs/multidict/pull/628?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs). Last update [9dde0bd...e3b146f](https://codecov.io/gh/aio-libs/multidict/pull/628?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs).


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)