import sys
import subprocess
import shlex
import os
import argparse
import shutil
import logging
import coloredlogs


class _CombinedFormatter(
    argparse.ArgumentDefaultsHelpFormatter, argparse.RawDescriptionHelpFormatter
):
    pass


logger = logging.getLogger(__name__)
coloredlogs.install(
    fmt="%(asctime)s,%(msecs)03d %(levelname)s - %(message)s", datefmt="%H:%M:%S"
)


def bootstrap_components(components_source, concurrency, install_type):

    is_windows = sys.platform == "win32"

    source_glob = (
        components_source
        if components_source != "all"
        else "dash-core-components|dash-html-components|dash-table"
    )

    cmdstr = f"npx lerna exec --concurrency {concurrency} --scope *@({source_glob})* -- npm {install_type}"
    cmd = shlex.split(cmdstr, posix=not is_windows)
    print(cmdstr)

    with subprocess.Popen(
        cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=is_windows
    ) as proc:
        out, err = proc.communicate()
        status = proc.poll()

    if err:
        print(("🛑 " if status else "") + err.decode(), file=sys.stderr)

    if status or not out:
        print(
            "🚨 Failed installing npm dependencies for component packages: {source_glob} (status={status}) 🚨",
            file=sys.stderr,
        )
        sys.exit(1)
    else:
        print(
            f"🟢 Finished installing npm dependencies for component packages: {source_glob} 🟢",
            file=sys.stderr,
        )


def build_components(components_source, concurrency):

    is_windows = sys.platform == "win32"

    source_glob = (
        components_source
        if components_source != "all"
        else "dash-core-components|dash-html-components|dash-table"
    )

    cmdstr = f"npx lerna exec --concurrency {concurrency} --scope *@({source_glob})* -- npm run build"
    cmd = shlex.split(cmdstr, posix=not is_windows)
    print(cmdstr)

    with subprocess.Popen(
        cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=is_windows
    ) as proc:
        out, err = proc.communicate()
        status = proc.poll()

    if err:
        print(("🛑 " if status else "") + err.decode(), file=sys.stderr)

    if status or not out:
        print(
            f"🚨 Finished updating component packages: {source_glob} (status={status}) 🚨",
            file=sys.stderr,
        )
        sys.exit(1)

    for package in source_glob.split("|"):
        build_directory = os.path.join(
            "components", package, package.replace("-", "_").rstrip("/\\")
        )

        dest_dir = (
            "dcc"
            if package == "dash-core-components"
            else "html"
            if package == "dash-html-components"
            else "dash_table"
        )

        dest_path = os.path.join("dash", dest_dir)

        if not os.path.exists(dest_path):
            try:
                os.makedirs(dest_path)
            except OSError:
                logger.exception("🚨 Having issues manipulating %s", dest_path)
                sys.exit(1)

        if not os.path.exists(build_directory):
            print(
                "🚨 Could not locate build artifacts."
                + " Check that the npm build process completed"
                + f" successfully for package: {package} 🚨"
            )
            sys.exit(1)
        else:
            print(f"🚚 Moving build artifacts from {build_directory} to Dash 🚚")
            shutil.rmtree(dest_path)
            shutil.copytree(build_directory, dest_path)
            with open(os.path.join(dest_path, ".gitkeep"), "w"):
                pass
            print(f"🟢 Finished moving build artifacts from {build_directory} to Dash 🟢")


def cli():
    parser = argparse.ArgumentParser(
        prog="dash-update-components",
        formatter_class=_CombinedFormatter,
        description="Update the specified subcomponent libraries within Dash"
        " by copying over build artifacts, dependencies, and dependency metadata.",
    )
    parser.add_argument(
        "components_source",
        help="A glob string that matches the Dash component libraries to be updated"
        " (eg.'dash-table' // 'dash-core-components|dash-html-components' // 'all')."
        " The default argument is 'all'.",
        default="all",
    )
    parser.add_argument(
        "--concurrency",
        type=int,
        default=3,
        help="Maximum concurrent steps, up to 3 (ie all components in parallel)",
    )
    parser.add_argument(
        "--ci",
        help="For clean-install use '--ci True'",
        default="False",
    )

    args = parser.parse_args()

    bootstrap_components(
        args.components_source, args.concurrency, "ci" if args.ci == "True" else "i"
    )
    build_components(args.components_source, args.concurrency)


if __name__ == "__main__":
    cli()

dash/development/update_components.py
import sys
import subprocess
import shlex
import os
import argparse
import shutil
import logging
import coloredlogs


class _CombinedFormatter(
    argparse.ArgumentDefaultsHelpFormatter, argparse.RawDescriptionHelpFormatter
):
    pass


logger = logging.getLogger(__name__)
coloredlogs.install(
    fmt="%(asctime)s,%(msecs)03d %(levelname)s - %(message)s", datefmt="%H:%M:%S"
)


