Repository: joke2k/faker

Issue:
Incorrect language set on Provider during locale fallback
* Faker version: 11.3.0

When faker is initialised with a locale not implemented by some of the built-in providers, it falls back to the DEFAULT_LOCALE for these providers. However, the language set on the provider’s instance in `__lang__` is still the requested locale and not the locale in effect.

This is due to `Factory._find_provider_class` not returning the locale it chose, in: https://github.com/joke2k/faker/blob/001ddee39c33b6b82196fe6a5ecc131bca3b964c/faker/factory.py#L102-L104 to `Factory._get_provider_class` which then proceeds to return the locale value as it was passed in at the first place. Thus, `provider.__lang__` does not contain the found locale (as the variable name `lang_found` would suggest): https://github.com/joke2k/faker/blob/001ddee39c33b6b82196fe6a5ecc131bca3b964c/faker/factory.py#L61

### Expected behavior

`provider.__lang__` should be set to the actual language / locale being used by the provider.

### Actual behavior

`provider.__lang__` is set to the locale that was requested but is not offered by this provider.

Comments:

Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 3a5d9a99..1762e314 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -128,9 +128,16 @@ jobs:
           python-version: ${{ matrix.python }}
       - name: Install Tox and any other packages
         run: python -m pip install tox coveralls
-      - name: Run Tox
-        # Run tox using the version of Python in `PATH`
-        run: tox -e py
+      - name: pytest
+        uses: liskin/gh-problem-matcher-wrap@v1
+        with:
+          linters: pytest
+          run: tox -e py
+        env:
+          COVERALLS_PARALLEL: true
+          COVERALLS_FLAG_NAME: run-ubuntu-${{ matrix.python }}
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+          COVERALLS_SERVICE_NAME: github
       - name: Publish coverage
         run: coveralls --service=github
         env:
diff --git a/faker/documentor.py b/faker/documentor.py
index ba72de21..5164bf7c 100644
--- a/faker/documentor.py
+++ b/faker/documentor.py
@@ -30,7 +30,7 @@ class Documentor:
         formatters = []
         providers: List[BaseProvider] = self.generator.get_providers()
         for provider in providers[::-1]:  # reverse
