Index: django/forms/models.py
===================================================================
--- django/forms/models.py	(revision 8273)
+++ django/forms/models.py	(working copy)
@@ -23,7 +23,7 @@
 __all__ = (
     'ModelForm', 'BaseModelForm', 'model_to_dict', 'fields_for_model',
     'save_instance', 'form_for_fields', 'ModelChoiceField',
-    'ModelMultipleChoiceField',
+    'ModelMultipleChoiceField', 'ModelFormSet', 'InlineFormSet'
 )
 
 
@@ -249,8 +249,8 @@
                 # This is an extra field that's not on the ModelForm, ignore it
                 continue
             if not isinstance(f, ModelField):
-                # This is an extra field that happens to have a name that matches, 
-                # for example, a related object accessor for this model.  So 
+                # This is an extra field that happens to have a name that matches,
+                # for example, a related object accessor for this model.  So
                 # get_field_by_name found it, but it is not a Field so do not proceed
                 # to use it as if it were.
                 continue
@@ -456,7 +456,45 @@
     FormSet.model = model
     return FormSet
 
+class ModelFormSetMetaclass(type):
+    def __new__(cls, name, bases, attrs):
+        try:
+            parents = [b for b in bases if issubclass(b, ModelFormSet)]
+        except NameError:
+            # We are defining ModelFormSet itself.
+            parents = None
+        new_class = super(ModelFormSetMetaclass, cls).__new__(cls, name, bases,
+                            attrs)
+        if not parents:
+            return new_class
 
+        if 'can_order' not in attrs:
+            new_class.can_order = False
+        if 'can_delete' not in attrs:
+            new_class.can_delete = False
+        if 'max_num' not in attrs:
+            new_class.max_num = 0
+        if 'extra' not in attrs:
+            new_class.extra = 1
+
+        if 'form' not in attrs:
+            raise Exception(u'Error in definition of ModelFormSet subclass %s. You need to define a form.' % name)
+
+        if 'model' not in attrs:
+            raise Exception(u'Error in definition of ModelFormSet subclass %s. You need to define a model.' % name)
+        model = attrs.get('model')
+
+        new_class.model = model
+
+        return new_class
+
+class ModelFormSet(BaseModelFormSet):
+    """
+    ModelFormSet subclasses need these attributes: form and model
+    """
+    __metaclass__ = ModelFormSetMetaclass
+
+
 # InlineFormSets #############################################################
 
 class BaseInlineFormSet(BaseModelFormSet):
@@ -578,7 +616,59 @@
     FormSet.fk = fk
     return FormSet
 
+class InlineFormSetMetaclass(type):
+    def __new__(cls, name, bases, attrs):
+        try:
+            parents = [b for b in bases if issubclass(b, InlineFormSet)]
+        except NameError:
+            # We are defining InlineFormSet itself.
+            parents = None
+        new_class = super(InlineFormSetMetaclass, cls).__new__(cls, name, bases,
+                            attrs)
+        if not parents:
+            return new_class
 
+        if 'can_order' not in attrs:
+            new_class.can_order = False
+        if 'can_delete' not in attrs:
+            new_class.can_delete = False
+        if 'max_num' not in attrs:
+            new_class.max_num = 0
+        if 'extra' not in attrs:
+            new_class.extra = 3
+
+        if 'form' not in attrs:
+            raise Exception(u'Error in definition of InlineFormSet subclass %s. You need to define a form.' % name)
+
+        if 'model' not in attrs:
+            raise Exception(u'Error in definition of InlineFormSet subclass %s. You need to define a model.' % name)
+        model = attrs.get('model')
+
+
+        if 'parent' not in attrs:
+            raise Exception(u'Error in definition of InlineFormSet subclass %s. You need to define a parent attribute.' % name)
+        parent = attrs.pop('parent')
+
+        try:
+            fk = (i for i in model._meta.fields if i.name == parent).next()
+        except:
+            raise Exception(u'Error in definition of InlineFormSet subclass %s. The parent attribute of the inlineformset is not a valid field.' % name)
+
+        from django.db.models import ForeignKey
+        if not isinstance(fk, ForeignKey):
+            raise Exception(u'Error in definition of InlineFormSet subclass %s. The parent attribute is not a ForeignKey.' % name)
+
+        new_class.fk = fk
+
+        return new_class
+
+class InlineFormSet(BaseInlineFormSet):
+    """
+    ModelFormSet subclasses need these attributes: form and model and parent
+    """
+    __metaclass__ = InlineFormSetMetaclass
+
+
 # Fields #####################################################################
 
 class InlineForeignKeyHiddenInput(HiddenInput):
