Repository: django/django
Issue category: Feature Request

About the FormSet class name generated through the formset_factory.
Description
	 
When you define Django Form, what format do you usually name it?
In my case, I usually name it "xxxForm".
The names of the forms used as examples in official documents related to Django Form are the same as my name define format.
# Working with forms docs.
class ContactForm(forms.Form):
	subject = forms.CharField(max_length=100)
	message = forms.CharField(widget=forms.Textarea)
	sender = forms.EmailField()
	cc_myself = forms.BooleanField(required=False)
# Working with forms docs.
class ArticleForm(forms.Form):
	title = forms.CharField()
	pub_date = forms.DateField()
# FormField docs.
class CommentForm(forms.Form):
	name = forms.CharField()
	url = forms.URLField()
	comment = forms.CharField()
The way django people name it may be based on the developers' tendencies or tastes, but I think most of them are built in the format as xxxForm.
The FormSet class I created by passing xxxForm name format to the formset_factory is named as follows.
>>> ArticleFormSet = formset_factory(ArticleForm)
>>> ArticleFormSet.__name__
'ArticleFormFormSet'
The name of the FormSet class created through the formset_factory in the form xxxForm name format is a little strange.
This is because when formset_factory creates a class through type, it adds a static "FormSet" to the form name.
def formset_factory(
	form,
	formset=BaseFormSet,
	extra=1,
	can_order=False,
	can_delete=False,
	max_num=None,
	validate_max=False,
	min_num=None,
	validate_min=False,
	absolute_max=None,
	can_delete_extra=True,
	renderer=None,
):
	...
	return type(form.__name__ + "FormSet", (formset,), attrs)
I wonder if the format of these names is intended :)


Functions to edit:
django/forms/formsets.py:formset_factory

Patch:
diff --git a/django/forms/formsets.py b/django/forms/formsets.py
index c8e5893f19b6..c2663154d4a5 100644
--- a/django/forms/formsets.py
+++ b/django/forms/formsets.py
@@ -570,7 +570,12 @@ def formset_factory(
         "validate_max": validate_max,
         "renderer": renderer,
     }
-    return type(form.__name__ + "FormSet", (formset,), attrs)
+    form_name = form.__name__
+    if form_name.endswith("Form"):
+        formset_name = form_name + "Set"
+    else:
+        formset_name = form_name + "FormSet"
+    return type(formset_name, (formset,), attrs)
 
 
 def all_valid(formsets):

Test patch: diff --git a/tests/forms_tests/tests/test_formsets.py b/tests/forms_tests/tests/test_formsets.py
index f80c1dc09e35..9f7012a11fd8 100644
--- a/tests/forms_tests/tests/test_formsets.py
+++ b/tests/forms_tests/tests/test_formsets.py
@@ -149,6 +149,12 @@ def test_basic_formset(self):
         self.assertFalse(formset.is_valid())
         self.assertFalse(formset.has_changed())
 
+    def test_formset_name(self):
+        ArticleFormSet = formset_factory(ArticleForm)
+        ChoiceFormSet = formset_factory(Choice)
+        self.assertEqual(ArticleFormSet.__name__, "ArticleFormSet")
+        self.assertEqual(ChoiceFormSet.__name__, "ChoiceFormSet")
+
     def test_form_kwargs_formset(self):
         """
         Custom kwargs set on the formset instance are passed to the