stubpy.resolver¶
stubpy.resolver¶
Parameter resolution — the core **kwargs / *args backtracing logic.
Two public entry points cover every resolution scenario:
resolve_params()— resolves a method on a class using the class MRO. Three strategies are applied in priority order:No variadics — own parameters returned unchanged.
cls()-call detection — a
@classmethodbody containscls(..., **kwargs); resolve againstcls.__init__.MRO walk — iterate ancestors that define the same method, collecting concrete parameters until all variadics are resolved. If variadics remain after the MRO is exhausted, module-level function targets (from ast_info) are resolved via namespace.
resolve_function_params()— resolves a module-level function using AST-detected forwarding targets and a runtime namespace lookup. When a target is a class constructor (atype), resolution delegates toresolve_params()so the chain continues correctly.
Both entry points call each other when mixed chains are encountered (method → function → method, function → class → method, etc.). Cycle detection via a _seen frozenset prevents infinite recursion.
Architecture note¶
AST scanning (detecting where **kwargs / *args is forwarded)
lives exclusively in stubpy.ast_pass — specifically in
_harvest_function(), which populates
kwargs_forwarded_to and
args_forwarded_to for every
function and method definition.
This module handles resolution only — it reads pre-scanned metadata
and performs runtime lookups, never re-parsing source code except in the
_detect_cls_call fallback path (backward compat for callers that
omit ast_info).
Class-method resolution
- resolve_params(cls: type, method_name: str, ast_info: FunctionInfo | None = None, namespace: dict[str, Any] | None = None, ast_info_by_name: dict[str, FunctionInfo] | None = None, _seen: frozenset[str] | None = None) list[ParamWithHints][source]¶
Return the fully-merged parameter list for method_name on cls.
Expands
**kwargsand*argsinto the concrete parameters they absorb using one of three strategies applied in order:No variadics — if the method has neither
*argsnor**kwargs, return its own parameters unchanged.cls()-call detection — if the method is a
@classmethodwhose body containscls(..., **kwargs), resolve againstcls.__init__. Parameters hardcoded in the call are excluded.MRO walk — iterate ancestors that define the same method, collecting concrete parameters until all variadics are resolved. If variadics remain after MRO exhaustion and namespace is provided, falls back to module-level function lookup (handles method → function forwarding chains).
selfandclsare always excluded from the result.- Parameters:
cls (type) – The class owning (or inheriting) the method.
method_name (str) – Name of the method to resolve.
ast_info (FunctionInfo, optional) – Pre-scanned metadata from
ASTHarvester. When provided, avoids a redundant inline AST parse inside_detect_cls_call()and enables post-MRO namespace fallback.namespace (dict, optional) – Module
__dict__— used for method → function chain resolution. Passctx.module_namespacefrom the emitter.ast_info_by_name (dict, optional) – Maps function name → FunctionInfo — enables recursive resolution when a namespace target also has variadics.
_seen (frozenset, optional) – Cycle-detection set. Do not pass manually.
- Returns:
list of ParamWithHints – Ordered
(Parameter, hints_dict)tuples. Own parameters come first, then ancestor parameters in MRO order. Unresolvable**kwargsand explicitly-typed*argsare appended last.
See also
resolve_function_paramsEntry point for standalone module-level functions.
stubpy.emitter.generate_method_stubConsumes the output of this function.
Examples
>>> class Base: ... def __init__(self, color: str, opacity: float = 1.0) -> None: ... >>> class Child(Base): ... def __init__(self, label: str, **kwargs) -> None: ... >>> params = resolve_params(Child, "__init__") >>> [p.name for p, _ in params] ['label', 'color', 'opacity']
Module-level function resolution
- resolve_function_params(live_fn: Any, ast_info: FunctionInfo | None, namespace: dict[str, Any], *, ast_info_by_name: dict[str, FunctionInfo] | None = None, _seen: frozenset[str] | None = None) list[ParamWithHints][source]¶
Return the fully-merged parameter list for a module-level function.
Expands
**kwargsand*argsinto concrete parameters by following AST-detected forwarding targets looked up in namespace.Unlike
resolve_params()(which walks the class MRO), this handles standalone functions where there is no class hierarchy. It relies onkwargs_forwarded_toandargs_forwarded_to— pre-populated by_harvest_function().When a forwarding target is a class (type), resolution delegates to
resolve_params()on that class’s__init__. This handles function → class constructor chains. Similarly,resolve_params()falls back to this function when a method forwards to a standalone function — enabling arbitrarily deep mixed chains.All parameter kinds are handled correctly:
POSITIONAL_ONLY(/) — promoted toPOSITIONAL_OR_KEYWORDwhen absorbed through**kwargs(callers use keyword form).POSITIONAL_OR_KEYWORD— merged as-is.VAR_POSITIONAL(*args) — resolved viaargs_forwarded_to; kept if still unresolved or explicitly typed.KEYWORD_ONLY(after*/*args) — merged as-is; the emitter inserts the bare*separator automatically.VAR_KEYWORD(**kwargs) — resolved viakwargs_forwarded_to; kept as residual only when no target fully resolved it.
Strategy (in order)¶
No variadics — own parameters returned unchanged.
No targets — own parameters returned unchanged (variadics preserved; no information to expand them).
Target resolution — look each name up in namespace, merge its concrete params (recursively for chained forwarding), with cycle detection via _seen. Class targets are handled via
resolve_params()on their__init__.Finalise — re-insert
*argsand residual**kwargs.
- param live_fn:
The live function object from the loaded module.
- type live_fn:
callable
- param ast_info:
Pre-scanned metadata from
ASTHarvester.- type ast_info:
FunctionInfo or None
- param namespace:
The module’s
__dict__— used to look up live callable targets.- type namespace:
dict
- param ast_info_by_name:
Maps function name →
FunctionInfofor functions in the same module. Enables recursive resolution when a forwarding target also has variadics.- type ast_info_by_name:
dict, optional
- param _seen:
Names already on the call stack — prevents infinite recursion in mutually-recursive forwarding patterns. Do not pass manually.
- type _seen:
frozenset, optional
- returns:
list of ParamWithHints – Ordered
(Parameter, hints_dict)tuples, variadics expanded as far as possible.
See also
resolve_paramsEntry point for class method resolution via MRO.
Examples
>>> def make_color(r: float, g: float, b: float, a: float = 1.0): ... >>> def make_red(r: float = 1.0, **kwargs): make_color(r=r, **kwargs)
Shared helpers
- _normalise_kind(p: Parameter) Parameter[source]¶
Promote
POSITIONAL_ONLYtoPOSITIONAL_OR_KEYWORD.When a positional-only parameter from a parent is absorbed by a child’s
**kwargs, callers pass it by keyword through the child interface. Keeping itPOSITIONAL_ONLYwould emit a misplaced/separator. This helper corrects that.
- _merge_concrete_params(base: list[tuple[Parameter, dict[str, Any]]], seen: set[str], source: list[tuple[Parameter, dict[str, Any]]]) None[source]¶
Merge non-variadic params from source into base, deduplicating by name.
VAR_POSITIONALandVAR_KEYWORDentries in source are skipped — those are handled separately by_finalise_variadics().POSITIONAL_ONLYparameters are promoted via_normalise_kind().
- _finalise_variadics(merged: list[tuple[Parameter, dict[str, Any]]], own_params: list[Parameter], own_hints: dict[str, Any], still_var_pos: bool, still_var_kw: bool) list[tuple[Parameter, dict[str, Any]]][source]¶
Re-insert
*argsand residual**kwargsat correct positions.Called after all concrete parameters have been merged.
*argsis inserted before the firstKEYWORD_ONLYorVAR_KEYWORDparameter, if still unresolved or if the original*argscarried an explicit type annotation.**kwargsis appended last, only when still unresolved.
- Parameters:
merged (list of ParamWithHints) – Merged concrete-param list. Modified in place.
own_params (list of inspect.Parameter) – Original parameter list — source of the
*args/**kwargsobjects (with their annotations and defaults).own_hints (dict) – Type hints for own_params.
still_var_pos (bool) –
Truewhen*argswas not fully resolved.still_var_kw (bool) –
Truewhen**kwargswas not fully resolved.
- Returns:
list of ParamWithHints – merged with variadics inserted / appended.
- _enforce_signature_validity(merged: list[tuple[Parameter, dict[str, Any]]]) list[tuple[Parameter, dict[str, Any]]][source]¶
Promote non-default params to KEYWORD_ONLY when they follow a default param.
Python requires that a parameter without a default value never follows a parameter that has one (outside of
/or*separation). This situation arises when params are absorbed from a forwarding target:def make_color(r, g, b, a=1.0): ... def make_red(r=1.0, **kwargs): make_color(r=r, **kwargs) # → merges to: make_red(r=1.0, g, b, a=1.0) ← invalid! # → fixed to: make_red(r=1.0, *, g, b, a=1.0) ← valid
The fix is correct semantically: absorbed params came in via
**kwargs, so callers must supply them by keyword anyway. Promoting them toKEYWORD_ONLYmakes that contract explicit in the stub.VAR_POSITIONALandVAR_KEYWORDsentinels are left untouched.- Parameters:
merged (list of ParamWithHints) – The fully-merged list (before adding
*//sentinels).- Returns:
list of ParamWithHints – Same list with any offending parameters promoted to
KEYWORD_ONLY.
Resolution strategies
resolve_params() (class methods) applies three strategies in order:
No variadics — return own parameters unchanged.
cls()-call detection — if the body contains
cls(..., **kwargs), resolve againstcls.__init__. Hardcoded keyword names are excluded.MRO walk — collect concrete params from ancestors until all variadics are resolved or the MRO is exhausted.
resolve_function_params() (standalone functions) applies:
No variadics — return own parameters unchanged.
No targets — return own parameters unchanged (variadics preserved).
Target resolution — look each name up in the module namespace and merge its concrete parameters. Recursive for chained forwarding; cycle-safe.
Default-ordering enforcement — absorbed non-default params following a defaulted own-param are promoted to
KEYWORD_ONLY.
Parameter ordering (both resolvers)
The function/method’s own concrete parameters (source order).
Parameters from the first resolved target / ancestor.
Parameters from further targets / ancestors, in resolution order.
*args— placed before keyword-only params and before**kwargs.Residual
**kwargs— appended last if still unresolved.
Positional-only parameters
POSITIONAL_ONLY parameters absorbed via **kwargs are promoted to
POSITIONAL_OR_KEYWORD by _normalise_kind() — a positional-only
param passed through **kwargs must be supplied by keyword.
Default-ordering rule
_enforce_signature_validity() promotes any non-default parameter that
follows a defaulted parameter in the positional portion to KEYWORD_ONLY.
This is correct semantically: absorbed params arrive via **kwargs and are
keyword arguments in practice.