Skip to content

Commit 53c46fd

Browse files
Gordon Shotwellschloerke
andauthored
Shiny create bugs (#965)
Co-authored-by: Barret Schloerke <[email protected]>
1 parent 6ed505b commit 53c46fd

File tree

7 files changed

+193
-68
lines changed

7 files changed

+193
-68
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88

99
## [UNRELEASED]
1010

11+
### Bug fixes
12+
13+
* CLI command `shiny create`... (#965)
14+
* has added a `-d`/`--dir` flag for saving to a specific output directory
15+
* will raise an error if if will overwrite existing files
16+
* prompt users to install `requirements.txt`
17+
* Fixed `js-react` template build error. (#965)
18+
1119

1220

1321
## [0.6.1.1] - 2023-12-22

README.md

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
Shiny for Python
2-
================
1+
# Shiny for Python
32

43
[![Release](https://img.shields.io/github/v/release/rstudio/py-shiny)](https://img.shields.io/github/v/release/rstudio/py-shiny)
54
[![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)
@@ -10,13 +9,13 @@ Shiny for Python is the best way to build fast, beautiful web applications in Py
109

1110
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:
1211

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

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

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

19-
- 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).
18+
- 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).
2019

2120
## Join the conversation
2221

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

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

29-
``` sh
28+
```sh
3029
pip install shiny
3130
```
3231

3332
To install the latest development version:
3433

35-
``` sh
34+
```sh
3635
# First install htmltools, then shiny
3736
pip install https://github.com/posit-dev/py-htmltools/tarball/main
3837
pip install https://github.com/posit-dev/py-shiny/tarball/main
3938
```
4039

41-
You can create and run your first application with:
42-
43-
```
44-
shiny create .
45-
shiny run app.py --reload
46-
```
40+
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`.
4741

4842
## Development
4943

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

5246
If you want to do development on Shiny for Python:
5347

54-
``` sh
48+
```sh
5549
pip install -e ".[dev,test]"
5650
```
5751

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

60-
``` sh
54+
```sh
6155
pre-commit install
6256

6357
# To disable:

shiny/_custom_component_template_questions.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,27 @@
11
import re
2+
from importlib import util
23
from pathlib import Path
34

45
from prompt_toolkit.document import Document
56
from questionary import ValidationError, Validator
67

78

9+
def is_existing_module(name: str) -> bool:
10+
"""
11+
Check if a module name can be imported, which indicates that it is either
12+
a standard module name, or the name of an installed module.
13+
In either case the new module would probably cause a name conflict.
14+
"""
15+
try:
16+
spec = util.find_spec(name)
17+
if spec is not None:
18+
return True
19+
else:
20+
return False
21+
except ImportError:
22+
return False
23+
24+
825
def is_pep508_identifier(name: str):
926
"""
1027
Checks if a package name is a PEP 508 identifier.
@@ -65,6 +82,14 @@ def validate(self, document: Document):
6582
cursor_position=len(name),
6683
)
6784

85+
# Using the name of an existing package causes an import error
86+
87+
if is_existing_module(name):
88+
raise ValidationError(
89+
message="Package already installed in your current environment.",
90+
cursor_position=len(name),
91+
)
92+
6893

6994
def update_component_name_in_template(template_dir: Path, new_component_name: str):
7095
"""

shiny/_main.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -513,18 +513,38 @@ def try_import_module(module: str) -> Optional[types.ModuleType]:
513513
"-g",
514514
help="The GitHub URL of the template sub-directory. For example https://github.com/posit-dev/py-shiny-templates/tree/main/dashboard",
515515
)
516+
@click.option(
517+
"--dir",
518+
"-d",
519+
help="The destination directory, you will be prompted if this is not provided.",
520+
)
521+
@click.option(
522+
"--package-name",
523+
help="""
524+
If you are using one of the JavaScript component templates,
525+
you can use this flag to specify the name of the resulting package without being prompted.
526+
""",
527+
)
516528
def create(
517529
template: Optional[str] = None,
518530
mode: Optional[str] = None,
519531
github: Optional[str] = None,
532+
dir: Optional[str | Path] = None,
533+
package_name: Optional[str] = None,
520534
) -> None:
521535
from ._template_utils import template_query, use_git_template
522536

537+
if github is not None and template is not None:
538+
raise click.UsageError("You cannot provide both --github and --template")
539+
540+
if isinstance(dir, str):
541+
dir = Path(dir)
542+
523543
if github is not None:
524-
use_git_template(github, mode)
544+
use_git_template(github, mode, dir)
525545
return
526546

527-
template_query(template, mode)
547+
template_query(template, mode, dir, package_name)
528548

529549

530550
@main.command(

shiny/_template_utils.py

Lines changed: 70 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,12 @@ def choice_from_dict(choice_dict: dict[str, str]) -> list[Choice]:
4040
return [Choice(title=key, value=value) for key, value in choice_dict.items()]
4141

4242

43-
def template_query(question_state: Optional[str] = None, mode: Optional[str] = None):
43+
def template_query(
44+
question_state: Optional[str] = None,
45+
mode: Optional[str] = None,
46+
dest_dir: Optional[Path] = None,
47+
package_name: Optional[str] = None,
48+
):
4449
"""
4550
This will initiate a CLI query which will ask the user which template they would like.
4651
If called without arguments this function will start from the top level and ask which
@@ -69,12 +74,12 @@ def template_query(question_state: Optional[str] = None, mode: Optional[str] = N
6974
if template is None or template == "cancel":
7075
sys.exit(1)
7176
elif template == "js-component":
72-
js_component_questions()
77+
js_component_questions(dest_dir=dest_dir, package_name=package_name)
7378
return
7479
elif template in package_template_choices.values():
75-
js_component_questions(template)
80+
js_component_questions(template, dest_dir=dest_dir, package_name=package_name)
7681
else:
77-
app_template_questions(template, mode)
82+
app_template_questions(template, mode, dest_dir=dest_dir)
7883

7984

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

9499

95-
def use_git_template(url: str, mode: Optional[str] = None):
100+
def use_git_template(
101+
url: str, mode: Optional[str] = None, dest_dir: Optional[Path] = None
102+
):
96103
# Github requires that we download the whole repository, so we need to
97104
# download and unzip the repo, then navigate to the subdirectory.
98105

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

117124
directory = repo_name + "-" + branch_name
118125
path = temp_dir / directory / subdirectory
119-
return app_template_questions(mode=mode, template_dir=path)
126+
return app_template_questions(mode=mode, template_dir=path, dest_dir=dest_dir)
120127

121128

122129
def app_template_questions(
123130
template: Optional[str] = None,
124131
mode: Optional[str] = None,
125132
template_dir: Optional[Path] = None,
133+
dest_dir: Optional[Path] = None,
126134
):
127135
if template_dir is None:
128136
if template is None:
@@ -154,30 +162,25 @@ def app_template_questions(
154162
template_query()
155163
return
156164

157-
appdir = questionary.path(
158-
"Enter destination directory:",
159-
default=build_path_string(""),
160-
only_directories=True,
161-
).ask()
162-
163-
if appdir is None:
164-
sys.exit(1)
165-
166-
if appdir == ".":
167-
appdir = build_path_string(template_dir.name)
165+
dest_dir = directory_prompt(template_dir, dest_dir)
168166

169167
app_dir = copy_template_files(
170-
Path(appdir),
168+
dest_dir,
171169
template_dir=template_dir,
172170
express_available=express_available,
173171
mode=mode,
174172
)
175173

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

179178

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

205-
# As what the user wants the name of their component to be
206-
component_name = questionary.text(
207-
"What do you want to name your component?",
208-
instruction="Name must be dash-delimited and all lowercase. E.g. 'my-component-name'",
209-
validate=ComponentNameValidator,
210-
).ask()
211-
212-
if component_name is None:
213-
sys.exit(1)
208+
# Ask what the user wants the name of their component to be
209+
if package_name is None:
210+
package_name = questionary.text(
211+
"What do you want to name your component?",
212+
instruction="Name must be dash-delimited and all lowercase. E.g. 'my-component-name'",
213+
validate=ComponentNameValidator,
214+
).ask()
214215

215-
appdir = questionary.path(
216-
"Enter destination directory:",
217-
default=build_path_string(component_name),
218-
only_directories=True,
219-
).ask()
216+
if package_name is None:
217+
sys.exit(1)
220218

221-
if appdir is None:
222-
sys.exit(1)
219+
template_dir = (
220+
Path(__file__).parent / "templates/package-templates" / component_type
221+
)
223222

224-
if appdir == ".":
225-
appdir = build_path_string(component_type)
223+
dest_dir = directory_prompt(template_dir, dest_dir)
226224

227225
app_dir = copy_template_files(
228-
Path(appdir),
229-
template_dir=Path(__file__).parent
230-
/ "templates/package-templates"
231-
/ component_type,
226+
dest_dir,
227+
template_dir=template_dir,
232228
express_available=False,
233229
mode=None,
234230
)
235231

236232
# Print messsage saying we're building the component
237-
print(f"Setting up {component_name} component package...")
238-
update_component_name_in_template(app_dir, component_name)
233+
print(f"Setting up {package_name} component package...")
234+
update_component_name_in_template(app_dir, package_name)
239235

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

247243

244+
def directory_prompt(
245+
template_dir: Path, dest_dir: Optional[Path | str | None] = None
246+
) -> Path:
247+
if dest_dir is not None:
248+
return Path(dest_dir)
249+
250+
app_dir = questionary.path(
251+
"Enter destination directory:",
252+
default=build_path_string(""),
253+
only_directories=True,
254+
).ask()
255+
256+
if app_dir is None:
257+
sys.exit(1)
258+
259+
if app_dir == ".":
260+
app_dir = build_path_string(template_dir.name)
261+
262+
return Path(app_dir)
263+
264+
248265
def build_path_string(*path: str):
249266
"""
250267
Build a path string that is valid for the current OS
@@ -258,9 +275,14 @@ def copy_template_files(
258275
express_available: bool,
259276
mode: Optional[str] = None,
260277
):
261-
duplicate_files = [
262-
file.name for file in template_dir.iterdir() if (app_dir / file.name).exists()
263-
]
278+
files_to_check = [file.name for file in template_dir.iterdir()]
279+
280+
if "__pycache__" in files_to_check:
281+
files_to_check.remove("__pycache__")
282+
283+
files_to_check.append("app.py")
284+
285+
duplicate_files = [file for file in files_to_check if (app_dir / file).exists()]
264286

265287
if any(duplicate_files):
266288
err_files = ", ".join(['"' + file + '"' for file in duplicate_files])
@@ -276,7 +298,8 @@ def copy_template_files(
276298
if item.is_file():
277299
shutil.copy(item, app_dir / item.name)
278300
else:
279-
shutil.copytree(item, app_dir / item.name)
301+
if item.name != "__pycache__":
302+
shutil.copytree(item, app_dir / item.name)
280303

281304
def rename_unlink(file_to_rename: str, file_to_delete: str, dir: Path = app_dir):
282305
(dir / file_to_rename).rename(dir / "app.py")

0 commit comments

Comments
 (0)