projectsyn/commodore

View on GitHub
commodore/component/__init__.py

Summary

Maintainability
A
0 mins
Test Coverage
A
99%
from __future__ import annotations

from collections.abc import Iterable
from pathlib import Path as P
from typing import Optional

import _jsonnet
import click
import git

from commodore.gitrepo import GitRepo
from commodore.multi_dependency import MultiDependency


class Component:
    _name: str
    _repo: Optional[GitRepo]
    _dependency: Optional[MultiDependency] = None
    _version: Optional[str] = None
    _dir: P
    _sub_path: str

    @classmethod
    def clone(cls, cfg, clone_url: str, name: str, version: str = "master"):
        cdep = cfg.register_dependency_repo(clone_url)
        c = Component(
            name,
            cdep,
            directory=component_dir(cfg.work_dir, name),
            version=version,
        )
        c.checkout()
        return c

    # pylint: disable=too-many-arguments
    def __init__(
        self,
        name: str,
        dependency: Optional[MultiDependency],
        work_dir: Optional[P] = None,
        version: Optional[str] = None,
        directory: Optional[P] = None,
        sub_path: str = "",
    ):
        self._name = name
        if directory:
            self._dir = directory
        elif work_dir:
            self._dir = component_dir(work_dir, self.name)
        else:
            raise click.ClickException(
                "Either `work_dir` or `directory` must be provided."
            )
        if dependency:
            self._dependency = dependency
            self._dependency.register_component(self.name, self._dir)
        self.version = version
        self._sub_path = sub_path
        self._repo = None

    @property
    def name(self) -> str:
        return self._name

    @property
    def repo(self) -> GitRepo:
        if not self._repo:
            if self._dependency:
                dep_repo = self._dependency.bare_repo
                author_name = dep_repo.author.name
                author_email = dep_repo.author.email
            else:
                # Fall back to author detection if we don't have a dependency
                author_name = None
                author_email = None
            self._repo = GitRepo(
                None,
                self._dir,
                author_name=author_name,
                author_email=author_email,
            )
        return self._repo

    @property
    def dependency(self) -> MultiDependency:
        if self._dependency is None:
            raise ValueError(
                f"Dependency for component {self._name} hasn't been initialized"
            )
        return self._dependency

    @dependency.setter
    def dependency(self, dependency: Optional[MultiDependency]):
        """Update the GitRepo backing the component"""
        if self._dependency:
            self._dependency.deregister_component(self.name)
        if dependency:
            dependency.register_component(self.name, self._dir)
        self._dependency = dependency
        # Clear worktree GitRepo wrapper when we update the component's backing
        # dependency. The new GitRepo wrapper will be created on the nex access of
        # `repo`.
        self._repo = None

    @property
    def repo_url(self) -> str:
        if self._dependency is None:
            raise ValueError(
                f"Dependency for component {self._name} hasn't been initialized"
            )
        return self._dependency.url

    @property
    def version(self) -> Optional[str]:
        return self._version

    @version.setter
    def version(self, version: str):
        self._version = version

    @property
    def repo_directory(self) -> P:
        return self._dir

    @property
    def target_directory(self) -> P:
        return self._dir / self._sub_path

    @property
    def target_dir(self) -> P:
        return self.target_directory

    @property
    def class_file(self) -> P:
        return self.target_directory / "class" / f"{self.name}.yml"

    @property
    def defaults_file(self) -> P:
        return self.target_directory / "class" / "defaults.yml"

    @property
    def lib_files(self) -> Iterable[P]:
        # NOTE(sg): Usage of yield makes the whole function return a generator.
        # So if the top-level condition is false, this should immediately raise
        # a StopIteration exception. Mypy has started to complain about the
        # `return []` that was previously part of this function.
        lib_dir = self.target_directory / "lib"
        if lib_dir.exists():
            for e in lib_dir.iterdir():
                # Skip hidden files in lib directory
                if not e.name.startswith("."):
                    yield e

    def get_library(self, libname: str) -> Optional[P]:
        lib_dir = self.target_directory / "lib"
        if not lib_dir.exists():
            return None

        for f in self.lib_files:
            if f.absolute() == P(lib_dir / libname).absolute():
                return f.absolute()

        return None

    @property
    def parameters_key(self):
        return component_parameters_key(self.name)

    def checkout(self):
        if self._dependency is None:
            raise ValueError(
                f"Dependency for component {self._name} hasn't been initialized"
            )
        self._dependency.checkout_component(self.name, self.version)

    def checkout_is_dirty(self) -> bool:
        if self._dependency:
            dep_repo = self._dependency.bare_repo
            author_name = dep_repo.author.name
            author_email = dep_repo.author.email
            worktree = self._dependency.get_component(self.name)
        else:
            author_name = None
            author_email = None
            worktree = self.target_dir

        if worktree and worktree.is_dir():
            r = GitRepo(
                None, worktree, author_name=author_name, author_email=author_email
            )
            return r.repo.is_dirty()
        else:
            return False

    def render_jsonnetfile_json(self, component_params):
        """
        Render jsonnetfile.json from jsonnetfile.jsonnet
        """
        jsonnetfile_jsonnet = self._dir / "jsonnetfile.jsonnet"
        jsonnetfile_json = self._dir / "jsonnetfile.json"
        if jsonnetfile_jsonnet.is_file():
            try:
                if jsonnetfile_json.name in self.repo.repo.tree():
                    click.secho(
                        f" > [WARN] Component {self.name} repo contains both jsonnetfile.json "
                        + "and jsonnetfile.jsonnet, continuing with jsonnetfile.jsonnet",
                        fg="yellow",
                    )
            except git.InvalidGitRepositoryError:
                pass
            # pylint: disable=c-extension-no-member
            output = _jsonnet.evaluate_file(
                str(jsonnetfile_jsonnet),
                ext_vars=component_params.get("jsonnetfile_parameters", {}),
            )
            with open(self._dir / "jsonnetfile.json", "w", encoding="utf-8") as fp:
                fp.write(output)
                fp.write("\n")


def component_dir(work_dir: P, name: str) -> P:
    return work_dir / "dependencies" / name


def component_parameters_key(name: str) -> str:
    return name.replace("-", "_")