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

Migrate from Typer to Cyclopts #157

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
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
11 changes: 11 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates

version: 2
updates:
- package-ecosystem: "pip" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: "weekly"
21 changes: 6 additions & 15 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
repos:
- repo: https://github.com/charliermarsh/ruff-pre-commit
# Ruff version.
rev: "v0.0.284"
rev: "v0.1.7"
hooks:
- id: ruff
args: []
exclude: ^(belay/snippets/)

- repo: https://github.com/psf/black
rev: 23.7.0
rev: 23.11.0
hooks:
- id: black
args:
Expand All @@ -20,13 +20,13 @@ repos:
exclude: ^(belay/snippets/)

- repo: https://github.com/asottile/blacken-docs
rev: 1.15.0
rev: 1.16.0
hooks:
- id: blacken-docs
exclude: ^(docs/source/How Belay Works.rst)

- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
rev: v4.5.0
hooks:
- id: check-added-large-files
- id: check-ast
Expand All @@ -53,21 +53,12 @@ repos:
exclude: \.(html|svg)$

- repo: https://github.com/fredrikaverpil/creosote.git
rev: v2.6.3
rev: v3.0.0
hooks:
- id: creosote
args:
- "--venv=.venv"
- "--paths=belay"
- "--deps-file=pyproject.toml"
- "--sections=tool.poetry.dependencies"
- "--exclude-deps"
- "importlib_resources"
- "pydantic"

- repo: https://github.com/codespell-project/codespell
rev: v2.2.5
rev: v2.2.6
hooks:
- id: codespell
exclude: ^(poetry.lock)
args: ["-L", "ser,"]
13 changes: 12 additions & 1 deletion belay/cli/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,12 @@
from .main import app
from belay.cli._cache import cache_app
from belay.cli._clean import clean
from belay.cli._exec import exec
from belay.cli._info import info
from belay.cli._install import install
from belay.cli._new import new
from belay.cli._run import run
from belay.cli._select import select
from belay.cli._sync import sync
from belay.cli._terminal import terminal
from belay.cli._update import update
from belay.cli.main import app
52 changes: 31 additions & 21 deletions belay/cli/cache.py → belay/cli/_cache.py
Original file line number Diff line number Diff line change
@@ -1,33 +1,41 @@
import builtins
import contextlib
import shutil

import questionary
from cyclopts import App, Parameter
from typing_extensions import Annotated

with contextlib.suppress(ImportError):
import readline
import shutil

import typer
from typer import Argument, Option, Typer

from belay.cli.main import app
from belay.project import find_cache_folder

app = Typer(no_args_is_help=True, help="Perform action's on Belay's cache.")
app.command(cache_app := App(name="cache", help="Perform action's on Belay's cache."))


@app.command()
@cache_app.command()
def clear(
prefix: str = Argument("", help="Clear all caches that start with this."),
yes: bool = Option(
False,
"--yes",
"-y",
help='Automatically answer "yes" to all confirmation prompts.',
),
all: bool = Option(False, "--all", "-a", help="Clear all caches."),
prefix: str = "",
*,
yes: Annotated[bool, Parameter(name=["--yes", "-y"])] = False,
all_: Annotated[bool, Parameter(name=["--all", "-a"])] = False,
):
"""Clear cache."""
if (not prefix and not all) or (prefix and all):
"""Clear cache.

Parameters
----------
prefix: str
Clear all caches that start with this.
yes: bool
Skip interactive prompts confirming clear action.
all_: bool
Clear all caches.
"""
if (not prefix and not all_) or (prefix and all_):
print('Either provide a prefix OR set the "--all" flag.')
raise typer.Exit()
return 1

cache_folder = find_cache_folder()

