From d57a40b5796ba787ea8d7da6a986f85d786fd9f1 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 21 Nov 2019 13:45:55 +0000 Subject: [PATCH] Drop set_context() in favour of 'requires_context = True' --- docs/api-guide/fields.md | 14 ++++++++- docs/api-guide/validators.md | 13 ++++---- rest_framework/fields.py | 58 ++++++++++++++++++++++++------------ rest_framework/validators.py | 19 ++++-------- 4 files changed, 64 insertions(+), 40 deletions(-) diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index 1e6f82eed..e964458f9 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -50,7 +50,19 @@ If set, this gives the default value that will be used for the field if no input The `default` is not applied during partial update operations. In the partial update case only fields that are provided in the incoming data will have a validated value returned. -May be set to a function or other callable, in which case the value will be evaluated each time it is used. When called, it will receive no arguments. If the callable has a `set_context` method, that will be called each time before getting the value with the field instance as only argument. +May be set to a function or other callable, in which case the value will be evaluated each time it is used. When called, it will receive no arguments. If the callable has a `requires_context = True` attribute, then the serializer field will be passed as an argument. + +For example: + + class CurrentUserDefault: + """ + May be applied as a `default=...` value on a serializer field. + Returns the current user. + """ + requires_context = True + + def __call__(self, serializer_field): + return serializer_field.context['request'].user When serializing the instance, default will be used if the object attribute or dictionary key is not present in the instance. diff --git a/docs/api-guide/validators.md b/docs/api-guide/validators.md index f04ffd7fe..009cd2468 100644 --- a/docs/api-guide/validators.md +++ b/docs/api-guide/validators.md @@ -294,15 +294,14 @@ To write a class-based validator, use the `__call__` method. Class-based validat #### Accessing the context In some advanced cases you might want a validator to be passed the serializer -field it is being used with as additional context. You can do so by using -`rest_framework.validators.ContextBasedValidator` as a base class for the -validator. The `__call__` method will then be called with the `serializer_field` +field it is being used with as additional context. You can do so by setting +a `requires_context = True` attribute on the validator. The `__call__` method +will then be called with the `serializer_field` or `serializer` as an additional argument. - def __call__(self, value, serializer_field): - # Determine if this is an update or a create operation. - is_update = serializer_field.parent.instance is not None + requires_context = True - pass # implementation of the validator that uses `is_update` + def __call__(self, value, serializer_field): + ... [cite]: https://docs.djangoproject.com/en/stable/ref/validators/ diff --git a/rest_framework/fields.py b/rest_framework/fields.py index fd99afc26..9507914e8 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -250,19 +250,30 @@ class CreateOnlyDefault: for create operations, but that do not return any value for update operations. """ + requires_context = True + def __init__(self, default): self.default = default - def set_context(self, serializer_field): - self.is_update = serializer_field.parent.instance is not None - if callable(self.default) and hasattr(self.default, 'set_context') and not self.is_update: - self.default.set_context(serializer_field) - - def __call__(self): - if self.is_update: + def __call__(self, serializer_field): + is_update = serializer_field.parent.instance is not None + if is_update: raise SkipField() if callable(self.default): - return self.default() + if hasattr(self.default, 'set_context'): + warnings.warn( + "Method `set_context` on defaults is deprecated and will " + "no longer be called starting with 3.12. Instead set " + "`requires_context = True` on the class, and accept the " + "context as an additional argument.", + DeprecationWarning, stacklevel=2 + ) + self.default.set_context(self) + + if getattr(self.default, 'requires_context', False): + return self.default(serializer_field) + else: + return self.default() return self.default def __repr__(self): @@ -270,11 +281,10 @@ class CreateOnlyDefault: class CurrentUserDefault: - def set_context(self, serializer_field): - self.user = serializer_field.context['request'].user + requires_context = True - def __call__(self): - return self.user + def __call__(self, serializer_field): + return serializer_field.context['request'].user def __repr__(self): return '%s()' % self.__class__.__name__ @@ -490,8 +500,20 @@ class Field: raise SkipField() if callable(self.default): if hasattr(self.default, 'set_context'): + warnings.warn( + "Method `set_context` on defaults is deprecated and will " + "no longer be called starting with 3.12. Instead set " + "`requires_context = True` on the class, and accept the " + "context as an additional argument.", + DeprecationWarning, stacklevel=2 + ) self.default.set_context(self) - return self.default() + + if getattr(self.default, 'requires_context', False): + return self.default(self) + else: + return self.default() + return self.default def validate_empty_values(self, data): @@ -549,22 +571,20 @@ class Field: Test the given value against all the validators on the field, and either raise a `ValidationError` or simply return. """ - from rest_framework.validators import ContextBasedValidator - errors = [] for validator in self.validators: if hasattr(validator, 'set_context'): warnings.warn( "Method `set_context` on validators is deprecated and will " - "no longer be called starting with 3.11. Instead derive the " - "validator from `rest_framwork.validators.ContextBasedValidator` " - "and accept the context as an additional argument.", + "no longer be called starting with 3.12. Instead set " + "`requires_context = True` on the class, and accept the " + "context as an additional argument.", DeprecationWarning, stacklevel=2 ) validator.set_context(self) try: - if isinstance(validator, ContextBasedValidator): + if getattr(validator, 'requires_context', False): validator(value, self) else: validator(value) diff --git a/rest_framework/validators.py b/rest_framework/validators.py index 0ead18089..2907312a9 100644 --- a/rest_framework/validators.py +++ b/rest_framework/validators.py @@ -30,23 +30,14 @@ def qs_filter(queryset, **kwargs): return queryset.none() -class ContextBasedValidator: - """Base class for validators that need a context during evaluation. - - In extension to regular validators their `__call__` method must not only - accept a value, but also an instance of a serializer. - """ - def __call__(self, value, serializer): - raise NotImplementedError('`__call__()` must be implemented.') - - -class UniqueValidator(ContextBasedValidator): +class UniqueValidator: """ Validator that corresponds to `unique=True` on a model field. Should be applied to an individual field on the serializer. """ message = _('This field must be unique.') + requires_context = True def __init__(self, queryset, message=None, lookup='exact'): self.queryset = queryset @@ -90,7 +81,7 @@ class UniqueValidator(ContextBasedValidator): ) -class UniqueTogetherValidator(ContextBasedValidator): +class UniqueTogetherValidator: """ Validator that corresponds to `unique_together = (...)` on a model class. @@ -98,6 +89,7 @@ class UniqueTogetherValidator(ContextBasedValidator): """ message = _('The fields {field_names} must make a unique set.') missing_message = _('This field is required.') + requires_context = True def __init__(self, queryset, fields, message=None): self.queryset = queryset @@ -174,9 +166,10 @@ class UniqueTogetherValidator(ContextBasedValidator): ) -class BaseUniqueForValidator(ContextBasedValidator): +class BaseUniqueForValidator: message = None missing_message = _('This field is required.') + requires_context = True def __init__(self, queryset, field, date_field, message=None): self.queryset = queryset