Additional changes.
Some checks failed
Python CI / test (push) Failing after 1m35s

This commit is contained in:
Duncan Tourolle 2025-06-06 20:54:33 +02:00
parent f7ad69f9ec
commit c0c366e9f4
13 changed files with 2661 additions and 325 deletions

48
.gitea/workflows/ci.yml Normal file
View File

@ -0,0 +1,48 @@
name: Python CI
on:
push:
branches: [ main, master, develop ]
pull_request:
branches: [ main, master, develop ]
jobs:
test:
runs-on: self-hosted
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.x' # or specify version like '3.9', '3.10', etc.
- name: Install dependencies
run: |
python -m pip install --upgrade pip
# Install package in development mode
pip install -e .
# Install test dependencies if they exist
if [ -f requirements-dev.txt ]; then pip install -r requirements-dev.txt; fi
if [ -f requirements/test.txt ]; then pip install -r requirements/test.txt; fi
# Install common test packages
pip install pytest pytest-cov flake8
- name: Lint with flake8
run: |
# Stop the build if there are Python syntax errors or undefined names
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
# Exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
- name: Run tests with pytest
run: |
# Run tests with coverage
python -m pytest tests/ -v --cov=pyWebLayout --cov-report=term-missing
- name: Test package installation
run: |
# Test that the package can be imported
python -c "import pyWebLayout; print('Package imported successfully')"

View File

@ -1,4 +1,4 @@
from .block import Block, BlockType, Parapgraph, Heading, HeadingLevel, Quote, CodeBlock
from .block import Block, BlockType, Paragraph, Heading, HeadingLevel, Quote, CodeBlock
from .block import HList, ListItem, ListStyle, Table, TableRow, TableCell
from .block import HorizontalRule, LineBreak, Image
from .inline import Word, FormattedSpan

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
from __future__ import annotations
from typing import List, Dict, Optional, Tuple, Union, Any
from enum import Enum
from .block import Block, BlockType, Heading, HeadingLevel, Parapgraph
from .block import Block, BlockType, Heading, HeadingLevel, Paragraph
from .functional import Link, Button, Form
from .inline import Word, FormattedSpan
@ -27,13 +27,14 @@ class Document:
This class manages the logical structure of the document without rendering concerns.
"""
def __init__(self, title: Optional[str] = None, language: str = "en-US"):
def __init__(self, title: Optional[str] = None, language: str = "en-US", default_style=None):
"""
Initialize a new document.
Args:
title: The document title
language: The document language code
default_style: Optional default style for child blocks
"""
self._blocks: List[Block] = []
self._metadata: Dict[MetadataType, Any] = {}
@ -41,6 +42,7 @@ class Document:
self._resources: Dict[str, Any] = {} # External resources like images
self._stylesheets: List[Dict[str, Any]] = [] # CSS stylesheets
self._scripts: List[str] = [] # JavaScript code
self._default_style = default_style
# Set basic metadata
if title:
@ -52,6 +54,16 @@ class Document:
"""Get the top-level blocks in this document"""
return self._blocks
@property
def default_style(self):
"""Get the default style for this document"""
return self._default_style
@default_style.setter
def default_style(self, style):
"""Set the default style for this document"""
self._default_style = style
def add_block(self, block: Block):
"""
Add a block to this document.
@ -61,6 +73,55 @@ class Document:
"""
self._blocks.append(block)
def create_paragraph(self, style=None) -> Paragraph:
"""
Create a new paragraph and add it to this document.
Args:
style: Optional style override. If None, inherits from document
Returns:
The newly created Paragraph object
"""
if style is None:
style = self._default_style
paragraph = Paragraph(style)
self.add_block(paragraph)
return paragraph
def create_heading(self, level: HeadingLevel = HeadingLevel.H1, style=None) -> Heading:
"""
Create a new heading and add it to this document.
Args:
level: The heading level
style: Optional style override. If None, inherits from document
Returns:
The newly created Heading object
"""
if style is None:
style = self._default_style
heading = Heading(level, style)
self.add_block(heading)
return heading
def create_chapter(self, title: Optional[str] = None, level: int = 1, style=None) -> 'Chapter':
"""
Create a new chapter with inherited style.
Args:
title: The chapter title
level: The chapter level
style: Optional style override. If None, inherits from document
Returns:
The newly created Chapter object
"""
if style is None:
style = self._default_style
return Chapter(title, level, style)
def set_metadata(self, meta_type: MetadataType, value: Any):
"""
Set a metadata value.
@ -229,18 +290,20 @@ class Chapter:
A chapter contains a sequence of blocks and has metadata.
"""
def __init__(self, title: Optional[str] = None, level: int = 1):
def __init__(self, title: Optional[str] = None, level: int = 1, style=None):
"""
Initialize a new chapter.
Args:
title: The chapter title
level: The chapter level (1 = top level, 2 = subsection, etc.)
style: Optional default style for child blocks
"""
self._title = title
self._level = level
self._blocks: List[Block] = []
self._metadata: Dict[str, Any] = {}
self._style = style
@property
def title(self) -> Optional[str]:
@ -262,6 +325,16 @@ class Chapter:
"""Get the blocks in this chapter"""
return self._blocks
@property
def style(self):
"""Get the default style for this chapter"""
return self._style
@style.setter
def style(self, style):
"""Set the default style for this chapter"""
self._style = style
def add_block(self, block: Block):
"""
Add a block to this chapter.
@ -271,6 +344,39 @@ class Chapter:
"""
self._blocks.append(block)
def create_paragraph(self, style=None) -> Paragraph:
"""
Create a new paragraph and add it to this chapter.
Args:
style: Optional style override. If None, inherits from chapter
Returns:
The newly created Paragraph object
"""
if style is None:
style = self._style
paragraph = Paragraph(style)
self.add_block(paragraph)
return paragraph
def create_heading(self, level: HeadingLevel = HeadingLevel.H1, style=None) -> Heading:
"""
Create a new heading and add it to this chapter.
Args:
level: The heading level
style: Optional style override. If None, inherits from chapter
Returns:
The newly created Heading object
"""
if style is None:
style = self._style
heading = Heading(level, style)
self.add_block(heading)
return heading
def set_metadata(self, key: str, value: Any):
"""
Set a metadata value.
@ -300,7 +406,8 @@ class Book(Document):
A book is a document that contains chapters.
"""
def __init__(self, title: Optional[str] = None, author: Optional[str] = None, language: str = "en-US"):
def __init__(self, title: Optional[str] = None, author: Optional[str] = None,
language: str = "en-US", default_style=None):
"""
Initialize a new book.
@ -308,8 +415,9 @@ class Book(Document):
title: The book title
author: The book author
language: The book language code
default_style: Optional default style for child chapters and blocks
"""
super().__init__(title, language)
super().__init__(title, language, default_style)
self._chapters: List[Chapter] = []
if author:
@ -329,18 +437,21 @@ class Book(Document):
"""
self._chapters.append(chapter)
def create_chapter(self, title: Optional[str] = None, level: int = 1) -> Chapter:
def create_chapter(self, title: Optional[str] = None, level: int = 1, style=None) -> Chapter:
"""
Create and add a new chapter.
Create and add a new chapter with inherited style.
Args:
title: The chapter title
level: The chapter level
style: Optional style override. If None, inherits from book
Returns:
The new chapter
"""
chapter = Chapter(title, level)
if style is None:
style = self._default_style
chapter = Chapter(title, level, style)
self.add_chapter(chapter)
return chapter

View File

