Skip to content

Commit

Permalink
Merge pull request #45 from tekktrik/fix/folder-creation
Browse files Browse the repository at this point in the history
Fix folder creation issue, add about command
  • Loading branch information
tekktrik committed Feb 25, 2024
2 parents 5cfe829 + 2892ade commit 7496d49
Show file tree
Hide file tree
Showing 9 changed files with 256 additions and 152 deletions.
2 changes: 0 additions & 2 deletions circfirm/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,9 @@
Author(s): Alec Delaney
"""


from circfirm.startup import setup_app_dir, setup_file, setup_folder

# Folders

APP_DIR = setup_app_dir("circfirm")
UF2_ARCHIVE = setup_folder(APP_DIR, "archive")

Expand Down
109 changes: 32 additions & 77 deletions circfirm/cli.py → circfirm/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@
Author(s): Alec Delaney
"""

import importlib.util
import os
import pathlib
import pkgutil
import shutil
import sys
import time
Expand All @@ -24,7 +25,7 @@
@click.group()
@click.version_option(package_name="circfirm")
def cli() -> None:
"""Install CircuitPython firmware from the command line."""
"""Manage CircuitPython firmware from the command line."""
circfirm.startup.ensure_app_setup()


Expand All @@ -44,6 +45,31 @@ def announce_and_await(
return resp


def load_subcmd_folder(path: str, super_import_name: str) -> None:
"""Load subcommands dynamically from a folder of modules and packages."""
subcmd_names = [
(modname, ispkg) for _, modname, ispkg in pkgutil.iter_modules((path,))
]
subcmd_paths = [
os.path.abspath(os.path.join(path, subcmd_name[0]))
for subcmd_name in subcmd_names
]

for (subcmd_name, ispkg), subcmd_path in zip(subcmd_names, subcmd_paths):
import_name = ".".join([super_import_name, subcmd_name])
import_path = subcmd_path if ispkg else subcmd_path + ".py"
module_spec = importlib.util.spec_from_file_location(import_name, import_path)
module = importlib.util.module_from_spec(module_spec)
module_spec.loader.exec_module(module)
source_cli: click.MultiCommand = getattr(module, "cli")
if isinstance(source_cli, click.Group):
subcmd = click.CommandCollection(sources=(source_cli,))
subcmd.help = source_cli.__doc__
else:
subcmd = source_cli
cli.add_command(subcmd, subcmd_name)


@cli.command()
@click.argument("version")
@click.option("-l", "--language", default="en_US", help="CircuitPython language/locale")
Expand Down Expand Up @@ -99,78 +125,7 @@ def install(version: str, language: str, board: Optional[str]) -> None:
click.echo("Device should reboot momentarily.")


@cli.group()
def cache():
"""Work with cached information."""


@cache.command()
@click.option("-b", "--board", default=None, help="CircuitPythonoard name")
@click.option("-v", "--version", default=None, help="CircuitPython version")
@click.option("-l", "--language", default=None, help="CircuitPython language/locale")
def clear(
board: Optional[str], version: Optional[str], language: Optional[str]
) -> None:
"""Clear the cache, either entirely or for a specific board/version."""
if board is None and version is None and language is None:
shutil.rmtree(circfirm.UF2_ARCHIVE)
circfirm.startup.ensure_app_setup()
click.echo("Cache cleared!")
return

glob_pattern = "*-*" if board is None else f"*-{board}"
language_pattern = "-*" if language is None else f"-{language}"
glob_pattern += language_pattern
version_pattern = "-*" if version is None else f"-{version}*"
glob_pattern += version_pattern
matching_files = pathlib.Path(circfirm.UF2_ARCHIVE).rglob(glob_pattern)
for matching_file in matching_files:
matching_file.unlink()

# Delete board folder if empty
for board_folder in pathlib.Path(circfirm.UF2_ARCHIVE).glob("*"):
if len(os.listdir(board_folder)) == 0:
shutil.rmtree(board_folder)

click.echo("Cache cleared of specified entries!")


@cache.command(name="list")
@click.option("-b", "--board", default=None, help="CircuitPython board name")
def cache_list(board: Optional[str]) -> None:
"""List all the boards/versions cached."""
board_list = os.listdir(circfirm.UF2_ARCHIVE)

if not board_list:
click.echo("Versions have not been cached yet for any boards.")
sys.exit(0)

if board is not None and board not in board_list:
click.echo(f"No versions for board '{board}' are not cached.")
sys.exit(0)

specified_board = board if board is not None else None
boards = circfirm.backend.get_sorted_boards(specified_board)

for rec_boardname, rec_boardvers in boards.items():
click.echo(f"{rec_boardname}")
for rec_boardver, rec_boardlangs in rec_boardvers.items():
for rec_boardlang in rec_boardlangs:
click.echo(f" * {rec_boardver} ({rec_boardlang})")


@cache.command(name="save")
@click.argument("board")
@click.argument("version")
@click.option("-l", "--language", default="en_US", help="CircuitPython language/locale")
def cache_save(board: str, version: str, language: str) -> None:
"""Install a version of CircuitPython to cache."""
try:
announce_and_await(
f"Caching firmware version {version} for {board}",
circfirm.backend.download_uf2,
args=(board, version, language),
)
except ConnectionError as err:
click.echo(" failed") # Mark as failed
raise click.exceptions.ClickException(err.args[0])
# Load extra commands from the rest of the circfirm.cli subpackage
cli_pkg_path = os.path.dirname(os.path.abspath(__file__))
cli_pkg_name = "circfirm.cli"
load_subcmd_folder(cli_pkg_path, cli_pkg_name)
16 changes: 16 additions & 0 deletions circfirm/cli/about.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# SPDX-FileCopyrightText: 2024 Alec Delaney, for Adafruit Industries
#
# SPDX-License-Identifier: MIT

"""CLI functionality for the about subcommand.
Author(s): Alec Delaney
"""

import click


@click.command()
def cli() -> None:
"""Information about circfirm."""
click.echo("Written by Alec Delaney, licensed under MIT License.")
98 changes: 98 additions & 0 deletions circfirm/cli/cache.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# SPDX-FileCopyrightText: 2024 Alec Delaney, for Adafruit Industries
#
# SPDX-License-Identifier: MIT

"""CLI functionality for the cache subcommand.
Author(s): Alec Delaney
"""

import os
import pathlib
import shutil
import sys
from typing import Optional

import click

import circfirm
import circfirm.backend
import circfirm.cli
import circfirm.startup


@click.group()
def cli():
"""Work with cached firmwares."""


@cli.command()
@click.option("-b", "--board", default=None, help="CircuitPythonoard name")
@click.option("-v", "--version", default=None, help="CircuitPython version")
@click.option("-l", "--language", default=None, help="CircuitPython language/locale")
def clear(
board: Optional[str], version: Optional[str], language: Optional[str]
) -> None:
"""Clear the cache, either entirely or for a specific board/version."""
if board is None and version is None and language is None:
shutil.rmtree(circfirm.UF2_ARCHIVE)
circfirm.startup.ensure_app_setup()
click.echo("Cache cleared!")
return

glob_pattern = "*-*" if board is None else f"*-{board}"
language_pattern = "-*" if language is None else f"-{language}"
glob_pattern += language_pattern
version_pattern = "-*" if version is None else f"-{version}*"
glob_pattern += version_pattern
matching_files = pathlib.Path(circfirm.UF2_ARCHIVE).rglob(glob_pattern)
for matching_file in matching_files:
matching_file.unlink()

# Delete board folder if empty
for board_folder in pathlib.Path(circfirm.UF2_ARCHIVE).glob("*"):
if len(os.listdir(board_folder)) == 0:
shutil.rmtree(board_folder)

click.echo("Cache cleared of specified entries!")


@cli.command(name="list")
@click.option("-b", "--board", default=None, help="CircuitPython board name")
def cache_list(board: Optional[str]) -> None:
"""List all the boards/versions cached."""
board_list = os.listdir(circfirm.UF2_ARCHIVE)

if not board_list:
click.echo("Versions have not been cached yet for any boards.")
sys.exit(0)

if board is not None and board not in board_list:
click.echo(f"No versions for board '{board}' are not cached.")
sys.exit(0)

specified_board = board if board is not None else None
boards = circfirm.backend.get_sorted_boards(specified_board)

for rec_boardname, rec_boardvers in boards.items():
click.echo(f"{rec_boardname}")
for rec_boardver, rec_boardlangs in rec_boardvers.items():
for rec_boardlang in rec_boardlangs:
click.echo(f" * {rec_boardver} ({rec_boardlang})")


@cli.command(name="save")
@click.argument("board")
@click.argument("version")
@click.option("-l", "--language", default="en_US", help="CircuitPython language/locale")
def cache_save(board: str, version: str, language: str) -> None:
"""Install a version of CircuitPython to cache."""
try:
circfirm.cli.announce_and_await(
f"Caching firmware version {version} for {board}",
circfirm.backend.download_uf2,
args=(board, version, language),
)
except ConnectionError as err:
click.echo(" failed") # Mark as failed
raise click.exceptions.ClickException(err.args[0])
8 changes: 2 additions & 6 deletions circfirm/startup.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,13 @@
def setup_app_dir(app_name: str) -> str:
"""Set up the application directory."""
app_path = click.get_app_dir(app_name)
FOLDER_LIST.append(app_name)
FOLDER_LIST.append(app_path)
return app_path


def setup_folder(*path_parts: str) -> str:
"""Add a folder to the global record list."""
folder_path = (
click.get_app_dir("circfirm")
if len(path_parts) == 1
else os.path.join(path_parts[0], *path_parts[1:])
)
folder_path = os.path.join(*path_parts)
FOLDER_LIST.append(folder_path)
return folder_path

Expand Down
4 changes: 3 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ Issues = "https://github.com/tekktrik/circfirm/issues"
circfirm = "circfirm.cli:cli"

[tool.setuptools]
packages = ["circfirm"]
packages = ["circfirm", "circfirm.cli"]
include-package-data = true

[tool.setuptools.dynamic.version]
Expand All @@ -67,6 +67,8 @@ file = "VERSION"
dependencies = {file = ["requirements.txt"]}
optional-dependencies = {dev = {file = ["requirements-dev.txt"]}}

[tool.setuptools_scm]

[tool.coverage.run]
source = [
"circfirm/",
Expand Down
21 changes: 21 additions & 0 deletions tests/cli/test_cli_about.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# SPDX-FileCopyrightText: 2024 Alec Delaney, for Adafruit Industries
#
# SPDX-License-Identifier: MIT

"""Tests the CLI functionality for about command.
Author(s): Alec Delaney
"""

from click.testing import CliRunner

from circfirm.cli import cli


def test_about() -> None:
"""Tests the about command."""
runner = CliRunner()

result = runner.invoke(cli, ["about"])
assert result.exit_code == 0
assert result.output == "Written by Alec Delaney, licensed under MIT License.\n"

0 comments on commit 7496d49

Please sign in to comment.