Repository: chiptopher/guet (language: python)

Issue:
Introduce pre-commit to project
## Feature Request

#### Description
Include pre-commit hooks through [pre-commit](https://pre-commit.com) to match what's run on the CI.



Discussion / hints:

Fix patch:
diff --git a/.github/workflows/pythonapp.yml b/.github/workflows/pythonapp.yml
index 9f2de8e..7a9e977 100644
--- a/.github/workflows/pythonapp.yml
+++ b/.github/workflows/pythonapp.yml
@@ -26,6 +26,7 @@ jobs:
           isort ./test/**/*.py --check-only
           isort ./e2e/**/*.py --check-only
           pylint --load-plugins=pylint.extensions.mccabe guet
+          pylint --load-plugins=pylint.extensions.mccabe test
       - name: Unit Tests
         run: |
           python -m unittest discover test
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 0000000..9ee1684
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,28 @@
+repos:
+  - repo: https://github.com/pycqa/pylint
+    rev: pylint-2.4.4
+    hooks:
+    - id: pylint
+      args: [--load-plugins=pylint.extensions.mccabe, guet]
+  - repo: https://github.com/PyCQA/isort
+    rev: 4.3.21
+    hooks:
+    - id: isort
+      args: [--check-only]
+  - repo: local
+    hooks:
+    -   id: unit-tests
+        name: unit-tests
+        entry: python3 -m unittest discover test
+        language: system
+        types: [python]
+        always_run: true
+  - repo: local
+    hooks:
+    -   id: e2e-tests
+        name: e2e-tests
+        entry: unittest-parallel --start-directory e2e -t .
+        language: system
+        types: [python]
+        always_run: true
+        stages: [push]
diff --git a/requirements.txt b/requirements.txt
index b2f0678..4e77013 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,4 +1,5 @@
 docker==4.1.0
+isort==4.3.21
+pre-commit==2.10.1
 pylint==2.4.4
 unittest-parallel==0.8.2
-isort==4.3.21


Test patch:
diff --git a/test/commands/test_command_map.py b/test/commands/test_command_map.py
index c6d92ba..2a76934 100644
--- a/test/commands/test_command_map.py
+++ b/test/commands/test_command_map.py
@@ -2,7 +2,6 @@
 from unittest.mock import Mock
 
 from guet.commands import CommandMap
-from guet.steps import Step
 
 
 class TestCommandMap(TestCase):
diff --git a/test/committers/test_committers2.py b/test/committers/test_committers2.py
index 7b9fd59..e597849 100644
--- a/test/committers/test_committers2.py
+++ b/test/committers/test_committers2.py
@@ -1,5 +1,5 @@
 from unittest import TestCase
-from unittest.mock import Mock, patch
+from unittest.mock import Mock
 
 from guet.committers._committers2 import Committers
 from guet.committers.committer import Committer
diff --git a/test/committers/test_local_committers_state.py b/test/committers/test_local_committers_state.py
index 5eeed5a..aa1012e 100644
--- a/test/committers/test_local_committers_state.py
+++ b/test/committers/test_local_committers_state.py
@@ -1,3 +1,5 @@
+# pylint: disable=protected-access
+
 from unittest import TestCase
 from unittest.mock import Mock, patch
 
@@ -9,7 +11,7 @@
 class TestLocalCommittersState(TestCase):
 
     @patch('guet.committers._local_committers_state.project_root')
-    def test_all_includes_all_local_committers_and_global_committers(self, mock_project_root):
+    def test_all_includes_all_local_committers_and_global_committers(self, _):
         committers_file = Mock()
         committers_file.read.return_value = [
             'initials2,name2,email2'
@@ -18,22 +20,22 @@ def test_all_includes_all_local_committers_and_global_committers(self, mock_proj
         file_system.get.return_value = committers_file
 
         global_state: GlobalCommittersState = Mock()
-        g = Committer('name1', 'email1', 'initial1')
-        global_state.all = Mock(return_value=[g])
+        global_committer = Committer('name1', 'email1', 'initial1')
+        global_state.all = Mock(return_value=[global_committer])
 
         state = LocalCommittersState(file_system, global_state)
 
         result = state.all()
 
         local = Committer('name2', 'email2', 'initials2')
-        self.assertEqual([g, local], result)
+        self.assertEqual([global_committer, local], result)
 
     def test_all_gives_local_committers_precedence(self):
         file_system = Mock()
 
         global_state: GlobalCommittersState = Mock()
-        g = Committer('name1', 'email1', 'initials')
-        global_state.all = Mock(return_value=[g])
+        committer = Committer('name1', 'email1', 'initials')
+        global_state.all = Mock(return_value=[committer])
 
         local = Committer('name2', 'email2', 'initials')
         state = LocalCommittersState(file_system, global_state)
@@ -94,7 +96,8 @@ def test_add_warns_when_local_committer_will_shadow_global_committer(self, mock_
 
         state.add(Committer('name2', 'email2', 'initials1'))
 
-        mock_print.assert_called_with('Adding committer with initials "initials1" will overshadow global committer with same initials.')
+        mock_print.assert_called_with(('Adding committer with initials "initials1" will '
+                                       'overshadow global committer with same initials.'))
 
     def test_remove_removes_committer(self):
         file_system = Mock()
diff --git a/test/files/test_file_system.py b/test/files/test_file_system.py
index 852ecb3..372e6cf 100644
--- a/test/files/test_file_system.py
+++ b/test/files/test_file_system.py
@@ -16,7 +16,7 @@ def test_get_returns_file_with_at_path(self, mock_file):
         self.assertEqual(mock_file.return_value, get)
 
     @patch('guet.files._file_system.File', side_effect=[Mock(), Mock()])
-    def test_get_returns_same_reference_when_requesting_file_more_than_once(self, mock_file):
+    def test_get_returns_same_reference_when_requesting_file_more_than_once(self, _):
         path = Path('/path/to/file')
 
         file_system = FileSystem()
diff --git a/test/git/test_git.py b/test/git/test_git.py
index 5c8813b..ef73202 100644
--- a/test/git/test_git.py
+++ b/test/git/test_git.py
@@ -1,14 +1,15 @@
+#pylint: disable=too-many-public-methods,protected-access
+
 from os.path import join
 from pathlib import Path
 from unittest import TestCase
 from unittest.mock import Mock, call, patch
 
-from guet.committers.committer import Committer
 from guet.git.author import Author
 from guet.git.errors import NoGitPresentError
 from guet.git.git import Git
 
-default_config_response = ['[core]',
+DEFAULT_CONFIG_RESPONSE = ['[core]',
                            '\trepositoryformatversion = 0',
                            '\tfilemode = true',
                            '\tbare = false',
@@ -34,10 +35,13 @@ def _mock_repository_path(*, is_dir=True) -> Path:
     return mock
 
 
-_mock_read_lines = [['First line of commit message', 'Second line of commit message'], default_config_response]
+_MOCK_READ_LINES = [
+        ['First line of commit message', 'Second line of commit message'],
+        DEFAULT_CONFIG_RESPONSE
+]
 
 
-@patch('guet.git.git.read_lines', side_effect=_mock_read_lines)
+@patch('guet.git.git.read_lines', side_effect=_MOCK_READ_LINES)
 @patch('guet.git._hook_loader.Hook')
 class TestGit(TestCase):
     def setUp(self) -> None:
@@ -51,8 +55,14 @@ def test_init_loads_all_possible_hooks(self, mock_hook, _1):
         post_commit_alongside_content = Mock()
         commit_msg_alongside_content = Mock()
 
-        expected_hooks = [pre_commit_content, post_commit_content, commit_msg_content,
-                          pre_commit_alongside_content, post_commit_alongside_content, commit_msg_alongside_content]
+        expected_hooks = [
+            pre_commit_content,
+            post_commit_content,
+            commit_msg_content,
+            pre_commit_alongside_content,
+            post_commit_alongside_content,
+            commit_msg_alongside_content
+        ]
 
         mock_hook.side_effect = expected_hooks
 
@@ -69,8 +79,8 @@ def test_init_loads_all_possible_hooks(self, mock_hook, _1):
         self.assertListEqual(expected_hooks, git.hooks)
 
     def test_init_swallows_file_not_found_error(self, mock_hook, _1):
-        mock_hook.side_effect = [FileNotFoundError(), FileNotFoundError(), FileNotFoundError(), FileNotFoundError(),
-                                 FileNotFoundError(), FileNotFoundError()]
+        mock_hook.side_effect = [FileNotFoundError(), FileNotFoundError(), FileNotFoundError(),
+                                 FileNotFoundError(), FileNotFoundError(), FileNotFoundError()]
 
         git = Git(self.path_to_git)
 
@@ -82,10 +92,12 @@ def test_init_loads_current_commit_msg(self, _1, mock_read_lines):
         git = Git(self.path_to_git)
         self.assertListEqual(expected_commit_msg, git.commit_msg)
 
-    def test_init_handles_there_not_being_a_commit_msg_file_becuase_of_no_commits(self, _1, mock_read_lines):
+    def test_init_handles_there_not_being_a_commit_msg_file_becuase_of_no_commits(self,
+                                                                                  _1,
+                                                                                  mock_read_lines):
         mock_read_lines.side_effect = [
             FileNotFoundError(),
-            default_config_response
+            DEFAULT_CONFIG_RESPONSE
         ]
         git = Git(self.path_to_git)
         self.assertListEqual([], git.commit_msg)
@@ -104,7 +116,7 @@ def test_init_loads_the_author_from_the_config_file(self, _1, _2):
         self.assertEqual('name@localhost', git.author.email)
 
     def test_init_loads_none_when_there_is_no_author_set_in_config(self, _1, mock_read_lines):
-        without_author = list(default_config_response)
+        without_author = list(DEFAULT_CONFIG_RESPONSE)
         del without_author[-1]
         del without_author[-2]
         del without_author[-3]
@@ -120,11 +132,12 @@ def test_setting_commit_msg_writes_it_to_file(self, mock_write_lines, _1, _2):
         new_content = ['New line 1', 'New line 2']
         git = Git(self.path_to_git)
         git.commit_msg = new_content
-        mock_write_lines.assert_called_with(self.path_to_git.joinpath('COMMIT_EDITMSG'), new_content)
+        mock_write_lines.assert_called_with(self.path_to_git.joinpath('COMMIT_EDITMSG'),
+                                            new_content)
 
     @patch('guet.git.git.write_lines')
     def test_setting_author_writes_it_to_file(self, mock_write_lines, _1, _2):
-        new_content = list(default_config_response)
+        new_content = list(DEFAULT_CONFIG_RESPONSE)
         new_content[-1] = '\temail = new_email@localhost'
         new_content[-2] = '\tname = new_name'
 
@@ -134,12 +147,16 @@ def test_setting_author_writes_it_to_file(self, mock_write_lines, _1, _2):
 
     @patch('guet.git.git.write_lines')
     def test_setting_writes_author_when_no_author_currently_present(self, mock_write_lines, _1, _2):
-        content_without_author = list(default_config_response)
+        content_without_author = list(DEFAULT_CONFIG_RESPONSE)
         del content_without_author[-1]
         del content_without_author[-1]
         del content_without_author[-1]
 
-        new_content = content_without_author + ['[user]', '\tname = new_name', '\temail = new_email@localhost']
+        new_content = content_without_author + [
+            '[user]',
+            '\tname = new_name',
+            '\temail = new_email@localhost'
+        ]
 
         git = Git(self.path_to_git)
         git._config_lines = content_without_author
@@ -164,8 +181,7 @@ def test_hooks_present_returns_false_if_normal_hooks_have_non_guet_content(self,
         ]
         self.assertFalse(git.hooks_present())
 
-    def test_hooks_present_returns_true_when_normal_hooks_have_non_guet_content_but_dash_hooks_are_present(self, _1,
-                                                                                                           _2):
+    def test_hooks_present_handles_dash_hooks(self, _1, _2):
         git = Git(self.path_to_git)
         git.hooks = [
             _mock_hook(join(str(self.path_to_git), 'hooks', 'post-commit'), is_guet_hook=False),
diff --git a/test/git/test_hook.py b/test/git/test_hook.py
index b3ca228..c3addbf 100644
--- a/test/git/test_hook.py
+++ b/test/git/test_hook.py
@@ -10,33 +10,39 @@
 @patch('guet.git.hook.read_lines', return_value=PYTHON3_GUET_HOOK)
 class TestHook(TestCase):
 
-    def test_reads_content_to_hook(self, mock_read_lines, mock_which):
+    def test_reads_content_to_hook(self, mock_read_lines, _):
         path = Path('/path/to/.git/hooks/name')
         hook = Hook(path)
         mock_read_lines.assert_called_with(path)
         self.assertEqual(mock_read_lines.return_value, hook.content)
 
-    def test_is_guet_hook_returns_whether_hook_matches_guet_content(self, mock_read_lines, mock_which):
+    def test_is_guet_hook_returns_whether_hook_matches_guet_content(self,
+                                                                    mock_read_lines,
+                                                                    _):
         mock_read_lines.return_value = ['Other', 'Content']
         hook = Hook(Path('/path/to/.git/hooks/name'))
         self.assertFalse(hook.is_guet_hook())
 
-    def test_works_with_specific_content(self, mock_read_lines, mock_which):
-        mock_read_lines.return_value = ['#! /usr/bin/env python3', 'from guet.hooks import manage', 'import sys',
-                                        'manage(sys.argv[0])']
+    def test_works_with_specific_content(self, mock_read_lines, _):
+        mock_read_lines.return_value = [
+            '#! /usr/bin/env python3',
+            'from guet.hooks import manage',
+            'import sys',
+            'manage(sys.argv[0])'
+        ]
         try:
             Hook(Path('/path/to/.git/hooks/name'))
-            pass
         except (NotGuetHookError, FileNotFoundError):
             self.fail('Should successfully create hook.')
 
-    def test_init_with_create_flag_catches_file_not_found_error_and_save_content_to_default(self, mock_read_lines,
-                                                                                            mock_which):
+    def test_init_with_create_flag_saves_content_to_default(self,
+                                                            mock_read_lines,
+                                                            _):
         mock_read_lines.side_effect = FileNotFoundError()
         hook = Hook(Path('/path/to/.git/hooks/name'), create=True)
         self.assertEqual(PYTHON3_GUET_HOOK, hook.content)
 
-    def test_init_with_create_flag_overwrites_already_present_content(self, mock_read_lines, mock_which):
+    def test_init_with_create_flag_overwrites_already_present_content(self, mock_read_lines, _):
         mock_read_lines.return_value = ['Other', 'Content']
         hook = Hook(Path('/path/to/.git/hooks/name'), create=True)
         self.assertEqual(PYTHON3_GUET_HOOK, hook.content)
@@ -44,7 +50,7 @@ def test_init_with_create_flag_overwrites_already_present_content(self, mock_rea
     @patch('guet.git.hook.chmod')
     @patch('guet.git.hook.stat')
     @patch('guet.git.hook.write_lines')
-    def test_save_writes_lines_to_file(self, mock_write_lines, mock_stat, mock_chmod, mock_read_lines, mock_which):
+    def test_save_writes_lines_to_file(self, mock_write_lines, _1, _2, mock_read_lines, _3):
         mock_read_lines.side_effect = FileNotFoundError()
         mock_path = Mock()
         hook = Hook(mock_path, create=True)
@@ -54,7 +60,7 @@ def test_save_writes_lines_to_file(self, mock_write_lines, mock_stat, mock_chmod
     @patch('guet.git.hook.chmod')
     @patch('guet.git.hook.stat')
     @patch('guet.git.hook.write_lines')
-    def test_save_chmods_file_to_executable(self, mock_write_lines, mock_stat, mock_chmod, mock_read_lines, mock_which):
+    def test_save_chmods_file_to_executable(self, _1, mock_stat, mock_chmod, mock_read_lines, _2):
         mock_read_lines.side_effect = FileNotFoundError()
         path = '/path/to/.git/hooks/name'
         mock_path = Mock()
@@ -63,7 +69,9 @@ def test_save_chmods_file_to_executable(self, mock_write_lines, mock_stat, mock_
         hook.save()
         mock_chmod.assert_called_with(path, mock_stat.return_value.st_mode | 0o111)
 
-    def test_is_get_hook_registers_python_shebang_without_the_3_as_guet_hook(self, mock_read_lines, mock_which):
+    def test_is_get_hook_registers_python_shebang_without_the_3_as_guet_hook(self,
+                                                                             mock_read_lines,
+                                                                             _):
         mock_read_lines.return_value = PYTHON_GUET_HOOK
         path = '/path/to/.git/hooks/name'
         hook = Hook(Path(path))
@@ -72,7 +80,11 @@ def test_is_get_hook_registers_python_shebang_without_the_3_as_guet_hook(self, m
     @patch('guet.git.hook.chmod')
     @patch('guet.git.hook.stat')
     @patch('guet.git.hook.write_lines')
-    def test_uses_python_if_python3_is_not_available(self, mock_write_lines, mock_stat, mock_chmod, mock_read_lines,
+    def test_uses_python_if_python3_is_not_available(self,
+                                                     mock_write_lines,
+                                                     _1,
+                                                     _2,
+                                                     mock_read_lines,
                                                      mock_which):
         mock_which.return_value = None
         mock_read_lines.side_effect = FileNotFoundError()
diff --git a/test/settings/test_set_settings.py b/test/settings/test_set_settings.py
index 58f3543..6590dcd 100644
--- a/test/settings/test_set_settings.py
+++ b/test/settings/test_set_settings.py
@@ -26,4 +26,5 @@ def test_converts_settings_to_lines_and_writes_to_config_file(self,
             '\n',
             'pairReset=False\n'
         ]
-        mock_write_lines.assert_called_with(Path(join(CONFIGURATION_DIRECTORY, constants.CONFIG)), expected_result)
+        mock_write_lines.assert_called_with(Path(join(CONFIGURATION_DIRECTORY, constants.CONFIG)),
+                                            expected_result)
diff --git a/test/step/check/test_committers_exist.py b/test/step/check/test_committers_exist.py
index d302082..5f4c7c6 100644
--- a/test/step/check/test_committers_exist.py
+++ b/test/step/check/test_committers_exist.py
@@ -1,7 +1,6 @@
 from unittest import TestCase
 from unittest.mock import Mock
 
-from guet.errors import InvalidInitialsError
 from guet.steps.check._committers_exist import CommittersExistCheck
 
 
diff --git a/test/util/test_args.py b/test/util/test_args.py
index 58a2903..6f6fae4 100644
--- a/test/util/test_args.py
+++ b/test/util/test_args.py
@@ -1,5 +1,4 @@
 from unittest import TestCase
-from unittest.mock import Mock, patch
 
 from guet.util import Args