From ace2123d2732dbd15920eb71bcec92a9eabebfd5 Mon Sep 17 00:00:00 2001 From: Damien Nozay Date: Thu, 11 Dec 2014 14:34:45 -0800 Subject: [PATCH 01/13] Allow to subclass a Field and make it read-only Rather than subclass a `Field` and override `__init__` you may want a more data-driven approach where the defaults for `read_only`, `write_only` and `allow_null` are specified on the class. e.g. ```python class ReadOnlyLink(serializers.HyperlinkedRelatedField) read_only = True ``` --- rest_framework/fields.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index aab80982a..d3ccb6591 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -156,13 +156,27 @@ class Field(object): default_empty_html = empty initial = None - def __init__(self, read_only=False, write_only=False, + # allows to subclass a Field and have defaults on the class. + read_only = False + write_only = False + allow_null = False + + def __init__(self, read_only=None, write_only=None, required=None, default=empty, initial=empty, source=None, label=None, help_text=None, style=None, - error_messages=None, validators=None, allow_null=False): + error_messages=None, validators=None, allow_null=None): self._creation_counter = Field._creation_counter Field._creation_counter += 1 + if read_only is None: + read_only = self.read_only + + if write_only is None: + write_only = self.write_only + + if allow_null is None: + allow_null = self.allow_null + # If `required` is unset, then use `True` unless a default is provided. if required is None: required = default is empty and not read_only From 6e5517ae2d0f15327d5eb9c37b584995fa963d49 Mon Sep 17 00:00:00 2001 From: Damien Nozay Date: Sun, 4 Jan 2015 12:07:35 -0800 Subject: [PATCH 02/13] Allow CharField to specify defaults directly on class --- rest_framework/fields.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index d3ccb6591..f1950cefa 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -563,18 +563,22 @@ class CharField(Field): 'min_length': _('Ensure this field has at least {min_length} characters.') } initial = '' + # allows subclasses to change defaults + allow_blank = False + max_length = None + min_length = None def __init__(self, **kwargs): - self.allow_blank = kwargs.pop('allow_blank', False) - max_length = kwargs.pop('max_length', None) - min_length = kwargs.pop('min_length', None) + self.allow_blank = kwargs.pop('allow_blank', self.allow_blank) + self.max_length = kwargs.pop('max_length', self.max_length) + self.min_length = kwargs.pop('min_length', self.min_length) super(CharField, self).__init__(**kwargs) - if max_length is not None: - message = self.error_messages['max_length'].format(max_length=max_length) - self.validators.append(MaxLengthValidator(max_length, message=message)) - if min_length is not None: - message = self.error_messages['min_length'].format(min_length=min_length) - self.validators.append(MinLengthValidator(min_length, message=message)) + if self.max_length is not None: + message = self.error_messages['max_length'].format(max_length=self.max_length) + self.validators.append(MaxLengthValidator(self.max_length, message=message)) + if self.min_length is not None: + message = self.error_messages['min_length'].format(min_length=self.min_length) + self.validators.append(MinLengthValidator(self.min_length, message=message)) def run_validation(self, data=empty): # Test for the empty string here so that it does not get validated, From 399b46ad20f51562b0be64ed1dc0d88d9c777337 Mon Sep 17 00:00:00 2001 From: Damien Nozay Date: Sun, 4 Jan 2015 12:09:53 -0800 Subject: [PATCH 03/13] Allow IntegerField to specify defaults directly on class --- rest_framework/fields.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index f1950cefa..bd746f39f 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -658,17 +658,20 @@ class IntegerField(Field): 'max_string_length': _('String value too large') } MAX_STRING_LENGTH = 1000 # Guard against malicious string inputs. + # allows subclasses to change defaults + max_value = None + min_value = None def __init__(self, **kwargs): - max_value = kwargs.pop('max_value', None) - min_value = kwargs.pop('min_value', None) + self.max_value = kwargs.pop('max_value', self.max_value) + self.min_value = kwargs.pop('min_value', self.min_value) super(IntegerField, self).__init__(**kwargs) - if max_value is not None: - message = self.error_messages['max_value'].format(max_value=max_value) - self.validators.append(MaxValueValidator(max_value, message=message)) - if min_value is not None: - message = self.error_messages['min_value'].format(min_value=min_value) - self.validators.append(MinValueValidator(min_value, message=message)) + if self.max_value is not None: + message = self.error_messages['max_value'].format(max_value=self.max_value) + self.validators.append(MaxValueValidator(self.max_value, message=message)) + if self.min_value is not None: + message = self.error_messages['min_value'].format(min_value=self.min_value) + self.validators.append(MinValueValidator(self.min_value, message=message)) def to_internal_value(self, data): if isinstance(data, six.text_type) and len(data) > self.MAX_STRING_LENGTH: From b0b4a469e9fd84f1f9c5256cd4bb1f1c6d0aaf55 Mon Sep 17 00:00:00 2001 From: Damien Nozay Date: Sun, 4 Jan 2015 12:12:39 -0800 Subject: [PATCH 04/13] Allow FloatField to specify defaults directly on class --- rest_framework/fields.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index bd746f39f..6e47ced89 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -695,17 +695,20 @@ class FloatField(Field): 'max_string_length': _('String value too large') } MAX_STRING_LENGTH = 1000 # Guard against malicious string inputs. + # allows subclasses to change defaults + max_value = None + min_value = None def __init__(self, **kwargs): - max_value = kwargs.pop('max_value', None) - min_value = kwargs.pop('min_value', None) + self.max_value = kwargs.pop('max_value', self.max_value) + self.min_value = kwargs.pop('min_value', self.min_value) super(FloatField, self).__init__(**kwargs) - if max_value is not None: - message = self.error_messages['max_value'].format(max_value=max_value) - self.validators.append(MaxValueValidator(max_value, message=message)) - if min_value is not None: - message = self.error_messages['min_value'].format(min_value=min_value) - self.validators.append(MinValueValidator(min_value, message=message)) + if self.max_value is not None: + message = self.error_messages['max_value'].format(max_value=self.max_value) + self.validators.append(MaxValueValidator(self.max_value, message=message)) + if self.min_value is not None: + message = self.error_messages['min_value'].format(min_value=self.min_value) + self.validators.append(MinValueValidator(self.min_value, message=message)) def to_internal_value(self, data): if isinstance(data, six.text_type) and len(data) > self.MAX_STRING_LENGTH: From 230a395ca4489e6ea489f99c932faa9f27c7c47b Mon Sep 17 00:00:00 2001 From: Damien Nozay Date: Sun, 4 Jan 2015 12:21:34 -0800 Subject: [PATCH 05/13] Allow DecimalField to specify defaults directly on class --- rest_framework/fields.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 6e47ced89..e7426314d 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -734,20 +734,27 @@ class DecimalField(Field): 'max_string_length': _('String value too large') } MAX_STRING_LENGTH = 1000 # Guard against malicious string inputs. + # allows subclasses to change defaults + max_value = None + min_value = None + # todo: max_digits = None + # todo: decimal_places = None coerce_to_string = api_settings.COERCE_DECIMAL_TO_STRING - def __init__(self, max_digits, decimal_places, coerce_to_string=None, max_value=None, min_value=None, **kwargs): + def __init__(self, max_digits, decimal_places, coerce_to_string=None, **kwargs): self.max_digits = max_digits self.decimal_places = decimal_places self.coerce_to_string = coerce_to_string if (coerce_to_string is not None) else self.coerce_to_string + self.max_value = kwargs.pop('max_value', self.max_value) + self.min_value = kwargs.pop('min_value', self.min_value) super(DecimalField, self).__init__(**kwargs) - if max_value is not None: - message = self.error_messages['max_value'].format(max_value=max_value) - self.validators.append(MaxValueValidator(max_value, message=message)) - if min_value is not None: - message = self.error_messages['min_value'].format(min_value=min_value) - self.validators.append(MinValueValidator(min_value, message=message)) + if self.max_value is not None: + message = self.error_messages['max_value'].format(max_value=self.max_value) + self.validators.append(MaxValueValidator(self.max_value, message=message)) + if self.min_value is not None: + message = self.error_messages['min_value'].format(min_value=self.min_value) + self.validators.append(MinValueValidator(self.min_value, message=message)) def to_internal_value(self, data): """ From be661824ac2154d7ec7c674df46682cd6e9b4da4 Mon Sep 17 00:00:00 2001 From: Damien Nozay Date: Sun, 4 Jan 2015 12:27:58 -0800 Subject: [PATCH 06/13] Allow DateTimeField to specify defaults directly on class --- rest_framework/fields.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index e7426314d..de767c08e 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -828,10 +828,10 @@ class DateTimeField(Field): input_formats = api_settings.DATETIME_INPUT_FORMATS default_timezone = timezone.get_default_timezone() if settings.USE_TZ else None - def __init__(self, format=empty, input_formats=None, default_timezone=None, *args, **kwargs): - self.format = format if format is not empty else self.format - self.input_formats = input_formats if input_formats is not None else self.input_formats - self.default_timezone = default_timezone if default_timezone is not None else self.default_timezone + def __init__(self, *args, **kwargs): + self.format = kwargs.pop('format', self.format) + self.default_timezone = kwargs.pop('default_timezone', self.default_timezone) + self.input_formats = kwargs.pop('input_formats', self.input_formats) super(DateTimeField, self).__init__(*args, **kwargs) def enforce_timezone(self, value): From ff28a6652df0a6508146086c9441fda15b8fbaff Mon Sep 17 00:00:00 2001 From: Damien Nozay Date: Sun, 4 Jan 2015 12:28:52 -0800 Subject: [PATCH 07/13] Allow DateField to specify defaults directly on class --- rest_framework/fields.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index de767c08e..46dfe406c 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -824,6 +824,7 @@ class DateTimeField(Field): 'invalid': _('Datetime has wrong format. Use one of these formats instead: {format}'), 'date': _('Expected a datetime but got a date.'), } + # allows subclasses to change defaults format = api_settings.DATETIME_FORMAT input_formats = api_settings.DATETIME_INPUT_FORMATS default_timezone = timezone.get_default_timezone() if settings.USE_TZ else None @@ -889,12 +890,13 @@ class DateField(Field): 'invalid': _('Date has wrong format. Use one of these formats instead: {format}'), 'datetime': _('Expected a date but got a datetime.'), } + # allows subclasses to change defaults format = api_settings.DATE_FORMAT input_formats = api_settings.DATE_INPUT_FORMATS - def __init__(self, format=empty, input_formats=None, *args, **kwargs): - self.format = format if format is not empty else self.format - self.input_formats = input_formats if input_formats is not None else self.input_formats + def __init__(self, *args, **kwargs): + self.format = kwargs.pop('format', self.format) + self.input_formats = kwargs.pop('input_formats', self.input_formats) super(DateField, self).__init__(*args, **kwargs) def to_internal_value(self, value): From 29f494f4473f00fd2975ad881adc468e58ced6c5 Mon Sep 17 00:00:00 2001 From: Damien Nozay Date: Sun, 4 Jan 2015 12:30:29 -0800 Subject: [PATCH 08/13] Allow TimeField to specify defaults directly on class --- rest_framework/fields.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 46dfe406c..611005d68 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -948,12 +948,13 @@ class TimeField(Field): default_error_messages = { 'invalid': _('Time has wrong format. Use one of these formats instead: {format}'), } + # allows subclasses to change defaults format = api_settings.TIME_FORMAT input_formats = api_settings.TIME_INPUT_FORMATS - def __init__(self, format=empty, input_formats=None, *args, **kwargs): - self.format = format if format is not empty else self.format - self.input_formats = input_formats if input_formats is not None else self.input_formats + def __init__(self, *args, **kwargs): + self.format = kwargs.pop('format', self.format) + self.input_formats = kwargs.pop('input_formats', self.input_formats) super(TimeField, self).__init__(*args, **kwargs) def to_internal_value(self, value): From 8471a4a2f73effaf964db42f6d622cc6d7b8d305 Mon Sep 17 00:00:00 2001 From: Damien Nozay Date: Sun, 4 Jan 2015 12:33:08 -0800 Subject: [PATCH 09/13] Allow FileField to specify defaults directly on class --- rest_framework/fields.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 611005d68..f9c88bc9c 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -1084,11 +1084,14 @@ class FileField(Field): 'empty': _("The submitted file is empty."), 'max_length': _('Ensure this filename has at most {max_length} characters (it has {length}).'), } + # allows subclasses to change defaults use_url = api_settings.UPLOADED_FILES_USE_URL + max_length = None + allow_empty_file = False def __init__(self, *args, **kwargs): - self.max_length = kwargs.pop('max_length', None) - self.allow_empty_file = kwargs.pop('allow_empty_file', False) + self.max_length = kwargs.pop('max_length', self.max_length) + self.allow_empty_file = kwargs.pop('allow_empty_file', self.allow_empty_file) self.use_url = kwargs.pop('use_url', self.use_url) super(FileField, self).__init__(*args, **kwargs) From 737f663d6976bc37b716411bc55479436d7c0016 Mon Sep 17 00:00:00 2001 From: Damien Nozay Date: Sun, 4 Jan 2015 12:39:10 -0800 Subject: [PATCH 10/13] Allow ModelField to specify defaults directly on class --- rest_framework/fields.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index f9c88bc9c..df021d724 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -1286,16 +1286,18 @@ class ModelField(Field): default_error_messages = { 'max_length': _('Ensure this field has no more than {max_length} characters.'), } + # allows subclasses to change defaults + max_length = None def __init__(self, model_field, **kwargs): self.model_field = model_field # The `max_length` option is supported by Django's base `Field` class, # so we'd better support it here. - max_length = kwargs.pop('max_length', None) + self.max_length = kwargs.pop('max_length', self.max_length) super(ModelField, self).__init__(**kwargs) - if max_length is not None: - message = self.error_messages['max_length'].format(max_length=max_length) - self.validators.append(MaxLengthValidator(max_length, message=message)) + if self.max_length is not None: + message = self.error_messages['max_length'].format(max_length=self.max_length) + self.validators.append(MaxLengthValidator(self.max_length, message=message)) def to_internal_value(self, data): rel = getattr(self.model_field, 'rel', None) From ae48939c33373d4ffca4851271bed7887e34b344 Mon Sep 17 00:00:00 2001 From: Damien Nozay Date: Sun, 4 Jan 2015 12:58:58 -0800 Subject: [PATCH 11/13] Allow ChoiceField to specify defaults directly on class --- rest_framework/fields.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index df021d724..1fc36e155 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -1005,8 +1005,18 @@ class ChoiceField(Field): default_error_messages = { 'invalid_choice': _('`{input}` is not a valid choice.') } + # allows subclasses to change defaults + allow_blank = False + choices = None + + def __init__(self, *args, **kwargs): + if args: + choices = args[0] + else: + choices = kwargs.pop('choices', self.choices) + # not available on class or as kwarg + assert choices is not None, 'need to specify `choices`.' - def __init__(self, choices, **kwargs): # Allow either single or paired choices style: # choices = [1, 2, 3] # choices = [(1, 'First'), (2, 'Second'), (3, 'Third')] @@ -1026,7 +1036,7 @@ class ChoiceField(Field): (six.text_type(key), key) for key in self.choices.keys() ]) - self.allow_blank = kwargs.pop('allow_blank', False) + self.allow_blank = kwargs.pop('allow_blank', self.allow_blank) super(ChoiceField, self).__init__(**kwargs) From 4881126544e6520b9db36bb14bee34661a3ff95a Mon Sep 17 00:00:00 2001 From: Damien Nozay Date: Mon, 5 Jan 2015 09:49:33 -0800 Subject: [PATCH 12/13] remove '# allows subclasses to change defaults' comments --- rest_framework/fields.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 1fc36e155..2a2c95f51 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -563,7 +563,6 @@ class CharField(Field): 'min_length': _('Ensure this field has at least {min_length} characters.') } initial = '' - # allows subclasses to change defaults allow_blank = False max_length = None min_length = None @@ -658,7 +657,6 @@ class IntegerField(Field): 'max_string_length': _('String value too large') } MAX_STRING_LENGTH = 1000 # Guard against malicious string inputs. - # allows subclasses to change defaults max_value = None min_value = None @@ -695,7 +693,6 @@ class FloatField(Field): 'max_string_length': _('String value too large') } MAX_STRING_LENGTH = 1000 # Guard against malicious string inputs. - # allows subclasses to change defaults max_value = None min_value = None @@ -734,7 +731,6 @@ class DecimalField(Field): 'max_string_length': _('String value too large') } MAX_STRING_LENGTH = 1000 # Guard against malicious string inputs. - # allows subclasses to change defaults max_value = None min_value = None # todo: max_digits = None @@ -824,7 +820,6 @@ class DateTimeField(Field): 'invalid': _('Datetime has wrong format. Use one of these formats instead: {format}'), 'date': _('Expected a datetime but got a date.'), } - # allows subclasses to change defaults format = api_settings.DATETIME_FORMAT input_formats = api_settings.DATETIME_INPUT_FORMATS default_timezone = timezone.get_default_timezone() if settings.USE_TZ else None @@ -890,7 +885,6 @@ class DateField(Field): 'invalid': _('Date has wrong format. Use one of these formats instead: {format}'), 'datetime': _('Expected a date but got a datetime.'), } - # allows subclasses to change defaults format = api_settings.DATE_FORMAT input_formats = api_settings.DATE_INPUT_FORMATS @@ -948,7 +942,6 @@ class TimeField(Field): default_error_messages = { 'invalid': _('Time has wrong format. Use one of these formats instead: {format}'), } - # allows subclasses to change defaults format = api_settings.TIME_FORMAT input_formats = api_settings.TIME_INPUT_FORMATS @@ -1005,7 +998,6 @@ class ChoiceField(Field): default_error_messages = { 'invalid_choice': _('`{input}` is not a valid choice.') } - # allows subclasses to change defaults allow_blank = False choices = None @@ -1094,7 +1086,6 @@ class FileField(Field): 'empty': _("The submitted file is empty."), 'max_length': _('Ensure this filename has at most {max_length} characters (it has {length}).'), } - # allows subclasses to change defaults use_url = api_settings.UPLOADED_FILES_USE_URL max_length = None allow_empty_file = False @@ -1296,7 +1287,6 @@ class ModelField(Field): default_error_messages = { 'max_length': _('Ensure this field has no more than {max_length} characters.'), } - # allows subclasses to change defaults max_length = None def __init__(self, model_field, **kwargs): From 5a899aea1ad7c38dbd1f53cb4238e948cfbf2b8b Mon Sep 17 00:00:00 2001 From: Damien Nozay Date: Mon, 5 Jan 2015 10:14:53 -0800 Subject: [PATCH 13/13] rework Field.__init__ to use either kwargs / class attrs. Precedence is given to kwargs over class attributes. You can create a subclass of a Field for 99% of your use cases while still needing a one-off instance where you need to change an attribute; hence kwargs is still useful. Other important point is that kwargs is the current de-facto standard for declaring fields in a serializer. --- rest_framework/fields.py | 42 ++++++++++++++++++++++++++-------------- 1 file changed, 27 insertions(+), 15 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 2a2c95f51..afde52ae2 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -154,28 +154,40 @@ class Field(object): } default_validators = [] default_empty_html = empty - initial = None - # allows to subclass a Field and have defaults on the class. + # allows subclasses to change defaults read_only = False write_only = False + required = None + default = empty + initial = None + source = None + label = None + help_text = None + style = None + error_messages = None + validators = None allow_null = False - def __init__(self, read_only=None, write_only=None, - required=None, default=empty, initial=empty, source=None, - label=None, help_text=None, style=None, - error_messages=None, validators=None, allow_null=None): + def __init__(self, **kwargs): self._creation_counter = Field._creation_counter Field._creation_counter += 1 - if read_only is None: - read_only = self.read_only - - if write_only is None: - write_only = self.write_only - - if allow_null is None: - allow_null = self.allow_null + # precedence is given to kwargs over class attributes. + # you can create a subclass of a Field for maximum reuse while still + # needing a one-off instance where you need to change an attribute. + read_only = kwargs.pop('read_only', self.read_only) + write_only = kwargs.pop('write_only', self.write_only) + required = kwargs.pop('required', self.required) + default = kwargs.pop('default', self.default) + initial = kwargs.pop('initial', self.initial) + source = kwargs.pop('source', self.source) + label = kwargs.pop('label', self.label) + help_text = kwargs.pop('help_text', self.help_text) + style = kwargs.pop('style', self.style) + error_messages = kwargs.pop('error_messages', self.error_messages) + validators = kwargs.pop('validators', self.validators) + allow_null = kwargs.pop('allow_null', self.allow_null) # If `required` is unset, then use `True` unless a default is provided. if required is None: @@ -192,7 +204,7 @@ class Field(object): self.required = required self.default = default self.source = source - self.initial = self.initial if (initial is empty) else initial + self.initial = initial self.label = label self.help_text = help_text self.style = {} if style is None else style