Repository: joke2k/faker

Issue:
feature request: concat locale providers
I would like to include a way to include multiple locales into a faker instance. For example, it would be nice to instantiate Faker with a list of locales and then faker will provide data from both locales.

### Expected behavior

from faker import Faker
fake = Faker(['en_US', 'zh_CN'])
fake.name()
u'Andrea Barnes'
fake.name()
u'\u6797\u79c0\u73cd'

### Actual behavior

Faker only allows one locale(?)

How to change locale?
* Faker version: 1.0.7

I don't see an API exposed to change locale for a Faker object programatically. I'm wondering if it's possible to do so.


Discussion:
I was thinking about this issue as well. I've had instances where I wanted multiple locales.

It would probably be nice to have an include all type of thing, maybe an exclude as well. 

Example:
`fake = Faker(exclude=['en_US', 'zh_CN'])`

Where if exclude is not None, then it will include all providers.

With the way it's setup now, it makes it a little difficult to do. For instance, if you had location and you wanted to generate a random country. Do you combine all the countries together first or select a random locale first and then get a country. 

Programmatically, I'm not really sure the best approach to do this. Thoughts?
to add some more info, one of our main use cases for using faker is to make our application unicode compatible. Currently, some of the providers have very thin coverage which cause foreign key issues since we are saving objects to our database. We're basically trying to increase the pool of provider data to choose from to get unicode support and avoid foreign key constraints.

@kungfu71186 i really like the idea of having an "all" configuration. I also like the idea of exclude as well.
@zdelagrange Not sure how you are using faker as of right now, but it could be possible to kinda get around it for right now. I don't see a way to set the locale after creating a Faker instance. This could be one possibility.

Another possibility is to create a list of faker objects with different locales. Then randomly select one and then generate what you want.

For example,

```
from faker Import Faker
import random

faker = (Faker('en_US'), Faker('zh_CN'))
random.choice(faker).address()
```

this could be a workaround for you in the meantime.

thanks for the suggestion @kungfu71186 , a proxy type object was what i was thinking as well. that's a fairly good plan for a simple implementation.
Do you mean like this? https://github.com/joke2k/faker#localization

Sorry for not replying promptly. Similar to what you described but seems that a single Faker instance can only use one locale, which is read upon initialization. Is it possible to extend this feature so we can change locale during runtime?
Im not certain if this is currently possible. A workaround would be initializing multiple Faker instances, one for each locale. Which you could keep in a dict for easy access.

```python
fakers = {locale: Faker(locale) for locale in ['ja_JP', 'tr_TR', 'hi_IN', …]}
…
fakers[current_locale].name()
```

Resolving patch:
diff --git a/faker/__init__.py b/faker/__init__.py
--- a/faker/__init__.py
+++ b/faker/__init__.py
@@ -1,6 +1,5 @@
 from faker.generator import Generator  # noqa F401
 from faker.factory import Factory  # noqa F401
+from faker.proxy import Faker  # noqa F401
 
 VERSION = '2.0.5'
