From f0ff44923256745f316585f324ab97501d2849d0 Mon Sep 17 00:00:00 2001 From: Tyson Date: Tue, 12 Apr 2022 20:48:25 +1000 Subject: [PATCH] Raise PEP 3134 chained exceptions Use PEP 3134 (https://peps.python.org/pep-3134/) exception chaining to provide enhanced reporting and extra context when errors are encountered. --- rest_framework/authentication.py | 8 ++++---- rest_framework/fields.py | 20 +++++++++++++------- rest_framework/generics.py | 4 ++-- rest_framework/pagination.py | 6 +++--- rest_framework/parsers.py | 4 ++-- rest_framework/relations.py | 15 ++++++--------- rest_framework/request.py | 7 ++----- rest_framework/schemas/coreapi.py | 4 ++-- rest_framework/serializers.py | 25 +++++++++++++------------ rest_framework/settings.py | 2 +- rest_framework/throttling.py | 4 ++-- 11 files changed, 50 insertions(+), 49 deletions(-) diff --git a/rest_framework/authentication.py b/rest_framework/authentication.py index 382abf158..d03ec8e73 100644 --- a/rest_framework/authentication.py +++ b/rest_framework/authentication.py @@ -79,9 +79,9 @@ class BasicAuthentication(BaseAuthentication): except UnicodeDecodeError: auth_decoded = base64.b64decode(auth[1]).decode('latin-1') auth_parts = auth_decoded.partition(':') - except (TypeError, UnicodeDecodeError, binascii.Error): + except (TypeError, UnicodeDecodeError, binascii.Error) as exc: msg = _('Invalid basic header. Credentials not correctly base64 encoded.') - raise exceptions.AuthenticationFailed(msg) + raise exceptions.AuthenticationFailed(msg) from exc userid, password = auth_parts[0], auth_parts[2] return self.authenticate_credentials(userid, password, request) @@ -189,9 +189,9 @@ class TokenAuthentication(BaseAuthentication): try: token = auth[1].decode() - except UnicodeError: + except UnicodeError as exc: msg = _('Invalid token header. Token string should not contain invalid characters.') - raise exceptions.AuthenticationFailed(msg) + raise exceptions.AuthenticationFailed(msg) from exc return self.authenticate_credentials(token) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index d7e7816ce..61970ccf3 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -4,6 +4,7 @@ import decimal import functools import inspect import re +import sys import uuid import warnings from collections import OrderedDict @@ -104,7 +105,7 @@ def get_attribute(instance, attrs): # If we raised an Attribute or KeyError here it'd get treated # as an omitted field in `Field.get_attribute()`. Instead we # raise a ValueError to ensure the exception is not masked. - raise ValueError('Exception raised in callable attribute "{}"; original exception was: {}'.format(attr, exc)) + raise ValueError('Exception raised in callable attribute "{}"; original exception was: {}'.format(attr, exc)) from exc return instance @@ -466,14 +467,14 @@ class Field: instance=instance.__class__.__name__, ) ) - raise type(exc)(msg) + raise type(exc)(msg) from exc except (KeyError, AttributeError) as exc: if self.default is not empty: return self.get_default() if self.allow_null: return None if not self.required: - raise SkipField() + raise SkipField() from exc msg = ( 'Got {exc_type} when attempting to get a value for field ' '`{field}` on serializer `{serializer}`.\nThe serializer ' @@ -487,7 +488,7 @@ class Field: exc=exc ) ) - raise type(exc)(msg) + raise type(exc)(msg) from exc def get_default(self): """ @@ -633,12 +634,17 @@ class Field: """ try: msg = self.error_messages[key] - except KeyError: + except KeyError as exc: class_name = self.__class__.__name__ msg = MISSING_ERROR_MESSAGE.format(class_name=class_name, key=key) - raise AssertionError(msg) + raise AssertionError(msg) from exc message_string = msg.format(**kwargs) - raise ValidationError(message_string, code=key) + err = ValidationError(message_string, code=key) + (_, exc, _) = sys.exc_info() + if exc: + raise err from exc + else: + raise err @property def root(self): diff --git a/rest_framework/generics.py b/rest_framework/generics.py index 55cfafda4..5ce84224a 100644 --- a/rest_framework/generics.py +++ b/rest_framework/generics.py @@ -17,8 +17,8 @@ def get_object_or_404(queryset, *filter_args, **filter_kwargs): """ try: return _get_object_or_404(queryset, *filter_args, **filter_kwargs) - except (TypeError, ValueError, ValidationError): - raise Http404 + except (TypeError, ValueError, ValidationError) as exc: + raise Http404 from exc class GenericAPIView(views.APIView): diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py index e815d8d5c..a317f28eb 100644 --- a/rest_framework/pagination.py +++ b/rest_framework/pagination.py @@ -206,7 +206,7 @@ class PageNumberPagination(BasePagination): msg = self.invalid_page_message.format( page_number=page_number, message=str(exc) ) - raise NotFound(msg) + raise NotFound(msg) from exc if paginator.num_pages > 1 and self.template is not None: # The browsable API should display pagination controls. @@ -862,8 +862,8 @@ class CursorPagination(BasePagination): reverse = bool(int(reverse)) position = tokens.get('p', [None])[0] - except (TypeError, ValueError): - raise NotFound(self.invalid_cursor_message) + except (TypeError, ValueError) as exc: + raise NotFound(self.invalid_cursor_message) from exc return Cursor(offset=offset, reverse=reverse, position=position) diff --git a/rest_framework/parsers.py b/rest_framework/parsers.py index fc4eb1428..711b9f79a 100644 --- a/rest_framework/parsers.py +++ b/rest_framework/parsers.py @@ -64,7 +64,7 @@ class JSONParser(BaseParser): parse_constant = json.strict_constant if self.strict else None return json.load(decoded_stream, parse_constant=parse_constant) except ValueError as exc: - raise ParseError('JSON parse error - %s' % str(exc)) + raise ParseError('JSON parse error - %s' % str(exc)) from exc class FormParser(BaseParser): @@ -109,7 +109,7 @@ class MultiPartParser(BaseParser): data, files = parser.parse() return DataAndFiles(data, files) except MultiPartParserError as exc: - raise ParseError('Multipart form parse error - %s' % str(exc)) + raise ParseError('Multipart form parse error - %s' % str(exc)) from exc class FileUploadParser(BaseParser): diff --git a/rest_framework/relations.py b/rest_framework/relations.py index c98700784..7662067f5 100644 --- a/rest_framework/relations.py +++ b/rest_framework/relations.py @@ -1,4 +1,3 @@ -import sys from collections import OrderedDict from urllib import parse @@ -316,12 +315,10 @@ class HyperlinkedRelatedField(RelatedField): try: return queryset.get(**lookup_kwargs) - except ValueError: - exc = ObjectValueError(str(sys.exc_info()[1])) - raise exc.with_traceback(sys.exc_info()[2]) - except TypeError: - exc = ObjectTypeError(str(sys.exc_info()[1])) - raise exc.with_traceback(sys.exc_info()[2]) + except ValueError as exc: + raise ObjectValueError(str(exc)) from exc + except TypeError as exc: + raise ObjectTypeError(str(exc)) from exc def get_url(self, obj, view_name, request, format): """ @@ -399,7 +396,7 @@ class HyperlinkedRelatedField(RelatedField): # Return the hyperlink, or error if incorrectly configured. try: url = self.get_url(value, self.view_name, request, format) - except NoReverseMatch: + except NoReverseMatch as exc: msg = ( 'Could not resolve URL for hyperlinked relationship using ' 'view name "%s". You may have failed to include the related ' @@ -413,7 +410,7 @@ class HyperlinkedRelatedField(RelatedField): "was %s, which may be why it didn't match any " "entries in your URL conf." % value_string ) - raise ImproperlyConfigured(msg % self.view_name) + raise ImproperlyConfigured(msg % self.view_name) from exc if url is None: return None diff --git a/rest_framework/request.py b/rest_framework/request.py index 17ceadb08..133ecc718 100644 --- a/rest_framework/request.py +++ b/rest_framework/request.py @@ -9,7 +9,6 @@ The wrapped request then offers a richer API, in particular : - form overloading of HTTP method, content type and content """ import io -import sys from contextlib import contextmanager from django.conf import settings @@ -72,10 +71,8 @@ def wrap_attributeerrors(): """ try: yield - except AttributeError: - info = sys.exc_info() - exc = WrappedAttributeError(str(info[1])) - raise exc.with_traceback(info[2]) + except AttributeError as exc: + raise WrappedAttributeError(str(exc)) from exc class Empty: diff --git a/rest_framework/schemas/coreapi.py b/rest_framework/schemas/coreapi.py index 179f0fa3c..b87b0c3f9 100644 --- a/rest_framework/schemas/coreapi.py +++ b/rest_framework/schemas/coreapi.py @@ -89,13 +89,13 @@ def insert_into(target, keys, value): try: target.links.append((keys[-1], value)) - except TypeError: + except TypeError as exc: msg = INSERT_INTO_COLLISION_FMT.format( value_url=value.url, target_url=target.url, keys=keys ) - raise ValueError(msg) + raise ValueError(msg) from exc class SchemaGenerator(BaseSchemaGenerator): diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 389680517..19f1c0e08 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -12,7 +12,6 @@ response content is handled by parsers and renderers. """ import copy import inspect -import traceback from collections import OrderedDict, defaultdict from collections.abc import Mapping @@ -227,12 +226,14 @@ class BaseSerializer(Field): self._validated_data = self.run_validation(self.initial_data) except ValidationError as exc: self._validated_data = {} + self._exc = exc self._errors = exc.detail else: + self._exc = None self._errors = {} - if self._errors and raise_exception: - raise ValidationError(self.errors) + if raise_exception and self._exc is not None: + raise ValidationError(self.errors) from self._exc return not bool(self._errors) @@ -429,7 +430,7 @@ class Serializer(BaseSerializer, metaclass=SerializerMetaclass): value = self.validate(value) assert value is not None, '.validate() should return the validated data' except (ValidationError, DjangoValidationError) as exc: - raise ValidationError(detail=as_serializer_error(exc)) + raise ValidationError(detail=as_serializer_error(exc)) from exc return value @@ -621,7 +622,7 @@ class ListSerializer(BaseSerializer): value = self.validate(value) assert value is not None, '.validate() should return the validated data' except (ValidationError, DjangoValidationError) as exc: - raise ValidationError(detail=as_serializer_error(exc)) + raise ValidationError(detail=as_serializer_error(exc)) from exc return value @@ -748,12 +749,14 @@ class ListSerializer(BaseSerializer): self._validated_data = self.run_validation(self.initial_data) except ValidationError as exc: self._validated_data = [] + self._exc = exc self._errors = exc.detail else: + self._exc = None self._errors = [] - if self._errors and raise_exception: - raise ValidationError(self.errors) + if raise_exception and self._exc is not None: + raise ValidationError(self.errors) from self._exc return not bool(self._errors) @@ -960,25 +963,23 @@ class ModelSerializer(Serializer): try: instance = ModelClass._default_manager.create(**validated_data) - except TypeError: - tb = traceback.format_exc() + except TypeError as exc: msg = ( 'Got a `TypeError` when calling `%s.%s.create()`. ' 'This may be because you have a writable field on the ' 'serializer class that is not a valid argument to ' '`%s.%s.create()`. You may need to make the field ' 'read-only, or override the %s.create() method to handle ' - 'this correctly.\nOriginal exception was:\n %s' % + 'this correctly.' % ( ModelClass.__name__, ModelClass._default_manager.name, ModelClass.__name__, ModelClass._default_manager.name, self.__class__.__name__, - tb ) ) - raise TypeError(msg) + raise TypeError(msg) from exc # Save many-to-many relationships after the instance is created. if many_to_many: diff --git a/rest_framework/settings.py b/rest_framework/settings.py index 9eb4c5653..ae18fc7cc 100644 --- a/rest_framework/settings.py +++ b/rest_framework/settings.py @@ -177,7 +177,7 @@ def import_from_string(val, setting_name): return import_string(val) except ImportError as e: msg = "Could not import '%s' for API setting '%s'. %s: %s." % (val, setting_name, e.__class__.__name__, e) - raise ImportError(msg) + raise ImportError(msg) from e class APISettings: diff --git a/rest_framework/throttling.py b/rest_framework/throttling.py index e262b886b..55aa79053 100644 --- a/rest_framework/throttling.py +++ b/rest_framework/throttling.py @@ -90,9 +90,9 @@ class SimpleRateThrottle(BaseThrottle): try: return self.THROTTLE_RATES[self.scope] - except KeyError: + except KeyError as exc: msg = "No default throttle rate set for '%s' scope" % self.scope - raise ImproperlyConfigured(msg) + raise ImproperlyConfigured(msg) from exc def parse_rate(self, rate): """