Skip to content

Instrumentation Module

The instrumentation module implements mutation switching, the key speed optimization in pytest-gremlins. Instead of modifying source files for each mutation, code is instrumented once with all mutations embedded, and toggled via an environment variable.

Overview

Traditional mutation testing rewrites files for each mutant:

Text Only
For each mutation:
    1. Modify source file
    2. Run tests
    3. Restore original file

# With 100 mutations = 100 file rewrites

Mutation switching instruments once:

Text Only
1. Transform source with ALL mutations embedded
2. For each mutation:
    - Set ACTIVE_GREMLIN=gXXX
    - Run tests (mutation auto-activates)

# With 100 mutations = 1 transformation, 100 test runs

Module Exports

Python
from pytest_gremlins.instrumentation import (
    Gremlin,                  # Mutation dataclass
    transform_source,        # Main transformation function
    ACTIVE_GREMLIN_ENV_VAR,  # Environment variable name
    get_active_gremlin,      # Get current active gremlin ID
)

Gremlin

A Gremlin represents a single mutation injected into source code.

Gremlin dataclass

Python
Gremlin(gremlin_id, file_path, line_number, original_node, mutated_node, operator_name, description, pardoned=False, pardon_reason=None)

A mutation (gremlin) injected into source code.

Attributes:

Name Type Description
gremlin_id str

Unique identifier for this gremlin (e.g., 'g001').

file_path str

Path to the source file containing this mutation.

line_number int

Line number where the mutation occurs.

original_node expr | stmt

The original AST node before mutation.

mutated_node expr | stmt

The mutated AST node.

operator_name str

Name of the operator that created this mutation.

description str

Human-readable description of the mutation.

pardoned bool

True when an inline suppression pragma covers this mutation.

pardon_reason str | None

The reason code and justification from the pragma, e.g. 'equivalent: floor division is integer arithmetic'. None when not pardoned.

Gremlin Attributes

Attribute Type Description
gremlin_id str Unique identifier (e.g., 'g001', 'g002')
file_path str Path to source file containing the mutation
line_number int Line number where mutation occurs
original_node ast.expr \| ast.stmt Original AST node before mutation
mutated_node ast.expr \| ast.stmt Mutated AST node
operator_name str Name of operator that created this mutation
description str Human-readable description (e.g., '>= to >')
pardoned bool True when an inline pardon pragma covers this mutation (default False)

Example

Python
from pytest_gremlins.instrumentation import transform_source

source = '''
def is_adult(age):
    return age >= 18
'''

gremlins, tree = transform_source(source, 'example.py')

for g in gremlins:
    print(f'{g.gremlin_id}: {g.description} at line {g.line_number}')

# Output:
# g001: >= to > at line 3
# g002: >= to < at line 3
# g003: boundary shift +/-1 at line 3
# g004: boundary shift +/-1 at line 3
# g005: return value to None at line 3

Transformer

transform_source

The main entry point for instrumenting Python source code.

transform_source

Python
transform_source(source, file_path, operators=None)

Transform source code by embedding mutation switching.

This is the main entry point for instrumenting Python source code. It parses the source, identifies mutation points, and replaces them with switching expressions that can toggle between original and mutated behavior based on the ACTIVE_GREMLIN environment variable.

Parameters:

Name Type Description Default
source str

The Python source code to transform.

required
file_path str

The path to the source file (for gremlin metadata).

required
operators list[GremlinOperator] | None

Optional list of operators to use. If None, uses all 5 default operators.

None

Returns:

Type Description
tuple[list[Gremlin], Module]

Tuple of (list of gremlins, transformed AST with embedded switches).