-
-Faker = Factory.create
diff --git a/faker/proxy.py b/faker/proxy.py
new file mode 100644
--- /dev/null
+++ b/faker/proxy.py
@@ -0,0 +1,236 @@
+# coding=utf-8
+
+from __future__ import absolute_import, unicode_literals
+
+from collections import OrderedDict
+import random
+import re
+import six
+
+from faker.config import DEFAULT_LOCALE
+from faker.factory import Factory
+from faker.generator import Generator
+from faker.utils.distribution import choices_distribution
+
+
+class Faker(object):
+    """Proxy class capable of supporting multiple locales"""
+
+    cache_pattern = re.compile(r'^_cached_\w*_mapping$')
+    generator_attrs = [
+        attr for attr in dir(Generator)
+        if not attr.startswith('__')
+        and attr not in ['seed', 'seed_instance', 'random']
+    ]
+
+    def __init__(self, locale=None, providers=None,
+                 generator=None, includes=None, **config):
+        self._factory_map = OrderedDict()
+        self._weights = None
+
+        if isinstance(locale, six.string_types):
+            locales = [locale.replace('-', '_')]
+
+        # This guarantees a FIFO ordering of elements in `locales` based on the final
+        # locale string while discarding duplicates after processing
+        elif isinstance(locale, (list, tuple, set)):
+            assert all(isinstance(l, six.string_types) for l in locale)
+            locales = []
+            for l in locale:
+                final_locale = l.replace('-', '_')
+                if final_locale not in locales:
+                    locales.append(final_locale)
+
+        elif isinstance(locale, OrderedDict):
+            assert all(isinstance(v, (int, float)) for v in locale.values())
+            odict = OrderedDict()
+            for k, v in locale.items():
+                key = k.replace('-', '_')
+                odict[key] = v
+            locales = list(odict.keys())
+            self._weights = list(odict.values())
+
+        else:
+            locales = [DEFAULT_LOCALE]
+
+        for locale in locales:
+            self._factory_map[locale] = Factory.create(locale, providers, generator, includes, **config)
+
+        self._locales = locales
+        self._factories = list(self._factory_map.values())
+
+    def __getitem__(self, locale):
+        return self._factory_map[locale.replace('-', '_')]
+
+    def __getattribute__(self, attr):
+        """
+        Handles the "attribute resolution" behavior for declared members of this proxy class
+
+        The class method `seed` cannot be called from an instance.
+
+        :param attr: attribute name
+        :return: the appropriate attribute
+        """
+        if attr == 'seed':
+            msg = (
+                'Calling `.seed()` on instances is deprecated. '
+                'Use the class method `Faker.seed()` instead.'
+            )
+            raise TypeError(msg)
+        else:
+            return super(Faker, self).__getattribute__(attr)
+
+    def __getattr__(self, attr):
+        """
+        Handles cache access and proxying behavior
+
+        :param attr: attribute name
+        :return: the appropriate attribute
+        """
+
+        if len(self._factories) == 1:
+            return getattr(self._factories[0], attr)
+        elif attr in self.generator_attrs:
+            msg = 'Proxying calls to `%s` is not implemented in multiple locale mode.' % attr
+            raise NotImplementedError(msg)
+        elif self.cache_pattern.match(attr):
+            msg = 'Cached attribute `%s` does not exist' % attr
+            raise AttributeError(msg)
+        else:
+            factory = self._select_factory(attr)
+            return getattr(factory, attr)
+
+    def _select_factory(self, method_name):
+        """
+        Returns a random factory that supports the provider method
+
+        :param method_name: Name of provider method
+        :return: A factory that supports the provider method
+        """
+
+        if len(self._factories) == 1:
+            return self._factories[0]
+
+        factories, weights = self._map_provider_method(method_name)
+        if len(factories) == 0:
+            msg = "No generator object has attribute '{}'".format(method_name)
+            raise AttributeError(msg)
+        elif len(factories) == 1:
+            return factories[0]
+
+        if weights:
+            factory = choices_distribution(factories, weights, length=1)[0]
+        else:
+            factory = random.choice(factories)
+        return factory
+
+    def _map_provider_method(self, method_name):
+        """
+        Creates a 2-tuple of factories and weights for the given provider method name
+
+        The first element of the tuple contains a list of compatible factories.
+        The second element of the tuple contains a list of distribution weights.
+
+        :param method_name: Name of provider method
+        :return: 2-tuple (factories, weights)
+        """
+
+        # Return cached mapping if it exists for given method
+        attr = '_cached_{}_mapping'.format(method_name)
+        if hasattr(self, attr):
+            return getattr(self, attr)
+
+        # Create mapping if it does not exist
+        if self._weights:
+            value = [
+                (factory, weight)
+                for factory, weight in zip(self.factories, self._weights)
+                if hasattr(factory, method_name)
+            ]
+            factories, weights = zip(*value)
+            mapping = list(factories), list(weights)
+        else:
+            value = [
+                factory
+                for factory in self.factories
+                if hasattr(factory, method_name)
+            ]
+            mapping = value, None
+
+        # Then cache and return results
+        setattr(self, attr, mapping)
+        return mapping
+
+    @classmethod
+    def seed(cls, seed=None):
+        """
+        Seeds the shared `random.Random` object across all factories
+
+        :param seed: seed value
+        """
+        Generator.seed(seed)
+
+    def seed_instance(self, seed=None):
+        """
+        Creates and seeds a new `random.Random` object for each factory
+
+        :param seed: seed value
+        """
+        for factory in self._factories:
+            factory.seed_instance(seed)
+
+    def seed_locale(self, locale, seed=None):
+        """
+        Creates and seeds a new `random.Random` object for the factory of the specified locale
+
+        :param locale: locale string
+        :param seed: seed value
+        """
+        self._factory_map[locale.replace('-', '_')].seed_instance(seed)
+
+    @property
+    def random(self):
+        """
+        Proxies `random` getter calls
+
+        In single locale mode, this will be proxied to the `random` getter
+        of the only internal `Generator` object. Subclasses will have to
+        implement desired behavior in multiple locale mode.
+        """
+
+        if len(self._factories) == 1:
+            return self._factories[0].random
+        else:
+            msg = 'Proxying `random` getter calls is not implemented in multiple locale mode.'
+            raise NotImplementedError(msg)
+
+    @random.setter
+    def random(self, value):
+        """
+        Proxies `random` setter calls
+
+        In single locale mode, this will be proxied to the `random` setter
+        of the only internal `Generator` object. Subclasses will have to
+        implement desired behavior in multiple locale mode.
+        """
+
+        if len(self._factories) == 1:
+            self._factories[0].random = value
+        else:
+            msg = 'Proxying `random` setter calls is not implemented in multiple locale mode.'
+            raise NotImplementedError(msg)
+
+    @property
+    def locales(self):
+        return list(self._locales)
+
+    @property
+    def weights(self):
+        return self._weights
+
+    @property
+    def factories(self):
+        return self._factories
+
+    def items(self):
+        return self._factory_map.items()


