Repository: joke2k/faker
Pull request #1121 (2020-02-27 13:07:03)

locale pt-BR fixed official cellphone_formats and PhoneNumberProvider without DDD
https://en.wikipedia.org/wiki/Telephone_numbers_in_Brazil

All phone numbers must have the DDD

### What does this changes
The variation of phone_number generation on locale pt-BR

### What was wrong
#### PhoneNumber formats
the format '####-####' is a bad practice (without DDD)
#### msisdn_formats
the format '55AA#########' is wrong because in the example '55AA**4**12345678' the char at position 4 is only acceptable as a '9' digit else if must not exist this additional char.  
it must be '55AA12345678' or '55AA**9**12345678'
#### cellphone_formats
the format '+55 9#### ####' is wrong because is missing DDD, the correct would be '+55 ## 9#### ####'.

### How this fixes it
It fixes these issues by correcting the formats for random_element generation



Patch:
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 204de659..132da2c8 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -1,7 +1,7 @@
 Changelog
 =========
 
-`4.0.1 - 17-February-2019 <https://github.com/joke2k/faker/compare/v4.0.0...v4.0.1>`__
+`4.0.1 - 17-February-2020 <https://github.com/joke2k/faker/compare/v4.0.0...v4.0.1>`__
 --------------------------------------------------------------------------------------
 
 * Provider improvements for Russian locale:
@@ -24,12 +24,12 @@ Changelog
 * Add Birth Number to czech locale. Thanks @Jengah.
 * Add persons provider for ``fr_QC``. Thanks @Lionesk.
 
-`4.0.0 - 14-January-2019 <https://github.com/joke2k/faker/compare/v3.0.1...v4.0.0>`__
+`4.0.0 - 14-January-2020 <https://github.com/joke2k/faker/compare/v3.0.1...v4.0.0>`__
 -------------------------------------------------------------------------------------
 
 * Breaking change: Remove support for end-of-life Python 2.7.
 
-`3.0.1 - 14-January-2019 <https://github.com/joke2k/faker/compare/v3.0.0...v3.0.1>`__
+`3.0.1 - 14-January-2020 <https://github.com/joke2k/faker/compare/v3.0.0...v3.0.1>`__
 -------------------------------------------------------------------------------------
 
 **NOTE**: This is the last release to support Python 2.7.x.
diff --git a/faker/providers/automotive/fr_FR/__init__.py b/faker/providers/automotive/fr_FR/__init__.py
new file mode 100644
index 00000000..3fc9c7a2
--- /dev/null
+++ b/faker/providers/automotive/fr_FR/__init__.py
@@ -0,0 +1,11 @@
+from .. import Provider as AutomotiveProvider
+
+
+class Provider(AutomotiveProvider):
+    # Source (english): https://en.wikipedia.org/wiki/Vehicle_registration_plates_of_France
+    license_formats = (
+        # New format
+        '??-###-??',
+        # Old format for plates < 2009
+        '###-???-##',
+    )
diff --git a/faker/providers/date_time/__init__.py b/faker/providers/date_time/__init__.py
index 39dab838..d0d33aae 100644
--- a/faker/providers/date_time/__init__.py
+++ b/faker/providers/date_time/__init__.py
@@ -29,6 +29,29 @@ def timestamp_to_datetime(timestamp, tzinfo):
     return pick
 
 
+def change_year(current_date, year_diff):
+    """
+    Unless the current_date is February 29th, it is fine to just subtract years.
+    If it is a leap day, and we are rolling back to a non-leap year, it will
+    cause a ValueError.
+    Since this is relatively uncommon, just catch the error and roll forward to
+    March 1
+
+    current_date: date  object
+    year_diff: int year delta value, positive or negative
+    """
+    year = current_date.year + year_diff
+    try:
+        return current_date.replace(year=year)
+    except ValueError as e:
+        # ValueError thrown if trying to move date to a non-leap year if the current
+        # date is February 29th
+        if year != 0 and current_date.month == 2 and current_date.day == 29:
+            return current_date.replace(month=3, day=1, year=year)
+        else:
+            raise e
+
+
 class ParseError(ValueError):
     pass
 
@@ -1996,8 +2019,8 @@ class Provider(BaseProvider):
         # boundary.
 
         now = datetime.now(tzinfo).date()
-        start_date = now.replace(year=now.year - (maximum_age+1))
-        end_date = now.replace(year=now.year - minimum_age)
+        start_date = change_year(now, -(maximum_age + 1))
+        end_date = change_year(now, -minimum_age)
 
         dob = self.date_time_ad(tzinfo=tzinfo, start_datetime=start_date, end_datetime=end_date).date()
 