@ -28,6 +28,91 @@ class Word:
self._next = None
self._hyphenated_parts = None # Will store hyphenated parts if word is hyphenated
@classmethod
def create_and_add_to(cls, text: str, container, style: Optional[Font] = None,
background=None) -> 'Word':
"""
Create a new Word and add it to a container, inheriting style and language
from the container if not explicitly provided.
This method provides a convenient way to create words that automatically
inherit styling from their container (Paragraph, FormattedSpan, etc.)
without copying string values - using object references instead.
Args:
text: The text content of the word
container: The container to add the word to (must have add_word method and style property)
style: Optional Font style override. If None, inherits from container
background: Optional background color override. If None, inherits from container
Returns:
The newly created Word object
Raises:
AttributeError: If the container doesn't have the required add_word method or style property
"""
# Inherit style from container if not provided
if style is None:
if hasattr(container, 'style'):
style = container.style
else:
raise AttributeError(f"Container {type(container).__name__} must have a 'style' property")
# Inherit background from container if not provided
if background is None and hasattr(container, 'background'):
background = container.background
# Determine the previous word for proper linking
previous = None
if hasattr(container, '_words') and container._words:
# Container has a _words list (like FormattedSpan)
previous = container._words[-1]
elif hasattr(container, 'words'):
# Container has a words() method (like Paragraph)
try:
# Get the last word from the iterator
for _, word in container.words():
previous = word
except (StopIteration, TypeError):
previous = None
# Create the new word
word = cls(text, style, background, previous)
# Link the previous word to this new one
if previous:
previous.add_next(word)
# Add the word to the container
if hasattr(container, 'add_word'):
# Check if add_word expects a Word object or text string
import inspect
sig = inspect.signature(container.add_word)
params = list(sig.parameters.keys())
if len(params) > 0:
# Peek at the parameter name to guess the expected type
param_name = params[0]
if param_name in ['word', 'word_obj', 'word_object']:
# Expects a Word object
container.add_word(word)
else:
# Might expect text string (like FormattedSpan.add_word)
# In this case, we can't use the container's add_word as it would create
# a duplicate Word. We need to add directly to the container's word list.
if hasattr(container, '_words'):
container._words.append(word)
else:
# Fallback: try calling with the Word object anyway
container.add_word(word)
else:
# No parameters, shouldn't happen with add_word methods
container.add_word(word)
else:
raise AttributeError(f"Container {type(container).__name__} must have an 'add_word' method")
return word
@property
def text(self) -> str:
"""Get the text content of the word"""
@ -167,6 +252,45 @@ class FormattedSpan:
self._background = background if background else style.background
self._words: List[Word] = []
@classmethod
def create_and_add_to(cls, container, style: Optional[Font] = None, background=None) -> 'FormattedSpan':
"""
Create a new FormattedSpan and add it to a container, inheriting style from
the container if not explicitly provided.
Args:
container: The container to add the span to (must have add_span method and style property)
style: Optional Font style override. If None, inherits from container
background: Optional background color override
Returns:
The newly created FormattedSpan object
Raises:
AttributeError: If the container doesn't have the required add_span method or style property
"""
# Inherit style from container if not provided
if style is None:
if hasattr(container, 'style'):
style = container.style
else:
raise AttributeError(f"Container {type(container).__name__} must have a 'style' property")
# Inherit background from container if not provided
if background is None and hasattr(container, 'background'):
background = container.background
# Create the new span
span = cls(style, background)
# Add the span to the container
if hasattr(container, 'add_span'):
container.add_span(span)
else:
raise AttributeError(f"Container {type(container).__name__} must have an 'add_span' method")
return span
@property
def style(self) -> Font:
"""Get the font style of this span"""

View File

@ -27,7 +27,7 @@ def main():
parser.add_argument('epub_file', help='Path to EPUB file')
parser.add_argument('--output-dir', '-o', default='output', help='Output directory for rendered pages')
parser.add_argument('--width', '-w', type=int, default=800, help='Page width')
parser.add_argument('--height', '-h', type=int, default=1000, help='Page height')
parser.add_argument('--height', '-y', type=int, default=1000, help='Page height')
parser.add_argument('--margin', '-m', type=int, default=50, help='Page margin')
parser.add_argument('--max-pages', '-p', type=int, default=10, help='Maximum number of pages to render')
args = parser.parse_args()

View File

@ -0,0 +1,86 @@
#!/usr/bin/env python3
"""
Simple EPUB test script to isolate the issue.
"""
import sys
from pathlib import Path
# Add the parent directory to the path to import pyWebLayout
sys.path.append(str(Path(__file__).parent.parent.parent))
def test_epub_basic():
"""Test basic EPUB functionality without full HTML parsing."""
print("Testing basic EPUB components...")
try:
# Test basic document classes
from pyWebLayout.abstract.document import Document, Book, Chapter, MetadataType
print("✓ Document classes imported")
# Test creating a simple book
book = Book("Test Book", "Test Author")
chapter = book.create_chapter("Test Chapter")
print("✓ Book and chapter created")
return True
except Exception as e:
print(f"✗ Basic test failed: {e}")
import traceback
traceback.print_exc()
return False
def test_epub_file():
"""Test opening the EPUB file without full parsing."""
print("Testing EPUB file access...")
try:
import zipfile
import os
epub_path = "pg174-images-3.epub"
if not os.path.exists(epub_path):
print(f"✗ EPUB file not found: {epub_path}")
return False
with zipfile.ZipFile(epub_path, 'r') as zip_ref:
file_list = zip_ref.namelist()
print(f"✓ EPUB file opened, contains {len(file_list)} files")
# Look for key files
has_container = any('container.xml' in f for f in file_list)
has_opf = any('.opf' in f for f in file_list)
print(f"✓ Container file: {'found' if has_container else 'not found'}")
print(f"✓ Package file: {'found' if has_opf else 'not found'}")
return True
except Exception as e:
print(f"✗ EPUB file test failed: {e}")
import traceback
traceback.print_exc()
return False
def main():
print("Simple EPUB Test")
print("=" * 50)
# Test basic functionality
if not test_epub_basic():
return False
print()
# Test EPUB file access
if not test_epub_file():
return False
print()
print("All basic tests passed!")
return True
if __name__ == "__main__":
success = main()
sys.exit(0 if success else 1)

View File

@ -370,7 +370,7 @@ class EPUBReader:
# Parse HTML and add blocks to chapter
base_url = os.path.dirname(path)
document = parse_html(html, base_url)
document = parse_html(html, base_url=base_url)
# Copy blocks to the chapter
for block in document.blocks:
@ -381,8 +381,11 @@ class EPUBReader:
# Add an error message block
from pyWebLayout.abstract.block import Parapgraph
from pyWebLayout.abstract.inline import Word
from pyWebLayout.style import Font
error_para = Parapgraph()
error_para.add_word(Word(f"Error loading chapter: {str(e)}"))
# Create a default font style for the error message
default_font = Font()
error_para.add_word(Word(f"Error loading chapter: {str(e)}", default_font))
chapter.add_block(error_para)

View File

