/ github / PullRequest.py
PullRequest.py
  1  ############################ Copyrights and license ############################
  2  #                                                                              #
  3  # Copyright 2012 Michael Stead <michael.stead@gmail.com>                       #
  4  # Copyright 2012 Vincent Jacques <vincent@vincent-jacques.net>                 #
  5  # Copyright 2012 Zearin <zearin@gonk.net>                                      #
  6  # Copyright 2013 AKFish <akfish@gmail.com>                                     #
  7  # Copyright 2013 Vincent Jacques <vincent@vincent-jacques.net>                 #
  8  # Copyright 2013 martinqt <m.ki2@laposte.net>                                  #
  9  # Copyright 2014 Vincent Jacques <vincent@vincent-jacques.net>                 #
 10  # Copyright 2016 @tmshn <tmshn@r.recruit.co.jp>                                #
 11  # Copyright 2016 Jannis Gebauer <ja.geb@me.com>                                #
 12  # Copyright 2016 Peter Buckley <dx-pbuckley@users.noreply.github.com>          #
 13  # Copyright 2017 Aaron Levine <allevin@sandia.gov>                             #
 14  # Copyright 2017 Simon <spam@esemi.ru>                                         #
 15  # Copyright 2018 Ben Yohay <ben@lightricks.com>                                #
 16  # Copyright 2018 Gilad Shefer <gshefer@redhat.com>                             #
 17  # Copyright 2018 Martin Monperrus <monperrus@users.noreply.github.com>         #
 18  # Copyright 2018 Matt Babineau <9685860+babineaum@users.noreply.github.com>    #
 19  # Copyright 2018 Shinichi TAMURA <shnch.tmr@gmail.com>                         #
 20  # Copyright 2018 Steve Kowalik <steven@wedontsleep.org>                        #
 21  # Copyright 2018 Thibault Jamet <tjamet@users.noreply.github.com>              #
 22  # Copyright 2018 per1234 <accounts@perglass.com>                               #
 23  # Copyright 2018 sfdye <tsfdye@gmail.com>                                      #
 24  #                                                                              #
 25  # This file is part of PyGithub.                                               #
 26  # http://pygithub.readthedocs.io/                                              #
 27  #                                                                              #
 28  # PyGithub is free software: you can redistribute it and/or modify it under    #
 29  # the terms of the GNU Lesser General Public License as published by the Free  #
 30  # Software Foundation, either version 3 of the License, or (at your option)    #
 31  # any later version.                                                           #
 32  #                                                                              #
 33  # PyGithub is distributed in the hope that it will be useful, but WITHOUT ANY  #
 34  # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS    #
 35  # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more #
 36  # details.                                                                     #
 37  #                                                                              #
 38  # You should have received a copy of the GNU Lesser General Public License     #
 39  # along with PyGithub. If not, see <http://www.gnu.org/licenses/>.             #
 40  #                                                                              #
 41  ################################################################################
 42  from __future__ import annotations
 43  
 44  import urllib.parse
 45  from datetime import datetime
 46  from typing import TYPE_CHECKING, Any
 47  
 48  from typing_extensions import NotRequired, TypedDict
 49  
 50  import github.Commit
 51  import github.File
 52  import github.IssueComment
 53  import github.IssueEvent
 54  import github.Label
 55  import github.Milestone
 56  import github.NamedUser
 57  import github.PaginatedList
 58  import github.PullRequestComment
 59  import github.PullRequestMergeStatus
 60  import github.PullRequestPart
 61  import github.PullRequestReview
 62  import github.Team
 63  from github import Consts
 64  from github.GithubObject import (
 65      Attribute,
 66      CompletableGithubObject,
 67      NotSet,
 68      Opt,
 69      is_defined,
 70      is_optional,
 71      is_optional_list,
 72      is_undefined,
 73  )
 74  from github.Issue import Issue
 75  from github.PaginatedList import PaginatedList
 76  
 77  if TYPE_CHECKING:
 78      from github.NamedUser import NamedUser
 79  
 80  
 81  class ReviewComment(TypedDict):
 82      path: str
 83      position: NotRequired[int]
 84      body: str
 85      line: NotRequired[int]
 86      side: NotRequired[str]
 87      start_line: NotRequired[int]
 88      start_side: NotRequired[str]
 89  
 90  
 91  class PullRequest(CompletableGithubObject):
 92      """
 93      This class represents PullRequests. The reference can be found here https://docs.github.com/en/rest/reference/pulls
 94      """
 95  
 96      def _initAttributes(self) -> None:
 97          self._additions: Attribute[int] = NotSet
 98          self._assignee: Attribute[github.NamedUser.NamedUser] = NotSet
 99          self._assignees: Attribute[list[NamedUser]] = NotSet
