Skip to content

Commit

Permalink
Improve aliases validation
Browse files Browse the repository at this point in the history
  • Loading branch information
lk-geimfari committed Dec 30, 2023
1 parent 62ea7e0 commit dd54946
Show file tree
Hide file tree
Showing 5 changed files with 83 additions and 14 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ Version 12.0.0
--------------

- Python 3.8 and 3.9 are no longer supported.
- Added support for field aliases. See docs for more information.
- Added method ``calver`` for ``Development``.
- Added method ``stage`` for ``Development``.
- Removed parameter ``providers`` for ``Field`` and ``Fieldset``. Use custom field handlers instead.
Expand Down
39 changes: 29 additions & 10 deletions docs/schema.rst
Original file line number Diff line number Diff line change
Expand Up @@ -151,8 +151,11 @@ Using Field Aliases

.. versionadded:: 12.0.0

Sometimes it is necessary to use a field name that is more relevant to your specific domain and this is where
field aliases come in handy.
Sometimes, you need a field name that truly matches what your domain is about, and that's when field aliases become useful.

In order to utilize field aliases, it's necessary to instantiate either a :class:`~mimesis.schema.Field` or
:class:`~mimesis.schema.Fieldset` and then update the attribute ``aliases`` (essentially a regular :class:`dict`) to
associate aliases with field names.

Let's take a look at the example:

Expand All @@ -161,14 +164,17 @@ Let's take a look at the example:
from mimesis import Field, Locale
field = Field(Locale.EN)
field.aliases = {
# The key is an alias, the value is the field
# name to which the alias is associated (both should be strings).
field.aliases.update({
'🇺🇸': 'country',
'🧬': 'dna_sequence',
'📧': 'email',
'📞': 'telephone',
'🍆': 'vegetable',
'ебаныйтокен': 'token_hex',
}
})
You can now use aliases instead of standard field names:
Expand All @@ -190,9 +196,26 @@ You can now use aliases instead of standard field names:
As you can see, you can use any string as an alias, so I'm doing my part to get someone fired for emoji-driven code.
Putting jokes aside, although any string can work as an alias, it's wise to choose one that fits your domain or
context better to enhance clarity and comprehension.

Jokes aside, while any string can serve as an alias, it's advisable to opt for a string that aligns with your
specific domain or context for improved clarity and understanding.
If you try to replace the ``aliases`` attribute with anything other than a non-nested dictionary,
you'll receive an exception :class:`~mimesis.exceptions.AliasesTypeError`.

.. code-block:: python
>>> field.aliases = None # Raises AliasesTypeError
When you no longer need aliases, you can remove them individually like regular dictionary keys or clear them all at once:

.. code-block:: python
>>> field.aliases.pop('🇺🇸')
# clear all aliases
>>> field.aliases.clear()
Key Functions and Post-Processing
Expand Down Expand Up @@ -326,10 +349,6 @@ Custom Field Handlers
We use :class:`~mimesis.schema.Field` in our examples, but all the features described
below are available for :class:`~mimesis.schema.Fieldset` as well.

.. warning::

For obvious reasons, aliases cannot be applied to custom field handlers.

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

Expand Down
10 changes: 10 additions & 0 deletions mimesis/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,3 +93,13 @@ class FieldArityError(ValueError):

def __str__(self) -> str:
return "The custom handler must accept at least two arguments: 'random' and '**kwargs'"


class AliasesTypeError(TypeError):
"""Raised when the aliases attribute is set to a format other than a flat dictionary."""

def __str__(self) -> str:
return (
"The 'aliases' attribute needs to be a non-nested dictionary where "
"keys are the aliases and values are the corresponding field names."
)
21 changes: 18 additions & 3 deletions mimesis/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import typing as t

from mimesis.exceptions import (
AliasesTypeError,
FieldArityError,
FieldError,
FieldNameError,
Expand Down Expand Up @@ -109,15 +110,15 @@ def _lookup_method(self, name: str) -> t.Any:
:return: Callable object.
:raise FieldError: When field is invalid.
"""
# Check if the field is defined in aliases
name = self.aliases.get(name, name)

# Support additional delimiters
name = re.sub(r"[/:\s]", ".", name)

if name.count(".") > 1:
raise FieldError(name)

if name in self.aliases:
name = self.aliases[name]

if name not in self._cache:
if "." not in name:
method = self._fuzzy_lookup(name)
Expand All @@ -127,6 +128,17 @@ def _lookup_method(self, name: str) -> t.Any:

return self._cache[name]

def _validate_aliases(self) -> bool:
"""Validate aliases."""
if not isinstance(self.aliases, dict) or any(
not isinstance(key, str) or not isinstance(value, str)
for key, value in self.aliases.items()
):
# Reset to valid state
self.aliases = {}
raise AliasesTypeError()
return True

def perform(
self,
name: str | None = None,
Expand Down Expand Up @@ -168,6 +180,9 @@ def perform(
:return: The result of method.
:raises ValueError: if provider is not supported or if field is not defined.
"""
# Validate aliases before lookup
self._validate_aliases()

if name is None:
raise FieldError()

Expand Down
26 changes: 25 additions & 1 deletion tests/test_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

from mimesis.enums import Gender
from mimesis.exceptions import (
AliasesTypeError,
FieldArityError,
FieldError,
FieldNameError,
Expand Down Expand Up @@ -517,7 +518,7 @@ def test_unregister_all_handlers(default_field):
assert len(default_field._field_handlers.keys()) == 0


def test_base_field_aliasing(default_field):
def test_field_aliasing(default_field):
default_field.aliases = {
"🇺🇸": "country_code",
}
Expand All @@ -526,3 +527,26 @@ def test_base_field_aliasing(default_field):
with pytest.raises(FieldError):
default_field.aliases.clear()
default_field("🇺🇸")


@pytest.mark.parametrize(
"aliases",
[
{"🇺🇸": tuple},
{"hey": 22},
{12: "email"},
{b"hey": "email"},
None,
[],
tuple(),
],
)
def test_field_invalid_aliases(default_field, aliases):
default_field.aliases = aliases
with pytest.raises(AliasesTypeError):
default_field("email")

default_field.aliases = aliases

with pytest.raises(AliasesTypeError):
default_field._validate_aliases()

0 comments on commit dd54946

Please sign in to comment.