python-pptx2 2.13.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 +152 -0
- pptx2/_color.py +75 -0
- pptx2/_slide_importer.py +597 -0
- pptx2/_svg.py +155 -0
- pptx2/_template_applier.py +292 -0
- pptx2/_textstyle.py +187 -0
- pptx2/accessibility.py +365 -0
- pptx2/action.py +270 -0
- pptx2/animation.py +2237 -0
- pptx2/api.py +49 -0
- pptx2/audit.py +258 -0
- pptx2/chart/__init__.py +0 -0
- pptx2/chart/analytics.py +381 -0
- pptx2/chart/axis.py +543 -0
- pptx2/chart/category.py +200 -0
- pptx2/chart/chart.py +670 -0
- pptx2/chart/data.py +864 -0
- pptx2/chart/datalabel.py +406 -0
- pptx2/chart/legend.py +86 -0
- pptx2/chart/marker.py +70 -0
- pptx2/chart/palettes.py +129 -0
- pptx2/chart/plot.py +462 -0
- pptx2/chart/point.py +101 -0
- pptx2/chart/quick_layouts.py +325 -0
- pptx2/chart/series.py +334 -0
- pptx2/chart/xlsx.py +272 -0
- pptx2/chart/xmlwriter.py +1845 -0
- pptx2/compose/__init__.py +28 -0
- pptx2/compose/from_spec.py +1094 -0
- pptx2/design/__init__.py +8 -0
- pptx2/design/components.py +607 -0
- pptx2/design/figures.py +389 -0
- pptx2/design/layout.py +370 -0
- pptx2/design/recipes.py +1967 -0
- pptx2/design/style.py +209 -0
- pptx2/design/tokens.py +915 -0
- pptx2/diagrams.py +754 -0
- pptx2/dml/__init__.py +0 -0
- pptx2/dml/chtfmt.py +40 -0
- pptx2/dml/color.py +496 -0
- pptx2/dml/effect.py +909 -0
- pptx2/dml/fill.py +691 -0
- pptx2/dml/line.py +287 -0
- pptx2/dml/picture.py +212 -0
- pptx2/dml/three_d.py +381 -0
- pptx2/enum/__init__.py +0 -0
- pptx2/enum/action.py +71 -0
- pptx2/enum/animation.py +31 -0
- pptx2/enum/base.py +218 -0
- pptx2/enum/chart.py +574 -0
- pptx2/enum/dml.py +740 -0
- pptx2/enum/lang.py +685 -0
- pptx2/enum/presentation.py +133 -0
- pptx2/enum/shapes.py +1029 -0
- pptx2/enum/text.py +230 -0
- pptx2/exc.py +42 -0
- pptx2/formats.py +139 -0
- pptx2/geometry.py +420 -0
- pptx2/inherit.py +109 -0
- pptx2/lint.py +2256 -0
- pptx2/math.py +177 -0
- pptx2/media.py +197 -0
- pptx2/opc/__init__.py +0 -0
- pptx2/opc/constants.py +332 -0
- pptx2/opc/oxml.py +188 -0
- pptx2/opc/package.py +762 -0
- pptx2/opc/packuri.py +109 -0
- pptx2/opc/serialized.py +296 -0
- pptx2/opc/shared.py +20 -0
- pptx2/opc/spec.py +45 -0
- pptx2/oxml/__init__.py +555 -0
- pptx2/oxml/action.py +53 -0
- pptx2/oxml/chart/__init__.py +0 -0
- pptx2/oxml/chart/axis.py +337 -0
- pptx2/oxml/chart/chart.py +481 -0
- pptx2/oxml/chart/datalabel.py +253 -0
- pptx2/oxml/chart/legend.py +72 -0
- pptx2/oxml/chart/marker.py +61 -0
- pptx2/oxml/chart/plot.py +365 -0
- pptx2/oxml/chart/series.py +425 -0
- pptx2/oxml/chart/shared.py +220 -0
- pptx2/oxml/coreprops.py +288 -0
- pptx2/oxml/dml/__init__.py +0 -0
- pptx2/oxml/dml/color.py +135 -0
- pptx2/oxml/dml/effect.py +213 -0
- pptx2/oxml/dml/fill.py +316 -0
- pptx2/oxml/dml/line.py +12 -0
- pptx2/oxml/dml/three_d.py +110 -0
- pptx2/oxml/ns.py +135 -0
- pptx2/oxml/presentation.py +313 -0
- pptx2/oxml/shapes/__init__.py +19 -0
- pptx2/oxml/shapes/autoshape.py +467 -0
- pptx2/oxml/shapes/connector.py +107 -0
- pptx2/oxml/shapes/graphfrm.py +347 -0
- pptx2/oxml/shapes/groupshape.py +329 -0
- pptx2/oxml/shapes/picture.py +270 -0
- pptx2/oxml/shapes/shared.py +577 -0
- pptx2/oxml/simpletypes.py +1027 -0
- pptx2/oxml/slide.py +563 -0
- pptx2/oxml/table.py +650 -0
- pptx2/oxml/text.py +815 -0
- pptx2/oxml/theme.py +36 -0
- pptx2/oxml/xmlchemy.py +717 -0
- pptx2/package.py +222 -0
- pptx2/parts/__init__.py +0 -0
- pptx2/parts/chart.py +95 -0
- pptx2/parts/coreprops.py +167 -0
- pptx2/parts/diagram.py +37 -0
- pptx2/parts/embeddedpackage.py +93 -0
- pptx2/parts/image.py +275 -0
- pptx2/parts/media.py +37 -0
- pptx2/parts/presentation.py +136 -0
- pptx2/parts/slide.py +371 -0
- pptx2/presentation.py +408 -0
- pptx2/py.typed +0 -0
- pptx2/render.py +586 -0
- pptx2/section.py +272 -0
- pptx2/shapes/__init__.py +26 -0
- pptx2/shapes/autoshape.py +442 -0
- pptx2/shapes/base.py +1078 -0
- pptx2/shapes/connector.py +297 -0
- pptx2/shapes/freeform.py +337 -0
- pptx2/shapes/graphfrm.py +316 -0
- pptx2/shapes/group.py +264 -0
- pptx2/shapes/picture.py +422 -0
- pptx2/shapes/placeholder.py +468 -0
- pptx2/shapes/shapetree.py +2027 -0
- pptx2/shared.py +82 -0
- pptx2/skill/SKILL.md +450 -0
- pptx2/skill/__init__.py +78 -0
- pptx2/skill/__main__.py +64 -0
- pptx2/skill/references/animations.md +189 -0
- pptx2/skill/references/basics.md +421 -0
- pptx2/skill/references/charts.md +254 -0
- pptx2/skill/references/compose.md +234 -0
- pptx2/skill/references/design.md +366 -0
- pptx2/skill/references/effects.md +249 -0
- pptx2/skill/references/end-to-end-deck.md +231 -0
- pptx2/skill/references/geometry-and-arrows.md +334 -0
- pptx2/skill/references/lint.md +275 -0
- pptx2/skill/references/math.md +86 -0
- pptx2/skill/references/picture-effects.md +129 -0
- pptx2/skill/references/render.md +151 -0
- pptx2/skill/references/smart-art.md +75 -0
- pptx2/skill/references/space-aware-authoring.md +249 -0
- pptx2/skill/references/tables.md +244 -0
- pptx2/skill/references/theme.md +127 -0
- pptx2/skill/references/three-d.md +109 -0
- pptx2/skill/references/transitions.md +100 -0
- pptx2/slide.py +1244 -0
- pptx2/smart_art.py +220 -0
- pptx2/spec.py +633 -0
- pptx2/table.py +1181 -0
- pptx2/table_styles.py +184 -0
- pptx2/templates/default.pptx +0 -0
- pptx2/templates/docx-icon.emf +0 -0
- pptx2/templates/generic-icon.emf +0 -0
- pptx2/templates/notes.xml +23 -0
- pptx2/templates/notesMaster.xml +352 -0
- pptx2/templates/pptx-icon.emf +0 -0
- pptx2/templates/theme.xml +321 -0
- pptx2/templates/xlsx-icon.emf +0 -0
- pptx2/text/__init__.py +0 -0
- pptx2/text/fonts.py +482 -0
- pptx2/text/layout.py +374 -0
- pptx2/text/text.py +1272 -0
- pptx2/theme.py +721 -0
- pptx2/types.py +36 -0
- pptx2/util.py +263 -0
- python_pptx2-2.13.0.dist-info/METADATA +351 -0
- python_pptx2-2.13.0.dist-info/RECORD +175 -0
- python_pptx2-2.13.0.dist-info/WHEEL +5 -0
- python_pptx2-2.13.0.dist-info/entry_points.txt +3 -0
- python_pptx2-2.13.0.dist-info/licenses/LICENSE +22 -0
- python_pptx2-2.13.0.dist-info/top_level.txt +1 -0
pptx2/shared.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Objects shared by pptx2 modules."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from pptx2.opc.package import XmlPart
|
|
9
|
+
from pptx2.oxml.xmlchemy import BaseOxmlElement
|
|
10
|
+
from pptx2.types import ProvidesPart
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ElementProxy(object):
|
|
14
|
+
"""Base class for lxml element proxy classes.
|
|
15
|
+
|
|
16
|
+
An element proxy class is one whose primary responsibilities are fulfilled by manipulating the
|
|
17
|
+
attributes and child elements of an XML element. They are the most common type of class in
|
|
18
|
+
python-pptx other than custom element (oxml) classes.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, element: BaseOxmlElement):
|
|
22
|
+
self._element = element
|
|
23
|
+
|
|
24
|
+
def __eq__(self, other: object) -> bool:
|
|
25
|
+
"""Return |True| if this proxy object refers to the same oxml element as does *other*.
|
|
26
|
+
|
|
27
|
+
ElementProxy objects are value objects and should maintain no mutable local state.
|
|
28
|
+
Equality for proxy objects is defined as referring to the same XML element, whether or not
|
|
29
|
+
they are the same proxy object instance.
|
|
30
|
+
"""
|
|
31
|
+
if not isinstance(other, ElementProxy):
|
|
32
|
+
return False
|
|
33
|
+
return self._element is other._element
|
|
34
|
+
|
|
35
|
+
def __ne__(self, other: object) -> bool:
|
|
36
|
+
if not isinstance(other, ElementProxy):
|
|
37
|
+
return True
|
|
38
|
+
return self._element is not other._element
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def element(self):
|
|
42
|
+
"""The lxml element proxied by this object."""
|
|
43
|
+
return self._element
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class ParentedElementProxy(ElementProxy):
|
|
47
|
+
"""Provides access to ancestor objects and part.
|
|
48
|
+
|
|
49
|
+
An ancestor may occasionally be required to provide a service, such as add or drop a
|
|
50
|
+
relationship. Provides the :attr:`_parent` attribute to subclasses and the public
|
|
51
|
+
:attr:`parent` read-only property.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def __init__(self, element: BaseOxmlElement, parent: ProvidesPart):
|
|
55
|
+
super(ParentedElementProxy, self).__init__(element)
|
|
56
|
+
self._parent = parent
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def parent(self):
|
|
60
|
+
"""The ancestor proxy object to this one.
|
|
61
|
+
|
|
62
|
+
For example, the parent of a shape is generally the |SlideShapes| object that contains it.
|
|
63
|
+
"""
|
|
64
|
+
return self._parent
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def part(self) -> XmlPart:
|
|
68
|
+
"""The package part containing this object."""
|
|
69
|
+
return self._parent.part
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class PartElementProxy(ElementProxy):
|
|
73
|
+
"""Provides common members for proxy-objects that wrap a part's root element, e.g. `p:sld`."""
|
|
74
|
+
|
|
75
|
+
def __init__(self, element: BaseOxmlElement, part: XmlPart):
|
|
76
|
+
super(PartElementProxy, self).__init__(element)
|
|
77
|
+
self._part = part
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def part(self) -> XmlPart:
|
|
81
|
+
"""The package part containing this object."""
|
|
82
|
+
return self._part
|
pptx2/skill/SKILL.md
ADDED
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: python-pptx2
|
|
3
|
+
description: Build PowerPoint (.pptx) decks from Python with the python-pptx2 library — a fork of power-pptx / python-pptx. Use this skill whenever the user wants to generate, mutate, lint, theme, animate, or render PowerPoint decks programmatically. The headline reason this line exists is **space-awareness**: text that doesn't overflow its box and shapes that don't slide off the edges of the slide. Reach for it especially when generation is dynamic (LLM, DB, CLI, JSON spec) and the deck has to look right without manual cleanup. Other features include native LaTeX equations, visual effects, animations, transitions, theme writer, design tokens, slide recipes, slide thumbnails, chart palettes, SVG embedding, 3D, and SmartArt text substitution.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# python-pptx2
|
|
7
|
+
|
|
8
|
+
`python-pptx2` is a fork of `power-pptx` (and, through it, `python-pptx`),
|
|
9
|
+
distributed on PyPI as `python-pptx2` and imported as `import pptx2`.
|
|
10
|
+
Use it for every PowerPoint generation / mutation task.
|
|
11
|
+
|
|
12
|
+
## The headline: space-aware authoring
|
|
13
|
+
|
|
14
|
+
The single biggest reason this fork exists is to make programmatic
|
|
15
|
+
decks **physically correct**: text doesn't overflow its container,
|
|
16
|
+
shapes don't sit off the slide, and elements that overlap do so on
|
|
17
|
+
purpose. Three layered tools — used together — catch ~all real-world
|
|
18
|
+
issues:
|
|
19
|
+
|
|
20
|
+
1. **`TextFrame.fit_text(...)`** measures with Pillow font metrics
|
|
21
|
+
and bakes a fitting size into the XML *before* save.
|
|
22
|
+
2. **`text_frame.auto_size = MSO_AUTO_SIZE.TEXT_TO_FIT_SHAPE`** lets
|
|
23
|
+
PowerPoint shrink at render time as a fallback.
|
|
24
|
+
3. **`slide.lint()`** catches what slipped through; `auto_fix()`
|
|
25
|
+
nudges off-slide shapes back inside.
|
|
26
|
+
|
|
27
|
+
**Read `references/space-aware-authoring.md` first** if the user is
|
|
28
|
+
generating decks from any dynamic input. It's the reason this skill
|
|
29
|
+
exists.
|
|
30
|
+
|
|
31
|
+
The whole upstream 1.0.2 API still works — the rest of this skill
|
|
32
|
+
focuses on the post-fork additions because they're what's most often
|
|
33
|
+
missed by snippets pulled from the wider internet.
|
|
34
|
+
|
|
35
|
+
## Cheat sheet (most common operations)
|
|
36
|
+
|
|
37
|
+
The 25 calls that cover ~90% of deck-generation tasks. Reach for
|
|
38
|
+
`references/geometry-and-arrows.md` for the full surface; this is the
|
|
39
|
+
working set:
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
from pptx2 import Presentation, BBox, audit
|
|
43
|
+
from pptx2.diagrams import horizontal_pipeline, hub_and_spoke, cycle
|
|
44
|
+
from pptx2.enum.shapes import MSO_SHAPE
|
|
45
|
+
from pptx2.util import Inches, Pt
|
|
46
|
+
|
|
47
|
+
# --- open / save ---
|
|
48
|
+
prs = Presentation() # new blank deck
|
|
49
|
+
prs = Presentation("file.pptx") # open existing
|
|
50
|
+
prs.save("out.pptx")
|
|
51
|
+
|
|
52
|
+
# --- slides ---
|
|
53
|
+
slide = prs.slides.add_slide(prs.slide_layouts[5]) # Title Only
|
|
54
|
+
slide = prs.slides.add_slide(prs.slide_layouts[6]) # Blank
|
|
55
|
+
|
|
56
|
+
# --- geometry (BBox is splattable into add_*) ---
|
|
57
|
+
bb = BBox.from_inches(1, 2, 8, 4)
|
|
58
|
+
left, right = bb.split_h([1, 1], gap=Inches(0.2))
|
|
59
|
+
inner = bb.inset(all=Inches(0.2))
|
|
60
|
+
|
|
61
|
+
# --- n-up rows/grids: never hand-compute (avail - (n-1)*gap) / n ---
|
|
62
|
+
for cell in bb.columns(3, gap=Pt(16)): # equal columns; .rows(n) too
|
|
63
|
+
slide.shapes.add_shape(MSO_SHAPE.ROUNDED_RECTANGLE, *cell)
|
|
64
|
+
|
|
65
|
+
# --- text (one call) ---
|
|
66
|
+
slide.shapes.add_text(bb, text="Hello",
|
|
67
|
+
size_pt=24, bold=True,
|
|
68
|
+
color="#0B5CFF", align="center")
|
|
69
|
+
|
|
70
|
+
# --- native equation from LaTeX (pip install "python-pptx2[math]") ---
|
|
71
|
+
slide.shapes.add_equation(bb, latex=r"\frac{a}{b}", size_pt=28)
|
|
72
|
+
para = slide.shapes.add_text(bb, text="Euler: ").text_frame.paragraphs[0]
|
|
73
|
+
para.add_math(r"e^{i\pi}+1=0")
|
|
74
|
+
|
|
75
|
+
# --- shape with chainable colour ---
|
|
76
|
+
slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, *bb) \
|
|
77
|
+
.fill_hex("#FFFFFF").line_hex("#0D0D0D", weight_pt=1.25)
|
|
78
|
+
|
|
79
|
+
# --- flat card: no theme drop-shadow, radius in points ---
|
|
80
|
+
card = slide.shapes.add_shape(MSO_SHAPE.ROUNDED_RECTANGLE, *inner)
|
|
81
|
+
card.shadow.clear() # kills the inherited effectRef too
|
|
82
|
+
card.corner_radius = Pt(6) # not adjustments[0]
|
|
83
|
+
|
|
84
|
+
# --- arrow with proper triangular head + auto edge routing ---
|
|
85
|
+
start_shape = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, *left)
|
|
86
|
+
end_shape = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, *right)
|
|
87
|
+
slide.shapes.add_arrow(start=start_shape, end=end_shape,
|
|
88
|
+
head="triangle", color="#0B5CFF", weight_pt=1.5)
|
|
89
|
+
|
|
90
|
+
# --- format-preserving text replacement (templated placeholders) ---
|
|
91
|
+
title_shape.set_text_preserving_format("New title")
|
|
92
|
+
|
|
93
|
+
# --- picture replacement (broken / sub-quality picture → native shapes) ---
|
|
94
|
+
picture.replace_with(lambda slide, bbox: ..., padding=Inches(0.1))
|
|
95
|
+
|
|
96
|
+
# --- diagram recipes ---
|
|
97
|
+
horizontal_pipeline(slide, bb, steps=["Extract", "Classify", "Enrich"])
|
|
98
|
+
hub_and_spoke(slide, bb, centre="Agent",
|
|
99
|
+
spokes=["Memory", "Tools", "Planning"])
|
|
100
|
+
|
|
101
|
+
# --- table styling without dropping to raw fill/font loops ---
|
|
102
|
+
table.format_cells(rows=0, fill="#1F2937", color="#FFFFFF", bold=True)
|
|
103
|
+
table.format_cells(rows=slice(1, None), size_pt=11, align="right")
|
|
104
|
+
|
|
105
|
+
# --- space-aware fit (returns the size it applied) ---
|
|
106
|
+
size = tf.fit_text(font_family="Inter", max_size=24)
|
|
107
|
+
|
|
108
|
+
# --- single-call cleanup before save ---
|
|
109
|
+
slide.tidy() # lints + safe auto-fixes
|
|
110
|
+
|
|
111
|
+
# --- tell the linter an overlap is deliberate (widest -> narrowest) ---
|
|
112
|
+
slide.lint_group_overlaps(card, accent, label) # one visual cluster
|
|
113
|
+
badge.allow_overlap_with(card) # exactly this one pair
|
|
114
|
+
card.layer, badge.layer_above = "card", "card" # also asserts z-order
|
|
115
|
+
|
|
116
|
+
# --- validate at save time (off by default) ---
|
|
117
|
+
prs.lint_on_save = "raise" # or "warn"; raises before writing
|
|
118
|
+
|
|
119
|
+
# --- whole-deck audit (markdown summary) ---
|
|
120
|
+
print(audit(prs).markdown())
|
|
121
|
+
|
|
122
|
+
# --- render thumbnails ---
|
|
123
|
+
from pptx2.render import render_slides
|
|
124
|
+
render_slides(prs, slides=[0, 1, 2], out_dir="thumbs",
|
|
125
|
+
name_template="slide-{:02d}.png")
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
That whole sheet fits on one screen — it's the working set.
|
|
129
|
+
|
|
130
|
+
## When to use this skill
|
|
131
|
+
|
|
132
|
+
- The user wants to **generate a deck** from Python or a JSON / dict spec
|
|
133
|
+
- The user is concerned about **text overflow** or **layout correctness**
|
|
134
|
+
in generated decks (lead with `space-aware-authoring.md`)
|
|
135
|
+
- The user wants to **add visual effects** (shadow, glow, soft edges,
|
|
136
|
+
blur, reflection, alpha) to shapes
|
|
137
|
+
- The user wants **animations**, **transitions**, or **motion paths**
|
|
138
|
+
- The user wants to **read or write a theme** (palette + fonts), or
|
|
139
|
+
apply one from a `.potx`
|
|
140
|
+
- The user wants to **lint / auto-fix** geometry issues
|
|
141
|
+
- The user wants to **import a slide** between decks or **apply a template**
|
|
142
|
+
- The user wants a **design system** (tokens, recipes, Grid/Stack layout)
|
|
143
|
+
- The user wants **chart palettes**, **quick layouts**, or per-series
|
|
144
|
+
gradient/pattern fills
|
|
145
|
+
- The user wants **slide thumbnails** rendered to PNG
|
|
146
|
+
- The user wants **3D** primitives (bevels / extrusion) or **SmartArt
|
|
147
|
+
text substitution**
|
|
148
|
+
- The user wants **native SVG embedding** with PNG fallback
|
|
149
|
+
|
|
150
|
+
## Install
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
pip install python-pptx2
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
The `cairosvg` dependency is optional — install only if you want
|
|
157
|
+
`add_svg_picture(...)` to auto-rasterise the PNG fallback. `pyyaml` is
|
|
158
|
+
optional too — install only if you want `DesignTokens.from_yaml`.
|
|
159
|
+
|
|
160
|
+
## Reference snippets
|
|
161
|
+
|
|
162
|
+
This skill ships a `references/` directory with focused recipe
|
|
163
|
+
collections. Read just the file you need — they're self-contained.
|
|
164
|
+
|
|
165
|
+
| File | What it covers |
|
|
166
|
+
|---|---|
|
|
167
|
+
| `references/space-aware-authoring.md` | **READ THIS FIRST.** Pre-flight measurement (`fit_text`, `TextFitter.best_fit_font_size`), `auto_size` flags, the linter, and a robust layout pattern. **Phase 2 + Phase 6 text-fit estimator.** |
|
|
168
|
+
| `references/geometry-and-arrows.md` | `BBox` value object (`columns`/`rows`/`split_h`/`grid`), `add_text` / `add_arrow` / `fill_hex` / `line_hex` convenience, `set_text_preserving_format`, `Picture.replace_with`, `Slide.tidy()`, diagram recipes (`horizontal_pipeline`, `hub_and_spoke`, `cycle`, `decision_tree`, `comparison_columns`), `audit(prs)`. **v2.8.** |
|
|
169
|
+
| `references/lint.md` | Detail on `slide.lint()`, issue types, `auto_fix`, and the `from_spec(..., lint="raise")` hook. **Phase 2.** |
|
|
170
|
+
| `references/design.md` | `DesignTokens`, `shape.style` facade, `Grid` / `Stack` layout primitives (geometry-safe placement), slide recipes (`title_slide`, `bullet_slide`, `kpi_slide`, `quote_slide`, `image_hero_slide`), starter pack. **Phase 9.** |
|
|
171
|
+
| `references/math.md` | Native PowerPoint equations from LaTeX (`add_equation`, `paragraph.add_math`). Requires `python-pptx2[math]`. **v2.13.** |
|
|
172
|
+
| `references/basics.md` | The 1.0.2 surface: `Presentation`, slides, placeholders, shapes, textboxes, tables, pictures, charts. Quick-reference cheatsheet. |
|
|
173
|
+
| `references/effects.md` | **The canonical fills-and-effects reference.** Shadow (including `shadow.clear()`), glow, soft edges, blur, reflection, alpha-tinted colors, gradient fills (linear / radial / rectangular / shape), line ends/caps/joins/compound. Other files link here rather than repeat it. **Phase 3 + Phase 6.** |
|
|
174
|
+
| `references/animations.md` | `Entrance` / `Exit` / `Emphasis` presets, triggers, by-paragraph reveal, sequencing context manager, motion paths. **Phase 5.** |
|
|
175
|
+
| `references/transitions.md` | Per-slide and deck-wide transitions including Morph and other `p14:` extensions. **Phase 4.** |
|
|
176
|
+
| `references/compose.md` | `from_spec` (JSON authoring with built-in lint), `import_slide`, `apply_template`. **Phase 2 + Phase 7.** |
|
|
177
|
+
| `references/theme.md` | Reading + writing the theme palette and fonts; `theme.apply(...)`; theme-aware color resolution via `pptx2.inherit.resolve_color`. **Phase 6 + Phase 7.** |
|
|
178
|
+
| `references/picture-effects.md` | Picture transparency / brightness / contrast / recolor (grayscale / sepia / washout / duotone) and SVG embedding. **Phase 6.** |
|
|
179
|
+
| `references/charts.md` | Chart palettes, quick layouts, per-series gradient/pattern fills, plus the inherited chart API. **Phase 10.** |
|
|
180
|
+
| `references/render.md` | Slide thumbnails via LibreOffice. **Phase 10.** |
|
|
181
|
+
| `references/three-d.md` | Bevels and extrusion via `shape.three_d`. **Phase 8.** |
|
|
182
|
+
| `references/smart-art.md` | Text substitution inside an existing template's SmartArt. **Phase 8.** |
|
|
183
|
+
| `references/tables.md` | The inherited table API, plus `cell.format(...)` / `table.format_cells(...)` styling, `Cell.borders`, and `fit_to_box`. |
|
|
184
|
+
| `references/end-to-end-deck.md` | A complete worked example: tokens, recipes, animations, transitions, charts, **and a lint pass before save**. |
|
|
185
|
+
|
|
186
|
+
## Top-level imports beyond `Presentation`
|
|
187
|
+
|
|
188
|
+
These are stable package-root re-exports — prefer them over deeper
|
|
189
|
+
import paths:
|
|
190
|
+
|
|
191
|
+
```python
|
|
192
|
+
from pptx2 import (
|
|
193
|
+
Presentation,
|
|
194
|
+
# Immutable rectangular region; splats into add_* APIs.
|
|
195
|
+
BBox,
|
|
196
|
+
# One-call deck audit (lint + picture + empty-slide + font checks).
|
|
197
|
+
audit, AuditReport,
|
|
198
|
+
# Figure adapters — Plotly / Matplotlib / SVG / HTML → slide picture.
|
|
199
|
+
# Third-party deps are imported lazily; missing deps surface a clear
|
|
200
|
+
# FigureBackendUnavailable with the right pip install command.
|
|
201
|
+
add_plotly_figure, add_matplotlib_figure,
|
|
202
|
+
add_svg_figure, add_html_figure,
|
|
203
|
+
FigureBackendUnavailable,
|
|
204
|
+
MathBackendUnavailable,
|
|
205
|
+
# Shape-level building blocks (token-driven; return small
|
|
206
|
+
# dataclasses exposing constituent shapes for further tweaks).
|
|
207
|
+
add_kpi_card, add_progress_bar,
|
|
208
|
+
KpiCard, ProgressBar,
|
|
209
|
+
)
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
## House rules for code you write
|
|
213
|
+
|
|
214
|
+
1. **Always `from pptx2 import Presentation`** — never invent another
|
|
215
|
+
import path.
|
|
216
|
+
2. **Default to space-aware patterns** for any text the user controls
|
|
217
|
+
at runtime: `fit_text` *or* `auto_size = TEXT_TO_FIT_SHAPE`, plus a
|
|
218
|
+
`slide.lint()` pass before save.
|
|
219
|
+
3. **Reads should not mutate.** All effect / color / line proxies in
|
|
220
|
+
python-pptx2 return `None` for unset properties; assign `None` to
|
|
221
|
+
clear.
|
|
222
|
+
4. **Use EMU through helpers**: `Inches`, `Pt`, `Emu`, `Cm` from
|
|
223
|
+
`pptx2.util`. Never write raw EMU integers when a helper exists.
|
|
224
|
+
5. **Use `BBox` / `Grid` / `Stack` for placement** when you have more
|
|
225
|
+
than two shapes on a slide — they compute geometry from the slide's
|
|
226
|
+
real dimensions (or a region you hand them), so you can't
|
|
227
|
+
accidentally walk off the right edge, and there's no column
|
|
228
|
+
arithmetic to get wrong.
|
|
229
|
+
6. **Prefer recipes for whole-slide layouts** when the user wants a
|
|
230
|
+
"good enough" pitch deck; drop down to direct `add_shape` /
|
|
231
|
+
`add_textbox` only when the recipes don't fit.
|
|
232
|
+
7. **Save once at the end** — build the deck in memory, then call
|
|
233
|
+
`prs.save(...)`. Don't open and re-save inside loops.
|
|
234
|
+
8. **For released-version constraints**: pin `python-pptx2>=2.8.0`
|
|
235
|
+
when generating requirements files — that's the minimum that
|
|
236
|
+
ships the `BBox`, `add_text`, `add_arrow`, `diagrams`, and
|
|
237
|
+
`audit` surface used in this skill.
|
|
238
|
+
|
|
239
|
+
## A space-aware mini-template
|
|
240
|
+
|
|
241
|
+
The pattern you'll reach for most often:
|
|
242
|
+
|
|
243
|
+
```python
|
|
244
|
+
from pptx2 import Presentation
|
|
245
|
+
from pptx2.enum.text import MSO_AUTO_SIZE
|
|
246
|
+
from pptx2.util import Inches
|
|
247
|
+
|
|
248
|
+
prs = Presentation()
|
|
249
|
+
slide = prs.slides.add_slide(prs.slide_layouts[5])
|
|
250
|
+
slide.shapes.title.text = "Q4 Review"
|
|
251
|
+
|
|
252
|
+
# Body box that has to swallow runtime-supplied text
|
|
253
|
+
box = slide.shapes.add_textbox(Inches(0.6), Inches(1.6),
|
|
254
|
+
Inches(12), Inches(5))
|
|
255
|
+
tf = box.text_frame
|
|
256
|
+
tf.word_wrap = True
|
|
257
|
+
tf.text = USER_SUPPLIED_BODY
|
|
258
|
+
|
|
259
|
+
# Belt: pick a determined size now using Pillow font metrics
|
|
260
|
+
tf.fit_text(font_family="Inter", max_size=24)
|
|
261
|
+
|
|
262
|
+
# Braces: let PowerPoint shrink on the way down if a user later edits
|
|
263
|
+
tf.auto_size = MSO_AUTO_SIZE.TEXT_TO_FIT_SHAPE
|
|
264
|
+
|
|
265
|
+
# Catch anything that slipped through. auto_fix() mutates the slide
|
|
266
|
+
# (nudges OffSlide shapes in, autofits overflowing frames, restacks
|
|
267
|
+
# contradicted layer declarations) and refreshes report.issues itself.
|
|
268
|
+
slide.lint().auto_fix()
|
|
269
|
+
report = slide.lint()
|
|
270
|
+
errors = [i for i in report.issues if i.severity.value == "error"]
|
|
271
|
+
if errors:
|
|
272
|
+
raise RuntimeError("\n".join(str(e) for e in errors))
|
|
273
|
+
|
|
274
|
+
prs.save("out.pptx")
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
## Recent additions worth knowing
|
|
278
|
+
|
|
279
|
+
These changes ship after v2.5 and are easy to miss:
|
|
280
|
+
|
|
281
|
+
- **`Chart.recolour(palette)`** is the recommended single entry
|
|
282
|
+
point — auto-dispatches per chart type (per-point on pie /
|
|
283
|
+
doughnut, per-series otherwise). `apply_palette` warns and
|
|
284
|
+
routes when called on a doughnut.
|
|
285
|
+
- **`Chart.line_color`** and **`Chart.apply_dark_theme(text=, line=)`**
|
|
286
|
+
pin axis lines + gridlines for dark-deck styling.
|
|
287
|
+
- **Horizontal bar charts (`BAR_*`)** now default to top-to-bottom
|
|
288
|
+
reading order (`reverse_order=True`). Override with
|
|
289
|
+
`chart.category_axis.reverse_order = False` for legacy ordering.
|
|
290
|
+
Column charts are unaffected.
|
|
291
|
+
- **`anchor=` keyword on `add_picture` / `add_shape` / `add_textbox`**
|
|
292
|
+
collapses corner / centre placement to one call (see
|
|
293
|
+
`references/basics.md`).
|
|
294
|
+
- **`add_table(..., style="clean")`** disables every inherited style
|
|
295
|
+
flag — use it whenever you'll set custom cell borders or fills.
|
|
296
|
+
- **`add_kpi_card(slide, ...)` / `add_progress_bar(slide, ...)`** —
|
|
297
|
+
shape-level building blocks beneath the slide-level recipes
|
|
298
|
+
(see `references/design.md`).
|
|
299
|
+
- **`shape.shadow.clear()`** is the way to guarantee *no* shadow.
|
|
300
|
+
Clearing the individual shadow properties (or the deprecated
|
|
301
|
+
`shadow.inherit = False`) leaves the shape's `<a:effectRef idx="2"/>`
|
|
302
|
+
in place — a soft drop shadow in most themes — so cards keep a
|
|
303
|
+
phantom shadow nobody asked for. `clear()` drops the explicit shadow
|
|
304
|
+
elements *and* re-points the effect reference, keeping glow / soft
|
|
305
|
+
edges / blur / reflection intact.
|
|
306
|
+
- **`shape.corner_radius`** reads and writes a rounded rectangle's
|
|
307
|
+
radius as a real length (`card.corner_radius = Pt(6)`), instead of the
|
|
308
|
+
`adjustments[0]` fraction-of-the-shorter-side that has to be eyeballed
|
|
309
|
+
per shape size.
|
|
310
|
+
- **`BBox.columns(n, gap=...)` / `BBox.rows(n, gap=...)`** replace the
|
|
311
|
+
`(available - (n - 1) * gap) / n` loop for card rows, stat grids, and
|
|
312
|
+
panels. `Grid.from_box(box, cols=..., rows=...)` puts a full grid over
|
|
313
|
+
a region rather than the whole slide.
|
|
314
|
+
- **`cell.format(...)` / `table.format_cells(rows=..., cols=..., ...)`**
|
|
315
|
+
style table cells with the same keywords as `add_text` (`fill`,
|
|
316
|
+
`color`, `bold`, `size_pt`, `align`, `anchor`, `margin`) — no more
|
|
317
|
+
`cell.fill.solid(); cell.fill.fore_color.rgb = ...` loops.
|
|
318
|
+
- **`fit_text` is explicit about fallback metrics.** It returns the size
|
|
319
|
+
it applied, and warns (`FontMetricsWarning`) when a *named* family
|
|
320
|
+
isn't installed and measurement silently drops to Pillow's default
|
|
321
|
+
font — naming one is what makes it audible, so omitting `font_family`
|
|
322
|
+
stays quiet while `font_family="Calibri"` does not. `strict=True`
|
|
323
|
+
raises instead; `pptx2.text.fonts.font_is_installed("Inter")`
|
|
324
|
+
checks up front.
|
|
325
|
+
- **Float coordinates from arithmetic are coerced** at constructor
|
|
326
|
+
entry and at `shape.left/top/width/height` setters, so
|
|
327
|
+
`(Inches(N) - gutter) / 2` style expressions can be passed straight
|
|
328
|
+
through. Pre-2.6.1 these produced float-valued `<a:off>` / `<a:ext>`
|
|
329
|
+
attributes that PowerPoint rejected with the "Repair?" dialog.
|
|
330
|
+
|
|
331
|
+
## Anti-patterns to avoid
|
|
332
|
+
|
|
333
|
+
LLM-generated python-pptx2 code falls into the same handful of traps.
|
|
334
|
+
Flagging them up front saves the trial-and-error round.
|
|
335
|
+
|
|
336
|
+
- **Don't** access `tf.paragraphs` twice and compare wrapper objects
|
|
337
|
+
with `is`. The property returns *fresh* `_Paragraph` objects every
|
|
338
|
+
call, so `p is not para` is always true — your filter will remove
|
|
339
|
+
the wrong paragraph. Use `set_text_preserving_format(new_text)` for
|
|
340
|
+
the common "replace text, keep formatting" case.
|
|
341
|
+
- **Don't** assume `add_connector(MSO_CONNECTOR.STRAIGHT, ...)` puts
|
|
342
|
+
an arrowhead on the line. It produces a bare line. Use
|
|
343
|
+
`slide.shapes.add_arrow(start, end, head="triangle")` — it sets the
|
|
344
|
+
arrowhead, inset, edge routing, and colour in one call.
|
|
345
|
+
- **Don't** size a diagram to a broken picture's bbox when there's an
|
|
346
|
+
enclosing card. The card area is what you want. Use
|
|
347
|
+
`picture.enclosing_container()` to find the right box, then
|
|
348
|
+
`picture.replace_with(builder)`.
|
|
349
|
+
- **Don't** delete a picture and then assume sibling shape indexes
|
|
350
|
+
are stable. Process in reverse index order, or capture the shapes
|
|
351
|
+
to mutate *before* iterating.
|
|
352
|
+
- **Don't** import `RGBColor` / `PP_ALIGN` / `MSO_VERTICAL_ANCHOR`
|
|
353
|
+
for every styling call. Use the hex-string and short-name kwargs:
|
|
354
|
+
`slide.shapes.add_text(bb, text="…", color="#0B5CFF", align="center",
|
|
355
|
+
anchor="middle")`. Hex strings, tuples, and `RGBColor` all work
|
|
356
|
+
everywhere a colour is accepted.
|
|
357
|
+
- **Don't** write raw EMU integers. `BBox.from_inches(1, 2, 8, 4)`
|
|
358
|
+
for regions, `Inches(1)` / `Pt(12)` for individual lengths. Float
|
|
359
|
+
arithmetic on EMU is fine — coordinates are coerced at the setter.
|
|
360
|
+
- **Don't** lint + auto_fix + lint again to clear safe issues. Use
|
|
361
|
+
`slide.tidy()` — it's the one-call wrapper.
|
|
362
|
+
- **Don't** hand-roll `col_w = (avail - (n - 1) * gap) / n` and a running
|
|
363
|
+
cursor for card rows or stat grids. `bb.columns(n, gap=Pt(16))` (and
|
|
364
|
+
`bb.rows(...)`, `bb.grid(cols, rows)`, `Grid.from_box(bb, cols=...)`)
|
|
365
|
+
return exact, drift-free boxes.
|
|
366
|
+
- **Don't** try to remove a shadow by assigning `None` to
|
|
367
|
+
`shadow.blur_radius` / `distance`, or by `shadow.inherit = False`.
|
|
368
|
+
Neither touches the theme effect style, so the shadow is still
|
|
369
|
+
rendered. Use `shape.shadow.clear()` — the only call that does.
|
|
370
|
+
- **Don't** set a corner radius by guessing `adjustments[0]` (it's a
|
|
371
|
+
fraction of the shorter side, so the same value means a different
|
|
372
|
+
radius on every differently-sized card). Use
|
|
373
|
+
`shape.corner_radius = Pt(6)`.
|
|
374
|
+
- **Don't** invent OMML / `a14:m` XML, and don't rasterise ordinary
|
|
375
|
+
formulas with matplotlib just to get them on a slide. Use
|
|
376
|
+
`slide.shapes.add_equation(bb, latex=r"\frac{a}{b}")` or
|
|
377
|
+
`paragraph.add_math(...)` (`pip install "python-pptx2[math]"`).
|
|
378
|
+
- **Don't** style table cells with `cell.fill.solid()` +
|
|
379
|
+
`cell.fill.fore_color.rgb` + per-run font loops. Use
|
|
380
|
+
`cell.format(...)` / `table.format_cells(...)` — and note that a
|
|
381
|
+
cell's anchor and insets belong on the cell, not on its text frame.
|
|
382
|
+
- **Don't** trust `fit_text` when the font isn't installed. The
|
|
383
|
+
measurement falls back to Pillow's default metrics and the result is
|
|
384
|
+
an estimate — bundle the `.ttf` and pass `font_file=`, or pass
|
|
385
|
+
`strict=True` so the build fails instead of shipping a guess.
|
|
386
|
+
|
|
387
|
+
## Common pitfalls
|
|
388
|
+
|
|
389
|
+
- **Calling `shape.shadow.inherit`** raises `DeprecationWarning`. Read
|
|
390
|
+
individual properties (`blur_radius`, `distance`, `direction`,
|
|
391
|
+
`color`) and check for `None` instead; to *remove* a shadow call
|
|
392
|
+
`shape.shadow.clear()`.
|
|
393
|
+
- **`fit_text` degrades to a best guess for uninstalled fonts.** Font
|
|
394
|
+
metrics come from the machine running the build, so a brand face that
|
|
395
|
+
isn't installed (the usual case in a container or CI) is measured with
|
|
396
|
+
Pillow's default font. Check with
|
|
397
|
+
`pptx2.text.fonts.font_is_installed(...)`, pass `font_file=` with
|
|
398
|
+
the real `.ttf`, or use `strict=True`. `slide.lint()` is unaffected —
|
|
399
|
+
its overflow check is font-agnostic — which is why the lint pass is
|
|
400
|
+
worth keeping even when the metrics are exact.
|
|
401
|
+
- **Bare-int sizes in `DesignTokens` typography** are interpreted as
|
|
402
|
+
**EMU**, not points. Use floats (`44.0`) or `Pt(44)` to mean
|
|
403
|
+
44-point font.
|
|
404
|
+
- **Recipes use the Blank layout**, so `slide.shapes.title` is `None`.
|
|
405
|
+
Address shapes by index (`slide.shapes[0]`, `slide.shapes[1]`, …).
|
|
406
|
+
- **`add_svg_picture` without `cairosvg` and without a `png_fallback`**
|
|
407
|
+
raises `CairoSvgUnavailable`. Either install cairosvg or supply a
|
|
408
|
+
pre-rasterised PNG.
|
|
409
|
+
- **`auto_fix()` repairs geometry, not judgment.** It clamps `OffSlide`,
|
|
410
|
+
flips `TextOverflow` frames to `TEXT_TO_FIT_SHAPE`, snaps
|
|
411
|
+
`OffGridDrift`, and restacks `LayerOrderViolation`. It will never
|
|
412
|
+
touch `ShapeCollision`, `LowContrast`, `MinFontSize` or
|
|
413
|
+
`ZOrderAnomaly` — those need a designer. Prefer `tf.fit_text(...)`
|
|
414
|
+
*before* save over relying on the overflow fix.
|
|
415
|
+
- **Slide thumbnails require `soffice` on PATH** (LibreOffice).
|
|
416
|
+
Otherwise you get `ThumbnailRendererUnavailable`.
|
|
417
|
+
- **`MSO_PATTERN_TYPE.ERCENT_40`** is the upstream typo and emits a
|
|
418
|
+
`DeprecationWarning`. Use `PERCENT_40`.
|
|
419
|
+
- **Calling `chart.apply_palette` on a pie / doughnut** emits a
|
|
420
|
+
`UserWarning` and routes through `color_by_category`. Use
|
|
421
|
+
`chart.recolour(palette)` directly to silence it.
|
|
422
|
+
|
|
423
|
+
## Where to look in the project
|
|
424
|
+
|
|
425
|
+
If the user has the `python-pptx2` repo checked out alongside this
|
|
426
|
+
skill, these paths are useful for source-of-truth lookup:
|
|
427
|
+
|
|
428
|
+
- `src/pptx2/lint.py` — `SlideLintReport`, `TextOverflow`, `OffSlide`,
|
|
429
|
+
`ShapeCollision`, `LayerOrderViolation`, `LintSeverity`.
|
|
430
|
+
- `src/pptx2/shapes/base.py` — `lint_group`, `lint_skip`,
|
|
431
|
+
`allow_overlap_with`, `layer` / `layer_above` (overlap intent).
|
|
432
|
+
- `src/pptx2/text/text.py`, `src/pptx2/text/layout.py` — `fit_text`,
|
|
433
|
+
`TextFitter`, `_best_fit_font_size`.
|
|
434
|
+
- `src/pptx2/animation.py` — `Entrance`, `Exit`, `Emphasis`,
|
|
435
|
+
`MotionPath`, `SlideAnimations`.
|
|
436
|
+
- `src/pptx2/compose/` — `from_spec`, plus the `import_slide` /
|
|
437
|
+
`apply_template` re-exports.
|
|
438
|
+
- `src/pptx2/theme.py`, `src/pptx2/inherit.py` — theme reader/writer and
|
|
439
|
+
`resolve_color`.
|
|
440
|
+
- `src/pptx2/dml/effect.py`, `src/pptx2/dml/picture.py`,
|
|
441
|
+
`src/pptx2/dml/line.py` — Phase 3/6 visual effects, picture filters,
|
|
442
|
+
line-end formatting.
|
|
443
|
+
- `src/pptx2/design/` — `tokens`, `style`, `layout`, `recipes`.
|
|
444
|
+
- `src/pptx2/chart/palettes.py`, `src/pptx2/chart/quick_layouts.py`.
|
|
445
|
+
- `src/pptx2/render.py` — slide-thumbnail renderer.
|
|
446
|
+
- `src/pptx2/smart_art.py`, `src/pptx2/_svg.py`.
|
|
447
|
+
- `examples/starter_pack/` — three example token sets and a build script.
|
|
448
|
+
|
|
449
|
+
The user-facing Sphinx documentation under `docs/user/` mirrors the
|
|
450
|
+
sections in this skill and is a good source of additional prose.
|
pptx2/skill/__init__.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Bundled Claude Code skill for python-pptx2.
|
|
2
|
+
|
|
3
|
+
Ships the ``SKILL.md`` and reference markdown that drive the Claude
|
|
4
|
+
Code (and Claude Agent SDK) ``python-pptx2`` skill, so that pip-installing
|
|
5
|
+
python-pptx2 is enough to make the skill available wherever the library
|
|
6
|
+
runs.
|
|
7
|
+
|
|
8
|
+
Typical usage from a shell::
|
|
9
|
+
|
|
10
|
+
# Install into the current user's Claude skills directory
|
|
11
|
+
python -m pptx2.skill install
|
|
12
|
+
|
|
13
|
+
# Or just print where the skill files live inside the package
|
|
14
|
+
python -m pptx2.skill path
|
|
15
|
+
|
|
16
|
+
Programmatic usage::
|
|
17
|
+
|
|
18
|
+
from pptx2.skill import skill_root, install_skill
|
|
19
|
+
|
|
20
|
+
src = skill_root() # pathlib.Path inside the package
|
|
21
|
+
dest = install_skill() # default: ~/.claude/skills/python-pptx2
|
|
22
|
+
dest = install_skill(target="/some/other/dir/python-pptx2")
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import shutil
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
from typing import Optional
|
|
30
|
+
|
|
31
|
+
__all__ = ["skill_root", "install_skill", "DEFAULT_INSTALL_DIR"]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
#: Default destination directory for ``install_skill``.
|
|
35
|
+
#:
|
|
36
|
+
#: Matches the convention that Claude Code uses for user-level skills.
|
|
37
|
+
#: Override by passing ``target=`` to :func:`install_skill`.
|
|
38
|
+
DEFAULT_INSTALL_DIR = Path.home() / ".claude" / "skills" / "python-pptx2"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def skill_root() -> Path:
|
|
42
|
+
"""Return the directory inside the installed package that holds the skill.
|
|
43
|
+
|
|
44
|
+
Contains ``SKILL.md`` and a ``references/`` subdirectory. This is
|
|
45
|
+
a regular filesystem path even when the package is installed inside
|
|
46
|
+
a wheel, because setuptools copies ``package_data`` files onto disk.
|
|
47
|
+
"""
|
|
48
|
+
return Path(__file__).resolve().parent
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def install_skill(
|
|
52
|
+
target: Optional[Path] = None, *, overwrite: bool = True
|
|
53
|
+
) -> Path:
|
|
54
|
+
"""Copy the bundled skill to *target* (default :data:`DEFAULT_INSTALL_DIR`).
|
|
55
|
+
|
|
56
|
+
Returns the destination directory. When *overwrite* is ``True``
|
|
57
|
+
(the default), an existing skill at *target* is replaced; pass
|
|
58
|
+
``overwrite=False`` to raise :class:`FileExistsError` instead.
|
|
59
|
+
"""
|
|
60
|
+
src = skill_root()
|
|
61
|
+
dest = Path(target) if target is not None else DEFAULT_INSTALL_DIR
|
|
62
|
+
|
|
63
|
+
if dest.exists():
|
|
64
|
+
if not overwrite:
|
|
65
|
+
raise FileExistsError(
|
|
66
|
+
f"refusing to overwrite existing skill at {dest!s}; "
|
|
67
|
+
"pass overwrite=True or remove it first"
|
|
68
|
+
)
|
|
69
|
+
shutil.rmtree(dest)
|
|
70
|
+
|
|
71
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
72
|
+
# Copy SKILL.md and references/ but not __init__.py / __main__.py.
|
|
73
|
+
dest.mkdir()
|
|
74
|
+
shutil.copy2(src / "SKILL.md", dest / "SKILL.md")
|
|
75
|
+
refs_src = src / "references"
|
|
76
|
+
if refs_src.is_dir():
|
|
77
|
+
shutil.copytree(refs_src, dest / "references")
|
|
78
|
+
return dest
|
pptx2/skill/__main__.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Command-line entry point for the bundled Claude skill.
|
|
2
|
+
|
|
3
|
+
Usage::
|
|
4
|
+
|
|
5
|
+
python -m pptx2.skill install [TARGET] # copy skill into target dir
|
|
6
|
+
python -m pptx2.skill path # print on-disk skill source
|
|
7
|
+
python -m pptx2.skill # same as `path`
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import argparse
|
|
13
|
+
import sys
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from pptx2.skill import DEFAULT_INSTALL_DIR, install_skill, skill_root
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def main(argv: list[str] | None = None) -> int:
|
|
20
|
+
parser = argparse.ArgumentParser(
|
|
21
|
+
prog="python -m pptx2.skill",
|
|
22
|
+
description=(
|
|
23
|
+
"Manage the bundled python-pptx2 Claude skill. "
|
|
24
|
+
"Ships SKILL.md and reference docs to your Claude skills directory."
|
|
25
|
+
),
|
|
26
|
+
)
|
|
27
|
+
sub = parser.add_subparsers(dest="cmd")
|
|
28
|
+
|
|
29
|
+
install = sub.add_parser(
|
|
30
|
+
"install", help="copy the bundled skill into a Claude skills directory"
|
|
31
|
+
)
|
|
32
|
+
install.add_argument(
|
|
33
|
+
"target",
|
|
34
|
+
nargs="?",
|
|
35
|
+
type=Path,
|
|
36
|
+
default=None,
|
|
37
|
+
help=f"destination directory (default: {DEFAULT_INSTALL_DIR})",
|
|
38
|
+
)
|
|
39
|
+
install.add_argument(
|
|
40
|
+
"--no-overwrite",
|
|
41
|
+
action="store_true",
|
|
42
|
+
help="error out if the destination already exists",
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
sub.add_parser("path", help="print the package-internal skill directory")
|
|
46
|
+
|
|
47
|
+
args = parser.parse_args(argv)
|
|
48
|
+
|
|
49
|
+
if args.cmd == "install":
|
|
50
|
+
try:
|
|
51
|
+
dest = install_skill(args.target, overwrite=not args.no_overwrite)
|
|
52
|
+
except FileExistsError as exc:
|
|
53
|
+
print(str(exc), file=sys.stderr)
|
|
54
|
+
return 1
|
|
55
|
+
print(f"installed python-pptx2 skill -> {dest}")
|
|
56
|
+
return 0
|
|
57
|
+
|
|
58
|
+
# default + `path`
|
|
59
|
+
print(skill_root())
|
|
60
|
+
return 0
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
if __name__ == "__main__":
|
|
64
|
+
raise SystemExit(main())
|