22
33import copy
44import functools
5+ import logging
56import re
67
78from collections import OrderedDict
1920
2021RetType = TypeVar ("RetType" )
2122
23+ logger = logging .getLogger (__name__ )
24+
2225
2326class Faker :
2427 """Proxy class capable of supporting multiple locales"""
2528
29+ cache_attr_name = "_cached_{method_name}_mapping"
2630 cache_pattern : Pattern = re .compile (r"^_cached_\w*_mapping$" )
2731 generator_attrs = [
2832 attr for attr in dir (Generator ) if not attr .startswith ("__" ) and attr not in ["seed" , "seed_instance" , "random" ]
@@ -39,8 +43,6 @@ def __init__(
3943 ) -> None :
4044 self ._factory_map : OrderedDict [str , Generator | Faker ] = OrderedDict ()
4145 self ._weights = None
42- self ._unique_proxy = UniqueProxy (self )
43- self ._optional_proxy = OptionalProxy (self )
4446
4547 if isinstance (locale , str ):
4648 locales = [locale .replace ("-" , "_" )]
@@ -146,21 +148,26 @@ def __deepcopy__(self, memodict):
146148 result ._factory_map = copy .deepcopy (self ._factory_map , memodict )
147149 result ._factories = list (result ._factory_map .values ())
148150 result ._weights = copy .deepcopy (self ._weights , memodict )
149- result ._unique_proxy = UniqueProxy (result )
150- result ._unique_proxy ._seen = {k : {result ._unique_proxy ._sentinel } for k in self ._unique_proxy ._seen .keys ()}
151- result ._optional_proxy = OptionalProxy (result )
151+ result .unique ._seen = {k : {result .unique ._sentinel } for k in self .unique ._seen .keys ()}
152+ result .preferred_unique ._seen = {
153+ k : {result .preferred_unique ._sentinel } for k in self .preferred_unique ._seen .keys ()
154+ }
152155 return result
153156
154157 def __setstate__ (self , state : Any ) -> None :
155158 self .__dict__ .update (state )
156159
157- @property
160+ @functools . cached_property
158161 def unique (self ) -> UniqueProxy :
159- return self . _unique_proxy
162+ return UniqueProxy ( self )
160163
161- @property
164+ @functools .cached_property
165+ def preferred_unique (self ) -> UniqueProxy :
166+ return UniqueProxy (self , only_prefer_uniqueness = True )
167+
168+ @functools .cached_property
162169 def optional (self ) -> OptionalProxy :
163- return self . _optional_proxy
170+ return OptionalProxy ( self )
164171
165172 def _select_factory (self , method_name : str ) -> Factory :
166173 """
@@ -182,6 +189,7 @@ def _select_factory(self, method_name: str) -> Factory:
182189 factory = self ._select_factory_distribution (factories , weights )
183190 else :
184191 factory = self ._select_factory_choice (factories )
192+
185193 return factory
186194
187195 def _select_factory_distribution (self , factories , weights ):
@@ -202,7 +210,7 @@ def _map_provider_method(self, method_name: str) -> tuple[list[Factory], list[fl
202210 """
203211
204212 # Return cached mapping if it exists for given method
205- attr = f"_cached_ { method_name } _mapping"
213+ attr = self . cache_attr_name . format ( method_name = method_name )
206214 if hasattr (self , attr ):
207215 return getattr (self , attr )
208216
@@ -299,11 +307,16 @@ def items(self) -> list[tuple[str, Generator | Faker]]:
299307
300308
301309class UniqueProxy :
302- def __init__ (self , proxy : Faker , excluded_types : tuple [type , ...] = ()):
310+ def __init__ (self , proxy : Faker , excluded_types : tuple [type , ...] = (), only_prefer_uniqueness : bool = False ):
303311 self ._proxy = proxy
304312 self ._seen : dict = {}
305313 self ._sentinel = object ()
306314 self ._excluded_types = excluded_types
315+ self ._only_prefer_uniqueness = only_prefer_uniqueness
316+ # Round-robin state for ``current_*`` methods. Kept private to this
317+ # proxy so that the factory mapping cached on the ``Faker`` proxy and
318+ # shared with regular (non-unique) calls is never mutated.
319+ self ._current_factory_pool : dict = {}
307320
308321 def clear (self ) -> None :
309322 self ._seen = {}
@@ -337,11 +350,13 @@ def __getitem__(self, locale: str) -> UniqueProxy:
337350 return unique_proxy
338351
339352 def __getattr__ (self , name : str ) -> Any :
353+ if name .startswith ("current_" ):
354+ return self ._select_current_factory_method (name )
355+
340356 obj = getattr (self ._proxy , name )
341- if callable (obj ):
357+ if callable (obj ) and not name . startswith ( "__" ) :
342358 return self ._wrap (name , obj )
343- else :
344- raise TypeError ("Accessing non-functions through .unique is not supported." )
359+ return obj
345360
346361 def __getstate__ (self ):
347362 # Copy the object's state from self.__dict__ which contains
@@ -366,6 +381,9 @@ def _make_hashable(self, value: Any) -> Any:
366381 def _wrap (self , name : str , function : Callable ) -> Callable :
367382 @functools .wraps (function )
368383 def wrapper (* args , ** kwargs ):
384+ key = (name , args , tuple (sorted (kwargs .items ())))
385+ generated = self ._seen .setdefault (key , {self ._sentinel })
386+
369387 # If types are excluded, call function once to check return type
370388 if self ._excluded_types :
371389 retval = function (* args , ** kwargs )
@@ -375,8 +393,6 @@ def wrapper(*args, **kwargs):
375393 # If not excluded, continue with normal uniqueness logic
376394 # but we already have a value, so we'll use it if unique
377395 hashable_retval = self ._make_hashable (retval )
378- key = (name , args , tuple (sorted (kwargs .items ())))
379- generated = self ._seen .setdefault (key , {self ._sentinel })
380396
381397 # Check if this first value is unique
382398 if hashable_retval not in generated :
@@ -385,26 +401,66 @@ def wrapper(*args, **kwargs):
385401 # Not unique, continue with normal loop below
386402 else :
387403 # No exclusions, use original logic
388- key = (name , args , tuple (sorted (kwargs .items ())))
389- generated = self ._seen .setdefault (key , {self ._sentinel })
390404 retval = self ._sentinel
391405 hashable_retval = self ._make_hashable (retval )
392406
393407 # Original uniqueness logic (with potential first attempt already done)
394- for i in range (_UNIQUE_ATTEMPTS ):
395- if hashable_retval not in generated :
408+ for _ in range (_UNIQUE_ATTEMPTS ):
409+ if hashable_retval is None or hashable_retval not in generated :
396410 break
397411 retval = function (* args , ** kwargs )
398412 hashable_retval = self ._make_hashable (retval )
399413 else :
400- raise UniquenessException (f"Got duplicated values after { _UNIQUE_ATTEMPTS :,} iterations." )
414+ if self ._only_prefer_uniqueness :
415+ logger .warning (
416+ f'There seem to be no more unique values for generator "{ name } ". '
417+ "Resetting store of generated values as uniqueness is not being enforced."
418+ )
419+ generated .clear ()
420+ else :
421+ raise UniquenessException (f"Got duplicated values after { _UNIQUE_ATTEMPTS :,} iterations." )
401422
402423 generated .add (hashable_retval )
403424
404425 return retval
405426
406427 return wrapper
407428
429+ def _select_current_factory_method (self , name : str ) -> Any :
430+ """Round-robin through the factories supporting a ``current_*`` method.
431+
432+ The value of a ``current_*`` method is a property of its locale, so it
433+ cannot be made unique by re-rolling it. Instead, draw factories without
434+ replacement from a pool that is refilled once exhausted, so that
435+ consecutive calls go through all possible provider options.
436+
437+ The pool is stored on this proxy: the factory mapping cached on the
438+ ``Faker`` proxy is only ever read here, never mutated, so regular
439+ (non-unique) calls keep access to every locale.
440+ """
441+ # No need to re-roll the factory list if only one is present
442+ if len (getattr (self ._proxy , "factories" , [])) <= 1 :
443+ return getattr (self ._proxy , name )
444+
445+ factories , weights = self ._proxy ._map_provider_method (name )
446+ if not factories :
447+ # Let the proxy raise the usual AttributeError
448+ return getattr (self ._proxy , name )
449+
450+ pool = self ._current_factory_pool .setdefault (name , list (factories ))
451+ if weights :
452+ pool_weights = [weights [factories .index (factory )] for factory in pool ]
453+ factory = choices_distribution (pool , pool_weights , self ._proxy ._factories [0 ].random , length = 1 )[0 ]
454+ else :
455+ factory = self ._proxy ._factories [0 ].random .choice (pool )
456+
457+ pool .remove (factory )
458+ # Refill the pool once every factory has been used
459+ if not pool :
460+ self ._current_factory_pool [name ] = list (factories )
461+
462+ return getattr (factory , name )
463+
408464
409465class OptionalProxy :
410466 """
0 commit comments