diff --git a/pyproject.toml b/pyproject.toml index a741452..86f6981 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "cmi_docx" -version = "0.1.6" +version = "0.2.0" description = ".docx utilities" authors = ["Reinder Vos de Wael "] license = "LGPL-2.1" diff --git a/src/cmi_docx/__init__.py b/src/cmi_docx/__init__.py index e5edcac..8b39fc2 100644 --- a/src/cmi_docx/__init__.py +++ b/src/cmi_docx/__init__.py @@ -3,4 +3,5 @@ from cmi_docx.document import ExtendDocument # noqa: F401 from cmi_docx.paragraph import ExtendParagraph, FindParagraph # noqa: F401 from cmi_docx.run import ExtendRun, FindRun # noqa: F401 +from cmi_docx.styles import ParagraphStyle, RunStyle, TableStyle # noqa: F401 from cmi_docx.table import ExtendCell # noqa: F401 diff --git a/src/cmi_docx/document.py b/src/cmi_docx/document.py index 9c0bc46..5dbbae6 100644 --- a/src/cmi_docx/document.py +++ b/src/cmi_docx/document.py @@ -1,12 +1,11 @@ """Extends a python-docx Word document with additional functionality.""" import pathlib -from typing import Any from docx import document from docx.text import paragraph as docx_paragraph -from cmi_docx import paragraph, run +from cmi_docx import paragraph, run, styles class ExtendDocument: @@ -46,7 +45,7 @@ def find_in_runs(self, needle: str) -> list[run.FindRun]: ] def replace( - self, needle: str, replace: str, style: dict[str, Any] | None = None + self, needle: str, replace: str, style: styles.RunStyle | None = None ) -> None: """Finds and replaces text in a Word document. diff --git a/src/cmi_docx/paragraph.py b/src/cmi_docx/paragraph.py index 31d6f5e..53fc808 100644 --- a/src/cmi_docx/paragraph.py +++ b/src/cmi_docx/paragraph.py @@ -4,13 +4,12 @@ import dataclasses import itertools import re -from typing import Any from docx.enum import text from docx.text import paragraph as docx_paragraph from docx.text import run as docx_run -from cmi_docx import run +from cmi_docx import run, styles @dataclasses.dataclass @@ -99,7 +98,7 @@ def find_in_runs(self, needle: str) -> list[run.FindRun]: return run_finds def replace( - self, needle: str, replace: str, style: dict[str, Any] | None = None + self, needle: str, replace: str, style: styles.RunStyle | None = None ) -> None: """Finds and replaces text in a Word paragraph. @@ -116,7 +115,7 @@ def replace( for run_find in run_finder: run_find.replace(replace, style) - def insert_run(self, index: int, text: str, style: dict[str, Any]) -> docx_run.Run: + def insert_run(self, index: int, text: str, style: styles.RunStyle) -> docx_run.Run: """Inserts a run into a paragraph. Args: @@ -137,7 +136,7 @@ def insert_run(self, index: int, text: str, style: dict[str, Any]) -> docx_run.R else: self.paragraph.runs[index]._element.addprevious(new_run) - run.ExtendRun(self.paragraph.runs[index]).format(**style) + run.ExtendRun(self.paragraph.runs[index]).format(style) return self.paragraph.runs[index] def format( @@ -178,8 +177,10 @@ def format( for paragraph_run in self.paragraph.runs: run.ExtendRun(paragraph_run).format( - bold=bold, - italics=italics, - font_size=font_size, - font_rgb=font_rgb, + styles.RunStyle( + bold=bold, + italic=italics, + font_size=font_size, + font_rgb=font_rgb, + ) ) diff --git a/src/cmi_docx/run.py b/src/cmi_docx/run.py index 3a3e494..9f85bf2 100644 --- a/src/cmi_docx/run.py +++ b/src/cmi_docx/run.py @@ -1,10 +1,10 @@ """Module for extending python-docx Run objects.""" -from typing import Any - from docx import shared from docx.text import paragraph as docx_paragraph +from cmi_docx import styles + class FindRun: """Data class for maintaing find results in runs. @@ -40,7 +40,7 @@ def runs(self) -> list[docx_paragraph.Run]: """Returns the runs containing the text.""" return self.paragraph.runs[self.run_indices[0] : self.run_indices[1] + 1] - def replace(self, replace: str, style: dict[str, Any] | None = None) -> None: + def replace(self, replace: str, style: styles.RunStyle | None = None) -> None: """Replaces the text in the runs with the replacement text. Args: @@ -79,7 +79,7 @@ def _replace_without_style(self, replace: str) -> None: run.clear() self.runs[-1].text = self.runs[-1].text[end:] - def _replace_with_style(self, replace: str, style: dict[str, Any]) -> None: + def _replace_with_style(self, replace: str, style: styles.RunStyle) -> None: """Replaces the text in the runs with the replacement text and style. Args: @@ -95,13 +95,13 @@ def _replace_with_style(self, replace: str, style: dict[str, Any]) -> None: new_run = self.paragraph._element._new_r() new_run.text = replace self.paragraph.runs[self.run_indices[0]]._element.addnext(new_run) - ExtendRun(self.paragraph.runs[self.run_indices[0] + 1]).format(**style) + ExtendRun(self.paragraph.runs[self.run_indices[0] + 1]).format(style) post_run = self.paragraph._element._new_r() post_run.text = post self.paragraph.runs[self.run_indices[0] + 1]._element.addnext(post_run) pre_style = ExtendRun(self.paragraph.runs[self.run_indices[0]]).get_format() - ExtendRun(self.paragraph.runs[self.run_indices[0] + 2]).format(**pre_style) + ExtendRun(self.paragraph.runs[self.run_indices[0] + 2]).format(pre_style) def __lt__(self, other: "FindRun") -> bool: """Sorts FindRun in order of appearance in the paragraph. @@ -135,59 +135,43 @@ def __init__(self, run: docx_paragraph.Run) -> None: """ self.run = run - def format( - self, - *, - bold: bool | None = None, - italics: bool | None = None, - underline: bool | None = None, - superscript: bool | None = None, - subscript: bool | None = None, - font_size: int | None = None, - font_rgb: tuple[int, int, int] | None = None, - ) -> None: + def format(self, style: styles.RunStyle) -> None: """Formats a run in a Word document. Args: - bold: Whether to bold the run. - italics: Whether to italicize the run. - underline: Whether to underline the run. - superscript: Whether to superscript the run. - subscript: Whether to subscript the run. - font_size: The font size of the run. - font_rgb: The font color of the run. + style: The style to apply to the run. """ - if superscript and subscript: + if style.superscript and style.subscript: msg = "Cannot have superscript and subscript at the same time." raise ValueError(msg) - if bold is not None: - self.run.bold = bold - if italics is not None: - self.run.italic = italics - if underline is not None: - self.run.underline = underline - if superscript is not None: - self.run.font.superscript = superscript - if subscript is not None: - self.run.font.subscript = subscript - if font_size is not None: - self.run.font.size = font_size - if font_rgb is not None: - self.run.font.color.rgb = shared.RGBColor(*font_rgb) - - def get_format(self) -> dict[str, Any]: + if style.bold is not None: + self.run.bold = style.bold + if style.italic is not None: + self.run.italic = style.italic + if style.underline is not None: + self.run.underline = style.underline + if style.superscript is not None: + self.run.font.superscript = style.superscript + if style.subscript is not None: + self.run.font.subscript = style.subscript + if style.font_size is not None: + self.run.font.size = style.font_size + if style.font_rgb is not None: + self.run.font.color.rgb = shared.RGBColor(*style.font_rgb) + + def get_format(self) -> styles.RunStyle: """Returns the formatting of the run. Returns: The formatting of the run. """ - return { - "bold": self.run.bold, - "italics": self.run.italic, - "underline": self.run.underline, - "superscript": self.run.font.superscript, - "subscript": self.run.font.subscript, - "font_size": self.run.font.size, - "font_rgb": self.run.font.color.rgb, - } + return styles.RunStyle( + bold=self.run.bold, + italic=self.run.italic, + underline=self.run.underline, + superscript=self.run.font.superscript, + subscript=self.run.font.subscript, + font_size=self.run.font.size, + font_rgb=self.run.font.color.rgb, + ) diff --git a/src/cmi_docx/styles.py b/src/cmi_docx/styles.py new file mode 100644 index 0000000..e3993b3 --- /dev/null +++ b/src/cmi_docx/styles.py @@ -0,0 +1,42 @@ +"""Style interfaces for document properties.""" + +import dataclasses + +from docx.enum import text + + +@dataclasses.dataclass +class RunStyle: + """Dataclass for run style arguments.""" + + bold: bool | None = None + italic: bool | None = None + underline: bool | None = None + superscript: bool | None = None + subscript: bool | None = None + font_size: int | None = None + font_rgb: tuple[int, int, int] | None = None + + +@dataclasses.dataclass +class ParagraphStyle: + """Dataclass for paragraph style arguments.""" + + bold: bool | None = None + italic: bool | None = None + font_size: int | None = None + font_rgb: tuple[int, int, int] | None = None + line_spacing: float | None = None + space_before: float | None = None + space_after: float | None = None + alignment: text.WD_PARAGRAPH_ALIGNMENT | None = None + + +@dataclasses.dataclass +class TableStyle: + """Dataclass for table style arguments.""" + + space_before: float | None = None + space_after: float | None = None + background_rgb: tuple[int, int, int] | None = None + paragraph_style: ParagraphStyle | None = None diff --git a/tests/test_document.py b/tests/test_document.py index f06270c..2dadc1e 100644 --- a/tests/test_document.py +++ b/tests/test_document.py @@ -3,7 +3,7 @@ import docx import pytest -from cmi_docx import document +from cmi_docx import document, styles def test_find_in_paragraphs() -> None: @@ -96,7 +96,7 @@ def test_replace_with_style() -> None: doc.add_paragraph("Hello, world!") extend_document = document.ExtendDocument(doc) - extend_document.replace("Hello", "Goodbye", {"bold": True}) + extend_document.replace("Hello", "Goodbye", styles.RunStyle(bold=True)) assert doc.paragraphs[0].text == "Goodbye, world!" assert not doc.paragraphs[0].runs[0].bold diff --git a/tests/test_paragraph.py b/tests/test_paragraph.py index 69e6c1d..0514da4 100644 --- a/tests/test_paragraph.py +++ b/tests/test_paragraph.py @@ -4,7 +4,7 @@ import pytest from docx.text import paragraph as docx_paragraph -from cmi_docx import paragraph +from cmi_docx import paragraph, styles @pytest.fixture @@ -96,7 +96,7 @@ def test_insert_run_middle() -> None: para.add_run("world!") extend_paragraph = paragraph.ExtendParagraph(para) - extend_paragraph.insert_run(1, "beautiful ", {"bold": True}) + extend_paragraph.insert_run(1, "beautiful ", styles.RunStyle(bold=True)) assert para.text == "Hello beautiful world!" assert para.runs[1].bold @@ -108,7 +108,7 @@ def test_insert_run_start() -> None: para = document.add_paragraph("world!") extend_paragraph = paragraph.ExtendParagraph(para) - extend_paragraph.insert_run(0, "Hello ", {"bold": True}) + extend_paragraph.insert_run(0, "Hello ", styles.RunStyle(bold=True)) assert para.text == "Hello world!" assert para.runs[0].bold @@ -121,7 +121,7 @@ def test_insert_run_end(index: int) -> None: para = document.add_paragraph("Hello") extend_paragraph = paragraph.ExtendParagraph(para) - extend_paragraph.insert_run(index, " world!", {"bold": True}) + extend_paragraph.insert_run(index, " world!", styles.RunStyle(bold=True)) assert para.text == "Hello world!" assert para.runs[1].bold @@ -133,7 +133,7 @@ def test_insert_run_empty() -> None: para = document.add_paragraph("") extend_paragraph = paragraph.ExtendParagraph(para) - extend_paragraph.insert_run(0, "Hello", {"bold": True}) + extend_paragraph.insert_run(0, "Hello", styles.RunStyle(bold=True)) assert para.text == "Hello" assert para.runs[0].bold diff --git a/tests/test_run.py b/tests/test_run.py index d570201..53f2d4c 100644 --- a/tests/test_run.py +++ b/tests/test_run.py @@ -3,7 +3,7 @@ import docx import pytest -from cmi_docx import run +from cmi_docx import run, styles def test_find_run_lt_same_paragraph() -> None: @@ -75,7 +75,7 @@ def test_find_run_replace_with_style() -> None: paragraph.add_run("Hello, world!") find_run = run.FindRun(paragraph, (0, 1), (0, 5)) - find_run.replace("Goodbye", {"bold": True}) + find_run.replace("Goodbye", styles.RunStyle(bold=True)) assert paragraph.text == "Goodbye, world!" assert not paragraph.runs[0].bold @@ -91,11 +91,13 @@ def test_extend_run_format() -> None: extend_run = run.ExtendRun(paragraph_run) extend_run.format( - bold=True, - italics=True, - underline=True, - superscript=True, - font_rgb=(1, 0, 0), + styles.RunStyle( + bold=True, + italic=True, + underline=True, + superscript=True, + font_rgb=(1, 0, 0), + ) ) assert paragraph_run.bold