Source code in src/pytest_gremlins/instrumentation/transformer.py
Python
def transform_source(
    source: str,
    file_path: str,
    operators: list[GremlinOperator] | None = None,
) -> tuple[list[Gremlin], ast.Module]:
    """Transform source code by embedding mutation switching.

    This is the main entry point for instrumenting Python source code.
    It parses the source, identifies mutation points, and replaces them
    with switching expressions that can toggle between original and
    mutated behavior based on the ACTIVE_GREMLIN environment variable.

    Args:
        source: The Python source code to transform.
        file_path: The path to the source file (for gremlin metadata).
        operators: Optional list of operators to use. If None, uses all 5 default operators.

    Returns:
        Tuple of (list of gremlins, transformed AST with embedded switches).
    """
    tree = ast.parse(source)
    transformer = MutationSwitchingTransformer(file_path, operators=operators)
    new_tree = transformer.visit(tree)
    if not isinstance(new_tree, ast.Module):  # pragma: no cover
        raise TypeError(f'Expected ast.Module, got {type(new_tree).__name__}')

    pardoned_lines = parse_pardoned_lines(source, file_path=file_path)
    gremlins_by_line: dict[int, list[int]] = {}
    for i, g in enumerate(transformer.gremlins):
        gremlins_by_line.setdefault(g.line_number, []).append(i)

    _logger = logging.getLogger(__name__)
    for line_number in pardoned_lines:
        if line_number not in gremlins_by_line:
            _logger.warning(
                'Dead gremlin pragma at line %d of %s: no gremlins on that line',
                line_number,
                file_path,
            )

    resolved_gremlins: list[Gremlin] = []
    for gremlin in transformer.gremlins:
        if gremlin.line_number in pardoned_lines:
            reason_code, justification = pardoned_lines[gremlin.line_number]
            resolved_gremlins.append(
                dataclasses.replace(
                    gremlin,
                    pardoned=True,
                    pardon_reason=f'{reason_code}: {justification}',
                )
            )
        else:
            resolved_gremlins.append(gremlin)

    return resolved_gremlins, new_tree

Parameters

Parameter Type Default Description
source str Required Python source code to transform
file_path str Required Path for gremlin metadata
operators list[GremlinOperator] \| None None Operators to use (None = all 5)

Returns

Element Type Description
[0] list[Gremlin] List of generated gremlins
[1] ast.Module Transformed AST with embedded switches

Example

Python
from pytest_gremlins.instrumentation import transform_source
from pytest_gremlins.operators import ComparisonOperator

source = 'result = x > 0'

# Use all default operators
gremlins, tree = transform_source(source, 'test.py')

# Use specific operators only
gremlins, tree = transform_source(
    source,
    'test.py',
    operators=[ComparisonOperator()]
)

MutationSwitchingTransformer

Internal AST transformer that replaces mutation points with switching expressions.

MutationSwitchingTransformer

Python
MutationSwitchingTransformer(file_path, operators=None)

Bases: NodeTransformer

AST transformer that replaces mutation points with switching expressions.

This transformer walks the AST and replaces each mutation point (e.g., comparison expression) with a switching expression that selects the appropriate mutation based on the gremlin_active variable.

Source code in src/pytest_gremlins/instrumentation/transformer.py
Python
def __init__(
    self,
    file_path: str,
    operators: list[GremlinOperator] | None = None,
) -> None:
    self.file_path = file_path
    self.gremlins: list[Gremlin] = []
    self._gremlin_counter = 0
    self._operators = operators if operators is not None else get_default_registry().get_all()
    # Create a short unique prefix from file path to avoid ID collisions
    # when processing multiple files in parallel
    self._file_prefix = self._make_file_prefix(file_path)

visit_Compare

Python
visit_Compare(node)

Replace comparison nodes with mutation switching expressions.

Source code in src/pytest_gremlins/instrumentation/transformer.py
Python
def visit_Compare(self, node: ast.Compare) -> ast.expr:
    """Replace comparison nodes with mutation switching expressions."""
    self.generic_visit(node)

    gremlins = self._create_gremlins_for_node(node)
    if not gremlins:
        return node

    self.gremlins.extend(gremlins)
    return build_switching_expression(node, gremlins)

visit_BinOp

Python
visit_BinOp(node)

