diff --git a/pyproject.toml b/pyproject.toml index 400cb6bf..10010986 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -97,9 +97,6 @@ python_functions = [ "and_", ] -[tool.ruff] -line-length = 100 - # -- don't check these locations -- exclude = [ # -- docs/ - documentation Python code is incidental -- @@ -112,6 +109,10 @@ exclude = [ "spec", ] +[tool.ruff] +line-length = 100 +target-version = "py313" + [tool.ruff.lint] select = [ "C4", # -- flake8-comprehensions -- @@ -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 -- @@ -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"] diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 00000000..283339c6 --- /dev/null +++ b/pytest.ini @@ -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* diff --git a/tests/test_chart_labels_legend.py b/tests/test_chart_labels_legend.py new file mode 100644 index 00000000..8dfee263 --- /dev/null +++ b/tests/test_chart_labels_legend.py @@ -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 diff --git a/tests/test_hyperlinks.py b/tests/test_hyperlinks.py new file mode 100644 index 00000000..d369ef11 --- /dev/null +++ b/tests/test_hyperlinks.py @@ -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] +