Every time pyrpc dev regenerates types (on startup, on each procedure edit, on every watched module reload) it also re-runs the wiring. The tsconfig is reconfigured, and the bundler is reconfigured. Doing this on every regen only works because both operations are written to be idempotent: the hundredth run must leave the files byte-identical to the first.
The per-client loop
for client_dir in client_dirs:
output_path = os.path.abspath(os.path.join(client_dir, "__pyrpc.ts"))
n = _run_codegen(module, output_path, reload=reload)
try:
configure_tsconfig(client_dir)
except Exception as e:
console.print(f"[yellow]⚠ Could not configure tsconfig in {client_dir}: {e}[/yellow]")
try:
if not configure_bundler(client_dir):
console.print(
f"[yellow]⚠ Could not auto-configure bundler in {client_dir}, "
"add a bundler alias '@pyrpc/types' → './__pyrpc.ts' ..."
)
except Exception as e:
console.print(f"[yellow]⚠ Could not configure bundler in {client_dir}: {e}[/yellow]")Generation and wiring happen in one loop, per client, in a fixed order: write the file, point the compiler at it, point the bundler at it. The two config steps have independent failure handling, a tsconfig problem and a bundler problem surface as separate warnings so a developer can fix them separately.
Tsconfig: read the current value first
configure_tsconfig never blindly writes. It probes the existing value of the compilerOptions.paths["@pyrpc/types"] key using a sentinel edit:
existing = _get_existing_value(content, ["compilerOptions", "paths", "@pyrpc/types"])
if existing is not None:
clean_val = normalize(existing) # strip comments + whitespace
if clean_val == '["./__pyrpc.ts"]':
return True # already correct → no write
raise RuntimeError(
"@pyrpc/types is already configured to point elsewhere"
)If the alias already matches the expected value, the function returns without writing, preserving the file's mtime and avoiding a churn loop. If the alias points somewhere else (the developer deliberately overrode it), it raises instead of fighting the override.
Reading jsonc without a JSON parser
The tsconfig is JSON with comments and trailing commas, jsonc, not JSON. Rather than parsing it, _get_existing_value uses jsonc_edit.modify to ask where the key would sit and reads the raw source at that span. It is a read-through-edit: probe with a sentinel value, intercept the returned span, extract the original text. The write path then uses apply_edits for a surgical insertion that preserves comments and formatting.
Idempotency is a system property
Three separate guards compose into the guarantee:
- Tsconfig: the existing-value probe returns early on match.
- Bundler:
_already_aliasedskips injection when the markers exist. - Both: writes happen only when the new content differs from the current file.
Because every regen passes through all three, the config files converge to a fixed point, and stay there. That is what makes running the wiring on every codegen step safe, which in turn is what makes the dev loop feel magical: you never configure, you only edit Python.

pyRPC