Skip to content

Module cruft

cruft

Allows you to maintain all the necessary cruft for packaging and building projects separate from the code you intentionally write. Built on-top of, and fully compatible with, CookieCutter.

View Source
"""**cruft**

Allows you to maintain all the necessary cruft for packaging and building projects separate from

the code you intentionally write. Built on-top of, and fully compatible with, CookieCutter.

"""

from cruft._commands import check, create, diff, link, update

from cruft._version import __version__

__all__ = ["create", "check", "diff", "update", "link", "__version__"]

Sub-modules

Variables

__version__

Functions

check

def check(
    project_dir: pathlib.Path = PosixPath('.'),
    checkout: Union[str, NoneType] = None,
    strict: bool = True
) -> bool

Checks to see if there have been any updates to the Cookiecutter template

used to generate this project.

View Source
@example()

def check(

    project_dir: Path = Path("."), checkout: Optional[str] = None, strict: bool = True

) -> bool:

    """Checks to see if there have been any updates to the Cookiecutter template

    used to generate this project."""

    cruft_file = utils.cruft.get_cruft_file(project_dir)

    cruft_state = json.loads(cruft_file.read_text())

    with AltTemporaryDirectory(cruft_state.get("directory")) as cookiecutter_template_dir:

        with utils.cookiecutter.get_cookiecutter_repo(

            cruft_state["template"],

            Path(cookiecutter_template_dir),

            checkout,

            filter="blob:none",

            no_checkout=True,

        ) as repo:

            last_commit = repo.head.object.hexsha

            if utils.cruft.is_project_updated(repo, cruft_state["commit"], last_commit, strict):

                typer.secho(

                    "SUCCESS: Good work! Project's cruft is up to date "

                    "and as clean as possible :).",

                    fg=typer.colors.GREEN,

                )

                return True

        typer.secho(

            "FAILURE: Project's cruft is out of date! Run `cruft update` to clean this mess up.",

            fg=typer.colors.RED,

        )

        return False

create

def create(
    template_git_url: str,
    output_dir: pathlib.Path = PosixPath('.'),
    config_file: Union[pathlib.Path, NoneType] = None,
    default_config: bool = False,
    extra_context: Union[Dict[str, Any], NoneType] = None,
    extra_context_file: Union[pathlib.Path, NoneType] = None,
    no_input: bool = True,
    directory: Union[str, NoneType] = None,
    checkout: Union[str, NoneType] = None,
    overwrite_if_exists: bool = False,
    skip: Union[List[str], NoneType] = None
) -> pathlib.Path

Expand a Git based Cookiecutter template into a new project on disk.

View Source
@example("https://github.com/timothycrosley/cookiecutter-python/")

def create(

    template_git_url: str,

    output_dir: Path = Path("."),

    config_file: Optional[Path] = None,

    default_config: bool = False,

    extra_context: Optional[Dict[str, Any]] = None,

    extra_context_file: Optional[Path] = None,

    no_input: bool = True,

    directory: Optional[str] = None,

    checkout: Optional[str] = None,

    overwrite_if_exists: bool = False,

    skip: Optional[List[str]] = None,

) -> Path:

    """Expand a Git based Cookiecutter template into a new project on disk."""

    template_git_url = utils.cookiecutter.resolve_template_url(template_git_url)

    with AltTemporaryDirectory(directory) as cookiecutter_template_dir_str:

        cookiecutter_template_dir = Path(cookiecutter_template_dir_str)

        with utils.cookiecutter.get_cookiecutter_repo(

            template_git_url, cookiecutter_template_dir, checkout

        ) as repo:

            last_commit = repo.head.object.hexsha

            if directory:

                cookiecutter_template_dir = cookiecutter_template_dir / directory

            if extra_context_file:

                extra_context = utils.cookiecutter.get_extra_context_from_file(extra_context_file)

            context = utils.cookiecutter.generate_cookiecutter_context(

                template_git_url,

                cookiecutter_template_dir,

                config_file,

                default_config,

                extra_context,

                no_input,

            )

        project_dir = Path(

            generate_files(

                repo_dir=cookiecutter_template_dir,

                context=context,

                overwrite_if_exists=overwrite_if_exists,

                output_dir=str(output_dir),

            )

        )

        cruft_content = {

            "template": template_git_url,

            "commit": last_commit,

            "checkout": checkout,

            "context": context,

            "directory": directory,

        }

        if skip:

            cruft_content["skip"] = skip

        # After generating the project - save the cruft state

        # into the cruft file.

        (project_dir / ".cruft.json").write_text(utils.cruft.json_dumps(cruft_content))

        return project_dir

diff

def diff(
    project_dir: pathlib.Path = PosixPath('.'),
    exit_code: bool = False,
    checkout: Union[str, NoneType] = None
) -> bool

Show the diff between the project and the linked Cookiecutter template

