python-pptx2 2.17.0__py3-none-any.whl → 2.19.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pptx2/__init__.py CHANGED
@@ -57,7 +57,7 @@ from pptx2.parts.slide import (
57
57
  if TYPE_CHECKING:
58
58
  from pptx2.opc.package import Part
59
59
 
60
- __version__ = "2.17.0"
60
+ __version__ = "2.19.0"
61
61
 
62
62
  sys.modules["pptx2.exceptions"] = exceptions
63
63
  del sys
@@ -0,0 +1,210 @@
1
+ """Agent-friendly kwarg normalization for the one-call shape helpers.
2
+
3
+ Code generators (LLM-trained models among them) reliably spell keyword
4
+ arguments the way *other* libraries spell them — matplotlib's
5
+ ``fontfamily``/``fontsize``/``ha``/``va``, CSS-flavored ``text-align``,
6
+ ``colour`` — and die on ``TypeError: unexpected keyword argument``. This
7
+ module absorbs that whole error class for the ergonomic helpers
8
+ (``add_text`` / ``add_equation`` / ``add_arrow``) in three layers:
9
+
10
+ 1. **Synonyms** — a fixed map from common spellings onto each canonical
11
+ argument (``halign`` → ``align``, ``font_size`` → ``size_pt`` …).
12
+ 2. **Fuzzy matching** — a near-miss kwarg (``algn``, ``colr``) is resolved
13
+ to its closest canonical or synonym when unambiguous.
14
+ 3. **Didactic errors** — anything still unknown raises a ``TypeError``
15
+ that names the closest candidate and lists every accepted kwarg, so a
16
+ model reading the traceback self-corrects in one step.
17
+
18
+ An alias may *substitute* for its canonical but never contradict it:
19
+ passing two different values for the same logical argument is a genuine
20
+ caller bug and still raises.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import difflib
26
+ import functools
27
+ import inspect
28
+
29
+ # Canonical kwarg -> spellings other ecosystems use for the same thing.
30
+ SYNONYMS: dict[str, tuple[str, ...]] = {
31
+ "text": ("txt", "string", "content", "label", "caption", "value"),
32
+ "font": (
33
+ "font_family", "fontfamily", "font_name", "fontname", "typeface", "family", "face",
34
+ ),
35
+ "size_pt": ("size", "font_size", "fontsize", "pt_size", "point_size"),
36
+ "align": ("halign", "ha", "horizontal_align", "horizontal_alignment", "text_align", "text_alignment"),
37
+ "anchor": ("valign", "va", "vertical_align", "vertical_alignment", "v_align"),
38
+ "color": (
39
+ "colour", "font_color", "font_colour", "text_color", "text_colour",
40
+ "fg_color", "line_color", "stroke_color",
41
+ ),
42
+ "weight_pt": ("weight", "line_weight", "width_pt", "stroke_width"),
43
+ "bold": ("font_bold", "is_bold"),
44
+ "italic": ("font_italic", "is_italic", "oblique"),
45
+ "word_wrap": ("wrap", "wrap_text", "text_wrap"),
46
+ "margin_pt": ("margin", "padding", "padding_pt", "inset", "inset_pt"),
47
+ "latex": ("tex", "formula", "equation", "expression", "math"),
48
+ "left": ("x",),
49
+ "top": ("y",),
50
+ "width": ("w",),
51
+ "height": ("h",),
52
+ "start": ("begin", "start_point", "start_shape", "from", "source"),
53
+ "end": ("to", "end_point", "end_shape", "target"),
54
+ "image_file": ("image", "img", "path", "file", "filename", "image_path", "file_path"),
55
+ "steps": ("items", "stages", "nodes", "boxes"),
56
+ }
57
+
58
+ _ALIAS_TO_CANONICAL: dict[str, str] = {
59
+ alias: canonical
60
+ for canonical, aliases in SYNONYMS.items()
61
+ for alias in aliases
62
+ }
63
+
64
+ _FUZZY_CUTOFF = 0.85
65
+
66
+
67
+ def _did_you_mean(name: str, candidates: list[str], known: "list[str] | tuple[str, ...]") -> str | None:
68
+ """Return the canonical arg *name* points at, or None.
69
+
70
+ Near-misses that hit both a canonical and one of its own aliases
71
+ (``algn`` → ``align`` and ``halign``) are NOT ambiguous — they agree —
72
+ so candidates are collapsed to their canonical form before the
73
+ ambiguity check.
74
+ """
75
+ if not candidates:
76
+ return None
77
+ matches = difflib.get_close_matches(name, candidates, n=3, cutoff=_FUZZY_CUTOFF)
78
+ distinct = set()
79
+ for m in matches:
80
+ canonical = m if m in known else _ALIAS_TO_CANONICAL.get(m)
81
+ if canonical in known:
82
+ distinct.add(canonical)
83
+ if len(distinct) == 1:
84
+ return distinct.pop()
85
+ return None
86
+
87
+
88
+ def absorb_agent_kwargs(
89
+ method: str,
90
+ kwargs: dict,
91
+ canonical_names: "list[str] | tuple[str, ...]",
92
+ extra_synonyms: "dict[str, tuple[str, ...]] | None" = None,
93
+ pass_through_unknown: bool = False,
94
+ ) -> dict:
95
+ """Map alias / near-miss *kwargs* onto canonical names; raise else.
96
+
97
+ *canonical_names* lists the kwargs the method actually understands
98
+ (including any already-bound explicit parameters). *extra_synonyms*
99
+ adds call-site-specific spellings (e.g. ``{"cx": ("width", "w")}``
100
+ for ``add_chart``). Returns a dict containing only canonical names
101
+ — unless *pass_through_unknown* is set (for functions that accept
102
+ ``**kwargs`` themselves), in which case unabsorbable names are
103
+ passed through verbatim. A synonym that matches a canonical the
104
+ caller already supplied with a *different* value raises
105
+ ``TypeError``; equal values are fine.
106
+ """
107
+ known = list(canonical_names)
108
+ alias_map = dict(_ALIAS_TO_CANONICAL)
109
+ if extra_synonyms:
110
+ for canonical, aliases in extra_synonyms.items():
111
+ for alias in aliases:
112
+ alias_map[alias] = canonical
113
+ alias_space = [a for a in alias_map if a not in known]
114
+ candidates = known + alias_space
115
+
116
+ resolved: dict = {}
117
+ for name, value in kwargs.items():
118
+ if name in known or name in resolved:
119
+ if name in resolved and resolved[name] != value:
120
+ raise TypeError(
121
+ f"{method}(): got {name!r} twice with different values "
122
+ f"({resolved[name]!r} vs {value!r})"
123
+ )
124
+ resolved[name] = value
125
+ continue
126
+ canonical = alias_map.get(name)
127
+ if canonical is not None and canonical not in known:
128
+ # The alias resolves to an argument this method doesn't take
129
+ # (e.g. ``to=`` on add_text); fall through to the fuzzy/error
130
+ # path rather than silently dropping the value.
131
+ canonical = None
132
+ if canonical is None:
133
+ canonical = _did_you_mean(name, candidates, known)
134
+ if canonical is None:
135
+ if pass_through_unknown:
136
+ resolved[name] = value
137
+ continue
138
+ raise TypeError(
139
+ f"{method}(): got an unexpected keyword argument {name!r}. "
140
+ f"Accepted: {', '.join(sorted(known))} "
141
+ f"(synonyms like font_family/halign/valign/colour are fine)"
142
+ )
143
+ if canonical in resolved and resolved[canonical] != value:
144
+ raise TypeError(
145
+ f"{method}(): {name!r} and the value already given for "
146
+ f"{canonical!r} disagree ({value!r} vs {resolved[canonical]!r}); "
147
+ f"pass one spelling only"
148
+ )
149
+ resolved[canonical] = value
150
+ return resolved
151
+
152
+
153
+ def agent_friendly(extra_synonyms=None):
154
+ """Decorator: make a method/function absorb alias and fuzzy kwargs.
155
+
156
+ Wrap any public helper so its keyword arguments get the full
157
+ treatment from :func:`absorb_agent_kwargs` — global synonyms,
158
+ per-call *extra_synonyms*, fuzzy near-misses, and didactic errors —
159
+ without touching its body. Usable bare (``@agent_friendly``) or
160
+ with per-method synonyms. A keyword that repeats a value already
161
+ bound positionally (through a synonym or exact name) is dropped when
162
+ equal and raises ``TypeError`` when it contradicts::
163
+
164
+ @agent_friendly({"cx": ("width", "w"), "cy": ("height", "h")})
165
+ def add_chart(self, x, y, cx, cy, chart_type, chart_data): ...
166
+ """
167
+ if callable(extra_synonyms): # used bare: @agent_friendly above def
168
+ return _decorate_friendly(extra_synonyms, None)
169
+
170
+ def decorator(func):
171
+ return _decorate_friendly(func, extra_synonyms)
172
+
173
+ return decorator
174
+
175
+
176
+ def _decorate_friendly(func, extra_synonyms):
177
+ sig = inspect.signature(func)
178
+ has_self = bool(sig.parameters) and next(iter(sig.parameters)) in ("self", "cls")
179
+ param_list = [
180
+ p
181
+ for p in sig.parameters.values()
182
+ if p.kind not in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD)
183
+ and p.name not in ("self", "cls")
184
+ ]
185
+ known = tuple(p.name for p in param_list)
186
+ takes_var_kw = any(
187
+ p.kind == inspect.Parameter.VAR_KEYWORD for p in sig.parameters.values()
188
+ )
189
+
190
+ @functools.wraps(func)
191
+ def wrapper(*args, **kwargs):
192
+ if kwargs:
193
+ kwargs = absorb_agent_kwargs(
194
+ func.__name__, kwargs, known, extra_synonyms,
195
+ pass_through_unknown=takes_var_kw,
196
+ )
197
+ offset = 1 if has_self else 0
198
+ for i, p in enumerate(param_list[: len(args) - offset]):
199
+ if p.name in kwargs:
200
+ if kwargs[p.name] != args[offset + i]:
201
+ raise TypeError(
202
+ f"{func.__name__}(): {p.name} given positionally "
203
+ f"({args[offset + i]!r}) and by keyword "
204
+ f"({kwargs[p.name]!r}) with different values"
205
+ )
206
+ del kwargs[p.name]
207
+ return func(*args, **kwargs)
208
+
209
+ return wrapper
210
+
pptx2/diagrams.py CHANGED
@@ -32,6 +32,7 @@ import math
32
32
  from dataclasses import dataclass, field
33
33
  from typing import TYPE_CHECKING, Any, Sequence
34
34
 
35
+ from pptx2._agent_friendly import agent_friendly
35
36
  from pptx2.enum.shapes import MSO_SHAPE
36
37
  from pptx2.geometry import BBox
37
38
  from pptx2.util import Emu, Inches, Pt
@@ -245,6 +246,14 @@ def _card(
245
246
  # ----------------------------------------------------------------------------- pipelines
246
247
 
247
248
 
249
+ @agent_friendly(
250
+ {
251
+ "text_color": ("color", "colour", "fg_color", "text_colour"),
252
+ "accent": ("accent_color", "primary", "primary_color"),
253
+ "fill": ("fill_color", "background", "bg"),
254
+ }
255
+
256
+ )
248
257
  def horizontal_pipeline(
249
258
  slide,
250
259
  bbox: BBox,
@@ -312,6 +321,14 @@ def horizontal_pipeline(
312
321
  return PipelineResult(cards=cards, arrows=arrows)
313
322
 
314
323
 
324
+ @agent_friendly(
325
+ {
326
+ "text_color": ("color", "colour", "fg_color", "text_colour"),
327
+ "accent": ("accent_color", "primary", "primary_color"),
328
+ "fill": ("fill_color", "background", "bg"),
329
+ }
330
+
331
+ )
315
332
  def vertical_pipeline(
316
333
  slide,
317
334
  bbox: BBox,
@@ -371,6 +388,15 @@ def vertical_pipeline(
371
388
  # ----------------------------------------------------------------------------- hub
372
389
 
373
390
 
391
+ @agent_friendly(
392
+ {
393
+ "text_color": ("color", "colour", "fg_color", "text_colour"),
394
+ "accent": ("accent_color", "primary", "primary_color"),
395
+ "fill": ("fill_color", "background", "bg"),
396
+ "spokes": ("items", "stages", "nodes"),
397
+ "centre": ("center", "hub", "hub_label", "title"),
398
+ }
399
+ )
374
400
  def hub_and_spoke(
375
401
  slide,
376
402
  bbox: BBox,
@@ -479,6 +505,14 @@ def hub_and_spoke(
479
505
  # ----------------------------------------------------------------------------- cycle
480
506
 
481
507
 
508
+ @agent_friendly(
509
+ {
510
+ "text_color": ("color", "colour", "fg_color", "text_colour"),
511
+ "accent": ("accent_color", "primary", "primary_color"),
512
+ "fill": ("fill_color", "background", "bg"),
513
+ }
514
+
515
+ )
482
516
  def cycle(
483
517
  slide,
484
518
  bbox: BBox,
@@ -553,6 +587,15 @@ def cycle(
553
587
  # ----------------------------------------------------------------------------- decision tree
554
588
 
555
589
 
590
+ @agent_friendly(
591
+ {
592
+ "text_color": ("color", "colour", "fg_color", "text_colour"),
593
+ "accent": ("accent_color", "primary", "primary_color"),
594
+ "fill": ("fill_color", "background", "bg"),
595
+ "branches": ("items", "nodes", "children", "steps"),
596
+ "root": ("root_label", "title", "center"),
597
+ }
598
+ )
556
599
  def decision_tree(
557
600
  slide,
558
601
  bbox: BBox,
@@ -675,6 +718,14 @@ def decision_tree(
675
718
  # ----------------------------------------------------------------------------- columns
676
719
 
677
720
 
721
+ @agent_friendly(
722
+ {
723
+ "text_color": ("color", "colour", "fg_color", "text_colour"),
724
+ "accent": ("accent_color", "primary", "primary_color"),
725
+ "fill": ("fill_color", "background", "bg"),
726
+ "columns": ("items", "cols", "sections"),
727
+ }
728
+ )
678
729
  def comparison_columns(
679
730
  slide,
680
731
  bbox: BBox,
pptx2/shapes/shapetree.py CHANGED
@@ -34,6 +34,7 @@ from pptx2.shapes.placeholder import (
34
34
  TablePlaceholder,
35
35
  )
36
36
  from pptx2.shared import ParentedElementProxy
37
+ from pptx2._agent_friendly import agent_friendly
37
38
  from pptx2.util import Emu, _coerce_emu, lazyproperty
38
39
 
39
40
  if TYPE_CHECKING:
@@ -557,24 +558,6 @@ class _BaseShapes(ParentedElementProxy):
557
558
  return BaseShapeFactory(shape_elm, self)
558
559
 
559
560
 
560
- def _resolve_alias(method: str, arg: str, canonical, alias):
561
- """Return *canonical*, falling back to its *alias* kwarg (both nullable).
562
-
563
- Code generators (matplotlib-trained models among them) habitually spell
564
- ``font`` as ``font_family`` and ``anchor`` as ``valign``; accepting the
565
- alias removes a whole class of one-keyword-away TypeErrors. Passing
566
- both with different values is a genuine caller bug and still raises.
567
- """
568
- if canonical is not None:
569
- if alias is not None and alias != canonical:
570
- raise TypeError(
571
- f"{method}(): pass either {arg}= or its alias, not two "
572
- f"different values ({canonical!r} vs {alias!r})"
573
- )
574
- return canonical
575
- return alias
576
-
577
-
578
561
  class _BaseGroupShapes(_BaseShapes):
579
562
  """Base class for shape-trees that can add shapes."""
580
563
 
@@ -585,6 +568,7 @@ class _BaseGroupShapes(_BaseShapes):
585
568
  super(_BaseGroupShapes, self).__init__(grpSp, parent)
586
569
  self._grpSp = grpSp
587
570
 
571
+ @agent_friendly({"cx": ("width", "w"), "cy": ("height", "h"), "chart_data": ("data", "chartdata"), "chart_type": ("type", "charttype")})
588
572
  def add_chart(
589
573
  self,
590
574
  chart_type: XL_CHART_TYPE,
@@ -619,6 +603,7 @@ class _BaseGroupShapes(_BaseShapes):
619
603
  _apply_horizontal_bar_default(shape, chart_type)
620
604
  return cast("Chart", shape)
621
605
 
606
+ @agent_friendly({"connector_type": ("type", "kind", "connector"), "begin_x": ("x1", "start_x"), "begin_y": ("y1", "start_y"), "end_x": ("x2", "to_x"), "end_y": ("y2", "to_y")})
622
607
  def add_connector(
623
608
  self,
624
609
  connector_type: MSO_CONNECTOR_TYPE,
@@ -639,6 +624,7 @@ class _BaseGroupShapes(_BaseShapes):
639
624
  self._recalculate_extents()
640
625
  return cast(Connector, self._shape_factory(cxnSp))
641
626
 
627
+ @agent_friendly({"shapes": ("items", "members")})
642
628
  def add_group_shape(self, shapes: Iterable[BaseShape] = ()) -> GroupShape:
643
629
  """Return a |GroupShape| object newly appended to this shape tree.
644
630
 
@@ -657,6 +643,7 @@ class _BaseGroupShapes(_BaseShapes):
657
643
  grpSp.recalculate_extents()
658
644
  return cast(GroupShape, self._shape_factory(grpSp))
659
645
 
646
+ @agent_friendly
660
647
  def add_ole_object(
661
648
  self,
662
649
  object_file: str | IO[bytes],
@@ -718,6 +705,7 @@ class _BaseGroupShapes(_BaseShapes):
718
705
  self._recalculate_extents()
719
706
  return cast(GraphicFrame, self._shape_factory(graphicFrame))
720
707
 
708
+ @agent_friendly
721
709
  def add_picture(
722
710
  self,
723
711
  image_file: str | os.PathLike[str] | IO[bytes],
@@ -780,6 +768,7 @@ class _BaseGroupShapes(_BaseShapes):
780
768
  picture.top = Emu(ct + new_top)
781
769
  return picture
782
770
 
771
+ @agent_friendly({"svg_file": ("svg", "svg_path", "svg_source")})
783
772
  def add_svg_picture(
784
773
  self,
785
774
  svg_file,
@@ -843,6 +832,7 @@ class _BaseGroupShapes(_BaseShapes):
843
832
  self._recalculate_extents()
844
833
  return cast(Picture, self._shape_factory(pic))
845
834
 
835
+ @agent_friendly({"autoshape_type_id": ("shape_type", "autoshape_type", "shape", "preset_shape")})
846
836
  def add_shape(
847
837
  self,
848
838
  autoshape_type_id: MSO_SHAPE,
@@ -881,6 +871,7 @@ class _BaseGroupShapes(_BaseShapes):
881
871
  shape.top = Emu(ct + new_top)
882
872
  return shape
883
873
 
874
+ @agent_friendly
884
875
  def add_textbox(
885
876
  self,
886
877
  left: Length,
@@ -918,53 +909,115 @@ class _BaseGroupShapes(_BaseShapes):
918
909
  *bbox_or_positional,
919
910
  text: str = "",
920
911
  font: str | None = None,
921
- font_family: str | None = None,
922
912
  size_pt: float | None = None,
923
913
  bold: bool | None = None,
924
914
  italic: bool | None = None,
925
915
  color=None,
926
916
  align: str | None = None,
927
917
  anchor: str | None = None,
928
- valign: str | None = None,
929
918
  margin_pt: float | tuple[float, float, float, float] | None = None,
930
919
  word_wrap: bool | None = True,
920
+ **kwargs,
931
921
  ) -> Shape:
932
922
  """Add a textbox carrying *text* with one-call styling.
933
923
 
934
- Accepts either a :class:`~pptx2.geometry.BBox` positionally
935
- or the four ``(left, top, width, height)`` lengths::
924
+ Accepts either a :class:`~pptx2.geometry.BBox` positionally, the
925
+ four ``(left, top, width, height)`` lengths positionally, *or* the
926
+ same four as keyword arguments (``x`` / ``y`` / ``w`` / ``h``
927
+ synonyms included)::
936
928
 
937
929
  slide.shapes.add_text(bbox, text="Hello", size_pt=24, bold=True,
938
930
  color="#0B5CFF", align="center")
939
931
  slide.shapes.add_text(Inches(1), Inches(2), Inches(4), Inches(1),
940
932
  text="Hello")
933
+ slide.shapes.add_text(x=Inches(1), y=Inches(2), w=Inches(4),
934
+ h=Inches(1), text="Hello")
941
935
 
942
936
  Keyword args:
943
937
 
938
+ * ``text`` — the string (``label`` / ``content`` synonyms work).
944
939
  * ``font`` — typeface name (e.g. ``"Inter"``); ``None`` inherits.
945
- ``font_family`` is accepted as an alias (matplotlib habits die
946
- hard).
947
- * ``size_pt`` — font size in points; ``None`` inherits.
940
+ * ``size_pt`` — font size in points (synonyms: ``size``,
941
+ ``font_size``, ``fontsize``).
948
942
  * ``bold`` / ``italic`` — ``True``/``False``/``None``.
949
943
  * ``color`` — any "color-like" (``"#RRGGBB"``, ``RGBColor``,
950
- ``(r, g, b)``).
944
+ ``(r, g, b)``); ``colour`` / ``text_color`` synonyms work.
951
945
  * ``align`` — ``"left"`` / ``"center"`` / ``"right"`` /
952
- ``"justify"``; ``None`` inherits.
946
+ ``"justify"``; ``halign`` is a synonym.
953
947
  * ``anchor`` — vertical anchor: ``"top"`` / ``"middle"`` (also
954
- ``"mid"`` / ``"center"``) / ``"bottom"``; ``None`` inherits.
955
- ``valign`` is accepted as an alias.
948
+ ``"mid"`` / ``"center"``) / ``"bottom"``; ``valign`` is a
949
+ synonym.
956
950
  * ``margin_pt`` — uniform margin in points, or a 4-tuple
957
951
  ``(top, right, bottom, left)``.
958
952
  * ``word_wrap`` — defaults to ``True``.
959
953
 
954
+ Misspelled or cross-library kwargs are absorbed when unambiguous
955
+ (``font_family`` → ``font``, ``fontsize`` → ``size_pt``, ``algn``
956
+ → ``align`` …); anything unrecognizable raises a ``TypeError``
957
+ naming the accepted arguments.
958
+
960
959
  Returns the textbox :class:`Shape` so further mutation works as
961
960
  normal.
962
961
  """
962
+ from pptx2._agent_friendly import absorb_agent_kwargs
963
963
  from pptx2._textstyle import apply_margins, apply_text_style, coerce_anchor
964
964
  from pptx2.geometry import BBox
965
965
 
966
- font = _resolve_alias("add_text", "font", font, font_family)
967
- anchor = _resolve_alias("add_text", "anchor", anchor, valign)
966
+ if kwargs:
967
+ kwargs = absorb_agent_kwargs(
968
+ "add_text",
969
+ kwargs,
970
+ (
971
+ "text", "font", "size_pt", "bold", "italic", "color",
972
+ "align", "anchor", "margin_pt", "word_wrap",
973
+ "left", "top", "width", "height",
974
+ ),
975
+ )
976
+ geo_names = ("left", "top", "width", "height")
977
+ if any(g in kwargs for g in geo_names):
978
+ if bbox_or_positional:
979
+ raise TypeError(
980
+ "add_text(): got both positional geometry and geometry "
981
+ "keyword arguments; pass one form only"
982
+ )
983
+ if not all(g in kwargs for g in geo_names):
984
+ missing = [g for g in geo_names if g not in kwargs]
985
+ raise TypeError(
986
+ "add_text(): geometry keywords need all of "
987
+ f"left/top/width/height; missing {', '.join(missing)}"
988
+ )
989
+ bbox_or_positional = tuple(kwargs[g] for g in geo_names)
990
+ defaults = {
991
+ "text": "", "font": None, "size_pt": None, "bold": None,
992
+ "italic": None, "color": None, "align": None, "anchor": None,
993
+ "margin_pt": None, "word_wrap": True,
994
+ }
995
+ given = {
996
+ "text": text, "font": font, "size_pt": size_pt, "bold": bold,
997
+ "italic": italic, "color": color, "align": align,
998
+ "anchor": anchor, "margin_pt": margin_pt,
999
+ "word_wrap": word_wrap,
1000
+ }
1001
+ for name in defaults:
1002
+ if name not in kwargs:
1003
+ continue
1004
+ value = kwargs[name]
1005
+ if given[name] != defaults[name] and given[name] != value:
1006
+ raise TypeError(
1007
+ f"add_text(): {name!r} given twice with different "
1008
+ f"values ({given[name]!r} vs {value!r})"
1009
+ )
1010
+ given[name] = value
1011
+ text = given["text"]
1012
+ font = given["font"]
1013
+ size_pt = given["size_pt"]
1014
+ bold = given["bold"]
1015
+ italic = given["italic"]
1016
+ color = given["color"]
1017
+ align = given["align"]
1018
+ anchor = given["anchor"]
1019
+ margin_pt = given["margin_pt"]
1020
+ word_wrap = given["word_wrap"]
968
1021
 
969
1022
  if len(bbox_or_positional) == 1 and isinstance(bbox_or_positional[0], BBox):
970
1023
  box = bbox_or_positional[0]
@@ -1009,21 +1062,21 @@ class _BaseGroupShapes(_BaseShapes):
1009
1062
  def add_equation(
1010
1063
  self,
1011
1064
  *bbox_or_positional,
1012
- latex: str,
1065
+ latex: str | None = None,
1013
1066
  display: bool = True,
1014
1067
  font: str | None = None,
1015
- font_family: str | None = None,
1016
1068
  size_pt: float | None = None,
1017
1069
  color=None,
1018
1070
  align: str | None = "center",
1019
1071
  anchor: str | None = "middle",
1020
- valign: str | None = None,
1021
1072
  margin_pt: float | tuple[float, float, float, float] | None = None,
1073
+ **kwargs,
1022
1074
  ) -> Shape:
1023
1075
  """Add a text box containing a native PowerPoint equation from LaTeX.
1024
1076
 
1025
- Accepts either a :class:`~pptx2.geometry.BBox` or
1026
- ``(left, top, width, height)``::
1077
+ Accepts either a :class:`~pptx2.geometry.BBox`, the four
1078
+ ``(left, top, width, height)`` lengths positionally, or the same
1079
+ four as keyword arguments (``x`` / ``y`` / ``w`` / ``h`` included)::
1027
1080
 
1028
1081
  slide.shapes.add_equation(bbox, latex=r"\\frac{a}{b}", size_pt=28)
1029
1082
  slide.shapes.add_equation(
@@ -1036,16 +1089,74 @@ class _BaseGroupShapes(_BaseShapes):
1036
1089
  equation editor.
1037
1090
 
1038
1091
  Keyword args match :meth:`add_text` for *font* / *size_pt* /
1039
- *color* / *align* / *anchor* / *margin_pt* (including the
1040
- ``font_family`` / ``valign`` aliases). *display* (default
1041
- |True|) emits a display-math paragraph; set |False| for inline
1042
- OMML.
1092
+ *color* / *align* / *anchor* / *margin_pt*, and *latex* accepts
1093
+ the ``tex`` / ``formula`` / ``equation`` synonyms. Misspelled or
1094
+ cross-library kwargs are absorbed when unambiguous (see
1095
+ :meth:`add_text`). *display* (default |True|) emits a display-math
1096
+ paragraph; set |False| for inline OMML.
1043
1097
  """
1098
+ from pptx2._agent_friendly import absorb_agent_kwargs
1044
1099
  from pptx2._textstyle import apply_margins, coerce_align, coerce_anchor
1045
1100
  from pptx2.geometry import BBox
1046
1101
 
1047
- font = _resolve_alias("add_equation", "font", font, font_family)
1048
- anchor = _resolve_alias("add_equation", "anchor", anchor, valign)
1102
+ if kwargs:
1103
+ kwargs = absorb_agent_kwargs(
1104
+ "add_equation",
1105
+ kwargs,
1106
+ (
1107
+ "latex", "display", "font", "size_pt", "color", "align",
1108
+ "anchor", "margin_pt", "left", "top", "width", "height",
1109
+ ),
1110
+ )
1111
+ geo_names = ("left", "top", "width", "height")
1112
+ if any(g in kwargs for g in geo_names):
1113
+ if bbox_or_positional:
1114
+ raise TypeError(
1115
+ "add_equation(): got both positional geometry and "
1116
+ "geometry keyword arguments; pass one form only"
1117
+ )
1118
+ if not all(g in kwargs for g in geo_names):
1119
+ missing = [g for g in geo_names if g not in kwargs]
1120
+ raise TypeError(
1121
+ "add_equation(): geometry keywords need all of "
1122
+ f"left/top/width/height; missing {', '.join(missing)}"
1123
+ )
1124
+ bbox_or_positional = tuple(kwargs[g] for g in geo_names)
1125
+ defaults = {
1126
+ "latex": None, "display": True, "font": None, "size_pt": None,
1127
+ "color": None, "align": "center", "anchor": "middle",
1128
+ "margin_pt": None,
1129
+ }
1130
+ given = {
1131
+ "latex": latex, "display": display, "font": font,
1132
+ "size_pt": size_pt, "color": color, "align": align,
1133
+ "anchor": anchor, "margin_pt": margin_pt,
1134
+ }
1135
+ for name in defaults:
1136
+ if name not in kwargs:
1137
+ continue
1138
+ value = kwargs[name]
1139
+ if given[name] != defaults[name] and given[name] != value:
1140
+ raise TypeError(
1141
+ f"add_equation(): {name!r} given twice with different "
1142
+ f"values ({given[name]!r} vs {value!r})"
1143
+ )
1144
+ given[name] = value
1145
+ latex = given["latex"]
1146
+ display = given["display"]
1147
+ font = given["font"]
1148
+ size_pt = given["size_pt"]
1149
+ color = given["color"]
1150
+ align = given["align"]
1151
+ anchor = given["anchor"]
1152
+ margin_pt = given["margin_pt"]
1153
+
1154
+ if latex is None:
1155
+ raise TypeError(
1156
+ "add_equation() missing required argument: 'latex' "
1157
+ "(synonyms 'tex' / 'formula' / 'equation' accepted)"
1158
+ )
1159
+
1049
1160
 
1050
1161
  if len(bbox_or_positional) == 1 and isinstance(bbox_or_positional[0], BBox):
1051
1162
  box = bbox_or_positional[0]
@@ -1080,8 +1191,8 @@ class _BaseGroupShapes(_BaseShapes):
1080
1191
 
1081
1192
  def add_arrow(
1082
1193
  self,
1083
- start,
1084
- end,
1194
+ start=None,
1195
+ end=None,
1085
1196
  *,
1086
1197
  head: str | None = "triangle",
1087
1198
  tail: str | None = None,
@@ -1094,6 +1205,7 @@ class _BaseGroupShapes(_BaseShapes):
1094
1205
  inset_pt: float = 0.0,
1095
1206
  end_side: str = "auto",
1096
1207
  start_side: str = "auto",
1208
+ **kwargs,
1097
1209
  ) -> Connector:
1098
1210
  """Add an arrow connector with proper arrowhead and inset routing.
1099
1211
 
@@ -1116,12 +1228,18 @@ class _BaseGroupShapes(_BaseShapes):
1116
1228
 
1117
1229
  ``style`` is ``"solid"`` / ``"dashed"`` / ``"dotted"``.
1118
1230
 
1119
- ``route`` is ``"straight"`` (default), ``"elbow"``, or
1120
- ``"curved"`` — picks the underlying
1231
+ ``route`` is ``"straight"`` (default), ``"elbow"``, or ``"curved"``
1232
+ — picks the underlying
1121
1233
  :class:`~pptx2.enum.shapes.MSO_CONNECTOR_TYPE`.
1122
1234
 
1235
+ Keyword synonyms are absorbed: ``begin`` / ``from`` / ``source``
1236
+ for ``start``, ``to`` / ``target`` for ``end``, ``stroke_color`` /
1237
+ ``colour`` for ``color``, ``weight`` / ``line_weight`` for
1238
+ ``weight_pt`` — see :meth:`add_text` for the full aliasing story.
1239
+
1123
1240
  Returns the :class:`Connector` so callers can tweak further.
1124
1241
  """
1242
+ from pptx2._agent_friendly import absorb_agent_kwargs
1125
1243
  from pptx2._color import coerce_color
1126
1244
  from pptx2.enum.dml import (
1127
1245
  MSO_LINE_DASH_STYLE,
@@ -1131,6 +1249,64 @@ class _BaseGroupShapes(_BaseShapes):
1131
1249
  from pptx2.enum.shapes import MSO_CONNECTOR_TYPE
1132
1250
  from pptx2.util import Pt
1133
1251
 
1252
+ if kwargs:
1253
+ kwargs = absorb_agent_kwargs(
1254
+ "add_arrow",
1255
+ kwargs,
1256
+ (
1257
+ "start", "end", "head", "tail", "head_size", "tail_size",
1258
+ "color", "weight_pt", "style", "route", "inset_pt",
1259
+ "end_side", "start_side",
1260
+ ),
1261
+ )
1262
+ defaults = {
1263
+ "head": "triangle", "tail": None, "head_size": "medium",
1264
+ "tail_size": "medium", "color": None, "weight_pt": 1.5,
1265
+ "style": "solid", "route": "straight", "inset_pt": 0.0,
1266
+ "end_side": "auto", "start_side": "auto",
1267
+ }
1268
+ given = {
1269
+ "head": head, "tail": tail, "head_size": head_size,
1270
+ "tail_size": tail_size, "color": color, "weight_pt": weight_pt,
1271
+ "style": style, "route": route, "inset_pt": inset_pt,
1272
+ "end_side": end_side, "start_side": start_side,
1273
+ }
1274
+ for name in defaults:
1275
+ if name not in kwargs:
1276
+ continue
1277
+ value = kwargs[name]
1278
+ if given[name] != defaults[name] and given[name] != value:
1279
+ raise TypeError(
1280
+ f"add_arrow(): {name!r} given twice with different "
1281
+ f"values ({given[name]!r} vs {value!r})"
1282
+ )
1283
+ given[name] = value
1284
+ head = given["head"]
1285
+ tail = given["tail"]
1286
+ head_size = given["head_size"]
1287
+ tail_size = given["tail_size"]
1288
+ color = given["color"]
1289
+ weight_pt = given["weight_pt"]
1290
+ style = given["style"]
1291
+ route = given["route"]
1292
+ inset_pt = given["inset_pt"]
1293
+ end_side = given["end_side"]
1294
+ start_side = given["start_side"]
1295
+ # start/end synonyms (from=/to=/begin=/source=/target=)
1296
+ if "start" in kwargs:
1297
+ start = kwargs["start"]
1298
+ if "end" in kwargs:
1299
+ end = kwargs["end"]
1300
+ if start is None or end is None:
1301
+ raise TypeError(
1302
+ "add_arrow() missing required argument(s): "
1303
+ + ", ".join(
1304
+ name for name, value in (("start", start), ("end", end))
1305
+ if value is None
1306
+ )
1307
+ + " (synonyms begin/from/source and to/target accepted)"
1308
+ )
1309
+
1134
1310
  _CONNECTOR = {
1135
1311
  "straight": MSO_CONNECTOR_TYPE.STRAIGHT,
1136
1312
  "elbow": MSO_CONNECTOR_TYPE.ELBOW,
@@ -1331,6 +1507,7 @@ class _BaseGroupShapes(_BaseShapes):
1331
1507
  sp = self._spTree.add_textbox(id_, name, x, y, cx, cy)
1332
1508
  return sp
1333
1509
 
1510
+ @agent_friendly({"cols": ("columns", "num_cols", "col_count"), "rows": ("num_rows", "row_count")})
1334
1511
  def add_table(
1335
1512
  self,
1336
1513
  rows: int,
@@ -1382,6 +1559,7 @@ class _BaseGroupShapes(_BaseShapes):
1382
1559
  tbl.vert_banding = False
1383
1560
  return shape
1384
1561
 
1562
+ @agent_friendly({"movie_file": ("video", "video_file", "movie", "path", "file")})
1385
1563
  def add_movie(
1386
1564
  self,
1387
1565
  movie_file: str | IO[bytes],
@@ -449,3 +449,16 @@ print(slide.notes) # "" when the slide has no notes
449
449
  `start_slide_index` claims every slide from that position to the deck
450
450
  end. `move` raises `IndexError` out of range; `reorder` raises
451
451
  `ValueError` unless given a clean permutation.
452
+
453
+ ### Forgiving keyword arguments
454
+
455
+ Every shape helper (`add_text`, `add_shape`, `add_picture`,
456
+ `add_table`, `add_chart`, `add_textbox`, `add_connector`, `add_movie`,
457
+ `add_arrow`, `add_equation`, ...) and the diagram recipes
458
+ (`horizontal_pipeline`, `hub_and_spoke`, ...) absorb cross-library and
459
+ near-miss kwargs: `font_family=`, `halign=`/`valign=`, `fontsize=`,
460
+ `colour`, `x,y,w,h` geometry keywords, `shape_type=`, `image=`,
461
+ `columns=`, `data=`, `begin=`/`to=`, `items=`/`nodes=` ... Synonyms
462
+ substitute, never contradict (two different values for the same
463
+ logical argument still raise), and a truly unknown kwarg raises a
464
+ `TypeError` listing every accepted name.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-pptx2
3
- Version: 2.17.0
3
+ Version: 2.19.0
4
4
  Summary: Create, read, and update PowerPoint 2007+ (.pptx) files. Fork of power-pptx / python-pptx, published as python-pptx2.
5
5
  Author: Matěj Štágl
6
6
  Author-email: stagl@wattlescript.org
@@ -1,4 +1,5 @@
1
- pptx2/__init__.py,sha256=lLf03V_PykqZkDfLk7wwFUMFnN9JnqDoW3ziwibWXfE,4076
1
+ pptx2/__init__.py,sha256=SyJ-yjsqXoYHIjHWLRvKdyYTilNWX5KpkyHWIepB8HA,4076
2
+ pptx2/_agent_friendly.py,sha256=wYh9LmVuZMVlwOxzyd1mw0NmtPKQ2cTC21Y1Ww8ehO8,8834
2
3
  pptx2/_color.py,sha256=Ureg4dSEdHbwpVZ5eJIyt0kLx_W3mGftmznejth_oLQ,2889
3
4
  pptx2/_slide_importer.py,sha256=Ot3lW20eS7fMKsaGaeqPyeS9zVzt5D1WkovwozWwbYY,27033
4
5
  pptx2/_svg.py,sha256=pMURsUQaJ9-dqRl4nEZKt1VECNpA8Ghp4EwNRWcBG-c,5973
@@ -9,7 +10,7 @@ pptx2/action.py,sha256=BiDtM80P6oFsNOtLIT1nX82LXEvaGOg4Usaf5CA7kNY,9648
9
10
  pptx2/animation.py,sha256=g0OxAkCoGu0QEx3QB9Yu4BOnGLjD3tcLpKoYBAtUW6s,83756
10
11
  pptx2/api.py,sha256=n98a5qUhVxSwZ_yG_aXxtDCxtnEp37LPsSIsYsbwsfA,1841
11
12
  pptx2/audit.py,sha256=xg1QAs-ByW3xwBEcJ-Y9ptYe2WHKmVuVWAgRKQZfcr8,11276
12
- pptx2/diagrams.py,sha256=5NqGg-CMj6ZYoEsI9pBD1UHmwQhSAK8oAFYD89NPsVg,24654
13
+ pptx2/diagrams.py,sha256=4NmdFBg367hRjfFh7pQCVivScspmyvPnp94IyncJgCc,26277
13
14
  pptx2/exc.py,sha256=k6p-e9h5cKal09-sAv3E1QTXmVDuF8FqewW1CKFQmfw,1276
14
15
  pptx2/formats.py,sha256=w0b8YXBjBbPhDu1UV9-1iUJdL62d9_gcQSp9sHKVr_s,5081
15
16
  pptx2/geometry.py,sha256=X1tzwqTxXCMPPRpZhHSax6PKGy_-64MAudMI9h3ILJk,15221
@@ -136,12 +137,12 @@ pptx2/shapes/graphfrm.py,sha256=mwA9Wb4WRKBy6sA7KzOZFl3aHlpN-TmWRcE-kiMoqRI,1189
136
137
  pptx2/shapes/group.py,sha256=xlcGi3MAzBHX2IM16IATxRgw8NwsPFzTMVbRPYQ7OKg,11036
137
138
  pptx2/shapes/picture.py,sha256=U7rwiy7OXn0RgFSEdh07bdhIplWsI82OjFk-mt975J0,14938
138
139
  pptx2/shapes/placeholder.py,sha256=9tIeNscnV46hWyMr8ZSiZO3TZr-tt4kZgx6BRB0tCvc,17838
139
- pptx2/shapes/shapetree.py,sha256=bbrwutelMOcRX8pT88qRLvPyE3x6avIhripnUGalH1I,82851
140
+ pptx2/shapes/shapetree.py,sha256=ZXO1mE2-sMF8Era-EA4mT61Mcj2Hfsr8v-7xOZM_oBw,91287
140
141
  pptx2/skill/SKILL.md,sha256=6w7sWflZvhYYIzBsXLjx2uVKqB0793ZTvoZ_SUfzQxg,23668
141
142
  pptx2/skill/__init__.py,sha256=bUkfMETwf3K6J7GSE-tLQjdNziS_JceunComLiLIeGI,2665
142
143
  pptx2/skill/__main__.py,sha256=ohMLGy1jbjKV5_FqY6aGj4tm_isJ6fznvqUKiE1H6s8,1777
143
144
  pptx2/skill/references/animations.md,sha256=ca02l5XCtOQpD-uSCItQixGaW0yONIY0P55yy-T-J1Q,5875
144
- pptx2/skill/references/basics.md,sha256=jqg6YgXaP2qqpwQoLWPm2OTnECmJEfxuorNXVXY5sm4,14141
145
+ pptx2/skill/references/basics.md,sha256=O2ODa85fIK6mkom4aK2ehb2laEsogmtQ31RF5ckh6B0,14806
145
146
  pptx2/skill/references/charts.md,sha256=skCS4Kn1j49mlIQJYDAkcu2VHc3S1mzwqkGT3ZE94wg,8119
146
147
  pptx2/skill/references/compose.md,sha256=AfFwqAyQ9ggR140laqRDIUE3OcmTtXpIZH1oPcTOunA,8088
147
148
  pptx2/skill/references/design.md,sha256=ne3xVyIwqpkhEkd8XK74QEija040eHve5zBHBx9LeNo,11706
@@ -170,9 +171,9 @@ pptx2/text/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
170
171
  pptx2/text/fonts.py,sha256=ECa9HuBfeTqOi-uJTz_bu7eurC7e1_64RTwDatxmkpk,16498
171
172
  pptx2/text/layout.py,sha256=k8bqod6GyshHJtfoqFtH9W-eHTI43IB0e6SLT1ibSUU,12815
172
173
  pptx2/text/text.py,sha256=LPqF1B4GSQNSnSlVMm1v7pgtHCymOJPTUG_Kdt5_ou4,55039
173
- python_pptx2-2.17.0.dist-info/licenses/LICENSE,sha256=hLdud18ZPS9iSG3kBCBLiGYzj4A6zedCHiDejIDBXNg,1230
174
- python_pptx2-2.17.0.dist-info/METADATA,sha256=mx9AhEC2xmYL-p2zCSn6iHbmj4GaBnNVBBg81sHNg88,14374
175
- python_pptx2-2.17.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
176
- python_pptx2-2.17.0.dist-info/entry_points.txt,sha256=UpD1_pA-2BPeN0iDeD-u4NZ_yC78TvBdWu3V1xLsSRE,105
177
- python_pptx2-2.17.0.dist-info/top_level.txt,sha256=NDdhJ4rOySlDU-cw4g-bQMoZCiy41WszlRXmIiReSRs,6
178
- python_pptx2-2.17.0.dist-info/RECORD,,
174
+ python_pptx2-2.19.0.dist-info/licenses/LICENSE,sha256=hLdud18ZPS9iSG3kBCBLiGYzj4A6zedCHiDejIDBXNg,1230
175
+ python_pptx2-2.19.0.dist-info/METADATA,sha256=ttj2GNUUnqaxdWQqPvqwfN4lOn6KcRzMWtcBt1UnnCA,14374
176
+ python_pptx2-2.19.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
177
+ python_pptx2-2.19.0.dist-info/entry_points.txt,sha256=UpD1_pA-2BPeN0iDeD-u4NZ_yC78TvBdWu3V1xLsSRE,105
178
+ python_pptx2-2.19.0.dist-info/top_level.txt,sha256=NDdhJ4rOySlDU-cw4g-bQMoZCiy41WszlRXmIiReSRs,6
179
+ python_pptx2-2.19.0.dist-info/RECORD,,