Repository: lk-geimfari/mimesis

Issue:
Change how works generating data by schema
The current design is bad for following reasons:

`Field` is an object which can represent any method of any provider and it's mean that is data provider which should return data and it's the only thing that we want by this object, but here how it works right now:

```
>>> from mimesis.schema import Field
>>> from mimesis.enums import Gender
_ = Field('en')
>>> _ = Field('en')
>>> app_schema = (
...     lambda: {
...         "id": _('uuid'),
...         "name": _('word'),
...         "version": _('version'),
...         "owner": {
...             "email": _('email'),
...             "token": _('token'),
...             "creator": _('full_name', gender=Gender.FEMALE),
...         },
...     }
... )
>>> _.fill(schema=app_schema, iterations=10)
```

It looks really annoying.

It should be done using another way because here every instance of `Field` contains method `fill`, but why? Good question... because this is disgusting API designed by me. And now I see my mistake and suggest that it be corrected.

I suggest this:

```
from mimesis.schema import Field, Schema
_ = Field('en')
app_data = Schema(lambda: {
    "id": _('uuid'),
    "name": _('word'),
    "version": _('version'),
    "owner": {
        "email": _('email'),
        "token": _('token'),
        "creator": _('full_name', gender=Gender.FEMALE),
    },
}).create(iterations=20)
```

I think that is much better because the code is much more readable and looks more logical.


Discussion:

Resolving patch:
diff --git a/mimesis/exceptions.py b/mimesis/exceptions.py
--- a/mimesis/exceptions.py
+++ b/mimesis/exceptions.py
@@ -1,7 +1,14 @@
+from typing import Any, Optional
+
+
+class UnsupportedAlgorithm(AttributeError):
+    """ Raised when the user wants to use an unsupported algorithm. """
+
+
 class UnsupportedLocale(KeyError):
     """ Raised when a locale isn't supported. """
 
-    def __init__(self, locale: str) -> None:
+    def __init__(self, locale: Optional[str] = None) -> None:
         self.locale = locale
         self.message = 'Locale «{}» is not supported'
 
@@ -9,20 +16,19 @@ def __str__(self) -> str:
         return self.message.format(self.locale)
 
 
-class UnsupportedAlgorithm(AttributeError):
-    """ Raised when the user wants to use an unsupported algorithm. """
-
-
 class UndefinedSchema(ValueError):
     """ Raised when schema is empty. """
 
+    def __str__(self) -> str:
+        return 'Schema should be defined in lambda.'
+
 
 class NonEnumerableError(TypeError):
     """ Raised when object is not instance of Enum """
 
     message = 'You should use one item of: «{}» of the object mimesis.enums.{}'
 
-    def __init__(self, enum_obj) -> None:
+    def __init__(self, enum_obj: Any) -> None:
         if enum_obj:
             self.name = enum_obj
             self.items = ', '.join([str(i) for i in enum_obj])
@@ -32,3 +38,17 @@ def __init__(self, enum_obj) -> None:
     def __str__(self) -> str:
         return self.message.format(self.items,
                                    self.name.__name__)
+
+
+class UnsupportedField(ValueError):
+    def __init__(self, name: Optional[str] = None) -> None:
+        self.name = name
+        self.message = 'Field «{}» is not supported.'
+
+    def __str__(self) -> str:
+        return self.message.format(self.name)
+
+
+class UndefinedField(ValueError):
+    def __str__(self) -> str:
+        return 'Undefined field. Filed cannot be None.'
diff --git a/mimesis/providers/generic.py b/mimesis/providers/generic.py
--- a/mimesis/providers/generic.py
+++ b/mimesis/providers/generic.py
@@ -1,4 +1,5 @@
 import inspect
+from typing import List
 
 from mimesis.providers.address import Address
 from mimesis.providers.base import BaseProvider
@@ -22,32 +23,6 @@
 from mimesis.providers.transport import Transport
 from mimesis.providers.units import UnitSystem
 
-# When you add new data provider, don't forget to add it's name
-# which you use in Generic to elements of this list in alphabetical order.
-# GENERIC ATTRS has used by the object mimesis.schema.AbstractField
-GENERIC_ATTRS = [
-    'address',
-    'business',
-    'clothing_sizes',
-    'code',
-    'cryptographic',
-    'datetime',
-    'development',
-    'file',
-    'food',
-    'games',
-    'hardware',
-    'internet',
-    'numbers',
-    'path',
-    'payment',
-    'personal',
-    'science',
-    'text',
-    'transport',
-    'unit_system',
-]
-
 
 class Generic(BaseProvider):
     """A lazy initialization of locale for all classes that have locales."""
@@ -85,6 +60,12 @@ def __getattr__(self, attrname):
         if attribute and callable(attribute):
             return attribute(self.locale)
 
