Skip to content

Add Python stub module generation for IDE autocomplete#8

Merged
santolucito merged 11 commits into
Barnard-PL-Labs:mainfrom
chrishyoroklee:main
Mar 9, 2026
Merged

Add Python stub module generation for IDE autocomplete#8
santolucito merged 11 commits into
Barnard-PL-Labs:mainfrom
chrishyoroklee:main

Conversation

@chrishyoroklee
Copy link
Copy Markdown
Collaborator

@chrishyoroklee chrishyoroklee commented Feb 28, 2026

Motivation

MaxPyLang lets users build Max patches in Python, but there's no way for IDE autocomplete or LLM agents (like Claude, Cursor, Copilot) to know what Max objects exist. Users have to memorize object names and manually write MaxObject("cycle~") every time. The generated stub modules solve both problems: IDEs can autocomplete object names, and LLM agents can read the docstrings to understand what each object does, what arguments it takes, and how to use it.

Additionally, when a user writes MaxObject('multislir') (typo for multislider), the code silently creates a broken object with 0 inlets/outlets and only prints to stdout — easy to miss. Agents and IDEs have no way to detect this.

Example

Before:

patch.place(MaxObject("cycle~"))  # user must know the exact Max name
MaxObject("multislir")            # silent failure, only a print() to stdout

After:

from maxpylang.objects import cycle_tilde, dac_tilde, metro, toggle

patch = MaxPatch()
[t] = patch.place(toggle)
[m] = patch.place(metro)
[c] = patch.place(cycle_tilde)
[d] = patch.place(dac_tilde)
patch.connect(
    (t.outs[0], m.ins[0]),
    (m.outs[0], c.ins[0]),
    (c.outs[0], d.ins[0]),
)
patch.save("my_patch.maxpat")

MaxObject("multislir")  # UnknownObjectWarning with file/line info

An LLM agent can read the docstring for cycle_tilde and learn it's a sinusoidal oscillator, what inlets/outlets it has, what attributes are available, and what related objects exist — all without needing access to Max documentation.

Summary

  • Extracts XML documentation metadata (digest, description, inlets, outlets, methods, seealso) from Max reference files and stores it in JSON alongside existing object info
  • Generates Python stub modules (objects/max.py, objects/msp.py, objects/jit.py) containing pre-instantiated MaxObject variables with rich docstrings, so users get IDE autocomplete and can write patch.place(cycle_tilde) instead of patch.place(MaxObject("cycle~"))
  • Adds sanitize_py_name() to convert Max names to valid Python identifiers (cycle~cycle_tilde, jit.gl.renderjit_gl_render, 2d.wave~_2d_wave_tilde, ifif_, dictdict_)
  • Replaces silent print() error messages with warnings.warn() using UnknownObjectWarning for unknown objects and bad arguments — IDEs highlight the issue, agents can detect it, and users can suppress with warnings.filterwarnings
  • Embeds full API reference, available objects table, and usage patterns in __init__.py module docstring so agents can access everything via help(maxpylang) after pip install

How it works

  1. User runs import_objs("vanilla") (one-time setup, Max must be open)
  2. JSON object info is saved as before
  3. New: generate_stubs() reads the JSON and writes maxpylang/objects/{package}.py with one variable per object
  4. User imports stubs: from maxpylang.objects import cycle_tilde, metro, dac_tilde
  5. Each stub is a real MaxObject instance — cycle_tilde.name returns "cycle~"

Generated stub format

"""
cycle~ - Sinusoidal oscillator

Args:
  frequency (number, optional)

Inlets:
  0 (signal/float): Frequency
  ...

Attributes: frequency, phase, buffer, ...

See also: saw~, tri~, rect~
"""
cycle_tilde = MaxObject('cycle~')

Warnings for unknown objects

Before: MaxObject('multislir') prints "ObjectError: multislir : creation : object unknown" to stdout — easy to miss.

After: raises UnknownObjectWarning with file/line pointing to the user's code:

main.py:4: UnknownObjectWarning: Unknown Max object: 'multislir'
  bad = mp.MaxObject("multislir")
  • Works for unknown objects, missing required args, and bad arg types
  • Suppressible: warnings.filterwarnings("ignore", category=UnknownObjectWarning)
  • Programmatic check: obj.notknown()
  • Object still created (0 inlets/outlets) — no crash

Agent discoverability

