Skip to content

Commit 6630a34

Browse files
Refactor style inputs to a dataclass (#15)
1 parent c144f41 commit 6630a34

9 files changed

Lines changed: 106 additions & 77 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "cmi_docx"
3-
version = "0.1.6"
3+
version = "0.2.0"
44
description = ".docx utilities"
55
authors = ["Reinder Vos de Wael <reinder.vosdewael@childmind.org>"]
66
license = "LGPL-2.1"

src/cmi_docx/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@
33
from cmi_docx.document import ExtendDocument # noqa: F401
44
from cmi_docx.paragraph import ExtendParagraph, FindParagraph # noqa: F401
55
from cmi_docx.run import ExtendRun, FindRun # noqa: F401
6+
from cmi_docx.styles import ParagraphStyle, RunStyle, TableStyle # noqa: F401
67
from cmi_docx.table import ExtendCell # noqa: F401

src/cmi_docx/document.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
"""Extends a python-docx Word document with additional functionality."""
22

33
import pathlib
4-
from typing import Any
54

65
from docx import document
76
from docx.text import paragraph as docx_paragraph
87

9-
from cmi_docx import paragraph, run
8+
from cmi_docx import paragraph, run, styles
109

1110

1211
class ExtendDocument:
@@ -46,7 +45,7 @@ def find_in_runs(self, needle: str) -> list[run.FindRun]:
4645
]
4746

