Programming

1. rename app

#!/usr/bin/env python3
"""
Rename the `portfolio` app files to `loanbook` (or any other name).

Run it once, from your project root, AFTER copying the files into the app
directory and BEFORE the first `makemigrations`.

    python rename_app.py loanbook

What it changes:
  * absolute imports        from portfolio.bridge  ->  from loanbook.bridge
  * template lookups        "portfolio/base.html"  ->  "loanbook/base.html"
  * URL namespaces          {% url 'portfolio:x' %} -> {% url 'loanbook:x' %}
  * static paths            'portfolio/portfolio.css' -> 'loanbook/loanbook.css'
  * apps.py `name`, urls.py `app_name`, the Plotly PNG download filename
  * directory names         templates/portfolio/, static/portfolio/

It does NOT touch your settings.py or root urls.py — those are Step 3 and 4,
and they are one line each.
"""
from __future__ import annotations

import re
import shutil
import sys
from pathlib import Path

OLD = "portfolio"
TEXT_SUFFIXES = {".py", ".html", ".css", ".txt", ".md", ".json", ".cfg"}
SKIP_DIRS = {"__pycache__", ".git", "node_modules", ".venv", "venv", "migrations"}

# Human-facing strings that should keep the word "portfolio" — renaming these
# would produce nonsense like "Credit loanbook analytics".
KEEP_VERBATIM = (
    "Credit portfolio analytics",
    "credit portfolio",
    "loan portfolio",
)


def rewrite(text: str, new: str) -> tuple[str, int]:
    placeholders = {}
    for i, phrase in enumerate(KEEP_VERBATIM):
        token = f"\x00KEEP{i}\x00"
        if phrase in text:
            placeholders[token] = phrase
            text = text.replace(phrase, token)

    text, n = re.subn(rf"\b{OLD}\b", new, text)

    for token, phrase in placeholders.items():
        text = text.replace(token, phrase)
    return text, n


def main(new: str, root: Path) -> int:
    app_dir = root / new
    if not app_dir.is_dir():
        print(f"! no directory {app_dir} — create the app first, then copy the files in")
        return 1

    # 1. Rename the nested template/static directories.
    for parent in ("templates", "static"):
        old_dir = app_dir / parent / OLD
        new_dir = app_dir / parent / new
        if old_dir.is_dir():
            if new_dir.exists():
                print(f"! {new_dir} already exists — merge it yourself and re-run")
                return 1
            old_dir.rename(new_dir)
            print(f"  dir   {parent}/{OLD}/ -> {parent}/{new}/")

    # 2. Rename the stylesheet so the file matches the app.
    css_old = app_dir / "static" / new / f"{OLD}.css"
    if css_old.is_file():
        css_old.rename(app_dir / "static" / new / f"{new}.css")
        print(f"  file  static/{new}/{OLD}.css -> {new}.css")

    # 3. Rewrite file contents.
    total_files = total_hits = 0
    for path in sorted(app_dir.rglob("*")):
        if not path.is_file() or path.suffix not in TEXT_SUFFIXES:
            continue
        if any(part in SKIP_DIRS for part in path.parts):
            continue
        original = path.read_text(encoding="utf-8")
        updated, n = rewrite(original, new)
        if n:
            path.write_text(updated, encoding="utf-8")
            total_files += 1
            total_hits += n
            print(f"  edit  {path.relative_to(root)}  ({n})")

    # 4. Drop any pre-baked migration; it carries the old app label.
    stale = [p for p in (app_dir / "migrations").glob("0*.py")]
    for p in stale:
        p.unlink()
        print(f"  del   {p.relative_to(root)} (regenerate with makemigrations)")

    # 5. Clear caches so stale .pyc files don't shadow the rename.
    for cache in app_dir.rglob("__pycache__"):
        shutil.rmtree(cache, ignore_errors=True)

    print(f"\ndone: {total_hits} replacements across {total_files} files")
    print("\nnext:")
    print(f"  1. add \"{new}\" to INSTALLED_APPS")
    print(f"  2. add path(\"{new}/\", include(\"{new}.urls\")) to your root urls.py")
    print(f"  3. python manage.py makemigrations {new} && python manage.py migrate")
    return 0


if __name__ == "__main__":
    name = sys.argv[1] if len(sys.argv) > 1 else "loanbook"
    if not re.fullmatch(r"[a-z][a-z0-9_]*", name):
        print("app name must be a valid lowercase Python identifier")
        raise SystemExit(1)
    raise SystemExit(main(name, Path.cwd()))
Helpful? Dislike 0 Log in to react