Skip to content

Commit

Permalink
feat(app): extract main cli logic out to command/option callbacks.
Browse files Browse the repository at this point in the history
Additionally resolve some known issues and make minor improvements.
Fixed #338

Signed-off-by: Braden Mars <bradenmars@bradenmars.me>
  • Loading branch information
BradenM committed Mar 27, 2023
1 parent f6b50ec commit ae8f5d8
Show file tree
Hide file tree
Showing 2 changed files with 170 additions and 54 deletions.
219 changes: 168 additions & 51 deletions micropy/app/main.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import sys
from enum import Enum
from pathlib import Path
from typing import Optional
from typing import Optional, cast

import micropy.exceptions as exc
import questionary as prompt
import typer
from micropy import logger, utils
from micropy.main import MicroPy
from micropy.project import Project, modules
from micropy.stubs.stubs import Stub
from questionary import Choice

from .stubs import stubs_app
Expand Down Expand Up @@ -47,25 +47,110 @@ def main_callback(ctx: typer.Context):
log.info("You can update via: $[pip install --upgrade micropy-cli]\n")


TemplateEnum = Enum(
"TemplateEnum", {t: t for t in list(modules.TemplatesModule.TEMPLATES.keys())}, type=str
)
TemplateEnum = Enum("TemplateEnum", {t: t for t in modules.TemplatesModule.TEMPLATES}, type=str)


def template_callback(
ctx: typer.Context, value: Optional[list[TemplateEnum]]
) -> Optional[list[TemplateEnum]]:
if ctx.resilient_parsing:
return
if not value:
templates = modules.TemplatesModule.TEMPLATES.items()
templ_choices = [Choice(str(val[1]), value=t) for t, val in templates]
value = prompt.checkbox("Choose any Templates to Generate", choices=templ_choices).ask()
if not value:
if not prompt.confirm(
"You have chosen to use NO templates. Are you sure you want to continue?",
default=False,
).ask():
raise typer.Abort()
return []
value = [TemplateEnum(k) for k in value]
return value


def path_callback(ctx: typer.Context, value: Optional[Path]) -> Optional[Path]:
if ctx.resilient_parsing:
return
return value if value else Path.cwd()


def name_callback(ctx: typer.Context, value: Optional[str]) -> Optional[str]:
if ctx.resilient_parsing:
return
if not value:
path = ctx.params.get("path", Path.cwd())
default_name = path.name
prompt_name = prompt.text("Project Name", default=default_name).ask()
if prompt_name is None:
raise typer.Abort("You must provide a project name via prompt or --name option.")
return prompt_name.strip()
return value


def stubs_callback(ctx: typer.Context, value: Optional[list[str]]) -> Optional[list[Stub]]:
if ctx.resilient_parsing:
return
mpy = ctx.ensure_object(MicroPy)
stub_values = (
[i for i in [mpy.stubs.add(s) for s in value] if i is not None]
if value
else list(mpy.stubs)
)
if not stub_values:
mpy.log.error("You don't have any stubs!")
mpy.log.title("To add stubs to micropy, use $[micropy stubs add <STUB_NAME>]")
mpy.log.info("See: $[micropy stubs --help] for more information.")
raise typer.Abort(1)
if not value:
# if value was not explicitly provided, ask for selections.
stubs = [Choice(str(s), value=s) for s in stub_values]
stub_choices = prompt.checkbox("Which stubs would you like to use?", choices=stubs).ask()
if not stub_choices:
# mpy.log.error("You must choose at least one stub!")
raise typer.BadParameter(
"You must choose at least one stub!",
ctx,
)
return stub_choices
return stub_values


