Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 77 additions & 6 deletions discussions.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,82 @@

from github import Github

COMMENTS_PAGE_SIZE = 100

# Fetches further pages of a single discussion's comments once the first page
# returned by the search query is exhausted.
COMMENTS_QUERY = """
Comment on lines +12 to +16
query($id: ID!, $cursor: String, $pageSize: Int!) {
node(id: $id) {
... on Discussion {
comments(first: $pageSize, after: $cursor) {
nodes {
createdAt
author {
login
__typename
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
}
}
"""


def _fetch_remaining_comments(
github_connection: Github, discussion: dict, max_comments: int
):
"""Page a single discussion's comments until max_comments is satisfied.

def get_discussions(github_connection: Github, search_query: str):
The search query returns only the first page of each discussion's comments,
so without this the mentor counting branch silently stops at that page.

Args:
github_connection (Github): An authenticated PyGithub connection.
discussion (dict): A discussion node, updated in place.
max_comments (int): Maximum number of comments to collect.
"""
comments = discussion.get("comments")
if not comments:
return

nodes = comments.get("nodes", [])
page_info = comments.get("pageInfo") or {}

while len(nodes) < max_comments and page_info.get("hasNextPage"):
variables = {
"id": discussion["id"],
"cursor": page_info.get("endCursor"),
"pageSize": min(COMMENTS_PAGE_SIZE, max_comments - len(nodes)),
}
_, response_json = github_connection.requester.graphql_query(
COMMENTS_QUERY, variables
)
page = response_json["data"]["node"]["comments"]
nodes.extend(page.get("nodes", []))
page_info = page.get("pageInfo") or {}

comments["pageInfo"] = page_info


def get_discussions(
github_connection: Github, search_query: str, max_comments: int = 20
):
"""Get a list of discussions in a GitHub repository that match the search query.

Args:
github_connection (Github): An authenticated PyGithub connection.
GitHub Enterprise routing is handled by the connection's base URL.
search_query (str): The search query to filter discussions by.
max_comments (int): Maximum number of comments to collect per discussion.
Values above the GraphQL page size trigger extra requests, so that
discussions match the issue and pull request branches, which keep
paginating until the limit is satisfied.

Returns:
list: A list of discussions in the repository that match the search query.
Expand All @@ -28,17 +96,14 @@ def get_discussions(github_connection: Github, search_query: str):
edges {
node {
... on Discussion {
id
title
url
createdAt
author {
login
__typename
}
# Only the first 100 comments are fetched (no
# pagination). MAX_COMMENTS_EVAL defaults to 20, so
# this ceiling is not hit in practice; setting it above
# 100 would silently cap discussion mentor counts.
comments(first: 100) {
nodes {
createdAt
Expand All @@ -47,6 +112,10 @@ def get_discussions(github_connection: Github, search_query: str):
__typename
}
}
pageInfo {
hasNextPage
endCursor
}
}
answerChosenAt
closedAt
Expand Down Expand Up @@ -81,7 +150,9 @@ def get_discussions(github_connection: Github, search_query: str):

# Extract the discussions from the current page
for edge in data["search"]["edges"]:
discussions.append(edge["node"])
discussion = edge["node"]
_fetch_remaining_comments(github_connection, discussion, max_comments)
discussions.append(discussion)

# Check if there are more pages
page_info = data["search"]["pageInfo"]
Expand Down
2 changes: 1 addition & 1 deletion issue_metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,7 @@ def main(): # pragma: no cover
raise ValueError(
"The search query for discussions cannot include labels to measure"
)
issues = get_discussions(github_connection, search_query)
issues = get_discussions(github_connection, search_query, max_comments_eval)
if len(issues) <= 0:
print("No discussions found")
write_to_markdown(
Expand Down
123 changes: 123 additions & 0 deletions test_discussions.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,3 +133,126 @@ def test_get_discussions_propagates_github_exception(self):

with self.assertRaises(GithubException):
get_discussions(github_connection, "repo:user/repo type:discussions query")

def test_get_discussions_paginates_comments(self):
"""Comments beyond the first page are fetched when max_comments allows."""

def comment(index):
return {
"createdAt": "2021-01-01T00:00:00Z",
"author": {"login": f"user{index}", "__typename": "User"},
}

first_page = [comment(index) for index in range(100)]
second_page = [comment(index) for index in range(100, 150)]

discussion = {
"id": "D_kwDO",
"title": "Discussion 1",
"url": "https://github.com/user/repo/discussions/1",
"createdAt": "2021-01-01T00:00:00Z",
"author": {"login": "author", "__typename": "User"},
"comments": {
"nodes": first_page,
"pageInfo": {"hasNextPage": True, "endCursor": "comment100"},
},
"answerChosenAt": None,
"closedAt": None,
}

github_connection = MagicMock()
github_connection.requester.graphql_query.side_effect = [
({}, self._create_mock_response([discussion], has_next_page=False)),
(
{},
{
"data": {
"node": {
"comments": {
"nodes": second_page,
"pageInfo": {
"hasNextPage": False,
"endCursor": "comment150",
},
}
}
}
},
),
]

discussions = get_discussions(
github_connection, "repo:user/repo type:discussions query", max_comments=150
)

# All 150 comments are collected, not just the first page of 100
self.assertEqual(len(discussions[0]["comments"]["nodes"]), 150)

# A follow-up query was made for the remaining comments
self.assertEqual(github_connection.requester.graphql_query.call_count, 2)
comment_call_variables = (
github_connection.requester.graphql_query.call_args_list[1].args[1]
)
self.assertEqual(comment_call_variables["id"], "D_kwDO")
self.assertEqual(comment_call_variables["cursor"], "comment100")
self.assertEqual(comment_call_variables["pageSize"], 50)

def test_get_discussions_stops_paginating_comments_at_max_comments(self):
"""No extra request is made once max_comments comments are collected."""
discussion = {
"id": "D_kwDO",
"title": "Discussion 1",
"url": "https://github.com/user/repo/discussions/1",
"createdAt": "2021-01-01T00:00:00Z",
"author": {"login": "author", "__typename": "User"},
"comments": {
"nodes": [
{
"createdAt": "2021-01-01T00:00:00Z",
"author": {"login": "user1", "__typename": "User"},
}
],
"pageInfo": {"hasNextPage": True, "endCursor": "comment1"},
},
"answerChosenAt": None,
"closedAt": None,
}

github_connection = MagicMock()
github_connection.requester.graphql_query.return_value = (
{},
self._create_mock_response([discussion], has_next_page=False),
)

discussions = get_discussions(
github_connection, "repo:user/repo type:discussions query", max_comments=1
)

self.assertEqual(len(discussions[0]["comments"]["nodes"]), 1)
self.assertEqual(github_connection.requester.graphql_query.call_count, 1)

def test_get_discussions_with_no_comments(self):
"""A discussion without a comments connection triggers no extra request."""
discussion = {
"id": "D_kwDO",
"title": "Discussion 1",
"url": "https://github.com/user/repo/discussions/1",
"createdAt": "2021-01-01T00:00:00Z",
"author": {"login": "author", "__typename": "User"},
"comments": None,
"answerChosenAt": None,
"closedAt": None,
}

github_connection = MagicMock()
github_connection.requester.graphql_query.return_value = (
{},
self._create_mock_response([discussion], has_next_page=False),
)

discussions = get_discussions(
github_connection, "repo:user/repo type:discussions query", max_comments=150
)

self.assertIsNone(discussions[0]["comments"])
self.assertEqual(github_connection.requester.graphql_query.call_count, 1)