@@ -85,11 +85,28 @@ def __init__(self, spTree: CT_GroupShape, parent: ProvidesPart):
8585 self ._spTree = spTree
8686 self ._cached_max_shape_id = None
8787
88- def __getitem__ (self , idx : int ) -> BaseShape :
89- """Return shape at `idx` in sequence, e.g. `shapes[2]`."""
88+ def __getitem__ (self , key : int | str ) -> BaseShape :
89+ """Return shape at `key`. Mapping-like dispatch by key type.
90+
91+ - Integer ``key`` returns the shape at that index in document
92+ order, e.g. ``shapes[2]``. Raises |IndexError| if out of range.
93+ - String ``key`` returns the shape whose ``.name`` equals ``key``
94+ (the same lookup as :meth:`by_name`), e.g. ``shapes["Title 1"]``.
95+ Raises |KeyError| with a clear message on miss.
96+
97+ ``bool`` keys are rejected (|TypeError|) — they're a subclass of
98+ ``int`` so would otherwise silently resolve to index 0/1, which
99+ is almost certainly an unintended call.
100+
101+ Closes scanny/python-pptx#800.
102+ """
103+ if isinstance (key , bool ):
104+ raise TypeError ("shape key must be int or str, got bool" )
105+ if isinstance (key , str ):
106+ return self .by_name (key )
90107 shape_elms = list (self ._iter_member_elms ())
91108 try :
92- shape_elm = shape_elms [idx ]
109+ shape_elm = shape_elms [key ]
93110 except IndexError :
94111 raise IndexError ("shape index out of range" )
95112 return self ._shape_factory (shape_elm )
@@ -125,6 +142,67 @@ def by_name(self, name: str) -> BaseShape:
125142 return shape
126143 raise KeyError ("no shape named %r in this collection" % name )
127144
145+ def __contains__ (self , key : object ) -> bool :
146+ """Mapping-like membership: `"Title 1" in shapes` checks names.
147+
148+ - String key: True when any shape in this collection has a matching
149+ ``.name`` (case-sensitive).
150+ - Integer key: True when ``0 <= key < len(self)`` — sequence-style
151+ index range check, matching `__getitem__(int)` semantics.
152+
153+ ``bool`` and other key types return False (no implicit coercion;
154+ bools rejected for the same reason `__getitem__` rejects them —
155+ ``True``/``False`` as an index is almost always a bug).
156+ """
157+ if isinstance (key , bool ):
158+ return False
159+ if isinstance (key , str ):
160+ return any (shape .name == key for shape in self )
161+ if isinstance (key , int ):
162+ return 0 <= key < len (self )
163+ return False
164+
165+ def keys (self ) -> list [str ]:
166+ """List of every shape's ``.name`` in document order.
167+
168+ Mapping-like helper. Names may not be unique (PowerPoint doesn't
169+ enforce); duplicates appear in iteration order.
170+ """
171+ return [shape .name for shape in self ]
172+
173+ def iter_leaf_shapes (self ) -> Iterator [BaseShape ]:
174+ """Recursively yield every non-group shape in this collection.
175+
176+ Descends into `GroupShape` children; the group containers themselves
177+ are NOT yielded — only the leaf shapes (autoshapes, pictures,
178+ connectors, text frames, tables, charts, placeholders, etc.) inside
179+ them. A consumer wanting the group containers should use the
180+ regular `for shape in shapes` iteration.
181+
182+ Closes scanny/python-pptx#435.
183+ """
184+ # ---deferred import to avoid circular dependency---
185+ from pptx .shapes .group import GroupShape
186+
187+ for shape in self :
188+ if isinstance (shape , GroupShape ):
189+ yield from shape .shapes .iter_leaf_shapes ()
190+ else :
191+ yield shape
192+
193+ def in_selection_pane_order (self ) -> tuple [BaseShape , ...]:
194+ """Return shapes in PowerPoint's Selection Pane order.
195+
196+ The Selection Pane lists shapes from top-most (most recently drawn,
197+ rendered on top) to bottom-most. Top-most in PowerPoint is the
198+ last child in XML document order, so this is the reverse of
199+ ``tuple(self)``. Read-only snapshot — does not auto-update if
200+ the collection changes after the call.
201+
202+ Closes scanny/python-pptx#532.
203+ """
204+ return tuple (reversed (list (self )))
205+
128206 def clone_placeholder (self , placeholder : LayoutPlaceholder ) -> None :
129207 """Add a new placeholder shape based on `placeholder`."""
130208 sp = placeholder .element
@@ -859,22 +937,56 @@ def _shape_factory( # pyright: ignore[reportIncompatibleMethodOverride]
859937class SlidePlaceholders (ParentedElementProxy ):
860938 """Collection of placeholder shapes on a slide.
861939
862- Supports iteration, :func:`len`, and dictionary-style lookup on the `idx` value of the
863- placeholders it contains .
940+ Supports iteration, :func:`len`, and dictionary-style lookup by both the
941+ `idx` value (int) and the placeholder ``.name`` (str) .
864942 """
865943
866944 _element : CT_GroupShape
867945
868- def __getitem__ (self , idx : int ):
869- """Access placeholder shape having `idx`.
946+ def __getitem__ (self , key : int | str ):
947+ """Access placeholder shape by `idx` value (int) or `.name` (str).
948+
949+ Note that while this looks like list access, integer ``key`` is a
950+ dictionary key against the placeholder's ``ph_idx`` (NOT a sequence
951+ index) and will raise |KeyError| if no placeholder with that idx
952+ is in the collection. String ``key`` looks up by ``.name`` and
953+ raises |KeyError| on miss. ``bool`` keys are rejected (|TypeError|)
954+ — they're a subclass of ``int`` so would otherwise silently resolve
955+ to a `ph_idx == 0/1` lookup, almost certainly unintended.
870956
871- Note that while this looks like list access, idx is actually a dictionary key and will
872- raise |KeyError| if no placeholder with that idx value is in the collection.
957+ Closes scanny/python-pptx#800.
873958 """
959+ if isinstance (key , bool ):
960+ raise TypeError ("placeholder key must be int or str, got bool" )
961+ if isinstance (key , str ):
962+ for ph in self :
963+ if ph .name == key :
964+ return ph
965+ raise KeyError ("no placeholder named %r in this collection" % key )
874966 for e in self ._element .iter_ph_elms ():
875- if e .ph_idx == idx :
967+ if e .ph_idx == key :
876968 return SlideShapeFactory (e , self )
877- raise KeyError ("no placeholder on this slide with idx == %d" % idx )
969+ raise KeyError ("no placeholder on this slide with idx == %d" % key )
970+
971+ def __contains__ (self , key : object ) -> bool :
972+ """Mapping-like membership: `"Title 1" in placeholders` checks names.
973+
974+ - String key: True when any placeholder's ``.name`` matches.
975+ - Integer key: True when a placeholder with that ``ph_idx`` exists.
976+ - ``bool`` and other key types return False (bools rejected for the
977+ same reason `__getitem__` rejects them).
978+ """
979+ if isinstance (key , bool ):
980+ return False
981+ if isinstance (key , str ):
982+ return any (ph .name == key for ph in self )
983+ if isinstance (key , int ):
984+ return any (e .ph_idx == key for e in self ._element .iter_ph_elms ())
985+ return False
986+
987+ def keys (self ) -> list [str ]:
988+ """List of every placeholder's ``.name`` in iteration order."""
989+ return [ph .name for ph in self ]
878990
879991 def __iter__ (self ):
880992 """Generate placeholder shapes in `idx` order."""
0 commit comments