Fix CaseInsensitiveDict.copy() sharing state with the original (#7783)

* Fix CaseInsensitiveDict.copy() sharing state with the original

* Address review: don't re-normalise in __copy__, keep _keys in sync in __ior__

UserDict.__ior__ writes self.data directly, bypassing __setitem__, so
_keys never learned about the new keys.
This commit is contained in:
Shadow_Lu 2026-07-28 15:51:55 +05:30 committed by GitHub
parent 7e8b58a2b2
commit 59ebb26e60
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 47 additions and 0 deletions

View File

@ -132,6 +132,22 @@ class CaseInsensitiveDict(collections.UserDict[str | bytes, Any]):
def __repr__(self) -> str:
return f"<{self.__class__.__name__}: {super().__repr__()}>"
# UserDict.copy() shallow-copies the instance, which would share self._keys
# between the copy and the original.
def __copy__(self) -> Self:
new = self.__class__()
new.data = self.data.copy()
new._keys = self._keys.copy()
return new
copy = __copy__
# UserDict.__ior__ updates self.data directly, which would leave self._keys
# out of date.
def __ior__(self, other: Any) -> Self: # type: ignore[override,misc]
self.update(other)
return self
def _normkey(self, key: str | bytes) -> str | bytes:
return key

View File

@ -204,6 +204,15 @@ class TestCaseInsensitiveDictBase(ABC):
assert h1.get("header1") == h3.get("header1")
assert h1.get("header1") == h3.get("HEADER1")
def test_copy_is_independent(self):
h1 = self.dict_class({"header1": "value1", "header2": "value2"})
for h2 in (copy.copy(h1), h1.copy()):
del h2["header1"]
h2["header3"] = "value3"
assert "header1" in h1
assert "header3" not in h1
assert dict(h1) == {"header1": "value1", "header2": "value2"}
class TestCaseInsensitiveDict(TestCaseInsensitiveDictBase):
dict_class = CaseInsensitiveDict # type: ignore[assignment]
@ -220,6 +229,28 @@ class TestCaseInsensitiveDict(TestCaseInsensitiveDictBase):
assert isinstance(iterkeys, Iterator)
assert list(iterkeys) == ["AsDf", "FoO"]
def test_copy_keeps_values(self):
class MyDict(self.dict_class):
def _normvalue(self, value):
return value + 1
d = MyDict({"key": 1})
for copied in (copy.copy(d), d.copy()):
assert copied["key"] == 2
def test_ior(self):
d = self.dict_class({"header1": "value1"})
d |= {"HEADER1": "value2", "header2": "value3"}
assert len(d) == 2
assert d["HeAdEr1"] == "value2"
assert d["HeAdEr2"] == "value3"
def test_ior_mapping(self):
d = self.dict_class({"header1": "value1"})
d |= self.dict_class({"HEADER1": "value2"})
assert len(d) == 1
assert d["HeAdEr1"] == "value2"
@pytest.mark.filterwarnings("ignore::scrapy.exceptions.ScrapyDeprecationWarning")
class TestCaselessDict(TestCaseInsensitiveDictBase):