Replace binary operation nodes with mutation switching expressions.

Source code in src/pytest_gremlins/instrumentation/transformer.py
Python
def visit_BinOp(self, node: ast.BinOp) -> ast.expr:
    """Replace binary operation nodes with mutation switching expressions."""
    self.generic_visit(node)

    gremlins = self._create_gremlins_for_node(node)
    if not gremlins:
        return node

    self.gremlins.extend(gremlins)
    return build_switching_expression(node, gremlins)

visit_BoolOp

Python
visit_BoolOp(node)

Replace boolean operation nodes with mutation switching expressions.

Source code in src/pytest_gremlins/instrumentation/transformer.py
Python
def visit_BoolOp(self, node: ast.BoolOp) -> ast.expr:
    """Replace boolean operation nodes with mutation switching expressions."""
    self.generic_visit(node)

    gremlins = self._create_gremlins_for_node(node)
    if not gremlins:
        return node

    self.gremlins.extend(gremlins)
    return build_switching_expression(node, gremlins)

visit_UnaryOp

Python
visit_UnaryOp(node)

Replace unary operation nodes (including 'not') with mutation switching.

Source code in src/pytest_gremlins/instrumentation/transformer.py
Python
def visit_UnaryOp(self, node: ast.UnaryOp) -> ast.expr:
    """Replace unary operation nodes (including 'not') with mutation switching."""
    self.generic_visit(node)

    gremlins = self._create_gremlins_for_node(node)
    if not gremlins:
        return node

    self.gremlins.extend(gremlins)
    return build_switching_expression(node, gremlins)

visit_Constant

Python
visit_Constant(node)

Replace boolean constants with mutation switching expressions.

Source code in src/pytest_gremlins/instrumentation/transformer.py
Python
def visit_Constant(self, node: ast.Constant) -> ast.expr:
    """Replace boolean constants with mutation switching expressions."""
    gremlins = self._create_gremlins_for_node(node)
    if not gremlins:
        return node

    self.gremlins.extend(gremlins)
    return build_switching_expression(node, gremlins)

visit_Return

Python
visit_Return(node)

Replace return statements with mutation switching.

Source code in src/pytest_gremlins/instrumentation/transformer.py
Python
def visit_Return(self, node: ast.Return) -> ast.stmt:
    """Replace return statements with mutation switching."""
    self.generic_visit(node)

    gremlins = self._create_gremlins_for_node(node)
    if not gremlins:
        return node

    self.gremlins.extend(gremlins)
    return build_switching_statement(node, gremlins)

How Switching Works

The transformer replaces mutation points with conditional expressions:

Original code:

Python
if age >= 18:
    return "adult"

Transformed code (conceptual):

Python
if (
    'g002' if __gremlin_active__ == 'g001' else
    'g003' if __gremlin_active__ == 'g002' else
    age >= 18
):
    return (
        None if __gremlin_active__ == 'g003' else
        "adult"
    )

When __gremlin_active__ is:

  • None - Original code executes
  • 'g001' - First mutation activates (>= to >)
  • 'g002' - Second mutation activates (>= to <)
  • etc.

build_switching_expression

Builds a nested ternary expression for mutation switching.

build_switching_expression

Python
build_switching_expression(original, gremlins)

Build an AST expression that switches between original and mutated code.

Creates a nested IfExp (ternary expression) that checks gremlin_active and returns the appropriate expression based on which gremlin is active.

The generated code is equivalent to

mutated1 if gremlin_active == 'g001' else ( mutated2 if gremlin_active == 'g002' else ( original ) )

Parameters:

Name Type Description Default
original expr

The original AST expression node.

required
gremlins list[Gremlin]

List of gremlins that apply to this expression.

required

Returns:

Type Description
IfExp

An IfExp AST node implementing the switching logic.