Expand All @@ -37,13 +45,15 @@ def clear(

if not cache_paths:
print(f'No caches found starting with "{prefix}"')
raise typer.Exit()
return 0

if not yes:
print("Found caches:")
for cache_name in cache_names:
print(f" • {cache_name}")
typer.confirm("Clear these caches?", abort=True)
confirmed = questionary.confirm("Clear these caches?").ask()
if not confirmed:
return 0

for path in cache_paths:
if path.is_file():
Expand All @@ -52,7 +62,7 @@ def clear(
shutil.rmtree(path)


@app.command()
@cache_app.command()
def list():
"""List cache elements."""
cache_folder = find_cache_folder()
Expand All @@ -62,7 +72,7 @@ def list():
print(item)


@app.command()
@cache_app.command()
def info():
"""Display cache location and size."""
cache_folder = find_cache_folder()
Expand Down
2 changes: 2 additions & 0 deletions belay/cli/clean.py → belay/cli/_clean.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import shutil

from belay.cli.main import app
from belay.project import find_dependencies_folder, load_groups


@app.command
def clean():
"""Remove any downloaded dependencies if they are no longer specified in pyproject."""
groups = load_groups()
Expand Down
25 changes: 25 additions & 0 deletions belay/cli/_exec.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from typing import Optional

from belay import Device
from belay.cli.common import remove_stacktrace
from belay.cli.main import app


@app.command
def exec(port: str, statement: str, *, password: Optional[str] = None):
"""Execute python statement on-device.

Parameters
----------
port: str
Port (like /dev/ttyUSB0) or WebSocket (like ws://192.168.1.100) of device.
statement: str
Statement to execute on-device.
password: Optional[str]
Password for communication methods (like WebREPL) that require authentication.
"""
kwargs = {}
if password is not None:
kwargs["password"] = password
with Device(port, **kwargs) as device, remove_stacktrace():
device(statement)
27 changes: 27 additions & 0 deletions belay/cli/_info.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from typing import Optional

from belay import Device
from belay.cli.main import app


@app.command
def info(
port: str,
*,
password: Optional[str] = None,
):
"""Display device firmware information.

Parameters
----------
port: str
Port (like /dev/ttyUSB0) or WebSocket (like ws://192.168.1.100) of device.
password: Optional[str]
Password for communication methods (like WebREPL) that require authentication.
"""
kwargs = {}
if password is not None:
kwargs["password"] = password
with Device(port, **kwargs) as device:
version_str = "v" + ".".join(str(x) for x in device.implementation.version)
print(f"{device.implementation.name} {version_str} - {device.implementation.platform}")
51 changes: 39 additions & 12 deletions belay/cli/install.py → belay/cli/_install.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,36 +4,63 @@
from tempfile import TemporaryDirectory
from typing import List, Optional

from cyclopts import Parameter
from rich.progress import Progress
from typer import Argument, Option
from typing_extensions import Annotated

from belay import Device
from belay.cli.common import help_password, help_port, remove_stacktrace
from belay.cli.sync import sync_device as _sync_device
from belay.cli._sync import sync_device as _sync_device
from belay.cli.common import remove_stacktrace
from belay.cli.main import app
from belay.project import find_project_folder, load_groups, load_pyproject


@app.command
def install(
port: str = Argument(..., help=help_port),
password: str = Option("", help=help_password),
mpy_cross_binary: Optional[Path] = Option(None, help="Compile py files with this executable."),
run: Optional[Path] = Option(None, help="Run script on-device after installing."),
main: Optional[Path] = Option(None, help="Sync script to /main.py after installing."),
with_groups: List[str] = Option(None, "--with", help="Include specified optional dependency group."),
follow: bool = Option(False, "--follow", "-f", help="Follow the stdout after upload."),
port: str,
*,
password: Optional[str] = None,
mpy_cross_binary: Optional[Path] = None,
run: Optional[Path] = None,
main: Optional[Path] = None,
with_groups: Annotated[Optional[List[str]], Parameter(name="--with")] = None,
follow: Annotated[bool, Parameter(name=["--follow", "-f"])] = False,
):
"""Sync dependencies and project itself to device."""
"""Sync dependencies and project itself to device.

Parameters
----------
port: str
Port (like /dev/ttyUSB0) or WebSocket (like ws://192.168.1.100) of device.
password: Optional[str]
Password for communication methods (like WebREPL) that require authentication.
mpy_cross_binary: Optional[Path]
Compile py files with this executable.
run: Optional[Path]
Run script on-device after installing.
main: Optional[Path]
Sync script to /main.py after installing.
with_groups: List[str]
Include specified optional dependency group.
follow: bool
Follow the stdout after upload.
"""
kwargs = {}
if run and run.suffix != ".py":
raise ValueError("Run script MUST be a python file.")
if main and main.suffix != ".py":
raise ValueError("Main script MUST be a python file.")
if with_groups is None:
with_groups = []
if password is not None:
kwargs["password"] = password

config = load_pyproject()
project_folder = find_project_folder()
project_package = config.name
groups = load_groups()

with Device(port, password=password) as device:
with Device(port, **kwargs) as device:
sync_device = partial(
_sync_device,
device,
Expand Down
14 changes: 11 additions & 3 deletions belay/cli/new.py → belay/cli/_new.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,24 @@
from pathlib import Path

from packaging.utils import canonicalize_name
from typer import Argument, Option

if sys.version_info < (3, 9, 0):
import importlib_resources
else:
import importlib.resources as importlib_resources

from belay.cli.main import app

def new(project_name: str = Argument(..., help="Project Name.")):
"""Create a new micropython project structure."""

@app.command
def new(project_name: str):
"""Create a new micropython project structure.

Parameters
----------
project_name: str
New project name.
"""
package_name = canonicalize_name(project_name)
dst_dir = Path() / project_name
template_dir = importlib_resources.files("belay") / "cli" / "new_template"
Expand Down
43 changes: 43 additions & 0 deletions belay/cli/_run.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from pathlib import Path
from typing import Optional

from belay import Device
from belay.cli.common import remove_stacktrace
from belay.cli.main import app


@app.command
def run(
port: str,
file: Path,
*,
password: Optional[str] = None,
):
"""Run file on-device.

If the first argument, ``port``, is resolvable to an executable,
the remainder of the command will be interpreted as a shell command
that will be executed in a pseudo-micropython-virtual-environment.
As of right now, this just sets ``MICROPYPATH`` to all of the dependency
groups' folders.

.. code-block:: console

$ belay run micropython -m unittest

Parameters
----------
port: str
Port (like /dev/ttyUSB0) or WebSocket (like ws://192.168.1.100) of device.
file: Path
File to run on-device.
password: Optional[str]
Password for communication methods (like WebREPL) that require authentication.
"""
kwargs = {}
if password is not None:
kwargs["password"] = password

content = file.read_text()
with Device(port, **kwargs) as device, remove_stacktrace():
device(content)
Loading
Loading