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

View File

@ -190,6 +190,46 @@ class Page(Renderable, Queriable):
"""Get a copy of the children list""" """Get a copy of the children list"""
return self._children.copy() 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: def _get_child_height(self, child: Renderable) -> int:
""" """
Get the height of a child object. Get the height of a child object.
@ -200,20 +240,15 @@ class Page(Renderable, Queriable):
Returns: Returns:
Height in pixels Height in pixels
""" """
if hasattr(child, '_size') and child._size is not None: # Try to get height from size property (index 1)
if isinstance( height = self._get_child_property(child, '_size', 'size', index=1)
child._size, (list, tuple, np.ndarray)) and len( if height is not None:
child._size) >= 2: return height
return int(child._size[1])
if hasattr(child, 'size') and child.size is not None: # Try direct height attribute
if isinstance( height = self._get_child_property(child, '_height', 'height')
child.size, (list, tuple, np.ndarray)) and len( if height is not None:
child.size) >= 2: return height
return int(child.size[1])
if hasattr(child, 'height'):
return int(child.height)
# Default fallback height # Default fallback height
return 20 return 20
@ -286,19 +321,12 @@ class Page(Renderable, Queriable):
Returns: Returns:
Tuple of (x, y) coordinates Tuple of (x, y) coordinates
""" """
if hasattr(child, '_origin') and child._origin is not None: # Try to get x coordinate
if isinstance(child._origin, np.ndarray): x = self._get_child_property(child, '_origin', 'position', index=0, default=0)
return (int(child._origin[0]), int(child._origin[1])) # Try to get y coordinate
elif isinstance(child._origin, (list, tuple)) and len(child._origin) >= 2: y = self._get_child_property(child, '_origin', 'position', index=1, default=0)
return (int(child._origin[0]), int(child._origin[1]))
if hasattr(child, 'position'): return (x, y)
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)
def query_point(self, point: Tuple[int, int]) -> Optional[QueryResult]: def query_point(self, point: Tuple[int, int]) -> Optional[QueryResult]:
""" """
@ -377,20 +405,20 @@ class Page(Renderable, Queriable):
Returns: Returns:
Tuple of (width, height) or None if size cannot be determined Tuple of (width, height) or None if size cannot be determined
""" """
if hasattr(child, '_size') and child._size is not None: # Try to get width and height from size property
if isinstance( width = self._get_child_property(child, '_size', 'size', index=0)
child._size, (list, tuple, np.ndarray)) and len( height = self._get_child_property(child, '_size', 'size', index=1)
child._size) >= 2:
return (int(child._size[0]), int(child._size[1]))
if hasattr(child, 'size') and child.size is not None: # If size property worked, return it
if isinstance( if width is not None and height is not None:
child.size, (list, tuple, np.ndarray)) and len( return (width, height)
child.size) >= 2:
return (int(child.size[0]), int(child.size[1]))
if hasattr(child, 'width') and hasattr(child, 'height'): # Try direct width/height attributes
return (int(child.width), int(child.height)) 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 return None

View File

@ -331,10 +331,16 @@ class BlockContainer:
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self._blocks = [] self._blocks = []
@property
def blocks(self): 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): def add_block(self, block):
""" """

View File

@ -359,62 +359,48 @@ class Font:
"""Get the minimum width required for hyphenation to be considered""" """Get the minimum width required for hyphenation to be considered"""
return self._min_hyphenation_width 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): def with_size(self, size: int):
"""Create a new Font object with modified size""" """Create a new Font object with modified size"""
return Font( return self._with_modified(font_size=size)
self._font_path,
size,
self._colour,
self._weight,
self._style,
self._decoration,
self._background
)
def with_colour(self, colour: Tuple[int, int, int]): def with_colour(self, colour: Tuple[int, int, int]):
"""Create a new Font object with modified colour""" """Create a new Font object with modified colour"""
return Font( return self._with_modified(colour=colour)
self._font_path,
self._font_size,
colour,
self._weight,
self._style,
self._decoration,
self._background
)
def with_weight(self, weight: FontWeight): def with_weight(self, weight: FontWeight):
"""Create a new Font object with modified weight""" """Create a new Font object with modified weight"""
return Font( return self._with_modified(weight=weight)
self._font_path,
self._font_size,
self._colour,
weight,
self._style,
self._decoration,
self._background
)
def with_style(self, style: FontStyle): def with_style(self, style: FontStyle):
"""Create a new Font object with modified style""" """Create a new Font object with modified style"""
return Font( return self._with_modified(style=style)
self._font_path,
self._font_size,
self._colour,
self._weight,
style,
self._decoration,
self._background
)
def with_decoration(self, decoration: TextDecoration): def with_decoration(self, decoration: TextDecoration):
"""Create a new Font object with modified decoration""" """Create a new Font object with modified decoration"""
return Font( return self._with_modified(decoration=decoration)
self._font_path,
self._font_size,
self._colour,
self._weight,
self._style,
decoration,
self._background
)