Repository: joke2k/faker

Issue:
EAN-13 starting with 0
ean13 function generates codes starting with zeroes.

According to http://www.barcodeisland.com/ean13.phtml ean13 has to duplicate the leading zero.
So basically UPC-A code "075678164125" in ean13 form should be "0075678164125".

Discussion:
malefice: There are some flake8 errors and some errors caused by Python3-only syntax. I will fix all these errors once I have reviewer feedback.

Patch:
diff --git a/build32bit.sh b/build32bit.sh
index c493d7f3..c9df66f0 100755
--- a/build32bit.sh
+++ b/build32bit.sh
@@ -10,6 +10,7 @@ docker run -v ${PWD}:/code -e INSTALL_REQUIREMENTS=${INSTALL_REQUIREMENTS} i386/
     && DEBIAN_FRONTEND=noninteractive apt-get install -yq python3 locales python3-pip debianutils \
     && pip3 install tox coveralls \
     && locale-gen en_US.UTF-8 \
+    && export LANG='en_US.UTF-8' \
     && cd /code \
     && coverage run --source=faker setup.py test \
     && coverage report"
diff --git a/faker/generator.py b/faker/generator.py
index 40135a1b..b66b89bb 100644
--- a/faker/generator.py
+++ b/faker/generator.py
@@ -4,6 +4,7 @@ from __future__ import unicode_literals
 
 import random as random_module
 import re
+import six
 
 _re_token = re.compile(r'\{\{(\s?)(\w+)(\s?)\}\}')
 random = random_module.Random()
@@ -108,5 +109,5 @@ class Generator(object):
 
     def __format_token(self, matches):
         formatter = list(matches.groups())
-        formatter[1] = self.format(formatter[1])
+        formatter[1] = six.text_type(self.format(formatter[1]))
         return ''.join(formatter)
diff --git a/faker/providers/barcode/__init__.py b/faker/providers/barcode/__init__.py
index 0ccd34e2..512a576a 100644
--- a/faker/providers/barcode/__init__.py
+++ b/faker/providers/barcode/__init__.py
@@ -1,17 +1,50 @@
 # coding=utf-8
 
 from __future__ import unicode_literals
+
+import re
+from six import string_types
+
 from .. import BaseProvider
 
 
 class Provider(BaseProvider):
 
-    def ean(self, length=13):
-        code = [self.random_digit() for _ in range(length - 1)]
+    upc_e_base_pattern = re.compile(r'^\d{6}$')
+    upc_ae_pattern1 = re.compile(
+        r'^(?P<number_system_digit>[01])'   # The first digit must be 0 or 1
+        r'(?=\d{11}$)'                      # followed by 11 digits of which
+        r'(?P<mfr_code>\d{2})'              # the first 2 digits make up the manufacturer code,
+        r'(?:(?P<extra>[012])0{4})'         # if immediately followed by 00000, 10000, or 20000,
+        r'(?P<product_code>\d{3})'          # a 3-digit product code,
+        r'(?P<check_digit>\d)$',            # and finally a check digit.
+    )
+    upc_ae_pattern2 = re.compile(
+        r'^(?P<number_system_digit>[01])'   # The first digit must be 0 or 1
+        r'(?=\d{11}$)'                      # followed by 11 digits of which
+        r'(?P<mfr_code>\d{3,4}?)'           # the first 3 or 4 digits make up the manufacturer code,
+        r'(?:0{5})'                         # if immediately followed by 00000,
+        r'(?P<product_code>\d{1,2})'        # a 2-digit or single digit product code,
+        r'(?P<check_digit>\d)$',            # and finally a check digit.
+    )
+    upc_ae_pattern3 = re.compile(
+        r'^(?P<number_system_digit>[01])'   # The first digit must be 0 or 1
+        r'(?=\d{11}$)'                      # followed by 11 digits of which
+        r'(?P<mfr_code>\d{5})'              # the first 5 digits make up the manufacturer code,
+        r'(?:0{4}(?P<extra>[5-9]))'         # if immediately followed by 0000 and a 5, 6, 7, 8, or 9,
+        r'(?P<check_digit>\d)$',            # and finally a check digit.
+    )
 
+    def _ean(self, length=13, leading_zero=None):
         if length not in (8, 13):
             raise AssertionError("length can only be 8 or 13")
 
