Skip to content

Commit

Permalink
add .optional proxy that may or may not return a fake value (#1867)
Browse files Browse the repository at this point in the history
* PoC: a .optional proxy that may or may not return a fake value

* add type annotations for the OptionalProxy

* actually honour the probability :-)

* blacken

* add docstring to `OptionalProxy` class

---------

Co-authored-by: Flavio Curella <89607+fcurella@users.noreply.github.com>
  • Loading branch information
ligne and fcurella committed Jul 7, 2023
1 parent e73f64e commit 5fdaaad
Show file tree
Hide file tree
Showing 2 changed files with 86 additions and 1 deletion.
43 changes: 42 additions & 1 deletion faker/proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from collections import OrderedDict
from random import Random
from typing import Any, Callable, Dict, List, Optional, Pattern, Sequence, Tuple, Union
from typing import Any, Callable, Dict, List, Optional, Pattern, Sequence, Tuple, TypeVar, Union

from .config import DEFAULT_LOCALE
from .exceptions import UniquenessException
Expand All @@ -15,6 +15,8 @@

_UNIQUE_ATTEMPTS = 1000

RetType = TypeVar("RetType")


class Faker:
"""Proxy class capable of supporting multiple locales"""
Expand All @@ -36,6 +38,7 @@ def __init__(
self._factory_map = OrderedDict()
self._weights = None
self._unique_proxy = UniqueProxy(self)
self._optional_proxy = OptionalProxy(self)

if isinstance(locale, str):
locales = [locale.replace("-", "_")]
Expand Down Expand Up @@ -137,6 +140,10 @@ def __setstate__(self, state: Any) -> None:
def unique(self) -> "UniqueProxy":
return self._unique_proxy

@property
def optional(self) -> "OptionalProxy":
return self._optional_proxy

def _select_factory(self, method_name: str) -> Factory:
"""
Returns a random factory that supports the provider method
Expand Down Expand Up @@ -322,3 +329,37 @@ def wrapper(*args, **kwargs):
return retval

return wrapper


class OptionalProxy:
"""
Return either a fake value or None, with a customizable probability.
"""
def __init__(self, proxy: Faker):
self._proxy = proxy

def __getattr__(self, name: str) -> Any:
obj = getattr(self._proxy, name)
if callable(obj):
return self._wrap(name, obj)
else:
raise TypeError("Accessing non-functions through .optional is not supported.")

def __getstate__(self):
# Copy the object's state from self.__dict__ which contains
# all our instance attributes. Always use the dict.copy()
# method to avoid modifying the original state.
state = self.__dict__.copy()
return state

def __setstate__(self, state):
self.__dict__.update(state)

def _wrap(self, name: str, function: Callable[..., RetType]) -> Callable[..., Optional[RetType]]:
@functools.wraps(function)
def wrapper(*args: Any, prob: float = 0.5, **kwargs: Any) -> Optional[RetType]:
if not 0 < prob <= 1.0:
raise ValueError("prob must be between 0 and 1")
return function(*args, **kwargs) if self._proxy.boolean(chance_of_getting_true=int(prob * 100)) else None

return wrapper
44 changes: 44 additions & 0 deletions tests/test_optional.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import pytest

from faker import Faker


class TestOptionalClass:
def test_optional(self) -> None:
fake = Faker()

assert {fake.optional.boolean() for _ in range(10)} == {True, False, None}

def test_optional_probability(self) -> None:
"""The probability is configurable."""
fake = Faker()

fake.optional.name(prob=0.1)

def test_optional_arguments(self) -> None:
"""Other arguments are passed through to the function."""
fake = Faker()

fake.optional.pyint(1, 2, prob=0.4)

def test_optional_valid_range(self) -> None:
"""Only probabilities in the range (0, 1]."""
fake = Faker()

with pytest.raises(ValueError, match=""):
fake.optional.name(prob=0)

with pytest.raises(ValueError, match=""):
fake.optional.name(prob=1.1)

with pytest.raises(ValueError, match=""):
fake.optional.name(prob=-3)

def test_functions_only(self):
"""Accessing non-functions through the `.optional` attribute
will throw a TypeError."""

fake = Faker()

with pytest.raises(TypeError, match="Accessing non-functions through .optional is not supported."):
fake.optional.locales

0 comments on commit 5fdaaad

Please sign in to comment.