Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add '--standalone' CLI flag #113

Merged
merged 2 commits into from
Sep 25, 2023
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
3 changes: 0 additions & 3 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,6 @@ importance.
- add cmd line options to specify the project name, author, etc. so the user
doesn't have to enter them manually. `Not sure if this is needed once we add
the CLI parameters to the config file. May be useful for automation though`.
- add a command line option to specify the project type so the user doesn't have
to enter it manually. ie `--standalone` or `--package`(latter is default and
wouldn't need to be specified).
- add a command to the CLI template command to show the template files as a
tree, marking whether each file/folder is from the internal templates or the
user's templates.
Expand Down
10 changes: 9 additions & 1 deletion docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ When it asks "Package Name?" you can choose two variants :
2. **For a stand-alone tool** that will not be uploaded to PyPI, or is not a
library, enter '-' for the package name. In this case the `main.py` will just
be placed in the project root and no package folder will be created or
referenced.
referenced. You can also specify `--standalone` on the command line to skip
this question.

For option 1 above, the App will check if the package name is available on PyPI
or if it has already been used. In the latter case, you will be asked to choose
Expand Down Expand Up @@ -84,6 +85,13 @@ If you choose to run `poetry` automatically, this will also add a customized
`mkdocs.yml` file and create a new default MkDocs site in the `docs` folder.
Some useful plugins are also installed and added to the `mkdocs.yml` file.

### `--standalone`

Generate a stand-alone script instead of a package. This will place the
`main.py` file in the project root and not create a package folder. This is
useful for creating a single script that can be run from the command line.
this is equivalent to entering `-` for the package name.

## Run `poetry install` automatically

You will be asked if you want to run `poetry install` automatically. This will
Expand Down
10 changes: 10 additions & 0 deletions py_maker/commands/new.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,15 @@ def new(
Optional[bool],
typer.Option(help="Add the MkDocs boilerplate.", show_default=False),
] = None,
standalone: Annotated[
Optional[bool],
typer.Option(
"--standalone",
"-s",
help="Create a standalone project, not a package.",
show_default=False,
),
] = False,
) -> None:
"""Create a new Python project."""
options = {
Expand All @@ -47,6 +56,7 @@ def new(
"lint": settings.include_linters if lint is None else lint,
"docs": settings.include_mkdocs if docs is None else docs,
"accept_defaults": accept_defaults,
"standalone": standalone,
}

if " " in location:
Expand Down
189 changes: 110 additions & 79 deletions py_maker/pymaker.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ def generate_template(self) -> None:
)

# ---------- rename or delete the 'app' dir if required ---------- #
if self.choices.package_name != "-":
if not self.choices.standalone:
Path(self.choices.project_dir / "app").rename(
self.choices.project_dir / self.choices.package_name
)
Expand Down Expand Up @@ -230,6 +230,9 @@ def post_process(self) -> None:
"""
print(output)

# ------------------------------------------------------------------------ #
# get a sanitized package name from user input. #
# ------------------------------------------------------------------------ #
def get_sanitized_package_name(self, pk_name: str) -> str:
"""Return a sanitized package name from user input."""
while True:
Expand Down Expand Up @@ -270,94 +273,75 @@ def get_sanitized_package_name(self, pk_name: str) -> str:
return name

# ------------------------------------------------------------------------ #
# The main application loop is on the .run() method. #
# accept all the default values for the project. #
# ------------------------------------------------------------------------ #
def run(self) -> None:
"""Entry point for the application."""
self.choices.project_dir = Path.cwd() / self.location

# ensure that the chosen location is empty.
if (
self.choices.project_dir.exists()
and len(os.listdir(self.choices.project_dir)) > 0
):
print(
"\n[red]Error: The chosen folder is not empty. "
"Please specify a different location.[/red]\n"
)
sys.exit(ExitErrors.FOLDER_NOT_EMPTY)

print(
"[green]Creating a new project at[/green] "
f"{self.choices.project_dir}\n"
def accept_defaults(self):
"""Accept the default values for the project."""
self.choices.name = get_title(PurePath(self.choices.project_dir).name)
self.choices.package_name = sanitize(self.choices.project_dir.name)
self.choices.description = ""
self.choices.author = self.settings.author_name
self.choices.email = self.settings.author_email
self.choices.license = self.settings.default_license

def get_input(self):
"""Get the user input for the project."""
self.choices.name = Prompt.ask(
"Name of the Application?",
default=get_title(PurePath(self.choices.project_dir).name),
)
pk_name = sanitize(self.location)

if self.options["accept_defaults"]:
self.choices.name = get_title(
PurePath(self.choices.project_dir).name
)
self.choices.package_name = sanitize(self.choices.project_dir.name)
self.choices.description = ""
self.choices.author = self.settings.author_name
self.choices.email = self.settings.author_email
self.choices.license = self.settings.default_license
self.choices.standalone = False
else:
self.choices.name = Prompt.ask(
"Name of the Application?",
default=get_title(PurePath(self.choices.project_dir).name),
)
pk_name = sanitize(self.location)
# if this is not a standalone script, ask for more details, useful
# for PypI uploads.
if not self.choices.standalone:
self.choices.package_name = self.get_sanitized_package_name(pk_name)
self.choices.homepage = Prompt.ask("Homepage URL?", default="")

# if this is not a standalone script, ask for more details, useful
# for PypI uploads.
if not self.choices.standalone:
self.choices.homepage = Prompt.ask("Homepage URL?", default="")

github_username = (
self.settings.github_username
if self.settings.github_username
else "<your GitHub username>"
)
self.choices.repository = Prompt.ask(
"Repository URL?",
default=(
f"https://github.com/{github_username}/"
f"{re.sub(r'[_.]+', '-', self.choices.package_name)}"
),
)

self.choices.description = Prompt.ask(
"Description of the Application?",
github_username = (
self.settings.github_username
if self.settings.github_username
else "<your GitHub username>"
)
self.choices.author = Prompt.ask(
"Author Name?", default=self.settings.author_name
self.choices.repository = Prompt.ask(
"Repository URL?",
default=(
f"https://github.com/{github_username}/"
f"{re.sub(r'[_.]+', '-', self.choices.package_name)}"
),
)

self.choices.email = Prompt.ask(
"Author Email?", default=self.settings.author_email
)
self.choices.license = Prompt.ask(
"Application License?",
choices=license_names,
default=self.settings.default_license,
)
self.choices.description = Prompt.ask(
"Description of the Application?",
)
self.choices.author = Prompt.ask(
"Author Name?", default=self.settings.author_name
)

if self.choices.package_name == "-":
self.choices.standalone = True
self.choices.email = Prompt.ask(
"Author Email?", default=self.settings.author_email
)
self.choices.license = Prompt.ask(
"Application License?",
choices=license_names,
default=self.settings.default_license,
)

if not self.confirm_values():
# User chose not to continue
print("\n[red]Aborting![/red]")
sys.exit(ExitErrors.USER_ABORT)
if not self.confirm_values():
# User chose not to continue
print("\n[red]Aborting![/red]")
sys.exit(ExitErrors.USER_ABORT)

print()
print()

self.create_folders()
self.generate_template()
# ------------------------------------------------------------------------ #
# run 'poetry install' if required. #
# ------------------------------------------------------------------------ #
def run_poetry(self):
"""Run poetry install if required.

# run poetry install if required
We also create the MkDocs project if enabled.
"""
if self.options["accept_defaults"] or Confirm.ask(
"\nShould I Run 'poetry install' now?", default=True
):
Expand All @@ -375,10 +359,14 @@ def run(self) -> None:
MKDOCS_CONFIG.format(name=self.choices.name)
)

self.create_git_repo()
# ------------------------------------------------------------------------ #
# optionally install and update the pre-commit hooks #
# ------------------------------------------------------------------------ #
def install_precommit(self):
"""Install pre-commit hooks - IF poetry and git are run.

# install and update pre-commit hooks IF poetry and git are run.
# this would fail without either of them.
This would fail without either of them.
"""
if (
self.poetry_is_run
and self.git_is_run
Expand Down Expand Up @@ -420,4 +408,47 @@ def run(self) -> None:
"""
)

# ------------------------------------------------------------------------ #
# The main application loop is on the .run() method. #
# ------------------------------------------------------------------------ #
def run(self) -> None:
"""Run the full process to create the project.

We get input from the user, as well as the options passed in from the
command line. We then create the project skeleton, copy the template
files, create the license file, create the git repo, optionally run
'poetry install' and install the 'pre-commit' hooks.
"""
self.choices.project_dir = Path.cwd() / self.location

# ensure that the chosen location is empty.
if (
self.choices.project_dir.exists()
and len(os.listdir(self.choices.project_dir)) > 0
):
print(
"\n[red]Error: The chosen folder is not empty. "
"Please specify a different location.[/red]\n"
)
sys.exit(ExitErrors.FOLDER_NOT_EMPTY)

print(
"[green]Creating a new project at[/green] "
f"{self.choices.project_dir}\n"
)

self.choices.standalone = True if self.options["standalone"] else False

if self.options["accept_defaults"]:
self.accept_defaults()
else:
self.get_input()

self.create_folders()
self.generate_template()

self.run_poetry()
self.create_git_repo()
self.install_precommit()

self.post_process()