Repository: joke2k/faker (Python)

Issue:
Feature Request: Support nonbinary gender options in Person Provider
* Faker version: < Faker 4.1.1
* OS: All

Many applications support more than a straightforward binary Male/Female gender selection and require adequate test data. Presently, Faker only supports Male and Female prefixes, suffixes, and names.


Discussion:

Patch:
diff --git a/faker/providers/person/__init__.py b/faker/providers/person/__init__.py
index f19f9f7144..cfb53278cc 100644
--- a/faker/providers/person/__init__.py
+++ b/faker/providers/person/__init__.py
@@ -67,6 +67,14 @@ def name_male(self):
         pattern = self.random_element(formats)
         return self.generator.parse(pattern)
 
+    def name_nonbinary(self):
+        if hasattr(self, 'formats_nonbinary'):
+            formats = self.formats_nonbinary
+        else:
+            formats = self.formats
+        pattern = self.random_element(formats)
+        return self.generator.parse(pattern)
+
     def name_female(self):
         if hasattr(self, 'formats_female'):
             formats = self.formats_female
@@ -80,6 +88,11 @@ def first_name_male(self):
             return self.random_element(self.first_names_male)
         return self.first_name()
 
+    def first_name_nonbinary(self):
+        if hasattr(self, 'first_names_nonbinary'):
+            return self.random_element(self.first_names_nonbinary)
+        return self.first_name()
+
     def first_name_female(self):
         if hasattr(self, 'first_names_female'):
             return self.random_element(self.first_names_female)
@@ -90,6 +103,11 @@ def last_name_male(self):
             return self.random_element(self.last_names_male)
         return self.last_name()
 
+    def last_name_nonbinary(self):
+        if hasattr(self, 'last_names_nonbinary'):
+            return self.random_element(self.last_names_nonbinary)
+        return self.last_name()
+
     def last_name_female(self):
         if hasattr(self, 'last_names_female'):
             return self.random_element(self.last_names_female)
@@ -98,6 +116,10 @@ def last_name_female(self):
     def prefix(self):
         if hasattr(self, 'prefixes'):
             return self.random_element(self.prefixes)
+        if hasattr(self, 'prefixes_male') and hasattr(self, 'prefixes_female') and hasattr(self, 'prefixes_nonbinary'):
+            prefixes = self.random_element(
+                (self.prefixes_male, self.prefixes_female, self.prefixes_nonbinary))
+            return self.random_element(prefixes)
         if hasattr(self, 'prefixes_male') and hasattr(self, 'prefixes_female'):
             prefixes = self.random_element(
                 (self.prefixes_male, self.prefixes_female))
@@ -109,6 +131,11 @@ def prefix_male(self):
             return self.random_element(self.prefixes_male)
         return self.prefix()
 
+    def prefix_nonbinary(self):
+        if hasattr(self, 'prefixes_nonbinary'):
+            return self.random_element(self.prefixes_nonbinary)
+        return self.prefix()
+
     def prefix_female(self):
         if hasattr(self, 'prefixes_female'):
             return self.random_element(self.prefixes_female)
@@ -117,6 +144,10 @@ def prefix_female(self):
     def suffix(self):
         if hasattr(self, 'suffixes'):
             return self.random_element(self.suffixes)
+        if hasattr(self, 'suffixes_male') and hasattr(self, 'suffixes_female') and hasattr(self, 'suffixes_nonbinary'):
+            suffixes = self.random_element(
+                (self.suffixes_male, self.suffixes_female, self.suffixes_nonbinary))
+            return self.random_element(suffixes)
         if hasattr(self, 'suffixes_male') and hasattr(self, 'suffixes_female'):
             suffixes = self.random_element(
                 (self.suffixes_male, self.suffixes_female))
@@ -128,6 +159,11 @@ def suffix_male(self):
             return self.random_element(self.suffixes_male)
         return self.suffix()
 
+    def suffix_nonbinary(self):
+        if hasattr(self, 'suffixes_nonbinary'):
+            return self.random_element(self.suffixes_nonbinary)
+        return self.suffix()
+
     def suffix_female(self):
         if hasattr(self, 'suffixes_female'):
             return self.random_element(self.suffixes_female)
diff --git a/faker/providers/person/en_US/__init__.py b/faker/providers/person/en_US/__init__.py
index 33755321a9..6d04530965 100644
--- a/faker/providers/person/en_US/__init__.py
+++ b/faker/providers/person/en_US/__init__.py
@@ -11,6 +11,13 @@ class Provider(PersonProvider):
         ('{{prefix_female}} {{first_name_female}} {{last_name}} {{suffix_female}}', 0.005),
     ))
 
