Skip to content
Merged
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
10 changes: 5 additions & 5 deletions release_notes_generator/utils/github_rate_limiter.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
import logging
import time
from datetime import datetime
from typing import Optional, Callable, Any
from typing import Optional, Callable, Any, cast
from github import Github

logger = logging.getLogger(__name__)
Expand All @@ -33,7 +33,7 @@ class GithubRateLimiter:
"""

def __init__(self, github_client: Github):
self.github_client = github_client
self.github_client: Github = github_client

def __call__(self, method: Callable) -> Callable:
"""
Expand All @@ -44,9 +44,9 @@ def __call__(self, method: Callable) -> Callable:
"""

def wrapped_method(*args, **kwargs) -> Optional[Any]:
# rate_limit = self.github_client.get_rate_limit().core
remaining_calls = self.github_client.get_rate_limit().core.remaining
reset_time = self.github_client.get_rate_limit().core.reset.timestamp()
rate_limit_overview = self.github_client.get_rate_limit()
remaining_calls = rate_limit_overview.rate.remaining
reset_time = rate_limit_overview.rate.reset.timestamp()

if remaining_calls < 5:
logger.info("Rate limit almost reached. Sleeping until reset time.")
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
pytest==8.4.2
pytest-cov==6.3.0
pytest-mock==3.15.0
PyGithub==1.59.0
pylint==3.3.8
requests==2.32.5
black==25.1.0
Expand All @@ -11,3 +10,4 @@ mypy==1.17.1
mypy-extensions==1.1.0
types-requests==2.32.4.20250809
types-PyYAML==6.0.12.20250822
PyGithub==2.8.1
13 changes: 5 additions & 8 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

import time

from datetime import datetime
from datetime import datetime, timedelta

import pytest

Expand All @@ -26,7 +26,7 @@
from github.Issue import Issue
from github.PullRequest import PullRequest
from github.Rate import Rate
from github.RateLimit import RateLimit
from github.RateLimitOverview import RateLimitOverview
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Invalid import: RateLimitOverview doesn’t exist in PyGithub.

Docs and sources reference github.RateLimit.RateLimit; there’s no RateLimitOverview class to import. This will fail at import time. (pygithub.readthedocs.io, github.com)

-from github.RateLimitOverview import RateLimitOverview
+# No RateLimitOverview in PyGithub; remove this import
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
from github.RateLimitOverview import RateLimitOverview
# No RateLimitOverview in PyGithub; remove this import
🤖 Prompt for AI Agents
In tests/conftest.py around line 29, the import "from github.RateLimitOverview
import RateLimitOverview" is invalid because PyGithub exposes RateLimit as
github.RateLimit.RateLimit; replace the import with the correct module/class
(e.g., "from github.RateLimit import RateLimit") and update any subsequent
references to use RateLimit (or the appropriate object returned by the library)
so tests import the real PyGithub class and do not fail at import time.

from github.Repository import Repository

from release_notes_generator.model.issue_record import IssueRecord
Expand Down Expand Up @@ -112,14 +112,11 @@ def rate_limiter(mocker, request):
@pytest.fixture
def mock_rate_limiter(mocker):
mock_rate = mocker.Mock(spec=Rate)
mock_rate.timestamp = mocker.Mock(return_value=time.time() + 3600)

mock_core = mocker.Mock(spec=RateLimit)
mock_core.reset = mock_rate
mock_rate.reset = datetime.now() + timedelta(hours=1)

mock = mocker.Mock(spec=GithubRateLimiter)
mock.core = mock_core
mock.core.remaining = 10
mock.rate = mock_rate
mock.rate.remaining = 10

Comment on lines +115 to 120
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Mock the right spec and prefer core.

Return a RateLimit-like object from get_rate_limit(), not a GithubRateLimiter mock. Also align on core.*.

-    mock_rate = mocker.Mock(spec=Rate)
-    mock_rate.reset = datetime.now() + timedelta(hours=1)
-
-    mock = mocker.Mock(spec=GithubRateLimiter)
-    mock.rate = mock_rate
-    mock.rate.remaining = 10
+    mock_rate = mocker.Mock(spec=Rate)
+    mock_rate.reset = datetime.now() + timedelta(hours=1)
+
+    # This object mimics github.RateLimit.RateLimit
+    from github.RateLimit import RateLimit  # optional: type hint only
+    mock = mocker.Mock()  # don’t use GithubRateLimiter as spec here
+    mock.core = mock_rate
+    mock.core.remaining = 10

Run to catch stray usages:


🏁 Script executed:

#!/bin/bash
rg -nP '\.get_rate_limit\(\)\.rate\b|\.rate\.(remaining|reset)\b'

Length of output: 1782


Mock core instead of rate across the test suite

Replace all occurrences of mock*.rate.* with mock*.core.* when mocking the GitHub RateLimit:

  • Apply the diff in tests/conftest.py (use mock.core per the snippet).
  • Propagate the change in:
    • tests/test_release_notes_generator.py (lines 36–37, 72, 118, 170)
    • tests/test_miner.py (lines 71, 98)
    • tests/utils/test_github_rate_limiter.py (lines 24, 53–54)
    • tests/release_notes/test_record_factory.py (lines 180–181, 274–275, 309–310)
  • Update release_notes_generator/utils/github_rate_limiter.py to consume .core instead of .rate.

Ensure get_rate_limit() returns a RateLimit-like object on .core, not a GithubRateLimiter mock.

🤖 Prompt for AI Agents
In tests/conftest.py around lines 115–120, the tests currently mock
GithubRateLimiter attributes on .rate but the code expects a RateLimit-like
object under .core; change mock_rate assignment to set mock.core = mock_rate and
replace uses of mock.rate.* with mock.core.*; propagate this replacement in
tests/test_release_notes_generator.py (lines 36–37, 72, 118, 170),
tests/test_miner.py (lines 71, 98), tests/utils/test_github_rate_limiter.py
(lines 24, 53–54), and tests/release_notes/test_record_factory.py (lines
180–181, 274–275, 309–310) so all tests reference mock*.core.*; finally update
release_notes_generator/utils/github_rate_limiter.py to consume .core from
get_rate_limit() (ensure get_rate_limit() returns a RateLimit-like object
available on .core, not a GithubRateLimiter mock) so production code and tests
align.

return mock

Expand Down
12 changes: 6 additions & 6 deletions tests/release_notes/test_record_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,8 +177,8 @@ def test_generate_with_issues_and_pulls_and_commits(mocker, mock_repo):
issue1, issue2, pr1, pr2, commit1, commit2 = setup_issues_pulls_commits(mocker)

mock_rate_limit = mocker.Mock()
mock_rate_limit.core.remaining = 10
mock_rate_limit.core.reset.timestamp.return_value = time.time() + 3600
mock_rate_limit.rate.remaining = 10
mock_rate_limit.rate.reset.timestamp.return_value = time.time() + 3600
mock_github_client.get_rate_limit.return_value = mock_rate_limit

data = MinedData()
Expand Down Expand Up @@ -271,8 +271,8 @@ def test_generate_with_no_commits(mocker, mock_repo):
data.pull_requests = [pr1] # PR linked to a non-fetched issues (due to since condition)

mock_rate_limit = mocker.Mock()
mock_rate_limit.core.remaining = 10
mock_rate_limit.core.reset.timestamp.return_value = time.time() + 3600
mock_rate_limit.rate.remaining = 10
mock_rate_limit.rate.reset.timestamp.return_value = time.time() + 3600
mock_github_client.get_rate_limit.return_value = mock_rate_limit
mock_repo.get_issue.return_value = issue2

Expand Down Expand Up @@ -306,8 +306,8 @@ def test_generate_with_no_commits_with_wrong_issue_number_in_pull_body_mention(m
data.pull_requests = [pr1] # PR linked to a non-fetched issues (due to since condition)

mock_rate_limit = mocker.Mock()
mock_rate_limit.core.remaining = 10
mock_rate_limit.core.reset.timestamp.return_value = time.time() + 3600
mock_rate_limit.rate.remaining = 10
mock_rate_limit.rate.reset.timestamp.return_value = time.time() + 3600
mock_github_client.get_rate_limit.return_value = mock_rate_limit
mock_repo.get_issue.return_value = issue2

Expand Down
4 changes: 2 additions & 2 deletions tests/test_miner.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ def test_get_latest_release_from_tag_name_not_defined_2_releases_type_error(mock
data = MinedDataMock(mocker, mock_git_releases, mock_repo)

mock_rate_limit = mocker.Mock()
mock_rate_limit.core.remaining = 1000
mock_rate_limit.rate.remaining = 1000
github_mock.get_rate_limit.return_value = mock_rate_limit

release_notes_miner = DataMiner(github_mock, mock_rate_limit)
Expand All @@ -95,7 +95,7 @@ def test_get_latest_release_from_tag_name_not_defined_2_releases_value_error(moc
data = MinedDataMock(mocker, mock_git_releases, mock_repo)

mock_rate_limit = mocker.Mock()
mock_rate_limit.core.remaining = 1000
mock_rate_limit.rate.remaining = 1000
github_mock.get_rate_limit.return_value = mock_rate_limit

release_notes_miner = DataMiner(github_mock, mock_rate_limit)
Expand Down
11 changes: 5 additions & 6 deletions tests/test_release_notes_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ def test_generate_release_notes_repository_not_found(mocker):
github_mock.get_repo.return_value = None

mock_rate_limit = mocker.Mock()
mock_rate_limit.core.remaining = 10
mock_rate_limit.core.reset.timestamp.return_value = time.time() + 3600
mock_rate_limit.rate.remaining = 10
mock_rate_limit.rate.reset.timestamp.return_value = time.time() + 3600
github_mock.get_rate_limit.return_value = mock_rate_limit

custom_chapters = CustomChapters(print_empty_chapters=True)
Expand Down Expand Up @@ -69,7 +69,7 @@ def test_generate_release_notes_latest_release_not_found(
mocker.patch("release_notes_generator.miner.DataMiner.get_latest_release", return_value=None)
mocker.patch("release_notes_generator.record.record_factory.get_issues_for_pr", return_value=[])
mock_rate_limit = mocker.Mock()
mock_rate_limit.core.remaining = 1000
mock_rate_limit.rate.remaining = 1000
github_mock.get_rate_limit.return_value = mock_rate_limit

custom_chapters = CustomChapters(print_empty_chapters=True)
Expand Down Expand Up @@ -115,7 +115,7 @@ def test_generate_release_notes_latest_release_found_by_created_at(
mocker.patch("release_notes_generator.record.record_factory.get_issues_for_pr", return_value=[])

mock_rate_limit = mocker.Mock()
mock_rate_limit.core.remaining = 1000
mock_rate_limit.rate.remaining = 1000
github_mock.get_rate_limit.return_value = mock_rate_limit

mock_get_action_input = mocker.patch("release_notes_generator.utils.gh_action.get_action_input")
Expand Down Expand Up @@ -166,9 +166,8 @@ def test_generate_release_notes_latest_release_found_by_published_at(
mocker.patch("release_notes_generator.miner.DataMiner.get_latest_release", return_value=mock_git_release)
mocker.patch("release_notes_generator.record.record_factory.get_issues_for_pr", return_value=[])


mock_rate_limit = mocker.Mock()
mock_rate_limit.core.remaining = 1000
mock_rate_limit.rate.remaining = 1000
github_mock.get_rate_limit.return_value = mock_rate_limit

custom_chapters = CustomChapters(print_empty_chapters=True)
Expand Down
7 changes: 4 additions & 3 deletions tests/utils/test_github_rate_limiter.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,13 @@
#

import time
from datetime import datetime, timedelta


def test_rate_limiter_extended_sleep_remaining_1(mocker, rate_limiter, mock_rate_limiter):
# Patch time.sleep to avoid actual delay and track call count
mock_sleep = mocker.patch("time.sleep", return_value=None)
mock_rate_limiter.core.remaining = 1
mock_rate_limiter.rate.remaining = 1

# Mock method to be wrapped
method_mock = mocker.Mock()
Expand Down Expand Up @@ -49,8 +50,8 @@ def test_rate_limiter_extended_sleep_remaining_10(mocker, rate_limiter):
def test_rate_limiter_extended_sleep_remaining_1_negative_reset_time(mocker, rate_limiter, mock_rate_limiter):
# Patch time.sleep to avoid actual delay and track call count
mock_sleep = mocker.patch("time.sleep", return_value=None)
mock_rate_limiter.core.remaining = 1
mock_rate_limiter.core.reset.timestamp = mocker.Mock(return_value=time.time() - 1000)
mock_rate_limiter.rate.remaining = 1
mock_rate_limiter.rate.reset = datetime.now() + timedelta(hours=1)

# Mock method to be wrapped
method_mock = mocker.Mock()
Expand Down
Loading