Repository: joke2k/faker
Pull request #1243 (2020-08-12 06:07:44)

Parser argument support
### What does this changes

Adds the ability to send arguments through the Generator Parser, to further refine the generated values.  Anything using the parser, like DSV would benefit hugely by this.   Methods have been added for Set, Get, Del, and the __config is used to store the arguments.

Using the format {{ formatter:group }}, the arguments can be used.

Example:

generator.set_arguments('red_rgb', {"hue":"red", "color_format":"rgb"})
generator.parse("{{ color:red_rgb }}")

I would like to align JSON and Fixed Width to DSV, and implement the use of the parser.  This would allow JSON to be created using a Dict, rather than tuples and overall be much simpler, with the benefits DSV has by using the parser.

fcurella: I agree, this approach looks much cleaner. Thank you so much!

Patch:
diff --git a/faker/generator.py b/faker/generator.py
index 0402659b..46f176d1 100644
--- a/faker/generator.py
+++ b/faker/generator.py
@@ -1,14 +1,16 @@
 import random as random_module
 import re
 
-_re_token = re.compile(r'\{\{(\s?)(\w+)(\s?)\}\}')
+_re_token = re.compile(r'\{\{\s*(\w+)(:\s*\w+?)?\s*\}\}')
 random = random_module.Random()
 mod_random = random  # compat with name released in 0.8
 
 
 class Generator:
 
-    __config = {}
+    __config = {
+        'arguments': {},
+    }
 
     def __init__(self, **config):
         self.providers = []
@@ -71,7 +73,6 @@ class Generator:
         """
         This is a secure way to make a fake from another Provider.
         """
-        # TODO: data export?
         return self.get_formatter(formatter)(*args, **kwargs)
 
     def get_formatter(self, formatter):
@@ -95,14 +96,84 @@ class Generator:
         """
         setattr(self, name, method)
 
+    def set_arguments(self, group, argument, value=None):
+        """
+        Creates an argument group, with an individual argument or a dictionary
+        of arguments.  Used with the Generator.parse method.
+
+        generator.set_arguments('small', 'max_value', 10)
+        generator.set_arguments('small', {'min_value': 5, 'max_value': 10})
+        """
+        if group not in self.__config['arguments']:
+            self.__config['arguments'][group] = {}
+
+        if isinstance(argument, dict):
+            self.__config['arguments'][group] = argument
+        elif not isinstance(argument, str):
+            raise ValueError("Arguments must be either a string or dictionary")
+        else:
+            self.__config['arguments'][group][argument] = value
+
+    def get_arguments(self, group, argument=None):
+        """
+        Get the value of an argument configured within a argument group, or
+        the entire group as a dictionary.
+
+        generator.get_arguments('small', 'max_value')
+        generator.get_arguments('small')
+        """
+        if group in self.__config['arguments'] and argument:
+            result = self.__config['arguments'][group].get(argument)
+        else:
+            result = self.__config['arguments'].get(group)
+
+        return result
+
+    def del_arguments(self, group, argument=None):
+        """
+        Delete an argument from an argument group or the entire
+        argument group.
+
+        generator.del_arguments('small')
+        generator.del_arguments('small', 'max_value')
+        """
+        if group in self.__config['arguments']:
+            if argument:
+                result = self.__config['arguments'][group].pop(argument)
+            else:
+                result = self.__config['arguments'].pop(group)
+        else:
+            result = None
+
+        return result
+
     def parse(self, text):
         """
         Replaces tokens (like '{{ tokenName }}' or '{{tokenName}}')
-        with the result from the token method call.
+        with the result from the token method call. Arguments can be
+        parsed by using an argument group. '{{ tokenName:group }}'
+
+        Example:
+
+        generator.set_arguments('red_rgb', {'hue': 'red', 'color_format': 'rgb'})
+        generator.set_arguments('small', 'max_value', 10)
+
+        generator.parse('{{ color:red_rgb }} - {{ pyint:small }}')
         """
         return _re_token.sub(self.__format_token, text)
 
     def __format_token(self, matches):
-        formatter = list(matches.groups())
-        formatter[1] = str(self.format(formatter[1]))
-        return ''.join(formatter)
+        formatter, argument_group = list(matches.groups())
+        argument_group = argument_group.lstrip(":").strip() if argument_group else ''
+
+        if argument_group:
+            try:
+                arguments = self.__config['arguments'][argument_group]
+            except KeyError:
+                raise AttributeError('Unknown argument group "{}"'.format(argument_group))
+
+            formatted = str(self.format(formatter, **arguments))
+        else:
+            formatted = str(self.format(formatter))
+
+        return ''.join(formatted)


Test patch:
diff --git a/tests/test_generator.py b/tests/test_generator.py
index b7e58a0d..c38cbd75 100644
--- a/tests/test_generator.py
+++ b/tests/test_generator.py
@@ -17,7 +17,7 @@ class FooProvider:
         return 'foobar'
 
     def foo_formatter_with_arguments(self, param='', append=''):
-        return 'baz' + param + append
+        return 'baz' + str(param) + str(append)
 
 
 @pytest.fixture(autouse=True)
@@ -61,7 +61,41 @@ class TestGenerator:
 
     def test_parse_with_valid_formatter_tokens(self, generator):
         result = generator.parse('This is {{foo_formatter}} a text with "{{ foo_formatter }}"')
-        assert result == 'This is foobar a text with " foobar "'
+        assert result == 'This is foobar a text with "foobar"'
+
+    def test_arguments_group_with_values(self, generator):
+        generator.set_arguments('group1', 'argument1', 1)
+        generator.set_arguments('group1', 'argument2', 2)
+        assert generator.get_arguments('group1', 'argument1') == 1
+        assert generator.del_arguments('group1', 'argument2') == 2
+        assert generator.get_arguments('group1', 'argument2') is None
+        assert generator.get_arguments('group1') == {'argument1': 1}
+
+    def test_arguments_group_with_dictionaries(self, generator):
+        generator.set_arguments('group2', {'argument1': 3, 'argument2': 4})
+        assert generator.get_arguments('group2') == {'argument1': 3, 'argument2': 4}
+        assert generator.del_arguments('group2') == {'argument1': 3, 'argument2': 4}
+        assert generator.get_arguments('group2') is None
+
+    def test_arguments_group_with_invalid_name(self, generator):
+        assert generator.get_arguments('group3') is None
+        assert generator.del_arguments('group3') is None
+
+    def test_arguments_group_with_invalid_argument_type(self, generator):
+        with pytest.raises(ValueError) as excinfo:
+            generator.set_arguments('group', ['foo', 'bar'])
+        assert str(excinfo.value) == "Arguments must be either a string or dictionary"
+
+    def test_parse_with_valid_formatter_arguments(self, generator):
+        generator.set_arguments('format_name', {"param": "foo", "append": "bar"})
+        result = generator.parse('This is "{{foo_formatter_with_arguments:format_name}}"')
+        generator.del_arguments('format_name')
+        assert result == 'This is "bazfoobar"'
+
+    def test_parse_with_unknown_arguments_group(self, generator):
+        with pytest.raises(AttributeError) as excinfo:
+            generator.parse('This is "{{foo_formatter_with_arguments:unknown}}"')
+        assert str(excinfo.value) == 'Unknown argument group "unknown"'
 
     def test_parse_with_unknown_formatter_token(self, generator):
         with pytest.raises(AttributeError) as excinfo: