You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
ChainMap has wrong or unintuitive type after #6042 #8430
#6042/#6044 gives me new mypy complaints on my code base.
ChainMap only ever writes to the first mapping in maps. That's my understanding. For example ChainMap({}, defaults) is a typical use-case where the user gets a dict-like and cannot mutate the defaults. That's the typical stacking property of the ChainMap, you only ever mutate the top, most recent mapping.
I don't think we can have correct hints for the maps member, and new_child of course expects a mutable mapping because iirc it is exactly the next top most thing on the stack.
My understanding is that the typeshed policy is to err on the side of false negatives ... Here is an example in a ChainMap subclass ...
While I'm not sure we've stated this out loud, historically, typeshed has very consistently preferred false positives for subclassers of classes over false positives for other users of classes.
I just discovered collections.ChainMap, and I have to say I was surprised that it requires all member mappings to be mutable. I like the proposed __init__ typing, and I'd like to add that perhaps it's not so bad that maps would be typed as (immutable) Mapping: if I understand correctly end users aren't expected to modify inner mappings directly anyway (chainmap.maps[1]["key"] = "value"), so from their perspective it's only right. The fact that the stdlib implementation itself "violates" it is unfortunate, but as I see it it's more an implementation detail than a true violation: it knows that the first element is mutable and acts on it. Whether it does that by storing a separate reference or by accessing it through maps is irrelevant.
I believe that the most troublesome part with this suggestion is that the ChainMap documentation states that maps may be modified by the end user (e.g. maps[0] = {}). If we type it as suggested above, that would allow the user to set an immutable mapping at maps[0], that the stdlib implementation would then happily modify. As long as maps is intended to be mutable directly by the end user, I don't think the suggested change is feasible (except with python/typing#548 as stated in the original thread), however much I'd like to see it.
I was intrigued by the idea of an immutable variant, so I gave it a shot. It does require (safe) type ignores if built on top of the stdlib ChainMap, but I think that's by far preferable over the alternative. I suspect it also takes a performance hit, considering that this implements the Python ABC, while ChainMap (I'm assuming, haven't checked) is written in C. That said, I'll put it here in case anyone is interested.
I made two variations: one that is in fact still mutable but protects its member mappings from mutation. And one that is immutable itself. The benefit of the latter is that it can confidently implement the maps and parents properties, while the meanings of those get (partially) lost for the former. I did not do extensive testing on either of these.
importcollectionsfromcollections.abcimportIterator, Mapping, MutableMapping, SequencefromtypingimportSelfclassOverlayChainMap[K, V](MutableMapping[K, V]):
""" ChainMap implementation that protects its member mappings from mutation, while still allowing mutations on the chain map itself. See collections.ChainMap for more details. Does not implement the `maps` and `parents` properties, because they lose much of their meaning due to the overlay mapping sitting in between the user-supplied ones. """def__init__(self, *maps: Mapping[K, V]) ->None:
# construct backing ChainMap with owned, mutable dict as first mapself._chainmap: collections.ChainMap[K, V] =collections.ChainMap(
{},
# This is a type violation due to a limitation of the ChainMap constructor (python/typeshed#8430).# This workaround builds on the (documented) knowledge that ChainMap only ever modifies the first mapping,# and the fact that the constructed chain map is never exposed to the end user.# Therefore the type violation is a safe, if imperfect, workaround to the typing limitation.*maps, # type: ignore
)
defnew_child(self, m: Mapping[K, V] |None=None, **kwargs: V) ->Self:
returnself.__class__(
*self._chainmap.new_child(
m, # type: ignore
).new_child({}, **kwargs).maps
)
def__getitem__(self, key: K) ->V:
returnself._chainmap[key]
def__setitem__(self, key: K, value: V) ->None:
self._chainmap[key] =valuedef__delitem__(self, key: K) ->None:
delself._chainmap[key]
def__iter__(self) ->Iterator[K]:
returniter(self._chainmap)
def__len__(self) ->int:
returnlen(self._chainmap)
classImmutableChainMap[K, V](Mapping[K, V]):
""" Immutable ChainMap implementation. See collections.ChainMap for more details. """def__init__(self, *maps: Mapping[K, V]) ->None:
# construct backing ChainMap with owned, mutable dict as first mapself._chainmap: collections.ChainMap[K, V] =collections.ChainMap(
# This is a type violation due to a limitation of the ChainMap constructor (python/typeshed#8430).# This workaround builds on the fact that we own the backing chain map, we never mutate it, and we only# expose it or its attributes as immutable properties.*maps# type: ignore
)
defnew_child(self, m: Mapping[K, V] |None=None) ->Self:
returnself.__class__(
*self._chainmap.new_child(
m, # type: ignore
).maps
)
@propertydefmaps(self) ->Sequence[Mapping[K, V]]:
returnself._chainmap.maps@propertydefparents(self) ->Self:
returnself.__class__(*self._chainmap.parents.maps)
def__getitem__(self, key: K) ->V:
returnself._chainmap[key]
def__iter__(self) ->Iterator[K]:
returniter(self._chainmap)
def__len__(self) ->int:
returnlen(self._chainmap)
d1: MutableMapping[str, int] = {"one": 1, "shared": 1}
d1_copy: Mapping[str, int] = {**d1}
d2: MutableMapping[str, int] = {"two": 2, "shared": 2}
d2_copy: Mapping[str, int] = {**d2}
copied: Mapping[str, int] = {**d1, **d2}
chained: MutableMapping[str, int] =collections.ChainMap(d2, d1)
overlayed: MutableMapping[str, int] =OverlayChainMap(d2, d1)
immutably_chained: Mapping[str, int] =ImmutableChainMap(d2, d1)
assert (
list(chained.items())
==list(overlayed.items())
==list(immutably_chained.items())
==list(copied.items())
== [("one", 1), ("shared", 2), ("two", 2)]
)
overlayed["three"] =3# adds a new itemoverlayed["one"] =42# changes the value in overlayed, not in d1overlayed["two"] =42# changes the value in overlayed, not in d2assertlist(overlayed.items()) == [("one", 42), ("shared", 2), ("two", 42), ("three", 3)]
assertd1==d1_copyassertd2==d2_copychained["three"] =3# adds a new itemchained["one"] =42# changes the value in chained, not in d1chained["two"] =42# changes the value in both chained and d2assertlist(chained.items()) == [("one", 42), ("shared", 2), ("two", 42), ("three", 3)]
assertd1==d1_copyassertd2!=d2_copy
Activity
kaste commentedon Jul 28, 2022
@hauntsaninja wrote
fix: Fix types
ci: Fix typing
fix: Don't deepcopy the local config
sanderr commentedon Jan 23, 2025
I just discovered
collections.ChainMap, and I have to say I was surprised that it requires all member mappings to be mutable. I like the proposed__init__typing, and I'd like to add that perhaps it's not so bad thatmapswould be typed as (immutable)Mapping: if I understand correctly end users aren't expected to modify inner mappings directly anyway (chainmap.maps[1]["key"] = "value"), so from their perspective it's only right. The fact that the stdlib implementation itself "violates" it is unfortunate, but as I see it it's more an implementation detail than a true violation: it knows that the first element is mutable and acts on it. Whether it does that by storing a separate reference or by accessing it throughmapsis irrelevant.I believe that the most troublesome part with this suggestion is that the
ChainMapdocumentation states thatmapsmay be modified by the end user (e.g.maps[0] = {}). If we type it as suggested above, that would allow the user to set an immutable mapping atmaps[0], that the stdlib implementation would then happily modify. As long asmapsis intended to be mutable directly by the end user, I don't think the suggested change is feasible (except with python/typing#548 as stated in the original thread), however much I'd like to see it.I was intrigued by the idea of an immutable variant, so I gave it a shot. It does require (safe) type ignores if built on top of the stdlib
ChainMap, but I think that's by far preferable over the alternative. I suspect it also takes a performance hit, considering that this implements the Python ABC, whileChainMap(I'm assuming, haven't checked) is written in C. That said, I'll put it here in case anyone is interested.I made two variations: one that is in fact still mutable but protects its member mappings from mutation. And one that is immutable itself. The benefit of the latter is that it can confidently implement the
mapsandparentsproperties, while the meanings of those get (partially) lost for the former. I did not do extensive testing on either of these.ci: Fix typing
fix: Don't deepcopy the local config