Source code in src/pytest_gremlins/instrumentation/transformer.py
Python
def build_switching_expression(original: ast.expr, gremlins: list[Gremlin]) -> ast.IfExp:
    """Build an AST expression that switches between original and mutated code.

    Creates a nested IfExp (ternary expression) that checks __gremlin_active__
    and returns the appropriate expression based on which gremlin is active.

    The generated code is equivalent to:
        mutated1 if __gremlin_active__ == 'g001' else (
            mutated2 if __gremlin_active__ == 'g002' else (
                original
            )
        )

    Args:
        original: The original AST expression node.
        gremlins: List of gremlins that apply to this expression.

    Returns:
        An IfExp AST node implementing the switching logic.
    """
    gremlin_active = ast.Name(id='__gremlin_active__', ctx=ast.Load())

    result: ast.expr = copy.deepcopy(original)

    for gremlin in reversed(gremlins):
        condition = ast.Compare(
            left=gremlin_active,
            ops=[ast.Eq()],
            comparators=[ast.Constant(value=gremlin.gremlin_id)],
        )
        # mutated_node is ast.expr | ast.stmt; we know only expr gremlins reach here
        mutated_expr: ast.expr = copy.deepcopy(gremlin.mutated_node)  # type: ignore[assignment]
        result = ast.IfExp(
            test=condition,
            body=mutated_expr,
            orelse=result,
        )

    return result  # type: ignore[return-value]

build_switching_statement

Builds a nested if statement for statement-level mutations (like return).

build_switching_statement

Python
build_switching_statement(original, gremlins)

Build an AST statement that switches between original and mutated statements.

Creates a nested If statement that checks gremlin_active and executes the appropriate statement based on which gremlin is active.

Parameters:

Name Type Description Default
original stmt

The original AST statement node.

required
gremlins list[Gremlin]

List of gremlins that apply to this statement.

required

Returns:

Type Description
If

An If AST node implementing the switching logic.

Source code in src/pytest_gremlins/instrumentation/transformer.py
Python
def build_switching_statement(
    original: ast.stmt,
    gremlins: list[Gremlin],
) -> ast.If:
    """Build an AST statement that switches between original and mutated statements.

    Creates a nested If statement that checks __gremlin_active__
    and executes the appropriate statement based on which gremlin is active.

    Args:
        original: The original AST statement node.
        gremlins: List of gremlins that apply to this statement.

    Returns:
        An If AST node implementing the switching logic.
    """
    gremlin_active = ast.Name(id='__gremlin_active__', ctx=ast.Load())

    result: ast.stmt = copy.deepcopy(original)

    for gremlin in reversed(gremlins):
        condition = ast.Compare(
            left=gremlin_active,
            ops=[ast.Eq()],
            comparators=[ast.Constant(value=gremlin.gremlin_id)],
        )
        # mutated_node is ast.expr | ast.stmt; we know only stmt gremlins reach here
        mutated_stmt: ast.stmt = copy.deepcopy(gremlin.mutated_node)  # type: ignore[assignment]
        result = ast.If(
            test=condition,
            body=[mutated_stmt],
            orelse=[result],
        )

    return result  # type: ignore[return-value]

get_default_registry

Returns the default operator registry with all 5 built-in operators.

get_default_registry

Python
get_default_registry()

Get the default operator registry with all 5 operators registered.

Returns:

Type Description
OperatorRegistry

OperatorRegistry with comparison, arithmetic, boolean, boundary, and return operators.

Source code in src/pytest_gremlins/instrumentation/transformer.py
Python
def get_default_registry() -> OperatorRegistry:
    """Get the default operator registry with all 5 operators registered.

    Returns:
        OperatorRegistry with comparison, arithmetic, boolean, boundary, and return operators.
    """
    return _default_registry

Registered Operators

Name Class Description
comparison ComparisonOperator <, <=, >, >=, ==, !=
arithmetic ArithmeticOperator +, -, *, /, //, %, **
boolean BooleanOperator and/or, True/False, not
boundary BoundaryOperator Integer constants ± 1
return ReturnOperator Return value mutations