The maxpylang/__init__.py module docstring now contains a complete API reference including:

  • Core API (MaxPatch, MaxObject, connections)
  • Stub naming conventions
  • Common objects by category (audio, control, routing, MIDI, UI, etc.)
  • Usage patterns and layout rules
  • Warning behavior

This means any LLM agent can read __init__.py and build correct patches without needing access to the repo's CLAUDE.md or Max documentation.

Files changed

  • maxpylang/exceptions.py — New file. UnknownObjectWarning(UserWarning) class
  • maxpylang/tools/objfuncs/reffile.py — Replaced print() with warnings.warn() for unknown objects
  • maxpylang/tools/objfuncs/args.py — Replaced print() with warnings.warn() for bad arguments
  • maxpylang/tools/patchfuncs/placing.py — Removed duplicate print (already warned by constructor)
  • maxpylang/objects/__init__.py — Suppresses warnings during stub import; re-exports from generated sub-modules
  • maxpylang/__init__.py — Full API reference docstring; optional from . import objects
  • maxpylang/importobjs.py — Added sanitize_py_name(), _build_docstring(), generate_stubs(); wired into import_objs()
  • maxpylang/CLAUDE.md — Added available objects table and warning documentation

Edge cases handled

  • ~_tilde, ._, -_
  • Leading digit → prepend _
  • Python keywords/builtins → append _
  • Triple-quote injection in docstrings → escaped
  • Warnings suppressed during stub import (stubs intentionally omit required args)
  • objects/__init__.py dynamically regenerated based on existing stub files (supports third-party packages)

Automated tests passed (43 tests)

  • sanitize_py_name: all transform rules (tilde, dot, hyphen, leading digit, keywords, builtins, combined, empty string)
  • _build_docstring: digest-only, full docstring, empty doc, no args, COMMON filtering, triple-quote safety
  • generate_stubs: files created, valid Python (ast.parse), all/_NAMES consistency, init.py regenerated, custom packages
  • Backward compatibility: MaxObject("cycle~"), MaxPatch create/place/connect/save, imports with and without stubs
  • Stub E2E: import, isinstance check, name preserved, place/connect in patch, string equivalence, no stdout noise
  • Examples: hello_world produces valid .maxpat JSON

Abstraction mode

When users create Max abstractions (custom .maxpat sub-patches), MaxObject("my_synth") only works if the .maxpat file exists in the current working directory. Otherwise it triggers UnknownObjectWarning and creates a broken object with 0 inlets/outlets. This blocks common workflows: building an abstraction and its parent patch in the same script, referencing abstractions in Max's search path or packages folder, or prototyping patch structure before the abstractions are built.
abstraction=True lets users declare an abstraction with known I/O counts, skipping the file look up entirely:

# Before: fails if my_synth.maxpat isn't in cwd
synth = MaxObject("my_synth")  # UnknownObjectWarning, 0 ins/outs, broken

# After: declare the interface, no file needed
synth = MaxObject("my_synth", abstraction=True, inlets=2, outlets=2)
placed = patch.place(synth)[0]
patch.connect([placed.outs[0], dac.ins[0]])  # works

Under the hood, abstraction=True short-circuits build_from_specs before get_ref() is calledsets _ref_file = "abstraction", builds the object dict with declared inlet/outlet counts via create_declared_abstraction(), and processes args and @-attributes normally. notknown() returns False.
Existing behavior is unchanged when the flag is not set.

Files changed:

- maxpylang/maxobject.pyAdded abstraction, inlets, outlets params to __init__, passed through to build_from_specs
- maxpylang/tools/objfuncs/instantiation.pyEarly return in build_from_specs when abstraction=True, before get_ref() lookup
- maxpylang/tools/objfuncs/specialobjs.pyAdded create_declared_abstraction() method that builds abstraction dict with declared I/O counts, no file needed
- maxpylang/__init__.pyAdded abstraction section to module docstring

@chrishyoroklee chrishyoroklee marked this pull request as ready for review February 28, 2026 22:49
@santolucito
Copy link
Copy Markdown
Member

look good, merging, but I am also opening an issue to update the docs accordingly (regarding abstraction=true)

@santolucito
Copy link
Copy Markdown
Member

wait i think we need to also bump the version here

version = "0.1.1"

@santolucito santolucito merged commit c12f396 into Barnard-PL-Labs:main Mar 9, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants