Source code for pynguin.assertion.mutation_analysis.mutators

#  This file is part of Pynguin.
#
#  SPDX-FileCopyrightText: 2019–2026 Pynguin Contributors
#
#  SPDX-License-Identifier: MIT
#
"""Provides classes for mutating ASTs.

Based on https://github.com/se2p/mutpy-pynguin/blob/main/mutpy/controller.py
and integrated in Pynguin.
"""

from __future__ import annotations

import abc
from typing import TYPE_CHECKING

from pynguin.assertion.mutation_analysis.strategies import FirstToLastHOMStrategy, HOMStrategy

if TYPE_CHECKING:
    import ast
    import types
    from collections.abc import Generator

    from pynguin.assertion.mutation_analysis.operators.base import Mutation, MutationOperator


[docs] class Mutator(abc.ABC): """A mutator is responsible for mutating an AST."""
[docs] @abc.abstractmethod def mutate( self, target_ast: ast.AST, module: types.ModuleType, ) -> Generator[tuple[list[Mutation], ast.AST]]: """Mutate the given AST. Args: target_ast: The AST to mutate. module: The module to mutate. Yields: A generator of mutations and the mutated AST. """
[docs] class FirstOrderMutator(Mutator): """A mutator that applies first order mutations.""" def __init__(self, operators: list[type[MutationOperator]]) -> None: """Initialize the mutator. Args: operators: The operators to use for mutation. """ self.operators = operators
[docs] def mutate( # noqa: D102 self, target_ast: ast.AST, module: types.ModuleType, ) -> Generator[tuple[list[Mutation], ast.AST]]: for op in self.operators: for mutation, mutant in op.mutate(target_ast, module): yield [mutation], mutant
[docs] class HighOrderMutator(FirstOrderMutator): """A mutator that applies high order mutations.""" def __init__( self, operators: list[type[MutationOperator]], hom_strategy: HOMStrategy | None = None, ) -> None: """Initialize the mutator. Args: operators: The operators to use for mutation. hom_strategy: The strategy to use for higher order mutations. """ super().__init__(operators) self.hom_strategy = hom_strategy or FirstToLastHOMStrategy()
[docs] def mutate( # noqa: D102 self, target_ast: ast.AST, module: types.ModuleType, ) -> Generator[tuple[list[Mutation], ast.AST]]: mutations = self._generate_all_mutations(module, target_ast) for mutations_to_apply in self.hom_strategy.generate(mutations): generators = [] applied_mutations = [] mutant = target_ast for mutation in mutations_to_apply: generator = mutation.operator.mutate(mutant, module, mutation) next_value = next(generator, None) assert next_value is not None new_mutation, mutant = next_value applied_mutations.append(new_mutation) generators.append(generator) yield applied_mutations, mutant self._finish_generators(generators)
def _generate_all_mutations( self, module: types.ModuleType, target_ast: ast.AST, ) -> list[Mutation]: mutations: list[Mutation] = [] for op in self.operators: for mutation, _ in op.mutate(target_ast, module): mutations.append(mutation) return mutations @staticmethod def _finish_generators(generators: list[Generator]) -> None: for generator in reversed(generators): value = next(generator, None) assert value is None, "too many mutations!"