diff --git a/dependabot_file.py b/dependabot_file.py index 1dbc09f..2c7ddcc 100644 --- a/dependabot_file.py +++ b/dependabot_file.py @@ -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 @@ -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, @@ -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 diff --git a/evergreen.py b/evergreen.py index 6468d82..accd553 100644 --- a/evergreen.py +++ b/evergreen.py @@ -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 @@ -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 diff --git a/exceptions.py b/exceptions.py new file mode 100644 index 0000000..112d5a9 --- /dev/null +++ b/exceptions.py @@ -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 diff --git a/test_exceptions.py b/test_exceptions.py new file mode 100644 index 0000000..a013737 --- /dev/null +++ b/test_exceptions.py @@ -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()