Skip to content

scanner

agent_cover.instrumentation.structures.scanner

This module provides functionality to scan and analyze data structures (Pydantic models, dataclasses) within a given codebase.

It uses an abstraction layer for the inspect module to improve testability and isolate the scanning process from the actual filesystem and inspection calls.

Classes

InspectionProvider

Helper class to abstract the calls to inspect that touch the filesystem.

Methods:

Name Description
get_file

Any) -> str: Returns the file path of the given object.

get_source_lines

Any) -> Tuple[List[str], int]: Returns the source code lines and starting line number of the given object.

Source code in src/agent_cover/instrumentation/structures/scanner.py
class InspectionProvider:
    """Helper class to abstract the calls to inspect that touch the filesystem.

    Attributes:
        None

    Methods:
        get_file(obj: Any) -> str: Returns the file path of the given object.
        get_source_lines(obj: Any) -> Tuple[List[str], int]: Returns the source code lines and starting line number of the given object.
    """

    def get_file(self, obj: Any) -> str:
        """Returns the file path of the given object.

        Args:
            obj: The object to get the file path for.

        Returns:
            The file path of the object.
        """
        return inspect.getfile(obj)

    def get_source_lines(self, obj: Any) -> Tuple[List[str], int]:
        """Returns the source code lines and starting line number of the given object.

        Args:
            obj: The object to get the source code lines for.

        Returns:
            A tuple containing the source code lines as a list of strings and the starting line number.
        """
        return inspect.getsourcelines(obj)
Functions
get_file(obj)

Returns the file path of the given object.

Parameters:

Name Type Description Default
obj Any

The object to get the file path for.

required

Returns:

Type Description
str

The file path of the object.

Source code in src/agent_cover/instrumentation/structures/scanner.py
def get_file(self, obj: Any) -> str:
    """Returns the file path of the given object.

    Args:
        obj: The object to get the file path for.

    Returns:
        The file path of the object.
    """
    return inspect.getfile(obj)
get_source_lines(obj)

Returns the source code lines and starting line number of the given object.

Parameters:

Name Type Description Default
obj Any

The object to get the source code lines for.

required

Returns:

Type Description
Tuple[List[str], int]

A tuple containing the source code lines as a list of strings and the starting line number.

Source code in src/agent_cover/instrumentation/structures/scanner.py
def get_source_lines(self, obj: Any) -> Tuple[List[str], int]:
    """Returns the source code lines and starting line number of the given object.

    Args:
        obj: The object to get the source code lines for.

    Returns:
        A tuple containing the source code lines as a list of strings and the starting line number.
    """
    return inspect.getsourcelines(obj)

Functions

scan_pydantic_models(registry=None, config=None, root_path=None, adapter_registry=None, module_iterator=None, inspector=None)

Scans loaded modules to automatically generate Business Logic decisions.

This function inspects Pydantic models and Dataclasses to find fields with finite sets of expected values. It automatically populates the AgentCoverConfig with these rules.

Supported Types: - Enum: Expects all enum members. - Literal: Expects all literal values (e.g., Literal["yes", "no"]). - Bool: Expects True and False.

Parameters:

Name Type Description Default
registry Optional[AgentRegistry]

The AgentRegistry instance. Defaults to the result of get_registry().

None
config Optional[AgentCoverConfig]

The AgentCoverConfig instance. Defaults to the result of get_config().

None
root_path Optional[str]

The root path to scan for modules. Defaults to the current working directory.

None
adapter_registry Optional[AdapterRegistry]

The AdapterRegistry instance. Defaults to the result of get_default_adapter_registry().

None
module_iterator Optional[Callable[[], Dict[str, Any]]]

A callable that returns an iterator over the modules. Defaults to _default_module_iterator.

None
inspector Optional[InspectionProvider]

An instance of InspectionProvider for abstracting inspect calls. Defaults to None.

None

Examples:

If you have this model:

class Sentiment(BaseModel):
    label: Literal["POSITIVE", "NEGATIVE"]
This scanner creates a Decision rule expecting both "POSITIVE" and "NEGATIVE" to appear in the label field during testing.

Source code in src/agent_cover/instrumentation/structures/scanner.py
def scan_pydantic_models(
    registry: Optional[AgentRegistry] = None,
    config: Optional[AgentCoverConfig] = None,
    root_path: Optional[str] = None,
    adapter_registry: Optional[AdapterRegistry] = None,
    module_iterator: Optional[Callable[[], Dict[str, Any]]] = None,
    inspector: Optional[InspectionProvider] = None,
):
    """Scans loaded modules to automatically generate Business Logic decisions.

    This function inspects Pydantic models and Dataclasses to find fields with
    finite sets of expected values. It automatically populates the
    [`AgentCoverConfig`][agent_cover.config.AgentCoverConfig] with these rules.

    Supported Types:
    - **Enum**: Expects all enum members.
    - **Literal**: Expects all literal values (e.g., `Literal["yes", "no"]`).
    - **Bool**: Expects `True` and `False`.

    Args:
        registry: The AgentRegistry instance. Defaults to the result of get_registry().
        config: The AgentCoverConfig instance. Defaults to the result of get_config().
        root_path: The root path to scan for modules. Defaults to the current working directory.
        adapter_registry: The AdapterRegistry instance. Defaults to the result of get_default_adapter_registry().
        module_iterator: A callable that returns an iterator over the modules. Defaults to _default_module_iterator.
        inspector: An instance of InspectionProvider for abstracting inspect calls. Defaults to None.

    Examples:
        If you have this model:
        ```python
        class Sentiment(BaseModel):
            label: Literal["POSITIVE", "NEGATIVE"]
        ```
        This scanner creates a Decision rule expecting both "POSITIVE" and "NEGATIVE"
        to appear in the `label` field during testing.
    """
    if registry is None:
        registry = get_registry()
    if config is None:
        config = get_config()
    if root_path is None:
        root_path = os.getcwd()
    if adapter_registry is None:
        adapter_registry = get_default_adapter_registry()
    if module_iterator is None:
        module_iterator = _default_module_iterator
    if inspector is None:
        inspector = InspectionProvider()

    existing_ids = {d.id for d in config.decisions}
    count = 0

    modules_snapshot = module_iterator()

    for mod_name, mod in modules_snapshot.items():
        if not hasattr(mod, "__file__") or not mod.__file__:
            continue
        mod_file = os.path.abspath(mod.__file__)

        # User code filter
        if not mod_file.startswith(root_path) or "site-packages" in mod_file:
            continue

        for name, val in list(vars(mod).items()):
            adapter = adapter_registry.get_adapter_for_class(val)
            if not adapter:
                continue

            try:
                # We use the injected inspector to get the file
                class_file = inspector.get_file(val)
                class_file = os.path.abspath(class_file)

                if (
                    not class_file.startswith(root_path)
                    or "site-packages" in class_file
                ):
                    continue

                try:
                    # We use the injected inspector for the lines of code
                    lines, start_line = inspector.get_source_lines(val)
                except Exception as e:
                    logger.warning(e, exc_info=True)
                    start_line = 0
            except Exception as e:
                logger.warning(e, exc_info=True)
                # If get_file fails (e.g. built-in or dynamic class without mocks), we skip
                continue

            try:
                fields_map = adapter.get_fields(val)
                _analyze_model_fields(
                    val, fields_map, config, existing_ids, class_file, start_line
                )
                count += 1
            except Exception as e:
                logger.warning(f"[AgentCover][STRUCT] Skip {name}: {e}", exc_info=True)