Repository: joke2k/faker
Pull request #1766 (2022-12-14 15:48:23)

Add fr ca date time provider
See issue #1764 for detail.


davebulaval: I was unsure if you wanted to duplicate the `TestFrFr` for coverage purposes. I think the less we can do is to add a mention of the shared format. Also, I have renamed the `TestFrFr` to `TestFrFrAndFrCa` to reflect this shared format.
davebulaval: Not quite sure of the failure reason.

Patch:
diff --git a/.bumpversion.cfg b/.bumpversion.cfg
index e94cfc84..44a52a99 100644
--- a/.bumpversion.cfg
+++ b/.bumpversion.cfg
@@ -1,5 +1,5 @@
 [bumpversion]
-current_version = 15.3.4
+current_version = 16.1.0
 files = VERSION faker/__init__.py docs/conf.py
 commit = True
 tag = True
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 39514ecf..4c4b8e10 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -260,7 +260,7 @@ jobs:
         python-version: '3.x'
     - name: Install dependencies
       run: |
-        python -m pip install tox
+        python -m pip install tox==3.27.1
     - name: Run tests
       run: tox
       env:
@@ -287,7 +287,7 @@ jobs:
         python-version: '3.x'
     - name: Install dependencies
       run: |
-        python -m pip install tox
+        python -m pip install tox==3.27.1
     - name: Run tests
       run: tox
       env:
diff --git a/.github/workflows/issues.yml b/.github/workflows/issues.yml
index a1e435b5..9ab12747 100644
--- a/.github/workflows/issues.yml
+++ b/.github/workflows/issues.yml
@@ -10,7 +10,7 @@ jobs:
       issues: write
       pull-requests: write
     steps:
-      - uses: actions/stale@v6
+      - uses: actions/stale@v7
         with:
           days-before-issue-stale: 90
           days-before-issue-close: 14
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 37d1aabd..572a4fb4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,17 @@
 ## Changelog
 
+### [v16.1.0 - 2023-01-09](https://github.com/joke2k/faker/compare/v16.0.1...v16.1.0)
+
+* Allow passing optional json encoder in `json` provider. Thanks @NI1993.
+
+### [v16.0.1 - 2023-01-09](https://github.com/joke2k/faker/compare/v16.0.0...v16.0.1)
+
+* Fix missing comma in `en_NZ` address provider. Thanks @davebulaval.
+
+### [v16.0.0 - 2023-01-09](https://github.com/joke2k/faker/compare/v15.3.4...v16.0.0)
+
+* Fix amount and layout of sample data. Thanks @mportesdev.
+
 ### [v15.3.4 - 2022-11-30](https://github.com/joke2k/faker/compare/v15.3.3...v15.3.4)
 
 * Fix `it_IT` `SSN` generation with latin chars in name or surname. Thanks @panicofr. 
diff --git a/README.rst b/README.rst
index 93695aa0..f796e635 100644
--- a/README.rst
+++ b/README.rst
@@ -422,12 +422,18 @@ Run tests:
 
     $ tox
 
-Write documentation for providers:
+Write documentation for the providers of the default locale:
 
 .. code:: bash
 
     $ python -m faker > docs.txt
 
+Write documentation for the providers of a specific locale:
+
+.. code:: bash
+
+    $ python -m faker --lang=de_DE > docs_de.txt
+
 
 Contribute
 ----------
diff --git a/VERSION b/VERSION
index 59c8a50a..e0228cf1 100644
--- a/VERSION
+++ b/VERSION
@@ -1,1 +1,1 @@
-15.3.4
+16.1.0
diff --git a/docs/communityproviders.rst b/docs/communityproviders.rst
index 5fb206a5..be9f49f6 100644
--- a/docs/communityproviders.rst
+++ b/docs/communityproviders.rst
@@ -21,6 +21,9 @@ Here's a list of Providers written by the community:
 | Education     | Public school name and    | `faker_education`_               |
 |               | info for testing purposes |                                  |
 +---------------+---------------------------+----------------------------------+
+| Faker File    | Generate files with fake  | `faker-file`_                    |
+|               | content                   |                                  |
++---------------+---------------------------+----------------------------------+
 | Geoscience    | Earth sciences-related    | `faker_geoscience`_              |
 |               | fake-random data          |                                  |
 |               | generators.               |                                  |
@@ -64,6 +67,7 @@ In order to be included, your provider must satisfy these requirement:
 .. _faker_biology: https://pypi.org/project/faker_biology/
 .. _faker_credit_score: https://pypi.org/project/faker-credit-score/
 .. _faker_education: https://pypi.org/project/faker_education/
+.. _faker-file: https://pypi.org/project/faker-file/
 .. _faker_geoscience: https://pypi.org/project/faker-geoscience/
 .. _faker_microservice: https://pypi.org/project/faker-microservice/
 .. _faker_music: https://pypi.org/project/faker_music/
diff --git a/docs/conf.py b/docs/conf.py
index ef2f0ac9..5ee74e8d 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -51,9 +51,9 @@ copyright = "2014, Daniele Faraglia"
 # built documents.
 #
 # The short X.Y version.
