Source code for soupsavvy.testing.generators.generators

"""
Module with generators for producing HTML content.
Generators can be used to create customizable HTML content,
that can be used to test selectors and workflows.

Classes
-------
- `AttributeGenerator` - class for generating HTML attribute strings.
- `TagGenerator` - class for generating HTML tag strings.
"""

from __future__ import annotations

from collections.abc import Iterable
from typing import Optional, Union

import soupsavvy.exceptions as exc
from soupsavvy.testing.generators import namespace
from soupsavvy.testing.generators.base import BaseGenerator
from soupsavvy.testing.generators.templates.base import BaseTemplate
from soupsavvy.testing.generators.templates.config import (
    DEFAULT_TEXT_TEMPLATE,
    DEFAULT_VALUE_TEMPLATE,
)
from soupsavvy.testing.generators.templates.templates import ConstantTemplate

# types for generators
TemplateType = Optional[Union[BaseTemplate, str]]

AttributeType = Union["AttributeGenerator", str, tuple[str, TemplateType]]
ChildType = Union["TagGenerator", str]

AttributeList = Iterable[AttributeType]
ChildList = Iterable[ChildType]

AttributeGeneratorInitExceptions = (
    TypeError,
    exc.InvalidTemplateException,
    exc.EmptyNameException,
)


def _get_template_type(
    value: TemplateType, param: str, default: BaseTemplate
) -> BaseTemplate:
    """
    Get the template type from the input value.

    Parameters
    ----------
    value : TemplateType
        The value to get the template type from.
    param : str
        The name of the parameter.
    default : BaseTemplate
        The default template to use if the value is None.

    Returns
    -------
    BaseTemplate
        The template instance.

    Raises
    ------
    InvalidTemplateException
        If the value is not a string, BaseTemplate or None.
    """
    if value is None:
        return default
    elif isinstance(value, str):
        return ConstantTemplate(value)
    elif not isinstance(value, BaseTemplate):
        raise exc.InvalidTemplateException(
            f"{param} must be a string or a BaseTemplate, not {type(value)}"
        )

    return value