4847
def replace(
49-
self, needle: str, replace: str, style: dict[str, Any] | None = None
48+
self, needle: str, replace: str, style: styles.RunStyle | None = None
5049
) -> None:
5150
"""Finds and replaces text in a Word document.
5251

src/cmi_docx/paragraph.py

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,12 @@
44
import dataclasses
55
import itertools
66
import re
7-
from typing import Any
87

98
from docx.enum import text
109
from docx.text import paragraph as docx_paragraph
1110
from docx.text import run as docx_run
1211

13-
from cmi_docx import run
12+
from cmi_docx import run, styles
1413

1514

1615
@dataclasses.dataclass
@@ -99,7 +98,7 @@ def find_in_runs(self, needle: str) -> list[run.FindRun]:
9998
return run_finds
10099

101100
def replace(
102-
self, needle: str, replace: str, style: dict[str, Any] | None = None
101+
self, needle: str, replace: str, style: styles.RunStyle | None = None
103102
) -> None:
104103
"""Finds and replaces text in a Word paragraph.
105104
@@ -116,7 +115,7 @@ def replace(
116115
for run_find in run_finder:
117116
run_find.replace(replace, style)
118117

119-
def insert_run(self, index: int, text: str, style: dict[str, Any]) -> docx_run.Run:
118+
def insert_run(self, index: int, text: str, style: styles.RunStyle) -> docx_run.Run:
120119
"""Inserts a run into a paragraph.
121120
122121
Args:
@@ -137,7 +136,7 @@ def insert_run(self, index: int, text: str, style: dict[str, Any]) -> docx_run.R
137136
else:
138137
self.paragraph.runs[index]._element.addprevious(new_run)
139138

140-
run.ExtendRun(self.paragraph.runs[index]).format(**style)
139+
run.ExtendRun(self.paragraph.runs[index]).format(style)
141140
return self.paragraph.runs[index]
142141

143142
def format(
@@ -178,8 +177,10 @@ def format(
178177

179178
for paragraph_run in self.paragraph.runs:
180179
run.ExtendRun(paragraph_run).format(
181-
bold=bold,
182-
italics=italics,
183-
font_size=font_size,
184-
font_rgb=font_rgb,
180+
styles.RunStyle(
181+
bold=bold,
182+
italic=italics,
183+
font_size=font_size,
184+
font_rgb=font_rgb,
185+
)
185186
)

src/cmi_docx/run.py

Lines changed: 34 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
"""Module for extending python-docx Run objects."""
22

3-
from typing import Any
4-
53
from docx import shared
64
from docx.text import paragraph as docx_paragraph
75

6+
from cmi_docx import styles
7+
88

99
class FindRun:
1010
"""Data class for maintaing find results in runs.
@@ -40,7 +40,7 @@ def runs(self) -> list[docx_paragraph.Run]:
4040
"""Returns the runs containing the text."""
4141
return self.paragraph.runs[self.run_indices[0] : self.run_indices[1] + 1]
4242

43-
def replace(self, replace: str, style: dict[str, Any] | None = None) -> None:
43+
def replace(self, replace: str, style: styles.RunStyle | None = None) -> None:
4444
"""Replaces the text in the runs with the replacement text.
4545
4646
Args:
@@ -79,7 +79,7 @@ def _replace_without_style(self, replace: str) -> None:
7979
run.clear()
8080
self.runs[-1].text = self.runs[-1].text[end:]
8181

82-
def _replace_with_style(self, replace: str, style: dict[str, Any]) -> None:
82+
def _replace_with_style(self, replace: str, style: styles.RunStyle) -> None:
8383
"""Replaces the text in the runs with the replacement text and style.
8484
8585
Args:
@@ -95,13 +95,13 @@ def _replace_with_style(self, replace: str, style: dict[str, Any]) -> None:
9595
new_run = self.paragraph._element._new_r()
9696
new_run.text = replace
9797
self.paragraph.runs[self.run_indices[0]]._element.addnext(new_run)
98-
ExtendRun(self.paragraph.runs[self.run_indices[0] + 1]).format(**style)
98+
ExtendRun(self.paragraph.runs[self.run_indices[0] + 1]).format(style)
9999

100100
post_run = self.paragraph._element._new_r()
101101
post_run.text = post
102102
self.paragraph.runs[self.run_indices[0] + 1]._element.addnext(post_run)
103103
pre_style = ExtendRun(self.paragraph.runs[self.run_indices[0]]).get_format()
104-
ExtendRun(self.paragraph.runs[self.run_indices[0] + 2]).format(**pre_style)
104+
ExtendRun(self.paragraph.runs[self.run_indices[0] + 2]).format(pre_style)
105105

106106
def __lt__(self, other: "FindRun") -> bool:
107107
"""Sorts FindRun in order of appearance in the paragraph.
@@ -135,59 +135,43 @@ def __init__(self, run: docx_paragraph.Run) -> None:
135135
"""
136136
self.run = run
137137

138-
def format(
139-
self,
140-
*,
141-
bold: bool | None = None,
142-
italics: bool | None = None,
143-
underline: bool | None = None,
144-
superscript: bool | None = None,
145-
subscript: bool | None = None,
146-
font_size: int | None = None,
147-
font_rgb: tuple[int, int, int] | None = None,
148-
) -> None:
138+
def format(self, style: styles.RunStyle) -> None:
149139
"""Formats a run in a Word document.
150140
151141
Args:
152-
bold: Whether to bold the run.
153-
italics: Whether to italicize the run.
154-
underline: Whether to underline the run.
155-
superscript: Whether to superscript the run.
156-
subscript: Whether to subscript the run.
157-
font_size: The font size of the run.
158-
font_rgb: The font color of the run.
142+
style: The style to apply to the run.
159143
"""
160-
if superscript and subscript:
144+
if style.superscript and style.subscript:
161145
msg = "Cannot have superscript and subscript at the same time."
162146
raise ValueError(msg)
163147

164-
if bold is not None:
165-
self.run.bold = bold
166-
if italics is not None:
167-
self.run.italic = italics
168-
if underline is not None:
169-
self.run.underline = underline
170-
if superscript is not None:
171-
self.run.font.superscript = superscript
172-
if subscript is not None:
173-
self.run.font.subscript = subscript
174-
if font_size is not None:
175-
self.run.font.size = font_size
176-
if font_rgb is not None:
177-
self.run.font.color.rgb = shared.RGBColor(*font_rgb)
178-
179-
def get_format(self) -> dict[str, Any]:
148+
if style.bold is not None:
149+
self.run.bold = style.bold
150+
if style.italic is not None:
151+
self.run.italic = style.italic
152+
if style.underline is not None:
153+
self.run.underline = style.underline
154+
if style.superscript is not None:
155+
self.run.font.superscript = style.superscript
156+
if style.subscript is not None:
157+
self.run.font.subscript = style.subscript
158+
if style.font_size is not None:
159+
self.run.font.size = style.font_size
160+
if style.font_rgb is not None:
161+
self.run.font.color.rgb = shared.RGBColor(*style.font_rgb)
162+
163+
def get_format(self) -> styles.RunStyle:
180164
"""Returns the formatting of the run.
181165
182166
Returns:
183167
The formatting of the run.
184168
"""
185-
return {
186-
"bold": self.run.bold,
187-
"italics": self.run.italic,
188-
"underline": self.run.underline,
189-
"superscript": self.run.font.superscript,
190-
"subscript": self.run.font.subscript,
191-
"font_size": self.run.font.size,
192-
"font_rgb": self.run.font.color.rgb,
193-
}
169+
return styles.RunStyle(
170+
bold=self.run.bold,
171+
italic=self.run.italic,
172+
underline=self.run.underline,
173+
superscript=self.run.font.superscript,
174+
subscript=self.run.font.subscript,
175+
font_size=self.run.font.size,
176+
font_rgb=self.run.font.color.rgb,
177+
)

src/cmi_docx/styles.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
"""Style interfaces for document properties."""
2+
3+
import dataclasses
4+
5+
from docx.enum import text
6+
7+
8+
@dataclasses.dataclass
9+
class RunStyle:
10+
"""Dataclass for run style arguments."""
11+
12+
bold: bool | None = None
13+
italic: bool | None = None
14+
underline: bool | None = None
15+
superscript: bool | None = None
16+
subscript: bool | None = None
17+
font_size: int | None = None
18+
font_rgb: tuple[int, int, int] | None = None
19+
20+
21+
@dataclasses.dataclass
22+
class ParagraphStyle:
23+
"""Dataclass for paragraph style arguments."""
24+
25+
bold: bool | None = None
26+
italic: bool | None = None
27+
font_size: int | None = None
28+
font_rgb: tuple[int, int, int] | None = None
29+
line_spacing: float | None = None
30+
space_before: float | None = None
31+
space_after: float | None = None
32+
alignment: text.WD_PARAGRAPH_ALIGNMENT | None = None
33+
34+
35+
@dataclasses.dataclass
36+
class TableStyle:
37+
"""Dataclass for table style arguments."""
38+
39+
space_before: float | None = None
40+
space_after: float | None = None
41+
background_rgb: tuple[int, int, int] | None = None
42+
paragraph_style: ParagraphStyle | None = None

tests/test_document.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import docx
44
import pytest
55

6-
from cmi_docx import document
6+
from cmi_docx import document, styles
77

88

99
def test_find_in_paragraphs() -> None:
@@ -96,7 +96,7 @@ def test_replace_with_style() -> None:
9696
doc.add_paragraph("Hello, world!")
9797
extend_document = document.ExtendDocument(doc)
9898

99-
extend_document.replace("Hello", "Goodbye", {"bold": True})
99+
extend_document.replace("Hello", "Goodbye", styles.RunStyle(bold=True))
100100

101101
assert doc.paragraphs[0].text == "Goodbye, world!"
102102
assert not doc.paragraphs[0].runs[0].bold

tests/test_paragraph.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import pytest
55
from docx.text import paragraph as docx_paragraph
66

7-
from cmi_docx import paragraph
7+
from cmi_docx import paragraph, styles
88

99

1010
@pytest.fixture
@@ -96,7 +96,7 @@ def test_insert_run_middle() -> None:
9696
para.add_run("world!")
9797
extend_paragraph = paragraph.ExtendParagraph(para)
9898

99-
extend_paragraph.insert_run(1, "beautiful ", {"bold": True})
99+
extend_paragraph.insert_run(1, "beautiful ", styles.RunStyle(bold=True))
100100

101101
assert para.text == "Hello beautiful world!"
102102
assert para.runs[1].bold
@@ -108,7 +108,7 @@ def test_insert_run_start() -> None:
108108
para = document.add_paragraph("world!")
109109
extend_paragraph = paragraph.ExtendParagraph(para)
110110

111-
extend_paragraph.insert_run(0, "Hello ", {"bold": True})
111+
extend_paragraph.insert_run(0, "Hello ", styles.RunStyle(bold=True))
112112

113113
assert para.text == "Hello world!"
114114
assert para.runs[0].bold
@@ -121,7 +121,7 @@ def test_insert_run_end(index: int) -> None:
121121
para = document.add_paragraph("Hello")
122122
extend_paragraph = paragraph.ExtendParagraph(para)
123123

124-
extend_paragraph.insert_run(index, " world!", {"bold": True})
124+
extend_paragraph.insert_run(index, " world!", styles.RunStyle(bold=True))
125125

126126
assert para.text == "Hello world!"
127127
assert para.runs[1].bold
@@ -133,7 +133,7 @@ def test_insert_run_empty() -> None:
133133
para = document.add_paragraph("")
134134
extend_paragraph = paragraph.ExtendParagraph(para)
135135

136-
extend_paragraph.insert_run(0, "Hello", {"bold": True})
136+
extend_paragraph.insert_run(0, "Hello", styles.RunStyle(bold=True))
137137

138138
assert para.text == "Hello"
139139
assert para.runs[0].bold

tests/test_run.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import docx
44
import pytest
55

6-
from cmi_docx import run
6+
from cmi_docx import run, styles
77

88

99
def test_find_run_lt_same_paragraph() -> None:
@@ -75,7 +75,7 @@ def test_find_run_replace_with_style() -> None:
7575
paragraph.add_run("Hello, world!")
7676

7777
find_run = run.FindRun(paragraph, (0, 1), (0, 5))
78-
find_run.replace("Goodbye", {"bold": True})
78+
find_run.replace("Goodbye", styles.RunStyle(bold=True))
7979

8080
assert paragraph.text == "Goodbye, world!"
8181
assert not paragraph.runs[0].bold
@@ -91,11 +91,13 @@ def test_extend_run_format() -> None:
9191

9292
extend_run = run.ExtendRun(paragraph_run)
9393
extend_run.format(
94-
bold=True,
95-
italics=True,
96-
underline=True,
97-
superscript=True,
98-
font_rgb=(1, 0, 0),
94+
styles.RunStyle(
95+
bold=True,
96+
italic=True,
97+
underline=True,
98+
superscript=True,
99+
font_rgb=(1, 0, 0),
100+
)
99101
)
100102

101103
assert paragraph_run.bold

0 commit comments

Comments
 (0)