Skip to content

Commit

Permalink
Custom field handlers support (#1399)
Browse files Browse the repository at this point in the history
  • Loading branch information
lk-geimfari committed Aug 19, 2023
1 parent 06c0da6 commit e30943b
Show file tree
Hide file tree
Showing 9 changed files with 348 additions and 11 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
Version 11.0.0
--------------

**Added**:

- Added support for registering custom fields for ``Schema``. This allows you to use your own fields in schemas. See docs for more information.


Version 10.2.0
--------------

Expand Down
5 changes: 5 additions & 0 deletions docs/_static/css/custom.css
Original file line number Diff line number Diff line change
Expand Up @@ -264,4 +264,9 @@ div.highlight pre, table.highlighttable pre {
input[value="Go"]:hover {
background-color: #2c2f36 !important;
}

.highlight .kc {
color: #f47067 !important;
font-weight: bold;
}
}
115 changes: 115 additions & 0 deletions docs/schema.rst
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,121 @@ Output:
Isn't it cool? Of course, it is!


Custom Field Handlers
---------------------

.. versionadded:: 11.0.0
.. note:: This feature is experimental and may be changed or removed in future versions.

Sometimes, it's necessary to register custom fields or override existing ones to return custom data. This
can be achieved using **custom field handlers**.

A custom field handler can be any callable object. It should accept an instance of :class:`~mimesis.random.Random` as
its first argument, and **keyword arguments** for the remaining arguments, returning the result.


.. warning::

**Every** field handler must take a random instance as its first argument.
This ensures it uses the same :class:`~mimesis.random.Random` instance as the rest of the library.

Below you can see examples of valid signatures of field handlers:

- ``field_handler(random, **kwargs)``
- ``field_handler(random, a=None, b=None, c=None, **kwargs)``
- ``field_handler(random, **{a: None, b: None, c: None})``

The **main thing** is that the first argument must be positional (a random instance), and the rest must be **keyword arguments**.


Register Field Handler
~~~~~~~~~~~~~~~~~~~~~~

Suppose you want to create a field that returns a random value from a list of values. First, you need to
create a field handler. Let's call it ``my_field``.

.. code:: python
def my_field(random, a=None, b=None) -> Any:
return random.choice([a, b])
Afterwards, you need to register this field handler using a name you intend to use later. In this example,
we will name the field ``hohoho``.

.. code:: python
>>> from mimesis.schema import Field
>>> field = Field()
>>> field.register_field("hohoho", my_field)
>>> # Now you can use it:
>>> field("hohoho", a="a", b="b")
'a'
>>> # Note that you can still use the key function:
>>> field("hohoho", key=str.upper, a="a", b="b")
'A'
You can register multiple fields at once:

.. code:: python
>>> field.register_fields(
fields=[
('mf1', my_field_1),
('mf2', my_field_2),
]
)
>>> field("mf1", key=str.lower)
>>> field("mf2", key=str.upper)
.. note::

It's important to note that **every** field handler must be registered using a unique name,
otherwise it will override the existing field handler with the same name.


Unregister Field Handler
~~~~~~~~~~~~~~~~~~~~~~~~

If you want to unregister a field handler, you can do it like this:

.. code:: python
>>> field.unregister_field("hohoho")
Now you can't use it anymore and will get a ``FieldError`` if you try to do so.

If you attempt to unregister a field that was never registered, nothing will happen:

.. code:: python
>>> field.unregister_field("blabla") # nothing happens
It's pretty obvious that you can unregister multiple fields at once as well:

.. code:: python
>>> field.unregister_fields(
fields=[
'wow',
'much',
'fields',
]
)
or all fields at once:

.. code:: python
>>> field.unregister_all_fields()
All the features described above are also available for :class:`~mimesis.schema.Fieldset`.


Key Functions
-------------

Expand Down
2 changes: 1 addition & 1 deletion mimesis/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@
"__license__",
]

__version__ = "10.2.1"
__version__ = "11.0.0"
__title__ = "mimesis"
__description__ = "Mimesis: Fake Data Generator."
__url__ = "https://github.com/lk-geimfari/mimesis"
Expand Down
5 changes: 5 additions & 0 deletions mimesis/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,8 @@ class FieldsetError(ValueError):

def __str__(self) -> str:
return "The «iterations» parameter must be greater than 1."


class FieldArityError(ValueError):
def __str__(self) -> str:
return "The custom handler must accept at least two arguments: 'random' and '**kwargs'"
87 changes: 83 additions & 4 deletions mimesis/schema.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
"""Implements classes for generating data by schema."""

import csv
import inspect
import json
import pickle
import re
import typing as t

