Source code for github3_utils.check_labels

#!/usr/bin/env python3
#
#  check_labels.py
"""
Helpers for creating labels to mark pull requests with which tests are failing.

.. versionadded:: 0.4.0
"""
#
#  Copyright © 2021 Dominic Davis-Foster <dominic@davis-foster.co.uk>
#
#  Permission is hereby granted, free of charge, to any person obtaining a copy
#  of this software and associated documentation files (the "Software"), to deal
#  in the Software without restriction, including without limitation the rights
#  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
#  copies of the Software, and to permit persons to whom the Software is
#  furnished to do so, subject to the following conditions:
#
#  The above copyright notice and this permission notice shall be included in all
#  copies or substantial portions of the Software.
#
#  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
#  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
#  MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
#  IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
#  DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
#  OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
#  OR OTHER DEALINGS IN THE SOFTWARE.
#

# stdlib
import re
from typing import Dict, NamedTuple, Set, Union

# 3rd party
import attr
import github3.issues.label
from domdf_python_tools.doctools import prettify_docstrings
from github3.checks import CheckRun
from github3.issues import Issue
from github3.pulls import PullRequest, ShortPullRequest
from github3.repos import Repository
from github3.repos.commit import ShortCommit

__all__ = ("Label", "check_status_labels", "Checks", "get_checks_for_pr", "label_pr_failures")


[docs]@prettify_docstrings @attr.s(frozen=True, slots=True) class Label: """ Represents an issue or pull request label. """ #: The text of the label. name: str = attr.ib(converter=str) #: The background colour of the label. color: str = attr.ib(converter=str) #: A short description of the label. description: str = attr.ib(default=None)
[docs] def __str__(self) -> str: return self.name
[docs] def to_dict(self) -> Dict[str, str]: """ Return the :class:`~.Label` as a dictionary. """ return { "name": self.name, "color": self.color, "description": self.description, }
[docs] def create(self, repo: Repository) -> github3.issues.label.Label: """ Create this label on the given repository. :param repo: """ return repo.create_label(**self.to_dict())
check_status_labels: Dict[str, Label] = { label.name: label for label in [ Label("failure: flake8", "#B60205", "The Flake8 check is failing."), Label("failure: mypy", "#DC1C13", "The mypy check is failing."), Label("failure: docs", "#EA4C46", "The docs check is failing."), Label("failure: Windows", "#F07470", "The Windows tests are failing."), Label("failure: Linux", "#F6BDC0", "The Linux tests are failing."), # Label("failure: Multiple", "#D93F0B", "Multiple checks are failing."), ] } """ Labels corresponding to failing pull request checks. """ # The ``failure: Multiple`` label is used if three or more categories failing.
[docs]class Checks(NamedTuple): """ Represents the sets of status checks returned by :func:`~.get_checks_for_pr`. """ successful: Set[str] failing: Set[str] running: Set[str] skipped: Set[str] neutral: Set[str]
[docs]def get_checks_for_pr(pull: Union[PullRequest, ShortPullRequest]) -> Checks: """ Returns a :class:`~.Checks` object containing sets of check names grouped by their status. :param pull: The pull request to obtain checks for. """ head_commit: ShortCommit = list(pull.commits())[-1] failing = set() running = set() successful = set() skipped = set() neutral = set() check_run: CheckRun for check_run in head_commit.check_runs(): # pylint: disable=loop-invariant-statement if check_run.status in {"queued", "running", "in_progress"}: running.add(check_run.name) elif check_run.conclusion in {"failure", "cancelled", "timed_out", "action_required"}: failing.add(check_run.name) elif check_run.conclusion == "success": successful.add(check_run.name) elif check_run.conclusion == "skipped": skipped.add(check_run.name) elif check_run.conclusion == "neutral": neutral.add(check_run.name) # pylint: enable=loop-invariant-statement # Remove failing checks from successful etc. (as all checks appear twice for PRs) successful = successful - failing - running running = running - failing skipped = skipped - running - failing - successful neutral = neutral - running - failing - successful return Checks( successful=successful, failing=failing, running=running, skipped=skipped, neutral=neutral, )
_python_dev_re = re.compile(r".*Python\s*\d+\.\d+.*(dev|alpha|beta|rc).*", flags=re.IGNORECASE)
[docs]def label_pr_failures(pull: Union[PullRequest, ShortPullRequest]) -> Set[str]: """ Labels the given pull request to indicate which checks are failing. :param pull: :return: The new labels set for the pull request. """ pr_checks = get_checks_for_pr(pull) failure_labels: Set[str] = set() success_labels: Set[str] = set() def determine_labels(from_: Set[str], to: Set[str]) -> None: for check in from_: if _python_dev_re.match(check): # pylint: disable=loop-global-usage continue # pylint: disable=loop-invariant-statement if check in {"Flake8", "docs"}: to.add(f"failure: {check.lower()}") elif check.startswith("mypy"): to.add("failure: mypy") elif check.startswith("ubuntu"): to.add("failure: Linux") elif check.startswith("windows"): to.add("failure: Windows") # pylint: enable=loop-invariant-statement determine_labels(pr_checks.failing, failure_labels) determine_labels(pr_checks.successful, success_labels) issue: Issue = pull.issue() current_labels = {label.name for label in issue.labels()} for label in success_labels: if label in current_labels and label not in failure_labels: issue.remove_label(label) current_labels -= success_labels current_labels.update(failure_labels) issue.add_labels(*current_labels) return current_labels