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
5 changes: 3 additions & 2 deletions dependabot_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import github3
import ruamel.yaml
from exceptions import OptionalFileNotFoundError, check_optional_file
from ruamel.yaml.scalarstring import SingleQuotedScalarString

# Define data structure for dependabot.yaml
Expand Down Expand Up @@ -192,7 +193,7 @@ def build_dependabot_file(
continue
for file in manifest_files:
try:
if repo.file_contents(file):
if check_optional_file(repo, file):
package_managers_found[manager] = True
make_dependabot_config(
manager,
Expand All @@ -204,7 +205,7 @@ def build_dependabot_file(
extra_dependabot_config,
)
break
except github3.exceptions.NotFoundError:
except OptionalFileNotFoundError:
# The file does not exist and is not required,
# so we should continue to the next one rather than raising error or logging
pass
Expand Down
7 changes: 4 additions & 3 deletions evergreen.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import requests
import ruamel.yaml
from dependabot_file import build_dependabot_file
from exceptions import OptionalFileNotFoundError, check_optional_file


def main(): # pragma: no cover
Expand Down Expand Up @@ -314,10 +315,10 @@ def check_existing_config(repo, filename):
"""
existing_config = None
try:
existing_config = repo.file_contents(filename)
if existing_config.size > 0:
existing_config = check_optional_file(repo, filename)
if existing_config:
return existing_config
except github3.exceptions.NotFoundError:
except OptionalFileNotFoundError:
# The file does not exist and is not required,
# so we should continue to the next one rather than raising error or logging
pass
Expand Down
49 changes: 49 additions & 0 deletions exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
"""Custom exceptions for the evergreen application."""

import github3.exceptions


class OptionalFileNotFoundError(github3.exceptions.NotFoundError):
"""Exception raised when an optional file is not found.

This exception inherits from github3.exceptions.NotFoundError but provides
a more explicit name for cases where missing files are expected and should
not be treated as errors. This is typically used for optional configuration
files or dependency files that may not exist in all repositories.

Args:
resp: The HTTP response object from the failed request
"""


def check_optional_file(repo, filename):
"""
Example utility function demonstrating OptionalFileNotFoundError usage.

This function shows how the new exception type can be used to provide
more explicit error handling for optional files that may not exist.

Args:
repo: GitHub repository object
filename: Name of the optional file to check

Returns:
File contents object if file exists, None if optional file is missing

Raises:
OptionalFileNotFoundError: When the file is not found (expected for optional files)
Other exceptions: For unexpected errors (permissions, network issues, etc.)
"""
try:
file_contents = repo.file_contents(filename)
# Handle both real file contents objects and test mocks that return boolean
if hasattr(file_contents, "size"):
# Real file contents object
if file_contents.size > 0:
return file_contents
return None
# Test mock or other truthy value
return file_contents if file_contents else None
except github3.exceptions.NotFoundError as e:
# Re-raise as our more specific exception type for better semantic clarity
raise OptionalFileNotFoundError(resp=e.response) from e
137 changes: 137 additions & 0 deletions test_exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
"""Tests for the exceptions module."""

import unittest
from unittest.mock import Mock

import github3.exceptions
from exceptions import OptionalFileNotFoundError, check_optional_file


class TestOptionalFileNotFoundError(unittest.TestCase):
"""Test the OptionalFileNotFoundError exception."""

def test_optional_file_not_found_error_inherits_from_not_found_error(self):
"""Test that OptionalFileNotFoundError inherits from github3.exceptions.NotFoundError."""
mock_resp = Mock()
mock_resp.status_code = 404
error = OptionalFileNotFoundError(resp=mock_resp)
self.assertIsInstance(error, github3.exceptions.NotFoundError)

def test_optional_file_not_found_error_creation(self):
"""Test OptionalFileNotFoundError can be created."""
mock_resp = Mock()
mock_resp.status_code = 404
error = OptionalFileNotFoundError(resp=mock_resp)
self.assertIsInstance(error, OptionalFileNotFoundError)

def test_optional_file_not_found_error_with_response(self):
"""Test OptionalFileNotFoundError with HTTP response."""
mock_resp = Mock()
mock_resp.status_code = 404
error = OptionalFileNotFoundError(resp=mock_resp)

# Should be created successfully
self.assertIsInstance(error, OptionalFileNotFoundError)

def test_can_catch_as_github3_not_found_error(self):
"""Test that OptionalFileNotFoundError can be caught as github3.exceptions.NotFoundError."""
mock_resp = Mock()
mock_resp.status_code = 404

try:
raise OptionalFileNotFoundError(resp=mock_resp)
except github3.exceptions.NotFoundError as e:
self.assertIsInstance(e, OptionalFileNotFoundError)
except Exception: # pylint: disable=broad-exception-caught
self.fail(
"OptionalFileNotFoundError should be catchable as github3.exceptions.NotFoundError"
)

def test_can_catch_specifically(self):
"""Test that OptionalFileNotFoundError can be caught specifically."""
mock_resp = Mock()
mock_resp.status_code = 404

try:
raise OptionalFileNotFoundError(resp=mock_resp)
except OptionalFileNotFoundError as e:
self.assertIsInstance(e, OptionalFileNotFoundError)
except Exception: # pylint: disable=broad-exception-caught
self.fail("OptionalFileNotFoundError should be catchable specifically")

def test_optional_file_not_found_error_properties(self):
"""Test OptionalFileNotFoundError has expected properties."""
mock_resp = Mock()
mock_resp.status_code = 404

error = OptionalFileNotFoundError(resp=mock_resp)
self.assertEqual(error.code, 404)
self.assertEqual(error.response, mock_resp)


class TestCheckOptionalFile(unittest.TestCase):
"""Test the check_optional_file utility function."""

def test_check_optional_file_with_existing_file(self):
"""Test check_optional_file when file exists."""
mock_repo = Mock()
mock_file_contents = Mock()
mock_file_contents.size = 100
mock_repo.file_contents.return_value = mock_file_contents

result = check_optional_file(mock_repo, "config.yml")

self.assertEqual(result, mock_file_contents)
mock_repo.file_contents.assert_called_once_with("config.yml")

def test_check_optional_file_with_empty_file(self):
"""Test check_optional_file when file exists but is empty."""
mock_repo = Mock()
mock_file_contents = Mock()
mock_file_contents.size = 0
mock_repo.file_contents.return_value = mock_file_contents

result = check_optional_file(mock_repo, "config.yml")

self.assertIsNone(result)
mock_repo.file_contents.assert_called_once_with("config.yml")

def test_check_optional_file_with_missing_file(self):
"""Test check_optional_file when file doesn't exist."""
mock_repo = Mock()
mock_resp = Mock()
mock_resp.status_code = 404

original_error = github3.exceptions.NotFoundError(resp=mock_resp)
mock_repo.file_contents.side_effect = original_error

with self.assertRaises(OptionalFileNotFoundError) as context:
check_optional_file(mock_repo, "missing.yml")

# Check that the original exception is chained
self.assertEqual(context.exception.__cause__, original_error)
self.assertEqual(context.exception.response, mock_resp)
mock_repo.file_contents.assert_called_once_with("missing.yml")

def test_check_optional_file_can_catch_as_not_found_error(self):
"""Test that OptionalFileNotFoundError from check_optional_file can be caught as NotFoundError."""
mock_repo = Mock()
mock_resp = Mock()
mock_resp.status_code = 404

mock_repo.file_contents.side_effect = github3.exceptions.NotFoundError(
resp=mock_resp
)

try:
check_optional_file(mock_repo, "missing.yml")
except github3.exceptions.NotFoundError as e:
self.assertIsInstance(e, OptionalFileNotFoundError)
except Exception: # pylint: disable=broad-exception-caught
self.fail(
"Should be able to catch OptionalFileNotFoundError as NotFoundError"
)


if __name__ == "__main__":
unittest.main()
Loading