# -*- coding: utf-8 -*-
"""
github3.pulls
=============
This module contains all the classes relating to pull requests.
"""
from __future__ import unicode_literals
from re import match
from json import dumps
from .repos.commit import RepoCommit
from .models import GitHubObject, GitHubCore, BaseComment
from .users import User
from .decorators import requires_auth
from .issues.comment import IssueComment
from uritemplate import URITemplate
class PullDestination(GitHubCore):
"""The :class:`PullDestination ` object.
See also: http://developer.github.com/v3/pulls/#get-a-single-pull-request
"""
def __init__(self, dest, direction):
super(PullDestination, self).__init__(dest)
#: Direction of the merge with respect to this destination
self.direction = direction
#: Full reference string of the object
self.ref = dest.get('ref')
#: label of the destination
self.label = dest.get('label')
#: :class:`User ` representing the owner
self.user = None
if dest.get('user'):
self.user = User(dest.get('user'), None)
#: SHA of the commit at the head
self.sha = dest.get('sha')
self._repo_name = ''
self._repo_owner = ''
if dest.get('repo'):
self._repo_name = dest['repo'].get('name')
self._repo_owner = dest['repo']['owner'].get('login')
self.repo = (self._repo_owner, self._repo_name)
def _repr(self):
return '<{0} [{1}]>'.format(self.direction, self.label)
class PullFile(GitHubObject):
"""The :class:`PullFile ` object.
See also: http://developer.github.com/v3/pulls/#list-pull-requests-files
"""
def _update_attributes(self, pfile):
#: SHA of the commit
self.sha = pfile.get('sha')
#: Name of the file
self.filename = pfile.get('filename')
#: Status of the file, e.g., 'added'
self.status = pfile.get('status')
#: Number of additions on this file
self.additions_count = pfile.get('additions')
#: Number of deletions on this file
self.deletions_count = pfile.get('deletions')
#: Number of changes made to this file
self.changes_count = pfile.get('changes')
#: URL to view the blob for this file
self.blob_url = pfile.get('blob_url')
#: URL to view the raw diff of this file
self.raw_url = pfile.get('raw_url')
#: Patch generated by this pull request
self.patch = pfile.get('patch')
def _repr(self):
return ''.format(self.filename)
class PullRequest(GitHubCore):
"""The :class:`PullRequest ` object.
Two pull request instances can be checked like so::
p1 == p2
p1 != p2
And is equivalent to::
p1.id == p2.id
p1.id != p2.id
See also: http://developer.github.com/v3/pulls/
"""
def _update_attributes(self, pull):
self._api = pull.get('url', '')
#: Base of the merge
self.base = PullDestination(pull.get('base'), 'Base')
#: Body of the pull request message
self.body = pull.get('body', '')
#: Body of the pull request as HTML
self.body_html = pull.get('body_html', '')
#: Body of the pull request as plain text
self.body_text = pull.get('body_text', '')
#: Number of additions on this pull request
self.additions_count = pull.get('additions')
#: Number of deletions on this pull request
self.deletions_count = pull.get('deletions')
#: datetime object representing when the pull was closed
self.closed_at = self._strptime(pull.get('closed_at'))
#: Number of comments
self.comments_count = pull.get('comments')
#: Comments url (not a template)
self.comments_url = pull.get('comments_url')
#: Number of commits
self.commits_count = pull.get('commits')
#: GitHub.com url of commits in this pull request
self.commits_url = pull.get('commits_url')
#: datetime object representing when the pull was created
self.created_at = self._strptime(pull.get('created_at'))
#: URL to view the diff associated with the pull
self.diff_url = pull.get('diff_url')
#: The new head after the pull request
self.head = PullDestination(pull.get('head'), 'Head')
#: The URL of the pull request
self.html_url = pull.get('html_url')
#: The unique id of the pull request
self.id = pull.get('id')
#: The URL of the associated issue
self.issue_url = pull.get('issue_url')
#: Statuses URL
self.statuses_url = pull.get('statuses_url')
#: Dictionary of _links. Changed in 1.0
self.links = pull.get('_links')
#: datetime object representing when the pull was merged
self.merged_at = self._strptime(pull.get('merged_at'))
#: Whether the pull is deemed mergeable by GitHub
self.mergeable = pull.get('mergeable', False)
#: Whether it would be a clean merge or not
self.mergeable_state = pull.get('mergeable_state', '')
user = pull.get('merged_by')
#: :class:`User ` who merged this pull
self.merged_by = User(user, self) if user else None
#: Number of the pull/issue on the repository
self.number = pull.get('number')
#: The URL of the patch
self.patch_url = pull.get('patch_url')
comments = pull.get('review_comment_url')
#: Review comment URL Template. Expands with ``number``
self.review_comment_url = URITemplate(comments) if comments else None
#: Number of review comments on the pull request
self.review_comments_count = pull.get('review_comments')
#: GitHub.com url for review comments (not a template)
self.review_comments_url = pull.get('review_comments_url')
m = match('https?://[\w\d\-\.\:]+/(\S+)/(\S+)/(?:issues|pull)?/\d+',
self.issue_url)
#: Returns ('owner', 'repository') this issue was filed on.
self.repository = m.groups()
#: The state of the pull
self.state = pull.get('state')
#: The title of the request
self.title = pull.get('title')
#: datetime object representing the last time the object was changed
self.updated_at = self._strptime(pull.get('updated_at'))
#: :class:`User ` object representing the creator
#: of the pull request
self.user = pull.get('user')
if self.user:
self.user = User(self.user, self)
#: :class:`User ` object representing the assignee
#: of the pull request
self.assignee = pull.get('assignee')
if self.assignee:
self.assignee = User(self.assignee, self)
def _repr(self):
return ''.format(self.number)
@requires_auth
def close(self):
"""Close this Pull Request without merging.
:returns: bool
"""
return self.update(self.title, self.body, 'closed')
@requires_auth
def create_review_comment(self, body, commit_id, path, position):
"""Create a review comment on this pull request.
All parameters are required by the GitHub API.
:param str body: The comment text itself
:param str commit_id: The SHA of the commit to comment on
:param str path: The relative path of the file to comment on
:param int position: The line index in the diff to comment on.
:returns: The created review comment.
:rtype: :class:`~github3.pulls.ReviewComment`
"""
url = self._build_url('comments', base_url=self._api)
data = {'body': body, 'commit_id': commit_id, 'path': path,
'position': str(position)}
json = self._json(self._post(url, data=data), 201)
return self._instance_or_null(ReviewComment, json)
def diff(self):
"""Return the diff.
:returns: bytestring representation of the diff.
"""
resp = self._get(self._api,
headers={'Accept': 'application/vnd.github.diff'})
return resp.content if self._boolean(resp, 200, 404) else b''
def is_merged(self):
"""Check to see if the pull request was merged.
:returns: bool
"""
url = self._build_url('merge', base_url=self._api)
return self._boolean(self._get(url), 204, 404)
def commits(self, number=-1, etag=None):
r"""Iterate over the commits on this pull request.
:param int number: (optional), number of commits to return. Default:
-1 returns all available commits.
:param str etag: (optional), ETag from a previous request to the same
endpoint
:returns: generator of
:class:`RepoCommit `\ s
"""
url = self._build_url('commits', base_url=self._api)
return self._iter(int(number), url, RepoCommit, etag=etag)
def files(self, number=-1, etag=None):
r"""Iterate over the files associated with this pull request.
:param int number: (optional), number of files to return. Default:
-1 returns all available files.
:param str etag: (optional), ETag from a previous request to the same
endpoint
:returns: generator of :class:`PullFile `\ s
"""
url = self._build_url('files', base_url=self._api)
return self._iter(int(number), url, PullFile, etag=etag)
def issue_comments(self, number=-1, etag=None):
r"""Iterate over the issue comments on this pull request.
:param int number: (optional), number of comments to return. Default:
-1 returns all available comments.
:param str etag: (optional), ETag from a previous request to the same
endpoint
:returns: generator of :class:`IssueComment `\ s
"""
comments = self.links.get('comments', {})
url = comments.get('href')
if not url:
url = self._build_url(
'comments', base_url=self._api.replace('pulls', 'issues')
)
return self._iter(int(number), url, IssueComment, etag=etag)
@requires_auth
def merge(self, commit_message=''):
"""Merge this pull request.
:param str commit_message: (optional), message to be used for the
merge commit
:returns: bool
"""
data = None
if commit_message:
data = dumps({'commit_message': commit_message})
url = self._build_url('merge', base_url=self._api)
json = self._json(self._put(url, data=data), 200)
if not json:
return False
return json['merged']
def patch(self):
"""Return the patch.
:returns: bytestring representation of the patch
"""
resp = self._get(self._api,
headers={'Accept': 'application/vnd.github.patch'})
return resp.content if self._boolean(resp, 200, 404) else b''
@requires_auth
def reopen(self):
"""Re-open a closed Pull Request.
:returns: bool
"""
return self.update(self.title, self.body, 'open')
def review_comments(self, number=-1, etag=None):
r"""Iterate over the review comments on this pull request.
:param int number: (optional), number of comments to return. Default:
-1 returns all available comments.
:param str etag: (optional), ETag from a previous request to the same
endpoint
:returns: generator of :class:`ReviewComment `\ s
"""
url = self._build_url('comments', base_url=self._api)
return self._iter(int(number), url, ReviewComment, etag=etag)
@requires_auth
def update(self, title=None, body=None, state=None):
"""Update this pull request.
:param str title: (optional), title of the pull
:param str body: (optional), body of the pull request
:param str state: (optional), ('open', 'closed')
:returns: bool
"""
data = {'title': title, 'body': body, 'state': state}
json = None
self._remove_none(data)
if data:
json = self._json(self._patch(self._api, data=dumps(data)), 200)
if json:
self._update_attributes(json)
return True
return False
class ReviewComment(BaseComment):
"""The :class:`ReviewComment ` object.
This is used to represent comments on pull requests.
Two comment instances can be checked like so::
c1 == c2
c1 != c2
And is equivalent to::
c1.id == c2.id
c1.id != c2.id
See also: http://developer.github.com/v3/pulls/comments/
"""
def _update_attributes(self, comment):
super(ReviewComment, self)._update_attributes(comment)
#: :class:`User ` who made the comment
self.user = None
if comment.get('user'):
self.user = User(comment.get('user'), self)
#: Original position inside the file
self.original_position = comment.get('original_position')
#: Path to the file
self.path = comment.get('path')
#: Position within the commit
self.position = comment.get('position') or 0
#: SHA of the commit the comment is on
self.commit_id = comment.get('commit_id')
#: The diff hunk
self.diff_hunk = comment.get('diff_hunk')
#: Original commit SHA
self.original_commit_id = comment.get('original_commit_id')
#: API URL for the Pull Request
self.pull_request_url = comment.get('pull_request_url')
def _repr(self):
return ''.format(self.user.login)
@requires_auth
def reply(self, body):
"""Reply to this review comment with a new review comment.
:param str body: The text of the comment.
:returns: The created review comment.
:rtype: :class:`~github3.pulls.ReviewComment`
"""
url = self._build_url('comments', base_url=self.pull_request_url)
index = self._api.rfind('/') + 1
in_reply_to = self._api[index:]
json = self._json(self._post(url, data={
'body': body, 'in_reply_to': in_reply_to
}), 201)
return self._instance_or_null(ReviewComment, json)