Skip to content

signatures

Class info

🛈 DocStrings

Signature utils.

create_bound_callable

create_bound_callable(
    original_callable: Callable[..., Any],
    by_name: dict[str, Any] | None = None,
    by_type: dict[type, Any] | None = None,
    bind_kwargs: bool = False,
) -> Callable[..., Awaitable[Any]]

Create a wrapper that pre-binds parameters by name or type.

Parameters are bound by their position in the function signature. Only positional and positional-or-keyword parameters can be bound by default. If bind_kwargs=True, keyword-only parameters can also be bound using the same by_name/by_type logic. Binding by name takes priority over binding by type.

Parameters:

Name Type Description Default
original_callable Callable[..., Any]

The original callable that may need parameter binding

required
by_name dict[str, Any] | None

Parameters to bind by exact parameter name

None
by_type dict[type, Any] | None

Parameters to bind by parameter type annotation

None
bind_kwargs bool

Whether to also bind keyword-only parameters

False

Returns:

Type Description
Callable[..., Awaitable[Any]]

New callable with parameters pre-bound and proper introspection

Raises:

Type Description
ValueError

If the callable's signature cannot be inspected

Source code in src/llmling_agent/utils/signatures.py
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
def create_bound_callable(  # noqa: PLR0915
    original_callable: Callable[..., Any],
    by_name: dict[str, Any] | None = None,
    by_type: dict[type, Any] | None = None,
    bind_kwargs: bool = False,
) -> Callable[..., Awaitable[Any]]:
    """Create a wrapper that pre-binds parameters by name or type.

    Parameters are bound by their position in the function signature. Only
    positional and positional-or-keyword parameters can be bound by default.
    If bind_kwargs=True, keyword-only parameters can also be bound using the
    same by_name/by_type logic. Binding by name takes priority over binding by type.

    Args:
        original_callable: The original callable that may need parameter binding
        by_name: Parameters to bind by exact parameter name
        by_type: Parameters to bind by parameter type annotation
        bind_kwargs: Whether to also bind keyword-only parameters

    Returns:
        New callable with parameters pre-bound and proper introspection

    Raises:
        ValueError: If the callable's signature cannot be inspected
    """
    try:
        sig = inspect.signature(original_callable)
    except (ValueError, TypeError) as e:
        msg = f"Cannot inspect signature of {original_callable}. Ensure callable is inspectable."
        raise ValueError(msg) from e

    # Build position-to-value mapping for positional binding
    context_values = {}
    # Build name-to-value mapping for keyword-only binding
    kwarg_bindings = {}

    for i, param in enumerate(sig.parameters.values()):
        # Bind positional and positional-or-keyword parameters
        if param.kind in (
            inspect.Parameter.POSITIONAL_ONLY,
            inspect.Parameter.POSITIONAL_OR_KEYWORD,
        ):
            # Bind by name first (higher priority)
            if by_name and param.name in by_name:
                context_values[i] = by_name[param.name]
            # Then bind by type if not already bound
            elif by_type and _find_matching_type(param.annotation, by_type) is not None:
                context_values[i] = _find_matching_type(param.annotation, by_type)
        # Bind keyword-only parameters if enabled
        elif bind_kwargs and param.kind == inspect.Parameter.KEYWORD_ONLY:
            # Bind by name first (higher priority)
            if by_name and param.name in by_name:
                kwarg_bindings[param.name] = by_name[param.name]
            # Then bind by type if not already bound
            elif by_type and _find_matching_type(param.annotation, by_type) is not None:
                kwarg_bindings[param.name] = _find_matching_type(param.annotation, by_type)

    async def wrapper(*args: Any, **kwargs: Any) -> Any:
        # Filter out kwargs that would conflict with bound parameters
        param_names = list(sig.parameters.keys())
        bound_param_names = {param_names[i] for i in context_values}
        bound_kwarg_names = set(kwarg_bindings.keys())
        filtered_kwargs = {
            k: v
            for k, v in kwargs.items()
            if k not in bound_param_names and k not in bound_kwarg_names
        }

        # Add bound keyword-only parameters
        filtered_kwargs.update(kwarg_bindings)

        # Build new_args with context values at correct positions
        new_args = []
        arg_index = 0
        for param_index in range(len(sig.parameters)):
            if param_index in context_values:
                new_args.append(context_values[param_index])
            elif arg_index < len(args):
                new_args.append(args[arg_index])
                arg_index += 1

        # Add any remaining positional args
        if arg_index < len(args):
            new_args.extend(args[arg_index:])

        if inspect.iscoroutinefunction(original_callable):
            return await original_callable(*new_args, **filtered_kwargs)
        return original_callable(*new_args, **filtered_kwargs)

    # Preserve introspection attributes
    wrapper.__name__ = getattr(original_callable, "__name__", "wrapper")
    wrapper.__doc__ = getattr(original_callable, "__doc__", None)
    wrapper.__module__ = getattr(original_callable, "__module__", None)  # type: ignore[assignment]
    wrapper.__wrapped__ = original_callable  # type: ignore[attr-defined]
    wrapper.__llmling_wrapped__ = original_callable  # type: ignore[attr-defined]

    # Create modified signature without context parameters
    try:
        params = list(sig.parameters.values())
        # Remove parameters at context positions and bound kwargs
        context_positions = set(context_values.keys())
        bound_kwarg_names = set(kwarg_bindings.keys())
        new_params = [
            param
            for i, param in enumerate(params)
            if i not in context_positions and param.name not in bound_kwarg_names
        ]
        new_sig = sig.replace(parameters=new_params)
        wrapper.__signature__ = new_sig  # type: ignore[attr-defined]
        wrapper.__annotations__ = {
            name: param.annotation for name, param in new_sig.parameters.items()
        }
        if sig.return_annotation != inspect.Signature.empty:
            wrapper.__annotations__["return"] = sig.return_annotation

    except (ValueError, TypeError):
        logger.debug("Failed to update wrapper signature", original=original_callable)

    return wrapper

create_modified_signature

create_modified_signature(
    fn_or_sig: Callable[..., Any] | Signature,
    *,
    remove: str | list[str] | None = None,
    inject: dict[str, type] | None = None
) -> Signature

Create a modified signature by removing specified parameters / injecting new ones.

Parameters:

Name Type Description Default
fn_or_sig Callable[..., Any] | Signature

The function or signature to modify.

required
remove str | list[str] | None

The parameter(s) to remove.

None
inject dict[str, type] | None

The parameter(s) to inject.

None

Returns:

Type Description
Signature

The modified signature.

Source code in src/llmling_agent/utils/signatures.py
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
def create_modified_signature(
    fn_or_sig: Callable[..., Any] | inspect.Signature,
    *,
    remove: str | list[str] | None = None,
    inject: dict[str, type] | None = None,
) -> inspect.Signature:
    """Create a modified signature by removing specified parameters / injecting new ones.

    Args:
        fn_or_sig: The function or signature to modify.
        remove: The parameter(s) to remove.
        inject: The parameter(s) to inject.

    Returns:
        The modified signature.
    """
    sig = fn_or_sig if isinstance(fn_or_sig, inspect.Signature) else inspect.signature(fn_or_sig)
    rem_keys = [remove] if isinstance(remove, str) else remove or []
    new_params = [p for p in sig.parameters.values() if p.name not in rem_keys]
    if inject:
        injected_params = []
        for k, v in inject.items():
            injected_params.append(
                inspect.Parameter(k, inspect.Parameter.POSITIONAL_OR_KEYWORD, annotation=v)
            )
        new_params = injected_params + new_params
    return sig.replace(parameters=new_params)