View Source
def diff(

    project_dir: Path = Path("."), exit_code: bool = False, checkout: Optional[str] = None

) -> bool:

    """Show the diff between the project and the linked Cookiecutter template"""

    cruft_file = utils.cruft.get_cruft_file(project_dir)

    cruft_state = json.loads(cruft_file.read_text())

    checkout = checkout or cruft_state.get("commit")

    has_diff = False

    with AltTemporaryDirectory(cruft_state.get("directory")) as tmpdir_:

        tmpdir = Path(tmpdir_)

        repo_dir = tmpdir / "repo"

        remote_template_dir = tmpdir / "remote"

        local_template_dir = tmpdir / "local"

        # Create all the directories

        remote_template_dir.mkdir(parents=True, exist_ok=True)

        local_template_dir.mkdir(parents=True, exist_ok=True)

        # Let's clone the template

        with utils.cookiecutter.get_cookiecutter_repo(

            cruft_state["template"], repo_dir, checkout=checkout

        ) as repo:

            # We generate the template for the revision expected by the project

            utils.generate.cookiecutter_template(

                output_dir=remote_template_dir,

                repo=repo,

                cruft_state=cruft_state,

                project_dir=project_dir,

                checkout=checkout,

                update_deleted_paths=True,

            )

        # Then we create a new tree with each file in the template that also exist

        # locally.

        for path in sorted(remote_template_dir.glob("**/*")):

            relative_path = path.relative_to(remote_template_dir)

            local_path = project_dir / relative_path

            destination = local_template_dir / relative_path

            if path.is_file():

                shutil.copy(str(local_path), str(destination))

            else:

                destination.mkdir(parents=True, exist_ok=True)

                destination.chmod(local_path.stat().st_mode)

        # Finally we can compute and print the diff.

        diff = utils.diff.get_diff(local_template_dir, remote_template_dir)

        if diff.strip():

            has_diff = True

            if exit_code or not sys.stdout.isatty():

                # The current shell doesn't run on a TTY or the "--exit-code" flag

                # is set. This means we're probably not displaying the diff to an

                # end-user. Let's just output the sanitized version of the diff.

                #

                # Note that we can't delegate this check to "git diff" command

                # because it would show absolute paths to files as we're working in

                # temporary, non-gitted directories. Doing so would prevent the user

                # from applying the patch later on as the temporary directories wouldn't

                # exist anymore.

                typer.echo(diff, nl=False)

            else:

                # We're outputing the diff to a real user. We can delegate the job

                # to git diff so that they can benefit from coloration and paging.

                # Ouputing absolute paths is less of a concern although it would be

                # better to find a way to make git shrink those paths.

                utils.diff.display_diff(local_template_dir, remote_template_dir)

    return not (has_diff and exit_code)
def link(
    template_git_url: str,
    project_dir: pathlib.Path = PosixPath('.'),
    checkout: Union[str, NoneType] = None,
    no_input: bool = True,
    config_file: Union[pathlib.Path, NoneType] = None,
    default_config: bool = False,
    extra_context: Union[Dict[str, Any], NoneType] = None,
    directory: Union[str, NoneType] = None
) -> bool

Links an existing project created from a template, to the template it was created from.

View Source
@example("https://github.com/timothycrosley/cookiecutter-python/")

def link(

    template_git_url: str,

    project_dir: Path = Path("."),

    checkout: Optional[str] = None,

    no_input: bool = True,

    config_file: Optional[Path] = None,

    default_config: bool = False,

    extra_context: Optional[Dict[str, Any]] = None,

    directory: Optional[str] = None,

) -> bool:

    """Links an existing project created from a template, to the template it was created from."""

    cruft_file = utils.cruft.get_cruft_file(project_dir, exists=False)

    template_git_url = utils.cookiecutter.resolve_template_url(template_git_url)

    with AltTemporaryDirectory(directory) as cookiecutter_template_dir_str:

        cookiecutter_template_dir = Path(cookiecutter_template_dir_str)

        with utils.cookiecutter.get_cookiecutter_repo(

            template_git_url, cookiecutter_template_dir, checkout

        ) as repo:

            last_commit = repo.head.object.hexsha

        if directory:

            cookiecutter_template_dir = cookiecutter_template_dir / directory

        context = utils.cookiecutter.generate_cookiecutter_context(

            template_git_url,

            cookiecutter_template_dir,

            config_file,

            default_config,

            extra_context,

            no_input,

        )

        if no_input:

            use_commit = last_commit

        else:

            typer.echo(

                f"Linking against the commit: {last_commit}"

                f" which corresponds with the git reference: {checkout}"

            )

            typer.echo("Press enter to link against this commit or provide an alternative commit.")

            use_commit = typer.prompt("Link to template at commit", default=last_commit)

        cruft_file.write_text(

            utils.cruft.json_dumps(

                {

                    "template": template_git_url,

                    "commit": use_commit,

                    "checkout": checkout,

                    "context": context,

                    "directory": directory,

                }

            )

        )

        return True

update

