""" Test the new abstract/concrete style system. This test demonstrates how the new style system addresses the memory efficiency concerns by using abstract styles that can be resolved to concrete styles based on user preferences. """ import pytest from pyWebLayout.style.abstract_style import ( AbstractStyle, AbstractStyleRegistry, FontFamily, FontSize ) from pyWebLayout.style.concrete_style import ( ConcreteStyleRegistry, RenderingContext, StyleResolver ) from pyWebLayout.style.fonts import FontWeight def test_abstract_style_is_hashable(): """Test that AbstractStyle objects are hashable and can be used as dict keys.""" # Create two identical styles style1 = AbstractStyle( font_family=FontFamily.SERIF, font_size=16, font_weight=FontWeight.BOLD, color="red" ) style2 = AbstractStyle( font_family=FontFamily.SERIF, font_size=16, font_weight=FontWeight.BOLD, color="red" ) # They should be equal and have the same hash assert style1 == style2 assert hash(style1) == hash(style2) # They should work as dictionary keys style_dict = {style1: "first", style2: "second"} assert len(style_dict) == 1 # Should be deduplicated assert style_dict[style1] == "second" # Last value wins def test_abstract_style_registry_deduplication(): """Test that the registry prevents duplicate styles.""" registry = AbstractStyleRegistry() # Create the same style twice style1 = AbstractStyle(font_size=18, font_weight=FontWeight.BOLD) style2 = AbstractStyle(font_size=18, font_weight=FontWeight.BOLD) # Register both - should get same ID id1, _ = registry.get_or_create_style(style1) id2, _ = registry.get_or_create_style(style2) assert id1 == id2 # Same style should get same ID assert registry.get_style_count() == 2 # Only default + our style def test_style_inheritance(): """Test that style inheritance works properly.""" registry = AbstractStyleRegistry() # Create base style base_style = AbstractStyle(font_size=16, color="black") base_id, _ = registry.get_or_create_style(base_style) # Create derived style derived_id, derived_style = registry.create_derived_style( base_id, font_weight=FontWeight.BOLD, color="red" ) # Resolve effective style effective = registry.resolve_effective_style(derived_id) assert effective.font_size == 16 # Inherited from base assert effective.font_weight == FontWeight.BOLD # Overridden assert effective.color == "red" # Overridden def test_style_resolver_user_preferences(): """Test that user preferences affect concrete style resolution.""" # Create rendering context with larger fonts context = RenderingContext( base_font_size=20, # Larger base size font_scale_factor=1.5, # Additional scaling large_text=True # Accessibility preference ) resolver = StyleResolver(context) # Create abstract style with medium size abstract_style = AbstractStyle(font_size=FontSize.MEDIUM) # Resolve to concrete style concrete_style = resolver.resolve_style(abstract_style) # Font size should be: 20 (base) * 1.0 (medium) * 1.5 (scale) * 1.2 # (large_text) = 36 expected_size = int(20 * 1.0 * 1.5 * 1.2) assert concrete_style.font_size == expected_size def test_style_resolver_color_resolution(): """Test color name resolution.""" context = RenderingContext() resolver = StyleResolver(context) # Test named colors red_style = AbstractStyle(color="red") concrete_red = resolver.resolve_style(red_style) assert concrete_red.color == (255, 0, 0) # Test hex colors hex_style = AbstractStyle(color="#ff0000") concrete_hex = resolver.resolve_style(hex_style) assert concrete_hex.color == (255, 0, 0) # Test RGB tuple (should pass through) rgb_style = AbstractStyle(color=(128, 64, 192)) concrete_rgb = resolver.resolve_style(rgb_style) assert concrete_rgb.color == (128, 64, 192) def test_concrete_style_caching(): """Test that concrete styles are cached efficiently.""" context = RenderingContext() registry = ConcreteStyleRegistry(StyleResolver(context)) # Create abstract style abstract_style = AbstractStyle(font_size=16, color="blue") # Get font twice - should be cached font1 = registry.get_font(abstract_style) font2 = registry.get_font(abstract_style) # Should be the same object (cached) assert font1 is font2 # Check cache stats stats = registry.get_cache_stats() assert stats["concrete_styles"] == 1 assert stats["fonts"] == 1 def test_global_font_scaling(): """Test that global font scaling affects all text.""" # Create two contexts with different scaling context_normal = RenderingContext(font_scale_factor=1.0) context_large = RenderingContext(font_scale_factor=2.0) resolver_normal = StyleResolver(context_normal) resolver_large = StyleResolver(context_large) # Same abstract style abstract_style = AbstractStyle(font_size=16) # Resolve with different contexts concrete_normal = resolver_normal.resolve_style(abstract_style) concrete_large = resolver_large.resolve_style(abstract_style) # Large should be 2x the size assert concrete_large.font_size == concrete_normal.font_size * 2 def test_memory_efficiency(): """Test that the new system is more memory efficient.""" registry = AbstractStyleRegistry() # Create many "different" styles that are actually the same styles = [] for i in range(100): # All these styles are identical style = AbstractStyle( font_size=16, font_weight=FontWeight.NORMAL, color="black" ) style_id, _ = registry.get_or_create_style(style) styles.append(style_id) # All should reference the same style assert len(set(styles)) == 1 # All IDs are the same assert registry.get_style_count() == 2 # Only default + our style # This demonstrates that we don't create duplicate styles def test_word_style_reference_concept(): """Demonstrate how words would reference styles instead of storing fonts.""" registry = AbstractStyleRegistry() # Create paragraph style para_style = AbstractStyle(font_size=16, color="black") para_id, _ = registry.get_or_create_style(para_style) # Create bold word style bold_style = AbstractStyle(font_size=16, color="black", font_weight=FontWeight.BOLD) bold_id, _ = registry.get_or_create_style(bold_style) # Simulate words storing style IDs instead of full Font objects words_data = [ {"text": "This", "style_id": para_id}, {"text": "is", "style_id": para_id}, {"text": "bold", "style_id": bold_id}, {"text": "text", "style_id": para_id}, ] # To get the actual font for rendering, we resolve through registry context = RenderingContext() concrete_registry = ConcreteStyleRegistry(StyleResolver(context)) for word_data in words_data: abstract_style = registry.get_style_by_id(word_data["style_id"]) font = concrete_registry.get_font(abstract_style) # Now we have the actual Font object for rendering assert font is not None assert hasattr(font, 'font_size') # Bold word should have bold weight if word_data["text"] == "bold": assert font.weight == FontWeight.BOLD else: assert font.weight == FontWeight.NORMAL if __name__ == "__main__": pytest.main([__file__])