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:

Patch:
diff --git a/.bumpversion.cfg b/.bumpversion.cfg
index 3dcef7e4..baa45f0c 100644
--- a/.bumpversion.cfg
+++ b/.bumpversion.cfg
@@ -1,5 +1,5 @@
 [bumpversion]
-current_version = 14.1.0
+current_version = 14.1.2
 files = VERSION faker/__init__.py docs/conf.py
 commit = True
 tag = True
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 5838f07c..e39d1757 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -8,6 +8,9 @@ on:
     branches:
       - master
 
+permissions:
+  contents: read
+
 jobs:
   flake8:
     runs-on: ubuntu-latest
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 746cc1c0..4b3f37c1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,13 @@
 ## Changelog
 
+### [v14.1.2 - 2022-08-31](https://github.com/joke2k/faker/compare/v14.1.1...v14.1.2)
+
+*  Fix `uk_UA` `postcode` generation. Thanks @ajinkyac03.
+
+### [v14.1.1 - 2022-08-30](https://github.com/joke2k/faker/compare/v14.1.0...v14.1.1)
+
+* Relax minimum version of `typing-extensions`. Thanks @stefan6419846.
+
 ### [v14.1.0 - 2022-08-17](https://github.com/joke2k/faker/compare/v14.0.0...v14.1.0)
 
 * Add providers for `bn_BD`: `address`, `automotive`, `bank`, `color`, `company`, 
diff --git a/VERSION b/VERSION
index 7b3b6e02..900dba2d 100644
--- a/VERSION
+++ b/VERSION
@@ -1,1 +1,1 @@
-14.1.0
+14.1.2
diff --git a/docs/conf.py b/docs/conf.py
index 52d24776..74bd6b03 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -51,9 +51,9 @@ copyright = "2014, Daniele Faraglia"
 # built documents.
 #
 # The short X.Y version.
-version = "14.1.0"
+version = "14.1.2"
 # The full version, including alpha/beta/rc tags.
-release = "14.1.0"
+release = "14.1.2"
 
 # The language for content autogenerated by Sphinx. Refer to documentation
 # for a list of supported languages.
diff --git a/faker/__init__.py b/faker/__init__.py
index a58af72d..86588e74 100644
--- a/faker/__init__.py
+++ b/faker/__init__.py
@@ -2,6 +2,6 @@ from faker.factory import Factory
 from faker.generator import Generator
 from faker.proxy import Faker
 
-VERSION = "14.1.0"
+VERSION = "14.1.2"
 
 __all__ = ("Factory", "Generator", "Faker")
diff --git a/faker/providers/address/uk_UA/__init__.py b/faker/providers/address/uk_UA/__init__.py
index 33d44cfc..9fc23921 100644
--- a/faker/providers/address/uk_UA/__init__.py
+++ b/faker/providers/address/uk_UA/__init__.py
@@ -2235,7 +2235,7 @@ class Provider(AddressProvider):
 
     def postcode(self) -> str:
         """The code consists of five digits (01000-99999)"""
-        return f"{self.generator.random.randint(0, 10)}{self.generator.random.randint(1000, 10000)}"
+        return f"{self.generator.random.randrange(1000, 99999):05}"
 
     def street_prefix(self, min_length: Optional[int] = None, max_length: Optional[int] = None) -> str:
         return self.random_element(self.street_prefixes, min_length, max_length)
diff --git a/faker/providers/python/__init__.py b/faker/providers/python/__init__.py
index 135012c1..ceaa024f 100644
--- a/faker/providers/python/__init__.py
+++ b/faker/providers/python/__init__.py
@@ -51,21 +51,23 @@ class Provider(BaseProvider):
     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}}",
diff --git a/setup.py b/setup.py
index 62809a10..bde881c6 100644
--- a/setup.py
+++ b/setup.py
@@ -72,6 +72,6 @@ setup(
     python_requires=">=3.6",
     install_requires=[
         "python-dateutil>=2.4",
-        "typing-extensions>=3.10.0.2;python_version<'3.8'",
+        "typing-extensions>=3.7.4.3;python_version<'3.8'",
     ],
 )


Test patch:
diff --git a/tests/providers/test_python.py b/tests/providers/test_python.py
index 819ea3fa..39c067b7 100644
--- a/tests/providers/test_python.py
+++ b/tests/providers/test_python.py
@@ -397,6 +397,56 @@ class TestPydecimal(unittest.TestCase):
         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 @@ class TestPython(unittest.TestCase):
         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()