Test patch:
diff --git a/tests/providers/__init__.py b/tests/providers/__init__.py
--- a/tests/providers/__init__.py
+++ b/tests/providers/__init__.py
@@ -6,7 +6,7 @@
 class BaseProviderTestCase(unittest.TestCase):
     def setUp(self):
         self.factory = Faker('en_US')
-        self.factory.seed(0)
+        Faker.seed(0)
         self.provider = BaseProvider(generator=self.factory)
 
     def test_random_digit_or_empty(self):
@@ -14,7 +14,7 @@ def test_random_digit_or_empty(self):
         assert isinstance(ret, int)
         assert ret >= 0 and ret <= 9
 
-        self.factory.seed(1)
+        Faker.seed(1)
         assert self.provider.random_digit_or_empty() == ''
 
     def test_random_digit_not_null_or_empty(self):
@@ -22,7 +22,7 @@ def test_random_digit_not_null_or_empty(self):
         assert isinstance(ret, int)
         assert ret >= 0 and ret <= 9
 
-        self.factory.seed(1)
+        Faker.seed(1)
         assert self.provider.random_digit_not_null_or_empty() == ''
 
     def test_randomize_nb_elements(self):
diff --git a/tests/providers/test_color.py b/tests/providers/test_color.py
--- a/tests/providers/test_color.py
+++ b/tests/providers/test_color.py
@@ -54,7 +54,7 @@ def test_color(self):
 
         # The `color` provider method should behave like the `generate`
         # method of a standalone RandomColor instance for a given seed
-        self.factory.seed(4761)
+        Faker.seed(4761)
         colors = [self.factory.color() for _ in range(10000)]
         assert colors == expected
 
diff --git a/tests/providers/test_date_time.py b/tests/providers/test_date_time.py
--- a/tests/providers/test_date_time.py
+++ b/tests/providers/test_date_time.py
@@ -65,7 +65,7 @@ class TestDateTime(unittest.TestCase):
 
     def setUp(self):
         self.factory = Faker()