+    formats_nonbinary = OrderedDict((
+        ('{{first_name_nonbinary}} {{last_name}}', 0.97),
+        ('{{prefix_nonbinary}} {{first_name_nonbinary}} {{last_name}}', 0.015),
+        ('{{first_name_nonbinary}} {{last_name}} {{suffix_nonbinary}}', 0.02),
+        ('{{prefix_nonbinary}} {{first_name_nonbinary}} {{last_name}} {{suffix_nonbinary}}', 0.005),
+    ))
+
     formats_male = OrderedDict((
         ('{{first_name_male}} {{last_name}}', 0.97),
         ('{{prefix_male}} {{first_name_male}} {{last_name}}', 0.015),
@@ -741,6 +748,9 @@ class Provider(PersonProvider):
     first_names = first_names_male.copy()
     first_names.update(first_names_female)
 
+    first_names_nonbinary = first_names_male.copy()
+    first_names_nonbinary.update(first_names_female)
+
     # Top 1000 US surnames from US Census data
     # Weighted by number of occurrences
     # By way of http://names.mongabay.com/data/1000.html on 2/10/2016
@@ -1758,6 +1768,14 @@ class Provider(PersonProvider):
         ('Dr.', 0.3),
     ))
 
+    # https://en.wikipedia.org/wiki/Gender-neutral_title
+    prefixes_nonbinary = OrderedDict((
+        ('Mx.', 0.5),
+        ('Ind.', 0.1),
+        ('Misc.', 0.1),
+        ('Dr.', 0.3),
+    ))
+
     suffixes_female = OrderedDict((
         ('MD', 0.5),
         ('DDS', 0.3),
@@ -1777,3 +1795,5 @@ class Provider(PersonProvider):
         ('PhD', 0.1),
         ('DVM', 0.1),
     ))
+
+    suffixes_nonbinary = suffixes_male.copy()


Test patch:
diff --git a/tests/providers/test_person.py b/tests/providers/test_person.py
index 3aec73e704..add22acf14 100644
--- a/tests/providers/test_person.py
+++ b/tests/providers/test_person.py
@@ -7,6 +7,8 @@
 from faker import Faker
 from faker.providers.person.ar_AA import Provider as ArProvider
 from faker.providers.person.cs_CZ import Provider as CsCZProvider
+from faker.providers.person.en import Provider as EnProvider
+from faker.providers.person.en_US import Provider as EnUSProvider
 from faker.providers.person.es_ES import Provider as EsESProvider
 from faker.providers.person.fi_FI import Provider as FiProvider
 from faker.providers.person.hy_AM import Provider as HyAmProvider
@@ -611,3 +613,86 @@ def setUp(self):
     def test_language_name(self):
         language_name = self.fake.language_name()
         assert language_name in EsESProvider.language_names
+
+
+class TestUs(unittest.TestCase):
+    """ Tests person in the en_US locale """
+
+    def setUp(self):
+        self.fake = Faker('en_US')
+        Faker.seed(0)
+
+    def test_first_names(self):
+        # General first name
+        name = self.fake.first_name()
+        self.assertIsInstance(name, str)
+        assert name in EnUSProvider.first_names
+
+        # Female first name
+        name = self.fake.first_name_female()
+        self.assertIsInstance(name, str)
+        assert name in EnUSProvider.first_names
+        assert name in EnUSProvider.first_names_female
+
+        # Male first name
+        name = self.fake.first_name_male()
+        self.assertIsInstance(name, str)
+        assert name in EnUSProvider.first_names
+        assert name in EnUSProvider.first_names_male
+
+        # Nonbinary first name
+        name = self.fake.first_name_nonbinary()
+        self.assertIsInstance(name, str)
+        assert name in EnUSProvider.first_names
+        assert name in EnUSProvider.first_names_nonbinary
+
+    def test_last_names(self):
+
+        # General last name
+        name = self.fake.last_name()
+        self.assertIsInstance(name, str)
+        assert name in EnUSProvider.last_names
+
+        # Female last name
+        name = self.fake.last_name_female()
+        self.assertIsInstance(name, str)
+        assert name in EnUSProvider.last_names
+
+        # Male last name
+        name = self.fake.last_name_male()
+        self.assertIsInstance(name, str)
+        assert name in EnUSProvider.last_names
+
+        # Nonbinary last name
+        name = self.fake.last_name_nonbinary()
+        self.assertIsInstance(name, str)
+        assert name in EnUSProvider.last_names
+
+    def test_prefix(self):
+
+        # Nonbinary prefix
+        prefix = self.fake.prefix_nonbinary()
+        self.assertIsInstance(prefix, str)
+        assert prefix in EnUSProvider.prefixes_nonbinary
+
+    def test_suffix(self):
+
+        # Nonbinary suffix
+        suffix = self.fake.suffix_nonbinary()
+        self.assertIsInstance(suffix, str)
+        assert suffix in EnUSProvider.suffixes_nonbinary
+
+
+class TestEn(unittest.TestCase):
+    """ Tests person in the en locale """
+
+    def setUp(self):
+        self.fake = Faker('en')
+        Faker.seed(0)
+
+    def test_suffix(self):
+
+        # Traditional suffix -- provider does not offer a nonbinary suffix at this time
+        suffix = self.fake.suffix()
+        self.assertIsInstance(suffix, str)
+        assert suffix in EnProvider.suffixes_male or suffix in EnProvider.suffixes_female