From ed39f40bad94b9f6f39351540d558548f08079f5 Mon Sep 17 00:00:00 2001 From: Duncan Tourolle Date: Sat, 7 Jun 2025 17:10:35 +0200 Subject: [PATCH] Add: Coverage gutters Remove: Un-used funcs --- COVERAGE_GUTTERS_SETUP.md | 122 + coverage.xml | 4019 +++++++++++++++++++++ pyWebLayout/io/readers/html_extraction.py | 506 +-- pyproject.toml | 31 + run_coverage_gutters.py | 48 + scripts/run_coverage.py | 4 +- 6 files changed, 4484 insertions(+), 246 deletions(-) create mode 100644 COVERAGE_GUTTERS_SETUP.md create mode 100644 coverage.xml create mode 100644 run_coverage_gutters.py diff --git a/COVERAGE_GUTTERS_SETUP.md b/COVERAGE_GUTTERS_SETUP.md new file mode 100644 index 0000000..f037e49 --- /dev/null +++ b/COVERAGE_GUTTERS_SETUP.md @@ -0,0 +1,122 @@ +# Coverage Gutters Setup Guide + +This guide helps you set up Coverage Gutters in VSCode to visualize code coverage for the pyWebLayout project. + +## Prerequisites + +1. **Install VSCode Extension**: Make sure you have the "Coverage Gutters" extension installed in VSCode. +2. **Install Python packages**: + ```bash + pip install pytest pytest-cov coverage + ``` + +## Configuration Files + +### 1. VSCode Settings (`.vscode/settings.json`) + +Your VSCode settings are already configured with: +```json +{ + "coverage-gutters.coverageBaseDir": "./", + "coverage-gutters.coverageFileNames": [ + "coverage.xml", + "lcov.info", + "cov.xml", + "coverage.info" + ], + "coverage-gutters.showGutterCoverage": true, + "coverage-gutters.showLineCoverage": true, + "coverage-gutters.showRulerCoverage": true, + "coverage-gutters.xmlname": "coverage.xml" +} +``` + +### 2. Coverage Configuration (`pyproject.toml`) + +Coverage settings are configured in `pyproject.toml`: +```toml +[tool.coverage.run] +source = ["pyWebLayout"] +branch = true +omit = [ + "*/tests/*", + "*/test_*", + "setup.py", + "*/examples/*", + "*/__main__.py" +] + +[tool.coverage.xml] +output = "coverage.xml" + +[tool.coverage.html] +directory = "htmlcov" +``` + +## How to Generate Coverage + +### Option 1: Quick Coverage for Gutters +```bash +python run_coverage_gutters.py +``` + +### Option 2: Manual pytest command +```bash +python -m pytest tests/ --cov=pyWebLayout --cov-report=xml --cov-report=html --cov-report=term +``` + +### Option 3: Full coverage analysis +```bash +python scripts/run_coverage.py +``` + +## Using Coverage Gutters in VSCode + +1. **Generate coverage data** using one of the options above +2. **Open VSCode** and navigate to your Python source files +3. **Enable Coverage Gutters**: + - Press `Ctrl+Shift+P` (or `Cmd+Shift+P` on Mac) + - Type "Coverage Gutters: Display Coverage" + - Or click the "Watch" button in the status bar + +## What You'll See + +- **Green lines**: Code that is covered by tests +- **Red lines**: Code that is NOT covered by tests +- **Yellow lines**: Partially covered code (branches) +- **Coverage percentage** in the status bar + +## Troubleshooting + +1. **No coverage showing**: + - Make sure `coverage.xml` exists in the project root + - Check that the Coverage Gutters extension is enabled + - Try reloading VSCode window + +2. **Coverage not updating**: + - Re-run the coverage command + - Click "Watch" in the status bar to refresh + +3. **Tests not running**: + - Make sure you're in the project root directory + - Install missing dependencies: `pip install pytest pytest-cov` + +## Coverage Files Generated + +After running coverage, you should see: +- `coverage.xml` - XML format for Coverage Gutters +- `htmlcov/` - HTML coverage report directory +- `coverage.json` - JSON format for badges +- `.coverage` - Coverage database file + +## Commands Summary + +```bash +# Install dependencies +pip install pytest pytest-cov coverage + +# Generate coverage for gutters +python run_coverage_gutters.py + +# View HTML report +open htmlcov/index.html diff --git a/coverage.xml b/coverage.xml new file mode 100644 index 0000000..ab2f637 --- /dev/null +++ b/coverage.xml @@ -0,0 +1,4019 @@ + + + + + + /home/dtourolle/Development/pyWebLayout/pyWebLayout + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pyWebLayout/io/readers/html_extraction.py b/pyWebLayout/io/readers/html_extraction.py index c64a8bf..04d9064 100644 --- a/pyWebLayout/io/readers/html_extraction.py +++ b/pyWebLayout/io/readers/html_extraction.py @@ -11,9 +11,20 @@ from typing import List, Dict, Any, Optional, Union, Callable, Tuple, NamedTuple from bs4 import BeautifulSoup, Tag, NavigableString from pyWebLayout.abstract.inline import Word, FormattedSpan from pyWebLayout.abstract.block import ( - Block, Paragraph, Heading, HeadingLevel, Quote, CodeBlock, - HList, ListItem, ListStyle, Table, TableRow, TableCell, - HorizontalRule, Image + Block, + Paragraph, + Heading, + HeadingLevel, + Quote, + CodeBlock, + HList, + ListItem, + ListStyle, + Table, + TableRow, + TableCell, + HorizontalRule, + Image, ) from pyWebLayout.style import Font, FontWeight, FontStyle, TextDecoration @@ -23,34 +34,37 @@ class StyleContext(NamedTuple): Immutable style context passed to handler functions. Contains all styling information including inherited styles, CSS hints, and element attributes. """ + font: Font background: Optional[Tuple[int, int, int, int]] css_classes: set css_styles: Dict[str, str] element_attributes: Dict[str, Any] parent_elements: List[str] # Stack of parent element names - - def with_font(self, font: Font) -> 'StyleContext': + + def with_font(self, font: Font) -> "StyleContext": """Create new context with modified font.""" return self._replace(font=font) - - def with_background(self, background: Optional[Tuple[int, int, int, int]]) -> 'StyleContext': + + def with_background( + self, background: Optional[Tuple[int, int, int, int]] + ) -> "StyleContext": """Create new context with modified background.""" return self._replace(background=background) - - def with_css_classes(self, css_classes: set) -> 'StyleContext': + + def with_css_classes(self, css_classes: set) -> "StyleContext": """Create new context with modified CSS classes.""" return self._replace(css_classes=css_classes) - - def with_css_styles(self, css_styles: Dict[str, str]) -> 'StyleContext': + + def with_css_styles(self, css_styles: Dict[str, str]) -> "StyleContext": """Create new context with modified CSS styles.""" return self._replace(css_styles=css_styles) - - def with_attributes(self, attributes: Dict[str, Any]) -> 'StyleContext': + + def with_attributes(self, attributes: Dict[str, Any]) -> "StyleContext": """Create new context with modified element attributes.""" return self._replace(element_attributes=attributes) - - def push_element(self, element_name: str) -> 'StyleContext': + + def push_element(self, element_name: str) -> "StyleContext": """Create new context with element pushed onto parent stack.""" return self._replace(parent_elements=self.parent_elements + [element_name]) @@ -58,10 +72,10 @@ class StyleContext(NamedTuple): def create_base_context(base_font: Optional[Font] = None) -> StyleContext: """ Create a base style context with default values. - + Args: base_font: Base font to use, defaults to system default - + Returns: StyleContext with default values """ @@ -71,99 +85,105 @@ def create_base_context(base_font: Optional[Font] = None) -> StyleContext: css_classes=set(), css_styles={}, element_attributes={}, - parent_elements=[] + parent_elements=[], ) def apply_element_styling(context: StyleContext, element: Tag) -> StyleContext: """ Apply element-specific styling to context based on HTML element and attributes. - + Args: context: Current style context element: BeautifulSoup Tag object - + Returns: New StyleContext with applied styling """ tag_name = element.name.lower() attributes = dict(element.attrs) if element.attrs else {} - + # Start with current context new_context = context.with_attributes(attributes).push_element(tag_name) - + # Apply CSS classes css_classes = new_context.css_classes.copy() - if 'class' in attributes: - classes = attributes['class'].split() if isinstance(attributes['class'], str) else attributes['class'] + if "class" in attributes: + classes = ( + attributes["class"].split() + if isinstance(attributes["class"], str) + else attributes["class"] + ) css_classes.update(classes) new_context = new_context.with_css_classes(css_classes) - + # Apply inline styles css_styles = new_context.css_styles.copy() - if 'style' in attributes: - inline_styles = parse_inline_styles(attributes['style']) + if "style" in attributes: + inline_styles = parse_inline_styles(attributes["style"]) css_styles.update(inline_styles) new_context = new_context.with_css_styles(css_styles) - + # Apply element-specific default styles font = apply_element_font_styles(new_context.font, tag_name, css_styles) new_context = new_context.with_font(font) - + # Apply background from styles background = apply_background_styles(new_context.background, css_styles) new_context = new_context.with_background(background) - + return new_context def parse_inline_styles(style_text: str) -> Dict[str, str]: """ Parse CSS inline styles into dictionary. - + Args: style_text: CSS style text (e.g., "color: red; font-weight: bold;") - + Returns: Dictionary of CSS property-value pairs """ styles = {} - for declaration in style_text.split(';'): - if ':' in declaration: - prop, value = declaration.split(':', 1) + for declaration in style_text.split(";"): + if ":" in declaration: + prop, value = declaration.split(":", 1) styles[prop.strip().lower()] = value.strip() return styles -def apply_element_font_styles(font: Font, tag_name: str, css_styles: Dict[str, str]) -> Font: +def apply_element_font_styles( + font: Font, tag_name: str, css_styles: Dict[str, str] +) -> Font: """ Apply font styling based on HTML element and CSS styles. - + Args: font: Current font tag_name: HTML tag name css_styles: CSS styles dictionary - + Returns: New Font object with applied styling """ # Default element styles element_font_styles = { - 'b': {'weight': FontWeight.BOLD}, - 'strong': {'weight': FontWeight.BOLD}, - 'i': {'style': FontStyle.ITALIC}, - 'em': {'style': FontStyle.ITALIC}, - 'u': {'decoration': TextDecoration.UNDERLINE}, - 's': {'decoration': TextDecoration.STRIKETHROUGH}, - 'del': {'decoration': TextDecoration.STRIKETHROUGH}, - 'h1': {'size': 24, 'weight': FontWeight.BOLD}, - 'h2': {'size': 20, 'weight': FontWeight.BOLD}, - 'h3': {'size': 18, 'weight': FontWeight.BOLD}, - 'h4': {'size': 16, 'weight': FontWeight.BOLD}, - 'h5': {'size': 14, 'weight': FontWeight.BOLD}, - 'h6': {'size': 12, 'weight': FontWeight.BOLD}, + "b": {"weight": FontWeight.BOLD}, + "strong": {"weight": FontWeight.BOLD}, + "i": {"style": FontStyle.ITALIC}, + "em": {"style": FontStyle.ITALIC}, + "u": {"decoration": TextDecoration.UNDERLINE}, + "s": {"decoration": TextDecoration.STRIKETHROUGH}, + "del": {"decoration": TextDecoration.STRIKETHROUGH}, + "h1": {"size": 24, "weight": FontWeight.BOLD}, + "h2": {"size": 20, "weight": FontWeight.BOLD}, + "h3": {"size": 18, "weight": FontWeight.BOLD}, + "h4": {"size": 16, "weight": FontWeight.BOLD}, + "h5": {"size": 14, "weight": FontWeight.BOLD}, + "h6": {"size": 12, "weight": FontWeight.BOLD}, } - + # Start with current font properties font_size = font.font_size colour = font.colour @@ -172,70 +192,70 @@ def apply_element_font_styles(font: Font, tag_name: str, css_styles: Dict[str, s decoration = font.decoration background = font.background language = font.language - + # Apply element default styles if tag_name in element_font_styles: elem_styles = element_font_styles[tag_name] - if 'size' in elem_styles: - font_size = elem_styles['size'] - if 'weight' in elem_styles: - weight = elem_styles['weight'] - if 'style' in elem_styles: - style = elem_styles['style'] - if 'decoration' in elem_styles: - decoration = elem_styles['decoration'] - + if "size" in elem_styles: + font_size = elem_styles["size"] + if "weight" in elem_styles: + weight = elem_styles["weight"] + if "style" in elem_styles: + style = elem_styles["style"] + if "decoration" in elem_styles: + decoration = elem_styles["decoration"] + # Apply CSS styles (override element defaults) - if 'font-size' in css_styles: + if "font-size" in css_styles: # Parse font-size (simplified - could be enhanced) - size_value = css_styles['font-size'].lower() - if size_value.endswith('px'): + size_value = css_styles["font-size"].lower() + if size_value.endswith("px"): try: font_size = int(float(size_value[:-2])) except ValueError: pass - elif size_value.endswith('pt'): + elif size_value.endswith("pt"): try: font_size = int(float(size_value[:-2])) except ValueError: pass - - if 'font-weight' in css_styles: - weight_value = css_styles['font-weight'].lower() - if weight_value in ['bold', '700', '800', '900']: + + if "font-weight" in css_styles: + weight_value = css_styles["font-weight"].lower() + if weight_value in ["bold", "700", "800", "900"]: weight = FontWeight.BOLD - elif weight_value in ['normal', '400']: + elif weight_value in ["normal", "400"]: weight = FontWeight.NORMAL - - if 'font-style' in css_styles: - style_value = css_styles['font-style'].lower() - if style_value == 'italic': + + if "font-style" in css_styles: + style_value = css_styles["font-style"].lower() + if style_value == "italic": style = FontStyle.ITALIC - elif style_value == 'normal': + elif style_value == "normal": style = FontStyle.NORMAL - - if 'text-decoration' in css_styles: - decoration_value = css_styles['text-decoration'].lower() - if 'underline' in decoration_value: + + if "text-decoration" in css_styles: + decoration_value = css_styles["text-decoration"].lower() + if "underline" in decoration_value: decoration = TextDecoration.UNDERLINE - elif 'line-through' in decoration_value: + elif "line-through" in decoration_value: decoration = TextDecoration.STRIKETHROUGH - elif 'none' in decoration_value: + elif "none" in decoration_value: decoration = TextDecoration.NONE - - if 'color' in css_styles: + + if "color" in css_styles: # Parse color (simplified - could be enhanced for hex, rgb, etc.) - color_value = css_styles['color'].lower() + color_value = css_styles["color"].lower() color_map = { - 'black': (0, 0, 0), - 'white': (255, 255, 255), - 'red': (255, 0, 0), - 'green': (0, 255, 0), - 'blue': (0, 0, 255), + "black": (0, 0, 0), + "white": (255, 255, 255), + "red": (255, 0, 0), + "green": (0, 255, 0), + "blue": (0, 0, 255), } if color_value in color_map: colour = color_map[color_value] - elif color_value.startswith('#') and len(color_value) == 7: + elif color_value.startswith("#") and len(color_value) == 7: try: r = int(color_value[1:3], 16) g = int(color_value[3:5], 16) @@ -243,7 +263,7 @@ def apply_element_font_styles(font: Font, tag_name: str, css_styles: Dict[str, s colour = (r, g, b) except ValueError: pass - + return Font( font_path=font._font_path, font_size=font_size, @@ -252,44 +272,45 @@ def apply_element_font_styles(font: Font, tag_name: str, css_styles: Dict[str, s style=style, decoration=decoration, background=background, - language=language + language=language, ) -def apply_background_styles(current_background: Optional[Tuple[int, int, int, int]], - css_styles: Dict[str, str]) -> Optional[Tuple[int, int, int, int]]: +def apply_background_styles( + current_background: Optional[Tuple[int, int, int, int]], css_styles: Dict[str, str] +) -> Optional[Tuple[int, int, int, int]]: """ Apply background styling from CSS. - + Args: current_background: Current background color (RGBA) css_styles: CSS styles dictionary - + Returns: New background color or None """ - if 'background-color' in css_styles: - bg_value = css_styles['background-color'].lower() - if bg_value == 'transparent': + if "background-color" in css_styles: + bg_value = css_styles["background-color"].lower() + if bg_value == "transparent": return None # Add color parsing logic here if needed - + return current_background def extract_text_content(element: Tag, context: StyleContext) -> List[Word]: """ Extract text content from an element, handling inline formatting. - + Args: element: BeautifulSoup Tag object context: Current style context - + Returns: List of Word objects """ words = [] - + for child in element.children: if isinstance(child, NavigableString): # Plain text - split into words @@ -301,7 +322,27 @@ def extract_text_content(element: Tag, context: StyleContext) -> List[Word]: words.append(Word(word_text, context.font, context.background)) elif isinstance(child, Tag): # Process inline elements - if child.name.lower() in ['span', 'a', 'strong', 'b', 'em', 'i', 'u', 's', 'del', 'ins', 'mark', 'small', 'sub', 'sup', 'code', 'q', 'cite', 'abbr', 'time']: + if child.name.lower() in [ + "span", + "a", + "strong", + "b", + "em", + "i", + "u", + "s", + "del", + "ins", + "mark", + "small", + "sub", + "sup", + "code", + "q", + "cite", + "abbr", + "time", + ]: child_context = apply_element_styling(context, child) child_words = extract_text_content(child, child_context) words.extend(child_words) @@ -317,18 +358,20 @@ def extract_text_content(element: Tag, context: StyleContext) -> List[Word]: elif isinstance(child_result, Paragraph): for _, word in child_result.words(): words.append(word) - + return words -def process_element(element: Tag, context: StyleContext) -> Union[Block, List[Block], None]: +def process_element( + element: Tag, context: StyleContext +) -> Union[Block, List[Block], None]: """ Process a single HTML element using appropriate handler. - + Args: element: BeautifulSoup Tag object context: Current style context - + Returns: Block object(s) or None if element should be ignored """ @@ -340,6 +383,7 @@ def process_element(element: Tag, context: StyleContext) -> Union[Block, List[Bl # Handler function signatures: # All handlers receive (element: Tag, context: StyleContext) -> Union[Block, List[Block], None] + def paragraph_handler(element: Tag, context: StyleContext) -> Paragraph: """Handle

