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 @dataclass decorator and synthesises an __init__ stub from __dataclass_fields__, correctly handling default_factory, init=False, and ClassVar fields.

  • Abstract methods — emits @abstractmethod for any callable whose __isabstractmethod__ attribute is set.

  • Generic base classes — reads __orig_bases__ (PEP 560) to emit class 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 the alias_style configuration option (compatible Name: TypeAlias = ..., or Python 3.12+ type Name = ... form).

  • generate_overload_group_stub() emits one @overload stub per variant, suppressing the concrete implementation per PEP 484.

Parameter separators

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 .pyi block for cls.

Dispatches to specialised generators for NamedTuple subclasses and dataclasses before falling back to standard reflection. Abstract methods receive @abstractmethod; async methods receive async def.

Parameters:
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 .pyi stub line(s) for a single method on cls.

Dispatches on the descriptor type in cls.__dict__:

  • property@property with optional @name.setter.

  • classmethod@classmethod with cls first.

  • staticmethod@staticmethod with no implicit first parameter.

  • Regular method — self as first parameter.

Emits async def when the underlying callable is a coroutine or async generator function, and @abstractmethod when __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 in cls.__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_DUNDERS are 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 .pyi stub line(s) for a module-level function.

Handles both synchronous and asynchronous functions. The async keyword is emitted when is_async is True or inspect.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:
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: Type line for a module-level variable.

Resolution order for the type string:

  1. AST annotation string — the annotation as written in source.

  2. Inferred typetype(live_value).__name__ when the variable has no annotation. A WARNING diagnostic is recorded because the inferred type may be imprecise.

  3. Skip — returns "" if neither source is available.

Parameters:
Returns:

str – A single name: Type line, 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" — uses pep695 when running on Python 3.12+, otherwise falls back to compatible.

TypeVar / ParamSpec / TypeVarTuple / NewType always emit as Name = Kind(...) regardless of alias_style.

Parameters:
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-@overload def) is absent from the stub. This function produces the former; suppression of the latter is handled by the caller (generate_stub()).

Each variant in variants maps to a FunctionSymbol whose ast_info holds the raw parameter annotations from the AST pre-pass.

Parameters:
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 *args is present but keyword-only parameters exist, this function inserts a sentinel inspect.Parameter named _KW_SEP_NAME that generate_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 sentinel inspect.Parameter named _POS_SEP_NAME immediately after the last POSITIONAL_ONLY parameter so that generate_method_stub() and generate_function_stub() can emit it as a literal /.

If no POSITIONAL_ONLY parameters are present, the list is returned unchanged.

Parameters:

params_with_hints (list of ParamWithHints) – The parameter list, usually from resolve_params() or from inspect.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.