@ -0,0 +1,468 @@
"""
Unit tests for abstract document elements.
Tests the Document, Chapter, Book, and MetadataType classes that handle
document structure and metadata management.
"""
import unittest
from pyWebLayout.abstract.document import Document, Chapter, Book, MetadataType
from pyWebLayout.abstract.block import Parapgraph, Heading, HeadingLevel, BlockType
from pyWebLayout.abstract.inline import Word, FormattedSpan
from pyWebLayout.style import Font
class TestMetadataType(unittest.TestCase):
"""Test cases for MetadataType enum."""
def test_metadata_types(self):
"""Test that all expected metadata types exist."""
expected_types = [
'TITLE', 'AUTHOR', 'DESCRIPTION', 'KEYWORDS', 'LANGUAGE',
'PUBLICATION_DATE', 'MODIFIED_DATE', 'PUBLISHER', 'IDENTIFIER',
'COVER_IMAGE', 'CUSTOM'
]
for type_name in expected_types:
self.assertTrue(hasattr(MetadataType, type_name))
# Test custom type has expected value
self.assertEqual(MetadataType.CUSTOM.value, 100)
class TestDocument(unittest.TestCase):
"""Test cases for Document class."""
def setUp(self):
"""Set up test fixtures."""
self.doc = Document("Test Document", "en-US")
self.font = Font()
def test_document_creation(self):
"""Test document creation with basic parameters."""
self.assertEqual(self.doc.get_title(), "Test Document")
self.assertEqual(self.doc.get_metadata(MetadataType.LANGUAGE), "en-US")
self.assertEqual(len(self.doc.blocks), 0)
def test_document_creation_minimal(self):
"""Test document creation with minimal parameters."""
doc = Document()
self.assertIsNone(doc.get_title())
self.assertEqual(doc.get_metadata(MetadataType.LANGUAGE), "en-US")
def test_metadata_management(self):
"""Test setting and getting metadata."""
# Set various metadata types
self.doc.set_metadata(MetadataType.AUTHOR, "John Doe")
self.doc.set_metadata(MetadataType.DESCRIPTION, "A test document")
self.doc.set_metadata(MetadataType.KEYWORDS, ["test", "document"])
# Test retrieval
self.assertEqual(self.doc.get_metadata(MetadataType.AUTHOR), "John Doe")
self.assertEqual(self.doc.get_metadata(MetadataType.DESCRIPTION), "A test document")
self.assertEqual(self.doc.get_metadata(MetadataType.KEYWORDS), ["test", "document"])
# Test non-existent metadata
self.assertIsNone(self.doc.get_metadata(MetadataType.PUBLISHER))
def test_title_convenience_methods(self):
"""Test title getter and setter convenience methods."""
# Test setting title
self.doc.set_title("New Title")
self.assertEqual(self.doc.get_title(), "New Title")
# Test that it's also in metadata
self.assertEqual(self.doc.get_metadata(MetadataType.TITLE), "New Title")
def test_block_management(self):
"""Test adding and managing blocks."""
# Create some blocks
para1 = Parapgraph()
para2 = Parapgraph()
heading = Heading(HeadingLevel.H1)
# Add blocks
self.doc.add_block(para1)
self.doc.add_block(heading)
self.doc.add_block(para2)
# Test blocks list
self.assertEqual(len(self.doc.blocks), 3)
self.assertEqual(self.doc.blocks[0], para1)
self.assertEqual(self.doc.blocks[1], heading)
self.assertEqual(self.doc.blocks[2], para2)
def test_anchor_management(self):
"""Test named anchor functionality."""
heading = Heading(HeadingLevel.H1)
para = Parapgraph()
# Add anchors
self.doc.add_anchor("intro", heading)
self.doc.add_anchor("content", para)
# Test retrieval
self.assertEqual(self.doc.get_anchor("intro"), heading)
self.assertEqual(self.doc.get_anchor("content"), para)
self.assertIsNone(self.doc.get_anchor("nonexistent"))
def test_resource_management(self):
"""Test document resource management."""
# Add various resources
self.doc.add_resource("image1", {"type": "image", "path": "test.jpg"})
self.doc.add_resource("style1", {"type": "css", "content": "body {}"})
# Test retrieval
image = self.doc.get_resource("image1")
self.assertEqual(image["type"], "image")
self.assertEqual(image["path"], "test.jpg")
style = self.doc.get_resource("style1")
self.assertEqual(style["type"], "css")
# Test non-existent resource
self.assertIsNone(self.doc.get_resource("nonexistent"))
def test_stylesheet_management(self):
"""Test stylesheet addition."""
# Add stylesheets
css1 = {"href": "style.css", "type": "text/css"}
css2 = {"href": "theme.css", "type": "text/css"}
self.doc.add_stylesheet(css1)
self.doc.add_stylesheet(css2)
# Test that stylesheets are stored
self.assertEqual(len(self.doc._stylesheets), 2)
self.assertEqual(self.doc._stylesheets[0], css1)
self.assertEqual(self.doc._stylesheets[1], css2)
def test_script_management(self):
"""Test script addition."""
# Add scripts
script1 = "console.log('Hello');"
script2 = "document.ready(function(){});"
self.doc.add_script(script1)
self.doc.add_script(script2)
# Test that scripts are stored
self.assertEqual(len(self.doc._scripts), 2)
self.assertEqual(self.doc._scripts[0], script1)
self.assertEqual(self.doc._scripts[1], script2)
def test_find_blocks_by_type(self):
"""Test finding blocks by type."""
# Create blocks of different types
para1 = Parapgraph()
para2 = Parapgraph()
heading1 = Heading(HeadingLevel.H1)
heading2 = Heading(HeadingLevel.H2)
# Add blocks to document
self.doc.add_block(para1)
self.doc.add_block(heading1)
self.doc.add_block(para2)
self.doc.add_block(heading2)
# Test finding paragraphs
paragraphs = self.doc.find_blocks_by_type(BlockType.PARAGRAPH)
self.assertEqual(len(paragraphs), 2)
self.assertIn(para1, paragraphs)
self.assertIn(para2, paragraphs)
# Test finding headings
headings = self.doc.find_blocks_by_type(BlockType.HEADING)
self.assertEqual(len(headings), 2)
self.assertIn(heading1, headings)
self.assertIn(heading2, headings)
def test_find_headings(self):
"""Test finding heading blocks specifically."""
# Create mixed blocks
para = Parapgraph()
h1 = Heading(HeadingLevel.H1)
h2 = Heading(HeadingLevel.H2)
# Add words to headings for title extraction
word1 = Word("Chapter", self.font)
word2 = Word("One", self.font)
h1.add_word(word1)
h1.add_word(word2)
word3 = Word("Section", self.font)
h2.add_word(word3)
self.doc.add_block(para)
self.doc.add_block(h1)
self.doc.add_block(h2)
# Test finding headings
headings = self.doc.find_headings()
self.assertEqual(len(headings), 2)
self.assertIn(h1, headings)
self.assertIn(h2, headings)
self.assertNotIn(para, headings)
def test_generate_table_of_contents(self):
"""Test table of contents generation."""
# Create headings with content
h1 = Heading(HeadingLevel.H1)
h2 = Heading(HeadingLevel.H2)
h3 = Heading(HeadingLevel.H3)
# Add words to headings
h1.add_word(Word("Introduction", self.font))
h2.add_word(Word("Getting", self.font))
h2.add_word(Word("Started", self.font))
h3.add_word(Word("Installation", self.font))
self.doc.add_block(h1)
self.doc.add_block(h2)
self.doc.add_block(h3)
# Generate TOC
toc = self.doc.generate_table_of_contents()
# Test TOC structure
self.assertEqual(len(toc), 3)
# Test first entry
level, title, block = toc[0]
self.assertEqual(level, 1) # H1
self.assertEqual(title, "Introduction")
self.assertEqual(block, h1)
# Test second entry
level, title, block = toc[1]
self.assertEqual(level, 2) # H2
self.assertEqual(title, "Getting Started")
self.assertEqual(block, h2)
# Test third entry
level, title, block = toc[2]
self.assertEqual(level, 3) # H3
self.assertEqual(title, "Installation")
self.assertEqual(block, h3)
class TestChapter(unittest.TestCase):
"""Test cases for Chapter class."""
def setUp(self):
"""Set up test fixtures."""
self.chapter = Chapter("Test Chapter", 1)
def test_chapter_creation(self):
"""Test chapter creation."""
self.assertEqual(self.chapter.title, "Test Chapter")
self.assertEqual(self.chapter.level, 1)
self.assertEqual(len(self.chapter.blocks), 0)
def test_chapter_creation_minimal(self):
"""Test chapter creation with minimal parameters."""
chapter = Chapter()
self.assertIsNone(chapter.title)
self.assertEqual(chapter.level, 1)
def test_title_property(self):
"""Test title property getter and setter."""
# Test setter
self.chapter.title = "New Chapter Title"
self.assertEqual(self.chapter.title, "New Chapter Title")
# Test setting to None
self.chapter.title = None
self.assertIsNone(self.chapter.title)
def test_level_property(self):
"""Test level property."""
self.assertEqual(self.chapter.level, 1)
# Level should be read-only (no setter test)
# This is by design based on the class definition
def test_block_management(self):
"""Test adding blocks to chapter."""
para1 = Parapgraph()
para2 = Parapgraph()
heading = Heading(HeadingLevel.H2)
# Add blocks
self.chapter.add_block(para1)
self.chapter.add_block(heading)
self.chapter.add_block(para2)
# Test blocks list
self.assertEqual(len(self.chapter.blocks), 3)
self.assertEqual(self.chapter.blocks[0], para1)
self.assertEqual(self.chapter.blocks[1], heading)
self.assertEqual(self.chapter.blocks[2], para2)
def test_metadata_management(self):
"""Test chapter metadata."""
# Set metadata
self.chapter.set_metadata("author", "Jane Doe")
self.chapter.set_metadata("word_count", 1500)
self.chapter.set_metadata("tags", ["intro", "basics"])
# Test retrieval
self.assertEqual(self.chapter.get_metadata("author"), "Jane Doe")
self.assertEqual(self.chapter.get_metadata("word_count"), 1500)
self.assertEqual(self.chapter.get_metadata("tags"), ["intro", "basics"])
# Test non-existent metadata
self.assertIsNone(self.chapter.get_metadata("nonexistent"))
class TestBook(unittest.TestCase):
"""Test cases for Book class."""
def setUp(self):
"""Set up test fixtures."""
self.book = Book("Test Book", "Author Name", "en-US")
def test_book_creation(self):
"""Test book creation with all parameters."""
self.assertEqual(self.book.get_title(), "Test Book")
self.assertEqual(self.book.get_author(), "Author Name")
self.assertEqual(self.book.get_metadata(MetadataType.LANGUAGE), "en-US")
self.assertEqual(len(self.book.chapters), 0)
def test_book_creation_minimal(self):
"""Test book creation with minimal parameters."""
book = Book()
self.assertIsNone(book.get_title())
self.assertIsNone(book.get_author())
self.assertEqual(book.get_metadata(MetadataType.LANGUAGE), "en-US")
def test_book_creation_partial(self):
"""Test book creation with partial parameters."""
book = Book(title="Just Title")
self.assertEqual(book.get_title(), "Just Title")
self.assertIsNone(book.get_author())
def test_author_convenience_methods(self):
"""Test author getter and setter convenience methods."""
# Test setting author
self.book.set_author("New Author")
self.assertEqual(self.book.get_author(), "New Author")
# Test that it's also in metadata
self.assertEqual(self.book.get_metadata(MetadataType.AUTHOR), "New Author")
def test_chapter_management(self):
"""Test adding and managing chapters."""
# Create chapters
ch1 = Chapter("Introduction", 1)
ch2 = Chapter("Getting Started", 1)
ch3 = Chapter("Advanced Topics", 1)
# Add chapters
self.book.add_chapter(ch1)
self.book.add_chapter(ch2)
self.book.add_chapter(ch3)
# Test chapters list
self.assertEqual(len(self.book.chapters), 3)
self.assertEqual(self.book.chapters[0], ch1)
self.assertEqual(self.book.chapters[1], ch2)
self.assertEqual(self.book.chapters[2], ch3)
def test_create_chapter(self):
"""Test creating chapters through the book."""
# Create chapter with title and level
ch1 = self.book.create_chapter("Chapter 1", 1)
self.assertEqual(ch1.title, "Chapter 1")
self.assertEqual(ch1.level, 1)
self.assertEqual(len(self.book.chapters), 1)
self.assertEqual(self.book.chapters[0], ch1)
# Create chapter with minimal parameters
ch2 = self.book.create_chapter()
self.assertIsNone(ch2.title)
self.assertEqual(ch2.level, 1)
self.assertEqual(len(self.book.chapters), 2)
def test_generate_book_toc(self):
"""Test table of contents generation for book."""
# Create chapters with different levels
ch1 = Chapter("Introduction", 1)
ch2 = Chapter("Getting Started", 1)
ch3 = Chapter("Basic Concepts", 2)
ch4 = Chapter("Advanced Topics", 1)
ch5 = Chapter("Best Practices", 2)
# Add chapters to book
self.book.add_chapter(ch1)
self.book.add_chapter(ch2)
self.book.add_chapter(ch3)
self.book.add_chapter(ch4)
self.book.add_chapter(ch5)
# Generate TOC
toc = self.book.generate_table_of_contents()
# Test TOC structure
self.assertEqual(len(toc), 5)
# Test entries
expected = [
(1, "Introduction", ch1),
(1, "Getting Started", ch2),
(2, "Basic Concepts", ch3),
(1, "Advanced Topics", ch4),
(2, "Best Practices", ch5)
]
for i, (exp_level, exp_title, exp_chapter) in enumerate(expected):
level, title, chapter = toc[i]
self.assertEqual(level, exp_level)
self.assertEqual(title, exp_title)
self.assertEqual(chapter, exp_chapter)
def test_generate_book_toc_with_untitled_chapters(self):
"""Test TOC generation with chapters that have no title."""
# Create chapters, some without titles
ch1 = Chapter("Introduction", 1)
ch2 = Chapter(None, 1) # No title
ch3 = Chapter("Conclusion", 1)
self.book.add_chapter(ch1)
self.book.add_chapter(ch2)
self.book.add_chapter(ch3)
# Generate TOC
toc = self.book.generate_table_of_contents()
# Should only include chapters with titles
self.assertEqual(len(toc), 2)
level, title, chapter = toc[0]
self.assertEqual(title, "Introduction")
self.assertEqual(chapter, ch1)
level, title, chapter = toc[1]
self.assertEqual(title, "Conclusion")
self.assertEqual(chapter, ch3)
def test_book_inherits_document_features(self):
"""Test that Book inherits all Document functionality."""
# Test that book can use all document methods
# Add blocks directly to book
para = Parapgraph()
self.book.add_block(para)
self.assertEqual(len(self.book.blocks), 1)
# Test metadata
self.book.set_metadata(MetadataType.PUBLISHER, "Test Publisher")
self.assertEqual(self.book.get_metadata(MetadataType.PUBLISHER), "Test Publisher")
# Test anchors
heading = Heading(HeadingLevel.H1)
self.book.add_anchor("preface", heading)
self.assertEqual(self.book.get_anchor("preface"), heading)
if __name__ == '__main__':
unittest.main()

