Repository: joke2k/faker

Issue:
Allow prefix and suffix values for pystr provider
* Faker version: 14.1.0
* OS: OpenSUSE Leap 15.3

I am trying to replace [`factory.fuzzy.FuzzyText`](https://factoryboy.readthedocs.io/en/stable/fuzzy.html#fuzzytext) with the *faker* equivalent, as the *fuzzy* implementations are going to be discontinued in the future. Unfortunately, *prefix* and *suffix* values for `Faker.pystr` are not supported at the moment.

### Expected behavior

An optional prefix and suffix are supported for the *pystr* provider.



Discussion:

Resolving patch:
diff --git a/faker/providers/python/__init__.py b/faker/providers/python/__init__.py
--- a/faker/providers/python/__init__.py
+++ b/faker/providers/python/__init__.py
@@ -51,21 +51,23 @@ def _check_signature(self, value_types: Optional[TypesSpec], allowed_types: Opti
     def pybool(self) -> bool:
         return self.random_int(0, 1) == 1
 
-    def pystr(self, min_chars: Optional[int] = None, max_chars: int = 20) -> str:
+    def pystr(self, min_chars: Optional[int] = None, max_chars: int = 20, prefix: str = "", suffix: str = "") -> str:
         """
         Generates a random string of upper and lowercase letters.
         :return: Random of random length between min and max characters.
         """
         if min_chars is None:
-            return "".join(self.random_letters(length=max_chars))
+            chars = "".join(self.random_letters(length=max_chars))
         else:
             assert max_chars >= min_chars, "Maximum length must be greater than or equal to minimum length"
-            return "".join(
+            chars = "".join(
                 self.random_letters(
                     length=self.generator.random.randint(min_chars, max_chars),
                 ),
             )
 
+        return prefix + chars + suffix
+
     def pystr_format(
         self,
         string_format: str = "?#-###{{random_int}}{{random_letter}}",


Test patch:
diff --git a/tests/providers/test_python.py b/tests/providers/test_python.py
--- a/tests/providers/test_python.py
+++ b/tests/providers/test_python.py
@@ -397,6 +397,56 @@ def test_min_value_10_pow_1000_return_greater_number(self):
         self.assertGreater(result, 10**1000)
 
 
+class TestPystr(unittest.TestCase):
+    def setUp(self):
+        self.fake = Faker(includes=["tests.mymodule.en_US"])
+        Faker.seed(0)
+
+    def test_no_parameters(self):
+        some_string = self.fake.pystr()
+        assert isinstance(some_string, str)
+        assert len(some_string) <= 20
+
+    def test_lower_length_limit(self):
+        some_string = self.fake.pystr(min_chars=3)
+        assert isinstance(some_string, str)
+        assert len(some_string) >= 3
+        assert len(some_string) <= 20
+
+    def test_upper_length_limit(self):
+        some_string = self.fake.pystr(max_chars=5)
+        assert isinstance(some_string, str)
+        assert len(some_string) <= 5
+
+    def test_invalid_length_limits(self):
+        with self.assertRaises(AssertionError):
+            self.fake.pystr(min_chars=6, max_chars=5)
+
+    def test_exact_length(self):
+        some_string = self.fake.pystr(min_chars=5, max_chars=5)
+        assert isinstance(some_string, str)
+        assert len(some_string) == 5
+
+    def test_prefix(self):
+        some_string = self.fake.pystr(prefix="START_")
+        assert isinstance(some_string, str)
+        assert some_string.startswith("START_")
+        assert len(some_string) == 26
+
+    def test_suffix(self):
+        some_string = self.fake.pystr(suffix="_END")
+        assert isinstance(some_string, str)
+        assert some_string.endswith("_END")
+        assert len(some_string) == 24
+
+    def test_prefix_and_suffix(self):
+        some_string = self.fake.pystr(min_chars=9, max_chars=20, prefix="START_", suffix="_END")
+        assert isinstance(some_string, str)
+        assert some_string.startswith("START_")
+        assert some_string.endswith("_END")
+        assert len(some_string) >= 19
+
+
 class TestPystrFormat(unittest.TestCase):
     def setUp(self):
         self.fake = Faker(includes=["tests.mymodule.en_US"])
@@ -422,26 +472,6 @@ def test_pybool(self):
         some_bool = self.factory.pybool()
         assert isinstance(some_bool, bool)
 
-    def py_str(self):
-        some_string = self.factory.pystr()
-        assert isinstance(some_string, str)
-        assert len(some_string) <= 20
-
-        some_string = self.factory.pystr(min_chars=3)
-        assert isinstance(some_string, str)
-        assert len(some_string) >= 3
-        assert len(some_string) <= 20
-
-        some_string = self.factory.pystr(max_chars=5)
-        assert isinstance(some_string, str)
-        assert len(some_string) <= 5
-
-        with self.assertRaises(AssertionError):
-            self.factory.pystr(min_chars=6, max_chars=5)
-
-        with self.assertRaises(AssertionError):
-            self.factory.pystr(min_chars=5, max_chars=5)
-
     def test_pytuple(self):
         with warnings.catch_warnings(record=True) as w:
             some_tuple = Faker().pytuple()