+        code = [self.random_digit() for _ in range(length - 1)]
+        if leading_zero is True:
+            code[0] = 0
+        elif leading_zero is False:
+            code[0] = self.random_int(1, 9)
+
         if length == 8:
             weights = [3, 1, 3, 1, 3, 1, 3]
         elif length == 13:
@@ -23,8 +56,146 @@ class Provider(BaseProvider):
 
         return ''.join(str(x) for x in code)
 
+    def _convert_upc_a2e(self, upc_a):
+        """
+        Converts a 12-digit UPC-A barcode to its 8-digit UPC-E equivalent
+
+        Not all UPC-A barcodes can be converted.
+
+        :param upc_a: UPC-A barcode to convert
+        :return: UPC-E equivalent barcode
+        """
+        if not isinstance(upc_a, string_types):
+            raise TypeError('`upc_a` is not a string')
+        m1 = self.upc_ae_pattern1.match(upc_a)
+        m2 = self.upc_ae_pattern2.match(upc_a)
+        m3 = self.upc_ae_pattern3.match(upc_a)
+        if not any([m1, m2, m3]):
+            raise ValueError('`upc_a` has an invalid value')
+        upc_e_template = '{number_system_digit}{mfr_code}{product_code}{extra}{check_digit}'
+        if m1:
+            upc_e = upc_e_template.format(**m1.groupdict())
+        elif m2:
+            groupdict = m2.groupdict()
+            groupdict['extra'] = str(len(groupdict.get('mfr_code')))
+            upc_e = upc_e_template.format(**groupdict)
+        else:
+            groupdict = m3.groupdict()
+            groupdict['product_code'] = ''
+            upc_e = upc_e_template.format(**groupdict)
+        return upc_e
+
+    def _upc_ae(self, base=None, number_system_digit=None):
+        """
+        Creates a 12-digit UPC-A barcode that can be converted to UPC-E
+
+        Notes on this method:
+        - Please view notes on `upc_a()` and `upc_e()` first.
+
+        :param base: A 6-digit string
+        :param number_system_digit: 0 or 1
+        :return: 12-digit UPC-A barcode that can be converted to UPC-E
+        """
+        if isinstance(base, string_types) and self.upc_e_base_pattern.match(base):
+            base = [int(x) for x in base]
+        else:
+            base = [self.random_int(0, 9) for _ in range(6)]
+        if number_system_digit not in [0, 1]:
+            number_system_digit = self.random_int(0, 1)
+
+        if base[-1] <= 2:
+            code = base[:2] + base[-1:] + [0] * 4 + base[2:-1]
+        elif base[-1] <= 4:
+            code = base[:base[-1]] + [0] * 5 + base[base[-1]:-1]
+        else:
+            code = base[:5] + [0] * 4 + base[-1:]
+
+        code.insert(0, number_system_digit)
+        weights = [3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3]
+        weighted_sum = sum(x * y for x, y in zip(code, weights))
+        check_digit = (10 - weighted_sum % 10) % 10
+        code.append(check_digit)
+        return ''.join(str(x) for x in code)
+
+    def ean(self, length=13):
+        return self._ean(length)
+
     def ean8(self):
