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
48 changes: 30 additions & 18 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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
41 changes: 12 additions & 29 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
}
]
}
```

Expand All @@ -99,12 +92,8 @@ API error messages will include the following keys:
"invalid_params": [
{
"name": "field_name",
"reason": [
"error"
// ...
]
"reason": ["error"]
}
// ...
]
}
```
Expand All @@ -114,10 +103,7 @@ API error messages will include the following keys:
```json
{
"title": "Error message.",
"detail": [
"error"
// ...
],
"detail": ["error"],
"invalid_params": null
}
```
Expand Down Expand Up @@ -157,12 +143,8 @@ If `CAMELIZE` is set to `True`:
"invalidParams": [
{
"name": "fieldName",
"reason": [
"error"
// ...
]
"reason": ["error"]
}
// ...
]
}
```
Expand Down Expand Up @@ -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
```

Expand Down
2 changes: 1 addition & 1 deletion drf_simple_api_errors/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from .exception_handler import exception_handler

__all__ = ("exception_handler",)
__version__ = "2.0.0"
__version__ = "2.1.0"
9 changes: 5 additions & 4 deletions drf_simple_api_errors/exception_handler.py
Original file line number Diff line number Diff line change
@@ -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.

Expand Down Expand Up @@ -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)
Expand Down
11 changes: 0 additions & 11 deletions drf_simple_api_errors/exceptions.py

This file was deleted.

8 changes: 3 additions & 5 deletions drf_simple_api_errors/formatter.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion drf_simple_api_errors/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -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 <[email protected]>"]
license = "MIT"
Expand Down
9 changes: 9 additions & 0 deletions tests/test_exception_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
21 changes: 16 additions & 5 deletions tests/test_formatter.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,21 +207,32 @@ 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`.
"""
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):
Expand Down
4 changes: 4 additions & 0 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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):
Expand Down