Skip to content

Enriching stubs from your C header

The stubs above are the name-based fallback. The .pyi docstrings are derived directly from the Doxygen comments in your native/inc/<pkg>/<obj>/<obj>_core.h, so what you write in C surfaces in your editor's tooltips, in the rendered Python API docs, and — for methods, properties, and free functions — as runnable doctests.

Two header comments feed the class stub: <obj>_create() supplies the class docstring, and each method/property is documented from its own declaration.

The gain snippets below are illustrative. To watch the whole arc run in a project you can build yourself, use the accumulator example — it enriches two objects' headers and its end-to-end test asserts the docstrings reached the .pyi, then executes the header-authored doctests against the built .so:

just-makeit example accumulator

The class docstring — from create()

Give <pkg>_gain_create a real @brief:

/**
 * @brief Construct a scalar gain stage.
 * @param gain  Linear gain applied to each sample (default: 1.0).
 * @return Heap-allocated <pkg>_gain_state_t, or NULL on allocation failure.
 */
<pkg>_gain_state_t *<pkg>_gain_create(float gain);

Run just-makeit apply (or any mutating command) and the @brief becomes the class summary. The Parameters section documents the constructor — for an object built from [[<obj>.init_params]] each @param description flows through; a plain --state object documents its state fields generically. The Examples block is synthesized by jm (a construction call plus getter read-backs and a reset round-trip) and runs as a doctest out of the box:

@final
class Gain:
    """Construct a scalar gain stage.

    Parameters
    ----------
    gain : float, default 1.0
        gain state variable.

    Examples
    --------
    Create with defaults:

    >>> from my_dsp import Gain
    >>> obj = Gain(gain=1.0)
    >>> obj.get_gain()
    1.0

    Reset restores defaults:

    >>> obj.set_gain(0.0)
    >>> obj.reset()
    >>> obj.get_gain()
    1.0

    """

The class summary is the @brief line only: an extended-description paragraph and @return on create() are not rendered into the class docstring, and the Examples block is always the synthesized construction demo — never a @code snippet. To author your own runnable example, put it on a method.

Method docstrings — where @code becomes a doctest

A method added with just-makeit method derives its entire docstring from the header, and a @code block becomes a runnable Examples doctest:

/**
 * @brief Scale one sample by the gain and return it.
 * @param x  Input sample.
 * @return The scaled sample.
 * @code
 * >>> from my_dsp import Gain
 * >>> Gain(2.0).scale(1.5)
 * 3.0
 * @endcode
 */
float <pkg>_gain_scale(const <pkg>_gain_state_t *state, float x);

Regenerate and gain.pyi carries:

    def scale(self, x: float, /) -> float:
        """Scale one sample by the gain and return it.

        Parameters
        ----------
        x : float
            Input sample.

        Returns
        -------
        float
            The scaled sample.

        Examples
        --------
        >>> from my_dsp import Gain
        >>> Gain(2.0).scale(1.5)
        3.0

        """

That doctest runs against the built extension — pass -v to watch each >>> line execute (if the C returned 3.1, the scale step would fail instead of ok):

python -m doctest -v src/my_dsp/gain.pyi Trying: from my_dsp import Gain Expecting nothing ok Trying: Gain(2.0).scale(1.5) Expecting: 3.0 ok ... Test passed.

A just-makeit property getter's @brief becomes the property docstring the same way. The built-in step()/steps() keep their standard docstrings — a @param on step refines the argument description, but they are not the place for a @brief or @code.

Which Doxygen tags jm reads

Tag On create() On a method / property / free function
@brief Class summary Docstring summary
@param <name> <doc> Parameters (init-params) Parameters entry
@return / @returns — (not rendered) Returns entry
@code … @endcode — (not rendered) Runnable Examples doctest
@note, @remark, @pre, @post, @invariant — (not rendered) Notes
@warning — (not rendered) Warnings
@see / @sa — (not rendered) See Also
@throws / @exception — (not rendered) Raises
@retval — (not rendered) rows joined to Returns

Inline word-references — @p name, @c name, @a/@e/@b name, and @ref name — are reduced to the bare word so the prose reads cleanly in Python. C-only metadata (@todo, @bug, @since, @version, @author, @file, …) is dropped.

Three things worth knowing when your docs don't seem to "take":

  • Your @brief must say more than the function name. A brief that merely restates the name (jm's own scaffold brief, Create a gain instance., or one that only names the function) is treated as empty, and jm keeps the name-based fallback until you write something real.
  • @code examples are runnable, not run for you. make test and the jm ci workflow do not execute them; run them against the built extension with python -m pytest --doctest-glob='*.pyi' src/ (or, once installed, python -m doctest -v src/<pkg>/<obj>.pyi), and add that line to your CI if a >>> expecting 3.0 should fail the build when the C returns 3.1. Write examples with deterministic, printable output (whole-number results avoid floating-point noise).
  • Stubs are glue — regenerate, don't hand-edit. After editing a header, run just-makeit apply and commit the updated .pyi alongside it; just-makeit status --check fails the manifest-drift gate if they diverge.

For the full parsing pipeline and the CI wiring, see Docstring derivation (C → .pyi).