diff --git a/CHANGELOG.md b/CHANGELOG.md index 982a29bd..3b782c9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added + +* Introduction of autohooks modes. Modes configure how to handle loading + autohooks, plugins and dependencies when running the git hook. The + `pythonpath` mode requires to put the necessary python packages into the + **PYTHONPATH** manually. The `pipenv` mode uses pipenv to handle the + dependencies. Using the `pipenv` mode is recommended. + [#24](https://github.com/greenbone/autohooks/pull/24) + ### Changed + +* The installed git hook will fail now if autohooks can't be loaded. Before the + git hook raised only a warning and was ignored. This a major change compared + to the previous versions. To update existing installations it requires + overriding the installed git hook by running `autohooks activate --force`. + [#24](https://github.com/greenbone/autohooks/pull/24) + ### Deprecated ### Fixed ### Removed diff --git a/README.md b/README.md index 9b748179..22bf27a4 100644 --- a/README.md +++ b/README.md @@ -9,11 +9,13 @@ in Python - [Why?](#why) - [Solution](#solution) +- [Modes](#modes) + - [Pythonpath mode](#pythonpath-mode) + - [Pipenv mode](#pipenv-mode) - [Installation](#installation) - [Install autohooks python package](#install-autohooks-python-package) + - [Configure mode and plugins to be run](#configure-mode-and-plugins-to-be-run) - [Activating the git hooks](#activating-the-git-hooks) - - [Configure plugins to be run](#configure-plugins-to-be-run) -- [Proposed Workflow](#proposed-workflow) - [Plugins](#plugins) - [How-to write a Plugin](#how-to-write-a-plugin) - [Linting plugin](#linting-plugin) @@ -39,26 +41,65 @@ itself and doesn't install in the current environment. Autohooks is a pure python library that installs a minimal [executable git hook](https://github.com/greenbone/autohooks/blob/master/autohooks/precommit/template). -If autohooks isn't installed in your current python path the hooks aren't -executed. So autohooks is always opt-in by installing the package into your -current development environment. It would be even possible to run different -versions of autohooks by switching the environment. +It allows you to decide how to maintain your hook dependencies by supporting +different modes. -Autohooks doesn't interfere with your work. If autohooks can't be run or fails -executing a plugin, an error is shown only and the git hook will proceed. +## Modes + +Currently two modes for using autohooks are supported: + +* `pythonpath` and +* `pipenv` + +The modes handle how autohooks, the plugins and their dependencies are loaded +during git hook execution. + +If no mode is specified in the [`pyproject.toml` config file](#configure-mode-and-plugins-to-be-run) +and no mode is set during [activation](#activating-the-git-hooks), autohooks +will use the [pythonpath mode](#pythonpath-mode) by default. + +### Pythonpath mode + +In the `pythonpath` mode the user has to install autohooks, the desired +plugins and their dependencies into the [PYTHONPATH](https://docs.python.org/3/library/sys.html#sys.path) +manually. + +This can be achieved by running `pip install --user autohooks ...` to put them +into the installation directory of the [current user](https://docs.python.org/3/library/site.html#site.USER_SITE) +or with `pip install authooks ...` for a system wide installation. + +Alternatively a [virtual environment](https://packaging.python.org/tutorials/installing-packages/#creating-and-using-virtual-environments) +could be used, which separates the installation from your global and user wide +Python packages. + +It would also be possible to use [pipenv] for the management of the virtual +environment but the activation of the environment has to be done manually. + +Therefore it would be even possible to run different versions of autohooks by +using the `pythonpath` mode and switching a virtual environment. + +### Pipenv mode + +In the `pipenv` mode [pipenv] is used to run autohooks in a dedicated virtual +environment. Pipenv uses a lockfile to install exact versions. Therefore the +installation is deterministic and reliable between different developer setups. +In contrast to the `pythonpath` mode the activation of the virtual environment +provided by [pipenv] is done automatically in the background. + +Using the `pipenv` mode is highly recommended. ## Installation For the installation of autohooks three steps are necessary: -1. Install the autohooks package into your current environment -2. Activate the [git hooks](https://git-scm.com/docs/githooks) -3. Configure the plugins to be run +1. Install autohooks package into your current environment +3. Configure mode and plugins to be run +2. Activate [git hooks](https://git-scm.com/docs/githooks) ### Install autohooks python package -For installing the autohooks python package, using -[pipenv](https://pipenv.readthedocs.io/) is highly recommended. +For installing the autohooks python package, using [pipenv] is highly +recommended. To install autohooks as a development dependency run @@ -88,35 +129,25 @@ autohooks = {git = "https://github.com/greenbone/autohooks"} to the `[dev-packages]` section of your `Pipfile`. -### Activating the git hooks - -If autohooks is installed from git or a source tarball, the git hooks should be -activated automatically. The activation can be verified by running e.g. -`autohooks check`. +### Configure mode and plugins to be run -Installing autohooks from a [wheel](https://www.python.org/dev/peps/pep-0427/) -package will **NOT** activate the git commit hooks. - -To manually activate the git hooks you can run - -```sh -pipenv run autohooks activate -``` +Autohooks uses the *pyproject.toml* file specified in +[PEP518](https://www.python.org/dev/peps/pep-0518/) for its configuration. +Adding a *[tool.autohooks]* section allows to specify the desired [autohooks mode](#modes) +and to set python modules to be run as [autohooks plugin](#plugins). -### Configure plugins to be run +The mode can be set by adding a `mode =` line to the *pyproject.toml* file. +Current possible options are `"pythonpath"` and `"pipenv".`See +[autohooks mode](#modes) for more details. If the mode setting is missing it +falls back to `pythonpath` mode. To actually run an action on git hooks, [autohooks plugins](#plugins) have to be installed and configured. To install e.g. python linting via pylint run -``` +```bash pipenv install --dev autohooks-plugin-pylint ``` -Autohooks uses the *pyproject.toml* file specified in -[PEP518](https://www.python.org/dev/peps/pep-0518/) for its configuration. -Adding a *[tool.autohooks]* section allows to set python modules to be run on a -specific git hook. - Example *pyproject.toml*: ```toml @@ -125,49 +156,31 @@ requires = ["setuptools", "wheel"] [tool.autohooks] pre-commit = ["autohooks.plugins.black"] +mode = "pipenv" ``` -## Proposed Workflow - -Using [pipenv](https://pipenv.readthedocs.io/) allows to install all -dependencies and tools with a specific version into a virtual, easily removable -Python environment. Therefore it's best to maintain **autohooks** also via -pipenv. Because it is not required to build or run your software, it should be -[installed as a development dependency](#install-autohooks-python-package). -Installing and [activating](#activating-the-git-hooks) autohooks doesn't -actually run any check or formatting by itself. Therefore, it is required to -[choose and install a plugin](#configure-plugins-to-be-run). +### Activating the git hooks -If all these tasks have been resolved, the developers are able to install -and activate autohooks with only one single command from your project's git -repository: +If autohooks is installed from git or a source tarball, the git hooks should be +activated automatically. The activation can be verified by running e.g. +`autohooks check`. -```sh -pipenv install --dev -``` +Installing autohooks from a [wheel](https://www.python.org/dev/peps/pep-0427/) +package will **NOT** activate the git commit hooks automatically. -Because virtual environments are used for all dependencies including -autohooks, the linting, formatting, etc. can only by done when running -`git commit` within the virtual environment. +To manually activate the git hooks you can run -```sh -$ cd myproject -$ pipenv install --dev -$ pipenv shell -(myproject)$ git commit +```bash +pipenv run autohooks activate ``` -The advantage of this process is, if the user is not running `git commit` within -the active virtual environment, autohooks and its plugins are not executed. +Calling `activate` also allows for overriding the [mode](#modes) defined in the +*pyproject.toml* settings. E.g. -```sh -$ cd myproject -$ git commit +```bash +pipenv run autohooks activate --mode pipenv ``` -This allows the user to choose whether to execute the hooks by activating the -virtual environment or to ignore them by deactivating it. - ## Plugins * Python code formatting via [black](https://github.com/greenbone/autohooks-plugin-black) @@ -181,7 +194,7 @@ virtual environment or to ignore them by deactivating it. Plugins need to be available in the [Python import path](https://docs.python.org/3/reference/import.html). The easiest way to achieve this, is to upload a plugin to [PyPI](https://pypi.org/) -and install it via [pip]() or [pipenv](http://pipenv.readthedocs.io/). +and install it via [pip] or [pipenv]. Alternatively, a plugin can also be put into a *.autohooks* directory at the root directory of the git repository where the hooks should be executed. @@ -349,3 +362,6 @@ first. Copyright (C) 2019 [Greenbone Networks GmbH](https://www.greenbone.net/) Licensed under the [GNU General Public License v3.0 or later](LICENSE). + +[pip]: https://pip.pypa.io/ +[pipenv]: https://pipenv.readthedocs.io/ diff --git a/autohooks/cli/__init__.py b/autohooks/cli/__init__.py index c1cc8773..edd23166 100644 --- a/autohooks/cli/__init__.py +++ b/autohooks/cli/__init__.py @@ -17,6 +17,7 @@ import argparse +from autohooks.setting import Mode from autohooks.version import get_version from autohooks.cli.activate import install_hooks @@ -43,6 +44,14 @@ def main(): action='store_true', help='Force activation of hook even if a hook already exists', ) + activate_parser.add_argument( + '-m', + '--mode', + dest='mode', + choices=[str(Mode.PYTHONPATH), str(Mode.PIPENV)], + help='Mode for loading autohooks during hook execution. Either load ' + 'autohooks from the PYTHON_PATH or via pipenv.', + ) subparsers.add_parser('check', help='Check installed pre-commit hook') diff --git a/autohooks/cli/activate.py b/autohooks/cli/activate.py index e84a64d5..84f1fc8e 100644 --- a/autohooks/cli/activate.py +++ b/autohooks/cli/activate.py @@ -27,6 +27,7 @@ get_pre_commit_hook_path, get_autohooks_pre_commit_hook, ) +from autohooks.setting import Mode def install_hooks(args): @@ -49,9 +50,16 @@ def install_hooks(args): file=sys.stderr, ) - autohooks_pre_commit_hook = get_autohooks_pre_commit_hook() + if args.mode: + mode = Mode.from_string(args.mode) + else: + mode = config.get_mode() + + autohooks_pre_commit_hook = get_autohooks_pre_commit_hook(mode) install_pre_commit_hook(autohooks_pre_commit_hook, pre_commit_hook_path) print( - 'pre-commit hook installed at {}'.format(str(pre_commit_hook_path)) + 'pre-commit hook installed at {} using {} mode'.format( + str(pre_commit_hook_path), str(mode.get_effective_mode()) + ) ) diff --git a/autohooks/cli/check.py b/autohooks/cli/check.py index 57c62166..41c39bbe 100644 --- a/autohooks/cli/check.py +++ b/autohooks/cli/check.py @@ -17,7 +17,7 @@ from autohooks.install import ( get_pre_commit_hook_path, - get_pre_commit_hook_template_path, + is_autohooks_pre_commit_hook, ) from autohooks.config import ( @@ -38,13 +38,9 @@ def check_hooks(): pre_commit_hook = get_pre_commit_hook_path() - pre_commit_template = get_pre_commit_hook_template_path() - - template = pre_commit_template.read_text() if pre_commit_hook.is_file(): - hook = pre_commit_hook.read_text() - if hook == template: + if is_autohooks_pre_commit_hook(pre_commit_hook): ok('autohooks pre-commit hook is active.') else: error( diff --git a/autohooks/config.py b/autohooks/config.py index b62f32c9..9125b173 100644 --- a/autohooks/config.py +++ b/autohooks/config.py @@ -17,6 +17,7 @@ import toml +from autohooks.setting import Mode from autohooks.utils import get_pyproject_toml_path AUTOHOOKS_SECTION = 'tool.autohooks' @@ -61,6 +62,19 @@ def get_pre_commit_script_names(self): return [] + def get_mode(self): + if self.has_autohooks_config(): + mode = self._autohooks_config.get_value('mode') + if not mode: + return Mode.UNDEFINED + + try: + return Mode[mode.upper()] + except KeyError: + return Mode.UNKNOWN + + return Mode.UNDEFINED + def get_config(self): return self._config diff --git a/autohooks/install.py b/autohooks/install.py index 5472c026..5c1d563b 100644 --- a/autohooks/install.py +++ b/autohooks/install.py @@ -18,10 +18,10 @@ from setuptools.command.install import install from setuptools.command.develop import develop -from autohooks.utils import ( - get_git_hook_directory_path, - get_autohooks_directory_path, -) +from autohooks.config import load_config_from_pyproject_toml +from autohooks.template import PreCommitTemplate + +from autohooks.utils import get_git_hook_directory_path def get_pre_commit_hook_path(): @@ -29,18 +29,21 @@ def get_pre_commit_hook_path(): return git_hook_dir_path / 'pre-commit' -def get_pre_commit_hook_template_path(): - setup_dir_path = get_autohooks_directory_path() - return setup_dir_path / 'precommit' / 'template' +def get_autohooks_pre_commit_hook(mode): + template = PreCommitTemplate() + + return template.render(mode=mode) -def get_autohooks_pre_commit_hook(): - template_path = get_pre_commit_hook_template_path() - return template_path.read_text() +def is_autohooks_pre_commit_hook(path): + hook = path.read_text() + lines = hook.split('\n') + return len(lines) > 5 and "autohooks.precommit" in lines[5] def install_pre_commit_hook(pre_commit_hook, pre_commit_hook_path): pre_commit_hook_path.write_text(pre_commit_hook) + pre_commit_hook_path.chmod(0o775) class AutohooksInstall: @@ -48,7 +51,10 @@ def install_git_hook(self): try: pre_commit_hook_path = get_pre_commit_hook_path() if not pre_commit_hook_path.exists(): - autohooks_pre_commit_hook = get_autohooks_pre_commit_hook() + config = load_config_from_pyproject_toml() + + mode = config.get_mode() + autohooks_pre_commit_hook = get_autohooks_pre_commit_hook(mode) install_pre_commit_hook( autohooks_pre_commit_hook, pre_commit_hook_path ) diff --git a/autohooks/precommit/template b/autohooks/precommit/template index 57b46e15..b14a059d 100755 --- a/autohooks/precommit/template +++ b/autohooks/precommit/template @@ -1,4 +1,4 @@ -#!/usr/bin/env python3 +#!$SHEBANG import sys @@ -6,6 +6,9 @@ try: from autohooks.precommit import run sys.exit(run()) except ImportError: - print('autohooks not installed. Ignoring pre-commit hook.') - sys.exit(0) - + print( + "autohooks is not installed. To force creating a commit without " + "verification via autohooks run 'git commit --no-verify'.", + file=sys.stderr, + ) + sys.exit(1) diff --git a/autohooks/setting.py b/autohooks/setting.py new file mode 100644 index 00000000..19088a98 --- /dev/null +++ b/autohooks/setting.py @@ -0,0 +1,44 @@ +# Copyright (C) 2019 Greenbone Networks GmbH +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from enum import Enum + + +class Mode(Enum): + PIPENV = 1 + PYTHONPATH = 2 + UNDEFINED = -1 + UNKNOWN = -2 + + def get_effective_mode(self): + # pylint: disable=comparison-with-callable + if self.value == Mode.PIPENV.value: + return Mode.PIPENV + return Mode.PYTHONPATH + + @staticmethod + def from_string(modestring): + if not modestring: + return Mode.UNDEFINED + + try: + return Mode[modestring.upper()] + except KeyError: + return Mode.UNKNOWN + + def __str__(self): + return self.name.lower() # pylint: disable=no-member diff --git a/autohooks/template.py b/autohooks/template.py new file mode 100644 index 00000000..ac39082a --- /dev/null +++ b/autohooks/template.py @@ -0,0 +1,50 @@ +# Copyright (C) 2019 Greenbone Networks GmbH +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from string import Template + +from autohooks.setting import Mode +from autohooks.utils import get_autohooks_directory_path + + +PYTHON3_SHEBANG = '/usr/bin/env python3' +PIPENV_SHEBANG = '/usr/bin/env -S pipenv run python3' + + +def get_pre_commit_hook_template_path(): + setup_dir_path = get_autohooks_directory_path() + return setup_dir_path / 'precommit' / 'template' + + +class PreCommitTemplate: + def __init__(self, template_path=None): + if template_path is None: + template_path = get_pre_commit_hook_template_path() + self._load(template_path) + + def _load(self, template_path): + self._template = Template(template_path.read_text()) + + def render(self, *, mode): + mode = mode.get_effective_mode() + + if mode == Mode.PIPENV: + params = dict(SHEBANG=PIPENV_SHEBANG) + else: + params = dict(SHEBANG=PYTHON3_SHEBANG) + + return self._template.safe_substitute(params) diff --git a/tests/test_config.py b/tests/test_config.py index f92884ea..34e425d1 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -22,6 +22,7 @@ from autohooks.config import ( AutohooksConfig, Config, + Mode, load_config_from_pyproject_toml, ) @@ -54,6 +55,7 @@ def test_load_from_non_existing_toml_file(self): self.assertFalse(config.has_config()) self.assertFalse(config.has_autohooks_config()) self.assertFalse(config.is_autohooks_enabled()) + self.assertEqual(config.get_mode(), Mode.UNDEFINED) self.assertEqual(len(config.get_pre_commit_script_names()), 0) @@ -63,6 +65,7 @@ def test_empty_config(self): self.assertFalse(config.has_config()) self.assertFalse(config.has_autohooks_config()) self.assertFalse(config.is_autohooks_enabled()) + self.assertEqual(config.get_mode(), Mode.UNDEFINED) self.assertEqual(len(config.get_pre_commit_script_names()), 0) @@ -72,6 +75,7 @@ def test_empty_config_dict(self): self.assertTrue(config.has_config()) self.assertFalse(config.has_autohooks_config()) self.assertFalse(config.is_autohooks_enabled()) + self.assertEqual(config.get_mode(), Mode.UNDEFINED) self.assertEqual(len(config.get_pre_commit_script_names()), 0) @@ -81,6 +85,49 @@ def test_missing_pre_commit(self): self.assertTrue(config.has_config()) self.assertTrue(config.has_autohooks_config()) self.assertTrue(config.is_autohooks_enabled()) + self.assertEqual(config.get_mode(), Mode.UNDEFINED) + + self.assertEqual(len(config.get_pre_commit_script_names()), 0) + + def test_get_mode_pipenv(self): + config = AutohooksConfig({'tool': {'autohooks': {'mode': 'pipenv'}}}) + + self.assertTrue(config.has_config()) + self.assertTrue(config.has_autohooks_config()) + self.assertTrue(config.is_autohooks_enabled()) + self.assertEqual(config.get_mode(), Mode.PIPENV) + + self.assertEqual(len(config.get_pre_commit_script_names()), 0) + + def test_get_mode_pythonpath(self): + config = AutohooksConfig( + {'tool': {'autohooks': {'mode': 'pythonpath'}}} + ) + + self.assertTrue(config.has_config()) + self.assertTrue(config.has_autohooks_config()) + self.assertTrue(config.is_autohooks_enabled()) + self.assertEqual(config.get_mode(), Mode.PYTHONPATH) + + self.assertEqual(len(config.get_pre_commit_script_names()), 0) + + def test_get_mode_unknown(self): + config = AutohooksConfig({'tool': {'autohooks': {'mode': 'foo'}}}) + + self.assertTrue(config.has_config()) + self.assertTrue(config.has_autohooks_config()) + self.assertTrue(config.is_autohooks_enabled()) + self.assertEqual(config.get_mode(), Mode.UNKNOWN) + + self.assertEqual(len(config.get_pre_commit_script_names()), 0) + + def test_get_mode_undefined(self): + config = AutohooksConfig({'tool': {'autohooks': {'mode': None}}}) + + self.assertTrue(config.has_config()) + self.assertTrue(config.has_autohooks_config()) + self.assertTrue(config.is_autohooks_enabled()) + self.assertEqual(config.get_mode(), Mode.UNDEFINED) self.assertEqual(len(config.get_pre_commit_script_names()), 0) @@ -90,6 +137,7 @@ def test_get_config_dict(self): self.assertTrue(config.has_config()) self.assertTrue(config.has_autohooks_config()) + self.assertEqual(config.get_mode(), Mode.UNDEFINED) config_out = config.get_config() @@ -170,3 +218,7 @@ def test_config_point_syntax(self): bar_config = config.get('tool', 'autohooks', 'plugins', 'bar') self.assertFalse(bar_config.is_empty()) self.assertEqual(bar_config.get_value('lorem'), 'ipsum') + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_install.py b/tests/test_install.py index a0377664..d08320ea 100644 --- a/tests/test_install.py +++ b/tests/test_install.py @@ -24,9 +24,11 @@ from autohooks.install import ( get_autohooks_pre_commit_hook, get_pre_commit_hook_path, - get_pre_commit_hook_template_path, install_pre_commit_hook, + is_autohooks_pre_commit_hook, ) +from autohooks.setting import Mode +from autohooks.template import get_pre_commit_hook_template_path from autohooks.utils import exec_git @@ -57,16 +59,9 @@ def test_get_path(self): ) -class GetPreCommitHookTemplatePath(unittest.TestCase): - def test_template_exists(self): - template_path = get_pre_commit_hook_template_path() - self.assertTrue(template_path.exists()) - self.assertTrue(template_path.is_file()) - - class InstallPreCommitHook(GitDirTestCase): def test_install(self): - hooks = get_autohooks_pre_commit_hook() + hooks = get_autohooks_pre_commit_hook(mode=Mode.PIPENV) pre_commmit_hook_path = get_pre_commit_hook_path() self.assertFalse(pre_commmit_hook_path.exists()) @@ -74,3 +69,25 @@ def test_install(self): install_pre_commit_hook(hooks, pre_commmit_hook_path) self.assertTrue(pre_commmit_hook_path.exists()) + + +class FakeHookPath: + def __init__(self, text): + self._text = text + + def read_text(self): + return self._text + + +class IsAutohooksPreCommitHook(unittest.TestCase): + def test_other_hook(self): + path = FakeHookPath('foo\nbar') + self.assertFalse(is_autohooks_pre_commit_hook(path)) + + def test_pre_commit_template_path(self): + path = get_pre_commit_hook_template_path() + self.assertTrue(is_autohooks_pre_commit_hook(path)) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_setting.py b/tests/test_setting.py new file mode 100644 index 00000000..09b7aa7e --- /dev/null +++ b/tests/test_setting.py @@ -0,0 +1,51 @@ +# Copyright (C) 2019 Greenbone Networks GmbH +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import unittest + +from autohooks.setting import Mode + + +class ModeTestCase(unittest.TestCase): + def test_get_effective_mode(self): + self.assertEqual(Mode.PIPENV.get_effective_mode(), Mode.PIPENV) + self.assertEqual(Mode.PYTHONPATH.get_effective_mode(), Mode.PYTHONPATH) + self.assertEqual(Mode.UNDEFINED.get_effective_mode(), Mode.PYTHONPATH) + self.assertEqual(Mode.UNKNOWN.get_effective_mode(), Mode.PYTHONPATH) + + def test_get_pipenv_mode_from_string(self): + self.assertEqual(Mode.from_string('pipenv'), Mode.PIPENV) + self.assertEqual(Mode.from_string('PIPENV'), Mode.PIPENV) + + def test_get_pythonpath_mode_from_string(self): + self.assertEqual(Mode.from_string('pythonpath'), Mode.PYTHONPATH) + self.assertEqual(Mode.from_string('PYTHONPATH'), Mode.PYTHONPATH) + + def test_get_invalid_mode_from_string(self): + self.assertEqual(Mode.from_string('foo'), Mode.UNKNOWN) + self.assertEqual(Mode.from_string(None), Mode.UNDEFINED) + self.assertEqual(Mode.from_string(''), Mode.UNDEFINED) + + def test_string_conversion(self): + self.assertEqual(str(Mode.PIPENV), 'pipenv') + self.assertEqual(str(Mode.PYTHONPATH), 'pythonpath') + self.assertEqual(str(Mode.UNKNOWN), 'unknown') + self.assertEqual(str(Mode.UNDEFINED), 'undefined') + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_template.py b/tests/test_template.py new file mode 100644 index 00000000..437d6855 --- /dev/null +++ b/tests/test_template.py @@ -0,0 +1,89 @@ +# Copyright (C) 2019 Greenbone Networks GmbH +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import unittest + +from autohooks.setting import Mode +from autohooks.template import ( + PreCommitTemplate, + get_pre_commit_hook_template_path, +) + +DEFAULT_TEMPLATE = """#!/usr/bin/env python3 + +import sys + +try: + from autohooks.precommit import run + sys.exit(run()) +except ImportError: + print( + "autohooks is not installed. To force creating a commit without " + "verification via autohooks run 'git commit --no-verify'.", + file=sys.stderr, + ) + sys.exit(1) +""" + + +class FakeTemplatePath: + def __init__(self, text): + self._text = text + + def read_text(self): + return self._text + + +class PreCommitTemplateTestCase(unittest.TestCase): + def test_should_use_default_template(self): + template = PreCommitTemplate() + self.assertEqual( + template.render(mode=Mode.PYTHONPATH), DEFAULT_TEMPLATE + ) + + def test_should_render_mode_pipenv(self): + path = FakeTemplatePath("$SHEBANG") + template = PreCommitTemplate(path) + self.assertEqual( + template.render(mode=Mode.PIPENV), + "/usr/bin/env -S pipenv run python3", + ) + + def test_should_render_mode_unknown(self): + path = FakeTemplatePath("$SHEBANG") + template = PreCommitTemplate(path) + self.assertEqual( + template.render(mode=Mode.UNDEFINED), "/usr/bin/env python3" + ) + + def test_should_render_mode_undefined(self): + path = FakeTemplatePath("$SHEBANG") + template = PreCommitTemplate(path) + self.assertEqual( + template.render(mode=Mode.UNDEFINED), "/usr/bin/env python3" + ) + + +class GetPreCommitHookTemplatePath(unittest.TestCase): + def test_template_exists(self): + template_path = get_pre_commit_hook_template_path() + self.assertTrue(template_path.exists()) + self.assertTrue(template_path.is_file()) + + +if __name__ == '__main__': + unittest.main()