def bootstrap_components(components_source, concurrency, install_type):

    is_windows = sys.platform == "win32"

    source_glob = (
        components_source
        if components_source != "all"
        else "dash-core-components|dash-html-components|dash-table"
    )

    cmdstr = f"npx lerna exec --concurrency {concurrency} --scope *@({source_glob})* -- npm {install_type}"
    cmd = shlex.split(cmdstr, posix=not is_windows)
    print(cmdstr)

    with subprocess.Popen(
        cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=is_windows
    ) as proc:
        out, err = proc.communicate()
        status = proc.poll()

    if err:
        print(("🛑 " if status else "") + err.decode(), file=sys.stderr)

    if status or not out:
        print(
            f"🚨 Failed installing npm dependencies for component packages: {source_glob} (status={status}) 🚨",
            file=sys.stderr,
        )
        sys.exit(1)
    else:
        print(
            f"🟢 Finished installing npm dependencies for component packages: {source_glob} 🟢",
            file=sys.stderr,
        )


def build_components(components_source, concurrency):

    is_windows = sys.platform == "win32"

    source_glob = (
        components_source
        if components_source != "all"
        else "dash-core-components|dash-html-components|dash-table"
    )

    cmdstr = f"npx lerna exec --concurrency {concurrency} --scope *@({source_glob})* -- npm run build"
    cmd = shlex.split(cmdstr, posix=not is_windows)
    print(cmdstr)

    with subprocess.Popen(
        cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=is_windows
    ) as proc:
        out, err = proc.communicate()
        status = proc.poll()

    if err:
        print(("🛑 " if status else "") + err.decode(), file=sys.stderr)

    if status or not out:
        print(
            f"🚨 Finished updating component packages: {source_glob} (status={status}) 🚨",
            file=sys.stderr,
        )
        sys.exit(1)

    for package in source_glob.split("|"):
        build_directory = os.path.join(
            "components", package, package.replace("-", "_").rstrip("/\\")
        )

        dest_dir = (
            "dcc"
            if package == "dash-core-components"
            else "html"
            if package == "dash-html-components"
            else "dash_table"
        )

        dest_path = os.path.join("dash", dest_dir)

        if not os.path.exists(dest_path):
            try:
                os.makedirs(dest_path)
            except OSError:
                logger.exception("🚨 Having issues manipulating %s", dest_path)
                sys.exit(1)

        if not os.path.exists(build_directory):
            print(
                "🚨 Could not locate build artifacts."
                + " Check that the npm build process completed"
                + f" successfully for package: {package} 🚨"
            )
            sys.exit(1)
        else:
            print(f"🚚 Moving build artifacts from {build_directory} to Dash 🚚")
            shutil.rmtree(dest_path)
            shutil.copytree(build_directory, dest_path)
            with open(os.path.join(dest_path, ".gitkeep"), "w"):
                pass
            print(f"🟢 Finished moving build artifacts from {build_directory} to Dash 🟢")


def cli():
    parser = argparse.ArgumentParser(
        prog="dash-update-components",
        formatter_class=_CombinedFormatter,
        description="Update the specified subcomponent libraries within Dash"
        " by copying over build artifacts, dependencies, and dependency metadata.",
    )
    parser.add_argument(
        "components_source",
        help="A glob string that matches the Dash component libraries to be updated"
        " (eg.'dash-table' // 'dash-core-components|dash-html-components' // 'all')."
        " The default argument is 'all'.",
        default="all",
    )
    parser.add_argument(
        "--concurrency",
        type=int,
        default=3,
        help="Maximum concurrent steps, up to 3 (ie all components in parallel)",
    )
    parser.add_argument(
        "--ci",
        help="For clean-install use '--ci True'",
        default="False",
    )

    args = parser.parse_args()

    bootstrap_components(
        args.components_source, args.concurrency, "ci" if args.ci == "True" else "i"
    )
    build_components(args.components_source, args.concurrency)


if __name__ == "__main__":
    cli()

dash/development/update_components.py
diff --git a/dash/development/update_components.py b/dash/development/update_components.py
index b16b438a76..b67d0a639e 100644
--- a/dash/development/update_components.py
+++ b/dash/development/update_components.py
@@ -45,7 +45,7 @@ def bootstrap_components(components_source, concurrency, install_type):
 
     if status or not out:
         print(
-            "🚨 Failed installing npm dependencies for component packages: {source_glob} (status={status}) 🚨",
+            f"🚨 Failed installing npm dependencies for component packages: {source_glob} (status={status}) 🚨",
             file=sys.stderr,
         )
         sys.exit(1)

Missing `f` prefix on f-strings
Some strings looks like they're meant to be f-strings but are missing the `f` prefix meaning variable interpolation won't happen.

https://github.com/plotly/dash/blob/f6b51a841e7deddebfbada374e7de4c297bb2ce8/dash/development/update_components.py#L48

I found this issue automatically. I'm a bot. Beep Boop 🦊. See other issues I found in your repo [here](https://codereview.doctor/plotly/dash)
