Skip to content

Commit

Permalink
feat: add init service and init command (#522)
Browse files Browse the repository at this point in the history
Signed-off-by: Callahan Kovacs <[email protected]>
Co-authored-by: Dariusz Duda <[email protected]>
  • Loading branch information
mr-cal and dariuszd21 authored Oct 22, 2024
1 parent 33bc6f6 commit f2a4bec
Show file tree
Hide file tree
Showing 19 changed files with 1,393 additions and 44 deletions.
13 changes: 6 additions & 7 deletions craft_application/commands/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,16 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""Command classes for a craft application."""

from craft_application.commands.base import AppCommand, ExtensibleCommand
from craft_application.commands import lifecycle
from craft_application.commands.lifecycle import (
get_lifecycle_command_group,
LifecycleCommand,
)
from craft_application.commands.other import get_other_command_group
from .base import AppCommand, ExtensibleCommand
from . import lifecycle
from .init import InitCommand
from .lifecycle import get_lifecycle_command_group, LifecycleCommand
from .other import get_other_command_group

__all__ = [
"AppCommand",
"ExtensibleCommand",
"InitCommand",
"lifecycle",
"LifecycleCommand",
"get_lifecycle_command_group",
Expand Down
144 changes: 144 additions & 0 deletions craft_application/commands/init.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
# Copyright 2024 Canonical Ltd.
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License version 3, as
# published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
# SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE.
# See the GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
"""Command to initialise a project."""

from __future__ import annotations

import argparse
import importlib.resources
import pathlib
from textwrap import dedent
from typing import cast

import craft_cli

from craft_application.util import humanize_list

from . import base


class InitCommand(base.AppCommand):
"""Command to create initial project files.
The init command should always produce a working and ready-to-build project.
"""

name = "init"
help_msg = "Create an initial project filetree"
overview = dedent(
"""
Initialise a project.
If '<project-dir>' is provided, initialise in that directory,
otherwise initialise in the current working directory.
If '--name <name>' is provided, the project will be named '<name>'.
Otherwise, the project will be named after the directory it is initialised in.
'--profile <profile>' is used to initialise the project for a specific use case.
Init can work in an existing project directory. If there are any files in the
directory that would be overwritten, then init command will fail.
"""
)
common = True

default_profile = "simple"
"""The default profile to use when initialising a project."""

def fill_parser(self, parser: argparse.ArgumentParser) -> None:
"""Specify command's specific parameters."""
parser.add_argument(
"project_dir",
type=pathlib.Path,
nargs="?",
default=None,
help="Path to initialise project in; defaults to current working directory.",
)
parser.add_argument(
"--name",
type=str,
default=None,
help="The name of project; defaults to the name of <project_dir>",
)
# TODO: this fails to render in `--help` (#530)
parser.add_argument(
"--profile",
type=str,
choices=self.profiles,
default=self.default_profile,
help=(
f"Use the specified project profile (default is {self.default_profile}, "
f"choices are {humanize_list(self.profiles, 'and')})"
),
)

@property
def parent_template_dir(self) -> pathlib.Path:
"""Return the path to the directory that contains all templates."""
with importlib.resources.path(
self._app.name, "templates"
) as _parent_template_dir:
return _parent_template_dir

@property
def profiles(self) -> list[str]:
"""A list of profile names generated from template directories."""
template_dirs = [
path for path in self.parent_template_dir.iterdir() if path.is_dir()
]
return sorted([template.name for template in template_dirs])

def run(self, parsed_args: argparse.Namespace) -> None:
"""Run the command."""
project_dir = self._get_project_dir(parsed_args)
project_name = self._get_name(parsed_args)
template_dir = pathlib.Path(self.parent_template_dir / parsed_args.profile)

craft_cli.emit.progress("Checking project directory.")
self._services.init.check_for_existing_files(
project_dir=project_dir, template_dir=template_dir
)

craft_cli.emit.progress("Initialising project.")
self._services.init.initialise_project(
project_dir=project_dir,
project_name=project_name,
template_dir=template_dir,
)
craft_cli.emit.message("Successfully initialised project.")

def _get_name(self, parsed_args: argparse.Namespace) -> str:
"""Get name of the package that is about to be initialised.
Check if name is set explicitly or fallback to project_dir.
"""
if parsed_args.name is not None:
return cast(str, parsed_args.name)
return self._get_project_dir(parsed_args).name

@staticmethod
def _get_project_dir(parsed_args: argparse.Namespace) -> pathlib.Path:
"""Get project dir where project should be initialised.
It applies rules in the following order:
- if <project_dir> is specified explicitly, it returns <project_dir>
- if <project_dir> is undefined, it defaults to cwd
"""
# if set explicitly, just return it
if parsed_args.project_dir is not None:
return pathlib.Path(parsed_args.project_dir).expanduser().resolve()

# If both args are undefined, default to current dir
return pathlib.Path.cwd().resolve()
5 changes: 3 additions & 2 deletions craft_application/commands/other.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

from craft_cli import CommandGroup, emit

from craft_application.commands import base
from . import InitCommand, base

if TYPE_CHECKING: # pragma: no cover
import argparse
Expand All @@ -27,6 +27,7 @@
def get_other_command_group() -> CommandGroup:
"""Return the lifecycle related command group."""
commands: list[type[base.AppCommand]] = [
InitCommand,
VersionCommand,
]

Expand All @@ -37,7 +38,7 @@ def get_other_command_group() -> CommandGroup:


class VersionCommand(base.AppCommand):
"""Show the snapcraft version."""
"""Show the application version."""

name = "version"
help_msg = "Show the application version and exit"
Expand Down
4 changes: 4 additions & 0 deletions craft_application/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -246,3 +246,7 @@ def __init__( # (too many arguments)

class FetchServiceError(CraftError):
"""Errors related to the fetch-service."""


class InitError(CraftError):
"""Errors related to initialising a project."""
2 changes: 2 additions & 0 deletions craft_application/services/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from craft_application.services.config import ConfigService
from craft_application.services.fetch import FetchService
from craft_application.services.lifecycle import LifecycleService
from craft_application.services.init import InitService
from craft_application.services.package import PackageService
from craft_application.services.provider import ProviderService
from craft_application.services.remotebuild import RemoteBuildService
Expand All @@ -31,6 +32,7 @@
"ProjectService",
"ConfigService",
"LifecycleService",
"InitService",
"PackageService",
"ProviderService",
"RemoteBuildService",
Expand Down
Loading

0 comments on commit f2a4bec

Please sign in to comment.