reduced redundant code
All checks were successful
Python CI / test (3.10) (push) Successful in 2m23s
Python CI / test (3.12) (push) Successful in 2m15s
Python CI / test (3.13) (push) Successful in 2m11s

This commit is contained in:
Duncan Tourolle 2025-11-11 21:19:41 +01:00
parent 23d3278b50
commit 2a543d0319
4 changed files with 145 additions and 324 deletions

View File

@ -6,7 +6,7 @@ import urllib.request
import urllib.parse
from PIL import Image as PILImage
from .inline import Word, FormattedSpan
from ..core import Hierarchical, Styleable, FontRegistry
from ..core import Hierarchical, Styleable, FontRegistry, ContainerAware, BlockContainer
class BlockType(Enum):
@ -50,7 +50,7 @@ class Block(Hierarchical):
return self._block_type
class Paragraph(Styleable, FontRegistry, Block):
class Paragraph(Styleable, FontRegistry, ContainerAware, Block):
"""
A paragraph is a block-level element that contains a sequence of words.
@ -85,22 +85,15 @@ class Paragraph(Styleable, FontRegistry, Block):
Raises:
AttributeError: If the container doesn't have the required add_block method
"""
# Inherit style from container if not provided
if style is None and hasattr(container, 'style'):
style = container.style
elif style is None and hasattr(container, 'default_style'):
style = container.default_style
# Validate container and inherit style using ContainerAware utilities
cls._validate_container(container)
style = cls._inherit_style(container, style)
# Create the new paragraph
paragraph = cls(style)
# Add the paragraph to the container
if hasattr(container, 'add_block'):
container.add_block(paragraph)
else:
raise AttributeError(
f"Container {type(container).__name__} must have an 'add_block' method"
)
container.add_block(paragraph)
return paragraph
@ -237,22 +230,15 @@ class Heading(Paragraph):
Raises:
AttributeError: If the container doesn't have the required add_block method
"""
# Inherit style from container if not provided
if style is None and hasattr(container, 'style'):
style = container.style
elif style is None and hasattr(container, 'default_style'):
style = container.default_style
# Validate container and inherit style using ContainerAware utilities
cls._validate_container(container)
style = cls._inherit_style(container, style)
# Create the new heading
heading = cls(level, style)
# Add the heading to the container
if hasattr(container, 'add_block'):
container.add_block(heading)
else:
raise AttributeError(
f"Container {type(container).__name__} must have an 'add_block' method"
)
container.add_block(heading)
return heading
@ -267,7 +253,7 @@ class Heading(Paragraph):
self._level = level
class Quote(Block):
class Quote(BlockContainer, ContainerAware, Block):
"""
A blockquote element that can contain other block elements.
"""
@ -280,7 +266,6 @@ class Quote(Block):
style: Optional default style for child blocks
"""
super().__init__(BlockType.QUOTE)
self._blocks: List[Block] = []
self._style = style
@classmethod
@ -299,22 +284,15 @@ class Quote(Block):
Raises:
AttributeError: If the container doesn't have the required add_block method
"""
# Inherit style from container if not provided
if style is None and hasattr(container, 'style'):
style = container.style
elif style is None and hasattr(container, 'default_style'):
style = container.default_style
# Validate container and inherit style using ContainerAware utilities
cls._validate_container(container)
style = cls._inherit_style(container, style)
# Create the new quote
quote = cls(style)
# Add the quote to the container
if hasattr(container, 'add_block'):
container.add_block(quote)
else:
raise AttributeError(
f"Container {type(container).__name__} must have an 'add_block' method"
)
container.add_block(quote)
return quote
@ -328,54 +306,6 @@ class Quote(Block):
"""Set the default style for this quote"""
self._style = style
def add_block(self, block: Block):
"""
Add a block element to this quote.
Args:
block: The Block object to add
"""
self._blocks.append(block)
block.parent = self
def create_paragraph(self, style=None) -> Paragraph:
"""
Create a new paragraph and add it to this quote.
Args:
style: Optional style override. If None, inherits from quote
Returns:
The newly created Paragraph object
"""
return Paragraph.create_and_add_to(self, style)
def create_heading(
self,
level: HeadingLevel = HeadingLevel.H1,
style=None) -> Heading:
"""
Create a new heading and add it to this quote.
Args:
level: The heading level
style: Optional style override. If None, inherits from quote
Returns:
The newly created Heading object
"""
return Heading.create_and_add_to(self, level, style)
def blocks(self) -> Iterator[Block]:
"""
Iterate over the blocks in this quote.
Yields:
Each Block in the quote
"""
for block in self._blocks:
yield block
class CodeBlock(Block):
"""
@ -463,7 +393,7 @@ class ListStyle(Enum):
DEFINITION = 3 # <dl>
class HList(Block):
class HList(ContainerAware, Block):
"""
An HTML list element (ul, ol, dl).
"""
@ -502,22 +432,15 @@ class HList(Block):
Raises:
AttributeError: If the container doesn't have the required add_block method
"""
# Inherit style from container if not provided
if default_style is None and hasattr(container, 'style'):
default_style = container.style
elif default_style is None and hasattr(container, 'default_style'):
default_style = container.default_style
# Validate container and inherit style using ContainerAware utilities
cls._validate_container(container)
default_style = cls._inherit_style(container, default_style)
# Create the new list
hlist = cls(style, default_style)
# Add the list to the container
if hasattr(container, 'add_block'):
container.add_block(hlist)
else:
raise AttributeError(
f"Container {type(container).__name__} must have an 'add_block' method"
)
container.add_block(hlist)
return hlist
@ -580,7 +503,7 @@ class HList(Block):
return len(self._items)
class ListItem(Block):
class ListItem(BlockContainer, ContainerAware, Block):
"""
A list item element that can contain other block elements.
"""
@ -594,7 +517,6 @@ class ListItem(Block):
style: Optional default style for child blocks
"""
super().__init__(BlockType.LIST_ITEM)
self._blocks: List[Block] = []
self._term = term
self._style = style
@ -619,22 +541,15 @@ class ListItem(Block):
Raises:
AttributeError: If the container doesn't have the required add_item method
"""
# Inherit style from container if not provided
if style is None and hasattr(container, 'default_style'):
style = container.default_style
elif style is None and hasattr(container, 'style'):
style = container.style
# Validate container and inherit style using ContainerAware utilities
cls._validate_container(container, required_method='add_item')
style = cls._inherit_style(container, style)
# Create the new list item
item = cls(term, style)
# Add the list item to the container
if hasattr(container, 'add_item'):
container.add_item(item)
else:
raise AttributeError(
f"Container {type(container).__name__} must have an 'add_item' method"
)
container.add_item(item)
return item
@ -658,56 +573,8 @@ class ListItem(Block):
"""Set the default style for this list item"""
self._style = style
def add_block(self, block: Block):
"""
Add a block element to this list item.
Args:
block: The Block object to add
"""
self._blocks.append(block)
block.parent = self
def create_paragraph(self, style=None) -> Paragraph:
"""
Create a new paragraph and add it to this list item.
Args:
style: Optional style override. If None, inherits from list item
Returns:
The newly created Paragraph object
"""
return Paragraph.create_and_add_to(self, style)
def create_heading(
self,
level: HeadingLevel = HeadingLevel.H1,
style=None) -> Heading:
"""
Create a new heading and add it to this list item.
Args:
level: The heading level
style: Optional style override. If None, inherits from list item
Returns:
The newly created Heading object
"""
return Heading.create_and_add_to(self, level, style)
def blocks(self) -> Iterator[Block]:
"""
Iterate over the blocks in this list item.
Yields:
Each Block in the list item
"""
for block in self._blocks:
yield block
class TableCell(Block):
class TableCell(BlockContainer, ContainerAware, Block):
"""
A table cell element that can contain other block elements.
"""
@ -731,7 +598,6 @@ class TableCell(Block):
self._is_header = is_header
self._colspan = colspan
self._rowspan = rowspan
self._blocks: List[Block] = []
self._style = style
@classmethod
@ -754,20 +620,15 @@ class TableCell(Block):
Raises:
AttributeError: If the container doesn't have the required add_cell method
"""
# Inherit style from container if not provided
if style is None and hasattr(container, 'style'):
style = container.style
# Validate container and inherit style using ContainerAware utilities
cls._validate_container(container, required_method='add_cell')
style = cls._inherit_style(container, style)
# Create the new table cell
cell = cls(is_header, colspan, rowspan, style)
# Add the cell to the container
if hasattr(container, 'add_cell'):
container.add_cell(cell)
else:
raise AttributeError(
f"Container {type(container).__name__} must have an 'add_cell' method"
)
container.add_cell(cell)
return cell
@ -811,56 +672,8 @@ class TableCell(Block):
"""Set the default style for this table cell"""
self._style = style
def add_block(self, block: Block):
"""
Add a block element to this cell.
Args:
block: The Block object to add
"""
self._blocks.append(block)
block.parent = self
def create_paragraph(self, style=None) -> Paragraph:
"""
Create a new paragraph and add it to this table cell.
Args:
style: Optional style override. If None, inherits from cell
Returns:
The newly created Paragraph object
"""
return Paragraph.create_and_add_to(self, style)
def create_heading(
self,
level: HeadingLevel = HeadingLevel.H1,
style=None) -> Heading:
"""
Create a new heading and add it to this table cell.
Args:
level: The heading level
style: Optional style override. If None, inherits from cell
Returns:
The newly created Heading object
"""
return Heading.create_and_add_to(self, level, style)
def blocks(self) -> Iterator[Block]:
"""
Iterate over the blocks in this cell.
Yields:
Each Block in the cell
"""
for block in self._blocks:
yield block
class TableRow(Block):
class TableRow(ContainerAware, Block):
"""
A table row element containing table cells.
"""
@ -897,20 +710,15 @@ class TableRow(Block):
Raises:
AttributeError: If the container doesn't have the required add_row method
"""
# Inherit style from container if not provided
if style is None and hasattr(container, 'style'):
style = container.style
# Validate container and inherit style using ContainerAware utilities
cls._validate_container(container, required_method='add_row')
style = cls._inherit_style(container, style)
# Create the new table row
row = cls(style)
# Add the row to the container
if hasattr(container, 'add_row'):
container.add_row(row, section)
else:
raise AttributeError(
f"Container {type(container).__name__} must have an 'add_row' method"
)
container.add_row(row, section)
return row
@ -970,7 +778,7 @@ class TableRow(Block):
return len(self._cells)
class Table(Block):
class Table(ContainerAware, Block):
"""
A table element containing rows and cells.
"""
@ -1011,22 +819,15 @@ class Table(Block):
Raises:
AttributeError: If the container doesn't have the required add_block method
"""
# Inherit style from container if not provided
if style is None and hasattr(container, 'style'):
style = container.style
elif style is None and hasattr(container, 'default_style'):
style = container.default_style
# Validate container and inherit style using ContainerAware utilities
cls._validate_container(container)
style = cls._inherit_style(container, style)
# Create the new table
table = cls(caption, style)
# Add the table to the container
if hasattr(container, 'add_block'):
container.add_block(table)
else:
raise AttributeError(
f"Container {type(container).__name__} must have an 'add_block' method"
)
container.add_block(table)
return table