-        self.factory.seed(0)
+        Faker.seed(0)
 
     def assertBetween(self, date, start_date, end_date):
         assert date <= end_date
@@ -583,7 +583,7 @@ class DatesOfBirth(unittest.TestCase):
 
     def setUp(self):
         self.factory = Faker()
-        self.factory.seed(0)
+        Faker.seed(0)
 
     def test_date_of_birth(self):
         dob = self.factory.date_of_birth()
diff --git a/tests/providers/test_misc.py b/tests/providers/test_misc.py
--- a/tests/providers/test_misc.py
+++ b/tests/providers/test_misc.py
@@ -29,10 +29,10 @@ def test_uuid4_seedability(self):
         for _ in range(10):
             random_seed = self.factory.random_int()
             baseline_fake = Faker()
-            baseline_fake.seed(random_seed)
+            Faker.seed(random_seed)
             expected_uuids = [baseline_fake.uuid4() for i in range(1000)]
 
             new_fake = Faker()
-            new_fake.seed(random_seed)
+            Faker.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
--- a/tests/providers/test_python.py
+++ b/tests/providers/test_python.py
@@ -99,7 +99,7 @@ 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.object(self.factory['en_US'], 'foo') as mock_foo:
             with patch('faker.providers.BaseProvider.bothify',
                        wraps=self.factory.bothify) as mock_bothify:
                 mock_foo.return_value = 'barbar'
diff --git a/tests/providers/test_ssn.py b/tests/providers/test_ssn.py
--- a/tests/providers/test_ssn.py
+++ b/tests/providers/test_ssn.py
@@ -184,7 +184,7 @@ def test_police_id(self):
 class TestEnCA(unittest.TestCase):
     def setUp(self):
         self.factory = Faker('en_CA')
-        self.factory.seed(0)
+        Faker.seed(0)
 
     def test_ssn(self):
         for _ in range(100):
@@ -201,7 +201,7 @@ def test_ssn(self):
 class TestEnUS(unittest.TestCase):
     def setUp(self):
         self.factory = Faker('en_US')
-        self.factory.seed(0)
+        Faker.seed(0)
 
     def test_ssn(self):
         for _ in range(100):
@@ -335,7 +335,7 @@ def test_prohibited_ssn_value(self):
         # is one that would (if not specifically checked for) return an
         # SSN with an area of '666'.
 
-        self.factory.seed(19031)
+        Faker.seed(19031)
         ssn = self.factory.ssn()
         [area, group, serial] = ssn.split('-')
         assert area != '666'
@@ -663,7 +663,7 @@ def test_vat_id(self):
 class TestHuHU(unittest.TestCase):
     def setUp(self):
         self.factory = Faker('hu_HU')
-        self.factory.seed(0)
+        Faker.seed(0)
 
     def test_ssn(self):
         for _ in range(100):
diff --git a/tests/test_factory.py b/tests/test_factory.py
--- a/tests/test_factory.py
+++ b/tests/test_factory.py
@@ -18,6 +18,7 @@
 import six
 
 from faker import Faker, Generator
+from faker.factory import Factory
 from faker.generator import random
 from faker.utils import decorators, text
 
@@ -910,7 +911,7 @@ def test_random_number(self):
         assert len(str(number)) == 10
 
     def test_instance_seed_chain(self):
-        factory = Faker()
+        factory = Factory.create()
 
         names = ['Real Name0', 'Real Name1', 'Real Name2', 'Real Name0', 'Real Name2']
         anonymized = [factory.seed_instance(name).name() for name in names]
