stubpy.emitter¶
stubpy.emitter¶
Stub text generation — converts live objects and module-level symbols
into .pyi source text.
Formatting modes¶
Two formatting modes are chosen automatically:
Inline — used when a function / method has ≤ 2 non-self/cls parameters; the entire signature fits on one line.
Multi-line — used for larger signatures; each parameter gets its own indented line with a trailing comma.
Special-class handling¶
generate_class_stub() applies dedicated logic for common Python
patterns before falling back to the general reflection path:
NamedTuple subclasses — emits
class Name(NamedTuple):with per-field annotations and default values.@dataclass classes — emits the
@dataclassdecorator and synthesises an__init__stub from__dataclass_fields__, correctly handlingdefault_factory,init=False, andClassVarfields.Abstract methods — emits
@abstractmethodfor any callable whose__isabstractmethod__attribute is set.Generic base classes — reads
__orig_bases__(PEP 560) to emitclass Foo(Generic[T]):correctly, preserving subscripted type parameters that__bases__erases.
Alias and overload stubs¶
generate_alias_stub()re-emits TypeVar, TypeAlias, NewType, ParamSpec, and TypeVarTuple declarations from the AST pre-pass, with support for thealias_styleconfiguration option (compatibleName: TypeAlias = ..., or Python 3.12+type Name = ...form).generate_overload_group_stub()emits one@overloadstub per variant, suppressing the concrete implementation per PEP 484.
Parameter separators¶
insert_kw_separator()inserts a bare*before the first keyword-only parameter when no*argsis present.insert_pos_separator()inserts a bare/after the lastPOSITIONAL_ONLYparameter (PEP 570).
All methods emit async def when inspect.iscoroutinefunction()
or inspect.isasyncgenfunction() returns True.
Class stubs
- generate_class_stub(cls: type, ctx: StubContext) str[source]¶
Generate the full
.pyiblock for cls.Dispatches to specialised generators for NamedTuple subclasses and dataclasses before falling back to standard reflection. Abstract methods receive
@abstractmethod; async methods receiveasync def.- Parameters:
cls (type) – The class to stub.
ctx (StubContext) – The current
StubContext.
- Returns:
str – Complete class stub as a multi-line string without a trailing newline.
Examples
>>> from stubpy.context import StubContext >>> class Point: ... x: float ... y: float ... def __init__(self, x: float, y: float) -> None: ... >>> stub = generate_class_stub(Point, StubContext()) >>> "class Point:" in stub True >>> "x: float" in stub True
- generate_method_stub(cls: type, method_name: str, ctx: StubContext, indent: str = ' ') str[source]¶
Generate the
.pyistub line(s) for a single method on cls.Dispatches on the descriptor type in
cls.__dict__:property—@propertywith optional@name.setter.classmethod—@classmethodwithclsfirst.staticmethod—@staticmethodwith no implicit first parameter.Regular method —
selfas first parameter.
Emits
async defwhen the underlying callable is a coroutine or async generator function, and@abstractmethodwhen__isabstractmethod__is set.Methods with ≤ 2 non-self params are formatted inline; larger signatures are split across lines with a trailing comma on each parameter.
- Parameters:
cls (type) – The class that owns the method.
method_name (str) – Name of the method as it appears in
cls.__dict__.ctx (StubContext) – The current
StubContext.indent (str, optional) – Indentation string prepended to each line. Default is four spaces.
- Returns:
str – One or more stub lines, or
""if method_name is not incls.__dict__.
Examples
>>> from stubpy.context import StubContext >>> class A: ... def move(self, x: float, y: float) -> None: ... >>> stub = generate_method_stub(A, "move", StubContext()) >>> stub ' def move(self, x: float, y: float) -> None: ...'
- methods_defined_on(cls: type) list[str][source]¶
Return names of callable members defined directly on cls.
Only inspects
cls.__dict__— inherited members are excluded. Dunder names not in_PUBLIC_DUNDERSare silently skipped. Insertion order is preserved.- Parameters:
cls (type) – The class to inspect.
- Returns:
list of str – Method names (including classmethods, staticmethods, properties) defined on cls that should appear in a stub.
Examples
>>> class Parent: ... def parent_method(self) -> None: ... >>> class Child(Parent): ... def child_method(self) -> None: ... >>> methods_defined_on(Child) ['child_method']
Module-level symbol stubs
- generate_function_stub(sym: FunctionSymbol, ctx: StubContext) str[source]¶
Generate the
.pyistub line(s) for a module-level function.Handles both synchronous and asynchronous functions. The
asynckeyword is emitted whenis_asyncisTrueorinspect.iscoroutinefunction()confirms it at runtime.Parameter formatting follows the same inline / multi-line rules as
generate_method_stub(): inline when ≤ 2 parameters, multi-line otherwise. Raw AST annotation strings are used where they preserve type-alias information that runtime evaluation would destroy.- Parameters:
sym (FunctionSymbol) – The function symbol from the
SymbolTable.ctx (StubContext) – The current
StubContext.
- Returns:
str – One or more stub lines.
Examples
>>> from stubpy.context import StubContext >>> from stubpy.symbols import FunctionSymbol >>> from stubpy.ast_pass import FunctionInfo >>> fi = FunctionInfo(name="greet", lineno=1, raw_return_annotation="str") >>> def greet(name: str) -> str: return name >>> sym = FunctionSymbol("greet", 1, live_func=greet, ast_info=fi) >>> "def greet" in generate_function_stub(sym, StubContext()) True
- generate_variable_stub(sym: VariableSymbol, ctx: StubContext) str[source]¶
Generate a
name: Typeline for a module-level variable.Resolution order for the type string:
AST annotation string — the annotation as written in source.
Inferred type —
type(live_value).__name__when the variable has no annotation. AWARNINGdiagnostic is recorded because the inferred type may be imprecise.Skip — returns
""if neither source is available.
- Parameters:
sym (VariableSymbol) – The variable symbol from the
SymbolTable.ctx (StubContext) – The current
StubContext.
- Returns:
str – A single
name: Typeline, or""when the type cannot be determined.
Examples
>>> from stubpy.context import StubContext >>> from stubpy.symbols import VariableSymbol >>> sym = VariableSymbol("MAX", 1, annotation_str="int", live_value=100) >>> generate_variable_stub(sym, StubContext()) 'MAX: int' >>> sym2 = VariableSymbol("FLAG", 2, live_value=True, inferred_type_str="bool") >>> generate_variable_stub(sym2, StubContext()) 'FLAG: bool'
- generate_alias_stub(sym: AliasSymbol, ctx: StubContext) str[source]¶
Re-emit a TypeVar, TypeAlias, NewType, ParamSpec, or TypeVarTuple declaration.
The source text is taken verbatim from the AST pre-pass (
ast_info) so that constraints, bounds, and alias expansions are preserved exactly as written.Emission format per kind is controlled by
alias_style:TypeAlias declarations (annotated, implicit bare, or PEP 695):
"compatible"(default) —Name: TypeAlias = <rhs>Works on all Python 3.10+ versions."pep695"—type Name = <rhs>Python 3.12+ only."auto"— usespep695when running on Python 3.12+, otherwise falls back tocompatible.
TypeVar / ParamSpec / TypeVarTuple / NewType always emit as
Name = Kind(...)regardless ofalias_style.- Parameters:
sym (AliasSymbol) – The alias symbol from the
SymbolTable.ctx (StubContext) – The current
StubContext.
- Returns:
str – One declaration line, or
""when insufficient AST data is available.
Examples
>>> from stubpy.context import StubContext >>> from stubpy.symbols import AliasSymbol >>> from stubpy.ast_pass import TypeVarInfo >>> tv = TypeVarInfo(name="T", lineno=1, kind="TypeVar", source_str="TypeVar('T')") >>> sym = AliasSymbol("T", lineno=1, ast_info=tv) >>> generate_alias_stub(sym, StubContext()) "T = TypeVar('T')"
- generate_overload_group_stub(group: OverloadGroup, ctx: StubContext) str[source]¶
Emit one
@overload-decorated stub per variant in group.PEP 484 mandates that stub files contain one decorated stub per overload variant and that the concrete implementation (the non-
@overloaddef) is absent from the stub. This function produces the former; suppression of the latter is handled by the caller (generate_stub()).Each variant in
variantsmaps to aFunctionSymbolwhoseast_infoholds the raw parameter annotations from the AST pre-pass.- Parameters:
group (OverloadGroup) – The overload group from the
SymbolTable.ctx (StubContext) – The current
StubContext.
- Returns:
str – All overload stubs joined by
"\n\n", or""if the group has no variants.
Examples
>>> from stubpy.context import StubContext >>> from stubpy.symbols import OverloadGroup, FunctionSymbol >>> from stubpy.ast_pass import FunctionInfo >>> fi1 = FunctionInfo("parse", 1, raw_return_annotation="int", ... raw_arg_annotations={"x": "int"}) >>> fi2 = FunctionInfo("parse", 3, raw_return_annotation="str", ... raw_arg_annotations={"x": "str"}) >>> sym1 = FunctionSymbol("parse", 1, ast_info=fi1) >>> sym2 = FunctionSymbol("parse", 3, ast_info=fi2) >>> g = OverloadGroup("parse", 1, variants=[sym1, sym2]) >>> stub = generate_overload_group_stub(g, StubContext()) >>> stub.count("@overload") 2 >>> "@overload" in stub True
Parameter helpers
- insert_kw_separator(params_with_hints: list[tuple[Parameter, dict[str, Any]]]) list[tuple[Parameter, dict[str, Any]]][source]¶
Insert a bare
*sentinel before the first keyword-only parameter.Python requires a bare
*(or a*args) before any keyword-only parameters in a function signature. When no*argsis present but keyword-only parameters exist, this function inserts a sentinelinspect.Parameternamed_KW_SEP_NAMEthatgenerate_method_stub()emits as a literal*.If a
VAR_POSITIONAL(*args) parameter is already present no sentinel is needed and the list is returned unchanged.- Parameters:
params_with_hints (list of ParamWithHints) – The parameter list from
resolve_params().- Returns:
list of ParamWithHints – The same list with the sentinel inserted at the correct position, or the original list unchanged if no insertion is needed.
Examples
>>> import inspect >>> kw = inspect.Parameter("b", inspect.Parameter.KEYWORD_ONLY) >>> pos = inspect.Parameter("a", inspect.Parameter.POSITIONAL_OR_KEYWORD) >>> result = insert_kw_separator([(pos, {}), (kw, {})]) >>> result[1][0].name # sentinel is between a and b '__kw_sep__'
- insert_pos_separator(params_with_hints: list[tuple[Parameter, dict[str, Any]]]) list[tuple[Parameter, dict[str, Any]]][source]¶
Insert a bare
/sentinel after the last positional-only parameter.Python 3.8+ allows
def foo(a, b, /, c)to declare a and b as positional-only. Stub files must preserve this with a/separator (PEP 570). This function inserts a sentinelinspect.Parameternamed_POS_SEP_NAMEimmediately after the lastPOSITIONAL_ONLYparameter so thatgenerate_method_stub()andgenerate_function_stub()can emit it as a literal/.If no
POSITIONAL_ONLYparameters are present, the list is returned unchanged.- Parameters:
params_with_hints (list of ParamWithHints) – The parameter list, usually from
resolve_params()or frominspect.signature().- Returns:
list of ParamWithHints – The same list with the
/sentinel inserted after the last positional-only parameter, or the original list unchanged.
Examples
>>> import inspect >>> a = inspect.Parameter("a", inspect.Parameter.POSITIONAL_ONLY) >>> b = inspect.Parameter("b", inspect.Parameter.POSITIONAL_OR_KEYWORD) >>> result = insert_pos_separator([(a, {}), (b, {})]) >>> result[1][0].name # sentinel between a and b '__pos_sep__'
Formatting rules
Inline (≤ 2 non-self/cls parameters):
def area(self) -> float: ...
def scale(self, sx: float, sy: Optional[float] = None) -> Element: ...
Multi-line (> 2 non-self/cls parameters), each param on its own line with a trailing comma:
def __init__(
self,
width: float,
height: float,
depth: float = 1.0,
) -> None: ...
Trailing commas make diffs cleaner — adding or removing a parameter changes exactly one line.
Positional-only separator
When a function declares positional-only parameters (PEP 570), a bare /
is inserted after the last positional-only parameter:
def move(x: float, y: float, /, z: float = 0.0) -> None: ...
insert_pos_separator() manages this, mirroring how
insert_kw_separator() manages the * separator.
TypeVar and Generic stubs
TypeVar, TypeAlias, NewType, ParamSpec, and TypeVarTuple declarations are
re-emitted verbatim from the AST pre-pass via generate_alias_stub(),
preserving bounds, constraints, and alias right-hand sides.
Generic base classes use __orig_bases__ (PEP 560) instead of __bases__
so subscripts like Generic[T] and Generic[K, V] are preserved exactly.
@overload stubs
When a module declares @overload-decorated functions, each variant
gets its own stub and the concrete implementation is suppressed, per PEP 484:
@overload
def parse(x: int) -> int: ...
@overload
def parse(x: str) -> str: ...
Public dunders
Only the methods listed in the internal _PUBLIC_DUNDERS set are
included in stubs. Internal Python machinery names (__dict__,
__weakref__, __class__, etc.) are omitted.