View File

@ -0,0 +1,529 @@
"""
Unit tests for abstract functional elements.
Tests the Link, Button, Form, FormField, and related classes that handle
interactive functionality and user interface elements.
"""
import unittest
from unittest.mock import Mock, patch
from pyWebLayout.abstract.functional import (
Link, LinkType, Button, Form, FormField, FormFieldType
)
class TestLinkType(unittest.TestCase):
"""Test cases for LinkType enum."""
def test_link_types(self):
"""Test that all expected link types exist."""
expected_types = ['INTERNAL', 'EXTERNAL', 'API', 'FUNCTION']
for type_name in expected_types:
self.assertTrue(hasattr(LinkType, type_name))
# Test specific values
self.assertEqual(LinkType.INTERNAL.value, 1)
self.assertEqual(LinkType.EXTERNAL.value, 2)
self.assertEqual(LinkType.API.value, 3)
self.assertEqual(LinkType.FUNCTION.value, 4)
class TestLink(unittest.TestCase):
"""Test cases for Link class."""
def setUp(self):
"""Set up test fixtures."""
self.mock_callback = Mock(return_value="callback_result")
def test_link_creation_minimal(self):
"""Test link creation with minimal parameters."""
link = Link("test-location")
self.assertEqual(link.location, "test-location")
self.assertEqual(link.link_type, LinkType.INTERNAL) # Default
self.assertEqual(link.params, {})
self.assertIsNone(link.title)
self.assertIsNone(link._callback)
def test_link_creation_full(self):
"""Test link creation with all parameters."""
params = {"param1": "value1", "param2": "value2"}
link = Link(
location="https://example.com",
link_type=LinkType.EXTERNAL,
callback=self.mock_callback,
params=params,
title="Example Link"
)
self.assertEqual(link.location, "https://example.com")
self.assertEqual(link.link_type, LinkType.EXTERNAL)
self.assertEqual(link.params, params)
self.assertEqual(link.title, "Example Link")
self.assertEqual(link._callback, self.mock_callback)
def test_internal_link_execution(self):
"""Test executing internal links."""
link = Link("#section1", LinkType.INTERNAL)
result = link.execute()
# Internal links should return the location
self.assertEqual(result, "#section1")
def test_external_link_execution(self):
"""Test executing external links."""
link = Link("https://example.com", LinkType.EXTERNAL)
result = link.execute()
# External links should return the location
self.assertEqual(result, "https://example.com")
def test_api_link_execution(self):
"""Test executing API links with callback."""
params = {"action": "save", "id": 123}
link = Link(
location="/api/save",
link_type=LinkType.API,
callback=self.mock_callback,
params=params
)
result = link.execute()
# Should call callback with location and params
self.mock_callback.assert_called_once_with("/api/save", action="save", id=123)
self.assertEqual(result, "callback_result")
def test_function_link_execution(self):
"""Test executing function links with callback."""
params = {"data": "test"}
link = Link(
location="save_document",
link_type=LinkType.FUNCTION,
callback=self.mock_callback,
params=params
)
result = link.execute()
# Should call callback with location and params
self.mock_callback.assert_called_once_with("save_document", data="test")
self.assertEqual(result, "callback_result")
def test_api_link_without_callback(self):
"""Test API link without callback returns location."""
link = Link("/api/endpoint", LinkType.API)
result = link.execute()
# Without callback, should return location
self.assertEqual(result, "/api/endpoint")
def test_function_link_without_callback(self):
"""Test function link without callback returns location."""
link = Link("function_name", LinkType.FUNCTION)
result = link.execute()
# Without callback, should return location
self.assertEqual(result, "function_name")
def test_link_properties(self):
"""Test link property access."""
params = {"key": "value"}
link = Link(
location="test",
link_type=LinkType.API,
params=params,
title="Test Title"
)
# Test all property getters
self.assertEqual(link.location, "test")
self.assertEqual(link.link_type, LinkType.API)
self.assertEqual(link.params, params)
self.assertEqual(link.title, "Test Title")
class TestButton(unittest.TestCase):
"""Test cases for Button class."""
def setUp(self):
"""Set up test fixtures."""
self.mock_callback = Mock(return_value="button_clicked")
def test_button_creation_minimal(self):
"""Test button creation with minimal parameters."""
button = Button("Click Me", self.mock_callback)
self.assertEqual(button.label, "Click Me")
self.assertEqual(button._callback, self.mock_callback)
self.assertEqual(button.params, {})
self.assertTrue(button.enabled)
def test_button_creation_full(self):
"""Test button creation with all parameters."""
params = {"action": "submit", "form_id": "test_form"}
button = Button(
label="Submit",
callback=self.mock_callback,
params=params,
enabled=False
)
self.assertEqual(button.label, "Submit")
self.assertEqual(button._callback, self.mock_callback)
self.assertEqual(button.params, params)
self.assertFalse(button.enabled)
def test_button_label_property(self):
"""Test button label getter and setter."""
button = Button("Original", self.mock_callback)
# Test getter
self.assertEqual(button.label, "Original")
# Test setter
button.label = "New Label"
self.assertEqual(button.label, "New Label")
def test_button_enabled_property(self):
"""Test button enabled getter and setter."""
button = Button("Test", self.mock_callback, enabled=True)
# Test initial state
self.assertTrue(button.enabled)
# Test setter
button.enabled = False
self.assertFalse(button.enabled)
button.enabled = True
self.assertTrue(button.enabled)
def test_button_execute_enabled(self):
"""Test executing enabled button."""
params = {"data": "test_data"}
button = Button("Test", self.mock_callback, params=params, enabled=True)
result = button.execute()
# Should call callback with params
self.mock_callback.assert_called_once_with(data="test_data")
self.assertEqual(result, "button_clicked")
def test_button_execute_disabled(self):
"""Test executing disabled button."""
button = Button("Test", self.mock_callback, enabled=False)
result = button.execute()
# Should not call callback and return None
self.mock_callback.assert_not_called()
self.assertIsNone(result)
def test_button_execute_no_callback(self):
"""Test executing button without callback."""
button = Button("Test", None, enabled=True)
result = button.execute()
# Should return None
self.assertIsNone(result)
class TestFormFieldType(unittest.TestCase):
"""Test cases for FormFieldType enum."""
def test_form_field_types(self):
"""Test that all expected form field types exist."""
expected_types = [
'TEXT', 'PASSWORD', 'CHECKBOX', 'RADIO', 'SELECT', 'TEXTAREA',
'NUMBER', 'DATE', 'TIME', 'EMAIL', 'URL', 'COLOR', 'RANGE', 'HIDDEN'
]
for type_name in expected_types:
self.assertTrue(hasattr(FormFieldType, type_name))
# Test some specific values
self.assertEqual(FormFieldType.TEXT.value, 1)
self.assertEqual(FormFieldType.PASSWORD.value, 2)
self.assertEqual(FormFieldType.HIDDEN.value, 14)
class TestFormField(unittest.TestCase):
"""Test cases for FormField class."""
def test_form_field_creation_minimal(self):
"""Test form field creation with minimal parameters."""
field = FormField("username", FormFieldType.TEXT)
self.assertEqual(field.name, "username")
self.assertEqual(field.field_type, FormFieldType.TEXT)
self.assertEqual(field.label, "username") # Default to name
self.assertIsNone(field.value)
self.assertFalse(field.required)
self.assertEqual(field.options, [])
self.assertIsNone(field.form)
def test_form_field_creation_full(self):
"""Test form field creation with all parameters."""
options = [("value1", "Label 1"), ("value2", "Label 2")]
field = FormField(
name="country",
field_type=FormFieldType.SELECT,
label="Country",
value="value1",
required=True,
options=options
)
self.assertEqual(field.name, "country")
self.assertEqual(field.field_type, FormFieldType.SELECT)
self.assertEqual(field.label, "Country")
self.assertEqual(field.value, "value1")
self.assertTrue(field.required)
self.assertEqual(field.options, options)
def test_form_field_value_property(self):
"""Test form field value getter and setter."""
field = FormField("test", FormFieldType.TEXT, value="initial")
# Test getter
self.assertEqual(field.value, "initial")
# Test setter
field.value = "new_value"
self.assertEqual(field.value, "new_value")
def test_form_field_form_property(self):
"""Test form field form getter and setter."""
field = FormField("test", FormFieldType.TEXT)
mock_form = Mock()
# Initial state
self.assertIsNone(field.form)
# Test setter
field.form = mock_form
self.assertEqual(field.form, mock_form)
def test_form_field_properties(self):
"""Test all form field property getters."""
options = [("opt1", "Option 1")]
field = FormField(
name="test_field",
field_type=FormFieldType.CHECKBOX,
label="Test Field",
value=True,
required=True,
options=options
)
# Test all getters
self.assertEqual(field.name, "test_field")
self.assertEqual(field.field_type, FormFieldType.CHECKBOX)
self.assertEqual(field.label, "Test Field")
self.assertTrue(field.value)
self.assertTrue(field.required)
self.assertEqual(field.options, options)
class TestForm(unittest.TestCase):
"""Test cases for Form class."""
def setUp(self):
"""Set up test fixtures."""
self.mock_callback = Mock(return_value="form_submitted")
def test_form_creation_minimal(self):
"""Test form creation with minimal parameters."""
form = Form("test_form")
self.assertEqual(form.form_id, "test_form")
self.assertIsNone(form.action)
self.assertIsNone(form._callback)
self.assertEqual(len(form._fields), 0)
def test_form_creation_full(self):
"""Test form creation with all parameters."""
form = Form(
form_id="contact_form",
action="/submit",
callback=self.mock_callback
)
self.assertEqual(form.form_id, "contact_form")
self.assertEqual(form.action, "/submit")
self.assertEqual(form._callback, self.mock_callback)
def test_form_field_management(self):
"""Test adding and retrieving form fields."""
form = Form("test_form")
# Create fields
field1 = FormField("username", FormFieldType.TEXT, value="john")
field2 = FormField("password", FormFieldType.PASSWORD, value="secret")
field3 = FormField("email", FormFieldType.EMAIL, value="john@example.com")
# Add fields
form.add_field(field1)
form.add_field(field2)
form.add_field(field3)
# Test that fields are stored correctly
self.assertEqual(len(form._fields), 3)
# Test field retrieval
self.assertEqual(form.get_field("username"), field1)
self.assertEqual(form.get_field("password"), field2)
self.assertEqual(form.get_field("email"), field3)
self.assertIsNone(form.get_field("nonexistent"))
# Test that fields have form reference
self.assertEqual(field1.form, form)
self.assertEqual(field2.form, form)
self.assertEqual(field3.form, form)
def test_form_get_values(self):
"""Test getting form values."""
form = Form("test_form")
# Add fields with values
form.add_field(FormField("name", FormFieldType.TEXT, value="John Doe"))
form.add_field(FormField("age", FormFieldType.NUMBER, value=30))
form.add_field(FormField("subscribe", FormFieldType.CHECKBOX, value=True))
# Get values
values = form.get_values()
expected = {
"name": "John Doe",
"age": 30,
"subscribe": True
}
self.assertEqual(values, expected)
def test_form_get_values_empty(self):
"""Test getting values from empty form."""
form = Form("empty_form")
values = form.get_values()
self.assertEqual(values, {})
def test_form_execute_with_callback(self):
"""Test executing form with callback."""
form = Form("test_form", callback=self.mock_callback)
# Add some fields
form.add_field(FormField("field1", FormFieldType.TEXT, value="value1"))
form.add_field(FormField("field2", FormFieldType.TEXT, value="value2"))
result = form.execute()
# Should call callback with form_id and values
expected_values = {"field1": "value1", "field2": "value2"}
self.mock_callback.assert_called_once_with("test_form", expected_values)
self.assertEqual(result, "form_submitted")
def test_form_execute_without_callback(self):
"""Test executing form without callback."""
form = Form("test_form")
# Add a field
form.add_field(FormField("test", FormFieldType.TEXT, value="test_value"))
result = form.execute()
# Should return the form values
expected = {"test": "test_value"}
self.assertEqual(result, expected)
def test_form_properties(self):
"""Test form property getters."""
form = Form("test_form", action="/submit")
self.assertEqual(form.form_id, "test_form")
self.assertEqual(form.action, "/submit")
class TestFormIntegration(unittest.TestCase):
"""Integration tests for form functionality."""
def test_complete_form_workflow(self):
"""Test a complete form creation and submission workflow."""
# Create form
form = Form("registration_form", action="/register")
# Add various field types
form.add_field(FormField(
"username", FormFieldType.TEXT,
label="Username", required=True, value="testuser"
))
form.add_field(FormField(
"password", FormFieldType.PASSWORD,
label="Password", required=True, value="secret123"
))
form.add_field(FormField(
"email", FormFieldType.EMAIL,
label="Email", required=True, value="test@example.com"
))
form.add_field(FormField(
"country", FormFieldType.SELECT,
label="Country", value="US",
options=[("US", "United States"), ("CA", "Canada"), ("UK", "United Kingdom")]
))
form.add_field(FormField(
"newsletter", FormFieldType.CHECKBOX,
label="Subscribe to newsletter", value=True
))
# Test form state
self.assertEqual(len(form._fields), 5)
# Test individual field access
username_field = form.get_field("username")
self.assertEqual(username_field.value, "testuser")
self.assertTrue(username_field.required)
# Test getting all values
values = form.get_values()
expected = {
"username": "testuser",
"password": "secret123",
"email": "test@example.com",
"country": "US",
"newsletter": True
}
self.assertEqual(values, expected)
# Test form submission
result = form.execute()
self.assertEqual(result, expected)
def test_form_field_modification(self):
"""Test modifying form fields after creation."""
form = Form("test_form")
# Add field
field = FormField("test", FormFieldType.TEXT, value="initial")
form.add_field(field)
# Modify field value
field.value = "modified"
# Test that form reflects the change
values = form.get_values()
self.assertEqual(values["test"], "modified")
# Test getting the modified field
retrieved_field = form.get_field("test")
self.assertEqual(retrieved_field.value, "modified")
if __name__ == '__main__':
unittest.main()