100          self._base: Attribute[github.PullRequestPart.PullRequestPart] = NotSet
101          self._body: Attribute[str] = NotSet
102          self._changed_files: Attribute[int] = NotSet
103          self._closed_at: Attribute[datetime | None] = NotSet
104          self._comments: Attribute[int] = NotSet
105          self._comments_url: Attribute[str] = NotSet
106          self._commits: Attribute[int] = NotSet
107          self._commits_url: Attribute[str] = NotSet
108          self._created_at: Attribute[datetime] = NotSet
109          self._deletions: Attribute[int] = NotSet
110          self._diff_url: Attribute[str] = NotSet
111          self._draft: Attribute[bool] = NotSet
112          self._head: Attribute[github.PullRequestPart.PullRequestPart] = NotSet
113          self._html_url: Attribute[str] = NotSet
114          self._id: Attribute[int] = NotSet
115          self._issue_url: Attribute[str] = NotSet
116          self._labels: Attribute[list[github.Label.Label]] = NotSet
117          self._merge_commit_sha: Attribute[str] = NotSet
118          self._mergeable: Attribute[bool] = NotSet
119          self._mergeable_state: Attribute[str] = NotSet
120          self._merged: Attribute[bool] = NotSet
121          self._merged_at: Attribute[datetime | None] = NotSet
122          self._merged_by: Attribute[github.NamedUser.NamedUser] = NotSet
123          self._milestone: Attribute[github.Milestone.Milestone] = NotSet
124          self._number: Attribute[int] = NotSet
125          self._patch_url: Attribute[str] = NotSet
126          self._rebaseable: Attribute[bool] = NotSet
127          self._requested_reviewers: Attribute[list[NamedUser]] = NotSet
128          self._review_comment_url: Attribute[str] = NotSet
129          self._review_comments: Attribute[int] = NotSet
130          self._review_comments_url: Attribute[str] = NotSet
131          self._state: Attribute[str] = NotSet
132          self._title: Attribute[str] = NotSet
133          self._updated_at: Attribute[datetime | None] = NotSet
134          self._url: Attribute[str] = NotSet
135          self._user: Attribute[github.NamedUser.NamedUser] = NotSet
136          self._maintainer_can_modify: Attribute[bool] = NotSet
137  
138      def __repr__(self) -> str:
139          return self.get__repr__({"number": self._number.value, "title": self._title.value})
140  
141      @property
142      def additions(self) -> int:
143          self._completeIfNotSet(self._additions)
144          return self._additions.value
145  
146      @property
147      def assignee(self) -> github.NamedUser.NamedUser:
148          self._completeIfNotSet(self._assignee)
149          return self._assignee.value
150  
151      @property
152      def assignees(self) -> list[github.NamedUser.NamedUser]:
153          self._completeIfNotSet(self._assignees)
154          return self._assignees.value
155  
156      @property
157      def base(self) -> github.PullRequestPart.PullRequestPart:
158          self._completeIfNotSet(self._base)
159          return self._base.value
160  
161      @property
162      def body(self) -> str:
163          self._completeIfNotSet(self._body)
164          return self._body.value
165  
166      @property
167      def changed_files(self) -> int:
168          self._completeIfNotSet(self._changed_files)
169          return self._changed_files.value
170  
171      @property
172      def closed_at(self) -> datetime | None:
173          self._completeIfNotSet(self._closed_at)
174          return self._closed_at.value
175  
176      @property
177      def comments(self) -> int:
178          self._completeIfNotSet(self._comments)
179          return self._comments.value
180  
181      @property
182      def comments_url(self) -> str:
183          self._completeIfNotSet(self._comments_url)
184          return self._comments_url.value
185  
186      @property
187      def commits(self) -> int:
188          self._completeIfNotSet(self._commits)
189          return self._commits.value
190  
191      @property
192      def commits_url(self) -> str:
193          self._completeIfNotSet(self._commits_url)
194          return self._commits_url.value
195  
196      @property
197      def created_at(self) -> datetime:
198          self._completeIfNotSet(self._created_at)
199          return self._created_at.value
200  
201      @property
202      def deletions(self) -> int:
203          self._completeIfNotSet(self._deletions)
204          return self._deletions.value
205  
206      @property
207      def diff_url(self) -> str:
208          self._completeIfNotSet(self._diff_url)
209          return self._diff_url.value
210  
211      @property
212      def draft(self) -> bool:
213          self._completeIfNotSet(self._draft)
214          return self._draft.value
215  
216      @property
217      def head(self) -> github.PullRequestPart.PullRequestPart:
218          self._completeIfNotSet(self._head)
219          return self._head.value
220  
221      @property
222      def html_url(self) -> str:
223          self._completeIfNotSet(self._html_url)
224          return self._html_url.value
225  
226      @property
227      def id(self) -> int:
228          self._completeIfNotSet(self._id)
229          return self._id.value
230  
231      @property
232      def issue_url(self) -> str:
233          self._completeIfNotSet(self._issue_url)
234          return self._issue_url.value
235  
236      @property
237      def labels(self) -> list[github.Label.Label]:
238          self._completeIfNotSet(self._labels)
239          return self._labels.value
240  
241      @property
242      def merge_commit_sha(self) -> str:
243          self._completeIfNotSet(self._merge_commit_sha)
244          return self._merge_commit_sha.value
245  
246      @property
247      def mergeable(self) -> bool:
248          self._completeIfNotSet(self._mergeable)
249          return self._mergeable.value
250  
251      @property
252      def mergeable_state(self) -> str:
253          self._completeIfNotSet(self._mergeable_state)
254          return self._mergeable_state.value
255  
256      @property
257      def merged(self) -> bool:
258          self._completeIfNotSet(self._merged)
259          return self._merged.value
260  
261      @property
262      def merged_at(self) -> datetime | None:
263          self._completeIfNotSet(self._merged_at)
264          return self._merged_at.value
265  
266      @property
267      def merged_by(self) -> github.NamedUser.NamedUser:
268          self._completeIfNotSet(self._merged_by)
269          return self._merged_by.value
270  
271      @property
272      def milestone(self) -> github.Milestone.Milestone:
273          self._completeIfNotSet(self._milestone)
274          return self._milestone.value
275  
276      @property
277      def number(self) -> int:
278          self._completeIfNotSet(self._number)
279          return self._number.value
280  
281      @property
282      def patch_url(self) -> str:
283          self._completeIfNotSet(self._patch_url)
284          return self._patch_url.value
285  
286      @property
287      def rebaseable(self) -> bool:
288          self._completeIfNotSet(self._rebaseable)
289          return self._rebaseable.value
290  
291      @property
292      def review_comment_url(self) -> str:
293          self._completeIfNotSet(self._review_comment_url)
294          return self._review_comment_url.value
295  
296      @property
297      def review_comments(self) -> int:
298          self._completeIfNotSet(self._review_comments)
299          return self._review_comments.value
300  
301      @property
302      def review_comments_url(self) -> str:
303          self._completeIfNotSet(self._review_comments_url)
304          return self._review_comments_url.value
305  
306      @property
307      def state(self) -> str:
308          self._completeIfNotSet(self._state)
309          return self._state.value
310  
311      @property
312      def title(self) -> str:
313          self._completeIfNotSet(self._title)
314          return self._title.value
315  
316      @property
317      def updated_at(self) -> datetime | None:
318          self._completeIfNotSet(self._updated_at)
319          return self._updated_at.value
320  
321      @property
322      def requested_reviewers(self) -> list[github.NamedUser.NamedUser]:
323          self._completeIfNotSet(self._requested_reviewers)
324          return self._requested_reviewers.value
325  
326      @property
327      def requested_teams(self) -> list[github.Team.Team]:
328          self._completeIfNotSet(self._requested_teams)
329          return self._requested_teams.value
330  
331      @property
332      def url(self) -> str:
333          self._completeIfNotSet(self._url)
334          return self._url.value
335  
336      @property
337      def user(self) -> NamedUser:
338          self._completeIfNotSet(self._user)
339          return self._user.value
340  
341      @property
342      def maintainer_can_modify(self) -> bool:
343          self._completeIfNotSet(self._maintainer_can_modify)
344          return self._maintainer_can_modify.value
345  
346      def as_issue(self) -> Issue:
347          """
348          :calls: `GET /repos/{owner}/{repo}/issues/{number} <https://docs.github.com/en/rest/reference/issues>`_
349          """
350          headers, data = self._requester.requestJsonAndCheck("GET", self.issue_url)
351          return github.Issue.Issue(self._requester, headers, data, completed=True)
352  
353      def create_comment(
354          self, body: str, commit: github.Commit.Commit, path: str, position: int
355      ) -> github.PullRequestComment.PullRequestComment:
356          """
357          :calls: `POST /repos/{owner}/{repo}/pulls/{number}/comments <https://docs.github.com/en/rest/reference/pulls#review-comments>`_
358          """
359          return self.create_review_comment(body, commit, path, position)
360  
361      def create_review_comment(
362          self,
363          body: str,
364          commit: github.Commit.Commit,
365          path: str,
366          # line replaces deprecated position argument, so we put it between path and side
367          line: Opt[int] = NotSet,
368          side: Opt[str] = NotSet,
369          start_line: Opt[int] = NotSet,
370          start_side: Opt[int] = NotSet,
371          in_reply_to: Opt[int] = NotSet,
372          subject_type: Opt[str] = NotSet,
373          as_suggestion: bool = False,
374      ) -> github.PullRequestComment.PullRequestComment:
375          """
376          :calls: `POST /repos/{owner}/{repo}/pulls/{number}/comments <https://docs.github.com/en/rest/reference/pulls#review-comments>`_
377          """
378          assert isinstance(body, str), body
379          assert isinstance(commit, github.Commit.Commit), commit
380          assert isinstance(path, str), path
381          assert is_optional(line, int), line
382          assert is_undefined(side) or side in ["LEFT", "RIGHT"], side
383          assert is_optional(start_line, int), start_line
384          assert is_undefined(start_side) or start_side in [
385              "LEFT",
386              "RIGHT",
387              "side",
388          ], start_side
389          assert is_optional(in_reply_to, int), in_reply_to
390          assert is_undefined(subject_type) or subject_type in [
391              "line",
392              "file",
393          ], subject_type
394          assert isinstance(as_suggestion, bool), as_suggestion
395  
396          if as_suggestion:
397              body = f"```suggestion\n{body}\n```"
398          post_parameters = NotSet.remove_unset_items(
399              {
400                  "body": body,
401                  "commit_id": commit._identity,
402                  "path": path,
403                  "line": line,
404                  "side": side,
405                  "start_line": start_line,
406                  "start_side": start_side,
407                  "in_reply_to": in_reply_to,
408                  "subject_type": subject_type,
409              }
410          )
411  
412          headers, data = self._requester.requestJsonAndCheck("POST", f"{self.url}/comments", input=post_parameters)
413          return github.PullRequestComment.PullRequestComment(self._requester, headers, data, completed=True)
414  
415      def create_review_comment_reply(self, comment_id: int, body: str) -> github.PullRequestComment.PullRequestComment:
416          """
417          :calls: `POST /repos/{owner}/{repo}/pulls/{pull_number}/comments/{comment_id}/replies <https://docs.github.com/en/rest/reference/pulls#review-comments>`_
418          """
419          assert isinstance(comment_id, int), comment_id
420          assert isinstance(body, str), body
421          post_parameters = {"body": body}
422          headers, data = self._requester.requestJsonAndCheck(
423              "POST",
424              f"{self.url}/comments/{comment_id}/replies",
425              input=post_parameters,
426          )
427          return github.PullRequestComment.PullRequestComment(self._requester, headers, data, completed=True)
428  
429      def create_issue_comment(self, body: str) -> github.IssueComment.IssueComment:
430          """
431          :calls: `POST /repos/{owner}/{repo}/issues/{number}/comments <https://docs.github.com/en/rest/reference/issues#comments>`_
432          """
433          assert isinstance(body, str), body
434          post_parameters = {
435              "body": body,
436          }
437          headers, data = self._requester.requestJsonAndCheck("POST", f"{self.issue_url}/comments", input=post_parameters)
438          return github.IssueComment.IssueComment(self._requester, headers, data, completed=True)
439  
440      def create_review(
441          self,
442          commit: Opt[github.Commit.Commit] = NotSet,
443          body: Opt[str] = NotSet,
444          event: Opt[str] = NotSet,
445          comments: Opt[list[ReviewComment]] = NotSet,
446      ) -> github.PullRequestReview.PullRequestReview:
447          """
448          :calls: `POST /repos/{owner}/{repo}/pulls/{number}/reviews <https://docs.github.com/en/free-pro-team@latest/rest/pulls/reviews?apiVersion=2022-11-28#create-a-review-for-a-pull-request>`_
449          """
450          assert is_optional(commit, github.Commit.Commit), commit
451          assert is_optional(body, str), body
452          assert is_optional(event, str), event
453          assert is_optional_list(comments, dict), comments
454          post_parameters: dict[str, Any] = NotSet.remove_unset_items({"body": body})
455          post_parameters["event"] = "COMMENT" if is_undefined(event) else event
456          if is_defined(commit):
457              post_parameters["commit_id"] = commit.sha
458          if is_defined(comments):
459              post_parameters["comments"] = comments
460          else:
461              post_parameters["comments"] = []
462          headers, data = self._requester.requestJsonAndCheck("POST", f"{self.url}/reviews", input=post_parameters)
463          return github.PullRequestReview.PullRequestReview(self._requester, headers, data, completed=True)
464  
465      def create_review_request(
466          self,
467          reviewers: Opt[list[str] | str] = NotSet,
468          team_reviewers: Opt[list[str] | str] = NotSet,
469      ) -> None:
470          """
471          :calls: `POST /repos/{owner}/{repo}/pulls/{number}/requested_reviewers <https://docs.github.com/en/rest/reference/pulls#review-requests>`_
472          """
473          assert is_optional(reviewers, str) or is_optional_list(reviewers, str), reviewers
474          assert is_optional(team_reviewers, str) or is_optional_list(team_reviewers, str), team_reviewers
475  
476          post_parameters = NotSet.remove_unset_items({"reviewers": reviewers, "team_reviewers": team_reviewers})
477  
478          headers, data = self._requester.requestJsonAndCheck(
479              "POST", f"{self.url}/requested_reviewers", input=post_parameters
480          )
481  
482      def delete_review_request(
483          self,
484          reviewers: Opt[list[str] | str] = NotSet,
485          team_reviewers: Opt[list[str] | str] = NotSet,
486      ) -> None:
487          """
488          :calls: `DELETE /repos/{owner}/{repo}/pulls/{number}/requested_reviewers <https://docs.github.com/en/rest/reference/pulls#review-requests>`_
489          """
490          assert is_optional(reviewers, str) or is_optional_list(reviewers, str), reviewers
491          assert is_optional(team_reviewers, str) or is_optional_list(team_reviewers, str), team_reviewers
492  
493          post_parameters = NotSet.remove_unset_items({"reviewers": reviewers, "team_reviewers": team_reviewers})
494  
495          headers, data = self._requester.requestJsonAndCheck(
496              "DELETE", f"{self.url}/requested_reviewers", input=post_parameters
497          )
498  
499      def edit(
500          self,
501          title: Opt[str] = NotSet,
502          body: Opt[str] = NotSet,
503          state: Opt[str] = NotSet,
504          base: Opt[str] = NotSet,
505          maintainer_can_modify: Opt[bool] = NotSet,
506      ) -> None:
507          """
508          :calls: `PATCH /repos/{owner}/{repo}/pulls/{number} <https://docs.github.com/en/rest/reference/pulls>`_
509          """
510          assert is_optional(title, str), title
511          assert is_optional(body, str), body
512          assert is_optional(state, str), state
513          assert is_optional(base, str), base
514          assert is_optional(maintainer_can_modify, bool), maintainer_can_modify
515          post_parameters = NotSet.remove_unset_items(
516              {"title": title, "body": body, "state": state, "base": base, "maintainer_can_modify": maintainer_can_modify}
517          )
518  
519          headers, data = self._requester.requestJsonAndCheck("PATCH", self.url, input=post_parameters)
520          self._useAttributes(data)
521  
522      def get_comment(self, id: int) -> github.PullRequestComment.PullRequestComment:
523          """
524          :calls: `GET /repos/{owner}/{repo}/pulls/comments/{number} <https://docs.github.com/en/rest/reference/pulls#review-comments>`_
525          """
526          return self.get_review_comment(id)
527  
528      def get_review_comment(self, id: int) -> github.PullRequestComment.PullRequestComment:
529          """
530          :calls: `GET /repos/{owner}/{repo}/pulls/comments/{number} <https://docs.github.com/en/rest/reference/pulls#review-comments>`_
531          """
532          assert isinstance(id, int), id
533          headers, data = self._requester.requestJsonAndCheck("GET", f"{self._parentUrl(self.url)}/comments/{id}")
534          return github.PullRequestComment.PullRequestComment(self._requester, headers, data, completed=True)
535  
536      def get_comments(
537          self,
538          sort: Opt[str] = NotSet,
539          direction: Opt[str] = NotSet,
540          since: Opt[datetime] = NotSet,
541      ) -> PaginatedList[github.PullRequestComment.PullRequestComment]:
542          """
543          Warning: this only returns review comments. For normal conversation comments, use get_issue_comments.
544  
545          :calls: `GET /repos/{owner}/{repo}/pulls/{number}/comments <https://docs.github.com/en/rest/reference/pulls#review-comments>`_
546          :param sort: string 'created' or 'updated'
547          :param direction: string 'asc' or 'desc'
548          :param since: datetime
549          """
550          return self.get_review_comments(sort=sort, direction=direction, since=since)
551  
552      # v3: remove *, added here to force named parameters because order has changed
553      def get_review_comments(
554          self,
555          *,
556          sort: Opt[str] = NotSet,
557          direction: Opt[str] = NotSet,
558          since: Opt[datetime] = NotSet,
559      ) -> PaginatedList[github.PullRequestComment.PullRequestComment]:
560          """
561          :calls: `GET /repos/{owner}/{repo}/pulls/{number}/comments <https://docs.github.com/en/rest/reference/pulls#review-comments>`_
562          :param sort: string 'created' or 'updated'
563          :param direction: string 'asc' or 'desc'
564          :param since: datetime
565          """
566          assert is_optional(sort, str), sort
567          assert is_optional(direction, str), direction
568          assert is_optional(since, datetime), since
569  
570          url_parameters = NotSet.remove_unset_items({"sort": sort, "direction": direction})
571          if is_defined(since):
572              url_parameters["since"] = since.strftime("%Y-%m-%dT%H:%M:%SZ")
573  
574          return PaginatedList(
575              github.PullRequestComment.PullRequestComment,
576              self._requester,
577              f"{self.url}/comments",
578              url_parameters,
579          )
580  
581      def get_single_review_comments(self, id: int) -> PaginatedList[github.PullRequestComment.PullRequestComment]:
582          """
583          :calls: `GET /repos/{owner}/{repo}/pulls/{number}/review/{id}/comments <https://docs.github.com/en/rest/reference/pulls#reviews>`_
584          """
585          assert isinstance(id, int), id
586          return PaginatedList(
587              github.PullRequestComment.PullRequestComment,
588              self._requester,
589              f"{self.url}/reviews/{id}/comments",
590              None,
591          )
592  
593      def get_commits(self) -> PaginatedList[github.Commit.Commit]:
594          """
595          :calls: `GET /repos/{owner}/{repo}/pulls/{number}/commits <https://docs.github.com/en/rest/reference/pulls>`_
596          """
597          return PaginatedList(github.Commit.Commit, self._requester, f"{self.url}/commits", None)
598  
599      def get_files(self) -> PaginatedList[github.File.File]:
600          """
601          :calls: `GET /repos/{owner}/{repo}/pulls/{number}/files <https://docs.github.com/en/rest/reference/pulls>`_
602          """
603          return PaginatedList(github.File.File, self._requester, f"{self.url}/files", None)
604  
605      def get_issue_comment(self, id: int) -> github.IssueComment.IssueComment:
606          """
607          :calls: `GET /repos/{owner}/{repo}/issues/comments/{id} <https://docs.github.com/en/rest/reference/issues#comments>`_
608          """
609          assert isinstance(id, int), id
610          headers, data = self._requester.requestJsonAndCheck("GET", f"{self._parentUrl(self.issue_url)}/comments/{id}")
611          return github.IssueComment.IssueComment(self._requester, headers, data, completed=True)
612  
613      def get_issue_comments(self) -> PaginatedList[github.IssueComment.IssueComment]:
614          """
615          :calls: `GET /repos/{owner}/{repo}/issues/{number}/comments <https://docs.github.com/en/rest/reference/issues#comments>`_
616          """
617          return PaginatedList(
618              github.IssueComment.IssueComment,
619              self._requester,
620              f"{self.issue_url}/comments",
621              None,
622          )
623  
624      def get_issue_events(self) -> PaginatedList[github.IssueEvent.IssueEvent]:
625          """
626          :calls: `GET /repos/{owner}/{repo}/issues/{issue_number}/events <https://docs.github.com/en/rest/reference/issues#events>`_
627          :rtype: :class:`github.PaginatedList.PaginatedList` of :class:`github.IssueEvent.IssueEvent`
628          """
629          return PaginatedList(
630              github.IssueEvent.IssueEvent,
631              self._requester,
632              f"{self.issue_url}/events",
633              None,
634              headers={"Accept": Consts.mediaTypeLockReasonPreview},
635          )
636  
637      def get_review(self, id: int) -> github.PullRequestReview.PullRequestReview:
638          """
639          :calls: `GET /repos/{owner}/{repo}/pulls/{number}/reviews/{id} <https://docs.github.com/en/rest/reference/pulls#reviews>`_
640          :param id: integer
641          :rtype: :class:`github.PullRequestReview.PullRequestReview`
642          """
643          assert isinstance(id, int), id
644          headers, data = self._requester.requestJsonAndCheck(
645              "GET",
646              f"{self.url}/reviews/{id}",
647          )
648          return github.PullRequestReview.PullRequestReview(self._requester, headers, data, completed=True)
649  
650      def get_reviews(self) -> PaginatedList[github.PullRequestReview.PullRequestReview]:
651          """
652          :calls: `GET /repos/{owner}/{repo}/pulls/{number}/reviews <https://docs.github.com/en/rest/reference/pulls#reviews>`_
653          :rtype: :class:`github.PaginatedList.PaginatedList` of :class:`github.PullRequestReview.PullRequestReview`
654          """
655          return PaginatedList(
656              github.PullRequestReview.PullRequestReview,
657              self._requester,
658              f"{self.url}/reviews",
659              None,
660          )
661  
662      def get_review_requests(self) -> tuple[PaginatedList[NamedUser], PaginatedList[github.Team.Team]]:
663          """
664          :calls: `GET /repos/{owner}/{repo}/pulls/{number}/requested_reviewers <https://docs.github.com/en/rest/reference/pulls#review-requests>`_
665          :rtype: tuple of :class:`github.PaginatedList.PaginatedList` of :class:`github.NamedUser.NamedUser` and of :class:`github.PaginatedList.PaginatedList` of :class:`github.Team.Team`
666          """
667          return (
668              PaginatedList(
669                  github.NamedUser.NamedUser,
670                  self._requester,
671                  f"{self.url}/requested_reviewers",
672                  None,
673                  list_item="users",
674              ),
675              PaginatedList(
676                  github.Team.Team,
677                  self._requester,
678                  f"{self.url}/requested_reviewers",
679                  None,
680                  list_item="teams",
681              ),
682          )
683  
684      def get_labels(self) -> PaginatedList[github.Label.Label]:
685          """
686          :calls: `GET /repos/{owner}/{repo}/issues/{number}/labels <https://docs.github.com/en/rest/reference/issues#labels>`_
687          """
688          return PaginatedList(github.Label.Label, self._requester, f"{self.issue_url}/labels", None)
689  
690      def add_to_labels(self, *labels: github.Label.Label | str) -> None:
691          """
692          :calls: `POST /repos/{owner}/{repo}/issues/{number}/labels <https://docs.github.com/en/rest/reference/issues#labels>`_
693          """
694          assert all(isinstance(element, (github.Label.Label, str)) for element in labels), labels
695          post_parameters = [label.name if isinstance(label, github.Label.Label) else label for label in labels]
696          headers, data = self._requester.requestJsonAndCheck("POST", f"{self.issue_url}/labels", input=post_parameters)
697  
698      def delete_labels(self) -> None:
699          """
700          :calls: `DELETE /repos/{owner}/{repo}/issues/{number}/labels <https://docs.github.com/en/rest/reference/issues#labels>`_
701          """
702          headers, data = self._requester.requestJsonAndCheck("DELETE", f"{self.issue_url}/labels")
703  
704      def remove_from_labels(self, label: github.Label.Label | str) -> None:
705          """
706          :calls: `DELETE /repos/{owner}/{repo}/issues/{number}/labels/{name} <https://docs.github.com/en/rest/reference/issues#labels>`_
707          """
708          assert isinstance(label, (github.Label.Label, str)), label
709          if isinstance(label, github.Label.Label):
710              label = label._identity
711          else:
712              label = urllib.parse.quote(label)
713          headers, data = self._requester.requestJsonAndCheck("DELETE", f"{self.issue_url}/labels/{label}")
714  
715      def set_labels(self, *labels: github.Label.Label | str) -> None:
716          """
717          :calls: `PUT /repos/{owner}/{repo}/issues/{number}/labels <https://docs.github.com/en/rest/reference/issues#labels>`_
718          """
719          assert all(isinstance(element, (github.Label.Label, str)) for element in labels), labels
720          post_parameters = [label.name if isinstance(label, github.Label.Label) else label for label in labels]
721          headers, data = self._requester.requestJsonAndCheck("PUT", f"{self.issue_url}/labels", input=post_parameters)
722  
723      def is_merged(self) -> bool:
724          """
725          :calls: `GET /repos/{owner}/{repo}/pulls/{number}/merge <https://docs.github.com/en/rest/reference/pulls>`_
726          """
727          status, headers, data = self._requester.requestJson("GET", f"{self.url}/merge")
728          return status == 204
729  
730      def merge(
731          self,
732          commit_message: Opt[str] = NotSet,
733          commit_title: Opt[str] = NotSet,
734          merge_method: Opt[str] = NotSet,
735          sha: Opt[str] = NotSet,
736      ) -> github.PullRequestMergeStatus.PullRequestMergeStatus:
737          """
738          :calls: `PUT /repos/{owner}/{repo}/pulls/{number}/merge <https://docs.github.com/en/rest/reference/pulls>`_
739          """
740          assert is_optional(commit_message, str), commit_message
741          assert is_optional(commit_title, str), commit_title
742          assert is_optional(merge_method, str), merge_method
743          assert is_optional(sha, str), sha
744          post_parameters = NotSet.remove_unset_items(
745              {"commit_message": commit_message, "commit_title": commit_title, "merge_method": merge_method, "sha": sha}
746          )
747          headers, data = self._requester.requestJsonAndCheck("PUT", f"{self.url}/merge", input=post_parameters)
748          return github.PullRequestMergeStatus.PullRequestMergeStatus(self._requester, headers, data, completed=True)
749  
750      def add_to_assignees(self, *assignees: github.NamedUser.NamedUser) -> None:
751          """
752          :calls: `POST /repos/{owner}/{repo}/issues/{number}/assignees <https://docs.github.com/en/rest/reference/issues#assignees>`_
753          """
754          assert all(isinstance(element, (github.NamedUser.NamedUser, str)) for element in assignees), assignees
755          post_parameters = {
756              "assignees": [
757                  assignee.login if isinstance(assignee, github.NamedUser.NamedUser) else assignee
758                  for assignee in assignees
759              ]
760          }
761          headers, data = self._requester.requestJsonAndCheck(
762              "POST", f"{self.issue_url}/assignees", input=post_parameters
763          )
764          # Only use the assignees attribute, since we call this PR as an issue
765          self._useAttributes({"assignees": data["assignees"]})
766  
767      def remove_from_assignees(self, *assignees: github.NamedUser.NamedUser | str) -> None:
768          """
769          :calls: `DELETE /repos/{owner}/{repo}/issues/{number}/assignees <https://docs.github.com/en/rest/reference/issues#assignees>`_
770          """
771          assert all(isinstance(element, (github.NamedUser.NamedUser, str)) for element in assignees), assignees
772          post_parameters = {
773              "assignees": [
774                  assignee.login if isinstance(assignee, github.NamedUser.NamedUser) else assignee
775                  for assignee in assignees
776              ]
777          }
778          headers, data = self._requester.requestJsonAndCheck(
779              "DELETE", f"{self.issue_url}/assignees", input=post_parameters
780          )
781          # Only use the assignees attribute, since we call this PR as an issue
782          self._useAttributes({"assignees": data["assignees"]})
783  
784      def update_branch(self, expected_head_sha: Opt[str] = NotSet) -> bool:
785          """
786          :calls `PUT /repos/{owner}/{repo}/pulls/{pull_number}/update-branch <https://docs.github.com/en/rest/reference/pulls>`_
787          """
788          assert is_optional(expected_head_sha, str), expected_head_sha
789          post_parameters = NotSet.remove_unset_items({"expected_head_sha": expected_head_sha})
790          status, headers, data = self._requester.requestJson(
791              "PUT",
792              f"{self.url}/update-branch",
793              input=post_parameters,
794              headers={"Accept": Consts.updateBranchPreview},
795          )
796          return status == 202
797  
798      def _useAttributes(self, attributes: dict[str, Any]) -> None:
799          if "additions" in attributes:  # pragma no branch
800              self._additions = self._makeIntAttribute(attributes["additions"])
801          if "assignee" in attributes:  # pragma no branch
802              self._assignee = self._makeClassAttribute(github.NamedUser.NamedUser, attributes["assignee"])
803          if "assignees" in attributes:  # pragma no branch
804              self._assignees = self._makeListOfClassesAttribute(github.NamedUser.NamedUser, attributes["assignees"])
805          elif "assignee" in attributes:
806              if attributes["assignee"] is not None:
807                  self._assignees = self._makeListOfClassesAttribute(github.NamedUser.NamedUser, [attributes["assignee"]])
808              else:
809                  self._assignees = self._makeListOfClassesAttribute(github.NamedUser.NamedUser, [])
810          if "base" in attributes:  # pragma no branch
811              self._base = self._makeClassAttribute(github.PullRequestPart.PullRequestPart, attributes["base"])
812          if "body" in attributes:  # pragma no branch
813              self._body = self._makeStringAttribute(attributes["body"])
814          if "changed_files" in attributes:  # pragma no branch
815              self._changed_files = self._makeIntAttribute(attributes["changed_files"])
816          if "closed_at" in attributes:  # pragma no branch
817              self._closed_at = self._makeDatetimeAttribute(attributes["closed_at"])
818          if "comments" in attributes:  # pragma no branch
819              self._comments = self._makeIntAttribute(attributes["comments"])
820          if "comments_url" in attributes:  # pragma no branch
821              self._comments_url = self._makeStringAttribute(attributes["comments_url"])
822          if "commits" in attributes:  # pragma no branch
823              self._commits = self._makeIntAttribute(attributes["commits"])
824          if "commits_url" in attributes:  # pragma no branch
825              self._commits_url = self._makeStringAttribute(attributes["commits_url"])
826          if "created_at" in attributes:  # pragma no branch
827              self._created_at = self._makeDatetimeAttribute(attributes["created_at"])
828          if "deletions" in attributes:  # pragma no branch
829              self._deletions = self._makeIntAttribute(attributes["deletions"])
830          if "diff_url" in attributes:  # pragma no branch
831              self._diff_url = self._makeStringAttribute(attributes["diff_url"])
832          if "draft" in attributes:  # pragma no branch
833              self._draft = self._makeBoolAttribute(attributes["draft"])
834          if "head" in attributes:  # pragma no branch
835              self._head = self._makeClassAttribute(github.PullRequestPart.PullRequestPart, attributes["head"])
836          if "html_url" in attributes:  # pragma no branch
837              self._html_url = self._makeStringAttribute(attributes["html_url"])
838          if "id" in attributes:  # pragma no branch
839              self._id = self._makeIntAttribute(attributes["id"])
840          if "issue_url" in attributes:  # pragma no branch
841              self._issue_url = self._makeStringAttribute(attributes["issue_url"])
842          if "labels" in attributes:  # pragma no branch
843              self._labels = self._makeListOfClassesAttribute(github.Label.Label, attributes["labels"])
844          if "maintainer_can_modify" in attributes:  # pragma no branch
845              self._maintainer_can_modify = self._makeBoolAttribute(attributes["maintainer_can_modify"])
846          if "merge_commit_sha" in attributes:  # pragma no branch
847              self._merge_commit_sha = self._makeStringAttribute(attributes["merge_commit_sha"])
848          if "mergeable" in attributes:  # pragma no branch
849              self._mergeable = self._makeBoolAttribute(attributes["mergeable"])
850          if "mergeable_state" in attributes:  # pragma no branch
851              self._mergeable_state = self._makeStringAttribute(attributes["mergeable_state"])
852          if "merged" in attributes:  # pragma no branch
853              self._merged = self._makeBoolAttribute(attributes["merged"])
854          if "merged_at" in attributes:  # pragma no branch
855              self._merged_at = self._makeDatetimeAttribute(attributes["merged_at"])
856          if "merged_by" in attributes:  # pragma no branch
857              self._merged_by = self._makeClassAttribute(github.NamedUser.NamedUser, attributes["merged_by"])
858          if "milestone" in attributes:  # pragma no branch
859              self._milestone = self._makeClassAttribute(github.Milestone.Milestone, attributes["milestone"])
860          if "number" in attributes:  # pragma no branch
861              self._number = self._makeIntAttribute(attributes["number"])
862          if "patch_url" in attributes:  # pragma no branch
863              self._patch_url = self._makeStringAttribute(attributes["patch_url"])
864          if "rebaseable" in attributes:  # pragma no branch
865              self._rebaseable = self._makeBoolAttribute(attributes["rebaseable"])
866          if "review_comment_url" in attributes:  # pragma no branch
867              self._review_comment_url = self._makeStringAttribute(attributes["review_comment_url"])
868          if "review_comments" in attributes:  # pragma no branch
869              self._review_comments = self._makeIntAttribute(attributes["review_comments"])
870          if "review_comments_url" in attributes:  # pragma no branch
871              self._review_comments_url = self._makeStringAttribute(attributes["review_comments_url"])
872          if "state" in attributes:  # pragma no branch
873              self._state = self._makeStringAttribute(attributes["state"])
874          if "title" in attributes:  # pragma no branch
875              self._title = self._makeStringAttribute(attributes["title"])
876          if "updated_at" in attributes:  # pragma no branch
877              self._updated_at = self._makeDatetimeAttribute(attributes["updated_at"])
878          if "url" in attributes:  # pragma no branch
879              self._url = self._makeStringAttribute(attributes["url"])
880          if "user" in attributes:  # pragma no branch
881              self._user = self._makeClassAttribute(github.NamedUser.NamedUser, attributes["user"])
882          if "requested_reviewers" in attributes:
883              self._requested_reviewers = self._makeListOfClassesAttribute(
884                  github.NamedUser.NamedUser, attributes["requested_reviewers"]
885              )
886          if "requested_teams" in attributes:
887              self._requested_teams = self._makeListOfClassesAttribute(github.Team.Team, attributes["requested_teams"])