-version = "15.3.4"
+version = "16.1.0"
 # The full version, including alpha/beta/rc tags.
-release = "15.3.4"
+release = "16.1.0"
 
 # The language for content autogenerated by Sphinx. Refer to documentation
 # for a list of supported languages.
diff --git a/faker/__init__.py b/faker/__init__.py
index 7a8981b1..1063a5d1 100644
--- a/faker/__init__.py
+++ b/faker/__init__.py
@@ -2,6 +2,6 @@ from faker.factory import Factory
 from faker.generator import Generator
 from faker.proxy import Faker
 
-VERSION = "15.3.4"
+VERSION = "16.1.0"
 
 __all__ = ("Factory", "Generator", "Faker")
diff --git a/faker/cli.py b/faker/cli.py
index 2973edee..75513fff 100644
--- a/faker/cli.py
+++ b/faker/cli.py
@@ -1,8 +1,10 @@
 import argparse
+import itertools
 import logging
 import os
 import random
 import sys
+import textwrap
 
 from io import TextIOWrapper
 from pathlib import Path
@@ -34,11 +36,18 @@ def print_provider(
     print(f"### {doc.get_provider_name(provider)}", file=output)
     print(file=output)
 
+    margin = max(30, doc.max_name_len + 2)
     for signature, example in formatters.items():
         if signature in excludes:
             continue
+        signature_lines = textwrap.wrap(signature, width=margin, subsequent_indent="  ")
         try:
-            lines = str(example).expandtabs().splitlines()
+            lines = textwrap.wrap(
+                str(example).expandtabs(),
+                width=150 - margin,
+                initial_indent="# ",
+                subsequent_indent="  ",
+            )
         except UnicodeDecodeError:
             # The example is actually made of bytes.
             # We could coerce to bytes, but that would fail anyway when we wiil
@@ -46,16 +55,8 @@ def print_provider(
             lines = ["<bytes>"]
         except UnicodeEncodeError:
             raise Exception(f"error on {signature!r} with value {example!r}")
-        margin = max(30, doc.max_name_len + 1)
-        remains = 150 - margin
-        separator = "#"
-        for line in lines:
-            for i in range(0, (len(line) // remains) + 1):
-                print(
-                    f"\t{signature:<{margin}}{separator} {line[i * remains:(i + 1) * remains]}",
-                    file=output,
-                )
-                signature = separator = " "
+        for left, right in itertools.zip_longest(signature_lines, lines, fillvalue=""):
+            print(f"\t{left:<{margin}}  {right}", file=output)
 
 
 def print_doc(
@@ -112,23 +113,6 @@ def print_doc(
         for provider, fakers in formatters:
             print_provider(doc, provider, fakers, output=output)
 
-        for language in AVAILABLE_LOCALES:
-            if language == lang:
-                continue
-            print(file=output)
-            print(f"## LANGUAGE {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,
-                locale=language,
-                excludes=base_provider_formatters + unsupported,
-            ):
-                print_provider(d, p, fs, output=output)
-
 
 class Command:
     def __init__(self, argv: Optional[str] = None) -> None:
diff --git a/faker/documentor.py b/faker/documentor.py
index 78aff989..02d71476 100644
--- a/faker/documentor.py
+++ b/faker/documentor.py
@@ -119,7 +119,7 @@ class Documentor:
                 continue
             formatters[signature] = example
 
-            self.max_name_len = max(self.max_name_len, len(signature))
+            self.max_name_len = max(self.max_name_len, *(len(part) for part in signature.split()))
             self.already_generated.append(name)
 
         return formatters
diff --git a/faker/providers/address/en_NZ/__init__.py b/faker/providers/address/en_NZ/__init__.py
index be6ad009..77e59cdc 100644
--- a/faker/providers/address/en_NZ/__init__.py
+++ b/faker/providers/address/en_NZ/__init__.py
@@ -31,7 +31,8 @@ class Provider(AddressProvider):
         "port",
         "mouth",
         "stone",
-        "ings" "mouth",
+        "ings",
+        "mouth",
         "fort",
         "haven",
         "leigh",
diff --git a/faker/providers/date_time/fr_CA/__init__.py b/faker/providers/date_time/fr_CA/__init__.py
new file mode 100644
index 00000000..3756d8a6
--- /dev/null
+++ b/faker/providers/date_time/fr_CA/__init__.py
@@ -0,0 +1,8 @@
+from typing import Any
+
+from ..fr_FR import Provider as FRFRProvider
+
+
+class Provider(FRFRProvider):
+    def __init__(self, *args: Any, **kwargs: Any) -> None:
+        super().__init__(*args, **kwargs)
diff --git a/faker/providers/misc/__init__.py b/faker/providers/misc/__init__.py
index 215047ba..b37953ab 100644
--- a/faker/providers/misc/__init__.py
+++ b/faker/providers/misc/__init__.py
@@ -9,7 +9,7 @@ import tarfile
 import uuid
 import zipfile
 
-from typing import Any, Callable, Dict, List, Optional, Sequence, Set, Tuple, Union
+from typing import Any, Callable, Dict, List, Optional, Sequence, Set, Tuple, Type, Union
 
 from faker.exceptions import UnsupportedFeature
 
@@ -488,7 +488,13 @@ class Provider(BaseProvider):
             delimiter="|",
         )
 
-    def json(self, data_columns: Optional[List] = None, num_rows: int = 10, indent: Optional[int] = None) -> str:
+    def json(
+        self,
+        data_columns: Optional[List] = None,
+        num_rows: int = 10,
+        indent: Optional[int] = None,
+        cls: Optional[Type[json.JSONEncoder]] = None,
+    ) -> str:
         """
         Generate random JSON structure values.
 
@@ -521,6 +527,8 @@ class Provider(BaseProvider):
         :type num_rows: int
         :param indent: number of spaces to indent the fields
         :type indent: int
+        :param cls: optional json encoder to use for non-standard objects such as datetimes
+        :type cls: json.JSONEncoder
         :return: Serialized JSON data
         :rtype: str
 
@@ -585,10 +593,10 @@ class Provider(BaseProvider):
             raise TypeError("Invalid data_columns type. Must be a dictionary or list")
 
         if num_rows == 1:
-            return json.dumps(create_json_structure(data_columns), indent=indent)
+            return json.dumps(create_json_structure(data_columns), indent=indent, cls=cls)
 
         data = [create_json_structure(data_columns) for _ in range(num_rows)]
-        return json.dumps(data, indent=indent)
+        return json.dumps(data, indent=indent, cls=cls)
 
     def fixed_width(self, data_columns: Optional[list] = None, num_rows: int = 10, align: str = "left") -> str:
         """
diff --git a/tox.ini b/tox.ini
index d5f8f469..0a667658 100644
--- a/tox.ini
+++ b/tox.ini
@@ -58,11 +58,13 @@ commands =
 basepython = python
 passenv = TEST_32BIT
 commands = ./build32bit.sh
+allowlist_externals = ./build32bit.sh
 
 [testenv:alpine]
 basepython = python
 passenv = TEST_ALPINE
 commands = ./build-alpine.sh
+allowlist_externals = ./build-alpine.sh
 
 [flake8]
 max-line-length = 120


Test patch:
diff --git a/tests/providers/test_date_time.py b/tests/providers/test_date_time.py
index 4865a62b..58fd515c 100644
--- a/tests/providers/test_date_time.py
+++ b/tests/providers/test_date_time.py
@@ -1207,3 +1207,18 @@ class TestFrFr(unittest.TestCase):
     def test_month(self):
         day = self.fake.month_name()
         assert day in FrFrProvider.MONTH_NAMES.values()
+
+
+class TestFrCa(unittest.TestCase):
+    # Both fr_FR and fr_CA share the same date format so we use the same test suite
+    def setUp(self):
+        self.fake = Faker("fr-CA")
+        Faker.seed(0)
+
+    def test_day(self):
+        day = self.fake.day_of_week()
+        assert day in FrFrProvider.DAY_NAMES.values()
+
+    def test_month(self):
+        day = self.fake.month_name()
+        assert day in FrFrProvider.MONTH_NAMES.values()
diff --git a/tests/providers/test_misc.py b/tests/providers/test_misc.py
index dbe4d671..695defe7 100644
--- a/tests/providers/test_misc.py
+++ b/tests/providers/test_misc.py
@@ -1,4 +1,5 @@
 import csv
+import datetime
 import io
 import itertools
 import json
@@ -51,6 +52,9 @@ class _FooBarProvider:
     def test_float(self, multi=1) -> float:
         return 1.1 * multi
 
+    def test_date_time(self) -> datetime.datetime:
+        return datetime.datetime(2022, 12, 22, 13, 42, 33, 123)
+
 
 class TestMiscProvider:
     """Test miscellaneous provider methods"""
@@ -670,6 +674,29 @@ class TestMiscProvider:
         assert json_data["dict"]["item1"] == "FooBars"
         assert json_data["dict"]["item2"] == "FooBar"
 
+    def test_json_type_integrity_datetime_using_encoder(self, faker_with_foobar):
+        class DateTimeEncoder(json.JSONEncoder):
+            def default(self, obj):
+                if isinstance(obj, datetime.datetime):
+                    return str(obj)
+
+                # Let the base class default method raise the TypeError
+                return json.JSONEncoder.default(self, obj)
+
+        kwargs = {
+            "data_columns": {"item1": "test_date_time"},
+            "num_rows": 1,
+            "cls": DateTimeEncoder,
+        }
+        json_data = json.loads(faker_with_foobar.json(**kwargs))
+        assert isinstance(json_data["item1"], str)
+        assert json_data["item1"] == "2022-12-22 13:42:33.000123"
+
+    def test_json_type_integrity_datetime_no_encoder(self, faker_with_foobar):
+        kwargs = {"data_columns": {"item1": "test_date_time"}, "num_rows": 1}
+        with pytest.raises(TypeError):
+            faker_with_foobar.json(**kwargs)
+
     def test_fixed_width_with_arguments(self, faker_with_foobar):
         kwargs = {
             "data_columns": [