Skip to content
Open
16 changes: 13 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -97,9 +97,6 @@ python_functions = [
"and_",
]

[tool.ruff]
line-length = 100

# -- don't check these locations --
exclude = [
# -- docs/ - documentation Python code is incidental --
Expand All @@ -112,6 +109,10 @@ exclude = [
"spec",
]

[tool.ruff]
line-length = 100
target-version = "py313"

[tool.ruff.lint]
select = [
"C4", # -- flake8-comprehensions --
Expand All @@ -128,6 +129,7 @@ select = [
"UP032", # -- Use f-string instead of `.format()` call --
"UP034", # -- Avoid extraneous parentheses --
"W", # -- Warnings, including invalid escape-sequence --

]
ignore = [
"COM812", # -- over aggressively insists on trailing commas where not desireable --
Expand All @@ -137,6 +139,14 @@ ignore = [
"PT012", # -- pytest.raises() block should contain a single simple statement --
"SIM117", # -- merge `with` statements for context managers that have same scope --
]
extend-select = ["E","F","I"] # basic errors + imports
[tool.ruff.lint.per-file-ignores]
"tests/**" = [
"PT006","PT007","PT018",
"C401","C402","C404","C416","C420",
"UP015",
"E501",
]

[tool.ruff.lint.isort]
known-first-party = ["pptx"]
Expand Down
8 changes: 8 additions & 0 deletions pytest.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[pytest]
testpaths = tests
norecursedirs =
tests/test_files
addopts = -q --cov=pptx --cov-report=term-missing --cov-fail-under=60
python_files = test_*.py
python_functions = test_*
python_classes = Test*
70 changes: 70 additions & 0 deletions tests/test_chart_labels_legend.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import io

from pptx import Presentation
from pptx.chart.data import CategoryChartData
from pptx.enum.chart import XL_CHART_TYPE


def _blank_slide(prs: Presentation):
# Blank layout
return prs.slides.add_slide(prs.slide_layouts[6])


def _add_line_chart(prs: Presentation):
slide = _blank_slide(prs)
chart_data = CategoryChartData()
chart_data.categories = ["A", "B", "C"]
chart_data.add_series("S1", (1, 2, 3))
x, y, cx, cy = 1_000_000, 1_000_000, 6_000_000, 4_000_000
shape = slide.shapes.add_chart(
XL_CHART_TYPE.LINE_MARKERS, x, y, cx, cy, chart_data
)
return shape.chart


def _first_chart(slide):
for shp in slide.shapes:
if hasattr(shp, "chart"):
return shp.chart
raise AssertionError("No chart found on slide")


def test_datalabels_toggle_and_number_format_roundtrip():
prs = Presentation()
chart = _add_line_chart(prs)
plot = chart.plots[0]

plot.has_data_labels = True
dlabels = plot.data_labels
dlabels.show_value = True
dlabels.show_category_name = False
dlabels.show_series_name = False
dlabels.number_format = "#,##0.00"

buf = io.BytesIO()
prs.save(buf)
buf.seek(0)
prs2 = Presentation(buf)

chart2 = _first_chart(prs2.slides[0])
d2 = chart2.plots[0].data_labels

assert d2.show_value is True
assert (d2.show_category_name or False) is False
assert (d2.show_series_name or False) is False
assert d2.number_format in ("#,##0.00",)


def test_line_chart_defaults_legend_and_vary_by_categories():
prs = Presentation()
slide = _blank_slide(prs)
data = CategoryChartData()
data.categories = ["Q1", "Q2"]
data.add_series("S1", (1, 2))
shape = slide.shapes.add_chart(
XL_CHART_TYPE.LINE_MARKERS, 0, 0, 6_000_000, 4_000_000, data
)
chart = shape.chart

assert chart.has_legend is True
assert chart.plots[0].vary_by_categories is False
97 changes: 97 additions & 0 deletions tests/test_hyperlinks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import io
from pathlib import Path

from pptx import Presentation
from pptx.enum.shapes import MSO_AUTO_SHAPE_TYPE


def _blank_slide(prs: Presentation):
# Blank layout
return prs.slides.add_slide(prs.slide_layouts[6])


def _roundtrip(prs: Presentation) -> Presentation:
buf = io.BytesIO()
prs.save(buf)
buf.seek(0)
return Presentation(buf)


def _last_shape(slide):
return slide.shapes[-1]


def test_hyperlink_http_roundtrip():
prs = Presentation()
slide = _blank_slide(prs)

shp = slide.shapes.add_shape(
MSO_AUTO_SHAPE_TYPE.RECTANGLE, 1_000_000, 1_000_000, 2_000_000, 1_000_000
)
shp.click_action.hyperlink.address = "https://example.com/page#section"

prs2 = _roundtrip(prs)
shp2 = _last_shape(prs2.slides[0])
addr = shp2.click_action.hyperlink.address or ""

# Keep it simple and short to avoid E501
assert "example.com/page#section" in addr


def test_hyperlink_file_uri_roundtrip(tmp_path: Path):
prs = Presentation()
slide = _blank_slide(prs)

file_path = tmp_path / "doc.txt"
file_path.write_text("hello")
uri = file_path.as_uri()

shp = slide.shapes.add_shape(
MSO_AUTO_SHAPE_TYPE.ROUNDED_RECTANGLE, 0, 0, 2_000_000, 1_000_000
)
shp.click_action.hyperlink.address = uri

prs2 = _roundtrip(prs)
shp2 = _last_shape(prs2.slides[0])
addr = shp2.click_action.hyperlink.address or ""

assert addr.startswith("file:")
assert file_path.name in addr


def test_hyperlink_mailto_roundtrip():
prs = Presentation()
slide = _blank_slide(prs)

shp = slide.shapes.add_shape(
MSO_AUTO_SHAPE_TYPE.ROUNDED_RECTANGLE, 0, 0, 2_000_000, 1_000_000
)
shp.click_action.hyperlink.address = "mailto:foo@example.com?subject=Hello"

prs2 = _roundtrip(prs)
shp2 = _last_shape(prs2.slides[0])
addr = shp2.click_action.hyperlink.address or ""

# PT018: break assertions
assert addr.startswith("mailto:")
assert "subject=Hello" in addr


def test_hyperlink_slide_anchor_via_target_slide_roundtrip():
prs = Presentation()
slide1 = _blank_slide(prs)
slide2 = _blank_slide(prs)

shp = slide1.shapes.add_shape(
MSO_AUTO_SHAPE_TYPE.ROUNDED_RECTANGLE, 0, 0, 2_000_000, 1_000_000
)
# was: shp.click_action.hyperlink.target_slide = slide2
shp.click_action.target_slide = slide2

prs2 = _roundtrip(prs)
shp2 = _last_shape(prs2.slides[0])
# was: target = shp2.click_action.hyperlink.target_slide
target = shp2.click_action.target_slide

assert target is prs2.slides[1]