Repository: joke2k/faker

Issue:
python providers should use value_types as a keyword argument
Some of the python providers use a slightly unusual function call signature like this:

```
fake.pydict(nb_elements=10, variable_nb_elements=True, *value_types)
fake.pylist(nb_elements=10, variable_nb_elements=True, *value_types)

```
This leads to some usage problems with factory_boy (https://github.com/FactoryBoy/factory_boy/issues/387).

IMHO it would be much more convenient to be able to call the python providers with `value_types` as a keyword argument like so:

```
fake.pydict(nb_elements=10, variable_nb_elements=True, value_types=['str'])
fake.pylist(nb_elements=10, variable_nb_elements=True, value_types=['str'])

```

I know this would introduce a braking change to the python provider api, but if this is acceptable I can try to provide a PR.

python providers should use value_types as a keyword argument
Some of the python providers use a slightly unusual function call signature like this:

```
fake.pydict(nb_elements=10, variable_nb_elements=True, *value_types)
fake.pylist(nb_elements=10, variable_nb_elements=True, *value_types)

```
This leads to some usage problems with factory_boy (https://github.com/FactoryBoy/factory_boy/issues/387).

IMHO it would be much more convenient to be able to call the python providers with `value_types` as a keyword argument like so:

```
fake.pydict(nb_elements=10, variable_nb_elements=True, value_types=['str'])
fake.pylist(nb_elements=10, variable_nb_elements=True, value_types=['str'])

```

I know this would introduce a braking change to the python provider api, but if this is acceptable I can try to provide a PR.



Discussion:
Looks like it's already implemented. Unless I'm misunderstanding
Hi @mhubig, it seems reasonable.
To handle the backcompatibility we could use some signature like that:
```
fake.pydict(nb_elements=10, variable_nb_elements=True, value_types=None, *allowed_types)
fake.pylist(nb_elements=10, variable_nb_elements=True, value_types=None, *allowed_types)
```
Combining `values_types` and `allowed_types` carefully.
I agree this confused me a little bit.

At first I was doing this
```
>>> fake.pydict(nb_elements=5, variable_nb_elements=False, ['str', 'str', 'str', 'str', 'str'])
  File "<input>", line 1
SyntaxError: positional argument follows keyword argument
```

Figured out you should not use the keywords if you want to specify value_types
```
>>> fake.pydict(5, False, 'str')
{'kind': 'NNxcLDYxjKXcCypxOanT', 'social': 'RfNzeHzBFhcHdEbUMiGL', 'article': 'HrTMTBGFOAcsUAibdVRo', 'per': 'gwZMuksrnrvMyVPXAkJB', '
imagine': 'RkvSHFrUMESRLIQDgkxL'}
```

I hope this helps someone going through the same issue.
Hi all,

Is someone working on this? @mhubig 
I know it is not a good thing to do to modify a library directly but there is tested workaround (I have also tried it) of the factory_boy but it is not pulled/released yet: 

[FactoryBoy/factory_boy#456](https://github.com/FactoryBoy/factory_boy/pull/456
)

Hopefully it will be in the released version soon.
**This is not only true for FactoryBoy compat.**

I agree with @fodra: `value_types` passed as variable arguments not only forces you out of named argument passing -- which really is a shame for boolean arguments -- but also makes the syntax weird enough to be confusing (and python's syntax errors don't help you there, cf. #366).
I think it would be ok to break compatibility, as long as we release a new major version.
Looks like it's already implemented. Unless I'm misunderstanding
Hi @mhubig, it seems reasonable.
To handle the backcompatibility we could use some signature like that:
```
fake.pydict(nb_elements=10, variable_nb_elements=True, value_types=None, *allowed_types)
fake.pylist(nb_elements=10, variable_nb_elements=True, value_types=None, *allowed_types)
```
Combining `values_types` and `allowed_types` carefully.
I agree this confused me a little bit.

At first I was doing this
```
>>> fake.pydict(nb_elements=5, variable_nb_elements=False, ['str', 'str', 'str', 'str', 'str'])
  File "<input>", line 1
SyntaxError: positional argument follows keyword argument
```

Figured out you should not use the keywords if you want to specify value_types
```
>>> fake.pydict(5, False, 'str')
{'kind': 'NNxcLDYxjKXcCypxOanT', 'social': 'RfNzeHzBFhcHdEbUMiGL', 'article': 'HrTMTBGFOAcsUAibdVRo', 'per': 'gwZMuksrnrvMyVPXAkJB', '
imagine': 'RkvSHFrUMESRLIQDgkxL'}
```

I hope this helps someone going through the same issue.
Hi all,

Is someone working on this? @mhubig 
I know it is not a good thing to do to modify a library directly but there is tested workaround (I have also tried it) of the factory_boy but it is not pulled/released yet: 

[FactoryBoy/factory_boy#456](https://github.com/FactoryBoy/factory_boy/pull/456
)

Hopefully it will be in the released version soon.
**This is not only true for FactoryBoy compat.**

I agree with @fodra: `value_types` passed as variable arguments not only forces you out of named argument passing -- which really is a shame for boolean arguments -- but also makes the syntax weird enough to be confusing (and python's syntax errors don't help you there, cf. #366).
I think it would be ok to break compatibility, as long as we release a new major version.

Resolving patch:
diff --git a/faker/providers/python/__init__.py b/faker/providers/python/__init__.py
--- a/faker/providers/python/__init__.py
+++ b/faker/providers/python/__init__.py
@@ -1,5 +1,6 @@
 import string
 import sys
+import warnings
 
 from decimal import Decimal
 
@@ -7,6 +8,23 @@
 
 
 class Provider(BaseProvider):
+    default_value_types = (
+        'str', 'str', 'str', 'str', 'float', 'int', 'int', 'decimal',
+        'date_time', 'uri', 'email',
+    )
+
+    def _check_signature(self, value_types, allowed_types):
+        if value_types is not None and not isinstance(value_types, (list, tuple)):
+            value_types = [value_types]
+            warnings.warn(
+                'Passing value types as positional arguments is going to be '
+                'deprecated.  Pass them as a list or tuple instead.',
+                PendingDeprecationWarning,
+            )
+        if value_types is None:
+            value_types = ()
+        return tuple(value_types) + allowed_types
+
     def pybool(self):
         return self.random_int(0, 1) == 1
 
@@ -93,34 +111,39 @@ def pydecimal(self, left_digits=None, right_digits=None, positive=False,
             left_digits, right_digits, positive, min_value, max_value)
         return Decimal(str(float_))
 
-    def pytuple(self, nb_elements=10, variable_nb_elements=True, *value_types):
+    def pytuple(self, nb_elements=10, variable_nb_elements=True, value_types=None, *allowed_types):
         return tuple(
             self.pyset(
                 nb_elements,
                 variable_nb_elements,
-                *value_types))
+                value_types,
+                *allowed_types))
 
-    def pyset(self, nb_elements=10, variable_nb_elements=True, *value_types):
+    def pyset(self, nb_elements=10, variable_nb_elements=True, value_types=None, *allowed_types):
         return set(
             self._pyiterable(
                 nb_elements,
                 variable_nb_elements,
-                *value_types))
+                value_types,
+                *allowed_types))
 
