Repository: gradio-app/gradio
Pull request: Custom Component CLI Improvements
## Description

Closes: #6354 
Closes: #6349 

Summary of changes:
* The `gradio cc create` command now asks users to specify some project metadata as suggested in #6354. There's a cli flag to disable this and answering the questions is optional. Adding the keywords will help with custom component discoverability!
* The `gradio cc build` clearly mentions which version will be used for the wheel file. Added a `--bump-version` flag to automatically do a patch bump. Defaults to false because people can build multiple times.
* The `gradio cc publish` now explains how to publish if your pypi account has 2fa. Also we can read credentials automatically from a `~/.pypirc` file (the community standard). 
* Improve the default Docker image as described in #6349
* Automatically adds a title and description to the Dockerfile of the published space as well.
* Rename the five minute guide for SEO based on @aliabid94 's comment

## 🎯 PRs Should Target Issues

Before your create a PR, please check to see if there is [an existing issue](https://github.com/gradio-app/gradio/issues) for this change. If not, please create an issue before you create this PR, unless the fix is very small. 

Not adhering to this guideline will result in the PR being closed. 

## Tests

1. PRs will only be merged if tests pass on CI. To run the tests locally, please set up [your Gradio environment locally](https://github.com/gradio-app/gradio/blob/main/CONTRIBUTING.md) and run the tests: `bash scripts/run_all_tests.sh`

2. You may need to run the linters: `bash scripts/format_backend.sh` and `bash scripts/format_frontend.sh`
  


Problem statement:
Improve the custom component deploy dockerfile
- [ ] I have searched to see if a similar issue already exists.

We should add the following lines to the `Dockerfile` used in custom component demos so that developers can download transformers models without getting an error that the cache is not set.

```
RUN mkdir -p /tmp/cache/
RUN chmod a+rwx -R /tmp/cache/
ENV TRANSFORMERS_CACHE=/tmp/cache/ 
```

The reason we're using a Dockerfile is so that users can install their component in a space without deploying to pypi but maybe we can use the gradio sdk space if they did deploy to pypi.


**Is your feature request related to a problem? Please describe.**  
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]

**Describe the solution you'd like**  
A clear and concise description of what you want to happen.

**Additional context**  
Add any other context or screenshots about the feature request here.


Custom Components CLI improvements
- [x] I have searched to see if a similar issue already exists.


**Following features are still missing**  

- [x] `gradio cc publish` deployment to pypi doesn't support accounts with 2FA
- [ ] `gradio cc create` should not actually create the folder if the template isn't found
- [ ]  `pyproject.toml` could also set a few more inputs based on CLI. In oarticular the `[project]` part

```
[project]
--
  | name = "gradio_molecule3d"
  | version = "0.0.1"  # set each time cc publish is called
  | description = "Molecule3D custom component to visualize pdb or sdf files" # gradio cc create
  | readme = "README.md"
  | license = "MIT" # gradio cc create
  | requires-python = ">=3.8" # gradio cc create
  | authors = [{ name = "Simon Dürr", email = "dev@simonduerr.eu" }] # gradio cc create
  | keywords = [
  | "machine learning",
  | "reproducibility",
  | "visualization",
  | "gradio",
  | "gradio custom component",
  | "gradio-template-File",
  | "protein"
  | ] # gradio cc create
```

Hints from issue discussion:
Thanks @duerrsimon ! I like the idea of optionally setting more config via the cli.

BTW `publish` already supports accounts with 2fa. Use the username `__token__`  and the password is your API key. Will make this clearer.

Commit message: Fix link:

Patch to review:
diff --git a/.changeset/long-ways-switch.md b/.changeset/long-ways-switch.md
new file mode 100644
index 0000000000..187d7f6488
--- /dev/null
+++ b/.changeset/long-ways-switch.md
@@ -0,0 +1,6 @@
+---
+"gradio": minor
+"website": minor
+---
+
+feat:Custom Component CLI Improvements
diff --git a/gradio/cli/commands/components/build.py b/gradio/cli/commands/components/build.py
index 0781f305df..41c89a8cb6 100644
--- a/gradio/cli/commands/components/build.py
+++ b/gradio/cli/commands/components/build.py
@@ -3,7 +3,9 @@
 import sys
 from pathlib import Path
 
+import semantic_version
 import typer
+from tomlkit import dump, parse
 from typing_extensions import Annotated
 
 import gradio
@@ -20,6 +22,9 @@ def _build(
     build_frontend: Annotated[
         bool, typer.Option(help="Whether to build the frontend as well.")
     ] = True,
+    bump_version: Annotated[
+        bool, typer.Option(help="Whether to bump the version number automatically.")
+    ] = False,
 ):
     name = Path(path).resolve()
     if not (name / "pyproject.toml").exists():
@@ -29,6 +34,24 @@ def _build(
         live.update(
             f":package: Building package in [orange3]{str(name.name)}[/]", add_sleep=0.2
         )
+        pyproject_toml = parse((path / "pyproject.toml").read_text())
+        if bump_version:
+            pyproject_toml = parse((path / "pyproject.toml").read_text())
+            version = semantic_version.Version(pyproject_toml["project"]["version"]).next_patch()  # type: ignore
+            live.update(
+                f":1234: Using version [bold][magenta]{version}[/][/]. "
+                "Set [bold][magenta]--no-bump-version[/][/] to use the version in pyproject.toml file."
+            )
+            pyproject_toml["project"]["version"] = str(version)  # type: ignore
+            with open(path / "pyproject.toml", "w") as f:
+                dump(pyproject_toml, f)
+        else:
+            version = pyproject_toml["project"]["version"]  # type: ignore
+            live.update(
+                f":1234: Package will use version [bold][magenta]{version}[/][/] defined in pyproject.toml file. "
+                "Set [bold][magenta]--bump-version[/][/] to automatically bump the version number."
+            )
+
         if build_frontend:
             live.update(":art: Building frontend")
             component_directory = path.resolve()
diff --git a/gradio/cli/commands/components/create.py b/gradio/cli/commands/components/create.py
index 10f8a3bd1c..fb46f41786 100644
--- a/gradio/cli/commands/components/create.py
+++ b/gradio/cli/commands/components/create.py
@@ -3,6 +3,10 @@
 from typing import Optional
 
 import typer
+from rich import print
+from rich.panel import Panel
+from rich.prompt import Confirm, Prompt
+from tomlkit import dump, parse
 from typing_extensions import Annotated
 
 from gradio.cli.commands.components.install_component import _get_npm, _install_command
@@ -48,6 +52,12 @@ def _create(
         bool,
         typer.Option(help="Whether to overwrite the existing component if it exists."),
     ] = False,
+    configure_metadata: Annotated[
+        bool,
+        typer.Option(
+            help="Whether to interactively configure project metadata based on user input"
+        ),
+    ] = True,
 ):
     if not directory:
         directory = Path(name.lower())
@@ -92,3 +102,57 @@ def _create(
 
         if install:
             _install_command(directory, live, npm_install)
+
+        live._panel.stop()
+
+        if configure_metadata:
+            print(
+                Panel(
+                    "It is recommended to answer the following [bold][magenta]4 questions[/][/] to finish configuring your custom component's metadata."
+                    "\nYou can also answer them later by editing the [bold][magenta]pyproject.toml[/][/] file in your component directory."
+                )
+            )
+
+            answer_qs = Confirm.ask("\nDo you want to answer them now?")
+
+            if answer_qs:
+                pyproject_toml = parse((directory / "pyproject.toml").read_text())
+                name = pyproject_toml["project"]["name"]  # type: ignore
+
+                description = Prompt.ask(
+                    "\n:pencil: Please enter a one sentence [bold][magenta]description[/][/] for your component"
+                )
+                if description:
+                    pyproject_toml["project"]["description"] = description  # type: ignore
+
+                license_ = Prompt.ask(
+                    "\n:bookmark_tabs: Please enter a [bold][magenta]software license[/][/] for your component. Leave blank for 'MIT'"
+                )
+                license_ = license_ or "MIT"
+                print(f":bookmark_tabs: Using license [bold][magenta]{license_}[/][/]")
+                pyproject_toml["project"]["license"] = license_  # type: ignore
+
+                requires_python = Prompt.ask(
+                    "\n:snake: Please enter the [bold][magenta]allowed python[/][/] versions for your component. Leave blank for '>=3.8'"
+                )
+                requires_python = requires_python or ">=3.8"
+                print(
+                    f":snake: Using requires-python of [bold][magenta]{requires_python}[/][/]"
+                )
+                pyproject_toml["project"]["requires-python"] = requires_python or ">=3.8"  # type: ignore
+
+                keywords = []
+                print(
+                    "\n:label: Please add some keywords to help others discover your component."
+                )
+                while True:
+                    keyword = Prompt.ask(":label: Leave blank to stop adding keywords")
+                    if keyword:
+                        keywords.append(keyword)
+                    else:
+                        break
+                current_keywords = pyproject_toml["project"].get("keywords", [])  # type: ignore
+                pyproject_toml["project"]["keywords"] = current_keywords + keywords  # type: ignore
+                with open(directory / "pyproject.toml", "w") as f:
+                    dump(pyproject_toml, f)
+                print("\nComponent creation [bold][magenta]complete[/][/]!")
diff --git a/gradio/cli/commands/components/files/pyproject_.toml b/gradio/cli/commands/components/files/pyproject_.toml
index 6fc83a9e8f..e71fae672f 100644
--- a/gradio/cli/commands/components/files/pyproject_.toml
+++ b/gradio/cli/commands/components/files/pyproject_.toml
@@ -15,11 +15,7 @@ license = "Apache-2.0"
 requires-python = ">=3.8"
 authors = [{ name = "YOUR NAME", email = "YOUREMAIL@domain.com" }]
 keywords = [
-  "machine learning",
-  "reproducibility",
-  "visualization",
-  "gradio",
-  "gradio custom component",
+  "gradio-custom-component",
   "<<template>>"
 ]
 # Add dependencies here
diff --git a/gradio/cli/commands/components/publish.py b/gradio/cli/commands/components/publish.py
index 2eac34da85..58b02b5026 100644
--- a/gradio/cli/commands/components/publish.py
+++ b/gradio/cli/commands/components/publish.py
@@ -1,5 +1,4 @@
 import random
-import re
 import shutil
 import tempfile
 from pathlib import Path
@@ -10,11 +9,10 @@
 from rich.console import Console
 from rich.panel import Panel
 from rich.prompt import Confirm, Prompt
+from tomlkit import parse
 from typer import Argument, Option
 from typing_extensions import Annotated
 
-from gradio.cli.commands.components._create_utils import PATTERN_RE
-
 colors = ["red", "yellow", "green", "blue", "indigo", "purple", "pink", "gray"]
 
 PYPI_REGISTER_URL = "https://pypi.org/account/register/"
@@ -38,6 +36,10 @@
 
 COPY --link --chown=1000 . .
 
+RUN mkdir -p /tmp/cache/
+RUN chmod a+rwx -R /tmp/cache/
+ENV TRANSFORMERS_CACHE=/tmp/cache/ 
+
 RUN pip install --no-cache-dir -r requirements.txt
 
 ENV PYTHONUNBUFFERED=1 \
@@ -65,6 +67,9 @@ def _publish(
         Argument(help=f"Path to the wheel directory. Default is {Path('.') / 'dist'}"),
     ] = Path(".")
     / "dist",
+    bump_version: Annotated[
+        bool, Option(help="Whether to bump the version number.")
+    ] = True,
     upload_pypi: Annotated[bool, Option(help="Whether to upload to PyPI.")] = True,
     pypi_username: Annotated[str, Option(help="The username for PyPI.")] = "",
     pypi_password: Annotated[str, Option(help="The password for PyPI.")] = "",
@@ -90,6 +95,8 @@ def _publish(
     upload_source = source_dir is not None
     console = Console()
     dist_dir = dist_dir.resolve()
+    name = None
+    description = None
     if not dist_dir.exists():
         raise ValueError(
             f"{dist_dir} does not exist. Run `gradio cc build` to create a wheel and source distribution."
@@ -105,6 +112,7 @@ def _publish(
             "A wheel file was not found in the distribution directory. "
             "Run `gradio cc build` to create a wheel file."
         )
+    config_file = None
     if upload_pypi and (not pypi_username or not pypi_password):
         panel = Panel(
             "It is recommended to upload your component to pypi so that [bold][magenta]anyone[/][/] "
@@ -113,7 +121,13 @@ def _publish(
         )
         print(panel)
         upload_pypi = Confirm.ask(":snake: Upload to pypi?")
-        if upload_pypi:
+        if upload_pypi and (Path.home() / ".pypirc").exists():
+            print(":closed_lock_with_key: Found .pypirc file in home directory.")
+            config_file = str(Path.home() / ".pypirc")
+        elif upload_pypi:
+            print(
+                ":light_bulb: If you have Two Factor Authentication enabled, the username is __token__ and your password is your API key."
+            )
             pypi_username = Prompt.ask(":laptop_computer: Enter your pypi username")
             pypi_password = Prompt.ask(
                 ":closed_lock_with_key: Enter your pypi password", password=True
@@ -127,8 +141,14 @@ def _publish(
                 "The twine library must be installed to publish to pypi."
                 "Install it with pip, pip install twine."
             ) from e
-
-        twine_settings = Settings(username=pypi_username, password=pypi_password)
+        if pypi_username and pypi_password:
+            twine_settings = Settings(username=pypi_username, password=pypi_password)
+        elif config_file:
+            twine_settings = Settings(config_file=config_file)
+        else:
+            raise ValueError(
+                "No pypi username or password provided and no ~/.pypirc file found."
+            )
         try:
             twine_files = [str(p) for p in distribution_files]
             print(f"Uploading files: {','.join(twine_files)}")
@@ -199,21 +219,32 @@ def _publish(
             readme = Path(tempdir) / "README.md"
             template = ""
             if upload_source and source_dir:
-                match = re.search(
-                    PATTERN_RE, (source_dir / "pyproject.toml").read_text()
-                )
-                if match:
-                    template = f", {match.group(0)}"
-
-            readme.write_text(
-                README_CONTENTS.format(
-                    package_name=package_name,
-                    version=version,
-                    color_from=color_from,
-                    color_to=color_to,
-                    template=template,
-                )
+                pyproject_toml = parse((source_dir / "pyproject.toml").read_text())
+                keywords = pyproject_toml["project"].get("keywords", [])  # type: ignore
+                keywords = [
+                    k
+                    for k in keywords
+                    if k not in {"gradio-custom-component", "gradio custom component"}
+                ]
+                if keywords:
+                    template = "," + ",".join(keywords)
+                name = pyproject_toml["project"]["name"]  # type: ignore
+                description = pyproject_toml["project"]["description"]  # type: ignore
+
+            readme_text = README_CONTENTS.format(
+                package_name=package_name,
+                version=version,
+                color_from=color_from,
+                color_to=color_to,
+                template=template,
             )
+
+            if name and description:
+                readme_text += f"\n\n# Name: {name}"
+                readme_text += f"\n\nDescription: {description}"
+                readme_text += f"\n\nInstall with: pip install {package_name}"
+
+            readme.write_text(readme_text)
             dockerfile = Path(tempdir) / "Dockerfile"
             dockerfile.write_text(DOCKERFILE)
 
diff --git a/guides/05_custom-components/01_five-minute-guide.md b/guides/05_custom-components/01_custom-components-in-five-minutes.md
similarity index 100%
rename from guides/05_custom-components/01_five-minute-guide.md
rename to guides/05_custom-components/01_custom-components-in-five-minutes.md
diff --git a/guides/05_custom-components/07_pdf-component-example.md b/guides/05_custom-components/07_pdf-component-example.md
index 3b04281af4..31b7fe63e7 100644
--- a/guides/05_custom-components/07_pdf-component-example.md
+++ b/guides/05_custom-components/07_pdf-component-example.md
@@ -9,7 +9,7 @@ This is a sneak preview of what our finished component will look like:
 ## Step 0: Prerequisites
 Make sure you have gradio 4.0 installed as well as node 18+.
 As of the time of publication, the latest release is 4.1.1.
-Also, please read the [Five Minute Tour](./five-minute-guide) of custom components and the [Key Concepts](./key-component-concepts) guide before starting.
+Also, please read the [Five Minute Tour](./custom-components-in-five-minutes) of custom components and the [Key Concepts](./key-component-concepts) guide before starting.
 
 
 ## Step 1: Creating the custom component
diff --git a/js/_website/src/routes/redirects.js b/js/_website/src/routes/redirects.js
index d2f321a772..7e0034b4cf 100644
--- a/js/_website/src/routes/redirects.js
+++ b/js/_website/src/routes/redirects.js
@@ -1,5 +1,7 @@
 export const redirects = {
-	"/guides/creating-a-new-component": "/guides/five-minute-guide",
+	"/guides/creating-a-new-component":
+		"/guides/custom-components-in-five-minutes",
+	"/guides/five-minute-guide": "/guides/custom-components-in-five-minutes",
 	"/controlling-layout": "/guides/controlling-layout",
 	"/state-in-blocks": "/guides/state-in-blocks",
 	"/custom-CSS-and-JS": "/guides/custom-CSS-and-JS",
diff --git a/test/test_gradio_component_cli.py b/test/test_gradio_component_cli.py
index 7fb8b8a267..e3377549d3 100644
--- a/test/test_gradio_component_cli.py
+++ b/test/test_gradio_component_cli.py
@@ -35,7 +35,14 @@
     ],
 )
 def test_template_override_component(template, tmp_path):
-    _create("MyComponent", tmp_path, template=template, overwrite=True, install=False)
+    _create(
+        "MyComponent",
+        tmp_path,
+        template=template,
+        overwrite=True,
+        install=False,
+        configure_metadata=False,
+    )
     app = (tmp_path / "demo" / "app.py").read_text()
     answer = textwrap.dedent(
         f"""
@@ -62,12 +69,18 @@ def test_raise_error_component_template_does_not_exist(tmp_path):
             template="NonExistentComponent",
             overwrite=True,
             install=False,
+            configure_metadata=False,
         )
 
 
 def test_do_not_replace_class_name_in_import_statement(tmp_path):
     _create(
-        "MyImage", template="Image", directory=tmp_path, overwrite=True, install=False
+        "MyImage",
+        template="Image",
+        directory=tmp_path,
+        overwrite=True,
+        install=False,
+        configure_metadata=False,
     )
     code = (tmp_path / "backend" / "gradio_myimage" / "myimage.py").read_text()
     assert "from PIL import Image as _Image" in code
@@ -79,7 +92,7 @@ def test_raises_if_directory_exists(tmp_path):
     with pytest.raises(
         Exception
     ):  # Keeping it a general exception since the specific exception seems to differ between operating systems
-        _create("MyComponent", tmp_path)
+        _create("MyComponent", tmp_path, configure_metadata=False)
 
 
 def test_show(capsys):
@@ -101,6 +114,7 @@ def test_build(template, tmp_path):
         directory=tmp_path,
         overwrite=True,
         install=True,
+        configure_metadata=False,
     )
     _build(tmp_path, build_frontend=True)
     template_dir: Path = (
@@ -118,6 +132,7 @@ def test_install(tmp_path):
         directory=tmp_path,
         overwrite=True,
         install=False,
+        configure_metadata=False,
     )
 
     assert not (tmp_path / "frontend" / "node_modules").exists()
@@ -131,6 +146,7 @@ def test_fallback_template_app(tmp_path):
         directory=tmp_path,
         overwrite=True,
         install=False,
+        configure_metadata=False,
     )
     app = (tmp_path / "demo" / "app.py").read_text()
     answer = textwrap.dedent(


Code review comments:
diff_hunk: @@ -1,5 +1,7 @@
 export const redirects = {
-	"/guides/creating-a-new-component": "/guides/five-minute-guide",
+	"/guides/creating-a-new-component":
+		"/guides/custom-components-in-five-minutes",
+	"/guides/five-minute-guide": "/guides/custom-components-in-five-minutes",
path: js/_website/src/routes/redirects.js
text: @user1:
this (and the redirect before it) are breaking the website currently because they try to redirect to "/guides/custom-components-in-five-minutes" which doesn't exist yet (it's still /main/guides/custom-components-in-five-minutes until the guide is included in a released version) I would just remove them until we release or redirect to main. 

@author:
Thanks @user1 !