from mimesis.exceptions import FieldError, FieldsetError, SchemaError
from mimesis.exceptions import (
FieldArityError,
FieldError,
FieldsetError,
SchemaError,
)
from mimesis.locales import Locale
from mimesis.providers.base import BaseProvider
from mimesis.providers.generic import Generic
from mimesis.random import Random
from mimesis.types import (
JSON,
CallableSchema,
Expand All @@ -21,6 +28,10 @@

__all__ = ["BaseField", "Field", "Fieldset", "Schema"]

FieldHandler = t.Callable[[Random, t.Any], t.Any]
RegisterableFieldHandler = t.Tuple[str, FieldHandler]
RegisterableFieldHandlers = t.Sequence[RegisterableFieldHandler]


class BaseField:
def __init__(
Expand All @@ -40,6 +51,7 @@ def __init__(
self._gen.add_providers(*providers)

self._cache: FieldCache = {}
self._custom_fields: t.Dict[str, FieldHandler] = {}

def reseed(self, seed: Seed = MissingSeed) -> None:
"""Reseed the random generator.
Expand All @@ -48,6 +60,13 @@ def reseed(self, seed: Seed = MissingSeed) -> None:
"""
self._gen.reseed(seed)

def get_random_instance(self) -> Random:
"""Get random object from Generic.
:return: Random object.
"""
return self._gen.random

def _explicit_lookup(self, name: str) -> t.Any:
"""An explicit method lookup.
Expand Down Expand Up @@ -149,13 +168,19 @@ def perform(
if name is None:
raise FieldError()

result = self._lookup_method(name)(**kwargs)
random = self.get_random_instance()

# Check if there is a custom field handler.
if name in self._custom_fields:
result = self._custom_fields[name](random, **kwargs) # type: ignore
else:
result = self._lookup_method(name)(**kwargs)

if key and callable(key):
try:
# If key function accepts two parameters
# then pass random instance to it.
return key(result, self._gen.random) # type: ignore
return key(result, random) # type: ignore
except TypeError:
return key(result)

Expand All @@ -164,6 +189,60 @@ def perform(
def __str__(self) -> str:
return f"{self.__class__.__name__} <{self._gen.locale}>"

def register_field(self, field_name: str, field_handler: FieldHandler) -> None:
"""Register a new field handler.
:param field_name: Name of the field.
:param field_handler: Callable object.
"""
if not isinstance(field_name, str):
raise TypeError("Field name must be a string.")

if not callable(field_handler):
raise TypeError("Handler must be a callable object.")

callable_signature = inspect.signature(field_handler)

if len(callable_signature.parameters) <= 1:
raise FieldArityError()

if field_name not in self._custom_fields:
self._custom_fields[field_name] = field_handler

def register_fields(self, fields: RegisterableFieldHandlers) -> None:
"""Register a new field handlers.
:param fields: A sequence of sequences with field name and handler.
:return: None.
"""
for name, handler in fields:
self.register_field(name, handler)

def unregister_field(self, field_name: str) -> None:
"""Unregister a field handler.
:param field_name: Name of the field.
"""
if field_name in self._custom_fields:
del self._custom_fields[field_name]

def unregister_fields(self, field_names: t.Sequence[str] = ()) -> None:
"""Unregister a field handlers with given names.
:param field_names: Names of the fields.
:return: None.
"""

for name in field_names:
self.unregister_field(name)

def unregister_all_fields(self) -> None:
"""Unregister all field handlers.
:return: None.
"""
self._custom_fields.clear()


class Field(BaseField):
"""Greedy field.
Expand All @@ -174,7 +253,7 @@ class Field(BaseField):
There is no case when you need to instance **field** in loops.
If you doing this:
If you are doing this:
>>> for i in range(1000):
... field = Field()
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "mimesis"
version = "10.2.1"
version = "11.0.0"
description = "Mimesis: Fake Data Generator."
authors = ["Isaak Uchakaev <likid.geimfari@gmail.com>"]
license = "MIT"
Expand Down
8 changes: 4 additions & 4 deletions tests/test_keys.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ def test_maybe():
(
Locale.RU,
" ".join(
"АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ" "абвгдеёжзийклмнопрстуфхцчшщъыьэюя"
"АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдеёжзийклмнопрстуфхцчшщъыьэюя"
),
(
"A B V G D E Yo Zh Z I Ye K L M N O P R S T U F Kh Ts "
Expand All @@ -45,7 +45,7 @@ def test_romanize_cyrillic_string(locale, string, expected):

def test_romanize_invalid_locale():
with pytest.raises(LocaleError):
romanize(locale="sdsdsd")
romanize(locale="sdsdsd") # type: ignore


def test_romanize_unsupported_locale():
Expand All @@ -55,7 +55,7 @@ def test_romanize_unsupported_locale():

def test_romanize_missing_positional_arguments():
with pytest.raises(TypeError):
romanize()
romanize() # type: ignore

with pytest.raises(TypeError):
romanize(locale=Locale.RU)()
romanize(locale=Locale.RU)() # type: ignore
Loading

0 comments on commit e30943b

Please sign in to comment.