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,
last_commit,
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
directory = cruft_state.get("directory", "")
if directory:
directory = str(Path("repo") / directory)
else:
directory = "repo"
with AltTemporaryDirectory(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)
link
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,
last_commit,
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('.'),
template_path: Union[pathlib.Path, NoneType] = None,
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("."),
template_path: Optional[Path] = None,
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"
if template_path is None:
template_git_str = cruft_state["template"]
else:
template_git_str = utils.cookiecutter.resolve_template_url(str(template_path))
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(template_git_str, 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