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"])