Switcher

ACTIVE_GREMLIN_ENV_VAR

The environment variable that controls which gremlin is active.

Python
from pytest_gremlins.instrumentation import ACTIVE_GREMLIN_ENV_VAR

print(ACTIVE_GREMLIN_ENV_VAR)  # 'ACTIVE_GREMLIN'

get_active_gremlin

Returns the currently active gremlin ID from the environment.

get_active_gremlin

Python
get_active_gremlin()

Get the currently active gremlin ID from the environment.

Returns:

Type Description
str | None

The gremlin ID if ACTIVE_GREMLIN is set, None otherwise.

Source code in src/pytest_gremlins/instrumentation/switcher.py
Python
def get_active_gremlin() -> str | None:
    """Get the currently active gremlin ID from the environment.

    Returns:
        The gremlin ID if ACTIVE_GREMLIN is set, None otherwise.
    """
    return os.environ.get(ACTIVE_GREMLIN_ENV_VAR)

Example

Python
import os
from pytest_gremlins.instrumentation import get_active_gremlin

# No gremlin active
print(get_active_gremlin())  # None

# Activate a gremlin
os.environ['ACTIVE_GREMLIN'] = 'g001'
print(get_active_gremlin())  # 'g001'

Import Hooks

The import hooks module intercepts Python imports to inject instrumented code.

GremlinFinder

MetaPathFinder that intercepts imports for instrumented modules.

GremlinFinder

Python
GremlinFinder(instrumented_modules)

Bases: MetaPathFinder

Finder that intercepts imports for instrumented modules.

This finder is registered on sys.meta_path and returns a ModuleSpec with GremlinLoader for modules that have instrumented code available.

Parameters:

Name Type Description Default
instrumented_modules dict[str, Module]

Mapping of module names to their instrumented ASTs.

required
Source code in src/pytest_gremlins/instrumentation/import_hooks.py
Python
def __init__(self, instrumented_modules: dict[str, ast.Module]) -> None:
    """Initialize the finder with instrumented module ASTs.

    Args:
        instrumented_modules: Mapping of module names to their instrumented ASTs.
    """
    self._instrumented_modules = instrumented_modules

find_spec

Python
find_spec(fullname, path, target=None)

Find a module spec for the given module name.

Parameters:

Name Type Description Default
fullname str

The fully qualified module name.

required
path Sequence[str] | None

The module search path (unused).

required
target ModuleType | None

The target module (unused).

None

Returns:

Type Description
ModuleSpec | None

ModuleSpec with GremlinLoader if module is instrumented, None otherwise.

Source code in src/pytest_gremlins/instrumentation/import_hooks.py
Python
def find_spec(
    self,
    fullname: str,
    path: Sequence[str] | None,  # noqa: ARG002
    target: types.ModuleType | None = None,  # noqa: ARG002
) -> ModuleSpec | None:
    """Find a module spec for the given module name.

    Args:
        fullname: The fully qualified module name.
        path: The module search path (unused).
        target: The target module (unused).

    Returns:
        ModuleSpec with GremlinLoader if module is instrumented, None otherwise.
    """
    if fullname not in self._instrumented_modules:
        return None

    tree = self._instrumented_modules[fullname]
    loader = GremlinLoader(tree, fullname)
    return ModuleSpec(fullname, loader)

GremlinLoader

Loader that executes instrumented AST code.

GremlinLoader

Python
GremlinLoader(tree, module_name)

Bases: Loader

Loader that executes instrumented AST code.

This loader compiles and executes a pre-transformed AST instead of loading code from disk. It also injects the gremlin_active variable from the ACTIVE_GREMLIN environment variable.

Parameters:

Name Type Description Default
tree Module

The instrumented AST to execute.

required
module_name str

The name of the module being loaded.

required
Source code in src/pytest_gremlins/instrumentation/import_hooks.py
Python
def __init__(self, tree: ast.Module, module_name: str) -> None:
    """Initialize the loader with an instrumented AST.

    Args:
        tree: The instrumented AST to execute.
        module_name: The name of the module being loaded.
    """
    self._tree = tree
    self._module_name = module_name