View File

@ -190,6 +190,46 @@ class Page(Renderable, Queriable):
"""Get a copy of the children list"""
return self._children.copy()
def _get_child_property(self, child: Renderable, private_attr: str,
public_attr: str, index: Optional[int] = None,
default: Optional[int] = None) -> Optional[int]:
"""
Generic helper to extract properties from child objects with multiple fallback strategies.
Args:
child: The child object
private_attr: Name of the private attribute (e.g., '_size')
public_attr: Name of the public property (e.g., 'size')
index: Optional index for array-like properties (0 for width, 1 for height)
default: Default value if property cannot be determined
Returns:
Property value or default
"""
# Try private attribute first
if hasattr(child, private_attr):
value = getattr(child, private_attr)
if value is not None:
if isinstance(value, (list, tuple, np.ndarray)):
if index is not None and len(value) > index:
return int(value[index])
elif index is None:
return value
# Try public property
if hasattr(child, public_attr):
value = getattr(child, public_attr)
if value is not None:
if isinstance(value, (list, tuple, np.ndarray)):
if index is not None and len(value) > index:
return int(value[index])
elif index is None:
return value
else:
return int(value)
return default
def _get_child_height(self, child: Renderable) -> int:
"""
Get the height of a child object.
@ -200,20 +240,15 @@ class Page(Renderable, Queriable):
Returns:
Height in pixels
"""
if hasattr(child, '_size') and child._size is not None:
if isinstance(
child._size, (list, tuple, np.ndarray)) and len(
child._size) >= 2:
return int(child._size[1])
# Try to get height from size property (index 1)
height = self._get_child_property(child, '_size', 'size', index=1)
if height is not None:
return height
if hasattr(child, 'size') and child.size is not None:
if isinstance(
child.size, (list, tuple, np.ndarray)) and len(
child.size) >= 2:
return int(child.size[1])
if hasattr(child, 'height'):
return int(child.height)
# Try direct height attribute
height = self._get_child_property(child, '_height', 'height')
if height is not None:
return height
# Default fallback height
return 20
@ -286,19 +321,12 @@ class Page(Renderable, Queriable):
Returns:
Tuple of (x, y) coordinates
"""
if hasattr(child, '_origin') and child._origin is not None:
if isinstance(child._origin, np.ndarray):
return (int(child._origin[0]), int(child._origin[1]))
elif isinstance(child._origin, (list, tuple)) and len(child._origin) >= 2:
return (int(child._origin[0]), int(child._origin[1]))
# Try to get x coordinate
x = self._get_child_property(child, '_origin', 'position', index=0, default=0)
# Try to get y coordinate
y = self._get_child_property(child, '_origin', 'position', index=1, default=0)
if hasattr(child, 'position'):
pos = child.position
if isinstance(pos, (list, tuple)) and len(pos) >= 2:
return (int(pos[0]), int(pos[1]))
# Default to origin
return (0, 0)
return (x, y)
def query_point(self, point: Tuple[int, int]) -> Optional[QueryResult]:
"""
@ -377,20 +405,20 @@ class Page(Renderable, Queriable):
Returns:
Tuple of (width, height) or None if size cannot be determined
"""
if hasattr(child, '_size') and child._size is not None:
if isinstance(
child._size, (list, tuple, np.ndarray)) and len(
child._size) >= 2:
return (int(child._size[0]), int(child._size[1]))
# Try to get width and height from size property
width = self._get_child_property(child, '_size', 'size', index=0)
height = self._get_child_property(child, '_size', 'size', index=1)
if hasattr(child, 'size') and child.size is not None:
if isinstance(
child.size, (list, tuple, np.ndarray)) and len(
child.size) >= 2:
return (int(child.size[0]), int(child.size[1]))
# If size property worked, return it
if width is not None and height is not None:
return (width, height)
if hasattr(child, 'width') and hasattr(child, 'height'):
return (int(child.width), int(child.height))
# Try direct width/height attributes
width = self._get_child_property(child, '_width', 'width')
height = self._get_child_property(child, '_height', 'height')
if width is not None and height is not None:
return (width, height)
return None

View File

@ -331,10 +331,16 @@ class BlockContainer:
super().__init__(*args, **kwargs)
self._blocks = []
@property
def blocks(self):
"""Get the list of blocks in this container"""
return self._blocks
"""
Get an iterator over the blocks in this container.
Can be used as blocks() for iteration or accessing the _blocks list directly.
Returns:
Iterator over blocks
"""
return iter(self._blocks)
def add_block(self, block):
"""

