Skip to content

Commit

Permalink
Merge pull request #32 from y0urself/typehints
Browse files Browse the repository at this point in the history
Added type hints/annotations to all methods
  • Loading branch information
bjoernricks authored Oct 14, 2019
2 parents 55a8384 + 35a4ee8 commit d833275
Show file tree
Hide file tree
Showing 14 changed files with 110 additions and 76 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
[#24](https://github.com/greenbone/autohooks/pull/24)
* Add `poetry` mode to run autohooks via [poetry](https://poetry.eustace.io/)
[#29](https://github.com/greenbone/autohooks/pull/29)
* Added type hints/annotations to all methods [#32](https://github.com/greenbone/autohooks/pull/32)

### Changed

Expand Down
2 changes: 1 addition & 1 deletion autohooks/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,5 @@
from autohooks.terminal import ok, fail, error, warning


def out(message):
def out(message: str):
print(message)
67 changes: 41 additions & 26 deletions autohooks/api/git.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,17 @@
from enum import Enum
from pathlib import Path
from tempfile import NamedTemporaryFile
from types import TracebackType
from typing import Any, List, Type, Optional, Generator, Union, TYPE_CHECKING

from autohooks.utils import exec_git, get_project_root_path, GitError

# https://stackoverflow.com/questions/49959656/typing-exit-in-3-5-fails-on-runtime-but-typechecks
if TYPE_CHECKING:
BaseExceptionType = Type[BaseException]
else:
BaseExceptionType = bool # don't care, as long is it doesn't error

__all__ = [
'exec_git',
'get_staged_status',
Expand Down Expand Up @@ -59,7 +67,7 @@ class Status(Enum):


class StatusEntry:
def __init__(self, status_string, root_path=None):
def __init__(self, status_string: str, root_path: Path = None) -> None:
status = status_string[:2]
filename = status_string[3:]

Expand All @@ -74,35 +82,35 @@ def __init__(self, status_string, root_path=None):
else:
self.path = Path(filename)

def __str__(self):
def __str__(self) -> str:
return '{}{} {}'.format(
self.index.value, self.working_tree.value, str(self.path)
)

def __repr__(self):
def __repr__(self) -> str:
return '<StatusEntry {}>'.format(str(self))

def absolute_path(self):
def absolute_path(self) -> Path:
if self.root_path:
return (self.root_path / self.path).resolve()
return self.path.resolve()


def _parse_status(output):
def _parse_status(output: str) -> Generator[str, None, None]:
output = output.rstrip('\0')
if not output:
return

output = output.split('\0')
while output:
line = output.pop(0)
output_list = output.split('\0')
while output_list:
line = output_list.pop(0)
if line[0] == Status.RENAMED.value:
yield '{}\0{}'.format(line, output.pop(0))
yield '{}\0{}'.format(line, output_list.pop(0))
else:
yield line


def is_staged_status(status):
def is_staged_status(status: StatusEntry) -> bool:
return (
status.index != Status.UNMODIFIED
and status.index != Status.UNTRACKED
Expand All @@ -111,7 +119,7 @@ def is_staged_status(status):
)


def is_partially_staged_status(status):
def is_partially_staged_status(status: StatusEntry) -> bool:
return (
status.index != Status.UNMODIFIED
and status.index != Status.UNTRACKED
Expand All @@ -123,7 +131,7 @@ def is_partially_staged_status(status):
)


def get_status(files=None):
def get_status(files: List[Union[Path, str]] = None) -> List[StatusEntry]:
args = [
'status',
'--porcelain=v1',
Expand All @@ -141,17 +149,19 @@ def get_status(files=None):
return [StatusEntry(f, root_path) for f in _parse_status(output)]


def get_staged_status(files=None):
def get_staged_status(
files: List[Union[Path, str]] = None
) -> List[StatusEntry]:
status = get_status(files)
return [s for s in status if is_staged_status(s)]


def stage_files_from_status_list(status_list):
def stage_files_from_status_list(status_list: List[StatusEntry]) -> None:
filenames = [str(s.path) for s in status_list]
exec_git('add', *filenames)


def get_diff(files=None):
def get_diff(files: List[StatusEntry] = None) -> str:
args = ['--no-pager', 'diff']

if files is not None:
Expand All @@ -161,24 +171,24 @@ def get_diff(files=None):
return exec_git(*args)


def _write_tree():
def _write_tree() -> str:
return exec_git('write-tree').strip()


def _read_tree(ref_or_hashid):
def _read_tree(ref_or_hashid: str) -> None:
exec_git('read-tree', ref_or_hashid)


def _checkout_from_index(status_list):
def _checkout_from_index(status_list: List[StatusEntry]) -> None:
filenames = [str(s.path) for s in status_list]
exec_git('checkout-index', '-f', '--', *filenames)


def _set_ref(name, hashid):
def _set_ref(name: str, hashid: str) -> None:
exec_git('update-ref', name, hashid)


def _get_tree_diff(tree1, tree2):
def _get_tree_diff(tree1: str, tree2: str) -> bytes:
return subprocess.check_output(
[
'git',
Expand All @@ -194,7 +204,7 @@ def _get_tree_diff(tree1, tree2):
)


def _apply_diff(patch):
def _apply_diff(patch: bytes) -> None:
with NamedTemporaryFile(mode='wb', buffering=0) as f:
f.write(patch)

Expand All @@ -214,12 +224,12 @@ def _apply_diff(patch):


class stash_unstaged_changes: # pylint: disable=invalid-name
def __init__(self, status_list):
def __init__(self, status_list: List[StatusEntry]) -> None:
self.partially_staged = [
s for s in status_list if is_partially_staged_status(s)
]

def stash_changes(self):
def stash_changes(self) -> None:
# save current staging area aka. index
self.index = _write_tree()
# add ref to be able to restore index manually
Expand All @@ -236,17 +246,22 @@ def stash_changes(self):
_read_tree(self.index)
_checkout_from_index(self.partially_staged)

def restore_working_tree(self):
def restore_working_tree(self) -> None:
# restore working tree
_read_tree(self.working_tree)
# checkout working tree
_checkout_from_index(self.partially_staged)

def __enter__(self):
def __enter__(self) -> None:
if self.partially_staged:
self.stash_changes()

def __exit__(self, exc_type, exc_value, traceback):
def __exit__(
self,
exc_type: Optional[BaseExceptionType],
exc_value: Optional[BaseException],
traceback: Optional[TracebackType],
) -> Any:
if not self.partially_staged:
return

Expand Down
7 changes: 5 additions & 2 deletions autohooks/api/path.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,17 @@

import fnmatch

from typing import Iterable
from pathlib import Path

def is_python_path(path):

def is_python_path(path: Path) -> bool:
if not path:
return False
return path.match('*.py')


def match(path, pattern_list):
def match(path: Path, pattern_list: Iterable) -> bool:
"""
Check if a Path matches to one of the patterns
Expand Down
4 changes: 3 additions & 1 deletion autohooks/cli/activate.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@

import sys

from argparse import Namespace

from autohooks.config import (
load_config_from_pyproject_toml,
get_pyproject_toml_path,
Expand All @@ -30,7 +32,7 @@
from autohooks.setting import Mode


def install_hooks(args):
def install_hooks(args: Namespace) -> None:
pre_commit_hook_path = get_pre_commit_hook_path()
pyproject_toml = get_pyproject_toml_path()
config = load_config_from_pyproject_toml(pyproject_toml)
Expand Down
2 changes: 1 addition & 1 deletion autohooks/cli/check.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
from autohooks.terminal import ok, error, warning


def check_hooks():
def check_hooks() -> None:
pre_commit_hook = get_pre_commit_hook_path()

if pre_commit_hook.is_file():
Expand Down
29 changes: 17 additions & 12 deletions autohooks/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

from typing import Dict, List
from pathlib import Path

import toml

from autohooks.setting import Mode
Expand All @@ -24,45 +27,45 @@


class Config:
def __init__(self, config_dict=None):
def __init__(self, config_dict: Dict = None) -> None:
self._config_dict = config_dict or {}

def get(self, *keys):
def get(self, *keys: str) -> 'Config':
config_dict = self._config_dict

for key in keys:
config_dict = config_dict.get(key, {})

return Config(config_dict)

def get_value(self, key, default=None):
def get_value(self, key: str, default: List[str] = None) -> List[str]:
return self._config_dict.get(key, default)

def is_empty(self):
return False if self._config_dict else True
def is_empty(self) -> bool:
return not bool(self._config_dict)


class AutohooksConfig:
def __init__(self, config_dict=None):
def __init__(self, config_dict: Dict = None) -> None:
self._config = Config(config_dict)
self._autohooks_config = self._config.get('tool').get('autohooks')

def has_config(self):
def has_config(self) -> bool:
return not self._config.is_empty()

def has_autohooks_config(self):
def has_autohooks_config(self) -> bool:
return not self._autohooks_config.is_empty()

def is_autohooks_enabled(self):
def is_autohooks_enabled(self) -> bool:
return self.has_autohooks_config()

def get_pre_commit_script_names(self):
def get_pre_commit_script_names(self) -> List[str]:
if self.has_autohooks_config():
return self._autohooks_config.get_value('pre-commit', [])

return []

def get_mode(self):
def get_mode(self) -> Mode:
if self.has_autohooks_config():
mode = self._autohooks_config.get_value('mode')
if not mode:
Expand All @@ -79,7 +82,9 @@ def get_config(self):
return self._config


def load_config_from_pyproject_toml(pyproject_toml=None):
def load_config_from_pyproject_toml(
pyproject_toml: Path = None
) -> AutohooksConfig:
if pyproject_toml is None:
pyproject_toml = get_pyproject_toml_path()

Expand Down
20 changes: 12 additions & 8 deletions autohooks/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,39 +15,43 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

from pathlib import Path

from setuptools.command.install import install
from setuptools.command.develop import develop

from autohooks.config import load_config_from_pyproject_toml
from autohooks.template import PreCommitTemplate

from autohooks.setting import Mode
from autohooks.utils import get_git_hook_directory_path


def get_pre_commit_hook_path():
def get_pre_commit_hook_path() -> Path:
git_hook_dir_path = get_git_hook_directory_path()
return git_hook_dir_path / 'pre-commit'


def get_autohooks_pre_commit_hook(mode):
def get_autohooks_pre_commit_hook(mode: Mode) -> str:
template = PreCommitTemplate()

return template.render(mode=mode)


def is_autohooks_pre_commit_hook(path):
def is_autohooks_pre_commit_hook(path: Path) -> bool:
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):
def install_pre_commit_hook(
pre_commit_hook: str, pre_commit_hook_path: Path
) -> None:
pre_commit_hook_path.write_text(pre_commit_hook)
pre_commit_hook_path.chmod(0o775)


class AutohooksInstall:
def install_git_hook(self):
def install_git_hook(self) -> None:
try:
pre_commit_hook_path = get_pre_commit_hook_path()
if not pre_commit_hook_path.exists():
Expand All @@ -63,12 +67,12 @@ def install_git_hook(self):


class PostInstall(install, AutohooksInstall):
def run(self):
def run(self) -> None:
super().run()
self.install_git_hook()


class PostDevelop(develop, AutohooksInstall):
def install_for_development(self):
def install_for_development(self) -> None:
super().install_for_development()
self.install_git_hook()
Loading

0 comments on commit d833275

Please sign in to comment.