View File

@ -0,0 +1,520 @@
"""
Unit tests for abstract inline elements.
Tests the Word and FormattedSpan classes that handle inline text elements
and formatting within documents.
"""
import unittest
from unittest.mock import Mock, patch, MagicMock
from pyWebLayout.abstract.inline import Word, FormattedSpan
from pyWebLayout.style import Font
class TestWord(unittest.TestCase):
"""Test cases for Word class."""
def setUp(self):
"""Set up test fixtures."""
self.font = Font()
# Note: Font background is a tuple (255, 255, 255, 0) by default
# Note: Font language is set via constructor parameter (langauge - with typo)
def test_word_creation_minimal(self):
"""Test word creation with minimal parameters."""
word = Word("hello", self.font)
self.assertEqual(word.text, "hello")
self.assertEqual(word.style, self.font)
self.assertEqual(word.background, self.font.background)
self.assertIsNone(word.previous)
self.assertIsNone(word.next)
self.assertIsNone(word.hyphenated_parts)
def test_word_creation_with_previous(self):
"""Test word creation with previous word reference."""
word1 = Word("first", self.font)
word2 = Word("second", self.font, previous=word1)
self.assertEqual(word2.previous, word1)
self.assertIsNone(word1.previous)
self.assertIsNone(word1.next)
self.assertIsNone(word2.next)
def test_word_creation_with_background_override(self):
"""Test word creation with background color override."""
word = Word("test", self.font, background="yellow")
self.assertEqual(word.background, "yellow")
# Original font background should be unchanged - it's a tuple
self.assertEqual(word.style.background, (255, 255, 255, 0))
def test_word_properties(self):
"""Test word property getters."""
word1 = Word("first", self.font)
word2 = Word("second", self.font, background="blue", previous=word1)
# Test all properties
self.assertEqual(word2.text, "second")
self.assertEqual(word2.style, self.font)
self.assertEqual(word2.background, "blue")
self.assertEqual(word2.previous, word1)
self.assertIsNone(word2.next)
self.assertIsNone(word2.hyphenated_parts)
def test_add_next_word(self):
"""Test linking words with add_next method."""
word1 = Word("first", self.font)
word2 = Word("second", self.font)
word3 = Word("third", self.font)
# Link words
word1.add_next(word2)
word2.add_next(word3)
# Test forward links
self.assertEqual(word1.next, word2)
self.assertEqual(word2.next, word3)
self.assertIsNone(word3.next)
# Backward links should remain as set in constructor
self.assertIsNone(word1.previous)
self.assertIsNone(word2.previous)
self.assertIsNone(word3.previous)
def test_word_chain(self):
"""Test creating a chain of linked words."""
word1 = Word("first", self.font)
word2 = Word("second", self.font, previous=word1)
word3 = Word("third", self.font, previous=word2)
# Add forward links
word1.add_next(word2)
word2.add_next(word3)
# Test complete chain
self.assertIsNone(word1.previous)
self.assertEqual(word1.next, word2)
self.assertEqual(word2.previous, word1)
self.assertEqual(word2.next, word3)
self.assertEqual(word3.previous, word2)
self.assertIsNone(word3.next)
@patch('pyWebLayout.abstract.inline.pyphen')
def test_can_hyphenate_true(self, mock_pyphen):
"""Test can_hyphenate method when word can be hyphenated."""
# Mock pyphen behavior
mock_dic = Mock()
mock_dic.inserted.return_value = "hy-phen-ated"
mock_pyphen.Pyphen.return_value = mock_dic
word = Word("hyphenated", self.font)
result = word.can_hyphenate()
self.assertTrue(result)
# Font language is set as "en_EN" by default (with typo in constructor param)
mock_pyphen.Pyphen.assert_called_once_with(lang="en_EN")
mock_dic.inserted.assert_called_once_with("hyphenated", hyphen='-')
@patch('pyWebLayout.abstract.inline.pyphen')
def test_can_hyphenate_false(self, mock_pyphen):
"""Test can_hyphenate method when word cannot be hyphenated."""
# Mock pyphen behavior for non-hyphenatable word
mock_dic = Mock()
mock_dic.inserted.return_value = "cat" # No hyphens added
mock_pyphen.Pyphen.return_value = mock_dic
word = Word("cat", self.font)
result = word.can_hyphenate()
self.assertFalse(result)
mock_dic.inserted.assert_called_once_with("cat", hyphen='-')
@patch('pyWebLayout.abstract.inline.pyphen')
def test_can_hyphenate_with_language_override(self, mock_pyphen):
"""Test can_hyphenate with explicit language parameter."""
mock_dic = Mock()
mock_dic.inserted.return_value = "hy-phen"
mock_pyphen.Pyphen.return_value = mock_dic
word = Word("hyphen", self.font)
result = word.can_hyphenate("de_DE")
self.assertTrue(result)
mock_pyphen.Pyphen.assert_called_once_with(lang="de_DE")
@patch('pyWebLayout.abstract.inline.pyphen')
def test_hyphenate_success(self, mock_pyphen):
"""Test successful word hyphenation."""
# Mock pyphen behavior
mock_dic = Mock()
mock_dic.inserted.return_value = "hy-phen-ation"
mock_pyphen.Pyphen.return_value = mock_dic
word = Word("hyphenation", self.font)
result = word.hyphenate()
self.assertTrue(result)
self.assertEqual(word.hyphenated_parts, ["hy-", "phen-", "ation"])
mock_pyphen.Pyphen.assert_called_once_with(lang="en_EN")
@patch('pyWebLayout.abstract.inline.pyphen')
def test_hyphenate_failure(self, mock_pyphen):
"""Test word hyphenation when word cannot be hyphenated."""
# Mock pyphen behavior for non-hyphenatable word
mock_dic = Mock()
mock_dic.inserted.return_value = "cat" # No hyphens
mock_pyphen.Pyphen.return_value = mock_dic
word = Word("cat", self.font)
result = word.hyphenate()
self.assertFalse(result)
self.assertIsNone(word.hyphenated_parts)
@patch('pyWebLayout.abstract.inline.pyphen')
def test_hyphenate_with_language_override(self, mock_pyphen):
"""Test hyphenation with explicit language parameter."""
mock_dic = Mock()
mock_dic.inserted.return_value = "Wort-teil"
mock_pyphen.Pyphen.return_value = mock_dic
word = Word("Wortteil", self.font)
result = word.hyphenate("de_DE")
self.assertTrue(result)
self.assertEqual(word.hyphenated_parts, ["Wort-", "teil"])
mock_pyphen.Pyphen.assert_called_once_with(lang="de_DE")
def test_dehyphenate(self):
"""Test removing hyphenation from word."""
word = Word("test", self.font)
# Simulate hyphenated state
word._hyphenated_parts = ["test-", "ing"]
word.dehyphenate()
self.assertIsNone(word.hyphenated_parts)
def test_get_hyphenated_part(self):
"""Test getting specific hyphenated parts."""
word = Word("testing", self.font)
# Simulate hyphenated state
word._hyphenated_parts = ["test-", "ing"]
# Test valid indices
self.assertEqual(word.get_hyphenated_part(0), "test-")
self.assertEqual(word.get_hyphenated_part(1), "ing")
# Test invalid index
with self.assertRaises(IndexError):
word.get_hyphenated_part(2)
def test_get_hyphenated_part_not_hyphenated(self):
"""Test getting hyphenated part from non-hyphenated word."""
word = Word("test", self.font)
with self.assertRaises(IndexError) as context:
word.get_hyphenated_part(0)
self.assertIn("Word has not been hyphenated", str(context.exception))
def test_get_hyphenated_part_count(self):
"""Test getting hyphenated part count."""
word = Word("test", self.font)
# Test non-hyphenated word
self.assertEqual(word.get_hyphenated_part_count(), 0)
# Test hyphenated word
word._hyphenated_parts = ["hy-", "phen-", "ated"]
self.assertEqual(word.get_hyphenated_part_count(), 3)
@patch('pyWebLayout.abstract.inline.pyphen')
def test_complex_hyphenation_scenario(self, mock_pyphen):
"""Test complex hyphenation with multiple syllables."""
# Mock pyphen for a complex word
mock_dic = Mock()
mock_dic.inserted.return_value = "un-der-stand-ing"
mock_pyphen.Pyphen.return_value = mock_dic
word = Word("understanding", self.font)
result = word.hyphenate()
self.assertTrue(result)
expected_parts = ["un-", "der-", "stand-", "ing"]
self.assertEqual(word.hyphenated_parts, expected_parts)
self.assertEqual(word.get_hyphenated_part_count(), 4)
# Test getting individual parts
for i, expected_part in enumerate(expected_parts):
self.assertEqual(word.get_hyphenated_part(i), expected_part)
class TestFormattedSpan(unittest.TestCase):
"""Test cases for FormattedSpan class."""
def setUp(self):
"""Set up test fixtures."""
self.font = Font()
# Font background is a tuple, not a string
def test_formatted_span_creation_minimal(self):
"""Test formatted span creation with minimal parameters."""
span = FormattedSpan(self.font)
self.assertEqual(span.style, self.font)
self.assertEqual(span.background, self.font.background)
self.assertEqual(len(span.words), 0)
def test_formatted_span_creation_with_background(self):
"""Test formatted span creation with background override."""
span = FormattedSpan(self.font, background="yellow")
self.assertEqual(span.style, self.font)
self.assertEqual(span.background, "yellow")
self.assertNotEqual(span.background, self.font.background)
def test_formatted_span_properties(self):
"""Test formatted span property getters."""
span = FormattedSpan(self.font, background="blue")
self.assertEqual(span.style, self.font)
self.assertEqual(span.background, "blue")
self.assertIsInstance(span.words, list)
self.assertEqual(len(span.words), 0)
def test_add_single_word(self):
"""Test adding a single word to formatted span."""
span = FormattedSpan(self.font)
word = span.add_word("hello")
# Test returned word
self.assertIsInstance(word, Word)
self.assertEqual(word.text, "hello")
self.assertEqual(word.style, self.font)
self.assertEqual(word.background, self.font.background)
self.assertIsNone(word.previous)
# Test span state
self.assertEqual(len(span.words), 1)
self.assertEqual(span.words[0], word)
def test_add_multiple_words(self):
"""Test adding multiple words to formatted span."""
span = FormattedSpan(self.font)
word1 = span.add_word("first")
word2 = span.add_word("second")
word3 = span.add_word("third")
# Test span contains all words
self.assertEqual(len(span.words), 3)
self.assertEqual(span.words[0], word1)
self.assertEqual(span.words[1], word2)
self.assertEqual(span.words[2], word3)
# Test word linking
self.assertIsNone(word1.previous)
self.assertEqual(word1.next, word2)
self.assertEqual(word2.previous, word1)
self.assertEqual(word2.next, word3)
self.assertEqual(word3.previous, word2)
self.assertIsNone(word3.next)
def test_add_word_with_background_override(self):
"""Test that added words inherit span background."""
span = FormattedSpan(self.font, background="red")
word = span.add_word("test")
# Word should inherit span's background
self.assertEqual(word.background, "red")
self.assertEqual(word.style, self.font)
def test_word_style_consistency(self):
"""Test that all words in span have consistent style."""
span = FormattedSpan(self.font, background="green")
words = []
for text in ["this", "is", "a", "test"]:
words.append(span.add_word(text))
# All words should have same style and background
for word in words:
self.assertEqual(word.style, self.font)
self.assertEqual(word.background, "green")
def test_word_chain_integrity(self):
"""Test that word chain is properly maintained."""
span = FormattedSpan(self.font)
words = []
for i in range(5):
words.append(span.add_word(f"word{i}"))
# Test complete chain
for i in range(5):
word = words[i]
# Test previous link
if i == 0:
self.assertIsNone(word.previous)
else:
self.assertEqual(word.previous, words[i-1])
# Test next link
if i == 4:
self.assertIsNone(word.next)
else:
self.assertEqual(word.next, words[i+1])
def test_empty_span_operations(self):
"""Test operations on empty formatted span."""
span = FormattedSpan(self.font)
# Test empty state
self.assertEqual(len(span.words), 0)
self.assertEqual(span.words, [])
# Add first word
first_word = span.add_word("first")
self.assertIsNone(first_word.previous)
self.assertIsNone(first_word.next)
class TestWordFormattedSpanIntegration(unittest.TestCase):
"""Integration tests between Word and FormattedSpan classes."""
def setUp(self):
"""Set up test fixtures."""
self.font = Font()
# Font background is a tuple, not a string
def test_sentence_creation(self):
"""Test creating a complete sentence with formatted span."""
span = FormattedSpan(self.font)
sentence_words = ["The", "quick", "brown", "fox", "jumps"]
words = []
for word_text in sentence_words:
words.append(span.add_word(word_text))
# Test sentence structure
self.assertEqual(len(span.words), 5)
# Test word content
for i, expected_text in enumerate(sentence_words):
self.assertEqual(words[i].text, expected_text)
# Test linking
for i in range(5):
if i > 0:
self.assertEqual(words[i].previous, words[i-1])
if i < 4:
self.assertEqual(words[i].next, words[i+1])
@patch('pyWebLayout.abstract.inline.pyphen')
def test_span_with_hyphenated_words(self, mock_pyphen):
"""Test formatted span containing hyphenated words."""
# Mock pyphen
mock_dic = Mock()
mock_pyphen.Pyphen.return_value = mock_dic
def mock_inserted(word, hyphen='-'):
if word == "understanding":
return "un-der-stand-ing"
elif word == "hyphenation":
return "hy-phen-ation"
else:
return word # No hyphenation
mock_dic.inserted.side_effect = mock_inserted
span = FormattedSpan(self.font)
# Add words, some of which can be hyphenated
word1 = span.add_word("The")
word2 = span.add_word("understanding")
word3 = span.add_word("of")
word4 = span.add_word("hyphenation")
# Test hyphenation
self.assertTrue(word2.can_hyphenate())
self.assertTrue(word2.hyphenate())
self.assertFalse(word1.can_hyphenate())
self.assertTrue(word4.can_hyphenate())
self.assertTrue(word4.hyphenate())
# Test hyphenated parts
self.assertEqual(word2.hyphenated_parts, ["un-", "der-", "stand-", "ing"])
self.assertEqual(word4.hyphenated_parts, ["hy-", "phen-", "ation"])
self.assertIsNone(word1.hyphenated_parts)
self.assertIsNone(word3.hyphenated_parts)
def test_multiple_spans_same_style(self):
"""Test creating multiple spans with the same style."""
font = Font()
span1 = FormattedSpan(font)
span2 = FormattedSpan(font)
# Add words to both spans
span1_words = [span1.add_word("First"), span1.add_word("span")]
span2_words = [span2.add_word("Second"), span2.add_word("span")]
# Test that spans are independent
self.assertEqual(len(span1.words), 2)
self.assertEqual(len(span2.words), 2)
# Test that words in different spans are not linked
self.assertIsNone(span1_words[1].next)
self.assertIsNone(span2_words[0].previous)
# But words within spans are linked
self.assertEqual(span1_words[0].next, span1_words[1])
self.assertEqual(span2_words[1].previous, span2_words[0])
def test_span_style_inheritance(self):
"""Test that words properly inherit span styling."""
font = Font()
# Font background is a tuple (255, 255, 255, 0)
# Test with span background override
span = FormattedSpan(font, background="lightgreen")
word1 = span.add_word("styled")
word2 = span.add_word("text")
# Both words should have span's background, not font's
self.assertEqual(word1.background, "lightgreen")
self.assertEqual(word2.background, "lightgreen")
# But they should have font's other properties
self.assertEqual(word1.style, font)
self.assertEqual(word2.style, font)
def test_word_modification_after_creation(self):
"""Test modifying words after they've been added to span."""
span = FormattedSpan(self.font)
word = span.add_word("original")
# Verify initial state
self.assertEqual(word.text, "original")
self.assertEqual(len(span.words), 1)
# Words are immutable by design (no setter for text property)
# But we can test that the reference is maintained
self.assertEqual(span.words[0], word)
self.assertEqual(span.words[0].text, "original")
if __name__ == '__main__':
unittest.main()