View File

@ -359,62 +359,48 @@ class Font:
"""Get the minimum width required for hyphenation to be considered"""
return self._min_hyphenation_width
def _with_modified(self, **kwargs):
"""
Internal helper to create a new Font with modified parameters.
This consolidates the duplication across all with_* methods.
Args:
**kwargs: Parameters to override (e.g., font_size=20, colour=(255,0,0))
Returns:
New Font object with modified parameters
"""
params = {
'font_path': self._font_path,
'font_size': self._font_size,
'colour': self._colour,
'weight': self._weight,
'style': self._style,
'decoration': self._decoration,
'background': self._background,
'language': self.language,
'min_hyphenation_width': self._min_hyphenation_width
}
params.update(kwargs)
return Font(**params)
def with_size(self, size: int):
"""Create a new Font object with modified size"""
return Font(
self._font_path,
size,
self._colour,
self._weight,
self._style,
self._decoration,
self._background
)
return self._with_modified(font_size=size)
def with_colour(self, colour: Tuple[int, int, int]):
"""Create a new Font object with modified colour"""
return Font(
self._font_path,
self._font_size,
colour,
self._weight,
self._style,
self._decoration,
self._background
)
return self._with_modified(colour=colour)
def with_weight(self, weight: FontWeight):
"""Create a new Font object with modified weight"""
return Font(
self._font_path,
self._font_size,
self._colour,
weight,
self._style,
self._decoration,
self._background
)
return self._with_modified(weight=weight)
def with_style(self, style: FontStyle):
"""Create a new Font object with modified style"""
return Font(
self._font_path,
self._font_size,
self._colour,
self._weight,
style,
self._decoration,
self._background
)
return self._with_modified(style=style)
def with_decoration(self, decoration: TextDecoration):
"""Create a new Font object with modified decoration"""
return Font(
self._font_path,
self._font_size,
self._colour,
self._weight,
self._style,
decoration,
self._background
)
return self._with_modified(decoration=decoration)