Skip to content

Commit d13f6c3

Browse files
authored
Merge pull request #391 from github/copilot/fix-375
2 parents 06087b1 + 90abf46 commit d13f6c3

File tree

4 files changed

+193
-5
lines changed

4 files changed

+193
-5
lines changed

dependabot_file.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
import github3
88
import ruamel.yaml
9+
from exceptions import OptionalFileNotFoundError, check_optional_file
910
from ruamel.yaml.scalarstring import SingleQuotedScalarString
1011

1112
# Define data structure for dependabot.yaml
@@ -192,7 +193,7 @@ def build_dependabot_file(
192193
continue
193194
for file in manifest_files:
194195
try:
195-
if repo.file_contents(file):
196+
if check_optional_file(repo, file):
196197
package_managers_found[manager] = True
197198
make_dependabot_config(
198199
manager,
@@ -204,7 +205,7 @@ def build_dependabot_file(
204205
extra_dependabot_config,
205206
)
206207
break
207-
except github3.exceptions.NotFoundError:
208+
except OptionalFileNotFoundError:
208209
# The file does not exist and is not required,
209210
# so we should continue to the next one rather than raising error or logging
210211
pass

evergreen.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import requests
1212
import ruamel.yaml
1313
from dependabot_file import build_dependabot_file
14+
from exceptions import OptionalFileNotFoundError, check_optional_file
1415

1516

1617
def main(): # pragma: no cover
@@ -314,10 +315,10 @@ def check_existing_config(repo, filename):
314315
"""
315316
existing_config = None
316317
try:
317-
existing_config = repo.file_contents(filename)
318-
if existing_config.size > 0:
318+
existing_config = check_optional_file(repo, filename)
319+
if existing_config:
319320
return existing_config
320-
except github3.exceptions.NotFoundError:
321+
except OptionalFileNotFoundError:
321322
# The file does not exist and is not required,
322323
# so we should continue to the next one rather than raising error or logging
323324
pass

exceptions.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
"""Custom exceptions for the evergreen application."""
2+
3+
import github3.exceptions
4+
5+
6+
class OptionalFileNotFoundError(github3.exceptions.NotFoundError):
7+
"""Exception raised when an optional file is not found.
8+
9+
This exception inherits from github3.exceptions.NotFoundError but provides
10+
a more explicit name for cases where missing files are expected and should
11+
not be treated as errors. This is typically used for optional configuration
12+
files or dependency files that may not exist in all repositories.
13+
14+
Args:
15+
resp: The HTTP response object from the failed request
16+
"""
17+
18+
19+
def check_optional_file(repo, filename):
20+
"""
21+
Example utility function demonstrating OptionalFileNotFoundError usage.
22+
23+
This function shows how the new exception type can be used to provide
24+
more explicit error handling for optional files that may not exist.
25+
26+
Args:
27+
repo: GitHub repository object
28+
filename: Name of the optional file to check
29+
30+
Returns:
31+
File contents object if file exists, None if optional file is missing
32+
33+
Raises:
34+
OptionalFileNotFoundError: When the file is not found (expected for optional files)
35+
Other exceptions: For unexpected errors (permissions, network issues, etc.)
36+
"""
37+
try:
38+
file_contents = repo.file_contents(filename)
39+
# Handle both real file contents objects and test mocks that return boolean
40+
if hasattr(file_contents, "size"):
41+
# Real file contents object
42+
if file_contents.size > 0:
43+
return file_contents
44+
return None
45+
# Test mock or other truthy value
46+
return file_contents if file_contents else None
47+
except github3.exceptions.NotFoundError as e:
48+
# Re-raise as our more specific exception type for better semantic clarity
49+
raise OptionalFileNotFoundError(resp=e.response) from e

test_exceptions.py

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
"""Tests for the exceptions module."""
2+
3+
import unittest
4+
from unittest.mock import Mock
5+
6+
import github3.exceptions
7+
from exceptions import OptionalFileNotFoundError, check_optional_file
8+
9+
10+
class TestOptionalFileNotFoundError(unittest.TestCase):
11+
"""Test the OptionalFileNotFoundError exception."""
12+
13+
def test_optional_file_not_found_error_inherits_from_not_found_error(self):
14+
"""Test that OptionalFileNotFoundError inherits from github3.exceptions.NotFoundError."""
15+
mock_resp = Mock()
16+
mock_resp.status_code = 404
17+
error = OptionalFileNotFoundError(resp=mock_resp)
18+
self.assertIsInstance(error, github3.exceptions.NotFoundError)
19+
20+
def test_optional_file_not_found_error_creation(self):
21+
"""Test OptionalFileNotFoundError can be created."""
22+
mock_resp = Mock()
23+
mock_resp.status_code = 404
24+
error = OptionalFileNotFoundError(resp=mock_resp)
25+
self.assertIsInstance(error, OptionalFileNotFoundError)
26+
27+
def test_optional_file_not_found_error_with_response(self):
28+
"""Test OptionalFileNotFoundError with HTTP response."""
29+
mock_resp = Mock()
30+
mock_resp.status_code = 404
31+
error = OptionalFileNotFoundError(resp=mock_resp)
32+
33+
# Should be created successfully
34+
self.assertIsInstance(error, OptionalFileNotFoundError)
35+
36+
def test_can_catch_as_github3_not_found_error(self):
37+
"""Test that OptionalFileNotFoundError can be caught as github3.exceptions.NotFoundError."""
38+
mock_resp = Mock()
39+
mock_resp.status_code = 404
40+
41+
try:
42+
raise OptionalFileNotFoundError(resp=mock_resp)
43+
except github3.exceptions.NotFoundError as e:
44+
self.assertIsInstance(e, OptionalFileNotFoundError)
45+
except Exception: # pylint: disable=broad-exception-caught
46+
self.fail(
47+
"OptionalFileNotFoundError should be catchable as github3.exceptions.NotFoundError"
48+
)
49+
50+
def test_can_catch_specifically(self):
51+
"""Test that OptionalFileNotFoundError can be caught specifically."""
52+
mock_resp = Mock()
53+
mock_resp.status_code = 404
54+
55+
try:
56+
raise OptionalFileNotFoundError(resp=mock_resp)
57+
except OptionalFileNotFoundError as e:
58+
self.assertIsInstance(e, OptionalFileNotFoundError)
59+
except Exception: # pylint: disable=broad-exception-caught
60+
self.fail("OptionalFileNotFoundError should be catchable specifically")
61+
62+
def test_optional_file_not_found_error_properties(self):
63+
"""Test OptionalFileNotFoundError has expected properties."""
64+
mock_resp = Mock()
65+
mock_resp.status_code = 404
66+
67+
error = OptionalFileNotFoundError(resp=mock_resp)
68+
self.assertEqual(error.code, 404)
69+
self.assertEqual(error.response, mock_resp)
70+
71+
72+
class TestCheckOptionalFile(unittest.TestCase):
73+
"""Test the check_optional_file utility function."""
74+
75+
def test_check_optional_file_with_existing_file(self):
76+
"""Test check_optional_file when file exists."""
77+
mock_repo = Mock()
78+
mock_file_contents = Mock()
79+
mock_file_contents.size = 100
80+
mock_repo.file_contents.return_value = mock_file_contents
81+
82+
result = check_optional_file(mock_repo, "config.yml")
83+
84+
self.assertEqual(result, mock_file_contents)
85+
mock_repo.file_contents.assert_called_once_with("config.yml")
86+
87+
def test_check_optional_file_with_empty_file(self):
88+
"""Test check_optional_file when file exists but is empty."""
89+
mock_repo = Mock()
90+
mock_file_contents = Mock()
91+
mock_file_contents.size = 0
92+
mock_repo.file_contents.return_value = mock_file_contents
93+
94+
result = check_optional_file(mock_repo, "config.yml")
95+
96+
self.assertIsNone(result)
97+
mock_repo.file_contents.assert_called_once_with("config.yml")
98+
99+
def test_check_optional_file_with_missing_file(self):
100+
"""Test check_optional_file when file doesn't exist."""
101+
mock_repo = Mock()
102+
mock_resp = Mock()
103+
mock_resp.status_code = 404
104+
105+
original_error = github3.exceptions.NotFoundError(resp=mock_resp)
106+
mock_repo.file_contents.side_effect = original_error
107+
108+
with self.assertRaises(OptionalFileNotFoundError) as context:
109+
check_optional_file(mock_repo, "missing.yml")
110+
111+
# Check that the original exception is chained
112+
self.assertEqual(context.exception.__cause__, original_error)
113+
self.assertEqual(context.exception.response, mock_resp)
114+
mock_repo.file_contents.assert_called_once_with("missing.yml")
115+
116+
def test_check_optional_file_can_catch_as_not_found_error(self):
117+
"""Test that OptionalFileNotFoundError from check_optional_file can be caught as NotFoundError."""
118+
mock_repo = Mock()
119+
mock_resp = Mock()
120+
mock_resp.status_code = 404
121+
122+
mock_repo.file_contents.side_effect = github3.exceptions.NotFoundError(
123+
resp=mock_resp
124+
)
125+
126+
try:
127+
check_optional_file(mock_repo, "missing.yml")
128+
except github3.exceptions.NotFoundError as e:
129+
self.assertIsInstance(e, OptionalFileNotFoundError)
130+
except Exception: # pylint: disable=broad-exception-caught
131+
self.fail(
132+
"Should be able to catch OptionalFileNotFoundError as NotFoundError"
133+
)
134+
135+
136+
if __name__ == "__main__":
137+
unittest.main()

0 commit comments

Comments
 (0)