[docs] class AttributeGenerator(BaseGenerator): """ Class for generating HTML attribute strings. AttributeGenerator allows to generate empty HTML attribute with specific name: Example -------- >>> gen = AttributeGenerator("class") ... gen.generate() 'class=""' Attribute can also be generated with specified constant value: Example -------- >>> gen = AttributeGenerator("class", value="container") ... gen.generate() 'class="container"' Value can be passed as BaseTemplate instance as well: Example -------- >>> from soupsavvy.testing.generators import RandomTemplate ... template = RandomTemplate(length=4, seed=42) ... gen = AttributeGenerator("id", value=template) ... gen.generate() 'id="Nbrn"' For more information on available Templates, how to use them and customize for your needs, see the documentation. See also -------- - `soupsavvy.testing.generators.templates` package - `soupsavvy.testing.TagGenerator` class """
[docs] def __init__(self, name: str, value: TemplateType = None) -> None: """ Initializes the AttributeGenerator. Parameters ---------- name : str The name of the attribute. value : TemplateType, optional The value of the attribute. Defaults to None. """ self._check_name(name) self.name = name self.value = _get_template_type( value, param="name", default=DEFAULT_VALUE_TEMPLATE, )
def _check_name(self, name: str) -> None: """ Check if the input name is a valid HTML attribute name. Parameters ---------- name : str The name to check. Raises ------ TypeError If the name is not a string. EmptyNameException If the name is an empty string. """ if not isinstance(name, str): raise TypeError(f"'name' parameter must be a string, not {type(name)}") if not name: raise exc.EmptyNameException( "Empty string is not allowed html attribute name, " "not-empty string must be provided for 'name' parameter." )
[docs] def generate(self) -> str: """ Generates the HTML attribute string. Returns ------- str The generated HTML attribute string. """ value = self.value.generate() return f'{self.name}="{value}"'
[docs] class TagGenerator(BaseGenerator): """ Class for generating HTML tag strings. TagGenerator allows to generate empty HTML tag with specific name: Example -------- >>> gen = TagGenerator("div") ... gen.generate() '<div></div>' Tag can also be generated with specified attributes: Example -------- >>> gen = TagGenerator("div", attrs=[("class", "container")]) ... gen.generate() '<div class="container"></div>' Attributes can be passed as an iterable of of mixed types: - AttributeGenerator - instance of AttributeGenerator class - str - attribute name, value would be a default Template (empty string) - tuple[str, TemplateType] - attribute name and literal value or Template Example -------- >>> gen = TagGenerator( ... name="a", ... attrs=[ ... ("id", "link"), ... AttributeGenerator("href", "/endpoint"), ... "class", ... ], ... ) ... gen.generate() '<div id="link" href="/endpoint", class=""></div>' Similarly, children can be passed as an iterable of mixed types: - TagGenerator - instance of TagGenerator class - str - tag name, children tags would be empty Example -------- >>> gen = TagGenerator( ... name="div", ... children=[ ... "a", ... TagGenerator("span", attrs=[("class", "container")], ... ], ... ) ... gen.generate() '<div><a></a><span class="container"></span></div>' Text of the tag can be passed as a string or a Template: Example -------- >>> gen = TagGenerator("p", text="Hello, World!") ... gen.generate() '<p>Hello, World!</p>' Example -------- >>> from soupsavvy.testing.generators import RandomTemplate ... template = RandomTemplate(length=4, seed=42) ... gen = TagGenerator("p", text=template) ... gen.generate() '<p>Nbrn</p>' For more information on available Templates, how to use them and customize for your needs, see the documentation. Void tags like `<img>`, `<br>`, `<hr>` etc. can be generated as well and are automatically closed: Example -------- >>> gen = TagGenerator("img", attrs=[("src", "/path/to/image.jpg")]) ... gen.generate() '<img src="/path/to/image.jpg"/>' No children are allowed for void tags, and an error will be raised. See also -------- - `soupsavvy.testing.generators.templates` package - `soupsavvy.testing.AttributeGenerator` class """
[docs] def __init__( self, name: str, attrs: AttributeList = (), children: ChildList = (), text: TemplateType = None, ) -> None: """ Initialize the `TagGenerator`. Parameters ---------- name : str The name of the HTML tag. attrs : AttributeList, optional The attributes of the tag. Defaults to empty tuple. children : ChildList, optional The children of the tag. Defaults to empty tuple. text : TemplateType, optional The text content of the tag. Defaults to None, which generates empty string. """ if isinstance(attrs, str): raise TypeError("'attrs' must be an iterable of attributes, not a string") self._void = name in namespace.VOID_TAGS if self._void and children: raise exc.VoidTagWithChildrenException( f"'{name}' is a void tag and cannot have children" ) self._check_name(name) self.name = name self.text = _get_template_type( text, param="text", default=DEFAULT_TEXT_TEMPLATE, ) self.attributes = self._process_attributes(attrs) self.children = [ child if isinstance(child, TagGenerator) else TagGenerator(child) for child in children ]
def _check_name(self, name: str) -> None: """ Check if the input name is a valid HTML tag name. Parameters ---------- name : str The name to check. Raises ------ TypeError If the name is not a string. EmptyNameException If the name is an empty string. """ if not isinstance(name, str): raise TypeError(f"'name' parameter must be a string, not {type(name)}") if not name: raise exc.EmptyNameException( "Empty string is not allowed html tag name, " "not-empty string must be provided for 'name' parameter." ) def _process_attributes( self, attributes: AttributeList ) -> list[AttributeGenerator]: """ Process the input attributes of the tag. Parameters ---------- attributes : AttributeList The attributes to process. Returns ------- list[AttributeGenerator] The processed instances of AttributeGenerators. Raises ------ AttributeParsingError If the input attributes could not be parsed into AttributeGenerators. """ # dict approach for uniqueness check attr_dict: dict[str, AttributeGenerator] = {} for attr in attributes: if isinstance(attr, str): attr = (attr, None) if not isinstance(attr, AttributeGenerator): try: attr = AttributeGenerator(*attr) except AttributeGeneratorInitExceptions as e: raise exc.AttributeParsingError( f"Attribute {attr} could not be parsed into AttributeGenerator " "due to following error:" ) from e attr_name = attr.name if attr_name in attr_dict: raise exc.NotUniqueAttributesException( f"Input attribute {attr_name} is not unique." ) attr_dict[attr_name] = attr return list(attr_dict.values())
[docs] def generate(self) -> str: """ Generates the HTML tag string. Returns ------- str The generated HTML tag string. """ attrs = " ".join(attr.generate() for attr in self.attributes) sep = " " if attrs else "" children = "".join(child.generate() for child in self.children) tag_content = f"{self.name}{sep}{attrs}" if self._void: return f"<{tag_content}/>" text = self.text.generate() return f"<{tag_content}>{children}{text}</{self.name}>"