Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [UNRELEASED]

### Bug fixes

* CLI command `shiny create`... (#965)
* has added a `-d`/`--dir` flag for saving to a specific output directory
* will raise an error if if will overwrite existing files
* prompt users to install `requirements.txt`
* Fixed `js-react` template build error. (#965)



## [0.6.1.1] - 2023-12-22
Expand Down
26 changes: 10 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
Shiny for Python
================
# Shiny for Python

[![Release](https://img.shields.io/github/v/release/rstudio/py-shiny)](https://img.shields.io/github/v/release/rstudio/py-shiny)
[![Build status](https://img.shields.io/github/actions/workflow/status/rstudio/py-shiny/pytest.yaml?branch=main)](https://img.shields.io/github/actions/workflow/status/rstudio/py-shiny/pytest.yaml?branch=main)
Expand All @@ -10,13 +9,13 @@ Shiny for Python is the best way to build fast, beautiful web applications in Py

To learn more about Shiny see the [Shiny for Python website](https://shiny.posit.co/py/). If you're new to the framework we recommend these resources:

- How [Shiny is different](https://posit.co/blog/why-shiny-for-python/) from Dash and Streamlit.
- How [Shiny is different](https://posit.co/blog/why-shiny-for-python/) from Dash and Streamlit.

- How [reactive programming](https://shiny.posit.co/py/docs/reactive-programming.html) can help you build better applications.
- How [reactive programming](https://shiny.posit.co/py/docs/reactive-programming.html) can help you build better applications.

- How to [use modules](https://shiny.posit.co/py/docs/workflow-modules.html) to efficiently develop large applications.
- How to [use modules](https://shiny.posit.co/py/docs/workflow-modules.html) to efficiently develop large applications.

- Hosting applications for free on [shinyapps.io](https://shiny.posit.co/py/docs/deploy.html#deploy-to-shinyapps.io-cloud-hosting), [Hugging Face](https://shiny.posit.co/blog/posts/shiny-on-hugging-face/), or [Shinylive](https://shiny.posit.co/py/docs/shinylive.html).
- Hosting applications for free on [shinyapps.io](https://shiny.posit.co/py/docs/deploy.html#deploy-to-shinyapps.io-cloud-hosting), [Hugging Face](https://shiny.posit.co/blog/posts/shiny-on-hugging-face/), or [Shinylive](https://shiny.posit.co/py/docs/shinylive.html).

## Join the conversation

Expand All @@ -26,38 +25,33 @@ If you have questions about Shiny for Python, or want to help us decide what to

To get started with shiny follow the [installation instructions](https://shiny.posit.co/py/docs/install.html) or just install it from pip.

``` sh
```sh
pip install shiny
```

To install the latest development version:

``` sh
```sh
# First install htmltools, then shiny
pip install https://github.com/posit-dev/py-htmltools/tarball/main
pip install https://github.com/posit-dev/py-shiny/tarball/main
```

You can create and run your first application with:

```
shiny create .
shiny run app.py --reload
```
You can create and run your first application with `shiny create`, the CLI will ask you which template you would like to use. You can either run the app with the Shiny extension, or call `shiny run app.py --reload --launch-browser`.

## Development

API documentation for the `main` branch of Shiny: https://posit-dev.github.io/py-shiny/api/

If you want to do development on Shiny for Python:

``` sh
```sh
pip install -e ".[dev,test]"
```

Additionally, you can install pre-commit hooks which will automatically reformat and lint the code when you make a commit:

``` sh
```sh
pre-commit install

# To disable:
Expand Down
25 changes: 25 additions & 0 deletions shiny/_custom_component_template_questions.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,27 @@
import re
from importlib import util
from pathlib import Path

from prompt_toolkit.document import Document
from questionary import ValidationError, Validator


def is_existing_module(name: str) -> bool:
"""
Check if a module name can be imported, which indicates that it is either
a standard module name, or the name of an installed module.
In either case the new module would probably cause a name conflict.
"""
try:
spec = util.find_spec(name)
if spec is not None:
return True
else:
return False
except ImportError:
return False


def is_pep508_identifier(name: str):
"""
Checks if a package name is a PEP 508 identifier.
Expand Down Expand Up @@ -65,6 +82,14 @@ def validate(self, document: Document):
cursor_position=len(name),
)

# Using the name of an existing package causes an import error

if is_existing_module(name):
raise ValidationError(
message="Package already installed in your current environment.",
cursor_position=len(name),
)


def update_component_name_in_template(template_dir: Path, new_component_name: str):
"""
Expand Down
24 changes: 22 additions & 2 deletions shiny/_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -513,18 +513,38 @@ def try_import_module(module: str) -> Optional[types.ModuleType]:
"-g",
help="The GitHub URL of the template sub-directory. For example https://github.com/posit-dev/py-shiny-templates/tree/main/dashboard",
)
@click.option(
"--dir",
"-d",
help="The destination directory, you will be prompted if this is not provided.",
)
@click.option(
"--package-name",
help="""
If you are using one of the JavaScript component templates,
you can use this flag to specify the name of the resulting package without being prompted.
""",
)
def create(
template: Optional[str] = None,
mode: Optional[str] = None,
github: Optional[str] = None,
dir: Optional[str | Path] = None,
package_name: Optional[str] = None,
) -> None:
from ._template_utils import template_query, use_git_template

if github is not None and template is not None:
raise click.UsageError("You cannot provide both --github and --template")

if isinstance(dir, str):
dir = Path(dir)

if github is not None:
use_git_template(github, mode)
use_git_template(github, mode, dir)
return

template_query(template, mode)
template_query(template, mode, dir, package_name)


@main.command(
Expand Down
117 changes: 70 additions & 47 deletions shiny/_template_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,12 @@ def choice_from_dict(choice_dict: dict[str, str]) -> list[Choice]:
return [Choice(title=key, value=value) for key, value in choice_dict.items()]


def template_query(question_state: Optional[str] = None, mode: Optional[str] = None):
def template_query(
question_state: Optional[str] = None,
mode: Optional[str] = None,
dest_dir: Optional[Path] = None,
package_name: Optional[str] = None,
):
"""
This will initiate a CLI query which will ask the user which template they would like.
If called without arguments this function will start from the top level and ask which
Expand Down Expand Up @@ -69,12 +74,12 @@ def template_query(question_state: Optional[str] = None, mode: Optional[str] = N
if template is None or template == "cancel":
sys.exit(1)
elif template == "js-component":
js_component_questions()
js_component_questions(dest_dir=dest_dir, package_name=package_name)
return
elif template in package_template_choices.values():
js_component_questions(template)
js_component_questions(template, dest_dir=dest_dir, package_name=package_name)
else:
app_template_questions(template, mode)
app_template_questions(template, mode, dest_dir=dest_dir)


def download_and_extract_zip(url: str, temp_dir: Path):
Expand All @@ -92,7 +97,9 @@ def download_and_extract_zip(url: str, temp_dir: Path):
zip_file.extractall(temp_dir)


def use_git_template(url: str, mode: Optional[str] = None):
def use_git_template(
url: str, mode: Optional[str] = None, dest_dir: Optional[Path] = None
):
# Github requires that we download the whole repository, so we need to
# download and unzip the repo, then navigate to the subdirectory.

Expand All @@ -116,13 +123,14 @@ def use_git_template(url: str, mode: Optional[str] = None):

directory = repo_name + "-" + branch_name
path = temp_dir / directory / subdirectory
return app_template_questions(mode=mode, template_dir=path)
return app_template_questions(mode=mode, template_dir=path, dest_dir=dest_dir)


def app_template_questions(
template: Optional[str] = None,
mode: Optional[str] = None,
template_dir: Optional[Path] = None,
dest_dir: Optional[Path] = None,
):
if template_dir is None:
if template is None:
Expand Down Expand Up @@ -154,30 +162,25 @@ def app_template_questions(
template_query()
return

appdir = questionary.path(
"Enter destination directory:",
default=build_path_string(""),
only_directories=True,
).ask()

if appdir is None:
sys.exit(1)

if appdir == ".":
appdir = build_path_string(template_dir.name)
dest_dir = directory_prompt(template_dir, dest_dir)

app_dir = copy_template_files(
Path(appdir),
dest_dir,
template_dir=template_dir,
express_available=express_available,
mode=mode,
)

print(f"Created Shiny app at {app_dir}")
print(f"Next steps open and edit the app file: {app_dir}/app.py")
print("You may need to install packages with: `pip install -r requirements.txt`")


def js_component_questions(component_type: Optional[str] = None):
def js_component_questions(
component_type: Optional[str] = None,
dest_dir: Optional[Path] = None,
package_name: Optional[str] = None,
):
"""
Hand question branch for the custom js templates. This should handle the entire rest
of the question flow and is responsible for placing files etc. Currently it repeats
Expand All @@ -202,40 +205,33 @@ def js_component_questions(component_type: Optional[str] = None):
if component_type is None or component_type == "cancel":
sys.exit(1)

# As what the user wants the name of their component to be
component_name = questionary.text(
"What do you want to name your component?",
instruction="Name must be dash-delimited and all lowercase. E.g. 'my-component-name'",
validate=ComponentNameValidator,
).ask()

if component_name is None:
sys.exit(1)
# Ask what the user wants the name of their component to be
if package_name is None:
package_name = questionary.text(
"What do you want to name your component?",
instruction="Name must be dash-delimited and all lowercase. E.g. 'my-component-name'",
validate=ComponentNameValidator,
).ask()

appdir = questionary.path(
"Enter destination directory:",
default=build_path_string(component_name),
only_directories=True,
).ask()
if package_name is None:
sys.exit(1)

if appdir is None:
sys.exit(1)
template_dir = (
Path(__file__).parent / "templates/package-templates" / component_type
)

if appdir == ".":
appdir = build_path_string(component_type)
dest_dir = directory_prompt(template_dir, dest_dir)

app_dir = copy_template_files(
Path(appdir),
template_dir=Path(__file__).parent
/ "templates/package-templates"
/ component_type,
dest_dir,
template_dir=template_dir,
express_available=False,
mode=None,
)

# Print messsage saying we're building the component
print(f"Setting up {component_name} component package...")
update_component_name_in_template(app_dir, component_name)
print(f"Setting up {package_name} component package...")
update_component_name_in_template(app_dir, package_name)

print("\nNext steps:")
print(f"- Run `cd {app_dir}` to change into the new directory")
Expand All @@ -245,6 +241,27 @@ def js_component_questions(component_type: Optional[str] = None):
print("- Open and run the example app in the `example-app` directory")


def directory_prompt(
template_dir: Path, dest_dir: Optional[Path | str | None] = None
) -> Path:
if dest_dir is not None:
return Path(dest_dir)

app_dir = questionary.path(
"Enter destination directory:",
default=build_path_string(""),
only_directories=True,
).ask()

if app_dir is None:
sys.exit(1)

if app_dir == ".":
app_dir = build_path_string(template_dir.name)

return Path(app_dir)


def build_path_string(*path: str):
"""
Build a path string that is valid for the current OS
Expand All @@ -258,9 +275,14 @@ def copy_template_files(
express_available: bool,
mode: Optional[str] = None,
):
duplicate_files = [
file.name for file in template_dir.iterdir() if (app_dir / file.name).exists()
]
files_to_check = [file.name for file in template_dir.iterdir()]

if "__pycache__" in files_to_check:
files_to_check.remove("__pycache__")

files_to_check.append("app.py")

duplicate_files = [file for file in files_to_check if (app_dir / file).exists()]

if any(duplicate_files):
err_files = ", ".join(['"' + file + '"' for file in duplicate_files])
Expand All @@ -276,7 +298,8 @@ def copy_template_files(
if item.is_file():
shutil.copy(item, app_dir / item.name)
else:
shutil.copytree(item, app_dir / item.name)
if item.name != "__pycache__":
shutil.copytree(item, app_dir / item.name)

def rename_unlink(file_to_rename: str, file_to_delete: str, dir: Path = app_dir):
(dir / file_to_rename).rename(dir / "app.py")
Expand Down
Loading