Repository: joke2k/faker

Issue:
Some currency symbols missing from currency provider
* Faker version: v4.0.1 onwards
* OS: Archlinux

Some currencies do not have any symbols associated with them.

### Steps to reproduce

```
from faker import Faker
fake = Faker()
currency = fake.currency()  # returns ("GGP", "Guernsey pound")
currency = ("GGP", "Guernsey pound")
symbol = fake.currency_symbol(code=currency[0])
print(symbol)
```

### Expected behavior

```
# should print the pound sign
'£'
```

### Actual behavior

```
faker/providers/currency/__init__.py", line 326, in currency_symbol
    return self.currency_symbols[code]
 KeyError: 'GGP'
```



Discussion:

Patch:
diff --git a/.bumpversion.cfg b/.bumpversion.cfg
index 02941076..00918578 100644
--- a/.bumpversion.cfg
+++ b/.bumpversion.cfg
@@ -1,5 +1,5 @@
 [bumpversion]
-current_version = 18.3.2
+current_version = 18.3.4
 files = VERSION faker/__init__.py docs/conf.py
 commit = True
 tag = True
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 729b7d05..39a36dfa 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,13 @@
 ## Changelog
 
+### [v18.3.4 - 2023-04-04](https://github.com/joke2k/faker/compare/v18.3.3...v18.3.4)
+
+* Ensure `pyfloat` honors min and max values. Thanks @mvanderlee.
+
+### [v18.3.3 - 2023-04-04](https://github.com/joke2k/faker/compare/v18.3.2...v18.3.3)
+
+* Use correct union type for provider argument (#1840). Thanks @DanielSchaffer.
+
 ### [v18.3.2 - 2023-04-01](https://github.com/joke2k/faker/compare/v18.3.1...v18.3.2)
 
 * Bump actions/stale from 7 to 8 (#1833). Thanks @dependabot[bot].
diff --git a/README.rst b/README.rst
index f383d396..9fa5472e 100644
--- a/README.rst
+++ b/README.rst
@@ -381,7 +381,7 @@ Seeding the Generator
 
 When using Faker for unit testing, you will often want to generate the same
 data set. For convenience, the generator also provide a ``seed()`` method,
-which seeds the shared random number generator. Seed produces the same result 
+which seeds the shared random number generator. Seed produces the same result
 when the same methods with the same version of faker are called.
 
 .. code:: python
diff --git a/VERSION b/VERSION
index a19b2d9a..0548779e 100644
--- a/VERSION
+++ b/VERSION
@@ -1,1 +1,1 @@
-18.3.2
+18.3.4
diff --git a/docs/conf.py b/docs/conf.py
index 6216e232..fe20f861 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 = "18.3.2"
+version = "18.3.4"
 # The full version, including alpha/beta/rc tags.
-release = "18.3.2"
+release = "18.3.4"
 
 # 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 7ae9788d..27fea391 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 = "18.3.2"
+VERSION = "18.3.4"
 
 __all__ = ("Factory", "Generator", "Faker")
diff --git a/faker/generator.py b/faker/generator.py
index 2e5dc250..5eda9648 100644
--- a/faker/generator.py
+++ b/faker/generator.py
@@ -1,7 +1,7 @@
 import random as random_module
 import re
 
-from typing import TYPE_CHECKING, Any, Callable, Dict, Hashable, List, Optional
+from typing import TYPE_CHECKING, Any, Callable, Dict, Hashable, List, Optional, Type, Union
 
 from .typing import SeedType
 
@@ -29,7 +29,7 @@ class Generator:
         self.__config = dict(list(self.__config.items()) + list(config.items()))
         self.__random = random
 
-    def add_provider(self, provider: "BaseProvider") -> None:
+    def add_provider(self, provider: Union["BaseProvider", Type["BaseProvider"]]) -> None:
         if isinstance(provider, type):
             provider = provider(self)
 
@@ -97,12 +97,12 @@ class Generator:
                 raise AttributeError(f"Unknown formatter {formatter!r}")
             raise AttributeError(msg)
 
-    def set_formatter(self, name: str, method: Callable) -> None:
+    def set_formatter(self, name: str, formatter: Callable) -> None:
         """
         This method adds a provider method to generator.
         Override this method to add some decoration or logging stuff.
         """
-        setattr(self, name, method)
+        setattr(self, name, formatter)
 
     def set_arguments(self, group: str, argument: str, value: Optional[Any] = None) -> None:
         """
diff --git a/faker/providers/currency/__init__.py b/faker/providers/currency/__init__.py
index 21ac5d16..1492d2ed 100644
--- a/faker/providers/currency/__init__.py
+++ b/faker/providers/currency/__init__.py
@@ -221,88 +221,173 @@ class Provider(BaseProvider):
         ("ADA", "Cardano"),
     )
 
-    # List of currency symbols in Unicode, source: https://www.unicode.org/charts/beta/nameslist/n_20A0.html
+    # List of currency symbols
+    # source: https://en.wikipedia.org/wiki/Currency_symbol
     currency_symbols: Dict[str, str] = {
+        "AED": "\u002e\u062f\u002e\u0625",
         "AFN": "\u060B",
+        "ALL": "Lek",
+        "AMD": "\u058F",
         "ANG": "\u0192",
+        "AOA": "Kz",
         "ARS": "\u0024",
         "AUD": "\u0024",
         "AWG": "\u0192",
+        "AZN": "\u20bc",
+        "BAM": "KM",
         "BBD": "\u0024",
         "BDT": "\u09F3",
+        "BGN": "Lev",
+        "BHD": "\u062F\u0628",
+        "BIF": "Fr",
         "BMD": "\u0024",
         "BND": "\u0024",
         "BOB": "\u0024",
         "BRL": "\u0024",
         "BSD": "\u0024",
+        "BTN": "Nu",
+        "BWP": "P",
+        "BYR": "R",
         "BZD": "\u0024",
         "CAD": "\u0024",
+        "CDF": "Fr",
+        "CHF": "Fr",
         "CLP": "\u0024",
         "CNY": "\u00A5",
         "COP": "\u0024",
         "CRC": "\u20A1",
+        "CUC": "\u0024",
         "CUP": "\u0024",
         "CVE": "\u0024",
+        "CZK": "\u004b\u010d\u0073",
+        "DJF": "Fr",
+        "DKK": "kr",
         "DOP": "\u0024",
+        "DZD": "\u062f\u062c\u200e",
         "EGP": "\u00A3",
+        "ERN": "Nfk",
+        "ETB": "Br",
         "EUR": "\u20AC",
         "FJD": "\u0024",
         "FKP": "\u00A3",
         "GBP": "\u00A3",
+        "GEL": "\u20BE",
+        "GGP": "\u00A3",
         "GHS": "\u20B5",
         "GIP": "\u00A3",
+        "GMD": "D",
+        "GNF": "FG",
+        "GTQ": "Q",
         "GYD": "\u0024",
         "HKD": "\u0024",
-        "HUF": "\u0192",
-        "IDR": "\u20A8",
+        "HNL": "L",
+        "HRK": "kn",
+        "HTG": "G",
+        "HUF": "Ft",
+        "IDR": "Rp",
         "ILS": "\u20AA",
+        "IMP": "\u00A3",
         "INR": "\u20B9",
+        "IQD": "\u062F\u0639",
         "IRR": "\uFDFC",
+        "ISK": "kr",
+        "JEP": "\u00A3",
         "JMD": "\u0024",
+        "JOD": "JD",
         "JPY": "\u00A5",
+        "KES": "KSh",
+        "KGS": "\u20C0",
         "KHR": "\u17DB",
+        "KMF": "FC",
         "KPW": "\u20A9",
         "KRW": "\u20A9",
+        "KWD": "KD",
         "KYD": "\u0024",
         "KZT": "\u20B8",
         "LAK": "\u20AD",
         "LBP": "\u00A3",
         "LKR": "\u20A8",
         "LRD": "\u0024",
+        "LSL": "M",
+        "LTL": "L",
+        "LYD": "LD",
+        "MAD": "Dhs",
+        "MDL": "leu",
+        "MGA": "Ar",
+        "MKD": "DEN",
+        "MMK": "Ks",
         "MNT": "\u20AE",
         "MOP": "\u0024",
+        "MRO": "UM",
         "MUR": "\u20A8",
+        "MVR": "\u0078",
+        "MWK": "K",
         "MXN": "\u0024",
+        "MYR": "RM",
+        "MZN": "Mt",
         "NAD": "\u0024",
         "NGN": "\u20A6",
         "NIO": "\u0024",
+        "NIS": "\u20AA",
+        "NOK": "kr",
         "NPR": "\u20A8",
         "NZD": "\u0024",
         "OMR": "\uFDFC",
+        "PAB": "B/",
+        "PEN": "S/",
+        "PGK": "K",
         "PHP": "\u20B1",
         "PKR": "\u20A8",
+        "PLN": "\u007a\u0142",
         "PYG": "\u20B2",
         "QAR": "\uFDFC",
+        "RON": "leu",
+        "RSD": "\u0434\u0438\u043d",
         "RUB": "\u20BD",
+        "RWF": "F",
         "SAR": "\uFDFC",
         "SBD": "\u0024",
+        "SCR": "\u20A8",
         "SDG": "\u00A3",
+        "SEK": "kr",
         "SGD": "\u0024",
         "SHP": "\u00A3",
+        "SLL": "Le",
+        "SOS": "Sh.So.",
+        "SPL": "L",
         "SRD": "\u0024",
+        "STD": "Db",
+        "SVC": "\u20A1",
         "SYP": "\u00A3",
+        "SZL": "E",
         "THB": "\u0E3F",
-        "TOP": "\u0024",
+        "TJS": "SM",
+        "TMT": "m",
+        "TND": "DT",
+        "TOP": "\u00a2",
         "TRY": "\u20BA",
         "TTD": "\u0024",
+        "TVD": "\u0024",
         "TWD": "\u0024",
+        "TWD": "\u0024",
+        "TZS": "Tsh",
         "UAH": "\u20B4",
+        "UGX": "USh",
         "USD": "\u0024",
-        "UY": "\u0024",
+        "UYU": "\u0024",
+        "UZS": "\u043b\u0432",
+        "VEF": "\u0042\u0073",
         "VND": "\u20AB",
+        "VUV": "VT",
         "WST": "\u0024",
+        "XAF": "Fr",
         "XCD": "\u0024",
+        "XDR": "SDR",
+        "XOF": "Fr",
+        "XPF": "Fr",
         "YER": "\uFDFC",
+        "ZAR": "R",
+        "ZMW": "K",
         "ZWD": "\u0024",
     }
 
@@ -323,7 +408,9 @@ class Provider(BaseProvider):
         """
         if code is None:
             code = self.random_element(self.currency_symbols.keys())
-        return self.currency_symbols[code]
+        elif code not in [currency[0] for currency in self.currencies]:
+            raise KeyError("The supplied code is not valid")
+        return self.currency_symbols.get(code, "\u00A4")
 
     def cryptocurrency(self) -> Tuple[str, str]:
         return self.random_element(self.cryptocurrencies)
diff --git a/faker/providers/python/__init__.py b/faker/providers/python/__init__.py
index 15aa8257..3cf61157 100644
--- a/faker/providers/python/__init__.py
+++ b/faker/providers/python/__init__.py
@@ -178,20 +178,23 @@ class Provider(BaseProvider):
 
         sign = ""
         if (min_value is not None) or (max_value is not None):
+            # Copy values to ensure we're not modifying the original values and thus going out of bounds
+            left_min_value = min_value
+            left_max_value = max_value
             # Make sure left_digits still respected
             if left_digits is not None:
                 if max_value is None:
-                    max_value = 10**left_digits  # minus smallest representable, adjusted later
+                    left_max_value = 10**left_digits  # minus smallest representable, adjusted later
                 if min_value is None:
-                    min_value = -(10**left_digits)  # plus smallest representable, adjusted later
+                    left_min_value = -(10**left_digits)  # plus smallest representable, adjusted later
 
             if max_value is not None and max_value < 0:
-                max_value += 1  # as the random_int will be generated up to max_value - 1
+                left_max_value += 1  # as the random_int will be generated up to max_value - 1
             if min_value is not None and min_value < 0:
-                min_value += 1  # as we then append digits after the left_number
+                left_min_value += 1  # as we then append digits after the left_number
             left_number = self._safe_random_int(
-                min_value,
-                max_value,
+                left_min_value,
+                left_max_value,
                 positive,
             )
         else:
@@ -212,11 +215,17 @@ class Provider(BaseProvider):
             result = min(result, 10**left_digits - 1)
             result = max(result, -(10**left_digits + 1))
 
-        # It's possible for the result to end up > than max_value
-        # This is a quick hack to ensure result is always smaller.
+        # It's possible for the result to end up > than max_value or < than min_value
+        # When this happens we introduce some variance so we're not always the exactly the min_value or max_value.
+        # Which can happen a lot depending on the difference of the values.
+        # Ensure the variance is bound by the difference between the max and min
         if max_value is not None:
             if result > max_value:
-                result = result - (result - max_value)
+                result = result - (result - max_value + self.generator.random.uniform(0, max_value - min_value))
+        if min_value is not None:
+            if result < min_value:
+                result = result + (min_value - result + self.generator.random.uniform(0, max_value - min_value))
+
         return result
 
     def _safe_random_int(self, min_value: float, max_value: float, positive: bool) -> int:
diff --git a/faker/proxy.py b/faker/proxy.py
index 421da0b4..78f159bc 100644
--- a/faker/proxy.py
+++ b/faker/proxy.py
@@ -46,7 +46,7 @@ class Faker:
             locales = []
             for code in locale:
                 if not isinstance(code, str):
-                    raise TypeError('The locale "%s" must be a string.' % str(code))
+                    raise TypeError(f'The locale "{str(code)}" must be a string.')
                 final_locale = code.replace("-", "_")
                 if final_locale not in locales:
                     locales.append(final_locale)


Test patch:
diff --git a/tests/providers/test_currency.py b/tests/providers/test_currency.py
index b04685e8..751117e7 100644
--- a/tests/providers/test_currency.py
+++ b/tests/providers/test_currency.py
@@ -34,6 +34,12 @@ class TestCurrencyProvider:
             name = faker.currency_name()
             assert isinstance(name, str) and name in self.currency_names
 
+    def test_currency_code_has_symbol(self, faker, num_samples):
+        for _ in range(num_samples):
+            code = faker.currency_code()
+            symbol = faker.currency_symbol(code=code)
+            assert isinstance(symbol, str)
+
     def test_currency_symbol_no_code_supplied(self, faker, num_samples):
         for _ in range(num_samples):
             symbol = faker.currency_symbol()
diff --git a/tests/providers/test_python.py b/tests/providers/test_python.py
index ff30a279..27a2a69e 100644
--- a/tests/providers/test_python.py
+++ b/tests/providers/test_python.py
@@ -199,6 +199,17 @@ class TestPyfloat(unittest.TestCase):
         self.assertLessEqual(result, 100)
         self.assertGreater(result, 0)
 
+    def test_max_and_min_value_positive_with_decimals(self):
+        """
+        Combining the max_value and min_value keyword arguments with
+        positive values for each produces numbers that obey both of
+        those constraints.
+        """
+        for _ in range(1000):
+            result = self.fake.pyfloat(min_value=100.123, max_value=200.321)
+            self.assertLessEqual(result, 200.321)
+            self.assertGreaterEqual(result, 100.123)
+
     def test_max_and_min_value_negative(self):
         """
         Combining the max_value and min_value keyword arguments with
@@ -210,6 +221,17 @@ class TestPyfloat(unittest.TestCase):
         self.assertLessEqual(result, -100)
         self.assertGreaterEqual(result, -200)
 
+    def test_max_and_min_value_negative_with_decimals(self):
+        """
+        Combining the max_value and min_value keyword arguments with
+        negative values for each produces numbers that obey both of
+        those constraints.
+        """
+        for _ in range(1000):
+            result = self.fake.pyfloat(max_value=-100.123, min_value=-200.321)
+            self.assertLessEqual(result, -100.123)
+            self.assertGreaterEqual(result, -200.321)
+
     def test_positive_and_min_value_incompatible(self):
         """
         An exception should be raised if positive=True is set, but