Repository: joke2k/faker
Pull request #745 (2018-04-22 04:35:40)

Add logging functionality
This pull request adds logging functionality to the faker factory and CLI.

In the past, users of faker could have been misled into thinking they were using a specified locale when in fact that was not the case. The locale they were calling could have been unavailable with the provider they were using.

Through logging, this pull request attempts to add transparency so that the user is fully aware of any locale default switches that are happening in the background.


**Implementation**
Running as a module:
- INFO log events are created when provider has been localized with the intended locale
- WARNING log events are created when intended locale is not available for a provider so the default locale is used instead

In CLI :
 - when invoked, optional argument -v / --verbose will output the above-listed log info to the command line


Patch:
diff --git a/faker/cli.py b/faker/cli.py
index d6f2282d..dbeb9e13 100644
--- a/faker/cli.py
+++ b/faker/cli.py
@@ -7,10 +7,13 @@ import os
 import sys
 import argparse
 
+import random
+
 from faker import Faker, documentor
 from faker import VERSION
 from faker.config import AVAILABLE_LOCALES, DEFAULT_LOCALE, META_PROVIDERS_MODULES
 
+import logging
 
 if sys.version < '3':
     text_type = unicode
@@ -61,10 +64,12 @@ def print_provider(doc, provider, formatters, excludes=None, output=None):
 
 
 def print_doc(provider_or_field=None,
-              args=None, lang=DEFAULT_LOCALE, output=None, includes=None):
+              args=None, lang=DEFAULT_LOCALE, output=None, seed=None,
+              includes=None):
     args = args or []
     output = output or sys.stdout
     fake = Faker(locale=lang, includes=includes)
+    fake.seed_instance(seed)
 
     from faker.providers import BaseProvider
     base_provider_formatters = [f for f in dir(BaseProvider)]