create_module

Python
create_module(spec)

Return None to use default module creation semantics.

Parameters:

Name Type Description Default
spec ModuleSpec

The module specification.

required

Returns:

Type Description
ModuleType | None

None to use default module creation.

Source code in src/pytest_gremlins/instrumentation/import_hooks.py
Python
def create_module(self, spec: ModuleSpec) -> types.ModuleType | None:  # noqa: ARG002
    """Return None to use default module creation semantics.

    Args:
        spec: The module specification.

    Returns:
        None to use default module creation.
    """
    return None

exec_module

Python
exec_module(module)

Execute the instrumented AST in the module's namespace.

This method: 1. Injects gremlin_active from the ACTIVE_GREMLIN env var 2. Compiles the instrumented AST 3. Executes it in the module's namespace

Parameters:

Name Type Description Default
module ModuleType

The module to execute code in.

required
Source code in src/pytest_gremlins/instrumentation/import_hooks.py
Python
def exec_module(self, module: types.ModuleType) -> None:
    """Execute the instrumented AST in the module's namespace.

    This method:
    1. Injects __gremlin_active__ from the ACTIVE_GREMLIN env var
    2. Compiles the instrumented AST
    3. Executes it in the module's namespace

    Args:
        module: The module to execute code in.
    """
    # Inject __gremlin_active__ from environment variable
    module.__dict__['__gremlin_active__'] = os.environ.get(ACTIVE_GREMLIN_ENV_VAR)

    # Compile and execute the instrumented AST
    # Note: This exec is intentional - we're executing pre-validated, transformed AST
    # from our own instrumentation process, not arbitrary user input.
    try:
        code = compile(self._tree, self._module_name, 'exec')
        exec(code, module.__dict__)  # noqa: S102
    except Exception:
        logger.error(
            'Failed to execute instrumented module %s',
            self._module_name,
            exc_info=True,
        )
        raise

register_import_hooks

Registers import hooks for instrumented modules.

register_import_hooks

Python
register_import_hooks(instrumented_modules)

Register import hooks for instrumented modules.

This function adds a GremlinFinder to sys.meta_path that will intercept imports for the specified modules and load instrumented code instead.

Parameters:

Name Type Description Default
instrumented_modules dict[str, Module]

Mapping of module names to their instrumented ASTs.

required
Source code in src/pytest_gremlins/instrumentation/import_hooks.py
Python
def register_import_hooks(instrumented_modules: dict[str, ast.Module]) -> None:
    """Register import hooks for instrumented modules.

    This function adds a GremlinFinder to sys.meta_path that will intercept
    imports for the specified modules and load instrumented code instead.

    Args:
        instrumented_modules: Mapping of module names to their instrumented ASTs.
    """
    global _registered_finder  # noqa: PLW0603
    unregister_import_hooks()  # Clean up any existing registration

    _registered_finder = GremlinFinder(instrumented_modules)
    sys.meta_path.insert(0, _registered_finder)

unregister_import_hooks

Removes import hooks from sys.meta_path.

unregister_import_hooks

Python
unregister_import_hooks()

Unregister import hooks from sys.meta_path.

This function removes any previously registered GremlinFinder from sys.meta_path. It is safe to call even if no hooks are registered.

Source code in src/pytest_gremlins/instrumentation/import_hooks.py
Python
def unregister_import_hooks() -> None:
    """Unregister import hooks from sys.meta_path.

    This function removes any previously registered GremlinFinder from
    sys.meta_path. It is safe to call even if no hooks are registered.
    """
    global _registered_finder  # noqa: PLW0603

    if _registered_finder is not None and _registered_finder in sys.meta_path:
        sys.meta_path.remove(_registered_finder)

    _registered_finder = None

    # Also remove any GremlinFinder instances that might have been added
    # by other means (defensive cleanup)
    sys.meta_path[:] = [f for f in sys.meta_path if not isinstance(f, GremlinFinder)]

