proxy.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317
  1. import copy
  2. import functools
  3. import random
  4. import re
  5. from collections import OrderedDict
  6. from random import Random
  7. from typing import Any, Callable, Dict, Hashable, List, Optional, Pattern, Sequence, Tuple, Union
  8. from .config import DEFAULT_LOCALE
  9. from .exceptions import UniquenessException
  10. from .factory import Factory
  11. from .generator import Generator
  12. from .utils.distribution import choices_distribution
  13. _UNIQUE_ATTEMPTS = 1000
  14. class Faker:
  15. """Proxy class capable of supporting multiple locales"""
  16. cache_pattern: Pattern = re.compile(r"^_cached_\w*_mapping$")
  17. generator_attrs = [
  18. attr for attr in dir(Generator) if not attr.startswith("__") and attr not in ["seed", "seed_instance", "random"]
  19. ]
  20. def __init__(
  21. self,
  22. locale: Optional[Union[str, Sequence[str], Dict[str, Union[int, float]]]] = None,
  23. providers: Optional[List[str]] = None,
  24. generator: Optional[Generator] = None,
  25. includes: Optional[List[str]] = None,
  26. use_weighting: bool = True,
  27. **config: Any,
  28. ) -> None:
  29. self._factory_map = OrderedDict()
  30. self._weights = None
  31. self._unique_proxy = UniqueProxy(self)
  32. if isinstance(locale, str):
  33. locales = [locale.replace("-", "_")]
  34. # This guarantees a FIFO ordering of elements in `locales` based on the final
  35. # locale string while discarding duplicates after processing
  36. elif isinstance(locale, (list, tuple, set)):
  37. locales = []
  38. for code in locale:
  39. if not isinstance(code, str):
  40. raise TypeError('The locale "%s" must be a string.' % str(code))
  41. final_locale = code.replace("-", "_")
  42. if final_locale not in locales:
  43. locales.append(final_locale)
  44. elif isinstance(locale, OrderedDict):
  45. assert all(isinstance(v, (int, float)) for v in locale.values())
  46. odict = OrderedDict()
  47. for k, v in locale.items():
  48. key = k.replace("-", "_")
  49. odict[key] = v
  50. locales = list(odict.keys())
  51. self._weights = list(odict.values())
  52. else:
  53. locales = [DEFAULT_LOCALE]
  54. for locale in locales:
  55. self._factory_map[locale] = Factory.create(
  56. locale,
  57. providers,
  58. generator,
  59. includes,
  60. use_weighting=use_weighting,
  61. **config,
  62. )
  63. self._locales = locales
  64. self._factories = list(self._factory_map.values())
  65. def __dir__(self):
  66. attributes = set(super(Faker, self).__dir__())
  67. for factory in self.factories:
  68. attributes |= {attr for attr in dir(factory) if not attr.startswith("_")}
  69. return sorted(attributes)
  70. def __getitem__(self, locale: str) -> Generator:
  71. return self._factory_map[locale.replace("-", "_")]
  72. def __getattribute__(self, attr: str) -> Any:
  73. """
  74. Handles the "attribute resolution" behavior for declared members of this proxy class
  75. The class method `seed` cannot be called from an instance.
  76. :param attr: attribute name
  77. :return: the appropriate attribute
  78. """
  79. if attr == "seed":
  80. msg = "Calling `.seed()` on instances is deprecated. " "Use the class method `Faker.seed()` instead."
  81. raise TypeError(msg)
  82. else:
  83. return super().__getattribute__(attr)
  84. def __getattr__(self, attr: str) -> Any:
  85. """
  86. Handles cache access and proxying behavior
  87. :param attr: attribute name
  88. :return: the appropriate attribute
  89. """
  90. if len(self._factories) == 1:
  91. return getattr(self._factories[0], attr)
  92. elif attr in self.generator_attrs:
  93. msg = "Proxying calls to `%s` is not implemented in multiple locale mode." % attr
  94. raise NotImplementedError(msg)
  95. elif self.cache_pattern.match(attr):
  96. msg = "Cached attribute `%s` does not exist" % attr
  97. raise AttributeError(msg)
  98. else:
  99. factory = self._select_factory(attr)
  100. return getattr(factory, attr)
  101. def __deepcopy__(self, memodict: Dict = {}) -> "Faker":
  102. cls = self.__class__
  103. result = cls.__new__(cls)
  104. result._locales = copy.deepcopy(self._locales)
  105. result._factories = copy.deepcopy(self._factories)
  106. result._factory_map = copy.deepcopy(self._factory_map)
  107. result._weights = copy.deepcopy(self._weights)
  108. result._unique_proxy = UniqueProxy(self)
  109. result._unique_proxy._seen = {k: {result._unique_proxy._sentinel} for k in self._unique_proxy._seen.keys()}
  110. return result
  111. def __setstate__(self, state: Any) -> None:
  112. self.__dict__.update(state)
  113. @property
  114. def unique(self) -> "UniqueProxy":
  115. return self._unique_proxy
  116. def _select_factory(self, method_name: str) -> Factory:
  117. """
  118. Returns a random factory that supports the provider method
  119. :param method_name: Name of provider method
  120. :return: A factory that supports the provider method
  121. """
  122. factories, weights = self._map_provider_method(method_name)
  123. if len(factories) == 0:
  124. msg = f"No generator object has attribute {method_name!r}"
  125. raise AttributeError(msg)
  126. elif len(factories) == 1:
  127. return factories[0]
  128. if weights:
  129. factory = choices_distribution(factories, weights, length=1)[0]
  130. else:
  131. factory = random.choice(factories)
  132. return factory
  133. def _map_provider_method(self, method_name: str) -> Tuple[List[Factory], Optional[List[float]]]:
  134. """
  135. Creates a 2-tuple of factories and weights for the given provider method name
  136. The first element of the tuple contains a list of compatible factories.
  137. The second element of the tuple contains a list of distribution weights.
  138. :param method_name: Name of provider method
  139. :return: 2-tuple (factories, weights)
  140. """
  141. # Return cached mapping if it exists for given method
  142. attr = f"_cached_{method_name}_mapping"
  143. if hasattr(self, attr):
  144. return getattr(self, attr)
  145. # Create mapping if it does not exist
  146. if self._weights:
  147. value = [
  148. (factory, weight)
  149. for factory, weight in zip(self.factories, self._weights)
  150. if hasattr(factory, method_name)
  151. ]
  152. factories, weights = zip(*value)
  153. mapping = list(factories), list(weights)
  154. else:
  155. value = [factory for factory in self.factories if hasattr(factory, method_name)] # type: ignore
  156. mapping = value, None # type: ignore
  157. # Then cache and return results
  158. setattr(self, attr, mapping)
  159. return mapping
  160. @classmethod
  161. def seed(cls, seed: Optional[Hashable] = None) -> None:
  162. """
  163. Hashables the shared `random.Random` object across all factories
  164. :param seed: seed value
  165. """
  166. Generator.seed(seed)
  167. def seed_instance(self, seed: Optional[Hashable] = None) -> None:
  168. """
  169. Creates and seeds a new `random.Random` object for each factory
  170. :param seed: seed value
  171. """
  172. for factory in self._factories:
  173. factory.seed_instance(seed)
  174. def seed_locale(self, locale: str, seed: Optional[Hashable] = None) -> None:
  175. """
  176. Creates and seeds a new `random.Random` object for the factory of the specified locale
  177. :param locale: locale string
  178. :param seed: seed value
  179. """
  180. self._factory_map[locale.replace("-", "_")].seed_instance(seed)
  181. @property
  182. def random(self) -> Random:
  183. """
  184. Proxies `random` getter calls
  185. In single locale mode, this will be proxied to the `random` getter
  186. of the only internal `Generator` object. Subclasses will have to
  187. implement desired behavior in multiple locale mode.
  188. """
  189. if len(self._factories) == 1:
  190. return self._factories[0].random
  191. else:
  192. msg = "Proxying `random` getter calls is not implemented in multiple locale mode."
  193. raise NotImplementedError(msg)
  194. @random.setter
  195. def random(self, value: Random) -> None:
  196. """
  197. Proxies `random` setter calls
  198. In single locale mode, this will be proxied to the `random` setter
  199. of the only internal `Generator` object. Subclasses will have to
  200. implement desired behavior in multiple locale mode.
  201. """
  202. if len(self._factories) == 1:
  203. self._factories[0].random = value
  204. else:
  205. msg = "Proxying `random` setter calls is not implemented in multiple locale mode."
  206. raise NotImplementedError(msg)
  207. @property
  208. def locales(self) -> List[str]:
  209. return list(self._locales)
  210. @property
  211. def weights(self) -> Optional[List[Union[int, float]]]:
  212. return self._weights
  213. @property
  214. def factories(self) -> List[Generator]:
  215. return self._factories
  216. def items(self) -> List[Tuple[str, Generator]]:
  217. return list(self._factory_map.items())
  218. class UniqueProxy:
  219. def __init__(self, proxy: Faker):
  220. self._proxy = proxy
  221. self._seen: Dict = {}
  222. self._sentinel = object()
  223. def clear(self) -> None:
  224. self._seen = {}
  225. def __getattr__(self, name: str) -> Any:
  226. obj = getattr(self._proxy, name)
  227. if callable(obj):
  228. return self._wrap(name, obj)
  229. else:
  230. raise TypeError("Accessing non-functions through .unique is not supported.")
  231. def __getstate__(self):
  232. # Copy the object's state from self.__dict__ which contains
  233. # all our instance attributes. Always use the dict.copy()
  234. # method to avoid modifying the original state.
  235. state = self.__dict__.copy()
  236. return state
  237. def __setstate__(self, state):
  238. self.__dict__.update(state)
  239. def _wrap(self, name: str, function: Callable) -> Callable:
  240. @functools.wraps(function)
  241. def wrapper(*args, **kwargs):
  242. key = (name, args, tuple(sorted(kwargs.items())))
  243. generated = self._seen.setdefault(key, {self._sentinel})
  244. # With use of a sentinel value rather than None, we leave
  245. # None open as a valid return value.
  246. retval = self._sentinel
  247. for i in range(_UNIQUE_ATTEMPTS):
  248. if retval not in generated:
  249. break
  250. retval = function(*args, **kwargs)
  251. else:
  252. raise UniquenessException(f"Got duplicated values after {_UNIQUE_ATTEMPTS:,} iterations.")
  253. generated.add(retval)
  254. return retval
  255. return wrapper