Skip to content

Commit

Permalink
Annotate core functions and remove sphinx style argument types (#205)
Browse files Browse the repository at this point in the history
* Annotate core functions and remove sphinx style argument types

* Tidy up

* Fix type annotations on config object

* Remove variable reuse

* Add missing import for raw operator. Add pyright task

* Be more explicit with the git command args to detect the overload

* Install dependencies. Fix potentially (although unreachable) unbound value errors

* Patch mock git function

* Annotate metrics and state modules. Pin gitpython since the arguments are being fixed.

* Update pytest-cov

* Start annotation of cache config and state modules

* Annotate all of cache module

* fix types for archivers

* use generics for base types and named tuples

* add missing docstrings

* de-dataclass

* reformat

* reformat

* use abstract methods

* remove unused import

* fix list-metrics command

* Refactor wily config into submodule to reduce cyclical imports

* Move defaults into separate submodule

* Update git imports

* Force tabulate range

* Fix a bug in the archiver fallback sequence

* Cleanup temp paths

* Update mypy call

* Don't use argument cls in generics, not supported in Python 3.7,3.8

* Update metric type field name

* Update list_metrics

* Update some missing return values, remove more docstring types

* Add missing import
  • Loading branch information
tonybaloney committed Aug 4, 2023
1 parent 594abe2 commit 8eb4416
Show file tree
Hide file tree
Showing 35 changed files with 593 additions and 585 deletions.
16 changes: 16 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,19 @@ jobs:
- uses: actions/checkout@v3
- run: pip install --user ruff
- run: ruff --format=github .

pyright:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: 3.11
- name: install dependencies
run: |
python -m pip install --upgrade pip setuptools wheel
pip install flit
flit install --extras=all
- uses: jakebailey/pyright-action@v1
with:
working-directory: src
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ compile_messages:
lint_python:
ruff .
@# TODO(skarzi): fix type hints and require `mypy` to pass
mypy . || true
mypy --install-types --non-interactive src || true

.PHONY: lint_formatting
lint_formatting:
Expand Down
6 changes: 3 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,12 @@ classifiers = [
"Programming Language :: Python :: 3 :: Only",
]
dependencies = [
"gitpython>=3.0.0,<4.0.0",
"gitpython>=3.1.32,<4.0.0",
"radon>=5.1,<5.2",
"click>=7.0,<9.0",
"nbformat>=5.1.3,<6.0.0",
"colorlog>=4.0.0,<5.0.0",
"tabulate>=0.8.2,<1.0.0",
"tabulate>=0.9.0,<1.0.0",
"plotly>=4.0.0,<6.0.0",
"progress>=1.4,<2.0",
"dataclasses; python_version == '3.6'",
Expand All @@ -37,7 +37,7 @@ dynamic=["version", "description"]
[project.optional-dependencies]
test = [
"pytest~=7.2",
"pytest-cov~=3.0.0",
"pytest-cov~=4.1.0",
]
dev = [
"black~=22.6.0",
Expand Down
6 changes: 3 additions & 3 deletions src/wily/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,16 +60,16 @@
MAX_MESSAGE_WIDTH = 50


def format_date(timestamp):
def format_date(timestamp: float) -> str:
"""Reusable timestamp -> date."""
return datetime.date.fromtimestamp(timestamp).isoformat()


def format_datetime(timestamp):
def format_datetime(timestamp: float) -> str:
"""Reusable timestamp -> datetime."""
return datetime.datetime.fromtimestamp(timestamp).isoformat()


def format_revision(sha):
def format_revision(sha: str) -> str:
"""Return a shorter git sha."""
return sha[:7]
4 changes: 2 additions & 2 deletions src/wily/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
from wily import WILY_LOG_NAME, __version__, logger
from wily.archivers import resolve_archiver
from wily.cache import exists, get_default_metrics
from wily.config import DEFAULT_CONFIG_PATH, DEFAULT_GRID_STYLE
from wily.config import load as load_config
from wily.defaults import DEFAULT_CONFIG_PATH, DEFAULT_GRID_STYLE
from wily.helper import get_style
from wily.helper.custom_enums import ReportFormat
from wily.lang import _
Expand Down Expand Up @@ -503,7 +503,7 @@ def handle_no_cache(context):

if __name__ == "__main__": # pragma: no cover
try:
cli()
cli() # type: ignore
except Exception as runtime:
logger.error(f"Oh no, Wily crashed! See {WILY_LOG_NAME} for information.")
logger.info(
Expand Down
65 changes: 44 additions & 21 deletions src/wily/archivers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,20 @@
Specifies a standard interface for finding revisions (versions) of a path and switching to them.
"""

from collections import namedtuple
from dataclasses import dataclass
from typing import List
from typing import Any, Dict, Generic, List, Optional, Type, TypeVar

from wily.config.types import WilyConfig


@dataclass
class Revision:
"""Represents a revision in the archiver."""

key: str
author_name: str
author_email: str
date: str
author_name: Optional[str]
author_email: Optional[str]
date: int
message: str
tracked_files: List[str]
tracked_dirs: List[str]
Expand All @@ -28,32 +29,31 @@ class Revision:
class BaseArchiver:
"""Abstract Archiver Class."""

name: str

def __init__(self, config: "WilyConfig"):
"""Initialise the archiver."""
...

def revisions(self, path: str, max_revisions: int) -> List[Revision]:
"""
Get the list of revisions.
:param path: the path to target.
:type path: ``str``
:param max_revisions: the maximum number of revisions.
:type max_revisions: ``int``
:return: A list of revisions.
:rtype: ``list`` of :class:`Revision`
"""
raise NotImplementedError
...

def checkout(self, revision: Revision, **options):
def checkout(self, revision: Revision, options: Dict[Any, Any]) -> None:
"""
Checkout a specific revision.
:param revision: The revision identifier.
:type revision: :class:`Revision`
:param options: Any additional options.
:type options: ``dict``
"""
raise NotImplementedError
...

def finish(self):
"""Clean up any state if processing completed/failed."""
Expand All @@ -69,22 +69,45 @@ def find(self, search: str) -> Revision:
:return: An instance of revision.
:rtype: Instance of :class:`Revision`
"""
raise NotImplementedError
...


from wily.archivers.filesystem import FilesystemArchiver
from wily.archivers.git import GitArchiver

"""Type for an operator"""
Archiver = namedtuple("Archiver", "name cls description")
"""Type for an Archiver"""

T = TypeVar("T")


"""Git Operator defined in `wily.archivers.git`"""
ARCHIVER_GIT = Archiver(name="git", cls=GitArchiver, description="Git archiver")
class Archiver(Generic[T]):
"""Holder for archivers."""

name: str
archiver_cls: Type[T]
description: str

def __init__(self, name: str, archiver_cls: Type[T], description: str):
"""Initialise the archiver."""
self.name = name
self.archiver_cls = archiver_cls
self.description = description

def __str__(self):
"""Return the name of the archiver."""
return self.name


"""Git Archiver defined in `wily.archivers.git`"""
ARCHIVER_GIT = Archiver(
name="git", archiver_cls=GitArchiver, description="Git archiver"
)

"""Filesystem archiver"""
ARCHIVER_FILESYSTEM = Archiver(
name="filesystem", cls=FilesystemArchiver, description="Filesystem archiver"
name="filesystem",
archiver_cls=FilesystemArchiver,
description="Filesystem archiver",
)

"""Set of all available archivers"""
Expand Down
16 changes: 4 additions & 12 deletions src/wily/archivers/filesystem.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@
import hashlib
import logging
import os.path
from typing import List
from typing import Any, Dict, List

from wily.archivers import BaseArchiver, Revision
from wily.config.types import WilyConfig

logger = logging.getLogger(__name__)

Expand All @@ -18,12 +19,11 @@ class FilesystemArchiver(BaseArchiver):

name = "filesystem"

def __init__(self, config):
def __init__(self, config: "WilyConfig"):
"""
Instantiate a new Filesystem Archiver.
:param config: The wily configuration
:type config: :class:`wily.config.WilyConfig`
"""
self.config = config

Expand All @@ -32,13 +32,8 @@ def revisions(self, path: str, max_revisions: int) -> List[Revision]:
Get the list of revisions.
:param path: the path to target.
:type path: ``str``
:param max_revisions: the maximum number of revisions.
:type max_revisions: ``int``
:return: A list of revisions.
:rtype: ``list`` of :class:`Revision`
"""
mtime = os.path.getmtime(path)
key = hashlib.sha1(str(mtime).encode()).hexdigest()[:7]
Expand All @@ -57,15 +52,12 @@ def revisions(self, path: str, max_revisions: int) -> List[Revision]:
)
]

def checkout(self, revision: Revision, options):
def checkout(self, revision: Revision, options: Dict[Any, Any]) -> None:
"""
Checkout a specific revision.
:param revision: The revision identifier.
:type revision: :class:`Revision`
:param options: Any additional options.
:type options: ``dict``
"""
# effectively noop since there are no revision
pass
35 changes: 16 additions & 19 deletions src/wily/archivers/git.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@
Implementation of the archiver API for the gitpython module.
"""
import logging
from typing import Dict, List, Tuple
from typing import Any, Dict, List, Tuple

import git.exc
from git import Commit
from git.repo import Repo

from wily.archivers import BaseArchiver, Revision
from wily.config.types import WilyConfig

logger = logging.getLogger(__name__)

Expand All @@ -24,7 +25,7 @@ class InvalidGitRepositoryError(Exception):
class DirtyGitRepositoryError(Exception):
"""Error for a dirty git repository (untracked files)."""

def __init__(self, untracked_files):
def __init__(self, untracked_files: List[str]):
"""
Raise error for untracked files.
Expand All @@ -38,10 +39,16 @@ def __init__(self, untracked_files):
def get_tracked_files_dirs(repo: Repo, commit: Commit) -> Tuple[List[str], List[str]]:
"""Get tracked files in a repo for a commit hash using ls-tree."""
paths = repo.git.execute(
["git", "ls-tree", "--name-only", "--full-tree", "-r", commit.hexsha]
["git", "ls-tree", "--name-only", "--full-tree", "-r", commit.hexsha],
with_extended_output=False,
as_process=False,
stdout_as_string=True,
).split("\n")
dirs = [""] + repo.git.execute(
["git", "ls-tree", "--name-only", "--full-tree", "-r", "-d", commit.hexsha]
["git", "ls-tree", "--name-only", "--full-tree", "-r", "-d", commit.hexsha],
with_extended_output=False,
as_process=False,
stdout_as_string=True,
).split("\n")
return paths, dirs

Expand Down Expand Up @@ -72,12 +79,11 @@ class GitArchiver(BaseArchiver):

name = "git"

def __init__(self, config):
def __init__(self, config: "WilyConfig"):
"""
Instantiate a new Git Archiver.
:param config: The wily configuration
:type config: :class:`wily.config.WilyConfig`
"""
try:
self.repo = Repo(config.path)
Expand All @@ -96,18 +102,14 @@ def revisions(self, path: str, max_revisions: int) -> List[Revision]:
Get the list of revisions.
:param path: the path to target.
:type path: ``str``
:param max_revisions: the maximum number of revisions.
:type max_revisions: ``int``
:return: A list of revisions.
:rtype: ``list`` of :class:`Revision`
"""
if self.repo.is_dirty():
raise DirtyGitRepositoryError(self.repo.untracked_files)

revisions = []
revisions: List[Revision] = []
for commit in self.repo.iter_commits(
self.current_branch, max_count=max_revisions, reverse=True
):
Expand All @@ -133,7 +135,7 @@ def revisions(self, path: str, max_revisions: int) -> List[Revision]:
author_name=commit.author.name,
author_email=commit.author.email,
date=commit.committed_date,
message=commit.message,
message=str(commit.message),
tracked_files=tracked_files,
tracked_dirs=tracked_dirs,
added_files=added_files,
Expand All @@ -143,15 +145,12 @@ def revisions(self, path: str, max_revisions: int) -> List[Revision]:
revisions.append(rev)
return revisions[::-1]

def checkout(self, revision: Revision, options: Dict):
def checkout(self, revision: Revision, options: Dict[Any, Any]) -> None:
"""
Checkout a specific revision.
:param revision: The revision identifier.
:type revision: :class:`Revision`
:param options: Any additional options.
:type options: ``dict``
"""
rev = revision.key
self.repo.git.checkout(rev)
Expand All @@ -170,10 +169,8 @@ def find(self, search: str) -> Revision:
Search a string and return a single revision.
:param search: The search term.
:type search: ``str``
:return: An instance of revision.
:rtype: Instance of :class:`Revision`
"""
commit = self.repo.commit(search)
tracked_files, tracked_dirs = get_tracked_files_dirs(self.repo, commit)
Expand All @@ -191,7 +188,7 @@ def find(self, search: str) -> Revision:
author_name=commit.author.name,
author_email=commit.author.email,
date=commit.committed_date,
message=commit.message,
message=str(commit.message),
tracked_files=tracked_files,
tracked_dirs=tracked_dirs,
added_files=added_files,
Expand Down
Loading

0 comments on commit 8eb4416

Please sign in to comment.