-    def pylist(self, nb_elements=10, variable_nb_elements=True, *value_types):
+    def pylist(self, nb_elements=10, variable_nb_elements=True, value_types=None, *allowed_types):
         return list(
             self._pyiterable(
                 nb_elements,
                 variable_nb_elements,
-                *value_types))
+                value_types,
+                *allowed_types))
 
     def pyiterable(
             self,
             nb_elements=10,
             variable_nb_elements=True,
-            *value_types):
+            value_types=None,
+            *allowed_types):
+        value_types = self._check_signature(value_types, allowed_types)
         return self.random_element([self.pylist, self.pytuple, self.pyset])(
-            nb_elements, variable_nb_elements, *value_types)
+            nb_elements, variable_nb_elements, value_types, *allowed_types)
 
     def _random_type(self, type_list):
         value_type = self.random_element(type_list)
@@ -135,15 +158,17 @@ def _pyiterable(
             self,
             nb_elements=10,
             variable_nb_elements=True,
-            *value_types):
+            value_types=None,
+            *allowed_types):
+
+        value_types = self._check_signature(value_types, allowed_types)
 
         value_types = [t if isinstance(t, str) else getattr(t, '__name__', type(t).__name__).lower()
                        for t in value_types
                        # avoid recursion
                        if t not in ['iterable', 'list', 'tuple', 'dict', 'set']]
         if not value_types:
