From 48dd1e63bbb93479666208535a56f8c7c4aeab3a Mon Sep 17 00:00:00 2001 From: Andrew Godwin Date: Fri, 28 Jun 2013 17:26:05 +0100 Subject: [PATCH] Ported over Field.deconstruct() from my schema alteration branch. This is to help other ongoing branches which would benefit from this functionality. --- django/db/models/fields/__init__.py | 188 +++++++++++++++++- django/db/models/fields/files.py | 19 ++ django/db/models/fields/related.py | 42 ++++ tests/field_deconstruction/__init__.py | 0 tests/field_deconstruction/models.py | 0 tests/field_deconstruction/tests.py | 253 +++++++++++++++++++++++++ 6 files changed, 501 insertions(+), 1 deletion(-) create mode 100644 tests/field_deconstruction/__init__.py create mode 100644 tests/field_deconstruction/models.py create mode 100644 tests/field_deconstruction/tests.py diff --git a/django/db/models/fields/__init__.py b/django/db/models/fields/__init__.py index c0dce2e58e..a12b150cf6 100644 --- a/django/db/models/fields/__init__.py +++ b/django/db/models/fields/__init__.py @@ -99,7 +99,8 @@ class Field(object): db_tablespace=None, auto_created=False, validators=[], error_messages=None): self.name = name - self.verbose_name = verbose_name + self.verbose_name = verbose_name # May be set by set_attributes_from_name + self._verbose_name = verbose_name # Store original for deconstruction self.primary_key = primary_key self.max_length, self._unique = max_length, unique self.blank, self.null = blank, null @@ -128,14 +129,99 @@ class Field(object): self.creation_counter = Field.creation_counter Field.creation_counter += 1 + self._validators = validators # Store for deconstruction later self.validators = self.default_validators + validators messages = {} for c in reversed(self.__class__.__mro__): messages.update(getattr(c, 'default_error_messages', {})) messages.update(error_messages or {}) + self._error_messages = error_messages # Store for deconstruction later self.error_messages = messages + def deconstruct(self): + """ + Returns enough information to recreate the field as a 4-tuple: + + * The name of the field on the model, if contribute_to_class has been run + * The import path of the field, including the class: django.db.models.IntegerField + This should be the most portable version, so less specific may be better. + * A list of positional arguments + * A dict of keyword arguments + + Note that the positional or keyword arguments must contain values of the + following types (including inner values of collection types): + + * None, bool, str, unicode, int, long, float, complex, set, frozenset, list, tuple, dict + * UUID + * datetime.datetime (naive), datetime.date + * top-level classes, top-level functions - will be referenced by their full import path + * Storage instances - these have their own deconstruct() method + + This is because the values here must be serialised into a text format + (possibly new Python code, possibly JSON) and these are the only types + with encoding handlers defined. + + There's no need to return the exact way the field was instantiated this time, + just ensure that the resulting field is the same - prefer keyword arguments + over positional ones, and omit parameters with their default values. + """ + # Short-form way of fetching all the default parameters + keywords = {} + possibles = { + "verbose_name": None, + "primary_key": False, + "max_length": None, + "unique": False, + "blank": False, + "null": False, + "db_index": False, + "default": NOT_PROVIDED, + "editable": True, + "serialize": True, + "unique_for_date": None, + "unique_for_month": None, + "unique_for_year": None, + "choices": [], + "help_text": '', + "db_column": None, + "db_tablespace": settings.DEFAULT_INDEX_TABLESPACE, + "auto_created": False, + "validators": [], + "error_messages": None, + } + attr_overrides = { + "unique": "_unique", + "choices": "_choices", + "error_messages": "_error_messages", + "validators": "_validators", + "verbose_name": "_verbose_name", + } + equals_comparison = set(["choices", "validators", "db_tablespace"]) + for name, default in possibles.items(): + value = getattr(self, attr_overrides.get(name, name)) + if name in equals_comparison: + if value != default: + keywords[name] = value + else: + if value is not default: + keywords[name] = value + # Work out path - we shorten it for known Django core fields + path = "%s.%s" % (self.__class__.__module__, self.__class__.__name__) + if path.startswith("django.db.models.fields.related"): + path = path.replace("django.db.models.fields.related", "django.db.models") + if path.startswith("django.db.models.fields.files"): + path = path.replace("django.db.models.fields.files", "django.db.models") + if path.startswith("django.db.models.fields"): + path = path.replace("django.db.models.fields", "django.db.models") + # Return basic info - other fields should override this. + return ( + self.name, + path, + [], + keywords, + ) + def __eq__(self, other): # Needed for @total_ordering if isinstance(other, Field): @@ -566,6 +652,7 @@ class Field(object): return '<%s: %s>' % (path, name) return '<%s>' % path + class AutoField(Field): description = _("Integer") @@ -580,6 +667,12 @@ class AutoField(Field): kwargs['blank'] = True Field.__init__(self, *args, **kwargs) + def deconstruct(self): + name, path, args, kwargs = super(AutoField, self).deconstruct() + del kwargs['blank'] + kwargs['primary_key'] = True + return name, path, args, kwargs + def get_internal_type(self): return "AutoField" @@ -630,6 +723,11 @@ class BooleanField(Field): kwargs['blank'] = True Field.__init__(self, *args, **kwargs) + def deconstruct(self): + name, path, args, kwargs = super(BooleanField, self).deconstruct() + del kwargs['blank'] + return name, path, args, kwargs + def get_internal_type(self): return "BooleanField" @@ -733,6 +831,18 @@ class DateField(Field): kwargs['blank'] = True Field.__init__(self, verbose_name, name, **kwargs) + def deconstruct(self): + name, path, args, kwargs = super(DateField, self).deconstruct() + if self.auto_now: + kwargs['auto_now'] = True + del kwargs['editable'] + del kwargs['blank'] + if self.auto_now_add: + kwargs['auto_now_add'] = True + del kwargs['editable'] + del kwargs['blank'] + return name, path, args, kwargs + def get_internal_type(self): return "DateField" @@ -927,6 +1037,14 @@ class DecimalField(Field): self.max_digits, self.decimal_places = max_digits, decimal_places Field.__init__(self, verbose_name, name, **kwargs) + def deconstruct(self): + name, path, args, kwargs = super(DecimalField, self).deconstruct() + if self.max_digits: + kwargs['max_digits'] = self.max_digits + if self.decimal_places: + kwargs['decimal_places'] = self.decimal_places + return name, path, args, kwargs + def get_internal_type(self): return "DecimalField" @@ -989,6 +1107,12 @@ class EmailField(CharField): kwargs['max_length'] = kwargs.get('max_length', 75) CharField.__init__(self, *args, **kwargs) + def deconstruct(self): + name, path, args, kwargs = super(EmailField, self).deconstruct() + # We do not exclude max_length if it matches default as we want to change + # the default in future. + return name, path, args, kwargs + def formfield(self, **kwargs): # As with CharField, this will cause email validation to be performed # twice. @@ -1008,6 +1132,22 @@ class FilePathField(Field): kwargs['max_length'] = kwargs.get('max_length', 100) Field.__init__(self, verbose_name, name, **kwargs) + def deconstruct(self): + name, path, args, kwargs = super(FilePathField, self).deconstruct() + if self.path != '': + kwargs['path'] = self.path + if self.match is not None: + kwargs['match'] = self.match + if self.recursive is not False: + kwargs['recursive'] = self.recursive + if self.allow_files is not True: + kwargs['allow_files'] = self.allow_files + if self.allow_folders is not False: + kwargs['allow_folders'] = self.allow_folders + if kwargs.get("max_length", None) == 100: + del kwargs["max_length"] + return name, path, args, kwargs + def formfield(self, **kwargs): defaults = { 'path': self.path, @@ -1115,6 +1255,11 @@ class IPAddressField(Field): kwargs['max_length'] = 15 Field.__init__(self, *args, **kwargs) + def deconstruct(self): + name, path, args, kwargs = super(IPAddressField, self).deconstruct() + del kwargs['max_length'] + return name, path, args, kwargs + def get_internal_type(self): return "IPAddressField" @@ -1131,12 +1276,23 @@ class GenericIPAddressField(Field): def __init__(self, verbose_name=None, name=None, protocol='both', unpack_ipv4=False, *args, **kwargs): self.unpack_ipv4 = unpack_ipv4 + self.protocol = protocol self.default_validators, invalid_error_message = \ validators.ip_address_validators(protocol, unpack_ipv4) self.default_error_messages['invalid'] = invalid_error_message kwargs['max_length'] = 39 Field.__init__(self, verbose_name, name, *args, **kwargs) + def deconstruct(self): + name, path, args, kwargs = super(GenericIPAddressField, self).deconstruct() + if self.unpack_ipv4 is not False: + kwargs['unpack_ipv4'] = self.unpack_ipv4 + if self.protocol != "both": + kwargs['protocol'] = self.protocol + if kwargs.get("max_length", None) == 39: + del kwargs['max_length'] + return name, path, args, kwargs + def get_internal_type(self): return "GenericIPAddressField" @@ -1177,6 +1333,12 @@ class NullBooleanField(Field): kwargs['blank'] = True Field.__init__(self, *args, **kwargs) + def deconstruct(self): + name, path, args, kwargs = super(NullBooleanField, self).deconstruct() + del kwargs['null'] + del kwargs['blank'] + return name, path, args, kwargs + def get_internal_type(self): return "NullBooleanField" @@ -1254,6 +1416,16 @@ class SlugField(CharField): kwargs['db_index'] = True super(SlugField, self).__init__(*args, **kwargs) + def deconstruct(self): + name, path, args, kwargs = super(SlugField, self).deconstruct() + if kwargs.get("max_length", None) == 50: + del kwargs['max_length'] + if self.db_index is False: + kwargs['db_index'] = False + else: + del kwargs['db_index'] + return name, path, args, kwargs + def get_internal_type(self): return "SlugField" @@ -1302,6 +1474,14 @@ class TimeField(Field): kwargs['blank'] = True Field.__init__(self, verbose_name, name, **kwargs) + def deconstruct(self): + name, path, args, kwargs = super(TimeField, self).deconstruct() + if self.auto_now is not False: + kwargs["auto_now"] = self.auto_now + if self.auto_now_add is not False: + kwargs["auto_now_add"] = self.auto_now_add + return name, path, args, kwargs + def get_internal_type(self): return "TimeField" @@ -1367,6 +1547,12 @@ class URLField(CharField): kwargs['max_length'] = kwargs.get('max_length', 200) CharField.__init__(self, verbose_name, name, **kwargs) + def deconstruct(self): + name, path, args, kwargs = super(URLField, self).deconstruct() + if kwargs.get("max_length", None) == 200: + del kwargs['max_length'] + return name, path, args, kwargs + def formfield(self, **kwargs): # As with CharField, this will cause URL validation to be performed # twice. diff --git a/django/db/models/fields/files.py b/django/db/models/fields/files.py index e631f177e9..0a913e908b 100644 --- a/django/db/models/fields/files.py +++ b/django/db/models/fields/files.py @@ -227,6 +227,17 @@ class FileField(Field): kwargs['max_length'] = kwargs.get('max_length', 100) super(FileField, self).__init__(verbose_name, name, **kwargs) + def deconstruct(self): + name, path, args, kwargs = super(FileField, self).deconstruct() + if kwargs.get("max_length", None) != 100: + kwargs["max_length"] = 100 + else: + del kwargs["max_length"] + kwargs['upload_to'] = self.upload_to + if self.storage is not default_storage: + kwargs['storage'] = self.storage + return name, path, args, kwargs + def get_internal_type(self): return "FileField" @@ -326,6 +337,14 @@ class ImageField(FileField): self.width_field, self.height_field = width_field, height_field super(ImageField, self).__init__(verbose_name, name, **kwargs) + def deconstruct(self): + name, path, args, kwargs = super(ImageField, self).deconstruct() + if self.width_field: + kwargs['width_field'] = self.width_field + if self.height_field: + kwargs['height_field'] = self.height_field + return name, path, args, kwargs + def contribute_to_class(self, cls, name): super(ImageField, self).contribute_to_class(cls, name) # Attach update_dimension_fields so that dimension fields declared diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py index 0367d24fe8..c708bff49f 100644 --- a/django/db/models/fields/related.py +++ b/django/db/models/fields/related.py @@ -1151,6 +1151,27 @@ class ForeignKey(ForeignObject): ) super(ForeignKey, self).__init__(to, ['self'], [to_field], **kwargs) + def deconstruct(self): + name, path, args, kwargs = super(ForeignKey, self).deconstruct() + # Handle the simpler arguments + if self.db_index: + del kwargs['db_index'] + else: + kwargs['db_index'] = False + if self.db_constraint is not True: + kwargs['db_constraint'] = self.db_constraint + if self.rel.on_delete is not CASCADE: + kwargs['on_delete'] = self.rel.on_delete + # Rel needs more work. + rel = self.rel + if self.rel.field_name: + kwargs['to_field'] = self.rel.field_name + if isinstance(self.rel.to, basestring): + kwargs['to'] = self.rel.to + else: + kwargs['to'] = "%s.%s" % (self.rel.to._meta.app_label, self.rel.to._meta.object_name) + return name, path, args, kwargs + @property def related_field(self): return self.foreign_related_fields[0] @@ -1268,6 +1289,12 @@ class OneToOneField(ForeignKey): kwargs['unique'] = True super(OneToOneField, self).__init__(to, to_field, OneToOneRel, **kwargs) + def deconstruct(self): + name, path, args, kwargs = super(OneToOneField, self).deconstruct() + if "unique" in kwargs: + del kwargs['unique'] + return name, path, args, kwargs + def contribute_to_related_class(self, cls, related): setattr(cls, related.get_accessor_name(), SingleRelatedObjectDescriptor(related)) @@ -1357,6 +1384,21 @@ class ManyToManyField(RelatedField): super(ManyToManyField, self).__init__(**kwargs) + def deconstruct(self): + name, path, args, kwargs = super(ManyToManyField, self).deconstruct() + # Handle the simpler arguments + if self.rel.db_constraint is not True: + kwargs['db_constraint'] = self.db_constraint + if "help_text" in kwargs: + del kwargs['help_text'] + # Rel needs more work. + rel = self.rel + if isinstance(self.rel.to, basestring): + kwargs['to'] = self.rel.to + else: + kwargs['to'] = "%s.%s" % (self.rel.to._meta.app_label, self.rel.to._meta.object_name) + return name, path, args, kwargs + def _get_path_info(self, direct=False): """ Called by both direct an indirect m2m traversal. diff --git a/tests/field_deconstruction/__init__.py b/tests/field_deconstruction/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/field_deconstruction/models.py b/tests/field_deconstruction/models.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/field_deconstruction/tests.py b/tests/field_deconstruction/tests.py new file mode 100644 index 0000000000..4ccf4d048c --- /dev/null +++ b/tests/field_deconstruction/tests.py @@ -0,0 +1,253 @@ +from django.test import TestCase +from django.db import models + + +class FieldDeconstructionTests(TestCase): + """ + Tests the deconstruct() method on all core fields. + """ + + def test_name(self): + """ + Tests the outputting of the correct name if assigned one. + """ + # First try using a "normal" field + field = models.CharField(max_length=65) + name, path, args, kwargs = field.deconstruct() + self.assertEqual(name, None) + field.set_attributes_from_name("is_awesome_test") + name, path, args, kwargs = field.deconstruct() + self.assertEqual(name, "is_awesome_test") + # Now try with a ForeignKey + field = models.ForeignKey("some_fake.ModelName") + name, path, args, kwargs = field.deconstruct() + self.assertEqual(name, None) + field.set_attributes_from_name("author") + name, path, args, kwargs = field.deconstruct() + self.assertEqual(name, "author") + + def test_auto_field(self): + field = models.AutoField(primary_key=True) + field.set_attributes_from_name("id") + name, path, args, kwargs = field.deconstruct() + self.assertEqual(path, "django.db.models.AutoField") + self.assertEqual(args, []) + self.assertEqual(kwargs, {"primary_key": True}) + + def test_big_integer_field(self): + field = models.BigIntegerField() + name, path, args, kwargs = field.deconstruct() + self.assertEqual(path, "django.db.models.BigIntegerField") + self.assertEqual(args, []) + self.assertEqual(kwargs, {}) + + def test_boolean_field(self): + field = models.BooleanField() + name, path, args, kwargs = field.deconstruct() + self.assertEqual(path, "django.db.models.BooleanField") + self.assertEqual(args, []) + self.assertEqual(kwargs, {}) + field = models.BooleanField(default=True) + name, path, args, kwargs = field.deconstruct() + self.assertEqual(path, "django.db.models.BooleanField") + self.assertEqual(args, []) + self.assertEqual(kwargs, {"default": True}) + + def test_char_field(self): + field = models.CharField(max_length=65) + name, path, args, kwargs = field.deconstruct() + self.assertEqual(path, "django.db.models.CharField") + self.assertEqual(args, []) + self.assertEqual(kwargs, {"max_length": 65}) + field = models.CharField(max_length=65, null=True, blank=True) + name, path, args, kwargs = field.deconstruct() + self.assertEqual(path, "django.db.models.CharField") + self.assertEqual(args, []) + self.assertEqual(kwargs, {"max_length": 65, "null": True, "blank": True}) + + def test_csi_field(self): + field = models.CommaSeparatedIntegerField(max_length=100) + name, path, args, kwargs = field.deconstruct() + self.assertEqual(path, "django.db.models.CommaSeparatedIntegerField") + self.assertEqual(args, []) + self.assertEqual(kwargs, {"max_length": 100}) + + def test_date_field(self): + field = models.DateField() + name, path, args, kwargs = field.deconstruct() + self.assertEqual(path, "django.db.models.DateField") + self.assertEqual(args, []) + self.assertEqual(kwargs, {}) + field = models.DateField(auto_now=True) + name, path, args, kwargs = field.deconstruct() + self.assertEqual(path, "django.db.models.DateField") + self.assertEqual(args, []) + self.assertEqual(kwargs, {"auto_now": True}) + + def test_datetime_field(self): + field = models.DateTimeField() + name, path, args, kwargs = field.deconstruct() + self.assertEqual(path, "django.db.models.DateTimeField") + self.assertEqual(args, []) + self.assertEqual(kwargs, {}) + field = models.DateTimeField(auto_now_add=True) + name, path, args, kwargs = field.deconstruct() + self.assertEqual(path, "django.db.models.DateTimeField") + self.assertEqual(args, []) + self.assertEqual(kwargs, {"auto_now_add": True}) + + def test_decimal_field(self): + field = models.DecimalField(max_digits=5, decimal_places=2) + name, path, args, kwargs = field.deconstruct() + self.assertEqual(path, "django.db.models.DecimalField") + self.assertEqual(args, []) + self.assertEqual(kwargs, {"max_digits": 5, "decimal_places": 2}) + + def test_email_field(self): + field = models.EmailField() + name, path, args, kwargs = field.deconstruct() + self.assertEqual(path, "django.db.models.EmailField") + self.assertEqual(args, []) + self.assertEqual(kwargs, {"max_length": 75}) + field = models.EmailField(max_length=255) + name, path, args, kwargs = field.deconstruct() + self.assertEqual(path, "django.db.models.EmailField") + self.assertEqual(args, []) + self.assertEqual(kwargs, {"max_length": 255}) + + def test_file_field(self): + field = models.FileField(upload_to="foo/bar") + name, path, args, kwargs = field.deconstruct() + self.assertEqual(path, "django.db.models.FileField") + self.assertEqual(args, []) + self.assertEqual(kwargs, {"upload_to": "foo/bar"}) + + def test_file_path_field(self): + field = models.FilePathField(match=".*\.txt$") + name, path, args, kwargs = field.deconstruct() + self.assertEqual(path, "django.db.models.FilePathField") + self.assertEqual(args, []) + self.assertEqual(kwargs, {"match": ".*\.txt$"}) + field = models.FilePathField(recursive=True, allow_folders=True) + name, path, args, kwargs = field.deconstruct() + self.assertEqual(path, "django.db.models.FilePathField") + self.assertEqual(args, []) + self.assertEqual(kwargs, {"recursive": True, "allow_folders": True}) + + def test_float_field(self): + field = models.FloatField() + name, path, args, kwargs = field.deconstruct() + self.assertEqual(path, "django.db.models.FloatField") + self.assertEqual(args, []) + self.assertEqual(kwargs, {}) + + def test_foreign_key(self): + field = models.ForeignKey("auth.User") + name, path, args, kwargs = field.deconstruct() + self.assertEqual(path, "django.db.models.ForeignKey") + self.assertEqual(args, []) + self.assertEqual(kwargs, {"to": "auth.User"}) + field = models.ForeignKey("something.Else") + name, path, args, kwargs = field.deconstruct() + self.assertEqual(path, "django.db.models.ForeignKey") + self.assertEqual(args, []) + self.assertEqual(kwargs, {"to": "something.Else"}) + field = models.ForeignKey("auth.User", on_delete=models.SET_NULL) + name, path, args, kwargs = field.deconstruct() + self.assertEqual(path, "django.db.models.ForeignKey") + self.assertEqual(args, []) + self.assertEqual(kwargs, {"to": "auth.User", "on_delete": models.SET_NULL}) + + def test_image_field(self): + field = models.ImageField(upload_to="foo/barness", width_field="width", height_field="height") + name, path, args, kwargs = field.deconstruct() + self.assertEqual(path, "django.db.models.ImageField") + self.assertEqual(args, []) + self.assertEqual(kwargs, {"upload_to": "foo/barness", "width_field": "width", "height_field": "height"}) + + def test_integer_field(self): + field = models.IntegerField() + name, path, args, kwargs = field.deconstruct() + self.assertEqual(path, "django.db.models.IntegerField") + self.assertEqual(args, []) + self.assertEqual(kwargs, {}) + + def test_ip_address_field(self): + field = models.IPAddressField() + name, path, args, kwargs = field.deconstruct() + self.assertEqual(path, "django.db.models.IPAddressField") + self.assertEqual(args, []) + self.assertEqual(kwargs, {}) + + def test_generic_ip_address_field(self): + field = models.GenericIPAddressField() + name, path, args, kwargs = field.deconstruct() + self.assertEqual(path, "django.db.models.GenericIPAddressField") + self.assertEqual(args, []) + self.assertEqual(kwargs, {}) + field = models.GenericIPAddressField(protocol="IPv6") + name, path, args, kwargs = field.deconstruct() + self.assertEqual(path, "django.db.models.GenericIPAddressField") + self.assertEqual(args, []) + self.assertEqual(kwargs, {"protocol": "IPv6"}) + + def test_many_to_many_field(self): + field = models.ManyToManyField("auth.User") + name, path, args, kwargs = field.deconstruct() + self.assertEqual(path, "django.db.models.ManyToManyField") + self.assertEqual(args, []) + self.assertEqual(kwargs, {"to": "auth.User"}) + + def test_null_boolean_field(self): + field = models.NullBooleanField() + name, path, args, kwargs = field.deconstruct() + self.assertEqual(path, "django.db.models.NullBooleanField") + self.assertEqual(args, []) + self.assertEqual(kwargs, {}) + + def test_positive_integer_field(self): + field = models.PositiveIntegerField() + name, path, args, kwargs = field.deconstruct() + self.assertEqual(path, "django.db.models.PositiveIntegerField") + self.assertEqual(args, []) + self.assertEqual(kwargs, {}) + + def test_positive_small_integer_field(self): + field = models.PositiveSmallIntegerField() + name, path, args, kwargs = field.deconstruct() + self.assertEqual(path, "django.db.models.PositiveSmallIntegerField") + self.assertEqual(args, []) + self.assertEqual(kwargs, {}) + + def test_slug_field(self): + field = models.SlugField() + name, path, args, kwargs = field.deconstruct() + self.assertEqual(path, "django.db.models.SlugField") + self.assertEqual(args, []) + self.assertEqual(kwargs, {}) + field = models.SlugField(db_index=False) + name, path, args, kwargs = field.deconstruct() + self.assertEqual(path, "django.db.models.SlugField") + self.assertEqual(args, []) + self.assertEqual(kwargs, {"db_index": False}) + + def test_small_integer_field(self): + field = models.SmallIntegerField() + name, path, args, kwargs = field.deconstruct() + self.assertEqual(path, "django.db.models.SmallIntegerField") + self.assertEqual(args, []) + self.assertEqual(kwargs, {}) + + def test_text_field(self): + field = models.TextField() + name, path, args, kwargs = field.deconstruct() + self.assertEqual(path, "django.db.models.TextField") + self.assertEqual(args, []) + self.assertEqual(kwargs, {}) + + def test_url_field(self): + field = models.URLField() + name, path, args, kwargs = field.deconstruct() + self.assertEqual(path, "django.db.models.URLField") + self.assertEqual(args, []) + self.assertEqual(kwargs, {})