From 4e1eb8652b24f7d9651fdcb49f3f00adeabac26a Mon Sep 17 00:00:00 2001 From: Daizu Date: Sun, 7 Jul 2024 22:17:43 +0900 Subject: [PATCH] borders --- src/pptxlib/app.py | 12 ++-- src/pptxlib/core.py | 7 ++- src/pptxlib/lines.py | 37 +++++++++++++ src/pptxlib/shapes.py | 20 ++++--- src/pptxlib/tables.py | 73 ++++++++++++++++--------- tests/conftest.py | 12 +++- tests/test_app.py | 32 +++++++++-- tests/test_lines.py | 13 +++++ tests/test_shapes.py | 35 ++++++++++-- tests/test_tables.py | 124 +++++++++++++++++++++++++++++++++++++++++- 10 files changed, 314 insertions(+), 51 deletions(-) create mode 100644 src/pptxlib/lines.py create mode 100644 tests/test_lines.py diff --git a/src/pptxlib/app.py b/src/pptxlib/app.py index 9d502a4..d4abb45 100644 --- a/src/pptxlib/app.py +++ b/src/pptxlib/app.py @@ -69,7 +69,11 @@ def active(self) -> Presentation: @dataclass(repr=False) class Slide(Element): - parent: Slides + parent: Presentation + + @classmethod + def get_parent(cls, parent: Slides) -> Presentation: + return parent.parent @property def shapes(self) -> Shapes: @@ -90,11 +94,11 @@ def title(self, text): @property def width(self) -> float: - return self.parent.parent.width + return self.parent.width @property def height(self) -> float: - return self.parent.parent.height + return self.parent.height class Slides(Collection[Slide]): @@ -120,7 +124,7 @@ def add(self, index: int | None = None, layout=None): else: slide = self.api.AddSlide(index, layout) - return Slide(slide, self) + return Slide(slide, self.parent) @property def active(self): diff --git a/src/pptxlib/core.py b/src/pptxlib/core.py index 46590f1..e2ac546 100644 --- a/src/pptxlib/core.py +++ b/src/pptxlib/core.py @@ -45,6 +45,10 @@ def select(self): def delete(self): self.api.Delete() + @classmethod + def get_parent(cls, parent: Collection) -> Base: + return parent + SomeElement = TypeVar("SomeElement", bound=Element) @@ -65,7 +69,8 @@ def __call__(self, index: int | None = None) -> SomeElement: if index is None: index = len(self) - return self.type(self.api(index), self) # type: ignore + parent = self.type.get_parent(self) + return self.type(self.api(index), parent) # type: ignore def __iter__(self) -> Iterator[SomeElement]: for index in range(len(self)): diff --git a/src/pptxlib/lines.py b/src/pptxlib/lines.py new file mode 100644 index 0000000..3e361a3 --- /dev/null +++ b/src/pptxlib/lines.py @@ -0,0 +1,37 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import TYPE_CHECKING + +from pptxlib.core import Element + +if TYPE_CHECKING: + from pptxlib.tables import Borders + + +@dataclass(repr=False) +class LineFormat(Element): + parent: Borders + + +# def get_borders( +# cell: Cell | CellRange, +# border_type: Literal["bottom","left","right","top"], +# width: float = 1, +# color: int | str | tuple[int, int, int] = 0, +# line_style: Literal["-", "--"] = "-", +# *, +# visible: bool = True, +# ): +# border_type_int = getattr(constants, "ppBorder" + border_type[0].upper() + border_type[1:]) +# border = cell.api.Borders(border_type_int) +# border.Visible = visible + +# if not visible: +# return + +# border.Weight = width +# border.ForeColor.RGB = color + +# if line_style == "--": +# border.DashStyle = constants.msoLineDash" diff --git a/src/pptxlib/shapes.py b/src/pptxlib/shapes.py index 94a342c..522da65 100644 --- a/src/pptxlib/shapes.py +++ b/src/pptxlib/shapes.py @@ -16,7 +16,11 @@ class Shape(Element): - parent: Shapes | Cell + parent: Slide | Cell + + @classmethod + def get_parent(cls, parent: Shapes) -> Slide: + return parent.parent @property def text_range(self) -> DispatchBaseClass: @@ -32,8 +36,10 @@ def text(self, text: str) -> None: @property def slide(self) -> Slide: - if isinstance(self.parent, Shapes): - return self.parent.parent + from pptxlib.app import Slide + + if isinstance(self.parent, Slide): + return self.parent raise NotImplementedError @@ -189,7 +195,7 @@ class Shapes(Collection[Shape]): @property def title(self) -> Shape: - return Shape(self.api.Title, self) + return Shape(self.api.Title, Shape.get_parent(self)) def add( self, @@ -205,7 +211,7 @@ def add( kind = getattr(constants, f"msoShape{kind}") api = self.api.AddShape(kind, left, top, width, height) - shape = Shape(api, self) + shape = Shape(api, Shape.get_parent(self)) shape.text = text shape.set_style(**kwargs) @@ -228,7 +234,7 @@ def add_label( if auto_size is False: api.TextFrame.AutoSize = False - shape = Shape(api, self) + shape = Shape(api, Shape.get_parent(self)) shape.text = text shape.set_style(**kwargs) @@ -244,7 +250,7 @@ def add_table( height: float = 100, ) -> Shape: api = self.api.AddTable(num_rows, num_columns, left, top, width, height) - return Shape(api, self) + return Shape(api, Shape.get_parent(self)) # def add_picture(self, path=None, left=0, top=0, width=None, height=None, scale=1, **kwargs): diff --git a/src/pptxlib/tables.py b/src/pptxlib/tables.py index 56a2d94..4fa21cc 100644 --- a/src/pptxlib/tables.py +++ b/src/pptxlib/tables.py @@ -6,6 +6,7 @@ from win32com.client import constants from pptxlib.core import Collection, Element +from pptxlib.lines import LineFormat from pptxlib.shapes import Shape if TYPE_CHECKING: @@ -109,7 +110,7 @@ def __iter__(self) -> Iterator[Table]: api = self.api(index + 1) # type: ignore if api.HasTable: - yield Table(api.Table, Shape(api, self.parent.shapes)) + yield Table(api.Table, Shape(api, self.parent)) def __len__(self) -> int: return len(list(iter(self))) @@ -131,12 +132,16 @@ def add( height: float = 100, ) -> Table: api = self.api.AddTable(num_rows, num_columns, left, top, width, height) - return Table(api.Table, Shape(api, self.parent.shapes)) + return Table(api.Table, Shape(api, self.parent)) @dataclass(repr=False) class Row(Element): - parent: Rows + parent: Table + + @classmethod + def get_parent(cls, parent: Rows) -> Table: + return parent.parent @property def height(self) -> float: @@ -146,6 +151,10 @@ def height(self) -> float: def height(self, value: float) -> None: self.api.Height = value + @property + def cells(self) -> CellRange: + return CellRange(self.api.Cells, self) + @dataclass(repr=False) class Rows(Collection[Row]): @@ -164,7 +173,11 @@ def height(self, value: list[float]) -> None: @dataclass(repr=False) class Column(Element): - parent: Columns + parent: Table + + @classmethod + def get_parent(cls, parent: Columns) -> Table: + return parent.parent @property def width(self) -> float: @@ -174,6 +187,10 @@ def width(self) -> float: def width(self, value: float) -> None: self.api.Width = value + @property + def cells(self) -> CellRange: + return CellRange(self.api.Cells, self) + @dataclass(repr=False) class Columns(Collection[Column]): @@ -230,27 +247,33 @@ def value(self): def value(self, value): self.text = value - def set_border( - self, - pos: str, - width: float = 1, - color: int | str | tuple[int, int, int] = 0, - line_style: Literal["-", "--"] = "-", - *, - visible: bool = True, - ): - pos = getattr(constants, "ppBorder" + pos[0].upper() + pos[1:]) - border = self.api.Borders(pos) - border.Visible = visible - - if not visible: - return - - border.Weight = width - border.ForeColor.RGB = color - - if line_style == "--": - border.DashStyle = constants.msoLineDash + @property + def borders(self) -> Borders: + borders = Borders(self) # type: ignore + borders.parent = self.parent + return borders + + +@dataclass(repr=False) +class CellRange(Element): + parent: Row | Column + + @property + def borders(self) -> Borders: + borders = Borders(self) # type: ignore + borders.parent = self.parent.parent + return borders + + +@dataclass(repr=False) +class Borders(Collection[LineFormat]): + parent: Table + type: ClassVar[type[Element]] = LineFormat + + def __call__(self, type: Literal["bottom", "left", "right", "top"]) -> LineFormat: # noqa: A002 + type_int = getattr(constants, "ppBorder" + type[0].upper() + type[1:]) + api = self.api(type_int) # type: ignore + return LineFormat(api, self) # from win32com.client import constants diff --git a/tests/conftest.py b/tests/conftest.py index 9470f49..de4935a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,7 +2,7 @@ from pptxlib.app import PowerPoint, Presentation, Presentations, Slide, Slides from pptxlib.shapes import Shapes -from pptxlib.tables import Table, Tables +from pptxlib.tables import Rows, Table, Tables @pytest.fixture(scope="session") @@ -74,3 +74,13 @@ def rows(table: Table): @pytest.fixture def columns(table: Table): return table.columns + + +@pytest.fixture +def cell(table: Table): + return table.cell(1, 1) + + +@pytest.fixture +def cell_range(rows: Rows): + return rows(1).cells diff --git a/tests/test_app.py b/tests/test_app.py index 4f39804..089dd6c 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -125,21 +125,43 @@ def test_slide_title(slide: Slide): assert slide.title == "Slide Title" -def test_repr_powerpoint(pp: PowerPoint): +def test_powerpoint_repr(pp: PowerPoint): assert repr(pp) == "" -def test_repr_presentations(prs: Presentations): +def test_presentations_repr(prs: Presentations): assert repr(prs) == "" -def test_repr_presentation(pr: Presentation): +def test_presentation_repr(pr: Presentation): assert repr(pr).startswith("" -def test_repr_slide(slide: Slide): +def test_slide_repr(slide: Slide): assert repr(slide) == "" + + +def test_presentations_parent(prs: Presentations): + assert prs.api.Parent.__class__.__name__ == "_Application" + assert prs.parent.__class__.__name__ == "PowerPoint" + + +def test_presentation_parent(pr: Presentation, prs: Presentations): + assert pr.api.Parent.__class__.__name__ == "Presentations" + assert pr.parent.__class__.__name__ == "Presentations" + assert prs(1).parent.__class__.__name__ == "Presentations" + + +def test_slides_parent(slides: Slides): + assert slides.api.Parent.__class__.__name__ == "_Presentation" + assert slides.parent.__class__.__name__ == "Presentation" + + +def test_slide_parent(slide: Slide, slides: Slides): + assert slide.api.Parent.__class__.__name__ == "_Presentation" + assert slide.parent.__class__.__name__ == "Presentation" + assert slides(1).parent.__class__.__name__ == "Presentation" diff --git a/tests/test_lines.py b/tests/test_lines.py new file mode 100644 index 0000000..1a89657 --- /dev/null +++ b/tests/test_lines.py @@ -0,0 +1,13 @@ +from pptxlib.tables import Cell, CellRange + + +def test_cell_border(cell: Cell): + line_format = cell.borders("left") + assert line_format.api.__class__.__name__ == "LineFormat" + assert line_format.__class__.__name__ == "LineFormat" + + +def test_cell_border_parent(cell: Cell): + line_format = cell.borders("left") + assert line_format.api.Parent.__class__.__name__ == "Borders" + assert line_format.parent.__class__.__name__ == "Borders" diff --git a/tests/test_shapes.py b/tests/test_shapes.py index 1c5c4c8..224b760 100644 --- a/tests/test_shapes.py +++ b/tests/test_shapes.py @@ -41,7 +41,7 @@ def test_left(shape: Shape): def test_left_center(shape: Shape): shape.left = "center" - assert round(shape.left + shape.width / 2) == round(shape.slide.width / 2) + assert round(shape.left + shape.width / 2) == round(shape.slide.width / 2) # type: ignore def test_left_neg(shape: Shape): @@ -143,13 +143,15 @@ def test_set_style(shape: Shape): assert shape.line_color == 32768 -def test_add_shape(shapes: Shapes): +def test_add(shapes: Shapes): shape = shapes.add("Oval", 100, 100, 40, 60) assert shape.text == "" assert shape.left == 100 assert shape.top == 100 assert shape.width == 40 assert shape.height == 60 + assert shape.api.Parent.__class__.__name__ == "_Slide" + assert shape.parent.__class__.__name__ == "Slide" shape.delete() @@ -163,18 +165,41 @@ def test_add_label(shapes: Shapes): shape.text = "ABC ABC" assert width < shape.width assert height == shape.height + assert shape.api.Parent.__class__.__name__ == "_Slide" + assert shape.parent.__class__.__name__ == "Slide" shape.delete() -def test_repr_slides(shapes: Shapes): +def test_add_table(shapes: Shapes): + shape = shapes.add_table(2, 3, 100, 100, 240, 360) + assert isinstance(shape, Shape) + assert shape.api.Parent.__class__.__name__ == "_Slide" + assert shape.parent.__class__.__name__ == "Slide" + assert shape.api.Table.__class__.__name__ == "Table" + assert shape.api.Table.Parent.__class__.__name__ == "Shape" + shape.delete() + + +def test_slides_repr(shapes: Shapes): assert repr(shapes) == "" -def test_repr_slide(shape: Shape): +def test_slide_repr(shape: Shape): assert repr(shape) == "" -def test_repr_slide_oval(shapes: Shapes): +def test_slide_oval_repr(shapes: Shapes): shape = shapes.add("Oval", 100, 100, 40, 60) assert repr(shape) == "" shape.delete() + + +def test_slides_parent(shapes: Shapes): + assert shapes.api.Parent.__class__.__name__ == "_Slide" + assert shapes.parent.__class__.__name__ == "Slide" + + +def test_slide_parent(shape: Shape, shapes: Shapes): + assert shape.api.Parent.__class__.__name__ == "_Slide" + assert shape.parent.__class__.__name__ == "Slide" + assert shapes(1).parent.__class__.__name__ == "Slide" diff --git a/tests/test_tables.py b/tests/test_tables.py index 82e4aa3..ad8e532 100644 --- a/tests/test_tables.py +++ b/tests/test_tables.py @@ -1,7 +1,7 @@ import pytest from pptxlib.shapes import Shapes -from pptxlib.tables import Columns, Rows, Table, Tables +from pptxlib.tables import Cell, CellRange, Columns, Rows, Table, Tables def test_add(tables: Tables, shapes: Shapes): @@ -123,14 +123,132 @@ def test_minimize_height(table: Table): assert x > y -def test_repr_tables(tables: Tables): +def test_cells(columns: Columns): + cells = columns(1).cells + assert cells.api.__class__.__name__ == "CellRange" + assert cells.__class__.__name__ == "CellRange" + + +def test_tables_repr(tables: Tables): assert repr(tables) == "" -def test_repr_table(table: Table): +def test_table_repr(table: Table): assert repr(table) == "" +def test_rows_repr(rows: Rows): + assert repr(rows) == "" + + +def test_row_repr(rows: Rows): + assert repr(rows(1)) == "" + + +def test_columns_repr(columns: Columns): + assert repr(columns) == "" + + +def test_column_repr(columns: Columns): + assert repr(columns(1)) == "" + + +def test_cell_repr(table: Table): + assert repr(table.cell(1)) == "" + + +def test_cells_repr(rows: Rows, columns: Columns): + assert repr(rows(1).cells) == "" + assert repr(columns(1).cells) == "" + + +def test_tables_parent(tables: Tables): + assert tables.api.Parent.__class__.__name__ == "_Slide" + assert tables.parent.__class__.__name__ == "Slide" + + +def test_table_parent(table: Table, tables: Tables): + assert table.api.Parent.__class__.__name__ == "Shape" + assert table.parent.__class__.__name__ == "Shape" + assert tables(1).parent.__class__.__name__ == "Shape" + + +def test_rows_parent(rows: Rows): + assert rows.api.Parent.__class__.__name__ == "Table" + assert rows.parent.__class__.__name__ == "Table" + + +def test_row_parent(rows: Rows): + assert rows(1).api.Parent.__class__.__name__ == "Table" + assert rows(1).parent.__class__.__name__ == "Table" + + +def test_columns_parent(columns: Columns): + assert columns.api.Parent.__class__.__name__ == "Table" + assert columns.parent.__class__.__name__ == "Table" + + +def test_column_parent(columns: Columns): + assert columns(1).api.Parent.__class__.__name__ == "Table" + assert columns(1).parent.__class__.__name__ == "Table" + + +def test_cell_parent(table: Table): + cell = table.cell(1, 1) + assert cell.api.Parent.__class__.__name__ == "Table" + assert cell.parent.__class__.__name__ == "Table" + + +def test_cells_parent(rows: Rows, columns: Columns): + assert rows(1).cells.api.Parent.__class__.__name__ == "Row" + assert rows(1).cells.parent.__class__.__name__ == "Row" + assert columns(1).cells.api.Parent.__class__.__name__ == "Column" + assert columns(1).cells.parent.__class__.__name__ == "Column" + + +def test_cell_borders(cell: Cell): + assert isinstance(cell, Cell) + assert cell.borders.api.__class__.__name__ == "Borders" + assert cell.borders.__class__.__name__ == "Borders" + + +def test_cell_range_borders(cell_range: CellRange): + assert isinstance(cell_range, CellRange) + assert cell_range.borders.api.__class__.__name__ == "Borders" + assert cell_range.borders.__class__.__name__ == "Borders" + + +def test_cell_borders_parent(cell: Cell): + assert cell.borders.api.Parent.__class__.__name__ == "Table" + assert cell.borders.parent.__class__.__name__ == "Table" + + +def test_cell_range_borders_parent(cell_range: CellRange): + assert isinstance(cell_range, CellRange) + assert cell_range.borders.api.Parent.__class__.__name__ == "Table" + assert cell_range.borders.parent.__class__.__name__ == "Table" + + +# def test_table_repr(table: Table): +# assert repr(table) == "
" + + +# def test_rows_repr(rows: Rows): +# assert repr(rows) == "" + + +# def test_row_repr(rows: Rows): +# assert repr(rows(1)) == "" + + +# def test_columns_repr(columns: Columns): +# assert repr(columns) == "" + + +# def test_column_repr(columns: Columns): +# assert repr(columns(1)) == "" + + # a = 1 # from pptxlib import PowerPoint