@@ -75,6 +80,7 @@ def print_doc(provider_or_field=None,
             locale = parts[-2] if parts[-2] in AVAILABLE_LOCALES else lang
             fake = Faker(locale, providers=[
                          provider_or_field], includes=includes)
+            fake.seed_instance(seed)
             doc = documentor.Documentor(fake)
             doc.already_generated = base_provider_formatters
             print_provider(
@@ -109,6 +115,7 @@ def print_doc(provider_or_field=None,
             print(file=output)
             print('## LANGUAGE {0}'.format(language), file=output)
             fake = Faker(locale=language)
+            fake.seed_instance(seed)
             d = documentor.Documentor(fake)
 
             for p, fs in d.get_formatters(with_args=True, with_defaults=True,
@@ -179,6 +186,14 @@ examples:
         parser.add_argument("--version", action="version",
                             version="%(prog)s {0}".format(VERSION))
 
+        parser.add_argument('-v',
+                            '--verbose',
+                            action='store_true',
+                            help="show INFO logging events instead "
+                            "of CRITICAL, which is the default. These logging "
+                            "events provide insight into localization of "
+                            "specific providers.")
+
         parser.add_argument('-o', metavar="output",
                             type=argparse.FileType('w'),
                             default=sys.stdout,
@@ -199,6 +214,12 @@ examples:
                             help="use the specified separator after each "
                             "output")
 
+        parser.add_argument('--seed', metavar='SEED',
+                            type=int,
+                            help="specify a seed for the random generator so "
+                            "that results are repeatable. Also compatible "
+                            "with 'repeat' option")
+
         parser.add_argument('-i',
                             '--include',
                             default=META_PROVIDERS_MODULES,
@@ -225,12 +246,21 @@ examples:
 
         arguments = parser.parse_args(self.argv[1:])
 
-        for _ in range(arguments.repeat):
+        if arguments.verbose:
+            logging.basicConfig(level=logging.INFO)
+        else:
+            logging.basicConfig(level=logging.CRITICAL)
+
+        random.seed(arguments.seed)
+        seeds = random.sample(range(arguments.repeat*10),arguments.repeat)
+
+        for i in range(arguments.repeat):
 
             print_doc(arguments.fake,
                       arguments.fake_args,
                       lang=arguments.lang,
                       output=arguments.o,
+                      seed=seeds[i],
                       includes=arguments.include
                       )
             print(arguments.sep, file=arguments.o)
diff --git a/faker/factory.py b/faker/factory.py
index 3bc00497..9f6551e0 100644
--- a/faker/factory.py
+++ b/faker/factory.py
@@ -6,11 +6,24 @@ from __future__ import absolute_import
 from importlib import import_module
 import locale as pylocale
 
+import logging
+import sys
+
 from faker import Generator
 from faker.config import DEFAULT_LOCALE, PROVIDERS, AVAILABLE_LOCALES
 from faker.utils.loading import list_module
 
 
+logger = logging.getLogger(__name__)
+
+# identify if python is being run in interactive mode. If so, disable logging.
+inREPL = bool(getattr(sys, 'ps1', False))
+if inREPL:
+    logger.setLevel(logging.CRITICAL)
+else:
+    logger.debug('Not in REPL -> leaving logger event level as is.')
+
+
 class Factory(object):
 
     @classmethod
@@ -28,7 +41,7 @@ class Factory(object):
         locale = locale.replace('-', '_') if locale else DEFAULT_LOCALE
         locale = pylocale.normalize(locale).split('.')[0]
         if locale not in AVAILABLE_LOCALES:
-            msg = 'Invalid configuration for faker locale "{0}"'.format(locale)
+            msg = 'Invalid configuration for faker locale `{0}`'.format(locale)
             raise AttributeError(msg)
 
         config['locale'] = locale
@@ -69,26 +82,49 @@ class Factory(object):
         if provider_class:
             return provider_class, None
 
-        msg = 'Unable to find provider "{0}" with locale "{1}"'.format(
+        msg = 'Unable to find provider `{0}` with locale `{1}`'.format(
             provider, locale)
         raise ValueError(msg)
 
     @classmethod
     def _find_provider_class(cls, provider_path, locale=None):
+
         provider_module = import_module(provider_path)
 
         if getattr(provider_module, 'localized', False):
+
+            logger.debug('Looking for locale `{}` in provider `{}`.'.format(
+                locale, provider_module.__name__))
+
             available_locales = list_module(provider_module)
             if not locale or locale not in available_locales:
+                unavailable_locale = locale
                 locale = getattr(
                     provider_module, 'default_locale', DEFAULT_LOCALE)
+                logger.warning('Specified locale `{}` is not available for '
+                               'provider `{}`. Locale reset to `{}` for this '
+                               'provider.'.format(unavailable_locale,
+                                                  provider_module.__name__,
+                                                  locale)
+                               )
+            else:
+                logger.info('Provider `{}` has been localized to `{}`.'.format(
+                    provider_module.__name__, locale))
 
             path = "{provider_path}.{locale}".format(
                 provider_path=provider_path,
                 locale=locale,
             )
             provider_module = import_module(path)
+
         else:
+
+            logger.info('Provider `{}` does not feature localization. '
+                        'Specified locale `{}` is not utilized for this '
+                        'provider.'.format(
+                            provider_module.__name__, locale)
+                        )
+
             if locale is not None:
                 provider_module = import_module(provider_path)
 
diff --git a/faker/generator.py b/faker/generator.py
index a491bd98..f0c24037 100644
--- a/faker/generator.py
+++ b/faker/generator.py
@@ -5,7 +5,6 @@ from __future__ import unicode_literals
 import re
 import random as random_module
 
-
 _re_token = re.compile(r'\{\{(\s?)(\w+)(\s?)\}\}')
 random = random_module.Random()
 mod_random = random  # compat with name released in 0.8


Test patch:
diff --git a/tests/test_factory.py b/tests/test_factory.py
index 6be72a3e..f9d6624c 100644
--- a/tests/test_factory.py
+++ b/tests/test_factory.py
@@ -8,6 +8,8 @@ import string
 import sys
 from ipaddress import ip_address, ip_network
 
+import logging
+
 try:
     from StringIO import StringIO
 except ImportError:  # pragma: no cover
@@ -123,6 +125,57 @@ class FactoryTestCase(unittest.TestCase):
         finally:
             sys.stdout = orig_stdout
 
+    def test_cli_seed(self):
+        from faker.cli import Command
+        orig_stdout = sys.stdout
+        try:
+            sys.stdout = StringIO()
+            base_args = ['faker', 'address']
+            target_args = ['--seed', '967']
+            commands = [Command(base_args + target_args), Command(base_args + target_args)]
+            cli_output = [None] * 2
+            for i in range(2):
+                commands[i].execute()
+                cli_output[i] = sys.stdout.getvalue()
+            cli_output[1] = cli_output[1][len(cli_output[0]):]
+            self.assertEqual(cli_output[0][:10], cli_output[1][:10])
+        finally:
+            sys.stdout = orig_stdout
+
+    def test_cli_seed_with_repeat(self):
+        from faker.cli import Command
+        orig_stdout = sys.stdout
+        try:
+            sys.stdout = StringIO()
+            base_args = ['faker', 'address', '-r', '3']
+            target_args = ['--seed', '967']
+            commands = [Command(base_args + target_args), Command(base_args + target_args)]
+            cli_output = [None] * 2
+            for i in range(2):
+                commands[i].execute()
+                cli_output[i] = sys.stdout.getvalue()
+            cli_output[1] = cli_output[1][len(cli_output[0]):]
+            self.assertEqual(cli_output[0], cli_output[1])
+        finally:
+            sys.stdout = orig_stdout
+
+    def test_cli_verbosity(self):
+        from faker.cli import Command
+        orig_stdout = sys.stdout
+        try:
+            sys.stdout = StringIO()
+            base_args = ['faker', 'address', '--seed', '769']
+            target_args = ['-v']
+            commands = [Command(base_args), Command(base_args + target_args)]
+            cli_output = [None] * 2
+            for i in range(2):
+                commands[i].execute()
+                cli_output[i] = sys.stdout.getvalue()
+            simple_output, verbose_output = cli_output
+            self.assertNotEqual(simple_output, verbose_output)
+        finally:
+            sys.stdout = orig_stdout
+
     def test_slugify(self):
         slug = text.slugify("a'b/c")
         self.assertEqual(slug, 'abc')