-        return self.ean(8)
+        return self._ean(8)
+
+    def ean13(self, leading_zero=None):
+        """
+        Creates an EAN-13 barcode
+
+        :param leading_zero: Leading digit will be 0 if True, 1-9 if False, and 0-9 otherwise
+        :return: An EAN-13 barcode
+        """
+        return self._ean(13, leading_zero=leading_zero)
+
+    def upc_a(self, upc_ae_mode=False, base=None, number_system_digit=None):
+        """
+        Creates a 12-digit UPC-A barcode
+
+        Notes on UPC-A barcode and this method:
+        - EAN-13 is a superset of UPC-A. Simply put, leading zero + UPC-A = EAN-13.
+        - For the lack of a concise and better term, this provider uses the term "upc_ae" to mean
+          that a UPC-A barcode can be converted to UPC-E.
+        - If `upc_ae_mode` is enabled, this method will only return UPC-A barcodes that can be
+          converted to UPC-E. In this mode the leading digit (number system) of the UPC-A barcode
+          may only start with 0 or 1.
+        - When `upc_ae_mode` is enabled, the `number_system_digit` may be explicitly set to 0 or 1.
+          If it is not set or is invalid, either values will be chosen at random.
+        - When `upc_ae_mode` is enabled, a 6-digit UPC-E string `base` may be explicitly set. If it
+          is not set or is invalid, a random 6 digit combination will be used.
+        - If `upc_ae_mode` is disabled, the values of `base` and `number_system_digit` are ignored.
+
+        :param upc_ae_mode: Set to True explicitly to enable
+        :param base: A 6-digit string
+        :param number_system_digit: 0 or 1
+        :return: 12-digit UPC-A barcode
+        """
+        if upc_ae_mode is True:
+            return self._upc_ae(base=base, number_system_digit=number_system_digit)
+        else:
+            ean13 = self.ean13(leading_zero=True)
+            return ean13[1:]
+
+    def upc_e(self, base=None, number_system_digit=None, safe_mode=True):
+        """
+        Creates an 8-digit UPC-E barcode
+
+        Notes on UPC-E barcode and this method:
+        - UPC-E barcodes can be expressed in 6, 7, or 8-digit formats, but this method uses the
+          8 digit format, since it is trivial to convert to the other two formats.
+        - The first digit of the barcode denotes the number system used, either 0 or 1.
+        - The last digit is the check digit (more on that below).
+        - The remaining 6 digits are a bit more involved, but they are referred to as the `base`
+          argument elsewhere in this provider (for the lack of a concise and better term).
+        - Each UPC-E `base` have 2 UPC-A equivalents depending on the number system used.
+        - If the value of `number_system_digit` is not 0 or 1, either values will be chosen at random.
+        - A 6-digit string `base` may also be explicitly set. If invalid, it will be ignored, and a
+          new value for `base` will be generated.
+        - Internally, this method first generates a UPC-A barcode that can be converted to UPC-E, and
+          then actually performs a conversion for the result. This is because both the number
+          system and check digits of the UPC-E barcode are inherited from its UPC-A equivalent.
+        - Then there is `safe_mode`. Enabling this guarantees that every UPC-E barcode generated can
+          be converted to UPC-A, and that UPC-A value can be converted back again to UPC-E and still
+          be equal to the original value. In other words, the statement a2e(e2a(e)) == e holds True.
+        - As to why there is `safe_mode`, it is because there are some UPC-E values that share the
+          same UPC-A equivalent. For example, any UPC-E barcode of the form abc0000d, abc0003d, and
+          abc0004d share the same UPC-A value abc00000000d, but that UPC-A value will only convert
+          to abc0000d because of (a) how UPC-E is just a zero-suppressed version of UPC-A and
+          (b) the rules around the conversion.
 
-    def ean13(self):
-        return self.ean(13)
+        :param base: A 6-digit string
+        :param number_system_digit: First digit of the barcode
+        :param safe_mode: True or False. Guarantees that every UPC-E barcode generated can be
+                          converted back-and-forth between UPC-A and UPC-E.
+        :return: 8-digit UPC-E barcode
+        """
+        if safe_mode is not False:
+            upc_ae = self._upc_ae(base=base, number_system_digit=number_system_digit)
+            return self._convert_upc_a2e(upc_ae)
+        else:
+            upc_ae = self._upc_ae(base=base, number_system_digit=number_system_digit)
+            return upc_ae[0] + ''.join(str(x) for x in base) + upc_ae[-1]
diff --git a/faker/providers/isbn/__init__.py b/faker/providers/isbn/__init__.py
index ada84898..2d6ace9c 100644
--- a/faker/providers/isbn/__init__.py
+++ b/faker/providers/isbn/__init__.py
@@ -52,7 +52,7 @@ class Provider(BaseProvider):
         :returns: A (registrant, publication) tuple of strings.
         """
         for rule in rules:
-            if rule.min <= reg_pub <= rule.max:
+            if rule.min <= reg_pub[:-1] <= rule.max:
                 reg_len = rule.registrant_length
                 break
         else:
diff --git a/faker/providers/phone_number/fa_IR/__init__.py b/faker/providers/phone_number/fa_IR/__init__.py
index 07f60d0b..ed6a1acb 100644
--- a/faker/providers/phone_number/fa_IR/__init__.py
+++ b/faker/providers/phone_number/fa_IR/__init__.py
@@ -5,14 +5,34 @@ from .. import Provider as PhoneNumberProvider
 class Provider(PhoneNumberProvider):
     formats = (
         # Mobile
+        # Mci
         '+98 91# ### ####',
         '091# ### ####',
+        '+98 990 ### ####',
+        '0990 ### ####',
+        '+98 991 ### ####',
+        '0991 ### ####',
+        # Rightel Mobile prefixes
         '+98 920 ### ####',
         '0920 ### ####',
         '+98 921 ### ####',
         '0921 ### ####',
+        '+98 922 ### ####',
+        '0922 ### ####',
+        # Samantel Mobile prefixes
+        '+98 999 ### ####',
+        '0999 ### ####',
+        # Mtn and Talia
         '+98 93# ### ####',
         '093# ### ####',
+        '+98 901 ### ####',
+        '0901 ### ####',
+        '+98 902 ### ####',
+        '902 ### ####',
+        '+98 903 ### ####',
+        '0903 ### ####',
+        '+98 905 ### ####',
+        '0905 ### ####',
         # Land lines,
         # https://en.wikipedia.org/wiki/List_of_dialling_codes_in_Iran
         '+98 21 #### ####',
diff --git a/faker/providers/python/__init__.py b/faker/providers/python/__init__.py
index eb8a70a6..36317627 100644
--- a/faker/providers/python/__init__.py
+++ b/faker/providers/python/__init__.py
@@ -3,6 +3,7 @@
 from __future__ import unicode_literals
 
 from decimal import Decimal
+import string
 import sys
 
 import six
@@ -32,6 +33,9 @@ class Provider(BaseProvider):
                 ),
             )
 
+    def pystr_format(self, string_format='?#-###{{random_int}}{{random_letter}}', letters=string.ascii_letters):
+        return self.bothify(self.generator.parse(string_format), letters=letters)
+
     def pyfloat(self, left_digits=None, right_digits=None, positive=False,
                 min_value=None, max_value=None):
 


Test patch:
diff --git a/tests/providers/test_barcode.py b/tests/providers/test_barcode.py
new file mode 100644
index 00000000..03e93e0f
--- /dev/null
+++ b/tests/providers/test_barcode.py
@@ -0,0 +1,172 @@
+# coding=utf-8
+
+from __future__ import unicode_literals
+
+import re
+import unittest
+
+from faker import Faker
+
+
+class TestBarcodeProvider(unittest.TestCase):
+    """ Tests barcode provider """
+
+    num_sample_runs = 1000
+
+    def setUp(self):
+        self.ean8_pattern = re.compile(r'^\d{8}$')
+        self.ean13_pattern = re.compile(r'^\d{13}$')
+        self.upc_a_pattern = re.compile(r'^\d{12}$')
+        self.upc_e_pattern = re.compile(r'^[01]\d{7}$')
+        self.factory = Faker()
+
+    def test_ean(self):
+        for _ in range(self.num_sample_runs):
+            ean8 = self.factory.ean(8)
+            ean13 = self.factory.ean(13)
+            assert self.ean8_pattern.match(ean8)
+            assert self.ean13_pattern.match(ean13)
+
+            ean8_digits = [int(digit) for digit in ean8]
+            ean13_digits = [int(digit) for digit in ean13]
+            assert (sum(ean8_digits) + 2 * sum(ean8_digits[::2])) % 10 == 0
+            assert (sum(ean13_digits) + 2 * sum(ean13_digits[1::2])) % 10 == 0
+
+    def test_ean8(self):
+        for _ in range(self.num_sample_runs):
+            ean8 = self.factory.ean8()
+            assert self.ean8_pattern.match(ean8)
+
+            # Included check digit must be correct
+            ean8_digits = [int(digit) for digit in ean8]
+            assert (sum(ean8_digits) + 2 * sum(ean8_digits[::2])) % 10 == 0
+
+    def test_ean13(self):
+        for _ in range(self.num_sample_runs):
+            ean13 = self.factory.ean13()
+            assert self.ean13_pattern.match(ean13)
+
+            # Included check digit must be correct
+            ean13_digits = [int(digit) for digit in ean13]
+            assert (sum(ean13_digits) + 2 * sum(ean13_digits[1::2])) % 10 == 0
+
+    def test_ean13_no_leading_zero(self):
+        for _ in range(1000):
+            ean13 = self.factory.ean13(leading_zero=False)
+            assert self.ean13_pattern.match(ean13)
+            assert ean13[0] != '0'
+
+            # Included check digit must be correct
+            ean13_digits = [int(digit) for digit in ean13]
+            assert (sum(ean13_digits) + 2 * sum(ean13_digits[1::2])) % 10 == 0
+
+    def test_ean13_leading_zero(self):
+        for _ in range(1000):
+            ean13 = self.factory.ean13(leading_zero=True)
+            assert self.ean13_pattern.match(ean13)
+            assert ean13[0] == '0'
+
+            # Included check digit must be correct
+            ean13_digits = [int(digit) for digit in ean13]
+            assert (sum(ean13_digits) + 2 * sum(ean13_digits[1::2])) % 10 == 0
+
+    def test_upc_a(self):
+        for _ in range(self.num_sample_runs):
+            upc_a = self.factory.upc_a()
+            assert self.upc_a_pattern.match(upc_a)
+
+            # Included check digit must be correct
+            upc_a_digits = [int(digit) for digit in upc_a]
+            assert (sum(upc_a_digits) + 2 * sum(upc_a_digits[::2])) % 10 == 0
+
+    def test_upc_ae_mode(self):
+        for _ in range(self.num_sample_runs):
+            upc_ae = self.factory.upc_a(upc_ae_mode=True)
+            assert self.upc_a_pattern.match(upc_ae)
+
+            # Included check digit must be correct
+            upc_ae_digits = [int(digit) for digit in upc_ae]
+            assert (sum(upc_ae_digits) + 2 * sum(upc_ae_digits[::2])) % 10 == 0
+
+    def test_upc_e_explicit_number_system(self):
+        for _ in range(self.num_sample_runs):
+            upc_e_0 = self.factory.upc_e(number_system_digit=0)
+            upc_e_1 = self.factory.upc_e(number_system_digit=1)
+            assert self.upc_e_pattern.match(upc_e_0)
+            assert self.upc_e_pattern.match(upc_e_1)
+            assert upc_e_0[0] == '0'
+            assert upc_e_1[0] == '1'
+
+    def test_upc_e_safe_mode(self):
+        # For this test, we explicitly specify a base and a number system digit
+        # so we do not have to wait for RNG to produce the right combinations.
+        for _ in range(100):
+            # Be aware that there are other unsafe combinations
+            unsafe_base = '{:02}000{}'.format(self.factory.random_int(0, 99), self.factory.random_int(3, 4))
+            safe_base = unsafe_base[:2] + '0000'
+            number_system_digit = self.factory.random_int(0, 1)
+
+            # Safe mode will create a UPC-E barcode with the safe base
+            # even if an unsafe base was supplied
+            upc_e_safe = self.factory.upc_e(base=unsafe_base,
+                                            number_system_digit=number_system_digit,
+                                            safe_mode=True)
+            assert upc_e_safe[1:-1] == safe_base
+            assert upc_e_safe[1:-1] != unsafe_base
+
+            # Unsafe mode will force create a UPC-E barcode with unsafe base
+            upc_e_unsafe = self.factory.upc_e(base=unsafe_base,
+                                              number_system_digit=number_system_digit,
+                                              safe_mode=False)
+            assert upc_e_unsafe[1:-1] != safe_base
+            assert upc_e_unsafe[1:-1] == unsafe_base
+
+            # What will be the same are their number system and check digits
+            assert upc_e_safe[0] == upc_e_unsafe[0]
+            assert upc_e_safe[-1] == upc_e_unsafe[-1]
+
+    def test_upc_a2e2a(self):
+        from faker.providers.barcode import Provider
+        provider = Provider(self.factory)
+
+        for _ in range(self.num_sample_runs):
+            upc_a = self.factory.upc_a(upc_ae_mode=True)
+            assert self.upc_a_pattern.match(upc_a)
+
+            # Convert UPC-A to UPC-E
+            upc_e = provider._convert_upc_a2e(upc_a)
+
+            # Number system and check digits must be the same
+            assert int(upc_a[0]) == int(upc_e[0])
+            assert int(upc_a[-1]) == int(upc_e[-1])
+
+            # Create a new UPC-A barcode based on the UPC-E barcode
+            new_upc_a = self.factory.upc_a(upc_ae_mode=True,
+                                           base=upc_e[1:-1],
+                                           number_system_digit=int(upc_e[0]))
+
+            # New UPC-A barcode must be the same as the original
+            assert upc_a == new_upc_a
+
+    def test_upc_e2a2e(self):
+        from faker.providers.barcode import Provider
+        provider = Provider(self.factory)
+
+        for _ in range(self.num_sample_runs):
+            upc_e = self.factory.upc_e()
+            assert self.upc_e_pattern.match(upc_e)
+
+            # Create a new UPC-A barcode based on the UPC-E barcode
+            upc_a = self.factory.upc_a(upc_ae_mode=True,
+                                       base=upc_e[1:-1],
+                                       number_system_digit=int(upc_e[0]))
+
+            # Number system and check digits must be the same
+            assert int(upc_a[0]) == int(upc_e[0])
+            assert int(upc_a[-1]) == int(upc_e[-1])
+
+            # Convert UPC-A to UPC-E
+            new_upc_e = provider._convert_upc_a2e(upc_a)
+
+            # New UPC-E barcode must be the same as the original
+            assert new_upc_e == upc_e
diff --git a/tests/providers/test_isbn.py b/tests/providers/test_isbn.py
index 4d6fab06..fe01c4aa 100644
--- a/tests/providers/test_isbn.py
+++ b/tests/providers/test_isbn.py
@@ -43,8 +43,13 @@ class TestProvider(unittest.TestCase):
     def test_reg_pub_separation(self):
         r1 = RegistrantRule('0000000', '0000001', 1)
         r2 = RegistrantRule('0000002', '0000003', 2)
-        assert self.prov._registrant_publication('0000000', [r1, r2]) == ('0', '000000')
-        assert self.prov._registrant_publication('0000002', [r1, r2]) == ('00', '00002')
+        assert self.prov._registrant_publication('00000000', [r1, r2]) == ('0', '0000000')
+        assert self.prov._registrant_publication('00000010', [r1, r2]) == ('0', '0000010')
+        assert self.prov._registrant_publication('00000019', [r1, r2]) == ('0', '0000019')
+        assert self.prov._registrant_publication('00000020', [r1, r2]) == ('00', '000020')
+        assert self.prov._registrant_publication('00000030', [r1, r2]) == ('00', '000030')
+        assert self.prov._registrant_publication('00000031', [r1, r2]) == ('00', '000031')
+        assert self.prov._registrant_publication('00000039', [r1, r2]) == ('00', '000039')
 
     def test_rule_not_found(self):
         with pytest.raises(Exception):
diff --git a/tests/providers/test_misc.py b/tests/providers/test_misc.py
index 1ee2476e..be2806f0 100644
--- a/tests/providers/test_misc.py
+++ b/tests/providers/test_misc.py
@@ -24,3 +24,15 @@ class TestMisc(unittest.TestCase):
         uuid4 = self.factory.uuid4(cast_to=lambda x: x)
         assert uuid4
         assert isinstance(uuid4, uuid.UUID)
+
+    def test_uuid4_seedability(self):
+        for _ in range(10):
+            random_seed = self.factory.random_int()
+            baseline_fake = Faker()
+            baseline_fake.seed(random_seed)
+            expected_uuids = [baseline_fake.uuid4() for i in range(1000)]
+
+            new_fake = Faker()
+            new_fake.seed(random_seed)
+            new_uuids = [new_fake.uuid4() for i in range(1000)]
+            assert new_uuids == expected_uuids
diff --git a/tests/providers/test_python.py b/tests/providers/test_python.py
index 61f5ca02..ccb93c79 100644
--- a/tests/providers/test_python.py
+++ b/tests/providers/test_python.py
@@ -1,6 +1,10 @@
 #  -*- coding: utf-8 -*-
 
 import unittest
+try:
+    from unittest.mock import patch
+except ImportError:
+    from mock import patch
 
 from faker import Faker
 
@@ -87,3 +91,19 @@ class TestPyfloat(unittest.TestCase):
 
         message = str(raises.exception)
         self.assertEqual(message, expected_message)
+
+
+class TestPystrFormat(unittest.TestCase):
+
+    def setUp(self):
+        self.factory = Faker(includes=['tests.mymodule.en_US'])
+
+    def test_formatter_invocation(self):
+        with patch.object(self.factory, 'foo') as mock_foo:
+            with patch('faker.providers.BaseProvider.bothify',
+                       wraps=self.factory.bothify) as mock_bothify:
+                mock_foo.return_value = 'barbar'
+                value = self.factory.pystr_format('{{foo}}?#?{{foo}}?#?{{foo}}', letters='abcde')
+                assert value.count('barbar') == 3
+                assert mock_foo.call_count == 3
+                mock_bothify.assert_called_once_with('barbar?#?barbar?#?barbar', letters='abcde')