When a user changes client_root in pyrpc.json, the types file needs to move. The old location has a generatedindex.ts file. The new location will need one too. What should pyrpc do? Delete the old file? Move it? Keep both? Ask the user? The answer depends on three questions:
- Does the old file still exist?
- Does the new location already have a file?
- If both exist, are they the same?
The answer to each question determines one of three migration strategies.
Case 1: Old missing, new empty
This is the simplest case. If the old types file doesn’t exist (maybe the user never ran pyrpc dev before, or they already cleaned up), there’s nothing to migrate. pyrpc generates fresh types at the new location and moves on.
if not os.path.isfile(old_path):
return # nothing to migrate, just generate freshThis early return is important — it means migration only fires when there’s actually something to migrate. No unnecessary prompts, no spurious warnings.
Case 2: Old exists, new location is empty
This happens when a user changes client_root from one directory to another. The old types file is still sitting at the old path. The new path doesn’t have a types file yet — the directory might not even exist.
pyrpc prompts the user:
? Types location changed. Old: project/old-frontend/node_modules/@pyrpc/types/src/index.ts New: project/new-frontend/node_modules/@pyrpc/types/src/index.ts Move generated types? (Y/n)
If the user says yes, pyrpc creates the directory structure at the new location and moves the file using shutil.move. This preserves the file exactly — same content, same permissions, same modification time. The old directory is left in place (it may contain other files likepackage.json).
if ans:
os.makedirs(os.path.dirname(new_path), exist_ok=True)
shutil.move(old_path, new_path)If the user says no, we leave the old file in place. The nextregenerate() call (triggered by the dev server starting up) will write fresh types to the new location. The old file becomes orphaned but doesn’t cause any harm.
Case 3: Both locations have a file
This is the most interesting case. Both the old and new locations already have a generated index.ts. The question is: are they the same?
pyrpc uses SHA256 to compare them:
if _hash_file(old_path) == _hash_file(new_path):
# Identical content - remove old, keep new
os.remove(old_path)
console.print("Generated types already exist at new location.")
console.print("Removed old copy.")Sub-case 3a: Identical content. Both files are the same. This means the user already regenerated at the new location (perhaps by runningpyrpc dev after changing client_root, then changing it back). pyrpc auto-cleans the old copy without prompting. The user doesn’t need to make a decision about two identical files.
Sub-case 3b: Different content. Both files exist and they’re different. This means the type definitions diverged — perhaps the server was updated between the two generations, or a different version of pyrpc was used. pyrpc can’t auto-resolve this:
? Generated types found in both locations.
Recommended: Regenerate from the current server.
What would you like to do?
> Regenerate at new location and remove old location
Keep both locations
Cancel“Regenerate at new location and remove old location” is the recommended default because the server-side default_router is the authoritative source of truth. The cached types at either location may be stale. By regenerating from the server, we guarantee the new location has the correct, up-to-date types.
“Keep both locations” is for users who aren’t sure yet. Both files survive; neither is deleted. The dev server regenerates at the new location on startup anyway.
“Cancel” aborts the operation. The dev server still starts, but no migration happens. The user can manually resolve the situation later.
Why not always auto-migrate?
The conservative approach (prompt in case 2 and case 3b) is intentional. Types files don’t contain secrets, but they do represent work — and deleting or moving them without consent would be a violation of the “do what I say, not what I might have said” principle. The only auto-migration is when we can prove the files are identical (case 3a) — at which point deleting the duplicate is always safe.
Case 1 (old missing) doesn’t need a prompt because there’s nothing to ask about. The file doesn’t exist. pyrpc generates fresh types and moves on.
Edge case: KeyboardInterrupt during migration
Both questionary.confirm().ask() andquestionary.select().ask() return None when the user presses Ctrl+C or Escape. pyrpc checks for this and treats it as “cancel silently”:
ans = questionary.confirm(...).ask()
if ans is None:
return # user cancelled - do nothingThis means Ctrl+C during a migration prompt exits cleanly. The dev server continues starting with the old config. No half-migrated state. No orphaned files.
The full flow
Here’s the complete decision tree, from the moment pyrpc devdetects a client_root change:
1. Compare old_client_root vs new_client_root
↓ (different)
2. Check if old types file exists
↓ (no) → generate fresh, done
↓ (yes)
3. Check if new types file exists
↓ (no) → prompt: move? → yes: move file → no: generate fresh
↓ (yes)
4. Compare SHA256 of both files
↓ (same) → delete old, keep new → done
↓ (different) → prompt: regenerate/keep/cancel
→ regenerate: delete old, generate fresh at new
→ keep: do nothing
→ cancel: do nothingEvery path ends with a consistent state. No partial writes, no data loss, and — in the three most common scenarios (first-time setup, identical files, old file missing) — no prompt at all. The user only interacts when a human judgment call is genuinely needed.

pyRPC