diff --git a/CHANGELOG.md b/CHANGELOG.md index 395c8bc..d89d8dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,24 +1,31 @@ # Change Log -## [1.0.0] - 2024-04-26 +## [2.1.0] - 2025-08-17 Changes: -- First release on PyPI +- Camelize would capitalize words starting with an underscore. It now leaves them unchanged. -## [1.0.1] - 2024-04-30 + - Old: `"_special"` -> `"Special"` + - Now: `"_special"` -> `"_special"` -Changes: +- Improve test coverage +- Update README -- Upgrade dependencies +## [2.0.0] - 2025-07-16 -## [1.0.2] - 2024-09-08 +Breaking changes: + +- The API error response now **always** includes the keys: `title`, `detail`, and `invalid_param`. The `title` key is always populated, while `detail` and `invalid_param` may be `null` depending on the error source. +- Drop support for python 3.8 Changes: -- Add docstrings -- Improve Makefile -- Improve README +- Improve code modularity and readability +- Split tests in unittest and integration tests +- Improve test coverage +- Update Makefile +- Update README ## [1.0.3] - 2025-03-16 @@ -28,17 +35,22 @@ Changes: - Improve tests - Update README -## [2.0.0] - 2025-07-11 +## [1.0.2] - 2024-09-08 + +Changes: -Breaking changes: +- Add docstrings +- Improve Makefile +- Improve README -- The API error response now **always** includes the keys: `title`, `detail`, and `invalid_param`. The `title` key is always populated, while `detail` and `invalid_param` may be `null` depending on the error source. -- Drop support for python 3.8 +## [1.0.1] - 2024-04-30 Changes: -- Improve code modularity and readability -- Split tests in unittest and integration tests -- Improve test coverage -- Update Makefile -- Update README +- Upgrade dependencies + +## [1.0.0] - 2024-04-26 + +Changes: + +- First release on PyPI diff --git a/README.md b/README.md index e610452..a285a0d 100644 --- a/README.md +++ b/README.md @@ -70,21 +70,14 @@ API error messages will include the following keys: ```json { - "title": "Error message.", - "detail": [ - "error", - ... - ], - "invalid_params": [ - { - "name": "field_name", - "reason": [ - "error", - ... - ] - }, - ... - ] + "title": "Error message.", + "detail": ["error"], + "invalid_params": [ + { + "name": "field_name", + "reason": ["error"] + } + ] } ``` @@ -99,12 +92,8 @@ API error messages will include the following keys: "invalid_params": [ { "name": "field_name", - "reason": [ - "error" - // ... - ] + "reason": ["error"] } - // ... ] } ``` @@ -114,10 +103,7 @@ API error messages will include the following keys: ```json { "title": "Error message.", - "detail": [ - "error" - // ... - ], + "detail": ["error"], "invalid_params": null } ``` @@ -157,12 +143,8 @@ If `CAMELIZE` is set to `True`: "invalidParams": [ { "name": "fieldName", - "reason": [ - "error" - // ... - ] + "reason": ["error"] } - // ... ] } ``` @@ -226,6 +208,7 @@ make test Finally, run `tox` to ensure the changes work for every supported python version: ``` +pip install tox # only if necessary tox -v ``` diff --git a/drf_simple_api_errors/__init__.py b/drf_simple_api_errors/__init__.py index a75c52d..c6a5d26 100644 --- a/drf_simple_api_errors/__init__.py +++ b/drf_simple_api_errors/__init__.py @@ -1,4 +1,4 @@ from .exception_handler import exception_handler __all__ = ("exception_handler",) -__version__ = "2.0.0" +__version__ = "2.1.0" diff --git a/drf_simple_api_errors/exception_handler.py b/drf_simple_api_errors/exception_handler.py index ccbbbab..e184bcc 100644 --- a/drf_simple_api_errors/exception_handler.py +++ b/drf_simple_api_errors/exception_handler.py @@ -1,17 +1,19 @@ import logging +from typing import Optional from rest_framework import exceptions from rest_framework.response import Response from rest_framework.views import set_rollback from drf_simple_api_errors import formatter, handlers -from drf_simple_api_errors.exceptions import ServerError from drf_simple_api_errors.types import ExceptionHandlerContext logger = logging.getLogger(__name__) -def exception_handler(exc: Exception, context: ExceptionHandlerContext) -> Response: +def exception_handler( + exc: Exception, context: ExceptionHandlerContext +) -> Optional[Response]: """ Custom exception handler for DRF. @@ -44,8 +46,7 @@ def exception_handler(exc: Exception, context: ExceptionHandlerContext) -> Respo # This is because it's not good practice to expose the details of # unhandled exceptions to the client. if not isinstance(exc, exceptions.APIException): - logger.info("Server error (500) from unexpected exception.", exc_info=True) - return ServerError + return None # Get the API response headers from the exception. headers = handlers.get_response_headers(exc) diff --git a/drf_simple_api_errors/exceptions.py b/drf_simple_api_errors/exceptions.py deleted file mode 100644 index f918d69..0000000 --- a/drf_simple_api_errors/exceptions.py +++ /dev/null @@ -1,11 +0,0 @@ -from django.utils.translation import gettext_lazy as _ -from rest_framework import status -from rest_framework.exceptions import APIException - - -class ServerError(APIException): - """A class for REST framework '500 Internal Server' Error exception.""" - - status_code = status.HTTP_500_INTERNAL_SERVER_ERROR - default_detail = _("Server error.") - default_code = "internal_server_error" diff --git a/drf_simple_api_errors/formatter.py b/drf_simple_api_errors/formatter.py index eb7b9e9..505c601 100644 --- a/drf_simple_api_errors/formatter.py +++ b/drf_simple_api_errors/formatter.py @@ -101,12 +101,10 @@ def format_exc(exc: exceptions.APIException) -> APIErrorResponseDict: # Create the API error response based on the exception detail... if isinstance(exc_detail, dict): return _format_exc_detail_dict(data, exc_detail) - elif isinstance(exc_detail, list): - # If the exception detail is a list, we will return all the errors - # in a single list. - return _format_exc_detail_list(data, exc_detail) else: - return data.to_dict() + # If the exception detail in not a dict, it must be a list, and + # we will return all the errors in a single list. + return _format_exc_detail_list(data, exc_detail) def _format_exc_detail_dict( diff --git a/drf_simple_api_errors/utils.py b/drf_simple_api_errors/utils.py index fb5b083..8dce1ce 100644 --- a/drf_simple_api_errors/utils.py +++ b/drf_simple_api_errors/utils.py @@ -29,7 +29,7 @@ def underscore_to_camel(match: re.Match) -> str: if len(group) == 3: return group[0] + group[2].upper() else: - return group[1].upper() + return group if not api_settings.CAMELIZE: return s diff --git a/pyproject.toml b/pyproject.toml index 444c39e..fc72f49 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "drf-simple-api-errors" -version = "2.0.0" +version = "2.1.0" description = "A library for Django Rest Framework returning consistent and easy-to-parse API error messages." authors = ["Gian <30044863+gripep@users.noreply.github.com>"] license = "MIT" diff --git a/tests/test_exception_handler.py b/tests/test_exception_handler.py index f12fda8..62e90cc 100644 --- a/tests/test_exception_handler.py +++ b/tests/test_exception_handler.py @@ -399,3 +399,12 @@ def test_drf_throttled(self, mocker, wait, expected_detail): "invalid_params": None, } assert render_response(response.data) == expected_response + + def test_unexpected_exception(self, mocker): + """ + Test the exception handler for unexpected exceptions. + This should return a 500 Internal Server Error response. + """ + exc = Exception("Unexpected error") + response = exception_handler(exc, mocker.Mock()) + assert response is None diff --git a/tests/test_formatter.py b/tests/test_formatter.py index 0f844a3..02c4d92 100644 --- a/tests/test_formatter.py +++ b/tests/test_formatter.py @@ -207,13 +207,24 @@ def test_exc_detail_is_dict_with_non_field_errors_formats(self, mocker, exc_deta assert data["invalid_params"] is None @pytest.mark.parametrize( - "exc_detail", + "exc_detail, expected_detail", [ - "This is a non-field error.", - ["This is a non-field error."], + ("This is a non-field error.", ["This is a non-field error."]), + (["This is a non-field error."], ["This is a non-field error."]), + ( + ["This is a non-field error.", "Another error."], + ["This is a non-field error.", "Another error."], + ), + ( + [ + "This is a non-field error.", + ["Another error.", "Yet another error."], + ], + ["This is a non-field error.", "Another error.", "Yet another error."], + ), ], ) - def test_exc_detail_is_list_formats(self, exc_detail): + def test_exc_detail_is_list_formats(self, exc_detail, expected_detail): """ Test that when the exception detail is a list or a string, the detail is set to the error messages list and invalid_params is `None`. @@ -221,7 +232,7 @@ def test_exc_detail_is_list_formats(self, exc_detail): exc = drf_exceptions.ValidationError(exc_detail) data = formatter.format_exc(exc) - assert data["detail"] == ["This is a non-field error."] + assert data["detail"] == expected_detail assert data["invalid_params"] is None def test_format_exc_detail_is_list_error_when_unexpected_type(self): diff --git a/tests/test_utils.py b/tests/test_utils.py index dd60f41..ec81f4d 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -14,6 +14,9 @@ class TestCamelize: ("first_name", "firstName"), ("family_tree_name", "familyTreeName"), ("very_long_last_name_and_first_name", "veryLongLastNameAndFirstName"), + # This is a special case where the underscore is at the start + # and should NOT be removed. + ("_special", "_special"), ], ) def test_with_camelize_settings_true(self, mocker, field_input, expected_output): @@ -33,6 +36,7 @@ def test_with_camelize_settings_true(self, mocker, field_input, expected_output) "very_long_last_name_and_first_name", "very_long_last_name_and_first_name", ), + ("_special", "_special"), ], ) def test_with_camelize_settings_false(self, field_input, expected_output):