From 80af201e58221e2106eb4264cf45c16282ed5270 Mon Sep 17 00:00:00 2001 From: Elaheh Salehi Rizi Date: Wed, 2 Mar 2022 14:39:26 +0100 Subject: [PATCH 01/80] Add Armenian locale --- arrow/constants.py | 2 + arrow/locales.py | 86 +++++++++++++++++++++++++++++++++++++++++++ tests/test_arrow.py | 4 ++ tests/test_locales.py | 64 ++++++++++++++++++++++++++++++++ 4 files changed, 156 insertions(+) diff --git a/arrow/constants.py b/arrow/constants.py index 4c6fa5cb..83627cd3 100644 --- a/arrow/constants.py +++ b/arrow/constants.py @@ -168,4 +168,6 @@ "kk-kz", "am", "am-et", + "hy-am", + "hy", } diff --git a/arrow/locales.py b/arrow/locales.py index d5652370..37791c8b 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -6271,3 +6271,89 @@ def describe( humanized = self.timeframes_only_distance[timeframe].format(trunc(abs(delta))) return humanized + + +class ArmenianLocale(Locale): + names = ["hy", "hy-am"] + past = "{0} առաջ" + future = "{0}ից" + and_word = "Եվ" + + timeframes = { + "now": "հիմա", + "second": "վայրկյան", + "seconds": "{0} վայրկյան", + "minute": "րոպե", + "minutes": "{0} րոպե", + "hour": "ժամ", + "hours": "{0} ժամ", + "day": "օր", + "days": "{0} օր", + "month": "ամիս", + "months": "{0} ամիս", + "year": "տարին", + "years": "{0} տարին", + "week": "շաբաթ", + "weeks": "{0} շաբաթ", + } + + meridians = { + "am": "Ամ", + "pm": "պ.մ.", + "AM": "Ամ", + "PM": "պ.մ.", + } + + month_names = [ + "", + "հունվար", + "փետրվար", + "մարտ", + "ապրիլ", + "մայիս", + "հունիս", + "հուլիս", + "օգոստոս", + "սեպտեմբեր", + "հոկտեմբեր", + "նոյեմբեր", + "դեկտեմբեր", + ] + + month_abbreviations = [ + "", + "հունվար", + "փետրվար", + "մարտ", + "ապրիլ", + "մայիս", + "հունիս", + "հուլիս", + "օգոստոս", + "սեպտեմբեր", + "հոկտեմբեր", + "նոյեմբեր", + "դեկտեմբեր", + ] + + day_names = [ + "", + "երկուշաբթի", + "երեքշաբթի", + "չորեքշաբթի", + "հինգշաբթի", + "ուրբաթ", + "շաբաթ", + "կիրակի", + ] + + day_abbreviations = [ + "", + "երկ.", + "երեք.", + "չորեք.", + "հինգ.", + "ուրբ.", + "շաբ.", + "կիր.", + ] diff --git a/tests/test_arrow.py b/tests/test_arrow.py index 2e2ffe91..742f41d4 100644 --- a/tests/test_arrow.py +++ b/tests/test_arrow.py @@ -2470,6 +2470,8 @@ def locale_list_no_weeks() -> List[str]: "ka-ge", "kk", "kk-kz", + "hy", + "hy-am", ] return tested_langs @@ -2544,6 +2546,8 @@ def locale_list_with_weeks() -> List[str]: "ta-lk", "kk", "kk-kz", + "hy", + "hy-am", ] return tested_langs diff --git a/tests/test_locales.py b/tests/test_locales.py index 0e42074d..3ad3a2a7 100644 --- a/tests/test_locales.py +++ b/tests/test_locales.py @@ -2995,3 +2995,67 @@ def test_weekday(self): dt = arrow.Arrow(2015, 4, 11, 17, 30, 00) assert self.locale.day_name(dt.isoweekday()) == "ቅዳሜ" assert self.locale.day_abbreviation(dt.isoweekday()) == "ዓ" + + +@pytest.mark.usefixtures("lang_locale") +class TestArmenianLocale: + def test_describe(self): + assert self.locale.describe("now", only_distance=True) == "հիմա" + assert self.locale.describe("now", only_distance=False) == "հիմա" + + def test_plurals(self): + assert self.locale._format_timeframe("now", 0) == "հիմա" + assert self.locale._format_timeframe("second", 1) == "վայրկյան" + assert self.locale._format_timeframe("seconds", 30) == "30 վայրկյան" + assert self.locale._format_timeframe("minute", 1) == "րոպե" + assert self.locale._format_timeframe("minutes", 40) == "40 րոպե" + assert self.locale._format_timeframe("hour", 1) == "ժամ" + assert self.locale._format_timeframe("hours", 23) == "23 ժամ" + assert self.locale._format_timeframe("day", 1) == "օր" + assert self.locale._format_timeframe("days", 12) == "12 օր" + assert self.locale._format_timeframe("month", 1) == "ամիս" + assert self.locale._format_timeframe("months", 11) == "11 ամիս" + assert self.locale._format_timeframe("year", 1) == "տարին" + assert self.locale._format_timeframe("years", 12) == "12 տարին" + + def test_format_timeframe(self): + # Second(s) + assert self.locale._format_timeframe("second", -1) == "վայրկյան" + assert self.locale._format_timeframe("second", 1) == "վայրկյան" + assert self.locale._format_timeframe("seconds", -3) == "3 վայրկյան" + assert self.locale._format_timeframe("seconds", 3) == "3 վայրկյան" + + # Minute(s) + assert self.locale._format_timeframe("minute", -1) == "րոպե" + assert self.locale._format_timeframe("minute", 1) == "րոպե" + assert self.locale._format_timeframe("minutes", -4) == "4 րոպե" + assert self.locale._format_timeframe("minutes", 4) == "4 րոպե" + + # Hour(s) + assert self.locale._format_timeframe("hour", -1) == "ժամ" + assert self.locale._format_timeframe("hour", 1) == "ժամ" + assert self.locale._format_timeframe("hours", -23) == "23 ժամ" + assert self.locale._format_timeframe("hours", 23) == "23 ժամ" + + # Day(s) + assert self.locale._format_timeframe("day", -1) == "օր" + assert self.locale._format_timeframe("day", 1) == "օր" + assert self.locale._format_timeframe("days", -12) == "12 օր" + assert self.locale._format_timeframe("days", 12) == "12 օր" + + # Month(s) + assert self.locale._format_timeframe("month", -1) == "ամիս" + assert self.locale._format_timeframe("month", 1) == "ամիս" + assert self.locale._format_timeframe("months", -2) == "2 ամիս" + assert self.locale._format_timeframe("months", 2) == "2 ամիս" + + # Year(s) + assert self.locale._format_timeframe("year", -1) == "տարին" + assert self.locale._format_timeframe("year", 1) == "տարին" + assert self.locale._format_timeframe("years", -2) == "2 տարին" + assert self.locale._format_timeframe("years", 2) == "2 տարին" + + def test_weekday(self): + dt = arrow.Arrow(2015, 4, 11, 17, 30, 00) + assert self.locale.day_name(dt.isoweekday()) == "շաբաթ" + assert self.locale.day_abbreviation(dt.isoweekday()) == "շաբ." From ec98f1b064cf8fde9aae88d2499f733be6443775 Mon Sep 17 00:00:00 2001 From: Elaheh Salehi Rizi Date: Thu, 17 Mar 2022 09:34:37 +0100 Subject: [PATCH 02/80] Add Armenian --- tests/test_locales.py | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/tests/test_locales.py b/tests/test_locales.py index 3ad3a2a7..98f55ddd 100644 --- a/tests/test_locales.py +++ b/tests/test_locales.py @@ -3003,20 +3003,11 @@ def test_describe(self): assert self.locale.describe("now", only_distance=True) == "հիմա" assert self.locale.describe("now", only_distance=False) == "հիմա" - def test_plurals(self): - assert self.locale._format_timeframe("now", 0) == "հիմա" - assert self.locale._format_timeframe("second", 1) == "վայրկյան" - assert self.locale._format_timeframe("seconds", 30) == "30 վայրկյան" - assert self.locale._format_timeframe("minute", 1) == "րոպե" - assert self.locale._format_timeframe("minutes", 40) == "40 րոպե" - assert self.locale._format_timeframe("hour", 1) == "ժամ" - assert self.locale._format_timeframe("hours", 23) == "23 ժամ" - assert self.locale._format_timeframe("day", 1) == "օր" - assert self.locale._format_timeframe("days", 12) == "12 օր" - assert self.locale._format_timeframe("month", 1) == "ամիս" - assert self.locale._format_timeframe("months", 11) == "11 ամիս" - assert self.locale._format_timeframe("year", 1) == "տարին" - assert self.locale._format_timeframe("years", 12) == "12 տարին" + def test_meridians_hy(self): + assert self.locale.meridian(7, "A") == "Ամ" + assert self.locale.meridian(18, "A") == "պ.մ." + assert self.locale.meridian(10, "a") == "Ամ" + assert self.locale.meridian(22, "a") == "պ.մ." def test_format_timeframe(self): # Second(s) From c0057fa48bba51c7d7eabd7f6455a07b941dde9f Mon Sep 17 00:00:00 2001 From: Elaheh Salehi Rizi Date: Thu, 17 Mar 2022 11:19:03 +0100 Subject: [PATCH 03/80] Add more Armenian tests --- tests/test_locales.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/test_locales.py b/tests/test_locales.py index 98f55ddd..37c857e3 100644 --- a/tests/test_locales.py +++ b/tests/test_locales.py @@ -3015,36 +3015,42 @@ def test_format_timeframe(self): assert self.locale._format_timeframe("second", 1) == "վայրկյան" assert self.locale._format_timeframe("seconds", -3) == "3 վայրկյան" assert self.locale._format_timeframe("seconds", 3) == "3 վայրկյան" + assert self.locale._format_timeframe("seconds", 30) == "30 վայրկյան" # Minute(s) assert self.locale._format_timeframe("minute", -1) == "րոպե" assert self.locale._format_timeframe("minute", 1) == "րոպե" assert self.locale._format_timeframe("minutes", -4) == "4 րոպե" assert self.locale._format_timeframe("minutes", 4) == "4 րոպե" + assert self.locale._format_timeframe("minutes", 40) == "40 րոպե" # Hour(s) assert self.locale._format_timeframe("hour", -1) == "ժամ" assert self.locale._format_timeframe("hour", 1) == "ժամ" assert self.locale._format_timeframe("hours", -23) == "23 ժամ" assert self.locale._format_timeframe("hours", 23) == "23 ժամ" + assert self.locale._format_timeframe("hours", 23) == "23 ժամ" # Day(s) assert self.locale._format_timeframe("day", -1) == "օր" assert self.locale._format_timeframe("day", 1) == "օր" assert self.locale._format_timeframe("days", -12) == "12 օր" assert self.locale._format_timeframe("days", 12) == "12 օր" + assert self.locale._format_timeframe("days", 12) == "12 օր" # Month(s) assert self.locale._format_timeframe("month", -1) == "ամիս" assert self.locale._format_timeframe("month", 1) == "ամիս" assert self.locale._format_timeframe("months", -2) == "2 ամիս" assert self.locale._format_timeframe("months", 2) == "2 ամիս" + assert self.locale._format_timeframe("months", 11) == "11 ամիս" # Year(s) assert self.locale._format_timeframe("year", -1) == "տարին" assert self.locale._format_timeframe("year", 1) == "տարին" assert self.locale._format_timeframe("years", -2) == "2 տարին" assert self.locale._format_timeframe("years", 2) == "2 տարին" + assert self.locale._format_timeframe("years", 12) == "12 տարին" def test_weekday(self): dt = arrow.Arrow(2015, 4, 11, 17, 30, 00) From 0543c48ad7979b067313153def6e1103ab49beb6 Mon Sep 17 00:00:00 2001 From: Elaheh Salehi Rizi Date: Thu, 17 Mar 2022 11:46:00 +0100 Subject: [PATCH 04/80] Delete duplicate Armenian tests --- tests/test_locales.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/test_locales.py b/tests/test_locales.py index 37c857e3..3ea584b3 100644 --- a/tests/test_locales.py +++ b/tests/test_locales.py @@ -3029,14 +3029,12 @@ def test_format_timeframe(self): assert self.locale._format_timeframe("hour", 1) == "ժամ" assert self.locale._format_timeframe("hours", -23) == "23 ժամ" assert self.locale._format_timeframe("hours", 23) == "23 ժամ" - assert self.locale._format_timeframe("hours", 23) == "23 ժամ" # Day(s) assert self.locale._format_timeframe("day", -1) == "օր" assert self.locale._format_timeframe("day", 1) == "օր" assert self.locale._format_timeframe("days", -12) == "12 օր" assert self.locale._format_timeframe("days", 12) == "12 օր" - assert self.locale._format_timeframe("days", 12) == "12 օր" # Month(s) assert self.locale._format_timeframe("month", -1) == "ամիս" From 1904804a8d19581397afb2a2b10304e741bcb921 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Sun, 1 May 2022 14:36:59 -0700 Subject: [PATCH 05/80] Fix failing CI lint task. (#1107) --- .pre-commit-config.yaml | 8 +++--- arrow/arrow.py | 6 ++--- arrow/constants.py | 2 +- tests/test_arrow.py | 24 ++++++++--------- tests/test_factory.py | 21 +++++++-------- tests/test_parser.py | 57 +++++++++++++++-------------------------- tox.ini | 2 +- 7 files changed, 51 insertions(+), 69 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 28b5f8a0..b352b70b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,7 +2,7 @@ default_language_version: python: python3 repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.1.0 + rev: v4.2.0 hooks: - id: check-ast - id: check-yaml @@ -22,7 +22,7 @@ repos: hooks: - id: isort - repo: https://github.com/asottile/pyupgrade - rev: v2.30.1 + rev: v2.32.0 hooks: - id: pyupgrade args: [--py36-plus] @@ -38,7 +38,7 @@ repos: - id: rst-inline-touching-normal - id: text-unicode-replacement-char - repo: https://github.com/psf/black - rev: 21.12b0 + rev: 22.3.0 hooks: - id: black args: [--safe, --quiet, --target-version=py36] @@ -48,7 +48,7 @@ repos: - id: flake8 additional_dependencies: [flake8-bugbear,flake8-annotations] - repo: https://github.com/pre-commit/mirrors-mypy - rev: 'v0.930' + rev: 'v0.950' hooks: - id: mypy additional_dependencies: [types-python-dateutil] diff --git a/arrow/arrow.py b/arrow/arrow.py index 21b0347f..1ede107f 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -1384,7 +1384,7 @@ def dehumanize(self, input_string: str, locale: str = "en_us") -> "Arrow": search_string = search_string.format(r"\d+") # Create search pattern and find within string - pattern = re.compile(fr"(^|\b|\d){search_string}") + pattern = re.compile(rf"(^|\b|\d){search_string}") match = pattern.search(input_string) # If there is no match continue to next iteration @@ -1426,12 +1426,12 @@ def dehumanize(self, input_string: str, locale: str = "en_us") -> "Arrow": # Sign logic future_string = locale_obj.future future_string = future_string.format(".*") - future_pattern = re.compile(fr"^{future_string}$") + future_pattern = re.compile(rf"^{future_string}$") future_pattern_match = future_pattern.findall(input_string) past_string = locale_obj.past past_string = past_string.format(".*") - past_pattern = re.compile(fr"^{past_string}$") + past_pattern = re.compile(rf"^{past_string}$") past_pattern_match = past_pattern.findall(input_string) # If a string contains the now unit, there will be no relative units, hence the need to check if the now unit diff --git a/arrow/constants.py b/arrow/constants.py index 4c6fa5cb..ce29bf1a 100644 --- a/arrow/constants.py +++ b/arrow/constants.py @@ -21,7 +21,7 @@ # Must get max value of ctime on Windows based on architecture (x32 vs x64) # https://docs.microsoft.com/en-us/cpp/c-runtime-library/reference/ctime-ctime32-ctime64-wctime-wctime32-wctime64 # Note: this may occur on both 32-bit Linux systems (issue #930) along with Windows systems - is_64bits = sys.maxsize > 2 ** 32 + is_64bits = sys.maxsize > 2**32 _MAX_TIMESTAMP = ( datetime(3000, 1, 1, 23, 59, 59, 999999).timestamp() if is_64bits diff --git a/tests/test_arrow.py b/tests/test_arrow.py index 2e2ffe91..863b3cf1 100644 --- a/tests/test_arrow.py +++ b/tests/test_arrow.py @@ -1909,7 +1909,7 @@ def test_granularity(self): assert self.now.humanize(later4000, granularity="day") == "0 days ago" assert later4000.humanize(self.now, granularity="day") == "in 0 days" - later105 = self.now.shift(seconds=10 ** 5) + later105 = self.now.shift(seconds=10**5) assert self.now.humanize(later105, granularity="hour") == "27 hours ago" assert later105.humanize(self.now, granularity="hour") == "in 27 hours" assert self.now.humanize(later105, granularity="day") == "a day ago" @@ -1921,7 +1921,7 @@ def test_granularity(self): assert self.now.humanize(later105, granularity=["month"]) == "0 months ago" assert later105.humanize(self.now, granularity=["month"]) == "in 0 months" - later106 = self.now.shift(seconds=3 * 10 ** 6) + later106 = self.now.shift(seconds=3 * 10**6) assert self.now.humanize(later106, granularity="day") == "34 days ago" assert later106.humanize(self.now, granularity="day") == "in 34 days" assert self.now.humanize(later106, granularity="week") == "4 weeks ago" @@ -1931,7 +1931,7 @@ def test_granularity(self): assert self.now.humanize(later106, granularity="year") == "0 years ago" assert later106.humanize(self.now, granularity="year") == "in 0 years" - later506 = self.now.shift(seconds=50 * 10 ** 6) + later506 = self.now.shift(seconds=50 * 10**6) assert self.now.humanize(later506, granularity="week") == "82 weeks ago" assert later506.humanize(self.now, granularity="week") == "in 82 weeks" assert self.now.humanize(later506, granularity="month") == "18 months ago" @@ -1943,27 +1943,27 @@ def test_granularity(self): assert self.now.humanize(later1, granularity="quarter") == "0 quarters ago" assert later1.humanize(self.now, granularity="quarter") == "in 0 quarters" - later107 = self.now.shift(seconds=10 ** 7) + later107 = self.now.shift(seconds=10**7) assert self.now.humanize(later107, granularity="quarter") == "a quarter ago" assert later107.humanize(self.now, granularity="quarter") == "in a quarter" - later207 = self.now.shift(seconds=2 * 10 ** 7) + later207 = self.now.shift(seconds=2 * 10**7) assert self.now.humanize(later207, granularity="quarter") == "2 quarters ago" assert later207.humanize(self.now, granularity="quarter") == "in 2 quarters" - later307 = self.now.shift(seconds=3 * 10 ** 7) + later307 = self.now.shift(seconds=3 * 10**7) assert self.now.humanize(later307, granularity="quarter") == "3 quarters ago" assert later307.humanize(self.now, granularity="quarter") == "in 3 quarters" - later377 = self.now.shift(seconds=3.7 * 10 ** 7) + later377 = self.now.shift(seconds=3.7 * 10**7) assert self.now.humanize(later377, granularity="quarter") == "4 quarters ago" assert later377.humanize(self.now, granularity="quarter") == "in 4 quarters" - later407 = self.now.shift(seconds=4 * 10 ** 7) + later407 = self.now.shift(seconds=4 * 10**7) assert self.now.humanize(later407, granularity="quarter") == "5 quarters ago" assert later407.humanize(self.now, granularity="quarter") == "in 5 quarters" - later108 = self.now.shift(seconds=10 ** 8) + later108 = self.now.shift(seconds=10**8) assert self.now.humanize(later108, granularity="year") == "3 years ago" assert later108.humanize(self.now, granularity="year") == "in 3 years" - later108onlydistance = self.now.shift(seconds=10 ** 8) + later108onlydistance = self.now.shift(seconds=10**8) assert ( self.now.humanize( later108onlydistance, only_distance=True, granularity="year" @@ -2012,7 +2012,7 @@ def test_multiple_granularity(self): == "0 days an hour and 6 minutes ago" ) - later105 = self.now.shift(seconds=10 ** 5) + later105 = self.now.shift(seconds=10**5) assert ( self.now.humanize(later105, granularity=["hour", "day", "minute"]) == "a day 3 hours and 46 minutes ago" @@ -2020,7 +2020,7 @@ def test_multiple_granularity(self): with pytest.raises(ValueError): self.now.humanize(later105, granularity=["error", "second"]) - later108onlydistance = self.now.shift(seconds=10 ** 8) + later108onlydistance = self.now.shift(seconds=10**8) assert ( self.now.humanize( later108onlydistance, only_distance=True, granularity=["year"] diff --git a/tests/test_factory.py b/tests/test_factory.py index 53bba20d..f368126c 100644 --- a/tests/test_factory.py +++ b/tests/test_factory.py @@ -359,18 +359,15 @@ def test_three_args(self): def test_full_kwargs(self): - assert ( - self.factory.get( - year=2016, - month=7, - day=14, - hour=7, - minute=16, - second=45, - microsecond=631092, - ) - == datetime(2016, 7, 14, 7, 16, 45, 631092, tzinfo=tz.tzutc()) - ) + assert self.factory.get( + year=2016, + month=7, + day=14, + hour=7, + minute=16, + second=45, + microsecond=631092, + ) == datetime(2016, 7, 14, 7, 16, 45, 631092, tzinfo=tz.tzutc()) def test_three_kwargs(self): diff --git a/tests/test_parser.py b/tests/test_parser.py index 4a4cfe41..bb4ab148 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -145,24 +145,18 @@ def test_YY_and_YYYY_format_list(self): 2019, 1, 15 ) - assert ( - self.parser.parse( - "15/01/2019T04:05:06.789120Z", - ["D/M/YYThh:mm:ss.SZ", "D/M/YYYYThh:mm:ss.SZ"], - ) - == datetime(2019, 1, 15, 4, 5, 6, 789120, tzinfo=tz.tzutc()) - ) + assert self.parser.parse( + "15/01/2019T04:05:06.789120Z", + ["D/M/YYThh:mm:ss.SZ", "D/M/YYYYThh:mm:ss.SZ"], + ) == datetime(2019, 1, 15, 4, 5, 6, 789120, tzinfo=tz.tzutc()) # regression test for issue #447 def test_timestamp_format_list(self): # should not match on the "X" token - assert ( - self.parser.parse( - "15 Jul 2000", - ["MM/DD/YYYY", "YYYY-MM-DD", "X", "DD-MMMM-YYYY", "D MMM YYYY"], - ) - == datetime(2000, 7, 15) - ) + assert self.parser.parse( + "15 Jul 2000", + ["MM/DD/YYYY", "YYYY-MM-DD", "X", "DD-MMMM-YYYY", "D MMM YYYY"], + ) == datetime(2000, 7, 15) with pytest.raises(ParserError): self.parser.parse("15 Jul", "X") @@ -503,21 +497,15 @@ def test_parse_with_extra_words_at_start_and_end_valid(self): "2016-05-16T04:05:06.789120 blah", "YYYY-MM-DDThh:mm:ss.S" ) == datetime(2016, 5, 16, 4, 5, 6, 789120) - assert ( - self.parser.parse( - "Meet me at 2016-05-16T04:05:06.789120 at the restaurant.", - "YYYY-MM-DDThh:mm:ss.S", - ) - == datetime(2016, 5, 16, 4, 5, 6, 789120) - ) + assert self.parser.parse( + "Meet me at 2016-05-16T04:05:06.789120 at the restaurant.", + "YYYY-MM-DDThh:mm:ss.S", + ) == datetime(2016, 5, 16, 4, 5, 6, 789120) - assert ( - self.parser.parse( - "Meet me at 2016-05-16 04:05:06.789120 at the restaurant.", - "YYYY-MM-DD hh:mm:ss.S", - ) - == datetime(2016, 5, 16, 4, 5, 6, 789120) - ) + assert self.parser.parse( + "Meet me at 2016-05-16 04:05:06.789120 at the restaurant.", + "YYYY-MM-DD hh:mm:ss.S", + ) == datetime(2016, 5, 16, 4, 5, 6, 789120) # regression test for issue #701 # tests cases of a partial match surrounded by punctuation @@ -783,14 +771,11 @@ def test_parse_normalize_whitespace(self): with pytest.raises(ParserError): self.parser.parse("Jun 1 2005 1:33PM", "MMM D YYYY H:mmA") - assert ( - self.parser.parse( - "\t 2013-05-05 T \n 12:30:45\t123456 \t \n", - "YYYY-MM-DD T HH:mm:ss S", - normalize_whitespace=True, - ) - == datetime(2013, 5, 5, 12, 30, 45, 123456) - ) + assert self.parser.parse( + "\t 2013-05-05 T \n 12:30:45\t123456 \t \n", + "YYYY-MM-DD T HH:mm:ss S", + normalize_whitespace=True, + ) == datetime(2013, 5, 5, 12, 30, 45, 123456) with pytest.raises(ParserError): self.parser.parse( diff --git a/tox.ini b/tox.ini index fefa3e7e..c51432a2 100644 --- a/tox.ini +++ b/tox.ini @@ -45,4 +45,4 @@ include_trailing_comma = true [flake8] per-file-ignores = arrow/__init__.py:F401,tests/*:ANN001,ANN201 -ignore = E203,E501,W503,ANN101,ANN102 +ignore = E203,E501,W503,ANN101,ANN102,ANN401 From f98ad731779c38e01920d74fef9c4b4d24342f89 Mon Sep 17 00:00:00 2001 From: cyriaka90 Date: Mon, 2 May 2022 01:40:19 +0200 Subject: [PATCH 06/80] Add Laotian locale (#1105) Co-authored-by: Jad Chaar --- arrow/constants.py | 2 + arrow/locales.py | 108 ++++++++++++++++++++++++++++++++++++++++++ tests/test_arrow.py | 2 + tests/test_locales.py | 57 ++++++++++++++++++++++ 4 files changed, 169 insertions(+) diff --git a/arrow/constants.py b/arrow/constants.py index ce29bf1a..b06f7b13 100644 --- a/arrow/constants.py +++ b/arrow/constants.py @@ -166,6 +166,8 @@ "ka-ge", "kk", "kk-kz", + # "lo", + # "lo-la", "am", "am-et", } diff --git a/arrow/locales.py b/arrow/locales.py index d5652370..f6723741 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -3971,6 +3971,114 @@ def _format_relative( return relative_string +class LaotianLocale(Locale): + + names = ["lo", "lo-la"] + + past = "{0} ກ່ອນຫນ້ານີ້" + future = "ໃນ {0}" + + timeframes = { + "now": "ດຽວນີ້", + "second": "ວິນາທີ", + "seconds": "{0} ວິນາທີ", + "minute": "ນາທີ", + "minutes": "{0} ນາທີ", + "hour": "ຊົ່ວໂມງ", + "hours": "{0} ຊົ່ວໂມງ", + "day": "ມື້", + "days": "{0} ມື້", + "week": "ອາທິດ", + "weeks": "{0} ອາທິດ", + "month": "ເດືອນ", + "months": "{0} ເດືອນ", + "year": "ປີ", + "years": "{0} ປີ", + } + + month_names = [ + "", + "ມັງກອນ", # mangkon + "ກຸມພາ", # kumpha + "ມີນາ", # mina + "ເມສາ", # mesa + "ພຶດສະພາ", # phudsapha + "ມິຖຸນາ", # mithuna + "ກໍລະກົດ", # kolakod + "ສິງຫາ", # singha + "ກັນຍາ", # knaia + "ຕຸລາ", # tula + "ພະຈິກ", # phachik + "ທັນວາ", # thanuaa + ] + month_abbreviations = [ + "", + "ມັງກອນ", + "ກຸມພາ", + "ມີນາ", + "ເມສາ", + "ພຶດສະພາ", + "ມິຖຸນາ", + "ກໍລະກົດ", + "ສິງຫາ", + "ກັນຍາ", + "ຕຸລາ", + "ພະຈິກ", + "ທັນວາ", + ] + + day_names = [ + "", + "ວັນຈັນ", # vanchan + "ວັນອັງຄານ", # vnoangkhan + "ວັນພຸດ", # vanphud + "ວັນພະຫັດ", # vanphahad + "ວັນ​ສຸກ", # vansuk + "ວັນເສົາ", # vansao + "ວັນອາທິດ", # vnoathid + ] + day_abbreviations = [ + "", + "ວັນຈັນ", + "ວັນອັງຄານ", + "ວັນພຸດ", + "ວັນພະຫັດ", + "ວັນ​ສຸກ", + "ວັນເສົາ", + "ວັນອາທິດ", + ] + + BE_OFFSET = 543 + + def year_full(self, year: int) -> str: + """Lao always use Buddhist Era (BE) which is CE + 543""" + year += self.BE_OFFSET + return f"{year:04d}" + + def year_abbreviation(self, year: int) -> str: + """Lao always use Buddhist Era (BE) which is CE + 543""" + year += self.BE_OFFSET + return f"{year:04d}"[2:] + + def _format_relative( + self, + humanized: str, + timeframe: TimeFrameLiteral, + delta: Union[float, int], + ) -> str: + """Lao normally doesn't have any space between words""" + if timeframe == "now": + return humanized + + direction = self.past if delta < 0 else self.future + relative_string = direction.format(humanized) + + if timeframe == "seconds": + relative_string = relative_string.replace(" ", "") + + return relative_string + + class BengaliLocale(Locale): names = ["bn", "bn-bd", "bn-in"] diff --git a/tests/test_arrow.py b/tests/test_arrow.py index 863b3cf1..3809f128 100644 --- a/tests/test_arrow.py +++ b/tests/test_arrow.py @@ -2470,6 +2470,8 @@ def locale_list_no_weeks() -> List[str]: "ka-ge", "kk", "kk-kz", + # "lo", + # "lo-la", ] return tested_langs diff --git a/tests/test_locales.py b/tests/test_locales.py index 0e42074d..ed62dc80 100644 --- a/tests/test_locales.py +++ b/tests/test_locales.py @@ -1294,6 +1294,63 @@ def test_ordinal_number(self): assert self.locale.ordinal_number(1) == "1a" +@pytest.mark.usefixtures("lang_locale") +class TestLaotianLocale: + def test_year_full(self): + assert self.locale.year_full(2015) == "2558" + + def test_year_abbreviation(self): + assert self.locale.year_abbreviation(2015) == "58" + + def test_format_relative_now(self): + result = self.locale._format_relative("ດຽວນີ້", "now", 0) + assert result == "ດຽວນີ້" + + def test_format_relative_past(self): + result = self.locale._format_relative("1 ຊົ່ວໂມງ", "hour", 1) + assert result == "ໃນ 1 ຊົ່ວໂມງ" + result = self.locale._format_relative("{0} ຊົ່ວໂມງ", "hours", 2) + assert result == "ໃນ {0} ຊົ່ວໂມງ" + result = self.locale._format_relative("ວິນາທີ", "seconds", 42) + assert result == "ໃນວິນາທີ" + + def test_format_relative_future(self): + result = self.locale._format_relative("1 ຊົ່ວໂມງ", "hour", -1) + assert result == "1 ຊົ່ວໂມງ ກ່ອນຫນ້ານີ້" + + def test_format_timeframe(self): + # minute(s) + assert self.locale._format_timeframe("minute", 1) == "ນາທີ" + assert self.locale._format_timeframe("minute", -1) == "ນາທີ" + assert self.locale._format_timeframe("minutes", 7) == "7 ນາທີ" + assert self.locale._format_timeframe("minutes", -20) == "20 ນາທີ" + # day(s) + assert self.locale._format_timeframe("day", 1) == "ມື້" + assert self.locale._format_timeframe("day", -1) == "ມື້" + assert self.locale._format_timeframe("days", 7) == "7 ມື້" + assert self.locale._format_timeframe("days", -20) == "20 ມື້" + # week(s) + assert self.locale._format_timeframe("week", 1) == "ອາທິດ" + assert self.locale._format_timeframe("week", -1) == "ອາທິດ" + assert self.locale._format_timeframe("weeks", 7) == "7 ອາທິດ" + assert self.locale._format_timeframe("weeks", -20) == "20 ອາທິດ" + # month(s) + assert self.locale._format_timeframe("month", 1) == "ເດືອນ" + assert self.locale._format_timeframe("month", -1) == "ເດືອນ" + assert self.locale._format_timeframe("months", 7) == "7 ເດືອນ" + assert self.locale._format_timeframe("months", -20) == "20 ເດືອນ" + # year(s) + assert self.locale._format_timeframe("year", 1) == "ປີ" + assert self.locale._format_timeframe("year", -1) == "ປີ" + assert self.locale._format_timeframe("years", 7) == "7 ປີ" + assert self.locale._format_timeframe("years", -20) == "20 ປີ" + + def test_weekday(self): + dt = arrow.Arrow(2015, 4, 11, 17, 30, 00) + assert self.locale.day_name(dt.isoweekday()) == "ວັນເສົາ" + assert self.locale.day_abbreviation(dt.isoweekday()) == "ວັນເສົາ" + + @pytest.mark.usefixtures("lang_locale") class TestThaiLocale: def test_year_full(self): From 4357f8c8dbdf8cf8c01a3f230873e19bd8a1a4d0 Mon Sep 17 00:00:00 2001 From: Soo Hur Date: Mon, 2 May 2022 12:14:41 -0700 Subject: [PATCH 07/80] Add Uzbek (#1098) --- arrow/constants.py | 2 + arrow/locales.py | 127 ++++++++++++++++++++++-------------------- tests/test_arrow.py | 4 ++ tests/test_locales.py | 79 ++++++++++++++++++++++++++ 4 files changed, 152 insertions(+), 60 deletions(-) diff --git a/arrow/constants.py b/arrow/constants.py index b06f7b13..c0fef8cd 100644 --- a/arrow/constants.py +++ b/arrow/constants.py @@ -170,4 +170,6 @@ # "lo-la", "am", "am-et", + "uz", + "uz-uz", } diff --git a/arrow/locales.py b/arrow/locales.py index f6723741..282ed0dc 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -45,7 +45,6 @@ str, Sequence[str], Mapping[str, str], Mapping[str, Sequence[str]] ] - _locale_map: Dict[str, Type["Locale"]] = {} @@ -296,7 +295,6 @@ def _format_relative( class EnglishLocale(Locale): - names = [ "en", "en-us", @@ -560,7 +558,6 @@ def _ordinal_number(self, n: int) -> str: class FrenchBaseLocale(Locale): - past = "il y a {0}" future = "dans {0}" and_word = "et" @@ -622,7 +619,6 @@ def _ordinal_number(self, n: int) -> str: class FrenchLocale(FrenchBaseLocale, Locale): - names = ["fr", "fr-fr"] month_abbreviations = [ @@ -643,7 +639,6 @@ class FrenchLocale(FrenchBaseLocale, Locale): class FrenchCanadianLocale(FrenchBaseLocale, Locale): - names = ["fr-ca"] month_abbreviations = [ @@ -664,7 +659,6 @@ class FrenchCanadianLocale(FrenchBaseLocale, Locale): class GreekLocale(Locale): - names = ["el", "el-gr"] past = "{0} πριν" @@ -734,7 +728,6 @@ class GreekLocale(Locale): class JapaneseLocale(Locale): - names = ["ja", "ja-jp"] past = "{0}前" @@ -795,7 +788,6 @@ class JapaneseLocale(Locale): class SwedishLocale(Locale): - names = ["sv", "sv-se"] past = "för {0} sen" @@ -865,7 +857,6 @@ class SwedishLocale(Locale): class FinnishLocale(Locale): - names = ["fi", "fi-fi"] # The finnish grammar is very complex, and its hard to convert @@ -952,7 +943,6 @@ def _ordinal_number(self, n: int) -> str: class ChineseCNLocale(Locale): - names = ["zh", "zh-cn"] past = "{0}前" @@ -1012,7 +1002,6 @@ class ChineseCNLocale(Locale): class ChineseTWLocale(Locale): - names = ["zh-tw"] past = "{0}前" @@ -1073,7 +1062,6 @@ class ChineseTWLocale(Locale): class HongKongLocale(Locale): - names = ["zh-hk"] past = "{0}前" @@ -1133,7 +1121,6 @@ class HongKongLocale(Locale): class KoreanLocale(Locale): - names = ["ko", "ko-kr"] past = "{0} 전" @@ -1229,7 +1216,6 @@ def _format_relative( # derived locale types & implementations. class DutchLocale(Locale): - names = ["nl", "nl-nl"] past = "{0} geleden" @@ -1318,7 +1304,6 @@ def _format_timeframe(self, timeframe: TimeFrameLiteral, delta: int) -> str: class BelarusianLocale(SlavicBaseLocale): - names = ["be", "be-by"] past = "{0} таму" @@ -1397,7 +1382,6 @@ class BelarusianLocale(SlavicBaseLocale): class PolishLocale(SlavicBaseLocale): - names = ["pl", "pl-pl"] past = "{0} temu" @@ -1488,7 +1472,6 @@ class PolishLocale(SlavicBaseLocale): class RussianLocale(SlavicBaseLocale): - names = ["ru", "ru-ru"] past = "{0} назад" @@ -1579,7 +1562,6 @@ class RussianLocale(SlavicBaseLocale): class AfrikaansLocale(Locale): - names = ["af", "af-nl"] past = "{0} gelede" @@ -1646,7 +1628,6 @@ class AfrikaansLocale(Locale): class BulgarianLocale(SlavicBaseLocale): - names = ["bg", "bg-bg"] past = "{0} назад" @@ -1725,7 +1706,6 @@ class BulgarianLocale(SlavicBaseLocale): class UkrainianLocale(SlavicBaseLocale): - names = ["ua", "uk", "uk-ua"] past = "{0} тому" @@ -1903,7 +1883,6 @@ class MacedonianLocale(SlavicBaseLocale): class GermanBaseLocale(Locale): - past = "vor {0}" future = "in {0}" and_word = "und" @@ -2009,17 +1988,14 @@ def describe( class GermanLocale(GermanBaseLocale, Locale): - names = ["de", "de-de"] class SwissLocale(GermanBaseLocale, Locale): - names = ["de-ch"] class AustrianLocale(GermanBaseLocale, Locale): - names = ["de-at"] month_names = [ @@ -2040,7 +2016,6 @@ class AustrianLocale(GermanBaseLocale, Locale): class NorwegianLocale(Locale): - names = ["nb", "nb-no"] past = "for {0} siden" @@ -2112,7 +2087,6 @@ def _ordinal_number(self, n: int) -> str: class NewNorwegianLocale(Locale): - names = ["nn", "nn-no"] past = "for {0} sidan" @@ -2259,7 +2233,6 @@ class BrazilianPortugueseLocale(PortugueseLocale): class TagalogLocale(Locale): - names = ["tl", "tl-ph"] past = "nakaraang {0}" @@ -2333,7 +2306,6 @@ def _ordinal_number(self, n: int) -> str: class VietnameseLocale(Locale): - names = ["vi", "vi-vn"] past = "{0} trước" @@ -2402,7 +2374,6 @@ class VietnameseLocale(Locale): class TurkishLocale(Locale): - names = ["tr", "tr-tr"] past = "{0} önce" @@ -2474,7 +2445,6 @@ class TurkishLocale(Locale): class AzerbaijaniLocale(Locale): - names = ["az", "az-az"] past = "{0} əvvəl" @@ -2862,7 +2832,6 @@ def _format_timeframe(self, timeframe: TimeFrameLiteral, delta: int) -> str: class DanishLocale(Locale): - names = ["da", "da-dk"] past = "for {0} siden" @@ -2935,7 +2904,6 @@ def _ordinal_number(self, n: int) -> str: class MalayalamLocale(Locale): - names = ["ml"] past = "{0} മുമ്പ്" @@ -3009,7 +2977,6 @@ class MalayalamLocale(Locale): class HindiLocale(Locale): - names = ["hi", "hi-in"] past = "{0} पहले" @@ -3338,7 +3305,6 @@ def _format_timeframe(self, timeframe: TimeFrameLiteral, delta: int) -> str: class FarsiLocale(Locale): - names = ["fa", "fa-ir"] past = "{0} قبل" @@ -3412,7 +3378,6 @@ class FarsiLocale(Locale): class HebrewLocale(Locale): - names = ["he", "he-il"] past = "לפני {0}" @@ -3523,7 +3488,6 @@ def describe_multi( class MarathiLocale(Locale): - names = ["mr"] past = "{0} आधी" @@ -3730,7 +3694,6 @@ class BasqueLocale(Locale): class HungarianLocale(Locale): - names = ["hu", "hu-hu"] past = "{0} ezelőtt" @@ -3882,7 +3845,6 @@ def _ordinal_number(self, n: int) -> str: class ThaiLocale(Locale): - names = ["th", "th-th"] past = "{0} ที่ผ่านมา" @@ -4080,7 +4042,6 @@ def _format_relative( class BengaliLocale(Locale): - names = ["bn", "bn-bd", "bn-in"] past = "{0} আগে" @@ -4161,7 +4122,6 @@ def _ordinal_number(self, n: int) -> str: class RomanshLocale(Locale): - names = ["rm", "rm-ch"] past = "avant {0}" @@ -4368,7 +4328,6 @@ class SlovenianLocale(Locale): class IndonesianLocale(Locale): - names = ["id", "id-id"] past = "{0} yang lalu" @@ -4588,7 +4547,6 @@ def _format_timeframe(self, timeframe: TimeFrameLiteral, delta: int) -> str: class LatvianLocale(Locale): - names = ["lv", "lv-lv"] past = "pirms {0}" @@ -4669,7 +4627,6 @@ class LatvianLocale(Locale): class SwahiliLocale(Locale): - names = [ "sw", "sw-ke", @@ -4754,7 +4711,6 @@ class SwahiliLocale(Locale): class CroatianLocale(Locale): - names = ["hr", "hr-hr"] past = "prije {0}" @@ -4846,7 +4802,6 @@ def _format_timeframe(self, timeframe: TimeFrameLiteral, delta: int) -> str: class LatinLocale(Locale): - names = ["la", "la-va"] past = "ante {0}" @@ -4927,7 +4882,6 @@ class LatinLocale(Locale): class LithuanianLocale(Locale): - names = ["lt", "lt-lt"] past = "prieš {0}" @@ -5008,7 +4962,6 @@ class LithuanianLocale(Locale): class MalayLocale(Locale): - names = ["ms", "ms-my", "ms-bn"] past = "{0} yang lalu" @@ -5089,7 +5042,6 @@ class MalayLocale(Locale): class MalteseLocale(Locale): - names = ["mt", "mt-mt"] past = "{0} ilu" @@ -5181,7 +5133,6 @@ def _format_timeframe(self, timeframe: TimeFrameLiteral, delta: int) -> str: class SamiLocale(Locale): - names = ["se", "se-fi", "se-no", "se-se"] past = "{0} dassái" @@ -5261,7 +5212,6 @@ class SamiLocale(Locale): class OdiaLocale(Locale): - names = ["or", "or-in"] past = "{0} ପୂର୍ବେ" @@ -5352,7 +5302,6 @@ def _ordinal_number(self, n: int) -> str: class SerbianLocale(Locale): - names = ["sr", "sr-rs", "sr-sp"] past = "pre {0}" @@ -5444,7 +5393,6 @@ def _format_timeframe(self, timeframe: TimeFrameLiteral, delta: int) -> str: class LuxembourgishLocale(Locale): - names = ["lb", "lb-lu"] past = "virun {0}" @@ -5535,7 +5483,6 @@ def describe( delta: Union[int, float] = 0, only_distance: bool = False, ) -> str: - if not only_distance: return super().describe(timeframe, delta, only_distance) @@ -5546,7 +5493,6 @@ def describe( class ZuluLocale(Locale): - names = ["zu", "zu-za"] past = "{0} edlule" @@ -5644,7 +5590,6 @@ def _format_timeframe(self, timeframe: TimeFrameLiteral, delta: int) -> str: class TamilLocale(Locale): - names = ["ta", "ta-in", "ta-lk"] past = "{0} நேரத்திற்கு முன்பு" @@ -5732,7 +5677,6 @@ def _ordinal_number(self, n: int) -> str: class AlbanianLocale(Locale): - names = ["sq", "sq-al"] past = "{0} më parë" @@ -5813,7 +5757,6 @@ class AlbanianLocale(Locale): class GeorgianLocale(Locale): - names = ["ka", "ka-ge"] past = "{0} წინ" # ts’in @@ -5898,7 +5841,6 @@ class GeorgianLocale(Locale): class SinhalaLocale(Locale): - names = ["si", "si-lk"] past = "{0}ට පෙර" @@ -6062,7 +6004,6 @@ def describe( class UrduLocale(Locale): - names = ["ur", "ur-pk"] past = "پہلے {0}" @@ -6143,7 +6084,6 @@ class UrduLocale(Locale): class KazakhLocale(Locale): - names = ["kk", "kk-kz"] past = "{0} бұрын" @@ -6379,3 +6319,70 @@ def describe( humanized = self.timeframes_only_distance[timeframe].format(trunc(abs(delta))) return humanized + + +class UzbekLocale(Locale): + names = ["uz", "uz-uz"] + past = "{0}dan avval" + future = "{0}dan keyin" + timeframes = { + "now": "hozir", + "second": "bir soniya", + "seconds": "{0} soniya", + "minute": "bir daqiqa", + "minutes": "{0} daqiqa", + "hour": "bir soat", + "hours": "{0} soat", + "day": "bir kun", + "days": "{0} kun", + "week": "bir hafta", + "weeks": "{0} hafta", + "month": "bir oy", + "months": "{0} oy", + "year": "bir yil", + "years": "{0} yil", + } + + month_names = [ + "", + "Yanvar", + "Fevral", + "Mart", + "Aprel", + "May", + "Iyun", + "Iyul", + "Avgust", + "Sentyabr", + "Oktyabr", + "Noyabr", + "Dekabr", + ] + + month_abbreviations = [ + "", + "Yan", + "Fev", + "Mar", + "Apr", + "May", + "Iyn", + "Iyl", + "Avg", + "Sen", + "Okt", + "Noy", + "Dek", + ] + + day_names = [ + "", + "Dushanba", + "Seshanba", + "Chorshanba", + "Payshanba", + "Juma", + "Shanba", + "Yakshanba", + ] + day_abbreviations = ["", "Dush", "Sesh", "Chor", "Pay", "Jum", "Shan", "Yak"] diff --git a/tests/test_arrow.py b/tests/test_arrow.py index 3809f128..2289c583 100644 --- a/tests/test_arrow.py +++ b/tests/test_arrow.py @@ -2470,6 +2470,8 @@ def locale_list_no_weeks() -> List[str]: "ka-ge", "kk", "kk-kz", + "uz", + "uz-uz", # "lo", # "lo-la", ] @@ -2546,6 +2548,8 @@ def locale_list_with_weeks() -> List[str]: "ta-lk", "kk", "kk-kz", + "uz", + "uz-uz", ] return tested_langs diff --git a/tests/test_locales.py b/tests/test_locales.py index ed62dc80..2927a492 100644 --- a/tests/test_locales.py +++ b/tests/test_locales.py @@ -3052,3 +3052,82 @@ def test_weekday(self): dt = arrow.Arrow(2015, 4, 11, 17, 30, 00) assert self.locale.day_name(dt.isoweekday()) == "ቅዳሜ" assert self.locale.day_abbreviation(dt.isoweekday()) == "ዓ" + + +@pytest.mark.usefixtures("lang_locale") +class TestUzbekLocale: + def test_singles_mk(self): + assert self.locale._format_timeframe("second", 1) == "bir soniya" + assert self.locale._format_timeframe("minute", 1) == "bir daqiqa" + assert self.locale._format_timeframe("hour", 1) == "bir soat" + assert self.locale._format_timeframe("day", 1) == "bir kun" + assert self.locale._format_timeframe("week", 1) == "bir hafta" + assert self.locale._format_timeframe("month", 1) == "bir oy" + assert self.locale._format_timeframe("year", 1) == "bir yil" + + def test_describe_mk(self): + assert self.locale.describe("second", only_distance=True) == "bir soniya" + assert ( + self.locale.describe("second", only_distance=False) == "bir soniyadan keyin" + ) + assert self.locale.describe("minute", only_distance=True) == "bir daqiqa" + assert ( + self.locale.describe("minute", only_distance=False) == "bir daqiqadan keyin" + ) + assert self.locale.describe("hour", only_distance=True) == "bir soat" + assert self.locale.describe("hour", only_distance=False) == "bir soatdan keyin" + assert self.locale.describe("day", only_distance=True) == "bir kun" + assert self.locale.describe("day", only_distance=False) == "bir kundan keyin" + assert self.locale.describe("week", only_distance=True) == "bir hafta" + assert self.locale.describe("week", only_distance=False) == "bir haftadan keyin" + assert self.locale.describe("month", only_distance=True) == "bir oy" + assert self.locale.describe("month", only_distance=False) == "bir oydan keyin" + assert self.locale.describe("year", only_distance=True) == "bir yil" + assert self.locale.describe("year", only_distance=False) == "bir yildan keyin" + + def test_relative_mk(self): + assert self.locale._format_relative("hozir", "now", 0) == "hozir" + assert ( + self.locale._format_relative("1 soniya", "seconds", 1) + == "1 soniyadan keyin" + ) + assert ( + self.locale._format_relative("1 soniya", "seconds", -1) + == "1 soniyadan avval" + ) + assert ( + self.locale._format_relative("1 daqiqa", "minutes", 1) + == "1 daqiqadan keyin" + ) + assert ( + self.locale._format_relative("1 daqiqa", "minutes", -1) + == "1 daqiqadan avval" + ) + assert self.locale._format_relative("1 soat", "hours", 1) == "1 soatdan keyin" + assert self.locale._format_relative("1 soat", "hours", -1) == "1 soatdan avval" + assert self.locale._format_relative("1 kun", "days", 1) == "1 kundan keyin" + assert self.locale._format_relative("1 kun", "days", -1) == "1 kundan avval" + assert self.locale._format_relative("1 hafta", "weeks", 1) == "1 haftadan keyin" + assert ( + self.locale._format_relative("1 hafta", "weeks", -1) == "1 haftadan avval" + ) + assert self.locale._format_relative("1 oy", "months", 1) == "1 oydan keyin" + assert self.locale._format_relative("1 oy", "months", -1) == "1 oydan avval" + assert self.locale._format_relative("1 yil", "years", 1) == "1 yildan keyin" + assert self.locale._format_relative("1 yil", "years", -1) == "1 yildan avval" + + def test_plurals_mk(self): + assert self.locale._format_timeframe("now", 0) == "hozir" + assert self.locale._format_timeframe("second", 1) == "bir soniya" + assert self.locale._format_timeframe("seconds", 30) == "30 soniya" + assert self.locale._format_timeframe("minute", 1) == "bir daqiqa" + assert self.locale._format_timeframe("minutes", 40) == "40 daqiqa" + assert self.locale._format_timeframe("hour", 1) == "bir soat" + assert self.locale._format_timeframe("hours", 23) == "23 soat" + assert self.locale._format_timeframe("days", 12) == "12 kun" + assert self.locale._format_timeframe("week", 1) == "bir hafta" + assert self.locale._format_timeframe("weeks", 38) == "38 hafta" + assert self.locale._format_timeframe("month", 1) == "bir oy" + assert self.locale._format_timeframe("months", 11) == "11 oy" + assert self.locale._format_timeframe("year", 1) == "bir yil" + assert self.locale._format_timeframe("years", 12) == "12 yil" From fbe19bd177e17bb58cc23e40fea6f0d695ee436c Mon Sep 17 00:00:00 2001 From: Chris <30196510+systemcatch@users.noreply.github.com> Date: Wed, 18 May 2022 19:47:48 +0100 Subject: [PATCH 08/80] Fix syntax in locales --- arrow/locales.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arrow/locales.py b/arrow/locales.py index a94ddea7..c25b98a2 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -6366,7 +6366,7 @@ class ArmenianLocale(Locale): "հոկտեմբեր", "նոյեմբեր", "դեկտեմբեր", - } + ] month_abbreviations = [ "", From de1c5bdca7279311fd2a9d57c38b5f7ec450ddce Mon Sep 17 00:00:00 2001 From: Chris <30196510+systemcatch@users.noreply.github.com> Date: Wed, 18 May 2022 19:47:58 +0100 Subject: [PATCH 09/80] Fix syntax in locales --- arrow/locales.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arrow/locales.py b/arrow/locales.py index c25b98a2..d0f31448 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -6382,7 +6382,7 @@ class ArmenianLocale(Locale): "հոկտեմբեր", "նոյեմբեր", "դեկտեմբեր", - } + ] day_names = [ "", From 3fa1b92d3168ac734f1ab5e47a98d711d3bf566a Mon Sep 17 00:00:00 2001 From: ElahehAx Date: Thu, 2 Jun 2022 14:58:07 +0200 Subject: [PATCH 10/80] Add Armenian locale --- arrow/locales.py | 13 ++++++------- tests/test_locales.py | 4 ++-- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/arrow/locales.py b/arrow/locales.py index d0f31448..3627497f 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -6325,7 +6325,7 @@ class ArmenianLocale(Locale): names = ["hy", "hy-am"] past = "{0} առաջ" future = "{0}ից" - and_word = "Եվ" + and_word = "Եվ" # Yev timeframes = { "now": "հիմա", @@ -6351,7 +6351,7 @@ class ArmenianLocale(Locale): "AM": "Ամ", "PM": "պ.մ.", } - + month_names = [ "", "հունվար", @@ -6383,7 +6383,7 @@ class ArmenianLocale(Locale): "նոյեմբեր", "դեկտեմբեր", ] - + day_names = [ "", "երկուշաբթի", @@ -6393,7 +6393,7 @@ class ArmenianLocale(Locale): "ուրբաթ", "շաբաթ", "կիրակի", - ] + ] day_abbreviations = [ "", @@ -6406,7 +6406,7 @@ class ArmenianLocale(Locale): "կիր.", ] - + class UzbekLocale(Locale): names = ["uz", "uz-uz"] past = "{0}dan avval" @@ -6471,6 +6471,5 @@ class UzbekLocale(Locale): "Shanba", "Yakshanba", ] - - day_abbreviations = ["", "Dush", "Sesh", "Chor", "Pay", "Jum", "Shan", "Yak"] + day_abbreviations = ["", "Dush", "Sesh", "Chor", "Pay", "Jum", "Shan", "Yak"] diff --git a/tests/test_locales.py b/tests/test_locales.py index 4df9667c..5ff00c1b 100644 --- a/tests/test_locales.py +++ b/tests/test_locales.py @@ -3111,8 +3111,8 @@ def test_weekday(self): dt = arrow.Arrow(2015, 4, 11, 17, 30, 00) assert self.locale.day_name(dt.isoweekday()) == "շաբաթ" assert self.locale.day_abbreviation(dt.isoweekday()) == "շաբ." - - + + @pytest.mark.usefixtures("lang_locale") class TestUzbekLocale: def test_singles_mk(self): From 7b5c1aa73e97c98ea7a10ba5a4743fc9d3e41a2e Mon Sep 17 00:00:00 2001 From: "Kristijan \"Fremen\" Velkovski" Date: Wed, 22 Jun 2022 19:55:44 -0500 Subject: [PATCH 11/80] Sphinx language set to "en" as they no longer support None as of 5.x releases. (#1114) --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index f106cb7f..68800bca 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -30,7 +30,7 @@ source_suffix = ".rst" pygments_style = "sphinx" -language = None +language = "en" # -- Options for HTML output ------------------------------------------------- From 8842f8c3263d1f1219c189a0500aa67abdd0a214 Mon Sep 17 00:00:00 2001 From: Chris <30196510+systemcatch@users.noreply.github.com> Date: Sun, 26 Jun 2022 00:25:28 +0100 Subject: [PATCH 12/80] Bump version to 1.2.3 and update CHANGELOG (#1116) --- CHANGELOG.rst | 7 +++++++ arrow/_version.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 12bc86a6..3bf23e02 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,13 @@ Changelog ========= +1.2.3 (2022-06-25) +------------------ + +- [NEW] Added Amharic, Armenian, Georgian, Laotian and Uzbek locales. +- [FIX] Updated Danish locale and associated tests. +- [INTERNAl] Small fixes to CI. + 1.2.2 (2022-01-19) ------------------ diff --git a/arrow/_version.py b/arrow/_version.py index bc86c944..10aa336c 100644 --- a/arrow/_version.py +++ b/arrow/_version.py @@ -1 +1 @@ -__version__ = "1.2.2" +__version__ = "1.2.3" From cb03dd32e6625c6d7dc44e267ea7f0e2c1582337 Mon Sep 17 00:00:00 2001 From: "Kristijan \"Fremen\" Velkovski" Date: Tue, 19 Jul 2022 17:32:19 -0500 Subject: [PATCH 13/80] Update Github CI Actions. --- .github/workflows/continuous_integration.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/continuous_integration.yml b/.github/workflows/continuous_integration.yml index d60e4bd3..1ab9b5d1 100644 --- a/.github/workflows/continuous_integration.yml +++ b/.github/workflows/continuous_integration.yml @@ -29,15 +29,15 @@ jobs: - os: windows-latest path: ~\AppData\Local\pip\Cache steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Cache pip - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: ${{ matrix.path }} key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements*.txt') }} restore-keys: ${{ runner.os }}-pip- - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v3 with: python-version: ${{ matrix.python-version }} - name: Install dependencies @@ -47,26 +47,26 @@ jobs: - name: Test with tox run: tox - name: Upload coverage to Codecov - uses: codecov/codecov-action@v2 + uses: codecov/codecov-action@v3 with: file: coverage.xml lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python 3.10 - uses: actions/setup-python@v2 + uses: actions/setup-python@v3 with: python-version: "3.10" - name: Cache pip - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: ~/.cache/pip key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements*.txt') }} restore-keys: ${{ runner.os }}-pip- - name: Cache pre-commit - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: ~/.cache/pre-commit key: ${{ runner.os }}-pre-commit-${{ hashFiles('**/.pre-commit-config.yaml') }} From aa862642ab7bf496a503351e6903401a6954a939 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henri=20Niemel=C3=A4inen?= Date: Fri, 22 Jul 2022 05:04:11 +0300 Subject: [PATCH 14/80] Update Finnish locale Use the correct declension for singular second and day, and remove the "muutama" from the plural seconds (seems to be old mishap with localization). --- arrow/locales.py | 6 +++--- tests/test_locales.py | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/arrow/locales.py b/arrow/locales.py index 3627497f..ef7a8edd 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -867,13 +867,13 @@ class FinnishLocale(Locale): timeframes: ClassVar[Mapping[TimeFrameLiteral, Union[str, Mapping[str, str]]]] = { "now": "juuri nyt", - "second": "sekunti", - "seconds": {"past": "{0} muutama sekunti", "future": "{0} muutaman sekunnin"}, + "second": {"past": "sekunti", "future": "sekunnin"}, + "seconds": {"past": "{0} sekuntia", "future": "{0} sekunnin"}, "minute": {"past": "minuutti", "future": "minuutin"}, "minutes": {"past": "{0} minuuttia", "future": "{0} minuutin"}, "hour": {"past": "tunti", "future": "tunnin"}, "hours": {"past": "{0} tuntia", "future": "{0} tunnin"}, - "day": "päivä", + "day": {"past": "päivä", "future": "päivän"}, "days": {"past": "{0} päivää", "future": "{0} päivän"}, "month": {"past": "kuukausi", "future": "kuukauden"}, "months": {"past": "{0} kuukautta", "future": "{0} kuukauden"}, diff --git a/tests/test_locales.py b/tests/test_locales.py index 5ff00c1b..099f6f67 100644 --- a/tests/test_locales.py +++ b/tests/test_locales.py @@ -1111,9 +1111,9 @@ def test_format_timeframe(self): # Second(s) assert self.locale._format_timeframe("second", -1) == "sekunti" - assert self.locale._format_timeframe("second", 1) == "sekunti" - assert self.locale._format_timeframe("seconds", -2) == "2 muutama sekunti" - assert self.locale._format_timeframe("seconds", 2) == "2 muutaman sekunnin" + assert self.locale._format_timeframe("second", 1) == "sekunnin" + assert self.locale._format_timeframe("seconds", -2) == "2 sekuntia" + assert self.locale._format_timeframe("seconds", 2) == "2 sekunnin" # Minute(s) assert self.locale._format_timeframe("minute", -1) == "minuutti" @@ -1129,7 +1129,7 @@ def test_format_timeframe(self): # Day(s) assert self.locale._format_timeframe("day", -1) == "päivä" - assert self.locale._format_timeframe("day", 1) == "päivä" + assert self.locale._format_timeframe("day", 1) == "päivän" assert self.locale._format_timeframe("days", -2) == "2 päivää" assert self.locale._format_timeframe("days", 2) == "2 päivän" From 4eb070fd858dc4d167748e498d3be7b0ea01f1cc Mon Sep 17 00:00:00 2001 From: karsazoltan <61280910+karsazoltan@users.noreply.github.com> Date: Mon, 29 Aug 2022 22:28:47 +0200 Subject: [PATCH 15/80] Hungarian Locale Update (#1123) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Karsa Zoltán --- arrow/locales.py | 2 ++ tests/test_locales.py | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/arrow/locales.py b/arrow/locales.py index ef7a8edd..f0d4bc19 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -3709,6 +3709,8 @@ class HungarianLocale(Locale): "hours": {"past": "{0} órával", "future": "{0} óra"}, "day": {"past": "egy nappal", "future": "egy nap"}, "days": {"past": "{0} nappal", "future": "{0} nap"}, + "week": {"past": "egy héttel", "future": "egy hét"}, + "weeks": {"past": "{0} héttel", "future": "{0} hét"}, "month": {"past": "egy hónappal", "future": "egy hónap"}, "months": {"past": "{0} hónappal", "future": "{0} hónap"}, "year": {"past": "egy évvel", "future": "egy év"}, diff --git a/tests/test_locales.py b/tests/test_locales.py index 099f6f67..bef91d74 100644 --- a/tests/test_locales.py +++ b/tests/test_locales.py @@ -1269,6 +1269,12 @@ def test_format_timeframe(self): assert self.locale._format_timeframe("days", -2) == "2 nappal" assert self.locale._format_timeframe("days", 2) == "2 nap" + # Week(s) + assert self.locale._format_timeframe("week", -1) == "egy héttel" + assert self.locale._format_timeframe("week", 1) == "egy hét" + assert self.locale._format_timeframe("weeks", -2) == "2 héttel" + assert self.locale._format_timeframe("weeks", 2) == "2 hét" + # Month(s) assert self.locale._format_timeframe("month", -1) == "egy hónappal" assert self.locale._format_timeframe("month", 1) == "egy hónap" From 5f9dfbef9e5db15266205f0799d17ac8d0a79004 Mon Sep 17 00:00:00 2001 From: Konrad Weihmann <46938494+priv-kweihmann@users.noreply.github.com> Date: Tue, 30 Aug 2022 04:46:29 +0200 Subject: [PATCH 16/80] Parser: Allow UTC prefix in TzInfoParser (#1099) --- arrow/parser.py | 2 +- tests/test_parser.py | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/arrow/parser.py b/arrow/parser.py index e95d78b0..6bf2fba2 100644 --- a/arrow/parser.py +++ b/arrow/parser.py @@ -740,7 +740,7 @@ def _generate_choice_re( class TzinfoParser: _TZINFO_RE: ClassVar[Pattern[str]] = re.compile( - r"^([\+\-])?(\d{2})(?:\:?(\d{2}))?$" + r"^(?:\(UTC)*([\+\-])?(\d{2})(?:\:?(\d{2}))?" ) @classmethod diff --git a/tests/test_parser.py b/tests/test_parser.py index bb4ab148..e92d30c1 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -1367,6 +1367,14 @@ def test_parse_utc(self): assert self.parser.parse("utc") == tz.tzutc() assert self.parser.parse("UTC") == tz.tzutc() + def test_parse_utc_withoffset(self): + assert self.parser.parse("(UTC+01:00") == tz.tzoffset(None, 3600) + assert self.parser.parse("(UTC-01:00") == tz.tzoffset(None, -3600) + assert self.parser.parse("(UTC+01:00") == tz.tzoffset(None, 3600) + assert self.parser.parse( + "(UTC+01:00) Amsterdam, Berlin, Bern, Rom, Stockholm, Wien" + ) == tz.tzoffset(None, 3600) + def test_parse_iso(self): assert self.parser.parse("01:00") == tz.tzoffset(None, 3600) From f8f306848f42742cf771bba2cac5735238e6dcae Mon Sep 17 00:00:00 2001 From: Marc Sommerhalder Date: Wed, 31 Aug 2022 20:27:31 +0200 Subject: [PATCH 17/80] Typo Fix in Italian locale, update Romansh locale (#1121) --- arrow/locales.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/arrow/locales.py b/arrow/locales.py index f0d4bc19..ec6af726 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -425,7 +425,7 @@ class ItalianLocale(Locale): "hours": "{0} ore", "day": "un giorno", "days": "{0} giorni", - "week": "una settimana,", + "week": "una settimana", "weeks": "{0} settimane", "month": "un mese", "months": "{0} mesi", @@ -4139,6 +4139,8 @@ class RomanshLocale(Locale): "hours": "{0} ura", "day": "in di", "days": "{0} dis", + "week": "in'emna", + "weeks": "{0} emnas", "month": "in mais", "months": "{0} mais", "year": "in onn", From 11712752c0829b2d8d27c40009923de8245f1c54 Mon Sep 17 00:00:00 2001 From: gruebel Date: Sat, 1 Oct 2022 19:11:13 +0200 Subject: [PATCH 18/80] add support for Python 3.11 --- .github/workflows/continuous_integration.yml | 2 +- setup.py | 1 + tox.ini | 3 ++- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/continuous_integration.yml b/.github/workflows/continuous_integration.yml index 1ab9b5d1..1f9d5547 100644 --- a/.github/workflows/continuous_integration.yml +++ b/.github/workflows/continuous_integration.yml @@ -15,7 +15,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["pypy-3.7", "3.6", "3.7", "3.8", "3.9", "3.10"] + python-version: ["pypy-3.7", "3.6", "3.7", "3.8", "3.9", "3.10", "3.11-dev"] os: [ubuntu-latest, macos-latest, windows-latest] exclude: # pypy3 randomly fails on Windows builds diff --git a/setup.py b/setup.py index 350a5a0f..52563cf9 100644 --- a/setup.py +++ b/setup.py @@ -38,6 +38,7 @@ "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", ], keywords="arrow date time datetime timestamp timezone humanize", project_urls={ diff --git a/tox.ini b/tox.ini index c51432a2..fcd2d9c1 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] minversion = 3.18.0 -envlist = py{py3,36,37,38,39,310} +envlist = py{py3,36,37,38,39,310,311} skip_missing_interpreters = true [gh-actions] @@ -11,6 +11,7 @@ python = 3.8: py38 3.9: py39 3.10: py310 + 3.11-dev: py311 [testenv] deps = -r requirements/requirements-tests.txt From 48520e7352b4ae290cc0ed64ada8025e1bac1f0b Mon Sep 17 00:00:00 2001 From: gruebel Date: Sun, 2 Oct 2022 10:51:12 +0200 Subject: [PATCH 19/80] adjust python version name in tox.ini --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index fcd2d9c1..11d70cb2 100644 --- a/tox.ini +++ b/tox.ini @@ -11,7 +11,7 @@ python = 3.8: py38 3.9: py39 3.10: py310 - 3.11-dev: py311 + 3.11: py311 [testenv] deps = -r requirements/requirements-tests.txt From 9fcdd6f3437d97c5b3194c9a3cef342a775b541d Mon Sep 17 00:00:00 2001 From: gruebel Date: Sat, 8 Oct 2022 21:42:02 +0200 Subject: [PATCH 20/80] upgrade pre-commit hooks and GHA versions --- .github/workflows/continuous_integration.yml | 4 ++-- .pre-commit-config.yaml | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/continuous_integration.yml b/.github/workflows/continuous_integration.yml index 1f9d5547..21b5417c 100644 --- a/.github/workflows/continuous_integration.yml +++ b/.github/workflows/continuous_integration.yml @@ -37,7 +37,7 @@ jobs: key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements*.txt') }} restore-keys: ${{ runner.os }}-pip- - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v3 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Install dependencies @@ -56,7 +56,7 @@ jobs: steps: - uses: actions/checkout@v3 - name: Set up Python 3.10 - uses: actions/setup-python@v3 + uses: actions/setup-python@v4 with: python-version: "3.10" - name: Cache pip diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b352b70b..2847af8b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,7 +2,7 @@ default_language_version: python: python3 repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.2.0 + rev: v4.3.0 hooks: - id: check-ast - id: check-yaml @@ -22,7 +22,7 @@ repos: hooks: - id: isort - repo: https://github.com/asottile/pyupgrade - rev: v2.32.0 + rev: v3.0.0 hooks: - id: pyupgrade args: [--py36-plus] @@ -38,12 +38,12 @@ repos: - id: rst-inline-touching-normal - id: text-unicode-replacement-char - repo: https://github.com/psf/black - rev: 22.3.0 + rev: 22.8.0 hooks: - id: black args: [--safe, --quiet, --target-version=py36] - repo: https://github.com/pycqa/flake8 - rev: 4.0.1 + rev: 5.0.4 hooks: - id: flake8 additional_dependencies: [flake8-bugbear,flake8-annotations] From 054749e00a71720fc320538b7a8e333a1598001a Mon Sep 17 00:00:00 2001 From: gruebel Date: Fri, 7 Oct 2022 21:16:22 +0200 Subject: [PATCH 21/80] upgrade mypy version --- .pre-commit-config.yaml | 2 +- arrow/locales.py | 1 + tests/test_locales.py | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b352b70b..457e509e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -48,7 +48,7 @@ repos: - id: flake8 additional_dependencies: [flake8-bugbear,flake8-annotations] - repo: https://github.com/pre-commit/mirrors-mypy - rev: 'v0.950' + rev: v0.982 hooks: - id: mypy additional_dependencies: [types-python-dateutil] diff --git a/arrow/locales.py b/arrow/locales.py index ec6af726..ea4c84b2 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -4121,6 +4121,7 @@ def _ordinal_number(self, n: int) -> str: return f"{n}র্থ" if n == 6: return f"{n}ষ্ঠ" + return "" class RomanshLocale(Locale): diff --git a/tests/test_locales.py b/tests/test_locales.py index bef91d74..8312a939 100644 --- a/tests/test_locales.py +++ b/tests/test_locales.py @@ -1394,7 +1394,7 @@ def test_ordinal_number(self): assert self.locale._ordinal_number(10) == "10ম" assert self.locale._ordinal_number(11) == "11তম" assert self.locale._ordinal_number(42) == "42তম" - assert self.locale._ordinal_number(-1) is None + assert self.locale._ordinal_number(-1) == "" @pytest.mark.usefixtures("lang_locale") From d2ceb53279612c4696effe20e6a90f7389e90ed6 Mon Sep 17 00:00:00 2001 From: gruebel Date: Fri, 7 Oct 2022 21:17:46 +0200 Subject: [PATCH 22/80] add error codes to type ignore comments --- arrow/arrow.py | 6 +++--- arrow/parser.py | 6 +++--- setup.cfg | 3 +++ 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/arrow/arrow.py b/arrow/arrow.py index 1ede107f..2e1d977c 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -495,7 +495,7 @@ def range( yield current values = [getattr(current, f) for f in cls._ATTRS] - current = cls(*values, tzinfo=tzinfo).shift( # type: ignore + current = cls(*values, tzinfo=tzinfo).shift( # type: ignore[misc] **{frame_relative: relative_steps} ) @@ -578,7 +578,7 @@ def span( for _ in range(3 - len(values)): values.append(1) - floor = self.__class__(*values, tzinfo=self.tzinfo) # type: ignore + floor = self.__class__(*values, tzinfo=self.tzinfo) # type: ignore[misc] if frame_absolute == "week": # if week_start is greater than self.isoweekday() go back one week by setting delta = 7 @@ -1259,7 +1259,7 @@ def humanize( ) if trunc(abs(delta)) != 1: - granularity += "s" # type: ignore + granularity += "s" # type: ignore[assignment] return locale.describe(granularity, delta, only_distance=only_distance) else: diff --git a/arrow/parser.py b/arrow/parser.py index 6bf2fba2..ee31a5bc 100644 --- a/arrow/parser.py +++ b/arrow/parser.py @@ -187,7 +187,7 @@ def __init__(self, locale: str = DEFAULT_LOCALE, cache_size: int = 0) -> None: } ) if cache_size > 0: - self._generate_pattern_re = lru_cache(maxsize=cache_size)( # type: ignore + self._generate_pattern_re = lru_cache(maxsize=cache_size)( # type: ignore[assignment] self._generate_pattern_re ) @@ -341,7 +341,7 @@ def parse( f"Unable to find a match group for the specified token {token!r}." ) - self._parse_token(token, value, parts) # type: ignore + self._parse_token(token, value, parts) # type: ignore[arg-type] return self._build_datetime(parts) @@ -508,7 +508,7 @@ def _parse_token( elif token in ["MMMM", "MMM"]: # FIXME: month_number() is nullable - parts["month"] = self.locale.month_number(value.lower()) # type: ignore + parts["month"] = self.locale.month_number(value.lower()) # type: ignore[typeddict-item] elif token in ["MM", "M"]: parts["month"] = int(value) diff --git a/setup.cfg b/setup.cfg index 3add2419..6ffbd02b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,9 @@ [mypy] python_version = 3.6 +show_error_codes = True +pretty = True + allow_any_expr = True allow_any_decorated = True allow_any_explicit = True From 9c5f0ce28fff47468b7de982bc748ac23cd790c1 Mon Sep 17 00:00:00 2001 From: erwinmintiens Date: Thu, 13 Oct 2022 13:34:55 +0200 Subject: [PATCH 23/80] Added tests for DutchLocale --- tests/test_locales.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/test_locales.py b/tests/test_locales.py index bef91d74..0a7f9137 100644 --- a/tests/test_locales.py +++ b/tests/test_locales.py @@ -2439,6 +2439,26 @@ def test_ordinal_number(self): assert self.locale.ordinal_number(100) == "100번째" +@pytest.mark.usefixtures("lang_locale") +class TestDutchLocale: + def test_plurals(self): + assert self.locale._format_timeframe("now", 0) == "nu" + assert self.locale._format_timeframe("second", 1) == "een seconde" + assert self.locale._format_timeframe("seconds", 30) == "30 seconden" + assert self.locale._format_timeframe("minute", 1) == "een minuut" + assert self.locale._format_timeframe("minutes", 40) == "40 minuten" + assert self.locale._format_timeframe("hour", 1) == "een uur" + assert self.locale._format_timeframe("hours", 23) == "23 uur" + assert self.locale._format_timeframe("day", 1) == "een dag" + assert self.locale._format_timeframe("days", 12) == "12 dagen" + assert self.locale._format_timeframe("week", 1) == "een week" + assert self.locale._format_timeframe("weeks", 38) == "38 weken" + assert self.locale._format_timeframe("month", 1) == "een maand" + assert self.locale._format_timeframe("months", 11) == "11 maanden" + assert self.locale._format_timeframe("year", 1) == "een jaar" + assert self.locale._format_timeframe("years", 12) == "12 jaar" + + @pytest.mark.usefixtures("lang_locale") class TestJapaneseLocale: def test_format_timeframe(self): From 8cac5fabf588b19818f21c0f90c55b851b8fefe6 Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Tue, 18 Oct 2022 19:35:10 -0400 Subject: [PATCH 24/80] Spelling Fixes (#1131) --- CHANGELOG.rst | 4 ++-- arrow/arrow.py | 4 ++-- tests/test_arrow.py | 4 ++-- tests/test_parser.py | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 3bf23e02..5b079798 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -285,7 +285,7 @@ After 8 years we're pleased to announce Arrow v1.0. Thanks to the entire Python - [FIX] Consolidated and simplified German locales. - [INTERNAL] Moved testing suite from nosetest/Chai to pytest/pytest-mock. - [INTERNAL] Converted xunit-style setup and teardown functions in tests to pytest fixtures. -- [INTERNAL] Setup Github Actions for CI alongside Travis. +- [INTERNAL] Setup GitHub Actions for CI alongside Travis. - [INTERNAL] Help support Arrow's future development by donating to the project on `Open Collective `_. 0.15.5 (2020-01-03) @@ -659,7 +659,7 @@ The following will work in v0.15.0: - [NEW] Brazilian locale (Augusto2112) - [NEW] Dutch locale (OrangeTux) - [NEW] Italian locale (Pertux) -- [NEW] Austrain locale (LeChewbacca) +- [NEW] Austrian locale (LeChewbacca) - [NEW] Tagalog locale (Marksteve) - [FIX] Corrected spelling and day numbers in German locale (LeChewbacca) - [FIX] Factory ``get`` method should now handle unicode strings correctly (Bwells) diff --git a/arrow/arrow.py b/arrow/arrow.py index 1ede107f..9802a910 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -1314,7 +1314,7 @@ def gather_timeframes(_delta: float, _frame: TimeFrameLiteral) -> float: def dehumanize(self, input_string: str, locale: str = "en_us") -> "Arrow": """Returns a new :class:`Arrow ` object, that represents - the time difference relative to the attrbiutes of the + the time difference relative to the attributes of the :class:`Arrow ` object. :param timestring: a ``str`` representing a humanized relative time. @@ -1419,7 +1419,7 @@ def dehumanize(self, input_string: str, locale: str = "en_us") -> "Arrow": # Assert error if string does not modify any units if not any([True for k, v in unit_visited.items() if v]): raise ValueError( - "Input string not valid. Note: Some locales do not support the week granulairty in Arrow. " + "Input string not valid. Note: Some locales do not support the week granularity in Arrow. " "If you are attempting to use the week granularity on an unsupported locale, this could be the cause of this error." ) diff --git a/tests/test_arrow.py b/tests/test_arrow.py index 38d5000a..5cd12c82 100644 --- a/tests/test_arrow.py +++ b/tests/test_arrow.py @@ -2295,8 +2295,8 @@ def test_empty_granularity_list(self): arw.humanize(later, granularity=[]) # Bulgarian is an example of a language that overrides _format_timeframe - # Applicabale to all locales. Note: Contributors need to make sure - # that if they override describe or describe_mutli, that delta + # Applicable to all locales. Note: Contributors need to make sure + # that if they override describe or describe_multi, that delta # is truncated on call def test_no_floats(self): diff --git a/tests/test_parser.py b/tests/test_parser.py index e92d30c1..bdcc1026 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -917,9 +917,9 @@ def test_timestamp_milli(self): def test_time(self): time_re = parser.DateTimeParser._TIME_RE - time_seperators = [":", ""] + time_separators = [":", ""] - for sep in time_seperators: + for sep in time_separators: assert time_re.findall("12") == [("12", "", "", "", "")] assert time_re.findall(f"12{sep}35") == [("12", "35", "", "", "")] assert time_re.findall("12{sep}35{sep}46".format(sep=sep)) == [ From e7fa554dc9f80170990bd4621683802b57d73fc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20K=C3=A4ufl?= Date: Tue, 25 Oct 2022 14:37:13 +0200 Subject: [PATCH 25/80] Run tests against Python 3.11 stable --- .github/workflows/continuous_integration.yml | 2 +- Makefile | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/continuous_integration.yml b/.github/workflows/continuous_integration.yml index 21b5417c..34d9c4f2 100644 --- a/.github/workflows/continuous_integration.yml +++ b/.github/workflows/continuous_integration.yml @@ -15,7 +15,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["pypy-3.7", "3.6", "3.7", "3.8", "3.9", "3.10", "3.11-dev"] + python-version: ["pypy-3.7", "3.6", "3.7", "3.8", "3.9", "3.10", "3.11"] os: [ubuntu-latest, macos-latest, windows-latest] exclude: # pypy3 randomly fails on Windows builds diff --git a/Makefile b/Makefile index 5f885157..f55a3dce 100644 --- a/Makefile +++ b/Makefile @@ -7,6 +7,7 @@ build37: PYTHON_VER = python3.7 build38: PYTHON_VER = python3.8 build39: PYTHON_VER = python3.9 build310: PYTHON_VER = python3.10 +build311: PYTHON_VER = python3.11 build36 build37 build38 build39 build310: clean $(PYTHON_VER) -m venv venv From e916a2d55d77b51c1bd7983b1e2bc2c18b4d6b35 Mon Sep 17 00:00:00 2001 From: Haffi Mazhar <53666594+haffi96@users.noreply.github.com> Date: Tue, 1 Nov 2022 18:34:06 +0000 Subject: [PATCH 26/80] Changing Documentation Theme to Improve Readability (#1135) --- docs/api-guide.rst | 28 ++ docs/conf.py | 30 +- docs/getting-started.rst | 9 + docs/guide.rst | 555 +++++++++++++++++++++++++++ docs/index.rst | 586 +---------------------------- docs/releases.rst | 4 + requirements/requirements-docs.txt | 1 + 7 files changed, 626 insertions(+), 587 deletions(-) create mode 100644 docs/api-guide.rst create mode 100644 docs/getting-started.rst create mode 100644 docs/guide.rst diff --git a/docs/api-guide.rst b/docs/api-guide.rst new file mode 100644 index 00000000..3cf4d394 --- /dev/null +++ b/docs/api-guide.rst @@ -0,0 +1,28 @@ +*************************************** +API Guide +*************************************** + +:mod:`arrow.arrow` +===================== + +.. automodule:: arrow.arrow + :members: + +:mod:`arrow.factory` +===================== + +.. automodule:: arrow.factory + :members: + +:mod:`arrow.api` +===================== + +.. automodule:: arrow.api + :members: + +:mod:`arrow.locale` +===================== + +.. automodule:: arrow.locales + :members: + :undoc-members: diff --git a/docs/conf.py b/docs/conf.py index 68800bca..dee71470 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -20,7 +20,11 @@ # -- General configuration --------------------------------------------------- -extensions = ["sphinx.ext.autodoc", "sphinx_autodoc_typehints"] +extensions = [ + "sphinx.ext.autodoc", + "sphinx_autodoc_typehints", + "sphinx_rtd_theme", +] templates_path = [] @@ -34,7 +38,7 @@ # -- Options for HTML output ------------------------------------------------- -html_theme = "alabaster" +html_theme = "sphinx_rtd_theme" html_theme_path = [] html_static_path = [] @@ -42,21 +46,21 @@ html_show_sphinx = False html_show_copyright = True -# https://alabaster.readthedocs.io/en/latest/customization.html -html_theme_options = { - "description": "Arrow is a sensible and human-friendly approach to dates, times and timestamps.", +html_context = { + "display_github": True, "github_user": "arrow-py", "github_repo": "arrow", - "github_banner": True, - "show_related": False, - "show_powered_by": False, - "github_button": True, - "github_type": "star", - "github_count": "true", # must be a string + "github_version": "master/docs/", } -html_sidebars = { - "**": ["about.html", "localtoc.html", "relations.html", "searchbox.html"] +# https://sphinx-rtd-theme.readthedocs.io/en/stable/index.html +html_theme_options = { + "logo_only": False, + "prev_next_buttons_location": "both", + "style_nav_header_background": "grey", + # TOC options + "collapse_navigation": False, + "navigation_depth": 3, } # Generate PDFs with unicode characters diff --git a/docs/getting-started.rst b/docs/getting-started.rst new file mode 100644 index 00000000..6ebd3346 --- /dev/null +++ b/docs/getting-started.rst @@ -0,0 +1,9 @@ +*************************************** +Getting started +*************************************** + +Assuming you have Python already, follow the guidelines below to get started with Arrow. + +.. include:: ../README.rst + :start-after: Quick Start + :end-before: end-inclusion-marker-do-not-remove diff --git a/docs/guide.rst b/docs/guide.rst new file mode 100644 index 00000000..aef0e880 --- /dev/null +++ b/docs/guide.rst @@ -0,0 +1,555 @@ +*************************************** +User’s Guide +*************************************** + + +Creation +~~~~~~~~ + +Get 'now' easily: + +.. code-block:: python + + >>> arrow.utcnow() + + + >>> arrow.now() + + + >>> arrow.now('US/Pacific') + + +Create from timestamps (:code:`int` or :code:`float`): + +.. code-block:: python + + >>> arrow.get(1367900664) + + + >>> arrow.get(1367900664.152325) + + +Use a naive or timezone-aware datetime, or flexibly specify a timezone: + +.. code-block:: python + + >>> arrow.get(datetime.utcnow()) + + + >>> arrow.get(datetime(2013, 5, 5), 'US/Pacific') + + + >>> from dateutil import tz + >>> arrow.get(datetime(2013, 5, 5), tz.gettz('US/Pacific')) + + + >>> arrow.get(datetime.now(tz.gettz('US/Pacific'))) + + +Parse from a string: + +.. code-block:: python + + >>> arrow.get('2013-05-05 12:30:45', 'YYYY-MM-DD HH:mm:ss') + + +Search a date in a string: + +.. code-block:: python + + >>> arrow.get('June was born in May 1980', 'MMMM YYYY') + + +Some ISO 8601 compliant strings are recognized and parsed without a format string: + + >>> arrow.get('2013-09-30T15:34:00.000-07:00') + + +Arrow objects can be instantiated directly too, with the same arguments as a datetime: + +.. code-block:: python + + >>> arrow.get(2013, 5, 5) + + + >>> arrow.Arrow(2013, 5, 5) + + +Properties +~~~~~~~~~~ + +Get a datetime or timestamp representation: + +.. code-block:: python + + >>> a = arrow.utcnow() + >>> a.datetime + datetime.datetime(2013, 5, 7, 4, 38, 15, 447644, tzinfo=tzutc()) + +Get a naive datetime, and tzinfo: + +.. code-block:: python + + >>> a.naive + datetime.datetime(2013, 5, 7, 4, 38, 15, 447644) + + >>> a.tzinfo + tzutc() + +Get any datetime value: + +.. code-block:: python + + >>> a.year + 2013 + +Call datetime functions that return properties: + +.. code-block:: python + + >>> a.date() + datetime.date(2013, 5, 7) + + >>> a.time() + datetime.time(4, 38, 15, 447644) + +Replace & Shift +~~~~~~~~~~~~~~~ + +Get a new :class:`Arrow ` object, with altered attributes, just as you would with a datetime: + +.. code-block:: python + + >>> arw = arrow.utcnow() + >>> arw + + + >>> arw.replace(hour=4, minute=40) + + +Or, get one with attributes shifted forward or backward: + +.. code-block:: python + + >>> arw.shift(weeks=+3) + + +Even replace the timezone without altering other attributes: + +.. code-block:: python + + >>> arw.replace(tzinfo='US/Pacific') + + +Move between the earlier and later moments of an ambiguous time: + +.. code-block:: python + + >>> paris_transition = arrow.Arrow(2019, 10, 27, 2, tzinfo="Europe/Paris", fold=0) + >>> paris_transition + + >>> paris_transition.ambiguous + True + >>> paris_transition.replace(fold=1) + + +Format +~~~~~~ + +.. code-block:: python + + >>> arrow.utcnow().format('YYYY-MM-DD HH:mm:ss ZZ') + '2013-05-07 05:23:16 -00:00' + +Convert +~~~~~~~ + +Convert from UTC to other timezones by name or tzinfo: + +.. code-block:: python + + >>> utc = arrow.utcnow() + >>> utc + + + >>> utc.to('US/Pacific') + + + >>> utc.to(tz.gettz('US/Pacific')) + + +Or using shorthand: + +.. code-block:: python + + >>> utc.to('local') + + + >>> utc.to('local').to('utc') + + + +Humanize +~~~~~~~~ + +Humanize relative to now: + +.. code-block:: python + + >>> past = arrow.utcnow().shift(hours=-1) + >>> past.humanize() + 'an hour ago' + +Or another Arrow, or datetime: + +.. code-block:: python + + >>> present = arrow.utcnow() + >>> future = present.shift(hours=2) + >>> future.humanize(present) + 'in 2 hours' + +Indicate time as relative or include only the distance + +.. code-block:: python + + >>> present = arrow.utcnow() + >>> future = present.shift(hours=2) + >>> future.humanize(present) + 'in 2 hours' + >>> future.humanize(present, only_distance=True) + '2 hours' + + +Indicate a specific time granularity (or multiple): + +.. code-block:: python + + >>> present = arrow.utcnow() + >>> future = present.shift(minutes=66) + >>> future.humanize(present, granularity="minute") + 'in 66 minutes' + >>> future.humanize(present, granularity=["hour", "minute"]) + 'in an hour and 6 minutes' + >>> present.humanize(future, granularity=["hour", "minute"]) + 'an hour and 6 minutes ago' + >>> future.humanize(present, only_distance=True, granularity=["hour", "minute"]) + 'an hour and 6 minutes' + +Support for a growing number of locales (see ``locales.py`` for supported languages): + +.. code-block:: python + + + >>> future = arrow.utcnow().shift(hours=1) + >>> future.humanize(a, locale='ru') + 'через 2 час(а,ов)' + +Dehumanize +~~~~~~~~~~ + +Take a human readable string and use it to shift into a past time: + +.. code-block:: python + + >>> arw = arrow.utcnow() + >>> arw + + >>> earlier = arw.dehumanize("2 days ago") + >>> earlier + + +Or use it to shift into a future time: + +.. code-block:: python + + >>> arw = arrow.utcnow() + >>> arw + + >>> later = arw.dehumanize("in a month") + >>> later + + +Support for a growing number of locales (see ``constants.py`` for supported languages): + +.. code-block:: python + + >>> arw = arrow.utcnow() + >>> arw + + >>> later = arw.dehumanize("एक माह बाद", locale="hi") + >>> later + + +Ranges & Spans +~~~~~~~~~~~~~~ + +Get the time span of any unit: + +.. code-block:: python + + >>> arrow.utcnow().span('hour') + (, ) + +Or just get the floor and ceiling: + +.. code-block:: python + + >>> arrow.utcnow().floor('hour') + + + >>> arrow.utcnow().ceil('hour') + + +You can also get a range of time spans: + +.. code-block:: python + + >>> start = datetime(2013, 5, 5, 12, 30) + >>> end = datetime(2013, 5, 5, 17, 15) + >>> for r in arrow.Arrow.span_range('hour', start, end): + ... print(r) + ... + (, ) + (, ) + (, ) + (, ) + (, ) + +Or just iterate over a range of time: + +.. code-block:: python + + >>> start = datetime(2013, 5, 5, 12, 30) + >>> end = datetime(2013, 5, 5, 17, 15) + >>> for r in arrow.Arrow.range('hour', start, end): + ... print(repr(r)) + ... + + + + + + +.. toctree:: + :maxdepth: 2 + +Factories +~~~~~~~~~ + +Use factories to harness Arrow's module API for a custom Arrow-derived type. First, derive your type: + +.. code-block:: python + + >>> class CustomArrow(arrow.Arrow): + ... + ... def days_till_xmas(self): + ... + ... xmas = arrow.Arrow(self.year, 12, 25) + ... + ... if self > xmas: + ... xmas = xmas.shift(years=1) + ... + ... return (xmas - self).days + + +Then get and use a factory for it: + +.. code-block:: python + + >>> factory = arrow.ArrowFactory(CustomArrow) + >>> custom = factory.utcnow() + >>> custom + >>> + + >>> custom.days_till_xmas() + >>> 211 + +Supported Tokens +~~~~~~~~~~~~~~~~ + +Use the following tokens for parsing and formatting. Note that they are **not** the same as the tokens for `strptime `_: + ++--------------------------------+--------------+-------------------------------------------+ +| |Token |Output | ++================================+==============+===========================================+ +|**Year** |YYYY |2000, 2001, 2002 ... 2012, 2013 | ++--------------------------------+--------------+-------------------------------------------+ +| |YY |00, 01, 02 ... 12, 13 | ++--------------------------------+--------------+-------------------------------------------+ +|**Month** |MMMM |January, February, March ... [#t1]_ | ++--------------------------------+--------------+-------------------------------------------+ +| |MMM |Jan, Feb, Mar ... [#t1]_ | ++--------------------------------+--------------+-------------------------------------------+ +| |MM |01, 02, 03 ... 11, 12 | ++--------------------------------+--------------+-------------------------------------------+ +| |M |1, 2, 3 ... 11, 12 | ++--------------------------------+--------------+-------------------------------------------+ +|**Day of Year** |DDDD |001, 002, 003 ... 364, 365 | ++--------------------------------+--------------+-------------------------------------------+ +| |DDD |1, 2, 3 ... 364, 365 | ++--------------------------------+--------------+-------------------------------------------+ +|**Day of Month** |DD |01, 02, 03 ... 30, 31 | ++--------------------------------+--------------+-------------------------------------------+ +| |D |1, 2, 3 ... 30, 31 | ++--------------------------------+--------------+-------------------------------------------+ +| |Do |1st, 2nd, 3rd ... 30th, 31st | ++--------------------------------+--------------+-------------------------------------------+ +|**Day of Week** |dddd |Monday, Tuesday, Wednesday ... [#t2]_ | ++--------------------------------+--------------+-------------------------------------------+ +| |ddd |Mon, Tue, Wed ... [#t2]_ | ++--------------------------------+--------------+-------------------------------------------+ +| |d |1, 2, 3 ... 6, 7 | ++--------------------------------+--------------+-------------------------------------------+ +|**ISO week date** |W |2011-W05-4, 2019-W17 | ++--------------------------------+--------------+-------------------------------------------+ +|**Hour** |HH |00, 01, 02 ... 23, 24 | ++--------------------------------+--------------+-------------------------------------------+ +| |H |0, 1, 2 ... 23, 24 | ++--------------------------------+--------------+-------------------------------------------+ +| |hh |01, 02, 03 ... 11, 12 | ++--------------------------------+--------------+-------------------------------------------+ +| |h |1, 2, 3 ... 11, 12 | ++--------------------------------+--------------+-------------------------------------------+ +|**AM / PM** |A |AM, PM, am, pm [#t1]_ | ++--------------------------------+--------------+-------------------------------------------+ +| |a |am, pm [#t1]_ | ++--------------------------------+--------------+-------------------------------------------+ +|**Minute** |mm |00, 01, 02 ... 58, 59 | ++--------------------------------+--------------+-------------------------------------------+ +| |m |0, 1, 2 ... 58, 59 | ++--------------------------------+--------------+-------------------------------------------+ +|**Second** |ss |00, 01, 02 ... 58, 59 | ++--------------------------------+--------------+-------------------------------------------+ +| |s |0, 1, 2 ... 58, 59 | ++--------------------------------+--------------+-------------------------------------------+ +|**Sub-second** |S... |0, 02, 003, 000006, 123123123123... [#t3]_ | ++--------------------------------+--------------+-------------------------------------------+ +|**Timezone** |ZZZ |Asia/Baku, Europe/Warsaw, GMT ... [#t4]_ | ++--------------------------------+--------------+-------------------------------------------+ +| |ZZ |-07:00, -06:00 ... +06:00, +07:00, +08, Z | ++--------------------------------+--------------+-------------------------------------------+ +| |Z |-0700, -0600 ... +0600, +0700, +08, Z | ++--------------------------------+--------------+-------------------------------------------+ +|**Seconds Timestamp** |X |1381685817, 1381685817.915482 ... [#t5]_ | ++--------------------------------+--------------+-------------------------------------------+ +|**ms or µs Timestamp** |x |1569980330813, 1569980330813221 | ++--------------------------------+--------------+-------------------------------------------+ + +.. rubric:: Footnotes + +.. [#t1] localization support for parsing and formatting +.. [#t2] localization support only for formatting +.. [#t3] the result is truncated to microseconds, with `half-to-even rounding `_. +.. [#t4] timezone names from `tz database `_ provided via dateutil package, note that abbreviations such as MST, PDT, BRST are unlikely to parse due to ambiguity. Use the full IANA zone name instead (Asia/Shanghai, Europe/London, America/Chicago etc). +.. [#t5] this token cannot be used for parsing timestamps out of natural language strings due to compatibility reasons + +Built-in Formats +++++++++++++++++ + +There are several formatting standards that are provided as built-in tokens. + +.. code-block:: python + + >>> arw = arrow.utcnow() + >>> arw.format(arrow.FORMAT_ATOM) + '2020-05-27 10:30:35+00:00' + >>> arw.format(arrow.FORMAT_COOKIE) + 'Wednesday, 27-May-2020 10:30:35 UTC' + >>> arw.format(arrow.FORMAT_RSS) + 'Wed, 27 May 2020 10:30:35 +0000' + >>> arw.format(arrow.FORMAT_RFC822) + 'Wed, 27 May 20 10:30:35 +0000' + >>> arw.format(arrow.FORMAT_RFC850) + 'Wednesday, 27-May-20 10:30:35 UTC' + >>> arw.format(arrow.FORMAT_RFC1036) + 'Wed, 27 May 20 10:30:35 +0000' + >>> arw.format(arrow.FORMAT_RFC1123) + 'Wed, 27 May 2020 10:30:35 +0000' + >>> arw.format(arrow.FORMAT_RFC2822) + 'Wed, 27 May 2020 10:30:35 +0000' + >>> arw.format(arrow.FORMAT_RFC3339) + '2020-05-27 10:30:35+00:00' + >>> arw.format(arrow.FORMAT_W3C) + '2020-05-27 10:30:35+00:00' + +Escaping Formats +~~~~~~~~~~~~~~~~ + +Tokens, phrases, and regular expressions in a format string can be escaped when parsing and formatting by enclosing them within square brackets. + +Tokens & Phrases +++++++++++++++++ + +Any `token `_ or phrase can be escaped as follows: + +.. code-block:: python + + >>> fmt = "YYYY-MM-DD h [h] m" + >>> arw = arrow.get("2018-03-09 8 h 40", fmt) + + >>> arw.format(fmt) + '2018-03-09 8 h 40' + + >>> fmt = "YYYY-MM-DD h [hello] m" + >>> arw = arrow.get("2018-03-09 8 hello 40", fmt) + + >>> arw.format(fmt) + '2018-03-09 8 hello 40' + + >>> fmt = "YYYY-MM-DD h [hello world] m" + >>> arw = arrow.get("2018-03-09 8 hello world 40", fmt) + + >>> arw.format(fmt) + '2018-03-09 8 hello world 40' + +This can be useful for parsing dates in different locales such as French, in which it is common to format time strings as "8 h 40" rather than "8:40". + +Regular Expressions ++++++++++++++++++++ + +You can also escape regular expressions by enclosing them within square brackets. In the following example, we are using the regular expression :code:`\s+` to match any number of whitespace characters that separate the tokens. This is useful if you do not know the number of spaces between tokens ahead of time (e.g. in log files). + +.. code-block:: python + + >>> fmt = r"ddd[\s+]MMM[\s+]DD[\s+]HH:mm:ss[\s+]YYYY" + >>> arrow.get("Mon Sep 08 16:41:45 2014", fmt) + + + >>> arrow.get("Mon \tSep 08 16:41:45 2014", fmt) + + + >>> arrow.get("Mon Sep 08 16:41:45 2014", fmt) + + +Punctuation +~~~~~~~~~~~ + +Date and time formats may be fenced on either side by one punctuation character from the following list: ``, . ; : ? ! " \` ' [ ] { } ( ) < >`` + +.. code-block:: python + + >>> arrow.get("Cool date: 2019-10-31T09:12:45.123456+04:30.", "YYYY-MM-DDTHH:mm:ss.SZZ") + + + >>> arrow.get("Tomorrow (2019-10-31) is Halloween!", "YYYY-MM-DD") + + + >>> arrow.get("Halloween is on 2019.10.31.", "YYYY.MM.DD") + + + >>> arrow.get("It's Halloween tomorrow (2019-10-31)!", "YYYY-MM-DD") + # Raises exception because there are multiple punctuation marks following the date + +Redundant Whitespace +~~~~~~~~~~~~~~~~~~~~ + +Redundant whitespace characters (spaces, tabs, and newlines) can be normalized automatically by passing in the ``normalize_whitespace`` flag to ``arrow.get``: + +.. code-block:: python + + >>> arrow.get('\t \n 2013-05-05T12:30:45.123456 \t \n', normalize_whitespace=True) + + + >>> arrow.get('2013-05-05 T \n 12:30:45\t123456', 'YYYY-MM-DD T HH:mm:ss S', normalize_whitespace=True) + diff --git a/docs/index.rst b/docs/index.rst index d4f9ec2a..0ad2fdba 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -3,593 +3,31 @@ Arrow: Better dates & times for Python Release v\ |release| (`Installation`_) (`Changelog `_) +`Go to repository `_ + .. include:: ../README.rst :start-after: start-inclusion-marker-do-not-remove :end-before: end-inclusion-marker-do-not-remove -User's Guide ------------- - -Creation -~~~~~~~~ - -Get 'now' easily: - -.. code-block:: python - - >>> arrow.utcnow() - - - >>> arrow.now() - - - >>> arrow.now('US/Pacific') - - -Create from timestamps (:code:`int` or :code:`float`): - -.. code-block:: python - - >>> arrow.get(1367900664) - - - >>> arrow.get(1367900664.152325) - - -Use a naive or timezone-aware datetime, or flexibly specify a timezone: - -.. code-block:: python - - >>> arrow.get(datetime.utcnow()) - - - >>> arrow.get(datetime(2013, 5, 5), 'US/Pacific') - - - >>> from dateutil import tz - >>> arrow.get(datetime(2013, 5, 5), tz.gettz('US/Pacific')) - - - >>> arrow.get(datetime.now(tz.gettz('US/Pacific'))) - - -Parse from a string: - -.. code-block:: python - - >>> arrow.get('2013-05-05 12:30:45', 'YYYY-MM-DD HH:mm:ss') - - -Search a date in a string: - -.. code-block:: python - - >>> arrow.get('June was born in May 1980', 'MMMM YYYY') - - -Some ISO 8601 compliant strings are recognized and parsed without a format string: - - >>> arrow.get('2013-09-30T15:34:00.000-07:00') - - -Arrow objects can be instantiated directly too, with the same arguments as a datetime: - -.. code-block:: python - - >>> arrow.get(2013, 5, 5) - - - >>> arrow.Arrow(2013, 5, 5) - - -Properties -~~~~~~~~~~ - -Get a datetime or timestamp representation: - -.. code-block:: python - - >>> a = arrow.utcnow() - >>> a.datetime - datetime.datetime(2013, 5, 7, 4, 38, 15, 447644, tzinfo=tzutc()) - -Get a naive datetime, and tzinfo: - -.. code-block:: python - - >>> a.naive - datetime.datetime(2013, 5, 7, 4, 38, 15, 447644) - - >>> a.tzinfo - tzutc() - -Get any datetime value: - -.. code-block:: python - - >>> a.year - 2013 - -Call datetime functions that return properties: - -.. code-block:: python - - >>> a.date() - datetime.date(2013, 5, 7) - - >>> a.time() - datetime.time(4, 38, 15, 447644) - -Replace & Shift -~~~~~~~~~~~~~~~ - -Get a new :class:`Arrow ` object, with altered attributes, just as you would with a datetime: - -.. code-block:: python - - >>> arw = arrow.utcnow() - >>> arw - - - >>> arw.replace(hour=4, minute=40) - - -Or, get one with attributes shifted forward or backward: - -.. code-block:: python - - >>> arw.shift(weeks=+3) - - -Even replace the timezone without altering other attributes: - -.. code-block:: python - - >>> arw.replace(tzinfo='US/Pacific') - - -Move between the earlier and later moments of an ambiguous time: - -.. code-block:: python - - >>> paris_transition = arrow.Arrow(2019, 10, 27, 2, tzinfo="Europe/Paris", fold=0) - >>> paris_transition - - >>> paris_transition.ambiguous - True - >>> paris_transition.replace(fold=1) - - -Format -~~~~~~ - -.. code-block:: python - - >>> arrow.utcnow().format('YYYY-MM-DD HH:mm:ss ZZ') - '2013-05-07 05:23:16 -00:00' - -Convert -~~~~~~~ - -Convert from UTC to other timezones by name or tzinfo: - -.. code-block:: python - - >>> utc = arrow.utcnow() - >>> utc - - - >>> utc.to('US/Pacific') - - - >>> utc.to(tz.gettz('US/Pacific')) - - -Or using shorthand: - -.. code-block:: python - - >>> utc.to('local') - - - >>> utc.to('local').to('utc') - - - -Humanize -~~~~~~~~ - -Humanize relative to now: - -.. code-block:: python - - >>> past = arrow.utcnow().shift(hours=-1) - >>> past.humanize() - 'an hour ago' - -Or another Arrow, or datetime: - -.. code-block:: python - - >>> present = arrow.utcnow() - >>> future = present.shift(hours=2) - >>> future.humanize(present) - 'in 2 hours' - -Indicate time as relative or include only the distance - -.. code-block:: python - - >>> present = arrow.utcnow() - >>> future = present.shift(hours=2) - >>> future.humanize(present) - 'in 2 hours' - >>> future.humanize(present, only_distance=True) - '2 hours' - - -Indicate a specific time granularity (or multiple): - -.. code-block:: python - - >>> present = arrow.utcnow() - >>> future = present.shift(minutes=66) - >>> future.humanize(present, granularity="minute") - 'in 66 minutes' - >>> future.humanize(present, granularity=["hour", "minute"]) - 'in an hour and 6 minutes' - >>> present.humanize(future, granularity=["hour", "minute"]) - 'an hour and 6 minutes ago' - >>> future.humanize(present, only_distance=True, granularity=["hour", "minute"]) - 'an hour and 6 minutes' - -Support for a growing number of locales (see ``locales.py`` for supported languages): - -.. code-block:: python - - - >>> future = arrow.utcnow().shift(hours=1) - >>> future.humanize(a, locale='ru') - 'через 2 час(а,ов)' - -Dehumanize -~~~~~~~~~~ - -Take a human readable string and use it to shift into a past time: - -.. code-block:: python - - >>> arw = arrow.utcnow() - >>> arw - - >>> earlier = arw.dehumanize("2 days ago") - >>> earlier - - -Or use it to shift into a future time: - -.. code-block:: python - - >>> arw = arrow.utcnow() - >>> arw - - >>> later = arw.dehumanize("in a month") - >>> later - - -Support for a growing number of locales (see ``constants.py`` for supported languages): - -.. code-block:: python - - >>> arw = arrow.utcnow() - >>> arw - - >>> later = arw.dehumanize("एक माह बाद", locale="hi") - >>> later - - -Ranges & Spans -~~~~~~~~~~~~~~ - -Get the time span of any unit: - -.. code-block:: python - - >>> arrow.utcnow().span('hour') - (, ) - -Or just get the floor and ceiling: - -.. code-block:: python - - >>> arrow.utcnow().floor('hour') - - - >>> arrow.utcnow().ceil('hour') - - -You can also get a range of time spans: - -.. code-block:: python - - >>> start = datetime(2013, 5, 5, 12, 30) - >>> end = datetime(2013, 5, 5, 17, 15) - >>> for r in arrow.Arrow.span_range('hour', start, end): - ... print(r) - ... - (, ) - (, ) - (, ) - (, ) - (, ) - -Or just iterate over a range of time: - -.. code-block:: python - - >>> start = datetime(2013, 5, 5, 12, 30) - >>> end = datetime(2013, 5, 5, 17, 15) - >>> for r in arrow.Arrow.range('hour', start, end): - ... print(repr(r)) - ... - - - - - - .. toctree:: - :maxdepth: 2 - -Factories -~~~~~~~~~ - -Use factories to harness Arrow's module API for a custom Arrow-derived type. First, derive your type: - -.. code-block:: python - - >>> class CustomArrow(arrow.Arrow): - ... - ... def days_till_xmas(self): - ... - ... xmas = arrow.Arrow(self.year, 12, 25) - ... - ... if self > xmas: - ... xmas = xmas.shift(years=1) - ... - ... return (xmas - self).days - - -Then get and use a factory for it: - -.. code-block:: python - - >>> factory = arrow.ArrowFactory(CustomArrow) - >>> custom = factory.utcnow() - >>> custom - >>> - - >>> custom.days_till_xmas() - >>> 211 - -Supported Tokens -~~~~~~~~~~~~~~~~ + :maxdepth: 2 -Use the following tokens for parsing and formatting. Note that they are **not** the same as the tokens for `strptime `_: + getting-started -+--------------------------------+--------------+-------------------------------------------+ -| |Token |Output | -+================================+==============+===========================================+ -|**Year** |YYYY |2000, 2001, 2002 ... 2012, 2013 | -+--------------------------------+--------------+-------------------------------------------+ -| |YY |00, 01, 02 ... 12, 13 | -+--------------------------------+--------------+-------------------------------------------+ -|**Month** |MMMM |January, February, March ... [#t1]_ | -+--------------------------------+--------------+-------------------------------------------+ -| |MMM |Jan, Feb, Mar ... [#t1]_ | -+--------------------------------+--------------+-------------------------------------------+ -| |MM |01, 02, 03 ... 11, 12 | -+--------------------------------+--------------+-------------------------------------------+ -| |M |1, 2, 3 ... 11, 12 | -+--------------------------------+--------------+-------------------------------------------+ -|**Day of Year** |DDDD |001, 002, 003 ... 364, 365 | -+--------------------------------+--------------+-------------------------------------------+ -| |DDD |1, 2, 3 ... 364, 365 | -+--------------------------------+--------------+-------------------------------------------+ -|**Day of Month** |DD |01, 02, 03 ... 30, 31 | -+--------------------------------+--------------+-------------------------------------------+ -| |D |1, 2, 3 ... 30, 31 | -+--------------------------------+--------------+-------------------------------------------+ -| |Do |1st, 2nd, 3rd ... 30th, 31st | -+--------------------------------+--------------+-------------------------------------------+ -|**Day of Week** |dddd |Monday, Tuesday, Wednesday ... [#t2]_ | -+--------------------------------+--------------+-------------------------------------------+ -| |ddd |Mon, Tue, Wed ... [#t2]_ | -+--------------------------------+--------------+-------------------------------------------+ -| |d |1, 2, 3 ... 6, 7 | -+--------------------------------+--------------+-------------------------------------------+ -|**ISO week date** |W |2011-W05-4, 2019-W17 | -+--------------------------------+--------------+-------------------------------------------+ -|**Hour** |HH |00, 01, 02 ... 23, 24 | -+--------------------------------+--------------+-------------------------------------------+ -| |H |0, 1, 2 ... 23, 24 | -+--------------------------------+--------------+-------------------------------------------+ -| |hh |01, 02, 03 ... 11, 12 | -+--------------------------------+--------------+-------------------------------------------+ -| |h |1, 2, 3 ... 11, 12 | -+--------------------------------+--------------+-------------------------------------------+ -|**AM / PM** |A |AM, PM, am, pm [#t1]_ | -+--------------------------------+--------------+-------------------------------------------+ -| |a |am, pm [#t1]_ | -+--------------------------------+--------------+-------------------------------------------+ -|**Minute** |mm |00, 01, 02 ... 58, 59 | -+--------------------------------+--------------+-------------------------------------------+ -| |m |0, 1, 2 ... 58, 59 | -+--------------------------------+--------------+-------------------------------------------+ -|**Second** |ss |00, 01, 02 ... 58, 59 | -+--------------------------------+--------------+-------------------------------------------+ -| |s |0, 1, 2 ... 58, 59 | -+--------------------------------+--------------+-------------------------------------------+ -|**Sub-second** |S... |0, 02, 003, 000006, 123123123123... [#t3]_ | -+--------------------------------+--------------+-------------------------------------------+ -|**Timezone** |ZZZ |Asia/Baku, Europe/Warsaw, GMT ... [#t4]_ | -+--------------------------------+--------------+-------------------------------------------+ -| |ZZ |-07:00, -06:00 ... +06:00, +07:00, +08, Z | -+--------------------------------+--------------+-------------------------------------------+ -| |Z |-0700, -0600 ... +0600, +0700, +08, Z | -+--------------------------------+--------------+-------------------------------------------+ -|**Seconds Timestamp** |X |1381685817, 1381685817.915482 ... [#t5]_ | -+--------------------------------+--------------+-------------------------------------------+ -|**ms or µs Timestamp** |x |1569980330813, 1569980330813221 | -+--------------------------------+--------------+-------------------------------------------+ - -.. rubric:: Footnotes - -.. [#t1] localization support for parsing and formatting -.. [#t2] localization support only for formatting -.. [#t3] the result is truncated to microseconds, with `half-to-even rounding `_. -.. [#t4] timezone names from `tz database `_ provided via dateutil package, note that abbreviations such as MST, PDT, BRST are unlikely to parse due to ambiguity. Use the full IANA zone name instead (Asia/Shanghai, Europe/London, America/Chicago etc). -.. [#t5] this token cannot be used for parsing timestamps out of natural language strings due to compatibility reasons - -Built-in Formats -++++++++++++++++ - -There are several formatting standards that are provided as built-in tokens. - -.. code-block:: python - - >>> arw = arrow.utcnow() - >>> arw.format(arrow.FORMAT_ATOM) - '2020-05-27 10:30:35+00:00' - >>> arw.format(arrow.FORMAT_COOKIE) - 'Wednesday, 27-May-2020 10:30:35 UTC' - >>> arw.format(arrow.FORMAT_RSS) - 'Wed, 27 May 2020 10:30:35 +0000' - >>> arw.format(arrow.FORMAT_RFC822) - 'Wed, 27 May 20 10:30:35 +0000' - >>> arw.format(arrow.FORMAT_RFC850) - 'Wednesday, 27-May-20 10:30:35 UTC' - >>> arw.format(arrow.FORMAT_RFC1036) - 'Wed, 27 May 20 10:30:35 +0000' - >>> arw.format(arrow.FORMAT_RFC1123) - 'Wed, 27 May 2020 10:30:35 +0000' - >>> arw.format(arrow.FORMAT_RFC2822) - 'Wed, 27 May 2020 10:30:35 +0000' - >>> arw.format(arrow.FORMAT_RFC3339) - '2020-05-27 10:30:35+00:00' - >>> arw.format(arrow.FORMAT_W3C) - '2020-05-27 10:30:35+00:00' - -Escaping Formats -~~~~~~~~~~~~~~~~ - -Tokens, phrases, and regular expressions in a format string can be escaped when parsing and formatting by enclosing them within square brackets. - -Tokens & Phrases -++++++++++++++++ - -Any `token `_ or phrase can be escaped as follows: - -.. code-block:: python - - >>> fmt = "YYYY-MM-DD h [h] m" - >>> arw = arrow.get("2018-03-09 8 h 40", fmt) - - >>> arw.format(fmt) - '2018-03-09 8 h 40' - - >>> fmt = "YYYY-MM-DD h [hello] m" - >>> arw = arrow.get("2018-03-09 8 hello 40", fmt) - - >>> arw.format(fmt) - '2018-03-09 8 hello 40' - - >>> fmt = "YYYY-MM-DD h [hello world] m" - >>> arw = arrow.get("2018-03-09 8 hello world 40", fmt) - - >>> arw.format(fmt) - '2018-03-09 8 hello world 40' - -This can be useful for parsing dates in different locales such as French, in which it is common to format time strings as "8 h 40" rather than "8:40". - -Regular Expressions -+++++++++++++++++++ - -You can also escape regular expressions by enclosing them within square brackets. In the following example, we are using the regular expression :code:`\s+` to match any number of whitespace characters that separate the tokens. This is useful if you do not know the number of spaces between tokens ahead of time (e.g. in log files). - -.. code-block:: python - - >>> fmt = r"ddd[\s+]MMM[\s+]DD[\s+]HH:mm:ss[\s+]YYYY" - >>> arrow.get("Mon Sep 08 16:41:45 2014", fmt) - - - >>> arrow.get("Mon \tSep 08 16:41:45 2014", fmt) - - - >>> arrow.get("Mon Sep 08 16:41:45 2014", fmt) - - -Punctuation -~~~~~~~~~~~ - -Date and time formats may be fenced on either side by one punctuation character from the following list: ``, . ; : ? ! " \` ' [ ] { } ( ) < >`` - -.. code-block:: python - - >>> arrow.get("Cool date: 2019-10-31T09:12:45.123456+04:30.", "YYYY-MM-DDTHH:mm:ss.SZZ") - - - >>> arrow.get("Tomorrow (2019-10-31) is Halloween!", "YYYY-MM-DD") - - - >>> arrow.get("Halloween is on 2019.10.31.", "YYYY.MM.DD") - - - >>> arrow.get("It's Halloween tomorrow (2019-10-31)!", "YYYY-MM-DD") - # Raises exception because there are multiple punctuation marks following the date - -Redundant Whitespace -~~~~~~~~~~~~~~~~~~~~ - -Redundant whitespace characters (spaces, tabs, and newlines) can be normalized automatically by passing in the ``normalize_whitespace`` flag to ``arrow.get``: - -.. code-block:: python - - >>> arrow.get('\t \n 2013-05-05T12:30:45.123456 \t \n', normalize_whitespace=True) - - - >>> arrow.get('2013-05-05 T \n 12:30:45\t123456', 'YYYY-MM-DD T HH:mm:ss S', normalize_whitespace=True) - - -API Guide ---------- - -arrow.arrow -~~~~~~~~~~~ - -.. automodule:: arrow.arrow - :members: - -arrow.factory -~~~~~~~~~~~~~ +--------------- -.. automodule:: arrow.factory - :members: +.. toctree:: + :maxdepth: 2 -arrow.api -~~~~~~~~~ + guide -.. automodule:: arrow.api - :members: +--------------- -arrow.locale -~~~~~~~~~~~~ +.. toctree:: + :maxdepth: 2 -.. automodule:: arrow.locales - :members: - :undoc-members: + api-guide -Release History --------------- .. toctree:: diff --git a/docs/releases.rst b/docs/releases.rst index 22e1e59c..ed21b487 100644 --- a/docs/releases.rst +++ b/docs/releases.rst @@ -1,3 +1,7 @@ +*************************************** +Release History +*************************************** + .. _releases: .. include:: ../CHANGELOG.rst diff --git a/requirements/requirements-docs.txt b/requirements/requirements-docs.txt index de59f1a3..abc47b28 100644 --- a/requirements/requirements-docs.txt +++ b/requirements/requirements-docs.txt @@ -3,3 +3,4 @@ doc8 sphinx sphinx-autobuild sphinx-autodoc-typehints +sphinx_rtd_theme From 74a759b88447b6ecd9fd5de610f272c8fb6130a2 Mon Sep 17 00:00:00 2001 From: "Kristijan \"Fremen\" Velkovski" Date: Tue, 15 Nov 2022 10:25:22 -0600 Subject: [PATCH 27/80] Upgrade pre-commit hook versions (#1140) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d13b4529..b035115a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -22,7 +22,7 @@ repos: hooks: - id: isort - repo: https://github.com/asottile/pyupgrade - rev: v3.0.0 + rev: v3.2.0 hooks: - id: pyupgrade args: [--py36-plus] @@ -38,7 +38,7 @@ repos: - id: rst-inline-touching-normal - id: text-unicode-replacement-char - repo: https://github.com/psf/black - rev: 22.8.0 + rev: 22.10.0 hooks: - id: black args: [--safe, --quiet, --target-version=py36] From de0aea98051f9d99f237e7edd27ae3e5c6bf1489 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Mon, 11 Sep 2023 22:30:56 -0700 Subject: [PATCH 28/80] Fix failing builds and drop Py36 and 37 support (#1163) --- .github/workflows/continuous_integration.yml | 23 +-- .pre-commit-config.yaml | 14 +- .readthedocs.yaml | 29 +++ LICENSE | 2 +- Makefile | 7 +- arrow/arrow.py | 21 +- arrow/factory.py | 3 - arrow/formatter.py | 4 - arrow/locales.py | 17 +- arrow/parser.py | 16 +- docs/conf.py | 2 +- requirements/requirements-docs.txt | 4 +- requirements/requirements.txt | 1 - setup.py | 10 +- tests/test_arrow.py | 199 ------------------- tests/test_factory.py | 41 ---- tests/test_formatter.py | 13 -- tests/test_locales.py | 42 ---- tests/test_parser.py | 66 ------ tox.ini | 3 +- 20 files changed, 74 insertions(+), 443 deletions(-) create mode 100644 .readthedocs.yaml diff --git a/.github/workflows/continuous_integration.yml b/.github/workflows/continuous_integration.yml index 34d9c4f2..29b8df6d 100644 --- a/.github/workflows/continuous_integration.yml +++ b/.github/workflows/continuous_integration.yml @@ -9,18 +9,18 @@ on: - cron: "0 0 1 * *" jobs: - test: + unit-tests: name: ${{ matrix.os }} (${{ matrix.python-version }}) runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: - python-version: ["pypy-3.7", "3.6", "3.7", "3.8", "3.9", "3.10", "3.11"] + python-version: ["pypy-3.9", "3.8", "3.9", "3.10", "3.11", "3.12-dev"] os: [ubuntu-latest, macos-latest, windows-latest] exclude: # pypy3 randomly fails on Windows builds - os: windows-latest - python-version: "pypy-3.7" + python-version: "pypy-3.9" include: - os: ubuntu-latest path: ~/.cache/pip @@ -51,26 +51,25 @@ jobs: with: file: coverage.xml - lint: + linting: + name: Linting runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - name: Set up Python 3.10 - uses: actions/setup-python@v4 - with: - python-version: "3.10" - - name: Cache pip - uses: actions/cache@v3 + - uses: actions/cache@v3 with: path: ~/.cache/pip key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements*.txt') }} restore-keys: ${{ runner.os }}-pip- - - name: Cache pre-commit - uses: actions/cache@v3 + - uses: actions/cache@v3 with: path: ~/.cache/pre-commit key: ${{ runner.os }}-pre-commit-${{ hashFiles('**/.pre-commit-config.yaml') }} restore-keys: ${{ runner.os }}-pre-commit- + - name: Set up Python ${{ runner.python-version }} + uses: actions/setup-python@v4 + with: + python-version: "3.11" - name: Install dependencies run: | pip install -U pip setuptools wheel diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b035115a..b8a60a2c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,7 +2,7 @@ default_language_version: python: python3 repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.3.0 + rev: v4.4.0 hooks: - id: check-ast - id: check-yaml @@ -18,16 +18,16 @@ repos: args: [requirements/requirements.txt, requirements/requirements-docs.txt, requirements/requirements-tests.txt] - id: trailing-whitespace - repo: https://github.com/timothycrosley/isort - rev: 5.10.1 + rev: 5.12.0 hooks: - id: isort - repo: https://github.com/asottile/pyupgrade - rev: v3.2.0 + rev: v3.10.1 hooks: - id: pyupgrade args: [--py36-plus] - repo: https://github.com/pre-commit/pygrep-hooks - rev: v1.9.0 + rev: v1.10.0 hooks: - id: python-no-eval - id: python-check-blanket-noqa @@ -38,17 +38,17 @@ repos: - id: rst-inline-touching-normal - id: text-unicode-replacement-char - repo: https://github.com/psf/black - rev: 22.10.0 + rev: 23.9.1 hooks: - id: black args: [--safe, --quiet, --target-version=py36] - repo: https://github.com/pycqa/flake8 - rev: 5.0.4 + rev: 6.1.0 hooks: - id: flake8 additional_dependencies: [flake8-bugbear,flake8-annotations] - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.982 + rev: v1.5.1 hooks: - id: mypy additional_dependencies: [types-python-dateutil] diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 00000000..57f8dd08 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,29 @@ +# Read the Docs configuration file for Sphinx projects +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +version: 2 + +build: + os: ubuntu-22.04 + tools: + python: "3.11" + +# Build documentation in the "docs/" directory with Sphinx +sphinx: + configuration: docs/conf.py + # You can configure Sphinx to use a different builder, for instance use the dirhtml builder for simpler URLs + # builder: "dirhtml" + # Fail on all warnings to avoid broken references + # fail_on_warning: true + +# Optionally build your docs in additional formats such as PDF and ePub +# formats: +# - pdf +# - epub + +# Optional but recommended, declare the Python requirements required +# to build your documentation +# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html +python: + install: + - requirements: requirements/requirements-docs.txt diff --git a/LICENSE b/LICENSE index 4f9eea5d..ff864f3b 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2021 Chris Smith + Copyright 2023 Chris Smith Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/Makefile b/Makefile index f55a3dce..27d5cbe4 100644 --- a/Makefile +++ b/Makefile @@ -1,15 +1,14 @@ .PHONY: auto test docs clean -auto: build310 +auto: build311 -build36: PYTHON_VER = python3.6 -build37: PYTHON_VER = python3.7 build38: PYTHON_VER = python3.8 build39: PYTHON_VER = python3.9 build310: PYTHON_VER = python3.10 build311: PYTHON_VER = python3.11 +build312: PYTHON_VER = python3.12 -build36 build37 build38 build39 build310: clean +build36 build37 build38 build39 build310 build311 build312: clean $(PYTHON_VER) -m venv venv . venv/bin/activate; \ pip install -U pip setuptools wheel; \ diff --git a/arrow/arrow.py b/arrow/arrow.py index e855eee0..8d329efd 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -168,9 +168,9 @@ def __init__( isinstance(tzinfo, dt_tzinfo) and hasattr(tzinfo, "localize") and hasattr(tzinfo, "zone") - and tzinfo.zone # type: ignore[attr-defined] + and tzinfo.zone ): - tzinfo = parser.TzinfoParser.parse(tzinfo.zone) # type: ignore[attr-defined] + tzinfo = parser.TzinfoParser.parse(tzinfo.zone) elif isinstance(tzinfo, str): tzinfo = parser.TzinfoParser.parse(tzinfo) @@ -792,7 +792,6 @@ def __str__(self) -> str: return self._datetime.isoformat() def __format__(self, formatstr: str) -> str: - if len(formatstr) > 0: return self.format(formatstr) @@ -804,7 +803,6 @@ def __hash__(self) -> int: # attributes and properties def __getattr__(self, name: str) -> int: - if name == "week": return self.isocalendar()[1] @@ -965,7 +963,6 @@ def replace(self, **kwargs: Any) -> "Arrow": absolute_kwargs = {} for key, value in kwargs.items(): - if key in self._ATTRS: absolute_kwargs[key] = value elif key in ["week", "quarter"]: @@ -1022,7 +1019,6 @@ def shift(self, **kwargs: Any) -> "Arrow": additional_attrs = ["weeks", "quarters", "weekday"] for key, value in kwargs.items(): - if key in self._ATTRS_PLURAL or key in additional_attrs: relative_kwargs[key] = value else: @@ -1263,7 +1259,6 @@ def humanize( return locale.describe(granularity, delta, only_distance=only_distance) else: - if not granularity: raise ValueError( "Empty granularity list provided. " @@ -1367,7 +1362,6 @@ def dehumanize(self, input_string: str, locale: str = "en_us") -> "Arrow": # Search input string for each time unit within locale for unit, unit_object in locale_obj.timeframes.items(): - # Need to check the type of unit_object to create the correct dictionary if isinstance(unit_object, Mapping): strings_to_search = unit_object @@ -1378,7 +1372,6 @@ def dehumanize(self, input_string: str, locale: str = "en_us") -> "Arrow": # Needs to cycle all through strings as some locales have strings that # could overlap in a regex match, since input validation isn't being performed. for time_delta, time_string in strings_to_search.items(): - # Replace {0} with regex \d representing digits search_string = str(time_string) search_string = search_string.format(r"\d+") @@ -1718,7 +1711,6 @@ def for_json(self) -> str: # math def __add__(self, other: Any) -> "Arrow": - if isinstance(other, (timedelta, relativedelta)): return self.fromdatetime(self._datetime + other, self._datetime.tzinfo) @@ -1736,7 +1728,6 @@ def __sub__(self, other: Union[dt_datetime, "Arrow"]) -> timedelta: pass # pragma: no cover def __sub__(self, other: Any) -> Union[timedelta, "Arrow"]: - if isinstance(other, (timedelta, relativedelta)): return self.fromdatetime(self._datetime - other, self._datetime.tzinfo) @@ -1749,7 +1740,6 @@ def __sub__(self, other: Any) -> Union[timedelta, "Arrow"]: return NotImplemented def __rsub__(self, other: Any) -> timedelta: - if isinstance(other, dt_datetime): return other - self._datetime @@ -1758,42 +1748,36 @@ def __rsub__(self, other: Any) -> timedelta: # comparisons def __eq__(self, other: Any) -> bool: - if not isinstance(other, (Arrow, dt_datetime)): return False return self._datetime == self._get_datetime(other) def __ne__(self, other: Any) -> bool: - if not isinstance(other, (Arrow, dt_datetime)): return True return not self.__eq__(other) def __gt__(self, other: Any) -> bool: - if not isinstance(other, (Arrow, dt_datetime)): return NotImplemented return self._datetime > self._get_datetime(other) def __ge__(self, other: Any) -> bool: - if not isinstance(other, (Arrow, dt_datetime)): return NotImplemented return self._datetime >= self._get_datetime(other) def __lt__(self, other: Any) -> bool: - if not isinstance(other, (Arrow, dt_datetime)): return NotImplemented return self._datetime < self._get_datetime(other) def __le__(self, other: Any) -> bool: - if not isinstance(other, (Arrow, dt_datetime)): return NotImplemented @@ -1865,7 +1849,6 @@ def _get_frames(cls, name: _T_FRAMES) -> Tuple[str, str, int]: def _get_iteration_params(cls, end: Any, limit: Optional[int]) -> Tuple[Any, int]: """Sets default end and limit values for range method.""" if end is None: - if limit is None: raise ValueError("One of 'end' or 'limit' is required.") diff --git a/arrow/factory.py b/arrow/factory.py index aad4af8b..f35085f1 100644 --- a/arrow/factory.py +++ b/arrow/factory.py @@ -267,11 +267,9 @@ def get(self, *args: Any, **kwargs: Any) -> Arrow: raise TypeError(f"Cannot parse single argument of type {type(arg)!r}.") elif arg_count == 2: - arg_1, arg_2 = args[0], args[1] if isinstance(arg_1, datetime): - # (datetime, tzinfo/str) -> fromdatetime @ tzinfo if isinstance(arg_2, (dt_tzinfo, str)): return self.type.fromdatetime(arg_1, tzinfo=arg_2) @@ -281,7 +279,6 @@ def get(self, *args: Any, **kwargs: Any) -> Arrow: ) elif isinstance(arg_1, date): - # (date, tzinfo/str) -> fromdate @ tzinfo if isinstance(arg_2, (dt_tzinfo, str)): return self.type.fromdate(arg_1, tzinfo=arg_2) diff --git a/arrow/formatter.py b/arrow/formatter.py index 728bea1a..d45f7153 100644 --- a/arrow/formatter.py +++ b/arrow/formatter.py @@ -29,7 +29,6 @@ class DateTimeFormatter: - # This pattern matches characters enclosed in square brackets are matched as # an atomic group. For more info on atomic groups and how to they are # emulated in Python's re library, see https://stackoverflow.com/a/13577411/2701578 @@ -41,18 +40,15 @@ class DateTimeFormatter: locale: locales.Locale def __init__(self, locale: str = DEFAULT_LOCALE) -> None: - self.locale = locales.get_locale(locale) def format(cls, dt: datetime, fmt: str) -> str: - # FIXME: _format_token() is nullable return cls._FORMAT_RE.sub( lambda m: cast(str, cls._format_token(dt, m.group(0))), fmt ) def _format_token(self, dt: datetime, token: Optional[str]) -> Optional[str]: - if token and token.startswith("[") and token.endswith("]"): return token[1:-1] diff --git a/arrow/locales.py b/arrow/locales.py index ea4c84b2..b2e7ee03 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -129,7 +129,6 @@ def __init_subclass__(cls, **kwargs: Any) -> None: _locale_map[locale_name.lower().replace("_", "-")] = cls def __init__(self) -> None: - self._month_name_to_ordinal = None def describe( @@ -174,7 +173,7 @@ def describe_multi( # Needed to determine the correct relative string to use timeframe_value = 0 - for _unit_name, unit_value in timeframes: + for _, unit_value in timeframes: if trunc(unit_value) != 0: timeframe_value = trunc(unit_value) break @@ -285,7 +284,6 @@ def _format_relative( timeframe: TimeFrameLiteral, delta: Union[float, int], ) -> str: - if timeframe == "now": return humanized @@ -1887,7 +1885,7 @@ class GermanBaseLocale(Locale): future = "in {0}" and_word = "und" - timeframes = { + timeframes: ClassVar[Dict[TimeFrameLiteral, str]] = { "now": "gerade eben", "second": "einer Sekunde", "seconds": "{0} Sekunden", @@ -1982,7 +1980,9 @@ def describe( return super().describe(timeframe, delta, only_distance) # German uses a different case without 'in' or 'ago' - humanized = self.timeframes_only_distance[timeframe].format(trunc(abs(delta))) + humanized: str = self.timeframes_only_distance[timeframe].format( + trunc(abs(delta)) + ) return humanized @@ -3936,7 +3936,6 @@ def _format_relative( class LaotianLocale(Locale): - names = ["lo", "lo-la"] past = "{0} ກ່ອນຫນ້ານີ້" @@ -5404,7 +5403,7 @@ class LuxembourgishLocale(Locale): future = "an {0}" and_word = "an" - timeframes = { + timeframes: ClassVar[Dict[TimeFrameLiteral, str]] = { "now": "just elo", "second": "enger Sekonn", "seconds": "{0} Sekonnen", @@ -5492,7 +5491,9 @@ def describe( return super().describe(timeframe, delta, only_distance) # Luxembourgish uses a different case without 'in' or 'ago' - humanized = self.timeframes_only_distance[timeframe].format(trunc(abs(delta))) + humanized: str = self.timeframes_only_distance[timeframe].format( + trunc(abs(delta)) + ) return humanized diff --git a/arrow/parser.py b/arrow/parser.py index ee31a5bc..645e3da7 100644 --- a/arrow/parser.py +++ b/arrow/parser.py @@ -159,7 +159,6 @@ class DateTimeParser: _input_re_map: Dict[_FORMAT_TYPE, Pattern[str]] def __init__(self, locale: str = DEFAULT_LOCALE, cache_size: int = 0) -> None: - self.locale = locales.get_locale(locale) self._input_re_map = self._BASE_INPUT_RE_MAP.copy() self._input_re_map.update( @@ -187,7 +186,7 @@ def __init__(self, locale: str = DEFAULT_LOCALE, cache_size: int = 0) -> None: } ) if cache_size > 0: - self._generate_pattern_re = lru_cache(maxsize=cache_size)( # type: ignore[assignment] + self._generate_pattern_re = lru_cache(maxsize=cache_size)( # type: ignore self._generate_pattern_re ) @@ -196,7 +195,6 @@ def __init__(self, locale: str = DEFAULT_LOCALE, cache_size: int = 0) -> None: def parse_iso( self, datetime_string: str, normalize_whitespace: bool = False ) -> datetime: - if normalize_whitespace: datetime_string = re.sub(r"\s+", " ", datetime_string.strip()) @@ -236,13 +234,14 @@ def parse_iso( ] if has_time: - if has_space_divider: date_string, time_string = datetime_string.split(" ", 1) else: date_string, time_string = datetime_string.split("T", 1) - time_parts = re.split(r"[\+\-Z]", time_string, 1, re.IGNORECASE) + time_parts = re.split( + r"[\+\-Z]", time_string, maxsplit=1, flags=re.IGNORECASE + ) time_components: Optional[Match[str]] = self._TIME_RE.match(time_parts[0]) @@ -303,7 +302,6 @@ def parse( fmt: Union[List[str], str], normalize_whitespace: bool = False, ) -> datetime: - if normalize_whitespace: datetime_string = re.sub(r"\s+", " ", datetime_string) @@ -346,7 +344,6 @@ def parse( return self._build_datetime(parts) def _generate_pattern_re(self, fmt: str) -> Tuple[List[_FORMAT_TYPE], Pattern[str]]: - # fmt is a string of tokens like 'YYYY-MM-DD' # we construct a new string by replacing each # token by its pattern: @@ -498,7 +495,6 @@ def _parse_token( value: Any, parts: _Parts, ) -> None: - if token == "YYYY": parts["year"] = int(value) @@ -588,7 +584,6 @@ def _build_datetime(parts: _Parts) -> datetime: weekdate = parts.get("weekdate") if weekdate is not None: - year, week = int(weekdate[0]), int(weekdate[1]) if weekdate[2] is not None: @@ -712,7 +707,6 @@ def _build_datetime(parts: _Parts) -> datetime: ) def _parse_multiformat(self, string: str, formats: Iterable[str]) -> datetime: - _datetime: Optional[datetime] = None for fmt in formats: @@ -745,7 +739,6 @@ class TzinfoParser: @classmethod def parse(cls, tzinfo_string: str) -> dt_tzinfo: - tzinfo: Optional[dt_tzinfo] = None if tzinfo_string == "local": @@ -755,7 +748,6 @@ def parse(cls, tzinfo_string: str) -> dt_tzinfo: tzinfo = tz.tzutc() else: - iso_match = cls._TZINFO_RE.match(tzinfo_string) if iso_match: diff --git a/docs/conf.py b/docs/conf.py index dee71470..aa6fb444 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -13,7 +13,7 @@ # -- Project information ----------------------------------------------------- project = "Arrow 🏹" -copyright = "2021, Chris Smith" +copyright = "2023, Chris Smith" author = "Chris Smith" release = about["__version__"] diff --git a/requirements/requirements-docs.txt b/requirements/requirements-docs.txt index abc47b28..35ca4ded 100644 --- a/requirements/requirements-docs.txt +++ b/requirements/requirements-docs.txt @@ -1,6 +1,6 @@ -r requirements.txt doc8 -sphinx +sphinx>=7.0.0 sphinx-autobuild sphinx-autodoc-typehints -sphinx_rtd_theme +sphinx_rtd_theme>=1.3.0 diff --git a/requirements/requirements.txt b/requirements/requirements.txt index bcdff0e8..65134a19 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -1,2 +1 @@ python-dateutil>=2.7.0 -typing_extensions; python_version < '3.8' diff --git a/setup.py b/setup.py index 52563cf9..a2d8921e 100644 --- a/setup.py +++ b/setup.py @@ -21,11 +21,8 @@ packages=["arrow"], package_data={"arrow": ["py.typed"]}, zip_safe=False, - python_requires=">=3.6", - install_requires=[ - "python-dateutil>=2.7.0", - "typing_extensions; python_version<'3.8'", - ], + python_requires=">=3.8", + install_requires=["python-dateutil>=2.7.0"], classifiers=[ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", @@ -33,12 +30,11 @@ "Topic :: Software Development :: Libraries :: Python Modules", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", ], keywords="arrow date time datetime timestamp timezone humanize", project_urls={ diff --git a/tests/test_arrow.py b/tests/test_arrow.py index 5cd12c82..507c1ab0 100644 --- a/tests/test_arrow.py +++ b/tests/test_arrow.py @@ -18,7 +18,6 @@ class TestTestArrowInit: def test_init_bad_input(self): - with pytest.raises(TypeError): arrow.Arrow(2013) @@ -29,7 +28,6 @@ def test_init_bad_input(self): arrow.Arrow(2013, 2, 2, 12, 30, 45, 9999999) def test_init(self): - result = arrow.Arrow(2013, 2, 2) self.expected = datetime(2013, 2, 2, tzinfo=tz.tzutc()) assert result._datetime == self.expected @@ -60,7 +58,6 @@ def test_init(self): # regression tests for issue #626 def test_init_pytz_timezone(self): - result = arrow.Arrow( 2013, 2, 2, 12, 30, 45, 999999, tzinfo=pytz.timezone("Europe/Paris") ) @@ -84,7 +81,6 @@ def test_init_with_fold(self): class TestTestArrowFactory: def test_now(self): - result = arrow.Arrow.now() assert_datetime_equality( @@ -92,7 +88,6 @@ def test_now(self): ) def test_utcnow(self): - result = arrow.Arrow.utcnow() assert_datetime_equality( @@ -102,7 +97,6 @@ def test_utcnow(self): assert result.fold == 0 def test_fromtimestamp(self): - timestamp = time.time() result = arrow.Arrow.fromtimestamp(timestamp) @@ -126,7 +120,6 @@ def test_fromtimestamp(self): arrow.Arrow.fromtimestamp("invalid timestamp") def test_utcfromtimestamp(self): - timestamp = time.time() result = arrow.Arrow.utcfromtimestamp(timestamp) @@ -138,7 +131,6 @@ def test_utcfromtimestamp(self): arrow.Arrow.utcfromtimestamp("invalid timestamp") def test_fromdatetime(self): - dt = datetime(2013, 2, 3, 12, 30, 45, 1) result = arrow.Arrow.fromdatetime(dt) @@ -146,7 +138,6 @@ def test_fromdatetime(self): assert result._datetime == dt.replace(tzinfo=tz.tzutc()) def test_fromdatetime_dt_tzinfo(self): - dt = datetime(2013, 2, 3, 12, 30, 45, 1, tzinfo=tz.gettz("US/Pacific")) result = arrow.Arrow.fromdatetime(dt) @@ -154,7 +145,6 @@ def test_fromdatetime_dt_tzinfo(self): assert result._datetime == dt.replace(tzinfo=tz.gettz("US/Pacific")) def test_fromdatetime_tzinfo_arg(self): - dt = datetime(2013, 2, 3, 12, 30, 45, 1) result = arrow.Arrow.fromdatetime(dt, tz.gettz("US/Pacific")) @@ -162,7 +152,6 @@ def test_fromdatetime_tzinfo_arg(self): assert result._datetime == dt.replace(tzinfo=tz.gettz("US/Pacific")) def test_fromdate(self): - dt = date(2013, 2, 3) result = arrow.Arrow.fromdate(dt, tz.gettz("US/Pacific")) @@ -170,7 +159,6 @@ def test_fromdate(self): assert result._datetime == datetime(2013, 2, 3, tzinfo=tz.gettz("US/Pacific")) def test_strptime(self): - formatted = datetime(2013, 2, 3, 12, 30, 45).strftime("%Y-%m-%d %H:%M:%S") result = arrow.Arrow.strptime(formatted, "%Y-%m-%d %H:%M:%S") @@ -184,7 +172,6 @@ def test_strptime(self): ) def test_fromordinal(self): - timestamp = 1607066909.937968 with pytest.raises(TypeError): arrow.Arrow.fromordinal(timestamp) @@ -205,43 +192,36 @@ def test_fromordinal(self): @pytest.mark.usefixtures("time_2013_02_03") class TestTestArrowRepresentation: def test_repr(self): - result = self.arrow.__repr__() assert result == f"" def test_str(self): - result = self.arrow.__str__() assert result == self.arrow._datetime.isoformat() def test_hash(self): - result = self.arrow.__hash__() assert result == self.arrow._datetime.__hash__() def test_format(self): - result = f"{self.arrow:YYYY-MM-DD}" assert result == "2013-02-03" def test_bare_format(self): - result = self.arrow.format() assert result == "2013-02-03 12:30:45+00:00" def test_format_no_format_string(self): - result = f"{self.arrow}" assert result == str(self.arrow) def test_clone(self): - result = self.arrow.clone() assert result is not self.arrow @@ -251,12 +231,10 @@ def test_clone(self): @pytest.mark.usefixtures("time_2013_01_01") class TestArrowAttribute: def test_getattr_base(self): - with pytest.raises(AttributeError): self.arrow.prop def test_getattr_week(self): - assert self.arrow.week == 1 def test_getattr_quarter(self): @@ -281,31 +259,24 @@ def test_getattr_quarter(self): assert q4.quarter == 4 def test_getattr_dt_value(self): - assert self.arrow.year == 2013 def test_tzinfo(self): - assert self.arrow.tzinfo == tz.tzutc() def test_naive(self): - assert self.arrow.naive == self.arrow._datetime.replace(tzinfo=None) def test_timestamp(self): - assert self.arrow.timestamp() == self.arrow._datetime.timestamp() def test_int_timestamp(self): - assert self.arrow.int_timestamp == int(self.arrow._datetime.timestamp()) def test_float_timestamp(self): - assert self.arrow.float_timestamp == self.arrow._datetime.timestamp() def test_getattr_fold(self): - # UTC is always unambiguous assert self.now.fold == 0 @@ -318,7 +289,6 @@ def test_getattr_fold(self): ambiguous_dt.fold = 0 def test_getattr_ambiguous(self): - assert not self.now.ambiguous ambiguous_dt = arrow.Arrow(2017, 10, 29, 2, 0, tzinfo="Europe/Stockholm") @@ -326,7 +296,6 @@ def test_getattr_ambiguous(self): assert ambiguous_dt.ambiguous def test_getattr_imaginary(self): - assert not self.now.imaginary imaginary_dt = arrow.Arrow(2013, 3, 31, 2, 30, tzinfo="Europe/Paris") @@ -337,19 +306,16 @@ def test_getattr_imaginary(self): @pytest.mark.usefixtures("time_utcnow") class TestArrowComparison: def test_eq(self): - assert self.arrow == self.arrow assert self.arrow == self.arrow.datetime assert not (self.arrow == "abc") def test_ne(self): - assert not (self.arrow != self.arrow) assert not (self.arrow != self.arrow.datetime) assert self.arrow != "abc" def test_gt(self): - arrow_cmp = self.arrow.shift(minutes=1) assert not (self.arrow > self.arrow) @@ -362,7 +328,6 @@ def test_gt(self): assert self.arrow < arrow_cmp.datetime def test_ge(self): - with pytest.raises(TypeError): self.arrow >= "abc" # noqa: B015 @@ -370,7 +335,6 @@ def test_ge(self): assert self.arrow >= self.arrow.datetime def test_lt(self): - arrow_cmp = self.arrow.shift(minutes=1) assert not (self.arrow < self.arrow) @@ -383,7 +347,6 @@ def test_lt(self): assert self.arrow < arrow_cmp.datetime def test_le(self): - with pytest.raises(TypeError): self.arrow <= "abc" # noqa: B015 @@ -394,53 +357,44 @@ def test_le(self): @pytest.mark.usefixtures("time_2013_01_01") class TestArrowMath: def test_add_timedelta(self): - result = self.arrow.__add__(timedelta(days=1)) assert result._datetime == datetime(2013, 1, 2, tzinfo=tz.tzutc()) def test_add_other(self): - with pytest.raises(TypeError): self.arrow + 1 def test_radd(self): - result = self.arrow.__radd__(timedelta(days=1)) assert result._datetime == datetime(2013, 1, 2, tzinfo=tz.tzutc()) def test_sub_timedelta(self): - result = self.arrow.__sub__(timedelta(days=1)) assert result._datetime == datetime(2012, 12, 31, tzinfo=tz.tzutc()) def test_sub_datetime(self): - result = self.arrow.__sub__(datetime(2012, 12, 21, tzinfo=tz.tzutc())) assert result == timedelta(days=11) def test_sub_arrow(self): - result = self.arrow.__sub__(arrow.Arrow(2012, 12, 21, tzinfo=tz.tzutc())) assert result == timedelta(days=11) def test_sub_other(self): - with pytest.raises(TypeError): self.arrow - object() def test_rsub_datetime(self): - result = self.arrow.__rsub__(datetime(2012, 12, 21, tzinfo=tz.tzutc())) assert result == timedelta(days=-11) def test_rsub_other(self): - with pytest.raises(TypeError): timedelta(days=1) - self.arrow @@ -448,25 +402,21 @@ def test_rsub_other(self): @pytest.mark.usefixtures("time_utcnow") class TestArrowDatetimeInterface: def test_date(self): - result = self.arrow.date() assert result == self.arrow._datetime.date() def test_time(self): - result = self.arrow.time() assert result == self.arrow._datetime.time() def test_timetz(self): - result = self.arrow.timetz() assert result == self.arrow._datetime.timetz() def test_astimezone(self): - other_tz = tz.gettz("US/Pacific") result = self.arrow.astimezone(other_tz) @@ -474,61 +424,51 @@ def test_astimezone(self): assert result == self.arrow._datetime.astimezone(other_tz) def test_utcoffset(self): - result = self.arrow.utcoffset() assert result == self.arrow._datetime.utcoffset() def test_dst(self): - result = self.arrow.dst() assert result == self.arrow._datetime.dst() def test_timetuple(self): - result = self.arrow.timetuple() assert result == self.arrow._datetime.timetuple() def test_utctimetuple(self): - result = self.arrow.utctimetuple() assert result == self.arrow._datetime.utctimetuple() def test_toordinal(self): - result = self.arrow.toordinal() assert result == self.arrow._datetime.toordinal() def test_weekday(self): - result = self.arrow.weekday() assert result == self.arrow._datetime.weekday() def test_isoweekday(self): - result = self.arrow.isoweekday() assert result == self.arrow._datetime.isoweekday() def test_isocalendar(self): - result = self.arrow.isocalendar() assert result == self.arrow._datetime.isocalendar() def test_isoformat(self): - result = self.arrow.isoformat() assert result == self.arrow._datetime.isoformat() def test_isoformat_timespec(self): - result = self.arrow.isoformat(timespec="hours") assert result == self.arrow._datetime.isoformat(timespec="hours") @@ -542,19 +482,16 @@ def test_isoformat_timespec(self): assert result == self.arrow._datetime.isoformat(sep="x", timespec="seconds") def test_simplejson(self): - result = json.dumps({"v": self.arrow.for_json()}, for_json=True) assert json.loads(result)["v"] == self.arrow._datetime.isoformat() def test_ctime(self): - result = self.arrow.ctime() assert result == self.arrow._datetime.ctime() def test_strftime(self): - result = self.arrow.strftime("%Y") assert result == self.arrow._datetime.strftime("%Y") @@ -609,7 +546,6 @@ def test_dst(self): class TestArrowConversion: def test_to(self): - dt_from = datetime.now() arrow_from = arrow.Arrow.fromdatetime(dt_from, tz.gettz("US/Pacific")) @@ -632,7 +568,6 @@ def test_to_amsterdam_then_utc(self): # regression test for #690 def test_to_israel_same_offset(self): - result = arrow.Arrow(2019, 10, 27, 2, 21, 1, tzinfo="+03:00").to("Israel") expected = arrow.Arrow(2019, 10, 27, 1, 21, 1, tzinfo="Israel") @@ -648,7 +583,6 @@ def test_anchorage_dst(self): # issue 476 def test_chicago_fall(self): - result = arrow.Arrow(2017, 11, 5, 2, 1, tzinfo="-05:00").to("America/Chicago") expected = arrow.Arrow(2017, 11, 5, 1, 1, tzinfo="America/Chicago") @@ -656,7 +590,6 @@ def test_chicago_fall(self): assert result.utcoffset() != expected.utcoffset() def test_toronto_gap(self): - before = arrow.Arrow(2011, 3, 13, 6, 30, tzinfo="UTC").to("America/Toronto") after = arrow.Arrow(2011, 3, 13, 7, 30, tzinfo="UTC").to("America/Toronto") @@ -666,7 +599,6 @@ def test_toronto_gap(self): assert before.utcoffset() != after.utcoffset() def test_sydney_gap(self): - before = arrow.Arrow(2012, 10, 6, 15, 30, tzinfo="UTC").to("Australia/Sydney") after = arrow.Arrow(2012, 10, 6, 16, 30, tzinfo="UTC").to("Australia/Sydney") @@ -678,7 +610,6 @@ def test_sydney_gap(self): class TestArrowPickling: def test_pickle_and_unpickle(self): - dt = arrow.Arrow.utcnow() pickled = pickle.dumps(dt) @@ -690,12 +621,10 @@ def test_pickle_and_unpickle(self): class TestArrowReplace: def test_not_attr(self): - with pytest.raises(ValueError): arrow.Arrow.utcnow().replace(abc=1) def test_replace(self): - arw = arrow.Arrow(2013, 5, 5, 12, 30, 45) assert arw.replace(year=2012) == arrow.Arrow(2012, 5, 5, 12, 30, 45) @@ -706,7 +635,6 @@ def test_replace(self): assert arw.replace(second=1) == arrow.Arrow(2013, 5, 5, 12, 30, 1) def test_replace_tzinfo(self): - arw = arrow.Arrow.utcnow().to("US/Eastern") result = arw.replace(tzinfo=tz.gettz("US/Pacific")) @@ -714,7 +642,6 @@ def test_replace_tzinfo(self): assert result == arw.datetime.replace(tzinfo=tz.gettz("US/Pacific")) def test_replace_fold(self): - before = arrow.Arrow(2017, 11, 5, 1, tzinfo="America/New_York") after = before.replace(fold=1) @@ -724,19 +651,16 @@ def test_replace_fold(self): assert before.utcoffset() != after.utcoffset() def test_replace_fold_and_other(self): - arw = arrow.Arrow(2013, 5, 5, 12, 30, 45) assert arw.replace(fold=1, minute=50) == arrow.Arrow(2013, 5, 5, 12, 50, 45) assert arw.replace(minute=50, fold=1) == arrow.Arrow(2013, 5, 5, 12, 50, 45) def test_replace_week(self): - with pytest.raises(ValueError): arrow.Arrow.utcnow().replace(week=1) def test_replace_quarter(self): - with pytest.raises(ValueError): arrow.Arrow.utcnow().replace(quarter=1) @@ -748,14 +672,12 @@ def test_replace_quarter_and_fold(self): arrow.utcnow().replace(quarter=1, fold=1) def test_replace_other_kwargs(self): - with pytest.raises(AttributeError): arrow.utcnow().replace(abc="def") class TestArrowShift: def test_not_attr(self): - now = arrow.Arrow.utcnow() with pytest.raises(ValueError): @@ -765,7 +687,6 @@ def test_not_attr(self): now.shift(week=1) def test_shift(self): - arw = arrow.Arrow(2013, 5, 5, 12, 30, 45) assert arw.shift(years=1) == arrow.Arrow(2014, 5, 5, 12, 30, 45) @@ -822,7 +743,6 @@ def test_shift(self): assert arw.shift(weekday=SU(2)) == arrow.Arrow(2013, 5, 12, 12, 30, 45) def test_shift_negative(self): - arw = arrow.Arrow(2013, 5, 5, 12, 30, 45) assert arw.shift(years=-1) == arrow.Arrow(2012, 5, 5, 12, 30, 45) @@ -858,7 +778,6 @@ def test_shift_negative(self): assert arw.shift(weekday=SU(-2)) == arrow.Arrow(2013, 4, 28, 12, 30, 45) def test_shift_quarters_bug(self): - arw = arrow.Arrow(2013, 5, 5, 12, 30, 45) # The value of the last-read argument was used instead of the ``quarters`` argument. @@ -876,7 +795,6 @@ def test_shift_quarters_bug(self): ) def test_shift_positive_imaginary(self): - # Avoid shifting into imaginary datetimes, take into account DST and other timezone changes. new_york = arrow.Arrow(2017, 3, 12, 1, 30, tzinfo="America/New_York") @@ -907,7 +825,6 @@ def test_shift_positive_imaginary(self): ) def test_shift_negative_imaginary(self): - new_york = arrow.Arrow(2011, 3, 13, 3, 30, tzinfo="America/New_York") assert new_york.shift(hours=-1) == arrow.Arrow( 2011, 3, 13, 3, 30, tzinfo="America/New_York" @@ -951,7 +868,6 @@ def shift_imaginary_seconds(self): class TestArrowRange: def test_year(self): - result = list( arrow.Arrow.range( "year", datetime(2013, 1, 2, 3, 4, 5), datetime(2016, 4, 5, 6, 7, 8) @@ -966,7 +882,6 @@ def test_year(self): ] def test_quarter(self): - result = list( arrow.Arrow.range( "quarter", datetime(2013, 2, 3, 4, 5, 6), datetime(2013, 5, 6, 7, 8, 9) @@ -979,7 +894,6 @@ def test_quarter(self): ] def test_month(self): - result = list( arrow.Arrow.range( "month", datetime(2013, 2, 3, 4, 5, 6), datetime(2013, 5, 6, 7, 8, 9) @@ -994,7 +908,6 @@ def test_month(self): ] def test_week(self): - result = list( arrow.Arrow.range( "week", datetime(2013, 9, 1, 2, 3, 4), datetime(2013, 10, 1, 2, 3, 4) @@ -1010,7 +923,6 @@ def test_week(self): ] def test_day(self): - result = list( arrow.Arrow.range( "day", datetime(2013, 1, 2, 3, 4, 5), datetime(2013, 1, 5, 6, 7, 8) @@ -1025,7 +937,6 @@ def test_day(self): ] def test_hour(self): - result = list( arrow.Arrow.range( "hour", datetime(2013, 1, 2, 3, 4, 5), datetime(2013, 1, 2, 6, 7, 8) @@ -1048,7 +959,6 @@ def test_hour(self): assert result == [arrow.Arrow(2013, 1, 2, 3, 4, 5)] def test_minute(self): - result = list( arrow.Arrow.range( "minute", datetime(2013, 1, 2, 3, 4, 5), datetime(2013, 1, 2, 3, 7, 8) @@ -1063,7 +973,6 @@ def test_minute(self): ] def test_second(self): - result = list( arrow.Arrow.range( "second", datetime(2013, 1, 2, 3, 4, 5), datetime(2013, 1, 2, 3, 4, 8) @@ -1078,7 +987,6 @@ def test_second(self): ] def test_arrow(self): - result = list( arrow.Arrow.range( "day", @@ -1095,7 +1003,6 @@ def test_arrow(self): ] def test_naive_tz(self): - result = arrow.Arrow.range( "year", datetime(2013, 1, 2, 3), datetime(2016, 4, 5, 6), "US/Pacific" ) @@ -1104,7 +1011,6 @@ def test_naive_tz(self): assert r.tzinfo == tz.gettz("US/Pacific") def test_aware_same_tz(self): - result = arrow.Arrow.range( "day", arrow.Arrow(2013, 1, 1, tzinfo=tz.gettz("US/Pacific")), @@ -1115,7 +1021,6 @@ def test_aware_same_tz(self): assert r.tzinfo == tz.gettz("US/Pacific") def test_aware_different_tz(self): - result = arrow.Arrow.range( "day", datetime(2013, 1, 1, tzinfo=tz.gettz("US/Eastern")), @@ -1126,7 +1031,6 @@ def test_aware_different_tz(self): assert r.tzinfo == tz.gettz("US/Eastern") def test_aware_tz(self): - result = arrow.Arrow.range( "day", datetime(2013, 1, 1, tzinfo=tz.gettz("US/Eastern")), @@ -1150,7 +1054,6 @@ def test_imaginary(self): assert len(utc_range) == len(set(utc_range)) def test_unsupported(self): - with pytest.raises(ValueError): next(arrow.Arrow.range("abc", datetime.utcnow(), datetime.utcnow())) @@ -1206,7 +1109,6 @@ def test_range_over_year_maintains_end_date_across_leap_year(self): class TestArrowSpanRange: def test_year(self): - result = list( arrow.Arrow.span_range("year", datetime(2013, 2, 1), datetime(2016, 3, 31)) ) @@ -1231,7 +1133,6 @@ def test_year(self): ] def test_quarter(self): - result = list( arrow.Arrow.span_range( "quarter", datetime(2013, 2, 2), datetime(2013, 5, 15) @@ -1244,7 +1145,6 @@ def test_quarter(self): ] def test_month(self): - result = list( arrow.Arrow.span_range("month", datetime(2013, 1, 2), datetime(2013, 4, 15)) ) @@ -1257,7 +1157,6 @@ def test_month(self): ] def test_week(self): - result = list( arrow.Arrow.span_range("week", datetime(2013, 2, 2), datetime(2013, 2, 28)) ) @@ -1277,7 +1176,6 @@ def test_week(self): ] def test_day(self): - result = list( arrow.Arrow.span_range( "day", datetime(2013, 1, 1, 12), datetime(2013, 1, 4, 12) @@ -1304,7 +1202,6 @@ def test_day(self): ] def test_days(self): - result = list( arrow.Arrow.span_range( "days", datetime(2013, 1, 1, 12), datetime(2013, 1, 4, 12) @@ -1331,7 +1228,6 @@ def test_days(self): ] def test_hour(self): - result = list( arrow.Arrow.span_range( "hour", datetime(2013, 1, 1, 0, 30), datetime(2013, 1, 1, 3, 30) @@ -1368,7 +1264,6 @@ def test_hour(self): ] def test_minute(self): - result = list( arrow.Arrow.span_range( "minute", datetime(2013, 1, 1, 0, 0, 30), datetime(2013, 1, 1, 0, 3, 30) @@ -1395,7 +1290,6 @@ def test_minute(self): ] def test_second(self): - result = list( arrow.Arrow.span_range( "second", datetime(2013, 1, 1), datetime(2013, 1, 1, 0, 0, 3) @@ -1422,7 +1316,6 @@ def test_second(self): ] def test_naive_tz(self): - tzinfo = tz.gettz("US/Pacific") result = arrow.Arrow.span_range( @@ -1434,7 +1327,6 @@ def test_naive_tz(self): assert c.tzinfo == tzinfo def test_aware_same_tz(self): - tzinfo = tz.gettz("US/Pacific") result = arrow.Arrow.span_range( @@ -1448,7 +1340,6 @@ def test_aware_same_tz(self): assert c.tzinfo == tzinfo def test_aware_different_tz(self): - tzinfo1 = tz.gettz("US/Pacific") tzinfo2 = tz.gettz("US/Eastern") @@ -1463,7 +1354,6 @@ def test_aware_different_tz(self): assert c.tzinfo == tzinfo1 def test_aware_tz(self): - result = arrow.Arrow.span_range( "hour", datetime(2013, 1, 1, 0, tzinfo=tz.gettz("US/Eastern")), @@ -1476,7 +1366,6 @@ def test_aware_tz(self): assert c.tzinfo == tz.gettz("US/Central") def test_bounds_param_is_passed(self): - result = list( arrow.Arrow.span_range( "quarter", datetime(2013, 2, 2), datetime(2013, 5, 15), bounds="[]" @@ -1489,7 +1378,6 @@ def test_bounds_param_is_passed(self): ] def test_exact_bound_exclude(self): - result = list( arrow.Arrow.span_range( "hour", @@ -1709,40 +1597,34 @@ def test_exact(self): @pytest.mark.usefixtures("time_2013_02_15") class TestArrowSpan: def test_span_attribute(self): - with pytest.raises(ValueError): self.arrow.span("span") def test_span_year(self): - floor, ceil = self.arrow.span("year") assert floor == datetime(2013, 1, 1, tzinfo=tz.tzutc()) assert ceil == datetime(2013, 12, 31, 23, 59, 59, 999999, tzinfo=tz.tzutc()) def test_span_quarter(self): - floor, ceil = self.arrow.span("quarter") assert floor == datetime(2013, 1, 1, tzinfo=tz.tzutc()) assert ceil == datetime(2013, 3, 31, 23, 59, 59, 999999, tzinfo=tz.tzutc()) def test_span_quarter_count(self): - floor, ceil = self.arrow.span("quarter", 2) assert floor == datetime(2013, 1, 1, tzinfo=tz.tzutc()) assert ceil == datetime(2013, 6, 30, 23, 59, 59, 999999, tzinfo=tz.tzutc()) def test_span_year_count(self): - floor, ceil = self.arrow.span("year", 2) assert floor == datetime(2013, 1, 1, tzinfo=tz.tzutc()) assert ceil == datetime(2014, 12, 31, 23, 59, 59, 999999, tzinfo=tz.tzutc()) def test_span_month(self): - floor, ceil = self.arrow.span("month") assert floor == datetime(2013, 2, 1, tzinfo=tz.tzutc()) @@ -1775,75 +1657,64 @@ def test_span_week(self): assert ceil == datetime(2013, 2, 16, 23, 59, 59, 999999, tzinfo=tz.tzutc()) def test_span_day(self): - floor, ceil = self.arrow.span("day") assert floor == datetime(2013, 2, 15, tzinfo=tz.tzutc()) assert ceil == datetime(2013, 2, 15, 23, 59, 59, 999999, tzinfo=tz.tzutc()) def test_span_hour(self): - floor, ceil = self.arrow.span("hour") assert floor == datetime(2013, 2, 15, 3, tzinfo=tz.tzutc()) assert ceil == datetime(2013, 2, 15, 3, 59, 59, 999999, tzinfo=tz.tzutc()) def test_span_minute(self): - floor, ceil = self.arrow.span("minute") assert floor == datetime(2013, 2, 15, 3, 41, tzinfo=tz.tzutc()) assert ceil == datetime(2013, 2, 15, 3, 41, 59, 999999, tzinfo=tz.tzutc()) def test_span_second(self): - floor, ceil = self.arrow.span("second") assert floor == datetime(2013, 2, 15, 3, 41, 22, tzinfo=tz.tzutc()) assert ceil == datetime(2013, 2, 15, 3, 41, 22, 999999, tzinfo=tz.tzutc()) def test_span_microsecond(self): - floor, ceil = self.arrow.span("microsecond") assert floor == datetime(2013, 2, 15, 3, 41, 22, 8923, tzinfo=tz.tzutc()) assert ceil == datetime(2013, 2, 15, 3, 41, 22, 8923, tzinfo=tz.tzutc()) def test_floor(self): - floor, ceil = self.arrow.span("month") assert floor == self.arrow.floor("month") assert ceil == self.arrow.ceil("month") def test_span_inclusive_inclusive(self): - floor, ceil = self.arrow.span("hour", bounds="[]") assert floor == datetime(2013, 2, 15, 3, tzinfo=tz.tzutc()) assert ceil == datetime(2013, 2, 15, 4, tzinfo=tz.tzutc()) def test_span_exclusive_inclusive(self): - floor, ceil = self.arrow.span("hour", bounds="(]") assert floor == datetime(2013, 2, 15, 3, 0, 0, 1, tzinfo=tz.tzutc()) assert ceil == datetime(2013, 2, 15, 4, tzinfo=tz.tzutc()) def test_span_exclusive_exclusive(self): - floor, ceil = self.arrow.span("hour", bounds="()") assert floor == datetime(2013, 2, 15, 3, 0, 0, 1, tzinfo=tz.tzutc()) assert ceil == datetime(2013, 2, 15, 3, 59, 59, 999999, tzinfo=tz.tzutc()) def test_bounds_are_validated(self): - with pytest.raises(ValueError): floor, ceil = self.arrow.span("hour", bounds="][") def test_exact(self): - result_floor, result_ceil = self.arrow.span("hour", exact=True) expected_floor = datetime(2013, 2, 15, 3, 41, 22, 8923, tzinfo=tz.tzutc()) @@ -1853,28 +1724,24 @@ def test_exact(self): assert result_ceil == expected_ceil def test_exact_inclusive_inclusive(self): - floor, ceil = self.arrow.span("minute", bounds="[]", exact=True) assert floor == datetime(2013, 2, 15, 3, 41, 22, 8923, tzinfo=tz.tzutc()) assert ceil == datetime(2013, 2, 15, 3, 42, 22, 8923, tzinfo=tz.tzutc()) def test_exact_exclusive_inclusive(self): - floor, ceil = self.arrow.span("day", bounds="(]", exact=True) assert floor == datetime(2013, 2, 15, 3, 41, 22, 8924, tzinfo=tz.tzutc()) assert ceil == datetime(2013, 2, 16, 3, 41, 22, 8923, tzinfo=tz.tzutc()) def test_exact_exclusive_exclusive(self): - floor, ceil = self.arrow.span("second", bounds="()", exact=True) assert floor == datetime(2013, 2, 15, 3, 41, 22, 8924, tzinfo=tz.tzutc()) assert ceil == datetime(2013, 2, 15, 3, 41, 23, 8922, tzinfo=tz.tzutc()) def test_all_parameters_specified(self): - floor, ceil = self.arrow.span("week", bounds="()", exact=True, count=2) assert floor == datetime(2013, 2, 15, 3, 41, 22, 8924, tzinfo=tz.tzutc()) @@ -1884,7 +1751,6 @@ def test_all_parameters_specified(self): @pytest.mark.usefixtures("time_2013_01_01") class TestArrowHumanize: def test_granularity(self): - assert self.now.humanize(granularity="second") == "just now" later1 = self.now.shift(seconds=1) @@ -2054,7 +1920,6 @@ def test_multiple_granularity(self): ) def test_seconds(self): - later = self.now.shift(seconds=10) # regression test for issue #727 @@ -2065,7 +1930,6 @@ def test_seconds(self): assert later.humanize(self.now, only_distance=True) == "10 seconds" def test_minute(self): - later = self.now.shift(minutes=1) assert self.now.humanize(later) == "a minute ago" @@ -2075,7 +1939,6 @@ def test_minute(self): assert later.humanize(self.now, only_distance=True) == "a minute" def test_minutes(self): - later = self.now.shift(minutes=2) assert self.now.humanize(later) == "2 minutes ago" @@ -2085,7 +1948,6 @@ def test_minutes(self): assert later.humanize(self.now, only_distance=True) == "2 minutes" def test_hour(self): - later = self.now.shift(hours=1) assert self.now.humanize(later) == "an hour ago" @@ -2095,7 +1957,6 @@ def test_hour(self): assert later.humanize(self.now, only_distance=True) == "an hour" def test_hours(self): - later = self.now.shift(hours=2) assert self.now.humanize(later) == "2 hours ago" @@ -2105,7 +1966,6 @@ def test_hours(self): assert later.humanize(self.now, only_distance=True) == "2 hours" def test_day(self): - later = self.now.shift(days=1) assert self.now.humanize(later) == "a day ago" @@ -2127,7 +1987,6 @@ def test_day(self): assert later.humanize(self.now, only_distance=True) == "a day" def test_days(self): - later = self.now.shift(days=2) assert self.now.humanize(later) == "2 days ago" @@ -2147,7 +2006,6 @@ def test_days(self): assert later.humanize(self.now) == "in 4 days" def test_week(self): - later = self.now.shift(weeks=1) assert self.now.humanize(later) == "a week ago" @@ -2157,7 +2015,6 @@ def test_week(self): assert later.humanize(self.now, only_distance=True) == "a week" def test_weeks(self): - later = self.now.shift(weeks=2) assert self.now.humanize(later) == "2 weeks ago" @@ -2168,7 +2025,6 @@ def test_weeks(self): @pytest.mark.xfail(reason="known issue with humanize month limits") def test_month(self): - later = self.now.shift(months=1) # TODO this test now returns "4 weeks ago", we need to fix this to be correct on a per month basis @@ -2179,7 +2035,6 @@ def test_month(self): assert later.humanize(self.now, only_distance=True) == "a month" def test_month_plus_4_days(self): - # TODO needed for coverage, remove when month limits are fixed later = self.now.shift(months=1, days=4) @@ -2188,7 +2043,6 @@ def test_month_plus_4_days(self): @pytest.mark.xfail(reason="known issue with humanize month limits") def test_months(self): - later = self.now.shift(months=2) earlier = self.now.shift(months=-2) @@ -2199,7 +2053,6 @@ def test_months(self): assert later.humanize(self.now, only_distance=True) == "2 months" def test_year(self): - later = self.now.shift(years=1) assert self.now.humanize(later) == "a year ago" @@ -2209,7 +2062,6 @@ def test_year(self): assert later.humanize(self.now, only_distance=True) == "a year" def test_years(self): - later = self.now.shift(years=2) assert self.now.humanize(later) == "2 years ago" @@ -2225,7 +2077,6 @@ def test_years(self): assert result == "in a year" def test_arrow(self): - arw = arrow.Arrow.fromdatetime(self.datetime) result = arw.humanize(arrow.Arrow.fromdatetime(self.datetime)) @@ -2233,7 +2084,6 @@ def test_arrow(self): assert result == "just now" def test_datetime_tzinfo(self): - arw = arrow.Arrow.fromdatetime(self.datetime) result = arw.humanize(self.datetime.replace(tzinfo=tz.tzutc())) @@ -2241,21 +2091,18 @@ def test_datetime_tzinfo(self): assert result == "just now" def test_other(self): - arw = arrow.Arrow.fromdatetime(self.datetime) with pytest.raises(TypeError): arw.humanize(object()) def test_invalid_locale(self): - arw = arrow.Arrow.fromdatetime(self.datetime) with pytest.raises(ValueError): arw.humanize(locale="klingon") def test_none(self): - arw = arrow.Arrow.utcnow() result = arw.humanize() @@ -2277,7 +2124,6 @@ def test_week_limit(self): assert result == "a week ago" def test_untranslated_granularity(self, mocker): - arw = arrow.Arrow.utcnow() later = arw.shift(weeks=1) @@ -2317,7 +2163,6 @@ def test_no_floats_multi_gran(self): @pytest.mark.usefixtures("time_2013_01_01") class TestArrowHumanizeTestsWithLocale: def test_now(self): - arw = arrow.Arrow(2013, 1, 1, 0, 0, 0) result = arw.humanize(self.datetime, locale="ru") @@ -2331,7 +2176,6 @@ def test_seconds(self): assert result == "через 44 секунды" def test_years(self): - arw = arrow.Arrow(2011, 7, 2) result = arw.humanize(self.datetime, locale="ru") @@ -2582,9 +2426,7 @@ def slavic_locales() -> List[str]: class TestArrowDehumanize: def test_now(self, locale_list_no_weeks: List[str]): - for lang in locale_list_no_weeks: - arw = arrow.Arrow(2000, 6, 18, 5, 55, 0) second_ago = arw.shift(seconds=-1) second_future = arw.shift(seconds=1) @@ -2600,9 +2442,7 @@ def test_now(self, locale_list_no_weeks: List[str]): assert arw.dehumanize(second_future_string, locale=lang) == arw def test_seconds(self, locale_list_no_weeks: List[str]): - for lang in locale_list_no_weeks: - arw = arrow.Arrow(2000, 6, 18, 5, 55, 0) second_ago = arw.shift(seconds=-5) second_future = arw.shift(seconds=5) @@ -2618,9 +2458,7 @@ def test_seconds(self, locale_list_no_weeks: List[str]): assert arw.dehumanize(second_future_string, locale=lang) == second_future def test_minute(self, locale_list_no_weeks: List[str]): - for lang in locale_list_no_weeks: - arw = arrow.Arrow(2001, 6, 18, 5, 55, 0) minute_ago = arw.shift(minutes=-1) minute_future = arw.shift(minutes=1) @@ -2636,9 +2474,7 @@ def test_minute(self, locale_list_no_weeks: List[str]): assert arw.dehumanize(minute_future_string, locale=lang) == minute_future def test_minutes(self, locale_list_no_weeks: List[str]): - for lang in locale_list_no_weeks: - arw = arrow.Arrow(2007, 1, 10, 5, 55, 0) minute_ago = arw.shift(minutes=-5) minute_future = arw.shift(minutes=5) @@ -2654,9 +2490,7 @@ def test_minutes(self, locale_list_no_weeks: List[str]): assert arw.dehumanize(minute_future_string, locale=lang) == minute_future def test_hour(self, locale_list_no_weeks: List[str]): - for lang in locale_list_no_weeks: - arw = arrow.Arrow(2009, 4, 20, 5, 55, 0) hour_ago = arw.shift(hours=-1) hour_future = arw.shift(hours=1) @@ -2670,9 +2504,7 @@ def test_hour(self, locale_list_no_weeks: List[str]): assert arw.dehumanize(hour_future_string, locale=lang) == hour_future def test_hours(self, locale_list_no_weeks: List[str]): - for lang in locale_list_no_weeks: - arw = arrow.Arrow(2010, 2, 16, 7, 55, 0) hour_ago = arw.shift(hours=-3) hour_future = arw.shift(hours=3) @@ -2686,9 +2518,7 @@ def test_hours(self, locale_list_no_weeks: List[str]): assert arw.dehumanize(hour_future_string, locale=lang) == hour_future def test_week(self, locale_list_with_weeks: List[str]): - for lang in locale_list_with_weeks: - arw = arrow.Arrow(2012, 2, 18, 1, 52, 0) week_ago = arw.shift(weeks=-1) week_future = arw.shift(weeks=1) @@ -2702,9 +2532,7 @@ def test_week(self, locale_list_with_weeks: List[str]): assert arw.dehumanize(week_future_string, locale=lang) == week_future def test_weeks(self, locale_list_with_weeks: List[str]): - for lang in locale_list_with_weeks: - arw = arrow.Arrow(2020, 3, 18, 5, 3, 0) week_ago = arw.shift(weeks=-7) week_future = arw.shift(weeks=7) @@ -2718,9 +2546,7 @@ def test_weeks(self, locale_list_with_weeks: List[str]): assert arw.dehumanize(week_future_string, locale=lang) == week_future def test_year(self, locale_list_no_weeks: List[str]): - for lang in locale_list_no_weeks: - arw = arrow.Arrow(2000, 1, 10, 5, 55, 0) year_ago = arw.shift(years=-1) year_future = arw.shift(years=1) @@ -2734,9 +2560,7 @@ def test_year(self, locale_list_no_weeks: List[str]): assert arw.dehumanize(year_future_string, locale=lang) == year_future def test_years(self, locale_list_no_weeks: List[str]): - for lang in locale_list_no_weeks: - arw = arrow.Arrow(2000, 1, 10, 5, 55, 0) year_ago = arw.shift(years=-10) year_future = arw.shift(years=10) @@ -2750,9 +2574,7 @@ def test_years(self, locale_list_no_weeks: List[str]): assert arw.dehumanize(year_future_string, locale=lang) == year_future def test_gt_than_10_years(self, locale_list_no_weeks: List[str]): - for lang in locale_list_no_weeks: - arw = arrow.Arrow(2000, 1, 10, 5, 55, 0) year_ago = arw.shift(years=-25) year_future = arw.shift(years=25) @@ -2766,9 +2588,7 @@ def test_gt_than_10_years(self, locale_list_no_weeks: List[str]): assert arw.dehumanize(year_future_string, locale=lang) == year_future def test_mixed_granularity(self, locale_list_no_weeks: List[str]): - for lang in locale_list_no_weeks: - arw = arrow.Arrow(2000, 1, 10, 5, 55, 0) past = arw.shift(hours=-1, minutes=-1, seconds=-1) future = arw.shift(hours=1, minutes=1, seconds=1) @@ -2784,9 +2604,7 @@ def test_mixed_granularity(self, locale_list_no_weeks: List[str]): assert arw.dehumanize(future_string, locale=lang) == future def test_mixed_granularity_hours(self, locale_list_no_weeks: List[str]): - for lang in locale_list_no_weeks: - arw = arrow.Arrow(2000, 1, 10, 5, 55, 0) past = arw.shift(hours=-3, minutes=-1, seconds=-15) future = arw.shift(hours=3, minutes=1, seconds=15) @@ -2802,9 +2620,7 @@ def test_mixed_granularity_hours(self, locale_list_no_weeks: List[str]): assert arw.dehumanize(future_string, locale=lang) == future def test_mixed_granularity_day(self, locale_list_no_weeks: List[str]): - for lang in locale_list_no_weeks: - arw = arrow.Arrow(2000, 1, 10, 5, 55, 0) past = arw.shift(days=-3, minutes=-1, seconds=-15) future = arw.shift(days=3, minutes=1, seconds=15) @@ -2820,9 +2636,7 @@ def test_mixed_granularity_day(self, locale_list_no_weeks: List[str]): assert arw.dehumanize(future_string, locale=lang) == future def test_mixed_granularity_day_hour(self, locale_list_no_weeks: List[str]): - for lang in locale_list_no_weeks: - arw = arrow.Arrow(2000, 1, 10, 5, 55, 0) past = arw.shift(days=-3, hours=-23, seconds=-15) future = arw.shift(days=3, hours=23, seconds=15) @@ -2839,7 +2653,6 @@ def test_mixed_granularity_day_hour(self, locale_list_no_weeks: List[str]): # Test to make sure unsupported locales error out def test_unsupported_locale(self): - arw = arrow.Arrow(2000, 6, 18, 5, 55, 0) second_ago = arw.shift(seconds=-5) second_future = arw.shift(seconds=5) @@ -2860,7 +2673,6 @@ def test_unsupported_locale(self): # Test to ensure old style locale strings are supported def test_normalized_locale(self): - arw = arrow.Arrow(2000, 6, 18, 5, 55, 0) second_ago = arw.shift(seconds=-5) second_future = arw.shift(seconds=5) @@ -2877,9 +2689,7 @@ def test_normalized_locale(self): # Ensures relative units are required in string def test_require_relative_unit(self, locale_list_no_weeks: List[str]): - for lang in locale_list_no_weeks: - arw = arrow.Arrow(2000, 6, 18, 5, 55, 0) second_ago = arw.shift(seconds=-5) second_future = arw.shift(seconds=5) @@ -2899,9 +2709,7 @@ def test_require_relative_unit(self, locale_list_no_weeks: List[str]): # Test for scrambled input def test_scrambled_input(self, locale_list_no_weeks: List[str]): - for lang in locale_list_no_weeks: - arw = arrow.Arrow(2000, 6, 18, 5, 55, 0) second_ago = arw.shift(seconds=-5) second_future = arw.shift(seconds=5) @@ -2927,9 +2735,7 @@ def test_scrambled_input(self, locale_list_no_weeks: List[str]): arw.dehumanize(second_future_string, locale=lang) def test_no_units_modified(self, locale_list_no_weeks: List[str]): - for lang in locale_list_no_weeks: - arw = arrow.Arrow(2000, 6, 18, 5, 55, 0) # Ensures we pass the first stage of checking whether relative units exist @@ -2944,7 +2750,6 @@ def test_no_units_modified(self, locale_list_no_weeks: List[str]): arw.dehumanize(empty_future_string, locale=lang) def test_slavic_locales(self, slavic_locales: List[str]): - # Relevant units for Slavic locale plural logic units = [ 0, @@ -2975,7 +2780,6 @@ def test_slavic_locales(self, slavic_locales: List[str]): assert arw.dehumanize(future_string, locale=lang) == future def test_czech_slovak(self): - # Relevant units for Slavic locale plural logic units = [ 0, @@ -3082,7 +2886,6 @@ def test_value_error_exception(self): class TestArrowUtil: def test_get_datetime(self): - get_datetime = arrow.Arrow._get_datetime arw = arrow.Arrow.utcnow() @@ -3100,7 +2903,6 @@ def test_get_datetime(self): assert "not recognized as a datetime or timestamp" in str(raise_ctx.value) def test_get_tzinfo(self): - get_tzinfo = arrow.Arrow._get_tzinfo with pytest.raises(ValueError) as raise_ctx: @@ -3108,7 +2910,6 @@ def test_get_tzinfo(self): assert "not recognized as a timezone" in str(raise_ctx.value) def test_get_iteration_params(self): - assert arrow.Arrow._get_iteration_params("end", None) == ("end", sys.maxsize) assert arrow.Arrow._get_iteration_params(None, 100) == (arrow.Arrow.max, 100) assert arrow.Arrow._get_iteration_params(100, 120) == (100, 120) diff --git a/tests/test_factory.py b/tests/test_factory.py index f368126c..4e328000 100644 --- a/tests/test_factory.py +++ b/tests/test_factory.py @@ -14,13 +14,11 @@ @pytest.mark.usefixtures("arrow_factory") class TestGet: def test_no_args(self): - assert_datetime_equality( self.factory.get(), datetime.utcnow().replace(tzinfo=tz.tzutc()) ) def test_timestamp_one_arg_no_arg(self): - no_arg = self.factory.get(1406430900).timestamp() one_arg = self.factory.get("1406430900", "X").timestamp() @@ -31,14 +29,12 @@ def test_one_arg_none(self): self.factory.get(None) def test_struct_time(self): - assert_datetime_equality( self.factory.get(time.gmtime()), datetime.utcnow().replace(tzinfo=tz.tzutc()), ) def test_one_arg_timestamp(self): - int_timestamp = int(time.time()) timestamp_dt = datetime.utcfromtimestamp(int_timestamp).replace( tzinfo=tz.tzutc() @@ -66,7 +62,6 @@ def test_one_arg_timestamp(self): self.factory.get(timestamp) def test_one_arg_expanded_timestamp(self): - millisecond_timestamp = 1591328104308 microsecond_timestamp = 1591328104308505 @@ -79,7 +74,6 @@ def test_one_arg_expanded_timestamp(self): ).replace(tzinfo=tz.tzutc()) def test_one_arg_timestamp_with_tzinfo(self): - timestamp = time.time() timestamp_dt = datetime.fromtimestamp(timestamp, tz=tz.tzutc()).astimezone( tz.gettz("US/Pacific") @@ -91,27 +85,23 @@ def test_one_arg_timestamp_with_tzinfo(self): ) def test_one_arg_arrow(self): - arw = self.factory.utcnow() result = self.factory.get(arw) assert arw == result def test_one_arg_datetime(self): - dt = datetime.utcnow().replace(tzinfo=tz.tzutc()) assert self.factory.get(dt) == dt def test_one_arg_date(self): - d = date.today() dt = datetime(d.year, d.month, d.day, tzinfo=tz.tzutc()) assert self.factory.get(d) == dt def test_one_arg_tzinfo(self): - self.expected = ( datetime.utcnow() .replace(tzinfo=tz.tzutc()) @@ -132,7 +122,6 @@ def test_one_arg_dateparser_datetime(self): assert dt_output == expected def test_kwarg_tzinfo(self): - self.expected = ( datetime.utcnow() .replace(tzinfo=tz.tzutc()) @@ -144,7 +133,6 @@ def test_kwarg_tzinfo(self): ) def test_kwarg_tzinfo_string(self): - self.expected = ( datetime.utcnow() .replace(tzinfo=tz.tzutc()) @@ -176,7 +164,6 @@ def test_kwarg_normalize_whitespace(self): # regression test for #944 def test_one_arg_datetime_tzinfo_kwarg(self): - dt = datetime(2021, 4, 29, 6) result = self.factory.get(dt, tzinfo="America/Chicago") @@ -186,7 +173,6 @@ def test_one_arg_datetime_tzinfo_kwarg(self): assert_datetime_equality(result._datetime, expected) def test_one_arg_arrow_tzinfo_kwarg(self): - arw = Arrow(2021, 4, 29, 6) result = self.factory.get(arw, tzinfo="America/Chicago") @@ -196,7 +182,6 @@ def test_one_arg_arrow_tzinfo_kwarg(self): assert_datetime_equality(result._datetime, expected) def test_one_arg_date_tzinfo_kwarg(self): - da = date(2021, 4, 29) result = self.factory.get(da, tzinfo="America/Chicago") @@ -207,7 +192,6 @@ def test_one_arg_date_tzinfo_kwarg(self): assert result.tzinfo == expected.tzinfo def test_one_arg_iso_calendar_tzinfo_kwarg(self): - result = self.factory.get((2004, 1, 7), tzinfo="America/Chicago") expected = Arrow(2004, 1, 4, tzinfo="America/Chicago") @@ -215,7 +199,6 @@ def test_one_arg_iso_calendar_tzinfo_kwarg(self): assert_datetime_equality(result, expected) def test_one_arg_iso_str(self): - dt = datetime.utcnow() assert_datetime_equality( @@ -223,7 +206,6 @@ def test_one_arg_iso_str(self): ) def test_one_arg_iso_calendar(self): - pairs = [ (datetime(2004, 1, 4), (2004, 1, 7)), (datetime(2008, 12, 30), (2009, 1, 2)), @@ -252,12 +234,10 @@ def test_one_arg_iso_calendar(self): self.factory.get((2014, 7, 10)) def test_one_arg_other(self): - with pytest.raises(TypeError): self.factory.get(object()) def test_one_arg_bool(self): - with pytest.raises(TypeError): self.factory.get(False) @@ -272,47 +252,39 @@ def test_one_arg_decimal(self): ) def test_two_args_datetime_tzinfo(self): - result = self.factory.get(datetime(2013, 1, 1), tz.gettz("US/Pacific")) assert result._datetime == datetime(2013, 1, 1, tzinfo=tz.gettz("US/Pacific")) def test_two_args_datetime_tz_str(self): - result = self.factory.get(datetime(2013, 1, 1), "US/Pacific") assert result._datetime == datetime(2013, 1, 1, tzinfo=tz.gettz("US/Pacific")) def test_two_args_date_tzinfo(self): - result = self.factory.get(date(2013, 1, 1), tz.gettz("US/Pacific")) assert result._datetime == datetime(2013, 1, 1, tzinfo=tz.gettz("US/Pacific")) def test_two_args_date_tz_str(self): - result = self.factory.get(date(2013, 1, 1), "US/Pacific") assert result._datetime == datetime(2013, 1, 1, tzinfo=tz.gettz("US/Pacific")) def test_two_args_datetime_other(self): - with pytest.raises(TypeError): self.factory.get(datetime.utcnow(), object()) def test_two_args_date_other(self): - with pytest.raises(TypeError): self.factory.get(date.today(), object()) def test_two_args_str_str(self): - result = self.factory.get("2013-01-01", "YYYY-MM-DD") assert result._datetime == datetime(2013, 1, 1, tzinfo=tz.tzutc()) def test_two_args_str_tzinfo(self): - result = self.factory.get("2013-01-01", tzinfo=tz.gettz("US/Pacific")) assert_datetime_equality( @@ -320,7 +292,6 @@ def test_two_args_str_tzinfo(self): ) def test_two_args_twitter_format(self): - # format returned by twitter API for created_at: twitter_date = "Fri Apr 08 21:08:54 +0000 2016" result = self.factory.get(twitter_date, "ddd MMM DD HH:mm:ss Z YYYY") @@ -328,24 +299,20 @@ def test_two_args_twitter_format(self): assert result._datetime == datetime(2016, 4, 8, 21, 8, 54, tzinfo=tz.tzutc()) def test_two_args_str_list(self): - result = self.factory.get("2013-01-01", ["MM/DD/YYYY", "YYYY-MM-DD"]) assert result._datetime == datetime(2013, 1, 1, tzinfo=tz.tzutc()) def test_two_args_unicode_unicode(self): - result = self.factory.get("2013-01-01", "YYYY-MM-DD") assert result._datetime == datetime(2013, 1, 1, tzinfo=tz.tzutc()) def test_two_args_other(self): - with pytest.raises(TypeError): self.factory.get(object(), object()) def test_three_args_with_tzinfo(self): - timefmt = "YYYYMMDD" d = "20150514" @@ -354,11 +321,9 @@ def test_three_args_with_tzinfo(self): ) def test_three_args(self): - assert self.factory.get(2013, 1, 1) == datetime(2013, 1, 1, tzinfo=tz.tzutc()) def test_full_kwargs(self): - assert self.factory.get( year=2016, month=7, @@ -370,7 +335,6 @@ def test_full_kwargs(self): ) == datetime(2016, 7, 14, 7, 16, 45, 631092, tzinfo=tz.tzutc()) def test_three_kwargs(self): - assert self.factory.get(year=2016, month=7, day=14) == datetime( 2016, 7, 14, 0, 0, tzinfo=tz.tzutc() ) @@ -380,7 +344,6 @@ def test_tzinfo_string_kwargs(self): assert result._datetime == datetime(2019, 7, 28, 7, 0, 0, 0, tzinfo=tz.tzutc()) def test_insufficient_kwargs(self): - with pytest.raises(TypeError): self.factory.get(year=2016) @@ -409,7 +372,6 @@ def test_locale_with_tzinfo(self): @pytest.mark.usefixtures("arrow_factory") class TestUtcNow: def test_utcnow(self): - assert_datetime_equality( self.factory.utcnow()._datetime, datetime.utcnow().replace(tzinfo=tz.tzutc()), @@ -419,15 +381,12 @@ def test_utcnow(self): @pytest.mark.usefixtures("arrow_factory") class TestNow: def test_no_tz(self): - assert_datetime_equality(self.factory.now(), datetime.now(tz.tzlocal())) def test_tzinfo(self): - assert_datetime_equality( self.factory.now(tz.gettz("EST")), datetime.now(tz.gettz("EST")) ) def test_tz_str(self): - assert_datetime_equality(self.factory.now("EST"), datetime.now(tz.gettz("EST"))) diff --git a/tests/test_formatter.py b/tests/test_formatter.py index 06831f1e..0b6c256c 100644 --- a/tests/test_formatter.py +++ b/tests/test_formatter.py @@ -23,7 +23,6 @@ @pytest.mark.usefixtures("arrow_formatter") class TestFormatterFormatToken: def test_format(self): - dt = datetime(2013, 2, 5, 12, 32, 51) result = self.formatter.format(dt, "MM-DD-YYYY hh:mm:ss a") @@ -31,13 +30,11 @@ def test_format(self): assert result == "02-05-2013 12:32:51 pm" def test_year(self): - dt = datetime(2013, 1, 1) assert self.formatter._format_token(dt, "YYYY") == "2013" assert self.formatter._format_token(dt, "YY") == "13" def test_month(self): - dt = datetime(2013, 1, 1) assert self.formatter._format_token(dt, "MMMM") == "January" assert self.formatter._format_token(dt, "MMM") == "Jan" @@ -45,7 +42,6 @@ def test_month(self): assert self.formatter._format_token(dt, "M") == "1" def test_day(self): - dt = datetime(2013, 2, 1) assert self.formatter._format_token(dt, "DDDD") == "032" assert self.formatter._format_token(dt, "DDD") == "32" @@ -58,7 +54,6 @@ def test_day(self): assert self.formatter._format_token(dt, "d") == "5" def test_hour(self): - dt = datetime(2013, 1, 1, 2) assert self.formatter._format_token(dt, "HH") == "02" assert self.formatter._format_token(dt, "H") == "2" @@ -81,19 +76,16 @@ def test_hour(self): assert self.formatter._format_token(dt, "h") == "12" def test_minute(self): - dt = datetime(2013, 1, 1, 0, 1) assert self.formatter._format_token(dt, "mm") == "01" assert self.formatter._format_token(dt, "m") == "1" def test_second(self): - dt = datetime(2013, 1, 1, 0, 0, 1) assert self.formatter._format_token(dt, "ss") == "01" assert self.formatter._format_token(dt, "s") == "1" def test_sub_second(self): - dt = datetime(2013, 1, 1, 0, 0, 0, 123456) assert self.formatter._format_token(dt, "SSSSSS") == "123456" assert self.formatter._format_token(dt, "SSSSS") == "12345" @@ -111,7 +103,6 @@ def test_sub_second(self): assert self.formatter._format_token(dt, "S") == "0" def test_timestamp(self): - dt = datetime.now(tz=dateutil_tz.UTC) expected = str(dt.timestamp()) assert self.formatter._format_token(dt, "X") == expected @@ -122,7 +113,6 @@ def test_timestamp(self): assert self.formatter._format_token(dt, "x") == expected def test_timezone(self): - dt = datetime.utcnow().replace(tzinfo=dateutil_tz.gettz("US/Pacific")) result = self.formatter._format_token(dt, "ZZ") @@ -133,7 +123,6 @@ def test_timezone(self): @pytest.mark.parametrize("full_tz_name", make_full_tz_list()) def test_timezone_formatter(self, full_tz_name): - # This test will fail if we use "now" as date as soon as we change from/to DST dt = datetime(1986, 2, 14, tzinfo=pytz.timezone("UTC")).replace( tzinfo=dateutil_tz.gettz(full_tz_name) @@ -144,7 +133,6 @@ def test_timezone_formatter(self, full_tz_name): assert result == abbreviation def test_am_pm(self): - dt = datetime(2012, 1, 1, 11) assert self.formatter._format_token(dt, "a") == "am" assert self.formatter._format_token(dt, "A") == "AM" @@ -167,7 +155,6 @@ def test_nonsense(self): assert self.formatter._format_token(dt, "NONSENSE") is None def test_escape(self): - assert ( self.formatter.format( datetime(2015, 12, 10, 17, 9), "MMMM D, YYYY [at] h:mma" diff --git a/tests/test_locales.py b/tests/test_locales.py index 4bbbd3dc..d6abb2a1 100644 --- a/tests/test_locales.py +++ b/tests/test_locales.py @@ -8,7 +8,6 @@ class TestLocaleValidation: """Validate locales to ensure that translations are valid and complete""" def test_locale_validation(self): - for locale_cls in self.locales.values(): # 7 days + 1 spacer to allow for 1-indexing of months assert len(locale_cls.day_names) == 8 @@ -34,7 +33,6 @@ def test_locale_validation(self): assert locale_cls.future is not None def test_locale_name_validation(self): - for locale_cls in self.locales.values(): for locale_name in locale_cls.names: assert len(locale_name) == 2 or len(locale_name) == 5 @@ -90,7 +88,6 @@ def test_get_locale_by_class_name(self, mocker): assert result == mock_locale_obj def test_locales(self): - assert len(locales._locale_map) > 0 @@ -116,24 +113,20 @@ def test_describe(self): assert self.locale.describe("now", only_distance=False) == "just now" def test_format_timeframe(self): - assert self.locale._format_timeframe("hours", 2) == "2 hours" assert self.locale._format_timeframe("hour", 0) == "an hour" def test_format_relative_now(self): - result = self.locale._format_relative("just now", "now", 0) assert result == "just now" def test_format_relative_past(self): - result = self.locale._format_relative("an hour", "hour", 1) assert result == "in an hour" def test_format_relative_future(self): - result = self.locale._format_relative("an hour", "hour", -1) assert result == "an hour ago" @@ -438,7 +431,6 @@ def test_plurals2(self): @pytest.mark.usefixtures("lang_locale") class TestPolishLocale: def test_plurals(self): - assert self.locale._format_timeframe("seconds", 0) == "0 sekund" assert self.locale._format_timeframe("second", 1) == "sekundę" assert self.locale._format_timeframe("seconds", 2) == "2 sekundy" @@ -491,7 +483,6 @@ def test_plurals(self): @pytest.mark.usefixtures("lang_locale") class TestIcelandicLocale: def test_format_timeframe(self): - assert self.locale._format_timeframe("now", 0) == "rétt í þessu" assert self.locale._format_timeframe("second", -1) == "sekúndu" @@ -534,23 +525,19 @@ def test_format_timeframe(self): @pytest.mark.usefixtures("lang_locale") class TestMalayalamLocale: def test_format_timeframe(self): - assert self.locale._format_timeframe("hours", 2) == "2 മണിക്കൂർ" assert self.locale._format_timeframe("hour", 0) == "ഒരു മണിക്കൂർ" def test_format_relative_now(self): - result = self.locale._format_relative("ഇപ്പോൾ", "now", 0) assert result == "ഇപ്പോൾ" def test_format_relative_past(self): - result = self.locale._format_relative("ഒരു മണിക്കൂർ", "hour", 1) assert result == "ഒരു മണിക്കൂർ ശേഷം" def test_format_relative_future(self): - result = self.locale._format_relative("ഒരു മണിക്കൂർ", "hour", -1) assert result == "ഒരു മണിക്കൂർ മുമ്പ്" @@ -585,22 +572,18 @@ def test_weekday(self): @pytest.mark.usefixtures("lang_locale") class TestHindiLocale: def test_format_timeframe(self): - assert self.locale._format_timeframe("hours", 2) == "2 घंटे" assert self.locale._format_timeframe("hour", 0) == "एक घंटा" def test_format_relative_now(self): - result = self.locale._format_relative("अभी", "now", 0) assert result == "अभी" def test_format_relative_past(self): - result = self.locale._format_relative("एक घंटा", "hour", 1) assert result == "एक घंटा बाद" def test_format_relative_future(self): - result = self.locale._format_relative("एक घंटा", "hour", -1) assert result == "एक घंटा पहले" @@ -675,17 +658,14 @@ def test_format_timeframe(self): assert self.locale._format_timeframe("years", 5) == "5 let" def test_format_relative_now(self): - result = self.locale._format_relative("Teď", "now", 0) assert result == "Teď" def test_format_relative_future(self): - result = self.locale._format_relative("hodinu", "hour", 1) assert result == "Za hodinu" def test_format_relative_past(self): - result = self.locale._format_relative("hodinou", "hour", -1) assert result == "Před hodinou" @@ -693,7 +673,6 @@ def test_format_relative_past(self): @pytest.mark.usefixtures("lang_locale") class TestSlovakLocale: def test_format_timeframe(self): - assert self.locale._format_timeframe("seconds", -5) == "5 sekundami" assert self.locale._format_timeframe("seconds", -2) == "2 sekundami" assert self.locale._format_timeframe("second", -1) == "sekundou" @@ -753,17 +732,14 @@ def test_format_timeframe(self): assert self.locale._format_timeframe("now", 0) == "Teraz" def test_format_relative_now(self): - result = self.locale._format_relative("Teraz", "now", 0) assert result == "Teraz" def test_format_relative_future(self): - result = self.locale._format_relative("hodinu", "hour", 1) assert result == "O hodinu" def test_format_relative_past(self): - result = self.locale._format_relative("hodinou", "hour", -1) assert result == "Pred hodinou" @@ -1400,7 +1376,6 @@ def test_ordinal_number(self): @pytest.mark.usefixtures("lang_locale") class TestRomanianLocale: def test_timeframes(self): - assert self.locale._format_timeframe("hours", 2) == "2 ore" assert self.locale._format_timeframe("months", 2) == "2 luni" @@ -1435,7 +1410,6 @@ def test_relative_timeframes(self): @pytest.mark.usefixtures("lang_locale") class TestArabicLocale: def test_timeframes(self): - # single assert self.locale._format_timeframe("minute", 1) == "دقيقة" assert self.locale._format_timeframe("hour", 1) == "ساعة" @@ -2514,22 +2488,18 @@ def test_ordinal_number(self): assert self.locale._ordinal_number(-1) == "" def test_format_timeframe(self): - assert self.locale._format_timeframe("hours", 2) == "2 ଘଣ୍ଟା" assert self.locale._format_timeframe("hour", 0) == "ଏକ ଘଣ୍ଟା" def test_format_relative_now(self): - result = self.locale._format_relative("ବର୍ତ୍ତମାନ", "now", 0) assert result == "ବର୍ତ୍ତମାନ" def test_format_relative_past(self): - result = self.locale._format_relative("ଏକ ଘଣ୍ଟା", "hour", 1) assert result == "ଏକ ଘଣ୍ଟା ପରେ" def test_format_relative_future(self): - result = self.locale._format_relative("ଏକ ଘଣ୍ଟା", "hour", -1) assert result == "ଏକ ଘଣ୍ଟା ପୂର୍ବେ" @@ -2758,13 +2728,11 @@ def test_format_relative_now(self): assert result == "දැන්" def test_format_relative_future(self): - result = self.locale._format_relative("පැයකින්", "පැය", 1) assert result == "පැයකින්" # (in) one hour def test_format_relative_past(self): - result = self.locale._format_relative("පැයක", "පැය", -1) assert result == "පැයකට පෙර" # an hour ago @@ -2868,24 +2836,20 @@ def test_ordinal_number(self): assert self.locale.ordinal_number(1) == "1." def test_format_timeframe(self): - assert self.locale._format_timeframe("hours", 2) == "2 timer" assert self.locale._format_timeframe("hour", 0) == "en time" def test_format_relative_now(self): - result = self.locale._format_relative("nå nettopp", "now", 0) assert result == "nå nettopp" def test_format_relative_past(self): - result = self.locale._format_relative("en time", "hour", 1) assert result == "om en time" def test_format_relative_future(self): - result = self.locale._format_relative("en time", "hour", -1) assert result == "for en time siden" @@ -2924,24 +2888,20 @@ def test_ordinal_number(self): assert self.locale.ordinal_number(1) == "1." def test_format_timeframe(self): - assert self.locale._format_timeframe("hours", 2) == "2 timar" assert self.locale._format_timeframe("hour", 0) == "ein time" def test_format_relative_now(self): - result = self.locale._format_relative("no nettopp", "now", 0) assert result == "no nettopp" def test_format_relative_past(self): - result = self.locale._format_relative("ein time", "hour", 1) assert result == "om ein time" def test_format_relative_future(self): - result = self.locale._format_relative("ein time", "hour", -1) assert result == "for ein time sidan" @@ -3063,13 +3023,11 @@ def test_ordinal_number(self): assert self.locale.ordinal_number(1) == "1ኛ" def test_format_relative_future(self): - result = self.locale._format_relative("በአንድ ሰዓት", "hour", 1) assert result == "በአንድ ሰዓት ውስጥ" # (in) one hour def test_format_relative_past(self): - result = self.locale._format_relative("ከአንድ ሰዓት", "hour", -1) assert result == "ከአንድ ሰዓት በፊት" # an hour ago diff --git a/tests/test_parser.py b/tests/test_parser.py index bdcc1026..1932b450 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -83,7 +83,6 @@ def test_parse_token_invalid_meridians(self): assert parts == {} def test_parser_no_caching(self, mocker): - mocked_parser = mocker.patch( "arrow.parser.DateTimeParser._generate_pattern_re", fmt="fmt_a" ) @@ -135,7 +134,6 @@ def test_parser_multiple_line_caching(self, mocker): assert mocked_parser.call_args_list[1] == mocker.call(fmt="fmt_b") def test_YY_and_YYYY_format_list(self): - assert self.parser.parse("15/01/19", ["DD/MM/YY", "DD/MM/YYYY"]) == datetime( 2019, 1, 15 ) @@ -165,7 +163,6 @@ def test_timestamp_format_list(self): @pytest.mark.usefixtures("dt_parser") class TestDateTimeParserParse: def test_parse_list(self, mocker): - mocker.patch( "arrow.parser.DateTimeParser._parse_multiformat", string="str", @@ -177,7 +174,6 @@ def test_parse_list(self, mocker): assert result == "result" def test_parse_unrecognized_token(self, mocker): - mocker.patch.dict("arrow.parser.DateTimeParser._BASE_INPUT_RE_MAP") del arrow.parser.DateTimeParser._BASE_INPUT_RE_MAP["YYYY"] @@ -187,17 +183,14 @@ def test_parse_unrecognized_token(self, mocker): _parser.parse("2013-01-01", "YYYY-MM-DD") def test_parse_parse_no_match(self): - with pytest.raises(ParserError): self.parser.parse("01-01", "YYYY-MM-DD") def test_parse_separators(self): - with pytest.raises(ParserError): self.parser.parse("1403549231", "YYYY-MM-DD") def test_parse_numbers(self): - self.expected = datetime(2012, 1, 1, 12, 5, 10) assert ( self.parser.parse("2012-01-01 12:05:10", "YYYY-MM-DD HH:mm:ss") @@ -205,19 +198,16 @@ def test_parse_numbers(self): ) def test_parse_am(self): - with pytest.raises(ParserMatchError): self.parser.parse("2021-01-30 14:00:00 AM", "YYYY-MM-DD HH:mm:ss A") def test_parse_year_two_digit(self): - self.expected = datetime(1979, 1, 1, 12, 5, 10) assert ( self.parser.parse("79-01-01 12:05:10", "YY-MM-DD HH:mm:ss") == self.expected ) def test_parse_timestamp(self): - tz_utc = tz.tzutc() float_timestamp = time.time() int_timestamp = int(float_timestamp) @@ -296,14 +286,12 @@ def test_parse_expanded_timestamp(self): self.parser.parse(f"{timestamp:f}", "x") def test_parse_names(self): - self.expected = datetime(2012, 1, 1) assert self.parser.parse("January 1, 2012", "MMMM D, YYYY") == self.expected assert self.parser.parse("Jan 1, 2012", "MMM D, YYYY") == self.expected def test_parse_pm(self): - self.expected = datetime(1, 1, 1, 13, 0, 0) assert self.parser.parse("1 pm", "H a") == self.expected assert self.parser.parse("1 pm", "h a") == self.expected @@ -321,19 +309,16 @@ def test_parse_pm(self): assert self.parser.parse("12 pm", "h A") == self.expected def test_parse_tz_hours_only(self): - self.expected = datetime(2025, 10, 17, 5, 30, 10, tzinfo=tz.tzoffset(None, 0)) parsed = self.parser.parse("2025-10-17 05:30:10+00", "YYYY-MM-DD HH:mm:ssZ") assert parsed == self.expected def test_parse_tz_zz(self): - self.expected = datetime(2013, 1, 1, tzinfo=tz.tzoffset(None, -7 * 3600)) assert self.parser.parse("2013-01-01 -07:00", "YYYY-MM-DD ZZ") == self.expected @pytest.mark.parametrize("full_tz_name", make_full_tz_list()) def test_parse_tz_name_zzz(self, full_tz_name): - self.expected = datetime(2013, 1, 1, tzinfo=tz.gettz(full_tz_name)) assert ( self.parser.parse(f"2013-01-01 {full_tz_name}", "YYYY-MM-DD ZZZ") @@ -727,7 +712,6 @@ def test_parse_HH_24(self): self.parser.parse("2019-12-31T24:00:00.999999", "YYYY-MM-DDTHH:mm:ss.S") def test_parse_W(self): - assert self.parser.parse("2011-W05-4", "W") == datetime(2011, 2, 3) assert self.parser.parse("2011W054", "W") == datetime(2011, 2, 3) assert self.parser.parse("2011-W05", "W") == datetime(2011, 1, 31) @@ -794,31 +778,24 @@ def test_parse_normalize_whitespace(self): @pytest.mark.usefixtures("dt_parser_regex") class TestDateTimeParserRegex: def test_format_year(self): - assert self.format_regex.findall("YYYY-YY") == ["YYYY", "YY"] def test_format_month(self): - assert self.format_regex.findall("MMMM-MMM-MM-M") == ["MMMM", "MMM", "MM", "M"] def test_format_day(self): - assert self.format_regex.findall("DDDD-DDD-DD-D") == ["DDDD", "DDD", "DD", "D"] def test_format_hour(self): - assert self.format_regex.findall("HH-H-hh-h") == ["HH", "H", "hh", "h"] def test_format_minute(self): - assert self.format_regex.findall("mm-m") == ["mm", "m"] def test_format_second(self): - assert self.format_regex.findall("ss-s") == ["ss", "s"] def test_format_subsecond(self): - assert self.format_regex.findall("SSSSSS-SSSSS-SSSS-SSS-SS-S") == [ "SSSSSS", "SSSSS", @@ -829,23 +806,18 @@ def test_format_subsecond(self): ] def test_format_tz(self): - assert self.format_regex.findall("ZZZ-ZZ-Z") == ["ZZZ", "ZZ", "Z"] def test_format_am_pm(self): - assert self.format_regex.findall("A-a") == ["A", "a"] def test_format_timestamp(self): - assert self.format_regex.findall("X") == ["X"] def test_format_timestamp_milli(self): - assert self.format_regex.findall("x") == ["x"] def test_escape(self): - escape_regex = parser.DateTimeParser._ESCAPE_RE assert escape_regex.findall("2018-03-09 8 [h] 40 [hello]") == ["[h]", "[hello]"] @@ -869,7 +841,6 @@ def test_month_abbreviations(self): assert result == calendar.month_abbr[1:] def test_digits(self): - assert parser.DateTimeParser._ONE_OR_TWO_DIGIT_RE.findall("4-56") == ["4", "56"] assert parser.DateTimeParser._ONE_OR_TWO_OR_THREE_DIGIT_RE.findall( "4-56-789" @@ -940,7 +911,6 @@ def test_time(self): @pytest.mark.usefixtures("dt_parser") class TestDateTimeParserISO: def test_YYYY(self): - assert self.parser.parse_iso("2013") == datetime(2013, 1, 1) def test_YYYY_DDDD(self): @@ -968,7 +938,6 @@ def test_YYYY_DDDD(self): assert self.parser.parse_iso("2017-366") == datetime(2018, 1, 1) def test_YYYY_DDDD_HH_mm_ssZ(self): - assert self.parser.parse_iso("2013-036 04:05:06+01:00") == datetime( 2013, 2, 5, 4, 5, 6, tzinfo=tz.tzoffset(None, 3600) ) @@ -982,67 +951,55 @@ def test_YYYY_MM_DDDD(self): self.parser.parse_iso("2014-05-125") def test_YYYY_MM(self): - for separator in DateTimeParser.SEPARATORS: assert self.parser.parse_iso(separator.join(("2013", "02"))) == datetime( 2013, 2, 1 ) def test_YYYY_MM_DD(self): - for separator in DateTimeParser.SEPARATORS: assert self.parser.parse_iso( separator.join(("2013", "02", "03")) ) == datetime(2013, 2, 3) def test_YYYY_MM_DDTHH_mmZ(self): - assert self.parser.parse_iso("2013-02-03T04:05+01:00") == datetime( 2013, 2, 3, 4, 5, tzinfo=tz.tzoffset(None, 3600) ) def test_YYYY_MM_DDTHH_mm(self): - assert self.parser.parse_iso("2013-02-03T04:05") == datetime(2013, 2, 3, 4, 5) def test_YYYY_MM_DDTHH(self): - assert self.parser.parse_iso("2013-02-03T04") == datetime(2013, 2, 3, 4) def test_YYYY_MM_DDTHHZ(self): - assert self.parser.parse_iso("2013-02-03T04+01:00") == datetime( 2013, 2, 3, 4, tzinfo=tz.tzoffset(None, 3600) ) def test_YYYY_MM_DDTHH_mm_ssZ(self): - assert self.parser.parse_iso("2013-02-03T04:05:06+01:00") == datetime( 2013, 2, 3, 4, 5, 6, tzinfo=tz.tzoffset(None, 3600) ) def test_YYYY_MM_DDTHH_mm_ss(self): - assert self.parser.parse_iso("2013-02-03T04:05:06") == datetime( 2013, 2, 3, 4, 5, 6 ) def test_YYYY_MM_DD_HH_mmZ(self): - assert self.parser.parse_iso("2013-02-03 04:05+01:00") == datetime( 2013, 2, 3, 4, 5, tzinfo=tz.tzoffset(None, 3600) ) def test_YYYY_MM_DD_HH_mm(self): - assert self.parser.parse_iso("2013-02-03 04:05") == datetime(2013, 2, 3, 4, 5) def test_YYYY_MM_DD_HH(self): - assert self.parser.parse_iso("2013-02-03 04") == datetime(2013, 2, 3, 4) def test_invalid_time(self): - with pytest.raises(ParserError): self.parser.parse_iso("2013-02-03T") @@ -1053,19 +1010,16 @@ def test_invalid_time(self): self.parser.parse_iso("2013-02-03 04:05:06.") def test_YYYY_MM_DD_HH_mm_ssZ(self): - assert self.parser.parse_iso("2013-02-03 04:05:06+01:00") == datetime( 2013, 2, 3, 4, 5, 6, tzinfo=tz.tzoffset(None, 3600) ) def test_YYYY_MM_DD_HH_mm_ss(self): - assert self.parser.parse_iso("2013-02-03 04:05:06") == datetime( 2013, 2, 3, 4, 5, 6 ) def test_YYYY_MM_DDTHH_mm_ss_S(self): - assert self.parser.parse_iso("2013-02-03T04:05:06.7") == datetime( 2013, 2, 3, 4, 5, 6, 700000 ) @@ -1100,7 +1054,6 @@ def test_YYYY_MM_DDTHH_mm_ss_S(self): ) def test_YYYY_MM_DDTHH_mm_ss_SZ(self): - assert self.parser.parse_iso("2013-02-03T04:05:06.7+01:00") == datetime( 2013, 2, 3, 4, 5, 6, 700000, tzinfo=tz.tzoffset(None, 3600) ) @@ -1126,7 +1079,6 @@ def test_YYYY_MM_DDTHH_mm_ss_SZ(self): ) def test_W(self): - assert self.parser.parse_iso("2011-W05-4") == datetime(2011, 2, 3) assert self.parser.parse_iso("2011-W05-4T14:17:01") == datetime( @@ -1140,7 +1092,6 @@ def test_W(self): ) def test_invalid_Z(self): - with pytest.raises(ParserError): self.parser.parse_iso("2013-02-03T04:05:06.78912z") @@ -1198,7 +1149,6 @@ def test_gnu_date(self): ) def test_isoformat(self): - dt = datetime.utcnow() assert self.parser.parse_iso(dt.isoformat()) == dt @@ -1359,11 +1309,9 @@ def test_midnight_end_day(self): @pytest.mark.usefixtures("tzinfo_parser") class TestTzinfoParser: def test_parse_local(self): - assert self.parser.parse("local") == tz.tzlocal() def test_parse_utc(self): - assert self.parser.parse("utc") == tz.tzutc() assert self.parser.parse("UTC") == tz.tzutc() @@ -1376,7 +1324,6 @@ def test_parse_utc_withoffset(self): ) == tz.tzoffset(None, 3600) def test_parse_iso(self): - assert self.parser.parse("01:00") == tz.tzoffset(None, 3600) assert self.parser.parse("11:35") == tz.tzoffset(None, 11 * 3600 + 2100) assert self.parser.parse("+01:00") == tz.tzoffset(None, 3600) @@ -1391,11 +1338,9 @@ def test_parse_iso(self): assert self.parser.parse("-01") == tz.tzoffset(None, -3600) def test_parse_str(self): - assert self.parser.parse("US/Pacific") == tz.gettz("US/Pacific") def test_parse_fails(self): - with pytest.raises(parser.ParserError): self.parser.parse("fail") @@ -1403,31 +1348,25 @@ def test_parse_fails(self): @pytest.mark.usefixtures("dt_parser") class TestDateTimeParserMonthName: def test_shortmonth_capitalized(self): - assert self.parser.parse("2013-Jan-01", "YYYY-MMM-DD") == datetime(2013, 1, 1) def test_shortmonth_allupper(self): - assert self.parser.parse("2013-JAN-01", "YYYY-MMM-DD") == datetime(2013, 1, 1) def test_shortmonth_alllower(self): - assert self.parser.parse("2013-jan-01", "YYYY-MMM-DD") == datetime(2013, 1, 1) def test_month_capitalized(self): - assert self.parser.parse("2013-January-01", "YYYY-MMMM-DD") == datetime( 2013, 1, 1 ) def test_month_allupper(self): - assert self.parser.parse("2013-JANUARY-01", "YYYY-MMMM-DD") == datetime( 2013, 1, 1 ) def test_month_alllower(self): - assert self.parser.parse("2013-january-01", "YYYY-MMMM-DD") == datetime( 2013, 1, 1 ) @@ -1574,13 +1513,11 @@ def test_french(self): @pytest.mark.usefixtures("dt_parser") class TestDateTimeParserSearchDate: def test_parse_search(self): - assert self.parser.parse( "Today is 25 of September of 2003", "DD of MMMM of YYYY" ) == datetime(2003, 9, 25) def test_parse_search_with_numbers(self): - assert self.parser.parse( "2000 people met the 2012-01-01 12:05:10", "YYYY-MM-DD HH:mm:ss" ) == datetime(2012, 1, 1, 12, 5, 10) @@ -1590,7 +1527,6 @@ def test_parse_search_with_numbers(self): ) == datetime(1979, 1, 1, 12, 5, 10) def test_parse_search_with_names(self): - assert self.parser.parse("June was born in May 1980", "MMMM YYYY") == datetime( 1980, 5, 1 ) @@ -1607,12 +1543,10 @@ def test_parse_search_locale_with_names(self): ) def test_parse_search_fails(self): - with pytest.raises(parser.ParserError): self.parser.parse("Jag föddes den 25 Augusti 1975", "DD MMMM YYYY") def test_escape(self): - format = "MMMM D, YYYY [at] h:mma" assert self.parser.parse( "Thursday, December 10, 2015 at 5:09pm", format diff --git a/tox.ini b/tox.ini index 11d70cb2..03c4d2af 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] minversion = 3.18.0 -envlist = py{py3,36,37,38,39,310,311} +envlist = py{py3,38,39,310,311,312} skip_missing_interpreters = true [gh-actions] @@ -12,6 +12,7 @@ python = 3.9: py39 3.10: py310 3.11: py311 + 3.12: py312 [testenv] deps = -r requirements/requirements-tests.txt From 1b957339101e9bd8cd7abe1aa5ddf8ede98d56cf Mon Sep 17 00:00:00 2001 From: ville <118881666+vreima@users.noreply.github.com> Date: Sat, 30 Sep 2023 23:55:54 +0300 Subject: [PATCH 29/80] Added translation for weeks in Finnish locale (#1157) Co-authored-by: Jad Chaar --- arrow/locales.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/arrow/locales.py b/arrow/locales.py index b2e7ee03..fe8d4395 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -873,6 +873,8 @@ class FinnishLocale(Locale): "hours": {"past": "{0} tuntia", "future": "{0} tunnin"}, "day": {"past": "päivä", "future": "päivän"}, "days": {"past": "{0} päivää", "future": "{0} päivän"}, + "week": {"past": "viikko", "future": "viikon"}, + "weeks": {"past": "{0} viikkoa", "future": "{0} viikon"}, "month": {"past": "kuukausi", "future": "kuukauden"}, "months": {"past": "{0} kuukautta", "future": "{0} kuukauden"}, "year": {"past": "vuosi", "future": "vuoden"}, From 3a6cd95389dfe1c93684aa083a6e3db408269fdb Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Sat, 30 Sep 2023 14:04:59 -0700 Subject: [PATCH 30/80] Add python dateutil types --- .pre-commit-config.yaml | 2 +- requirements/requirements.txt | 1 + setup.cfg | 2 +- setup.py | 5 ++++- 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b8a60a2c..b99f4c48 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -22,7 +22,7 @@ repos: hooks: - id: isort - repo: https://github.com/asottile/pyupgrade - rev: v3.10.1 + rev: v3.13.0 hooks: - id: pyupgrade args: [--py36-plus] diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 65134a19..7d97ff8e 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -1 +1,2 @@ python-dateutil>=2.7.0 +types-python-dateutil>=2.8.10 diff --git a/setup.cfg b/setup.cfg index 6ffbd02b..916477e5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [mypy] -python_version = 3.6 +python_version = 3.11 show_error_codes = True pretty = True diff --git a/setup.py b/setup.py index a2d8921e..d58b9f3b 100644 --- a/setup.py +++ b/setup.py @@ -22,7 +22,10 @@ package_data={"arrow": ["py.typed"]}, zip_safe=False, python_requires=">=3.8", - install_requires=["python-dateutil>=2.7.0"], + install_requires=[ + "python-dateutil>=2.7.0", + "types-python-dateutil>=2.8.10", + ], classifiers=[ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", From 522a65b16742a76b21154efaff3c3768e95a9bcc Mon Sep 17 00:00:00 2001 From: Abdullah Alajmi <16920216+AbdullahAlajmi@users.noreply.github.com> Date: Sun, 1 Oct 2023 00:07:00 +0300 Subject: [PATCH 31/80] add 'week' and 'weeks' to Arabic locale (#1155) Co-authored-by: Jad Chaar --- arrow/locales.py | 2 ++ tests/test_locales.py | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/arrow/locales.py b/arrow/locales.py index fe8d4395..34b2a098 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -2549,6 +2549,8 @@ class ArabicLocale(Locale): "hours": {"2": "ساعتين", "ten": "{0} ساعات", "higher": "{0} ساعة"}, "day": "يوم", "days": {"2": "يومين", "ten": "{0} أيام", "higher": "{0} يوم"}, + "week": "اسبوع", + "weeks": {"2": "اسبوعين", "ten": "{0} أسابيع", "higher": "{0} اسبوع"}, "month": "شهر", "months": {"2": "شهرين", "ten": "{0} أشهر", "higher": "{0} شهر"}, "year": "سنة", diff --git a/tests/test_locales.py b/tests/test_locales.py index d6abb2a1..cd14274e 100644 --- a/tests/test_locales.py +++ b/tests/test_locales.py @@ -1414,6 +1414,7 @@ def test_timeframes(self): assert self.locale._format_timeframe("minute", 1) == "دقيقة" assert self.locale._format_timeframe("hour", 1) == "ساعة" assert self.locale._format_timeframe("day", 1) == "يوم" + assert self.locale._format_timeframe("week", 1) == "اسبوع" assert self.locale._format_timeframe("month", 1) == "شهر" assert self.locale._format_timeframe("year", 1) == "سنة" @@ -1421,6 +1422,7 @@ def test_timeframes(self): assert self.locale._format_timeframe("minutes", 2) == "دقيقتين" assert self.locale._format_timeframe("hours", 2) == "ساعتين" assert self.locale._format_timeframe("days", 2) == "يومين" + assert self.locale._format_timeframe("weeks", 2) == "اسبوعين" assert self.locale._format_timeframe("months", 2) == "شهرين" assert self.locale._format_timeframe("years", 2) == "سنتين" @@ -1428,12 +1430,14 @@ def test_timeframes(self): assert self.locale._format_timeframe("minutes", 3) == "3 دقائق" assert self.locale._format_timeframe("hours", 4) == "4 ساعات" assert self.locale._format_timeframe("days", 5) == "5 أيام" + assert self.locale._format_timeframe("weeks", 7) == "7 أسابيع" assert self.locale._format_timeframe("months", 6) == "6 أشهر" assert self.locale._format_timeframe("years", 10) == "10 سنوات" # more than ten assert self.locale._format_timeframe("minutes", 11) == "11 دقيقة" assert self.locale._format_timeframe("hours", 19) == "19 ساعة" + assert self.locale._format_timeframe("weeks", 20) == "20 اسبوع" assert self.locale._format_timeframe("months", 24) == "24 شهر" assert self.locale._format_timeframe("days", 50) == "50 يوم" assert self.locale._format_timeframe("years", 115) == "115 سنة" From 431e9793be2d353f11b48532f9e96d5486f778f0 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Sat, 30 Sep 2023 14:49:15 -0700 Subject: [PATCH 32/80] Migrate to pyproject.toml (#1164) * Migrate to pyproject.toml * Downgrade pytz --- .github/workflows/release.yml | 30 +++++++++++++ MANIFEST.in | 4 -- Makefile | 16 +++---- pyproject.toml | 66 +++++++++++++++++++++++++++++ requirements/requirements-tests.txt | 2 - setup.py | 48 --------------------- tox.ini | 9 ++++ 7 files changed, 110 insertions(+), 65 deletions(-) create mode 100644 .github/workflows/release.yml delete mode 100644 MANIFEST.in create mode 100644 pyproject.toml delete mode 100644 setup.py diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..35e5fb73 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,30 @@ +name: release + +on: + workflow_dispatch: # run manually + push: # run on matching tags + tags: + - '*.*.*' + +jobs: + release-to-pypi: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/cache@v3 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements*.txt') }} + restore-keys: ${{ runner.os }}-pip- + - uses: actions/setup-python@v4 + with: + python-version: "3.11" + - name: Install dependencies + run: | + pip install -U pip setuptools wheel + pip install -U tox + - name: Publish package to PyPI + env: + FLIT_USERNAME: __token__ + FLIT_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} + run: tox -e publish diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 9abe9773..00000000 --- a/MANIFEST.in +++ /dev/null @@ -1,4 +0,0 @@ -include LICENSE CHANGELOG.rst README.rst Makefile tox.ini -recursive-include requirements *.txt -recursive-include tests *.py -recursive-include docs *.py *.rst *.bat Makefile diff --git a/Makefile b/Makefile index 27d5cbe4..9db6fba0 100644 --- a/Makefile +++ b/Makefile @@ -23,7 +23,7 @@ test: lint: . venv/bin/activate; \ - pre-commit run --all-files + pre-commit run --all-files --show-diff-on-failure clean-docs: rm -rf docs/_build @@ -42,15 +42,9 @@ clean: clean-dist rm -f .coverage coverage.xml ./**/*.pyc clean-dist: - rm -rf dist build .egg .eggs arrow.egg-info + rm -rf dist build *.egg *.eggs *.egg-info -build-dist: +build-dist: clean-dist . venv/bin/activate; \ - pip install -U pip setuptools twine wheel; \ - python setup.py sdist bdist_wheel - -upload-dist: - . venv/bin/activate; \ - twine upload dist/* - -publish: test clean-dist build-dist upload-dist clean-dist + pip install -U flit; \ + flit build diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..5ccc497a --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,66 @@ +[build-system] +requires = ["flit_core >=3.2,<4"] +build-backend = "flit_core.buildapi" + +[project] +name = "arrow" +authors = [{name = "Chris Smith", email = "crsmithdev@gmail.com"}] +readme = "README.rst" +license = {file = "LICENSE"} +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "Intended Audience :: Information Technology", + "License :: OSI Approved :: Apache Software License", + "Topic :: Software Development :: Libraries :: Python Modules", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Operating System :: OS Independent", +] +dependencies = [ + "python-dateutil>=2.7.0", + "types-python-dateutil>=2.8.10", +] +requires-python = ">=3.8" +description = "Better dates & times for Python" +keywords = [ + "arrow", + "date", + "time", + "datetime", + "timestamp", + "timezone", + "humanize", +] +dynamic = ["version"] + +[project.optional-dependencies] +test = [ + "dateparser==1.*", + "pre-commit", + "pytest", + "pytest-cov", + "pytest-mock", + "pytz==2021.1", + "simplejson==3.*", +] +doc = [ + "doc8", + "sphinx>=7.0.0", + "sphinx-autobuild", + "sphinx-autodoc-typehints", + "sphinx_rtd_theme>=1.3.0", +] + +[project.urls] +Documentation = "https://arrow.readthedocs.io" +Source = "https://github.com/arrow-py/arrow" +Issues = "https://github.com/arrow-py/arrow/issues" + +[tool.flit.module] +name = "arrow" diff --git a/requirements/requirements-tests.txt b/requirements/requirements-tests.txt index 7e9fbe3f..77005e0b 100644 --- a/requirements/requirements-tests.txt +++ b/requirements/requirements-tests.txt @@ -4,7 +4,5 @@ pre-commit pytest pytest-cov pytest-mock -python-dateutil>=2.7.0 pytz==2021.1 simplejson==3.* -typing_extensions; python_version < '3.8' diff --git a/setup.py b/setup.py deleted file mode 100644 index d58b9f3b..00000000 --- a/setup.py +++ /dev/null @@ -1,48 +0,0 @@ -# mypy: ignore-errors -from pathlib import Path - -from setuptools import setup - -readme = Path("README.rst").read_text(encoding="utf-8") -version = Path("arrow/_version.py").read_text(encoding="utf-8") -about = {} -exec(version, about) - -setup( - name="arrow", - version=about["__version__"], - description="Better dates & times for Python", - long_description=readme, - long_description_content_type="text/x-rst", - url="https://arrow.readthedocs.io", - author="Chris Smith", - author_email="crsmithdev@gmail.com", - license="Apache 2.0", - packages=["arrow"], - package_data={"arrow": ["py.typed"]}, - zip_safe=False, - python_requires=">=3.8", - install_requires=[ - "python-dateutil>=2.7.0", - "types-python-dateutil>=2.8.10", - ], - classifiers=[ - "Development Status :: 5 - Production/Stable", - "Intended Audience :: Developers", - "License :: OSI Approved :: Apache Software License", - "Topic :: Software Development :: Libraries :: Python Modules", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - ], - keywords="arrow date time datetime timestamp timezone humanize", - project_urls={ - "Repository": "https://github.com/arrow-py/arrow", - "Bug Reports": "https://github.com/arrow-py/arrow/issues", - "Documentation": "https://arrow.readthedocs.io", - }, -) diff --git a/tox.ini b/tox.ini index 03c4d2af..c410a8e5 100644 --- a/tox.ini +++ b/tox.ini @@ -36,6 +36,15 @@ commands = doc8 index.rst ../README.rst --extension .rst --ignore D001 make html SPHINXOPTS="-W --keep-going" +[testenv:publish] +passenv = * +skip_install = true +deps = + -r requirements/requirements.txt + flit +allowlist_externals = flit +commands = flit publish --setup-py + [pytest] addopts = -v --cov-branch --cov=arrow --cov-fail-under=99 --cov-report=term-missing --cov-report=xml testpaths = tests From 87a1a774aad0505d9da18ad1d16f6e571f262503 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Sat, 30 Sep 2023 15:03:06 -0700 Subject: [PATCH 33/80] Bump version and add changelog --- CHANGELOG.rst | 14 +++++++++++++- arrow/_version.py | 2 +- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 5b079798..b5daf6ed 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,12 +1,24 @@ Changelog ========= +1.3.0 (2023-09-30) +------------------ + +- [ADDED] Added official support for Python 3.11 and 3.12. +- [ADDED] Added dependency on ``types-python-dateutil`` to improve Arrow mypy compatibility. `PR #1102 `_ +- [FIX] Updates to Italian, Romansh, Hungarian, Finish and Arabic locales. +- [FIX] Handling parsing of UTC prefix in timezone strings. +- [CHANGED] Update documentation to improve readability. +- [CHANGED] Dropped support for Python 3.6 and 3.7, which are end-of-life. +- [INTERNAL] Migrate from ``setup.py``/Twine to ``pyproject.toml``/Flit for packaging and distribution. +- [INTERNAL] Adopt ``.readthedocs.yaml`` configuration file for continued ReadTheDocs support. + 1.2.3 (2022-06-25) ------------------ - [NEW] Added Amharic, Armenian, Georgian, Laotian and Uzbek locales. - [FIX] Updated Danish locale and associated tests. -- [INTERNAl] Small fixes to CI. +- [INTERNAL] Small fixes to CI. 1.2.2 (2022-01-19) ------------------ diff --git a/arrow/_version.py b/arrow/_version.py index 10aa336c..67bc602a 100644 --- a/arrow/_version.py +++ b/arrow/_version.py @@ -1 +1 @@ -__version__ = "1.2.3" +__version__ = "1.3.0" From 41717e5e0e495b6d78a54a2fbee85b629832ad44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Klinkovsk=C3=BD?= Date: Sun, 23 Jun 2024 18:31:16 +0200 Subject: [PATCH 34/80] Remove typing_extensions (#1176) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit typing_extensions was used for python<3.8, but this package requires python>=3.8 Co-authored-by: Jakub Klinkovský <1289205+lahwaacz@users.noreply.github.com> --- arrow/arrow.py | 8 ++------ arrow/constants.py | 6 +----- arrow/formatter.py | 9 +-------- arrow/locales.py | 7 +------ arrow/parser.py | 8 ++------ 5 files changed, 7 insertions(+), 31 deletions(-) diff --git a/arrow/arrow.py b/arrow/arrow.py index 8d329efd..6c617ae7 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -18,9 +18,11 @@ from typing import ( Any, ClassVar, + Final, Generator, Iterable, List, + Literal, Mapping, Optional, Tuple, @@ -36,12 +38,6 @@ from arrow.constants import DEFAULT_LOCALE, DEHUMANIZE_LOCALES from arrow.locales import TimeFrameLiteral -if sys.version_info < (3, 8): # pragma: no cover - from typing_extensions import Final, Literal -else: - from typing import Final, Literal # pragma: no cover - - TZ_EXPR = Union[dt_tzinfo, str] _T_FRAMES = Literal[ diff --git a/arrow/constants.py b/arrow/constants.py index 53d163b9..532e9596 100644 --- a/arrow/constants.py +++ b/arrow/constants.py @@ -2,11 +2,7 @@ import sys from datetime import datetime - -if sys.version_info < (3, 8): # pragma: no cover - from typing_extensions import Final -else: - from typing import Final # pragma: no cover +from typing import Final # datetime.max.timestamp() errors on Windows, so we must hardcode # the highest possible datetime value that can output a timestamp. diff --git a/arrow/formatter.py b/arrow/formatter.py index d45f7153..8cd61e90 100644 --- a/arrow/formatter.py +++ b/arrow/formatter.py @@ -1,21 +1,14 @@ """Provides the :class:`Arrow ` class, an improved formatter for datetimes.""" import re -import sys from datetime import datetime, timedelta -from typing import Optional, Pattern, cast +from typing import Final, Optional, Pattern, cast from dateutil import tz as dateutil_tz from arrow import locales from arrow.constants import DEFAULT_LOCALE -if sys.version_info < (3, 8): # pragma: no cover - from typing_extensions import Final -else: - from typing import Final # pragma: no cover - - FORMAT_ATOM: Final[str] = "YYYY-MM-DD HH:mm:ssZZ" FORMAT_COOKIE: Final[str] = "dddd, DD-MMM-YYYY HH:mm:ss ZZZ" FORMAT_RFC822: Final[str] = "ddd, DD MMM YY HH:mm:ss Z" diff --git a/arrow/locales.py b/arrow/locales.py index 34b2a098..cd00b035 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -1,12 +1,12 @@ """Provides internationalization for arrow in over 60 languages and dialects.""" -import sys from math import trunc from typing import ( Any, ClassVar, Dict, List, + Literal, Mapping, Optional, Sequence, @@ -16,11 +16,6 @@ cast, ) -if sys.version_info < (3, 8): # pragma: no cover - from typing_extensions import Literal -else: - from typing import Literal # pragma: no cover - TimeFrameLiteral = Literal[ "now", "second", diff --git a/arrow/parser.py b/arrow/parser.py index 645e3da7..b794f1b7 100644 --- a/arrow/parser.py +++ b/arrow/parser.py @@ -1,7 +1,6 @@ """Provides the :class:`Arrow ` class, a better way to parse datetime strings.""" import re -import sys from datetime import datetime, timedelta from datetime import tzinfo as dt_tzinfo from functools import lru_cache @@ -11,12 +10,14 @@ Dict, Iterable, List, + Literal, Match, Optional, Pattern, SupportsFloat, SupportsInt, Tuple, + TypedDict, Union, cast, overload, @@ -28,11 +29,6 @@ from arrow.constants import DEFAULT_LOCALE from arrow.util import next_weekday, normalize_timestamp -if sys.version_info < (3, 8): # pragma: no cover - from typing_extensions import Literal, TypedDict -else: - from typing import Literal, TypedDict # pragma: no cover - class ParserError(ValueError): pass From 2bd89ab7cb8eb9e71b33e855c92c1d328ebedfe3 Mon Sep 17 00:00:00 2001 From: Tom Sarantis Date: Tue, 9 Jul 2024 14:48:10 +1000 Subject: [PATCH 35/80] add a doc link for arrow.format (#1180) --- arrow/arrow.py | 3 ++- docs/guide.rst | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/arrow/arrow.py b/arrow/arrow.py index 6c617ae7..78285256 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -1088,7 +1088,8 @@ def format( self, fmt: str = "YYYY-MM-DD HH:mm:ssZZ", locale: str = DEFAULT_LOCALE ) -> str: """Returns a string representation of the :class:`Arrow ` object, - formatted according to the provided format string. + formatted according to the provided format string. For a list of formatting values, + see :ref:`supported-tokens` :param fmt: the format string. :param locale: the locale to format. diff --git a/docs/guide.rst b/docs/guide.rst index aef0e880..5bdf337d 100644 --- a/docs/guide.rst +++ b/docs/guide.rst @@ -156,6 +156,8 @@ Move between the earlier and later moments of an ambiguous time: Format ~~~~~~ +For a list of formatting values, see :ref:`supported-tokens` + .. code-block:: python >>> arrow.utcnow().format('YYYY-MM-DD HH:mm:ss ZZ') @@ -365,6 +367,8 @@ Then get and use a factory for it: >>> custom.days_till_xmas() >>> 211 +.. _supported-tokens: + Supported Tokens ~~~~~~~~~~~~~~~~ From 7225592f8e1d85ecc49ff0ad4b4291386520802f Mon Sep 17 00:00:00 2001 From: James Page Date: Sat, 24 Aug 2024 01:43:24 +0100 Subject: [PATCH 36/80] Move dateutil types to test requirements (#1183) types-python-dateutils is only needed when running mypy static type checking; move from runtime to test requirements. --- pyproject.toml | 1 - requirements/requirements-tests.txt | 1 + requirements/requirements.txt | 1 - 3 files changed, 1 insertion(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 5ccc497a..89609bd1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,6 @@ classifiers = [ ] dependencies = [ "python-dateutil>=2.7.0", - "types-python-dateutil>=2.8.10", ] requires-python = ">=3.8" description = "Better dates & times for Python" diff --git a/requirements/requirements-tests.txt b/requirements/requirements-tests.txt index 77005e0b..1f2a2ec4 100644 --- a/requirements/requirements-tests.txt +++ b/requirements/requirements-tests.txt @@ -6,3 +6,4 @@ pytest-cov pytest-mock pytz==2021.1 simplejson==3.* +types-python-dateutil>=2.8.10 diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 7d97ff8e..65134a19 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -1,2 +1 @@ python-dateutil>=2.7.0 -types-python-dateutil>=2.8.10 From 587af5fbe949e18d60387b0b7cb1e14855e85401 Mon Sep 17 00:00:00 2001 From: Thang Do <5171823+csessh@users.noreply.github.com> Date: Sat, 24 Aug 2024 10:19:29 +0930 Subject: [PATCH 37/80] #1178: changes to address datetime.utcnow deprecation warning (#1182) Co-authored-by: Jad Chaar --- arrow/arrow.py | 4 ++-- tests/test_arrow.py | 14 +++++++++----- tests/test_factory.py | 20 ++++++++++---------- tests/test_formatter.py | 4 ++-- tests/test_parser.py | 4 ++-- tests/test_util.py | 4 ++-- 6 files changed, 27 insertions(+), 23 deletions(-) diff --git a/arrow/arrow.py b/arrow/arrow.py index 78285256..c3f208df 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -11,7 +11,7 @@ from datetime import date from datetime import datetime as dt_datetime from datetime import time as dt_time -from datetime import timedelta +from datetime import timedelta, timezone from datetime import tzinfo as dt_tzinfo from math import trunc from time import struct_time @@ -1144,7 +1144,7 @@ def humanize( locale = locales.get_locale(locale) if other is None: - utc = dt_datetime.utcnow().replace(tzinfo=dateutil_tz.tzutc()) + utc = dt_datetime.now(timezone.utc).replace(tzinfo=dateutil_tz.tzutc()) dt = utc.astimezone(self._datetime.tzinfo) elif isinstance(other, Arrow): diff --git a/tests/test_arrow.py b/tests/test_arrow.py index 507c1ab0..becdd53a 100644 --- a/tests/test_arrow.py +++ b/tests/test_arrow.py @@ -1,7 +1,7 @@ import pickle import sys import time -from datetime import date, datetime, timedelta +from datetime import date, datetime, timedelta, timezone from typing import List import dateutil @@ -91,7 +91,7 @@ def test_utcnow(self): result = arrow.Arrow.utcnow() assert_datetime_equality( - result._datetime, datetime.utcnow().replace(tzinfo=tz.tzutc()) + result._datetime, datetime.now(timezone.utc).replace(tzinfo=tz.tzutc()) ) assert result.fold == 0 @@ -124,7 +124,7 @@ def test_utcfromtimestamp(self): result = arrow.Arrow.utcfromtimestamp(timestamp) assert_datetime_equality( - result._datetime, datetime.utcnow().replace(tzinfo=tz.tzutc()) + result._datetime, datetime.now(timezone.utc).replace(tzinfo=tz.tzutc()) ) with pytest.raises(ValueError): @@ -1055,7 +1055,11 @@ def test_imaginary(self): def test_unsupported(self): with pytest.raises(ValueError): - next(arrow.Arrow.range("abc", datetime.utcnow(), datetime.utcnow())) + next( + arrow.Arrow.range( + "abc", datetime.now(timezone.utc), datetime.now(timezone.utc) + ) + ) def test_range_over_months_ending_on_different_days(self): # regression test for issue #842 @@ -2889,7 +2893,7 @@ def test_get_datetime(self): get_datetime = arrow.Arrow._get_datetime arw = arrow.Arrow.utcnow() - dt = datetime.utcnow() + dt = datetime.now(timezone.utc) timestamp = time.time() assert get_datetime(arw) == arw.datetime diff --git a/tests/test_factory.py b/tests/test_factory.py index 4e328000..0ee9c4e0 100644 --- a/tests/test_factory.py +++ b/tests/test_factory.py @@ -1,5 +1,5 @@ import time -from datetime import date, datetime +from datetime import date, datetime, timezone from decimal import Decimal import pytest @@ -15,7 +15,7 @@ class TestGet: def test_no_args(self): assert_datetime_equality( - self.factory.get(), datetime.utcnow().replace(tzinfo=tz.tzutc()) + self.factory.get(), datetime.now(timezone.utc).replace(tzinfo=tz.tzutc()) ) def test_timestamp_one_arg_no_arg(self): @@ -31,7 +31,7 @@ def test_one_arg_none(self): def test_struct_time(self): assert_datetime_equality( self.factory.get(time.gmtime()), - datetime.utcnow().replace(tzinfo=tz.tzutc()), + datetime.now(timezone.utc).replace(tzinfo=tz.tzutc()), ) def test_one_arg_timestamp(self): @@ -91,7 +91,7 @@ def test_one_arg_arrow(self): assert arw == result def test_one_arg_datetime(self): - dt = datetime.utcnow().replace(tzinfo=tz.tzutc()) + dt = datetime.now(timezone.utc).replace(tzinfo=tz.tzutc()) assert self.factory.get(dt) == dt @@ -103,7 +103,7 @@ def test_one_arg_date(self): def test_one_arg_tzinfo(self): self.expected = ( - datetime.utcnow() + datetime.now(timezone.utc) .replace(tzinfo=tz.tzutc()) .astimezone(tz.gettz("US/Pacific")) ) @@ -123,7 +123,7 @@ def test_one_arg_dateparser_datetime(self): def test_kwarg_tzinfo(self): self.expected = ( - datetime.utcnow() + datetime.now(timezone.utc) .replace(tzinfo=tz.tzutc()) .astimezone(tz.gettz("US/Pacific")) ) @@ -134,7 +134,7 @@ def test_kwarg_tzinfo(self): def test_kwarg_tzinfo_string(self): self.expected = ( - datetime.utcnow() + datetime.now(timezone.utc) .replace(tzinfo=tz.tzutc()) .astimezone(tz.gettz("US/Pacific")) ) @@ -199,7 +199,7 @@ def test_one_arg_iso_calendar_tzinfo_kwarg(self): assert_datetime_equality(result, expected) def test_one_arg_iso_str(self): - dt = datetime.utcnow() + dt = datetime.now(timezone.utc) assert_datetime_equality( self.factory.get(dt.isoformat()), dt.replace(tzinfo=tz.tzutc()) @@ -273,7 +273,7 @@ def test_two_args_date_tz_str(self): def test_two_args_datetime_other(self): with pytest.raises(TypeError): - self.factory.get(datetime.utcnow(), object()) + self.factory.get(datetime.now(timezone.utc), object()) def test_two_args_date_other(self): with pytest.raises(TypeError): @@ -374,7 +374,7 @@ class TestUtcNow: def test_utcnow(self): assert_datetime_equality( self.factory.utcnow()._datetime, - datetime.utcnow().replace(tzinfo=tz.tzutc()), + datetime.now(timezone.utc).replace(tzinfo=tz.tzutc()), ) diff --git a/tests/test_formatter.py b/tests/test_formatter.py index 0b6c256c..36e15d9f 100644 --- a/tests/test_formatter.py +++ b/tests/test_formatter.py @@ -1,4 +1,4 @@ -from datetime import datetime +from datetime import datetime, timezone import pytest import pytz @@ -113,7 +113,7 @@ def test_timestamp(self): assert self.formatter._format_token(dt, "x") == expected def test_timezone(self): - dt = datetime.utcnow().replace(tzinfo=dateutil_tz.gettz("US/Pacific")) + dt = datetime.now(timezone.utc).replace(tzinfo=dateutil_tz.gettz("US/Pacific")) result = self.formatter._format_token(dt, "ZZ") assert result == "-07:00" or result == "-08:00" diff --git a/tests/test_parser.py b/tests/test_parser.py index 1932b450..3eb44d16 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -1,7 +1,7 @@ import calendar import os import time -from datetime import datetime +from datetime import datetime, timezone import pytest from dateutil import tz @@ -1149,7 +1149,7 @@ def test_gnu_date(self): ) def test_isoformat(self): - dt = datetime.utcnow() + dt = datetime.now(timezone.utc) assert self.parser.parse_iso(dt.isoformat()) == dt diff --git a/tests/test_util.py b/tests/test_util.py index 3b32e1bc..2454dac5 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -1,5 +1,5 @@ import time -from datetime import datetime +from datetime import datetime, timezone import pytest @@ -87,7 +87,7 @@ def test_validate_ordinal(self): except (ValueError, TypeError) as exp: pytest.fail(f"Exception raised when shouldn't have ({type(exp)}).") - ordinal = datetime.utcnow().toordinal() + ordinal = datetime.now(timezone.utc).toordinal() ordinal_str = str(ordinal) ordinal_float = float(ordinal) + 0.5 From 02e3b1b4c56b905a13f7ae3f552728a5a17fc5bd Mon Sep 17 00:00:00 2001 From: Nikolaos Pothitos Date: Mon, 26 Aug 2024 03:41:56 +0300 Subject: [PATCH 38/80] Fix "ago" Greek translation and month typo (#1184) --- arrow/locales.py | 4 ++-- tests/test_locales.py | 11 +++++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/arrow/locales.py b/arrow/locales.py index cd00b035..d29d9f45 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -654,7 +654,7 @@ class FrenchCanadianLocale(FrenchBaseLocale, Locale): class GreekLocale(Locale): names = ["el", "el-gr"] - past = "{0} πριν" + past = "πριν από {0}" future = "σε {0}" and_word = "και" @@ -697,7 +697,7 @@ class GreekLocale(Locale): "Φεβ", "Μαρ", "Απρ", - "Μαϊ", + "Μαΐ", "Ιον", "Ιολ", "Αυγ", diff --git a/tests/test_locales.py b/tests/test_locales.py index cd14274e..3e0035b0 100644 --- a/tests/test_locales.py +++ b/tests/test_locales.py @@ -3178,3 +3178,14 @@ def test_plurals_mk(self): assert self.locale._format_timeframe("months", 11) == "11 oy" assert self.locale._format_timeframe("year", 1) == "bir yil" assert self.locale._format_timeframe("years", 12) == "12 yil" + + +@pytest.mark.usefixtures("lang_locale") +class TestGreekLocale: + def test_format_relative_future(self): + result = self.locale._format_relative("μία ώρα", "ώρα", -1) + + assert result == "πριν από μία ώρα" # an hour ago + + def test_month_abbreviation(self): + assert self.locale.month_abbreviations[5] == "Μαΐ" From f77aa1d4251df9b8d8b48e6bf8b1dbbe2c06a8f7 Mon Sep 17 00:00:00 2001 From: Nikolaos Pothitos Date: Tue, 27 Aug 2024 05:41:18 +0300 Subject: [PATCH 39/80] Improve "second" and "day(s)" translation to Greek (#1186) --- arrow/locales.py | 6 +++--- tests/test_locales.py | 6 ++++++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/arrow/locales.py b/arrow/locales.py index d29d9f45..c1b7869b 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -660,14 +660,14 @@ class GreekLocale(Locale): timeframes = { "now": "τώρα", - "second": "ένα δεύτερο", + "second": "ένα δευτερόλεπτο", "seconds": "{0} δευτερόλεπτα", "minute": "ένα λεπτό", "minutes": "{0} λεπτά", "hour": "μία ώρα", "hours": "{0} ώρες", - "day": "μία μέρα", - "days": "{0} μέρες", + "day": "μία ημέρα", + "days": "{0} ημέρες", "week": "μία εβδομάδα", "weeks": "{0} εβδομάδες", "month": "ένα μήνα", diff --git a/tests/test_locales.py b/tests/test_locales.py index 3e0035b0..2122b9c7 100644 --- a/tests/test_locales.py +++ b/tests/test_locales.py @@ -3189,3 +3189,9 @@ def test_format_relative_future(self): def test_month_abbreviation(self): assert self.locale.month_abbreviations[5] == "Μαΐ" + + def test_format_timeframe(self): + assert self.locale._format_timeframe("second", 1) == "ένα δευτερόλεπτο" + assert self.locale._format_timeframe("seconds", 3) == "3 δευτερόλεπτα" + assert self.locale._format_timeframe("day", 1) == "μία ημέρα" + assert self.locale._format_timeframe("days", 6) == "6 ημέρες" From 96a88a61714722d602a0dbb528cf89aed7007c97 Mon Sep 17 00:00:00 2001 From: Klaus Rettinghaus Date: Fri, 18 Oct 2024 05:33:40 +0200 Subject: [PATCH 40/80] feat: add dependabot for GH actions (#1193) * feat: add dependabot for GH actions * fix: change frequency to weekly --- .github/dependabot.yml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..5ace4600 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" From 136620988c43daf863aae481e0248d07a4012331 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 17 Oct 2024 20:44:29 -0700 Subject: [PATCH 41/80] Bump actions/checkout from 3 to 4 (#1198) Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/continuous_integration.yml | 4 ++-- .github/workflows/release.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/continuous_integration.yml b/.github/workflows/continuous_integration.yml index 29b8df6d..f26cb6eb 100644 --- a/.github/workflows/continuous_integration.yml +++ b/.github/workflows/continuous_integration.yml @@ -29,7 +29,7 @@ jobs: - os: windows-latest path: ~\AppData\Local\pip\Cache steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Cache pip uses: actions/cache@v3 with: @@ -55,7 +55,7 @@ jobs: name: Linting runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: actions/cache@v3 with: path: ~/.cache/pip diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 35e5fb73..4eb0f225 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -10,7 +10,7 @@ jobs: release-to-pypi: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: actions/cache@v3 with: path: ~/.cache/pip From 6321d81485566cfe5878834e01b5e17cbe4a815b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 17 Oct 2024 20:52:48 -0700 Subject: [PATCH 42/80] Bump actions/cache from 3 to 4 (#1195) Bumps [actions/cache](https://github.com/actions/cache) from 3 to 4. - [Release notes](https://github.com/actions/cache/releases) - [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md) - [Commits](https://github.com/actions/cache/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/cache dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/continuous_integration.yml | 6 +++--- .github/workflows/release.yml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/continuous_integration.yml b/.github/workflows/continuous_integration.yml index f26cb6eb..383b7dc4 100644 --- a/.github/workflows/continuous_integration.yml +++ b/.github/workflows/continuous_integration.yml @@ -31,7 +31,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Cache pip - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ${{ matrix.path }} key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements*.txt') }} @@ -56,12 +56,12 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/cache@v3 + - uses: actions/cache@v4 with: path: ~/.cache/pip key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements*.txt') }} restore-keys: ${{ runner.os }}-pip- - - uses: actions/cache@v3 + - uses: actions/cache@v4 with: path: ~/.cache/pre-commit key: ${{ runner.os }}-pre-commit-${{ hashFiles('**/.pre-commit-config.yaml') }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4eb0f225..0c5006d3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/cache@v3 + - uses: actions/cache@v4 with: path: ~/.cache/pip key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements*.txt') }} From 403c29fd48b1a5f306f6523b6c82faf0f78d22b2 Mon Sep 17 00:00:00 2001 From: Yuktha P S <140039720+psyuktha@users.noreply.github.com> Date: Sat, 19 Oct 2024 20:21:46 +0530 Subject: [PATCH 43/80] Update shift() for issue #1145 (#1194) * Update shift() added check_imaginary parameter to the function * added tests to issue #1145 and Format code with black * Update arrow.py --- arrow/arrow.py | 18 +++++++++++++----- tests/test_arrow.py | 10 ++++++++++ 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/arrow/arrow.py b/arrow/arrow.py index c3f208df..ee959d7d 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -492,7 +492,7 @@ def range( values = [getattr(current, f) for f in cls._ATTRS] current = cls(*values, tzinfo=tzinfo).shift( # type: ignore[misc] - **{frame_relative: relative_steps} + check_imaginary=True, **{frame_relative: relative_steps} ) if frame in ["month", "quarter", "year"] and current.day < original_day: @@ -583,7 +583,9 @@ def span( elif frame_absolute == "quarter": floor = floor.shift(months=-((self.month - 1) % 3)) - ceil = floor.shift(**{frame_relative: count * relative_steps}) + ceil = floor.shift( + check_imaginary=True, **{frame_relative: count * relative_steps} + ) if bounds[0] == "(": floor = floor.shift(microseconds=+1) @@ -981,10 +983,15 @@ def replace(self, **kwargs: Any) -> "Arrow": return self.fromdatetime(current) - def shift(self, **kwargs: Any) -> "Arrow": + def shift(self, check_imaginary: bool = True, **kwargs: Any) -> "Arrow": """Returns a new :class:`Arrow ` object with attributes updated according to inputs. + Parameters: + check_imaginary (bool): If True (default), will check for and resolve + imaginary times (like during DST transitions). If False, skips this check. + + Use pluralized property names to relatively shift their current value: >>> import arrow @@ -1031,7 +1038,8 @@ def shift(self, **kwargs: Any) -> "Arrow": current = self._datetime + relativedelta(**relative_kwargs) - if not dateutil_tz.datetime_exists(current): + # If check_imaginary is True, perform the check for imaginary times (DST transitions) + if check_imaginary and not dateutil_tz.datetime_exists(current): current = dateutil_tz.resolve_imaginary(current) return self.fromdatetime(current) @@ -1441,7 +1449,7 @@ def dehumanize(self, input_string: str, locale: str = "en_us") -> "Arrow": time_changes = {k: sign_val * v for k, v in time_object_info.items()} - return current_time.shift(**time_changes) + return current_time.shift(check_imaginary=True, **time_changes) # query functions diff --git a/tests/test_arrow.py b/tests/test_arrow.py index becdd53a..daf6209f 100644 --- a/tests/test_arrow.py +++ b/tests/test_arrow.py @@ -847,6 +847,16 @@ def test_shift_negative_imaginary(self): 2011, 12, 31, 23, tzinfo="Pacific/Apia" ) + def test_shift_with_imaginary_check(self): + dt = arrow.Arrow(2024, 3, 10, 2, 30, tzinfo=tz.gettz("US/Eastern")) + shifted = dt.shift(hours=1) + assert shifted.datetime.hour == 3 + + def test_shift_without_imaginary_check(self): + dt = arrow.Arrow(2024, 3, 10, 2, 30, tzinfo=tz.gettz("US/Eastern")) + shifted = dt.shift(hours=1, check_imaginary=False) + assert shifted.datetime.hour == 3 + @pytest.mark.skipif( dateutil.__version__ < "2.7.1", reason="old tz database (2018d needed)" ) From a68fc17c9a0e772b00e12de2713ead310465b585 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 19 Oct 2024 07:52:12 -0700 Subject: [PATCH 44/80] Bump actions/setup-python from 4 to 5 (#1197) Bumps [actions/setup-python](https://github.com/actions/setup-python) from 4 to 5. - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/setup-python dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/continuous_integration.yml | 4 ++-- .github/workflows/release.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/continuous_integration.yml b/.github/workflows/continuous_integration.yml index 383b7dc4..f9e96f01 100644 --- a/.github/workflows/continuous_integration.yml +++ b/.github/workflows/continuous_integration.yml @@ -37,7 +37,7 @@ jobs: key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements*.txt') }} restore-keys: ${{ runner.os }}-pip- - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies @@ -67,7 +67,7 @@ jobs: key: ${{ runner.os }}-pre-commit-${{ hashFiles('**/.pre-commit-config.yaml') }} restore-keys: ${{ runner.os }}-pre-commit- - name: Set up Python ${{ runner.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: "3.11" - name: Install dependencies diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0c5006d3..84adac2f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,7 +16,7 @@ jobs: path: ~/.cache/pip key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements*.txt') }} restore-keys: ${{ runner.os }}-pip- - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version: "3.11" - name: Install dependencies From 714db9c302c2d9a1cab0e1f70e30c337e5e78c67 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 19 Oct 2024 07:52:29 -0700 Subject: [PATCH 45/80] Bump codecov/codecov-action from 3 to 4 (#1196) Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 3 to 4. - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/v3...v4) --- updated-dependencies: - dependency-name: codecov/codecov-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/continuous_integration.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/continuous_integration.yml b/.github/workflows/continuous_integration.yml index f9e96f01..588bff25 100644 --- a/.github/workflows/continuous_integration.yml +++ b/.github/workflows/continuous_integration.yml @@ -47,7 +47,7 @@ jobs: - name: Test with tox run: tox - name: Upload coverage to Codecov - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 with: file: coverage.xml From cbe4949c49feb1501875818d7b63c15c9306f269 Mon Sep 17 00:00:00 2001 From: Yangs Date: Sat, 19 Oct 2024 23:53:28 +0900 Subject: [PATCH 46/80] =?UTF-8?q?Modifying=20Spelling=20`=EC=A0=9C?= =?UTF-8?q?=EC=9E=91=EB=85=84`,=20Remove=20poorly=20used=20expressions=20(?= =?UTF-8?q?#1181)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Modifying Spelling `제작년`, Remove poorly used expressions * add test Modifying Spelling `제작년`, Remove poorly used expressions --------- Co-authored-by: Jad Chaar --- arrow/locales.py | 3 +-- tests/test_locales.py | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/arrow/locales.py b/arrow/locales.py index c1b7869b..7f91b565 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -1140,7 +1140,6 @@ class KoreanLocale(Locale): } special_dayframes = { - -3: "그끄제", -2: "그제", -1: "어제", 1: "내일", @@ -1149,7 +1148,7 @@ class KoreanLocale(Locale): 4: "그글피", } - special_yearframes = {-2: "제작년", -1: "작년", 1: "내년", 2: "내후년"} + special_yearframes = {-2: "재작년", -1: "작년", 1: "내년", 2: "내후년"} month_names = [ "", diff --git a/tests/test_locales.py b/tests/test_locales.py index 2122b9c7..f58eb221 100644 --- a/tests/test_locales.py +++ b/tests/test_locales.py @@ -2390,14 +2390,14 @@ def test_format_relative(self): assert self.locale._format_relative("2시간", "hours", -2) == "2시간 전" assert self.locale._format_relative("하루", "day", -1) == "어제" assert self.locale._format_relative("2일", "days", -2) == "그제" - assert self.locale._format_relative("3일", "days", -3) == "그끄제" + assert self.locale._format_relative("3일", "days", -3) == "3일 전" assert self.locale._format_relative("4일", "days", -4) == "4일 전" assert self.locale._format_relative("1주", "week", -1) == "1주 전" assert self.locale._format_relative("2주", "weeks", -2) == "2주 전" assert self.locale._format_relative("한달", "month", -1) == "한달 전" assert self.locale._format_relative("2개월", "months", -2) == "2개월 전" assert self.locale._format_relative("1년", "year", -1) == "작년" - assert self.locale._format_relative("2년", "years", -2) == "제작년" + assert self.locale._format_relative("2년", "years", -2) == "재작년" assert self.locale._format_relative("3년", "years", -3) == "3년 전" def test_ordinal_number(self): From d5bd7db75d2337c6af59cde44e5e7b8d5065da9b Mon Sep 17 00:00:00 2001 From: Ori Avtalion Date: Sat, 19 Oct 2024 17:53:53 +0300 Subject: [PATCH 47/80] Fix type hint of Arrow.__getattr__ (#1171) Co-authored-by: Jad Chaar --- arrow/arrow.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/arrow/arrow.py b/arrow/arrow.py index ee959d7d..6e9c3cf7 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -800,7 +800,7 @@ def __hash__(self) -> int: # attributes and properties - def __getattr__(self, name: str) -> int: + def __getattr__(self, name: str) -> Any: if name == "week": return self.isocalendar()[1] @@ -808,7 +808,7 @@ def __getattr__(self, name: str) -> int: return int((self.month - 1) / self._MONTHS_PER_QUARTER) + 1 if not name.startswith("_"): - value: Optional[int] = getattr(self._datetime, name, None) + value: Optional[Any] = getattr(self._datetime, name, None) if value is not None: return value @@ -1867,7 +1867,7 @@ def _get_iteration_params(cls, end: Any, limit: Optional[int]) -> Tuple[Any, int @staticmethod def _is_last_day_of_month(date: "Arrow") -> bool: """Returns a boolean indicating whether the datetime is the last day of the month.""" - return date.day == calendar.monthrange(date.year, date.month)[1] + return cast(int, date.day) == calendar.monthrange(date.year, date.month)[1] Arrow.min = Arrow.fromdatetime(dt_datetime.min) From c78e35e8ee31c6f828638e147459447eb00f273c Mon Sep 17 00:00:00 2001 From: Amir <140071494+Crimson-Amir@users.noreply.github.com> Date: Tue, 22 Oct 2024 09:23:15 +0330 Subject: [PATCH 48/80] added week and quarter to persian/farsi (#1190) --- arrow/locales.py | 4 ++++ tests/test_locales.py | 21 +++++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/arrow/locales.py b/arrow/locales.py index 7f91b565..94f7902d 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -3318,6 +3318,10 @@ class FarsiLocale(Locale): "hours": "{0} ساعت", "day": "یک روز", "days": "{0} روز", + "week": "یک هفته", + "weeks": "{0} هفته", + "quarter": "یک فصل", + "quarters": "{0} فصل", "month": "یک ماه", "months": "{0} ماه", "year": "یک سال", diff --git a/tests/test_locales.py b/tests/test_locales.py index f58eb221..3eff7f44 100644 --- a/tests/test_locales.py +++ b/tests/test_locales.py @@ -1443,6 +1443,27 @@ def test_timeframes(self): assert self.locale._format_timeframe("years", 115) == "115 سنة" +@pytest.mark.usefixtures("lang_locale") +class TestFarsiLocale: + def test_timeframes(self): + assert self.locale._format_timeframe("now", 0) == "اکنون" + # single + assert self.locale._format_timeframe("minute", 1) == "یک دقیقه" + assert self.locale._format_timeframe("hour", 1) == "یک ساعت" + assert self.locale._format_timeframe("day", 1) == "یک روز" + assert self.locale._format_timeframe("week", 1) == "یک هفته" + assert self.locale._format_timeframe("month", 1) == "یک ماه" + assert self.locale._format_timeframe("year", 1) == "یک سال" + + # double + assert self.locale._format_timeframe("minutes", 2) == "2 دقیقه" + assert self.locale._format_timeframe("hours", 2) == "2 ساعت" + assert self.locale._format_timeframe("days", 2) == "2 روز" + assert self.locale._format_timeframe("weeks", 2) == "2 هفته" + assert self.locale._format_timeframe("months", 2) == "2 ماه" + assert self.locale._format_timeframe("years", 2) == "2 سال" + + @pytest.mark.usefixtures("lang_locale") class TestNepaliLocale: def test_format_timeframe(self): From 74815dfe735e14095d78d95e34df4651855d1a12 Mon Sep 17 00:00:00 2001 From: "Kristijan \"Fremen\" Velkovski" Date: Thu, 24 Oct 2024 00:45:38 -0500 Subject: [PATCH 49/80] Add Python versions and bump ci deps (#1177) --- .github/workflows/continuous_integration.yml | 2 +- .pre-commit-config.yaml | 8 +-- Makefile | 3 +- README.rst | 2 +- arrow/arrow.py | 1 - arrow/factory.py | 1 - arrow/locales.py | 58 ++++++++++++++++++-- pyproject.toml | 1 + tox.ini | 5 +- 9 files changed, 64 insertions(+), 17 deletions(-) diff --git a/.github/workflows/continuous_integration.yml b/.github/workflows/continuous_integration.yml index 588bff25..0f573850 100644 --- a/.github/workflows/continuous_integration.yml +++ b/.github/workflows/continuous_integration.yml @@ -15,7 +15,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["pypy-3.9", "3.8", "3.9", "3.10", "3.11", "3.12-dev"] + python-version: ["pypy-3.9", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] os: [ubuntu-latest, macos-latest, windows-latest] exclude: # pypy3 randomly fails on Windows builds diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b99f4c48..942ff760 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,7 +2,7 @@ default_language_version: python: python3 repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 + rev: v4.5.0 hooks: - id: check-ast - id: check-yaml @@ -18,11 +18,11 @@ repos: args: [requirements/requirements.txt, requirements/requirements-docs.txt, requirements/requirements-tests.txt] - id: trailing-whitespace - repo: https://github.com/timothycrosley/isort - rev: 5.12.0 + rev: 5.13.2 hooks: - id: isort - repo: https://github.com/asottile/pyupgrade - rev: v3.13.0 + rev: v3.16.0 hooks: - id: pyupgrade args: [--py36-plus] @@ -48,7 +48,7 @@ repos: - id: flake8 additional_dependencies: [flake8-bugbear,flake8-annotations] - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.5.1 + rev: v1.10.0 hooks: - id: mypy additional_dependencies: [types-python-dateutil] diff --git a/Makefile b/Makefile index 9db6fba0..b1b9f53b 100644 --- a/Makefile +++ b/Makefile @@ -7,8 +7,9 @@ build39: PYTHON_VER = python3.9 build310: PYTHON_VER = python3.10 build311: PYTHON_VER = python3.11 build312: PYTHON_VER = python3.12 +build313: PYTHON_VER = python3.13 -build36 build37 build38 build39 build310 build311 build312: clean +build36 build37 build38 build39 build310 build311 build312 build313: clean $(PYTHON_VER) -m venv venv . venv/bin/activate; \ pip install -U pip setuptools wheel; \ diff --git a/README.rst b/README.rst index 69f91fe5..11c44155 100644 --- a/README.rst +++ b/README.rst @@ -47,7 +47,7 @@ Features -------- - Fully-implemented, drop-in replacement for datetime -- Support for Python 3.6+ +- Support for Python 3.8+ - Timezone-aware and UTC by default - Super-simple creation options for many common input scenarios - ``shift`` method with support for relative offsets, including weeks diff --git a/arrow/arrow.py b/arrow/arrow.py index 6e9c3cf7..9d1f5e30 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -4,7 +4,6 @@ """ - import calendar import re import sys diff --git a/arrow/factory.py b/arrow/factory.py index f35085f1..53eb8d12 100644 --- a/arrow/factory.py +++ b/arrow/factory.py @@ -5,7 +5,6 @@ """ - import calendar from datetime import date, datetime from datetime import tzinfo as dt_tzinfo diff --git a/arrow/locales.py b/arrow/locales.py index 94f7902d..de05d8d5 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -776,7 +776,16 @@ class JapaneseLocale(Locale): "12", ] - day_names = ["", "月曜日", "火曜日", "水曜日", "木曜日", "金曜日", "土曜日", "日曜日"] + day_names = [ + "", + "月曜日", + "火曜日", + "水曜日", + "木曜日", + "金曜日", + "土曜日", + "日曜日", + ] day_abbreviations = ["", "月", "火", "水", "木", "金", "土", "日"] @@ -992,7 +1001,16 @@ class ChineseCNLocale(Locale): "12", ] - day_names = ["", "星期一", "星期二", "星期三", "星期四", "星期五", "星期六", "星期日"] + day_names = [ + "", + "星期一", + "星期二", + "星期三", + "星期四", + "星期五", + "星期六", + "星期日", + ] day_abbreviations = ["", "一", "二", "三", "四", "五", "六", "日"] @@ -1111,7 +1129,16 @@ class HongKongLocale(Locale): "12", ] - day_names = ["", "星期一", "星期二", "星期三", "星期四", "星期五", "星期六", "星期日"] + day_names = [ + "", + "星期一", + "星期二", + "星期三", + "星期四", + "星期五", + "星期六", + "星期日", + ] day_abbreviations = ["", "一", "二", "三", "四", "五", "六", "日"] @@ -1181,11 +1208,32 @@ class KoreanLocale(Locale): "12", ] - day_names = ["", "월요일", "화요일", "수요일", "목요일", "금요일", "토요일", "일요일"] + day_names = [ + "", + "월요일", + "화요일", + "수요일", + "목요일", + "금요일", + "토요일", + "일요일", + ] day_abbreviations = ["", "월", "화", "수", "목", "금", "토", "일"] def _ordinal_number(self, n: int) -> str: - ordinals = ["0", "첫", "두", "세", "네", "다섯", "여섯", "일곱", "여덟", "아홉", "열"] + ordinals = [ + "0", + "첫", + "두", + "세", + "네", + "다섯", + "여섯", + "일곱", + "여덟", + "아홉", + "열", + ] if n < len(ordinals): return f"{ordinals[n]}번째" return f"{n}번째" diff --git a/pyproject.toml b/pyproject.toml index 89609bd1..68e0d6c0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,6 +20,7 @@ classifiers = [ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Operating System :: OS Independent", ] dependencies = [ diff --git a/tox.ini b/tox.ini index c410a8e5..51c2c6a6 100644 --- a/tox.ini +++ b/tox.ini @@ -1,18 +1,17 @@ [tox] minversion = 3.18.0 -envlist = py{py3,38,39,310,311,312} +envlist = py{py3,38,39,310,311,312,313} skip_missing_interpreters = true [gh-actions] python = pypy-3.7: pypy3 - 3.6: py36 - 3.7: py37 3.8: py38 3.9: py39 3.10: py310 3.11: py311 3.12: py312 + 3.13: py313 [testenv] deps = -r requirements/requirements-tests.txt From 540182adf57ed43e527c50ce1ea906b9a66a15ac Mon Sep 17 00:00:00 2001 From: rimu <3310831+rimu@users.noreply.github.com> Date: Thu, 24 Oct 2024 18:45:53 +1300 Subject: [PATCH 50/80] add weeks to catalan locale (#1189) --- arrow/locales.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/arrow/locales.py b/arrow/locales.py index de05d8d5..6c10134d 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -3621,6 +3621,8 @@ class CatalanLocale(Locale): "hours": "{0} hores", "day": "un dia", "days": "{0} dies", + "week": "una setmana", + "weeks": "{0} setmanes", "month": "un mes", "months": "{0} mesos", "year": "un any", From c2dfa12bde6bf8b097ad5bb369a0c71ae58a8386 Mon Sep 17 00:00:00 2001 From: Mani Mozaffar Date: Tue, 29 Oct 2024 05:29:00 +0100 Subject: [PATCH 51/80] Add fa to lang and fix linting issue (#1166) Co-authored-by: Kristijan "Fremen" Velkovski Co-authored-by: Jad Chaar --- arrow/locales.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/arrow/locales.py b/arrow/locales.py index 6c10134d..16abd6f6 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -3374,6 +3374,10 @@ class FarsiLocale(Locale): "months": "{0} ماه", "year": "یک سال", "years": "{0} سال", + "week": "یک هفته", + "weeks": "{0} هفته", + "quarter": "یک فصل", + "quarters": "{0} فصل", } meridians = { From 016fb9eb9b3835bcd4efc69dc048a0436bd7bfec Mon Sep 17 00:00:00 2001 From: Farhad Fouladi Date: Tue, 29 Oct 2024 05:30:01 +0100 Subject: [PATCH 52/80] fix: adding persian names of months and month-abbreviations and day-abbreviations in Gregorian calendar (#1172) * fix: adding persian names of months in Gregorian calendar * test: running all tests * test: add tests for weekdays in farsi * test: add 2 more tests for month-name and mont-abbr. --------- Co-authored-by: Farhad Fouladi Co-authored-by: Jad Chaar --- arrow/locales.py | 59 +++++++++++++++++++++++++------------------ tests/test_locales.py | 7 +++++ 2 files changed, 41 insertions(+), 25 deletions(-) diff --git a/arrow/locales.py b/arrow/locales.py index 16abd6f6..6ed2120c 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -3389,33 +3389,33 @@ class FarsiLocale(Locale): month_names = [ "", - "January", - "February", - "March", - "April", - "May", - "June", - "July", - "August", - "September", - "October", - "November", - "December", + "ژانویه", + "فوریه", + "مارس", + "آوریل", + "مه", + "ژوئن", + "ژوئیه", + "اوت", + "سپتامبر", + "اکتبر", + "نوامبر", + "دسامبر", ] month_abbreviations = [ "", - "Jan", - "Feb", - "Mar", - "Apr", - "May", - "Jun", - "Jul", - "Aug", - "Sep", - "Oct", - "Nov", - "Dec", + "ژانویه", + "فوریه", + "مارس", + "آوریل", + "مه", + "ژوئن", + "ژوئیه", + "اوت", + "سپتامبر", + "اکتبر", + "نوامبر", + "دسامبر", ] day_names = [ @@ -3428,7 +3428,16 @@ class FarsiLocale(Locale): "شنبه", "یکشنبه", ] - day_abbreviations = ["", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] + day_abbreviations = [ + "", + "دو شنبه", + "سه شنبه", + "چهارشنبه", + "پنجشنبه", + "جمعه", + "شنبه", + "یکشنبه", + ] class HebrewLocale(Locale): diff --git a/tests/test_locales.py b/tests/test_locales.py index 3eff7f44..b1916ae0 100644 --- a/tests/test_locales.py +++ b/tests/test_locales.py @@ -1463,6 +1463,13 @@ def test_timeframes(self): assert self.locale._format_timeframe("months", 2) == "2 ماه" assert self.locale._format_timeframe("years", 2) == "2 سال" + def test_weekday(self): + fa = arrow.Arrow(2024, 10, 25, 17, 30, 00) + assert self.locale.day_name(fa.isoweekday()) == "جمعه" + assert self.locale.day_abbreviation(fa.isoweekday()) == "جمعه" + assert self.locale.month_name(fa.month) == "اکتبر" + assert self.locale.month_abbreviation(fa.month) == "اکتبر" + @pytest.mark.usefixtures("lang_locale") class TestNepaliLocale: From 0a01d345cbdc2ef8391c8b02f4ac1d3009e73434 Mon Sep 17 00:00:00 2001 From: "Kristijan \"Fremen\" Velkovski" Date: Tue, 5 Nov 2024 17:57:55 -0600 Subject: [PATCH 53/80] Add Macedonian in Latin. (#1200) * add macedonian latin. * add mk-latn name. --- arrow/locales.py | 99 +++++++++++++++++++++++++++ tests/test_locales.py | 151 ++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 245 insertions(+), 5 deletions(-) diff --git a/arrow/locales.py b/arrow/locales.py index 6ed2120c..4cc31a4a 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -1924,6 +1924,105 @@ class MacedonianLocale(SlavicBaseLocale): ] +class MacedonianLatinLocale(SlavicBaseLocale): + names = ["mk-latn", "mk-mk-latn"] + + past = "pred {0}" + future = "za {0}" + + timeframes: ClassVar[Mapping[TimeFrameLiteral, Union[str, Mapping[str, str]]]] = { + "now": "sega", + "second": "edna sekunda", + "seconds": { + "singular": "{0} sekunda", + "dual": "{0} sekundi", + "plural": "{0} sekundi", + }, + "minute": "edna minuta", + "minutes": { + "singular": "{0} minuta", + "dual": "{0} minuti", + "plural": "{0} minuti", + }, + "hour": "eden saat", + "hours": {"singular": "{0} saat", "dual": "{0} saati", "plural": "{0} saati"}, + "day": "eden den", + "days": {"singular": "{0} den", "dual": "{0} dena", "plural": "{0} dena"}, + "week": "edna nedela", + "weeks": { + "singular": "{0} nedela", + "dual": "{0} nedeli", + "plural": "{0} nedeli", + }, + "month": "eden mesec", + "months": { + "singular": "{0} mesec", + "dual": "{0} meseci", + "plural": "{0} meseci", + }, + "year": "edna godina", + "years": { + "singular": "{0} godina", + "dual": "{0} godini", + "plural": "{0} godini", + }, + } + + meridians = {"am": "dp", "pm": "pp", "AM": "pretpladne", "PM": "popladne"} + + month_names = [ + "", + "Januari", + "Fevruari", + "Mart", + "April", + "Maj", + "Juni", + "Juli", + "Avgust", + "Septemvri", + "Oktomvri", + "Noemvri", + "Dekemvri", + ] + month_abbreviations = [ + "", + "Jan", + "Fev", + "Mar", + "Apr", + "Maj", + "Jun", + "Jul", + "Avg", + "Sep", + "Okt", + "Noe", + "Dek", + ] + + day_names = [ + "", + "Ponedelnik", + "Vtornik", + "Sreda", + "Chetvrtok", + "Petok", + "Sabota", + "Nedela", + ] + day_abbreviations = [ + "", + "Pon", + "Vt", + "Sre", + "Chet", + "Pet", + "Sab", + "Ned", + ] + + class GermanBaseLocale(Locale): past = "vor {0}" future = "in {0}" diff --git a/tests/test_locales.py b/tests/test_locales.py index b1916ae0..6db3ad26 100644 --- a/tests/test_locales.py +++ b/tests/test_locales.py @@ -33,14 +33,13 @@ def test_locale_validation(self): assert locale_cls.future is not None def test_locale_name_validation(self): + import re + for locale_cls in self.locales.values(): for locale_name in locale_cls.names: - assert len(locale_name) == 2 or len(locale_name) == 5 assert locale_name.islower() - # Not a two-letter code - if len(locale_name) > 2: - assert "-" in locale_name - assert locale_name.count("-") == 1 + pattern = r"^[a-z]{2}(-[a-z]{2})?(?:-latn|-cyrl)?$" + assert re.match(pattern, locale_name) def test_duplicated_locale_name(self): with pytest.raises(LookupError): @@ -909,6 +908,148 @@ def test_multi_describe_mk(self): assert describe(seconds60, only_distance=True) == "1 секунда" +@pytest.mark.usefixtures("lang_locale") +class TestMacedonianLatinLocale: + def test_singles_mk(self): + assert self.locale._format_timeframe("second", 1) == "edna sekunda" + assert self.locale._format_timeframe("minute", 1) == "edna minuta" + assert self.locale._format_timeframe("hour", 1) == "eden saat" + assert self.locale._format_timeframe("day", 1) == "eden den" + assert self.locale._format_timeframe("week", 1) == "edna nedela" + assert self.locale._format_timeframe("month", 1) == "eden mesec" + assert self.locale._format_timeframe("year", 1) == "edna godina" + + def test_meridians_mk(self): + assert self.locale.meridian(7, "A") == "pretpladne" + assert self.locale.meridian(18, "A") == "popladne" + assert self.locale.meridian(10, "a") == "dp" + assert self.locale.meridian(22, "a") == "pp" + + def test_describe_mk(self): + assert self.locale.describe("second", only_distance=True) == "edna sekunda" + assert self.locale.describe("second", only_distance=False) == "za edna sekunda" + assert self.locale.describe("minute", only_distance=True) == "edna minuta" + assert self.locale.describe("minute", only_distance=False) == "za edna minuta" + assert self.locale.describe("hour", only_distance=True) == "eden saat" + assert self.locale.describe("hour", only_distance=False) == "za eden saat" + assert self.locale.describe("day", only_distance=True) == "eden den" + assert self.locale.describe("day", only_distance=False) == "za eden den" + assert self.locale.describe("week", only_distance=True) == "edna nedela" + assert self.locale.describe("week", only_distance=False) == "za edna nedela" + assert self.locale.describe("month", only_distance=True) == "eden mesec" + assert self.locale.describe("month", only_distance=False) == "za eden mesec" + assert self.locale.describe("year", only_distance=True) == "edna godina" + assert self.locale.describe("year", only_distance=False) == "za edna godina" + + def test_relative_mk(self): + # time + assert self.locale._format_relative("sega", "now", 0) == "sega" + assert self.locale._format_relative("1 sekunda", "seconds", 1) == "za 1 sekunda" + assert self.locale._format_relative("1 minuta", "minutes", 1) == "za 1 minuta" + assert self.locale._format_relative("1 saat", "hours", 1) == "za 1 saat" + assert self.locale._format_relative("1 den", "days", 1) == "za 1 den" + assert self.locale._format_relative("1 nedela", "weeks", 1) == "za 1 nedela" + assert self.locale._format_relative("1 mesec", "months", 1) == "za 1 mesec" + assert self.locale._format_relative("1 godina", "years", 1) == "za 1 godina" + assert ( + self.locale._format_relative("1 sekunda", "seconds", -1) == "pred 1 sekunda" + ) + assert ( + self.locale._format_relative("1 minuta", "minutes", -1) == "pred 1 minuta" + ) + assert self.locale._format_relative("1 saat", "hours", -1) == "pred 1 saat" + assert self.locale._format_relative("1 den", "days", -1) == "pred 1 den" + assert self.locale._format_relative("1 nedela", "weeks", -1) == "pred 1 nedela" + assert self.locale._format_relative("1 mesec", "months", -1) == "pred 1 mesec" + assert self.locale._format_relative("1 godina", "years", -1) == "pred 1 godina" + + def test_plurals_mk(self): + # Seconds + assert self.locale._format_timeframe("seconds", 0) == "0 sekundi" + assert self.locale._format_timeframe("seconds", 1) == "1 sekunda" + assert self.locale._format_timeframe("seconds", 2) == "2 sekundi" + assert self.locale._format_timeframe("seconds", 4) == "4 sekundi" + assert self.locale._format_timeframe("seconds", 5) == "5 sekundi" + assert self.locale._format_timeframe("seconds", 21) == "21 sekunda" + assert self.locale._format_timeframe("seconds", 22) == "22 sekundi" + assert self.locale._format_timeframe("seconds", 25) == "25 sekundi" + + # Minutes + assert self.locale._format_timeframe("minutes", 0) == "0 minuti" + assert self.locale._format_timeframe("minutes", 1) == "1 minuta" + assert self.locale._format_timeframe("minutes", 2) == "2 minuti" + assert self.locale._format_timeframe("minutes", 4) == "4 minuti" + assert self.locale._format_timeframe("minutes", 5) == "5 minuti" + assert self.locale._format_timeframe("minutes", 21) == "21 minuta" + assert self.locale._format_timeframe("minutes", 22) == "22 minuti" + assert self.locale._format_timeframe("minutes", 25) == "25 minuti" + + # Hours + assert self.locale._format_timeframe("hours", 0) == "0 saati" + assert self.locale._format_timeframe("hours", 1) == "1 saat" + assert self.locale._format_timeframe("hours", 2) == "2 saati" + assert self.locale._format_timeframe("hours", 4) == "4 saati" + assert self.locale._format_timeframe("hours", 5) == "5 saati" + assert self.locale._format_timeframe("hours", 21) == "21 saat" + assert self.locale._format_timeframe("hours", 22) == "22 saati" + assert self.locale._format_timeframe("hours", 25) == "25 saati" + + # Days + assert self.locale._format_timeframe("days", 0) == "0 dena" + assert self.locale._format_timeframe("days", 1) == "1 den" + assert self.locale._format_timeframe("days", 2) == "2 dena" + assert self.locale._format_timeframe("days", 3) == "3 dena" + assert self.locale._format_timeframe("days", 21) == "21 den" + + # Weeks + assert self.locale._format_timeframe("weeks", 0) == "0 nedeli" + assert self.locale._format_timeframe("weeks", 1) == "1 nedela" + assert self.locale._format_timeframe("weeks", 2) == "2 nedeli" + assert self.locale._format_timeframe("weeks", 4) == "4 nedeli" + assert self.locale._format_timeframe("weeks", 5) == "5 nedeli" + assert self.locale._format_timeframe("weeks", 21) == "21 nedela" + assert self.locale._format_timeframe("weeks", 22) == "22 nedeli" + assert self.locale._format_timeframe("weeks", 25) == "25 nedeli" + + # Months + assert self.locale._format_timeframe("months", 0) == "0 meseci" + assert self.locale._format_timeframe("months", 1) == "1 mesec" + assert self.locale._format_timeframe("months", 2) == "2 meseci" + assert self.locale._format_timeframe("months", 4) == "4 meseci" + assert self.locale._format_timeframe("months", 5) == "5 meseci" + assert self.locale._format_timeframe("months", 21) == "21 mesec" + assert self.locale._format_timeframe("months", 22) == "22 meseci" + assert self.locale._format_timeframe("months", 25) == "25 meseci" + + # Years + assert self.locale._format_timeframe("years", 1) == "1 godina" + assert self.locale._format_timeframe("years", 2) == "2 godini" + assert self.locale._format_timeframe("years", 5) == "5 godini" + + def test_multi_describe_mk(self): + describe = self.locale.describe_multi + + fulltest = [("years", 5), ("weeks", 1), ("hours", 1), ("minutes", 6)] + assert describe(fulltest) == "za 5 godini 1 nedela 1 saat 6 minuti" + seconds4000_0days = [("days", 0), ("hours", 1), ("minutes", 6)] + assert describe(seconds4000_0days) == "za 0 dena 1 saat 6 minuti" + seconds4000 = [("hours", 1), ("minutes", 6)] + assert describe(seconds4000) == "za 1 saat 6 minuti" + assert describe(seconds4000, only_distance=True) == "1 saat 6 minuti" + seconds3700 = [("hours", 1), ("minutes", 1)] + assert describe(seconds3700) == "za 1 saat 1 minuta" + seconds300_0hours = [("hours", 0), ("minutes", 5)] + assert describe(seconds300_0hours) == "za 0 saati 5 minuti" + seconds300 = [("minutes", 5)] + assert describe(seconds300) == "za 5 minuti" + seconds60 = [("minutes", 1)] + assert describe(seconds60) == "za 1 minuta" + assert describe(seconds60, only_distance=True) == "1 minuta" + seconds60 = [("seconds", 1)] + assert describe(seconds60) == "za 1 sekunda" + assert describe(seconds60, only_distance=True) == "1 sekunda" + + @pytest.mark.usefixtures("time_2013_01_01") @pytest.mark.usefixtures("lang_locale") class TestHebrewLocale: From b84ac0b36bcf9171cdbe79f7e0498796e2a2da1e Mon Sep 17 00:00:00 2001 From: "Kristijan \"Fremen\" Velkovski" Date: Tue, 5 Nov 2024 18:04:38 -0600 Subject: [PATCH 54/80] Adding docstrings to parser.py (#988) (#1010) * Adding docstrings to parser.py --- arrow/parser.py | 169 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 169 insertions(+) diff --git a/arrow/parser.py b/arrow/parser.py index b794f1b7..39297887 100644 --- a/arrow/parser.py +++ b/arrow/parser.py @@ -31,6 +31,14 @@ class ParserError(ValueError): + """ + A custom exception class for handling parsing errors in the parser. + + Notes: + This class inherits from the built-in `ValueError` class and is used to raise exceptions + when an error occurs during the parsing process. + """ + pass @@ -40,6 +48,14 @@ class ParserError(ValueError): # _parse_multiformat() and the appropriate error message was not # transmitted to the user. class ParserMatchError(ParserError): + """ + This class is a subclass of the ParserError class and is used to raise errors that occur during the matching process. + + Notes: + This class is part of the Arrow parser and is used to provide error handling when a parsing match fails. + + """ + pass @@ -81,6 +97,29 @@ class ParserMatchError(ParserError): class _Parts(TypedDict, total=False): + """ + A dictionary that represents different parts of a datetime. + + :class:`_Parts` is a TypedDict that represents various components of a date or time, + such as year, month, day, hour, minute, second, microsecond, timestamp, expanded_timestamp, tzinfo, + am_pm, day_of_week, and weekdate. + + :ivar year: The year, if present, as an integer. + :ivar month: The month, if present, as an integer. + :ivar day_of_year: The day of the year, if present, as an integer. + :ivar day: The day, if present, as an integer. + :ivar hour: The hour, if present, as an integer. + :ivar minute: The minute, if present, as an integer. + :ivar second: The second, if present, as an integer. + :ivar microsecond: The microsecond, if present, as an integer. + :ivar timestamp: The timestamp, if present, as a float. + :ivar expanded_timestamp: The expanded timestamp, if present, as an integer. + :ivar tzinfo: The timezone info, if present, as a :class:`dt_tzinfo` object. + :ivar am_pm: The AM/PM indicator, if present, as a string literal "am" or "pm". + :ivar day_of_week: The day of the week, if present, as an integer. + :ivar weekdate: The week date, if present, as a tuple of three integers or None. + """ + year: int month: int day_of_year: int @@ -98,6 +137,16 @@ class _Parts(TypedDict, total=False): class DateTimeParser: + """A :class:`DateTimeParser ` object + + Contains the regular expressions and functions to parse and split the input strings into tokens and eventually + produce a datetime that is used by :class:`Arrow ` internally. + + :param locale: the locale string + :param cache_size: the size of the LRU cache used for regular expressions. Defaults to 0. + + """ + _FORMAT_RE: ClassVar[Pattern[str]] = re.compile( r"(YYY?Y?|MM?M?M?|Do|DD?D?D?|d?d?d?d|HH?|hh?|mm?|ss?|S+|ZZ?Z?|a|A|x|X|W)" ) @@ -155,6 +204,15 @@ class DateTimeParser: _input_re_map: Dict[_FORMAT_TYPE, Pattern[str]] def __init__(self, locale: str = DEFAULT_LOCALE, cache_size: int = 0) -> None: + """ + Contains the regular expressions and functions to parse and split the input strings into tokens and eventually + produce a datetime that is used by :class:`Arrow ` internally. + + :param locale: the locale string + :type locale: str + :param cache_size: the size of the LRU cache used for regular expressions. Defaults to 0. + :type cache_size: int + """ self.locale = locales.get_locale(locale) self._input_re_map = self._BASE_INPUT_RE_MAP.copy() self._input_re_map.update( @@ -191,6 +249,23 @@ def __init__(self, locale: str = DEFAULT_LOCALE, cache_size: int = 0) -> None: def parse_iso( self, datetime_string: str, normalize_whitespace: bool = False ) -> datetime: + """ + Parses a datetime string using a ISO 8601-like format. + + :param datetime_string: The datetime string to parse. + :param normalize_whitespace: Whether to normalize whitespace in the datetime string (default is False). + :type datetime_string: str + :type normalize_whitespace: bool + :returns: The parsed datetime object. + :rtype: datetime + :raises ParserError: If the datetime string is not in a valid ISO 8601-like format. + + Usage:: + >>> import arrow.parser + >>> arrow.parser.DateTimeParser().parse_iso('2021-10-12T14:30:00') + datetime.datetime(2021, 10, 12, 14, 30) + + """ if normalize_whitespace: datetime_string = re.sub(r"\s+", " ", datetime_string.strip()) @@ -298,6 +373,27 @@ def parse( fmt: Union[List[str], str], normalize_whitespace: bool = False, ) -> datetime: + """ + Parses a datetime string using a specified format. + + :param datetime_string: The datetime string to parse. + :param fmt: The format string or list of format strings to use for parsing. + :param normalize_whitespace: Whether to normalize whitespace in the datetime string (default is False). + :type datetime_string: str + :type fmt: Union[List[str], str] + :type normalize_whitespace: bool + :returns: The parsed datetime object. + :rtype: datetime + :raises ParserMatchError: If the datetime string does not match the specified format. + + Usage:: + + >>> import arrow.parser + >>> arrow.parser.DateTimeParser().parse('2021-10-12 14:30:00', 'YYYY-MM-DD HH:mm:ss') + datetime.datetime(2021, 10, 12, 14, 30) + + + """ if normalize_whitespace: datetime_string = re.sub(r"\s+", " ", datetime_string) @@ -340,6 +436,15 @@ def parse( return self._build_datetime(parts) def _generate_pattern_re(self, fmt: str) -> Tuple[List[_FORMAT_TYPE], Pattern[str]]: + """ + Generates a regular expression pattern from a format string. + + :param fmt: The format string to convert into a regular expression pattern. + :type fmt: str + :returns: A tuple containing a list of format tokens and the corresponding regular expression pattern. + :rtype: Tuple[List[_FORMAT_TYPE], Pattern[str]] + :raises ParserError: If an unrecognized token is encountered in the format string. + """ # fmt is a string of tokens like 'YYYY-MM-DD' # we construct a new string by replacing each # token by its pattern: @@ -491,6 +596,20 @@ def _parse_token( value: Any, parts: _Parts, ) -> None: + """ + Parse a token and its value, and update the `_Parts` dictionary with the parsed values. + + The function supports several tokens, including "YYYY", "YY", "MMMM", "MMM", "MM", "M", "DDDD", "DDD", "DD", "D", "Do", "dddd", "ddd", "HH", "H", "mm", "m", "ss", "s", "S", "X", "x", "ZZZ", "ZZ", "Z", "a", "A", and "W". Each token is matched and the corresponding value is parsed and added to the `_Parts` dictionary. + + :param token: The token to parse. + :type token: Any + :param value: The value of the token. + :type value: Any + :param parts: A dictionary to update with the parsed values. + :type parts: _Parts + :raises ParserMatchError: If the hour token value is not between 0 and 12 inclusive for tokens "a" or "A". + + """ if token == "YYYY": parts["year"] = int(value) @@ -577,6 +696,14 @@ def _parse_token( @staticmethod def _build_datetime(parts: _Parts) -> datetime: + """ + Build a datetime object from a dictionary of date parts. + + :param parts: A dictionary containing the date parts extracted from a date string. + :type parts: dict + :return: A datetime object representing the date and time. + :rtype: datetime.datetime + """ weekdate = parts.get("weekdate") if weekdate is not None: @@ -703,6 +830,21 @@ def _build_datetime(parts: _Parts) -> datetime: ) def _parse_multiformat(self, string: str, formats: Iterable[str]) -> datetime: + """ + Parse a date and time string using multiple formats. + + Tries to parse the provided string with each format in the given `formats` + iterable, returning the resulting `datetime` object if a match is found. If no + format matches the string, a `ParserError` is raised. + + :param string: The date and time string to parse. + :type string: str + :param formats: An iterable of date and time format strings to try, in order. + :type formats: Iterable[str] + :returns: The parsed date and time. + :rtype: datetime.datetime + :raises ParserError: If no format matches the input string. + """ _datetime: Optional[datetime] = None for fmt in formats: @@ -725,16 +867,43 @@ def _parse_multiformat(self, string: str, formats: Iterable[str]) -> datetime: def _generate_choice_re( choices: Iterable[str], flags: Union[int, re.RegexFlag] = 0 ) -> Pattern[str]: + """ + Generate a regular expression pattern that matches a choice from an iterable. + + Takes an iterable of strings (`choices`) and returns a compiled regular expression + pattern that matches any of the choices. The pattern is created by joining the + choices with the '|' (OR) operator, which matches any of the enclosed patterns. + + :param choices: An iterable of strings to match. + :type choices: Iterable[str] + :param flags: Optional regular expression flags. Default is 0. + :type flags: Union[int, re.RegexFlag], optional + :returns: A compiled regular expression pattern that matches any of the choices. + :rtype: re.Pattern[str] + """ return re.compile(r"({})".format("|".join(choices)), flags=flags) class TzinfoParser: + """ + Parser for timezone information. + """ + _TZINFO_RE: ClassVar[Pattern[str]] = re.compile( r"^(?:\(UTC)*([\+\-])?(\d{2})(?:\:?(\d{2}))?" ) @classmethod def parse(cls, tzinfo_string: str) -> dt_tzinfo: + """ + Parse a timezone string and return a datetime timezone object. + + :param tzinfo_string: The timezone string to parse. + :type tzinfo_string: str + :returns: The parsed datetime timezone object. + :rtype: datetime.timezone + :raises ParserError: If the timezone string cannot be parsed. + """ tzinfo: Optional[dt_tzinfo] = None if tzinfo_string == "local": From d05ca1dbd215515d4a346010ea329d7b3a472f5a Mon Sep 17 00:00:00 2001 From: "Kristijan \"Fremen\" Velkovski" Date: Sun, 17 Nov 2024 13:13:46 -0600 Subject: [PATCH 55/80] Revert "Add fa to lang and fix linting issue (#1166)" (#1202) This reverts commit c2dfa12bde6bf8b097ad5bb369a0c71ae58a8386. --- arrow/locales.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/arrow/locales.py b/arrow/locales.py index 4cc31a4a..63b2d48c 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -3473,10 +3473,6 @@ class FarsiLocale(Locale): "months": "{0} ماه", "year": "یک سال", "years": "{0} سال", - "week": "یک هفته", - "weeks": "{0} هفته", - "quarter": "یک فصل", - "quarters": "{0} فصل", } meridians = { From 2aaafcd581115c50cdcbf04b196657dad1692a26 Mon Sep 17 00:00:00 2001 From: Tom Sarantis Date: Mon, 18 Nov 2024 06:26:32 +1100 Subject: [PATCH 56/80] use zoneinfo instead of pytz (#1179) Co-authored-by: Jad Chaar --- pyproject.toml | 1 + requirements/requirements-tests.txt | 1 + tests/test_arrow.py | 15 +++++++++++++++ tests/test_formatter.py | 8 ++++++-- tests/utils.py | 9 ++++++--- 5 files changed, 29 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 68e0d6c0..3e236917 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,6 +41,7 @@ dynamic = ["version"] [project.optional-dependencies] test = [ + "backports.zoneinfo==0.2.1;python_version<'3.9'", "dateparser==1.*", "pre-commit", "pytest", diff --git a/requirements/requirements-tests.txt b/requirements/requirements-tests.txt index 1f2a2ec4..94e5cc84 100644 --- a/requirements/requirements-tests.txt +++ b/requirements/requirements-tests.txt @@ -1,4 +1,5 @@ -r requirements.txt +backports.zoneinfo==0.2.1;python_version<'3.9' dateparser==1.* pre-commit pytest diff --git a/tests/test_arrow.py b/tests/test_arrow.py index daf6209f..5afe9baa 100644 --- a/tests/test_arrow.py +++ b/tests/test_arrow.py @@ -1,3 +1,8 @@ +try: + import zoneinfo +except ImportError: + from backports import zoneinfo + import pickle import sys import time @@ -67,6 +72,16 @@ def test_init_pytz_timezone(self): assert result._datetime == self.expected assert_datetime_equality(result._datetime, self.expected, 1) + def test_init_zoneinfo_timezone(self): + result = arrow.Arrow( + 2024, 7, 10, 18, 55, 45, 999999, tzinfo=zoneinfo.ZoneInfo("Europe/Paris") + ) + self.expected = datetime( + 2024, 7, 10, 18, 55, 45, 999999, tzinfo=zoneinfo.ZoneInfo("Europe/Paris") + ) + assert result._datetime == self.expected + assert_datetime_equality(result._datetime, self.expected, 1) + def test_init_with_fold(self): before = arrow.Arrow(2017, 10, 29, 2, 0, tzinfo="Europe/Stockholm") after = arrow.Arrow(2017, 10, 29, 2, 0, tzinfo="Europe/Stockholm", fold=1) diff --git a/tests/test_formatter.py b/tests/test_formatter.py index 36e15d9f..b09b778f 100644 --- a/tests/test_formatter.py +++ b/tests/test_formatter.py @@ -1,7 +1,11 @@ +try: + import zoneinfo +except ImportError: + from backports import zoneinfo + from datetime import datetime, timezone import pytest -import pytz from dateutil import tz as dateutil_tz from arrow import ( @@ -124,7 +128,7 @@ def test_timezone(self): @pytest.mark.parametrize("full_tz_name", make_full_tz_list()) def test_timezone_formatter(self, full_tz_name): # This test will fail if we use "now" as date as soon as we change from/to DST - dt = datetime(1986, 2, 14, tzinfo=pytz.timezone("UTC")).replace( + dt = datetime(1986, 2, 14, tzinfo=zoneinfo.ZoneInfo("UTC")).replace( tzinfo=dateutil_tz.gettz(full_tz_name) ) abbreviation = dt.tzname() diff --git a/tests/utils.py b/tests/utils.py index 95b47c16..7a74b7e4 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,11 +1,14 @@ -import pytz +try: + import zoneinfo +except ImportError: + from backports import zoneinfo from dateutil.zoneinfo import get_zonefile_instance def make_full_tz_list(): dateutil_zones = set(get_zonefile_instance().zones) - pytz_zones = set(pytz.all_timezones) - return dateutil_zones.union(pytz_zones) + zoneinfo_zones = set(zoneinfo.available_timezones()) + return dateutil_zones.union(zoneinfo_zones) def assert_datetime_equality(dt1, dt2, within=10): From 1d70d0091980ea489a64fa95a48e99b45f29f0e7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 19 Nov 2024 23:41:58 -0600 Subject: [PATCH 57/80] Bump codecov/codecov-action from 4 to 5 (#1203) Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 4 to 5. - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/v4...v5) --- updated-dependencies: - dependency-name: codecov/codecov-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/continuous_integration.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/continuous_integration.yml b/.github/workflows/continuous_integration.yml index 0f573850..c94ba640 100644 --- a/.github/workflows/continuous_integration.yml +++ b/.github/workflows/continuous_integration.yml @@ -47,7 +47,7 @@ jobs: - name: Test with tox run: tox - name: Upload coverage to Codecov - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v5 with: file: coverage.xml From 0fe5f065718e5af3ab47903d1bec82d87d202a63 Mon Sep 17 00:00:00 2001 From: "Kristijan \"Fremen\" Velkovski" Date: Sun, 18 May 2025 17:31:07 -0500 Subject: [PATCH 58/80] Add FORMAT_RFC3339_STRICT with a T separator. (#1201) Co-authored-by: Anish Nyayachavadi <55898433+anishnya@users.noreply.github.com> --- arrow/__init__.py | 2 ++ arrow/formatter.py | 1 + tests/test_formatter.py | 7 +++++++ 3 files changed, 10 insertions(+) diff --git a/arrow/__init__.py b/arrow/__init__.py index bc597097..9232b379 100644 --- a/arrow/__init__.py +++ b/arrow/__init__.py @@ -11,6 +11,7 @@ FORMAT_RFC1123, FORMAT_RFC2822, FORMAT_RFC3339, + FORMAT_RFC3339_STRICT, FORMAT_RSS, FORMAT_W3C, ) @@ -33,6 +34,7 @@ "FORMAT_RFC1123", "FORMAT_RFC2822", "FORMAT_RFC3339", + "FORMAT_RFC3339_STRICT", "FORMAT_RSS", "FORMAT_W3C", "ParserError", diff --git a/arrow/formatter.py b/arrow/formatter.py index 8cd61e90..6634545f 100644 --- a/arrow/formatter.py +++ b/arrow/formatter.py @@ -17,6 +17,7 @@ FORMAT_RFC1123: Final[str] = "ddd, DD MMM YYYY HH:mm:ss Z" FORMAT_RFC2822: Final[str] = "ddd, DD MMM YYYY HH:mm:ss Z" FORMAT_RFC3339: Final[str] = "YYYY-MM-DD HH:mm:ssZZ" +FORMAT_RFC3339_STRICT: Final[str] = "YYYY-MM-DDTHH:mm:ssZZ" FORMAT_RSS: Final[str] = "ddd, DD MMM YYYY HH:mm:ss Z" FORMAT_W3C: Final[str] = "YYYY-MM-DD HH:mm:ssZZ" diff --git a/tests/test_formatter.py b/tests/test_formatter.py index b09b778f..53868288 100644 --- a/tests/test_formatter.py +++ b/tests/test_formatter.py @@ -17,6 +17,7 @@ FORMAT_RFC1123, FORMAT_RFC2822, FORMAT_RFC3339, + FORMAT_RFC3339_STRICT, FORMAT_RSS, FORMAT_W3C, ) @@ -258,6 +259,12 @@ def test_rfc3339(self): == "1975-12-25 14:15:16-05:00" ) + def test_rfc3339_strict(self): + assert ( + self.formatter.format(self.datetime, FORMAT_RFC3339_STRICT) + == "1975-12-25T14:15:16-05:00" + ) + def test_rss(self): assert ( self.formatter.format(self.datetime, FORMAT_RSS) From d8bf13d4441300fff017d2fe33d4eae032e37768 Mon Sep 17 00:00:00 2001 From: _HOPE_ <76970465+Hope1010@users.noreply.github.com> Date: Tue, 5 Aug 2025 12:39:05 +0700 Subject: [PATCH 59/80] Add week, weeks to timeframes in class ThaiLocale (#1218) * Add week, weeks to timeframes in class ThaiLocale * Update ThaiLocale translations and add tests for timeframes and weekdays * Format day_names in ThaiLocale for improved readability --- arrow/locales.py | 27 +++++++++++++++++++-------- tests/test_locales.py | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 8 deletions(-) diff --git a/arrow/locales.py b/arrow/locales.py index 63b2d48c..5d31aa24 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -4015,16 +4015,18 @@ class ThaiLocale(Locale): timeframes = { "now": "ขณะนี้", "second": "วินาที", - "seconds": "{0} ไม่กี่วินาที", - "minute": "1 นาที", + "seconds": "{0} วินาที", + "minute": "นาที", "minutes": "{0} นาที", - "hour": "1 ชั่วโมง", + "hour": "ชั่วโมง", "hours": "{0} ชั่วโมง", - "day": "1 วัน", + "day": "วัน", "days": "{0} วัน", - "month": "1 เดือน", + "week": "สัปดาห์", + "weeks": "{0} สัปดาห์", + "month": "เดือน", "months": "{0} เดือน", - "year": "1 ปี", + "year": "ปี", "years": "{0} ปี", } @@ -4059,8 +4061,17 @@ class ThaiLocale(Locale): "ธ.ค.", ] - day_names = ["", "จันทร์", "อังคาร", "พุธ", "พฤหัสบดี", "ศุกร์", "เสาร์", "อาทิตย์"] - day_abbreviations = ["", "จ", "อ", "พ", "พฤ", "ศ", "ส", "อา"] + day_names = [ + "", + "วันจันทร์", + "วันอังคาร", + "วันพุธ", + "วันพฤหัสบดี", + "วันศุกร์", + "วันเสาร์", + "วันอาทิตย์", + ] + day_abbreviations = ["", "จ.", "อ.", "พ.", "พฤ.", "ศ.", "ส.", "อา."] meridians = {"am": "am", "pm": "pm", "AM": "AM", "PM": "PM"} diff --git a/tests/test_locales.py b/tests/test_locales.py index 6db3ad26..37444a02 100644 --- a/tests/test_locales.py +++ b/tests/test_locales.py @@ -1498,6 +1498,44 @@ def test_format_relative_future(self): result = self.locale._format_relative("1 ชั่วโมง", "hour", -1) assert result == "1 ชั่วโมง ที่ผ่านมา" + def test_format_timeframe(self): + # Now + assert self.locale._format_timeframe("now", 0) == "ขณะนี้" + # Second(s) + assert self.locale._format_timeframe("second", 1) == "วินาที" + assert self.locale._format_timeframe("seconds", 2) == "2 วินาที" + # Minute(s) + assert self.locale._format_timeframe("minute", 1) == "นาที" + assert self.locale._format_timeframe("minutes", 5) == "5 นาที" + # Hour(s) + assert self.locale._format_timeframe("hour", 1) == "ชั่วโมง" + assert self.locale._format_timeframe("hours", 3) == "3 ชั่วโมง" + # Day(s) + assert self.locale._format_timeframe("day", 1) == "วัน" + assert self.locale._format_timeframe("days", 7) == "7 วัน" + # Week(s) + assert self.locale._format_timeframe("week", 1) == "สัปดาห์" + assert self.locale._format_timeframe("weeks", 2) == "2 สัปดาห์" + # Month(s) + assert self.locale._format_timeframe("month", 1) == "เดือน" + assert self.locale._format_timeframe("months", 4) == "4 เดือน" + # Year(s) + assert self.locale._format_timeframe("year", 1) == "ปี" + assert self.locale._format_timeframe("years", 10) == "10 ปี" + + def test_weekday(self): + dt = arrow.Arrow(2015, 4, 11, 17, 30, 0) + # These values depend on the actual Thai locale implementation + # Replace with correct Thai names if available + assert self.locale.day_name(dt.isoweekday()) == "วันเสาร์" + assert self.locale.day_abbreviation(dt.isoweekday()) == "ส." + + def test_ordinal_number(self): + # Thai ordinal numbers are not commonly used, but test for fallback + assert self.locale.ordinal_number(1) == "1" + assert self.locale.ordinal_number(10) == "10" + assert self.locale.ordinal_number(0) == "0" + @pytest.mark.usefixtures("lang_locale") class TestBengaliLocale: From 97695bfce6977fb117459600933768827ef23aaf Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Wed, 6 Aug 2025 21:49:53 -0700 Subject: [PATCH 60/80] Migrate Arrow to use ZoneInfo for timezones (#1217) * Fix test errors and deprecation warnings - Fix Asia/Hanoi timezone issue by filtering timezone list to only include zoneinfo-compatible timezones - Fix Anchorage DST test by using correct time after spring-forward transition (3:01 AM instead of 2:01 AM) - Replace deprecated datetime.utcfromtimestamp() with datetime.fromtimestamp(timestamp, timezone.utc) - Update timezone test utilities to ensure compatibility with zoneinfo migration * Migrate to timezone.utc * Migrate from tzlocal to datetime.now().astimezone() * Upgrade dependencies * Add test to validate dateutil intialization --- .gitignore | 4 + arrow/arrow.py | 20 ++--- arrow/factory.py | 8 +- arrow/formatter.py | 6 +- arrow/parser.py | 22 +++-- pyproject.toml | 5 +- requirements/requirements-tests.txt | 3 +- requirements/requirements.txt | 2 + tests/conftest.py | 8 +- tests/test_arrow.py | 130 +++++++++++++++------------- tests/test_factory.py | 75 ++++++++-------- tests/test_formatter.py | 10 +-- tests/test_parser.py | 49 ++++++----- tests/utils.py | 24 ++++- 14 files changed, 208 insertions(+), 158 deletions(-) diff --git a/.gitignore b/.gitignore index 0448d0cf..26f42fb3 100644 --- a/.gitignore +++ b/.gitignore @@ -209,3 +209,7 @@ $RECYCLE.BIN/ # Windows shortcuts *.lnk + +# Claude Code configurations +CLAUDE*.md +.claude/ diff --git a/arrow/arrow.py b/arrow/arrow.py index 9d1f5e30..99452556 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -157,7 +157,7 @@ def __init__( **kwargs: Any, ) -> None: if tzinfo is None: - tzinfo = dateutil_tz.tzutc() + tzinfo = timezone.utc # detect that tzinfo is a pytz object (issue #626) elif ( isinstance(tzinfo, dt_tzinfo) @@ -192,7 +192,7 @@ def now(cls, tzinfo: Optional[dt_tzinfo] = None) -> "Arrow": """ if tzinfo is None: - tzinfo = dateutil_tz.tzlocal() + tzinfo = dt_datetime.now().astimezone().tzinfo dt = dt_datetime.now(tzinfo) @@ -220,7 +220,7 @@ def utcnow(cls) -> "Arrow": """ - dt = dt_datetime.now(dateutil_tz.tzutc()) + dt = dt_datetime.now(timezone.utc) return cls( dt.year, @@ -249,7 +249,7 @@ def fromtimestamp( """ if tzinfo is None: - tzinfo = dateutil_tz.tzlocal() + tzinfo = dt_datetime.now().astimezone().tzinfo elif isinstance(tzinfo, str): tzinfo = parser.TzinfoParser.parse(tzinfo) @@ -283,7 +283,7 @@ def utcfromtimestamp(cls, timestamp: Union[int, float, str]) -> "Arrow": raise ValueError(f"The provided timestamp {timestamp!r} is invalid.") timestamp = util.normalize_timestamp(float(timestamp)) - dt = dt_datetime.utcfromtimestamp(timestamp) + dt = dt_datetime.fromtimestamp(timestamp, timezone.utc) return cls( dt.year, @@ -293,7 +293,7 @@ def utcfromtimestamp(cls, timestamp: Union[int, float, str]) -> "Arrow": dt.minute, dt.second, dt.microsecond, - dateutil_tz.tzutc(), + timezone.utc, fold=getattr(dt, "fold", 0), ) @@ -317,7 +317,7 @@ def fromdatetime(cls, dt: dt_datetime, tzinfo: Optional[TZ_EXPR] = None) -> "Arr if tzinfo is None: if dt.tzinfo is None: - tzinfo = dateutil_tz.tzutc() + tzinfo = timezone.utc else: tzinfo = dt.tzinfo @@ -344,7 +344,7 @@ def fromdate(cls, date: date, tzinfo: Optional[TZ_EXPR] = None) -> "Arrow": """ if tzinfo is None: - tzinfo = dateutil_tz.tzutc() + tzinfo = timezone.utc return cls(date.year, date.month, date.day, tzinfo=tzinfo) @@ -1151,7 +1151,7 @@ def humanize( locale = locales.get_locale(locale) if other is None: - utc = dt_datetime.now(timezone.utc).replace(tzinfo=dateutil_tz.tzutc()) + utc = dt_datetime.now(timezone.utc).replace(tzinfo=timezone.utc) dt = utc.astimezone(self._datetime.tzinfo) elif isinstance(other, Arrow): @@ -1792,7 +1792,7 @@ def __le__(self, other: Any) -> bool: def _get_tzinfo(tz_expr: Optional[TZ_EXPR]) -> dt_tzinfo: """Get normalized tzinfo object from various inputs.""" if tz_expr is None: - return dateutil_tz.tzutc() + return timezone.utc if isinstance(tz_expr, dt_tzinfo): return tz_expr else: diff --git a/arrow/factory.py b/arrow/factory.py index 53eb8d12..3e138a6a 100644 --- a/arrow/factory.py +++ b/arrow/factory.py @@ -6,14 +6,12 @@ """ import calendar -from datetime import date, datetime +from datetime import date, datetime, timezone from datetime import tzinfo as dt_tzinfo from decimal import Decimal from time import struct_time from typing import Any, List, Optional, Tuple, Type, Union, overload -from dateutil import tz as dateutil_tz - from arrow import parser from arrow.arrow import TZ_EXPR, Arrow from arrow.constants import DEFAULT_LOCALE @@ -229,7 +227,7 @@ def get(self, *args: Any, **kwargs: Any) -> Arrow: elif not isinstance(arg, str) and is_timestamp(arg): if tz is None: # set to UTC by default - tz = dateutil_tz.tzutc() + tz = timezone.utc return self.type.fromtimestamp(arg, tzinfo=tz) # (Arrow) -> from the object's datetime @ tzinfo @@ -337,7 +335,7 @@ def now(self, tz: Optional[TZ_EXPR] = None) -> Arrow: """ if tz is None: - tz = dateutil_tz.tzlocal() + tz = datetime.now().astimezone().tzinfo elif not isinstance(tz, dt_tzinfo): tz = parser.TzinfoParser.parse(tz) diff --git a/arrow/formatter.py b/arrow/formatter.py index 6634545f..6c6a718c 100644 --- a/arrow/formatter.py +++ b/arrow/formatter.py @@ -1,11 +1,9 @@ """Provides the :class:`Arrow ` class, an improved formatter for datetimes.""" import re -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from typing import Final, Optional, Pattern, cast -from dateutil import tz as dateutil_tz - from arrow import locales from arrow.constants import DEFAULT_LOCALE @@ -122,7 +120,7 @@ def _format_token(self, dt: datetime, token: Optional[str]) -> Optional[str]: if token in ["ZZ", "Z"]: separator = ":" if token == "ZZ" else "" - tz = dateutil_tz.tzutc() if dt.tzinfo is None else dt.tzinfo + tz = timezone.utc if dt.tzinfo is None else dt.tzinfo # `dt` must be aware object. Otherwise, this line will raise AttributeError # https://github.com/arrow-py/arrow/pull/883#discussion_r529866834 # datetime awareness: https://docs.python.org/3/library/datetime.html#aware-and-naive-objects diff --git a/arrow/parser.py b/arrow/parser.py index 39297887..4a623291 100644 --- a/arrow/parser.py +++ b/arrow/parser.py @@ -1,7 +1,7 @@ """Provides the :class:`Arrow ` class, a better way to parse datetime strings.""" import re -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from datetime import tzinfo as dt_tzinfo from functools import lru_cache from typing import ( @@ -23,7 +23,10 @@ overload, ) -from dateutil import tz +try: + from zoneinfo import ZoneInfo, ZoneInfoNotFoundError +except ImportError: + from backports.zoneinfo import ZoneInfo, ZoneInfoNotFoundError # type: ignore[no-redef] from arrow import locales from arrow.constants import DEFAULT_LOCALE @@ -727,14 +730,14 @@ def _build_datetime(parts: _Parts) -> datetime: timestamp = parts.get("timestamp") if timestamp is not None: - return datetime.fromtimestamp(timestamp, tz=tz.tzutc()) + return datetime.fromtimestamp(timestamp, tz=timezone.utc) expanded_timestamp = parts.get("expanded_timestamp") if expanded_timestamp is not None: return datetime.fromtimestamp( normalize_timestamp(expanded_timestamp), - tz=tz.tzutc(), + tz=timezone.utc, ) day_of_year = parts.get("day_of_year") @@ -907,10 +910,10 @@ def parse(cls, tzinfo_string: str) -> dt_tzinfo: tzinfo: Optional[dt_tzinfo] = None if tzinfo_string == "local": - tzinfo = tz.tzlocal() + tzinfo = datetime.now().astimezone().tzinfo elif tzinfo_string in ["utc", "UTC", "Z"]: - tzinfo = tz.tzutc() + tzinfo = timezone.utc else: iso_match = cls._TZINFO_RE.match(tzinfo_string) @@ -925,10 +928,13 @@ def parse(cls, tzinfo_string: str) -> dt_tzinfo: if sign == "-": seconds *= -1 - tzinfo = tz.tzoffset(None, seconds) + tzinfo = timezone(timedelta(seconds=seconds)) else: - tzinfo = tz.gettz(tzinfo_string) + try: + tzinfo = ZoneInfo(tzinfo_string) + except ZoneInfoNotFoundError: + tzinfo = None if tzinfo is None: raise ParserError(f"Could not parse timezone expression {tzinfo_string!r}.") diff --git a/pyproject.toml b/pyproject.toml index 3e236917..71e75010 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,6 +25,8 @@ classifiers = [ ] dependencies = [ "python-dateutil>=2.7.0", + "backports.zoneinfo==0.2.1;python_version<'3.9'", + "tzdata;python_version>='3.9'", ] requires-python = ">=3.8" description = "Better dates & times for Python" @@ -41,13 +43,12 @@ dynamic = ["version"] [project.optional-dependencies] test = [ - "backports.zoneinfo==0.2.1;python_version<'3.9'", "dateparser==1.*", "pre-commit", "pytest", "pytest-cov", "pytest-mock", - "pytz==2021.1", + "pytz==2025.2", "simplejson==3.*", ] doc = [ diff --git a/requirements/requirements-tests.txt b/requirements/requirements-tests.txt index 94e5cc84..d9f660e3 100644 --- a/requirements/requirements-tests.txt +++ b/requirements/requirements-tests.txt @@ -1,10 +1,9 @@ -r requirements.txt -backports.zoneinfo==0.2.1;python_version<'3.9' dateparser==1.* pre-commit pytest pytest-cov pytest-mock -pytz==2021.1 +pytz==2025.2 simplejson==3.* types-python-dateutil>=2.8.10 diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 65134a19..fa333102 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -1 +1,3 @@ +backports.zoneinfo==0.2.1;python_version<'3.9' python-dateutil>=2.7.0 +tzdata;python_version>='3.9' diff --git a/tests/conftest.py b/tests/conftest.py index 5d5b9980..b6127073 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,7 +1,11 @@ from datetime import datetime import pytest -from dateutil import tz as dateutil_tz + +try: + from zoneinfo import ZoneInfo +except ImportError: + from backports.zoneinfo import ZoneInfo from arrow import arrow, factory, formatter, locales, parser @@ -32,7 +36,7 @@ def time_2013_02_15(request): @pytest.fixture(scope="class") def time_1975_12_25(request): request.cls.datetime = datetime( - 1975, 12, 25, 14, 15, 16, tzinfo=dateutil_tz.gettz("America/New_York") + 1975, 12, 25, 14, 15, 16, tzinfo=ZoneInfo("America/New_York") ) request.cls.arrow = arrow.Arrow.fromdatetime(request.cls.datetime) diff --git a/tests/test_arrow.py b/tests/test_arrow.py index 5afe9baa..c682fa3d 100644 --- a/tests/test_arrow.py +++ b/tests/test_arrow.py @@ -1,7 +1,7 @@ try: - import zoneinfo + from zoneinfo import ZoneInfo except ImportError: - from backports import zoneinfo + from backports.zoneinfo import ZoneInfo import pickle import sys @@ -54,10 +54,10 @@ def test_init(self): assert result._datetime == self.expected result = arrow.Arrow( - 2013, 2, 2, 12, 30, 45, 999999, tzinfo=tz.gettz("Europe/Paris") + 2013, 2, 2, 12, 30, 45, 999999, tzinfo=ZoneInfo("Europe/Paris") ) self.expected = datetime( - 2013, 2, 2, 12, 30, 45, 999999, tzinfo=tz.gettz("Europe/Paris") + 2013, 2, 2, 12, 30, 45, 999999, tzinfo=ZoneInfo("Europe/Paris") ) assert result._datetime == self.expected @@ -67,17 +67,27 @@ def test_init_pytz_timezone(self): 2013, 2, 2, 12, 30, 45, 999999, tzinfo=pytz.timezone("Europe/Paris") ) self.expected = datetime( - 2013, 2, 2, 12, 30, 45, 999999, tzinfo=tz.gettz("Europe/Paris") + 2013, 2, 2, 12, 30, 45, 999999, tzinfo=ZoneInfo("Europe/Paris") ) assert result._datetime == self.expected assert_datetime_equality(result._datetime, self.expected, 1) def test_init_zoneinfo_timezone(self): result = arrow.Arrow( - 2024, 7, 10, 18, 55, 45, 999999, tzinfo=zoneinfo.ZoneInfo("Europe/Paris") + 2024, 7, 10, 18, 55, 45, 999999, tzinfo=ZoneInfo("Europe/Paris") ) self.expected = datetime( - 2024, 7, 10, 18, 55, 45, 999999, tzinfo=zoneinfo.ZoneInfo("Europe/Paris") + 2024, 7, 10, 18, 55, 45, 999999, tzinfo=ZoneInfo("Europe/Paris") + ) + assert result._datetime == self.expected + assert_datetime_equality(result._datetime, self.expected, 1) + + def test_init_dateutil_timezone(self): + result = arrow.Arrow( + 2024, 7, 10, 18, 55, 45, 999999, tzinfo=tz.gettz("Europe/Paris") + ) + self.expected = datetime( + 2024, 7, 10, 18, 55, 45, 999999, tzinfo=ZoneInfo("Europe/Paris") ) assert result._datetime == self.expected assert_datetime_equality(result._datetime, self.expected, 1) @@ -98,15 +108,13 @@ class TestTestArrowFactory: def test_now(self): result = arrow.Arrow.now() - assert_datetime_equality( - result._datetime, datetime.now().replace(tzinfo=tz.tzlocal()) - ) + assert_datetime_equality(result._datetime, datetime.now().astimezone()) def test_utcnow(self): result = arrow.Arrow.utcnow() assert_datetime_equality( - result._datetime, datetime.now(timezone.utc).replace(tzinfo=tz.tzutc()) + result._datetime, datetime.now(timezone.utc).replace(tzinfo=timezone.utc) ) assert result.fold == 0 @@ -115,20 +123,18 @@ def test_fromtimestamp(self): timestamp = time.time() result = arrow.Arrow.fromtimestamp(timestamp) - assert_datetime_equality( - result._datetime, datetime.now().replace(tzinfo=tz.tzlocal()) - ) + assert_datetime_equality(result._datetime, datetime.now().astimezone()) - result = arrow.Arrow.fromtimestamp(timestamp, tzinfo=tz.gettz("Europe/Paris")) + result = arrow.Arrow.fromtimestamp(timestamp, tzinfo=ZoneInfo("Europe/Paris")) assert_datetime_equality( result._datetime, - datetime.fromtimestamp(timestamp, tz.gettz("Europe/Paris")), + datetime.fromtimestamp(timestamp, ZoneInfo("Europe/Paris")), ) result = arrow.Arrow.fromtimestamp(timestamp, tzinfo="Europe/Paris") assert_datetime_equality( result._datetime, - datetime.fromtimestamp(timestamp, tz.gettz("Europe/Paris")), + datetime.fromtimestamp(timestamp, ZoneInfo("Europe/Paris")), ) with pytest.raises(ValueError): @@ -139,7 +145,7 @@ def test_utcfromtimestamp(self): result = arrow.Arrow.utcfromtimestamp(timestamp) assert_datetime_equality( - result._datetime, datetime.now(timezone.utc).replace(tzinfo=tz.tzutc()) + result._datetime, datetime.now(timezone.utc).replace(tzinfo=timezone.utc) ) with pytest.raises(ValueError): @@ -153,25 +159,25 @@ def test_fromdatetime(self): assert result._datetime == dt.replace(tzinfo=tz.tzutc()) def test_fromdatetime_dt_tzinfo(self): - dt = datetime(2013, 2, 3, 12, 30, 45, 1, tzinfo=tz.gettz("US/Pacific")) + dt = datetime(2013, 2, 3, 12, 30, 45, 1, tzinfo=ZoneInfo("US/Pacific")) result = arrow.Arrow.fromdatetime(dt) - assert result._datetime == dt.replace(tzinfo=tz.gettz("US/Pacific")) + assert result._datetime == dt.replace(tzinfo=ZoneInfo("US/Pacific")) def test_fromdatetime_tzinfo_arg(self): dt = datetime(2013, 2, 3, 12, 30, 45, 1) - result = arrow.Arrow.fromdatetime(dt, tz.gettz("US/Pacific")) + result = arrow.Arrow.fromdatetime(dt, ZoneInfo("US/Pacific")) - assert result._datetime == dt.replace(tzinfo=tz.gettz("US/Pacific")) + assert result._datetime == dt.replace(tzinfo=ZoneInfo("US/Pacific")) def test_fromdate(self): dt = date(2013, 2, 3) - result = arrow.Arrow.fromdate(dt, tz.gettz("US/Pacific")) + result = arrow.Arrow.fromdate(dt, ZoneInfo("US/Pacific")) - assert result._datetime == datetime(2013, 2, 3, tzinfo=tz.gettz("US/Pacific")) + assert result._datetime == datetime(2013, 2, 3, tzinfo=ZoneInfo("US/Pacific")) def test_strptime(self): formatted = datetime(2013, 2, 3, 12, 30, 45).strftime("%Y-%m-%d %H:%M:%S") @@ -180,10 +186,10 @@ def test_strptime(self): assert result._datetime == datetime(2013, 2, 3, 12, 30, 45, tzinfo=tz.tzutc()) result = arrow.Arrow.strptime( - formatted, "%Y-%m-%d %H:%M:%S", tzinfo=tz.gettz("Europe/Paris") + formatted, "%Y-%m-%d %H:%M:%S", tzinfo=ZoneInfo("Europe/Paris") ) assert result._datetime == datetime( - 2013, 2, 3, 12, 30, 45, tzinfo=tz.gettz("Europe/Paris") + 2013, 2, 3, 12, 30, 45, tzinfo=ZoneInfo("Europe/Paris") ) def test_fromordinal(self): @@ -277,7 +283,7 @@ def test_getattr_dt_value(self): assert self.arrow.year == 2013 def test_tzinfo(self): - assert self.arrow.tzinfo == tz.tzutc() + assert self.arrow.tzinfo == timezone.utc def test_naive(self): assert self.arrow.naive == self.arrow._datetime.replace(tzinfo=None) @@ -432,7 +438,7 @@ def test_timetz(self): assert result == self.arrow._datetime.timetz() def test_astimezone(self): - other_tz = tz.gettz("US/Pacific") + other_tz = ZoneInfo("US/Pacific") result = self.arrow.astimezone(other_tz) @@ -538,20 +544,20 @@ class TestArrowFalsePositiveDst: def test_dst(self): self.before_1 = arrow.Arrow( - 2016, 11, 6, 3, 59, tzinfo=tz.gettz("America/New_York") + 2016, 11, 6, 3, 59, tzinfo=ZoneInfo("America/New_York") ) - self.before_2 = arrow.Arrow(2016, 11, 6, tzinfo=tz.gettz("America/New_York")) - self.after_1 = arrow.Arrow(2016, 11, 6, 4, tzinfo=tz.gettz("America/New_York")) + self.before_2 = arrow.Arrow(2016, 11, 6, tzinfo=ZoneInfo("America/New_York")) + self.after_1 = arrow.Arrow(2016, 11, 6, 4, tzinfo=ZoneInfo("America/New_York")) self.after_2 = arrow.Arrow( - 2016, 11, 6, 23, 59, tzinfo=tz.gettz("America/New_York") + 2016, 11, 6, 23, 59, tzinfo=ZoneInfo("America/New_York") ) self.before_3 = arrow.Arrow( - 2018, 11, 4, 3, 59, tzinfo=tz.gettz("America/New_York") + 2018, 11, 4, 3, 59, tzinfo=ZoneInfo("America/New_York") ) - self.before_4 = arrow.Arrow(2018, 11, 4, tzinfo=tz.gettz("America/New_York")) - self.after_3 = arrow.Arrow(2018, 11, 4, 4, tzinfo=tz.gettz("America/New_York")) + self.before_4 = arrow.Arrow(2018, 11, 4, tzinfo=ZoneInfo("America/New_York")) + self.after_3 = arrow.Arrow(2018, 11, 4, 4, tzinfo=ZoneInfo("America/New_York")) self.after_4 = arrow.Arrow( - 2018, 11, 4, 23, 59, tzinfo=tz.gettz("America/New_York") + 2018, 11, 4, 23, 59, tzinfo=ZoneInfo("America/New_York") ) assert self.before_1.day == self.before_2.day assert self.after_1.day == self.after_2.day @@ -562,9 +568,9 @@ def test_dst(self): class TestArrowConversion: def test_to(self): dt_from = datetime.now() - arrow_from = arrow.Arrow.fromdatetime(dt_from, tz.gettz("US/Pacific")) + arrow_from = arrow.Arrow.fromdatetime(dt_from, ZoneInfo("US/Pacific")) - self.expected = dt_from.replace(tzinfo=tz.gettz("US/Pacific")).astimezone( + self.expected = dt_from.replace(tzinfo=ZoneInfo("US/Pacific")).astimezone( tz.tzutc() ) @@ -592,7 +598,7 @@ def test_to_israel_same_offset(self): # issue 315 def test_anchorage_dst(self): before = arrow.Arrow(2016, 3, 13, 1, 59, tzinfo="America/Anchorage") - after = arrow.Arrow(2016, 3, 13, 2, 1, tzinfo="America/Anchorage") + after = arrow.Arrow(2016, 3, 13, 3, 1, tzinfo="America/Anchorage") assert before.utcoffset() != after.utcoffset() @@ -652,9 +658,9 @@ def test_replace(self): def test_replace_tzinfo(self): arw = arrow.Arrow.utcnow().to("US/Eastern") - result = arw.replace(tzinfo=tz.gettz("US/Pacific")) + result = arw.replace(tzinfo=ZoneInfo("US/Pacific")) - assert result == arw.datetime.replace(tzinfo=tz.gettz("US/Pacific")) + assert result == arw.datetime.replace(tzinfo=ZoneInfo("US/Pacific")) def test_replace_fold(self): before = arrow.Arrow(2017, 11, 5, 1, tzinfo="America/New_York") @@ -863,12 +869,12 @@ def test_shift_negative_imaginary(self): ) def test_shift_with_imaginary_check(self): - dt = arrow.Arrow(2024, 3, 10, 2, 30, tzinfo=tz.gettz("US/Eastern")) + dt = arrow.Arrow(2024, 3, 10, 2, 30, tzinfo=ZoneInfo("US/Eastern")) shifted = dt.shift(hours=1) assert shifted.datetime.hour == 3 def test_shift_without_imaginary_check(self): - dt = arrow.Arrow(2024, 3, 10, 2, 30, tzinfo=tz.gettz("US/Eastern")) + dt = arrow.Arrow(2024, 3, 10, 2, 30, tzinfo=ZoneInfo("US/Eastern")) shifted = dt.shift(hours=1, check_imaginary=False) assert shifted.datetime.hour == 3 @@ -1033,38 +1039,38 @@ def test_naive_tz(self): ) for r in result: - assert r.tzinfo == tz.gettz("US/Pacific") + assert r.tzinfo == ZoneInfo("US/Pacific") def test_aware_same_tz(self): result = arrow.Arrow.range( "day", - arrow.Arrow(2013, 1, 1, tzinfo=tz.gettz("US/Pacific")), - arrow.Arrow(2013, 1, 3, tzinfo=tz.gettz("US/Pacific")), + arrow.Arrow(2013, 1, 1, tzinfo=ZoneInfo("US/Pacific")), + arrow.Arrow(2013, 1, 3, tzinfo=ZoneInfo("US/Pacific")), ) for r in result: - assert r.tzinfo == tz.gettz("US/Pacific") + assert r.tzinfo == ZoneInfo("US/Pacific") def test_aware_different_tz(self): result = arrow.Arrow.range( "day", - datetime(2013, 1, 1, tzinfo=tz.gettz("US/Eastern")), - datetime(2013, 1, 3, tzinfo=tz.gettz("US/Pacific")), + datetime(2013, 1, 1, tzinfo=ZoneInfo("US/Eastern")), + datetime(2013, 1, 3, tzinfo=ZoneInfo("US/Pacific")), ) for r in result: - assert r.tzinfo == tz.gettz("US/Eastern") + assert r.tzinfo == ZoneInfo("US/Eastern") def test_aware_tz(self): result = arrow.Arrow.range( "day", - datetime(2013, 1, 1, tzinfo=tz.gettz("US/Eastern")), - datetime(2013, 1, 3, tzinfo=tz.gettz("US/Pacific")), - tz=tz.gettz("US/Central"), + datetime(2013, 1, 1, tzinfo=ZoneInfo("US/Eastern")), + datetime(2013, 1, 3, tzinfo=ZoneInfo("US/Pacific")), + tz=ZoneInfo("US/Central"), ) for r in result: - assert r.tzinfo == tz.gettz("US/Central") + assert r.tzinfo == ZoneInfo("US/Central") def test_imaginary(self): # issue #72, avoid duplication in utc column @@ -1345,7 +1351,7 @@ def test_second(self): ] def test_naive_tz(self): - tzinfo = tz.gettz("US/Pacific") + tzinfo = ZoneInfo("US/Pacific") result = arrow.Arrow.span_range( "hour", datetime(2013, 1, 1, 0), datetime(2013, 1, 1, 3, 59), "US/Pacific" @@ -1356,7 +1362,7 @@ def test_naive_tz(self): assert c.tzinfo == tzinfo def test_aware_same_tz(self): - tzinfo = tz.gettz("US/Pacific") + tzinfo = ZoneInfo("US/Pacific") result = arrow.Arrow.span_range( "hour", @@ -1369,8 +1375,8 @@ def test_aware_same_tz(self): assert c.tzinfo == tzinfo def test_aware_different_tz(self): - tzinfo1 = tz.gettz("US/Pacific") - tzinfo2 = tz.gettz("US/Eastern") + tzinfo1 = ZoneInfo("US/Pacific") + tzinfo2 = ZoneInfo("US/Eastern") result = arrow.Arrow.span_range( "hour", @@ -1385,14 +1391,14 @@ def test_aware_different_tz(self): def test_aware_tz(self): result = arrow.Arrow.span_range( "hour", - datetime(2013, 1, 1, 0, tzinfo=tz.gettz("US/Eastern")), - datetime(2013, 1, 1, 2, 59, tzinfo=tz.gettz("US/Eastern")), + datetime(2013, 1, 1, 0, tzinfo=ZoneInfo("US/Eastern")), + datetime(2013, 1, 1, 2, 59, tzinfo=ZoneInfo("US/Eastern")), tz="US/Central", ) for f, c in result: - assert f.tzinfo == tz.gettz("US/Central") - assert c.tzinfo == tz.gettz("US/Central") + assert f.tzinfo == ZoneInfo("US/Central") + assert c.tzinfo == ZoneInfo("US/Central") def test_bounds_param_is_passed(self): result = list( diff --git a/tests/test_factory.py b/tests/test_factory.py index 0ee9c4e0..056cee41 100644 --- a/tests/test_factory.py +++ b/tests/test_factory.py @@ -5,6 +5,11 @@ import pytest from dateutil import tz +try: + from zoneinfo import ZoneInfo +except ImportError: + from backports.zoneinfo import ZoneInfo + from arrow import Arrow from arrow.parser import ParserError @@ -15,7 +20,7 @@ class TestGet: def test_no_args(self): assert_datetime_equality( - self.factory.get(), datetime.now(timezone.utc).replace(tzinfo=tz.tzutc()) + self.factory.get(), datetime.now(timezone.utc).replace(tzinfo=timezone.utc) ) def test_timestamp_one_arg_no_arg(self): @@ -31,12 +36,12 @@ def test_one_arg_none(self): def test_struct_time(self): assert_datetime_equality( self.factory.get(time.gmtime()), - datetime.now(timezone.utc).replace(tzinfo=tz.tzutc()), + datetime.now(timezone.utc).replace(tzinfo=timezone.utc), ) def test_one_arg_timestamp(self): int_timestamp = int(time.time()) - timestamp_dt = datetime.utcfromtimestamp(int_timestamp).replace( + timestamp_dt = datetime.fromtimestamp(int_timestamp, timezone.utc).replace( tzinfo=tz.tzutc() ) @@ -46,7 +51,7 @@ def test_one_arg_timestamp(self): self.factory.get(str(int_timestamp)) float_timestamp = time.time() - timestamp_dt = datetime.utcfromtimestamp(float_timestamp).replace( + timestamp_dt = datetime.fromtimestamp(float_timestamp, timezone.utc).replace( tzinfo=tz.tzutc() ) @@ -66,19 +71,19 @@ def test_one_arg_expanded_timestamp(self): microsecond_timestamp = 1591328104308505 # Regression test for issue #796 - assert self.factory.get(millisecond_timestamp) == datetime.utcfromtimestamp( - 1591328104.308 + assert self.factory.get(millisecond_timestamp) == datetime.fromtimestamp( + 1591328104.308, timezone.utc ).replace(tzinfo=tz.tzutc()) - assert self.factory.get(microsecond_timestamp) == datetime.utcfromtimestamp( - 1591328104.308505 + assert self.factory.get(microsecond_timestamp) == datetime.fromtimestamp( + 1591328104.308505, timezone.utc ).replace(tzinfo=tz.tzutc()) def test_one_arg_timestamp_with_tzinfo(self): timestamp = time.time() timestamp_dt = datetime.fromtimestamp(timestamp, tz=tz.tzutc()).astimezone( - tz.gettz("US/Pacific") + ZoneInfo("US/Pacific") ) - timezone = tz.gettz("US/Pacific") + timezone = ZoneInfo("US/Pacific") assert_datetime_equality( self.factory.get(timestamp, tzinfo=timezone), timestamp_dt @@ -105,11 +110,11 @@ def test_one_arg_tzinfo(self): self.expected = ( datetime.now(timezone.utc) .replace(tzinfo=tz.tzutc()) - .astimezone(tz.gettz("US/Pacific")) + .astimezone(ZoneInfo("US/Pacific")) ) assert_datetime_equality( - self.factory.get(tz.gettz("US/Pacific")), self.expected + self.factory.get(ZoneInfo("US/Pacific")), self.expected ) # regression test for issue #658 @@ -125,18 +130,18 @@ def test_kwarg_tzinfo(self): self.expected = ( datetime.now(timezone.utc) .replace(tzinfo=tz.tzutc()) - .astimezone(tz.gettz("US/Pacific")) + .astimezone(ZoneInfo("US/Pacific")) ) assert_datetime_equality( - self.factory.get(tzinfo=tz.gettz("US/Pacific")), self.expected + self.factory.get(tzinfo=ZoneInfo("US/Pacific")), self.expected ) def test_kwarg_tzinfo_string(self): self.expected = ( datetime.now(timezone.utc) .replace(tzinfo=tz.tzutc()) - .astimezone(tz.gettz("US/Pacific")) + .astimezone(ZoneInfo("US/Pacific")) ) assert_datetime_equality(self.factory.get(tzinfo="US/Pacific"), self.expected) @@ -168,7 +173,7 @@ def test_one_arg_datetime_tzinfo_kwarg(self): result = self.factory.get(dt, tzinfo="America/Chicago") - expected = datetime(2021, 4, 29, 6, tzinfo=tz.gettz("America/Chicago")) + expected = datetime(2021, 4, 29, 6, tzinfo=ZoneInfo("America/Chicago")) assert_datetime_equality(result._datetime, expected) @@ -177,7 +182,7 @@ def test_one_arg_arrow_tzinfo_kwarg(self): result = self.factory.get(arw, tzinfo="America/Chicago") - expected = datetime(2021, 4, 29, 6, tzinfo=tz.gettz("America/Chicago")) + expected = datetime(2021, 4, 29, 6, tzinfo=ZoneInfo("America/Chicago")) assert_datetime_equality(result._datetime, expected) @@ -186,7 +191,7 @@ def test_one_arg_date_tzinfo_kwarg(self): result = self.factory.get(da, tzinfo="America/Chicago") - expected = Arrow(2021, 4, 29, tzinfo=tz.gettz("America/Chicago")) + expected = Arrow(2021, 4, 29, tzinfo=ZoneInfo("America/Chicago")) assert result.date() == expected.date() assert result.tzinfo == expected.tzinfo @@ -201,9 +206,7 @@ def test_one_arg_iso_calendar_tzinfo_kwarg(self): def test_one_arg_iso_str(self): dt = datetime.now(timezone.utc) - assert_datetime_equality( - self.factory.get(dt.isoformat()), dt.replace(tzinfo=tz.tzutc()) - ) + assert_datetime_equality(self.factory.get(dt.isoformat()), dt) def test_one_arg_iso_calendar(self): pairs = [ @@ -252,24 +255,24 @@ def test_one_arg_decimal(self): ) def test_two_args_datetime_tzinfo(self): - result = self.factory.get(datetime(2013, 1, 1), tz.gettz("US/Pacific")) + result = self.factory.get(datetime(2013, 1, 1), ZoneInfo("US/Pacific")) - assert result._datetime == datetime(2013, 1, 1, tzinfo=tz.gettz("US/Pacific")) + assert result._datetime == datetime(2013, 1, 1, tzinfo=ZoneInfo("US/Pacific")) def test_two_args_datetime_tz_str(self): result = self.factory.get(datetime(2013, 1, 1), "US/Pacific") - assert result._datetime == datetime(2013, 1, 1, tzinfo=tz.gettz("US/Pacific")) + assert result._datetime == datetime(2013, 1, 1, tzinfo=ZoneInfo("US/Pacific")) def test_two_args_date_tzinfo(self): - result = self.factory.get(date(2013, 1, 1), tz.gettz("US/Pacific")) + result = self.factory.get(date(2013, 1, 1), ZoneInfo("US/Pacific")) - assert result._datetime == datetime(2013, 1, 1, tzinfo=tz.gettz("US/Pacific")) + assert result._datetime == datetime(2013, 1, 1, tzinfo=ZoneInfo("US/Pacific")) def test_two_args_date_tz_str(self): result = self.factory.get(date(2013, 1, 1), "US/Pacific") - assert result._datetime == datetime(2013, 1, 1, tzinfo=tz.gettz("US/Pacific")) + assert result._datetime == datetime(2013, 1, 1, tzinfo=ZoneInfo("US/Pacific")) def test_two_args_datetime_other(self): with pytest.raises(TypeError): @@ -285,10 +288,10 @@ def test_two_args_str_str(self): assert result._datetime == datetime(2013, 1, 1, tzinfo=tz.tzutc()) def test_two_args_str_tzinfo(self): - result = self.factory.get("2013-01-01", tzinfo=tz.gettz("US/Pacific")) + result = self.factory.get("2013-01-01", tzinfo=ZoneInfo("US/Pacific")) assert_datetime_equality( - result._datetime, datetime(2013, 1, 1, tzinfo=tz.gettz("US/Pacific")) + result._datetime, datetime(2013, 1, 1, tzinfo=ZoneInfo("US/Pacific")) ) def test_two_args_twitter_format(self): @@ -362,11 +365,11 @@ def test_locale(self): def test_locale_kwarg_only(self): res = self.factory.get(locale="ja") - assert res.tzinfo == tz.tzutc() + assert res.tzinfo == timezone.utc def test_locale_with_tzinfo(self): - res = self.factory.get(locale="ja", tzinfo=tz.gettz("Asia/Tokyo")) - assert res.tzinfo == tz.gettz("Asia/Tokyo") + res = self.factory.get(locale="ja", tzinfo=ZoneInfo("Asia/Tokyo")) + assert res.tzinfo == ZoneInfo("Asia/Tokyo") @pytest.mark.usefixtures("arrow_factory") @@ -374,19 +377,19 @@ class TestUtcNow: def test_utcnow(self): assert_datetime_equality( self.factory.utcnow()._datetime, - datetime.now(timezone.utc).replace(tzinfo=tz.tzutc()), + datetime.now(timezone.utc), ) @pytest.mark.usefixtures("arrow_factory") class TestNow: def test_no_tz(self): - assert_datetime_equality(self.factory.now(), datetime.now(tz.tzlocal())) + assert_datetime_equality(self.factory.now(), datetime.now().astimezone()) def test_tzinfo(self): assert_datetime_equality( - self.factory.now(tz.gettz("EST")), datetime.now(tz.gettz("EST")) + self.factory.now(ZoneInfo("EST")), datetime.now(ZoneInfo("EST")) ) def test_tz_str(self): - assert_datetime_equality(self.factory.now("EST"), datetime.now(tz.gettz("EST"))) + assert_datetime_equality(self.factory.now("EST"), datetime.now(ZoneInfo("EST"))) diff --git a/tests/test_formatter.py b/tests/test_formatter.py index 53868288..ff3fea28 100644 --- a/tests/test_formatter.py +++ b/tests/test_formatter.py @@ -1,7 +1,7 @@ try: - import zoneinfo + from zoneinfo import ZoneInfo except ImportError: - from backports import zoneinfo + from backports.zoneinfo import ZoneInfo from datetime import datetime, timezone @@ -118,7 +118,7 @@ def test_timestamp(self): assert self.formatter._format_token(dt, "x") == expected def test_timezone(self): - dt = datetime.now(timezone.utc).replace(tzinfo=dateutil_tz.gettz("US/Pacific")) + dt = datetime.now(timezone.utc).replace(tzinfo=ZoneInfo("US/Pacific")) result = self.formatter._format_token(dt, "ZZ") assert result == "-07:00" or result == "-08:00" @@ -129,8 +129,8 @@ def test_timezone(self): @pytest.mark.parametrize("full_tz_name", make_full_tz_list()) def test_timezone_formatter(self, full_tz_name): # This test will fail if we use "now" as date as soon as we change from/to DST - dt = datetime(1986, 2, 14, tzinfo=zoneinfo.ZoneInfo("UTC")).replace( - tzinfo=dateutil_tz.gettz(full_tz_name) + dt = datetime(1986, 2, 14, tzinfo=timezone.utc).replace( + tzinfo=ZoneInfo(full_tz_name) ) abbreviation = dt.tzname() diff --git a/tests/test_parser.py b/tests/test_parser.py index 3eb44d16..7038d880 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -1,11 +1,16 @@ import calendar import os import time -from datetime import datetime, timezone +from datetime import datetime, timedelta, timezone import pytest from dateutil import tz +try: + from zoneinfo import ZoneInfo +except ImportError: + from backports.zoneinfo import ZoneInfo + import arrow from arrow import formatter, parser from arrow.constants import MAX_TIMESTAMP_US @@ -319,7 +324,7 @@ def test_parse_tz_zz(self): @pytest.mark.parametrize("full_tz_name", make_full_tz_list()) def test_parse_tz_name_zzz(self, full_tz_name): - self.expected = datetime(2013, 1, 1, tzinfo=tz.gettz(full_tz_name)) + self.expected = datetime(2013, 1, 1, tzinfo=ZoneInfo(full_tz_name)) assert ( self.parser.parse(f"2013-01-01 {full_tz_name}", "YYYY-MM-DD ZZZ") == self.expected @@ -1309,36 +1314,40 @@ def test_midnight_end_day(self): @pytest.mark.usefixtures("tzinfo_parser") class TestTzinfoParser: def test_parse_local(self): - assert self.parser.parse("local") == tz.tzlocal() + from datetime import datetime + + assert self.parser.parse("local") == datetime.now().astimezone().tzinfo def test_parse_utc(self): - assert self.parser.parse("utc") == tz.tzutc() - assert self.parser.parse("UTC") == tz.tzutc() + assert self.parser.parse("utc") == timezone.utc + assert self.parser.parse("UTC") == timezone.utc def test_parse_utc_withoffset(self): - assert self.parser.parse("(UTC+01:00") == tz.tzoffset(None, 3600) - assert self.parser.parse("(UTC-01:00") == tz.tzoffset(None, -3600) - assert self.parser.parse("(UTC+01:00") == tz.tzoffset(None, 3600) + assert self.parser.parse("(UTC+01:00") == timezone(timedelta(seconds=3600)) + assert self.parser.parse("(UTC-01:00") == timezone(timedelta(seconds=-3600)) + assert self.parser.parse("(UTC+01:00") == timezone(timedelta(seconds=3600)) assert self.parser.parse( "(UTC+01:00) Amsterdam, Berlin, Bern, Rom, Stockholm, Wien" - ) == tz.tzoffset(None, 3600) + ) == timezone(timedelta(seconds=3600)) def test_parse_iso(self): - assert self.parser.parse("01:00") == tz.tzoffset(None, 3600) - assert self.parser.parse("11:35") == tz.tzoffset(None, 11 * 3600 + 2100) - assert self.parser.parse("+01:00") == tz.tzoffset(None, 3600) - assert self.parser.parse("-01:00") == tz.tzoffset(None, -3600) + assert self.parser.parse("01:00") == timezone(timedelta(seconds=3600)) + assert self.parser.parse("11:35") == timezone( + timedelta(seconds=11 * 3600 + 2100) + ) + assert self.parser.parse("+01:00") == timezone(timedelta(seconds=3600)) + assert self.parser.parse("-01:00") == timezone(timedelta(seconds=-3600)) - assert self.parser.parse("0100") == tz.tzoffset(None, 3600) - assert self.parser.parse("+0100") == tz.tzoffset(None, 3600) - assert self.parser.parse("-0100") == tz.tzoffset(None, -3600) + assert self.parser.parse("0100") == timezone(timedelta(seconds=3600)) + assert self.parser.parse("+0100") == timezone(timedelta(seconds=3600)) + assert self.parser.parse("-0100") == timezone(timedelta(seconds=-3600)) - assert self.parser.parse("01") == tz.tzoffset(None, 3600) - assert self.parser.parse("+01") == tz.tzoffset(None, 3600) - assert self.parser.parse("-01") == tz.tzoffset(None, -3600) + assert self.parser.parse("01") == timezone(timedelta(seconds=3600)) + assert self.parser.parse("+01") == timezone(timedelta(seconds=3600)) + assert self.parser.parse("-01") == timezone(timedelta(seconds=-3600)) def test_parse_str(self): - assert self.parser.parse("US/Pacific") == tz.gettz("US/Pacific") + assert self.parser.parse("US/Pacific") == ZoneInfo("US/Pacific") def test_parse_fails(self): with pytest.raises(parser.ParserError): diff --git a/tests/utils.py b/tests/utils.py index 7a74b7e4..834aee05 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -8,9 +8,29 @@ def make_full_tz_list(): dateutil_zones = set(get_zonefile_instance().zones) zoneinfo_zones = set(zoneinfo.available_timezones()) - return dateutil_zones.union(zoneinfo_zones) + # Since the tests create ZoneInfo objects, we can only use timezones + # that are available in zoneinfo. Filter out any dateutil-only timezones + # that are not available in zoneinfo (like Asia/Hanoi which was renamed to Asia/Ho_Chi_Minh) + all_zones = dateutil_zones.union(zoneinfo_zones) + return {tz for tz in all_zones if tz in zoneinfo_zones} def assert_datetime_equality(dt1, dt2, within=10): - assert dt1.tzinfo == dt2.tzinfo + # Compare timezone behavior instead of object identity for cross-platform compatibility + assert_timezone_equivalence(dt1.tzinfo, dt2.tzinfo, dt1) assert abs((dt1 - dt2).total_seconds()) < within + + +def assert_timezone_equivalence(tz1, tz2, dt): + # Timezone objects are equivalent + if tz1 == tz2: + return + + # Compare timezone names + assert tz1.tzname(dt) == tz2.tzname(dt) + + # Compare UTC offset and DST behavior at the given datetime + assert tz1.utcoffset(dt) == tz2.utcoffset( + dt + ), f"UTC offset mismatch: {tz1.utcoffset(dt)} != {tz2.utcoffset(dt)}" + assert tz1.dst(dt) == tz2.dst(dt), f"DST mismatch: {tz1.dst(dt)} != {tz2.dst(dt)}" From 86f7520f15b2dd46f9c49f71ce5c15bbc537ed67 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 20 Aug 2025 20:51:34 -0700 Subject: [PATCH 61/80] Bump actions/checkout from 4 to 5 (#1219) Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '5' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/continuous_integration.yml | 4 ++-- .github/workflows/release.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/continuous_integration.yml b/.github/workflows/continuous_integration.yml index c94ba640..0c8d8fa4 100644 --- a/.github/workflows/continuous_integration.yml +++ b/.github/workflows/continuous_integration.yml @@ -29,7 +29,7 @@ jobs: - os: windows-latest path: ~\AppData\Local\pip\Cache steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Cache pip uses: actions/cache@v4 with: @@ -55,7 +55,7 @@ jobs: name: Linting runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/cache@v4 with: path: ~/.cache/pip diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 84adac2f..95083c0b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -10,7 +10,7 @@ jobs: release-to-pypi: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/cache@v4 with: path: ~/.cache/pip From ea756ea779468b0c6a9f45439dc05cfd5e71ee1e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 7 Sep 2025 19:30:06 -0700 Subject: [PATCH 62/80] Bump actions/setup-python from 5 to 6 (#1220) Bumps [actions/setup-python](https://github.com/actions/setup-python) from 5 to 6. - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v5...v6) --- updated-dependencies: - dependency-name: actions/setup-python dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/continuous_integration.yml | 4 ++-- .github/workflows/release.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/continuous_integration.yml b/.github/workflows/continuous_integration.yml index 0c8d8fa4..e060685c 100644 --- a/.github/workflows/continuous_integration.yml +++ b/.github/workflows/continuous_integration.yml @@ -37,7 +37,7 @@ jobs: key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements*.txt') }} restore-keys: ${{ runner.os }}-pip- - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} - name: Install dependencies @@ -67,7 +67,7 @@ jobs: key: ${{ runner.os }}-pre-commit-${{ hashFiles('**/.pre-commit-config.yaml') }} restore-keys: ${{ runner.os }}-pre-commit- - name: Set up Python ${{ runner.python-version }} - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: "3.11" - name: Install dependencies diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 95083c0b..ae364d5c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,7 +16,7 @@ jobs: path: ~/.cache/pip key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements*.txt') }} restore-keys: ${{ runner.os }}-pip- - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: python-version: "3.11" - name: Install dependencies From 05cd9b47d0db1c22b44cb739832f1b9701855313 Mon Sep 17 00:00:00 2001 From: "Kristijan \"Fremen\" Velkovski" Date: Wed, 1 Oct 2025 21:29:46 -0500 Subject: [PATCH 63/80] fix humanize month limits (#1224) * fix month humanize * remove comment and extra coverage test as month humanize now works properly. --- arrow/arrow.py | 48 ++++++++++++++++++++++++++++++++------------- tests/test_arrow.py | 10 ---------- 2 files changed, 34 insertions(+), 24 deletions(-) diff --git a/arrow/arrow.py b/arrow/arrow.py index 99452556..d3d476e7 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -123,6 +123,7 @@ class Arrow: ] _ATTRS_PLURAL: Final[List[str]] = [f"{a}s" for a in _ATTRS] _MONTHS_PER_QUARTER: Final[int] = 3 + _MONTHS_PER_YEAR: Final[int] = 12 _SECS_PER_MINUTE: Final[int] = 60 _SECS_PER_HOUR: Final[int] = 60 * 60 _SECS_PER_DAY: Final[int] = 60 * 60 * 24 @@ -1189,6 +1190,7 @@ def humanize( elif diff < self._SECS_PER_MINUTE * 2: return locale.describe("minute", sign, only_distance=only_distance) + elif diff < self._SECS_PER_HOUR: minutes = sign * max(delta_second // self._SECS_PER_MINUTE, 2) return locale.describe( @@ -1197,36 +1199,54 @@ def humanize( elif diff < self._SECS_PER_HOUR * 2: return locale.describe("hour", sign, only_distance=only_distance) + elif diff < self._SECS_PER_DAY: hours = sign * max(delta_second // self._SECS_PER_HOUR, 2) return locale.describe("hours", hours, only_distance=only_distance) - elif diff < self._SECS_PER_DAY * 2: + + calendar_diff = ( + relativedelta(dt, self._datetime) + if self._datetime < dt + else relativedelta(self._datetime, dt) + ) + calendar_months = ( + calendar_diff.years * self._MONTHS_PER_YEAR + calendar_diff.months + ) + + # For months, if more than 2 weeks, count as a full month + if calendar_diff.days > 14: + calendar_months += 1 + + calendar_months = min(calendar_months, self._MONTHS_PER_YEAR) + + if diff < self._SECS_PER_DAY * 2: return locale.describe("day", sign, only_distance=only_distance) + elif diff < self._SECS_PER_WEEK: days = sign * max(delta_second // self._SECS_PER_DAY, 2) return locale.describe("days", days, only_distance=only_distance) + elif calendar_months >= 1 and diff < self._SECS_PER_YEAR: + if calendar_months == 1: + return locale.describe( + "month", sign, only_distance=only_distance + ) + else: + months = sign * calendar_months + return locale.describe( + "months", months, only_distance=only_distance + ) + elif diff < self._SECS_PER_WEEK * 2: return locale.describe("week", sign, only_distance=only_distance) + elif diff < self._SECS_PER_MONTH: weeks = sign * max(delta_second // self._SECS_PER_WEEK, 2) return locale.describe("weeks", weeks, only_distance=only_distance) - elif diff < self._SECS_PER_MONTH * 2: - return locale.describe("month", sign, only_distance=only_distance) - elif diff < self._SECS_PER_YEAR: - # TODO revisit for humanization during leap years - self_months = self._datetime.year * 12 + self._datetime.month - other_months = dt.year * 12 + dt.month - - months = sign * max(abs(other_months - self_months), 2) - - return locale.describe( - "months", months, only_distance=only_distance - ) - elif diff < self._SECS_PER_YEAR * 2: return locale.describe("year", sign, only_distance=only_distance) + else: years = sign * max(delta_second // self._SECS_PER_YEAR, 2) return locale.describe("years", years, only_distance=only_distance) diff --git a/tests/test_arrow.py b/tests/test_arrow.py index c682fa3d..9daee5e4 100644 --- a/tests/test_arrow.py +++ b/tests/test_arrow.py @@ -2058,25 +2058,15 @@ def test_weeks(self): assert self.now.humanize(later, only_distance=True) == "2 weeks" assert later.humanize(self.now, only_distance=True) == "2 weeks" - @pytest.mark.xfail(reason="known issue with humanize month limits") def test_month(self): later = self.now.shift(months=1) - # TODO this test now returns "4 weeks ago", we need to fix this to be correct on a per month basis assert self.now.humanize(later) == "a month ago" assert later.humanize(self.now) == "in a month" assert self.now.humanize(later, only_distance=True) == "a month" assert later.humanize(self.now, only_distance=True) == "a month" - def test_month_plus_4_days(self): - # TODO needed for coverage, remove when month limits are fixed - later = self.now.shift(months=1, days=4) - - assert self.now.humanize(later) == "a month ago" - assert later.humanize(self.now) == "in a month" - - @pytest.mark.xfail(reason="known issue with humanize month limits") def test_months(self): later = self.now.shift(months=2) earlier = self.now.shift(months=-2) From 7ccbe66122ca53d90acf9c69c37aa95c81a1fe89 Mon Sep 17 00:00:00 2001 From: "Kristijan \"Fremen\" Velkovski" Date: Wed, 1 Oct 2025 22:16:43 -0500 Subject: [PATCH 64/80] add codecov test results (#1223) Co-authored-by: Jad Chaar --- .github/workflows/continuous_integration.yml | 5 +++++ tox.ini | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/continuous_integration.yml b/.github/workflows/continuous_integration.yml index e060685c..e0776f95 100644 --- a/.github/workflows/continuous_integration.yml +++ b/.github/workflows/continuous_integration.yml @@ -50,6 +50,11 @@ jobs: uses: codecov/codecov-action@v5 with: file: coverage.xml + - name: Upload test results to Codecov + if: ${{ !cancelled() }} + uses: codecov/test-results-action@v1 + with: + token: ${{ secrets.CODECOV_TOKEN }} linting: name: Linting diff --git a/tox.ini b/tox.ini index 51c2c6a6..3400453f 100644 --- a/tox.ini +++ b/tox.ini @@ -45,7 +45,7 @@ allowlist_externals = flit commands = flit publish --setup-py [pytest] -addopts = -v --cov-branch --cov=arrow --cov-fail-under=99 --cov-report=term-missing --cov-report=xml +addopts = -v --cov-branch --cov=arrow --cov-fail-under=99 --cov-report=term-missing --cov-report=xml --junitxml=junit.xml -o junit_family=legacy testpaths = tests [isort] From 4c8c44aefc0e5504b9c3cbf0d6adbb0ec4aa4571 Mon Sep 17 00:00:00 2001 From: "Kristijan \"Fremen\" Velkovski" Date: Wed, 1 Oct 2025 22:44:02 -0500 Subject: [PATCH 65/80] Add week_start parameter to floor() and ceil() (#1222) * add kwargs to ceil and floor. pass it through to span. * add to guide.rst --------- Co-authored-by: Jad Chaar --- arrow/arrow.py | 22 +++++-- docs/guide.rst | 6 ++ tests/test_arrow.py | 156 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 178 insertions(+), 6 deletions(-) diff --git a/arrow/arrow.py b/arrow/arrow.py index d3d476e7..d40927ed 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -550,14 +550,14 @@ def span( (, ) """ - if not 1 <= week_start <= 7: - raise ValueError("week_start argument must be between 1 and 7.") util.validate_bounds(bounds) frame_absolute, frame_relative, relative_steps = self._get_frames(frame) if frame_absolute == "week": + if not 1 <= week_start <= 7: + raise ValueError("week_start argument must be between 1 and 7.") attr = "day" elif frame_absolute == "quarter": attr = "month" @@ -595,39 +595,49 @@ def span( return floor, ceil - def floor(self, frame: _T_FRAMES) -> "Arrow": + def floor(self, frame: _T_FRAMES, **kwargs: Any) -> "Arrow": """Returns a new :class:`Arrow ` object, representing the "floor" of the timespan of the :class:`Arrow ` object in a given timeframe. Equivalent to the first element in the 2-tuple returned by :func:`span `. :param frame: the timeframe. Can be any ``datetime`` property (day, hour, minute...). + :param week_start: (optional) only used in combination with the week timeframe. Follows isoweekday() where + Monday is 1 and Sunday is 7. Usage:: >>> arrow.utcnow().floor('hour') + >>> arrow.utcnow().floor('week', week_start=7) + + """ - return self.span(frame)[0] + return self.span(frame, **kwargs)[0] - def ceil(self, frame: _T_FRAMES) -> "Arrow": + def ceil(self, frame: _T_FRAMES, **kwargs: Any) -> "Arrow": """Returns a new :class:`Arrow ` object, representing the "ceiling" of the timespan of the :class:`Arrow ` object in a given timeframe. Equivalent to the second element in the 2-tuple returned by :func:`span `. :param frame: the timeframe. Can be any ``datetime`` property (day, hour, minute...). + :param week_start: (optional) only used in combination with the week timeframe. Follows isoweekday() where + Monday is 1 and Sunday is 7. Usage:: >>> arrow.utcnow().ceil('hour') + >>> arrow.utcnow().ceil('week', week_start=7) + + """ - return self.span(frame)[1] + return self.span(frame, **kwargs)[1] @classmethod def span_range( diff --git a/docs/guide.rst b/docs/guide.rst index 5bdf337d..5e0e697a 100644 --- a/docs/guide.rst +++ b/docs/guide.rst @@ -303,6 +303,12 @@ Or just get the floor and ceiling: >>> arrow.utcnow().ceil('hour') + >>> arrow.utcnow().floor('week', week_start=7) + + + >>> arrow.utcnow().ceil('week', week_start=7) + + You can also get a range of time spans: .. code-block:: python diff --git a/tests/test_arrow.py b/tests/test_arrow.py index 9daee5e4..b595e4e2 100644 --- a/tests/test_arrow.py +++ b/tests/test_arrow.py @@ -1727,6 +1727,162 @@ def test_floor(self): assert floor == self.arrow.floor("month") assert ceil == self.arrow.ceil("month") + def test_floor_week_start(self): + """ + Test floor method with week_start parameter for different week starts. + """ + # Test with default week_start=1 (Monday) + floor_default = self.arrow.floor("week") + floor_span_default, _ = self.arrow.span("week") + assert floor_default == floor_span_default + + # Test with week_start=1 (Monday) - explicit + floor_monday = self.arrow.floor("week", week_start=1) + floor_span_monday, _ = self.arrow.span("week", week_start=1) + assert floor_monday == floor_span_monday + + # Test with week_start=7 (Sunday) + floor_sunday = self.arrow.floor("week", week_start=7) + floor_span_sunday, _ = self.arrow.span("week", week_start=7) + assert floor_sunday == floor_span_sunday + + # Test with week_start=6 (Saturday) + floor_saturday = self.arrow.floor("week", week_start=6) + floor_span_saturday, _ = self.arrow.span("week", week_start=6) + assert floor_saturday == floor_span_saturday + + # Test with week_start=2 (Tuesday) + floor_tuesday = self.arrow.floor("week", week_start=2) + floor_span_tuesday, _ = self.arrow.span("week", week_start=2) + assert floor_tuesday == floor_span_tuesday + + def test_ceil_week_start(self): + """ + Test ceil method with week_start parameter for different week starts. + """ + # Test with default week_start=1 (Monday) + ceil_default = self.arrow.ceil("week") + _, ceil_span_default = self.arrow.span("week") + assert ceil_default == ceil_span_default + + # Test with week_start=1 (Monday) - explicit + ceil_monday = self.arrow.ceil("week", week_start=1) + _, ceil_span_monday = self.arrow.span("week", week_start=1) + assert ceil_monday == ceil_span_monday + + # Test with week_start=7 (Sunday) + ceil_sunday = self.arrow.ceil("week", week_start=7) + _, ceil_span_sunday = self.arrow.span("week", week_start=7) + assert ceil_sunday == ceil_span_sunday + + # Test with week_start=6 (Saturday) + ceil_saturday = self.arrow.ceil("week", week_start=6) + _, ceil_span_saturday = self.arrow.span("week", week_start=6) + assert ceil_saturday == ceil_span_saturday + + # Test with week_start=2 (Tuesday) + ceil_tuesday = self.arrow.ceil("week", week_start=2) + _, ceil_span_tuesday = self.arrow.span("week", week_start=2) + assert ceil_tuesday == ceil_span_tuesday + + def test_floor_ceil_week_start_values(self): + """ + Test specific date values for floor and ceil with different week_start values. + The test arrow is 2013-02-15 (Friday, isoweekday=5). + """ + # Test Monday start (week_start=1) + # Friday should floor to previous Monday (2013-02-11) + floor_mon = self.arrow.floor("week", week_start=1) + assert floor_mon == datetime(2013, 2, 11, tzinfo=tz.tzutc()) + # Friday should ceil to next Sunday (2013-02-17) + ceil_mon = self.arrow.ceil("week", week_start=1) + assert ceil_mon == datetime(2013, 2, 17, 23, 59, 59, 999999, tzinfo=tz.tzutc()) + + # Test Sunday start (week_start=7) + # Friday should floor to previous Sunday (2013-02-10) + floor_sun = self.arrow.floor("week", week_start=7) + assert floor_sun == datetime(2013, 2, 10, tzinfo=tz.tzutc()) + # Friday should ceil to next Saturday (2013-02-16) + ceil_sun = self.arrow.ceil("week", week_start=7) + assert ceil_sun == datetime(2013, 2, 16, 23, 59, 59, 999999, tzinfo=tz.tzutc()) + + # Test Saturday start (week_start=6) + # Friday should floor to previous Saturday (2013-02-09) + floor_sat = self.arrow.floor("week", week_start=6) + assert floor_sat == datetime(2013, 2, 9, tzinfo=tz.tzutc()) + # Friday should ceil to next Friday (2013-02-15) + ceil_sat = self.arrow.ceil("week", week_start=6) + assert ceil_sat == datetime(2013, 2, 15, 23, 59, 59, 999999, tzinfo=tz.tzutc()) + + def test_floor_ceil_week_start_backward_compatibility(self): + """ + Test that floor and ceil methods maintain backward compatibility + when called without the week_start parameter. + """ + # Test that calling floor/ceil without parameters works the same as before + floor_old = self.arrow.floor("week") + floor_new = self.arrow.floor("week", week_start=1) # default value + assert floor_old == floor_new + + ceil_old = self.arrow.ceil("week") + ceil_new = self.arrow.ceil("week", week_start=1) # default value + assert ceil_old == ceil_new + + def test_floor_ceil_week_start_ignored_for_non_week_frames(self): + """ + Test that week_start parameter is ignored for non-week frames. + """ + # Test that week_start parameter is ignored for different frames + for frame in ["hour", "day", "month", "year"]: + # floor should work the same with or without week_start for non-week frames + floor_without = self.arrow.floor(frame) + floor_with = self.arrow.floor(frame, week_start=7) # should be ignored + assert floor_without == floor_with + + # ceil should work the same with or without week_start for non-week frames + ceil_without = self.arrow.ceil(frame) + ceil_with = self.arrow.ceil(frame, week_start=7) # should be ignored + assert ceil_without == ceil_with + + def test_floor_ceil_week_start_validation(self): + """ + Test that week_start parameter validation works correctly for week frames. + """ + # Valid values should work for week frames + for week_start in range(1, 8): + self.arrow.floor("week", week_start=week_start) + self.arrow.ceil("week", week_start=week_start) + + # Invalid values should raise ValueError for week frames + with pytest.raises( + ValueError, match="week_start argument must be between 1 and 7" + ): + self.arrow.floor("week", week_start=0) + + with pytest.raises( + ValueError, match="week_start argument must be between 1 and 7" + ): + self.arrow.floor("week", week_start=8) + + with pytest.raises( + ValueError, match="week_start argument must be between 1 and 7" + ): + self.arrow.ceil("week", week_start=0) + + with pytest.raises( + ValueError, match="week_start argument must be between 1 and 7" + ): + self.arrow.ceil("week", week_start=8) + + # Invalid week_start values should be ignored for non-week frames (no validation) + # This ensures the parameter doesn't cause errors for other frames + for frame in ["hour", "day", "month", "year"]: + # These should not raise errors even though week_start is invalid + self.arrow.floor(frame, week_start=0) + self.arrow.floor(frame, week_start=8) + self.arrow.ceil(frame, week_start=0) + self.arrow.ceil(frame, week_start=8) + def test_span_inclusive_inclusive(self): floor, ceil = self.arrow.span("hour", bounds="[]") From 85cd905332d108303da1cbd0f2336fcae058147f Mon Sep 17 00:00:00 2001 From: Klaus Rettinghaus Date: Sat, 18 Oct 2025 18:48:59 +0200 Subject: [PATCH 66/80] Run tests on next Python release (#1227) * feat: run tests on next Python release * fix: use release candidate first * fix: fix typo * feat: add Python 3.14-dev * fix: use official release * fix: change in previously missed placed --- .github/workflows/continuous_integration.yml | 2 +- Makefile | 3 ++- tox.ini | 1 + 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/continuous_integration.yml b/.github/workflows/continuous_integration.yml index e0776f95..27eb0c3b 100644 --- a/.github/workflows/continuous_integration.yml +++ b/.github/workflows/continuous_integration.yml @@ -15,7 +15,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["pypy-3.9", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] + python-version: ["pypy-3.9", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] os: [ubuntu-latest, macos-latest, windows-latest] exclude: # pypy3 randomly fails on Windows builds diff --git a/Makefile b/Makefile index b1b9f53b..1435ac21 100644 --- a/Makefile +++ b/Makefile @@ -8,8 +8,9 @@ build310: PYTHON_VER = python3.10 build311: PYTHON_VER = python3.11 build312: PYTHON_VER = python3.12 build313: PYTHON_VER = python3.13 +build314: PYTHON_VER = python3.14 -build36 build37 build38 build39 build310 build311 build312 build313: clean +build36 build37 build38 build39 build310 build311 build312 build313 build314: clean $(PYTHON_VER) -m venv venv . venv/bin/activate; \ pip install -U pip setuptools wheel; \ diff --git a/tox.ini b/tox.ini index 3400453f..3e560147 100644 --- a/tox.ini +++ b/tox.ini @@ -12,6 +12,7 @@ python = 3.11: py311 3.12: py312 3.13: py313 + 3.14: py314 [testenv] deps = -r requirements/requirements-tests.txt From c3bb589536b283e3e3886d1d6bc01a6374c8e656 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Sat, 18 Oct 2025 10:24:37 -0700 Subject: [PATCH 67/80] Update pypy CI version and update pre-commit dependencies --- .github/workflows/continuous_integration.yml | 6 +----- .gitignore | 1 + .pre-commit-config.yaml | 14 ++++++-------- arrow/api.py | 15 +++++---------- arrow/factory.py | 12 ++++-------- arrow/parser.py | 15 +++++---------- junit.xml | 1 + pyproject.toml | 1 + tox.ini | 6 +++--- 9 files changed, 27 insertions(+), 44 deletions(-) create mode 100644 junit.xml diff --git a/.github/workflows/continuous_integration.yml b/.github/workflows/continuous_integration.yml index 27eb0c3b..19242214 100644 --- a/.github/workflows/continuous_integration.yml +++ b/.github/workflows/continuous_integration.yml @@ -15,12 +15,8 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["pypy-3.9", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] + python-version: ["pypy-3.11", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] os: [ubuntu-latest, macos-latest, windows-latest] - exclude: - # pypy3 randomly fails on Windows builds - - os: windows-latest - python-version: "pypy-3.9" include: - os: ubuntu-latest path: ~/.cache/pip diff --git a/.gitignore b/.gitignore index 26f42fb3..10273dba 100644 --- a/.gitignore +++ b/.gitignore @@ -50,6 +50,7 @@ htmlcov/ .cache nosetests.xml coverage.xml +junit.xml *.cover .hypothesis/ .pytest_cache/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 942ff760..474517cd 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,7 +2,7 @@ default_language_version: python: python3 repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.5.0 + rev: v6.0.0 hooks: - id: check-ast - id: check-yaml @@ -12,17 +12,15 @@ repos: - id: check-builtin-literals - id: debug-statements - id: end-of-file-fixer - - id: fix-encoding-pragma - args: [--remove] - id: requirements-txt-fixer args: [requirements/requirements.txt, requirements/requirements-docs.txt, requirements/requirements-tests.txt] - id: trailing-whitespace - repo: https://github.com/timothycrosley/isort - rev: 5.13.2 + rev: 7.0.0 hooks: - id: isort - repo: https://github.com/asottile/pyupgrade - rev: v3.16.0 + rev: v3.21.0 hooks: - id: pyupgrade args: [--py36-plus] @@ -38,17 +36,17 @@ repos: - id: rst-inline-touching-normal - id: text-unicode-replacement-char - repo: https://github.com/psf/black - rev: 23.9.1 + rev: 25.9.0 hooks: - id: black args: [--safe, --quiet, --target-version=py36] - repo: https://github.com/pycqa/flake8 - rev: 6.1.0 + rev: 7.3.0 hooks: - id: flake8 additional_dependencies: [flake8-bugbear,flake8-annotations] - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.10.0 + rev: v1.18.2 hooks: - id: mypy additional_dependencies: [types-python-dateutil] diff --git a/arrow/api.py b/arrow/api.py index d8ed24b9..6fd2640d 100644 --- a/arrow/api.py +++ b/arrow/api.py @@ -26,8 +26,7 @@ def get( locale: str = DEFAULT_LOCALE, tzinfo: Optional[TZ_EXPR] = None, normalize_whitespace: bool = False, -) -> Arrow: - ... # pragma: no cover +) -> Arrow: ... # pragma: no cover @overload @@ -36,8 +35,7 @@ def get( locale: str = DEFAULT_LOCALE, tzinfo: Optional[TZ_EXPR] = None, normalize_whitespace: bool = False, -) -> Arrow: - ... # pragma: no cover +) -> Arrow: ... # pragma: no cover @overload @@ -57,8 +55,7 @@ def get( locale: str = DEFAULT_LOCALE, tzinfo: Optional[TZ_EXPR] = None, normalize_whitespace: bool = False, -) -> Arrow: - ... # pragma: no cover +) -> Arrow: ... # pragma: no cover @overload @@ -69,8 +66,7 @@ def get( locale: str = DEFAULT_LOCALE, tzinfo: Optional[TZ_EXPR] = None, normalize_whitespace: bool = False, -) -> Arrow: - ... # pragma: no cover +) -> Arrow: ... # pragma: no cover @overload @@ -81,8 +77,7 @@ def get( locale: str = DEFAULT_LOCALE, tzinfo: Optional[TZ_EXPR] = None, normalize_whitespace: bool = False, -) -> Arrow: - ... # pragma: no cover +) -> Arrow: ... # pragma: no cover def get(*args: Any, **kwargs: Any) -> Arrow: diff --git a/arrow/factory.py b/arrow/factory.py index 3e138a6a..0913bfe1 100644 --- a/arrow/factory.py +++ b/arrow/factory.py @@ -38,8 +38,7 @@ def get( locale: str = DEFAULT_LOCALE, tzinfo: Optional[TZ_EXPR] = None, normalize_whitespace: bool = False, - ) -> Arrow: - ... # pragma: no cover + ) -> Arrow: ... # pragma: no cover @overload def get( @@ -59,8 +58,7 @@ def get( locale: str = DEFAULT_LOCALE, tzinfo: Optional[TZ_EXPR] = None, normalize_whitespace: bool = False, - ) -> Arrow: - ... # pragma: no cover + ) -> Arrow: ... # pragma: no cover @overload def get( @@ -71,8 +69,7 @@ def get( locale: str = DEFAULT_LOCALE, tzinfo: Optional[TZ_EXPR] = None, normalize_whitespace: bool = False, - ) -> Arrow: - ... # pragma: no cover + ) -> Arrow: ... # pragma: no cover @overload def get( @@ -83,8 +80,7 @@ def get( locale: str = DEFAULT_LOCALE, tzinfo: Optional[TZ_EXPR] = None, normalize_whitespace: bool = False, - ) -> Arrow: - ... # pragma: no cover + ) -> Arrow: ... # pragma: no cover def get(self, *args: Any, **kwargs: Any) -> Arrow: """Returns an :class:`Arrow ` object based on flexible inputs. diff --git a/arrow/parser.py b/arrow/parser.py index 4a623291..46b83388 100644 --- a/arrow/parser.py +++ b/arrow/parser.py @@ -554,8 +554,7 @@ def _parse_token( ], value: Union[str, bytes, SupportsInt, bytearray], parts: _Parts, - ) -> None: - ... # pragma: no cover + ) -> None: ... # pragma: no cover @overload def _parse_token( @@ -563,8 +562,7 @@ def _parse_token( token: Literal["X"], value: Union[str, bytes, SupportsFloat, bytearray], parts: _Parts, - ) -> None: - ... # pragma: no cover + ) -> None: ... # pragma: no cover @overload def _parse_token( @@ -572,8 +570,7 @@ def _parse_token( token: Literal["MMMM", "MMM", "dddd", "ddd", "S"], value: Union[str, bytes, bytearray], parts: _Parts, - ) -> None: - ... # pragma: no cover + ) -> None: ... # pragma: no cover @overload def _parse_token( @@ -581,8 +578,7 @@ def _parse_token( token: Literal["a", "A", "ZZZ", "ZZ", "Z"], value: Union[str, bytes], parts: _Parts, - ) -> None: - ... # pragma: no cover + ) -> None: ... # pragma: no cover @overload def _parse_token( @@ -590,8 +586,7 @@ def _parse_token( token: Literal["W"], value: Tuple[_WEEKDATE_ELEMENT, _WEEKDATE_ELEMENT, Optional[_WEEKDATE_ELEMENT]], parts: _Parts, - ) -> None: - ... # pragma: no cover + ) -> None: ... # pragma: no cover def _parse_token( self, diff --git a/junit.xml b/junit.xml new file mode 100644 index 00000000..12263739 --- /dev/null +++ b/junit.xml @@ -0,0 +1 @@ + diff --git a/pyproject.toml b/pyproject.toml index 71e75010..d7d176a7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,6 +21,7 @@ classifiers = [ "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Operating System :: OS Independent", ] dependencies = [ diff --git a/tox.ini b/tox.ini index 3e560147..77f02052 100644 --- a/tox.ini +++ b/tox.ini @@ -1,11 +1,11 @@ [tox] minversion = 3.18.0 -envlist = py{py3,38,39,310,311,312,313} +envlist = py{py3,38,39,310,311,312,313,314} skip_missing_interpreters = true [gh-actions] python = - pypy-3.7: pypy3 + pypy-3.11: pypy3 3.8: py38 3.9: py39 3.10: py310 @@ -56,4 +56,4 @@ include_trailing_comma = true [flake8] per-file-ignores = arrow/__init__.py:F401,tests/*:ANN001,ANN201 -ignore = E203,E501,W503,ANN101,ANN102,ANN401 +extend-ignore = E203,E501,W503,ANN101,ANN102,ANN401 From c61de9299733da6d4a7e8badabae43684a3f9da2 Mon Sep 17 00:00:00 2001 From: "Kristijan \"Fremen\" Velkovski" Date: Sat, 18 Oct 2025 12:38:11 -0500 Subject: [PATCH 68/80] Bump version and add changelog (#1228) Co-authored-by: Jad Chaar --- CHANGELOG.rst | 25 +++++++++++++++++++++++++ arrow/_version.py | 2 +- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index b5daf6ed..f1329c49 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,31 @@ Changelog ========= +1.4.0 (2025-10-02) +------------------ + +- [ADDED] Added ``week_start`` parameter to ``floor()`` and ``ceil()`` methods. `PR #1222 `_ +- [ADDED] Added ``FORMAT_RFC3339_STRICT`` with a T separator. `PR #1201 `_ +- [ADDED] Added Macedonian in Latin locale support. `PR #1200 `_ +- [ADDED] Added Persian/Farsi locale support. `PR #1190 `_ +- [ADDED] Added week and weeks to Thai locale timeframes. `PR #1218 `_ +- [ADDED] Added weeks to Catalan locale. `PR #1189 `_ +- [ADDED] Added Persian names of months, month-abbreviations and day-abbreviations in Gregorian calendar. `PR #1172 `_ +- [CHANGED] Migrated Arrow to use ZoneInfo for timezones instead of pytz. `PR #1217 `_ +- [FIXED] Fixed humanize month limits. `PR #1224 `_ +- [FIXED] Fixed type hint of ``Arrow.__getattr__``. `PR #1171 `_ +- [FIXED] Fixed spelling and removed poorly used expressions in Korean locale. `PR #1181 `_ +- [FIXED] Updated ``shift()`` method for issue #1145. `PR #1194 `_ +- [FIXED] Improved Greek locale translations (seconds, days, "ago", and month typo). `PR #1184 `_, `PR #1186 `_ +- [FIXED] Addressed ``datetime.utcnow`` deprecation warning. `PR #1182 `_ +- [INTERNAL] Added codecov test results. `PR #1223 `_ +- [INTERNAL] Updated CI dependencies (actions/setup-python, actions/checkout, codecov/codecov-action, actions/cache). +- [INTERNAL] Added docstrings to parser.py. `PR #1010 `_ +- [INTERNAL] Updated Python versions support and bumped CI dependencies. `PR #1177 `_ +- [INTERNAL] Added dependabot for GitHub actions. `PR #1193 `_ +- [INTERNAL] Moved dateutil types to test requirements. `PR #1183 `_ +- [INTERNAL] Added documentation link for ``arrow.format``. `PR #1180 `_ + 1.3.0 (2023-09-30) ------------------ diff --git a/arrow/_version.py b/arrow/_version.py index 67bc602a..3e8d9f94 100644 --- a/arrow/_version.py +++ b/arrow/_version.py @@ -1 +1 @@ -__version__ = "1.3.0" +__version__ = "1.4.0" From 2483bfea4ec8a4da609036aba3ce1b32cb571875 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Sat, 18 Oct 2025 10:38:35 -0700 Subject: [PATCH 69/80] Bump changelog date --- CHANGELOG.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index f1329c49..4324cdf1 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,7 +1,7 @@ Changelog ========= -1.4.0 (2025-10-02) +1.4.0 (2025-10-18) ------------------ - [ADDED] Added ``week_start`` parameter to ``floor()`` and ``ceil()`` methods. `PR #1222 `_ From 2c1f5185ad66bf3b1cf8c0812f4b4fe65ac46656 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Sat, 18 Oct 2025 10:40:28 -0700 Subject: [PATCH 70/80] Bump release CI flow to use Python 3.14 --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ae364d5c..788ed60f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,7 +18,7 @@ jobs: restore-keys: ${{ runner.os }}-pip- - uses: actions/setup-python@v6 with: - python-version: "3.11" + python-version: "3.14" - name: Install dependencies run: | pip install -U pip setuptools wheel From edebda47ff1220112585197124aabb340a49fecc Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Sat, 25 Oct 2025 09:18:35 -0700 Subject: [PATCH 71/80] Remove junit.xml --- junit.xml | 1 - 1 file changed, 1 deletion(-) delete mode 100644 junit.xml diff --git a/junit.xml b/junit.xml deleted file mode 100644 index 12263739..00000000 --- a/junit.xml +++ /dev/null @@ -1 +0,0 @@ - From 6c4e0db81922f23d316d8a5f2d0b192c62766186 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Sun, 14 Dec 2025 21:09:58 -0800 Subject: [PATCH 72/80] Fix linting --- .gitignore | 3 +++ .pre-commit-config.yaml | 7 ++++--- arrow/arrow.py | 6 +++--- arrow/parser.py | 2 +- arrow/util.py | 7 ++----- 5 files changed, 13 insertions(+), 12 deletions(-) diff --git a/.gitignore b/.gitignore index 10273dba..4c8153df 100644 --- a/.gitignore +++ b/.gitignore @@ -214,3 +214,6 @@ $RECYCLE.BIN/ # Claude Code configurations CLAUDE*.md .claude/ + +# Kiro +.kiro/** diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 474517cd..7c6bc401 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,7 +20,7 @@ repos: hooks: - id: isort - repo: https://github.com/asottile/pyupgrade - rev: v3.21.0 + rev: v3.21.2 hooks: - id: pyupgrade args: [--py36-plus] @@ -36,7 +36,7 @@ repos: - id: rst-inline-touching-normal - id: text-unicode-replacement-char - repo: https://github.com/psf/black - rev: 25.9.0 + rev: 25.12.0 hooks: - id: black args: [--safe, --quiet, --target-version=py36] @@ -46,7 +46,8 @@ repos: - id: flake8 additional_dependencies: [flake8-bugbear,flake8-annotations] - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.18.2 + rev: v1.19.0 hooks: - id: mypy + args: [--cache-fine-grained, --show-error-codes] additional_dependencies: [types-python-dateutil] diff --git a/arrow/arrow.py b/arrow/arrow.py index d40927ed..eecf2326 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -7,7 +7,7 @@ import calendar import re import sys -from datetime import date +from datetime import date as dt_date from datetime import datetime as dt_datetime from datetime import time as dt_time from datetime import timedelta, timezone @@ -335,7 +335,7 @@ def fromdatetime(cls, dt: dt_datetime, tzinfo: Optional[TZ_EXPR] = None) -> "Arr ) @classmethod - def fromdate(cls, date: date, tzinfo: Optional[TZ_EXPR] = None) -> "Arrow": + def fromdate(cls, date: dt_date, tzinfo: Optional[TZ_EXPR] = None) -> "Arrow": """Constructs an :class:`Arrow ` object from a ``date`` and optional replacement timezone. All time values are set to 0. @@ -1542,7 +1542,7 @@ def is_between( # datetime methods - def date(self) -> date: + def date(self) -> dt_date: """Returns a ``date`` object with the same year, month and day. Usage:: diff --git a/arrow/parser.py b/arrow/parser.py index 46b83388..fc3774b0 100644 --- a/arrow/parser.py +++ b/arrow/parser.py @@ -26,7 +26,7 @@ try: from zoneinfo import ZoneInfo, ZoneInfoNotFoundError except ImportError: - from backports.zoneinfo import ZoneInfo, ZoneInfoNotFoundError # type: ignore[no-redef] + from backports.zoneinfo import ZoneInfo, ZoneInfoNotFoundError # type: ignore[import-not-found, no-redef] from arrow import locales from arrow.constants import DEFAULT_LOCALE diff --git a/arrow/util.py b/arrow/util.py index f3eaa21c..7171d92c 100644 --- a/arrow/util.py +++ b/arrow/util.py @@ -1,7 +1,7 @@ """Helpful functions used internally within arrow.""" import datetime -from typing import Any, Optional, cast +from typing import Any, Optional from dateutil.rrule import WEEKLY, rrule @@ -39,10 +39,7 @@ def next_weekday( """ if weekday < 0 or weekday > 6: raise ValueError("Weekday must be between 0 (Monday) and 6 (Sunday).") - return cast( - datetime.datetime, - rrule(freq=WEEKLY, dtstart=start_date, byweekday=weekday, count=1)[0], - ) + return rrule(freq=WEEKLY, dtstart=start_date, byweekday=weekday, count=1)[0] def is_timestamp(value: Any) -> bool: From edfff3c095999cdb40d14f9f218cb8d12a621b87 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Sun, 14 Dec 2025 21:21:45 -0800 Subject: [PATCH 73/80] Fix linting (#1239) From 53274f91fea4fad0c496c43ff2c1e64da8abda3d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 14 Dec 2025 21:36:45 -0800 Subject: [PATCH 74/80] Bump actions/checkout from 5 to 6 (#1235) Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v5...v6) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Jad Chaar --- .github/workflows/continuous_integration.yml | 4 ++-- .github/workflows/release.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/continuous_integration.yml b/.github/workflows/continuous_integration.yml index 19242214..0e4b3d54 100644 --- a/.github/workflows/continuous_integration.yml +++ b/.github/workflows/continuous_integration.yml @@ -25,7 +25,7 @@ jobs: - os: windows-latest path: ~\AppData\Local\pip\Cache steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Cache pip uses: actions/cache@v4 with: @@ -56,7 +56,7 @@ jobs: name: Linting runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - uses: actions/cache@v4 with: path: ~/.cache/pip diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 788ed60f..c9ace027 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -10,7 +10,7 @@ jobs: release-to-pypi: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - uses: actions/cache@v4 with: path: ~/.cache/pip From 598b72a3019baf58f29200087addcfc1a35648c9 Mon Sep 17 00:00:00 2001 From: Amirhosein Bolouk Asli <70592529+ABolouk@users.noreply.github.com> Date: Mon, 15 Dec 2025 09:08:12 +0330 Subject: [PATCH 75/80] Add Locale Tests (#1236) * add AfrikaansLocale tests * add AlgeriaTunisiaArabicLocale tests * add AustrianLocale tests * add BasqueLocale tests * add BelarusianLocale tests * add LevantArabicLocale tests * add MauritaniaArabicLocale tests * add MoroccoArabicLocale tests * add RomanshLocale tests * add SlovenianLocale tests * add UkrainianLocale tests * add VietnameseLocale tests * add CatalanLocale tests * remove CatalanLocale tests related to week * remove RomanshLocale tests related to week * remove redundant cast * rename datetime.date for lint error --------- Co-authored-by: Jad Chaar --- tests/test_locales.py | 375 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 375 insertions(+) diff --git a/tests/test_locales.py b/tests/test_locales.py index 37444a02..1a7a0b2e 100644 --- a/tests/test_locales.py +++ b/tests/test_locales.py @@ -3402,3 +3402,378 @@ def test_format_timeframe(self): assert self.locale._format_timeframe("seconds", 3) == "3 δευτερόλεπτα" assert self.locale._format_timeframe("day", 1) == "μία ημέρα" assert self.locale._format_timeframe("days", 6) == "6 ημέρες" + + +@pytest.mark.usefixtures("lang_locale") +class TestAfrikaansLocale: + def test_timeframes(self): + assert self.locale._format_timeframe("now", 0) == "nou" + assert self.locale._format_timeframe("second", 1) == "n sekonde" + assert self.locale._format_timeframe("minute", 1) == "minuut" + assert self.locale._format_timeframe("hour", 1) == "uur" + assert self.locale._format_timeframe("day", 1) == "een dag" + assert self.locale._format_timeframe("month", 1) == "een maand" + assert self.locale._format_timeframe("year", 1) == "een jaar" + + assert self.locale._format_timeframe("seconds", 2) == "2 sekondes" + assert self.locale._format_timeframe("minutes", 2) == "2 minute" + assert self.locale._format_timeframe("hours", 2) == "2 ure" + assert self.locale._format_timeframe("days", 2) == "2 dae" + assert self.locale._format_timeframe("months", 2) == "2 maande" + assert self.locale._format_timeframe("years", 2) == "2 jaar" + + assert self.locale._format_timeframe("seconds", 5) == "5 sekondes" + assert self.locale._format_timeframe("minutes", 5) == "5 minute" + assert self.locale._format_timeframe("hours", 5) == "5 ure" + assert self.locale._format_timeframe("days", 5) == "5 dae" + assert self.locale._format_timeframe("months", 5) == "5 maande" + assert self.locale._format_timeframe("years", 5) == "5 jaar" + + def test_weekday(self): + dt = arrow.Arrow(2015, 4, 11, 17, 30, 00) + assert self.locale.day_name(dt.isoweekday()) == "Saterdag" + assert self.locale.day_abbreviation(dt.isoweekday()) == "Za" + + assert self.locale.month_name(dt.month) == "April" + assert self.locale.month_abbreviation(dt.month) == "Apr" + + def test_format_relative_past(self): + assert self.locale._format_relative("uur", "hour", -1) == "uur gelede" + + def test_format_relative_future(self): + assert self.locale._format_relative("uur", "hour", 1) == "in uur" + + +@pytest.mark.usefixtures("lang_locale") +class TestAlgeriaTunisiaArabicLocale: + def test_weekday(self): + dt = arrow.Arrow(2015, 4, 11, 17, 30, 00) + assert self.locale.day_name(dt.isoweekday()) == "السبت" + assert self.locale.day_abbreviation(dt.isoweekday()) == "سبت" + + assert self.locale.month_name(dt.month) == "أفريل" + assert self.locale.month_abbreviation(dt.month) == "أفريل" + + def test_format_relative_past(self): + assert self.locale._format_relative("ساعة", "hour", -1) == "منذ ساعة" + + def test_format_relative_future(self): + assert self.locale._format_relative("ساعة", "hour", 1) == "خلال ساعة" + + +@pytest.mark.usefixtures("lang_locale") +class TestAustrianLocale: + def test_weekday(self): + dt = arrow.Arrow(2015, 1, 11, 17, 30, 00) + assert self.locale.day_name(dt.isoweekday()) == "Sonntag" + assert self.locale.day_abbreviation(dt.isoweekday()) == "So" + + assert self.locale.month_name(dt.month) == "Jänner" + assert self.locale.month_abbreviation(dt.month) == "Jan" + + +@pytest.mark.usefixtures("lang_locale") +class TestBasqueLocale: + def test_timeframes(self): + assert self.locale._format_timeframe("now", 0) == "Orain" + assert self.locale._format_timeframe("second", 1) == "segundo bat" + assert self.locale._format_timeframe("minute", 1) == "minutu bat" + assert self.locale._format_timeframe("hour", 1) == "ordu bat" + assert self.locale._format_timeframe("day", 1) == "egun bat" + assert self.locale._format_timeframe("month", 1) == "hilabete bat" + assert self.locale._format_timeframe("year", 1) == "urte bat" + + assert self.locale._format_timeframe("seconds", 2) == "2 segundu" + assert self.locale._format_timeframe("minutes", 2) == "2 minutu" + assert self.locale._format_timeframe("hours", 2) == "2 ordu" + assert self.locale._format_timeframe("days", 2) == "2 egun" + assert self.locale._format_timeframe("months", 2) == "2 hilabet" + assert self.locale._format_timeframe("years", 2) == "2 urte" + + assert self.locale._format_timeframe("seconds", 5) == "5 segundu" + assert self.locale._format_timeframe("minutes", 5) == "5 minutu" + assert self.locale._format_timeframe("hours", 5) == "5 ordu" + assert self.locale._format_timeframe("days", 5) == "5 egun" + assert self.locale._format_timeframe("months", 5) == "5 hilabet" + assert self.locale._format_timeframe("years", 5) == "5 urte" + + def test_weekday(self): + dt = arrow.Arrow(2015, 4, 11, 17, 30, 00) + assert self.locale.day_name(dt.isoweekday()) == "larunbata" + assert self.locale.day_abbreviation(dt.isoweekday()) == "lr" + + assert self.locale.month_name(dt.month) == "apirilak" + assert self.locale.month_abbreviation(dt.month) == "api" + + def test_format_relative_past(self): + assert self.locale._format_relative("ordu bat", "hour", -1) == "duela ordu bat" + + def test_format_relative_future(self): + assert self.locale._format_relative("ordu bat", "hour", 1) == "ordu bat" + + +@pytest.mark.usefixtures("lang_locale") +class TestBelarusianLocale: + def test_timeframes(self): + assert self.locale._format_timeframe("now", 0) == "зараз" + assert self.locale._format_timeframe("second", 1) == "секунду" + assert self.locale._format_timeframe("minute", 1) == "хвіліну" + assert self.locale._format_timeframe("hour", 1) == "гадзіну" + assert self.locale._format_timeframe("day", 1) == "дзень" + assert self.locale._format_timeframe("month", 1) == "месяц" + assert self.locale._format_timeframe("year", 1) == "год" + + assert self.locale._format_timeframe("seconds", 2) == "2 некалькі секунд" + assert self.locale._format_timeframe("minutes", 2) == "2 хвіліны" + assert self.locale._format_timeframe("hours", 2) == "2 гадзіны" + assert self.locale._format_timeframe("days", 2) == "2 дні" + assert self.locale._format_timeframe("months", 2) == "2 месяцы" + assert self.locale._format_timeframe("years", 2) == "2 гады" + + assert self.locale._format_timeframe("seconds", 5) == "5 некалькі секунд" + assert self.locale._format_timeframe("minutes", 5) == "5 хвілін" + assert self.locale._format_timeframe("hours", 5) == "5 гадзін" + assert self.locale._format_timeframe("days", 5) == "5 дзён" + assert self.locale._format_timeframe("months", 5) == "5 месяцаў" + assert self.locale._format_timeframe("years", 5) == "5 гадоў" + + def test_weekday(self): + dt = arrow.Arrow(2015, 4, 11, 17, 30, 00) + assert self.locale.day_name(dt.isoweekday()) == "субота" + assert self.locale.day_abbreviation(dt.isoweekday()) == "сб" + + assert self.locale.month_name(dt.month) == "красавіка" + assert self.locale.month_abbreviation(dt.month) == "крас" + + def test_format_relative_past(self): + assert self.locale._format_relative("гадзіну", "hour", -1) == "гадзіну таму" + + def test_format_relative_future(self): + assert self.locale._format_relative("гадзіну", "hour", 1) == "праз гадзіну" + + +@pytest.mark.usefixtures("lang_locale") +class TestLevantArabicLocale: + def test_weekday(self): + dt = arrow.Arrow(2015, 4, 11, 17, 30, 00) + assert self.locale.month_name(dt.month) == "نيسان" + assert self.locale.month_abbreviation(dt.month) == "نيسان" + + +@pytest.mark.usefixtures("lang_locale") +class TestMauritaniaArabicLocale: + def test_weekday(self): + dt = arrow.Arrow(2015, 4, 11, 17, 30, 00) + assert self.locale.month_name(dt.month) == "إبريل" + assert self.locale.month_abbreviation(dt.month) == "إبريل" + + +@pytest.mark.usefixtures("lang_locale") +class TestMoroccoArabicLocale: + def test_weekday(self): + dt = arrow.Arrow(2015, 4, 11, 17, 30, 00) + assert self.locale.month_name(dt.month) == "أبريل" + assert self.locale.month_abbreviation(dt.month) == "أبريل" + + +@pytest.mark.usefixtures("lang_locale") +class TestRomanshLocale: + def test_timeframes(self): + assert self.locale._format_timeframe("now", 0) == "en quest mument" + assert self.locale._format_timeframe("second", 1) == "in secunda" + assert self.locale._format_timeframe("minute", 1) == "ina minuta" + assert self.locale._format_timeframe("hour", 1) == "in'ura" + assert self.locale._format_timeframe("day", 1) == "in di" + assert self.locale._format_timeframe("month", 1) == "in mais" + assert self.locale._format_timeframe("year", 1) == "in onn" + + assert self.locale._format_timeframe("seconds", 2) == "2 secundas" + assert self.locale._format_timeframe("minutes", 2) == "2 minutas" + assert self.locale._format_timeframe("hours", 2) == "2 ura" + assert self.locale._format_timeframe("days", 2) == "2 dis" + assert self.locale._format_timeframe("months", 2) == "2 mais" + assert self.locale._format_timeframe("years", 2) == "2 onns" + + assert self.locale._format_timeframe("seconds", 5) == "5 secundas" + assert self.locale._format_timeframe("minutes", 5) == "5 minutas" + assert self.locale._format_timeframe("hours", 5) == "5 ura" + assert self.locale._format_timeframe("days", 5) == "5 dis" + assert self.locale._format_timeframe("months", 5) == "5 mais" + assert self.locale._format_timeframe("years", 5) == "5 onns" + + def test_weekday(self): + dt = arrow.Arrow(2015, 4, 11, 17, 30, 00) + assert self.locale.day_name(dt.isoweekday()) == "sonda" + assert self.locale.day_abbreviation(dt.isoweekday()) == "so" + + assert self.locale.month_name(dt.month) == "avrigl" + assert self.locale.month_abbreviation(dt.month) == "avr" + + def test_format_relative_past(self): + assert self.locale._format_relative("in'ura", "hour", -1) == "avant in'ura" + + def test_format_relative_future(self): + assert self.locale._format_relative("in'ura", "hour", 1) == "en in'ura" + + +@pytest.mark.usefixtures("lang_locale") +class TestSlovenianLocale: + def test_timeframes(self): + assert self.locale._format_timeframe("now", 0) == "zdaj" + assert self.locale._format_timeframe("second", 1) == "sekundo" + assert self.locale._format_timeframe("minute", 1) == "minuta" + assert self.locale._format_timeframe("hour", 1) == "uro" + assert self.locale._format_timeframe("day", 1) == "dan" + assert self.locale._format_timeframe("month", 1) == "mesec" + assert self.locale._format_timeframe("year", 1) == "leto" + + assert self.locale._format_timeframe("seconds", 2) == "2 sekund" + assert self.locale._format_timeframe("minutes", 2) == "2 minutami" + assert self.locale._format_timeframe("hours", 2) == "2 ur" + assert self.locale._format_timeframe("days", 2) == "2 dni" + assert self.locale._format_timeframe("months", 2) == "2 mesecev" + assert self.locale._format_timeframe("years", 2) == "2 let" + + assert self.locale._format_timeframe("seconds", 5) == "5 sekund" + assert self.locale._format_timeframe("minutes", 5) == "5 minutami" + assert self.locale._format_timeframe("hours", 5) == "5 ur" + assert self.locale._format_timeframe("days", 5) == "5 dni" + assert self.locale._format_timeframe("months", 5) == "5 mesecev" + assert self.locale._format_timeframe("years", 5) == "5 let" + + def test_weekday(self): + dt = arrow.Arrow(2015, 4, 11, 17, 30, 00) + assert self.locale.day_name(dt.isoweekday()) == "Sobota" + assert self.locale.day_abbreviation(dt.isoweekday()) == "Sob" + + assert self.locale.month_name(dt.month) == "April" + assert self.locale.month_abbreviation(dt.month) == "Apr" + + def test_format_relative_past(self): + assert self.locale._format_relative("in'ura", "hour", -1) == "pred in'ura" + + def test_format_relative_future(self): + assert self.locale._format_relative("in'ura", "hour", 1) == "čez in'ura" + + +@pytest.mark.usefixtures("lang_locale") +class TestUkrainianLocale: + def test_timeframes(self): + assert self.locale._format_timeframe("now", 0) == "зараз" + assert self.locale._format_timeframe("second", 1) == "секунда" + assert self.locale._format_timeframe("minute", 1) == "хвилину" + assert self.locale._format_timeframe("hour", 1) == "годину" + assert self.locale._format_timeframe("day", 1) == "день" + assert self.locale._format_timeframe("month", 1) == "місяць" + assert self.locale._format_timeframe("year", 1) == "рік" + + assert self.locale._format_timeframe("seconds", 2) == "2 кілька секунд" + assert self.locale._format_timeframe("minutes", 2) == "2 хвилини" + assert self.locale._format_timeframe("hours", 2) == "2 години" + assert self.locale._format_timeframe("days", 2) == "2 дні" + assert self.locale._format_timeframe("months", 2) == "2 місяці" + assert self.locale._format_timeframe("years", 2) == "2 роки" + + assert self.locale._format_timeframe("seconds", 5) == "5 кілька секунд" + assert self.locale._format_timeframe("minutes", 5) == "5 хвилин" + assert self.locale._format_timeframe("hours", 5) == "5 годин" + assert self.locale._format_timeframe("days", 5) == "5 днів" + assert self.locale._format_timeframe("months", 5) == "5 місяців" + assert self.locale._format_timeframe("years", 5) == "5 років" + + def test_weekday(self): + dt = arrow.Arrow(2015, 4, 11, 17, 30, 00) + assert self.locale.day_name(dt.isoweekday()) == "субота" + assert self.locale.day_abbreviation(dt.isoweekday()) == "сб" + + assert self.locale.month_name(dt.month) == "квітня" + assert self.locale.month_abbreviation(dt.month) == "квіт" + + def test_format_relative_past(self): + assert self.locale._format_relative("годину", "hour", -1) == "годину тому" + + def test_format_relative_future(self): + assert self.locale._format_relative("годину", "hour", 1) == "за годину" + + +@pytest.mark.usefixtures("lang_locale") +class TestVietnameseLocale: + def test_timeframes(self): + assert self.locale._format_timeframe("now", 0) == "hiện tại" + assert self.locale._format_timeframe("second", 1) == "một giây" + assert self.locale._format_timeframe("minute", 1) == "một phút" + assert self.locale._format_timeframe("hour", 1) == "một giờ" + assert self.locale._format_timeframe("day", 1) == "một ngày" + assert self.locale._format_timeframe("week", 1) == "một tuần" + assert self.locale._format_timeframe("month", 1) == "một tháng" + assert self.locale._format_timeframe("year", 1) == "một năm" + + assert self.locale._format_timeframe("seconds", 2) == "2 giây" + assert self.locale._format_timeframe("minutes", 2) == "2 phút" + assert self.locale._format_timeframe("hours", 2) == "2 giờ" + assert self.locale._format_timeframe("days", 2) == "2 ngày" + assert self.locale._format_timeframe("weeks", 2) == "2 tuần" + assert self.locale._format_timeframe("months", 2) == "2 tháng" + assert self.locale._format_timeframe("years", 2) == "2 năm" + + assert self.locale._format_timeframe("seconds", 5) == "5 giây" + assert self.locale._format_timeframe("minutes", 5) == "5 phút" + assert self.locale._format_timeframe("hours", 5) == "5 giờ" + assert self.locale._format_timeframe("days", 5) == "5 ngày" + assert self.locale._format_timeframe("weeks", 5) == "5 tuần" + assert self.locale._format_timeframe("months", 5) == "5 tháng" + assert self.locale._format_timeframe("years", 5) == "5 năm" + + def test_weekday(self): + dt = arrow.Arrow(2015, 4, 11, 17, 30, 00) + assert self.locale.day_name(dt.isoweekday()) == "Thứ Bảy" + assert self.locale.day_abbreviation(dt.isoweekday()) == "Thứ 7" + + assert self.locale.month_name(dt.month) == "Tháng Tư" + assert self.locale.month_abbreviation(dt.month) == "Tháng 4" + + def test_format_relative_past(self): + assert self.locale._format_relative("một giờ", "hour", -1) == "một giờ trước" + + def test_format_relative_future(self): + assert self.locale._format_relative("một giờ", "hour", 1) == "một giờ nữa" + + +@pytest.mark.usefixtures("lang_locale") +class TestCatalanLocale: + def test_timeframes(self): + assert self.locale._format_timeframe("now", 0) == "Ara mateix" + assert self.locale._format_timeframe("second", 1) == "un segon" + assert self.locale._format_timeframe("minute", 1) == "un minut" + assert self.locale._format_timeframe("hour", 1) == "una hora" + assert self.locale._format_timeframe("day", 1) == "un dia" + assert self.locale._format_timeframe("month", 1) == "un mes" + assert self.locale._format_timeframe("year", 1) == "un any" + + assert self.locale._format_timeframe("seconds", 2) == "2 segons" + assert self.locale._format_timeframe("minutes", 2) == "2 minuts" + assert self.locale._format_timeframe("hours", 2) == "2 hores" + assert self.locale._format_timeframe("days", 2) == "2 dies" + assert self.locale._format_timeframe("months", 2) == "2 mesos" + assert self.locale._format_timeframe("years", 2) == "2 anys" + + assert self.locale._format_timeframe("seconds", 5) == "5 segons" + assert self.locale._format_timeframe("minutes", 5) == "5 minuts" + assert self.locale._format_timeframe("hours", 5) == "5 hores" + assert self.locale._format_timeframe("days", 5) == "5 dies" + assert self.locale._format_timeframe("months", 5) == "5 mesos" + assert self.locale._format_timeframe("years", 5) == "5 anys" + + def test_weekday(self): + dt = arrow.Arrow(2015, 4, 11, 17, 30, 00) + assert self.locale.day_name(dt.isoweekday()) == "dissabte" + assert self.locale.day_abbreviation(dt.isoweekday()) == "ds." + + assert self.locale.month_name(dt.month) == "abril" + assert self.locale.month_abbreviation(dt.month) == "abr." + + def test_format_relative_past(self): + assert self.locale._format_relative("una hora", "hour", -1) == "Fa una hora" + + def test_format_relative_future(self): + assert self.locale._format_relative("una hora", "hour", 1) == "En una hora" From 9da15b8fd2159c3b5c74ed6c505c22a6ec062332 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 14 Dec 2025 21:43:28 -0800 Subject: [PATCH 76/80] Bump actions/cache from 4 to 5 (#1238) Bumps [actions/cache](https://github.com/actions/cache) from 4 to 5. - [Release notes](https://github.com/actions/cache/releases) - [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md) - [Commits](https://github.com/actions/cache/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/cache dependency-version: '5' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Jad Chaar --- .github/workflows/continuous_integration.yml | 6 +++--- .github/workflows/release.yml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/continuous_integration.yml b/.github/workflows/continuous_integration.yml index 0e4b3d54..e335b71d 100644 --- a/.github/workflows/continuous_integration.yml +++ b/.github/workflows/continuous_integration.yml @@ -27,7 +27,7 @@ jobs: steps: - uses: actions/checkout@v6 - name: Cache pip - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: ${{ matrix.path }} key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements*.txt') }} @@ -57,12 +57,12 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - - uses: actions/cache@v4 + - uses: actions/cache@v5 with: path: ~/.cache/pip key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements*.txt') }} restore-keys: ${{ runner.os }}-pip- - - uses: actions/cache@v4 + - uses: actions/cache@v5 with: path: ~/.cache/pre-commit key: ${{ runner.os }}-pre-commit-${{ hashFiles('**/.pre-commit-config.yaml') }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c9ace027..07cbd63a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - - uses: actions/cache@v4 + - uses: actions/cache@v5 with: path: ~/.cache/pip key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements*.txt') }} From b247ad073d4c350270528edb5afff8e7f4f7d9e4 Mon Sep 17 00:00:00 2001 From: Jason Mitchell Date: Mon, 15 Dec 2025 20:41:26 -0800 Subject: [PATCH 77/80] Added weeks to afrikaans locale (#1234) * Added weeks to afrikaans locale * Afrikaans tests * Removed junit.xml changes * Fix linting from merge conflic --------- Co-authored-by: Jad Chaar --- arrow/locales.py | 2 ++ tests/test_locales.py | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/arrow/locales.py b/arrow/locales.py index 5d31aa24..757df480 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -1619,6 +1619,8 @@ class AfrikaansLocale(Locale): "hours": "{0} ure", "day": "een dag", "days": "{0} dae", + "week": "een week", + "weeks": "{0} weke", "month": "een maand", "months": "{0} maande", "year": "een jaar", diff --git a/tests/test_locales.py b/tests/test_locales.py index 1a7a0b2e..3d7120e3 100644 --- a/tests/test_locales.py +++ b/tests/test_locales.py @@ -3408,17 +3408,22 @@ def test_format_timeframe(self): class TestAfrikaansLocale: def test_timeframes(self): assert self.locale._format_timeframe("now", 0) == "nou" + + # singular assert self.locale._format_timeframe("second", 1) == "n sekonde" assert self.locale._format_timeframe("minute", 1) == "minuut" assert self.locale._format_timeframe("hour", 1) == "uur" assert self.locale._format_timeframe("day", 1) == "een dag" + assert self.locale._format_timeframe("week", 1) == "een week" assert self.locale._format_timeframe("month", 1) == "een maand" assert self.locale._format_timeframe("year", 1) == "een jaar" + # plural assert self.locale._format_timeframe("seconds", 2) == "2 sekondes" assert self.locale._format_timeframe("minutes", 2) == "2 minute" assert self.locale._format_timeframe("hours", 2) == "2 ure" assert self.locale._format_timeframe("days", 2) == "2 dae" + assert self.locale._format_timeframe("weeks", 2) == "2 weke" assert self.locale._format_timeframe("months", 2) == "2 maande" assert self.locale._format_timeframe("years", 2) == "2 jaar" From 67ca577fb248ce9d803a9a85584b90a77ca989f0 Mon Sep 17 00:00:00 2001 From: "Kristijan \"Fremen\" Velkovski" Date: Wed, 18 Feb 2026 21:17:06 -0600 Subject: [PATCH 78/80] chore: add codecov_token coverage (#1246) --- .github/workflows/continuous_integration.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/continuous_integration.yml b/.github/workflows/continuous_integration.yml index e335b71d..3b9c3804 100644 --- a/.github/workflows/continuous_integration.yml +++ b/.github/workflows/continuous_integration.yml @@ -43,9 +43,11 @@ jobs: - name: Test with tox run: tox - name: Upload coverage to Codecov + if: ${{ success() }} uses: codecov/codecov-action@v5 with: - file: coverage.xml + files: coverage.xml + token: ${{ secrets.CODECOV_TOKEN }} - name: Upload test results to Codecov if: ${{ !cancelled() }} uses: codecov/test-results-action@v1 From b423717da81aaf8117313b4b377efaa6413a9639 Mon Sep 17 00:00:00 2001 From: Varun Chawla <34209028+veeceey@users.noreply.github.com> Date: Thu, 19 Feb 2026 07:24:45 -0800 Subject: [PATCH 79/80] Fix humanize reporting "a month" for 16-day differences (#1240) (#1242) The humanize method incorrectly reported differences of ~15-30 days within the same calendar month as "a month" instead of weeks. This was caused by two issues: 1. The `calendar_diff.days > 14` rounding unconditionally added a month even when no calendar month boundary had been crossed (calendar_months was 0). Now this rounding only applies when at least one calendar month has already elapsed. 2. The weeks/week check came after the months check, so it was unreachable for within-month differences that got incorrectly promoted. The weeks checks now come before months, guarded by `calendar_months < 1` so that true calendar month differences (e.g., Feb 8 to Mar 8 = 28 days) still correctly show as "a month". Co-authored-by: Claude Opus 4.6 Co-authored-by: Kristijan "Fremen" Velkovski --- arrow/arrow.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/arrow/arrow.py b/arrow/arrow.py index eecf2326..f0a57f04 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -1223,8 +1223,10 @@ def humanize( calendar_diff.years * self._MONTHS_PER_YEAR + calendar_diff.months ) - # For months, if more than 2 weeks, count as a full month - if calendar_diff.days > 14: + # Round up partial months when already at least one + # calendar month has elapsed and the remaining days + # are more than halfway through another month. + if calendar_months >= 1 and calendar_diff.days > 14: calendar_months += 1 calendar_months = min(calendar_months, self._MONTHS_PER_YEAR) @@ -1236,6 +1238,13 @@ def humanize( days = sign * max(delta_second // self._SECS_PER_DAY, 2) return locale.describe("days", days, only_distance=only_distance) + elif diff < self._SECS_PER_WEEK * 2 and calendar_months < 1: + return locale.describe("week", sign, only_distance=only_distance) + + elif calendar_months < 1 and diff < self._SECS_PER_MONTH: + weeks = sign * max(delta_second // self._SECS_PER_WEEK, 2) + return locale.describe("weeks", weeks, only_distance=only_distance) + elif calendar_months >= 1 and diff < self._SECS_PER_YEAR: if calendar_months == 1: return locale.describe( @@ -1247,13 +1256,6 @@ def humanize( "months", months, only_distance=only_distance ) - elif diff < self._SECS_PER_WEEK * 2: - return locale.describe("week", sign, only_distance=only_distance) - - elif diff < self._SECS_PER_MONTH: - weeks = sign * max(delta_second // self._SECS_PER_WEEK, 2) - return locale.describe("weeks", weeks, only_distance=only_distance) - elif diff < self._SECS_PER_YEAR * 2: return locale.describe("year", sign, only_distance=only_distance) From 2224255c4acc594d734cef0bbc83360452a67983 Mon Sep 17 00:00:00 2001 From: "Kristijan \"Fremen\" Velkovski" Date: Thu, 30 Apr 2026 03:13:11 -0500 Subject: [PATCH 80/80] Revert "Fix humanize reporting "a month" for 16-day differences (#1240) (#1242)" (#1264) This reverts commit b423717da81aaf8117313b4b377efaa6413a9639. --- arrow/arrow.py | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/arrow/arrow.py b/arrow/arrow.py index f0a57f04..eecf2326 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -1223,10 +1223,8 @@ def humanize( calendar_diff.years * self._MONTHS_PER_YEAR + calendar_diff.months ) - # Round up partial months when already at least one - # calendar month has elapsed and the remaining days - # are more than halfway through another month. - if calendar_months >= 1 and calendar_diff.days > 14: + # For months, if more than 2 weeks, count as a full month + if calendar_diff.days > 14: calendar_months += 1 calendar_months = min(calendar_months, self._MONTHS_PER_YEAR) @@ -1238,13 +1236,6 @@ def humanize( days = sign * max(delta_second // self._SECS_PER_DAY, 2) return locale.describe("days", days, only_distance=only_distance) - elif diff < self._SECS_PER_WEEK * 2 and calendar_months < 1: - return locale.describe("week", sign, only_distance=only_distance) - - elif calendar_months < 1 and diff < self._SECS_PER_MONTH: - weeks = sign * max(delta_second // self._SECS_PER_WEEK, 2) - return locale.describe("weeks", weeks, only_distance=only_distance) - elif calendar_months >= 1 and diff < self._SECS_PER_YEAR: if calendar_months == 1: return locale.describe( @@ -1256,6 +1247,13 @@ def humanize( "months", months, only_distance=only_distance ) + elif diff < self._SECS_PER_WEEK * 2: + return locale.describe("week", sign, only_distance=only_distance) + + elif diff < self._SECS_PER_MONTH: + weeks = sign * max(delta_second // self._SECS_PER_WEEK, 2) + return locale.describe("weeks", weeks, only_distance=only_distance) + elif diff < self._SECS_PER_YEAR * 2: return locale.describe("year", sign, only_distance=only_distance)