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

feat: add init service and init command #522

Merged
merged 5 commits into from
Oct 22, 2024
Merged
Show file tree
Hide file tree
Changes from 3 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
13 changes: 6 additions & 7 deletions craft_application/commands/__init__.py
mr-cal marked this conversation as resolved.
Show resolved Hide resolved
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)
mr-cal marked this conversation as resolved.
Show resolved Hide resolved
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:
mr-cal marked this conversation as resolved.
Show resolved Hide resolved
"""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)
mr-cal marked this conversation as resolved.
Show resolved Hide resolved

# 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
lengau marked this conversation as resolved.
Show resolved Hide resolved

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
Loading