Index: tests/modeltests/model_formsets/models.py
===================================================================
--- tests/modeltests/model_formsets/models.py	(revision 8273)
+++ tests/modeltests/model_formsets/models.py	(working copy)
@@ -27,7 +27,7 @@
 
     def __unicode__(self):
         return self.title
-    
+
 class BookWithCustomPK(models.Model):
     my_pk = models.DecimalField(max_digits=5, decimal_places=0, primary_key=True)
     author = models.ForeignKey(Author)
@@ -35,13 +35,13 @@
 
     def __unicode__(self):
         return u'%s: %s' % (self.my_pk, self.title)
-    
+
 class AlternateBook(Book):
     notes = models.CharField(max_length=100)
-    
+
     def __unicode__(self):
         return u'%s - %s' % (self.title, self.notes)
-    
+
 class AuthorMeeting(models.Model):
     name = models.CharField(max_length=100)
     authors = models.ManyToManyField(Author)
@@ -60,7 +60,7 @@
 class Place(models.Model):
     name = models.CharField(max_length=50)
     city = models.CharField(max_length=50)
-    
+
     def __unicode__(self):
         return self.name
 
@@ -68,7 +68,7 @@
     auto_id = models.AutoField(primary_key=True)
     name = models.CharField(max_length=100)
     place = models.ForeignKey(Place)
-    
+
     def __unicode__(self):
         return "%s at %s" % (self.name, self.place)
 
@@ -81,13 +81,13 @@
 class OwnerProfile(models.Model):
     owner = models.OneToOneField(Owner, primary_key=True)
     age = models.PositiveIntegerField()
-    
+
     def __unicode__(self):
         return "%s is %d" % (self.owner.name, self.age)
 
 class Restaurant(Place):
     serves_pizza = models.BooleanField()
-    
+
     def __unicode__(self):
         return self.name
 
@@ -114,17 +114,17 @@
 # using inlineformset_factory.
 class Repository(models.Model):
     name = models.CharField(max_length=25)
-    
+
     def __unicode__(self):
         return self.name
 
 class Revision(models.Model):
     repository = models.ForeignKey(Repository)
     revision = models.CharField(max_length=40)
-    
+
     class Meta:
         unique_together = (("repository", "revision"),)
-    
+
     def __unicode__(self):
         return u"%s (%s)" % (self.revision, unicode(self.repository))
 
@@ -146,7 +146,7 @@
 class Player(models.Model):
     team = models.ForeignKey(Team, null=True)
     name = models.CharField(max_length=100)
-    
+
     def __unicode__(self):
         return self.name
 
@@ -526,7 +526,7 @@
 ...     print book.title
 Les Fleurs du Mal
 
-Test inline formsets where the inline-edited object uses multi-table inheritance, thus 
+Test inline formsets where the inline-edited object uses multi-table inheritance, thus
 has a non AutoField yet auto-created primary key.
 
 >>> AuthorBooksFormSet3 = inlineformset_factory(Author, AlternateBook, can_delete=False, extra=1)
@@ -663,7 +663,7 @@
 >>> formset.save()
 [<OwnerProfile: Joe Perry is 55>]
 
-# ForeignKey with unique=True should enforce max_num=1 
+# ForeignKey with unique=True should enforce max_num=1
 
 >>> FormSet = inlineformset_factory(Place, Location, can_delete=False)
 >>> formset = FormSet(instance=place)
@@ -861,4 +861,192 @@
 >>> formset.get_queryset()
 [<Player: Bobby>]
 