+    def __dir__(self) -> List[str]:
+        exclude = BaseProvider().__dict__.keys()
+        attrs = [a.replace('_', '') for a
+                 in self.__dict__ if a not in exclude]
+        return attrs
+
     def add_provider(self, cls) -> None:
         """Add a custom provider to Generic() object.
 
diff --git a/mimesis/schema.py b/mimesis/schema.py
--- a/mimesis/schema.py
+++ b/mimesis/schema.py
@@ -1,12 +1,13 @@
 from types import LambdaType
-from typing import Any, List, Optional
+from typing import Any, Callable, List, Optional
 
-from mimesis.exceptions import UndefinedSchema
+from mimesis.exceptions import (UndefinedField, UndefinedSchema,
+                                UnsupportedField)
 from mimesis.providers.base import StrMixin
-from mimesis.providers.generic import GENERIC_ATTRS, Generic
+from mimesis.providers.generic import Generic
 from mimesis.typing import JSON
 
-__all__ = ['AbstractField', 'Field']
+__all__ = ['AbstractField', 'Field', 'Schema']
 
 
 class AbstractField(StrMixin):
@@ -26,47 +27,53 @@ def __init__(self, locale: str = 'en') -> None:
         self.locale = locale
         self.gen = Generic(self.locale)
 
-    def __call__(self, name: Optional[str] = None, **kwargs) -> Any:
+    def __call__(self, name: Optional[str] = None,
+                 key: Optional[Callable] = None, **kwargs) -> Any:
         """This magic override standard call so it's take any string which
         represents name of the any method of any supported data provider
         and the ``**kwargs`` of this method.
 
         :param name: Name of method.
+        :param key: A key function (or other callable object)
+            which will be applied to result.
         :param kwargs: Kwargs of method.
         :return: Value which represented by method.
         :raises ValueError: if provider is not
             supported or if field is not defined.
         """
-        if name is not None:
-            for provider in GENERIC_ATTRS:
-                if hasattr(self.gen, provider):
-                    provider = getattr(self.gen, provider)
-                    if hasattr(provider, name):
-                        method = getattr(provider, name)
-                        return method(**kwargs)
+        if name is None:
+            raise UndefinedField()
+
+        # TODO: This is a really slow solution. Fix it.
+        for provider in dir(self.gen):
+            if hasattr(self.gen, provider):
+                provider = getattr(self.gen, provider)
+                if name in dir(provider):
+                    method = getattr(provider, name)
+                    result = method(**kwargs)
+                    if key and callable(key):
+                        return key(result)
+                    return result
             else:
-                raise ValueError('Field «{}» is not supported'.format(name))
+                continue
         else:
-            raise ValueError('Undefined field')
+            raise UnsupportedField(name)
 
 
-class Field(AbstractField):
+class Schema(object):
     """
-    Subclass of AbstractField which supports method ``fill()``.
+    Class which return list of filled schemas.
     """
 
-    @staticmethod
-    def fill(schema: LambdaType, iterations: int = 1) -> List[JSON]:
-        """Fill schema with data.
-
-        :param lambda schema: Lambda function with schema.
-        :param int iterations: Count of iterations.
-        :return: Filled schema.
-        :raises UndefinedSchema: if schema is empty dict.
-        """
-        if schema() and isinstance(schema, LambdaType):
-            result = map(lambda _: schema(), range(iterations))
-            return list(result)
+    def __init__(self, schema: LambdaType) -> None:
+        if callable(schema) and isinstance(schema, LambdaType):
+            self.schema = schema
         else:
-            raise UndefinedSchema(
-                'Schema should be defined in lambda.')
+            raise UndefinedSchema()
+
+    def create(self, iterations: int = 1) -> List[JSON]:
+        data = map(lambda _: self.schema(), range(iterations))
+        return list(data)
+
+
+Field = AbstractField


Test patch:
diff --git a/tests/test_schema.py b/tests/test_schema.py
--- a/tests/test_schema.py
+++ b/tests/test_schema.py
@@ -2,8 +2,9 @@
 
 from mimesis import config
 from mimesis.enums import Gender
-from mimesis.exceptions import UndefinedSchema
-from mimesis.schema import Field
+from mimesis.exceptions import (UndefinedField, UndefinedSchema,
+                                UnsupportedField)
+from mimesis.schema import Field, Schema
 
 
 @pytest.mark.parametrize(
@@ -11,43 +12,53 @@
 )
 def test_field(locale):
     filed = Field(locale)
-    result = filed('username', template='l.d')
-    assert result.split('.')[1].isdigit()
+    result = filed('full_name')
+    assert result
+    assert isinstance(result, str)
 
-    with pytest.raises(ValueError):
-        filed()  # default is None
+    with pytest.raises(UnsupportedField):
         filed('unsupported_field')
 
+    with pytest.raises(UndefinedField):
+        filed()
+
 
 @pytest.fixture
-def valid():
-    _ = Field('en')
+def _():
+    return Field('en')
+
+
+@pytest.fixture
+def valid_schema(_):
     return lambda: {
         'id': _('uuid'),
         'name': _('word'),
-        'version': _('version'),
+        'version': _('version', key=str.lower, pre_release=True),
         'owner': {
-            'email': _('email'),
+            'email': _('email', key=str.lower),
             'token': _('token'),
             'creator': _('full_name', gender=Gender.FEMALE),
         },
     }
 
 
-@pytest.fixture
-def _():
-    return Field('en')
-
-
 def test_str(_):
     name = str(_).split(':')[0]
     assert name == _.__class__.__name__
 
 
-def test_fill(_, valid):
-    result = _.fill(schema=valid, iterations=2)
+def test_fill(_, valid_schema):
+    result = Schema(schema=valid_schema).create(iterations=2)
     assert isinstance(result, list)
     assert isinstance(result[0], dict)
 
     with pytest.raises(UndefinedSchema):
-        _.fill(schema=lambda: {})
+        Schema(schema=None).create()
+
+
+def test_field_with_key(_):
+    usual_result = _('age')
+    assert isinstance(usual_result, int)
+
+    result_on_key = _('age', key=lambda v: float(v))
+    assert isinstance(result_on_key, float)