-            if locale and provider.__lang__ != locale:
+            if locale and provider.__lang__ and provider.__lang__ != locale:
                 continue
             formatters.append(
                 (provider, self.get_provider_formatters(provider, **kwargs)),
diff --git a/faker/factory.py b/faker/factory.py
index 4d1a66dd..56f47748 100644
--- a/faker/factory.py
+++ b/faker/factory.py
@@ -54,7 +54,7 @@ class Factory:
             if prov_name == "faker.providers":
                 continue
 
-            prov_cls, lang_found = cls._get_provider_class(prov_name, locale)
+            prov_cls, lang_found, _ = cls._find_provider_class(prov_name, locale)
             provider = prov_cls(faker)
             provider.__use_weighting__ = use_weighting
             provider.__provider__ = prov_name
@@ -64,31 +64,14 @@ class Factory:
         return faker
 
     @classmethod
-    def _get_provider_class(cls, provider: str, locale: Optional[str] = "") -> Tuple[Any, Optional[str]]:
-
-        provider_class = cls._find_provider_class(provider, locale)
-
-        if provider_class:
-            return provider_class, locale
-
-        if locale and locale != DEFAULT_LOCALE:
-            # fallback to default locale
-            provider_class = cls._find_provider_class(provider, DEFAULT_LOCALE)
-            if provider_class:
-                return provider_class, DEFAULT_LOCALE
-
-        # fallback to no locale
-        provider_class = cls._find_provider_class(provider)
-        if provider_class:
-            return provider_class, None
-
-        msg = f"Unable to find provider `{provider}` with locale `{locale}`"
-        raise ValueError(msg)
-
-    @classmethod
-    def _find_provider_class(cls, provider_path: str, locale: Optional[str] = None) -> Any:
+    def _find_provider_class(
+        cls,
+        provider_path: str,
+        locale: Optional[str] = None,
+    ) -> Tuple[Any, Optional[str], Optional[str]]:
 
         provider_module = import_module(provider_path)
+        default_locale = getattr(provider_module, "default_locale", "")
 
         if getattr(provider_module, "localized", False):
 
@@ -101,7 +84,7 @@ class Factory:
             available_locales = list_module(provider_module)
             if not locale or locale not in available_locales:
                 unavailable_locale = locale
-                locale = getattr(provider_module, "default_locale", DEFAULT_LOCALE)
+                locale = default_locale or DEFAULT_LOCALE
                 logger.debug(
                     "Specified locale `%s` is not available for "
                     "provider `%s`. Locale reset to `%s` for this "
@@ -122,15 +105,14 @@ class Factory:
 
         else:
 
-            logger.debug(
-                "Provider `%s` does not feature localization. "
-                "Specified locale `%s` is not utilized for this "
-                "provider.",
-                provider_module.__name__,
-                locale,
-            )
-
-            if locale is not None:
-                provider_module = import_module(provider_path)
+            if locale:
+                logger.debug(
+                    "Provider `%s` does not feature localization. "
+                    "Specified locale `%s` is not utilized for this "
+                    "provider.",
+                    provider_module.__name__,
+                    locale,
+                )
+            locale = default_locale = None
 
-        return provider_module.Provider  # type: ignore
+        return provider_module.Provider, locale, default_locale  # type: ignore


Test patch:
diff --git a/tests/test_factory.py b/tests/test_factory.py
index 1b119923..48c9015e 100644
--- a/tests/test_factory.py
+++ b/tests/test_factory.py
@@ -3,6 +3,8 @@ import string
 import sys
 import unittest
 
+from unittest.mock import MagicMock, patch
+
 import pytest
 
 from faker import Faker, Generator
@@ -108,6 +110,74 @@ class FactoryTestCase(unittest.TestCase):
         finally:
             sys.stdout = orig_stdout
 
+    def test_unknown_provider(self):
+        with pytest.raises(ModuleNotFoundError) as excinfo:
+            Factory.create(providers=["dummy_provider"])
+        assert str(excinfo.value) == "No module named 'dummy_provider'"
+
+        with pytest.raises(ModuleNotFoundError) as excinfo:
+            Factory.create(providers=["dummy_provider"], locale="it_IT")
+        assert str(excinfo.value) == "No module named 'dummy_provider'"
+
+    def test_unknown_locale(self):
+        with pytest.raises(AttributeError) as excinfo:
+            Factory.create(locale="77")
+        assert str(excinfo.value) == "Invalid configuration for faker locale `77`"
+
+        with pytest.raises(AttributeError) as excinfo:
+            Factory.create(locale="77_US")
+        assert str(excinfo.value) == "Invalid configuration for faker locale `77_US`"
+
+    def test_lang_unlocalized_provider(self):
+        for locale in (None, "", "en_GB", "it_IT"):
+            factory = Factory.create(providers=["faker.providers.file"], locale=locale)
+            assert len(factory.providers) == 1
+            assert factory.providers[0].__provider__ == "faker.providers.file"
+            assert factory.providers[0].__lang__ is None
+
+    def test_lang_localized_provider(self, with_default=True):
+        class DummyProviderModule:
+            localized = True
+
+            def __init__(self):
+                if with_default:
+                    self.default_locale = "ar_EG"
+
+            @property
+            def __name__(self):
+                return self.__class__.__name__
+
+            class Provider:
+                def __init__(self, *args, **kwargs):
+                    pass
+
+        with patch.multiple(
+            "faker.factory",
+            import_module=MagicMock(return_value=DummyProviderModule()),
+            list_module=MagicMock(return_value=("en_GB", "it_IT")),
+            DEFAULT_LOCALE="ko_KR",
+        ):
+            test_cases = [
+                (None, False),
+                ("", False),
+                ("ar", False),
+                ("es_CO", False),
+                ("en", False),
+                ("en_GB", True),
+                ("ar_EG", with_default),  # True if module defines a default locale
+            ]
+            for locale, expected_used in test_cases:
+                factory = Factory.create(providers=["dummy"], locale=locale)
+                assert factory.providers[0].__provider__ == "dummy"
+                from faker.config import DEFAULT_LOCALE
+
+                print(f"requested locale = {locale} , DEFAULT LOCALE {DEFAULT_LOCALE}")
+                expected_locale = locale if expected_used else ("ar_EG" if with_default else "ko_KR")
+                assert factory.providers[0].__lang__ == expected_locale
+
+    def test_lang_localized_provider_without_default(self):
+        self.test_lang_localized_provider(with_default=False)
+
     def test_slugify(self):
         slug = text.slugify("a'b/c")
         assert slug == "abc"
diff --git a/tests/test_providers_formats.py b/tests/test_providers_formats.py
index e4ebd4c2..8d0e0293 100644
--- a/tests/test_providers_formats.py
+++ b/tests/test_providers_formats.py
@@ -3,7 +3,7 @@ import re
 import pytest
 
 from faker import Factory
-from faker.config import AVAILABLE_LOCALES, PROVIDERS
+from faker.config import AVAILABLE_LOCALES, DEFAULT_LOCALE, PROVIDERS
 
 locales = AVAILABLE_LOCALES
 
@@ -26,8 +26,16 @@ def test_no_invalid_formats(locale):
     for provider in PROVIDERS:
         if provider == "faker.providers":
             continue
-        prov_cls, lang = Factory._get_provider_class(provider, locale)
-        assert lang == locale
+        prov_cls, lang, default_lang = Factory._find_provider_class(provider, locale)
+        if default_lang is None:
+            # for non-localized providers, the discovered language will be None
+            assert lang is None
+        else:
+            # for localized providers, the discovered language will either be
+            # the requested one
+            # or the default language of the provider
+            # or the fallback locale
+            assert lang in (locale, default_lang or DEFAULT_LOCALE)
 
         attributes = set(dir(prov_cls))