Source code for hbutils.testing.generator.base

"""
Test case generation utilities for combinatorial testing workflows.

This module provides foundational utilities and a base class for building
test case generators that emit combinations of values. It focuses on
normalizing user-provided values into tuples and providing a structured
interface for derived generators that implement concrete combination
strategies.

The module contains the following main public components:

* :class:`BaseGenerator` - Base class for implementing test case generators

Example::

    >>> class SimpleGenerator(BaseGenerator):
    ...     def cases(self):
    ...         for a in self.values['a']:
    ...             for b in self.values['b']:
    ...                 yield {'a': a, 'b': b}
    ...
    >>> gen = SimpleGenerator({'a': [1, 2], 'b': ['x', 'y']})
    >>> list(gen.tuple_cases())
    [(1, 'x'), (1, 'y'), (2, 'x'), (2, 'y')]

.. note::
   This module is intended to be extended. The :meth:`BaseGenerator.cases`
   method must be implemented in subclasses.

"""

from types import GeneratorType
from typing import Tuple, Mapping, Optional, List, Iterator, Set


def _single_to_tuple(s: object) -> Tuple[object, ...]:
    """
    Convert a single value or iterable to a tuple.

    :param s: The value to convert. Can be a single value, list, tuple, generator, or range.
    :type s: object
    :return: A tuple containing the value(s). If input is already iterable (list, tuple,
        generator, or range), returns it as a tuple. Otherwise, wraps the single value in a tuple.
    :rtype: Tuple[object, ...]

    Example::

        >>> _single_to_tuple(5)
        (5,)
        >>> _single_to_tuple([1, 2, 3])
        (1, 2, 3)
        >>> _single_to_tuple((4, 5))
        (4, 5)
    """
    if isinstance(s, (list, tuple, GeneratorType, range)):
        return tuple(s)
    else:
        return (s,)


def _single_dict_process(s: Mapping[str, object]) -> Mapping[str, Tuple[object, ...]]:
    """
    Process a dictionary by converting all values to tuples.

    :param s: A dictionary with string keys and arbitrary values.
    :type s: Mapping[str, object]
    :return: A new dictionary with the same keys, but all values converted to tuples.
    :rtype: Mapping[str, Tuple[object, ...]]

    Example::

        >>> _single_dict_process({'a': 1, 'b': [2, 3]})
        {'a': (1,), 'b': (2, 3)}
    """
    return {key: _single_to_tuple(value) for key, value in s.items()}


def _check_keys(item: Mapping[str, object], names: Set[str]) -> None:
    """
    Validate that all keys in the item exist in the allowed names set.

    :param item: A dictionary to validate.
    :type item: Mapping[str, object]
    :param names: A set of valid key names.
    :type names: Set[str]
    :raises KeyError: If any key in item is not present in the names set.

    Example::

        >>> _check_keys({'a': 1, 'b': 2}, {'a', 'b', 'c'})  # No error
        >>> _check_keys({'a': 1, 'd': 2}, {'a', 'b', 'c'})  # Raises KeyError
        Traceback (most recent call last):
            ...
        KeyError: "Invalid key - 'd'."
    """
    for key in item.keys():
        if key not in names:
            raise KeyError(f'Invalid key - {repr(key)}.')


[docs] class BaseGenerator: """ Base generator class for creating test case combinations. This class provides the foundation for generating test cases with different combinations of input values. It stores a mapping of parameter names to their possible values and provides methods to iterate over test cases in different formats. Subclasses should implement the :meth:`cases` method to define the specific strategy for generating test case combinations. :param values: A mapping of parameter names to their possible values. Each value can be a single item or an iterable (list, tuple, generator, range). Single values will be automatically converted to tuples. For example: ``{'a': [2, 3], 'b': ['x', 'y']}``. :type values: Mapping[str, object] :param names: Optional list of parameter names to define the order of parameters. If not provided, uses the sorted keys from values. Default is ``None``. :type names: Optional[List[str]] :ivar values: Mapping of parameter names to tuples of possible values. :vartype values: Mapping[str, Tuple[object, ...]] :ivar names: Ordered list of parameter names to be used for output tuples. :vartype names: List[str] Example:: >>> gen = BaseGenerator({'a': [1, 2], 'b': ['x', 'y']}) >>> gen.names ['a', 'b'] >>> gen.values {'a': (1, 2), 'b': ('x', 'y')} """
[docs] def __init__(self, values: Mapping[str, object], names: Optional[List[str]] = None) -> None: """ Initialize the BaseGenerator with values and optional names. :param values: A mapping of parameter names to their possible values. Each value can be a single item or an iterable (list, tuple, generator, range). Single values will be automatically converted to tuples. For example: ``{'a': [2, 3], 'b': ['x', 'y']}``. :type values: Mapping[str, object] :param names: Optional list of parameter names to define the order of parameters. If not provided, uses the sorted keys from values. Default is ``None``. :type names: Optional[List[str]] Example:: >>> gen = BaseGenerator({'a': [1, 2], 'b': ['x', 'y']}) >>> gen.names ['a', 'b'] >>> gen.values {'a': (1, 2), 'b': ('x', 'y')} """ self.__values = _single_dict_process(values) self.__names = list(names or sorted(self.__values.keys()))
@property def values(self) -> Mapping[str, Tuple[object, ...]]: """ Get the selection values for test case generation. :return: A mapping of parameter names to tuples of possible values. :rtype: Mapping[str, Tuple[object, ...]] Example:: >>> gen = BaseGenerator({'a': [1, 2], 'b': 'x'}) >>> gen.values {'a': (1, 2), 'b': ('x',)} """ return self.__values @property def names(self) -> List[str]: """ Get the ordered list of parameter names. :return: A list of parameter names in the order they should be used. :rtype: List[str] Example:: >>> gen = BaseGenerator({'b': [1, 2], 'a': [3, 4]}) >>> gen.names ['a', 'b'] """ return self.__names
[docs] def cases(self) -> Iterator[Mapping[str, object]]: """ Generate test cases as dictionaries. This is a virtual method that must be implemented in subclasses to define the specific strategy for generating test case combinations. :return: An iterator yielding dictionaries where keys are parameter names and values are the selected values for that test case. :rtype: Iterator[Mapping[str, object]] :raises NotImplementedError: This method must be implemented by subclasses. Example:: >>> # In a subclass implementation: >>> for case in generator.cases(): ... print(case) {'a': 1, 'b': 'x'} {'a': 2, 'b': 'y'} """ raise NotImplementedError # pragma: no cover
[docs] def tuple_cases(self) -> Iterator[Tuple[object, ...]]: """ Generate test cases as tuples. This method converts the dictionary-based cases from :meth:`cases` into tuples, with values ordered according to :attr:`names`. This format is convenient for use with testing frameworks that expect tuple arguments. :return: An iterator yielding tuples of test case values in the order defined by names. :rtype: Iterator[Tuple[object, ...]] Example:: >>> # Assuming cases() yields {'a': 1, 'b': 'x'} and {'a': 2, 'b': 'y'} >>> # and names is ['a', 'b'] >>> for case in generator.tuple_cases(): ... print(case) (1, 'x') (2, 'y') """ for d in self.cases(): yield tuple(d[name] for name in self.names)