1#  Copyright (c) 2019 Red Hat, Inc.
2#
3#  Permission is hereby granted, free of charge, to any person obtaining a copy
4#  of this software and associated documentation files (the "Software"), to
5#  deal in the Software without restriction, including without limitation the
6#  rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
7#  sell copies of the Software, and to permit persons to whom the Software is
8#  furnished to do so, subject to the following conditions:
9#
10#  The above copyright notice and this permission notice shall be included in
11#  all copies or substantial portions of the Software.
12#
13#  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14#  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15#  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16#  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17#  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
18#  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
19#  DEALINGS IN THE SOFTWARE.
20"""Linter module for pre-commit related code."""
21
22import os
23
24import sh
25
26from molecule import logger
27from molecule import util
28from molecule.verifier.lint import base
29
30LOG = logger.get_logger(__name__)
31
32
33class PreCommit(base.Base):
34    """
35    Pre-commit tool verifier wrapper.
36
37    This class is used to lint files by executing the pre-commit
38    command line tool for files in the test folder with a prefix
39    of ``test_``.
40
41    `Pre-Commit`_ is not the default verifier linter.
42
43    `Pre-Commit`_ is a linter for python files and more.
44
45    Additional options can be passed to ``pre-commit`` through the options
46    dict.  Any option set in this section will override the defaults.
47
48    .. code-block:: yaml
49
50        verifier:
51          name: testinfra
52          lint:
53            name: pre-commit
54            options:
55              remove-tabs:
56
57    Test file linting can be disabled by setting ``enabled`` to False.
58
59    .. code-block:: yaml
60
61        verifier:
62          name: testinfra
63          lint:
64            name: pre-commit
65            enabled: False
66
67    Environment variables can be passed to lint.
68
69    .. code-block:: yaml
70
71        verifier:
72          name: testinfra
73          lint:
74            name: pre-commit
75            env:
76              FOO: bar
77
78    Example pre-commit configuration file (``.pre-commit-config.yaml``) to run
79    flake8.
80
81    .. code-block:: yaml
82
83        repos:
84          - repo: local
85            hooks:
86              - id: flake8
87                name: flake8
88                entry: python -m flake8 --max-line-length=120
89                language: system
90                types: [python]
91
92    .. _`Pre-Commit`: https://pre-commit.com/
93    """
94
95    def __init__(self, config):
96        """
97        Set up the requirements to execute ``pre-commit`` tool.
98
99        :param config: An instance of a Molecule config.
100        """
101        super(PreCommit, self).__init__(config)
102        self._precommit_command = None
103        if config:
104            self._tests = self._get_tests()
105
106    @property
107    def default_options(self):
108        """Default options for pre-commit tool runtime."""
109        return {}
110
111    @property
112    def default_env(self):
113        """Default environment variables for pre-commit tool runtime."""
114        return util.merge_dicts(os.environ.copy(), self._config.env)
115
116    def bake(self):
117        """Bake a ready to execute ``pre-commit`` command."""
118        self._precommit_command = sh.Command('pre-commit').bake(
119            'run',
120            self.options,
121            '--files',
122            self._tests,
123            _env=self.env,
124            _out=LOG.out,
125            _err=LOG.error,
126        )
127
128    def execute(self):
129        """Execute the pre-commit command."""
130        if not self.enabled:
131            msg = 'Skipping, verifier_lint is disabled.'
132            LOG.warn(msg)
133            return
134
135        if not self._tests:
136            msg = 'Skipping, no tests found.'
137            LOG.warn(msg)
138            return
139
140        if self._precommit_command is None:
141            self.bake()
142
143        msg = 'Executing pre-commit on files found in {}/...'.format(
144            self._config.verifier.directory
145        )
146        LOG.info(msg)
147
148        try:
149            util.run_command(self._precommit_command, debug=self._config.debug)
150            msg = 'Lint completed successfully.'
151            LOG.success(msg)
152        except sh.ErrorReturnCode as e:
153            util.sysexit(e.exit_code)
154
155    def _get_tests(self):
156        """
157        Get a list of test files from the verifier's directory.
158
159        :return: list
160        """
161        return list(util.os_walk(self._config.verifier.directory, 'test_*.py'))
162