diff --git a/faker/providers/internet/__init__.py b/faker/providers/internet/__init__.py
index 79a97816..60dab824 100644
--- a/faker/providers/internet/__init__.py
+++ b/faker/providers/internet/__init__.py
@@ -76,6 +76,10 @@ class Provider(BaseProvider):
         '.html', '.html', '.html', '.htm', '.htm', '.php', '.php', '.jsp',
         '.asp',
     )
+    http_methods = (
+        'GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'CONNECT', 'OPTIONS', 'TRACE',
+        'PATCH',
+    )
 
     user_name_formats = (
         '{{last_name}}.{{first_name}}',
@@ -221,6 +225,15 @@ class Provider(BaseProvider):
     def tld(self):
         return self.random_element(self.tlds)
 
+    def http_method(self):
+        """Returns random HTTP method
+        https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods
+
+        :rtype: str
+        """
+
+        return self.random_element(self.http_methods)
+
     def url(self, schemes=None):
         """
         :param schemes: a list of strings to use as schemes, one will chosen randomly.
diff --git a/faker/providers/phone_number/pt_BR/__init__.py b/faker/providers/phone_number/pt_BR/__init__.py
index 72ff4f5d..5e944dd0 100644
--- a/faker/providers/phone_number/pt_BR/__init__.py
+++ b/faker/providers/phone_number/pt_BR/__init__.py
@@ -11,6 +11,7 @@ class Provider(PhoneNumberProvider):
         '+55 (061) #### ####',
         '+55 (071) #### ####',
         '+55 (081) #### ####',
+        '+55 (084) #### ####',
         '+55 11 #### ####',
         '+55 21 #### ####',
         '+55 31 #### ####',
@@ -19,6 +20,7 @@ class Provider(PhoneNumberProvider):
         '+55 61 #### ####',
         '+55 71 #### ####',
         '+55 81 #### ####',
+        '+55 84 #### ####',
         '+55 (011) ####-####',
         '+55 (021) ####-####',
         '+55 (031) ####-####',
@@ -27,6 +29,7 @@ class Provider(PhoneNumberProvider):
         '+55 (061) ####-####',
         '+55 (071) ####-####',
         '+55 (081) ####-####',
+        '+55 (084) ####-####',
         '+55 11 ####-####',
         '+55 21 ####-####',
         '+55 31 ####-####',
@@ -35,6 +38,7 @@ class Provider(PhoneNumberProvider):
         '+55 61 ####-####',
         '+55 71 ####-####',
         '+55 81 ####-####',
+        '+55 84 ####-####',
         '(011) #### ####',
         '(021) #### ####',
         '(031) #### ####',
@@ -43,6 +47,7 @@ class Provider(PhoneNumberProvider):
         '(061) #### ####',
         '(071) #### ####',
         '(081) #### ####',
+        '(084) #### ####',
         '11 #### ####',
         '21 #### ####',
         '31 #### ####',
@@ -51,6 +56,7 @@ class Provider(PhoneNumberProvider):
         '61 #### ####',
         '71 #### ####',
         '81 #### ####',
+        '84 #### ####',
         '(011) ####-####',
         '(021) ####-####',
         '(031) ####-####',
@@ -59,6 +65,7 @@ class Provider(PhoneNumberProvider):
         '(061) ####-####',
         '(071) ####-####',
         '(081) ####-####',
+        '(084) ####-####',
         '11 ####-####',
         '21 ####-####',
         '31 ####-####',
@@ -67,23 +74,41 @@ class Provider(PhoneNumberProvider):
         '61 ####-####',
         '71 ####-####',
         '81 ####-####',
-        '#### ####',
-        '####-####',
+        '84 ####-####',
     )
 
     msisdn_formats = (
-        '5511#########',
-        '5521#########',
-        '5531#########',
-        '5541#########',
-        '5551#########',
-        '5561#########',
-        '5571#########',
-        '5581#########',
+        '5511########',
+        '5521########',
+        '5531########',
+        '5541########',
+        '5551########',
+        '5561########',
+        '5571########',
+        '5581########',
+        '5584########',
+        '55119########',
+        '55219########',
+        '55319########',
+        '55419########',
+        '55519########',
+        '55619########',
+        '55719########',
+        '55819########',
+        '55849########',
     )
 
     cellphone_formats = (
-        '+55 9#### ####',
+        '+55 ## 9#### ####',
+        '+55 ## 9 #### ####',
+        '+55 (0##) 9#### ####',
+        '+55 (##) 9#### ####',
+        '+55 (##) 9 #### ####',
+        '+55 ## 9####-####',
+        '+55 ## 9 ####-####',
+        '+55 (0##) 9####-####',
+        '+55 (##) 9####-####',
+        '+55 (##) 9 ####-####',
     )
 
     def cellphone_number(self):


Test patch:
diff --git a/tests/providers/test_automotive.py b/tests/providers/test_automotive.py
index 7ad2874f..ddffb945 100644
--- a/tests/providers/test_automotive.py
+++ b/tests/providers/test_automotive.py
@@ -133,3 +133,16 @@ class TestRuRU(unittest.TestCase):
     def test_vehicle_category(self):
         category = self.fake.vehicle_category()
         assert isinstance(category, str)
+
+
+class TestFrFR(unittest.TestCase):
+
+    def setUp(self):
+        self.fake = Faker('fr_FR')
+        Faker.seed(0)
+        self.pattern = re.compile(r'^\d{3}-[A-Z]{3}-\d{2}$|^[A-Z]{2}-\d{3}-[A-Z]{2}')
+
+    def test_fr_FR_plate_format(self):
+        plate = self.fake.license_plate()
+        assert isinstance(plate, str)
+        assert self.pattern.match(plate)
diff --git a/tests/providers/test_date_time.py b/tests/providers/test_date_time.py
index 30e9cf09..82c4c8c3 100644
--- a/tests/providers/test_date_time.py
+++ b/tests/providers/test_date_time.py
@@ -10,10 +10,12 @@ from datetime import date, datetime
 from datetime import time as datetime_time
 from datetime import timedelta, tzinfo
 
+import freezegun
 import pytest
 
 from faker import Faker
 from faker.providers.date_time import Provider as DatetimeProvider
+from faker.providers.date_time import change_year
 from faker.providers.date_time.ar_AA import Provider as ArProvider
 from faker.providers.date_time.ar_EG import Provider as EgProvider
 from faker.providers.date_time.hy_AM import Provider as HyAmProvider
@@ -369,8 +371,8 @@ class TestDateTime(unittest.TestCase):
 
     def test_date_time_between(self):
         now = datetime.now()
-        _30_years_ago = now.replace(year=now.year - 30)
-        _20_years_ago = now.replace(year=now.year - 20)
+        _30_years_ago = change_year(now, -30)
+        _20_years_ago = change_year(now, -20)
 
         random_datetime = self.fake.date_time_between(start_date='-30y', end_date='-20y')
 
@@ -379,8 +381,8 @@ class TestDateTime(unittest.TestCase):
 
     def test_date_between(self):
         today = date.today()
-        _30_years_ago = today.replace(year=today.year - 30)
-        _20_years_ago = today.replace(year=today.year - 20)
+        _30_years_ago = change_year(today, -30)
+        _20_years_ago = change_year(today, -20)
 
         random_date = self.fake.date_between(start_date='-30y', end_date='-20y')
 
@@ -501,6 +503,20 @@ class TestDateTime(unittest.TestCase):
             if platform.system() != 'Windows':
                 del os.environ['TZ']
 
+    def test_change_year(self):
+        _2020_06_01 = datetime.strptime("2020-06-01", "%Y-%m-%d")
+        _20_years_ago = change_year(_2020_06_01, -20)
+        self.assertEqual(_20_years_ago.strftime("%Y-%m-%d"), "2000-06-01")
+        # Verify a leap day
+        today = datetime.strptime("2020-02-29", "%Y-%m-%d")
+        with self.assertRaises(ValueError):
+            _11_years_ago = today.replace(year=11)
+        _11_years_ago = change_year(today, -11)
+        self.assertEqual(_11_years_ago.strftime("%Y-%m-%d"), "2009-03-01")
+        # 0 is an invalid year, so it should still raise a ValueError
+        with self.assertRaises(ValueError):
+            change_year(today, -today.year)
+
 
 class TestPlPL(unittest.TestCase):
 
@@ -593,6 +609,15 @@ class DatesOfBirth(unittest.TestCase):
         dob = self.fake.date_of_birth()
         assert isinstance(dob, date)
 
+    @freezegun.freeze_time('2020-02-29')
+    def test_date_of_birth_on_leap_day(self):
+        """
+        Freeze the date to a leap day to verify that the date_of_birth method  does not
+        raise an error
+        """
+        dob = self.fake.date_of_birth()
+        assert isinstance(dob, date)
+
     def test_value_errors(self):
         with self.assertRaises(ValueError):
             self.fake.date_of_birth(minimum_age=-1)
@@ -622,7 +647,7 @@ class DatesOfBirth(unittest.TestCase):
             now = datetime.now(utc).date()
 
             days_since_now = now - now
-            days_since_six_years_ago = now - now.replace(year=now.year-6)
+            days_since_six_years_ago = now - change_year(now, -6)
 
             dob = self.fake.date_of_birth(tzinfo=utc, minimum_age=0, maximum_age=5)
             days_since_dob = now - dob
@@ -635,7 +660,7 @@ class DatesOfBirth(unittest.TestCase):
             now = datetime.now(utc).date()
 
             days_since_now = now - now
-            days_since_nineteen_years_ago = now - now.replace(year=now.year-19)
+            days_since_nineteen_years_ago = now - change_year(now, -19)
 
             dob = self.fake.date_of_birth(tzinfo=utc, minimum_age=0, maximum_age=18)
             days_since_dob = now - dob
@@ -647,8 +672,8 @@ class DatesOfBirth(unittest.TestCase):
         for _ in range(100):
             now = datetime.now(utc).date()
 
-            days_since_five_years_ago = now - now.replace(year=now.year-5)
-            days_since_six_years_ago = now - now.replace(year=now.year-6)
+            days_since_five_years_ago = now - change_year(now, -5)
+            days_since_six_years_ago = now - change_year(now, -6)
 
             dob = self.fake.date_of_birth(tzinfo=utc, minimum_age=5, maximum_age=5)
             days_since_dob = now - dob
@@ -660,8 +685,8 @@ class DatesOfBirth(unittest.TestCase):
         for _ in range(100):
             now = datetime.now(utc).date()
 
-            days_since_one_hundred_years_ago = now - now.replace(year=now.year-100)
-            days_since_one_hundred_eleven_years_ago = now - now.replace(year=now.year-111)
+            days_since_one_hundred_years_ago = now - change_year(now, -100)
+            days_since_one_hundred_eleven_years_ago = now - change_year(now, -111)
 
             dob = self.fake.date_of_birth(minimum_age=100, maximum_age=110)
             days_since_dob = now - dob
diff --git a/tests/providers/test_phone_number.py b/tests/providers/test_phone_number.py
index 5baa268b..8bbb2ef9 100644
--- a/tests/providers/test_phone_number.py
+++ b/tests/providers/test_phone_number.py
@@ -68,20 +68,26 @@ class TestPtBr(unittest.TestCase):
         self.fake = Faker('pt_br')
         Faker.seed(0)
 
+    def test_phone_number(self):
+        phone_number = self.fake.phone_number()
+        re.match(r"^[+]?([0-9]{2})?[ ]?[(]?[0]?\d{2}[)]?[ ]?[09]?[ ]?\d{4}[ -]?\d{4}$", phone_number)
+
     def test_msisdn(self):
         msisdn = self.fake.msisdn()
-        formats = ('5511', '5521', '5531', '5541', '5551', '5561', '5571', '5581')
+        formats = ('5511', '5521', '5531', '5541', '5551', '5561', '5571', '5581', '5584')
 
         assert msisdn is not None
         assert isinstance(msisdn, str)
-        assert len(msisdn) == 13
+        assert len(msisdn) == 13 or len(msisdn) == 12
         assert msisdn.isdigit()
         assert msisdn[0:4] in formats
+        re.match(r"^[+]?([0-9]{2})?[ ]?[(]?[0]?\d{2}[)]?[ ]?[09]?[ ]?\d{4}[ -]?\d{4}$", msisdn)
 
     def test_cellphone(self):
         cellphone = self.fake.cellphone_number()
         assert cellphone is not None
-        assert len(cellphone) == 14
+        assert cellphone[0:3] == '+55'
+        re.match(r"^[+]?([0-9]{2})?[ ]?[(]?[0]?\d{2}[)]?[ ]?[09]?[ ]?\d{4}[ -]?\d{4}$", cellphone)
 
 
 class TestHuHU(unittest.TestCase):
diff --git a/tests/test_factory.py b/tests/test_factory.py
index 148b1790..a93df7cf 100644
--- a/tests/test_factory.py
+++ b/tests/test_factory.py
@@ -876,6 +876,19 @@ class FactoryTestCase(unittest.TestCase):
             assert 1024 <= faker.port_number(is_user=True) <= 49151
             assert 49152 <= faker.port_number(is_dynamic=True) <= 65535
 
+    def test_http_method(self):
+        faker = Faker()
+        expected_methods = [
+            'CONNECT', 'DELETE', 'GET', 'HEAD', 'OPTIONS', 'PATCH', 'POST',
+            'PUT', 'TRACE',
+        ]
+
+        got_methods = set()
+        for _ in range(99):
+            got_methods.add(faker.http_method())
+
+        assert expected_methods == sorted(got_methods)
+
     def test_random_sample_unique(self):
         from faker.providers import BaseProvider
         provider = BaseProvider(self.generator)