@app.command(name="init")
def main_init(
ctx: typer.Context,
path: Optional[Path] = typer.Argument(
None, help="Path to project. Defaults to current working directory."
None,
help="Path to project. Defaults to current working directory.",
callback=path_callback,
dir_okay=True,
file_okay=False,
show_default=False,
),
name: Optional[str] = typer.Option(
None, "--name", "-n", help="Project Name. Defaults to path name."
None,
"--name",
"-n",
help="Project Name. Defaults to path name.",
show_default=False,
callback=name_callback,
),
template: Optional[list[TemplateEnum]] = typer.Option(
None,
"--template",
"-t",
help="Templates to generate for project. Can be specified multiple times. Skips interactive prompt.",
show_default=False,
callback=template_callback,
),
stubs: Optional[list[str]] = typer.Option(
None,
"--stubs",
"-s",
help="Name of stubs to add to project. Can be specified multiple times. Skips interactive prompt.",
callback=stubs_callback,
show_default=False,
),
):
"""Create new Micropython Project.
Expand All @@ -76,26 +161,18 @@ def main_init(
"""
mpy: MicroPy = ctx.find_object(MicroPy)
mpy.log.title("Creating New Project")
if not path:
path = Path.cwd()
default_name = path.name
prompt_name = prompt.text("Project Name", default=default_name).ask()
name = prompt_name.strip()
if not template:
templates = modules.TemplatesModule.TEMPLATES.items()
templ_choices = [Choice(str(val[1]), value=t) for t, val in templates]
template = prompt.checkbox("Choose any Templates to Generate", choices=templ_choices).ask()
stubs = [Choice(str(s), value=s) for s in mpy.stubs]
if not stubs:
mpy.log.error("You don't have any stubs!")
mpy.log.title("To add stubs to micropy, use $[micropy stubs add <STUB_NAME>]")
sys.exit(1)
stub_choices = prompt.checkbox("Which stubs would you like to use?", choices=stubs).ask()
# weird issue where "template" from args
# gets set a [None,None], but its correct in params.
template = ctx.params.get("template", template)
project = Project(path, name=name)
project.add(modules.StubsModule, mpy.stubs, stubs=stub_choices)
project.add(modules.StubsModule, mpy.stubs, stubs=stubs)
project.add(modules.PackagesModule, "requirements.txt")
project.add(modules.DevPackagesModule, "dev-requirements.txt")
project.add(modules.TemplatesModule, templates=template, run_checks=mpy.RUN_CHECKS)
project.add(
modules.TemplatesModule,
templates=[t.value for t in template if t],
run_checks=mpy.RUN_CHECKS,
)
proj_path = project.create()
try:
rel_path = f"./{proj_path.relative_to(Path.cwd())}"
Expand All @@ -104,13 +181,68 @@ def main_init(
mpy.log.title(f"Created $w[{project.name}] at $w[{rel_path}]")


def ensure_project(ctx: typer.Context) -> Project:
mpy = ctx.ensure_object(MicroPy)
project = mpy.project
if not project.exists:
mpy.log.error("You are not currently in an active project!")
raise typer.Abort(1)
# todo: fix type issue.
return cast(Project, project)


def install_local_callback(ctx: typer.Context, value: Optional[Path]) -> Optional[Path]:
"""Handle package installation from local path."""
if ctx.resilient_parsing:
return
if value is None:
return value
mpy = ctx.ensure_object(MicroPy)
project = ensure_project(ctx)
pkg_name = next(iter(ctx.args), None)
mpy.log.title("Installing Local Package")
pkg_path = "-e " + str(value)
project.add_package(pkg_path, dev=ctx.params.get("dev", False), name=pkg_name)
raise typer.Exit()


def install_project_callback(ctx: typer.Context, value: Optional[list[str]]) -> Optional[list[str]]:
"""Handle project requirements install."""
if ctx.resilient_parsing:
return
if "path" in ctx.params:
return
if not value:
# only if no packages are provided.
mpy = ctx.ensure_object(MicroPy)
project = ensure_project(ctx)
mpy.log.title("Installing all Requirements")
try:
project.add_from_file(dev=ctx.params.get("dev", False))
except Exception as e:
mpy.log.error("Failed to load requirements!", exception=e)
raise typer.Abort() from e
else:
mpy.log.success("\nRequirements Installed!")
raise typer.Exit()
return value


@app.command(name="install")
def main_install(
ctx: typer.Context,
packages: list[str],
dev: bool = False,
packages: Optional[list[str]] = typer.Argument(
None, help="Packages to install.", callback=install_project_callback
),
dev: bool = typer.Option(
default=False,
help="Install as development package. This will not generate stubs for the package.",
show_default=True,
),
path: Optional[Path] = typer.Option(
None, help="Add dependency from local path. Can be a file or directory."
None,
help="Add dependency from local path. Can be a file or directory.",
callback=install_local_callback,
),
):
"""Install Packages as Project Requirements.
Expand All @@ -119,6 +251,12 @@ def main_install(
Install a project dependency while enabling
intellisense, autocompletion, and linting for it.
\b
$ micropy install picoweb==1.8.2 blynklib
\b
\b
If no packages are passed and a requirements.txt file is found,
then micropy will install all packages listed in it.
Expand All @@ -145,34 +283,13 @@ def main_install(
_import <package_name>_
"""
mpy: MicroPy = ctx.find_object(MicroPy)
project = mpy.project
if not project.exists:
mpy.log.error("You are not currently in an active project!")
sys.exit(1)
if path:
pkg_name = next(iter(packages), None)
mpy.log.title("Installing Local Package")
pkg_path = "-e " + path
project.add_package(pkg_path, dev=dev, name=pkg_name)
sys.exit(0)
if not packages:
mpy.log.title("Installing all Requirements")
try:
project.add_from_file(dev=dev)
except Exception as e:
mpy.log.error("Failed to load requirements!", exception=e)
raise typer.Abort() from e
else:
mpy.log.success("\nRequirements Installed!")
sys.exit(0)
mpy: MicroPy = ctx.ensure_object(MicroPy)
project = ensure_project(ctx)
mpy.log.title("Installing Packages")
for pkg in packages:
try:
project.add_package(pkg, dev=dev)
except exc.RequirementException as e:
pkg_name = str(e.package)
mpy.log.error(
(f"Failed to install {pkg_name}!" " Is it available on PyPi?"), exception=e
)
mpy.log.error(f"Failed to install {pkg_name}!" " Is it available on PyPi?", exception=e)
raise typer.Abort() from e
5 changes: 2 additions & 3 deletions micropy/app/stubs.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,11 +82,10 @@ def stubs_create(
),
):
"""Create stubs from a micropython-enabled device."""
mpy: MicroPy = ctx.find_object(MicroPy)
log = mpy.log
mp: MicroPy = ctx.find_object(MicroPy)
log = mp.log
log.title(f"Connecting to Pyboard @ $[{port}]")
pyb_log = Log.add_logger("Pyboard", "bright_white")
mp = MicroPy()

def _get_desc(name: str, cfg: dict):
desc = f"{pyb_log.get_service()} {name}"
Expand Down

0 comments on commit ae8f5d8

Please sign in to comment.