Skip to content

Commit

Permalink
feat: add type annotations throughout (#16)
Browse files Browse the repository at this point in the history
* feat: add type annotations throughout

* test: enable mypy as a pre-commit hook

* fix: address mypy violations

* style: sort pyproject.toml

* fix: add py.typed per pep-561

* test: add pylint pre-commit check

* test: enable mypy strict checks

* feat: lock fuzzfetch to 1.3.3

* fix: address mypy strict violations
  • Loading branch information
pyoor committed Jul 30, 2021
1 parent 6fccc77 commit ac70459
Show file tree
Hide file tree
Showing 8 changed files with 219 additions and 93 deletions.
12 changes: 12 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,15 @@ repos:
rev: v0.19.0
hooks:
- id: toml-sort
- repo: local
hooks:
- id: pylint
name: pylint
entry: poetry run pylint -j 0 bugmon
pass_filenames: false
language: system
- id: mypy
name: mypy
entry: poetry run mypy bugmon
pass_filenames: false
language: system
109 changes: 61 additions & 48 deletions bugmon/bug.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@
import platform
import re
from datetime import datetime
from typing import Any, Dict, List, Union, Optional, NoReturn, cast, Type

import requests
from autobisect import JSEvaluator
from bugsy import Attachment, Bug, Comment
from bugsy import Attachment, Bug, Comment, Bugsy
from fuzzfetch import BuildFlags, Fetcher, FetcherException, Platform

from .utils import HG_BASE, _get_milestone, _get_url
Expand All @@ -20,7 +21,7 @@
BID_MATCH = r"([0-9]{8}-)([a-f0-9]{12})"


def sanitize_bug(obj):
def sanitize_bug(obj: Any) -> Any:
"""
Helper method for converting Bug to JSON
:param obj:
Expand Down Expand Up @@ -67,31 +68,31 @@ class EnhancedBug(Bug):
}
)

def __init__(self, bugsy, **kwargs):
def __init__(self, bugsy: Bugsy, **kwargs: Dict[str, Any]):
"""Initializes LocalAttachment"""
super().__init__(bugsy, **kwargs)

if bugsy is None and ("attachments" not in kwargs or "comments" not in kwargs):
raise BugException("Cannot init Bug without Bugsy instance or cached data")

# Initialize placeholders
self._branch = None
self._branches = None
self._build_flags = None
self._central_version = None
self._comment_zero = None
self._env_variables = None
self._initial_build_id = None
self._platform = None

def __setattr__(self, attr, value):
self._branch: Optional[str] = None
self._branches: Optional[Dict[str, int]] = None
self._build_flags: Optional[BuildFlags] = None
self._central_version: Optional[int] = None
self._comment_zero: Optional[str] = None
self._env_variables: Optional[Dict[str, str]] = None
self._initial_build_id: Optional[str] = None
self._platform: Optional[Platform] = None

def __setattr__(self, attr: str, value: Any) -> None:
if attr in self.LOCAL_ATTRS:
object.__setattr__(self, attr, value)
else:
super().__setattr__(attr, value)

@property
def branch(self):
def branch(self) -> str:
"""
Attempt to enumerate the branch the bug was filed against
"""
Expand All @@ -101,10 +102,13 @@ def branch(self):
self._branch = alias
break

# Type guard
assert self._branch is not None

return self._branch

@property
def branches(self):
def branches(self) -> Dict[str, int]:
"""
Create map of fuzzfetch branch aliases and bugzilla version tags
"""
Expand All @@ -127,7 +131,7 @@ def branches(self):
return self._branches

@property
def build_flags(self):
def build_flags(self) -> BuildFlags:
"""
Attempt to enumerate build type based on flags listed in comment 0
"""
Expand Down Expand Up @@ -162,7 +166,7 @@ def build_flags(self):
return self._build_flags

@property
def central_version(self):
def central_version(self) -> int:
"""
Return numeric version for tip
"""
Expand All @@ -172,7 +176,7 @@ def central_version(self):
return self._central_version

@property
def commands(self):
def commands(self) -> Dict[str, Optional[str]]:
"""
Attempt to extract commands from whiteboard
"""
Expand All @@ -190,7 +194,7 @@ def commands(self):
return commands

@commands.setter
def commands(self, value):
def commands(self, value: Dict[str, str]) -> None:
parts = ",".join([f"{k}={v}" if v is not None else k for k, v in value.items()])
if len(parts) != 0:
if re.search(r"(?<=\[bugmon:)([^]]*)", self._bug["whiteboard"]):
Expand All @@ -209,7 +213,7 @@ def commands(self, value):
self._bug["whiteboard"] = result

@property
def comment_zero(self):
def comment_zero(self) -> str:
"""
Helper function for retrieving comment zero
"""
Expand All @@ -220,7 +224,7 @@ def comment_zero(self):
return self._comment_zero

@property
def env(self):
def env(self) -> Dict[Any, Any]:
"""
Attempt to enumerate any env_variables required
"""
Expand All @@ -237,14 +241,18 @@ def env(self):
return self._env_variables

@property
def initial_build_id(self):
def initial_build_id(self) -> str:
"""
Attempt to enumerate the original rev specified in comment 0 or bugmon origRev command
"""
if self._initial_build_id is None:
tokens = []
if re.match(rf"^{REV_MATCH}$", self.commands.get("origRev", "")):
tokens.append(self.commands["origRev"])
# Type guard needed due to self.commands.get -> Optional[str]
original_rev = self.commands.get("origRev", "")
assert original_rev is not None

if re.match(rf"^{REV_MATCH}$", original_rev):
tokens.append(original_rev)
else:
tokens.extend(re.findall(r"([A-Za-z0-9_-]+)", self.comment_zero))

Expand All @@ -267,12 +275,13 @@ def initial_build_id(self):
break
else:
# If original rev isn't specified, use the date the bug was created
assert isinstance(self.creation_time, str)
self._initial_build_id = self.creation_time.split("T")[0]

return self._initial_build_id

@property
def platform(self):
def platform(self) -> Platform:
"""
Attempt to enumerate the target platform
"""
Expand Down Expand Up @@ -301,7 +310,7 @@ def platform(self):
return self._platform

@property
def version(self):
def version(self) -> int:
"""
Attempt to enumerate the version the bug was filed against
"""
Expand All @@ -311,7 +320,7 @@ def version(self):
return self.central_version

@property
def runtime_opts(self):
def runtime_opts(self) -> List[str]:
"""
Attempt to enumerate the runtime flags specified in comment 0
"""
Expand All @@ -327,17 +336,17 @@ def runtime_opts(self):

return flags

def get_attachments(self):
def get_attachments(self) -> List[Attachment]:
"""
Return list of attachments
"""
if self._bugsy is None:
attachments = self._bug.get("attachments", [])
return [LocalAttachment(**a) for a in attachments]

return super().get_attachments()
return cast(List[Attachment], super().get_attachments())

def add_attachment(self, attachment):
def add_attachment(self, attachment: Attachment) -> None:
"""
Add a new attachment when a bugsy instance is present
Expand All @@ -347,7 +356,7 @@ def add_attachment(self, attachment):
raise TypeError("Method not supported when using a cached bug")
super().add_attachment(attachment)

def get_comments(self):
def get_comments(self) -> List[Comment]:
"""
Returns list of comments
Bugs without a bugsy instance are expected to include comments
Expand All @@ -356,9 +365,9 @@ def get_comments(self):
comments = self._bug.get("comments", [])
return [LocalComment(**c) for c in comments]

return super().get_comments()
return cast(List[Comment], super().get_comments())

def add_comment(self, comment):
def add_comment(self, comment: Comment) -> None:
"""
Add a new comment when a bugsy instance is present
Expand All @@ -368,20 +377,22 @@ def add_comment(self, comment):
raise TypeError("Method not supported when using a cached bug")
super().add_comment(comment)

def diff(self):
def diff(self) -> Dict[str, Union[str, Dict[str, Union[str, bool]]]]:
"""
Overload Bug.diff() to strip attachments and comments
:return:
"""
changed = super().diff()
changed = cast(
Dict[str, Union[str, Dict[str, Union[str, bool]]]], super().diff()
)

# These keys should never occur in the diff
changed.pop("attachments", None)
changed.pop("comments", None)

return changed

def find_patch_rev(self, branch):
def find_patch_rev(self, branch: str) -> Optional[str]:
"""
Attempt to determine patch rev for the supplied branch
Expand All @@ -395,14 +406,16 @@ def find_patch_rev(self, branch):
pattern = re.compile(rf"(?:{HG_BASE}/releases/{alias}/rev/){REV_MATCH}")

comments = self.get_comments()
for comment in sorted(comments, key=lambda c: c.creation_time, reverse=True):
for comment in sorted(
comments, key=lambda c: cast(str, c.creation_time), reverse=True
):
match = pattern.match(comment.text)
if match:
return match.group(1)

return None

def to_dict(self):
def to_dict(self) -> Dict[str, Any]:
"""
Bug.to_dict() is used via Bugsy remote methods
To avoid sending bad data, we need to exclude attachments and comments
Expand All @@ -411,7 +424,7 @@ def to_dict(self):
excluded = ["attachments", "comments"]
return {k: v for k, v in self._bug.items() if k not in excluded}

def to_json(self):
def to_json(self) -> str:
"""
Export entire bug in JSON safe format
May include attachments and comments
Expand All @@ -420,15 +433,15 @@ def to_json(self):
"""
return json.dumps(self._bug, default=sanitize_bug)

def update(self):
def update(self) -> None:
"""Update bug when a bugsy instance is present"""
if self._bugsy is None:
raise TypeError("Method not supported when using a cached bug")

super().update()

@classmethod
def cache_bug(cls, bug):
def cache_bug(cls: Type["EnhancedBug"], bug: "EnhancedBug") -> "EnhancedBug":
"""
Create a cached instance of EnhancedBug
Expand Down Expand Up @@ -459,11 +472,11 @@ class LocalAttachment(Attachment):
:param kwargs: Bug data
"""

def __init__(self, **kwargs):
def __init__(self, **kwargs: Dict[str, Any]) -> None:
"""Initializes LocalAttachment"""
super().__init__(None, **kwargs)

def update(self):
def update(self) -> NoReturn:
"""
Disable update
"""
Expand All @@ -477,28 +490,28 @@ class LocalComment(Comment):
:param kwargs: Comment data
"""

def __init__(self, **kwargs):
def __init__(self, **kwargs: Dict[str, Any]) -> None:
"""Initializes LocalComment"""
super().__init__(None, **kwargs)

def add_tags(self, tags):
def add_tags(self, tags: Union[str, List[str]]) -> NoReturn:
"""
Disable add_tags
:param tags:
"""
raise TypeError("Method not supported when using a cached comment")

def remove_tags(self, tags):
def remove_tags(self, tags: Union[str, List[str]]) -> NoReturn:
"""
Disable remove_tags
:param tags:
"""
raise TypeError("Method not supported when using a cached comment")

def to_dict(self):
def to_dict(self) -> Dict[str, Any]:
"""
Return comment content as dict
"""
return self._comment
return cast(Dict[str, Any], self._comment)
Loading

0 comments on commit ac70459

Please sign in to comment.