diff --git a/tests/test_proxy.py b/tests/test_proxy.py
new file mode 100644
--- /dev/null
+++ b/tests/test_proxy.py
@@ -0,0 +1,302 @@
+#  -*- coding: utf-8 -*-
+
+from __future__ import unicode_literals
+
+from collections import OrderedDict
+import unittest
+try:
+    from unittest.mock import patch, PropertyMock
+except ImportError:
+    from mock import patch, PropertyMock
+
+from faker import Faker
+from faker.config import DEFAULT_LOCALE
+from faker.generator import Generator
+
+
+class TestFakerProxyClass(unittest.TestCase):
+
+    def setUp(self):
+        self.faker = None
+
+    def test_unspecified_locale(self):
+        self.faker = Faker()
+        assert len(self.faker.locales) == 1
+        assert len(self.faker.factories) == 1
+        assert self.faker.locales[0] == DEFAULT_LOCALE
+
+    def test_locale_as_string(self):
+        locale = 'en_US'
+        self.faker = Faker()
+        assert len(self.faker.locales) == 1
+        assert len(self.faker.factories) == 1
+        assert self.faker.locales[0] == locale
+
+    def test_locale_as_list(self):
+        locale = ['en-US', 'en_PH', 'ja_JP', 'de-DE']
+        expected = ['en_US', 'en_PH', 'ja_JP', 'de_DE']
+        for _ in range(10):
+            self.faker = Faker(locale)
+            assert self.faker.locales == expected
+            assert len(self.faker.factories) == len(expected)
+
+        locale = ['en-US', 'en_PH', 'ja_JP', 'de-DE', 'ja-JP', 'de_DE', 'en-US'] * 3
+        expected = ['en_US', 'en_PH', 'ja_JP', 'de_DE']
+        for _ in range(10):
+            self.faker = Faker(locale)
+            assert self.faker.locales == expected
+            assert len(self.faker.factories) == len(expected)
+
+    def test_locale_as_ordereddict(self):
+        locale = OrderedDict([
+            ('de_DE', 3),
+            ('en-US', 2),
+            ('en-PH', 1),
+            ('ja_JP', 5),
+        ])
+
+        self.faker = Faker(locale)
+        assert len(self.faker.locales) == 4
+        assert self.faker.locales == ['de_DE', 'en_US', 'en_PH', 'ja_JP']
+        assert self.faker.weights == [3, 2, 1, 5]
+
+        locale = OrderedDict([
+            ('de_DE', 3),
+            ('en-US', 2),
+            ('en-PH', 1),
+            ('ja_JP', 5),
+            ('de-DE', 4),
+            ('ja-JP', 2),
+            ('en-US', 1),
+        ])
+        self.faker = Faker(locale)
+        assert self.faker.locales == ['de_DE', 'en_US', 'en_PH', 'ja_JP']
+        assert self.faker.weights == [4, 1, 1, 2]
+
+    def test_items(self):
+        locale = ['de_DE', 'en-US', 'en-PH', 'ja_JP', 'de-DE', 'ja-JP', 'en-US']
+        processed_locale = list({l.replace('-', '_') for l in locale})
+        self.faker = Faker(locale)
+        for locale_name, factory in self.faker.items():
+            assert locale_name in processed_locale
+            assert isinstance(factory, Generator)
+
+    def test_dunder_getitem(self):
+        locale = ['de_DE', 'en-US', 'en-PH', 'ja_JP']
+        self.faker = Faker(locale)
+
+        for l in locale:
+            assert isinstance(self.faker[l], Generator)
+
+        with self.assertRaises(KeyError):
+            self.faker['en_GB']
+
+    def test_seed_classmethod(self):
+        self.faker = Faker()
+
+        # Verify `seed()` is not callable from a class instance
+        with self.assertRaises(TypeError):
+            self.faker.seed(0)
+
+        # Verify calls to `seed()` from a class object are proxied properly
+        with patch('faker.generator.Generator.seed') as mock_seed:
+            mock_seed.assert_not_called()
+            Faker.seed(0)
+            mock_seed.assert_called_once_with(0)
+
+    def test_seed_instance(self):
+        locale = ['de_DE', 'en-US', 'en-PH', 'ja_JP']
+        self.faker = Faker(locale)
+
+        with patch('faker.generator.Generator.seed_instance') as mock_seed_instance:
+            mock_seed_instance.assert_not_called()
+            self.faker.seed_instance(0)
+
+            # Verify `seed_instance(0)` was called 4 times (one for each locale)
+            calls = mock_seed_instance.call_args_list
+            assert len(calls) == 4
+            for call in calls:
+                args, kwargs = call
+                assert args == (0, )
+                assert kwargs == {}
+
+    def test_seed_locale(self):
+        from faker.generator import random as shared_random_instance
+
+        locale = ['de_DE', 'en-US', 'en-PH', 'ja_JP']
+        self.faker = Faker(locale)
+
+        # Get current state of each factory's random instance
+        states = {}
+        for locale, factory in self.faker.items():
+            states[locale] = factory.random.getstate()
+
+        # Create a new random instance for en_US factory with seed value
+        self.faker.seed_locale('en_US', 0)
+
+        for locale, factory in self.faker.items():
+            # en_US factory should have changed
+            if locale == 'en_US':
+                assert factory.random != shared_random_instance
+                assert factory.random.getstate() != states[locale]
+
+            # There should be no changes for the rest
+            else:
+                assert factory.random == shared_random_instance
+                assert factory.random.getstate() == states[locale]
+
+    def test_single_locale_proxy_behavior(self):
+        self.faker = Faker()
+        internal_factory = self.faker.factories[0]
+
+        # Test if `Generator` attributes are proxied properly
+        for attr in self.faker.generator_attrs:
+            assert getattr(self.faker, attr) == getattr(internal_factory, attr)
+
+        # Test if `random` getter and setter are proxied properly
+        tmp_random = self.faker.random
+        assert internal_factory.random != 1
+        self.faker.random = 1
+        assert internal_factory.random == 1
+        self.faker.random = tmp_random
+
+        # Test if a valid provider method is proxied properly
+        # Factory selection logic should not be triggered
+        with patch('faker.proxy.Faker._select_factory') as mock_select_factory:
+            mock_select_factory.assert_not_called()
+            assert self.faker.name == internal_factory.name
+            self.faker.name()
+            mock_select_factory.assert_not_called()
+
+    def test_multiple_locale_proxy_behavior(self):
+        self.faker = Faker(['de-DE', 'en-US', 'en-PH', 'ja-JP'])
+
+        # `Generator` attributes are not implemented
+        for attr in self.faker.generator_attrs:
+            with self.assertRaises(NotImplementedError):
+                getattr(self.faker, attr)
+
+        # The `random` getter is not implemented
+        with self.assertRaises(NotImplementedError):
+            random = self.faker.random
+            random.seed(0)
+
+        # The `random` setter is not implemented
+        with self.assertRaises(NotImplementedError):
+            self.faker.random = 1
+
+    def test_multiple_locale_caching_behavior(self):
+        self.faker = Faker(['de_DE', 'en-US', 'en-PH', 'ja_JP'])
+
+        with patch('faker.proxy.Faker._map_provider_method',
+                   wraps=self.faker._map_provider_method) as mock_map_method:
+            mock_map_method.assert_not_called()
+            assert not hasattr(self.faker, '_cached_name_mapping')
+
+            # Test cache creation
+            self.faker.name()
+            assert hasattr(self.faker, '_cached_name_mapping')
+            mock_map_method.assert_called_once_with('name')
+
+            # Test subsequent cache access
+            with patch.object(Faker, '_cached_name_mapping', create=True,
+                              new_callable=PropertyMock) as mock_cached_map:
+                # Keep test fast by patching the cached mapping to return something simpler
+                mock_cached_map.return_value = [self.faker['en_US']], [1]
+                for _ in range(100):
+                    self.faker.name()
+
+                # Python's hasattr() internally calls getattr()
+                # So each call to name() accesses the cached mapping twice
+                assert mock_cached_map.call_count == 200
+
+    @patch('faker.proxy.random.choice')
+    @patch('faker.proxy.choices_distribution')
+    def test_multiple_locale_factory_selection_no_weights(self, mock_choices_fn, mock_random_choice):
+        self.faker = Faker(['de_DE', 'en-US', 'en-PH', 'ja_JP'])
+
+        # There are no distribution weights, so factory selection logic will use `random.choice`
+        # if multiple factories have the specified provider method
+        with patch('faker.proxy.Faker._select_factory',
+                   wraps=self.faker._select_factory) as mock_select_factory:
+            mock_select_factory.assert_not_called()
+            mock_choices_fn.assert_not_called()
+            mock_random_choice.assert_not_called()
+
+            # All factories for the listed locales have the `name` provider method
+            self.faker.name()
+            mock_select_factory.assert_called_once_with('name')
+            mock_choices_fn.assert_not_called()
+            mock_random_choice.assert_called_once_with(self.faker.factories)
+            mock_select_factory.reset_mock()
+            mock_choices_fn.reset_mock()
+            mock_random_choice.reset_mock()
+
+            # Only `en_PH` factory has provider method `luzon_province`, so there is no
+            # need for `random.choice` factory selection logic to run
+            self.faker.luzon_province()
+            mock_select_factory.assert_called_with('luzon_province')
+            mock_choices_fn.assert_not_called()
+            mock_random_choice.assert_not_called()
+            mock_select_factory.reset_mock()
+            mock_choices_fn.reset_mock()
+            mock_random_choice.reset_mock()
+
+            # Both `en_US` and `ja_JP` factories have provider method `zipcode`
+            self.faker.zipcode()
+            mock_select_factory.assert_called_once_with('zipcode')
+            mock_choices_fn.assert_not_called()
+            mock_random_choice.assert_called_once_with(
+                [self.faker['en_US'], self.faker['ja_JP']],
+            )
+            mock_select_factory.reset_mock()
+            mock_choices_fn.reset_mock()
+            mock_random_choice.reset_mock()
+
+    @patch('faker.proxy.random.choice')
+    @patch('faker.proxy.choices_distribution')
+    def test_multiple_locale_factory_selection_with_weights(self, mock_choices_fn, mock_random_choice):
+        locale = OrderedDict([
+            ('de_DE', 3),
+            ('en-US', 2),
+            ('en-PH', 1),
+            ('ja_JP', 5),
+        ])
+        self.faker = Faker(locale)
+        mock_choices_fn.assert_not_called()
+        mock_random_choice.assert_not_called()
+
+        # Distribution weights have been specified, so factory selection logic will use
+        # `choices_distribution` if multiple factories have the specified provider method
+        with patch('faker.proxy.Faker._select_factory',
+                   wraps=self.faker._select_factory) as mock_select_factory:
+
+            # All factories for the listed locales have the `name` provider method
+            self.faker.name()
+            mock_select_factory.assert_called_once_with('name')
+            mock_choices_fn.assert_called_once_with(self.faker.factories, self.faker.weights, length=1)
+            mock_random_choice.assert_not_called()
+            mock_select_factory.reset_mock()
+            mock_choices_fn.reset_mock()
+            mock_random_choice.reset_mock()
+
+            # Only `en_PH` factory has provider method `luzon_province`, so there is no
+            # need for `choices_distribution` factory selection logic to run
+            self.faker.luzon_province()
+            mock_select_factory.assert_called_once_with('luzon_province')
+            mock_choices_fn.assert_not_called()
+            mock_random_choice.assert_not_called()
+            mock_select_factory.reset_mock()
+            mock_choices_fn.reset_mock()
+            mock_random_choice.reset_mock()
+
+            # Both `en_US` and `ja_JP` factories have provider method `zipcode`
+            self.faker.zipcode()
+            mock_select_factory.assert_called_once_with('zipcode')
+            mock_choices_fn.assert_called_once_with(
+                [self.faker['en_US'], self.faker['ja_JP']], [2, 5], length=1,
+            )
+            mock_random_choice.assert_not_called()
+            mock_select_factory.reset_mock()
+            mock_choices_fn.reset_mock()
+            mock_random_choice.reset_mock()