Import Hook Flow

Text Only
1. register_import_hooks({'mymodule': instrumented_ast})
2. import mymodule  # Python calls GremlinFinder.find_spec()
3. GremlinFinder returns ModuleSpec with GremlinLoader
4. Python calls GremlinLoader.exec_module()
5. Loader injects __gremlin_active__ and executes instrumented AST

Finder

MutationPointVisitor

AST visitor that collects nodes that can be mutated.

MutationPointVisitor

Python
MutationPointVisitor()

Bases: NodeVisitor

AST visitor that collects nodes that can be mutated.

Source code in src/pytest_gremlins/instrumentation/finder.py
Python
def __init__(self) -> None:
    self.mutation_points: list[ast.AST] = []

visit_Compare

Python
visit_Compare(node)

Collect comparison nodes as mutation points.

Source code in src/pytest_gremlins/instrumentation/finder.py
Python
def visit_Compare(self, node: ast.Compare) -> None:
    """Collect comparison nodes as mutation points."""
    self.mutation_points.append(node)
    self.generic_visit(node)

find_mutation_points

Finds all mutation points in an AST.

find_mutation_points

Python
find_mutation_points(tree)

Find all mutation points in an AST.

Parameters:

Name Type Description Default
tree AST

The AST to search for mutation points.

required

Returns:

Type Description
list[AST]

List of AST nodes that can be mutated.

Source code in src/pytest_gremlins/instrumentation/finder.py
Python
def find_mutation_points(tree: ast.AST) -> list[ast.AST]:
    """Find all mutation points in an AST.

    Args:
        tree: The AST to search for mutation points.

    Returns:
        List of AST nodes that can be mutated.
    """
    visitor = MutationPointVisitor()
    visitor.visit(tree)
    return visitor.mutation_points

Example

Python
import ast
from pytest_gremlins.instrumentation.finder import find_mutation_points

source = '''
def check(x, y):
    if x > y:
        return x - y
    return y - x
'''

tree = ast.parse(source)
points = find_mutation_points(tree)
print(f'Found {len(points)} mutation points')

Helper Functions

create_gremlins_for_node

Creates gremlins for any AST node using a specific operator.

create_gremlins_for_node

Python
create_gremlins_for_node(node, operator, file_path, id_generator)

Create gremlins for any AST node using a specific operator.

Parameters:

Name Type Description Default
node expr | stmt

The AST node to mutate.

required
operator GremlinOperator

The operator to use for mutation.

required
file_path str

Path to the source file (for gremlin metadata).

required
id_generator Callable[[], str]

Callable that returns the next gremlin ID.

required

Returns:

Type Description
list[Gremlin]

List of Gremlin objects for each possible mutation.

Source code in src/pytest_gremlins/instrumentation/transformer.py
Python
def create_gremlins_for_node(
    node: ast.expr | ast.stmt,
    operator: GremlinOperator,
    file_path: str,
    id_generator: Callable[[], str],
) -> list[Gremlin]:
    """Create gremlins for any AST node using a specific operator.

    Args:
        node: The AST node to mutate.
        operator: The operator to use for mutation.
        file_path: Path to the source file (for gremlin metadata).
        id_generator: Callable that returns the next gremlin ID.

    Returns:
        List of Gremlin objects for each possible mutation.
    """
    if not operator.can_mutate(node):
        return []

    mutations = operator.mutate(node)
    gremlins: list[Gremlin] = []

    for mutated_node in mutations:
        if not isinstance(mutated_node, (ast.expr, ast.stmt)):
            continue  # pragma: no cover
        description = _get_mutation_description(node, mutated_node, operator)
        gremlin = Gremlin(
            gremlin_id=id_generator(),
            file_path=file_path,
            line_number=getattr(node, 'lineno', 0),
            original_node=node,
            mutated_node=mutated_node,
            operator_name=operator.name,
            description=description,
        )
        gremlins.append(gremlin)

    return gremlins