-            value_types = ['str', 'str', 'str', 'str', 'float',
-                           'int', 'int', 'decimal', 'date_time', 'uri', 'email']
+            value_types = self.default_value_types
 
         if variable_nb_elements:
             nb_elements = self.randomize_nb_elements(nb_elements, min=1)
@@ -151,7 +176,7 @@ def _pyiterable(
         for _ in range(nb_elements):
             yield self._random_type(value_types)
 
-    def pydict(self, nb_elements=10, variable_nb_elements=True, *value_types):
+    def pydict(self, nb_elements=10, variable_nb_elements=True, value_types=None, *allowed_types):
         """
         Returns a dictionary.
 
@@ -164,18 +189,18 @@ def pydict(self, nb_elements=10, variable_nb_elements=True, *value_types):
 
         return dict(zip(
             self.generator.words(nb_elements, unique=True),
-            self._pyiterable(nb_elements, False, *value_types),
+            self._pyiterable(nb_elements, False, value_types, *allowed_types),
         ))
 
-    def pystruct(self, count=10, *value_types):
+    def pystruct(self, count=10, value_types=None, *allowed_types):
+        value_types = self._check_signature(value_types, allowed_types)
 
         value_types = [t if isinstance(t, str) else getattr(t, '__name__', type(t).__name__).lower()
                        for t in value_types
                        # avoid recursion
                        if t != 'struct']
         if not value_types:
-            value_types = ['str', 'str', 'str', 'str', 'float',
-                           'int', 'int', 'decimal', 'date_time', 'uri', 'email']
+            value_types = self.default_value_types
 
         types = []
         d = {}


Test patch:
diff --git a/tests/providers/test_python.py b/tests/providers/test_python.py
--- a/tests/providers/test_python.py
+++ b/tests/providers/test_python.py
@@ -1,4 +1,5 @@
 import unittest
+import warnings
 
 from unittest.mock import patch
 
@@ -106,3 +107,69 @@ def test_formatter_invocation(self):
                 assert value.count('barbar') == 3
                 assert mock_foo.call_count == 3
                 mock_bothify.assert_called_once_with('barbar?#?barbar?#?barbar', letters='abcde')
+
+
+class TestPython(unittest.TestCase):
+    """Tests python generators"""
+    def setUp(self):
+        self.factory = Faker()
+
+    def test_pybool(self):
+        some_bool = self.factory.pybool()
+        assert isinstance(some_bool, bool)
+
+    def py_str(self):
+        some_string = self.factory.pystr()
+        assert isinstance(some_string, str)
+        assert len(some_string) <= 20
+
+        some_string = self.factory.pystr(min_chars=3)
+        assert isinstance(some_string, str)
+        assert len(some_string) >= 3
+        assert len(some_string) <= 20
+
+        some_string = self.factory.pystr(max_chars=5)
+        assert isinstance(some_string, str)
+        assert len(some_string) <= 5
+
+        with self.assertRaises(AssertionError):
+            self.factory.pystr(min_chars=6, max_chars=5)
+
+        with self.assertRaises(AssertionError):
+            self.factory.pystr(min_chars=5, max_chars=5)
+
+    def test_pylist(self):
+        with warnings.catch_warnings(record=True) as w:
+            some_list = self.factory.pylist()
+            assert len(w) == 0
+        assert some_list
+        assert isinstance(some_list, list)
+
+    def test_pylist_types(self):
+        with warnings.catch_warnings(record=True) as w:
+            some_list = self.factory.pylist(10, True, [int])
+            assert len(w) == 0
+        assert some_list
+        for item in some_list:
+            assert isinstance(item, int)
+
+        with warnings.catch_warnings(record=True) as w:
+            some_list = self.factory.pylist(10, True, value_types=[int])
+            assert len(w) == 0
+        assert some_list
+        for item in some_list:
+            assert isinstance(item, int)
+
+        with warnings.catch_warnings(record=True) as w:
+            some_list = self.factory.pylist(10, True, int)
+            assert len(w) == 1
+        assert some_list
+        for item in some_list:
+            assert isinstance(item, int)
+
+        with warnings.catch_warnings(record=True) as w:
+            some_list = self.factory.pylist(10, True, int, float)
+            assert len(w) == 1
+        assert some_list
+        for item in some_list:
+            assert isinstance(item, (int, float))