View File

@ -0,0 +1,176 @@
#!/usr/bin/env python3
"""
Demonstration of the new create_and_add_to pattern in pyWebLayout.
This script shows how the pattern enables automatic style and language inheritance
throughout the document hierarchy without copying strings - using object references instead.
"""
# Mock the style system for this demonstration
class MockFont:
def __init__(self, family="Arial", size=12, language="en-US", background="white"):
self.family = family
self.size = size
self.language = language
self.background = background
def __str__(self):
return f"Font(family={self.family}, size={self.size}, lang={self.language}, bg={self.background})"
# Import the abstract classes
from pyWebLayout.abstract import (
Document, Paragraph, Heading, HeadingLevel, Quote, HList, ListStyle,
Table, TableRow, TableCell, Word, FormattedSpan
)
def demonstrate_create_and_add_pattern():
"""Demonstrate the create_and_add_to pattern with style inheritance."""
print("=== pyWebLayout create_and_add_to Pattern Demonstration ===\n")
# Create a document with a default style
document_style = MockFont(family="Georgia", size=14, language="en-US", background="white")
doc = Document("Style Inheritance Demo", default_style=document_style)
print(f"1. Document created with style: {document_style}")
print(f" Document default style: {doc.default_style}\n")
# Create a paragraph using the new pattern - it inherits the document's style
para1 = Paragraph.create_and_add_to(doc)
print(f"2. Paragraph created with inherited style: {para1.style}")
print(f" Style object ID matches document: {id(para1.style) == id(doc.default_style)}")
print(f" Number of blocks in document: {len(doc.blocks)}\n")
# Create words using the paragraph's create_word method
word1 = para1.create_word("Hello")
word2 = para1.create_word("World")
print(f"3. Words created with inherited paragraph style:")
print(f" Word 1 '{word1.text}' style: {word1.style}")
print(f" Word 2 '{word2.text}' style: {word2.style}")
print(f" Style object IDs match paragraph: {id(word1.style) == id(para1.style)}")
print(f" Word count in paragraph: {para1.word_count}\n")
# Create a quote with a different style
quote_style = MockFont(family="Times", size=13, language="en-US", background="lightgray")
quote = Quote.create_and_add_to(doc, style=quote_style)
print(f"4. Quote created with custom style: {quote.style}")
print(f" Style object ID different from document: {id(quote.style) != id(doc.default_style)}")
# Create a paragraph inside the quote - it inherits the quote's style
quote_para = Paragraph.create_and_add_to(quote)
print(f" Quote paragraph inherits quote style: {quote_para.style}")
print(f" Style object ID matches quote: {id(quote_para.style) == id(quote.style)}\n")
# Create a heading with specific styling
heading_style = MockFont(family="Arial Black", size=18, language="en-US", background="white")
heading = Heading.create_and_add_to(doc, HeadingLevel.H1, style=heading_style)
print(f"5. Heading created with custom style: {heading.style}")
# Add words to the heading
heading.create_word("Chapter")
heading.create_word("One")
print(f" Heading words inherit heading style:")
for i, word in heading.words():
print(f" - Word {i}: '{word.text}' with style: {word.style}")
print()
# Create a list with inherited style
list_obj = HList.create_and_add_to(doc, ListStyle.UNORDERED)
print(f"6. List created with inherited document style: {list_obj.default_style}")
# Create list items that inherit from the list
item1 = list_obj.create_item()
item2 = list_obj.create_item()
print(f" List item 1 style: {item1.style}")
print(f" List item 2 style: {item2.style}")
print(f" Both inherit from list: {id(item1.style) == id(list_obj.default_style)}")
# Create paragraphs in list items
item1_para = item1.create_paragraph()
item2_para = item2.create_paragraph()
print(f" Item 1 paragraph style: {item1_para.style}")
print(f" Item 2 paragraph style: {item2_para.style}")
print(f" Both inherit from list item: {id(item1_para.style) == id(item1.style)}\n")
# Create a table with inherited style
table = Table.create_and_add_to(doc, "Example Table")
print(f"7. Table created with inherited document style: {table.style}")
# Create table rows and cells
header_row = table.create_row("header")
header_cell1 = header_row.create_cell(is_header=True)
header_cell2 = header_row.create_cell(is_header=True)
print(f" Header row style: {header_row.style}")
print(f" Header cell 1 style: {header_cell1.style}")
print(f" Header cell 2 style: {header_cell2.style}")
# Create paragraphs in cells
cell1_para = header_cell1.create_paragraph()
cell2_para = header_cell2.create_paragraph()
print(f" Cell 1 paragraph style: {cell1_para.style}")
print(f" Cell 2 paragraph style: {cell2_para.style}")
# Add words to cell paragraphs
cell1_para.create_word("Name")
cell2_para.create_word("Age")
print(f" All styles inherit properly through the hierarchy\n")
# Create a formatted span to show style inheritance
span = para1.create_span()
span_word = span.add_word("formatted")
print(f"8. FormattedSpan and Word inheritance:")
print(f" Span style: {span.style}")
print(f" Span word style: {span_word.style}")
print(f" Both inherit from paragraph: {id(span.style) == id(para1.style)}")
print()
# Demonstrate the object reference pattern vs string copying
print("9. Object Reference vs String Copying Demonstration:")
print(" - All child elements reference the SAME style object")
print(" - No string copying occurs - efficient memory usage")
print(" - Changes to parent style affect all children automatically")
print()
# Show the complete hierarchy
print("10. Document Structure Summary:")
print(f" Document blocks: {len(doc.blocks)}")
for i, block in enumerate(doc.blocks):
if hasattr(block, 'word_count'):
print(f" - Block {i}: {type(block).__name__} with {block.word_count} words")
elif hasattr(block, 'item_count'):
print(f" - Block {i}: {type(block).__name__} with {block.item_count} items")
elif hasattr(block, 'row_count'):
counts = block.row_count
print(f" - Block {i}: {type(block).__name__} with {counts['total']} total rows")
else:
print(f" - Block {i}: {type(block).__name__}")
print("\n=== Pattern Benefits ===")
print("✓ Automatic style inheritance throughout document hierarchy")
print("✓ Object references instead of string copying (memory efficient)")
print("✓ Consistent API pattern across all container/child relationships")
print("✓ Language and styling properties inherited as objects")
print("✓ Easy to use fluent interface for document building")
print("✓ Type safety with proper return types")
if __name__ == "__main__":
try:
demonstrate_create_and_add_pattern()
except ImportError as e:
print(f"Import error: {e}")
print("Note: This demo requires the pyWebLayout abstract classes")
print("Make sure the pyWebLayout package is in your Python path")
except Exception as e:
print(f"Error during demonstration: {e}")
import traceback
traceback.print_exc()

44
tests/test_epub_fix.py Normal file
View File

@ -0,0 +1,44 @@
#!/usr/bin/env python3
"""
Simple test script to verify that the EPUB reader fixes are working correctly.
"""
import sys
import os
# Add the pyWebLayout directory to the Python path
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'pyWebLayout'))
try:
from pyWebLayout.io.readers.epub_reader import read_epub
print("Successfully imported epub_reader module")
# Test reading the EPUB file
epub_path = os.path.join('pyWebLayout', 'examples', 'pg174-images-3.epub')
if not os.path.exists(epub_path):
print(f"EPUB file not found: {epub_path}")
sys.exit(1)
print(f"Reading EPUB file: {epub_path}")
# Try to read the EPUB
book = read_epub(epub_path)
print(f"Successfully read EPUB file!")
print(f"Book title: {book.title}")
print(f"Number of chapters: {len(book.chapters)}")
# Check first chapter
if book.chapters:
first_chapter = book.chapters[0]
print(f"First chapter title: {first_chapter.title}")
print(f"First chapter has {len(first_chapter.blocks)} blocks")
except Exception as e:
print(f"Error: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
print("Test completed successfully!")