create_gremlins_for_compare

Creates gremlins specifically for comparison nodes.

create_gremlins_for_compare

Python
create_gremlins_for_compare(node, file_path, id_generator)

Create gremlins for a comparison node.

This is the shared logic for creating gremlins from comparison AST nodes. Used by both GremlinCollector and MutationSwitchingTransformer.

Parameters:

Name Type Description Default
node Compare

The comparison AST node.

required
file_path str

Path to the source file (for gremlin metadata).

required
id_generator Callable[[], str]

Callable that returns the next gremlin ID.

required

Returns:

Type Description
list[Gremlin]

List of Gremlin objects for each possible mutation.

Source code in src/pytest_gremlins/instrumentation/transformer.py
Python
def create_gremlins_for_compare(
    node: ast.Compare,
    file_path: str,
    id_generator: Callable[[], str],
) -> list[Gremlin]:
    """Create gremlins for a comparison node.

    This is the shared logic for creating gremlins from comparison AST nodes.
    Used by both GremlinCollector and MutationSwitchingTransformer.

    Args:
        node: The comparison AST node.
        file_path: Path to the source file (for gremlin metadata).
        id_generator: Callable that returns the next gremlin ID.

    Returns:
        List of Gremlin objects for each possible mutation.
    """
    gremlins: list[Gremlin] = []
    mutations = generate_comparison_mutations(node)
    for mutated_node in mutations:
        original_op = _comparison_operator.get_symbol(node.ops[0])
        mutated_op = _comparison_operator.get_symbol(mutated_node.ops[0])
        gremlin = Gremlin(
            gremlin_id=id_generator(),
            file_path=file_path,
            line_number=node.lineno,
            original_node=node,
            mutated_node=mutated_node,
            operator_name='comparison',
            description=f'{original_op} to {mutated_op}',
        )
        gremlins.append(gremlin)
    return gremlins

collect_gremlins

Collects gremlins from source without instrumenting it.

collect_gremlins

Python
collect_gremlins(source, file_path)

Collect gremlins from source code without modifying it.

This function parses the source and identifies all potential mutation points, returning the gremlins found and the original (unmodified) AST.

Note: This does NOT instrument the code. For instrumentation with mutation switching embedded, use transform_source() instead.

Parameters:

Name Type Description Default
source str

The Python source code to analyze.

required
file_path str

The path to the source file (for gremlin metadata).

required

Returns:

Type Description
tuple[list[Gremlin], Module]

Tuple of (list of gremlins, original unmodified AST).

Source code in src/pytest_gremlins/instrumentation/transformer.py
Python
def collect_gremlins(source: str, file_path: str) -> tuple[list[Gremlin], ast.Module]:
    """Collect gremlins from source code without modifying it.

    This function parses the source and identifies all potential mutation
    points, returning the gremlins found and the original (unmodified) AST.

    Note: This does NOT instrument the code. For instrumentation with
    mutation switching embedded, use transform_source() instead.

    Args:
        source: The Python source code to analyze.
        file_path: The path to the source file (for gremlin metadata).

    Returns:
        Tuple of (list of gremlins, original unmodified AST).
    """
    tree = ast.parse(source)
    collector = GremlinCollector(file_path)
    collector.visit(tree)
    return collector.gremlins, tree

Performance Considerations

Why Mutation Switching is Fast

Approach File I/O per Mutation Module Reloads
Traditional Read + Write + Restore Yes
Switching None (env var only) No

Memory Trade-off

The transformed AST is larger because it contains all mutations embedded. For a file with N mutation points generating M mutations total:

  • Original AST: ~1x size
  • Transformed AST: ~1x + (M * switch_overhead)

This trade-off is worthwhile because:

  1. Transformation happens once per file
  2. Test execution happens M times per file
  3. Memory is cheap; I/O is expensive