+
+# Tests for declarative modelformset
+
+# clean up first
+>>> Author.objects.all().delete()
+
+>>> class AuthorForm(forms.ModelForm):
+...     class Meta:
+...         model = Author
+
+# test definition checks
+>>> class BadFormSet1(forms.models.ModelFormSet):
+...     form = AuthorForm
+Traceback (most recent call last):
+...
+Exception: Error in definition of ModelFormSet subclass BadFormSet1. You need to define a model.
+
+>>> class BadFormSet2(forms.models.ModelFormSet):
+...     model = Author
+Traceback (most recent call last):
+...
+Exception: Error in definition of ModelFormSet subclass BadFormSet2. You need to define a form.
+
+# test the modelformset functionality
+>>> class DeclarativeAuthorFormSet(forms.models.ModelFormSet):
+...     form = AuthorForm
+...     model = Author
+...     extra = 3
+
+>>> qs = Author.objects.all()
+>>> formset = DeclarativeAuthorFormSet(queryset=qs)
+>>> for form in formset.forms:
+...     print form.as_p()
+<p><label for="id_form-0-name">Name:</label> <input id="id_form-0-name" type="text" name="form-0-name" maxlength="100" /><input type="hidden" name="form-0-id" id="id_form-0-id" /></p>
+<p><label for="id_form-1-name">Name:</label> <input id="id_form-1-name" type="text" name="form-1-name" maxlength="100" /><input type="hidden" name="form-1-id" id="id_form-1-id" /></p>
+<p><label for="id_form-2-name">Name:</label> <input id="id_form-2-name" type="text" name="form-2-name" maxlength="100" /><input type="hidden" name="form-2-id" id="id_form-2-id" /></p>
+
+>>> data = {
+...     'form-TOTAL_FORMS': '3', # the number of forms rendered
+...     'form-INITIAL_FORMS': '0', # the number of forms with initial data
+...     'form-0-name': 'Charles Baudelaire',
+...     'form-1-name': 'Arthur Rimbaud',
+...     'form-2-name': '',
+... }
+
+>>> formset = DeclarativeAuthorFormSet(data=data, queryset=qs)
+>>> formset.is_valid()
+True
+
+>>> formset.save()
+[<Author: Charles Baudelaire>, <Author: Arthur Rimbaud>]
+
+>>> for author in Author.objects.order_by('name'):
+...     print author.name
+Arthur Rimbaud
+Charles Baudelaire
+
+
+
+# Tests for declarative inlineformset
+
+# clean up first
+>>> Book.objects.all().delete()
+>>> author = Author.objects.get(pk=1)
+>>> class BookForm(forms.ModelForm):
+...     class Meta:
+...         model = Book
+
+# test definition checks
+>>> class BadFormSet1(forms.models.InlineFormSet):
+...     form = BookForm
+...     parent = 'author'
+Traceback (most recent call last):
+...
+Exception: Error in definition of InlineFormSet subclass BadFormSet1. You need to define a model.
+
+>>> class BadFormSet2(forms.models.InlineFormSet):
+...     model = Book
+...     parent = 'author'
+Traceback (most recent call last):
+...
+Exception: Error in definition of InlineFormSet subclass BadFormSet2. You need to define a form.
+
+>>> class BadFormSet3(forms.models.InlineFormSet):
+...     model = Book
+...     form = BookForm
+Traceback (most recent call last):
+...
+Exception: Error in definition of InlineFormSet subclass BadFormSet3. You need to define a parent attribute.
+
+>>> class BadFormSet4(forms.models.InlineFormSet):
+...     model = Book
+...     form = BookForm
+...     parent = 'unexistingfield'
+Traceback (most recent call last):
+...
+Exception: Error in definition of InlineFormSet subclass BadFormSet4. The parent attribute of the inlineformset is not a valid field.
+
+>>> class BadFormSet5(forms.models.InlineFormSet):
+...     model = Book
+...     form = BookForm
+...     parent = 'title'
+Traceback (most recent call last):
+...
+Exception: Error in definition of InlineFormSet subclass BadFormSet5. The parent attribute is not a ForeignKey.
+
+# test the inlineformset functionality
+>>> class DeclarativeAuthorBooksFormSet(forms.models.InlineFormSet):
+...     model = Book
+...     form = BookForm
+...     parent = 'author'
+
+>>> formset = DeclarativeAuthorBooksFormSet(instance=author)
+>>> for form in formset.forms:
+...     print form.as_p()
+<p><label for="id_book_set-0-title">Title:</label> <input id="id_book_set-0-title" type="text" name="book_set-0-title" maxlength="100" /><input type="hidden" name="book_set-0-author" value="1" id="id_book_set-0-author" /><input type="hidden" name="book_set-0-id" id="id_book_set-0-id" /></p>
+<p><label for="id_book_set-1-title">Title:</label> <input id="id_book_set-1-title" type="text" name="book_set-1-title" maxlength="100" /><input type="hidden" name="book_set-1-author" value="1" id="id_book_set-1-author" /><input type="hidden" name="book_set-1-id" id="id_book_set-1-id" /></p>
+<p><label for="id_book_set-2-title">Title:</label> <input id="id_book_set-2-title" type="text" name="book_set-2-title" maxlength="100" /><input type="hidden" name="book_set-2-author" value="1" id="id_book_set-2-author" /><input type="hidden" name="book_set-2-id" id="id_book_set-2-id" /></p>
+
+>>> data = {
+...     'book_set-TOTAL_FORMS': '3', # the number of forms rendered
+...     'book_set-INITIAL_FORMS': '0', # the number of forms with initial data
+...     'book_set-0-title': 'Les Fleurs du Mal',
+...     'book_set-1-title': '',
+...     'book_set-2-title': '',
+... }
+
+>>> formset = DeclarativeAuthorBooksFormSet(data, instance=author)
+>>> formset.is_valid()
+True
+
+>>> formset.save()
+[<Book: Les Fleurs du Mal>]
+
+>>> for book in author.book_set.all():
+...     print book.title
+Les Fleurs du Mal
+
+>>> formset = DeclarativeAuthorBooksFormSet(instance=author)
+>>> for form in formset.forms:
+...     print form.as_p()
+<p><label for="id_book_set-0-title">Title:</label> <input id="id_book_set-0-title" type="text" name="book_set-0-title" value="Les Fleurs du Mal" maxlength="100" /><input type="hidden" name="book_set-0-author" value="1" id="id_book_set-0-author" /><input type="hidden" name="book_set-0-id" value="1" id="id_book_set-0-id" /></p>
+<p><label for="id_book_set-1-title">Title:</label> <input id="id_book_set-1-title" type="text" name="book_set-1-title" maxlength="100" /><input type="hidden" name="book_set-1-author" value="1" id="id_book_set-1-author" /><input type="hidden" name="book_set-1-id" id="id_book_set-1-id" /></p>
+<p><label for="id_book_set-2-title">Title:</label> <input id="id_book_set-2-title" type="text" name="book_set-2-title" maxlength="100" /><input type="hidden" name="book_set-2-author" value="1" id="id_book_set-2-author" /><input type="hidden" name="book_set-2-id" id="id_book_set-2-id" /></p>
+<p><label for="id_book_set-3-title">Title:</label> <input id="id_book_set-3-title" type="text" name="book_set-3-title" maxlength="100" /><input type="hidden" name="book_set-3-author" value="1" id="id_book_set-3-author" /><input type="hidden" name="book_set-3-id" id="id_book_set-3-id" /></p>
+
+>>> data = {
+...     'book_set-TOTAL_FORMS': '3', # the number of forms rendered
+...     'book_set-INITIAL_FORMS': '1', # the number of forms with initial data
+...     'book_set-0-id': '1',
+...     'book_set-0-title': 'Les Fleurs du Mal',
+...     'book_set-1-title': 'Le Spleen de Paris',
+...     'book_set-2-title': '',
+...     'book_set-3-title': '',
+... }
+
+>>> formset = DeclarativeAuthorBooksFormSet(data, instance=author)
+>>> formset.is_valid()
+True
+
+>>> formset.save()
+[<Book: Le Spleen de Paris>]
+
+
+# specific declarative field definitions just work on the declarative form for formsets
+# no need to define the widget in __init__
+>>> class MembershipForm(forms.ModelForm):
+...     date_joined = forms.SplitDateTimeField(initial=now, widget=forms.SplitDateTimeWidget)
+...     class Meta:
+...         model = Membership
+
+>>> class FormSet(forms.models.InlineFormSet):
+...     model = Membership
+...     form = MembershipForm
+...     parent = 'person'
+...     extra = 1
+>>> data = {
+...     'membership_set-TOTAL_FORMS': '1',
+...     'membership_set-INITIAL_FORMS': '0',
+...     'membership_set-0-date_joined_0': unicode(now.strftime('%Y-%m-%d')),
+...     'membership_set-0-date_joined_1': unicode(now.strftime('%H:%M:%S')),
+...     'initial-membership_set-0-date_joined': unicode(now.strftime('%Y-%m-%d %H:%M:%S')),
+...     'membership_set-0-karma': '',
+... }
+>>> formset = FormSet(data, instance=person)
+>>> formset.is_valid()
+True
+
 """}
