Source code for pyscaffold.extensions.pre_commit

"""
Extension that generates configuration files for Yelp `pre-commit`_.

.. _pre-commit: https://pre-commit.com
"""
from functools import partial
from typing import List

from .. import shell, structure
from ..actions import Action, ActionParams, ScaffoldOpts, Structure
from ..exceptions import ShellCommandException
from ..file_system import chdir
from ..log import logger
from ..operations import FileOp, no_overwrite
from ..structure import AbstractContent, ResolvedLeaf
from ..templates import get_template
from . import Extension, venv

EXECUTABLE = "pre-commit"
CMD_OPT = "____command-pre_commit"  # we don't want this to be persisted
INSERT_AFTER = ".. _pyscaffold-notes:\n"

UPDATE_MSG = """
It is a good idea to update the hooks to the latest version:

    pre-commit autoupdate

Don't forget to tell your contributors to also install and use pre-commit.
"""

SUCCESS_MSG = "\nA pre-commit hook was installed in your repo." + UPDATE_MSG

ERROR_MSG = """
Impossible to install pre-commit automatically, please open an issue in
https://github.com/pyscaffold/pyscaffold/issues.
"""

INSTALL_MSG = f"""
A `.pre-commit-config.yaml` file was generated inside your project but in
order to make sure the hooks will run, please install `pre-commit`:

    cd {{project_path}}
    # it is a good idea to create and activate a virtualenv here
    pip install pre-commit
{UPDATE_MSG}
"""

README_NOTE = f"""
Making Changes & Contributing
=============================

This project uses `pre-commit`_, please make sure to install it before making any
changes::

    pip install pre-commit
    cd {{name}}
    pre-commit install
{UPDATE_MSG.replace(':', '::')}
.. _pre-commit: https://pre-commit.com/
"""


[docs]class PreCommit(Extension): """Generate pre-commit configuration file"""
[docs] def activate(self, actions: List[Action]) -> List[Action]: """Activate extension Args: actions (list): list of actions to perform Returns: list: updated list of actions """ actions = self.register(actions, add_files, after="define_structure") actions = self.register(actions, find_executable, after="define_structure") return self.register(actions, install, before="report_done")
[docs]def add_files(struct: Structure, opts: ScaffoldOpts) -> ActionParams: """Add .pre-commit-config.yaml file to structure Since the default template uses isort, this function also provides an initial version of .isort.cfg that can be extended by the user (it contains some useful skips, e.g. tox and venv) """ files: Structure = { ".pre-commit-config.yaml": (get_template("pre-commit-config"), no_overwrite()), ".isort.cfg": (get_template("isort_cfg"), no_overwrite()), } struct = structure.modify(struct, "README.rst", partial(add_instructions, opts)) return structure.merge(struct, files), opts
[docs]def find_executable(struct: Structure, opts: ScaffoldOpts) -> ActionParams: """Find the pre-commit executable that should run later in the next action... Or take advantage of the venv to install it... """ pre_commit = shell.get_command(EXECUTABLE) opts = opts.copy() if pre_commit: opts.setdefault(CMD_OPT, pre_commit) else: # We can try to add it for venv to install... it will only work if the user is # already creating a venv anyway. opts.setdefault("venv_install", []).extend(["pre-commit"]) return struct, opts
[docs]def install(struct: Structure, opts: ScaffoldOpts) -> ActionParams: """Attempts to install pre-commit in the project""" project_path = opts.get("project_path", "PROJECT_DIR") pre_commit = opts.get(CMD_OPT) or shell.get_command(EXECUTABLE, venv.get_path(opts)) # ^ try again after venv, maybe it was installed if pre_commit: try: with chdir(opts.get("project_path", "."), **opts): pre_commit("install", pretend=opts.get("pretend")) logger.warning(SUCCESS_MSG) return struct, opts except ShellCommandException: logger.error(ERROR_MSG, exc_info=True) logger.warning(INSTALL_MSG.format(project_path=project_path)) return struct, opts
[docs]def add_instructions( opts: ScaffoldOpts, content: AbstractContent, file_op: FileOp ) -> ResolvedLeaf: """Add pre-commit instructions to README""" text = structure.reify_content(content, opts) if text is not None: i = text.find(INSERT_AFTER) assert i > 0, f"{INSERT_AFTER!r} not found in README template:\n{text}" j = i + len(INSERT_AFTER) text = text[:j] + README_NOTE.format(**opts) + text[j:] return text, file_op