def update(
    project_dir: pathlib.Path = PosixPath('.'),
    cookiecutter_input: bool = False,
    refresh_private_variables: bool = False,
    skip_apply_ask: bool = True,
    skip_update: bool = False,
    checkout: Union[str, NoneType] = None,
    strict: bool = True,
    allow_untracked_files: bool = False,
    extra_context: Union[Dict[str, Any], NoneType] = None,
    extra_context_file: Union[pathlib.Path, NoneType] = None
) -> bool

Update specified project's cruft to the latest and greatest release.

View Source
@example(skip_apply_ask=False)

@example()

def update(

    project_dir: Path = Path("."),

    cookiecutter_input: bool = False,

    refresh_private_variables: bool = False,

    skip_apply_ask: bool = True,

    skip_update: bool = False,

    checkout: Optional[str] = None,

    strict: bool = True,

    allow_untracked_files: bool = False,

    extra_context: Optional[Dict[str, Any]] = None,

    extra_context_file: Optional[Path] = None,

) -> bool:

    """Update specified project's cruft to the latest and greatest release."""

    cruft_file = utils.cruft.get_cruft_file(project_dir)

    if extra_context_file:

        if extra_context_file.samefile(cruft_file):

            typer.secho(

                f"The file path given to --variables-to-update-file cannot be the same as the"

                f" project's cruft file ({cruft_file}), as the update process needs"

                f" to know the old/original values of variables as well. Please specify a"

                f" different path, and the project's cruft file will be updated as"

                f" part of the process.",

                fg=typer.colors.RED,

            )

            return False

        extra_context_from_cli = extra_context

        with open(extra_context_file, "r") as extra_context_fp:

            extra_context = json.load(extra_context_fp) or {}

        extra_context = extra_context.get("context") or {}

        extra_context = extra_context.get("cookiecutter") or {}

        if extra_context_from_cli:

            extra_context.update(extra_context_from_cli)

    # If the project dir is a git repository, we ensure

    # that the user has a clean working directory before proceeding.

    if not _is_project_repo_clean(project_dir, allow_untracked_files):

        typer.secho(

            "Cruft cannot apply updates on an unclean git project."

            " Please make sure your git working tree is clean before proceeding.",

            fg=typer.colors.RED,

        )

        return False

    cruft_state = json.loads(cruft_file.read_text())

    directory = cruft_state.get("directory", "")

    if directory:

        directory = str(Path("repo") / directory)

    else:

        directory = "repo"

    with AltTemporaryDirectory(directory) as tmpdir_:

        # Initial setup

        tmpdir = Path(tmpdir_)

        repo_dir = tmpdir / "repo"

        current_template_dir = tmpdir / "current_template"

        new_template_dir = tmpdir / "new_template"

        deleted_paths: Set[Path] = set()

        # Clone the template

        with utils.cookiecutter.get_cookiecutter_repo(

            cruft_state["template"], repo_dir, checkout

        ) as repo:

            last_commit = repo.head.object.hexsha

            # Bail early if the repo is already up to date and no inputs are asked

            if not (

                extra_context or cookiecutter_input or refresh_private_variables

            ) and utils.cruft.is_project_updated(repo, cruft_state["commit"], last_commit, strict):

                typer.secho(

                    "Nothing to do, project's cruft is already up to date!", fg=typer.colors.GREEN

                )

                return True

            # Generate clean outputs via the cookiecutter

            # from the current cruft state commit of the cookiecutter and the updated

            # cookiecutter.

            # For the current cruft state, we do not try to update the cookiecutter_input

            # because we want to keep the current context input intact.

            _ = utils.generate.cookiecutter_template(

                output_dir=current_template_dir,

                repo=repo,

                cruft_state=cruft_state,

                project_dir=project_dir,

                checkout=cruft_state["commit"],

                deleted_paths=deleted_paths,

                update_deleted_paths=True,

            )

            # Remove private variables from cruft_state to refresh their values

            # from the cookiecutter template config

            if refresh_private_variables:

                _clean_cookiecutter_private_variables(cruft_state)

            # Add new input data from command line to cookiecutter context

            if extra_context:

                extra = cruft_state["context"]["cookiecutter"]

                for k, v in extra_context.items():

                    extra[k] = v

            new_context = utils.generate.cookiecutter_template(

                output_dir=new_template_dir,

                repo=repo,

                cruft_state=cruft_state,

                project_dir=project_dir,

                cookiecutter_input=cookiecutter_input,

                checkout=last_commit,

                deleted_paths=deleted_paths,

            )

        # Given the two versions of the cookiecutter outputs based

        # on the current project's context we calculate the diff and

        # apply the updates to the current project.

        if _apply_project_updates(

            current_template_dir,

            new_template_dir,

            project_dir,

            skip_update,

            skip_apply_ask,

            allow_untracked_files,

        ):

            # Update the cruft state and dump the new state

            # to the cruft file

            cruft_state["commit"] = last_commit

            cruft_state["checkout"] = checkout

            cruft_state["context"] = new_context

            cruft_file.write_text(utils.cruft.json_dumps(cruft_state))

            typer.secho(

                "Good work! Project's cruft has been updated and is as clean as possible!",

                fg=typer.colors.GREEN,

            )

        return True