elements.""" paragraph = Paragraph(context.font) @@ -367,14 +411,14 @@ def div_handler(element: Tag, context: StyleContext) -> List[Block]: def heading_handler(element: Tag, context: StyleContext) -> Heading: """Handle

-

elements.""" level_map = { - 'h1': HeadingLevel.H1, - 'h2': HeadingLevel.H2, - 'h3': HeadingLevel.H3, - 'h4': HeadingLevel.H4, - 'h5': HeadingLevel.H5, - 'h6': HeadingLevel.H6, + "h1": HeadingLevel.H1, + "h2": HeadingLevel.H2, + "h3": HeadingLevel.H3, + "h4": HeadingLevel.H4, + "h5": HeadingLevel.H5, + "h6": HeadingLevel.H6, } - + level = level_map.get(element.name.lower(), HeadingLevel.H1) heading = Heading(level, context.font) words = extract_text_content(element, context) @@ -401,23 +445,23 @@ def blockquote_handler(element: Tag, context: StyleContext) -> Quote: def preformatted_handler(element: Tag, context: StyleContext) -> CodeBlock: """Handle
 elements."""
-    language = context.element_attributes.get('data-language', '')
+    language = context.element_attributes.get("data-language", "")
     code_block = CodeBlock(language)
-    
+
     # Preserve whitespace and line breaks in preformatted text
-    text = element.get_text(separator='\n', strip=False)
-    for line in text.split('\n'):
+    text = element.get_text(separator="\n", strip=False)
+    for line in text.split("\n"):
         code_block.add_line(line)
-    
+
     return code_block
 
 
 def code_handler(element: Tag, context: StyleContext) -> Union[CodeBlock, None]:
     """Handle  elements."""
     # If parent is 
, this is handled by preformatted_handler
-    if context.parent_elements and context.parent_elements[-1] == 'pre':
+    if context.parent_elements and context.parent_elements[-1] == "pre":
         return None  # Will be handled by parent
-    
+
     # Inline code - handled during text extraction
     return None
 
@@ -426,7 +470,7 @@ def unordered_list_handler(element: Tag, context: StyleContext) -> HList:
     """Handle 
    elements.""" hlist = HList(ListStyle.UNORDERED, context.font) for child in element.children: - if isinstance(child, Tag) and child.name.lower() == 'li': + if isinstance(child, Tag) and child.name.lower() == "li": child_context = apply_element_styling(context, child) item = process_element(child, child_context) if item: @@ -438,7 +482,7 @@ def ordered_list_handler(element: Tag, context: StyleContext) -> HList: """Handle
      elements.""" hlist = HList(ListStyle.ORDERED, context.font) for child in element.children: - if isinstance(child, Tag) and child.name.lower() == 'li': + if isinstance(child, Tag) and child.name.lower() == "li": child_context = apply_element_styling(context, child) item = process_element(child, child_context) if item: @@ -449,7 +493,7 @@ def ordered_list_handler(element: Tag, context: StyleContext) -> HList: def list_item_handler(element: Tag, context: StyleContext) -> ListItem: """Handle
    1. elements.""" list_item = ListItem(None, context.font) - + for child in element.children: if isinstance(child, Tag): child_context = apply_element_styling(context, child) @@ -470,37 +514,37 @@ def list_item_handler(element: Tag, context: StyleContext) -> ListItem: if word_text: paragraph.add_word(Word(word_text, context.font)) list_item.add_block(paragraph) - + return list_item def table_handler(element: Tag, context: StyleContext) -> Table: """Handle elements.""" caption = None - caption_elem = element.find('caption') + caption_elem = element.find("caption") if caption_elem: caption = caption_elem.get_text(strip=True) - + table = Table(caption, context.font) - + # Process table rows for child in element.children: if isinstance(child, Tag): - if child.name.lower() == 'tr': + if child.name.lower() == "tr": child_context = apply_element_styling(context, child) row = process_element(child, child_context) if row: table.add_row(row) - elif child.name.lower() in ['thead', 'tbody', 'tfoot']: - section = 'header' if child.name.lower() == 'thead' else 'body' - section = 'footer' if child.name.lower() == 'tfoot' else section - - for row_elem in child.find_all('tr'): + elif child.name.lower() in ["thead", "tbody", "tfoot"]: + section = "header" if child.name.lower() == "thead" else "body" + section = "footer" if child.name.lower() == "tfoot" else section + + for row_elem in child.find_all("tr"): child_context = apply_element_styling(context, row_elem) row = process_element(row_elem, child_context) if row: table.add_row(row, section) - + return table @@ -508,7 +552,7 @@ def table_row_handler(element: Tag, context: StyleContext) -> TableRow: """Handle elements.""" row = TableRow(context.font) for child in element.children: - if isinstance(child, Tag) and child.name.lower() in ['td', 'th']: + if isinstance(child, Tag) and child.name.lower() in ["td", "th"]: child_context = apply_element_styling(context, child) cell = process_element(child, child_context) if cell: @@ -518,10 +562,10 @@ def table_row_handler(element: Tag, context: StyleContext) -> TableRow: def table_cell_handler(element: Tag, context: StyleContext) -> TableCell: """Handle
      elements.""" - colspan = int(context.element_attributes.get('colspan', 1)) - rowspan = int(context.element_attributes.get('rowspan', 1)) + colspan = int(context.element_attributes.get("colspan", 1)) + rowspan = int(context.element_attributes.get("rowspan", 1)) cell = TableCell(False, colspan, rowspan, context.font) - + # Process cell content for child in element.children: if isinstance(child, Tag): @@ -543,16 +587,16 @@ def table_cell_handler(element: Tag, context: StyleContext) -> TableCell: if word_text: paragraph.add_word(Word(word_text, context.font)) cell.add_block(paragraph) - + return cell def table_header_cell_handler(element: Tag, context: StyleContext) -> TableCell: """Handle elements.""" - colspan = int(context.element_attributes.get('colspan', 1)) - rowspan = int(context.element_attributes.get('rowspan', 1)) + colspan = int(context.element_attributes.get("colspan", 1)) + rowspan = int(context.element_attributes.get("rowspan", 1)) cell = TableCell(True, colspan, rowspan, context.font) - + # Process cell content (same as td) for child in element.children: if isinstance(child, Tag): @@ -573,7 +617,7 @@ def table_header_cell_handler(element: Tag, context: StyleContext) -> TableCell: if word_text: paragraph.add_word(Word(word_text, context.font)) cell.add_block(paragraph) - + return cell @@ -590,19 +634,19 @@ def line_break_handler(element: Tag, context: StyleContext) -> None: def image_handler(element: Tag, context: StyleContext) -> Image: """Handle elements.""" - src = context.element_attributes.get('src', '') - alt_text = context.element_attributes.get('alt', '') - + src = context.element_attributes.get("src", "") + alt_text = context.element_attributes.get("alt", "") + # Parse dimensions if provided width = height = None try: - if 'width' in context.element_attributes: - width = int(context.element_attributes['width']) - if 'height' in context.element_attributes: - height = int(context.element_attributes['height']) + if "width" in context.element_attributes: + width = int(context.element_attributes["width"]) + if "height" in context.element_attributes: + height = int(context.element_attributes["height"]) except ValueError: pass - + return Image(source=src, alt_text=alt_text, width=width, height=height) @@ -619,89 +663,87 @@ def generic_handler(element: Tag, context: StyleContext) -> List[Block]: # Handler registry - maps HTML tag names to handler functions HANDLERS: Dict[str, Callable[[Tag, StyleContext], Union[Block, List[Block], None]]] = { # Block elements - 'p': paragraph_handler, - 'div': div_handler, - 'h1': heading_handler, - 'h2': heading_handler, - 'h3': heading_handler, - 'h4': heading_handler, - 'h5': heading_handler, - 'h6': heading_handler, - 'blockquote': blockquote_handler, - 'pre': preformatted_handler, - 'code': code_handler, - 'ul': unordered_list_handler, - 'ol': ordered_list_handler, - 'li': list_item_handler, - 'table': table_handler, - 'tr': table_row_handler, - 'td': table_cell_handler, - 'th': table_header_cell_handler, - 'hr': horizontal_rule_handler, - 'br': line_break_handler, - + "p": paragraph_handler, + "div": div_handler, + "h1": heading_handler, + "h2": heading_handler, + "h3": heading_handler, + "h4": heading_handler, + "h5": heading_handler, + "h6": heading_handler, + "blockquote": blockquote_handler, + "pre": preformatted_handler, + "code": code_handler, + "ul": unordered_list_handler, + "ol": ordered_list_handler, + "li": list_item_handler, + "table": table_handler, + "tr": table_row_handler, + "td": table_cell_handler, + "th": table_header_cell_handler, + "hr": horizontal_rule_handler, + "br": line_break_handler, # Semantic elements (treated as containers) - 'section': div_handler, - 'article': div_handler, - 'aside': div_handler, - 'nav': div_handler, - 'header': div_handler, - 'footer': div_handler, - 'main': div_handler, - 'figure': div_handler, - 'figcaption': paragraph_handler, - + "section": div_handler, + "article": div_handler, + "aside": div_handler, + "nav": div_handler, + "header": div_handler, + "footer": div_handler, + "main": div_handler, + "figure": div_handler, + "figcaption": paragraph_handler, # Media elements - 'img': image_handler, - + "img": image_handler, # Inline elements (handled during text extraction) - 'span': ignore_handler, - 'a': ignore_handler, - 'strong': ignore_handler, - 'b': ignore_handler, - 'em': ignore_handler, - 'i': ignore_handler, - 'u': ignore_handler, - 's': ignore_handler, - 'del': ignore_handler, - 'ins': ignore_handler, - 'mark': ignore_handler, - 'small': ignore_handler, - 'sub': ignore_handler, - 'sup': ignore_handler, - 'q': ignore_handler, - 'cite': ignore_handler, - 'abbr': ignore_handler, - 'time': ignore_handler, - + "span": ignore_handler, + "a": ignore_handler, + "strong": ignore_handler, + "b": ignore_handler, + "em": ignore_handler, + "i": ignore_handler, + "u": ignore_handler, + "s": ignore_handler, + "del": ignore_handler, + "ins": ignore_handler, + "mark": ignore_handler, + "small": ignore_handler, + "sub": ignore_handler, + "sup": ignore_handler, + "q": ignore_handler, + "cite": ignore_handler, + "abbr": ignore_handler, + "time": ignore_handler, # Ignored elements - 'script': ignore_handler, - 'style': ignore_handler, - 'meta': ignore_handler, - 'link': ignore_handler, - 'head': ignore_handler, - 'title': ignore_handler, + "script": ignore_handler, + "style": ignore_handler, + "meta": ignore_handler, + "link": ignore_handler, + "head": ignore_handler, + "title": ignore_handler, } -def parse_html_string(html_string: str, base_font: Optional[Font] = None) -> List[Block]: +def parse_html_string( + html_string: str, base_font: Optional[Font] = None +) -> List[Block]: """ Parse HTML string and return list of Block objects. - + Args: html_string: HTML content to parse base_font: Base font for styling, defaults to system default - + Returns: List of Block objects representing the document structure """ - soup = BeautifulSoup(html_string, 'html.parser') + soup = BeautifulSoup(html_string, "html.parser") context = create_base_context(base_font) blocks = [] - + # Process the body if it exists, otherwise process all top-level elements - root_element = soup.find('body') or soup - + root_element = soup.find("body") or soup + for element in root_element.children: if isinstance(element, Tag): element_context = apply_element_styling(context, element) @@ -711,29 +753,5 @@ def parse_html_string(html_string: str, base_font: Optional[Font] = None) -> Lis blocks.extend(result) else: blocks.append(result) - + return blocks - - -def register_handler(tag_name: str, handler: Callable[[Tag, StyleContext], Union[Block, List[Block], None]]): - """ - Register a custom handler for an HTML tag. - - Args: - tag_name: HTML tag name (lowercase) - handler: Handler function with signature (element: Tag, context: StyleContext) -> Union[Block, List[Block], None] - """ - HANDLERS[tag_name] = handler - - -def get_handler(tag_name: str) -> Callable[[Tag, StyleContext], Union[Block, List[Block], None]]: - """ - Get handler function for HTML tag. - - Args: - tag_name: HTML tag name (lowercase) - - Returns: - Handler function or generic_handler if tag not found - """ - return HANDLERS.get(tag_name.lower(), generic_handler) diff --git a/pyproject.toml b/pyproject.toml index 5d070b3..561126a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,3 +18,34 @@ dependencies = [ "pyphen", "beautifulsoup4", ] + +[tool.coverage.run] +source = ["pyWebLayout"] +branch = true +omit = [ + "*/tests/*", + "*/test_*", + "setup.py", + "*/examples/*", + "*/__main__.py" +] + +[tool.coverage.report] +exclude_lines = [ + "pragma: no cover", + "def __repr__", + "if self.debug:", + "if settings.DEBUG", + "raise AssertionError", + "raise NotImplementedError", + "if 0:", + "if __name__ == .__main__.:" +] +precision = 2 +show_missing = true + +[tool.coverage.xml] +output = "coverage.xml" + +[tool.coverage.html] +directory = "htmlcov" diff --git a/run_coverage_gutters.py b/run_coverage_gutters.py new file mode 100644 index 0000000..e8b5990 --- /dev/null +++ b/run_coverage_gutters.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 +""" +Simple coverage runner for Coverage Gutters extension. +Generates coverage.xml file needed by the VSCode Coverage Gutters extension. +""" + +import subprocess +import sys +import os + + +def main(): + """Run coverage for Coverage Gutters.""" + print("Generating coverage for Coverage Gutters...") + + try: + # Run tests with coverage and generate XML report + cmd = [ + sys.executable, "-m", "pytest", + "tests/", + "--cov=pyWebLayout", + "--cov-report=xml", + "--cov-report=term" + ] + + result = subprocess.run(cmd, check=True) + + # Check if coverage.xml was created + if os.path.exists("coverage.xml"): + print("✓ coverage.xml generated successfully!") + print("Coverage Gutters should now be able to display coverage data.") + print("\nTo use Coverage Gutters in VSCode:") + print("1. Open Command Palette (Ctrl+Shift+P)") + print("2. Run 'Coverage Gutters: Display Coverage'") + print("3. Or use the Coverage Gutters buttons in the status bar") + else: + print("✗ coverage.xml was not generated") + + except subprocess.CalledProcessError as e: + print(f"Error running tests: {e}") + sys.exit(1) + except FileNotFoundError: + print("pytest not found. Please install it with: pip install pytest pytest-cov") + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/scripts/run_coverage.py b/scripts/run_coverage.py index 52cfb94..64fd701 100644 --- a/scripts/run_coverage.py +++ b/scripts/run_coverage.py @@ -53,7 +53,7 @@ def main(): # Run tests with coverage print("\n2. Running tests with coverage...") - test_cmd = "python -m pytest tests/ -v --cov=pyWebLayout --cov-report=term-missing --cov-report=json --cov-report=html" + test_cmd = "python -m pytest tests/ -v --cov=pyWebLayout --cov-report=term-missing --cov-report=json --cov-report=html --cov-report=xml" run_command(test_cmd, "Running tests with coverage") # Generate test coverage badge @@ -89,7 +89,7 @@ else: # List generated files print("\n6. Generated files:") - files = ["coverage.svg", "coverage-docs.svg", "coverage-summary.txt", "htmlcov/", "coverage.json"] + files = ["coverage.svg", "coverage-docs.svg", "coverage-summary.txt", "htmlcov/", "coverage.json", "coverage.xml"] for file in files: if os.path.exists(file): print(f" ✓ {file}")